diff options
Diffstat (limited to 'src')
84 files changed, 1094 insertions, 1054 deletions
diff --git a/src/Utils.ts b/src/Utils.ts index 8c56896c5..2e06b5631 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -141,7 +141,7 @@ export namespace Utils { const isTransparentFunctionHack = 'isTransparent(__value__)'; export const noRecursionHack = '__noRecursion'; - export const noDragsDocFilter = 'noDragDocs:any:check'; + export const noDragsDocFilter = 'noDragDocs::any::check'; export function IsRecursiveFilter(val: string) { return !val.includes(noRecursionHack); } @@ -150,14 +150,14 @@ export namespace Utils { } export function IsTransparentFilter() { // bcz: isTransparent(__value__) is a hack. it would be nice to have acual functions be parsed, but now Doc.matchFieldValue is hardwired to recognize just this one - return `backgroundColor:${isTransparentFunctionHack},${noRecursionHack}:check`; // bcz: hack. noRecursion should probably be either another ':' delimited field, or it should be a modifier to the comparision (eg., check, x, etc) field + return `backgroundColor::${isTransparentFunctionHack},${noRecursionHack}::check`; // bcz: hack. noRecursion should probably be either another ':' delimited field, or it should be a modifier to the comparision (eg., check, x, etc) field } export function IsOpaqueFilter() { // bcz: isTransparent(__value__) is a hack. it would be nice to have acual functions be parsed, but now Doc.matchFieldValue is hardwired to recognize just this one - return `backgroundColor:${isTransparentFunctionHack},${noRecursionHack}:x`; // bcz: hack. noRecursion should probably be either another ':' delimited field, or it should be a modifier to the comparision (eg., check, x, etc) field + return `backgroundColor::${isTransparentFunctionHack},${noRecursionHack}::x`; // bcz: hack. noRecursion should probably be either another ':' delimited field, or it should be a modifier to the comparision (eg., check, x, etc) field } export function IsPropUnsetFilter(prop: string) { - return `${prop}:any,${noRecursionHack}:unset`; + return `${prop}::any,${noRecursionHack}::unset`; } export function toRGBAstr(col: { r: number; g: number; b: number; a?: number }) { diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 55bc4a38b..85bbc9fed 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -25,7 +25,7 @@ import { DirectoryImportBox } from '../util/Import & Export/DirectoryImportBox'; import { FollowLinkScript } from '../util/LinkFollower'; import { LinkManager } from '../util/LinkManager'; import { ScriptingGlobals } from '../util/ScriptingGlobals'; -import { undoable, undoBatch, UndoManager } from '../util/UndoManager'; +import { undoable, UndoManager } from '../util/UndoManager'; import { CollectionDockingView } from '../views/collections/CollectionDockingView'; import { DimUnit } from '../views/collections/collectionMulticolumn/CollectionMulticolumnView'; import { CollectionView } from '../views/collections/CollectionView'; @@ -184,7 +184,8 @@ export class DocumentOptions { opacity?: NUMt = new NumInfo('document opacity'); viewTransitionTime?: NUMt = new NumInfo('transition duration for view parameters'); dontRegisterView?: BOOLt = new BoolInfo('are views of this document registered so that they can be found when following links, etc'); - dontUndo?: BOOLt = new BoolInfo('whether button clicks should be undoable (true for Undo/Redo/and sidebar) AND whether modifications to document are undoable (true for linearview menu buttons to prevent open/close from entering undo stack)'); + _undoIgnoreFields?: List<string>; //'fields that should not be added to the undo stack (opacity for Undo/Redo/and sidebar) AND whether modifications to document are undoable (true for linearview menu buttons to prevent open/close from entering undo stack)' + undoIgnoreFields?: List<string>; //'fields that should not be added to the undo stack (opacity for Undo/Redo/and sidebar) AND whether modifications to document are undoable (true for linearview menu buttons to prevent open/close from entering undo stack)' _headerHeight?: NUMt = new NumInfo('height of document header used for displaying title'); _headerFontSize?: NUMt = new NumInfo('font size of header of custom notes'); _headerPointerEvents?: PEVt = new PEInfo('types of events the header of a custom text document can consume'); @@ -355,20 +356,20 @@ export class DocumentOptions { 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; - dragFactory_count?: NUMt = new NumInfo('number of items created from a drag button (used for setting title with incrementing index)', true); openFactoryLocation?: string; // an OpenWhere value to place the factory created document openFactoryAsDelegate?: BOOLt = new BoolInfo('create a delegate of the factory'); _forceActive?: BOOLt = new BoolInfo('flag to handle pointer events when not selected (or otherwise active)'); - enableDragWhenActive?: BOOLt = new BoolInfo('allow dragging even if document contentts are active (e.g., tree, groups)'); - _stayInCollection?: BOOLt = new BoolInfo('whether the document should remain in its collection when someone tries to drag and drop it elsewhere'); + _dragOnlyWithinContainer?: BOOLt = new BoolInfo('whether the document should remain in its collection when someone tries to drag and drop it elsewhere'); _raiseWhenDragged?: BOOLt = new BoolInfo('whether a document is brought to front when dragged.'); - allowOverlayDrop?: BOOLt = new BoolInfo('can documents be dropped onto this document without using dragging title bar or holding down embed key (ctrl)?', true); - childDropAction?: DROPt = new DAInfo("what should happen to the source document when it's dropped onto a child of a collection "); - targetDropAction?: DROPt = new DAInfo('what should happen to the source document when ??? '); + childDragAction?: DROPt = new DAInfo('what should happen to the child documents when they are dragged from the collection'); + dropConverter?: ScriptField; // script to run when documents are dropped on this Document. + dropAction?: DROPt = new DAInfo("what should happen to this document when it's dropped somewhere else"); _dropAction?: DROPt = new DAInfo("what should happen to this document when it's dropped somewhere else"); - _removeDropProperties?: List<string>; // list of properties that should be removed from a document when it is dropped. e.g., a creator button may be forceActive to allow it be dragged, but the forceActive property can be removed from the dropped document + _dropPropertiesToRemove?: List<string>; // list of properties that should be removed from a document when it is dropped. e.g., a creator button may be forceActive to allow it be dragged, but the forceActive property can be removed from the dropped document cloneFieldFilter?: List<string>; // fields not to copy when the document is clonedclipboard?: Doc; - dropConverter?: ScriptField; // script to run when documents are dropped on this Document. + dragWhenActive?: BOOLt = new BoolInfo('should document drag when it is active - e.g., pileView, group'); + dragAction?: DROPt = new DAInfo('how to drag document when it is active (e.g., tree, groups)'); + dragFactory_count?: NUMt = new NumInfo('number of items created from a drag button (used for setting title with incrementing index)', true); dragFactory?: Doc; // document to create when dragging with a suitable onDragStart script clickFactory?: Doc; // document to create when clicking on a button with a suitable onClick script 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 @@ -536,7 +537,7 @@ export namespace Docs { link_description: '', layout_showCaption: 'link_description', backgroundColor: 'lightblue', // lightblue is default color for linking dot and link documents text comment area - _removeDropProperties: new List(['onClick']), + _dropPropertiesToRemove: new List(['onClick']), }, }, ], @@ -545,7 +546,7 @@ export namespace Docs { { data: new List<Doc>(), layout: { view: EmptyBox, dataField: defaultDataKey }, - options: { childDropAction: 'embed', title: 'Global Link Database' }, + options: { title: 'Global Link Database' }, }, ], [ @@ -553,7 +554,7 @@ export namespace Docs { { data: new List<Doc>(), layout: { view: EmptyBox, dataField: defaultDataKey }, - options: { childDropAction: 'embed', title: 'Global Script Database' }, + options: { title: 'Global Script Database' }, }, ], [ @@ -615,7 +616,7 @@ export namespace Docs { DocumentType.FONTICON, { layout: { view: FontIconBox, dataField: 'icon' }, - options: { defaultDoubleClick: 'ignore', waitForDoubleClickToClick: 'never', enableDragWhenActive: true, layout_hideLinkButton: true, _width: 40, _height: 40 }, + options: { defaultDoubleClick: 'ignore', waitForDoubleClickToClick: 'never', layout_hideLinkButton: true, _width: 40, _height: 40 }, }, ], [ @@ -659,7 +660,7 @@ export namespace Docs { { data: '', layout: { view: ComparisonBox, dataField: defaultDataKey }, - options: { backgroundColor: 'gray', targetDropAction: 'embed' }, + options: { backgroundColor: 'gray', dropAction: 'move', waitForDoubleClickToClick: 'always' }, }, ], [ @@ -667,7 +668,7 @@ export namespace Docs { { data: new List<Doc>(), layout: { view: EmptyBox, dataField: defaultDataKey }, - options: { childDropAction: 'embed', title: 'Global Group Database' }, + options: { title: 'Global Group Database' }, }, ], [ @@ -1067,12 +1068,7 @@ export namespace Docs { } export function PileDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) { - return InstanceFromProto( - Prototypes.get(DocumentType.COL), - new List(documents), - { enableDragWhenActive: true, _forceActive: true, _freeform_noZoom: true, _freeform_noAutoPan: true, ...options, _type_collection: CollectionViewType.Pile }, - id - ); + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { dropAction: 'move', _forceActive: true, _freeform_noZoom: true, _freeform_noAutoPan: true, ...options, _type_collection: CollectionViewType.Pile }, id); } export function LinearDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) { @@ -1218,7 +1214,7 @@ export namespace DocUtils { const filterFacets: { [key: string]: { [value: string]: string } } = {}; // maps each filter key to an object with value=>modifier fields childFilters.forEach(filter => { - const fields = filter.split(':'); + const fields = filter.split(Doc.FilterSep); const key = fields[0]; const value = fields[1]; const modifiers = fields[2]; @@ -1236,7 +1232,7 @@ export namespace DocUtils { return false; } - for (const facetKey of Object.keys(filterFacets).filter(fkey => fkey !== 'cookies' && fkey !== Utils.noDragsDocFilter.split(':')[0])) { + for (const facetKey of Object.keys(filterFacets).filter(fkey => fkey !== 'cookies' && fkey !== Utils.noDragsDocFilter.split(Doc.FilterSep)[0])) { const facet = filterFacets[facetKey]; // facets that match some value in the field of the document (e.g. some text field) @@ -1524,7 +1520,7 @@ export namespace DocUtils { !simpleMenu && ContextMenu.Instance.addItem({ description: 'Quick Notes', - subitems: DocListCast((Doc.UserDoc()['template-notes'] as Doc).data).map((note, i) => ({ + 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('', { @@ -1584,10 +1580,10 @@ export namespace DocUtils { } export function findTemplate(templateName: string, type: string, signature: string) { let docLayoutTemplate: Opt<Doc>; - const iconViews = DocListCast(Cast(Doc.UserDoc()['template-icons'], Doc, null)?.data); - const templBtns = DocListCast(Cast(Doc.UserDoc()['template-buttons'], Doc, null)?.data); - const noteTypes = DocListCast(Cast(Doc.UserDoc()['template-notes'], Doc, null)?.data); - const clickFuncs = DocListCast(Cast(Doc.UserDoc()['template-clickFuncs'], Doc, null)?.data); + const iconViews = DocListCast(Cast(Doc.UserDoc()['template_icons'], Doc, null)?.data); + const templBtns = DocListCast(Cast(Doc.UserDoc()['template_buttons'], Doc, null)?.data); + const noteTypes = DocListCast(Cast(Doc.UserDoc()['template_notes'], Doc, null)?.data); + const clickFuncs = DocListCast(Cast(Doc.UserDoc()['template_clickFuncs'], Doc, null)?.data); const allTemplates = iconViews .concat(templBtns) .concat(noteTypes) @@ -1669,7 +1665,7 @@ export namespace DocUtils { }); }); if (create) { - const newCollection = Docs.Create.PileDocument(docList, { title: 'pileup', x: (x || 0) - size, y: (y || 0) - size, _width: size * 2, _height: size * 2 }); + const newCollection = Docs.Create.PileDocument(docList, { title: 'pileup', x: (x || 0) - size, y: (y || 0) - size, _width: size * 2, _height: size * 2, dragWhenActive: true }); newCollection.x = NumCast(newCollection.x) + NumCast(newCollection._width) / 2 - size; newCollection.y = NumCast(newCollection.y) + NumCast(newCollection._height) / 2 - size; newCollection._width = newCollection._height = size * 2; diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 592f6d836..11a8dcaf6 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -97,7 +97,7 @@ export class CurrentUserUtils { const reqdOpts:DocumentOptions = { title: "Experimental Tools", _xMargin: 0, _layout_showTitle: "title", _chromeHidden: true, - _stayInCollection: true, _layout_hideContextMenu: true, _forceActive: true, isSystem: true, + _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, _forceActive: true, isSystem: true, _layout_autoHeight: true, _width: 500, _height: 300, _layout_fitWidth: true, _columnWidth: 35, ignoreClick: true, _lockedPosition: true, }; const reqdScripts = { dropConverter : "convertToButtons(dragData)" }; @@ -123,7 +123,7 @@ export class CurrentUserUtils { } /// Initializes templates for editing click funcs of a document - static setupClickEditorTemplates(doc: Doc, field = "template-clickFuncs") { + static setupClickEditorTemplates(doc: Doc, field = "template_clickFuncs") { const tempClicks = DocCast(doc[field]); const reqdClickOpts:DocumentOptions = { _width: 300, _height:200, isSystem: true}; const reqdTempOpts:{opts:DocumentOptions, script: string}[] = [ @@ -144,7 +144,7 @@ export class CurrentUserUtils { } /// Initializes templates that can be applied to notes - static setupNoteTemplates(doc: Doc, field="template-notes") { + static setupNoteTemplates(doc: Doc, field="template_notes") { const tempNotes = DocCast(doc[field]); const reqdTempOpts:DocumentOptions[] = [ { noteType: "Note", backgroundColor: "yellow", icon: "sticky-note"}, @@ -174,7 +174,7 @@ export class CurrentUserUtils { } // setup templates for different document types when they are iconified from Document Decorations - static setupDefaultIconTemplates(doc: Doc, field="template-icons") { + static setupDefaultIconTemplates(doc: Doc, field="template_icons") { const reqdOpts = { title: "icon templates", _height: 75, isSystem: true }; const templateIconsDoc = DocUtils.AssignOpts(DocCast(doc[field]), reqdOpts) ?? (doc[field] = Docs.Create.TreeDocument([], reqdOpts)); @@ -279,11 +279,11 @@ 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, treeViewHideUnrendered: true}}, - {key: "Trail", creator: Docs.Create.PresDocument, opts: { _width: 400, _height: 30, _type_collection: CollectionViewType.Stacking, targetDropAction: "embed" as dropActionType, treeViewHideTitle: true, _layout_fitWidth:true, _chromeHidden: true, layout_boxShadow: "0 0" }}, + {key: "Trail", creator: Docs.Create.PresDocument, opts: { _width: 400, _height: 30, _type_collection: CollectionViewType.Stacking, dropAction: "embed" as dropActionType, treeViewHideTitle: true, _layout_fitWidth:true, _chromeHidden: 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, treeViewHasOverlay: true, _text_fontSize: "20px", _layout_autoHeight: true, - allowOverlayDrop: true, treeViewType: TreeViewType.outline, + dropAction:'move', treeViewType: TreeViewType.outline, backgroundColor: "white", _xMargin: 0, _yMargin: 0, _createDocOnCR: true }, funcs: {title: 'self.text?.Text'}}, ]; @@ -322,17 +322,16 @@ export class CurrentUserUtils { const creatorBtns = CurrentUserUtils.creatorBtnDescriptors(doc).map((reqdOpts) => { const btn = dragCreatorDoc ? DocListCast(dragCreatorDoc.data).find(doc => doc.title === reqdOpts.title): undefined; const opts:DocumentOptions = {...OmitKeys(reqdOpts, ["funcs", "scripts", "backgroundColor"]).omit, - _width: 35, _height: 35, _layout_hideContextMenu: true, _stayInCollection: true, + _width: 35, _height: 35, _layout_hideContextMenu: true, _dragOnlyWithinContainer: true, btnType: ButtonType.ToolButton, backgroundColor: reqdOpts.backgroundColor ?? Colors.DARK_GRAY, color: Colors.WHITE, isSystem: true, - _removeDropProperties: new List<string>(["_stayInCollection"]), }; return DocUtils.AssignScripts(DocUtils.AssignOpts(btn, opts) ?? Docs.Create.FontIconDocument(opts), reqdOpts.scripts, reqdOpts.funcs); }); const reqdOpts:DocumentOptions = { - title: "Basic Item Creators", _layout_showTitle: "title", _xMargin: 0, _stayInCollection: true, _layout_hideContextMenu: true, _chromeHidden: true, isSystem: true, + title: "Basic Item 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, - childDropAction: 'embed' + childDragAction: 'embed' }; const reqdScripts = { dropConverter: "convertToButtons(dragData)" }; return DocUtils.AssignScripts(DocUtils.AssignOpts(dragCreatorDoc, reqdOpts, creatorBtns) ?? Docs.Create.MasonryDocument(creatorBtns, reqdOpts), reqdScripts); @@ -357,7 +356,7 @@ export class CurrentUserUtils { /// the empty panel that is filled with whichever left menu button's panel has been selected static setupLeftSidebarPanel(doc: Doc, field="myLeftSidebarPanel") { - DocUtils.AssignDocField(doc, field, (opts) => ((doc:Doc) => {doc.isSystem = true; return doc;})(new Doc()), {isSystem:true}); + DocUtils.AssignDocField(doc, field, (opts) => Doc.assign(new Doc(), opts as any), {isSystem:true, undoIgnoreFields: new List<string>(['proto'])}); } /// Initializes the left sidebar menu buttons and the panels they open up @@ -367,15 +366,14 @@ export class CurrentUserUtils { const menuBtns = CurrentUserUtils.leftSidebarMenuBtnDescriptions(doc).map(({ title, target, icon, scripts, funcs }) => { const btnDoc = myLeftSidebarMenu ? DocListCast(myLeftSidebarMenu.data).find(doc => doc.title === title) : undefined; const reqdBtnOpts:DocumentOptions = { - title, icon, target, btnType: ButtonType.MenuButton, isSystem: true, dontUndo: true, dontRegisterView: true, - _width: 60, _height: 60, _stayInCollection: true, _layout_hideContextMenu: true, - _removeDropProperties: new List<string>(["_stayInCollection"]), + title, icon, target, btnType: ButtonType.MenuButton, isSystem: true, undoIgnoreFields: new List<string>(['height', 'data_columnHeaders']), dontRegisterView: true, + _width: 60, _height: 60, _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, }; return DocUtils.AssignScripts(DocUtils.AssignOpts(btnDoc, reqdBtnOpts) ?? Docs.Create.FontIconDocument(reqdBtnOpts), scripts, funcs); }); const reqdStackOpts:DocumentOptions ={ - title: "menuItemPanel", childDropAction: "same", backgroundColor: Colors.DARK_GRAY, layout_boxShadow: "rgba(0,0,0,0)", dontRegisterView: true, ignoreClick: true, + title: "menuItemPanel", childDragAction: "same", backgroundColor: Colors.DARK_GRAY, layout_boxShadow: "rgba(0,0,0,0)", dontRegisterView: true, ignoreClick: true, _chromeHidden: true, _gridGap: 0, _yMargin: 0, _yPadding: 0, _xMargin: 0, _layout_autoHeight: false, _width: 60, _columnWidth: 60, _lockedPosition: true, isSystem: true, }; return DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.StackingDocument(items??[], opts), reqdStackOpts, menuBtns, { dropConverter: "convertToButtons(dragData)" }); @@ -458,7 +456,7 @@ export class CurrentUserUtils { /// Search option on the left side button panel static setupSearcher(doc: Doc, field:string) { return DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.SearchDocument(opts), { - dontRegisterView: true, backgroundColor: "dimgray", ignoreClick: true, title: "Search Panel", isSystem: true, childDropAction: "embed", + dontRegisterView: true, backgroundColor: "dimgray", ignoreClick: true, title: "Search Panel", isSystem: true, childDragAction: "embed", _lockedPosition: true, _type_collection: CollectionViewType.Schema }); } @@ -469,7 +467,7 @@ export class CurrentUserUtils { const templateBtns = CurrentUserUtils.setupExperimentalTemplateButtons(doc,DocListCast(myTools?.data)?.length > 1 ? DocListCast(myTools.data)[1]:undefined); const reqdToolOps:DocumentOptions = { title: "My Tools", isSystem: true, ignoreClick: true, layout_boxShadow: "0 0", - _layout_showTitle: "title", _width: 500, _yMargin: 20, _lockedPosition: true, _forceActive: true, _stayInCollection: true, _layout_hideContextMenu: true, _chromeHidden: true, + _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]); } @@ -481,7 +479,7 @@ export class CurrentUserUtils { const toggleDarkTheme = `this.colorScheme = this.colorScheme ? undefined : "${ColorScheme.Dark}"`; const newDashboard = `createNewDashboard()`; - const reqdBtnOpts:DocumentOptions = { _forceActive: true, _width: 30, _height: 30, _stayInCollection: true, _layout_hideContextMenu: true, + const reqdBtnOpts:DocumentOptions = { _forceActive: true, _width: 30, _height: 30, _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, title: "new dashboard", btnType: ButtonType.ClickButton, toolTip: "Create new dashboard", buttonText: "New trail", icon: "plus", isSystem: true }; const reqdBtnScript = {onClick: newDashboard,} const newDashboardButton = DocUtils.AssignScripts(DocUtils.AssignOpts(DocCast(myDashboards?.layout_headerButton), reqdBtnOpts) ?? Docs.Create.FontIconDocument(reqdBtnOpts), reqdBtnScript); @@ -495,8 +493,8 @@ export class CurrentUserUtils { const childContextMenuIcons = ["chalkboard", "tv", "camera", "users", "times", "trash"]; // entries must be kept in synch with childContextMenuScripts, childContextMenuLabels, and childContextMenuFilters const reqdOpts:DocumentOptions = { title: "My Dashboards", childHideLinkButton: true, treeViewFreezeChildren: "remove|add", treeViewHideTitle: true, layout_boxShadow: "0 0", childDontRegisterViews: true, - targetDropAction: "same", treeViewType: TreeViewType.fileSystem, isFolder: true, isSystem: true, treeViewTruncateTitleWidth: 350, ignoreClick: true, - layout_headerButton: newDashboardButton, childDropAction: "embed", + dropAction: "same", treeViewType: TreeViewType.fileSystem, isFolder: true, isSystem: true, treeViewTruncateTitleWidth: 350, ignoreClick: true, + layout_headerButton: newDashboardButton, childDragAction: "embed", _layout_showTitle: "title", _height: 400, _gridGap: 5, _forceActive: true, _lockedPosition: true, contextMenuLabels:new List<string>(contextMenuLabels), contextMenuIcons:new List<string>(contextMenuIcons), @@ -520,20 +518,20 @@ export class CurrentUserUtils { /// initializes the left sidebar File system pane static setupFilesystem(doc: Doc, field:string) { var myFilesystem = DocCast(doc[field]); - const myFileOrphans = DocUtils.AssignDocField(doc, "myFileOrphans", (opts) => Docs.Create.TreeDocument([], opts), { title: "Unfiled", _stayInCollection: true, isSystem: true, isFolder: true }); + const myFileOrphans = DocUtils.AssignDocField(doc, "myFileOrphans", (opts) => Docs.Create.TreeDocument([], opts), { title: "Unfiled", undoIgnoreFields:new List<string>(['treeViewSortCriterion']), _dragOnlyWithinContainer: true, isSystem: true, isFolder: true }); const newFolder = `TreeView_addNewFolder()`; const newFolderOpts: DocumentOptions = { - _forceActive: true, _stayInCollection: true, _layout_hideContextMenu: true, _width: 30, _height: 30, + _forceActive: true, _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, _width: 30, _height: 30, undoIgnoreFields:new List<string>(['treeViewSortCriterion']), title: "New folder", btnType: ButtonType.ClickButton, toolTip: "Create new folder", buttonText: "New folder", icon: "folder-plus", isSystem: true }; const newFolderScript = { onClick: newFolder}; const newFolderButton = DocUtils.AssignScripts(DocUtils.AssignOpts(DocCast(myFilesystem?.layout_headerButton), newFolderOpts) ?? Docs.Create.FontIconDocument(newFolderOpts), newFolderScript); const reqdOpts:DocumentOptions = { _layout_showTitle: "title", _height: 100, _gridGap: 5, _forceActive: true, _lockedPosition: true, - title: "My Documents", layout_headerButton: newFolderButton, treeViewHideTitle: true, targetDropAction: "proto", isSystem: true, + title: "My Documents", layout_headerButton: newFolderButton, treeViewHideTitle: true, dropAction: "proto", isSystem: true, isFolder: true, treeViewType: TreeViewType.fileSystem, childHideLinkButton: true, layout_boxShadow: "0 0", childDontRegisterViews: true, - treeViewTruncateTitleWidth: 350, ignoreClick: true, childDropAction: "embed", + treeViewTruncateTitleWidth: 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." @@ -549,8 +547,8 @@ export class CurrentUserUtils { /// initializes the panel displaying docs that have been recently closed static setupRecentlyClosed(doc: Doc, field:string) { const reqdOpts:DocumentOptions = { _layout_showTitle: "title", _lockedPosition: true, _gridGap: 5, _forceActive: true, - title: "My Recently Closed", childHideLinkButton: true, treeViewHideTitle: true, childDropAction: "embed", isSystem: true, - treeViewTruncateTitleWidth: 350, ignoreClick: true, layout_boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", + title: "My Recently Closed", childHideLinkButton: true, treeViewHideTitle: true, childDragAction: "embed", isSystem: true, + treeViewTruncateTitleWidth: 350, ignoreClick: true, layout_boxShadow: "0 0", childDontRegisterViews: true, dropAction: "same", contextMenuLabels: new List<string>(["Empty recently closed"]), contextMenuIcons:new List<string>(["trash"]), layout_explainer: "Recently closed documents appear in this menu. They will only be deleted if you explicity empty this list." @@ -558,7 +556,7 @@ export class CurrentUserUtils { const recentlyClosed = DocUtils.AssignDocField(doc, field, (opts) => Docs.Create.TreeDocument([], opts), reqdOpts); const clearAll = (target:string) => `getProto(${target}).data = new List([])`; - const clearBtnsOpts:DocumentOptions = { _width: 30, _height: 30, _forceActive: true, _stayInCollection: true, _layout_hideContextMenu: true, + const clearBtnsOpts:DocumentOptions = { _width: 30, _height: 30, _forceActive: true, _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, title: "Empty", target: recentlyClosed, btnType: ButtonType.ClickButton, buttonText: "Empty", icon: "trash", isSystem: true, toolTip: "Empty recently closed",}; DocUtils.AssignDocField(recentlyClosed, "layout_headerButton", (opts) => Docs.Create.FontIconDocument(opts), clearBtnsOpts, undefined, {onClick: clearAll("self.target")}); @@ -575,7 +573,7 @@ export class CurrentUserUtils { static setupUserDocView(doc: Doc, field:string) { const reqdOpts:DocumentOptions = { _lockedPosition: true, _gridGap: 5, _forceActive: true, title: Doc.CurrentUserEmail +"-view", - layout_boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", ignoreClick: true, isSystem: true, + layout_boxShadow: "0 0", childDontRegisterViews: true, dropAction: "same", ignoreClick: true, isSystem: true, treeViewHideTitle: true, treeViewTruncateTitleWidth: 350 }; if (!doc[field]) DocUtils.AssignOpts(doc, {treeViewOpen: true, treeViewExpandedView: "fields" }); @@ -590,7 +588,7 @@ export class CurrentUserUtils { static createToolButton = (opts: DocumentOptions) => Docs.Create.FontIconDocument({ btnType: ButtonType.ToolButton, _forceActive: true, _layout_hideContextMenu: true, - _removeDropProperties: new List<string>([ "_layout_hideContextMenu", "stayInCollection"]), + _dropPropertiesToRemove: new List<string>([ "_layout_hideContextMenu"]), _nativeWidth: 40, _nativeHeight: 40, _width: 40, _height: 40, isSystem: true, ...opts, }) @@ -608,9 +606,9 @@ export class CurrentUserUtils { { scripts: { }, opts: { title: "linker", layout: "<LinkingUI>", toolTip: "link started"}}, { scripts: { }, opts: { title: "currently playing", layout: "<CurrentlyPlayingUI>", toolTip: "currently playing media"}}, ]; - const btns = btnDescs.map(desc => dockBtn({_width: 30, _height: 30, defaultDoubleClick: 'ignore', dontUndo: true, _stayInCollection: true, ...desc.opts}, desc.scripts)); + 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 = { - title: "docked buttons", _height: 40, flexGap: 0, layout_boxShadow: "standard", childDropAction: 'embed', + title: "docked buttons", _height: 40, flexGap: 0, layout_boxShadow: "standard", childDragAction: 'move', childDontRegisterViews: true, linearView_IsExpanded: true, linearView_Expandable: true, ignoreClick: true }; reaction(() => UndoManager.redoStack.slice(), () => Doc.GetProto(btns.find(btn => btn.title === "redo")!).opacity = UndoManager.CanRedo() ? 1 : 0.4, { fireImmediately: true }); @@ -716,12 +714,11 @@ export class CurrentUserUtils { const reqdOpts:DocumentOptions = { ...OmitKeys(params, ["scripts", "funcs", "subMenu"]).omit, backgroundColor: params.backgroundColor ??"transparent", /// a bit hacky. if an onClick is specified, then assume a toggle uses onClick to get the backgroundColor (see below). Otherwise, assume a transparent background - color: Colors.WHITE, isSystem: true, //dontUndo: true, + color: Colors.WHITE, isSystem: true, _nativeWidth: params.width ?? 30, _width: params.width ?? 30, _height: 30, _nativeHeight: 30, linearBtnWidth: params.linearBtnWidth, toolType: params.toolType, expertMode: params.expertMode, - _stayInCollection: true, _layout_hideContextMenu: true, _lockedPosition: true, - _removeDropProperties: new List<string>([ "_stayInCollection"]), + _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, _lockedPosition: true, }; const reqdFuncs:{[key:string]:any} = { ...params.funcs, @@ -732,14 +729,14 @@ export class CurrentUserUtils { /// Initializes all the default buttons for the top bar context menu static setupContextMenuButtons(doc: Doc, field="myContextMenuBtns") { - const reqdCtxtOpts:DocumentOptions = { title: "context menu buttons", dontUndo:true, flexGap: 0, childDropAction: 'embed', childDontRegisterViews: true, linearView_IsExpanded: true, ignoreClick: true, linearView_Expandable: false, _height: 35 }; + const reqdCtxtOpts:DocumentOptions = { title: "context menu buttons", undoIgnoreFields:new List<string>(['width', "linearView_IsExpanded"]), flexGap: 0, childDragAction: 'embed', childDontRegisterViews: true, linearView_IsExpanded: true, ignoreClick: true, linearView_Expandable: false, _height: 35 }; const ctxtMenuBtnsDoc = DocUtils.AssignDocField(doc, field, (opts, items) => this.linearButtonList(opts, items??[]), reqdCtxtOpts, undefined); const ctxtMenuBtns = CurrentUserUtils.contextMenuTools().map(params => { const menuBtnDoc = DocListCast(ctxtMenuBtnsDoc?.data).find(doc => doc.title === params.title); if (!params.subMenu) { return this.setupContextMenuButton(params, menuBtnDoc); } else { - const reqdSubMenuOpts = { ...OmitKeys(params, ["scripts", "funcs", "subMenu"]).omit, dontUndo: true, + const reqdSubMenuOpts = { ...OmitKeys(params, ["scripts", "funcs", "subMenu"]).omit, undoIgnoreFields: new List<string>(['width', "linearView_IsExpanded"]), childDontRegisterViews: true, flexGap: 0, _height: 30, ignoreClick: params.scripts?.onClick ? false : true, linearView_SubMenu: true, linearView_Expandable: true, }; const items = params.subMenu?.map(sub => @@ -786,13 +783,13 @@ export class CurrentUserUtils { const sharedDocOpts:DocumentOptions = { title: "My Shared Docs", userColor: "rgb(202, 202, 202)", - isFolder:true, + isFolder:true, undoIgnoreFields:new List<string>(['treeViewSortCriterion']), childContextMenuFilters: new List<ScriptField>([dashboardFilter!,]), childContextMenuScripts: new List<ScriptField>([addToDashboards!,]), childContextMenuLabels: new List<string>(["Add to Dashboards",]), childContextMenuIcons: new List<string>(["user-plus",]), "acl-Public": SharingPermissions.Augment, "_acl-Public": SharingPermissions.Augment, - childDropAction: "embed", isSystem: true, contentPointerEvents: "all", childLimitHeight: 0, _yMargin: 0, _gridGap: 15, childDontRegisterViews:true, + childDragAction: "embed", isSystem: true, contentPointerEvents: "all", childLimitHeight: 0, _yMargin: 0, _gridGap: 15, childDontRegisterViews:true, // NOTE: treeViewHideTitle & _layout_showTitle is for a TreeView's editable title, _layout_showTitle is for DocumentViews title bar _layout_showTitle: "title", treeViewHideTitle: true, ignoreClick: true, _lockedPosition: true, layout_boxShadow: "0 0", _chromeHidden: true, dontRegisterView: true, layout_explainer: "This is where documents or dashboards that other users have shared with you will appear. To share a document or dashboard right click and select 'Share'" @@ -805,14 +802,14 @@ export class CurrentUserUtils { static setupImportSidebar(doc: Doc, field:string) { const reqdOpts:DocumentOptions = { title: "My Imports", _forceActive: true, ignoreClick: true, _layout_showTitle: "title", - _stayInCollection: true, _layout_hideContextMenu: true, childLimitHeight: 0, - childDropAction: "copy", _layout_autoHeight: true, _yMargin: 50, _gridGap: 15, layout_boxShadow: "0 0", _lockedPosition: true, isSystem: true, _chromeHidden: true, + _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, childLimitHeight: 0, + childDragAction: "copy", _layout_autoHeight: true, _yMargin: 50, _gridGap: 15, layout_boxShadow: "0 0", _lockedPosition: true, isSystem: true, _chromeHidden: true, dontRegisterView: true, layout_explainer: "This is where documents that are Imported into Dash will go." }; const myImports = DocUtils.AssignDocField(doc, field, (opts) => Docs.Create.StackingDocument([], opts), reqdOpts); const reqdBtnOpts:DocumentOptions = { _forceActive: true, toolTip: "Import from computer", - _width: 30, _height: 30, _stayInCollection: true, _layout_hideContextMenu: true, title: "Import", btnType: ButtonType.ClickButton, + _width: 30, _height: 30, _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, title: "Import", btnType: ButtonType.ClickButton, buttonText: "Import", icon: "upload", isSystem: true }; DocUtils.AssignDocField(myImports, "layout_headerButton", (opts) => Docs.Create.FontIconDocument(opts), reqdBtnOpts, undefined, { onClick: "importDocument()" }); return myImports; @@ -864,7 +861,7 @@ export class CurrentUserUtils { this.setupDocTemplates(doc); // sets up the template menu of templates this.setupFieldInfos(doc); // sets up the collection of field info descriptions for each possible DocumentOption DocUtils.AssignDocField(doc, "globalScriptDatabase", (opts) => Docs.Prototypes.MainScriptDocument(), {}); - DocUtils.AssignDocField(doc, "myHeaderBar", (opts) => Docs.Create.MulticolumnDocument([], opts), { title: "header bar", isSystem: true }); // drop down panel at top of dashboard for stashing documents + DocUtils.AssignDocField(doc, "myHeaderBar", (opts) => Docs.Create.MulticolumnDocument([], opts), { title: "header bar", isSystem: true, childDocumentsActive:false, dropAction: 'move'}); // drop down panel at top of dashboard for stashing documents Doc.AddDocToList(Doc.MyFilesystem, undefined, Doc.MySharedDocs) diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index 612f16ce9..3f0848d00 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -25,7 +25,13 @@ export class DocumentManager { @observable public RecordingEvent = 0; @observable public LinkedDocumentViews: { a: DocumentView; b: DocumentView; l: Doc }[] = []; @computed public get DocumentViews() { - return Array.from(this._documentViews).filter(view => !(view.ComponentView instanceof KeyValueBox)); + return Array.from(this._documentViews).filter(view => !(view.ComponentView instanceof KeyValueBox) && (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(view.docViewPath))); + } + public AddDocumentView(dv: DocumentView) { + this._documentViews.add(dv); + } + public DeleteDocumentView(dv: DocumentView) { + this._documentViews.delete(dv); } private static _instance: DocumentManager; @@ -83,7 +89,7 @@ export class DocumentManager { // this.LinkedDocumentViews.forEach(view => console.log(" LV = " + view.a.props.Document.title + "/" + view.a.props.LayoutTemplateString + " --> " + // view.b.props.Document.title + "/" + view.b.props.LayoutTemplateString)); } else { - this._documentViews.add(view); + this.AddDocumentView(view); } this.callAddViewFuncs(view); }; @@ -101,7 +107,7 @@ export class DocumentManager { const index = this.LinkAnchorBoxViews.indexOf(view); this.LinkAnchorBoxViews.splice(index, 1); } else { - this._documentViews.delete(view); + this.DeleteDocumentView(view); } SelectionManager.DeselectView(view); }); @@ -228,7 +234,7 @@ export class DocumentManager { public showDocumentView = async (targetDocView: DocumentView, options: DocFocusOptions) => { const docViewPath = targetDocView.docViewPath.slice(); let rootContextView = docViewPath.shift(); - await (rootContextView && this.focusViewsInPath(rootContextView, options, async () => ({ childDocView: docViewPath.shift(), viewSpec: undefined }))); + await (rootContextView && this.focusViewsInPath(rootContextView, options, async () => ({ childDocView: docViewPath.shift(), viewSpec: undefined, focused: false }))); if (options.toggleTarget && (!options.didMove || targetDocView.rootDoc.hidden)) targetDocView.rootDoc.hidden = !targetDocView.rootDoc.hidden; else if (options.openLocation?.startsWith(OpenWhere.toggle) && !options.didMove && rootContextView) DocumentViewInternal.addDocTabFunc(rootContextView.rootDoc, options.openLocation); }; @@ -242,20 +248,22 @@ export class DocumentManager { public showDocument = async ( targetDoc: Doc, // document to display options: DocFocusOptions, // options for how to navigate to target - finished?: () => void + finished?: (changed: boolean) => void // func called after focusing on target with flag indicating whether anything needed to be done. ) => { const docContextPath = DocumentManager.GetContextPath(targetDoc, true); if (docContextPath.some(doc => doc.hidden)) options.toggleTarget = false; - let rootContextView = await new Promise<DocumentView>(res => { - const viewIndex = docContextPath.findIndex(doc => this.getDocumentView(doc)); - if (viewIndex !== -1) { - viewIndex && docContextPath.splice(0, viewIndex); - return res(this.getDocumentView(docContextPath[0])!); - } - options.didMove = true; - docContextPath.some(doc => TabDocView.Activate(doc)) || DocumentViewInternal.addDocTabFunc(docContextPath[0], options.openLocation ?? OpenWhere.addRight); - this.AddViewRenderedCb(docContextPath[0], dv => res(dv)); - }); + let rootContextView = + docContextPath.length && + (await new Promise<DocumentView>(res => { + const viewIndex = docContextPath.findIndex(doc => this.getDocumentView(doc)); + if (viewIndex !== -1) { + viewIndex && docContextPath.splice(0, viewIndex); + return res(this.getDocumentView(docContextPath[0])!); + } + options.didMove = true; + docContextPath.some(doc => TabDocView.Activate(doc)) || DocumentViewInternal.addDocTabFunc(docContextPath[0], options.openLocation ?? OpenWhere.addRight); + this.AddViewRenderedCb(docContextPath[0], dv => res(dv)); + })); if (options.openLocation === OpenWhere.lightbox) { // even if we found the document view, if the target is a lightbox, we try to open it in the lightbox to preserve lightbox semantics (eg, there's only one active doc in the lightbox) const target = DocCast(targetDoc.annotationOn, targetDoc); @@ -267,21 +275,29 @@ export class DocumentManager { docContextPath.shift(); const childViewIterator = async (docView: DocumentView) => { const innerDoc = docContextPath.shift(); - return { viewSpec: innerDoc, childDocView: innerDoc && !innerDoc.layout_unrendered ? (await docView.ComponentView?.getView?.(innerDoc)) ?? this.getDocumentView(innerDoc) : undefined }; + return { focused: false, viewSpec: innerDoc, childDocView: innerDoc && !innerDoc.layout_unrendered ? (await docView.ComponentView?.getView?.(innerDoc)) ?? this.getDocumentView(innerDoc) : undefined }; }; - const target = await this.focusViewsInPath(rootContextView, options, childViewIterator); - this.restoreDocView(target.viewSpec, target.docView, options, target.contextView ?? target.docView, targetDoc); - finished?.(); + if (rootContextView) { + const target = await this.focusViewsInPath(rootContextView, options, childViewIterator); + this.restoreDocView(target.viewSpec, target.docView, options, target.contextView ?? target.docView, targetDoc); + finished?.(target.focused); + } else finished?.(false); }; - focusViewsInPath = async (docView: DocumentView, options: DocFocusOptions, iterator: (docView: DocumentView) => Promise<{ viewSpec: Opt<Doc>; childDocView: Opt<DocumentView> }>) => { + focusViewsInPath = async ( + docView: DocumentView, // + options: DocFocusOptions, + iterator: (docView: DocumentView) => Promise<{ viewSpec: Opt<Doc>; childDocView: Opt<DocumentView>; focused: boolean }> + ) => { let contextView: DocumentView | undefined; // view containing context that contains target + let focused = false; while (true) { docView.rootDoc.layout_fieldKey === 'layout_icon' ? await new Promise<void>(res => docView.iconify(res)) : undefined; - docView.props.focus(docView.rootDoc, options); // focus the view within its container + const nextFocus = docView.props.focus(docView.rootDoc, options); // focus the view within its container + focused = focused || (nextFocus === undefined ? false : true); // keep track of whether focusing on a view needed to actually change anything const { childDocView, viewSpec } = await iterator(docView); - if (!childDocView) return { viewSpec: options.anchorDoc ?? viewSpec ?? docView.rootDoc, docView, contextView }; + if (!childDocView) return { viewSpec: options.anchorDoc ?? viewSpec ?? docView.rootDoc, docView, contextView, focused }; contextView = docView; docView = childDocView; } diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 87833d322..85101fcab 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -15,42 +15,36 @@ import { SelectionManager } from './SelectionManager'; import { SnappingManager } from './SnappingManager'; import { UndoManager } from './UndoManager'; -export type dropActionType = 'embed' | 'copy' | 'move' | 'same' | 'proto' | 'none' | undefined; // undefined = move, "same" = move but don't call removeDropProperties +export type dropActionType = 'embed' | 'copy' | 'move' | 'same' | 'proto' | 'none' | undefined; // undefined = move, "same" = move but don't call dropPropertiesToRemove /** * Initialize drag * @param _reference: The HTMLElement that is being dragged * @param docFunc: The Dash document being moved - * @param moveFunc: The function called when the document is moved - * @param dropAction: What to do with the document when it is dropped - * @param dragStarted: Method to call when the drag is started */ -export function SetupDrag(_reference: React.RefObject<HTMLElement>, docFunc: () => Doc | Promise<Doc | undefined> | undefined, moveFunc?: DragManager.MoveFunction, dropAction?: dropActionType, dragStarted?: () => void) { - const onRowMove = async (e: PointerEvent) => { +export function SetupDrag(_reference: React.RefObject<HTMLElement>, docFunc: () => Doc | undefined) { + const onRowMove = (e: PointerEvent) => { e.stopPropagation(); e.preventDefault(); document.removeEventListener('pointermove', onRowMove); document.removeEventListener('pointerup', onRowUp); - const doc = await docFunc(); + const doc = docFunc(); if (doc) { const dragData = new DragManager.DocumentDragData([doc]); - dragData.dropAction = dropAction; - dragData.moveDocument = moveFunc; DragManager.StartDocumentDrag([_reference.current!], dragData, e.x, e.y); - dragStarted?.(); } }; const onRowUp = (): void => { document.removeEventListener('pointermove', onRowMove); document.removeEventListener('pointerup', onRowUp); }; - const onItemDown = async (e: React.PointerEvent) => { + const onItemDown = (e: React.PointerEvent) => { if (e.button === 0) { e.stopPropagation(); if (e.shiftKey) { e.persist(); - const dragDoc = await docFunc(); + const dragDoc = docFunc(); dragDoc && DragManager.StartWindowDrag?.(e, [dragDoc]); } else { document.addEventListener('pointermove', onRowMove); @@ -132,7 +126,7 @@ export namespace DragManager { userDropAction: dropActionType; // the user requested drop action -- this will be honored as specified by modifier keys defaultDropAction?: dropActionType; // an optionally specified default drop action when there is no user drop actionl - this will be honored if there is no user drop action dropAction: dropActionType; // a drop action request by the initiating code. the actual drop action may be different -- eg, if the request is 'embed', but the document is dropped within the same collection, the drop action will be switched to 'move' - removeDropProperties?: string[]; + dropPropertiesToRemove?: string[]; moveDocument?: MoveFunction; removeDocument?: RemoveFunction; isDocDecorationMove?: boolean; // Flags that Document decorations are used to drag document which allows suppression of onDragStart scripts @@ -189,7 +183,7 @@ export namespace DragManager { const handler = (e: Event) => dropFunc(e, (e as CustomEvent<DropEvent>).detail); const preDropHandler = (e: Event) => { const de = (e as CustomEvent<DropEvent>).detail; - (preDropFunc ?? defaultPreDropFunc)(e, de, StrCast(doc?.targetDropAction) as dropActionType); + (preDropFunc ?? defaultPreDropFunc)(e, de, StrCast(doc?.dropAction) as dropActionType); }; element.addEventListener('dashOnDrop', handler); doc && element.addEventListener('dashPreDrop', preDropHandler); @@ -230,11 +224,13 @@ export namespace DragManager { ) ).filter(d => d); !['same', 'proto'].includes(docDragData.dropAction as any) && - docDragData.droppedDocuments.forEach((drop: Doc, i: number) => { - const dragProps = StrListCast(dragData.draggedDocuments[i].removeDropProperties); - const remProps = (dragData?.removeDropProperties || []).concat(Array.from(dragProps)); - remProps.map(prop => (drop[prop] = undefined)); - }); + docDragData.droppedDocuments + // .filter(drop => !drop.dragOnlyWithinContainer || ['embed', 'copy'].includes(docDragData.dropAction as any)) + .forEach((drop: Doc, i: number) => { + const dragProps = StrListCast(dragData.draggedDocuments[i].dropPropertiesToRemove); + const remProps = (dragData?.dropPropertiesToRemove || []).concat(Array.from(dragProps)); + [...remProps, 'dropPropertiesToRemove'].map(prop => (drop[prop] = undefined)); + }); } return e; }; @@ -586,6 +582,7 @@ export namespace DragManager { async function dispatchDrag(target: Element, e: PointerEvent, complete: DragCompleteEvent, pos: { x: number; y: number }, finishDrag?: (e: DragCompleteEvent) => void, options?: DragOptions, endDrag?: () => void) { const dropArgs = { + cancelable: true, // allows preventDefault() to be called to cancel the drop bubbles: true, detail: { ...pos, @@ -598,8 +595,9 @@ export namespace DragManager { }, }; target.dispatchEvent(new CustomEvent<DropEvent>('dashPreDrop', dropArgs)); + UndoManager.StartTempBatch(); // run drag/drop in temp batch in case drop is not allowed (so we can undo any intermediate changes) await finishDrag?.(complete); - target.dispatchEvent(new CustomEvent<DropEvent>('dashOnDrop', dropArgs)); + UndoManager.EndTempBatch(target.dispatchEvent(new CustomEvent<DropEvent>('dashOnDrop', dropArgs))); // event return val is true unless the event preventDefault() is called options?.dragComplete?.(complete); endDrag?.(); } diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts index f46ea393a..47997cc5c 100644 --- a/src/client/util/DropConverter.ts +++ b/src/client/util/DropConverter.ts @@ -57,11 +57,11 @@ export function convertDropDataToButtons(data: DragManager.DocumentDragData) { let dbox = doc; // bcz: isButtonBar is intended to allow a collection of linear buttons to be dropped and nested into another collection of buttons... it's not being used yet, and isn't very elegant if (doc.type === DocumentType.FONTICON || StrCast(Doc.Layout(doc).layout).includes('FontIconBox')) { - if (data.removeDropProperties || dbox.removeDropProperties) { + if (data.dropPropertiesToRemove || dbox.dropPropertiesToRemove) { //dbox = Doc.MakeEmbedding(doc); // don't need to do anything if dropping an icon doc onto an icon bar since there should be no layout data for an icon dbox = Doc.MakeEmbedding(dbox); - const dragProps = Cast(dbox.removeDropProperties, listSpec('string'), []); - const remProps = (data.removeDropProperties || []).concat(Array.from(dragProps)); + const dragProps = Cast(dbox.dropPropertiesToRemove, listSpec('string'), []); + const remProps = (data.dropPropertiesToRemove || []).concat(Array.from(dragProps)); remProps.map(prop => (dbox[prop] = undefined)); } } else if (!doc.onDragStart && !doc.isButtonBar) { @@ -81,7 +81,7 @@ export function convertDropDataToButtons(data: DragManager.DocumentDragData) { icon: layoutDoc.isTemplateDoc ? 'font' : 'bolt', }); dbox.dragFactory = layoutDoc; - dbox.removeDropProperties = doc.removeDropProperties instanceof ObjectField ? ObjectField.MakeCopy(doc.removeDropProperties) : undefined; + dbox.dropPropertiesToRemove = doc.dropPropertiesToRemove instanceof ObjectField ? ObjectField.MakeCopy(doc.dropPropertiesToRemove) : undefined; dbox.onDragStart = ScriptField.MakeFunction('makeDelegate(this.dragFactory)'); } else if (doc.isButtonBar) { dbox.ignoreClick = true; diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts index 3aa72f501..ce422f849 100644 --- a/src/client/util/LinkManager.ts +++ b/src/client/util/LinkManager.ts @@ -194,10 +194,8 @@ export class LinkManager { public static getOppositeAnchor(linkDoc: Doc, anchor: Doc): Doc | undefined { const a1 = Cast(linkDoc.link_anchor_1, Doc, null); const a2 = Cast(linkDoc.link_anchor_2, Doc, null); - if (Doc.AreProtosEqual(anchor, a1)) return a2; - if (Doc.AreProtosEqual(anchor, a2)) return a1; - if (Doc.AreProtosEqual(anchor, a1.annotationOn as Doc)) return a2; - if (Doc.AreProtosEqual(anchor, a2.annotationOn as Doc)) return a1; + if (Doc.AreProtosEqual(DocCast(anchor.annotationOn, anchor), DocCast(a1.annotationOn, a1))) return a2; + if (Doc.AreProtosEqual(DocCast(anchor.annotationOn, anchor), DocCast(a2.annotationOn, a2))) return a1; if (Doc.AreProtosEqual(anchor, linkDoc)) return linkDoc; } } diff --git a/src/client/util/PingManager.ts b/src/client/util/PingManager.ts new file mode 100644 index 000000000..0c41a1ea7 --- /dev/null +++ b/src/client/util/PingManager.ts @@ -0,0 +1,37 @@ +import { action, observable } from 'mobx'; +import { Networking } from '../Network'; +export class PingManager { + // create static instance and getter for global use + @observable static _instance: PingManager; + static get Instance(): PingManager { + return PingManager._instance; + } + + @observable IsBeating = true; + private setIsBeating = action((status: boolean) => { + this.IsBeating = status; + setTimeout(this.showAlert, 100); + }); + + showAlert = () => { + alert(PingManager.Instance.IsBeating ? 'The server connection is active' : 'The server connection has been interrupted.NOTE: Any changes made will appear to persist but will be lost after a browser refreshes.'); + }; + sendPing = async (): Promise<void> => { + try { + await Networking.PostToServer('/ping', { date: new Date() }); + !this.IsBeating && this.setIsBeating(true); + } catch { + if (this.IsBeating) { + this.setIsBeating(false); + } + } + }; + + // not used now, but may need to clear interval + private _interval: NodeJS.Timeout | null = null; + INTERVAL_SECONDS = 1; + constructor() { + PingManager._instance = this; + this._interval = setInterval(this.sendPing, this.INTERVAL_SECONDS * 1000); + } +} diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts index d4532c8ef..b59af6656 100644 --- a/src/client/util/UndoManager.ts +++ b/src/client/util/UndoManager.ts @@ -1,4 +1,5 @@ import { observable, action, runInAction } from 'mobx'; +import { Field } from '../../fields/Doc'; import { Without } from '../../Utils'; function getBatchName(target: any, key: string | symbol): string { @@ -91,11 +92,11 @@ export namespace UndoManager { let currentBatch: UndoBatch | undefined; export let batchCounter = observable.box(0); let undoing = false; - let tempEvents: UndoEvent[] | undefined = undefined; + export let tempEvents: UndoEvent[] | undefined = undefined; export function AddEvent(event: UndoEvent, value?: any): void { if (currentBatch && batchCounter.get() && !undoing) { - // console.log(' '.slice(0, batchCounter.get()) + 'UndoEvent : ' + event.prop + ' = ' + value); + console.log(' '.slice(0, batchCounter.get()) + 'UndoEvent : ' + event.prop + ' = ' + (value instanceof Array ? value.map(val => Field.toScriptString(val)).join(',') : Field.toScriptString(value))); currentBatch.push(event); tempEvents?.push(event); } @@ -187,15 +188,11 @@ export namespace UndoManager { return false; }); - export function RunInTempBatch<T>(fn: () => T) { + export function StartTempBatch() { tempEvents = []; - try { - const success = runInAction(fn); - if (!success) UndoManager.UndoTempBatch(); - return success; - } finally { - tempEvents = undefined; - } + } + export function EndTempBatch<T>(success: boolean) { + UndoManager.UndoTempBatch(success); } //TODO Make this return the return value export function RunInBatch<T>(fn: () => T, batchName: string) { @@ -206,10 +203,11 @@ export namespace UndoManager { batch.end(); } } - export const UndoTempBatch = action(() => { - if (tempEvents) { + export const UndoTempBatch = action((success: any) => { + if (tempEvents && !success) { undoing = true; for (let i = tempEvents.length - 1; i >= 0; i--) { + currentBatch?.includes(tempEvents[i]) && currentBatch.splice(currentBatch.indexOf(tempEvents[i])); tempEvents[i].undo(); } undoing = false; diff --git a/src/client/views/DashboardView.tsx b/src/client/views/DashboardView.tsx index 50cf2226e..f6d843201 100644 --- a/src/client/views/DashboardView.tsx +++ b/src/client/views/DashboardView.tsx @@ -347,7 +347,8 @@ export class DashboardView extends React.Component { }, ], }; - Doc.SetInPlace(dashboard, 'dockingConfig', JSON.stringify(reset), true); + if (dashboard.dockingConfig && dashboard.dockingConfig !== Doc.GetProto(dashboard).dockingConfig) dashboard.dockingConfig = JSON.stringify(reset); + else Doc.SetInPlace(dashboard, 'dockingConfig', JSON.stringify(reset), true); return reset; }; @@ -389,7 +390,7 @@ export class DashboardView extends React.Component { _forceActive: true, _width: 30, _height: 30, - _stayInCollection: true, + _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, title: 'New trail', toolTip: 'Create new trail', @@ -412,7 +413,7 @@ export class DashboardView extends React.Component { _layout_fitWidth: true, _gridGap: 5, _forceActive: true, - childDropAction: 'embed', + childDragAction: 'embed', treeViewTruncateTitleWidth: 150, ignoreClick: true, layout_headerButton: myTrailsBtn, @@ -421,7 +422,7 @@ export class DashboardView extends React.Component { _lockedPosition: true, layout_boxShadow: '0 0', childDontRegisterViews: true, - targetDropAction: 'same', + dropAction: 'same', isSystem: true, layout_explainer: 'All of the trails that you have created will appear here.', }; diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index aa8b145be..f3aa8451a 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -173,8 +173,8 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>() return true; } const first = doc instanceof Doc ? doc : doc[0]; - if (!first?._stayInCollection && addDocument !== returnFalse) { - return UndoManager.RunInTempBatch(() => this.removeDocument(doc, annotationKey, true) && addDocument(doc, annotationKey)); + if (!first?._dragOnlyWithinContainer && addDocument !== returnFalse) { + return this.removeDocument(doc, annotationKey, false) && addDocument(doc, annotationKey); } return false; }; @@ -227,7 +227,7 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>() .map(doc => { // only make a pushpin if we have acl's to edit the document //DocUtils.LeavePushpin(doc); - doc._stayInCollection = undefined; + doc._dragOnlyWithinContainer = undefined; doc.embedContainer = this.props.Document; if (annotationKey ?? this._annotationKeySuffix()) Doc.GetProto(doc).annotationOn = this.rootDoc; const parent = DocCast(doc.embedContainer); @@ -243,7 +243,7 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>() }); const annoDocs = targetDataDoc[annotationKey ?? this.annotationKey] as List<Doc>; - if (annoDocs instanceof List) annoDocs.push(...added); + 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(new Date(Date.now())); } diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index 35b0b22a8..16f5ad168 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -464,6 +464,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV action(() => { this._isRecording = false; this._stopFunc(); + b.end(); }), emptyFunction ); diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 964aada53..4454a3ec1 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -620,7 +620,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P let actualdH = Math.max(docheight + dH * scale, 20); let dX = !dWin ? 0 : scale * refCent[0] * (1 - (1 + dWin / refWidth)); let dY = !dHin ? 0 : scale * refCent[1] * (1 - (1 + dHin / refHeight)); - const preserveNativeDim = doc._nativeHeightUnfrozen === false && doc._nativeDimModifiable === false; + const preserveNativeDim = !doc._nativeHeightUnfrozen && !doc._nativeDimModifiable; const fixedAspect = nwidth && nheight && (!doc._layout_fitWidth || preserveNativeDim || e.ctrlKey || doc.nativeHeightUnfrozen || doc.nativeDimModifiable); if (fixedAspect) { if ((Math.abs(dW) > Math.abs(dH) && ((!dragBottom && !dragTop) || !modifyNativeDim)) || dragRight) { @@ -653,7 +653,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P } } if (!modifyNativeDim) { - actualdH = (nheight / nwidth) * docwidth; //, actualdH); + actualdH = (nheight / nwidth) * NumCast(doc._width); //, actualdH); } doc._height = actualdH; } @@ -780,7 +780,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P hideDecorations || seldocview.props.hideOpenButton || seldocview.rootDoc.layout_hideOpenButton || - SelectionManager.Views().some(docView => docView.rootDoc._stayInCollection || docView.rootDoc.isGroup || docView.rootDoc.layout_hideOpenButton) || + SelectionManager.Views().some(docView => docView.rootDoc._dragOnlyWithinContainer || docView.rootDoc.isGroup || docView.rootDoc.layout_hideOpenButton) || this._isRounding || this._isRotating; const hideDeleteButton = @@ -791,7 +791,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P seldocview.rootDoc.hideDeleteButton || SelectionManager.Views().some(docView => { const collectionAcl = docView.props.docViewPath()?.lastElement() ? GetEffectiveAcl(docView.props.docViewPath().lastElement().rootDoc[DocData]) : AclEdit; - return (docView.rootDoc.stayInCollection && !docView.rootDoc._isTimelineLabel) || (collectionAcl !== AclAdmin && collectionAcl !== AclEdit && GetEffectiveAcl(docView.rootDoc) !== AclAdmin); + return collectionAcl !== AclAdmin && collectionAcl !== AclEdit && GetEffectiveAcl(docView.rootDoc) !== AclAdmin; }); const topBtn = (key: string, icon: string, pointerDown: undefined | ((e: React.PointerEvent) => void), click: undefined | ((e: any) => void), title: string) => ( <Tooltip key={key} title={<div className="dash-tooltip">{title}</div>} placement="top"> @@ -962,7 +962,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P }}> {this._isRotating ? null : ( <Tooltip enterDelay={750} title={<div className="dash-tooltip">tap to set rotate center, drag to rotate</div>}> - <div className="documentDecorations-rotation" style={{ pointerEvents: 'all' }} onPointerDown={this.onRotateDown} onContextMenu={e => e.preventDefault()}> + <div className="documentDecorations-rotation" style={{ pointerEvents: 'all', color: 'blue' }} onPointerDown={this.onRotateDown} onContextMenu={e => e.preventDefault()}> <IconButton icon={<FaUndo />} color={Colors.LIGHT_GRAY} /> </div> </Tooltip> diff --git a/src/client/views/FilterPanel.tsx b/src/client/views/FilterPanel.tsx index fe42628cd..a5c18cd8b 100644 --- a/src/client/views/FilterPanel.tsx +++ b/src/client/views/FilterPanel.tsx @@ -71,7 +71,7 @@ export class FilterPanel extends React.Component<filterProps> { * @returns a string array of the current attributes */ @computed get currentFacets() { - return this.activeFilters.map(filter => filter.split(':')[0]); + return this.activeFilters.map(filter => filter.split(Doc.FilterSep)[0]); } gatherFieldValues(childDocs: Doc[], facetKey: string) { @@ -108,8 +108,8 @@ export class FilterPanel extends React.Component<filterProps> { @observable _chosenFacets = new ObservableMap<string, 'text' | 'checkbox' | 'slider' | 'range'>(); @computed get activeFacets() { const facets = new Map<string, 'text' | 'checkbox' | 'slider' | 'range'>(this._chosenFacets); - StrListCast(this.targetDoc?._childFilters).map(filter => facets.set(filter.split(':')[0], filter.split(':')[2] === 'match' ? 'text' : 'checkbox')); - setTimeout(() => StrListCast(this.targetDoc?._childFilters).map(action(filter => this._chosenFacets.set(filter.split(':')[0], filter.split(':')[2] === 'match' ? 'text' : 'checkbox')))); + StrListCast(this.targetDoc?._childFilters).map(filter => facets.set(filter.split(Doc.FilterSep)[0], filter.split(Doc.FilterSep)[2] === 'match' ? 'text' : 'checkbox')); + setTimeout(() => StrListCast(this.targetDoc?._childFilters).map(action(filter => this._chosenFacets.set(filter.split(Doc.FilterSep)[0], filter.split(Doc.FilterSep)[2] === 'match' ? 'text' : 'checkbox')))); return facets; } /** @@ -203,8 +203,8 @@ export class FilterPanel extends React.Component<filterProps> { <input placeholder={ StrListCast(this.targetDoc._childFilters) - .find(filter => filter.split(':')[0] === facetHeader) - ?.split(':')[1] ?? '-empty-' + .find(filter => filter.split(Doc.FilterSep)[0] === facetHeader) + ?.split(Doc.FilterSep)[1] ?? '-empty-' } onBlur={undoable(e => Doc.setDocFilter(this.targetDoc, facetHeader, e.currentTarget.value, !e.currentTarget.value ? 'remove' : 'match'), 'set text filter')} onKeyDown={e => e.key === 'Enter' && undoable(e => Doc.setDocFilter(this.targetDoc, facetHeader, e.currentTarget.value, !e.currentTarget.value ? 'remove' : 'match'), 'set text filter')(e)} @@ -219,8 +219,8 @@ export class FilterPanel extends React.Component<filterProps> { style={{ width: 20, marginLeft: 20 }} checked={ StrListCast(this.targetDoc._childFilters) - .find(filter => filter.split(':')[0] === facetHeader && filter.split(':')[1] == facetValue) - ?.split(':')[2] === 'check' + .find(filter => filter.split(Doc.FilterSep)[0] === facetHeader && filter.split(Doc.FilterSep)[1] == facetValue) + ?.split(Doc.FilterSep)[2] === 'check' } type={type} onChange={undoable(e => Doc.setDocFilter(this.targetDoc, facetHeader, fval, e.target.checked ? 'check' : 'remove'), 'set filter')} diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx index 0951bff22..54ad519de 100644 --- a/src/client/views/GestureOverlay.tsx +++ b/src/client/views/GestureOverlay.tsx @@ -4,8 +4,7 @@ import { action, computed, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import { Doc, Opt } from '../../fields/Doc'; import { InkData, InkTool } from '../../fields/InkField'; -import { ScriptField } from '../../fields/ScriptField'; -import { Cast, FieldValue, NumCast } from '../../fields/Types'; +import { NumCast } from '../../fields/Types'; import MobileInkOverlay from '../../mobile/MobileInkOverlay'; import { GestureUtils } from '../../pen-gestures/GestureUtils'; import { MobileInkOverlayContent } from '../../server/Message'; @@ -35,7 +34,6 @@ import { InkTranscription } from './InkTranscription'; import { checkInksToGroup } from './nodes/button/FontIconBox'; import { DocumentView } from './nodes/DocumentView'; import { RadialMenu } from './nodes/RadialMenu'; -import HorizontalPalette from './Palette'; import { Touchable } from './Touchable'; import TouchScrollableMenu, { TouchScrollableMenuItem } from './TouchScrollableMenu'; @@ -79,7 +77,6 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> { private _overlayRef = React.createRef<HTMLDivElement>(); private _d1: Doc | undefined; private _inkToTextDoc: Doc | undefined; - private _thumbDoc: Doc | undefined; private thumbIdentifier?: number; private pointerIdentifier?: number; private _hands: Map<number, React.Touch[]> = new Map<number, React.Touch[]>(); @@ -93,78 +90,12 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> { GestureOverlay.Instances.push(this); } - static setupThumbButtons(doc: Doc) { - const docProtoData: { title: string; icon: string; drag?: string; toolType?: string; ignoreClick?: boolean; pointerDown?: string; pointerUp?: string; clipboard?: Doc; backgroundColor?: string; dragFactory?: Doc }[] = [ - { title: 'use pen', icon: 'pen-nib', pointerUp: 'resetPen()', pointerDown: 'setPen(2, this.backgroundColor)', backgroundColor: 'blue' }, - { title: 'use highlighter', icon: 'highlighter', pointerUp: 'resetPen()', pointerDown: 'setPen(20, this.backgroundColor)', backgroundColor: 'yellow' }, - { - title: 'notepad', - icon: 'clipboard', - pointerUp: 'GestureOverlay.Instance.closeFloatingDoc()', - pointerDown: 'GestureOverlay.Instance.openFloatingDoc(this.clipboard)', - clipboard: Docs.Create.FreeformDocument([], { _width: 300, _height: 300, isSystem: true }), - backgroundColor: 'orange', - }, - { title: 'interpret text', icon: 'font', toolType: 'inktotext', pointerUp: "setToolglass('none')", pointerDown: 'setToolglass(self.toolType)', backgroundColor: 'orange' }, - { title: 'ignore gestures', icon: 'signature', toolType: 'ignoregesture', pointerUp: "setToolglass('none')", pointerDown: 'setToolglass(self.toolType)', backgroundColor: 'green' }, - ]; - return docProtoData.map(data => - Docs.Create.FontIconDocument({ - _nativeWidth: 10, - _nativeHeight: 10, - _width: 10, - _height: 10, - title: data.title, - icon: data.icon, - toolType: data.toolType, - _dropAction: data.pointerDown ? 'copy' : undefined, - ignoreClick: data.ignoreClick, - onDragStart: data.drag ? ScriptField.MakeFunction(data.drag) : undefined, - clipboard: data.clipboard, - onPointerUp: data.pointerUp ? ScriptField.MakeScript(data.pointerUp) : undefined, - onPointerDown: data.pointerDown ? ScriptField.MakeScript(data.pointerDown) : undefined, - backgroundColor: data.backgroundColor, - dragFactory: data.dragFactory, - isSystem: true, - }) - ); - } - - static setupThumbDoc(userDoc: Doc) { - if (!userDoc.thumbDoc) { - const thumbDoc = Docs.Create.LinearDocument(GestureOverlay.setupThumbButtons(userDoc), { - _width: 100, - _height: 50, - ignoreClick: true, - _lockedPosition: true, - title: 'buttons', - _layout_autoHeight: true, - _yMargin: 5, - linearView_IsExpanded: true, - backgroundColor: 'white', - isSystem: true, - }); - thumbDoc.inkToTextDoc = Docs.Create.LinearDocument([], { - _width: 300, - _height: 25, - _layout_autoHeight: true, - linearView_IsExpanded: true, - flexDirection: 'column', - isSystem: true, - }); - userDoc.thumbDoc = thumbDoc; - } - return Cast(userDoc.thumbDoc, Doc); - } - componentWillUnmount() { GestureOverlay.Instances.splice(GestureOverlay.Instances.indexOf(this), 1); GestureOverlay.Instance = GestureOverlay.Instances.lastElement(); } componentDidMount = () => { GestureOverlay.Instance = this; - this._thumbDoc = FieldValue(Cast(GestureOverlay.setupThumbDoc(Doc.UserDoc()), Doc)); - this._inkToTextDoc = FieldValue(Cast(this._thumbDoc?.inkToTextDoc, Doc)); }; // TODO: nda - add dragging groups with one finger drag and have to click into group to scroll within the group @@ -395,24 +326,6 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> { this.thumbIdentifier = thumb?.identifier; this._hands.set(thumb.identifier, fingers); - const others = fingers.filter(f => f !== thumb); - const minX = Math.min(...others.map(f => f.clientX)); - const minY = Math.min(...others.map(f => f.clientY)); - - // load up the palette collection around the thumb - const thumbDoc = await Cast(GestureOverlay.setupThumbDoc(Doc.UserDoc()), Doc); - if (thumbDoc) { - runInAction(() => { - RadialMenu.Instance._display = false; - this._inkToTextDoc = FieldValue(Cast(thumbDoc.inkToTextDoc, Doc)); - this._thumbDoc = thumbDoc; - this._thumbX = thumb.clientX; - this._thumbY = thumb.clientY; - this._menuX = thumb.clientX + 50; - this._menuY = thumb.clientY; - this._palette = <HorizontalPalette key="palette" x={minX} y={minY} thumb={[thumb.clientX, thumb.clientY]} thumbDoc={thumbDoc} />; - }); - } this.removeMoveListeners(); document.removeEventListener('touchmove', this.handleHandMove); @@ -462,11 +375,6 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> { if (Math.abs(pt.clientY - this._thumbY) > 10 * window.devicePixelRatio) { this._selectedIndex = Math.min(Math.max(-1, -Math.ceil((pt.clientY - this._thumbY) / (10 * window.devicePixelRatio)) - 1), this._possibilities.length - 1); } - } else if (this._thumbDoc) { - if (Math.abs(pt.clientX - this._thumbX) > 15 * window.devicePixelRatio) { - this._thumbDoc.selectedIndex = Math.max(-1, NumCast(this._thumbDoc.selectedIndex) - Math.sign(pt.clientX - this._thumbX)); - this._thumbX = pt.clientX; - } } } } @@ -485,7 +393,6 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> { if (this.thumbIdentifier) this._hands.delete(this.thumbIdentifier); this._palette = undefined; this.thumbIdentifier = undefined; - this._thumbDoc = undefined; // this chunk of code is for handling the ink to text toolglass let scriptWorked = false; diff --git a/src/client/views/InkTranscription.tsx b/src/client/views/InkTranscription.tsx index e38474ea0..c39fa79da 100644 --- a/src/client/views/InkTranscription.tsx +++ b/src/client/views/InkTranscription.tsx @@ -124,9 +124,9 @@ export class InkTranscription extends React.Component { const strokes: InkData[] = []; const times: number[] = []; validInks - .filter(i => Cast(i.data, InkField)) + .filter(i => Cast(i[Doc.LayoutFieldKey(i)], InkField)) .forEach(i => { - const d = Cast(i.data, InkField, null); + const d = Cast(i[Doc.LayoutFieldKey(i)], InkField, null); const inkStroke = DocumentManager.Instance.getDocumentView(i)?.ComponentView as InkingStroke; strokes.push(d.inkData.map(pd => inkStroke.ptToScreen({ X: pd.X, Y: pd.Y }))); times.push(DateCast(i.author_date).getDate().getTime()); diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index 7da9dc603..ed39cde13 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -15,6 +15,7 @@ import { TrackMovements } from '../util/TrackMovements'; import { CollectionView } from './collections/CollectionView'; import { MainView } from './MainView'; import * as dotenv from 'dotenv'; // see https://github.com/motdotla/dotenv#how-do-i-use-dotenv-with-import +import { PingManager } from '../util/PingManager'; dotenv.config(); AssignAllExtensions(); @@ -48,6 +49,7 @@ FieldLoader.ServerLoadStatus = { requested: 0, retrieved: 0 }; // bcz: not sure new LinkManager(); new TrackMovements(); new ReplayMovements(); + new PingManager(); root.render(<MainView />); }, 0); })(); diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index c827bc92a..258674d53 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -483,6 +483,8 @@ export class MainView extends React.Component { fa.faSquareRootAlt, fa.faVolumeMute, fa.faUserCircle, + fa.faHeart, + fa.faHeartBroken, fa.faHighlighter, fa.faRemoveFormat, fa.faHandPointUp, @@ -566,7 +568,7 @@ export class MainView extends React.Component { @action createNewFolder = async () => { - const folder = Docs.Create.TreeDocument([], { title: 'Untitled folder', _stayInCollection: true, isFolder: true }); + const folder = Docs.Create.TreeDocument([], { title: 'Untitled folder', _dragOnlyWithinContainer: true, isFolder: true }); Doc.AddDocToList(Doc.MyFilesystem, 'data', folder); }; @@ -577,6 +579,8 @@ export class MainView extends React.Component { waitForDoubleClick = () => (this._exploreMode ? 'never' : undefined); headerBarScreenXf = () => new Transform(-this.leftScreenOffsetOfMainDocView - this.leftMenuFlyoutWidth(), -this.headerBarDocHeight(), 1); mainScreenToLocalXf = () => new Transform(-this.leftScreenOffsetOfMainDocView - this.leftMenuFlyoutWidth(), -this.topOfMainDocContent, 1); + addHeaderDoc = (doc: Doc | Doc[], annotationKey?: string) => (doc instanceof Doc ? [doc] : doc).reduce((done, doc) => Doc.AddDocToList(this.headerBarDoc, 'data', doc), true); + removeHeaderDoc = (doc: Doc | Doc[], annotationKey?: string) => (doc instanceof Doc ? [doc] : doc).reduce((done, doc) => Doc.RemoveDocFromList(this.headerBarDoc, 'data', doc), true); @computed get headerBarDocView() { return ( <div className="mainView-headerBar" style={{ height: this.headerBarDocHeight() }}> @@ -584,18 +588,19 @@ export class MainView extends React.Component { key="headerBarDoc" Document={this.headerBarDoc} DataDoc={undefined} - addDocument={undefined} addDocTab={DocumentViewInternal.addDocTabFunc} pinToPres={emptyFunction} docViewPath={returnEmptyDoclist} styleProvider={DefaultStyleProvider} rootSelected={returnTrue} - removeDocument={returnFalse} + addDocument={this.addHeaderDoc} + removeDocument={this.removeHeaderDoc} fitContentsToBox={returnTrue} isDocumentActive={returnTrue} // headerBar is always documentActive (ie, the docView gets pointer events) isContentActive={returnTrue} // headerBar is awlays contentActive which means its items are always documentActive ScreenToLocalTransform={this.headerBarScreenXf} childHideResizeHandles={returnTrue} + childDragAction="move" dontRegisterView={true} hideResizeHandles={true} PanelWidth={this.headerBarDocWidth} @@ -860,7 +865,7 @@ export class MainView extends React.Component { this._leftMenuFlyoutWidth = 0; }); - remButtonDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg: boolean, doc) => flg && Doc.RemoveDocFromList(Doc.MyDockedBtns, 'data', doc), true); + remButtonDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg: boolean, doc) => flg && !doc.dragOnlyWithinContainer && Doc.RemoveDocFromList(Doc.MyDockedBtns, 'data', doc), true); moveButtonDoc = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => this.remButtonDoc(doc) && addDocument(doc); addButtonDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg: boolean, doc) => flg && Doc.AddDocToList(Doc.MyDockedBtns, 'data', doc), true); diff --git a/src/client/views/Palette.scss b/src/client/views/Palette.scss deleted file mode 100644 index 0ec879288..000000000 --- a/src/client/views/Palette.scss +++ /dev/null @@ -1,30 +0,0 @@ -.palette-container { - .palette-thumb { - touch-action: pan-x; - position: absolute; - height: 70px; - overflow: hidden; - - .palette-thumbContent { - transition: transform .3s; - width: max-content; - overflow: hidden; - - .collectionView { - overflow: visible; - - .collectionLinearView-outer { - overflow: visible; - } - } - } - - .palette-cover { - width: 50px; - height: 50px; - position: absolute; - bottom: 0; - border: 1px solid black; - } - } -}
\ No newline at end of file diff --git a/src/client/views/Palette.tsx b/src/client/views/Palette.tsx deleted file mode 100644 index 749eb08a2..000000000 --- a/src/client/views/Palette.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { IReactionDisposer, observable, reaction } from 'mobx'; -import { observer } from 'mobx-react'; -import * as React from 'react'; -import { Doc } from '../../fields/Doc'; -import { NumCast } from '../../fields/Types'; -import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnTrue, emptyPath } from '../../Utils'; -import { Transform } from '../util/Transform'; -import { DocumentView } from './nodes/DocumentView'; -import './Palette.scss'; - -export interface PaletteProps { - x: number; - y: number; - thumb: number[]; - thumbDoc: Doc; -} - -@observer -export default class Palette extends React.Component<PaletteProps> { - private _selectedDisposer?: IReactionDisposer; - @observable private _selectedIndex: number = 0; - - componentDidMount = () => { - this._selectedDisposer = reaction( - () => NumCast(this.props.thumbDoc.selectedIndex), - i => (this._selectedIndex = i), - { fireImmediately: true } - ); - }; - - componentWillUnmount = () => { - this._selectedDisposer?.(); - }; - - render() { - return ( - <div className="palette-container" style={{ transform: `translate(${this.props.x}px, ${this.props.y}px)` }}> - <div className="palette-thumb" style={{ transform: `translate(${this.props.thumb[0] - this.props.x}px, ${this.props.thumb[1] - this.props.y}px)` }}> - <div className="palette-thumbContent" style={{ transform: `translate(-${this._selectedIndex * 50 + 10}px, 0px)` }}> - <DocumentView - Document={this.props.thumbDoc} - DataDoc={undefined} - addDocument={undefined} - addDocTab={returnFalse} - rootSelected={returnTrue} - pinToPres={emptyFunction} - removeDocument={undefined} - ScreenToLocalTransform={Transform.Identity} - PanelWidth={() => window.screen.width} - PanelHeight={() => window.screen.height} - renderDepth={0} - isDocumentActive={returnTrue} - isContentActive={emptyFunction} - focus={emptyFunction} - docViewPath={returnEmptyDoclist} - styleProvider={returnEmptyString} - whenChildContentsActiveChanged={emptyFunction} - bringToFront={emptyFunction} - childFilters={returnEmptyFilter} - childFiltersByRanges={returnEmptyFilter} - searchFilterDocs={returnEmptyDoclist} - /> - <div className="palette-cover" style={{ transform: `translate(${Math.max(0, this._selectedIndex) * 50.75 + 23}px, 0px)` }}></div> - </div> - </div> - </div> - ); - } -} diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx index 11b89fd69..74dd1c2f7 100644 --- a/src/client/views/PropertiesButtons.tsx +++ b/src/client/views/PropertiesButtons.tsx @@ -209,6 +209,7 @@ export class PropertiesButtons extends React.Component<{}, {}> { (dv, doc) => { doc.isGroup = !doc.isGroup; doc.forceActive = doc.isGroup; + doc.dragWhenActive = doc.isGroup; } ); } diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx index 904755132..14291b537 100644 --- a/src/client/views/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -288,7 +288,6 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { pinToPres={emptyFunction} bringToFront={returnFalse} dontRegisterView={true} - dropAction={undefined} /> </div> ); diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx index 2cca2c5a9..9ba0e5e26 100644 --- a/src/client/views/StyleProvider.tsx +++ b/src/client/views/StyleProvider.tsx @@ -133,7 +133,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps return undefined; case StyleProp.DocContents:return undefined; case StyleProp.WidgetColor:return isAnnotated ? Colors.LIGHT_BLUE : darkScheme() ? 'lightgrey' : 'dimgrey'; - case StyleProp.Opacity: return props?.LayoutTemplateString?.includes(KeyValueBox.name) ? 1 : Cast(doc?._opacity, "number", Cast(doc?.opacity, 'number', null)); + case StyleProp.Opacity: return props?.LayoutTemplateString?.includes(KeyValueBox.name) ? 1 : doc?.textInlineAnnotations ? 0 : Cast(doc?._opacity, "number", Cast(doc?.opacity, 'number', null)); case StyleProp.HideLinkBtn:return props?.hideLinkButton || (!selected && doc?.layout_hideLinkButton); 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))); @@ -227,7 +227,9 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps ? '#00000010' // faint interior for collections on PDFs, images, etc : doc?._isGroup ? undefined - : Cast((props?.renderDepth || 0) > 0 ? Doc.UserDoc().activeCollectionNestedBackground : Doc.UserDoc().activeCollectionBackground, 'string') ?? (darkScheme() ? Colors.BLACK : 'linear-gradient(#065fff, #85c1f9)')); + : doc._type_collection === CollectionViewType.Stacking ? + (darkScheme() ? Colors.MEDIUM_GRAY : Colors.DARK_GRAY) + : Cast((props?.renderDepth || 0) > 0 ? Doc.UserDoc().activeCollectionNestedBackground : Doc.UserDoc().activeCollectionBackground, 'string') ?? (darkScheme() ? Colors.BLACK : Colors.MEDIUM_GRAY)); break; //if (doc._type_collection !== CollectionViewType.Freeform && doc._type_collection !== CollectionViewType.Time) return "rgb(62,62,62)"; default: docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : Colors.WHITE); @@ -265,6 +267,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps } case StyleProp.PointerEvents: const isInk = doc && StrCast(Doc.Layout(doc).layout).includes(InkingStroke.name) && !props?.LayoutTemplateString; + if (StrCast(doc?.pointerEvents) && !props?.LayoutTemplateString?.includes(KeyValueBox.name)) return StrCast(doc!.pointerEvents); // honor pointerEvents field (set by lock button usually) if it's not a keyValue view of the Doc if (docProps?.DocumentView?.().ComponentView?.overridePointerEvents?.() !== undefined) return docProps?.DocumentView?.().ComponentView?.overridePointerEvents?.(); if (MainView.Instance._exploreMode || doc?.layout_unrendered) return isInk ? 'visiblePainted' : 'all'; if (props?.contentPointerEvents) return StrCast(props.contentPointerEvents); diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx index 62f47ba71..5c2ab3f70 100644 --- a/src/client/views/TemplateMenu.tsx +++ b/src/client/views/TemplateMenu.tsx @@ -97,8 +97,8 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> { TraceMobx(); const firstDoc = this.props.docViews[0].props.Document; const templateName = StrCast(firstDoc.layout_fieldKey, 'layout').replace('layout_', ''); - const noteTypes = DocListCast(Cast(Doc.UserDoc()['template-notes'], Doc, null)?.data); - const addedTypes = DocListCast(Cast(Doc.UserDoc()['template-clickFuncs'], Doc, null)?.data); + const noteTypes = DocListCast(Cast(Doc.UserDoc()['template_notes'], Doc, null)?.data); + const addedTypes = DocListCast(Cast(Doc.UserDoc()['template_clickFuncs'], Doc, null)?.data); const templateMenu: Array<JSX.Element> = []; this.props.templates?.forEach((checked, template) => templateMenu.push(<TemplateToggle key={template} template={template} checked={checked} toggle={this.toggleTemplate} />)); templateMenu.push(<OtherToggle key={'default'} name={'Default'} checked={templateName === 'layout'} toggle={this.toggleDefault} />); @@ -122,7 +122,6 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> { rootSelected={returnFalse} onCheckedClick={this.scriptField} onChildClick={this.scriptField} - dropAction={undefined} isAnyChildContentActive={returnFalse} isContentActive={returnTrue} bringToFront={emptyFunction} diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx index 0eb61a0b2..ea02bcd4c 100644 --- a/src/client/views/collections/CollectionCarouselView.tsx +++ b/src/client/views/collections/CollectionCarouselView.tsx @@ -4,7 +4,7 @@ import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, Opt } from '../../../fields/Doc'; import { NumCast, ScriptCast, StrCast } from '../../../fields/Types'; -import { returnFalse, returnZero, StopEvent } from '../../../Utils'; +import { emptyFunction, returnFalse, returnZero, StopEvent } from '../../../Utils'; import { DragManager } from '../../util/DragManager'; import { DocumentView, DocumentViewProps } from '../nodes/DocumentView'; import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; @@ -60,6 +60,8 @@ export class CollectionCarouselView extends CollectionSubView() { NativeHeight={returnZero} onDoubleClick={this.onContentDoubleClick} onClick={this.onContentClick} + isDocumentActive={this.props.childDocumentsActive?.() ? this.props.isDocumentActive : this.props.isContentActive} + isContentActive={this.props.childContentsActive ?? this.props.isContentActive() === false ? returnFalse : emptyFunction} hideCaptions={show_captions ? true : false} renderDepth={this.props.renderDepth + 1} LayoutTemplate={this.props.childLayoutTemplate} diff --git a/src/client/views/collections/CollectionDockingView.scss b/src/client/views/collections/CollectionDockingView.scss index 78e44dfa2..4c15d5eed 100644 --- a/src/client/views/collections/CollectionDockingView.scss +++ b/src/client/views/collections/CollectionDockingView.scss @@ -15,6 +15,12 @@ cursor: grab; color: $black; } +.collectiondockingview-container .lm_splitter { + opacity: 0.2; + &:hover { + opacity: 1; + } +} .lm_title.focus-visible { -webkit-appearance: none; diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 14f03044e..32fb4d8df 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -29,6 +29,7 @@ import { CollectionFreeFormView } from './collectionFreeForm'; import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView'; import { TabDocView } from './TabDocView'; import React = require('react'); +import { DocumentManager } from '../../util/DocumentManager'; import { DocAcl } from '../../../fields/DocSymbols'; const _global = (window /* browser */ || global) /* node */ as any; @@ -360,6 +361,7 @@ export class CollectionDockingView extends CollectionSubView() { } catch (e) {} this._goldenLayout?.destroy(); window.removeEventListener('resize', this.onResize); + window.removeEventListener('mouseup', this.onPointerUp); this._reactionDisposer?.(); this._lightboxReactionDisposer?.(); @@ -410,7 +412,14 @@ export class CollectionDockingView extends CollectionSubView() { if (!htmlTarget.closest('*.lm_content') && (htmlTarget.closest('*.lm_tab') || htmlTarget.closest('*.lm_stack'))) { const className = typeof htmlTarget.className === 'string' ? htmlTarget.className : ''; if (className.includes('lm_maximise')) this._flush = UndoManager.StartBatch('tab maximize'); - else if (!className.includes('lm_close')) DocServer.UPDATE_SERVER_CACHE(); + else { + const tabTarget = (e.target as HTMLElement)?.parentElement?.className.includes('lm_tab') ? (e.target as HTMLElement).parentElement : (e.target as HTMLElement); + const map = Array.from(this.tabMap).find(tab => tab.element[0] === tabTarget); + if (map?.DashDoc && DocumentManager.Instance.getFirstDocumentView(map.DashDoc)) { + SelectionManager.SelectView(DocumentManager.Instance.getFirstDocumentView(map.DashDoc), false); + } + if (!className.includes('lm_close')) DocServer.UPDATE_SERVER_CACHE(); + } } } if (!InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE) && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) { diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx index 64f9c6a87..6f88f6727 100644 --- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx +++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx @@ -98,14 +98,15 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr rowDrop = action((e: Event, de: DragManager.DropEvent) => { this._createEmbeddingSelected = false; if (de.complete.docDragData) { - this.props.parent.Document.dropConverter instanceof ScriptField && this.props.parent.Document.dropConverter.script.run({ dragData: de.complete.docDragData }); const key = this.props.pivotField; const castedValue = this.getValue(this.heading); const onLayoutDoc = this.onLayoutDoc(key); - de.complete.docDragData.droppedDocuments.forEach(d => Doc.SetInPlace(d, key, castedValue, !onLayoutDoc)); - this.props.parent.onInternalDrop(e, de); - e.stopPropagation(); + if (this.props.parent.onInternalDrop(e, de)) { + de.complete.docDragData.droppedDocuments.forEach(d => Doc.SetInPlace(d, key, castedValue, !onLayoutDoc)); + } + return true; } + return false; }); getValue = (value: string): any => { diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx index fc3f713ce..7d71bce13 100644 --- a/src/client/views/collections/CollectionMenu.tsx +++ b/src/client/views/collections/CollectionMenu.tsx @@ -427,8 +427,9 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu if (docDragData?.draggedDocuments.length) { this._buttonizableCommands?.filter(c => c.title === this._currentKey).map(c => c.immediate(docDragData.draggedDocuments || [])); e.stopPropagation(); + return true; } - return true; + return false; } dragViewDown = (e: React.PointerEvent) => { diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx index 675f23970..53a42d2a6 100644 --- a/src/client/views/collections/CollectionNoteTakingView.tsx +++ b/src/client/views/collections/CollectionNoteTakingView.tsx @@ -10,7 +10,7 @@ import { listSpec } from '../../../fields/Schema'; import { SchemaHeaderField } from '../../../fields/SchemaHeaderField'; import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; -import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, returnZero, smoothScroll, Utils } from '../../../Utils'; +import { emptyFunction, returnFalse, returnZero, smoothScroll, Utils } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { DragManager, dropActionType } from '../../util/DragManager'; import { SnappingManager } from '../../util/SnappingManager'; @@ -47,7 +47,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { @observable _cursor: CursorProperty = 'grab'; @observable _scroll = 0; @computed get chromeHidden() { - return BoolCast(this.layoutDoc.chromeHidden); + return BoolCast(this.layoutDoc.chromeHidden) || this.props.onBrowseClick?.() ? true : false; } // columnHeaders returns the list of SchemaHeaderFields currently being used by the layout doc to render the columns @computed get colHeaderData() { @@ -192,7 +192,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { if (found) { const top = found.getBoundingClientRect().top; const localTop = this.props.ScreenToLocalTransform().transformPoint(0, top); - if (Math.floor(localTop[1]) !== 0) { + if (Math.floor(localTop[1]) !== 0 && Math.ceil(this.props.PanelHeight()) < (this._mainCont?.scrollHeight || 0)) { let focusSpeed = options.zoomTime ?? 500; smoothScroll(focusSpeed, this._mainCont!, localTop[1] + this._mainCont!.scrollTop, options.easeFunc); return focusSpeed; @@ -250,8 +250,9 @@ export class CollectionNoteTakingView extends CollectionSubView() { dontRegisterView={dataDoc ? true : BoolCast(this.layoutDoc.childDontRegisterViews, this.props.dontRegisterView)} rootSelected={this.rootSelected} layout_showTitle={this.props.childlayout_showTitle} - dropAction={StrCast(this.layoutDoc.childDropAction) as dropActionType} + dragAction={StrCast(this.layoutDoc.childDragAction) as dropActionType} onClick={this.onChildClickHandler} + onBrowseClick={this.props.onBrowseClick} onDoubleClick={this.onChildDoubleClickHandler} ScreenToLocalTransform={noteTakingDocTransform} focus={this.focusDocument} @@ -441,23 +442,24 @@ export class CollectionNoteTakingView extends CollectionSubView() { docs.splice(previousDocIndex + 1, 0, ...newDocs); } } + return true; } } else if (de.complete.linkDragData?.dragDocument.embedContainer === this.props.Document && de.complete.linkDragData?.linkDragView?.props.CollectionFreeFormDocumentView?.()) { const source = Docs.Create.TextDocument('', { _width: 200, _height: 75, _layout_fitWidth: true, title: 'dropped annotation' }); - this.props.addDocument?.(source); + if (!this.props.addDocument?.(source)) e.preventDefault(); de.complete.linkDocument = DocUtils.MakeLink(source, de.complete.linkDragData.linkSourceGetAnchor(), { link_relationship: 'doc annotation' }); // TODODO this is where in text links get passed e.stopPropagation(); - } else if (de.complete.annoDragData?.dragDocument && super.onInternalDrop(e, de)) return this.internalAnchorAnnoDrop(e, de.complete.annoDragData); + return true; + } else if (de.complete.annoDragData?.dragDocument && super.onInternalDrop(e, de)) { + return this.internalAnchorAnnoDrop(e, de.complete.annoDragData); + } return false; }; @undoBatch internalAnchorAnnoDrop(e: Event, annoDragData: DragManager.AnchorAnnoDragData) { const dropCreator = annoDragData.dropDocCreator; - annoDragData.dropDocCreator = (annotationOn: Doc | undefined) => { - const dropDoc = dropCreator(annotationOn); - return dropDoc || this.rootDoc; - }; + annoDragData.dropDocCreator = (annotationOn: Doc | undefined) => dropCreator(annotationOn) || this.rootDoc; return true; } @@ -619,7 +621,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { } @computed get backgroundEvents() { - return SnappingManager.GetIsDragging(); + return this.props.isContentActive() === false ? 'none' : undefined; } observer: any; diff --git a/src/client/views/collections/CollectionNoteTakingViewColumn.tsx b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx index 2f28ecd00..3286d60bd 100644 --- a/src/client/views/collections/CollectionNoteTakingViewColumn.tsx +++ b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx @@ -92,6 +92,7 @@ export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColu columnDrop = action((e: Event, de: DragManager.DropEvent) => { const drop = { docs: de.complete.docDragData?.droppedDocuments, val: this.getValue(this._heading) }; drop.docs?.forEach(d => Doc.SetInPlace(d, this.props.pivotField, drop.val, false)); + return true; }); getValue = (value: string): any => { diff --git a/src/client/views/collections/CollectionPileView.tsx b/src/client/views/collections/CollectionPileView.tsx index 57d9bbb49..91be31289 100644 --- a/src/client/views/collections/CollectionPileView.tsx +++ b/src/client/views/collections/CollectionPileView.tsx @@ -52,20 +52,25 @@ export class CollectionPileView extends CollectionSubView() { @computed get toggleIcon() { return ScriptField.MakeScript('documentView.iconify()', { documentView: 'any' }); } + @computed get contentEvents() { + const isStarburst = this.layoutEngine() === computeStarburstLayout.name; + return this.props.isContentActive() && isStarburst ? undefined : 'none'; + } // returns the contents of the pileup in a CollectionFreeFormView @computed get contents() { - const isStarburst = this.layoutEngine() === computeStarburstLayout.name; return ( - <div className="collectionPileView-innards" style={{ pointerEvents: isStarburst || SnappingManager.GetIsDragging() ? undefined : 'none' }}> + <div className="collectionPileView-innards" style={{ pointerEvents: this.contentEvents }}> <CollectionFreeFormView - {...this.props} - childContentsActive={returnFalse} + {...this.props} // layoutEngine={this.layoutEngine} addDocument={this.addPileDoc} - childCanEmbedOnDrag={true} - childClickScript={this.toggleIcon} moveDocument={this.removePileDoc} + // pile children never have their contents active, but will be document active whenever the entire pile is. + childContentsActive={returnFalse} + childDocumentsActive={this.props.isDocumentActive} + childDragAction="move" + childClickScript={this.toggleIcon} /> </div> ); @@ -103,7 +108,6 @@ export class CollectionPileView extends CollectionSubView() { _undoBatch: UndoManager.Batch | undefined; pointerDown = (e: React.PointerEvent) => { let dist = 0; - SnappingManager.SetIsDragging(true); setupMoveUpEvents( this, e, @@ -126,7 +130,6 @@ export class CollectionPileView extends CollectionSubView() { () => { this._undoBatch?.end(); this._undoBatch = undefined; - SnappingManager.SetIsDragging(false); }, emptyFunction, e.shiftKey && this.layoutEngine() === computePassLayout.name, diff --git a/src/client/views/collections/CollectionStackedTimeline.scss b/src/client/views/collections/CollectionStackedTimeline.scss index a55b70e22..a19d8e696 100644 --- a/src/client/views/collections/CollectionStackedTimeline.scss +++ b/src/client/views/collections/CollectionStackedTimeline.scss @@ -104,7 +104,7 @@ .collectionStackedTimeline-left-resizer, .collectionStackedTimeline-resizer { - background: $medium-gray; + background: $dark-gray; position: absolute; top: 0; height: 100%; diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx index 9d5cb257a..ad84d859d 100644 --- a/src/client/views/collections/CollectionStackedTimeline.tsx +++ b/src/client/views/collections/CollectionStackedTimeline.tsx @@ -119,8 +119,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack // onClick play scripts CollectionStackedTimeline.RangeScript = CollectionStackedTimeline.RangeScript || - ScriptField.MakeFunction(`setTimeout(() => scriptContext.clickAnchor(this, clientX))`, { - // setTimeout is a hack to run script in its own properly named undo group (instead of being part of the generic onClick) + ScriptField.MakeFunction(`scriptContext.clickAnchor(this, clientX)`, { self: Doc.name, scriptContext: 'any', clientX: 'number', @@ -370,22 +369,22 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack // handles dragging and dropping markers in timeline @action internalDocDrop(e: Event, de: DragManager.DropEvent, docDragData: DragManager.DocumentDragData, xp: number) { - if (!de.embedKey && this.props.Document._isGroup) return false; - if (!super.onInternalDrop(e, de)) return false; - - // determine x coordinate of drop and assign it to the documents being dragged --- see internalDocDrop of collectionFreeFormView.tsx for how it's done when dropping onto a 2D freeform view - const localPt = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y); - const x = localPt[0] - docDragData.offset[0]; - const timelinePt = this.toTimeline(x + this._scroll, this.timelineContentWidth); - docDragData.droppedDocuments.forEach(drop => { - const anchorEnd = this.anchorEnd(drop); - if (anchorEnd !== undefined) { - Doc.SetInPlace(drop, drop._timecodeToHide === undefined ? this.props.endTag : 'timecodeToHide', timelinePt + anchorEnd - this.anchorStart(drop), false); - } - Doc.SetInPlace(drop, drop._timecodeToShow === undefined ? this.props.startTag : 'timecodeToShow', timelinePt, false); - }); + if (super.onInternalDrop(e, de)) { + // determine x coordinate of drop and assign it to the documents being dragged --- see internalDocDrop of collectionFreeFormView.tsx for how it's done when dropping onto a 2D freeform view + const localPt = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y); + const x = localPt[0] - docDragData.offset[0]; + const timelinePt = this.toTimeline(x + this._scroll, this.timelineContentWidth); + docDragData.droppedDocuments.forEach(drop => { + const anchorEnd = this.anchorEnd(drop); + if (anchorEnd !== undefined) { + Doc.SetInPlace(drop, drop._timecodeToHide === undefined ? this.props.endTag : 'timecodeToHide', timelinePt + anchorEnd - this.anchorStart(drop), false); + } + Doc.SetInPlace(drop, drop._timecodeToShow === undefined ? this.props.startTag : 'timecodeToShow', timelinePt, false); + }); - return true; + return true; + } + return false; } onInternalDrop = (e: Event, de: DragManager.DropEvent) => { @@ -406,7 +405,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack title: ComputedField.MakeFunction(`self["${endTag}"] ? "#" + formatToTime(self["${startTag}"]) + "-" + formatToTime(self["${endTag}"]) : "#" + formatToTime(self["${startTag}"])`) as any, _label_minFontSize: 12, _label_maxFontSize: 24, - _stayInCollection: true, + _dragOnlyWithinContainer: true, backgroundColor: 'rgba(128, 128, 128, 0.5)', layout_hideLinkButton: true, onClick: FollowLinkScript(), @@ -528,6 +527,9 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack ); } + @computed get timelineEvents() { + return this.props.isContentActive() ? 'all' : this.props.isContentActive() === false ? 'none' : undefined; + } render() { const overlaps: { anchorStartTime: number; @@ -540,7 +542,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack })); const maxLevel = overlaps.reduce((m, o) => Math.max(m, o.level), 0) + 2; return this.clipDuration === 0 ? null : ( - <div ref={this.createDashEventsTarget} style={{ pointerEvents: SnappingManager.GetIsDragging() ? 'all' : undefined }}> + <div ref={this.createDashEventsTarget} style={{ pointerEvents: this.timelineEvents }}> <div className="collectionStackedTimeline-timelineContainer" style={{ width: this.props.PanelWidth(), cursor: SnappingManager.GetIsDragging() ? 'grab' : '' }} diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 3c0d8cbc3..ec529afc3 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -17,7 +17,7 @@ import { CollectionViewType } from '../../documents/DocumentTypes'; import { DragManager, dropActionType } from '../../util/DragManager'; import { SnappingManager } from '../../util/SnappingManager'; import { Transform } from '../../util/Transform'; -import { undoBatch } from '../../util/UndoManager'; +import { undoBatch, UndoManager } from '../../util/UndoManager'; import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; import { EditableView } from '../EditableView'; @@ -31,6 +31,7 @@ import { CollectionMasonryViewFieldRow } from './CollectionMasonryViewFieldRow'; import './CollectionStackingView.scss'; import { CollectionStackingViewFieldColumn } from './CollectionStackingViewFieldColumn'; import { CollectionSubView } from './CollectionSubView'; +import { Colors } from '../global/globalEnums'; const _global = (window /* browser */ || global) /* node */ as any; export type collectionStackingViewProps = { @@ -84,7 +85,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.HeaderMargin); } @computed get xMargin() { - return NumCast(this.layoutDoc._xMargin, Math.min(3, 0.05 * this.props.PanelWidth())); + return NumCast(this.layoutDoc._xMargin, Math.max(3, 0.05 * this.props.PanelWidth())); } @computed get yMargin() { return this.props.yPadding || NumCast(this.layoutDoc._yMargin, Math.min(5, 0.05 * this.props.PanelWidth())); @@ -137,7 +138,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection // 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 ? { width: width(), marginTop: i ? this.gridGap : 0, height: height() } : { gridRowEnd: `span ${rowSpan}` }; + const style = this.isStackingView ? { background: Colors.MEDIUM_GRAY, 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}> @@ -295,7 +296,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection return this.addDocument?.(newDoc); } }; - isContentActive = () => (this.props.isSelected() || this.props.isContentActive() ? true : this.props.isSelected() === false || this.props.isContentActive() === false ? false : undefined); + isContentActive = () => (this.props.isContentActive() ? true : this.props.isSelected() === false || this.props.isContentActive() === false ? false : undefined); @observable _renderCount = 5; isChildContentActive = () => @@ -341,7 +342,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection dontRegisterView={BoolCast(this.layoutDoc.childDontRegisterViews, this.props.dontRegisterView)} // used to be true if DataDoc existed, but template textboxes won't layout_autoHeight resize if dontRegisterView is set, but they need to. rootSelected={this.rootSelected} layout_showTitle={this.props.childlayout_showTitle} - dropAction={StrCast(this.layoutDoc.childDropAction) as dropActionType} + dragAction={(this.layoutDoc.childDragAction ?? this.props.childDragAction) as dropActionType} onClick={this.onChildClickHandler} onDoubleClick={this.onChildDoubleClickHandler} ScreenToLocalTransform={stackedDocTransform} @@ -402,11 +403,15 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection // This following three functions must be from the view Mehek showed columnDividerDown = (e: React.PointerEvent) => { runInAction(() => (this._cursor = 'grabbing')); + const batch = UndoManager.StartBatch('stacking width'); setupMoveUpEvents( this, e, this.onDividerMove, - action(() => (this._cursor = 'grab')), + action(() => { + this._cursor = 'grab'; + batch.end(); + }), emptyFunction ); }; @@ -455,18 +460,24 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection const docs = this.childDocList; // still figuring out where to add the document if (docs && newDocs.length) { + newDocs.forEach(newdoc => docs.indexOf(newdoc) !== -1 && docs.splice(docs.indexOf(newdoc), 1)); const insertInd = dropInd === -1 ? docs.length : dropInd + dropAfter; const offset = newDocs.reduce((off, ndoc) => (this.filteredChildren.find((fdoc, i) => ndoc === fdoc && i < insertInd) ? off + 1 : off), 0); newDocs.filter(ndoc => docs.indexOf(ndoc) !== -1).forEach(ndoc => docs.splice(docs.indexOf(ndoc), 1)); docs.splice(insertInd - offset, 0, ...newDocs); } + return true; } } else if (de.complete.linkDragData?.dragDocument.embedContainer === this.props.Document && de.complete.linkDragData?.linkDragView?.props.CollectionFreeFormDocumentView?.()) { const source = Docs.Create.TextDocument('', { _width: 200, _height: 75, _layout_fitWidth: true, title: 'dropped annotation' }); - this.props.addDocument?.(source); + if (!this.props.addDocument?.(source)) e.preventDefault(); de.complete.linkDocument = DocUtils.MakeLink(source, de.complete.linkDragData.linkSourceGetAnchor(), { link_relationship: 'doc annotation' }); // TODODO this is where in text links get passed e.stopPropagation(); - } else if (de.complete.annoDragData?.dragDocument && super.onInternalDrop(e, de)) return this.internalAnchorAnnoDrop(e, de.complete.annoDragData); + return true; + } else if (de.complete.annoDragData?.dragDocument && super.onInternalDrop(e, de)) { + return this.internalAnchorAnnoDrop(e, de.complete.annoDragData); + } + e.preventDefault(); return false; }; @@ -700,7 +711,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection } @computed get backgroundEvents() { - return SnappingManager.GetIsDragging(); + return this.props.isContentActive() === false ? 'none' : undefined; } observer: any; render() { @@ -710,7 +721,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection SetValue: this.addGroup, contents: '+ ADD A GROUP', }; - const buttonMenu = this.rootDoc.buttonMenu; + const buttonMenu = this.rootDoc.layout_headerButton; const noviceExplainer = this.rootDoc.layout_explainer; return ( <> @@ -727,7 +738,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection style={{ overflowY: this.isContentActive() ? 'auto' : 'hidden', background: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor), - pointerEvents: (this.props.pointerEvents?.() as any) ?? (this.backgroundEvents ? 'all' : undefined), + pointerEvents: (this.props.pointerEvents?.() as any) ?? this.backgroundEvents, }} onScroll={action(e => (this._scroll = e.currentTarget.scrollTop))} onDrop={this.onExternalDrop.bind(this)} diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx index 6be9cb72d..ebb4ba5a1 100644 --- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx +++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx @@ -94,6 +94,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC columnDrop = action((e: Event, de: DragManager.DropEvent) => { const drop = { docs: de.complete.docDragData?.droppedDocuments, val: this.getValue(this._heading) }; this.props.pivotField && drop.docs?.forEach(d => Doc.SetInPlace(d, this.props.pivotField, drop.val, false)); + return true; }); getValue = (value: string): any => { const parsed = parseInt(value); diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index d1b7f6ff6..78789247f 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -185,12 +185,14 @@ export function CollectionSubView<X>(moreProps?: X) { @undoBatch protected onGesture(e: Event, ge: GestureUtils.GestureEvent) {} - protected onInternalPreDrop(e: Event, de: DragManager.DropEvent, targetAction: dropActionType) { + protected onInternalPreDrop(e: Event, de: DragManager.DropEvent) { if (de.complete.docDragData) { - // if targetDropAction is, say 'embed', but we're just dragging within a collection, we want to ignore the targetAction. - // otherwise, the targetAction should become the actual action (which can still be overridden by the userDropAction -eg, shift/ctrl keys) - if (targetAction && !de.complete.docDragData.draggedDocuments.some(d => d.embedContainer === this.props.Document && this.childDocs.includes(d))) { - de.complete.docDragData.dropAction = targetAction; + // override the dropEvent's dropAction + const dropAction = this.layoutDoc.dropAction as dropActionType; + // if the dropEvent's dragAction is, say 'embed', but we're just dragging within a collection, we may not actually want to make an embedding. + // so we check if our collection has a dropAction set on it and if so, we use that instead. + if (dropAction && !de.complete.docDragData.draggedDocuments.some(d => d.embedContainer === this.props.Document && this.childDocs.includes(d))) { + de.complete.docDragData.dropAction = dropAction; } e.stopPropagation(); } @@ -203,7 +205,7 @@ export function CollectionSubView<X>(moreProps?: X) { protected onInternalDrop(e: Event, de: DragManager.DropEvent): boolean { const docDragData = de.complete.docDragData; if (docDragData) { - let added = false; + let added = undefined; const dropAction = docDragData.dropAction || docDragData.userDropAction; const targetDocments = DocListCast(this.dataDoc[this.props.fieldKey]); const someMoved = !dropAction && docDragData.draggedDocuments.some(drag => targetDocments.includes(drag)); @@ -212,22 +214,23 @@ export function CollectionSubView<X>(moreProps?: X) { const movedDocs = docDragData.droppedDocuments.filter((d, i) => docDragData.draggedDocuments[i] === d); const addedDocs = docDragData.droppedDocuments.filter((d, i) => docDragData.draggedDocuments[i] !== d); if (movedDocs.length) { - const canAdd = this.props.Document._type_collection === CollectionViewType.Pile || de.embedKey || this.props.Document.allowOverlayDrop || Doc.AreProtosEqual(Cast(movedDocs[0].annotationOn, Doc, null), this.props.Document); - added = docDragData.moveDocument(movedDocs, this.props.Document, canAdd ? this.addDocument : returnFalse); + const canAdd = de.embedKey || dropAction || Doc.AreProtosEqual(Cast(movedDocs[0].annotationOn, Doc, null), this.rootDoc); + const moved = docDragData.moveDocument(movedDocs, this.rootDoc, canAdd ? this.addDocument : returnFalse); + added = canAdd || moved ? moved : undefined; } else { - ScriptCast(this.props.Document.dropConverter)?.script.run({ dragData: docDragData }); + ScriptCast(this.rootDoc.dropConverter)?.script.run({ dragData: docDragData }); added = addedDocs.length ? this.addDocument(addedDocs) : true; } - added && e.stopPropagation(); - return added; } else { - ScriptCast(this.props.Document.dropConverter)?.script.run({ dragData: docDragData }); + ScriptCast(this.rootDoc.dropConverter)?.script.run({ dragData: docDragData }); added = this.addDocument(docDragData.droppedDocuments); + !added && alert('You cannot perform this move'); } - !added && alert('You cannot perform this move'); - e.stopPropagation(); - return added; + added === false && !this.props.isAnnotationOverlay && e.preventDefault(); + added === true && e.stopPropagation(); + return added ? true : false; } else if (de.complete.annoDragData) { + e.stopPropagation(); const dropCreator = de.complete.annoDragData.dropDocCreator; de.complete.annoDragData.dropDocCreator = () => { const dropped = dropCreator(this.props.isAnnotationOverlay ? this.rootDoc : undefined); diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx index 60e6815e5..192d48c64 100644 --- a/src/client/views/collections/CollectionTimeView.tsx +++ b/src/client/views/collections/CollectionTimeView.tsx @@ -34,8 +34,6 @@ export class CollectionTimeView extends CollectionSubView() { async componentDidMount() { this.props.setContentView?.(this); - //const detailView = (await DocCastAsync(this.props.Document.childClickedOpenTemplateView)) || DocUtils.findTemplate("detailView", StrCast(this.rootDoc.type), ""); - ///const childText = "const embedding = getEmbedding(self); switchView(embedding, detailView); embedding.dropAction='embed'; useRightSplit(embedding, shiftKey); "; runInAction(() => { this._childClickedScript = ScriptField.MakeScript('openInLightbox(self)', { this: Doc.name }); this._viewDefDivClick = ScriptField.MakeScript('pivotColumnClick(this,payload)', { payload: 'any' }); diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index ed1e0c067..4cd3885f5 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -3,7 +3,6 @@ import { observer } from 'mobx-react'; import { Doc, DocListCast, Opt, StrListCast } from '../../../fields/Doc'; import { DocData, Height, Width } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; -import { InkTool } from '../../../fields/InkField'; import { listSpec } from '../../../fields/Schema'; import { ScriptField } from '../../../fields/ScriptField'; import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; @@ -90,8 +89,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree // these should stay in synch with counterparts in DocComponent.ts ViewBoxAnnotatableComponent @observable _isAnyChildContentActive = false; whenChildContentsActiveChanged = action((isActive: boolean) => this.props.whenChildContentsActiveChanged((this._isAnyChildContentActive = isActive))); - isContentActive = (outsideReaction?: boolean) => - Doc.ActiveTool !== InkTool.None || this.props.isContentActive?.() || this.props.Document.forceActive || this.props.isSelected(outsideReaction) || this._isAnyChildContentActive || this.props.rootSelected(outsideReaction) ? true : false; + isContentActive = (outsideReaction?: boolean) => (this._isAnyChildContentActive ? true : this.props.isContentActive() ? true : false); componentWillUnmount() { this._isDisposing = true; @@ -142,11 +140,16 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree if ((this._mainEle = ele)) this._treedropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.doc, this.onInternalPreDrop.bind(this)); }; - protected onInternalPreDrop = (e: Event, de: DragManager.DropEvent, targetAction: dropActionType) => { + protected onInternalPreDrop = (e: Event, de: DragManager.DropEvent) => { + const dropAction = this.layoutDoc.dropAction as dropActionType; const dragData = de.complete.docDragData; if (dragData) { - const isInTree = () => Doc.AreProtosEqual(dragData.treeViewDoc, this.props.Document) || dragData.draggedDocuments.some(d => d.embedContainer === this.doc && this.childDocs.includes(d)); - dragData.dropAction = targetAction && !isInTree() ? targetAction : this.doc === dragData?.treeViewDoc ? 'same' : dragData.dropAction; + const sameTree = Doc.AreProtosEqual(dragData.treeViewDoc, this.rootDoc) ? true : false; + const isAlreadyInTree = () => sameTree || dragData.draggedDocuments.some(d => d.embedContainer === this.doc && this.childDocs.includes(d)); + if (isAlreadyInTree() !== sameTree) { + console.log('WHAAAT'); + } + dragData.dropAction = dropAction && !isAlreadyInTree() ? dropAction : sameTree ? 'same' : dragData.dropAction; } }; @@ -261,7 +264,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree @observable _renderCount = 1; @computed get treeViewElements() { TraceMobx(); - const dropAction = StrCast(this.doc.childDropAction) as dropActionType; + const dragAction = StrCast(this.doc.childDragAction) as dropActionType; const addDoc = (doc: Doc | Doc[], relativeTo?: Doc, before?: boolean) => this.addDoc(doc, relativeTo, before); const moveDoc = (d: Doc | Doc[], target: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => this.props.moveDocument?.(d, target, addDoc) || false; if (this._renderCount < this.treeChildren.length) setTimeout(action(() => (this._renderCount = Math.min(this.treeChildren.length, this._renderCount + 20)))); @@ -276,7 +279,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree addDoc, this.remove, moveDoc, - dropAction, + dragAction, this.props.addDocTab, this.props.styleProvider, this.screenToLocalTransform, @@ -383,7 +386,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree @observable _headerHeight = 0; @computed get content() { const background = () => this.props.styleProvider?.(this.doc, this.props, StyleProp.BackgroundColor); - const pointerEvents = () => (!this.props.isContentActive() && !SnappingManager.GetIsDragging() ? 'none' : undefined); + const pointerEvents = () => (this.props.isContentActive() === false ? 'none' : undefined); const titleBar = this.props.treeViewHideTitle || this.doc.treeViewHideTitle ? null : this.titleBar; return ( <div style={{ display: 'flex', flexDirection: 'column', height: '100%', pointerEvents: 'all' }}> @@ -396,6 +399,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree <div className="collectionTreeView-contents" key="tree" + ref={r => !this.doc.treeViewHasOverlay && r && this.createTreeDropTarget(r)} style={{ ...(!titleBar ? { marginLeft: this.marginX(), paddingTop: this.marginTop() } : {}), overflow: 'auto', @@ -419,8 +423,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree minHeight: '100%', }} onWheel={e => e.stopPropagation()} - onDrop={this.onTreeDrop} - ref={r => !this.doc.treeViewHasOverlay && r && this.createTreeDropTarget(r)}> + onDrop={this.onTreeDrop}> <ul className={`no-indent${this.outlineMode ? '-outline' : ''}`}>{this.treeViewElements}</ul> </div> </div> @@ -440,12 +443,12 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree setContentView={emptyFunction} NativeWidth={returnZero} NativeHeight={returnZero} - pointerEvents={SnappingManager.GetIsDragging() ? returnAll : returnNone} + pointerEvents={this.props.isContentActive() && SnappingManager.GetIsDragging() ? returnAll : returnNone} isAnnotationOverlay={true} isAnnotationOverlayScrollable={true} childDocumentsActive={this.props.isDocumentActive} fieldKey={this.props.fieldKey + '_annotations'} - dropAction={'move'} + dropAction="move" select={emptyFunction} addDocument={this.addAnnotationDocument} removeDocument={this.remAnnotationDocument} diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 7913d3188..c33519afb 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -9,6 +9,7 @@ import { TraceMobx } from '../../../fields/util'; import { returnEmptyString } from '../../../Utils'; import { DocUtils } from '../../documents/Documents'; import { CollectionViewType } from '../../documents/DocumentTypes'; +import { dropActionType } from '../../util/DragManager'; import { ImageUtils } from '../../util/Import & Export/ImageUtils'; import { InteractionUtils } from '../../util/InteractionUtils'; import { ContextMenu } from '../ContextMenu'; @@ -55,7 +56,7 @@ interface CollectionViewProps_ extends FieldViewProps { childHideDecorationTitle?: () => boolean; childHideResizeHandles?: () => boolean; childLayoutTemplate?: () => Doc | undefined; // specify a layout Doc template to use for children of the collection - childCanEmbedOnDrag?: boolean; + childDragAction?: dropActionType; childXPadding?: number; childYPadding?: number; childLayoutString?: string; @@ -220,7 +221,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab childHideResizeHandles = () => this.props.childHideResizeHandles?.() ?? BoolCast(this.Document.childHideResizeHandles); childHideDecorationTitle = () => this.props.childHideDecorationTitle?.() ?? BoolCast(this.Document.childHideDecorationTitle); childLayoutTemplate = () => this.props.childLayoutTemplate?.() || Cast(this.rootDoc.childLayoutTemplate, Doc, null); - isContentActive = (outsideReaction?: boolean) => this.props.isContentActive() || this.isAnyChildContentActive(); + isContentActive = (outsideReaction?: boolean) => (this.isAnyChildContentActive() ? true : this.props.isContentActive()); render() { TraceMobx(); diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx index 3b99339af..4d780f46b 100644 --- a/src/client/views/collections/TabDocView.tsx +++ b/src/client/views/collections/TabDocView.tsx @@ -2,7 +2,7 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@material-ui/core'; import { clamp } from 'lodash'; -import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; +import { action, computed, IReactionDisposer, observable, ObservableSet, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as ReactDOM from 'react-dom/client'; import { Doc, Opt } from '../../../fields/Doc'; @@ -43,12 +43,21 @@ interface TabDocViewProps { } @observer export class TabDocView extends React.Component<TabDocViewProps> { + static _allTabs = new ObservableSet<TabDocView>(); _mainCont: HTMLDivElement | null = null; _tabReaction: IReactionDisposer | undefined; @observable _activated: boolean = false; @observable _panelWidth = 0; @observable _panelHeight = 0; + @observable _hovering = false; @observable _isActive: boolean = false; + @observable _isAnyChildContentActive = false; + @computed get _isUserActivated() { + return SelectionManager.Views().some(view => view.rootDoc === this._document) || this._isAnyChildContentActive; + } + @computed get _isContentActive() { + return this._isUserActivated || this._hovering; + } @observable _document: Doc | undefined; @observable _view: DocumentView | undefined; @@ -61,9 +70,7 @@ export class TabDocView extends React.Component<TabDocViewProps> { return 'transparent'; } @computed get tabColor() { - let tabColor = StrCast(this._document?._backgroundColor, StrCast(this._document?.backgroundColor, DefaultStyleProvider(this._document, undefined, StyleProp.BackgroundColor))); - if (tabColor === 'transparent') return 'black'; - return tabColor; + return this._isUserActivated ? Colors.WHITE : this._hovering ? Colors.LIGHT_GRAY : Colors.MEDIUM_GRAY; } @computed get tabTextColor() { return this._document?.type === DocumentType.PRES ? 'black' : StrCast(this._document?._color, StrCast(this._document?.color, DefaultStyleProvider(this._document, undefined, StyleProp.Color))); @@ -191,7 +198,7 @@ export class TabDocView extends React.Component<TabDocViewProps> { } }); tab._disposers.selectionDisposer = reaction( - () => SelectionManager.Views().some(v => v.topMost && v.props.Document === doc), + () => SelectionManager.Views().some(view => view.rootDoc === this._document), action(selected => { if (selected) this._activated = true; const toggle = tab.element[0].children[2].children[0] as HTMLInputElement; @@ -305,6 +312,7 @@ export class TabDocView extends React.Component<TabDocViewProps> { setTimeout(batch.end, 500); // need to wait until dockingview (goldenlayout) updates all its structurs } + @action componentDidMount() { new _global.ResizeObserver( action((entries: any) => { @@ -319,14 +327,17 @@ export class TabDocView extends React.Component<TabDocViewProps> { // this._tabReaction = reaction(() => ({ selected: this.active(), title: this.tab?.titleElement[0] }), // ({ selected, title }) => title && (title.style.backgroundColor = selected ? "white" : ""), // { fireImmediately: true }); + TabDocView._allTabs.add(this); } componentDidUpdate() { this._view && DocumentManager.Instance.AddView(this._view); } + @action componentWillUnmount() { this._tabReaction?.(); this._view && DocumentManager.Instance.RemoveView(this._view); + TabDocView._allTabs.delete(this); this.props.glContainer.layoutManager.off('activeContentItemChanged', this.onActiveContentItemChanged); } @@ -335,6 +346,7 @@ export class TabDocView extends React.Component<TabDocViewProps> { private onActiveContentItemChanged(contentItem: any) { if (!contentItem || (this.stack === contentItem.parent && ((contentItem?.tab === this.tab && !this._isActive) || (contentItem?.tab !== this.tab && this._isActive)))) { this._activated = this._isActive = !contentItem || contentItem?.tab === this.tab; + if (!this._view) setTimeout(() => SelectionManager.SelectView(this._view, false)); !this._isActive && this._document && Doc.UnBrushDoc(this._document); // bcz: bad -- trying to simulate a pointer leave event when a new tab is opened up on top of an existing one. } } @@ -404,11 +416,11 @@ export class TabDocView extends React.Component<TabDocViewProps> { }; PanelWidth = () => this._panelWidth; PanelHeight = () => this._panelHeight; - miniMapColor = () => this.tabColor; + miniMapColor = () => Colors.MEDIUM_GRAY; tabView = () => this._view; disableMinimap = () => !this._document || this._document.layout !== CollectionView.LayoutString(Doc.LayoutFieldKey(this._document)) || this._document?._type_collection !== CollectionViewType.Freeform; - hideMinimap = () => this.disableMinimap() || BoolCast(this._document?.layout_hideMinimap); - + whenChildContentActiveChanges = (isActive: boolean) => (this._isAnyChildContentActive = isActive); + isContentActive = () => this._isContentActive; @computed get docView() { return !this._activated || !this._document ? null : ( <> @@ -426,7 +438,7 @@ export class TabDocView extends React.Component<TabDocViewProps> { DataDoc={!Doc.AreProtosEqual(this._document[DocData], this._document) ? this._document[DocData] : undefined} onBrowseClick={MainView.Instance.exploreMode} waitForDoubleClickToClick={MainView.Instance.waitForDoubleClick} - isContentActive={returnTrue} + isContentActive={this.isContentActive} isDocumentActive={returnFalse} PanelWidth={this.PanelWidth} PanelHeight={this.PanelHeight} @@ -440,30 +452,15 @@ export class TabDocView extends React.Component<TabDocViewProps> { ScreenToLocalTransform={this.ScreenToLocalTransform} dontCenter={'y'} rootSelected={returnTrue} - whenChildContentsActiveChanged={emptyFunction} + whenChildContentsActiveChanged={this.whenChildContentActiveChanges} focus={this.focusFunc} docViewPath={returnEmptyDoclist} bringToFront={emptyFunction} pinToPres={TabDocView.PinDoc} /> - <TabMinimapView key="minimap" hideMinimap={this.hideMinimap} addDocTab={this.addDocTab} PanelHeight={this.PanelHeight} PanelWidth={this.PanelWidth} background={this.miniMapColor} document={this._document} tabView={this.tabView} /> - <Tooltip key="ttip" title={<div className="dash-tooltip">{this._document.layout_hideMinimap ? 'Open minimap' : 'Close minimap'}</div>}> - <div - className="miniMap-hidden" - style={{ - display: this.disableMinimap() || this._document._type_collection !== 'freeform' ? 'none' : undefined, - color: this._document.layout_hideMinimap ? Colors.BLACK : Colors.WHITE, - backgroundColor: this._document.layout_hideMinimap ? Colors.LIGHT_GRAY : Colors.MEDIUM_BLUE, - boxShadow: this._document.layout_hideMinimap ? Shadows.STANDARD_SHADOW : undefined, - }} - onPointerDown={e => e.stopPropagation()} - onClick={action(e => { - e.stopPropagation(); - this._document!.layout_hideMinimap = !this._document!.layout_hideMinimap; - })}> - <FontAwesomeIcon icon={'globe-asia'} size="lg" /> - </div> - </Tooltip> + {this.disableMinimap() || this._document._type_collection !== CollectionViewType.Freeform ? null : ( + <TabMinimapView key="minimap" addDocTab={this.addDocTab} PanelHeight={this.PanelHeight} PanelWidth={this.PanelWidth} background={this.miniMapColor} document={this._document} tabView={this.tabView} /> + )} </> ); } @@ -475,6 +472,10 @@ export class TabDocView extends React.Component<TabDocViewProps> { style={{ fontFamily: Doc.UserDoc().renderStyle === 'comic' ? 'Comic Sans MS' : undefined, }} + onPointerEnter={action(() => (this._hovering = true))} + onPointerLeave={action(() => (this._hovering = false))} + onDragOver={action(() => (this._hovering = true))} + onDragLeave={action(() => (this._hovering = false))} ref={ref => { if ((this._mainCont = ref)) { if (this._lastTab) { @@ -494,7 +495,6 @@ export class TabDocView extends React.Component<TabDocViewProps> { interface TabMinimapViewProps { document: Doc; - hideMinimap: () => boolean; tabView: () => DocumentView | undefined; addDocTab: (doc: Doc, where: OpenWhere) => boolean; PanelWidth: () => number; @@ -569,47 +569,67 @@ export class TabMinimapView extends React.Component<TabMinimapViewProps> { if (!this.renderBounds) return null; const miniWidth = (this.props.PanelWidth() / NumCast(this.props.document._freeform_scale, 1) / this.renderBounds.dim) * 100; const miniHeight = (this.props.PanelHeight() / NumCast(this.props.document._freeform_scale, 1) / this.renderBounds.dim) * 100; - const miniLeft = 50 + ((NumCast(this.props.document._freeform_) - this.renderBounds.cx) / this.renderBounds.dim) * 100 - miniWidth / 2; + const miniLeft = 50 + ((NumCast(this.props.document._freeform_panX) - this.renderBounds.cx) / this.renderBounds.dim) * 100 - miniWidth / 2; const miniTop = 50 + ((NumCast(this.props.document._freeform_panY) - this.renderBounds.cy) / this.renderBounds.dim) * 100 - miniHeight / 2; const miniSize = this.returnMiniSize(); - return this.props.hideMinimap() ? null : ( - <div className="miniMap" style={{ width: miniSize, height: miniSize, background: this.props.background() }}> - <CollectionFreeFormView - Document={this.props.document} - docViewPath={returnEmptyDoclist} - childLayoutTemplate={this.childLayoutTemplate} // bcz: Ugh .. should probably be rendering a CollectionView or the minimap should be part of the collectionFreeFormView to avoid having to set stuff like this. - noOverlay={true} // don't render overlay Docs since they won't scale - setHeight={returnFalse} - isContentActive={emptyFunction} - isAnyChildContentActive={returnFalse} - select={emptyFunction} - dropAction={undefined} - isSelected={returnFalse} - dontRegisterView={true} - fieldKey={Doc.LayoutFieldKey(this.props.document)} - bringToFront={emptyFunction} - rootSelected={returnTrue} - addDocument={returnFalse} - moveDocument={returnFalse} - removeDocument={returnFalse} - PanelWidth={this.returnMiniSize} - PanelHeight={this.returnMiniSize} - ScreenToLocalTransform={Transform.Identity} - renderDepth={0} - whenChildContentsActiveChanged={emptyFunction} - focus={emptyFunction} - styleProvider={TabMinimapView.miniStyleProvider} - addDocTab={this.props.addDocTab} - pinToPres={TabDocView.PinDoc} - childFilters={CollectionDockingView.Instance?.childDocFilters ?? returnEmptyDoclist} - childFiltersByRanges={CollectionDockingView.Instance?.childDocRangeFilters ?? returnEmptyDoclist} - searchFilterDocs={CollectionDockingView.Instance?.searchFilterDocs ?? returnEmptyDoclist} - fitContentsToBox={returnTrue} - /> - <div className="miniOverlay" onPointerDown={this.miniDown}> - <div className="miniThumb" style={{ width: `${miniWidth}% `, height: `${miniHeight}% `, left: `${miniLeft}% `, top: `${miniTop}% ` }} /> - </div> - </div> + return ( + <> + {' '} + {this.props.document?.layout_hideMinimap ? null : ( + <div className="miniMap" style={{ width: miniSize, height: miniSize, background: this.props.background() }}> + <CollectionFreeFormView + Document={this.props.document} + docViewPath={returnEmptyDoclist} + childLayoutTemplate={this.childLayoutTemplate} // bcz: Ugh .. should probably be rendering a CollectionView or the minimap should be part of the collectionFreeFormView to avoid having to set stuff like this. + noOverlay={true} // don't render overlay Docs since they won't scale + setHeight={returnFalse} + isContentActive={emptyFunction} + isAnyChildContentActive={returnFalse} + select={emptyFunction} + isSelected={returnFalse} + dontRegisterView={true} + fieldKey={Doc.LayoutFieldKey(this.props.document)} + bringToFront={emptyFunction} + rootSelected={returnTrue} + addDocument={returnFalse} + moveDocument={returnFalse} + removeDocument={returnFalse} + PanelWidth={this.returnMiniSize} + PanelHeight={this.returnMiniSize} + ScreenToLocalTransform={Transform.Identity} + renderDepth={0} + whenChildContentsActiveChanged={emptyFunction} + focus={emptyFunction} + styleProvider={TabMinimapView.miniStyleProvider} + addDocTab={this.props.addDocTab} + pinToPres={TabDocView.PinDoc} + childFilters={CollectionDockingView.Instance?.childDocFilters ?? returnEmptyDoclist} + childFiltersByRanges={CollectionDockingView.Instance?.childDocRangeFilters ?? returnEmptyDoclist} + searchFilterDocs={CollectionDockingView.Instance?.searchFilterDocs ?? returnEmptyDoclist} + fitContentsToBox={returnTrue} + /> + <div className="miniOverlay" onPointerDown={this.miniDown}> + <div className="miniThumb" style={{ width: `${miniWidth}% `, height: `${miniHeight}% `, left: `${miniLeft}% `, top: `${miniTop}% ` }} /> + </div> + </div> + )} + <Tooltip key="ttip" title={<div className="dash-tooltip">{this.props.document.layout_hideMinimap ? 'Open minimap' : 'Close minimap'}</div>}> + <div + className="miniMap-hidden" + style={{ + color: this.props.document.layout_hideMinimap ? Colors.BLACK : Colors.WHITE, + backgroundColor: this.props.document.layout_hideMinimap ? Colors.LIGHT_GRAY : Colors.MEDIUM_BLUE, + boxShadow: this.props.document.layout_hideMinimap ? Shadows.STANDARD_SHADOW : undefined, + }} + onPointerDown={e => e.stopPropagation()} + onClick={action(e => { + e.stopPropagation(); + this.props.document!.layout_hideMinimap = !this.props.document!.layout_hideMinimap; + })}> + <FontAwesomeIcon icon="globe-asia" size="lg" /> + </div> + </Tooltip> + </> ); } } diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx index 6bc30c451..8d8d895c6 100644 --- a/src/client/views/collections/TreeView.tsx +++ b/src/client/views/collections/TreeView.tsx @@ -21,7 +21,7 @@ import { ScriptingGlobals } from '../../util/ScriptingGlobals'; import { SelectionManager } from '../../util/SelectionManager'; import { SnappingManager } from '../../util/SnappingManager'; import { Transform } from '../../util/Transform'; -import { undoBatch, UndoManager } from '../../util/UndoManager'; +import { undoable, undoBatch, UndoManager } from '../../util/UndoManager'; import { EditableView } from '../EditableView'; import { TREE_BULLET_WIDTH } from '../global/globalCssVariables.scss'; import { DocumentView, DocumentViewInternal, DocumentViewProps, OpenWhere, StyleProviderFunc } from '../nodes/DocumentView'; @@ -45,7 +45,7 @@ export interface TreeViewProps { dataDoc?: Doc; treeViewParent: Doc; renderDepth: number; - dropAction: dropActionType; + dragAction: dropActionType; addDocTab: (doc: Doc, where: OpenWhere) => boolean; panelWidth: () => number; panelHeight: () => number; @@ -276,7 +276,7 @@ export class TreeView extends React.Component<TreeViewProps> { }; onPointerEnter = (e: React.PointerEvent): void => { this.props.isContentActive(true) && Doc.BrushDoc(this.dataDoc); - if (e.buttons === 1 && SnappingManager.GetIsDragging()) { + if (e.buttons === 1 && SnappingManager.GetIsDragging() && this.props.isContentActive()) { this._header.current!.className = 'treeView-header'; document.removeEventListener('pointermove', this.onDragMove, true); document.removeEventListener('pointerup', this.onDragUp, true); @@ -341,13 +341,13 @@ export class TreeView extends React.Component<TreeViewProps> { }; makeFolder = () => { - const folder = Docs.Create.TreeDocument([], { title: 'Untitled folder', _stayInCollection: true, isFolder: true }); + 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); }; deleteItem = () => this.props.removeDoc?.(this.doc); - preTreeDrop = (e: Event, de: DragManager.DropEvent, targetAction: dropActionType) => { + preTreeDrop = (e: Event, de: DragManager.DropEvent) => { const dragData = de.complete.docDragData; dragData && (dragData.dropAction = this.props.treeView.props.Document === dragData.treeViewDoc ? 'same' : dragData.dropAction); }; @@ -355,7 +355,7 @@ export class TreeView extends React.Component<TreeViewProps> { @undoBatch treeDrop = (e: Event, de: DragManager.DropEvent) => { const pt = [de.x, de.y]; - if (!this._header.current) return; + if (!this._header.current) return false; const rect = this._header.current.getBoundingClientRect(); const before = pt[1] < rect.top + rect.height / 2; const inside = this.props.treeView.fileSysMode && !this.doc.isFolder ? false : pt[0] > Math.min(rect.left + 75, rect.left + rect.width * 0.75) || (!before && this.treeViewOpen && this.childDocs?.length ? true : false); @@ -364,14 +364,25 @@ export class TreeView extends React.Component<TreeViewProps> { const destDoc = this.doc; DocUtils.MakeLink(sourceDoc, destDoc, { link_relationship: 'tree link' }); e.stopPropagation(); + return true; } const docDragData = de.complete.docDragData; if (docDragData && pt[0] < rect.left + rect.width) { if (docDragData.draggedDocuments[0] === this.doc) return true; - if (this.dropDocuments(docDragData.droppedDocuments, before, inside, docDragData.dropAction, docDragData.removeDocument, docDragData.moveDocument, docDragData.treeViewDoc === this.props.treeView.props.Document)) { - e.stopPropagation(); - } + const added = this.dropDocuments( + docDragData.droppedDocuments, // + before, + inside, + docDragData.dropAction, + docDragData.removeDocument, + docDragData.moveDocument, + docDragData.treeViewDoc === this.props.treeView.props.Document + ); + e.stopPropagation(); + !added && e.preventDefault(); + return added; } + return false; }; dropping: boolean = false; @@ -391,7 +402,7 @@ export class TreeView extends React.Component<TreeViewProps> { const canAdd = (!this.props.treeView.outlineMode && !StrCast((inside ? this.props.document : this.props.treeViewParent)?.treeViewFreezeChildren).includes('add')) || forceAdd; if (canAdd) { this.props.parentTreeView instanceof TreeView && (this.props.parentTreeView.dropping = true); - const res = UndoManager.RunInTempBatch(() => droppedDocuments.reduce((added, d) => (move ? move(d, undefined, addDoc) || (dropAction === 'proto' ? addDoc(d) : false) : addDoc(d)) || added, false)); + const res = droppedDocuments.reduce((added, d) => (move ? move(d, undefined, addDoc) || (dropAction === 'proto' ? addDoc(d) : false) : addDoc(d)) || added, false); this.props.parentTreeView instanceof TreeView && (this.props.parentTreeView.dropping = false); return res; } @@ -460,7 +471,7 @@ export class TreeView extends React.Component<TreeViewProps> { addDoc, remDoc, moveDoc, - this.props.dropAction, + this.props.dragAction, this.props.addDocTab, this.titleStyleProvider, this.props.ScreenToLocalTransform, @@ -585,12 +596,12 @@ export class TreeView extends React.Component<TreeViewProps> { downY = e.clientY; e.stopPropagation(); }} - onClick={e => { + onClick={undoable(e => { if (this.props.isContentActive() && Math.abs(e.clientX - downX) < 3 && Math.abs(e.clientY - downY) < 3) { !this.props.treeView.outlineMode && (this.doc.treeViewSortCriterion = sortKeys[(curSortIndex + 1) % sortKeys.length]); e.stopPropagation(); } - }}> + }, 'sort order')}> {!docs ? null : TreeView.GetChildElements( @@ -604,7 +615,7 @@ export class TreeView extends React.Component<TreeViewProps> { addDoc, remDoc, moveDoc, - StrCast(this.doc.childDropAction, this.props.dropAction) as dropActionType, + StrCast(this.doc.childDragAction, this.props.dragAction) as dropActionType, this.props.addDocTab, this.titleStyleProvider, this.props.ScreenToLocalTransform, @@ -904,7 +915,6 @@ export class TreeView extends React.Component<TreeViewProps> { hideDecorationTitle={this.props.treeView.outlineMode} hideResizeHandles={this.props.treeView.outlineMode} styleProvider={this.titleStyleProvider} - enableDragWhenActive={true} onClickScriptDisable="never" // tree docViews have a script to show fields, etc. docViewPath={this.props.treeView.props.docViewPath} treeViewDoc={this.props.treeView.props.Document} @@ -914,7 +924,7 @@ export class TreeView extends React.Component<TreeViewProps> { pinToPres={emptyFunction} onClick={this.onChildClick} onDoubleClick={this.onChildDoubleClick} - dropAction={this.props.dropAction} + dragAction={this.props.dragAction} moveDocument={this.move} removeDocument={this.props.removeDoc} ScreenToLocalTransform={this.getTransform} @@ -925,7 +935,7 @@ export class TreeView extends React.Component<TreeViewProps> { PanelHeight={return18} contextMenuItems={this.contextMenuItems} renderDepth={1} - isContentActive={this.props.isContentActive} + isContentActive={emptyFunction} //this.props.isContentActive} isDocumentActive={this.props.isContentActive} focus={this.refocus} whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged} @@ -950,7 +960,7 @@ export class TreeView extends React.Component<TreeViewProps> { fontWeight: Doc.IsSearchMatch(this.doc) !== undefined ? 'bold' : undefined, textDecoration: Doc.GetT(this.doc, 'title', 'string', true) ? 'underline' : undefined, outline: this.doc === Doc.ActiveDashboard ? 'dashed 1px #06123232' : undefined, - pointerEvents: !this.props.isContentActive() && !SnappingManager.GetIsDragging() ? 'none' : undefined, + pointerEvents: !this.props.isContentActive() ? 'none' : undefined, }}> {view} </div> @@ -1132,7 +1142,7 @@ export class TreeView extends React.Component<TreeViewProps> { add: (doc: Doc | Doc[], relativeTo?: Doc, before?: boolean) => boolean, remove: undefined | ((doc: Doc | Doc[]) => boolean), move: DragManager.MoveFunction, - dropAction: dropActionType, + dragAction: dropActionType, addDocTab: (doc: Doc, where: OpenWhere) => boolean, styleProvider: undefined | StyleProviderFunc, screenToLocalXf: () => Transform, @@ -1218,7 +1228,7 @@ export class TreeView extends React.Component<TreeViewProps> { panelHeight={rowHeight} dontRegisterView={dontRegisterView} moveDocument={move} - dropAction={dropAction} + dragAction={dragAction} addDocTab={addDocTab} ScreenToLocalTransform={screenToLocalXf} isContentActive={isContentActive} @@ -1239,6 +1249,6 @@ export class TreeView extends React.Component<TreeViewProps> { ScriptingGlobals.add(function TreeView_addNewFolder() { TreeView._editTitleOnLoad = { id: Utils.GenerateGuid(), parent: undefined }; - const opts = { title: 'Untitled folder', _stayInCollection: true, isFolder: true }; + 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/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 36472dd2e..ba31916a7 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -418,15 +418,16 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @undoBatch internalLinkDrop(e: Event, de: DragManager.DropEvent, linkDragData: DragManager.LinkDragData, xp: number, yp: number) { if (linkDragData.linkDragView.props.docViewPath().includes(this.props.docViewPath().lastElement())) { - // dragged document is a child of this collection - if (!linkDragData.linkDragView.props.CollectionFreeFormDocumentView?.() || linkDragData.dragDocument.embedContainer !== this.props.Document) { - // if the source doc view's embedContainer isn't this same freeformcollectionlinkDragData.dragDocument.embedContainer === this.props.Document + let added = false; + // do nothing if link is dropped into any freeform view parent of dragged document + if (!linkDragData.dragDocument.embedContainer || linkDragData.dragDocument.embedContainer !== this.rootDoc) { const source = Docs.Create.TextDocument('', { _width: 200, _height: 75, x: xp, y: yp, title: 'dropped annotation' }); - this.props.addDocument?.(source); + 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 } - e.stopPropagation(); // do nothing if link is dropped into any freeform view parent of dragged document - return true; + e.stopPropagation(); + !added && e.preventDefault(); + return added; } return false; } @@ -1052,7 +1053,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection if (this.layoutDoc._Transform || this.props.Document.treeViewOutlineMode === TreeViewType.outline) return; e.stopPropagation(); const docHeight = NumCast(this.rootDoc[Doc.LayoutFieldKey(this.rootDoc) + '_nativeHeight'], this.nativeHeight); - const scrollable = NumCast(this.layoutDoc[this.scaleFieldKey], 1) === 1 && docHeight > this.props.PanelHeight() / this.nativeDimScaling; + const scrollable = NumCast(this.layoutDoc[this.scaleFieldKey], 1) === 1 && docHeight > this.props.PanelHeight() / this.nativeDimScaling + 1e-4; switch (!e.ctrlKey ? Doc.UserDoc().freeformScrollMode : freeformScrollMode.Pan) { case freeformScrollMode.Pan: // if ctrl is selected then zoom @@ -1240,7 +1241,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }; }; - isContentActive = () => this.props.isSelected() || this.props.isContentActive(); + isContentActive = () => this.props.isContentActive(); @undoBatch @action @@ -1267,7 +1268,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection const engine = this.props.layoutEngine?.() || StrCast(this.props.Document._layoutEngine); const pointerEvents = DocumentDecorations.Instance.Interacting ? 'none' - : this.props.childPointerEvents ?? (this.props.viewDefDivClick || (engine === computePassLayout.name && !this.props.isSelected(true)) ? 'none' : this.props.pointerEvents?.()); + : this.props.childPointerEvents ?? (this.props.viewDefDivClick || (engine === computePassLayout.name && !this.props.isSelected(true)) || this.isContentActive() === false ? 'none' : this.props.pointerEvents?.()); return pointerEvents; }; getChildDocView(entry: PoolData) { @@ -1297,8 +1298,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection childFilters={this.childDocFilters} childFiltersByRanges={this.childDocRangeFilters} searchFilterDocs={this.searchFilterDocs} - isDocumentActive={this.props.childDocumentsActive?.() ? this.props.isDocumentActive : this.isContentActive} - isContentActive={this.props.childContentsActive ?? emptyFunction} + isDocumentActive={this.props.childDocumentsActive?.() || this.rootDoc._isGroup ? this.props.isDocumentActive : this.isContentActive} + isContentActive={this.props.childContentsActive ?? this.isContentActive() === false ? returnFalse : emptyFunction} focus={this.Document._isGroup ? this.groupFocus : this.isAnnotationOverlay ? this.props.focus : this.focus} addDocTab={this.addDocTab} addDocument={this.props.addDocument} @@ -1308,10 +1309,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged} docViewPath={this.props.docViewPath} styleProvider={this.getClusterColor} - canEmbedOnDrag={this.props.childCanEmbedOnDrag} + dragAction={(this.rootDoc.childDragAction ?? this.props.childDragAction) as dropActionType} dataProvider={this.childDataProvider} sizeProvider={this.childSizeProvider} - dropAction={StrCast(this.props.Document.childDropAction) as dropActionType} bringToFront={this.bringToFront} layout_showTitle={this.props.childlayout_showTitle} dontRegisterView={this.props.dontRenderDocuments || this.props.dontRegisterView} @@ -1987,7 +1987,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection onDragOver={e => e.preventDefault()} onContextMenu={this.onContextMenu} style={{ - pointerEvents: SnappingManager.GetIsDragging() && this.childDocs.includes(DragManager.docsBeingDragged.lastElement()) ? 'all' : (this.props.pointerEvents?.() as any), + pointerEvents: this.props.isContentActive() && SnappingManager.GetIsDragging() ? 'all' : (this.props.pointerEvents?.() as any), textAlign: this.isAnnotationOverlay ? 'initial' : undefined, transform: `scale(${this.nativeDimScaling || 1})`, width: `${100 / (this.nativeDimScaling || 1)}%`, @@ -2256,22 +2256,20 @@ class CollectionFreeFormBackgroundGrid extends React.Component<CollectionFreeFor export function CollectionBrowseClick(dv: DocumentView, clientX: number, clientY: number) { const browseTransitionTime = 500; SelectionManager.DeselectAll(); - if ( - dv.props.focus(dv.props.Document, { - willZoomCentered: true, - zoomScale: 0.8, - zoomTime: browseTransitionTime, - }) === undefined - ) { - const selfFfview = !dv.rootDoc._isGroup && dv.ComponentView instanceof CollectionFreeFormView ? dv.ComponentView : undefined; - let parFfview = dv.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView; - while (parFfview?.rootDoc._isGroup) parFfview = parFfview.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView; - const ffview = selfFfview && selfFfview.rootDoc[selfFfview.scaleFieldKey] !== 0.5 ? selfFfview : parFfview; // if focus doc is a freeform that is not at it's default 0.5 scale, then zoom out on it. Otherwise, zoom out on the parent ffview - ffview?.zoomSmoothlyAboutPt(ffview.getTransform().transformPoint(clientX, clientY), 0.5, browseTransitionTime); - Doc.linkFollowHighlight(dv?.props.Document, false); - } else { - DocumentManager.Instance.showDocument(dv.rootDoc, { zoomScale: 0.8, willZoomCentered: true }); - } + DocumentManager.Instance.showDocument(dv.rootDoc, { zoomScale: 0.8, willZoomCentered: true }, (focused: boolean) => { + if (!focused) { + const selfFfview = !dv.rootDoc._isGroup && dv.ComponentView instanceof CollectionFreeFormView ? dv.ComponentView : undefined; + let containers = dv.props.docViewPath(); + let parFfview = dv.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView; + for (var cont of containers) { + parFfview = parFfview ?? cont.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView; + } + while (parFfview?.rootDoc._isGroup) parFfview = parFfview.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView; + const ffview = selfFfview && selfFfview.rootDoc[selfFfview.scaleFieldKey] !== 0.5 ? selfFfview : parFfview; // if focus doc is a freeform that is not at it's default 0.5 scale, then zoom out on it. Otherwise, zoom out on the parent ffview + ffview?.zoomSmoothlyAboutPt(ffview.getTransform().transformPoint(clientX, clientY), 0.5, browseTransitionTime); + Doc.linkFollowHighlight(dv?.props.Document, false); + } + }); } ScriptingGlobals.add(CollectionBrowseClick); ScriptingGlobals.add(function nextKeyFrame(readOnly: boolean) { diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 430e8ecd5..9b0abc48b 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -385,8 +385,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque newCollection._width = this.Bounds.width; newCollection._height = this.Bounds.height; newCollection._isGroup = makeGroup; + newCollection._dragWhenActive = makeGroup; newCollection.forceActive = makeGroup; - newCollection.enableDragWhenActive = makeGroup; newCollection.x = this.Bounds.left; newCollection.y = this.Bounds.top; newCollection.layout_fitWidth = true; diff --git a/src/client/views/collections/collectionGrid/CollectionGridView.tsx b/src/client/views/collections/collectionGrid/CollectionGridView.tsx index e8ae88ae5..cd8b7a0cc 100644 --- a/src/client/views/collections/collectionGrid/CollectionGridView.tsx +++ b/src/client/views/collections/collectionGrid/CollectionGridView.tsx @@ -368,7 +368,7 @@ export class CollectionGridView extends CollectionSubView() { <div className="collectionGridView-contents" ref={this.createDashEventsTarget} - style={{ pointerEvents: !this.props.isContentActive() && !SnappingManager.GetIsDragging() ? 'none' : undefined }} + style={{ pointerEvents: !this.props.isContentActive() ? 'none' : undefined }} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onDrop={this.onExternalDrop}> diff --git a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx index 65578f214..8f90e4444 100644 --- a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx +++ b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx @@ -1,6 +1,6 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@material-ui/core'; -import { action, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; +import { action, IReactionDisposer, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, Opt } from '../../../../fields/Doc'; @@ -50,34 +50,10 @@ export class CollectionLinearView extends CollectionSubView() { width => this.childDocs.length && (this.layoutDoc._width = width), { fireImmediately: true } ); - - this._selectedDisposer = reaction( - () => NumCast(this.layoutDoc.selectedIndex), - i => - runInAction(() => { - this._selectedIndex = i; - let selected: any = undefined; - this.childLayoutPairs.map(async (pair, ind) => { - const isSelected = this._selectedIndex === ind; - if (isSelected) { - selected = pair; - } else { - ScriptCast(DocCast(pair.layout.proto)?.onPointerUp)?.script.run({ this: pair.layout.proto }, console.log); - } - }); - if (selected && selected.layout) { - ScriptCast(DocCast(selected.layout.proto)?.onPointerDown)?.script.run({ this: selected.layout.proto }, console.log); - } - }), - { fireImmediately: true } - ); } protected createDashEventsTarget = (ele: HTMLDivElement | null) => { - //used for stacking and masonry view - this._dropDisposer && this._dropDisposer(); - if (ele) { - this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.layoutDoc); - } + this._dropDisposer?.(); + if (ele) this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.layoutDoc); }; dimension = () => NumCast(this.rootDoc._height); @@ -89,12 +65,8 @@ export class CollectionLinearView extends CollectionSubView() { @action exitLongLinks = () => { - if (DocumentLinksButton.StartLink) { - if (DocumentLinksButton.StartLink.Document) { - action((e: React.PointerEvent<HTMLDivElement>) => { - Doc.UnBrushDoc(DocumentLinksButton.StartLink?.Document as Doc); - }); - } + if (DocumentLinksButton.StartLink?.Document) { + action((e: React.PointerEvent<HTMLDivElement>) => Doc.UnBrushDoc(DocumentLinksButton.StartLink?.Document as Doc)); } DocumentLinksButton.StartLink = undefined; DocumentLinksButton.StartLinkView = undefined; @@ -201,7 +173,7 @@ export class CollectionLinearView extends CollectionSubView() { moveDocument={this.props.moveDocument} addDocTab={this.props.addDocTab} pinToPres={emptyFunction} - dropAction={StrCast(this.layoutDoc.childDropAction) as dropActionType} + dragAction={(this.layoutDoc.childDragAction ?? this.props.childDragAction) as dropActionType} rootSelected={this.props.isSelected} removeDocument={this.props.removeDocument} ScreenToLocalTransform={docXf} diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss index 821c8d804..f87a06033 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss +++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss @@ -8,6 +8,11 @@ display: flex; flex-direction: column; width: 100%; + align-items: center; + + .contentFittingDocumentView { + width: unset; + } .label-wrapper { display: flex; @@ -15,7 +20,6 @@ justify-content: center; height: 20px; } - } .multiColumnResizer { @@ -30,5 +34,4 @@ transition: 0.5s background-color ease; } } - -}
\ No newline at end of file +} diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx index 34fa0343d..10532b9d9 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx +++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx @@ -5,7 +5,6 @@ import { Doc, DocListCast } from '../../../../fields/Doc'; import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; import { returnFalse } from '../../../../Utils'; import { DragManager, dropActionType } from '../../../util/DragManager'; -import { SnappingManager } from '../../../util/SnappingManager'; import { Transform } from '../../../util/Transform'; import { undoBatch } from '../../../util/UndoManager'; import { DocumentView } from '../../nodes/DocumentView'; @@ -194,11 +193,7 @@ export class CollectionMulticolumnView extends CollectionSubView() { let dropInd = -1; if (de.complete.docDragData && this._mainCont) { let curInd = -1; - de.complete.docDragData?.droppedDocuments.forEach( - action((d: Doc) => { - curInd = this.childDocs.indexOf(d); - }) - ); + de.complete.docDragData?.droppedDocuments.forEach(d => (curInd = this.childDocs.indexOf(d))); Array.from(this._mainCont.children).forEach((child, index) => { const brect = child.getBoundingClientRect(); if (brect.x < de.x && brect.x + brect.width > de.x) { @@ -225,6 +220,7 @@ export class CollectionMulticolumnView extends CollectionSubView() { } }) ); + return true; } } return false; @@ -234,8 +230,15 @@ export class CollectionMulticolumnView extends CollectionSubView() { onChildDoubleClickHandler = () => ScriptCast(this.Document.onChildDoubleClick); isContentActive = () => this.props.isSelected() || this.props.isContentActive() || this.props.isAnyChildContentActive(); - isChildContentActive = () => (((this.props.childDocumentsActive?.() || this.Document._childDocumentsActive) && this.props.isDocumentActive?.() && SnappingManager.GetIsDragging()) || this.isContentActive() ? true : false); - getDisplayDoc = (layout: Doc, dxf: () => Transform, width: () => number, height: () => number) => { + isChildContentActive = () => { + const childDocsActive = this.props.childDocumentsActive?.() ?? this.rootDoc.childDocumentsActive; + return this.props.isContentActive?.() === false || childDocsActive === false + ? false // + : this.props.isDocumentActive?.() && childDocsActive + ? true + : undefined; + }; + getDisplayDoc = (layout: Doc, dxf: () => Transform, width: () => number, height: () => number, shouldNotScale: () => boolean) => { return ( <DocumentView Document={layout} @@ -247,8 +250,9 @@ export class CollectionMulticolumnView extends CollectionSubView() { renderDepth={this.props.renderDepth + 1} PanelWidth={width} PanelHeight={height} + shouldNotScale={shouldNotScale} rootSelected={this.rootSelected} - dropAction={StrCast(this.props.Document.childDropAction) as dropActionType} + dragAction={(this.props.Document.childDragAction ?? this.props.childDragAction) as dropActionType} onClick={this.onChildClickHandler} onDoubleClick={this.onChildDoubleClickHandler} suppressSetHeight={true} @@ -284,15 +288,19 @@ export class CollectionMulticolumnView extends CollectionSubView() { const collector: JSX.Element[] = []; for (let i = 0; i < childLayoutPairs.length; i++) { const { layout } = childLayoutPairs[i]; + const aspect = Doc.NativeAspect(layout, undefined, true); + const width = () => this.lookupPixels(layout); + const height = () => PanelHeight() - 2 * NumCast(Document._yMargin) - (BoolCast(Document.showWidthLabels) ? 20 : 0); + const docwidth = () => (layout._layout_forceReflow ? width() : Math.min(height() * aspect, width())); + const docheight = () => Math.min(docwidth() / aspect, height()); const dxf = () => this.lookupIndividualTransform(layout) - .translate(-NumCast(Document._xMargin), -NumCast(Document._yMargin)) + .translate(-NumCast(Document._xMargin) - (width() - docwidth()) / 2, -NumCast(Document._yMargin)) .scale(this.props.NativeDimScaling?.() || 1); - const width = () => this.lookupPixels(layout); - const height = () => PanelHeight() - 2 * NumCast(Document._yMargin) - (BoolCast(Document.showWidthLabels) ? 20 : 0); + const shouldNotScale = () => this.props.fitContentsToBox?.() || BoolCast(layout.freeform_fitContentsToBox); collector.push( - <div className={'document-wrapper'} key={'wrapper' + i} style={{ width: width() }}> - {this.getDisplayDoc(layout, dxf, width, height)} + <div className="document-wrapper" key={'wrapper' + i} style={{ width: width() }}> + {this.getDisplayDoc(layout, dxf, docwidth, docheight, shouldNotScale)} <WidthLabel layout={layout} collectionDoc={Document} /> </div>, <ResizeBar diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.scss b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.scss index 79fb195e8..ec7200a03 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.scss +++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.scss @@ -9,6 +9,12 @@ display: flex; flex-direction: row; height: 100%; + align-items: center; + margin: auto; + + .contentFittingDocumentView { + height: unset; + } .label-wrapper { display: flex; @@ -16,7 +22,6 @@ justify-content: center; height: 20px; } - } .multiRowResizer { @@ -31,5 +36,4 @@ transition: 0.5s background-color ease; } } - -}
\ No newline at end of file +} diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx index bab77ed48..04cfc5456 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx +++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx @@ -5,7 +5,6 @@ import { Doc, DocListCast } from '../../../../fields/Doc'; import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; import { returnFalse } from '../../../../Utils'; import { DragManager, dropActionType } from '../../../util/DragManager'; -import { SnappingManager } from '../../../util/SnappingManager'; import { Transform } from '../../../util/Transform'; import { undoBatch } from '../../../util/UndoManager'; import { DocumentView } from '../../nodes/DocumentView'; @@ -194,11 +193,7 @@ export class CollectionMultirowView extends CollectionSubView() { let dropInd = -1; if (de.complete.docDragData && this._mainCont) { let curInd = -1; - de.complete.docDragData?.droppedDocuments.forEach( - action((d: Doc) => { - curInd = this.childDocs.indexOf(d); - }) - ); + de.complete.docDragData?.droppedDocuments.forEach(d => (curInd = this.childDocs.indexOf(d))); Array.from(this._mainCont.children).forEach((child, index) => { const brect = child.getBoundingClientRect(); if (brect.y < de.y && brect.y + brect.height > de.y) { @@ -225,6 +220,7 @@ export class CollectionMultirowView extends CollectionSubView() { } }) ); + return true; } } return false; @@ -234,8 +230,15 @@ export class CollectionMultirowView extends CollectionSubView() { onChildDoubleClickHandler = () => ScriptCast(this.Document.onChildDoubleClick); isContentActive = () => this.props.isSelected() || this.props.isContentActive() || this.props.isAnyChildContentActive(); - isChildContentActive = () => (((this.props.childDocumentsActive?.() || this.Document._childDocumentsActive) && this.props.isDocumentActive?.() && SnappingManager.GetIsDragging()) || this.isContentActive() ? true : false); - getDisplayDoc = (layout: Doc, dxf: () => Transform, width: () => number, height: () => number) => { + isChildContentActive = () => { + const childDocsActive = this.props.childDocumentsActive?.() ?? this.rootDoc.childDocumentsActive; + return this.props.isContentActive?.() === false || childDocsActive === false + ? false // + : this.props.isDocumentActive?.() && childDocsActive + ? true + : undefined; + }; + getDisplayDoc = (layout: Doc, dxf: () => Transform, width: () => number, height: () => number, shouldNotScale: () => boolean) => { return ( <DocumentView Document={layout} @@ -247,8 +250,9 @@ export class CollectionMultirowView extends CollectionSubView() { renderDepth={this.props.renderDepth + 1} PanelWidth={width} PanelHeight={height} + shouldNotScale={shouldNotScale} rootSelected={this.rootSelected} - dropAction={StrCast(this.props.Document.childDropAction) as dropActionType} + dropAction={StrCast(this.rootDoc.childDragAction) as dropActionType} onClick={this.onChildClickHandler} onDoubleClick={this.onChildDoubleClickHandler} ScreenToLocalTransform={dxf} @@ -257,6 +261,7 @@ export class CollectionMultirowView extends CollectionSubView() { hideResizeHandles={this.props.childHideResizeHandles?.()} hideDecorationTitle={this.props.childHideDecorationTitle?.()} fitContentsToBox={this.props.fitContentsToBox} + dragAction={this.props.childDragAction} focus={this.props.focus} childFilters={this.childDocFilters} childFiltersByRanges={this.childDocRangeFilters} @@ -283,15 +288,19 @@ export class CollectionMultirowView extends CollectionSubView() { const collector: JSX.Element[] = []; for (let i = 0; i < childLayoutPairs.length; i++) { const { layout } = childLayoutPairs[i]; + const aspect = Doc.NativeAspect(layout, undefined, true); + const height = () => this.lookupPixels(layout); + const width = () => PanelWidth() - 2 * NumCast(Document._xMargin) - (BoolCast(Document.showWidthLabels) ? 20 : 0); + const docheight = () => Math.min(width() / aspect, height()); + const docwidth = () => (layout._layout_forceReflow ? width() : Math.min(width(), docheight() * aspect)); const dxf = () => this.lookupIndividualTransform(layout) - .translate(-NumCast(Document._xMargin), -NumCast(Document._yMargin)) + .translate(-NumCast(Document._xMargin) - (width() - docwidth()) / 2, -NumCast(Document._yMargin) - (height() - docheight()) / 2) .scale(this.props.NativeDimScaling?.() || 1); - const height = () => this.lookupPixels(layout); - const width = () => PanelWidth() - 2 * NumCast(Document._xMargin) - (BoolCast(Document.showWidthLabels) ? 20 : 0); + const shouldNotScale = () => this.props.fitContentsToBox?.() || BoolCast(layout.freeform_fitContentsToBox); collector.push( - <div className={'document-wrapper'} style={{ height: height() }} key={'wrapper' + i}> - {this.getDisplayDoc(layout, dxf, width, height)} + <div className="document-wrapper" style={{ height: height() }} key={'wrapper' + i}> + {this.getDisplayDoc(layout, dxf, docwidth, docheight, shouldNotScale)} <HeightLabel layout={layout} collectionDoc={Document} /> </div>, <ResizeBar diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx index 15424de98..ee5bf82ed 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx +++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx @@ -434,7 +434,6 @@ export class CollectionSchemaView extends CollectionSubView() { @action onInternalDrop = (e: Event, de: DragManager.DropEvent) => { if (de.complete.columnDragData) { - e.stopPropagation(); const mouseX = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y)[0]; const index = this.findDropIndex(mouseX); this.moveColumn(de.complete.columnDragData.colIndex, index ?? de.complete.columnDragData.colIndex); @@ -448,6 +447,7 @@ export class CollectionSchemaView extends CollectionSubView() { }); }); + e.stopPropagation(); return true; } const draggedDocs = de.complete.docDragData?.draggedDocuments; @@ -462,7 +462,6 @@ export class CollectionSchemaView extends CollectionSubView() { if (draggedView) DocumentManager.Instance.RemoveView(draggedView); DocumentManager.Instance.AddViewRenderedCb(doc, dv => dv.select(true)); }); - e.stopPropagation(); return true; } return false; @@ -952,7 +951,7 @@ class CollectionSchemaViewDocs extends React.Component<CollectionSchemaViewDocsP styleProvider={DefaultStyleProvider} waitForDoubleClickToClick={returnNever} defaultDoubleClick={returnIgnore} - enableDragWhenActive={true} + dragAction="move" onClickScriptDisable="always" focus={this.props.schema.focusDocument} childFilters={this.props.schema.childDocFilters} @@ -970,7 +969,6 @@ class CollectionSchemaViewDocs extends React.Component<CollectionSchemaViewDocsP hideLinkAnchors={true} layout_fitWidth={returnTrue} scriptContext={this} - canEmbedOnDrag={true} /> </div> ); diff --git a/src/client/views/collections/collectionSchema/SchemaRowBox.tsx b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx index 4f3503751..e8e606030 100644 --- a/src/client/views/collections/collectionSchema/SchemaRowBox.tsx +++ b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx @@ -53,13 +53,13 @@ export class SchemaRowBox extends ViewBoxBaseComponent<FieldViewProps>() { }; onPointerEnter = (e: any) => { - if (!SnappingManager.GetIsDragging()) return; - document.removeEventListener('pointermove', this.onPointerMove); - document.addEventListener('pointermove', this.onPointerMove); + if (SnappingManager.GetIsDragging() && this.props.isContentActive()) { + document.removeEventListener('pointermove', this.onPointerMove); + document.addEventListener('pointermove', this.onPointerMove); + } }; onPointerMove = (e: any) => { - if (!SnappingManager.GetIsDragging()) return; const dragIsRow = DragManager.docsBeingDragged.some(doc => doc.embedContainer === this.schemaDoc); // this.schemaView?._selectedDocs.has(doc) ?? false; if (this._ref && dragIsRow) { diff --git a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx index 97264508c..a958f53ea 100644 --- a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx +++ b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx @@ -76,7 +76,7 @@ export class SchemaTableCell extends React.Component<SchemaTableCellProps> { isSelected: returnFalse, setHeight: returnFalse, select: emptyFunction, - dropAction: 'embed', + dragAction: 'move', bringToFront: emptyFunction, renderDepth: 1, isContentActive: returnFalse, diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx index 5af05e491..737d675aa 100644 --- a/src/client/views/linking/LinkMenuItem.tsx +++ b/src/client/views/linking/LinkMenuItem.tsx @@ -78,7 +78,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { e, e => { const dragData = new DragManager.DocumentDragData([this.props.linkDoc], 'embed'); - dragData.removeDropProperties = ['hidden']; + dragData.dropPropertiesToRemove = ['hidden']; DragManager.StartDocumentDrag([this._editRef.current!], dragData, e.x, e.y); return true; }, @@ -170,7 +170,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { linkSrc: this.props.sourceDoc, linkDoc: this.props.linkDoc, showHeader: false, - location: [this._drag.current?.getBoundingClientRect().right ?? 100, this._drag.current?.getBoundingClientRect().top ?? e.clientY], + location: [(this._drag.current?.getBoundingClientRect().left ?? 100) + 40, (this._drag.current?.getBoundingClientRect().top ?? e.clientY) + 25], noPreview: false, }) }> diff --git a/src/client/views/linking/LinkPopup.tsx b/src/client/views/linking/LinkPopup.tsx index 664a0fa4f..745efea08 100644 --- a/src/client/views/linking/LinkPopup.tsx +++ b/src/client/views/linking/LinkPopup.tsx @@ -70,7 +70,6 @@ export class LinkPopup extends React.Component<LinkPopupProps> { linkSearch={true} linkCreated={this.props.linkCreated} fieldKey="data" - dropAction="move" isSelected={returnTrue} isContentActive={returnTrue} select={returnTrue} diff --git a/src/client/views/nodes/ComparisonBox.scss b/src/client/views/nodes/ComparisonBox.scss index 660045a6f..a12f1c12b 100644 --- a/src/client/views/nodes/ComparisonBox.scss +++ b/src/client/views/nodes/ComparisonBox.scss @@ -93,4 +93,4 @@ display: flex; } } -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx index e6a4635c0..2290e0711 100644 --- a/src/client/views/nodes/ComparisonBox.tsx +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -2,11 +2,10 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; import { Doc, Opt } from '../../../fields/Doc'; -import { Cast, NumCast, StrCast } from '../../../fields/Types'; +import { DocCast, NumCast, StrCast } from '../../../fields/Types'; import { emptyFunction, returnFalse, returnNone, returnZero, setupMoveUpEvents } from '../../../Utils'; -import { Docs } from '../../documents/Documents'; +import { Docs, DocUtils } from '../../documents/Documents'; import { DragManager } from '../../util/DragManager'; -import { SnappingManager } from '../../util/SnappingManager'; import { undoBatch } from '../../util/UndoManager'; import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent'; import { StyleProp } from '../StyleProvider'; @@ -32,47 +31,56 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl get clipWidthKey() { return '_' + this.props.fieldKey + '_clipWidth'; } - componentDidMount() { this.props.setContentView?.(this); } - protected createDropTarget = (ele: HTMLDivElement | null, fieldKey: string, disposerId: number) => { this._disposers[disposerId]?.(); if (ele) { - // create disposers identified by disposerId to remove drag & drop listeners this._disposers[disposerId] = DragManager.MakeDropTarget(ele, (e, dropEvent) => this.internalDrop(e, dropEvent, fieldKey), this.layoutDoc); } }; @undoBatch - private internalDrop = (event: Event, dropEvent: DragManager.DropEvent, fieldKey: string) => { + private internalDrop = (e: Event, dropEvent: DragManager.DropEvent, fieldKey: string) => { if (dropEvent.complete.docDragData) { - event.stopPropagation(); // prevent parent Doc from registering new position so that it snaps back into place const droppedDocs = dropEvent.complete.docDragData?.droppedDocuments; + const added = dropEvent.complete.docDragData.moveDocument?.(droppedDocs, this.rootDoc, (doc: Doc | Doc[]) => this.addDoc(doc instanceof Doc ? doc : doc.lastElement(), fieldKey)); droppedDocs.lastElement().embedContainer = this.dataDoc; - this.dataDoc[fieldKey] = droppedDocs.lastElement(); + !added && e.preventDefault(); + e.stopPropagation(); // prevent parent Doc from registering new position so that it snaps back into place + return added; } }; private registerSliding = (e: React.PointerEvent<HTMLDivElement>, targetWidth: number) => { - e.button !== 2 && + if (e.button !== 2) { setupMoveUpEvents( this, e, this.onPointerMove, emptyFunction, + action((e, doubleTap) => { + if (doubleTap) { + this._isAnyChildContentActive = true; + if (!this.dataDoc[this.fieldKey + '_1']) this.dataDoc[this.fieldKey + '_1'] = DocUtils.copyDragFactory(Doc.UserDoc().emptyNote as Doc); + if (!this.dataDoc[this.fieldKey + '_2']) this.dataDoc[this.fieldKey + '_2'] = DocUtils.copyDragFactory(Doc.UserDoc().emptyNote as Doc); + } + }), + false, + undefined, action(() => { - // on click, animate slider movement to the targetWidth + if (this._isAnyChildContentActive) return; this._animating = 'all 200ms'; + // on click, animate slider movement to the targetWidth this.layoutDoc[this.clipWidthKey] = (targetWidth * 100) / this.props.PanelWidth(); setTimeout( action(() => (this._animating = '')), 200 ); - }), - false + }) ); + } }; @action @@ -86,7 +94,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { const anchor = Docs.Create.ConfigDocument({ - title: 'ImgAnchor:' + this.rootDoc.title, + title: 'CompareAnchor:' + this.rootDoc.title, // set presentation timing properties for restoring view presTransition: 1000, annotationOn: this.rootDoc, @@ -105,12 +113,29 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl e.stopPropagation; // prevent click event action (slider movement) in registerSliding delete this.dataDoc[fieldKey]; }; + moveDoc = (doc: Doc, addDocument: (document: Doc | Doc[]) => boolean, which: string) => this.remDoc(doc, which) && addDocument(doc); + addDoc = (doc: Doc, which: string) => { + this.dataDoc[which] = doc; + return true; + }; + remDoc = (doc: Doc, which: string) => { + if (this.dataDoc[which] === doc) { + this.dataDoc[which] = undefined; + return true; + } + return false; + }; + + whenChildContentsActiveChanged = action((isActive: boolean) => (this._isAnyChildContentActive = isActive)); docStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string): any => { if (property === StyleProp.PointerEvents) return 'none'; return this.props.styleProvider?.(doc, props, property); }; - + moveDoc1 = (doc: Doc | Doc[], targetCol: Doc | undefined, addDoc: any) => (doc instanceof Doc ? [doc] : doc).reduce((res, doc: Doc) => res && this.moveDoc(doc, addDoc, this.fieldKey + '_1'), true); + moveDoc2 = (doc: Doc | Doc[], targetCol: Doc | undefined, addDoc: any) => (doc instanceof Doc ? [doc] : doc).reduce((res, doc: Doc) => res && this.moveDoc(doc, addDoc, this.fieldKey + '_2'), true); + remDoc1 = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((res, doc) => res && this.remDoc(doc, this.fieldKey + '_1'), true); + remDoc2 = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((res, doc) => res && this.remDoc(doc, this.fieldKey + '_2'), true); render() { const clearButton = (which: string) => { return ( @@ -118,33 +143,35 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl className={`clear-button ${which}`} onPointerDown={e => e.stopPropagation()} // prevent triggering slider movement in registerSliding onClick={e => this.clearDoc(e, which)}> - <FontAwesomeIcon className={`clear-button ${which}`} icon={'times'} size="sm" /> + <FontAwesomeIcon className={`clear-button ${which}`} icon="times" size="sm" /> </div> ); }; const displayDoc = (which: string) => { - const whichDoc = Cast(this.dataDoc[which], Doc, null); - // if (whichDoc?.type === DocumentType.MARKER) whichDoc = Cast(whichDoc.annotationOn, Doc, null); - const targetDoc = Cast(whichDoc?.annotationOn, Doc, null) ?? whichDoc; - return whichDoc ? ( + const whichDoc = DocCast(this.dataDoc[which]); + const targetDoc = DocCast(whichDoc?.annotationOn, whichDoc); + return targetDoc ? ( <> <DocumentView {...this.props} + Document={targetDoc} + DataDoc={undefined} + moveDocument={which.endsWith('1') ? this.moveDoc1 : this.moveDoc2} + removeDocument={which.endsWith('1') ? this.remDoc1 : this.remDoc2} NativeWidth={returnZero} NativeHeight={returnZero} - isContentActive={returnFalse} + isContentActive={emptyFunction} isDocumentActive={returnFalse} - styleProvider={this.docStyleProvider} - Document={targetDoc} - DataDoc={undefined} + whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} + styleProvider={this._isAnyChildContentActive ? this.props.styleProvider : this.docStyleProvider} hideLinkButton={true} - pointerEvents={returnNone} + pointerEvents={this._isAnyChildContentActive ? undefined : returnNone} /> {clearButton(which)} </> // placeholder image if doc is missing ) : ( <div className="placeholder"> - <FontAwesomeIcon className="upload-icon" icon={'cloud-upload-alt'} size="lg" /> + <FontAwesomeIcon className="upload-icon" icon="cloud-upload-alt" size="lg" /> </div> ); }; @@ -157,7 +184,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl }; return ( - <div className={`comparisonBox${this.props.isContentActive() || SnappingManager.GetIsDragging() ? '-interactive' : ''}` /* change className to easily disable/enable pointer events in CSS */}> + <div className={`comparisonBox${this.props.isContentActive() ? '-interactive' : ''}` /* change className to easily disable/enable pointer events in CSS */}> {displayBox(`${this.fieldKey}_2`, 1, this.props.PanelWidth() - 3)} <div className="clip-div" style={{ width: this.clipWidth + '%', transition: this._animating, background: StrCast(this.layoutDoc._backgroundColor, 'gray') }}> {displayBox(`${this.fieldKey}_1`, 0, 0)} @@ -169,7 +196,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl left: `calc(${this.clipWidth + '%'} - 0.5px)`, cursor: this.clipWidth < 5 ? 'e-resize' : this.clipWidth / 100 > (this.props.PanelWidth() - 5) / this.props.PanelWidth() ? 'w-resize' : undefined, }} - onPointerDown={e => this.registerSliding(e, this.props.PanelWidth() / 2)} /* if clicked, return slide-bar to center */ + onPointerDown={e => !this._isAnyChildContentActive && this.registerSliding(e, this.props.PanelWidth() / 2)} /* if clicked, return slide-bar to center */ > <div className="slide-handle" /> </div> diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss index 1265651ad..f1627e1e1 100644 --- a/src/client/views/nodes/DocumentView.scss +++ b/src/client/views/nodes/DocumentView.scss @@ -24,7 +24,7 @@ width: 100%; height: 100%; border-radius: inherit; - transition: outline 0.3s linear; + // transition: outline 0.3s linear; // background: $white; //overflow: hidden; transform-origin: center; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 47f3fe37b..dab269474 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -14,7 +14,7 @@ import { ScriptField } from '../../../fields/ScriptField'; import { BoolCast, Cast, DocCast, ImageCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { AudioField } from '../../../fields/URLField'; import { GetEffectiveAcl, TraceMobx } from '../../../fields/util'; -import { emptyFunction, isTargetChildOf as isParentOf, lightOrDark, return18, returnEmptyString, returnFalse, returnTrue, returnVal, returnZero, simulateMouseClick, Utils } from '../../../Utils'; +import { emptyFunction, isTargetChildOf as isParentOf, lightOrDark, returnEmptyString, returnFalse, returnTrue, returnVal, simulateMouseClick, Utils } from '../../../Utils'; import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; import { DocServer } from '../../DocServer'; import { Docs, DocUtils } from '../../documents/Documents'; @@ -47,6 +47,7 @@ import { DocumentLinksButton } from './DocumentLinksButton'; import './DocumentView.scss'; import { FieldViewProps } from './FieldView'; import { FormattedTextBox } from './formattedText/FormattedTextBox'; +import { KeyValueBox } from './KeyValueBox'; import { LinkAnchorBox } from './LinkAnchorBox'; import { PresEffect, PresEffectDirection } from './trails'; import { PinProps, PresBox } from './trails/PresBox'; @@ -163,6 +164,7 @@ export interface DocumentViewSharedProps { docViewPath: () => DocumentView[]; childHideDecorationTitle?: () => boolean; childHideResizeHandles?: () => boolean; + childDragAction?: dropActionType; // allows child documents to be dragged out of collection without holding the embedKey or dragging the doc decorations title bar. dataTransition?: string; // specifies animation transition - used by collectionPile and potentially other layout engines when changing the size of documents so that the change won't be abrupt styleProvider: Opt<StyleProviderFunc>; setTitleFocus?: () => void; @@ -182,7 +184,7 @@ export interface DocumentViewSharedProps { pinToPres: (document: Doc, pinProps: PinProps) => void; ScreenToLocalTransform: () => Transform; bringToFront: (doc: Doc, sendToBack?: boolean) => void; - canEmbedOnDrag?: boolean; + dragAction?: dropActionType; treeViewDoc?: Doc; xPadding?: number; yPadding?: number; @@ -194,7 +196,6 @@ export interface DocumentViewSharedProps { forceAutoHeight?: boolean; disableBrushing?: boolean; // should highlighting for this view be disabled when same document in another view is hovered over. onClickScriptDisable?: 'never' | 'always'; // undefined = only when selected - enableDragWhenActive?: boolean; waitForDoubleClickToClick?: () => 'never' | 'always' | undefined; defaultDoubleClick?: () => 'default' | 'ignore' | undefined; pointerEvents?: () => Opt<string>; @@ -314,7 +315,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps @computed get titleHeight() { return this.props?.styleProvider?.(this.layoutDoc, this.props, StyleProp.TitleHeight) || 0; } - @computed get pointerEvents(): 'none' | 'all' | 'visiblePainted' | undefined { + get pointerEvents(): 'none' | 'all' | 'visiblePainted' | undefined { return this.props.styleProvider?.(this.Document, this.props, StyleProp.PointerEvents + (this.props.isSelected() ? ':selected' : '')); } @computed get finalLayoutKey() { @@ -355,11 +356,17 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps componentDidMount() { this.setupHandlers(); } - + preDropFunc = (e: Event, de: DragManager.DropEvent) => { + const dropAction = this.layoutDoc.dropAction as dropActionType; + if (de.complete.docDragData && this.isContentActive() && !this.props.treeViewDoc) { + dropAction && (de.complete.docDragData.dropAction = dropAction); + e.stopPropagation(); + } + }; setupHandlers() { this.cleanupHandlers(false); if (this._mainCont.current) { - this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, this.drop.bind(this), this.props.Document); + this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, this.drop.bind(this), this.props.Document, this.preDropFunc); this._multiTouchDisposer = InteractionUtils.MakeMultiTouchTarget(this._mainCont.current, this.onTouchStart.bind(this)); this._holdDisposer = InteractionUtils.MakeHoldTouchTarget(this._mainCont.current, this.handle1PointerHoldStart.bind(this)); } @@ -387,7 +394,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps dragData.treeViewDoc = this.props.treeViewDoc; dragData.removeDocument = this.props.removeDocument; dragData.moveDocument = this.props.moveDocument; - dragData.canEmbed = this.props.canEmbedOnDrag; + dragData.canEmbed = this.rootDoc.dragAction ?? this.props.dragAction ? true : false; const ffview = this.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView; ffview && runInAction(() => (ffview.ChildDrag = this.props.DocumentView())); DragManager.StartDocumentDrag( @@ -478,7 +485,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps : ''; DocumentViewInternal.addDocTabFunc = oldFunc; }; - clickFunc = () => (this.props.Document.dontUndo ? func() : UndoManager.RunInBatch(func, 'click ' + this.rootDoc.title)); + clickFunc = () => UndoManager.RunInBatch(func, 'click ' + this.rootDoc.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.Document.rootDocument) && !(e.ctrlKey || e.button > 0)) { @@ -486,8 +493,13 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps } preventDefault = false; } - - this._singleClickFunc = clickFunc ?? (() => this._componentView?.select?.(e.ctrlKey || e.metaKey, e.shiftKey) ?? this.props.select(e.ctrlKey || e.metaKey || e.shiftKey)); + const sendToBack = e.altKey; + this._singleClickFunc = + clickFunc ?? + (() => + sendToBack + ? this.props.DocumentView().props.CollectionFreeFormDocumentView?.().props.bringToFront(this.rootDoc, true) + : this._componentView?.select?.(e.ctrlKey || e.metaKey, e.shiftKey) ?? this.props.select(e.ctrlKey || e.metaKey || e.shiftKey)); const waitFordblclick = this.props.waitForDoubleClickToClick?.() ?? this.Document.waitForDoubleClickToClick; if ((clickFunc && waitFordblclick !== 'never') || waitFordblclick === 'always') { this._doubleClickTimeout && clearTimeout(this._doubleClickTimeout); @@ -506,7 +518,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps onPointerDown = (e: React.PointerEvent): void => { this._longPressSelector = setTimeout(() => { if (DocumentView.LongPress) { - if (this.rootDoc.dontUndo) { + if (this.rootDoc.undoIgnoreFields) { runInAction(() => (UndoStack.HideInline = !UndoStack.HideInline)); } else { this.props.select(false); @@ -535,7 +547,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps //if (this.props.isSelected(true) && this.rootDoc.type !== DocumentType.PDF && this.layoutDoc._type_collection !== CollectionViewType.Docking) e.preventDefault(); // listen to move events if document content isn't active or document is draggable - if (!this.layoutDoc._lockedPosition && (!this.isContentActive() || this.props.enableDragWhenActive || this.rootDoc.enableDragWhenActive)) { + if (!this.layoutDoc._lockedPosition && (!this.isContentActive() || BoolCast(this.rootDoc._dragWhenActive))) { document.addEventListener('pointermove', this.onPointerMove); } } @@ -549,7 +561,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps if (!Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, Date.now())) { this.cleanupPointerEvents(); - this.startDragging(this._downX, this._downY, ((e.ctrlKey || e.altKey) && 'embed') || ((this.Document.dropAction || this.props.dropAction || undefined) as dropActionType)); + this._longPressSelector && clearTimeout(this._longPressSelector); + this.startDragging(this._downX, this._downY, ((e.ctrlKey || e.altKey) && 'embed') || ((this.Document.dragAction || this.props.dragAction || undefined) as dropActionType)); } }; @@ -608,16 +621,17 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps @undoBatch @action drop = (e: Event, de: DragManager.DropEvent) => { - if (this.props.dontRegisterView || this.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) return; + if (this.props.dontRegisterView || this.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) return false; if (this.props.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 don't have permission to modify the destination." : 'Linking to document tabs not yet supported. Drop link on document content.'); - return; + return true; } const linkdrag = de.complete.annoDragData ?? de.complete.linkDragData; if (linkdrag) { linkdrag.linkSourceDoc = linkdrag.linkSourceGetAnchor(); if (linkdrag.linkSourceDoc) { - e.stopPropagation(); if (de.complete.annoDragData && !de.complete.annoDragData.dropDocument) { de.complete.annoDragData.dropDocument = de.complete.annoDragData.dropDocCreator(undefined); } @@ -625,8 +639,11 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps const dropDoc = de.complete.annoDragData?.dropDocument ?? this._componentView?.getAnchor?.(true) ?? this.rootDoc; de.complete.linkDocument = DocUtils.MakeLink(linkdrag.linkSourceDoc, dropDoc, {}, undefined, [de.x, de.y - 50]); } + e.stopPropagation(); + return true; } } + return false; }; @undoBatch @@ -712,6 +729,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps const templateDoc = Cast(this.props.Document[StrCast(this.props.Document.layout_fieldKey)], Doc, null); const appearance = cm.findByDescription('UI Controls...'); const appearanceItems: ContextMenuProps[] = appearance && 'subitems' in appearance ? appearance.subitems : []; + + if (this.props.renderDepth === 0) { + appearanceItems.push({ description: 'Open in Lightbox', event: () => LightboxView.SetLightboxDoc(this.rootDoc), icon: 'hand-point-right' }); + } !Doc.noviceMode && templateDoc && appearanceItems.push({ description: 'Open Template ', event: () => this.props.addDocTab(templateDoc, OpenWhere.addRight), icon: 'eye' }); !appearance && appearanceItems.length && cm.addItem({ description: 'UI Controls...', subitems: appearanceItems, icon: 'compass' }); @@ -851,7 +872,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps setHeight = (height: number) => (this.layoutDoc._height = height); setContentView = action((view: { getAnchor?: (addAsAnnotation: boolean) => Doc; forward?: () => boolean; back?: () => boolean }) => (this._componentView = view)); isContentActive = (outsideReaction?: boolean): boolean | undefined => { - return this.props.isContentActive() === false + // 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 active it or its contents (eg clicking) + return this.props.isContentActive() === false || this.props.pointerEvents?.() === 'none' || (this.rootDoc.pointerEvents === 'none' && !StrCast(this.props.LayoutTemplateString).includes(KeyValueBox.name)) ? false : Doc.ActiveTool !== InkTool.None || SnappingManager.GetIsDragging() || @@ -871,17 +895,19 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps .some(doc => Doc.AreProtosEqual(DocCast(doc.annotationOn), this.rootDoc)); const childOverlayed = () => Array.from(DocumentManager._overlayViews).some(view => Doc.AreProtosEqual(view.rootDoc, this.rootDoc)); return !this.props.LayoutTemplateString && - !this.props.isSelected() && + !this.isContentActive() && LightboxView.LightboxDoc !== this.rootDoc && this.thumb && !Doc.AreProtosEqual(DocumentLinksButton.StartLink, this.rootDoc) && - ((!childHighlighted() && !childOverlayed() && !Doc.isBrushedHighlightedDegree(this.rootDoc)) || this.rootDoc._type_collection === CollectionViewType.Docking) && - !this._componentView?.isAnyChildContentActive?.() + ((!childHighlighted() && !childOverlayed() && !Doc.isBrushedHighlightedDegree(this.rootDoc)) || this.rootDoc._type_collection === CollectionViewType.Docking) ? true : false; }; childFilters = () => [...this.props.childFilters(), ...StrListCast(this.layoutDoc.childFilters)]; - contentPointerEvents = () => (!this.disableClickScriptFunc && this.onClickHandler ? 'none' : this.pointerEvents); + + /// disable pointer events on content when there's an enabled onClick script (but not the browse script), or if contents are marked inactive + contentPointerEvents = () => ((!this.disableClickScriptFunc && this.onClickHandler && !this.props.onBrowseClick?.()) || this.isContentActive() === false ? 'none' : this.pointerEvents); + @computed get contents() { TraceMobx(); const isInk = StrCast(this.layoutDoc.layout).includes(InkingStroke.name) && !this.props.LayoutTemplateString; @@ -889,7 +915,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps <div className="documentView-contentsView" style={{ - pointerEvents: (isInk ? 'none' : this.pointerEvents) ?? 'all', + pointerEvents: (isInk ? 'none' : this.contentPointerEvents()) ?? 'all', height: this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined, }}> {!this._retryThumb || !this.thumbShown() ? null : ( @@ -1085,7 +1111,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps ); const targetDoc = showTitle?.startsWith('_') ? this.layoutDoc : this.rootDoc; const background = StrCast( - SharingManager.Instance.users.find(users => users.user.email === this.dataDoc.author)?.sharingDoc.userColor, + SharingManager.Instance.users.find(u => u.user.email === this.dataDoc.author)?.sharingDoc.userColor, Doc.UserDoc().layout_showTitle && [DocumentType.RTF, DocumentType.COL].includes(this.rootDoc.type as any) ? StrCast(Doc.SharingDoc().userColor) : 'rgba(0,0,0,0.4)' ); const sidebarWidthPercent = +StrCast(this.layoutDoc.layout_sidebarWidthPercent).replace('%', ''); diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index 85dd779fc..4ebf22ddf 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -45,7 +45,7 @@ export interface FieldViewProps extends DocumentViewSharedProps { @observer export class FieldView extends React.Component<FieldViewProps> { public static LayoutString(fieldType: { name: string }, fieldStr: string) { - return `<${fieldType.name} {...props} fieldKey={'${fieldStr}'}/>`; //e.g., "<ImageBox {...props} fieldKey={"data} />" + return `<${fieldType.name} {...props} fieldKey={'${fieldStr}'}/>`; //e.g., "<ImageBox {...props} fieldKey={'data'} />" } @computed diff --git a/src/client/views/nodes/FunctionPlotBox.tsx b/src/client/views/nodes/FunctionPlotBox.tsx index 8a88bede6..61711417f 100644 --- a/src/client/views/nodes/FunctionPlotBox.tsx +++ b/src/client/views/nodes/FunctionPlotBox.tsx @@ -80,9 +80,10 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps> @undoBatch drop = (e: Event, de: DragManager.DropEvent) => { if (de.complete.docDragData?.droppedDocuments.length) { + const added = de.complete.docDragData.droppedDocuments.reduce((res, doc) => res && Doc.AddDocToList(this.dataDoc, this.props.fieldKey, doc), true); + !added && e.preventDefault(); e.stopPropagation(); // prevent parent Doc from registering new position so that it snaps back into place - de.complete.docDragData.droppedDocuments.map(doc => Doc.AddDocToList(this.dataDoc, this.props.fieldKey, doc)); - return false; + return added; } return false; }; diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 9acbee1e7..909a420fe 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -135,19 +135,17 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp @action drop = (e: Event, de: DragManager.DropEvent) => { if (de.complete.docDragData) { + let added: boolean | undefined = undefined; const targetIsBullseye = (ele: HTMLElement): boolean => { if (!ele) return false; if (ele === this._overlayIconRef.current) return true; return targetIsBullseye(ele.parentElement as HTMLElement); }; if (de.metaKey || targetIsBullseye(e.target as HTMLElement)) { - de.complete.docDragData.droppedDocuments.forEach( - action((drop: Doc) => { - Doc.AddDocToList(this.dataDoc, this.fieldKey + '-alternates', drop); - this.rootDoc[this.fieldKey + '_usePath'] = 'alternate:hover'; - e.stopPropagation(); - }) - ); + added = de.complete.docDragData.droppedDocuments.reduce((last: boolean, drop: Doc) => { + this.rootDoc[this.fieldKey + '_usePath'] = 'alternate:hover'; + return last && Doc.AddDocToList(this.dataDoc, this.fieldKey + '-alternates', drop); + }, true); } else if (de.altKey || !this.dataDoc[this.fieldKey]) { const layoutDoc = de.complete.docDragData?.draggedDocuments[0]; const targetField = Doc.LayoutFieldKey(layoutDoc); @@ -156,10 +154,13 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp this.dataDoc[this.fieldKey] = ObjectField.MakeCopy(targetDoc[targetField] as ImageField); Doc.SetNativeWidth(this.dataDoc, Doc.NativeWidth(targetDoc), this.fieldKey); Doc.SetNativeHeight(this.dataDoc, Doc.NativeHeight(targetDoc), this.fieldKey); - e.stopPropagation(); } } + added === false && e.preventDefault(); + added !== undefined && e.stopPropagation(); + return added; } + return false; }; @undoBatch @@ -169,14 +170,13 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp setNativeSize = action(() => { const scaling = (this.props.DocumentView?.().props.ScreenToLocalTransform().Scale || 1) / NumCast(this.rootDoc._freeform_scale, 1); const nscale = NumCast(this.props.PanelWidth()) / scaling; - const nh = nscale / NumCast(this.dataDoc[this.fieldKey + '_nativeHeight']); const nw = nscale / NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']); - this.dataDoc[this.fieldKey + '_nativeHeight'] = NumCast(this.dataDoc[this.fieldKey + '_nativeHeight']) * nh; - this.dataDoc[this.fieldKey + '_nativeWidth'] = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']) * nh; - this.rootDoc._freeform_panX = nh * NumCast(this.rootDoc._freeform_panX); - this.rootDoc._freeform_panY = nh * NumCast(this.rootDoc._freeform_panY); - this.dataDoc._freeform_panXMax = this.dataDoc._freeform_panXMax ? nh * NumCast(this.dataDoc._freeform_panXMax) : undefined; - this.dataDoc._freeform_panXMin = this.dataDoc._freeform_panXMin ? nh * NumCast(this.dataDoc._freeform_panXMin) : undefined; + this.dataDoc[this.fieldKey + '_nativeHeight'] = NumCast(this.dataDoc[this.fieldKey + '_nativeHeight']) * nw; + this.dataDoc[this.fieldKey + '_nativeWidth'] = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']) * nw; + this.rootDoc._freeform_panX = nw * NumCast(this.rootDoc._freeform_panX); + this.rootDoc._freeform_panY = nw * NumCast(this.rootDoc._freeform_panY); + this.dataDoc._freeform_panXMax = this.dataDoc._freeform_panXMax ? nw * NumCast(this.dataDoc._freeform_panXMax) : undefined; + this.dataDoc._freeform_panXMin = this.dataDoc._freeform_panXMin ? nw * NumCast(this.dataDoc._freeform_panXMin) : undefined; this.dataDoc._freeform_panYMax = this.dataDoc._freeform_panYMax ? nw * NumCast(this.dataDoc._freeform_panYMax) : undefined; this.dataDoc._freeform_panYMin = this.dataDoc._freeform_panYMin ? nw * NumCast(this.dataDoc._freeform_panYMin) : undefined; }); @@ -387,7 +387,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp ref={this._overlayIconRef} onPointerDown={e => setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, e => (this.rootDoc[`_${this.fieldKey}_usePath`] = usePath === undefined ? 'alternate' : usePath === 'alternate' ? 'alternate:hover' : undefined))} style={{ - display: (SnappingManager.GetIsDragging() && DragManager.DocDragData?.canEmbed) || DocListCast(this.dataDoc[this.fieldKey + '-alternates']).length ? 'block' : 'none', + display: (this.props.isContentActive() !== false && DragManager.DocDragData?.canEmbed) || DocListCast(this.dataDoc[this.fieldKey + '-alternates']).length ? 'block' : 'none', width: 'min(10%, 25px)', height: 'min(10%, 25px)', background: usePath === undefined ? 'white' : usePath === 'alternate' ? 'black' : 'gray', @@ -505,7 +505,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp } })} style={{ - display: !SnappingManager.GetIsDragging() && this.props.thumbShown?.() ? 'none' : undefined, + display: !this.props.isContentActive() && this.props.thumbShown?.() ? 'none' : undefined, width: this.props.PanelWidth() ? undefined : `100%`, height: this.props.PanelWidth() ? undefined : `100%`, pointerEvents: this.layoutDoc._lockedPosition ? 'none' : undefined, diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index 5b6b0b5a7..a6712a3db 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -213,18 +213,18 @@ export class KeyValueBox extends React.Component<FieldViewProps> { document.addEventListener('pointerup', this.onDividerUp); }; - getFieldView = async () => { + getFieldView = () => { const rows = this.rows.filter(row => row.isChecked); if (rows.length > 1) { - const parent = Docs.Create.StackingDocument([], { _layout_autoHeight: true, _width: 300, title: `field views for ${DocCast(this.props.Document.data).title}`, _chromeHidden: true }); + const parent = Docs.Create.StackingDocument([], { _layout_autoHeight: true, _width: 300, title: `field views for ${DocCast(this.props.Document).title}`, _chromeHidden: true }); for (const row of rows) { - const field = this.createFieldView(DocCast(this.props.Document.data), row); + const field = this.createFieldView(DocCast(this.props.Document), row); field && Doc.AddDocToList(parent, 'data', field); row.uncheck(); } return parent; } - return rows.length ? this.createFieldView(DocCast(this.props.Document.data), rows.lastElement()) : undefined; + return rows.length ? this.createFieldView(DocCast(this.props.Document), rows.lastElement()) : undefined; }; createFieldView = (templateDoc: Doc, row: KeyValuePair) => { diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx index d0db60b37..01acdccb7 100644 --- a/src/client/views/nodes/KeyValuePair.tsx +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -67,7 +67,6 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { isSelected: returnFalse, setHeight: returnFalse, select: emptyFunction, - dropAction: 'embed', bringToFront: emptyFunction, renderDepth: 1, isContentActive: returnFalse, diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx index 52f3575cb..4439be0cd 100644 --- a/src/client/views/nodes/LabelBox.tsx +++ b/src/client/views/nodes/LabelBox.tsx @@ -74,7 +74,9 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps & LabelBoxProp if (docDragData && missingParams?.includes((e.target as any).textContent)) { this.paramsDoc[(e.target as any).textContent] = new List<Doc>(docDragData.droppedDocuments.map((d, i) => (d.onDragStart ? docDragData.draggedDocuments[i] : d))); e.stopPropagation(); + return true; } + return false; }; @observable _mouseOver = false; diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx index e86b881a8..9bcd04cf5 100644 --- a/src/client/views/nodes/LinkAnchorBox.tsx +++ b/src/client/views/nodes/LinkAnchorBox.tsx @@ -47,7 +47,7 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() { if (separation > 100) { const dragData = new DragManager.DocumentDragData([this.rootDoc]); dragData.dropAction = 'embed'; - dragData.removeDropProperties = ['link_anchor_1_x', 'link_anchor_1_y', 'link_anchor_2_x', 'link_anchor_2_y', 'onClick']; + dragData.dropPropertiesToRemove = ['link_anchor_1_x', 'link_anchor_1_y', 'link_anchor_2_x', 'link_anchor_2_y', 'onClick']; DragManager.StartDocumentDrag([this._ref.current!], dragData, pt[0], pt[1]); return true; } else { diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx index 93e54ffb7..de0b57fd7 100644 --- a/src/client/views/nodes/MapBox/MapBox.tsx +++ b/src/client/views/nodes/MapBox/MapBox.tsx @@ -512,9 +512,8 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps return this.addDocument(doc, annotationKey); }; - pointerEvents = () => { - return this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' && !MarqueeOptionsMenu.Instance.isShown() ? 'all' : SnappingManager.GetIsDragging() ? undefined : 'none'; - }; + pointerEvents = () => (this.props.isContentActive() && !MarqueeOptionsMenu.Instance.isShown() ? 'all' : 'none'); + @computed get annotationLayer() { return ( <div className="mapBox-annotationLayer" style={{ height: Doc.NativeHeight(this.Document) || undefined }} ref={this._annotationLayer}> diff --git a/src/client/views/nodes/MapBox/MapBox2.tsx b/src/client/views/nodes/MapBox/MapBox2.tsx index 72f37b62c..a9154c5bb 100644 --- a/src/client/views/nodes/MapBox/MapBox2.tsx +++ b/src/client/views/nodes/MapBox/MapBox2.tsx @@ -510,7 +510,7 @@ export class MapBox2 extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }; pointerEvents = () => { - return this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' && !MarqueeOptionsMenu.Instance.isShown() ? 'all' : SnappingManager.GetIsDragging() ? undefined : 'none'; + return this.props.isContentActive() === false ? 'none' : this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' && !MarqueeOptionsMenu.Instance.isShown() ? 'all' : SnappingManager.GetIsDragging() ? undefined : 'none'; }; @computed get annotationLayer() { return ( diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 6d040ca1a..fd4c6366b 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -559,7 +559,6 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps </div> ); } - isPdfContentActive = () => this.isAnyChildContentActive() || this.props.isSelected() || (this.props.renderDepth === 0 && LightboxView.IsLightboxDocView(this.props.docViewPath())); @computed get renderPdfView() { TraceMobx(); const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1; @@ -592,7 +591,6 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps pdf={this._pdf} focus={this.focus} url={this.pdfUrl!.url.pathname} - isContentActive={this.isPdfContentActive} anchorMenuClick={this.anchorMenuClick} loaded={!Doc.NativeAspect(this.dataDoc) ? this.loaded : undefined} setPdfViewer={this.setPdfViewer} diff --git a/src/client/views/nodes/ScriptingBox.tsx b/src/client/views/nodes/ScriptingBox.tsx index 37fda14fc..3ad3c911d 100644 --- a/src/client/views/nodes/ScriptingBox.tsx +++ b/src/client/views/nodes/ScriptingBox.tsx @@ -270,8 +270,12 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable // sets field of the corresponding field key (param name) to be dropped document @action onDrop = (e: Event, de: DragManager.DropEvent, fieldKey: string) => { - Doc.SetInPlace(this.rootDoc, fieldKey, de.complete.docDragData?.droppedDocuments[0], true); - e.stopPropagation(); + if (de.complete.docDragData) { + de.complete.docDragData.droppedDocuments.forEach(doc => Doc.SetInPlace(this.rootDoc, fieldKey, doc, true)); + e.stopPropagation(); + return true; + } + return false; }; // deletes a param from all areas in which it is stored diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index 16705a6b3..34a1229ba 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -918,7 +918,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps ScreenToLocalTransform={this.scrollXf} NativeDimScaling={returnOne} focus={this.focus} - dropAction="embed" childFilters={childFilters} select={emptyFunction} isAnyChildContentActive={returnFalse} @@ -1003,8 +1002,11 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } return this.props.styleProvider?.(doc, props, property); }; - pointerEvents = () => (!this._draggingSidebar && this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' && !MarqueeOptionsMenu.Instance?.isShown() ? 'all' : SnappingManager.GetIsDragging() ? undefined : 'none'); - annotationPointerEvents = () => (this._isAnnotating || SnappingManager.GetIsDragging() || Doc.ActiveTool !== InkTool.None ? 'all' : 'none'); + pointerEvents = () => + !this._draggingSidebar && this.props.isContentActive() && !MarqueeOptionsMenu.Instance?.isShown() + ? 'all' // + : 'none'; + annotationPointerEvents = () => (this.props.isContentActive() && (this._isAnnotating || SnappingManager.GetIsDragging() || Doc.ActiveTool !== InkTool.None) ? 'all' : 'none'); render() { const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1; const pointerEvents = this.layoutDoc._lockedPosition ? 'none' : (this.props.pointerEvents?.() as any); @@ -1013,7 +1015,11 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps <div className="webBox" ref={this._mainCont} - style={{ pointerEvents: this.pointerEvents(), position: SnappingManager.GetIsDragging() ? 'absolute' : undefined, display: !SnappingManager.GetIsDragging() && this.props.thumbShown?.() ? 'none' : undefined }}> + style={{ + pointerEvents: this.pointerEvents(), // + position: SnappingManager.GetIsDragging() ? 'absolute' : undefined, + display: !this.props.isContentActive() && this.props.thumbShown?.() ? 'none' : undefined, + }}> <div className="webBox-background" style={{ backgroundColor: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor) }} /> <div className="webBox-container" diff --git a/src/client/views/nodes/button/FontIconBox.scss b/src/client/views/nodes/button/FontIconBox.scss index f3b43501b..9d9fa26b0 100644 --- a/src/client/views/nodes/button/FontIconBox.scss +++ b/src/client/views/nodes/button/FontIconBox.scss @@ -18,7 +18,7 @@ .fontIconBox-label { color: $white; - bottom: 0; + bottom: -1; position: absolute; text-align: center; font-size: 7px; @@ -27,7 +27,7 @@ border-radius: 8px; padding: 0; width: 100%; - font-family: 'ROBOTO'; + font-family: 'system-ui'; text-transform: uppercase; font-weight: bold; transition: 0.15s; diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index dc2fa8ee8..202a9f851 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -297,7 +297,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps }; dispatchTransaction = (tx: Transaction) => { - if (this._editorView) { + if (this._editorView && (this._editorView as any).docView) { const state = this._editorView.state.apply(tx); this._editorView.updateState(state); @@ -531,36 +531,50 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps @undoBatch @action drop = (e: Event, de: DragManager.DropEvent) => { - if (de.complete.annoDragData) de.complete.annoDragData.dropDocCreator = () => this.getAnchor(true); + if (de.complete.annoDragData) { + de.complete.annoDragData.dropDocCreator = () => this.getAnchor(true); + e.stopPropagation(); + return true; + } const dragData = de.complete.docDragData; if (dragData) { - const draggedDoc = dragData.draggedDocuments.length && dragData.draggedDocuments[0]; - // replace text contents whend dragging with Alt - if (draggedDoc && draggedDoc.type === DocumentType.RTF && !Doc.AreProtosEqual(draggedDoc, this.props.Document) && de.altKey) { - if (draggedDoc.data instanceof RichTextField) { - Doc.GetProto(this.dataDoc)[this.fieldKey] = new RichTextField(draggedDoc.data.Data, draggedDoc.data.Text); - e.stopPropagation(); - } - // embed document when dragg marked as embed - } else if (de.embedKey) { - const target = dragData.droppedDocuments[0]; - const node = schema.nodes.dashDoc.create({ - width: target[Width](), - height: target[Height](), - title: 'dashDoc', - docId: target[Id], - float: 'unset', - }); - if (!['embed', 'copy'].includes((dragData.dropAction ?? '') as any)) { - dragData.removeDocument?.(dragData.draggedDocuments[0]); + const dataDoc = Doc.IsDelegateField(DocCast(this.layoutDoc.proto), this.fieldKey) ? DocCast(this.layoutDoc.proto) : this.dataDoc; + const effectiveAcl = GetEffectiveAcl(dataDoc); + let added = [AclEdit, AclAdmin, AclSelfEdit].includes(effectiveAcl); + const draggedDoc = dragData.draggedDocuments.lastElement(); + if (added) { + // replace text contents when dragging with Alt + if (de.altKey) { + const fieldKey = Doc.LayoutFieldKey(draggedDoc); + if (draggedDoc[fieldKey] instanceof RichTextField && !Doc.AreProtosEqual(draggedDoc, this.props.Document)) { + Doc.GetProto(this.dataDoc)[this.fieldKey] = Field.Copy(draggedDoc[fieldKey]); + } + + // embed document when drag marked as embed + } else if (de.embedKey) { + const node = schema.nodes.dashDoc.create({ + width: draggedDoc[Width](), + height: draggedDoc[Height](), + title: 'dashDoc', + docId: draggedDoc[Id], + float: 'unset', + }); + if (!['embed', 'copy'].includes((dragData.dropAction ?? '') as any)) { + added = dragData.removeDocument?.(draggedDoc) ? true : false; + } + if (added) { + draggedDoc._freeform_fitContentsToBox = true; + draggedDoc.embedContainer = this.rootDoc; + const view = this._editorView!; + view.dispatch(view.state.tr.insert(view.posAtCoords({ left: de.x, top: de.y })!.pos, node)); + } } - target._freeform_fitContentsToBox = true; - target.embedContainer = this.rootDoc; - const view = this._editorView!; - view.dispatch(view.state.tr.insert(view.posAtCoords({ left: de.x, top: de.y })!.pos, node)); - e.stopPropagation(); } // otherwise, fall through to outer collection to handle drop + !added && e.preventDefault(); + e.stopPropagation(); + return added; } + return false; }; getNodeEndpoints(context: Node, node: Node): { from: number; to: number } | null { @@ -865,11 +879,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const optionItems = options && 'subitems' in options ? options.subitems : []; optionItems.push({ description: `Generate Dall-E Image`, event: () => this.generateImage(), icon: 'star' }); optionItems.push({ description: `Ask GPT-3`, event: () => this.askGPT(), icon: 'lightbulb' }); - optionItems.push({ - description: !this.Document._createDocOnCR ? 'Create New Doc on Carriage Return' : 'Allow Carriage Returns', - event: () => (this.layoutDoc._createDocOnCR = !this.layoutDoc._createDocOnCR), - icon: !this.Document._createDocOnCR ? 'grip-lines' : 'bars', - }); + this.props.renderDepth && + optionItems.push({ + description: !this.Document._createDocOnCR ? 'Create New Doc on Carriage Return' : 'Allow Carriage Returns', + event: () => (this.layoutDoc._createDocOnCR = !this.layoutDoc._createDocOnCR), + icon: !this.Document._createDocOnCR ? 'grip-lines' : 'bars', + }); !Doc.noviceMode && optionItems.push({ description: `${this.Document._layout_autoHeight ? 'Lock' : 'Auto'} Height`, @@ -1506,8 +1521,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps this._editorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size)))); } else if (curText && !FormattedTextBox.DontSelectInitialText) { selectAll(this._editorView.state, this._editorView?.dispatch); - } else { - this._editorView.dispatch(this._editorView.state.tr.addStoredMark(schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) }))); } } selectOnLoad && this._editorView!.focus(); @@ -1520,7 +1533,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps this._editorView.dispatch( this._editorView.state.tr.setStoredMarks([ ...(this._editorView.state.storedMarks ?? []), - ...(!this._editorView.state.storedMarks?.some(mark => mark.type === schema.marks.user_mark) ? [schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) })] : []), + ...[schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) })], ...(Doc.UserDoc().fontColor !== 'transparent' && Doc.UserDoc().fontColor ? [schema.mark(schema.marks.pFontColor, { color: StrCast(Doc.UserDoc().fontColor) })] : []), ...(Doc.UserDoc().fontStyle === 'italics' ? [schema.mark(schema.marks.em)] : []), ...(Doc.UserDoc().textDecoration === 'underline' ? [schema.mark(schema.marks.underline)] : []), @@ -1541,6 +1554,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps componentWillUnmount() { Object.values(this._disposers).forEach(disposer => disposer?.()); this.endUndoTypingBatch(); + FormattedTextBox.LiveTextUndo?.end(); + FormattedTextBox.LiveTextUndo = undefined; this.unhighlightSearchTerms(); this._editorView?.destroy(); RichTextMenu.Instance?.TextView === this && RichTextMenu.Instance.updateMenu(undefined, undefined, undefined); @@ -1842,7 +1857,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps tryUpdateScrollHeight = () => { const margins = 2 * NumCast(this.layoutDoc._yMargin, this.props.yPadding || 0); const children = this.ProseRef?.children.length ? Array.from(this.ProseRef.children[0].children) : undefined; - if (children) { + if (children && !SnappingManager.GetIsDragging()) { const toNum = (val: string) => Number(val.replace('px', '').replace('auto', '0')); const toHgt = (node: Element) => { const { height, marginTop, marginBottom } = getComputedStyle(node); @@ -1853,6 +1868,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps if (this.props.setHeight && scrollHeight && !this.props.dontRegisterView) { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation const setScrollHeight = () => (this.rootDoc[this.fieldKey + '_scrollHeight'] = scrollHeight); + if (this.rootDoc === this.layoutDoc || this.layoutDoc.resolvedDataDoc) { setScrollHeight(); } else { @@ -2025,17 +2041,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps _oldWheel: any; render() { TraceMobx(); - const active = this.props.isContentActive() || this.props.isSelected(); - const selected = active; + const active = this.props.isContentActive(); const scale = (this.props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._freeform_scale, 1); const rounded = StrCast(this.layoutDoc.layout_borderRounding) === '100%' ? '-rounded' : ''; - const interactive = (Doc.ActiveTool === InkTool.None || SnappingManager.GetIsDragging()) && (this.layoutDoc.z || !this.layoutDoc._lockedPosition); - if (!selected && FormattedTextBoxComment.textBox === this) setTimeout(FormattedTextBoxComment.Hide); + if (!active && FormattedTextBoxComment.textBox === this) setTimeout(FormattedTextBoxComment.Hide); const minimal = this.props.ignoreAutoHeight; const paddingX = NumCast(this.layoutDoc._xMargin, this.props.xPadding || 0); const paddingY = NumCast(this.layoutDoc._yMargin, this.props.yPadding || 0); - const selPad = (selected && !this.layoutDoc._createDocOnCR) || minimal ? Math.min(paddingY, Math.min(paddingX, 10)) : 0; - const selPaddingClass = selected && !this.layoutDoc._createDocOnCR && paddingY >= 10 ? '-selected' : ''; + const selPad = (active && !this.layoutDoc._createDocOnCR) || minimal ? Math.min(paddingY, Math.min(paddingX, 10)) : 0; + const selPaddingClass = active && !this.layoutDoc._createDocOnCR && paddingY >= 10 ? '-selected' : ''; const styleFromLayoutString = Doc.styleFromLayoutString(this.rootDoc, this.layoutDoc, this.props, scale); // this converts any expressions in the format string to style props. e.g., <FormattedTextBox height='{this._headerHeight}px' > return styleFromLayoutString?.height === '0px' ? null : ( <div @@ -2056,7 +2070,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps width: `${100 / scale}%`, height: `${100 / scale}%`, }), - display: !SnappingManager.GetIsDragging() && this.props.thumbShown?.() ? 'none' : undefined, + display: !this.props.isContentActive() && this.props.thumbShown?.() ? 'none' : undefined, transition: 'inherit', // overflowY: this.layoutDoc._layout_autoHeight ? "hidden" : undefined, color: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color), @@ -2071,7 +2085,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps style={{ overflow: this.layout_autoHeight && this.props.CollectionFreeFormDocumentView?.() ? 'hidden' : undefined, //x this breaks viewing an layout_autoHeight doc in its own tab, or in the lightbox height: this.props.height || (this.layout_autoHeight && this.props.renderDepth && !this.props.suppressSetHeight ? 'max-content' : undefined), - pointerEvents: interactive ? undefined : 'none', + pointerEvents: Doc.ActiveTool === InkTool.None && !this.props.onBrowseClick?.() ? undefined : 'none', }} onContextMenu={this.specificContextMenu} onKeyDown={this.onKeyDown} @@ -2083,11 +2097,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps onPointerDown={this.onPointerDown} onDoubleClick={this.onDoubleClick}> <div - className={`formattedTextBox-outer${selected ? '-selected' : ''}`} + className={`formattedTextBox-outer${active ? '-selected' : ''}`} ref={this._scrollRef} style={{ width: this.props.dontSelectOnLoad ? '100%' : `calc(100% - ${this.layout_sidebarWidthPercent})`, - pointerEvents: !active && !SnappingManager.GetIsDragging() ? 'none' : undefined, overflow: this.layoutDoc._createDocOnCR ? 'hidden' : this.layoutDoc._layout_autoHeight ? 'visible' : undefined, }} onScroll={this.onScroll} @@ -2101,7 +2114,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps paddingRight: StrCast(this.layoutDoc._textBoxPaddingX, `${paddingX - selPad}px`), paddingTop: StrCast(this.layoutDoc._textBoxPaddingY, `${paddingY - selPad}px`), paddingBottom: StrCast(this.layoutDoc._textBoxPaddingY, `${paddingY - selPad}px`), - pointerEvents: !active && !SnappingManager.GetIsDragging() ? (IsFollowLinkScript(this.layoutDoc.onClick) ? 'none' : undefined) : undefined, + // pointerEvents: !active && IsFollowLinkScript(this.layoutDoc.onClick) ? 'none' : undefined, }} /> </div> diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx index 4f7d2e1c1..ef1c3911c 100644 --- a/src/client/views/nodes/trails/PresBox.tsx +++ b/src/client/views/nodes/trails/PresBox.tsx @@ -1029,9 +1029,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { removeDocument = (doc: Doc) => Doc.RemoveDocFromList(this.rootDoc, this.fieldKey, doc); getTransform = () => this.props.ScreenToLocalTransform().translate(-5, -65); // listBox padding-left and pres-box-cont minHeight panelHeight = () => this.props.PanelHeight() - 40; - isContentActive = (outsideReaction?: boolean) => this.props.isContentActive(outsideReaction); - //.ActiveTool === InkTool.None && !this.layoutDoc._lockedPosition && (this.layoutDoc.forceActive || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0) ? true : false; - /** * For sorting the array so that the order is maintained when it is dropped. */ @@ -2462,6 +2459,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { childIgnoreNativeSize={true} moveDocument={returnFalse} ignoreUnrendered={true} + childDragAction="move" //childLayoutFitWidth={returnTrue} childOpacity={returnOne} childClickScript={PresBox.navigateToDocScript} diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx index 34e069046..f31cf6147 100644 --- a/src/client/views/nodes/trails/PresElementBox.tsx +++ b/src/client/views/nodes/trails/PresElementBox.tsx @@ -192,13 +192,14 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { const dragArray = this.presBoxView?._dragArray ?? []; const dragData = new DragManager.DocumentDragData(this.presBoxView?.sortArray() ?? []); if (!dragData.draggedDocuments.length) dragData.draggedDocuments.push(this.rootDoc); - dragData.dropAction = 'move'; dragData.treeViewDoc = this.presBox?._type_collection === CollectionViewType.Tree ? this.presBox : undefined; // this.props.DocumentView?.()?.props.treeViewDoc; dragData.moveDocument = this.props.moveDocument; const dragItem: HTMLElement[] = []; + const classesToRestore = new Map<HTMLElement, string>(); if (dragArray.length === 1) { const doc = this._itemRef.current || dragArray[0]; if (doc) { + classesToRestore.set(doc, doc.className); doc.className = miniView ? 'presItem-miniSlide' : 'presItem-slide'; dragItem.push(doc); } @@ -212,16 +213,19 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { dragItem.push(doc); } - // const dropEvent = () => runInAction(() => this._dragging = false); if (activeItem) { + runInAction(() => (this._dragging = true)); DragManager.StartDocumentDrag( dragItem.map(ele => ele), dragData, e.clientX, e.clientY, - undefined + undefined, + action(() => { + Array.from(classesToRestore).forEach(pair => (pair[0].className = pair[1])); + this._dragging = false; + }) ); - // runInAction(() => this._dragging = true); return true; } return false; @@ -536,7 +540,6 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { <div className="presItem-number" title="select without navigation" - style={{ pointerEvents: this.presBoxView?.isContentActive() ? 'all' : undefined }} onPointerDown={e => { e.stopPropagation(); if (this._itemRef.current && this._dragRef.current) { diff --git a/src/client/views/pdf/PDFViewer.scss b/src/client/views/pdf/PDFViewer.scss index 470aa3eb1..cfe07f6cb 100644 --- a/src/client/views/pdf/PDFViewer.scss +++ b/src/client/views/pdf/PDFViewer.scss @@ -26,15 +26,9 @@ .textLayer { opacity: unset; mix-blend-mode: multiply; // bcz: makes text fuzzy! - - // span { - // padding-right: 5px; - // padding-bottom: 4px; - // } } - .textLayer ::selection { - background: #accef7; + background: #accef76a; } // should match the backgroundColor in createAnnotation() diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index dd202418b..0fd93868a 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -483,16 +483,17 @@ export class PDFViewer extends React.Component<IViewerProps> { } }; - pointerEvents = () => (this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' && !MarqueeOptionsMenu.Instance.isShown() ? 'all' : SnappingManager.GetIsDragging() ? undefined : 'none'); + pointerEvents = () => + this.props.isContentActive() && !MarqueeOptionsMenu.Instance.isShown() + ? 'all' // + : 'none'; @computed get annotationLayer() { + const inlineAnnos = this.inlineTextAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y)).filter(anno => !anno.hidden); return ( <div className="pdfViewerDash-annotationLayer" style={{ height: Doc.NativeHeight(this.props.Document), transform: `scale(${NumCast(this.props.layoutDoc._freeform_scale, 1)})` }} ref={this._annotationLayer}> - {this.inlineTextAnnotations - .sort((a, b) => NumCast(a.y) - NumCast(b.y)) - .filter(anno => !anno.hidden) - .map(anno => ( - <Annotation {...this.props} fieldKey={this.props.fieldKey + '_annotations'} pointerEvents={this.pointerEvents} showInfo={this.showInfo} dataDoc={this.props.dataDoc} anno={anno} key={`${anno[Id]}-annotation`} /> - ))} + {inlineAnnos.map(anno => ( + <Annotation {...this.props} fieldKey={this.props.fieldKey + '_annotations'} pointerEvents={this.pointerEvents} showInfo={this.showInfo} dataDoc={this.props.dataDoc} anno={anno} key={`${anno[Id]}-annotation`} /> + ))} </div> ); } @@ -514,10 +515,10 @@ export class PDFViewer extends React.Component<IViewerProps> { panelWidth = () => this.props.PanelWidth() / (this.props.NativeDimScaling?.() || 1); panelHeight = () => this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1); transparentFilter = () => [...this.props.childFilters(), Utils.IsTransparentFilter()]; - opaqueFilter = () => [...this.props.childFilters(), Utils.noDragsDocFilter, ...(DragManager.docsBeingDragged.length ? [] : [Utils.IsOpaqueFilter()])]; + opaqueFilter = () => [...this.props.childFilters(), Utils.noDragsDocFilter, ...(DragManager.docsBeingDragged.length && this.props.isContentActive() ? [] : [Utils.IsOpaqueFilter()])]; childStyleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string): any => { if (doc instanceof Doc && property === StyleProp.PointerEvents) { - if (this.inlineTextAnnotations.includes(doc)) return 'none'; + if (this.inlineTextAnnotations.includes(doc) || this.props.isContentActive() === false) return 'none'; return 'all'; } return this.props.styleProvider?.(doc, props, property); @@ -536,8 +537,8 @@ export class PDFViewer extends React.Component<IViewerProps> { NativeWidth={returnZero} NativeHeight={returnZero} setContentView={emptyFunction} // override setContentView to do nothing - pointerEvents={SnappingManager.GetIsDragging() ? returnAll : returnNone} // freeform view doesn't get events unless something is being dragged onto it. - childPointerEvents="all" // but freeform children need to get events to allow text editing, etc + pointerEvents={SnappingManager.GetIsDragging() && this.props.isContentActive() ? returnAll : returnNone} // freeform view doesn't get events unless something is being dragged onto it. + childPointerEvents={this.props.isContentActive() !== false ? 'all' : 'none'} // but freeform children need to get events to allow text editing, etc renderDepth={this.props.renderDepth + 1} isAnnotationOverlay={true} fieldKey={this.props.fieldKey + '_annotations'} @@ -549,7 +550,6 @@ export class PDFViewer extends React.Component<IViewerProps> { ScreenToLocalTransform={this.overlayTransform} isAnyChildContentActive={returnFalse} isAnnotationOverlayScrollable={true} - dropAction="embed" childFilters={childFilters} select={emptyFunction} bringToFront={emptyFunction} @@ -558,14 +558,15 @@ export class PDFViewer extends React.Component<IViewerProps> { </div> ); @computed get overlayTransparentAnnotations() { - return this.renderAnnotations(this.transparentFilter, 'multiply', DragManager.docsBeingDragged.length ? 'none' : undefined); + const transparentChildren = DocUtils.FilterDocs(DocListCast(this.props.dataDoc[this.props.fieldKey + '_annotations']), this.transparentFilter(), []); + return !transparentChildren.length ? null : this.renderAnnotations(this.transparentFilter, 'multiply', DragManager.docsBeingDragged.length && this.props.isContentActive() ? 'none' : undefined); } @computed get overlayOpaqueAnnotations() { return this.renderAnnotations(this.opaqueFilter, this.allAnnotations.some(anno => anno.mixBlendMode) ? 'hard-light' : undefined); } @computed get overlayLayer() { return ( - <div style={{ pointerEvents: SnappingManager.GetIsDragging() ? 'all' : 'none' }}> + <div style={{ pointerEvents: this.props.isContentActive() && SnappingManager.GetIsDragging() ? 'all' : 'none' }}> {this.overlayTransparentAnnotations} {this.overlayOpaqueAnnotations} </div> diff --git a/src/client/views/topbar/TopBar.scss b/src/client/views/topbar/TopBar.scss index a1131b92e..ede59a910 100644 --- a/src/client/views/topbar/TopBar.scss +++ b/src/client/views/topbar/TopBar.scss @@ -1,243 +1,256 @@ -@import "../global/globalCssVariables"; - - +@import '../global/globalCssVariables'; + +.iconButton-container.primary { + color: white; + .iconButton-background { + filter: unset; + background: transparent; + } +} +.topbarHeart-red { + .iconButton-container.primary { + .iconButton-content { + color: red; + } + .iconButton-background { + background: black; + } + } +} .topbar-container { - flex-direction: column; - font-size: 10px; - line-height: 1; - overflow-y: auto; - overflow-x: visible; - background: $dark-gray; - overflow: visible; - z-index: 1000; - align-items: center; - height: $topbar-height; - background-color: $dark-gray; - border-bottom: $standard-border; - padding: 0px 10px; - cursor: default; - display: flex; - justify-content: center; + flex-direction: column; + font-size: 10px; + line-height: 1; + overflow-y: auto; + overflow-x: visible; + background: $dark-gray; + overflow: visible; + z-index: 1000; + align-items: center; + height: $topbar-height; + background-color: $dark-gray; + border-bottom: $standard-border; + padding: 0px 10px; + cursor: default; + display: flex; + justify-content: center; + width: 100%; + + .topbar-inner-container { + display: flex; + flex-direction: row; + position: relative; + display: grid; + grid-auto-columns: 33.3% 33.3% 33.3%; width: 100%; + align-items: center; - .topbar-inner-container { - display: flex; - flex-direction: row; - position: relative; - display: grid; - grid-auto-columns: 33.3% 33.3% 33.3%; - width: 100%; - align-items: center; - - // &:first-child { - // height: 20px; - // } - } - - .topbar-button-text { - color: $white; - padding: 10px; - size: 15; + // &:first-child { + // height: 20px; + // } + } - &:hover { - font-weight: 500; - } - } + .topbar-button-text { + color: $white; + padding: 10px; + size: 15; - .topbar-button-icon { - cursor: pointer; - width: fit-content; - display: flex; - justify-content: center; - gap: 4px; - align-items: center; - justify-self: center; - align-self: center; - padding: 5px; - transition: linear 0.2s; - color: $white; - - &:hover { - background-color: darken($color: $light-gray, $amount: 20); - font-weight: 500; - } - } - - .topbar-title { - color: $white; - font-size: 17; + &:hover { font-weight: 500; - } + } + } - .topbar-center { - grid-column: 2; - display: inline-flex; + .topbar-button-icon { + cursor: pointer; + width: fit-content; + display: flex; + justify-content: center; + gap: 4px; + align-items: center; + justify-self: center; + align-self: center; + padding: 5px; + transition: linear 0.2s; + color: $white; + + &:hover { + background-color: darken($color: $light-gray, $amount: 20); + font-weight: 500; + } + } + + .topbar-title { + color: $white; + font-size: 17; + font-weight: 500; + } + + .topbar-center { + grid-column: 2; + display: inline-flex; + justify-content: center; + align-items: center; + gap: 5px; + + .topbar-dashboard-header { + font-weight: 600; + } + } + + .topbar-right { + grid-column: 3; + position: relative; + display: flex; + justify-content: flex-end; + gap: 5px; + margin-right: 5px; + } + + .topbar-left { + grid-column: 1; + color: black; + font-family: 'Roboto'; + position: relative; + display: flex; + width: fit-content; + gap: 5px; + + .logo-container { + font-size: 15; + display: flex; + flex-direction: row; justify-content: center; align-items: center; - gap: 5px; - - .topbar-dashboard-header { - font-weight: 600; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -o-user-select: none; + user-select: none; + + .logo { + background-color: transparent; + width: 25px; + height: 25px; + margin-right: 5px; } - } + } + .topBar-icon:hover { + background-color: $close-red; + } - .topbar-right { - grid-column: 3; - position: relative; - display: flex; - justify-content: flex-end; - gap: 5px; - margin-right: 5px; - } - - .topbar-left { - grid-column: 1; - color: black; + .topbar-lozenge-user, + .topbar-lozenge { + height: 23; + font-size: 12; + color: white; font-family: 'Roboto'; - position: relative; + font-weight: 400; + padding: 4px; + align-self: center; + margin-left: 7px; display: flex; - width: fit-content; - gap: 5px; - - .logo-container { - font-size: 15; - display: flex; - flex-direction: row; - 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; - - .logo { - background-color: transparent; - width: 25px; - height: 25px; - margin-right: 5px; - - } - } + align-items: center; - .topBar-icon:hover { - background-color: $close-red; - } + .topbar-dashSelect { + border: none; + background-color: transparent; + color: black; + font-family: 'Roboto'; + font-size: 17; + font-weight: 500; - .topbar-lozenge-user, - .topbar-lozenge { - height: 23; - font-size: 12; - color: white; - font-family: 'Roboto'; - font-weight: 400; - padding: 4px; - align-self: center; - margin-left: 7px; - display: flex; - align-items: center; - - .topbar-dashSelect { - border: none; - background-color: transparent; - color: black; - font-family: 'Roboto'; - font-size: 17; - font-weight: 500; - - &:hover { - cursor: pointer; - } - } + &:hover { + cursor: pointer; + } } + } + + .topbar-logoff { + border-radius: 3px; + background: olivedrab; + color: white; + display: none; + margin-left: 5px; + padding: 1px 2px 1px 2px; + cursor: pointer; + } - .topbar-logoff { - border-radius: 3px; - background: olivedrab; - color: white; - display: none; - margin-left: 5px; - padding: 1px 2px 1px 2px; - cursor: pointer; - } + .topbar-logoff { + background: red; + } + .topbar-lozenge-user:hover { .topbar-logoff { - background: red; - } - - .topbar-lozenge-user:hover { - .topbar-logoff { - display: inline-block; - } - } - } - - .topbar-barChild { - - &.topbar-collection { - flex: 0 1 auto; - margin-left: 2px; - margin-right: 2px + display: inline-block; } - - &.topbar-input { - margin: 5px; - border-radius: 20px; - border: $dark-gray; - display: block; - width: 130px; - -webkit-transition: width 0.4s; - transition: width 0.4s; - /* align-self: stretch; */ - outline: none; - - &:focus { - width: 500px; - outline: none; - } + } + } + + .topbar-barChild { + &.topbar-collection { + flex: 0 1 auto; + margin-left: 2px; + margin-right: 2px; + } + + &.topbar-input { + margin: 5px; + border-radius: 20px; + border: $dark-gray; + display: block; + width: 130px; + -webkit-transition: width 0.4s; + transition: width 0.4s; + /* align-self: stretch; */ + outline: none; + + &:focus { + width: 500px; + outline: none; } + } - &.topbar-filter { - align-self: stretch; + &.topbar-filter { + align-self: stretch; - button { - transform: none; + button { + transform: none; - &:hover { - transform: none; - } - } + &:hover { + transform: none; + } } + } - &.topbar-submit { - margin-left: 2px; - margin-right: 2px - } + &.topbar-submit { + margin-left: 2px; + margin-right: 2px; + } - &.topbar-close { - color: $white; - max-height: $topbar-height; - } - } + &.topbar-close { + color: $white; + max-height: $topbar-height; + } + } } .topbar-results { - display: flex; - flex-direction: column; - top: 300px; - display: flex; - flex-direction: column; - height: 100%; - overflow: visible; - - .no-result { - width: 500px; - background: $light-gray; - padding: 10px; - height: 50px; - text-transform: uppercase; - text-align: left; - font-weight: bold; - } -}
\ No newline at end of file + display: flex; + flex-direction: column; + top: 300px; + display: flex; + flex-direction: column; + height: 100%; + overflow: visible; + + .no-result { + width: 500px; + background: $light-gray; + padding: 10px; + height: 50px; + text-transform: uppercase; + text-align: left; + font-weight: bold; + } +} diff --git a/src/client/views/topbar/TopBar.tsx b/src/client/views/topbar/TopBar.tsx index 834156295..9f2eafcee 100644 --- a/src/client/views/topbar/TopBar.tsx +++ b/src/client/views/topbar/TopBar.tsx @@ -1,14 +1,16 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Button, IconButton, Size } from 'browndash-components'; -import { action, computed, observable } from 'mobx'; +import { Tooltip } from '@mui/material'; +import { Button, FontSize, IconButton, Size } from 'browndash-components'; +import { action, computed, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { FaBug, FaCamera, FaStamp } from 'react-icons/fa'; -import { Doc } from '../../../fields/Doc'; +import { Doc } from '../../../fields/Doc'; import { AclAdmin } from '../../../fields/DocSymbols'; import { StrCast } from '../../../fields/Types'; import { GetEffectiveAcl } from '../../../fields/util'; import { DocumentManager } from '../../util/DocumentManager'; +import { PingManager } from '../../util/PingManager'; import { ReportManager } from '../../util/ReportManager'; import { ServerStats } from '../../util/ServerStats'; import { SettingsManager } from '../../util/SettingsManager'; @@ -35,7 +37,16 @@ export class TopBar extends React.Component { }; @observable textColor: string = Colors.LIGHT_GRAY; - @observable backgroundColor: string = Colors.DARK_GRAY; + @computed get backgroundColor() { + return PingManager.Instance.IsBeating ? Colors.DARK_GRAY : Colors.MEDIUM_GRAY; + } + + @observable happyHeart: boolean = PingManager.Instance.IsBeating; + setHappyHeart = action((status: boolean) => (this.happyHeart = status)); + dispose = reaction( + () => PingManager.Instance.IsBeating, + isBeating => this.setHappyHeart(isBeating) + ); /** * Returns the left hand side of the topbar. @@ -138,6 +149,17 @@ export class TopBar extends React.Component { <IconButton size={Size.SMALL} color={Colors.LIGHT_GRAY} onClick={ReportManager.Instance.open} icon={<FaBug />} /> <IconButton size={Size.SMALL} color={Colors.LIGHT_GRAY} onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/', '_blank')} icon={<FontAwesomeIcon icon="question-circle" />} /> <IconButton size={Size.SMALL} color={Colors.LIGHT_GRAY} onClick={SettingsManager.Instance.open} icon={<FontAwesomeIcon icon="cog" />} /> + <Tooltip title={<div className="dash-tooltip">{'Server connection ' + (PingManager.Instance.IsBeating ? 'active' : 'broken')}</div>}> + <div className={'topbarHeart' + (this.happyHeart ? '' : '-red')}> + <IconButton + size={Size.SMALL} + onClick={PingManager.Instance.showAlert} + tooltip={'Server is ' + (PingManager.Instance.IsBeating ? '' : 'NOT ') + 'running'} + color={this.happyHeart ? Colors.LIGHT_BLUE : Colors.ERROR_RED} + icon={<FontAwesomeIcon icon={this.happyHeart ? 'heart' : 'heart-broken'} />} + /> + </div> + </Tooltip> {/* <Button text={'Logout'} borderRadius={5} hoverStyle={'gray'} backgroundColor={Colors.DARK_GRAY} color={this.textColor} fontSize={FontSize.SECONDARY} onClick={() => window.location.assign(Utils.prepend('/logout'))} /> */} </div> ); diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 8f9f36153..99712fb04 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -774,7 +774,7 @@ export namespace Doc { linkMap.set(link[Id], await Doc.makeClone(link, cloneMap, linkMap, rtfs, exclusions, pruneDocs, cloneLinks)); } }); - Doc.SetInPlace(copy, 'title', 'CLONE: ' + doc.title, true); + Doc.SetInPlace(copy, 'title', '>:' + doc.title, true); copy.cloneOf = doc; cloneMap.set(doc[Id], copy); @@ -1428,6 +1428,8 @@ export namespace Doc { } } + export const FilterSep = '::'; + // filters document in a container collection: // all documents with the specified value for the specified key are included/excluded // based on the modifiers :"check", "x", undefined @@ -1437,7 +1439,7 @@ export namespace Doc { const childFilters = StrListCast(container[filterField]); runInAction(() => { for (let i = 0; i < childFilters.length; i++) { - const fields = childFilters[i].split(':'); // split key:value:modifier + const fields = childFilters[i].split(FilterSep); // split key:value:modifier if (fields[0] === key && (fields[1] === value || modifiers === 'match' || (fields[2] === 'match' && modifiers === 'remove'))) { if (fields[2] === modifiers && modifiers && fields[1] === value) { if (toggle) modifiers = 'remove'; @@ -1452,7 +1454,7 @@ export namespace Doc { container[filterField] = undefined; } else if (modifiers !== 'remove') { !append && (childFilters.length = 0); - childFilters.push(key + ':' + value + ':' + modifiers); + childFilters.push(key + FilterSep + value + FilterSep + modifiers); container[filterField] = new List<string>(childFilters); } }); diff --git a/src/fields/Proxy.ts b/src/fields/Proxy.ts index 55d1d9ea4..c076f5fe1 100644 --- a/src/fields/Proxy.ts +++ b/src/fields/Proxy.ts @@ -1,5 +1,5 @@ import { Deserializable } from '../client/util/SerializationHelper'; -import { FieldWaiting, Opt } from './Doc'; +import { Field, FieldWaiting, Opt } from './Doc'; import { primitive, serializable } from 'serializr'; import { observable, action, runInAction, computed } from 'mobx'; import { DocServer } from '../client/DocServer'; @@ -39,7 +39,7 @@ export class ProxyField<T extends RefField> extends ObjectField { } [ToScriptString]() { - return 'invalid'; + 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' ? } [ToString]() { return 'ProxyField'; diff --git a/src/fields/documentSchemas.ts b/src/fields/documentSchemas.ts index 757d507be..76b287be7 100644 --- a/src/fields/documentSchemas.ts +++ b/src/fields/documentSchemas.ts @@ -95,12 +95,12 @@ export const documentSchema = createSchema({ link_displayArrow: 'boolean', // toggles directed arrows // drag drop properties - _stayInCollection: 'boolean', // whether document can be dropped into a different collection + _dragOnlyWithinContainer: 'boolean', // whether document can be dropped into a different collection dragFactory: Doc, // the document that serves as the "template" for the onDragStart script. ie, to drag out copies of the dragFactory document. - dropAction: 'string', // override specifying what should happen when this document is dropped (can be "embed", "copy", "move") - targetDropAction: 'string', // allows the target of a drop event to specify the dropAction ("embed", "copy", "move") NOTE: if the document is dropped within the same collection, the dropAction is coerced to 'move' - childDropAction: 'string', // specify the override for what should happen when the child of a collection is dragged from it and dropped (can be "embed" or "copy") - removeDropProperties: listSpec('string'), // properties that should be removed from the embed/copy/etc of this document when it is dropped + dropAction: 'string', // override specifying what should happen when something is dropped on this document (can be "embed", "copy", "move") + dragAction: 'string', // override specifying what should happen when this document s dragged (can be "embed", "copy", "move") + childDragAction: 'string', // specify the override for what should happen when the child of a collection is dragged from it and dropped (can be "embed" or "copy") + dropPropertiesToRemove: listSpec('string'), // properties that should be removed from the embed/copy/etc of this document when it is dropped }); export const collectionSchema = createSchema({ diff --git a/src/fields/util.ts b/src/fields/util.ts index d9ccb528b..0f613d926 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -7,8 +7,8 @@ import { SerializationHelper } from '../client/util/SerializationHelper'; import { UndoManager } from '../client/util/UndoManager'; import { returnZero } from '../Utils'; import CursorField from './CursorField'; -import { aclLevel, Doc, DocListCast, DocListCastAsync, HierarchyMapping, ReverseHierarchyMap, updateCachedAcls } from './Doc'; -import { AclAdmin, AclAugment, AclEdit, AclPrivate, DocAcl, DocData, DocLayout, FieldKeys, ForceServerWrite, Height, Initializing, SelfProxy, Update, UpdatingFromServer, Width } from './DocSymbols'; +import { aclLevel, Doc, DocListCast, DocListCastAsync, HierarchyMapping, ReverseHierarchyMap, StrListCast, updateCachedAcls } from './Doc'; +import { AclAdmin, AclAugment, AclEdit, AclPrivate, AclSelfEdit, DocAcl, DocData, DocLayout, FieldKeys, ForceServerWrite, Height, Initializing, SelfProxy, Update, UpdatingFromServer, Width } from './DocSymbols'; import { Id, OnUpdate, Parent, ToValue } from './FieldSymbols'; import { List } from './List'; import { ObjectField } from './ObjectField'; @@ -33,9 +33,8 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number return true; } - if (value !== undefined) { - value = value[SelfProxy] || value; - } + value = value?.[SelfProxy] ?? value; // convert any Doc type values to Proxy's + const curValue = target.__fieldTuples[prop]; if (curValue === value || (curValue instanceof ProxyField && value instanceof RefField && curValue.fieldId === value[Id])) { // TODO This kind of checks correctly in the case that curValue is a ProxyField and value is a RefField, but technically @@ -85,7 +84,7 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number DocServer.registerDocWithCachedUpdate(receiver, prop as string, curValue); } !receiver[Initializing] && - !receiver.dontUndo && + !StrListCast(receiver.undoIgnoreFields).includes(prop.toString()) && (!receiver[UpdatingFromServer] || receiver[ForceServerWrite]) && UndoManager.AddEvent( { @@ -477,7 +476,8 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any lastValue = ObjectField.MakeCopy(receiver[prop]); }, prop: 'assign list', - } + }, + diff?.items ); } target[Update](op); diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts index ba6d7acfe..820e815d8 100644 --- a/src/server/ApiManagers/UploadManager.ts +++ b/src/server/ApiManagers/UploadManager.ts @@ -43,6 +43,14 @@ export default class UploadManager extends ApiManager { protected initialize(register: Registration): void { register({ method: Method.POST, + subscription: '/ping', + secureHandler: async ({ req, res }) => { + _success(res, { message: 'pong', date: new Date() }); + }, + }); + + register({ + method: Method.POST, subscription: '/concatVideos', secureHandler: async ({ req, res }) => { // req.body contains the array of server paths to the videos |