From 19adcca1a89405a47da57fee64fe5fde4720c16a Mon Sep 17 00:00:00 2001 From: bobzel Date: Mon, 5 May 2025 14:05:32 -0400 Subject: got rid of dash _xPadding/_yPadding in favor of just using _xMargin/_yMargin. cleaned up linearView field names --- src/client/util/CurrentUserUtils.ts | 52 ++++++++++++++++++------------------- src/client/util/LinkManager.ts | 4 +-- 2 files changed, 28 insertions(+), 28 deletions(-) (limited to 'src/client/util') diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 8f4d568ab..d460cd0d7 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -52,7 +52,7 @@ export interface Button { numBtnMax?: number; switchToggle?: boolean; width?: number; - linearBtnWidth?: number; + linearView_btnWidth?: number; toolType?: string; // type of pen tool expertMode?: boolean;// available only in expert mode btnList?: List; @@ -205,7 +205,7 @@ export class CurrentUserUtils { const templateIconsDoc = DocUtils.AssignOpts(DocCast(doc[field]), reqdOpts) ?? (doc[field] = Docs.Create.TreeDocument([], reqdOpts)); const labelBox = (opts: DocumentOptions, fieldKey:string) => Docs.Create.LabelDocument({ - layout: LabelBox.LayoutString(fieldKey), letterSpacing: "unset", _label_minFontSize: 14, _label_maxFontSize: 14, layout_borderRounding: "5px", _width: 150, _height: 70, _xPadding: 10, _yPadding: 10, ...opts + layout: LabelBox.LayoutString(fieldKey), letterSpacing: "unset", _label_minFontSize: 14, _label_maxFontSize: 14, layout_borderRounding: "5px", _width: 150, _height: 70, _xMargin: 10, _yMargin: 10, ...opts }); const imageBox = (opts: DocumentOptions, fieldKey:string) => Docs.Create.ImageDocument( "http://www.cs.brown.edu/~bcz/noImage.png", { layout:ImageBox.LayoutString(fieldKey), "icon_nativeWidth": 360 / 4, "icon_nativeHeight": 270 / 4, iconTemplate:DocumentType.IMG, _width: 360 / 4, _height: 270 / 4, _layout_showTitle: "title", ...opts }); const fontBox = (opts:DocumentOptions, fieldKey:string) => Docs.Create.FontIconDocument({ layout:FontIconBox.LayoutString(fieldKey), _nativeHeight: 30, _nativeWidth: 30, _width: 30, _height: 30, ...opts }); @@ -402,7 +402,7 @@ pie title Minerals in my tap water {key: "Map", creator: opts => Docs.Create.MapDocument([], opts), opts: { _width: 800, _height: 600, _layout_fitWidth: true, }}, {key: "Screengrab", creator: Docs.Create.ScreenshotDocument, opts: { _width: 400, _height: 200 }}, {key: "WebCam", creator: opts => Docs.Create.WebCamDocument("", opts), opts: { _width: 400, _height: 200, recording:true, isSystem: true, cloneFieldFilter: new List(["isSystem"]) }}, - {key: "Button", creator: Docs.Create.ButtonDocument, opts: { _width: 150, _height: 50, _xPadding: 10, _yPadding: 10, title_custom: true, waitForDoubleClickToClick: 'never'}, scripts: {onClick: FollowLinkScript()?.script.originalScript ?? ""}}, + {key: "Button", creator: Docs.Create.ButtonDocument, opts: { _width: 150, _height: 50, _xMargin: 10, _yMargin: 10, title_custom: true, waitForDoubleClickToClick: 'never'}, scripts: {onClick: FollowLinkScript()?.script.originalScript ?? ""}}, {key: "Script", creator: opts => Docs.Create.ScriptingDocument(null, opts), opts: { _width: 200, _height: 250, }}, {key: "DataViz", creator: opts => Docs.Create.DataVizDocument("", opts), opts: { _width: 300, _height: 300, }}, // AARAV ADD // @@ -663,7 +663,7 @@ pie title Minerals in my tap water } static linearButtonList = (opts: DocumentOptions, docs: Doc[]) => Docs.Create.LinearDocument(docs, { - ...opts, _gridGap: 0, _xMargin: 5, _yMargin: 5, layout_boxShadow: "0 0", _forceActive: true, + ...opts, _gridGap: 0, _xMargin: 5, _yMargin: 5, layout_boxShadow: "0 0", _forceActive: true, linearView: true, dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }), _lockedPosition: true, isSystem: true, flexDirection: "row" }) @@ -697,7 +697,7 @@ pie title Minerals in my tap water const btns = btnDescs.map(desc => dockBtn({_width: 30, _height: 30, defaultDoubleClick: 'ignore', undoIgnoreFields: new List(['opacity']), _dragOnlyWithinContainer: true, ...desc.opts}, desc.scripts)); const dockBtnsReqdOpts:DocumentOptions = { title: "docked buttons", _height: 40, flexGap: 0, layout_boxShadow: "standard", childDragAction: dropActionType.move, - childDontRegisterViews: true, linearView_IsOpen: true, linearView_Expandable: true, ignoreClick: true + childDontRegisterViews: true, linearView_isOpen: true, linearView_expandable: true, ignoreClick: true }; reaction(() => UndoManager.redoStack.slice(), () => { Doc.GetProto(btns.find(btn => btn.title === "Redo")!).opacity = UndoManager.CanRedo() ? 1 : 0.4; }, { fireImmediately: true }); reaction(() => UndoManager.undoStack.slice(), () => { Doc.GetProto(btns.find(btn => btn.title === "Undo")!).opacity = UndoManager.CanUndo() ? 1 : 0.4; }, { fireImmediately: true }); @@ -789,24 +789,24 @@ 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, showUntilToggle: true, 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_);}' }}, { title: "Write", toolTip: "Write (Ctrl+Shift+P)", btnType: ButtonType.ToggleButton, icon: "pen", toolType: InkInkTool.Write, ignoreClick: true, scripts: {onClick:'{ return setActiveTool(this.toolType, true, _readOnly_);}' }, funcs: {hidden:"IsNoviceMode()" }}, ]}, - { 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: "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, linearView_btnWidth: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, showUntilToggle: true, 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_);}'}}, { title: "Area", toolTip: "Erase like a pencil", btnType: ButtonType.ToggleButton, icon: "circle-xmark",toolType:InkEraserTool.Radius, ignoreClick: true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'}}, ]}, - { title: " Size", toolTip: "Size of area pencil eraser", btnType: ButtonType.NumberSliderButton, toolType: InkProperty.EraserWidth,ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, funcs: {hidden:"NotRadiusEraser()"}, numBtnMin: 1, linearBtnWidth:40}, + { title: " Size", toolTip: "Size of area pencil eraser", btnType: ButtonType.NumberSliderButton, toolType: InkProperty.EraserWidth,ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, funcs: {hidden:"NotRadiusEraser()"}, numBtnMin: 1, linearView_btnWidth:40}, { title: "Mask", toolTip: "Make Stroke a Stencil Mask", btnType: ButtonType.ToggleButton, icon: "user-circle", toolType: InkProperty.Mask, scripts: {onClick:'{ return setInkProperty(this.toolType, value, _readOnly_);}'}, funcs: {hidden:"IsNoviceMode()" } }, { title: "Labels", toolTip: "Show Labels Inside Shapes", btnType: ButtonType.ToggleButton, icon: "text-width", toolType: InkProperty.Labels, scripts: {onClick:'{ return setInkProperty(this.toolType, value, _readOnly_);}'}}, - { title: "Smart Draw", toolTip: "Draw with AI", btnType: ButtonType.ToggleButton, icon: "user-pen", toolType: InkTool.SmartDraw, scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}'}, funcs: {hidden: "IsNoviceMode()"}}, + { title: "Smart Draw", toolTip: "Draw with AI", btnType: ButtonType.ToggleButton, icon: "user-pen", toolType: InkTool.SmartDraw, scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}'}, funcs: {hidden: "IsNoviceMode()"}}, ]; } @@ -843,7 +843,7 @@ pie title Minerals in my tap water { 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: "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: "B.Width", toolTip: "Border width", btnType: ButtonType.NumberSliderButton, ignoreClick: true, scripts: {script: '{ return setBorderWidth(value, _readOnly_);}'}, numBtnMin: 0, linearView_btnWidth: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_)'}}, { 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_);}'}}, @@ -851,17 +851,17 @@ pie title Minerals in my tap water { 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", 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: "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 + { 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)`}, linearView_btnWidth:58 }, // Only when Schema is selected ]; } @@ -872,7 +872,7 @@ pie title Minerals in my tap water ...OmitKeys(params, ["scripts", "funcs", "subMenu"]).omit, color: Colors.WHITE, _nativeWidth: params.width ?? 30, _width: params.width ?? 30, - _height: 30, _nativeHeight: 30, linearBtnWidth: params.linearBtnWidth, + _height: 30, _nativeHeight: 30, linearView_btnWidth: params.linearView_btnWidth, toolType: params.toolType, expertMode: params.expertMode, _dragOnlyWithinContainer: true, _lockedPosition: true, embedContainer: btnContainer @@ -891,9 +891,9 @@ pie title Minerals in my tap water return this.setupContextMenuButton(params, menuBtnDoc, menuDoc); } // linear view - const reqdSubMenuOpts = { ...OmitKeys(params, ["scripts", "funcs", "subMenu"]).omit, undoIgnoreFields: new List(['width', "linearView_IsOpen"]), + const reqdSubMenuOpts = { ...OmitKeys(params, ["scripts", "funcs", "subMenu"]).omit, undoIgnoreFields: new List(['width', "linearView_isOpen"]), childDontRegisterViews: true, flexGap: 0, _height: 30, _width: 30, ignoreClick: !params.scripts?.onClick, - linearView_SubMenu: true, linearView_Expandable: true, embedContainer: menuDoc}; + linearView_expandable: true, embedContainer: menuDoc}; const items = (menuBtn?:Doc) => !menuBtn ? [] : subMenu.map(sub => this.setupContextMenuBtn(sub, menuBtn) ); const creator = params.btnType === ButtonType.MultiToggleButton ? this.multiToggleList : this.linearButtonList; @@ -905,7 +905,7 @@ pie title Minerals in my tap water /// Initializes all the default buttons for the top bar context menu 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 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().map(params => this.setupContextMenuBtn(params, ctxtMenuBtnsDoc) ); return DocUtils.AssignOpts(ctxtMenuBtnsDoc, reqdCtxtOpts, ctxtMenuBtns); @@ -932,7 +932,7 @@ pie title Minerals in my tap water const btns = btnDescs.map(desc => dockBtn({_width: desc.opts.width??30, _height: 30, defaultDoubleClick: 'ignore', undoIgnoreFields: new List(['opacity']), _dragOnlyWithinContainer: true, ...desc.opts}, desc.scripts, desc.funcs)); const dockBtnsReqdOpts:DocumentOptions = { title: "docked buttons", _height: 40, flexGap: 0, layout_boxShadow: "standard", childDragAction: dropActionType.move, - childDontRegisterViews: true, linearView_IsOpen: true, linearView_Expandable: false, ignoreClick: true + childDontRegisterViews: true, linearView_isOpen: true, linearView_expandable: false, ignoreClick: true }; return DocUtils.AssignDocField(doc, field, (opts, items) => this.linearButtonList(opts, items??[]), dockBtnsReqdOpts, btns); } diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts index d8e0c4cbe..6461605a6 100644 --- a/src/client/util/LinkManager.ts +++ b/src/client/util/LinkManager.ts @@ -265,10 +265,10 @@ export function UPDATE_SERVER_CACHE() { cacheDocumentIds = newCacheUpdate; // print out cached docs - Doc.MyDockedBtns?.linearView_IsOpen && console.log('Set cached docs = '); + Doc.MyDockedBtns?.linearView_isOpen && console.log('Set cached docs = '); const isFiltered = filtered.filter(doc => !Doc.IsSystem(doc)); const strings = isFiltered.map(doc => StrCast(doc.title) + ' ' + (Doc.IsDataProto(doc) ? '(data)' : '(embedding)')); - Doc.MyDockedBtns?.linearView_IsOpen && strings.sort().forEach((str, i) => console.log(i.toString() + ' ' + str)); + Doc.MyDockedBtns?.linearView_isOpen && strings.sort().forEach((str, i) => console.log(i.toString() + ' ' + str)); rp.post(ClientUtils.prepend('/setCacheDocumentIds'), { body: { -- cgit v1.2.3-70-g09d2 From 4b8c4c9277cca69ea3341ca6ddf4c7f5befc78a0 Mon Sep 17 00:00:00 2001 From: bobzel Date: Wed, 7 May 2025 15:46:06 -0400 Subject: cleaned up ai metadata on generated drawings. --- src/client/documents/Documents.ts | 5 +- src/client/util/DocumentManager.ts | 2 +- .../collectionFreeForm/CollectionFreeFormView.tsx | 54 ++----------------- src/client/views/nodes/ImageBox.tsx | 6 +-- src/client/views/pdf/GPTPopup/GPTPopup.tsx | 4 +- src/client/views/smartdraw/DrawingFillHandler.tsx | 6 +-- src/client/views/smartdraw/SmartDrawHandler.scss | 1 + src/client/views/smartdraw/SmartDrawHandler.tsx | 63 +++++++++++----------- src/client/views/smartdraw/StickerPalette.tsx | 12 ++--- 9 files changed, 57 insertions(+), 96 deletions(-) (limited to 'src/client/util') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index bbe872a91..9b6acef7b 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -519,8 +519,9 @@ export class DocumentOptions { card_sort_isDesc?: BOOLt = new BoolInfo('whether the cards are sorted ascending or descending'); ai?: string; // to mark items as ai generated - ai_firefly_seed?: number; - ai_firefly_prompt?: string; + ai_prompt_seed?: NUMt = new NumInfo('seed to GAI engine to make results deterministic'); + ai_prompt?: STRt = new StrInfo('input prompt to GAI engine'); + ai_generatedDocs?: List; // list of documents generated by GAI engine /** * JSON‐stringified slot configuration for ScrapbookBox diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index 3bae2881e..fc8f22cf3 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -349,7 +349,7 @@ export class DocumentManager { // bcz: should this delay be an options parameter? setTimeout(() => { Doc.linkFollowHighlight(viewSpec ? [docView.Document, viewSpec] : docView.Document, undefined, options.effect); - const zoomableText = StrCast(targetDoc.text_html, StrCast(targetDoc.ai_firefly_prompt)); + const zoomableText = StrCast(targetDoc.text_html, StrCast(targetDoc.ai_prompt)); if (options.zoomTextSelections && Doc.IsUnhighlightTimerSet() && contextView && zoomableText) { // if the docView is a text anchor, the contextView is the PDF/Web/Text doc contextView.setTextHtmlOverlay(zoomableText, options.effect); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 05958e6b9..bb3c59eae 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1259,15 +1259,8 @@ export class CollectionFreeFormView extends CollectionSubView { - doc.$title = opts.text; - doc.$width = opts.size; - doc.$ai_drawing_input = opts.text; - doc.$ai_drawing_complexity = opts.complexity; - doc.$ai_drawing_colored = opts.autoColor; - doc.$ai_drawing_size = opts.size; - doc.$ai_drawing_data = gptRes; - doc.$ai = 'gpt'; + addDrawing = (doc: Doc, opts: DrawingOptions, x?: number, y?: number) => { + doc.$ai_prompt = opts.text; this._drawingContainer = doc; if (x !== undefined && y !== undefined) { [doc.x, doc.y] = this.screenToFreeformContentsXf.transformPoint(x, y); @@ -1977,7 +1970,7 @@ export class CollectionFreeFormView extends CollectionSubView { SmartDrawHandler.Instance.AddDrawing = this.addDrawing; SmartDrawHandler.Instance.RemoveDrawing = this.removeDrawing; - !SmartDrawHandler.Instance.ShowRegenerate ? SmartDrawHandler.Instance.displayRegenerate(this._downX, this._downY - 10) : SmartDrawHandler.Instance.hideRegenerate(); + !SmartDrawHandler.Instance.ShowRegenerate ? SmartDrawHandler.Instance.displayRegenerate(this._downX, this._downY - 10, NumCast(this.layoutDoc[this.scaleFieldKey])) : SmartDrawHandler.Instance.hideRegenerate(); }), icon: 'pen-to-square', }); @@ -2182,9 +2175,7 @@ export class CollectionFreeFormView extends CollectionSubView { - this._drawingFillInput = e.target.value; - })} + onChange={action(e => (this._drawingFillInput = e.target.value))} />
Similarity @@ -2213,11 +2204,7 @@ export class CollectionFreeFormView extends CollectionSubView { this._drawingFillLoading = true; - DrawingFillHandler.drawingToImage(this.props.Document, this._fireflyRefStrength, this._drawingFillInput || StrCast(this.Document.title))?.then( - action(() => { - this._drawingFillLoading = false; - }) - ); + DrawingFillHandler.drawingToImage(this.props.Document, this._fireflyRefStrength, this._drawingFillInput || StrCast(this.Document.title))?.then(action(() => (this._drawingFillLoading = false))); }), 'create image' )} @@ -2225,37 +2212,6 @@ export class CollectionFreeFormView extends CollectionSubView
-
- Regenerate -
- { - this._regenInput = e.target.value; - })} - placeholder="..under development.." - /> -
-
-
-
); }; diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index bf6915570..b648f2adb 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -225,7 +225,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { const drag = de.complete.docDragData.draggedDocuments.lastElement(); const dragField = drag[Doc.LayoutDataKey(drag)]; const descText = RTFCast(dragField)?.Text || StrCast(dragField) || RTFCast(drag.text)?.Text || StrCast(drag.text) || StrCast(this.Document.title); - const oldPrompt = StrCast(this.Document.ai_firefly_prompt, StrCast(this.Document.title)); + const oldPrompt = StrCast(this.Document.ai_prompt, StrCast(this.Document.title)); const newPrompt = (text: string) => (oldPrompt ? `${oldPrompt} ~~~ ${text}` : text); DrawingFillHandler.drawingToImage(this.Document, 90, newPrompt(descText), drag)?.then(action(() => (this._regenerateLoading = false))); added = false; @@ -735,9 +735,9 @@ export class ImageBox extends ViewBoxAnnotatableComponent() {
DocCast(this.Document.ai_firefly_generatedDocs) && DocumentView.showDocument(DocCast(this.Document.ai_firefly_generatedDocs)!, { openLocation: OpenWhere.addRight })} + onClick={() => DocCast(this.Document.ai_generatedDocs) && DocumentView.showDocument(DocCast(this.Document.ai_generatedDocs)!, { openLocation: OpenWhere.addRight })} style={{ - display: (this._props.isContentActive() && (SnappingManager.CanEmbed || this.Document.ai_firefly_generatedDocs)) || this._regenerateLoading ? 'block' : 'none', + display: (this._props.isContentActive() && (SnappingManager.CanEmbed || this.Document.ai_generatedDocs)) || this._regenerateLoading ? 'block' : 'none', transform: `scale(${this.uiBtnScaling})`, width: this._sideBtnWidth, height: this._sideBtnWidth, diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx index 9fbae5c90..568e48edf 100644 --- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx +++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx @@ -211,7 +211,7 @@ export class GPTPopup extends ObservableReactComponent { const selView = DocumentView.Selected().lastElement(); const selDoc = selView?.Document; if (selDoc && (selView._props.renderDepth > 1 || selDoc[Doc.LayoutDataKey(selDoc)] instanceof ImageField)) { - const oldPrompt = StrCast(selDoc.ai_firefly_prompt, StrCast(selDoc.title)); + const oldPrompt = StrCast(selDoc.ai_prompt, StrCast(selDoc.title)); const newPrompt = oldPrompt ? `${oldPrompt} ~~~ ${imgDesc}` : imgDesc; return DrawingFillHandler.drawingToImage(selDoc, 100, newPrompt, selDoc) .then(action(() => (this._userPrompt = ''))) @@ -767,7 +767,7 @@ export class GPTPopup extends ObservableReactComponent { //prettier-ignore switch (this._mode) { case GPTPopupMode.USER_PROMPT: return this.promptBox("ASK", this._userPrompt, this.setUserPrompt, 'Ask GPT to sort, tag, define, or filter your documents for you!'); - case GPTPopupMode.FIREFLY: return this.promptBox("CREATE", this._userPrompt, this.setUserPrompt, StrCast(DocumentView.Selected().lastElement()?.Document.ai_firefly_prompt, 'Ask Firefly to generate images')); + case GPTPopupMode.FIREFLY: return this.promptBox("CREATE", this._userPrompt, this.setUserPrompt, StrCast(DocumentView.Selected().lastElement()?.Document.ai_prompt, 'Ask Firefly to generate images')); case GPTPopupMode.QUIZ_RESPONSE: return this.promptBox("QUIZ", this._quizAnswer, this.setQuizAnswer, 'Describe/answer the selected document!'); case GPTPopupMode.GPT_MENU: return this.menuBox(); case GPTPopupMode.SUMMARY: return this.summaryBox(); diff --git a/src/client/views/smartdraw/DrawingFillHandler.tsx b/src/client/views/smartdraw/DrawingFillHandler.tsx index 2c69284db..f773957e7 100644 --- a/src/client/views/smartdraw/DrawingFillHandler.tsx +++ b/src/client/views/smartdraw/DrawingFillHandler.tsx @@ -51,19 +51,19 @@ export class DrawingFillHandler { if (error.includes('Dropbox') && confirm('Create image failed. Try authorizing DropBox?\r\n' + error.replace(/^[^"]*/, ''))) { return DrawingFillHandler.authorizeDropbox(); } - const genratedDocs = DocCast(drawing.ai_firefly_generatedDocs) ?? Docs.Create.MasonryDocument([], { title: StrCast(drawing.title) + ' AI Images', _width: 400, _height: 400 }); - drawing.$ai_firefly_generatedDocs = genratedDocs; + const genratedDocs = DocCast(drawing.ai_generatedDocs) ?? Docs.Create.MasonryDocument([], { title: StrCast(drawing.title) + ' AI Images', _width: 400, _height: 400 }); + drawing.$ai_generatedDocs = genratedDocs; (res as Upload.ImageInformation[]).map(info => Doc.AddDocToList( genratedDocs, undefined, Docs.Create.ImageDocument(info.accessPaths.agnostic.client, { ai: 'firefly', + ai_prompt: newPrompt, tags: new List(['@ai']), title: newPrompt, _data_usePath: 'alternate:hover', data_alternates: new List([drawing]), - ai_firefly_prompt: newPrompt, _width: 500, data_nativeWidth: info.nativeWidth, data_nativeHeight: info.nativeHeight, diff --git a/src/client/views/smartdraw/SmartDrawHandler.scss b/src/client/views/smartdraw/SmartDrawHandler.scss index cca7d77c7..e80f1122b 100644 --- a/src/client/views/smartdraw/SmartDrawHandler.scss +++ b/src/client/views/smartdraw/SmartDrawHandler.scss @@ -73,5 +73,6 @@ .edit-box { display: flex; flex-direction: row; + color: black; } } diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx index a6e221f3e..3976ec39e 100644 --- a/src/client/views/smartdraw/SmartDrawHandler.tsx +++ b/src/client/views/smartdraw/SmartDrawHandler.tsx @@ -61,7 +61,6 @@ export class SmartDrawHandler extends ObservableReactComponent { static Instance: SmartDrawHandler; private _lastInput: DrawingOptions = { text: '', complexity: 5, size: 350, autoColor: true, x: 0, y: 0 }; - private _lastResponse: string = ''; private _selectedDocs: Doc[] = []; @observable private _display: boolean = false; @@ -97,7 +96,7 @@ export class SmartDrawHandler extends ObservableReactComponent { CollectionFreeForm, FormattedTextBox, StickerPalette) to define how a drawing document should be added or removed in their respective locations (to the freeform canvas, to the sticker palette's preview, etc.) */ - public AddDrawing: (doc: Doc, opts: DrawingOptions, gptRes: string, x?: number, y?: number) => void = unimplementedFunction; + public AddDrawing: (doc: Doc, opts: DrawingOptions, x?: number, y?: number) => void = unimplementedFunction; public RemoveDrawing: (useLastContainer: boolean, doc?: Doc) => void = unimplementedFunction; /** * This creates the ink document that represents a drawing, so it goes through the strokes that make up the drawing, @@ -105,7 +104,7 @@ export class SmartDrawHandler extends ObservableReactComponent { * classes to customize the way the drawing docs get created. For example, the freeform canvas has a different way of * defining document bounds, so CreateDrawingDoc is redefined when that class calls gpt draw functions. */ - public static CreateDrawingDoc: (strokeList: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => Doc | undefined = (strokeList: [InkData, string, string][], opts: DrawingOptions) => { + public static CreateDrawingDoc: (strokeList: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => Doc | undefined = (strokeList: [InkData, string, string][], opts: DrawingOptions, gptRes: string) => { const drawing: Doc[] = []; strokeList.forEach((stroke: [InkData, string, string]) => { const bounds = InkField.getBounds(stroke[0]); @@ -130,7 +129,14 @@ export class SmartDrawHandler extends ObservableReactComponent { drawing.push(inkDoc); }); - return MarqueeView.getCollection(drawing, undefined, true, { left: 1, top: 1, width: 1, height: 1 }); + const drawn = MarqueeView.getCollection(drawing, undefined, true, { left: 1, top: 1, width: 1, height: 1 }); + + drawn.$ai_drawing = true; + drawn.$ai_drawing_complexity = opts.complexity; + drawn.$ai_drawing_colored = opts.autoColor; + drawn.$ai_drawing_size = opts.size; + drawn.$ai_drawing_data = gptRes; + return drawn; }; @action @@ -146,15 +152,16 @@ export class SmartDrawHandler extends ObservableReactComponent { * the regenerate popup show by user command. */ @action - displayRegenerate = (x: number, y: number) => { + displayRegenerate = (x: number, y: number, scale: number) => { this._selectedDocs = [DocumentView.SelectedDocs()?.lastElement()]; [this._pageX, this._pageY] = [x, y]; + this._scale = scale; this._display = false; this.ShowRegenerate = true; this._showEditBox = false; const docData = this._selectedDocs[0]; - this._lastResponse = StrCast(docData.$drawingData); - this._lastInput = { text: StrCast(docData.$ai_drawing_input), complexity: NumCast(docData.$ai_drawing_complexity), size: NumCast(docData.$ai_drawing_size), autoColor: BoolCast(docData.$ai_drawing_colored), x: this._pageX, y: this._pageY }; + this._regenInput = StrCast(docData.$ai_prompt, StrCast(docData.title)); + this._lastInput = { text: StrCast(docData.$ai_prompt), complexity: NumCast(docData.$ai_drawing_complexity), size: NumCast(docData.$ai_drawing_size), autoColor: BoolCast(docData.$ai_drawing_colored), x: this._pageX, y: this._pageY }; }; /** @@ -168,9 +175,6 @@ export class SmartDrawHandler extends ObservableReactComponent { this._isLoading = false; this._showOptions = false; this._userInput = ''; - this._complexity = 5; - this._size = 350; - this._autoColor = true; Doc.ActiveTool = InkTool.None; } }; @@ -184,7 +188,6 @@ export class SmartDrawHandler extends ObservableReactComponent { this.ShowRegenerate = false; this._isLoading = false; this._regenInput = ''; - this._lastInput = { text: '', complexity: 5, size: 350, autoColor: true, x: 0, y: 0 }; } }; @@ -208,7 +211,9 @@ export class SmartDrawHandler extends ObservableReactComponent { this._isLoading = true; this._canInteract = false; if (this.ShowRegenerate) { - await this.regenerate(this._selectedDocs, undefined, undefined, this._regenInput).then(action(() => (this._showEditBox = false))); + this._lastInput.x = X; + this._lastInput.y = Y; + await this.regenerate(this._selectedDocs).then(action(() => (this._showEditBox = false))); } else { this._showOptions = false; try { @@ -234,13 +239,15 @@ export class SmartDrawHandler extends ObservableReactComponent { */ drawWithGPT = async (screenPt: { X: number; Y: number }, input: string, complexity: number, size: number, autoColor: boolean) => { if (input) { - this._lastInput = { text: input, complexity: complexity, size: size, autoColor: autoColor, x: screenPt.X, y: screenPt.Y }; + this._lastInput = { text: input, complexity, size, autoColor, x: screenPt.X, y: screenPt.Y }; const res = await gptAPICall(`"${input}", "${complexity}", "${size}"`, GPTCallType.DRAW, undefined, true); if (res) { const strokeData = await this.parseSvg(res, { X: 0, Y: 0 }, false, autoColor); const drawingDoc = strokeData && SmartDrawHandler.CreateDrawingDoc(strokeData.data, strokeData.lastInput, strokeData.lastRes); - drawingDoc && this.AddDrawing(drawingDoc, this._lastInput, res, screenPt.X, screenPt.Y); - drawingDoc && this._selectedDocs.push(drawingDoc); + if (drawingDoc) { + this.AddDrawing(drawingDoc, this._lastInput, screenPt.X, screenPt.Y); + this._selectedDocs.push(drawingDoc); + } return strokeData; } else { console.error('GPT call failed'); @@ -255,7 +262,7 @@ export class SmartDrawHandler extends ObservableReactComponent { createImageWithFirefly = (input: string, seed?: number): Promise => { this._lastInput.text = input; return SmartDrawHandler.CreateWithFirefly(input, this._imgDims, seed).then(doc => { - doc instanceof Doc && this.AddDrawing(doc, this._lastInput, input, this._pageX, this._pageY); + doc instanceof Doc && this.AddDrawing(doc, this._lastInput, this._pageX, this._pageY); return doc; }); }; /** @@ -301,8 +308,8 @@ export class SmartDrawHandler extends ObservableReactComponent { _width: Math.min(400, dims.width), _height: (Math.min(400, dims.width) * dims.height) / dims.width, ai: 'firefly', - ai_firefly_seed: +(newseed ?? 0), - ai_firefly_prompt: input, + ai_prompt_seed: +(newseed ?? 0), + ai_prompt: input, }); }) .catch(e => { @@ -316,33 +323,30 @@ export class SmartDrawHandler extends ObservableReactComponent { * @param doc the drawing Docs to regenerate */ @action - regenerate = (drawingDocs: Doc[], lastInput?: DrawingOptions, lastResponse?: string, regenInput?: string, changeInPlace?: boolean) => { - if (lastInput) this._lastInput = lastInput; - if (lastResponse) this._lastResponse = lastResponse; + regenerate = (drawingDocs: Doc[], regenInput?: string, changeInPlace?: boolean) => { if (regenInput) this._regenInput = regenInput; return Promise.all( drawingDocs.map(async doc => { switch (doc.type) { case DocumentType.IMG: { const func = changeInPlace ? this.recreateImageWithFirefly : this.createImageWithFirefly; - const newPrompt = doc.ai_firefly_prompt ? `${doc.ai_firefly_prompt} ~~~ ${this._regenInput}` : this._regenInput; - return this._regenInput ? func(newPrompt, NumCast(doc?.ai_firefly_seed)) : func(this._lastInput.text || StrCast(doc.ai_firefly_prompt)); + const newPrompt = doc.ai_prompt && doc.ai_prompt !== this._regenInput ? `${doc.ai_prompt} ~~~ ${this._regenInput}` : this._regenInput; + return this._regenInput ? func(newPrompt, NumCast(doc?.ai_prompt_seed)) : func(this._lastInput.text || StrCast(doc.ai_prompt)); } case DocumentType.COL: { try { const res = await (async () => { if (this._regenInput) { - const prompt = `This is your previously generated svg code: ${this._lastResponse} for the user input "${this._lastInput.text}". Please regenerate it with the provided specifications.`; + const prompt = `This is your previously generated svg code: ${doc.$ai_drawing_data} for the user input "${doc.ai_prompt}". Please regenerate it with the provided specifications.`; this._lastInput.text = `${this._lastInput.text} ~~~ ${this._regenInput}`; return gptAPICall(`"${this._regenInput}"`, GPTCallType.DRAW, prompt, true); } - return gptAPICall(`"${this._lastInput.text}", "${this._lastInput.complexity}", "${this._lastInput.size}"`, GPTCallType.DRAW, undefined, true); + return gptAPICall(`"${doc.$ai_prompt}", "${doc.$ai_drawing_complexity}", "${doc.$ai_drawing_size}"`, GPTCallType.DRAW, undefined, true); })(); if (res) { - const strokeData = await this.parseSvg(res, { X: this._lastInput.x ?? 0, Y: this._lastInput.y ?? 0 }, true, lastInput?.autoColor || this._autoColor); - this.RemoveDrawing !== unimplementedFunction && this.RemoveDrawing(true, doc); + const strokeData = await this.parseSvg(res, { X: 0, Y: 0 }, true, this._autoColor); const drawingDoc = strokeData && SmartDrawHandler.CreateDrawingDoc(strokeData.data, strokeData.lastInput, strokeData.lastRes); - drawingDoc && this.AddDrawing(drawingDoc, this._lastInput, res); + drawingDoc && this.AddDrawing(drawingDoc, this._lastInput, this._lastInput.x, this._lastInput.y); } else { console.error('GPT call failed'); } @@ -363,7 +367,6 @@ export class SmartDrawHandler extends ObservableReactComponent { const svg = res.match(/]*>([\s\S]*?)<\/svg>/g); if (svg) { - this._lastResponse = svg[0]; const svgObject = await parse(svg[0]); console.log(res, svgObject); const svgStrokes: INode[] = svgObject.children; @@ -664,7 +667,7 @@ export class SmartDrawHandler extends ObservableReactComponent {
: } + icon={this._isLoading ? : } color={SettingsManager.userColor} onClick={() => this.handleSendClick(this._pageX, this._pageY)} /> diff --git a/src/client/views/smartdraw/StickerPalette.tsx b/src/client/views/smartdraw/StickerPalette.tsx index 0e234e966..6ef3d26ad 100644 --- a/src/client/views/smartdraw/StickerPalette.tsx +++ b/src/client/views/smartdraw/StickerPalette.tsx @@ -9,7 +9,7 @@ import ReactLoading from 'react-loading'; import { returnEmptyFilter, returnFalse, returnTrue } from '../../../ClientUtils'; import { emptyFunction, numberRange } from '../../../Utils'; import { Doc, DocListCast, returnEmptyDoclist } from '../../../fields/Doc'; -import { ImageCast, NumCast } from '../../../fields/Types'; +import { ImageCast, NumCast, StrCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; import { DocumentType } from '../../documents/DocumentTypes'; import { Docs } from '../../documents/Documents'; @@ -147,9 +147,9 @@ export class StickerPalette extends ObservableReactComponent { + numberRange(3).map(() => { return this._showRegenerate - ? SmartDrawHandler.Instance.regenerate(prevDrawings, this._opts, this._gptRes[i], this._userInput) + ? SmartDrawHandler.Instance.regenerate(prevDrawings, this._userInput) : SmartDrawHandler.Instance.drawWithGPT({ X: 0, Y: 0 }, this._userInput, this._opts.complexity || 0, this._opts.size || 0, !!this._opts.autoColor); }) ).then(() => { @@ -161,8 +161,8 @@ export class StickerPalette extends ObservableReactComponent { - this._gptRes.push(gptRes); + addDrawing = (drawing: Doc) => { + this._gptRes.push(StrCast(drawing.$ai_drawing_data)); drawing.$freeform_fitContentsToBox = true; Doc.AddDocToList(this._props.Doc, 'data', drawing); }; @@ -176,7 +176,7 @@ export class StickerPalette extends ObservableReactComponent Date: Fri, 9 May 2025 23:01:36 -0400 Subject: added user templates to template_user global field. fixed schemas to not erase field values when setting a new column name. fixed templates to render properly when switching by fixing contentsRef to be observable. fixed templates with fields in stacks to autoHeight update correctly by . fixed images in templates to set their nativeDim after rendering. fixed text views of descriptions to auto-updated by setting the modification date of the description field. --- src/client/documents/DocUtils.ts | 20 +++++++++---------- src/client/util/DropConverter.ts | 4 +++- .../CollectionStackingViewFieldColumn.tsx | 6 ++++-- src/client/views/collections/CollectionSubView.tsx | 13 ++++++++++-- .../collectionSchema/CollectionSchemaView.tsx | 2 +- src/client/views/nodes/DocumentView.tsx | 6 +++--- src/client/views/nodes/ImageBox.tsx | 5 +++-- .../views/nodes/formattedText/FormattedTextBox.tsx | 3 ++- src/fields/Doc.ts | 23 +++++++++++++++++++--- src/server/SharedMediaTypes.ts | 3 +++ 10 files changed, 60 insertions(+), 25 deletions(-) (limited to 'src/client/util') diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts index 9135899d7..36e03daed 100644 --- a/src/client/documents/DocUtils.ts +++ b/src/client/documents/DocUtils.ts @@ -642,8 +642,15 @@ export namespace DocUtils { return dd; } - export function assignImageInfo(result: Upload.FileInformation, protoIn: Doc) { + export function assignUploadInfo(result: Upload.FileInformation, protoIn: Doc) { const proto = protoIn; + + if (Upload.isTextInformation(result)) { + proto.text = result.rawText; + } + if (Upload.isVideoInformation(result)) { + proto.data_duration = result.duration; + } if (Upload.isImageInformation(result)) { const maxNativeDim = Math.max(result.nativeHeight, result.nativeWidth); const exifRotation = StrCast(result.exifData?.data?.Orientation).toLowerCase(); @@ -677,15 +684,8 @@ export namespace DocUtils { const pathname = result.accessPaths.agnostic.client; const doc = await DocUtils.DocumentFromType(type, pathname, full, overwriteDoc); if (doc) { - const proto = Doc.GetProto(doc); - proto.text = result.rawText; - !(result instanceof Error) && DocUtils.assignImageInfo(result, proto); - if (Upload.isVideoInformation(result)) { - proto.data_duration = result.duration; - } - if (overwriteDoc) { - Doc.removeCurrentlyLoading(overwriteDoc); - } + DocUtils.assignUploadInfo(result, Doc.GetProto(doc)); + overwriteDoc && Doc.removeCurrentlyLoading(overwriteDoc); generatedDocuments.push(doc); } return doc; diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts index b6b111930..1d4779d8a 100644 --- a/src/client/util/DropConverter.ts +++ b/src/client/util/DropConverter.ts @@ -41,7 +41,7 @@ function makeTemplate(doc: Doc, first: boolean = true): boolean { if (first && !docs.length) { // bcz: feels hacky : if the root level document has items, it's not a field template isTemplate = Doc.MakeMetadataFieldTemplate(doc, layoutDoc[DocData], true) || isTemplate; - } else if (docData instanceof RichTextField || docData instanceof ImageField) { + } else if (docData instanceof RichTextField || docData instanceof ImageField || (docData === undefined && doc.type === DocumentType.IMG)) { if (!StrCast(layoutDoc.title).startsWith('-')) { isTemplate = Doc.MakeMetadataFieldTemplate(layoutDoc, layoutDoc[DocData], true); } @@ -86,6 +86,8 @@ export function makeUserTemplateButtonOrImage(doc: Doc, image?: string) { dbox.dragFactory = layoutDoc; dbox.dropPropertiesToRemove = doc.dropPropertiesToRemove instanceof ObjectField ? ObjectField.MakeCopy(doc.dropPropertiesToRemove) : undefined; dbox.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory)'); + const userTemplatesDoc = DocCast(Doc.UserDoc().template_user); + userTemplatesDoc && Doc.AddDocToList(userTemplatesDoc, 'data', layoutDoc); return dbox; } diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx index 994669734..345f60e75 100644 --- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx +++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx @@ -71,6 +71,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent< } _ele: HTMLElement | null = null; + _eleMasonrySingle = React.createRef(); protected onInternalPreDrop = (e: Event, de: DragManager.DropEvent, targetDropAction: dropActionType) => { const dragData = de.complete.docDragData; @@ -92,13 +93,13 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent< createColumnDropRef = (ele: HTMLDivElement | null) => { this.dropDisposer?.(); if (ele) this.dropDisposer = DragManager.MakeDropTarget(ele, this.columnDrop.bind(this), this._props.Doc, this.onInternalPreDrop.bind(this)); - else if (this._ele) this.props.refList.splice(this.props.refList.indexOf(this._ele), 1); + else if (this._eleMasonrySingle.current) this.props.refList.splice(this.props.refList.indexOf(this._eleMasonrySingle.current), 1); this._ele = ele; }; @action componentDidMount() { - this._ele && this.props.refList.push(this._ele); + this._eleMasonrySingle.current && this.props.refList.push(this._eleMasonrySingle.current); this._disposers.collapser = reaction( () => this._props.headingObject?.collapsed, collapsed => { this.collapsed = collapsed !== undefined ? BoolCast(collapsed) : false; }, // prettier-ignore @@ -363,6 +364,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent< }}>
() { } const loading = Docs.Create.LoadingDocument(file, options); Doc.addCurrentlyLoading(loading); - DocUtils.uploadFileToDoc(file, {}, loading); + DocUtils.uploadFileToDoc(file, {}, loading).then(d => { + if (d && d?.type === DocumentType.IMG) { + const imgTemplate = DocListCast(DocCast(Doc.UserDoc().template_user)?.data).find(d => d.title === 'shower'); + if (imgTemplate) { + d.layout_fieldKey = 'layout_shower'; + d.layout_shower = imgTemplate; + } + } + }); + return loading; }) )) diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx index c06391f35..6442385c0 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx +++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx @@ -359,7 +359,7 @@ export class CollectionSchemaView extends CollectionSubView() { @action addNewKey = (key: string, defaultVal: FieldType | undefined) => { this.childDocs.forEach(doc => { - doc[DocData][key] = defaultVal; + if (doc[DocData][key] === undefined) doc[DocData][key] = defaultVal; }); }; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index bdb97d7bb..f9c21451e 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -740,7 +740,7 @@ export class DocumentViewInternal extends DocComponent (this.widgetDecorations ? this.widgetOverlay : null); viewingAiEditor = () => (this._props.showAIEditor && this._componentView?.componentAIView?.() !== undefined ? this.aiEditor : null); - _contentsRef = React.createRef(); + @observable _contentsRef: DocumentContentsView | undefined = undefined; @computed get viewBoxContents() { TraceMobx(); const isInk = this.layoutDoc._layout_isSvg && !this._props.LayoutTemplateString; @@ -756,7 +756,7 @@ export class DocumentViewInternal extends DocComponent (this._contentsRef = r))} layoutFieldKey={StrCast(this.Document.layout_fieldKey, 'layout')} pointerEvents={this.contentPointerEvents} setContentViewBox={this.setContentView} @@ -1168,7 +1168,7 @@ export class DocumentView extends DocComponent() { * @returns boolean whether sub-component Doc is in synch with the layoutDoc that this view thinks its rendering */ IsInvalid = (renderDoc?: Doc): boolean => { - const docContents = this._docViewInternal?._contentsRef.current; + const docContents = this._docViewInternal?._contentsRef; return !( (!renderDoc || (docContents?.layoutDoc === renderDoc && // diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index b648f2adb..f7ad5c7e2 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -152,7 +152,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { this._disposers.path = reaction( () => ({ nativeSize: this.nativeSize, width: NumCast(this.layoutDoc._width), height: this.layoutDoc._height }), ({ nativeSize, width, height }) => { - if (!this.layoutDoc._layout_nativeDimEditable || !height) { + if (!this.layoutDoc._layout_nativeDimEditable || !height || this.layoutDoc.layout_resetNativeDim) { + this.layoutDoc.layout_resetNativeDim = undefined; // template images need to reset their dimensions when they are rendered with content. afterwards, remove this flag. this.layoutDoc._height = (width * nativeSize.nativeHeight) / nativeSize.nativeWidth; } }, @@ -1048,7 +1049,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent() { alert('Error uploading files - possibly due to unsupported file types'); } else { this.dataDoc[this.fieldKey] = new ImageField(result.accessPaths.agnostic.client); - !(result instanceof Error) && DocUtils.assignImageInfo(result, this.dataDoc); + !(result instanceof Error) && DocUtils.assignUploadInfo(result, this.dataDoc); } disposer(); } else { diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 824ac97da..57720baae 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -1224,13 +1224,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { if (this.EditorView && this.ApplyingChange !== this.fieldKey) { if (incomingValue?.data) { - const updatedState = JSON.parse(incomingValue.data.Data); + const updatedState = JSON.parse(incomingValue.data.Data.replace(/\n/g, '')); if (JSON.stringify(this.EditorView.state.toJSON()) !== JSON.stringify(updatedState)) { this.EditorView.updateState(EditorState.fromJSON(this.config, updatedState)); this.tryUpdateScrollHeight(); } } else if (this.EditorView.state.doc.textContent !== (incomingValue?.str ?? '')) { selectAll(this.EditorView.state, tx => this.EditorView?.dispatch(tx.insertText(incomingValue?.str ?? ''))); + this.tryUpdateScrollHeight(); } } }, diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index ba94f0504..990b6606f 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -25,6 +25,7 @@ import { ComputedField, ScriptField } from './ScriptField'; import { BoolCast, Cast, DocCast, FieldValue, ImageCastWithSuffix, NumCast, RTFCast, StrCast, ToConstructor, toList } from './Types'; import { containedFieldChangedHandler, deleteProperty, GetEffectiveAcl, getField, getter, makeEditable, makeReadOnly, setter, SharingPermissions } from './util'; import { gptImageLabel } from '../client/apis/gpt/GPT'; +import { DateField } from './DateField'; export let ObjGetRefField: (id: string, force?: boolean) => Promise; export let ObjGetRefFields: (ids: string[]) => Promise>; @@ -1109,6 +1110,10 @@ export namespace Doc { Cast(templateFieldValue, listSpec(Doc), [])?.map(d => d instanceof Doc && MakeMetadataFieldTemplate(d, templateDoc)); Doc.GetProto(templateField)[metadataFieldKey] = ObjectField.MakeCopy(templateFieldValue); } + if (templateField.type === DocumentType.IMG) { + // bcz: should be a better way .. but, if the image is a template, then we can't expect to know the aspect ratio. When the image is replaced by data and rendered, we want to recomputed the native dimensions. + templateField[DocData].layout_resetNativeDim = true; + } // get the layout string that the template uses to specify its layout const templateFieldLayoutString = StrCast(Doc.LayoutField(templateField[DocLayout])); @@ -1184,13 +1189,15 @@ export namespace Doc { return Doc.NativeWidth(doc, dataDoc, useDim) / (Doc.NativeHeight(doc, dataDoc, useDim) || 1); } export function NativeWidth(doc?: Doc, dataDoc?: Doc, useWidth?: boolean) { - return !doc ? 0 : NumCast(doc._nativeWidth, NumCast((dataDoc || doc)[Doc.LayoutDataKey(doc) + '_nativeWidth'], useWidth ? NumCast(doc._width) : 0)); + // if this is a field template, then don't use the doc's nativeWidth/height + return !doc ? 0 : NumCast(doc.isTemplateForField ? undefined : doc._nativeWidth, NumCast((dataDoc || doc)[Doc.LayoutDataKey(doc) + '_nativeWidth'], !doc.isTemplateForField && useWidth ? NumCast(doc._width) : 0)); } export function NativeHeight(doc?: Doc, dataDoc?: Doc, useHeight?: boolean) { if (!doc) return 0; const nheight = (Doc.NativeWidth(doc, dataDoc, useHeight) / NumCast(doc._width)) * NumCast(doc._height); // divide before multiply to avoid floating point errrorin case nativewidth = width const dheight = NumCast((dataDoc || doc)[Doc.LayoutDataKey(doc) + '_nativeHeight'], useHeight ? NumCast(doc._height) : 0); - return NumCast(doc._nativeHeight, nheight || dheight); + // if this is a field template, then don't use the doc's nativeWidth/height + return NumCast(doc.isTemplateForField ? undefined : doc._nativeHeight, nheight || dheight); } export function OutpaintingWidth(doc?: Doc, dataDoc?: Doc, useWidth?: boolean) { @@ -1504,7 +1511,13 @@ export namespace Doc { case DocumentType.RTF: return RTFCast(tdoc[Doc.LayoutDataKey(tdoc)])?.Text ?? StrCast(tdoc[Doc.LayoutDataKey(tdoc)]); default: return StrCast(tdoc.title).startsWith("Untitled") ? "" : StrCast(tdoc.title); }}); // prettier-ignore - return docText(doc).then(text => (doc['$' + Doc.LayoutDataKey(doc) + '_description'] = text)); + return docText(doc).then( + action(text => { + // set the time when the date changes. This also allows a live textbox view to react to the update, otherwise, it wouldn't take effect until the next time the view is rerendered. + doc['$' + Doc.LayoutDataKey(doc) + '_description_modificationDate'] = new DateField(); + return (doc['$' + Doc.LayoutDataKey(doc) + '_description'] = text); + }) + ); } // prettier-ignore @@ -1820,3 +1833,7 @@ ScriptingGlobals.add(function setDocRangeFilter(container: Doc, key: string, ran ScriptingGlobals.add(function toJavascriptString(str: string) { return Field.toJavascriptString(str as FieldType); }); +// eslint-disable-next-line prefer-arrow-callback +ScriptingGlobals.add(function getDescription(doc: Doc) { + return Doc.getDescription(doc); +}); diff --git a/src/server/SharedMediaTypes.ts b/src/server/SharedMediaTypes.ts index 9aa4b120f..43a9ce963 100644 --- a/src/server/SharedMediaTypes.ts +++ b/src/server/SharedMediaTypes.ts @@ -19,6 +19,9 @@ export enum AudioAnnoState { } export namespace Upload { + export function isTextInformation(uploadResponse: Upload.FileInformation): uploadResponse is Upload.ImageInformation { + return 'rawText' in uploadResponse; + } export function isImageInformation(uploadResponse: Upload.FileInformation): uploadResponse is Upload.ImageInformation { return 'nativeWidth' in uploadResponse; } -- cgit v1.2.3-70-g09d2 From 534532aebbab49b00dc76ad6a0cf6c20368d818c Mon Sep 17 00:00:00 2001 From: bobzel Date: Sat, 10 May 2025 00:01:39 -0400 Subject: added an imageTemplate ui button. --- src/client/util/CurrentUserUtils.ts | 1 + src/client/views/collections/CollectionSubView.tsx | 2 +- src/client/views/global/globalScripts.ts | 5 +++ src/client/views/nodes/DocumentView.tsx | 38 +++++++++++++--------- src/fields/Doc.ts | 4 --- 5 files changed, 30 insertions(+), 20 deletions(-) (limited to 'src/client/util') diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index d460cd0d7..ea3b8d146 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -841,6 +841,7 @@ pie title Minerals in my tap water { 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_); }'} }, + { title: "Img Temp",icon: "portrait", toolTip: "Default Image Template",btnType:ButtonType.ToggleButton, expertMode: false, toolType:DocumentType.IMG, scripts: { onClick: '{ return setDefaultImageTemplate(_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: "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, linearView_btnWidth:40}, diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 4ac755b9d..cafe367b7 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -563,7 +563,7 @@ export function CollectionSubView() { Doc.addCurrentlyLoading(loading); DocUtils.uploadFileToDoc(file, {}, loading).then(d => { if (d && d?.type === DocumentType.IMG) { - const imgTemplate = DocListCast(DocCast(Doc.UserDoc().template_user)?.data).find(d => d.title === 'shower'); + const imgTemplate = DocCast(Doc.UserDoc().defaultImageLayout); if (imgTemplate) { d.layout_fieldKey = 'layout_shower'; d.layout_shower = imgTemplate; diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts index f4beb1004..cb3adae10 100644 --- a/src/client/views/global/globalScripts.ts +++ b/src/client/views/global/globalScripts.ts @@ -156,6 +156,11 @@ ScriptingGlobals.add(function setDefaultTemplate(checkResult?: boolean) { }); // toggle: Set overlay status of selected document // eslint-disable-next-line prefer-arrow-callback +ScriptingGlobals.add(function setDefaultImageTemplate(checkResult?: boolean) { + return DocumentView.setDefaultImageTemplate(checkResult); +}); +// toggle: Set overlay status of selected document +// eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function setHeaderColor(color?: string, checkResult?: boolean) { if (checkResult) { return DocumentView.Selected().length ? StrCast(DocumentView.SelectedDocs().lastElement().layout_headingColor) : Doc.SharingDoc()?.headingColor; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index f9c21451e..05706fe6b 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1389,28 +1389,36 @@ export class DocumentView extends DocComponent() { custom && DocUtils.makeCustomViewClicked(this.Document, Docs.Create.StackingDocument, layout, undefined); }, 'set custom view'); - public static setDefaultTemplate(checkResult?: boolean) { - if (checkResult) { - return Doc.UserDoc().defaultTextLayout; + private static getTemplate(view: DocumentView | undefined) { + if (view) { + if (!view.layoutDoc.isTemplateDoc) { + MakeTemplate(view.Document); + Doc.AddDocToList(Doc.UserDoc(), 'template_user', view.Document); + Doc.AddDocToList(DocListCast(Doc.MyTools?.data)[1], 'data', makeUserTemplateButtonOrImage(view.Document)); + DocCast(Doc.UserDoc().template_user) && view.Document && Doc.AddDocToList(DocCast(Doc.UserDoc().template_user)!, 'data', view.Document); + return view.Document; + } + return DocCast(Doc.LayoutField(view.Document)) ?? view.Document; } + } + public static setDefaultTemplate(checkResult?: boolean) { + if (checkResult) return Doc.UserDoc().defaultTextLayout; const view = DocumentView.Selected()[0]?._props.renderDepth > 0 ? DocumentView.Selected()[0] : undefined; undoable(() => { - let tempDoc: Opt; - if (view) { - if (!view.layoutDoc.isTemplateDoc) { - tempDoc = view.Document; - MakeTemplate(tempDoc); - Doc.AddDocToList(Doc.UserDoc(), 'template_user', tempDoc); - Doc.AddDocToList(DocListCast(Doc.MyTools?.data)[1], 'data', makeUserTemplateButtonOrImage(tempDoc)); - DocCast(Doc.UserDoc().template_user) && tempDoc && Doc.AddDocToList(DocCast(Doc.UserDoc().template_user)!, 'data', tempDoc); - } else { - tempDoc = DocCast(Doc.LayoutField(view.Document)); - } - } + const tempDoc = DocumentView.getTemplate(view); Doc.UserDoc().defaultTextLayout = tempDoc ? new PrefetchProxy(tempDoc) : undefined; }, 'set default template')(); return undefined; } + public static setDefaultImageTemplate(checkResult?: boolean) { + if (checkResult) return Doc.UserDoc().defaultImageLayout; + const view = DocumentView.Selected()[0]?._props.renderDepth > 0 ? DocumentView.Selected()[0] : undefined; + undoable(() => { + const tempDoc = DocumentView.getTemplate(view); + Doc.UserDoc().defaultImageLayout = tempDoc ? new PrefetchProxy(tempDoc) : undefined; + }, 'set default image template')(); + return undefined; + } /** * This switches between the current view of a Doc and a specified alternate layout view. diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 990b6606f..0d11b9743 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -1833,7 +1833,3 @@ ScriptingGlobals.add(function setDocRangeFilter(container: Doc, key: string, ran ScriptingGlobals.add(function toJavascriptString(str: string) { return Field.toJavascriptString(str as FieldType); }); -// eslint-disable-next-line prefer-arrow-callback -ScriptingGlobals.add(function getDescription(doc: Doc) { - return Doc.getDescription(doc); -}); -- cgit v1.2.3-70-g09d2 From b1bb206c73a0fbc4fb439cedd212565f7f85f4f8 Mon Sep 17 00:00:00 2001 From: bobzel Date: Sat, 10 May 2025 20:26:00 -0400 Subject: changed how we corsProxy web pages to be simpler and work better. changed dragging off annotations after text selections to only create a text doc whent the drop target is the parent collection -- otherwise links are created. --- package-lock.json | 131 ++---------- package.json | 13 +- src/ClientUtils.ts | 2 +- src/client/documents/Documents.ts | 2 +- src/client/util/CurrentUserUtils.ts | 2 +- src/client/views/MarqueeAnnotator.tsx | 1 + src/client/views/collections/CollectionSubView.tsx | 18 +- .../collectionSchema/SchemaColumnHeader.tsx | 7 +- src/client/views/nodes/WebBox.tsx | 22 ++- src/client/views/nodes/WebBoxRenderer.js | 2 +- src/server/server_Initialization.ts | 220 ++++++++------------- 11 files changed, 135 insertions(+), 285 deletions(-) (limited to 'src/client/util') diff --git a/package-lock.json b/package-lock.json index b4c008ffd..28b81dbc7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -68,7 +68,7 @@ "any-base": "^1.1.0", "archiver": "^7.0.1", "async": "^3.2.5", - "axios": "^1.7.3", + "axios": "^1.9.0", "babel-loader": "^10.0.0", "bcrypt-nodejs": "0.0.3", "better-react-mathjax": "^2.0.4-beta1", @@ -83,7 +83,6 @@ "bson": "^6.2.0", "canvas": "^3.1.0", "chart.js": "^4.4.0", - "cheerio": "^1.0.0", "child_process": "^1.0.2", "class-transformer": "^0.5.1", "cohere-ai": "^7.10.6", @@ -95,6 +94,7 @@ "cookie-parser": "^1.4.6", "cookie-session": "^2.0.0", "copy-webpack-plugin": "^13.0.0", + "copyfiles": "^2.4.1", "core-js": "^3.33.3", "cors": "^2.8.5", "css-loader": "^7.1.2", @@ -111,7 +111,7 @@ "eslint-webpack-plugin": "^5.0.0", "exif": "^0.6.0", "exifr": "^7.1.3", - "express": "^4.18.2", + "express": "^4.21.2", "express-flash": "0.0.2", "express-session": "^1.17.3", "express-validator": "^7.0.1", @@ -158,7 +158,7 @@ "jpeg-autorotate": "^9.0.0", "jquery": "^3.7.1", "js-datepicker": "^5.18.2", - "jsdom": "^26.0.0", + "jsdom": "^26.1.0", "jsonschema": "^1.4.1", "jszip": "^3.10.1", "ldrs": "^1.0.2", @@ -259,7 +259,6 @@ "tough-cookie": "^5.0.0", "tslint": "^5.20.1", "tslint-loader": "^3.5.4", - "typescript": "^5.3.3", "typescript-collections": "^1.3.3", "typescript-language-server": "^4.1.3", "uninstall": "^0.0.0", @@ -297,7 +296,7 @@ "@types/dom-mediacapture-record": "^1.0.19", "@types/dompurify": "^3.0.5", "@types/exif": "^0.6.5", - "@types/express": "^5.0.0", + "@types/express": "^5.0.1", "@types/express-session": "^1.17.10", "@types/file-saver": "^2.0.7", "@types/fuzzy-search": "^2.1.5", @@ -308,7 +307,7 @@ "@types/libxmljs": "^0.18.12", "@types/lodash": "^4.14.202", "@types/mocha": "^10.0.6", - "@types/node": "^22.4.2", + "@types/node": "^22.15.17", "@types/nodemailer": "^6.4.14", "@types/passport": "^1.0.16", "@types/passport-google-oauth20": "^2.0.14", @@ -332,7 +331,6 @@ "@types/webscopeio__react-textarea-autocomplete": "^4.7.5", "@types/youtube": "^0.1.0", "chai": "^5.0.0", - "copyfiles": "^2.4.1", "cross-env": "^7.0.3", "eslint": "^9.12.0", "eslint-plugin-react": "^7.37.1", @@ -343,6 +341,7 @@ "ts-loader": "^9.5.1", "ts-node": "^10.9.1", "ts-node-dev": "^2.0.0", + "typescript": "^5.8.3", "typescript-eslint": "^8.8.0", "webpack-dev-server": "^5.0.4" }, @@ -14952,9 +14951,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.14.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz", - "integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==", + "version": "22.15.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.17.tgz", + "integrity": "sha512-wIX2aSZL5FE+MR0JlvF87BNVrtFWf6AE6rxSE9X7OwnVvoyCQjpzSRJ+M87se/4QCkCiebQAqrJ0y6fwIyi7nw==", "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -16832,9 +16831,9 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", - "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", + "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -17899,31 +17898,6 @@ "node": ">= 16" } }, - "node_modules/cheerio": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz", - "integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==", - "license": "MIT", - "dependencies": { - "cheerio-select": "^2.1.0", - "dom-serializer": "^2.0.0", - "domhandler": "^5.0.3", - "domutils": "^3.1.0", - "encoding-sniffer": "^0.2.0", - "htmlparser2": "^9.1.0", - "parse5": "^7.1.2", - "parse5-htmlparser2-tree-adapter": "^7.0.0", - "parse5-parser-stream": "^7.1.2", - "undici": "^6.19.5", - "whatwg-mimetype": "^4.0.0" - }, - "engines": { - "node": ">=18.17" - }, - "funding": { - "url": "https://github.com/cheeriojs/cheerio?sponsor=1" - } - }, "node_modules/cheerio-select": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", @@ -18747,7 +18721,6 @@ "version": "2.4.1", "resolved": "https://registry.npmjs.org/copyfiles/-/copyfiles-2.4.1.tgz", "integrity": "sha512-fereAvAvxDrQDOXybk3Qu3dPbOoKoysFMWtkY3mv5BsL8//OSZVL5DCLYqgRfY5cWirgRzlC+WSrxp6Bo3eNZg==", - "dev": true, "license": "MIT", "dependencies": { "glob": "^7.0.5", @@ -18768,7 +18741,6 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -20477,31 +20449,6 @@ "iconv-lite": "^0.6.2" } }, - "node_modules/encoding-sniffer": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz", - "integrity": "sha512-ju7Wq1kg04I3HtiYIOrUrdfdDvkyO9s5XM8QAj/bN61Yo/Vb4vgJxy5vi4Yxk01gWHbrofpPtpxM8bKger9jhg==", - "license": "MIT", - "dependencies": { - "iconv-lite": "^0.6.3", - "whatwg-encoding": "^3.1.1" - }, - "funding": { - "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" - } - }, - "node_modules/encoding-sniffer/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/encoding/node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -23863,25 +23810,6 @@ "node": ">=8.0.0" } }, - "node_modules/htmlparser2": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", - "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.1.0", - "entities": "^4.5.0" - } - }, "node_modules/http-browserify": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/http-browserify/-/http-browserify-1.7.0.tgz", @@ -27707,7 +27635,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true, "license": "MIT", "bin": { "mkdirp": "bin/cmd.js" @@ -28569,7 +28496,6 @@ "version": "0.0.0", "resolved": "https://registry.npmjs.org/noms/-/noms-0.0.0.tgz", "integrity": "sha512-lNDU9VJaOPxUmXcLb+HQFeUgQQPtMI24Gt6hgfuMHRJgMRHMF/qZ4HJD3GDru4sSw9IQl2jPjAYnQrdIeLbwow==", - "dev": true, "license": "ISC", "dependencies": { "inherits": "^2.0.1", @@ -28580,14 +28506,12 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", - "dev": true, "license": "MIT" }, "node_modules/noms/node_modules/readable-stream": { "version": "1.0.34", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", "integrity": "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==", - "dev": true, "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", @@ -28600,7 +28524,6 @@ "version": "0.10.31", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", - "dev": true, "license": "MIT" }, "node_modules/normalize-path": { @@ -31794,18 +31717,6 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/parse5-parser-stream": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", - "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", - "license": "MIT", - "dependencies": { - "parse5": "^7.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, "node_modules/parseley": { "version": "0.12.1", "resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz", @@ -37293,7 +37204,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, "license": "MIT", "dependencies": { "readable-stream": "~2.3.6", @@ -37304,14 +37214,12 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, "license": "MIT" }, "node_modules/through2/node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", @@ -37327,14 +37235,12 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, "license": "MIT" }, "node_modules/through2/node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "~5.1.0" @@ -38496,15 +38402,6 @@ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", "license": "MIT" }, - "node_modules/undici": { - "version": "6.21.2", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.2.tgz", - "integrity": "sha512-uROZWze0R0itiAKVPsYhFov9LxrPMHLMEQFszeI2gCN6bnIIZ8twzBCJcN2LJrBBLfrP0t1FW0g+JmKVl8Vk1g==", - "license": "MIT", - "engines": { - "node": ">=18.17" - } - }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -38781,7 +38678,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -40242,7 +40138,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.4" diff --git a/package.json b/package.json index 07e72792c..6a579efd2 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "@types/dom-mediacapture-record": "^1.0.19", "@types/dompurify": "^3.0.5", "@types/exif": "^0.6.5", - "@types/express": "^5.0.0", + "@types/express": "^5.0.1", "@types/express-session": "^1.17.10", "@types/file-saver": "^2.0.7", "@types/fuzzy-search": "^2.1.5", @@ -57,7 +57,7 @@ "@types/libxmljs": "^0.18.12", "@types/lodash": "^4.14.202", "@types/mocha": "^10.0.6", - "@types/node": "^22.4.2", + "@types/node": "^22.15.17", "@types/nodemailer": "^6.4.14", "@types/passport": "^1.0.16", "@types/passport-google-oauth20": "^2.0.14", @@ -91,6 +91,7 @@ "ts-loader": "^9.5.1", "ts-node": "^10.9.1", "ts-node-dev": "^2.0.0", + "typescript": "^5.8.3", "typescript-eslint": "^8.8.0", "webpack-dev-server": "^5.0.4" }, @@ -151,7 +152,7 @@ "any-base": "^1.1.0", "archiver": "^7.0.1", "async": "^3.2.5", - "axios": "^1.7.3", + "axios": "^1.9.0", "babel-loader": "^10.0.0", "bcrypt-nodejs": "0.0.3", "better-react-mathjax": "^2.0.4-beta1", @@ -166,7 +167,6 @@ "bson": "^6.2.0", "canvas": "^3.1.0", "chart.js": "^4.4.0", - "cheerio": "^1.0.0", "child_process": "^1.0.2", "class-transformer": "^0.5.1", "cohere-ai": "^7.10.6", @@ -195,7 +195,7 @@ "eslint-webpack-plugin": "^5.0.0", "exif": "^0.6.0", "exifr": "^7.1.3", - "express": "^4.18.2", + "express": "^4.21.2", "express-flash": "0.0.2", "express-session": "^1.17.3", "express-validator": "^7.0.1", @@ -242,7 +242,7 @@ "jpeg-autorotate": "^9.0.0", "jquery": "^3.7.1", "js-datepicker": "^5.18.2", - "jsdom": "^26.0.0", + "jsdom": "^26.1.0", "jsonschema": "^1.4.1", "jszip": "^3.10.1", "ldrs": "^1.0.2", @@ -343,7 +343,6 @@ "tough-cookie": "^5.0.0", "tslint": "^5.20.1", "tslint-loader": "^3.5.4", - "typescript": "^5.3.3", "typescript-collections": "^1.3.3", "typescript-language-server": "^4.1.3", "uninstall": "^0.0.0", diff --git a/src/ClientUtils.ts b/src/ClientUtils.ts index 03ff13924..cc8b715b4 100644 --- a/src/ClientUtils.ts +++ b/src/ClientUtils.ts @@ -194,7 +194,7 @@ export namespace ClientUtils { } export function CorsProxy(url: string): string { - return prepend('/corsProxy/') + encodeURIComponent(url); + return prepend('/corsproxy/') + encodeURIComponent(url); } export function CopyText(text: string) { diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 9b6acef7b..2df6f3e23 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -1029,7 +1029,7 @@ export namespace Docs { const nwid = options._nativeWidth || undefined; const nhght = options._nativeHeight || undefined; if (!nhght && width && height && nwid) options._nativeHeight = (Number(nwid) * Number(height)) / Number(width); - return InstanceFromProto(Prototypes.get(DocumentType.WEB), new WebField(url || 'https://www.wikipedia.org/'), options); + return InstanceFromProto(Prototypes.get(DocumentType.WEB), new WebField(url || 'https://wikipedia.org/'), options); } export function HtmlDocument(html: string, options: DocumentOptions = {}) { diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index ea3b8d146..28b19d55e 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -1073,7 +1073,7 @@ pie title Minerals in my tap water Doc.MyRecentlyClosed && Doc.AddDocToList(Doc.MyFilesystem, undefined, Doc.MyRecentlyClosed); } - DocCast(Doc.UserDoc().emptyWebpage) && (Doc.GetProto(DocCast(Doc.UserDoc().emptyWebpage)!).data = new WebField("https://www.wikipedia.org")); + DocCast(Doc.UserDoc().emptyWebpage) && (Doc.GetProto(DocCast(Doc.UserDoc().emptyWebpage)!).data = new WebField("https://wikipedia.org")); DocServer.CacheNeedsUpdate() && setTimeout(UPDATE_SERVER_CACHE, 2500); setInterval(UPDATE_SERVER_CACHE, 120000); diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx index e4811a902..b2e42652d 100644 --- a/src/client/views/MarqueeAnnotator.tsx +++ b/src/client/views/MarqueeAnnotator.tsx @@ -198,6 +198,7 @@ export class MarqueeAnnotator extends ObservableReactComponent { const target = DocUtils.GetNewTextDoc('Note linked to ' + this.props.Document.title, 0, 0, 100, 100, annotationOn, 'yellow'); + target.layout_fitWidth = true; DocumentView.SetSelectOnLoad(target); return target; }; diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index cafe367b7..bdec694e8 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -356,12 +356,16 @@ export function CollectionSubView() { return !!added; } if (de.complete.annoDragData) { - const dropCreator = de.complete.annoDragData.dropDocCreator; - de.complete.annoDragData.dropDocCreator = () => { - const dropped = dropCreator(this._props.isAnnotationOverlay ? this.Document : undefined); - this.addDocument(dropped); - return dropped; - }; + if (![de.complete.annoDragData.dragDocument.embedContainer, de.complete.annoDragData.dragDocument].includes(this.Document)) { + de.complete.annoDragData.dropDocCreator = () => this.getAnchor?.(true) || this.Document; + } else { + const dropCreator = de.complete.annoDragData.dropDocCreator; + de.complete.annoDragData.dropDocCreator = () => { + const dropped = dropCreator(this._props.isAnnotationOverlay ? this.Document : undefined); + this.addDocument(dropped); + return dropped; + }; + } return true; } return false; @@ -415,7 +419,7 @@ export function CollectionSubView() { const tags = html.split('<'); if (tags[0] === '') tags.splice(0, 1); let img = tags[0].startsWith('img') ? tags[0] : tags.length > 1 && tags[1].startsWith('img') ? tags[1] : ''; - const cors = img.includes('corsProxy') ? img.match(/http.*corsProxy\//)![0] : ''; + const cors = img.includes('corsproxy') ? img.match(/http.*corsproxy\//)![0] : ''; img = cors ? img.replace(cors, '') : img; if (img) { const imgSrc = img.split('src="')[1].split('"')[0]; diff --git a/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx b/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx index 16d33eb93..134f2ed31 100644 --- a/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx +++ b/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx @@ -115,12 +115,11 @@ export class SchemaColumnHeader extends ObservableReactComponent { @@ -232,6 +230,7 @@ export class SchemaColumnHeader extends ObservableReactComponent { this.handlePointerEnter(); diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 5603786f0..838dbea9d 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -454,7 +454,7 @@ export class WebBox extends ViewBoxAnnotatableComponent() { iframeDown = (e: PointerEvent) => { this._textAnnotationCreator = undefined; const sel = this._url ? this._iframe?.contentDocument?.getSelection() : window.document.getSelection(); - if (sel?.empty) + if (sel?.empty && !(e.target as any).textContent) sel.empty(); // Chrome else if (sel?.removeAllRanges) sel.removeAllRanges(); // Firefox @@ -509,10 +509,10 @@ export class WebBox extends ViewBoxAnnotatableComponent() { try { href = iframe?.contentWindow?.location.href; } catch { - runInAction(() => this._warning++); + // runInAction(() => this._warning++); href = undefined; } - let requrlraw = decodeURIComponent(href?.replace(ClientUtils.prepend('') + '/corsProxy/', '') ?? this._url.toString()); + let requrlraw = decodeURIComponent(href?.replace(ClientUtils.prepend('') + '/corsproxy/', '') ?? this._url.toString()); if (requrlraw !== this._url.toString()) { if (requrlraw.match(/q=.*&/)?.length && this._url.toString().match(/q=.*&/)?.length) { const matches = requrlraw.match(/[^a-zA-z]q=[^&]*/g); @@ -565,9 +565,9 @@ export class WebBox extends ViewBoxAnnotatableComponent() { 'click', undoable( action((e: MouseEvent) => { - let eleHref = ''; + let eleHref = (e.target as any)?.outerHTML?.split('"="')[1]?.split('"')[0]; for (let ele = e.target as HTMLElement | Element | null; ele; ele = ele.parentElement) { - if (ele instanceof HTMLAnchorElement) { + if ('href' in ele) { eleHref = (typeof ele.href === 'string' ? ele.href : eleHref) || (ele.parentElement && 'href' in ele.parentElement ? (ele.parentElement.href as string) : eleHref); } } @@ -576,7 +576,8 @@ export class WebBox extends ViewBoxAnnotatableComponent() { const batch = UndoManager.StartBatch('webclick'); e.stopPropagation(); setTimeout(() => { - this.setData(eleHref.replace(ClientUtils.prepend(''), origin)); + const url = eleHref.replace(ClientUtils.prepend(''), origin); + this.setData(url); batch.end(); }); if (this._outerRef.current) { @@ -858,7 +859,7 @@ export class WebBox extends ViewBoxAnnotatableComponent() { ); } if (field instanceof WebField) { - const url = this.layoutDoc[this.fieldKey + '_useCors'] ? ClientUtils.CorsProxy(this._webUrl) : this._webUrl; + const url = this.layoutDoc[this.fieldKey + '_useCors'] ? '/corsproxy/' + this._webUrl : this._webUrl; const scripts = this.dataDoc[this.fieldKey + '_allowScripts'] || this._webUrl.includes('wikipedia.org') || this._webUrl.includes('google.com') || this._webUrl.startsWith('https://bing'); // if (!scripts) console.log('No scripts for: ' + url); return ( @@ -1074,15 +1075,15 @@ export class WebBox extends ViewBoxAnnotatableComponent() { childPointerEvents = () => (this._props.isContentActive() ? 'all' : undefined); @computed get webpage() { TraceMobx(); - const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1; + // const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1; const pointerEvents = this.layoutDoc._lockedPosition ? 'none' : (this._props.pointerEvents?.() as Property.PointerEvents | undefined); - const scale = previewScale * (this._props.NativeDimScaling?.() || 1); + // const scale = previewScale * (this._props.NativeDimScaling?.() || 1); return (
() { className="webBox-container" style={{ width: `calc(${100 / scale}% - ${!this.SidebarShown ? 0 : ((this.sidebarWidth() - WebBox.sidebarResizerWidth) / scale) * (this._previewWidth ? scale : 1)}px)`, + height: `${100 / scale}%`, transform: `scale(${scale})`, pointerEvents, }} diff --git a/src/client/views/nodes/WebBoxRenderer.js b/src/client/views/nodes/WebBoxRenderer.js index b727107a9..ef465c453 100644 --- a/src/client/views/nodes/WebBoxRenderer.js +++ b/src/client/views/nodes/WebBoxRenderer.js @@ -21,7 +21,7 @@ const ForeignHtmlRenderer = function (styleSheets) { return window.location.origin + extension; } function CorsProxy(url) { - return prepend('/corsProxy/') + encodeURIComponent(url); + return prepend('/corsproxy/') + encodeURIComponent(url); } /** * diff --git a/src/server/server_Initialization.ts b/src/server/server_Initialization.ts index a56ab5d18..514e2ce1e 100644 --- a/src/server/server_Initialization.ts +++ b/src/server/server_Initialization.ts @@ -1,19 +1,15 @@ import * as bodyParser from 'body-parser'; -import * as brotli from 'brotli'; import { blue, yellow } from 'colors'; import * as flash from 'connect-flash'; import * as MongoStoreConnect from 'connect-mongo'; -import * as cors from 'cors'; import * as express from 'express'; import * as expressFlash from 'express-flash'; import * as session from 'express-session'; import { createServer } from 'https'; import * as passport from 'passport'; -import * as request from 'request'; import * as webpack from 'webpack'; import * as wdm from 'webpack-dev-middleware'; import * as whm from 'webpack-hot-middleware'; -import * as zlib from 'zlib'; import * as config from '../../webpack.config'; import { logPort } from './ActionUtilities'; import RouteManager from './RouteManager'; @@ -23,6 +19,8 @@ import { SSL } from './apis/google/CredentialsLoader'; import { getForgot, getLogin, getLogout, getReset, getSignup, postForgot, postLogin, postReset, postSignup } from './authentication/AuthenticationManager'; import { Database } from './database'; import { WebSocket } from './websocket'; +import axios from 'axios'; +import { JSDOM } from 'jsdom'; /* RouteSetter is a wrapper around the server that prevents the server from being exposed. */ @@ -84,142 +82,96 @@ function buildWithMiddleware(server: express.Express) { return server; } -function registerEmbeddedBrowseRelativePathHandler(server: express.Express) { - server.use('*', (req, res) => { - // res.setHeader('Access-Control-Allow-Origin', '*'); - // res.header('Access-Control-Allow-Methods', 'GET, PUT, PATCH, POST, DELETE'); - // res.header('Access-Control-Allow-Headers', req.header('access-control-request-headers')); - const relativeUrl = req.originalUrl; - if (!res.headersSent && req.headers.referer?.includes('corsProxy')) { - if (!req.user) res.redirect('/home'); // When no user is logged in, we interpret a relative URL as being a reference to something they don't have access to and redirect to /home - // a request for something by a proxied referrer means it must be a relative reference. So construct a proxied absolute reference here. - try { - const proxiedRefererUrl = decodeURIComponent(req.headers.referer); // (e.g., http://localhost:/corsProxy/https://en.wikipedia.org/wiki/Engelbart) - const dashServerUrl = proxiedRefererUrl.match(/.*corsProxy\//)![0]; // the dash server url (e.g.: http://localhost:/corsProxy/ ) - const actualReferUrl = proxiedRefererUrl.replace(dashServerUrl, ''); // the url of the referer without the proxy (e.g., : https://en.wikipedia.org/wiki/Engelbart) - const absoluteTargetBaseUrl = actualReferUrl.match(/https?:\/\/[^/]*/)![0]; // the base of the original url (e.g., https://en.wikipedia.org) - const redirectedProxiedUrl = dashServerUrl + encodeURIComponent(absoluteTargetBaseUrl + relativeUrl); // the new proxied full url (e.g., http://localhost:/corsProxy/https://en.wikipedia.org/) - const redirectUrl = relativeUrl.startsWith('//') ? 'http:' + relativeUrl : redirectedProxiedUrl; - res.redirect(redirectUrl); - } catch (e) { - console.log('Error embed: ', e); +function registerCorsProxy(server: express.Express) { + // .replace('', ' ') + server.use('/corsproxy', async (req, res) => { + try { + // Extract URL from either query param or path + let targetUrl: string; + + if (req.query.url) { + // Case 1: URL passed as query parameter (/corsproxy?url=...) + targetUrl = req.query.url as string; + } else { + // Case 2: URL passed as path (/corsproxy/http://example.com) + const path = req.originalUrl.replace(/^\/corsproxy\/?/, ''); + targetUrl = decodeURIComponent(path); + + // Add protocol if missing (assuming https as default) + if (!targetUrl.startsWith('http://') && !targetUrl.startsWith('https://')) { + targetUrl = `https://${targetUrl}`; + } + } + + if (!targetUrl) { + res.send(` + Error +

Failed to load: ${targetUrl}

+

URL is required

+ `); + // res.status(400).json({ error: 'URL is required' }); + return; } - } else if (relativeUrl.startsWith('/search') && !req.headers.referer?.includes('corsProxy')) { - // detect search query and use default search engine - res.redirect(req.headers.referer + 'corsProxy/' + encodeURIComponent('http://www.google.com' + relativeUrl)); - } else { - res.status(404).json({ error: 'no such file or endpoint: try /home /logout /login' }); - } - }); -} -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function proxyServe(req: any, requrl: string, response: any) { - // eslint-disable-next-line @typescript-eslint/no-require-imports - const htmlBodyMemoryStream = new (require('memorystream'))(); - let wasinBrFormat = false; - const sendModifiedBody = () => { - const header = response.headers['content-encoding']; - const refToCors = (match: string, tag: string, sym: string, href: string) => `${tag}=${sym + resolvedServerUrl}/corsProxy/${href + sym}`; - // const relpathToCors = (match: any, href: string, offset: any, string: any) => `="${resolvedServerUrl + '/corsProxy/' + decodeURIComponent(req.originalUrl.split('/corsProxy/')[1].match(/https?:\/\/[^\/]*/)?.[0] ?? '') + '/' + href}"`; - if (header) { + // Validate URL format try { - const bodyStream = htmlBodyMemoryStream.read(); - if (bodyStream) { - const htmlInputText = wasinBrFormat ? Buffer.from(brotli.decompress(bodyStream)) : header.includes('gzip') ? zlib.gunzipSync(bodyStream) : bodyStream; - const htmlText = htmlInputText - .toString('utf8') - .replace('', ' ') - .replace(/(src|href)=(['"])(https?[^\n]*)\1/g, refToCors) // replace src or href='http(s)://...' or href="http(s)://.." - // .replace(/= *"\/([^"]*)"/g, relpathToCors) - .replace(/data-srcset="[^"]*"/g, '') - .replace(/srcset="[^"]*"/g, '') - .replace(/target="_blank"/g, ''); - response.send(header?.includes('gzip') ? zlib.gzipSync(htmlText) : htmlText); - } else { - req.pipe(request(requrl)) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .on('error', (e: any) => console.log('requrl ', e)) - .pipe(response) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .on('error', (e: any) => console.log('response pipe error', e)); - console.log('EMPTY body:' + req.url); - } + new URL(targetUrl); } catch (e) { - console.log('ERROR?: ', e); - } - } else { - req.pipe(htmlBodyMemoryStream) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .on('error', (e: any) => console.log('html body memorystream error', e)) - .pipe(response) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .on('error', (e: any) => console.log('html body memory stream response error', e)); - } - }; - const retrieveHTTPBody = () => { - // req.headers.cookie = ''; - req.pipe(request(requrl)) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .on('error', (e: any) => { - console.log(`CORS url error: ${requrl}`, e); - response.send(` + res.send(` Error -

Failed to load: ${requrl}

+

Failed to load: ${targetUrl}

${e}

`); - }) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .on('response', (res: any) => { - res.headers; - const headers = Object.keys(res.headers); - const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/; - headers.forEach(headerName => { - const header = res.headers[headerName]; - if (Array.isArray(header)) { - res.headers[headerName] = header.filter(h => !headerCharRegex.test(h)); - } else if (headerCharRegex.test(header || '')) { - delete res.headers[headerName]; - } else res.headers[headerName] = header; - if (headerName === 'content-encoding') { - wasinBrFormat = res.headers[headerName] === 'br'; - res.headers[headerName] = 'gzip'; - } + //res.status(400).json({ error: 'Invalid URL format' }); + return; + } + + const response = await axios.get(targetUrl as string, { + headers: { 'User-Agent': req.headers['user-agent'] || 'Mozilla/5.0' }, + responseType: 'text', + }); + + const baseUrl = new URL(targetUrl as string); + + if (response.headers['content-type']?.includes('text/html')) { + const dom = new JSDOM(response.data); + const document = dom.window.document; + + // Process all elements with href/src + const elements = document.querySelectorAll('[href],[src]'); + elements.forEach(elem => { + const attrs = []; + if (elem.hasAttribute('href')) attrs.push('href'); + if (elem.hasAttribute('src')) attrs.push('src'); + + attrs.forEach(attr => { + const originalUrl = elem.getAttribute(attr); + if (!originalUrl || originalUrl.startsWith('http://') || originalUrl.startsWith('https://') || originalUrl.startsWith('data:') || /^[a-z]+:/.test(originalUrl)) { + return; + } + + const resolvedUrl = new URL(originalUrl, baseUrl).toString(); + elem.setAttribute(attr, resolvedUrl); + }); }); - res.headers['x-permitted-cross-domain-policies'] = 'all'; - res.headers['x-frame-options'] = ''; - res.headers['content-security-policy'] = ''; - response.headers = response._headers = res.headers; - }) - .on('end', sendModifiedBody) - .pipe(htmlBodyMemoryStream) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .on('error', (e: any) => console.log('http body pipe error', e)); - }; - retrieveHTTPBody(); -} -function registerCorsProxy(server: express.Express) { - server.use('/corsProxy', async (req, res) => { - res.setHeader('Access-Control-Allow-Origin', '*'); - res.header('Access-Control-Allow-Methods', 'GET, PUT, PATCH, POST, DELETE'); - res.header('Access-Control-Allow-Headers', req.header('access-control-request-headers')); - const referer = req.headers.referer ? decodeURIComponent(req.headers.referer) : ''; - let requrlraw = decodeURIComponent(req.url.substring(1)); - const qsplit = requrlraw.split('?q='); - const newqsplit = requrlraw.split('&q='); - if (qsplit.length > 1 && newqsplit.length > 1) { - const lastq = newqsplit[newqsplit.length - 1]; - requrlraw = qsplit[0] + '?q=' + lastq.split('&')[0] + '&' + qsplit[1].split('&')[1]; - } - const requrl = requrlraw.startsWith('/') ? referer + requrlraw : requrlraw; - // cors weirdness here... - // if the referer is a cors page and the cors() route (I think) redirected to /corsProxy/ and the requested url path was relative, - // then we redirect again to the cors referer and just add the relative path. - if (!requrl.startsWith('http') && req.originalUrl.startsWith('/corsProxy') && referer?.includes('corsProxy')) { - res.redirect(referer + (referer.endsWith('/') ? '' : '/') + requrl); - } else { - proxyServe(req, requrl, res); + // Handle base tag + const baseTags = document.querySelectorAll('base'); + baseTags.forEach(tag => tag.remove()); + + const newBase = document.createElement('base'); + newBase.setAttribute('href', `${baseUrl}/`); + document.head.insertBefore(newBase, document.head.firstChild); + + response.data = dom.serialize(); + } + + res.set({ + 'Access-Control-Allow-Origin': '*', + 'Content-Type': response.headers['content-type'], + }).send(response.data); + } catch (error: unknown) { + res.status(500).json({ error: 'Proxy error', details: (error as { message: string }).message }); } }); } @@ -255,13 +207,11 @@ export default async function InitializeServer(routeSetter: RouteSetter) { app.use(whm(compiler)); app.get(/^\/+$/, (req, res) => res.redirect(req.user ? '/home' : '/login')); // target urls that consist of one or more '/'s with nothing in between app.use(express.static(publicDirectory, { setHeaders: res => res.setHeader('Access-Control-Allow-Origin', '*') })); // all urls that start with dash's public directory: /files/ (e.g., /files/images, /files/audio, etc) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - app.use(cors({ origin: (_origin: any, callback: any) => callback(null, true) })); + // app.use(cors({ origin: (_origin: any, callback: any) => callback(null, true) })); registerAuthenticationRoutes(app); // this adds routes to authenticate a user (login, etc) - registerCorsProxy(app); // this adds a /corsProxy/ route to allow clients to get to urls that would otherwise be blocked by cors policies + registerCorsProxy(app); // this adds a /corsproxy/ route to allow clients to get to urls that would otherwise be blocked by cors policies isRelease && !SSL.Loaded && SSL.exit(); routeSetter(new RouteManager(app, isRelease)); // this sets up all the regular supervised routes (things like /home, download/upload api's, pdf, search, session, etc) - registerEmbeddedBrowseRelativePathHandler(app); // this allows renered web pages which internally have relative paths to find their content isRelease && process.env.serverPort && (resolvedPorts.server = Number(process.env.serverPort)); const server = isRelease ? createServer(SSL.Credentials, app) : app; await new Promise(resolve => { -- cgit v1.2.3-70-g09d2 From 87484331f805f583c98226c5a8c6b1ec5fc88925 Mon Sep 17 00:00:00 2001 From: bobzel Date: Tue, 13 May 2025 13:37:54 -0400 Subject: fixed setting up link anchors for video to be added to Doc. fixed imageTemplate to work with templates in tabs. fixed link following from link menu. cleaned up getView with sidebars to open them when needed properly.. --- src/client/documents/DocUtils.ts | 2 +- src/client/util/DocumentManager.ts | 2 +- src/client/views/MarqueeAnnotator.tsx | 3 +- src/client/views/SidebarAnnos.tsx | 29 +++++++++-- .../collectionFreeForm/CollectionFreeFormView.tsx | 7 +-- src/client/views/linking/LinkMenuItem.tsx | 18 +++---- src/client/views/nodes/DocumentView.tsx | 2 +- src/client/views/nodes/MapBox/MapBox.tsx | 59 +++++++--------------- .../views/nodes/MapboxMapBox/MapboxContainer.tsx | 51 ++++++++++--------- src/client/views/nodes/PDFBox.tsx | 17 +++---- src/client/views/nodes/VideoBox.tsx | 17 ++++--- src/client/views/nodes/WebBox.tsx | 26 +++++----- .../views/nodes/formattedText/FormattedTextBox.tsx | 17 +++---- 13 files changed, 124 insertions(+), 126 deletions(-) (limited to 'src/client/util') diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts index 36e03daed..41a61b154 100644 --- a/src/client/documents/DocUtils.ts +++ b/src/client/documents/DocUtils.ts @@ -724,7 +724,7 @@ 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_autoHeight: true, - backgroundColor: StrCast(Doc.UserDoc().textBackgroundColor), + backgroundColor: backgroundColor ?? StrCast(Doc.UserDoc().textBackgroundColor), borderColor: Doc.UserDoc().borderColor as string, borderWidth: Doc.UserDoc().borderWidth as number, text_centered: BoolCast(Doc.UserDoc().textCentered), diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index fc8f22cf3..422613968 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -250,7 +250,7 @@ export class DocumentManager { const options = optionsIn; Doc.MyRecentlyClosed && Doc.RemoveDocFromList(Doc.MyRecentlyClosed, undefined, targetDoc); const docContextPath = DocumentManager.GetContextPath(targetDoc, true); - if (docContextPath.some(doc => doc.hidden)) options.toggleTarget = false; + if (docContextPath.some(doc => doc !== targetDoc && doc.hidden)) options.toggleTarget = false; let activatedTab = false; if (DocumentView.activateTabView(docContextPath[0])) { options.toggleTarget = false; diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx index b2e42652d..d354dd2c0 100644 --- a/src/client/views/MarqueeAnnotator.tsx +++ b/src/client/views/MarqueeAnnotator.tsx @@ -194,14 +194,13 @@ export class MarqueeAnnotator extends ObservableReactComponent { e.preventDefault(); e.stopPropagation(); - const sourceAnchorCreator = () => this.highlight(this.props.highlightDragSrcColor ?? 'rgba(173, 216, 230, 0.75)', true, undefined, true); // hyperlink color - const targetCreator = (annotationOn: Doc | undefined) => { const target = DocUtils.GetNewTextDoc('Note linked to ' + this.props.Document.title, 0, 0, 100, 100, annotationOn, 'yellow'); target.layout_fitWidth = true; DocumentView.SetSelectOnLoad(target); return target; }; + const sourceAnchorCreator = () => this.highlight(this.props.highlightDragSrcColor ?? 'rgba(173, 216, 230, 0.75)', true, undefined, true); // hyperlink color DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.props.docView(), sourceAnchorCreator, targetCreator), e.pageX, e.pageY, { dragComplete: dragEv => { if (!dragEv.aborted && dragEv.annoDragData && dragEv.annoDragData.linkSourceDoc && dragEv.annoDragData.dropDocument && dragEv.linkDocument) { diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx index 573c28ccf..71b479a22 100644 --- a/src/client/views/SidebarAnnos.tsx +++ b/src/client/views/SidebarAnnos.tsx @@ -3,7 +3,7 @@ import { observer } from 'mobx-react'; import * as React from 'react'; import { ClientUtils, returnFalse, returnOne, returnZero } from '../../ClientUtils'; import { emptyFunction } from '../../Utils'; -import { Doc, DocListCast, Field, FieldResult, FieldType, StrListCast } from '../../fields/Doc'; +import { Doc, DocListCast, Field, FieldResult, FieldType, Opt, StrListCast } from '../../fields/Doc'; import { Id } from '../../fields/FieldSymbols'; import { List } from '../../fields/List'; import { RichTextField } from '../../fields/RichTextField'; @@ -19,6 +19,7 @@ import { StyleProp } from './StyleProp'; import { CollectionStackingView } from './collections/CollectionStackingView'; import { DocumentView } from './nodes/DocumentView'; import { FieldViewProps } from './nodes/FieldView'; +import { FocusViewOptions } from './nodes/FocusViewOptions'; interface ExtraProps { fieldKey: string; @@ -149,15 +150,33 @@ export class SidebarAnnos extends ObservableReactComponent { if (DocListCast(this._props.Doc[this.sidebarKey]).find(anno => Doc.AreProtosEqual(doc.layout_unrendered ? DocCast(doc.annotationOn) : doc, anno))) { - if (this.childFilters()) { + if (this.childFilters().length) { // if any child filters exist, get rid of them this._props.layoutDoc._childFilters = new List(); + return true; } - return true; } return false; }; + public static getView(sidebar: SidebarAnnos | null, sidebarShown: boolean, toggleSidebar: () => void, doc: Doc, options: FocusViewOptions) { + if (!sidebarShown) { + options.didMove = true; + toggleSidebar(); + } + options.didMove = sidebar?.makeDocUnfiltered(doc) || options.didMove; + + if (!doc.hidden) { + if (!options.didMove && options.toggleTarget) { + options.toggleTarget = false; + options.didMove = doc.hidden = true; + } + } else { + options.didMove = !(doc.hidden = false); + } + return new Promise>(res => DocumentView.addViewRenderedCb(doc, res)); + } + get sidebarKey() { return this._props.fieldKey + '_sidebar'; } @@ -181,8 +200,8 @@ export class SidebarAnnos extends ObservableReactComponent 'title'; setHeightCallback = (height: number) => this._props.setHeight?.(height + this.filtersHeight()); sortByLinkAnchorY = (a: Doc, b: Doc) => { - const ay = Doc.Links(a).length && DocCast(Doc.Links(a)[0].link_anchor_1).y; - const by = Doc.Links(b).length && DocCast(Doc.Links(b)[0].link_anchor_1).y; + const ay = Doc.Links(a).length && DocCast(Doc.Links(a)[0].link_anchor_1)?.y; + const by = Doc.Links(b).length && DocCast(Doc.Links(b)[0].link_anchor_1)?.y; return NumCast(ay) - NumCast(by); }; render() { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index bb3c59eae..77615c650 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1738,10 +1738,11 @@ export class CollectionFreeFormView extends CollectionSubView([anchor]); + this.dataDoc[fieldKey] = new List([anchor]); } } return anchor; diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx index c984a7053..6c1ad568d 100644 --- a/src/client/views/linking/LinkMenuItem.tsx +++ b/src/client/views/linking/LinkMenuItem.tsx @@ -105,11 +105,7 @@ export class LinkMenuItem extends ObservableReactComponent { LinkManager.Instance.currentLinkAnchor = LinkManager.Instance.currentLink ? this.sourceAnchor : undefined; if ((SnappingManager.PropertiesWidth ?? 0) < 100) { - setTimeout( - action(() => { - SnappingManager.SetPropertiesWidth(250); - }) - ); + setTimeout(action(() => SnappingManager.SetPropertiesWidth(250))); } } }) @@ -136,14 +132,14 @@ export class LinkMenuItem extends ObservableReactComponent { this._props.itemHandler?.(this._props.linkDoc); } else { const focusDoc = - Cast(this._props.linkDoc.link_anchor_1, Doc, null)?.annotationOn === this._props.sourceDoc - ? Cast(this._props.linkDoc.link_anchor_1, Doc, null) - : Cast(this._props.linkDoc.link_anchor_2, Doc, null)?.annotationOn === this._props.sourceDoc - ? Cast(this._props.linkDoc.link_anchor_12, Doc, null) - : undefined; + DocCast(this._props.linkDoc.link_anchor_1)?.annotationOn === this._props.sourceDoc + ? DocCast(this._props.linkDoc.link_anchor_1) + : DocCast(this._props.linkDoc.link_anchor_2)?.annotationOn === this._props.sourceDoc + ? DocCast(this._props.linkDoc.link_anchor_2) + : undefined; // prettier-ignore if (focusDoc) this._props.docView._props.focus(focusDoc, { instant: true }); - DocumentView.FollowLink(this._props.linkDoc, this._props.sourceDoc, false); + DocumentView.FollowLink(this._props.linkDoc, focusDoc ?? this._props.sourceDoc, false); } } ); diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 05706fe6b..b0415ef18 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -1412,7 +1412,7 @@ export class DocumentView extends DocComponent() { } public static setDefaultImageTemplate(checkResult?: boolean) { if (checkResult) return Doc.UserDoc().defaultImageLayout; - const view = DocumentView.Selected()[0]?._props.renderDepth > 0 ? DocumentView.Selected()[0] : undefined; + const view = DocumentView.Selected()[0]?._props.renderDepth > 0 || DocumentView.Selected()[0]?.Document.isTemplateDoc ? DocumentView.Selected()[0] : undefined; undoable(() => { const tempDoc = DocumentView.getTemplate(view); Doc.UserDoc().defaultImageLayout = tempDoc ? new PrefetchProxy(tempDoc) : undefined; diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx index a563b7c1b..a279ccc48 100644 --- a/src/client/views/nodes/MapBox/MapBox.tsx +++ b/src/client/views/nodes/MapBox/MapBox.tsx @@ -13,7 +13,7 @@ import { CirclePicker, ColorResult } from 'react-color'; import { Layer, MapProvider, MapRef, Map as MapboxMap, Marker, Source, ViewState, ViewStateChangeEvent } from 'react-map-gl/mapbox'; import { ClientUtils, setupMoveUpEvents } from '../../../../ClientUtils'; import { emptyFunction } from '../../../../Utils'; -import { Doc, DocListCast, Field, LinkedTo, Opt, StrListCast } from '../../../../fields/Doc'; +import { Doc, DocListCast, Field, LinkedTo, StrListCast } from '../../../../fields/Doc'; import { List } from '../../../../fields/List'; import { RichTextField } from '../../../../fields/RichTextField'; import { DocCast, NumCast, StrCast, toList } from '../../../../fields/Types'; @@ -111,12 +111,12 @@ export class MapBox extends ViewBoxAnnotatableComponent() { // this list contains pushpins and configs @computed get allAnnotations() { return DocListCast(this.dataDoc[this.annotationKey]); } // prettier-ignore - @computed get allSidebarDocs() { return DocListCast(this.dataDoc[this.SidebarKey]); } // prettier-ignore + @computed get allSidebarDocs() { return DocListCast(this.dataDoc[this.sidebarKey]); } // prettier-ignore @computed get allPushpins() { return this.allAnnotations.filter(anno => anno.type === DocumentType.PUSHPIN); } // prettier-ignore @computed get allRoutes() { return this.allAnnotations.filter(anno => anno.type === DocumentType.MAPROUTE); } // prettier-ignore @computed get SidebarShown() { return !!this.layoutDoc._layout_showSidebar; } // prettier-ignore @computed get sidebarWidthPercent() { return StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%'); } // prettier-ignore - @computed get SidebarKey() { return this.fieldKey + '_sidebar'; } // prettier-ignore + @computed get sidebarKey() { return this.fieldKey + '_sidebar'; } // prettier-ignore @computed get sidebarColor() { return StrCast(this.layoutDoc.sidebar_color, StrCast(this.layoutDoc[this._props.fieldKey + '_backgroundColor'], '#e4e4e4')); } @@ -260,7 +260,7 @@ export class MapBox extends ViewBoxAnnotatableComponent() { removeMapDocument = (doc: Doc | Doc[], annotationKey?: string) => { this.allAnnotations - .filter(anno => toList(doc).includes(DocCast(anno.mapPin))) + .filter(anno => toList(doc).includes(DocCast(anno.mapPin)!)) .forEach(anno => { anno.mapPin = undefined; }); @@ -339,27 +339,19 @@ export class MapBox extends ViewBoxAnnotatableComponent() { startAnchorDrag = (e: PointerEvent, ele: HTMLElement) => { e.preventDefault(); e.stopPropagation(); - - const sourceAnchorCreator = action(() => { - const note = this.getAnchor(true); - if (note && this._selectedPinOrRoute) { - note.latitude = this._selectedPinOrRoute.latitude; - note.longitude = this._selectedPinOrRoute.longitude; - note.map = this._selectedPinOrRoute.map; - } - return note as Doc; - }); - const targetCreator = (annotationOn: Doc | undefined) => { const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn, 'yellow'); + target.layout_fitWidth = true; DocumentView.SetSelectOnLoad(target); return target; }; + + const sourceAnchorCreator = () => this.getAnchor(true); const docView = this.DocumentView?.(); docView && DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(docView, sourceAnchorCreator, targetCreator), e.pageX, e.pageY, { dragComplete: dragEv => { - if (!dragEv.aborted && dragEv.annoDragData && dragEv.annoDragData.linkSourceDoc && dragEv.annoDragData.dropDocument && dragEv.linkDocument) { + if (!dragEv.aborted && dragEv.annoDragData?.linkSourceDoc && dragEv.annoDragData.dropDocument && dragEv.linkDocument) { dragEv.annoDragData.linkSourceDoc.followLinkToggle = dragEv.annoDragData.dropDocument.annotationOn === this.Document; dragEv.annoDragData.linkSourceDoc.followLinkZoom = false; } @@ -368,17 +360,7 @@ export class MapBox extends ViewBoxAnnotatableComponent() { }; createNoteAnnotation = () => { - const createFunc = undoable( - action(() => { - const note = this._sidebarRef.current?.anchorMenuClick(this.getAnchor(true), ['latitude', 'longitude', LinkedTo]); - if (note && this._selectedPinOrRoute) { - note.latitude = this._selectedPinOrRoute.latitude; - note.longitude = this._selectedPinOrRoute.longitude; - note.map = this._selectedPinOrRoute.map; - } - }), - 'create note annotation' - ); + const createFunc = undoable(() => this._sidebarRef.current?.anchorMenuClick(this.getAnchor(true), ['latitude', 'longitude', LinkedTo]), 'create note annotation'); if (!this.layoutDoc.layout_showSidebar) { this.toggleSidebar(); setTimeout(createFunc); @@ -428,14 +410,11 @@ export class MapBox extends ViewBoxAnnotatableComponent() { } }; - getView = (doc: Doc, options: FocusViewOptions) => { - if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) { - this.toggleSidebar(); - options.didMove = true; + getView = async (doc: Doc, options: FocusViewOptions) => { + if (DocListCast(this.dataDoc[this.sidebarKey]).find(anno => Doc.AreProtosEqual(doc.layout_unrendered ? DocCast(doc.annotationOn) : doc, anno))) { + SidebarAnnos.getView(this._sidebarRef.current, this.SidebarShown, this.toggleSidebar, doc, options); } - return new Promise>(res => { - DocumentView.addViewRenderedCb(doc, dv => res(dv)); - }); + return undefined; }; /* @@ -476,13 +455,14 @@ export class MapBox extends ViewBoxAnnotatableComponent() { @action deleteSelectedPinOrRoute = undoable(() => { - if (this._selectedPinOrRoute) { + const selPin = DocCast(this._selectedPinOrRoute); + if (selPin) { // Removes filter - Doc.setDocFilter(this.Document, 'latitude', NumCast(this._selectedPinOrRoute.latitude), 'remove'); - Doc.setDocFilter(this.Document, 'longitude', NumCast(this._selectedPinOrRoute.longitude), 'remove'); - Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(DocCast(this._selectedPinOrRoute))}`, 'remove'); + Doc.setDocFilter(this.Document, 'latitude', NumCast(selPin.latitude), 'remove'); + Doc.setDocFilter(this.Document, 'longitude', NumCast(selPin.longitude), 'remove'); + Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(selPin)}`, 'remove'); - this.removePushpinOrRoute(this._selectedPinOrRoute); + this.removePushpinOrRoute(selPin); } MapAnchorMenu.Instance.fadeOut(true); document.removeEventListener('pointerdown', this.tryHideMapAnchorMenu, true); @@ -1299,7 +1279,6 @@ export class MapBox extends ViewBoxAnnotatableComponent() { removeMapDocument = (docsIn: Doc | Doc[], annotationKey?: string) => { const docs = toList(docsIn); this.allAnnotations - .filter(anno => docs.includes(DocCast(anno.mapPin))) + .filter(anno => docs.includes(DocCast(anno.mapPin)!)) .forEach(anno => { anno.mapPin = undefined; }); @@ -224,6 +224,12 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent startAnchorDrag = (e: PointerEvent, ele: HTMLElement) => { e.preventDefault(); e.stopPropagation(); + const targetCreator = (annotationOn: Doc | undefined) => { + const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn, 'yellow'); + target.layout_fitWidth = true; + DocumentView.SetSelectOnLoad(target); + return target; + }; const sourceAnchorCreator = action(() => { const note = this.getAnchor(true); @@ -235,11 +241,6 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent return note as Doc; }); - const targetCreator = (annotationOn: Doc | undefined) => { - const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn, 'yellow'); - DocumentView.SetSelectOnLoad(target); - return target; - }; const docView = this.DocumentView?.(); docView && DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(docView, sourceAnchorCreator, targetCreator), e.pageX, e.pageY, { @@ -362,22 +363,22 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent @action deselectPin = () => { - if (this.selectedPin) { + const selPin = DocCast(this.selectedPin); + if (selPin) { // Removes filter - Doc.setDocFilter(this.Document, 'latitude', NumCast(this.selectedPin.latitude), 'remove'); - Doc.setDocFilter(this.Document, 'longitude', NumCast(this.selectedPin.longitude), 'remove'); - Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(DocCast(this.selectedPin))}`, 'remove'); + Doc.setDocFilter(this.Document, 'latitude', NumCast(selPin.latitude), 'remove'); + Doc.setDocFilter(this.Document, 'longitude', NumCast(selPin.longitude), 'remove'); + Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(selPin)}`, 'remove'); - const temp = this.selectedPin; if (!this._unmounting) { - this._bingMap.current.entities.remove(this.map_docToPinMap.get(temp)); + this._bingMap.current.entities.remove(this.map_docToPinMap.get(selPin)); } - const newpin = new this.MicrosoftMaps.Pushpin(new this.MicrosoftMaps.Location(temp.latitude, temp.longitude)); - this.MicrosoftMaps.Events.addHandler(newpin, 'click', () => this.pushpinClicked(temp as Doc)); + const newpin = new this.MicrosoftMaps.Pushpin(new this.MicrosoftMaps.Location(selPin.latitude, selPin.longitude)); + this.MicrosoftMaps.Events.addHandler(newpin, 'click', () => this.pushpinClicked(selPin)); if (!this._unmounting) { this._bingMap.current.entities.push(newpin); } - this.map_docToPinMap.set(temp, newpin); + this.map_docToPinMap.set(selPin, newpin); this.selectedPin = undefined; this.bingSearchBarContents = this.Document.map; } @@ -388,9 +389,7 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent this.toggleSidebar(); options.didMove = true; } - return new Promise>(res => { - DocumentView.addViewRenderedCb(doc, dv => res(dv)); - }); + return new Promise>(res => DocumentView.addViewRenderedCb(doc, res)); }; /* * Pushpin onclick @@ -535,13 +534,14 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent @action deleteSelectedPin = undoable(() => { - if (this.selectedPin) { + const selPin = this.selectedPin; + if (selPin) { // Removes filter - Doc.setDocFilter(this.Document, 'latitude', NumCast(this.selectedPin.latitude), 'remove'); - Doc.setDocFilter(this.Document, 'longitude', NumCast(this.selectedPin.longitude), 'remove'); - Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(DocCast(this.selectedPin))}`, 'remove'); + Doc.setDocFilter(this.Document, 'latitude', NumCast(selPin.latitude), 'remove'); + Doc.setDocFilter(this.Document, 'longitude', NumCast(selPin.longitude), 'remove'); + Doc.setDocFilter(this.Document, LinkedTo, `mapPin=${Field.toScriptString(selPin)}`, 'remove'); - this.removePushpin(this.selectedPin); + this.removePushpin(selPin); } MapAnchorMenu.Instance.fadeOut(true); document.removeEventListener('pointerdown', this.tryHideMapAnchorMenu, true); @@ -638,7 +638,10 @@ export class MapBoxContainer extends ViewBoxAnnotatableComponent this._disposers.highlight = reaction( () => this.allAnnotations.map(doc => doc[Highlight]), () => { - const allConfigPins = this.allAnnotations.map(doc => ({ doc, pushpin: DocCast(doc.mapPin) })).filter(pair => pair.pushpin); + const allConfigPins = this.allAnnotations + .map(doc => ({ doc, pushpin: DocCast(doc.mapPin) })) + .filter(pair => pair.pushpin) + .map(pair => ({ doc: pair.doc, pushpin: pair.pushpin! })); allConfigPins.forEach(({ pushpin }) => { if (!pushpin[Highlight] && this.map_pinHighlighted.get(pushpin)) { this.recolorPin(pushpin); diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 55e6d5596..e4050a3c3 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -10,7 +10,7 @@ import { DocData } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; import { ComputedField } from '../../../fields/ScriptField'; -import { Cast, FieldValue, NumCast, StrCast, toList } from '../../../fields/Types'; +import { Cast, DocCast, FieldValue, NumCast, StrCast, toList } from '../../../fields/Types'; import { ImageField, PdfField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; import { emptyFunction } from '../../../Utils'; @@ -27,7 +27,6 @@ import { Colors } from '../global/globalEnums'; import { PDFViewer } from '../pdf/PDFViewer'; import { PinDocView, PinProps } from '../PinFuncs'; import { SidebarAnnos } from '../SidebarAnnos'; -import { DocumentView } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; import { FocusViewOptions } from './FocusViewOptions'; import { ImageBox } from './ImageBox'; @@ -56,6 +55,9 @@ export class PDFBox extends ViewBoxAnnotatableComponent() { @observable private _pdf: Opt = undefined; @observable private _pageControls = false; + @computed get sidebarKey() { + return this.fieldKey + '_sidebar'; + } @computed get pdfUrl() { return Cast(this.dataDoc[this._props.fieldKey], PdfField); } @@ -232,14 +234,11 @@ export class PDFBox extends ViewBoxAnnotatableComponent() { return this._pdfViewer?.scrollFocus(anchor, NumCast(anchor.y, NumCast(anchor.config_scrollTop)), options); }; - getView = (doc: Doc, options: FocusViewOptions) => { - if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) { - options.didMove = true; - this.toggleSidebar(false); + getView = async (doc: Doc, options: FocusViewOptions) => { + if (DocListCast(this.dataDoc[this.sidebarKey]).find(anno => Doc.AreProtosEqual(doc.layout_unrendered ? DocCast(doc.annotationOn) : doc, anno))) { + SidebarAnnos.getView(this._sidebarRef.current, this.SidebarShown, () => this.toggleSidebar(false), doc, options); } - return new Promise>(res => { - DocumentView.addViewRenderedCb(doc, dv => res(dv)); - }); + return undefined; }; getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index fa099178c..b3cb0e1db 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -338,12 +338,17 @@ export class VideoBox extends ViewBoxAnnotatableComponent() { getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { const timecode = Cast(this.layoutDoc._layout_currentTimecode, 'number', null); const marquee = AnchorMenu.Instance.GetAnchor?.(undefined, addAsAnnotation); + const docAnchor = () => + Docs.Create.ConfigDocument({ + title: '#' + timecode, + _timecodeToShow: timecode, + annotationOn: this.Document, + }); if (!addAsAnnotation && marquee) marquee.backgroundColor = 'transparent'; - const anchor = - addAsAnnotation && marquee - ? CollectionStackedTimeline.createAnchor(this.Document, this.dataDoc, this.annotationKey, timecode || undefined, undefined, marquee, addAsAnnotation) || this.Document - : Docs.Create.ConfigDocument({ title: '#' + timecode, _timecodeToShow: timecode, annotationOn: this.Document }); + const visibleAnchor = () => addAsAnnotation && marquee && (CollectionStackedTimeline.createAnchor(this.Document, this.dataDoc, this.annotationKey, timecode || undefined, undefined, marquee, addAsAnnotation) || this.Document); + const anchor = visibleAnchor() || docAnchor(); PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), temporal: true, pannable: true } }, this.Document); + addAsAnnotation && this.addDocument(anchor); return anchor; }; @@ -376,9 +381,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent() { } return this._stackedTimeline.getView(doc, options); } - return new Promise>(res => { - DocumentView.addViewRenderedCb(doc, dv => res(dv)); - }); + return new Promise>(res => DocumentView.addViewRenderedCb(doc, res)); }; // extracts video thumbnails and saves them as field of doc diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 838dbea9d..779ccab40 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -13,7 +13,7 @@ import { InkTool } from '../../../fields/InkField'; import { List } from '../../../fields/List'; import { RefField } from '../../../fields/RefField'; import { listSpec } from '../../../fields/Schema'; -import { Cast, NumCast, StrCast, toList, WebCast } from '../../../fields/Types'; +import { Cast, DocCast, NumCast, StrCast, toList, WebCast } from '../../../fields/Types'; import { ImageField, WebField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; import { emptyFunction, stringHash } from '../../../Utils'; @@ -104,6 +104,9 @@ export class WebBox extends ViewBoxAnnotatableComponent() { @computed get webField() { return Cast(this.Document[this._props.fieldKey], WebField)?.url; } + @computed get sidebarKey() { + return this.fieldKey + '_sidebar'; + } constructor(props: FieldViewProps) { super(props); @@ -308,18 +311,17 @@ export class WebBox extends ViewBoxAnnotatableComponent() { }; @action - getView = (doc: Doc /* , options: FocusViewOptions */) => { - if (Doc.AreProtosEqual(doc, this.Document)) - return new Promise>(res => { - res(this.DocumentView?.()); - }); + getView = async (doc: Doc, options: FocusViewOptions) => { + if (Doc.AreProtosEqual(doc, this.Document)) return new Promise>(res => res(this.DocumentView?.())); + if (this.Document.layout_fieldKey === 'layout_icon') this.DocumentView?.().iconify(); const webUrl = WebCast(doc.config_data)?.url; if (this._url && webUrl && webUrl.href !== this._url) this.setData(webUrl.href); - if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) this.toggleSidebar(false); - return new Promise>(res => { - DocumentView.addViewRenderedCb(doc, dv => res(dv)); - }); + + if (DocListCast(this.dataDoc[this.sidebarKey]).find(anno => Doc.AreProtosEqual(doc.layout_unrendered ? DocCast(doc.annotationOn) : doc, anno))) { + return SidebarAnnos.getView(this._sidebarRef.current, this.SidebarShown, () => this.toggleSidebar(false), doc, options); + } + return undefined; }; sidebarAddDocTab = (doc: Doc, where: OpenWhere) => { @@ -454,7 +456,7 @@ export class WebBox extends ViewBoxAnnotatableComponent() { iframeDown = (e: PointerEvent) => { this._textAnnotationCreator = undefined; const sel = this._url ? this._iframe?.contentDocument?.getSelection() : window.document.getSelection(); - if (sel?.empty && !(e.target as any).textContent) + if (sel?.empty && !(e.target as HTMLElement).textContent) sel.empty(); // Chrome else if (sel?.removeAllRanges) sel.removeAllRanges(); // Firefox @@ -565,7 +567,7 @@ export class WebBox extends ViewBoxAnnotatableComponent() { 'click', undoable( action((e: MouseEvent) => { - let eleHref = (e.target as any)?.outerHTML?.split('"="')[1]?.split('"')[0]; + let eleHref = (e.target as HTMLElement)?.outerHTML?.split('"="')[1]?.split('"')[0]; for (let ele = e.target as HTMLElement | Element | null; ele; ele = ele.parentElement) { if ('href' in ele) { eleHref = (typeof ele.href === 'string' ? ele.href : eleHref) || (ele.parentElement && 'href' in ele.parentElement ? (ele.parentElement.href as string) : eleHref); diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 57720baae..bab97f681 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -270,13 +270,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { - const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn); + const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn, 'yellow'); + target.layout_fitWidth = true; DocumentView.SetSelectOnLoad(target); return target; }; + const sourceAnchorCreator = () => this.getAnchor(true); + const docView = this.DocumentView?.(); - docView && DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(docView, () => this.getAnchor(true), targetCreator), e.pageX, e.pageY); + docView && DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(docView, sourceAnchorCreator, targetCreator), e.pageX, e.pageY); }); AnchorMenu.Instance.AddDrawingAnnotation = (drawing: Doc) => { @@ -1070,15 +1073,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { if (DocListCast(this.dataDoc[this.sidebarKey]).find(anno => Doc.AreProtosEqual(doc.layout_unrendered ? DocCast(doc.annotationOn) : doc, anno))) { - if (!this.SidebarShown) { - this.toggleSidebar(false); - options.didMove = true; - } - setTimeout(() => this._sidebarRef?.current?.makeDocUnfiltered(doc)); + return SidebarAnnos.getView(this._sidebarRef.current, this.SidebarShown, () => this.toggleSidebar(false), doc, options); } - return new Promise>(res => { - DocumentView.addViewRenderedCb(doc, dv => res(dv)); - }); + return new Promise>(res => DocumentView.addViewRenderedCb(doc, res)); }; focus = (textAnchor: Doc, options: FocusViewOptions) => { const focusSpeed = options.zoomTime ?? 500; -- cgit v1.2.3-70-g09d2 From 2c26d7c96e56344a1a73a7c6f6dec3d6b0f0ee7e Mon Sep 17 00:00:00 2001 From: bobzel Date: Fri, 16 May 2025 11:46:22 -0400 Subject: changed isGroup to freeform_isGroup to fix issues with groups that are changed into other collection types. fixed toggleTarget links to work when opening in a light box (previously they flickered on, then off). fixed text boxes with a number field as data to act like a number input box. --- src/client/documents/Documents.ts | 2 +- src/client/util/DocumentManager.ts | 1 + src/client/views/DocumentDecorations.tsx | 8 ++-- src/client/views/PropertiesButtons.tsx | 4 +- src/client/views/PropertiesView.tsx | 6 +-- src/client/views/StyleProvider.tsx | 8 ++-- src/client/views/collections/CollectionView.tsx | 2 +- .../collectionFreeForm/CollectionFreeFormView.tsx | 42 +++++++++-------- .../collections/collectionFreeForm/MarqueeView.tsx | 4 +- src/client/views/nodes/DocumentLinksButton.tsx | 1 - src/client/views/nodes/DocumentView.tsx | 2 +- src/client/views/nodes/KeyValueBox.tsx | 8 ++-- .../views/nodes/formattedText/FormattedTextBox.tsx | 12 ++--- src/client/views/nodes/trails/PresBox.tsx | 2 +- src/fields/Doc.ts | 54 +++++++++++++--------- 15 files changed, 85 insertions(+), 71 deletions(-) (limited to 'src/client/util') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 2df6f3e23..9aa1d53c2 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -361,7 +361,6 @@ export class DocumentOptions { isBaseProto?: BOOLt = new BoolInfo('is doc a base level prototype for data documents as opposed to data documents which are prototypes for layout documents. base protos are not cloned during a deep'); isTemplateForField?: string; // the field key for which the containing document is a rendering template isTemplateDoc?: BOOLt = new BoolInfo('is the document a template for creating other documents'); - isGroup?: BOOLt = new BoolInfo('should collection use a grouping UI behavior'); isFolder?: BOOLt = new BoolInfo('is document a tree view folder'); _isTimelineLabel?: BOOLt = new BoolInfo('is document a timeline label'); isLightbox?: BOOLt = new BoolInfo('whether a collection acts as a lightbox by opening lightbox links by hiding all other documents in collection besides link target'); @@ -401,6 +400,7 @@ export class DocumentOptions { // freeform properties freeform?: STRt = new StrInfo(''); + freeform_isGroup?: BOOLt = new BoolInfo('should collection use a grouping UI behavior'); _freeform_backgroundGrid?: BOOLt = new BoolInfo('whether background grid is shown on freeform collections'); _freeform_scale_min?: NUMt = new NumInfo('how far out a view can zoom (used by image/videoBoxes that are clipped'); _freeform_scale_max?: NUMt = new NumInfo('how far in a view can zoom (used by sidebar freeform views'); diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index 422613968..1c2ffab1b 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -274,6 +274,7 @@ export class DocumentManager { // even if we found the document view, if the target is a lightbox, we try to open it in the lightbox to preserve lightbox semantics (eg, there's only one active doc in the lightbox) const target = DocCast(targetDoc.annotationOn, targetDoc)!; const compView = this.getDocumentView(DocCast(target.embedContainer))?.ComponentView; + options.didMove = target.hidden || options.didMove ? true : false; if ((compView?.addDocTab ?? compView?._props.addDocTab)?.(target, options.openLocation)) { await new Promise(waitres => { setTimeout(() => waitres()); diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index ab665e984..1e7f4fb52 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -524,7 +524,7 @@ export class DocumentDecorations extends ObservableReactComponent { const doc = docView.Document; - if (doc.isGroup) { + if (Doc.IsFreeformGroup(doc)) { DocListCast(doc.data) .map(member => DocumentView.getDocumentView(member, docView)!) .forEach(member => this.resizeViewForOutpainting(member, refPt, scale, opts)); @@ -598,14 +598,14 @@ export class DocumentDecorations extends ObservableReactComponent (doc.isGroup ? DocListCast(doc.data).some(this.hasFixedAspect) : !BoolCast(doc._layout_nativeDimEditable)); + hasFixedAspect = (doc: Doc): boolean => (Doc.IsFreeformGroup(doc) ? DocListCast(doc.data).some(this.hasFixedAspect) : !BoolCast(doc._layout_nativeDimEditable)); // // resize a single DocumentView about the specified reference point, possibly setting/updating the native dimensions of the Doc // resizeView = (docView: DocumentView, refPt: number[], scale: { x: number; y: number }, opts: { dragHdl: string; freezeNativeDims: boolean }) => { const doc = docView.Document; - if (doc.isGroup) { + if (Doc.IsFreeformGroup(doc)) { DocListCast(doc.data) .map(member => DocumentView.getDocumentView(member, docView)!) .forEach(member => this.resizeView(member, refPt, scale, opts)); @@ -737,7 +737,7 @@ export class DocumentDecorations extends ObservableReactComponent docView.Document._dragOnlyWithinContainer || docView.Document.isGroup || docView.Document.layout_hideOpenButton) || + DocumentView.Selected().some(docView => docView.Document._dragOnlyWithinContainer || docView.Document.layout_hideOpenButton) || this._isRounding || this._isRotating; const hideDeleteButton = diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx index ded342df0..2c84d7fe7 100644 --- a/src/client/views/PropertiesButtons.tsx +++ b/src/client/views/PropertiesButtons.tsx @@ -238,8 +238,8 @@ export class PropertiesButtons extends React.Component { // on => `Display collection as a Group`, // on => 'object-group', // (dv, doc) => { - // doc.isGroup = !doc.isGroup; - // doc.forceActive = doc.isGroup; + // docfreeform_isGroup = !docfreeform_isGroup; + // doc.forceActive = docfreeform_isGroup; // } // ); // } diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx index acf6f928a..06463b2a2 100644 --- a/src/client/views/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -81,7 +81,7 @@ export class PropertiesView extends ObservableReactComponent, props: Opt, props: Opt, props: Opt, props: Opt Doc) { - if (!Doc.IsSystem(this.Document) && this.Document._type_collection !== CollectionViewType.Docking && !this.dataDoc.isGroup && !this.Document.annotationOn) { + if (!Doc.IsSystem(this.Document) && this.Document._type_collection !== CollectionViewType.Docking && !this.Document.annotationOn) { // prettier-ignore const subItems: ContextMenuProps[] = [ { description: 'Freeform', event: () => func(CollectionViewType.Freeform), icon: 'signature' }, diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 77615c650..3571dab1a 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -176,7 +176,7 @@ export class CollectionFreeFormView extends CollectionSubView { const { pointFocus, zoomTime, didMove } = options; - if (!this.Document.isGroup && pointFocus && !didMove) { + if (!this.Document.freeform_isGroup && pointFocus && !didMove) { const dfltScale = this.isAnnotationOverlay ? 1 : 0.25; if (this.layoutDoc[this.scaleFieldKey] !== dfltScale) { this.zoomSmoothlyAboutPt(this.screenToFreeformContentsXf.transformPoint(pointFocus.X, pointFocus.Y), dfltScale, zoomTime); @@ -380,7 +380,7 @@ export class CollectionFreeFormView extends CollectionSubView { - if (anchor.isGroup && !options.docTransform && options.contextPath?.length) { + if (Doc.IsFreeformGroup(anchor) && !options.docTransform && options.contextPath?.length) { // don't focus on group if there's a context path because we're about to focus on a group item // which will override any group focus. (If we allowed the group to focus, it would mark didMove even if there were no net movement) return undefined; @@ -395,7 +395,7 @@ export class CollectionFreeFormView extends CollectionSubView { - if (this.Document.isGroup || this.Document[(this._props.viewField ?? '_') + 'freeform_noZoom']) return; + if (this.Document.freeform_isGroup || this.Document[(this._props.viewField ?? '_') + 'freeform_noZoom']) return; let deltaScale = deltaY > 0 ? 1 / 1.05 : 1.05; if (deltaScale < 0) deltaScale = -deltaScale; const [x, y] = this.screenToFreeformContentsXf.transformPoint(pointX, pointY); @@ -1298,7 +1298,7 @@ export class CollectionFreeFormView extends CollectionSubView { - if (this.Document.isGroup || !this.isContentActive()) return; // group style collections neither pan nor zoom + if (this.Document.freeform_isGroup || !this.isContentActive()) return; // group style collections neither pan nor zoom SnappingManager.TriggerUserPanned(); if (this.layoutDoc._Transform || this.Document.treeView_OutlineMode === TreeViewType.outline) return; e.stopPropagation(); @@ -1422,7 +1422,7 @@ export class CollectionFreeFormView extends CollectionSubView { const ret = !!this._props.removeDocument?.(docs, annotationKey); // if this is a group and we have fewer than 2 Docs, then just promote what's left to our parent and get rid of the group. - if (ret && DocListCast(this.dataDoc[annotationKey ?? this.fieldKey]).length < 2 && this.Document.isGroup) { + if (ret && DocListCast(this.dataDoc[annotationKey ?? this.fieldKey]).length < 2 && this.Document.freeform_isGroup) { this.promoteCollection(); } return ret; @@ -1548,7 +1548,7 @@ export class CollectionFreeFormView extends CollectionSubView { - if (this.Document.isGroup && this.childDocs.length === this.childDocList?.length) { + if (this.Document.freeform_isGroup && this.childDocs.length === this.childDocList?.length) { const clist = this.childDocs.map(cd => ({ x: NumCast(cd.x), y: NumCast(cd.y), width: NumCast(cd._width), height: NumCast(cd._height) })); return aggregateBounds(clist, NumCast(this.layoutDoc._xMargin), NumCast(this.layoutDoc._yMargin)); } @@ -1929,8 +1929,8 @@ export class CollectionFreeFormView extends CollectionSubView this.updateIcon(), icon: 'compress-arrows-alt' }); this._props.renderDepth && appearanceItems.push({ description: 'Ungroup collection', event: this.promoteCollection, icon: 'table' }); - this.Document.isGroup && this.Document.transcription && appearanceItems.push({ description: 'Ink to text', event: this.transcribeStrokes, icon: 'font' }); + this.Document.freeform_isGroup && this.Document.transcription && appearanceItems.push({ description: 'Ink to text', event: this.transcribeStrokes, icon: 'font' }); !Doc.noviceMode ? appearanceItems.push({ description: 'Arrange contents in grid', event: this.layoutDocsInGrid, icon: 'table' }) : null; !Doc.noviceMode ? appearanceItems.push({ description: (this.Document._freeform_useClusters ? 'Hide' : 'Show') + ' Clusters', event: () => this._clusters.updateClusters(!this.Document._freeform_useClusters), icon: 'braille' }) : null; @@ -2007,7 +2007,7 @@ export class CollectionFreeFormView extends CollectionSubView { - if (this.Document.isGroup && this.Document.transcription) { + if (this.Document.freeform_isGroup && this.Document.transcription) { const text = StrCast(this.Document.transcription); const lines = text.split('\n'); const height = 30 + 15 * lines.length; @@ -2025,7 +2025,7 @@ export class CollectionFreeFormView extends CollectionSubView()) => { if (visited.has(this.Document)) return; visited.add(this.Document); - showGroupDragTarget && (this.GroupChildDrag = BoolCast(this.Document.isGroup)); + showGroupDragTarget && (this.GroupChildDrag = BoolCast(this.Document.freeform_isGroup)); const activeDocs = this.getActiveDocuments(); const size = this.screenToFreeformContentsXf.transformDirection(this._props.PanelWidth(), this._props.PanelHeight()); const selRect = { left: this.panX() - size[0] / 2, top: this.panY() - size[1] / 2, width: size[0], height: size[1] }; @@ -2033,13 +2033,15 @@ export class CollectionFreeFormView extends CollectionSubView intersectRect(docDims(doc), rect); const snappableDocs = activeDocs.filter(doc => doc.z === undefined && isDocInView(doc, selRect)); // first see if there are any foreground docs to snap to - activeDocs.filter(doc => doc.isGroup && SnappingManager.IsResizing !== doc[Id] && !DragManager.docsBeingDragged.includes(doc)).forEach(doc => DocumentView.getDocumentView(doc)?.ComponentView?.dragStarting?.(snapToDraggedDoc, false, visited)); + activeDocs + .filter(doc => Doc.IsFreeformGroup(doc) && SnappingManager.IsResizing !== doc[Id] && !DragManager.docsBeingDragged.includes(doc)) + .forEach(doc => DocumentView.getDocumentView(doc)?.ComponentView?.dragStarting?.(snapToDraggedDoc, false, visited)); const horizLines: number[] = []; const vertLines: number[] = []; const invXf = this.screenToFreeformContentsXf.inverse(); snappableDocs - .filter(doc => !doc.isGroup && (snapToDraggedDoc || (SnappingManager.IsResizing !== doc[Id] && !DragManager.docsBeingDragged.includes(doc)))) + .filter(doc => !Doc.IsFreeformGroup(doc) && (snapToDraggedDoc || (SnappingManager.IsResizing !== doc[Id] && !DragManager.docsBeingDragged.includes(doc)))) .forEach(doc => { const { left, top, width, height } = docDims(doc); const topLeftInScreen = invXf.transformPoint(left, top); @@ -2131,7 +2133,7 @@ export class CollectionFreeFormView extends CollectionSubView 0 ? undefined : this.nudge} addDocTab={this.addDocTab} slowLoadDocuments={this.slowLoadDocuments} diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 3cc7c0f2d..37959a5f0 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -372,7 +372,7 @@ export class MarqueeView extends ObservableReactComponent { doc.$data = new List(selected); - doc.$isGroup = makeGroup; + doc.$freeform_isGroup = makeGroup; doc.$title = makeGroup ? 'grouping' : 'nested freeform'; doc._freeform_panX = doc._freeform_panY = 0; return doc; @@ -508,7 +508,7 @@ export class MarqueeView extends ObservableReactComponent
() { }; createFieldView = (templateDoc: Doc, row: KeyValuePair) => { - const metaKey = row._props.keyName; - const fieldTempDoc = Doc.IsDelegateField(templateDoc, metaKey) ? Doc.MakeDelegate(templateDoc) : Doc.MakeEmbedding(templateDoc); - fieldTempDoc.title = metaKey; + const keyName = row._props.keyName; + const fieldTempDoc = Doc.IsDelegateField(templateDoc, keyName) ? Doc.MakeDelegate(templateDoc) : Doc.MakeEmbedding(templateDoc); + fieldTempDoc.title = keyName; fieldTempDoc.layout_fitWidth = true; fieldTempDoc._xMargin = 10; fieldTempDoc._yMargin = 10; fieldTempDoc._width = 100; fieldTempDoc._height = 40; - fieldTempDoc.layout = this.inferType(templateDoc[metaKey], metaKey); + fieldTempDoc.layout = this.inferType(templateDoc[keyName], keyName); return fieldTempDoc; }; diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index bab97f681..4189fcc0c 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -428,10 +428,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent(); const oldAutoLinks = Doc.Links(this.Document).filter( link => - ((!Doc.isTemplateForField(this.Document) && - ((DocCast(link.link_anchor_1) && !Doc.isTemplateForField(DocCast(link.link_anchor_1)!)) || !Doc.AreProtosEqual(DocCast(link.link_anchor_1), this.Document)) && - ((DocCast(link.link_anchor_2) && !Doc.isTemplateForField(DocCast(link.link_anchor_2)!)) || !Doc.AreProtosEqual(DocCast(link.link_anchor_2), this.Document))) || - (Doc.isTemplateForField(this.Document) && (link.link_anchor_1 === this.Document || link.link_anchor_2 === this.Document))) && + ((!Doc.IsTemplateForField(this.Document) && + ((DocCast(link.link_anchor_1) && !Doc.IsTemplateForField(DocCast(link.link_anchor_1)!)) || !Doc.AreProtosEqual(DocCast(link.link_anchor_1), this.Document)) && + ((DocCast(link.link_anchor_2) && !Doc.IsTemplateForField(DocCast(link.link_anchor_2)!)) || !Doc.AreProtosEqual(DocCast(link.link_anchor_2), this.Document))) || + (Doc.IsTemplateForField(this.Document) && (link.link_anchor_1 === this.Document || link.link_anchor_2 === this.Document))) && link.link_relationship === LinkManager.AutoKeywords ); // prettier-ignore if (this.EditorView?.state.doc.textContent) { @@ -1216,7 +1216,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent= layoutTime ? (protoTime >= dataTime ? protoData : dataData) : layoutTime >= protoTime ? layoutData : protoData; const whichData = recentData ?? (this.layoutDoc.isTemplateDoc ? layoutData : protoData) ?? protoData; - return !whichData ? undefined : { data: RTFCast(whichData), str: Field.toString(DocCast(whichData) ?? StrCast(whichData)) }; + return !whichData ? undefined : { data: RTFCast(whichData), str: Field.toString(DocCast(whichData) ?? NumCast(whichData)?.toString() ?? StrCast(whichData)) }; }, incomingValue => { if (this.EditorView && this.ApplyingChange !== this.fieldKey) { @@ -1767,7 +1767,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent { diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx index 11f35b8ef..cb2a1f13f 100644 --- a/src/client/views/nodes/trails/PresBox.tsx +++ b/src/client/views/nodes/trails/PresBox.tsx @@ -702,7 +702,7 @@ export class PresBox extends ViewBoxBaseComponent() { transTime + 10 ); } - if ((pinDataTypes?.pannable || (!pinDataTypes && (activeItem.config_viewBounds !== undefined || activeItem.config_panX !== undefined || activeItem.config_viewScale !== undefined))) && !bestTarget.isGroup) { + if ((pinDataTypes?.pannable || (!pinDataTypes && (activeItem.config_viewBounds !== undefined || activeItem.config_panX !== undefined || activeItem.config_viewScale !== undefined))) && !Doc.IsFreeformGroup(bestTarget)) { const contentBounds = Cast(activeItem.config_viewBounds, listSpec('number')); if (contentBounds) { const viewport = { panX: (contentBounds[0] + contentBounds[2]) / 2, panY: (contentBounds[1] + contentBounds[3]) / 2, width: contentBounds[2] - contentBounds[0], height: contentBounds[3] - contentBounds[1] }; diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 0d11b9743..e110550e6 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -536,23 +536,35 @@ export namespace Doc { export function GetT(doc: Doc, key: string, ctor: ToConstructor, ignoreProto: boolean = false): FieldResult { return Cast(Get(doc, key, ignoreProto), ctor) as FieldResult; } - export function isTemplateDoc(doc: Doc) { - return GetT(doc, 'isTemplateDoc', 'boolean', true); - } - export function isTemplateForField(doc: Doc) { - return GetT(doc, 'isTemplateForField', 'string', true); - } - export function IsDataProto(doc: Doc) { - return GetT(doc, 'isDataDoc', 'boolean', true); - } - export function IsBaseProto(doc: Doc) { - return GetT(doc, 'isBaseProto', 'boolean', true); - } - export function IsSystem(doc: Doc) { - return GetT(doc, 'isSystem', 'boolean', true); - } - export function IsDelegateField(doc: Doc, fieldKey: string) { - return doc && Get(doc, fieldKey, true) !== undefined; + /** + * Tests whether the Doc is flagged as being a template. + * Templates can be set as a layout for another target Doc. When rendered by the target Doc, the template + * creates an instance of itself that holds rendering data specific to the target + * @param doc + */ + export function IsTemplateDoc(doc: Doc) { return GetT(doc, 'isTemplateDoc', 'boolean', true); } // prettier-ignore + /** + * Tests whether the Doc is flagged as being a template for rendering a specific field of a Doc + * When flagged as field template and rendered, the template will redirect its componentView to write to the + * specified template field. In general, a compound Doc template will contain multiple field templates, one for each of the + * data fields rendered by the compound template. + * @param doc + * @returns + */ + export function IsTemplateForField(doc: Doc) { return GetT(doc, 'isTemplateForField', 'string', true); } // prettier-ignore + export function IsDataProto(doc: Doc) { return GetT(doc, 'isDataDoc', 'boolean', true); } // prettier-ignore + export function IsBaseProto(doc: Doc) { return GetT(doc, 'isBaseProto', 'boolean', true); } // prettier-ignore + export function IsSystem(doc: Doc) { return GetT(doc, 'isSystem', 'boolean', true); } // prettier-ignore + export function IsDelegateField(doc: Doc, fieldKey: string) { return doc && Get(doc, fieldKey, true) !== undefined; } // prettier-ignore + /** + * Tests whether a doc is a freeform collection that renders as a group. + * The group variant of a collection automatically resizes so that none of its contents + * are ever hidden. + * @param doc doc to test for being a freeform group + * @returns boolean + */ + export function IsFreeformGroup(doc: Doc) { + return doc.freeform_isGroup && doc.type_collection === CollectionViewType.Freeform; } // // this will write the value to the key on either the data doc or the embedding doc. The choice @@ -734,7 +746,7 @@ export namespace Doc { const FindDocsInRTF = new RegExp(/(audioId|textId|anchorId|docId)"\s*:\s*"(.*?)"/g); export function makeClone(doc: Doc, cloneMap: Map, linkMap: Map, rtfs: { copy: Doc; key: string; field: RichTextField }[], exclusions: string[], pruneDocs: Doc[], cloneLinks: boolean, cloneTemplates: boolean): Doc { - if (Doc.IsBaseProto(doc) || ((Doc.isTemplateDoc(doc) || Doc.isTemplateForField(doc)) && !cloneTemplates)) { + if (Doc.IsBaseProto(doc) || ((Doc.IsTemplateDoc(doc) || Doc.IsTemplateForField(doc)) && !cloneTemplates)) { return doc; } if (cloneMap.get(doc[Id])) return cloneMap.get(doc[Id])!; @@ -808,7 +820,7 @@ export namespace Doc { const docAtKey = DocCast(clone[key]); if (docAtKey && !Doc.IsSystem(docAtKey)) { if (!Array.from(cloneMap.values()).includes(docAtKey)) { - clone[key] = !cloneTemplates && (Doc.isTemplateDoc(docAtKey) || Doc.isTemplateForField(docAtKey)) ? docAtKey : cloneMap.get(docAtKey[Id]); + clone[key] = !cloneTemplates && (Doc.IsTemplateDoc(docAtKey) || Doc.IsTemplateForField(docAtKey)) ? docAtKey : cloneMap.get(docAtKey[Id]); } else { repairClone(docAtKey, cloneMap, cloneTemplates, visited); } @@ -859,7 +871,7 @@ export namespace Doc { */ export function expandTemplateLayout(templateLayoutDoc: Doc, targetDoc?: Doc, layoutFieldKey?: string) { // nothing to do if the layout isn't a template or we don't have a target that's different than the template - if (!targetDoc || templateLayoutDoc === targetDoc || (!Doc.isTemplateForField(templateLayoutDoc) && !Doc.isTemplateDoc(templateLayoutDoc))) { + if (!targetDoc || templateLayoutDoc === targetDoc || (!Doc.IsTemplateForField(templateLayoutDoc) && !Doc.IsTemplateDoc(templateLayoutDoc))) { return templateLayoutDoc; } @@ -922,7 +934,7 @@ export namespace Doc { console.log('Warning: GetLayoutDataDocPair childDoc not defined'); return { layout: childDoc, data: childDoc }; } - const data = Doc.AreProtosEqual(containerDataDoc, containerDoc) || (!Doc.isTemplateDoc(childDoc) && !Doc.isTemplateForField(childDoc)) ? undefined : containerDataDoc; + const data = Doc.AreProtosEqual(containerDataDoc, containerDoc) || (!Doc.IsTemplateDoc(childDoc) && !Doc.IsTemplateForField(childDoc)) ? undefined : containerDataDoc; const templateRoot = DocCast(containerDoc?.rootDocument); return { layout: Doc.expandTemplateLayout(childDoc, templateRoot, layoutFieldKey), data }; } -- cgit v1.2.3-70-g09d2 From cb2323bd828c564aa0847d6f172c452893f80098 Mon Sep 17 00:00:00 2001 From: bobzel Date: Fri, 16 May 2025 14:03:23 -0400 Subject: made pdf buttons maintain scren size. --- src/client/util/SearchUtil.ts | 36 +++++++++++++++++------------ src/client/views/nodes/LinkDocPreview.tsx | 1 - src/client/views/nodes/PDFBox.scss | 2 +- src/client/views/nodes/PDFBox.tsx | 38 +++++++++++++++++++------------ src/client/views/pdf/PDFViewer.scss | 3 +++ src/client/views/pdf/PDFViewer.tsx | 6 ----- 6 files changed, 49 insertions(+), 37 deletions(-) (limited to 'src/client/util') diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts index fc3bb99ab..17c4af9d1 100644 --- a/src/client/util/SearchUtil.ts +++ b/src/client/util/SearchUtil.ts @@ -8,6 +8,13 @@ import { DocOptions, FInfo } from '../documents/Documents'; export namespace SearchUtil { export type HighlightingResult = { [id: string]: { [key: string]: string[] } }; + /** + * Recursively search all Docs within the collection for the query string. + * @param {Doc} collectionDoc - The collection document to search within. + * @param {string} queryIn - The query string to search for. + * @param {boolean} matchKeyNames - Whether to match metadata field names in addition to field values + * @param {string[]} onlyKeys - Optional: restrict search to only look in the specified array of field names. + */ export function SearchCollection(collectionDoc: Opt, queryIn: string, matchKeyNames: boolean, onlyKeys?: string[]) { const blockedTypes = [DocumentType.PRESSLIDE, DocumentType.CONFIG, DocumentType.KVP, DocumentType.FONTICON, DocumentType.BUTTON, DocumentType.SCRIPTING]; const blockedKeys = matchKeyNames @@ -27,11 +34,13 @@ export namespace SearchUtil { const dtype = StrCast(doc.type) as DocumentType; if (dtype && !blockedTypes.includes(dtype) && !docIDs.includes(doc[Id]) && depth >= 0) { const hlights = new Set(); - (onlyKeys ?? SearchUtil.documentKeys(doc)).forEach( - key => - (val => (exact ? val === query.toLowerCase() : val.includes(query.toLowerCase())))( - matchKeyNames ? key : Field.toString(doc[key] as FieldType)) - && hlights.add(key) + const fieldsToSearch = onlyKeys ?? SearchUtil.documentKeys(doc); + fieldsToSearch.forEach( + key => { + const val = (matchKeyNames ? key : Field.toString(doc[key] as FieldType)).toLowerCase(); + const accept = (exact ? val === query.toLowerCase() : val.includes(query.toLowerCase())); + accept && hlights.add(key); + } ); // prettier-ignore blockedKeys.forEach(key => hlights.delete(key)); @@ -45,18 +54,17 @@ export namespace SearchUtil { return results; } /** - * @param {Doc} doc - doc for which keys are returned + * @param {Doc} doc - Doc to search for used field names * - * This method returns a list of a document doc's keys. + * An array of all field names used by the Doc or its prototypes. */ export function documentKeys(doc: Doc) { - const keys: { [key: string]: boolean } = {}; - Doc.GetAllPrototypes(doc).map(proto => - Object.keys(proto).forEach(key => { - keys[key] = false; - }) - ); - return Array.from(Object.keys(keys)); + return Object.keys(Doc.GetAllPrototypes(doc).reduce( + (keys, proto) => { + Object.keys(proto).forEach(keys.add); + return keys; + }, + new Set())); // prettier-ignore } /** diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx index 5026f52fb..5b07b303a 100644 --- a/src/client/views/nodes/LinkDocPreview.tsx +++ b/src/client/views/nodes/LinkDocPreview.tsx @@ -40,7 +40,6 @@ export class LinkInfo { LinkInfo._instance = this; makeObservable(this); } - // eslint-disable-next-line no-use-before-define @observable public LinkInfo: Opt = undefined; public static get Instance() { diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss index eaea272dc..f09a2630a 100644 --- a/src/client/views/nodes/PDFBox.scss +++ b/src/client/views/nodes/PDFBox.scss @@ -198,7 +198,7 @@ pointer-events: all; .pdfBox-searchBar { - width: 70%; + width: calc(100% - 120px); // less size of search buttons font-size: 14px; } } diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index e4050a3c3..45fa5cc12 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -396,7 +396,15 @@ export class PDFBox extends ViewBoxAnnotatableComponent() { onKeyDown={e => ([KeyCodes.BACKSPACE, KeyCodes.DELETE].includes(e.keyCode) ? e.stopPropagation() : true)} onPointerDown={e => e.stopPropagation()} style={{ display: this._props.isContentActive() ? 'flex' : 'none' }}> -
e.stopPropagation()} style={{ left: `${this._searching ? 0 : 100}%` }}> +
e.stopPropagation()} + style={{ + transformOrigin: 'bottom left', + transform: `scale(${this._props.DocumentView?.().UIBtnScaling || 1})`, + width: `${100 / (this._props.DocumentView?.().UIBtnScaling || 1)}%`, + left: `${this._searching ? 0 : 100}%`, + }}> -
+
99 ? 4 : 3}ch`, pointerEvents: 'all' }} @@ -636,19 +652,11 @@ export class PDFBox extends ViewBoxAnnotatableComponent() { const pdfView = !this._pdf ? null : this.renderPdfView; const href = this.pdfUrl?.url.href; if (!pdfView && href) { - if (PDFBox.pdfcache.get(href)) - setTimeout( - action(() => { - this._pdf = PDFBox.pdfcache.get(href); - }) - ); + if (PDFBox.pdfcache.get(href)) setTimeout(action(() => (this._pdf = PDFBox.pdfcache.get(href)))); else { - if (!PDFBox.pdfpromise.get(href)) PDFBox.pdfpromise.set(href, Pdfjs.getDocument(href).promise); - PDFBox.pdfpromise.get(href)?.then( - action(pdf => { - PDFBox.pdfcache.set(href, (this._pdf = pdf)); - }) - ); + const pdfPromise = PDFBox.pdfpromise.get(href) ?? Pdfjs.getDocument(href).promise; + PDFBox.pdfpromise.set(href, pdfPromise); + pdfPromise.then(action(pdf => PDFBox.pdfcache.set(href, (this._pdf = pdf)))); } } return pdfView ?? this.renderTitleBox; diff --git a/src/client/views/pdf/PDFViewer.scss b/src/client/views/pdf/PDFViewer.scss index 1aab2b853..a54505443 100644 --- a/src/client/views/pdf/PDFViewer.scss +++ b/src/client/views/pdf/PDFViewer.scss @@ -118,6 +118,9 @@ .pdfViewerDash-interactive { pointer-events: all; + &::-webkit-scrollbar { + width: 40px; + } } .loading-spinner { diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index fc2567fbc..97fe183dd 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -359,12 +359,6 @@ export class PDFViewer extends ObservableReactComponent { @action onPointerDown = (e: React.PointerEvent): void => { - // const hit = document.elementFromPoint(e.clientX, e.clientY); - // bcz: Change. drag selecting requires that preventDefault is NOT called. This used to happen in DocumentView, - // but that's changed, so this shouldn't be needed. - // if (hit && hit.localName === "span" && this.annotationsActive(true)) { // drag selecting text stops propagation - // e.button === 0 && e.stopPropagation(); - // } // if alt+left click, drag and annotate this._downX = e.clientX; this._downY = e.clientY; -- cgit v1.2.3-70-g09d2 From 9eb4669477eb03fb404b11eec41e09a93ec15425 Mon Sep 17 00:00:00 2001 From: bobzel Date: Mon, 19 May 2025 10:32:35 -0400 Subject: fix crash from last. --- src/client/util/SearchUtil.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/client/util') diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts index 17c4af9d1..2f23d07dc 100644 --- a/src/client/util/SearchUtil.ts +++ b/src/client/util/SearchUtil.ts @@ -59,9 +59,9 @@ export namespace SearchUtil { * An array of all field names used by the Doc or its prototypes. */ export function documentKeys(doc: Doc) { - return Object.keys(Doc.GetAllPrototypes(doc).reduce( + return Object.keys(Doc.GetAllPrototypes(doc).filter(proto => proto).reduce( (keys, proto) => { - Object.keys(proto).forEach(keys.add); + Object.keys(proto).forEach(keys.add.bind(keys)); return keys; }, new Set())); // prettier-ignore -- cgit v1.2.3-70-g09d2