From 6493d95e92c8ed58bb3a8c07ea4ca28dae82ea1d Mon Sep 17 00:00:00 2001 From: Jenny Yu Date: Tue, 7 Jun 2022 08:51:41 -0700 Subject: removing decorations on recording box --- src/client/documents/Documents.ts | 2815 ++++++++++---------- src/client/views/DocumentDecorations.tsx | 1068 ++++---- src/client/views/nodes/DocumentView.tsx | 2517 ++++++++--------- .../views/nodes/RecordingBox/RecordingView.tsx | 20 +- src/client/views/nodes/trails/PresElementBox.tsx | 10 +- 5 files changed, 3221 insertions(+), 3209 deletions(-) (limited to 'src') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 166e68c94..ba6467da2 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -65,18 +65,18 @@ import { RecordingBox } from "../views/nodes/RecordingBox/RecordingBox"; const defaultNativeImageDim = Number(DFLT_IMAGE_NATIVE_DIM.replace("px", "")); class EmptyBox { - public static LayoutString() { - return ""; - } + public static LayoutString() { + return ""; + } } abstract class FInfo { - description: string = ""; - type?: string; - values?: Field[]; - layoutField?: boolean; // is this field a layout (or datadoc) field - // format?: string; // format to display values (e.g, decimal places, $, etc) - // parse?: ScriptField; // parse a value from a string - constructor(d: string, l: boolean = false) { this.description = d; this.layoutField = l; } + description: string = ""; + type?: string; + values?: Field[]; + layoutField?: boolean; // is this field a layout (or datadoc) field + // format?: string; // format to display values (e.g, decimal places, $, etc) + // parse?: ScriptField; // parse a value from a string + constructor(d: string, l: boolean = false) { this.description = d; this.layoutField = l; } } class BoolInfo extends FInfo { type?= "boolean"; values?: boolean[] = [true, false]; } class NumInfo extends FInfo { type?= "number"; values?: number[] = []; } @@ -93,1423 +93,1426 @@ type DIMt = DimInfo | typeof DimUnit.Pixel | typeof DimUnit.Ratio; type PEVt = PEInfo | "none" | "all"; type DROPt = DAInfo | dropActionType; export class DocumentOptions { - system?: BOOLt = new BoolInfo("is this a system created/owned doc"); - _dropAction?: DROPt = new DAInfo("what should happen to this document when it's dropped somewhere else"); - allowOverlayDrop?: BOOLt = new BoolInfo("can documents be dropped onto this document without using dragging title bar or holding down embed key (ctrl)?"); - 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 ??? "); - color?: string; // foreground color data doc - backgroundColor?: STRt = new StrInfo("background color for data doc"); - _backgroundColor?: STRt = new StrInfo("background color for each template layout doc (overrides backgroundColor)", true); - _autoHeight?: BOOLt = new BoolInfo("whether document automatically resizes vertically to display contents", true); - _headerHeight?: NUMt = new NumInfo("height of document header used for displaying title", true); - _headerFontSize?: NUMt = new NumInfo("font size of header of custom notes", true); - _headerPointerEvents?: PEVt = new PEInfo("types of events the header of a custom text document can consume", true); - _panX?: NUMt = new NumInfo("horizontal pan location of a freeform view", true); - _panY?: NUMt = new NumInfo("vertical pan location of a freeform view", true); - _width?: NUMt = new NumInfo("displayed width of a document", true); - _height?: NUMt = new NumInfo("displayed height of document", true); - _nativeWidth?: NUMt = new NumInfo("native width of document contents (e.g., the pixel width of an image)", true); - _nativeHeight?: NUMt = new NumInfo("native height of document contents (e.g., the pixel height of an image)", true); - _dimMagnitude?: NUMt = new NumInfo("magnitude of collectionMulti{row,col} element's width or height", true); - _dimUnit?: DIMt = new DimInfo("units of collectionMulti{row,col} element's width or height - 'px' or '*' for pixels or relative units", true); - _fitWidth?: BOOLt = new BoolInfo("whether document should scale its contents to fit its rendered width or not (e.g., for PDFviews)", true); - _fitToBox?: boolean; // whether a freeformview should zoom/scale to create a shrinkwrapped view of its contents - _lockedPosition?: boolean; // lock the x,y coordinates of the document so that it can't be dragged - _lockedTransform?: boolean; // lock the panx,pany and scale parameters of the document so that it be panned/zoomed - _isPushpin?: boolean; // whether document, when clicked, toggles display of its link target - _showTitle?: string; // field name to display in header (:hover is an optional suffix) - _showCaption?: string; // which field to display in the caption area. leave empty to have no caption - _scrollTop?: number; // scroll location for pdfs - _noAutoscroll?: boolean;// whether collections autoscroll when this item is dragged - _chromeHidden?: boolean; // whether the editing chrome for a document is hidden - _searchDoc?: boolean; // is this a search document (used to change UI for search results in schema view) - _forceActive?: boolean; // flag to handle pointer events when not selected (or otherwise active) - _stayInCollection?: boolean;// whether the document should remain in its collection when someone tries to drag and drop it elsewhere - _raiseWhenDragged?: boolean; // whether a document is brought to front when dragged. - _hideContextMenu?: boolean; // whether the context menu can be shown - _viewType?: string; // sub type of a collection - _gridGap?: number; // gap between items in masonry view - _viewScale?: number; // how much a freeform view has been scaled (zoomed) - _overflow?: string; // set overflow behavior - _xMargin?: number; // gap between left edge of document and start of masonry/stacking layouts - _yMargin?: number; // gap between top edge of dcoument and start of masonry/stacking layouts - _xPadding?: number; - _yPadding?: number; - _itemIndex?: number; // which item index the carousel viewer is showing - _showSidebar?: boolean; //whether an annotationsidebar should be displayed for text docuemnts - _singleLine?: boolean; // whether text document is restricted to a single line (carriage returns make new document) - _minFontSize?: number; // minimum font size for labelBoxes - _maxFontSize?: number; // maximum font size for labelBoxes - _columnWidth?: number; - _columnsHideIfEmpty?: boolean; // whether stacking view column headings should be hidden - _fontSize?: string; - _fontFamily?: string; - _fontWeight?: string; - _pivotField?: string; // field key used to determine headings for sections in stacking, masonry, pivot views - _curPage?: number; // current page of a PDF or other? paginated document - _currentTimecode?: number; // the current timecode of a time-based document (e.g., current time of a video) value is in seconds - _currentFrame?: number; // the current frame of a frame-based collection (e.g., progressive slide) - _timecodeToShow?: number; // the time that a document should be displayed (e.g., when an annotation shows up as a video plays) - _timecodeToHide?: number; // the time that a document should be hidden - _timelineLabel?: boolean; // whether the document exists on a timeline - "_carousel-caption-xMargin"?: number; - "_carousel-caption-yMargin"?: number; - "icon-nativeWidth"?: number; - "icon-nativeHeight"?: number; - "dragFactory-count"?: number; // number of items created from a drag button (used for setting title with incrementing index) - x?: number; - y?: number; - z?: number; // whether document is in overlay (1) or not (0 or undefined) - lat?: number; - lng?: number; - infoWindowOpen?: boolean; - author?: string; - _layoutKey?: string; - unrendered?: boolean; // denotes an annotation that is not rendered with a DocumentView (e.g, rtf/pdf text selections and links to scroll locations in web/pdf) - type?: string; - title?: string; - "acl-Public"?: string; // public permissions - "_acl-Public"?: string; // public permissions - version?: string; // version identifier for a document - label?: string; - hidden?: boolean; - _hidden?: boolean; - pointerEvents?: string; // pointer events that the documentview should have - mediaState?: string; // status of media document: "pendingRecording", "recording", "paused", "playing" - autoPlayAnchors?: boolean; // whether to play audio/video when an anchor is clicked in a stackedTimeline. - dontPlayLinkOnSelect?: boolean; // whether an audio/video should start playing when a link is followed to it. - toolTip?: string; // tooltip to display on hover - dontUndo?: boolean; // whether button clicks should be undoable (this is set to true for Undo/Redo/and sidebar buttons that open the siebar panel) - description?: string; // added for links - layout?: string | Doc; // default layout string for a document - contentPointerEvents?: string; // pointer events allowed for content of a document view. eg. set to "none" in menuSidebar for sharedDocs so that you can select a document, but not interact with its contents - childLimitHeight?: number; // whether to limit the height of collection children. 0 - means height can be no bigger than width - childLayoutTemplate?: Doc; // template for collection to use to render its children (see PresBox layout in tree view) - childLayoutString?: string; // template string for collection to use to render its children - childDontRegisterViews?: boolean; - childHideLinkButton?: boolean; // hide link buttons on all children - hideLinkButton?: boolean; // whether the blue link counter button should be hidden - hideAllLinks?: boolean; // whether all individual blue anchor dots should be hidden - isTemplateForField?: string; // the field key for which the containing document is a rendering template - isTemplateDoc?: boolean; - watchedDocuments?: Doc; // list of documents an icon doc monitors in order to display a badge count - targetScriptKey?: string; // where to write a template script (used by collections with click templates which need to target onClick, onDoubleClick, etc) - templates?: List; - hero?: ImageField; // primary image that best represents a compound document (e.g., for a buxton device document that has multiple images) - caption?: RichTextField; - opacity?: number; - defaultBackgroundColor?: string; - _isLinkButton?: boolean; // marks a document as a button that will follow its primary link when clicked - _linkAutoMove?: boolean; // whether link endpoint should move around the edges of a document to make shortest path to other link endpoint - isFolder?: boolean; - lastFrame?: number; // the last frame of a frame-based collection (e.g., progressive slide) - activeFrame?: number; // the active frame of a document in a frame base collection - appearFrame?: number; // the frame in which the document appears - presTransition?: number; //the time taken for the transition TO a document - presDuration?: number; //the duration of the slide in presentation view - presProgressivize?: boolean; - borderRounding?: string; - boxShadow?: string; - data?: any; - baseProto?: boolean; // is this a base prototoype - dontRegisterView?: boolean; - lookupField?: ScriptField; // script that returns the value of a field. This script is passed the rootDoc, layoutDoc, field, and container of the document. see PresBox. - "onDoubleClick-rawScript"?: string; // onDoubleClick script in raw text form - "onChildDoubleClick-rawScript"?: string; // onChildDoubleClick script in raw text form - "onChildClick-rawScript"?: string; // on ChildClick script in raw text form - "onClick-rawScript"?: string; // onClick script in raw text form - "onCheckedClick-rawScript"?: string; // onChecked script in raw text form - "onCheckedClick-params"?: List; // parameter list for onChecked treeview functions - columnHeaders?: List; // headers for stacking views - schemaHeaders?: List; // headers for schema view - clipWidth?: number; // percent transition from before to after in comparisonBox - dockingConfig?: string; - annotationOn?: Doc; - isPushpin?: boolean; - isGroup?: boolean; // whether a collection should use a grouping UI behavior - _removeDropProperties?: List; // 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 - - // BACKGROUND GRID - _backgroundGridShow?: boolean; - - //BUTTONS - buttonText?: string; - iconShape?: string; // shapes of the fonticon border - btnType?: string; - btnList?: List; - docColorBtn?: string; - userColorBtn?: string; - canClick?: string; - script?: ScriptField; - numBtnType?: string; - numBtnMax?: number; - numBtnMin?: number; - switchToggle?: boolean; - - //LINEAR VIEW - linearViewIsExpanded?: boolean; // is linear view expanded - linearViewExpandable?: boolean; // can linear view be expanded - linearViewToggleButton?: string; // button to open close linear view group - linearViewSubMenu?: boolean; - linearViewFloating?: boolean; - flexGap?: number; // Linear view flex gap - flexDirection?: "unset" | "row" | "column" | "row-reverse" | "column-reverse"; - - layout_linkView?: Doc; // view template for a link document - layout_keyValue?: string; // view tempalte for key value docs - linkRelationship?: string; // type of relatinoship a link represents - linkDisplay?: boolean; // whether a link line should be dipslayed between the two link anchors - anchor1?: Doc; - anchor2?: Doc; - "anchor1-useLinkSmallAnchor"?: boolean; // whether anchor1 of a link should use a miniature anchor dot (as when the anchor is a text selection) - "anchor2-useLinkSmallAnchor"?: boolean; // whether anchor1 of a link should use a miniature anchor dot (as when the anchor is a text selection) - ignoreClick?: boolean; - onClick?: ScriptField; - onDoubleClick?: ScriptField; - onChildClick?: ScriptField; // script given to children of a collection to execute when they are clicked - onChildDoubleClick?: ScriptField; // script given to children of a collection to execute when they are double clicked - onPointerDown?: ScriptField; - onPointerUp?: ScriptField; - dropConverter?: ScriptField; // script to run when documents are dropped on this Document. - 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 - cloneFieldFilter?: List; // fields not to copy when the document is clonedclipboard?: Doc; - useCors?: boolean; - icon?: string; - target?: Doc; // available for use in scripts as the primary target document - sourcePanel?: Doc; // panel to display in 'targetContainer' as the result of a button onClick script - targetContainer?: Doc; // document whose proto will be set to 'panel' as the result of a onClick click script - searchFileTypes?: List; // file types allowed in a search query - strokeWidth?: number; - freezeChildren?: string; // whether children are now allowed to be added and or removed from a collection - treeViewHideTitle?: boolean; // whether to hide the top document title of a tree view - treeViewHideHeaderIfTemplate?: boolean; // whether to hide the header for a document in a tree view only if a childLayoutTemplate is provided (presBox) - treeViewHideHeader?: boolean; // whether to hide the header for a document in a tree view - treeViewHideHeaderFields?: boolean; // whether to hide the drop down options for tree view items. - treeViewGrowsHorizontally?: boolean; // whether an embedded tree view of the document can grow horizontally without growing vertically - - // Action Button - buttonMenu?: boolean; // whether a action button should be displayed - buttonMenuDoc?: Doc; - explainer?: string; - - treeViewOpenIsTransient?: boolean; // ignores the treeViewOpen Doc flag, allowing a treeViewItem's expand/collapse state to be independent of other views of the same document in the same or any other tree view - _treeViewOpen?: boolean; // whether this document is expanded in a tree view (note: need _ and regular versions since this can be specified for both proto and layout docs) - treeViewOpen?: boolean; // whether this document is expanded in a tree view - treeViewExpandedView?: string; // which field/thing is displayed when this item is opened in tree view - treeViewExpandedViewLock?: boolean; // whether the expanded view can be changed - treeViewChecked?: ScriptField; // script to call when a tree view checkbox is checked - treeViewTruncateTitleWidth?: number; - treeViewHasOverlay?: boolean; // whether the treeview has an overlay for freeform annotations - treeViewType?: string; // whether treeview is a Slide, file system, or (default) collection hierarchy - sidebarColor?: string; // background color of text sidebar - sidebarViewType?: string; // collection type of text sidebar - docMaxAutoHeight?: number; // maximum height for newly created (eg, from pasting) text documents - text?: string; - textTransform?: string; // is linear view expanded - letterSpacing?: string; // is linear view expanded - selectedIndex?: number; // which item in a linear view has been selected using the "thumb doc" ui - clipboard?: Doc; - searchQuery?: string; // for quersyBox - useLinkSmallAnchor?: boolean; // whether links to this document should use a miniature linkAnchorBox - border?: string; //for searchbox - hoverBackgroundColor?: string; // background color of a label when hovered - linkRelationshipList?: List; // for storing different link relationships (when set by user in the link editor) - linkRelationshipSizes?: List; //stores number of links contained in each relationship - linkColorList?: List; // colors of links corresponding to specific link relationships + system?: BOOLt = new BoolInfo("is this a system created/owned doc"); + _dropAction?: DROPt = new DAInfo("what should happen to this document when it's dropped somewhere else"); + allowOverlayDrop?: BOOLt = new BoolInfo("can documents be dropped onto this document without using dragging title bar or holding down embed key (ctrl)?"); + 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 ??? "); + color?: string; // foreground color data doc + backgroundColor?: STRt = new StrInfo("background color for data doc"); + _backgroundColor?: STRt = new StrInfo("background color for each template layout doc (overrides backgroundColor)", true); + _autoHeight?: BOOLt = new BoolInfo("whether document automatically resizes vertically to display contents", true); + _headerHeight?: NUMt = new NumInfo("height of document header used for displaying title", true); + _headerFontSize?: NUMt = new NumInfo("font size of header of custom notes", true); + _headerPointerEvents?: PEVt = new PEInfo("types of events the header of a custom text document can consume", true); + _panX?: NUMt = new NumInfo("horizontal pan location of a freeform view", true); + _panY?: NUMt = new NumInfo("vertical pan location of a freeform view", true); + _width?: NUMt = new NumInfo("displayed width of a document", true); + _height?: NUMt = new NumInfo("displayed height of document", true); + _nativeWidth?: NUMt = new NumInfo("native width of document contents (e.g., the pixel width of an image)", true); + _nativeHeight?: NUMt = new NumInfo("native height of document contents (e.g., the pixel height of an image)", true); + _dimMagnitude?: NUMt = new NumInfo("magnitude of collectionMulti{row,col} element's width or height", true); + _dimUnit?: DIMt = new DimInfo("units of collectionMulti{row,col} element's width or height - 'px' or '*' for pixels or relative units", true); + _fitWidth?: BOOLt = new BoolInfo("whether document should scale its contents to fit its rendered width or not (e.g., for PDFviews)", true); + _fitToBox?: boolean; // whether a freeformview should zoom/scale to create a shrinkwrapped view of its contents + _lockedPosition?: boolean; // lock the x,y coordinates of the document so that it can't be dragged + _lockedTransform?: boolean; // lock the panx,pany and scale parameters of the document so that it be panned/zoomed + _isPushpin?: boolean; // whether document, when clicked, toggles display of its link target + _showTitle?: string; // field name to display in header (:hover is an optional suffix) + _showCaption?: string; // which field to display in the caption area. leave empty to have no caption + _scrollTop?: number; // scroll location for pdfs + _noAutoscroll?: boolean;// whether collections autoscroll when this item is dragged + _chromeHidden?: boolean; // whether the editing chrome for a document is hidden + _searchDoc?: boolean; // is this a search document (used to change UI for search results in schema view) + _forceActive?: boolean; // flag to handle pointer events when not selected (or otherwise active) + _stayInCollection?: boolean;// whether the document should remain in its collection when someone tries to drag and drop it elsewhere + _raiseWhenDragged?: boolean; // whether a document is brought to front when dragged. + _hideContextMenu?: boolean; // whether the context menu can be shown + _viewType?: string; // sub type of a collection + _gridGap?: number; // gap between items in masonry view + _viewScale?: number; // how much a freeform view has been scaled (zoomed) + _overflow?: string; // set overflow behavior + _xMargin?: number; // gap between left edge of document and start of masonry/stacking layouts + _yMargin?: number; // gap between top edge of dcoument and start of masonry/stacking layouts + _xPadding?: number; + _yPadding?: number; + _itemIndex?: number; // which item index the carousel viewer is showing + _showSidebar?: boolean; //whether an annotationsidebar should be displayed for text docuemnts + _singleLine?: boolean; // whether text document is restricted to a single line (carriage returns make new document) + _minFontSize?: number; // minimum font size for labelBoxes + _maxFontSize?: number; // maximum font size for labelBoxes + _columnWidth?: number; + _columnsHideIfEmpty?: boolean; // whether stacking view column headings should be hidden + _fontSize?: string; + _fontFamily?: string; + _fontWeight?: string; + _pivotField?: string; // field key used to determine headings for sections in stacking, masonry, pivot views + _curPage?: number; // current page of a PDF or other? paginated document + _currentTimecode?: number; // the current timecode of a time-based document (e.g., current time of a video) value is in seconds + _currentFrame?: number; // the current frame of a frame-based collection (e.g., progressive slide) + _timecodeToShow?: number; // the time that a document should be displayed (e.g., when an annotation shows up as a video plays) + _timecodeToHide?: number; // the time that a document should be hidden + _timelineLabel?: boolean; // whether the document exists on a timeline + "_carousel-caption-xMargin"?: number; + "_carousel-caption-yMargin"?: number; + "icon-nativeWidth"?: number; + "icon-nativeHeight"?: number; + "dragFactory-count"?: number; // number of items created from a drag button (used for setting title with incrementing index) + x?: number; + y?: number; + z?: number; // whether document is in overlay (1) or not (0 or undefined) + lat?: number; + lng?: number; + infoWindowOpen?: boolean; + author?: string; + _layoutKey?: string; + unrendered?: boolean; // denotes an annotation that is not rendered with a DocumentView (e.g, rtf/pdf text selections and links to scroll locations in web/pdf) + type?: string; + title?: string; + "acl-Public"?: string; // public permissions + "_acl-Public"?: string; // public permissions + version?: string; // version identifier for a document + label?: string; + hidden?: boolean; + _hidden?: boolean; + pointerEvents?: string; // pointer events that the documentview should have + mediaState?: string; // status of media document: "pendingRecording", "recording", "paused", "playing" + autoPlayAnchors?: boolean; // whether to play audio/video when an anchor is clicked in a stackedTimeline. + dontPlayLinkOnSelect?: boolean; // whether an audio/video should start playing when a link is followed to it. + toolTip?: string; // tooltip to display on hover + dontUndo?: boolean; // whether button clicks should be undoable (this is set to true for Undo/Redo/and sidebar buttons that open the siebar panel) + description?: string; // added for links + layout?: string | Doc; // default layout string for a document + contentPointerEvents?: string; // pointer events allowed for content of a document view. eg. set to "none" in menuSidebar for sharedDocs so that you can select a document, but not interact with its contents + childLimitHeight?: number; // whether to limit the height of collection children. 0 - means height can be no bigger than width + childLayoutTemplate?: Doc; // template for collection to use to render its children (see PresBox layout in tree view) + childLayoutString?: string; // template string for collection to use to render its children + childDontRegisterViews?: boolean; + childHideLinkButton?: boolean; // hide link buttons on all children + hideLinkButton?: boolean; // whether the blue link counter button should be hidden + hideDecorationTitle?: boolean; + hideResizeHandles?: boolean; + hideDocumentButtonBar?: boolean; + hideAllLinks?: boolean; // whether all individual blue anchor dots should be hidden + isTemplateForField?: string; // the field key for which the containing document is a rendering template + isTemplateDoc?: boolean; + watchedDocuments?: Doc; // list of documents an icon doc monitors in order to display a badge count + targetScriptKey?: string; // where to write a template script (used by collections with click templates which need to target onClick, onDoubleClick, etc) + templates?: List; + hero?: ImageField; // primary image that best represents a compound document (e.g., for a buxton device document that has multiple images) + caption?: RichTextField; + opacity?: number; + defaultBackgroundColor?: string; + _isLinkButton?: boolean; // marks a document as a button that will follow its primary link when clicked + _linkAutoMove?: boolean; // whether link endpoint should move around the edges of a document to make shortest path to other link endpoint + isFolder?: boolean; + lastFrame?: number; // the last frame of a frame-based collection (e.g., progressive slide) + activeFrame?: number; // the active frame of a document in a frame base collection + appearFrame?: number; // the frame in which the document appears + presTransition?: number; //the time taken for the transition TO a document + presDuration?: number; //the duration of the slide in presentation view + presProgressivize?: boolean; + borderRounding?: string; + boxShadow?: string; + data?: any; + baseProto?: boolean; // is this a base prototoype + dontRegisterView?: boolean; + lookupField?: ScriptField; // script that returns the value of a field. This script is passed the rootDoc, layoutDoc, field, and container of the document. see PresBox. + "onDoubleClick-rawScript"?: string; // onDoubleClick script in raw text form + "onChildDoubleClick-rawScript"?: string; // onChildDoubleClick script in raw text form + "onChildClick-rawScript"?: string; // on ChildClick script in raw text form + "onClick-rawScript"?: string; // onClick script in raw text form + "onCheckedClick-rawScript"?: string; // onChecked script in raw text form + "onCheckedClick-params"?: List; // parameter list for onChecked treeview functions + columnHeaders?: List; // headers for stacking views + schemaHeaders?: List; // headers for schema view + clipWidth?: number; // percent transition from before to after in comparisonBox + dockingConfig?: string; + annotationOn?: Doc; + isPushpin?: boolean; + isGroup?: boolean; // whether a collection should use a grouping UI behavior + _removeDropProperties?: List; // 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 + + // BACKGROUND GRID + _backgroundGridShow?: boolean; + + //BUTTONS + buttonText?: string; + iconShape?: string; // shapes of the fonticon border + btnType?: string; + btnList?: List; + docColorBtn?: string; + userColorBtn?: string; + canClick?: string; + script?: ScriptField; + numBtnType?: string; + numBtnMax?: number; + numBtnMin?: number; + switchToggle?: boolean; + + //LINEAR VIEW + linearViewIsExpanded?: boolean; // is linear view expanded + linearViewExpandable?: boolean; // can linear view be expanded + linearViewToggleButton?: string; // button to open close linear view group + linearViewSubMenu?: boolean; + linearViewFloating?: boolean; + flexGap?: number; // Linear view flex gap + flexDirection?: "unset" | "row" | "column" | "row-reverse" | "column-reverse"; + + layout_linkView?: Doc; // view template for a link document + layout_keyValue?: string; // view tempalte for key value docs + linkRelationship?: string; // type of relatinoship a link represents + linkDisplay?: boolean; // whether a link line should be dipslayed between the two link anchors + anchor1?: Doc; + anchor2?: Doc; + "anchor1-useLinkSmallAnchor"?: boolean; // whether anchor1 of a link should use a miniature anchor dot (as when the anchor is a text selection) + "anchor2-useLinkSmallAnchor"?: boolean; // whether anchor1 of a link should use a miniature anchor dot (as when the anchor is a text selection) + ignoreClick?: boolean; + onClick?: ScriptField; + onDoubleClick?: ScriptField; + onChildClick?: ScriptField; // script given to children of a collection to execute when they are clicked + onChildDoubleClick?: ScriptField; // script given to children of a collection to execute when they are double clicked + onPointerDown?: ScriptField; + onPointerUp?: ScriptField; + dropConverter?: ScriptField; // script to run when documents are dropped on this Document. + 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 + cloneFieldFilter?: List; // fields not to copy when the document is clonedclipboard?: Doc; + useCors?: boolean; + icon?: string; + target?: Doc; // available for use in scripts as the primary target document + sourcePanel?: Doc; // panel to display in 'targetContainer' as the result of a button onClick script + targetContainer?: Doc; // document whose proto will be set to 'panel' as the result of a onClick click script + searchFileTypes?: List; // file types allowed in a search query + strokeWidth?: number; + freezeChildren?: string; // whether children are now allowed to be added and or removed from a collection + treeViewHideTitle?: boolean; // whether to hide the top document title of a tree view + treeViewHideHeaderIfTemplate?: boolean; // whether to hide the header for a document in a tree view only if a childLayoutTemplate is provided (presBox) + treeViewHideHeader?: boolean; // whether to hide the header for a document in a tree view + treeViewHideHeaderFields?: boolean; // whether to hide the drop down options for tree view items. + treeViewGrowsHorizontally?: boolean; // whether an embedded tree view of the document can grow horizontally without growing vertically + + // Action Button + buttonMenu?: boolean; // whether a action button should be displayed + buttonMenuDoc?: Doc; + explainer?: string; + + treeViewOpenIsTransient?: boolean; // ignores the treeViewOpen Doc flag, allowing a treeViewItem's expand/collapse state to be independent of other views of the same document in the same or any other tree view + _treeViewOpen?: boolean; // whether this document is expanded in a tree view (note: need _ and regular versions since this can be specified for both proto and layout docs) + treeViewOpen?: boolean; // whether this document is expanded in a tree view + treeViewExpandedView?: string; // which field/thing is displayed when this item is opened in tree view + treeViewExpandedViewLock?: boolean; // whether the expanded view can be changed + treeViewChecked?: ScriptField; // script to call when a tree view checkbox is checked + treeViewTruncateTitleWidth?: number; + treeViewHasOverlay?: boolean; // whether the treeview has an overlay for freeform annotations + treeViewType?: string; // whether treeview is a Slide, file system, or (default) collection hierarchy + sidebarColor?: string; // background color of text sidebar + sidebarViewType?: string; // collection type of text sidebar + docMaxAutoHeight?: number; // maximum height for newly created (eg, from pasting) text documents + text?: string; + textTransform?: string; // is linear view expanded + letterSpacing?: string; // is linear view expanded + selectedIndex?: number; // which item in a linear view has been selected using the "thumb doc" ui + clipboard?: Doc; + searchQuery?: string; // for quersyBox + useLinkSmallAnchor?: boolean; // whether links to this document should use a miniature linkAnchorBox + border?: string; //for searchbox + hoverBackgroundColor?: string; // background color of a label when hovered + linkRelationshipList?: List; // for storing different link relationships (when set by user in the link editor) + linkRelationshipSizes?: List; //stores number of links contained in each relationship + linkColorList?: List; // colors of links corresponding to specific link relationships } export namespace Docs { - const _docOptions = new DocumentOptions(); - - export async function setupFieldInfos() { - return await DocServer.GetRefField("FieldInfos8") as Doc ?? - runInAction(() => { - const infos = new Doc("FieldInfos8", true); - const keys = Object.keys(new DocumentOptions()); - for (const key of keys) { - const options = (_docOptions as any)[key] as FInfo; - const finfo = new Doc(); - finfo.name = key; - switch (options.type) { - case "boolean": finfo.options = new List(options.values as any as boolean[]); break; - case "number": finfo.options = new List(options.values as any as number[]); break; - case "Doc": finfo.options = new List(options.values as any as Doc[]); break; - default: // string, pointerEvents, dimUnit, dropActionType - finfo.options = new List(options.values as any as string[]); break; - } - finfo.layoutField = options.layoutField; - finfo.description = options.description; - finfo.type = options.type; - infos[key] = finfo; - } - return infos; - }); - } - - export let newAccount: boolean = false; - - export namespace Prototypes { - - type LayoutSource = { LayoutString: (key: string) => string }; - type PrototypeTemplate = { - layout: { - view: LayoutSource, - dataField: string - }, - data?: any, - options?: Partial - }; - type TemplateMap = Map; - type PrototypeMap = Map; - const defaultDataKey = "data"; - - const TemplateMap: TemplateMap = new Map([ - [DocumentType.RTF, { - layout: { view: FormattedTextBox, dataField: "text" }, - options: { - _height: 35, _xMargin: 10, _yMargin: 10, nativeDimModifiable: true, nativeHeightUnfrozen: true, treeViewGrowsHorizontally: true, - links: "@links(self)" - } - }], - [DocumentType.SEARCH, { - layout: { view: SearchBox, dataField: defaultDataKey }, - options: { _width: 400, links: "@links(self)" } - }], - [DocumentType.FILTER, { - layout: { view: FilterBox, dataField: defaultDataKey }, - options: { _width: 400, links: "@links(self)" } - }], - [DocumentType.COLOR, { - layout: { view: ColorBox, dataField: defaultDataKey }, - options: { _nativeWidth: 220, _nativeHeight: 300, links: "@links(self)" } - }], - [DocumentType.IMG, { - layout: { view: ImageBox, dataField: defaultDataKey }, - options: { links: "@links(self)" } - }], - [DocumentType.WEB, { - layout: { view: WebBox, dataField: defaultDataKey }, - options: { _height: 300, _fitWidth: true, nativeDimModifiable: true, nativeHeightUnfrozen: true, links: "@links(self)" } - }], - [DocumentType.COL, { - layout: { view: CollectionView, dataField: defaultDataKey }, - options: { _fitWidth: true, _panX: 0, _panY: 0, _viewScale: 1, links: "@links(self)" } - }], - [DocumentType.KVP, { - layout: { view: KeyValueBox, dataField: defaultDataKey }, - options: { _fitWidth: true, _height: 150 } - }], - [DocumentType.VID, { - layout: { view: VideoBox, dataField: defaultDataKey }, - options: { _currentTimecode: 0, links: "@links(self)" }, - }], - [DocumentType.AUDIO, { - layout: { view: AudioBox, dataField: defaultDataKey }, - options: { _height: 100, backgroundColor: "lightGray", links: "@links(self)" } - }], - [DocumentType.REC, { - layout: { view: VideoBox, dataField: defaultDataKey }, - options: { _height: 100, backgroundColor: "pink", links: "@links(self)" } - }], - [DocumentType.PDF, { - layout: { view: PDFBox, dataField: defaultDataKey }, - options: { _curPage: 1, _fitWidth: true, nativeDimModifiable: true, nativeHeightUnfrozen: true, links: "@links(self)" } - }], - [DocumentType.MAP, { - layout: { view: MapBox, dataField: defaultDataKey }, - options: { _height: 600, _width: 800, links: "@links(self)" } - }], - [DocumentType.IMPORT, { - layout: { view: DirectoryImportBox, dataField: defaultDataKey }, - options: { _height: 150 } - }], - [DocumentType.LINK, { - layout: { view: LinkBox, dataField: defaultDataKey }, - options: { - childDontRegisterViews: true, _isLinkButton: true, _height: 150, description: "", showCaption: "description", - backgroundColor: "lightblue", // lightblue is default color for linking dot and link documents text comment area - links: "@links(self)", - _removeDropProperties: new List(["isLinkButton"]), - } - }], - [DocumentType.LINKDB, { - data: new List(), - layout: { view: EmptyBox, dataField: defaultDataKey }, - options: { childDropAction: "alias", title: "Global Link Database" } - }], - [DocumentType.SCRIPTDB, { - data: new List(), - layout: { view: EmptyBox, dataField: defaultDataKey }, - options: { childDropAction: "alias", title: "Global Script Database" } - }], - [DocumentType.SCRIPTING, { - layout: { view: ScriptingBox, dataField: defaultDataKey }, - options: { links: "@links(self)" } - }], - [DocumentType.YOUTUBE, { - layout: { view: YoutubeBox, dataField: defaultDataKey } - }], - [DocumentType.LABEL, { - layout: { view: LabelBox, dataField: defaultDataKey }, - options: { links: "@links(self)", _singleLine: true } - }], - [DocumentType.EQUATION, { - layout: { view: EquationBox, dataField: defaultDataKey }, - options: { links: "@links(self)", hideResizeHandles: true, hideDecorationTitle: true } - }], - [DocumentType.FUNCPLOT, { - layout: { view: FunctionPlotBox, dataField: defaultDataKey }, - options: { links: "@links(self)" } - }], - [DocumentType.BUTTON, { - layout: { view: LabelBox, dataField: "onClick" }, - options: { links: "@links(self)" } - }], - [DocumentType.SLIDER, { - layout: { view: SliderBox, dataField: defaultDataKey, }, - options: { links: "@links(self)", treeViewGrowsHorizontally: true } - }], - [DocumentType.PRES, { - layout: { view: PresBox, dataField: defaultDataKey }, - options: { links: "@links(self)" } - }], - [DocumentType.FONTICON, { - layout: { view: FontIconBox, dataField: defaultDataKey }, - options: { hideLinkButton: true, _width: 40, _height: 40, borderRounding: "100%", links: "@links(self)" }, - }], - [DocumentType.WEBCAM, { - layout: { view: RecordingBox, dataField: defaultDataKey }, - options: { links: "@links(self)" } - }], - [DocumentType.PRESELEMENT, { - layout: { view: PresElementBox, dataField: defaultDataKey } - }], - [DocumentType.MARKER, { - layout: { view: CollectionView, dataField: defaultDataKey }, - options: { links: "@links(self)", hideLinkButton: true, pointerEvents: "none" } - }], - [DocumentType.INK, { // NOTE: this is unused!! ink fields are filled in directly within the InkDocument() method - layout: { view: InkingStroke, dataField: defaultDataKey }, - options: { links: "@links(self)" } - }], - [DocumentType.SCREENSHOT, { - layout: { view: ScreenshotBox, dataField: defaultDataKey }, - options: { links: "@links(self)" } - }], - [DocumentType.COMPARISON, { - layout: { view: ComparisonBox, dataField: defaultDataKey }, - options: { clipWidth: 50, backgroundColor: "gray", targetDropAction: "alias", links: "@links(self)" } - }], - [DocumentType.GROUPDB, { - data: new List(), - layout: { view: EmptyBox, dataField: defaultDataKey }, - options: { childDropAction: "alias", title: "Global Group Database" } - }], - [DocumentType.GROUP, { - layout: { view: EmptyBox, dataField: defaultDataKey }, - options: { links: "@links(self)" } - }], - ]); - - const suffix = "Proto"; - - /** - * This function loads or initializes the prototype for each docment type. - * - * This is an asynchronous function because it has to attempt - * to fetch the prototype documents from the server. - * - * Once we have this object that maps the prototype ids to a potentially - * undefined document, we either initialize our private prototype - * variables with the document returned from the server or, if prototypes - * haven't been initialized, the newly initialized prototype document. - */ - export async function initialize(): Promise { - ProxyField.initPlugin(); - ComputedField.initPlugin(); - // non-guid string ids for each document prototype - const prototypeIds = Object.values(DocumentType).filter(type => type !== DocumentType.NONE).map(type => type + suffix); - // fetch the actual prototype documents from the server - const actualProtos = Docs.newAccount ? {} : await DocServer.GetRefFields(prototypeIds); - if (!Docs.newAccount) { - Cast(actualProtos[DocumentType.WEB + suffix], Doc, null).nativeHeightUnfrozen = true; // to avoid having to recreate the DB - Cast(actualProtos[DocumentType.PDF + suffix], Doc, null).nativeHeightUnfrozen = true; - Cast(actualProtos[DocumentType.RTF + suffix], Doc, null).nativeHeightUnfrozen = true; - Cast(actualProtos[DocumentType.WEB + suffix], Doc, null).nativeDimModifiable = true; // to avoid having to recreate the DB - Cast(actualProtos[DocumentType.PDF + suffix], Doc, null).nativeDimModifiable = true; - Cast(actualProtos[DocumentType.RTF + suffix], Doc, null).nativeDimModifiable = true; - } - - // update this object to include any default values: DocumentOptions for all prototypes - prototypeIds.map(id => { - const existing = actualProtos[id] as Doc; - const type = id.replace(suffix, "") as DocumentType; - // get or create prototype of the specified type... - const target = existing || buildPrototype(type, id); - // ...and set it if not undefined (can be undefined only if TemplateMap does not contain - // an entry dedicated to the given DocumentType) - target && PrototypeMap.set(type, target); - }); - } - - /** - * Retrieves the prototype for the given document type, or - * undefined if that type's proto doesn't have a configuration - * in the template map. - * @param type - */ - const PrototypeMap: PrototypeMap = new Map(); - export function get(type: DocumentType): Doc { return PrototypeMap.get(type)!; } - - /** - * A collection of all links in the database. Ideally, this would be a search, but for now all links are cached here. - */ - export function MainLinkDocument() { return Prototypes.get(DocumentType.LINKDB); } - - /** - * A collection of all scripts in the database - */ - export function MainScriptDocument() { return Prototypes.get(DocumentType.SCRIPTDB); } - - /** - * A collection of all user acl groups in the database - */ - export function MainGroupDocument() { return Prototypes.get(DocumentType.GROUPDB); } - - /** - * This is a convenience method that is used to initialize - * prototype documents for the first time. - * - * @param protoId the id of the prototype, indicating the specific prototype - * to initialize (see the *protoId list at the top of the namespace) - * @param title the prototype document's title, follows *-PROTO - * @param layout the layout key for this prototype and thus the - * layout key that all delegates will inherit - * @param options any value specified in the DocumentOptions object likewise - * becomes the default value for that key for all delegates - */ - function buildPrototype(type: DocumentType, prototypeId: string): Opt { - // load template from type - const template = TemplateMap.get(type); - if (!template) { - return undefined; - } - const layout = template.layout; - // create title - const upper = suffix.toUpperCase(); - const title = prototypeId.toUpperCase().replace(upper, `_${upper}`); - // synthesize the default options, the type and title from computed values and - // whatever options pertain to this specific prototype - const options: DocumentOptions = { - system: true, _layoutKey: "layout", title, type, baseProto: true, x: 0, y: 0, _width: 300, ...(template.options || {}), - layout: layout.view?.LayoutString(layout.dataField), data: template.data, layout_keyValue: KeyValueBox.LayoutString("") + const _docOptions = new DocumentOptions(); + + export async function setupFieldInfos() { + return await DocServer.GetRefField("FieldInfos8") as Doc ?? + runInAction(() => { + const infos = new Doc("FieldInfos8", true); + const keys = Object.keys(new DocumentOptions()); + for (const key of keys) { + const options = (_docOptions as any)[key] as FInfo; + const finfo = new Doc(); + finfo.name = key; + switch (options.type) { + case "boolean": finfo.options = new List(options.values as any as boolean[]); break; + case "number": finfo.options = new List(options.values as any as number[]); break; + case "Doc": finfo.options = new List(options.values as any as Doc[]); break; + default: // string, pointerEvents, dimUnit, dropActionType + finfo.options = new List(options.values as any as string[]); break; + } + finfo.layoutField = options.layoutField; + finfo.description = options.description; + finfo.type = options.type; + infos[key] = finfo; + } + return infos; + }); + } + + export let newAccount: boolean = false; + + export namespace Prototypes { + + type LayoutSource = { LayoutString: (key: string) => string }; + type PrototypeTemplate = { + layout: { + view: LayoutSource, + dataField: string + }, + data?: any, + options?: Partial }; - Object.entries(options).map(pair => { - if (typeof pair[1] === "string" && pair[1].startsWith("@")) { - (options as any)[pair[0]] = ComputedField.MakeFunction(pair[1].substring(1)); - } - }); - return Doc.assign(new Doc(prototypeId, true), options as any, undefined, true); - } - } - - /** - * Encapsulates the factory used to create new document instances - * delegated from top-level prototypes - */ - export namespace Create { - - /** - * This function receives the relevant document prototype and uses - * it to create a new of that base-level prototype, or the - * underlying data document, which it then delegates again - * to create the view document. - * - * It also takes the opportunity to register the user - * that created the document and the time of creation. - * - * @param proto the specific document prototype off of which to model - * this new instance (textProto, imageProto, etc.) - * @param data the Field to store at this new instance's data key - * @param options any initial values to provide for this new instance - * @param delegId if applicable, an existing document id. If undefined, Doc's - * constructor just generates a new GUID. This is currently used - * only when creating a DockDocument from the current user's already existing - * main document. - */ - function InstanceFromProto(proto: Doc, data: Field | undefined, options: DocumentOptions, delegId?: string, fieldKey: string = "data", protoId?: string) { - const viewKeys = ["x", "y", "system"]; // keys that should be addded to the view document even though they don't begin with an "_" - const { omit: dataProps, extract: viewProps } = OmitKeys(options, viewKeys, "^_"); - - dataProps["acl-Override"] = "None"; - dataProps["acl-Public"] = options["acl-Public"] ? options["acl-Public"] : Doc.UserDoc()?.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Augment; - - dataProps.system = viewProps.system; - dataProps.isPrototype = true; - dataProps.author = Doc.CurrentUserEmail; - dataProps.creationDate = new DateField; - dataProps[`${fieldKey}-lastModified`] = new DateField; - - dataProps[fieldKey] = data; - - // so that the list of annotations is already initialised, prevents issues in addonly. - // without this, if a doc has no annotations but the user has AddOnly privileges, they won't be able to add an annotation because they would have needed to create the field's list which they don't have permissions to do. - dataProps[fieldKey + "-annotations"] = new List(); - const dataDoc = Doc.assign(Doc.MakeDelegate(proto, protoId), dataProps, undefined, true); - - const viewFirstProps: { [id: string]: any } = {}; - viewFirstProps["acl-Public"] = options["_acl-Public"] ? options["_acl-Public"] : Doc.UserDoc()?.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Augment; - viewFirstProps["acl-Override"] = "None"; - viewFirstProps.author = Doc.CurrentUserEmail; - const viewDoc = Doc.assign(Doc.MakeDelegate(dataDoc, delegId), viewFirstProps, true, true); - Doc.assign(viewDoc, viewProps, true, true); - ![DocumentType.LINK, DocumentType.MARKER, DocumentType.LABEL].includes(viewDoc.type as any) && DocUtils.MakeLinkToActiveAudio(() => viewDoc); - - !Doc.IsSystem(dataDoc) && ![DocumentType.MARKER, DocumentType.KVP, DocumentType.LINK, DocumentType.LINKANCHOR].includes(proto.type as any) && - !dataDoc.isFolder && !dataProps.annotationOn && Doc.AddDocToList(Cast(Doc.UserDoc().myFileOrphans, Doc, null), "data", dataDoc); - - updateCachedAcls(dataDoc); - updateCachedAcls(viewDoc); - return viewDoc; - } - - export function ImageDocument(url: string, options: DocumentOptions = {}) { - const imgField = new ImageField(url); - return InstanceFromProto(Prototypes.get(DocumentType.IMG), imgField, { title: basename(url), ...options }); - } - - export function PresDocument(options: DocumentOptions = {}) { - return InstanceFromProto(Prototypes.get(DocumentType.PRES), new List(), options); - } - - export function ScriptingDocument(script: Opt, options: DocumentOptions = {}, fieldKey?: string) { - return InstanceFromProto(Prototypes.get(DocumentType.SCRIPTING), script, - { ...options, layout: fieldKey ? ScriptingBox.LayoutString(fieldKey) : undefined }); - } - - export function VideoDocument(url: string, options: DocumentOptions = {}) { - return InstanceFromProto(Prototypes.get(DocumentType.VID), new VideoField(url), options); - } - - export function YoutubeDocument(url: string, options: DocumentOptions = {}) { - return InstanceFromProto(Prototypes.get(DocumentType.YOUTUBE), new YoutubeField(url), options); - } - - export function WebCamDocument(url: string, options: DocumentOptions = {}) { - return InstanceFromProto(Prototypes.get(DocumentType.WEBCAM), "", options); - } - - export function ScreenshotDocument(options: DocumentOptions = {}) { - return InstanceFromProto(Prototypes.get(DocumentType.SCREENSHOT), "", options); - } - - export function ComparisonDocument(options: DocumentOptions = { title: "Comparison Box" }) { - return InstanceFromProto(Prototypes.get(DocumentType.COMPARISON), "", options); - } - - export function AudioDocument(url: string, options: DocumentOptions = {}) { - return InstanceFromProto(Prototypes.get(DocumentType.AUDIO), new AudioField(url), - { ...options, backgroundColor: ComputedField.MakeFunction("this._mediaState === 'playing' ? 'green':'gray'") as any }); - } - - export function RecordingDocument(url: string, options: DocumentOptions = {}) { - return InstanceFromProto(Prototypes.get(DocumentType.REC), "", options); - } - - export function SearchDocument(options: DocumentOptions = {}) { - return InstanceFromProto(Prototypes.get(DocumentType.SEARCH), new List([]), options); - } - - export function ColorDocument(options: DocumentOptions = {}) { - return InstanceFromProto(Prototypes.get(DocumentType.COLOR), "", options); - } - - export function RTFDocument(field: RichTextField, options: DocumentOptions = {}, fieldKey: string = "text") { - return InstanceFromProto(Prototypes.get(DocumentType.RTF), field, options, undefined, fieldKey); - } - export function TextDocument(text: string, options: DocumentOptions = {}, fieldKey: string = "text") { - const rtf = { - doc: { - type: "doc", content: [{ - type: "paragraph", - content: [{ - type: "text", - text - }] - }] - }, - selection: { type: "text", anchor: 1, head: 1 }, - storedMarks: [] + type TemplateMap = Map; + type PrototypeMap = Map; + const defaultDataKey = "data"; + + const TemplateMap: TemplateMap = new Map([ + [DocumentType.RTF, { + layout: { view: FormattedTextBox, dataField: "text" }, + options: { + _height: 35, _xMargin: 10, _yMargin: 10, nativeDimModifiable: true, nativeHeightUnfrozen: true, treeViewGrowsHorizontally: true, + links: "@links(self)" + } + }], + [DocumentType.SEARCH, { + layout: { view: SearchBox, dataField: defaultDataKey }, + options: { _width: 400, links: "@links(self)" } + }], + [DocumentType.FILTER, { + layout: { view: FilterBox, dataField: defaultDataKey }, + options: { _width: 400, links: "@links(self)" } + }], + [DocumentType.COLOR, { + layout: { view: ColorBox, dataField: defaultDataKey }, + options: { _nativeWidth: 220, _nativeHeight: 300, links: "@links(self)" } + }], + [DocumentType.IMG, { + layout: { view: ImageBox, dataField: defaultDataKey }, + options: { links: "@links(self)" } + }], + [DocumentType.WEB, { + layout: { view: WebBox, dataField: defaultDataKey }, + options: { _height: 300, _fitWidth: true, nativeDimModifiable: true, nativeHeightUnfrozen: true, links: "@links(self)" } + }], + [DocumentType.COL, { + layout: { view: CollectionView, dataField: defaultDataKey }, + options: { _fitWidth: true, _panX: 0, _panY: 0, _viewScale: 1, links: "@links(self)" } + }], + [DocumentType.KVP, { + layout: { view: KeyValueBox, dataField: defaultDataKey }, + options: { _fitWidth: true, _height: 150 } + }], + [DocumentType.VID, { + layout: { view: VideoBox, dataField: defaultDataKey }, + options: { _currentTimecode: 0, links: "@links(self)" }, + }], + [DocumentType.AUDIO, { + layout: { view: AudioBox, dataField: defaultDataKey }, + options: { _height: 100, backgroundColor: "lightGray", links: "@links(self)" } + }], + [DocumentType.REC, { + layout: { view: VideoBox, dataField: defaultDataKey }, + options: { _height: 100, backgroundColor: "pink", links: "@links(self)" } + }], + [DocumentType.PDF, { + layout: { view: PDFBox, dataField: defaultDataKey }, + options: { _curPage: 1, _fitWidth: true, nativeDimModifiable: true, nativeHeightUnfrozen: true, links: "@links(self)" } + }], + [DocumentType.MAP, { + layout: { view: MapBox, dataField: defaultDataKey }, + options: { _height: 600, _width: 800, links: "@links(self)" } + }], + [DocumentType.IMPORT, { + layout: { view: DirectoryImportBox, dataField: defaultDataKey }, + options: { _height: 150 } + }], + [DocumentType.LINK, { + layout: { view: LinkBox, dataField: defaultDataKey }, + options: { + childDontRegisterViews: true, _isLinkButton: true, _height: 150, description: "", showCaption: "description", + backgroundColor: "lightblue", // lightblue is default color for linking dot and link documents text comment area + links: "@links(self)", + _removeDropProperties: new List(["isLinkButton"]), + } + }], + [DocumentType.LINKDB, { + data: new List(), + layout: { view: EmptyBox, dataField: defaultDataKey }, + options: { childDropAction: "alias", title: "Global Link Database" } + }], + [DocumentType.SCRIPTDB, { + data: new List(), + layout: { view: EmptyBox, dataField: defaultDataKey }, + options: { childDropAction: "alias", title: "Global Script Database" } + }], + [DocumentType.SCRIPTING, { + layout: { view: ScriptingBox, dataField: defaultDataKey }, + options: { links: "@links(self)" } + }], + [DocumentType.YOUTUBE, { + layout: { view: YoutubeBox, dataField: defaultDataKey } + }], + [DocumentType.LABEL, { + layout: { view: LabelBox, dataField: defaultDataKey }, + options: { links: "@links(self)", _singleLine: true } + }], + [DocumentType.EQUATION, { + layout: { view: EquationBox, dataField: defaultDataKey }, + options: { links: "@links(self)", hideResizeHandles: true, hideDecorationTitle: true } + }], + [DocumentType.FUNCPLOT, { + layout: { view: FunctionPlotBox, dataField: defaultDataKey }, + options: { links: "@links(self)" } + }], + [DocumentType.BUTTON, { + layout: { view: LabelBox, dataField: "onClick" }, + options: { links: "@links(self)" } + }], + [DocumentType.SLIDER, { + layout: { view: SliderBox, dataField: defaultDataKey, }, + options: { links: "@links(self)", treeViewGrowsHorizontally: true } + }], + [DocumentType.PRES, { + layout: { view: PresBox, dataField: defaultDataKey }, + options: { links: "@links(self)" } + }], + [DocumentType.FONTICON, { + layout: { view: FontIconBox, dataField: defaultDataKey }, + options: { hideLinkButton: true, _width: 40, _height: 40, borderRounding: "100%", links: "@links(self)", hideDocumentButtonBar: true }, + }], + [DocumentType.WEBCAM, { + layout: { view: RecordingBox, dataField: defaultDataKey }, + options: { links: "@links(self)" } + }], + [DocumentType.PRESELEMENT, { + layout: { view: PresElementBox, dataField: defaultDataKey } + }], + [DocumentType.MARKER, { + layout: { view: CollectionView, dataField: defaultDataKey }, + options: { links: "@links(self)", hideLinkButton: true, pointerEvents: "none" } + }], + [DocumentType.INK, { // NOTE: this is unused!! ink fields are filled in directly within the InkDocument() method + layout: { view: InkingStroke, dataField: defaultDataKey }, + options: { links: "@links(self)" } + }], + [DocumentType.SCREENSHOT, { + layout: { view: ScreenshotBox, dataField: defaultDataKey }, + options: { links: "@links(self)" } + }], + [DocumentType.COMPARISON, { + layout: { view: ComparisonBox, dataField: defaultDataKey }, + options: { clipWidth: 50, backgroundColor: "gray", targetDropAction: "alias", links: "@links(self)" } + }], + [DocumentType.GROUPDB, { + data: new List(), + layout: { view: EmptyBox, dataField: defaultDataKey }, + options: { childDropAction: "alias", title: "Global Group Database" } + }], + [DocumentType.GROUP, { + layout: { view: EmptyBox, dataField: defaultDataKey }, + options: { links: "@links(self)" } + }], + ]); + + const suffix = "Proto"; + + /** + * This function loads or initializes the prototype for each docment type. + * + * This is an asynchronous function because it has to attempt + * to fetch the prototype documents from the server. + * + * Once we have this object that maps the prototype ids to a potentially + * undefined document, we either initialize our private prototype + * variables with the document returned from the server or, if prototypes + * haven't been initialized, the newly initialized prototype document. + */ + export async function initialize(): Promise { + ProxyField.initPlugin(); + ComputedField.initPlugin(); + // non-guid string ids for each document prototype + const prototypeIds = Object.values(DocumentType).filter(type => type !== DocumentType.NONE).map(type => type + suffix); + // fetch the actual prototype documents from the server + const actualProtos = Docs.newAccount ? {} : await DocServer.GetRefFields(prototypeIds); + if (!Docs.newAccount) { + Cast(actualProtos[DocumentType.WEB + suffix], Doc, null).nativeHeightUnfrozen = true; // to avoid having to recreate the DB + Cast(actualProtos[DocumentType.PDF + suffix], Doc, null).nativeHeightUnfrozen = true; + Cast(actualProtos[DocumentType.RTF + suffix], Doc, null).nativeHeightUnfrozen = true; + Cast(actualProtos[DocumentType.WEB + suffix], Doc, null).nativeDimModifiable = true; // to avoid having to recreate the DB + Cast(actualProtos[DocumentType.PDF + suffix], Doc, null).nativeDimModifiable = true; + Cast(actualProtos[DocumentType.RTF + suffix], Doc, null).nativeDimModifiable = true; + } + + // update this object to include any default values: DocumentOptions for all prototypes + prototypeIds.map(id => { + const existing = actualProtos[id] as Doc; + const type = id.replace(suffix, "") as DocumentType; + // get or create prototype of the specified type... + const target = existing || buildPrototype(type, id); + // ...and set it if not undefined (can be undefined only if TemplateMap does not contain + // an entry dedicated to the given DocumentType) + target && PrototypeMap.set(type, target); + }); + } + + /** + * Retrieves the prototype for the given document type, or + * undefined if that type's proto doesn't have a configuration + * in the template map. + * @param type + */ + const PrototypeMap: PrototypeMap = new Map(); + export function get(type: DocumentType): Doc { return PrototypeMap.get(type)!; } + + /** + * A collection of all links in the database. Ideally, this would be a search, but for now all links are cached here. + */ + export function MainLinkDocument() { return Prototypes.get(DocumentType.LINKDB); } + + /** + * A collection of all scripts in the database + */ + export function MainScriptDocument() { return Prototypes.get(DocumentType.SCRIPTDB); } + + /** + * A collection of all user acl groups in the database + */ + export function MainGroupDocument() { return Prototypes.get(DocumentType.GROUPDB); } + + /** + * This is a convenience method that is used to initialize + * prototype documents for the first time. + * + * @param protoId the id of the prototype, indicating the specific prototype + * to initialize (see the *protoId list at the top of the namespace) + * @param title the prototype document's title, follows *-PROTO + * @param layout the layout key for this prototype and thus the + * layout key that all delegates will inherit + * @param options any value specified in the DocumentOptions object likewise + * becomes the default value for that key for all delegates + */ + function buildPrototype(type: DocumentType, prototypeId: string): Opt { + // load template from type + const template = TemplateMap.get(type); + if (!template) { + return undefined; + } + const layout = template.layout; + // create title + const upper = suffix.toUpperCase(); + const title = prototypeId.toUpperCase().replace(upper, `_${upper}`); + // synthesize the default options, the type and title from computed values and + // whatever options pertain to this specific prototype + const options: DocumentOptions = { + system: true, _layoutKey: "layout", title, type, baseProto: true, x: 0, y: 0, _width: 300, ...(template.options || {}), + layout: layout.view?.LayoutString(layout.dataField), data: template.data, layout_keyValue: KeyValueBox.LayoutString("") + }; + Object.entries(options).map(pair => { + if (typeof pair[1] === "string" && pair[1].startsWith("@")) { + (options as any)[pair[0]] = ComputedField.MakeFunction(pair[1].substring(1)); + } + }); + return Doc.assign(new Doc(prototypeId, true), options as any, undefined, true); + } + } + + /** + * Encapsulates the factory used to create new document instances + * delegated from top-level prototypes + */ + export namespace Create { + + /** + * This function receives the relevant document prototype and uses + * it to create a new of that base-level prototype, or the + * underlying data document, which it then delegates again + * to create the view document. + * + * It also takes the opportunity to register the user + * that created the document and the time of creation. + * + * @param proto the specific document prototype off of which to model + * this new instance (textProto, imageProto, etc.) + * @param data the Field to store at this new instance's data key + * @param options any initial values to provide for this new instance + * @param delegId if applicable, an existing document id. If undefined, Doc's + * constructor just generates a new GUID. This is currently used + * only when creating a DockDocument from the current user's already existing + * main document. + */ + function InstanceFromProto(proto: Doc, data: Field | undefined, options: DocumentOptions, delegId?: string, fieldKey: string = "data", protoId?: string) { + const viewKeys = ["x", "y", "system"]; // keys that should be addded to the view document even though they don't begin with an "_" + const { omit: dataProps, extract: viewProps } = OmitKeys(options, viewKeys, "^_"); + + dataProps["acl-Override"] = "None"; + dataProps["acl-Public"] = options["acl-Public"] ? options["acl-Public"] : Doc.UserDoc()?.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Augment; + + dataProps.system = viewProps.system; + dataProps.isPrototype = true; + dataProps.author = Doc.CurrentUserEmail; + dataProps.creationDate = new DateField; + dataProps[`${fieldKey}-lastModified`] = new DateField; + + dataProps[fieldKey] = data; + + // so that the list of annotations is already initialised, prevents issues in addonly. + // without this, if a doc has no annotations but the user has AddOnly privileges, they won't be able to add an annotation because they would have needed to create the field's list which they don't have permissions to do. + dataProps[fieldKey + "-annotations"] = new List(); + const dataDoc = Doc.assign(Doc.MakeDelegate(proto, protoId), dataProps, undefined, true); + + const viewFirstProps: { [id: string]: any } = {}; + viewFirstProps["acl-Public"] = options["_acl-Public"] ? options["_acl-Public"] : Doc.UserDoc()?.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Augment; + viewFirstProps["acl-Override"] = "None"; + viewFirstProps.author = Doc.CurrentUserEmail; + const viewDoc = Doc.assign(Doc.MakeDelegate(dataDoc, delegId), viewFirstProps, true, true); + Doc.assign(viewDoc, viewProps, true, true); + ![DocumentType.LINK, DocumentType.MARKER, DocumentType.LABEL].includes(viewDoc.type as any) && DocUtils.MakeLinkToActiveAudio(() => viewDoc); + + !Doc.IsSystem(dataDoc) && ![DocumentType.MARKER, DocumentType.KVP, DocumentType.LINK, DocumentType.LINKANCHOR].includes(proto.type as any) && + !dataDoc.isFolder && !dataProps.annotationOn && Doc.AddDocToList(Cast(Doc.UserDoc().myFileOrphans, Doc, null), "data", dataDoc); + + updateCachedAcls(dataDoc); + updateCachedAcls(viewDoc); + return viewDoc; + } + + export function ImageDocument(url: string, options: DocumentOptions = {}) { + const imgField = new ImageField(url); + return InstanceFromProto(Prototypes.get(DocumentType.IMG), imgField, { title: basename(url), ...options }); + } + + export function PresDocument(options: DocumentOptions = {}) { + return InstanceFromProto(Prototypes.get(DocumentType.PRES), new List(), options); + } + + export function ScriptingDocument(script: Opt, options: DocumentOptions = {}, fieldKey?: string) { + return InstanceFromProto(Prototypes.get(DocumentType.SCRIPTING), script, + { ...options, layout: fieldKey ? ScriptingBox.LayoutString(fieldKey) : undefined }); + } + + export function VideoDocument(url: string, options: DocumentOptions = {}) { + return InstanceFromProto(Prototypes.get(DocumentType.VID), new VideoField(url), options); + } + + export function YoutubeDocument(url: string, options: DocumentOptions = {}) { + return InstanceFromProto(Prototypes.get(DocumentType.YOUTUBE), new YoutubeField(url), options); + } + + export function WebCamDocument(url: string, options: DocumentOptions = {}) { + return InstanceFromProto(Prototypes.get(DocumentType.WEBCAM), "", options); + } + + export function ScreenshotDocument(options: DocumentOptions = {}) { + return InstanceFromProto(Prototypes.get(DocumentType.SCREENSHOT), "", options); + } + + export function ComparisonDocument(options: DocumentOptions = { title: "Comparison Box" }) { + return InstanceFromProto(Prototypes.get(DocumentType.COMPARISON), "", options); + } + + export function AudioDocument(url: string, options: DocumentOptions = {}) { + return InstanceFromProto(Prototypes.get(DocumentType.AUDIO), new AudioField(url), + { ...options, backgroundColor: ComputedField.MakeFunction("this._mediaState === 'playing' ? 'green':'gray'") as any }); + } + + export function RecordingDocument(url: string, options: DocumentOptions = {}) { + return InstanceFromProto(Prototypes.get(DocumentType.REC), "", options); + } + + export function SearchDocument(options: DocumentOptions = {}) { + return InstanceFromProto(Prototypes.get(DocumentType.SEARCH), new List([]), options); + } + + export function ColorDocument(options: DocumentOptions = {}) { + return InstanceFromProto(Prototypes.get(DocumentType.COLOR), "", options); + } + + export function RTFDocument(field: RichTextField, options: DocumentOptions = {}, fieldKey: string = "text") { + return InstanceFromProto(Prototypes.get(DocumentType.RTF), field, options, undefined, fieldKey); + } + export function TextDocument(text: string, options: DocumentOptions = {}, fieldKey: string = "text") { + const rtf = { + doc: { + type: "doc", content: [{ + type: "paragraph", + content: [{ + type: "text", + text + }] + }] + }, + selection: { type: "text", anchor: 1, head: 1 }, + storedMarks: [] + }; + const field = text ? new RichTextField(JSON.stringify(rtf), text) : undefined; + return InstanceFromProto(Prototypes.get(DocumentType.RTF), field, options, undefined, fieldKey); + } + + export function LinkDocument(source: { doc: Doc, ctx?: Doc }, target: { doc: Doc, ctx?: Doc }, options: DocumentOptions = {}, id?: string) { + const linkDoc = InstanceFromProto(Prototypes.get(DocumentType.LINK), undefined, { + anchor1: source.doc, anchor2: target.doc, ...options + }, id); + + LinkManager.Instance.addLink(linkDoc); + + return linkDoc; + } + + export function InkDocument(color: string, tool: string, strokeWidth: number, strokeBezier: string, fillColor: string, arrowStart: string, arrowEnd: string, dash: string, points: PointData[], options: DocumentOptions = {}) { + const I = new Doc(); + I[Initializing] = true; + I.type = DocumentType.INK; + I.layout = InkingStroke.LayoutString("data"); + I.color = color; + I.hideDecorationTitle = true; // don't show title when selected + // I.hideOpenButton = true; // don't show open full screen button when selected + I.fillColor = fillColor; + I.strokeWidth = strokeWidth; + I.strokeBezier = strokeBezier; + I.strokeStartMarker = arrowStart; + I.strokeEndMarker = arrowEnd; + I.strokeDash = dash; + I.tool = tool; + I["text-align"] = "center"; + I.title = "ink"; + I.x = options.x; + I.y = options.y; + I._width = options._width as number; + I._height = options._height as number; + I._fontFamily = "cursive"; + I.author = Doc.CurrentUserEmail; + I.rotation = 0; + I.data = new InkField(points); + I.creationDate = new DateField; + I["acl-Public"] = Doc.UserDoc()?.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Augment; + I["acl-Override"] = "None"; + I.links = ComputedField.MakeFunction("links(self)"); + I[Initializing] = false; + return I; + } + + export function PdfDocument(url: string, options: DocumentOptions = {}) { + const width = options._width || undefined; + const height = options._height || undefined; + const nwid = options._nativeWidth || undefined; + const nhght = options._nativeHeight || undefined; + if (!nhght && width && height && nwid) options._nativeHeight = Number(nwid) * Number(height) / Number(width); + return InstanceFromProto(Prototypes.get(DocumentType.PDF), new PdfField(url), options); + } + + export function WebDocument(url: string, options: DocumentOptions = {}) { + const width = options._width || undefined; + const height = options._height || undefined; + const nwid = options._nativeWidth || undefined; + const nhght = options._nativeHeight || undefined; + if (!nhght && width && height && nwid) options._nativeHeight = Number(nwid) * Number(height) / Number(width); + return InstanceFromProto(Prototypes.get(DocumentType.WEB), url ? new WebField(url) : undefined, options); + } + + export function HtmlDocument(html: string, options: DocumentOptions = {}) { + return InstanceFromProto(Prototypes.get(DocumentType.WEB), new HtmlField(html), options); + } + + export function MapDocument(documents: Array, options: DocumentOptions = {}) { + return InstanceFromProto(Prototypes.get(DocumentType.MAP), new List(documents), options); + } + + export function MapMarkerDocument(lat: number, lng: number, infoWindowOpen: boolean, documents: Array, options: DocumentOptions, id?: string) { + return InstanceFromProto(Prototypes.get(DocumentType.MARKER), new List(documents), { lat, lng, infoWindowOpen, ...options }, id); + } + + export function KVPDocument(document: Doc, options: DocumentOptions = {}) { + return InstanceFromProto(Prototypes.get(DocumentType.KVP), document, { title: document.title + ".kvp", ...options }); + } + + export function FreeformDocument(documents: Array, options: DocumentOptions, id?: string) { + const inst = InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _xPadding: 20, _yPadding: 20, ...options, _viewType: CollectionViewType.Freeform }, id); + documents.map(d => d.context = inst); + return inst; + } + + export function WebanchorDocument(url?: string, options: DocumentOptions = {}, id?: string) { + return InstanceFromProto(Prototypes.get(DocumentType.MARKER), url, options, id); + } + + export function TextanchorDocument(options: DocumentOptions = {}, id?: string) { + return InstanceFromProto(Prototypes.get(DocumentType.MARKER), options?.data, options, id); + } + + export function HTMLAnchorDocument(documents: Array, options: DocumentOptions, id?: string) { + return InstanceFromProto(Prototypes.get(DocumentType.MARKER), new List(documents), options, id); + } + + export function PileDocument(documents: Array, options: DocumentOptions, id?: string) { + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _overflow: "visible", _forceActive: true, _noAutoscroll: true, ...options, _viewType: CollectionViewType.Pile }, id); + } + + export function LinearDocument(documents: Array, options: DocumentOptions, id?: string) { + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Linear }, id); + } + + export function MapCollectionDocument(documents: Array, options: DocumentOptions = {}) { + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Map }); + } + + export function CarouselDocument(documents: Array, options: DocumentOptions) { + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Carousel }); + } + + export function Carousel3DDocument(documents: Array, options: DocumentOptions) { + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Carousel3D }); + } + + export function SchemaDocument(schemaHeaders: SchemaHeaderField[], documents: Array, options: DocumentOptions) { + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { schemaHeaders: new List(schemaHeaders), ...options, _viewType: CollectionViewType.Schema }); + } + + export function TreeDocument(documents: Array, options: DocumentOptions, id?: string, protoId?: string) { + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _xMargin: 5, _yMargin: 5, ...options, _viewType: CollectionViewType.Tree }, id, undefined, protoId); + } + + export function StackingDocument(documents: Array, options: DocumentOptions, id?: string, protoId?: string) { + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Stacking }, id, undefined, protoId); + } + + export function MulticolumnDocument(documents: Array, options: DocumentOptions) { + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Multicolumn }); + } + export function MultirowDocument(documents: Array, options: DocumentOptions) { + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Multirow }); + } + + export function MasonryDocument(documents: Array, options: DocumentOptions) { + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Masonry }); + } + + export function LabelDocument(options?: DocumentOptions) { + return InstanceFromProto(Prototypes.get(DocumentType.LABEL), undefined, { ...(options || {}) }); + } + + export function EquationDocument(options?: DocumentOptions) { + return InstanceFromProto(Prototypes.get(DocumentType.EQUATION), undefined, { ...(options || {}) }); + } + + export function FunctionPlotDocument(documents: Array, options?: DocumentOptions) { + return InstanceFromProto(Prototypes.get(DocumentType.FUNCPLOT), new List(documents), { ...(options || {}) }); + } + + export function ButtonDocument(options?: DocumentOptions) { + return InstanceFromProto(Prototypes.get(DocumentType.BUTTON), undefined, { ...(options || {}), "onClick-rawScript": "-script-" }); + } + + export function SliderDocument(options?: DocumentOptions) { + return InstanceFromProto(Prototypes.get(DocumentType.SLIDER), undefined, { ...(options || {}) }); + } + + export function FontIconDocument(options?: DocumentOptions) { + return InstanceFromProto(Prototypes.get(DocumentType.FONTICON), undefined, { ...(options || {}) }); + } + export function FilterDocument(options?: DocumentOptions) { + return InstanceFromProto(Prototypes.get(DocumentType.FILTER), undefined, { ...(options || {}) }); + } + + export function PresElementBoxDocument(options?: DocumentOptions) { + return InstanceFromProto(Prototypes.get(DocumentType.PRESELEMENT), undefined, { ...(options || {}) }); + } + + export function DockDocument(documents: Array, config: string, options: DocumentOptions, id?: string) { + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { freezeChildren: "remove|add", ...options, _viewType: CollectionViewType.Docking, dockingConfig: config }, id); + } + + export function DirectoryImportDocument(options: DocumentOptions = {}) { + return InstanceFromProto(Prototypes.get(DocumentType.IMPORT), new List(), options); + } + + export type DocConfig = { + doc: Doc, + initialWidth?: number, + path?: Doc[] }; - const field = text ? new RichTextField(JSON.stringify(rtf), text) : undefined; - return InstanceFromProto(Prototypes.get(DocumentType.RTF), field, options, undefined, fieldKey); - } - - export function LinkDocument(source: { doc: Doc, ctx?: Doc }, target: { doc: Doc, ctx?: Doc }, options: DocumentOptions = {}, id?: string) { - const linkDoc = InstanceFromProto(Prototypes.get(DocumentType.LINK), undefined, { - anchor1: source.doc, anchor2: target.doc, ...options - }, id); - - LinkManager.Instance.addLink(linkDoc); - - return linkDoc; - } - - export function InkDocument(color: string, tool: string, strokeWidth: number, strokeBezier: string, fillColor: string, arrowStart: string, arrowEnd: string, dash: string, points: PointData[], options: DocumentOptions = {}) { - const I = new Doc(); - I[Initializing] = true; - I.type = DocumentType.INK; - I.layout = InkingStroke.LayoutString("data"); - I.color = color; - I.hideDecorationTitle = true; // don't show title when selected - // I.hideOpenButton = true; // don't show open full screen button when selected - I.fillColor = fillColor; - I.strokeWidth = strokeWidth; - I.strokeBezier = strokeBezier; - I.strokeStartMarker = arrowStart; - I.strokeEndMarker = arrowEnd; - I.strokeDash = dash; - I.tool = tool; - I["text-align"] = "center"; - I.title = "ink"; - I.x = options.x; - I.y = options.y; - I._width = options._width as number; - I._height = options._height as number; - I._fontFamily = "cursive"; - I.author = Doc.CurrentUserEmail; - I.rotation = 0; - I.data = new InkField(points); - I.creationDate = new DateField; - I["acl-Public"] = Doc.UserDoc()?.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Augment; - I["acl-Override"] = "None"; - I.links = ComputedField.MakeFunction("links(self)"); - I[Initializing] = false; - return I; - } - - export function PdfDocument(url: string, options: DocumentOptions = {}) { - const width = options._width || undefined; - const height = options._height || undefined; - const nwid = options._nativeWidth || undefined; - const nhght = options._nativeHeight || undefined; - if (!nhght && width && height && nwid) options._nativeHeight = Number(nwid) * Number(height) / Number(width); - return InstanceFromProto(Prototypes.get(DocumentType.PDF), new PdfField(url), options); - } - - export function WebDocument(url: string, options: DocumentOptions = {}) { - const width = options._width || undefined; - const height = options._height || undefined; - const nwid = options._nativeWidth || undefined; - const nhght = options._nativeHeight || undefined; - if (!nhght && width && height && nwid) options._nativeHeight = Number(nwid) * Number(height) / Number(width); - return InstanceFromProto(Prototypes.get(DocumentType.WEB), url ? new WebField(url) : undefined, options); - } - - export function HtmlDocument(html: string, options: DocumentOptions = {}) { - return InstanceFromProto(Prototypes.get(DocumentType.WEB), new HtmlField(html), options); - } - - export function MapDocument(documents: Array, options: DocumentOptions = {}) { - return InstanceFromProto(Prototypes.get(DocumentType.MAP), new List(documents), options); - } - - export function MapMarkerDocument(lat: number, lng: number, infoWindowOpen: boolean, documents: Array, options: DocumentOptions, id?: string) { - return InstanceFromProto(Prototypes.get(DocumentType.MARKER), new List(documents), { lat, lng, infoWindowOpen, ...options }, id); - } - - export function KVPDocument(document: Doc, options: DocumentOptions = {}) { - return InstanceFromProto(Prototypes.get(DocumentType.KVP), document, { title: document.title + ".kvp", ...options }); - } - - export function FreeformDocument(documents: Array, options: DocumentOptions, id?: string) { - const inst = InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _xPadding: 20, _yPadding: 20, ...options, _viewType: CollectionViewType.Freeform }, id); - documents.map(d => d.context = inst); - return inst; - } - - export function WebanchorDocument(url?: string, options: DocumentOptions = {}, id?: string) { - return InstanceFromProto(Prototypes.get(DocumentType.MARKER), url, options, id); - } - - export function TextanchorDocument(options: DocumentOptions = {}, id?: string) { - return InstanceFromProto(Prototypes.get(DocumentType.MARKER), options?.data, options, id); - } - - export function HTMLAnchorDocument(documents: Array, options: DocumentOptions, id?: string) { - return InstanceFromProto(Prototypes.get(DocumentType.MARKER), new List(documents), options, id); - } - - export function PileDocument(documents: Array, options: DocumentOptions, id?: string) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _overflow: "visible", _forceActive: true, _noAutoscroll: true, ...options, _viewType: CollectionViewType.Pile }, id); - } - - export function LinearDocument(documents: Array, options: DocumentOptions, id?: string) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Linear }, id); - } - - export function MapCollectionDocument(documents: Array, options: DocumentOptions = {}) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Map }); - } - - export function CarouselDocument(documents: Array, options: DocumentOptions) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Carousel }); - } - - export function Carousel3DDocument(documents: Array, options: DocumentOptions) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Carousel3D }); - } - - export function SchemaDocument(schemaHeaders: SchemaHeaderField[], documents: Array, options: DocumentOptions) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { schemaHeaders: new List(schemaHeaders), ...options, _viewType: CollectionViewType.Schema }); - } - - export function TreeDocument(documents: Array, options: DocumentOptions, id?: string, protoId?: string) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _xMargin: 5, _yMargin: 5, ...options, _viewType: CollectionViewType.Tree }, id, undefined, protoId); - } - - export function StackingDocument(documents: Array, options: DocumentOptions, id?: string, protoId?: string) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Stacking }, id, undefined, protoId); - } - - export function MulticolumnDocument(documents: Array, options: DocumentOptions) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Multicolumn }); - } - export function MultirowDocument(documents: Array, options: DocumentOptions) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Multirow }); - } - - export function MasonryDocument(documents: Array, options: DocumentOptions) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Masonry }); - } - - export function LabelDocument(options?: DocumentOptions) { - return InstanceFromProto(Prototypes.get(DocumentType.LABEL), undefined, { ...(options || {}) }); - } - - export function EquationDocument(options?: DocumentOptions) { - return InstanceFromProto(Prototypes.get(DocumentType.EQUATION), undefined, { ...(options || {}) }); - } - - export function FunctionPlotDocument(documents: Array, options?: DocumentOptions) { - return InstanceFromProto(Prototypes.get(DocumentType.FUNCPLOT), new List(documents), { ...(options || {}) }); - } - - export function ButtonDocument(options?: DocumentOptions) { - return InstanceFromProto(Prototypes.get(DocumentType.BUTTON), undefined, { ...(options || {}), "onClick-rawScript": "-script-" }); - } - - export function SliderDocument(options?: DocumentOptions) { - return InstanceFromProto(Prototypes.get(DocumentType.SLIDER), undefined, { ...(options || {}) }); - } - - export function FontIconDocument(options?: DocumentOptions) { - return InstanceFromProto(Prototypes.get(DocumentType.FONTICON), undefined, { ...(options || {}) }); - } - export function FilterDocument(options?: DocumentOptions) { - return InstanceFromProto(Prototypes.get(DocumentType.FILTER), undefined, { ...(options || {}) }); - } - - export function PresElementBoxDocument(options?: DocumentOptions) { - return InstanceFromProto(Prototypes.get(DocumentType.PRESELEMENT), undefined, { ...(options || {}) }); - } - - export function DockDocument(documents: Array, config: string, options: DocumentOptions, id?: string) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { freezeChildren: "remove|add", ...options, _viewType: CollectionViewType.Docking, dockingConfig: config }, id); - } - - export function DirectoryImportDocument(options: DocumentOptions = {}) { - return InstanceFromProto(Prototypes.get(DocumentType.IMPORT), new List(), options); - } - - export type DocConfig = { - doc: Doc, - initialWidth?: number, - path?: Doc[] - }; - - export function StandardCollectionDockingDocument(configs: Array, options: DocumentOptions, id?: string, type: string = "row") { - const layoutConfig = { - content: [ - { - type: type, + + export function StandardCollectionDockingDocument(configs: Array, options: DocumentOptions, id?: string, type: string = "row") { + const layoutConfig = { content: [ - ...configs.map(config => CollectionDockingView.makeDocumentConfig(config.doc, undefined, config.initialWidth)) + { + type: type, + content: [ + ...configs.map(config => CollectionDockingView.makeDocumentConfig(config.doc, undefined, config.initialWidth)) + ] + } ] - } - ] - }; - return DockDocument(configs.map(c => c.doc), JSON.stringify(layoutConfig), options, id); - } + }; + return DockDocument(configs.map(c => c.doc), JSON.stringify(layoutConfig), options, id); + } - export function DelegateDocument(proto: Doc, options: DocumentOptions = {}) { - return InstanceFromProto(proto, undefined, options); - } - } + export function DelegateDocument(proto: Doc, options: DocumentOptions = {}) { + return InstanceFromProto(proto, undefined, options); + } + } } export namespace DocUtils { - export function Excluded(d: Doc, docFilters: string[]) { - const filterFacets: { [key: string]: { [value: string]: string } } = {}; // maps each filter key to an object with value=>modifier fields - docFilters.forEach(filter => { - const fields = filter.split(":"); - const key = fields[0]; - const value = fields[1]; - const modifiers = fields[2]; - if (!filterFacets[key]) { - filterFacets[key] = {}; - } - filterFacets[key][value] = modifiers; - }); - - if (d.z) return false; - for (const facetKey of Object.keys(filterFacets)) { - const facet = filterFacets[facetKey]; - const xs = Object.keys(facet).filter(value => facet[value] === "x"); - const failsNotEqualFacets = xs?.some(value => Doc.matchFieldValue(d, facetKey, value)); - if (failsNotEqualFacets) { - return true; - } - } - return false; - } - /** - * @param docs - * @param docFilters - * @param docRangeFilters - * @param viewSpecScript - * Given a list of docs and docFilters, @returns the list of Docs that match those filters - */ - export function FilterDocs(docs: Doc[], docFilters: string[], docRangeFilters: string[], viewSpecScript?: ScriptField, parentCollection?: Doc) { - const childDocs = viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs; - if (!docFilters?.length && !docRangeFilters?.length) { - return childDocs.filter(d => !d.cookies); // remove documents that need a cookie if there are no filters to provide one - } - - const filterFacets: { [key: string]: { [value: string]: string } } = {}; // maps each filter key to an object with value=>modifier fields - docFilters.forEach(filter => { - const fields = filter.split(":"); - const key = fields[0]; - const value = fields[1]; - const modifiers = fields[2]; - if (!filterFacets[key]) { - filterFacets[key] = {}; - } - filterFacets[key][value] = modifiers; - }); - - const filteredDocs = docFilters.length ? childDocs.filter(d => { - if (d.z) return true; - // if the document needs a cookie but no filter provides the cookie, then the document does not pass the filter - if (d.cookies && (!filterFacets.cookies || !Object.keys(filterFacets.cookies).some(key => d.cookies === key))) { - return false; - } - - for (const facetKey of Object.keys(filterFacets).filter(fkey => fkey !== "cookies")) { - const facet = filterFacets[facetKey]; - - // facets that match some value in the field of the document (e.g. some text field) - const matches = Object.keys(facet).filter(value => value !== "cookies" && facet[value] === "match"); - - // facets that have a check next to them - const checks = Object.keys(facet).filter(value => facet[value] === "check"); - - // metadata facets that exist - const exists = Object.keys(facet).filter(value => facet[value] === "exists"); - - // metadata facets that exist - const unsets = Object.keys(facet).filter(value => facet[value] === "unset"); - - // facets that have an x next to them - const xs = Object.keys(facet).filter(value => facet[value] === "x"); - - if (!unsets.length && !exists.length && !xs.length && !checks.length && !matches.length) return true; - const failsNotEqualFacets = !xs.length ? false : xs.some(value => Doc.matchFieldValue(d, facetKey, value)); - const satisfiesCheckFacets = !checks.length ? true : checks.some(value => Doc.matchFieldValue(d, facetKey, value)); - const satisfiesExistsFacets = !exists.length ? true : exists.some(value => d[facetKey] !== undefined); - const satisfiesUnsetsFacets = !unsets.length ? true : unsets.some(value => d[facetKey] === undefined); - const satisfiesMatchFacets = !matches.length ? true : matches.some(value => { - if (facetKey.startsWith("*")) { // fields starting with a '*' are used to match families of related fields. ie, *lastModified will match text-lastModified, data-lastModified, etc - const allKeys = Array.from(Object.keys(d)); - allKeys.push(...Object.keys(Doc.GetProto(d))); - const keys = allKeys.filter(key => key.includes(facetKey.substring(1))); - return keys.some(key => Field.toString(d[key] as Field).includes(value)); - } - return Field.toString(d[facetKey] as Field).includes(value); - }); - // if we're ORing them together, the default return is false, and we return true for a doc if it satisfies any one set of criteria - if ((parentCollection?.currentFilter as Doc)?.filterBoolean === "OR") { - if (satisfiesUnsetsFacets && satisfiesExistsFacets && satisfiesCheckFacets && !failsNotEqualFacets && satisfiesMatchFacets) return true; - } - // if we're ANDing them together, the default return is true, and we return false for a doc if it doesn't satisfy any set of criteria - else { - if (!satisfiesUnsetsFacets || !satisfiesExistsFacets || !satisfiesCheckFacets || failsNotEqualFacets || (matches.length && !satisfiesMatchFacets)) return false; - } - - } - return (parentCollection?.currentFilter as Doc)?.filterBoolean === "OR" ? false : true; - }) : childDocs; - const rangeFilteredDocs = filteredDocs.filter(d => { - for (let i = 0; i < docRangeFilters.length; i += 3) { - const key = docRangeFilters[i]; - const min = Number(docRangeFilters[i + 1]); - const max = Number(docRangeFilters[i + 2]); - const val = Cast(d[key], "number", null); - if (val < min || val > max) return false; - if (val === undefined) { - //console.log("Should 'undefined' pass range filter or not?") - } - } - return true; - }); - return rangeFilteredDocs; - } - - export function Publish(promoteDoc: Doc, targetID: string, addDoc: any, remDoc: any) { - targetID = targetID.replace(/^-/, "").replace(/\([0-9]*\)$/, ""); - DocServer.GetRefField(targetID).then(doc => { - if (promoteDoc !== doc) { - let copy = doc as Doc; - if (copy) { - Doc.Overwrite(promoteDoc, copy, true); - } else { - copy = Doc.MakeCopy(promoteDoc, true, targetID); - } - !doc && (copy.title = undefined) && (Doc.GetProto(copy).title = targetID); - addDoc && addDoc(copy); - remDoc && remDoc(promoteDoc); - if (!doc) { - DocListCastAsync(promoteDoc.links).then(links => { - links && links.map(async link => { - if (link) { - const a1 = await Cast(link.anchor1, Doc); - if (a1 && Doc.AreProtosEqual(a1, promoteDoc)) link.anchor1 = copy; - const a2 = await Cast(link.anchor2, Doc); - if (a2 && Doc.AreProtosEqual(a2, promoteDoc)) link.anchor2 = copy; - LinkManager.Instance.deleteLink(link); - LinkManager.Instance.addLink(link); - } + export function Excluded(d: Doc, docFilters: string[]) { + const filterFacets: { [key: string]: { [value: string]: string } } = {}; // maps each filter key to an object with value=>modifier fields + docFilters.forEach(filter => { + const fields = filter.split(":"); + const key = fields[0]; + const value = fields[1]; + const modifiers = fields[2]; + if (!filterFacets[key]) { + filterFacets[key] = {}; + } + filterFacets[key][value] = modifiers; + }); + + if (d.z) return false; + for (const facetKey of Object.keys(filterFacets)) { + const facet = filterFacets[facetKey]; + const xs = Object.keys(facet).filter(value => facet[value] === "x"); + const failsNotEqualFacets = xs?.some(value => Doc.matchFieldValue(d, facetKey, value)); + if (failsNotEqualFacets) { + return true; + } + } + return false; + } + /** + * @param docs + * @param docFilters + * @param docRangeFilters + * @param viewSpecScript + * Given a list of docs and docFilters, @returns the list of Docs that match those filters + */ + export function FilterDocs(docs: Doc[], docFilters: string[], docRangeFilters: string[], viewSpecScript?: ScriptField, parentCollection?: Doc) { + const childDocs = viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs; + if (!docFilters?.length && !docRangeFilters?.length) { + return childDocs.filter(d => !d.cookies); // remove documents that need a cookie if there are no filters to provide one + } + + const filterFacets: { [key: string]: { [value: string]: string } } = {}; // maps each filter key to an object with value=>modifier fields + docFilters.forEach(filter => { + const fields = filter.split(":"); + const key = fields[0]; + const value = fields[1]; + const modifiers = fields[2]; + if (!filterFacets[key]) { + filterFacets[key] = {}; + } + filterFacets[key][value] = modifiers; + }); + + const filteredDocs = docFilters.length ? childDocs.filter(d => { + if (d.z) return true; + // if the document needs a cookie but no filter provides the cookie, then the document does not pass the filter + if (d.cookies && (!filterFacets.cookies || !Object.keys(filterFacets.cookies).some(key => d.cookies === key))) { + return false; + } + + for (const facetKey of Object.keys(filterFacets).filter(fkey => fkey !== "cookies")) { + const facet = filterFacets[facetKey]; + + // facets that match some value in the field of the document (e.g. some text field) + const matches = Object.keys(facet).filter(value => value !== "cookies" && facet[value] === "match"); + + // facets that have a check next to them + const checks = Object.keys(facet).filter(value => facet[value] === "check"); + + // metadata facets that exist + const exists = Object.keys(facet).filter(value => facet[value] === "exists"); + + // metadata facets that exist + const unsets = Object.keys(facet).filter(value => facet[value] === "unset"); + + // facets that have an x next to them + const xs = Object.keys(facet).filter(value => facet[value] === "x"); + + if (!unsets.length && !exists.length && !xs.length && !checks.length && !matches.length) return true; + const failsNotEqualFacets = !xs.length ? false : xs.some(value => Doc.matchFieldValue(d, facetKey, value)); + const satisfiesCheckFacets = !checks.length ? true : checks.some(value => Doc.matchFieldValue(d, facetKey, value)); + const satisfiesExistsFacets = !exists.length ? true : exists.some(value => d[facetKey] !== undefined); + const satisfiesUnsetsFacets = !unsets.length ? true : unsets.some(value => d[facetKey] === undefined); + const satisfiesMatchFacets = !matches.length ? true : matches.some(value => { + if (facetKey.startsWith("*")) { // fields starting with a '*' are used to match families of related fields. ie, *lastModified will match text-lastModified, data-lastModified, etc + const allKeys = Array.from(Object.keys(d)); + allKeys.push(...Object.keys(Doc.GetProto(d))); + const keys = allKeys.filter(key => key.includes(facetKey.substring(1))); + return keys.some(key => Field.toString(d[key] as Field).includes(value)); + } + return Field.toString(d[facetKey] as Field).includes(value); + }); + // if we're ORing them together, the default return is false, and we return true for a doc if it satisfies any one set of criteria + if ((parentCollection?.currentFilter as Doc)?.filterBoolean === "OR") { + if (satisfiesUnsetsFacets && satisfiesExistsFacets && satisfiesCheckFacets && !failsNotEqualFacets && satisfiesMatchFacets) return true; + } + // if we're ANDing them together, the default return is true, and we return false for a doc if it doesn't satisfy any set of criteria + else { + if (!satisfiesUnsetsFacets || !satisfiesExistsFacets || !satisfiesCheckFacets || failsNotEqualFacets || (matches.length && !satisfiesMatchFacets)) return false; + } + + } + return (parentCollection?.currentFilter as Doc)?.filterBoolean === "OR" ? false : true; + }) : childDocs; + const rangeFilteredDocs = filteredDocs.filter(d => { + for (let i = 0; i < docRangeFilters.length; i += 3) { + const key = docRangeFilters[i]; + const min = Number(docRangeFilters[i + 1]); + const max = Number(docRangeFilters[i + 2]); + const val = Cast(d[key], "number", null); + if (val < min || val > max) return false; + if (val === undefined) { + //console.log("Should 'undefined' pass range filter or not?") + } + } + return true; + }); + return rangeFilteredDocs; + } + + export function Publish(promoteDoc: Doc, targetID: string, addDoc: any, remDoc: any) { + targetID = targetID.replace(/^-/, "").replace(/\([0-9]*\)$/, ""); + DocServer.GetRefField(targetID).then(doc => { + if (promoteDoc !== doc) { + let copy = doc as Doc; + if (copy) { + Doc.Overwrite(promoteDoc, copy, true); + } else { + copy = Doc.MakeCopy(promoteDoc, true, targetID); + } + !doc && (copy.title = undefined) && (Doc.GetProto(copy).title = targetID); + addDoc && addDoc(copy); + remDoc && remDoc(promoteDoc); + if (!doc) { + DocListCastAsync(promoteDoc.links).then(links => { + links && links.map(async link => { + if (link) { + const a1 = await Cast(link.anchor1, Doc); + if (a1 && Doc.AreProtosEqual(a1, promoteDoc)) link.anchor1 = copy; + const a2 = await Cast(link.anchor2, Doc); + if (a2 && Doc.AreProtosEqual(a2, promoteDoc)) link.anchor2 = copy; + LinkManager.Instance.deleteLink(link); + LinkManager.Instance.addLink(link); + } + }); + }); + } + } + }); + } + + export function DefaultFocus(doc: Doc, options?: DocFocusOptions) { + options?.afterFocus?.(false); + } + + export let ActiveRecordings: { props: FieldViewProps, getAnchor: () => Doc }[] = []; + + export function MakeLinkToActiveAudio(getSourceDoc: () => Doc, broadcastEvent = true) { + broadcastEvent && runInAction(() => DocumentManager.Instance.RecordingEvent = DocumentManager.Instance.RecordingEvent + 1); + return DocUtils.ActiveRecordings.map(audio => + DocUtils.MakeLink({ doc: getSourceDoc() }, { doc: audio.getAnchor() || audio.props.Document }, "recording annotation:linked recording", "recording timeline")); + } + + export function MakeLink(source: { doc: Doc }, target: { doc: Doc }, linkRelationship: string = "", description: string = "", id?: string, allowParCollectionLink?: boolean, showPopup?: number[]) { + if (!linkRelationship) linkRelationship = target.doc.type === DocumentType.RTF ? "Commentary:Comments On" : "link"; + const sv = DocumentManager.Instance.getDocumentView(source.doc); + if (!allowParCollectionLink && sv?.props.ContainingCollectionDoc === target.doc) return; + if (target.doc === Doc.UserDoc()) return undefined; + + const makeLink = action((linkDoc: Doc, showPopup?: number[]) => { + if (showPopup) { + LinkManager.currentLink = linkDoc; + + TaskCompletionBox.textDisplayed = "Link Created"; + TaskCompletionBox.popupX = showPopup[0]; + TaskCompletionBox.popupY = showPopup[1] - 33; + TaskCompletionBox.taskCompleted = true; + + LinkDescriptionPopup.popupX = showPopup[0]; + LinkDescriptionPopup.popupY = showPopup[1]; + LinkDescriptionPopup.descriptionPopup = true; + + const rect = document.body.getBoundingClientRect(); + if (LinkDescriptionPopup.popupX + 200 > rect.width) { + LinkDescriptionPopup.popupX -= 190; + TaskCompletionBox.popupX -= 40; + } + if (LinkDescriptionPopup.popupY + 100 > rect.height) { + LinkDescriptionPopup.popupY -= 40; + TaskCompletionBox.popupY -= 40; + } + + setTimeout(action(() => TaskCompletionBox.taskCompleted = false), 2500); + } + return linkDoc; + }); + + return makeLink(Docs.Create.LinkDocument(source, target, { + title: ComputedField.MakeFunction("generateLinkTitle(self)") as any, + "anchor1-useLinkSmallAnchor": source.doc.useLinkSmallAnchor ? true : undefined, + "anchor2-useLinkSmallAnchor": target.doc.useLinkSmallAnchor ? true : undefined, + "acl-Public": SharingPermissions.Augment, + "_acl-Public": SharingPermissions.Augment, + linkDisplay: true, + _hidden: true, + _linkAutoMove: true, + linkRelationship, + _showCaption: "description", + _showTitle: "linkRelationship", + description + }, id), showPopup); + } + + export function DocumentFromField(target: Doc, fieldKey: string, proto?: Doc, options?: DocumentOptions): Doc | undefined { + let created: Doc | undefined; + let layout: ((fieldKey: string) => string) | undefined; + const field = target[fieldKey]; + const resolved = options || {}; + if (field instanceof ImageField) { + created = Docs.Create.ImageDocument((field).url.href, resolved); + layout = ImageBox.LayoutString; + } else if (field instanceof Doc) { + created = field; + } else if (field instanceof VideoField) { + created = Docs.Create.VideoDocument((field).url.href, resolved); + layout = VideoBox.LayoutString; + } else if (field instanceof PdfField) { + created = Docs.Create.PdfDocument((field).url.href, resolved); + layout = PDFBox.LayoutString; + } else if (field instanceof AudioField) { + created = Docs.Create.AudioDocument((field).url.href, resolved); + layout = AudioBox.LayoutString; + } else if (field instanceof RecordingField) { + created = Docs.Create.RecordingDocument((field).url.href, resolved); + layout = RecordingBox.LayoutString; + } else if (field instanceof InkField) { + created = Docs.Create.InkDocument(ActiveInkColor(), CurrentUserUtils.SelectedTool, ActiveInkWidth(), ActiveInkBezierApprox(), ActiveFillColor(), ActiveArrowStart(), ActiveArrowEnd(), ActiveDash(), (field).inkData, resolved); + layout = InkingStroke.LayoutString; + } else if (field instanceof List && field[0] instanceof Doc) { + created = Docs.Create.StackingDocument(DocListCast(field), resolved); + layout = CollectionView.LayoutString; + } else if (field instanceof MapField) { + created = Docs.Create.MapDocument(DocListCast(field), resolved); + layout = MapBox.LayoutString; + } + else { + created = Docs.Create.TextDocument("", { ...{ _width: 200, _height: 25, _autoHeight: true }, ...resolved }); + layout = FormattedTextBox.LayoutString; + } + if (created) { + created.layout = layout?.(fieldKey); + created.title = fieldKey; + proto && created.proto && (created.proto = Doc.GetProto(proto)); + } + return created; + } + + export async function DocumentFromType(type: string, path: string, options: DocumentOptions): Promise> { + let ctor: ((path: string, options: DocumentOptions) => (Doc | Promise)) | undefined = undefined; + if (type.indexOf("image") !== -1) { + ctor = Docs.Create.ImageDocument; + if (!options._width) options._width = 300; + } + if (type.indexOf("video") !== -1) { + ctor = Docs.Create.VideoDocument; + if (!options._width) options._width = 600; + if (!options._height) options._height = (options._width as number) * 2 / 3; + } + if (type.indexOf("audio") !== -1) { + ctor = Docs.Create.AudioDocument; + } + if (type.indexOf("pdf") !== -1) { + ctor = Docs.Create.PdfDocument; + if (!options._width) options._width = 400; + if (!options._height) options._height = (options._width as number) * 1200 / 927; + } + //TODO:al+glr + // if (type.indexOf("map") !== -1) { + // ctor = Docs.Create.MapDocument; + // if (!options._width) options._width = 800; + // if (!options._height) options._height = (options._width as number) * 3 / 4; + // } + if (type.indexOf("html") !== -1) { + if (path.includes(window.location.hostname)) { + const s = path.split('/'); + const id = s[s.length - 1]; + return DocServer.GetRefField(id).then(field => { + if (field instanceof Doc) { + const alias = Doc.MakeAlias(field); + alias.x = options.x || 0; + alias.y = options.y || 0; + alias._width = (options._width as number) || 300; + alias._height = (options._height as number) || (options._width as number) || 300; + return alias; + } + return undefined; }); - }); - } - } - }); - } - - export function DefaultFocus(doc: Doc, options?: DocFocusOptions) { - options?.afterFocus?.(false); - } - - export let ActiveRecordings: { props: FieldViewProps, getAnchor: () => Doc }[] = []; - - export function MakeLinkToActiveAudio(getSourceDoc: () => Doc, broadcastEvent = true) { - broadcastEvent && runInAction(() => DocumentManager.Instance.RecordingEvent = DocumentManager.Instance.RecordingEvent + 1); - return DocUtils.ActiveRecordings.map(audio => - DocUtils.MakeLink({ doc: getSourceDoc() }, { doc: audio.getAnchor() || audio.props.Document }, "recording annotation:linked recording", "recording timeline")); - } - - export function MakeLink(source: { doc: Doc }, target: { doc: Doc }, linkRelationship: string = "", description: string = "", id?: string, allowParCollectionLink?: boolean, showPopup?: number[]) { - if (!linkRelationship) linkRelationship = target.doc.type === DocumentType.RTF ? "Commentary:Comments On" : "link"; - const sv = DocumentManager.Instance.getDocumentView(source.doc); - if (!allowParCollectionLink && sv?.props.ContainingCollectionDoc === target.doc) return; - if (target.doc === Doc.UserDoc()) return undefined; - - const makeLink = action((linkDoc: Doc, showPopup?: number[]) => { - if (showPopup) { - LinkManager.currentLink = linkDoc; - - TaskCompletionBox.textDisplayed = "Link Created"; - TaskCompletionBox.popupX = showPopup[0]; - TaskCompletionBox.popupY = showPopup[1] - 33; - TaskCompletionBox.taskCompleted = true; - - LinkDescriptionPopup.popupX = showPopup[0]; - LinkDescriptionPopup.popupY = showPopup[1]; - LinkDescriptionPopup.descriptionPopup = true; - - const rect = document.body.getBoundingClientRect(); - if (LinkDescriptionPopup.popupX + 200 > rect.width) { - LinkDescriptionPopup.popupX -= 190; - TaskCompletionBox.popupX -= 40; - } - if (LinkDescriptionPopup.popupY + 100 > rect.height) { - LinkDescriptionPopup.popupY -= 40; - TaskCompletionBox.popupY -= 40; - } - - setTimeout(action(() => TaskCompletionBox.taskCompleted = false), 2500); - } - return linkDoc; - }); - - return makeLink(Docs.Create.LinkDocument(source, target, { - title: ComputedField.MakeFunction("generateLinkTitle(self)") as any, - "anchor1-useLinkSmallAnchor": source.doc.useLinkSmallAnchor ? true : undefined, - "anchor2-useLinkSmallAnchor": target.doc.useLinkSmallAnchor ? true : undefined, - "acl-Public": SharingPermissions.Augment, - "_acl-Public": SharingPermissions.Augment, - linkDisplay: true, - _hidden: true, - _linkAutoMove: true, - linkRelationship, - _showCaption: "description", - _showTitle: "linkRelationship", - description - }, id), showPopup); - } - - export function DocumentFromField(target: Doc, fieldKey: string, proto?: Doc, options?: DocumentOptions): Doc | undefined { - let created: Doc | undefined; - let layout: ((fieldKey: string) => string) | undefined; - const field = target[fieldKey]; - const resolved = options || {}; - if (field instanceof ImageField) { - created = Docs.Create.ImageDocument((field).url.href, resolved); - layout = ImageBox.LayoutString; - } else if (field instanceof Doc) { - created = field; - } else if (field instanceof VideoField) { - created = Docs.Create.VideoDocument((field).url.href, resolved); - layout = VideoBox.LayoutString; - } else if (field instanceof PdfField) { - created = Docs.Create.PdfDocument((field).url.href, resolved); - layout = PDFBox.LayoutString; - } else if (field instanceof AudioField) { - created = Docs.Create.AudioDocument((field).url.href, resolved); - layout = AudioBox.LayoutString; - } else if (field instanceof RecordingField) { - created = Docs.Create.RecordingDocument((field).url.href, resolved); - layout = RecordingBox.LayoutString; - } else if (field instanceof InkField) { - created = Docs.Create.InkDocument(ActiveInkColor(), CurrentUserUtils.SelectedTool, ActiveInkWidth(), ActiveInkBezierApprox(), ActiveFillColor(), ActiveArrowStart(), ActiveArrowEnd(), ActiveDash(), (field).inkData, resolved); - layout = InkingStroke.LayoutString; - } else if (field instanceof List && field[0] instanceof Doc) { - created = Docs.Create.StackingDocument(DocListCast(field), resolved); - layout = CollectionView.LayoutString; - } else if (field instanceof MapField) { - created = Docs.Create.MapDocument(DocListCast(field), resolved); - layout = MapBox.LayoutString; - } - else { - created = Docs.Create.TextDocument("", { ...{ _width: 200, _height: 25, _autoHeight: true }, ...resolved }); - layout = FormattedTextBox.LayoutString; - } - if (created) { - created.layout = layout?.(fieldKey); - created.title = fieldKey; - proto && created.proto && (created.proto = Doc.GetProto(proto)); - } - return created; - } - - export async function DocumentFromType(type: string, path: string, options: DocumentOptions): Promise> { - let ctor: ((path: string, options: DocumentOptions) => (Doc | Promise)) | undefined = undefined; - if (type.indexOf("image") !== -1) { - ctor = Docs.Create.ImageDocument; - if (!options._width) options._width = 300; - } - if (type.indexOf("video") !== -1) { - ctor = Docs.Create.VideoDocument; - if (!options._width) options._width = 600; - if (!options._height) options._height = (options._width as number) * 2 / 3; - } - if (type.indexOf("audio") !== -1) { - ctor = Docs.Create.AudioDocument; - } - if (type.indexOf("pdf") !== -1) { - ctor = Docs.Create.PdfDocument; - if (!options._width) options._width = 400; - if (!options._height) options._height = (options._width as number) * 1200 / 927; - } - //TODO:al+glr - // if (type.indexOf("map") !== -1) { - // ctor = Docs.Create.MapDocument; - // if (!options._width) options._width = 800; - // if (!options._height) options._height = (options._width as number) * 3 / 4; - // } - if (type.indexOf("html") !== -1) { - if (path.includes(window.location.hostname)) { - const s = path.split('/'); - const id = s[s.length - 1]; - return DocServer.GetRefField(id).then(field => { - if (field instanceof Doc) { - const alias = Doc.MakeAlias(field); - alias.x = options.x || 0; - alias.y = options.y || 0; - alias._width = (options._width as number) || 300; - alias._height = (options._height as number) || (options._width as number) || 300; - return alias; - } - return undefined; - }); - } - ctor = Docs.Create.WebDocument; - options = { ...options, _width: 400, _height: 512, title: path, }; - } - return ctor ? ctor(path, options) : undefined; - } - - export function addDocumentCreatorMenuItems(docTextAdder: (d: Doc) => void, docAdder: (d: Doc) => void, x: number, y: number, simpleMenu: boolean = false): void { - !simpleMenu && ContextMenu.Instance.addItem({ - description: "Quick Notes", - subitems: DocListCast((Doc.UserDoc()["template-notes"] as Doc).data).map((note, i) => ({ - description: ":" + StrCast(note.title), - event: undoBatch((args: { x: number, y: number }) => { - const textDoc = Docs.Create.TextDocument("", { - _width: 200, x, y, _autoHeight: note._autoHeight !== false, - title: StrCast(note.title) + "#" + (note.aliasCount = NumCast(note.aliasCount) + 1) - }); - textDoc.layoutKey = "layout_" + note.title; - textDoc[textDoc.layoutKey] = note; - docTextAdder(textDoc); - }), - icon: StrCast(note.icon) as IconProp - })) as ContextMenuProps[], - icon: "sticky-note" - }); - const math: ContextMenuProps = ({ - description: ":Math", event: () => { - const created = Docs.Create.EquationDocument(); - if (created) { - created.author = Doc.CurrentUserEmail; - created.x = x; - created.y = y; - created.width = 300; - created.height = 35; - EquationBox.SelectOnLoad = created[Id]; - docAdder?.(created); - } - }, icon: "calculator" - }); - const documentList: ContextMenuProps[] = DocListCast(Cast(Doc.UserDoc().myItemCreators, Doc, null)?.data).filter(btnDoc => !btnDoc.hidden).map(btnDoc => Cast(btnDoc?.dragFactory, Doc, null)).filter(doc => doc && doc !== Doc.UserDoc().emptyPresentation).map((dragDoc, i) => ({ - description: ":" + StrCast(dragDoc.title), - event: undoBatch((args: { x: number, y: number }) => { - const newDoc = Doc.copyDragFactory(dragDoc); - if (newDoc) { - newDoc.author = Doc.CurrentUserEmail; - newDoc.x = x; - newDoc.y = y; - if (newDoc.type === DocumentType.RTF) FormattedTextBox.SelectOnLoad = newDoc[Id]; - docAdder?.(newDoc); - } - }), - icon: Doc.toIcon(dragDoc), - })) as ContextMenuProps[]; - documentList.push(math); - ContextMenu.Instance.addItem({ - description: "Create document", - subitems: documentList, - icon: "file" - }); - }// applies a custom template to a document. the template is identified by it's short name (e.g, slideView not layout_slideView) - export function makeCustomViewClicked(doc: Doc, creator: Opt<(documents: Array, options: DocumentOptions, id?: string) => Doc>, templateSignature: string = "custom", docLayoutTemplate?: Doc) { - const batch = UndoManager.StartBatch("makeCustomViewClicked"); - runInAction(() => { - doc.layoutKey = "layout_" + templateSignature; - if (doc[doc.layoutKey] === undefined) { - createCustomView(doc, creator, templateSignature, docLayoutTemplate); - } - }); - batch.end(); - return doc; - } - export function findTemplate(templateName: string, type: string, signature: string) { - let docLayoutTemplate: Opt; - 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().clickFuncs, Doc, null)?.data); - const allTemplates = iconViews.concat(templBtns).concat(noteTypes).concat(clickFuncs).map(btnDoc => (btnDoc.dragFactory as Doc) || btnDoc).filter(doc => doc.isTemplateDoc); - // bcz: this is hacky -- want to have different templates be applied depending on the "type" of a document. but type is not reliable and there could be other types of template searches so this should be generalized - // first try to find a template that matches the specific document type (_). otherwise, fallback to a general match on - !docLayoutTemplate && allTemplates.forEach(tempDoc => StrCast(tempDoc.title) === templateName + "_" + type && (docLayoutTemplate = tempDoc)); - !docLayoutTemplate && allTemplates.forEach(tempDoc => StrCast(tempDoc.title) === templateName && (docLayoutTemplate = tempDoc)); - return docLayoutTemplate; - } - export function createCustomView(doc: Doc, creator: Opt<(documents: Array, options: DocumentOptions, id?: string) => Doc>, templateSignature: string = "custom", docLayoutTemplate?: Doc) { - const templateName = templateSignature.replace(/\(.*\)/, ""); - docLayoutTemplate = docLayoutTemplate || findTemplate(templateName, StrCast(doc.type), templateSignature); - - const customName = "layout_" + templateSignature; - const _width = NumCast(doc._width); - const _height = NumCast(doc._height); - const options = { title: "data", backgroundColor: StrCast(doc.backgroundColor), _autoHeight: true, _width, x: -_width / 2, y: - _height / 2, _showSidebar: false }; - - if (docLayoutTemplate) { - Doc.ApplyTemplateTo(docLayoutTemplate, doc, customName, undefined); - } else { - let fieldTemplate: Opt; - if (doc.data instanceof RichTextField || typeof (doc.data) === "string") { - fieldTemplate = Docs.Create.TextDocument("", options); - } else if (doc.data instanceof PdfField) { - fieldTemplate = Docs.Create.PdfDocument("http://www.msn.com", options); - } else if (doc.data instanceof VideoField) { - fieldTemplate = Docs.Create.VideoDocument("http://www.cs.brown.edu", options); - } else if (doc.data instanceof AudioField) { - fieldTemplate = Docs.Create.AudioDocument("http://www.cs.brown.edu", options); - } else if (doc.data instanceof ImageField) { - fieldTemplate = Docs.Create.ImageDocument("http://www.cs.brown.edu", options); - } - const docTemplate = creator?.(fieldTemplate ? [fieldTemplate] : [], { title: customName + "(" + doc.title + ")", isTemplateDoc: true, _width: _width + 20, _height: Math.max(100, _height + 45) }); - fieldTemplate && Doc.MakeMetadataFieldTemplate(fieldTemplate, docTemplate ? Doc.GetProto(docTemplate) : docTemplate); - docTemplate && Doc.ApplyTemplateTo(docTemplate, doc, customName, undefined); - } - } - export function makeCustomView(doc: Doc, custom: boolean, layout: string) { - Doc.setNativeView(doc); - if (custom) { - makeCustomViewClicked(doc, Docs.Create.StackingDocument, layout, undefined); - } - } - export function iconify(doc: Doc) { - const layoutKey = Cast(doc.layoutKey, "string", null); - DocUtils.makeCustomViewClicked(doc, Docs.Create.StackingDocument, "icon", undefined); - if (layoutKey && layoutKey !== "layout" && layoutKey !== "layout_icon") doc.deiconifyLayout = layoutKey.replace("layout_", ""); - } - - export function pileup(docList: Doc[], x?: number, y?: number, size: number = 55, create: boolean = true) { - let w = 0, h = 0; - runInAction(() => { - docList.forEach(d => { - DocUtils.iconify(d); - w = Math.max(NumCast(d._width), w); - h = Math.max(NumCast(d._height), h); + } + ctor = Docs.Create.WebDocument; + options = { ...options, _width: 400, _height: 512, title: path, }; + } + return ctor ? ctor(path, options) : undefined; + } + + export function addDocumentCreatorMenuItems(docTextAdder: (d: Doc) => void, docAdder: (d: Doc) => void, x: number, y: number, simpleMenu: boolean = false): void { + !simpleMenu && ContextMenu.Instance.addItem({ + description: "Quick Notes", + subitems: DocListCast((Doc.UserDoc()["template-notes"] as Doc).data).map((note, i) => ({ + description: ":" + StrCast(note.title), + event: undoBatch((args: { x: number, y: number }) => { + const textDoc = Docs.Create.TextDocument("", { + _width: 200, x, y, _autoHeight: note._autoHeight !== false, + title: StrCast(note.title) + "#" + (note.aliasCount = NumCast(note.aliasCount) + 1) + }); + textDoc.layoutKey = "layout_" + note.title; + textDoc[textDoc.layoutKey] = note; + docTextAdder(textDoc); + }), + icon: StrCast(note.icon) as IconProp + })) as ContextMenuProps[], + icon: "sticky-note" }); - docList.forEach((d, i) => { - d.x = Math.cos(Math.PI * 2 * i / docList.length) * size; - d.y = Math.sin(Math.PI * 2 * i / docList.length) * size; - d._timecodeToShow = undefined; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection + const math: ContextMenuProps = ({ + description: ":Math", event: () => { + const created = Docs.Create.EquationDocument(); + if (created) { + created.author = Doc.CurrentUserEmail; + created.x = x; + created.y = y; + created.width = 300; + created.height = 35; + EquationBox.SelectOnLoad = created[Id]; + docAdder?.(created); + } + }, icon: "calculator" }); - const aggBounds = aggregateBounds(docList.map(d => ({ x: NumCast(d.x), y: NumCast(d.y), width: NumCast(d._width), height: NumCast(d._height) })), 0, 0); - docList.forEach((d, i) => { - d.x = NumCast(d.x) - ((aggBounds.r + aggBounds.x) / 2); - d.y = NumCast(d.y) - ((aggBounds.b + aggBounds.y) / 2); - d._timecodeToShow = undefined; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection + const documentList: ContextMenuProps[] = DocListCast(Cast(Doc.UserDoc().myItemCreators, Doc, null)?.data).filter(btnDoc => !btnDoc.hidden).map(btnDoc => Cast(btnDoc?.dragFactory, Doc, null)).filter(doc => doc && doc !== Doc.UserDoc().emptyPresentation).map((dragDoc, i) => ({ + description: ":" + StrCast(dragDoc.title), + event: undoBatch((args: { x: number, y: number }) => { + const newDoc = Doc.copyDragFactory(dragDoc); + if (newDoc) { + newDoc.author = Doc.CurrentUserEmail; + newDoc.x = x; + newDoc.y = y; + if (newDoc.type === DocumentType.RTF) FormattedTextBox.SelectOnLoad = newDoc[Id]; + docAdder?.(newDoc); + } + }), + icon: Doc.toIcon(dragDoc), + })) as ContextMenuProps[]; + documentList.push(math); + ContextMenu.Instance.addItem({ + description: "Create document", + subitems: documentList, + icon: "file" }); - }); - if (create) { - const newCollection = Docs.Create.PileDocument(docList, { title: "pileup", x: (x || 0) - size, y: (y || 0) - size, _width: size * 2, _height: size * 2, }); - 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; - newCollection._jitterRotation = 10; - return newCollection; - } - } - - export function LeavePushpin(doc: Doc, annotationField: string) { - if (doc.isPushpin) return undefined; - const context = Cast(doc.context, Doc, null) ?? Cast(doc.annotationOn, Doc, null); - const hasContextAnchor = DocListCast(doc.links). - some(l => - (l.anchor2 === doc && Cast(l.anchor1, Doc, null)?.annotationOn === context) || - (l.anchor1 === doc && Cast(l.anchor2, Doc, null)?.annotationOn === context)); - if (context && !hasContextAnchor && (context.type === DocumentType.VID || context.type === DocumentType.WEB || context.type === DocumentType.PDF || context.type === DocumentType.IMG)) { - const pushpin = Docs.Create.FontIconDocument({ - title: "pushpin", label: "", annotationOn: Cast(doc.annotationOn, Doc, null), isPushpin: true, - icon: "map-pin", x: Cast(doc.x, "number", null), y: Cast(doc.y, "number", null), backgroundColor: "#ACCEF7", - _width: 15, _height: 15, _xPadding: 0, _isLinkButton: true, _timecodeToShow: Cast(doc._timecodeToShow, "number", null) + }// applies a custom template to a document. the template is identified by it's short name (e.g, slideView not layout_slideView) + export function makeCustomViewClicked(doc: Doc, creator: Opt<(documents: Array, options: DocumentOptions, id?: string) => Doc>, templateSignature: string = "custom", docLayoutTemplate?: Doc) { + const batch = UndoManager.StartBatch("makeCustomViewClicked"); + runInAction(() => { + doc.layoutKey = "layout_" + templateSignature; + if (doc[doc.layoutKey] === undefined) { + createCustomView(doc, creator, templateSignature, docLayoutTemplate); + } }); - Doc.AddDocToList(context, annotationField, pushpin); - const pushpinLink = DocUtils.MakeLink({ doc: pushpin }, { doc: doc }, "pushpin", ""); - doc._timecodeToShow = undefined; - return pushpin; - } - return undefined; - } - - // /** - // * - // * @param dms Degree Minute Second format exif gps data - // * @param ref ref that determines negativity of decimal coordinates - // * @returns a decimal format of gps latitude / longitude - // */ - // function getDecimalfromDMS(dms?: number[], ref?: string) { - // if (dms && ref) { - // let degrees = dms[0] / dms[1]; - // let minutes = dms[2] / dms[3] / 60.0; - // let seconds = dms[4] / dms[5] / 3600.0; - - // if (['S', 'W'].includes(ref)) { - // degrees = -degrees; minutes = -minutes; seconds = -seconds - // } - // return (degrees + minutes + seconds).toFixed(5); - // } - // } - - function ConvertDMSToDD(degrees: number, minutes: number, seconds: number, direction: string) { - var dd = degrees + minutes / 60 + seconds / (60 * 60); - if (direction === "S" || direction === "W") { - dd = dd * -1; - } // Don't do anything for N or E - return dd; - } - - async function processFileupload(generatedDocuments: Doc[], name: string, type: string, result: Error | Upload.FileInformation, options: DocumentOptions) { - if (result instanceof Error) { - alert(`Upload failed: ${result.message}`); - return; - } - const full = { ...options, _width: 400, title: name }; - const pathname = Utils.prepend(result.accessPaths.agnostic.client); - const doc = await DocUtils.DocumentFromType(type, pathname, full); - if (doc) { - const proto = Doc.GetProto(doc); - proto.text = result.rawText; - proto.fileUpload = pathname.replace(/.*\//, "").replace("upload_", "").replace(/\.[a-z0-9]*$/, ""); - if (Upload.isImageInformation(result)) { - const maxNativeDim = Math.min(Math.max(result.nativeHeight, result.nativeWidth), defaultNativeImageDim); - proto["data-nativeOrientation"] = result.exifData?.data?.image?.Orientation ?? ((StrCast((result.exifData?.data as any)?.Orientation).includes("Rotate 90")) ? 5 : undefined); - proto["data-nativeWidth"] = (result.nativeWidth < result.nativeHeight) ? maxNativeDim * result.nativeWidth / result.nativeHeight : maxNativeDim; - proto["data-nativeHeight"] = (result.nativeWidth < result.nativeHeight) ? maxNativeDim : maxNativeDim / (result.nativeWidth / result.nativeHeight); - if (NumCast(proto["data-nativeOrientation"]) >= 5) { - proto["data-nativeHeight"] = (result.nativeWidth < result.nativeHeight) ? maxNativeDim * result.nativeWidth / result.nativeHeight : maxNativeDim; - proto["data-nativeWidth"] = (result.nativeWidth < result.nativeHeight) ? maxNativeDim : maxNativeDim / (result.nativeWidth / result.nativeHeight); - } - proto.contentSize = result.contentSize; - // exif gps data coordinates are stored in DMS (Degrees Minutes Seconds), the following operation converts that to decimal coordinates - const latitude = result.exifData?.data?.GPSLatitude; - const latitudeDirection = result.exifData?.data?.GPSLatitudeRef; - const longitude = result.exifData?.data?.GPSLongitude; - const longitudeDirection = result.exifData?.data?.GPSLongitudeRef; - if (latitude !== undefined && longitude !== undefined && latitudeDirection !== undefined && longitudeDirection !== undefined) { - proto.lat = ConvertDMSToDD(latitude[0], latitude[1], latitude[2], latitudeDirection); - proto.lng = ConvertDMSToDD(longitude[0], longitude[1], longitude[2], longitudeDirection); - } - - } - generatedDocuments.push(doc); - } - } - - export async function uploadYoutubeVideo(videoId: string, options: DocumentOptions) { - const generatedDocuments: Doc[] = []; - for (const { source: { name, type }, result } of await Networking.UploadYoutubeToServer(videoId)) { - name && type && processFileupload(generatedDocuments, name, type, result, options); - } - return generatedDocuments; - } - export async function uploadFilesToDocs(files: File[], options: DocumentOptions) { - const generatedDocuments: Doc[] = []; - const upfiles = await Networking.UploadFilesToServer(files); - for (const { source: { name, type }, result } of upfiles) { - name && type && processFileupload(generatedDocuments, name, type, result, options); - } - return generatedDocuments; - } + batch.end(); + return doc; + } + export function findTemplate(templateName: string, type: string, signature: string) { + let docLayoutTemplate: Opt; + 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().clickFuncs, Doc, null)?.data); + const allTemplates = iconViews.concat(templBtns).concat(noteTypes).concat(clickFuncs).map(btnDoc => (btnDoc.dragFactory as Doc) || btnDoc).filter(doc => doc.isTemplateDoc); + // bcz: this is hacky -- want to have different templates be applied depending on the "type" of a document. but type is not reliable and there could be other types of template searches so this should be generalized + // first try to find a template that matches the specific document type (_). otherwise, fallback to a general match on + !docLayoutTemplate && allTemplates.forEach(tempDoc => StrCast(tempDoc.title) === templateName + "_" + type && (docLayoutTemplate = tempDoc)); + !docLayoutTemplate && allTemplates.forEach(tempDoc => StrCast(tempDoc.title) === templateName && (docLayoutTemplate = tempDoc)); + return docLayoutTemplate; + } + export function createCustomView(doc: Doc, creator: Opt<(documents: Array, options: DocumentOptions, id?: string) => Doc>, templateSignature: string = "custom", docLayoutTemplate?: Doc) { + const templateName = templateSignature.replace(/\(.*\)/, ""); + docLayoutTemplate = docLayoutTemplate || findTemplate(templateName, StrCast(doc.type), templateSignature); + + const customName = "layout_" + templateSignature; + const _width = NumCast(doc._width); + const _height = NumCast(doc._height); + const options = { title: "data", backgroundColor: StrCast(doc.backgroundColor), _autoHeight: true, _width, x: -_width / 2, y: - _height / 2, _showSidebar: false }; + + if (docLayoutTemplate) { + Doc.ApplyTemplateTo(docLayoutTemplate, doc, customName, undefined); + } else { + let fieldTemplate: Opt; + if (doc.data instanceof RichTextField || typeof (doc.data) === "string") { + fieldTemplate = Docs.Create.TextDocument("", options); + } else if (doc.data instanceof PdfField) { + fieldTemplate = Docs.Create.PdfDocument("http://www.msn.com", options); + } else if (doc.data instanceof VideoField) { + fieldTemplate = Docs.Create.VideoDocument("http://www.cs.brown.edu", options); + } else if (doc.data instanceof AudioField) { + fieldTemplate = Docs.Create.AudioDocument("http://www.cs.brown.edu", options); + } else if (doc.data instanceof ImageField) { + fieldTemplate = Docs.Create.ImageDocument("http://www.cs.brown.edu", options); + } + const docTemplate = creator?.(fieldTemplate ? [fieldTemplate] : [], { title: customName + "(" + doc.title + ")", isTemplateDoc: true, _width: _width + 20, _height: Math.max(100, _height + 45) }); + fieldTemplate && Doc.MakeMetadataFieldTemplate(fieldTemplate, docTemplate ? Doc.GetProto(docTemplate) : docTemplate); + docTemplate && Doc.ApplyTemplateTo(docTemplate, doc, customName, undefined); + } + } + export function makeCustomView(doc: Doc, custom: boolean, layout: string) { + Doc.setNativeView(doc); + if (custom) { + makeCustomViewClicked(doc, Docs.Create.StackingDocument, layout, undefined); + } + } + export function iconify(doc: Doc) { + const layoutKey = Cast(doc.layoutKey, "string", null); + DocUtils.makeCustomViewClicked(doc, Docs.Create.StackingDocument, "icon", undefined); + if (layoutKey && layoutKey !== "layout" && layoutKey !== "layout_icon") doc.deiconifyLayout = layoutKey.replace("layout_", ""); + } + + export function pileup(docList: Doc[], x?: number, y?: number, size: number = 55, create: boolean = true) { + let w = 0, h = 0; + runInAction(() => { + docList.forEach(d => { + DocUtils.iconify(d); + w = Math.max(NumCast(d._width), w); + h = Math.max(NumCast(d._height), h); + }); + docList.forEach((d, i) => { + d.x = Math.cos(Math.PI * 2 * i / docList.length) * size; + d.y = Math.sin(Math.PI * 2 * i / docList.length) * size; + d._timecodeToShow = undefined; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection + }); + const aggBounds = aggregateBounds(docList.map(d => ({ x: NumCast(d.x), y: NumCast(d.y), width: NumCast(d._width), height: NumCast(d._height) })), 0, 0); + docList.forEach((d, i) => { + d.x = NumCast(d.x) - ((aggBounds.r + aggBounds.x) / 2); + d.y = NumCast(d.y) - ((aggBounds.b + aggBounds.y) / 2); + d._timecodeToShow = undefined; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection + }); + }); + if (create) { + const newCollection = Docs.Create.PileDocument(docList, { title: "pileup", x: (x || 0) - size, y: (y || 0) - size, _width: size * 2, _height: size * 2, }); + 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; + newCollection._jitterRotation = 10; + return newCollection; + } + } + + export function LeavePushpin(doc: Doc, annotationField: string) { + if (doc.isPushpin) return undefined; + const context = Cast(doc.context, Doc, null) ?? Cast(doc.annotationOn, Doc, null); + const hasContextAnchor = DocListCast(doc.links). + some(l => + (l.anchor2 === doc && Cast(l.anchor1, Doc, null)?.annotationOn === context) || + (l.anchor1 === doc && Cast(l.anchor2, Doc, null)?.annotationOn === context)); + if (context && !hasContextAnchor && (context.type === DocumentType.VID || context.type === DocumentType.WEB || context.type === DocumentType.PDF || context.type === DocumentType.IMG)) { + const pushpin = Docs.Create.FontIconDocument({ + title: "pushpin", label: "", annotationOn: Cast(doc.annotationOn, Doc, null), isPushpin: true, + icon: "map-pin", x: Cast(doc.x, "number", null), y: Cast(doc.y, "number", null), backgroundColor: "#ACCEF7", + _width: 15, _height: 15, _xPadding: 0, _isLinkButton: true, _timecodeToShow: Cast(doc._timecodeToShow, "number", null) + }); + Doc.AddDocToList(context, annotationField, pushpin); + const pushpinLink = DocUtils.MakeLink({ doc: pushpin }, { doc: doc }, "pushpin", ""); + doc._timecodeToShow = undefined; + return pushpin; + } + return undefined; + } + + // /** + // * + // * @param dms Degree Minute Second format exif gps data + // * @param ref ref that determines negativity of decimal coordinates + // * @returns a decimal format of gps latitude / longitude + // */ + // function getDecimalfromDMS(dms?: number[], ref?: string) { + // if (dms && ref) { + // let degrees = dms[0] / dms[1]; + // let minutes = dms[2] / dms[3] / 60.0; + // let seconds = dms[4] / dms[5] / 3600.0; + + // if (['S', 'W'].includes(ref)) { + // degrees = -degrees; minutes = -minutes; seconds = -seconds + // } + // return (degrees + minutes + seconds).toFixed(5); + // } + // } + + function ConvertDMSToDD(degrees: number, minutes: number, seconds: number, direction: string) { + var dd = degrees + minutes / 60 + seconds / (60 * 60); + if (direction === "S" || direction === "W") { + dd = dd * -1; + } // Don't do anything for N or E + return dd; + } + + async function processFileupload(generatedDocuments: Doc[], name: string, type: string, result: Error | Upload.FileInformation, options: DocumentOptions) { + if (result instanceof Error) { + alert(`Upload failed: ${result.message}`); + return; + } + const full = { ...options, _width: 400, title: name }; + const pathname = Utils.prepend(result.accessPaths.agnostic.client); + const doc = await DocUtils.DocumentFromType(type, pathname, full); + if (doc) { + const proto = Doc.GetProto(doc); + proto.text = result.rawText; + proto.fileUpload = pathname.replace(/.*\//, "").replace("upload_", "").replace(/\.[a-z0-9]*$/, ""); + if (Upload.isImageInformation(result)) { + const maxNativeDim = Math.min(Math.max(result.nativeHeight, result.nativeWidth), defaultNativeImageDim); + proto["data-nativeOrientation"] = result.exifData?.data?.image?.Orientation ?? ((StrCast((result.exifData?.data as any)?.Orientation).includes("Rotate 90")) ? 5 : undefined); + proto["data-nativeWidth"] = (result.nativeWidth < result.nativeHeight) ? maxNativeDim * result.nativeWidth / result.nativeHeight : maxNativeDim; + proto["data-nativeHeight"] = (result.nativeWidth < result.nativeHeight) ? maxNativeDim : maxNativeDim / (result.nativeWidth / result.nativeHeight); + if (NumCast(proto["data-nativeOrientation"]) >= 5) { + proto["data-nativeHeight"] = (result.nativeWidth < result.nativeHeight) ? maxNativeDim * result.nativeWidth / result.nativeHeight : maxNativeDim; + proto["data-nativeWidth"] = (result.nativeWidth < result.nativeHeight) ? maxNativeDim : maxNativeDim / (result.nativeWidth / result.nativeHeight); + } + proto.contentSize = result.contentSize; + // exif gps data coordinates are stored in DMS (Degrees Minutes Seconds), the following operation converts that to decimal coordinates + const latitude = result.exifData?.data?.GPSLatitude; + const latitudeDirection = result.exifData?.data?.GPSLatitudeRef; + const longitude = result.exifData?.data?.GPSLongitude; + const longitudeDirection = result.exifData?.data?.GPSLongitudeRef; + if (latitude !== undefined && longitude !== undefined && latitudeDirection !== undefined && longitudeDirection !== undefined) { + proto.lat = ConvertDMSToDD(latitude[0], latitude[1], latitude[2], latitudeDirection); + proto.lng = ConvertDMSToDD(longitude[0], longitude[1], longitude[2], longitudeDirection); + } + + } + generatedDocuments.push(doc); + } + } + + export async function uploadYoutubeVideo(videoId: string, options: DocumentOptions) { + const generatedDocuments: Doc[] = []; + for (const { source: { name, type }, result } of await Networking.UploadYoutubeToServer(videoId)) { + name && type && processFileupload(generatedDocuments, name, type, result, options); + } + return generatedDocuments; + } + export async function uploadFilesToDocs(files: File[], options: DocumentOptions) { + const generatedDocuments: Doc[] = []; + const upfiles = await Networking.UploadFilesToServer(files); + for (const { source: { name, type }, result } of upfiles) { + name && type && processFileupload(generatedDocuments, name, type, result, options); + } + return generatedDocuments; + } } ScriptingGlobals.add("Docs", Docs); ScriptingGlobals.add(function makeDelegate(proto: any) { const d = Docs.Create.DelegateDocument(proto, { title: "child of " + proto.title }); return d; }); ScriptingGlobals.add(function generateLinkTitle(self: Doc) { - const anchor1title = self.anchor1 && self.anchor1 !== self ? Cast(self.anchor1, Doc, null).title : ""; - const anchor2title = self.anchor2 && self.anchor2 !== self ? Cast(self.anchor2, Doc, null).title : ""; - const relation = self.linkRelationship || "to"; - return `${anchor1title} (${relation}) ${anchor2title}`; + const anchor1title = self.anchor1 && self.anchor1 !== self ? Cast(self.anchor1, Doc, null).title : ""; + const anchor2title = self.anchor2 && self.anchor2 !== self ? Cast(self.anchor2, Doc, null).title : ""; + const relation = self.linkRelationship || "to"; + return `${anchor1title} (${relation}) ${anchor2title}`; }); ScriptingGlobals.add(function openTabAlias(tab: Doc) { - CollectionDockingView.AddSplit(Doc.MakeAlias(tab), "right"); + CollectionDockingView.AddSplit(Doc.MakeAlias(tab), "right"); }); \ No newline at end of file diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 29e088143..bc4fec3f0 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -33,548 +33,552 @@ import React = require("react"); @observer export class DocumentDecorations extends React.Component<{ PanelWidth: number, PanelHeight: number, boundsLeft: number, boundsTop: number }, { value: string }> { - static Instance: DocumentDecorations; - private _resizeHdlId = ""; - private _keyinput = React.createRef(); - private _resizeBorderWidth = 16; - private _linkBoxHeight = 20 + 3; // link button height + margin - private _titleHeight = 20; - private _resizeUndo?: UndoManager.Batch; - private _offX = 0; _offY = 0; // offset from click pt to inner edge of resize border - private _snapX = 0; _snapY = 0; // last snapped location of resize border - private _dragHeights = new Map(); - private _inkDragDocs: { doc: Doc, x: number, y: number, width: number, height: number }[] = []; - - @observable private _accumulatedTitle = ""; - @observable private _titleControlString: string = "#title"; - @observable private _edtingTitle = false; - @observable private _hidden = false; - @observable public AddToSelection = false; // if Shift is pressed, then this should be set so that clicking on the selection background is ignored so overlapped documents can be added to the selection set. - @observable public Interacting = false; - @observable public pushIcon: IconProp = "arrow-alt-circle-up"; - @observable public pullIcon: IconProp = "arrow-alt-circle-down"; - @observable public pullColor: string = "white"; - - constructor(props: any) { - super(props); - DocumentDecorations.Instance = this; - reaction(() => SelectionManager.Views().slice(), action(docs => this._edtingTitle = false)); - } - - @computed - get Bounds() { - const views = SelectionManager.Views(); - return views.filter(dv => dv.props.renderDepth > 0).map(dv => dv.getBounds()).reduce((bounds, rect) => - !rect ? bounds : - { - x: Math.min(rect.left, bounds.x), - y: Math.min(rect.top, bounds.y), - r: Math.max(rect.right, bounds.r), - b: Math.max(rect.bottom, bounds.b), - c: views.length === 1 ? rect.center : undefined - }, - { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: Number.MIN_VALUE, b: Number.MIN_VALUE, c: undefined as ({ X: number, Y: number } | undefined) }); - } - - @action - titleBlur = () => { - this._edtingTitle = false; - if (this._accumulatedTitle.startsWith("#") || this._accumulatedTitle.startsWith("=")) { - this._titleControlString = this._accumulatedTitle; - } else if (this._titleControlString.startsWith("#")) { - const titleFieldKey = this._titleControlString.substring(1); - UndoManager.RunInBatch(() => titleFieldKey && SelectionManager.Views().forEach(d => { - if (titleFieldKey === "title") { - d.dataDoc["title-custom"] = !this._accumulatedTitle.startsWith("-"); - if (StrCast(d.rootDoc.title).startsWith("@") && !this._accumulatedTitle.startsWith("@")) { - Doc.RemoveDocFromList(Doc.UserDoc(), "myPublishedDocs", d.rootDoc); - } - if (!StrCast(d.rootDoc.title).startsWith("@") && this._accumulatedTitle.startsWith("@")) { - Doc.AddDocToList(Doc.UserDoc(), "myPublishedDocs", d.rootDoc); - } - } - //@ts-ignore - const titleField = (+this._accumulatedTitle === this._accumulatedTitle ? +this._accumulatedTitle : this._accumulatedTitle); - Doc.SetInPlace(d.rootDoc, titleFieldKey, titleField, true); - - if (d.rootDoc.syncLayoutFieldWithTitle) { - const title = titleField.toString(); - const curKey = Doc.LayoutFieldKey(d.rootDoc); - if (curKey !== title && d.dataDoc[title] === undefined) { - d.rootDoc.layout = FormattedTextBox.LayoutString(title); - setTimeout(() => { - const val = d.dataDoc[curKey]; - d.dataDoc[curKey] = undefined; - d.dataDoc[title] = val; - }); - } - } - }), "title blur"); - } - } - - titleEntered = (e: React.KeyboardEvent) => { - if (e.key === "Enter") { - e.stopPropagation(); - (e.target as any).blur(); - } - } - - @action onTitleDown = (e: React.PointerEvent): void => { - setupMoveUpEvents(this, e, e => this.onBackgroundMove(true, e), (e) => { }, action((e) => { - !this._edtingTitle && (this._accumulatedTitle = this._titleControlString.startsWith("#") ? this.selectionTitle : this._titleControlString); - this._edtingTitle = true; - this._keyinput.current && setTimeout(this._keyinput.current.focus); - })); - } - - onBackgroundDown = (e: React.PointerEvent) => setupMoveUpEvents(this, e, e => this.onBackgroundMove(false, e), emptyFunction, emptyFunction); - - @action - onBackgroundMove = (dragTitle: boolean, e: PointerEvent): boolean => { - const dragDocView = SelectionManager.Views()[0]; - const { left, top } = dragDocView.getBounds() || { left: 0, top: 0 }; - const dragData = new DragManager.DocumentDragData(SelectionManager.Views().map(dv => dv.props.Document), dragDocView.props.dropAction); - dragData.offset = dragDocView.props.ScreenToLocalTransform().transformDirection(e.x - left, e.y - top); - dragData.moveDocument = dragDocView.props.moveDocument; - dragData.isDocDecorationMove = true; - dragData.canEmbed = dragTitle; - this._hidden = this.Interacting = true; - DragManager.StartDocumentDrag(SelectionManager.Views().map(dv => dv.ContentDiv!), dragData, e.x, e.y, { - dragComplete: action(e => { - dragData.canEmbed && SelectionManager.DeselectAll(); - this._hidden = this.Interacting = false; - }), - hideSource: true - }); - return true; - } - - _deleteAfterIconify = false; - _iconifyBatch: UndoManager.Batch | undefined; - onCloseClick = (forceDeleteOrIconify: boolean | undefined) => { - if (this.canDelete) { - const views = SelectionManager.Views().slice().filter(v => v); - if (forceDeleteOrIconify === false && this._iconifyBatch) return; - this._deleteAfterIconify = forceDeleteOrIconify || this._iconifyBatch ? true : false; - if (!this._iconifyBatch) { - this._iconifyBatch = UndoManager.StartBatch("iconifying"); - } else { - forceDeleteOrIconify = false; // can't force immediate close in the middle of iconifying -- have to wait until iconifying completes + static Instance: DocumentDecorations; + private _resizeHdlId = ""; + private _keyinput = React.createRef(); + private _resizeBorderWidth = 16; + private _linkBoxHeight = 20 + 3; // link button height + margin + private _titleHeight = 20; + private _resizeUndo?: UndoManager.Batch; + private _offX = 0; _offY = 0; // offset from click pt to inner edge of resize border + private _snapX = 0; _snapY = 0; // last snapped location of resize border + private _dragHeights = new Map(); + private _inkDragDocs: { doc: Doc, x: number, y: number, width: number, height: number }[] = []; + + @observable private _accumulatedTitle = ""; + @observable private _titleControlString: string = "#title"; + @observable private _edtingTitle = false; + @observable private _hidden = false; + @observable public AddToSelection = false; // if Shift is pressed, then this should be set so that clicking on the selection background is ignored so overlapped documents can be added to the selection set. + @observable public Interacting = false; + @observable public pushIcon: IconProp = "arrow-alt-circle-up"; + @observable public pullIcon: IconProp = "arrow-alt-circle-down"; + @observable public pullColor: string = "white"; + + constructor(props: any) { + super(props); + DocumentDecorations.Instance = this; + reaction(() => SelectionManager.Views().slice(), action(docs => this._edtingTitle = false)); + } + + @computed + get Bounds() { + const views = SelectionManager.Views(); + return views.filter(dv => dv.props.renderDepth > 0).map(dv => dv.getBounds()).reduce((bounds, rect) => + !rect ? bounds : + { + x: Math.min(rect.left, bounds.x), + y: Math.min(rect.top, bounds.y), + r: Math.max(rect.right, bounds.r), + b: Math.max(rect.bottom, bounds.b), + c: views.length === 1 ? rect.center : undefined + }, + { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: Number.MIN_VALUE, b: Number.MIN_VALUE, c: undefined as ({ X: number, Y: number } | undefined) }); + } + + @action + titleBlur = () => { + this._edtingTitle = false; + if (this._accumulatedTitle.startsWith("#") || this._accumulatedTitle.startsWith("=")) { + this._titleControlString = this._accumulatedTitle; + } else if (this._titleControlString.startsWith("#")) { + const titleFieldKey = this._titleControlString.substring(1); + UndoManager.RunInBatch(() => titleFieldKey && SelectionManager.Views().forEach(d => { + if (titleFieldKey === "title") { + d.dataDoc["title-custom"] = !this._accumulatedTitle.startsWith("-"); + if (StrCast(d.rootDoc.title).startsWith("@") && !this._accumulatedTitle.startsWith("@")) { + Doc.RemoveDocFromList(Doc.UserDoc(), "myPublishedDocs", d.rootDoc); + } + if (!StrCast(d.rootDoc.title).startsWith("@") && this._accumulatedTitle.startsWith("@")) { + Doc.AddDocToList(Doc.UserDoc(), "myPublishedDocs", d.rootDoc); + } + } + //@ts-ignore + const titleField = (+this._accumulatedTitle === this._accumulatedTitle ? +this._accumulatedTitle : this._accumulatedTitle); + Doc.SetInPlace(d.rootDoc, titleFieldKey, titleField, true); + + if (d.rootDoc.syncLayoutFieldWithTitle) { + const title = titleField.toString(); + const curKey = Doc.LayoutFieldKey(d.rootDoc); + if (curKey !== title && d.dataDoc[title] === undefined) { + d.rootDoc.layout = FormattedTextBox.LayoutString(title); + setTimeout(() => { + const val = d.dataDoc[curKey]; + d.dataDoc[curKey] = undefined; + d.dataDoc[title] = val; + }); + } + } + }), "title blur"); } - var iconifyingCount = views.length; - const finished = action((force?: boolean) => { - if ((force || --iconifyingCount === 0) && this._iconifyBatch) { - if (this._deleteAfterIconify) { - views.forEach(iconView => iconView.props.removeDocument?.(iconView.props.Document)); - SelectionManager.DeselectAll(); - } - this._iconifyBatch?.end(); - this._iconifyBatch = undefined; - } + } + + titleEntered = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + e.stopPropagation(); + (e.target as any).blur(); + } + } + + @action onTitleDown = (e: React.PointerEvent): void => { + setupMoveUpEvents(this, e, e => this.onBackgroundMove(true, e), (e) => { }, action((e) => { + !this._edtingTitle && (this._accumulatedTitle = this._titleControlString.startsWith("#") ? this.selectionTitle : this._titleControlString); + this._edtingTitle = true; + this._keyinput.current && setTimeout(this._keyinput.current.focus); + })); + } + + onBackgroundDown = (e: React.PointerEvent) => setupMoveUpEvents(this, e, e => this.onBackgroundMove(false, e), emptyFunction, emptyFunction); + + @action + onBackgroundMove = (dragTitle: boolean, e: PointerEvent): boolean => { + const dragDocView = SelectionManager.Views()[0]; + const { left, top } = dragDocView.getBounds() || { left: 0, top: 0 }; + const dragData = new DragManager.DocumentDragData(SelectionManager.Views().map(dv => dv.props.Document), dragDocView.props.dropAction); + dragData.offset = dragDocView.props.ScreenToLocalTransform().transformDirection(e.x - left, e.y - top); + dragData.moveDocument = dragDocView.props.moveDocument; + dragData.isDocDecorationMove = true; + dragData.canEmbed = dragTitle; + this._hidden = this.Interacting = true; + DragManager.StartDocumentDrag(SelectionManager.Views().map(dv => dv.ContentDiv!), dragData, e.x, e.y, { + dragComplete: action(e => { + dragData.canEmbed && SelectionManager.DeselectAll(); + this._hidden = this.Interacting = false; + }), + hideSource: true }); - if (forceDeleteOrIconify) finished(forceDeleteOrIconify); - else if (!this._deleteAfterIconify) views.forEach(dv => dv.iconify(finished)); - } - } - onMaximizeDown = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, () => { - DragManager.StartWindowDrag?.(e, [SelectionManager.Views().slice(-1)[0].rootDoc]); return true; - }, emptyFunction, this.onMaximizeClick, false, false); - } - - onMaximizeClick = (e: any): void => { - const selectedDocs = SelectionManager.Views(); - if (selectedDocs.length) { - if (e.ctrlKey) { // open an alias in a new tab with Ctrl Key - const bestAlias = DocListCast(selectedDocs[0].props.Document.aliases).find(doc => !doc.context && doc.author === Doc.CurrentUserEmail); - CollectionDockingView.AddSplit(bestAlias ?? Doc.MakeAlias(selectedDocs[0].props.Document), "right"); - } else if (e.shiftKey) { // open centered in a new workspace with Shift Key - const alias = Doc.MakeAlias(selectedDocs[0].props.Document); - alias.context = undefined; - alias.x = -alias[WidthSym]() / 2; - alias.y = -alias[HeightSym]() / 2; - CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([alias], { title: "Tab for " + alias.title }), "right"); - } else if (e.altKey) { // open same document in new tab - CollectionDockingView.ToggleSplit(selectedDocs[0].props.Document, "right"); - } else { - var openDoc = selectedDocs[0].props.Document; - if (openDoc.layoutKey === "layout_icon") { - openDoc = DocListCast(openDoc.aliases).find(alias => !alias.context) ?? Doc.MakeAlias(openDoc); - Doc.deiconifyView(openDoc); - } - LightboxView.SetLightboxDoc(openDoc, undefined, selectedDocs.slice(1).map(view => view.props.Document)); + } + + _deleteAfterIconify = false; + _iconifyBatch: UndoManager.Batch | undefined; + onCloseClick = (forceDeleteOrIconify: boolean | undefined) => { + if (this.canDelete) { + const views = SelectionManager.Views().slice().filter(v => v); + if (forceDeleteOrIconify === false && this._iconifyBatch) return; + this._deleteAfterIconify = forceDeleteOrIconify || this._iconifyBatch ? true : false; + if (!this._iconifyBatch) { + this._iconifyBatch = UndoManager.StartBatch("iconifying"); + } else { + forceDeleteOrIconify = false; // can't force immediate close in the middle of iconifying -- have to wait until iconifying completes + } + var iconifyingCount = views.length; + const finished = action((force?: boolean) => { + if ((force || --iconifyingCount === 0) && this._iconifyBatch) { + if (this._deleteAfterIconify) { + views.forEach(iconView => iconView.props.removeDocument?.(iconView.props.Document)); + SelectionManager.DeselectAll(); + } + this._iconifyBatch?.end(); + this._iconifyBatch = undefined; + } + }); + if (forceDeleteOrIconify) finished(forceDeleteOrIconify); + else if (!this._deleteAfterIconify) views.forEach(dv => dv.iconify(finished)); } - } - SelectionManager.DeselectAll(); - } - - onIconifyClick = (): void => { - SelectionManager.Views().forEach(dv => dv?.iconify()); - SelectionManager.DeselectAll(); - } - - onSelectorClick = () => SelectionManager.Views()?.[0]?.props.ContainingCollectionView?.props.select(false); - - onRadiusDown = (e: React.PointerEvent): void => { - this._resizeUndo = UndoManager.StartBatch("DocDecs set radius"); - setupMoveUpEvents(this, e, (e, down) => { - const dist = Math.sqrt((e.clientX - down[0]) * (e.clientX - down[0]) + (e.clientY - down[1]) * (e.clientY - down[1])); - SelectionManager.Views().map(dv => dv.props.Document).map(doc => doc.layout instanceof Doc ? doc.layout : doc.isTemplateForField ? doc : Doc.GetProto(doc)). - map(d => d.borderRounding = `${Math.max(0, dist < 3 ? 0 : dist)}px`); - return false; - }, (e) => this._resizeUndo?.end(), (e) => { }); - } - - @action - onRotateDown = (e: React.PointerEvent): void => { - const rotateUndo = UndoManager.StartBatch("rotatedown"); - const selectedInk = SelectionManager.Views().filter(i => i.ComponentView instanceof InkingStroke); - const centerPoint = !selectedInk.length ? { X: this.Bounds.x, Y: this.Bounds.y } : { X: this.Bounds.c?.X ?? (this.Bounds.x + this.Bounds.r) / 2, Y: this.Bounds.c?.Y ?? (this.Bounds.y + this.Bounds.b) / 2 }; - setupMoveUpEvents(this, e, - (e: PointerEvent, down: number[], delta: number[]) => { - const previousPoint = { X: e.clientX, Y: e.clientY }; - const movedPoint = { X: e.clientX - delta[0], Y: e.clientY - delta[1] }; - const angle = InkStrokeProperties.angleChange(previousPoint, movedPoint, centerPoint); - if (selectedInk.length) { - angle && InkStrokeProperties.Instance.rotateInk(selectedInk, -angle, centerPoint); - } else { - SelectionManager.Views().forEach(dv => dv.rootDoc._jitterRotation = NumCast(dv.rootDoc._jitterRotation) - angle * 180 / Math.PI); - } - return false; - }, - () => { - rotateUndo?.end(); - UndoManager.FilterBatches(["data", "x", "y", "width", "height"]); - }, - emptyFunction); - } - - @action - onPointerDown = (e: React.PointerEvent): void => { - DragManager.docsBeingDragged = SelectionManager.Views().map(dv => dv.rootDoc); - this._inkDragDocs = DragManager.docsBeingDragged - .filter(doc => doc.type === DocumentType.INK) - .map(doc => { - if (InkStrokeProperties.Instance._lock) { - Doc.SetNativeHeight(doc, NumCast(doc._height)); - Doc.SetNativeWidth(doc, NumCast(doc._width)); - } - return ({ doc, x: NumCast(doc.x), y: NumCast(doc.y), width: NumCast(doc._width), height: NumCast(doc._height) }); - }); - - setupMoveUpEvents(this, e, this.onPointerMove, this.onPointerUp, emptyFunction); - this.Interacting = true; // turns off pointer events on things like youtube videos and web pages so that dragging doesn't get "stuck" when cursor moves over them - this._resizeHdlId = e.currentTarget.className; - const bounds = e.currentTarget.getBoundingClientRect(); - this._offX = this._resizeHdlId.toLowerCase().includes("left") ? bounds.right - e.clientX : bounds.left - e.clientX; - this._offY = this._resizeHdlId.toLowerCase().includes("top") ? bounds.bottom - e.clientY : bounds.top - e.clientY; - this._resizeUndo = UndoManager.StartBatch("DocDecs resize"); - this._snapX = e.pageX; - this._snapY = e.pageY; - const ffviewSet = new Set(); - SelectionManager.Views().forEach(docView => { - const ffview = docView.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView; - ffview && ffviewSet.add(ffview); - this._dragHeights.set(docView.layoutDoc, { start: NumCast(docView.rootDoc._height), lowest: NumCast(docView.rootDoc._height) }); - }); - Array.from(ffviewSet).map(ffview => ffview.setupDragLines(false)); - } - - onPointerMove = (e: PointerEvent, down: number[], move: number[]): boolean => { - const first = SelectionManager.Views()[0]; - let thisPt = { x: e.clientX - this._offX, y: e.clientY - this._offY }; - var fixedAspect = Doc.NativeAspect(first.layoutDoc); - InkStrokeProperties.Instance._lock && SelectionManager.Views().filter(dv => dv.rootDoc.type === DocumentType.INK) - .forEach(dv => fixedAspect = Doc.NativeAspect(dv.rootDoc)); - - const resizeHdl = this._resizeHdlId.split(" ")[0]; - if (fixedAspect && (resizeHdl === "documentDecorations-bottomRightResizer" || resizeHdl === "documentDecorations-topLeftResizer")) { // need to generalize for bl and tr drag handles - const project = (p: number[], a: number[], b: number[]) => { - const atob = [b[0] - a[0], b[1] - a[1]]; - const atop = [p[0] - a[0], p[1] - a[1]]; - const len = atob[0] * atob[0] + atob[1] * atob[1]; - let dot = atop[0] * atob[0] + atop[1] * atob[1]; - const t = dot / len; - dot = (b[0] - a[0]) * (p[1] - a[1]) - (b[1] - a[1]) * (p[0] - a[0]); - return [a[0] + atob[0] * t, a[1] + atob[1] * t]; - }; - const tl = first.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); - const drag = project([e.clientX + this._offX, e.clientY + this._offY], tl, [tl[0] + fixedAspect, tl[1] + 1]); - thisPt = DragManager.snapDragAspect(drag, fixedAspect); - } else { - thisPt = DragManager.snapDrag(e, -this._offX, -this._offY, this._offX, this._offY); - } - - move[0] = thisPt.x - this._snapX; - move[1] = thisPt.y - this._snapY; - this._snapX = thisPt.x; - this._snapY = thisPt.y; - let dragBottom = false, dragRight = false, dragBotRight = false; - let dX = 0, dY = 0, dW = 0, dH = 0; - switch (this._resizeHdlId.split(" ")[0]) { - case "": break; - case "documentDecorations-topLeftResizer": - dX = -1; - dY = -1; - dW = -move[0]; - dH = -move[1]; - break; - case "documentDecorations-topRightResizer": - dW = move[0]; - dY = -1; - dH = -move[1]; - break; - case "documentDecorations-topResizer": - dY = -1; - dH = -move[1]; - dragBottom = true; - break; - case "documentDecorations-bottomLeftResizer": - dX = -1; - dW = -move[0]; - dH = move[1]; - break; - case "documentDecorations-bottomRightResizer": - dW = move[0]; - dH = move[1]; - dragBotRight = true; - break; - case "documentDecorations-bottomResizer": - dH = move[1]; - dragBottom = true; - break; - case "documentDecorations-leftResizer": - dX = -1; - dW = -move[0]; - break; - case "documentDecorations-rightResizer": - dW = move[0]; - dragRight = true; - break; - } - - SelectionManager.Views().forEach(action((docView: DocumentView) => { - if (e.ctrlKey && !Doc.NativeHeight(docView.props.Document)) docView.toggleNativeDimensions(); - if (dX !== 0 || dY !== 0 || dW !== 0 || dH !== 0) { - const doc = Document(docView.rootDoc); - const nwidth = docView.nativeWidth; - const nheight = docView.nativeHeight; - const docheight = doc._height || 0; - const docwidth = doc._width || 0; - const width = docwidth; - let height = (docheight || (nheight / nwidth * width)); - height = !height || isNaN(height) ? 20 : height; - const scale = docView.props.ScreenToLocalTransform().Scale; - const modifyNativeDim = (e.ctrlKey || doc.forceReflow) && doc.nativeDimModifiable; - if (nwidth && nheight) { - if (nwidth / nheight !== width / height && !dragBottom) { - height = nheight / nwidth * width; - } - if (modifyNativeDim && !dragBottom) { // ctrl key enables modification of the nativeWidth or nativeHeight durin the interaction - if (Math.abs(dW) > Math.abs(dH)) dH = dW * nheight / nwidth; - else dW = dH * nwidth / nheight; - } - } - let actualdW = Math.max(width + (dW * scale), 20); - let actualdH = Math.max(height + (dH * scale), 20); - const fixedAspect = (nwidth && nheight && !doc._fitWidth); - if (fixedAspect) { - if ((Math.abs(dW) > Math.abs(dH) && (!dragBottom || !modifyNativeDim)) || dragRight) { - if (dragRight && modifyNativeDim) { - doc._nativeWidth = actualdW / (doc._width || 1) * Doc.NativeWidth(doc); - } else { - if (!doc._fitWidth) { - actualdH = nheight / nwidth * actualdW; - doc._height = actualdH; - } - else if (!modifyNativeDim || dragBotRight) doc._height = actualdH; + } + onMaximizeDown = (e: React.PointerEvent) => { + setupMoveUpEvents(this, e, () => { + DragManager.StartWindowDrag?.(e, [SelectionManager.Views().slice(-1)[0].rootDoc]); + return true; + }, emptyFunction, this.onMaximizeClick, false, false); + } + + onMaximizeClick = (e: any): void => { + const selectedDocs = SelectionManager.Views(); + if (selectedDocs.length) { + if (e.ctrlKey) { // open an alias in a new tab with Ctrl Key + const bestAlias = DocListCast(selectedDocs[0].props.Document.aliases).find(doc => !doc.context && doc.author === Doc.CurrentUserEmail); + CollectionDockingView.AddSplit(bestAlias ?? Doc.MakeAlias(selectedDocs[0].props.Document), "right"); + } else if (e.shiftKey) { // open centered in a new workspace with Shift Key + const alias = Doc.MakeAlias(selectedDocs[0].props.Document); + alias.context = undefined; + alias.x = -alias[WidthSym]() / 2; + alias.y = -alias[HeightSym]() / 2; + CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([alias], { title: "Tab for " + alias.title }), "right"); + } else if (e.altKey) { // open same document in new tab + CollectionDockingView.ToggleSplit(selectedDocs[0].props.Document, "right"); + } else { + var openDoc = selectedDocs[0].props.Document; + if (openDoc.layoutKey === "layout_icon") { + openDoc = DocListCast(openDoc.aliases).find(alias => !alias.context) ?? Doc.MakeAlias(openDoc); + Doc.deiconifyView(openDoc); } - doc._width = actualdW; - } - else { - if (dragBottom && (modifyNativeDim || - (docView.layoutDoc.nativeHeightUnfrozen && docView.layoutDoc._fitWidth))) { // frozen web pages, PDFs, and some RTFS have frozen nativewidth/height. But they are marked to allow their nativeHeight to be explicitly modified with fitWidth and vertical resizing. (ie, with fitWidth they can't grow horizontally to match a vertical resize so it makes more sense to change their nativeheight even if the ctrl key isn't used) - doc._nativeHeight = actualdH / (doc._height || 1) * Doc.NativeHeight(doc); - doc._autoHeight = false; + LightboxView.SetLightboxDoc(openDoc, undefined, selectedDocs.slice(1).map(view => view.props.Document)); + } + } + SelectionManager.DeselectAll(); + } + + onIconifyClick = (): void => { + SelectionManager.Views().forEach(dv => dv?.iconify()); + SelectionManager.DeselectAll(); + } + + onSelectorClick = () => SelectionManager.Views()?.[0]?.props.ContainingCollectionView?.props.select(false); + + onRadiusDown = (e: React.PointerEvent): void => { + this._resizeUndo = UndoManager.StartBatch("DocDecs set radius"); + setupMoveUpEvents(this, e, (e, down) => { + const dist = Math.sqrt((e.clientX - down[0]) * (e.clientX - down[0]) + (e.clientY - down[1]) * (e.clientY - down[1])); + SelectionManager.Views().map(dv => dv.props.Document).map(doc => doc.layout instanceof Doc ? doc.layout : doc.isTemplateForField ? doc : Doc.GetProto(doc)). + map(d => d.borderRounding = `${Math.max(0, dist < 3 ? 0 : dist)}px`); + return false; + }, (e) => this._resizeUndo?.end(), (e) => { }); + } + + @action + onRotateDown = (e: React.PointerEvent): void => { + const rotateUndo = UndoManager.StartBatch("rotatedown"); + const selectedInk = SelectionManager.Views().filter(i => i.ComponentView instanceof InkingStroke); + const centerPoint = !selectedInk.length ? { X: this.Bounds.x, Y: this.Bounds.y } : { X: this.Bounds.c?.X ?? (this.Bounds.x + this.Bounds.r) / 2, Y: this.Bounds.c?.Y ?? (this.Bounds.y + this.Bounds.b) / 2 }; + setupMoveUpEvents(this, e, + (e: PointerEvent, down: number[], delta: number[]) => { + const previousPoint = { X: e.clientX, Y: e.clientY }; + const movedPoint = { X: e.clientX - delta[0], Y: e.clientY - delta[1] }; + const angle = InkStrokeProperties.angleChange(previousPoint, movedPoint, centerPoint); + if (selectedInk.length) { + angle && InkStrokeProperties.Instance.rotateInk(selectedInk, -angle, centerPoint); } else { - if (!doc._fitWidth) { - actualdW = nwidth / nheight * actualdH; - doc._width = actualdW; - } - else if (!modifyNativeDim || dragBotRight) doc._width = actualdW; + SelectionManager.Views().forEach(dv => dv.rootDoc._jitterRotation = NumCast(dv.rootDoc._jitterRotation) - angle * 180 / Math.PI); } - if (!modifyNativeDim) { - actualdH = Math.min(nheight / nwidth * NumCast(doc._width), actualdH); - doc._height = actualdH; + return false; + }, + () => { + rotateUndo?.end(); + UndoManager.FilterBatches(["data", "x", "y", "width", "height"]); + }, + emptyFunction); + } + + @action + onPointerDown = (e: React.PointerEvent): void => { + DragManager.docsBeingDragged = SelectionManager.Views().map(dv => dv.rootDoc); + this._inkDragDocs = DragManager.docsBeingDragged + .filter(doc => doc.type === DocumentType.INK) + .map(doc => { + if (InkStrokeProperties.Instance._lock) { + Doc.SetNativeHeight(doc, NumCast(doc._height)); + Doc.SetNativeWidth(doc, NumCast(doc._width)); } - else doc._height = actualdH; - } - } else { - dH && (doc._height = actualdH); - dW && (doc._width = actualdW); - dH && (doc._autoHeight = false); - } - doc.x = (doc.x || 0) + dX * (actualdW - docwidth); - doc.y = (doc.y || 0) + dY * (actualdH - docheight); - doc._lastModified = new DateField(); - } - const val = this._dragHeights.get(docView.layoutDoc); - if (val) this._dragHeights.set(docView.layoutDoc, { start: val.start, lowest: Math.min(val.lowest, NumCast(docView.layoutDoc._height)) }); - })); - return false; - } - - @action - onPointerUp = (e: PointerEvent): void => { - this._resizeHdlId = ""; - this.Interacting = false; - this._resizeUndo?.end(); - SnappingManager.clearSnapLines(); - - // detect autoHeight gesture and apply - SelectionManager.Views().map(docView => ({ doc: docView.layoutDoc, hgts: this._dragHeights.get(docView.layoutDoc) })) - .filter(pair => pair.hgts && pair.hgts.lowest < pair.hgts.start && pair.hgts.lowest <= 20) - .forEach(pair => pair.doc._autoHeight = true); - //need to change points for resize, or else rotation/control points will fail. - this._inkDragDocs.map(oldbds => ({ oldbds, inkPts: Cast(oldbds.doc.data, InkField)?.inkData || [] })) - .forEach(({ oldbds: { doc, x, y, width, height }, inkPts }) => { - Doc.GetProto(doc).data = new InkField(inkPts.map(ipt => // (new x — oldx) + newWidth * (oldxpoint /oldWidth) - ({ - X: (NumCast(doc.x) - x) + NumCast(doc.width) * ipt.X / width, - Y: (NumCast(doc.y) - y) + NumCast(doc.height) * ipt.Y / height - }))); - Doc.SetNativeWidth(doc, undefined); - Doc.SetNativeHeight(doc, undefined); + return ({ doc, x: NumCast(doc.x), y: NumCast(doc.y), width: NumCast(doc._width), height: NumCast(doc._height) }); + }); + + setupMoveUpEvents(this, e, this.onPointerMove, this.onPointerUp, emptyFunction); + this.Interacting = true; // turns off pointer events on things like youtube videos and web pages so that dragging doesn't get "stuck" when cursor moves over them + this._resizeHdlId = e.currentTarget.className; + const bounds = e.currentTarget.getBoundingClientRect(); + this._offX = this._resizeHdlId.toLowerCase().includes("left") ? bounds.right - e.clientX : bounds.left - e.clientX; + this._offY = this._resizeHdlId.toLowerCase().includes("top") ? bounds.bottom - e.clientY : bounds.top - e.clientY; + this._resizeUndo = UndoManager.StartBatch("DocDecs resize"); + this._snapX = e.pageX; + this._snapY = e.pageY; + const ffviewSet = new Set(); + SelectionManager.Views().forEach(docView => { + const ffview = docView.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView; + ffview && ffviewSet.add(ffview); + this._dragHeights.set(docView.layoutDoc, { start: NumCast(docView.rootDoc._height), lowest: NumCast(docView.rootDoc._height) }); }); - } - - @computed - get selectionTitle(): string { - if (SelectionManager.Views().length === 1) { - const selected = SelectionManager.Views()[0]; - if (selected.ComponentView?.getTitle?.()) { - return selected.ComponentView.getTitle(); + Array.from(ffviewSet).map(ffview => ffview.setupDragLines(false)); + } + + onPointerMove = (e: PointerEvent, down: number[], move: number[]): boolean => { + const first = SelectionManager.Views()[0]; + let thisPt = { x: e.clientX - this._offX, y: e.clientY - this._offY }; + var fixedAspect = Doc.NativeAspect(first.layoutDoc); + InkStrokeProperties.Instance._lock && SelectionManager.Views().filter(dv => dv.rootDoc.type === DocumentType.INK) + .forEach(dv => fixedAspect = Doc.NativeAspect(dv.rootDoc)); + + const resizeHdl = this._resizeHdlId.split(" ")[0]; + if (fixedAspect && (resizeHdl === "documentDecorations-bottomRightResizer" || resizeHdl === "documentDecorations-topLeftResizer")) { // need to generalize for bl and tr drag handles + const project = (p: number[], a: number[], b: number[]) => { + const atob = [b[0] - a[0], b[1] - a[1]]; + const atop = [p[0] - a[0], p[1] - a[1]]; + const len = atob[0] * atob[0] + atob[1] * atob[1]; + let dot = atop[0] * atob[0] + atop[1] * atob[1]; + const t = dot / len; + dot = (b[0] - a[0]) * (p[1] - a[1]) - (b[1] - a[1]) * (p[0] - a[0]); + return [a[0] + atob[0] * t, a[1] + atob[1] * t]; + }; + const tl = first.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); + const drag = project([e.clientX + this._offX, e.clientY + this._offY], tl, [tl[0] + fixedAspect, tl[1] + 1]); + thisPt = DragManager.snapDragAspect(drag, fixedAspect); + } else { + thisPt = DragManager.snapDrag(e, -this._offX, -this._offY, this._offX, this._offY); } - if (this._titleControlString.startsWith("=")) { - return ScriptField.MakeFunction(this._titleControlString.substring(1), { doc: Doc.name })!.script.run({ self: selected.rootDoc, this: selected.layoutDoc }, console.log).result?.toString() || ""; + + move[0] = thisPt.x - this._snapX; + move[1] = thisPt.y - this._snapY; + this._snapX = thisPt.x; + this._snapY = thisPt.y; + let dragBottom = false, dragRight = false, dragBotRight = false; + let dX = 0, dY = 0, dW = 0, dH = 0; + switch (this._resizeHdlId.split(" ")[0]) { + case "": break; + case "documentDecorations-topLeftResizer": + dX = -1; + dY = -1; + dW = -move[0]; + dH = -move[1]; + break; + case "documentDecorations-topRightResizer": + dW = move[0]; + dY = -1; + dH = -move[1]; + break; + case "documentDecorations-topResizer": + dY = -1; + dH = -move[1]; + dragBottom = true; + break; + case "documentDecorations-bottomLeftResizer": + dX = -1; + dW = -move[0]; + dH = move[1]; + break; + case "documentDecorations-bottomRightResizer": + dW = move[0]; + dH = move[1]; + dragBotRight = true; + break; + case "documentDecorations-bottomResizer": + dH = move[1]; + dragBottom = true; + break; + case "documentDecorations-leftResizer": + dX = -1; + dW = -move[0]; + break; + case "documentDecorations-rightResizer": + dW = move[0]; + dragRight = true; + break; } - if (this._titleControlString.startsWith("#")) { - return Field.toString(selected.props.Document[this._titleControlString.substring(1)] as Field) || "-unset-"; + + SelectionManager.Views().forEach(action((docView: DocumentView) => { + if (e.ctrlKey && !Doc.NativeHeight(docView.props.Document)) docView.toggleNativeDimensions(); + if (dX !== 0 || dY !== 0 || dW !== 0 || dH !== 0) { + const doc = Document(docView.rootDoc); + const nwidth = docView.nativeWidth; + const nheight = docView.nativeHeight; + const docheight = doc._height || 0; + const docwidth = doc._width || 0; + const width = docwidth; + let height = (docheight || (nheight / nwidth * width)); + height = !height || isNaN(height) ? 20 : height; + const scale = docView.props.ScreenToLocalTransform().Scale; + const modifyNativeDim = (e.ctrlKey || doc.forceReflow) && doc.nativeDimModifiable; + if (nwidth && nheight) { + if (nwidth / nheight !== width / height && !dragBottom) { + height = nheight / nwidth * width; + } + if (modifyNativeDim && !dragBottom) { // ctrl key enables modification of the nativeWidth or nativeHeight durin the interaction + if (Math.abs(dW) > Math.abs(dH)) dH = dW * nheight / nwidth; + else dW = dH * nwidth / nheight; + } + } + let actualdW = Math.max(width + (dW * scale), 20); + let actualdH = Math.max(height + (dH * scale), 20); + const fixedAspect = (nwidth && nheight && !doc._fitWidth); + if (fixedAspect) { + if ((Math.abs(dW) > Math.abs(dH) && (!dragBottom || !modifyNativeDim)) || dragRight) { + if (dragRight && modifyNativeDim) { + doc._nativeWidth = actualdW / (doc._width || 1) * Doc.NativeWidth(doc); + } else { + if (!doc._fitWidth) { + actualdH = nheight / nwidth * actualdW; + doc._height = actualdH; + } + else if (!modifyNativeDim || dragBotRight) doc._height = actualdH; + } + doc._width = actualdW; + } + else { + if (dragBottom && (modifyNativeDim || + (docView.layoutDoc.nativeHeightUnfrozen && docView.layoutDoc._fitWidth))) { // frozen web pages, PDFs, and some RTFS have frozen nativewidth/height. But they are marked to allow their nativeHeight to be explicitly modified with fitWidth and vertical resizing. (ie, with fitWidth they can't grow horizontally to match a vertical resize so it makes more sense to change their nativeheight even if the ctrl key isn't used) + doc._nativeHeight = actualdH / (doc._height || 1) * Doc.NativeHeight(doc); + doc._autoHeight = false; + } else { + if (!doc._fitWidth) { + actualdW = nwidth / nheight * actualdH; + doc._width = actualdW; + } + else if (!modifyNativeDim || dragBotRight) doc._width = actualdW; + } + if (!modifyNativeDim) { + actualdH = Math.min(nheight / nwidth * NumCast(doc._width), actualdH); + doc._height = actualdH; + } + else doc._height = actualdH; + } + } else { + dH && (doc._height = actualdH); + dW && (doc._width = actualdW); + dH && (doc._autoHeight = false); + } + doc.x = (doc.x || 0) + dX * (actualdW - docwidth); + doc.y = (doc.y || 0) + dY * (actualdH - docheight); + doc._lastModified = new DateField(); + } + const val = this._dragHeights.get(docView.layoutDoc); + if (val) this._dragHeights.set(docView.layoutDoc, { start: val.start, lowest: Math.min(val.lowest, NumCast(docView.layoutDoc._height)) }); + })); + return false; + } + + @action + onPointerUp = (e: PointerEvent): void => { + this._resizeHdlId = ""; + this.Interacting = false; + this._resizeUndo?.end(); + SnappingManager.clearSnapLines(); + + // detect autoHeight gesture and apply + SelectionManager.Views().map(docView => ({ doc: docView.layoutDoc, hgts: this._dragHeights.get(docView.layoutDoc) })) + .filter(pair => pair.hgts && pair.hgts.lowest < pair.hgts.start && pair.hgts.lowest <= 20) + .forEach(pair => pair.doc._autoHeight = true); + //need to change points for resize, or else rotation/control points will fail. + this._inkDragDocs.map(oldbds => ({ oldbds, inkPts: Cast(oldbds.doc.data, InkField)?.inkData || [] })) + .forEach(({ oldbds: { doc, x, y, width, height }, inkPts }) => { + Doc.GetProto(doc).data = new InkField(inkPts.map(ipt => // (new x — oldx) + newWidth * (oldxpoint /oldWidth) + ({ + X: (NumCast(doc.x) - x) + NumCast(doc.width) * ipt.X / width, + Y: (NumCast(doc.y) - y) + NumCast(doc.height) * ipt.Y / height + }))); + Doc.SetNativeWidth(doc, undefined); + Doc.SetNativeHeight(doc, undefined); + }); + } + + @computed + get selectionTitle(): string { + if (SelectionManager.Views().length === 1) { + const selected = SelectionManager.Views()[0]; + if (selected.ComponentView?.getTitle?.()) { + return selected.ComponentView.getTitle(); + } + if (this._titleControlString.startsWith("=")) { + return ScriptField.MakeFunction(this._titleControlString.substring(1), { doc: Doc.name })!.script.run({ self: selected.rootDoc, this: selected.layoutDoc }, console.log).result?.toString() || ""; + } + if (this._titleControlString.startsWith("#")) { + return Field.toString(selected.props.Document[this._titleControlString.substring(1)] as Field) || "-unset-"; + } + return this._accumulatedTitle; + } + return SelectionManager.Views().length > 1 ? "-multiple-" : "-unset-"; + } + + @computed get canDelete() { + return SelectionManager.Views().some(docView => { + if (docView.rootDoc.stayInCollection) return false; + const collectionAcl = docView.props.ContainingCollectionView ? GetEffectiveAcl(docView.props.ContainingCollectionDoc?.[DataSym]) : AclEdit; + //return (!docView.rootDoc._stayInCollection || docView.rootDoc.isInkMask) && + return (collectionAcl === AclAdmin || collectionAcl === AclEdit || GetEffectiveAcl(docView.rootDoc) === AclAdmin); + }); + } + @computed get hasIcons() { + return SelectionManager.Views().some(docView => docView.rootDoc.layoutKey === "layout_icon"); + } + + render() { + const bounds = this.Bounds; + const seldoc = SelectionManager.Views().slice(-1)[0]; + if (SnappingManager.GetIsDragging() || bounds.r - bounds.x < 1 || bounds.x === Number.MAX_VALUE || !seldoc || this._hidden || isNaN(bounds.r) || isNaN(bounds.b) || isNaN(bounds.x) || isNaN(bounds.y)) { + return (null); } - return this._accumulatedTitle; - } - return SelectionManager.Views().length > 1 ? "-multiple-" : "-unset-"; - } - - @computed get canDelete() { - return SelectionManager.Views().some(docView => { - if (docView.rootDoc.stayInCollection) return false; - const collectionAcl = docView.props.ContainingCollectionView ? GetEffectiveAcl(docView.props.ContainingCollectionDoc?.[DataSym]) : AclEdit; - //return (!docView.rootDoc._stayInCollection || docView.rootDoc.isInkMask) && - return (collectionAcl === AclAdmin || collectionAcl === AclEdit || GetEffectiveAcl(docView.rootDoc) === AclAdmin); - }); - } - @computed get hasIcons() { - return SelectionManager.Views().some(docView => docView.rootDoc.layoutKey === "layout_icon"); - } - - render() { - const bounds = this.Bounds; - const seldoc = SelectionManager.Views().slice(-1)[0]; - if (SnappingManager.GetIsDragging() || bounds.r - bounds.x < 1 || bounds.x === Number.MAX_VALUE || !seldoc || this._hidden || isNaN(bounds.r) || isNaN(bounds.b) || isNaN(bounds.x) || isNaN(bounds.y)) { - return (null); - } - const hideResizers = seldoc.props.hideResizeHandles || seldoc.rootDoc.hideResizeHandles || seldoc.rootDoc._isGroup; - const hideTitle = seldoc.props.hideDecorationTitle || seldoc.rootDoc.hideDecorationTitle; - const canOpen = SelectionManager.Views().some(docView => !docView.props.Document._stayInCollection && !docView.props.Document.isGroup && !docView.props.Document.hideOpenButton); - const canDelete = this.canDelete; - const topBtn = (key: string, icon: string, pointerDown: undefined | ((e: React.PointerEvent) => void), click: undefined | ((e: any) => void), title: string) => ( - {title}} placement="top"> -
e.preventDefault()} - onPointerDown={pointerDown ?? (e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(e => click!(e))))} > - -
-
); - - const colorScheme = StrCast(CurrentUserUtils.ActiveDashboard?.colorScheme); - const titleArea = hideTitle ?
: - this._edtingTitle ? - this.titleBlur()} - onChange={action(e => this._accumulatedTitle = e.target.value)} - onKeyDown={this.titleEntered} /> : -
- {`${this.selectionTitle}`} -
; - - - const leftBounds = this.props.boundsLeft; - const topBounds = LightboxView.LightboxDoc ? 0 : this.props.boundsTop; - bounds.x = Math.max(leftBounds, bounds.x - this._resizeBorderWidth / 2) + this._resizeBorderWidth / 2; - bounds.y = Math.max(topBounds, bounds.y - this._resizeBorderWidth / 2 - this._titleHeight) + this._resizeBorderWidth / 2 + this._titleHeight; - const borderRadiusDraggerWidth = 15; - bounds.r = Math.max(bounds.x, Math.max(leftBounds, Math.min(window.innerWidth, bounds.r + borderRadiusDraggerWidth + this._resizeBorderWidth / 2) - this._resizeBorderWidth / 2 - borderRadiusDraggerWidth)); - bounds.b = Math.max(bounds.y, Math.max(topBounds, Math.min(window.innerHeight, bounds.b + this._resizeBorderWidth / 2 + this._linkBoxHeight) - this._resizeBorderWidth / 2 - this._linkBoxHeight)); - - const useRotation = seldoc.ComponentView instanceof InkingStroke || seldoc.ComponentView instanceof ImageBox; - const resizerScheme = colorScheme ? "documentDecorations-resizer" + colorScheme : ""; - - const rotation = NumCast(seldoc.rootDoc._jitterRotation); - - return (
-
{ e.preventDefault(); e.stopPropagation(); }} /> - {bounds.r - bounds.x < 15 && bounds.b - bounds.y < 15 ? (null) : <> -
- {!canDelete ?
: topBtn("close", this.hasIcons ? "times" : "window-maximize", undefined, e => this.onCloseClick(this.hasIcons ? true : undefined), "Close")} - {titleArea} - {!canOpen ? (null) : topBtn("open", "external-link-alt", this.onMaximizeDown, undefined, "Open in Tab (ctrl: as alias, shift: in new collection)")} - {hideResizers ? (null) : - <> -
e.preventDefault()} /> -
e.preventDefault()} /> -
e.preventDefault()} /> -
e.preventDefault()} /> -
-
e.preventDefault()} /> -
e.preventDefault()} /> -
e.preventDefault()} /> -
e.preventDefault()} /> - - {seldoc.props.renderDepth <= 1 || !seldoc.props.ContainingCollectionView ? (null) : - topBtn("selector", "arrow-alt-circle-up", undefined, this.onSelectorClick, "tap to select containing document")} -
e.preventDefault()}>{useRotation && "⟲"}
- - } - {seldoc?.Document.type === DocumentType.FONTICON ? (null) : -
- -
} -
- } -
- ); - } + // hide the decorations if the parent chooses to hide it or if the document itself hides it + const hideResizers = seldoc.props.hideResizeHandles || seldoc.rootDoc.hideResizeHandles || seldoc.rootDoc._isGroup; + const hideTitle = seldoc.props.hideDecorationTitle || seldoc.rootDoc.hideDecorationTitle; + const hideDocumentButtonBar = seldoc.props.hideDocumentButtonBar || seldoc.rootDoc.hideDocumentButtonBar; + // if multiple documents have been opened at the same time, then don't show open button + const canOpen = SelectionManager.Views().some(docView => !docView.props.Document._stayInCollection && !docView.props.Document.isGroup && !docView.props.Document.hideOpenButton); + const canDelete = this.canDelete; + const topBtn = (key: string, icon: string, pointerDown: undefined | ((e: React.PointerEvent) => void), click: undefined | ((e: any) => void), title: string) => ( + {title}
} placement="top"> +
e.preventDefault()} + onPointerDown={pointerDown ?? (e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(e => click!(e))))} > + +
+ ); + + const colorScheme = StrCast(CurrentUserUtils.ActiveDashboard?.colorScheme); + const titleArea = hideTitle ?
: + this._edtingTitle ? + this.titleBlur()} + onChange={action(e => this._accumulatedTitle = e.target.value)} + onKeyDown={this.titleEntered} /> : +
+ {`${this.selectionTitle}`} +
; + + + const leftBounds = this.props.boundsLeft; + const topBounds = LightboxView.LightboxDoc ? 0 : this.props.boundsTop; + bounds.x = Math.max(leftBounds, bounds.x - this._resizeBorderWidth / 2) + this._resizeBorderWidth / 2; + bounds.y = Math.max(topBounds, bounds.y - this._resizeBorderWidth / 2 - this._titleHeight) + this._resizeBorderWidth / 2 + this._titleHeight; + const borderRadiusDraggerWidth = 15; + bounds.r = Math.max(bounds.x, Math.max(leftBounds, Math.min(window.innerWidth, bounds.r + borderRadiusDraggerWidth + this._resizeBorderWidth / 2) - this._resizeBorderWidth / 2 - borderRadiusDraggerWidth)); + bounds.b = Math.max(bounds.y, Math.max(topBounds, Math.min(window.innerHeight, bounds.b + this._resizeBorderWidth / 2 + this._linkBoxHeight) - this._resizeBorderWidth / 2 - this._linkBoxHeight)); + + const useRotation = seldoc.ComponentView instanceof InkingStroke || seldoc.ComponentView instanceof ImageBox; + const resizerScheme = colorScheme ? "documentDecorations-resizer" + colorScheme : ""; + + const rotation = NumCast(seldoc.rootDoc._jitterRotation); + + return (
+
{ e.preventDefault(); e.stopPropagation(); }} /> + {bounds.r - bounds.x < 15 && bounds.b - bounds.y < 15 ? (null) : <> +
+ {!canDelete ?
: topBtn("close", this.hasIcons ? "times" : "window-maximize", undefined, e => this.onCloseClick(this.hasIcons ? true : undefined), "Close")} + {titleArea} + {!canOpen ? (null) : topBtn("open", "external-link-alt", this.onMaximizeDown, undefined, "Open in Tab (ctrl: as alias, shift: in new collection)")} + {hideResizers ? (null) : + <> +
e.preventDefault()} /> +
e.preventDefault()} /> +
e.preventDefault()} /> +
e.preventDefault()} /> +
+
e.preventDefault()} /> +
e.preventDefault()} /> +
e.preventDefault()} /> +
e.preventDefault()} /> + + {seldoc.props.renderDepth <= 1 || !seldoc.props.ContainingCollectionView ? (null) : + topBtn("selector", "arrow-alt-circle-up", undefined, this.onSelectorClick, "tap to select containing document")} +
e.preventDefault()}>{useRotation && "⟲"}
+ + } + + {hideDocumentButtonBar ? (null) : +
+ +
} +
+ } +
+ ); + } } \ No newline at end of file diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 373e3ee57..96c989e77 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -54,1344 +54,1345 @@ import { FieldViewProps } from "./FieldView"; const { Howl } = require('howler'); interface Window { - MediaRecorder: MediaRecorder; + MediaRecorder: MediaRecorder; } declare class MediaRecorder { - // whatever MediaRecorder has - constructor(e: any); + // whatever MediaRecorder has + constructor(e: any); } export enum ViewAdjustment { - resetView = 1, - doNothing = 0 + resetView = 1, + doNothing = 0 } export const ViewSpecPrefix = "viewSpec"; // field prefix for anchor fields that are immediately copied over to the target document when link is followed. Other anchor properties will be copied over in the specific setViewSpec() method on their view (which allows for seting preview values instead of writing to the document) export interface DocFocusOptions { - originalTarget?: Doc; // set in JumpToDocument, used by TabDocView to determine whether to fit contents to tab - willZoom?: boolean; // determines whether to zoom in on target document - scale?: number; // percent of containing frame to zoom into document - afterFocus?: DocAfterFocusFunc; // function to call after focusing on a document - docTransform?: Transform; // when a document can't be panned and zoomed within its own container (say a group), then we need to continue to move up the render hierarchy to find something that can pan and zoom. when this happens the docTransform must accumulate all the transforms of each level of the hierarchy - instant?: boolean; // whether focus should happen instantly (as opposed to smooth zoom) + originalTarget?: Doc; // set in JumpToDocument, used by TabDocView to determine whether to fit contents to tab + willZoom?: boolean; // determines whether to zoom in on target document + scale?: number; // percent of containing frame to zoom into document + afterFocus?: DocAfterFocusFunc; // function to call after focusing on a document + docTransform?: Transform; // when a document can't be panned and zoomed within its own container (say a group), then we need to continue to move up the render hierarchy to find something that can pan and zoom. when this happens the docTransform must accumulate all the transforms of each level of the hierarchy + instant?: boolean; // whether focus should happen instantly (as opposed to smooth zoom) } export type DocAfterFocusFunc = (notFocused: boolean) => Promise; export type DocFocusFunc = (doc: Doc, options?: DocFocusOptions) => void; export type StyleProviderFunc = (doc: Opt, props: Opt, property: string) => any; export interface DocComponentView { - updateIcon?: () => void; // updates the icon representation of the document - getAnchor?: () => Doc; // returns an Anchor Doc that represents the current state of the doc's componentview (e.g., the current playhead location of a an audio/video box) - scrollFocus?: (doc: Doc, smooth: boolean) => Opt; // returns the duration of the focus - setViewSpec?: (anchor: Doc, preview: boolean) => void; // sets viewing information for a componentview, typically when following a link. 'preview' tells the view to use the values without writing to the document - reverseNativeScaling?: () => boolean; // DocumentView's setup screenToLocal based on the doc having a nativeWidth/Height. However, some content views (e.g., FreeFormView w/ fitToBox set) may ignore the native dimensions so this flags the DocumentView to not do Nativre scaling. - shrinkWrap?: () => void; // requests a document to display all of its contents with no white space. currently only implemented (needed?) for freeform views - menuControls?: () => JSX.Element; // controls to display in the top menu bar when the document is selected. - isAnyChildContentActive?: () => boolean; // is any child content of the document active - getKeyFrameEditing?: () => boolean; // whether the document is in keyframe editing mode (if it is, then all hidden documents that are not active at the keyframe time will still be shown) - setKeyFrameEditing?: (set: boolean) => void; // whether the document is in keyframe editing mode (if it is, then all hidden documents that are not active at the keyframe time will still be shown) - playFrom?: (time: number, endTime?: number) => void; - Pause?: () => void; - setFocus?: () => void; - componentUI?: (boundsLeft: number, boundsTop: number) => JSX.Element | null; - fieldKey?: string; - annotationKey?: string; - getTitle?: () => string; - getScrollHeight?: () => number; - getCenter?: (xf: Transform) => { X: number, Y: number }; - ptToScreen?: (pt: { X: number, Y: number }) => { X: number, Y: number }; - ptFromScreen?: (pt: { X: number, Y: number }) => { X: number, Y: number }; - snapPt?: (pt: { X: number, Y: number }, excludeSegs?: number[]) => { nearestPt: { X: number, Y: number }, distance: number }; - search?: (str: string, bwd?: boolean, clear?: boolean) => boolean; + updateIcon?: () => void; // updates the icon representation of the document + getAnchor?: () => Doc; // returns an Anchor Doc that represents the current state of the doc's componentview (e.g., the current playhead location of a an audio/video box) + scrollFocus?: (doc: Doc, smooth: boolean) => Opt; // returns the duration of the focus + setViewSpec?: (anchor: Doc, preview: boolean) => void; // sets viewing information for a componentview, typically when following a link. 'preview' tells the view to use the values without writing to the document + reverseNativeScaling?: () => boolean; // DocumentView's setup screenToLocal based on the doc having a nativeWidth/Height. However, some content views (e.g., FreeFormView w/ fitToBox set) may ignore the native dimensions so this flags the DocumentView to not do Nativre scaling. + shrinkWrap?: () => void; // requests a document to display all of its contents with no white space. currently only implemented (needed?) for freeform views + menuControls?: () => JSX.Element; // controls to display in the top menu bar when the document is selected. + isAnyChildContentActive?: () => boolean; // is any child content of the document active + getKeyFrameEditing?: () => boolean; // whether the document is in keyframe editing mode (if it is, then all hidden documents that are not active at the keyframe time will still be shown) + setKeyFrameEditing?: (set: boolean) => void; // whether the document is in keyframe editing mode (if it is, then all hidden documents that are not active at the keyframe time will still be shown) + playFrom?: (time: number, endTime?: number) => void; + Pause?: () => void; + setFocus?: () => void; + componentUI?: (boundsLeft: number, boundsTop: number) => JSX.Element | null; + fieldKey?: string; + annotationKey?: string; + getTitle?: () => string; + getScrollHeight?: () => number; + getCenter?: (xf: Transform) => { X: number, Y: number }; + ptToScreen?: (pt: { X: number, Y: number }) => { X: number, Y: number }; + ptFromScreen?: (pt: { X: number, Y: number }) => { X: number, Y: number }; + snapPt?: (pt: { X: number, Y: number }, excludeSegs?: number[]) => { nearestPt: { X: number, Y: number }, distance: number }; + search?: (str: string, bwd?: boolean, clear?: boolean) => boolean; } // These props are passed to both FieldViews and DocumentViews export interface DocumentViewSharedProps { - fieldKey?: string; // only used by FieldViews but helpful here to allow styleProviders to access fieldKey of FieldViewProps. In priniciple, passing a fieldKey to a documentView could override or be the default fieldKey for fieldViews - DocumentView?: () => DocumentView; - renderDepth: number; - Document: Doc; - DataDoc?: Doc; - fitContentsToDoc?: () => boolean; // used by freeformview to fit its contents to its panel. corresponds to _fitToBox property on a Document - ContainingCollectionView: Opt; - ContainingCollectionDoc: Opt; - suppressSetHeight?: boolean; - thumbShown?: () => boolean; - isHovering?: () => boolean; - setContentView?: (view: DocComponentView) => any; - CollectionFreeFormDocumentView?: () => CollectionFreeFormDocumentView; - PanelWidth: () => number; - PanelHeight: () => number; - docViewPath: () => DocumentView[]; - childHideDecorationTitle?: () => boolean; - childHideResizeHandles?: () => boolean; - 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; - focus: DocFocusFunc; - fitWidth?: (doc: Doc) => boolean; - docFilters: () => string[]; - docRangeFilters: () => string[]; - searchFilterDocs: () => Doc[]; - showTitle?: () => string; - whenChildContentsActiveChanged: (isActive: boolean) => void; - rootSelected: (outsideReaction?: boolean) => boolean; // whether the root of a template has been selected - addDocTab: (doc: Doc, where: string) => boolean; - filterAddDocument?: (doc: Doc[]) => boolean; // allows a document that renders a Collection view to filter or modify any documents added to the collection (see PresBox for an example) - addDocument?: (doc: Doc | Doc[]) => boolean; - removeDocument?: (doc: Doc | Doc[]) => boolean; - moveDocument?: (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => boolean; - pinToPres: (document: Doc) => void; - ScreenToLocalTransform: () => Transform; - bringToFront: (doc: Doc, sendToBack?: boolean) => void; - dropAction?: dropActionType; - dontRegisterView?: boolean; - hideLinkButton?: boolean; - hideCaptions?: boolean; - ignoreAutoHeight?: boolean; - forceAutoHeight?: boolean; - disableDocBrushing?: boolean; // should highlighting for this view be disabled when same document in another view is hovered over. - pointerEvents?: () => Opt; - scriptContext?: any; // can be assigned anything and will be passed as 'scriptContext' to any OnClick script that executes on this document - createNewFilterDoc?: () => void; - updateFilterDoc?: (doc: Doc) => void; + fieldKey?: string; // only used by FieldViews but helpful here to allow styleProviders to access fieldKey of FieldViewProps. In priniciple, passing a fieldKey to a documentView could override or be the default fieldKey for fieldViews + DocumentView?: () => DocumentView; + renderDepth: number; + Document: Doc; + DataDoc?: Doc; + fitContentsToDoc?: () => boolean; // used by freeformview to fit its contents to its panel. corresponds to _fitToBox property on a Document + ContainingCollectionView: Opt; + ContainingCollectionDoc: Opt; + suppressSetHeight?: boolean; + thumbShown?: () => boolean; + isHovering?: () => boolean; + setContentView?: (view: DocComponentView) => any; + CollectionFreeFormDocumentView?: () => CollectionFreeFormDocumentView; + PanelWidth: () => number; + PanelHeight: () => number; + docViewPath: () => DocumentView[]; + childHideDecorationTitle?: () => boolean; + childHideResizeHandles?: () => boolean; + 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; + focus: DocFocusFunc; + fitWidth?: (doc: Doc) => boolean; + docFilters: () => string[]; + docRangeFilters: () => string[]; + searchFilterDocs: () => Doc[]; + showTitle?: () => string; + whenChildContentsActiveChanged: (isActive: boolean) => void; + rootSelected: (outsideReaction?: boolean) => boolean; // whether the root of a template has been selected + addDocTab: (doc: Doc, where: string) => boolean; + filterAddDocument?: (doc: Doc[]) => boolean; // allows a document that renders a Collection view to filter or modify any documents added to the collection (see PresBox for an example) + addDocument?: (doc: Doc | Doc[]) => boolean; + removeDocument?: (doc: Doc | Doc[]) => boolean; + moveDocument?: (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => boolean; + pinToPres: (document: Doc) => void; + ScreenToLocalTransform: () => Transform; + bringToFront: (doc: Doc, sendToBack?: boolean) => void; + dropAction?: dropActionType; + dontRegisterView?: boolean; + hideLinkButton?: boolean; + hideCaptions?: boolean; + ignoreAutoHeight?: boolean; + forceAutoHeight?: boolean; + disableDocBrushing?: boolean; // should highlighting for this view be disabled when same document in another view is hovered over. + pointerEvents?: () => Opt; + scriptContext?: any; // can be assigned anything and will be passed as 'scriptContext' to any OnClick script that executes on this document + createNewFilterDoc?: () => void; + updateFilterDoc?: (doc: Doc) => void; } // these props are specific to DocuentViews export interface DocumentViewProps extends DocumentViewSharedProps { - // properties specific to DocumentViews but not to FieldView - freezeDimensions?: boolean; - hideResizeHandles?: boolean; // whether to suppress DocumentDecorations when this document is selected - hideTitle?: boolean; // forces suppression of title. e.g, treeView document labels suppress titles in case they are globally active via settings - hideDecorationTitle?: boolean; // forces suppression of title. e.g, treeView document labels suppress titles in case they are globally active via settings - treeViewDoc?: Doc; - isDocumentActive?: () => boolean | undefined; // whether a document should handle pointer events - isContentActive: () => boolean | undefined; // whether document contents should handle pointer events - contentPointerEvents?: string; // pointer events allowed for content of a document view. eg. set to "none" in menuSidebar for sharedDocs so that you can select a document, but not interact with its contents - radialMenu?: String[]; - LayoutTemplateString?: string; - dontCenter?: "x" | "y" | "xy"; - dontScaleFilter?: (doc: Doc) => boolean; // decides whether a document can be scaled to fit its container vs native size with scrolling - ContentScaling?: () => number; // scaling the DocumentView does to transform its contents into its panel & needed by ScreenToLocal - NativeWidth?: () => number; - NativeHeight?: () => number; - LayoutTemplate?: () => Opt; - contextMenuItems?: () => { script: ScriptField, filter?: ScriptField, label: string, icon: string }[]; - onClick?: () => ScriptField; - onDoubleClick?: () => ScriptField; - onPointerDown?: () => ScriptField; - onPointerUp?: () => ScriptField; - onBrowseClick?: () => (ScriptField | undefined); - onKey?: (e: React.KeyboardEvent, fieldProps: FieldViewProps) => (boolean | undefined); + // properties specific to DocumentViews but not to FieldView + freezeDimensions?: boolean; + hideResizeHandles?: boolean; // whether to suppress DocumentDecorations when this document is selected + hideTitle?: boolean; // forces suppression of title. e.g, treeView document labels suppress titles in case they are globally active via settings + hideDecorationTitle?: boolean; // forces suppression of title. e.g, treeView document labels suppress titles in case they are globally active via settings + hideDocumentButtonBar?: boolean; + treeViewDoc?: Doc; + isDocumentActive?: () => boolean | undefined; // whether a document should handle pointer events + isContentActive: () => boolean | undefined; // whether document contents should handle pointer events + contentPointerEvents?: string; // pointer events allowed for content of a document view. eg. set to "none" in menuSidebar for sharedDocs so that you can select a document, but not interact with its contents + radialMenu?: String[]; + LayoutTemplateString?: string; + dontCenter?: "x" | "y" | "xy"; + dontScaleFilter?: (doc: Doc) => boolean; // decides whether a document can be scaled to fit its container vs native size with scrolling + ContentScaling?: () => number; // scaling the DocumentView does to transform its contents into its panel & needed by ScreenToLocal + NativeWidth?: () => number; + NativeHeight?: () => number; + LayoutTemplate?: () => Opt; + contextMenuItems?: () => { script: ScriptField, filter?: ScriptField, label: string, icon: string }[]; + onClick?: () => ScriptField; + onDoubleClick?: () => ScriptField; + onPointerDown?: () => ScriptField; + onPointerUp?: () => ScriptField; + onBrowseClick?: () => (ScriptField | undefined); + onKey?: (e: React.KeyboardEvent, fieldProps: FieldViewProps) => (boolean | undefined); } // these props are only available in DocumentViewIntenral export interface DocumentViewInternalProps extends DocumentViewProps { - NativeWidth: () => number; - NativeHeight: () => number; - isSelected: (outsideReaction?: boolean) => boolean; - select: (ctrlPressed: boolean) => void; - DocumentView: () => DocumentView; - viewPath: () => DocumentView[]; + NativeWidth: () => number; + NativeHeight: () => number; + isSelected: (outsideReaction?: boolean) => boolean; + select: (ctrlPressed: boolean) => void; + DocumentView: () => DocumentView; + viewPath: () => DocumentView[]; } @observer export class DocumentViewInternal extends DocComponent() { - public static SelectAfterContextMenu = true; // whether a document should be selected after it's contextmenu is triggered. - _animateScaleTime = 300; // milliseconds; - @observable _animateScalingTo = 0; - @observable _mediaState = 0; - @observable _pendingDoubleClick = false; - private _disposers: { [name: string]: IReactionDisposer } = {}; - private _downX: number = 0; - private _downY: number = 0; - private _firstX: number = -1; - private _firstY: number = -1; - private _lastTap: number = 0; - private _doubleTap = false; - private _mainCont = React.createRef(); - private _titleRef = React.createRef(); - private _timeout: NodeJS.Timeout | undefined; - private _dropDisposer?: DragManager.DragDropDisposer; - private _holdDisposer?: InteractionUtils.MultiTouchEventDisposer; - protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; - @observable _componentView: Opt; // needs to be accessed from DocumentView wrapper class - - private get topMost() { return this.props.renderDepth === 0 && !LightboxView.LightboxDoc; } - public get displayName() { return "DocumentView(" + this.props.Document.title + ")"; } // this makes mobx trace() statements more descriptive - public get ContentDiv() { return this._mainCont.current; } - public get LayoutFieldKey() { return Doc.LayoutFieldKey(this.layoutDoc); } - @computed get ShowTitle() { return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.ShowTitle) as (Opt); } - @computed get ContentScale() { return this.props.ContentScaling?.() || 1; } - @computed get thumb() { return ImageCast(this.layoutDoc["thumb-frozen"], ImageCast(this.layoutDoc.thumb))?.url.href.replace(".png", "_m.png"); } - @computed get hidden() { return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Hidden); } - @computed get opacity() { return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Opacity); } - @computed get boxShadow() { return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BoxShadow); } - @computed get borderRounding() { return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding); } - @computed get hideLinkButton() { return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.HideLinkButton + (this.props.isSelected() ? ":selected" : "")); } - @computed get widgetDecorations() { return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Decorations + (this.props.isSelected() ? ":selected" : "")); } - @computed get backgroundColor() { return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor); } - @computed get docContents() { return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.DocContents); } - @computed get headerMargin() { return this.props?.styleProvider?.(this.layoutDoc, this.props, StyleProp.HeaderMargin) || 0; } - @computed get titleHeight() { return this.props?.styleProvider?.(this.layoutDoc, this.props, StyleProp.TitleHeight) || 0; } - @computed get pointerEvents() { return this.props.styleProvider?.(this.Document, this.props, StyleProp.PointerEvents + (this.props.isSelected() ? ":selected" : "")); } - @computed get finalLayoutKey() { return StrCast(this.Document.layoutKey, "layout"); } - @computed get nativeWidth() { return this.props.NativeWidth(); } - @computed get nativeHeight() { return this.props.NativeHeight(); } - @computed get onClickHandler() { return this.props.onClick?.() ?? (this.props.onBrowseClick?.() ?? Cast(this.Document.onClick, ScriptField, Cast(this.layoutDoc.onClick, ScriptField, null))); } - @computed get onDoubleClickHandler() { return this.props.onDoubleClick?.() ?? (Cast(this.layoutDoc.onDoubleClick, ScriptField, null) ?? this.Document.onDoubleClick); } - @computed get onPointerDownHandler() { return this.props.onPointerDown?.() ?? ScriptCast(this.Document.onPointerDown); } - @computed get onPointerUpHandler() { return this.props.onPointerUp?.() ?? ScriptCast(this.Document.onPointerUp); } - - - componentWillUnmount() { this.cleanupHandlers(true); } - componentDidMount() { this.setupHandlers(); } - //componentDidUpdate() { this.setupHandlers(); } - setupHandlers() { - this.cleanupHandlers(false); - if (this._mainCont.current) { - this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, this.drop.bind(this), this.props.Document); - this._multiTouchDisposer = InteractionUtils.MakeMultiTouchTarget(this._mainCont.current, this.onTouchStart.bind(this)); - this._holdDisposer = InteractionUtils.MakeHoldTouchTarget(this._mainCont.current, this.handle1PointerHoldStart.bind(this)); - } - } - @action - cleanupHandlers(unbrush: boolean) { - this._dropDisposer?.(); - this._multiTouchDisposer?.(); - this._holdDisposer?.(); - unbrush && Doc.UnBrushDoc(this.props.Document); - Object.values(this._disposers).forEach(disposer => disposer?.()); - } - - handle1PointerHoldStart = (e: Event, me: InteractionUtils.MultiTouchEvent): any => { - this.removeMoveListeners(); - this.removeEndListeners(); - document.removeEventListener("pointermove", this.onPointerMove); - document.removeEventListener("pointerup", this.onPointerUp); - if (RadialMenu.Instance._display === false) { - this.addHoldMoveListeners(); - this.addHoldEndListeners(); - this.onRadialMenu(e, me); + public static SelectAfterContextMenu = true; // whether a document should be selected after it's contextmenu is triggered. + _animateScaleTime = 300; // milliseconds; + @observable _animateScalingTo = 0; + @observable _mediaState = 0; + @observable _pendingDoubleClick = false; + private _disposers: { [name: string]: IReactionDisposer } = {}; + private _downX: number = 0; + private _downY: number = 0; + private _firstX: number = -1; + private _firstY: number = -1; + private _lastTap: number = 0; + private _doubleTap = false; + private _mainCont = React.createRef(); + private _titleRef = React.createRef(); + private _timeout: NodeJS.Timeout | undefined; + private _dropDisposer?: DragManager.DragDropDisposer; + private _holdDisposer?: InteractionUtils.MultiTouchEventDisposer; + protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; + @observable _componentView: Opt; // needs to be accessed from DocumentView wrapper class + + private get topMost() { return this.props.renderDepth === 0 && !LightboxView.LightboxDoc; } + public get displayName() { return "DocumentView(" + this.props.Document.title + ")"; } // this makes mobx trace() statements more descriptive + public get ContentDiv() { return this._mainCont.current; } + public get LayoutFieldKey() { return Doc.LayoutFieldKey(this.layoutDoc); } + @computed get ShowTitle() { return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.ShowTitle) as (Opt); } + @computed get ContentScale() { return this.props.ContentScaling?.() || 1; } + @computed get thumb() { return ImageCast(this.layoutDoc["thumb-frozen"], ImageCast(this.layoutDoc.thumb))?.url.href.replace(".png", "_m.png"); } + @computed get hidden() { return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Hidden); } + @computed get opacity() { return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Opacity); } + @computed get boxShadow() { return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BoxShadow); } + @computed get borderRounding() { return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding); } + @computed get hideLinkButton() { return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.HideLinkButton + (this.props.isSelected() ? ":selected" : "")); } + @computed get widgetDecorations() { return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Decorations + (this.props.isSelected() ? ":selected" : "")); } + @computed get backgroundColor() { return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor); } + @computed get docContents() { return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.DocContents); } + @computed get headerMargin() { return this.props?.styleProvider?.(this.layoutDoc, this.props, StyleProp.HeaderMargin) || 0; } + @computed get titleHeight() { return this.props?.styleProvider?.(this.layoutDoc, this.props, StyleProp.TitleHeight) || 0; } + @computed get pointerEvents() { return this.props.styleProvider?.(this.Document, this.props, StyleProp.PointerEvents + (this.props.isSelected() ? ":selected" : "")); } + @computed get finalLayoutKey() { return StrCast(this.Document.layoutKey, "layout"); } + @computed get nativeWidth() { return this.props.NativeWidth(); } + @computed get nativeHeight() { return this.props.NativeHeight(); } + @computed get onClickHandler() { return this.props.onClick?.() ?? (this.props.onBrowseClick?.() ?? Cast(this.Document.onClick, ScriptField, Cast(this.layoutDoc.onClick, ScriptField, null))); } + @computed get onDoubleClickHandler() { return this.props.onDoubleClick?.() ?? (Cast(this.layoutDoc.onDoubleClick, ScriptField, null) ?? this.Document.onDoubleClick); } + @computed get onPointerDownHandler() { return this.props.onPointerDown?.() ?? ScriptCast(this.Document.onPointerDown); } + @computed get onPointerUpHandler() { return this.props.onPointerUp?.() ?? ScriptCast(this.Document.onPointerUp); } + + + componentWillUnmount() { this.cleanupHandlers(true); } + componentDidMount() { this.setupHandlers(); } + //componentDidUpdate() { this.setupHandlers(); } + setupHandlers() { + this.cleanupHandlers(false); + if (this._mainCont.current) { + this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, this.drop.bind(this), this.props.Document); + this._multiTouchDisposer = InteractionUtils.MakeMultiTouchTarget(this._mainCont.current, this.onTouchStart.bind(this)); + this._holdDisposer = InteractionUtils.MakeHoldTouchTarget(this._mainCont.current, this.handle1PointerHoldStart.bind(this)); + } + } + @action + cleanupHandlers(unbrush: boolean) { + this._dropDisposer?.(); + this._multiTouchDisposer?.(); + this._holdDisposer?.(); + unbrush && Doc.UnBrushDoc(this.props.Document); + Object.values(this._disposers).forEach(disposer => disposer?.()); + } + + handle1PointerHoldStart = (e: Event, me: InteractionUtils.MultiTouchEvent): any => { + this.removeMoveListeners(); + this.removeEndListeners(); + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + if (RadialMenu.Instance._display === false) { + this.addHoldMoveListeners(); + this.addHoldEndListeners(); + this.onRadialMenu(e, me); + const pt = me.touchEvent.touches[me.touchEvent.touches.length - 1]; + this._firstX = pt.pageX; + this._firstY = pt.pageY; + } + } + + handle1PointerHoldMove = (e: Event, me: InteractionUtils.MultiTouchEvent): void => { const pt = me.touchEvent.touches[me.touchEvent.touches.length - 1]; - this._firstX = pt.pageX; - this._firstY = pt.pageY; - } - } - - handle1PointerHoldMove = (e: Event, me: InteractionUtils.MultiTouchEvent): void => { - const pt = me.touchEvent.touches[me.touchEvent.touches.length - 1]; - - if (this._firstX === -1 || this._firstY === -1) { - return; - } - if (Math.abs(pt.pageX - this._firstX) > 150 || Math.abs(pt.pageY - this._firstY) > 150) { - this.handle1PointerHoldEnd(e, me); - } - } - - handle1PointerHoldEnd = (e: Event, me: InteractionUtils.MultiTouchEvent): void => { - this.removeHoldMoveListeners(); - this.removeHoldEndListeners(); - RadialMenu.Instance.closeMenu(); - this._firstX = -1; - this._firstY = -1; - SelectionManager.DeselectAll(); - me.touchEvent.stopPropagation(); - me.touchEvent.preventDefault(); - e.stopPropagation(); - if (RadialMenu.Instance.used) { - this.onContextMenu(undefined, me.touches[0].pageX, me.touches[0].pageY); - } - } - - handle2PointersDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent) => { - if (!e.nativeEvent.cancelBubble && !this.props.isSelected()) { + + if (this._firstX === -1 || this._firstY === -1) { + return; + } + if (Math.abs(pt.pageX - this._firstX) > 150 || Math.abs(pt.pageY - this._firstY) > 150) { + this.handle1PointerHoldEnd(e, me); + } + } + + handle1PointerHoldEnd = (e: Event, me: InteractionUtils.MultiTouchEvent): void => { + this.removeHoldMoveListeners(); + this.removeHoldEndListeners(); + RadialMenu.Instance.closeMenu(); + this._firstX = -1; + this._firstY = -1; + SelectionManager.DeselectAll(); + me.touchEvent.stopPropagation(); + me.touchEvent.preventDefault(); e.stopPropagation(); - e.preventDefault(); + if (RadialMenu.Instance.used) { + this.onContextMenu(undefined, me.touches[0].pageX, me.touches[0].pageY); + } + } - this.removeMoveListeners(); - this.addMoveListeners(); - this.removeEndListeners(); - this.addEndListeners(); - } - } - - handle1PointerDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent) => { - SelectionManager.DeselectAll(); - if (this.Document.onPointerDown) return; - const touch = me.touchEvent.changedTouches.item(0); - if (touch) { - this._downX = touch.clientX; - this._downY = touch.clientY; - if (!e.nativeEvent.cancelBubble) { - if ((this.props.isDocumentActive?.() || this.layoutDoc.onDragStart || this.onClickHandler) && !e.ctrlKey && !this.layoutDoc._lockedPosition && !CurrentUserUtils.OverlayDocs.includes(this.layoutDoc)) e.stopPropagation(); - this.removeMoveListeners(); - this.addMoveListeners(); - this.removeEndListeners(); - this.addEndListeners(); - e.stopPropagation(); + handle2PointersDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent) => { + if (!e.nativeEvent.cancelBubble && !this.props.isSelected()) { + e.stopPropagation(); + e.preventDefault(); + + this.removeMoveListeners(); + this.addMoveListeners(); + this.removeEndListeners(); + this.addEndListeners(); } - } - } + } - handle1PointerMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent) => { - if (e.cancelBubble && this.props.isDocumentActive?.()) { - this.removeMoveListeners(); - } - else if (!e.cancelBubble && (this.props.isDocumentActive?.() || this.layoutDoc.onDragStart || this.onClickHandler) && !this.layoutDoc._lockedPosition && !CurrentUserUtils.OverlayDocs.includes(this.layoutDoc)) { + handle1PointerDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent) => { + SelectionManager.DeselectAll(); + if (this.Document.onPointerDown) return; const touch = me.touchEvent.changedTouches.item(0); - if (touch && (Math.abs(this._downX - touch.clientX) > 3 || Math.abs(this._downY - touch.clientY) > 3)) { - if (!e.altKey && (!this.topMost || this.layoutDoc.onDragStart || this.onClickHandler)) { - this.cleanUpInteractions(); - this.startDragging(this._downX, this._downY, this.Document.dropAction ? this.Document.dropAction as any : e.ctrlKey || e.altKey ? "alias" : undefined); - } + if (touch) { + this._downX = touch.clientX; + this._downY = touch.clientY; + if (!e.nativeEvent.cancelBubble) { + if ((this.props.isDocumentActive?.() || this.layoutDoc.onDragStart || this.onClickHandler) && !e.ctrlKey && !this.layoutDoc._lockedPosition && !CurrentUserUtils.OverlayDocs.includes(this.layoutDoc)) e.stopPropagation(); + this.removeMoveListeners(); + this.addMoveListeners(); + this.removeEndListeners(); + this.addEndListeners(); + e.stopPropagation(); + } } - e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers - e.preventDefault(); - } - } - - @action - handle2PointersMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent) => { - const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true); - const pt1 = myTouches[0]; - const pt2 = myTouches[1]; - const oldPoint1 = this.prevPoints.get(pt1.identifier); - const oldPoint2 = this.prevPoints.get(pt2.identifier); - const pinching = InteractionUtils.Pinning(pt1, pt2, oldPoint1!, oldPoint2!); - if (pinching !== 0 && oldPoint1 && oldPoint2) { - const dW = (Math.abs(pt1.clientX - pt2.clientX) - Math.abs(oldPoint1.clientX - oldPoint2.clientX)); - const dH = (Math.abs(pt1.clientY - pt2.clientY) - Math.abs(oldPoint1.clientY - oldPoint2.clientY)); - const dX = -1 * Math.sign(dW); - const dY = -1 * Math.sign(dH); - - if (dX !== 0 || dY !== 0 || dW !== 0 || dH !== 0) { - const doc = Document(this.props.Document); - const layoutDoc = Document(Doc.Layout(this.props.Document)); - let nwidth = Doc.NativeWidth(layoutDoc); - let nheight = Doc.NativeHeight(layoutDoc); - const width = (layoutDoc._width || 0); - const height = (layoutDoc._height || (nheight / nwidth * width)); - const scale = this.props.ScreenToLocalTransform().Scale * this.ContentScale; - const actualdW = Math.max(width + (dW * scale), 20); - const actualdH = Math.max(height + (dH * scale), 20); - doc.x = (doc.x || 0) + dX * (actualdW - width); - doc.y = (doc.y || 0) + dY * (actualdH - height); - const fixedAspect = e.ctrlKey || (nwidth && nheight); - if (fixedAspect && (!nwidth || !nheight)) { - Doc.SetNativeWidth(layoutDoc, nwidth = layoutDoc._width || 0); - Doc.SetNativeHeight(layoutDoc, nheight = layoutDoc._height || 0); - } - if (nwidth > 0 && nheight > 0) { - if (Math.abs(dW) > Math.abs(dH)) { - if (!fixedAspect) { - Doc.SetNativeWidth(layoutDoc, actualdW / (layoutDoc._width || 1) * Doc.NativeWidth(layoutDoc)); + } + + handle1PointerMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent) => { + if (e.cancelBubble && this.props.isDocumentActive?.()) { + this.removeMoveListeners(); + } + else if (!e.cancelBubble && (this.props.isDocumentActive?.() || this.layoutDoc.onDragStart || this.onClickHandler) && !this.layoutDoc._lockedPosition && !CurrentUserUtils.OverlayDocs.includes(this.layoutDoc)) { + const touch = me.touchEvent.changedTouches.item(0); + if (touch && (Math.abs(this._downX - touch.clientX) > 3 || Math.abs(this._downY - touch.clientY) > 3)) { + if (!e.altKey && (!this.topMost || this.layoutDoc.onDragStart || this.onClickHandler)) { + this.cleanUpInteractions(); + this.startDragging(this._downX, this._downY, this.Document.dropAction ? this.Document.dropAction as any : e.ctrlKey || e.altKey ? "alias" : undefined); + } + } + e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers + e.preventDefault(); + } + } + + @action + handle2PointersMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent) => { + const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true); + const pt1 = myTouches[0]; + const pt2 = myTouches[1]; + const oldPoint1 = this.prevPoints.get(pt1.identifier); + const oldPoint2 = this.prevPoints.get(pt2.identifier); + const pinching = InteractionUtils.Pinning(pt1, pt2, oldPoint1!, oldPoint2!); + if (pinching !== 0 && oldPoint1 && oldPoint2) { + const dW = (Math.abs(pt1.clientX - pt2.clientX) - Math.abs(oldPoint1.clientX - oldPoint2.clientX)); + const dH = (Math.abs(pt1.clientY - pt2.clientY) - Math.abs(oldPoint1.clientY - oldPoint2.clientY)); + const dX = -1 * Math.sign(dW); + const dY = -1 * Math.sign(dH); + + if (dX !== 0 || dY !== 0 || dW !== 0 || dH !== 0) { + const doc = Document(this.props.Document); + const layoutDoc = Document(Doc.Layout(this.props.Document)); + let nwidth = Doc.NativeWidth(layoutDoc); + let nheight = Doc.NativeHeight(layoutDoc); + const width = (layoutDoc._width || 0); + const height = (layoutDoc._height || (nheight / nwidth * width)); + const scale = this.props.ScreenToLocalTransform().Scale * this.ContentScale; + const actualdW = Math.max(width + (dW * scale), 20); + const actualdH = Math.max(height + (dH * scale), 20); + doc.x = (doc.x || 0) + dX * (actualdW - width); + doc.y = (doc.y || 0) + dY * (actualdH - height); + const fixedAspect = e.ctrlKey || (nwidth && nheight); + if (fixedAspect && (!nwidth || !nheight)) { + Doc.SetNativeWidth(layoutDoc, nwidth = layoutDoc._width || 0); + Doc.SetNativeHeight(layoutDoc, nheight = layoutDoc._height || 0); } - layoutDoc._width = actualdW; - if (fixedAspect && !this.props.DocumentView().fitWidth) layoutDoc._height = nheight / nwidth * layoutDoc._width; - else layoutDoc._height = actualdH; - } - else { - if (!fixedAspect) { - Doc.SetNativeHeight(layoutDoc, actualdH / (layoutDoc._height || 1) * Doc.NativeHeight(doc)); + if (nwidth > 0 && nheight > 0) { + if (Math.abs(dW) > Math.abs(dH)) { + if (!fixedAspect) { + Doc.SetNativeWidth(layoutDoc, actualdW / (layoutDoc._width || 1) * Doc.NativeWidth(layoutDoc)); + } + layoutDoc._width = actualdW; + if (fixedAspect && !this.props.DocumentView().fitWidth) layoutDoc._height = nheight / nwidth * layoutDoc._width; + else layoutDoc._height = actualdH; + } + else { + if (!fixedAspect) { + Doc.SetNativeHeight(layoutDoc, actualdH / (layoutDoc._height || 1) * Doc.NativeHeight(doc)); + } + layoutDoc._height = actualdH; + if (fixedAspect && !this.props.DocumentView().fitWidth) layoutDoc._width = nwidth / nheight * layoutDoc._height; + else layoutDoc._width = actualdW; + } + } else { + dW && (layoutDoc._width = actualdW); + dH && (layoutDoc._height = actualdH); + dH && layoutDoc._autoHeight && (layoutDoc._autoHeight = false); } - layoutDoc._height = actualdH; - if (fixedAspect && !this.props.DocumentView().fitWidth) layoutDoc._width = nwidth / nheight * layoutDoc._height; - else layoutDoc._width = actualdW; - } - } else { - dW && (layoutDoc._width = actualdW); - dH && (layoutDoc._height = actualdH); - dH && layoutDoc._autoHeight && (layoutDoc._autoHeight = false); - } + } + e.stopPropagation(); + e.preventDefault(); } - e.stopPropagation(); - e.preventDefault(); - } - } - - @action - onRadialMenu = (e: Event, me: InteractionUtils.MultiTouchEvent): void => { - const pt = me.touchEvent.touches[me.touchEvent.touches.length - 1]; - RadialMenu.Instance.openMenu(pt.pageX - 15, pt.pageY - 15); - - // RadialMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "add:right"), icon: "map-pin", selected: -1 }); - const effectiveAcl = GetEffectiveAcl(this.props.Document[DataSym]); - (effectiveAcl === AclEdit || effectiveAcl === AclAdmin) && RadialMenu.Instance.addItem({ description: "Delete", event: () => { this.props.ContainingCollectionView?.removeDocument(this.props.Document), RadialMenu.Instance.closeMenu(); }, icon: "external-link-square-alt", selected: -1 }); - // RadialMenu.Instance.addItem({ description: "Open in a new tab", event: () => this.props.addDocTab(this.props.Document, "add:right"), icon: "trash", selected: -1 }); - RadialMenu.Instance.addItem({ description: "Pin", event: () => this.props.pinToPres(this.props.Document), icon: "map-pin", selected: -1 }); - RadialMenu.Instance.addItem({ description: "Open", event: () => MobileInterface.Instance.handleClick(this.props.Document), icon: "trash", selected: -1 }); - - SelectionManager.DeselectAll(); - } - - startDragging(x: number, y: number, dropAction: dropActionType, hideSource = false) { - if (this._mainCont.current) { - const dragData = new DragManager.DocumentDragData([this.props.Document]); - const [left, top] = this.props.ScreenToLocalTransform().scale(this.ContentScale).inverse().transformPoint(0, 0); - dragData.offset = this.props.ScreenToLocalTransform().scale(this.ContentScale).transformDirection(x - left, y - top); - dragData.offset[0] = Math.min(this.rootDoc[WidthSym](), dragData.offset[0]); - dragData.offset[1] = Math.min(this.rootDoc[HeightSym](), dragData.offset[1]); - dragData.dropAction = dropAction; - dragData.treeViewDoc = this.props.treeViewDoc; - dragData.removeDocument = this.props.removeDocument; - dragData.moveDocument = this.props.moveDocument; - const ffview = this.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView; - ffview && runInAction(() => (ffview.ChildDrag = this.props.DocumentView())); - DragManager.StartDocumentDrag([this._mainCont.current], dragData, x, y, { hideSource: hideSource || (!dropAction && !this.layoutDoc.onDragStart) }, - () => setTimeout(action(() => ffview && (ffview.ChildDrag = undefined)))); // this needs to happen after the drop event is processed. - ffview?.setupDragLines(false); - } - } - - onKeyDown = (e: React.KeyboardEvent) => { - if (e.altKey && !e.nativeEvent.cancelBubble) { - e.stopPropagation(); - e.preventDefault(); - if (e.key === "†" || e.key === "t") { - if (!StrCast(this.layoutDoc._showTitle)) this.layoutDoc._showTitle = "title"; - if (!this._titleRef.current) setTimeout(() => this._titleRef.current?.setIsFocused(true), 0); - else if (!this._titleRef.current.setIsFocused(true)) { // if focus didn't change, focus on interior text... - this._titleRef.current?.setIsFocused(false); - this._componentView?.setFocus?.(); - } + } + + @action + onRadialMenu = (e: Event, me: InteractionUtils.MultiTouchEvent): void => { + const pt = me.touchEvent.touches[me.touchEvent.touches.length - 1]; + RadialMenu.Instance.openMenu(pt.pageX - 15, pt.pageY - 15); + + // RadialMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "add:right"), icon: "map-pin", selected: -1 }); + const effectiveAcl = GetEffectiveAcl(this.props.Document[DataSym]); + (effectiveAcl === AclEdit || effectiveAcl === AclAdmin) && RadialMenu.Instance.addItem({ description: "Delete", event: () => { this.props.ContainingCollectionView?.removeDocument(this.props.Document), RadialMenu.Instance.closeMenu(); }, icon: "external-link-square-alt", selected: -1 }); + // RadialMenu.Instance.addItem({ description: "Open in a new tab", event: () => this.props.addDocTab(this.props.Document, "add:right"), icon: "trash", selected: -1 }); + RadialMenu.Instance.addItem({ description: "Pin", event: () => this.props.pinToPres(this.props.Document), icon: "map-pin", selected: -1 }); + RadialMenu.Instance.addItem({ description: "Open", event: () => MobileInterface.Instance.handleClick(this.props.Document), icon: "trash", selected: -1 }); + + SelectionManager.DeselectAll(); + } + + startDragging(x: number, y: number, dropAction: dropActionType, hideSource = false) { + if (this._mainCont.current) { + const dragData = new DragManager.DocumentDragData([this.props.Document]); + const [left, top] = this.props.ScreenToLocalTransform().scale(this.ContentScale).inverse().transformPoint(0, 0); + dragData.offset = this.props.ScreenToLocalTransform().scale(this.ContentScale).transformDirection(x - left, y - top); + dragData.offset[0] = Math.min(this.rootDoc[WidthSym](), dragData.offset[0]); + dragData.offset[1] = Math.min(this.rootDoc[HeightSym](), dragData.offset[1]); + dragData.dropAction = dropAction; + dragData.treeViewDoc = this.props.treeViewDoc; + dragData.removeDocument = this.props.removeDocument; + dragData.moveDocument = this.props.moveDocument; + const ffview = this.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView; + ffview && runInAction(() => (ffview.ChildDrag = this.props.DocumentView())); + DragManager.StartDocumentDrag([this._mainCont.current], dragData, x, y, { hideSource: hideSource || (!dropAction && !this.layoutDoc.onDragStart) }, + () => setTimeout(action(() => ffview && (ffview.ChildDrag = undefined)))); // this needs to happen after the drop event is processed. + ffview?.setupDragLines(false); } - } - } - - focus = (anchor: Doc, options?: DocFocusOptions) => { - LightboxView.SetCookie(StrCast(anchor["cookies-set"])); - // copying over VIEW fields immediately allows the view type to switch to create the right _componentView - Array.from(Object.keys(Doc.GetProto(anchor))).filter(key => key.startsWith(ViewSpecPrefix)).forEach(spec => { - this.layoutDoc[spec.replace(ViewSpecPrefix, "")] = ((field) => field instanceof ObjectField ? ObjectField.MakeCopy(field) : field)(anchor[spec]); - }); - // after a timeout, the right _componentView should have been created, so call it to update its view spec values - setTimeout(() => this._componentView?.setViewSpec?.(anchor, LinkDocPreview.LinkInfo ? true : false)); - const focusSpeed = this._componentView?.scrollFocus?.(anchor, !options?.instant || !LinkDocPreview.LinkInfo); // bcz: smooth parameter should really be passed into focus() instead of inferred here - const endFocus = focusSpeed === undefined ? options?.afterFocus : async (moved: boolean) => options?.afterFocus ? options?.afterFocus(true) : ViewAdjustment.doNothing; - this.props.focus(options?.docTransform ? anchor : this.rootDoc, { - ...options, afterFocus: (didFocus: boolean) => - new Promise(res => setTimeout(async () => res(endFocus ? await endFocus(didFocus) : ViewAdjustment.doNothing), focusSpeed ?? 0)) - }); - - } - onClick = action((e: React.MouseEvent | React.PointerEvent) => { - if (!e.nativeEvent.cancelBubble && !this.Document.ignoreClick && this.props.renderDepth >= 0 && - (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD)) { - let stopPropagate = true; - let preventDefault = true; - (this.rootDoc._raiseWhenDragged === undefined ? Doc.UserDoc()._raiseWhenDragged : this.rootDoc._raiseWhenDragged) && this.props.bringToFront(this.rootDoc); - if (this._doubleTap && (this.props.Document.type !== DocumentType.FONTICON || this.onDoubleClickHandler)) {// && !this.onClickHandler?.script) { // disable double-click to show full screen for things that have an on click behavior since clicking them twice can be misinterpreted as a double click - if (this._timeout) { - clearTimeout(this._timeout); - this._pendingDoubleClick = false; - this._timeout = undefined; - } - if (this.onDoubleClickHandler?.script && !StrCast(Doc.LayoutField(this.layoutDoc))?.includes(ScriptingBox.name)) { // bcz: hack? don't execute script if you're clicking on a scripting box itself - const { clientX, clientY, shiftKey } = e; - const func = () => this.onDoubleClickHandler.script.run({ - this: this.layoutDoc, - self: this.rootDoc, - scriptContext: this.props.scriptContext, - thisContainer: this.props.ContainingCollectionDoc, - documentView: this.props.DocumentView(), - clientX, clientY, shiftKey - }, console.log); - UndoManager.RunInBatch(() => func().result?.select === true ? this.props.select(false) : "", "on double click"); - } else if (!Doc.IsSystem(this.rootDoc)) { - UndoManager.RunInBatch(() => - LightboxView.AddDocTab(this.rootDoc, "lightbox", this.props.LayoutTemplate?.()) - , "double tap"); - SelectionManager.DeselectAll(); - Doc.UnBrushDoc(this.props.Document); - } - } else if (this.onClickHandler?.script && !StrCast(Doc.LayoutField(this.layoutDoc))?.includes(ScriptingBox.name)) { // bcz: hack? don't execute script if you're clicking on a scripting box itself - const { clientX, clientY, shiftKey } = e; - const func = () => this.onClickHandler.script.run({ - this: this.layoutDoc, - self: this.rootDoc, - _readOnly_: false, - scriptContext: this.props.scriptContext, - thisContainer: this.props.ContainingCollectionDoc, - documentView: this.props.DocumentView(), - clientX, clientY, shiftKey - }, console.log).result?.select === true ? this.props.select(false) : ""; - const clickFunc = () => this.props.Document.dontUndo ? func() : UndoManager.RunInBatch(func, "on click"); - if (this.onDoubleClickHandler) { - runInAction(() => this._pendingDoubleClick = true); - this._timeout = setTimeout(() => { this._timeout = undefined; clickFunc(); }, 350); - } else clickFunc(); - } else if (this.allLinks && this.Document.type !== DocumentType.LINK && this.Document.isLinkButton && !e.shiftKey && !e.ctrlKey) { - this.allLinks.length && LinkManager.FollowLink(undefined, this.props.Document, this.props, e.altKey); - } else { - if ((this.layoutDoc.onDragStart || this.props.Document.rootDocument) && !(e.ctrlKey || e.button > 0)) { // onDragStart implies a button doc that we don't want to select when clicking. RootDocument & isTemplaetForField implies we're clicking on part of a template instance and we want to select the whole template, not the part - stopPropagate = false; // don't stop propagation for field templates -- want the selection to propagate up to the root document of the template - } else { - runInAction(() => this._pendingDoubleClick = true); - this._timeout = setTimeout(action(() => { this._pendingDoubleClick = false; this._timeout = undefined; }), 350); - this.props.select(e.ctrlKey || e.shiftKey); - } - preventDefault = false; + } + + onKeyDown = (e: React.KeyboardEvent) => { + if (e.altKey && !e.nativeEvent.cancelBubble) { + e.stopPropagation(); + e.preventDefault(); + if (e.key === "†" || e.key === "t") { + if (!StrCast(this.layoutDoc._showTitle)) this.layoutDoc._showTitle = "title"; + if (!this._titleRef.current) setTimeout(() => this._titleRef.current?.setIsFocused(true), 0); + else if (!this._titleRef.current.setIsFocused(true)) { // if focus didn't change, focus on interior text... + this._titleRef.current?.setIsFocused(false); + this._componentView?.setFocus?.(); + } + } } - stopPropagate && e.stopPropagation(); - preventDefault && e.preventDefault(); - } - }); - - onPointerDown = (e: React.PointerEvent): void => { - if (this.rootDoc.type === DocumentType.INK && CurrentUserUtils.SelectedTool === InkTool.Eraser) return; - // continue if the event hasn't been canceled AND we are using a mouse or this has an onClick or onDragStart function (meaning it is a button document) - if (!(InteractionUtils.IsType(e, InteractionUtils.MOUSETYPE) || [InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(CurrentUserUtils.SelectedTool))) { - if (!InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) { - e.stopPropagation(); - if (SelectionManager.IsSelected(this.props.DocumentView(), true) && this.props.Document._viewType !== CollectionViewType.Docking) e.preventDefault(); // goldenlayout needs to be able to move its tabs, so can't preventDefault for it - // TODO: check here for panning/inking + } + + focus = (anchor: Doc, options?: DocFocusOptions) => { + LightboxView.SetCookie(StrCast(anchor["cookies-set"])); + // copying over VIEW fields immediately allows the view type to switch to create the right _componentView + Array.from(Object.keys(Doc.GetProto(anchor))).filter(key => key.startsWith(ViewSpecPrefix)).forEach(spec => { + this.layoutDoc[spec.replace(ViewSpecPrefix, "")] = ((field) => field instanceof ObjectField ? ObjectField.MakeCopy(field) : field)(anchor[spec]); + }); + // after a timeout, the right _componentView should have been created, so call it to update its view spec values + setTimeout(() => this._componentView?.setViewSpec?.(anchor, LinkDocPreview.LinkInfo ? true : false)); + const focusSpeed = this._componentView?.scrollFocus?.(anchor, !options?.instant || !LinkDocPreview.LinkInfo); // bcz: smooth parameter should really be passed into focus() instead of inferred here + const endFocus = focusSpeed === undefined ? options?.afterFocus : async (moved: boolean) => options?.afterFocus ? options?.afterFocus(true) : ViewAdjustment.doNothing; + this.props.focus(options?.docTransform ? anchor : this.rootDoc, { + ...options, afterFocus: (didFocus: boolean) => + new Promise(res => setTimeout(async () => res(endFocus ? await endFocus(didFocus) : ViewAdjustment.doNothing), focusSpeed ?? 0)) + }); + + } + onClick = action((e: React.MouseEvent | React.PointerEvent) => { + if (!e.nativeEvent.cancelBubble && !this.Document.ignoreClick && this.props.renderDepth >= 0 && + (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD)) { + let stopPropagate = true; + let preventDefault = true; + (this.rootDoc._raiseWhenDragged === undefined ? Doc.UserDoc()._raiseWhenDragged : this.rootDoc._raiseWhenDragged) && this.props.bringToFront(this.rootDoc); + if (this._doubleTap && (this.props.Document.type !== DocumentType.FONTICON || this.onDoubleClickHandler)) {// && !this.onClickHandler?.script) { // disable double-click to show full screen for things that have an on click behavior since clicking them twice can be misinterpreted as a double click + if (this._timeout) { + clearTimeout(this._timeout); + this._pendingDoubleClick = false; + this._timeout = undefined; + } + if (this.onDoubleClickHandler?.script && !StrCast(Doc.LayoutField(this.layoutDoc))?.includes(ScriptingBox.name)) { // bcz: hack? don't execute script if you're clicking on a scripting box itself + const { clientX, clientY, shiftKey } = e; + const func = () => this.onDoubleClickHandler.script.run({ + this: this.layoutDoc, + self: this.rootDoc, + scriptContext: this.props.scriptContext, + thisContainer: this.props.ContainingCollectionDoc, + documentView: this.props.DocumentView(), + clientX, clientY, shiftKey + }, console.log); + UndoManager.RunInBatch(() => func().result?.select === true ? this.props.select(false) : "", "on double click"); + } else if (!Doc.IsSystem(this.rootDoc)) { + UndoManager.RunInBatch(() => + LightboxView.AddDocTab(this.rootDoc, "lightbox", this.props.LayoutTemplate?.()) + , "double tap"); + SelectionManager.DeselectAll(); + Doc.UnBrushDoc(this.props.Document); + } + } else if (this.onClickHandler?.script && !StrCast(Doc.LayoutField(this.layoutDoc))?.includes(ScriptingBox.name)) { // bcz: hack? don't execute script if you're clicking on a scripting box itself + const { clientX, clientY, shiftKey } = e; + const func = () => this.onClickHandler.script.run({ + this: this.layoutDoc, + self: this.rootDoc, + _readOnly_: false, + scriptContext: this.props.scriptContext, + thisContainer: this.props.ContainingCollectionDoc, + documentView: this.props.DocumentView(), + clientX, clientY, shiftKey + }, console.log).result?.select === true ? this.props.select(false) : ""; + const clickFunc = () => this.props.Document.dontUndo ? func() : UndoManager.RunInBatch(func, "on click"); + if (this.onDoubleClickHandler) { + runInAction(() => this._pendingDoubleClick = true); + this._timeout = setTimeout(() => { this._timeout = undefined; clickFunc(); }, 350); + } else clickFunc(); + } else if (this.allLinks && this.Document.type !== DocumentType.LINK && this.Document.isLinkButton && !e.shiftKey && !e.ctrlKey) { + this.allLinks.length && LinkManager.FollowLink(undefined, this.props.Document, this.props, e.altKey); + } else { + if ((this.layoutDoc.onDragStart || this.props.Document.rootDocument) && !(e.ctrlKey || e.button > 0)) { // onDragStart implies a button doc that we don't want to select when clicking. RootDocument & isTemplaetForField implies we're clicking on part of a template instance and we want to select the whole template, not the part + stopPropagate = false; // don't stop propagation for field templates -- want the selection to propagate up to the root document of the template + } else { + runInAction(() => this._pendingDoubleClick = true); + this._timeout = setTimeout(action(() => { this._pendingDoubleClick = false; this._timeout = undefined; }), 350); + this.props.select(e.ctrlKey || e.shiftKey); + } + preventDefault = false; + } + stopPropagate && e.stopPropagation(); + preventDefault && e.preventDefault(); } - return; - } - this._downX = e.clientX; - this._downY = e.clientY; - if ((!e.nativeEvent.cancelBubble || this.onClickHandler || this.layoutDoc.onDragStart) && - // if this is part of a template, let the event go up to the tempalte root unless right/ctrl clicking - !(this.props.Document.rootDocument && !(e.ctrlKey || e.button > 0))) { - if ((this.props.isDocumentActive?.() || this.layoutDoc.onDragStart) && - !this.props.onBrowseClick?.() && - !this.Document.ignoreClick && - !e.ctrlKey && - (e.button === 0 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) && - !CurrentUserUtils.OverlayDocs.includes(this.layoutDoc)) { - e.stopPropagation(); - // don't preventDefault anymore. Goldenlayout, PDF text selection and RTF text selection all need it to go though - //if (this.props.isSelected(true) && this.rootDoc.type !== DocumentType.PDF && this.layoutDoc._viewType !== CollectionViewType.Docking) e.preventDefault(); + }); + + onPointerDown = (e: React.PointerEvent): void => { + if (this.rootDoc.type === DocumentType.INK && CurrentUserUtils.SelectedTool === InkTool.Eraser) return; + // continue if the event hasn't been canceled AND we are using a mouse or this has an onClick or onDragStart function (meaning it is a button document) + if (!(InteractionUtils.IsType(e, InteractionUtils.MOUSETYPE) || [InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(CurrentUserUtils.SelectedTool))) { + if (!InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) { + e.stopPropagation(); + if (SelectionManager.IsSelected(this.props.DocumentView(), true) && this.props.Document._viewType !== CollectionViewType.Docking) e.preventDefault(); // goldenlayout needs to be able to move its tabs, so can't preventDefault for it + // TODO: check here for panning/inking + } + return; } - if (this.props.isDocumentActive?.()) { - document.removeEventListener("pointermove", this.onPointerMove); - document.addEventListener("pointermove", this.onPointerMove); + this._downX = e.clientX; + this._downY = e.clientY; + if ((!e.nativeEvent.cancelBubble || this.onClickHandler || this.layoutDoc.onDragStart) && + // if this is part of a template, let the event go up to the tempalte root unless right/ctrl clicking + !(this.props.Document.rootDocument && !(e.ctrlKey || e.button > 0))) { + if ((this.props.isDocumentActive?.() || this.layoutDoc.onDragStart) && + !this.props.onBrowseClick?.() && + !this.Document.ignoreClick && + !e.ctrlKey && + (e.button === 0 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) && + !CurrentUserUtils.OverlayDocs.includes(this.layoutDoc)) { + e.stopPropagation(); + // don't preventDefault anymore. Goldenlayout, PDF text selection and RTF text selection all need it to go though + //if (this.props.isSelected(true) && this.rootDoc.type !== DocumentType.PDF && this.layoutDoc._viewType !== CollectionViewType.Docking) e.preventDefault(); + } + if (this.props.isDocumentActive?.()) { + document.removeEventListener("pointermove", this.onPointerMove); + document.addEventListener("pointermove", this.onPointerMove); + } + document.removeEventListener("pointerup", this.onPointerUp); + document.addEventListener("pointerup", this.onPointerUp); } + } + + onPointerMove = (e: PointerEvent): void => { + if (e.cancelBubble) return; + if ((InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || [InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(CurrentUserUtils.SelectedTool))) return; + + if ((this.props.isDocumentActive?.() || this.layoutDoc.onDragStart) && !this.layoutDoc._lockedPosition && !CurrentUserUtils.OverlayDocs.includes(this.layoutDoc)) { + if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) { + if (!e.altKey && (!this.topMost || this.layoutDoc.onDragStart || this.onClickHandler) && (e.buttons === 1 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE))) { + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + this.startDragging(this._downX, this._downY, ((e.ctrlKey || e.altKey) && "alias") || (this.props.dropAction || this.Document.dropAction || undefined) as dropActionType); + } + } + e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers + e.preventDefault(); + } + } + + cleanupPointerEvents = () => { + this.cleanUpInteractions(); + document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); - document.addEventListener("pointerup", this.onPointerUp); - } - } - - onPointerMove = (e: PointerEvent): void => { - if (e.cancelBubble) return; - if ((InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || [InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(CurrentUserUtils.SelectedTool))) return; - - if ((this.props.isDocumentActive?.() || this.layoutDoc.onDragStart) && !this.layoutDoc._lockedPosition && !CurrentUserUtils.OverlayDocs.includes(this.layoutDoc)) { - if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) { - if (!e.altKey && (!this.topMost || this.layoutDoc.onDragStart || this.onClickHandler) && (e.buttons === 1 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE))) { - document.removeEventListener("pointermove", this.onPointerMove); - document.removeEventListener("pointerup", this.onPointerUp); - this.startDragging(this._downX, this._downY, ((e.ctrlKey || e.altKey) && "alias") || (this.props.dropAction || this.Document.dropAction || undefined) as dropActionType); - } + } + + onPointerUp = (e: PointerEvent): void => { + this.cleanupPointerEvents(); + + if (this.onPointerUpHandler?.script && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) { + this.onPointerUpHandler.script.run({ self: this.rootDoc, this: this.layoutDoc }, console.log); + } else { + this._doubleTap = (Date.now() - this._lastTap < 300 && e.button === 0 && Math.abs(e.clientX - this._downX) < 2 && Math.abs(e.clientY - this._downY) < 2); + // bcz: this is a placeholder. documents, when selected, should stopPropagation on doubleClicks if they want to keep the DocumentView from getting them + if (!this.props.isSelected(true) || ![DocumentType.PDF, DocumentType.RTF].includes(StrCast(this.rootDoc.type) as any)) this._lastTap = Date.now();// don't want to process the start of a double tap if the doucment is selected + } + } + + @undoBatch @action + toggleFollowLink = (location: Opt, zoom?: boolean, setPushpin?: boolean): void => { + this.Document.ignoreClick = false; + if (setPushpin) { + this.Document.isPushpin = !this.Document.isPushpin; + this.Document._isLinkButton = this.Document.isPushpin || this.Document._isLinkButton; + } else { + this.Document._isLinkButton = !this.Document._isLinkButton; } - e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers - e.preventDefault(); - } - } - - cleanupPointerEvents = () => { - this.cleanUpInteractions(); - document.removeEventListener("pointermove", this.onPointerMove); - document.removeEventListener("pointerup", this.onPointerUp); - } - - onPointerUp = (e: PointerEvent): void => { - this.cleanupPointerEvents(); - - if (this.onPointerUpHandler?.script && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) { - this.onPointerUpHandler.script.run({ self: this.rootDoc, this: this.layoutDoc }, console.log); - } else { - this._doubleTap = (Date.now() - this._lastTap < 300 && e.button === 0 && Math.abs(e.clientX - this._downX) < 2 && Math.abs(e.clientY - this._downY) < 2); - // bcz: this is a placeholder. documents, when selected, should stopPropagation on doubleClicks if they want to keep the DocumentView from getting them - if (!this.props.isSelected(true) || ![DocumentType.PDF, DocumentType.RTF].includes(StrCast(this.rootDoc.type) as any)) this._lastTap = Date.now();// don't want to process the start of a double tap if the doucment is selected - } - } - - @undoBatch @action - toggleFollowLink = (location: Opt, zoom?: boolean, setPushpin?: boolean): void => { - this.Document.ignoreClick = false; - if (setPushpin) { - this.Document.isPushpin = !this.Document.isPushpin; - this.Document._isLinkButton = this.Document.isPushpin || this.Document._isLinkButton; - } else { - this.Document._isLinkButton = !this.Document._isLinkButton; - } - if (this.Document._isLinkButton && !this.onClickHandler) { - zoom !== undefined && (this.Document.followLinkZoom = zoom); + if (this.Document._isLinkButton && !this.onClickHandler) { + zoom !== undefined && (this.Document.followLinkZoom = zoom); + this.Document.followLinkLocation = location; + } else if (this.Document._isLinkButton && this.onClickHandler) { + this.Document._isLinkButton = false; + this.Document["onClick-rawScript"] = this.dataDoc["onClick-rawScript"] = this.dataDoc.onClick = this.Document.onClick = this.layoutDoc.onClick = undefined; + } + } + @undoBatch @action + toggleTargetOnClick = (): void => { + this.Document.ignoreClick = false; + this.Document._isLinkButton = true; + this.Document.isPushpin = true; + } + @undoBatch @action + followLinkOnClick = (location: Opt, zoom: boolean,): void => { + this.Document.ignoreClick = false; + this.Document._isLinkButton = true; + this.Document.isPushpin = false; + this.Document.followLinkZoom = zoom; this.Document.followLinkLocation = location; - } else if (this.Document._isLinkButton && this.onClickHandler) { + } + @undoBatch @action + selectOnClick = (): void => { + this.Document.ignoreClick = false; this.Document._isLinkButton = false; - this.Document["onClick-rawScript"] = this.dataDoc["onClick-rawScript"] = this.dataDoc.onClick = this.Document.onClick = this.layoutDoc.onClick = undefined; - } - } - @undoBatch @action - toggleTargetOnClick = (): void => { - this.Document.ignoreClick = false; - this.Document._isLinkButton = true; - this.Document.isPushpin = true; - } - @undoBatch @action - followLinkOnClick = (location: Opt, zoom: boolean,): void => { - this.Document.ignoreClick = false; - this.Document._isLinkButton = true; - this.Document.isPushpin = false; - this.Document.followLinkZoom = zoom; - this.Document.followLinkLocation = location; - } - @undoBatch @action - selectOnClick = (): void => { - this.Document.ignoreClick = false; - this.Document._isLinkButton = false; - this.Document.isPushpin = false; - this.Document.onClick = this.layoutDoc.onClick = undefined; - } - @undoBatch - noOnClick = (): void => { - this.Document.ignoreClick = false; - this.Document._isLinkButton = false; - } - - @undoBatch deleteClicked = () => this.props.removeDocument?.(this.props.Document); - @undoBatch setToggleDetail = () => this.Document.onClick = ScriptField.MakeScript(`toggleDetail(documentView, "${StrCast(this.Document.layoutKey).replace("layout_", "")}")`, { documentView: "any" }); - - @undoBatch @action - drop = async (e: Event, de: DragManager.DropEvent) => { - if (this.props.dontRegisterView || this.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) return; - if (this.props.Document === CurrentUserUtils.ActiveDashboard) { - 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; - } - 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); + this.Document.isPushpin = false; + this.Document.onClick = this.layoutDoc.onClick = undefined; + } + @undoBatch + noOnClick = (): void => { + this.Document.ignoreClick = false; + this.Document._isLinkButton = false; + } + + @undoBatch deleteClicked = () => this.props.removeDocument?.(this.props.Document); + @undoBatch setToggleDetail = () => this.Document.onClick = ScriptField.MakeScript(`toggleDetail(documentView, "${StrCast(this.Document.layoutKey).replace("layout_", "")}")`, { documentView: "any" }); + + @undoBatch @action + drop = async (e: Event, de: DragManager.DropEvent) => { + if (this.props.dontRegisterView || this.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) return; + if (this.props.Document === CurrentUserUtils.ActiveDashboard) { + 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; } - if (de.complete.annoDragData || this.rootDoc !== linkdrag.linkSourceDoc.context) { - const dropDoc = de.complete.annoDragData?.dropDocument ?? this._componentView?.getAnchor?.() ?? this.props.Document; - de.complete.linkDocument = DocUtils.MakeLink({ doc: linkdrag.linkSourceDoc }, { doc: dropDoc }, undefined, undefined, undefined, undefined, [de.x, de.y - 50]); + 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); + } + if (de.complete.annoDragData || this.rootDoc !== linkdrag.linkSourceDoc.context) { + const dropDoc = de.complete.annoDragData?.dropDocument ?? this._componentView?.getAnchor?.() ?? this.props.Document; + de.complete.linkDocument = DocUtils.MakeLink({ doc: linkdrag.linkSourceDoc }, { doc: dropDoc }, undefined, undefined, undefined, undefined, [de.x, de.y - 50]); + } } - } - } - - @undoBatch - @action - makeIntoPortal = async () => { - const portalLink = this.allLinks.find(d => d.anchor1 === this.props.Document); - if (!portalLink) { - const portal = Docs.Create.FreeformDocument([], { _width: NumCast(this.layoutDoc._width) + 10, _height: NumCast(this.layoutDoc._height), _fitWidth: true, title: StrCast(this.props.Document.title) + " [Portal]" }); - DocUtils.MakeLink({ doc: this.props.Document }, { doc: portal }, "portal to:portal from"); - } - this.Document.followLinkLocation = "inPlace"; - this.Document.followLinkZoom = true; - this.Document._isLinkButton = true; - } - - @action - onContextMenu = (e?: React.MouseEvent, pageX?: number, pageY?: number) => { - if (e && this.rootDoc._hideContextMenu && Doc.UserDoc().noviceMode) { - e.preventDefault(); - e.stopPropagation(); - //!this.props.isSelected(true) && SelectionManager.SelectView(this.props.DocumentView(), false); - } - // the touch onContextMenu is button 0, the pointer onContextMenu is button 2 - if (e) { - if (e.button === 0 && !e.ctrlKey || e.isDefaultPrevented()) { - e.preventDefault(); - return; + } + + @undoBatch + @action + makeIntoPortal = async () => { + const portalLink = this.allLinks.find(d => d.anchor1 === this.props.Document); + if (!portalLink) { + const portal = Docs.Create.FreeformDocument([], { _width: NumCast(this.layoutDoc._width) + 10, _height: NumCast(this.layoutDoc._height), _fitWidth: true, title: StrCast(this.props.Document.title) + " [Portal]" }); + DocUtils.MakeLink({ doc: this.props.Document }, { doc: portal }, "portal to:portal from"); } - e.preventDefault(); - e.stopPropagation(); - e.persist(); - - if (!navigator.userAgent.includes("Mozilla") && (Math.abs(this._downX - e?.clientX) > 3 || Math.abs(this._downY - e?.clientY) > 3)) { - return; + this.Document.followLinkLocation = "inPlace"; + this.Document.followLinkZoom = true; + this.Document._isLinkButton = true; + } + + @action + onContextMenu = (e?: React.MouseEvent, pageX?: number, pageY?: number) => { + if (e && this.rootDoc._hideContextMenu && Doc.UserDoc().noviceMode) { + e.preventDefault(); + e.stopPropagation(); + //!this.props.isSelected(true) && SelectionManager.SelectView(this.props.DocumentView(), false); } - } - - const cm = ContextMenu.Instance; - if (!cm || (e as any)?.nativeEvent?.SchemaHandled) return; - - if (e && !(e.nativeEvent as any).dash) { - const onDisplay = () => setTimeout(() => { - DocumentViewInternal.SelectAfterContextMenu && !this.props.isSelected(true) && SelectionManager.SelectView(this.props.DocumentView(), false); // on a mac, the context menu is triggered on mouse down, but a YouTube video becaomes interactive when selected which means that the context menu won't show up. by delaying the selection until hopefully after the pointer up, the context menu will appear. - setTimeout(() => { - const ele = document.elementFromPoint(e.clientX, e.clientY); - simulateMouseClick(ele, e.clientX, e.clientY, e.screenX, e.screenY); - }); - }); - if (navigator.userAgent.includes("Macintosh")) { - cm.displayMenu((e?.pageX || pageX || 0) - 15, (e?.pageY || pageY || 0) - 15, undefined, undefined, onDisplay); - } - else { - onDisplay(); - } - return; - } - - const customScripts = Cast(this.props.Document.contextMenuScripts, listSpec(ScriptField), []); - StrListCast(this.Document.contextMenuLabels).forEach((label, i) => - cm.addItem({ description: label, event: () => customScripts[i]?.script.run({ documentView: this, this: this.layoutDoc, scriptContext: this.props.scriptContext, self: this.rootDoc }), icon: "sticky-note" })); - this.props.contextMenuItems?.().forEach(item => - item.label && cm.addItem({ description: item.label, event: () => item.script.script.run({ this: this.layoutDoc, scriptContext: this.props.scriptContext, self: this.rootDoc }), icon: item.icon as IconProp })); - - if (!this.props.Document.isFolder) { - const templateDoc = Cast(this.props.Document[StrCast(this.props.Document.layoutKey)], Doc, null); - const appearance = cm.findByDescription("UI Controls..."); - const appearanceItems: ContextMenuProps[] = appearance && "subitems" in appearance ? appearance.subitems : []; - !Doc.UserDoc().noviceMode && templateDoc && appearanceItems.push({ description: "Open Template ", event: () => this.props.addDocTab(templateDoc, "add:right"), icon: "eye" }); - !Doc.UserDoc().noviceMode && appearanceItems.push({ - description: "Add a Field", event: () => { - const alias = Doc.MakeAlias(this.rootDoc); - alias.layout = FormattedTextBox.LayoutString("newfield"); - alias.title = "newfield"; - alias._height = 35; - alias._width = 100; - alias.syncLayoutFieldWithTitle = true; - alias.x = NumCast(this.rootDoc.x) + NumCast(this.rootDoc.width); - alias.y = NumCast(this.rootDoc.y); - this.props.addDocument?.(alias); - }, icon: "eye" - }); - DocListCast(this.Document.links).length && appearanceItems.splice(0, 0, { description: `${this.layoutDoc.hideLinkButton ? "Show" : "Hide"} Link Button`, event: action(() => this.layoutDoc.hideLinkButton = !this.layoutDoc.hideLinkButton), icon: "eye" }); - !appearance && cm.addItem({ description: "UI Controls...", subitems: appearanceItems, icon: "compass" }); - - if (!Doc.IsSystem(this.rootDoc) && this.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Tree) { - !Doc.UserDoc().noviceMode && appearanceItems.splice(0, 0, { description: `${!this.layoutDoc._showAudio ? "Show" : "Hide"} Audio Button`, event: action(() => this.layoutDoc._showAudio = !this.layoutDoc._showAudio), icon: "microphone" }); - const existingOnClick = cm.findByDescription("OnClick..."); - const onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : []; - - const zorders = cm.findByDescription("ZOrder..."); - const zorderItems: ContextMenuProps[] = zorders && "subitems" in zorders ? zorders.subitems : []; - if (this.props.bringToFront !== emptyFunction) { - zorderItems.push({ description: "Bring to Front", event: () => SelectionManager.Views().forEach(dv => dv.props.bringToFront(dv.rootDoc, false)), icon: "expand-arrows-alt" }); - zorderItems.push({ description: "Send to Back", event: () => SelectionManager.Views().forEach(dv => dv.props.bringToFront(dv.rootDoc, true)), icon: "expand-arrows-alt" }); - zorderItems.push({ description: this.rootDoc._raiseWhenDragged !== false ? "Keep ZIndex when dragged" : "Allow ZIndex to change when dragged", event: undoBatch(action(() => this.rootDoc._raiseWhenDragged = this.rootDoc._raiseWhenDragged === undefined ? false : undefined)), icon: "expand-arrows-alt" }); - } - !zorders && cm.addItem({ description: "ZOrder...", noexpand: true, subitems: zorderItems, icon: "compass" }); - - !Doc.UserDoc().noviceMode && onClicks.push({ description: "Enter Portal", event: this.makeIntoPortal, icon: "window-restore" }); - !Doc.UserDoc().noviceMode && onClicks.push({ description: "Toggle Detail", event: this.setToggleDetail, icon: "concierge-bell" }); - this.props.CollectionFreeFormDocumentView && onClicks.push({ description: (this.Document.followLinkZoom ? "Don't" : "") + " zoom following link", event: () => this.Document.followLinkZoom = !this.Document.followLinkZoom, icon: this.Document.ignoreClick ? "unlock" : "lock" }); - - if (!this.Document.annotationOn) { - const options = cm.findByDescription("Options..."); - const optionItems: ContextMenuProps[] = options && "subitems" in options ? options.subitems : []; - !options && cm.addItem({ description: "Options...", subitems: optionItems, icon: "compass" }); - - onClicks.push({ description: this.Document.ignoreClick ? "Select" : "Do Nothing", event: () => this.Document.ignoreClick = !this.Document.ignoreClick, icon: this.Document.ignoreClick ? "unlock" : "lock" }); - onClicks.push({ description: this.Document.isLinkButton ? "Remove Follow Behavior" : "Follow Link in Place", event: () => this.toggleFollowLink("inPlace", true, false), icon: "link" }); - !this.Document.isLinkButton && onClicks.push({ description: "Follow Link on Right", event: () => this.toggleFollowLink("add:right", false, false), icon: "link" }); - onClicks.push({ description: this.Document.isLinkButton || this.onClickHandler ? "Remove Click Behavior" : "Follow Link", event: () => this.toggleFollowLink(undefined, false, false), icon: "link" }); - onClicks.push({ description: (this.Document.isPushpin ? "Remove" : "Make") + " Pushpin", event: () => this.toggleFollowLink(undefined, false, true), icon: "map-pin" }); - onClicks.push({ description: "Edit onClick Script", event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.props.Document, undefined, "onClick"), "edit onClick"), icon: "terminal" }); - !existingOnClick && cm.addItem({ description: "OnClick...", addDivider: true, noexpand: true, subitems: onClicks, icon: "mouse-pointer" }); - } else if (DocListCast(this.Document.links).length) { - onClicks.push({ description: "Select on Click", event: () => this.selectOnClick(), icon: "link" }); - onClicks.push({ description: "Follow Link on Click", event: () => this.followLinkOnClick(undefined, false), icon: "link" }); - onClicks.push({ description: "Toggle Link Target on Click", event: () => this.toggleTargetOnClick(), icon: "map-pin" }); - !existingOnClick && cm.addItem({ description: "OnClick...", addDivider: true, subitems: onClicks, icon: "mouse-pointer" }); - } + // the touch onContextMenu is button 0, the pointer onContextMenu is button 2 + if (e) { + if (e.button === 0 && !e.ctrlKey || e.isDefaultPrevented()) { + e.preventDefault(); + return; + } + e.preventDefault(); + e.stopPropagation(); + e.persist(); + + if (!navigator.userAgent.includes("Mozilla") && (Math.abs(this._downX - e?.clientX) > 3 || Math.abs(this._downY - e?.clientY) > 3)) { + return; + } } - const funcs: ContextMenuProps[] = []; - if (!Doc.UserDoc().noviceMode && this.layoutDoc.onDragStart) { - funcs.push({ description: "Drag an Alias", icon: "edit", event: () => this.Document.dragFactory && (this.layoutDoc.onDragStart = ScriptField.MakeFunction('getAlias(this.dragFactory)')) }); - funcs.push({ description: "Drag a Copy", icon: "edit", event: () => this.Document.dragFactory && (this.layoutDoc.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)')) }); - funcs.push({ description: "Drag Document", icon: "edit", event: () => this.layoutDoc.onDragStart = undefined }); - cm.addItem({ description: "OnDrag...", noexpand: true, subitems: funcs, icon: "asterisk" }); + const cm = ContextMenu.Instance; + if (!cm || (e as any)?.nativeEvent?.SchemaHandled) return; + + if (e && !(e.nativeEvent as any).dash) { + const onDisplay = () => setTimeout(() => { + DocumentViewInternal.SelectAfterContextMenu && !this.props.isSelected(true) && SelectionManager.SelectView(this.props.DocumentView(), false); // on a mac, the context menu is triggered on mouse down, but a YouTube video becaomes interactive when selected which means that the context menu won't show up. by delaying the selection until hopefully after the pointer up, the context menu will appear. + setTimeout(() => { + const ele = document.elementFromPoint(e.clientX, e.clientY); + simulateMouseClick(ele, e.clientX, e.clientY, e.screenX, e.screenY); + }); + }); + if (navigator.userAgent.includes("Macintosh")) { + cm.displayMenu((e?.pageX || pageX || 0) - 15, (e?.pageY || pageY || 0) - 15, undefined, undefined, onDisplay); + } + else { + onDisplay(); + } + return; } - const more = cm.findByDescription("More..."); - const moreItems = more && "subitems" in more ? more.subitems : []; - if (!Doc.IsSystem(this.rootDoc)) { - (this.rootDoc._viewType !== CollectionViewType.Docking || !Doc.UserDoc().noviceMode) && moreItems.push({ description: "Share", event: () => SharingManager.Instance.open(this.props.DocumentView()), icon: "users" }); - if (!Doc.UserDoc().noviceMode) { - moreItems.push({ description: "Make View of Metadata Field", event: () => Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.DataDoc), icon: "concierge-bell" }); - moreItems.push({ description: `${this.Document._chromeHidden ? "Show" : "Hide"} Chrome`, event: () => this.Document._chromeHidden = !this.Document._chromeHidden, icon: "project-diagram" }); - - if (Cast(Doc.GetProto(this.props.Document).data, listSpec(Doc))) { - moreItems.push({ description: "Export to Google Photos Album", event: () => GooglePhotos.Export.CollectionToAlbum({ collection: this.props.Document }).then(console.log), icon: "caret-square-right" }); - moreItems.push({ description: "Tag Child Images via Google Photos", event: () => GooglePhotos.Query.TagChildImages(this.props.Document), icon: "caret-square-right" }); - moreItems.push({ description: "Write Back Link to Album", event: () => GooglePhotos.Transactions.AddTextEnrichment(this.props.Document), icon: "caret-square-right" }); - } - moreItems.push({ description: "Copy ID", event: () => Utils.CopyText(Doc.globalServerPath(this.props.Document)), icon: "fingerprint" }); - } + const customScripts = Cast(this.props.Document.contextMenuScripts, listSpec(ScriptField), []); + StrListCast(this.Document.contextMenuLabels).forEach((label, i) => + cm.addItem({ description: label, event: () => customScripts[i]?.script.run({ documentView: this, this: this.layoutDoc, scriptContext: this.props.scriptContext, self: this.rootDoc }), icon: "sticky-note" })); + this.props.contextMenuItems?.().forEach(item => + item.label && cm.addItem({ description: item.label, event: () => item.script.script.run({ this: this.layoutDoc, scriptContext: this.props.scriptContext, self: this.rootDoc }), icon: item.icon as IconProp })); + + if (!this.props.Document.isFolder) { + const templateDoc = Cast(this.props.Document[StrCast(this.props.Document.layoutKey)], Doc, null); + const appearance = cm.findByDescription("UI Controls..."); + const appearanceItems: ContextMenuProps[] = appearance && "subitems" in appearance ? appearance.subitems : []; + !Doc.UserDoc().noviceMode && templateDoc && appearanceItems.push({ description: "Open Template ", event: () => this.props.addDocTab(templateDoc, "add:right"), icon: "eye" }); + !Doc.UserDoc().noviceMode && appearanceItems.push({ + description: "Add a Field", event: () => { + const alias = Doc.MakeAlias(this.rootDoc); + alias.layout = FormattedTextBox.LayoutString("newfield"); + alias.title = "newfield"; + alias._height = 35; + alias._width = 100; + alias.syncLayoutFieldWithTitle = true; + alias.x = NumCast(this.rootDoc.x) + NumCast(this.rootDoc.width); + alias.y = NumCast(this.rootDoc.y); + this.props.addDocument?.(alias); + }, icon: "eye" + }); + DocListCast(this.Document.links).length && appearanceItems.splice(0, 0, { description: `${this.layoutDoc.hideLinkButton ? "Show" : "Hide"} Link Button`, event: action(() => this.layoutDoc.hideLinkButton = !this.layoutDoc.hideLinkButton), icon: "eye" }); + !appearance && cm.addItem({ description: "UI Controls...", subitems: appearanceItems, icon: "compass" }); + + if (!Doc.IsSystem(this.rootDoc) && this.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Tree) { + !Doc.UserDoc().noviceMode && appearanceItems.splice(0, 0, { description: `${!this.layoutDoc._showAudio ? "Show" : "Hide"} Audio Button`, event: action(() => this.layoutDoc._showAudio = !this.layoutDoc._showAudio), icon: "microphone" }); + const existingOnClick = cm.findByDescription("OnClick..."); + const onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : []; + + const zorders = cm.findByDescription("ZOrder..."); + const zorderItems: ContextMenuProps[] = zorders && "subitems" in zorders ? zorders.subitems : []; + if (this.props.bringToFront !== emptyFunction) { + zorderItems.push({ description: "Bring to Front", event: () => SelectionManager.Views().forEach(dv => dv.props.bringToFront(dv.rootDoc, false)), icon: "expand-arrows-alt" }); + zorderItems.push({ description: "Send to Back", event: () => SelectionManager.Views().forEach(dv => dv.props.bringToFront(dv.rootDoc, true)), icon: "expand-arrows-alt" }); + zorderItems.push({ description: this.rootDoc._raiseWhenDragged !== false ? "Keep ZIndex when dragged" : "Allow ZIndex to change when dragged", event: undoBatch(action(() => this.rootDoc._raiseWhenDragged = this.rootDoc._raiseWhenDragged === undefined ? false : undefined)), icon: "expand-arrows-alt" }); + } + !zorders && cm.addItem({ description: "ZOrder...", noexpand: true, subitems: zorderItems, icon: "compass" }); + + !Doc.UserDoc().noviceMode && onClicks.push({ description: "Enter Portal", event: this.makeIntoPortal, icon: "window-restore" }); + !Doc.UserDoc().noviceMode && onClicks.push({ description: "Toggle Detail", event: this.setToggleDetail, icon: "concierge-bell" }); + this.props.CollectionFreeFormDocumentView && onClicks.push({ description: (this.Document.followLinkZoom ? "Don't" : "") + " zoom following link", event: () => this.Document.followLinkZoom = !this.Document.followLinkZoom, icon: this.Document.ignoreClick ? "unlock" : "lock" }); + + if (!this.Document.annotationOn) { + const options = cm.findByDescription("Options..."); + const optionItems: ContextMenuProps[] = options && "subitems" in options ? options.subitems : []; + !options && cm.addItem({ description: "Options...", subitems: optionItems, icon: "compass" }); + + onClicks.push({ description: this.Document.ignoreClick ? "Select" : "Do Nothing", event: () => this.Document.ignoreClick = !this.Document.ignoreClick, icon: this.Document.ignoreClick ? "unlock" : "lock" }); + onClicks.push({ description: this.Document.isLinkButton ? "Remove Follow Behavior" : "Follow Link in Place", event: () => this.toggleFollowLink("inPlace", true, false), icon: "link" }); + !this.Document.isLinkButton && onClicks.push({ description: "Follow Link on Right", event: () => this.toggleFollowLink("add:right", false, false), icon: "link" }); + onClicks.push({ description: this.Document.isLinkButton || this.onClickHandler ? "Remove Click Behavior" : "Follow Link", event: () => this.toggleFollowLink(undefined, false, false), icon: "link" }); + onClicks.push({ description: (this.Document.isPushpin ? "Remove" : "Make") + " Pushpin", event: () => this.toggleFollowLink(undefined, false, true), icon: "map-pin" }); + onClicks.push({ description: "Edit onClick Script", event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.props.Document, undefined, "onClick"), "edit onClick"), icon: "terminal" }); + !existingOnClick && cm.addItem({ description: "OnClick...", addDivider: true, noexpand: true, subitems: onClicks, icon: "mouse-pointer" }); + } else if (DocListCast(this.Document.links).length) { + onClicks.push({ description: "Select on Click", event: () => this.selectOnClick(), icon: "link" }); + onClicks.push({ description: "Follow Link on Click", event: () => this.followLinkOnClick(undefined, false), icon: "link" }); + onClicks.push({ description: "Toggle Link Target on Click", event: () => this.toggleTargetOnClick(), icon: "map-pin" }); + !existingOnClick && cm.addItem({ description: "OnClick...", addDivider: true, subitems: onClicks, icon: "mouse-pointer" }); + } + } + + const funcs: ContextMenuProps[] = []; + if (!Doc.UserDoc().noviceMode && this.layoutDoc.onDragStart) { + funcs.push({ description: "Drag an Alias", icon: "edit", event: () => this.Document.dragFactory && (this.layoutDoc.onDragStart = ScriptField.MakeFunction('getAlias(this.dragFactory)')) }); + funcs.push({ description: "Drag a Copy", icon: "edit", event: () => this.Document.dragFactory && (this.layoutDoc.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)')) }); + funcs.push({ description: "Drag Document", icon: "edit", event: () => this.layoutDoc.onDragStart = undefined }); + cm.addItem({ description: "OnDrag...", noexpand: true, subitems: funcs, icon: "asterisk" }); + } + + const more = cm.findByDescription("More..."); + const moreItems = more && "subitems" in more ? more.subitems : []; + if (!Doc.IsSystem(this.rootDoc)) { + (this.rootDoc._viewType !== CollectionViewType.Docking || !Doc.UserDoc().noviceMode) && moreItems.push({ description: "Share", event: () => SharingManager.Instance.open(this.props.DocumentView()), icon: "users" }); + if (!Doc.UserDoc().noviceMode) { + moreItems.push({ description: "Make View of Metadata Field", event: () => Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.DataDoc), icon: "concierge-bell" }); + moreItems.push({ description: `${this.Document._chromeHidden ? "Show" : "Hide"} Chrome`, event: () => this.Document._chromeHidden = !this.Document._chromeHidden, icon: "project-diagram" }); + + if (Cast(Doc.GetProto(this.props.Document).data, listSpec(Doc))) { + moreItems.push({ description: "Export to Google Photos Album", event: () => GooglePhotos.Export.CollectionToAlbum({ collection: this.props.Document }).then(console.log), icon: "caret-square-right" }); + moreItems.push({ description: "Tag Child Images via Google Photos", event: () => GooglePhotos.Query.TagChildImages(this.props.Document), icon: "caret-square-right" }); + moreItems.push({ description: "Write Back Link to Album", event: () => GooglePhotos.Transactions.AddTextEnrichment(this.props.Document), icon: "caret-square-right" }); + } + moreItems.push({ description: "Copy ID", event: () => Utils.CopyText(Doc.globalServerPath(this.props.Document)), icon: "fingerprint" }); + } + } + + if (this.props.removeDocument && !Doc.IsSystem(this.rootDoc) && CurrentUserUtils.ActiveDashboard !== this.props.Document) { // need option to gray out menu items ... preferably with a '?' that explains why they're grayed out (eg., no permissions) + moreItems.push({ description: "Close", event: this.deleteClicked, icon: "times" }); + } + !more && moreItems.length && cm.addItem({ description: "More...", subitems: moreItems, icon: "compass" }); + + const help = cm.findByDescription("Help..."); + const helpItems: ContextMenuProps[] = help && "subitems" in help ? help.subitems : []; + !Doc.UserDoc().novice && helpItems.push({ description: "Show Fields ", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "add:right"), icon: "layer-group" }); + helpItems.push({ description: "Text Shortcuts Ctrl+/", event: () => this.props.addDocTab(Docs.Create.PdfDocument("/assets/cheat-sheet.pdf", { _width: 300, _height: 300 }), "add:right"), icon: "keyboard" }); + !Doc.UserDoc().novice && helpItems.push({ description: "Print Document in Console", event: () => console.log(this.props.Document), icon: "hand-point-right" }); + !Doc.UserDoc().novice && helpItems.push({ description: "Print DataDoc in Console", event: () => console.log(this.props.Document[DataSym]), icon: "hand-point-right" }); + cm.addItem({ description: "Help...", noexpand: true, subitems: helpItems, icon: "question" }); } - if (this.props.removeDocument && !Doc.IsSystem(this.rootDoc) && CurrentUserUtils.ActiveDashboard !== this.props.Document) { // need option to gray out menu items ... preferably with a '?' that explains why they're grayed out (eg., no permissions) - moreItems.push({ description: "Close", event: this.deleteClicked, icon: "times" }); - } - !more && moreItems.length && cm.addItem({ description: "More...", subitems: moreItems, icon: "compass" }); - - const help = cm.findByDescription("Help..."); - const helpItems: ContextMenuProps[] = help && "subitems" in help ? help.subitems : []; - !Doc.UserDoc().novice && helpItems.push({ description: "Show Fields ", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "add:right"), icon: "layer-group" }); - helpItems.push({ description: "Text Shortcuts Ctrl+/", event: () => this.props.addDocTab(Docs.Create.PdfDocument("/assets/cheat-sheet.pdf", { _width: 300, _height: 300 }), "add:right"), icon: "keyboard" }); - !Doc.UserDoc().novice && helpItems.push({ description: "Print Document in Console", event: () => console.log(this.props.Document), icon: "hand-point-right" }); - !Doc.UserDoc().novice && helpItems.push({ description: "Print DataDoc in Console", event: () => console.log(this.props.Document[DataSym]), icon: "hand-point-right" }); - cm.addItem({ description: "Help...", noexpand: true, subitems: helpItems, icon: "question" }); - } - - if (!this.topMost) e?.stopPropagation(); // DocumentViews should stop propagation of this event - cm.displayMenu((e?.pageX || pageX || 0) - 15, (e?.pageY || pageY || 0) - 15); - } - - collectionFilters = () => StrListCast(this.props.Document._docFilters); - collectionRangeDocFilters = () => StrListCast(this.props.Document._docRangeFilters); - @computed get showFilterIcon() { - return this.collectionFilters().length || this.collectionRangeDocFilters().length ? "hasFilter" : - this.props.docFilters?.().filter(f => Utils.IsRecursiveFilter(f)).length || this.props.docRangeFilters().length ? "inheritsFilter" : undefined; - } - rootSelected = (outsideReaction?: boolean) => this.props.isSelected(outsideReaction) || (this.props.Document.rootDocument && this.props.rootSelected?.(outsideReaction)) || false; - panelHeight = () => this.props.PanelHeight() - this.headerMargin; - screenToLocal = () => this.props.ScreenToLocalTransform().translate(0, -this.headerMargin); - contentScaling = () => this.ContentScale; - onClickFunc = () => this.onClickHandler; - setHeight = (height: number) => this.layoutDoc._height = height; - setContentView = action((view: { getAnchor?: () => Doc, forward?: () => boolean, back?: () => boolean }) => this._componentView = view); - isContentActive = (outsideReaction?: boolean) => { - return this.props.isContentActive() === false ? false : ( - CurrentUserUtils.SelectedTool !== InkTool.None || - SnappingManager.GetIsDragging() || - this.rootSelected() || - this.props.Document.forceActive || - this.props.isSelected(outsideReaction) || - this._componentView?.isAnyChildContentActive?.() || - this.props.isContentActive()) ? true : undefined; - } - @observable _retryThumb = 1; - thumbShown = () => { - return !this.props.isSelected() && LightboxView.LightboxDoc !== this.rootDoc && this.thumb && - !Doc.AreProtosEqual(DocumentLinksButton.StartLink, this.rootDoc) && - !Doc.isBrushedHighlightedDegree(this.props.Document) && - !this._componentView?.isAnyChildContentActive?.() ? true : false; - } - @computed get contents() { - TraceMobx(); - const audioView = !this.layoutDoc._showAudio ? (null) : -
- + if (!this.topMost) e?.stopPropagation(); // DocumentViews should stop propagation of this event + cm.displayMenu((e?.pageX || pageX || 0) - 15, (e?.pageY || pageY || 0) - 15); + } + + collectionFilters = () => StrListCast(this.props.Document._docFilters); + collectionRangeDocFilters = () => StrListCast(this.props.Document._docRangeFilters); + @computed get showFilterIcon() { + return this.collectionFilters().length || this.collectionRangeDocFilters().length ? "hasFilter" : + this.props.docFilters?.().filter(f => Utils.IsRecursiveFilter(f)).length || this.props.docRangeFilters().length ? "inheritsFilter" : undefined; + } + rootSelected = (outsideReaction?: boolean) => this.props.isSelected(outsideReaction) || (this.props.Document.rootDocument && this.props.rootSelected?.(outsideReaction)) || false; + panelHeight = () => this.props.PanelHeight() - this.headerMargin; + screenToLocal = () => this.props.ScreenToLocalTransform().translate(0, -this.headerMargin); + contentScaling = () => this.ContentScale; + onClickFunc = () => this.onClickHandler; + setHeight = (height: number) => this.layoutDoc._height = height; + setContentView = action((view: { getAnchor?: () => Doc, forward?: () => boolean, back?: () => boolean }) => this._componentView = view); + isContentActive = (outsideReaction?: boolean) => { + return this.props.isContentActive() === false ? false : ( + CurrentUserUtils.SelectedTool !== InkTool.None || + SnappingManager.GetIsDragging() || + this.rootSelected() || + this.props.Document.forceActive || + this.props.isSelected(outsideReaction) || + this._componentView?.isAnyChildContentActive?.() || + this.props.isContentActive()) ? true : undefined; + } + @observable _retryThumb = 1; + thumbShown = () => { + return !this.props.isSelected() && LightboxView.LightboxDoc !== this.rootDoc && this.thumb && + !Doc.AreProtosEqual(DocumentLinksButton.StartLink, this.rootDoc) && + !Doc.isBrushedHighlightedDegree(this.props.Document) && + !this._componentView?.isAnyChildContentActive?.() ? true : false; + } + @computed get contents() { + TraceMobx(); + const audioView = !this.layoutDoc._showAudio ? (null) : +
+ +
; + + return
+ {!this._retryThumb || !this.thumbShown() ? (null) : + - Doc.AreProtosEqual(link.anchor1 as Doc, this.rootDoc) || - Doc.AreProtosEqual(link.anchor2 as Doc, this.rootDoc) || - ((link.anchor1 as Doc).unrendered && Doc.AreProtosEqual((link.anchor1 as Doc).annotationOn as Doc, this.rootDoc)) || - ((link.anchor2 as Doc).unrendered && Doc.AreProtosEqual((link.anchor2 as Doc).annotationOn as Doc, this.rootDoc)) - ); - } - @computed get allLinks() { TraceMobx(); return LinkManager.Instance.getAllRelatedLinks(this.rootDoc); } - @computed get allLinkEndpoints() { // the small blue dots that mark the endpoints of links - TraceMobx(); - if (this.layoutDoc.unrendered || this.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) return null; - if (this.layoutDoc.presBox || this.rootDoc.type === DocumentType.LINK || this.props.dontRegisterView) return (null); - const filtered = DocUtils.FilterDocs(this.directLinks, this.props.docFilters?.() ?? [], []).filter(d => !d.hidden); - return filtered.map((link, i) => -
- -
); - } - - @action - onPointerEnter = () => { - const self = this; - const audioAnnos = DocListCast(this.dataDoc[this.LayoutFieldKey + "-audioAnnotations"]); - if (audioAnnos && audioAnnos.length && this._mediaState === 0) { - const anno = audioAnnos[Math.floor(Math.random() * audioAnnos.length)]; - anno.data instanceof AudioField && new Howl({ - src: [anno.data.url.href], - format: ["mp3"], - autoplay: true, - loop: false, - volume: 0.5, - onend: function () { - runInAction(() => self._mediaState = 0); - } - }); - this._mediaState = 1; - } - } - recordAudioAnnotation = () => { - let gumStream: any; - let recorder: any; - const self = this; - navigator.mediaDevices.getUserMedia({ - audio: true - }).then(function (stream) { - gumStream = stream; - recorder = new MediaRecorder(stream); - recorder.ondataavailable = async (e: any) => { - const [{ result }] = await Networking.UploadFilesToServer(e.data); - if (!(result instanceof Error)) { - const audioDoc = Docs.Create.AudioDocument(result.accessPaths.agnostic.client, { title: "audio test", _width: 200, _height: 32 }); - audioDoc.treeViewExpandedView = "layout"; - const audioAnnos = Cast(self.dataDoc[self.LayoutFieldKey + "-audioAnnotations"], listSpec(Doc)); - if (audioAnnos === undefined) { - self.dataDoc[self.LayoutFieldKey + "-audioAnnotations"] = new List([audioDoc]); - } else { - audioAnnos.push(audioDoc); - } - } - }; - runInAction(() => self._mediaState = 2); - recorder.start(); - setTimeout(() => { - recorder.stop(); - runInAction(() => self._mediaState = 0); - gumStream.getAudioTracks()[0].stop(); - }, 5000); - }); - } - - captionStyleProvider = (doc: Opt, props: Opt, property: string) => this.props?.styleProvider?.(doc, props, property + ":caption"); - @computed get innards() { - TraceMobx(); - const ffscale = () => (this.props.DocumentView().props.CollectionFreeFormDocumentView?.().props.ScreenToLocalTransform().Scale || 1); - const showTitle = this.ShowTitle?.split(":")[0]; - const showTitleHover = this.ShowTitle?.includes(":hover"); - const showCaption = !this.props.hideCaptions && this.Document._viewType !== CollectionViewType.Carousel ? StrCast(this.layoutDoc._showCaption) : undefined; - const captionView = !showCaption ? (null) : -
- -
; - const targetDoc = (showTitle?.startsWith("_") ? this.layoutDoc : this.rootDoc); - const background = StrCast(SharingManager.Instance.users.find(users => users.user.email === this.dataDoc.author)?.sharingDoc.userColor, - Doc.UserDoc().showTitle && [DocumentType.RTF, DocumentType.COL].includes(this.rootDoc.type as any) ? StrCast(Doc.SharingDoc().userColor) : "rgba(0,0,0,0.4)"); - const titleView = !showTitle ? (null) : -
- field.trim()).map(field => targetDoc[field]?.toString()).join("\\")} - display={"block"} - fontSize={10} - GetValue={() => showTitle.split(";").length === 1 ? showTitle + "=" + Field.toString(targetDoc[showTitle.split(";")[0]] as any as Field) : "#" + showTitle} - SetValue={undoBatch((input: string) => { - if (input?.startsWith("#")) { - if (this.props.showTitle) { - this.rootDoc._showTitle = input?.substring(1) ? input.substring(1) : undefined; - } else { - Doc.UserDoc().showTitle = input?.substring(1) ? input.substring(1) : "creationDate"; - } - } else { - var value = input.replace(new RegExp(showTitle + "="), "") as string | number; - if (showTitle !== "title" && Number(value).toString() === value) value = Number(value); - if (showTitle.includes("Date") || showTitle === "author") return true; - Doc.SetInPlace(targetDoc, showTitle, value, true); + } + // We need to use allrelatedLinks to get not just links to the document as a whole, but links to + // anchors that are not rendered as DocumentViews (marked as 'unrendered' with their 'annotationOn' set to this document). e.g., + // - PDF text regions are rendered as an Annotations without generating a DocumentView, ' + // - RTF selections are rendered via Prosemirror and have a mark which contains the Document ID for the annotation link + // - and links to PDF/Web docs at a certain scroll location never create an explicit view. + // For each of these, we create LinkAnchorBox's on the border of the DocumentView. + @computed get directLinks() { + TraceMobx(); return LinkManager.Instance.getAllRelatedLinks(this.rootDoc).filter(link => + Doc.AreProtosEqual(link.anchor1 as Doc, this.rootDoc) || + Doc.AreProtosEqual(link.anchor2 as Doc, this.rootDoc) || + ((link.anchor1 as Doc).unrendered && Doc.AreProtosEqual((link.anchor1 as Doc).annotationOn as Doc, this.rootDoc)) || + ((link.anchor2 as Doc).unrendered && Doc.AreProtosEqual((link.anchor2 as Doc).annotationOn as Doc, this.rootDoc)) + ); + } + @computed get allLinks() { TraceMobx(); return LinkManager.Instance.getAllRelatedLinks(this.rootDoc); } + @computed get allLinkEndpoints() { // the small blue dots that mark the endpoints of links + TraceMobx(); + if (this.layoutDoc.unrendered || this.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) return null; + if (this.layoutDoc.presBox || this.rootDoc.type === DocumentType.LINK || this.props.dontRegisterView) return (null); + const filtered = DocUtils.FilterDocs(this.directLinks, this.props.docFilters?.() ?? [], []).filter(d => !d.hidden); + return filtered.map((link, i) => +
+ +
); + } + + @action + onPointerEnter = () => { + const self = this; + const audioAnnos = DocListCast(this.dataDoc[this.LayoutFieldKey + "-audioAnnotations"]); + if (audioAnnos && audioAnnos.length && this._mediaState === 0) { + const anno = audioAnnos[Math.floor(Math.random() * audioAnnos.length)]; + anno.data instanceof AudioField && new Howl({ + src: [anno.data.url.href], + format: ["mp3"], + autoplay: true, + loop: false, + volume: 0.5, + onend: function () { + runInAction(() => self._mediaState = 0); } - return true; - })} - /> -
; - return this.props.hideTitle || (!showTitle && !showCaption) ? - this.contents : -
- {!this.headerMargin ? <> {this.contents} {titleView} : <> {titleView} {this.contents} } - {captionView} -
; - } - isHovering = () => this._isHovering; - @observable _isHovering = false; - @observable _: string = ""; - @computed get renderDoc() { - TraceMobx(); - const thumb = ImageCast(this.layoutDoc["thumb-frozen"], ImageCast(this.layoutDoc.thumb))?.url.href.replace(".png", "_m.png"); - const isButton = this.props.Document.type === DocumentType.FONTICON; - if (!(this.props.Document instanceof Doc) || GetEffectiveAcl(this.props.Document[DataSym]) === AclPrivate || (this.hidden && !this.props.treeViewDoc)) return null; - return this.docContents ?? -
this._isHovering = true)} - onPointerLeave={action(() => this._isHovering = false)} - style={{ - background: isButton || thumb ? undefined : this.backgroundColor, - opacity: this.opacity, - color: StrCast(this.layoutDoc.color, "inherit"), - fontFamily: StrCast(this.Document._fontFamily, "inherit"), - fontSize: Cast(this.Document._fontSize, "string", null), - transform: this._animateScalingTo ? `scale(${this._animateScalingTo})` : undefined, - transition: !this._animateScalingTo ? StrCast(this.Document.dataTransition) : `transform ${this._animateScaleTime / 1000}s ease-${this._animateScalingTo < 1 ? "in" : "out"}`, - }}> - - {this.innards} - {this.onClickHandler && this.props.ContainingCollectionView?.props.Document._viewType === CollectionViewType.Time ?
: (null)} - {this.widgetDecorations ?? null} -
; - } - render() { - TraceMobx(); - const highlightIndex = this.props.LayoutTemplateString ? (Doc.IsHighlighted(this.props.Document) ? 6 : 0) : Doc.isBrushedHighlightedDegree(this.props.Document); // bcz: Argh!! need to identify a tree view doc better than a LayoutTemlatString - const highlightColor = ["transparent", "rgb(68, 118, 247)", "rgb(68, 118, 247)", "orange", "lightBlue"][highlightIndex]; - const highlightStyle = ["solid", "dashed", "solid", "solid", "solid"][highlightIndex]; - const excludeTypes = !this.props.treeViewDoc && highlightIndex < 3 ? [DocumentType.FONTICON, DocumentType.INK] : [DocumentType.FONTICON]; - let highlighting = !this.props.disableDocBrushing && highlightIndex && !excludeTypes.includes(this.layoutDoc.type as any) && this.layoutDoc._viewType !== CollectionViewType.Linear; - highlighting = highlighting && this.props.focus !== emptyFunction && this.layoutDoc.title !== "[pres element template]"; // bcz: hack to turn off highlighting onsidebar panel documents. need to flag a document as not highlightable in a more direct way - - const borderPath = this.props.styleProvider?.(this.props.Document, this.props, StyleProp.BorderPath) || { path: undefined }; - const internal = PresBox.EffectsProvider(this.layoutDoc, this.renderDoc) || this.renderDoc; - const boxShadow = this.props.treeViewDoc ? null : highlighting && this.borderRounding && highlightStyle !== "dashed" ? `0 0 0 ${highlightIndex}px ${highlightColor}` : - this.boxShadow || (this.props.Document.isTemplateForField ? "black 0.2vw 0.2vw 0.8vw" : undefined); - - // Return surrounding highlight - return
!SnappingManager.GetIsDragging() && Doc.BrushDoc(this.props.Document))} - onPointerLeave={action(e => !hasDescendantTarget(e.nativeEvent.x, e.nativeEvent.y, this.ContentDiv) && Doc.UnBrushDoc(this.props.Document))} - style={{ - display: this.hidden ? "inline" : undefined, - borderRadius: this.borderRounding, - pointerEvents: this.pointerEvents, - outline: highlighting && !this.borderRounding ? `${highlightColor} ${highlightStyle} ${highlightIndex}px` : "solid 0px", - border: highlighting && this.borderRounding && highlightStyle === "dashed" ? `${highlightStyle} ${highlightColor} ${highlightIndex}px` : undefined, - boxShadow, - clipPath: borderPath.path ? `path('${borderPath.path}')` : undefined - }}> - {!borderPath.path ? internal : - <> - {/*
+ }); + this._mediaState = 1; + } + } + recordAudioAnnotation = () => { + let gumStream: any; + let recorder: any; + const self = this; + navigator.mediaDevices.getUserMedia({ + audio: true + }).then(function (stream) { + gumStream = stream; + recorder = new MediaRecorder(stream); + recorder.ondataavailable = async (e: any) => { + const [{ result }] = await Networking.UploadFilesToServer(e.data); + if (!(result instanceof Error)) { + const audioDoc = Docs.Create.AudioDocument(result.accessPaths.agnostic.client, { title: "audio test", _width: 200, _height: 32 }); + audioDoc.treeViewExpandedView = "layout"; + const audioAnnos = Cast(self.dataDoc[self.LayoutFieldKey + "-audioAnnotations"], listSpec(Doc)); + if (audioAnnos === undefined) { + self.dataDoc[self.LayoutFieldKey + "-audioAnnotations"] = new List([audioDoc]); + } else { + audioAnnos.push(audioDoc); + } + } + }; + runInAction(() => self._mediaState = 2); + recorder.start(); + setTimeout(() => { + recorder.stop(); + runInAction(() => self._mediaState = 0); + gumStream.getAudioTracks()[0].stop(); + }, 5000); + }); + } + + captionStyleProvider = (doc: Opt, props: Opt, property: string) => this.props?.styleProvider?.(doc, props, property + ":caption"); + @computed get innards() { + TraceMobx(); + const ffscale = () => (this.props.DocumentView().props.CollectionFreeFormDocumentView?.().props.ScreenToLocalTransform().Scale || 1); + const showTitle = this.ShowTitle?.split(":")[0]; + const showTitleHover = this.ShowTitle?.includes(":hover"); + const showCaption = !this.props.hideCaptions && this.Document._viewType !== CollectionViewType.Carousel ? StrCast(this.layoutDoc._showCaption) : undefined; + const captionView = !showCaption ? (null) : +
+ +
; + const targetDoc = (showTitle?.startsWith("_") ? this.layoutDoc : this.rootDoc); + const background = StrCast(SharingManager.Instance.users.find(users => users.user.email === this.dataDoc.author)?.sharingDoc.userColor, + Doc.UserDoc().showTitle && [DocumentType.RTF, DocumentType.COL].includes(this.rootDoc.type as any) ? StrCast(Doc.SharingDoc().userColor) : "rgba(0,0,0,0.4)"); + const titleView = !showTitle ? (null) : +
+ field.trim()).map(field => targetDoc[field]?.toString()).join("\\")} + display={"block"} + fontSize={10} + GetValue={() => showTitle.split(";").length === 1 ? showTitle + "=" + Field.toString(targetDoc[showTitle.split(";")[0]] as any as Field) : "#" + showTitle} + SetValue={undoBatch((input: string) => { + if (input?.startsWith("#")) { + if (this.props.showTitle) { + this.rootDoc._showTitle = input?.substring(1) ? input.substring(1) : undefined; + } else { + Doc.UserDoc().showTitle = input?.substring(1) ? input.substring(1) : "creationDate"; + } + } else { + var value = input.replace(new RegExp(showTitle + "="), "") as string | number; + if (showTitle !== "title" && Number(value).toString() === value) value = Number(value); + if (showTitle.includes("Date") || showTitle === "author") return true; + Doc.SetInPlace(targetDoc, showTitle, value, true); + } + return true; + })} + /> +
; + return this.props.hideTitle || (!showTitle && !showCaption) ? + this.contents : +
+ {!this.headerMargin ? <> {this.contents} {titleView} : <> {titleView} {this.contents} } + {captionView} +
; + } + isHovering = () => this._isHovering; + @observable _isHovering = false; + @observable _: string = ""; + @computed get renderDoc() { + TraceMobx(); + const thumb = ImageCast(this.layoutDoc["thumb-frozen"], ImageCast(this.layoutDoc.thumb))?.url.href.replace(".png", "_m.png"); + const isButton = this.props.Document.type === DocumentType.FONTICON; + if (!(this.props.Document instanceof Doc) || GetEffectiveAcl(this.props.Document[DataSym]) === AclPrivate || (this.hidden && !this.props.treeViewDoc)) return null; + return this.docContents ?? +
this._isHovering = true)} + onPointerLeave={action(() => this._isHovering = false)} + style={{ + background: isButton || thumb ? undefined : this.backgroundColor, + opacity: this.opacity, + color: StrCast(this.layoutDoc.color, "inherit"), + fontFamily: StrCast(this.Document._fontFamily, "inherit"), + fontSize: Cast(this.Document._fontSize, "string", null), + transform: this._animateScalingTo ? `scale(${this._animateScalingTo})` : undefined, + transition: !this._animateScalingTo ? StrCast(this.Document.dataTransition) : `transform ${this._animateScaleTime / 1000}s ease-${this._animateScalingTo < 1 ? "in" : "out"}`, + }}> + + {this.innards} + {this.onClickHandler && this.props.ContainingCollectionView?.props.Document._viewType === CollectionViewType.Time ?
: (null)} + {this.widgetDecorations ?? null} +
; + } + render() { + TraceMobx(); + const highlightIndex = this.props.LayoutTemplateString ? (Doc.IsHighlighted(this.props.Document) ? 6 : 0) : Doc.isBrushedHighlightedDegree(this.props.Document); // bcz: Argh!! need to identify a tree view doc better than a LayoutTemlatString + const highlightColor = ["transparent", "rgb(68, 118, 247)", "rgb(68, 118, 247)", "orange", "lightBlue"][highlightIndex]; + const highlightStyle = ["solid", "dashed", "solid", "solid", "solid"][highlightIndex]; + const excludeTypes = !this.props.treeViewDoc && highlightIndex < 3 ? [DocumentType.FONTICON, DocumentType.INK] : [DocumentType.FONTICON]; + let highlighting = !this.props.disableDocBrushing && highlightIndex && !excludeTypes.includes(this.layoutDoc.type as any) && this.layoutDoc._viewType !== CollectionViewType.Linear; + highlighting = highlighting && this.props.focus !== emptyFunction && this.layoutDoc.title !== "[pres element template]"; // bcz: hack to turn off highlighting onsidebar panel documents. need to flag a document as not highlightable in a more direct way + + const borderPath = this.props.styleProvider?.(this.props.Document, this.props, StyleProp.BorderPath) || { path: undefined }; + const internal = PresBox.EffectsProvider(this.layoutDoc, this.renderDoc) || this.renderDoc; + const boxShadow = this.props.treeViewDoc ? null : highlighting && this.borderRounding && highlightStyle !== "dashed" ? `0 0 0 ${highlightIndex}px ${highlightColor}` : + this.boxShadow || (this.props.Document.isTemplateForField ? "black 0.2vw 0.2vw 0.8vw" : undefined); + + // Return surrounding highlight + return
!SnappingManager.GetIsDragging() && Doc.BrushDoc(this.props.Document))} + onPointerLeave={action(e => !hasDescendantTarget(e.nativeEvent.x, e.nativeEvent.y, this.ContentDiv) && Doc.UnBrushDoc(this.props.Document))} + style={{ + display: this.hidden ? "inline" : undefined, + borderRadius: this.borderRounding, + pointerEvents: this.pointerEvents, + outline: highlighting && !this.borderRounding ? `${highlightColor} ${highlightStyle} ${highlightIndex}px` : "solid 0px", + border: highlighting && this.borderRounding && highlightStyle === "dashed" ? `${highlightStyle} ${highlightColor} ${highlightIndex}px` : undefined, + boxShadow, + clipPath: borderPath.path ? `path('${borderPath.path}')` : undefined + }}> + {!borderPath.path ? internal : + <> + {/*
{internal}
*/} - {internal} -
- - - -
- - } - {this.showFilterIcon ? - { this.props.select(false); CurrentUserUtils.propertiesWidth = 250; e.stopPropagation(); })} - /> - : (null)} -
; - } + {internal} +
+ + + +
+ + } + {this.showFilterIcon ? + { this.props.select(false); CurrentUserUtils.propertiesWidth = 250; e.stopPropagation(); })} + /> + : (null)} +
; + } } @observer export class DocumentView extends React.Component { - public static ROOT_DIV = "documentView-effectsWrapper"; - public get displayName() { return "DocumentView(" + this.props.Document?.title + ")"; } // this makes mobx trace() statements more descriptive - public ContentRef = React.createRef(); - private _disposers: { [name: string]: IReactionDisposer } = {}; - - public static showBackLinks(linkSource: Doc) { - const docid = Doc.CurrentUserEmail + Doc.GetProto(linkSource)[Id] + "-pivotish"; - DocServer.GetRefField(docid).then(docx => { - const rootAlias = () => { - const rootAlias = Doc.MakeAlias(linkSource); - rootAlias.x = rootAlias.y = 0; - return rootAlias; - }; - const linkCollection = ((docx instanceof Doc) && docx) || Docs.Create.StackingDocument([/*rootAlias()*/], { title: linkSource.title + "-pivot", _width: 500, _height: 500, }, docid); - linkCollection.linkSource = linkSource; - if (!linkCollection.reactionScript) linkCollection.reactionScript = ScriptField.MakeScript("updateLinkCollection(self)"); - LightboxView.SetLightboxDoc(linkCollection); - }); - } - - @observable public docView: DocumentViewInternal | undefined | null; - - get Document() { return this.props.Document; } - get topMost() { return this.props.renderDepth === 0; } - get rootDoc() { return this.docView?.rootDoc || this.Document; } - get dataDoc() { return this.docView?.dataDoc || this.Document; } - get finalLayoutKey() { return this.docView?.finalLayoutKey || "layout"; } - get ContentDiv() { return this.docView?.ContentDiv; } - get ComponentView() { return this.docView?._componentView; } - get allLinks() { return this.docView?.allLinks || []; } - get LayoutFieldKey() { return this.docView?.LayoutFieldKey || "layout"; } - get fitWidth() { return this.props.fitWidth?.(this.rootDoc) || this.layoutDoc.fitWidth; } - - @computed get docViewPath(): DocumentView[] { return this.props.docViewPath ? [...this.props.docViewPath(), this] : [this]; } - @computed get layoutDoc() { return Doc.Layout(this.Document, this.props.LayoutTemplate?.()); } - @computed get nativeWidth() { - return this.docView?._componentView?.reverseNativeScaling?.() ? 0 : - returnVal(this.props.NativeWidth?.(), Doc.NativeWidth(this.layoutDoc, this.props.DataDoc, this.props.freezeDimensions)); - } - @computed get nativeHeight() { - return this.docView?._componentView?.reverseNativeScaling?.() ? 0 : - returnVal(this.props.NativeHeight?.(), Doc.NativeHeight(this.layoutDoc, this.props.DataDoc, this.props.freezeDimensions)); - } - @computed get shouldNotScale() { - return (this.fitWidth && !this.nativeWidth) || - this.props.dontScaleFilter?.(this.Document) || - this.props.treeViewDoc || [CollectionViewType.Docking].includes(this.Document._viewType as any); - } - @computed get effectiveNativeWidth() { return this.shouldNotScale ? 0 : (this.nativeWidth || NumCast(this.layoutDoc.width)); } - @computed get effectiveNativeHeight() { return this.shouldNotScale ? 0 : (this.nativeHeight || NumCast(this.layoutDoc.height)); } - @computed get nativeScaling() { - if (this.shouldNotScale) return 1; - const minTextScale = this.Document.type === DocumentType.RTF ? 0.1 : 0; - if (this.fitWidth || this.props.PanelHeight() / (this.effectiveNativeHeight || 1) > this.props.PanelWidth() / (this.effectiveNativeWidth || 1)) { - return Math.max(minTextScale, this.props.PanelWidth() / (this.effectiveNativeWidth || 1)); // width-limited or fitWidth - } - return Math.max(minTextScale, this.props.PanelHeight() / (this.effectiveNativeHeight || 1)); // height-limited or unscaled - } - - @computed get panelWidth() { return this.effectiveNativeWidth ? this.effectiveNativeWidth * this.nativeScaling : this.props.PanelWidth(); } - @computed get panelHeight() { - if (this.effectiveNativeHeight) { - return Math.min(this.props.PanelHeight(), Math.max(this.ComponentView?.getScrollHeight?.() ?? NumCast(this.layoutDoc.scrollHeight), this.effectiveNativeHeight) * this.nativeScaling); - } - return this.props.PanelHeight(); - } - @computed get Xshift() { return this.effectiveNativeWidth ? (this.props.PanelWidth() - this.effectiveNativeWidth * this.nativeScaling) / 2 : 0; } - @computed get Yshift() { return this.effectiveNativeWidth && this.effectiveNativeHeight && Math.abs(this.Xshift) < 0.001 ? (this.props.PanelHeight() - this.effectiveNativeHeight * this.nativeScaling) / 2 : 0; } - @computed get centeringX() { return this.props.dontCenter?.includes("x") ? 0 : this.Xshift; } - @computed get centeringY() { return this.fitWidth || this.props.dontCenter?.includes("y") ? 0 : this.Yshift; } - - toggleNativeDimensions = () => this.docView && Doc.toggleNativeDimensions(this.layoutDoc, this.docView.ContentScale, this.props.PanelWidth(), this.props.PanelHeight()); - focus = (doc: Doc, options?: DocFocusOptions) => this.docView?.focus(doc, options); - getBounds = () => { - if (!this.docView || !this.docView.ContentDiv || this.docView.props.treeViewDoc || Doc.AreProtosEqual(this.props.Document, Doc.UserDoc())) { - return undefined; - } - const xf = (this.docView?.props.ScreenToLocalTransform().scale(this.nativeScaling)).inverse(); - const [[left, top], [right, bottom]] = [xf.transformPoint(0, 0), xf.transformPoint(this.panelWidth, this.panelHeight)]; - if (this.docView.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) { - const docuBox = this.docView.ContentDiv.getElementsByClassName("linkAnchorBox-cont"); - if (docuBox.length) return { ...docuBox[0].getBoundingClientRect(), center: undefined }; - } - return { left, top, right, bottom, center: this.ComponentView?.getCenter?.(xf) }; - } - - public iconify(finished?: () => void) { - this.ComponentView?.updateIcon?.(); - const layoutKey = Cast(this.Document.layoutKey, "string", null); - if (layoutKey !== "layout_icon") { - this.switchViews(true, "icon", finished); - if (layoutKey && layoutKey !== "layout" && layoutKey !== "layout_icon") this.Document.deiconifyLayout = layoutKey.replace("layout_", ""); - } else { - const deiconifyLayout = Cast(this.Document.deiconifyLayout, "string", null); - this.switchViews(deiconifyLayout ? true : false, deiconifyLayout, finished); - this.Document.deiconifyLayout = undefined; - this.props.bringToFront(this.rootDoc); - } - } - @undoBatch - @action - setCustomView = (custom: boolean, layout: string): void => { - Doc.setNativeView(this.props.Document); - custom && DocUtils.makeCustomViewClicked(this.props.Document, Docs.Create.StackingDocument, layout, undefined); - } - switchViews = action((custom: boolean, view: string, finished?: () => void) => { - this.docView && (this.docView._animateScalingTo = 0.1); // shrink doc - setTimeout(action(() => { - this.setCustomView(custom, view); - this.docView && (this.docView._animateScalingTo = 1); // expand it + public static ROOT_DIV = "documentView-effectsWrapper"; + public get displayName() { return "DocumentView(" + this.props.Document?.title + ")"; } // this makes mobx trace() statements more descriptive + public ContentRef = React.createRef(); + private _disposers: { [name: string]: IReactionDisposer } = {}; + + public static showBackLinks(linkSource: Doc) { + const docid = Doc.CurrentUserEmail + Doc.GetProto(linkSource)[Id] + "-pivotish"; + DocServer.GetRefField(docid).then(docx => { + const rootAlias = () => { + const rootAlias = Doc.MakeAlias(linkSource); + rootAlias.x = rootAlias.y = 0; + return rootAlias; + }; + const linkCollection = ((docx instanceof Doc) && docx) || Docs.Create.StackingDocument([/*rootAlias()*/], { title: linkSource.title + "-pivot", _width: 500, _height: 500, }, docid); + linkCollection.linkSource = linkSource; + if (!linkCollection.reactionScript) linkCollection.reactionScript = ScriptField.MakeScript("updateLinkCollection(self)"); + LightboxView.SetLightboxDoc(linkCollection); + }); + } + + @observable public docView: DocumentViewInternal | undefined | null; + + get Document() { return this.props.Document; } + get topMost() { return this.props.renderDepth === 0; } + get rootDoc() { return this.docView?.rootDoc || this.Document; } + get dataDoc() { return this.docView?.dataDoc || this.Document; } + get finalLayoutKey() { return this.docView?.finalLayoutKey || "layout"; } + get ContentDiv() { return this.docView?.ContentDiv; } + get ComponentView() { return this.docView?._componentView; } + get allLinks() { return this.docView?.allLinks || []; } + get LayoutFieldKey() { return this.docView?.LayoutFieldKey || "layout"; } + get fitWidth() { return this.props.fitWidth?.(this.rootDoc) || this.layoutDoc.fitWidth; } + + @computed get docViewPath(): DocumentView[] { return this.props.docViewPath ? [...this.props.docViewPath(), this] : [this]; } + @computed get layoutDoc() { return Doc.Layout(this.Document, this.props.LayoutTemplate?.()); } + @computed get nativeWidth() { + return this.docView?._componentView?.reverseNativeScaling?.() ? 0 : + returnVal(this.props.NativeWidth?.(), Doc.NativeWidth(this.layoutDoc, this.props.DataDoc, this.props.freezeDimensions)); + } + @computed get nativeHeight() { + return this.docView?._componentView?.reverseNativeScaling?.() ? 0 : + returnVal(this.props.NativeHeight?.(), Doc.NativeHeight(this.layoutDoc, this.props.DataDoc, this.props.freezeDimensions)); + } + @computed get shouldNotScale() { + return (this.fitWidth && !this.nativeWidth) || + this.props.dontScaleFilter?.(this.Document) || + this.props.treeViewDoc || [CollectionViewType.Docking].includes(this.Document._viewType as any); + } + @computed get effectiveNativeWidth() { return this.shouldNotScale ? 0 : (this.nativeWidth || NumCast(this.layoutDoc.width)); } + @computed get effectiveNativeHeight() { return this.shouldNotScale ? 0 : (this.nativeHeight || NumCast(this.layoutDoc.height)); } + @computed get nativeScaling() { + if (this.shouldNotScale) return 1; + const minTextScale = this.Document.type === DocumentType.RTF ? 0.1 : 0; + if (this.fitWidth || this.props.PanelHeight() / (this.effectiveNativeHeight || 1) > this.props.PanelWidth() / (this.effectiveNativeWidth || 1)) { + return Math.max(minTextScale, this.props.PanelWidth() / (this.effectiveNativeWidth || 1)); // width-limited or fitWidth + } + return Math.max(minTextScale, this.props.PanelHeight() / (this.effectiveNativeHeight || 1)); // height-limited or unscaled + } + + @computed get panelWidth() { return this.effectiveNativeWidth ? this.effectiveNativeWidth * this.nativeScaling : this.props.PanelWidth(); } + @computed get panelHeight() { + if (this.effectiveNativeHeight) { + return Math.min(this.props.PanelHeight(), Math.max(this.ComponentView?.getScrollHeight?.() ?? NumCast(this.layoutDoc.scrollHeight), this.effectiveNativeHeight) * this.nativeScaling); + } + return this.props.PanelHeight(); + } + @computed get Xshift() { return this.effectiveNativeWidth ? (this.props.PanelWidth() - this.effectiveNativeWidth * this.nativeScaling) / 2 : 0; } + @computed get Yshift() { return this.effectiveNativeWidth && this.effectiveNativeHeight && Math.abs(this.Xshift) < 0.001 ? (this.props.PanelHeight() - this.effectiveNativeHeight * this.nativeScaling) / 2 : 0; } + @computed get centeringX() { return this.props.dontCenter?.includes("x") ? 0 : this.Xshift; } + @computed get centeringY() { return this.fitWidth || this.props.dontCenter?.includes("y") ? 0 : this.Yshift; } + + toggleNativeDimensions = () => this.docView && Doc.toggleNativeDimensions(this.layoutDoc, this.docView.ContentScale, this.props.PanelWidth(), this.props.PanelHeight()); + focus = (doc: Doc, options?: DocFocusOptions) => this.docView?.focus(doc, options); + getBounds = () => { + if (!this.docView || !this.docView.ContentDiv || this.docView.props.treeViewDoc || Doc.AreProtosEqual(this.props.Document, Doc.UserDoc())) { + return undefined; + } + const xf = (this.docView?.props.ScreenToLocalTransform().scale(this.nativeScaling)).inverse(); + const [[left, top], [right, bottom]] = [xf.transformPoint(0, 0), xf.transformPoint(this.panelWidth, this.panelHeight)]; + if (this.docView.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) { + const docuBox = this.docView.ContentDiv.getElementsByClassName("linkAnchorBox-cont"); + if (docuBox.length) return { ...docuBox[0].getBoundingClientRect(), center: undefined }; + } + return { left, top, right, bottom, center: this.ComponentView?.getCenter?.(xf) }; + } + + public iconify(finished?: () => void) { + this.ComponentView?.updateIcon?.(); + const layoutKey = Cast(this.Document.layoutKey, "string", null); + if (layoutKey !== "layout_icon") { + this.switchViews(true, "icon", finished); + if (layoutKey && layoutKey !== "layout" && layoutKey !== "layout_icon") this.Document.deiconifyLayout = layoutKey.replace("layout_", ""); + } else { + const deiconifyLayout = Cast(this.Document.deiconifyLayout, "string", null); + this.switchViews(deiconifyLayout ? true : false, deiconifyLayout, finished); + this.Document.deiconifyLayout = undefined; + this.props.bringToFront(this.rootDoc); + } + } + @undoBatch + @action + setCustomView = (custom: boolean, layout: string): void => { + Doc.setNativeView(this.props.Document); + custom && DocUtils.makeCustomViewClicked(this.props.Document, Docs.Create.StackingDocument, layout, undefined); + } + switchViews = action((custom: boolean, view: string, finished?: () => void) => { + this.docView && (this.docView._animateScalingTo = 0.1); // shrink doc setTimeout(action(() => { - this.docView && (this.docView._animateScalingTo = 0); - finished?.(); + this.setCustomView(custom, view); + this.docView && (this.docView._animateScalingTo = 1); // expand it + setTimeout(action(() => { + this.docView && (this.docView._animateScalingTo = 0); + finished?.(); + }), this.docView!._animateScaleTime - 10); }), this.docView!._animateScaleTime - 10); - }), this.docView!._animateScaleTime - 10); - }); - - startDragging = (x: number, y: number, dropAction: dropActionType, hideSource = false) => this.docView?.startDragging(x, y, dropAction, hideSource); - - docViewPathFunc = () => this.docViewPath; - isSelected = (outsideReaction?: boolean) => SelectionManager.IsSelected(this, outsideReaction); - select = (extendSelection: boolean) => SelectionManager.SelectView(this, !SelectionManager.Views().some(v => v.props.Document === this.props.ContainingCollectionDoc) && extendSelection); - NativeWidth = () => this.effectiveNativeWidth; - NativeHeight = () => this.effectiveNativeHeight; - PanelWidth = () => this.panelWidth; - PanelHeight = () => this.panelHeight; - ContentScale = () => this.nativeScaling; - selfView = () => this; - screenToLocalTransform = () => { - return this.props.ScreenToLocalTransform().translate(-this.centeringX, -this.centeringY).scale(1 / this.nativeScaling); - } - componentDidMount() { - this._disposers.reactionScript = reaction( - () => ScriptCast(this.rootDoc.reactionScript)?.script?.run({ this: this.props.Document, self: Cast(this.rootDoc, Doc, null) || this.props.Document }).result, - output => output - ); - this._disposers.height = reaction( - () => NumCast(this.layoutDoc._height), - action(height => { - const docMax = NumCast(this.layoutDoc.docMaxAutoHeight); - if (docMax && docMax < height) this.layoutDoc.docMaxAutoHeight = height; - }) - ); - !BoolCast(this.props.Document.dontRegisterView, this.props.dontRegisterView) && DocumentManager.Instance.AddView(this); - } - componentWillUnmount() { - Object.values(this._disposers).forEach(disposer => disposer?.()); - !BoolCast(this.props.Document.dontRegisterView, this.props.dontRegisterView) && DocumentManager.Instance.RemoveView(this); - } - - render() { - TraceMobx(); - const xshift = () => (this.props.Document.isInkMask ? InkingStroke.MaskDim : Math.abs(this.Xshift) <= 0.001 ? this.props.PanelWidth() : undefined); - const yshift = () => (this.props.Document.isInkMask ? InkingStroke.MaskDim : Math.abs(this.Yshift) <= 0.001 ? this.props.PanelHeight() : undefined); - const isPresTreeElement: boolean = this.props.treeViewDoc?.type === DocumentType.PRES; - const isButton: boolean = this.props.Document.type === DocumentType.FONTICON || this.props.Document._viewType === CollectionViewType.Linear; - return (
- {!this.props.Document || !this.props.PanelWidth() ? (null) : ( -
- r && (this.docView = r))} /> -
)} -
); - } + }); + + startDragging = (x: number, y: number, dropAction: dropActionType, hideSource = false) => this.docView?.startDragging(x, y, dropAction, hideSource); + + docViewPathFunc = () => this.docViewPath; + isSelected = (outsideReaction?: boolean) => SelectionManager.IsSelected(this, outsideReaction); + select = (extendSelection: boolean) => SelectionManager.SelectView(this, !SelectionManager.Views().some(v => v.props.Document === this.props.ContainingCollectionDoc) && extendSelection); + NativeWidth = () => this.effectiveNativeWidth; + NativeHeight = () => this.effectiveNativeHeight; + PanelWidth = () => this.panelWidth; + PanelHeight = () => this.panelHeight; + ContentScale = () => this.nativeScaling; + selfView = () => this; + screenToLocalTransform = () => { + return this.props.ScreenToLocalTransform().translate(-this.centeringX, -this.centeringY).scale(1 / this.nativeScaling); + } + componentDidMount() { + this._disposers.reactionScript = reaction( + () => ScriptCast(this.rootDoc.reactionScript)?.script?.run({ this: this.props.Document, self: Cast(this.rootDoc, Doc, null) || this.props.Document }).result, + output => output + ); + this._disposers.height = reaction( + () => NumCast(this.layoutDoc._height), + action(height => { + const docMax = NumCast(this.layoutDoc.docMaxAutoHeight); + if (docMax && docMax < height) this.layoutDoc.docMaxAutoHeight = height; + }) + ); + !BoolCast(this.props.Document.dontRegisterView, this.props.dontRegisterView) && DocumentManager.Instance.AddView(this); + } + componentWillUnmount() { + Object.values(this._disposers).forEach(disposer => disposer?.()); + !BoolCast(this.props.Document.dontRegisterView, this.props.dontRegisterView) && DocumentManager.Instance.RemoveView(this); + } + + render() { + TraceMobx(); + const xshift = () => (this.props.Document.isInkMask ? InkingStroke.MaskDim : Math.abs(this.Xshift) <= 0.001 ? this.props.PanelWidth() : undefined); + const yshift = () => (this.props.Document.isInkMask ? InkingStroke.MaskDim : Math.abs(this.Yshift) <= 0.001 ? this.props.PanelHeight() : undefined); + const isPresTreeElement: boolean = this.props.treeViewDoc?.type === DocumentType.PRES; + const isButton: boolean = this.props.Document.type === DocumentType.FONTICON || this.props.Document._viewType === CollectionViewType.Linear; + return (
+ {!this.props.Document || !this.props.PanelWidth() ? (null) : ( +
+ r && (this.docView = r))} /> +
)} +
); + } } export function deiconifyViewFunc(documentView: DocumentView) { - documentView.iconify(); - //StrCast(doc.layoutKey).split("_")[1] === "icon" && setNativeView(doc); + documentView.iconify(); + //StrCast(doc.layoutKey).split("_")[1] === "icon" && setNativeView(doc); } ScriptingGlobals.add(function deiconifyView(documentView: DocumentView) { - documentView.iconify(); - documentView.select(false); + documentView.iconify(); + documentView.select(false); }); ScriptingGlobals.add(function toggleDetail(dv: DocumentView, detailLayoutKeySuffix: string) { - if (dv.Document.layoutKey === "layout_" + detailLayoutKeySuffix) dv.switchViews(false, "layout"); - else dv.switchViews(true, detailLayoutKeySuffix); + if (dv.Document.layoutKey === "layout_" + detailLayoutKeySuffix) dv.switchViews(false, "layout"); + else dv.switchViews(true, detailLayoutKeySuffix); }); ScriptingGlobals.add(function updateLinkCollection(linkCollection: Doc) { - const linkSource = Cast(linkCollection.linkSource, Doc, null); - const collectedLinks = DocListCast(Doc.GetProto(linkCollection).data); - let wid = linkSource[WidthSym](); - const links = DocListCast(linkSource.links); - links.forEach(link => { - const other = LinkManager.getOppositeAnchor(link, linkSource); - const otherdoc = !other ? undefined : other.annotationOn ? Cast(other.annotationOn, Doc, null) : other; - if (otherdoc && !collectedLinks?.some(d => Doc.AreProtosEqual(d, otherdoc))) { - const alias = Doc.MakeAlias(otherdoc); - alias.x = wid; - alias.y = 0; - alias._lockedPosition = false; - wid += otherdoc[WidthSym](); - Doc.AddDocToList(Doc.GetProto(linkCollection), "data", alias); - } - }); - return links; + const linkSource = Cast(linkCollection.linkSource, Doc, null); + const collectedLinks = DocListCast(Doc.GetProto(linkCollection).data); + let wid = linkSource[WidthSym](); + const links = DocListCast(linkSource.links); + links.forEach(link => { + const other = LinkManager.getOppositeAnchor(link, linkSource); + const otherdoc = !other ? undefined : other.annotationOn ? Cast(other.annotationOn, Doc, null) : other; + if (otherdoc && !collectedLinks?.some(d => Doc.AreProtosEqual(d, otherdoc))) { + const alias = Doc.MakeAlias(otherdoc); + alias.x = wid; + alias.y = 0; + alias._lockedPosition = false; + wid += otherdoc[WidthSym](); + Doc.AddDocToList(Doc.GetProto(linkCollection), "data", alias); + } + }); + return links; }); \ No newline at end of file diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx index 87716e9cc..5826abd65 100644 --- a/src/client/views/nodes/RecordingBox/RecordingView.tsx +++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx @@ -9,6 +9,7 @@ import { Networking } from '../../../Network'; import { Upload } from '../../../../server/SharedMediaTypes'; import { RecordingApi } from '../../../util/RecordingApi'; +import { emptyFunction, returnFalse, setupMoveUpEvents } from '../../../../Utils'; interface MediaSegment { videoChunks: any[], @@ -193,12 +194,15 @@ export function RecordingView(props: IRecordingViewProps) { } } - const startOrResume = () => { - if (!videoRecorder.current || videoRecorder.current.state === "inactive") { - record(); - } else if (videoRecorder.current.state === "paused") { - videoRecorder.current.resume(); - } + const startOrResume = (e: React.PointerEvent) => { + // the code to start or resume does not get triggered if we start dragging the button + setupMoveUpEvents({}, e, returnFalse, returnFalse, () => { + if (!videoRecorder.current || videoRecorder.current.state === "inactive") { + record(); + } else if (videoRecorder.current.state === "paused") { + videoRecorder.current.resume(); + } + }) } const clearPrevious = () => { @@ -240,8 +244,8 @@ export function RecordingView(props: IRecordingViewProps) {
{recording ? -
diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx index 99fc61cde..083ca5bea 100644 --- a/src/client/views/nodes/trails/PresElementBox.tsx +++ b/src/client/views/nodes/trails/PresElementBox.tsx @@ -51,11 +51,11 @@ export class PresElementBox extends ViewBoxBaseComponent() { componentWillUnmount() { this._heightDisposer?.(); } - - @action - presExpandDocumentClick = () => { + + @action + presExpandDocumentClick = () => { this.rootDoc.presExpandInlineButton = !this.rootDoc.presExpandInlineButton; - } + } /** * Returns a local transformed coordinate array for given coordinates. @@ -324,7 +324,7 @@ export class PresElementBox extends ViewBoxBaseComponent() { } else { // if we dont have any recording - const recording = Docs.Create.WebCamDocument("", { _width: 400, _height: 200, title: "recording", cloneFieldFilter: new List(["system"]) }); + const recording = Docs.Create.WebCamDocument("", { _width: 400, _height: 200, hideDocumentButtonBar: true, title: "recording", cloneFieldFilter: new List(["system"]) }); // attach the recording to the slide, and attach the slide to the recording recording.slides = activeItem -- cgit v1.2.3-70-g09d2 From d3c89f90d376310cd583ae1c6ac4a7eb4c5a03ac Mon Sep 17 00:00:00 2001 From: bobzel Date: Tue, 7 Jun 2022 13:39:17 -0400 Subject: fixed dragging items in overlayView to not invalidate DocumentView and cause a rerender. fixed loss of ability to select or drag after dragging a recording. --- src/Utils.ts | 10 +- src/client/util/DragManager.ts | 4 - src/client/views/OverlayView.tsx | 13 +- .../views/nodes/RecordingBox/RecordingBox.tsx | 85 ++-- .../views/nodes/RecordingBox/RecordingView.tsx | 491 +++++++++++---------- 5 files changed, 305 insertions(+), 298 deletions(-) (limited to 'src') diff --git a/src/Utils.ts b/src/Utils.ts index 6f3a31b49..07e95d438 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -698,6 +698,7 @@ export function setupMoveUpEvents( (target as any)._lastTap = Date.now(); (target as any)._downX = (target as any)._lastX = e.clientX; (target as any)._downY = (target as any)._lastY = e.clientY; + (target as any)._noClick = false; const _moveEvent = (e: PointerEvent): void => { if (Math.abs(e.clientX - (target as any)._downX) > Utils.DRAG_THRESHOLD || Math.abs(e.clientY - (target as any)._downY) > Utils.DRAG_THRESHOLD) { @@ -729,17 +730,20 @@ export function setupMoveUpEvents( clearTimeout((target as any)._doubleTime); (target as any)._doubleTime = undefined; } - clickEvent(e, (target as any)._doubleTap); + (target as any)._noClick = clickEvent(e, (target as any)._doubleTap); } document.removeEventListener("pointermove", _moveEvent); document.removeEventListener("pointerup", _upEvent); }; + const _clickEvent = (e: MouseEvent): void => { + if ((target as any)._noClick) e.stopPropagation(); + document.removeEventListener("click", _clickEvent, true); + } if (stopPropagation) { e.stopPropagation(); e.preventDefault(); } - document.removeEventListener("pointermove", _moveEvent); - document.removeEventListener("pointerup", _upEvent); document.addEventListener("pointermove", _moveEvent); document.addEventListener("pointerup", _upEvent); + document.addEventListener("click", _clickEvent, true); } \ No newline at end of file diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index f438a8c11..9f8c49081 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -210,10 +210,6 @@ export namespace DragManager { options?: DragOptions, dropEvent?: () => any ) { - // stop an 'accidental' on-click drag that may have occured if the user is in presenting mode - // note: dragData.dropAction is only undefined when the element itself being dragged without being selected - if (Doc.UserDoc()?.presentationMode === 'recording' && dragData.dropAction === undefined) return false; - const addAudioTag = (dropDoc: any) => { dropDoc && !dropDoc.creationDate && (dropDoc.creationDate = new DateField); dropDoc instanceof Doc && DocUtils.MakeLinkToActiveAudio(() => dropDoc); diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx index 9d970c06f..6796db51e 100644 --- a/src/client/views/OverlayView.tsx +++ b/src/client/views/OverlayView.tsx @@ -1,5 +1,7 @@ +import { docs } from "googleapis/build/src/apis/docs"; import { action, computed, observable } from "mobx"; import { observer } from "mobx-react"; +import { computedFn } from "mobx-utils"; import * as React from "react"; import ReactLoading from 'react-loading'; import { Doc, WidthSym, HeightSym } from "../../fields/Doc"; @@ -146,6 +148,12 @@ export class OverlayView extends React.Component { return remove; } + removeOverlayDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).map(doc => Doc.RemoveDocFromList((Doc.UserDoc().myOverlayDocs as Doc), "data", doc)).length ? true : false; + + + docScreenToLocalXf = computedFn(function docScreenToLocalXf(this: any, doc: Doc) { + return () => new Transform(-NumCast(doc.x), -NumCast(doc.y), 1); + }.bind(this)); @computed get overlayDocs() { return CurrentUserUtils.OverlayDocs?.map(d => { @@ -179,17 +187,16 @@ export class OverlayView extends React.Component { offsetx = NumCast(d.x) - e.clientX; offsety = NumCast(d.y) - e.clientY; }; - return
(doc instanceof Doc ? [doc] : doc).map(doc => Doc.RemoveDocFromList((Doc.UserDoc().myOverlayDocs as Doc), "data", doc)).length ? true : false} + removeDocument={this.removeOverlayDoc} PanelWidth={d[WidthSym]} PanelHeight={d[HeightSym]} - ScreenToLocalTransform={() => new Transform(-NumCast(d.x), -NumCast(d.y), 1)} + ScreenToLocalTransform={this.docScreenToLocalXf(d)} renderDepth={1} isDocumentActive={returnTrue} isContentActive={emptyFunction} diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx index 159271223..eac1c63f9 100644 --- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx +++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx @@ -16,47 +16,46 @@ import { Id } from "../../../../fields/FieldSymbols"; @observer export class RecordingBox extends ViewBoxBaseComponent() { - public static LayoutString(fieldKey: string) { return FieldView.LayoutString(RecordingBox, fieldKey); } - - private _ref: React.RefObject = React.createRef(); - - constructor(props: any) { - super(props); - } - - componentDidMount() { - console.log("set native width and height") - Doc.SetNativeWidth(this.dataDoc, 1280); - Doc.SetNativeHeight(this.dataDoc, 720); - } - - @observable result: Upload.FileInformation | undefined = undefined - @observable videoDuration: number | undefined = undefined - - @action - setVideoDuration = (duration: number) => { - this.videoDuration = duration - } - - @action - setResult = (info: Upload.FileInformation, trackScreen: boolean) => { - this.result = info - this.dataDoc.type = DocumentType.VID; - this.dataDoc[this.fieldKey + "-duration"] = this.videoDuration; - - this.dataDoc.layout = VideoBox.LayoutString(this.fieldKey); - this.dataDoc[this.props.fieldKey] = new VideoField(this.result.accessPaths.agnostic.client); - this.dataDoc[this.fieldKey + "-recorded"] = true; - // stringify the presenation and store it - if (trackScreen) { - this.dataDoc[this.fieldKey + "-presentation"] = JSON.stringify(RecordingApi.Instance.clear()); - } - } - - render() { - // console.log("Proto[Is]: ", this.rootDoc.proto?.[Id]) - return
- {!this.result && } -
; - } + public static LayoutString(fieldKey: string) { return FieldView.LayoutString(RecordingBox, fieldKey); } + + private _ref: React.RefObject = React.createRef(); + + constructor(props: any) { + super(props); + } + + componentDidMount() { + console.log("set native width and height") + Doc.SetNativeWidth(this.dataDoc, 1280); + Doc.SetNativeHeight(this.dataDoc, 720); + } + + @observable result: Upload.FileInformation | undefined = undefined + @observable videoDuration: number | undefined = undefined + + @action + setVideoDuration = (duration: number) => { + this.videoDuration = duration + } + + @action + setResult = (info: Upload.FileInformation, trackScreen: boolean) => { + this.result = info + this.dataDoc.type = DocumentType.VID; + this.dataDoc[this.fieldKey + "-duration"] = this.videoDuration; + + this.dataDoc.layout = VideoBox.LayoutString(this.fieldKey); + this.dataDoc[this.props.fieldKey] = new VideoField(this.result.accessPaths.agnostic.client); + this.dataDoc[this.fieldKey + "-recorded"] = true; + // stringify the presenation and store it + if (trackScreen) { + this.dataDoc[this.fieldKey + "-presentation"] = JSON.stringify(RecordingApi.Instance.clear()); + } + } + + render() { + return
+ {!this.result && } +
; + } } diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx index 5826abd65..b95335792 100644 --- a/src/client/views/nodes/RecordingBox/RecordingView.tsx +++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx @@ -9,273 +9,274 @@ import { Networking } from '../../../Network'; import { Upload } from '../../../../server/SharedMediaTypes'; import { RecordingApi } from '../../../util/RecordingApi'; -import { emptyFunction, returnFalse, setupMoveUpEvents } from '../../../../Utils'; +import { emptyFunction, returnFalse, returnTrue, setupMoveUpEvents } from '../../../../Utils'; interface MediaSegment { - videoChunks: any[], - endTime: number + videoChunks: any[], + endTime: number } interface IRecordingViewProps { - setResult: (info: Upload.FileInformation, trackScreen: boolean) => void - setDuration: (seconds: number) => void - id: string + setResult: (info: Upload.FileInformation, trackScreen: boolean) => void + setDuration: (seconds: number) => void + id: string } const MAXTIME = 100000; export function RecordingView(props: IRecordingViewProps) { - const [recording, setRecording] = useState(false); - const recordingTimerRef = useRef(0); - const [recordingTimer, setRecordingTimer] = useState(0); // unit is 0.01 second - const [playing, setPlaying] = useState(false); - const [progress, setProgress] = useState(0); - - const [videos, setVideos] = useState([]); - const videoRecorder = useRef(null); - const videoElementRef = useRef(null); - - const [finished, setFinished] = useState(false) - const [trackScreen, setTrackScreen] = useState(true) - - - - const DEFAULT_MEDIA_CONSTRAINTS = { - video: { - width: 1280, - height: 720, - }, - audio: { - echoCancellation: true, - noiseSuppression: true, - sampleRate: 44100 - } - } - - useEffect(() => { - - if (finished) { - props.setDuration(recordingTimer * 100) - let allVideoChunks: any = [] - videos.forEach((vid) => { - console.log(vid.videoChunks) - allVideoChunks = allVideoChunks.concat(vid.videoChunks) - }) - - const videoFile = new File(allVideoChunks, "video.mkv", { type: allVideoChunks[0].type, lastModified: Date.now() }); - - Networking.UploadFilesToServer(videoFile) - .then((data) => { - const result = data[0].result - if (!(result instanceof Error)) { // convert this screenshotBox into normal videoBox - props.setResult(result, trackScreen) - } else { - alert("video conversion failed"); - } - }) - - } - - - }, [finished]) - - useEffect(() => { - // check if the browser supports media devices on first load - if (!navigator.mediaDevices) { - console.log('This browser does not support getUserMedia.') - } - console.log('This device has the correct media devices.') - }, []) - - useEffect(() => { - // get access to the video element on every render - videoElementRef.current = document.getElementById(`video-${props.id}`) as HTMLVideoElement; - }) - - useEffect(() => { - let interval: any = null; - if (recording) { - interval = setInterval(() => { - setRecordingTimer(unit => unit + 1); - }, 10); - } else if (!recording && recordingTimer !== 0) { - clearInterval(interval); - } - return () => clearInterval(interval); - }, [recording]) - - useEffect(() => { - setVideoProgressHelper(recordingTimer) - recordingTimerRef.current = recordingTimer; - }, [recordingTimer]) - - const setVideoProgressHelper = (progress: number) => { - const newProgress = (progress / MAXTIME) * 100; - setProgress(newProgress) - } - const startShowingStream = async (mediaConstraints = DEFAULT_MEDIA_CONSTRAINTS) => { - const stream = await navigator.mediaDevices.getUserMedia(mediaConstraints) - - videoElementRef.current!.src = "" - videoElementRef.current!.srcObject = stream - videoElementRef.current!.muted = true - - return stream - } - - const record = async () => { - const stream = await startShowingStream(); - videoRecorder.current = new MediaRecorder(stream) - - // temporary chunks of video - let videoChunks: any = [] - - videoRecorder.current.ondataavailable = (event: any) => { - if (event.data.size > 0) { - videoChunks.push(event.data) - } - } - - videoRecorder.current.onstart = (event: any) => { - setRecording(true); - trackScreen && RecordingApi.Instance.start(); - } - - videoRecorder.current.onstop = () => { - // if we have a last portion - if (videoChunks.length > 1) { - // append the current portion to the video pieces - setVideos(videos => [...videos, { videoChunks: videoChunks, endTime: recordingTimerRef.current }]) - } - - // reset the temporary chunks - videoChunks = [] - setRecording(false); - setFinished(true); - trackScreen && RecordingApi.Instance.pause(); - } - - // recording paused - videoRecorder.current.onpause = (event: any) => { - // append the current portion to the video pieces - setVideos(videos => [...videos, { videoChunks: videoChunks, endTime: recordingTimerRef.current }]) + const [recording, setRecording] = useState(false); + const recordingTimerRef = useRef(0); + const [recordingTimer, setRecordingTimer] = useState(0); // unit is 0.01 second + const [playing, setPlaying] = useState(false); + const [progress, setProgress] = useState(0); + + const [videos, setVideos] = useState([]); + const videoRecorder = useRef(null); + const videoElementRef = useRef(null); + + const [finished, setFinished] = useState(false) + const [trackScreen, setTrackScreen] = useState(true) + + + + const DEFAULT_MEDIA_CONSTRAINTS = { + video: { + width: 1280, + height: 720, + }, + audio: { + echoCancellation: true, + noiseSuppression: true, + sampleRate: 44100 + } + } + + useEffect(() => { + + if (finished) { + props.setDuration(recordingTimer * 100) + let allVideoChunks: any = [] + videos.forEach((vid) => { + console.log(vid.videoChunks) + allVideoChunks = allVideoChunks.concat(vid.videoChunks) + }) - // reset the temporary chunks - videoChunks = [] - setRecording(false); - trackScreen && RecordingApi.Instance.pause(); + const videoFile = new File(allVideoChunks, "video.mkv", { type: allVideoChunks[0].type, lastModified: Date.now() }); + + Networking.UploadFilesToServer(videoFile) + .then((data) => { + const result = data[0].result + if (!(result instanceof Error)) { // convert this screenshotBox into normal videoBox + props.setResult(result, trackScreen) + } else { + alert("video conversion failed"); + } + }) + + } + + + }, [finished]) + + useEffect(() => { + // check if the browser supports media devices on first load + if (!navigator.mediaDevices) { + console.log('This browser does not support getUserMedia.') + } + console.log('This device has the correct media devices.') + }, []) + + useEffect(() => { + // get access to the video element on every render + videoElementRef.current = document.getElementById(`video-${props.id}`) as HTMLVideoElement; + }) + + useEffect(() => { + let interval: any = null; + if (recording) { + interval = setInterval(() => { + setRecordingTimer(unit => unit + 1); + }, 10); + } else if (!recording && recordingTimer !== 0) { + clearInterval(interval); + } + return () => clearInterval(interval); + }, [recording]) + + useEffect(() => { + setVideoProgressHelper(recordingTimer) + recordingTimerRef.current = recordingTimer; + }, [recordingTimer]) + + const setVideoProgressHelper = (progress: number) => { + const newProgress = (progress / MAXTIME) * 100; + setProgress(newProgress) + } + const startShowingStream = async (mediaConstraints = DEFAULT_MEDIA_CONSTRAINTS) => { + const stream = await navigator.mediaDevices.getUserMedia(mediaConstraints) + + videoElementRef.current!.src = "" + videoElementRef.current!.srcObject = stream + videoElementRef.current!.muted = true + + return stream + } + + const record = async () => { + const stream = await startShowingStream(); + videoRecorder.current = new MediaRecorder(stream) + + // temporary chunks of video + let videoChunks: any = [] + + videoRecorder.current.ondataavailable = (event: any) => { + if (event.data.size > 0) { + videoChunks.push(event.data) } - - videoRecorder.current.onresume = async (event: any) => { - await startShowingStream(); - setRecording(true); - trackScreen && RecordingApi.Instance.resume(); + } + + videoRecorder.current.onstart = (event: any) => { + setRecording(true); + trackScreen && RecordingApi.Instance.start(); + } + + videoRecorder.current.onstop = () => { + // if we have a last portion + if (videoChunks.length > 1) { + // append the current portion to the video pieces + setVideos(videos => [...videos, { videoChunks: videoChunks, endTime: recordingTimerRef.current }]) } - videoRecorder.current.start(200) - } - - - const stop = () => { - if (videoRecorder.current) { - if (videoRecorder.current.state !== "inactive") { - videoRecorder.current.stop(); - // recorder.current.stream.getTracks().forEach((track: any) => track.stop()) - } + // reset the temporary chunks + videoChunks = [] + setRecording(false); + setFinished(true); + trackScreen && RecordingApi.Instance.pause(); + } + + // recording paused + videoRecorder.current.onpause = (event: any) => { + // append the current portion to the video pieces + setVideos(videos => [...videos, { videoChunks: videoChunks, endTime: recordingTimerRef.current }]) + + // reset the temporary chunks + videoChunks = [] + setRecording(false); + trackScreen && RecordingApi.Instance.pause(); + } + + videoRecorder.current.onresume = async (event: any) => { + await startShowingStream(); + setRecording(true); + trackScreen && RecordingApi.Instance.resume(); + } + + videoRecorder.current.start(200) + } + + + const stop = () => { + if (videoRecorder.current) { + if (videoRecorder.current.state !== "inactive") { + videoRecorder.current.stop(); + // recorder.current.stream.getTracks().forEach((track: any) => track.stop()) } - } + } + } - const pause = () => { - if (videoRecorder.current) { - if (videoRecorder.current.state === "recording") { - videoRecorder.current.pause(); - } + const pause = () => { + if (videoRecorder.current) { + if (videoRecorder.current.state === "recording") { + videoRecorder.current.pause(); } - } - - const startOrResume = (e: React.PointerEvent) => { - // the code to start or resume does not get triggered if we start dragging the button - setupMoveUpEvents({}, e, returnFalse, returnFalse, () => { - if (!videoRecorder.current || videoRecorder.current.state === "inactive") { - record(); - } else if (videoRecorder.current.state === "paused") { - videoRecorder.current.resume(); - } - }) - } - - const clearPrevious = () => { - const numVideos = videos.length - setRecordingTimer(numVideos == 1 ? 0 : videos[numVideos - 2].endTime) - setVideoProgressHelper(numVideos == 1 ? 0 : videos[numVideos - 2].endTime) - setVideos(videos.filter((_, idx) => idx !== numVideos - 1)); - } - - const handleOnTimeUpdate = () => { - if (playing) { - setVideoProgressHelper(videoElementRef.current!.currentTime) + } + } + + const startOrResume = (e: React.PointerEvent) => { + // the code to start or resume does not get triggered if we start dragging the button + setupMoveUpEvents({}, e, returnTrue, returnFalse, e => { + if (!videoRecorder.current || videoRecorder.current.state === "inactive") { + record(); + } else if (videoRecorder.current.state === "paused") { + videoRecorder.current.resume(); } - }; - - const millisecondToMinuteSecond = (milliseconds: number) => { - const toTwoDigit = (digit: number) => { - return String(digit).length == 1 ? "0" + digit : digit - } - const minutes = Math.floor((milliseconds % (1000 * 60 * 60)) / (1000 * 60)); - const seconds = Math.floor((milliseconds % (1000 * 60)) / 1000); - return toTwoDigit(minutes) + " : " + toTwoDigit(seconds); - } - - return ( -
-
-
) } \ No newline at end of file -- cgit v1.2.3-70-g09d2 From c402d2a68670c50e91bc1a67c58c9537ce8ebdcc Mon Sep 17 00:00:00 2001 From: Jenny Yu Date: Tue, 7 Jun 2022 23:42:15 -0700 Subject: fix: recording box appear on bottom right corner, removed document decorations --- src/client/views/DocumentDecorations.tsx | 23 ++++++++++------------- src/client/views/nodes/DocumentView.tsx | 2 ++ src/client/views/nodes/trails/PresElementBox.tsx | 17 ++++++++++++----- 3 files changed, 24 insertions(+), 18 deletions(-) (limited to 'src') diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index bc4fec3f0..0e9bf50fc 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -468,14 +468,6 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P return SelectionManager.Views().length > 1 ? "-multiple-" : "-unset-"; } - @computed get canDelete() { - return SelectionManager.Views().some(docView => { - if (docView.rootDoc.stayInCollection) return false; - const collectionAcl = docView.props.ContainingCollectionView ? GetEffectiveAcl(docView.props.ContainingCollectionDoc?.[DataSym]) : AclEdit; - //return (!docView.rootDoc._stayInCollection || docView.rootDoc.isInkMask) && - return (collectionAcl === AclAdmin || collectionAcl === AclEdit || GetEffectiveAcl(docView.rootDoc) === AclAdmin); - }); - } @computed get hasIcons() { return SelectionManager.Views().some(docView => docView.rootDoc.layoutKey === "layout_icon"); } @@ -491,8 +483,13 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P const hideTitle = seldoc.props.hideDecorationTitle || seldoc.rootDoc.hideDecorationTitle; const hideDocumentButtonBar = seldoc.props.hideDocumentButtonBar || seldoc.rootDoc.hideDocumentButtonBar; // if multiple documents have been opened at the same time, then don't show open button - const canOpen = SelectionManager.Views().some(docView => !docView.props.Document._stayInCollection && !docView.props.Document.isGroup && !docView.props.Document.hideOpenButton); - const canDelete = this.canDelete; + const hideOpenButton = seldoc.props.hideOpenButton || seldoc.rootDoc.hideOpenButton || + SelectionManager.Views().some(docView => docView.props.Document._stayInCollection || docView.props.Document.isGroup || docView.props.Document.hideOpenButton); + const hideDeleteButton = seldoc.props.hideDeleteButton || seldoc.rootDoc.hideDeleteButton || + SelectionManager.Views().some(docView => { + const collectionAcl = docView.props.ContainingCollectionView ? GetEffectiveAcl(docView.props.ContainingCollectionDoc?.[DataSym]) : AclEdit; + return docView.rootDoc.stayInCollection || (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) => ( {title}
} placement="top">
e.preventDefault()} @@ -502,7 +499,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P ); const colorScheme = StrCast(CurrentUserUtils.ActiveDashboard?.colorScheme); - const titleArea = hideTitle ?
: + const titleArea = hideTitle ? (null) : this._edtingTitle ? - {!canDelete ?
: topBtn("close", this.hasIcons ? "times" : "window-maximize", undefined, e => this.onCloseClick(this.hasIcons ? true : undefined), "Close")} + {hideDeleteButton ?
: topBtn("close", this.hasIcons ? "times" : "window-maximize", undefined, e => this.onCloseClick(this.hasIcons ? true : undefined), "Close")} {titleArea} - {!canOpen ? (null) : topBtn("open", "external-link-alt", this.onMaximizeDown, undefined, "Open in Tab (ctrl: as alias, shift: in new collection)")} + {hideOpenButton ? (null) : topBtn("open", "external-link-alt", this.onMaximizeDown, undefined, "Open in Tab (ctrl: as alias, shift: in new collection)")} {hideResizers ? (null) : <>
e.preventDefault()} /> diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 96c989e77..ddadde52a 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -164,6 +164,8 @@ export interface DocumentViewProps extends DocumentViewSharedProps { hideTitle?: boolean; // forces suppression of title. e.g, treeView document labels suppress titles in case they are globally active via settings hideDecorationTitle?: boolean; // forces suppression of title. e.g, treeView document labels suppress titles in case they are globally active via settings hideDocumentButtonBar?: boolean; + hideOpenButton?: boolean; + hideDeleteButton?: boolean; treeViewDoc?: Doc; isDocumentActive?: () => boolean | undefined; // whether a document should handle pointer events isContentActive: () => boolean | undefined; // whether document contents should handle pointer events diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx index 083ca5bea..f1144b32d 100644 --- a/src/client/views/nodes/trails/PresElementBox.tsx +++ b/src/client/views/nodes/trails/PresElementBox.tsx @@ -324,16 +324,23 @@ export class PresElementBox extends ViewBoxBaseComponent() { } else { // if we dont have any recording - const recording = Docs.Create.WebCamDocument("", { _width: 400, _height: 200, hideDocumentButtonBar: true, title: "recording", cloneFieldFilter: new List(["system"]) }); + const recording = Docs.Create.WebCamDocument("", { + _width: 400, _height: 200, + hideDocumentButtonBar: true, + hideDecorationTitle: true, + hideOpenButton: true, + hideDeleteButton: true, + cloneFieldFilter: + new List(["system"]) + }); // attach the recording to the slide, and attach the slide to the recording recording.slides = activeItem activeItem.recording = recording - // TODO: Figure out exactly where we want the video to appear - const pt = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); - recording.x = pt[0]; - recording.y = pt[1]; + // make recording box appear in the bottom right corner of the screen + recording.x = window.innerWidth - recording._width - 20; + recording.y = window.innerHeight - recording._height - 20; Doc.AddDocToList((Doc.UserDoc().myOverlayDocs as Doc), undefined, recording); } } -- cgit v1.2.3-70-g09d2 From 58c2085ceb20cd2527ac69088efb7a46b20f8434 Mon Sep 17 00:00:00 2001 From: Jenny Yu Date: Wed, 8 Jun 2022 00:49:12 -0700 Subject: styling: pres element box --- src/client/views/DocumentDecorations.tsx | 44 +- src/client/views/collections/TabDocView.tsx | 904 +++++++++++----------- src/client/views/nodes/trails/PresElementBox.scss | 338 ++++---- src/client/views/nodes/trails/PresElementBox.tsx | 20 +- 4 files changed, 656 insertions(+), 650 deletions(-) (limited to 'src') diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 0e9bf50fc..b666b5977 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -153,29 +153,29 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P _deleteAfterIconify = false; _iconifyBatch: UndoManager.Batch | undefined; onCloseClick = (forceDeleteOrIconify: boolean | undefined) => { - if (this.canDelete) { - const views = SelectionManager.Views().slice().filter(v => v); - if (forceDeleteOrIconify === false && this._iconifyBatch) return; - this._deleteAfterIconify = forceDeleteOrIconify || this._iconifyBatch ? true : false; - if (!this._iconifyBatch) { - this._iconifyBatch = UndoManager.StartBatch("iconifying"); - } else { - forceDeleteOrIconify = false; // can't force immediate close in the middle of iconifying -- have to wait until iconifying completes - } - var iconifyingCount = views.length; - const finished = action((force?: boolean) => { - if ((force || --iconifyingCount === 0) && this._iconifyBatch) { - if (this._deleteAfterIconify) { - views.forEach(iconView => iconView.props.removeDocument?.(iconView.props.Document)); - SelectionManager.DeselectAll(); - } - this._iconifyBatch?.end(); - this._iconifyBatch = undefined; - } - }); - if (forceDeleteOrIconify) finished(forceDeleteOrIconify); - else if (!this._deleteAfterIconify) views.forEach(dv => dv.iconify(finished)); + + const views = SelectionManager.Views().slice().filter(v => v); + if (forceDeleteOrIconify === false && this._iconifyBatch) return; + this._deleteAfterIconify = forceDeleteOrIconify || this._iconifyBatch ? true : false; + if (!this._iconifyBatch) { + this._iconifyBatch = UndoManager.StartBatch("iconifying"); + } else { + forceDeleteOrIconify = false; // can't force immediate close in the middle of iconifying -- have to wait until iconifying completes } + var iconifyingCount = views.length; + const finished = action((force?: boolean) => { + if ((force || --iconifyingCount === 0) && this._iconifyBatch) { + if (this._deleteAfterIconify) { + views.forEach(iconView => iconView.props.removeDocument?.(iconView.props.Document)); + SelectionManager.DeselectAll(); + } + this._iconifyBatch?.end(); + this._iconifyBatch = undefined; + } + }); + if (forceDeleteOrIconify) finished(forceDeleteOrIconify); + else if (!this._deleteAfterIconify) views.forEach(dv => dv.iconify(finished)); + } onMaximizeDown = (e: React.PointerEvent) => { setupMoveUpEvents(this, e, () => { diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx index 2b78b20ea..39add0534 100644 --- a/src/client/views/collections/TabDocView.tsx +++ b/src/client/views/collections/TabDocView.tsx @@ -38,490 +38,490 @@ import React = require("react"); const _global = (window /* browser */ || global /* node */) as any; interface TabDocViewProps { - documentId: FieldId; - glContainer: any; + documentId: FieldId; + glContainer: any; } @observer export class TabDocView extends React.Component { - _mainCont: HTMLDivElement | null = null; - _tabReaction: IReactionDisposer | undefined; - @observable _activated: boolean = false; - @observable _panelWidth = 0; - @observable _panelHeight = 0; - @observable _isActive: boolean = false; - @observable _document: Doc | undefined; - @observable _view: DocumentView | undefined; + _mainCont: HTMLDivElement | null = null; + _tabReaction: IReactionDisposer | undefined; + @observable _activated: boolean = false; + @observable _panelWidth = 0; + @observable _panelHeight = 0; + @observable _isActive: boolean = false; + @observable _document: Doc | undefined; + @observable _view: DocumentView | undefined; - @computed get layoutDoc() { return this._document && Doc.Layout(this._document); } - @computed get tabColor() { return StrCast(this._document?._backgroundColor, StrCast(this._document?.backgroundColor, DefaultStyleProvider(this._document, undefined, StyleProp.BackgroundColor))); } - @computed get tabTextColor() { return this._document?.type === DocumentType.PRES ? "black" : StrCast(this._document?._color, StrCast(this._document?.color, DefaultStyleProvider(this._document, undefined, StyleProp.Color))); } - // @computed get renderBounds() { - // const bounds = this._document ? Cast(this._document._renderContentBounds, listSpec("number"), [0, 0, this.returnMiniSize(), this.returnMiniSize()]) : [0, 0, 0, 0]; - // const xbounds = bounds[2] - bounds[0]; - // const ybounds = bounds[3] - bounds[1]; - // const dim = Math.max(xbounds, ybounds); - // return { l: bounds[0] + xbounds / 2 - dim / 2, t: bounds[1] + ybounds / 2 - dim / 2, cx: bounds[0] + xbounds / 2, cy: bounds[1] + ybounds / 2, dim }; - // } + @computed get layoutDoc() { return this._document && Doc.Layout(this._document); } + @computed get tabColor() { return StrCast(this._document?._backgroundColor, StrCast(this._document?.backgroundColor, DefaultStyleProvider(this._document, undefined, StyleProp.BackgroundColor))); } + @computed get tabTextColor() { return this._document?.type === DocumentType.PRES ? "black" : StrCast(this._document?._color, StrCast(this._document?.color, DefaultStyleProvider(this._document, undefined, StyleProp.Color))); } + // @computed get renderBounds() { + // const bounds = this._document ? Cast(this._document._renderContentBounds, listSpec("number"), [0, 0, this.returnMiniSize(), this.returnMiniSize()]) : [0, 0, 0, 0]; + // const xbounds = bounds[2] - bounds[0]; + // const ybounds = bounds[3] - bounds[1]; + // const dim = Math.max(xbounds, ybounds); + // return { l: bounds[0] + xbounds / 2 - dim / 2, t: bounds[1] + ybounds / 2 - dim / 2, cx: bounds[0] + xbounds / 2, cy: bounds[1] + ybounds / 2, dim }; + // } - get stack() { return (this.props as any).glContainer.parent.parent; } - get tab() { return (this.props as any).glContainer.tab; } - get view() { return this._view; } + get stack() { return (this.props as any).glContainer.parent.parent; } + get tab() { return (this.props as any).glContainer.tab; } + get view() { return this._view; } - @action - init = (tab: any, doc: Opt) => { - if (tab.contentItem === tab.header.parent.getActiveContentItem()) this._activated = true; - if (tab.DashDoc !== doc && doc && tab.hasOwnProperty("contentItem") && tab.contentItem.config.type !== "stack") { - tab._disposers = {} as { [name: string]: IReactionDisposer }; - tab.contentItem.config.fixed && (tab.contentItem.parent.config.fixed = true); - tab.DashDoc = doc; - CollectionDockingView.Instance.tabMap.add(tab); - const iconType: IconProp = Doc.toIcon(doc); - // setup the title element and set its size according to the # of chars in the title. Show the full title when clicked. - const titleEle = tab.titleElement[0]; - const iconWrap = document.createElement("div"); - const closeWrap = document.createElement("div"); + @action + init = (tab: any, doc: Opt) => { + if (tab.contentItem === tab.header.parent.getActiveContentItem()) this._activated = true; + if (tab.DashDoc !== doc && doc && tab.hasOwnProperty("contentItem") && tab.contentItem.config.type !== "stack") { + tab._disposers = {} as { [name: string]: IReactionDisposer }; + tab.contentItem.config.fixed && (tab.contentItem.parent.config.fixed = true); + tab.DashDoc = doc; + CollectionDockingView.Instance.tabMap.add(tab); + const iconType: IconProp = Doc.toIcon(doc); + // setup the title element and set its size according to the # of chars in the title. Show the full title when clicked. + const titleEle = tab.titleElement[0]; + const iconWrap = document.createElement("div"); + const closeWrap = document.createElement("div"); - titleEle.size = StrCast(doc.title).length + 3; - titleEle.value = doc.title; - titleEle.onkeydown = (e: KeyboardEvent) => { - e.stopPropagation(); - }; - titleEle.onchange = undoBatch(action((e: any) => { - titleEle.size = e.currentTarget.value.length + 3; - Doc.GetProto(doc).title = e.currentTarget.value; - })); + titleEle.size = StrCast(doc.title).length + 3; + titleEle.value = doc.title; + titleEle.onkeydown = (e: KeyboardEvent) => { + e.stopPropagation(); + }; + titleEle.onchange = undoBatch(action((e: any) => { + titleEle.size = e.currentTarget.value.length + 3; + Doc.GetProto(doc).title = e.currentTarget.value; + })); - const dragBtnDown = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, e => !e.defaultPrevented && DragManager.StartDocumentDrag([iconWrap], new DragManager.DocumentDragData([doc], doc.dropAction as dropActionType), e.clientX, e.clientY), returnFalse, emptyFunction); - }; + const dragBtnDown = (e: React.PointerEvent) => { + setupMoveUpEvents(this, e, e => !e.defaultPrevented && DragManager.StartDocumentDrag([iconWrap], new DragManager.DocumentDragData([doc], doc.dropAction as dropActionType), e.clientX, e.clientY), returnFalse, emptyFunction); + }; - if (tab.element[0].children[1].children.length === 1) { - iconWrap.className = "lm_iconWrap"; - iconWrap.id = "lm_iconWrap"; - closeWrap.className = "lm_iconWrap"; - closeWrap.id = "lm_closeWrap"; - closeWrap.onclick = (e: MouseEvent) => { - tab.header.parent.contentItem.remove(); - Doc.AddDocToList(CurrentUserUtils.MyHeaderBarDoc, "data", tab.DashDoc); - Doc.AddDocToList(CurrentUserUtils.MyRecentlyClosed, "data", tab.DashDoc, undefined, true, true); - }; - const docIcon = ; - const closeIcon = ; - ReactDOM.render(docIcon, iconWrap); - ReactDOM.render(closeIcon, closeWrap); - // tab.element[0].append(closeWrap); - tab.element[0].prepend(iconWrap); - tab._disposers.layerDisposer = reaction(() => ({ layer: tab.DashDoc.activeLayer, color: this.tabColor }), - ({ layer, color }) => { - // console.log("TabDocView: " + this.tabColor); - // console.log("lightOrDark: " + lightOrDark(this.tabColor)); - const textColor = lightOrDark(this.tabColor); //not working with StyleProp.Color - titleEle.style.color = textColor; - titleEle.style.backgroundColor = "transparent"; - iconWrap.style.color = textColor; - closeWrap.style.color = textColor; - moreInfoDrag.style.backgroundColor = textColor; - tab.element[0].style.background = !layer ? color : "dimgrey"; - }, { fireImmediately: true }); - } - // shifts the focus to this tab when another tab is dragged over it - tab.element[0].onmouseenter = (e: MouseEvent) => { - if (SnappingManager.GetIsDragging() && tab.contentItem !== tab.header.parent.getActiveContentItem()) { - tab.header.parent.setActiveContentItem(tab.contentItem); - tab.setActive(true); - } - }; - + if (tab.element[0].children[1].children.length === 1) { + iconWrap.className = "lm_iconWrap"; + iconWrap.id = "lm_iconWrap"; + closeWrap.className = "lm_iconWrap"; + closeWrap.id = "lm_closeWrap"; + closeWrap.onclick = (e: MouseEvent) => { + tab.header.parent.contentItem.remove(); + Doc.AddDocToList(CurrentUserUtils.MyHeaderBarDoc, "data", tab.DashDoc); + Doc.AddDocToList(CurrentUserUtils.MyRecentlyClosed, "data", tab.DashDoc, undefined, true, true); + }; + const docIcon = ; + const closeIcon = ; + ReactDOM.render(docIcon, iconWrap); + ReactDOM.render(closeIcon, closeWrap); + // tab.element[0].append(closeWrap); + tab.element[0].prepend(iconWrap); + tab._disposers.layerDisposer = reaction(() => ({ layer: tab.DashDoc.activeLayer, color: this.tabColor }), + ({ layer, color }) => { + // console.log("TabDocView: " + this.tabColor); + // console.log("lightOrDark: " + lightOrDark(this.tabColor)); + const textColor = lightOrDark(this.tabColor); //not working with StyleProp.Color + titleEle.style.color = textColor; + titleEle.style.backgroundColor = "transparent"; + iconWrap.style.color = textColor; + closeWrap.style.color = textColor; + moreInfoDrag.style.backgroundColor = textColor; + tab.element[0].style.background = !layer ? color : "dimgrey"; + }, { fireImmediately: true }); + } + // shifts the focus to this tab when another tab is dragged over it + tab.element[0].onmouseenter = (e: MouseEvent) => { + if (SnappingManager.GetIsDragging() && tab.contentItem !== tab.header.parent.getActiveContentItem()) { + tab.header.parent.setActiveContentItem(tab.contentItem); + tab.setActive(true); + } + }; - // select the tab document when the tab is directly clicked and activate the tab whenver the tab document is selected - titleEle.onpointerdown = action((e: any) => { - if (e.target.className !== "lm_iconWrap") { - if (this.view) SelectionManager.SelectView(this.view, false); - else this._activated = true; - if (Date.now() - titleEle.lastClick < 1000) titleEle.select(); - titleEle.lastClick = Date.now(); - (document.activeElement !== titleEle) && titleEle.focus(); - } - }); - tab._disposers.selectionDisposer = reaction(() => SelectionManager.Views().some(v => v.topMost && v.props.Document === doc), - action((selected) => { - if (selected) this._activated = true; - const toggle = tab.element[0].children[1].children[0] as HTMLInputElement; - selected && tab.contentItem !== tab.header.parent.getActiveContentItem() && - UndoManager.RunInBatch(() => tab.header.parent.setActiveContentItem(tab.contentItem), "tab switch"); - // toggle.style.fontWeight = selected ? "bold" : ""; - // toggle.style.textTransform = selected ? "uppercase" : ""; - })); - //attach the selection doc buttons menu to the drag handle - const stack: HTMLDivElement = tab.contentItem.parent; - const header: HTMLDivElement = tab; - stack.onscroll = action((e: any) => { - console.log('scrolling...'); - }); - const moreInfoDrag = document.createElement("div"); - moreInfoDrag.className = "lm_iconWrap"; - tab._disposers.buttonDisposer = reaction(() => this.view, view => - view && [ReactDOM.render( [view]} Stack={stack} />, moreInfoDrag), tab._disposers.buttonDisposer?.()], - { fireImmediately: true }); - // tab.reactComponents = [moreInfoDrag]; - // tab.element[0].children[3].before(moreInfoDrag); + // select the tab document when the tab is directly clicked and activate the tab whenver the tab document is selected + titleEle.onpointerdown = action((e: any) => { + if (e.target.className !== "lm_iconWrap") { + if (this.view) SelectionManager.SelectView(this.view, false); + else this._activated = true; + if (Date.now() - titleEle.lastClick < 1000) titleEle.select(); + titleEle.lastClick = Date.now(); + (document.activeElement !== titleEle) && titleEle.focus(); + } + }); + tab._disposers.selectionDisposer = reaction(() => SelectionManager.Views().some(v => v.topMost && v.props.Document === doc), + action((selected) => { + if (selected) this._activated = true; + const toggle = tab.element[0].children[1].children[0] as HTMLInputElement; + selected && tab.contentItem !== tab.header.parent.getActiveContentItem() && + UndoManager.RunInBatch(() => tab.header.parent.setActiveContentItem(tab.contentItem), "tab switch"); + // toggle.style.fontWeight = selected ? "bold" : ""; + // toggle.style.textTransform = selected ? "uppercase" : ""; + })); - // highlight the tab when the tab document is brushed in any part of the UI - tab._disposers.reactionDisposer = reaction(() => ({ title: doc.title, degree: Doc.IsBrushedDegree(doc) }), ({ title, degree }) => { - //titleEle.value = title; - // titleEle.style.padding = degree ? 0 : 2; - // titleEle.style.border = `${["gray", "gray", "gray"][degree]} ${["none", "dashed", "solid"][degree]} 2px`; - }, { fireImmediately: true }); + //attach the selection doc buttons menu to the drag handle + const stack: HTMLDivElement = tab.contentItem.parent; + const header: HTMLDivElement = tab; + stack.onscroll = action((e: any) => { + console.log('scrolling...'); + }); + const moreInfoDrag = document.createElement("div"); + moreInfoDrag.className = "lm_iconWrap"; + tab._disposers.buttonDisposer = reaction(() => this.view, view => + view && [ReactDOM.render( [view]} Stack={stack} />, moreInfoDrag), tab._disposers.buttonDisposer?.()], + { fireImmediately: true }); + // tab.reactComponents = [moreInfoDrag]; + // tab.element[0].children[3].before(moreInfoDrag); - // clean up the tab when it is closed - tab.closeElement.off('click') //unbind the current click handler - .click(function () { - Object.values(tab._disposers).forEach((disposer: any) => disposer?.()); - SelectionManager.DeselectAll(); - UndoManager.RunInBatch(() => tab.contentItem.remove(), "delete tab"); - }); - } - } + // highlight the tab when the tab document is brushed in any part of the UI + tab._disposers.reactionDisposer = reaction(() => ({ title: doc.title, degree: Doc.IsBrushedDegree(doc) }), ({ title, degree }) => { + //titleEle.value = title; + // titleEle.style.padding = degree ? 0 : 2; + // titleEle.style.border = `${["gray", "gray", "gray"][degree]} ${["none", "dashed", "solid"][degree]} 2px`; + }, { fireImmediately: true }); - /** - * Adds a document to the presentation view - **/ - @action - public static async PinDoc(doc: Doc, pinProps?: PinProps) { - if (pinProps?.unpin) console.log('TODO: Remove UNPIN from this location'); - //add this new doc to props.Document - const curPres = CurrentUserUtils.ActivePresentation; - if (curPres) { - if (doc === curPres) { alert("Cannot pin presentation document to itself"); return; } - const batch = UndoManager.StartBatch("pinning doc"); - const pinDoc = Doc.MakeAlias(doc); - pinDoc.presentationTargetDoc = doc; - pinDoc.title = doc.title + " - Slide"; - pinDoc.data = new List(); // the children of the alias' layout are the presentation slide children. the alias' data field might be children of a collection, PDF data, etc -- in any case we don't want the tree view to "see" this data - pinDoc.presMovement = PresMovement.Zoom; - pinDoc.groupWithUp = false; - pinDoc.context = curPres; - // these should potentially all be props passed down by the CollectionTreeView to the TreeView elements. That way the PresBox could configure all of its children at render time - pinDoc.treeViewRenderAsBulletHeader = true; // forces a tree view to render the document next to the bullet in the header area - pinDoc.treeViewHeaderWidth = "100%"; // forces the header to grow to be the same size as its largest sibling. - pinDoc.treeViewChildrenOnRoot = true; // tree view will look for hierarchical children on the root doc, not the data doc. - pinDoc.treeViewFieldKey = "data"; // tree view will treat the 'data' field as the field where the hierarchical children are located instead of using the document's layout string field - pinDoc.treeViewExpandedView = "data";// in case the data doc has an expandedView set, this will mask that field and use the 'data' field when expanding the tree view - pinDoc.treeViewGrowsHorizontally = true;// the document expands horizontally when displayed as a tree view header - pinDoc.treeViewHideHeaderIfTemplate = true; // this will force the document to render itself as the tree view header - const presArray: Doc[] = PresBox.Instance?.sortArray(); - const size: number = PresBox.Instance?._selectedArray.size; - const presSelected: Doc | undefined = presArray && size ? presArray[size - 1] : undefined; - const duration = NumCast(doc[`${Doc.LayoutFieldKey(pinDoc)}-duration`], null); - Doc.AddDocToList(curPres, "data", pinDoc, presSelected); - if (!pinProps?.audioRange && duration !== undefined) { - pinDoc.mediaStart = "manual"; - pinDoc.mediaStop = "manual"; - pinDoc.presStartTime = NumCast(doc.clipStart); - pinDoc.presEndTime = NumCast(doc.clipEnd, duration); + // clean up the tab when it is closed + tab.closeElement.off('click') //unbind the current click handler + .click(function () { + Object.values(tab._disposers).forEach((disposer: any) => disposer?.()); + SelectionManager.DeselectAll(); + UndoManager.RunInBatch(() => tab.contentItem.remove(), "delete tab"); + }); } - //save position - if (pinProps?.setPosition || pinDoc.isInkMask) { - pinDoc.setPosition = true; - pinDoc.y = doc.y; - pinDoc.x = doc.x; - pinDoc.presHideAfter = true; - pinDoc.presHideBefore = true; - pinDoc.title = doc.title + " (move)"; - pinDoc.presMovement = PresMovement.None; - } - if (curPres.expandBoolean) pinDoc.presExpandInlineButton = true; - const dview = CollectionDockingView.Instance.props.Document; - const fieldKey = CollectionDockingView.Instance.props.fieldKey; - const sublists = DocListCast(dview[fieldKey]); - const tabs = Cast(sublists[0], Doc, null); - const tabdocs = await DocListCastAsync(tabs?.data); - runInAction(() => { - if (!pinProps?.hidePresBox && !tabdocs?.includes(curPres)) { - tabdocs?.push(curPres); // bcz: Argh! this is annoying. if multiple documents are pinned, this will get called multiple times before the presentation view is drawn. Thus it won't be in the tabdocs list and it will get created multple times. so need to explicilty add the presbox to the list of open tabs - CollectionDockingView.AddSplit(curPres, "right"); - } - PresBox.Instance?._selectedArray.clear(); - pinDoc && PresBox.Instance?._selectedArray.set(pinDoc, undefined); //Update selected array - DocumentManager.Instance.jumpToDocument(doc, false, undefined, []); - batch.end(); - }); - } - } + } - componentDidMount() { - new _global.ResizeObserver(action((entries: any) => { - for (const entry of entries) { - this._panelWidth = entry.contentRect.width; - this._panelHeight = entry.contentRect.height; + /** + * Adds a document to the presentation view + **/ + @action + public static async PinDoc(doc: Doc, pinProps?: PinProps) { + if (pinProps?.unpin) console.log('TODO: Remove UNPIN from this location'); + //add this new doc to props.Document + const curPres = CurrentUserUtils.ActivePresentation; + if (curPres) { + if (doc === curPres) { alert("Cannot pin presentation document to itself"); return; } + const batch = UndoManager.StartBatch("pinning doc"); + const pinDoc = Doc.MakeAlias(doc); + pinDoc.presentationTargetDoc = doc; + pinDoc.title = doc.title; + pinDoc.data = new List(); // the children of the alias' layout are the presentation slide children. the alias' data field might be children of a collection, PDF data, etc -- in any case we don't want the tree view to "see" this data + pinDoc.presMovement = PresMovement.Zoom; + pinDoc.groupWithUp = false; + pinDoc.context = curPres; + // these should potentially all be props passed down by the CollectionTreeView to the TreeView elements. That way the PresBox could configure all of its children at render time + pinDoc.treeViewRenderAsBulletHeader = true; // forces a tree view to render the document next to the bullet in the header area + pinDoc.treeViewHeaderWidth = "100%"; // forces the header to grow to be the same size as its largest sibling. + pinDoc.treeViewChildrenOnRoot = true; // tree view will look for hierarchical children on the root doc, not the data doc. + pinDoc.treeViewFieldKey = "data"; // tree view will treat the 'data' field as the field where the hierarchical children are located instead of using the document's layout string field + pinDoc.treeViewExpandedView = "data";// in case the data doc has an expandedView set, this will mask that field and use the 'data' field when expanding the tree view + pinDoc.treeViewGrowsHorizontally = true;// the document expands horizontally when displayed as a tree view header + pinDoc.treeViewHideHeaderIfTemplate = true; // this will force the document to render itself as the tree view header + const presArray: Doc[] = PresBox.Instance?.sortArray(); + const size: number = PresBox.Instance?._selectedArray.size; + const presSelected: Doc | undefined = presArray && size ? presArray[size - 1] : undefined; + const duration = NumCast(doc[`${Doc.LayoutFieldKey(pinDoc)}-duration`], null); + Doc.AddDocToList(curPres, "data", pinDoc, presSelected); + if (!pinProps?.audioRange && duration !== undefined) { + pinDoc.mediaStart = "manual"; + pinDoc.mediaStop = "manual"; + pinDoc.presStartTime = NumCast(doc.clipStart); + pinDoc.presEndTime = NumCast(doc.clipEnd, duration); + } + //save position + if (pinProps?.setPosition || pinDoc.isInkMask) { + pinDoc.setPosition = true; + pinDoc.y = doc.y; + pinDoc.x = doc.x; + pinDoc.presHideAfter = true; + pinDoc.presHideBefore = true; + pinDoc.title = doc.title + " (move)"; + pinDoc.presMovement = PresMovement.None; + } + if (curPres.expandBoolean) pinDoc.presExpandInlineButton = true; + const dview = CollectionDockingView.Instance.props.Document; + const fieldKey = CollectionDockingView.Instance.props.fieldKey; + const sublists = DocListCast(dview[fieldKey]); + const tabs = Cast(sublists[0], Doc, null); + const tabdocs = await DocListCastAsync(tabs?.data); + runInAction(() => { + if (!pinProps?.hidePresBox && !tabdocs?.includes(curPres)) { + tabdocs?.push(curPres); // bcz: Argh! this is annoying. if multiple documents are pinned, this will get called multiple times before the presentation view is drawn. Thus it won't be in the tabdocs list and it will get created multple times. so need to explicilty add the presbox to the list of open tabs + CollectionDockingView.AddSplit(curPres, "right"); + } + PresBox.Instance?._selectedArray.clear(); + pinDoc && PresBox.Instance?._selectedArray.set(pinDoc, undefined); //Update selected array + DocumentManager.Instance.jumpToDocument(doc, false, undefined, []); + batch.end(); + }); } - })).observe(this.props.glContainer._element[0]); - this.props.glContainer.layoutManager.on("activeContentItemChanged", this.onActiveContentItemChanged); - this.props.glContainer.tab?.isActive && this.onActiveContentItemChanged(undefined); - // this._tabReaction = reaction(() => ({ selected: this.active(), title: this.tab?.titleElement[0] }), - // ({ selected, title }) => title && (title.style.backgroundColor = selected ? "white" : ""), - // { fireImmediately: true }); - } + } - componentWillUnmount() { - this._tabReaction?.(); - this.tab && CollectionDockingView.Instance.tabMap.delete(this.tab); + componentDidMount() { + new _global.ResizeObserver(action((entries: any) => { + for (const entry of entries) { + this._panelWidth = entry.contentRect.width; + this._panelHeight = entry.contentRect.height; + } + })).observe(this.props.glContainer._element[0]); + this.props.glContainer.layoutManager.on("activeContentItemChanged", this.onActiveContentItemChanged); + this.props.glContainer.tab?.isActive && this.onActiveContentItemChanged(undefined); + // this._tabReaction = reaction(() => ({ selected: this.active(), title: this.tab?.titleElement[0] }), + // ({ selected, title }) => title && (title.style.backgroundColor = selected ? "white" : ""), + // { fireImmediately: true }); + } - this.props.glContainer.layoutManager.off("activeContentItemChanged", this.onActiveContentItemChanged); - } + componentWillUnmount() { + this._tabReaction?.(); + this.tab && CollectionDockingView.Instance.tabMap.delete(this.tab); - @action.bound - 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; - !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. - } - } + this.props.glContainer.layoutManager.off("activeContentItemChanged", this.onActiveContentItemChanged); + } + + @action.bound + 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; + !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. + } + } - // adds a tab to the layout based on the locaiton parameter which can be: - // close[:{left,right,top,bottom}] - e.g., "close" will close the tab, "close:left" will close the left tab, - // add[:{left,right,top,bottom}] - e.g., "add" will add a tab to the current stack, "add:right" will add a tab on the right - // replace[:{left,right,top,bottom,}] - e.g., "replace" will replace the current stack contents, - // "replace:right" - will replace the stack on the right named "right" if it exists, or create a stack on the right with that name, - // "replace:monkeys" - will replace any tab that has the label 'monkeys', or a tab with that label will be created by default on the right - // inPlace - will add the document to any collection along the path from the document to the docking view that has a field isInPlaceContainer. if none is found, inPlace adds a tab to current stack - addDocTab = (doc: Doc, location: string) => { - SelectionManager.DeselectAll(); - const locationFields = doc._viewType === CollectionViewType.Docking ? ["dashboard"] : location.split(":"); - const locationParams = locationFields.length > 1 ? locationFields[1] : ""; - switch (locationFields[0]) { - case "dashboard": return CurrentUserUtils.openDashboard(Doc.UserDoc(), doc); - case "close": return CollectionDockingView.CloseSplit(doc, locationParams); - case "fullScreen": return CollectionDockingView.OpenFullScreen(doc); - case "replace": return CollectionDockingView.ReplaceTab(doc, locationParams, this.stack); - case "lightbox": return LightboxView.AddDocTab(doc, location); - case "toggle": return CollectionDockingView.ToggleSplit(doc, locationParams, this.stack); - case "inPlace": - case "add": - default: - return CollectionDockingView.AddSplit(doc, locationParams, this.stack); - } - } - remDocTab = (doc: Doc | Doc[]) => { - if (doc === this._document) { + // adds a tab to the layout based on the locaiton parameter which can be: + // close[:{left,right,top,bottom}] - e.g., "close" will close the tab, "close:left" will close the left tab, + // add[:{left,right,top,bottom}] - e.g., "add" will add a tab to the current stack, "add:right" will add a tab on the right + // replace[:{left,right,top,bottom,}] - e.g., "replace" will replace the current stack contents, + // "replace:right" - will replace the stack on the right named "right" if it exists, or create a stack on the right with that name, + // "replace:monkeys" - will replace any tab that has the label 'monkeys', or a tab with that label will be created by default on the right + // inPlace - will add the document to any collection along the path from the document to the docking view that has a field isInPlaceContainer. if none is found, inPlace adds a tab to current stack + addDocTab = (doc: Doc, location: string) => { SelectionManager.DeselectAll(); - CollectionDockingView.CloseSplit(this._document); - return true; - } - return false; - } + const locationFields = doc._viewType === CollectionViewType.Docking ? ["dashboard"] : location.split(":"); + const locationParams = locationFields.length > 1 ? locationFields[1] : ""; + switch (locationFields[0]) { + case "dashboard": return CurrentUserUtils.openDashboard(Doc.UserDoc(), doc); + case "close": return CollectionDockingView.CloseSplit(doc, locationParams); + case "fullScreen": return CollectionDockingView.OpenFullScreen(doc); + case "replace": return CollectionDockingView.ReplaceTab(doc, locationParams, this.stack); + case "lightbox": return LightboxView.AddDocTab(doc, location); + case "toggle": return CollectionDockingView.ToggleSplit(doc, locationParams, this.stack); + case "inPlace": + case "add": + default: + return CollectionDockingView.AddSplit(doc, locationParams, this.stack); + } + } + remDocTab = (doc: Doc | Doc[]) => { + if (doc === this._document) { + SelectionManager.DeselectAll(); + CollectionDockingView.CloseSplit(this._document); + return true; + } + return false; + } - getCurrentFrame = () => { - return NumCast(Cast(PresBox.Instance.childDocs[PresBox.Instance.itemIndex].presentationTargetDoc, Doc, null)._currentFrame); - } - @action - focusFunc = (doc: Doc, options?: DocFocusOptions) => { - const shrinkwrap = options?.originalTarget === this._document && this.view?.ComponentView?.shrinkWrap; - if (shrinkwrap && this._document) { - const focusSpeed = 1000; - shrinkwrap(); - this._document._viewTransition = `transform ${focusSpeed}ms`; - setTimeout(action(() => { - this._document!._viewTransition = undefined; - options?.afterFocus?.(false); - }), focusSpeed); - } else { - options?.afterFocus?.(false); - } - if (!this.tab.header.parent._activeContentItem || this.tab.header.parent._activeContentItem !== this.tab.contentItem) { - this.tab.header.parent.setActiveContentItem(this.tab.contentItem); // glr: Panning does not work when this is set - (this line is for trying to make a tab that is not topmost become topmost) - } - } - active = () => this._isActive; - @observable _forceInvalidateScreenToLocal = 0; - ScreenToLocalTransform = () => { - this._forceInvalidateScreenToLocal; - const { translateX, translateY } = Utils.GetScreenTransform(this._mainCont?.children?.[0] as HTMLElement); - return CollectionDockingView.Instance?.props.ScreenToLocalTransform().translate(-translateX, -translateY); - } - PanelWidth = () => this._panelWidth; - PanelHeight = () => this._panelHeight; - miniMapColor = () => this.tabColor; - tabView = () => this._view; - disableMinimap = () => !this._document || (this._document.layout !== CollectionView.LayoutString(Doc.LayoutFieldKey(this._document)) || this._document?._viewType !== CollectionViewType.Freeform); - hideMinimap = () => this.disableMinimap() || BoolCast(this._document?.hideMinimap); + getCurrentFrame = () => { + return NumCast(Cast(PresBox.Instance.childDocs[PresBox.Instance.itemIndex].presentationTargetDoc, Doc, null)._currentFrame); + } + @action + focusFunc = (doc: Doc, options?: DocFocusOptions) => { + const shrinkwrap = options?.originalTarget === this._document && this.view?.ComponentView?.shrinkWrap; + if (shrinkwrap && this._document) { + const focusSpeed = 1000; + shrinkwrap(); + this._document._viewTransition = `transform ${focusSpeed}ms`; + setTimeout(action(() => { + this._document!._viewTransition = undefined; + options?.afterFocus?.(false); + }), focusSpeed); + } else { + options?.afterFocus?.(false); + } + if (!this.tab.header.parent._activeContentItem || this.tab.header.parent._activeContentItem !== this.tab.contentItem) { + this.tab.header.parent.setActiveContentItem(this.tab.contentItem); // glr: Panning does not work when this is set - (this line is for trying to make a tab that is not topmost become topmost) + } + } + active = () => this._isActive; + @observable _forceInvalidateScreenToLocal = 0; + ScreenToLocalTransform = () => { + this._forceInvalidateScreenToLocal; + const { translateX, translateY } = Utils.GetScreenTransform(this._mainCont?.children?.[0] as HTMLElement); + return CollectionDockingView.Instance?.props.ScreenToLocalTransform().translate(-translateX, -translateY); + } + PanelWidth = () => this._panelWidth; + PanelHeight = () => this._panelHeight; + miniMapColor = () => this.tabColor; + tabView = () => this._view; + disableMinimap = () => !this._document || (this._document.layout !== CollectionView.LayoutString(Doc.LayoutFieldKey(this._document)) || this._document?._viewType !== CollectionViewType.Freeform); + hideMinimap = () => this.disableMinimap() || BoolCast(this._document?.hideMinimap); - @computed get docView() { - return !this._activated || !this._document || this._document._viewType === CollectionViewType.Docking ? (null) : - <> this._view = r)} - renderDepth={0} - Document={this._document} - DataDoc={!Doc.AreProtosEqual(this._document[DataSym], this._document) ? this._document[DataSym] : undefined} - ContainingCollectionView={undefined} - ContainingCollectionDoc={undefined} - onBrowseClick={MainView.Instance.exploreMode} - isContentActive={returnTrue} - PanelWidth={this.PanelWidth} - PanelHeight={this.PanelHeight} - styleProvider={DefaultStyleProvider} - docFilters={CollectionDockingView.Instance.childDocFilters} - docRangeFilters={CollectionDockingView.Instance.childDocRangeFilters} - searchFilterDocs={CollectionDockingView.Instance.searchFilterDocs} - addDocument={undefined} - removeDocument={this.remDocTab} - addDocTab={this.addDocTab} - ScreenToLocalTransform={this.ScreenToLocalTransform} - dontCenter={"y"} - rootSelected={returnTrue} - whenChildContentsActiveChanged={emptyFunction} - focus={this.focusFunc} - docViewPath={returnEmptyDoclist} - bringToFront={emptyFunction} - pinToPres={TabDocView.PinDoc} /> - - {this._document.hideMinimap ? "Open minimap" : "Close minimap"}
}> -
e.stopPropagation()} - onClick={action(e => { e.stopPropagation(); this._document!.hideMinimap = !this._document!.hideMinimap; })} > - -
- - ; - } + @computed get docView() { + return !this._activated || !this._document || this._document._viewType === CollectionViewType.Docking ? (null) : + <> this._view = r)} + renderDepth={0} + Document={this._document} + DataDoc={!Doc.AreProtosEqual(this._document[DataSym], this._document) ? this._document[DataSym] : undefined} + ContainingCollectionView={undefined} + ContainingCollectionDoc={undefined} + onBrowseClick={MainView.Instance.exploreMode} + isContentActive={returnTrue} + PanelWidth={this.PanelWidth} + PanelHeight={this.PanelHeight} + styleProvider={DefaultStyleProvider} + docFilters={CollectionDockingView.Instance.childDocFilters} + docRangeFilters={CollectionDockingView.Instance.childDocRangeFilters} + searchFilterDocs={CollectionDockingView.Instance.searchFilterDocs} + addDocument={undefined} + removeDocument={this.remDocTab} + addDocTab={this.addDocTab} + ScreenToLocalTransform={this.ScreenToLocalTransform} + dontCenter={"y"} + rootSelected={returnTrue} + whenChildContentsActiveChanged={emptyFunction} + focus={this.focusFunc} + docViewPath={returnEmptyDoclist} + bringToFront={emptyFunction} + pinToPres={TabDocView.PinDoc} /> + + {this._document.hideMinimap ? "Open minimap" : "Close minimap"}
}> +
e.stopPropagation()} + onClick={action(e => { e.stopPropagation(); this._document!.hideMinimap = !this._document!.hideMinimap; })} > + +
+ + ; + } - render() { - return ( -
{ - if (this._mainCont = ref) { - (this._mainCont as any).InitTab = (tab: any) => this.init(tab, this._document); - DocServer.GetRefField(this.props.documentId).then(action(doc => doc instanceof Doc && (this._document = doc) && this.tab && this.init(this.tab, this._document))); - new _global.ResizeObserver(action((entries: any) => this._forceInvalidateScreenToLocal++)).observe(ref); - } - }} > - {this.docView} -
- ); - } + render() { + return ( +
{ + if (this._mainCont = ref) { + (this._mainCont as any).InitTab = (tab: any) => this.init(tab, this._document); + DocServer.GetRefField(this.props.documentId).then(action(doc => doc instanceof Doc && (this._document = doc) && this.tab && this.init(this.tab, this._document))); + new _global.ResizeObserver(action((entries: any) => this._forceInvalidateScreenToLocal++)).observe(ref); + } + }} > + {this.docView} +
+ ); + } } interface TabMinimapViewProps { - document: Doc; - hideMinimap: () => boolean; - tabView: () => DocumentView | undefined; - addDocTab: (doc: Doc, where: string) => boolean; - PanelWidth: () => number; - PanelHeight: () => number; - background: () => string; + document: Doc; + hideMinimap: () => boolean; + tabView: () => DocumentView | undefined; + addDocTab: (doc: Doc, where: string) => boolean; + PanelWidth: () => number; + PanelHeight: () => number; + background: () => string; } @observer export class TabMinimapView extends React.Component { - static miniStyleProvider = (doc: Opt, props: Opt, property: string): any => { - if (doc) { - switch (property.split(":")[0]) { - default: return DefaultStyleProvider(doc, props, property); - case StyleProp.PointerEvents: return "none"; - case StyleProp.DocContents: - const background = ((type: DocumentType) => { - switch (type) { - case DocumentType.PDF: return "pink"; - case DocumentType.AUDIO: return "lightgreen"; - case DocumentType.WEB: return "brown"; - case DocumentType.IMG: return "blue"; - case DocumentType.MAP: return "orange"; - case DocumentType.VID: return "purple"; - case DocumentType.RTF: return "yellow"; - case DocumentType.COL: return undefined; - default: return "gray"; - } - })(doc.type as DocumentType); - return !background ? - undefined : -
; + static miniStyleProvider = (doc: Opt, props: Opt, property: string): any => { + if (doc) { + switch (property.split(":")[0]) { + default: return DefaultStyleProvider(doc, props, property); + case StyleProp.PointerEvents: return "none"; + case StyleProp.DocContents: + const background = ((type: DocumentType) => { + switch (type) { + case DocumentType.PDF: return "pink"; + case DocumentType.AUDIO: return "lightgreen"; + case DocumentType.WEB: return "brown"; + case DocumentType.IMG: return "blue"; + case DocumentType.MAP: return "orange"; + case DocumentType.VID: return "purple"; + case DocumentType.RTF: return "yellow"; + case DocumentType.COL: return undefined; + default: return "gray"; + } + })(doc.type as DocumentType); + return !background ? + undefined : +
; + } } - } - } - @computed get renderBounds() { - const compView = this.props.tabView()?.ComponentView as CollectionFreeFormView; - const bounds = compView?.freeformData?.(true)?.bounds; - if (!bounds) return undefined; - const xbounds = bounds.r - bounds.x; - const ybounds = bounds.b - bounds.y; - const dim = Math.max(xbounds, ybounds); - return { l: bounds.x + xbounds / 2 - dim / 2, t: bounds.y + ybounds / 2 - dim / 2, cx: bounds.x + xbounds / 2, cy: bounds.y + ybounds / 2, dim }; - } - childLayoutTemplate = () => Cast(this.props.document.childLayoutTemplate, Doc, null); - returnMiniSize = () => NumCast(this.props.document._miniMapSize, 150); - miniDown = (e: React.PointerEvent) => { - const doc = this.props.document; - const renderBounds = this.renderBounds ?? { l: 0, r: 0, t: 0, b: 0, dim: 1 }; - const miniSize = this.returnMiniSize(); - doc && setupMoveUpEvents(this, e, action((e: PointerEvent, down: number[], delta: number[]) => { - doc._panX = clamp(NumCast(doc._panX) + delta[0] / miniSize * renderBounds.dim, renderBounds.l, renderBounds.l + renderBounds.dim); - doc._panY = clamp(NumCast(doc._panY) + delta[1] / miniSize * renderBounds.dim, renderBounds.t, renderBounds.t + renderBounds.dim); - return false; - }), emptyFunction, emptyFunction); - } - render() { - if (!this.renderBounds) return (null); - const miniWidth = this.props.PanelWidth() / NumCast(this.props.document._viewScale, 1) / this.renderBounds.dim * 100; - const miniHeight = this.props.PanelHeight() / NumCast(this.props.document._viewScale, 1) / this.renderBounds.dim * 100; - const miniLeft = 50 + (NumCast(this.props.document._panX) - this.renderBounds.cx) / this.renderBounds.dim * 100 - miniWidth / 2; - const miniTop = 50 + (NumCast(this.props.document._panY) - this.renderBounds.cy) / this.renderBounds.dim * 100 - miniHeight / 2; - const miniSize = this.returnMiniSize(); - return this.props.hideMinimap() ? (null) : -
- -
-
-
-
; - } + } + @computed get renderBounds() { + const compView = this.props.tabView()?.ComponentView as CollectionFreeFormView; + const bounds = compView?.freeformData?.(true)?.bounds; + if (!bounds) return undefined; + const xbounds = bounds.r - bounds.x; + const ybounds = bounds.b - bounds.y; + const dim = Math.max(xbounds, ybounds); + return { l: bounds.x + xbounds / 2 - dim / 2, t: bounds.y + ybounds / 2 - dim / 2, cx: bounds.x + xbounds / 2, cy: bounds.y + ybounds / 2, dim }; + } + childLayoutTemplate = () => Cast(this.props.document.childLayoutTemplate, Doc, null); + returnMiniSize = () => NumCast(this.props.document._miniMapSize, 150); + miniDown = (e: React.PointerEvent) => { + const doc = this.props.document; + const renderBounds = this.renderBounds ?? { l: 0, r: 0, t: 0, b: 0, dim: 1 }; + const miniSize = this.returnMiniSize(); + doc && setupMoveUpEvents(this, e, action((e: PointerEvent, down: number[], delta: number[]) => { + doc._panX = clamp(NumCast(doc._panX) + delta[0] / miniSize * renderBounds.dim, renderBounds.l, renderBounds.l + renderBounds.dim); + doc._panY = clamp(NumCast(doc._panY) + delta[1] / miniSize * renderBounds.dim, renderBounds.t, renderBounds.t + renderBounds.dim); + return false; + }), emptyFunction, emptyFunction); + } + render() { + if (!this.renderBounds) return (null); + const miniWidth = this.props.PanelWidth() / NumCast(this.props.document._viewScale, 1) / this.renderBounds.dim * 100; + const miniHeight = this.props.PanelHeight() / NumCast(this.props.document._viewScale, 1) / this.renderBounds.dim * 100; + const miniLeft = 50 + (NumCast(this.props.document._panX) - this.renderBounds.cx) / this.renderBounds.dim * 100 - miniWidth / 2; + const miniTop = 50 + (NumCast(this.props.document._panY) - this.renderBounds.cy) / this.renderBounds.dim * 100 - miniHeight / 2; + const miniSize = this.returnMiniSize(); + return this.props.hideMinimap() ? (null) : +
+ +
+
+
+
; + } } diff --git a/src/client/views/nodes/trails/PresElementBox.scss b/src/client/views/nodes/trails/PresElementBox.scss index a178be910..1919071df 100644 --- a/src/client/views/nodes/trails/PresElementBox.scss +++ b/src/client/views/nodes/trails/PresElementBox.scss @@ -5,155 +5,157 @@ $slide-background: #d5dce2; $slide-active: #5B9FDD; .presItem-container { - cursor: grab; - display: grid; - grid-template-columns: 20px auto; - font-family: Roboto; - letter-spacing: normal; - position: relative; - pointer-events: all; - width: 100%; - height: 100%; - font-weight: 400; - -webkit-touch-callout: none; - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - align-items: center; - - .presItem-number { - margin-top: 3.5px; - font-size: 12px; - font-weight: 700; - text-align: center; - justify-self: center; - align-self: flex-start; - position: relative; - display: inline-block; - overflow: hidden; - } + cursor: grab; + display: flex; + grid-template-columns: 20px auto; + font-family: Roboto; + letter-spacing: normal; + position: relative; + pointer-events: all; + width: 100%; + height: 100%; + font-weight: 400; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + align-items: center; + + // .presItem-number { + // margin-top: 3.5px; + // font-size: 12px; + // font-weight: 700; + // text-align: center; + // justify-self: center; + // align-self: flex-start; + // position: relative; + // display: inline-block; + // overflow: hidden; + // } } .presItem-slide { - position: relative; - background-color: #d5dce2; - border-radius: 5px; - height: calc(100% - 7px); - width: 100%; - display: grid; - grid-template-rows: 16px 10px auto; - grid-template-columns: max-content max-content max-content max-content auto; - - .presItem-name { - min-width: 20px; - z-index: 300; - top: 2px; - align-self: center; - font-size: 11px; - font-family: Roboto; - font-weight: 500; - position: relative; - padding-left: 10px; - padding-right: 10px; - letter-spacing: normal; - width: max-content; - text-overflow: ellipsis; - overflow: hidden; - white-space: pre; - } - - .presItem-docName { - min-width: 20px; - z-index: 300; - align-self: center; - font-size: 9px; - font-family: Roboto; - font-weight: 300; - position: relative; - padding-left: 10px; - padding-right: 10px; - letter-spacing: normal; - width: max-content; - text-overflow: ellipsis; - overflow: hidden; - white-space: pre; - grid-row: 2; - grid-column: 1/6; - } - - .presItem-time { - align-self: center; - position: relative; - padding-right: 10px; - top: 1px; - font-size: 10; - font-weight: 300; - font-family: Roboto; - z-index: 300; - letter-spacing: normal; - } - - .presItem-embedded { - overflow: hidden; - grid-row: 3; - grid-column: 1/8; - position: relative; - display: flex; - width: auto; - justify-content: center; - margin: auto; - margin-bottom: 2px; - border-bottom-left-radius: 5px; - border-bottom-right-radius: 5px; - } - - .presItem-embeddedMask { - width: 100%; - height: 100%; - position: absolute; - border-radius: 3px; - top: 0; - left: 0; - z-index: 1; - overflow: hidden; - } - - - .presItem-slideButtons { - display: flex; - grid-column: 7; - grid-row: 1/3; - width: max-content; - justify-self: right; - justify-content: flex-end; - - .slideButton { - cursor: pointer; + position: relative; + height: 100%; + width: 100%; + border-bottom: .5px solid grey; + display: flex; + justify-content: space-between; + align-items: center; + grid-template-rows: 16px 10px auto; + grid-template-columns: max-content max-content max-content max-content auto; + + .presItem-name { + display: flex; + min-width: 20px; + z-index: 300; + top: 2px; + align-self: center; + font-size: 11px; + font-family: Roboto; + font-weight: 500; position: relative; - border-radius: 100px; + padding-left: 10px; + padding-right: 10px; + letter-spacing: normal; + width: max-content; + text-overflow: ellipsis; + overflow: hidden; + white-space: pre; + } + + .presItem-docName { + min-width: 20px; z-index: 300; - width: 18px; - height: 18px; - display: flex; - font-size: 12px; - justify-self: center; align-self: center; - background-color: rgba(0, 0, 0, 0.5); - color: white; + font-size: 9px; + font-family: Roboto; + font-weight: 300; + position: relative; + padding-left: 10px; + padding-right: 10px; + letter-spacing: normal; + width: max-content; + text-overflow: ellipsis; + overflow: hidden; + white-space: pre; + grid-row: 2; + grid-column: 1/6; + } + + .presItem-time { + align-self: center; + position: relative; + padding-right: 10px; + top: 1px; + font-size: 10; + font-weight: 300; + font-family: Roboto; + z-index: 300; + letter-spacing: normal; + } + + .presItem-embedded { + overflow: hidden; + grid-row: 3; + grid-column: 1/8; + position: relative; + display: flex; + width: auto; justify-content: center; - align-items: center; - transition: 0.2s; - margin-right: 3px; - } - - .slideButton:hover { - background-color: rgba(0, 0, 0, 1); - transform: scale(1.2); - } - } + margin: auto; + margin-bottom: 2px; + border-bottom-left-radius: 5px; + border-bottom-right-radius: 5px; + } + + .presItem-embeddedMask { + width: 100%; + height: 100%; + position: absolute; + border-radius: 3px; + top: 0; + left: 0; + z-index: 1; + overflow: hidden; + } + + + .presItem-slideButtons { + display: flex; + grid-column: 7; + grid-row: 1/3; + width: max-content; + justify-self: right; + justify-content: flex-end; + + .slideButton { + cursor: pointer; + position: relative; + border-radius: 100px; + z-index: 300; + width: 18px; + height: 18px; + display: flex; + font-size: 12px; + justify-self: center; + align-self: center; + background-color: rgba(0, 0, 0, 0.5); + color: white; + justify-content: center; + align-items: center; + transition: 0.2s; + margin-right: 3px; + } + + .slideButton:hover { + background-color: rgba(0, 0, 0, 1); + transform: scale(1.2); + } + } } // .presItem-slide:hover { @@ -192,42 +194,42 @@ $slide-active: #5B9FDD; // } .presItem-slide.active { - box-shadow: 0 0 0px 1.5px $dark-blue; + box-shadow: 0 0 0px 1.5px $dark-blue; } .presItem-multiDrag { - font-family: Roboto; - font-weight: 600; - color: white; - text-align: center; - justify-content: center; - align-content: center; - width: 100px; - height: 30px; - position: absolute; - background-color: $dark-blue; - z-index: 4000; - border-radius: 10px; - box-shadow: black 0.4vw 0.4vw 0.8vw; - line-height: 30px; + font-family: Roboto; + font-weight: 600; + color: white; + text-align: center; + justify-content: center; + align-content: center; + width: 100px; + height: 30px; + position: absolute; + background-color: $dark-blue; + z-index: 4000; + border-radius: 10px; + box-shadow: black 0.4vw 0.4vw 0.8vw; + line-height: 30px; } .presItem-miniSlide { - font-weight: 700; - font-size: 12; - grid-column: 1/8; - align-self: center; - justify-self: center; - background-color: #d5dce2; - width: 26px; - text-align: center; - height: 26px; - line-height: 28px; - border-radius: 100%; + font-weight: 700; + font-size: 12; + grid-column: 1/8; + align-self: center; + justify-self: center; + background-color: #d5dce2; + width: 26px; + text-align: center; + height: 26px; + line-height: 28px; + border-radius: 100%; } .presItem-miniSlide.active { - box-shadow: 0 0 0px 1.5px $dark-blue; + box-shadow: 0 0 0px 1.5px $dark-blue; } // .presItem-slide:hover { diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx index f1144b32d..acdf9c43f 100644 --- a/src/client/views/nodes/trails/PresElementBox.tsx +++ b/src/client/views/nodes/trails/PresElementBox.tsx @@ -329,7 +329,7 @@ export class PresElementBox extends ViewBoxBaseComponent() { hideDocumentButtonBar: true, hideDecorationTitle: true, hideOpenButton: true, - hideDeleteButton: true, + // hideDeleteButton: true, cloneFieldFilter: new List(["system"]) }); @@ -387,7 +387,7 @@ export class PresElementBox extends ViewBoxBaseComponent() { onPointerDown={this.headerDown} onPointerUp={this.headerUp} > - {miniView ? + {/* {miniView ? // when width is LESS than 110 px
{`${this.indexInPres + 1}.`} @@ -396,13 +396,17 @@ export class PresElementBox extends ViewBoxBaseComponent() { // when width is MORE than 110 px
{`${this.indexInPres + 1}.`} -
} +
} */} + {/*
+ {`${this.indexInPres + 1}.`} +
*/} {miniView ? (null) :
+
{`${this.indexInPres + 1}. `}
() { SetValue={this.onSetValue} />
-
{"Movement speed"}
}>
{this.transition}
-
{"Duration"}
}>
{this.duration}
+ {/*
{"Movement speed"}
}>
{this.transition}
*/} + {/*
{"Duration"}
}>
{this.duration}
*/}
{"Update view"}
}>
() { } - {this.indexInPres === 0 ? (null) :
{activeItem.groupWithUp ? "Ungroup" : "Group with up"}
}> + {/* {this.indexInPres === 0 ? (null) :
{activeItem.groupWithUp ? "Ungroup" : "Group with up"}
}>
activeItem.groupWithUp = !activeItem.groupWithUp} style={{ @@ -453,7 +457,7 @@ export class PresElementBox extends ViewBoxBaseComponent() { e.stopPropagation()} />
-
} + } */} {/*
{this.rootDoc.presExpandInlineButton ? "Minimize" : "Expand"}
}>
{ e.stopPropagation(); this.presExpandDocumentClick(); }}> e.stopPropagation()} />
*/} @@ -463,7 +467,7 @@ export class PresElementBox extends ViewBoxBaseComponent() { e.stopPropagation()} />
-
{activeItem.presPinView ? (<>View of {targetDoc.title}) : targetDoc.title}
+ {/*
{activeItem.presPinView ? (<>View of {targetDoc.title}) : targetDoc.title}
*/} {this.renderEmbeddedInline}
}
); -- cgit v1.2.3-70-g09d2 From 88a8f35c33bbff61b0058a66d7bcc586d483deca Mon Sep 17 00:00:00 2001 From: Jenny Yu Date: Wed, 8 Jun 2022 01:03:44 -0700 Subject: fix: hide recording by stopping propagation --- src/client/views/nodes/trails/PresElementBox.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx index acdf9c43f..9bace1c8d 100644 --- a/src/client/views/nodes/trails/PresElementBox.tsx +++ b/src/client/views/nodes/trails/PresElementBox.tsx @@ -284,7 +284,8 @@ export class PresElementBox extends ViewBoxBaseComponent() { @undoBatch @action - hideRecording = () => { + hideRecording = (e: React.PointerEvent) => { + e.stopPropagation() DocListCast((Doc.UserDoc().myOverlayDocs as Doc).data).forEach((doc) => { if (doc.slides === this.rootDoc) { Doc.RemoveDocFromList((Doc.UserDoc().myOverlayDocs as Doc), undefined, doc); -- cgit v1.2.3-70-g09d2 From e08c690dbeca3e58b1bc0d387df0287f60f58b7a Mon Sep 17 00:00:00 2001 From: Jenny Yu Date: Wed, 8 Jun 2022 21:20:01 -0700 Subject: fix: remove recording from overlay when deleting slide --- src/client/views/nodes/trails/PresBox.tsx | 3 --- src/client/views/nodes/trails/PresElementBox.tsx | 24 ++++++++++++------------ 2 files changed, 12 insertions(+), 15 deletions(-) (limited to 'src') diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx index 591480023..fe7b5c8c4 100644 --- a/src/client/views/nodes/trails/PresBox.tsx +++ b/src/client/views/nodes/trails/PresBox.tsx @@ -327,10 +327,7 @@ export class PresBox extends ViewBoxBaseComponent() { navigateToView = (targetDoc: Doc, activeItem: Doc) => { clearTimeout(this._navTimer); const bestTarget = DocumentManager.Instance.getFirstDocumentView(targetDoc)?.props.Document; - if (bestTarget) console.log(bestTarget.title, bestTarget.type); - else console.log("no best target"); bestTarget && runInAction(() => { - console.log(bestTarget.title, bestTarget.type); if (bestTarget.type === DocumentType.PDF || bestTarget.type === DocumentType.WEB || bestTarget.type === DocumentType.RTF || bestTarget._viewType === CollectionViewType.Stacking) { bestTarget._viewTransition = activeItem.presTransition ? `transform ${activeItem.presTransition}ms` : 'all 0.5s'; bestTarget._scrollTop = activeItem.presPinViewScroll; diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx index f56ca5471..a2d76978a 100644 --- a/src/client/views/nodes/trails/PresElementBox.tsx +++ b/src/client/views/nodes/trails/PresElementBox.tsx @@ -261,11 +261,12 @@ export class PresElementBox extends ViewBoxBaseComponent() { @undoBatch removeItem = action((e: React.MouseEvent) => { + e.stopPropagation(); this.props.removeDocument?.(this.rootDoc); if (PresBox.Instance._selectedArray.has(this.rootDoc)) { PresBox.Instance._selectedArray.delete(this.rootDoc); } - e.stopPropagation(); + this.removeAllRecordingInOverlay() }); // set the value/title of the individual pres element @@ -318,10 +319,7 @@ export class PresElementBox extends ViewBoxBaseComponent() { return isInOverlay } - @undoBatch - @action - hideRecording = (e: React.PointerEvent) => { - e.stopPropagation() + removeAllRecordingInOverlay = () => { DocListCast((Doc.UserDoc().myOverlayDocs as Doc).data).forEach((doc) => { if (doc.slides === this.rootDoc) { Doc.RemoveDocFromList((Doc.UserDoc().myOverlayDocs as Doc), undefined, doc); @@ -329,15 +327,17 @@ export class PresElementBox extends ViewBoxBaseComponent() { }) } + @undoBatch + @action + hideRecording = (e: React.PointerEvent) => { + e.stopPropagation() + this.removeAllRecordingInOverlay() + } + @undoBatch @action showRecording = (activeItem: Doc) => { - // Remove every recording that already exists in overlay view - DocListCast((Doc.UserDoc().myOverlayDocs as Doc).data).forEach((doc) => { - if (doc.slides !== null) { - Doc.RemoveDocFromList((Doc.UserDoc().myOverlayDocs as Doc), undefined, doc); - } - }) + this.removeAllRecordingInOverlay() if (activeItem.recording) { // if we already have an existing recording Doc.AddDocToList((Doc.UserDoc().myOverlayDocs as Doc), undefined, Cast(activeItem.recording, Doc, null)); @@ -363,7 +363,7 @@ export class PresElementBox extends ViewBoxBaseComponent() { // if we dont have any recording const recording = Docs.Create.WebCamDocument("", { _width: 400, _height: 200, - hideDocumentButtonBar: true, + // hideDocumentButtonBar: true, hideDecorationTitle: true, hideOpenButton: true, // hideDeleteButton: true, -- cgit v1.2.3-70-g09d2 From 0e6f5e580b7e64ecc2002534e1a14ccee36cd28e Mon Sep 17 00:00:00 2001 From: Jenny Yu Date: Wed, 8 Jun 2022 22:55:16 -0700 Subject: feat: basic dashboard view --- src/client/views/DashboardView.scss | 39 +++ src/client/views/DashboardView.tsx | 50 ++++ src/client/views/Main.tsx | 1 + src/client/views/MainView.tsx | 20 +- src/client/views/global/globalCssVariables.scss | 2 +- src/client/views/topbar/TopBar.scss | 366 ++++++++++++------------ src/client/views/topbar/TopBar.tsx | 85 ++++-- 7 files changed, 341 insertions(+), 222 deletions(-) create mode 100644 src/client/views/DashboardView.scss create mode 100644 src/client/views/DashboardView.tsx (limited to 'src') diff --git a/src/client/views/DashboardView.scss b/src/client/views/DashboardView.scss new file mode 100644 index 000000000..1896d7bfb --- /dev/null +++ b/src/client/views/DashboardView.scss @@ -0,0 +1,39 @@ +.dashboard-view { + padding: 50px; + display: flex; + flex-direction: row; + + .left-menu { + display: flex; + flex-direction: column; + width: 300px; + } + + .all-dashboards { + display: flex; + flex-direction: row; + flex-wrap: wrap; + overflow-y: scroll; + } +} + + + +.dashboard-container { + border-radius: 10px; + width: 250px; + border: solid .5px grey; + display: flex; + flex-direction: column; + margin: 0 30px 30px 30px; + overflow: hidden; + + &:hover { + border: solid 1.5px grey; + } + + .title { + margin: 10px; + font-weight: 500; + } +} \ No newline at end of file diff --git a/src/client/views/DashboardView.tsx b/src/client/views/DashboardView.tsx new file mode 100644 index 000000000..5fd9b550d --- /dev/null +++ b/src/client/views/DashboardView.tsx @@ -0,0 +1,50 @@ +import { observable } from "mobx"; +import { observer } from "mobx-react"; +import * as React from 'react'; +import { Doc, DocListCast } from "../../fields/Doc"; +import { Id } from "../../fields/FieldSymbols"; +import { Cast, ImageCast, StrCast } from "../../fields/Types"; +import { CurrentUserUtils } from "../util/CurrentUserUtils"; +import { UndoManager } from "../util/UndoManager"; +import "./DashboardView.scss" + +@observer +export class DashboardView extends React.Component { + + //TODO: delete dashboard, share dashboard, etc. + + newDashboard = async () => { + const batch = UndoManager.StartBatch("new dash"); + await CurrentUserUtils.createNewDashboard(Doc.UserDoc()); + batch.end(); + } + + clickDashboard = async (e: React.MouseEvent, dashboard: Doc) => { + if (e.detail === 2) { + Doc.UserDoc().activeDashboard = dashboard + } + } + + render() { + const myDashboards = DocListCast(CurrentUserUtils.MyDashboards.data); + return
+
+
New
+
All Dashboards
+
+
+ {myDashboards.map((dashboard) => +
{ this.clickDashboard(e, dashboard) }}> + +
{StrCast(dashboard.title)}
+
+ + )} + {myDashboards.map((dashboard) => { + console.log(dashboard.thumb) + })} + +
+
+ } +} diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index 517fe097c..49c2dcf34 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -10,6 +10,7 @@ import { CurrentUserUtils } from "../util/CurrentUserUtils"; import { LinkManager } from "../util/LinkManager"; import { RecordingApi } from "../util/RecordingApi"; import { CollectionView } from "./collections/CollectionView"; +import { DashboardView } from './DashboardView'; import { MainView } from "./MainView"; AssignAllExtensions(); diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 15e1dbe18..cdf341c3b 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -37,6 +37,7 @@ import { CollectionView, CollectionViewType } from './collections/CollectionView import "./collections/TreeView.scss"; import { ComponentDecorations } from './ComponentDecorations'; import { ContextMenu } from './ContextMenu'; +import { DashboardView } from './DashboardView'; import { DictationOverlay } from './DictationOverlay'; import { DocumentDecorations } from './DocumentDecorations'; import { GestureOverlay } from './GestureOverlay'; @@ -659,12 +660,19 @@ export class MainView extends React.Component { {LinkDescriptionPopup.descriptionPopup ? : null} {DocumentLinksButton.LinkEditorDocView ? DocumentLinksButton.LinkEditorDocView = undefined)} docView={DocumentLinksButton.LinkEditorDocView} /> : (null)} {LinkDocPreview.LinkInfo ? : (null)} -
- -
- - {this.mainDashboardArea} - + + {Doc.UserDoc().activeDashboard ? + <> +
+ +
+ + {this.mainDashboardArea} + + : + + } + diff --git a/src/client/views/global/globalCssVariables.scss b/src/client/views/global/globalCssVariables.scss index a14634bdc..d41f9e06a 100644 --- a/src/client/views/global/globalCssVariables.scss +++ b/src/client/views/global/globalCssVariables.scss @@ -38,7 +38,7 @@ $small-text: 9px; // misc values $border-radius: 0.3em; $search-thumnail-size: 130; -$topbar-height: 32px; +$topbar-height: 50px; $antimodemenu-height: 36px; // dragged items diff --git a/src/client/views/topbar/TopBar.scss b/src/client/views/topbar/TopBar.scss index 923f1892e..9a8e7c2e5 100644 --- a/src/client/views/topbar/TopBar.scss +++ b/src/client/views/topbar/TopBar.scss @@ -1,29 +1,30 @@ @import "../global/globalCssVariables"; .topbar-container { - display: flex; - flex-direction: column; - width: 100%; - position: relative; - font-size: 10px; - line-height: 1; - overflow-y: auto; - overflow-x: visible; - background: $dark-gray; - overflow: visible; - z-index: 1000; - - .topbar-bar { - height: $topbar-height; - display: grid; - grid-auto-columns: 33.3% 33.3% 33.3%; - align-items: center; - background-color: $dark-gray; - - .topBar-icon { + display: flex; + flex-direction: column; + width: 100%; + position: relative; + font-size: 10px; + line-height: 1; + overflow-y: auto; + overflow-x: visible; + background: $dark-gray; + overflow: visible; + z-index: 1000; + + .topbar-button-text { + color: $white; + padding: 10px; + size: 15; + + &:hover { + font-weight: 500; + } + } + + .topbar-button-icon { cursor: pointer; - font-size: 12.5px; - font-family: 'Roboto'; width: fit-content; display: flex; justify-content: center; @@ -31,190 +32,179 @@ align-items: center; justify-self: center; align-self: center; - border-radius: $standard-border-radius; padding: 5px; transition: linear 0.2s; - color: $black; - background-color: $light-gray; + color: $white; &:hover { - background-color: darken($color: $light-gray, $amount: 20); + background-color: darken($color: $light-gray, $amount: 20); } - } - - - - - .topbar-center { - grid-column: 2; - display: inline-flex; - justify-content: center; + } + + .topbar-title { + color: $white; + font-size: 17; + font-weight: 500; + } + + .topbar-bar { + height: $topbar-height; + display: grid; + grid-auto-columns: 33.3% 33.3% 33.3%; align-items: center; - gap: 5px; - - .topbar-dashboards { - display: flex; - flex-direction: row; - gap: 5px; + background-color: $dark-gray; + + .topbar-center { + grid-column: 2; + display: inline-flex; + justify-content: center; + align-items: center; + gap: 5px; + + .topbar-dashboards { + display: flex; + flex-direction: row; + gap: 5px; + } } - .topbar-lozenge-dashboard { - display: flex; - - - - .topbar-dashSelect { - border: none; - background-color: $dark-gray; - color: $white; - font-family: 'Roboto'; - font-size: 17; - font-weight: 500; - - &:hover { - cursor: pointer; - } - } - } - } - - - .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; - .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-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 { + .topbar-left { + grid-column: 1; + color: black; + font-family: 'Roboto'; + position: relative; + display: flex; + width: fit-content; + gap: 5px; + + .topBar-icon:hover { + background-color: $close-red; + } + + .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; + } + } + } + + .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 { - display: inline-block; - } - } - } - - .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; - - button { - transform: none; - - &:hover { - transform: none; - } - } - } + .topbar-logoff { + background: red; + } - &.topbar-submit { - margin-left: 2px; - margin-right: 2px + .topbar-lozenge-user:hover { + .topbar-logoff { + display: inline-block; + } + } } - &.topbar-close { - color: $white; - max-height: $topbar-height; + .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; + + button { + transform: none; + + &:hover { + transform: none; + } + } + } + + &.topbar-submit { + margin-left: 2px; + margin-right: 2px + } + + &.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; - } + 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 diff --git a/src/client/views/topbar/TopBar.tsx b/src/client/views/topbar/TopBar.tsx index e486bcb52..1a11925dc 100644 --- a/src/client/views/topbar/TopBar.tsx +++ b/src/client/views/topbar/TopBar.tsx @@ -5,7 +5,7 @@ import { observer } from "mobx-react"; import * as React from 'react'; import { Doc, DocListCast } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; -import { StrCast } from '../../../fields/Types'; +import { Cast, StrCast } from '../../../fields/Types'; import { Utils } from '../../../Utils'; import { CurrentUserUtils } from "../../util/CurrentUserUtils"; import { SettingsManager } from "../../util/SettingsManager"; @@ -20,29 +20,54 @@ import "./TopBar.scss"; */ @observer export class TopBar extends React.Component { - render() { - const myDashboards = DocListCast(CurrentUserUtils.MyDashboards.data); - return ( - //TODO:glr Add support for light / dark mode -
-
-
+ navigateToHome = () => { + Doc.UserDoc().activeDashboard = undefined + } + render() { + const myDashboards = DocListCast(CurrentUserUtils.MyDashboards.data); + const activeDashboard = Cast(Doc.UserDoc().activeDashboard, Doc, null) + return ( + //TODO:glr Add support for light / dark mode +
+
+
+
Home
+
+
+
+ {activeDashboard ? StrCast(activeDashboard.title) : "Dash"} +
+
+
+
+ +
+
SettingsManager.Instance.open()}> + +
+
+ + {/*
{`${Doc.CurrentUserEmail}`}
window.location.assign(Utils.prepend("/logout"))}> {"Log out"}
-
-
+
*/} + {/*
- CurrentUserUtils.openDashboard(Doc.UserDoc(), myDashboards[Number(e.target.value)]))} value={myDashboards.indexOf(CurrentUserUtils.ActiveDashboard)} style={{ color: Colors.WHITE }}> {myDashboards.map((dash, i) => )} - -
-
+ */} + {/*
*/} + {/*
Create a new dashboard
} placement="bottom">
{ const batch = UndoManager.StartBatch("new dash"); await CurrentUserUtils.createNewDashboard(Doc.UserDoc()); @@ -65,19 +90,25 @@ export class TopBar extends React.Component { Explore
-
-
-
-
window.open( +
*/} + {/*
*/} + {/*
*/} + {/*
window.open( "https://brown-dash.github.io/Dash-Documentation/", "_blank")}> - {"Help"} -
-
SettingsManager.Instance.open()}> - {"Settings"} + +
+
SettingsManager.Instance.open()}> + +
*/} + {/*
+ +
+
SettingsManager.Instance.open()}> + +
+
*/}
-
-
-
- ); - } +
+ ); + } } \ No newline at end of file -- cgit v1.2.3-70-g09d2 From 83323cccd039c16ef1ec55c388e5fc075d69b487 Mon Sep 17 00:00:00 2001 From: Jenny Yu Date: Wed, 8 Jun 2022 23:13:31 -0700 Subject: feat: styling top bar(with share button) --- src/client/views/global/globalCssVariables.scss | 2 +- src/client/views/topbar/TopBar.scss | 224 ++++++++++++------------ src/client/views/topbar/TopBar.tsx | 42 +++-- 3 files changed, 143 insertions(+), 125 deletions(-) (limited to 'src') diff --git a/src/client/views/global/globalCssVariables.scss b/src/client/views/global/globalCssVariables.scss index d41f9e06a..ce9cc05d6 100644 --- a/src/client/views/global/globalCssVariables.scss +++ b/src/client/views/global/globalCssVariables.scss @@ -38,7 +38,7 @@ $small-text: 9px; // misc values $border-radius: 0.3em; $search-thumnail-size: 130; -$topbar-height: 50px; +$topbar-height: 55px; $antimodemenu-height: 36px; // dragged items diff --git a/src/client/views/topbar/TopBar.scss b/src/client/views/topbar/TopBar.scss index 9a8e7c2e5..6662386d4 100644 --- a/src/client/views/topbar/TopBar.scss +++ b/src/client/views/topbar/TopBar.scss @@ -1,10 +1,9 @@ @import "../global/globalCssVariables"; + .topbar-container { display: flex; flex-direction: column; - width: 100%; - position: relative; font-size: 10px; line-height: 1; overflow-y: auto; @@ -12,6 +11,21 @@ background: $dark-gray; overflow: visible; z-index: 1000; + height: $topbar-height; + background-color: $dark-gray; + + .topbar-inner-container { + display: flex; + flex-direction: row; + position: relative; + display: grid; + grid-auto-columns: 33.3% 33.3% 33.3%; + align-items: center; + + &:first-child { + height: 20px; + } + } .topbar-button-text { color: $white; @@ -47,144 +61,136 @@ font-weight: 500; } - .topbar-bar { - height: $topbar-height; - display: grid; - grid-auto-columns: 33.3% 33.3% 33.3%; + .topbar-center { + grid-column: 2; + display: inline-flex; + justify-content: center; align-items: center; - background-color: $dark-gray; + gap: 5px; - .topbar-center { - grid-column: 2; - display: inline-flex; - justify-content: center; - align-items: center; + .topbar-dashboards { + display: flex; + flex-direction: row; gap: 5px; - - .topbar-dashboards { - display: flex; - flex-direction: row; - gap: 5px; - } } + } - .topbar-right { - grid-column: 3; - position: relative; - display: flex; - justify-content: flex-end; - gap: 5px; - margin-right: 5px; + .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; + + .topBar-icon:hover { + background-color: $close-red; } - .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; - - .topBar-icon:hover { - background-color: $close-red; - } + align-items: center; - .topbar-lozenge-user, - .topbar-lozenge { - height: 23; - font-size: 12; - color: white; + .topbar-dashSelect { + border: none; + background-color: transparent; + color: black; 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; - } + font-size: 17; + font-weight: 500; + + &: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-logoff { + background: red; + } - .topbar-lozenge-user:hover { - .topbar-logoff { - display: inline-block; - } + .topbar-lozenge-user:hover { + .topbar-logoff { + display: inline-block; } } + } - .topbar-barChild { + .topbar-barChild { - &.topbar-collection { - flex: 0 1 auto; - margin-left: 2px; - margin-right: 2px - } + &.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; */ + &.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; - - &: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; } } } diff --git a/src/client/views/topbar/TopBar.tsx b/src/client/views/topbar/TopBar.tsx index 1a11925dc..ae56bb4ad 100644 --- a/src/client/views/topbar/TopBar.tsx +++ b/src/client/views/topbar/TopBar.tsx @@ -28,15 +28,12 @@ export class TopBar extends React.Component { const activeDashboard = Cast(Doc.UserDoc().activeDashboard, Doc, null) return ( //TODO:glr Add support for light / dark mode -
-
+
+
Home
-
- {activeDashboard ? StrCast(activeDashboard.title) : "Dash"} -
@@ -46,6 +43,22 @@ export class TopBar extends React.Component {
+
+
+
+ {activeDashboard ?
Freeform View (Placeholder)
: (null)} +
+
+
+ {activeDashboard ? StrCast(activeDashboard.title) : "Dash"} +
+
+
+
Share
+
+
+
+ {/*
@@ -55,19 +68,19 @@ export class TopBar extends React.Component { {"Log out"}
*/} - {/*
+ {/*
{activeDashboard ? StrCast(activeDashboard.title) : "Dash"}
*/} - {/* CurrentUserUtils.openDashboard(Doc.UserDoc(), myDashboards[Number(e.target.value)]))} value={myDashboards.indexOf(CurrentUserUtils.ActiveDashboard)} style={{ color: Colors.WHITE }}> {myDashboards.map((dash, i) => )} */} - {/*
*/} - {/*
+ {/*
*/ } + {/*
Create a new dashboard
} placement="bottom">
{ const batch = UndoManager.StartBatch("new dash"); await CurrentUserUtils.createNewDashboard(Doc.UserDoc()); @@ -91,24 +104,23 @@ export class TopBar extends React.Component {
*/} - {/*
*/} - {/*
*/} - {/*
window.open( + {/*
*/ } + {/*
*/ } + {/*
window.open( "https://brown-dash.github.io/Dash-Documentation/", "_blank")}>
SettingsManager.Instance.open()}>
*/} - {/*
+ {/*
SettingsManager.Instance.open()}>
*/} -
-
+ ); } } \ No newline at end of file -- cgit v1.2.3-70-g09d2 From 9fa00d5bf6f21081bfb9f7be8ead8ccb8574c2e7 Mon Sep 17 00:00:00 2001 From: bobzel Date: Thu, 9 Jun 2022 12:08:27 -0400 Subject: fixed setting view specs of documents after navigating from headerBar slides --- src/client/views/nodes/trails/PresBox.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx index da65e7919..1dd6fef9b 100644 --- a/src/client/views/nodes/trails/PresBox.tsx +++ b/src/client/views/nodes/trails/PresBox.tsx @@ -336,13 +336,10 @@ export class PresBox extends ViewBoxBaseComponent() { if (bestTarget) this._navTimer = PresBox.navigateToDoc(bestTarget, activeItem, false); } + // navigates to the bestTarget document by making sure it is on screen, + // then it applies the view specs stored in activeItem to @action static navigateToDoc(bestTarget:Doc, activeItem:Doc, jumpToDoc:boolean) { - if (jumpToDoc) { - const srcContext = Cast(bestTarget.context, Doc, null) ?? Cast(Cast(bestTarget.annotationOn, Doc, null)?.context, Doc, null); - const openInTab = (doc: Doc, finished?: () => void) => CollectionDockingView.AddSplit(doc, "right"); - DocumentManager.Instance.jumpToDocument(bestTarget, true, openInTab, srcContext ? [srcContext]:[], undefined, undefined, undefined,undefined, undefined, true, NumCast(activeItem.presZoom)); // documents open in new tab instead of on right - } if (bestTarget.type === DocumentType.PDF || bestTarget.type === DocumentType.WEB || bestTarget.type === DocumentType.RTF || bestTarget._viewType === CollectionViewType.Stacking) { bestTarget._viewTransition = activeItem.presTransition ? `transform ${activeItem.presTransition}ms` : 'all 0.5s'; bestTarget._scrollTop = activeItem.presPinViewScroll; @@ -2581,5 +2578,8 @@ export class PresBox extends ViewBoxBaseComponent() { } ScriptingGlobals.add(function navigateToDoc(bestTarget:Doc, activeItem:Doc) { - PresBox.navigateToDoc(bestTarget, activeItem, true); + const srcContext = Cast(bestTarget.context, Doc, null) ?? Cast(Cast(bestTarget.annotationOn, Doc, null)?.context, Doc, null); + const openInTab = (doc: Doc, finished?: () => void) => {CollectionDockingView.AddSplit(doc, "right"); finished?.(); }; + DocumentManager.Instance.jumpToDocument(bestTarget, true, openInTab, srcContext ? [srcContext]:[], + undefined, undefined, undefined, () => PresBox.navigateToDoc(bestTarget, activeItem, true), undefined, true, NumCast(activeItem.presZoom)); }); \ No newline at end of file -- cgit v1.2.3-70-g09d2 From dde011efbfc366dbc3bb04b52fdb6e30fcca37f5 Mon Sep 17 00:00:00 2001 From: Jenny Yu Date: Thu, 9 Jun 2022 11:30:58 -0700 Subject: style: topbar --- src/client/views/topbar/TopBar.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/client/views/topbar/TopBar.tsx b/src/client/views/topbar/TopBar.tsx index ae56bb4ad..fcc72b73d 100644 --- a/src/client/views/topbar/TopBar.tsx +++ b/src/client/views/topbar/TopBar.tsx @@ -36,7 +36,8 @@ export class TopBar extends React.Component {
-
+
window.open( + "https://brown-dash.github.io/Dash-Documentation/", "_blank")}>
SettingsManager.Instance.open()}> @@ -54,6 +55,8 @@ export class TopBar extends React.Component {
+ {/* TODO: if this is my dashboard, display share + if this is a shared dashboard, display "view original or view annotated" */}
Share
-- cgit v1.2.3-70-g09d2 From efb32ff12d94bff4474ac7ae57fbae754423a61a Mon Sep 17 00:00:00 2001 From: bobzel Date: Thu, 9 Jun 2022 14:38:20 -0400 Subject: cleaning up map Info Window just a bit --- src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx index db7dcb09d..bdf0f9d64 100644 --- a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx +++ b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx @@ -45,11 +45,9 @@ export class MapBoxInfoWindow extends React.Component doc.type === DocumentType.RTF; + addDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((p, d) => p && Doc.AddDocToList(this.props.place, "data", d), true as boolean); + removeDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((p, d) => p && Doc.RemoveDocFromList(this.props.place, "data", d), true as boolean); render() { return
@@ -73,10 +71,10 @@ export class MapBoxInfoWindow extends React.Component doc.type === DocumentType.RTF} + childFitWidth={this.childFitWidth} // childDocumentsActive={returnFalse} - removeDocument={(doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((p, d) => p && Doc.RemoveDocFromList(this.props.place, "data", d), true as boolean)} - addDocument={(doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((p, d) => p && Doc.AddDocToList(this.props.place, "data", d), true as boolean)} + removeDocument={this.removeDoc} + addDocument={this.addDoc} renderDepth={this.props.renderDepth + 1} viewType={CollectionViewType.Stacking} pointerEvents={returnAll} -- cgit v1.2.3-70-g09d2 From 140e2f643f97057ba9c89c502cff7843bd738cd6 Mon Sep 17 00:00:00 2001 From: bobzel Date: Thu, 9 Jun 2022 16:59:09 -0400 Subject: fixed issues with taking dashboard snapshots to update dashboard view --- src/client/util/CurrentUserUtils.ts | 9 ++- src/client/views/DashboardView.tsx | 12 ++-- .../collectionFreeForm/CollectionFreeFormView.tsx | 10 ++-- src/client/views/topbar/TopBar.tsx | 64 +--------------------- 4 files changed, 19 insertions(+), 76 deletions(-) (limited to 'src') diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index b1329e349..2d2dda2b8 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -1183,13 +1183,13 @@ export class CurrentUserUtils { input.click(); } - public static async snapshotDashboard(userDoc: Doc) { + public static CaptureDashboardThumbnail() { const docView = CollectionDockingView.Instance.props.DocumentView?.(); const content = docView?.ContentDiv; if (docView && content) { const _width = Number(getComputedStyle(content).width.replace("px","")); const _height = Number(getComputedStyle(content).height.replace("px","")); - CollectionFreeFormView.UpdateIcon( + return CollectionFreeFormView.UpdateIcon( docView.layoutDoc[Id] + "-icon" + (new Date()).getTime(), content, _width, _height, @@ -1199,10 +1199,13 @@ export class CurrentUserUtils { const proto = Cast(img.proto, Doc, null)!; proto["data-nativeWidth"] = _width; proto["data-nativeHeight"] = _height; - docView.dataDoc.thumb = img; + Doc.GetProto(CurrentUserUtils.ActiveDashboard).thumb = img; }); } + } + + public static async snapshotDashboard(userDoc: Doc) { const copy = await CollectionDockingView.Copy(CurrentUserUtils.ActiveDashboard); Doc.AddDocToList(Cast(userDoc.myDashboards, Doc, null), "data", copy); CurrentUserUtils.openDashboard(userDoc, copy); diff --git a/src/client/views/DashboardView.tsx b/src/client/views/DashboardView.tsx index 5fd9b550d..3cfece970 100644 --- a/src/client/views/DashboardView.tsx +++ b/src/client/views/DashboardView.tsx @@ -1,4 +1,5 @@ import { observable } from "mobx"; +import { extname } from 'path'; import { observer } from "mobx-react"; import * as React from 'react'; import { Doc, DocListCast } from "../../fields/Doc"; @@ -33,13 +34,16 @@ export class DashboardView extends React.Component {
All Dashboards
- {myDashboards.map((dashboard) => -
{ this.clickDashboard(e, dashboard) }}> - + {myDashboards.map((dashboard) => { + const url = ImageCast((dashboard.thumb as Doc)?.data)?.url; + const ext = url ? extname(url.href):""; + const href = url?.href.replace(ext, "_m"+ ext); // need to choose which resolution image to show. options are _s, _m, _l, _o (small/medium/large/original) + return
this.clickDashboard(e, dashboard)}> +
{StrCast(dashboard.title)}
- )} + })} {myDashboards.map((dashboard) => { console.log(dashboard.thumb) })} diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index f4d2d55d5..ef3a896e1 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1532,7 +1532,7 @@ export class CollectionFreeFormView extends CollectionSubView { - VideoBox.convertDataUri(data_url, filename).then( - returnedfilename => setTimeout(() => { - cb(returnedfilename as string, nativeWidth, nativeHeight); - }, 500)); + (async (data_url: any) => { + const returnedFilename = await VideoBox.convertDataUri(data_url, filename); + cb(returnedFilename as string, nativeWidth, nativeHeight); }) .catch(function (error: any) { console.error('oops, something went wrong!', error); diff --git a/src/client/views/topbar/TopBar.tsx b/src/client/views/topbar/TopBar.tsx index fcc72b73d..21ef807ff 100644 --- a/src/client/views/topbar/TopBar.tsx +++ b/src/client/views/topbar/TopBar.tsx @@ -21,7 +21,7 @@ import "./TopBar.scss"; @observer export class TopBar extends React.Component { navigateToHome = () => { - Doc.UserDoc().activeDashboard = undefined + CurrentUserUtils.CaptureDashboardThumbnail()?.then(() => Doc.UserDoc().activeDashboard = undefined); } render() { const myDashboards = DocListCast(CurrentUserUtils.MyDashboards.data); @@ -61,68 +61,6 @@ export class TopBar extends React.Component {
- - - {/*
-
- {`${Doc.CurrentUserEmail}`} -
-
window.location.assign(Utils.prepend("/logout"))}> - {"Log out"} -
-
*/} - {/*
-
-
- {activeDashboard ? StrCast(activeDashboard.title) : "Dash"} -
*/} - - {/* */} - {/*
*/ } - {/*
- Create a new dashboard
} placement="bottom">
{ - const batch = UndoManager.StartBatch("new dash"); - await CurrentUserUtils.createNewDashboard(Doc.UserDoc()); - batch.end(); - }}> - New -
- - Work on a copy of the dashboard layout
} placement="bottom"> -
{ - const batch = UndoManager.StartBatch("snapshot"); - await CurrentUserUtils.snapshotDashboard(Doc.UserDoc()); - batch.end(); - }}> - Snapshot -
- - Browsing mode for directly navigating to documents
} placement="bottom"> -
MainView.Instance._exploreMode = !MainView.Instance._exploreMode)}> - Explore -
- -
*/} - {/*
*/ } - {/*
*/ } - {/*
window.open( - "https://brown-dash.github.io/Dash-Documentation/", "_blank")}> - -
-
SettingsManager.Instance.open()}> - -
*/} - {/*
- -
-
SettingsManager.Instance.open()}> - -
-
*/} ); } -- cgit v1.2.3-70-g09d2 From d1f268c1788f54c62c0779aba224b5b7d30edb01 Mon Sep 17 00:00:00 2001 From: bobzel Date: Thu, 9 Jun 2022 17:13:49 -0400 Subject: changed tab numbering to be relative to dashboard. --- src/client/util/CurrentUserUtils.ts | 5 ++--- src/client/views/collections/CollectionDockingView.tsx | 12 ++++++------ 2 files changed, 8 insertions(+), 9 deletions(-) (limited to 'src') diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 2d2dda2b8..3107650d9 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -1215,8 +1215,6 @@ export class CurrentUserUtils { const presentation = Doc.MakeCopy(userDoc.emptyPresentation as Doc, true); const dashboards = await Cast(userDoc.myDashboards, Doc) as Doc; const dashboardCount = DocListCast(dashboards.data).length + 1; - const emptyPane = Cast(userDoc.emptyPane, Doc, null); - emptyPane["dragFactory-count"] = NumCast(emptyPane["dragFactory-count"]) + 1; const freeformOptions: DocumentOptions = { x: 0, y: 400, @@ -1224,7 +1222,7 @@ export class CurrentUserUtils { _height: 1000, _fitWidth: true, _backgroundGridShow: true, - title: `Untitled Tab ${NumCast(emptyPane["dragFactory-count"])}`, + title: `Untitled Tab 1`, }; const freeformDoc = CurrentUserUtils.GuestTarget || Docs.Create.FreeformDocument([], freeformOptions); const dashboardDoc = Docs.Create.StandardCollectionDockingDocument([{ doc: freeformDoc, initialWidth: 600 }], { title: `Dashboard ${dashboardCount}` }, id, "row"); @@ -1233,6 +1231,7 @@ export class CurrentUserUtils { // switching the tabs from the datadoc to the regular doc const dashboardTabs = DocListCast(dashboardDoc[DataSym].data); dashboardDoc.data = new List(dashboardTabs); + dashboardDoc["pane-count"] = 1; userDoc.activePresentation = presentation; diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index a790a0475..87149a4b8 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -428,10 +428,10 @@ export class CollectionDockingView extends CollectionSubView() { stackCreated = (stack: any) => { stack.header?.element.on('mousedown', (e: any) => { if (e.target === stack.header?.element[0] && e.button === 2) { - const emptyPane = CurrentUserUtils.EmptyPane; - emptyPane["dragFactory-count"] = NumCast(emptyPane["dragFactory-count"]) + 1; + const dashboard= CurrentUserUtils.ActiveDashboard; + dashboard["pane-count"] = NumCast(dashboard["pane-count"]) + 1; const docToAdd = Docs.Create.FreeformDocument([], { - _width: this.props.PanelWidth(), _height: this.props.PanelHeight(), _backgroundGridShow: true, _fitWidth: true, title: `Untitled Tab ${NumCast(emptyPane["dragFactory-count"])}`, + _width: this.props.PanelWidth(), _height: this.props.PanelHeight(), _backgroundGridShow: true, _fitWidth: true, title: `Untitled Tab ${NumCast(dashboard["pane-count"])}`, }); this.props.Document.isShared && inheritParentAcls(this.props.Document, docToAdd); CollectionDockingView.AddSplit(docToAdd, "", stack); @@ -455,10 +455,10 @@ export class CollectionDockingView extends CollectionSubView() { .off('click') //unbind the current click handler .click(action(() => { // stack.config.fixed = !stack.config.fixed; // force the stack to have a fixed size - const emptyPane = CurrentUserUtils.EmptyPane; - emptyPane["dragFactory-count"] = NumCast(emptyPane["dragFactory-count"]) + 1; + const dashboard = CurrentUserUtils.ActiveDashboard; + dashboard["pane-count"] = NumCast(dashboard["pane-count"]) + 1; const docToAdd = Docs.Create.FreeformDocument([], { - _width: this.props.PanelWidth(), _height: this.props.PanelHeight(), _fitWidth: true, _backgroundGridShow: true, title: `Untitled Tab ${NumCast(emptyPane["dragFactory-count"])}` + _width: this.props.PanelWidth(), _height: this.props.PanelHeight(), _fitWidth: true, _backgroundGridShow: true, title: `Untitled Tab ${NumCast(dashboard["pane-count"])}` }); this.props.Document.isShared && inheritParentAcls(this.props.Document, docToAdd); CollectionDockingView.AddSplit(docToAdd, "", stack); -- cgit v1.2.3-70-g09d2 From 43968431d3c94cea07f06421d29ac0f190c55cde Mon Sep 17 00:00:00 2001 From: bobzel Date: Thu, 9 Jun 2022 17:25:55 -0400 Subject: added activePage field for choosing dash page layout. --- src/client/util/CurrentUserUtils.ts | 1 + src/client/views/DashboardView.tsx | 3 ++- src/client/views/MainView.tsx | 21 +++++++++++---------- src/client/views/topbar/TopBar.tsx | 2 +- 4 files changed, 15 insertions(+), 12 deletions(-) (limited to 'src') diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 3107650d9..3b1009532 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -1093,6 +1093,7 @@ export class CurrentUserUtils { Docs.newAccount = !(field instanceof Doc); await Docs.Prototypes.initialize(); const userDoc = Docs.newAccount ? new Doc(userDocumentId, true) : field as Doc; + Docs.newAccount &&(userDoc.activePage = "home"); const updated = this.updateUserDocument(Doc.SetUserDoc(userDoc), sharingDocumentId, linkDatabaseId); (await DocListCastAsync(Cast(Doc.UserDoc().myLinkDatabase, Doc, null)?.data))?.forEach(async link => { // make sure anchors are loaded to avoid incremental updates to computedFn's in LinkManager const a1 = await Cast(link?.anchor1, Doc, null); diff --git a/src/client/views/DashboardView.tsx b/src/client/views/DashboardView.tsx index 3cfece970..b08151c0f 100644 --- a/src/client/views/DashboardView.tsx +++ b/src/client/views/DashboardView.tsx @@ -22,7 +22,8 @@ export class DashboardView extends React.Component { clickDashboard = async (e: React.MouseEvent, dashboard: Doc) => { if (e.detail === 2) { - Doc.UserDoc().activeDashboard = dashboard + Doc.UserDoc().activeDashboard = dashboard; + Doc.UserDoc().activePage = "dashboard"; } } diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index ad041384c..5fd76c388 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -662,16 +662,17 @@ export class MainView extends React.Component { {DocumentLinksButton.LinkEditorDocView ? DocumentLinksButton.LinkEditorDocView = undefined)} docView={DocumentLinksButton.LinkEditorDocView} /> : (null)} {LinkDocPreview.LinkInfo ? : (null)} - {Doc.UserDoc().activeDashboard ? - <> -
- -
- - {this.mainDashboardArea} - - : - + {((page:string) => { + switch (page) { + case "dashboard": + default:return <> +
+ +
+ {this.mainDashboardArea} + ; + case "home": return ; + } })(StrCast(Doc.UserDoc().activePage)) } diff --git a/src/client/views/topbar/TopBar.tsx b/src/client/views/topbar/TopBar.tsx index 21ef807ff..258891099 100644 --- a/src/client/views/topbar/TopBar.tsx +++ b/src/client/views/topbar/TopBar.tsx @@ -21,7 +21,7 @@ import "./TopBar.scss"; @observer export class TopBar extends React.Component { navigateToHome = () => { - CurrentUserUtils.CaptureDashboardThumbnail()?.then(() => Doc.UserDoc().activeDashboard = undefined); + CurrentUserUtils.CaptureDashboardThumbnail()?.then(() => Doc.UserDoc().activePage = "home"); } render() { const myDashboards = DocListCast(CurrentUserUtils.MyDashboards.data); -- cgit v1.2.3-70-g09d2 From ab5e48a2340f06628fc22d1267d081de9dbc572f Mon Sep 17 00:00:00 2001 From: bobzel Date: Thu, 9 Jun 2022 23:24:34 -0400 Subject: fixed up creating dashboard icons to work with pdfs in freeformviews with a grid. no idea why having a grid breaks thrings. --- src/client/util/CurrentUserUtils.ts | 2 +- src/client/views/DashboardView.tsx | 4 +- .../collectionFreeForm/CollectionFreeFormView.tsx | 53 ++++++++++++++-------- src/client/views/nodes/PDFBox.tsx | 44 +++++++----------- 4 files changed, 53 insertions(+), 50 deletions(-) (limited to 'src') diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 3b1009532..9a01f3e5d 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -1194,7 +1194,7 @@ export class CurrentUserUtils { docView.layoutDoc[Id] + "-icon" + (new Date()).getTime(), content, _width, _height, - _width, _height, 0, + _width, _height, 0, 1, true, docView.layoutDoc[Id] + "-icon", (iconFile, _nativeWidth, _nativeHeight) => { const img = Docs.Create.ImageDocument(new ImageField(iconFile), { title: docView.rootDoc.title+"-icon", _width, _height, _nativeWidth, _nativeHeight}); const proto = Cast(img.proto, Doc, null)!; diff --git a/src/client/views/DashboardView.tsx b/src/client/views/DashboardView.tsx index b08151c0f..e003968d6 100644 --- a/src/client/views/DashboardView.tsx +++ b/src/client/views/DashboardView.tsx @@ -36,9 +36,7 @@ export class DashboardView extends React.Component {
{myDashboards.map((dashboard) => { - const url = ImageCast((dashboard.thumb as Doc)?.data)?.url; - const ext = url ? extname(url.href):""; - const href = url?.href.replace(ext, "_m"+ ext); // need to choose which resolution image to show. options are _s, _m, _l, _o (small/medium/large/original) + const href = ImageCast((dashboard.thumb as Doc)?.data)?.url.href; return
this.clickDashboard(e, dashboard)}>
{StrCast(dashboard.title)}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index ef3a896e1..99f74b8a2 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1500,15 +1500,22 @@ export class CollectionFreeFormView extends CollectionSubView { - this.dataDoc.icon = new ImageField(iconFile); - this.dataDoc["icon-nativeWidth"] = nativeWidth; - this.dataDoc["icon-nativeHeight"] = nativeHeight; + this.dataDoc.icon = new ImageField(iconFile); + this.dataDoc["icon-nativeWidth"] = nativeWidth; + this.dataDoc["icon-nativeHeight"] = nativeHeight; }); - public static UpdateIcon(filename:string, docViewContent:HTMLElement, width: number, height: number, - panelWidth:number, panelHeight: number, scrollTop:number, cb:(iconFile:string, nativeWidth:number, nativeHeight:number) => any) { + public static UpdateIcon( + filename:string, docViewContent:HTMLElement, + width: number, height: number, + panelWidth:number, panelHeight: number, + scrollTop:number, + realNativeHeight: number, + noSuffix: boolean, + replaceRootFilename: string| undefined, + cb:(iconFile:string, nativeWidth:number, nativeHeight:number) => any) + { const newDiv = docViewContent.cloneNode(true) as HTMLDivElement; newDiv.style.width = width.toString(); newDiv.style.height = height.toString(); @@ -1538,10 +1553,10 @@ export class CollectionFreeFormView extends CollectionSubView { - const returnedFilename = await VideoBox.convertDataUri(data_url, filename); + const returnedFilename = await VideoBox.convertDataUri(data_url, filename, noSuffix, replaceRootFilename); cb(returnedFilename as string, nativeWidth, nativeHeight); }) .catch(function (error: any) { @@ -1771,7 +1786,7 @@ export class CollectionFreeFormView extends CollectionSubView
{this.layoutDoc._backgroundGridShow ? - : (null)} + />
: (null)} () { @@ -147,34 +148,23 @@ export class PDFBox extends ViewBoxAnnotatableComponent { - return; // currently we render pdf icons as text labels + // currently we render pdf icons as text labels const docViewContent = this.props.docViewPath().lastElement().ContentDiv!; - const newDiv = docViewContent.cloneNode(true) as HTMLDivElement; - newDiv.style.width = (this.layoutDoc[WidthSym]()).toString(); - newDiv.style.height = (this.layoutDoc[HeightSym]()).toString(); - this.replaceCanvases(docViewContent, newDiv); - const htmlString = this._pdfViewer?._mainCont.current && new XMLSerializer().serializeToString(newDiv); - const nativeWidth = this.layoutDoc[WidthSym](); - const nativeHeight = this.layoutDoc[HeightSym](); - - CreateImage( - "", - document.styleSheets, - htmlString, - nativeWidth, - nativeWidth * this.props.PanelHeight() / this.props.PanelWidth(), - NumCast(this.layoutDoc._scrollTop) * this.props.PanelHeight() / NumCast(this.rootDoc[this.fieldKey + "-nativeHeight"]) - ).then - ((data_url: any) => { - VideoBox.convertDataUri(data_url, this.layoutDoc[Id] + "-icon" + (new Date()).getTime(), true, this.layoutDoc[Id] + "-icon").then( - returnedfilename => setTimeout(action(() => { - this.dataDoc.icon = new ImageField(returnedfilename); - this.dataDoc["icon-nativeWidth"] = nativeWidth; - this.dataDoc["icon-nativeHeight"] = nativeHeight; - }), 500)); - }) - .catch(function (error: any) { - console.error('oops, something went wrong!', error); + const filename = this.layoutDoc[Id] + "-icon" + (new Date()).getTime(); + this._pdfViewer?._mainCont.current && CollectionFreeFormView.UpdateIcon( + filename, docViewContent, + this.layoutDoc[WidthSym](), this.layoutDoc[HeightSym](), + this.props.PanelWidth(), this.props.PanelHeight(), + NumCast(this.layoutDoc._scrollTop), + NumCast(this.rootDoc[this.fieldKey + "-nativeHeight"], 1), + true, + this.layoutDoc[Id] + "-icon", + (iconFile:string, nativeWidth:number, nativeHeight:number) => { + setTimeout(() => { + this.dataDoc.icon = new ImageField(iconFile); + this.dataDoc["icon-nativeWidth"] = nativeWidth; + this.dataDoc["icon-nativeHeight"] = nativeHeight; + }, 500); }); } -- cgit v1.2.3-70-g09d2 From 677741f2a5b844a95d92257f1c088607f2d51334 Mon Sep 17 00:00:00 2001 From: bobzel Date: Thu, 9 Jun 2022 23:38:52 -0400 Subject: fixed filtering of fields with strings. --- src/client/views/nodes/FilterBox.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/client/views/nodes/FilterBox.tsx b/src/client/views/nodes/FilterBox.tsx index 28834a202..e3708d7b9 100644 --- a/src/client/views/nodes/FilterBox.tsx +++ b/src/client/views/nodes/FilterBox.tsx @@ -206,7 +206,7 @@ export class FilterBox extends ViewBoxBaseComponent() { facetValues.strings.map(val => { const num = val ? Number(val) : Number.NaN; if (Number.isNaN(num)) { - num && nonNumbers++; + val && nonNumbers++; } else { minVal = Math.min(num, minVal); maxVal = Math.max(num, maxVal); -- cgit v1.2.3-70-g09d2 From f550bca52735d9bd2195ff453e43beca18bef309 Mon Sep 17 00:00:00 2001 From: bobzel Date: Thu, 9 Jun 2022 23:54:11 -0400 Subject: opening a shared dashboard now adds it to myDashboards --- src/client/util/CurrentUserUtils.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 9a01f3e5d..c801a6f2f 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -931,7 +931,6 @@ export class CurrentUserUtils { sharedDocs.childContextMenuScripts = new List([addToDashboards!,]); sharedDocs.childContextMenuLabels = new List(["Add to Dashboards",]); sharedDocs.childContextMenuIcons = new List(["user-plus",]); - } doc.mySharedDocs = new PrefetchProxy(sharedDocs as Doc); } @@ -1111,6 +1110,9 @@ export class CurrentUserUtils { public static openDashboard = (userDoc: Doc, doc: Doc, fromHistory = false) => { CurrentUserUtils.MainDocId = doc[Id]; + if (!DocListCast(CurrentUserUtils.MyDashboards.data).includes(doc)) { + Doc.AddDocToList(CurrentUserUtils.MyDashboards, "data", doc); + } if (doc) { // this has the side-effect of setting the main container since we're assigning the active/guest dashboard !("presentationView" in doc) && (doc.presentationView = new List([Docs.Create.TreeDocument([], { title: "Presentation" })])); -- cgit v1.2.3-70-g09d2 From 5089c14ca27a52326d3f38e7f3ef572548f72d2a Mon Sep 17 00:00:00 2001 From: Jenny Yu Date: Thu, 9 Jun 2022 20:59:22 -0700 Subject: feat: opening up sharing manager on dashboard --- src/client/views/collections/CollectionStackingView.scss | 5 ++--- src/client/views/topbar/TopBar.tsx | 3 ++- 2 files changed, 4 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss index 2f002736d..73572299a 100644 --- a/src/client/views/collections/CollectionStackingView.scss +++ b/src/client/views/collections/CollectionStackingView.scss @@ -30,11 +30,10 @@ width: 90%; margin: 5px; font-size: 11px; - background-color: $light-blue; color: $medium-blue; padding: 10px; - border-radius: 10px; - border: solid 2px $medium-blue; + border-radius: 5px; + border: solid .5px $medium-blue; } } diff --git a/src/client/views/topbar/TopBar.tsx b/src/client/views/topbar/TopBar.tsx index 258891099..79d48e13c 100644 --- a/src/client/views/topbar/TopBar.tsx +++ b/src/client/views/topbar/TopBar.tsx @@ -9,6 +9,7 @@ import { Cast, StrCast } from '../../../fields/Types'; import { Utils } from '../../../Utils'; import { CurrentUserUtils } from "../../util/CurrentUserUtils"; import { SettingsManager } from "../../util/SettingsManager"; +import { SharingManager } from "../../util/SharingManager"; import { undoBatch, UndoManager } from "../../util/UndoManager"; import { Borders, Colors } from "../global/globalEnums"; import { MainView } from "../MainView"; @@ -57,7 +58,7 @@ export class TopBar extends React.Component {
{/* TODO: if this is my dashboard, display share if this is a shared dashboard, display "view original or view annotated" */} -
Share
+
{SharingManager.Instance.open(undefined, activeDashboard)}}>Share
-- cgit v1.2.3-70-g09d2 From c159cec257544ff10c256181d1aedfccf3ae7843 Mon Sep 17 00:00:00 2001 From: Jenny Yu Date: Thu, 9 Jun 2022 21:05:20 -0700 Subject: fix: close active dashboard --- src/client/util/CurrentUserUtils.ts | 6 +++++- src/client/views/topbar/TopBar.tsx | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 3b1009532..d32f1bf70 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -1212,6 +1212,10 @@ export class CurrentUserUtils { CurrentUserUtils.openDashboard(userDoc, copy); } + public static closeActiveDashboard = () => { + Doc.UserDoc().activeDashboard = undefined; + } + public static createNewDashboard = async (userDoc: Doc, id?: string) => { const presentation = Doc.MakeCopy(userDoc.emptyPresentation as Doc, true); const dashboards = await Cast(userDoc.myDashboards, Doc) as Doc; @@ -1237,7 +1241,7 @@ export class CurrentUserUtils { userDoc.activePresentation = presentation; Doc.AddDocToList(dashboards, "data", dashboardDoc); - CurrentUserUtils.openDashboard(userDoc, dashboardDoc); + // CurrentUserUtils.openDashboard(userDoc, dashboardDoc); } public static GetNewTextDoc(title: string, x: number, y: number, width?: number, height?: number, noMargins?: boolean, annotationOn?: Doc, maxHeight?: number, backgroundColor?: string) { diff --git a/src/client/views/topbar/TopBar.tsx b/src/client/views/topbar/TopBar.tsx index 79d48e13c..57ceac2dd 100644 --- a/src/client/views/topbar/TopBar.tsx +++ b/src/client/views/topbar/TopBar.tsx @@ -22,6 +22,7 @@ import "./TopBar.scss"; @observer export class TopBar extends React.Component { navigateToHome = () => { + CurrentUserUtils.closeActiveDashboard(); CurrentUserUtils.CaptureDashboardThumbnail()?.then(() => Doc.UserDoc().activePage = "home"); } render() { -- cgit v1.2.3-70-g09d2 From 46de0c33f92660266c4e72161e7f4d2f9d8439d1 Mon Sep 17 00:00:00 2001 From: Jenny Yu Date: Thu, 9 Jun 2022 21:20:27 -0700 Subject: feat: toggle my dashboards and shared dashboards --- src/client/views/DashboardView.scss | 9 +++++++++ src/client/views/DashboardView.tsx | 32 ++++++++++++++++++++++++-------- 2 files changed, 33 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/src/client/views/DashboardView.scss b/src/client/views/DashboardView.scss index 1896d7bfb..67587dd2b 100644 --- a/src/client/views/DashboardView.scss +++ b/src/client/views/DashboardView.scss @@ -17,7 +17,16 @@ } } +.text-button { + padding: 10px 0; + &:hover { + font-weight: 500; + } + &.selected { + font-weight: 700; + } +} .dashboard-container { border-radius: 10px; diff --git a/src/client/views/DashboardView.tsx b/src/client/views/DashboardView.tsx index b08151c0f..e4e7e2dae 100644 --- a/src/client/views/DashboardView.tsx +++ b/src/client/views/DashboardView.tsx @@ -1,4 +1,4 @@ -import { observable } from "mobx"; +import { action, observable } from "mobx"; import { extname } from 'path'; import { observer } from "mobx-react"; import * as React from 'react'; @@ -9,11 +9,23 @@ import { CurrentUserUtils } from "../util/CurrentUserUtils"; import { UndoManager } from "../util/UndoManager"; import "./DashboardView.scss" +enum DashboardGroup { + MyDashboards, SharedDashboards +} + @observer export class DashboardView extends React.Component { //TODO: delete dashboard, share dashboard, etc. + @observable + private selectedDashboardGroup = DashboardGroup.MyDashboards; + + @action + selectDashboardGroup = (group: DashboardGroup) => { + this.selectedDashboardGroup = group + } + newDashboard = async () => { const batch = UndoManager.StartBatch("new dash"); await CurrentUserUtils.createNewDashboard(Doc.UserDoc()); @@ -27,15 +39,22 @@ export class DashboardView extends React.Component { } } + getDashboards = () => { + const allDashbaords = DocListCast(CurrentUserUtils.MyDashboards.data); + // TODO: filter the dashboards + // return allDashbaords.filter(...) + return allDashbaords + } + render() { - const myDashboards = DocListCast(CurrentUserUtils.MyDashboards.data); return
-
New
-
All Dashboards
+
New
+
this.selectDashboardGroup(DashboardGroup.MyDashboards) }>My Dashboards
+
this.selectDashboardGroup(DashboardGroup.SharedDashboards) }>Shared Dashboards
- {myDashboards.map((dashboard) => { + {this.getDashboards().map((dashboard) => { const url = ImageCast((dashboard.thumb as Doc)?.data)?.url; const ext = url ? extname(url.href):""; const href = url?.href.replace(ext, "_m"+ ext); // need to choose which resolution image to show. options are _s, _m, _l, _o (small/medium/large/original) @@ -45,9 +64,6 @@ export class DashboardView extends React.Component {
})} - {myDashboards.map((dashboard) => { - console.log(dashboard.thumb) - })}
-- cgit v1.2.3-70-g09d2 From 1ab1ff51b5b5769079db1922d5013ec4680a1aa1 Mon Sep 17 00:00:00 2001 From: bobzel Date: Fri, 10 Jun 2022 09:10:54 -0400 Subject: fixed closing tabs to move to headerBar --- src/client/views/collections/CollectionDockingView.tsx | 2 ++ src/client/views/collections/TabDocView.tsx | 12 ++---------- 2 files changed, 4 insertions(+), 10 deletions(-) (limited to 'src') diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index a790a0475..e657bd502 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -412,6 +412,8 @@ export class CollectionDockingView extends CollectionSubView() { } tabDestroyed = (tab: any) => { + Doc.AddDocToList(CurrentUserUtils.MyHeaderBarDoc, "data", tab.DashDoc); + Doc.AddDocToList(CurrentUserUtils.MyRecentlyClosed, "data", tab.DashDoc, undefined, true, true); const dview = CollectionDockingView.Instance.props.Document; const fieldKey = CollectionDockingView.Instance.props.fieldKey; Doc.RemoveDocFromList(dview, fieldKey, tab.DashDoc); diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx index 142013d1a..6c4437aed 100644 --- a/src/client/views/collections/TabDocView.tsx +++ b/src/client/views/collections/TabDocView.tsx @@ -115,20 +115,12 @@ export class TabDocView extends React.Component { } })); }; - - closeWrap.className = "lm_iconWrap"; - closeWrap.id = "lm_closeWrap"; - closeWrap.onclick = (e: MouseEvent) => { - tab.header.parent.contentItem.remove(); - Doc.AddDocToList(CurrentUserUtils.MyHeaderBarDoc, "data", tab.DashDoc); - Doc.AddDocToList(CurrentUserUtils.MyRecentlyClosed, "data", tab.DashDoc, undefined, true, true); - }; + const docIcon = ; - const closeIcon = ; + const closeIcon = ; ReactDOM.render(docIcon, iconWrap); ReactDOM.render(closeIcon, closeWrap); tab.reactComponents = [iconWrap, closeWrap]; - // tab.element[0].append(closeWrap); tab.element[0].prepend(iconWrap); tab._disposers.layerDisposer = reaction(() => ({ layer: tab.DashDoc.activeLayer, color: this.tabColor }), ({ layer, color }) => { -- cgit v1.2.3-70-g09d2 From bc662b4e7eaaa1f33f41f64ce64d60579939b971 Mon Sep 17 00:00:00 2001 From: bobzel Date: Fri, 10 Jun 2022 10:18:05 -0400 Subject: fixed some problems with dragging doc onto empty tab bar. made topBar be just one line to save vertical space. added a context menu to Dashboard --- src/client/util/CurrentUserUtils.ts | 14 ++++--- src/client/util/DragManager.ts | 2 +- .../collectionLinear/CollectionLinearView.tsx | 1 + src/client/views/global/globalCssVariables.scss | 2 +- src/client/views/nodes/DocumentView.tsx | 11 +++--- src/client/views/topbar/TopBar.scss | 6 +-- src/client/views/topbar/TopBar.tsx | 45 ++++++++++++---------- 7 files changed, 46 insertions(+), 35 deletions(-) (limited to 'src') diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index e46bb3b3e..60bfc165b 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -381,6 +381,7 @@ export class CurrentUserUtils { dontUndo: true, title, target, + dontRegisterView: true, hidden: hidden ? ComputedField.MakeFunction("IsNoviceMode()") as any : undefined, _dropAction: "alias", _removeDropProperties: new List(["dropAction", "_stayInCollection"]), @@ -399,6 +400,7 @@ export class CurrentUserUtils { _chromeHidden: true, backgroundColor: Colors.DARK_GRAY, boxShadow: "rgba(0,0,0,0)", + dontRegisterView: true, dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }), ignoreClick: true, _gridGap: 0, @@ -719,7 +721,7 @@ export class CurrentUserUtils { ]; doc.dockedBtns = CurrentUserUtils.linearButtonList({ title: "docked buttons", _height: 40, flexGap: 0, linearViewFloating: true, - linearViewIsExpanded: true, linearViewExpandable: true, ignoreClick: true + childDontRegisterViews: true, linearViewIsExpanded: true, linearViewExpandable: true, ignoreClick: true }, btnDescs.map(desc => doc[`dockedBtn-${desc.title}`] as Doc ?? (doc[`dockedBtn-${desc.title}`] = dockBtn({ title: desc.title, ...desc.opts() })))); } } @@ -833,12 +835,13 @@ export class CurrentUserUtils { }); if (doc.contextMenuBtns === undefined) { doc.contextMenuBtns = CurrentUserUtils.linearButtonList( - { title: "menu buttons", flexGap: 0, linearViewIsExpanded: true, ignoreClick: true, linearViewExpandable: false, _height: 35 }, + { title: "menu buttons", flexGap: 0, childDontRegisterViews: true, linearViewIsExpanded: true, ignoreClick: true, linearViewExpandable: false, _height: 35 }, CurrentUserUtils.contextMenuTools(doc).map(params => !params.subMenu ? btnFunc(params) : CurrentUserUtils.linearButtonList({ title: params.title, + childDontRegisterViews: true, linearViewSubMenu: true, flexGap: 0, ignoreClick: true, linearViewExpandable: true, icon: params.title, _height: 30, linearViewIsExpanded: params.expanded ? !(ComputedField.MakeFunction(params.expanded) as any) : undefined, @@ -854,6 +857,7 @@ export class CurrentUserUtils { btnFunc(params) : CurrentUserUtils.linearButtonList({ title: params.title, + childDontRegisterViews: true, linearViewSubMenu: true, flexGap: 0, ignoreClick: true, linearViewExpandable: true, icon: params.title, _height: 30, linearViewIsExpanded: params.expanded ? !(ComputedField.MakeFunction(params.expanded) as any) : undefined, @@ -919,7 +923,7 @@ export class CurrentUserUtils { title: "My SharedDocs", childDropAction: "alias", system: true, contentPointerEvents: "all", childLimitHeight: 0, _yMargin: 50, _gridGap: 15, _showTitle: "title", treeViewHideTitle: true, ignoreClick: true, _lockedPosition: true, "acl-Public": SharingPermissions.Augment, "_acl-Public": SharingPermissions.Augment, _chromeHidden: true, boxShadow: "0 0", - 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'" + dontRegisterView: true, 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'" }, sharingDocumentId + "outer", sharingDocumentId); (sharedDocs as Doc)["acl-Public"] = (sharedDocs as Doc)[DataSym]["acl-Public"] = SharingPermissions.Augment; } @@ -943,7 +947,7 @@ export class CurrentUserUtils { doc.myImportDocs = new PrefetchProxy(Docs.Create.StackingDocument([], { title: "My Imports", _forceActive: true, buttonMenu: true, buttonMenuDoc: newImportButton, ignoreClick: true, _showTitle: "title", _stayInCollection: true, _hideContextMenu: true, childLimitHeight: 0, childDropAction: "copy", _autoHeight: true, _yMargin: 50, _gridGap: 15, boxShadow: "0 0", _lockedPosition: true, system: true, _chromeHidden: true, - explainer: "This is where documents that are Imported into Dash will go." + dontRegisterView: true, explainer: "This is where documents that are Imported into Dash will go." })); } } @@ -952,7 +956,7 @@ export class CurrentUserUtils { static setupSearchSidebar(doc: Doc) { if (doc.mySearchPanel === undefined) { doc.mySearchPanel = new PrefetchProxy(Docs.Create.SearchDocument({ - backgroundColor: "dimgray", ignoreClick: true, _searchDoc: true, + dontRegisterView: true, backgroundColor: "dimgray", ignoreClick: true, _searchDoc: true, childDropAction: "alias", _lockedPosition: true, _viewType: CollectionViewType.Schema, title: "Search Panel", system: true })) as any as Doc; } diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 9f8c49081..c3c6c22df 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -455,7 +455,7 @@ export namespace DragManager { if (dragData instanceof DocumentDragData) { dragData.userDropAction = e.ctrlKey && e.altKey ? "copy" : e.ctrlKey ? "alias" : dragData.defaultDropAction; } - if (((e.target as any)?.className === "lm_tabs" || e?.shiftKey) && dragData.draggedDocuments.length === 1) { + if (((e.target as any)?.className === "lm_tabs" || ((e.target as any)?.className === "lm_header" || e?.shiftKey) && dragData.draggedDocuments.length === 1) { if (!startWindowDragTimer) { startWindowDragTimer = setTimeout(async () => { startWindowDragTimer = undefined; diff --git a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx index c0a33a5e0..9b1cb1601 100644 --- a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx +++ b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx @@ -153,6 +153,7 @@ export class CollectionLinearView extends CollectionSubView() { PanelWidth={nested ? doc[WidthSym] : this.dimension} PanelHeight={nested || doc._height ? doc[HeightSym] : this.dimension} renderDepth={this.props.renderDepth + 1} + dontRegisterView={BoolCast(this.rootDoc.childDontRegisterViews)} focus={emptyFunction} styleProvider={this.props.styleProvider} docViewPath={returnEmptyDoclist} diff --git a/src/client/views/global/globalCssVariables.scss b/src/client/views/global/globalCssVariables.scss index ce9cc05d6..a14634bdc 100644 --- a/src/client/views/global/globalCssVariables.scss +++ b/src/client/views/global/globalCssVariables.scss @@ -38,7 +38,7 @@ $small-text: 9px; // misc values $border-radius: 0.3em; $search-thumnail-size: 130; -$topbar-height: 55px; +$topbar-height: 32px; $antimodemenu-height: 36px; // dragged items diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 1591840e6..60d16f044 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -764,7 +764,7 @@ export class DocumentViewInternal extends DocComponent this.layoutDoc.hideLinkButton = !this.layoutDoc.hideLinkButton), icon: "eye" }); !appearance && cm.addItem({ description: "UI Controls...", subitems: appearanceItems, icon: "compass" }); - if (!Doc.IsSystem(this.rootDoc) && this.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Tree) { + if (!Doc.IsSystem(this.rootDoc) && this.rootDoc._viewType !== CollectionViewType.Docking && this.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Tree) { !Doc.UserDoc().noviceMode && appearanceItems.splice(0, 0, { description: `${!this.layoutDoc._showAudio ? "Show" : "Hide"} Audio Button`, event: action(() => this.layoutDoc._showAudio = !this.layoutDoc._showAudio), icon: "microphone" }); const existingOnClick = cm.findByDescription("OnClick..."); const onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : []; @@ -834,10 +834,10 @@ export class DocumentViewInternal extends DocComponent this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "add:right"), icon: "layer-group" }); - helpItems.push({ description: "Text Shortcuts Ctrl+/", event: () => this.props.addDocTab(Docs.Create.PdfDocument("/assets/cheat-sheet.pdf", { _width: 300, _height: 300 }), "add:right"), icon: "keyboard" }); - !Doc.UserDoc().novice && helpItems.push({ description: "Print Document in Console", event: () => console.log(this.props.Document), icon: "hand-point-right" }); - !Doc.UserDoc().novice && helpItems.push({ description: "Print DataDoc in Console", event: () => console.log(this.props.Document[DataSym]), icon: "hand-point-right" }); + helpItems.push({ description: "Show Fields ", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "add:right"), icon: "layer-group" }); + !Doc.UserDoc().noviceMode && helpItems.push({ description: "Text Shortcuts Ctrl+/", event: () => this.props.addDocTab(Docs.Create.PdfDocument("/assets/cheat-sheet.pdf", { _width: 300, _height: 300 }), "add:right"), icon: "keyboard" }); + !Doc.UserDoc().noviceMode && helpItems.push({ description: "Print Document in Console", event: () => console.log(this.props.Document), icon: "hand-point-right" }); + !Doc.UserDoc().noviceMode && helpItems.push({ description: "Print DataDoc in Console", event: () => console.log(this.props.Document[DataSym]), icon: "hand-point-right" }); cm.addItem({ description: "Help...", noexpand: true, subitems: helpItems, icon: "question" }); } @@ -1204,6 +1204,7 @@ export class DocumentView extends React.Component { @observable public docView: DocumentViewInternal | undefined | null; + showContextMenu(pageX:number, pageY:number) { return this.docView?.onContextMenu(undefined, pageX, pageY); } get Document() { return this.props.Document; } get topMost() { return this.props.renderDepth === 0; } get rootDoc() { return this.docView?.rootDoc || this.Document; } diff --git a/src/client/views/topbar/TopBar.scss b/src/client/views/topbar/TopBar.scss index 6662386d4..c5b340514 100644 --- a/src/client/views/topbar/TopBar.scss +++ b/src/client/views/topbar/TopBar.scss @@ -22,9 +22,9 @@ grid-auto-columns: 33.3% 33.3% 33.3%; align-items: center; - &:first-child { - height: 20px; - } + // &:first-child { + // height: 20px; + // } } .topbar-button-text { diff --git a/src/client/views/topbar/TopBar.tsx b/src/client/views/topbar/TopBar.tsx index 57ceac2dd..5bee58ccb 100644 --- a/src/client/views/topbar/TopBar.tsx +++ b/src/client/views/topbar/TopBar.tsx @@ -3,14 +3,18 @@ import { Tooltip } from "@material-ui/core"; import { action } from "mobx"; import { observer } from "mobx-react"; import * as React from 'react'; -import { Doc, DocListCast } from '../../../fields/Doc'; +import { AclAdmin, Doc, DocListCast } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; import { Cast, StrCast } from '../../../fields/Types'; +import { GetEffectiveAcl } from "../../../fields/util"; import { Utils } from '../../../Utils'; import { CurrentUserUtils } from "../../util/CurrentUserUtils"; +import { DocumentManager } from "../../util/DocumentManager"; +import { SelectionManager } from "../../util/SelectionManager"; import { SettingsManager } from "../../util/SettingsManager"; import { SharingManager } from "../../util/SharingManager"; import { undoBatch, UndoManager } from "../../util/UndoManager"; +import { ContextMenu } from "../ContextMenu"; import { Borders, Colors } from "../global/globalEnums"; import { MainView } from "../MainView"; import "./TopBar.scss"; @@ -22,22 +26,38 @@ import "./TopBar.scss"; @observer export class TopBar extends React.Component { navigateToHome = () => { - CurrentUserUtils.closeActiveDashboard(); - CurrentUserUtils.CaptureDashboardThumbnail()?.then(() => Doc.UserDoc().activePage = "home"); + CurrentUserUtils.CaptureDashboardThumbnail()?.then(() => { + Doc.UserDoc().activePage = "home"; + CurrentUserUtils.closeActiveDashboard(); // bcz: if we do this, we need some other way to keep track, for user convenience, of the last dashboard in use + }); } render() { - const myDashboards = DocListCast(CurrentUserUtils.MyDashboards.data); const activeDashboard = Cast(Doc.UserDoc().activeDashboard, Doc, null) return ( //TODO:glr Add support for light / dark mode
-
Home
+ {activeDashboard ?
{Doc.CurrentUserEmail} (Placeholder)
: (null)}
+
SelectionManager.SelectView(DocumentManager.Instance.getDocumentView(activeDashboard)!, false)}> + {activeDashboard ? StrCast(activeDashboard.title) : "Dash"} +
+
{ + const dashView = DocumentManager.Instance.getDocumentView(activeDashboard); + ContextMenu.Instance.addItem({ description: "Open Dashboard View", event: this.navigateToHome, icon: "edit" }); + dashView?.showContextMenu(e.clientX+20, e.clientY+30); + }}> + +
+
{SharingManager.Instance.open(undefined, activeDashboard)}}> + {/* TODO: if this is my dashboard, display share + if this is a shared dashboard, display "view original or view annotated" */} + { Doc.GetProto(CurrentUserUtils.ActiveDashboard)?.author === Doc.CurrentUserEmail ? "Share": "view original" } +
window.open( "https://brown-dash.github.io/Dash-Documentation/", "_blank")}> @@ -47,21 +67,6 @@ export class TopBar extends React.Component {
-
-
- {activeDashboard ?
Freeform View (Placeholder)
: (null)} -
-
-
- {activeDashboard ? StrCast(activeDashboard.title) : "Dash"} -
-
-
- {/* TODO: if this is my dashboard, display share - if this is a shared dashboard, display "view original or view annotated" */} -
{SharingManager.Instance.open(undefined, activeDashboard)}}>Share
-
-
); -- cgit v1.2.3-70-g09d2 From 4909910355c321c3776a6405fd131bfb2954c4b9 Mon Sep 17 00:00:00 2001 From: bobzel Date: Fri, 10 Jun 2022 10:23:48 -0400 Subject: added snapshot and explore back into topbar for now. --- src/client/views/topbar/TopBar.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/client/views/topbar/TopBar.tsx b/src/client/views/topbar/TopBar.tsx index 5bee58ccb..0efde8197 100644 --- a/src/client/views/topbar/TopBar.tsx +++ b/src/client/views/topbar/TopBar.tsx @@ -47,10 +47,20 @@ export class TopBar extends React.Component {
{ const dashView = DocumentManager.Instance.getDocumentView(activeDashboard); ContextMenu.Instance.addItem({ description: "Open Dashboard View", event: this.navigateToHome, icon: "edit" }); + ContextMenu.Instance.addItem({ description: "Snapshot Dashboard", event: async () => { + const batch = UndoManager.StartBatch("snapshot"); + await CurrentUserUtils.snapshotDashboard(Doc.UserDoc()); + batch.end(); + }, icon: "edit" }); dashView?.showContextMenu(e.clientX+20, e.clientY+30); }}> - +
+ Browsing mode for directly navigating to documents
} placement="bottom"> +
MainView.Instance._exploreMode = !MainView.Instance._exploreMode)}> + +
+
{SharingManager.Instance.open(undefined, activeDashboard)}}> -- cgit v1.2.3-70-g09d2 From 286740eab0a5111a155bd688a6ce0ab69909303e Mon Sep 17 00:00:00 2001 From: bobzel Date: Fri, 10 Jun 2022 10:29:41 -0400 Subject: added logout to user label --- src/client/views/topbar/TopBar.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/client/views/topbar/TopBar.tsx b/src/client/views/topbar/TopBar.tsx index 0efde8197..0db3950a2 100644 --- a/src/client/views/topbar/TopBar.tsx +++ b/src/client/views/topbar/TopBar.tsx @@ -38,7 +38,10 @@ export class TopBar extends React.Component {
- {activeDashboard ?
{Doc.CurrentUserEmail} (Placeholder)
: (null)} + {activeDashboard ?
{ + ContextMenu.Instance.addItem({ description: "Logout", event: () => window.location.assign(Utils.prepend("/logout")), icon: "edit" }); + ContextMenu.Instance.displayMenu(e.clientX +5, e.clientY + 10); + }}>{Doc.CurrentUserEmail}
: (null)}
SelectionManager.SelectView(DocumentManager.Instance.getDocumentView(activeDashboard)!, false)}> -- cgit v1.2.3-70-g09d2 From 910131b13080a2649606ef8a618c77fd81248343 Mon Sep 17 00:00:00 2001 From: bobzel Date: Fri, 10 Jun 2022 10:39:00 -0400 Subject: fixed errors --- src/client/util/DragManager.ts | 2 +- src/client/views/nodes/RecordingBox/RecordingBox.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index c3c6c22df..dbfba8992 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -455,7 +455,7 @@ export namespace DragManager { if (dragData instanceof DocumentDragData) { dragData.userDropAction = e.ctrlKey && e.altKey ? "copy" : e.ctrlKey ? "alias" : dragData.defaultDropAction; } - if (((e.target as any)?.className === "lm_tabs" || ((e.target as any)?.className === "lm_header" || e?.shiftKey) && dragData.draggedDocuments.length === 1) { + if (((e.target as any)?.className === "lm_tabs" || (e.target as any)?.className === "lm_header" || e?.shiftKey) && dragData.draggedDocuments.length === 1) { if (!startWindowDragTimer) { startWindowDragTimer = setTimeout(async () => { startWindowDragTimer = undefined; diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx index eac1c63f9..10393624b 100644 --- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx +++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx @@ -55,7 +55,7 @@ export class RecordingBox extends ViewBoxBaseComponent() { render() { return
- {!this.result && } + {!this.result && }
; } } -- cgit v1.2.3-70-g09d2 From 78b3b4f194b94a230ff0b07e397b595700d6529f Mon Sep 17 00:00:00 2001 From: bobzel Date: Fri, 10 Jun 2022 10:52:45 -0400 Subject: fixed some errors --- src/client/documents/Documents.ts | 4 ++++ src/client/views/nodes/trails/PresElementBox.tsx | 11 +++++------ 2 files changed, 9 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 088593871..0bb6ce27f 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -191,6 +191,10 @@ export class DocumentOptions { childDontRegisterViews?: boolean; childHideLinkButton?: boolean; // hide link buttons on all children hideLinkButton?: boolean; // whether the blue link counter button should be hidden + hideDecorationTitle?: boolean; + hideOpenButton?: boolean; + hideResizeHandles?: boolean; + hideDocumentButtonBar?: boolean; hideAllLinks?: boolean; // whether all individual blue anchor dots should be hidden isTemplateForField?: string; // the field key for which the containing document is a rendering template isTemplateDoc?: boolean; diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx index 9ad13eb84..50df00612 100644 --- a/src/client/views/nodes/trails/PresElementBox.tsx +++ b/src/client/views/nodes/trails/PresElementBox.tsx @@ -2,7 +2,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Tooltip } from "@material-ui/core"; import { action, computed, IReactionDisposer, observable, reaction } from "mobx"; import { observer } from "mobx-react"; -import { Doc, DocListCast, Opt } from "../../../../fields/Doc"; +import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../../fields/Doc"; import { Id } from "../../../../fields/FieldSymbols"; import { BoolCast, Cast, NumCast, StrCast } from "../../../../fields/Types"; import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents } from "../../../../Utils"; @@ -328,7 +328,7 @@ export class PresElementBox extends ViewBoxBaseComponent() { @undoBatch @action - hideRecording = (e: React.PointerEvent) => { + hideRecording = (e: React.MouseEvent) => { e.stopPropagation() this.removeAllRecordingInOverlay() } @@ -366,8 +366,7 @@ export class PresElementBox extends ViewBoxBaseComponent() { hideDecorationTitle: true, hideOpenButton: true, // hideDeleteButton: true, - cloneFieldFilter: - new List(["system"]) + cloneFieldFilter: new List(["system"]) }); // attach the recording to the slide, and attach the slide to the recording @@ -375,8 +374,8 @@ export class PresElementBox extends ViewBoxBaseComponent() { activeItem.recording = recording // make recording box appear in the bottom right corner of the screen - recording.x = window.innerWidth - recording._width - 20; - recording.y = window.innerHeight - recording._height - 20; + recording.x = window.innerWidth - recording[WidthSym]() - 20; + recording.y = window.innerHeight - recording[HeightSym]() - 20; Doc.AddDocToList((Doc.UserDoc().myOverlayDocs as Doc), undefined, recording); } } -- cgit v1.2.3-70-g09d2