diff options
Diffstat (limited to 'src')
105 files changed, 3039 insertions, 1888 deletions
diff --git a/src/.DS_Store b/src/.DS_Store Binary files differindex 06389d6ae..c363efb13 100644 --- a/src/.DS_Store +++ b/src/.DS_Store diff --git a/src/Utils.ts b/src/Utils.ts index 7f83ab8f5..29824ba94 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -3,10 +3,10 @@ import v5 = require('uuid/v5'); import { ColorState } from 'react-color'; import * as rp from 'request-promise'; import { Socket } from 'socket.io'; +import { DocumentType } from './client/documents/DocumentTypes'; import { Colors } from './client/views/global/globalEnums'; import { Message } from './server/Message'; import Color = require('color'); -import { DocumentType } from './client/documents/DocumentTypes'; export namespace Utils { export let CLICK_TIME = 300; @@ -758,8 +758,8 @@ export function DashColor(color: string) { } export function lightOrDark(color: any) { - if (color === 'transparent') return 'gray'; - if (color.startsWith?.('linear')) return 'black'; + if (color === 'transparent') return Colors.DARK_GRAY; + if (color.startsWith?.('linear')) return Colors.BLACK; const nonAlphaColor = color.startsWith('#') ? (color as string).substring(0, 7) : color.startsWith('rgba') ? color.replace(/,.[^,]*\)/, ')').replace('rgba', 'rgb') : color; const col = DashColor(nonAlphaColor).rgb(); const colsum = col.red() + col.green() + col.blue(); diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts index 53c7b857a..5fdea131b 100644 --- a/src/client/DocServer.ts +++ b/src/client/DocServer.ts @@ -1,6 +1,5 @@ import { runInAction } from 'mobx'; import * as rp from 'request-promise'; -import * as io from 'socket.io-client'; import { Doc, DocListCast, Opt } from '../fields/Doc'; import { UpdatingFromServer } from '../fields/DocSymbols'; import { FieldLoader } from '../fields/FieldLoader'; @@ -8,13 +7,13 @@ import { HandleUpdate, Id, Parent } from '../fields/FieldSymbols'; import { ObjectField } from '../fields/ObjectField'; import { RefField } from '../fields/RefField'; import { DocCast, StrCast } from '../fields/Types'; -import MobileInkOverlay from '../mobile/MobileInkOverlay'; +//import MobileInkOverlay from '../mobile/MobileInkOverlay'; import { emptyFunction, Utils } from '../Utils'; import { GestureContent, MessageStore, MobileDocumentUploadContent, MobileInkOverlayContent, UpdateMobileInkOverlayPositionContent, YoutubeQueryTypes } from './../server/Message'; import { DocumentType } from './documents/DocumentTypes'; import { LinkManager } from './util/LinkManager'; import { SerializationHelper } from './util/SerializationHelper'; -import { GestureOverlay } from './views/GestureOverlay'; +//import { GestureOverlay } from './views/GestureOverlay'; /** * This class encapsulates the transfer and cross-client synchronization of @@ -189,17 +188,17 @@ export namespace DocServer { // mobile ink overlay socket events to communicate between mobile view and desktop view _socket.addEventListener('receiveGesturePoints', (content: GestureContent) => { - MobileInkOverlay.Instance.drawStroke(content); + // MobileInkOverlay.Instance.drawStroke(content); }); _socket.addEventListener('receiveOverlayTrigger', (content: MobileInkOverlayContent) => { - GestureOverlay.Instance.enableMobileInkOverlay(content); - MobileInkOverlay.Instance.initMobileInkOverlay(content); + //GestureOverlay.Instance.enableMobileInkOverlay(content); + // MobileInkOverlay.Instance.initMobileInkOverlay(content); }); _socket.addEventListener('receiveUpdateOverlayPosition', (content: UpdateMobileInkOverlayPositionContent) => { - MobileInkOverlay.Instance.updatePosition(content); + // MobileInkOverlay.Instance.updatePosition(content); }); _socket.addEventListener('receiveMobileDocumentUpload', (content: MobileDocumentUploadContent) => { - MobileInkOverlay.Instance.uploadDocument(content); + // MobileInkOverlay.Instance.uploadDocument(content); }); } diff --git a/src/client/apis/youtube/YoutubeBox.tsx b/src/client/apis/youtube/YoutubeBox.tsx index 05879a247..2da9927c0 100644 --- a/src/client/apis/youtube/YoutubeBox.tsx +++ b/src/client/apis/youtube/YoutubeBox.tsx @@ -6,7 +6,7 @@ import { Cast, NumCast, StrCast } from '../../../fields/Types'; import { Utils } from '../../../Utils'; import { DocServer } from '../../DocServer'; import { Docs } from '../../documents/Documents'; -import { DocumentDecorations } from '../../views/DocumentDecorations'; +import { DocumentView } from '../../views/nodes/DocumentView'; import { FieldView, FieldViewProps } from '../../views/nodes/FieldView'; import '../../views/nodes/WebBox.scss'; import './YoutubeBox.scss'; @@ -355,9 +355,9 @@ export class YoutubeBox extends React.Component<FieldViewProps> { </div> ); - const frozen = !this.props.isSelected() || DocumentDecorations.Instance.Interacting; + const frozen = !this.props.isSelected() || DocumentView.Interacting; - const classname = 'webBox-cont' + (this.props.isSelected() && Doc.ActiveTool === InkTool.None && !DocumentDecorations.Instance.Interacting ? '-interactive' : ''); + const classname = 'webBox-cont' + (this.props.isSelected() && Doc.ActiveTool === InkTool.None && !DocumentView.Interacting ? '-interactive' : ''); return ( <> <div className={classname}>{content}</div> diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts index 1d0ddce40..f8d129e79 100644 --- a/src/client/documents/DocumentTypes.ts +++ b/src/client/documents/DocumentTypes.ts @@ -37,6 +37,7 @@ export enum DocumentType { YOUTUBE = 'youtube', COMPARISON = 'comparison', GROUP = 'group', + PUSHPIN = "pushpin", SCRIPTDB = 'scriptdb', // database of scripts GROUPDB = 'groupdb', // database of groups diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index e7d3e7efc..919958b24 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -19,7 +19,6 @@ import { aggregateBounds, OmitKeys, Utils } from '../../Utils'; import { YoutubeBox } from '../apis/youtube/YoutubeBox'; import { DocServer } from '../DocServer'; import { Networking } from '../Network'; -import { DocumentManager } from '../util/DocumentManager'; import { DragManager, dropActionType } from '../util/DragManager'; import { DirectoryImportBox } from '../util/Import & Export/DirectoryImportBox'; import { FollowLinkScript } from '../util/LinkFollower'; @@ -34,12 +33,12 @@ import { ContextMenuProps } from '../views/ContextMenuItem'; import { DFLT_IMAGE_NATIVE_DIM } from '../views/global/globalCssVariables.scss'; import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, InkingStroke } from '../views/InkingStroke'; import { AudioBox } from '../views/nodes/AudioBox'; -import { FontIconBox } from '../views/nodes/FontIconBox/FontIconBox'; import { ColorBox } from '../views/nodes/ColorBox'; import { ComparisonBox } from '../views/nodes/ComparisonBox'; import { DataVizBox } from '../views/nodes/DataVizBox/DataVizBox'; import { EquationBox } from '../views/nodes/EquationBox'; import { FieldViewProps } from '../views/nodes/FieldView'; +import { FontIconBox } from '../views/nodes/FontIconBox/FontIconBox'; import { FormattedTextBox } from '../views/nodes/formattedText/FormattedTextBox'; import { FunctionPlotBox } from '../views/nodes/FunctionPlotBox'; import { ImageBox } from '../views/nodes/ImageBox'; @@ -49,6 +48,7 @@ import { LinkBox } from '../views/nodes/LinkBox'; import { LinkDescriptionPopup } from '../views/nodes/LinkDescriptionPopup'; import { LoadingBox } from '../views/nodes/LoadingBox'; import { MapBox } from '../views/nodes/MapBox/MapBox'; +import { MapPushpinBox } from '../views/nodes/MapBox/MapPushpinBox'; import { PDFBox } from '../views/nodes/PDFBox'; import { PhysicsSimulationBox } from '../views/nodes/PhysicsBox/PhysicsSimulationBox'; import { RecordingBox } from '../views/nodes/RecordingBox/RecordingBox'; @@ -74,6 +74,8 @@ export class FInfo { readOnly: boolean = false; fieldType?: string = ''; values?: Field[]; + + filterable?: boolean = true; // format?: string; // format to display values (e.g, decimal places, $, etc) // parse?: ScriptField; // parse a value from a string constructor(d: string, readOnly?: boolean) { @@ -84,50 +86,61 @@ export class FInfo { class BoolInfo extends FInfo { fieldType? = 'boolean'; values?: boolean[] = [true, false]; + constructor(d: string, filterable?: boolean) { + super(d); + this.filterable = filterable; + } } class NumInfo extends FInfo { fieldType? = 'number'; values?: number[] = []; - constructor(d: string, readOnly?: boolean, values?: number[]) { + constructor(d: string, filterable?: boolean, readOnly?: boolean, values?: number[]) { super(d, readOnly); this.values = values; + this.filterable = filterable; } } class StrInfo extends FInfo { fieldType? = 'string'; values?: string[] = []; - constructor(d: string, readOnly?: boolean, values?: string[]) { + constructor(d: string, filterable?: boolean, readOnly?: boolean, values?: string[]) { super(d, readOnly); this.values = values; + this.filterable = filterable; } } class DocInfo extends FInfo { fieldType? = 'Doc'; values?: Doc[] = []; - constructor(d: string, values?: Doc[]) { + constructor(d: string, filterable?: boolean, values?: Doc[]) { super(d, true); this.values = values; + this.filterable = filterable; } } class DimInfo extends FInfo { fieldType? = 'enumeration'; values? = [DimUnit.Pixel, DimUnit.Ratio]; readOnly = false; + filterable = false; } class PEInfo extends FInfo { fieldType? = 'enumeration'; values? = ['all', 'none']; readOnly = false; + filterable = false; } class DAInfo extends FInfo { fieldType? = 'enumeration'; values? = ['embed', 'copy', 'move', 'same', 'proto', 'none']; readOnly = false; + filterable = false; } class CTypeInfo extends FInfo { fieldType? = 'enumeration'; values? = Array.from(Object.keys(CollectionViewType)); readOnly = false; + filterable = false; } class DTypeInfo extends FInfo { fieldType? = 'enumeration'; @@ -137,10 +150,16 @@ class DTypeInfo extends FInfo { class DateInfo extends FInfo { fieldType? = 'date'; values?: DateField[] = []; + filterable = true; +} +class ListInfo extends FInfo { + fieldType? = 'list'; + values?: List<any>[] = []; } type BOOLt = BoolInfo | boolean; type NUMt = NumInfo | number; type STRt = StrInfo | string; +type LISTt = ListInfo | List<any>; type DOCt = DocInfo | Doc; type DIMt = DimInfo | typeof DimUnit.Pixel | typeof DimUnit.Ratio; type PEVt = PEInfo | 'none' | 'all'; @@ -150,23 +169,26 @@ type DATEt = DateInfo | number; type DTYPEt = DTypeInfo | string; export class DocumentOptions { // coordinate and dimensions depending on view - x?: NUMt = new NumInfo('x coordinate of document in a freeform view'); - y?: NUMt = new NumInfo('y coordinage of document in a freeform view'); - z?: NUMt = new NumInfo('whether document is in overlay (1) or not (0)', false, [1, 0]); - _dimMagnitude?: NUMt = new NumInfo("magnitude of collectionMulti{row,col} element's width or height"); + x?: NUMt = new NumInfo('x coordinate of document in a freeform view', false); + y?: NUMt = new NumInfo('y coordinage of document in a freeform view', false); + z?: NUMt = new NumInfo('whether document is in overlay (1) or not (0)', false, false, [1, 0]); + _dimMagnitude?: NUMt = new NumInfo("magnitude of collectionMulti{row,col} element's width or height", false); _dimUnit?: DIMt = new DimInfo("units of collectionMulti{row,col} element's width or height - 'px' or '*' for pixels or relative units"); - lat?: NUMt = new NumInfo('latitude coordinate for map views'); - lng?: NUMt = new NumInfo('longitude coordinate for map views'); - _timecodeToShow?: NUMt = new NumInfo('the time that a document should be displayed (e.g., when an annotation shows up as a video plays)'); - _timecodeToHide?: NUMt = new NumInfo('the time that a document should be hidden'); + latitude?: NUMt = new NumInfo('latitude coordinate for map views', false); + longitude?: NUMt = new NumInfo('longitude coordinate for map views', false); + map?: STRt = new StrInfo('text location of map'); + map_type?: STRt = new StrInfo('type of map view', false); + map_zoom?: NUMt = new NumInfo('zoom of a map view', false); + _timecodeToShow?: NUMt = new NumInfo('the time that a document should be displayed (e.g., when an annotation shows up as a video plays)', false); + _timecodeToHide?: NUMt = new NumInfo('the time that a document should be hidden', false); _width?: NUMt = new NumInfo('displayed width of a document'); _height?: NUMt = new NumInfo('displayed height of document'); - data_nativeWidth?: NUMt = new NumInfo('native width of data field contents (e.g., the pixel width of an image)'); - data_nativeHeight?: NUMt = new NumInfo('native height of data field contents (e.g., the pixel height of an image)'); - linearBtnWidth?: NUMt = new NumInfo('unexpanded width of a linear menu button (button "width" changes when it expands)'); - _nativeWidth?: NUMt = new NumInfo('native width of document contents (e.g., the pixel width of an image)'); - _nativeHeight?: NUMt = new NumInfo('native height of document contents (e.g., the pixel height of an image)'); - _nativeDimModifiable?: BOOLt = new BoolInfo('native dimensions can be modified using document decoration reizers'); + data_nativeWidth?: NUMt = new NumInfo('native width of data field contents (e.g., the pixel width of an image)', false); + data_nativeHeight?: NUMt = new NumInfo('native height of data field contents (e.g., the pixel height of an image)', false); + linearBtnWidth?: NUMt = new NumInfo('unexpanded width of a linear menu button (button "width" changes when it expands)', false); + _nativeWidth?: NUMt = new NumInfo('native width of document contents (e.g., the pixel width of an image)', false); + _nativeHeight?: NUMt = new NumInfo('native height of document contents (e.g., the pixel height of an image)', false); + _nativeDimModifiable?: BOOLt = new BoolInfo('native dimensions can be modified using document decoration reizers', false); _nativeHeightUnfrozen?: BOOLt = new BoolInfo('native height can be changed independent of width by dragging decoration resizers'); 'acl-Guest'?: STRt = new StrInfo("permissions granted to users logged in as 'guest' (either view, or private)"); // public permissions @@ -174,30 +196,30 @@ export class DocumentOptions { type?: DTYPEt = new DTypeInfo('type of document', true); type_collection?: COLLt = new CTypeInfo('how collection is rendered'); // sub type of a collection _type_collection?: COLLt = new CTypeInfo('how collection is rendered'); // sub type of a collection - title?: STRt = new StrInfo('title of document'); + title?: STRt = new StrInfo('title of document', true); caption?: RichTextField; author?: string; // STRt = new StrInfo('creator of document'); // bcz: don't change this. Otherwise, the userDoc's field Infos will have a FieldInfo assigned to its author field which will render it unreadable author_date?: DATEt = new DateInfo('date the document was created', true); - annotationOn?: DOCt = new DocInfo('document annotated by this document'); - embedContainer?: DOCt = new DocInfo('document that displays (contains) this discument'); - color?: STRt = new StrInfo('foreground color data doc'); - hidden?: BOOLt = new BoolInfo('whether the document is not rendered by its collection'); - backgroundColor?: STRt = new StrInfo('background color for data doc'); - opacity?: NUMt = new NumInfo('document opacity'); - viewTransitionTime?: NUMt = new NumInfo('transition duration for view parameters'); - dontRegisterView?: BOOLt = new BoolInfo('are views of this document registered so that they can be found when following links, etc'); + annotationOn?: DOCt = new DocInfo('document annotated by this document', false); + embedContainer?: DOCt = new DocInfo('document that displays (contains) this discument', false); + color?: STRt = new StrInfo('foreground color data doc', false); + hidden?: BOOLt = new BoolInfo('whether the document is not rendered by its collection', false); + backgroundColor?: STRt = new StrInfo('background color for data doc', false); + opacity?: NUMt = new NumInfo('document opacity', false); + viewTransitionTime?: NUMt = new NumInfo('transition duration for view parameters', false); + dontRegisterView?: BOOLt = new BoolInfo('are views of this document registered so that they can be found when following links, etc', false); _undoIgnoreFields?: List<string>; //'fields that should not be added to the undo stack (opacity for Undo/Redo/and sidebar) AND whether modifications to document are undoable (true for linearview menu buttons to prevent open/close from entering undo stack)' undoIgnoreFields?: List<string>; //'fields that should not be added to the undo stack (opacity for Undo/Redo/and sidebar) AND whether modifications to document are undoable (true for linearview menu buttons to prevent open/close from entering undo stack)' - _headerHeight?: NUMt = new NumInfo('height of document header used for displaying title'); - _headerFontSize?: NUMt = new NumInfo('font size of header of custom notes'); + _headerHeight?: NUMt = new NumInfo('height of document header used for displaying title', false); + _headerFontSize?: NUMt = new NumInfo('font size of header of custom notes', false); _headerPointerEvents?: PEVt = new PEInfo('types of events the header of a custom text document can consume'); _lockedPosition?: BOOLt = new BoolInfo("lock the x,y coordinates of the document so that it can't be dragged"); _lockedTransform?: BOOLt = new BoolInfo('lock the freeform_panx,freeform_pany and scale parameters of the document so that it be panned/zoomed'); layout?: string | Doc; // default layout string or template document - layout_keyValue?: STRt = new StrInfo('layout definition for showing keyValue view of document'); - layout_explainer?: STRt = new StrInfo('explanation displayed at top of a collection to describe its purpose'); - layout_headerButton?: DOCt = new DocInfo('the (button) Doc to display at the top of a collection.'); + layout_keyValue?: STRt = new StrInfo('layout definition for showing keyValue view of document', false); + layout_explainer?: STRt = new StrInfo('explanation displayed at top of a collection to describe its purpose', false); + layout_headerButton?: DOCt = new DocInfo('the (button) Doc to display at the top of a collection.', false); layout_disableBrushing?: BOOLt = new BoolInfo('whether to suppress border highlighting'); layout_unrendered?: BOOLt = new BoolInfo('denotes an annotation that is not rendered with a DocumentView (e.g, rtf/pdf text selections and links to scroll locations in web/pdf)'); layout_hideOpenButton?: BOOLt = new BoolInfo('whether to hide the open full screen button when selected'); @@ -209,33 +231,33 @@ export class DocumentOptions { layout_hideDecorationTitle?: BOOLt = new BoolInfo('whether to suppress the document decortations title when selected'); layout_borderRounding?: string; layout_boxShadow?: string; // box-shadow css string OR "standard" to use dash standard box shadow - layout_maxAutoHeight?: NUMt = new NumInfo('maximum height for newly created (eg, from pasting) text documents'); + layout_maxAutoHeight?: NUMt = new NumInfo('maximum height for newly created (eg, from pasting) text documents', false); _layout_autoHeight?: BOOLt = new BoolInfo('whether document automatically resizes vertically to display contents'); - _layout_curPage?: NUMt = new NumInfo('current page of a PDF or other? paginated document'); - _layout_currentTimecode?: NUMt = new NumInfo('the current timecode of a time-based document (e.g., current time of a video) value is in seconds'); + _layout_curPage?: NUMt = new NumInfo('current page of a PDF or other? paginated document', false); + _layout_currentTimecode?: NUMt = new NumInfo('the current timecode of a time-based document (e.g., current time of a video) value is in seconds', false); _layout_hideContextMenu?: BOOLt = new BoolInfo('whether the context menu can be shown'); _layout_fitWidth?: BOOLt = new BoolInfo('whether document should scale its contents to fit its rendered width or not (e.g., for PDFviews)'); _layout_fitContentsToBox?: BOOLt = new BoolInfo('whether a freeformview should zoom/scale to create a shrinkwrapped view of its content'); - _layout_fieldKey?: STRt = new StrInfo('the field key containing the current layout definition'); + _layout_fieldKey?: STRt = new StrInfo('the field key containing the current layout definition', false); _layout_enableAltContentUI?: BOOLt = new BoolInfo('whether to show alternate content button'); _layout_showTitle?: string; // field name to display in header (:hover is an optional suffix) _layout_showSidebar?: BOOLt = new BoolInfo('whether an annotationsidebar should be displayed for text docuemnts'); _layout_showCaption?: string; // which field to display in the caption area. leave empty to have no caption _chromeHidden?: BOOLt = new BoolInfo('whether the editing chrome for a document is hidden'); - _gridGap?: NUMt = new NumInfo('gap between items in masonry view'); - _xMargin?: NUMt = new NumInfo('gap between left edge of document and start of masonry/stacking layouts'); - _yMargin?: NUMt = new NumInfo('gap between top edge of dcoument and start of masonry/stacking layouts'); - _xPadding?: NUMt = new NumInfo('x padding'); - _yPadding?: NUMt = new NumInfo('y padding'); + _gridGap?: NUMt = new NumInfo('gap between items in masonry view', false); + _xMargin?: NUMt = new NumInfo('gap between left edge of document and start of masonry/stacking layouts', false); + _yMargin?: NUMt = new NumInfo('gap between top edge of dcoument and start of masonry/stacking layouts', false); + _xPadding?: NUMt = new NumInfo('x padding', false); + _yPadding?: NUMt = new NumInfo('y padding', false); _singleLine?: boolean; // whether label box is restricted to one line of text _createDocOnCR?: boolean; // whether carriage returns and tabs create new text documents - _columnWidth?: NUMt = new NumInfo('width of table column'); + _columnWidth?: NUMt = new NumInfo('width of table column', false); _columnsHideIfEmpty?: BOOLt = new BoolInfo('whether stacking view column headings should be hidden'); - _caption_xMargin?: NUMt = new NumInfo('x margin of caption inside of a carousel collection', true); - _caption_yMargin?: NUMt = new NumInfo('y margin of caption inside of a carousel collection', true); - icon_nativeWidth?: NUMt = new NumInfo('native width of icon view', true); - icon_nativeHeight?: NUMt = new NumInfo('native height of icon view', true); + _caption_xMargin?: NUMt = new NumInfo('x margin of caption inside of a carousel collection', false, true); + _caption_yMargin?: NUMt = new NumInfo('y margin of caption inside of a carousel collection', false, true); + icon_nativeWidth?: NUMt = new NumInfo('native width of icon view', false, true); + icon_nativeHeight?: NUMt = new NumInfo('native height of icon view', false, true); _text_fontSize?: string; _text_fontFamily?: string; _text_fontWeight?: string; @@ -243,14 +265,16 @@ export class DocumentOptions { infoWindowOpen?: BOOLt = new BoolInfo('whether info window corresponding to pin is open (on MapDocuments)'); _carousel_index?: NUMt = new NumInfo('which item index the carousel viewer is showing'); - _label_minFontSize?: NUMt = new NumInfo('minimum font size for labelBoxes'); - _label_maxFontSize?: NUMt = new NumInfo('maximum font size for labelBoxes'); - stroke_width?: NUMt = new NumInfo('width of an ink stroke'); - icon_label?: STRt = new StrInfo('label to use for a fontIcon doc (otherwise, the title is used)'); - mediaState?: STRt = new StrInfo('status of audio/video media document: "pendingRecording", "recording", "paused", "playing"'); + _label_minFontSize?: NUMt = new NumInfo('minimum font size for labelBoxes', false); + _label_maxFontSize?: NUMt = new NumInfo('maximum font size for labelBoxes', false); + stroke_width?: NUMt = new NumInfo('width of an ink stroke', false); + icon_label?: STRt = new StrInfo('label to use for a fontIcon doc (otherwise, the title is used)', false); + mediaState?: STRt = new StrInfo('status of audio/video media document: "pendingRecording", "recording", "paused", "playing"', false); recording?: BOOLt = new BoolInfo('whether WebCam is recording or not'); autoPlayAnchors?: BOOLt = new BoolInfo('whether to play audio/video when an anchor is clicked in a stackedTimeline.'); dontPlayLinkOnSelect?: BOOLt = new BoolInfo('whether an audio/video should start playing when a link is followed to it.'); + openFactoryLocation?: string; // an OpenWhere value to place the factory created document + openFactoryAsDelegate?: boolean; // updateContentsScript?: ScriptField; // reactive script invoked when viewing a document that can update contents of a collection (or do anything) toolTip?: string; // tooltip to display on hover toolType?: string; // type of pen tool @@ -263,7 +287,7 @@ export class DocumentOptions { contextMenuIcons?: List<string>; childFilters_boolean?: string; childFilters?: List<string>; - childLimitHeight?: NUMt = new NumInfo('whether to limit the height of collection children. 0 - means height can be no bigger than width'); + childLimitHeight?: NUMt = new NumInfo('whether to limit the height of collection children. 0 - means height can be no bigger than width', false); 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 childDocumentsActive?: BOOLt = new BoolInfo('whether child documents are active when parent is document active'); @@ -275,12 +299,12 @@ export class DocumentOptions { childContextMenuIcons?: List<string>; targetScriptKey?: string; // where to write a template script (used by collections with click templates which need to target onClick, onDoubleClick, etc) - lastFrame?: NUMt = new NumInfo('the last frame of a frame-based collection (e.g., progressive slide)'); - activeFrame?: NUMt = new NumInfo('the active frame of a document in a frame base collection'); - appearFrame?: NUMt = new NumInfo('the frame in which the document appears'); - _currentFrame?: NUMt = new NumInfo('the current frame of a frame-based collection (e.g., progressive slide)'); + lastFrame?: NUMt = new NumInfo('the last frame of a frame-based collection (e.g., progressive slide)', false); + activeFrame?: NUMt = new NumInfo('the active frame of a document in a frame base collection', false); + appearFrame?: NUMt = new NumInfo('the frame in which the document appears', false); + _currentFrame?: NUMt = new NumInfo('the current frame of a frame-based collection (e.g., progressive slide)', false); - isSystem?: BOOLt = new BoolInfo('is this a system created/owned doc', true); + isSystem?: BOOLt = new BoolInfo('is this a system created/owned doc', false); isBaseProto?: BOOLt = new BoolInfo('is doc a base level prototype for data documents as opposed to data documents which are prototypes for layout documents. base protos are not cloned during a deep'); isTemplateForField?: string; // the field key for which the containing document is a rendering template isTemplateDoc?: BOOLt = new BoolInfo('is the document a template for creating other documents'); @@ -289,21 +313,28 @@ export class DocumentOptions { _isTimelineLabel?: BOOLt = new BoolInfo('is document a timeline label'); _isLightbox?: BOOLt = new BoolInfo('whether a collection acts as a lightbox by opening lightbox links by hiding all other documents in collection besides link target'); - config_panX?: NUMt = new NumInfo('panX saved as a view spec'); - config_panY?: NUMt = new NumInfo('panY saved as a view spec'); - config_viewScale?: NUMt = new NumInfo('viewScale saved as a view Spec'); - presentation_transition?: NUMt = new NumInfo('the time taken for the transition TO a document'); - presentation_duration?: NUMt = new NumInfo('the duration of the slide in presentation view'); - presentation_zoomText?: BOOLt = new BoolInfo('whether text anchors should shown in a larger box when following links to make them stand out'); + config_latitude?: NUMt = new NumInfo('latitude of a map', false); + config_longitude?: NUMt = new NumInfo('longitude of map', false); + config_map_zoom?: NUMt = new NumInfo('zoom of map', false); + config_map_type?: STRt = new StrInfo('map view type (e.g, aerial)', false); + config_map?: STRt = new StrInfo('text location of map', false); + config_panX?: NUMt = new NumInfo('panX saved as a view spec', false); + config_panY?: NUMt = new NumInfo('panY saved as a view spec', false); + config_viewScale?: NUMt = new NumInfo('viewScale saved as a view Spec', false); + presentation_transition?: NUMt = new NumInfo('the time taken for the transition TO a document', false); + presentation_duration?: NUMt = new NumInfo('the duration of the slide in presentation view', false); + presentation_zoomText?: BOOLt = new BoolInfo('whether text anchors should shown in a larger box when following links to make them stand out', false); data?: any; data_useCors?: BOOLt = new BoolInfo('whether CORS protocol should be used for web page'); columnHeaders?: List<SchemaHeaderField>; // headers for stacking views schemaHeaders?: List<SchemaHeaderField>; // headers for schema view - dockingConfig?: STRt = new StrInfo('configuration of golden layout windows (applies only if doc is rendered as a CollectionDockingView)'); + dockingConfig?: STRt = new StrInfo('configuration of golden layout windows (applies only if doc is rendered as a CollectionDockingView)', false); icon?: string; // icon used by fonticonbox to render button noteType?: string; + // STOPPING HERE + // freeform properties _freeform_backgroundGrid?: BOOLt = new BoolInfo('whether background grid is shown on freeform collections'); _freeform_scale?: NUMt = new NumInfo('how much a freeform view has been scaled (zoomed)'); @@ -360,11 +391,9 @@ export class DocumentOptions { waitForDoubleClickToClick?: 'always' | 'never' | 'default'; // whether a click function wait for double click to expire. 'default' undefined = wait only if there's a click handler, "never" = never wait, "always" = alway wait onPointerDown?: ScriptField; onPointerUp?: ScriptField; - openFactoryLocation?: string; // an OpenWhere value to place the factory created document - openFactoryAsDelegate?: BOOLt = new BoolInfo('create a delegate of the factory'); _forceActive?: BOOLt = new BoolInfo('flag to handle pointer events when not selected (or otherwise active)'); _dragOnlyWithinContainer?: BOOLt = new BoolInfo('whether the document should remain in its collection when someone tries to drag and drop it elsewhere'); - _raiseWhenDragged?: BOOLt = new BoolInfo('whether a document is brought to front when dragged.'); + _keepZWhenDragged?: BOOLt = new BoolInfo('whether a document should keep its z-order when dragged.'); childDragAction?: DROPt = new DAInfo('what should happen to the child documents when they are dragged from the collection'); dropConverter?: ScriptField; // script to run when documents are dropped on this Document. dropAction?: DROPt = new DAInfo("what should happen to this document when it's dropped somewhere else"); @@ -373,9 +402,9 @@ export class DocumentOptions { cloneFieldFilter?: List<string>; // fields not to copy when the document is clonedclipboard?: Doc; dragWhenActive?: BOOLt = new BoolInfo('should document drag when it is active - e.g., pileView, group'); dragAction?: DROPt = new DAInfo('how to drag document when it is active (e.g., tree, groups)'); - dragFactory_count?: NUMt = new NumInfo('number of items created from a drag button (used for setting title with incrementing index)', true); - dragFactory?: DOCt = new DocInfo('document to create when dragging with a suitable onDragStart script'); - clickFactory?: DOCt = new DocInfo('document to create when clicking on a button with a suitable onClick script'); + dragFactory_count?: NUMt = new NumInfo('number of items created from a drag button (used for setting title with incrementing index)', false, true); + dragFactory?: DOCt = new DocInfo('document to create when dragging with a suitable onDragStart script', false); + clickFactory?: DOCt = new DocInfo('document to create when clicking on a button with a suitable onClick script', false); onDragStart?: ScriptField; //script to execute at start of drag operation -- e.g., when a "creator" button is dragged this script generates a different document to drop target?: Doc; // available for use in scripts. used to provide a document parameter to the script (Note, this is a convenience entry since any field could be used for parameterizing a script) @@ -521,7 +550,7 @@ export namespace Docs { DocumentType.MAP, { layout: { view: MapBox, dataField: defaultDataKey }, - options: { _height: 600, _width: 800, nativeDimModifiable: true, systemIcon: 'BsFillPinMapFill' }, + options: { map: '', _height: 600, _width: 800, nativeDimModifiable: true, systemIcon: 'BsFillPinMapFill' }, }, ], [ @@ -680,7 +709,7 @@ export namespace Docs { DocumentType.DATAVIZ, { layout: { view: DataVizBox, dataField: defaultDataKey }, - options: { dataViz_title: '', _layout_fitWidth: true, nativeDimModifiable: true }, + options: { dataViz_title: '', dataViz_line: '', dataViz_pie: '', dataViz_histogram: '', dataViz: 'table', _layout_fitWidth: true, nativeDimModifiable: true }, }, ], [ @@ -713,6 +742,13 @@ export namespace Docs { }, }, ], + [ + DocumentType.PUSHPIN, + { + layout: { view: MapPushpinBox, dataField: defaultDataKey }, + options: {}, + }, + ], ]); const suffix = 'Proto'; @@ -1059,6 +1095,10 @@ export namespace Docs { return InstanceFromProto(Prototypes.get(DocumentType.MAP), new List(documents), options); } + export function PushpinDocument(latitude: number, longitude: number, infoWindowOpen: boolean, documents: Array<Doc>, options: DocumentOptions, id?: string) { + return InstanceFromProto(Prototypes.get(DocumentType.PUSHPIN), new List(documents), { latitude, longitude, infoWindowOpen, ...options }, id); + } + // shouldn't ever need to create a KVP document-- instead set the LayoutTemplateString to be a KeyValueBox for the DocumentView (see addDocTab in TabDocView) // export function KVPDocument(document: Doc, options: DocumentOptions = {}) { // return InstanceFromProto(Prototypes.get(DocumentType.KVP), document, { title: document.title + '.kvp', ...options }); @@ -1077,9 +1117,6 @@ export namespace Docs { export function HTMLMarkerDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) { return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Freeform }, id); } - export function MapMarkerDocument(lat: number, lng: number, infoWindowOpen: boolean, documents: Array<Doc>, options: DocumentOptions, id?: string) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { lat, lng, infoWindowOpen, ...options, _type_collection: CollectionViewType.Freeform }, id); - } export function PileDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) { return InstanceFromProto( @@ -1254,10 +1291,23 @@ export namespace DocUtils { 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' && fkey !== Utils.noDragsDocFilter.split(Doc.FilterSep)[0])) { const facet = filterFacets[facetKey]; + let links = true; + const linkedTo = filterFacets['-linkedTo'] && Array.from(Object.keys(filterFacets['-linkedTo']))?.[0]; + const linkedToField = filterFacets['-linkedTo']?.[linkedTo]; + const allLinks = linkedTo && linkedToField ? LinkManager.Instance.getAllRelatedLinks(d) : []; + // prettier-ignore + if (linkedTo) { + if (allLinks.some(d => linkedTo === Field.toScriptString(DocCast(DocCast(d.link_anchor_1)?.[linkedToField]))) || // + allLinks.some(d => linkedTo === Field.toScriptString(DocCast(DocCast(d.link_anchor_2)?.[linkedToField])))) + { + links = true; + } + else links = false + } + // 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'); @@ -1273,7 +1323,7 @@ export namespace DocUtils { // 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; + if (!linkedTo && !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); @@ -1292,11 +1342,11 @@ export namespace DocUtils { }); // 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?.childFilters_boolean === 'OR') { - if (satisfiesUnsetsFacets && satisfiesExistsFacets && satisfiesCheckFacets && !failsNotEqualFacets && satisfiesMatchFacets) return true; + if (links && 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; + if (!links || !satisfiesUnsetsFacets || !satisfiesExistsFacets || !satisfiesCheckFacets || failsNotEqualFacets || (matches.length && !satisfiesMatchFacets)) return false; } } return parentCollection?.childFilters_boolean === 'OR' ? false : true; @@ -1320,7 +1370,7 @@ export namespace DocUtils { export let ActiveRecordings: { props: FieldViewProps; getAnchor: (addAsAnnotation: boolean) => Doc }[] = []; export function MakeLinkToActiveAudio(getSourceDoc: () => Doc | undefined, broadcastEvent = true) { - broadcastEvent && runInAction(() => (DocumentManager.Instance.RecordingEvent = DocumentManager.Instance.RecordingEvent + 1)); + broadcastEvent && runInAction(() => (Doc.RecordingEvent = Doc.RecordingEvent + 1)); return DocUtils.ActiveRecordings.map(audio => { const sourceDoc = getSourceDoc(); return sourceDoc && DocUtils.MakeLink(sourceDoc, audio.getAnchor(true) || audio.props.Document, { link_displayLine: false, link_relationship: 'recording annotation:linked recording', link_description: 'recording timeline' }); @@ -1329,7 +1379,6 @@ export namespace DocUtils { export function MakeLink(source: Doc, target: Doc, linkSettings: { link_relationship?: string; link_description?: string; link_displayLine?: boolean }, id?: string, showPopup?: number[]) { if (!linkSettings.link_relationship) linkSettings.link_relationship = target.type === DocumentType.RTF ? 'Commentary:Comments On' : 'link'; - const sv = DocumentManager.Instance.getDocumentView(source); if (target.doc === Doc.UserDoc()) return undefined; const makeLink = action((linkDoc: Doc, showPopup?: number[]) => { @@ -1567,7 +1616,7 @@ export namespace DocUtils { const documentList: ContextMenuProps[] = DocListCast(DocListCast(Doc.MyTools?.data)[0]?.data) .filter(btnDoc => !btnDoc.hidden) .map(btnDoc => Cast(btnDoc?.dragFactory, Doc, null)) - .filter(doc => doc && doc !== Doc.UserDoc().emptyTrail && doc !== Doc.UserDoc().emptyDataViz) + .filter(doc => doc && doc !== Doc.UserDoc().emptyTrail && doc !== Doc.UserDoc().emptyNote && doc.title) .map((dragDoc, i) => ({ description: ':' + StrCast(dragDoc.title).replace('Untitled ', ''), event: undoable((args: { x: number; y: number }) => { @@ -1780,8 +1829,8 @@ export namespace DocUtils { 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); + proto.latitude = ConvertDMSToDD(latitude[0], latitude[1], latitude[2], latitudeDirection); + proto.longitude = ConvertDMSToDD(longitude[0], longitude[1], longitude[2], longitudeDirection); } } if (Upload.isVideoInformation(result)) { @@ -1789,8 +1838,6 @@ export namespace DocUtils { } if (overwriteDoc) { Doc.removeCurrentlyLoading(overwriteDoc); - // loading doc icons are just labels. so any icon views of loading docs need to be replaced with the proper icon view. - DocumentManager.Instance.getAllDocumentViews(overwriteDoc).forEach(dv => StrCast(dv.rootDoc.layout_fieldKey) === 'layout_icon' && dv.iconify(() => dv.iconify())); } generatedDocuments.push(doc); } diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 4dca38b21..1f130ab1e 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -1,7 +1,6 @@ import { observable, reaction, runInAction } from "mobx"; import * as rp from 'request-promise'; import { Doc, DocListCast, DocListCastAsync, Opt } from "../../fields/Doc"; -import { FieldLoader } from "../../fields/FieldLoader"; import { InkTool } from "../../fields/InkField"; import { List } from "../../fields/List"; import { PrefetchProxy } from "../../fields/Proxy"; @@ -12,16 +11,16 @@ import { Cast, DateCast, DocCast, StrCast } from "../../fields/Types"; import { nullAudio } from "../../fields/URLField"; import { SetCachedGroups, SharingPermissions } from "../../fields/util"; import { GestureUtils } from "../../pen-gestures/GestureUtils"; -import { OmitKeys, Utils, addStyleSheetRule } from "../../Utils"; +import { addStyleSheetRule, OmitKeys, Utils } from "../../Utils"; import { DocServer } from "../DocServer"; import { Docs, DocumentOptions, DocUtils, FInfo } from "../documents/Documents"; import { CollectionViewType, DocumentType } from "../documents/DocumentTypes"; import { TreeViewType } from "../views/collections/CollectionTreeView"; import { DashboardView } from "../views/DashboardView"; import { Colors } from "../views/global/globalEnums"; -import { MainView } from "../views/MainView"; -import { ButtonType } from "../views/nodes/FontIconBox/FontIconBox"; import { OpenWhere } from "../views/nodes/DocumentView"; +import { ButtonType } from "../views/nodes/FontIconBox/FontIconBox"; +import { ImportElementBox } from "../views/nodes/importBox/ImportElementBox"; import { OverlayView } from "../views/OverlayView"; import { DragManager, dropActionType } from "./DragManager"; import { MakeTemplate } from "./DropConverter"; @@ -31,7 +30,6 @@ import { ScriptingGlobals } from "./ScriptingGlobals"; import { ColorScheme, SettingsManager } from "./SettingsManager"; import { UndoManager } from "./UndoManager"; import { ImportElementBox } from "../views/nodes/importBox/ImportElementBox"; -import { DocumentManager } from "./DocumentManager"; interface Button { // DocumentOptions fields a button can set @@ -273,7 +271,7 @@ export class CurrentUserUtils { {key: "Webpage", creator: opts => Docs.Create.WebDocument("",opts), opts: { _width: 400, _height: 512, _nativeWidth: 850, data_useCors: true, }}, {key: "Comparison", creator: Docs.Create.ComparisonDocument, opts: { _width: 300, _height: 300 }}, {key: "Audio", creator: opts => Docs.Create.AudioDocument(nullAudio, opts),opts: { _width: 200, _height: 100, }}, - {key: "Map", creator: opts => Docs.Create.MapDocument([], opts), opts: { _width: 800, _height: 600, _layout_fitWidth: true, _layout_showSidebar: true, }}, + {key: "Map", creator: opts => Docs.Create.MapDocument([], opts), opts: { _width: 800, _height: 600, _layout_fitWidth: true, }}, {key: "Screengrab", creator: Docs.Create.ScreenshotDocument, opts: { _width: 400, _height: 200 }}, {key: "WebCam", creator: opts => Docs.Create.WebCamDocument("", opts), opts: { _width: 400, _height: 200, recording:true, isSystem: true, cloneFieldFilter: new List<string>(["isSystem"]) }}, {key: "Button", creator: Docs.Create.ButtonDocument, opts: { _width: 150, _height: 50, _xPadding: 10, _yPadding: 10}, scripts: {onClick: FollowLinkScript()?.script.originalScript ?? ""}}, @@ -624,9 +622,9 @@ export class CurrentUserUtils { static freeTools(): Button[] { return [ - { title: "Bottom", icon: "arrows-down-to-line",toolTip: "Make doc topmost", btnType: ButtonType.ClickButton, expertMode: false, funcs: {}, scripts: { onClick: 'sendToBack()'}}, // Only when floating document is selected in freeform - { title: "Top", icon: "arrows-up-to-line", toolTip: "Make doc bottommost", btnType: ButtonType.ClickButton, expertMode: false, funcs: {}, scripts: { onClick: 'bringToFront()'}}, // Only when floating document is selected in freeform - { title: "Z order", icon: "z", toolTip: "Bring Forward on Drag (double click to set for all)",waitForDoubleClickToClick:true, btnType: ButtonType.ToggleButton, expertMode: false, funcs: {}, scripts: { onClick: 'toggleRaiseOnDrag(false, _readOnly_)', onDoubleClick:`{ return toggleRaiseOnDrag(true, _readOnly_)`}}, // Only when floating document is selected in freeform + { title: "Bottom", icon: "arrows-down-to-line",toolTip: "Make doc topmost", btnType: ButtonType.ClickButton, expertMode: false, funcs: {}, scripts: { onClick: 'sendToBack()'}}, // Only when floating document is selected in freeform + { title: "Top", icon: "arrows-up-to-line", toolTip: "Make doc bottommost", btnType: ButtonType.ClickButton, expertMode: false, funcs: {}, scripts: { onClick: 'bringToFront()'}}, // Only when floating document is selected in freeform + { title: "Z order", icon: "z", toolTip: "Keep Z order on Drag", btnType: ButtonType.ToggleButton, expertMode: false, funcs: {}, scripts: { onClick: '{ return toggleRaiseOnDrag(_readOnly_);}'}}, // Only when floating document is selected in freeform ] } static viewTools(): Button[] { @@ -861,7 +859,6 @@ export class CurrentUserUtils { doc.isSystem ?? (doc.isSystem = true); doc.title ?? (doc.title = Doc.CurrentUserEmail); Doc.noviceMode ?? (Doc.noviceMode = true); - doc._raiseWhenDragged ?? (doc._raiseWhenDragged = true); doc._showLabel ?? (doc._showLabel = true); doc.textAlign ?? (doc.textAlign = "left"); doc.activeTool = InkTool.None; @@ -1016,8 +1013,5 @@ export class CurrentUserUtils { ScriptingGlobals.add(function MySharedDocs() { return Doc.MySharedDocs; }, "document containing all shared Docs"); ScriptingGlobals.add(function IsNoviceMode() { return Doc.noviceMode; }, "is Dash in novice mode"); ScriptingGlobals.add(function toggleComicMode() { Doc.UserDoc().renderStyle = Doc.UserDoc().renderStyle === "comic" ? undefined : "comic"; }, "switches between comic and normal document rendering"); -ScriptingGlobals.add(function createNewPresentation() { return MainView.Instance.createNewPresentation(); }, "creates a new presentation when called"); -ScriptingGlobals.add(function openPresentation(pres:Doc) { return MainView.Instance.openPresentation(pres); }, "creates a new presentation when called"); -ScriptingGlobals.add(function createNewFolder() { return MainView.Instance.createNewFolder(); }, "creates a new folder in myFiles when called"); ScriptingGlobals.add(function importDocument() { return CurrentUserUtils.importDocument(); }, "imports files from device directly into the import sidebar"); ScriptingGlobals.add(function setInkToolDefaults() { Doc.ActiveTool = InkTool.None; }); diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts index 717473aa1..0fd7e840c 100644 --- a/src/client/util/DictationManager.ts +++ b/src/client/util/DictationManager.ts @@ -11,7 +11,7 @@ import { Utils } from '../../Utils'; import { Docs } from '../documents/Documents'; import { DocumentType } from '../documents/DocumentTypes'; import { DictationOverlay } from '../views/DictationOverlay'; -import { DocumentView, OpenWhere, OpenWhereMod } from '../views/nodes/DocumentView'; +import { DocumentView, OpenWhere } from '../views/nodes/DocumentView'; import { SelectionManager } from './SelectionManager'; import { UndoManager } from './UndoManager'; diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index 7c3b5be05..c2827dac7 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -1,4 +1,4 @@ -import { action, computed, observable, ObservableSet } from 'mobx'; +import { action, computed, observable, ObservableSet, observe, reaction } from 'mobx'; import { Doc, DocListCast, Opt } from '../../fields/Doc'; import { AclAdmin, AclEdit, Animation } from '../../fields/DocSymbols'; import { Id } from '../../fields/FieldSymbols'; @@ -23,7 +23,6 @@ export class DocumentManager { //global holds all of the nodes (regardless of which collection they're in) @observable _documentViews = new Set<DocumentView>(); @observable public LinkAnchorBoxViews: DocumentView[] = []; - @observable public RecordingEvent = 0; @observable public LinkedDocumentViews: { a: DocumentView; b: DocumentView; l: Doc }[] = []; @computed public get DocumentViews() { return Array.from(this._documentViews).filter(view => !(view.ComponentView instanceof KeyValueBox) && (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(view.docViewPath))); @@ -41,7 +40,22 @@ export class DocumentManager { } //private constructor so no other class can create a nodemanager - private constructor() {} + private constructor() { + if (!Doc.CurrentlyLoading) Doc.CurrentlyLoading = []; + observe(Doc.CurrentlyLoading, change => { + // watch CurrentlyLoading-- when something is loaded, it's removed from the list and we have to update its icon if it were iconified since LoadingBox icons are different than the media they become + switch (change.type as any) { + case 'update': + break; + case 'remove': + // DocumentManager.Instance.getAllDocumentViews(change as any).forEach(dv => StrCast(dv.rootDoc.layout_fieldKey) === 'layout_icon' && dv.iconify(() => dv.iconify())); + break; + case 'splice': + (change as any).removed.forEach((doc: Doc) => DocumentManager.Instance.getAllDocumentViews(doc).forEach(dv => StrCast(dv.rootDoc.layout_fieldKey) === 'layout_icon' && dv.iconify(() => dv.iconify()))); + break; + } + }); + } private _viewRenderedCbs: { doc: Doc; func: (dv: DocumentView) => any }[] = []; public AddViewRenderedCb = (doc: Opt<Doc>, func: (dv: DocumentView) => any) => { @@ -310,7 +324,7 @@ export class DocumentManager { if (viewSpec && docView) { if (docView.ComponentView instanceof FormattedTextBox) docView.ComponentView?.focus(viewSpec, options); PresBox.restoreTargetDocView(docView, viewSpec, options.zoomTime ?? 500); - Doc.linkFollowHighlight(docView.rootDoc, undefined, options.effect); + Doc.linkFollowHighlight(viewSpec ? [docView.rootDoc, viewSpec] : docView.rootDoc, undefined, options.effect); if (options.playAudio) DocumentManager.playAudioAnno(docView.rootDoc); if (options.toggleTarget && (!options.didMove || docView.rootDoc.hidden)) docView.rootDoc.hidden = !docView.rootDoc.hidden; if (options.effect) docView.rootDoc[Animation] = options.effect; diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index 40bf57555..5a11f2dca 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -62,12 +62,6 @@ export namespace DragManager { export let StartWindowDrag: Opt<(e: { pageX: number; pageY: number }, dragDocs: Doc[], finishDrag?: (aborted: boolean) => void) => void>; export let CompleteWindowDrag: Opt<(aborted: boolean) => void>; - export function GetRaiseWhenDragged() { - return BoolCast(Doc.UserDoc()._raiseWhenDragged); - } - export function SetRaiseWhenDragged(val: boolean) { - Doc.UserDoc()._raiseWhenDragged = val; - } export function Root() { const root = document.getElementById('root'); if (!root) { @@ -401,6 +395,7 @@ export namespace DragManager { dragLabel.style.zIndex = '100001'; dragLabel.style.fontSize = '10px'; dragLabel.style.position = 'absolute'; + dragLabel.style.background = '#ffffff90'; dragLabel.innerText = 'drag titlebar to embed on drop'; // bcz: need to move this to a status bar dragDiv.appendChild(dragLabel); DragManager.Root().appendChild(dragDiv); @@ -516,7 +511,7 @@ export namespace DragManager { runInAction(() => docsBeingDragged.push(...docsToDrag)); const hideDragShowOriginalElements = (hide: boolean) => { - dragLabel.style.display = hide ? '' : 'none'; + dragLabel.style.display = hide && !CanEmbed ? '' : 'none'; !hide && dragElements.map(dragElement => dragElement.parentNode === dragDiv && dragDiv.removeChild(dragElement)); setTimeout(() => eles.forEach(ele => (ele.hidden = hide))); }; @@ -661,18 +656,9 @@ export namespace DragManager { } } -ScriptingGlobals.add(function toggleRaiseOnDrag(forAllDocs: boolean, readOnly?: boolean) { +ScriptingGlobals.add(function toggleRaiseOnDrag(readOnly?: boolean) { if (readOnly) { - if (SelectionManager.Views().length) - return SelectionManager.Views().some(dv => dv.rootDoc.raiseWhenDragged) - ? Colors.MEDIUM_BLUE - : SelectionManager.Views().some(dv => dv.rootDoc.raiseWhenDragged === false) - ? 'transparent' - : DragManager.GetRaiseWhenDragged() - ? Colors.MEDIUM_BLUE_ALT - : Colors.LIGHT_BLUE; - return DragManager.GetRaiseWhenDragged() ? Colors.MEDIUM_BLUE_ALT : 'transparent'; + return SelectionManager.Views().some(dv => dv.rootDoc.keepZWhenDragged); } - if (!forAllDocs) SelectionManager.Views().map(dv => (dv.rootDoc.raiseWhenDragged ? (dv.rootDoc.raiseWhenDragged = undefined) : dv.rootDoc.raiseWhenDragged === false ? (dv.rootDoc.raiseWhenDragged = true) : (dv.rootDoc.raiseWhenDragged = false))); - else DragManager.SetRaiseWhenDragged(!DragManager.GetRaiseWhenDragged()); + SelectionManager.Views().map(dv => (dv.rootDoc.keepZWhenDragged = !dv.rootDoc.keepZWhenDragged)); }); diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts index f235be192..dbdf580cd 100644 --- a/src/client/util/DropConverter.ts +++ b/src/client/util/DropConverter.ts @@ -1,15 +1,15 @@ -import { DragManager } from './DragManager'; import { Doc, DocListCast, Opt } from '../../fields/Doc'; -import { DocumentType } from '../documents/DocumentTypes'; import { ObjectField } from '../../fields/ObjectField'; -import { StrCast, Cast } from '../../fields/Types'; -import { Docs } from '../documents/Documents'; -import { ScriptField, ComputedField } from '../../fields/ScriptField'; import { RichTextField } from '../../fields/RichTextField'; -import { ImageField } from '../../fields/URLField'; -import { ScriptingGlobals } from './ScriptingGlobals'; import { listSpec } from '../../fields/Schema'; +import { ScriptField } from '../../fields/ScriptField'; +import { Cast, StrCast } from '../../fields/Types'; +import { ImageField } from '../../fields/URLField'; +import { Docs } from '../documents/Documents'; +import { DocumentType } from '../documents/DocumentTypes'; import { ButtonType } from '../views/nodes/FontIconBox/FontIconBox'; +import { DragManager } from './DragManager'; +import { ScriptingGlobals } from './ScriptingGlobals'; export function MakeTemplate(doc: Doc, first: boolean = true, rename: Opt<string> = undefined, templateField: string = '') { if (templateField) Doc.GetProto(doc).title = templateField; /// the title determines which field is being templated diff --git a/src/client/util/GroupManager.tsx b/src/client/util/GroupManager.tsx index f35844020..8973306bf 100644 --- a/src/client/util/GroupManager.tsx +++ b/src/client/util/GroupManager.tsx @@ -1,22 +1,22 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Button, IconButton, Size, Type } from 'browndash-components'; import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import Select from 'react-select'; import * as RequestPromise from 'request-promise'; -import { Doc, DocListCast, DocListCastAsync, Opt } from '../../fields/Doc'; -import { StrCast, Cast } from '../../fields/Types'; +import { DateField } from '../../fields/DateField'; +import { Doc, DocListCast, Opt } from '../../fields/Doc'; +import { Id } from '../../fields/FieldSymbols'; +import { listSpec } from '../../fields/Schema'; +import { Cast, StrCast } from '../../fields/Types'; import { Utils } from '../../Utils'; import { MainViewModal } from '../views/MainViewModal'; import { TaskCompletionBox } from '../views/nodes/TaskCompletedBox'; import './GroupManager.scss'; import { GroupMemberView } from './GroupMemberView'; -import { SharingManager, User } from './SharingManager'; -import { listSpec } from '../../fields/Schema'; -import { DateField } from '../../fields/DateField'; -import { Id } from '../../fields/FieldSymbols'; -import { Button, IconButton, Size, Type } from 'browndash-components'; import { SettingsManager } from './SettingsManager'; +import { SharingManager, User } from './SharingManager'; /** * Interface for options for the react-select component @@ -282,7 +282,7 @@ export class GroupManager extends React.Component<{}> { */ private get groupCreationModal() { const contents = ( - <div className="group-create" style={{ background: SettingsManager.Instance.userBackgroundColor, color: SettingsManager.Instance.userColor }}> + <div className="group-create" style={{ background: SettingsManager.userBackgroundColor, color: SettingsManager.userColor }}> <div className="group-heading" style={{ marginBottom: 0 }}> <p> <b>New Group</b> @@ -367,7 +367,7 @@ export class GroupManager extends React.Component<{}> { const groups = this.groupSort === 'ascending' ? this.allGroups.sort(sortGroups) : this.groupSort === 'descending' ? this.allGroups.sort(sortGroups).reverse() : this.allGroups; return ( - <div className="group-interface" style={{ background: SettingsManager.Instance.userBackgroundColor, color: SettingsManager.Instance.userColor }}> + <div className="group-interface" style={{ background: SettingsManager.userBackgroundColor, color: SettingsManager.userColor }}> {this.groupCreationModal} {this.currentGroup ? <GroupMemberView group={this.currentGroup} onCloseButtonClick={action(() => (this.currentGroup = undefined))} /> : null} <div className="group-heading"> diff --git a/src/client/util/GroupMemberView.tsx b/src/client/util/GroupMemberView.tsx index 535d8ccc2..7de0f336f 100644 --- a/src/client/util/GroupMemberView.tsx +++ b/src/client/util/GroupMemberView.tsx @@ -29,7 +29,7 @@ export class GroupMemberView extends React.Component<GroupMemberViewProps> { const hasEditAccess = GroupManager.Instance.hasEditAccess(this.props.group); return !this.props.group ? null : ( - <div className="editing-interface" style={{ background: SettingsManager.Instance.userBackgroundColor, color: SettingsManager.Instance.userColor }}> + <div className="editing-interface" style={{ background: SettingsManager.userBackgroundColor, color: SettingsManager.userColor }}> <div className="editing-header"> <input className="group-title" diff --git a/src/client/util/RTFMarkup.tsx b/src/client/util/RTFMarkup.tsx index a0fc617ab..78069d323 100644 --- a/src/client/util/RTFMarkup.tsx +++ b/src/client/util/RTFMarkup.tsx @@ -3,6 +3,8 @@ import { observer } from 'mobx-react'; import * as React from 'react'; import { MainViewModal } from '../views/MainViewModal'; import { SettingsManager } from './SettingsManager'; +import { Doc } from '../../fields/Doc'; +import { StrCast } from '../../fields/Types'; @observer export class RTFMarkup extends React.Component<{}> { @@ -133,6 +135,14 @@ export class RTFMarkup extends React.Component<{}> { } render() { - return <MainViewModal contents={this.cheatSheet} isDisplayed={this.isOpen} interactive={true} closeOnExternalClick={this.close} />; + return ( + <MainViewModal + dialogueBoxStyle={{ backgroundColor: StrCast(Doc.UserDoc().userBackgroundColor), color: StrCast(Doc.UserDoc().userColor), padding: '16px' }} + contents={this.cheatSheet} + isDisplayed={this.isOpen} + interactive={true} + closeOnExternalClick={this.close} + /> + ); } } diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts index d154c48a4..64aa7ba9b 100644 --- a/src/client/util/SearchUtil.ts +++ b/src/client/util/SearchUtil.ts @@ -1,13 +1,115 @@ import * as rp from 'request-promise'; import { DocServer } from '../DocServer'; -import { Doc } from '../../fields/Doc'; +import { Doc, DocListCast, Field, Opt } from '../../fields/Doc'; import { Id } from '../../fields/FieldSymbols'; import { Utils } from '../../Utils'; import { DocumentType } from '../documents/DocumentTypes'; +import { StrCast } from '../../fields/Types'; export namespace SearchUtil { export type HighlightingResult = { [id: string]: { [key: string]: string[] } }; + export function SearchCollection(rootDoc: Opt<Doc>, query: string) { + const blockedTypes = [DocumentType.PRESELEMENT, DocumentType.CONFIG, DocumentType.KVP, DocumentType.FONTICON, DocumentType.BUTTON, DocumentType.SCRIPTING]; + const blockedKeys = [ + 'x', + 'y', + 'proto', + 'width', + 'layout_autoHeight', + 'acl-Override', + 'acl-Guest', + 'embedContainer', + 'zIndex', + 'height', + 'text_scrollHeight', + 'text_height', + 'cloneFieldFilter', + 'isDataDoc', + 'text_annotations', + 'dragFactory_count', + 'text_noTemplate', + 'proto_embeddings', + 'isSystem', + 'layout_fieldKey', + 'isBaseProto', + 'xMargin', + 'yMargin', + 'links', + 'layout', + 'layout_keyValue', + 'layout_fitWidth', + 'type_collection', + 'title_custom', + 'freeform_panX', + 'freeform_panY', + 'freeform_scale', + ]; + query = query.toLowerCase(); + + const results = new Map<Doc, string[]>(); + if (rootDoc) { + const docs = DocListCast(rootDoc[Doc.LayoutFieldKey(rootDoc)]); + const docIDs: String[] = []; + SearchUtil.foreachRecursiveDoc(docs, (depth: number, doc: Doc) => { + const dtype = StrCast(doc.type) as DocumentType; + if (dtype && !blockedTypes.includes(dtype) && !docIDs.includes(doc[Id]) && depth >= 0) { + const hlights = new Set<string>(); + SearchUtil.documentKeys(doc).forEach( + key => + Field.toString(doc[key] as Field) + .toLowerCase() + .includes(query) && hlights.add(key) + ); + blockedKeys.forEach(key => hlights.delete(key)); + + if (Array.from(hlights.keys()).length > 0) { + results.set(doc, Array.from(hlights.keys())); + } + } + docIDs.push(doc[Id]); + }); + } + return results; + } + /** + * @param {Doc} doc - doc for which keys are returned + * + * This method returns a list of a document doc's keys. + */ + export function documentKeys(doc: Doc) { + const keys: { [key: string]: boolean } = {}; + Doc.GetAllPrototypes(doc).map(proto => Object.keys(proto).forEach(key => (keys[key] = false))); + return Array.from(Object.keys(keys)); + } + + /** + * @param {Doc[]} docs - docs to be searched through recursively + * @param {number, Doc => void} func - function to be called on each doc + * + * This method iterates through an array of docs and all docs within those docs, calling + * the function func on each doc. + */ + export function foreachRecursiveDoc(docs: Doc[], func: (depth: number, doc: Doc) => void) { + let newarray: Doc[] = []; + var depth = 0; + const visited: Doc[] = []; + while (docs.length > 0) { + newarray = []; + docs.filter(d => d && !visited.includes(d)).forEach(d => { + visited.push(d); + const fieldKey = Doc.LayoutFieldKey(d); + const annos = !Field.toString(Doc.LayoutField(d) as Field).includes('CollectionView'); + const data = d[annos ? fieldKey + '_annotations' : fieldKey]; + data && newarray.push(...DocListCast(data)); + const sidebar = d[fieldKey + '_sidebar']; + sidebar && newarray.push(...DocListCast(sidebar)); + func(depth, d); + }); + docs = newarray; + depth++; + } + } export interface IdSearchResult { ids: string[]; lines: string[][]; diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts index 4be9448b3..dbf33fcf5 100644 --- a/src/client/util/SelectionManager.ts +++ b/src/client/util/SelectionManager.ts @@ -1,11 +1,14 @@ import { action, observable, ObservableMap } from 'mobx'; import { computedFn } from 'mobx-utils'; import { Doc, Opt } from '../../fields/Doc'; -import { DocCast } from '../../fields/Types'; -import { CollectionViewType } from '../documents/DocumentTypes'; +import { List } from '../../fields/List'; +import { listSpec } from '../../fields/Schema'; +import { Cast, DocCast } from '../../fields/Types'; +import { CollectionViewType, DocumentType } from '../documents/DocumentTypes'; import { DocumentView } from '../views/nodes/DocumentView'; import { LinkManager } from './LinkManager'; import { ScriptingGlobals } from './ScriptingGlobals'; +import { UndoManager } from './UndoManager'; export namespace SelectionManager { class Manager { @@ -124,3 +127,27 @@ ScriptingGlobals.add(function SelectionManager_selectedDocType(type: string, exp ScriptingGlobals.add(function deselectAll() { SelectionManager.DeselectAll(); }); +ScriptingGlobals.add(function undo() { + SelectionManager.DeselectAll(); + return UndoManager.Undo(); +}); + +export function ShowUndoStack() { + SelectionManager.DeselectAll(); + var buffer = ''; + UndoManager.undoStack.forEach((batch, i) => { + buffer += 'Batch => ' + UndoManager.undoStackNames[i] + '\n'; + ///batch.forEach(undo => (buffer += ' ' + undo.prop + '\n')); + }); + alert(buffer); +} +ScriptingGlobals.add(function redo() { + SelectionManager.DeselectAll(); + return UndoManager.Redo(); +}); +ScriptingGlobals.add(function selectedDocs(container: Doc, excludeCollections: boolean, prevValue: any) { + const docs = SelectionManager.Views() + .map(dv => dv.props.Document) + .filter(d => !Doc.AreProtosEqual(d, container) && !d.annotationOn && d.type !== DocumentType.KVP && (!excludeCollections || d.type !== DocumentType.COL || !Cast(d.data, listSpec(Doc), null))); + return docs.length ? new List(docs) : prevValue; +}); diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx index 8133e9eff..720badd40 100644 --- a/src/client/util/SettingsManager.tsx +++ b/src/client/util/SettingsManager.tsx @@ -13,13 +13,9 @@ import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager import { DocServer } from '../DocServer'; import { Networking } from '../Network'; import { MainViewModal } from '../views/MainViewModal'; -import { FontIconBox } from '../views/nodes/FontIconBox/FontIconBox'; import { GroupManager } from './GroupManager'; import './SettingsManager.scss'; import { undoBatch } from './UndoManager'; -const higflyout = require('@hig/flyout'); -export const { anchorPoints } = higflyout; -export const Flyout = higflyout.default; export enum ColorScheme { Dark = 'Dark', @@ -51,13 +47,6 @@ export class SettingsManager extends React.Component<{}> { @observable public static propertiesWidth: number = 0; @observable public static headerBarHeight: number = 0; - @computed get backgroundColor() { - return Doc.UserDoc().activeCollectionBackground; - } - @computed get userTheme() { - return Doc.UserDoc().userTheme; - } - constructor(props: {}) { super(props); SettingsManager.Instance = this; @@ -77,15 +66,15 @@ export class SettingsManager extends React.Component<{}> { } }; - @computed get userColor() { + @computed public static get userColor() { return StrCast(Doc.UserDoc().userColor); } - @computed get userVariantColor() { + @computed public static get userVariantColor() { return StrCast(Doc.UserDoc().userVariantColor); } - @computed get userBackgroundColor() { + @computed public static get userBackgroundColor() { return StrCast(Doc.UserDoc().userBackgroundColor); } @@ -97,12 +86,8 @@ export class SettingsManager extends React.Component<{}> { Doc.UserDoc().userBackgroundColor = color; addStyleSheetRule(SettingsManager._settingsStyle, 'lm_header', { background: `${color} !important` }); }); - @undoBatch switchUserColor = action((color: string) => { - Doc.UserDoc().userColor = color; - }); - @undoBatch switchUserVariantColor = action((color: string) => { - Doc.UserDoc().userVariantColor = color; - }); + @undoBatch switchUserColor = action((color: string) => (Doc.UserDoc().userColor = color)); + @undoBatch switchUserVariantColor = action((color: string) => (Doc.UserDoc().userVariantColor = color)); @undoBatch playgroundModeToggle = action(() => { this.playgroundMode = !this.playgroundMode; if (this.playgroundMode) { @@ -164,35 +149,35 @@ export class SettingsManager extends React.Component<{}> { val: scheme, }))} dropdownType={DropdownType.SELECT} - color={this.userColor} + color={SettingsManager.userColor} fillWidth /> {userTheme === ColorScheme.Custom && ( <Group formLabel="Custom Theme"> <ColorPicker tooltip={'User Color'} // - color={this.userColor} + color={SettingsManager.userColor} type={Type.SEC} icon={<FaFillDrip />} - selectedColor={this.userColor} + selectedColor={SettingsManager.userColor} setSelectedColor={this.switchUserColor} setFinalColor={this.switchUserColor} /> <ColorPicker tooltip={'User Background Color'} - color={this.userColor} + color={SettingsManager.userColor} type={Type.SEC} icon={<FaPalette />} - selectedColor={this.userBackgroundColor} + selectedColor={SettingsManager.userBackgroundColor} setSelectedColor={this.switchUserBackgroundColor} setFinalColor={this.switchUserBackgroundColor} /> <ColorPicker tooltip={'User Variant Color'} - color={this.userColor} + color={SettingsManager.userColor} type={Type.SEC} icon={<FaPalette />} - selectedColor={this.userVariantColor} + selectedColor={SettingsManager.userVariantColor} setSelectedColor={this.switchUserVariantColor} setFinalColor={this.switchUserVariantColor} /> @@ -212,7 +197,7 @@ export class SettingsManager extends React.Component<{}> { onClick={e => (Doc.UserDoc().layout_showTitle = Doc.UserDoc().layout_showTitle ? undefined : 'author_date')} toggleStatus={Doc.UserDoc().layout_showTitle !== undefined} size={Size.XSMALL} - color={this.userColor} + color={SettingsManager.userColor} /> <Toggle formLabel={'Show Full Toolbar'} @@ -221,25 +206,25 @@ export class SettingsManager extends React.Component<{}> { onClick={e => (Doc.UserDoc()['documentLinksButton-fullMenu'] = !Doc.UserDoc()['documentLinksButton-fullMenu'])} toggleStatus={BoolCast(Doc.UserDoc()['documentLinksButton-fullMenu'])} size={Size.XSMALL} - color={this.userColor} + color={SettingsManager.userColor} /> <Toggle formLabel={'Show Button Labels'} formLabelPlacement={'right'} toggleType={ToggleType.SWITCH} - onClick={e => FontIconBox.SetShowLabels(!FontIconBox.GetShowLabels())} - toggleStatus={FontIconBox.GetShowLabels()} + onClick={e => Doc.SetShowIconLabels(!Doc.GetShowIconLabels())} + toggleStatus={Doc.GetShowIconLabels()} size={Size.XSMALL} - color={this.userColor} + color={SettingsManager.userColor} /> <Toggle formLabel={'Recognize Ink Gestures'} formLabelPlacement={'right'} toggleType={ToggleType.SWITCH} - onClick={e => FontIconBox.SetRecognizeGestures(!FontIconBox.GetRecognizeGestures())} - toggleStatus={FontIconBox.GetRecognizeGestures()} + onClick={e => Doc.SetRecognizeGestures(!Doc.GetRecognizeGestures())} + toggleStatus={Doc.GetRecognizeGestures()} size={Size.XSMALL} - color={this.userColor} + color={SettingsManager.userColor} /> <Toggle formLabel={'Hide Labels In Ink Shapes'} @@ -248,7 +233,7 @@ export class SettingsManager extends React.Component<{}> { onClick={e => (Doc.UserDoc().activeInkHideTextLabels = !Doc.UserDoc().activeInkHideTextLabels)} toggleStatus={BoolCast(Doc.UserDoc().activeInkHideTextLabels)} size={Size.XSMALL} - color={this.userColor} + color={SettingsManager.userColor} /> <Toggle formLabel={'Open Ink Docs in Lightbox'} @@ -257,7 +242,7 @@ export class SettingsManager extends React.Component<{}> { onClick={e => (Doc.UserDoc().openInkInLightbox = !Doc.UserDoc().openInkInLightbox)} toggleStatus={BoolCast(Doc.UserDoc().openInkInLightbox)} size={Size.XSMALL} - color={this.userColor} + color={SettingsManager.userColor} /> <Toggle formLabel={'Show Link Lines'} @@ -266,7 +251,7 @@ export class SettingsManager extends React.Component<{}> { onClick={e => (Doc.UserDoc().showLinkLines = !Doc.UserDoc().showLinkLines)} toggleStatus={BoolCast(Doc.UserDoc().showLinkLines)} size={Size.XSMALL} - color={this.userColor} + color={SettingsManager.userColor} /> </div> ); @@ -298,7 +283,7 @@ export class SettingsManager extends React.Component<{}> { <div className="tab-column-content"> {/* <NumberInput/> */} <Group formLabel={'Default Font'}> - <NumberDropdown color={this.userColor} numberDropdownType={'input'} min={0} max={50} step={2} type={Type.TERT} number={0} unit={'px'} setNumber={() => {}} /> + <NumberDropdown color={SettingsManager.userColor} numberDropdownType={'input'} min={0} max={50} step={2} type={Type.TERT} number={0} unit={'px'} setNumber={() => {}} /> <Dropdown items={fontFamilies.map(val => { return { @@ -315,7 +300,7 @@ export class SettingsManager extends React.Component<{}> { setSelectedVal={val => { this.changeFontFamily(val as string); }} - color={this.userColor} + color={SettingsManager.userColor} fillWidth /> </Group> @@ -343,12 +328,12 @@ export class SettingsManager extends React.Component<{}> { @computed get passwordContent() { return ( <div className="password-content"> - <EditableText placeholder="Current password" type={Type.SEC} color={this.userColor} val={''} setVal={val => this.changeVal(val as string, 'curr')} fillWidth password /> - <EditableText placeholder="New password" type={Type.SEC} color={this.userColor} val={''} setVal={val => this.changeVal(val as string, 'new')} fillWidth password /> - <EditableText placeholder="Confirm new password" type={Type.SEC} color={this.userColor} val={''} setVal={val => this.changeVal(val as string, 'conf')} fillWidth password /> + <EditableText placeholder="Current password" type={Type.SEC} color={SettingsManager.userColor} val={''} setVal={val => this.changeVal(val as string, 'curr')} fillWidth password /> + <EditableText placeholder="New password" type={Type.SEC} color={SettingsManager.userColor} val={''} setVal={val => this.changeVal(val as string, 'new')} fillWidth password /> + <EditableText placeholder="Confirm new password" type={Type.SEC} color={SettingsManager.userColor} val={''} setVal={val => this.changeVal(val as string, 'conf')} fillWidth password /> {!this.passwordResultText ? null : <div className={`${this.passwordResultText.startsWith('Error') ? 'error' : 'success'}-text`}>{this.passwordResultText}</div>} - <Button type={Type.SEC} text={'Forgot Password'} color={this.userColor} /> - <Button type={Type.TERT} text={'Submit'} onClick={this.changePassword} color={this.userColor} /> + <Button type={Type.SEC} text={'Forgot Password'} color={SettingsManager.userColor} /> + <Button type={Type.TERT} text={'Submit'} onClick={this.changePassword} color={SettingsManager.userColor} /> </div> ); } @@ -408,10 +393,10 @@ export class SettingsManager extends React.Component<{}> { dropdownType={DropdownType.SELECT} type={Type.TERT} placement="bottom-start" - color={this.userColor} + color={SettingsManager.userColor} fillWidth /> - <Toggle formLabel={'Playground Mode'} toggleType={ToggleType.SWITCH} toggleStatus={this.playgroundMode} onClick={this.playgroundModeToggle} color={this.userColor} /> + <Toggle formLabel={'Playground Mode'} toggleType={ToggleType.SWITCH} toggleStatus={this.playgroundMode} onClick={this.playgroundModeToggle} color={SettingsManager.userColor} /> </div> <div className="tab-column-title" style={{ marginTop: 20, marginBottom: 10 }}> Freeform Navigation @@ -436,15 +421,21 @@ export class SettingsManager extends React.Component<{}> { dropdownType={DropdownType.SELECT} type={Type.TERT} placement="bottom-start" - color={this.userColor} + color={SettingsManager.userColor} /> </div> </div> <div className="tab-column"> <div className="tab-column-title">Permissions</div> <div className="tab-column-content"> - <Button text={'Manage Groups'} type={Type.TERT} onClick={() => GroupManager.Instance?.open()} color={this.userColor} /> - <Toggle toggleType={ToggleType.SWITCH} formLabel={'Default access private'} color={this.userColor} toggleStatus={BoolCast(Doc.defaultAclPrivate)} onClick={action(() => (Doc.defaultAclPrivate = !Doc.defaultAclPrivate))} /> + <Button text={'Manage Groups'} type={Type.TERT} onClick={() => GroupManager.Instance?.open()} color={SettingsManager.userColor} /> + <Toggle + toggleType={ToggleType.SWITCH} + formLabel={'Default access private'} + color={SettingsManager.userColor} + toggleStatus={BoolCast(Doc.defaultAclPrivate)} + onClick={action(() => (Doc.defaultAclPrivate = !Doc.defaultAclPrivate))} + /> </div> </div> </div> @@ -463,7 +454,7 @@ export class SettingsManager extends React.Component<{}> { ]; return ( <div className="settings-interface"> - <div className="settings-panel" style={{ background: this.userColor }}> + <div className="settings-panel" style={{ background: SettingsManager.userColor }}> <div className="settings-tabs"> {tabs.map(tab => { const isActive = this.activeTab === tab.title; @@ -471,8 +462,8 @@ export class SettingsManager extends React.Component<{}> { <div key={tab.title} style={{ - background: isActive ? this.userBackgroundColor : this.userColor, - color: isActive ? this.userColor : this.userBackgroundColor, + background: isActive ? SettingsManager.userBackgroundColor : SettingsManager.userColor, + color: isActive ? SettingsManager.userColor : SettingsManager.userBackgroundColor, }} className={'tab-control ' + (isActive ? 'active' : 'inactive')} onClick={action(() => (this.activeTab = tab.title))}> @@ -483,19 +474,19 @@ export class SettingsManager extends React.Component<{}> { </div> <div className="settings-user"> - <div style={{ color: this.userBackgroundColor }}>{DashVersion}</div> - <div className="settings-username" style={{ color: this.userBackgroundColor }}> + <div style={{ color: SettingsManager.userBackgroundColor }}>{DashVersion}</div> + <div className="settings-username" style={{ color: SettingsManager.userBackgroundColor }}> {Doc.CurrentUserEmail} </div> - <Button text={Doc.GuestDashboard ? 'Exit' : 'Log Out'} type={Type.TERT} color={this.userVariantColor} onClick={() => window.location.assign(Utils.prepend('/logout'))} /> + <Button text={Doc.GuestDashboard ? 'Exit' : 'Log Out'} type={Type.TERT} color={SettingsManager.userVariantColor} onClick={() => window.location.assign(Utils.prepend('/logout'))} /> </div> </div> <div className="close-button"> - <Button icon={<FontAwesomeIcon icon={'times'} size={'lg'} />} onClick={this.close} color={this.userColor} /> + <Button icon={<FontAwesomeIcon icon={'times'} size={'lg'} />} onClick={this.close} color={SettingsManager.userColor} /> </div> - <div className="settings-content" style={{ color: this.userColor, background: this.userBackgroundColor }}> + <div className="settings-content" style={{ color: SettingsManager.userColor, background: SettingsManager.userBackgroundColor }}> {tabs.map(tab => ( <div key={tab.title} className={'tab-section ' + (this.activeTab === tab.title ? 'active' : 'inactive')}> {tab.ele} diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx index 6171c01d7..9a9097bf7 100644 --- a/src/client/util/SharingManager.tsx +++ b/src/client/util/SharingManager.tsx @@ -18,11 +18,10 @@ import { DictationOverlay } from '../views/DictationOverlay'; import { MainViewModal } from '../views/MainViewModal'; import { DocumentView } from '../views/nodes/DocumentView'; import { TaskCompletionBox } from '../views/nodes/TaskCompletedBox'; -import { SearchBox } from '../views/search/SearchBox'; import { DocumentManager } from './DocumentManager'; import { GroupManager, UserOptions } from './GroupManager'; import { GroupMemberView } from './GroupMemberView'; -import { LinkManager } from './LinkManager'; +import { SearchUtil } from './SearchUtil'; import { SelectionManager } from './SelectionManager'; import { SettingsManager } from './SettingsManager'; import './SharingManager.scss'; @@ -447,7 +446,7 @@ export class SharingManager extends React.Component<{}> { if (this.myDocAcls) { const newDocs: Doc[] = []; - SearchBox.foreachRecursiveDoc(docs, (depth, doc) => newDocs.push(doc)); + SearchUtil.foreachRecursiveDoc(docs, (depth, doc) => newDocs.push(doc)); docs = newDocs.filter(doc => GetEffectiveAcl(doc) === AclAdmin); } @@ -528,10 +527,10 @@ export class SharingManager extends React.Component<{}> { const permissions = uniform ? StrCast(targetDoc?.[groupKey]) : '-multiple-'; return !permissions ? null : ( - <div key={groupKey} className={'container'} style={{ background: SettingsManager.Instance.userBackgroundColor, color: SettingsManager.Instance.userColor }}> + <div key={groupKey} className={'container'} style={{ background: SettingsManager.userBackgroundColor, color: SettingsManager.userColor }}> <div className={'padding'}>{StrCast(group.title)}</div> - {group instanceof Doc ? <IconButton icon={<FontAwesomeIcon icon={'info-circle'} />} size={Size.XSMALL} color={SettingsManager.Instance.userColor} onClick={action(() => (GroupManager.Instance.currentGroup = group))} /> : null} + {group instanceof Doc ? <IconButton icon={<FontAwesomeIcon icon={'info-circle'} />} size={Size.XSMALL} color={SettingsManager.userColor} onClick={action(() => (GroupManager.Instance.currentGroup = group))} /> : null} <div className={'edit-actions'}> {admin || this.myDocAcls ? ( <select className={`permissions-dropdown-${permissions}`} value={permissions} onChange={e => this.setInternalGroupSharing(group, e.currentTarget.value)}> @@ -553,7 +552,7 @@ export class SharingManager extends React.Component<{}> { <div className="sharing-contents" style={{ - background: SettingsManager.Instance.userBackgroundColor, + background: SettingsManager.userBackgroundColor, color: StrCast(Doc.UserDoc().userColor), }}> <p className="share-title" style={{ color: StrCast(Doc.UserDoc().userColor) }}> diff --git a/src/client/util/reportManager/ReportManager.tsx b/src/client/util/reportManager/ReportManager.tsx index 7aad0f2b1..6a236face 100644 --- a/src/client/util/reportManager/ReportManager.tsx +++ b/src/client/util/reportManager/ReportManager.tsx @@ -20,9 +20,6 @@ import { Filter, FormInput, FormTextArea, IssueCard, IssueView, Tag } from './Re import { StrCast } from '../../../fields/Types'; import { MdRefresh } from 'react-icons/md'; import { SettingsManager } from '../SettingsManager'; -const higflyout = require('@hig/flyout'); -export const { anchorPoints } = higflyout; -export const Flyout = higflyout.default; /** * Class for reporting and viewing Github issues within the app. @@ -214,11 +211,11 @@ export class ReportManager extends React.Component<{}> { * @returns the component that dispays all issues */ private viewIssuesComponent = () => { - const darkMode = isDarkMode(SettingsManager.Instance.userBackgroundColor); + const darkMode = isDarkMode(SettingsManager.userBackgroundColor); const colors = darkMode ? darkColors : lightColors; return ( - <div className="view-issues" style={{ backgroundColor: SettingsManager.Instance.userBackgroundColor, color: colors.text }}> + <div className="view-issues" style={{ backgroundColor: SettingsManager.userBackgroundColor, color: colors.text }}> <div className="left" style={{ display: this.rightExpanded ? 'none' : 'flex' }}> <div className="report-header"> <h2 style={{ color: colors.text }}>Open Issues</h2> diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx index daa2c152a..c2cbca3e1 100644 --- a/src/client/views/ContextMenuItem.tsx +++ b/src/client/views/ContextMenuItem.tsx @@ -6,12 +6,13 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { UndoManager } from '../util/UndoManager'; import { Doc } from '../../fields/Doc'; import { StrCast } from '../../fields/Types'; +import { SettingsManager } from '../util/SettingsManager'; export interface OriginalMenuProps { description: string; event: (stuff?: any) => void; undoable?: boolean; - icon: IconProp; //maybe should be optional (icon?) + icon: IconProp | JSX.Element; //maybe should be optional (icon?) closeMenu?: () => void; } @@ -82,19 +83,20 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select ); }; + isJSXElement(val: any): val is JSX.Element { + return React.isValidElement(val); + } + render() { if ('event' in this.props) { return ( <div className={'contextMenu-item' + (this.props.selected ? ' contextMenu-itemSelected' : '')} onPointerDown={this.handleEvent}> - {this.props.icon ? ( - <span className="contextMenu-item-icon-background"> - <FontAwesomeIcon icon={this.props.icon} size="sm" /> - </span> - ) : null} + {this.props.icon ? <span className="contextMenu-item-icon-background">{this.isJSXElement(this.props.icon) ? this.props.icon : <FontAwesomeIcon icon={this.props.icon} size="sm" />}</span> : null} <div className="contextMenu-description">{this.props.description.replace(':', '')}</div> - <div className={`contextMenu-item-background`} + <div + className={`contextMenu-item-background`} style={{ - background: StrCast(Doc.UserDoc().userColor) + background: SettingsManager.userColor, }} /> </div> @@ -110,7 +112,7 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select style={{ marginLeft: window.innerHeight - this._overPosX - 50 > 0 ? '90%' : '20%', marginTop, - background: StrCast(Doc.UserDoc().userBackgroundColor) + background: SettingsManager.userBackgroundColor, }}> {this._items.map(prop => ( <ContextMenuItem {...prop} key={prop.description} closeMenu={this.props.closeMenu} /> @@ -141,9 +143,10 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select {this.props.description} <FontAwesomeIcon icon={'angle-right'} size="lg" style={{ position: 'absolute', right: '10px' }} /> </div> - <div className={`contextMenu-item-background`} + <div + className={`contextMenu-item-background`} style={{ - background: StrCast(Doc.UserDoc().userColor) + background: SettingsManager.userColor, }} /> {submenu} diff --git a/src/client/views/DashboardView.tsx b/src/client/views/DashboardView.tsx index 231a2d5fb..4387c6e96 100644 --- a/src/client/views/DashboardView.tsx +++ b/src/client/views/DashboardView.tsx @@ -172,68 +172,74 @@ export class DashboardView extends React.Component { onClick={() => this.selectDashboardGroup(DashboardGroup.SharedDashboards)} fillWidth /> + {} <Button icon={<FaPlus />} color={variant} iconPlacement="left" text="New Dashboard" type={Type.TERT} onClick={() => this.setNewDashboardName('')} /> </div> <div className="all-dashboards"> - {this.getDashboards(this.selectedDashboardGroup).map(dashboard => { - const href = ImageCast(dashboard.thumb)?.url?.href; - const shared = Object.keys(dashboard[DocAcl]) - .filter(key => key !== `acl-${Doc.CurrentUserEmailNormalized}` && !['acl-Me', 'acl-Guest'].includes(key)) - .some(key => dashboard[DocAcl][key] !== AclPrivate); - return ( - <div - className="dashboard-container" - key={dashboard[Id]} - style={{ background: this.isUnviewedSharedDashboard(dashboard) && this.selectedDashboardGroup === DashboardGroup.SharedDashboards ? '#6CB982' : shared ? variant : '' }} - onContextMenu={e => this.onContextMenu(dashboard, e)} - onClick={e => this.clickDashboard(e, dashboard)}> - <img - src={ - href ?? 'https://media.istockphoto.com/photos/hot-air-balloons-flying-over-the-botan-canyon-in-turkey-picture-id1297349747?b=1&k=20&m=1297349747&s=170667a&w=0&h=oH31fJty_4xWl_JQ4OIQWZKP8C6ji9Mz7L4XmEnbqRU=' - } - /> - <div className="info"> - <EditableText type={Type.PRIM} color={color} val={StrCast(dashboard.title)} setVal={val => (Doc.GetProto(dashboard).title = val)} /> - {this.selectedDashboardGroup === DashboardGroup.SharedDashboards && this.isUnviewedSharedDashboard(dashboard) ? <div>unviewed</div> : <div></div>} - <div - className="more" - onPointerDown={e => { - this._downX = e.clientX; - this._downY = e.clientY; - }} - onClick={e => { - e.preventDefault(); - e.stopPropagation(); - this.onContextMenu(dashboard, e); - }}> - <Button size={Size.SMALL} color={color} icon={<FontAwesomeIcon color={color} icon="bars" />} /> - </div> - </div> - <div - className={`background`} - style={{ - background: StrCast(Doc.UserDoc().userColor), - filter: 'opacity(0.2)', - }} - /> - <div className={'dashboard-status' + (shared ? '-shared' : '')}>{shared ? 'shared' : ''}</div> - </div> - ); - })} - <div - className="dashboard-container-new" - onClick={() => { - this.setNewDashboardName(''); - }}> - + + {this.selectedDashboardGroup === DashboardGroup.SharedDashboards && !this.getDashboards(this.selectedDashboardGroup).length + ? 'No one has shared a dashboard with you' + : this.getDashboards(this.selectedDashboardGroup).map(dashboard => { + const href = ImageCast(dashboard.thumb)?.url?.href; + const shared = Object.keys(dashboard[DocAcl]) + .filter(key => key !== `acl-${Doc.CurrentUserEmailNormalized}` && !['acl-Me', 'acl-Guest'].includes(key)) + .some(key => dashboard[DocAcl][key] !== AclPrivate); + return ( + <div + className="dashboard-container" + key={dashboard[Id]} + style={{ background: this.isUnviewedSharedDashboard(dashboard) && this.selectedDashboardGroup === DashboardGroup.SharedDashboards ? '#6CB982' : shared ? variant : '' }} + onContextMenu={e => this.onContextMenu(dashboard, e)} + onClick={e => this.clickDashboard(e, dashboard)}> + <img + src={ + href ?? + 'https://media.istockphoto.com/photos/hot-air-balloons-flying-over-the-botan-canyon-in-turkey-picture-id1297349747?b=1&k=20&m=1297349747&s=170667a&w=0&h=oH31fJty_4xWl_JQ4OIQWZKP8C6ji9Mz7L4XmEnbqRU=' + } + /> + <div className="info"> + <EditableText type={Type.PRIM} color={color} val={StrCast(dashboard.title)} setVal={val => (Doc.GetProto(dashboard).title = val)} /> + {this.selectedDashboardGroup === DashboardGroup.SharedDashboards && this.isUnviewedSharedDashboard(dashboard) ? <div>unviewed</div> : <div></div>} + <div + className="more" + onPointerDown={e => { + this._downX = e.clientX; + this._downY = e.clientY; + }} + onClick={e => { + e.preventDefault(); + e.stopPropagation(); + this.onContextMenu(dashboard, e); + }}> + <Button size={Size.SMALL} color={color} icon={<FontAwesomeIcon color={color} icon="bars" />} /> + </div> + </div> + <div + className={`background`} + style={{ + background: StrCast(Doc.UserDoc().userColor), + filter: 'opacity(0.2)', + }} + /> + <div className={'dashboard-status' + (shared ? '-shared' : '')}>{shared ? 'shared' : ''}</div> + </div> + ); + })} + {this.selectedDashboardGroup === DashboardGroup.SharedDashboards ? null : ( <div - className={`background`} - style={{ - background: StrCast(Doc.UserDoc().userColor), - filter: 'opacity(0.2)', - }} - /> - </div> + className="dashboard-container-new" + onClick={() => { + this.setNewDashboardName(''); + }}> + + + <div + className={`background`} + style={{ + background: StrCast(Doc.UserDoc().userColor), + filter: 'opacity(0.2)', + }} + /> + </div> + )} </div> </div> <MainViewModal @@ -402,6 +408,7 @@ export class DashboardView extends React.Component { backgroundColor: background, title: `Untitled Tab 1`, }; + const title = name ? name : `Dashboard ${dashboardCount}`; const freeformDoc = Doc.GuestTarget || Docs.Create.FreeformDocument([], freeformOptions); const dashboardDoc = Docs.Create.StandardCollectionDockingDocument([{ doc: freeformDoc, initialWidth: 600 }], { title: title }, id, 'row'); diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index f3daf3ffa..3018dad79 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -57,7 +57,6 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P @observable private _editingTitle = 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'; @@ -80,8 +79,10 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P // show decorations whenever pointer moves outside of selection bounds. 'pointermove', action(e => { - if (this.Bounds.x !== Number.MAX_VALUE && (this.Bounds.x > e.clientX || this.Bounds.r < e.clientX || this.Bounds.y > e.clientY || this.Bounds.b < e.clientY)) { - this._showNothing = false; + if (this.Bounds.x || this.Bounds.y || this.Bounds.r || this.Bounds.b) { + if (this.Bounds.x !== Number.MAX_VALUE && (this.Bounds.x > e.clientX || this.Bounds.r < e.clientX || this.Bounds.y > e.clientY || this.Bounds.b < e.clientY)) { + this._showNothing = false; + } } }) ); @@ -328,7 +329,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P */ @action onRadiusDown = (e: React.PointerEvent): void => { - this._isRounding = DocumentDecorations.Instance.Interacting = true; + this._isRounding = DocumentView.Interacting = true; this._resizeUndo = UndoManager.StartBatch('DocDecs set radius'); // Call util move event function setupMoveUpEvents( @@ -351,7 +352,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P return false; }, // moveEvent action(e => { - DocumentDecorations.Instance.Interacting = this._isRounding = false; + DocumentView.Interacting = this._isRounding = false; this._resizeUndo?.end(); }), // upEvent e => {}, // clickEvent, @@ -476,7 +477,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P @action onPointerDown = (e: React.PointerEvent): void => { 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 + DocumentView.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; @@ -686,7 +687,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P @action onPointerUp = (e: PointerEvent): void => { this._resizeHdlId = ''; - this.Interacting = false; + DocumentView.Interacting = false; this._resizeUndo?.end(); SnappingManager.clearSnapLines(); @@ -885,7 +886,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P ); return ( - <div className={`documentDecorations${colorScheme}`} style={{ opacity: this._showNothing ? 0.1 : undefined }}> + <div className={`documentDecorations${colorScheme}`} style={{ display: this._showNothing ? 'none' : undefined }}> <div className="documentDecorations-background" style={{ @@ -893,7 +894,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P height: bounds.b - bounds.y + this._resizeBorderWidth + 'px', left: bounds.x - this._resizeBorderWidth / 2, top: bounds.y - this._resizeBorderWidth / 2, - pointerEvents: DocumentDecorations.Instance.AddToSelection || this.Interacting ? 'none' : 'all', + pointerEvents: DocumentDecorations.Instance.AddToSelection || DocumentView.Interacting ? 'none' : 'all', display: SelectionManager.Views().length <= 1 || hideDecorations ? 'none' : undefined, }} onPointerDown={this.onBackgroundDown} diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx index d60617020..147921596 100644 --- a/src/client/views/EditableView.tsx +++ b/src/client/views/EditableView.tsx @@ -6,7 +6,6 @@ import { ObjectField } from '../../fields/ObjectField'; import './EditableView.scss'; import { DocumentIconContainer } from './nodes/DocumentIcon'; import { OverlayView } from './OverlayView'; -import { EditableText } from 'browndash-components'; export interface EditableProps { /** diff --git a/src/client/views/FilterPanel.scss b/src/client/views/FilterPanel.scss index 4f0460659..d6d2956aa 100644 --- a/src/client/views/FilterPanel.scss +++ b/src/client/views/FilterPanel.scss @@ -135,7 +135,7 @@ .filterBox-addFilter { width: 120px; - background-color: #e9e9e9; + backgroundcolor: #e9e9e9; border-radius: 12px; padding: 5px; margin: 5px; @@ -156,7 +156,7 @@ right: 0; top: 0; z-index: 1; - // background-color: #9f9f9f; + // background-color: #9f9f9f; .filterBox-tree { z-index: 0; @@ -191,22 +191,60 @@ } } +.filterBox-facetHeader { + display: flex; + align-items: center; + // float:right; + .filterBox-facetHeader-collapse { + // float: right; + // justify-items: right; + // align-items: flex-end; + margin-left: auto; + // margin-right: 9px; -.filterBox-facetHeader{ - display: flex; - align-items: center; - // float:right; - - .filterBox-facetHeader-collapse{ float: right; - justify-items: right; - align-items: flex-end; - margin-left: auto; - margin-right: 9px; + font-size: 16; } + .filterBox-facetHeader-remove { + // margin-left: auto; + float: right; + font-size: 16; + font-weight: bold; + } } - - +.filterbox-collpasedAndActive { + // left:100px; + text-indent: 18px; + // background-color: pink; + font-size: 12px; + font-weight: bold; +} + +.sliderBox-outerDiv { + display: inline-block; + vertical-align: middle; +} + +// .sliderBox-outerDiv { +// width: 30%;// width: calc(100% - 14px); // 14px accounts for handles that are at the max value of the slider that would extend outside the box +// height: 40; // height: 100%; +// border-radius: inherit; +// display: flex; +// flex-direction: column; +// position: relative; +// // background-color: yellow; +// .slider-tracks { +// top: 7px; +// position: relative; +// } +// .slider-ticks { +// position: relative; +// } +// .slider-handles { +// top: 7px; +// position: relative; +// } +// } diff --git a/src/client/views/FilterPanel.tsx b/src/client/views/FilterPanel.tsx index 63bd01b19..0d4f4df5a 100644 --- a/src/client/views/FilterPanel.tsx +++ b/src/client/views/FilterPanel.tsx @@ -1,24 +1,30 @@ import React = require('react'); import { action, computed, observable, ObservableMap } from 'mobx'; import { observer } from 'mobx-react'; +import { Handles, Rail, Slider, Ticks, Tracks } from 'react-compound-slider'; +import { AiOutlineMinusSquare, AiOutlinePlusSquare } from 'react-icons/ai'; +import { CiCircleRemove } from 'react-icons/ci'; import Select from 'react-select'; import { Doc, DocListCast, Field, StrListCast } from '../../fields/Doc'; import { RichTextField } from '../../fields/RichTextField'; -import { StrCast } from '../../fields/Types'; +import { DocumentOptions, FInfo } from '../documents/Documents'; import { DocumentManager } from '../util/DocumentManager'; import { UserOptions } from '../util/GroupManager'; +import { SearchUtil } from '../util/SearchUtil'; +import { undoable } from '../util/UndoManager'; import './FilterPanel.scss'; import { FieldView } from './nodes/FieldView'; -import { SearchBox } from './search/SearchBox'; -import { undoable } from '../util/UndoManager'; -import { AiOutlineMinusSquare } from 'react-icons/ai'; -import { CiCircleRemove } from 'react-icons/ci'; +import { Handle, Tick, TooltipRail, Track } from './nodes/SliderBox-components'; +import { SettingsManager } from '../util/SettingsManager'; interface filterProps { rootDoc: Doc; } + @observer export class FilterPanel extends React.Component<filterProps> { + private _documentOptions: DocumentOptions = new DocumentOptions(); + public static LayoutString(fieldKey: string) { return FieldView.LayoutString(FilterPanel, fieldKey); } @@ -41,8 +47,9 @@ export class FilterPanel extends React.Component<filterProps> { const allDocs = new Set<Doc>(); const targetDoc = this.targetDoc; if (targetDoc) { - SearchBox.foreachRecursiveDoc([this.targetDoc], (depth, doc) => allDocs.add(doc)); + SearchUtil.foreachRecursiveDoc([this.targetDoc], (depth, doc) => allDocs.add(doc)); } + console.log('this is all Docs' + Array.from(allDocs)); return Array.from(allDocs); } @@ -53,28 +60,61 @@ export class FilterPanel extends React.Component<filterProps> { const noviceFields = [...noviceReqFields, ...noviceLayoutFields]; const keys = new Set<string>(noviceFields); - this.allDocs.forEach(doc => SearchBox.documentKeys(doc).filter(key => keys.add(key))); + this.allDocs.forEach(doc => SearchUtil.documentKeys(doc).filter(key => keys.add(key))); const sortedKeys = Array.from(keys.keys()) .filter(key => key[0]) .filter(key => key.indexOf('modificationDate') !== -1 || (key[0] === key[0].toUpperCase() && !key.startsWith('_')) || noviceFields.includes(key) || !Doc.noviceMode) .sort(); + + // console.log('THIS IS HERE ' + Doc.UserDoc().color + 'space ' + Doc.UserDoc().color); noviceFields.forEach(key => sortedKeys.splice(sortedKeys.indexOf(key), 1)); + + console.log('this is novice fields ' + noviceFields + 'and this is sorted Keys ' + sortedKeys); + return [...noviceFields, ...sortedKeys]; } + @computed get rangeFilters() { + return StrListCast(this.targetDoc?._childFiltersByRanges).filter((filter, i) => !(i % 3)); + } + /** - * The current attributes selected to filter based on + * activeFilters( ) -- all filters that currently have a filter set on them in this document (ranges, and others) + * ["#tags::bob::check", "tags::joe::check", "width", "height"] */ @computed get activeFilters() { - return StrListCast(this.targetDoc?._childFilters); + return StrListCast(this.targetDoc?._childFilters).concat(this.rangeFilters); + } + + @computed get mapActiveFiltersToFacets() { + const filters = new Map<string, string>(); + //this.targetDoc.docFilters + this.activeFilters.map(filter => filters.set(filter.split(Doc.FilterSep)[1], filter.split(Doc.FilterSep)[0])); + return filters; } + // + // activeFacetHeaders() - just the facet names, not the rest of the filter + // + // this wants to return all the filter facets that have an existing filter set on them in order to show them in the rendered panel + // this set may overlap the selectedFilters + // if the components reloads, these will still exist and be shown + + // ["#tags", "width", "height"] + // + + @computed get activeFacetHeaders() { + const activeHeaders = new Array(); + this.activeFilters.map(filter => activeHeaders.push(filter.split(Doc.FilterSep)[0])); + + return activeHeaders; + } /** * @returns a string array of the current attributes */ - @computed get currentFacets() { - return this.activeFilters.map(filter => filter.split(Doc.FilterSep)[0]); - } + // @computed get currentFacets() { + // return this.activeFilters.map(filter => filter.split(Doc.FilterSep)[0]); + // } gatherFieldValues(childDocs: Doc[], facetKey: string) { const valueSet = new Set<string>(); @@ -86,6 +126,7 @@ export class FilterPanel extends React.Component<filterProps> { newarray = []; subDocs.forEach(t => { const facetVal = t[facetKey]; + // console.log("facetVal " + facetVal) if (facetVal instanceof RichTextField || typeof facetVal === 'string') rtFields++; facetVal !== undefined && valueSet.add(Field.toString(facetVal as Field)); (facetVal === true || facetVal == false) && valueSet.add(Field.toString(!facetVal)); @@ -99,6 +140,7 @@ export class FilterPanel extends React.Component<filterProps> { } // } // }); + return { strings: Array.from(valueSet.keys()), rtFields }; } @@ -107,45 +149,92 @@ export class FilterPanel extends React.Component<filterProps> { Doc.setDocRangeFilter(this.targetDoc, filterName, undefined); }; - @observable _chosenFacets = new ObservableMap<string, 'text' | 'checkbox' | 'slider' | 'range'>(); - @computed get activeFacets() { - const facets = new Map<string, 'text' | 'checkbox' | 'slider' | 'range'>(this._chosenFacets); - StrListCast(this.targetDoc?._childFilters).map(filter => facets.set(filter.split(Doc.FilterSep)[0], filter.split(Doc.FilterSep)[2] === 'match' ? 'text' : 'checkbox')); - setTimeout(() => StrListCast(this.targetDoc?._childFilters).map(action(filter => this._chosenFacets.set(filter.split(Doc.FilterSep)[0], filter.split(Doc.FilterSep)[2] === 'match' ? 'text' : 'checkbox')))); - return facets; + // @observable _chosenFacets = new ObservableMap<string, 'text' | 'checkbox' | 'slider' | 'range'>(); + @observable _chosenFacetsCollapse = new ObservableMap<string, boolean>(); + @observable _collapseReturnKeys = new Array(); + + // this computed function gets the active filters and maps them to their headers + + // + // activeRenderedFacetInfos() + // returns renderInfo for all user selected filters and for all existing filters set on the document + // Map("tags" => {"checkbox"}, + // "width" => {"rangs", domain:[1978,1992]}) + // + + @computed get activeRenderedFacetInfos() { + return new Set( + Array.from(new Set(Array.from(this._selectedFacetHeaders).concat(this.activeFacetHeaders))).map(facetHeader => { + const facetValues = this.gatherFieldValues(this.targetDocChildren, facetHeader); + + let nonNumbers = 0; + let minVal = Number.MAX_VALUE, + maxVal = -Number.MAX_VALUE; + facetValues.strings.map(val => { + const num = val ? Number(val) : Number.NaN; + if (Number.isNaN(num)) { + val && nonNumbers++; + } else { + minVal = Math.min(num, minVal); + maxVal = Math.max(num, maxVal); + } + }); + + if (facetHeader === 'text') { + return { facetHeader: facetHeader, renderType: 'text' }; + } else if (facetHeader !== 'tags' && nonNumbers / facetValues.strings.length < 0.1) { + const extendedMinVal = minVal - Math.min(1, Math.floor(Math.abs(maxVal - minVal) * 0.1)); + const extendedMaxVal = Math.max(minVal + 1, maxVal + Math.min(1, Math.ceil(Math.abs(maxVal - minVal) * 0.05))); + const ranged = Doc.readDocRangeFilter(this.targetDoc, facetHeader); // not the filter range, but the zooomed in range on the filter + return { facetHeader: facetHeader, renderType: 'range', domain: [extendedMinVal, extendedMaxVal], range: ranged ? ranged : [extendedMinVal, extendedMaxVal] }; + } else { + return { facetHeader: facetHeader, renderType: 'checkbox' }; + } + }) + ); } + + @observable _selectedFacetHeaders = new Set<string>(); + /** - * Responds to clicking the check box in the flyout menu + * user clicks on a filter facet because they want to see it. + * this adds this chosen filter to a set of user selected filters called: selectedFilters + * if this component reloads, then these filters will go away since they haven't been written to any Doc anywhere + * + * // this._selectedFacets.add(facetHeader); .. add to Set() not array */ + @action facetClick = (facetHeader: string) => { - if (!this.targetDoc) return; - const allCollectionDocs = this.targetDocChildren; - const facetValues = this.gatherFieldValues(this.targetDocChildren, facetHeader); + // just when someone chooses a facet - let nonNumbers = 0; - let minVal = Number.MAX_VALUE, - maxVal = -Number.MAX_VALUE; - facetValues.strings.map(val => { - const num = val ? Number(val) : Number.NaN; - if (Number.isNaN(num)) { - val && nonNumbers++; - } else { - minVal = Math.min(num, minVal); - maxVal = Math.max(num, maxVal); + this._selectedFacetHeaders.add(facetHeader); + + return; + }; + + @action + sortingCurrentFacetValues = (facetHeader: string) => { + this._collapseReturnKeys.splice(0); + + Array.from(this.activeRenderedFacetInfos.keys()).map(renderInfo => { + if (renderInfo.renderType === 'range' && renderInfo.facetHeader === facetHeader && renderInfo.range) { + this._collapseReturnKeys.push(renderInfo.range.map(number => number.toFixed(2))); } }); - if (facetHeader === 'text' || (facetValues.rtFields / allCollectionDocs.length > 0.1 && facetValues.strings.length > 20)) { - this._chosenFacets.set(facetHeader, 'text'); - } else if (facetHeader !== 'tags' && nonNumbers / facetValues.strings.length < 0.1) { - } else { - this._chosenFacets.set(facetHeader, 'checkbox'); + + for (var key of this.facetValues(facetHeader)) { + if (this.mapActiveFiltersToFacets.get(key)) { + this._collapseReturnKeys.push(key); + } } + + return <div className=" filterbox-collpasedAndActive">{this._collapseReturnKeys.join(', ')}</div>; }; facetValues = (facetHeader: string) => { const allCollectionDocs = new Set<Doc>(); - SearchBox.foreachRecursiveDoc(this.targetDocChildren, (depth: number, doc: Doc) => allCollectionDocs.add(doc)); + SearchUtil.foreachRecursiveDoc(this.targetDocChildren, (depth: number, doc: Doc) => allCollectionDocs.add(doc)); const set = new Set<string>([String.fromCharCode(127) + '--undefined--']); if (facetHeader === 'tags') allCollectionDocs.forEach(child => @@ -168,12 +257,63 @@ export class FilterPanel extends React.Component<filterProps> { }; render() { - const options = this._allFacets.filter(facet => this.currentFacets.indexOf(facet) === -1).map(facet => ({ value: facet, label: facet })); + // console.log('this is frist one today ' + this._allFacets); + this._allFacets.forEach(element => console.log(element)); + // const options = Object.entries(this._documentOptions).forEach((pair: [string, FInfo]) => pair[1].filterable ).map(facet => value: facet, label: facet) //this._allFacets.filter(facet => this.activeFacetHeaders.indexOf(facet) === -1).map(facet => ({ value: facet, label: facet })); + // console.log('HEELLLLLL ' + DocumentOptions); + + let filteredOptions: string[] = ['author', 'tags', 'text', 'acl-Guest']; + + Object.entries(this._documentOptions).forEach((pair: [string, FInfo]) => { + if (pair[1].filterable) { + filteredOptions.push(pair[0]); + console.log('THIS IS FILTERABLE ALKDJFIIEII' + filteredOptions); + } + }); + + let options = filteredOptions.map(facet => ({ value: facet, label: facet })); + + // Object.entries(this._documentOptions).forEach((pair: [string, FInfo]) => console.log('this is first piar ' + pair[0] + ' this is second piar ' + pair[1].filterable)); + return ( <div className="filterBox-treeView"> <div className="filterBox-select"> <div style={{ width: '100%' }}> - <Select placeholder="Add a filter..." options={options} isMulti={false} onChange={val => this.facetClick((val as UserOptions).value)} onKeyDown={e => e.stopPropagation()} value={null} closeMenuOnSelect={true} /> + <Select + styles={{ + control: (baseStyles, state) => ({ + ...baseStyles, + color: SettingsManager.userColor, + background: SettingsManager.userBackgroundColor, + }), + placeholder: (baseStyles, state) => ({ + ...baseStyles, + color: SettingsManager.userColor, + background: SettingsManager.userBackgroundColor, + }), + input: (baseStyles, state) => ({ + ...baseStyles, + color: SettingsManager.userColor, + background: SettingsManager.userBackgroundColor, + }), + option: (baseStyles, state) => ({ + ...baseStyles, + color: SettingsManager.userColor, + background: !state.isFocused ? SettingsManager.userBackgroundColor : SettingsManager.userVariantColor, + }), + menuList: (baseStyles, state) => ({ + ...baseStyles, + backgroundColor: SettingsManager.userBackgroundColor, + }), + }} + placeholder="Add a filter..." + options={options} + isMulti={false} + onChange={val => this.facetClick((val as UserOptions).value)} + onKeyDown={e => e.stopPropagation()} + value={null} + closeMenuOnSelect={true} + /> </div> {/* THE FOLLOWING CODE SHOULD BE DEVELOPER FOR BOOLEAN EXPRESSION (AND / OR) */} {/* <div className="filterBox-select-bool"> @@ -188,29 +328,58 @@ export class FilterPanel extends React.Component<filterProps> { </div> <div className="filterBox-tree" key="tree"> - {Array.from(this.activeFacets.keys()).map(facetHeader => ( - <div> - <div className="filterBox-facetHeader"> - <div className="filterBox-facetHeader-Header"> </div> - {facetHeader.charAt(0).toUpperCase() + facetHeader.slice(1)} + {Array.from(this.activeRenderedFacetInfos.keys()).map( + ( + renderInfo // iterato over activeFacetRenderInfos ==> renderInfo which you can renderInfo.facetHeader + ) => ( + <div> + <div className="filterBox-facetHeader"> + <div className="filterBox-facetHeader-Header"> </div> + {renderInfo.facetHeader.charAt(0).toUpperCase() + renderInfo.facetHeader.slice(1)} - <div className="filterBox-facetHeader-collapse"> - <AiOutlineMinusSquare /> - {/* <CiCircleRemove/> */} + <div + className="filterBox-facetHeader-collapse" + onClick={action(e => { + const collapseBoolValue = this._chosenFacetsCollapse.get(renderInfo.facetHeader); + this._chosenFacetsCollapse.set(renderInfo.facetHeader, !collapseBoolValue); + })}> + {this._chosenFacetsCollapse.get(renderInfo.facetHeader) ? <AiOutlinePlusSquare /> : <AiOutlineMinusSquare />} + </div> + + <div + className="filterBox-facetHeader-remove" + onClick={action(e => { + for (var key of this.facetValues(renderInfo.facetHeader)) { + if (this.mapActiveFiltersToFacets.get(key)) { + Doc.setDocFilter(this.targetDoc, renderInfo.facetHeader, key, 'remove'); + } + } + this._selectedFacetHeaders.delete(renderInfo.facetHeader); + this._chosenFacetsCollapse.delete(renderInfo.facetHeader); + + if (renderInfo.domain) { + Doc.setDocRangeFilter(this.targetDoc, renderInfo.facetHeader, renderInfo.domain, 'remove'); + } + })}> + <CiCircleRemove />{' '} + </div> </div> - </div> - {this.displayFacetValueFilterUIs(this.activeFacets.get(facetHeader), facetHeader)} - </div> - ))} + {this._chosenFacetsCollapse.get(renderInfo.facetHeader) + ? this.sortingCurrentFacetValues(renderInfo.facetHeader) + : this.displayFacetValueFilterUIs(renderInfo.renderType, renderInfo.facetHeader, renderInfo.domain, renderInfo.range)} + {/* */} + </div> + ) + )} </div> </div> ); } - private displayFacetValueFilterUIs(type: string | undefined, facetHeader: string): React.ReactNode { - switch (type) { - case 'text': + private displayFacetValueFilterUIs(type: string | undefined, facetHeader: string, renderInfoDomain?: number[] | undefined, renderInfoRange?: number[]): React.ReactNode { + switch (type /* renderInfo.type */) { + case 'text': // if (this.chosenFacets.get(facetHeader) === 'text') return ( <input placeholder={ @@ -241,6 +410,82 @@ export class FilterPanel extends React.Component<filterProps> { </div> ); }); + + case 'range': + const domain = renderInfoDomain; + const range = renderInfoRange; + + if (range) { + console.log('this is info range ' + range[0] + ' , ' + range[1]); + } + + if (domain) { + console.log('this is info domain ' + domain[0] + ', ' + domain[1]); + + return ( + <> + {/* <div className="sliderBox-outerDiv-checkBox" style={{ float: 'left' }}> + <Checkbox color="primary" onChange={action(() => console.log('on change'))} /> + </div> */} + + <div className="sliderBox-outerDiv" style={{ width: '95%', height: 45, float: 'right' }}> + <Slider + mode={2} + step={Math.min(1, 0.1 * (domain[1] - domain[0]))} + domain={[domain[0], domain[1]]} // -1000, 1000 + rootStyle={{ position: 'relative', width: '100%' }} + onChange={values => Doc.setDocRangeFilter(this.targetDoc, facetHeader, values)} + values={renderInfoRange!}> + <Rail>{railProps => <TooltipRail {...railProps} />}</Rail> + <Handles> + {({ handles, activeHandleID, getHandleProps }) => ( + <div className="slider-handles"> + {handles.map((handle, i) => { + // const value = i === 0 ? defaultValues[0] : defaultValues[1]; + return ( + <div> + <Handle key={handle.id} handle={handle} domain={domain} isActive={handle.id === activeHandleID} getHandleProps={getHandleProps} /> + </div> + ); + })} + </div> + )} + </Handles> + <Tracks left={false} right={false}> + {({ tracks, getTrackProps }) => ( + <div className="slider-tracks"> + {tracks.map(({ id, source, target }) => ( + <Track key={id} source={source} target={target} disabled={false} getTrackProps={getTrackProps} /> + ))} + </div> + )} + </Tracks> + <Ticks count={5}> + {({ ticks }) => ( + <div className="slider-ticks"> + {ticks.map(tick => ( + <Tick key={tick.id} tick={tick} count={ticks.length} format={(val: number) => val.toString()} /> + ))} + </div> + )} + </Ticks> + </Slider> + </div> + </> + ); + } + + // case 'range' + // return <Slider ... + // return <slider domain={renderInfo.domain}> domain is number[] for min and max + // onChange = { ... Doc.setDocRangeFilter(this.targetDoc, facetHeader, [extendedMinVal, extendedMaxVal] ) } + // + // OR + + // return <div> + // <slider domain={renderInfo.domain}> // domain is number[] for min and max + // <dimain changing handles > + // <?div } } } diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx index afb76b9ac..8f081b321 100644 --- a/src/client/views/LightboxView.tsx +++ b/src/client/views/LightboxView.tsx @@ -16,7 +16,6 @@ import { CollectionStackedTimeline } from './collections/CollectionStackedTimeli import { TabDocView } from './collections/TabDocView'; import { GestureOverlay } from './GestureOverlay'; import './LightboxView.scss'; -import { MainView } from './MainView'; import { DocumentView, OpenWhere, OpenWhereMod } from './nodes/DocumentView'; import { DefaultStyleProvider, wavyBorderPath } from './StyleProvider'; @@ -61,7 +60,7 @@ export class LightboxView extends React.Component<LightboxViewProps> { this._childFilters && (this._childFilters.length = 0); this._future = this._history = []; Doc.ActiveTool = InkTool.None; - MainView.Instance._exploreMode = false; + DocumentView.ExploreMode = false; } else { const l = DocUtils.MakeLinkToActiveAudio(() => doc).lastElement(); l && (Cast(l.link_anchor_2, Doc, null).backgroundColor = 'lightgreen'); @@ -270,7 +269,7 @@ export class LightboxView extends React.Component<LightboxViewProps> { addDocTab={this.addDocTab} pinToPres={TabDocView.PinDoc} bringToFront={emptyFunction} - onBrowseClick={MainView.Instance.exploreMode} + onBrowseClick={DocumentView.exploreMode} focus={emptyFunction} /> </GestureOverlay> @@ -335,12 +334,12 @@ export class LightboxView extends React.Component<LightboxViewProps> { <div className="lightboxView-exploreBtn" title="toggle explore mode to navigate among documents only" - style={{ background: MainView.Instance._exploreMode ? 'white' : undefined }} + style={{ background: DocumentView.ExploreMode ? 'white' : undefined }} onClick={action(e => { e.stopPropagation(); - MainView.Instance._exploreMode = !MainView.Instance._exploreMode; + DocumentView.ExploreMode = !DocumentView.ExploreMode; })}> - <FontAwesomeIcon color={MainView.Instance._exploreMode ? 'black' : 'white'} icon={'globe-americas'} size="2x" /> + <FontAwesomeIcon color={DocumentView.ExploreMode ? 'black' : 'white'} icon={'globe-americas'} size="2x" /> </div> </div> ); diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index 8f0c00cc3..96bd52d39 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -2,19 +2,19 @@ // (module as any).hot.accept(); // } +import * as dotenv from 'dotenv'; // see https://github.com/motdotla/dotenv#how-do-i-use-dotenv-with-import import * as React from 'react'; import * as ReactDOM from 'react-dom/client'; import { AssignAllExtensions } from '../../extensions/General/Extensions'; import { FieldLoader } from '../../fields/FieldLoader'; import { CurrentUserUtils } from '../util/CurrentUserUtils'; +import { PingManager } from '../util/PingManager'; import { ReplayMovements } from '../util/ReplayMovements'; import { TrackMovements } from '../util/TrackMovements'; import { CollectionView } from './collections/CollectionView'; +import './global/globalScripts'; import { MainView } from './MainView'; -import * as dotenv from 'dotenv'; // see https://github.com/motdotla/dotenv#how-do-i-use-dotenv-with-import import { BranchingTrailManager } from '../util/BranchingTrailManager'; -import { PingManager } from '../util/PingManager'; -import './global/globalScripts'; dotenv.config(); AssignAllExtensions(); diff --git a/src/client/views/MainView.scss b/src/client/views/MainView.scss index b3faff442..c880dde9a 100644 --- a/src/client/views/MainView.scss +++ b/src/client/views/MainView.scss @@ -78,7 +78,7 @@ h1, .mainView-container-Dark { .lm_header .lm_tab { padding: 0px; - opacity: 0.7; + //opacity: 0.7; box-shadow: none; height: 25px; border-bottom: black solid; @@ -194,11 +194,11 @@ h1, left: 0; position: absolute; z-index: 2; - background-color: linen; //$light-gray; + // background-color: linen; //$light-gray; - .editable-title { - background-color: linen; //$light-gray; - } + // .editable-title { + // background-color: linen; //$light-gray; + // } } } diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index a785ffd42..dde04dcc0 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -9,7 +9,6 @@ import { observer } from 'mobx-react'; import 'normalize.css'; import * as React from 'react'; import { Doc, DocListCast, Opt } from '../../fields/Doc'; -import { ScriptField } from '../../fields/ScriptField'; import { DocCast, StrCast } from '../../fields/Types'; import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, returnZero, setupMoveUpEvents, Utils } from '../../Utils'; import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager'; @@ -43,7 +42,6 @@ import { DictationOverlay } from './DictationOverlay'; import { DocumentDecorations } from './DocumentDecorations'; import { GestureOverlay } from './GestureOverlay'; import { LEFT_MENU_WIDTH, TOPBAR_HEIGHT } from './global/globalCssVariables.scss'; -import { Colors } from './global/globalEnums'; import { KeyManager } from './GlobalKeyHandler'; import { InkTranscription } from './InkTranscription'; import { LightboxView } from './LightboxView'; @@ -59,6 +57,8 @@ import GenerativeFill from './nodes/generativeFill/GenerativeFill'; import { ImageBox } from './nodes/ImageBox'; import { LinkDescriptionPopup } from './nodes/LinkDescriptionPopup'; import { LinkDocPreview } from './nodes/LinkDocPreview'; +import { MapAnchorMenu } from './nodes/MapBox/MapAnchorMenu'; +import { MapBox } from './nodes/MapBox/MapBox'; import { RadialMenu } from './nodes/RadialMenu'; import { TaskCompletionBox } from './nodes/TaskCompletedBox'; import { OverlayView } from './OverlayView'; @@ -76,7 +76,6 @@ export class MainView extends React.Component { public static Live: boolean = false; private _docBtnRef = React.createRef<HTMLDivElement>(); - @observable public LastButton: Opt<Doc>; @observable private _windowWidth: number = 0; @observable private _windowHeight: number = 0; @observable private _dashUIWidth: number = 0; // width of entire main dashboard region including left menu buttons and properties panel (but not including the dashboard selector button row) @@ -447,6 +446,7 @@ export class MainView extends React.Component { fa.faSortUp, fa.faSortDown, fa.faTable, + fa.faTableColumns, fa.faTh, fa.faThList, fa.faProjectDiagram, @@ -527,7 +527,6 @@ export class MainView extends React.Component { window.addEventListener('beforeunload', DocServer.UPDATE_SERVER_CACHE); window.addEventListener('drop', e => e.preventDefault(), false); // prevent default behavior of navigating to a new web page window.addEventListener('dragover', e => e.preventDefault(), false); - // document.addEventListener("pointermove", action(e => SearchBox.Instance._undoBackground = UndoManager.batchCounter ? "#000000a8" : undefined)); document.addEventListener('pointerdown', this.globalPointerDown, true); document.addEventListener('pointermove', this.globalPointerMove, true); document.addEventListener('pointerup', this.globalPointerClick, true); @@ -570,11 +569,7 @@ export class MainView extends React.Component { Doc.AddDocToList(Doc.MyFilesystem, 'data', folder); }; - @observable _exploreMode = false; - @computed get exploreMode() { - return () => (this._exploreMode ? ScriptField.MakeScript('CollectionBrowseClick(documentView, clientX, clientY)', { documentView: 'any', clientX: 'number', clientY: 'number' })! : undefined); - } - waitForDoubleClick = () => (this._exploreMode ? 'never' : undefined); + waitForDoubleClick = () => (DocumentView.ExploreMode ? 'never' : undefined); headerBarScreenXf = () => new Transform(-this.leftScreenOffsetOfMainDocView - this.leftMenuFlyoutWidth(), -this.headerBarDocHeight(), 1); mainScreenToLocalXf = () => new Transform(-this.leftScreenOffsetOfMainDocView - this.leftMenuFlyoutWidth(), -this.topOfMainDocContent, 1); addHeaderDoc = (doc: Doc | Doc[], annotationKey?: string) => (doc instanceof Doc ? [doc] : doc).reduce((done, doc) => Doc.AddDocToList(this.headerBarDoc, 'data', doc), true); @@ -745,7 +740,7 @@ export class MainView extends React.Component { @computed get leftMenuPanel() { return ( - <div key="menu" className="mainView-leftMenuPanel" style={{ background: StrCast(Doc.UserDoc().userBackgroundColor), display: LightboxView.LightboxDoc ? 'none' : undefined }}> + <div key="menu" className="mainView-leftMenuPanel" style={{ background: SettingsManager.userBackgroundColor, display: LightboxView.LightboxDoc ? 'none' : undefined }}> <DocumentView Document={Doc.MyLeftSidebarMenu} DataDoc={undefined} @@ -802,16 +797,16 @@ export class MainView extends React.Component { {this.flyout} <div className="mainView-libraryHandle" - style={{ background: StrCast(Doc.UserDoc().userBackgroundColor), left: leftMenuFlyoutWidth - 10 /* ~half width of handle */, display: !this._leftMenuFlyoutWidth ? 'none' : undefined }} + style={{ background: SettingsManager.userBackgroundColor, left: leftMenuFlyoutWidth - 10 /* ~half width of handle */, display: !this._leftMenuFlyoutWidth ? 'none' : undefined }} onPointerDown={this.onFlyoutPointerDown}> - <FontAwesomeIcon icon="chevron-left" color={StrCast(Doc.UserDoc().userColor)} style={{ opacity: '50%' }} size="sm" /> + <FontAwesomeIcon icon="chevron-left" color={SettingsManager.userColor} style={{ opacity: '50%' }} size="sm" /> </div> <div className="mainView-innerContainer" style={{ width: `calc(100% - ${width}px)` }}> {this.dockingContent} {this._hideUI ? null : ( - <div className="mainView-propertiesDragger" key="props" onPointerDown={this.onPropertiesPointerDown} style={{ right: this.propertiesWidth() - 1, background: 'linen' }}> - <FontAwesomeIcon icon={this.propertiesWidth() < 10 ? 'chevron-left' : 'chevron-right'} color={this.colorScheme === ColorScheme.Dark ? Colors.WHITE : Colors.BLACK} size="sm" /> + <div className="mainView-propertiesDragger" key="props" onPointerDown={this.onPropertiesPointerDown} style={{ background: SettingsManager.userBackgroundColor, right: this.propertiesWidth() - 1 }}> + <FontAwesomeIcon icon={this.propertiesWidth() < 10 ? 'chevron-left' : 'chevron-right'} color={SettingsManager.userColor} size="sm" /> </div> )} <div className="properties-container" style={{ width: this.propertiesWidth() }}> @@ -855,11 +850,11 @@ export class MainView extends React.Component { //setTimeout(action(() => (this._leftMenuFlyoutWidth += 0.5))); this._sidebarContent.proto = DocCast(button.target); - this.LastButton = button; + DocumentView.LastPressedSidebarBtn = button; }); closeFlyout = action(() => { - this.LastButton = undefined; + DocumentView.LastPressedSidebarBtn = undefined; this._panelContent = 'none'; this._sidebarContent.proto = undefined; this._leftMenuFlyoutWidth = 0; @@ -877,7 +872,7 @@ export class MainView extends React.Component { @computed get docButtons() { return !Doc.MyDockedBtns ? null : ( - <div className="mainView-docButtons" style={{ background: StrCast(Doc.UserDoc().userBackgroundColor), color: StrCast(Doc.UserDoc().userColor) }} ref={this._docBtnRef}> + <div className="mainView-docButtons" style={{ background: SettingsManager.userBackgroundColor, color: SettingsManager.userColor }} ref={this._docBtnRef}> <CollectionLinearView Document={Doc.MyDockedBtns} DataDoc={undefined} @@ -954,14 +949,50 @@ export class MainView extends React.Component { @computed get linkDocPreview() { return LinkDocPreview.LinkInfo ? <LinkDocPreview {...LinkDocPreview.LinkInfo} /> : null; } + @observable mapBoxHackBool = false; + @computed get mapBoxHack() { + return this.mapBoxHackBool ? null : ( + <MapBox + ref={action((r: any) => r && (this.mapBoxHackBool = true))} + fieldKey="data" + select={returnFalse} + isSelected={returnFalse} + Document={this.headerBarDoc} + DataDoc={undefined} + addDocTab={returnFalse} + pinToPres={emptyFunction} + docViewPath={returnEmptyDoclist} + styleProvider={DefaultStyleProvider} + rootSelected={returnTrue} + addDocument={returnFalse} + removeDocument={returnFalse} + fitContentsToBox={returnTrue} + isDocumentActive={returnTrue} // headerBar is always documentActive (ie, the docView gets pointer events) + isContentActive={returnTrue} // headerBar is awlays contentActive which means its items are always documentActive + ScreenToLocalTransform={Transform.Identity} + childHideResizeHandles={returnTrue} + childDragAction="move" + dontRegisterView={true} + PanelWidth={this.headerBarDocWidth} + PanelHeight={this.headerBarDocHeight} + renderDepth={0} + focus={emptyFunction} + whenChildContentsActiveChanged={emptyFunction} + bringToFront={emptyFunction} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + /> + ); + } render() { return ( <div className={`mainView-container ${this.colorScheme}`} style={{ - color: StrCast(Doc.UserDoc().userColor), - background: StrCast(Doc.UserDoc().userBackgroundColor), + color: SettingsManager.userColor, + background: SettingsManager.userBackgroundColor, }} onScroll={() => (ele => (ele.scrollTop = ele.scrollLeft = 0))(document.getElementById('root')!)} ref={r => { @@ -1009,6 +1040,7 @@ export class MainView extends React.Component { <ContextMenu /> <RadialMenu /> <AnchorMenu /> + <MapAnchorMenu /> <DashFieldViewMenu /> <MarqueeOptionsMenu /> <TimelineMenu /> @@ -1017,9 +1049,10 @@ export class MainView extends React.Component { {this.snapLines} <LightboxView key="lightbox" PanelWidth={this._windowWidth} PanelHeight={this._windowHeight} maxBorder={[200, 50]} /> <OverlayView /> + {this.mapBoxHack} <GPTPopup key="gptpopup" /> <GenerativeFill imageEditorOpen={ImageBox.imageEditorOpen} imageEditorSource={ImageBox.imageEditorSource} imageRootDoc={ImageBox.imageRootDoc} addDoc={ImageBox.addDoc} /> - {/* <NewLightboxView key="newLightbox" PanelWidth={this._windowWidth} PanelHeight={this._windowHeight} maxBorder={[200, 50]} /> */} + {/* <NewLightboxView key="newLightbox" PanelWidth={this._windowWidth} PanelHeight={this._windowHeight} maxBorder={[200, 50]} /> */} </div> ); } @@ -1028,3 +1061,12 @@ export class MainView extends React.Component { ScriptingGlobals.add(function selectMainMenu(doc: Doc, title: string) { MainView.Instance.selectMenu(doc); }); +ScriptingGlobals.add(function createNewPresentation() { + return MainView.Instance.createNewPresentation(); +}, 'creates a new presentation when called'); +ScriptingGlobals.add(function openPresentation(pres: Doc) { + return MainView.Instance.openPresentation(pres); +}, 'creates a new presentation when called'); +ScriptingGlobals.add(function createNewFolder() { + return MainView.Instance.createNewFolder(); +}, 'creates a new folder in myFiles when called'); diff --git a/src/client/views/MainViewModal.tsx b/src/client/views/MainViewModal.tsx index 42df99864..d0a79eb70 100644 --- a/src/client/views/MainViewModal.tsx +++ b/src/client/views/MainViewModal.tsx @@ -1,10 +1,9 @@ -import * as React from 'react'; -import './MainViewModal.scss'; +import { isDark } from 'browndash-components'; import { observer } from 'mobx-react'; +import * as React from 'react'; import { Doc } from '../../fields/Doc'; import { StrCast } from '../../fields/Types'; -import { isDark } from 'browndash-components'; -import { Colors } from './global/globalEnums'; +import './MainViewModal.scss'; export interface MainViewOverlayProps { isDisplayed: boolean; @@ -45,7 +44,7 @@ export class MainViewModal extends React.Component<MainViewOverlayProps> { className="overlay" onClick={this.props?.closeOnExternalClick} style={{ - backgroundColor: isDark(StrCast(Doc.UserDoc().userColor)) ? "#DFDFDF30" : "#32323230", + backgroundColor: isDark(StrCast(Doc.UserDoc().userColor)) ? '#DFDFDF30' : '#32323230', ...(p.overlayStyle || {}), }} /> diff --git a/src/client/views/Palette.tsx b/src/client/views/Palette.tsx new file mode 100644 index 000000000..749eb08a2 --- /dev/null +++ b/src/client/views/Palette.tsx @@ -0,0 +1,69 @@ +import { IReactionDisposer, observable, reaction } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { Doc } from '../../fields/Doc'; +import { NumCast } from '../../fields/Types'; +import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnTrue, emptyPath } from '../../Utils'; +import { Transform } from '../util/Transform'; +import { DocumentView } from './nodes/DocumentView'; +import './Palette.scss'; + +export interface PaletteProps { + x: number; + y: number; + thumb: number[]; + thumbDoc: Doc; +} + +@observer +export default class Palette extends React.Component<PaletteProps> { + private _selectedDisposer?: IReactionDisposer; + @observable private _selectedIndex: number = 0; + + componentDidMount = () => { + this._selectedDisposer = reaction( + () => NumCast(this.props.thumbDoc.selectedIndex), + i => (this._selectedIndex = i), + { fireImmediately: true } + ); + }; + + componentWillUnmount = () => { + this._selectedDisposer?.(); + }; + + render() { + return ( + <div className="palette-container" style={{ transform: `translate(${this.props.x}px, ${this.props.y}px)` }}> + <div className="palette-thumb" style={{ transform: `translate(${this.props.thumb[0] - this.props.x}px, ${this.props.thumb[1] - this.props.y}px)` }}> + <div className="palette-thumbContent" style={{ transform: `translate(-${this._selectedIndex * 50 + 10}px, 0px)` }}> + <DocumentView + Document={this.props.thumbDoc} + DataDoc={undefined} + addDocument={undefined} + addDocTab={returnFalse} + rootSelected={returnTrue} + pinToPres={emptyFunction} + removeDocument={undefined} + ScreenToLocalTransform={Transform.Identity} + PanelWidth={() => window.screen.width} + PanelHeight={() => window.screen.height} + renderDepth={0} + isDocumentActive={returnTrue} + isContentActive={emptyFunction} + focus={emptyFunction} + docViewPath={returnEmptyDoclist} + styleProvider={returnEmptyString} + whenChildContentsActiveChanged={emptyFunction} + bringToFront={emptyFunction} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + /> + <div className="palette-cover" style={{ transform: `translate(${Math.max(0, this._selectedIndex) * 50.75 + 23}px, 0px)` }}></div> + </div> + </div> + </div> + ); + } +} diff --git a/src/client/views/PreviewCursor.scss b/src/client/views/PreviewCursor.scss index 7be765ea9..82488c750 100644 --- a/src/client/views/PreviewCursor.scss +++ b/src/client/views/PreviewCursor.scss @@ -1,4 +1,3 @@ -.previewCursor-Dark, .previewCursor { color: black; position: absolute; @@ -9,7 +8,3 @@ opacity: 1; z-index: 1001; } - -.previewCursor-Dark { - color: white; -} diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx index 82d2bff56..1d88e9ad6 100644 --- a/src/client/views/PreviewCursor.tsx +++ b/src/client/views/PreviewCursor.tsx @@ -1,9 +1,9 @@ import { action, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { Doc } from '../../fields/Doc'; +import { Doc, Opt } from '../../fields/Doc'; import { StrCast } from '../../fields/Types'; -import { returnFalse } from '../../Utils'; +import { lightOrDark, returnFalse } from '../../Utils'; import { Docs, DocumentOptions, DocUtils } from '../documents/Documents'; import { ImageUtils } from '../util/Import & Export/ImageUtils'; import { Transform } from '../util/Transform'; @@ -21,6 +21,7 @@ export class PreviewCursor extends React.Component<{}> { static _slowLoadDocuments?: (files: File[] | string, options: DocumentOptions, generatedDocuments: Doc[], text: string, completed: ((doc: Doc[]) => void) | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => Promise<void>; @observable static _clickPoint = [0, 0]; @observable public static Visible = false; + public static Doc: Opt<Doc>; constructor(props: any) { super(props); document.addEventListener('keydown', this.onKeyPress); @@ -178,7 +179,12 @@ export class PreviewCursor extends React.Component<{}> { } render() { return !PreviewCursor._clickPoint || !PreviewCursor.Visible ? null : ( - <div className={`previewCursor${StrCast(Doc.ActiveDashboard?.colorScheme)}`} onBlur={this.onBlur} tabIndex={0} ref={e => e?.focus()} style={{ transform: `translate(${PreviewCursor._clickPoint[0]}px, ${PreviewCursor._clickPoint[1]}px)` }}> + <div + className="previewCursor" + onBlur={this.onBlur} + tabIndex={0} + ref={e => e?.focus()} + style={{ color: lightOrDark(PreviewCursor.Doc?.backgroundColor ?? 'white'), transform: `translate(${PreviewCursor._clickPoint[0]}px, ${PreviewCursor._clickPoint[1]}px)` }}> I </div> ); diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx index 8cae34d7d..42db0b9be 100644 --- a/src/client/views/PropertiesButtons.tsx +++ b/src/client/views/PropertiesButtons.tsx @@ -32,9 +32,6 @@ import { TfiBarChart } from 'react-icons/tfi'; import { CiGrid31 } from 'react-icons/ci'; import { RxWidth } from 'react-icons/rx'; import { Dropdown, DropdownType, IListItemProps, Toggle, ToggleType, Type } from 'browndash-components'; -const higflyout = require('@hig/flyout'); -export const { anchorPoints } = higflyout; -export const Flyout = higflyout.default; enum UtilityButtonState { Default, diff --git a/src/client/views/PropertiesDocBacklinksSelector.tsx b/src/client/views/PropertiesDocBacklinksSelector.tsx index da4438481..91a7b3da3 100644 --- a/src/client/views/PropertiesDocBacklinksSelector.tsx +++ b/src/client/views/PropertiesDocBacklinksSelector.tsx @@ -5,6 +5,7 @@ import { Cast } from '../../fields/Types'; import { DocumentType } from '../documents/DocumentTypes'; import { LinkManager } from '../util/LinkManager'; import { SelectionManager } from '../util/SelectionManager'; +import { SettingsManager } from '../util/SettingsManager'; import { LinkMenu } from './linking/LinkMenu'; import { OpenWhere, OpenWhereMod } from './nodes/DocumentView'; import './PropertiesDocBacklinksSelector.scss'; @@ -31,7 +32,7 @@ export class PropertiesDocBacklinksSelector extends React.Component<PropertiesDo render() { return !SelectionManager.Views().length ? null : ( - <div className="propertiesDocBacklinksSelector"> + <div className="propertiesDocBacklinksSelector" style={{ color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }}> {this.props.hideTitle ? null : <p key="contexts">Contexts:</p>} <LinkMenu docView={SelectionManager.Views().lastElement()} clearLinkEditor={undefined} itemHandler={this.getOnClick} style={{ left: 0, top: 0 }} /> </div> diff --git a/src/client/views/PropertiesSection.tsx b/src/client/views/PropertiesSection.tsx index 6fab0168b..b72e048df 100644 --- a/src/client/views/PropertiesSection.tsx +++ b/src/client/views/PropertiesSection.tsx @@ -49,7 +49,8 @@ export class PropertiesSection extends React.Component<PropertiesSectionProps> { this.props.setIsOpen(!this.props.isOpen); })} style={{ - background: this.props.isOpen ? this.variantColor : this.backgroundColor, + background: this.variantColor, + // this.props.isOpen ? this.variantColor : this.backgroundColor, color: this.color, }}> {this.props.title} diff --git a/src/client/views/PropertiesView.scss b/src/client/views/PropertiesView.scss index a0b054851..b116a622c 100644 --- a/src/client/views/PropertiesView.scss +++ b/src/client/views/PropertiesView.scss @@ -23,6 +23,8 @@ padding: 10px; font-size: 24px; font-weight: bold; + display: flex; + flex-direction: row; } overflow-x: hidden; @@ -33,6 +35,20 @@ padding: 5px 10px; } + .propertiesView-propAndInfoGrouping { + display: flex; + } + + .propertiesView-info { + margin-top: 20; + margin-right: 10; + float: right; + font-size: 20; + path { + fill: white !important; + } + } + .propertiesView-sharing { //border-bottom: 1px solid black; //padding: 8.5px; @@ -251,10 +267,10 @@ } } -.propertiesView-presentationTrails { - //border-bottom: 1px solid black; - //padding: 8.5px; -} +//.propertiesView-presentationTrails { +//border-bottom: 1px solid black; +//padding: 8.5px; +//} .inking-button { display: flex; diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx index 82ab54787..5f9439cbc 100644 --- a/src/client/views/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -40,9 +40,6 @@ import { DocumentView, OpenWhere, StyleProviderFunc } from './nodes/DocumentView import { KeyValueBox } from './nodes/KeyValueBox'; import { PresBox, PresEffect, PresEffectDirection } from './nodes/trails'; import { SettingsManager } from '../util/SettingsManager'; -const higflyout = require('@hig/flyout'); -export const { anchorPoints } = higflyout; -export const Flyout = higflyout.default; const _global = (window /* browser */ || global) /* node */ as any; interface PropertiesViewProps { @@ -191,7 +188,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { }); rows.push( - <div className="propertiesView-field" key="newKeyValue" style={{ marginTop: '3px', backgroundColor: 'white', textAlign: 'center' }}> + <div className="propertiesView-field" key="newKeyValue" style={{ marginTop: '3px', backgroundColor: SettingsManager.userBackgroundColor, textAlign: 'center' }}> <EditableView key="editableView" oneLine contents={'add key:value or #tags'} height={13} fontSize={10} GetValue={() => ''} SetValue={this.setKeyValue} /> </div> ); @@ -564,13 +561,16 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { } @computed get currentType() { + // console.log("current type " + this.selectedDoc?.type) + const documentType = StrCast(this.selectedDoc?.type); var currentType: string = documentType; var capitalizedDocType = Utils.cleanDocumentType(currentType as DocumentType); return ( <div> - <div className="propertiesView-wordType">Type</div> + Type + {/* <div className = "propertiesView-wordType">Type</div> */} <div className="currentType"> <div className="currentType-icon">{this.currentComponent}</div> @@ -673,7 +673,14 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { marginLeft: title === '∠:' ? '39px' : '', }}> <div className="inputBox-title"> {title} </div> - <input className="inputBox-input" type="text" value={value} onChange={e => setter(e.target.value)} onKeyPress={e => e.stopPropagation()} /> + <input + className="inputBox-input" + type="text" + value={value} + style={{ color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }} + onChange={e => setter(e.target.value)} + onKeyPress={e => e.stopPropagation()} + /> <div className="inputBox-button"> <div className="inputBox-button-up" key="up2" onPointerDown={undoBatch(action(() => this.upDownButtons('up', key)))}> <FontAwesomeIcon icon="caret-up" size="sm" /> @@ -930,7 +937,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { regInput = (key: string, value: any, setter: (val: string) => {}) => { return ( <div className="inputBox"> - <input className="inputBox-input" type="text" value={value} onChange={e => setter(e.target.value)} /> + <input className="inputBox-input" type="text" value={value} style={{ color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }} onChange={e => setter(e.target.value)} /> <div className="inputBox-button"> <div className="inputBox-button-up" key="up2" onPointerDown={undoBatch(action(() => this.upDownButtons('up', key)))}> <FontAwesomeIcon icon="caret-up" size="sm" /> @@ -966,6 +973,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { <input className="width-range" type="range" + style={{ color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }} defaultValue={Number(this.widthStk)} min={1} max={100} @@ -990,6 +998,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { className="width-range" type="range" defaultValue={this.markScal} + style={{ color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }} min={0} max={10} onChange={action(e => (this.markScal = +e.target.value))} @@ -1064,7 +1073,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { undoable((val: string) => !isNaN(Number(val)) && (this.shapeWid = val), 'set width') )} {this.getNumber( - 'X Coordinate', + 'X', //'X Coordinate', ' px', -2000, 2000, @@ -1072,7 +1081,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { undoable((val: string) => !isNaN(Number(val)) && (this.shapeXps = val), 'set x coord') )} {this.getNumber( - 'Y Coordinate', + 'Y', //'Y Coordinate', ' px', -2000, 2000, @@ -1369,6 +1378,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { get editRelationship() { return ( <input + style={{ color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }} autoComplete={'off'} id="link_relationship_input" value={StrCast(LinkManager.currentLink?.link_relationship)} @@ -1386,7 +1396,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { return ( <textarea autoComplete="off" - style={{ textAlign: 'left' }} + style={{ textAlign: 'left', color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }} id="link_description_input" value={StrCast(LinkManager.currentLink?.link_description)} onKeyDown={this.onDescriptionKey} @@ -1414,7 +1424,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { return ( <> - <div className="propertiesView-section" style={{ background: 'darkgray' }}> + <div className="propertiesView-section"> <div className="propertiesView-input first" style={{ display: 'grid', gridTemplateColumns: '84px auto' }}> <p>Relationship</p> {this.editRelationship} @@ -1458,7 +1468,10 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { <div className="propertiesView-section"> <div className="propertiesView-input inline first" style={{ display: 'grid', gridTemplateColumns: '84px calc(100% - 84px)' }}> <p>Follow by</p> - <select onChange={e => this.changeFollowBehavior(e.currentTarget.value === 'Default' ? undefined : e.currentTarget.value)} value={Cast(this.sourceAnchor?.followLinkLocation, 'string', null)}> + <select + style={{ color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }} + onChange={e => this.changeFollowBehavior(e.currentTarget.value === 'Default' ? undefined : e.currentTarget.value)} + value={Cast(this.sourceAnchor?.followLinkLocation, 'string', null)}> <option value={undefined}>Default</option> <option value={OpenWhere.addLeft}>Opening in new left pane</option> <option value={OpenWhere.addRight}>Opening in new right pane</option> @@ -1473,7 +1486,10 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { </div> <div className="propertiesView-input inline first" style={{ display: 'grid', gridTemplateColumns: '84px calc(100% - 134px) 50px' }}> <p>Animation</p> - <select style={{ width: '100%', gridColumn: 2 }} onChange={e => this.changeAnimationBehavior(e.currentTarget.value)} value={StrCast(this.sourceAnchor?.followLinkAnimEffect, 'default')}> + <select + style={{ width: '100%', gridColumn: 2, color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }} + onChange={e => this.changeAnimationBehavior(e.currentTarget.value)} + value={StrCast(this.sourceAnchor?.followLinkAnimEffect, 'default')}> <option value="default">Default</option> {[PresEffect.None, PresEffect.Zoom, PresEffect.Lightspeed, PresEffect.Fade, PresEffect.Flip, PresEffect.Rotate, PresEffect.Bounce, PresEffect.Roll].map(effect => ( <option key={effect.toString()} value={effect.toString()}> @@ -1588,7 +1604,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { <div className="propertiesView-input inline" style={{ display: 'grid', gridTemplateColumns: '78px calc(100% - 108px) 50px' }}> <p>Zoom %</p> <div className="ribbon-property" style={{ display: !targZoom ? 'none' : 'inline-flex' }}> - <input className="presBox-input" style={{ width: '100%' }} readOnly={true} type="number" value={zoom} /> + <input className="presBox-input" style={{ width: '100%', color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }} readOnly={true} type="number" value={zoom} /> <div className="ribbon-propertyUpDown" style={{ display: 'flex', flexDirection: 'column' }}> <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setZoom(String(zoom), 0.1))}> <FontAwesomeIcon icon={'caret-up'} /> @@ -1663,19 +1679,19 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { <div className="propertiesView-propAndInfoGrouping"> <div className="propertiesView-title" style={{ width: this.props.width }}> Properties - </div> - <div className="propertiesView-info" onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/')}> - <GrCircleInformation />{' '} + <div className="propertiesView-info" onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/properties')}> + <IconButton icon={<FontAwesomeIcon icon="info-circle" />} color={SettingsManager.userColor} /> + </div> </div> </div> <div className="propertiesView-name">{this.editableTitle}</div> <div className="propertiesView-type"> {this.currentType} </div> - {this.contextsSubMenu} + {this.optionsSubMenu} {this.linksSubMenu} {!this.selectedDoc || !LinkManager.currentLink || (!hasSelectedAnchor && this.selectedDoc !== LinkManager.currentLink) ? null : this.linkProperties} {this.inkSubMenu} - {this.optionsSubMenu} + {this.contextsSubMenu} {this.fieldsSubMenu} {isNovice ? null : this.sharingSubMenu} {isNovice ? null : this.filtersSubMenu} @@ -1706,8 +1722,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { className="propertiesView-presentationTrails-title" onPointerDown={action(() => (this.openPresTransitions = !this.openPresTransitions))} style={{ - color: SettingsManager.Instance.userColor, - backgroundColor: this.openPresTransitions ? SettingsManager.Instance.userVariantColor : SettingsManager.Instance.userBackgroundColor, + color: SettingsManager.userColor, + backgroundColor: this.openPresTransitions ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor, }}> <FontAwesomeIcon style={{ alignSelf: 'center' }} icon={'rocket'} /> Transitions <div className="propertiesView-presentationTrails-title-icon"> @@ -1723,8 +1739,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { className="propertiesView-presentationTrails-title" onPointerDown={action(() => (this.openPresVisibilityAndDuration = !this.openPresVisibilityAndDuration))} style={{ - color: SettingsManager.Instance.userColor, - backgroundColor: this.openPresVisibilityAndDuration ? SettingsManager.Instance.userVariantColor : SettingsManager.Instance.userBackgroundColor, + color: SettingsManager.userColor, + backgroundColor: this.openPresVisibilityAndDuration ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor, }}> <FontAwesomeIcon style={{ alignSelf: 'center' }} icon={'rocket'} /> Visibilty <div className="propertiesView-presentationTrails-title-icon"> @@ -1740,8 +1756,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { className="propertiesView-presentationTrails-title" onPointerDown={action(() => (this.openPresProgressivize = !this.openPresProgressivize))} style={{ - color: SettingsManager.Instance.userColor, - backgroundColor: this.openPresProgressivize ? SettingsManager.Instance.userVariantColor : SettingsManager.Instance.userBackgroundColor, + color: SettingsManager.userColor, + backgroundColor: this.openPresProgressivize ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor, }}> <FontAwesomeIcon style={{ alignSelf: 'center' }} icon={'rocket'} /> Progressivize <div className="propertiesView-presentationTrails-title-icon"> @@ -1757,8 +1773,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { className="propertiesView-presentationTrails-title" onPointerDown={action(() => (this.openSlideOptions = !this.openSlideOptions))} style={{ - color: SettingsManager.Instance.userColor, - backgroundColor: this.openSlideOptions ? SettingsManager.Instance.userVariantColor : SettingsManager.Instance.userBackgroundColor, + color: SettingsManager.userColor, + backgroundColor: this.openSlideOptions ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor, }}> <FontAwesomeIcon style={{ alignSelf: 'center' }} icon={type === DocumentType.AUDIO ? 'file-audio' : 'file-video'} /> {type === DocumentType.AUDIO ? 'Audio Options' : 'Video Options'} <div className="propertiesView-presentationTrails-title-icon"> diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx index db273cc88..f3452c780 100644 --- a/src/client/views/SidebarAnnos.tsx +++ b/src/client/views/SidebarAnnos.tsx @@ -9,11 +9,11 @@ import { emptyFunction, returnAll, returnFalse, returnOne, returnTrue, returnZer import { Docs, DocUtils } from '../documents/Documents'; import { CollectionViewType, DocumentType } from '../documents/DocumentTypes'; import { LinkManager } from '../util/LinkManager'; +import { SearchUtil } from '../util/SearchUtil'; import { Transform } from '../util/Transform'; import { CollectionStackingView } from './collections/CollectionStackingView'; import { FieldViewProps } from './nodes/FieldView'; import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; -import { SearchBox } from './search/SearchBox'; import './SidebarAnnos.scss'; import { StyleProp } from './StyleProvider'; import React = require('react'); @@ -43,7 +43,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { @computed get allMetadata() { const keys = new Map<string, FieldResult<Field>>(); DocListCast(this.props.rootDoc[this.sidebarKey]).forEach(doc => - SearchBox.documentKeys(doc) + SearchUtil.documentKeys(doc) .filter(key => key[0] && key[0] !== '_' && key[0] === key[0].toUpperCase()) .map(key => keys.set(key, doc[key])) ); @@ -66,7 +66,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { return '_' + this.sidebarKey + '_childFilters'; } - anchorMenuClick = (anchor: Doc) => { + anchorMenuClick = (anchor: Doc, filterExlusions?: string[]) => { const startup = StrListCast(this.props.rootDoc.childFilters) .map(filter => filter.split(':')[0]) .join(' '); @@ -79,6 +79,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { _layout_autoHeight: true, _text_fontSize: StrCast(Doc.UserDoc().fontSize), _text_fontFamily: StrCast(Doc.UserDoc().fontFamily), + target: 'HELLO' as any, }); FormattedTextBox.SelectOnLoad = target[Id]; FormattedTextBox.DontSelectInitialText = true; @@ -87,6 +88,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { const taggedContent = this.childFilters() .filter(data => data.split(':')[0]) + .filter(data => !filterExlusions?.includes(data.split(':')[0])) .map(data => { const key = data.split(':')[0]; const val = Field.Copy(this.allMetadata.get(key)); @@ -187,7 +189,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { }; render() { const renderTag = (tag: string) => { - const active = StrListCast(this.props.rootDoc[this.filtersKey]).includes(`tags:${tag}:check`); + const active = StrListCast(this.props.rootDoc[this.filtersKey]).includes(`tags::${tag}::check`); return ( <div key={tag} className={`sidebarAnnos-filterTag${active ? '-active' : ''}`} onClick={e => Doc.setDocFilter(this.props.rootDoc, 'tags', tag, 'check', true, this.sidebarKey, e.shiftKey)}> {tag} @@ -225,10 +227,11 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { <div className="sidebarAnnos-tagList" style={{ height: this.filtersHeight() }} onWheel={e => e.stopPropagation()}> {this.allUsers.map(renderUsers)} {this.allHashtags.map(renderTag)} - {Array.from(this.allMetadata.keys()) + {/* {Array.from(this.allMetadata.keys()) .sort() - .map(key => renderMeta(key, this.allMetadata.get(key)))} + .map(key => renderMeta(key, this.allMetadata.get(key)))} */} </div> + <div style={{ width: '100%', height: `calc(100% - 38px)`, position: 'relative' }}> <CollectionStackingView {...this.props} diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx index a710555fa..a9e4ac17d 100644 --- a/src/client/views/StyleProvider.tsx +++ b/src/client/views/StyleProvider.tsx @@ -4,6 +4,7 @@ import { Tooltip } from '@material-ui/core'; import { IconButton, Shadows, Size } from 'browndash-components'; import { action, runInAction } from 'mobx'; import { extname } from 'path'; +import { BsArrowDown, BsArrowDownUp, BsArrowUp } from 'react-icons/bs'; import { Doc, Opt, StrListCast } from '../../fields/Doc'; import { BoolCast, Cast, DocCast, ImageCast, NumCast, StrCast } from '../../fields/Types'; import { DashColor, lightOrDark, Utils } from '../../Utils'; @@ -14,15 +15,13 @@ import { LinkManager } from '../util/LinkManager'; import { SelectionManager } from '../util/SelectionManager'; import { ColorScheme, SettingsManager } from '../util/SettingsManager'; import { undoBatch, UndoManager } from '../util/UndoManager'; -import { TreeSort } from './collections/TreeView'; +import { TreeSort } from './collections/TreeSort'; import { Colors } from './global/globalEnums'; import { InkingStroke } from './InkingStroke'; -import { MainView } from './MainView'; -import { DocumentViewProps } from './nodes/DocumentView'; +import { DocumentView, DocumentViewProps } from './nodes/DocumentView'; import { FieldViewProps } from './nodes/FieldView'; import { KeyValueBox } from './nodes/KeyValueBox'; import { SliderBox } from './nodes/SliderBox'; -import { BsArrowDown, BsArrowUp, BsArrowDownUp } from 'react-icons/bs'; import './StyleProvider.scss'; import React = require('react'); @@ -159,7 +158,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps '' ); case StyleProp.Color: - if (MainView.Instance.LastButton === doc) return SettingsManager.Instance.userBackgroundColor; + if (DocumentView.LastPressedSidebarBtn === doc) return SettingsManager.userBackgroundColor; if (Doc.IsSystem(doc!)) return StrCast(Doc.UserDoc().userColor) if (doc?.type === DocumentType.FONTICON) return Doc.UserDoc().userColor; const docColor: Opt<string> = StrCast(doc?.[fieldKey + 'color'], StrCast(doc?._color)); @@ -194,7 +193,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps ? 15 : 0; case StyleProp.BackgroundColor: { - if (MainView.Instance.LastButton === doc) return StrCast(Doc.UserDoc().userColor); // hack to indicate active menu panel item + if (DocumentView.LastPressedSidebarBtn === doc) return StrCast(Doc.UserDoc().userColor); // hack to indicate active menu panel item let docColor: Opt<string> = StrCast(doc?.[fieldKey + '_backgroundColor'], StrCast(doc?._backgroundColor, isCaption ? 'rgba(0,0,0,0.4)' : '')); // prettier-ignore switch (doc?.type) { @@ -217,7 +216,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps case DocumentType.COL: if (StrCast(Doc.LayoutField(doc)).includes(SliderBox.name)) break; docColor = docColor || (Doc.IsSystem(doc) - ? SettingsManager.Instance.userBackgroundColor + ? SettingsManager.userBackgroundColor : doc.annotationOn ? '#00000010' // faint interior for collections on PDFs, images, etc : doc?._isGroup @@ -264,7 +263,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps const isInk = doc && StrCast(Doc.Layout(doc).layout).includes(InkingStroke.name) && !props?.LayoutTemplateString; if (StrCast(doc?.pointerEvents) && !props?.LayoutTemplateString?.includes(KeyValueBox.name)) return StrCast(doc!.pointerEvents); // honor pointerEvents field (set by lock button usually) if it's not a keyValue view of the Doc if (docProps?.DocumentView?.().ComponentView?.overridePointerEvents?.() !== undefined) return docProps?.DocumentView?.().ComponentView?.overridePointerEvents?.(); - if (MainView.Instance._exploreMode || doc?.layout_unrendered) return isInk ? 'visiblePainted' : 'all'; + if (DocumentView.ExploreMode || doc?.layout_unrendered) return isInk ? 'visiblePainted' : 'all'; if (props?.contentPointerEvents) return StrCast(props.contentPointerEvents); if (props?.pointerEvents?.() === 'none') return 'none'; if (opacity() === 0) return 'none'; diff --git a/src/client/views/UndoStack.tsx b/src/client/views/UndoStack.tsx index 813cdd050..a9dfbe8f0 100644 --- a/src/client/views/UndoStack.tsx +++ b/src/client/views/UndoStack.tsx @@ -19,12 +19,14 @@ export class UndoStack extends React.Component<UndoStackProps> { @observable static HideInline: boolean; @observable static Expand: boolean; render() { - const background = UndoManager.batchCounter.get() ? 'yellow' : SettingsManager.Instance.userBackgroundColor; + const background = UndoManager.batchCounter.get() ? 'yellow' : SettingsManager.userVariantColor; + const color = UndoManager.batchCounter.get() ? 'black' : SettingsManager.userColor; return this.props.inline && UndoStack.HideInline ? null : ( <div className="undoStack-outerContainer"> <Popup text={'Undo/Redo Stack'} - color={UndoManager.batchCounter.get() ? 'yellow' : StrCast(Doc.UserDoc().userVariantColor)} + color={color} + background={background} placement={`top-start`} type={Type.TERT} popup={ @@ -32,8 +34,8 @@ export class UndoStack extends React.Component<UndoStackProps> { className="undoStack-commandsContainer" ref={r => r?.scroll({ behavior: 'auto', top: r?.scrollHeight + 20 })} style={{ - background: background, - color: isDark(background) ? Colors.LIGHT_GRAY : Colors.DARK_GRAY, + background, + color, }}> {UndoManager.undoStackNames.map((name, i) => ( <div className="undoStack-resultContainer" key={i}> diff --git a/src/client/views/collections/CollectionDockingView.scss b/src/client/views/collections/CollectionDockingView.scss index d93015506..b13753025 100644 --- a/src/client/views/collections/CollectionDockingView.scss +++ b/src/client/views/collections/CollectionDockingView.scss @@ -37,6 +37,10 @@ display: flex; } +.lm_active { + height: 27px !important; +} + .lm_active .lm_title { -webkit-appearance: none; // font-weight: 700; diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index e15d57306..519b7c905 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -3,6 +3,7 @@ import { observer } from 'mobx-react'; import * as ReactDOM from 'react-dom/client'; import * as GoldenLayout from '../../../client/goldenLayout'; import { Doc, DocListCast, Opt } from '../../../fields/Doc'; +import { AclAdmin, AclEdit } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; import { List } from '../../../fields/List'; @@ -13,6 +14,7 @@ import { emptyFunction, incrementTitleCopy } from '../../../Utils'; import { DocServer } from '../../DocServer'; import { Docs } from '../../documents/Documents'; import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes'; +import { DocumentManager } from '../../util/DocumentManager'; import { DragManager } from '../../util/DragManager'; import { InteractionUtils } from '../../util/InteractionUtils'; import { ScriptingGlobals } from '../../util/ScriptingGlobals'; @@ -28,8 +30,6 @@ import './CollectionDockingView.scss'; import { CollectionFreeFormView } from './collectionFreeForm'; import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView'; import { TabDocView } from './TabDocView'; -import { DocumentManager } from '../../util/DocumentManager'; -import { AclAdmin, AclEdit } from '../../../fields/DocSymbols'; import React = require('react'); const _global = (window /* browser */ || global) /* node */ as any; diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx index f65e8698f..5c9dd2058 100644 --- a/src/client/views/collections/CollectionMenu.tsx +++ b/src/client/views/collections/CollectionMenu.tsx @@ -181,7 +181,7 @@ export class CollectionMenu extends AntimodeMenu<CollectionMenuProps> { <div className="collectionMenu-container" style={{ - background: SettingsManager.Instance.userBackgroundColor, + background: SettingsManager.userBackgroundColor, // borderColor: StrCast(Doc.UserDoc().userColor) }}> {this.contMenuButtons} diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx index de58e1fe7..d2be70577 100644 --- a/src/client/views/collections/CollectionStackedTimeline.tsx +++ b/src/client/views/collections/CollectionStackedTimeline.tsx @@ -2,12 +2,12 @@ import React = require('react'); import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import { computedFn } from 'mobx-utils'; -import { Doc, Opt, StrListCast } from '../../../fields/Doc'; +import { Doc, Opt } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { listSpec } from '../../../fields/Schema'; import { ComputedField, ScriptField } from '../../../fields/ScriptField'; -import { Cast, NumCast, ScriptCast } from '../../../fields/Types'; +import { Cast, NumCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; import { emptyFunction, formatTime, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnNone, returnTrue, returnZero, setupMoveUpEvents, smoothScrollHorizontal, StopEvent } from '../../../Utils'; import { Docs } from '../../documents/Documents'; diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx index ebb4ba5a1..7a22d4871 100644 --- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx +++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx @@ -22,9 +22,6 @@ import { EditableView } from '../EditableView'; import './CollectionStackingView.scss'; import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; import { Id } from '../../../fields/FieldSymbols'; -const higflyout = require('@hig/flyout'); -export const { anchorPoints } = higflyout; -export const Flyout = higflyout.default; // So this is how we are storing a column interface CSVFieldColumnProps { diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx index f379d09ff..f6acafa95 100644 --- a/src/client/views/collections/TabDocView.tsx +++ b/src/client/views/collections/TabDocView.tsx @@ -1,6 +1,6 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Tooltip } from '@material-ui/core'; +import { Popup, Type } from 'browndash-components'; import { clamp } from 'lodash'; import { action, computed, IReactionDisposer, observable, ObservableSet, reaction } from 'mobx'; import { observer } from 'mobx-react'; @@ -10,7 +10,7 @@ import { DocData, Height, Width } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { FieldId } from '../../../fields/RefField'; -import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../fields/Types'; +import { Cast, DocCast, NumCast, StrCast } from '../../../fields/Types'; import { emptyFunction, lightOrDark, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents, simulateMouseClick, Utils } from '../../../Utils'; import { DocServer } from '../../DocServer'; import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes'; @@ -21,9 +21,8 @@ import { SnappingManager } from '../../util/SnappingManager'; import { Transform } from '../../util/Transform'; import { undoable, UndoManager } from '../../util/UndoManager'; import { DashboardView } from '../DashboardView'; -import { Colors, Shadows } from '../global/globalEnums'; +import { Colors } from '../global/globalEnums'; import { LightboxView } from '../LightboxView'; -import { MainView } from '../MainView'; import { DocFocusOptions, DocumentView, DocumentViewProps, OpenWhere, OpenWhereMod } from '../nodes/DocumentView'; import { DashFieldView } from '../nodes/formattedText/DashFieldView'; import { KeyValueBox } from '../nodes/KeyValueBox'; @@ -34,7 +33,6 @@ import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormV import { CollectionView } from './CollectionView'; import './TabDocView.scss'; import React = require('react'); -import { Popup, Toggle, Type } from 'browndash-components'; const _global = (window /* browser */ || global) /* node */ as any; interface TabDocViewProps { @@ -426,6 +424,7 @@ export class TabDocView extends React.Component<TabDocViewProps> { disableMinimap = () => !this._document || this._document.layout !== CollectionView.LayoutString(Doc.LayoutFieldKey(this._document)) || this._document?._type_collection !== CollectionViewType.Freeform; whenChildContentActiveChanges = (isActive: boolean) => (this._isAnyChildContentActive = isActive); isContentActive = () => this._isContentActive; + waitForDoubleClick = () => (DocumentView.ExploreMode ? 'never' : undefined); @computed get docView() { return !this._activated || !this._document ? null : ( <> @@ -441,8 +440,8 @@ export class TabDocView extends React.Component<TabDocViewProps> { hideTitle={this.props.keyValue} Document={this._document} DataDoc={!Doc.AreProtosEqual(this._document[DocData], this._document) ? this._document[DocData] : undefined} - onBrowseClick={MainView.Instance.exploreMode} - waitForDoubleClickToClick={MainView.Instance.waitForDoubleClick} + onBrowseClick={DocumentView.exploreMode} + waitForDoubleClickToClick={this.waitForDoubleClick} isContentActive={this.isContentActive} isDocumentActive={returnFalse} PanelWidth={this.PanelWidth} diff --git a/src/client/views/collections/TreeSort.ts b/src/client/views/collections/TreeSort.ts new file mode 100644 index 000000000..977acd030 --- /dev/null +++ b/src/client/views/collections/TreeSort.ts @@ -0,0 +1,6 @@ +export enum TreeSort { + AlphaUp = 'alphabetical from z', + AlphaDown = 'alphabetical from A', + Zindex = 'by Z index', + WhenAdded = 'when added', +} diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx index db33c46bb..4f9310434 100644 --- a/src/client/views/collections/TreeView.tsx +++ b/src/client/views/collections/TreeView.tsx @@ -35,6 +35,7 @@ import { CollectionView } from './CollectionView'; import './TreeView.scss'; import React = require('react'); import { IconButton, Size } from 'browndash-components'; +import { TreeSort } from './TreeSort'; export interface TreeViewProps { treeView: CollectionTreeView; @@ -77,12 +78,6 @@ const treeBulletWidth = function () { return Number(TREE_BULLET_WIDTH.replace('px', '')); }; -export enum TreeSort { - AlphaUp = 'alphabetical from z', - AlphaDown = 'alphabetical from A', - Zindex = 'by Z index', - WhenAdded = 'when added', -} /** * Renders a treeView of a collection of documents * @@ -624,7 +619,7 @@ export class TreeView extends React.Component<TreeViewProps> { return ( <div> {!docs?.length || this.props.AddToMap /* hack to identify pres box trees */ ? null : ( - <div className='treeView-sorting'> + <div className="treeView-sorting"> <IconButton color={sortings[sorting]?.color} size={Size.XSMALL} @@ -648,7 +643,7 @@ export class TreeView extends React.Component<TreeViewProps> { style={{ cursor: 'inherit' }} key={expandKey + 'more'} title={`Sorted by : ${this.doc.treeView_SortCriterion}. click to cycle`} - className='' //this.doc.treeView_HideTitle ? 'no-indent' : ''} + className="" //this.doc.treeView_HideTitle ? 'no-indent' : ''} onPointerDown={e => { downX = e.clientX; downY = e.clientY; diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx index 7c869af24..420e6a318 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx @@ -12,10 +12,7 @@ export class CollectionFreeFormLinksView extends React.Component { @computed get uniqueConnections() { return Array.from(new Set(DocumentManager.Instance.LinkedDocumentViews)) .filter(c => !LightboxView.LightboxDoc || (LightboxView.IsLightboxDocView(c.a.docViewPath) && LightboxView.IsLightboxDocView(c.b.docViewPath))) - .map(c => { - console.log("got a connectoin", c) - return <CollectionFreeFormLinkView key={c.l[Id]} A={c.a} B={c.b} LinkDocs={[c.l]} />; - }); + .map(c => <CollectionFreeFormLinkView key={c.l[Id]} A={c.a} B={c.b} LinkDocs={[c.l]} />); } render() { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index f5cc1eb53..16d6f1270 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -371,7 +371,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection const nd = [Doc.NativeWidth(layoutDoc), Doc.NativeHeight(layoutDoc)]; layoutDoc._width = NumCast(layoutDoc._width, 300); layoutDoc._height = NumCast(layoutDoc._height, nd[0] && nd[1] ? (nd[1] / nd[0]) * NumCast(layoutDoc._width) : 300); - (d._raiseWhenDragged === undefined ? DragManager.GetRaiseWhenDragged() : d._raiseWhenDragged) && (d.zIndex = zsorted.length + 1 + i); // bringToFront + !d._keepZWhenDragged && (d.zIndex = zsorted.length + 1 + i); // bringToFront } if (this.layoutDoc._autoArrange || de.metaKey) { @@ -1267,7 +1267,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }; @computed get _pointerEvents() { const engine = this.props.layoutEngine?.() || StrCast(this.props.Document._layoutEngine); - const pointerEvents = DocumentDecorations.Instance.Interacting + const pointerEvents = DocumentView.Interacting ? 'none' : this.props.childPointerEvents ?? (this.props.viewDefDivClick || (engine === computePassLayout.name && !this.props.isSelected(true)) || this.isContentActive() === false ? 'none' : this.props.pointerEvents?.()); return pointerEvents; diff --git a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx index 0f51fe6ff..71900c63f 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx @@ -61,7 +61,7 @@ export class MarqueeOptionsMenu extends AntimodeMenu<AntimodeMenuProps> { <IconButton tooltip={"Pin selected region"} onPointerDown={this.pinWithView} - icon={presPinWithViewIcon} + icon={<FontAwesomeIcon icon="map-pin"/>} color={this.userColor} /> </> diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.scss b/src/client/views/collections/collectionFreeForm/MarqueeView.scss index e0f5cbe5b..7c9d0f6e1 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.scss +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.scss @@ -17,7 +17,6 @@ box-sizing: border-box; position: absolute; border-width: 1px; - border-color: black; pointer-events: none; .marquee-legend { bottom: -18px; diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 1c3da1dc5..cd7bd28e9 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -10,7 +10,7 @@ import { SchemaHeaderField } from '../../../../fields/SchemaHeaderField'; import { Cast, DocCast, FieldValue, NumCast, StrCast } from '../../../../fields/Types'; import { ImageField } from '../../../../fields/URLField'; import { distributeAcls, GetEffectiveAcl, SharingPermissions } from '../../../../fields/util'; -import { intersectRect, returnFalse, Utils } from '../../../../Utils'; +import { intersectRect, lightOrDark, returnFalse, Utils } from '../../../../Utils'; import { CognitiveServices } from '../../../cognitive_services/CognitiveServices'; import { Docs, DocumentOptions, DocUtils } from '../../../documents/Documents'; import { DocumentType } from '../../../documents/DocumentTypes'; @@ -39,7 +39,7 @@ interface MarqueeViewProps { trySelectCluster: (addToSel: boolean) => boolean; nudge?: (x: number, y: number, nudgeTime?: number) => boolean; ungroup?: () => void; - setPreviewCursor?: (func: (x: number, y: number, drag: boolean, hide: boolean) => void) => void; + setPreviewCursor?: (func: (x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void) => void; slowLoadDocuments: (files: File[] | string, options: DocumentOptions, generatedDocuments: Doc[], text: string, completed: ((doc: Doc[]) => void) | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => Promise<void>; } @@ -197,18 +197,18 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque const headers = ns[0].split('\t'); csvRows.push(headers.join(',')); ns[0] = ''; - const eachCell = ns.join('\t').split('\t') - let eachRow = [] - for (let i=1; i<eachCell.length; i++){ + const eachCell = ns.join('\t').split('\t'); + let eachRow = []; + for (let i = 1; i < eachCell.length; i++) { eachRow.push(eachCell[i].replace(/\,/g, '')); - if (i % headers.length == 0){ - csvRows.push(eachRow) + if (i % headers.length == 0) { + csvRows.push(eachRow); eachRow = []; } } - - const blob = new Blob([csvRows.join('\n')], {type: 'text/csv'}) - const options = { x: x, y: y, title: 'droppedTable', _width: 300, _height: 100, type:'text/csv'} + + const blob = new Blob([csvRows.join('\n')], { type: 'text/csv' }); + const options = { x: x, y: y, title: 'droppedTable', _width: 300, _height: 100, type: 'text/csv' }; const file = new File([blob], 'droppedTable', options); const loading = Docs.Create.LoadingDocument(file, options); DocUtils.uploadFileToDoc(file, {}, loading); @@ -225,7 +225,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque // allow marquee if right click OR alt+left click OR in adding presentation slide & left key drag mode if (e.button === 2 || (e.button === 0 && e.altKey)) { // if (e.altKey || (MarqueeView.DragMarquee && this.props.active(true))) { - this.setPreviewCursor(e.clientX, e.clientY, true, false); + this.setPreviewCursor(e.clientX, e.clientY, true, false, this.props.Document); // (!e.altKey) && e.stopPropagation(); // bcz: removed so that you can alt-click on button in a collection to switch link following behaviors. e.preventDefault(); // } @@ -298,17 +298,19 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque } } - setPreviewCursor = action((x: number, y: number, drag: boolean, hide: boolean) => { + setPreviewCursor = action((x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => { if (hide) { this._downX = this._lastX = x; this._downY = this._lastY = y; this._commandExecuted = false; PreviewCursor.Visible = false; + PreviewCursor.Doc = undefined; } else if (drag) { this._downX = this._lastX = x; this._downY = this._lastY = y; this._commandExecuted = false; PreviewCursor.Visible = false; + PreviewCursor.Doc = undefined; this.cleanupInteractions(true); document.addEventListener('pointermove', this.onPointerMove, true); document.addEventListener('pointerup', this.onPointerUp, true); @@ -318,6 +320,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque this._downY = y; const effectiveAcl = GetEffectiveAcl(this.props.Document[DocData]); if ([AclAdmin, AclEdit, AclAugment].includes(effectiveAcl)) { + PreviewCursor.Doc = doc; PreviewCursor.Show(x, y, this.onKeyPress, this.props.addLiveTextDocument, this.props.getTransform, this.props.addDocument, this.props.nudge, this.props.slowLoadDocuments); } this.clearSelection(); @@ -332,7 +335,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque if (!(e.nativeEvent as any).marqueeHit) { (e.nativeEvent as any).marqueeHit = true; if (!this.props.trySelectCluster(e.shiftKey)) { - this.setPreviewCursor(e.clientX, e.clientY, false, false); + this.setPreviewCursor(e.clientX, e.clientY, false, false, this.props.Document); } else e.stopPropagation(); } } @@ -629,12 +632,20 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque transform: `translate(${p[0]}px, ${p[1]}px)`, width: Math.abs(v[0]), height: Math.abs(v[1]), + color: lightOrDark(this.props.Document?.backgroundColor ?? 'white'), + borderColor: lightOrDark(this.props.Document?.backgroundColor ?? 'white'), zIndex: 2000, }}> {' '} {this._lassoFreehand ? ( <svg height={2000} width={2000}> - <polyline points={this._lassoPts.reduce((s, pt) => s + pt[0] + ',' + pt[1] + ' ', '')} fill="none" stroke="black" strokeWidth="1" strokeDasharray="3" /> + <polyline // + points={this._lassoPts.reduce((s, pt) => s + pt[0] + ',' + pt[1] + ' ', '')} + fill="none" + stroke={lightOrDark(this.props.Document?.backgroundColor ?? 'white')} + strokeWidth="1" + strokeDasharray="3" + /> </svg> ) : ( <span className="marquee-legend" /> diff --git a/src/client/views/collections/collectionLinear/CollectionLinearView.scss b/src/client/views/collections/collectionLinear/CollectionLinearView.scss index 6b3318bf3..d0c14a21d 100644 --- a/src/client/views/collections/collectionLinear/CollectionLinearView.scss +++ b/src/client/views/collections/collectionLinear/CollectionLinearView.scss @@ -17,7 +17,7 @@ .collectionLinearView-menuOpener { user-select: none; } - + > input:not(:checked) ~ &.true { background-color: transparent; } diff --git a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx index 707986ec3..0854bc611 100644 --- a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx +++ b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx @@ -23,6 +23,7 @@ import './CollectionLinearView.scss'; import { Button, Toggle, ToggleType, Type } from 'browndash-components'; import { Colors } from '../../global/globalEnums'; import { BranchingTrailManager } from '../../../util/BranchingTrailManager'; +import { SettingsManager } from '../../../util/SettingsManager'; /** * CollectionLinearView is the class for rendering the horizontal collection @@ -207,9 +208,10 @@ export class CollectionLinearView extends CollectionSubView() { const menuOpener = ( <Toggle text={Cast(this.props.Document.icon, 'string', null)} - icon={Cast(this.props.Document.icon, 'string', null) ? undefined : <FontAwesomeIcon icon={isExpanded ? 'minus' : 'plus'} />} + icon={Cast(this.props.Document.icon, 'string', null) ? undefined : <FontAwesomeIcon color={SettingsManager.userColor} icon={isExpanded ? 'minus' : 'plus'} />} + color={SettingsManager.userColor} + // background={SettingsManager.userVariantColor} type={Type.TERT} - color={StrCast(Doc.UserDoc().userVariantColor, Colors.MEDIUM_BLUE)} onPointerDown={e => e.stopPropagation()} toggleType={ToggleType.BUTTON} toggleStatus={BoolCast(this.layoutDoc.linearView_IsOpen)} diff --git a/src/client/views/linking/LinkMenu.scss b/src/client/views/linking/LinkMenu.scss index 80cf93ed8..0b9f32eee 100644 --- a/src/client/views/linking/LinkMenu.scss +++ b/src/client/views/linking/LinkMenu.scss @@ -12,7 +12,6 @@ position: relative; border: 1px solid #e4e4e4; box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23); - background: white; max-height: 230px; overflow-y: scroll; z-index: 10; diff --git a/src/client/views/linking/LinkMenu.tsx b/src/client/views/linking/LinkMenu.tsx index 65d13a6c3..9dc133e28 100644 --- a/src/client/views/linking/LinkMenu.tsx +++ b/src/client/views/linking/LinkMenu.tsx @@ -7,6 +7,7 @@ import { LinkDocPreview } from '../nodes/LinkDocPreview'; import './LinkMenu.scss'; import { LinkMenuGroup } from './LinkMenuGroup'; import React = require('react'); +import { SettingsManager } from '../../util/SettingsManager'; interface Props { docView: DocumentView; @@ -58,7 +59,7 @@ export class LinkMenu extends React.Component<Props> { const style = this.props.style ?? (dv => ({ left: dv?.left || 0, top: this.props.docView.topMost ? undefined : (dv?.bottom || 0) + 15, bottom: this.props.docView.topMost ? 20 : undefined, maxWidth: 200 }))(this.props.docView.getBounds()); return ( - <div className="linkMenu" ref={this._linkMenuRef} style={{ ...style }}> + <div className="linkMenu" ref={this._linkMenuRef} style={{ ...style, background: SettingsManager.userBackgroundColor, color: SettingsManager.userColor }}> <div className="linkMenu-list">{this.renderAllGroups(LinkManager.Instance.getRelatedGroupedLinks(sourceAnchor))}</div> </div> ); diff --git a/src/client/views/linking/LinkMenuGroup.tsx b/src/client/views/linking/LinkMenuGroup.tsx index 8324a97d9..c1a5a634c 100644 --- a/src/client/views/linking/LinkMenuGroup.tsx +++ b/src/client/views/linking/LinkMenuGroup.tsx @@ -23,10 +23,10 @@ interface LinkMenuGroupProps { export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> { private _menuRef = React.createRef<HTMLDivElement>(); - getBackgroundColor = (): string => { + getBackgroundColor = (): string | undefined => { const link_relationshipList = StrListCast(Doc.UserDoc().link_relationshipList); const linkColorList = StrListCast(Doc.UserDoc().link_ColorList); - let color = 'white'; + let color: string | undefined; // if this link's relationship property is not default "link", set its color if (link_relationshipList) { const relationshipIndex = link_relationshipList.indexOf(this.props.groupType); diff --git a/src/client/views/linking/LinkMenuItem.scss b/src/client/views/linking/LinkMenuItem.scss index 806a2c381..e83f631a1 100644 --- a/src/client/views/linking/LinkMenuItem.scss +++ b/src/client/views/linking/LinkMenuItem.scss @@ -12,8 +12,6 @@ padding-top: 1px; padding-bottom: 1px; - background-color: white; - .linkMenu-name { position: relative; width: auto; @@ -25,7 +23,7 @@ .linkMenu-source-title { text-decoration: none; - color: rgb(43, 43, 43); + //color: rgb(43, 43, 43); font-size: 7px; padding-bottom: 0.75px; @@ -48,13 +46,12 @@ height: 12px; padding: 1px; margin-right: 3px; - color: rgb(161, 161, 161); + // color: rgb(161, 161, 161); } } .linkMenu-destination-title { text-decoration: none; - color: #4476f7; font-size: 13px; line-height: 0.9; padding-bottom: 2px; @@ -64,7 +61,6 @@ &:hover { text-decoration: underline; - color: rgb(60, 90, 156); //display: inline; text-overflow: break; cursor: pointer; @@ -75,7 +71,6 @@ .linkMenu-description { text-decoration: none; font-style: italic; - color: rgb(95, 97, 102); font-size: 9px; line-height: 0.9; margin-left: 20px; @@ -97,17 +92,7 @@ width: 100%; } - .link-metadata { - padding: 0 10px 0 16px; - margin-bottom: 4px; - color: $medium-gray; - font-style: italic; - font-size: 10.5px; - } - &:hover { - background-color: rgb(201, 239, 252); - .linkMenu-item-buttons { opacity: 1; } @@ -115,7 +100,6 @@ .linkMenu-item-content { .linkMenu-destination-title { text-decoration: underline; - color: rgb(60, 90, 156); //display: inline; text-overflow: break; } @@ -129,7 +113,7 @@ display: flex; opacity: 0; - .button { + .linkMenu-deleteButton { width: 20px; height: 20px; margin: 0; @@ -137,7 +121,7 @@ padding-right: 6px; border-radius: 50%; pointer-events: auto; - background-color: $dark-gray; + background-color: red; color: $white; font-size: 65%; transition: transform 0.2s; diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx index a81707ea4..9bc8f5e8b 100644 --- a/src/client/views/linking/LinkMenuItem.tsx +++ b/src/client/views/linking/LinkMenuItem.tsx @@ -130,7 +130,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { }; deleteLink = (e: React.PointerEvent): void => setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => LinkManager.Instance.deleteLink(this.props.linkDoc)))); - + @observable _hover = false; render() { const destinationIcon = Doc.toIcon(this.props.destinationDoc) as any as IconProp; @@ -146,7 +146,11 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { : undefined; return ( - <div className="linkMenu-item" style={{ background: LinkManager.currentLink === this.props.linkDoc ? 'lightBlue' : undefined }}> + <div + className="linkMenu-item" + onPointerEnter={action(e => (this._hover = true))} + onPointerLeave={action(e => (this._hover = false))} + style={{ background: /*LinkManager.currentLink !== this.props.linkDoc*/ this._hover ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor }}> <div className="linkMenu-item-content expand-two"> <div ref={this._drag} @@ -154,7 +158,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { onPointerDown={this.onLinkButtonDown}> <div className="linkMenu-item-buttons"> <Tooltip title={<div className="dash-tooltip">Edit Link</div>}> - <div className="button" style={{ background: LinkManager.currentLink === this.props.linkDoc ? 'black' : 'gray' }} ref={this._editRef} onPointerDown={this.onEdit} onClick={e => e.stopPropagation()}> + <div className="button" ref={this._editRef} onPointerDown={this.onEdit} onClick={e => e.stopPropagation()}> <FontAwesomeIcon className="fa-icon" icon="edit" size="sm" /> </div> </Tooltip> @@ -193,7 +197,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { <div className="linkMenu-item-buttons"> <Tooltip title={<div className="dash-tooltip">Delete Link</div>}> - <div className="button" style={{ background: 'red' }} onPointerDown={this.deleteLink} onClick={e => e.stopPropagation()}> + <div className="linkMenu-deleteButton" onPointerDown={this.deleteLink} onClick={e => e.stopPropagation()}> <FontAwesomeIcon className="fa-icon" icon="trash" size="sm" /> </div> </Tooltip> diff --git a/src/client/views/linking/LinkPopup.tsx b/src/client/views/linking/LinkPopup.tsx index 6895c0746..9a9a89732 100644 --- a/src/client/views/linking/LinkPopup.tsx +++ b/src/client/views/linking/LinkPopup.tsx @@ -3,15 +3,14 @@ import { observer } from 'mobx-react'; import { EditorView } from 'prosemirror-view'; import { Doc } from '../../../fields/Doc'; import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from '../../../Utils'; -import { DocUtils } from '../../documents/Documents'; import { Transform } from '../../util/Transform'; import { undoBatch } from '../../util/UndoManager'; +import { OpenWhere } from '../nodes/DocumentView'; import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; import { SearchBox } from '../search/SearchBox'; import { DefaultStyleProvider } from '../StyleProvider'; import './LinkPopup.scss'; import React = require('react'); -import { OpenWhere } from '../nodes/DocumentView'; interface LinkPopupProps { linkFrom?: () => Doc | undefined; diff --git a/src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx b/src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx index ff17e5c12..3a95e5f74 100644 --- a/src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx +++ b/src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx @@ -1,14 +1,13 @@ -import './ButtonMenu.scss'; +import { action } from 'mobx'; import * as React from 'react'; -import { IButtonMenu } from './utils'; -import { NewLightboxView } from '../NewLightboxView'; -import { SelectionManager } from '../../../util/SelectionManager'; -import { CollectionDockingView } from '../../collections/CollectionDockingView'; -import { OpenWhereMod } from '../../nodes/DocumentView'; import { Doc } from '../../../../fields/Doc'; import { InkTool } from '../../../../fields/InkField'; -import { MainView } from '../../MainView'; -import { action } from 'mobx'; +import { SelectionManager } from '../../../util/SelectionManager'; +import { CollectionDockingView } from '../../collections/CollectionDockingView'; +import { DocumentView, OpenWhereMod } from '../../nodes/DocumentView'; +import { NewLightboxView } from '../NewLightboxView'; +import './ButtonMenu.scss'; +import { IButtonMenu } from './utils'; export const ButtonMenu = (props: IButtonMenu) => { return ( @@ -40,10 +39,10 @@ export const ButtonMenu = (props: IButtonMenu) => { <div className="newLightboxView-exploreBtn" title="toggle explore mode to navigate among documents only" - style={{ background: MainView.Instance._exploreMode ? 'white' : undefined }} + style={{ background: DocumentView.ExploreMode ? 'white' : undefined }} onClick={action(e => { e.stopPropagation(); - MainView.Instance._exploreMode = !MainView.Instance._exploreMode; + DocumentView.ExploreMode = !DocumentView.ExploreMode; })}></div> </div> ); diff --git a/src/client/views/newlightbox/NewLightboxView.tsx b/src/client/views/newlightbox/NewLightboxView.tsx index 3acbd1a32..ca90f6a0f 100644 --- a/src/client/views/newlightbox/NewLightboxView.tsx +++ b/src/client/views/newlightbox/NewLightboxView.tsx @@ -2,35 +2,32 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnTrue } from '../../../Utils'; -import { Doc, DocListCast, Opt, StrListCast } from '../../../fields/Doc'; +import { Doc, DocListCast, Opt } from '../../../fields/Doc'; import { InkTool } from '../../../fields/InkField'; import { Cast, NumCast, StrCast } from '../../../fields/Types'; +import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnTrue } from '../../../Utils'; import { DocUtils } from '../../documents/Documents'; import { DocumentManager } from '../../util/DocumentManager'; import { LinkManager } from '../../util/LinkManager'; import { SelectionManager } from '../../util/SelectionManager'; import { Transform } from '../../util/Transform'; -import { GestureOverlay } from '../GestureOverlay'; -import { MainView } from '../MainView'; -import { DefaultStyleProvider } from '../StyleProvider'; import { CollectionStackedTimeline } from '../collections/CollectionStackedTimeline'; import { TabDocView } from '../collections/TabDocView'; +import { GestureOverlay } from '../GestureOverlay'; +import { LightboxView } from '../LightboxView'; import { DocumentView, OpenWhere } from '../nodes/DocumentView'; +import { DefaultStyleProvider } from '../StyleProvider'; +import { IRecommendation } from './components'; import { ExploreView } from './ExploreView'; -import { IBounds, emptyBounds } from './ExploreView/utils'; +import { emptyBounds, IBounds } from './ExploreView/utils'; import { NewLightboxHeader } from './Header'; import './NewLightboxView.scss'; import { RecommendationList } from './RecommendationList'; -import { IRecommendation } from './components'; -import { fetchKeywords, fetchRecommendations } from './utils'; -import { List } from '../../../fields/List'; -import { LightboxView } from '../LightboxView'; enum LightboxStatus { - RECOMMENDATIONS = "recommendations", - ANNOTATIONS = "annotations", - NONE = "none" + RECOMMENDATIONS = 'recommendations', + ANNOTATIONS = 'annotations', + NONE = 'none', } interface LightboxViewProps { @@ -63,30 +60,30 @@ export class NewLightboxView extends React.Component<LightboxViewProps> { @observable private static _docView: Opt<DocumentView>; // keywords - @observable private static _keywords: string[] = [] + @observable private static _keywords: string[] = []; @action public static SetKeywords(kw: string[]) { - this._keywords = kw + this._keywords = kw; } @computed public static get Keywords() { - return this._keywords + return this._keywords; } // query - @observable private static _query: string = '' + @observable private static _query: string = ''; @action public static SetQuery(query: string) { - this._query = query + this._query = query; } @computed public static get Query() { - return this._query + return this._query; } // keywords - @observable private static _recs: IRecommendation[] = [] + @observable private static _recs: IRecommendation[] = []; @action public static SetRecs(recs: IRecommendation[]) { - this._recs = recs + this._recs = recs; } @computed public static get Recs() { - return this._recs + return this._recs; } // bounds @@ -108,7 +105,7 @@ export class NewLightboxView extends React.Component<LightboxViewProps> { } // newLightbox sidebar status - @observable private static _sidebarStatus: Opt<string> = ""; + @observable private static _sidebarStatus: Opt<string> = ''; @action public static SetSidebarStatus(sidebarStatus: Opt<string>) { this._sidebarStatus = sidebarStatus; } @@ -129,7 +126,7 @@ export class NewLightboxView extends React.Component<LightboxViewProps> { this._docFilters && (this._docFilters.length = 0); this._future = this._history = []; Doc.ActiveTool = InkTool.None; - MainView.Instance._exploreMode = false; + DocumentView.ExploreMode = false; } else { const l = DocUtils.MakeLinkToActiveAudio(() => doc).lastElement(); l && (Cast(l.link_anchor_2, Doc, null).backgroundColor = 'lightgreen'); @@ -289,35 +286,38 @@ export class NewLightboxView extends React.Component<LightboxViewProps> { @computed get documentView() { - if (!LightboxView.LightboxDoc) return null - else return (<GestureOverlay isActive={true}> - <DocumentView - ref={action((r: DocumentView | null) => (NewLightboxView._docView = r !== null ? r : undefined))} - Document={LightboxView.LightboxDoc} - DataDoc={undefined} - PanelWidth={this.newLightboxWidth} - PanelHeight={this.newLightboxHeight} - LayoutTemplate={NewLightboxView.LightboxDocTemplate} - isDocumentActive={returnTrue} // without this being true, sidebar annotations need to be activated before text can be selected. - isContentActive={returnTrue} - styleProvider={DefaultStyleProvider} - ScreenToLocalTransform={this.newLightboxScreenToLocal} - renderDepth={0} - rootSelected={returnTrue} - docViewPath={returnEmptyDoclist} - childFilters={this.docFilters} - childFiltersByRanges={returnEmptyFilter} - searchFilterDocs={returnEmptyDoclist} - addDocument={undefined} - removeDocument={undefined} - whenChildContentsActiveChanged={emptyFunction} - addDocTab={this.addDocTab} - pinToPres={TabDocView.PinDoc} - bringToFront={emptyFunction} - onBrowseClick={MainView.Instance.exploreMode} - focus={emptyFunction} - /> - </GestureOverlay>) + if (!LightboxView.LightboxDoc) return null; + else + return ( + <GestureOverlay isActive={true}> + <DocumentView + ref={action((r: DocumentView | null) => (NewLightboxView._docView = r !== null ? r : undefined))} + Document={LightboxView.LightboxDoc} + DataDoc={undefined} + PanelWidth={this.newLightboxWidth} + PanelHeight={this.newLightboxHeight} + LayoutTemplate={NewLightboxView.LightboxDocTemplate} + isDocumentActive={returnTrue} // without this being true, sidebar annotations need to be activated before text can be selected. + isContentActive={returnTrue} + styleProvider={DefaultStyleProvider} + ScreenToLocalTransform={this.newLightboxScreenToLocal} + renderDepth={0} + rootSelected={returnTrue} + docViewPath={returnEmptyDoclist} + childFilters={this.docFilters} + childFiltersByRanges={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + addDocument={undefined} + removeDocument={undefined} + whenChildContentsActiveChanged={emptyFunction} + addDocTab={this.addDocTab} + pinToPres={TabDocView.PinDoc} + bringToFront={emptyFunction} + onBrowseClick={DocumentView.exploreMode} + focus={emptyFunction} + /> + </GestureOverlay> + ); } future = () => NewLightboxView._future; @@ -337,29 +337,28 @@ export class NewLightboxView extends React.Component<LightboxViewProps> { NewLightboxView.SetNewLightboxDoc(undefined); } }}> - <div className={`app-document`} style={{gridTemplateColumns: `calc(100% - 400px) 400px`}}> - <div - className="newLightboxView-contents" - style={{ - top: 20, - left: 20, - width: this.newLightboxWidth(), - height: this.newLightboxHeight() - 40, - }}> + <div className={`app-document`} style={{ gridTemplateColumns: `calc(100% - 400px) 400px` }}> + <div + className="newLightboxView-contents" + style={{ + top: 20, + left: 20, + width: this.newLightboxWidth(), + height: this.newLightboxHeight() - 40, + }}> <NewLightboxHeader height={newLightboxHeaderHeight} width={this.newLightboxWidth()} /> - {!NewLightboxView._explore ? - <div className="newLightboxView-doc" style={{height: this.newLightboxHeight()}}> + {!NewLightboxView._explore ? ( + <div className="newLightboxView-doc" style={{ height: this.newLightboxHeight() }}> {this.documentView} </div> - : + ) : ( <div className={`explore`}> - <ExploreView recs={NewLightboxView.Recs} bounds={NewLightboxView.Bounds}/> + <ExploreView recs={NewLightboxView.Recs} bounds={NewLightboxView.Bounds} /> </div> - } - </div> - <RecommendationList keywords={NewLightboxView.Keywords}/> + )} + </div> + <RecommendationList keywords={NewLightboxView.Keywords} /> </div> - </div> ); } diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx index c07ab5ba1..b9db5fe15 100644 --- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx +++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx @@ -1,3 +1,4 @@ +import { Toggle, ToggleType, Type } from 'browndash-components'; import { action, computed, ObservableMap } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; @@ -9,12 +10,11 @@ import { Docs } from '../../../documents/Documents'; import { ViewBoxAnnotatableComponent } from '../../DocComponent'; import { FieldView, FieldViewProps } from '../FieldView'; import { PinProps } from '../trails'; +import { Histogram } from './components/Histogram'; import { LineChart } from './components/LineChart'; +import { PieChart } from './components/PieChart'; import { TableBox } from './components/TableBox'; import './DataVizBox.scss'; -import { Histogram } from './components/Histogram'; -import { PieChart } from './components/PieChart'; -import { Toggle, ToggleType, Type } from 'browndash-components'; export enum DataVizView { TABLE = 'table', @@ -29,33 +29,44 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { return FieldView.LayoutString(DataVizBox, fieldStr); } - // all data - static pairSet = new ObservableMap<string, { [key: string]: string }[]>(); - @computed.struct get pairs() { - return DataVizBox.pairSet.get(CsvCast(this.rootDoc[this.fieldKey]).url.href); - } + // all datasets that have been retrieved from the server stored as a map from the dataset url to an array of records + static dataset = new ObservableMap<string, { [key: string]: string }[]>(); + private _vizRenderer: LineChart | Histogram | PieChart | undefined; - private _chartRenderer: LineChart | Histogram | PieChart | undefined; + // all CSV records in the dataset (that aren't an empty row) + @computed.struct get records() { + var records = DataVizBox.dataset.get(CsvCast(this.rootDoc[this.fieldKey]).url.href); + return records?.filter(record => Object.keys(record).some(key => record[key])); + } - // current displayed chart type + // currently chosen visualization type: line, pie, histogram, table @computed get dataVizView(): DataVizView { return StrCast(this.layoutDoc._dataViz, 'table') as DataVizView; } + @computed get dataUrl() { + return Cast(this.dataDoc[this.fieldKey], CsvField); + } + @computed.struct get axes() { + return StrListCast(this.layoutDoc.dataViz_axes); + } + + selectAxes = (axes: string[]) => (this.layoutDoc.dataViz_axes = new List<string>(axes)); + @action // pinned / linked anchor doc includes selected rows, graph titles, and graph colors restoreView = (data: Doc) => { const changedView = this.dataVizView !== data.config_dataViz && (this.layoutDoc._dataViz = data.config_dataViz); const changedAxes = this.axes.join('') !== StrListCast(data.config_dataVizAxes).join('') && (this.layoutDoc._dataViz_axes = new List<string>(StrListCast(data.config_dataVizAxes))); this.layoutDoc.dataViz_selectedRows = Field.Copy(data.dataViz_selectedRows); - this.layoutDoc.histogramBarColors = Field.Copy(data.histogramBarColors); - this.layoutDoc.defaultHistogramColor = data.defaultHistogramColor; - this.layoutDoc.pieSliceColors = Field.Copy(data.pieSliceColors); + this.layoutDoc.dataViz_histogram_barColors = Field.Copy(data.dataViz_histogram_barColors); + this.layoutDoc.dataViz_histogram_defaultColor = data.dataViz_histogram_defaultColor; + this.layoutDoc.dataViz_pie_sliceColors = Field.Copy(data.dataViz_pie_sliceColors); Object.keys(this.layoutDoc).map(key => { - if (key.startsWith('histogram_title') || key.startsWith('lineChart_title') || key.startsWith('pieChart_title')) { + if (key.startsWith('dataViz_histogram_title') || key.startsWith('dataViz_lineChart_title') || key.startsWith('dataViz_pieChart_title')) { this.layoutDoc['_' + key] = data[key]; } }); - const func = () => this._chartRenderer?.restoreView(data); + const func = () => this._vizRenderer?.restoreView(data); if (changedView || changedAxes) { setTimeout(func, 100); return true; @@ -65,7 +76,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { getAnchor = (addAsAnnotation?: boolean, pinProps?: PinProps) => { const anchor = !pinProps ? this.rootDoc - : this._chartRenderer?.getAnchor(pinProps) ?? + : this._vizRenderer?.getAnchor(pinProps) ?? Docs.Create.ConfigDocument({ // when we clear selection -> we should have it so chartBox getAnchor returns undefined // this is for when we want the whole doc (so when the chartBox getAnchor returns without a marker) @@ -74,11 +85,11 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { anchor.config_dataViz = this.dataVizView; anchor.config_dataVizAxes = this.axes.length ? new List<string>(this.axes) : undefined; anchor.dataViz_selectedRows = Field.Copy(this.layoutDoc.dataViz_selectedRows); - anchor.histogramBarColors = Field.Copy(this.layoutDoc.histogramBarColors); - anchor.defaultHistogramColor = this.layoutDoc.defaultHistogramColor; - anchor.pieSliceColors = Field.Copy(this.layoutDoc.pieSliceColors); + anchor.dataViz_histogram_barColors = Field.Copy(this.layoutDoc.dataViz_histogram_barColors); + anchor.dataViz_histogram_defaultColor = this.layoutDoc.dataViz_histogram_defaultColor; + anchor.dataViz_pie_sliceColors = Field.Copy(this.layoutDoc.dataViz_pie_sliceColors); Object.keys(this.layoutDoc).map(key => { - if (key.startsWith('histogram_title') || key.startsWith('lineChart_title') || key.startsWith('pieChart_title')) { + if (key.startsWith('dataViz_histogram_title') || key.startsWith('dataViz_lineChart_title') || key.startsWith('dataViz_pieChart_title')) { anchor[key] = this.layoutDoc[key]; } }); @@ -86,87 +97,78 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { return anchor; }; - @computed.struct get axes() { - return StrListCast(this.layoutDoc.dataViz_axes); + componentDidMount() { + this.props.setContentView?.(this); + if (!DataVizBox.dataset.has(CsvCast(this.rootDoc[this.fieldKey]).url.href)) this.fetchData(); } - selectAxes = (axes: string[]) => (this.layoutDoc.dataViz_axes = new List<string>(axes)); + + fetchData = () => { + DataVizBox.dataset.set(CsvCast(this.rootDoc[this.fieldKey]).url.href, []); // assign temporary dataset as a lock to prevent duplicate server requests + fetch('/csvData?uri=' + this.dataUrl?.url.href) // + .then(res => res.json().then(action(res => !res.errno && DataVizBox.dataset.set(CsvCast(this.rootDoc[this.fieldKey]).url.href, res)))); + }; // toggles for user to decide which chart type to view the data in - @computed get selectView() { + renderVizView = () => { const width = this.props.PanelWidth() * 0.9; const height = (this.props.PanelHeight() - 32) /* height of 'change view' button */ * 0.9; const margin = { top: 10, right: 25, bottom: 75, left: 45 }; - if (!this.pairs) return 'no data'; - switch (this.dataVizView) { - case DataVizView.TABLE: - return <TableBox layoutDoc={this.layoutDoc} pairs={this.pairs} axes={this.axes} height={height} width={width} margin={margin} rootDoc={this.rootDoc} docView={this.props.DocumentView} selectAxes={this.selectAxes} />; - case DataVizView.LINECHART: - return ( - <LineChart - layoutDoc={this.layoutDoc} - ref={r => (this._chartRenderer = r ?? undefined)} - height={height} - width={width} - fieldKey={this.fieldKey} - margin={margin} - rootDoc={this.rootDoc} - axes={this.axes} - pairs={this.pairs} - dataDoc={this.dataDoc} - /> - ); - case DataVizView.HISTOGRAM: - return ( - <Histogram - layoutDoc={this.layoutDoc} - ref={r => (this._chartRenderer = r ?? undefined)} - height={height} - width={width} - fieldKey={this.fieldKey} - margin={margin} - rootDoc={this.rootDoc} - axes={this.axes} - pairs={this.pairs} - dataDoc={this.dataDoc} - /> - ); - case DataVizView.PIECHART: - return ( - <PieChart - layoutDoc={this.layoutDoc} - ref={r => (this._chartRenderer = r ?? undefined)} - height={height} - width={width} - fieldKey={this.fieldKey} - margin={margin} - rootDoc={this.rootDoc} - axes={this.axes} - pairs={this.pairs} - dataDoc={this.dataDoc} - /> - ); + if (this.records) { + switch (this.dataVizView) { + case DataVizView.TABLE: + return <TableBox layoutDoc={this.layoutDoc} records={this.records} axes={this.axes} height={height} width={width} margin={margin} rootDoc={this.rootDoc} docView={this.props.DocumentView} selectAxes={this.selectAxes} />; + case DataVizView.LINECHART: + return ( + <LineChart + layoutDoc={this.layoutDoc} + ref={r => (this._vizRenderer = r ?? undefined)} + height={height} + width={width} + fieldKey={this.fieldKey} + margin={margin} + rootDoc={this.rootDoc} + axes={this.axes} + records={this.records} + dataDoc={this.dataDoc} + /> + ); + case DataVizView.HISTOGRAM: + return ( + <Histogram + layoutDoc={this.layoutDoc} + ref={r => (this._vizRenderer = r ?? undefined)} + height={height} + width={width} + fieldKey={this.fieldKey} + margin={margin} + rootDoc={this.rootDoc} + axes={this.axes} + records={this.records} + dataDoc={this.dataDoc} + /> + ); + case DataVizView.PIECHART: + return ( + <PieChart + layoutDoc={this.layoutDoc} + ref={r => (this._vizRenderer = r ?? undefined)} + height={height} + width={width} + fieldKey={this.fieldKey} + margin={margin} + rootDoc={this.rootDoc} + axes={this.axes} + records={this.records} + dataDoc={this.dataDoc} + /> + ); + } } - } - - @computed get dataUrl() { - return Cast(this.dataDoc[this.fieldKey], CsvField); - } - - componentDidMount() { - this.props.setContentView?.(this); - this.fetchData(); - if (!this.layoutDoc._dataViz) this.layoutDoc._dataViz = this.dataVizView; - } - - fetchData() { - if (DataVizBox.pairSet.has(CsvCast(this.rootDoc[this.fieldKey]).url.href)) return; - DataVizBox.pairSet.set(CsvCast(this.rootDoc[this.fieldKey]).url.href, []); - fetch('/csvData?uri=' + this.dataUrl?.url.href) // - .then(res => res.json().then(action(res => !res.errno && DataVizBox.pairSet.set(CsvCast(this.rootDoc[this.fieldKey]).url.href, res)))); - } + return 'no data/visualization'; + }; render() { - return !this.pairs?.length ? ( + return !this.records?.length ? ( // displays how to get data into the DataVizBox if its empty <div className="start-message">To create a DataViz box, either import / drag a CSV file into your canvas or copy a data table and use the command 'ctrl + p' to bring the data table to your canvas.</div> ) : ( @@ -189,7 +191,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { <Toggle text={'HISTOGRAM'} toggleType={ToggleType.BUTTON} type={Type.SEC} color={'black'} onClick={e => (this.layoutDoc._dataViz = DataVizView.HISTOGRAM)} toggleStatus={this.layoutDoc._dataViz == DataVizView.HISTOGRAM} /> <Toggle text={'PIE CHART'} toggleType={ToggleType.BUTTON} type={Type.SEC} color={'black'} onClick={e => (this.layoutDoc._dataViz = DataVizView.PIECHART)} toggleStatus={this.layoutDoc._dataViz == DataVizView.PIECHART} /> </div> - {this.selectView} + {this.renderVizView()} </div> ); } diff --git a/src/client/views/nodes/DataVizBox/components/Chart.scss b/src/client/views/nodes/DataVizBox/components/Chart.scss index 35e5187b2..50dfe7f05 100644 --- a/src/client/views/nodes/DataVizBox/components/Chart.scss +++ b/src/client/views/nodes/DataVizBox/components/Chart.scss @@ -17,6 +17,18 @@ margin-top: -10px; margin-bottom: -10px; } + .asHistogram-checkBox { + // display: flex; + // flex-direction: row; + align-items: left; + align-self: left; + align-content: left; + justify-content: flex-end; + float: left; + left: 0; + position: relative; + margin-bottom: -35px; + } .selected-data{ align-items: center; text-align: center; @@ -76,9 +88,22 @@ fill: red; } } +.tableBox { + display: flex; + flex-direction: column; +} .table-container{ overflow: scroll; - margin: 10px; + margin: 5px; margin-left: 25px; - margin-top: 25px; + margin-right: 10px; + margin-bottom: 0; +} +.selectAll-buttons { + display: flex; + flex-direction: row; + justify-content: flex-end; + margin-top: 5px; + margin-right: 10px; + float: right; }
\ No newline at end of file diff --git a/src/client/views/nodes/DataVizBox/components/Histogram.tsx b/src/client/views/nodes/DataVizBox/components/Histogram.tsx index b3bdccbbb..e67e2bf31 100644 --- a/src/client/views/nodes/DataVizBox/components/Histogram.tsx +++ b/src/client/views/nodes/DataVizBox/components/Histogram.tsx @@ -1,26 +1,26 @@ +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { ColorPicker, EditableText, IconButton, Size, Type } from 'browndash-components'; +import * as d3 from 'd3'; +import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; -import { Doc, StrListCast } from '../../../../../fields/Doc'; import * as React from 'react'; -import * as d3 from 'd3'; -import { IReactionDisposer, action, computed, observable, reaction } from 'mobx'; -import { LinkManager } from '../../../../util/LinkManager'; -import { Cast, DocCast, StrCast } from '../../../../../fields/Types'; -import { PinProps, PresBox } from '../../trails'; -import { Docs } from '../../../../documents/Documents'; -import { List } from '../../../../../fields/List'; -import './Chart.scss'; -import { ColorPicker, EditableText, IconButton, Size, Type } from 'browndash-components'; import { FaFillDrip } from 'react-icons/fa'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Doc, NumListCast, StrListCast } from '../../../../../fields/Doc'; +import { List } from '../../../../../fields/List'; import { listSpec } from '../../../../../fields/Schema'; +import { Cast, DocCast, StrCast } from '../../../../../fields/Types'; +import { Docs } from '../../../../documents/Documents'; +import { LinkManager } from '../../../../util/LinkManager'; +import { undoable } from '../../../../util/UndoManager'; +import { PinProps, PresBox } from '../../trails'; import { scaleCreatorNumerical, yAxisCreator } from '../utils/D3Utils'; -import { undoBatch, undoable } from '../../../../util/UndoManager'; +import './Chart.scss'; export interface HistogramProps { rootDoc: Doc; layoutDoc: Doc; axes: string[]; - pairs: { [key: string]: any }[]; + records: { [key: string]: any }[]; width: number; height: number; dataDoc: Doc; @@ -46,44 +46,47 @@ export class Histogram extends React.Component<HistogramProps> { private selectedData: any = undefined; // Selection of selected bar private hoverOverData: any = undefined; // Selection of bar being hovered over + @computed get _tableDataIds() { + return !this.parentViz ? this.props.records.map((rec, i) => i) : NumListCast(this.parentViz.dataViz_selectedRows); + } + // returns all the data records that will be rendered by only returning those records that have been selected by the parent visualization (or all records if there is no parent) + @computed get _tableData() { + return !this.parentViz ? this.props.records : this._tableDataIds.map(rowId => this.props.records[rowId]); + } // filters all data to just display selected data if brushed (created from an incoming link) @computed get _histogramData() { - var guids = StrListCast(this.props.layoutDoc.dataViz_rowGuids); if (this.props.axes.length < 1) return []; if (this.props.axes.length < 2) { var ax0 = this.props.axes[0]; - if (/\d/.test(this.props.pairs[0][ax0])) { + if (/\d/.test(this.props.records[0][ax0])) { this.numericalXData = true; } - return this.props.pairs - ?.filter(pair => (!this.incomingLinks.length ? true : this.incomingLinks[0]!.dataViz_selectedRows && StrListCast(this.incomingLinks[0].dataViz_selectedRows).includes(guids[this.props.pairs.indexOf(pair)]))) - .map(pair => ({ [ax0]: pair[this.props.axes[0]] })); + return this._tableData.map(record => ({ [ax0]: record[this.props.axes[0]] })); } var ax0 = this.props.axes[0]; var ax1 = this.props.axes[1]; - if (/\d/.test(this.props.pairs[0][ax0])) { + if (/\d/.test(this.props.records[0][ax0])) { this.numericalXData = true; } - if (/\d/.test(this.props.pairs[0][ax1])) { + if (/\d/.test(this.props.records[0][ax1])) { this.numericalYData = true; } - return this.props.pairs - ?.filter(pair => (!this.incomingLinks.length ? true : this.incomingLinks[0]!.dataViz_selectedRows && StrListCast(this.incomingLinks[0].dataViz_selectedRows).includes(guids[this.props.pairs.indexOf(pair)]))) - .map(pair => ({ [ax0]: pair[this.props.axes[0]], [ax1]: pair[this.props.axes[1]] })); + return this._tableData.map(record => ({ [ax0]: record[this.props.axes[0]], [ax1]: record[this.props.axes[1]] })); } @computed get defaultGraphTitle() { var ax0 = this.props.axes[0]; var ax1 = this.props.axes.length > 1 ? this.props.axes[1] : undefined; - if (this.props.axes.length < 2 || !ax1 || !/\d/.test(this.props.pairs[0][ax1]) || !this.numericalYData) { + if (this.props.axes.length < 2 || !ax1 || !/\d/.test(this.props.records[0][ax1]) || !this.numericalYData) { return ax0 + ' Histogram'; } else return ax0 + ' by ' + ax1 + ' Histogram'; } - @computed get incomingLinks() { - return LinkManager.Instance.getAllRelatedLinks(this.props.rootDoc) // out of all links - .filter(link => link.link_anchor_1 == this.props.rootDoc.draggedFrom) // get links where this chart doc is the target of the link - .map(link => DocCast(link.link_anchor_1)); // then return the source of the link + @computed get parentViz() { + return DocCast(this.props.rootDoc.dataViz_parentViz); + // return LinkManager.Instance.getAllRelatedLinks(this.props.rootDoc) // out of all links + // .filter(link => link.link_anchor_1 == this.props.rootDoc.dataViz_parentViz) // get links where this chart doc is the target of the link + // .map(link => DocCast(link.link_anchor_1)); // then return the source of the link } @computed get rangeVals(): { xMin?: number; xMax?: number; yMin?: number; yMax?: number } { @@ -100,11 +103,7 @@ export class Histogram extends React.Component<HistogramProps> { componentDidMount = () => { this._disposers.chartData = reaction( () => ({ dataSet: this._histogramData, w: this.width, h: this.height }), - ({ dataSet, w, h }) => { - if (dataSet!.length > 0) { - this.drawChart(dataSet, w, h); - } - }, + ({ dataSet, w, h }) => dataSet!.length > 0 && this.drawChart(dataSet, w, h), { fireImmediately: true } ); }; @@ -114,7 +113,6 @@ export class Histogram extends React.Component<HistogramProps> { // create a document anchor that stores whatever is needed to reconstruct the viewing state (selection,zoom,etc) getAnchor = (pinProps?: PinProps) => { const anchor = Docs.Create.ConfigDocument({ - // title: 'histogram doc selection' + this._currSelected, }); PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this.props.rootDoc); @@ -131,21 +129,15 @@ export class Histogram extends React.Component<HistogramProps> { // cleans data by converting numerical data to numbers and taking out empty cells data = (dataSet: any) => { - var validData = dataSet.filter((d: { [x: string]: unknown }) => { - var valid = true; - Object.keys(dataSet[0]).map(key => { - if (!d[key] || Number.isNaN(d[key])) valid = false; - }); - return valid; - }); - var field = dataSet[0] ? Object.keys(dataSet[0])[0] : undefined; - const data = validData.map((d: { [x: string]: any }) => { - if (this.numericalXData) { - return +d[field!].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, ''); - } - return d[field!]; - }); - return data; + var validData = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || Number.isNaN(d[key]))); + const field = dataSet[0] ? Object.keys(dataSet[0])[0] : undefined; + return !field + ? [] + : validData.map((d: { [x: string]: any }) => + !this.numericalXData // + ? d[field] + : +d[field!].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '') + ); }; // outlines the bar selected / hovered over @@ -185,24 +177,18 @@ export class Histogram extends React.Component<HistogramProps> { d3.select(this._histogramRef.current).select('svg').remove(); d3.select(this._histogramRef.current).select('.tooltip').remove(); - var data = this.data(dataSet); - var xAxisTitle = Object.keys(dataSet[0])[0]; - var yAxisTitle = this.numericalYData ? Object.keys(dataSet[0])[1] : 'frequency'; - let uniqueArr: unknown[] = [...new Set(data)]; + const data = this.data(dataSet); + const xAxisTitle = Object.keys(dataSet[0])[0]; + const yAxisTitle = this.numericalYData ? Object.keys(dataSet[0])[1] : 'frequency'; + const uniqueArr: unknown[] = [...new Set(data)]; var numBins = this.numericalXData && Number.isInteger(data[0]) ? this.rangeVals.xMax! - this.rangeVals.xMin! : uniqueArr.length; var translateXAxis = !this.numericalXData || numBins < this.maxBins ? width / (numBins + 1) / 2 : 0; if (numBins > this.maxBins) numBins = this.maxBins; - var startingPoint = this.numericalXData ? this.rangeVals.xMin! : 0; - var endingPoint = this.numericalXData ? this.rangeVals.xMax! : numBins; + const startingPoint = this.numericalXData ? this.rangeVals.xMin! : 0; + const endingPoint = this.numericalXData ? this.rangeVals.xMax! : numBins; // converts data into Objects - var histDataSet = dataSet.filter((d: { [x: string]: unknown }) => { - var valid = true; - Object.keys(dataSet[0]).map(key => { - if (!d[key] || Number.isNaN(d[key])) valid = false; - }); - return valid; - }); + var histDataSet = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || Number.isNaN(d[key]))); if (!this.numericalXData) { var histStringDataSet: { [x: string]: unknown }[] = []; if (this.numericalYData) { @@ -321,7 +307,7 @@ export class Histogram extends React.Component<HistogramProps> { // click/hover const onPointClick = action((e: any) => this.highlightSelectedBar(true, svg, eachRectWidth, d3.pointer(e)[0], xAxisTitle, yAxisTitle, histDataSet)); const onHover = action((e: any) => { - const selected = this.highlightSelectedBar(false, svg, eachRectWidth, d3.pointer(e)[0], xAxisTitle, yAxisTitle, histDataSet); + this.highlightSelectedBar(false, svg, eachRectWidth, d3.pointer(e)[0], xAxisTitle, yAxisTitle, histDataSet); updateHighlights(); }); const mouseOut = action((e: any) => { @@ -360,10 +346,10 @@ export class Histogram extends React.Component<HistogramProps> { 'transform', this.numericalYData ? function (d) { - var eachData = histDataSet.filter((data: { [x: string]: number }) => { + const eachData = histDataSet.filter((data: { [x: string]: number }) => { return data[xAxisTitle] == d[0]; }); - var length = eachData.length ? eachData[0][yAxisTitle].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '') : 0; + const length = eachData.length ? eachData[0][yAxisTitle].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '') : 0; return 'translate(' + x(d.x0!) + ',' + y(length) + ')'; } : function (d) { @@ -374,10 +360,10 @@ export class Histogram extends React.Component<HistogramProps> { 'height', this.numericalYData ? function (d) { - var eachData = histDataSet.filter((data: { [x: string]: number }) => { + const eachData = histDataSet.filter((data: { [x: string]: number }) => { return data[xAxisTitle] == d[0]; }); - var length = eachData.length ? eachData[0][yAxisTitle].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '') : 0; + const length = eachData.length ? eachData[0][yAxisTitle].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '') : 0; return height - y(length); } : function (d) { @@ -389,7 +375,7 @@ export class Histogram extends React.Component<HistogramProps> { 'class', selected ? function (d) { - return selected && selected[0] == d[0] ? 'histogram-bar hover' : 'histogram-bar'; + return selected && selected[0] === d[0] ? 'histogram-bar hover' : 'histogram-bar'; } : function (d) { return 'histogram-bar'; @@ -397,67 +383,78 @@ export class Histogram extends React.Component<HistogramProps> { ) .attr('fill', d => { var barColor; - var barColors = StrListCast(this.props.layoutDoc.histogramBarColors).map(each => each.split('::')); - barColors.map(each => { + const barColors = StrListCast(this.props.layoutDoc.dataViz_histogram_barColors).map(each => each.split('::')); + barColors.forEach(each => { if (d[0] && d[0].toString() && each[0] == d[0].toString()) barColor = each[1]; else { - var range = StrCast(each[0]).split(' to '); + const range = StrCast(each[0]).split(' to '); if (Number(range[0]) <= d[0] && d[0] <= Number(range[1])) barColor = each[1]; } }); - return barColor ? StrCast(barColor) : StrCast(this.props.layoutDoc.defaultHistogramColor); + return barColor ? StrCast(barColor) : StrCast(this.props.layoutDoc.dataViz_histogram_defaultColor); }); }; @action changeSelectedColor = (color: string) => { this.curBarSelected.attr('fill', color); - var barName = StrCast(this._currSelected[this.props.axes[0]].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '')); + const barName = StrCast(this._currSelected[this.props.axes[0]].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '')); - const barColors = Cast(this.props.layoutDoc.histogramBarColors, listSpec('string'), null); - barColors.map(each => { - if (each.split('::')[0] == barName) barColors.splice(barColors.indexOf(each), 1); - }); + const barColors = Cast(this.props.layoutDoc.dataViz_histogram_barColors, listSpec('string'), null); + barColors.forEach(each => each.split('::')[0] === barName && barColors.splice(barColors.indexOf(each), 1)); barColors.push(StrCast(barName + '::' + color)); }; @action eraseSelectedColor = () => { - this.curBarSelected.attr('fill', this.props.layoutDoc.defaultHistogramColor); - var barName = StrCast(this._currSelected[this.props.axes[0]].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '')); + this.curBarSelected.attr('fill', this.props.layoutDoc.dataViz_histogram_defaultColor); + const barName = StrCast(this._currSelected[this.props.axes[0]].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '')); - const barColors = Cast(this.props.layoutDoc.histogramBarColors, listSpec('string'), null); - barColors.map(each => { - if (each.split('::')[0] == barName) barColors.splice(barColors.indexOf(each), 1); - }); + const barColors = Cast(this.props.layoutDoc.dataViz_histogram_barColors, listSpec('string'), null); + barColors.forEach(each => each.split('::')[0] === barName && barColors.splice(barColors.indexOf(each), 1)); + }; + + updateBarColors = () => { + var svg = this._histogramSvg; + if (svg) + svg.selectAll('rect').attr('fill', (d: any) => { + var barColor; + const barColors = StrListCast(this.props.layoutDoc.dataViz_histogram_barColors).map(each => each.split('::')); + barColors.forEach(each => { + if (d[0] && d[0].toString() && each[0] == d[0].toString()) barColor = each[1]; + else { + const range = StrCast(each[0]).split(' to '); + if (Number(range[0]) <= d[0] && d[0] <= Number(range[1])) barColor = each[1]; + } + }); + return barColor ? StrCast(barColor) : StrCast(this.props.layoutDoc.dataViz_histogram_defaultColor); + }); }; render() { + this.updateBarColors(); this._histogramData; var curSelectedBarName = ''; var titleAccessor: any = ''; - if (this.props.axes.length == 2) titleAccessor = 'dataViz_title_histogram_' + this.props.axes[0] + '-' + this.props.axes[1]; - else if (this.props.axes.length > 0) titleAccessor = 'dataViz_title_histogram_' + this.props.axes[0]; + if (this.props.axes.length == 2) titleAccessor = 'dataViz_histogram_title' + this.props.axes[0] + '-' + this.props.axes[1]; + else if (this.props.axes.length > 0) titleAccessor = 'dataViz_histogram_title' + this.props.axes[0]; if (!this.props.layoutDoc[titleAccessor]) this.props.layoutDoc[titleAccessor] = this.defaultGraphTitle; - if (!this.props.layoutDoc.defaultHistogramColor) this.props.layoutDoc.defaultHistogramColor = '#69b3a2'; - if (!this.props.layoutDoc.histogramBarColors) this.props.layoutDoc.histogramBarColors = new List<string>(); - var selected: string; + if (!this.props.layoutDoc.dataViz_histogram_defaultColor) this.props.layoutDoc.dataViz_histogram_defaultColor = '#69b3a2'; + if (!this.props.layoutDoc.dataViz_histogram_barColors) this.props.layoutDoc.dataViz_histogram_barColors = new List<string>(); + var selected = 'none'; if (this._currSelected) { curSelectedBarName = StrCast(this._currSelected![this.props.axes[0]].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '')); selected = '{ '; - Object.keys(this._currSelected).map(key => { - key != '' ? (selected += key + ': ' + this._currSelected[key] + ', ') : ''; - }); - selected = selected.substring(0, selected.length - 2); - selected += ' }'; - } else selected = 'none'; + Object.keys(this._currSelected).forEach(key => + key // + ? (selected += key + ': ' + this._currSelected[key] + ', ') + : '' + ); + selected = selected.substring(0, selected.length - 2) + ' }'; + } var selectedBarColor; var barColors = StrListCast(this.props.layoutDoc.histogramBarColors).map(each => each.split('::')); - barColors.map(each => { - if (each[0] == curSelectedBarName!) selectedBarColor = each[1]; - }); - - this.componentDidMount(); + barColors.forEach(each => each[0] === curSelectedBarName && (selectedBarColor = each[1])); - if (this._histogramData.length > 0 || (!this.incomingLinks || this.incomingLinks.length==0)) { + if (this._histogramData.length > 0 || !this.parentViz) { return this.props.axes.length >= 1 ? ( <div className="chart-container"> <div className="graph-title"> @@ -476,9 +473,9 @@ export class Histogram extends React.Component<HistogramProps> { tooltip={'Change Default Bar Color'} type={Type.SEC} icon={<FaFillDrip />} - selectedColor={StrCast(this.props.layoutDoc.defaultHistogramColor)} - setFinalColor={undoable(color => (this.props.layoutDoc.defaultHistogramColor = color), 'Change Default Bar Color')} - setSelectedColor={undoable(color => (this.props.layoutDoc.defaultHistogramColor = color), 'Change Default Bar Color')} + selectedColor={StrCast(this.props.layoutDoc.dataViz_histogram_defaultColor)} + setFinalColor={undoable(color => (this.props.layoutDoc.dataViz_histogram_defaultColor = color), 'Change Default Bar Color')} + setSelectedColor={undoable(color => (this.props.layoutDoc.dataViz_histogram_defaultColor = color), 'Change Default Bar Color')} size={Size.XSMALL} /> </div> @@ -514,10 +511,8 @@ export class Histogram extends React.Component<HistogramProps> { ) : ( <span className="chart-container"> {'first use table view to select a column to graph'}</span> ); - } else - return ( - // when it is a brushed table and the incoming table doesn't have any rows selected - <div className="chart-container">Selected rows of data from the incoming DataVizBox to display.</div> - ); + } + // when it is a brushed table and the incoming table doesn't have any rows selected + return <div className="chart-container">Selected rows of data from the incoming DataVizBox to display.</div>; } -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/DataVizBox/components/LineChart.tsx b/src/client/views/nodes/DataVizBox/components/LineChart.tsx index 46cf27705..3de7a0c4a 100644 --- a/src/client/views/nodes/DataVizBox/components/LineChart.tsx +++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx @@ -1,21 +1,19 @@ +import { EditableText, Size } from 'browndash-components'; +import * as d3 from 'd3'; import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import * as d3 from 'd3'; -import { Doc, DocListCast, StrListCast } from '../../../../../fields/Doc'; -import { Id } from '../../../../../fields/FieldSymbols'; +import { Doc, DocListCast, NumListCast, StrListCast } from '../../../../../fields/Doc'; import { List } from '../../../../../fields/List'; import { listSpec } from '../../../../../fields/Schema'; import { Cast, DocCast, StrCast } from '../../../../../fields/Types'; import { Docs } from '../../../../documents/Documents'; import { DocumentManager } from '../../../../util/DocumentManager'; -import { LinkManager } from '../../../../util/LinkManager'; +import { undoable } from '../../../../util/UndoManager'; import { PinProps, PresBox } from '../../trails'; import { DataVizBox } from '../DataVizBox'; import { createLineGenerator, drawLine, minMaxRange, scaleCreatorNumerical, xAxisCreator, xGrid, yAxisCreator, yGrid } from '../utils/D3Utils'; import './Chart.scss'; -import { EditableText, Size } from 'browndash-components'; -import { undoable } from '../../../../util/UndoManager'; export interface DataPoint { x: number; @@ -28,7 +26,7 @@ export interface LineChartProps { rootDoc: Doc; layoutDoc: Doc; axes: string[]; - pairs: { [key: string]: any }[]; + records: { [key: string]: any }[]; width: number; height: number; dataDoc: Doc; @@ -49,32 +47,35 @@ export class LineChart extends React.Component<LineChartProps> { @observable _currSelected: SelectedDataPoint | undefined = undefined; // TODO: nda - some sort of mapping that keeps track of the annotated points so we can easily remove when annotations list updates + @computed get _tableDataIds() { + return !this.parentViz ? this.props.records.map((rec, i) => i) : NumListCast(this.parentViz.dataViz_selectedRows); + } + // returns all the data records that will be rendered by only returning those records that have been selected by the parent visualization (or all records if there is no parent) + @computed get _tableData() { + return !this.parentViz ? this.props.records : this._tableDataIds.map(rowId => this.props.records[rowId]); + } @computed get _lineChartData() { - var guids = StrListCast(this.props.layoutDoc.dataViz_rowGuids); + var guids = StrListCast(this.props.layoutDoc.dataViz_rowIds); if (this.props.axes.length <= 1) return []; - return this.props.pairs - ?.filter(pair => (!this.incomingLinks.length ? true : this.incomingLinks[0]!.dataViz_selectedRows && StrListCast(this.incomingLinks[0].dataViz_selectedRows).includes(guids[this.props.pairs.indexOf(pair)]))) - .map(pair => ({ x: Number(pair[this.props.axes[0]]), y: Number(pair[this.props.axes[1]]) })) - .sort((a, b) => (a.x < b.x ? -1 : 1)); + return this._tableData.map(record => ({ x: Number(record[this.props.axes[0]]), y: Number(record[this.props.axes[1]]) })).sort((a, b) => (a.x < b.x ? -1 : 1)); } @computed get graphTitle() { return this.props.axes[1] + ' vs. ' + this.props.axes[0] + ' Line Chart'; } - @computed get incomingLinks() { - return LinkManager.Instance.getAllRelatedLinks(this.props.rootDoc) // out of all links - .filter(link => { - return link.link_anchor_1 == this.props.rootDoc.draggedFrom; - }) // get links where this chart doc is the target of the link - .map(link => DocCast(link.link_anchor_1)); // then return the source of the link + @computed get parentViz() { + return DocCast(this.props.rootDoc.dataViz_parentViz); + // return LinkManager.Instance.getAllRelatedLinks(this.props.rootDoc) // out of all links + // .filter(link => { + // return link.link_anchor_1 == this.props.rootDoc.dataViz_parentViz; + // }) // get links where this chart doc is the target of the link + // .map(link => DocCast(link.link_anchor_1)); // then return the source of the link } - @computed get incomingSelected() { + @computed get incomingHighlited() { // return selected x and y axes // otherwise, use the selection of whatever is linked to us - return this.incomingLinks // all links that are pointing to this node - .map(anchor => DocumentManager.Instance.getFirstDocumentView(anchor)?.ComponentView as DataVizBox) // get their data viz boxes - .filter(dvb => dvb) - .map(dvb => dvb.pairs?.filter(pair => pair['select' + dvb.rootDoc[Id]])) // get all the datapoints they have selected field set by incoming anchor - .lastElement(); + const incomingVizBox = DocumentManager.Instance.getFirstDocumentView(this.parentViz)?.ComponentView as DataVizBox; + const highlitedRowIds = NumListCast(incomingVizBox?.rootDoc?.dataViz_highlitedRows); + return this._tableData.filter((record, i) => highlitedRowIds.includes(this._tableDataIds[i])); // get all the datapoints they have selected field set by incoming anchor } @computed get rangeVals(): { xMin?: number; xMax?: number; yMin?: number; yMax?: number } { return minMaxRange([this._lineChartData]); @@ -88,10 +89,6 @@ export class LineChart extends React.Component<LineChartProps> { ({ dataSet, w, h }) => { if (dataSet) { this.drawChart([dataSet], this.rangeVals, w, h); - // redraw annotations when the chart data has changed, or the local or inherited selection has changed - this.clearAnnotations(); - this._currSelected && this.drawAnnotations(Number(this._currSelected.x), Number(this._currSelected.y), true); - this.incomingSelected?.forEach((pair: any) => this.drawAnnotations(Number(pair[this.props.axes[0]]), Number(pair[this.props.axes[1]]))); } }, { fireImmediately: true } @@ -103,7 +100,7 @@ export class LineChart extends React.Component<LineChartProps> { // could be blue colored to make it look like anchor // this.drawAnnotations() // loop through annotations and draw them - annotations.forEach(a => this.drawAnnotations(Number(a.x), Number(a.y))); + // annotations.forEach(a => this.drawAnnotations(Number(a.x), Number(a.y))); // this.drawAnnotations(annotations.x, annotations.y); }, { fireImmediately: true } @@ -111,13 +108,13 @@ export class LineChart extends React.Component<LineChartProps> { this._disposers.highlights = reaction( () => ({ selected: this._currSelected, - incomingSelected: this.incomingSelected, + incomingHighlited: this.incomingHighlited, }), - ({ selected, incomingSelected }) => { + ({ selected, incomingHighlited }) => { // redraw annotations when the chart data has changed, or the local or inherited selection has changed this.clearAnnotations(); selected && this.drawAnnotations(Number(selected.x), Number(selected.y), true); - incomingSelected?.forEach((pair: any) => this.drawAnnotations(Number(pair[this.props.axes[0]]), Number(pair[this.props.axes[1]]))); + incomingHighlited?.forEach((record: any) => this.drawAnnotations(Number(record[this.props.axes[0]]), Number(record[this.props.axes[1]]))); }, { fireImmediately: true } ); @@ -153,10 +150,6 @@ export class LineChart extends React.Component<LineChartProps> { } }; - removeAnnotations(dataX: number, dataY: number) { - // loop through and remove any annotations that no longer exist - } - @action restoreView = (data: Doc) => { const coords = Cast(data.config_dataVizSelection, listSpec('number'), null); @@ -193,7 +186,7 @@ export class LineChart extends React.Component<LineChartProps> { @computed get defaultGraphTitle() { var ax0 = this.props.axes[0]; var ax1 = this.props.axes.length > 1 ? this.props.axes[1] : undefined; - if (this.props.axes.length < 2 || !/\d/.test(this.props.pairs[0][ax0]) || !ax1) { + if (this.props.axes.length < 2 || !/\d/.test(this.props.records[0][ax0]) || !ax1) { return ax0 + ' Line Chart'; } else return ax1 + ' by ' + ax0 + ' Line Chart'; } @@ -217,7 +210,7 @@ export class LineChart extends React.Component<LineChartProps> { // TODO: nda - get rid of svg element in the list? if (this._currSelected && this._currSelected.x == x && this._currSelected.y == y) this._currSelected = undefined; else this._currSelected = x !== undefined && y !== undefined ? { x, y } : undefined; - this.props.pairs.forEach(pair => pair[this.props.axes[0]] === x && pair[this.props.axes[1]] === y && (pair.selected = true)); + this.props.records.forEach(record => record[this.props.axes[0]] === x && record[this.props.axes[1]] === y && (record.selected = true)); } drawDataPoints(data: DataPoint[], idx: number, xScale: d3.ScaleLinear<number, number, never>, yScale: d3.ScaleLinear<number, number, never>) { @@ -352,16 +345,15 @@ export class LineChart extends React.Component<LineChartProps> { } render() { - this.componentDidMount(); var titleAccessor: any = ''; - if (this.props.axes.length == 2) titleAccessor = 'dataViz_title_lineChart_' + this.props.axes[0] + '-' + this.props.axes[1]; - else if (this.props.axes.length > 0) titleAccessor = 'dataViz_title_lineChart_' + this.props.axes[0]; + if (this.props.axes.length == 2) titleAccessor = 'dataViz_lineChart_title' + this.props.axes[0] + '-' + this.props.axes[1]; + else if (this.props.axes.length > 0) titleAccessor = 'dataViz_lineChart_title' + this.props.axes[0]; if (!this.props.layoutDoc[titleAccessor]) this.props.layoutDoc[titleAccessor] = this.defaultGraphTitle; const selectedPt = this._currSelected ? `{ ${this.props.axes[0]}: ${this._currSelected.x} ${this.props.axes[1]}: ${this._currSelected.y} }` : 'none'; - if (this._lineChartData.length>0 || (!this.incomingLinks || this.incomingLinks.length==0)){ - return this.props.axes.length>=2 && /\d/.test(this.props.pairs[0][this.props.axes[0]]) && /\d/.test(this.props.pairs[0][this.props.axes[1]]) ? ( - <div className="chart-container" > - <div className="graph-title"> + if (this._lineChartData.length > 0 || !this.parentViz || this.parentViz.length == 0) { + return this.props.axes.length >= 2 && /\d/.test(this.props.records[0][this.props.axes[0]]) && /\d/.test(this.props.records[0][this.props.axes[1]]) ? ( + <div className="chart-container"> + <div className="graph-title"> <EditableText val={StrCast(this.props.layoutDoc[titleAccessor])} setVal={undoable( diff --git a/src/client/views/nodes/DataVizBox/components/PieChart.tsx b/src/client/views/nodes/DataVizBox/components/PieChart.tsx index 213baa8a4..561f39141 100644 --- a/src/client/views/nodes/DataVizBox/components/PieChart.tsx +++ b/src/client/views/nodes/DataVizBox/components/PieChart.tsx @@ -1,24 +1,24 @@ +import { ColorPicker, EditableText, Size, Type } from 'browndash-components'; +import * as d3 from 'd3'; +import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; -import { Doc, StrListCast } from '../../../../../fields/Doc'; import * as React from 'react'; -import * as d3 from 'd3'; -import { IReactionDisposer, action, computed, observable, reaction } from 'mobx'; -import { LinkManager } from '../../../../util/LinkManager'; -import { Cast, DocCast, StrCast } from '../../../../../fields/Types'; -import { PinProps, PresBox } from '../../trails'; -import { Docs } from '../../../../documents/Documents'; -import { List } from '../../../../../fields/List'; -import './Chart.scss'; -import { ColorPicker, EditableText, Size, Type } from 'browndash-components'; import { FaFillDrip } from 'react-icons/fa'; +import { Doc, NumListCast, StrListCast } from '../../../../../fields/Doc'; +import { List } from '../../../../../fields/List'; import { listSpec } from '../../../../../fields/Schema'; +import { Cast, DocCast, StrCast } from '../../../../../fields/Types'; +import { Docs } from '../../../../documents/Documents'; import { undoable } from '../../../../util/UndoManager'; +import { PinProps, PresBox } from '../../trails'; +import './Chart.scss'; +import { Checkbox } from '@material-ui/core'; export interface PieChartProps { rootDoc: Doc; layoutDoc: Doc; axes: string[]; - pairs: { [key: string]: any }[]; + records: { [key: string]: any }[]; width: number; height: number; dataDoc: Doc; @@ -36,47 +36,50 @@ export class PieChart extends React.Component<PieChartProps> { private _disposers: { [key: string]: IReactionDisposer } = {}; private _piechartRef: React.RefObject<HTMLDivElement> = React.createRef(); private _piechartSvg: d3.Selection<SVGGElement, unknown, null, undefined> | undefined; - private byCategory: boolean = true; // whether the data is organized by category or by specified number percentages/ratios - @observable _currSelected: any | undefined = undefined; // Object of selected slice private curSliceSelected: any = undefined; // d3 data of selected slice private selectedData: any = undefined; // Selection of selected slice private hoverOverData: any = undefined; // Selection of slice being hovered over + @observable _currSelected: any | undefined = undefined; // Object of selected slice + + @computed get _tableDataIds() { + return !this.parentViz ? this.props.records.map((rec, i) => i) : NumListCast(this.parentViz.dataViz_selectedRows); + } + // returns all the data records that will be rendered by only returning those records that have been selected by the parent visualization (or all records if there is no parent) + @computed get _tableData() { + return !this.parentViz ? this.props.records : this._tableDataIds.map(rowId => this.props.records[rowId]); + } + // organized by specified number percentages/ratios if one column is selected and it contains numbers + // otherwise, assume data is organized by categories + @computed get byCategory() { + return !/\d/.test(this.props.records[0][this.props.axes[0]]) || this.props.layoutDoc.dataViz_pie_asHistogram; + } // filters all data to just display selected data if brushed (created from an incoming link) - @computed get _piechartData() { - var guids = StrListCast(this.props.layoutDoc.dataViz_rowGuids); + @computed get _pieChartData() { if (this.props.axes.length < 1) return []; + + const ax0 = this.props.axes[0]; if (this.props.axes.length < 2) { - var ax0 = this.props.axes[0]; - if (/\d/.test(this.props.pairs[0][ax0])) { - this.byCategory = false; - } - return this.props.pairs - ?.filter(pair => (!this.incomingLinks.length ? true : this.incomingLinks[0]!.dataViz_selectedRows && StrListCast(this.incomingLinks[0].dataViz_selectedRows).includes(guids[this.props.pairs.indexOf(pair)]))) - .map(pair => ({ [ax0]: pair[this.props.axes[0]] })); - } - var ax0 = this.props.axes[0]; - var ax1 = this.props.axes[1]; - if (/\d/.test(this.props.pairs[0][ax0])) { - this.byCategory = false; + return this._tableData.map(record => ({ [ax0]: record[this.props.axes[0]] })); } - return this.props.pairs - ?.filter(pair => (!this.incomingLinks.length ? true : this.incomingLinks[0]!.dataViz_selectedRows && StrListCast(this.incomingLinks[0].dataViz_selectedRows).includes(guids[this.props.pairs.indexOf(pair)]))) - .map(pair => ({ [ax0]: pair[this.props.axes[0]], [ax1]: pair[this.props.axes[1]] })); + const ax1 = this.props.axes[1]; + return this._tableData.map(record => ({ [ax0]: record[this.props.axes[0]], [ax1]: record[this.props.axes[1]] })); } @computed get defaultGraphTitle() { var ax0 = this.props.axes[0]; var ax1 = this.props.axes.length > 1 ? this.props.axes[1] : undefined; - if (this.props.axes.length < 2 || !/\d/.test(this.props.pairs[0][ax0]) || !ax1) { + if (this.props.axes.length < 2 || !/\d/.test(this.props.records[0][ax0]) || !ax1) { return ax0 + ' Pie Chart'; - } else return ax1 + ' by ' + ax0 + ' Pie Chart'; + } + return ax1 + ' by ' + ax0 + ' Pie Chart'; } - @computed get incomingLinks() { - return LinkManager.Instance.getAllRelatedLinks(this.props.rootDoc) // out of all links - .filter(link => link.link_anchor_1 == this.props.rootDoc.draggedFrom) // get links where this chart doc is the target of the link - .map(link => DocCast(link.link_anchor_1)); // then return the source of the link + @computed get parentViz() { + return DocCast(this.props.rootDoc.dataViz_parentViz); + // return LinkManager.Instance.getAllRelatedLinks(this.props.rootDoc) // out of all links + // .filter(link => link.link_anchor_1 == this.props.rootDoc.dataViz_parentViz) // get links where this chart doc is the target of the link + // .map(link => DocCast(link.link_anchor_1)); // then return the source of the link } componentWillUnmount() { @@ -84,7 +87,7 @@ export class PieChart extends React.Component<PieChartProps> { } componentDidMount = () => { this._disposers.chartData = reaction( - () => ({ dataSet: this._piechartData, w: this.width, h: this.height }), + () => ({ dataSet: this._pieChartData, w: this.width, h: this.height }), ({ dataSet, w, h }) => { if (dataSet!.length > 0) { this.drawChart(dataSet, w, h); @@ -116,21 +119,15 @@ export class PieChart extends React.Component<PieChartProps> { // cleans data by converting numerical data to numbers and taking out empty cells data = (dataSet: any) => { - var validData = dataSet.filter((d: { [x: string]: unknown }) => { - var valid = true; - Object.keys(dataSet[0]).map(key => { - if (!d[key] || Number.isNaN(d[key])) valid = false; - }); - return valid; - }); - var field = dataSet[0] ? Object.keys(dataSet[0])[0] : undefined; - const data = validData.map((d: { [x: string]: any }) => { - if (!this.byCategory) { - return +d[field!].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, ''); - } - return d[field!]; - }); - return data; + const validData = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || Number.isNaN(d[key]))); + const field = dataSet[0] ? Object.keys(dataSet[0])[0] : undefined; + return !field + ? undefined + : validData.map((d: { [x: string]: any }) => + this.byCategory + ? d[field] // + : +d[field].replace(/\$/g, '').replace(/\%/g, '').replace(/\#/g, '').replace(/\</g, '') + ); }; // outlines the slice selected / hovered over @@ -162,7 +159,7 @@ export class PieChart extends React.Component<PieChartProps> { } if (lineCrossCount % 2 != 0) { // inside the slice of it crosses an odd number of edges - var showSelected = this.byCategory ? pieDataSet[index] : this._piechartData[index]; + var showSelected = this.byCategory ? pieDataSet[index] : this._pieChartData[index]; if (changeSelectedVariables) { // for when a bar is selected - not just hovered over sameAsCurrent = this._currSelected @@ -192,13 +189,7 @@ export class PieChart extends React.Component<PieChartProps> { // converts data into Objects var data = this.data(dataSet); - var pieDataSet = dataSet.filter((d: { [x: string]: unknown }) => { - var valid = true; - Object.keys(dataSet[0]).map(key => { - if (!d[key] || Number.isNaN(d[key])) valid = false; - }); - return valid; - }); + var pieDataSet = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || Number.isNaN(d[key]))); if (this.byCategory) { let uniqueCategories = [...new Set(data)]; var pieStringDataSet: { frequency: number }[] = []; @@ -232,7 +223,7 @@ export class PieChart extends React.Component<PieChartProps> { // click/hover const onPointClick = action((e: any) => this.highlightSelectedSlice(true, svg, arc, radius, d3.pointer(e), pieDataSet)); const onHover = action((e: any) => { - const selected = this.highlightSelectedSlice(false, svg, arc, radius, d3.pointer(e), pieDataSet); + this.highlightSelectedSlice(false, svg, arc, radius, d3.pointer(e), pieDataSet); updateHighlights(); }); const mouseOut = action((e: any) => { @@ -252,16 +243,21 @@ export class PieChart extends React.Component<PieChartProps> { // drawing the slices var selected = this.selectedData; var arcs = g.selectAll('arc').data(pie(data)).enter().append('g'); + const possibleDataPointVals: { [x: string]: any }[] = []; + pieDataSet.forEach((each: { [x: string]: any | { valueOf(): number } }) => { + var dataPointVal: { [x: string]: any } = {}; + dataPointVal[percentField] = each[percentField]; + if (descriptionField) dataPointVal[descriptionField] = each[descriptionField]; + try { + dataPointVal[percentField] = Number(dataPointVal[percentField].replace(/\$/g, '').replace(/\%/g, '').replace(/\#/g, '').replace(/\</g, '')); + } catch (error) {} + possibleDataPointVals.push(dataPointVal); + }); + const sliceColors = StrListCast(this.props.layoutDoc.dataViz_pie_sliceColors).map(each => each.split('::')); arcs.append('path') .attr('fill', (d, i) => { - var possibleDataPoints = pieDataSet.filter((each: { [x: string]: any | { valueOf(): number } }) => { - try { - return Number(each[percentField].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '')) == Number(d.data); - } catch (error) { - return each[percentField] == d.data; - } - }); var dataPoint; + const possibleDataPoints = possibleDataPointVals.filter((pval: any) => pval[percentField] === Number(d.data)); if (possibleDataPoints.length == 1) dataPoint = possibleDataPoints[0]; else { dataPoint = possibleDataPoints[trackDuplicates[d.data.toString()]]; @@ -269,11 +265,9 @@ export class PieChart extends React.Component<PieChartProps> { } var sliceColor; if (dataPoint) { - var accessByName = dataPoint[this.props.axes[0]].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, ''); - var sliceColors = StrListCast(this.props.layoutDoc.pieSliceColors).map(each => each.split('::')); - sliceColors.map(each => { - if (each[0] == StrCast(accessByName)) sliceColor = each[1]; - }); + const sliceTitle = dataPoint[this.props.axes[0]]; + const accessByName = StrCast(sliceTitle) ? StrCast(sliceTitle).replace(/\$/g, '').replace(/\%/g, '').replace(/\#/g, '').replace(/\</g, '') : sliceTitle; + sliceColors.forEach(each => each[0] == accessByName && (sliceColor = each[1])); } return sliceColor ? StrCast(sliceColor) : d3.schemeSet3[i] ? d3.schemeSet3[i] : d3.schemeSet3[i % d3.schemeSet3.length]; }) @@ -295,53 +289,55 @@ export class PieChart extends React.Component<PieChartProps> { // adding labels trackDuplicates = {}; data.forEach((eachData: any) => (!trackDuplicates[eachData] ? (trackDuplicates[eachData] = 0) : null)); - arcs.append('text') - .attr('transform', function (d) { - var centroid = arc.centroid(d as unknown as d3.DefaultArcObject); - var heightOffset = (centroid[1] / radius) * Math.abs(centroid[1]); - return 'translate(' + (centroid[0] + centroid[0] / (radius * 0.02)) + ',' + (centroid[1] + heightOffset) + ')'; - }) - .attr('text-anchor', 'middle') - .text(function (d) { - var possibleDataPoints = pieDataSet.filter((each: { [x: string]: any | { valueOf(): number } }) => { - try { - return Number(each[percentField].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '')) == Number(d.data); - } catch (error) { - return each[percentField] == d.data; + arcs.size() < 100 && + arcs + .append('text') + .attr('transform', function (d) { + var centroid = arc.centroid(d as unknown as d3.DefaultArcObject); + var heightOffset = (centroid[1] / radius) * Math.abs(centroid[1]); + return 'translate(' + (centroid[0] + centroid[0] / (radius * 0.02)) + ',' + (centroid[1] + heightOffset) + ')'; + }) + .attr('text-anchor', 'middle') + .text(function (d) { + var dataPoint; + const possibleDataPoints = possibleDataPointVals.filter((pval: any) => pval[percentField] === Number(d.data)); + if (possibleDataPoints.length == 1) dataPoint = pieDataSet[possibleDataPointVals.indexOf(possibleDataPoints[0])]; + else { + dataPoint = pieDataSet[possibleDataPointVals.indexOf(possibleDataPoints[trackDuplicates[d.data.toString()]])]; + trackDuplicates[d.data.toString()] = trackDuplicates[d.data.toString()] + 1; } + return dataPoint ? dataPoint[percentField]! + (!descriptionField ? '' : ' - ' + dataPoint[descriptionField])! : ''; }); - var dataPoint; - if (possibleDataPoints.length == 1) dataPoint = possibleDataPoints[0]; - else { - dataPoint = possibleDataPoints[trackDuplicates[d.data.toString()]]; - trackDuplicates[d.data.toString()] = trackDuplicates[d.data.toString()] + 1; - } - return dataPoint ? dataPoint[percentField]! + (!descriptionField ? '' : ' - ' + dataPoint[descriptionField])! : ''; - }); }; @action changeSelectedColor = (color: string) => { this.curSliceSelected.attr('fill', color); - var sliceName = this._currSelected[this.props.axes[0]].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, ''); + const sliceTitle = this._currSelected[this.props.axes[0]]; + const sliceName = StrCast(sliceTitle) ? StrCast(sliceTitle).replace(/\$/g, '').replace(/\%/g, '').replace(/\#/g, '').replace(/\</g, '') : sliceTitle; - const sliceColors = Cast(this.props.layoutDoc.pieSliceColors, listSpec('string'), null); + const sliceColors = Cast(this.props.layoutDoc.dataViz_pie_sliceColors, listSpec('string'), null); sliceColors.map(each => { if (each.split('::')[0] == sliceName) sliceColors.splice(sliceColors.indexOf(each), 1); }); sliceColors.push(StrCast(sliceName + '::' + color)); }; + @action changeHistogramCheckBox = () => { + this.props.layoutDoc.dataViz_pie_asHistogram = !this.props.layoutDoc.dataViz_pie_asHistogram; + this.drawChart(this._pieChartData, this.width, this.height); + }; + render() { - this.componentDidMount(); var titleAccessor: any = ''; - if (this.props.axes.length == 2) titleAccessor = 'dataViz_title_pieChart_' + this.props.axes[0] + '-' + this.props.axes[1]; - else if (this.props.axes.length > 0) titleAccessor = 'dataViz_title_pieChart_' + this.props.axes[0]; + if (this.props.axes.length == 2) titleAccessor = 'dataViz_pie_title' + this.props.axes[0] + '-' + this.props.axes[1]; + else if (this.props.axes.length > 0) titleAccessor = 'dataViz_pie_title' + this.props.axes[0]; if (!this.props.layoutDoc[titleAccessor]) this.props.layoutDoc[titleAccessor] = this.defaultGraphTitle; - if (!this.props.layoutDoc.pieSliceColors) this.props.layoutDoc.pieSliceColors = new List<string>(); + if (!this.props.layoutDoc.dataViz_pie_sliceColors) this.props.layoutDoc.dataViz_pie_sliceColors = new List<string>(); var selected: string; var curSelectedSliceName = ''; if (this._currSelected) { - curSelectedSliceName = StrCast(this._currSelected![this.props.axes[0]].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '')); + const sliceTitle = this._currSelected[this.props.axes[0]]; + curSelectedSliceName = StrCast(sliceTitle) ? StrCast(sliceTitle).replace(/\$/g, '').replace(/\%/g, '').replace(/\#/g, '').replace(/\</g, '') : sliceTitle; selected = '{ '; Object.keys(this._currSelected).map(key => { key != '' ? (selected += key + ': ' + this._currSelected[key] + ', ') : ''; @@ -350,12 +346,12 @@ export class PieChart extends React.Component<PieChartProps> { selected += ' }'; } else selected = 'none'; var selectedSliceColor; - var sliceColors = StrListCast(this.props.layoutDoc.pieSliceColors).map(each => each.split('::')); - sliceColors.map(each => { + var sliceColors = StrListCast(this.props.layoutDoc.dataViz_pie_sliceColors).map(each => each.split('::')); + sliceColors.forEach(each => { if (each[0] == curSelectedSliceName!) selectedSliceColor = each[1]; }); - if (this._piechartData.length>0 || (!this.incomingLinks || this.incomingLinks.length==0)){ + if (this._pieChartData.length > 0 || !this.parentViz) { return this.props.axes.length >= 1 ? ( <div className="chart-container"> <div className="graph-title"> @@ -370,6 +366,12 @@ export class PieChart extends React.Component<PieChartProps> { fillWidth /> </div> + {this.props.axes.length === 1 && /\d/.test(this.props.records[0][this.props.axes[0]]) ? ( + <div className={'asHistogram-checkBox'} style={{ width: this.props.width }}> + <Checkbox color="primary" onChange={this.changeHistogramCheckBox} checked={this.props.layoutDoc.dataViz_pie_asHistogram as boolean} /> + Organize data as histogram + </div> + ) : null} <div ref={this._piechartRef} /> {selected != 'none' ? ( <div className={'selected-data'}> diff --git a/src/client/views/nodes/DataVizBox/components/TableBox.tsx b/src/client/views/nodes/DataVizBox/components/TableBox.tsx index 70483ac6f..5045fde3a 100644 --- a/src/client/views/nodes/DataVizBox/components/TableBox.tsx +++ b/src/client/views/nodes/DataVizBox/components/TableBox.tsx @@ -1,21 +1,21 @@ -import { action, computed } from 'mobx'; +import { Button, Type } from 'browndash-components'; +import { action, computed, IReactionDisposer, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { Doc, Field, StrListCast } from '../../../../../fields/Doc'; +import { Doc, Field, NumListCast } from '../../../../../fields/Doc'; import { List } from '../../../../../fields/List'; +import { listSpec } from '../../../../../fields/Schema'; +import { Cast, DocCast } from '../../../../../fields/Types'; import { emptyFunction, setupMoveUpEvents, Utils } from '../../../../../Utils'; import { DragManager } from '../../../../util/DragManager'; import { DocumentView } from '../../DocumentView'; import { DataVizView } from '../DataVizBox'; -import { LinkManager } from '../../../../util/LinkManager'; -import { Cast, DocCast } from '../../../../../fields/Types'; import './Chart.scss'; -import { listSpec } from '../../../../../fields/Schema'; interface TableBoxProps { rootDoc: Doc; layoutDoc: Doc; - pairs: { [key: string]: any }[]; + records: { [key: string]: any }[]; selectAxes: (axes: string[]) => void; axes: string[]; width: number; @@ -31,84 +31,110 @@ interface TableBoxProps { @observer export class TableBox extends React.Component<TableBoxProps> { - // filters all data to just display selected data if brushed (created from an incoming link) + _inputChangedDisposer?: IReactionDisposer; + componentDidMount() { + // if the tableData changes (ie., when records are selected by the parent (input) visulization), + // then we need to remove any selected rows that are no longer part of the visualized dataset. + this._inputChangedDisposer = reaction(() => this._tableData.slice(), this.filterSelectedRowsDown, { fireImmediately: true }); + } + componentWillUnmount() { + this._inputChangedDisposer?.(); + } + @computed get _tableDataIds() { + return !this.parentViz ? this.props.records.map((rec, i) => i) : NumListCast(this.parentViz.dataViz_selectedRows); + } + // returns all the data records that will be rendered by only returning those records that have been selected by the parent visualization (or all records if there is no parent) @computed get _tableData() { - if (this.incomingLinks.length! <= 0) return this.props.pairs; - var guids = StrListCast(this.props.layoutDoc.dataViz_rowGuids); - return this.props.pairs?.filter(pair => this.incomingLinks[0]!.dataViz_selectedRows && StrListCast(this.incomingLinks[0].dataViz_selectedRows).includes(guids[this.props.pairs.indexOf(pair)])); + return !this.parentViz ? this.props.records : this._tableDataIds.map(rowId => this.props.records[rowId]); } - @computed get incomingLinks() { - return LinkManager.Instance.getAllRelatedLinks(this.props.rootDoc) // out of all links - .filter(link => { - return link.link_anchor_1 == this.props.rootDoc.draggedFrom; - }) // get links where this chart doc is the target of the link - .map(link => DocCast(link.link_anchor_1)); // then return the source of the link + @computed get parentViz() { + return DocCast(this.props.rootDoc.dataViz_parentViz); + // return LinkManager.Instance.getAllRelatedLinks(this.props.rootDoc) // out of all links + // .filter(link => link.link_anchor_1 == this.props.rootDoc.dataViz_parentViz) // get links where this chart doc is the target of the link + // .map(link => DocCast(link.link_anchor_1)); // then return the source of the link } @computed get columns() { - if (!this.props.layoutDoc.dataViz_rowGuids) this.props.layoutDoc.dataViz_rowGuids = new List<string>(); - const guids = Cast(this.props.layoutDoc.dataViz_rowGuids, listSpec('string'), null); - if (guids.length == 0) this.props.pairs.map(row => guids.push(Utils.GenerateGuid())); return this._tableData.length ? Array.from(Object.keys(this._tableData[0])).filter(header => header != '' && header != undefined) : []; } - // updates the 'selected' field to no longer include rows that aren't in the table - filterSelectedRowsDown() { - if (!this.props.layoutDoc.dataViz_selectedRows) this.props.layoutDoc.dataViz_selectedRows = new List<string>(); - const selected = Cast(this.props.layoutDoc.dataViz_selectedRows, listSpec('string'), null); - const incomingSelected = this.incomingLinks.length ? StrListCast(this.incomingLinks[0].dataViz_selectedRows) : undefined; - if (incomingSelected) { - selected.map(guid => { - if (!incomingSelected.includes(guid)) selected.splice(selected.indexOf(guid), 1); - }); // filters through selected to remove guids that were removed in the incoming data - } - } + // updates the 'dataViz_selectedRows' and 'dataViz_highlightedRows' fields to no longer include rows that aren't in the table + filterSelectedRowsDown = () => { + const selected = NumListCast(this.props.layoutDoc.dataViz_selectedRows); + this.props.layoutDoc.dataViz_selectedRows = new List<number>(selected.filter(rowId => this._tableDataIds.includes(rowId))); // filters through selected to remove guids that were removed in the incoming data + const highlighted = NumListCast(this.props.layoutDoc.dataViz_highlitedRows); + this.props.layoutDoc.dataViz_highlitedRows = new List<number>(highlighted.filter(rowId => this._tableDataIds.includes(rowId))); // filters through highlighted to remove guids that were removed in the incoming data + }; render() { - this.filterSelectedRowsDown(); if (this._tableData.length > 0) { return ( - <div className="table-container" style={{ height: this.props.height }}> - <table className="table"> - <thead> - <tr className="table-row"> - {this.columns - .filter(col => !col.startsWith('select')) - .map(col => { - const header = React.createRef<HTMLElement>(); - return ( - <th - key={this.columns.indexOf(col)} - ref={header as any} - style={{ - color: this.props.axes.slice().reverse().lastElement() === col ? 'darkgreen' : this.props.axes.lastElement() === col ? 'darkred' : undefined, - background: this.props.axes.slice().reverse().lastElement() === col ? '#E3fbdb' : this.props.axes.lastElement() === col ? '#Fbdbdb' : undefined, - fontWeight: 'bolder', - border: '3px solid black', - }} - onPointerDown={e => { - const downX = e.clientX; - const downY = e.clientY; - setupMoveUpEvents( - {}, - e, - e => { - // dragging off a column to create a brushed DataVizBox - const sourceAnchorCreator = () => this.props.docView?.()!.rootDoc!; - const targetCreator = (annotationOn: Doc | undefined) => { - const embedding = Doc.MakeEmbedding(this.props.docView?.()!.rootDoc!); - embedding._dataViz = DataVizView.TABLE; - embedding._dataViz_axes = new List<string>([col, col]); - embedding._draggedFrom = this.props.docView?.()!.rootDoc!; - embedding.annotationOn = annotationOn; //this.props.docView?.()!.rootDoc!; - embedding.histogramBarColors = Field.Copy(this.props.layoutDoc.histogramBarColors); - embedding.defaultHistogramColor = this.props.layoutDoc.defaultHistogramColor; - embedding.pieSliceColors = Field.Copy(this.props.layoutDoc.pieSliceColors); - return embedding; - }; - if (this.props.docView?.() && !Utils.isClick(e.clientX, e.clientY, downX, downY, Date.now())) { - DragManager.StartAnchorAnnoDrag([header.current!], new DragManager.AnchorAnnoDragData(this.props.docView()!, sourceAnchorCreator, targetCreator), downX, downY, { + <div + className="tableBox" + tabIndex={0} + onKeyDown={e => { + if (this.props.layoutDoc && e.key === 'a' && (e.ctrlKey || e.metaKey)) { + e.stopPropagation(); + this.props.layoutDoc.dataViz_selectedRows = new List<number>(this._tableDataIds); + } + }}> + <div className="selectAll-buttons"> + <Button onClick={action(() => (this.props.layoutDoc.dataViz_selectedRows = new List<number>(this._tableDataIds)))} text="Select All" type={Type.SEC} color={'black'} /> + <Button onClick={action(() => (this.props.layoutDoc.dataViz_selectedRows = new List<number>()))} text="Deselect All" type={Type.SEC} color={'black'} /> + </div> + <div + className="table-container" + style={{ height: this.props.height }} + ref={r => + r?.addEventListener( + 'wheel', // if scrollTop is 0, then don't let wheel trigger scroll on any container (which it would since onScroll won't be triggered on this) + (e: WheelEvent) => { + if (!r.scrollTop && e.deltaY <= 0) e.preventDefault(); + e.stopPropagation(); + }, + { passive: false } + ) + }> + <table className="table"> + <thead> + <tr className="table-row"> + {this.columns.map(col => ( + <th + key={this.columns.indexOf(col)} + style={{ + color: this.props.axes.slice().reverse().lastElement() === col ? 'darkgreen' : this.props.axes.lastElement() === col ? 'darkred' : undefined, + background: this.props.axes.slice().reverse().lastElement() === col ? '#E3fbdb' : this.props.axes.lastElement() === col ? '#Fbdbdb' : undefined, + fontWeight: 'bolder', + border: '3px solid black', + }} + onPointerDown={e => { + const downX = e.clientX; + const downY = e.clientY; + setupMoveUpEvents( + {}, + e, + e => { + // dragging off a column to create a brushed DataVizBox + const sourceAnchorCreator = () => this.props.docView?.()!.rootDoc!; + const targetCreator = (annotationOn: Doc | undefined) => { + const embedding = Doc.MakeEmbedding(this.props.docView?.()!.rootDoc!); + embedding._dataViz = DataVizView.TABLE; + embedding._dataViz_axes = new List<string>([col, col]); + embedding._dataViz_parentViz = this.props.rootDoc; + embedding.annotationOn = annotationOn; //this.props.docView?.()!.rootDoc!; + embedding.histogramBarColors = Field.Copy(this.props.layoutDoc.histogramBarColors); + embedding.defaultHistogramColor = this.props.layoutDoc.defaultHistogramColor; + embedding.pieSliceColors = Field.Copy(this.props.layoutDoc.pieSliceColors); + return embedding; + }; + if (this.props.docView?.() && !Utils.isClick(e.clientX, e.clientY, downX, downY, Date.now())) { + DragManager.StartAnchorAnnoDrag( + e.target instanceof HTMLElement ? [e.target] : [], + new DragManager.AnchorAnnoDragData(this.props.docView()!, sourceAnchorCreator, targetCreator), + downX, + downY, + { dragComplete: e => { if (!e.aborted && e.annoDragData && e.annoDragData.linkSourceDoc && e.annoDragData.dropDocument && e.linkDocument) { e.linkDocument.link_displayLine = true; @@ -117,67 +143,69 @@ export class TableBox extends React.Component<TableBoxProps> { // e.annoDragData.linkSourceDoc.followLinkZoom = false; } }, - }); - return true; - } - return false; - }, - emptyFunction, - action(e => { - const newAxes = this.props.axes; - if (newAxes.includes(col)) { - newAxes.splice(newAxes.indexOf(col), 1); - } else if (newAxes.length > 1) { - newAxes[1] = col; - } else { - newAxes.push(col); - } - this.props.selectAxes(newAxes); - }) - ); - }}> - {col} - </th> - ); - })} - </tr> - </thead> - <tbody> - {this._tableData?.map((p, i) => { - var containsData = false; - var guid = StrListCast(this.props.layoutDoc.dataViz_rowGuids)![this.props.pairs.indexOf(p)]; - this.columns.map(col => { - if (p[col] != '' && p[col] != null && p[col] != undefined) containsData = true; - }); - if (containsData) { - return ( + } + ); + return true; + } + return false; + }, + emptyFunction, + action(e => { + const newAxes = this.props.axes; + if (newAxes.includes(col)) newAxes.splice(newAxes.indexOf(col), 1); + else if (newAxes.length > 1) newAxes[1] = col; + else newAxes.push(col); + this.props.selectAxes(newAxes); + }) + ); + }}> + {col} + </th> + ))} + </tr> + </thead> + <tbody> + {this._tableDataIds + ?.map(rowId => ({ record: this.props.records[rowId], rowId })) + .map(({ record, rowId }) => ( <tr - key={i} + key={rowId} className="table-row" onClick={action(e => { - // selecting a row - const selected = Cast(this.props.layoutDoc.dataViz_selectedRows, listSpec('string'), null); - if (selected.includes(guid)) selected.splice(selected.indexOf(guid), 1); - else { - selected.push(guid); + const highlited = Cast(this.props.layoutDoc.dataViz_highlitedRows, listSpec('number'), null); + const selected = Cast(this.props.layoutDoc.dataViz_selectedRows, listSpec('number'), null); + if (e.metaKey) { + // highlighting a row + if (highlited?.includes(rowId)) highlited.splice(highlited.indexOf(rowId), 1); + else highlited?.push(rowId); + if (!selected?.includes(rowId)) selected?.push(rowId); + } else { + // selecting a row + if (selected?.includes(rowId)) { + if (highlited?.includes(rowId)) highlited.splice(highlited.indexOf(rowId), 1); + selected.splice(selected.indexOf(rowId), 1); + } else selected?.push(rowId); } + e.stopPropagation(); })} - style={{ background: StrListCast(this.props.layoutDoc.dataViz_selectedRows).includes(guid) ? 'lightgrey' : '', width: '110%' }}> + style={{ + background: NumListCast(this.props.layoutDoc.dataViz_highlitedRows).includes(rowId) ? 'lightYellow' : NumListCast(this.props.layoutDoc.dataViz_selectedRows).includes(rowId) ? 'lightgrey' : '', + width: '110%', + }}> {this.columns.map(col => { // each cell - var colSelected = this.props.axes.length > 1 ? this.props.axes[0] == col || this.props.axes[1] == col : this.props.axes.length > 0 ? this.props.axes[0] == col : false; + const colSelected = this.props.axes.length > 1 ? this.props.axes[0] == col || this.props.axes[1] == col : this.props.axes.length > 0 ? this.props.axes[0] == col : false; return ( <td key={this.columns.indexOf(col)} style={{ border: colSelected ? '3px solid black' : '1px solid black', fontWeight: colSelected ? 'bolder' : 'normal' }}> - {p[col]} + {record[col]} </td> ); })} </tr> - ); - } - })} - </tbody> - </table> + ))} + </tbody> + </table> + </div> </div> ); } else diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index 2d8663c9c..8ac9d6804 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -47,6 +47,7 @@ import { VideoBox } from './VideoBox'; import { WebBox } from './WebBox'; import React = require('react'); import XRegExp = require('xregexp'); +import { MapPushpinBox } from './MapBox/MapPushpinBox'; const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this? @@ -271,6 +272,7 @@ export class DocumentContentsView extends React.Component< PhysicsSimulationBox, SchemaRowBox, ImportElementBox, + MapPushpinBox, }} bindings={bindings} jsx={layoutFrame} diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx index 7723a088d..4db0bf5fa 100644 --- a/src/client/views/nodes/DocumentLinksButton.tsx +++ b/src/client/views/nodes/DocumentLinksButton.tsx @@ -18,10 +18,6 @@ import React = require('react'); import _ = require('lodash'); import { PinProps } from './trails'; -const higflyout = require('@hig/flyout'); -export const { anchorPoints } = higflyout; -export const Flyout = higflyout.default; - interface DocumentLinksButtonProps { View: DocumentView; Bottom?: boolean; diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index 533a047b1..74eee49e0 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -28,6 +28,7 @@ import { FollowLinkScript } from '../../util/LinkFollower'; import { LinkManager } from '../../util/LinkManager'; import { ScriptingGlobals } from '../../util/ScriptingGlobals'; import { SelectionManager } from '../../util/SelectionManager'; +import { SettingsManager } from '../../util/SettingsManager'; import { SharingManager } from '../../util/SharingManager'; import { SnappingManager } from '../../util/SnappingManager'; import { Transform } from '../../util/Transform'; @@ -51,7 +52,6 @@ import { LinkAnchorBox } from './LinkAnchorBox'; import { PresEffect, PresEffectDirection } from './trails'; import { PinProps, PresBox } from './trails/PresBox'; import React = require('react'); -import { SettingsManager } from '../../util/SettingsManager'; const { Howl } = require('howler'); interface Window { @@ -428,7 +428,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps if (!this.Document.ignoreClick && this.pointerEvents !== 'none' && this.props.renderDepth >= 0 && Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, this._downTime)) { let stopPropagate = true; let preventDefault = true; - (this.rootDoc._raiseWhenDragged === undefined ? DragManager.GetRaiseWhenDragged() : this.rootDoc._raiseWhenDragged) && this.props.bringToFront(this.rootDoc); + !this.rootDoc._keepZWhenDragged && this.props.bringToFront(this.rootDoc); if (this._doubleTap) { const defaultDblclick = this.props.defaultDoubleClick?.() || this.Document.defaultDoubleClick; if (this.onDoubleClickHandler?.script) { @@ -704,7 +704,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps if (e && !(e.nativeEvent as any).dash) { const onDisplay = () => { - 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. + if (this.rootDoc.type !== DocumentType.MAP) 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(() => simulateMouseClick(document.elementFromPoint(e.clientX, e.clientY), e.clientX, e.clientY, e.screenX, e.screenY)); }; if (navigator.userAgent.includes('Macintosh')) { @@ -744,11 +744,11 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps zorderItems.push({ description: 'Bring to Front', event: () => SelectionManager.Views().forEach(dv => dv.props.bringToFront(dv.rootDoc, false)), icon: 'arrow-up' }); zorderItems.push({ description: 'Send to Back', event: () => SelectionManager.Views().forEach(dv => dv.props.bringToFront(dv.rootDoc, true)), icon: 'arrow-down' }); 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))), + description: !this.rootDoc._keepZDragged ? 'Keep ZIndex when dragged' : 'Allow ZIndex to change when dragged', + event: undoBatch(action(() => (this.rootDoc._keepZWhenDragged = !this.rootDoc._keepZWhenDragged))), icon: 'hand-point-up', }); - !zorders && cm.addItem({ description: 'Z Order...', addDivider: true, noexpand: true, subitems: zorderItems, icon: 'compass' }); + !zorders && cm.addItem({ description: 'Z Order...', addDivider: true, noexpand: true, subitems: zorderItems, icon: 'layer-group' }); } onClicks.push({ description: 'Enter Portal', event: this.makeIntoPortal, icon: 'window-restore' }); @@ -806,7 +806,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps constantItems.push({ description: 'Close', event: this.deleteClicked, icon: 'times' }); } } - constantItems.push({ description: 'Show Metadata', event: () => this.props.addDocTab(this.props.Document, (OpenWhere.addRight.toString() + 'KeyValue') as OpenWhere), icon: 'layer-group' }); + constantItems.push({ description: 'Show Metadata', event: () => this.props.addDocTab(this.props.Document, (OpenWhere.addRight.toString() + 'KeyValue') as OpenWhere), icon: 'table-columns' }); cm.addItem({ description: 'General...', noexpand: false, subitems: constantItems, icon: 'question' }); const help = cm.findByDescription('Help...'); @@ -1114,7 +1114,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps </div> ); const targetDoc = showTitle?.startsWith('_') ? this.layoutDoc : this.rootDoc; - const background = StrCast(SharingManager.Instance.users.find(u => u.user.email === this.dataDoc.author)?.sharingDoc.headingColor, StrCast(Doc.SharingDoc().headingColor, SettingsManager.Instance.userVariantColor)); + const background = StrCast(SharingManager.Instance.users.find(u => u.user.email === this.dataDoc.author)?.sharingDoc.headingColor, StrCast(Doc.SharingDoc().headingColor, SettingsManager.userVariantColor)); const sidebarWidthPercent = +StrCast(this.layoutDoc.layout_sidebarWidthPercent).replace('%', ''); const titleView = !showTitle ? null : ( <div @@ -1276,7 +1276,13 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps @observer export class DocumentView extends React.Component<DocumentViewProps> { public static ROOT_DIV = 'documentView-effectsWrapper'; + @observable public static Interacting = false; @observable public static LongPress = false; + @observable public static ExploreMode = false; + @observable public static LastPressedSidebarBtn: Opt<Doc>; // bcz: this is a hack to handle highlighting buttons in the leftpanel menu .. need to find a cleaner approach + @computed public static get exploreMode() { + return () => (DocumentView.ExploreMode ? ScriptField.MakeScript('CollectionBrowseClick(documentView, clientX, clientY)', { documentView: 'any', clientX: 'number', clientY: 'number' })! : undefined); + } @observable public docView: DocumentViewInternal | undefined | null; @observable public textHtmlOverlay: Opt<string>; @observable private _isHovering = false; diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx index 286b80426..eded4b652 100644 --- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx +++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx @@ -10,17 +10,18 @@ import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../ import { Utils } from '../../../../Utils'; import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; import { SelectionManager } from '../../../util/SelectionManager'; +import { SettingsManager } from '../../../util/SettingsManager'; import { undoable, UndoManager } from '../../../util/UndoManager'; import { ContextMenu } from '../../ContextMenu'; import { DocComponent } from '../../DocComponent'; import { EditableView } from '../../EditableView'; -import { Colors } from '../../global/globalEnums'; import { SelectedDocView } from '../../selectedDoc'; import { StyleProp } from '../../StyleProvider'; import { OpenWhere } from '../DocumentView'; import { FieldView, FieldViewProps } from '../FieldView'; import { RichTextMenu } from '../formattedText/RichTextMenu'; import './FontIconBox.scss'; +import TrailsIcon from './TrailsIcon'; export enum ButtonType { TextButton = 'textBtn', @@ -67,19 +68,6 @@ export class FontIconBox extends DocComponent<ButtonProps>() { } }; - static GetShowLabels() { - return BoolCast(Doc.UserDoc()._showLabel); - } - static SetShowLabels(show: boolean) { - Doc.UserDoc()._showLabel = show; - } - static GetRecognizeGestures() { - return BoolCast(Doc.UserDoc()._recognizeGestures); - } - static SetRecognizeGestures(show: boolean) { - Doc.UserDoc()._recognizeGestures = show; - } - // Determining UI Specs @computed get label() { return StrCast(this.rootDoc.icon_label, StrCast(this.rootDoc.title)); @@ -92,8 +80,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { else return null; } icon = StrCast(this.dataDoc[this.fieldKey ?? 'icon'] ?? this.dataDoc.icon, 'user') as any; - const trailsIcon = () => <img src={`/assets/${'presTrails.png'}`} style={{ width: 30, height: 30, filter: `invert(${color === Colors.DARK_GRAY ? '0%' : '100%'})` }} />; - return !icon ? null : icon === 'pres-trail' ? trailsIcon() : <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={icon} color={color} />; + return !icon ? null : icon === 'pres-trail' ? TrailsIcon(color) : <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={icon} color={color} />; }; @computed get dropdown() { return BoolCast(this.rootDoc.dropDownOpen); @@ -140,11 +127,11 @@ export class FontIconBox extends DocComponent<ButtonProps>() { const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color); // Script for checking the outcome of the toggle const checkResult = Number(Number(numScript().result ?? 0).toPrecision(NumCast(this.dataDoc.numPrecision, 3))); - const label = !FontIconBox.GetShowLabels() ? null : <div className="fontIconBox-label">{this.label}</div>; return ( <NumberDropdown color={color} + background={SettingsManager.userBackgroundColor} numberDropdownType={type} showPlusMinus={false} tooltip={this.label} @@ -168,14 +155,14 @@ export class FontIconBox extends DocComponent<ButtonProps>() { return ( <div className={`menuButton ${this.type} ${active}`} - style={{ color: color, backgroundColor: backgroundColor, borderBottomLeftRadius: this.dropdown ? 0 : undefined }} + style={{ color, backgroundColor, borderBottomLeftRadius: this.dropdown ? 0 : undefined }} onClick={action(() => { this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen; this.noTooltip = this.rootDoc.dropDownOpen; Doc.UnBrushAllDocs(); })}> {this.Icon(color)} - {!this.label || !FontIconBox.GetShowLabels() ? null : ( + {!this.label || !Doc.GetShowIconLabels() ? null : ( <div className="fontIconBox-label" style={{ color: color, backgroundColor: backgroundColor }}> {' '} {this.label}{' '} @@ -193,10 +180,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { * Dropdown list */ @computed get dropdownListButton() { - const active: string = StrCast(this.rootDoc.dropDownOpen); const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color); - const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor); - const script = ScriptCast(this.rootDoc.script); let noviceList: string[] = []; @@ -220,11 +204,21 @@ export class FontIconBox extends DocComponent<ButtonProps>() { text = Utils.cleanDocumentType(StrCast(selected.type) as DocumentType); icon = Doc.toIcon(selected); } - return <Popup icon={<FontAwesomeIcon size={'1x'} icon={icon} />} text={text} type={Type.TERT} color={color} popup={<SelectedDocView selectedDocs={selectedDocs} />} fillWidth />; + return ( + <Popup + icon={<FontAwesomeIcon size={'1x'} icon={icon} />} + text={text} + type={Type.TERT} + color={SettingsManager.userColor} + background={SettingsManager.userVariantColor} + popup={<SelectedDocView selectedDocs={selectedDocs} />} + fillWidth + /> + ); } } else { dropdown = false; - return <Button text={`None Selected`} type={Type.TERT} color={color} fillWidth inactive />; + return <Button text="None Selected" type={Type.TERT} color={SettingsManager.userColor} background={SettingsManager.userVariantColor} fillWidth inactive />; } noviceList = [CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Stacking, CollectionViewType.NoteTaking]; } else { @@ -249,16 +243,19 @@ export class FontIconBox extends DocComponent<ButtonProps>() { })); return ( - <Dropdown - selectedVal={text} - setSelectedVal={undoable(val => script.script.run({ this: this.layoutDoc, self: this.rootDoc, val }), `dropdown select ${this.label}`)} - color={color} - type={isViewDropdown ? Type.TERT : Type.PRIM} - dropdownType={DropdownType.SELECT} - items={list} - tooltip={this.label} - fillWidth - /> + <div style={{ color: SettingsManager.userColor, background: SettingsManager.userBackgroundColor }}> + <Dropdown + selectedVal={text} + setSelectedVal={undoable(val => script.script.run({ this: this.layoutDoc, self: this.rootDoc, val }), `dropdown select ${this.label}`)} + color={SettingsManager.userColor} + background={isViewDropdown ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor} + type={Type.TERT} + dropdownType={DropdownType.SELECT} + items={list} + tooltip={this.label} + fillWidth + /> + </div> ); } @@ -290,6 +287,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { selectedColor={curColor} type={Type.PRIM} color={color} + background={SettingsManager.userBackgroundColor} icon={this.Icon(color)!} tooltip={tooltip} label={this.label} @@ -310,6 +308,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { tooltip={`Toggle ${tooltip}`} type={Type.PRIM} color={color} + background={SettingsManager.userBackgroundColor} label={this.label} items={DocListCast(this.rootDoc.data).map(item => ({ icon: <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={StrCast(item.icon) as any} color={color} />, @@ -344,6 +343,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { toggleStatus={toggleStatus} text={buttonText} color={color} + background={SettingsManager.userBackgroundColor} icon={this.Icon(color)!} label={this.label} onPointerDown={() => script.script.run({ this: this.layoutDoc, self: this.rootDoc, value: !toggleStatus, _readOnly_: false })} @@ -400,8 +400,10 @@ export class FontIconBox extends DocComponent<ButtonProps>() { case ButtonType.ToggleButton: return this.toggleButton; case ButtonType.ClickButton: case ButtonType.ToolButton: return <IconButton {...btnProps} size={Size.LARGE} color={color} />; - case ButtonType.TextButton: return <Button {...btnProps} text={StrCast(this.rootDoc.buttonText)}/>; - case ButtonType.MenuButton: return <IconButton {...btnProps} color={color} size={Size.LARGE} tooltipPlacement='right' onPointerDown={scriptFunc} />; + case ButtonType.TextButton: return <Button {...btnProps} color={color} + background={SettingsManager.userBackgroundColor} text={StrCast(this.rootDoc.buttonText)}/>; + case ButtonType.MenuButton: return <IconButton {...btnProps} color={color} + background={SettingsManager.userBackgroundColor} size={Size.LARGE} tooltipPlacement='right' onPointerDown={scriptFunc} />; } return this.defaultButton; } diff --git a/src/client/views/nodes/FontIconBox/TrailsIcon.tsx b/src/client/views/nodes/FontIconBox/TrailsIcon.tsx new file mode 100644 index 000000000..09fd6e3ae --- /dev/null +++ b/src/client/views/nodes/FontIconBox/TrailsIcon.tsx @@ -0,0 +1,81 @@ +import * as React from 'react'; + +const TrailsIcon = (fill: string) => ( + <svg version="1.0" xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 1080.000000 1080.000000" preserveAspectRatio="xMidYMid meet"> + <g transform="translate(0.000000,1080.000000) scale(0.100000,-0.100000)" fill={fill} stroke="none"> + <path + d="M665 9253 c-74 -10 -157 -38 -240 -81 -74 -37 -107 -63 -186 -141 +-104 -104 -156 -191 -201 -334 l-23 -72 0 -3215 c0 -3072 1 -3218 18 -3280 10 +-36 39 -108 64 -160 40 -82 59 -107 142 -190 81 -81 111 -103 191 -143 52 -26 +122 -55 155 -65 57 -16 322 -17 4775 -20 3250 -2 4736 1 4784 8 256 39 486 +220 588 462 63 148 59 -96 56 3413 -3 3049 -4 3203 -21 3260 -78 260 -285 467 +-542 542 -57 17 -308 18 -4795 19 -2604 1 -4748 -1 -4765 -3z m9187 -787 c65 +-19 114 -60 143 -120 l25 -51 0 -2898 c0 -2582 -2 -2901 -15 -2934 -24 -57 +-62 -101 -108 -126 l-42 -22 -4435 -3 c-3954 -2 -4440 0 -4481 13 -26 9 -63 +33 -87 56 -79 79 -72 -205 -72 3012 0 2156 3 2889 12 2918 20 70 91 136 168 +160 14 4 2010 8 4436 8 3710 1 4418 -1 4456 -13z" + /> + <path + d="M7692 7839 c-46 -14 -109 -80 -122 -128 -7 -27 -9 -472 -8 -1443 l3 +-1403 24 -38 c13 -21 42 -50 64 -65 l41 -27 816 0 816 0 41 27 c22 15 51 44 +64 65 l24 38 0 1425 0 1425 -24 38 c-13 21 -42 50 -64 65 l-41 27 -800 2 +c-488 1 -814 -2 -834 -8z" + /> + <path + d="M1982 7699 c-46 -14 -109 -80 -122 -128 -7 -27 -10 -308 -8 -893 l3 +-853 24 -38 c13 -21 42 -50 64 -65 l41 -27 1386 0 1386 0 41 27 c22 15 51 44 +64 65 l24 38 0 876 0 875 -27 41 c-15 22 -44 51 -65 64 l-38 24 -1370 2 c-847 +1 -1383 -2 -1403 -8z" + /> + <path + d="M6413 7093 c-13 -2 -23 -9 -23 -15 0 -24 21 -307 26 -343 l5 -40 182 +-1 c200 -1 307 -15 484 -65 57 -16 107 -29 112 -29 5 0 36 75 69 168 33 92 63 +175 67 184 6 14 -10 22 -92 48 -126 39 -308 76 -447 89 -106 11 -337 13 -383 +4z" + /> + <path + d="M5840 7033 c-63 -8 -238 -29 -388 -47 -150 -18 -274 -35 -276 -37 -2 +-2 8 -89 23 -194 22 -163 29 -190 44 -193 10 -2 91 6 180 17 89 12 258 32 376 +46 118 14 216 27 218 28 7 8 -43 391 -52 392 -5 1 -62 -4 -125 -12z" + /> + <path + d="M4762 4789 c-46 -14 -109 -80 -122 -128 -7 -27 -10 -323 -8 -943 l3 +-903 24 -38 c13 -21 42 -50 64 -65 l41 -27 926 0 926 0 41 27 c22 15 51 44 64 +65 l24 38 0 926 0 925 -27 41 c-15 22 -44 51 -65 64 l-38 24 -910 2 c-557 1 +-923 -2 -943 -8z" + /> + <path + d="M8487 4297 c-26 -215 -161 -474 -307 -585 -27 -20 -49 -40 -49 -44 +-1 -3 49 -79 110 -167 l110 -161 44 31 c176 126 333 350 418 594 30 86 77 282 +77 320 0 8 -57 19 -167 34 -93 13 -182 25 -199 28 -31 5 -31 5 -37 -50z" + /> + <path + d="M3965 4233 c-106 -9 -348 -36 -415 -47 -55 -8 -75 -15 -74 -26 1 -20 +56 -374 59 -377 1 -2 46 4 101 12 159 24 409 45 526 45 l108 0 0 200 0 200 +-132 -2 c-73 -1 -151 -3 -173 -5z" + /> + <path + d="M3020 4079 c-85 -23 -292 -94 -368 -125 -97 -40 -298 -140 -305 -151 +-5 -7 172 -315 192 -336 4 -4 41 10 82 32 103 55 272 123 414 165 66 20 125 +38 132 41 11 4 -4 70 -78 348 -10 39 -14 41 -69 26z" + /> + <path + d="M6955 3538 c-21 -91 -74 -362 -72 -364 7 -7 260 -44 367 -54 146 -13 +359 -13 475 0 49 6 90 12 91 13 2 1 -12 90 -29 197 -26 155 -36 194 -47 192 +-8 -2 -85 -6 -170 -9 -160 -6 -357 7 -505 33 -103 18 -104 18 -110 -8z" + /> + <path + d="M1993 3513 c-52 -67 -71 -106 -98 -198 -35 -122 -44 -284 -21 -415 9 +-51 18 -96 21 -98 4 -5 360 79 375 88 7 4 7 24 0 60 -21 109 -7 244 31 307 +l20 31 -146 131 c-80 72 -147 131 -149 131 -2 0 -17 -17 -33 -37z" + /> + <path + d="M2210 2519 c-91 -50 -166 -92 -168 -94 -2 -1 11 -26 28 -54 l32 -51 +244 0 c134 0 244 2 244 5 0 3 -23 33 -51 67 -28 35 -72 98 -97 140 -26 43 -51 +77 -57 77 -5 0 -84 -41 -175 -90z" + /> + </g> + </svg> +); + +export default TrailsIcon; diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index 2c8e97512..f5c6a9273 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -59,8 +59,12 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp @observable public static imageEditorOpen: boolean = false; @observable public static imageEditorSource: string = ''; @observable public static addDoc: ((doc: Doc | Doc[], annotationKey?: string) => boolean) | undefined; - @action public static setImageEditorOpen(open: boolean) {ImageBox.imageEditorOpen = open;} - @action public static setImageEditorSource(source: string) {ImageBox.imageEditorSource = source;} + @action public static setImageEditorOpen(open: boolean) { + ImageBox.imageEditorOpen = open; + } + @action public static setImageEditorSource(source: string) { + ImageBox.imageEditorSource = source; + } private _ignoreScroll = false; private _forcedScroll = false; private _dropDisposer?: DragManager.DragDropDisposer; @@ -307,7 +311,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp } choosePath(url: URL) { - if (!url?.href) return ""; + if (!url?.href) return ''; const lower = url.href.toLowerCase(); if (url.protocol === 'data') return url.href; if (url.href.indexOf(window.location.origin) === -1 && url.href.indexOf('dashblobstore') === -1) return Utils.CorsProxy(url.href); diff --git a/src/client/views/nodes/LinkDocPreview.scss b/src/client/views/nodes/LinkDocPreview.scss index c68e55f73..28216394d 100644 --- a/src/client/views/nodes/LinkDocPreview.scss +++ b/src/client/views/nodes/LinkDocPreview.scss @@ -1,7 +1,6 @@ .linkDocPreview { position: absolute; pointer-events: all; - background-color: lightblue; border: 8px solid white; border-radius: 7px; box-shadow: 3px 3px 1.5px grey; @@ -11,7 +10,6 @@ cursor: pointer; .linkDocPreview-inner { - background-color: white; width: 100%; height: 100%; pointer-events: none; diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx index d69009415..42c8ea6a4 100644 --- a/src/client/views/nodes/LinkDocPreview.tsx +++ b/src/client/views/nodes/LinkDocPreview.tsx @@ -4,7 +4,7 @@ import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; import wiki from 'wikijs'; import { Doc, DocCastAsync, Opt } from '../../../fields/Doc'; -import { DirectLinks, Height, Width } from '../../../fields/DocSymbols'; +import { Height, Width } from '../../../fields/DocSymbols'; import { Cast, DocCast, NumCast, PromiseValue, StrCast } from '../../../fields/Types'; import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnNone, setupMoveUpEvents } from '../../../Utils'; import { DocServer } from '../../DocServer'; @@ -13,9 +13,9 @@ import { DocumentType } from '../../documents/DocumentTypes'; import { DragManager } from '../../util/DragManager'; import { LinkFollower } from '../../util/LinkFollower'; import { LinkManager } from '../../util/LinkManager'; +import { SearchUtil } from '../../util/SearchUtil'; import { SettingsManager } from '../../util/SettingsManager'; import { Transform } from '../../util/Transform'; -import { SearchBox } from '../search/SearchBox'; import { DocumentView, DocumentViewSharedProps, OpenWhere } from './DocumentView'; import './LinkDocPreview.scss'; import React = require('react'); @@ -174,7 +174,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { LinkFollower.FollowLink(this._linkDoc, this._linkSrc, false); } else if (this.props.hrefs?.length) { const webDoc = - Array.from(SearchBox.staticSearchCollection(Doc.MyFilesystem, this.props.hrefs[0]).keys()).lastElement() ?? + Array.from(SearchUtil.SearchCollection(Doc.MyFilesystem, this.props.hrefs[0]).keys()).lastElement() ?? Docs.Create.WebDocument(this.props.hrefs[0], { title: this.props.hrefs[0], _nativeWidth: 850, _width: 200, _height: 400, data_useCors: true }); this.props.docProps?.addDocTab(webDoc, OpenWhere.lightbox); } diff --git a/src/client/views/nodes/LoadingBox.tsx b/src/client/views/nodes/LoadingBox.tsx index fcbd0128d..0b02626e9 100644 --- a/src/client/views/nodes/LoadingBox.tsx +++ b/src/client/views/nodes/LoadingBox.tsx @@ -1,14 +1,14 @@ -import { action, observable, runInAction } from 'mobx'; +import { observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import ReactLoading from 'react-loading'; import { Doc } from '../../../fields/Doc'; +import { Id } from '../../../fields/FieldSymbols'; import { StrCast } from '../../../fields/Types'; import { Networking } from '../../Network'; import { ViewBoxAnnotatableComponent } from '../DocComponent'; import { FieldView, FieldViewProps } from './FieldView'; import './LoadingBox.scss'; -import { Id } from '../../../fields/FieldSymbols'; /** * LoadingBox Class represents a placeholder doc for documents that are currently @@ -44,7 +44,7 @@ export class LoadingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { this.rootDoc.loadingError = 'Upload interrupted, please try again'; } else { const updateFunc = async () => { - const result = await Networking.QueryYoutubeProgress(StrCast(this.rootDoc[Id])); // We use the guid of the overwriteDoc to track file uploads. + const result = await Networking.QueryYoutubeProgress(StrCast(this.rootDoc[Id])); // We use the guid of the overwriteDoc to track file uploads. runInAction(() => (this.progress = result.progress)); this._timer = setTimeout(updateFunc, 1000); }; diff --git a/src/client/views/nodes/MapBox/MapAnchorMenu.scss b/src/client/views/nodes/MapBox/MapAnchorMenu.scss new file mode 100644 index 000000000..6990bdcf1 --- /dev/null +++ b/src/client/views/nodes/MapBox/MapAnchorMenu.scss @@ -0,0 +1,54 @@ +.anchorMenu-addTag { + display: grid; + width: 200px; + padding: 5px; + grid-template-columns: 90px 20px 90px; +} +.anchorMenu-highlighter { + padding-right: 5px; + .antimodeMenu-button { + padding: 0; + padding: 0; + padding-right: 0px; + padding-left: 0px; + width: 5px; + } +} +.anchor-color-preview-button { + width: 25px !important; + .anchor-color-preview { + display: flex; + flex-direction: column; + padding-right: 3px; + width: unset !important; + .color-preview { + width: 60%; + top: 80%; + height: 4px; + position: relative; + top: unset; + width: 15px; + margin-top: 5px; + display: block; + } + } +} + +.color-wrapper { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + + button.color-button { + width: 20px; + height: 20px; + border-radius: 15px !important; + margin: 3px; + border: 2px solid transparent !important; + padding: 3px; + + &.active { + border: 2px solid white; + } + } +}
\ No newline at end of file diff --git a/src/client/views/nodes/MapBox/MapAnchorMenu.tsx b/src/client/views/nodes/MapBox/MapAnchorMenu.tsx new file mode 100644 index 000000000..f731763af --- /dev/null +++ b/src/client/views/nodes/MapBox/MapAnchorMenu.tsx @@ -0,0 +1,146 @@ +import React = require('react'); +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { action, computed, IReactionDisposer, observable, ObservableMap, reaction } from 'mobx'; +import { observer } from 'mobx-react'; +import { ColorState } from 'react-color'; +import { Doc, Opt } from '../../../../fields/Doc'; +import { returnFalse, setupMoveUpEvents, unimplementedFunction, Utils } from '../../../../Utils'; +import { SelectionManager } from '../../../util/SelectionManager'; +import { AntimodeMenu, AntimodeMenuProps } from "../../AntimodeMenu" +import { LinkPopup } from '../../linking/LinkPopup'; +import { gptAPICall, GPTCallType } from '../../../apis/gpt/GPT'; +// import { GPTPopup, GPTPopupMode } from './../../GPTPopup/GPTPopup'; +import { EditorView } from 'prosemirror-view'; +import './MapAnchorMenu.scss'; +import { ColorPicker, Group, IconButton, Popup, Size, Toggle, ToggleType, Type } from 'browndash-components'; +import { StrCast } from '../../../../fields/Types'; +import { DocumentType } from '../../../documents/DocumentTypes'; + +@observer +export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> { + static Instance: MapAnchorMenu; + + private _disposer: IReactionDisposer | undefined; + private _disposer2: IReactionDisposer | undefined; + private _commentCont = React.createRef<HTMLButtonElement>(); + + + + + public onMakeAnchor: () => Opt<Doc> = () => undefined; // Method to get anchor from text search + + public Center: () => void = unimplementedFunction; + // public OnClick: (e: PointerEvent) => void = unimplementedFunction; + // public OnAudio: (e: PointerEvent) => void = unimplementedFunction; + // public StartDrag: (e: PointerEvent, ele: HTMLElement) => void = unimplementedFunction; + // public StartCropDrag: (e: PointerEvent, ele: HTMLElement) => void = unimplementedFunction; + public Highlight: (color: string, isTargetToggler: boolean, savedAnnotations?: ObservableMap<number, HTMLDivElement[]>, addAsAnnotation?: boolean) => Opt<Doc> = (color: string, isTargetToggler: boolean) => undefined; + public GetAnchor: (savedAnnotations: Opt<ObservableMap<number, HTMLDivElement[]>>, addAsAnnotation: boolean) => Opt<Doc> = (savedAnnotations: Opt<ObservableMap<number, HTMLDivElement[]>>, addAsAnnotation: boolean) => undefined; + public Delete: () => void = unimplementedFunction; + public LinkNote: () => void = unimplementedFunction; + // public MakeTargetToggle: () => void = unimplementedFunction; + // public ShowTargetTrail: () => void = unimplementedFunction; + public IsTargetToggler: () => boolean = returnFalse; + public get Active() { + return this._left > 0; + } + + constructor(props: Readonly<{}>) { + super(props); + + MapAnchorMenu.Instance = this; + MapAnchorMenu.Instance._canFade = false; + } + + componentWillUnmount() { + this._disposer?.(); + this._disposer2?.(); + } + + componentDidMount() { + this._disposer2 = reaction( + () => this._opacity, + opacity => { + if (!opacity) { + } + }, + { fireImmediately: true } + ); + this._disposer = reaction( + () => SelectionManager.Views().slice(), + selected => { + MapAnchorMenu.Instance.fadeOut(true); + } + ); + } + // audioDown = (e: React.PointerEvent) => { + // setupMoveUpEvents(this, e, returnFalse, returnFalse, e => this.OnAudio?.(e)); + // }; + + // cropDown = (e: React.PointerEvent) => { + // setupMoveUpEvents( + // this, + // e, + // (e: PointerEvent) => { + // this.StartCropDrag(e, this._commentCont.current!); + // return true; + // }, + // returnFalse, + // e => this.OnCrop?.(e) + // ); + // }; + + + static top = React.createRef<HTMLDivElement>(); + // public get Top(){ + // return this.top + // } + + + render() { + const buttons =( + <> + {( + <IconButton + tooltip="Delete Pin" // + onPointerDown={this.Delete} + icon={<FontAwesomeIcon icon="trash-alt" />} + color={StrCast(Doc.UserDoc().userColor)} + /> + )} + {( + <IconButton + tooltip="Link Note to Pin" // + onPointerDown={this.LinkNote} + icon={<FontAwesomeIcon icon="sticky-note" />} + color={StrCast(Doc.UserDoc().userColor)} + /> + )} + {( + <IconButton + tooltip="Center on pin" // + onPointerDown={this.Center} + icon={<FontAwesomeIcon icon="compress-arrows-alt" />} + color={StrCast(Doc.UserDoc().userColor)} + /> + )} + {/* {this.IsTargetToggler !== returnFalse && ( + <Toggle + tooltip={'Make target visibility toggle on click'} + type={Type.PRIM} + toggleType={ToggleType.BUTTON} + toggleStatus={this.IsTargetToggler()} + onClick={this.MakeTargetToggle} + icon={<FontAwesomeIcon icon="thumbtack" />} + color={StrCast(Doc.UserDoc().userColor)} + /> + )} */} + </> + ); + + return this.getElement(<div ref={MapAnchorMenu.top} style={{width:"100%", display:"flex"}}> + {buttons} + </div> + ); + } +} diff --git a/src/client/views/nodes/MapBox/MapBox.scss b/src/client/views/nodes/MapBox/MapBox.scss index fb15520f6..242677231 100644 --- a/src/client/views/nodes/MapBox/MapBox.scss +++ b/src/client/views/nodes/MapBox/MapBox.scss @@ -1,87 +1,108 @@ -@import "../../global/globalCssVariables.scss"; +@import '../../global/globalCssVariables.scss'; .mapBox { - width: 100%; - height: 100%; - overflow: hidden; - display: flex; - - .mapBox-infoWindow { - background-color: white; - opacity: 0.75; - padding: 12; - font-size: 17; - } + width: 100%; + height: 100%; + overflow: hidden; + display: flex; - .mapBox-overlayButton-sidebar { - background: #121721; - height: 25px; - width: 25px; - right: 5px; - display: flex; - position: absolute; - align-items: center; - justify-content: center; - border-radius: 3px; - pointer-events: all; - z-index: 1; // so it appears on top of the document's title, if shown - - box-shadow: $standard-box-shadow; - transition: 0.2s; - - &:hover{ - filter: brightness(0.85); - } + .mapBox-infoWindow { + background-color: white; + opacity: 0.75; + padding: 12; + font-size: 17; + } + .mapBox-searchbar { + display: flex; + flex-direction: row; + width: calc(100% - 40px); + .editableText-container { + width: 100%; + font-size: 16px !important; } - - .mapBox-wrapper { + input { width: 100%; - .mapBox-input { - box-sizing: border-box; - border: 1px solid transparent; - width: 240px; - height: 32px; - padding: 0 12px; - border-radius: 3px; - box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3); - font-size: 14px; - outline: none; - text-overflow: ellipses; - position: absolute; - left: 50%; - margin-left: -120px; - } } + } + .mapBox-topbar { + display: flex; + flex-direction: row; + } - .mapBox-sidebar-handle { - top: 0; - //top: calc(50% - 17.5px); // use this to center vertically -- make sure it looks okay for slide views - width: 10px; - height: 100%; - max-height: 35px; - background: lightgray; - border-radius: 20px; - cursor:grabbing; + .mapBox-overlayButton-sidebar { + background: #121721; + height: 25px; + width: 25px; + right: 5px; + display: flex; + position: absolute; + align-items: center; + justify-content: center; + border-radius: 3px; + pointer-events: all; + z-index: 1; // so it appears on top of the document's title, if shown + + box-shadow: $standard-box-shadow; + transition: 0.2s; + + &:hover { + filter: brightness(0.85); } - .mapBox-addMarker { + } + + .mapBox-wrapper { + width: 100%; + .mapBox-input { + box-sizing: border-box; + border: 1px solid transparent; + width: 240px; + height: 32px; + padding: 0 12px; + border-radius: 3px; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3); + font-size: 14px; + outline: none; + text-overflow: ellipses; + position: absolute; left: 50%; - margin-left: 120px; - right: unset !important; - margin-top: -10; - height: max-content; - } - .searchbox { - display:none; - } - .mapBox-addMarker { - display:none; + margin-left: -120px; } + } + .mapBox-sidebar { + position: absolute; + right: 0; + height: 100%; + } + + .mapBox-sidebar-handle { + top: 0; + //top: calc(50% - 17.5px); // use this to center vertically -- make sure it looks okay for slide views + width: 10px; + height: 100%; + max-height: 35px; + background: lightgray; + border-radius: 20px; + cursor: grabbing; + } + .mapBox-addMarker { + left: 50%; + margin-left: 120px; + right: unset !important; + margin-top: -10; + height: max-content; + } + .searchbox { + display: none; + } + .mapBox-addMarker { + display: none; + } } .mapBox:hover { .mapBox-addMarker { - display:block; + display: block; } .searchbox { - display :block; + display: block; } } diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx index cf017d746..f0106dbbb 100644 --- a/src/client/views/nodes/MapBox/MapBox.tsx +++ b/src/client/views/nodes/MapBox/MapBox.tsx @@ -1,31 +1,33 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Autocomplete, GoogleMap, GoogleMapProps, Marker } from '@react-google-maps/api'; import BingMapsReact from 'bingmaps-react'; -import { action, computed, IReactionDisposer, observable, ObservableMap, runInAction } from 'mobx'; +import { Button, EditableText, IconButton, Type } from 'browndash-components'; +import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { Doc, DocListCast, Opt } from '../../../../fields/Doc'; -import { Width } from '../../../../fields/DocSymbols'; -import { Id } from '../../../../fields/FieldSymbols'; +import { Doc, DocListCast, Field, Opt } from '../../../../fields/Doc'; +import { DocCss, Highlight, Width } from '../../../../fields/DocSymbols'; import { InkTool } from '../../../../fields/InkField'; -import { NumCast, StrCast } from '../../../../fields/Types'; -import { emptyFunction, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../../Utils'; -import { Docs } from '../../../documents/Documents'; +import { DocCast, NumCast, StrCast } from '../../../../fields/Types'; +import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnOne, returnTrue, setupMoveUpEvents, Utils } from '../../../../Utils'; +import { Docs, DocUtils } from '../../../documents/Documents'; +import { DocumentType } from '../../../documents/DocumentTypes'; +import { DocumentManager } from '../../../util/DocumentManager'; import { DragManager } from '../../../util/DragManager'; +import { LinkManager } from '../../../util/LinkManager'; import { SnappingManager } from '../../../util/SnappingManager'; -import { UndoManager } from '../../../util/UndoManager'; +import { Transform } from '../../../util/Transform'; +import { undoable, UndoManager } from '../../../util/UndoManager'; import { MarqueeOptionsMenu } from '../../collections/collectionFreeForm'; import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../../DocComponent'; import { Colors } from '../../global/globalEnums'; import { MarqueeAnnotator } from '../../MarqueeAnnotator'; -import { AnchorMenu } from '../../pdf/AnchorMenu'; -import { Annotation } from '../../pdf/Annotation'; import { SidebarAnnos } from '../../SidebarAnnos'; +import { DocumentView } from '../DocumentView'; import { FieldView, FieldViewProps } from '../FieldView'; -import { PinProps } from '../trails'; +import { PinProps, PresBox } from '../trails'; +import { MapAnchorMenu } from './MapAnchorMenu'; import './MapBox.scss'; -import { MapBoxInfoWindow } from './MapBoxInfoWindow'; - +// amongus /** * MapBox architecture: * Main component: MapBox.tsx @@ -39,29 +41,7 @@ import { MapBoxInfoWindow } from './MapBoxInfoWindow'; * A map marker is considered a document that contains a collection with stacking view of documents, it has a lat, lng location, which is passed to Maps API's custom marker (red pin) to be rendered on the google maps */ -// const _global = (window /* browser */ || global /* node */) as any; - -const mapContainerStyle = { - height: '100%', -}; - -const defaultCenter = { - lat: 42.360081, - lng: -71.058884, -}; - -const mapOptions = { - fullscreenControl: false, -}; - const bingApiKey = process.env.BING_MAPS; // if you're running local, get a Bing Maps api key here: https://www.bingmapsportal.com/ and then add it to the .env file in the Dash-Web root directory as: _CLIENT_BING_MAPS=<your apikey> -const apiKey = process.env.GOOGLE_MAPS; - -const script = document.createElement('script'); -script.defer = true; -script.async = true; -script.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places,drawing`; -document.head.appendChild(script); /** * Consider integrating later: allows for drawing, circling, making shapes on map @@ -79,245 +59,55 @@ document.head.appendChild(script); // }, // }); -// options for searchbox in Google Maps Places Autocomplete API -const options = { - fields: ['formatted_address', 'geometry', 'name'], // note: level of details is charged by item per retrieval, not recommended to return all fields - strictBounds: false, - types: ['establishment'], // type pf places, subject of change according to user need -} as google.maps.places.AutocompleteOptions; - @observer -export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps & Partial<GoogleMapProps>>() { - static UseBing = true; - private _dropDisposer?: DragManager.DragDropDisposer; - private _disposers: { [name: string]: IReactionDisposer } = {}; - private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); - @observable private _overlayAnnoInfo: Opt<Doc>; - showInfo = action((anno: Opt<Doc>) => (this._overlayAnnoInfo = anno)); +export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps>() { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(MapBox, fieldKey); } - public get SidebarKey() { - return this.fieldKey + '_sidebar'; - } + private _dragRef = React.createRef<HTMLDivElement>(); + private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); + private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); + private _sidebarRef = React.createRef<SidebarAnnos>(); + private _ref: React.RefObject<HTMLDivElement> = React.createRef(); + private _disposers: { [key: string]: IReactionDisposer } = {}; private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean) => void); - @computed get inlineTextAnnotations() { - return this.allMapMarkers.filter(a => a.text_inlineAnnotations); - } - @observable private _map: google.maps.Map = null as unknown as google.maps.Map; - @observable private selectedPlace: Doc | undefined; - @observable private markerMap: { [id: string]: google.maps.Marker } = {}; - @observable private center = navigator.geolocation ? navigator.geolocation.getCurrentPosition : defaultCenter; @observable private _marqueeing: number[] | undefined; - @observable private _isAnnotating = false; - @observable private inputRef = React.createRef<HTMLInputElement>(); - @observable private searchMarkers: google.maps.Marker[] = []; - @observable private searchBox = new window.google.maps.places.Autocomplete(this.inputRef.current!, options); @observable private _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>(); @computed get allSidebarDocs() { return DocListCast(this.dataDoc[this.SidebarKey]); } - @computed get allMapMarkers() { + // this list contains pushpins and configs + @computed get allAnnotations() { return DocListCast(this.dataDoc[this.annotationKey]); } - @observable private toggleAddMarker = false; - private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); - - @observable _showSidebar = false; + @computed get allPushpins() { + return this.allAnnotations.filter(anno => anno.type === DocumentType.PUSHPIN); + } @computed get SidebarShown() { - return this._showSidebar || this.layoutDoc._layout_showSidebar ? true : false; + return this.layoutDoc._layout_showSidebar ? true : false; + } + @computed get sidebarWidthPercent() { + return StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%'); + } + @computed get sidebarColor() { + return StrCast(this.layoutDoc.sidebar_color, StrCast(this.layoutDoc[this.props.fieldKey + '_backgroundColor'], '#e4e4e4')); + } + @computed get SidebarKey() { + return this.fieldKey + '_sidebar'; } - - static _canAnnotate = true; - static _hadSelection: boolean = false; - private _sidebarRef = React.createRef<SidebarAnnos>(); - private _ref: React.RefObject<HTMLDivElement> = React.createRef(); componentDidMount() { + this._unmounting = false; this.props.setContentView?.(this); } - @action - private setSearchBox = (searchBox: any) => { - this.searchBox = searchBox; - }; - - // iterate allMarkers to size, center, and zoom map to contain all markers - private fitBounds = (map: google.maps.Map) => { - const curBounds = map.getBounds() ?? new window.google.maps.LatLngBounds(); - const isFitting = this.allMapMarkers.reduce((fits, place) => fits && curBounds?.contains({ lat: NumCast(place.lat), lng: NumCast(place.lng) }), true as boolean); - !isFitting && map.fitBounds(this.allMapMarkers.reduce((bounds, place) => bounds.extend({ lat: NumCast(place.lat), lng: NumCast(place.lng) }), new window.google.maps.LatLngBounds())); - }; - - /** - * Custom control for add marker button - * @param controlDiv - * @param map - */ - private CenterControl = () => { - const controlDiv = document.createElement('div'); - controlDiv.className = 'mapBox-addMarker'; - // Set CSS for the control border. - const controlUI = document.createElement('div'); - controlUI.style.backgroundColor = '#fff'; - controlUI.style.borderRadius = '3px'; - controlUI.style.cursor = 'pointer'; - controlUI.style.marginTop = '10px'; - controlUI.style.borderRadius = '4px'; - controlUI.style.marginBottom = '22px'; - controlUI.style.textAlign = 'center'; - controlUI.style.position = 'absolute'; - controlUI.style.width = '32px'; - controlUI.style.height = '32px'; - controlUI.title = 'Click to toggle marker mode. In marker mode, click on map to place a marker.'; - - const plIcon = document.createElement('img'); - plIcon.src = 'https://cdn4.iconfinder.com/data/icons/wirecons-free-vector-icons/32/add-256.png'; - plIcon.style.color = 'rgb(25,25,25)'; - plIcon.style.fontFamily = 'Roboto,Arial,sans-serif'; - plIcon.style.fontSize = '16px'; - plIcon.style.lineHeight = '32px'; - plIcon.style.left = '18'; - plIcon.style.top = '15'; - plIcon.style.position = 'absolute'; - plIcon.width = 14; - plIcon.height = 14; - plIcon.innerHTML = 'Add'; - controlUI.appendChild(plIcon); - - // Set CSS for the control interior. - const markerIcon = document.createElement('img'); - markerIcon.src = 'https://cdn0.iconfinder.com/data/icons/small-n-flat/24/678111-map-marker-1024.png'; - markerIcon.style.color = 'rgb(25,25,25)'; - markerIcon.style.fontFamily = 'Roboto,Arial,sans-serif'; - markerIcon.style.fontSize = '16px'; - markerIcon.style.lineHeight = '32px'; - markerIcon.style.left = '-2'; - markerIcon.style.top = '1'; - markerIcon.width = 30; - markerIcon.height = 30; - markerIcon.style.position = 'absolute'; - markerIcon.innerHTML = 'Add'; - controlUI.appendChild(markerIcon); - - // Setup the click event listeners - controlUI.addEventListener('click', () => { - if (this.toggleAddMarker === true) { - this.toggleAddMarker = false; - console.log('add marker button status:' + this.toggleAddMarker); - controlUI.style.backgroundColor = '#fff'; - markerIcon.style.color = 'rgb(25,25,25)'; - } else { - this.toggleAddMarker = true; - console.log('add marker button status:' + this.toggleAddMarker); - controlUI.style.backgroundColor = '#4476f7'; - markerIcon.style.color = 'rgb(255,255,255)'; - } - }); - controlDiv.appendChild(controlUI); - return controlDiv; - }; - - /** - * Place the marker on google maps & store the empty marker as a MapMarker Document in allMarkers list - * @param position - the LatLng position where the marker is placed - * @param map - */ - @action - private placeMarker = (position: google.maps.LatLng, map: google.maps.Map) => { - const marker = new google.maps.Marker({ - position: position, - map: map, - }); - map.panTo(position); - const mapMarker = Docs.Create.MapMarkerDocument(NumCast(position.lat()), NumCast(position.lng()), false, [], {}); - this.addDocument(mapMarker, this.annotationKey); - }; - - _loadPending = true; - /** - * store a reference to google map instance - * setup the drawing manager on the top right corner of map - * fit map bounds to contain all markers - * @param map - */ - @action - private loadHandler = (map: google.maps.Map) => { - this._map = map; - this._loadPending = true; - const centerControlDiv = this.CenterControl(); - map.controls[google.maps.ControlPosition.TOP_RIGHT].push(centerControlDiv); - //drawingManager.setMap(map); - // if (navigator.geolocation) { - // navigator.geolocation.getCurrentPosition( - // (position: Position) => { - // const pos = { - // lat: position.coords.latitude, - // lng: position.coords.longitude, - // }; - // this._map.setCenter(pos); - // } - // ); - // } else { - // alert("Your geolocation is not supported by browser.") - // }; - map.setZoom(NumCast(this.dataDoc.mapZoom, 2.5)); - map.setCenter(new google.maps.LatLng(NumCast(this.dataDoc.mapLat), NumCast(this.dataDoc.mapLng))); - setTimeout(() => { - if (this._loadPending && this._map.getBounds()) { - this._loadPending = false; - this.layoutDoc.freeform_fitContentsToBox && this.fitBounds(this._map); - } - }, 250); - // listener to addmarker event - this._map.addListener('click', (e: MouseEvent) => { - if (this.toggleAddMarker === true) { - this.placeMarker((e as any).latLng, map); - } - }); - }; - - @action - centered = () => { - if (this._loadPending && this._map.getBounds()) { - this._loadPending = false; - this.layoutDoc.freeform_fitContentsToBox && this.fitBounds(this._map); - } - this.dataDoc.mapLat = this._map.getCenter()?.lat(); - this.dataDoc.mapLng = this._map.getCenter()?.lng(); - }; - - @action - zoomChanged = () => { - if (this._loadPending && this._map.getBounds()) { - this._loadPending = false; - this.layoutDoc.freeform_fitContentsToBox && this.fitBounds(this._map); - } - this.dataDoc.mapZoom = this._map.getZoom(); - }; - - /** - * Load and render all map markers - * @param marker - * @param place - */ - @action - private markerLoadHandler = (marker: google.maps.Marker, place: Doc) => { - place[Id] ? (this.markerMap[place[Id]] = marker) : null; - }; - - /** - * on clicking the map marker, set the selected place to the marker document & set infowindowopen to be true - * @param e - * @param place - */ - @action - private markerClickHandler = (e: google.maps.MapMouseEvent, place: Doc) => { - // set which place was clicked - this.selectedPlace = place; - place.infoWindowOpen = true; - }; + _unmounting = false; + componentWillUnmount(): void { + this._unmounting = true; + this.deselectPin(); + Object.keys(this._disposers).forEach(key => this._disposers[key]?.()); + } /** * Called when dragging documents into map sidebar or directly into infowindow; to create a map marker, ref to MapMarkerDocument in Documents.ts @@ -326,18 +116,23 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps * @returns */ sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => { - console.log('print all sidebar Docs'); if (!this.layoutDoc._layout_showSidebar) this.toggleSidebar(); const docs = doc instanceof Doc ? [doc] : doc; docs.forEach(doc => { - if (doc.lat !== undefined && doc.lng !== undefined) { - const existingMarker = this.allMapMarkers.find(marker => marker.lat === doc.lat && marker.lng === doc.lng); - if (existingMarker) { - Doc.AddDocToList(existingMarker, 'data', doc); - } else { - const marker = Docs.Create.MapMarkerDocument(NumCast(doc.lat), NumCast(doc.lng), false, [doc], {}); - this.addDocument(marker, this.annotationKey); - } + let existingPin = this.allPushpins.find(pin => pin.latitude === doc.latitude && pin.longitude === doc.longitude) ?? this.selectedPin; + if (doc.latitude !== undefined && doc.longitude !== undefined && !existingPin) { + existingPin = this.createPushpin(NumCast(doc.latitude), NumCast(doc.longitude), StrCast(doc.map)); + } + if (existingPin) { + setTimeout(() => { + // we use a timeout in case this is called from the sidebar which may have just added a link that hasn't made its way into th elink manager yet + if (!LinkManager.Instance.getAllRelatedLinks(doc).some(link => DocCast(link.link_anchor_1)?.mapPin === existingPin || DocCast(link.link_anchor_2)?.mapPin === existingPin)) { + const anchor = this.getAnchor(true, undefined, existingPin); + anchor && DocUtils.MakeLink(anchor, doc, { link_relationship: 'link to map location' }); + doc.latitude = existingPin?.latitude; + doc.longitude = existingPin?.longitude; + } + }); } }); //add to annotation list @@ -350,11 +145,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps * @param sidebarKey * @returns */ - sidebarRemoveDocument = (doc: Doc | Doc[], sidebarKey?: string) => { - if (this.layoutDoc._layout_showSidebar) this.toggleSidebar(); - const docs = doc instanceof Doc ? [doc] : doc; - return this.removeDocument(doc, sidebarKey); - }; + sidebarRemoveDocument = (doc: Doc | Doc[], sidebarKey?: string) => this.removeDocument(doc, sidebarKey); /** * Toggle sidebar onclick the tiny comment button on the top right corner @@ -373,11 +164,11 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps const fullWidth = this.layoutDoc[Width](); const mapWidth = fullWidth - this.sidebarWidth(); if (this.sidebarWidth() + localDelta[0] > 0) { - this._showSidebar = true; + this.layoutDoc._layout_showSidebar = true; this.layoutDoc._width = fullWidth + localDelta[0]; this.layoutDoc._layout_sidebarWidthPercent = ((100 * (this.sidebarWidth() + localDelta[0])) / (fullWidth + localDelta[0])).toString() + '%'; } else { - this._showSidebar = false; + this.layoutDoc._layout_showSidebar = false; this.layoutDoc._width = mapWidth; this.layoutDoc._layout_sidebarWidthPercent = '0%'; } @@ -387,60 +178,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps () => UndoManager.RunInBatch(this.toggleSidebar, 'toggle sidebar map') ); }; - - sidebarWidth = () => (Number(this.layout_sidebarWidthPercent.substring(0, this.layout_sidebarWidthPercent.length - 1)) / 100) * this.props.PanelWidth(); - @computed get layout_sidebarWidthPercent() { - return StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%'); - } - @computed get sidebarColor() { - return StrCast(this.layoutDoc.sidebar_color, StrCast(this.layoutDoc[this.props.fieldKey + '_backgroundColor'], '#e4e4e4')); - } - - /** - * function that reads the place inputed from searchbox, then zoom in on the location that's been autocompleted; - * add a customized temporary marker on the map - */ - @action - private handlePlaceChanged = () => { - const place = this.searchBox.getPlace(); - - if (!place.geometry || !place.geometry.location) { - // user entered the name of a place that wasn't suggested & pressed the enter key, or place details request failed - window.alert("No details available for input: '" + place.name + "'"); - return; - } - - // zoom in on the location of the search result - if (place.geometry.viewport) { - this._map.fitBounds(place.geometry.viewport); - } else { - this._map.setCenter(place.geometry.location); - this._map.setZoom(17); - } - - // customize icon => customized icon for the nature of the location selected - const icon = { - url: place.icon as string, - size: new google.maps.Size(71, 71), - origin: new google.maps.Point(0, 0), - anchor: new google.maps.Point(17, 34), - scaledSize: new google.maps.Size(25, 25), - }; - - // put temporary cutomized marker on searched location - this.searchMarkers.forEach(marker => { - marker.setMap(null); - }); - this.searchMarkers = []; - this.searchMarkers.push( - new window.google.maps.Marker({ - map: this._map, - icon, - title: place.name, - position: place.geometry.location, - }) - ); - }; + sidebarWidth = () => (Number(this.sidebarWidthPercent.substring(0, this.sidebarWidthPercent.length - 1)) / 100) * this.props.PanelWidth(); /** * Handles toggle of sidebar on click the little comment button @@ -465,12 +203,28 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps // TODO: Adding highlight box layer to Maps @action toggleSidebar = () => { - //1.2 * w * ? = .2 * w .2/1.2 const prevWidth = this.sidebarWidth(); this.layoutDoc._layout_showSidebar = (this.layoutDoc._layout_sidebarWidthPercent = StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%') === '0%' ? `${(100 * 0.2) / 1.2}%` : '0%') !== '0%'; this.layoutDoc._width = this.layoutDoc._layout_showSidebar ? NumCast(this.layoutDoc._width) * 1.2 : Math.max(20, NumCast(this.layoutDoc._width) - prevWidth); }; + createNoteAnnotation = () => { + const createFunc = undoable( + action(() => { + const note = this._sidebarRef.current?.anchorMenuClick(this.getAnchor(false), ['latitude', 'longitude', '-linkedTo']); + if (note && this.selectedPin) { + note.latitude = this.selectedPin.latitude; + note.longitude = this.selectedPin.longitude; + note.map = this.selectedPin.map; + } + }), + 'create note annotation' + ); + if (!this.layoutDoc.layout_showSidebar) { + this.toggleSidebar(); + setTimeout(createFunc); + } else createFunc(); + }; sidebarDown = (e: React.PointerEvent) => { setupMoveUpEvents(this, e, this.sidebarMove, emptyFunction, () => setTimeout(this.toggleSidebar), true); }; @@ -503,48 +257,13 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }; @action finishMarquee = (x?: number, y?: number) => { this._marqueeing = undefined; - this._isAnnotating = false; x !== undefined && y !== undefined && this._setPreviewCursor?.(x, y, false, false); }; - addDocumentWrapper = (doc: Doc | Doc[], annotationKey?: string) => { - return this.addDocument(doc, annotationKey); - }; + addDocumentWrapper = (doc: Doc | Doc[], annotationKey?: string) => this.addDocument(doc, annotationKey); pointerEvents = () => (this.props.isContentActive() && !MarqueeOptionsMenu.Instance.isShown() ? 'all' : 'none'); - @computed get annotationLayer() { - return ( - <div className="mapBox-annotationLayer" style={{ height: Doc.NativeHeight(this.Document) || undefined }} ref={this._annotationLayer}> - {this.inlineTextAnnotations - .sort((a, b) => NumCast(a.y) - NumCast(b.y)) - .map(anno => ( - <Annotation key={`${anno[Id]}-annotation`} {...this.props} fieldKey={this.annotationKey} pointerEvents={this.pointerEvents} showInfo={this.showInfo} dataDoc={this.dataDoc} anno={anno} /> - ))} - </div> - ); - } - - getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => AnchorMenu.Instance?.GetAnchor(this._savedAnnotations, addAsAnnotation) ?? this.rootDoc; - - /** - * render contents in allMapMarkers (e.g. images with exifData) into google maps as map marker - * @returns - */ - private renderMarkers = () => { - return this.allMapMarkers.map(place => ( - <Marker key={place[Id]} position={{ lat: NumCast(place.lat), lng: NumCast(place.lng) }} onLoad={marker => this.markerLoadHandler(marker, place)} onClick={(e: google.maps.MapMouseEvent) => this.markerClickHandler(e, place)} /> - )); - }; - - // TODO: auto center on select a document in the sidebar - private handleMapCenter = (map: google.maps.Map) => { - // console.log("print the selected views in selectionManager:") - // if (SelectionManager.Views().lastElement()) { - // console.log(SelectionManager.Views().lastElement()); - // } - }; - panelWidth = () => this.props.PanelWidth() / (this.props.NativeDimScaling?.() || 1) - this.sidebarWidth(); panelHeight = () => this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1); scrollXf = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._layout_scrollTop)); @@ -581,24 +300,408 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } else { this._bingSearchManager.geocode({ where: query, - callback: action((r: any) => { - res(r.results[0].location); - }), + callback: action((r: any) => res(r.results[0].location)), errorCallback: (e: any) => reject(), }); } }); }; + @observable + bingSearchBarContents: any = this.rootDoc.map; // For Bing Maps: The contents of the Bing search bar (string) + + geoDataRequestOptions = { + entityType: 'PopulatedPlace', + }; + + // incrementer: number = 0; + /* + * Creates Pushpin doc and adds it to the list of annotations + */ + @action + createPushpin = undoable((latitude: number, longitude: number, map?: string) => { + // Stores the pushpin as a MapMarkerDocument + const pushpin = Docs.Create.PushpinDocument( + NumCast(latitude), + NumCast(longitude), + false, + [], + { map: map } + // ,'pushpinIDamongus'+ this.incrementer++ + ); + this.addDocument(pushpin, this.annotationKey); + return pushpin; + // mapMarker.infoWindowOpen = true; + }, 'createpin'); + + // The pin that is selected + @observable selectedPin: Doc | undefined; + + @action + deselectPin = () => { + if (this.selectedPin) { + // Removes filter + Doc.setDocFilter(this.rootDoc, 'latitude', this.selectedPin.latitude, 'remove'); + Doc.setDocFilter(this.rootDoc, 'longitude', this.selectedPin.longitude, 'remove'); + Doc.setDocFilter(this.rootDoc, '-linkedTo', Field.toString(DocCast(this.selectedPin.mapPin)), 'removeAll'); + + const temp = this.selectedPin; + if (!this._unmounting) { + this._bingMap.current.entities.remove(this.map_docToPinMap.get(temp)); + } + const newpin = new this.MicrosoftMaps.Pushpin(new this.MicrosoftMaps.Location(temp.latitude, temp.longitude)); + this.MicrosoftMaps.Events.addHandler(newpin, 'click', (e: any) => this.pushpinClicked(temp as Doc)); + if (!this._unmounting) { + this._bingMap.current.entities.push(newpin); + } + this.map_docToPinMap.set(temp, newpin); + this.selectedPin = undefined; + this.bingSearchBarContents = this.rootDoc.map; + } + }; + + getView = async (doc: Doc) => { + if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) this.toggleSidebar(); + return new Promise<Opt<DocumentView>>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv))); + }; + /* + * Pushpin onclick + */ + @action + pushpinClicked = (pinDoc: Doc) => { + this.deselectPin(); + this.selectedPin = pinDoc; + this.bingSearchBarContents = pinDoc.map; + + // Doc.setDocFilter(this.rootDoc, 'latitude', this.selectedPin.latitude, 'match'); + // Doc.setDocFilter(this.rootDoc, 'longitude', this.selectedPin.longitude, 'match'); + Doc.setDocFilter(this.rootDoc, '-linkedTo', Field.toScriptString(this.selectedPin), 'mapPin' as any); + + this.recolorPin(this.selectedPin, 'green'); + + MapAnchorMenu.Instance.Delete = this.deleteSelectedPin; + MapAnchorMenu.Instance.Center = this.centerOnSelectedPin; + MapAnchorMenu.Instance.LinkNote = this.createNoteAnnotation; + + const point = this._bingMap.current.tryLocationToPixel(new this.MicrosoftMaps.Location(this.selectedPin.latitude, this.selectedPin.longitude)); + const x = point.x + (this.props.PanelWidth() - this.sidebarWidth()) / 2; + const y = point.y + this.props.PanelHeight() / 2 + 32; + const cpt = this.props.ScreenToLocalTransform().inverse().transformPoint(x, y); + MapAnchorMenu.Instance.jumpTo(cpt[0], cpt[1], true); + + document.addEventListener('pointerdown', this.tryHideMapAnchorMenu, true); + }; + + /** + * Map OnClick + */ + @action + mapOnClick = (e: { location: { latitude: any; longitude: any } }) => { + this.props.select(false); + this.deselectPin(); + }; + /* + * Updates values of layout doc to match the current map + */ + @action + mapRecentered = () => { + if ( + Math.abs(NumCast(this.dataDoc.latitude) - this._bingMap.current.getCenter().latitude) > 1e-7 || // + Math.abs(NumCast(this.dataDoc.longitude) - this._bingMap.current.getCenter().longitude) > 1e-7 + ) { + this.dataDoc.latitude = this._bingMap.current.getCenter().latitude; + this.dataDoc.longitude = this._bingMap.current.getCenter().longitude; + this.dataDoc.map = ''; + this.bingSearchBarContents = ''; + } + this.dataDoc.map_zoom = this._bingMap.current.getZoom(); + }; + /* + * Updates maptype + */ + @action + updateMapType = () => (this.dataDoc.map_type = this._bingMap.current.getMapTypeId()); + + /* + * For Bing Maps + * Called by search button's onClick + * Finds the geocode of the searched contents and sets location to that location + **/ + @action + bingSearch = () => { + return this.bingGeocode(this._bingMap, this.bingSearchBarContents).then(location => { + this.dataDoc.latitude = location.latitude; + this.dataDoc.longitude = location.longitude; + this.dataDoc.map_zoom = this._bingMap.current.getZoom(); + this.dataDoc.map = this.bingSearchBarContents; + }); + }; + + /* + * Returns doc w/ relevant info + */ + getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps, existingPin?: Doc) => { + /// this should use SELECTED pushpin for lat/long if there is a selection, otherwise CENTER + const anchor = Docs.Create.ConfigDocument({ + title: 'MapAnchor:' + this.rootDoc.title, + text: StrCast(this.selectedPin?.map) || StrCast(this.rootDoc.map) || 'map location', + config_latitude: NumCast((existingPin ?? this.selectedPin)?.latitude ?? this.dataDoc.latitude), + config_longitude: NumCast((existingPin ?? this.selectedPin)?.longitude ?? this.dataDoc.longitude), + config_map_zoom: NumCast(this.dataDoc.map_zoom), + config_map_type: StrCast(this.dataDoc.map_type), + config_map: StrCast((existingPin ?? this.selectedPin)?.map) || StrCast(this.dataDoc.map), + layout_unrendered: true, + }); + if (anchor) { + anchor.mapPin = existingPin ?? this.selectedPin; + if (!addAsAnnotation) anchor.backgroundColor = 'transparent'; + /* addAsAnnotation &&*/ this.addDocument(anchor); + PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), map: true } }, this.rootDoc); + return anchor; + } + return this.rootDoc; + }; + + map_docToPinMap = new Map<Doc, any>(); + map_pinHighlighted = new Map<Doc, boolean>(); + /* + * Input: pin doc + * Adds MicrosoftMaps Pushpin to the map (render) + */ + @action + addPushpin = (pin: Doc) => { + const pushPin = pin.infoWindowOpen + ? new this.MicrosoftMaps.Pushpin(new this.MicrosoftMaps.Location(pin.latitude, pin.longitude), {}) + : new this.MicrosoftMaps.Pushpin( + new this.MicrosoftMaps.Location(pin.latitude, pin.longitude) + // {icon: 'http://icons.iconarchive.com/icons/icons-land/vista-map-markers/24/Map-Marker-Marker-Outside-Chartreuse-icon.png'} + ); + + this._bingMap.current.entities.push(pushPin); + + this.MicrosoftMaps.Events.addHandler(pushPin, 'click', (e: any) => this.pushpinClicked(pin)); + // this.MicrosoftMaps.Events.addHandler(pushPin, 'dblclick', (e: any) => this.pushpinDblClicked(pushPin, pin)); + this.map_docToPinMap.set(pin, pushPin); + }; + + /* + * Input: pin doc + * Removes pin from annotations + */ + @action + removePushpin = (pinDoc: Doc) => this.removeDocument(pinDoc, this.annotationKey); + + /* + * Removes pushpin from map render + */ + deletePushpin = (pinDoc: Doc) => { + if (!this._unmounting) { + this._bingMap.current.entities.remove(this.map_docToPinMap.get(pinDoc)); + } + this.map_docToPinMap.delete(pinDoc); + this.selectedPin = undefined; + }; + + @action + deleteSelectedPin = undoable(() => { + if (this.selectedPin) { + // Removes filter + Doc.setDocFilter(this.rootDoc, 'latitude', this.selectedPin.latitude, 'remove'); + Doc.setDocFilter(this.rootDoc, 'longitude', this.selectedPin.longitude, 'remove'); + Doc.setDocFilter(this.rootDoc, '-linkedTo', Field.toString(DocCast(this.selectedPin.mapPin)), 'removeAll'); + + this.removePushpin(this.selectedPin); + } + MapAnchorMenu.Instance.fadeOut(true); + document.removeEventListener('pointerdown', this.tryHideMapAnchorMenu, true); + }, 'delete pin'); + + tryHideMapAnchorMenu = (e: PointerEvent) => { + let target = document.elementFromPoint(e.x, e.y); + while (target) { + if (target === MapAnchorMenu.top.current) return; + target = target.parentElement; + } + e.stopPropagation(); + e.preventDefault(); + MapAnchorMenu.Instance.fadeOut(true); + document.removeEventListener('pointerdown', this.tryHideMapAnchorMenu, true); + }; + + @action + centerOnSelectedPin = () => { + if (this.selectedPin) { + this.dataDoc.latitude = this.selectedPin.latitude; + this.dataDoc.longitude = this.selectedPin.longitude; + this.dataDoc.map = this.selectedPin.map ?? ''; + this.bingSearchBarContents = this.selectedPin.map; + } + MapAnchorMenu.Instance.fadeOut(true); + document.removeEventListener('pointerdown', this.tryHideMapAnchorMenu); + }; + + /** + * View options for bing maps + */ bingViewOptions = { - center: { latitude: defaultCenter.lat, longitude: defaultCenter.lng }, + // center: { latitude: this.dataDoc.latitude ?? defaultCenter.lat, longitude: this.dataDoc.longitude ?? defaultCenter.lng }, + zoom: this.dataDoc.latitude ?? 10, mapTypeId: 'grayscale', }; + + /** + * Map options + */ bingMapOptions = { navigationBarMode: 'square', + backgroundColor: '#f1f3f4', + enableInertia: true, + supportedMapTypes: ['grayscale', 'canvasLight'], + disableMapTypeSelectorMouseOver: true, + // showScalebar:true + // disableRoadView:true, + // disableBirdseye:true + streetsideOptions: { + showProblemReporting: false, + showCurrentAddress: false, + }, }; - bingMapReady = (map: any) => (this._bingMap = map.map); + + @action + searchbarOnEdit = (newText: string) => (this.bingSearchBarContents = newText); + + recolorPin = (pin: Doc, color?: string) => { + this._bingMap.current.entities.remove(this.map_docToPinMap.get(pin)); + this.map_docToPinMap.delete(pin); + const newpin = new this.MicrosoftMaps.Pushpin(new this.MicrosoftMaps.Location(pin.latitude, pin.longitude), color ? { color } : {}); + this.MicrosoftMaps.Events.addHandler(newpin, 'click', (e: any) => this.pushpinClicked(pin)); + this._bingMap.current.entities.push(newpin); + this.map_docToPinMap.set(pin, newpin); + }; + + /* + * Called when BingMap is first rendered + * Initializes starting values + */ + @observable _mapReady = false; + @action + bingMapReady = (map: any) => { + this._mapReady = true; + this._bingMap = map.map; + if (!this._bingMap.current) { + alert('NO Map!?'); + } + this.MicrosoftMaps.Events.addHandler(this._bingMap.current, 'click', this.mapOnClick); + this.MicrosoftMaps.Events.addHandler(this._bingMap.current, 'viewchangeend', undoable(this.mapRecentered, 'Map Layout Change')); + this.MicrosoftMaps.Events.addHandler(this._bingMap.current, 'maptypechanged', undoable(this.updateMapType, 'Map ViewType Change')); + + this._disposers.mapLocation = reaction( + () => this.rootDoc.map, + mapLoc => (this.bingSearchBarContents = mapLoc), + { fireImmediately: true } + ); + this._disposers.highlight = reaction( + () => this.allAnnotations.map(doc => doc[Highlight]), + () => { + const allConfigPins = this.allAnnotations.map(doc => ({ doc, pushpin: DocCast(doc.mapPin) })).filter(pair => pair.pushpin); + allConfigPins.forEach(({ doc, pushpin }) => { + if (!pushpin[Highlight] && this.map_pinHighlighted.get(pushpin)) { + this.recolorPin(pushpin); + this.map_pinHighlighted.delete(pushpin); + } + }); + allConfigPins.forEach(({ doc, pushpin }) => { + if (doc[Highlight] && !this.map_pinHighlighted.get(pushpin)) { + this.recolorPin(pushpin, 'orange'); + this.map_pinHighlighted.set(pushpin, true); + } + }); + }, + { fireImmediately: true } + ); + + this._disposers.location = reaction( + () => ({ lat: this.rootDoc.latitude, lng: this.rootDoc.longitude, zoom: this.rootDoc.map_zoom, mapType: this.rootDoc.map_type }), + locationObject => { + // if (this._bingMap.current) + try { + locationObject?.zoom && + this._bingMap.current?.setView({ + mapTypeId: locationObject.mapType, + zoom: locationObject.zoom, + center: new this.MicrosoftMaps.Location(locationObject.lat, locationObject.lng), + }); + } catch (e) { + console.log(e); + } + }, + { fireImmediately: true } + ); + }; + + dragToggle = (e: React.PointerEvent) => { + let dragClone: HTMLDivElement | undefined; + + setupMoveUpEvents( + e, + e, + e => { + if (!dragClone) { + dragClone = this._dragRef.current?.cloneNode(true) as HTMLDivElement; + dragClone.style.position = 'absolute'; + dragClone.style.zIndex = '10000'; + DragManager.Root().appendChild(dragClone); + } + dragClone.style.transform = `translate(${e.clientX - 15}px, ${e.clientY - 15}px)`; + return false; + }, + e => { + if (!dragClone) return; + DragManager.Root().removeChild(dragClone); + let target = document.elementFromPoint(e.x, e.y); + while (target) { + if (target === this._ref.current) { + const cpt = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY); + const x = cpt[0] - (this.props.PanelWidth() - this.sidebarWidth()) / 2; + const y = cpt[1] - 32 /* height of search bar */ - this.props.PanelHeight() / 2; + const location = this._bingMap.current.tryPixelToLocation(new this.MicrosoftMaps.Point(x, y)); + this.createPushpin(location.latitude, location.longitude); + break; + } + target = target.parentElement; + } + }, + e => { + const createPin = () => this.createPushpin(this.rootDoc.latitude, this.rootDoc.longitude, this.rootDoc.map); + if (this.bingSearchBarContents) { + this.bingSearch().then(createPin); + } else createPin(); + } + ); + }; + + searchbarKeyDown = (e: any) => e.key === 'Enter' && this.bingSearch(); + + static _firstRender = true; + static _rerenderDelay = 0; + _rerenderTimeout: any; render() { + // bcz: no idea what's going on here, but bings maps have some kind of bug + // such that we need to delay rendering a second map on startup until the first map is rendered. + this.rootDoc[DocCss]; + if (MapBox._firstRender) { + MapBox._firstRender = false; + MapBox._rerenderDelay = 500; + } else if (MapBox._rerenderDelay) { + // prettier-ignore + this._rerenderTimeout = this._rerenderTimeout ?? + setTimeout(action(() => { + MapBox._rerenderDelay = 0; + this.rootDoc[DocCss] = this.rootDoc[DocCss] + 1; + }), MapBox._rerenderDelay); + return null; + } + const renderAnnotations = (childFilters?: () => string[]) => null; return ( <div className="mapBox" ref={this._ref}> @@ -607,47 +710,86 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps onWheel={e => e.stopPropagation()} onPointerDown={async e => { e.button === 0 && !e.ctrlKey && e.stopPropagation(); - // just a simple test of bing maps geocode api - // const loc = await this.bingGeocode(this._bingMap, 'Philadelphia, PA'); - // this._bingMap.current.setView({ - // mapTypeId: this.MicrosoftMaps.MapTypeId.aerial, - // center: new this.MicrosoftMaps.Location(loc.latitude, loc.longitude), - // zoom: 15, - // }); }} - style={{ width: `calc(100% - ${this.layout_sidebarWidthPercent})`, pointerEvents: this.pointerEvents() }}> + style={{ width: `calc(100% - ${this.sidebarWidthPercent})`, pointerEvents: this.pointerEvents() }}> <div style={{ mixBlendMode: 'multiply' }}>{renderAnnotations(this.transparentFilter)}</div> {renderAnnotations(this.opaqueFilter)} {SnappingManager.GetIsDragging() ? null : renderAnnotations()} - {this.annotationLayer} - - {!MapBox.UseBing ? null : <BingMapsReact onMapReady={this.bingMapReady} bingMapsKey={bingApiKey} height="100%" mapOptions={this.bingMapOptions} width="100%" viewOptions={this.bingViewOptions} />} - <div style={{ display: MapBox.UseBing ? 'none' : undefined }}> - <GoogleMap mapContainerStyle={mapContainerStyle} onZoomChanged={this.zoomChanged} onCenterChanged={this.centered} onLoad={this.loadHandler} options={mapOptions}> - <Autocomplete onLoad={this.setSearchBox} onPlaceChanged={this.handlePlaceChanged}> - <input className="mapBox-input" ref={this.inputRef} type="text" onKeyDown={e => e.stopPropagation()} placeholder="Enter location" /> - </Autocomplete> - - {this.renderMarkers()} - {this.allMapMarkers - .filter(marker => marker.infoWindowOpen) - .map(marker => ( - <MapBoxInfoWindow - key={marker[Id]} - {...this.props} - setContentView={emptyFunction} - place={marker} - markerMap={this.markerMap} - PanelWidth={this.infoWidth} - PanelHeight={this.infoHeight} - moveDocument={this.moveDocument} - isAnyChildContentActive={this.isAnyChildContentActive} - whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} - /> - ))} - {/* {this.handleMapCenter(this._map)} */} - </GoogleMap> + + <div className="mapBox-searchbar"> + <EditableText + // editing + setVal={(newText: string | number) => typeof newText === 'string' && this.searchbarOnEdit(newText)} + onEnter={e => this.bingSearch()} + placeholder={this.bingSearchBarContents || 'enter city/zip/...'} + textAlign="center" + /> + <IconButton + icon={ + <svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="magnifying-glass" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" color="#DFDFDF"> + <path + fill="currentColor" + d="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352a144 144 0 1 0 0-288 144 144 0 1 0 0 288z"></path> + </svg> + } + onClick={this.bingSearch} + type={Type.TERT} + /> + <div style={{ width: 30, height: 30 }} ref={this._dragRef} onPointerDown={this.dragToggle}> + <Button tooltip="drag to place a pushpin" icon={<FontAwesomeIcon size={'lg'} icon={'bullseye'} />} /> + </div> + </div> + + <BingMapsReact + onMapReady={this.bingMapReady} // + bingMapsKey={bingApiKey} + height="100%" + mapOptions={this.bingMapOptions} + width="100%" + viewOptions={this.bingViewOptions} + /> + <div> + {!this._mapReady + ? null + : this.allAnnotations + .filter(anno => !anno.layout_unrendered) + .map(pushpin => ( + <DocumentView + {...this.props} + renderDepth={this.props.renderDepth + 1} + Document={pushpin} + DataDoc={undefined} + PanelWidth={returnOne} + PanelHeight={returnOne} + NativeWidth={returnOne} + NativeHeight={returnOne} + onKey={undefined} + onDoubleClick={undefined} + onBrowseClick={undefined} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + isDocumentActive={returnFalse} + isContentActive={returnFalse} + addDocTab={returnFalse} + ScreenToLocalTransform={Transform.Identity} + fitContentsToBox={undefined} + focus={returnOne} + /> + ))} </div> + {/* <MapBoxInfoWindow + key={Docs.Create.MapMarkerDocument(NumCast(40), NumCast(40), false, [], {})[Id]} + {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit} + place={Docs.Create.MapMarkerDocument(NumCast(40), NumCast(40), false, [], {})} + markerMap={this.markerMap} + PanelWidth={this.infoWidth} + PanelHeight={this.infoHeight} + moveDocument={this.moveDocument} + isAnyChildContentActive={this.isAnyChildContentActive} + whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} + /> */} + {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? null : ( <MarqueeAnnotator rootDoc={this.rootDoc} @@ -666,7 +808,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps )} </div> {/* </LoadScript > */} - <div className="mapBox-sidebar" style={{ position: 'absolute', right: 0, height: '100%', width: `${this.layout_sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}> + <div className="mapBox-sidebar" style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}> <SidebarAnnos ref={this._sidebarRef} {...this.props} diff --git a/src/client/views/nodes/MapBox/MapBox2.tsx b/src/client/views/nodes/MapBox/MapBox2.tsx index b3ae8242d..a54bdcd5e 100644 --- a/src/client/views/nodes/MapBox/MapBox2.tsx +++ b/src/client/views/nodes/MapBox/MapBox2.tsx @@ -229,7 +229,7 @@ export class MapBox2 extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps map: map, }); map.panTo(position); - const mapMarker = Docs.Create.MapMarkerDocument(NumCast(position.lat()), NumCast(position.lng()), false, [], {}); + const mapMarker = Docs.Create.PushpinDocument(NumCast(position.lat()), NumCast(position.lng()), false, [], {}); this.addDocument(mapMarker, this.annotationKey); }; @@ -260,7 +260,7 @@ export class MapBox2 extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps // } else { // alert("Your geolocation is not supported by browser.") // }; - map.setZoom(NumCast(this.dataDoc.mapZoom, 2.5)); + map.setZoom(NumCast(this.dataDoc.map_zoom, 2.5)); map.setCenter(new google.maps.LatLng(NumCast(this.dataDoc.mapLat), NumCast(this.dataDoc.mapLng))); setTimeout(() => { if (this._loadPending && this._map.getBounds()) { @@ -292,7 +292,7 @@ export class MapBox2 extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps this._loadPending = false; this.layoutDoc.freeform_fitContentsToBox && this.fitBounds(this._map); } - this.dataDoc.mapZoom = this._map.getZoom(); + this.dataDoc.map_zoom = this._map.getZoom(); }; /** @@ -333,7 +333,7 @@ export class MapBox2 extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps if (existingMarker) { Doc.AddDocToList(existingMarker, 'data', doc); } else { - const marker = Docs.Create.MapMarkerDocument(NumCast(doc.lat), NumCast(doc.lng), false, [doc], {}); + const marker = Docs.Create.PushpinDocument(NumCast(doc.lat), NumCast(doc.lng), false, [doc], {}); this.addDocument(marker, this.annotationKey); } } diff --git a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx index 577101445..66c47d131 100644 --- a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx +++ b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx @@ -47,7 +47,9 @@ export class MapBoxInfoWindow extends React.Component<MapBoxInfoWindowProps & Vi 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 ( - <InfoWindow anchor={this.props.markerMap[this.props.place[Id]]} onCloseClick={this.handleInfoWindowClose}> + <InfoWindow + // anchor={this.props.markerMap[this.props.place[Id]]} + onCloseClick={this.handleInfoWindowClose}> <div className="mapbox-infowindow"> <div style={{ width: this.props.PanelWidth(), height: this.props.PanelHeight() }}> <CollectionStackingView diff --git a/src/client/views/nodes/MapBox/MapPushpinBox.tsx b/src/client/views/nodes/MapBox/MapPushpinBox.tsx new file mode 100644 index 000000000..42bada0ef --- /dev/null +++ b/src/client/views/nodes/MapBox/MapPushpinBox.tsx @@ -0,0 +1,34 @@ +import { observer } from 'mobx-react'; +// import { SettingsManager } from '../../../util/SettingsManager'; +import { ViewBoxBaseComponent } from '../../DocComponent'; +import { FieldView, FieldViewProps } from '../FieldView'; +import React = require('react'); +import { computed } from 'mobx'; +import { MapBox } from './MapBox'; + +/** + * Map Pushpin doc class + */ +@observer +export class MapPushpinBox extends ViewBoxBaseComponent<FieldViewProps>() { + public static LayoutString(fieldKey: string) { + return FieldView.LayoutString(MapPushpinBox, fieldKey); + } + componentDidMount() { + this.mapBoxView.addPushpin(this.rootDoc); + } + componentWillUnmount() { + this.mapBoxView.deletePushpin(this.rootDoc); + } + + @computed get mapBoxView() { + return this.props.DocumentView?.()?.props.docViewPath().lastElement()?.ComponentView as MapBox; + } + @computed get mapBox() { + return this.props.DocumentView?.().props.docViewPath().lastElement()?.rootDoc; + } + + render() { + return <div />; + } +} diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx index 83a29f071..fa19caae1 100644 --- a/src/client/views/nodes/ScreenshotBox.tsx +++ b/src/client/views/nodes/ScreenshotBox.tsx @@ -314,7 +314,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl </> </CollectionFreeFormView> </div> - <div style={{ background: SettingsManager.Instance.userColor, position: 'relative', height: this.formattedPanelHeight() }}> + <div style={{ background: SettingsManager.userColor, position: 'relative', height: this.formattedPanelHeight() }}> {!(this.dataDoc[this.fieldKey + '_dictation'] instanceof Doc) ? null : ( <FormattedTextBox {...this.props} @@ -336,8 +336,8 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl </div> </div> {!this.props.isSelected() ? null : ( - <div className="screenshotBox-uiButtons" style={{ background: SettingsManager.Instance.userColor }}> - <div className="screenshotBox-recorder" style={{ color: SettingsManager.Instance.userBackgroundColor, background: SettingsManager.Instance.userVariantColor }} key="snap" onPointerDown={this.toggleRecording}> + <div className="screenshotBox-uiButtons" style={{ background: SettingsManager.userColor }}> + <div className="screenshotBox-recorder" style={{ color: SettingsManager.userBackgroundColor, background: SettingsManager.userVariantColor }} key="snap" onPointerDown={this.toggleRecording}> <FontAwesomeIcon icon="file" size="lg" /> </div> </div> diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 1bd98a3eb..5c2d09b0c 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -26,11 +26,10 @@ import { CollectionStackedTimeline, TrimScope } from '../collections/CollectionS import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent'; -import { DocumentDecorations } from '../DocumentDecorations'; import { MarqueeAnnotator } from '../MarqueeAnnotator'; import { AnchorMenu } from '../pdf/AnchorMenu'; import { StyleProp } from '../StyleProvider'; -import { OpenWhere } from './DocumentView'; +import { DocumentView, OpenWhere } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; import { RecordingBox } from './RecordingBox'; import { PinProps, PresBox } from './trails'; @@ -627,7 +626,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp () => !this._playing && this.Seek(NumCast(this.layoutDoc._layout_currentTimecode)) ); this._disposers.youtubeReactionDisposer = reaction( - () => Doc.ActiveTool === InkTool.None && this.props.isSelected(true) && !SnappingManager.GetIsDragging() && !DocumentDecorations.Instance.Interacting, + () => Doc.ActiveTool === InkTool.None && this.props.isSelected(true) && !SnappingManager.GetIsDragging() && !DocumentView.Interacting, interactive => (iframe.style.pointerEvents = interactive ? 'all' : 'none'), { fireImmediately: true } ); diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index da277826a..bd969b527 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -11,9 +11,10 @@ import { keymap } from 'prosemirror-keymap'; import { Fragment, Mark, Node, Slice } from 'prosemirror-model'; import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from 'prosemirror-state'; import { EditorView } from 'prosemirror-view'; +import { BsMarkdownFill } from 'react-icons/bs'; import { DateField } from '../../../../fields/DateField'; -import { Doc, DocListCast, StrListCast, Field, Opt } from '../../../../fields/Doc'; -import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DocCss, Height, Width, ForceServerWrite, UpdatingFromServer } from '../../../../fields/DocSymbols'; +import { Doc, DocListCast, Field, Opt } from '../../../../fields/Doc'; +import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DocCss, ForceServerWrite, Height, UpdatingFromServer, Width } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; import { InkTool } from '../../../../fields/InkField'; import { List } from '../../../../fields/List'; @@ -25,11 +26,10 @@ import { BoolCast, Cast, DocCast, FieldValue, NumCast, ScriptCast, StrCast } fro import { GetEffectiveAcl, TraceMobx } from '../../../../fields/util'; import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, returnFalse, returnZero, setupMoveUpEvents, smoothScroll, unimplementedFunction, Utils } from '../../../../Utils'; import { GoogleApiClientUtils, Pulls, Pushes } from '../../../apis/google_docs/GoogleApiClientUtils'; -import { gptAPICall, GPTCallType, gptImageCall } from '../../../apis/gpt/GPT'; +import { gptAPICall, GPTCallType } from '../../../apis/gpt/GPT'; import { DocServer } from '../../../DocServer'; import { Docs, DocUtils } from '../../../documents/Documents'; -import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; -import { Networking } from '../../../Network'; +import { CollectionViewType } from '../../../documents/DocumentTypes'; import { DictationManager } from '../../../util/DictationManager'; import { DocumentManager } from '../../../util/DocumentManager'; import { DragManager } from '../../../util/DragManager'; @@ -49,6 +49,7 @@ import { DocumentButtonBar } from '../../DocumentButtonBar'; import { Colors } from '../../global/globalEnums'; import { LightboxView } from '../../LightboxView'; import { AnchorMenu } from '../../pdf/AnchorMenu'; +import { GPTPopup } from '../../pdf/GPTPopup/GPTPopup'; import { SidebarAnnos } from '../../SidebarAnnos'; import { StyleProp } from '../../StyleProvider'; import { DocFocusOptions, DocumentView, DocumentViewInternal, OpenWhere } from '../DocumentView'; @@ -70,7 +71,6 @@ import { schema } from './schema_rts'; import { SummaryView } from './SummaryView'; import applyDevTools = require('prosemirror-dev-tools'); import React = require('react'); -import { GPTPopup, GPTPopupMode } from '../../pdf/GPTPopup/GPTPopup'; const translateGoogleApi = require('translate-google-api'); export const GoogleRef = 'googleDocId'; type PullHandler = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => void; @@ -574,7 +574,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps draggedDoc._freeform_fitContentsToBox = true; Doc.SetContainer(draggedDoc, this.rootDoc); const view = this._editorView!; - view.dispatch(view.state.tr.insert(view.posAtCoords({ left: de.x, top: de.y })!.pos, node)); + try { + const pos = view.posAtCoords({ left: de.x, top: de.y })?.pos; + pos && view.dispatch(view.state.tr.insert(pos, node)); + added = pos ? true : false; // pos will be null if you don't drop onto an actual text location + } catch (e) { + console.log('Drop failed', e); + added = false; + } } } } // otherwise, fall through to outer collection to handle drop @@ -899,7 +906,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps event: () => (this.layoutDoc._layout_autoHeight = !this.layoutDoc._layout_autoHeight), icon: this.Document._layout_autoHeight ? 'lock' : 'unlock', }); - optionItems.push({ description: `show markdown options`, event: RTFMarkup.Instance.open, icon: 'text' }); + optionItems.push({ description: `show markdown options`, event: RTFMarkup.Instance.open, icon: <BsMarkdownFill /> }); !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'eye' }); this._downX = this._downY = Number.NaN; }; @@ -930,7 +937,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps }); generateImage = async () => { - console.log('Generate image from text: ', (this.dataDoc.text as RichTextField)?.Text); GPTPopup.Instance?.setTextAnchor(this.getAnchor(false)); GPTPopup.Instance?.setImgTargetDoc(this.rootDoc); GPTPopup.Instance.addToCollection = this.props.addDocument; @@ -1119,7 +1125,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps componentDidMount() { !this.props.dontSelectOnLoad && this.props.setContentView?.(this); // this tells the DocumentView that this AudioBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link. this._cachedLinks = LinkManager.Links(this.Document); - this._disposers.breakupDictation = reaction(() => DocumentManager.Instance.RecordingEvent, this.breakupDictation); + this._disposers.breakupDictation = reaction(() => Doc.RecordingEvent, this.breakupDictation); this._disposers.layout_autoHeight = reaction( () => this.layout_autoHeight, layout_autoHeight => layout_autoHeight && this.tryUpdateScrollHeight() diff --git a/src/client/views/nodes/generativeFill/GenerativeFill.tsx b/src/client/views/nodes/generativeFill/GenerativeFill.tsx index 9c03600cf..1ec6d6e3f 100644 --- a/src/client/views/nodes/generativeFill/GenerativeFill.tsx +++ b/src/client/views/nodes/generativeFill/GenerativeFill.tsx @@ -1,26 +1,25 @@ -import './GenerativeFill.scss'; -import React = require('react'); -import { useEffect, useRef, useState } from 'react'; -import { APISuccess, ImageUtility } from './generativeFillUtils/ImageHandler'; -import { BrushHandler } from './generativeFillUtils/BrushHandler'; -import { IconButton } from 'browndash-components'; import { Checkbox, FormControlLabel, Slider, TextField } from '@mui/material'; -import { CursorData, ImageDimensions, Point } from './generativeFillUtils/generativeFillInterfaces'; -import { activeColor, canvasSize, eraserColor, freeformRenderSize, newCollectionSize, offsetDistanceY, offsetX } from './generativeFillUtils/generativeFillConstants'; -import { PointerHandler } from './generativeFillUtils/PointerHandler'; -import { IoMdUndo, IoMdRedo } from 'react-icons/io'; -import { MainView } from '../../MainView'; +import { IconButton } from 'browndash-components'; +import { useEffect, useRef, useState } from 'react'; +import { CgClose } from 'react-icons/cg'; +import { IoMdRedo, IoMdUndo } from 'react-icons/io'; import { Doc, DocListCast } from '../../../../fields/Doc'; -import { Networking } from '../../../Network'; -import { Utils } from '../../../../Utils'; -import { DocUtils, Docs } from '../../../documents/Documents'; +import { List } from '../../../../fields/List'; import { NumCast } from '../../../../fields/Types'; +import { Utils } from '../../../../Utils'; +import { Docs, DocUtils } from '../../../documents/Documents'; +import { Networking } from '../../../Network'; import { CollectionDockingView } from '../../collections/CollectionDockingView'; import { OpenWhereMod } from '../DocumentView'; -import Buttons from './GenerativeFillButtons'; -import { List } from '../../../../fields/List'; -import { CgClose } from 'react-icons/cg'; import { ImageBox } from '../ImageBox'; +import './GenerativeFill.scss'; +import Buttons from './GenerativeFillButtons'; +import { BrushHandler } from './generativeFillUtils/BrushHandler'; +import { activeColor, canvasSize, eraserColor, freeformRenderSize, newCollectionSize, offsetDistanceY, offsetX } from './generativeFillUtils/generativeFillConstants'; +import { CursorData, ImageDimensions, Point } from './generativeFillUtils/generativeFillInterfaces'; +import { APISuccess, ImageUtility } from './generativeFillUtils/ImageHandler'; +import { PointerHandler } from './generativeFillUtils/PointerHandler'; +import React = require('react'); enum BrushStyle { ADD, @@ -166,20 +165,27 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD // first load useEffect(() => { - if (!imageEditorSource || imageEditorSource === '') return; - const img = new Image(); - img.src = imageEditorSource; - currImg.current = img; - originalImg.current = img; - img.onload = () => { - const imgWidth = img.naturalWidth; - const imgHeight = img.naturalHeight; - const scale = Math.min(canvasSize / imgWidth, canvasSize / imgHeight); - const width = imgWidth * scale; - const height = imgHeight * scale; - setCanvasDims({ width, height }); + const loadInitial = async () => { + if (!imageEditorSource || imageEditorSource === '') return; + const img = new Image(); + const res = await ImageUtility.urlToBase64(imageEditorSource); + if (!res) return; + img.src = `data:image/png;base64,${res}`; + + img.onload = () => { + currImg.current = img; + originalImg.current = img; + const imgWidth = img.naturalWidth; + const imgHeight = img.naturalHeight; + const scale = Math.min(canvasSize / imgWidth, canvasSize / imgHeight); + const width = imgWidth * scale; + const height = imgHeight * scale; + setCanvasDims({ width, height }); + }; }; + loadInitial(); + // cleanup return () => { setInput(''); diff --git a/src/client/views/nodes/generativeFill/GenerativeFillButtons.tsx b/src/client/views/nodes/generativeFill/GenerativeFillButtons.tsx index 0dfcebea3..10eca358e 100644 --- a/src/client/views/nodes/generativeFill/GenerativeFillButtons.tsx +++ b/src/client/views/nodes/generativeFill/GenerativeFillButtons.tsx @@ -2,7 +2,8 @@ import './GenerativeFillButtons.scss'; import React = require('react'); import ReactLoading from 'react-loading'; import { activeColor } from './generativeFillUtils/generativeFillConstants'; -import { Button, Type } from 'browndash-components'; +import { Button, IconButton, Type } from 'browndash-components'; +import { AiOutlineInfo } from 'react-icons/ai'; interface ButtonContainerProps { getEdit: () => Promise<void>; @@ -35,6 +36,7 @@ const Buttons = ({ loading, getEdit, onReset }: ButtonContainerProps) => { }} /> )} + <IconButton type={Type.SEC} color={activeColor} tooltip="Open Documentation" icon={<AiOutlineInfo size={'16px'} />} onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/features/generativeai/#editing', '_blank')} /> </div> ); }; diff --git a/src/client/views/nodes/generativeFill/generativeFillUtils/ImageHandler.ts b/src/client/views/nodes/generativeFill/generativeFillUtils/ImageHandler.ts index 2ede625f6..47a14135f 100644 --- a/src/client/views/nodes/generativeFill/generativeFillUtils/ImageHandler.ts +++ b/src/client/views/nodes/generativeFill/generativeFillUtils/ImageHandler.ts @@ -283,4 +283,32 @@ export class ImageUtility { } return canvas; }; + + /** + * Converts a url to base64 (tainted canvas workaround) + */ + static urlToBase64 = async (imageUrl: string): Promise<string | undefined> => { + try { + const res = await fetch(imageUrl); + const blob = await res.blob(); + + return new Promise<string>((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => { + const base64Data = reader.result?.toString().split(',')[1]; + if (base64Data) { + resolve(base64Data); + } else { + reject(new Error('Failed to convert.')); + } + }; + reader.onerror = () => { + reject(new Error('Error reading image data')); + }; + reader.readAsDataURL(blob); + }); + } catch (err) { + console.error(err); + } + }; } diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx index 00c12d3d8..b0101d9a7 100644 --- a/src/client/views/nodes/trails/PresBox.tsx +++ b/src/client/views/nodes/trails/PresBox.tsx @@ -3,7 +3,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@material-ui/core'; import { action, computed, IReactionDisposer, observable, ObservableSet, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; -import { Doc, DocListCast, Field, FieldResult, Opt, StrListCast } from '../../../../fields/Doc'; +import { Doc, DocListCast, FieldResult, Opt, StrListCast } from '../../../../fields/Doc'; import { Animation } from '../../../../fields/DocSymbols'; import { Copy, Id } from '../../../../fields/FieldSymbols'; import { InkField } from '../../../../fields/InkField'; @@ -15,7 +15,7 @@ import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Ty import { AudioField } from '../../../../fields/URLField'; import { emptyFunction, emptyPath, returnFalse, returnOne, setupMoveUpEvents, StopEvent } from '../../../../Utils'; import { DocServer } from '../../../DocServer'; -import { Docs, DocUtils } from '../../../documents/Documents'; +import { Docs } from '../../../documents/Documents'; import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; import { DocumentManager } from '../../../util/DocumentManager'; import { ScriptingGlobals } from '../../../util/ScriptingGlobals'; @@ -55,6 +55,7 @@ export interface pinDataTypes { dataview?: boolean; poslayoutview?: boolean; dataannos?: boolean; + map?: boolean; } export interface PinProps { audioRange?: boolean; @@ -411,6 +412,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const inkable = [DocumentType.INK].includes(targetType); const scrollable = [DocumentType.PDF, DocumentType.RTF, DocumentType.WEB].includes(targetType) || target?._type_collection === CollectionViewType.Stacking; const pannable = [DocumentType.IMG, DocumentType.PDF].includes(targetType) || (targetType === DocumentType.COL && target?._type_collection === CollectionViewType.Freeform); + const map = [DocumentType.MAP].includes(targetType); const temporal = [DocumentType.AUDIO, DocumentType.VID].includes(targetType); const clippable = [DocumentType.COMPARISON].includes(targetType); const datarange = [DocumentType.FUNCPLOT].includes(targetType); @@ -420,7 +422,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const filters = true; const pivot = true; const dataannos = false; - return { scrollable, pannable, inkable, type_collection, pivot, filters, temporal, clippable, dataview, datarange, poslayoutview, dataannos }; + return { scrollable, pannable, inkable, type_collection, pivot, map, filters, temporal, clippable, dataview, datarange, poslayoutview, dataannos }; } @action @@ -488,6 +490,28 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { changed = true; } } + if (pinDataTypes?.map || (!pinDataTypes && activeItem.config_latitude !== undefined)) { + if (bestTarget.latitude !== activeItem.config_latitude) { + Doc.SetInPlace(bestTarget, 'latitude', NumCast(activeItem.config_latitude), true); + changed = true; + } + if (bestTarget.longitude !== activeItem.config_longitude) { + Doc.SetInPlace(bestTarget, 'longitude', NumCast(activeItem.config_longitude), true); + changed = true; + } + if (bestTarget.zoom !== activeItem.config_map_zoom) { + Doc.SetInPlace(bestTarget, 'map_zoom', NumCast(activeItem.config_map_zoom), true); + changed = true; + } + if (bestTarget.map_type !== activeItem.config_map_type) { + Doc.SetInPlace(bestTarget, 'map_type', StrCast(activeItem.config_map_type), true); + changed = true; + } + if (bestTarget.map !== activeItem.config_map) { + Doc.SetInPlace(bestTarget, 'map', StrCast(activeItem.config_map), true); + changed = true; + } + } if (pinDataTypes?.temporal || (!pinDataTypes && activeItem.config_clipStart !== undefined)) { if (bestTarget._layout_currentTimecode !== activeItem.config_clipStart) { bestTarget._layout_currentTimecode = activeItem.config_clipStart; @@ -599,7 +623,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const dv = DocumentManager.Instance.getDocumentView(bestTarget); if (dv) { changed = true; - const computedScale = NumCast(activeItem.presZoom, 1) * Math.min(dv.props.PanelWidth() / viewport.width, dv.props.PanelHeight() / viewport.height); + const computedScale = NumCast(activeItem.config_zoom, 1) * Math.min(dv.props.PanelWidth() / viewport.width, dv.props.PanelHeight() / viewport.height); activeItem.presentation_movement === PresMovement.Zoom && (bestTarget._freeform_scale = computedScale); dv.ComponentView?.brushView?.(viewport, transTime); } @@ -668,6 +692,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { pinDoc.config_xRange = undefined; //targetDoc?.xrange; pinDoc.config_yRange = undefined; //targetDoc?.yrange; } + if (pinProps.pinData.map) { + // pinDoc.config_latitude = targetDoc?.latitude; + // pinDoc.config_longitude = targetDoc?.longitude; + pinDoc.config_map_zoom = targetDoc?.map_zoom; + pinDoc.config_map_type = targetDoc?.map_type; + //... + } if (pinProps.pinData.poslayoutview) pinDoc.config_pinLayoutData = new List<string>( DocListCast(targetDoc[fkey] as ObjectField).map(d => @@ -761,7 +792,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const options: DocFocusOptions = { willPan: activeItem.presentation_movement !== PresMovement.None, willZoomCentered: activeItem.presentation_movement === PresMovement.Zoom || activeItem.presentation_movement === PresMovement.Jump || activeItem.presentation_movement === PresMovement.Center, - zoomScale: activeItem.presentation_movement === PresMovement.Center ? 0 : NumCast(activeItem.presZoom, 1), + zoomScale: activeItem.presentation_movement === PresMovement.Center ? 0 : NumCast(activeItem.config_zoom, 1), zoomTime: activeItem.presentation_movement === PresMovement.Jump ? 0 : Math.min(Math.max(effect ? 750 : 500, (effect ? 0.2 : 1) * presTime), presTime), effect: activeItem, noSelect: true, @@ -1405,7 +1436,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { if (change) scale += change; if (scale < 0.01) scale = 0.01; if (scale > 1) scale = 1; - this.selectedArray.forEach(doc => (doc.presZoom = scale)); + this.selectedArray.forEach(doc => (doc.config_zoom = scale)); }; /* @@ -1679,7 +1710,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { }; if (activeItem && this.targetDoc) { const transitionSpeed = activeItem.presentation_transition ? NumCast(activeItem.presentation_transition) / 1000 : 0.5; - const zoom = NumCast(activeItem.presZoom, 1) * 100; + const zoom = NumCast(activeItem.config_zoom, 1) * 100; const effect = activeItem.presentation_effect ? activeItem.presentation_effect : PresMovement.None; return ( <div diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx index b0924888a..6a8b06377 100644 --- a/src/client/views/pdf/AnchorMenu.tsx +++ b/src/client/views/pdf/AnchorMenu.tsx @@ -1,23 +1,20 @@ import React = require('react'); import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Tooltip } from '@material-ui/core'; +import { ColorPicker, Group, IconButton, Popup, Size, Toggle, ToggleType, Type } from 'browndash-components'; import { action, computed, IReactionDisposer, observable, ObservableMap, reaction } from 'mobx'; import { observer } from 'mobx-react'; +import { EditorView } from 'prosemirror-view'; import { ColorState } from 'react-color'; import { Doc, Opt } from '../../../fields/Doc'; +import { StrCast } from '../../../fields/Types'; import { returnFalse, setupMoveUpEvents, unimplementedFunction, Utils } from '../../../Utils'; +import { gptAPICall, GPTCallType } from '../../apis/gpt/GPT'; +import { DocumentType } from '../../documents/DocumentTypes'; import { SelectionManager } from '../../util/SelectionManager'; import { AntimodeMenu, AntimodeMenuProps } from '../AntimodeMenu'; import { LinkPopup } from '../linking/LinkPopup'; -import { ButtonDropdown } from '../nodes/formattedText/RichTextMenu'; -import { gptAPICall, GPTCallType } from '../../apis/gpt/GPT'; -import { GPTPopup, GPTPopupMode } from './GPTPopup/GPTPopup'; -import { LightboxView } from '../LightboxView'; -import { EditorView } from 'prosemirror-view'; import './AnchorMenu.scss'; -import { ColorPicker, Group, IconButton, Popup, Size, Toggle, ToggleType, Type } from 'browndash-components'; -import { StrCast } from '../../../fields/Types'; -import { DocumentType } from '../../documents/DocumentTypes'; +import { GPTPopup, GPTPopupMode } from './GPTPopup/GPTPopup'; @observer export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index 395fbd1ca..c3027f51f 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -46,7 +46,7 @@ interface IViewerProps extends FieldViewProps { sidebarAddDoc: (doc: Doc | Doc[], sidebarKey?: string | undefined) => boolean; loaded?: (nw: number, nh: number, np: number) => void; setPdfViewer: (view: PDFViewer) => void; - anchorMenuClick?: () => undefined | ((anchor: Doc, summarize?: boolean) => void); + anchorMenuClick?: () => undefined | ((anchor: Doc) => void); crop: (region: Doc | undefined, addCrop?: boolean) => Doc | undefined; } diff --git a/src/client/views/search/IconBar.tsx b/src/client/views/search/IconBar.tsx index 6103b245c..540c1b5e1 100644 --- a/src/client/views/search/IconBar.tsx +++ b/src/client/views/search/IconBar.tsx @@ -1,17 +1,15 @@ import { action, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { DocumentType } from "../../documents/DocumentTypes"; -// import "./SearchBox.scss"; -import "./IconBar.scss"; +import { DocumentType } from '../../documents/DocumentTypes'; +import './IconBar.scss'; import { IconButton } from './IconButton'; -import "./IconButton.scss"; +import './IconButton.scss'; export interface IconBarProps { setIcons: (icons: string[]) => void; } - @observer export class IconBar extends React.Component<IconBarProps> { public _allIcons: string[] = [DocumentType.AUDIO, DocumentType.COL, DocumentType.IMG, DocumentType.LINK, DocumentType.PDF, DocumentType.RTF, DocumentType.VID, DocumentType.WEB, DocumentType.MAP]; @@ -32,7 +30,9 @@ export class IconBar extends React.Component<IconBarProps> { } @action.bound - getIcons(): string[] { return this._icons; } + getIcons(): string[] { + return this._icons; + } constructor(props: any) { super(props); @@ -40,29 +40,33 @@ export class IconBar extends React.Component<IconBarProps> { } @action.bound - getList(): string[] { return this.getIcons(); } + getList(): string[] { + return this.getIcons(); + } @action.bound - updateList(newList: string[]) { this.updateIcon(newList); } + updateList(newList: string[]) { + this.updateIcon(newList); + } @action.bound resetSelf = () => { this._resetClicked = true; this.updateList([]); - } + }; @action.bound selectAll = () => { this._selectAllClicked = true; this.updateList(this._allIcons); - } + }; render() { return ( <div className="icon-bar"> - {this._allIcons.map((type: string) => + {this._allIcons.map((type: string) => ( <IconButton key={type.toString()} type={type} /> - )} + ))} </div> ); } diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx index 1ceea697a..6b6b0f2ff 100644 --- a/src/client/views/search/SearchBox.tsx +++ b/src/client/views/search/SearchBox.tsx @@ -19,6 +19,7 @@ import { fetchRecommendations } from '../newlightbox/utils'; import { IRecommendation, Recommendation } from '../newlightbox/components'; import { Colors } from '../global/globalEnums'; import { SettingsManager } from '../../util/SettingsManager'; +import { SearchUtil } from '../../util/SearchUtil'; const DAMPENING_FACTOR = 0.9; const MAX_ITERATIONS = 25; @@ -132,34 +133,6 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { * @param {Doc[]} docs - docs to be searched through recursively * @param {number, Doc => void} func - function to be called on each doc * - * This method iterates through an array of docs and all docs within those docs, calling - * the function func on each doc. - */ - static foreachRecursiveDoc(docs: Doc[], func: (depth: number, doc: Doc) => void) { - let newarray: Doc[] = []; - var depth = 0; - const visited: Doc[] = []; - while (docs.length > 0) { - newarray = []; - docs.filter(d => d && !visited.includes(d)).forEach(d => { - visited.push(d); - const fieldKey = Doc.LayoutFieldKey(d); - const annos = !Field.toString(Doc.LayoutField(d) as Field).includes('CollectionView'); - const data = d[annos ? fieldKey + '_annotations' : fieldKey]; - data && newarray.push(...DocListCast(data)); - const sidebar = d[fieldKey + '_sidebar']; - sidebar && newarray.push(...DocListCast(sidebar)); - func(depth, d); - }); - docs = newarray; - depth++; - } - } - - /** - * @param {Doc[]} docs - docs to be searched through recursively - * @param {number, Doc => void} func - function to be called on each doc - * * This method iterates asynchronously through an array of docs and all docs within those * docs, calling the function func on each doc. */ @@ -213,74 +186,10 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { @action searchCollection(query: string) { this._selectedResult = undefined; - this._results = SearchBox.staticSearchCollection(CollectionDockingView.Instance?.rootDoc, query); + this._results = SearchUtil.SearchCollection(CollectionDockingView.Instance?.rootDoc, query); this.computePageRanks(); } - @action - static staticSearchCollection(rootDoc: Opt<Doc>, query: string) { - const blockedTypes = [DocumentType.PRESELEMENT, DocumentType.CONFIG, DocumentType.KVP, DocumentType.SEARCH, DocumentType.FONTICON, DocumentType.BUTTON, DocumentType.SCRIPTING]; - const blockedKeys = [ - 'x', - 'y', - 'proto', - 'width', - 'layout_autoHeight', - 'acl-Override', - 'acl-Guest', - 'embedContainer', - 'zIndex', - 'height', - 'text_scrollHeight', - 'text_height', - 'cloneFieldFilter', - 'isDataDoc', - 'text_annotations', - 'dragFactory_count', - 'text_noTemplate', - 'proto_embeddings', - 'isSystem', - 'layout_fieldKey', - 'isBaseProto', - 'xMargin', - 'yMargin', - 'links', - 'layout', - 'layout_keyValue', - 'layout_fitWidth', - 'type_collection', - 'title_custom', - 'freeform_panX', - 'freeform_panY', - 'freeform_scale', - ]; - query = query.toLowerCase(); - - const results = new Map<Doc, string[]>(); - if (rootDoc) { - const docs = DocListCast(rootDoc[Doc.LayoutFieldKey(rootDoc)]); - const docIDs: String[] = []; - SearchBox.foreachRecursiveDoc(docs, (depth: number, doc: Doc) => { - const dtype = StrCast(doc.type) as DocumentType; - if (dtype && !blockedTypes.includes(dtype) && !docIDs.includes(doc[Id]) && depth >= 0) { - const hlights = new Set<string>(); - SearchBox.documentKeys(doc).forEach( - key => - Field.toString(doc[key] as Field) - .toLowerCase() - .includes(query) && hlights.add(key) - ); - blockedKeys.forEach(key => hlights.delete(key)); - - if (Array.from(hlights.keys()).length > 0) { - results.set(doc, Array.from(hlights.keys())); - } - } - docIDs.push(doc[Id]); - }); - } - return results; - } /** * This method initializes the page rank of every document to the reciprocal @@ -374,17 +283,6 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { } /** - * @param {Doc} doc - doc for which keys are returned - * - * This method returns a list of a document doc's keys. - */ - static documentKeys(doc: Doc) { - const keys: { [key: string]: boolean } = {}; - Doc.GetAllPrototypes(doc).map(proto => Object.keys(proto).forEach(key => (keys[key] = false))); - return Array.from(Object.keys(keys)); - } - - /** * This method submits a search with the _searchString as its query and updates * the results array accordingly. */ @@ -518,7 +416,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { className={className}> <div className="searchBox-result-title">{title as string}</div> <div className="searchBox-result-type">{formattedType}</div> - <div className="searchBox-result-keys" style={{ color: SettingsManager.Instance.userVariantColor }}> + <div className="searchBox-result-keys" style={{ color: SettingsManager.userVariantColor }}> {result[1].join(', ')} </div> </div> @@ -530,7 +428,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { const recommendationsJSX: JSX.Element[] = this._recommendations.map(props => <Recommendation {...props} />); return ( - <div className="searchBox-container" style={{ pointerEvents: 'all', color: SettingsManager.Instance.userColor, background: SettingsManager.Instance.userBackgroundColor }}> + <div className="searchBox-container" style={{ pointerEvents: 'all', color: SettingsManager.userColor, background: SettingsManager.userBackgroundColor }}> <div className="searchBox-bar"> {isLinkSearch ? null : ( <select name="type" id="searchBox-type" className="searchBox-type" onChange={this.onSelectChange}> diff --git a/src/client/views/topbar/TopBar.tsx b/src/client/views/topbar/TopBar.tsx index c194ede32..68446d4e7 100644 --- a/src/client/views/topbar/TopBar.tsx +++ b/src/client/views/topbar/TopBar.tsx @@ -1,13 +1,14 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Button, IconButton, Size, Type, isDark } from 'browndash-components'; +import { Button, IconButton, isDark, Size, Type } from 'browndash-components'; import { action, computed, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { FaBug, FaCamera, FaStamp } from 'react-icons/fa'; +import { FaBug, FaCamera } from 'react-icons/fa'; import { Doc, DocListCast } from '../../../fields/Doc'; import { AclAdmin, DashVersion } from '../../../fields/DocSymbols'; import { StrCast } from '../../../fields/Types'; import { GetEffectiveAcl } from '../../../fields/util'; +import { CurrentUserUtils } from '../../util/CurrentUserUtils'; import { DocumentManager } from '../../util/DocumentManager'; import { PingManager } from '../../util/PingManager'; import { ReportManager } from '../../util/reportManager/ReportManager'; @@ -15,13 +16,12 @@ import { ServerStats } from '../../util/ServerStats'; import { SettingsManager } from '../../util/SettingsManager'; import { SharingManager } from '../../util/SharingManager'; import { UndoManager } from '../../util/UndoManager'; +import { CollectionDockingView } from '../collections/CollectionDockingView'; import { ContextMenu } from '../ContextMenu'; import { DashboardView } from '../DashboardView'; -import { MainView } from '../MainView'; -import { CollectionDockingView } from '../collections/CollectionDockingView'; import { Colors } from '../global/globalEnums'; +import { DocumentView } from '../nodes/DocumentView'; import './TopBar.scss'; -import { CurrentUserUtils } from '../../util/CurrentUserUtils'; /** * ABOUT: This is the topbar in Dash, which included the current Dashboard as well as access to information on the user @@ -43,7 +43,7 @@ export class TopBar extends React.Component { return StrCast(Doc.UserDoc().userVariantColor, Colors.MEDIUM_BLUE); } @computed get backgroundColor() { - return PingManager.Instance.IsBeating ? SettingsManager.Instance.userBackgroundColor : Colors.MEDIUM_GRAY; + return PingManager.Instance.IsBeating ? SettingsManager.userBackgroundColor : Colors.MEDIUM_GRAY; } @observable happyHeart: boolean = PingManager.Instance.IsBeating; @@ -76,9 +76,7 @@ export class TopBar extends React.Component { <span style={{ color: isDark(this.backgroundColor) ? Colors.LIGHT_BLUE : Colors.MEDIUM_BLUE, fontWeight: 500 }}>dash</span> </div> )} - {Doc.ActiveDashboard && ( - <Button text="Explore" tooltip="Browsing mode for directly navigating to documents" size={Size.SMALL} color={this.color} onClick={action(() => (MainView.Instance._exploreMode = !MainView.Instance._exploreMode))} /> - )} + {Doc.ActiveDashboard && <Button text="Explore" tooltip="Browsing mode for directly navigating to documents" size={Size.SMALL} color={this.color} onClick={action(() => (DocumentView.ExploreMode = !DocumentView.ExploreMode))} />} </div> ); } @@ -144,7 +142,8 @@ export class TopBar extends React.Component { <Button text={GetEffectiveAcl(Doc.ActiveDashboard) === AclAdmin ? 'Share' : 'View Original'} type={Type.TERT} - color={this.variantColor} + color={SettingsManager.userColor} + background={this.variantColor} onClick={() => { SharingManager.Instance.open(undefined, Doc.ActiveDashboard); }} diff --git a/src/client/views/webcam/DashWebRTCVideo.tsx b/src/client/views/webcam/DashWebRTCVideo.tsx index 02e44a793..524492226 100644 --- a/src/client/views/webcam/DashWebRTCVideo.tsx +++ b/src/client/views/webcam/DashWebRTCVideo.tsx @@ -6,8 +6,8 @@ import { observer } from 'mobx-react'; import { Doc } from '../../../fields/Doc'; import { InkTool } from '../../../fields/InkField'; import '../../views/nodes/WebBox.scss'; -import { DocumentDecorations } from '../DocumentDecorations'; import { CollectionFreeFormDocumentViewProps } from '../nodes/CollectionFreeFormDocumentView'; +import { DocumentView } from '../nodes/DocumentView'; import { FieldView, FieldViewProps } from '../nodes/FieldView'; import './DashWebRTCVideo.scss'; import { hangup, initialize, refreshVideos } from './WebCamLogic'; @@ -71,8 +71,8 @@ export class DashWebRTCVideo extends React.Component<CollectionFreeFormDocumentV </div> ); - const frozen = !this.props.isSelected() || DocumentDecorations.Instance.Interacting; - const classname = 'webBox-cont' + (this.props.isSelected() && Doc.ActiveTool === InkTool.None && !DocumentDecorations.Instance.Interacting ? '-interactive' : ''); + const frozen = !this.props.isSelected() || DocumentView.Interacting; + const classname = 'webBox-cont' + (this.props.isSelected() && Doc.ActiveTool === InkTool.None && !DocumentView.Interacting ? '-interactive' : ''); return ( <> diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index ceacb8a08..f17e10d9e 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -7,9 +7,8 @@ import { DocServer } from '../client/DocServer'; import { DocumentType } from '../client/documents/DocumentTypes'; import { LinkManager } from '../client/util/LinkManager'; import { scriptingGlobal, ScriptingGlobals } from '../client/util/ScriptingGlobals'; -import { SelectionManager } from '../client/util/SelectionManager'; import { afterDocDeserialize, autoObject, Deserializable, SerializationHelper } from '../client/util/SerializationHelper'; -import { undoable, UndoManager } from '../client/util/UndoManager'; +import { undoable } from '../client/util/UndoManager'; import { decycle } from '../decycler/decycler'; import * as JSZipUtils from '../JSZipUtils'; import { DashColor, incrementTitleCopy, intersectRect, Utils } from '../Utils'; @@ -48,9 +47,9 @@ import { FieldId, RefField } from './RefField'; import { RichTextField } from './RichTextField'; import { listSpec } from './Schema'; import { ComputedField, ScriptField } from './ScriptField'; -import { Cast, DocCast, FieldValue, NumCast, StrCast, ToConstructor } from './Types'; +import { BoolCast, Cast, DocCast, FieldValue, NumCast, StrCast, ToConstructor } from './Types'; import { AudioField, CsvField, ImageField, PdfField, VideoField, WebField } from './URLField'; -import { deleteProperty, GetEffectiveAcl, getField, getter, makeEditable, makeReadOnly, normalizeEmail, setter, SharingPermissions, containedFieldChangedHandler } from './util'; +import { containedFieldChangedHandler, deleteProperty, GetEffectiveAcl, getField, getter, makeEditable, makeReadOnly, normalizeEmail, setter, SharingPermissions } from './util'; import JSZip = require('jszip'); export namespace Field { export function toKeyValueString(doc: Doc, key: string): string { @@ -157,7 +156,26 @@ export function updateCachedAcls(doc: Doc) { @scriptingGlobal @Deserializable('Doc', updateCachedAcls, ['id']) export class Doc extends RefField { - @observable public static CurrentlyLoading: Doc[]; + @observable public static RecordingEvent = 0; + + // this isn't really used at the moment, but is intended to indicate whether ink stroke are passed through a gesture recognizer + static GetRecognizeGestures() { + return BoolCast(Doc.UserDoc()._recognizeGestures); + } + static SetRecognizeGestures(show: boolean) { + Doc.UserDoc()._recognizeGestures = show; + } + + // + // This controls whether fontIconButtons will display labels under their icons or not + // + static GetShowIconLabels() { + return BoolCast(Doc.UserDoc()._showLabel); + } + static SetShowIconLabels(show: boolean) { + Doc.UserDoc()._showLabel = show; + } + @observable public static CurrentlyLoading: Doc[] = []; // this assignment doesn't work. the actual assignment happens in DocumentManager's constructor // removes from currently loading display @action public static removeCurrentlyLoading(doc: Doc) { @@ -170,9 +188,6 @@ export class Doc extends RefField { // adds doc to currently loading display @action public static addCurrentlyLoading(doc: Doc) { - if (!Doc.CurrentlyLoading) { - Doc.CurrentlyLoading = []; - } if (Doc.CurrentlyLoading.indexOf(doc) === -1) { Doc.CurrentlyLoading.push(doc); } @@ -510,6 +525,13 @@ export namespace Doc { export function IsDelegateField(doc: Doc, fieldKey: string) { return doc && Get(doc, fieldKey, true) !== undefined; } + // + // this will write the value to the key on either the data doc or the embedding doc. The choice + // of where to write it is based on: + // 1) if the embedding Doc already has this field defined on it, then it will be written to the embedding + // 2) if the data doc has the field, then it's written there. + // 3) if neither already has the field, then 'defaultProto' determines whether to write it to the data doc (or the embedding) + // export async function SetInPlace(doc: Doc, key: string, value: Field | undefined, defaultProto: boolean) { if (key.startsWith('_')) key = key.substring(1); const hasProto = Doc.GetProto(doc) !== doc ? Doc.GetProto(doc) : undefined; @@ -1454,21 +1476,35 @@ export namespace Doc { prevLayout === 'icon' && (doc.deiconifyLayout = undefined); doc.layout_fieldKey = deiconify || 'layout'; } - export function setDocRangeFilter(container: Opt<Doc>, key: string, range?: number[]) { + export function setDocRangeFilter(container: Opt<Doc>, key: string, range?: readonly number[], modifiers?: 'remove') { + //, modifiers: 'remove' | 'set' if (!container) return; + const childFiltersByRanges = Cast(container._childFiltersByRanges, listSpec('string'), []); + for (let i = 0; i < childFiltersByRanges.length; i += 3) { if (childFiltersByRanges[i] === key) { + console.log('this is key inside childfilters by range ' + key); childFiltersByRanges.splice(i, 3); + console.log('this is child filters by range ' + childFiltersByRanges); break; } } if (range !== undefined) { + console.log('in doc.ts in set range filter'); childFiltersByRanges.push(key); childFiltersByRanges.push(range[0].toString()); childFiltersByRanges.push(range[1].toString()); container._childFiltersByRanges = new List<string>(childFiltersByRanges); + console.log('this is child filters by range ' + childFiltersByRanges[0] + ',' + childFiltersByRanges[1] + ',' + childFiltersByRanges[2]); + console.log('this is new list ' + container._childFiltersByRange); + } + + if (modifiers) { + childFiltersByRanges.splice(0, 3); + container._childFiltersByRanges = new List<string>(childFiltersByRanges); } + console.log('this is child filters by range END' + childFiltersByRanges[0] + ',' + childFiltersByRanges[1] + ',' + childFiltersByRanges[2]); } export const FilterSep = '::'; @@ -1476,15 +1512,15 @@ export namespace Doc { // filters document in a container collection: // all documents with the specified value for the specified key are included/excluded // based on the modifiers :"check", "x", undefined - export function setDocFilter(container: Opt<Doc>, key: string, value: any, modifiers: 'remove' | 'match' | 'check' | 'x' | 'exists' | 'unset', toggle?: boolean, fieldPrefix?: string, append: boolean = true) { + export function setDocFilter(container: Opt<Doc>, key: string, value: any, modifiers: 'removeAll' | 'remove' | 'match' | 'check' | 'x' | 'exists' | 'unset', toggle?: boolean, fieldPrefix?: string, append: boolean = true) { if (!container) return; const filterField = '_' + (fieldPrefix ? fieldPrefix + '_' : '') + 'childFilters'; const childFilters = StrListCast(container[filterField]); runInAction(() => { for (let i = 0; i < childFilters.length; i++) { const fields = childFilters[i].split(FilterSep); // split key:value:modifier - if (fields[0] === key && (fields[1] === value || modifiers === 'match' || (fields[2] === 'match' && modifiers === 'remove'))) { - if (fields[2] === modifiers && modifiers && fields[1] === value) { + if (fields[0] === key && (fields[1] === value.toString() || modifiers === 'match' || modifiers === 'removeAll' || (fields[2] === 'match' && modifiers === 'remove'))) { + if (fields[2] === modifiers && modifiers && fields[1] === value.toString()) { if (toggle) modifiers = 'remove'; else return; } @@ -1495,7 +1531,7 @@ export namespace Doc { } if (!childFilters.length && modifiers === 'match' && value === undefined) { container[filterField] = undefined; - } else if (modifiers !== 'remove') { + } else if (modifiers !== 'remove' && modifiers !== 'removeAll') { !append && (childFilters.length = 0); childFilters.push(key + FilterSep + value + FilterSep + modifiers); container[filterField] = new List<string>(childFilters); @@ -1582,7 +1618,7 @@ export namespace Doc { case DocumentType.COMPARISON: return 'columns'; case DocumentType.RTF: return 'sticky-note'; case DocumentType.COL: - const folder: IconProp = isOpen === true ? 'folder-open' : isOpen === false ? 'folder' : 'question'; + const folder: IconProp = isOpen === true ? 'folder-open' : isOpen === false ? 'folder' : doc?.title==='Untitled Collection'? 'object-group': 'chalkboard'; const chevron: IconProp = isOpen === true ? 'chevron-down' : isOpen === false ? 'chevron-right' : 'question'; return !doc?.isFolder ? folder : chevron; case DocumentType.WEB: return 'globe-asia'; @@ -1598,6 +1634,10 @@ export namespace Doc { case DocumentType.PDF: return 'file-pdf'; case DocumentType.LINK: return 'link'; case DocumentType.MAP: return 'map-marker-alt'; + case DocumentType.DATAVIZ: return 'chart-bar'; + case DocumentType.EQUATION: return 'calculator'; + case DocumentType.SIMULATION: return 'rocket'; + case DocumentType.CONFIG: return 'question-circle'; default: return 'question'; } } @@ -1805,24 +1845,6 @@ ScriptingGlobals.add(function setInPlace(doc: any, field: any, value: any) { ScriptingGlobals.add(function sameDocs(doc1: any, doc2: any) { return Doc.AreProtosEqual(doc1, doc2); }); -ScriptingGlobals.add(function undo() { - SelectionManager.DeselectAll(); - return UndoManager.Undo(); -}); - -export function ShowUndoStack() { - SelectionManager.DeselectAll(); - var buffer = ''; - UndoManager.undoStack.forEach((batch, i) => { - buffer += 'Batch => ' + UndoManager.undoStackNames[i] + '\n'; - ///batch.forEach(undo => (buffer += ' ' + undo.prop + '\n')); - }); - alert(buffer); -} -ScriptingGlobals.add(function redo() { - SelectionManager.DeselectAll(); - return UndoManager.Redo(); -}); ScriptingGlobals.add(function DOC(id: string) { console.log("Can't parse a document id in a script"); return 'invalid'; @@ -1837,12 +1859,7 @@ ScriptingGlobals.add(function activePresentationItem() { const curPres = Doc.ActivePresentation; return curPres && DocListCast(curPres[Doc.LayoutFieldKey(curPres)])[NumCast(curPres._itemIndex)]; }); -ScriptingGlobals.add(function selectedDocs(container: Doc, excludeCollections: boolean, prevValue: any) { - const docs = SelectionManager.Views() - .map(dv => dv.props.Document) - .filter(d => !Doc.AreProtosEqual(d, container) && !d.annotationOn && d.type !== DocumentType.KVP && (!excludeCollections || d.type !== DocumentType.COL || !Cast(d.data, listSpec(Doc), null))); - return docs.length ? new List(docs) : prevValue; -}); + ScriptingGlobals.add(function setDocFilter(container: Doc, key: string, value: any, modifiers: 'match' | 'check' | 'x' | 'remove') { Doc.setDocFilter(container, key, value, modifiers); }); diff --git a/src/fields/IconField.ts b/src/fields/IconField.ts new file mode 100644 index 000000000..76c4ddf1b --- /dev/null +++ b/src/fields/IconField.ts @@ -0,0 +1,26 @@ +import { Deserializable } from "../client/util/SerializationHelper"; +import { serializable, primitive } from "serializr"; +import { ObjectField } from "./ObjectField"; +import { Copy, ToScriptString, ToString } from "./FieldSymbols"; + +@Deserializable("icon") +export class IconField extends ObjectField { + @serializable(primitive()) + readonly icon: string; + + constructor(icon: string) { + super(); + this.icon = icon; + } + + [Copy]() { + return new IconField(this.icon); + } + + [ToScriptString]() { + return "invalid"; + } + [ToString]() { + return "ICONfield"; + } +} diff --git a/src/fields/PresField.ts b/src/fields/PresField.ts new file mode 100644 index 000000000..f236a04fd --- /dev/null +++ b/src/fields/PresField.ts @@ -0,0 +1,6 @@ +//insert code here +import { ObjectField } from "./ObjectField"; + +export abstract class PresField extends ObjectField { + +}
\ No newline at end of file diff --git a/src/fields/RichTextUtils.ts b/src/fields/RichTextUtils.ts index 24cd078f2..5ecf25e08 100644 --- a/src/fields/RichTextUtils.ts +++ b/src/fields/RichTextUtils.ts @@ -2,20 +2,20 @@ import { AssertionError } from 'assert'; import { docs_v1 } from 'googleapis'; import { Fragment, Mark, Node } from 'prosemirror-model'; import { sinkListItem } from 'prosemirror-schema-list'; -import { Utils, DashColor } from '../Utils'; -import { Docs, DocUtils } from '../client/documents/Documents'; -import { schema } from '../client/views/nodes/formattedText/schema_rts'; +import { EditorState, TextSelection, Transaction } from 'prosemirror-state'; +import { GoogleApiClientUtils } from '../client/apis/google_docs/GoogleApiClientUtils'; import { GooglePhotos } from '../client/apis/google_docs/GooglePhotosClientUtils'; import { DocServer } from '../client/DocServer'; +import { Docs, DocUtils } from '../client/documents/Documents'; import { Networking } from '../client/Network'; import { FormattedTextBox } from '../client/views/nodes/formattedText/FormattedTextBox'; +import { schema } from '../client/views/nodes/formattedText/schema_rts'; +import { DashColor, Utils } from '../Utils'; import { Doc, Opt } from './Doc'; import { Id } from './FieldSymbols'; import { RichTextField } from './RichTextField'; import { Cast, StrCast } from './Types'; import Color = require('color'); -import { EditorState, TextSelection, Transaction } from 'prosemirror-state'; -import { GoogleApiClientUtils } from '../client/apis/google_docs/GoogleApiClientUtils'; export namespace RichTextUtils { const delimiter = '\n'; diff --git a/src/fields/documentSchemas.ts b/src/fields/documentSchemas.ts index e33a17416..8eeb52709 100644 --- a/src/fields/documentSchemas.ts +++ b/src/fields/documentSchemas.ts @@ -1,8 +1,7 @@ -import { makeInterface, createSchema, listSpec } from './Schema'; -import { ScriptField } from './ScriptField'; -import { Doc } from './Doc'; import { DateField } from './DateField'; -import { SchemaHeaderField } from './SchemaHeaderField'; +import { Doc } from './Doc'; +import { createSchema, listSpec, makeInterface } from './Schema'; +import { ScriptField } from './ScriptField'; export const documentSchema = createSchema({ // content properties @@ -26,8 +25,8 @@ export const documentSchema = createSchema({ z: 'number', // z "coordinate" - non-zero specifies the overlay layer of a freeformview zIndex: 'number', // zIndex of a document in a freeform view _layout_scrollTop: 'number', // scroll position of a scrollable document (pdf, text, web) - lat: 'number', - lng: 'number', + latitude: 'number', + longitude: 'number', // appearance properties on the layout '_backgroundGrid-spacing': 'number', // the size of the grid for collection views diff --git a/src/mobile/MobileInterface.tsx b/src/mobile/MobileInterface.tsx index 584a7b432..c16a1c124 100644 --- a/src/mobile/MobileInterface.tsx +++ b/src/mobile/MobileInterface.tsx @@ -584,6 +584,7 @@ export class MobileInterface extends React.Component { y: 400, title: 'Collection ' + dashboardCount, }; + const freeformDoc = Doc.GuestTarget || Docs.Create.FreeformDocument([], freeformOptions); const dashboardDoc = Docs.Create.StandardCollectionDockingDocument([{ doc: freeformDoc, initialWidth: 600 }], { title: `Dashboard ${dashboardCount}` }, id, 'row'); |