diff options
Diffstat (limited to 'src')
104 files changed, 1712 insertions, 1671 deletions
diff --git a/src/.DS_Store b/src/.DS_Store Binary files differindex c363efb13..f8d745dbf 100644 --- a/src/.DS_Store +++ b/src/.DS_Store diff --git a/src/Utils.ts b/src/Utils.ts index 852083834..e8bd35ac4 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -514,6 +514,10 @@ export function intersectRect(r1: { left: number; top: number; width: number; he return !(r2.left > r1.left + r1.width || r2.left + r2.width < r1.left || r2.top > r1.top + r1.height || r2.top + r2.height < r1.top); } +export function stringHash(s?: string) { + return !s ? undefined : Math.abs(s.split('').reduce((a: any, b: any) => (a => a & a)((a << 5) - a + b.charCodeAt(0)), 0)); +} + export function percent2frac(percent: string) { return Number(percent.substr(0, percent.length - 1)) / 100; } @@ -848,9 +852,11 @@ export function setupMoveUpEvents( (target as any)._downX = (target as any)._lastX = e.clientX; (target as any)._downY = (target as any)._lastY = e.clientY; (target as any)._noClick = false; + var moving = false; const _moveEvent = (e: PointerEvent): void => { - if (Math.abs(e.clientX - (target as any)._downX) > Utils.DRAG_THRESHOLD || Math.abs(e.clientY - (target as any)._downY) > Utils.DRAG_THRESHOLD) { + if (moving || Math.abs(e.clientX - (target as any)._downX) > Utils.DRAG_THRESHOLD || Math.abs(e.clientY - (target as any)._downY) > Utils.DRAG_THRESHOLD) { + moving = true; if ((target as any)._doubleTime) { clearTimeout((target as any)._doubleTime); (target as any)._doubleTime = undefined; diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index a0870ba43..2d2f5fe4a 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -59,6 +59,7 @@ import { WebBox } from '../views/nodes/WebBox'; import { SearchBox } from '../views/search/SearchBox'; import { CollectionViewType, DocumentType } from './DocumentTypes'; import { CalendarBox } from '../views/nodes/calendarBox/CalendarBox'; +import { OpenWhere } from '../views/nodes/DocumentView'; const { default: { DFLT_IMAGE_NATIVE_DIM } } = require('../views/global/globalCssVariables.module.scss'); // prettier-ignore const defaultNativeImageDim = Number(DFLT_IMAGE_NATIVE_DIM.replace('px', '')); @@ -80,6 +81,7 @@ export class FInfo { this.description = d; this.readOnly = readOnly ?? false; } + searchable = () => true; } class BoolInfo extends FInfo { fieldType? = 'boolean'; @@ -88,6 +90,7 @@ class BoolInfo extends FInfo { super(d); this.filterable = filterable; } + override searchable = () => false; } class NumInfo extends FInfo { fieldType? = 'number'; @@ -97,6 +100,7 @@ class NumInfo extends FInfo { this.values = values; this.filterable = filterable; } + override searchable = () => false; } class StrInfo extends FInfo { fieldType? = 'string'; @@ -115,34 +119,40 @@ class DocInfo extends FInfo { this.values = values; this.filterable = filterable; } + override searchable = () => false; } class DimInfo extends FInfo { fieldType? = 'enumeration'; values? = [DimUnit.Pixel, DimUnit.Ratio]; readOnly = false; filterable = false; + override searchable = () => false; } class PEInfo extends FInfo { fieldType? = 'enumeration'; values? = ['all', 'none']; readOnly = false; filterable = false; + override searchable = () => false; } class DAInfo extends FInfo { fieldType? = 'enumeration'; values? = ['embed', 'copy', 'move', 'same', 'proto', 'none']; readOnly = false; filterable = false; + override searchable = () => false; } class CTypeInfo extends FInfo { fieldType? = 'enumeration'; values? = Array.from(Object.keys(CollectionViewType)); readOnly = false; filterable = false; + override searchable = () => false; } class DTypeInfo extends FInfo { fieldType? = 'enumeration'; values? = Array.from(Object.keys(DocumentType)); + override searchable = () => false; } class DateInfo extends FInfo { fieldType? = 'date'; @@ -210,7 +220,7 @@ export class DocumentOptions { author?: string; // STRt = new StrInfo('creator of document'); // bcz: don't change this. Otherwise, the userDoc's field Infos will have a FieldInfo assigned to its author field which will render it unreadable author_date?: DATEt = new DateInfo('date the document was created', true); annotationOn?: DOCt = new DocInfo('document annotated by this document', false); - _embedContainer?: DOCt = new DocInfo('document that displays (contains) this discument', false); + _embedContainer?: DOCt = new DocInfo('document that displays (contains) this document', false); rootDocument?: DOCt = new DocInfo('document that supplies the information needed for a rendering template (eg, pres slide for PresElement)'); color?: STRt = new StrInfo('foreground color data doc', false); hidden?: BOOLt = new BoolInfo('whether the document is not rendered by its collection', false); @@ -242,6 +252,7 @@ export class DocumentOptions { layout_hideDecorationTitle?: BOOLt = new BoolInfo('whether to suppress the document decortations title when selected'); _layout_hideContextMenu?: BOOLt = new BoolInfo('whether the context menu can be shown'); layout_borderRounding?: string; + _layout_modificationDate?: DATEt = new DateInfo('last modification date of doc layout', false); _layout_nativeDimEditable?: BOOLt = new BoolInfo('native dimensions can be modified using document decoration reizers', false); _layout_reflowVertical?: BOOLt = new BoolInfo('native height can be changed independent of width by dragging decoration resizers'); _layout_reflowHorizontal?: BOOLt = new BoolInfo('whether a doc with a native size can be horizonally resized, causing some form of reflow'); @@ -407,7 +418,7 @@ export class DocumentOptions { onChildClick?: ScriptField; // script given to children of a collection to execute when they are clicked onChildDoubleClick?: ScriptField; // script given to children of a collection to execute when they are double clicked onClickScriptDisable?: STRt = new StrInfo('"always" disable click script, "never" disable click script, or default'); - defaultDoubleClick?: 'ignore' | 'default'; // ignore double clicks, or deafult (undefined) means open document full screen + defaultDoubleClick?: 'ignore' | 'default'; // ignore double clicks, or default (undefined) means open document full screen waitForDoubleClickToClick?: 'always' | 'never' | 'default'; // whether a click function wait for double click to expire. 'default' undefined = wait only if there's a click handler, "never" = never wait, "always" = alway wait onPointerDown?: ScriptField; onPointerUp?: ScriptField; @@ -427,7 +438,7 @@ export class DocumentOptions { clickFactory?: DOCt = new DocInfo('document to create when clicking on a button with a suitable onClick script', false); onDragStart?: ScriptField; //script to execute at start of drag operation -- e.g., when a "creator" button is dragged this script generates a different document to drop target?: Doc; // available for use in scripts. used to provide a document parameter to the script (Note, this is a convenience entry since any field could be used for parameterizing a script) - + tags?: LISTt = new ListInfo('hashtags added to document, typically using a text view', true); treeView_HideTitle?: BOOLt = new BoolInfo('whether to hide the top document title of a tree view'); treeView_HideUnrendered?: BOOLt = new BoolInfo("tells tree view not to display documents that have an 'layout_unrendered' tag unless they also have a treeView_FieldKey tag (presBox)"); treeView_HideHeaderIfTemplate?: BOOLt = new BoolInfo('whether to hide the header for a document in a tree view only if a childLayoutTemplate is provided (presBox)'); @@ -584,13 +595,12 @@ export namespace Docs { layout: { view: LinkBox, dataField: 'link' }, options: { childDontRegisterViews: true, - onClick: FollowLinkScript(), layout_hideLinkAnchors: true, _height: 1, _width: 1, link: '', link_description: '', - backgroundColor: 'lightblue', // lightblue is default color for linking dot and link documents text comment area + color: 'lightBlue', // lightblue is default color for linking dot and link documents text comment area _dropPropertiesToRemove: new List(['onClick']), }, }, @@ -1102,7 +1112,7 @@ export namespace Docs { I.stroke_isInkMask = isInkMask; I.text_align = 'center'; I.rotation = 0; - I.defaultDoubleClick = 'click'; + I.defaultDoubleClick = 'ignore'; I.author_date = new DateField(); I['acl-Guest'] = Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.View; //I['acl-Override'] = SharingPermissions.Unset; @@ -1449,7 +1459,7 @@ export namespace DocUtils { const makeLink = action((linkDoc: Doc, showPopup?: number[]) => { if (showPopup) { - LinkManager.currentLink = linkDoc; + LinkManager.Instance.currentLink = linkDoc; TaskCompletionBox.textDisplayed = 'Link Created'; TaskCompletionBox.popupX = showPopup[0]; @@ -1657,10 +1667,37 @@ export namespace DocUtils { } export function addDocumentCreatorMenuItems(docTextAdder: (d: Doc) => void, docAdder: (d: Doc) => void, x: number, y: number, simpleMenu: boolean = false, pivotField?: string, pivotValue?: string): void { + const documentList: ContextMenuProps[] = DocListCast(DocListCast(Doc.MyTools?.data)[0]?.data) + .filter(btnDoc => !btnDoc.hidden) + .map(btnDoc => Cast(btnDoc?.dragFactory, Doc, null)) + .filter(doc => doc && doc !== Doc.UserDoc().emptyTrail && doc.title) + .map((dragDoc, i) => ({ + description: ':' + StrCast(dragDoc.title).replace('Untitled ', ''), + event: undoable((args: { x: number; y: number }) => { + const newDoc = DocUtils.copyDragFactory(dragDoc); + if (newDoc) { + newDoc.author = Doc.CurrentUserEmail; + newDoc.x = x; + newDoc.y = y; + EquationBox.SelectOnLoad = newDoc[Id]; + if (newDoc.type === DocumentType.RTF) FormattedTextBox.SetSelectOnLoad(newDoc); + if (pivotField) { + newDoc[pivotField] = pivotValue; + } + docAdder?.(newDoc); + } + }, StrCast(dragDoc.title)), + icon: Doc.toIcon(dragDoc), + })) as ContextMenuProps[]; + ContextMenu.Instance.addItem({ + description: 'Create document', + subitems: documentList, + icon: 'file', + }); !simpleMenu && ContextMenu.Instance.addItem({ - description: 'Quick Notes', - subitems: DocListCast((Doc.UserDoc()['template_notes'] as Doc).data).map((note, i) => ({ + description: 'Styled Notes', + subitems: DocListCast((Doc.UserDoc().template_notes as Doc).data).map((note, i) => ({ description: ':' + StrCast(note.title), event: undoable((args: { x: number; y: number }) => { const textDoc = Docs.Create.TextDocument('', { @@ -1681,14 +1718,14 @@ export namespace DocUtils { })) as ContextMenuProps[], icon: 'sticky-note', }); - const documentList: ContextMenuProps[] = DocListCast(DocListCast(Doc.MyTools?.data)[0]?.data) + const userDocList: ContextMenuProps[] = DocListCast(DocListCast(Doc.MyTools?.data)[1]?.data) .filter(btnDoc => !btnDoc.hidden) .map(btnDoc => Cast(btnDoc?.dragFactory, Doc, null)) .filter(doc => doc && doc !== Doc.UserDoc().emptyTrail && doc !== Doc.UserDoc().emptyNote && doc.title) .map((dragDoc, i) => ({ description: ':' + StrCast(dragDoc.title).replace('Untitled ', ''), event: undoable((args: { x: number; y: number }) => { - const newDoc = DocUtils.copyDragFactory(dragDoc); + const newDoc = DocUtils.delegateDragFactory(dragDoc); if (newDoc) { newDoc.author = Doc.CurrentUserEmail; newDoc.x = x; @@ -1704,8 +1741,8 @@ export namespace DocUtils { icon: Doc.toIcon(dragDoc), })) as ContextMenuProps[]; ContextMenu.Instance.addItem({ - description: 'Create document', - subitems: documentList, + description: 'User Templates', + subitems: userDocList, icon: 'file', }); } // applies a custom template to a document. the template is identified by it's short name (e.g, slideView not layout_slideView) @@ -1796,6 +1833,24 @@ export namespace DocUtils { return newCollection; } } + export function makeIntoPortal(doc: Doc, layoutDoc: Doc, allLinks: Doc[]) { + const portalLink = allLinks.find(d => d.link_anchor_1 === doc && d.link_relationship === 'portal to:portal from'); + if (!portalLink) { + DocUtils.MakeLink( + doc, + Docs.Create.FreeformDocument([], { + _width: NumCast(layoutDoc._width) + 10, + _height: Math.max(NumCast(layoutDoc._height), NumCast(layoutDoc._width) + 10), + _isLightbox: true, + _layout_fitWidth: true, + title: StrCast(doc.title) + ' [Portal]', + }), + { link_relationship: 'portal to:portal from' } + ); + } + doc.followLinkLocation = OpenWhere.lightbox; + doc.onClick = FollowLinkScript(); + } export function LeavePushpin(doc: Doc, annotationField: string) { if (doc.followLinkToggle) return undefined; diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 07ee777cd..714e33d25 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -17,7 +17,7 @@ import { CollectionViewType, DocumentType } from "../documents/DocumentTypes"; import { DocUtils, Docs, DocumentOptions, FInfo } from "../documents/Documents"; import { DashboardView } from "../views/DashboardView"; import { OverlayView } from "../views/OverlayView"; -import { TreeViewType } from "../views/collections/CollectionTreeView"; +import { CollectionTreeView, TreeViewType } from "../views/collections/CollectionTreeView"; import { Colors } from "../views/global/globalEnums"; import { media_state } from "../views/nodes/AudioBox"; import { OpenWhere } from "../views/nodes/DocumentView"; @@ -61,49 +61,16 @@ export let resolvedPorts: { server: number, socket: number }; export class CurrentUserUtils { // initializes experimental advanced template views - slideView, headerView - static setupExperimentalTemplateButtons(doc: Doc, tempDocs?:Doc) { - const requiredTypeNameFields:{btnOpts:DocumentOptions, templateOpts:DocumentOptions, template:(opts:DocumentOptions) => Doc}[] = [ - { - btnOpts: { title: "slide", icon: "address-card" }, - templateOpts: { _width: 400, _height: 300, title: "slideView", _xMargin: 3, _yMargin: 3, isSystem: true }, - template: (opts:DocumentOptions) => Docs.Create.MultirowDocument( - [ - Docs.Create.MulticolumnDocument([], { title: "hero", _height: 200, isSystem: true }), - Docs.Create.TextDocument("", { title: "text", _layout_fitWidth:true, _height: 100, isSystem: true, _text_fontFamily: StrCast(Doc.UserDoc().fontFamily), _text_fontSize: StrCast(Doc.UserDoc().fontSize) }) - ], opts) - }, - { - btnOpts: { title: "mobile", icon: "mobile" }, - templateOpts: { title: "NEW MOBILE BUTTON", onClick: undefined, }, - template: (opts:DocumentOptions) => this.mobileButton(opts, - [this.createToolButton({ ignoreClick: true, icon: "mobile", backgroundColor: "transparent" }), - this.mobileTextContainer({}, - [this.mobileButtonText({}, "NEW MOBILE BUTTON"), this.mobileButtonInfo({}, "You can customize this button and make it your own.")]) - ] - ) - }, - ]; - const requiredTypes = requiredTypeNameFields.map(({ btnOpts, template, templateOpts }) => { - const tempBtn = DocListCast(tempDocs?.data)?.find(doc => doc.title === btnOpts.title); - const reqdScripts = { onDragStart: '{ return copyDragFactory(this.dragFactory,this.openFactoryAsDelegate); }' }; - const assignBtnAndTempOpts = (templateBtn:Opt<Doc>, btnOpts:DocumentOptions, templateOptions:DocumentOptions) => { - if (templateBtn) { - DocUtils.AssignOpts(templateBtn,btnOpts); - DocUtils.AssignDocField(templateBtn, "dragFactory", opts => template(opts), templateOptions); - } - return templateBtn; - }; - return DocUtils.AssignScripts(assignBtnAndTempOpts(tempBtn, btnOpts, templateOpts) ?? this.createToolButton( {...btnOpts, dragFactory: MakeTemplate(template(templateOpts))}), reqdScripts); - }); - + static setupUserDocumentCreatorButtons(doc: Doc, userDocTemplates: Opt<Doc>) { + const userTemplates = DocListCast(userDocTemplates?.data).filter(doc => !Doc.IsSystem(doc)); const reqdOpts:DocumentOptions = { - title: "Experimental Tools", _xMargin: 0, _layout_showTitle: "title", _chromeHidden: true, + title: "User Tools", _xMargin: 0, _layout_showTitle: "title", _chromeHidden: true, hidden: false, _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, isSystem: true, _forceActive: true, _layout_autoHeight: true, _width: 500, _height: 300, _layout_fitWidth: true, _columnWidth: 35, ignoreClick: true, _lockedPosition: true, }; const reqdScripts = { dropConverter : "convertToButtons(dragData)" }; - const reqdFuncs = { hidden: "IsNoviceMode()" }; - return DocUtils.AssignScripts(DocUtils.AssignOpts(tempDocs, reqdOpts, requiredTypes) ?? Docs.Create.MasonryDocument(requiredTypes, reqdOpts), reqdScripts, reqdFuncs); + const reqdFuncs = { /* hidden: "IsNoviceMode()" */ }; + return DocUtils.AssignScripts(DocUtils.AssignOpts(userDocTemplates, reqdOpts, userTemplates) ?? Docs.Create.MasonryDocument(userTemplates, reqdOpts), reqdScripts, reqdFuncs); } /// Initializes templates for editing click funcs of a document @@ -111,7 +78,7 @@ export class CurrentUserUtils { const tempClicks = DocCast(doc[field]); const reqdClickOpts:DocumentOptions = {_width: 300, _height:200, isSystem: true}; const reqdTempOpts:{opts:DocumentOptions, script: string}[] = [ - { opts: { title: "Open In Target", targetScriptKey: "onChildClick"}, script: "docCastAsync(documentView?.containerViewPath().lastElement()?.Document.target).then((target) => target && (target.proto.data = new List([self])))"}, + { opts: { title: "Open In Target", targetScriptKey: "onChildClick"}, script: "docCastAsync(documentView?.containerViewPath().lastElement()?.Document.target).then((target) => target && (target.proto.data = new List([this])))"}, { opts: { title: "Open Detail On Right", targetScriptKey: "onChildDoubleClick"}, script: `openDoc(this.doubleClickView.${OpenWhere.addRight})`}]; const reqdClickList = reqdTempOpts.map(opts => { const allOpts = {...reqdClickOpts, ...opts.opts}; @@ -148,7 +115,7 @@ export class CurrentUserUtils { static setupNoteTemplates(doc: Doc, field="template_notes") { const tempNotes = DocCast(doc[field]); const reqdTempOpts:DocumentOptions[] = [ - { noteType: "Note", backgroundColor: "yellow", icon: "sticky-note"}, + { noteType: "Postit", backgroundColor: "yellow", icon: "sticky-note"}, { noteType: "Idea", backgroundColor: "pink", icon: "lightbulb" }, { noteType: "Topic", backgroundColor: "lightblue", icon: "book-open" }]; const reqdNoteList = reqdTempOpts.map(opts => { @@ -201,7 +168,7 @@ export class CurrentUserUtils { makeIconTemplate(DocumentType.AUDIO, "title", { iconTemplate:DocumentType.LABEL, backgroundColor: "lightgreen"}), makeIconTemplate(DocumentType.PDF, "title", { iconTemplate:DocumentType.LABEL, backgroundColor: "pink"}), makeIconTemplate(DocumentType.WEB, "title", { iconTemplate:DocumentType.LABEL, backgroundColor: "brown"}), - makeIconTemplate(DocumentType.RTF, "text", { iconTemplate:DocumentType.LABEL, _layout_showTitle: "author_date"}), + makeIconTemplate(DocumentType.RTF, "text", { iconTemplate:DocumentType.LABEL, _layout_showTitle: "title"}), makeIconTemplate(DocumentType.IMG, "data", { iconTemplate:DocumentType.IMG, _height: undefined}), makeIconTemplate(DocumentType.COL, "icon", { iconTemplate:DocumentType.IMG}), makeIconTemplate(DocumentType.COL, "icon", { iconTemplate:DocumentType.IMG}), @@ -257,6 +224,16 @@ export class CurrentUserUtils { MakeTemplate(Doc.GetProto(header), true, "Untitled Header"); return header; } + const slideView = (opts:DocumentOptions) => { + const slide = Docs.Create.MultirowDocument( + [ + Docs.Create.MulticolumnDocument([], { title: "hero", _height: 200, isSystem: true }), + Docs.Create.TextDocument("", { title: "text", _layout_fitWidth:true, _height: 100, isSystem: true, _text_fontFamily: StrCast(Doc.UserDoc().fontFamily), _text_fontSize: StrCast(Doc.UserDoc().fontSize) }) + ], opts); + + MakeTemplate(Doc.GetProto(slide), true, "Untitled Slide View"); + return slide; + } const emptyThings:{key:string, // the field name where the empty thing will be stored opts:DocumentOptions, // the document options that are required for the empty thing funcs?:{[key:string]: any}, // computed fields that are rquired for the empth thing @@ -279,6 +256,7 @@ export class CurrentUserUtils { {key: "Script", creator: opts => Docs.Create.ScriptingDocument(null, opts), opts: { _width: 200, _height: 250, }}, {key: "DataViz", creator: opts => Docs.Create.DataVizDocument("/users/rz/Downloads/addresses.csv", opts), opts: { _width: 300, _height: 300 }}, {key: "Header", creator: headerTemplate, opts: { _width: 300, _height: 70, _headerPointerEvents: "all", _headerHeight: 12, _headerFontSize: 9, _layout_autoHeight: true, treeView_HideUnrendered: true}}, + {key: "ViewSlide", creator: slideView, opts: { _width: 400, _height: 300, _xMargin: 3, _yMargin: 3,}}, {key: "Trail", creator: Docs.Create.PresDocument, opts: { _width: 400, _height: 30, _type_collection: CollectionViewType.Stacking, dropAction: "embed" as dropActionType, treeView_HideTitle: true, _layout_fitWidth:true, layout_boxShadow: "0 0" }}, {key: "Tab", creator: opts => Docs.Create.FreeformDocument([], opts), opts: { _width: 500, _height: 800, _layout_fitWidth: true, _freeform_backgroundGrid: true, }}, {key: "Slide", creator: opts => Docs.Create.TreeDocument([], opts), opts: { _width: 300, _height: 200, _type_collection: CollectionViewType.Tree, @@ -307,6 +285,7 @@ export class CurrentUserUtils { { toolTip: "Tap or drag to create a scripting box", title: "Script", icon: "terminal", dragFactory: doc.emptyScript as Doc, clickFactory: DocCast(doc.emptyScript), funcs: { hidden: "IsNoviceMode()"}}, { toolTip: "Tap or drag to create a data viz node", title: "DataViz", icon: "chart-bar", dragFactory: doc.emptyDataViz as Doc, clickFactory: DocCast(doc.emptyDataViz)}, { toolTip: "Tap or drag to create a bullet slide", title: "PPT Slide", icon: "file", dragFactory: doc.emptySlide as Doc, clickFactory: DocCast(doc.emptySlide), openFactoryLocation: OpenWhere.overlay, funcs: { hidden: "IsNoviceMode()"}}, + { toolTip: "Tap or drag to create a view slide", title: "View Slide", icon: "address-card", dragFactory: doc.emptyViewSlide as Doc,clickFactory: DocCast(doc.emptyViewSlide),openFactoryLocation: OpenWhere.overlay,funcs: { hidden: "IsNoviceMode()"}}, { toolTip: "Tap or drag to create a data note", title: "DataNote", icon: "window-maximize",dragFactory: doc.emptyHeader as Doc,clickFactory: DocCast(doc.emptyHeader), openFactoryAsDelegate: true, funcs: { hidden: "IsNoviceMode()"} }, { toolTip: "Toggle a Calculator REPL", title: "replviewer", icon: "calculator", clickFactory: '<ScriptingRepl />' as any, openFactoryLocation: OpenWhere.overlay}, // hack: clickFactory is not a Doc but will get interpreted as a custom UI by the openDoc() onClick script // { toolTip: "Toggle an UndoStack", title: "undostacker", icon: "calculator", clickFactory: "<UndoStack />" as any, openFactoryLocation: OpenWhere.overlay}, @@ -330,7 +309,7 @@ export class CurrentUserUtils { }); const reqdOpts:DocumentOptions = { - title: "Basic Item Creators", _layout_showTitle: "title", _xMargin: 0, _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, _chromeHidden: true, isSystem: true, + title: "Document Creators", _layout_showTitle: "title", _xMargin: 0, _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, _chromeHidden: true, isSystem: true, _layout_autoHeight: true, _width: 500, _height: 300, _layout_fitWidth: true, _columnWidth: 40, ignoreClick: true, _lockedPosition: true, _forceActive: true, childDragAction: 'embed' }; @@ -464,14 +443,17 @@ export class CurrentUserUtils { /// Initializes the panel of draggable tools that is opened from the left sidebar. static setupToolsBtnPanel(doc: Doc, field:string) { const myTools = DocCast(doc[field]); - const creatorBtns = CurrentUserUtils.setupCreatorButtons(doc, DocListCast(myTools?.data)?.length ? DocListCast(myTools.data)[0]:undefined); - const templateBtns = CurrentUserUtils.setupExperimentalTemplateButtons(doc,DocListCast(myTools?.data)?.length > 1 ? DocListCast(myTools.data)[1]:undefined); + const allTools = DocListCast(myTools?.data); + const creatorBtns = CurrentUserUtils.setupCreatorButtons(doc, allTools?.length ? allTools[0]:undefined); + const userTools = allTools && allTools?.length > 1 ? allTools[1]:undefined; + const userBtns = CurrentUserUtils.setupUserDocumentCreatorButtons(doc, userTools); + //doc.myUserBtns = new PrefetchProxy(userBtns); const reqdToolOps:DocumentOptions = { title: "My Tools", isSystem: true, ignoreClick: true, layout_boxShadow: "0 0", layout_explainer: "This is a palette of documents that can be created.", _layout_showTitle: "title", _width: 500, _yMargin: 20, _lockedPosition: true, _forceActive: true, _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, _chromeHidden: true, }; - return DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.StackingDocument(items??[], opts), reqdToolOps, [creatorBtns, templateBtns]); + return DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.StackingDocument(items??[], opts), reqdToolOps, [creatorBtns, userBtns]); } /// initializes the left sidebar dashboard pane @@ -520,29 +502,21 @@ export class CurrentUserUtils { static setupFilesystem(doc: Doc, field:string) { var myFilesystem = DocCast(doc[field]); - const newFolder = `TreeView_addNewFolder()`; const newFolderOpts: DocumentOptions = { - _forceActive: true, _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, _width: 30, _height: 30, undoIgnoreFields:new List<string>(['treeView_SortCriterion']), + _forceActive: true, _dragOnlyWithinContainer: true, _embedContainer: Doc.MyFilesystem, _layout_hideContextMenu: true, _width: 30, _height: 30, undoIgnoreFields:new List<string>(['treeView_SortCriterion']), title: "New folder", color: Colors.BLACK, btnType: ButtonType.ClickButton, toolTip: "Create new folder", buttonText: "New folder", icon: "folder-plus", isSystem: true }; - const newFolderScript = { onClick: newFolder}; + const newFolderScript = { onClick: CollectionTreeView.AddTreeFunc}; const newFolderButton = DocUtils.AssignScripts(DocUtils.AssignOpts(DocCast(myFilesystem?.layout_headerButton), newFolderOpts) ?? Docs.Create.FontIconDocument(newFolderOpts), newFolderScript); const reqdOpts:DocumentOptions = { _layout_showTitle: "title", _height: 100, _forceActive: true, title: "My Documents", layout_headerButton: newFolderButton, treeView_HideTitle: true, dropAction: 'add', isSystem: true, isFolder: true, treeView_Type: TreeViewType.fileSystem, childHideLinkButton: true, layout_boxShadow: "0 0", childDontRegisterViews: true, treeView_TruncateTitleWidth: 350, ignoreClick: true, childDragAction: "embed", - childContextMenuLabels: new List<string>(["Create new folder"]), - childContextMenuIcons: new List<string>(["plus"]), layout_explainer: "This is your file manager where you can create folders to keep track of documents independently of your dashboard." }; const fileFolders = new Set(DocListCast(DocCast(doc[field])?.data)); - myFilesystem = DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.TreeDocument(items??[], opts), reqdOpts, Array.from(fileFolders)); - const childContextMenuScripts = [newFolder]; - if (Cast(myFilesystem.childContextMenuScripts, listSpec(ScriptField), null)?.length !== childContextMenuScripts.length) { - myFilesystem.childContextMenuScripts = new List<ScriptField>(childContextMenuScripts.map(script => ScriptField.MakeFunction(script)!)); - } - return myFilesystem; + return DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.TreeDocument(items??[], opts), reqdOpts, Array.from(fileFolders)); } /// initializes the panel displaying docs that have been recently closed @@ -562,8 +536,8 @@ export class CurrentUserUtils { toolTip: "Empty recently closed",}; DocUtils.AssignDocField(recentlyClosed, "layout_headerButton", (opts) => Docs.Create.FontIconDocument(opts), clearBtnsOpts, undefined, {onClick: clearAll("this.target")}); - if (!Cast(recentlyClosed.contextMenuScripts, listSpec(ScriptField),null)?.find((script) => script?.script.originalScript === clearAll("self"))) { - recentlyClosed.contextMenuScripts = new List<ScriptField>([ScriptField.MakeScript(clearAll("self"))!]) + if (!Cast(recentlyClosed.contextMenuScripts, listSpec(ScriptField),null)?.find((script) => script?.script.originalScript === clearAll("this"))) { + recentlyClosed.contextMenuScripts = new List<ScriptField>([ScriptField.MakeScript(clearAll("this"))!]) } return recentlyClosed; } @@ -604,12 +578,12 @@ export class CurrentUserUtils { CurrentUserUtils.createToolButton(opts), scripts, funcs); const btnDescs = [// setup reactions to change the highlights on the undo/redo buttons -- would be better to encode this in the undo/redo buttons, but the undo/redo stacks are not wired up that way yet - { scripts: { onClick: "undo()"}, opts: { title: "Undo", icon: "undo-alt", toolTip: "Undo ⌘Z" }}, - { scripts: { onClick: "redo()"}, opts: { title: "Redo", icon: "redo-alt", toolTip: "Redo ⌘⇧Z" }}, - { scripts: { }, opts: { title: "undoStack", layout: "<UndoStack>", toolTip: "Undo/Redo Stack"}}, // note: layout fields are hacks -- they don't actually run through the JSX parser (yet) - { scripts: { }, opts: { title: "linker", layout: "<LinkingUI>", toolTip: "link started"}}, - { scripts: { }, opts: { title: "currently playing", layout: "<CurrentlyPlayingUI>", toolTip: "currently playing media"}}, - { scripts: { }, opts: { title: "Branching", layout: "<Branching>", toolTip: "Branch, baby!"}} + { scripts: { onClick: "undo()"}, opts: { title: "Undo", icon: "undo-alt", toolTip: "Undo ⌘Z" }}, + { scripts: { onClick: "redo()"}, opts: { title: "Redo", icon: "redo-alt", toolTip: "Redo ⌘⇧Z" }}, + { scripts: { }, opts: { title: "undoStack", layout: "<UndoStack>", toolTip: "Undo/Redo Stack"}}, // note: layout fields are hacks -- they don't actually run through the JSX parser (yet) + { scripts: { }, opts: { title: "linker", layout: "<LinkingUI>", toolTip: "link started"}}, + { scripts: { }, opts: { title: "currently playing", layout: "<CurrentlyPlayingUI>", toolTip: "currently playing media"}}, + { scripts: { }, opts: { title: "Branching", layout: "<Branching>", toolTip: "Branch, baby!"}} ]; const btns = btnDescs.map(desc => dockBtn({_width: 30, _height: 30, defaultDoubleClick: 'ignore', undoIgnoreFields: new List<string>(['opacity']), _dragOnlyWithinContainer: true, ...desc.opts}, desc.scripts)); const dockBtnsReqdOpts:DocumentOptions = { diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index f730d17fe..a38a330da 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -1,6 +1,6 @@ import { Howl } from 'howler'; import { action, computed, makeObservable, observable, ObservableSet, observe } from 'mobx'; -import { Doc, DocListCast, Opt } from '../../fields/Doc'; +import { Doc, Opt } from '../../fields/Doc'; import { AclAdmin, AclEdit, Animation, DocData } from '../../fields/DocSymbols'; import { Id } from '../../fields/FieldSymbols'; import { listSpec } from '../../fields/Schema'; @@ -28,8 +28,6 @@ export class DocumentManager { //global holds all of the nodes (regardless of which collection they're in) @observable _documentViews = new Set<DocumentView>(); @observable.shallow public CurrentlyLoading: Doc[] = []; - @observable.shallow public LinkAnchorBoxViews: DocumentView[] = []; - @observable.shallow public LinkedDocumentViews: { a: DocumentView; b: DocumentView; l: Doc }[] = []; @computed public get DocumentViews() { return Array.from(this._documentViews).filter(view => !(view.ComponentView instanceof KeyValueBox) && (!LightboxView.LightboxDoc || LightboxView.Contains(view))); } @@ -90,37 +88,14 @@ export class DocumentManager { @action public AddView = (view: DocumentView) => { - if (view._props.LayoutTemplateString?.includes(KeyValueBox.name)) return; - if (view._props.LayoutTemplateString?.includes(LinkAnchorBox.name)) { - const viewAnchorIndex = view._props.LayoutTemplateString.includes('link_anchor_2') ? 'link_anchor_2' : 'link_anchor_1'; - const link = view.Document; - this.LinkAnchorBoxViews?.filter(dv => Doc.AreProtosEqual(dv.Document, link) && !dv._props.LayoutTemplateString?.includes(viewAnchorIndex)).forEach(otherView => - this.LinkedDocumentViews.push({ - a: viewAnchorIndex === 'link_anchor_2' ? otherView : view, - b: viewAnchorIndex === 'link_anchor_2' ? view : otherView, - l: link, - }) - ); - this.LinkAnchorBoxViews.push(view); - } else { + if (!view._props.LayoutTemplateString?.includes(KeyValueBox.name) && + !view._props.LayoutTemplateString?.includes(LinkAnchorBox.name)) { this.AddDocumentView(view); - } - this.callAddViewFuncs(view); + this.callAddViewFuncs(view); + } // prettier-ignore }; public RemoveView = action((view: DocumentView) => { - this.LinkedDocumentViews.slice().forEach( - action(pair => { - if (pair.a === view || pair.b === view) { - const li = this.LinkedDocumentViews.indexOf(pair); - li !== -1 && this.LinkedDocumentViews.splice(li, 1); - } - }) - ); - - if (view._props.LayoutTemplateString?.includes(LinkAnchorBox.name)) { - const index = this.LinkAnchorBoxViews.indexOf(view); - this.LinkAnchorBoxViews.splice(index, 1); - } else { + if (!view._props.LayoutTemplateString?.includes(KeyValueBox.name) && !view._props.LayoutTemplateString?.includes(LinkAnchorBox.name)) { this.DeleteDocumentView(view); } SelectionManager.DeselectView(view); @@ -234,6 +209,20 @@ export class DocumentManager { finished?.(); }; + public static LinkCommonAncestor(linkDoc: Doc) { + const anchor = (which: number) => { + const anch = DocCast(linkDoc['link_anchor_' + which]); + const anchor = anch?.layout_unrendered ? DocCast(anch.annotationOn) : anch; + return DocumentManager.Instance.getDocumentView(anchor); + }; + const anchor1 = anchor(1); + const anchor2 = anchor(2); + return anchor1 + ?.docViewPath() + .reverse() + .find(ancestor => anchor2?.docViewPath().includes(ancestor)); + } + // shows a documentView by: // traverses down through the viewPath of contexts to the view: // focusing on each context @@ -259,6 +248,11 @@ export class DocumentManager { Doc.RemoveDocFromList(Doc.MyRecentlyClosed, undefined, targetDoc); const docContextPath = DocumentManager.GetContextPath(targetDoc, true); if (docContextPath.some(doc => doc.hidden)) options.toggleTarget = false; + const tabView = Array.from(TabDocView._allTabs).find(view => view._document === docContextPath[0]); + if (!tabView?._activated && tabView?._document) { + options.toggleTarget = false; + TabDocView.Activate(tabView?._document); + } let rootContextView = docContextPath.length && (await new Promise<DocumentView>(res => { @@ -355,8 +349,8 @@ export function DocFocusOrOpen(doc: Doc, options: FocusViewOptions = { willZoomC }); } }; - if (Doc.IsDataProto(doc) && DocListCast(doc.proto_embeddings).some(embed => embed.hidden && [AclAdmin, AclEdit].includes(GetEffectiveAcl(embed)))) { - doc = DocListCast(doc.proto_embeddings).find(embed => embed.hidden && [AclAdmin, AclEdit].includes(GetEffectiveAcl(embed)))!; + if (Doc.IsDataProto(doc) && Doc.GetEmbeddings(doc).some(embed => embed.hidden && [AclAdmin, AclEdit].includes(GetEffectiveAcl(embed)))) { + doc = Doc.GetEmbeddings(doc).find(embed => embed.hidden && [AclAdmin, AclEdit].includes(GetEffectiveAcl(embed)))!; } if (doc.hidden) { doc.hidden = false; diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index a6ad0f1b3..1f093a33c 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -504,9 +504,9 @@ export namespace DragManager { const moveHandler = (e: PointerEvent) => { e.preventDefault(); // required or dragging text menu link item ends up dragging the link button as native drag/drop if (dragData instanceof DocumentDragData) { - dragData.userDropAction = e.ctrlKey && e.altKey ? 'copy' : e.ctrlKey ? 'embed' : dragData.defaultDropAction; + dragData.userDropAction = e.ctrlKey && e.altKey ? 'copy' : e.shiftKey ? 'move' : e.ctrlKey ? 'embed' : dragData.defaultDropAction; } - if (((e.target as any)?.className === 'lm_tabs' || (e.target as any)?.className === 'lm_header') && dragData.draggedDocuments.length === 1) { + if (['lm_tab', 'lm_title_wrap', 'lm_tabs', 'lm_header'].includes(typeof (e.target as any).className === 'string' ? (e.target as any)?.className : '') && dragData.draggedDocuments.length === 1) { if (!startWindowDragTimer) { startWindowDragTimer = setTimeout(async () => { startWindowDragTimer = undefined; diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts index f62ec8f83..54066d267 100644 --- a/src/client/util/DropConverter.ts +++ b/src/client/util/DropConverter.ts @@ -3,7 +3,7 @@ import { DocData } from '../../fields/DocSymbols'; import { ObjectField } from '../../fields/ObjectField'; import { RichTextField } from '../../fields/RichTextField'; import { listSpec } from '../../fields/Schema'; -import { ScriptField } from '../../fields/ScriptField'; +import { ComputedField, ScriptField } from '../../fields/ScriptField'; import { Cast, StrCast } from '../../fields/Types'; import { ImageField } from '../../fields/URLField'; import { Docs } from '../documents/Documents'; @@ -39,15 +39,12 @@ function makeTemplate(doc: Doc, first: boolean = true, rename: Opt<string> = und any = makeTemplate(d, false) || any; } }); - if (first) { - if (!docs.length) { - // bcz: feels hacky : if the root level document has items, it's not a field template - any = Doc.MakeMetadataFieldTemplate(doc, layoutDoc[DocData]) || any; - } - } - if (layoutDoc[fieldKey] instanceof RichTextField || layoutDoc[fieldKey] instanceof ImageField) { + if (first && !docs.length) { + // bcz: feels hacky : if the root level document has items, it's not a field template + any = Doc.MakeMetadataFieldTemplate(doc, layoutDoc[DocData], true) || any; + } else if (layoutDoc[fieldKey] instanceof RichTextField || layoutDoc[fieldKey] instanceof ImageField) { if (!StrCast(layoutDoc.title).startsWith('-')) { - any = Doc.MakeMetadataFieldTemplate(layoutDoc, layoutDoc[DocData]); + any = Doc.MakeMetadataFieldTemplate(layoutDoc, layoutDoc[DocData], true); } } rename && (doc.title = rename); @@ -76,11 +73,14 @@ export function convertDropDataToButtons(data: DragManager.DocumentDragData) { _nativeHeight: 100, _width: 100, _height: 100, + _layout_hideContextMenu: true, backgroundColor: StrCast(doc.backgroundColor), title: StrCast(layoutDoc.title), btnType: ButtonType.ClickButton, icon: 'bolt', + isSystem: false, }); + dbox.title = ComputedField.MakeFunction('this.dragFactory.title'); dbox.dragFactory = layoutDoc; dbox.dropPropertiesToRemove = doc.dropPropertiesToRemove instanceof ObjectField ? ObjectField.MakeCopy(doc.dropPropertiesToRemove) : undefined; dbox.onDragStart = ScriptField.MakeFunction('makeDelegate(this.dragFactory)'); diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts index 353f28a92..dd3b9bd07 100644 --- a/src/client/util/LinkManager.ts +++ b/src/client/util/LinkManager.ts @@ -23,8 +23,8 @@ import { ScriptingGlobals } from './ScriptingGlobals'; export class LinkManager { @observable static _instance: LinkManager; @observable.shallow userLinkDBs: Doc[] = []; - @observable public static currentLink: Opt<Doc> = undefined; - @observable public static currentLinkAnchor: Opt<Doc> = undefined; + @observable public currentLink: Opt<Doc> = undefined; + @observable public currentLinkAnchor: Opt<Doc> = undefined; public static get Instance() { return LinkManager._instance; } diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts index dfaacf318..f5e162d16 100644 --- a/src/client/util/Scripting.ts +++ b/src/client/util/Scripting.ts @@ -61,7 +61,8 @@ function Run(script: string | undefined, customParams: string[], diagnostics: an const compiledFunction = (() => { try { return new Function(...paramNames, `return ${script}`); - } catch { + } catch (e) { + console.log(e); return undefined; } })(); diff --git a/src/client/util/ScrollBox.tsx b/src/client/util/ScrollBox.tsx deleted file mode 100644 index 785526ab3..000000000 --- a/src/client/util/ScrollBox.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import * as React from 'react'; - -export class ScrollBox extends React.Component<React.PropsWithChildren<{}>> { - onWheel = (e: React.WheelEvent) => { - if (e.currentTarget.scrollHeight > e.currentTarget.clientHeight) { - // If the element has a scroll bar, then we don't want the containing collection to zoom - e.stopPropagation(); - } - }; - - render() { - return ( - <div - style={{ - overflow: 'auto', - width: '100%', - height: '100%', - }} - onWheel={this.onWheel}> - {this.props.children} - </div> - ); - } -} diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts index 2cc64f415..fff2737b6 100644 --- a/src/client/util/SearchUtil.ts +++ b/src/client/util/SearchUtil.ts @@ -1,50 +1,25 @@ +import { ObservableMap } from 'mobx'; import { Doc, DocListCast, Field, Opt } from '../../fields/Doc'; import { Id } from '../../fields/FieldSymbols'; import { StrCast } from '../../fields/Types'; import { DocumentType } from '../documents/DocumentTypes'; +import { DocOptions, FInfo } from '../documents/Documents'; export namespace SearchUtil { export type HighlightingResult = { [id: string]: { [key: string]: string[] } }; - export function SearchCollection(collectionDoc: Opt<Doc>, query: string) { + export function SearchCollection(collectionDoc: Opt<Doc>, query: string, matchKeyNames: boolean, onlyKeys?: string[]) { const blockedTypes = [DocumentType.PRESELEMENT, DocumentType.CONFIG, DocumentType.KVP, DocumentType.FONTICON, DocumentType.BUTTON, DocumentType.SCRIPTING]; - const blockedKeys = [ - 'x', - 'y', - 'proto', - 'width', - 'layout_autoHeight', - 'acl-Override', - 'acl-Guest', - 'embedContainer', - 'zIndex', - 'height', - 'text_scrollHeight', - 'text_height', - 'cloneFieldFilter', - 'isDataDoc', - 'text_annotations', - 'dragFactory_count', - 'text_noTemplate', - 'proto_embeddings', - 'isSystem', - 'layout_fieldKey', - 'isBaseProto', - 'xMargin', - 'yMargin', - 'links', - 'layout', - 'layout_keyValue', - 'layout_fitWidth', - 'type_collection', - 'title_custom', - 'freeform_panX', - 'freeform_panY', - 'freeform_scale', - ]; - query = query.toLowerCase(); + const blockedKeys = matchKeyNames + ? [] + : Object.entries(DocOptions) + .filter(([key, info]: [string, FInfo]) => !info?.searchable()) + .map(([key]) => key); - const results = new Map<Doc, string[]>(); + const exact = query.startsWith('='); + query = query.toLowerCase().split('=').lastElement(); + + const results = new ObservableMap<Doc, string[]>(); if (collectionDoc) { const docs = DocListCast(collectionDoc[Doc.LayoutFieldKey(collectionDoc)]); const docIDs: String[] = []; @@ -52,12 +27,12 @@ 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<string>(); - SearchUtil.documentKeys(doc).forEach( + (onlyKeys ?? SearchUtil.documentKeys(doc)).forEach( key => - (Field.toString(doc[key] as Field) + Field.toScriptString(doc[key] as Field)) - .toLowerCase() // - .includes(query) && hlights.add(key) - ); + (val => (exact ? val === query.toLowerCase() : val.includes(query.toLowerCase())))( + matchKeyNames ? key : Field.toString(doc[key] as Field)) + && hlights.add(key) + ); // prettier-ignore blockedKeys.forEach(key => hlights.delete(key)); if (Array.from(hlights.keys()).length > 0) { diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts index f2a327445..36b926053 100644 --- a/src/client/util/SelectionManager.ts +++ b/src/client/util/SelectionManager.ts @@ -38,6 +38,7 @@ export class SelectionManager { this.Instance.SelectedViews.push(docView); docView.IsSelected = true; docView._props.whenChildContentsActiveChanged(true); + docView.ComponentView?.select?.(false, false); } }); @@ -51,9 +52,11 @@ export class SelectionManager { public static DeselectAll = (except?: Doc): void => { const found = this.Instance.SelectedViews.find(dv => dv.Document === except); - LinkManager.currentLink = undefined; - LinkManager.currentLinkAnchor = undefined; - runInAction(() => (this.Instance.SelectedSchemaDocument = undefined)); + runInAction(() => { + LinkManager.Instance.currentLink = undefined; + LinkManager.Instance.currentLinkAnchor = undefined; + this.Instance.SelectedSchemaDocument = undefined; + }); this.Instance.SelectedViews.forEach(dv => { dv.IsSelected = false; dv._props.whenChildContentsActiveChanged(false); diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx index 5bf9e5b00..682704770 100644 --- a/src/client/util/SettingsManager.tsx +++ b/src/client/util/SettingsManager.tsx @@ -317,7 +317,17 @@ export class SettingsManager extends React.Component<{}> { <div className="tab-column-content"> {/* <NumberInput/> */} <Group formLabel={'Default Font'}> - <NumberDropdown color={SettingsManager.userColor} numberDropdownType={'input'} min={0} max={50} step={2} type={Type.TERT} number={0} unit={'px'} setNumber={() => {}} /> + <NumberDropdown + color={SettingsManager.userColor} + numberDropdownType="slider" + min={0} + max={50} + step={2} + type={Type.PRIM} + number={NumCast(Doc.UserDoc().fontSize, Number(StrCast(Doc.UserDoc().fontSize).replace('px', '')))} + unit={'px'} + setNumber={val => (Doc.UserDoc().fontSize = val + 'px')} + /> <Dropdown items={fontFamilies.map(val => { return { diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts index 9a6719ea5..421855bf3 100644 --- a/src/client/util/UndoManager.ts +++ b/src/client/util/UndoManager.ts @@ -102,7 +102,7 @@ export namespace UndoManager { 'UndoEvent : ' + event.prop + ' = ' + - (value instanceof RichTextField ? value.Text : value instanceof Array ? value.map(val => Field.toScriptString(val)).join(',') : Field.toScriptString(value)) + (value instanceof RichTextField ? value.Text : value instanceof Array ? value.map(val => Field.toJavascriptString(val)).join(',') : Field.toJavascriptString(value)) ); currentBatch.push(event); tempEvents?.push(event); diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx index 8dcdd80e5..8c3c9df2e 100644 --- a/src/client/views/ContextMenu.tsx +++ b/src/client/views/ContextMenu.tsx @@ -183,11 +183,12 @@ export class ContextMenu extends ObservableReactComponent<{}> { @computed get menuItems() { if (!this._searchString) { - return this._items.map((item, ind) => <ContextMenuItem {...item} noexpand={this.itemsNeedSearch ? true : (item as any).noexpand} key={ind + item.description} closeMenu={this.closeMenu} />); + return this._items.map((item, ind) => <ContextMenuItem key={item.description + ind} {...item} noexpand={this.itemsNeedSearch ? true : (item as any).noexpand} closeMenu={this.closeMenu} />); } return this.filteredItems.map((value, index) => Array.isArray(value) ? ( <div + key={index + value.join(' -> ')} className="contextMenu-group" style={{ background: StrCast(SettingsManager.userVariantColor), @@ -204,7 +205,10 @@ export class ContextMenu extends ObservableReactComponent<{}> { return this._showSearch ? 1 : this._items.reduce((p, mi) => p + ((mi as any).noexpand ? 1 : (mi as any).subitems?.length || 1), 0) > 15; } + _searchRef = React.createRef<HTMLInputElement>(); // bcz: we shouldn't need this, since we set autoFocus on the <input> tag, but for some reason we do... + render() { + this.itemsNeedSearch && setTimeout(() => this._searchRef.current?.focus()); return ( <div className="contextMenu-cont" @@ -226,7 +230,17 @@ export class ContextMenu extends ObservableReactComponent<{}> { <span className="icon-background"> <FontAwesomeIcon icon="search" size="lg" /> </span> - <input style={{ color: 'black' }} className="contextMenu-item contextMenu-description search" type="text" placeholder="Filter Menu..." value={this._searchString} onKeyDown={this.onKeyDown} onChange={this.onChange} autoFocus /> + <input + ref={this._searchRef} + style={{ color: 'black' }} + className="contextMenu-item contextMenu-description search" + type="text" + placeholder="Filter Menu..." + value={this._searchString} + onKeyDown={this.onKeyDown} + onChange={this.onChange} + autoFocus + /> </span> )} {this.menuItems} diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx index 3c9d821a9..b2076e1a5 100644 --- a/src/client/views/ContextMenuItem.tsx +++ b/src/client/views/ContextMenuItem.tsx @@ -38,16 +38,16 @@ export class ContextMenuItem extends ObservableReactComponent<ContextMenuProps & componentDidMount() { runInAction(() => (this._items.length = 0)); - if ((this.props as SubmenuProps)?.subitems) { - (this.props as SubmenuProps).subitems?.forEach(i => runInAction(() => this._items.push(i))); + if ((this._props as SubmenuProps)?.subitems) { + (this._props as SubmenuProps).subitems?.forEach(i => runInAction(() => this._items.push(i))); } } handleEvent = async (e: React.MouseEvent<HTMLDivElement>) => { - if ('event' in this.props) { - this.props.closeMenu?.(); - const batch = this.props.undoable !== false ? UndoManager.StartBatch(`Click Menu item: ${this.props.description}`) : undefined; - await this.props.event({ x: e.clientX, y: e.clientY }); + if ('event' in this._props) { + this._props.closeMenu?.(); + const batch = this._props.undoable !== false ? UndoManager.StartBatch(`Click Menu item: ${this._props.description}`) : undefined; + await this._props.event({ x: e.clientX, y: e.clientY }); batch?.end(); } }; @@ -91,11 +91,11 @@ export class ContextMenuItem extends ObservableReactComponent<ContextMenuProps & } render() { - if ('event' in this.props) { + if ('event' in this._props) { return ( - <div className={'contextMenu-item' + (this.props.selected ? ' contextMenu-itemSelected' : '')} onPointerDown={this.handleEvent}> - {this.props.icon ? <span className="contextMenu-item-icon-background">{this.isJSXElement(this.props.icon) ? this.props.icon : <FontAwesomeIcon icon={this.props.icon} size="sm" />}</span> : null} - <div className="contextMenu-description">{this.props.description.replace(':', '')}</div> + <div className={'contextMenu-item' + (this._props.selected ? ' contextMenu-itemSelected' : '')} onPointerDown={this.handleEvent}> + {this._props.icon ? <span className="contextMenu-item-icon-background">{this.isJSXElement(this._props.icon) ? this._props.icon : <FontAwesomeIcon icon={this._props.icon} size="sm" />}</span> : null} + <div className="contextMenu-description">{this._props.description.replace(':', '')}</div> <div className={`contextMenu-item-background`} style={{ @@ -104,7 +104,7 @@ export class ContextMenuItem extends ObservableReactComponent<ContextMenuProps & /> </div> ); - } else if ('subitems' in this.props) { + } else if ('subitems' in this._props) { const where = !this.overItem ? '' : this._overPosY < window.innerHeight / 3 ? 'flex-start' : this._overPosY > (window.innerHeight * 2) / 3 ? 'flex-end' : 'center'; const marginTop = !this.overItem ? '' : this._overPosY < window.innerHeight / 3 ? '20px' : this._overPosY > (window.innerHeight * 2) / 3 ? '-20px' : ''; @@ -118,32 +118,32 @@ export class ContextMenuItem extends ObservableReactComponent<ContextMenuProps & background: SettingsManager.userBackgroundColor, }}> {this._items.map(prop => ( - <ContextMenuItem {...prop} key={prop.description} closeMenu={this.props.closeMenu} /> + <ContextMenuItem {...prop} key={prop.description} closeMenu={this._props.closeMenu} /> ))} </div> ); - if (!(this.props as SubmenuProps).noexpand) { + if (!(this._props as SubmenuProps).noexpand) { return ( <div className="contextMenu-inlineMenu"> {this._items.map(prop => ( - <ContextMenuItem {...prop} key={prop.description} closeMenu={this.props.closeMenu} /> + <ContextMenuItem {...prop} key={prop.description} closeMenu={this._props.closeMenu} /> ))} </div> ); } return ( <div - className={'contextMenu-item' + (this.props.selected ? ' contextMenu-itemSelected' : '')} - style={{ alignItems: where, borderTop: this.props.addDivider ? 'solid 1px' : undefined }} + className={'contextMenu-item' + (this._props.selected ? ' contextMenu-itemSelected' : '')} + style={{ alignItems: where, borderTop: this._props.addDivider ? 'solid 1px' : undefined }} onMouseLeave={this.onPointerLeave} onMouseEnter={this.onPointerEnter}> - {this.props.icon ? ( + {this._props.icon ? ( <span className="contextMenu-item-icon-background" onMouseEnter={this.onPointerLeave} style={{ alignItems: 'center', alignSelf: 'center' }}> - <FontAwesomeIcon icon={this.props.icon} size="sm" /> + <FontAwesomeIcon icon={this._props.icon} size="sm" /> </span> ) : null} <div className="contextMenu-description" onMouseEnter={this.onPointerEnter} style={{ alignItems: 'center', alignSelf: 'center' }}> - {this.props.description} + {this._props.description} <FontAwesomeIcon icon={'angle-right'} size="lg" style={{ position: 'absolute', right: '10px' }} /> </div> <div diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index b21b13e4c..3d5a5b945 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -33,8 +33,10 @@ export interface ViewBoxInterface { getView?: (doc: Doc, options: FocusViewOptions) => Promise<Opt<DocumentView>>; // returns a nested DocumentView for the specified doc or undefined addDocTab?: (doc: Doc, where: OpenWhere) => boolean; // determines how to add a document - used in following links to open the target ina local lightbox addDocument?: (doc: Doc | Doc[], annotationKey?: string) => boolean; // add a document (used only by collections) + removeDocument?: (doc: Doc | Doc[], annotationKey?: string, leavePushpin?: boolean, dontAddToRemoved?: boolean) => boolean; // add a document (used only by collections) select?: (ctrlKey: boolean, shiftKey: boolean) => void; focus?: (textAnchor: Doc, options: FocusViewOptions) => Opt<number>; + viewTransition?: () => Opt<string>; // duration of a view transition animation isAnyChildContentActive?: () => boolean; // is any child content of the document active onClickScriptDisable?: () => 'never' | 'always'; // disable click scripts : never, always, or undefined = only when selected getKeyFrameEditing?: () => boolean; // whether the document is in keyframe editing mode (if it is, then all hidden documents that are not active at the keyframe time will still be shown) @@ -47,17 +49,17 @@ export interface ViewBoxInterface { setData?: (data: Field | Promise<RefField | undefined>) => boolean; componentUI?: (boundsLeft: number, boundsTop: number) => JSX.Element | null; dragStarting?: (snapToDraggedDoc: boolean, showGroupDragTarget: boolean, visited: Set<Doc>) => void; - dragConfig?: (dragData: DragManager.DocumentDragData) => void; + dragConfig?: (dragData: DragManager.DocumentDragData) => void; // function to setup dragData in custom way (see TreeViews which add a tree view flag) incrementalRendering?: () => void; infoUI?: () => JSX.Element | null; - screenBounds?: () => Opt<{ left: number; top: number; right: number; bottom: number; center?: { X: number; Y: number } }>; + screenBounds?: () => Opt<{ left: number; top: number; right: number; bottom: number; transition?: string }>; ptToScreen?: (pt: { X: number; Y: number }) => { X: number; Y: number }; ptFromScreen?: (pt: { X: number; Y: number }) => { X: number; Y: number }; snapPt?: (pt: { X: number; Y: number }, excludeSegs?: number[]) => { nearestPt: { X: number; Y: number }; distance: number }; search?: (str: string, bwd?: boolean, clear?: boolean) => boolean; } /** - * DocComponent returns a React base class used by Doc views with accessors for unpacking he Document,layoutDoc, and dataDoc's + * DocComponent returns a React base class used by Doc views with accessors for unpacking the Document,layoutDoc, and dataDoc's * (note: this should not be used for the 'Box' views that render the contents of Doc views) * Example derived views: CollectionFreeFormDocumentView, DocumentView, DocumentViewInternal) * */ @@ -188,8 +190,8 @@ export function ViewBoxAnnotatableComponent<P extends FieldViewProps>() { const recent = this.Document !== Doc.MyRecentlyClosed ? Doc.MyRecentlyClosed : undefined; toRemove.forEach(doc => { leavePushpin && DocUtils.LeavePushpin(doc, annotationKey ?? this.annotationKey); - Doc.RemoveDocFromList(targetDataDoc, annotationKey ?? this.annotationKey, doc); - Doc.RemoveDocFromList(doc[DocData], 'proto_embeddings', doc); + Doc.RemoveDocFromList(targetDataDoc, annotationKey ?? this.annotationKey, doc, true); + Doc.RemoveEmbedding(doc, doc); doc.embedContainer = undefined; if (recent && !dontAddToRemoved) { doc.type !== DocumentType.LOADING && Doc.AddDocToList(recent, 'data', doc, undefined, true, true); @@ -243,7 +245,7 @@ export function ViewBoxAnnotatableComponent<P extends FieldViewProps>() { inheritParentAcls(targetDataDoc, doc, true); }); - const annoDocs = targetDataDoc[annotationKey ?? this.annotationKey] as List<Doc>; + const annoDocs = Doc.Get(targetDataDoc, annotationKey ?? this.annotationKey, true) as List<Doc>; // get the dataDoc directly ... when using templates there may be some default items already there, but we can't change them. maybe we should copy them over, though... if (annoDocs instanceof List) annoDocs.push(...added.filter(add => !annoDocs.includes(add))); else targetDataDoc[annotationKey ?? this.annotationKey] = new List<Doc>(added); targetDataDoc[(annotationKey ?? this.annotationKey) + '_modificationDate'] = new DateField(); diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 5ef62b2c5..e9c4d9cc5 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -18,7 +18,6 @@ import { DocumentType } from '../documents/DocumentTypes'; import { Docs } from '../documents/Documents'; import { DocumentManager } from '../util/DocumentManager'; import { DragManager } from '../util/DragManager'; -import { LinkFollower } from '../util/LinkFollower'; import { SelectionManager } from '../util/SelectionManager'; import { SettingsManager } from '../util/SettingsManager'; import { SnappingManager } from '../util/SnappingManager'; @@ -112,10 +111,12 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora @action titleBlur = () => { - this._editingTitle = false; if (this._accumulatedTitle.startsWith('#') || this._accumulatedTitle.startsWith('=')) { this._titleControlString = this._accumulatedTitle; } else if (this._titleControlString.startsWith('#')) { + if (this._accumulatedTitle.startsWith('-->#')) { + SelectionManager.Docs.forEach(doc => (doc[DocData].onViewMounted = ScriptField.MakeScript(`updateTagsCollection(this)`))); + } const titleFieldKey = this._titleControlString.substring(1); UndoManager.RunInBatch( () => @@ -285,7 +286,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora } else { var openDoc = selectedDocs[0].Document; if (openDoc.layout_fieldKey === 'layout_icon') { - openDoc = DocListCast(openDoc.proto_embeddings).find(embedding => !embedding.embedContainer) ?? Doc.MakeEmbedding(openDoc); + openDoc = Doc.GetEmbeddings(openDoc).find(embedding => !embedding.embedContainer) ?? Doc.MakeEmbedding(openDoc); Doc.deiconifyView(openDoc); } LightboxView.Instance.SetLightboxDoc( @@ -616,7 +617,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora if (SelectionManager.Views.length === 1) { const selected = SelectionManager.Views[0]; if (this._titleControlString.startsWith('=')) { - return ScriptField.MakeFunction(this._titleControlString.substring(1), { doc: Doc.name })!.script.run({ self: selected.Document, this: selected.layoutDoc }, console.log).result?.toString() || ''; + return ScriptField.MakeFunction(this._titleControlString.substring(1), { doc: Doc.name })?.script.run({ self: selected.Document, this: selected.layoutDoc }, console.log).result?.toString() || ''; } if (this._titleControlString.startsWith('#')) { return Field.toString(selected.Document[this._titleControlString.substring(1)] as Field) || '-unset-'; @@ -641,7 +642,12 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora const { b, r, x, y } = this.Bounds; const seldocview = SelectionManager.Views.lastElement(); if (SnappingManager.IsDragging || r - x < 1 || x === Number.MAX_VALUE || !seldocview || this._hidden || isNaN(r) || isNaN(b) || isNaN(x) || isNaN(y)) { - setTimeout(action(() => (this._showNothing = true))); + setTimeout( + action(() => { + this._editingTitle = false; + this._showNothing = true; + }) + ); return null; } @@ -726,7 +732,10 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora name="dynbox" autoComplete="on" value={hideTitle ? '' : this._accumulatedTitle} - onBlur={e => !hideTitle && this.titleBlur()} + onBlur={action((e: React.FocusEvent) => { + this._editingTitle = false; + !hideTitle && this.titleBlur(); + })} onChange={action(e => !hideTitle && (this._accumulatedTitle = e.target.value))} onKeyDown={hideTitle ? emptyFunction : this.titleEntered} onPointerDown={e => e.stopPropagation()} diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx index 73ac1b032..4508d00a7 100644 --- a/src/client/views/EditableView.tsx +++ b/src/client/views/EditableView.tsx @@ -1,4 +1,4 @@ -import { action, IReactionDisposer, makeObservable, observable, reaction } from 'mobx'; +import { action, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import * as Autosuggest from 'react-autosuggest'; @@ -97,9 +97,10 @@ export class EditableView extends ObservableReactComponent<EditableProps> { super.componentDidUpdate(prevProps); if (this._editing && this._props.editing === false) { this._inputref?.value && this.finalizeEdit(this._inputref.value, false, true, false); - } else if (this._props.editing !== undefined) { - this._editing = this._props.editing; - } + } else + runInAction(() => { + if (this._props.editing !== undefined) this._editing = this._props.editing; + }); } componentWillUnmount() { @@ -248,7 +249,6 @@ export class EditableView extends ObservableReactComponent<EditableProps> { autoFocus={true} onChange={this.onChange} onKeyDown={this.onKeyDown} - onKeyPress={this.stopPropagation} onPointerDown={this.stopPropagation} onClick={this.stopPropagation} onPointerUp={this.stopPropagation} @@ -264,7 +264,6 @@ export class EditableView extends ObservableReactComponent<EditableProps> { autoFocus={true} onChange={this.onChange} onKeyDown={this.onKeyDown} - onKeyPress={this.stopPropagation} onPointerDown={this.stopPropagation} onClick={this.stopPropagation} onPointerUp={this.stopPropagation} @@ -299,15 +298,13 @@ export class EditableView extends ObservableReactComponent<EditableProps> { fontStyle: this._props.fontStyle, fontSize: this._props.fontSize, }} - //onPointerDown={this.stopPropagation} - onClick={this.onClick} - placeholder={this._props.placeholder}> + onClick={this.onClick}> <span style={{ fontStyle: this._props.fontStyle, fontSize: this._props.fontSize, }}> - {this._props.fieldContents ? <FieldView {...this._props.fieldContents} /> : this.props.contents ? this._props.contents?.valueOf() : this._props.placeholder?.valueOf()} + {this._props.fieldContents ? <FieldView {...this._props.fieldContents} /> : this.props.contents ? this._props.contents?.valueOf() : ''} </span> </div> ); diff --git a/src/client/views/FilterPanel.tsx b/src/client/views/FilterPanel.tsx index c4f65a5ca..818c81c9a 100644 --- a/src/client/views/FilterPanel.tsx +++ b/src/client/views/FilterPanel.tsx @@ -117,8 +117,8 @@ export class FilterPanel extends ObservableReactComponent<filterProps> { // return this.activeFilters.map(filter => filter.split(Doc.FilterSep)[0]); // } - gatherFieldValues(childDocs: Doc[], facetKey: string) { - const valueSet = new Set<string>(StrListCast(this.targetDoc.childFilters).map(filter => filter.split(Doc.FilterSep)[1])); + static gatherFieldValues(childDocs: Doc[], facetKey: string, childFilters: string[]) { + const valueSet = new Set<string>(childFilters.map(filter => filter.split(Doc.FilterSep)[1])); let rtFields = 0; let subDocs = childDocs; if (subDocs.length > 0) { @@ -165,7 +165,7 @@ export class FilterPanel extends ObservableReactComponent<filterProps> { @computed get activeRenderedFacetInfos() { return new Set( Array.from(new Set(Array.from(this._selectedFacetHeaders).concat(this.activeFacetHeaders))).map(facetHeader => { - const facetValues = this.gatherFieldValues(this.targetDocChildren, facetHeader); + const facetValues = FilterPanel.gatherFieldValues(this.targetDocChildren, facetHeader, StrListCast(this.targetDoc.childFilters)); let nonNumbers = 0; let minVal = Number.MAX_VALUE, diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index d134d9e7b..733383002 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -160,7 +160,7 @@ export class KeyManager { if (LightboxView.LightboxDoc) { LightboxView.Instance.SetLightboxDoc(undefined); SelectionManager.DeselectAll(); - } else DocumentDecorations.Instance.onCloseClick(true); + } else if (!window.getSelection()?.toString()) DocumentDecorations.Instance.onCloseClick(true); return { stopPropagation: true, preventDefault: true }; } break; diff --git a/src/client/views/InkControlPtHandles.tsx b/src/client/views/InkControlPtHandles.tsx index 7dd57e04d..31b13d2c8 100644 --- a/src/client/views/InkControlPtHandles.tsx +++ b/src/client/views/InkControlPtHandles.tsx @@ -189,6 +189,7 @@ export class InkEndPtHandles extends React.Component<InkEndProps> { @observable _overStart: boolean = false; @observable _overEnd: boolean = false; + _throttle = 0; // need to throttle dragging since the position may change when the control points change. this allows the stroke to settle so that we don't get increasingly bad jitter @action dragRotate = (e: React.PointerEvent, pt1: () => { X: number; Y: number }, pt2: () => { X: number; Y: number }) => { SnappingManager.SetIsDragging(true); @@ -196,6 +197,7 @@ export class InkEndPtHandles extends React.Component<InkEndProps> { this, e, action(e => { + if (this._throttle++ % 2 !== 0) return false; if (!this.props.inkView.controlUndo) this.props.inkView.controlUndo = UndoManager.StartBatch('stretch ink'); // compute stretch factor by finding scaling along axis between start and end points const p1 = pt1(); diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index eca0aca4c..b6cb845a6 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -10,6 +10,7 @@ import * as React from 'react'; import '../../../node_modules/browndash-components/dist/styles/global.min.css'; import { Utils, emptyFunction, lightOrDark, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, returnZero, setupMoveUpEvents } from '../../Utils'; import { Doc, DocListCast, Opt } from '../../fields/Doc'; +import { DocData } from '../../fields/DocSymbols'; import { DocCast, StrCast } from '../../fields/Types'; import { DocServer } from '../DocServer'; import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager'; @@ -50,7 +51,6 @@ import { CollectionDockingView } from './collections/CollectionDockingView'; import { CollectionMenu } from './collections/CollectionMenu'; import { TabDocView } from './collections/TabDocView'; import './collections/TreeView.scss'; -import { CollectionFreeFormLinksView } from './collections/collectionFreeForm'; import { MarqueeOptionsMenu } from './collections/collectionFreeForm/MarqueeOptionsMenu'; import { CollectionLinearView } from './collections/collectionLinear'; import { LinkMenu } from './linking/LinkMenu'; @@ -72,7 +72,6 @@ import { PresBox } from './nodes/trails'; import { AnchorMenu } from './pdf/AnchorMenu'; import { GPTPopup } from './pdf/GPTPopup/GPTPopup'; import { TopBar } from './topbar/TopBar'; -import { DocData } from '../../fields/DocSymbols'; const { default: { LEFT_MENU_WIDTH, TOPBAR_HEIGHT } } = require('./global/globalCssVariables.module.scss'); // prettier-ignore const _global = (window /* browser */ || global) /* node */ as any; @@ -641,7 +640,6 @@ export class MainView extends ObservableReactComponent<{}> { } @computed get mainDocView() { const headerBar = this._hideUI || !this.headerBarDocHeight?.() ? null : this.headerBarDocView; - console.log('Header = ' + this._hideUI + ' ' + this.headerBarDocHeight?.() + ' ' + headerBar); return ( <> {headerBar} @@ -1037,7 +1035,6 @@ export class MainView extends ObservableReactComponent<{}> { {/* <InkTranscription /> */} {this.snapLines} <LightboxView key="lightbox" PanelWidth={this._windowWidth} PanelHeight={this._windowHeight} maxBorder={[200, 50]} /> - <CollectionFreeFormLinksView /> <OverlayView /> <GPTPopup key="gptpopup" /> <SchemaCSVPopUp key="schemacsvpopup" /> diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx index bba6285c2..cb38ab602 100644 --- a/src/client/views/PropertiesButtons.tsx +++ b/src/client/views/PropertiesButtons.tsx @@ -411,7 +411,7 @@ export class PropertiesButtons extends React.Component<{}, {}> { docView.noOnClick(); switch (onClick) { case 'enterPortal': - docView.makeIntoPortal(); + DocUtils.makeIntoPortal(docView.Document, docView.layoutDoc, docView.allLinks); break; case 'toggleDetail': docView.setToggleDetail(); diff --git a/src/client/views/PropertiesDocBacklinksSelector.tsx b/src/client/views/PropertiesDocBacklinksSelector.tsx index 244cd4aa0..cf5105efc 100644 --- a/src/client/views/PropertiesDocBacklinksSelector.tsx +++ b/src/client/views/PropertiesDocBacklinksSelector.tsx @@ -25,7 +25,7 @@ export class PropertiesDocBacklinksSelector extends React.Component<PropertiesDo const linkAnchor = this.props.Document; const other = LinkManager.getOppositeAnchor(link, linkAnchor); const otherdoc = !other ? undefined : other.annotationOn && other.type !== DocumentType.RTF ? Cast(other.annotationOn, Doc, null) : other; - LinkManager.currentLink = link; + LinkManager.Instance.currentLink = link; if (otherdoc) { otherdoc.hidden = false; this.props.addDocTab(Doc.IsDataProto(otherdoc) ? Doc.MakeDelegate(otherdoc) : otherdoc, OpenWhere.toggleRight); diff --git a/src/client/views/PropertiesDocContextSelector.tsx b/src/client/views/PropertiesDocContextSelector.tsx index 54f141a36..361451c4d 100644 --- a/src/client/views/PropertiesDocContextSelector.tsx +++ b/src/client/views/PropertiesDocContextSelector.tsx @@ -28,14 +28,14 @@ export class PropertiesDocContextSelector extends ObservableReactComponent<Prope if (!this._props.DocView) return []; const target = this._props.DocView._props.Document; const targetContext = this._props.DocView.containerViewPath?.().lastElement()?.Document; - const embeddings = DocListCast(target.proto_embeddings); + const embeddings = Doc.GetEmbeddings(target); const containerProtos = embeddings.filter(embedding => embedding.embedContainer && embedding.embedContainer instanceof Doc).reduce((set, embedding) => set.add(Cast(embedding.embedContainer, Doc, null)), new Set<Doc>()); - const containerSets = Array.from(containerProtos.keys()).map(container => DocListCast(container.proto_embeddings)); + const containerSets = Array.from(containerProtos.keys()).map(container => Doc.GetEmbeddings(container)); const containers = containerSets.reduce((p, set) => { set.map(s => p.add(s)); return p; }, new Set<Doc>()); - const doclayoutSets = Array.from(containers.keys()).map(dp => DocListCast(dp.proto_embeddings)); + const doclayoutSets = Array.from(containers.keys()).map(dp => Doc.GetEmbeddings(dp)); const doclayouts = Array.from( doclayoutSets .reduce((p, set) => { diff --git a/src/client/views/PropertiesSection.scss b/src/client/views/PropertiesSection.scss index 3f92a70f8..d32da1bf1 100644 --- a/src/client/views/PropertiesSection.scss +++ b/src/client/views/PropertiesSection.scss @@ -4,20 +4,20 @@ .propertiesView-content { padding: 10px; } +} - .propertiesView-sectionTitle { - text-align: center; - display: flex; - padding: 3px 10px; - font-size: 14px; - font-weight: bold; - justify-content: space-between; - align-items: center; +.propertiesView-sectionTitle { + text-align: center; + display: flex; + padding: 3px 10px; + font-size: 14px; + font-weight: bold; + justify-content: space-between; + align-items: center; - .propertiesView-sectionTitle-icon { - width: 20px; - height: 20px; - align-items: flex-end; - } + .propertiesView-sectionTitle-icon { + width: 20px; + height: 20px; + align-items: flex-end; } } diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx index 05cd18182..c3fdee001 100644 --- a/src/client/views/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -46,6 +46,7 @@ import TextareaAutosize from 'react-textarea-autosize'; import { GeneratedResponse, StyleInput, generatePalette } from '../apis/gpt/customization'; import { FaFillDrip } from 'react-icons/fa'; +import { LinkBox } from './nodes/LinkBox'; const _global = (window /* browser */ || global) /* node */ as any; interface PropertiesViewProps { @@ -74,6 +75,10 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps return SelectionManager.SelectedSchemaDoc || this.selectedDocumentView?.Document || Doc.ActiveDashboard; } + @computed get selectedLink() { + return this.selectedDocumentView?.ComponentView instanceof LinkBox ? this.selectedDocumentView.Document : LinkManager.Instance.currentLink; + } + @computed get selectedLayoutDoc() { return SelectionManager.SelectedSchemaDoc || this.selectedDocumentView?.layoutDoc || Doc.ActiveDashboard; } @@ -167,7 +172,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps componentDidMount() { this._disposers.link = reaction( - () => LinkManager.currentLink, + () => this.selectedLink, link => { link && this.CloseAll(); link && (this.openLinks = true); @@ -230,10 +235,14 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps if (this.dataDoc && this.selectedDoc) { const ids = new Set<string>(reqdKeys); const docs: Doc[] = SelectionManager.Views.length < 2 ? [this.layoutFields ? Doc.Layout(this.selectedDoc) : this.dataDoc] : SelectionManager.Views.map(dv => (this.layoutFields ? dv.layoutDoc : dv.dataDoc)); - docs.forEach(doc => Object.keys(doc).forEach(key => doc[key] !== ComputedField.undefined && ids.add(key))); + docs.forEach(doc => + Object.keys(doc) + .filter(filter) + .forEach(key => doc[key] !== ComputedField.undefined && key && ids.add(key)) + ); // prettier-ignore - Array.from(ids).filter(filter).sort().map(key => { + Array.from(ids).sort().map(key => { const multiple = Array.from(docs.reduce((set,doc) => set.add(doc[key]), new Set<FieldResult>()).keys()).length > 1; const editableContents = multiple ? '-multiple-' : Field.toKeyValueString(docs[0], key); const displayContents = multiple ? '-multiple-' : Field.toString(docs[0][key] as Field); @@ -320,20 +329,19 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps @computed get contextCount() { if (this.selectedDocumentView) { const target = this.selectedDocumentView.Document; - const embeddings = DocListCast(target.proto_embeddings); - return embeddings.length - 1; + return Doc.GetEmbeddings(target).length - 1; } else { return 0; } } @computed get links() { - const selAnchor = this.selectedDocumentView?.anchorViewDoc ?? LinkManager.currentLinkAnchor ?? this.selectedDoc; + const selAnchor = this.selectedDocumentView?.anchorViewDoc ?? LinkManager.Instance.currentLinkAnchor ?? this.selectedDoc; return !selAnchor ? null : <PropertiesDocBacklinksSelector Document={selAnchor} hideTitle={true} addDocTab={this._props.addDocTab} />; } @computed get linkCount() { - const selAnchor = this.selectedDocumentView?.anchorViewDoc ?? LinkManager.currentLinkAnchor ?? this.selectedDoc; + const selAnchor = this.selectedDocumentView?.anchorViewDoc ?? LinkManager.Instance.currentLinkAnchor ?? this.selectedDoc; var counter = 0; LinkManager.Links(selAnchor).forEach((l, i) => counter++); @@ -629,19 +637,19 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps return ( <div> <EditableText val={title} setVal={this.setTitle} color={this.color} type={Type.SEC} formLabel={'Title'} fillWidth /> - {LinkManager.currentLinkAnchor ? ( + {LinkManager.Instance.currentLinkAnchor ? ( <p className="propertiesView-titleExtender"> <> <b>Anchor:</b> - {LinkManager.currentLinkAnchor.title} + {LinkManager.Instance.currentLinkAnchor.title} </> </p> ) : null} - {LinkManager.currentLink?.title ? ( + {this.selectedLink?.title ? ( <p className="propertiesView-titleExtender"> <> <b>Link:</b> - {LinkManager.currentLink.title} + {this.selectedLink.title} </> </p> ) : null} @@ -795,7 +803,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps switch (field) { case 'Xps': selDoc.x = NumCast(this.selectedDoc?.x) + (dirs === 'up' ? 10 : -10); break; case 'Yps': selDoc.y = NumCast(this.selectedDoc?.y) + (dirs === 'up' ? 10 : -10); break; - case 'stk': selDoc.stroke_width = NumCast(this.selectedDoc?.stroke_width) + (dirs === 'up' ? 0.1 : -0.1); break; + case 'stk': selDoc.stroke_width = NumCast(this.selectedDoc?.[DocData].stroke_width) + (dirs === 'up' ? 0.1 : -0.1); break; case 'wid': const oldWidth = NumCast(selDoc._width); const oldHeight = NumCast(selDoc._height); @@ -840,7 +848,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps }; getField(key: string) { - return Field.toString(this.selectedDoc?.[key] as Field); + return Field.toString(this.selectedDoc?.[DocData][key] as Field); } @computed get shapeXps() { @@ -855,6 +863,9 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps @computed get shapeWid() { return NumCast(this.selectedDoc?._width); } + @computed get strokeThk() { + return NumCast(this.selectedDoc?.[DocData].stroke_width); + } set shapeXps(value) { this.selectedDoc && (this.selectedDoc.x = Math.round(value * 100) / 100); } @@ -867,6 +878,9 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps set shapeHgt(value) { this.selectedDoc && (this.selectedDoc._height = Math.round(value * 100) / 100); } + set strokeThk(value) { + this.selectedDoc && (this.selectedDoc[DocData].stroke_width = Math.round(value * 100) / 100); + } @computed get hgtInput() { return this.inputBoxDuo( @@ -899,16 +913,16 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps private _lastDash: any = '2'; @computed get colorFil() { - return StrCast(this.selectedDoc?.fillColor); + return StrCast(this.selectedDoc?.[DocData].fillColor); } @computed get colorStk() { - return StrCast(this.selectedDoc?.color); + return StrCast(this.selectedDoc?.[DocData].color); } set colorFil(value) { - this.selectedDoc && (this.selectedDoc.fillColor = value ? value : undefined); + this.selectedDoc && (this.selectedDoc[DocData].fillColor = value ? value : undefined); } set colorStk(value) { - this.selectedDoc && (this.selectedDoc.color = value ? value : undefined); + this.selectedDoc && (this.selectedDoc[DocData].color = value ? value : undefined); } colorButton(value: string, type: string, setter: () => void) { @@ -997,19 +1011,19 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps } set dashdStk(value) { value && (this._lastDash = value); - this.selectedDoc && (this.selectedDoc.stroke_dash = value ? this._lastDash : undefined); + this.selectedDoc && (this.selectedDoc[DocData].stroke_dash = value ? this._lastDash : undefined); } set markScal(value) { - this.selectedDoc && (this.selectedDoc.stroke_markerScale = Number(value)); + this.selectedDoc && (this.selectedDoc[DocData].stroke_markerScale = Number(value)); } set widthStk(value) { - this.selectedDoc && (this.selectedDoc.stroke_width = Number(value)); + this.selectedDoc && (this.selectedDoc[DocData].stroke_width = Number(value)); } set markHead(value) { - this.selectedDoc && (this.selectedDoc.stroke_startMarker = value); + this.selectedDoc && (this.selectedDoc[DocData].stroke_startMarker = value); } set markTail(value) { - this.selectedDoc && (this.selectedDoc.stroke_endMarker = value); + this.selectedDoc && (this.selectedDoc[DocData].stroke_endMarker = value); } @computed get stkInput() { @@ -1050,53 +1064,11 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps @computed get widthAndDash() { return ( <div className="widthAndDash"> - <div className="width"> - <div className="width-top"> - <div className="width-title">Width:</div> - <div className="width-input">{this.stkInput}</div> - </div> - <input - className="width-range" - type="range" - style={{ color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }} - defaultValue={Number(this.widthStk)} - min={1} - max={100} - onChange={action(e => (this.widthStk = e.target.value))} - onMouseDown={e => { - this._widthUndo = UndoManager.StartBatch('width undo'); - }} - onMouseUp={e => { - this._widthUndo?.end(); - this._widthUndo = undefined; - }} - /> - </div> + <div className="width">{this.getNumber('Thickness', '', 0, Math.max(50, this.strokeThk), this.strokeThk, (val: number) => !isNaN(val) && (this.strokeThk = val), 50, 1)}</div> + <div className="width">{this.getNumber('Arrow Scale', '', 0, Math.max(10, this.markScal), this.markScal, (val: number) => !isNaN(val) && (this.markScal = val), 10, 1)}</div> <div className="arrows"> <div className="arrows-head"> - <div className="width-top"> - <div className="width-title">Arrow Scale:</div> - {/* <div className="width-input">{this.markScalInput}</div> */} - </div> - <input - className="width-range" - type="range" - defaultValue={this.markScal} - style={{ color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }} - min={0} - max={10} - onChange={action(e => (this.markScal = +e.target.value))} - onMouseDown={e => { - this._widthUndo = UndoManager.StartBatch('scale undo'); - }} - onMouseUp={e => { - this._widthUndo?.end(); - this._widthUndo = undefined; - }} - /> - </div> - <div className="arrows-head"> <div className="arrows-head-title">Arrow Head: </div> <input key="markHead" className="arrows-head-input" type="checkbox" checked={this.markHead !== ''} onChange={undoBatch(action(() => (this.markHead = this.markHead ? '' : 'arrow')))} /> </div> @@ -1334,10 +1306,10 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps } @computed get description() { - return Field.toString(LinkManager.currentLink?.link_description as any as Field); + return Field.toString(this.selectedLink?.link_description as any as Field); } @computed get relationship() { - return StrCast(LinkManager.currentLink?.link_relationship); + return StrCast(this.selectedLink?.link_relationship); } @observable private relationshipButtonColor: string = ''; @@ -1347,7 +1319,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps handleDescriptionChange = undoable( action((value: string) => { - if (LinkManager.currentLink && this.selectedDoc) { + if (this.selectedLink) { this.setDescripValue(value); } }), @@ -1356,39 +1328,26 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps handlelinkRelationshipChange = undoable( action((value: string) => { - if (LinkManager.currentLink && this.selectedDoc) { + if (this.selectedLink) { this.setlinkRelationshipValue(value); } }), 'change link relationship' ); - handleColorChange = undoable( - action((value: string) => { - if (LinkManager.currentLink && this.selectedDoc) { - this.setColorValue(value); - } - }), - 'change link color' - ); - @undoBatch setDescripValue = action((value: string) => { - if (LinkManager.currentLink) { - Doc.GetProto(LinkManager.currentLink).link_description = value; - - if (LinkManager.currentLink.show_description === undefined) { - LinkManager.currentLink.show_description = !LinkManager.currentLink.show_description; - } + if (this.selectedLink) { + this.selectedLink[DocData].link_description = value; } }); @undoBatch setlinkRelationshipValue = action((value: string) => { - if (LinkManager.currentLink) { - const prevRelationship = StrCast(LinkManager.currentLink.link_relationship); - LinkManager.currentLink.link_relationship = value; - Doc.GetProto(LinkManager.currentLink).link_relationship = value; + if (this.selectedLink) { + const prevRelationship = StrCast(this.selectedLink.link_relationship); + this.selectedLink.link_relationship = value; + Doc.GetProto(this.selectedLink).link_relationship = value; const linkRelationshipList = StrListCast(Doc.UserDoc().link_relationshipList); const linkRelationshipSizes = NumListCast(Doc.UserDoc().link_relationshipSizes); const linkColorList = StrListCast(Doc.UserDoc().link_ColorList); @@ -1425,13 +1384,6 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps } }); - @undoBatch - setColorValue = action((value: string) => { - if (LinkManager.currentLink) { - Doc.GetProto(LinkManager.currentLink).link_color = value; - } - }); - changeFollowBehavior = undoable((loc: Opt<string>) => this.sourceAnchor && (this.sourceAnchor.followLinkLocation = loc), 'change follow behavior'); @undoBatch @@ -1479,20 +1431,20 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps }; toggleLinkProp = (e: React.PointerEvent, prop: string) => { - setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => LinkManager.currentLink && (LinkManager.currentLink[prop] = !LinkManager.currentLink[prop])))); + setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => this.selectedLink && (this.selectedLink[prop] = !this.selectedLink[prop])))); }; @computed get destinationAnchor() { - const ldoc = LinkManager.currentLink; - const lanch = this.selectedDocumentView?.anchorViewDoc ?? LinkManager.currentLinkAnchor; + const ldoc = this.selectedLink; + const lanch = this.selectedDocumentView?.anchorViewDoc ?? LinkManager.Instance.currentLinkAnchor; if (ldoc && lanch) return LinkManager.getOppositeAnchor(ldoc, lanch) ?? lanch; return ldoc ? DocCast(ldoc.link_anchor_2) : ldoc; } @computed get sourceAnchor() { - const selAnchor = this.selectedDocumentView?.anchorViewDoc ?? LinkManager.currentLinkAnchor; + const selAnchor = this.selectedDocumentView?.anchorViewDoc ?? LinkManager.Instance.currentLinkAnchor; - return selAnchor ?? (LinkManager.currentLink && this.destinationAnchor ? LinkManager.getOppositeAnchor(LinkManager.currentLink, this.destinationAnchor) : LinkManager.currentLink); + return selAnchor ?? (this.selectedLink && this.destinationAnchor ? LinkManager.getOppositeAnchor(this.selectedLink, this.destinationAnchor) : this.selectedLink); } toggleAnchorProp = (e: React.PointerEvent, prop: string, anchor?: Doc, value: any = true, ovalue: any = false, cb: (val: any) => any = val => val) => { @@ -1518,7 +1470,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps style={{ color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }} autoComplete={'off'} id="link_relationship_input" - value={StrCast(LinkManager.currentLink?.link_relationship)} + value={StrCast(this.selectedLink?.link_relationship)} onKeyDown={this.onRelationshipKey} onBlur={this.onSelectOutRelationship} onChange={e => this.handlelinkRelationshipChange(e.currentTarget.value)} @@ -1535,7 +1487,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps autoComplete="off" style={{ textAlign: 'left', color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }} id="link_description_input" - value={StrCast(LinkManager.currentLink?.link_description)} + value={StrCast(this.selectedLink?.link_description)} onKeyDown={this.onDescriptionKey} onBlur={this.onSelectOutDesc} onChange={e => this.handleDescriptionChange(e.currentTarget.value)} @@ -1557,7 +1509,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps const zoom = Number((NumCast(this.sourceAnchor?.followLinkZoomScale, 1) * 100).toPrecision(3)); const targZoom = this.sourceAnchor?.followLinkZoom; const indent = 30; - const hasSelectedAnchor = LinkManager.Links(this.sourceAnchor).includes(LinkManager.currentLink!); + const hasSelectedAnchor = LinkManager.Links(this.sourceAnchor).includes(this.selectedLink!); return ( <> @@ -1570,58 +1522,6 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps <p>Description</p> {this.editDescription} </div> - <div className="propertiesView-input inline"> - <p>Show link</p> - <button - style={{ background: !LinkManager.currentLink?.link_displayLine ? '' : '#4476f7', borderRadius: 3 }} - onPointerDown={e => this.toggleLinkProp(e, 'link_displayLine')} - onClick={e => e.stopPropagation()} - className="propertiesButton"> - <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" /> - </button> - </div> - <div className="propertiesView-input inline" style={{ marginLeft: 10 }}> - <p>Auto-move anchors</p> - <button - style={{ background: !LinkManager.currentLink?.link_autoMoveAnchors ? '' : '#4476f7', borderRadius: 3 }} - onPointerDown={e => this.toggleLinkProp(e, 'link_autoMoveAnchors')} - onClick={e => e.stopPropagation()} - className="propertiesButton"> - <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" /> - </button> - </div> - <div className="propertiesView-input inline" style={{ marginLeft: 10 }}> - <p>Display arrow</p> - <button - style={{ background: !LinkManager.currentLink?.link_displayArrow ? '' : '#4476f7', borderRadius: 3 }} - onPointerDown={e => this.toggleLinkProp(e, 'link_displayArrow')} - onClick={e => e.stopPropagation()} - className="propertiesButton"> - <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" /> - </button> - </div> - <div className="propertiesView-input inline" style={{ marginLeft: 10 }}> - <p>Show description</p> - <button - style={{ background: !LinkManager.currentLink?.show_description ? '' : '#4476f7', borderRadius: 3 }} - onPointerDown={e => this.toggleLinkProp(e, 'show_description')} - onClick={e => e.stopPropagation()} - className="propertiesButton"> - <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" /> - </button> - </div> - <div className="propertiesView-input inline" style={{ marginLeft: 10 }}> - <p>Link color</p> - <ColorPicker - tooltip={'User Color'} // - color={SettingsManager.userColor} - type={Type.SEC} - icon={<FaFillDrip />} - selectedColor={LinkManager.currentLink?.link_color ? StrCast(LinkManager.currentLink?.link_color) : '#449ef7'} - setSelectedColor={this.handleColorChange} - setFinalColor={this.handleColorChange} - /> - </div> </div> {!hasSelectedAnchor ? null : ( <div className="propertiesView-section"> @@ -1640,7 +1540,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps <option value={OpenWhere.add}>Opening in new tab</option> <option value={OpenWhere.replace}>Replacing current tab</option> <option value={OpenWhere.inParent}>Opening in same collection</option> - {LinkManager.currentLink?.linksToAnnotation ? <option value="openExternal">Open in external page</option> : null} + {this.selectedLink?.linksToAnnotation ? <option value="openExternal">Open in external page</option> : null} </select> </div> <div className="propertiesView-input inline first" style={{ display: 'grid', gridTemplateColumns: '84px calc(100% - 134px) 50px' }}> @@ -1825,7 +1725,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps render() { const isNovice = Doc.noviceMode; - const hasSelectedAnchor = LinkManager.Links(this.sourceAnchor).includes(LinkManager.currentLink!); + const hasSelectedAnchor = LinkManager.Links(this.sourceAnchor).includes(this.selectedLink!); if (!this.selectedDoc && !this.isPres) { return ( <div className="propertiesView" style={{ width: this._props.width }}> @@ -1846,7 +1746,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps minWidth: this._props.width, }}> <div className="propertiesView-propAndInfoGrouping"> - <div className="propertiesView-title" style={{ width: this._props.width }}> + <div className="propertiesView-sectionTitle" style={{ width: this._props.width }}> Properties <div className="propertiesView-info" onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/properties')}> <IconButton icon={<FontAwesomeIcon icon="info-circle" />} color={SettingsManager.userColor} /> @@ -1857,12 +1757,12 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps <div className="propertiesView-name">{this.editableTitle}</div> <div className="propertiesView-type"> {this.currentType} </div> {this.stylingSubMenu} + {this.fieldsSubMenu} {this.optionsSubMenu} {this.linksSubMenu} - {!LinkManager.currentLink || !this.openLinks ? null : this.linkProperties} + {!this.selectedLink || !this.openLinks ? null : this.linkProperties} {this.inkSubMenu} {this.contextsSubMenu} - {this.fieldsSubMenu} {isNovice ? null : this.sharingSubMenu} {this.filtersSubMenu} {isNovice ? null : this.layoutSubMenu} diff --git a/src/client/views/StyleProvider.scss b/src/client/views/StyleProvider.scss index 4d3096f71..30a026dbc 100644 --- a/src/client/views/StyleProvider.scss +++ b/src/client/views/StyleProvider.scss @@ -1,5 +1,7 @@ .styleProvider-filter, .styleProvider-audio, +.styleProvider-paint, +.styleProvider-paint-selected, .styleProvider-lock { font-size: 10; width: 15; @@ -28,6 +30,13 @@ .styleProvider-audio { right: 30; } +.styleProvider-paint-selected, +.styleProvider-paint { + top: 15; +} +.styleProvider-paint-selected { + right: -30; +} .styleProvider-lock:hover, .styleProvider-audio:hover, .styleProvider-filter:hover { diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx index 5a0167338..0794efe4c 100644 --- a/src/client/views/StyleProvider.tsx +++ b/src/client/views/StyleProvider.tsx @@ -9,7 +9,7 @@ import { BsArrowDown, BsArrowDownUp, BsArrowUp } from 'react-icons/bs'; import { FaFilter } from 'react-icons/fa'; import { Doc, Opt, StrListCast } from '../../fields/Doc'; import { DocViews } from '../../fields/DocSymbols'; -import { BoolCast, Cast, DocCast, ImageCast, NumCast, StrCast } from '../../fields/Types'; +import { BoolCast, Cast, DocCast, ImageCast, NumCast, ScriptCast, StrCast } from '../../fields/Types'; import { DashColor, lightOrDark, Utils } from '../../Utils'; import { CollectionViewType, DocumentType } from '../documents/DocumentTypes'; import { DocFocusOrOpen, DocumentManager } from '../util/DocumentManager'; @@ -53,6 +53,16 @@ export enum StyleProp { function toggleLockedPosition(doc: Doc) { UndoManager.RunInBatch(() => Doc.toggleLockedPosition(doc), 'toggleBackground'); } +function togglePaintView(e: React.MouseEvent, doc: Opt<Doc>, props: Opt<FieldViewProps & DocumentViewProps>) { + const scriptProps = { + this: doc, + _readOnly_: false, + documentView: props?.DocumentView?.(), + value: undefined, + }; + e.stopPropagation(); + UndoManager.RunInBatch(() => doc && ScriptCast(doc.onPaint).script.run(scriptProps), 'togglePaintView'); +} export function wavyBorderPath(pw: number, ph: number, inset: number = 0.05) { return `M ${pw * 0.5} ${ph * inset} C ${pw * 0.6} ${ph * inset} ${pw * (1 - 2 * inset)} 0 ${pw * (1 - inset)} ${ph * inset} C ${pw} ${ph * (2 * inset)} ${pw * (1 - inset)} ${ph * 0.25} ${pw * (1 - inset)} ${ph * 0.3} C ${ @@ -121,10 +131,10 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps & case StyleProp.DocContents:return undefined; case StyleProp.WidgetColor:return isAnnotated ? Colors.LIGHT_BLUE : 'dimgrey'; case StyleProp.Opacity: return props?.LayoutTemplateString?.includes(KeyValueBox.name) ? 1 : doc?.text_inlineAnnotations ? 0 : Cast(doc?._opacity, "number", Cast(doc?.opacity, 'number', null)); - case StyleProp.FontSize: return StrCast(doc?.[fieldKey + 'fontSize'], StrCast(doc?._text_fontSize, StrCast(Doc.UserDoc().fontSize))); - case StyleProp.FontFamily: return StrCast(doc?.[fieldKey + 'fontFamily'], StrCast(doc?._text_fontFamily, StrCast(Doc.UserDoc().fontFamily))); - case StyleProp.FontWeight: return StrCast(doc?.[fieldKey + 'fontWeight'], StrCast(doc?._text_fontWeight, StrCast(Doc.UserDoc().fontWeight))); - case StyleProp.FillColor: return StrCast(doc?._fillColor, StrCast(doc?.fillColor, 'transparent')); + case StyleProp.FontSize: return StrCast(doc?.[fieldKey + 'fontSize'], StrCast(Doc.UserDoc().fontSize)); + case StyleProp.FontFamily: return StrCast(doc?.[fieldKey + 'fontFamily'], StrCast(Doc.UserDoc().fontFamily)); + case StyleProp.FontWeight: return StrCast(doc?.[fieldKey + 'fontWeight'], StrCast(Doc.UserDoc().fontWeight)); + case StyleProp.FillColor: return StrCast(doc?._fillColor, StrCast(doc?.fillColor, StrCast(doc?.backgroundColor, 'transparent'))); case StyleProp.ShowCaption:return props?.hideCaptions || doc?._type_collection === CollectionViewType.Carousel ? undefined: StrCast(doc?._layout_showCaption); case StyleProp.TitleHeight:return (props?.ScreenToLocalTransform().Scale ?? 1) * NumCast(Doc.UserDoc().headerHeight,30); case StyleProp.ShowTitle: @@ -181,7 +191,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps & : 0; case StyleProp.BackgroundColor: { if (SettingsManager.Instance.LastPressedBtn === doc) return SettingsManager.userColor; // hack to indicate active menu panel item - let docColor: Opt<string> = StrCast(doc?.[fieldKey + 'backgroundColor'], StrCast(doc?._backgroundColor, isCaption ? 'rgba(0,0,0,0.4)' : '')); + let docColor: Opt<string> = StrCast(doc?.[fieldKey + 'backgroundColor'], StrCast(doc?.backgroundColor, isCaption ? 'rgba(0,0,0,0.4)' : '')); // prettier-ignore switch (doc?.type) { case DocumentType.PRESELEMENT: docColor = docColor || ""; break; @@ -267,6 +277,11 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps & <FontAwesomeIcon icon='lock' size="lg" /> </div> ); + const paint = () => !doc?.onPaint ? null : ( + <div className={`styleProvider-paint${props?.DocumentView?.().IsSelected ? "-selected":""}`} onClick={e => togglePaintView(e, doc, props)}> + <FontAwesomeIcon icon='pen' size="lg" /> + </div> + ); const filter = () => { const dashView = untracked(() => DocumentManager.Instance.getDocumentView(Doc.ActiveDashboard)); const showFilterIcon = @@ -329,6 +344,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps & }; return ( <> + {paint()} {lock()} {filter()} {audio()} diff --git a/src/client/views/TouchScrollableMenu.tsx b/src/client/views/TouchScrollableMenu.tsx deleted file mode 100644 index 530c693a7..000000000 --- a/src/client/views/TouchScrollableMenu.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import * as React from 'react'; -import { computed } from 'mobx'; -import { observer } from 'mobx-react'; - -export interface TouchScrollableMenuProps { - options: JSX.Element[]; - bounds: { - right: number; - left: number; - bottom: number; - top: number; - width: number; - height: number; - }; - selectedIndex: number; - x: number; - y: number; -} - -export interface TouchScrollableMenuItemProps { - text: string; - onClick: () => any; -} - -@observer -export default class TouchScrollableMenu extends React.Component<TouchScrollableMenuProps> { - @computed - private get possibilities() { - return this.props.options; - } - - @computed - private get selectedIndex() { - return this.props.selectedIndex; - } - - render() { - return ( - <div - className="inkToTextDoc-cont" - style={{ - transform: `translate(${this.props.x}px, ${this.props.y}px)`, - width: 300, - height: this.possibilities.length * 25, - }}> - <div className="inkToTextDoc-scroller" style={{ transform: `translate(0, ${-this.selectedIndex * 25}px)` }}> - {this.possibilities} - </div> - <div className="shadow" style={{ height: `calc(100% - 25px - ${this.selectedIndex * 25}px)` }}></div> - </div> - ); - } -} - -export class TouchScrollableMenuItem extends React.Component<TouchScrollableMenuItemProps> { - render() { - return ( - <div className="menuItem-cont" onClick={this.props.onClick}> - {this.props.text} - </div> - ); - } -} diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 87973fd81..31ca86f0f 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -490,7 +490,7 @@ export class CollectionDockingView extends CollectionSubView() { // if you close a tab that is not embedded somewhere else (an embedded Doc can be opened simultaneously in a tab), then add the tab to recently closed if (tab.DashDoc.embedContainer === this.Document) tab.DashDoc.embedContainer = undefined; if (!tab.DashDoc.embedContainer) Doc.AddDocToList(Doc.MyRecentlyClosed, 'data', tab.DashDoc, undefined, true, true); - Doc.RemoveDocFromList(tab.DashDoc[DocData], 'proto_embeddings', tab.DashDoc); + Doc.RemoveEmbedding(tab.DashDoc, tab.DashDoc); } if (CollectionDockingView.Instance) { const dview = CollectionDockingView.Instance.Document; diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 89e72152a..54314f62c 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -135,14 +135,15 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection return docs.map((d, i) => { const height = () => this.getDocHeight(d); const width = () => this.getDocWidth(d); + const trans = () => this.getDocTransition(d); // assuming we need to get rowSpan because we might be dealing with many columns. Grid gap makes sense if multiple columns const rowSpan = Math.ceil((height() + this.gridGap) / this.gridGap); // just getting the style - const style = this.isStackingView ? { margin: this.Document._stacking_alignCenter ? 'auto' : undefined, width: width(), marginTop: i ? this.gridGap : 0, height: height() } : { gridRowEnd: `span ${rowSpan}` }; + const style = this.isStackingView ? { margin: this.Document._stacking_alignCenter ? 'auto' : undefined, transition: trans(), width: width(), marginTop: i ? this.gridGap : 0, height: height() } : { gridRowEnd: `span ${rowSpan}` }; // So we're choosing whether we're going to render a column or a masonry doc return ( <div className={`collectionStackingView-${this.isStackingView ? 'columnDoc' : 'masonryDoc'}`} key={d[Id]} style={style}> - {this.getDisplayDoc(d, width, i)} + {this.getDisplayDoc(d, width, trans, i)} </div> ); }); @@ -309,7 +310,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection childFitWidth = (doc: Doc) => Cast(this.Document.childLayoutFitWidth, 'boolean', this._props.childLayoutFitWidth?.(doc) ?? Cast(doc.layout_fitWidth, 'boolean', null)); // this is what renders the document that you see on the screen // called in Children: this actually adds a document to our children list - getDisplayDoc(doc: Doc, width: () => number, count: number) { + getDisplayDoc(doc: Doc, width: () => number, trans: () => string, count: number) { const dataDoc = doc.isTemplateDoc || doc.isTemplateForField ? this._props.TemplateDataDocument : undefined; const height = () => this.getDocHeight(doc); const panelHeight = () => (this.isStackingView ? height() : Math.min(height(), this._props.PanelHeight())); @@ -330,6 +331,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection layout_fitWidth={this.childFitWidth} isContentActive={doc.onClick ? this.isChildButtonContentActive : this.isChildContentActive} onKey={this.onKeyDown} + DataTransition={trans} onBrowseClickScript={this._props.onBrowseClickScript} isDocumentActive={this.isContentActive} LayoutTemplate={this._props.childLayoutTemplate} @@ -379,6 +381,10 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection } return maxWidth; } + getDocTransition(d?: Doc) { + if (!d) return ''; + return StrCast(d.dataTransition); + } getDocHeight(d?: Doc) { if (!d || d.hidden) return 0; const childLayoutDoc = Doc.Layout(d, this._props.childLayoutTemplate?.()); diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx index ee5147428..38f6aa3e7 100644 --- a/src/client/views/collections/CollectionTimeView.tsx +++ b/src/client/views/collections/CollectionTimeView.tsx @@ -200,8 +200,7 @@ export class CollectionTimeView extends CollectionSubView() { } menuCallback = (x: number, y: number) => { ContextMenu.Instance.clearItems(); - const docItems: ContextMenuProps[] = []; - const keySet: Set<string> = new Set(); + const keySet: Set<string> = new Set(['tags']); this.childLayoutPairs.map(pair => this._allFacets @@ -209,7 +208,9 @@ export class CollectionTimeView extends CollectionSubView() { .filter(fieldKey => fieldKey[0] !== '_' && (fieldKey === 'tags' || fieldKey[0] === toUpper(fieldKey)[0])) .map(fieldKey => keySet.add(fieldKey)) ); - Array.from(keySet).map(fieldKey => docItems.push({ description: ':' + fieldKey, event: () => (this.layoutDoc._pivotField = fieldKey), icon: 'compress-arrows-alt' })); + + const docItems: ContextMenuProps[] = Array.from(keySet).map(fieldKey => + ({ description: ':' + fieldKey, event: () => (this.layoutDoc._pivotField = fieldKey), icon: 'compress-arrows-alt' })); // prettier-ignore docItems.push({ description: ':default', event: () => (this.layoutDoc._pivotField = undefined), icon: 'compress-arrows-alt' }); ContextMenu.Instance.addItem({ description: 'Pivot Fields ...', subitems: docItems, icon: 'eye' }); ContextMenu.Instance.displayMenu(x, y, ':'); diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 741013148..786301136 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -8,8 +8,8 @@ import { listSpec } from '../../../fields/Schema'; import { ScriptField } from '../../../fields/ScriptField'; import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; -import { emptyFunction, returnAll, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnNone, returnOne, returnTrue, returnZero } from '../../../Utils'; -import { DocUtils } from '../../documents/Documents'; +import { emptyFunction, returnAll, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnNone, returnOne, returnTrue, returnZero, Utils } from '../../../Utils'; +import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentManager } from '../../util/DocumentManager'; import { DragManager, dropActionType } from '../../util/DragManager'; import { SelectionManager } from '../../util/SelectionManager'; @@ -27,6 +27,7 @@ import { CollectionFreeFormView } from './collectionFreeForm'; import { CollectionSubView } from './CollectionSubView'; import './CollectionTreeView.scss'; import { TreeView } from './TreeView'; +import { ScriptingGlobals } from '../../util/ScriptingGlobals'; const _global = (window /* browser */ || global) /* node */ as any; export type collectionTreeViewProps = { @@ -51,6 +52,7 @@ export enum TreeViewType { @observer export class CollectionTreeView extends CollectionSubView<Partial<collectionTreeViewProps>>() { + public static AddTreeFunc = 'addTreeFolder(this.embedContainer)'; private _treedropDisposer?: DragManager.DragDropDisposer; private _mainEle?: HTMLDivElement; private _titleRef?: HTMLDivElement | HTMLInputElement | null; @@ -140,6 +142,17 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree if ((this._mainEle = ele)) this._treedropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.Document, this.onInternalPreDrop.bind(this)); }; + protected onInternalDrop(e: Event, de: DragManager.DropEvent) { + const res = super.onInternalDrop(e, de); + if (res && de.complete.docDragData) { + if (this.Document !== Doc.MyRecentlyClosed) + de.complete.docDragData.droppedDocuments.forEach(doc => { + if (this.Document !== Doc.MyRecentlyClosed) Doc.RemoveDocFromList(Doc.MyRecentlyClosed, undefined, doc); + }); + } + return res; + } + protected onInternalPreDrop = (e: Event, de: DragManager.DropEvent, dropAction: dropActionType) => { const dragData = de.complete.docDragData; if (dragData) { @@ -150,9 +163,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree } }; - configDrag = (dragData: DragManager.DocumentDragData) => { - dragData.treeViewDoc = this.Document; - }; + dragConfig = (dragData: DragManager.DocumentDragData) => (dragData.treeViewDoc = this.Document); screenToLocalTransform = () => this.ScreenToLocalBoxXf().translate(0, -this._headerHeight); @@ -163,34 +174,41 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree const value = DocListCast(targetDataDoc[this._props.fieldKey]); const result = value.filter(v => !docs.includes(v)); if ((doc instanceof Doc ? [doc] : doc).some(doc => SelectionManager.Views.some(dv => Doc.AreProtosEqual(dv.Document, doc)))) SelectionManager.DeselectAll(); - if (result.length !== value.length && doc instanceof Doc) { - const ind = DocListCast(targetDataDoc[this._props.fieldKey]).indexOf(doc); - const prev = ind && DocListCast(targetDataDoc[this._props.fieldKey])[ind - 1]; - this._props.removeDocument?.(doc); - if (ind > 0 && prev) { - FormattedTextBox.SetSelectOnLoad(prev); - DocumentManager.Instance.getDocumentView(prev, this.DocumentView?.())?.select(false); + if (result.length !== value.length) { + if (doc instanceof Doc) { + const ind = DocListCast(targetDataDoc[this._props.fieldKey]).indexOf(doc); + const prev = ind && DocListCast(targetDataDoc[this._props.fieldKey])[ind - 1]; + this._props.removeDocument?.(doc); + if (ind > 0 && prev) { + FormattedTextBox.SetSelectOnLoad(prev); + DocumentManager.Instance.getDocumentView(prev, this.DocumentView?.())?.select(false); + } + return true; } - return true; + return this._props.removeDocument?.(doc) ?? false; } return false; }; @action addDoc = (docs: Doc | Doc[], relativeTo: Opt<Doc>, before?: boolean): boolean => { - const doAddDoc = (doc: Doc | Doc[]) => - (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => { - const res = flg && Doc.AddDocToList(this.Document[DocData], this._props.fieldKey, doc, relativeTo, before); - res && Doc.SetContainer(doc, this.Document); - return res; - }, true); + const doclist = docs instanceof Doc ? [docs] : docs; + const addDocRelativeTo = (doc: Doc | Doc[]) => doclist.reduce((flg, doc) => flg && Doc.AddDocToList(this.Document[DocData], this._props.fieldKey, doc, relativeTo, before), true); if (this.Document.resolvedDataDoc instanceof Promise) return false; - return relativeTo === undefined ? this._props.addDocument?.(docs) || false : doAddDoc(docs); + const res = relativeTo === undefined ? this._props.addDocument?.(docs) || false : addDocRelativeTo(docs); + res && + doclist.forEach(doc => { + Doc.SetContainer(doc, this.Document); + if (this.Document !== Doc.MyRecentlyClosed) Doc.RemoveDocFromList(Doc.MyRecentlyClosed, undefined, doc); + }); + return res; }; onContextMenu = (e: React.MouseEvent): void => { // need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout + const layoutItems: ContextMenuProps[] = []; + const menuDoc = ScriptCast(Cast(this.layoutDoc.layout_headerButton, Doc, null)?.onClick).script.originalScript === CollectionTreeView.AddTreeFunc; + menuDoc && layoutItems.push({ description: 'Create new folder', event: () => CollectionTreeView.addTreeFolder(this.Document), icon: 'paint-brush' }); if (!Doc.noviceMode) { - const layoutItems: ContextMenuProps[] = []; layoutItems.push({ description: 'Make tree state ' + (this.Document.treeView_OpenIsTransient ? 'persistent' : 'transient'), event: () => (this.Document.treeView_OpenIsTransient = !this.Document.treeView_OpenIsTransient), @@ -198,7 +216,9 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree }); layoutItems.push({ description: (this.Document.treeView_HideHeaderFields ? 'Show' : 'Hide') + ' Header Fields', event: () => (this.Document.treeView_HideHeaderFields = !this.Document.treeView_HideHeaderFields), icon: 'paint-brush' }); layoutItems.push({ description: (this.Document.treeView_HideTitle ? 'Show' : 'Hide') + ' Title', event: () => (this.Document.treeView_HideTitle = !this.Document.treeView_HideTitle), icon: 'paint-brush' }); - ContextMenu.Instance.addItem({ description: 'Options...', subitems: layoutItems, icon: 'eye' }); + } + ContextMenu.Instance.addItem({ description: 'Options...', subitems: layoutItems, icon: 'eye' }); + if (!Doc.noviceMode) { const existingOnClick = ContextMenu.Instance.findByDescription('OnClick...'); const onClicks: ContextMenuProps[] = existingOnClick && 'subitems' in existingOnClick ? existingOnClick.subitems : []; onClicks.push({ description: 'Edit onChecked Script', event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.Document, undefined, 'onCheckedClick'), 'edit onCheckedClick'), icon: 'edit' }); @@ -467,4 +487,13 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree </div> ); } + static addTreeFolder(container: Doc) { + TreeView._editTitleOnLoad = { id: Utils.GenerateGuid(), parent: undefined }; + const opts = { title: 'Untitled folder', _dragOnlyWithinContainer: true, isFolder: true }; + return Doc.AddDocToList(container, 'data', Docs.Create.TreeDocument([], opts, TreeView._editTitleOnLoad.id)); + } } + +ScriptingGlobals.add(function addTreeFolder(doc: Doc) { + CollectionTreeView.addTreeFolder(doc); +}); diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx index 9bc3ef822..02aa76d82 100644 --- a/src/client/views/collections/TabDocView.tsx +++ b/src/client/views/collections/TabDocView.tsx @@ -520,6 +520,7 @@ interface TabMiniThumbProps { miniLeft: () => number; } +@observer class TabMiniThumb extends React.Component<TabMiniThumbProps> { render() { return <div className="miniThumb" style={{ width: `${this.props.miniWidth()}% `, height: `${this.props.miniHeight()}% `, left: `${this.props.miniLeft()}% `, top: `${this.props.miniTop()}% ` }} />; diff --git a/src/client/views/collections/TreeView.scss b/src/client/views/collections/TreeView.scss index 0a1946f09..2ab1a5ac1 100644 --- a/src/client/views/collections/TreeView.scss +++ b/src/client/views/collections/TreeView.scss @@ -128,6 +128,10 @@ position: relative; z-index: 1; + .treeView-rightButtons > .iconButton-container { + min-height: unset; + } + .treeView-background { width: 100%; height: 100%; @@ -220,13 +224,13 @@ } .treeView-header-above { - border-top: black 1px solid; + border-top: red 1px solid; } .treeView-header-below { - border-bottom: black 1px solid; + border-bottom: red 1px solid; } .treeView-header-inside { - border: black 1px solid; + border: red 1px solid; } diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx index be5737a25..85f7cf7fe 100644 --- a/src/client/views/collections/TreeView.tsx +++ b/src/client/views/collections/TreeView.tsx @@ -383,7 +383,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { makeFolder = () => { const folder = Docs.Create.TreeDocument([], { title: 'Untitled folder', _dragOnlyWithinContainer: true, isFolder: true }); TreeView._editTitleOnLoad = { id: folder[Id], parent: this._props.parentTreeView }; - return this._props.addDocument(folder); + return this.localAdd(folder); }; preTreeDrop = (e: Event, de: DragManager.DropEvent, docDropAction: dropActionType) => { @@ -424,6 +424,16 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { return false; }; + localAdd = (doc: Doc | Doc[]) => { + const innerAdd = (doc: Doc) => { + const dataIsComputed = ComputedField.WithoutComputed(() => FieldValue(this.dataDoc[this.fieldKey])) instanceof ComputedField; + const added = (!dataIsComputed || (this.dropping && this.moving)) && Doc.AddDocToList(this.dataDoc, this.fieldKey, doc); + dataIsComputed && Doc.SetContainer(doc, DocCast(this.Document.embedContainer)); + return added; + }; + return (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && innerAdd(doc), true as boolean); + }; + dropping: boolean = false; dropDocuments( droppedDocuments: Doc[], @@ -436,16 +446,8 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { canEmbed?: boolean ) { const parentAddDoc = (doc: Doc | Doc[]) => this._props.addDocument(doc, undefined, undefined, before); - const localAdd = (doc: Doc | Doc[]) => { - const innerAdd = (doc: Doc) => { - const dataIsComputed = ComputedField.WithoutComputed(() => FieldValue(this.dataDoc[this.fieldKey])) instanceof ComputedField; - const added = (!dataIsComputed || (this.dropping && this.moving)) && Doc.AddDocToList(this.dataDoc, this.fieldKey, doc); - dataIsComputed && Doc.SetContainer(doc, DocCast(this.Document.embedContainer)); - return added; - }; - return (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && innerAdd(doc), true as boolean); - }; - const addDoc = inside ? localAdd : parentAddDoc; + + const addDoc = inside ? this.localAdd : parentAddDoc; const canAdd = !StrCast((inside ? this.Document : this._props.treeViewParent)?.treeView_FreezeChildren).includes('add') || forceAdd; if (canAdd && (dropAction !== 'inSame' || droppedDocuments.every(d => d.embedContainer === this._props.parentTreeView?.Document))) { const move = (!dropAction || canEmbed || dropAction === 'proto' || dropAction === 'move' || dropAction === 'same' || dropAction === 'inSame') && moveDocument; @@ -839,14 +841,13 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { }; contextMenuItems = () => { const makeFolder = { script: ScriptField.MakeFunction(`scriptContext.makeFolder()`, { scriptContext: 'any' })!, icon: 'folder-plus', label: 'New Folder' }; - const folderOp = this.childDocs?.length ? [makeFolder] : []; const openEmbedding = { script: ScriptField.MakeFunction(`openDoc(getEmbedding(this), "${OpenWhere.addRight}")`)!, icon: 'copy', label: 'Open New Embedding' }; const focusDoc = { script: ScriptField.MakeFunction(`DocFocusOrOpen(this)`)!, icon: 'eye', label: 'Focus or Open' }; const reopenDoc = { script: ScriptField.MakeFunction(`DocFocusOrOpen(this)`)!, icon: 'eye', label: 'Reopen' }; return [ ...(this._props.contextMenuItems ?? []).filter(mi => (!mi.filter ? true : mi.filter.script.run({ doc: this.Document })?.result)), ...(this.Document.isFolder - ? folderOp + ? [makeFolder] : Doc.IsSystem(this.Document) ? [] : this.treeView.fileSysMode && this.Document === this.Document[DocData] @@ -993,6 +994,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { onClickScript={this.onChildClick} onDoubleClickScript={this.onChildDoubleClick} dragAction={this._props.dragAction} + dragConfig={this.treeView.dragConfig} moveDocument={this.move} removeDocument={this._props.removeDoc} ScreenToLocalTransform={this.getTransform} @@ -1328,9 +1330,3 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { }); } } - -ScriptingGlobals.add(function TreeView_addNewFolder() { - TreeView._editTitleOnLoad = { id: Utils.GenerateGuid(), parent: undefined }; - const opts = { title: 'Untitled folder', _dragOnlyWithinContainer: true, isFolder: true }; - return Doc.AddDocToList(Doc.MyFilesystem, 'data', Docs.Create.TreeDocument([], opts, TreeView._editTitleOnLoad.id)); -}); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss deleted file mode 100644 index b44acfce8..000000000 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss +++ /dev/null @@ -1,12 +0,0 @@ -.collectionfreeformlinkview-linkLine { - stroke: black; - opacity: 0.8; - stroke-width: 3px; - transition: opacity 0.5s ease-in; - fill: transparent; -} -.collectionfreeformlinkview-linkText { - stroke: rgb(0, 0, 0); - pointer-events: all; - cursor: move; -} diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx deleted file mode 100644 index 1b9627bb6..000000000 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx +++ /dev/null @@ -1,320 +0,0 @@ -import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx'; -import { observer } from 'mobx-react'; -import * as React from 'react'; -import { Doc, Field } from '../../../../fields/Doc'; -import { Brushed, DocCss } from '../../../../fields/DocSymbols'; -import { Id } from '../../../../fields/FieldSymbols'; -import { List } from '../../../../fields/List'; -import { Cast, NumCast, StrCast } from '../../../../fields/Types'; -import { emptyFunction, setupMoveUpEvents, Utils } from '../../../../Utils'; -import { LinkManager } from '../../../util/LinkManager'; -import { SelectionManager } from '../../../util/SelectionManager'; -import { SettingsManager } from '../../../util/SettingsManager'; -import { SnappingManager } from '../../../util/SnappingManager'; -import { Colors } from '../../global/globalEnums'; -import { DocumentView } from '../../nodes/DocumentView'; -import { ObservableReactComponent } from '../../ObservableReactComponent'; -import './CollectionFreeFormLinkView.scss'; - -export interface CollectionFreeFormLinkViewProps { - A: DocumentView; - B: DocumentView; - LinkDocs: Doc[]; -} - -@observer -export class CollectionFreeFormLinkView extends ObservableReactComponent<CollectionFreeFormLinkViewProps> { - @observable _opacity: number = 0; - @observable _start = 0; - _anchorDisposer: IReactionDisposer | undefined; - _timeout: NodeJS.Timeout | undefined; - constructor(props: any) { - super(props); - makeObservable(this); - } - - componentWillUnmount() { - this._anchorDisposer?.(); - } - @action timeout: any = action(() => Date.now() < this._start++ + 1000 && (this._timeout = setTimeout(this.timeout, 25))); - componentDidMount() { - this._anchorDisposer = reaction( - () => [ - this._props.A.screenToViewTransform(), - Cast(Cast(Cast(this._props.A.Document, Doc, null)?.link_anchor_1, Doc, null)?.annotationOn, Doc, null)?.layout_scrollTop, - Cast(Cast(Cast(this._props.A.Document, Doc, null)?.link_anchor_1, Doc, null)?.annotationOn, Doc, null)?.[DocCss], - this._props.B.screenToViewTransform(), - Cast(Cast(Cast(this._props.A.Document, Doc, null)?.link_anchor_2, Doc, null)?.annotationOn, Doc, null)?.layout_scrollTop, - Cast(Cast(Cast(this._props.A.Document, Doc, null)?.link_anchor_2, Doc, null)?.annotationOn, Doc, null)?.[DocCss], - ], - action(() => { - this._start = Date.now(); - this._timeout && clearTimeout(this._timeout); - this._timeout = setTimeout(this.timeout, 25); - setTimeout(this.placeAnchors, 10); // when docs are dragged, their transforms will update before a render has been performed. placeanchors needs to come after a render to find things in the dom. a 0 timeout will still come before the render - }), - { fireImmediately: true } - ); - } - placeAnchors = () => { - const { A, B, LinkDocs } = this._props; - const linkDoc = LinkDocs[0]; - if (SnappingManager.IsDragging || !A.ContentDiv || !B.ContentDiv) return; - setTimeout( - action(() => (this._opacity = 0.75)), - 0 - ); // since the render code depends on querying the Dom through getBoudndingClientRect, we need to delay triggering render() - setTimeout( - action(() => (!LinkDocs.length || !(linkDoc.link_displayLine || Doc.UserDoc().showLinkLines)) && (this._opacity = 0.05)), - 750 - ); // this will unhighlight the link line. - const a = A.ContentDiv.getBoundingClientRect(); - const b = B.ContentDiv.getBoundingClientRect(); - const { left: aleft, top: atop, width: awidth, height: aheight } = A.ContentDiv.parentElement!.getBoundingClientRect(); - const { left: bleft, top: btop, width: bwidth, height: bheight } = B.ContentDiv.parentElement!.getBoundingClientRect(); - const apt = Utils.closestPtBetweenRectangles(aleft, atop, awidth, aheight, bleft, btop, bwidth, bheight, a.left + a.width / 2, a.top + a.height / 2); - const bpt = Utils.closestPtBetweenRectangles(bleft, btop, bwidth, bheight, aleft, atop, awidth, aheight, apt.point.x, apt.point.y); - - // really hacky stuff to make the LinkAnchorBox display where we want it to: - // if there's an element in the DOM with a classname containing a link anchor's id, - // then that DOM element is a hyperlink source for the current anchor and we want to place our link box at it's top right - // otherwise, we just use the computed nearest point on the document boundary to the target Document - const targetAhyperlink = Array.from(window.document.getElementsByClassName((linkDoc.link_anchor_1 as Doc)[Id])).lastElement(); - const targetBhyperlink = Array.from(window.document.getElementsByClassName((linkDoc.link_anchor_2 as Doc)[Id])).lastElement(); - if ((!targetAhyperlink && !a.width) || (!targetBhyperlink && !b.width)) return; - if (!targetAhyperlink) { - if (linkDoc.link_autoMoveAnchors) { - linkDoc.link_anchor_1_x = ((apt.point.x - aleft) / awidth) * 100; - linkDoc.link_anchor_1_y = ((apt.point.y - atop) / aheight) * 100; - } - } else { - const m = targetAhyperlink.getBoundingClientRect(); - const mp = A.screenToViewTransform().transformPoint(m.right, m.top + 5); - const mpx = mp[0] / A._props.PanelWidth(); - const mpy = mp[1] / A._props.PanelHeight(); - if (mpx >= 0 && mpx <= 1) linkDoc.link_anchor_1_x = mpx * 100; - if (mpy >= 0 && mpy <= 1) linkDoc.link_anchor_1_y = mpy * 100; - if (getComputedStyle(targetAhyperlink).fontSize === '0px') linkDoc.opacity = 0; - else linkDoc.opacity = 1; - } - if (!targetBhyperlink) { - if (linkDoc.link_autoMoveAnchors) { - linkDoc.link_anchor_2_x = ((bpt.point.x - bleft) / bwidth) * 100; - linkDoc.link_anchor_2_y = ((bpt.point.y - btop) / bheight) * 100; - } - } else { - const m = targetBhyperlink.getBoundingClientRect(); - const mp = B.screenToViewTransform().transformPoint(m.right, m.top + 5); - const mpx = mp[0] / B._props.PanelWidth(); - const mpy = mp[1] / B._props.PanelHeight(); - if (mpx >= 0 && mpx <= 1) linkDoc.link_anchor_2_x = mpx * 100; - if (mpy >= 0 && mpy <= 1) linkDoc.link_anchor_2_y = mpy * 100; - if (getComputedStyle(targetBhyperlink).fontSize === '0px') linkDoc.opacity = 0; - else linkDoc.opacity = 1; - } - }; - - pointerDown = (e: React.PointerEvent) => { - setupMoveUpEvents( - this, - e, - (e, down, delta) => { - this._props.LinkDocs[0].link_relationship_OffsetX = NumCast(this._props.LinkDocs[0].link_relationship_OffsetX) + delta[0]; - this._props.LinkDocs[0].link_relationship_OffsetY = NumCast(this._props.LinkDocs[0].link_relationship_OffsetY) + delta[1]; - return false; - }, - emptyFunction, - action(() => { - SelectionManager.DeselectAll(); - SelectionManager.SelectSchemaViewDoc(this._props.LinkDocs[0], true); - LinkManager.currentLink = this._props.LinkDocs[0]; - this.toggleProperties(); - // OverlayView.Instance.addElement( - // <LinkEditor sourceDoc={this._props.A.Document} linkDoc={this._props.LinkDocs[0]} - // showLinks={action(() => { })} - // />, { x: 300, y: 300 }); - }) - ); - }; - - visibleY = (el: any) => { - let rect = el.getBoundingClientRect(); - const top = rect.top, - height = rect.height; - var el = el.parentNode; - while (el && el !== document.body) { - if (el.className === 'tabDocView-content') break; - rect = el.getBoundingClientRect?.(); - if (rect?.width) { - if (top <= rect.bottom === false && getComputedStyle(el).overflow === 'hidden') return rect.bottom; - // Check if the element is out of view due to a container scrolling - if (top + height <= rect.top && getComputedStyle(el).overflow === 'hidden') return rect.top; - } - el = el.parentNode; - } - // Check its within the document viewport - return top; //top <= document.documentElement.clientHeight && getComputedStyle(document.documentElement).overflow === "hidden"; - }; - visibleX = (el: any) => { - let rect = el.getBoundingClientRect(); - const left = rect.left, - width = rect.width; - var el = el.parentNode; - while (el && el !== document.body) { - rect = el?.getBoundingClientRect(); - if (rect?.width) { - if (left <= rect.right === false && getComputedStyle(el).overflow === 'hidden') return rect.right; - // Check if the element is out of view due to a container scrolling - if (left + width <= rect.left && getComputedStyle(el).overflow === 'hidden') return rect.left; - } - el = el.parentNode; - } - // Check its within the document viewport - return left; //top <= document.documentElement.clientHeight && getComputedStyle(document.documentElement).overflow === "hidden"; - }; - - @action - toggleProperties = () => { - if ((SettingsManager.Instance.propertiesWidth ?? 0) < 100) { - SettingsManager.Instance.propertiesWidth = 250; - } - }; - - @action - onClickLine = () => { - SelectionManager.DeselectAll(); - SelectionManager.SelectSchemaViewDoc(this._props.LinkDocs[0], true); - LinkManager.currentLink = this._props.LinkDocs[0]; - this.toggleProperties(); - }; - - @computed.struct get renderData() { - this._start; - SnappingManager.IsDragging; - const { A, B, LinkDocs } = this._props; - if (!A.ContentDiv || !B.ContentDiv || !LinkDocs.length) return undefined; - const acont = A.ContentDiv.getElementsByClassName('linkAnchorBox-cont'); - const bcont = B.ContentDiv.getElementsByClassName('linkAnchorBox-cont'); - const adiv = acont.length ? acont[0] : A.ContentDiv; - const bdiv = bcont.length ? bcont[0] : B.ContentDiv; - for (let apdiv = adiv; apdiv; apdiv = apdiv.parentElement as any) if ((apdiv as any).hidden) return; - for (let bpdiv = bdiv; bpdiv; bpdiv = bpdiv.parentElement as any) if ((bpdiv as any).hidden) return; - const a = adiv.getBoundingClientRect(); - const b = bdiv.getBoundingClientRect(); - const atop = this.visibleY(adiv); - const btop = this.visibleY(bdiv); - if (!a.width || !b.width) return undefined; - const aDocBounds = (A._props as any).DocumentView?.().getBounds || { left: 0, right: 0, top: 0, bottom: 0 }; - const bDocBounds = (B._props as any).DocumentView?.().getBounds || { left: 0, right: 0, top: 0, bottom: 0 }; - const aleft = this.visibleX(adiv); - const bleft = this.visibleX(bdiv); - const aclipped = aleft !== a.left || atop !== a.top; - const bclipped = bleft !== b.left || btop !== b.top; - if (aclipped && bclipped) return undefined; - const clipped = aclipped || bclipped; - const pt1inside = NumCast(LinkDocs[0].link_anchor_1_x) % 100 !== 0 && NumCast(LinkDocs[0].link_anchor_1_y) % 100 !== 0; - const pt2inside = NumCast(LinkDocs[0].link_anchor_2_x) % 100 !== 0 && NumCast(LinkDocs[0].link_anchor_2_y) % 100 !== 0; - const pt1 = [aleft + a.width / 2, atop + a.height / 2]; - const pt2 = [bleft + b.width / 2, btop + b.width / 2]; - const pt2vec = pt2inside ? [-0.7071, 0.7071] : [(bDocBounds.left + bDocBounds.right) / 2 - pt2[0], (bDocBounds.top + bDocBounds.bottom) / 2 - pt2[1]]; - const pt1vec = pt1inside ? [-0.7071, 0.7071] : [(aDocBounds.left + aDocBounds.right) / 2 - pt1[0], (aDocBounds.top + aDocBounds.bottom) / 2 - pt1[1]]; - const pt1len = Math.sqrt(pt1vec[0] * pt1vec[0] + pt1vec[1] * pt1vec[1]); - const pt2len = Math.sqrt(pt2vec[0] * pt2vec[0] + pt2vec[1] * pt2vec[1]); - const ptlen = Math.sqrt((pt1[0] - pt2[0]) * (pt1[0] - pt2[0]) + (pt1[1] - pt2[1]) * (pt1[1] - pt2[1])) / 2; - const pt1norm = clipped ? [0, 0] : [-(pt1vec[0] / pt1len) * ptlen, -(pt1vec[1] / pt1len) * ptlen]; - const pt2norm = clipped ? [0, 0] : [-(pt2vec[0] / pt2len) * ptlen, -(pt2vec[1] / pt2len) * ptlen]; - const pt1normlen = Math.sqrt(pt1norm[0] * pt1norm[0] + pt1norm[1] * pt1norm[1]) || 1; - const pt2normlen = Math.sqrt(pt2norm[0] * pt2norm[0] + pt2norm[1] * pt2norm[1]) || 1; - const pt1normalized = [pt1norm[0] / pt1normlen, pt1norm[1] / pt1normlen]; - const pt2normalized = [pt2norm[0] / pt2normlen, pt2norm[1] / pt2normlen]; - const aActive = A.IsSelected || A.Document[Brushed]; - const bActive = B.IsSelected || B.Document[Brushed]; - - const textX = (Math.min(pt1[0], pt2[0]) + Math.max(pt1[0], pt2[0])) / 2 + NumCast(LinkDocs[0].link_relationship_OffsetX); - const textY = (pt1[1] + pt2[1]) / 2 + NumCast(LinkDocs[0].link_relationship_OffsetY); - const link = this._props.LinkDocs[0]; - return { - a, - b, - pt1norm, - pt2norm, - aActive, - bActive, - textX, - textY, - // fully connected - // pt1, - // pt2, - // this code adds space between links - pt1: link.link_displayArrow ? [pt1[0] + pt1normalized[0] * 3 * NumCast(link.link_displayArrow_scale, 4), pt1[1] + pt1normalized[1] * 3 * NumCast(link.link_displayArrow_scale, 3)] : pt1, - pt2: link.link_displayArrow ? [pt2[0] + pt2normalized[0] * 3 * NumCast(link.link_displayArrow_scale, 4), pt2[1] + pt2normalized[1] * 3 * NumCast(link.link_displayArrow_scale, 3)] : pt2, - }; - } - - render() { - if (!this.renderData) return null; - - const link = this._props.LinkDocs[0]; - const { a, b, pt1norm, pt2norm, aActive, bActive, textX, textY, pt1, pt2 } = this.renderData; - const linkRelationship = Field.toString(link?.link_relationship as any as Field); //get string representing relationship - const linkRelationshipList = Doc.UserDoc().link_relationshipList as List<string>; - const linkColorList = Doc.UserDoc().link_ColorList as List<string>; - const linkRelationshipSizes = Doc.UserDoc().link_relationshipSizes as List<number>; - const currRelationshipIndex = linkRelationshipList.indexOf(linkRelationship); - const linkDescription = Field.toString(link.link_description as any as Field).split('\n')[0]; - - const linkSize = Doc.noviceMode || currRelationshipIndex === -1 || currRelationshipIndex >= linkRelationshipSizes.length ? -1 : linkRelationshipSizes[currRelationshipIndex]; - - //access stroke color using index of the relationship in the color list (default black) - // const stroke = currRelationshipIndex === -1 || currRelationshipIndex >= linkColorList.length ? StrCast(link._backgroundColor, 'black') : linkColorList[currRelationshipIndex]; - const stroke = link.link_color ? Field.toString(link.link_color as any as Field) : '#449ef7'; - // const hexStroke = this.rgbToHex(stroke) - - //calculate stroke width/thickness based on the relative importance of the relationshipship (i.e. how many links the relationship has) - //thickness varies linearly from 3px to 12px for increasing link count - const strokeWidth = linkSize === -1 ? '3px' : Math.floor(2 + 10 * (linkSize / Math.max(...linkRelationshipSizes))) + 'px'; - - const arrowScale = NumCast(link.link_displayArrow_scale, 3); - return link.opacity === 0 || !a.width || !b.width || (!(Doc.UserDoc().showLinkLines || link.link_displayLine) && !aActive && !bActive) ? null : ( - <> - <defs> - <marker id={`${link[Id] + 'arrowhead'}`} markerWidth={`${4 * arrowScale}`} markerHeight={`${3 * arrowScale}`} refX="0" refY={`${1.5 * arrowScale}`} orient="auto"> - <polygon points={`0 0, ${3 * arrowScale} ${1.5 * arrowScale}, 0 ${3 * arrowScale}`} fill={stroke} /> - </marker> - <filter id="outline"> - <feMorphology in="SourceAlpha" result="expanded" operator="dilate" radius="1" /> - <feFlood floodColor={`${Colors.DARK_GRAY}`} /> - <feComposite in2="expanded" operator="in" /> - <feComposite in="SourceGraphic" /> - </filter> - <filter x="0" y="0" width="1" height="1" id={`${link[Id] + 'background'}`}> - <feFlood floodColor={`${StrCast(link._backgroundColor, 'white')}`} result="bg" /> - <feMerge> - <feMergeNode in="bg" /> - <feMergeNode in="SourceGraphic" /> - </feMerge> - </filter> - </defs> - <path - filter={LinkManager.currentLink === link ? 'url(#outline)' : ''} - fill="pink" - stroke="antiquewhite" - strokeWidth="4" - className="collectionfreeformlinkview-linkLine" - style={{ pointerEvents: 'visibleStroke', opacity: this._opacity, stroke, strokeWidth }} - onClick={this.onClickLine} - d={`M ${pt1[0]} ${pt1[1]} C ${pt1[0] + pt1norm[0]} ${pt1[1] + pt1norm[1]}, ${pt2[0] + pt2norm[0]} ${pt2[1] + pt2norm[1]}, ${pt2[0]} ${pt2[1]}`} - markerEnd={link.link_displayArrow ? `url(#${link[Id] + 'arrowhead'})` : ''} - /> - {link.show_description && - (textX === undefined || !linkDescription ? null : ( - <text filter={`url(#${link[Id] + 'background'})`} className="collectionfreeformlinkview-linkText" x={textX} y={textY} onPointerDown={this.pointerDown}> - <tspan> </tspan> - <tspan dy="2">{linkDescription.substring(0, 50) + (linkDescription.length > 50 ? '...' : '')}</tspan> - <tspan dy="2"> </tspan> - </text> - ))} - </> - ); - } -} diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss deleted file mode 100644 index 4ada1731f..000000000 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss +++ /dev/null @@ -1,13 +0,0 @@ -// TODO: change z-index to -1 when a modal is active? - -.collectionfreeformlinksview-svgCanvas { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - pointer-events: none; -} -.collectionfreeformlinksview-container { - pointer-events: none; -} diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx deleted file mode 100644 index e5b6c366f..000000000 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { computed } from 'mobx'; -import { observer } from 'mobx-react'; -import * as React from 'react'; -import { Id } from '../../../../fields/FieldSymbols'; -import { DocumentManager } from '../../../util/DocumentManager'; -import { LightboxView } from '../../LightboxView'; -import { CollectionFreeFormLinkView } from './CollectionFreeFormLinkView'; -import './CollectionFreeFormLinksView.scss'; - -@observer -export class CollectionFreeFormLinksView extends React.Component { - @computed get uniqueConnections() { - return Array.from(new Set(DocumentManager.Instance.LinkedDocumentViews)) - .filter(c => !LightboxView.LightboxDoc || (LightboxView.Contains(c.a) && LightboxView.Contains(c.b))) - .map(c => <CollectionFreeFormLinkView key={c.l[Id]} A={c.a} B={c.b} LinkDocs={[c.l]} />); - } - - render() { - return ( - <div className="collectionfreeformlinksview-container"> - <svg className="collectionfreeformlinksview-svgCanvas">{this.uniqueConnections}</svg> - </div> - ); - } -} diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss index 7d3acaea7..9e7d364ea 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss @@ -176,6 +176,11 @@ // touch action none means that the browser will handle none of the touch actions. this allows us to implement our own actions. touch-action: none; transform-origin: top left; + > svg { + height: 100%; + width: 100%; + margin: auto; + } .collectionfreeformview-placeholder { background: gray; @@ -270,34 +275,31 @@ padding: 10px; .msg { - position: relative; - // display: block; - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -o-user-select: none; - user-select: none; - + position: relative; + // display: block; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -o-user-select: none; + user-select: none; } - + .gif-container { - position: relative; - margin-top: 5px; - // display: block; - - justify-content: center; - align-items: center; - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -o-user-select: none; - user-select: none; - - + position: relative; + margin-top: 5px; + // display: block; + + justify-content: center; + align-items: center; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -o-user-select: none; + user-select: none; + .gif { background-color: transparent; height: 300px; } } - } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index fb9d42bc7..1e0840495 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -5,11 +5,12 @@ import { observer } from 'mobx-react'; import { computedFn } from 'mobx-utils'; import * as React from 'react'; import { DateField } from '../../../../fields/DateField'; -import { Doc, DocListCast, Opt } from '../../../../fields/Doc'; +import { Doc, DocListCast, Field, Opt } from '../../../../fields/Doc'; import { DocData, Height, Width } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; import { InkData, InkField, InkTool, PointData, Segment } from '../../../../fields/InkField'; import { List } from '../../../../fields/List'; +import { RichTextField } from '../../../../fields/RichTextField'; import { listSpec } from '../../../../fields/Schema'; import { ScriptField } from '../../../../fields/ScriptField'; import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; @@ -22,8 +23,8 @@ import { Docs, DocUtils } from '../../../documents/Documents'; import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; import { DocumentManager } from '../../../util/DocumentManager'; import { DragManager, dropActionType } from '../../../util/DragManager'; -import { FollowLinkScript } from '../../../util/LinkFollower'; import { ReplayMovements } from '../../../util/ReplayMovements'; +import { CompileScript } from '../../../util/Scripting'; import { ScriptingGlobals } from '../../../util/ScriptingGlobals'; import { SelectionManager } from '../../../util/SelectionManager'; import { freeformScrollMode, SettingsManager } from '../../../util/SettingsManager'; @@ -39,7 +40,7 @@ import { LightboxView } from '../../LightboxView'; import { CollectionFreeFormDocumentView, CollectionFreeFormDocumentViewWrapper } from '../../nodes/CollectionFreeFormDocumentView'; import { SchemaCSVPopUp } from '../../nodes/DataVizBox/SchemaCSVPopUp'; import { DocumentView, OpenWhere } from '../../nodes/DocumentView'; -import { FocusViewOptions, FieldViewProps } from '../../nodes/FieldView'; +import { FieldViewProps, FocusViewOptions } from '../../nodes/FieldView'; import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox'; import { PinProps, PresBox } from '../../nodes/trails/PresBox'; import { CreateImage } from '../../nodes/WebBoxRenderer'; @@ -78,6 +79,17 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection return 'CollectionFreeFormView(' + this.Document.title?.toString() + ')'; } // this makes mobx trace() statements more descriptive + @observable _paintedId = 'id' + Utils.GenerateGuid().replace(/-/g, ''); + @computed get paintFunc() { + const field = this.layoutDoc[this.fieldKey]; + const paintFunc = StrCast(Field.toJavascriptString(Cast(field, RichTextField, null)?.Text as Field)).trim(); + return !paintFunc + ? '' + : paintFunc.includes('dashDiv') + ? `const dashDiv = document.querySelector('#${this._paintedId}'); + (async () => { ${paintFunc} })()` + : paintFunc; + } constructor(props: any) { super(props); makeObservable(this); @@ -230,6 +242,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection onChildClickHandler = () => this._props.childClickScript || ScriptCast(this.Document.onChildClick); onChildDoubleClickHandler = () => this._props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick); elementFunc = () => this._layoutElements; + viewTransition = () => (this._panZoomTransition ? '' + this._panZoomTransition : undefined); fitContentOnce = () => { const vals = this.fitToContentVals; this.layoutDoc._freeform_panX = vals.bounds.cx; @@ -301,10 +314,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection focus = (anchor: Doc, options: FocusViewOptions) => { if (this._lightboxDoc) return; if (anchor === this.Document) { - if (options.willZoomCentered && options.zoomScale) { - this.fitContentOnce(); - options.didMove = true; - } + // if (options.willZoomCentered && options.zoomScale) { + // this.fitContentOnce(); + // options.didMove = true; + // } } if (anchor.type !== DocumentType.CONFIG && !DocListCast(this.Document[this.fieldKey ?? Doc.LayoutFieldKey(this.Document)]).includes(anchor)) return; const xfToCollection = options?.docTransform ?? Transform.Identity(); @@ -368,28 +381,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection !d._keepZWhenDragged && (d.zIndex = zsorted.length + 1 + i); // bringToFront } - if (this.layoutDoc._autoArrange || de.metaKey) { - const sorted = this.childLayoutPairs.slice().sort((a, b) => NumCast(a.layout.y) - NumCast(b.layout.y)); - sorted.splice( - sorted.findIndex(pair => pair.layout === refDoc), - 1 - ); - if (sorted.length && refDoc && NumCast(sorted[0].layout.y) < NumCast(refDoc.y)) { - const topIndexed = NumCast(refDoc.y) < NumCast(sorted[0].layout.y) + NumCast(sorted[0].layout._height) / 2; - const deltay = sorted.length > 1 ? NumCast(refDoc.y) - (NumCast(sorted[0].layout.y) + (topIndexed ? 0 : NumCast(sorted[0].layout._height))) : 0; - const deltax = sorted.length > 1 ? NumCast(refDoc.x) - NumCast(sorted[0].layout.x) : 0; - - let lastx = NumCast(refDoc.x); - let lasty = NumCast(refDoc.y) + (topIndexed ? 0 : NumCast(refDoc._height)); - runInAction(() => - sorted.slice(1).forEach((pair, i) => { - lastx = pair.layout.x = lastx + deltax; - lasty = (pair.layout.y = lasty + deltay) + (topIndexed ? 0 : NumCast(pair.layout._height)); - }) - ); - } - } - (docDragData.droppedDocuments.length === 1 || de.shiftKey) && this.updateClusterDocs(docDragData.droppedDocuments); return true; } @@ -416,27 +407,13 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection const [x, y] = this.screenToFreeformContentsXf.transformPoint(de.x, de.y); let added = false; // do nothing if link is dropped into any freeform view parent of dragged document - const source = - !linkDragData.dragDocument.embedContainer || linkDragData.dragDocument.embedContainer !== this.Document - ? Docs.Create.TextDocument('', { _width: 200, _height: 75, x, y, title: 'dropped annotation' }) - : Docs.Create.FontIconDocument({ - title: 'anchor', - icon_label: '', - followLinkToggle: true, - icon: 'map-pin', - x, - y, - backgroundColor: '#ACCEF7', - layout_hideAllLinks: true, - layout_hideLinkButton: true, - _width: 15, - _height: 15, - _xPadding: 0, - onClick: FollowLinkScript(), - }); + const source = Docs.Create.TextDocument('', { _width: 200, _height: 75, x, y, title: 'dropped annotation' }); added = this._props.addDocument?.(source) ? true : false; de.complete.linkDocument = DocUtils.MakeLink(linkDragData.linkSourceGetAnchor(), source, { link_relationship: 'annotated by:annotation of' }); // TODODO this is where in text links get passed - + if (de.complete.linkDocument) { + de.complete.linkDocument.layout_isSvg = true; + this.addDocument(de.complete.linkDocument); + } e.stopPropagation(); !added && e.preventDefault(); return added; @@ -1005,7 +982,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection if (!this.isAnnotationOverlay && clamp) { // this section wraps the pan position, horizontally and/or vertically whenever the content is panned out of the viewing bounds - const docs = this.childLayoutPairs.map(pair => pair.layout).filter(doc => doc instanceof Doc); + const docs = this.childLayoutPairs.map(pair => pair.layout).filter(doc => doc instanceof Doc && doc.type !== DocumentType.LINK); const measuredDocs = docs .map(doc => ({ pos: { x: NumCast(doc.x), y: NumCast(doc.y) }, size: { width: NumCast(doc._width), height: NumCast(doc._height) } })) .filter(({ pos, size }) => pos && size) @@ -1232,6 +1209,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection onKey={this.onKeyDown} onDoubleClickScript={this.onChildDoubleClickHandler} onBrowseClickScript={this.onBrowseClickHandler} + bringToFront={this.bringToFront} ScreenToLocalTransform={childLayout.z ? this.ScreenToLocalBoxXf : this.ScreenToContentsXf} PanelWidth={childLayout[Width]} PanelHeight={childLayout[Height]} @@ -1487,6 +1465,18 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection ); }) ); + + this._disposers.paintFunc = reaction( + () => ({ code: this.paintFunc, first: this._firstRender, width: this.Document._width, height: this.Document._height }), + ({ code, first }) => { + if (!code.includes('dashDiv')) { + const script = CompileScript(code, { params: { docView: 'any' }, typecheck: false, editable: true }); + if (script.compiled) script.run({ this: this.DocumentView?.() }); + } else code && !first && eval(code); + }, + { fireImmediately: true } + ); + this._disposers.layoutElements = reaction( // layoutElements can't be a computed value because doLayoutComputation() is an action that has side effect of updating clusters () => this.doInternalLayoutComputation, @@ -1630,9 +1620,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection toggleResetView = () => { this.dataDoc[this.autoResetFieldKey] = !this.dataDoc[this.autoResetFieldKey]; if (this.dataDoc[this.autoResetFieldKey]) { - this.dataDoc[this.panXFieldKey + '_reset'] = this.dataDoc[this.panXFieldKey]; - this.dataDoc[this.panYFieldKey + '_reset'] = this.dataDoc[this.panYFieldKey]; - this.dataDoc[this.scaleFieldKey + '_reset'] = this.dataDoc[this.scaleFieldKey]; + this.dataDoc[this.panXFieldKey + '_reset'] = this.layoutDoc[this.panXFieldKey]; + this.dataDoc[this.panYFieldKey + '_reset'] = this.layoutDoc[this.panYFieldKey]; + this.dataDoc[this.scaleFieldKey + '_reset'] = this.layoutDoc[this.scaleFieldKey]; } }; @@ -1737,12 +1727,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection const appearanceItems = appearance && 'subitems' in appearance ? appearance.subitems : []; !this.Document.isGroup && appearanceItems.push({ description: 'Reset View', event: this.resetView, icon: 'compress-arrows-alt' }); !this.Document.isGroup && appearanceItems.push({ description: 'Toggle Auto Reset View', event: this.toggleResetView, icon: 'compress-arrows-alt' }); - !Doc.noviceMode && - appearanceItems.push({ - description: 'Toggle auto arrange', - event: () => (this.layoutDoc._autoArrange = !this.layoutDoc._autoArrange), - icon: 'compress-arrows-alt', - }); if (this._props.setContentViewBox === emptyFunction) { !appearance && ContextMenu.Instance.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'eye' }); return; @@ -1871,7 +1855,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection ); } get pannableContents() { - this.incrementalRender(); + this.incrementalRender(); // needs to happen synchronously or freshly typed text documents will flash and miss their first characters return ( <CollectionFreeFormPannableContents Document={this.Document} @@ -1949,6 +1933,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection return ( <div className="collectionfreeformview-container" + id={this._paintedId} ref={r => { this.createDashEventsTarget(r); this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel); @@ -1970,7 +1955,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection width: `${100 / (this.nativeDimScaling || 1)}%`, height: this._props.getScrollHeight?.() ?? `${100 / (this.nativeDimScaling || 1)}%`, }}> - {this._lightboxDoc ? ( + {this.paintFunc ? null : this._lightboxDoc ? ( <div style={{ padding: 15, width: '100%', height: '100%' }}> <DocumentView {...this._props} @@ -2065,6 +2050,7 @@ ScriptingGlobals.add(function sendToBack(doc: Doc) { SelectionManager.Views.forEach(view => view.CollectionFreeFormView?.bringToFront(view.Document, true)); }); ScriptingGlobals.add(function datavizFromSchema(doc: Doc) { + // creating a dataviz doc to represent the schema table SelectionManager.Views.forEach(view => { if (!view.layoutDoc.schema_columnKeys) { view.layoutDoc.schema_columnKeys = new List<string>(['title', 'type', 'author', 'author_date']); @@ -2085,15 +2071,15 @@ ScriptingGlobals.add(function datavizFromSchema(doc: Doc) { csvRows.push(eachRow); } const blob = new Blob([csvRows.join('\n')], { type: 'text/csv' }); - const options = { x: 0, y: -300, title: 'schemaTable', _width: 300, _height: 100, type: 'text/csv' }; + const options = { x: 0, y: 0, title: 'schemaTable', _width: 300, _height: 100, type: 'text/csv' }; const file = new File([blob], 'schemaTable', options); const loading = Docs.Create.LoadingDocument(file, options); loading.presentation_openInLightbox = true; DocUtils.uploadFileToDoc(file, {}, loading); + // holds the doc in a popup until it is dragged onto a canvas if (view.ComponentView?.addDocument) { - // loading.dataViz_fromSchema = true; - loading.dataViz_asSchema = view.layoutDoc; + loading._dataViz_asSchema = view.layoutDoc; SchemaCSVPopUp.Instance.setView(view); SchemaCSVPopUp.Instance.setTarget(view.layoutDoc); SchemaCSVPopUp.Instance.setDataVizDoc(loading); diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index c2f8232c6..a417d777a 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -219,7 +219,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps const scrollMode = e.altKey ? (Doc.UserDoc().freeformScrollMode === freeformScrollMode.Pan ? freeformScrollMode.Zoom : freeformScrollMode.Pan) : Doc.UserDoc().freeformScrollMode; // allow marquee if right drag/meta drag, or pan mode - if (e.button === 2 || e.metaKey || scrollMode === freeformScrollMode.Pan) { + if (e.button === 2 || e.metaKey || (this._props.isContentActive() && scrollMode === freeformScrollMode.Pan && Doc.ActiveTool === InkTool.None)) { this.setPreviewCursor(e.clientX, e.clientY, true, false, this._props.Document); e.preventDefault(); } else PreviewCursor.Instance.Visible = false; diff --git a/src/client/views/collections/collectionFreeForm/index.ts b/src/client/views/collections/collectionFreeForm/index.ts index 702dc8d42..9a54ce63a 100644 --- a/src/client/views/collections/collectionFreeForm/index.ts +++ b/src/client/views/collections/collectionFreeForm/index.ts @@ -1,7 +1,5 @@ -export * from "./CollectionFreeFormLayoutEngines"; -export * from "./CollectionFreeFormLinkView"; -export * from "./CollectionFreeFormLinksView"; -export * from "./CollectionFreeFormRemoteCursors"; -export * from "./CollectionFreeFormView"; -export * from "./MarqueeOptionsMenu"; -export * from "./MarqueeView";
\ No newline at end of file +export * from './CollectionFreeFormLayoutEngines'; +export * from './CollectionFreeFormRemoteCursors'; +export * from './CollectionFreeFormView'; +export * from './MarqueeOptionsMenu'; +export * from './MarqueeView'; diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.scss b/src/client/views/collections/collectionSchema/CollectionSchemaView.scss index 29d121974..fed4e89cf 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaView.scss +++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.scss @@ -18,17 +18,21 @@ .schema-add { position: relative; - height: 30; + height: 35; display: flex; align-items: center; + top: -10px; width: 100%; text-align: right; background: lightgray; .editableView-container-editing { width: 100%; + height: 35px; + margin: 20px; } .editableView-input { width: 100%; + margin: 20px; float: right; text-align: right; background: yellow; @@ -128,7 +132,7 @@ .row-menu { display: flex; - justify-content: flex-end; + justify-content: center; } } @@ -224,7 +228,10 @@ display: flex; flex-direction: row; min-width: 50px; - justify-content: flex-end; + justify-content: center; + .iconButton-container { + min-width: unset !important; + } } .row-cells { diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx index 581425d77..31b4a2dd4 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx +++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx @@ -25,6 +25,7 @@ import { CollectionSubView } from '../CollectionSubView'; import './CollectionSchemaView.scss'; import { SchemaColumnHeader } from './SchemaColumnHeader'; import { SchemaRowBox } from './SchemaRowBox'; +const { default: { SCHEMA_NEW_NODE_HEIGHT } } = require('../../global/globalCssVariables.module.scss'); // prettier-ignore export enum ColumnType { Number, @@ -69,7 +70,7 @@ export class CollectionSchemaView extends CollectionSubView() { public static _minColWidth: number = 25; public static _rowMenuWidth: number = 60; public static _previewDividerWidth: number = 4; - public static _newNodeInputHeight: number = 20; + public static _newNodeInputHeight: number = Number(SCHEMA_NEW_NODE_HEIGHT); public fieldInfos = new ObservableMap<string, FInfo>(); @observable _menuKeys: string[] = []; diff --git a/src/client/views/collections/collectionSchema/SchemaRowBox.tsx b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx index f2fe0dde7..39fea2d2e 100644 --- a/src/client/views/collections/collectionSchema/SchemaRowBox.tsx +++ b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx @@ -121,7 +121,7 @@ export class SchemaRowBox extends ViewBoxBaseComponent<SchemaRowBoxProps>() { pointerEvents: !this._props.isContentActive() ? 'none' : undefined, }}> <IconButton - tooltip="whether document interations are enabled" + tooltip="whether document interactions are enabled" icon={this.Document._lockedPosition ? <CgLockUnlock size="12px" /> : <CgLock size="12px" />} size={Size.XSMALL} onPointerDown={e => diff --git a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx index dbaa6e110..001ad5ab6 100644 --- a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx +++ b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx @@ -24,6 +24,11 @@ import { KeyValueBox } from '../../nodes/KeyValueBox'; import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox'; import { ColumnType, FInfotoColType } from './CollectionSchemaView'; import './CollectionSchemaView.scss'; +import 'react-datepicker/dist/react-datepicker.css'; +import { Popup, Size, Type } from 'browndash-components'; +import { IconLookup, faCaretDown } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { SettingsManager } from '../../../util/SettingsManager'; export interface SchemaTableCellProps { Document: Doc; @@ -162,7 +167,7 @@ export class SchemaTableCell extends ObservableReactComponent<SchemaTableCellPro case ColumnType.Boolean: return <SchemaBoolCell {...this._props} />; case ColumnType.RTF: return <SchemaRTFCell {...this._props} />; case ColumnType.Enumeration: return <SchemaEnumerationCell {...this._props} options={this._props.getFinfo(this._props.fieldKey)?.values?.map(val => val.toString())} />; - case ColumnType.Date: // return <SchemaDateCell {...this._props} />; + case ColumnType.Date: return <SchemaDateCell {...this._props} />; default: return this.defaultCellContent; } } @@ -260,19 +265,39 @@ export class SchemaDateCell extends ObservableReactComponent<SchemaTableCellProp return DateCast(this._props.Document[this._props.fieldKey]); } - @action - handleChange = (date: any) => { + handleChange = undoable((date: Date | null) => { // const script = CompileScript(date.toString(), { requiredType: "Date", addReturn: true, params: { this: Doc.name } }); // if (script.compiled) { // this.applyToDoc(this._document, this._props.row, this._props.col, script.run); // } else { // ^ DateCast is always undefined for some reason, but that is what the field should be set to - this._props.Document[this._props.fieldKey] = new DateField(date as Date); + date && (this._props.Document[this._props.fieldKey] = new DateField(date)); //} - }; + }, 'date change'); render() { - return <DatePicker dateFormat={'Pp'} selected={this.date.date} onChange={(date: any) => this.handleChange(date)} />; + const { color, textDecoration, fieldProps, cursor, pointerEvents } = SchemaTableCell.renderProps(this._props); + return ( + <> + <div style={{ pointerEvents: 'none' }}> + <DatePicker dateFormat="Pp" selected={this.date?.date ?? Date.now()} onChange={e => {}} /> + </div> + {pointerEvents === 'none' ? null : ( + <Popup + icon={<FontAwesomeIcon size="sm" icon="caret-down" />} + size={Size.XSMALL} + type={Type.TERT} + color={SettingsManager.userColor} + background={SettingsManager.userBackgroundColor} + popup={ + <div style={{ width: 'fit-content', height: '200px' }}> + <DatePicker open={true} dateFormat="Pp" selected={this.date?.date ?? Date.now()} onChange={this.handleChange} /> + </div> + } + /> + )} + </> + ); } } @observer @@ -394,7 +419,7 @@ export class SchemaEnumerationCell extends ObservableReactComponent<SchemaTableC }), }} menuPortalTarget={this._props.menuTarget} - menuPosition={'absolute'} + menuPosition="absolute" placeholder={StrCast(this._props.Document[this._props.fieldKey], 'select...')} options={options} isMulti={false} diff --git a/src/client/views/global/globalCssVariables.module.scss b/src/client/views/global/globalCssVariables.module.scss index 44e8efe23..ad0c5c21d 100644 --- a/src/client/views/global/globalCssVariables.module.scss +++ b/src/client/views/global/globalCssVariables.module.scss @@ -64,6 +64,7 @@ $mainTextInput-zindex: 999; // then text input overlay so that it's context menu $docDecorations-zindex: 998; // then doc decorations appear over everything else $remoteCursors-zindex: 997; // ... not sure what level the remote cursors should go -- is this right? $SCHEMA_DIVIDER_WIDTH: 4; +$SCHEMA_NEW_NODE_HEIGHT: 30; $MINIMIZED_ICON_SIZE: 24; $MAX_ROW_HEIGHT: 44px; $DFLT_IMAGE_NATIVE_DIM: 900px; @@ -78,6 +79,7 @@ $DATA_VIZ_TABLE_ROW_HEIGHT: 30; :export { contextMenuZindex: $contextMenu-zindex; + SCHEMA_NEW_NODE_HEIGHT: $SCHEMA_NEW_NODE_HEIGHT; SCHEMA_DIVIDER_WIDTH: $SCHEMA_DIVIDER_WIDTH; MINIMIZED_ICON_SIZE: $MINIMIZED_ICON_SIZE; MAX_ROW_HEIGHT: $MAX_ROW_HEIGHT; diff --git a/src/client/views/global/globalCssVariables.module.scss.d.ts b/src/client/views/global/globalCssVariables.module.scss.d.ts index bcbb1f068..12cc3fc89 100644 --- a/src/client/views/global/globalCssVariables.module.scss.d.ts +++ b/src/client/views/global/globalCssVariables.module.scss.d.ts @@ -1,5 +1,6 @@ interface IGlobalScss { contextMenuZindex: string; // context menu shows up over everything + SCHEMA_NEW_NODE_HEIGHT: string; SCHEMA_DIVIDER_WIDTH: string; MINIMIZED_ICON_SIZE: string; MAX_ROW_HEIGHT: string; diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts index c2d6cea04..4531c8296 100644 --- a/src/client/views/global/globalScripts.ts +++ b/src/client/views/global/globalScripts.ts @@ -20,6 +20,7 @@ import { DocumentView } from '../nodes/DocumentView'; import { RichTextMenu } from '../nodes/formattedText/RichTextMenu'; import { WebBox } from '../nodes/WebBox'; import { VideoBox } from '../nodes/VideoBox'; +import { DocData } from '../../../fields/DocSymbols'; ScriptingGlobals.add(function IsNoneSelected() { return SelectionManager.Views.length <= 0; @@ -46,14 +47,14 @@ ScriptingGlobals.add(function setBackgroundColor(color?: string, checkResult?: b } else if (selectedViews.length) { if (checkResult) { const selView = selectedViews.lastElement(); - const fieldKey = selView.Document.type === DocumentType.INK ? 'fillColor' : 'backgroundColor'; + const fieldKey = selView.Document._layout_isSvg ? 'fillColor' : 'backgroundColor'; const layoutFrameNumber = Cast(selView.containerViewPath?.().lastElement()?.Document?._currentFrame, 'number'); // frame number that container is at which determines layout frame values const contentFrameNumber = Cast(selView.Document?._currentFrame, 'number', layoutFrameNumber ?? null); // frame number that content is at which determines what content is displayed return CollectionFreeFormDocumentView.getStringValues(selView?.Document, contentFrameNumber)[fieldKey] ?? 'transparent'; } selectedViews.some(dv => dv.ComponentView instanceof InkingStroke) && SetActiveFillColor(color ?? 'transparent'); selectedViews.forEach(dv => { - const fieldKey = dv.Document.type === DocumentType.INK ? 'fillColor' : 'backgroundColor'; + const fieldKey = dv.Document._layout_isSvg ? 'fillColor' : 'backgroundColor'; const layoutFrameNumber = Cast(dv.containerViewPath?.().lastElement()?.Document?._currentFrame, 'number'); // frame number that container is at which determines layout frame values const contentFrameNumber = Cast(dv.Document?._currentFrame, 'number', layoutFrameNumber ?? null); // frame number that content is at which determines what content is displayed if (contentFrameNumber !== undefined) { @@ -61,16 +62,16 @@ ScriptingGlobals.add(function setBackgroundColor(color?: string, checkResult?: b obj[fieldKey] = color; CollectionFreeFormDocumentView.setStringValues(contentFrameNumber, dv.Document, obj); } else { - dv.Document['_' + fieldKey] = color; + dv.Document[DocData][fieldKey] = color; } }); } else { - const selected = SelectionManager.Docs.length ? SelectionManager.Docs : LinkManager.currentLink ? [LinkManager.currentLink] : []; + const selected = SelectionManager.Docs.length ? SelectionManager.Docs : LinkManager.Instance.currentLink ? [LinkManager.Instance.currentLink] : []; if (checkResult) { return selected.lastElement()?._backgroundColor ?? 'transparent'; } SetActiveFillColor(color ?? 'transparent'); - selected.forEach(doc => (doc._backgroundColor = color)); + selected.forEach(doc => (doc[DocData].backgroundColor = color)); } }); @@ -130,11 +131,6 @@ ScriptingGlobals.add(function showFreeform(attr: 'flashcards' | 'center' | 'grid checkResult: (doc:Doc) => BoolCast(doc?._freeform_useClusters, false), setDoc: (doc:Doc,dv:DocumentView) => doc._freeform_useClusters = !doc._freeform_useClusters, }], - ['arrange', { - waitForRender: true, // flags that undo batch should terminate after a re-render giving the script the chance to fire - checkResult: (doc:Doc) => BoolCast(doc?._autoArrange, false), - setDoc: (doc:Doc,dv:DocumentView) => doc._autoArrange = !doc._autoArrange, - }], ['flashcards', { checkResult: (doc:Doc) => BoolCast(Doc.UserDoc().defaultToFlashcards, false), setDoc: (doc:Doc,dv:DocumentView) => Doc.UserDoc().defaultToFlashcards = !Doc.UserDoc().defaultToFlashcards, @@ -347,24 +343,24 @@ ScriptingGlobals.add(function setInkProperty(option: 'inkMask' | 'fillColor' | ' // prettier-ignore const map: Map<'inkMask' | 'fillColor' | 'strokeWidth' | 'strokeColor', { checkResult: () => any; setInk: (doc: Doc) => void; setMode: () => void }> = new Map([ ['inkMask', { - checkResult: () => ((selected?.type === DocumentType.INK ? BoolCast(selected.stroke_isInkMask) : ActiveIsInkMask())), - setInk: (doc: Doc) => (doc.stroke_isInkMask = !doc.stroke_isInkMask), + checkResult: () => ((selected?._layout_isSvg ? BoolCast(selected[DocData].stroke_isInkMask) : ActiveIsInkMask())), + setInk: (doc: Doc) => (doc[DocData].stroke_isInkMask = !doc.stroke_isInkMask), setMode: () => selected?.type !== DocumentType.INK && SetActiveIsInkMask(!ActiveIsInkMask()), }], ['fillColor', { - checkResult: () => (selected?.type === DocumentType.INK ? StrCast(selected.fillColor) : ActiveFillColor() ?? "transparent"), - setInk: (doc: Doc) => (doc.fillColor = StrCast(value)), + checkResult: () => (selected?._layout_isSvg ? StrCast(selected[DocData].fillColor) : ActiveFillColor() ?? "transparent"), + setInk: (doc: Doc) => (doc[DocData].fillColor = StrCast(value)), setMode: () => SetActiveFillColor(StrCast(value)), }], [ 'strokeWidth', { - checkResult: () => (selected?.type === DocumentType.INK ? NumCast(selected.stroke_width) : ActiveInkWidth()), - setInk: (doc: Doc) => (doc.stroke_width = NumCast(value)), - setMode: () => { SetActiveInkWidth(value.toString()); setActiveTool( GestureOverlay.Instance.InkShape ?? InkTool.Pen, true, false);}, + checkResult: () => (selected?._layout_isSvg ? NumCast(selected[DocData].stroke_width) : ActiveInkWidth()), + setInk: (doc: Doc) => (doc[DocData].stroke_width = NumCast(value)), + setMode: () => { SetActiveInkWidth(value.toString()); selected?.type === DocumentType.INK && setActiveTool( GestureOverlay.Instance.InkShape ?? InkTool.Pen, true, false);}, }], ['strokeColor', { - checkResult: () => (selected?.type === DocumentType.INK ? StrCast(selected.color) : ActiveInkColor()), - setInk: (doc: Doc) => (doc.color = String(value)), - setMode: () => { SetActiveInkColor(StrCast(value)); setActiveTool(GestureOverlay.Instance.InkShape ?? InkTool.Pen, true, false);}, + checkResult: () => (selected?._layout_isSvg? StrCast(selected[DocData].color) : ActiveInkColor()), + setInk: (doc: Doc) => (doc[DocData].color = String(value)), + setMode: () => { SetActiveInkColor(StrCast(value)); selected?.type === DocumentType.INK && setActiveTool(GestureOverlay.Instance.InkShape ?? InkTool.Pen, true, false);}, }], ]); @@ -372,7 +368,7 @@ ScriptingGlobals.add(function setInkProperty(option: 'inkMask' | 'fillColor' | ' return map.get(option)?.checkResult(); } map.get(option)?.setMode(); - SelectionManager.Docs.filter(doc => doc.type === DocumentType.INK).map(doc => map.get(option)?.setInk(doc)); + SelectionManager.Docs.filter(doc => doc._layout_isSvg).map(doc => map.get(option)?.setInk(doc)); }); /** WEB diff --git a/src/client/views/linking/LinkMenuItem.scss b/src/client/views/linking/LinkMenuItem.scss index 44c74236f..66ddd6eca 100644 --- a/src/client/views/linking/LinkMenuItem.scss +++ b/src/client/views/linking/LinkMenuItem.scss @@ -18,6 +18,21 @@ align-items: center; display: flex; + .linkMenu-icon-wrapper { + //border: 0.5px solid rgb(161, 161, 161); + margin-right: 2px; + padding-right: 2px; + + .linkMenu-icon { + float: left; + width: 12px; + height: 12px; + padding: 1px; + margin-right: 3px; + // color: rgb(161, 161, 161); + } + } + .linkMenu-text { width: 100%; @@ -34,22 +49,6 @@ display: flex; align-items: center; min-height: 20px; - - .destination-icon-wrapper { - //border: 0.5px solid rgb(161, 161, 161); - margin-right: 2px; - padding-right: 2px; - - .destination-icon { - float: left; - width: 12px; - height: 12px; - padding: 1px; - margin-right: 3px; - // color: rgb(161, 161, 161); - } - } - .linkMenu-destination-title { text-decoration: none; font-size: 13px; @@ -111,7 +110,6 @@ //@extend: right; position: relative; display: flex; - opacity: 0; .linkMenu-deleteButton { width: 20px; diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx index dc4aee1ca..e694806a5 100644 --- a/src/client/views/linking/LinkMenuItem.tsx +++ b/src/client/views/linking/LinkMenuItem.tsx @@ -9,6 +9,7 @@ import { Doc } from '../../../fields/Doc'; import { Cast, DocCast, StrCast } from '../../../fields/Types'; import { WebField } from '../../../fields/URLField'; import { DocumentType } from '../../documents/DocumentTypes'; +import { DocumentManager } from '../../util/DocumentManager'; import { DragManager } from '../../util/DragManager'; import { LinkFollower } from '../../util/LinkFollower'; import { LinkManager } from '../../util/LinkManager'; @@ -74,6 +75,15 @@ export class LinkMenuItem extends ObservableReactComponent<LinkMenuItemProps> { return this._props.sourceDoc; } + onIconDown = (e: React.PointerEvent) => { + setupMoveUpEvents(this, e, returnFalse, returnFalse, () => { + const ancestor = DocumentManager.LinkCommonAncestor(this._props.linkDoc); + if (!ancestor?.ComponentView?.removeDocument?.(this._props.linkDoc)) { + ancestor?.ComponentView?.addDocument?.(this._props.linkDoc); + } + }); + }; + onEdit = (e: React.PointerEvent) => { setupMoveUpEvents( this, @@ -92,8 +102,8 @@ export class LinkMenuItem extends ObservableReactComponent<LinkMenuItemProps> { DocumentViewInternal.addDocTabFunc(trail, OpenWhere.replaceRight); } else { SelectionManager.SelectView(this._props.docView, false); - LinkManager.currentLink = this._props.linkDoc === LinkManager.currentLink ? undefined : this._props.linkDoc; - LinkManager.currentLinkAnchor = LinkManager.currentLink ? this.sourceAnchor : undefined; + LinkManager.Instance.currentLink = this._props.linkDoc === LinkManager.Instance.currentLink ? undefined : this._props.linkDoc; + LinkManager.Instance.currentLinkAnchor = LinkManager.Instance.currentLink ? this.sourceAnchor : undefined; if ((SettingsManager.Instance.propertiesWidth ?? 0) < 100) { setTimeout(action(() => (SettingsManager.Instance.propertiesWidth = 250))); @@ -136,7 +146,7 @@ export class LinkMenuItem extends ObservableReactComponent<LinkMenuItemProps> { deleteLink = (e: React.PointerEvent): void => setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => LinkManager.Instance.deleteLink(this._props.linkDoc)))); @observable _hover = false; - docView = () => this.props.docView; + docView = () => this._props.docView; render() { const destinationIcon = Doc.toIcon(this._props.destinationDoc) as any as IconProp; @@ -159,7 +169,7 @@ export class LinkMenuItem extends ObservableReactComponent<LinkMenuItemProps> { style={{ fontSize: this._hover ? 'larger' : undefined, fontWeight: this._hover ? 'bold' : undefined, - background: LinkManager.currentLink === this._props.linkDoc ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor, + background: LinkManager.Instance.currentLink === this._props.linkDoc ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor, }}> <div className="linkMenu-item-content expand-two"> <div @@ -168,8 +178,13 @@ export class LinkMenuItem extends ObservableReactComponent<LinkMenuItemProps> { onPointerDown={this.onLinkButtonDown}> <div className="linkMenu-item-buttons"> <Tooltip title={<div className="dash-tooltip">Edit Link</div>}> - <div className="button" ref={this._editRef} onPointerDown={this.onEdit} onClick={e => e.stopPropagation()}> - <FontAwesomeIcon className="fa-icon" icon="edit" size="sm" /> + <div className="linkMenu-icon-wrapper" ref={this._editRef} onPointerDown={this.onEdit} onClick={e => e.stopPropagation()}> + <FontAwesomeIcon className="linkMenu-icon" icon="edit" size="sm" /> + </div> + </Tooltip> + <Tooltip title={<div className="dash-tooltip">Show/Hide Link</div>}> + <div title="click to show link" className="linkMenu-icon-wrapper" onPointerDown={this.onIconDown}> + <FontAwesomeIcon className="linkMenu-icon" icon={destinationIcon} size="sm" /> </div> </Tooltip> </div> @@ -196,12 +211,11 @@ export class LinkMenuItem extends ObservableReactComponent<LinkMenuItemProps> { </p> ) : null} <div className="linkMenu-title-wrapper"> - <div className="destination-icon-wrapper"> - <FontAwesomeIcon className="destination-icon" icon={destinationIcon} size="sm" /> - </div> - <p className="linkMenu-destination-title"> - {this._props.linkDoc.linksToAnnotation && Cast(this._props.destinationDoc.data, WebField)?.url.href === this._props.linkDoc.annotationUri ? 'Annotation in' : ''} {StrCast(title)} - </p> + <Tooltip title={<div className="dash-tooltip">Follow Link</div>}> + <p className="linkMenu-destination-title"> + {this._props.linkDoc.linksToAnnotation && Cast(this._props.destinationDoc.data, WebField)?.url.href === this._props.linkDoc.annotationUri ? 'Annotation in' : ''} {StrCast(title)} + </p> + </Tooltip> </div> {!this._props.linkDoc.link_description ? null : <p className="linkMenu-description">{StrCast(this._props.linkDoc.link_description).split('\n')[0].substring(0, 50)}</p>} </div> diff --git a/src/client/views/newlightbox/utils.ts b/src/client/views/newlightbox/utils.ts index 6016abca4..c879718e3 100644 --- a/src/client/views/newlightbox/utils.ts +++ b/src/client/views/newlightbox/utils.ts @@ -1,121 +1,120 @@ -import { DocumentType } from "../../documents/DocumentTypes"; -import { IRecommendation } from "./components"; +import { DocumentType } from '../../documents/DocumentTypes'; +import { IRecommendation } from './components'; export interface IDocRequest { - id: string, - title: string, - text: string, - type: string + id: string; + title: string; + text: string; + type: string; } export const fetchRecommendations = async (src: string, query: string, docs?: IDocRequest[], dummy?: boolean) => { - console.log("[rec] making request") + console.log('[rec] making request'); if (dummy) { return { - "recommendations": dummyRecs, - "keywords": dummyKeywords, - "num_recommendations": 4, - "max_x": 100, - "max_y": 100, - "min_x": 0, - "min_y": 0 - + recommendations: [], //dummyRecs, + keywords: dummyKeywords, + num_recommendations: 4, + max_x: 100, + max_y: 100, + min_x: 0, + min_y: 0, }; } const response = await fetch('http://127.0.0.1:8000/recommend', { method: 'POST', headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json' + Accept: 'application/json', + 'Content-Type': 'application/json', }, - body: JSON.stringify({ - "src": src, - "query": query, - "docs": docs - }) - }) + body: JSON.stringify({ + src: src, + query: query, + docs: docs, + }), + }); const data = await response.json(); - + return data; -} +}; export const fetchKeywords = async (text: string, n: number, dummy?: boolean) => { - console.log("[fetchKeywords]") + console.log('[fetchKeywords]'); if (dummy) { return { - "keywords": dummyKeywords + keywords: dummyKeywords, }; } const response = await fetch('http://127.0.0.1:8000/keywords', { method: 'POST', headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json' + Accept: 'application/json', + 'Content-Type': 'application/json', }, - body: JSON.stringify({ - "text": text, - "n": n - }) - }) - const data = await response.json() + body: JSON.stringify({ + text: text, + n: n, + }), + }); + const data = await response.json(); return data; -} +}; export const getType = (type: DocumentType | string) => { - switch(type) { + switch (type) { case DocumentType.AUDIO: - return "Audio" + return 'Audio'; case DocumentType.VID: - return "Video" + return 'Video'; case DocumentType.PDF: - return "PDF" + return 'PDF'; case DocumentType.WEB: - return "Webpage" - case "YouTube": - return "Video" - case "HTML": - return "Webpage" + return 'Webpage'; + case 'YouTube': + return 'Video'; + case 'HTML': + return 'Webpage'; default: - return "Unknown: " + type + return 'Unknown: ' + type; } -} +}; const dummyRecs = { - "a": { + a: { title: 'Vannevar Bush - American Engineer', - previewUrl: 'https://cdn.britannica.com/98/23598-004-1E6A382E/Vannevar-Bush-Differential-Analyzer-1935.jpg', + previewUrl: 'https://cdn.britannica.com/98/23598-004-1E6A382E/Vannevar-Bush-Differential-Analyzer-1935.jpg', type: 'web', distance: 2.3, source: 'www.britannica.com', related_concepts: ['vannevar bush', 'knowledge'], embedding: { x: 0, - y: 0 - } + y: 0, + }, }, - "b": { + b: { title: "From Memex to hypertext: Vannevar Bush and the mind's machine", type: 'pdf', distance: 5.4, source: 'Google Scholar', related_concepts: ['memex', 'vannevar bush', 'hypertext'], }, - "c": { + c: { title: 'How the hyperlink changed everything | Small Thing Big Idea, a TED series', - previewUrl: 'https://pi.tedcdn.com/r/talkstar-photos.s3.amazonaws.com/uploads/b17d043f-2642-4117-a913-52204505513f/MargaretGouldStewart_2018V-embed.jpg?u%5Br%5D=2&u%5Bs%5D=0.5&u%5Ba%5D=0.8&u%5Bt%5D=0.03&quality=82w=640', + previewUrl: 'https://pi.tedcdn.com/r/talkstar-photos.s3.amazonaws.com/uploads/b17d043f-2642-4117-a913-52204505513f/MargaretGouldStewart_2018V-embed.jpg?u%5Br%5D=2&u%5Bs%5D=0.5&u%5Ba%5D=0.8&u%5Bt%5D=0.03&quality=82w=640', type: 'youtube', distance: 5.3, source: 'www.youtube.com', - related_concepts: ['User Control', 'Explanations'] + related_concepts: ['User Control', 'Explanations'], }, - "d": { + d: { title: 'Recommender Systems: Behind the Scenes of Machine Learning-Based Personalization', - previewUrl: 'https://sloanreview.mit.edu/wp-content/uploads/2018/10/MAG-Ransbotham-Ratings-Recommendations-1200X627-1200x627.jpg', + previewUrl: 'https://sloanreview.mit.edu/wp-content/uploads/2018/10/MAG-Ransbotham-Ratings-Recommendations-1200X627-1200x627.jpg', type: 'pdf', distance: 9.3, source: 'www.altexsoft.com', - related_concepts: ['User Control', 'Explanations'] - } -} + related_concepts: ['User Control', 'Explanations'], + }, +}; -const dummyKeywords = ['user control', 'vannevar bush', 'hypermedia', 'hypertext']
\ No newline at end of file +const dummyKeywords = ['user control', 'vannevar bush', 'hypermedia', 'hypertext']; diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 0ae4ed62c..a0a64ab59 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -33,7 +33,6 @@ export interface CollectionFreeFormDocumentViewWrapperProps extends DocumentView opacity?: number; highlight?: boolean; transition?: string; - dataTransition?: string; RenderCutoffProvider: (doc: Doc) => boolean; CollectionFreeFormView: CollectionFreeFormView; } @@ -56,7 +55,6 @@ export class CollectionFreeFormDocumentViewWrapper extends ObservableReactCompon @observable Height = this.props.height; @observable AutoDim = this.props.autoDim; @observable Transition = this.props.transition; - @observable DataTransition = this.props.dataTransition; CollectionFreeFormView = this.props.CollectionFreeFormView; // needed for type checking RenderCutoffProvider = this.props.RenderCutoffProvider; // needed for type checking @@ -83,7 +81,6 @@ export class CollectionFreeFormDocumentViewWrapper extends ObservableReactCompon w_Height = () => this.Height; // prettier-ignore w_AutoDim = () => this.AutoDim; // prettier-ignore w_Transition = () => this.Transition; // prettier-ignore - w_DataTransition = () => this.DataTransition; // prettier-ignore PanelWidth = () => this._props.autoDim ? this._props.PanelWidth?.() : this.Width; // prettier-ignore PanelHeight = () => this._props.autoDim ? this._props.PanelHeight?.() : this.Height; // prettier-ignore @@ -117,7 +114,6 @@ export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { w_Transition: () => string | undefined; w_Width: () => number; w_Height: () => number; - w_DataTransition: () => string | undefined; PanelWidth: () => number; PanelHeight: () => number; RenderCutoffProvider: (doc: Doc) => boolean; @@ -288,14 +284,21 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF width: this._props.PanelWidth(), height: this._props.PanelHeight(), transform: `translate(${this._props.w_X()}px, ${this._props.w_Y()}px) rotate(${NumCast(this._props.w_Rotation?.())}deg)`, - transition: this._props.w_Transition?.() ?? (this._props.w_DataTransition?.() || this._props.w_Transition?.()), + transition: this._props.w_Transition?.() || StrCast(this.Document.dataTransition), zIndex: this._props.w_ZIndex?.(), display: this._props.w_Width?.() ? undefined : 'none', }}> {this._props.RenderCutoffProvider(this.Document) ? ( <div style={{ position: 'absolute', width: this._props.PanelWidth(), height: this._props.PanelHeight(), background: 'lightGreen' }} /> ) : ( - <DocumentView {...passOnProps} CollectionFreeFormDocumentView={this.returnThis} styleProvider={this.styleProvider} ScreenToLocalTransform={this.screenToLocalTransform} isGroupActive={this.isGroupActive} /> + <DocumentView + {...passOnProps} + DataTransition={this._props.w_Transition} + CollectionFreeFormDocumentView={this.returnThis} + styleProvider={this.styleProvider} + ScreenToLocalTransform={this.screenToLocalTransform} + isGroupActive={this.isGroupActive} + /> )} </div> ); diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.scss b/src/client/views/nodes/DataVizBox/DataVizBox.scss index a3132dc6e..6b5738790 100644 --- a/src/client/views/nodes/DataVizBox/DataVizBox.scss +++ b/src/client/views/nodes/DataVizBox/DataVizBox.scss @@ -29,6 +29,10 @@ } } + .liveSchema-checkBox { + margin-bottom: -35px; + } + .dataviz-sidebar { position: absolute; right: 0; diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx index 8365f4770..66a08f13e 100644 --- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx +++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx @@ -1,6 +1,6 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Colors, Toggle, ToggleType, Type } from 'browndash-components'; -import { ObservableMap, action, computed, makeObservable, observable, runInAction } from 'mobx'; +import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { emptyFunction, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents } from '../../../../Utils'; @@ -11,7 +11,7 @@ import { listSpec } from '../../../../fields/Schema'; import { Cast, CsvCast, DocCast, NumCast, StrCast } from '../../../../fields/Types'; import { CsvField } from '../../../../fields/URLField'; import { TraceMobx } from '../../../../fields/util'; -import { Docs } from '../../../documents/Documents'; +import { DocUtils, Docs } from '../../../documents/Documents'; import { DocumentManager } from '../../../util/DocumentManager'; import { UndoManager, undoable } from '../../../util/UndoManager'; import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../../DocComponent'; @@ -27,6 +27,7 @@ import { Histogram } from './components/Histogram'; import { LineChart } from './components/LineChart'; import { PieChart } from './components/PieChart'; import { TableBox } from './components/TableBox'; +import { Checkbox } from '@mui/material'; export enum DataVizView { TABLE = 'table', @@ -40,9 +41,9 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); private _marqueeref = React.createRef<MarqueeAnnotator>(); private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); + private _disposers: { [name: string]: IReactionDisposer } = {}; anchorMenuClick?: () => undefined | ((anchor: Doc) => void); crop: ((region: Doc | undefined, addCrop?: boolean) => Doc | undefined) | undefined; - @observable _schemaDataVizChildren: any = undefined; @observable _marqueeing: number[] | undefined = undefined; @observable _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>(); @@ -85,6 +86,10 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im // all datasets that have been retrieved from the server stored as a map from the dataset url to an array of records static dataset = new ObservableMap<string, { [key: string]: string }[]>(); + // when a dataset comes from schema view, this stores the original dataset to refer back to + // href : dataset + static datasetSchemaOG = new ObservableMap<string, { [key: string]: string }[]>(); + private _vizRenderer: LineChart | Histogram | PieChart | undefined; private _sidebarRef = React.createRef<SidebarAnnos>(); @@ -103,10 +108,13 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im return Cast(this.dataDoc[this.fieldKey], CsvField); } @computed.struct get axes() { - return StrListCast(this.layoutDoc.dataViz_axes); + return StrListCast(this.layoutDoc._dataViz_axes); } - - selectAxes = (axes: string[]) => (this.layoutDoc.dataViz_axes = new List<string>(axes)); + selectAxes = (axes: string[]) => (this.layoutDoc._dataViz_axes = new List<string>(axes)); + @computed.struct get titleCol() { + return StrCast(this.layoutDoc._dataViz_titleCol); + } + selectTitleCol = (titleCol: string) => (this.layoutDoc._dataViz_titleCol = titleCol); @action // pinned / linked anchor doc includes selected rows, graph titles, and graph colors restoreView = (data: Doc) => { @@ -256,12 +264,73 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im componentDidMount() { this._props.setContentViewBox?.(this); if (!DataVizBox.dataset.has(CsvCast(this.dataDoc[this.fieldKey]).url.href)) this.fetchData(); + this._disposers.datavis = reaction( + () => { + if (this.layoutDoc.dataViz_schemaLive==undefined) this.layoutDoc.dataViz_schemaLive = true; + const getFrom = DocCast(this.layoutDoc.dataViz_asSchema); + const keys = Cast(getFrom?.schema_columnKeys, listSpec('string'))?.filter(key => key != 'text'); + if (!keys) return; + const children = DocListCast(getFrom[Doc.LayoutFieldKey(getFrom)]); + var current: { [key: string]: string }[] = []; + children + .filter(child => child) + .forEach(child => { + const row: { [key: string]: string } = {}; + keys.forEach(key => { + var cell = child[key]; + if (cell && (cell as string)) cell = cell.toString().replace(/\,/g, ''); + row[key] = StrCast(cell); + }); + current.push(row); + }); + if (!this.layoutDoc._dataViz_schemaOG){ // makes a copy of the original table for the "live" toggle + let csvRows = []; + csvRows.push(keys.join(',')); + for (let i = 0; i < children.length-1; i++) { + let eachRow = []; + for (let j = 0; j < keys.length; j++) { + var cell = children[i][keys[j]]; + if (cell && (cell as string)) cell = cell.toString().replace(/\,/g, ''); + eachRow.push(cell); + } + csvRows.push(eachRow); + } + const blob = new Blob([csvRows.join('\n')], { type: 'text/csv' }); + const options = { x: 0, y: 0, title: 'schemaTable for static dataviz', _width: 300, _height: 100, type: 'text/csv' }; + const file = new File([blob], 'schemaTable for static dataviz', options); + const loading = Docs.Create.LoadingDocument(file, options); + DocUtils.uploadFileToDoc(file, {}, loading); + this.layoutDoc._dataViz_schemaOG = loading; + } + const ogDoc = this.layoutDoc._dataViz_schemaOG as Doc + const ogHref = CsvCast(ogDoc[this.fieldKey])? CsvCast(ogDoc[this.fieldKey]).url.href : undefined; + const href = CsvCast(this.Document[this.fieldKey]).url.href + if (ogHref && !DataVizBox.datasetSchemaOG.has(href)){ // sets original dataset to the var + const lastRow = current.pop(); + DataVizBox.datasetSchemaOG.set(href, current); + current.push(lastRow!); + fetch('/csvData?uri=' + ogHref) + .then(res => res.json().then(action(res => !res.errno && DataVizBox.datasetSchemaOG.set(href, res)))); + } + return current; + }, + current => { + if (current) { + const href = CsvCast(this.Document[this.fieldKey]).url.href; + if (this.layoutDoc.dataViz_schemaLive) DataVizBox.dataset.set(href, current); + else DataVizBox.dataset.set(href, DataVizBox.datasetSchemaOG.get(href)!); + } + }, + { fireImmediately: true } + ); } fetchData = () => { - DataVizBox.dataset.set(CsvCast(this.dataDoc[this.fieldKey]).url.href, []); // assign temporary dataset as a lock to prevent duplicate server requests - fetch('/csvData?uri=' + this.dataUrl?.url.href) // - .then(res => res.json().then(action(res => !res.errno && DataVizBox.dataset.set(CsvCast(this.dataDoc[this.fieldKey]).url.href, res)))); + if (!this.Document.dataViz_asSchema) { + DataVizBox.dataset.set(CsvCast(this.dataDoc[this.fieldKey]).url.href, []); // assign temporary dataset as a lock to prevent duplicate server requests + fetch('/csvData?uri=' + this.dataUrl?.url.href) // + .then(res => res.json().then(action(res => !res.errno && DataVizBox.dataset.set(CsvCast(this.dataDoc[this.fieldKey]).url.href, res)))); + } }; // toggles for user to decide which chart type to view the data in @@ -272,6 +341,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im layoutDoc: this.layoutDoc, records: this.records, axes: this.axes, + titleCol: this.titleCol, //width: this.SidebarShown? this._props.PanelWidth()*.9/1.2: this._props.PanelWidth() * 0.9, height: (this._props.PanelHeight() / scale - 32) /* height of 'change view' button */ * 0.9, width: ((this._props.PanelWidth() - this.sidebarWidth()) / scale) * 0.9, @@ -279,7 +349,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im }; if (!this.records.length) return 'no data/visualization'; switch (this.dataVizView) { - case DataVizView.TABLE: return <TableBox {...sharedProps} docView={this.DocumentView} selectAxes={this.selectAxes} />; + case DataVizView.TABLE: return <TableBox {...sharedProps} docView={this.DocumentView} selectAxes={this.selectAxes} selectTitleCol={this.selectTitleCol}/>; case DataVizView.LINECHART: return <LineChart {...sharedProps} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={r => (this._vizRenderer = r ?? undefined)} vizBox={this} />; case DataVizView.HISTOGRAM: return <Histogram {...sharedProps} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={r => (this._vizRenderer = r ?? undefined)} />; case DataVizView.PIECHART: return <PieChart {...sharedProps} dataDoc={this.dataDoc} fieldKey={this.fieldKey} ref={r => (this._vizRenderer = r ?? undefined)} @@ -328,32 +398,11 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im }; @action - updateSchemaViz = () => { - const getFrom = DocCast(this.layoutDoc.dataViz_asSchema); - const keys = Cast(getFrom.schema_columnKeys, listSpec('string'))?.filter(key => key != 'text'); - if (!keys) return; - const children = DocListCast(getFrom[Doc.LayoutFieldKey(getFrom)]); - var current: { [key: string]: string }[] = []; - for (let i = 0; i < children.length; i++) { - var row: { [key: string]: string } = {}; - if (children[i]) { - for (let j = 0; j < keys.length; j++) { - var cell = children[i][keys[j]]; - if (cell && (cell as string)) cell = cell.toString().replace(/\,/g, ''); - row[keys[j]] = StrCast(cell); - } - } - current.push(row); - } - DataVizBox.dataset.set(CsvCast(this.Document[this.fieldKey]).url.href, current); - }; + changeLiveSchemaCheckbox = () => { + this.layoutDoc.dataViz_schemaLive = !this.layoutDoc.dataViz_schemaLive + } render() { - if (this.layoutDoc && this.layoutDoc.dataViz_asSchema) { - this._schemaDataVizChildren = DocListCast(DocCast(this.layoutDoc.dataViz_asSchema)[Doc.LayoutFieldKey(DocCast(this.layoutDoc.dataViz_asSchema))]).length; - this.updateSchemaViz(); - } - const scale = this._props.NativeDimScaling?.() || 1; return !this.records.length ? ( // displays how to get data into the DataVizBox if its empty @@ -378,27 +427,15 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() im <Toggle text={'PIE CHART'} toggleType={ToggleType.BUTTON} type={Type.SEC} color={'black'} onClick={e => (this.layoutDoc._dataViz = DataVizView.PIECHART)} toggleStatus={this.layoutDoc._dataViz == -DataVizView.PIECHART} /> </div> - {/* <CollectionFreeFormView - ref={this._ffref} - {...this._props} - setContentView={emptyFunction} - renderDepth={this._props.renderDepth - 1} - fieldKey={this.annotationKey} - styleProvider={this._props.styleProvider} - isAnnotationOverlay={true} - annotationLayerHostsContent={false} - PanelWidth={this._props.PanelWidth} - PanelHeight={this._props.PanelHeight} - select={emptyFunction} - isAnyChildContentActive={returnFalse} - whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} - removeDocument={this.removeDocument} - moveDocument={this.moveDocument} - addDocument={this.addDocument}> - {this.renderVizView} - </CollectionFreeFormView> */} + {(this.layoutDoc && this.layoutDoc.dataViz_asSchema)?( + <div className={'liveSchema-checkBox'} style={{ width: this._props.width }}> + <Checkbox color="primary" onChange={this.changeLiveSchemaCheckbox} checked={this.layoutDoc.dataViz_schemaLive as boolean} /> + Display Live Updates to Canvas + </div> + ) : null} {this.renderVizView} + <div className="dataviz-sidebar" style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }} onPointerDown={this.onPointerDown}> <SidebarAnnos ref={this._sidebarRef} diff --git a/src/client/views/nodes/DataVizBox/components/Chart.scss b/src/client/views/nodes/DataVizBox/components/Chart.scss index 2f7dd0487..41ce637ac 100644 --- a/src/client/views/nodes/DataVizBox/components/Chart.scss +++ b/src/client/views/nodes/DataVizBox/components/Chart.scss @@ -19,8 +19,6 @@ margin-bottom: -20px; } .asHistogram-checkBox { - // display: flex; - // flex-direction: row; align-items: left; align-self: left; align-content: left; @@ -93,7 +91,6 @@ display: flex; flex-direction: column; cursor: default; - margin-top: 30px; height: calc(100% - 40px); // bcz: hack 40px is the size of the button rows .tableBox-container { overflow: scroll; diff --git a/src/client/views/nodes/DataVizBox/components/Histogram.tsx b/src/client/views/nodes/DataVizBox/components/Histogram.tsx index 4a1fb2ed1..6672603f3 100644 --- a/src/client/views/nodes/DataVizBox/components/Histogram.tsx +++ b/src/client/views/nodes/DataVizBox/components/Histogram.tsx @@ -20,6 +20,7 @@ export interface HistogramProps { Document: Doc; layoutDoc: Doc; axes: string[]; + titleCol: string; records: { [key: string]: any }[]; width: number; height: number; @@ -63,17 +64,17 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { if (this._props.axes.length < 1) return []; if (this._props.axes.length < 2) { var ax0 = this._props.axes[0]; - if (/\d/.test(this._props.records[0][ax0])) { + if (!/[A-Za-z-:]/.test(this._props.records[0][ax0])){ this.numericalXData = true; } return this._tableData.map(record => ({ [ax0]: record[this._props.axes[0]] })); } var ax0 = this._props.axes[0]; var ax1 = this._props.axes[1]; - if (/\d/.test(this._props.records[0][ax0])) { + if (!/[A-Za-z-:]/.test(this._props.records[0][ax0])) { this.numericalXData = true; } - if (/\d/.test(this._props.records[0][ax1])) { + if (!/[A-Za-z-:]/.test(this._props.records[0][ax1])) { this.numericalYData = true; } return this._tableData.map(record => ({ [ax0]: record[this._props.axes[0]], [ax1]: record[this._props.axes[1]] })); @@ -89,9 +90,6 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { @computed get parentViz() { return DocCast(this._props.Document.dataViz_parentViz); - // return LinkManager.Instance.getAllRelatedLinks(this._props.Document) // out of all links - // .filter(link => link.link_anchor_1 == this._props.Document.dataViz_parentViz) // get links where this chart doc is the target of the link - // .map(link => DocCast(link.link_anchor_1)); // then return the source of the link } @computed get rangeVals(): { xMin?: number; xMax?: number; yMin?: number; yMax?: number } { @@ -438,9 +436,9 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { this.updateBarColors(); this._histogramData; var curSelectedBarName = ''; - var titleAccessor: any = ''; - if (this._props.axes.length == 2) titleAccessor = 'dataViz_histogram_title' + this._props.axes[0] + '-' + this._props.axes[1]; - else if (this._props.axes.length > 0) titleAccessor = 'dataViz_histogram_title' + this._props.axes[0]; + var titleAccessor: any = 'dataViz_histogram_title'; + if (this._props.axes.length == 2) titleAccessor = titleAccessor + this._props.axes[0] + '-' + this._props.axes[1]; + else if (this._props.axes.length > 0) titleAccessor = titleAccessor + this._props.axes[0]; if (!this._props.layoutDoc[titleAccessor]) this._props.layoutDoc[titleAccessor] = this.defaultGraphTitle; if (!this._props.layoutDoc.dataViz_histogram_defaultColor) this._props.layoutDoc.dataViz_histogram_defaultColor = '#69b3a2'; if (!this._props.layoutDoc.dataViz_histogram_barColors) this._props.layoutDoc.dataViz_histogram_barColors = new List<string>(); @@ -454,6 +452,18 @@ export class Histogram extends ObservableReactComponent<HistogramProps> { : '' ); selected = selected.substring(0, selected.length - 2) + ' }'; + if (this._props.titleCol!="" && (!this._currSelected["frequency"] || this._currSelected["frequency"]<10)){ + selected+= "\n" + this._props.titleCol + ": " + this._tableData.forEach(each => { + if (this._currSelected[this._props.axes[0]]==each[this._props.axes[0]]) { + if (this._props.axes[1]){ + if (this._currSelected[this._props.axes[1]]==each[this._props.axes[1]]) selected+= each[this._props.titleCol] + ", "; + } + else selected+= each[this._props.titleCol] + ", "; + } + }) + selected = selected.slice(0,-1).slice(0,-1); + } } var selectedBarColor; var barColors = StrListCast(this._props.layoutDoc.histogramBarColors).map(each => each.split('::')); diff --git a/src/client/views/nodes/DataVizBox/components/LineChart.tsx b/src/client/views/nodes/DataVizBox/components/LineChart.tsx index 2a9a8b354..e093ec648 100644 --- a/src/client/views/nodes/DataVizBox/components/LineChart.tsx +++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx @@ -28,6 +28,7 @@ export interface LineChartProps { Document: Doc; layoutDoc: Doc; axes: string[]; + titleCol: string; records: { [key: string]: any }[]; width: number; height: number; @@ -46,7 +47,7 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { private _disposers: { [key: string]: IReactionDisposer } = {}; private _lineChartRef: React.RefObject<HTMLDivElement> = React.createRef(); private _lineChartSvg: d3.Selection<SVGGElement, unknown, null, undefined> | undefined; - @observable _currSelected: SelectedDataPoint | undefined = undefined; + @observable _currSelected: any | undefined = undefined; // TODO: nda - some sort of mapping that keeps track of the annotated points so we can easily remove when annotations list updates constructor(props: any) { super(props); @@ -235,21 +236,16 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { } } - // TODO: nda - can use d3.create() to create html element instead of appending drawChart = (dataSet: any[][], rangeVals: { xMin?: number; xMax?: number; yMin?: number; yMax?: number }, width: number, height: number) => { // clearing tooltip and the current chart d3.select(this._lineChartRef.current).select('svg').remove(); d3.select(this._lineChartRef.current).select('.tooltip').remove(); - const { xMin, xMax, yMin, yMax } = rangeVals; + var { xMin, xMax, yMin, yMax } = rangeVals; if (xMin === undefined || xMax === undefined || yMin === undefined || yMax === undefined) { return; } - // creating the x and y scales - const xScale = scaleCreatorNumerical(xMin, xMax, 0, width); - const yScale = scaleCreatorNumerical(0, yMax, height, 0); - // adding svg const margin = this._props.margin; const svg = (this._lineChartSvg = d3 @@ -261,24 +257,71 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { .append('g') .attr('transform', `translate(${margin.left}, ${margin.top})`)); + var validSecondData; + if (this._props.axes.length>2){ // for when there are 2 lines on the chart + var next = this._tableData.map(record => ({ x: Number(record[this._props.axes[0]]), y: Number(record[this._props.axes[2]]) })).sort((a, b) => (a.x < b.x ? -1 : 1)); + validSecondData = next.filter(d => { + if (!d.x || Number.isNaN(d.x) || !d.y || Number.isNaN(d.y)) return false; + return true; + }); + var secondDataRange = minMaxRange([validSecondData]); + if (secondDataRange.xMax!>xMax) xMax = secondDataRange.xMax; + if (secondDataRange.yMax!>yMax) yMax = secondDataRange.yMax; + if (secondDataRange.xMin!<xMin) xMin = secondDataRange.xMin; + if (secondDataRange.yMin!<yMin) yMin = secondDataRange.yMin; + } + + // creating the x and y scales + const xScale = scaleCreatorNumerical(xMin!, xMax!, 0, width); + const yScale = scaleCreatorNumerical(0, yMax!, height, 0); + const lineGen = createLineGenerator(xScale, yScale); + // create x and y grids xGrid(svg.append('g'), height, xScale); yGrid(svg.append('g'), width, yScale); xAxisCreator(svg.append('g'), height, xScale); yAxisCreator(svg.append('g'), width, yScale); + if (validSecondData) { + drawLine(svg.append('path'), validSecondData, lineGen, true); + this.drawDataPoints(validSecondData, 0, xScale, yScale); + svg.append('path').attr("stroke", "red"); + + // legend + var color = d3.scaleOrdinal() + .range(["black", "blue"]) + .domain([this._props.axes[1], this._props.axes[2]]) + svg.selectAll("mydots") + .data([this._props.axes[1], this._props.axes[2]]) + .enter() + .append("circle") + .attr("cx", 5) + .attr("cy", function(d,i){ return -30 + i*15}) + .attr("r", 7) + .style("fill", function(d){ return color(d)}) + svg.selectAll("mylabels") + .data([this._props.axes[1], this._props.axes[2]]) + .enter() + .append("text") + .attr("x", 25) + .attr("y", function(d,i){ return -30 + i*15}) + .style("fill", function(d){ return color(d)}) + .text(function(d){ return d}) + .attr("text-anchor", "left") + .style("alignment-baseline", "middle") + } + // get valid data points const data = dataSet[0]; - const lineGen = createLineGenerator(xScale, yScale); var validData = data.filter(d => { - var valid = true; Object.keys(data[0]).map(key => { - if (!d[key] || Number.isNaN(d[key])) valid = false; + if (!d[key] || Number.isNaN(d[key])) return false; }); - return valid; + return true; }); + // draw the plot line - drawLine(svg.append('path'), validData, lineGen); + drawLine(svg.append('path'), validData, lineGen, false); // draw the datapoint circle this.drawDataPoints(validData, 0, xScale, yScale); @@ -291,7 +334,7 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { const xPos = d3.pointer(e)[0]; const x0 = Math.min(data.length - 1, bisect(data, xScale.invert(xPos - 5))); // shift x by -5 so that you can reach points on the left-side axis const d0 = data[x0]; - if (!d0) return; + if (d0) this.updateTooltip(higlightFocusPt, xScale, d0, yScale, tooltip); this.updateTooltip(higlightFocusPt, xScale, d0, yScale, tooltip); }); @@ -327,7 +370,7 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { svg.append('text') .attr('transform', 'rotate(-90)' + ' ' + 'translate( 0, ' + -10 + ')') .attr('x', -(height / 2)) - .attr('y', -20) + .attr('y', -30) .attr('height', 20) .attr('width', 20) .style('text-anchor', 'middle') @@ -351,11 +394,22 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { } render() { - var titleAccessor: any = ''; - if (this._props.axes.length == 2) titleAccessor = 'dataViz_lineChart_title' + this._props.axes[0] + '-' + this._props.axes[1]; - else if (this._props.axes.length > 0) titleAccessor = 'dataViz_lineChart_title' + this._props.axes[0]; + var titleAccessor: any = 'dataViz_lineChart_title'; + if (this._props.axes.length == 2) titleAccessor = titleAccessor + this._props.axes[0] + '-' + this._props.axes[1]; + else if (this._props.axes.length > 0) titleAccessor = titleAccessor + this._props.axes[0]; if (!this._props.layoutDoc[titleAccessor]) this._props.layoutDoc[titleAccessor] = this.defaultGraphTitle; const selectedPt = this._currSelected ? `{ ${this._props.axes[0]}: ${this._currSelected.x} ${this._props.axes[1]}: ${this._currSelected.y} }` : 'none'; + var selectedTitle = ""; + if (this._currSelected && this._props.titleCol){ + selectedTitle+= "\n" + this._props.titleCol + ": " + this._tableData.forEach(each => { + var mapThisEntry = false; + if (this._currSelected.x==each[this._props.axes[0]] && this._currSelected.y==each[this._props.axes[1]]) mapThisEntry = true; + else if (this._currSelected.y==each[this._props.axes[0]] && this._currSelected.x==each[this._props.axes[1]]) mapThisEntry = true; + if (mapThisEntry) selectedTitle += each[this._props.titleCol] + ", "; + }) + selectedTitle = selectedTitle.slice(0,-1).slice(0,-1); + } if (this._lineChartData.length > 0 || !this.parentViz || this.parentViz.length == 0) { return this._props.axes.length >= 2 && /\d/.test(this._props.records[0][this._props.axes[0]]) && /\d/.test(this._props.records[0][this._props.axes[1]]) ? ( <div className="chart-container" style={{ width: this._props.width + this._props.margin.right }}> @@ -375,9 +429,9 @@ export class LineChart extends ObservableReactComponent<LineChartProps> { {selectedPt != 'none' ? ( <div className={'selected-data'}> {`Selected: ${selectedPt}`} + {`${selectedTitle}`} <Button onClick={e => { - console.log('test plzz'); this._props.vizBox.sidebarBtnDown; this._props.vizBox.sidebarAddDocument; }}></Button> diff --git a/src/client/views/nodes/DataVizBox/components/PieChart.tsx b/src/client/views/nodes/DataVizBox/components/PieChart.tsx index 1259a13ff..fc23f47de 100644 --- a/src/client/views/nodes/DataVizBox/components/PieChart.tsx +++ b/src/client/views/nodes/DataVizBox/components/PieChart.tsx @@ -19,6 +19,7 @@ export interface PieChartProps { Document: Doc; layoutDoc: Doc; axes: string[]; + titleCol: string; records: { [key: string]: any }[]; width: number; height: number; @@ -331,22 +332,34 @@ export class PieChart extends ObservableReactComponent<PieChartProps> { }; render() { - var titleAccessor: any = ''; - if (this._props.axes.length == 2) titleAccessor = 'dataViz_pie_title' + this._props.axes[0] + '-' + this._props.axes[1]; - else if (this._props.axes.length > 0) titleAccessor = 'dataViz_pie_title' + this._props.axes[0]; + var titleAccessor: any = 'dataViz_pie_title'; + if (this._props.axes.length == 2) titleAccessor = titleAccessor + this._props.axes[0] + '-' + this._props.axes[1]; + else if (this._props.axes.length > 0) titleAccessor = titleAccessor + this._props.axes[0]; if (!this._props.layoutDoc[titleAccessor]) this._props.layoutDoc[titleAccessor] = this.defaultGraphTitle; if (!this._props.layoutDoc.dataViz_pie_sliceColors) this._props.layoutDoc.dataViz_pie_sliceColors = new List<string>(); var selected: string; var curSelectedSliceName = ''; if (this._currSelected) { + selected = '{ '; const sliceTitle = this._currSelected[this._props.axes[0]]; curSelectedSliceName = StrCast(sliceTitle) ? StrCast(sliceTitle).replace(/\$/g, '').replace(/\%/g, '').replace(/\#/g, '').replace(/\</g, '') : sliceTitle; - selected = '{ '; Object.keys(this._currSelected).map(key => { key != '' ? (selected += key + ': ' + this._currSelected[key] + ', ') : ''; }); selected = selected.substring(0, selected.length - 2); selected += ' }'; + if (this._props.titleCol!="" && (!this._currSelected["frequency"] || this._currSelected["frequency"]<10)){ + selected+= "\n" + this._props.titleCol + ": " + this._tableData.forEach(each => { + if (this._currSelected[this._props.axes[0]]==each[this._props.axes[0]]) { + if (this._props.axes[1]){ + if (this._currSelected[this._props.axes[1]]==each[this._props.axes[1]]) selected+= each[this._props.titleCol] + ", "; + } + else selected+= each[this._props.titleCol] + ", "; + } + }) + selected = selected.slice(0,-1).slice(0,-1); + } } else selected = 'none'; var selectedSliceColor; var sliceColors = StrListCast(this._props.layoutDoc.dataViz_pie_sliceColors).map(each => each.split('::')); diff --git a/src/client/views/nodes/DataVizBox/components/TableBox.tsx b/src/client/views/nodes/DataVizBox/components/TableBox.tsx index ed44d9269..1b239b5e5 100644 --- a/src/client/views/nodes/DataVizBox/components/TableBox.tsx +++ b/src/client/views/nodes/DataVizBox/components/TableBox.tsx @@ -18,7 +18,9 @@ interface TableBoxProps { layoutDoc: Doc; records: { [key: string]: any }[]; selectAxes: (axes: string[]) => void; + selectTitleCol: (titleCol: string) => void; axes: string[]; + titleCol: string; width: number; height: number; margin: { @@ -83,14 +85,12 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { return this._props.docView?.()?.screenToViewTransform().Scale || 1; } @computed get rowHeight() { - console.log('scale = ' + this.viewScale + ' table = ' + this._tableHeight + ' ids = ' + this._tableDataIds.length); return (this.viewScale * this._tableHeight) / this._tableDataIds.length; } @computed get startID() { return this.rowHeight ? Math.max(Math.floor(this._scrollTop / this.rowHeight) - 1, 0) : 0; } @computed get endID() { - console.log('start = ' + this.startID + ' container = ' + this._tableContainerHeight + ' scale = ' + this.viewScale + ' row = ' + this.rowHeight); return Math.ceil(this.startID + (this._tableContainerHeight * this.viewScale) / (this.rowHeight || 1)); } @action handleScroll = () => { @@ -155,11 +155,18 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { }, emptyFunction, action(e => { - const newAxes = this._props.axes; - if (newAxes.includes(col)) newAxes.splice(newAxes.indexOf(col), 1); - else if (newAxes.length > 1) newAxes[1] = col; - else newAxes.push(col); - this._props.selectAxes(newAxes); + if (e.shiftKey){ + if (this._props.titleCol == col) this._props.titleCol = ""; + else this._props.titleCol = col; + this._props.selectTitleCol(this._props.titleCol); + } + else{ + const newAxes = this._props.axes; + if (newAxes.includes(col)) newAxes.splice(newAxes.indexOf(col), 1); + else if (newAxes.length > 2) newAxes[newAxes.length-1] = col; + else newAxes.push(col); + this._props.selectAxes(newAxes); + } }) ); }; @@ -213,8 +220,15 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { <th key={this.columns.indexOf(col)} style={{ - color: this._props.axes.slice().reverse().lastElement() === col ? 'darkgreen' : this._props.axes.lastElement() === col ? 'darkred' : undefined, - background: this._props.axes.slice().reverse().lastElement() === col ? '#E3fbdb' : this._props.axes.lastElement() === col ? '#Fbdbdb' : undefined, + color: this._props.axes.slice().reverse().lastElement() === col ? 'darkgreen' + : (this._props.axes.length>2 && this._props.axes.lastElement() === col) ? 'darkred' + : (this._props.axes.lastElement()===col || (this._props.axes.length>2 && this._props.axes[1]==col))? 'darkblue' : undefined, + background: this._props.axes.slice().reverse().lastElement() === col ? '#E3fbdb' + : (this._props.axes.length>2 && this._props.axes.lastElement() === col) ? '#Fbdbdb' + : (this._props.axes.lastElement()===col || (this._props.axes.length>2 && this._props.axes[1]==col))? '#c6ebf7' : undefined, + // blue: #ADD8E6 + // green: #E3fbdb + // red: #Fbdbdb fontWeight: 'bolder', border: '3px solid black', }} @@ -236,7 +250,11 @@ export class TableBox extends ObservableReactComponent<TableBoxProps> { background: NumListCast(this._props.layoutDoc.dataViz_highlitedRows).includes(rowId) ? 'lightYellow' : NumListCast(this._props.layoutDoc.dataViz_selectedRows).includes(rowId) ? 'lightgrey' : '', }}> {this.columns.map(col => { - const colSelected = this._props.axes.length > 1 ? this._props.axes[0] == col || this._props.axes[1] == col : this._props.axes.length > 0 ? this._props.axes[0] == col : false; + var colSelected = false; + if (this._props.axes.length>2) colSelected = this._props.axes[0]==col || this._props.axes[1]==col || this._props.axes[2]==col; + else if (this._props.axes.length>1) colSelected = this._props.axes[0]==col || this._props.axes[1]==col; + else if (this._props.axes.length>0) colSelected = this._props.axes[0]==col; + if (this._props.titleCol==col) colSelected = true; return ( <td key={this.columns.indexOf(col)} style={{ border: colSelected ? '3px solid black' : '1px solid black', fontWeight: colSelected ? 'bolder' : 'normal' }}> <div className="tableBox-cell">{this._props.records[rowId][col]}</div> diff --git a/src/client/views/nodes/DataVizBox/utils/D3Utils.ts b/src/client/views/nodes/DataVizBox/utils/D3Utils.ts index 10bfb0c64..336935d23 100644 --- a/src/client/views/nodes/DataVizBox/utils/D3Utils.ts +++ b/src/client/views/nodes/DataVizBox/utils/D3Utils.ts @@ -61,6 +61,6 @@ export const yGrid = (g: d3.Selection<SVGGElement, unknown, null, undefined>, wi ); }; -export const drawLine = (p: d3.Selection<SVGPathElement, unknown, null, undefined>, dataPts: DataPoint[], lineGen: d3.Line<DataPoint>) => { - p.datum(dataPts).attr('fill', 'none').attr('stroke', 'rgba(53, 162, 235, 0.5)').attr('stroke-width', 2).attr('class', 'line').attr('d', lineGen); +export const drawLine = (p: d3.Selection<SVGPathElement, unknown, null, undefined>, dataPts: DataPoint[], lineGen: d3.Line<DataPoint>, extra: boolean) => { + p.datum(dataPts).attr('fill', 'none').attr('stroke', 'rgba(53, 162, 235, 0.5)').attr('stroke-width', 2).attr('stroke', extra? 'blue' : 'black').attr('class', 'line').attr('d', lineGen); }; diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx index d1805308d..2a68d2bf6 100644 --- a/src/client/views/nodes/DocumentLinksButton.tsx +++ b/src/client/views/nodes/DocumentLinksButton.tsx @@ -157,7 +157,7 @@ export class DocumentLinksButton extends ObservableReactComponent<DocumentLinksB startLink = DocumentLinksButton.StartLinkView?.ComponentView?.getAnchor?.(true) || startLink; const linkDoc = DocUtils.MakeLink(startLink, endLink, { link_relationship: DocumentLinksButton.AnnotationId ? 'hypothes.is annotation' : undefined }); - LinkManager.currentLink = linkDoc; + LinkManager.Instance.currentLink = linkDoc; if (linkDoc) { if (DocumentLinksButton.AnnotationId && DocumentLinksButton.AnnotationUri) { diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 8eb354e1e..d131f72d5 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -20,13 +20,14 @@ import { DocServer } from '../../DocServer'; import { Networking } from '../../Network'; import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes'; -import { DocOptions, DocUtils, Docs, FInfo } from '../../documents/Documents'; +import { DocOptions, DocUtils, Docs } from '../../documents/Documents'; import { DictationManager } from '../../util/DictationManager'; import { DocumentManager } from '../../util/DocumentManager'; import { DragManager, dropActionType } from '../../util/DragManager'; import { FollowLinkScript } from '../../util/LinkFollower'; import { LinkManager } from '../../util/LinkManager'; import { ScriptingGlobals } from '../../util/ScriptingGlobals'; +import { SearchUtil } from '../../util/SearchUtil'; import { SelectionManager } from '../../util/SelectionManager'; import { SettingsManager } from '../../util/SettingsManager'; import { SharingManager } from '../../util/SharingManager'; @@ -103,9 +104,11 @@ export interface DocumentViewProps extends FieldViewSharedProps { dontHideOnDrag?: boolean; suppressSetHeight?: boolean; onClickScriptDisable?: 'never' | 'always'; // undefined = only when selected + DataTransition?: () => string | undefined; NativeWidth?: () => number; NativeHeight?: () => number; contextMenuItems?: () => { script: ScriptField; filter?: ScriptField; label: string; icon: string }[]; + dragConfig?: (data: DragManager.DocumentDragData) => void; dragStarting?: () => void; dragEnding?: () => void; } @@ -114,7 +117,6 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document // this makes mobx trace() statements more descriptive public get displayName() { return 'DocumentViewInternal(' + this.Document.title + ')'; } // prettier-ignore public static SelectAfterContextMenu = true; // whether a document should be selected after it's contextmenu is triggered. - /** * This function is filled in by MainView to allow non-viewBox views to add Docs as tabs without * needing to know about/reference MainView @@ -236,16 +238,15 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document runInAction(() => (this._mounted = true)); this.setupHandlers(); this._disposers.contentActive = reaction( - () => { + () => // true - if the document has been activated directly or indirectly (by having its children selected) // false - if its pointer events are explicitly turned off or if it's container tells it that it's inactive // undefined - it is not active, but it should be responsive to actions that might activate it or its contents (eg clicking) - return this._props.isContentActive() === false || this._props.pointerEvents?.() === 'none' + this._props.isContentActive() === false || this._props.pointerEvents?.() === 'none' ? false : Doc.ActiveTool !== InkTool.None || SnappingManager.CanEmbed || this.rootSelected() || this.Document.forceActive || this._componentView?.isAnyChildContentActive?.() || this._props.isContentActive() ? true - : undefined; - }, + : undefined, active => (this._isContentActive = active), { fireImmediately: true } ); @@ -289,7 +290,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document dragData.moveDocument = this._props.moveDocument; dragData.draggedViews = [docView]; dragData.canEmbed = this.Document.dragAction ?? this._props.dragAction ? true : false; - this._componentView?.dragConfig?.(dragData); + (this._props.dragConfig ?? this._componentView?.dragConfig)?.(dragData); DragManager.StartDocumentDrag( selected.map(dv => dv.ContentDiv!), dragData, @@ -312,20 +313,23 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document let stopPropagate = true; let preventDefault = true; !this.layoutDoc._keepZWhenDragged && this._props.bringToFront?.(this.Document); + const scriptProps = { + this: this.Document, + _readOnly_: false, + scriptContext: this._props.scriptContext, + documentView, + clientX: e.clientX, + clientY: e.clientY, + shiftKey: e.shiftKey, + altKey: e.altKey, + metaKey: e.metaKey, + value: undefined, + }; if (this._doubleTap) { const defaultDblclick = this._props.defaultDoubleClick?.() || this.Document.defaultDoubleClick; if (this.onDoubleClickHandler?.script) { - const { clientX, clientY, shiftKey, altKey, ctrlKey } = e; // or we could call e.persist() to capture variables - // prettier-ignore - const func = () => this.onDoubleClickHandler.script.run( { - this: this.Document, - scriptContext: this._props.scriptContext, - documentView, - clientX, clientY, altKey, shiftKey, ctrlKey, - value: undefined, - }, console.log ); - UndoManager.RunInBatch(() => (func().result?.select === true ? this._props.select(false) : ''), 'on double click'); - } else if (!Doc.IsSystem(this.Document) && (defaultDblclick === undefined || defaultDblclick === 'default')) { + UndoManager.RunInBatch(() => this.onDoubleClickHandler.script.run(scriptProps, console.log).result?.select && this._props.select(false), 'on double click: ' + this.Document.title); + } else if (!Doc.IsSystem(this.Document) && defaultDblclick !== 'ignore') { UndoManager.RunInBatch(() => LightboxView.Instance.AddDocTab(this.Document, OpenWhere.lightbox), 'double tap'); SelectionManager.DeselectAll(); Doc.UnBrushDoc(this.Document); @@ -338,33 +342,14 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document } else { let clickFunc: undefined | (() => any); if (!this.disableClickScriptFunc && this.onClickHandler?.script) { - const { clientX, clientY, shiftKey, altKey, metaKey } = e; - const func = () => { - // replace default add doc func with this view's add doc func. - // to allow override behaviors for how to display links to undisplayed documents. - // e.g., if this document is part of a labeled 'lightbox' container, then documents will be shown in place - // instead of in the global lightbox + clickFunc = undoable(() => { + // use this view's add doc func to override method for following links to undisplayed documents. + // e.g., if this document is part of a labeled 'lightbox' container, then documents will be shown in this container of in the global lightbox const oldFunc = DocumentViewInternal.addDocTabFunc; DocumentViewInternal.addDocTabFunc = this._props.addDocTab; - this.onClickHandler?.script.run( - { - this: this.Document, - _readOnly_: false, - scriptContext: this._props.scriptContext, - documentView, - clientX, - clientY, - shiftKey, - altKey, - metaKey, - }, - console.log - ).result?.select === true - ? this._props.select(false) - : ''; + this.onClickHandler?.script.run(scriptProps, console.log).result?.select && this._props.select(false); DocumentViewInternal.addDocTabFunc = oldFunc; - }; - clickFunc = () => UndoManager.RunInBatch(func, 'click ' + this.Document.title); + }, 'click ' + this.Document.title); } else { // onDragStart implies a button doc that we don't want to select when clicking. RootDocument & isTemplateForField implies we're clicking on part of a template instance and we want to select the whole template, not the part if ((this.layoutDoc.onDragStart || this._props.TemplateDataDocument) && !(e.ctrlKey || e.button > 0)) { @@ -376,7 +361,6 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document this._singleClickFunc = // prettier-ignore clickFunc ?? (() => (sendToBack ? documentView._props.bringToFront?.(this.Document, true) : - this._componentView?.select?.(e.ctrlKey || e.metaKey, e.shiftKey) ?? this._props.select(e.ctrlKey||e.shiftKey, e.metaKey))); const waitFordblclick = this._props.waitForDoubleClickToClick?.() ?? this.Document.waitForDoubleClickToClick; if ((clickFunc && waitFordblclick !== 'never') || waitFordblclick === 'always') { @@ -412,10 +396,9 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document !Doc.IsInMyOverlay(this.layoutDoc) ) { e.stopPropagation(); - // don't preventDefault anymore. Goldenlayout, PDF text selection and RTF text selection all need it to go though - //if (this._props.isSelected(true) && this.Document.type !== DocumentType.PDF && this.layoutDoc._type_collection !== CollectionViewType.Docking) e.preventDefault(); + // don't preventDefault. Goldenlayout, PDF text selection and RTF text selection all need it to go though - // listen to move events if document content isn't active or document is draggable + // listen to move events when document content isn't active or document is always draggable if (!this.layoutDoc._lockedPosition && (!this.isContentActive() || BoolCast(this.layoutDoc._dragWhenActive, this._props.dragWhenActive))) { document.addEventListener('pointermove', this.onPointerMove); } @@ -474,11 +457,11 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document deleteClicked = undoable(() => this._props.removeDocument?.(this.Document), 'delete doc'); setToggleDetail = undoable( - () => - (this.Document.onClick = ScriptField.MakeScript( + (defaultLayout: string, scriptFieldKey: 'onClick') => + (this.Document[scriptFieldKey] = ScriptField.MakeScript( `toggleDetail(documentView, "${StrCast(this.Document.layout_fieldKey) .replace('layout_', '') - .replace(/^layout$/, 'detail')}")`, + .replace(/^layout$/, 'detail')}", "${defaultLayout}")`, { documentView: 'any' } )), 'set toggle detail' @@ -489,26 +472,24 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document if (this.Document === Doc.ActiveDashboard) { e.stopPropagation(); e.preventDefault(); - alert( - (e.target as any)?.closest?.('*.lm_content') - ? "You can't perform this move most likely because you didn't drag the document's title bar to enable embedding in a different document." - : 'Linking to document tabs not yet supported. Drop link on document content.' - ); + alert((e.target as any)?.closest?.('*.lm_content') ? "You can't perform this move most likely because you didn't drag the document's title bar to enable embedding in a different document." : 'Linking to document tabs not yet supported.'); return true; } - const linkdrag = de.complete.annoDragData ?? de.complete.linkDragData; + const annoData = de.complete.annoDragData; + const linkdrag = annoData ?? de.complete.linkDragData; if (linkdrag) { linkdrag.linkSourceDoc = linkdrag.linkSourceGetAnchor(); if (linkdrag.linkSourceDoc && linkdrag.linkSourceDoc !== this.Document) { - if (de.complete.annoDragData && !de.complete.annoDragData.dropDocument) { - de.complete.annoDragData.dropDocument = de.complete.annoDragData.dropDocCreator(undefined); + if (annoData && !annoData.dropDocument) { + annoData.dropDocument = annoData.dropDocCreator(undefined); } - if (de.complete.annoDragData || this.Document !== linkdrag.linkSourceDoc.embedContainer) { - const dropDoc = de.complete.annoDragData?.dropDocument ?? this._componentView?.getAnchor?.(true) ?? this.Document; - de.complete.linkDocument = DocUtils.MakeLink(linkdrag.linkSourceDoc, dropDoc, {}, undefined, [de.x, de.y - 50]); - if (de.complete.linkDocument) { - de.complete.linkDocument.layout_isSvg = true; - this._docView?.CollectionFreeFormView?.addDocument(de.complete.linkDocument); + if (annoData || this.Document !== linkdrag.linkSourceDoc.embedContainer) { + const dropDoc = annoData?.dropDocument ?? this._componentView?.getAnchor?.(true) ?? this.Document; + const linkDoc = DocUtils.MakeLink(linkdrag.linkSourceDoc, dropDoc, {}, undefined, [de.x, de.y - 50]); + if (linkDoc) { + de.complete.linkDocument = linkDoc; + linkDoc.layout_isSvg = true; + DocumentManager.LinkCommonAncestor(linkDoc)?.ComponentView?.addDocument?.(linkDoc); } } e.stopPropagation(); @@ -518,25 +499,6 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document return false; }, 'drop doc'); - makeIntoPortal = undoable(() => { - const portalLink = this._allLinks.find(d => d.link_anchor_1 === this.Document && d.link_relationship === 'portal to:portal from'); - if (!portalLink) { - DocUtils.MakeLink( - this.Document, - Docs.Create.FreeformDocument([], { - _width: NumCast(this.layoutDoc._width) + 10, - _height: Math.max(NumCast(this.layoutDoc._height), NumCast(this.layoutDoc._width) + 10), - _isLightbox: true, - _layout_fitWidth: true, - title: StrCast(this.Document.title) + ' [Portal]', - }), - { link_relationship: 'portal to:portal from' } - ); - } - this.Document.followLinkLocation = OpenWhere.lightbox; - this.Document.onClick = FollowLinkScript(); - }, 'make into portal'); - importDocument = () => { const input = document.createElement('input'); input.type = 'file'; @@ -627,17 +589,13 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document const existingOnClick = cm.findByDescription('OnClick...'); const onClicks: ContextMenuProps[] = existingOnClick && 'subitems' in existingOnClick ? existingOnClick.subitems : []; - onClicks.push({ description: 'Enter Portal', event: this.makeIntoPortal, icon: 'window-restore' }); + onClicks.push({ description: 'Enter Portal', event: undoable(e => DocUtils.makeIntoPortal(this.Document, this.layoutDoc, this._allLinks), 'make into portal'), icon: 'window-restore' }); !Doc.noviceMode && onClicks.push({ description: 'Toggle Detail', event: this.setToggleDetail, icon: 'concierge-bell' }); if (!this.Document.annotationOn) { - const options = cm.findByDescription('Options...'); - const optionItems: ContextMenuProps[] = options && 'subitems' in options ? options.subitems : []; - !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'compass' }); - onClicks.push({ description: this.onClickHandler ? 'Remove Click Behavior' : 'Follow Link', event: () => this.toggleFollowLink(false, false), icon: 'link' }); !Doc.noviceMode && onClicks.push({ description: 'Edit onClick Script', event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.Document, undefined, 'onClick'), 'edit onClick'), icon: 'terminal' }); - !existingOnClick && cm.addItem({ description: 'OnClick...', noexpand: true, subitems: onClicks, icon: 'mouse-pointer' }); + cm.addItem({ description: 'OnClick...', noexpand: true, subitems: onClicks, icon: 'mouse-pointer' }); } else if (LinkManager.Links(this.Document).length) { onClicks.push({ description: 'Restore On Click default', event: () => this.noOnClick(), icon: 'link' }); onClicks.push({ description: 'Follow Link on Click', event: () => this.followLinkOnClick(), icon: 'link' }); @@ -768,7 +726,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document }; removeLinkByHiding = (link: Doc) => () => (link.link_displayLine = false); - allLinkEndpoints = () => { + @computed get allLinkEndpoints() { // the small blue dots that mark the endpoints of links if (this._componentView instanceof KeyValueBox || this._props.hideLinkAnchors || this.layoutDoc.layout_hideLinkAnchors || this._props.dontRegisterView || this.layoutDoc.layout_unrendered) return null; return this.filteredLinks.map(link => ( @@ -792,9 +750,9 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document /> </div> )); - }; + } - viewBoxContents = () => { + @computed get viewBoxContents() { TraceMobx(); const isInk = this.layoutDoc._layout_isSvg && !this._props.LayoutTemplateString; const noBackground = this.Document.isGroup && !this._props.LayoutTemplateString?.includes(KeyValueBox.name) && (!this.layoutDoc.backgroundColor || this.layoutDoc.backgroundColor === 'transparent'); @@ -818,12 +776,12 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document rootSelected={this.rootSelected} onClickScript={this.onClickFunc} setTitleFocus={this.setTitleFocus} - hideClickBehaviors={BoolCast(this.layoutDoc.hideClickBehaviors)} + hideClickBehaviors={BoolCast(this.Document.hideClickBehaviors)} /> - {this.layoutDoc.layout_hideAllLinks ? null : this.allLinkEndpoints()} + {this.layoutDoc.layout_hideAllLinks ? null : this.allLinkEndpoints} </div> ); - }; + } captionStyleProvider = (doc: Opt<Doc>, props: Opt<FieldViewProps>, property: string) => this._props?.styleProvider?.(doc, props, property + ':caption'); fieldsDropdown = (reqdFields: string[], dropdownWidth: number, placeholder: string, onChange: (val: string | number) => void, onClose: () => void) => { @@ -856,7 +814,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document * setting layout_showTitle using the format: field1[;field2[...][:hover]] * from the UI, this is done by clicking the title field and prefixin the format with '#'. eg., #field1[;field2;...][:hover] **/ - titleView = () => { + @computed get titleView() { const showTitle = this.layout_showTitle?.split(':')[0]; const showTitleHover = this.layout_showTitle?.includes(':hover'); @@ -930,9 +888,9 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document </div> </div> ); - }; + } - captionView = () => { + @computed get captionView() { return !this.layout_showCaption ? null : ( <div className="documentView-captionWrapper" @@ -955,7 +913,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document /> </div> ); - }; + } renderDoc = (style: object) => { TraceMobx(); @@ -975,15 +933,15 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document fontFamily: StrCast(this.Document._text_fontFamily, 'inherit'), fontSize: Cast(this.Document._text_fontSize, 'string', null), transform: this._animateScalingTo ? `scale(${this._animateScalingTo})` : undefined, - transition: !this._animateScalingTo ? StrCast(this.Document.dataTransition) : `transform ${this.animateScaleTime() / 1000}s ease-${this._animateScalingTo < 1 ? 'in' : 'out'}`, + transition: !this._animateScalingTo ? this._props.DataTransition?.() : `transform ${this.animateScaleTime() / 1000}s ease-${this._animateScalingTo < 1 ? 'in' : 'out'}`, }}> {this._props.hideTitle || (!showTitle && !this.layout_showCaption) ? ( - this.viewBoxContents() + this.viewBoxContents ) : ( <div className="documentView-styleWrapper"> - {this.titleView()} - {this.viewBoxContents()} - {this.captionView()} + {this.titleView} + {this.viewBoxContents} + {this.captionView} </div> )} {this.widgetDecorations ?? null} @@ -1117,6 +1075,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { private _disposers: { [name: string]: IReactionDisposer } = {}; private _viewTimer: NodeJS.Timeout | undefined; private _animEffectTimer: NodeJS.Timeout | undefined; + public Guid = Utils.GenerateGuid(); // a unique id associated with the main <div>. used by LinkBox's Xanchor to find the arrowhead locations. @computed public static get exploreMode() { return () => (SnappingManager.ExploreMode ? ScriptField.MakeScript('CollectionBrowseClick(documentView, clientX, clientY)', { documentView: 'any', clientX: 'number', clientY: 'number' })! : undefined); @@ -1193,6 +1152,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { } componentWillUnmount() { + this._viewTimer && clearTimeout(this._viewTimer); runInAction(() => this.Document[DocViews].delete(this)); Object.values(this._disposers).forEach(disposer => disposer?.()); !BoolCast(this.Document.dontRegisterView, this._props.dontRegisterView) && DocumentManager.Instance.RemoveView(this); @@ -1216,21 +1176,23 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { return this._props.LayoutTemplateString?.includes('link_anchor_2') ? DocCast(this.Document['link_anchor_2']) : this._props.LayoutTemplateString?.includes('link_anchor_1') ? DocCast(this.Document['link_anchor_1']) : undefined; } - @computed get getBounds() { - if (!this._docViewInternal?._contentDiv || Doc.AreProtosEqual(this.Document, Doc.UserDoc())) { + @computed get getBounds(): Opt<{ left: number; top: number; right: number; bottom: number; transition?: string }> { + if (!this.ContentDiv || Doc.AreProtosEqual(this.Document, Doc.UserDoc())) { return undefined; } - if (this._docViewInternal._componentView?.screenBounds?.()) { - return this._docViewInternal._componentView.screenBounds(); + if (this.ComponentView?.screenBounds?.()) { + return this.ComponentView.screenBounds(); } const xf = this.screenToContentsTransform().scale(this.nativeScaling).inverse(); const [[left, top], [right, bottom]] = [xf.transformPoint(0, 0), xf.transformPoint(this.panelWidth, this.panelHeight)]; if (this._props.LayoutTemplateString?.includes(LinkAnchorBox.name)) { - const docuBox = this._docViewInternal._contentDiv.getElementsByClassName('linkAnchorBox-cont'); - if (docuBox.length) return { ...docuBox[0].getBoundingClientRect(), center: undefined }; + const docuBox = this.ContentDiv.getElementsByClassName('linkAnchorBox-cont'); + if (docuBox.length) return { ...docuBox[0].getBoundingClientRect(), transition: undefined }; } - return { left, top, right, bottom }; + // transition is returned so that the bounds will 'update' at the end of an animated transition. This is needed by xAnchor in LinkBox + const transition = this.docViewPath().find((parent: DocumentView) => parent.DataTransition?.() || parent.ComponentView?.viewTransition?.()); + return { left, top, right, bottom, transition: transition?.DataTransition?.() || transition?.ComponentView?.viewTransition?.() }; } @computed get nativeWidth() { @@ -1254,9 +1216,8 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { this.layoutDoc._viewTransition = undefined; }; public noOnClick = () => this._docViewInternal?.noOnClick(); - public makeIntoPortal = () => this._docViewInternal?.makeIntoPortal(); public toggleFollowLink = (zoom?: boolean, setTargetToggle?: boolean): void => this._docViewInternal?.toggleFollowLink(zoom, setTargetToggle); - public setToggleDetail = () => this._docViewInternal?.setToggleDetail(); + public setToggleDetail = (defaultLayout = '', scriptFieldKey = 'onClick') => this._docViewInternal?.setToggleDetail(defaultLayout, scriptFieldKey); public onContextMenu = (e?: React.MouseEvent, pageX?: number, pageY?: number) => this._docViewInternal?.onContextMenu?.(e, pageX, pageY); public cleanupPointerEvents = () => this._docViewInternal?.cleanupPointerEvents(); public startDragging = (x: number, y: number, dropAction: dropActionType, hideSource = false) => this._docViewInternal?.startDragging(x, y, dropAction, hideSource); @@ -1376,6 +1337,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { } } }; + DataTransition = () => this._props.DataTransition?.() || StrCast(this.Document.dataTransition); ShouldNotScale = () => this.shouldNotScale; NativeWidth = () => this.effectiveNativeWidth; NativeHeight = () => this.effectiveNativeHeight; @@ -1428,7 +1390,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { const yshift = Math.abs(this.Yshift) <= 0.001 ? this._props.PanelHeight() : undefined; return ( - <div className="contentFittingDocumentView" onPointerEnter={action(() => (this._isHovering = true))} onPointerLeave={action(() => (this._isHovering = false))}> + <div id={this.Guid} className="contentFittingDocumentView" onPointerEnter={action(() => (this._isHovering = true))} onPointerLeave={action(() => (this._isHovering = false))}> {!this.Document || !this._props.PanelWidth() ? null : ( <div className="contentFittingDocumentView-previewDoc" @@ -1441,6 +1403,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() { <DocumentViewInternal {...this._props} fieldKey={this.LayoutFieldKey} + DataTransition={this.DataTransition} DocumentView={this.selfView} docViewPath={this.docViewPath} PanelWidth={this.PanelWidth} @@ -1504,13 +1467,13 @@ ScriptingGlobals.add(function deiconifyViewToLightbox(documentView: DocumentView LightboxView.Instance.AddDocTab(documentView.Document, OpenWhere.lightbox, 'layout'); //, 0); }); -ScriptingGlobals.add(function toggleDetail(dv: DocumentView, detailLayoutKeySuffix: string) { - if (dv.Document.layout_fieldKey === 'layout_' + detailLayoutKeySuffix) dv.switchViews(false, 'layout'); +ScriptingGlobals.add(function toggleDetail(dv: DocumentView, detailLayoutKeySuffix: string, defaultLayout = '') { + if (dv.Document.layout_fieldKey === 'layout_' + detailLayoutKeySuffix) dv.switchViews(defaultLayout ? true : false, defaultLayout, undefined, true); else dv.switchViews(true, detailLayoutKeySuffix, undefined, true); }); ScriptingGlobals.add(function updateLinkCollection(linkCollection: Doc, linkSource: Doc) { - const collectedLinks = DocListCast(Doc.GetProto(linkCollection).data); + const collectedLinks = DocListCast(linkCollection[DocData].data); let wid = NumCast(linkSource._width); let embedding: Doc | undefined; const links = LinkManager.Links(linkSource); @@ -1529,3 +1492,29 @@ ScriptingGlobals.add(function updateLinkCollection(linkCollection: Doc, linkSour embedding && DocServer.UPDATE_SERVER_CACHE(); // if a new embedding was made, update the client's server cache so that it will not come back as a promise return links; }); +ScriptingGlobals.add(function updateTagsCollection(collection: Doc) { + const tag = StrCast(collection.title).split('-->')[1]; + const matchedTags = Array.from(SearchUtil.SearchCollection(Doc.MyFilesystem, tag, false, ['tags']).keys()); + const collectionDocs = DocListCast(collection[DocData].data).concat(collection); + let wid = 100; + let created = false; + const matchedDocs = matchedTags + .filter(tagDoc => !Doc.AreProtosEqual(collection, tagDoc)) + .reduce((aset, tagDoc) => { + let embedding = Array.from(aset).find(doc => Doc.AreProtosEqual(tagDoc, doc)) ?? collectionDocs.find(doc => Doc.AreProtosEqual(tagDoc, doc)); + if (!embedding) { + embedding = Doc.MakeEmbedding(tagDoc); + embedding.x = wid; + embedding.y = 0; + embedding._lockedPosition = false; + wid += NumCast(tagDoc._width); + created = true; + } + Doc.SetContainer(embedding, collection); + aset.add(embedding); + return aset; + }, new Set<Doc>()); + + created && (collection[DocData].data = new List<Doc>(Array.from(matchedDocs))); + return true; +}); diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.scss b/src/client/views/nodes/FontIconBox/FontIconBox.scss index db2ffa756..2db285910 100644 --- a/src/client/views/nodes/FontIconBox/FontIconBox.scss +++ b/src/client/views/nodes/FontIconBox/FontIconBox.scss @@ -1,5 +1,15 @@ @import '../../global/globalCssVariables.module.scss'; +// bcz: something's messed up with the IconButton css. this mostly fixes the fit-all button, the color buttons, the undo +/- expander and the dropdown doc type list (eg 'text') +.iconButton-container { + width: unset !important; + min-width: 30px !important; + height: unset !important; + min-height: 30px; + .color { + height: 3px !important; + } +} .menuButton { height: 100%; display: flex; diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx index cf07d98be..3577cc8d9 100644 --- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx +++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx @@ -73,7 +73,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { }; specificContextMenu = (): void => { - if (!Doc.noviceMode) { + if (!Doc.noviceMode && Cast(this.layoutDoc.dragFactory, Doc, null)) { const cm = ContextMenu.Instance; cm.addItem({ description: 'Show Template', event: this.showTemplate, icon: 'tag' }); cm.addItem({ description: 'Use as Render Template', event: this.dragAsTemplate, icon: 'tag' }); @@ -366,7 +366,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { ); } - render() { + renderButton = () => { const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color); const tooltip = StrCast(this.Document.toolTip); const scriptFunc = () => ScriptCast(this.Document.onClick)?.script.run({ this: this.Document, self: this.Document, _readOnly_: false }); @@ -381,7 +381,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { case ButtonType.ColorButton: return this.colorButton; case ButtonType.MultiToggleButton: return this.multiToggleButton; case ButtonType.ToggleButton: return this.toggleButton; - case ButtonType.ClickButton: + case ButtonType.ClickButton:return <IconButton {...btnProps} size={Size.MEDIUM} color={color} />; case ButtonType.ToolButton: return <IconButton {...btnProps} size={Size.LARGE} color={color} />; case ButtonType.TextButton: return <Button {...btnProps} color={color} background={SettingsManager.userBackgroundColor} text={StrCast(this.dataDoc.buttonText)}/>; @@ -389,5 +389,13 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { background={SettingsManager.userBackgroundColor} size={Size.LARGE} tooltipPlacement='right' onPointerDown={scriptFunc} />; } return this.defaultButton; + }; + + render() { + return ( + <div style={{ margin: 'auto', width: '100%' }} onContextMenu={this.specificContextMenu}> + {this.renderButton()} + </div> + ); } } diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx index 10eeff08d..fd3074a88 100644 --- a/src/client/views/nodes/LabelBox.tsx +++ b/src/client/views/nodes/LabelBox.tsx @@ -88,7 +88,7 @@ export class LabelBox extends ViewBoxBaseComponent<LabelBoxProps>() { @observable _mouseOver = false; @computed get hoverColor() { - return this._mouseOver ? StrCast(this.layoutDoc._hoverBackgroundColor) : this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor); + return this._mouseOver ? StrCast(this.layoutDoc._hoverBackgroundColor) : this._props.styleProvider?.(this.Document, this._props, StyleProp.BackgroundColor); } getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { diff --git a/src/client/views/nodes/LinkBox.scss b/src/client/views/nodes/LinkBox.scss index 767f0291b..484fb301e 100644 --- a/src/client/views/nodes/LinkBox.scss +++ b/src/client/views/nodes/LinkBox.scss @@ -5,3 +5,28 @@ .linkBox-container { width: 100%; } + +.linkBox { + transition: inherit; + pointer-events: none; + position: absolute; + width: 100%; + height: 100%; + path { + transition: inherit; + fill: transparent; + } + svg { + transition: inherit; + overflow: visible; + } + text { + cursor: default; + text-anchor: middle; + font-size: 12; + stroke: black; + } + circle { + cursor: default; + } +} diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx index 8b6293806..decdbb240 100644 --- a/src/client/views/nodes/LinkBox.tsx +++ b/src/client/views/nodes/LinkBox.tsx @@ -1,14 +1,17 @@ -import { Bezier } from 'bezier-js'; -import { computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx'; +import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; +import Xarrow from 'react-xarrows'; +import { DocData } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { DocCast, NumCast, StrCast } from '../../../fields/Types'; -import { aggregateBounds, emptyFunction, returnAlways, returnFalse, Utils } from '../../../Utils'; +import { DashColor, emptyFunction, lightOrDark, returnFalse } from '../../../Utils'; import { DocumentManager } from '../../util/DocumentManager'; -import { Transform } from '../../util/Transform'; -import { CollectionFreeFormView } from '../collections/collectionFreeForm'; +import { LinkManager } from '../../util/LinkManager'; +import { SnappingManager } from '../../util/SnappingManager'; import { ViewBoxBaseComponent } from '../DocComponent'; +import { EditableView } from '../EditableView'; +import { LightboxView } from '../LightboxView'; import { StyleProp } from '../StyleProvider'; import { ComparisonBox } from './ComparisonBox'; import { FieldView, FieldViewProps } from './FieldView'; @@ -19,152 +22,178 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() { public static LayoutString(fieldKey: string = 'link') { return FieldView.LayoutString(LinkBox, fieldKey); } + disposer: IReactionDisposer | undefined; + @observable _forceAnimate = 0; // forces xArrow to animate when a transition animation is detected on something that affects an anchor + @observable _hide = false; // don't render if anchor is not visible since that breaks xAnchor constructor(props: FieldViewProps) { super(props); makeObservable(this); } + @computed get anchor1() { return this.anchor(1); } // prettier-ignore + @computed get anchor2() { return this.anchor(2); } // prettier-ignore - onClickScriptDisable = returnAlways; - @computed get anchor1() { - const anchor1 = DocCast(this.dataDoc.link_anchor_1); - const anchor_1 = anchor1?.layout_unrendered ? DocCast(anchor1.annotationOn) : anchor1; - return DocumentManager.Instance.getDocumentView(anchor_1, this.DocumentView?.().containerViewPath?.().lastElement()); - } - @computed get anchor2() { - const anchor2 = DocCast(this.dataDoc.link_anchor_2); - const anchor_2 = anchor2?.layout_unrendered ? DocCast(anchor2.annotationOn) : anchor2; - return DocumentManager.Instance.getDocumentView(anchor_2, this.DocumentView?.().containerViewPath?.().lastElement()); - } - screenBounds = () => { - if (this.layoutDoc._layout_isSvg && this.anchor1 && this.anchor2 && this.anchor1.CollectionFreeFormView) { - const a_invXf = this.anchor1.screenToViewTransform().inverse(); - const b_invXf = this.anchor2.screenToViewTransform().inverse(); - const a_scrBds = { tl: a_invXf.transformPoint(0, 0), br: a_invXf.transformPoint(NumCast(this.anchor1.Document._width), NumCast(this.anchor1.Document._height)) }; - const b_scrBds = { tl: b_invXf.transformPoint(0, 0), br: b_invXf.transformPoint(NumCast(this.anchor2.Document._width), NumCast(this.anchor2.Document._height)) }; - - const pts = [] as number[][]; - pts.push([(a_scrBds.tl[0] + a_scrBds.br[0]) / 2, (a_scrBds.tl[1] + a_scrBds.br[1]) / 2]); - pts.push(Utils.getNearestPointInPerimeter(a_scrBds.tl[0], a_scrBds.tl[1], a_scrBds.br[0] - a_scrBds.tl[0], a_scrBds.br[1] - a_scrBds.tl[1], (b_scrBds.tl[0] + b_scrBds.br[0]) / 2, (b_scrBds.tl[1] + b_scrBds.br[1]) / 2)); - pts.push(Utils.getNearestPointInPerimeter(b_scrBds.tl[0], b_scrBds.tl[1], b_scrBds.br[0] - b_scrBds.tl[0], b_scrBds.br[1] - b_scrBds.tl[1], (a_scrBds.tl[0] + a_scrBds.br[0]) / 2, (a_scrBds.tl[1] + a_scrBds.br[1]) / 2)); - pts.push([(b_scrBds.tl[0] + b_scrBds.br[0]) / 2, (b_scrBds.tl[1] + b_scrBds.br[1]) / 2]); - const agg = aggregateBounds( - pts.map(pt => ({ x: pt[0], y: pt[1] })), - 0, - 0 - ); - return { left: agg.x, top: agg.y, right: agg.r, bottom: agg.b, center: undefined }; - } - return undefined; + anchor = (which: number) => { + const anch = DocCast(this.dataDoc['link_anchor_' + which]); + const anchor = anch?.layout_unrendered ? DocCast(anch.annotationOn) : anch; + return DocumentManager.Instance.getDocumentView(anchor, this.DocumentView?.().containerViewPath?.().lastElement()); }; - disposer: IReactionDisposer | undefined; + componentWillUnmount() { + this.disposer?.(); + } componentDidMount() { this._props.setContentViewBox?.(this); this.disposer = reaction( - () => { - if (this.layoutDoc._layout_isSvg && (this.anchor1 || this.anchor2)?.CollectionFreeFormView) { - const a = (this.anchor1 ?? this.anchor2)!; - const b = (this.anchor2 ?? this.anchor1)!; - - const parxf = this.DocumentView?.().containerViewPath?.().lastElement().ComponentView as CollectionFreeFormView; - const this_xf = parxf?.screenToFreeformContentsXf ?? Transform.Identity; //this.ScreenToLocalTransform(); - const a_invXf = a.screenToViewTransform().inverse(); - const b_invXf = b.screenToViewTransform().inverse(); - const a_scrBds = { tl: a_invXf.transformPoint(0, 0), br: a_invXf.transformPoint(NumCast(a.Document._width), NumCast(a.Document._height)) }; - const b_scrBds = { tl: b_invXf.transformPoint(0, 0), br: b_invXf.transformPoint(NumCast(b.Document._width), NumCast(b.Document._height)) }; - const a_bds = { tl: this_xf.transformPoint(a_scrBds.tl[0], a_scrBds.tl[1]), br: this_xf.transformPoint(a_scrBds.br[0], a_scrBds.br[1]) }; - const b_bds = { tl: this_xf.transformPoint(b_scrBds.tl[0], b_scrBds.tl[1]), br: this_xf.transformPoint(b_scrBds.br[0], b_scrBds.br[1]) }; - - const ppt1 = [(a_bds.tl[0] + a_bds.br[0]) / 2, (a_bds.tl[1] + a_bds.br[1]) / 2]; - const pt1 = Utils.getNearestPointInPerimeter(a_bds.tl[0], a_bds.tl[1], a_bds.br[0] - a_bds.tl[0], a_bds.br[1] - a_bds.tl[1], (b_bds.tl[0] + b_bds.br[0]) / 2, (b_bds.tl[1] + b_bds.br[1]) / 2); - const pt2 = Utils.getNearestPointInPerimeter(b_bds.tl[0], b_bds.tl[1], b_bds.br[0] - b_bds.tl[0], b_bds.br[1] - b_bds.tl[1], (a_bds.tl[0] + a_bds.br[0]) / 2, (a_bds.tl[1] + a_bds.br[1]) / 2); - const ppt2 = [(b_bds.tl[0] + b_bds.br[0]) / 2, (b_bds.tl[1] + b_bds.br[1]) / 2]; - - const pts = [ppt1, pt1, pt2, ppt2].map(pt => [pt[0], pt[1]]); - const [lx, rx, ty, by] = [Math.min(pt1[0], pt2[0]), Math.max(pt1[0], pt2[0]), Math.min(pt1[1], pt2[1]), Math.max(pt1[1], pt2[1])]; - return { pts, lx, rx, ty, by }; - } - return undefined; - }, - params => { - this.renderProps = params; - if (params) { - if ( - Math.abs(params.lx - NumCast(this.layoutDoc.x)) > 1e-5 || - Math.abs(params.ty - NumCast(this.layoutDoc.y)) > 1e-5 || - Math.abs(params.rx - params.lx - NumCast(this.layoutDoc._width)) > 1e-5 || - Math.abs(params.by - params.ty - NumCast(this.layoutDoc._height)) > 1e-5 - ) { - this.layoutDoc.x = params?.lx; - this.layoutDoc.y = params?.ty; - this.layoutDoc._width = params.rx - params?.lx; - this.layoutDoc._height = params?.by - params?.ty; - } - } else { - this.layoutDoc._width = Math.max(50, NumCast(this.layoutDoc._width)); - this.layoutDoc._height = Math.max(50, NumCast(this.layoutDoc._height)); - } + () => ({ drag: SnappingManager.IsDragging }), + ({ drag }) => { + !LightboxView.Contains(this.DocumentView?.()) && + setTimeout( + // need to wait for drag manager to set 'hidden' flag on dragged DOM elements + action(() => { + const a = this.anchor1, + b = this.anchor2; + let a1 = a && document.getElementById(a.Guid); + let a2 = b && document.getElementById(b.Guid); + // test whether the anchors themselves are hidden,... + if (!a1 || !a2 || (a?.ContentDiv as any)?.hidden || (b?.ContentDiv as any)?.hidden) this._hide = true; + else { + // .. or whether and of their DOM parents are hidden + for (; a1 && !a1.hidden; a1 = a1.parentElement); + for (; a2 && !a2.hidden; a2 = a2.parentElement); + this._hide = a1 || a2 ? true : false; + } + }) + ); }, { fireImmediately: true } ); } - componentWillUnmount(): void { - this.disposer?.(); - } - @observable renderProps: { lx: number; rx: number; ty: number; by: number; pts: number[][] } | undefined = undefined; + render() { - if (this.renderProps) { + if (this._hide) return null; + const a = this.anchor1; + const b = this.anchor2; + this._forceAnimate; + const docView = this._props.docViewPath().lastElement(); + + if (a && b && !LightboxView.Contains(docView)) { + // text selection bounds are not directly observable, so we have to + // force an update when anything that could affect them changes (text edits causing reflow, scrolling) + a.Document[a.LayoutFieldKey]; + b.Document[b.LayoutFieldKey]; + a.Document.layout_scrollTop; + b.Document.layout_scrollTop; + + const axf = a.screenToViewTransform(); // these force re-render when a or b moves (so do NOT remove) + const bxf = b.screenToViewTransform(); + const scale = docView?.screenToViewTransform().Scale ?? 1; + const at = a.getBounds?.transition; // these force re-render when a or b change size and at the end of an animated transition + const bt = b.getBounds?.transition; // inquring getBounds() also causes text anchors to update whether or not they reflow (any size change triggers an invalidation) + + // if there's an element in the DOM with a classname containing a link anchor's id (eg a hypertext <a>), + // then that DOM element is a hyperlink source for the current anchor and we want to place our link box at it's top right + // otherwise, we just use the computed nearest point on the document boundary to the target Document + const targetAhyperlink = Array.from(document.getElementsByClassName(DocCast(this.dataDoc.link_anchor_1)[Id])).lastElement(); + const targetBhyperlink = Array.from(document.getElementsByClassName(DocCast(this.dataDoc.link_anchor_2)[Id])).lastElement(); + + const aid = targetAhyperlink?.id || a.Document[Id]; + const bid = targetBhyperlink?.id || b.Document[Id]; + if (!document.getElementById(aid) || !document.getElementById(bid)) { + setTimeout(action(() => (this._forceAnimate = this._forceAnimate + 0.01))); + return null; + } + + if (at || bt) setTimeout(action(() => (this._forceAnimate = this._forceAnimate + 0.01))); // this forces an update during a transition animation const highlight = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Highlighting); const highlightColor = highlight?.highlightIndex ? highlight?.highlightColor : undefined; + const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color); + const fontFamily = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontFamily); + const fontSize = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontSize); + const fontColor = (c => (c !== 'transparent' ? c : undefined))(StrCast(this.layoutDoc.link_fontColor)); + const { stroke_markerScale, stroke_width, stroke_startMarker, stroke_endMarker, stroke_dash } = this.Document; - const bez = new Bezier(this.renderProps.pts.map(p => ({ x: p[0], y: p[1] }))); - const text = bez.get(0.5); - const linkDesc = StrCast(this.dataDoc.link_description) || 'description'; - const strokeWidth = NumCast(this.dataDoc.stroke_width, 4); - const dash = StrCast(this.Document.stroke_dash); - const strokeDasharray = dash && Number(dash) ? String(strokeWidth * Number(dash)) : undefined; - const { pts, lx, ty, rx, by } = this.renderProps; + const strokeWidth = NumCast(stroke_width, 4); + const linkDesc = StrCast(this.dataDoc.link_description) || ' '; + const labelText = linkDesc.substring(0, 50) + (linkDesc.length > 50 ? '...' : ''); return ( - <div style={{ transition: 'inherit', pointerEvents: 'none', position: 'absolute', width: '100%', height: '100%' }}> - <svg width={Math.max(100, rx - lx)} height={Math.max(100, by - ty)} style={{ transition: 'inherit', overflow: 'visible' }}> - <defs> - <filter x="0" y="0" width="1" height="1" id={`${this.Document[Id] + 'background'}`}> - <feFlood floodColor={`${StrCast(this.layoutDoc._backgroundColor, 'lightblue')}`} result="bg" /> - <feMerge> - <feMergeNode in="bg" /> - <feMergeNode in="SourceGraphic" /> - </feMerge> - </filter> - </defs> - <path - className="collectionfreeformlinkview-linkLine" - style={{ - pointerEvents: this._props.pointerEvents?.() === 'none' ? 'none' : 'visibleStroke', // - stroke: highlightColor ?? 'lightblue', - strokeDasharray, - strokeWidth, - transition: 'inherit', - }} - d={`M ${pts[1][0] - lx} ${pts[1][1] - ty} C ${pts[1][0] + pts[1][0] - pts[0][0] - lx} ${pts[1][1] + pts[1][1] - pts[0][1] - ty}, - ${pts[2][0] + pts[2][0] - pts[3][0] - lx} ${pts[2][1] + pts[2][1] - pts[3][1] - ty}, ${pts[2][0] - lx} ${pts[2][1] - ty}`} + <> + {!highlightColor ? null : ( + <Xarrow + divContainerStyle={{ transform: `scale(${scale})` }} + start={aid} + end={bid} // + strokeWidth={strokeWidth + Math.max(2, strokeWidth * 0.1)} + showHead={stroke_startMarker ? true : false} + showTail={stroke_endMarker ? true : false} + headSize={NumCast(stroke_markerScale, 3)} + tailSize={NumCast(stroke_markerScale, 3)} + tailShape={stroke_endMarker === 'dot' ? 'circle' : 'arrow1'} + headShape={stroke_startMarker === 'dot' ? 'circle' : 'arrow1'} + color={highlightColor} /> - <text - filter={`url(#${this.Document[Id] + 'background'})`} - style={{ pointerEvents: this._props.pointerEvents?.() === 'none' ? 'none' : 'all', textAnchor: 'middle', fontSize: '12', stroke: 'black' }} - x={text.x - lx} - y={text.y - ty}> - <tspan> </tspan> - <tspan dy="2">{linkDesc.substring(0, 50) + (linkDesc.length > 50 ? '...' : '')}</tspan> - <tspan dy="2"> </tspan> - </text> - </svg> - </div> + )} + <Xarrow + divContainerStyle={{ transform: `scale(${scale})` }} + start={aid} + end={bid} // + strokeWidth={strokeWidth} + dashness={Number(stroke_dash) ? true : false} + showHead={stroke_startMarker ? true : false} + showTail={stroke_endMarker ? true : false} + headSize={NumCast(stroke_markerScale, 3)} + tailSize={NumCast(stroke_markerScale, 3)} + tailShape={stroke_endMarker === 'dot' ? 'circle' : 'arrow1'} + headShape={stroke_startMarker === 'dot' ? 'circle' : 'arrow1'} + color={color} + labels={ + <div + style={{ + borderRadius: '8px', + pointerEvents: this._props.isDocumentActive?.() ? 'all' : undefined, + fontSize, + fontFamily /*, fontStyle: 'italic'*/, + color: fontColor || lightOrDark(DashColor(color).fade(0.5).toString()), + paddingLeft: 4, + paddingRight: 4, + paddingTop: 3, + paddingBottom: 3, + background: DashColor((!docView?.isSelected() && highlightColor) || color) + .fade(0.5) + .toString(), + }}> + <EditableView + key="editableView" + oneLine + contents={labelText} + height={fontSize + 4} + fontSize={fontSize} + GetValue={() => linkDesc} + SetValue={action(val => { + this.Document[DocData].link_description = val; + return true; + })} + /> + + {/* <EditableText + placeholder={labelText} + background={color} + color={fontColor || lightOrDark(DashColor(color).fade(0.5).toString())} + type={Type.PRIM} + val={StrCast(this.Document[DocData].link_description)} + setVal={action(val => (this.Document[DocData].link_description = val))} + fillWidth + /> */} + </div> + } + passProps={{}} + /> + </> ); } return ( <div className={`linkBox-container${this._props.isContentActive() ? '-interactive' : ''}`} style={{ background: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) }}> <ComparisonBox - {...this._props} // + {...this.props} // fieldKey="link_anchor" setHeight={emptyFunction} dontRegisterView={true} diff --git a/src/client/views/nodes/LinkDescriptionPopup.tsx b/src/client/views/nodes/LinkDescriptionPopup.tsx index 13f0ac4fc..1645d0813 100644 --- a/src/client/views/nodes/LinkDescriptionPopup.tsx +++ b/src/client/views/nodes/LinkDescriptionPopup.tsx @@ -1,10 +1,11 @@ -import { action, makeObservable, observable } from 'mobx'; +import { action, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { DocData } from '../../../fields/DocSymbols'; import { LinkManager } from '../../util/LinkManager'; import './LinkDescriptionPopup.scss'; import { TaskCompletionBox } from './TaskCompletedBox'; +import { StrCast } from '../../../fields/Types'; @observer export class LinkDescriptionPopup extends React.Component<{}> { @@ -31,21 +32,26 @@ export class LinkDescriptionPopup extends React.Component<{}> { onDismiss = (add: boolean) => { this.display = false; if (add) { - LinkManager.currentLink && (LinkManager.currentLink[DocData].link_description = this.description); + LinkManager.Instance.currentLink && (LinkManager.Instance.currentLink[DocData].link_description = this.description); } + this.description = ''; }; @action onClick = (e: PointerEvent) => { if (this.popupRef && !!!this.popupRef.current?.contains(e.target as any)) { this.display = false; + this.description = ''; TaskCompletionBox.taskCompleted = false; } }; - @action componentDidMount() { document.addEventListener('pointerdown', this.onClick, true); + reaction( + () => this.display, + display => display && (this.description = StrCast(LinkManager.Instance.currentLink?.link_description)) + ); } componentWillUnmount() { @@ -65,8 +71,10 @@ export class LinkDescriptionPopup extends React.Component<{}> { className="linkDescriptionPopup-input" onKeyDown={e => e.stopPropagation()} onKeyPress={e => e.key === 'Enter' && this.onDismiss(true)} - placeholder={'(Optional) Enter link description...'} - onChange={e => this.descriptionChanged(e)}></input> + value={this.description} + placeholder={this.description || '(Optional) Enter link description...'} + onChange={e => this.descriptionChanged(e)} + /> <div className="linkDescriptionPopup-btn"> <div className="linkDescriptionPopup-btn-dismiss" onPointerDown={e => this.onDismiss(false)}> {' '} diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx index 4b242649a..ae25ff179 100644 --- a/src/client/views/nodes/LinkDocPreview.tsx +++ b/src/client/views/nodes/LinkDocPreview.tsx @@ -152,8 +152,8 @@ export class LinkDocPreview extends ObservableReactComponent<LinkDocPreviewProps returnFalse, emptyFunction, action(() => { - LinkManager.currentLink = this._linkDoc; - LinkManager.currentLinkAnchor = this._linkSrc; + LinkManager.Instance.currentLink = this._linkDoc; + LinkManager.Instance.currentLinkAnchor = this._linkSrc; this._props.DocumentView?.().select(false); if ((SettingsManager.Instance.propertiesWidth ?? 0) < 100) { SettingsManager.Instance.propertiesWidth = 250; @@ -184,7 +184,7 @@ export class LinkDocPreview extends ObservableReactComponent<LinkDocPreviewProps LinkFollower.FollowLink(this._linkDoc, this._linkSrc, false); } else if (this._props.hrefs?.length) { const webDoc = - Array.from(SearchUtil.SearchCollection(Doc.MyFilesystem, this._props.hrefs[0]).keys()).lastElement() ?? + Array.from(SearchUtil.SearchCollection(Doc.MyFilesystem, this._props.hrefs[0], false).keys()).lastElement() ?? Docs.Create.WebDocument(this._props.hrefs[0], { title: this._props.hrefs[0], _nativeWidth: 850, _width: 200, _height: 400, data_useCors: true }); DocumentManager.Instance.showDocument(webDoc, { openLocation: OpenWhere.lightbox, diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 5a07540da..2c5398e40 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -14,7 +14,7 @@ import { listSpec } from '../../../fields/Schema'; import { Cast, NumCast, StrCast, WebCast } from '../../../fields/Types'; import { ImageField, WebField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; -import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, getWordAtPoint, lightOrDark, returnFalse, returnOne, returnZero, setupMoveUpEvents, smoothScroll, Utils } from '../../../Utils'; +import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, getWordAtPoint, lightOrDark, returnFalse, returnOne, returnZero, setupMoveUpEvents, smoothScroll, stringHash, Utils } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentManager } from '../../util/DocumentManager'; import { ScriptingGlobals } from '../../util/ScriptingGlobals'; @@ -83,7 +83,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem return this.webField?.toString() || ''; } @computed get _urlHash() { - return this._url ? WebBox.urlHash(this._url) + '' : ''; + return ""+ (stringHash(this._url)??''); } @computed get scrollHeight() { return Math.max(NumCast(this.layoutDoc._height), this._scrollHeight); @@ -624,15 +624,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem return false; }; - static urlHash = (s: string) => { - const split = s.split(''); - return Math.abs( - split.reduce((a: any, b: any) => { - a = (a << 5) - a + b.charCodeAt(0); - return a & a; - }, 0) - ); - }; @action submitURL = (preview?: boolean, dontUpdateIframe?: boolean) => { try { @@ -1154,5 +1145,5 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implem } } ScriptingGlobals.add(function urlHash(url: string) { - return url ? WebBox.urlHash(url) : 0; + return stringHash(url); }); diff --git a/src/client/views/nodes/formattedText/DashDocCommentView.tsx b/src/client/views/nodes/formattedText/DashDocCommentView.tsx index b7d2a24c2..a72ed1813 100644 --- a/src/client/views/nodes/formattedText/DashDocCommentView.tsx +++ b/src/client/views/nodes/formattedText/DashDocCommentView.tsx @@ -3,6 +3,8 @@ import * as ReactDOM from 'react-dom/client'; import { Doc } from '../../../../fields/Doc'; import { DocServer } from '../../../DocServer'; import * as React from 'react'; +import { IReactionDisposer, computed, reaction } from 'mobx'; +import { NumCast } from '../../../../fields/Types'; // creates an inline comment in a note when '>>' is typed. // the comment sits on the right side of the note and vertically aligns with its anchor in the text. @@ -10,8 +12,10 @@ import * as React from 'react'; export class DashDocCommentView { dom: HTMLDivElement; // container for label and value root: any; + node: any; constructor(node: any, view: any, getPos: any) { + this.node = node; this.dom = document.createElement('div'); this.dom.style.width = node.attrs.width; this.dom.style.height = node.attrs.height; @@ -32,10 +36,14 @@ export class DashDocCommentView { }; this.root = ReactDOM.createRoot(this.dom); - this.root.render(<DashDocCommentViewInternal view={view} getPos={getPos} docId={node.attrs.docId} />); + this.root.render(<DashDocCommentViewInternal view={view} getPos={getPos} setHeight={this.setHeight} docId={node.attrs.docId} />); (this as any).dom = this.dom; } + setHeight = (hgt: number) => { + !this.node.attrs.reflow && DocServer.GetRefField(this.node.attrs.docId).then(doc => doc instanceof Doc && (this.dom.style.height = hgt + '')); + }; + destroy() { this.root.unmount(); } @@ -51,9 +59,15 @@ interface IDashDocCommentViewInternal { docId: string; view: any; getPos: any; + setHeight: (height: number) => void; } export class DashDocCommentViewInternal extends React.Component<IDashDocCommentViewInternal> { + _reactionDisposer: IReactionDisposer | undefined; + + @computed get _dashDoc() { + return DocServer.GetRefField(this.props.docId); + } constructor(props: any) { super(props); this.onPointerLeaveCollapsed = this.onPointerLeaveCollapsed.bind(this); @@ -61,15 +75,32 @@ export class DashDocCommentViewInternal extends React.Component<IDashDocCommentV this.onPointerUpCollapsed = this.onPointerUpCollapsed.bind(this); this.onPointerDownCollapsed = this.onPointerDownCollapsed.bind(this); } + componentDidMount(): void { + this._reactionDisposer?.(); + this._dashDoc.then( + doc => + doc instanceof Doc && + (this._reactionDisposer = reaction( + () => NumCast((doc as Doc)._height), + hgt => this.props.setHeight(hgt), + { + fireImmediately: true, + } + )) + ); + } + componentWillUnmount(): void { + this._reactionDisposer?.(); + } onPointerLeaveCollapsed(e: any) { - DocServer.GetRefField(this.props.docId).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowUnhighlight()); + this._dashDoc.then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowUnhighlight()); e.preventDefault(); e.stopPropagation(); } onPointerEnterCollapsed(e: any) { - DocServer.GetRefField(this.props.docId).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc, false)); + this._dashDoc.then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc, false)); e.preventDefault(); e.stopPropagation(); } @@ -82,7 +113,7 @@ export class DashDocCommentViewInternal extends React.Component<IDashDocCommentV const tr = this.props.view.state.tr.setNodeMarkup(target.pos, undefined, { ...target.node.attrs, hidden: target.node.attrs.hidden ? false : true }); this.props.view.dispatch(tr.setSelection(TextSelection.create(tr.doc, this.props.getPos() + (expand ? 2 : 1)))); // update the attrs setTimeout(() => { - expand && DocServer.GetRefField(this.props.docId).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc)); + expand && this._dashDoc.then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc)); try { this.props.view.dispatch(this.props.view.state.tr.setSelection(TextSelection.create(this.props.view.state.tr.doc, this.props.getPos() + (expand ? 2 : 1)))); } catch (e) {} diff --git a/src/client/views/nodes/formattedText/DashFieldView.scss b/src/client/views/nodes/formattedText/DashFieldView.scss index 3426ba1a7..7a0ff8776 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.scss +++ b/src/client/views/nodes/formattedText/DashFieldView.scss @@ -5,6 +5,16 @@ display: inline-flex; align-items: center; + select { + display: none; + } + + &:hover { + select { + display: unset; + } + } + .dashFieldView-enumerables { width: 10px; height: 10px; diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx index dc9914637..b49e7dcf0 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.tsx +++ b/src/client/views/nodes/formattedText/DashFieldView.tsx @@ -1,24 +1,27 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; -import { action, computed, IReactionDisposer, makeObservable, observable } from 'mobx'; +import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import * as ReactDOM from 'react-dom/client'; -import { Doc } from '../../../../fields/Doc'; +import Select from 'react-select'; +import { Doc, DocListCast, Field } from '../../../../fields/Doc'; import { List } from '../../../../fields/List'; import { listSpec } from '../../../../fields/Schema'; import { SchemaHeaderField } from '../../../../fields/SchemaHeaderField'; -import { Cast } from '../../../../fields/Types'; +import { Cast, StrCast } from '../../../../fields/Types'; import { emptyFunction, returnFalse, returnZero, setupMoveUpEvents } from '../../../../Utils'; import { DocServer } from '../../../DocServer'; import { CollectionViewType } from '../../../documents/DocumentTypes'; import { Transform } from '../../../util/Transform'; +import { undoable, undoBatch } from '../../../util/UndoManager'; import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu'; import { SchemaTableCell } from '../../collections/collectionSchema/SchemaTableCell'; import { ObservableReactComponent } from '../../ObservableReactComponent'; import { OpenWhere } from '../DocumentView'; import './DashFieldView.scss'; import { FormattedTextBox } from './FormattedTextBox'; +import { FilterPanel } from '../../FilterPanel'; export class DashFieldView { dom: HTMLDivElement; // container for label and value @@ -97,7 +100,7 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi _reactionDisposer: IReactionDisposer | undefined; _textBoxDoc: Doc; _fieldKey: string; - _fieldStringRef = React.createRef<HTMLSpanElement>(); + _fieldRef = React.createRef<HTMLDivElement>(); @observable _dashDoc: Doc | undefined = undefined; @observable _expanded = false; @@ -114,6 +117,13 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi } } + componentDidMount() { + this._reactionDisposer = reaction( + () => (this._dashDoc ? Field.toKeyValueString(this._dashDoc, this._props.fieldKey) : undefined), + keyvalue => keyvalue && this._props.tbox.tryUpdateDoc(true) + ); + } + componentWillUnmount() { this._reactionDisposer?.(); } @@ -173,10 +183,22 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi }); }; + @undoBatch + selectVal = (event: React.ChangeEvent<HTMLSelectElement> | undefined) => { + event && this._dashDoc && (this._dashDoc[this._fieldKey] = event.target.value); + }; + + @computed get values() { + const vals = FilterPanel.gatherFieldValues(DocListCast(Doc.ActiveDashboard?.data), this._fieldKey, []); + + return vals.strings.map(facet => ({ value: facet, label: facet })); + } + render() { return ( <div className="dashFieldView" + ref={this._fieldRef} style={{ width: this._props.width, height: this._props.height, @@ -184,11 +206,17 @@ export class DashFieldViewInternal extends ObservableReactComponent<IDashFieldVi }}> {this._props.hideKey ? null : ( <span className="dashFieldView-labelSpan" title="click to see related tags" onPointerDown={this.onPointerDownLabelSpan}> - {this._fieldKey} + {(this._textBoxDoc === this._dashDoc ? '' : this._dashDoc?.title + ':') + this._fieldKey} </span> )} - {this._props.fieldKey.startsWith('#') ? null : this.fieldValueContent} + {!this.values.length ? null : ( + <select onChange={this.selectVal} style={{ height: '10px', width: '15px', fontSize: '12px', background: 'transparent' }}> + {this.values.map(val => ( + <option value={val.value}>{val.label}</option> + ))} + </select> + )} </div> ); } diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 460f2a1c6..40acf164f 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -13,7 +13,7 @@ import { EditorView } from 'prosemirror-view'; import * as React from 'react'; import { BsMarkdownFill } from 'react-icons/bs'; import { DateField } from '../../../../fields/DateField'; -import { Doc, DocListCast, Field, Opt } from '../../../../fields/Doc'; +import { Doc, DocListCast, Field, Opt, StrListCast } from '../../../../fields/Doc'; import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DocCss, DocData, ForceServerWrite, UpdatingFromServer } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; import { InkTool } from '../../../../fields/InkField'; @@ -68,6 +68,7 @@ import { RichTextRules } from './RichTextRules'; import { schema } from './schema_rts'; import { SummaryView } from './SummaryView'; import { isDarkMode } from '../../../util/reportManager/reportManagerUtils'; +import Select from 'react-select'; // import * as applyDevTools from 'prosemirror-dev-tools'; @observer export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps>() implements ViewBoxInterface { @@ -326,13 +327,26 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } catch (e) {} }; + leafText = (node: Node) => { + if (node.type === this._editorView?.state.schema.nodes.dashField) { + const refDoc = !node.attrs.docId ? this.Document : (DocServer.GetCachedRefField(node.attrs.docId as string) as Doc); + return Field.toJavascriptString(refDoc[node.attrs.fieldKey as string] as Field); + } + return ''; + }; dispatchTransaction = (tx: Transaction) => { if (this._editorView && (this._editorView as any).docView) { const state = this._editorView.state.apply(tx); this._editorView.updateState(state); + this.tryUpdateDoc(false); + } + }; + tryUpdateDoc = (force: boolean) => { + if (this._editorView && (this._editorView as any).docView) { + const state = this._editorView.state; const dataDoc = Doc.IsDelegateField(DocCast(this.layoutDoc.proto), this.fieldKey) ? DocCast(this.layoutDoc.proto) : this.dataDoc; - const newText = state.doc.textBetween(0, state.doc.content.size, ' \n'); + const newText = state.doc.textBetween(0, state.doc.content.size, ' \n', this.leafText); const newJson = JSON.stringify(state.toJSON()); const prevData = Cast(this.layoutDoc[this.fieldKey], RichTextField, null); // the actual text in the text box const templateData = this.Document !== this.layoutDoc ? prevData : undefined; // the default text stored in a layout template @@ -348,16 +362,18 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps accumTags.push(node.attrs.fieldKey); } }); - dataDoc.tags = accumTags.length ? new List<string>(Array.from(new Set<string>(accumTags))) : undefined; + if (accumTags.some(atag => !StrListCast(dataDoc.tags).includes(atag))) { + dataDoc.tags = new List<string>(Array.from(new Set<string>(accumTags))); + } let unchanged = true; - if (this._applyingChange !== this.fieldKey && removeSelection(newJson) !== removeSelection(prevData?.Data)) { + if (this._applyingChange !== this.fieldKey && (force || removeSelection(newJson) !== removeSelection(prevData?.Data))) { this._applyingChange = this.fieldKey; const textChange = newText !== prevData?.Text; textChange && (dataDoc[this.fieldKey + '_modificationDate'] = new DateField(new Date(Date.now()))); - if ((!prevData && !protoData) || newText || (!newText && !templateData)) { + if ((!prevData && !protoData) || newText || (!newText && !protoData)) { // if no template, or there's text that didn't come from the layout template, write it to the document. (if this is driven by a template, then this overwrites the template text which is intended) - if ((this._finishingLink || this._props.isContentActive() || this._inDrop) && removeSelection(newJson) !== removeSelection(prevData?.Data)) { + if (force || ((this._finishingLink || this._props.isContentActive() || this._inDrop) && removeSelection(newJson) !== removeSelection(prevData?.Data))) { const numstring = NumCast(dataDoc[this.fieldKey], null); dataDoc[this.fieldKey] = numstring !== undefined ? Number(newText) : new RichTextField(newJson, newText); dataDoc[this.fieldKey + '_noTemplate'] = true; // mark the data field as being split from the template if it has been edited @@ -474,14 +490,29 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps }; // creates links between terms in a document and published documents (myPublishedDocs) that have titles starting with an '@' + /** + * Searches the text for occurences of any strings that match the names of 'published' documents. These document + * names will begin with an '@' prefix. However, valid matches within the text can have any of the following formats: + * name, @<name>, or ^@<name> + * The last of these is interpreted as an include directive when converting the text into evaluated code in the paint + * function of a freeform view that is driven by the text box's text. The include directive will copy the code of the published + * document into the code being evaluated. + */ hyperlinkTerm = (tr: any, target: Doc, newAutoLinks: Set<Doc>) => { const editorView = this._editorView; if (editorView && (editorView as any).docView && !Doc.AreProtosEqual(target, this.Document)) { const autoLinkTerm = StrCast(target.title).replace(/^@/, ''); var alink: Doc | undefined; this.findInNode(editorView, editorView.state.doc, autoLinkTerm).forEach(sel => { - const splitter = editorView.state.schema.marks.splitter.create({ id: Utils.GenerateGuid() }); - if (!sel.$anchor.pos || editorView.state.doc.textBetween(sel.$anchor.pos - 1, sel.$to.pos).trim() === autoLinkTerm) { + if ( + !sel.$anchor.pos || + autoLinkTerm === + editorView.state.doc + .textBetween(sel.$anchor.pos - 1, sel.$to.pos) + .trim() + .replace(/[\^@]+/, '') + ) { + const splitter = editorView.state.schema.marks.splitter.create({ id: Utils.GenerateGuid() }); tr = tr.addMark(sel.from, sel.to, splitter); tr.doc.nodesBetween(sel.from, sel.to, (node: any, pos: number, parent: any) => { if (node.firstChild === null && !node.marks.find((m: Mark) => m.type.name === schema.marks.noAutoLinkAnchor.name) && node.marks.find((m: Mark) => m.type.name === schema.marks.splitter.name)) { @@ -654,12 +685,22 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps let index = 0, foundAt; const ep = this.getNodeEndpoints(pm.state.doc, node); - const regexp = new RegExp(find.replace('*', ''), 'i'); + const regexp = new RegExp(find, 'i'); if (regexp) { - while (ep && (foundAt = node.textContent.slice(index).search(regexp)) > -1) { - const sel = new TextSelection(pm.state.doc.resolve(ep.from + index + foundAt + 1), pm.state.doc.resolve(ep.from + index + foundAt + find.length + 1)); - ret.push(sel); - index = index + foundAt + find.length; + var blockOffset = 0; + for (var i = 0; i < node.childCount; i++) { + var textContent = ''; + while (i < node.childCount && node.child(i).type === pm.state.schema.nodes.text) { + textContent += node.child(i).textContent; + i++; + } + while (ep && (foundAt = textContent.slice(index).search(regexp)) > -1) { + const sel = new TextSelection(pm.state.doc.resolve(ep.from + index + blockOffset + foundAt + 1), pm.state.doc.resolve(ep.from + index + blockOffset + foundAt + find.length + 1)); + ret.push(sel); + index = index + foundAt + find.length; + } + blockOffset += textContent.length; + if (i < node.childCount) blockOffset += node.child(i).nodeSize; } } } else { @@ -1171,8 +1212,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps this._cachedLinks = LinkManager.Links(this.Document); this._disposers.breakupDictation = reaction(() => Doc.RecordingEvent, this.breakupDictation); this._disposers.layout_autoHeight = reaction( - () => this.layout_autoHeight, - layout_autoHeight => layout_autoHeight && this.tryUpdateScrollHeight() + () => ({ autoHeight: this.layout_autoHeight, fontSize: this.fontSize }), + (autoHeight, fontSize) => setTimeout(() => autoHeight && this.tryUpdateScrollHeight()) ); this._disposers.highlights = reaction( () => Array.from(FormattedTextBox._globalHighlights).slice(), @@ -1537,15 +1578,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps this._downX = e.clientX; this._downY = e.clientY; this._downTime = Date.now(); - this._hadDownFocus = this.ProseRef?.children[0].className.includes('focused') ?? false; FormattedTextBoxComment.textBox = this; if (e.button === 0 && this._props.rootSelected?.() && !e.altKey && !e.ctrlKey && !e.metaKey) { if (e.clientX < this.ProseRef!.getBoundingClientRect().right) { // stop propagation if not in sidebar, otherwise nested boxes will lose focus to outer boxes. e.stopPropagation(); // if the text box's content is active, then it consumes all down events document.addEventListener('pointerup', this.onSelectEnd); + (this.ProseRef?.children?.[0] as any).focus(); } } + this._hadDownFocus = this.ProseRef?.children[0].className.includes('focused') ?? false; if (e.button === 2 || (e.button === 0 && e.ctrlKey)) { e.preventDefault(); } @@ -1706,7 +1748,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps FormattedTextBox.LiveTextUndo = undefined; const state = this._editorView!.state; - const curText = state.doc.textBetween(0, state.doc.content.size, ' \n'); if (StrCast(this.Document.title).startsWith('@') && !this.dataDoc.title_custom) { UndoManager.RunInBatch(() => { this.dataDoc.title_custom = true; @@ -2002,7 +2043,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } render() { TraceMobx(); - const scale = (this._props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._freeform_scale, 1); + const scale = this._props.NativeDimScaling?.() || 1; // * NumCast(this.layoutDoc._freeform_scale, 1); const rounded = StrCast(this.layoutDoc.layout_borderRounding) === '100%' ? '-rounded' : ''; setTimeout(() => !this._props.isContentActive() && FormattedTextBoxComment.textBox === this && FormattedTextBoxComment.Hide); const paddingX = NumCast(this.layoutDoc._xMargin, this._props.xPadding || 0); diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts index 08bad2d57..47527847b 100644 --- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts +++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts @@ -303,7 +303,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks()); const cr = state.selection.$from.node().textContent.endsWith('\n'); - if (cr || !newlineInCode(state, dispatch as any)) { + if (/*cr ||*/ !newlineInCode(state, dispatch as any)) { if ( !splitListItem(schema.nodes.list_item)(state as any, (tx2: Transaction) => { const tx3 = updateBullets(tx2, schema); diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index 5858c3b11..cd0cdaa74 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -141,12 +141,14 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { const activeSizes = active.activeSizes; const activeColors = active.activeColors; const activeHighlights = active.activeHighlights; + const refDoc = SelectionManager.Views.lastElement()?.layoutDoc ?? Doc.UserDoc(); + const refField = (pfx => (pfx ? pfx + '_' : ''))(SelectionManager.Views.lastElement()?.LayoutFieldKey); this.activeListType = this.getActiveListStyle(); this._activeAlignment = this.getActiveAlignment(); - this._activeFontFamily = !activeFamilies.length ? StrCast(this.TextView?.Document._text_fontFamily, StrCast(Doc.UserDoc().fontFamily, 'Arial')) : activeFamilies.length === 1 ? String(activeFamilies[0]) : 'various'; - this._activeFontSize = !activeSizes.length ? StrCast(this.TextView?.Document.fontSize, StrCast(Doc.UserDoc().fontSize, '10px')) : activeSizes[0]; - this._activeFontColor = !activeColors.length ? StrCast(this.TextView?.Document.fontColor, StrCast(Doc.UserDoc().fontColor, 'black')) : activeColors.length > 0 ? String(activeColors[0]) : '...'; + this._activeFontFamily = !activeFamilies.length ? StrCast(this.TextView?.Document._text_fontFamily, StrCast(refDoc[refField + 'fontFamily'], 'Arial')) : activeFamilies.length === 1 ? String(activeFamilies[0]) : 'various'; + this._activeFontSize = !activeSizes.length ? StrCast(this.TextView?.Document.fontSize, StrCast(refDoc[refField + 'fontSize'], '10px')) : activeSizes[0]; + this._activeFontColor = !activeColors.length ? StrCast(this.TextView?.Document.fontColor, StrCast(refDoc[refField + 'fontColor'], 'black')) : activeColors.length > 0 ? String(activeColors[0]) : '...'; this._activeHighlightColor = !activeHighlights.length ? '' : activeHighlights.length > 0 ? String(activeHighlights[0]) : '...'; // update link in current selection @@ -358,8 +360,8 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { this.setMark(fmark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(fmark)), true); this.view.focus(); } - } else if (SelectionManager.Views.some(dv => dv.ComponentView instanceof EquationBox)) { - SelectionManager.Views.forEach(dv => (dv.Document._text_fontSize = fontSize)); + } else if (SelectionManager.Views.length) { + SelectionManager.Views.forEach(dv => (dv.layoutDoc[dv.LayoutFieldKey + '_fontSize'] = fontSize)); } else Doc.UserDoc().fontSize = fontSize; this.updateMenu(this.view, undefined, this.props, this.layoutDoc); }; @@ -369,6 +371,8 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { const fmark = this.view.state.schema.marks.pFontFamily.create({ family: family }); this.setMark(fmark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(fmark)), true); this.view.focus(); + } else if (SelectionManager.Views.length) { + SelectionManager.Views.forEach(dv => (dv.layoutDoc[dv.LayoutFieldKey + '_fontFamily'] = family)); } else Doc.UserDoc().fontFamily = family; this.updateMenu(this.view, undefined, this.props, this.layoutDoc); }; @@ -387,6 +391,8 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { const colorMark = this.view.state.schema.mark(this.view.state.schema.marks.pFontColor, { color }); this.setMark(colorMark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(colorMark)), true); this.view.focus(); + } else if (SelectionManager.Views.length) { + SelectionManager.Views.forEach(dv => (dv.layoutDoc[dv.LayoutFieldKey + '_fontColor'] = color)); } else Doc.UserDoc().fontColor = color; this.updateMenu(this.view, undefined, this.props, this.layoutDoc); } diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts index 456ed4732..9bd41f42c 100644 --- a/src/client/views/nodes/formattedText/RichTextRules.ts +++ b/src/client/views/nodes/formattedText/RichTextRules.ts @@ -4,8 +4,7 @@ import { Doc, StrListCast } from '../../../../fields/Doc'; import { DocData } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; import { List } from '../../../../fields/List'; -import { ComputedField } from '../../../../fields/ScriptField'; -import { NumCast } from '../../../../fields/Types'; +import { NumCast, StrCast } from '../../../../fields/Types'; import { Utils } from '../../../../Utils'; import { DocServer } from '../../../DocServer'; import { Docs, DocUtils } from '../../../documents/Documents'; @@ -13,6 +12,9 @@ import { FormattedTextBox } from './FormattedTextBox'; import { wrappingInputRule } from './prosemirrorPatches'; import { RichTextMenu } from './RichTextMenu'; import { schema } from './schema_rts'; +import { CollectionView } from '../../collections/CollectionView'; +import { CollectionViewType } from '../../../documents/DocumentTypes'; +import { ContextMenu } from '../../ContextMenu'; export class RichTextRules { public Document: Doc; @@ -66,7 +68,21 @@ export class RichTextRules { ), // ``` create code block - textblockTypeInputRule(/^```$/, schema.nodes.code_block), + new InputRule(/^```$/, (state, match, start, end) => { + let $start = state.doc.resolve(start); + if (!$start.node(-1).canReplaceWith($start.index(-1), $start.indexAfter(-1), schema.nodes.code_block)) return null; + + // this enables text with code blocks to be used as a 'paint' function via a styleprovider button that is added to Docs that have an onPaint script + this.TextBox.layoutDoc.type_collection = CollectionViewType.Freeform; // make it a freeform when rendered as a collection since those are the only views that know about the paint function + const paintedField = 'layout_' + this.TextBox.fieldKey + 'Painted'; // make a layout field key for storing the CollectionView jsx string pointing to the textbox's text + this.TextBox.dataDoc[paintedField] = CollectionView.LayoutString(this.TextBox.fieldKey); + const layoutFieldKey = StrCast(this.TextBox.layoutDoc.layout_fieldKey); // save the current layout fieldkey + this.TextBox.layoutDoc.layout_fieldKey = paintedField; // setup the paint layout field key + this.TextBox.DocumentView?.().setToggleDetail(layoutFieldKey.replace('layout_', '').replace('layout', ''), 'onPaint'); // create the script to toggle between the painted and regular view + this.TextBox.layoutDoc.layout_fieldKey = layoutFieldKey; // restore the layout field key to text + + return state.tr.delete(start, end).setBlockType(start, start, schema.nodes.code_block); + }), // %<font-size> set the font size new InputRule(new RegExp(/%([0-9]+)\s$/), (state, match, start, end) => { @@ -75,6 +91,32 @@ export class RichTextRules { }), //Create annotation to a field on the text document + new InputRule(new RegExp(/>::$/), (state, match, start, end) => { + const creator = (doc: Doc) => { + const textDoc = this.Document[DocData]; + const numInlines = NumCast(textDoc.inlineTextCount); + textDoc.inlineTextCount = numInlines + 1; + const node = (state.doc.resolve(start) as any).nodeAfter; + const newNode = schema.nodes.dashComment.create({ docId: doc[Id], reflow: false }); + const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 35, title: 'dashDoc', docId: doc[Id], float: 'right' }); + const sm = state.storedMarks || undefined; + this.TextBox.EditorView?.dispatch( + node + ? this.TextBox.EditorView.state.tr + .insert(start, newNode) + .replaceRangeWith(start + 1, end + 2, dashDoc) + .insertText(' ', start + 2) + .setStoredMarks([...node.marks, ...(sm ? sm : [])]) + : this.TextBox.EditorView.state.tr + ); + }; + DocUtils.addDocumentCreatorMenuItems(creator, creator, 200, 200); + const cm = ContextMenu.Instance; + cm.displayMenu(200, 200, undefined, true); + + return null; + }), + //Create annotation to a field on the text document new InputRule(new RegExp(/>>$/), (state, match, start, end) => { const textDoc = this.Document[DocData]; const numInlines = NumCast(textDoc.inlineTextCount); @@ -98,7 +140,7 @@ export class RichTextRules { textDoc[inlineLayoutKey] = FormattedTextBox.LayoutString(inlineFieldKey); // create a layout string for the layout key that will render the annotation text textDoc[inlineFieldKey] = ''; // set a default value for the annotation const node = (state.doc.resolve(start) as any).nodeAfter; - const newNode = schema.nodes.dashComment.create({ docId: textDocInline[Id] }); + const newNode = schema.nodes.dashComment.create({ docId: textDocInline[Id], reflow: true }); const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 35, title: 'dashDoc', docId: textDocInline[Id], float: 'right' }); const sm = state.storedMarks || undefined; const replaced = node @@ -222,7 +264,7 @@ export class RichTextRules { return null; }), - // stop using active style + // toggle alternate text UI %/ new InputRule(new RegExp(/%\//), (state, match, start, end) => { setTimeout(this.TextBox.cycleAlternateText); return state.tr.deleteRange(start, end); @@ -246,49 +288,57 @@ export class RichTextRules { // [[fieldKey]] => show field // [[fieldKey=value]] => show field and also set its value // [[fieldKey:docTitle]] => show field of doc - new InputRule(new RegExp(/\[\[([a-zA-Z_\? \-0-9]*)(=[a-zA-Z_@\? /\-0-9]*)?(:[a-zA-Z_@:\.\? \-0-9]+)?\]\]$/), (state, match, start, end) => { - const fieldKey = match[1]; - const docTitle = match[3]?.replace(':', ''); - const value = match[2]?.substring(1); - const linkToDoc = (target: Doc) => { - const rstate = this.TextBox.EditorView?.state; - const selection = rstate?.selection.$from.pos; - if (rstate) { - this.TextBox.EditorView?.dispatch(rstate.tr.setSelection(new TextSelection(rstate.doc.resolve(start), rstate.doc.resolve(end - 3)))); - } + new InputRule( + new RegExp(/\[\[([a-zA-Z_\? \-0-9]*)(=[a-z,A-Z_@\? /\-0-9]*)?(:[a-zA-Z_@:\.\? \-0-9]+)?\]\]$/), + (state, match, start, end) => { + const fieldKey = match[1]; + const docTitle = match[3]?.replace(':', ''); + const value = match[2]?.substring(1); + const linkToDoc = (target: Doc) => { + const rstate = this.TextBox.EditorView?.state; + const selection = rstate?.selection.$from.pos; + if (rstate) { + this.TextBox.EditorView?.dispatch(rstate.tr.setSelection(new TextSelection(rstate.doc.resolve(start), rstate.doc.resolve(end - 3)))); + } - DocUtils.MakeLink(this.TextBox.getAnchor(true), target, { link_relationship: 'portal to:portal from' }); + DocUtils.MakeLink(this.TextBox.getAnchor(true), target, { link_relationship: 'portal to:portal from' }); - const fstate = this.TextBox.EditorView?.state; - if (fstate && selection) { - this.TextBox.EditorView?.dispatch(fstate.tr.setSelection(new TextSelection(fstate.doc.resolve(selection)))); - } - }; - const getTitledDoc = (docTitle: string) => { - if (!DocServer.FindDocByTitle(docTitle)) { - Doc.AddToMyPublished(Docs.Create.TextDocument('', { title: docTitle, _width: 400, _layout_autoHeight: true })); - } - const titledDoc = DocServer.FindDocByTitle(docTitle); - return titledDoc ? Doc.BestEmbedding(titledDoc) : titledDoc; - }; - if (!fieldKey) { - if (docTitle) { - const target = getTitledDoc(docTitle); - if (target) { - setTimeout(() => linkToDoc(target)); - return state.tr.deleteRange(end - 1, end).deleteRange(start, start + 3); + const fstate = this.TextBox.EditorView?.state; + if (fstate && selection) { + this.TextBox.EditorView?.dispatch(fstate.tr.setSelection(new TextSelection(fstate.doc.resolve(selection)))); } + }; + const getTitledDoc = (docTitle: string) => { + if (!DocServer.FindDocByTitle(docTitle)) { + Doc.AddToMyPublished(Docs.Create.TextDocument('', { title: docTitle, _width: 400, _layout_autoHeight: true })); + } + const titledDoc = DocServer.FindDocByTitle(docTitle); + return titledDoc ? Doc.BestEmbedding(titledDoc) : titledDoc; + }; + if (!fieldKey) { + if (docTitle) { + const target = getTitledDoc(docTitle); + if (target) { + setTimeout(() => linkToDoc(target)); + return state.tr.deleteRange(end - 1, end).deleteRange(start, start + 3); + } + } + return state.tr; } - return state.tr; - } - if (value !== '' && value !== undefined) { - const num = value.match(/^[0-9.]$/); - this.Document[DocData][fieldKey] = value === 'true' ? true : value === 'false' ? false : num ? Number(value) : value; - } - const target = getTitledDoc(docTitle); - const fieldView = state.schema.nodes.dashField.create({ fieldKey, docId: target?.[Id], hideKey: false }); - return state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))).replaceSelectionWith(fieldView, true); - }), + if (value?.includes(',')) { + const values = value.split(','); + const strs = values.some(v => !v.match(/^[-]?[0-9.]$/)); + this.Document[DocData][fieldKey] = strs ? new List<string>(values) : new List<number>(values.map(v => Number(v))); + } else if (value !== '' && value !== undefined) { + const num = value.match(/^[0-9.]$/); + this.Document[DocData][fieldKey] = value === 'true' ? true : value === 'false' ? false : num ? Number(value) : value; + } + const target = getTitledDoc(docTitle); + const fieldView = state.schema.nodes.dashField.create({ fieldKey, docId: target?.[Id], hideKey: false }); + return state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))).replaceSelectionWith(fieldView, true); + }, + { inCode: true } + ), // create a text display of a metadata field on this or another document, or create a hyperlink portal to another document // wiki:title diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts index a342285b0..a141ef041 100644 --- a/src/client/views/nodes/formattedText/marks_rts.ts +++ b/src/client/views/nodes/formattedText/marks_rts.ts @@ -1,6 +1,7 @@ import * as React from 'react'; import { DOMOutputSpec, Fragment, MarkSpec, Node, NodeSpec, Schema, Slice } from 'prosemirror-model'; import { Doc } from '../../../../fields/Doc'; +import { Utils } from '../../../../Utils'; const emDOM: DOMOutputSpec = ['em', 0]; const strongDOM: DOMOutputSpec = ['strong', 0]; @@ -44,7 +45,7 @@ export const marks: { [index: string]: MarkSpec } = { toDOM(node: any) { const targethrefs = node.attrs.allAnchors.reduce((p: string, item: { href: string; title: string; anchorId: string }) => (p ? p + ' ' + item.href : item.href), ''); const anchorids = node.attrs.allAnchors.reduce((p: string, item: { href: string; title: string; anchorId: string }) => (p ? p + ' ' + item.anchorId : item.anchorId), ''); - return ['a', { class: anchorids, 'data-targethrefs': targethrefs, /*'data-noPreview': 'true', */ 'data-linkdoc': node.attrs.linkDoc, title: node.attrs.title, style: `background: lightBlue` }, 0]; + return ['a', { id: Utils.GenerateGuid(), class: anchorids, 'data-targethrefs': targethrefs, /*'data-noPreview': 'true', */ 'data-linkdoc': node.attrs.linkDoc, title: node.attrs.title, style: `background: lightBlue` }, 0]; }, }, noAutoLinkAnchor: { @@ -104,7 +105,7 @@ export const marks: { [index: string]: MarkSpec } = { node.attrs.title, ], ] - : ['a', { class: anchorids, 'data-targethrefs': targethrefs, title: node.attrs.title, 'data-noPreview': node.attrs.noPreview, style: `text-decoration: underline; cursor: default` }, 0]; + : ['a', { id: '' + Utils.GenerateGuid(), class: anchorids, 'data-targethrefs': targethrefs, title: node.attrs.title, 'data-noPreview': node.attrs.noPreview, style: `text-decoration: underline; cursor: default` }, 0]; }, }, diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts index d023020e1..4706a97fa 100644 --- a/src/client/views/nodes/formattedText/nodes_rts.ts +++ b/src/client/views/nodes/formattedText/nodes_rts.ts @@ -2,6 +2,8 @@ import * as React from 'react'; import { DOMOutputSpec, Node, NodeSpec } from 'prosemirror-model'; import { listItem, orderedList } from 'prosemirror-schema-list'; import { ParagraphNodeSpec, toParagraphDOM, getParagraphNodeAttrs } from './ParagraphNodeSpec'; +import { DocServer } from '../../../DocServer'; +import { Doc, Field } from '../../../../fields/Doc'; const blockquoteDOM: DOMOutputSpec = ['blockquote', 0], hrDOM: DOMOutputSpec = ['hr'], @@ -177,6 +179,7 @@ export const nodes: { [index: string]: NodeSpec } = { dashComment: { attrs: { docId: { default: '' }, + reflow: { default: true }, }, inline: true, group: 'inline', @@ -264,6 +267,18 @@ export const nodes: { [index: string]: NodeSpec } = { hideKey: { default: false }, editable: { default: true }, }, + leafText: node => Field.toString((DocServer.GetCachedRefField(node.attrs.docId as string) as Doc)?.[node.attrs.fieldKey as string] as Field), + group: 'inline', + draggable: false, + toDOM(node) { + const attrs = { style: `width: ${node.attrs.width}, height: ${node.attrs.height}` }; + return ['div', { ...node.attrs, ...attrs }]; + }, + }, + + paintButton: { + inline: true, + attrs: {}, group: 'inline', draggable: false, toDOM(node) { diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx index b8b60d2a9..1b1b65e46 100644 --- a/src/client/views/nodes/trails/PresBox.tsx +++ b/src/client/views/nodes/trails/PresBox.tsx @@ -3,8 +3,8 @@ import { Tooltip } from '@mui/material'; import { action, computed, IReactionDisposer, makeObservable, observable, ObservableSet, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { Doc, DocListCast, FieldResult, NumListCast, Opt, StrListCast } from '../../../../fields/Doc'; -import { Animation, DocData } from '../../../../fields/DocSymbols'; +import { Doc, DocListCast, Field, FieldResult, NumListCast, Opt, StrListCast } from '../../../../fields/Doc'; +import { Animation, DocData, TransitionTimer } from '../../../../fields/DocSymbols'; import { Copy, Id } from '../../../../fields/FieldSymbols'; import { InkField } from '../../../../fields/InkField'; import { List } from '../../../../fields/List'; @@ -13,7 +13,7 @@ import { listSpec } from '../../../../fields/Schema'; import { ComputedField, ScriptField } from '../../../../fields/ScriptField'; import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types'; import { AudioField } from '../../../../fields/URLField'; -import { emptyFunction, emptyPath, lightOrDark, returnFalse, returnOne, setupMoveUpEvents, StopEvent } from '../../../../Utils'; +import { emptyFunction, emptyPath, lightOrDark, returnFalse, returnOne, setupMoveUpEvents, StopEvent, stringHash } from '../../../../Utils'; import { DocServer } from '../../../DocServer'; import { Docs } from '../../../documents/Documents'; import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; @@ -518,7 +518,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { bestTarget.rotation = NumCast(activeItem.config_rotation, NumCast(bestTarget.rotation)); bestTarget.width = NumCast(activeItem.config_width, NumCast(bestTarget.width)); bestTarget.height = NumCast(activeItem.config_height, NumCast(bestTarget.height)); - setTimeout(() => (bestTarget._dataTransition = undefined), transTime + 10); + bestTarget[TransitionTimer] && clearTimeout(bestTarget[TransitionTimer]); + bestTarget[TransitionTimer] = setTimeout(() => (bestTarget[TransitionTimer] = bestTarget._dataTransition = undefined), transTime + 10); changed = true; } } @@ -544,7 +545,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { else { const bestTargetData = bestTarget[DocData]; const current = bestTargetData[fkey]; - bestTargetData[fkey + '_' + Date.now()] = current instanceof ObjectField ? current[Copy]() : current; + const hash = bestTargetData[fkey] ? stringHash(Field.toString(bestTargetData[fkey] as Field)) : undefined; + if (hash) bestTargetData[fkey + '_' + hash] = current instanceof ObjectField ? current[Copy]() : current; bestTargetData[fkey] = activeItem.config_data instanceof ObjectField ? activeItem.config_data[Copy]() : activeItem.config_data; } bestTarget[fkey + '_usePath'] = activeItem.config_usePath; diff --git a/src/client/views/search/SearchBox.scss b/src/client/views/search/SearchBox.scss index 09e459f7d..94e64b952 100644 --- a/src/client/views/search/SearchBox.scss +++ b/src/client/views/search/SearchBox.scss @@ -8,6 +8,7 @@ background: none; z-index: 1000; padding: 0px; + overflow: auto; cursor: default; .searchBox-bar { diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx index 5dc4f5550..9f153e86d 100644 --- a/src/client/views/search/SearchBox.tsx +++ b/src/client/views/search/SearchBox.tsx @@ -55,9 +55,6 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { @observable _deletedDocsStatus: boolean = false; @observable _onlyEmbeddings: boolean = true; - /** - * This is the constructor for the SearchBox class. - */ constructor(props: SearchBoxProps) { super(props); makeObservable(this); @@ -69,11 +66,11 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { * the search panel, the search input box is automatically selected. This allows the user to * type in the search input box immediately, without needing clicking on it first. */ - componentDidMount = action(() => { + componentDidMount() { if (this._inputRef.current) { this._inputRef.current.focus(); } - }); + } /** * This method is called when the SearchBox component is about to be unmounted. When the user @@ -91,9 +88,11 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { * (Note: There is no longer a need to press enter to submit a search. Any update to the input * causes a search to be submitted automatically.) */ + _timeout: any = undefined; onInputChange = action((e: React.ChangeEvent<HTMLInputElement>) => { this._searchString = e.target.value; - this.submitSearch(); + this._timeout && clearTimeout(this._timeout); + this._timeout = setTimeout(() => this.submitSearch(), 300); }); /** @@ -165,12 +164,13 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { * which the first letter is capitalized. This is used when displaying the type on the * right side of each search result. */ - static formatType(type: String): String { - if (type === 'pdf') { - return 'PDF'; - } else if (type === 'image') { - return 'Img'; - } + static formatType(type: string, colType: string): String { + switch (type) { + case DocumentType.PDF : return 'PDF'; + case DocumentType.IMG : return 'Img'; + case DocumentType.RTF : return 'Rtf'; + case DocumentType.COL : return 'Col:'+colType.substring(0,3); + } // prettier-ignore return type.charAt(0).toUpperCase() + type.substring(1, 3); } @@ -186,7 +186,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { @action searchCollection(query: string) { this._selectedResult = undefined; - this._results = SearchUtil.SearchCollection(CollectionDockingView.Instance?.Document, query); + this._results = SearchUtil.SearchCollection(CollectionDockingView.Instance?.Document, query, this._docTypeString === 'keys'); this.computePageRanks(); } @@ -336,6 +336,8 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { * brushes and highlights. All search matches are cleared as well. */ resetSearch = action(() => { + this._timeout && clearTimeout(this._timeout); + this._timeout = undefined; this._results.forEach((_, doc) => { DocumentManager.Instance.getFirstDocumentView(doc)?.ComponentView?.search?.('', undefined, true); Doc.UnBrushDoc(doc); @@ -360,11 +362,11 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { */ @computed public get selectOptions() { - const selectValues = ['all', DocumentType.RTF, DocumentType.IMG, DocumentType.PDF, DocumentType.WEB, DocumentType.VID, DocumentType.AUDIO, DocumentType.COL]; + const selectValues = ['all', DocumentType.RTF, DocumentType.IMG, DocumentType.PDF, DocumentType.WEB, DocumentType.VID, DocumentType.AUDIO, DocumentType.COL, 'keys']; return selectValues.map(value => ( <option key={value} value={value}> - {SearchBox.formatType(value)} + {SearchBox.formatType(value, '')} </option> )); } @@ -390,10 +392,10 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { className += ' searchBox-results-scroll-view-result-selected'; } - const formattedType = SearchBox.formatType(StrCast(result[0].type)); + const formattedType = SearchBox.formatType(StrCast(result[0].type), StrCast(result[0].type_collection)); const title = result[0].title; - if (this._docTypeString === 'all' || this._docTypeString === result[0].type) { + if (this._docTypeString === 'keys' || this._docTypeString === 'all' || this._docTypeString === result[0].type) { validResults++; resultsJSX.push( <Tooltip key={result[0][Id]} placement={'right'} title={<div className="dash-tooltip">{title as string}</div>}> @@ -415,7 +417,9 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { }} className={className}> <div className="searchBox-result-title">{title as string}</div> - <div className="searchBox-result-type">{formattedType}</div> + <div className="searchBox-result-type" style={{ color: SettingsManager.userVariantColor }}> + {formattedType} + </div> <div className="searchBox-result-keys" style={{ color: SettingsManager.userVariantColor }}> {result[1].join(', ')} </div> @@ -436,10 +440,10 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { </select> )} <input - defaultValue={''} + defaultValue="" autoComplete="off" onChange={this.onInputChange} - onKeyPress={e => { + onKeyDown={e => { e.key === 'Enter' ? this.submitSearch() : null; e.stopPropagation(); }} diff --git a/src/fields/CursorField.ts b/src/fields/CursorField.ts index 84917ae53..870dfbeac 100644 --- a/src/fields/CursorField.ts +++ b/src/fields/CursorField.ts @@ -1,6 +1,6 @@ import { createSimpleSchema, object, serializable } from 'serializr'; import { Deserializable } from '../client/util/SerializationHelper'; -import { Copy, FieldChanged, ToScriptString, ToString } from './FieldSymbols'; +import { Copy, FieldChanged, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols'; import { ObjectField } from './ObjectField'; export type CursorPosition = { @@ -55,6 +55,9 @@ export default class CursorField extends ObjectField { return new CursorField(this.data); } + [ToJavascriptString]() { + return 'invalid'; + } [ToScriptString]() { return 'invalid'; } diff --git a/src/fields/DateField.ts b/src/fields/DateField.ts index 2ea619bd9..1e5222fb6 100644 --- a/src/fields/DateField.ts +++ b/src/fields/DateField.ts @@ -1,7 +1,7 @@ import { Deserializable } from '../client/util/SerializationHelper'; import { serializable, date } from 'serializr'; import { ObjectField } from './ObjectField'; -import { Copy, ToScriptString, ToString } from './FieldSymbols'; +import { Copy, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols'; import { scriptingGlobal, ScriptingGlobals } from '../client/util/ScriptingGlobals'; @scriptingGlobal @@ -23,6 +23,9 @@ export class DateField extends ObjectField { return `${this.date.toLocaleString()}`; } + [ToJavascriptString]() { + return `"${this.date.toISOString()}"`; + } [ToScriptString]() { return `new DateField(new Date("${this.date.toISOString()}"))`; } diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index ff416bbe7..56d50846a 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -16,9 +16,9 @@ import { DateField } from './DateField'; import { AclAdmin, AclAugment, AclEdit, AclPrivate, AclReadonly, Animation, AudioPlay, Brushed, CachedUpdates, DirectLinks, DocAcl, DocCss, DocData, DocFields, DocLayout, DocViews, FieldKeys, FieldTuples, ForceServerWrite, Height, Highlight, - Initializing, Self, SelfProxy, UpdatingFromServer, Width + Initializing, Self, SelfProxy, TransitionTimer, UpdatingFromServer, Width } from './DocSymbols'; // prettier-ignore -import { Copy, FieldChanged, HandleUpdate, Id, Parent, ToScriptString, ToString } from './FieldSymbols'; +import { Copy, FieldChanged, HandleUpdate, Id, Parent, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols'; import { InkField, InkTool } from './InkField'; import { List, ListFieldName } from './List'; import { ObjectField } from './ObjectField'; @@ -52,6 +52,28 @@ export namespace Field { default: return field?.[ToScriptString]?.() ?? 'null'; } // prettier-ignore } + export function toJavascriptString(field: Field) { + var rawjava = ''; + + switch (typeof field) { + case 'string': + case 'number': + case 'boolean':rawjava = String(field); + break; + default: rawjava = field?.[ToJavascriptString]?.() ?? ''; + } // prettier-ignore + var script = rawjava; + // this is a bit hacky, but we treat '^@' references to a published document + // as a kind of macro to include the content of those documents + Doc.MyPublishedDocs.forEach(doc => { + const regex = new RegExp(`^\\^${doc.title}\\s`, 'm'); + const sections = (Cast(doc.text, RichTextField, null)?.Text ?? '').split('--DOCDATA--'); + if (script.match(regex)) { + script = script.replace(regex, sections[0]) + (sections.length > 1 ? sections[1] : ''); + } + }); + return script; + } export function toString(field: Field) { if (typeof field === 'string' || typeof field === 'number' || typeof field === 'boolean') return String(field); return field?.[ToString]?.() || ''; @@ -287,6 +309,8 @@ export class Doc extends RefField { public [DocFields] = () => this[Self][FieldTuples]; // Object.keys(this).reduce((fields, key) => { fields[key] = this[key]; return fields; }, {} as any); public [Width] = () => NumCast(this[SelfProxy]._width); public [Height] = () => NumCast(this[SelfProxy]._height); + public [TransitionTimer]: any = undefined; + public [ToJavascriptString] = () => `idToDoc("${this[Self][Id]}")`; // what should go here? public [ToScriptString] = () => `idToDoc("${this[Self][Id]}")`; public [ToString] = () => `Doc(${GetEffectiveAcl(this[SelfProxy]) === AclPrivate ? '-inaccessible-' : this[SelfProxy].title})`; public get [DocLayout]() { return this[SelfProxy].__LAYOUT__; } // prettier-ignore @@ -367,7 +391,7 @@ export class Doc extends RefField { export namespace Doc { export function SetContainer(doc: Doc, container: Doc) { - doc.embedContainer = container; + container !== Doc.MyRecentlyClosed && (doc.embedContainer = container); } export function RunCachedUpdate(doc: Doc, field: string) { const update = doc[CachedUpdates][field]; @@ -508,12 +532,9 @@ export namespace Doc { * Removes doc from the list of Docs at listDoc[fieldKey] * @returns true if successful, false otherwise. */ - export function RemoveDocFromList(listDoc: Doc, fieldKey: string | undefined, doc: Doc) { + export function RemoveDocFromList(listDoc: Doc, fieldKey: string | undefined, doc: Doc, ignoreProto = false) { const key = fieldKey ? fieldKey : Doc.LayoutFieldKey(listDoc); - if (listDoc[key] === undefined) { - listDoc[DocData][key] = new List<Doc>(); - } - const list = Cast(listDoc[key], listSpec(Doc)); + const list = Doc.Get(listDoc, key, ignoreProto) === undefined ? (listDoc[DocData][key] = new List<Doc>()) : Cast(listDoc[key], listSpec(Doc)); if (list) { const ind = list.indexOf(doc); if (ind !== -1) { @@ -528,12 +549,9 @@ export namespace Doc { * Adds doc to the list of Docs stored at listDoc[fieldKey]. * @returns true if successful, false otherwise. */ - export function AddDocToList(listDoc: Doc, fieldKey: string | undefined, doc: Doc, relativeTo?: Doc, before?: boolean, first?: boolean, allowDuplicates?: boolean, reversed?: boolean) { + export function AddDocToList(listDoc: Doc, fieldKey: string | undefined, doc: Doc, relativeTo?: Doc, before?: boolean, first?: boolean, allowDuplicates?: boolean, reversed?: boolean, ignoreProto?: boolean) { const key = fieldKey ? fieldKey : Doc.LayoutFieldKey(listDoc); - if (listDoc[key] === undefined) { - listDoc[DocData][key] = new List<Doc>(); - } - const list = Cast(listDoc[key], listSpec(Doc)); + const list = Doc.Get(listDoc, key, ignoreProto) === undefined ? (listDoc[DocData][key] = new List<Doc>()) : Cast(listDoc[key], listSpec(Doc)); if (list) { if (!allowDuplicates) { const pind = list.findIndex(d => d instanceof Doc && d[Id] === doc[Id]); @@ -558,6 +576,16 @@ export namespace Doc { return false; } + export function RemoveEmbedding(doc: Doc, embedding: Doc) { + Doc.RemoveDocFromList(doc[DocData], 'proto_embeddings', embedding); + } + export function AddEmbedding(doc: Doc, embedding: Doc) { + Doc.AddDocToList(doc[DocData], 'proto_embeddings', embedding, undefined, undefined, undefined, undefined, undefined, true); + } + export function GetEmbeddings(doc: Doc) { + return DocListCast(Doc.Get(doc[DocData], 'proto_embeddings', true)); + } + export function MakeEmbedding(doc: Doc, id?: string) { const embedding = (!GetT(doc, 'isDataDoc', 'boolean', true) && doc.proto) || doc.type === DocumentType.CONFIG ? Doc.MakeCopy(doc, undefined, id) : Doc.MakeDelegate(doc, id); const layout = Doc.LayoutField(embedding); @@ -565,20 +593,18 @@ export namespace Doc { Doc.SetLayout(embedding, Doc.MakeEmbedding(layout)); } embedding.createdFrom = doc; - embedding.proto_embeddingId = doc[DocData].proto_embeddingId = DocListCast(doc[DocData].proto_embeddings).length - 1; + embedding.proto_embeddingId = doc[DocData].proto_embeddingId = Doc.GetEmbeddings(doc).length - 1; !Doc.GetT(embedding, 'title', 'string', true) && (embedding.title = ComputedField.MakeFunction(`renameEmbedding(this)`)); embedding.author = Doc.CurrentUserEmail; - Doc.AddDocToList(doc[DocData], 'proto_embeddings', embedding); - return embedding; } export function BestEmbedding(doc: Doc) { const dataDoc = doc[DocData]; - const availableEmbeddings = DocListCast(dataDoc.proto_embeddings); + const availableEmbeddings = Doc.GetEmbeddings(dataDoc); const bestEmbedding = [...(dataDoc !== doc ? [doc] : []), ...availableEmbeddings].find(doc => !doc.embedContainer && doc.author === Doc.CurrentUserEmail); - bestEmbedding && Doc.AddDocToList(dataDoc, 'protoEmbeddings', doc); + bestEmbedding && Doc.AddDocToList(dataDoc, 'protoEmbeddings', doc, undefined, undefined, undefined, undefined, undefined, true); return bestEmbedding ?? Doc.MakeEmbedding(doc); } @@ -852,7 +878,7 @@ export namespace Doc { export function FindReferences(infield: Doc | List<any>, references: Set<Doc>, system: boolean | undefined) { if (infield instanceof Promise) return; if (!(infield instanceof Doc)) { - infield.forEach(val => (val instanceof Doc || val instanceof List) && FindReferences(val, references, system)); + infield?.forEach(val => (val instanceof Doc || val instanceof List) && FindReferences(val, references, system)); return; } const doc = infield as Doc; @@ -937,7 +963,7 @@ export namespace Doc { Doc.GetProto(copy).embedContainer = undefined; Doc.GetProto(copy).proto_embeddings = new List<Doc>([copy]); } else { - Doc.AddDocToList(copy[DocData], 'proto_embeddings', copy); + Doc.AddEmbedding(copy, copy); } copy.embedContainer = undefined; if (retitle) { @@ -958,9 +984,10 @@ export namespace Doc { Object.keys(doc) .filter(key => key.startsWith('acl')) .forEach(key => (delegate[key] = doc[key])); - if (!Doc.IsSystem(doc)) Doc.AddDocToList(doc[DocData], 'proto_embeddings', delegate); + if (!Doc.IsSystem(doc)) Doc.AddEmbedding(doc, delegate); title && (delegate.title = title); delegate[Initializing] = false; + Doc.AddEmbedding(doc, delegate); return delegate; } return undefined; @@ -981,7 +1008,7 @@ export namespace Doc { delegate[Initializing] = true; delegate.proto = delegateProto; delegate.author = Doc.CurrentUserEmail; - Doc.AddDocToList(delegateProto[DocData], 'proto_embeddings', delegate); + Doc.AddEmbedding(delegateProto, delegate); delegate[Initializing] = false; delegateProto[Initializing] = false; return delegate; @@ -1018,13 +1045,13 @@ export namespace Doc { // This function converts a generic field layout display into a field layout that displays a specific // metadata field indicated by the title of the template field (not the default field that it was rendering) // - export function MakeMetadataFieldTemplate(templateField: Doc, templateDoc: Opt<Doc>): boolean { + export function MakeMetadataFieldTemplate(templateField: Doc, templateDoc: Opt<Doc>, keepFieldKey = false): boolean { // find the metadata field key that this template field doc will display (indicated by its title) - const metadataFieldKey = StrCast(templateField.isTemplateForField) || StrCast(templateField.title).replace(/^-/, ''); + const metadataFieldKey = keepFieldKey ? Doc.LayoutFieldKey(templateField) : StrCast(templateField.isTemplateForField) || StrCast(templateField.title).replace(/^-/, '') || Doc.LayoutFieldKey(templateField); // update the original template to mark it as a template templateField.isTemplateForField = metadataFieldKey; - templateField.title = metadataFieldKey; + !keepFieldKey && (templateField.title = metadataFieldKey); const templateFieldValue = templateField[metadataFieldKey] || templateField[Doc.LayoutFieldKey(templateField)]; const templateCaptionValue = templateField.caption; @@ -1640,3 +1667,6 @@ ScriptingGlobals.add(function setDocFilter(container: Doc, key: string, value: a ScriptingGlobals.add(function setDocRangeFilter(container: Doc, key: string, range: number[]) { Doc.setDocRangeFilter(container, key, range); }); +ScriptingGlobals.add(function toJavascriptString(str: string) { + return Field.toJavascriptString(str as Field); +}); diff --git a/src/fields/DocSymbols.ts b/src/fields/DocSymbols.ts index 9c563abbf..f8a57acd5 100644 --- a/src/fields/DocSymbols.ts +++ b/src/fields/DocSymbols.ts @@ -15,6 +15,7 @@ export const DocLayout = Symbol('DocLayout'); export const DocFields = Symbol('DocFields'); export const DocCss = Symbol('DocCss'); export const DocAcl = Symbol('DocAcl'); +export const TransitionTimer = Symbol('DocTransitionTimer'); export const DirectLinks = Symbol('DocDirectLinks'); export const AclPrivate = Symbol('DocAclOwnerOnly'); export const AclReadonly = Symbol('DocAclReadOnly'); diff --git a/src/fields/FieldSymbols.ts b/src/fields/FieldSymbols.ts index 0dbeb064b..0c44d2417 100644 --- a/src/fields/FieldSymbols.ts +++ b/src/fields/FieldSymbols.ts @@ -5,5 +5,6 @@ export const Parent = Symbol('FieldParent'); export const Copy = Symbol('FieldCopy'); export const ToValue = Symbol('FieldToValue'); export const ToScriptString = Symbol('FieldToScriptString'); +export const ToJavascriptString = Symbol('FieldToJavascriptString'); export const ToPlainText = Symbol('FieldToPlainText'); export const ToString = Symbol('FieldToString'); diff --git a/src/fields/HtmlField.ts b/src/fields/HtmlField.ts index 6e8bba977..b67f0f7e9 100644 --- a/src/fields/HtmlField.ts +++ b/src/fields/HtmlField.ts @@ -1,9 +1,9 @@ -import { Deserializable } from "../client/util/SerializationHelper"; -import { serializable, primitive } from "serializr"; -import { ObjectField } from "./ObjectField"; -import { Copy, ToScriptString, ToString} from "./FieldSymbols"; +import { Deserializable } from '../client/util/SerializationHelper'; +import { serializable, primitive } from 'serializr'; +import { ObjectField } from './ObjectField'; +import { Copy, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols'; -@Deserializable("html") +@Deserializable('html') export class HtmlField extends ObjectField { @serializable(primitive()) readonly html: string; @@ -17,8 +17,11 @@ export class HtmlField extends ObjectField { return new HtmlField(this.html); } + [ToJavascriptString]() { + return 'invalid'; + } [ToScriptString]() { - return "invalid"; + return 'invalid'; } [ToString]() { return this.html; diff --git a/src/fields/IconField.ts b/src/fields/IconField.ts index 76c4ddf1b..4d2badb68 100644 --- a/src/fields/IconField.ts +++ b/src/fields/IconField.ts @@ -1,9 +1,9 @@ -import { Deserializable } from "../client/util/SerializationHelper"; -import { serializable, primitive } from "serializr"; -import { ObjectField } from "./ObjectField"; -import { Copy, ToScriptString, ToString } from "./FieldSymbols"; +import { Deserializable } from '../client/util/SerializationHelper'; +import { serializable, primitive } from 'serializr'; +import { ObjectField } from './ObjectField'; +import { Copy, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols'; -@Deserializable("icon") +@Deserializable('icon') export class IconField extends ObjectField { @serializable(primitive()) readonly icon: string; @@ -17,10 +17,13 @@ export class IconField extends ObjectField { return new IconField(this.icon); } + [ToJavascriptString]() { + return 'invalid'; + } [ToScriptString]() { - return "invalid"; + return 'invalid'; } [ToString]() { - return "ICONfield"; + return 'ICONfield'; } } diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts index 22bea3927..b3e01229a 100644 --- a/src/fields/InkField.ts +++ b/src/fields/InkField.ts @@ -2,7 +2,7 @@ import { Bezier } from 'bezier-js'; import { alias, createSimpleSchema, list, object, serializable } from 'serializr'; import { ScriptingGlobals } from '../client/util/ScriptingGlobals'; import { Deserializable } from '../client/util/SerializationHelper'; -import { Copy, ToScriptString, ToString } from './FieldSymbols'; +import { Copy, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols'; import { ObjectField } from './ObjectField'; // Helps keep track of the current ink tool in use. @@ -85,6 +85,9 @@ export class InkField extends ObjectField { return new InkField(this.inkData); } + [ToJavascriptString]() { + return '[' + this.inkData.map(i => `{X: ${i.X}, Y: ${i.Y}}`) + ']'; + } [ToScriptString]() { return 'new InkField([' + this.inkData.map(i => `{X: ${i.X}, Y: ${i.Y}}`) + '])'; } diff --git a/src/fields/List.ts b/src/fields/List.ts index b8ad552d2..ec31f7dae 100644 --- a/src/fields/List.ts +++ b/src/fields/List.ts @@ -5,7 +5,7 @@ import { ScriptingGlobals } from '../client/util/ScriptingGlobals'; import { Deserializable, afterDocDeserialize, autoObject } from '../client/util/SerializationHelper'; import { Field } from './Doc'; import { FieldTuples, Self, SelfProxy } from './DocSymbols'; -import { Copy, FieldChanged, Parent, ToScriptString, ToString } from './FieldSymbols'; +import { Copy, FieldChanged, Parent, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols'; import { ObjectField } from './ObjectField'; import { ProxyField } from './Proxy'; import { RefField } from './RefField'; @@ -310,11 +310,14 @@ class ListImpl<T extends Field> extends ObjectField { private [Self] = this; private [SelfProxy]: List<Field>; // also used in utils.ts even though it won't be found using find all references + [ToJavascriptString]() { + return `[${(this as any).map((field: any) => Field.toScriptString(field))}]`; + } [ToScriptString]() { return `new List([${(this as any).map((field: any) => Field.toScriptString(field))}])`; } [ToString]() { - return `List(${(this as any).length})`; + return `[${(this as any).map((field: any) => Field.toString(field))}]`; } } export type List<T extends Field> = ListImpl<T> & (T | (T extends RefField ? Promise<T> : never))[]; diff --git a/src/fields/ObjectField.ts b/src/fields/ObjectField.ts index b5bc2952a..e1b5b036c 100644 --- a/src/fields/ObjectField.ts +++ b/src/fields/ObjectField.ts @@ -1,5 +1,5 @@ import { RefField } from './RefField'; -import { FieldChanged, Parent, Copy, ToScriptString, ToString } from './FieldSymbols'; +import { FieldChanged, Parent, Copy, ToScriptString, ToString, ToJavascriptString } from './FieldSymbols'; import { ScriptingGlobals } from '../client/util/ScriptingGlobals'; import { Field } from './Doc'; @@ -12,6 +12,7 @@ export abstract class ObjectField { public [Parent]?: RefField | ObjectField; abstract [Copy](): ObjectField; + abstract [ToJavascriptString](): string; abstract [ToScriptString](): string; abstract [ToString](): string; } diff --git a/src/fields/Proxy.ts b/src/fields/Proxy.ts index 3a46e3581..820d9b6ff 100644 --- a/src/fields/Proxy.ts +++ b/src/fields/Proxy.ts @@ -4,7 +4,7 @@ import { DocServer } from '../client/DocServer'; import { scriptingGlobal } from '../client/util/ScriptingGlobals'; import { Deserializable } from '../client/util/SerializationHelper'; import { Field, FieldWaiting, Opt } from './Doc'; -import { Copy, Id, ToScriptString, ToString, ToValue } from './FieldSymbols'; +import { Copy, Id, ToJavascriptString, ToScriptString, ToString, ToValue } from './FieldSymbols'; import { ObjectField } from './ObjectField'; import { RefField } from './RefField'; @@ -38,6 +38,9 @@ export class ProxyField<T extends RefField> extends ObjectField { return new ProxyField<T>(this.fieldId); } + [ToJavascriptString]() { + return Field.toScriptString(this[ToValue](undefined)?.value); + } [ToScriptString]() { return Field.toScriptString(this[ToValue](undefined)?.value); // not sure this is quite right since it doesn't recreate a proxy field, but better than 'invalid' ? } diff --git a/src/fields/RefField.ts b/src/fields/RefField.ts index b6ef69750..01828dd14 100644 --- a/src/fields/RefField.ts +++ b/src/fields/RefField.ts @@ -1,11 +1,11 @@ -import { serializable, primitive, alias } from "serializr"; -import { Utils } from "../Utils"; -import { Id, HandleUpdate, ToScriptString, ToString } from "./FieldSymbols"; -import { afterDocDeserialize } from "../client/util/SerializationHelper"; +import { serializable, primitive, alias } from 'serializr'; +import { Utils } from '../Utils'; +import { Id, HandleUpdate, ToScriptString, ToString, ToJavascriptString } from './FieldSymbols'; +import { afterDocDeserialize } from '../client/util/SerializationHelper'; export type FieldId = string; export abstract class RefField { - @serializable(alias("id", primitive({ afterDeserialize: afterDocDeserialize }))) + @serializable(alias('id', primitive({ afterDeserialize: afterDocDeserialize }))) private __id: FieldId; readonly [Id]: FieldId; @@ -16,6 +16,7 @@ export abstract class RefField { protected [HandleUpdate]?(diff: any): void | Promise<void>; + abstract [ToJavascriptString](): string; abstract [ToScriptString](): string; abstract [ToString](): string; } diff --git a/src/fields/RichTextField.ts b/src/fields/RichTextField.ts index 3e75a071f..50cfab988 100644 --- a/src/fields/RichTextField.ts +++ b/src/fields/RichTextField.ts @@ -1,7 +1,7 @@ import { serializable } from 'serializr'; import { scriptingGlobal } from '../client/util/ScriptingGlobals'; import { Deserializable } from '../client/util/SerializationHelper'; -import { Copy, ToScriptString, ToString } from './FieldSymbols'; +import { Copy, ToJavascriptString, ToScriptString, ToString } from './FieldSymbols'; import { ObjectField } from './ObjectField'; @scriptingGlobal @@ -27,6 +27,9 @@ export class RichTextField extends ObjectField { return new RichTextField(this.Data, this.Text); } + [ToJavascriptString]() { + return '`' + this.Text + '`'; + } [ToScriptString]() { return `new RichTextField("${this.Data.replace(/"/g, '\\"')}", "${this.Text}")`; } diff --git a/src/fields/SchemaHeaderField.ts b/src/fields/SchemaHeaderField.ts index 6dde2e5aa..fb4dc4e5b 100644 --- a/src/fields/SchemaHeaderField.ts +++ b/src/fields/SchemaHeaderField.ts @@ -1,7 +1,7 @@ import { Deserializable } from '../client/util/SerializationHelper'; import { serializable, primitive } from 'serializr'; import { ObjectField } from './ObjectField'; -import { Copy, ToScriptString, ToString, FieldChanged } from './FieldSymbols'; +import { Copy, ToScriptString, ToString, FieldChanged, ToJavascriptString } from './FieldSymbols'; import { scriptingGlobal, ScriptingGlobals } from '../client/util/ScriptingGlobals'; import { ColumnType } from '../client/views/collections/collectionSchema/CollectionSchemaView'; @@ -114,6 +114,9 @@ export class SchemaHeaderField extends ObjectField { return new SchemaHeaderField(this.heading, this.color, this.type, this.width, this.desc, this.collapsed); } + [ToJavascriptString]() { + return `["${this.heading}","${this.color}",${this.type},${this.width},${this.desc},${this.collapsed}]`; + } [ToScriptString]() { return `schemaHeaderField("${this.heading}","${this.color}",${this.type},${this.width},${this.desc},${this.collapsed})`; } diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts index 62690a9fb..c7fe72ca6 100644 --- a/src/fields/ScriptField.ts +++ b/src/fields/ScriptField.ts @@ -6,7 +6,7 @@ import { scriptingGlobal, ScriptingGlobals } from '../client/util/ScriptingGloba import { autoObject, Deserializable } from '../client/util/SerializationHelper'; import { numberRange } from '../Utils'; import { Doc, Field, Opt } from './Doc'; -import { Copy, Id, ToScriptString, ToString, ToValue } from './FieldSymbols'; +import { Copy, Id, ToJavascriptString, ToScriptString, ToString, ToValue } from './FieldSymbols'; import { List } from './List'; import { ObjectField } from './ObjectField'; import { Cast, StrCast } from './Types'; @@ -113,6 +113,9 @@ export class ScriptField extends ObjectField { return `${this.script.originalScript} + ${this.setterscript?.originalScript}`; } + [ToJavascriptString]() { + return this.script.originalScript; + } [ToScriptString]() { return this.script.originalScript; } diff --git a/src/fields/URLField.ts b/src/fields/URLField.ts index 817b62373..87334ad16 100644 --- a/src/fields/URLField.ts +++ b/src/fields/URLField.ts @@ -1,7 +1,7 @@ import { Deserializable } from '../client/util/SerializationHelper'; import { serializable, custom } from 'serializr'; import { ObjectField } from './ObjectField'; -import { ToScriptString, ToString, Copy } from './FieldSymbols'; +import { ToScriptString, ToString, Copy, ToJavascriptString } from './FieldSymbols'; import { scriptingGlobal } from '../client/util/ScriptingGlobals'; import { Utils } from '../Utils'; @@ -36,6 +36,12 @@ export abstract class URLField extends ObjectField { } return `new ${this.constructor.name}("${this.url?.href}")`; } + [ToJavascriptString]() { + if (Utils.prepend(this.url?.pathname) === this.url?.href) { + return `new ${this.constructor.name}("${this.url.pathname}")`; + } + return `new ${this.constructor.name}("${this.url?.href}")`; + } [ToString]() { if (Utils.prepend(this.url?.pathname) === this.url?.href) { return this.url.pathname; diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts index b1a7a9c5e..307aec6fc 100644 --- a/src/server/DashUploadUtils.ts +++ b/src/server/DashUploadUtils.ts @@ -14,7 +14,7 @@ import * as path from 'path'; import { basename } from 'path'; import * as parse from 'pdf-parse'; import * as request from 'request-promise'; -import { Duplex } from 'stream'; +import { Duplex, Stream } from 'stream'; import { filesDirectory, publicDirectory } from '.'; import { Utils } from '../Utils'; import { Opt } from '../fields/Doc'; @@ -349,10 +349,8 @@ export namespace DashUploadUtils { if (metadata instanceof Error) { return { name: metadata.name, message: metadata.message }; } - const outputFile = filename || metadata.filename; - if (!outputFile) { - return { name: source, message: 'output file not found' }; - } + const outputFile = filename || metadata.filename || ''; + return UploadInspectedImage(metadata, outputFile, prefix); }; @@ -552,7 +550,22 @@ export namespace DashUploadUtils { writtenFiles = {}; } } else { - writtenFiles = await outputResizedImages(metadata.source, resolved, pathToDirectory(Directory.images)); + try { + writtenFiles = await outputResizedImages(metadata.source, resolved, pathToDirectory(Directory.images)); + } catch (e) { + // input is a blob or other, try reading it to create a metadata source file. + const reqSource = request(metadata.source); + let readStream: Stream = reqSource instanceof Promise ? await reqSource : reqSource; + const readSource = `${prefix}upload_${Utils.GenerateGuid()}.${metadata.contentType.split('/')[1].toLowerCase()}`; + await new Promise<void>((res, rej) => + readStream + .pipe(createWriteStream(readSource)) + .on('close', () => res()) + .on('error', () => rej()) + ); + writtenFiles = await outputResizedImages(readSource, resolved, pathToDirectory(Directory.images)); + fs.unlink(readSource, err => console.log("Couldn't unlink temporary image file:" + readSource)); + } } for (const suffix of Object.keys(writtenFiles)) { information.accessPaths[suffix] = getAccessPaths(images, writtenFiles[suffix]); |