aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorEric <ericmabr@gmail.com>2023-08-13 16:08:28 -0400
committerEric <ericmabr@gmail.com>2023-08-13 16:08:28 -0400
commit0020ec69b847c8607affb57babddfddc812dc9b6 (patch)
treee24255039015745d2073806bee97ce449ddb5260 /src
parent7b2553514bb000eb7f618eb0f0d653baee78742c (diff)
parent3b45f1d30a947dc1702ec347b83e98374c5b603c (diff)
Merge branch 'master' into UI_Update_Eric_Ma
Diffstat (limited to 'src')
-rw-r--r--src/.DS_Storebin10244 -> 10244 bytes
-rw-r--r--src/Utils.ts36
-rw-r--r--src/client/DocServer.ts89
-rw-r--r--src/client/Network.ts39
-rw-r--r--src/client/documents/DocumentTypes.ts11
-rw-r--r--src/client/documents/Documents.ts703
-rw-r--r--src/client/goldenLayout.js2
-rw-r--r--src/client/theme.ts (renamed from src/fields/ListSpec.ts)0
-rw-r--r--src/client/util/CaptureManager.scss18
-rw-r--r--src/client/util/CaptureManager.tsx1
-rw-r--r--src/client/util/CurrentUserUtils.ts440
-rw-r--r--src/client/util/DictationManager.ts6
-rw-r--r--src/client/util/DocumentManager.ts82
-rw-r--r--src/client/util/DragManager.ts61
-rw-r--r--src/client/util/DropConverter.ts10
-rw-r--r--src/client/util/GroupManager.scss78
-rw-r--r--src/client/util/GroupManager.tsx109
-rw-r--r--src/client/util/GroupMemberView.scss33
-rw-r--r--src/client/util/GroupMemberView.tsx124
-rw-r--r--src/client/util/Import & Export/DirectoryImportBox.tsx2
-rw-r--r--src/client/util/InteractionUtils.tsx2
-rw-r--r--src/client/util/LinkFollower.ts32
-rw-r--r--src/client/util/LinkManager.ts84
-rw-r--r--src/client/util/PingManager.ts39
-rw-r--r--src/client/util/RTFMarkup.tsx4
-rw-r--r--src/client/util/ReplayMovements.ts1
-rw-r--r--src/client/util/ReportManager.scss88
-rw-r--r--src/client/util/ReportManager.tsx297
-rw-r--r--src/client/util/ScriptManager.ts38
-rw-r--r--src/client/util/SelectionManager.ts17
-rw-r--r--src/client/util/ServerStats.tsx26
-rw-r--r--src/client/util/SettingsManager.scss141
-rw-r--r--src/client/util/SettingsManager.tsx434
-rw-r--r--src/client/util/SharingManager.scss254
-rw-r--r--src/client/util/SharingManager.tsx435
-rw-r--r--src/client/util/TrackMovements.ts3
-rw-r--r--src/client/util/UndoManager.ts80
-rw-r--r--src/client/util/reportManager/ReportManager.scss364
-rw-r--r--src/client/util/reportManager/ReportManager.tsx406
-rw-r--r--src/client/util/reportManager/ReportManagerComponents.tsx381
-rw-r--r--src/client/util/reportManager/reportManagerSchema.ts877
-rw-r--r--src/client/util/reportManager/reportManagerUtils.ts254
-rw-r--r--src/client/views/.DS_Storebin10244 -> 10244 bytes
-rw-r--r--src/client/views/AntimodeMenu.scss32
-rw-r--r--src/client/views/AntimodeMenu.tsx5
-rw-r--r--src/client/views/AudioWaveform.tsx6
-rw-r--r--src/client/views/ContextMenu.scss24
-rw-r--r--src/client/views/ContextMenu.tsx17
-rw-r--r--src/client/views/ContextMenuItem.tsx18
-rw-r--r--src/client/views/DashboardView.scss112
-rw-r--r--src/client/views/DashboardView.tsx150
-rw-r--r--src/client/views/DocComponent.tsx63
-rw-r--r--src/client/views/DocumentButtonBar.tsx4
-rw-r--r--src/client/views/DocumentDecorations.scss103
-rw-r--r--src/client/views/DocumentDecorations.tsx208
-rw-r--r--src/client/views/EditableView.tsx38
-rw-r--r--src/client/views/FilterPanel.scss23
-rw-r--r--src/client/views/FilterPanel.tsx49
-rw-r--r--src/client/views/GestureOverlay.tsx110
-rw-r--r--src/client/views/GlobalKeyHandler.ts87
-rw-r--r--src/client/views/InkControlPtHandles.tsx34
-rw-r--r--src/client/views/InkStrokeProperties.ts1
-rw-r--r--src/client/views/InkTranscription.tsx21
-rw-r--r--src/client/views/InkingStroke.tsx82
-rw-r--r--src/client/views/LightboxView.tsx16
-rw-r--r--src/client/views/Main.scss1
-rw-r--r--src/client/views/Main.tsx17
-rw-r--r--src/client/views/MainView.scss17
-rw-r--r--src/client/views/MainView.tsx115
-rw-r--r--src/client/views/MainViewModal.scss6
-rw-r--r--src/client/views/MainViewModal.tsx7
-rw-r--r--src/client/views/MarqueeAnnotator.tsx15
-rw-r--r--src/client/views/MetadataEntryMenu.tsx119
-rw-r--r--src/client/views/OverlayView.scss5
-rw-r--r--src/client/views/OverlayView.tsx16
-rw-r--r--src/client/views/Palette.scss30
-rw-r--r--src/client/views/Palette.tsx69
-rw-r--r--src/client/views/PreviewCursor.tsx45
-rw-r--r--src/client/views/PropertiesButtons.scss96
-rw-r--r--src/client/views/PropertiesButtons.tsx461
-rw-r--r--src/client/views/PropertiesDocBacklinksSelector.scss2
-rw-r--r--src/client/views/PropertiesDocBacklinksSelector.tsx8
-rw-r--r--src/client/views/PropertiesDocContextSelector.tsx9
-rw-r--r--src/client/views/PropertiesSection.scss24
-rw-r--r--src/client/views/PropertiesSection.tsx64
-rw-r--r--src/client/views/PropertiesView.scss497
-rw-r--r--src/client/views/PropertiesView.tsx1282
-rw-r--r--src/client/views/ScriptingRepl.tsx3
-rw-r--r--src/client/views/SidebarAnnos.scss14
-rw-r--r--src/client/views/SidebarAnnos.tsx33
-rw-r--r--src/client/views/StyleProvider.scss4
-rw-r--r--src/client/views/StyleProvider.tsx202
-rw-r--r--src/client/views/TemplateMenu.tsx9
-rw-r--r--src/client/views/UndoStack.scss9
-rw-r--r--src/client/views/UndoStack.tsx61
-rw-r--r--src/client/views/animationtimeline/Keyframe.tsx10
-rw-r--r--src/client/views/animationtimeline/Timeline.tsx2
-rw-r--r--src/client/views/animationtimeline/Track.tsx42
-rw-r--r--src/client/views/collections/CollectionCarousel3DView.scss14
-rw-r--r--src/client/views/collections/CollectionCarousel3DView.tsx38
-rw-r--r--src/client/views/collections/CollectionCarouselView.tsx8
-rw-r--r--src/client/views/collections/CollectionDockingView.scss14
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx130
-rw-r--r--src/client/views/collections/CollectionMasonryViewFieldRow.tsx55
-rw-r--r--src/client/views/collections/CollectionMenu.scss653
-rw-r--r--src/client/views/collections/CollectionMenu.tsx114
-rw-r--r--src/client/views/collections/CollectionNoteTakingView.tsx139
-rw-r--r--src/client/views/collections/CollectionNoteTakingViewColumn.tsx6
-rw-r--r--src/client/views/collections/CollectionPileView.tsx54
-rw-r--r--src/client/views/collections/CollectionStackedTimeline.scss2
-rw-r--r--src/client/views/collections/CollectionStackedTimeline.tsx51
-rw-r--r--src/client/views/collections/CollectionStackingView.scss1
-rw-r--r--src/client/views/collections/CollectionStackingView.tsx88
-rw-r--r--src/client/views/collections/CollectionStackingViewFieldColumn.tsx5
-rw-r--r--src/client/views/collections/CollectionSubView.tsx81
-rw-r--r--src/client/views/collections/CollectionTimeView.tsx24
-rw-r--r--src/client/views/collections/CollectionTreeView.scss2
-rw-r--r--src/client/views/collections/CollectionTreeView.tsx65
-rw-r--r--src/client/views/collections/CollectionView.tsx15
-rw-r--r--src/client/views/collections/TabDocView.scss17
-rw-r--r--src/client/views/collections/TabDocView.tsx194
-rw-r--r--src/client/views/collections/TreeView.scss64
-rw-r--r--src/client/views/collections/TreeView.tsx206
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx11
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx26
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss12
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx217
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx69
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx50
-rw-r--r--src/client/views/collections/collectionGrid/CollectionGridView.tsx2
-rw-r--r--src/client/views/collections/collectionLinear/CollectionLinearView.scss7
-rw-r--r--src/client/views/collections/collectionLinear/CollectionLinearView.tsx149
-rw-r--r--src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss9
-rw-r--r--src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx40
-rw-r--r--src/client/views/collections/collectionMulticolumn/CollectionMultirowView.scss10
-rw-r--r--src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx41
-rw-r--r--src/client/views/collections/collectionSchema/CollectionSchemaView.scss2
-rw-r--r--src/client/views/collections/collectionSchema/CollectionSchemaView.tsx119
-rw-r--r--src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx5
-rw-r--r--src/client/views/collections/collectionSchema/SchemaRowBox.tsx26
-rw-r--r--src/client/views/collections/collectionSchema/SchemaTableCell.tsx67
-rw-r--r--src/client/views/global/globalCssVariables.scss11
-rw-r--r--src/client/views/global/globalCssVariables.scss.d.ts3
-rw-r--r--src/client/views/global/globalScripts.ts416
-rw-r--r--src/client/views/linking/LinkMenu.tsx2
-rw-r--r--src/client/views/linking/LinkMenuGroup.tsx2
-rw-r--r--src/client/views/linking/LinkMenuItem.tsx4
-rw-r--r--src/client/views/linking/LinkPopup.scss5
-rw-r--r--src/client/views/linking/LinkPopup.tsx9
-rw-r--r--src/client/views/newlightbox/ButtonMenu/ButtonMenu.scss15
-rw-r--r--src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx50
-rw-r--r--src/client/views/newlightbox/ButtonMenu/index.ts1
-rw-r--r--src/client/views/newlightbox/ButtonMenu/utils.ts3
-rw-r--r--src/client/views/newlightbox/ExploreView/ExploreView.scss44
-rw-r--r--src/client/views/newlightbox/ExploreView/ExploreView.tsx32
-rw-r--r--src/client/views/newlightbox/ExploreView/index.ts1
-rw-r--r--src/client/views/newlightbox/ExploreView/utils.ts20
-rw-r--r--src/client/views/newlightbox/Header/LightboxHeader.scss71
-rw-r--r--src/client/views/newlightbox/Header/LightboxHeader.tsx62
-rw-r--r--src/client/views/newlightbox/Header/index.ts1
-rw-r--r--src/client/views/newlightbox/Header/utils.ts4
-rw-r--r--src/client/views/newlightbox/NewLightboxStyles.scss73
-rw-r--r--src/client/views/newlightbox/NewLightboxView.scss34
-rw-r--r--src/client/views/newlightbox/NewLightboxView.tsx388
-rw-r--r--src/client/views/newlightbox/RecommendationList/RecommendationList.scss117
-rw-r--r--src/client/views/newlightbox/RecommendationList/RecommendationList.tsx196
-rw-r--r--src/client/views/newlightbox/RecommendationList/index.ts1
-rw-r--r--src/client/views/newlightbox/RecommendationList/utils.ts9
-rw-r--r--src/client/views/newlightbox/components/EditableText/EditableText.scss34
-rw-r--r--src/client/views/newlightbox/components/EditableText/EditableText.tsx65
-rw-r--r--src/client/views/newlightbox/components/EditableText/index.ts1
-rw-r--r--src/client/views/newlightbox/components/Recommendation/Recommendation.scss176
-rw-r--r--src/client/views/newlightbox/components/Recommendation/Recommendation.tsx102
-rw-r--r--src/client/views/newlightbox/components/Recommendation/index.ts2
-rw-r--r--src/client/views/newlightbox/components/Recommendation/utils.ts23
-rw-r--r--src/client/views/newlightbox/components/SkeletonDoc/SkeletonDoc.scss82
-rw-r--r--src/client/views/newlightbox/components/SkeletonDoc/SkeletonDoc.tsx22
-rw-r--r--src/client/views/newlightbox/components/SkeletonDoc/index.ts1
-rw-r--r--src/client/views/newlightbox/components/SkeletonDoc/utils.ts5
-rw-r--r--src/client/views/newlightbox/components/Template/Template.scss15
-rw-r--r--src/client/views/newlightbox/components/Template/Template.tsx10
-rw-r--r--src/client/views/newlightbox/components/Template/index.ts1
-rw-r--r--src/client/views/newlightbox/components/Template/utils.ts3
-rw-r--r--src/client/views/newlightbox/components/index.ts3
-rw-r--r--src/client/views/newlightbox/utils.ts121
-rw-r--r--src/client/views/nodes/AudioBox.tsx8
-rw-r--r--src/client/views/nodes/ColorBox.tsx5
-rw-r--r--src/client/views/nodes/ComparisonBox.scss2
-rw-r--r--src/client/views/nodes/ComparisonBox.tsx96
-rw-r--r--src/client/views/nodes/DataVizBox/DataVizBox.tsx16
-rw-r--r--src/client/views/nodes/DataVizBox/components/LineChart.tsx5
-rw-r--r--src/client/views/nodes/DataVizBox/components/TableBox.tsx4
-rw-r--r--src/client/views/nodes/DataVizBox/utils/D3Utils.ts1
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx9
-rw-r--r--src/client/views/nodes/DocumentLinksButton.tsx26
-rw-r--r--src/client/views/nodes/DocumentView.scss7
-rw-r--r--src/client/views/nodes/DocumentView.tsx343
-rw-r--r--src/client/views/nodes/EquationBox.scss9
-rw-r--r--src/client/views/nodes/EquationBox.tsx15
-rw-r--r--src/client/views/nodes/FieldView.tsx2
-rw-r--r--src/client/views/nodes/FontIconBox/ButtonInterface.ts (renamed from src/client/views/nodes/button/ButtonInterface.ts)0
-rw-r--r--src/client/views/nodes/FontIconBox/FontIconBadge.scss (renamed from src/client/views/nodes/button/FontIconBadge.scss)5
-rw-r--r--src/client/views/nodes/FontIconBox/FontIconBadge.tsx37
-rw-r--r--src/client/views/nodes/FontIconBox/FontIconBox.scss (renamed from src/client/views/nodes/button/FontIconBox.scss)4
-rw-r--r--src/client/views/nodes/FontIconBox/FontIconBox.tsx403
-rw-r--r--src/client/views/nodes/FunctionPlotBox.tsx10
-rw-r--r--src/client/views/nodes/ImageBox.tsx50
-rw-r--r--src/client/views/nodes/KeyValueBox.tsx10
-rw-r--r--src/client/views/nodes/KeyValuePair.scss4
-rw-r--r--src/client/views/nodes/KeyValuePair.tsx12
-rw-r--r--src/client/views/nodes/LabelBox.tsx10
-rw-r--r--src/client/views/nodes/LinkAnchorBox.tsx6
-rw-r--r--src/client/views/nodes/LinkBox.scss8
-rw-r--r--src/client/views/nodes/LinkDocPreview.tsx24
-rw-r--r--src/client/views/nodes/LoadingBox.scss4
-rw-r--r--src/client/views/nodes/LoadingBox.tsx3
-rw-r--r--src/client/views/nodes/MapBox/MapBox.tsx19
-rw-r--r--src/client/views/nodes/MapBox/MapBox2.tsx15
-rw-r--r--src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx4
-rw-r--r--src/client/views/nodes/PDFBox.tsx40
-rw-r--r--src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.tsx24
-rw-r--r--src/client/views/nodes/QueryBox.scss5
-rw-r--r--src/client/views/nodes/QueryBox.tsx38
-rw-r--r--src/client/views/nodes/RecordingBox/RecordingBox.tsx3
-rw-r--r--src/client/views/nodes/RecordingBox/RecordingView.tsx2
-rw-r--r--src/client/views/nodes/ScreenshotBox.scss36
-rw-r--r--src/client/views/nodes/ScreenshotBox.tsx24
-rw-r--r--src/client/views/nodes/ScriptingBox.tsx10
-rw-r--r--src/client/views/nodes/SliderBox.tsx99
-rw-r--r--src/client/views/nodes/VideoBox.tsx30
-rw-r--r--src/client/views/nodes/WebBox.tsx77
-rw-r--r--src/client/views/nodes/button/FontIconBadge.tsx37
-rw-r--r--src/client/views/nodes/button/FontIconBox.tsx960
-rw-r--r--src/client/views/nodes/button/colorDropdown/ColorDropdown.tsx77
-rw-r--r--src/client/views/nodes/button/colorDropdown/index.ts1
-rw-r--r--src/client/views/nodes/button/textButton/TextButton.tsx30
-rw-r--r--src/client/views/nodes/button/textButton/index.ts1
-rw-r--r--src/client/views/nodes/button/toggleButton/ToggleButton.tsx34
-rw-r--r--src/client/views/nodes/button/toggleButton/index.ts1
-rw-r--r--src/client/views/nodes/formattedText/DashDocView.tsx13
-rw-r--r--src/client/views/nodes/formattedText/DashFieldView.tsx10
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.scss20
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx360
-rw-r--r--src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts17
-rw-r--r--src/client/views/nodes/formattedText/RichTextMenu.tsx14
-rw-r--r--src/client/views/nodes/formattedText/RichTextRules.ts53
-rw-r--r--src/client/views/nodes/importBox/ImportElementBox.tsx38
-rw-r--r--src/client/views/nodes/trails/PresBox.scss1
-rw-r--r--src/client/views/nodes/trails/PresBox.tsx147
-rw-r--r--src/client/views/nodes/trails/PresElementBox.tsx69
-rw-r--r--src/client/views/pdf/AnchorMenu.tsx183
-rw-r--r--src/client/views/pdf/PDFViewer.scss8
-rw-r--r--src/client/views/pdf/PDFViewer.tsx48
-rw-r--r--src/client/views/search/SearchBox.scss64
-rw-r--r--src/client/views/search/SearchBox.tsx84
-rw-r--r--src/client/views/selectedDoc/SelectedDocView.scss3
-rw-r--r--src/client/views/selectedDoc/SelectedDocView.tsx47
-rw-r--r--src/client/views/selectedDoc/index.ts1
-rw-r--r--src/client/views/topbar/TopBar.scss417
-rw-r--r--src/client/views/topbar/TopBar.tsx112
-rw-r--r--src/fields/CursorField.ts41
-rw-r--r--src/fields/Doc.ts447
-rw-r--r--src/fields/DocSymbols.ts5
-rw-r--r--src/fields/FieldLoader.tsx5
-rw-r--r--src/fields/FieldSymbols.ts21
-rw-r--r--src/fields/IconField.ts26
-rw-r--r--src/fields/List.ts485
-rw-r--r--src/fields/ObjectField.ts15
-rw-r--r--src/fields/PresField.ts6
-rw-r--r--src/fields/Proxy.ts4
-rw-r--r--src/fields/Schema.ts78
-rw-r--r--src/fields/SchemaHeaderField.ts14
-rw-r--r--src/fields/Types.ts9
-rw-r--r--src/fields/documentSchemas.ts26
-rw-r--r--src/fields/util.ts411
-rw-r--r--src/mobile/AudioUpload.tsx10
-rw-r--r--src/mobile/ImageUpload.tsx2
-rw-r--r--src/mobile/MobileInterface.tsx22
-rw-r--r--src/mobile/MobileMain.tsx2
-rw-r--r--src/scraping/buxton/narratives/Theme - Chord Kbds.docxbin5701815 -> 0 bytes
-rw-r--r--src/server/ApiManagers/AzureManager.ts67
-rw-r--r--src/server/ApiManagers/UploadManager.ts11
-rw-r--r--src/server/ApiManagers/UserManager.ts16
-rw-r--r--src/server/DashSession/Session/utilities/session_config.ts2
-rw-r--r--src/server/DashUploadUtils.ts70
-rw-r--r--src/server/server_Initialization.ts32
286 files changed, 12847 insertions, 9134 deletions
diff --git a/src/.DS_Store b/src/.DS_Store
index 8f0e384aa..06389d6ae 100644
--- a/src/.DS_Store
+++ b/src/.DS_Store
Binary files differ
diff --git a/src/Utils.ts b/src/Utils.ts
index 73de6d754..7f83ab8f5 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -6,6 +6,7 @@ import { Socket } from 'socket.io';
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;
@@ -15,6 +16,21 @@ export namespace Utils {
return Date.now() - downTime < Utils.CLICK_TIME && Math.abs(x - downX) < Utils.DRAG_THRESHOLD && Math.abs(y - downY) < Utils.DRAG_THRESHOLD;
}
+ export function cleanDocumentType(type: DocumentType) {
+ switch (type) {
+ case DocumentType.IMG:
+ return 'Image';
+ case DocumentType.AUDIO:
+ return 'Audio';
+ case DocumentType.COL:
+ return 'Collection';
+ case DocumentType.RTF:
+ return 'Text';
+ default:
+ return type.charAt(0).toUpperCase() + type.slice(1);
+ }
+ }
+
export function readUploadedFileAsText(inputFile: File) {
const temporaryFileReader = new FileReader();
@@ -60,11 +76,11 @@ export namespace Utils {
});
return returnedUri;
} catch (e) {
- console.log('VideoBox :' + e);
+ console.log('ConvertDataURI :' + e);
}
}
- export function GetScreenTransform(ele?: HTMLElement): { scale: number; translateX: number; translateY: number } {
+ export function GetScreenTransform(ele?: HTMLElement | null): { scale: number; translateX: number; translateY: number } {
if (!ele) {
return { scale: 1, translateX: 1, translateY: 1 };
}
@@ -124,7 +140,7 @@ export namespace Utils {
}
export function colorString(color: ColorState) {
- return color.hex.startsWith('#') ? color.hex + (color.rgb.a ? decimalToHexString(Math.round(color.rgb.a * 255)) : 'ff') : color.hex;
+ return color.hex.startsWith('#') && color.hex.length < 8 ? color.hex + (color.rgb.a ? decimalToHexString(Math.round(color.rgb.a * 255)) : 'ff') : color.hex;
}
export function fromRGBAstr(rgba: string) {
@@ -141,7 +157,7 @@ export namespace Utils {
const isTransparentFunctionHack = 'isTransparent(__value__)';
export const noRecursionHack = '__noRecursion';
- export const noDragsDocFilter = 'noDragDocs:any:check';
+ export const noDragsDocFilter = 'noDragDocs::any::check';
export function IsRecursiveFilter(val: string) {
return !val.includes(noRecursionHack);
}
@@ -150,14 +166,14 @@ export namespace Utils {
}
export function IsTransparentFilter() {
// bcz: isTransparent(__value__) is a hack. it would be nice to have acual functions be parsed, but now Doc.matchFieldValue is hardwired to recognize just this one
- return `backgroundColor:${isTransparentFunctionHack},${noRecursionHack}:check`; // bcz: hack. noRecursion should probably be either another ':' delimited field, or it should be a modifier to the comparision (eg., check, x, etc) field
+ return `backgroundColor::${isTransparentFunctionHack},${noRecursionHack}::check`; // bcz: hack. noRecursion should probably be either another ':' delimited field, or it should be a modifier to the comparision (eg., check, x, etc) field
}
export function IsOpaqueFilter() {
// bcz: isTransparent(__value__) is a hack. it would be nice to have acual functions be parsed, but now Doc.matchFieldValue is hardwired to recognize just this one
- return `backgroundColor:${isTransparentFunctionHack},${noRecursionHack}:x`; // bcz: hack. noRecursion should probably be either another ':' delimited field, or it should be a modifier to the comparision (eg., check, x, etc) field
+ return `backgroundColor::${isTransparentFunctionHack},${noRecursionHack}::x`; // bcz: hack. noRecursion should probably be either another ':' delimited field, or it should be a modifier to the comparision (eg., check, x, etc) field
}
export function IsPropUnsetFilter(prop: string) {
- return `${prop}:any,${noRecursionHack}:unset`;
+ return `${prop}::any,${noRecursionHack}::unset`;
}
export function toRGBAstr(col: { r: number; g: number; b: number; a?: number }) {
@@ -527,6 +543,10 @@ export function returnDefault(): 'default' {
return 'default';
}
+export function return18() {
+ return 18;
+}
+
export function returnFalse() {
return false;
}
@@ -540,7 +560,7 @@ export function returnNone() {
}
export function returnVal(val1?: number, val2?: number) {
- return val1 !== undefined ? val1 : val2 !== undefined ? val2 : 0;
+ return val1 ? val1 : val2 !== undefined ? val2 : 0;
}
export function returnOne() {
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
index ba64f993c..53c7b857a 100644
--- a/src/client/DocServer.ts
+++ b/src/client/DocServer.ts
@@ -1,15 +1,18 @@
import { runInAction } from 'mobx';
import * as rp from 'request-promise';
import * as io from 'socket.io-client';
-import { Doc, Opt, UpdatingFromServer } from '../fields/Doc';
+import { Doc, DocListCast, Opt } from '../fields/Doc';
+import { UpdatingFromServer } from '../fields/DocSymbols';
import { FieldLoader } from '../fields/FieldLoader';
import { HandleUpdate, Id, Parent } from '../fields/FieldSymbols';
import { ObjectField } from '../fields/ObjectField';
import { RefField } from '../fields/RefField';
-import { StrCast } from '../fields/Types';
+import { DocCast, StrCast } from '../fields/Types';
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';
@@ -29,38 +32,51 @@ import { GestureOverlay } from './views/GestureOverlay';
export namespace DocServer {
let _cache: { [id: string]: RefField | Promise<Opt<RefField>> } = {};
- export function QUERY_SERVER_CACHE(title: string) {
+ export function FindDocByTitle(title: string) {
const foundDocId = Array.from(Object.keys(_cache))
.filter(key => _cache[key] instanceof Doc)
.find(key => (_cache[key] as Doc).title === title);
return foundDocId ? (_cache[foundDocId] as Doc) : undefined;
}
- export function UPDATE_SERVER_CACHE(print: boolean = false) {
- if (print) {
- const strings: string[] = [];
- Array.from(Object.keys(_cache)).forEach(key => {
- const doc = _cache[key];
- if (doc instanceof Doc) strings.push(StrCast(doc.author) + ' ' + StrCast(doc.title) + ' ' + StrCast(Doc.GetT(doc, 'title', 'string', true)));
- });
- strings.sort().forEach((str, i) => console.log(i.toString() + ' ' + str));
- }
- const filtered = Array.from(Object.keys(_cache)).filter(key => {
- const doc = _cache[key] as Doc;
- if (!(StrCast(doc.author).includes('.edu') || StrCast(doc.author).includes('.com')) || doc.author == Doc.CurrentUserEmail) return true;
- return false;
+ let cacheDocumentIds = ''; // ; separate string of all documents ids in the user's working set (cached on the server)
+ export let CacheNeedsUpdate = false;
+ export function UPDATE_SERVER_CACHE() {
+ const prototypes = Object.values(DocumentType)
+ .filter(type => type !== DocumentType.NONE)
+ .map(type => _cache[type + 'Proto'])
+ .filter(doc => doc instanceof Doc)
+ .map(doc => doc as Doc);
+ const references = new Set<Doc>(prototypes);
+ Doc.FindReferences(Doc.UserDoc(), references, undefined);
+ DocListCast(DocCast(Doc.UserDoc().myLinkDatabase).data).forEach(link => {
+ if (!references.has(DocCast(link.link_anchor_1)) && !references.has(DocCast(link.link_anchor_2))) {
+ Doc.RemoveDocFromList(DocCast(Doc.UserDoc().myLinkDatabase), 'data', link);
+ }
});
+ LinkManager.userLinkDBs.forEach(linkDb => Doc.FindReferences(linkDb, references, undefined));
+ const filtered = Array.from(references);
+
+ const newCacheUpdate = filtered.map(doc => doc[Id]).join(';');
+ if (newCacheUpdate === cacheDocumentIds) return;
+ cacheDocumentIds = newCacheUpdate;
+
+ // print out cached docs
+ console.log('Set cached docs = ');
+ const is_filtered = filtered.filter(doc => !Doc.IsSystem(doc));
+ const strings = is_filtered.map(doc => StrCast(doc.title) + ' ' + (Doc.IsDataProto(doc) ? '(data)' : '(embedding)'));
+ strings.sort().forEach((str, i) => console.log(i.toString() + ' ' + str));
rp.post(Utils.prepend('/setCacheDocumentIds'), {
body: {
- cacheDocumentIds: filtered.join(';'),
+ cacheDocumentIds,
},
json: true,
});
}
export let _socket: SocketIOClient.Socket;
// this client's distinct GUID created at initialization
- let GUID: string;
+ let USER_ID: string;
// indicates whether or not a document is currently being udpated, and, if so, its id
export enum WriteMode {
@@ -72,10 +88,14 @@ export namespace DocServer {
const fieldWriteModes: { [field: string]: WriteMode } = {};
const docsWithUpdates: { [field: string]: Set<Doc> } = {};
- export var PlaygroundFields: string[];
- export function setPlaygroundFields(livePlaygroundFields: string[]) {
- DocServer.PlaygroundFields = livePlaygroundFields;
- livePlaygroundFields.forEach(f => DocServer.setFieldWriteMode(f, DocServer.WriteMode.Playground));
+ export var PlaygroundFields: string[] = [];
+ export function setLivePlaygroundFields(livePlaygroundFields: string[]) {
+ DocServer.PlaygroundFields.push(...livePlaygroundFields);
+ livePlaygroundFields.forEach(f => DocServer.setFieldWriteMode(f, DocServer.WriteMode.LivePlayground));
+ }
+ export function setPlaygroundFields(playgroundFields: string[]) {
+ DocServer.PlaygroundFields.push(...playgroundFields);
+ playgroundFields.forEach(f => DocServer.setFieldWriteMode(f, DocServer.WriteMode.Playground));
}
export function IsPlaygroundField(field: string) {
return DocServer.PlaygroundFields?.includes(field.replace(/^_/, ''));
@@ -93,7 +113,7 @@ export namespace DocServer {
}
export function getFieldWriteMode(field: string) {
- return Doc.CurrentUserEmail === 'guest' ? WriteMode.LiveReadonly : fieldWriteModes[field] || WriteMode.Default;
+ return Doc.CurrentUserEmail === 'guest' ? WriteMode.LivePlayground : fieldWriteModes[field] || WriteMode.Default;
}
export function registerDocWithCachedUpdate(doc: Doc, field: string, oldValue: any) {
@@ -146,9 +166,9 @@ export namespace DocServer {
export function init(protocol: string, hostname: string, port: number, identifier: string) {
_cache = {};
- GUID = identifier;
+ USER_ID = identifier;
protocol = protocol.startsWith('https') ? 'wss' : 'ws';
- _socket = io.connect(`${protocol}://${hostname}:${port}`);
+ _socket = require('socket.io-client')(`${protocol}://${hostname}:${port}`, { transports: ['websocket'], rejectUnauthorized: false });
// io.connect(`https://7f079dda.ngrok.io`);// if using ngrok, create a special address for the websocket
_GetCachedRefField = _GetCachedRefFieldImpl;
@@ -199,7 +219,7 @@ export namespace DocServer {
}
export function makeEditable() {
- if (_isReadOnly) {
+ if (Control.isReadOnly()) {
location.reload();
// _isReadOnly = false;
// _CreateField = _CreateFieldImpl;
@@ -220,7 +240,7 @@ export namespace DocServer {
* indicating that this client has connected
*/
function onConnection() {
- _socket.emit(MessageStore.Bar.Message, GUID);
+ _socket.emit(MessageStore.Bar.Message, USER_ID);
}
export namespace Util {
@@ -344,7 +364,7 @@ export namespace DocServer {
// i) which documents need to be fetched
// ii) which are already in the process of being fetched
// iii) which already exist in the cache
- for (const id of ids) {
+ for (const id of ids.filter(id => id)) {
const cached = _cache[id];
if (cached === undefined) {
defaultPromises.push({
@@ -373,7 +393,7 @@ export namespace DocServer {
// fields for the given ids. This returns a promise, which, when resolved, indicates that all the JSON serialized versions of
// the fields have been returned from the server
console.log('Requesting ' + requestedIds.length);
- FieldLoader.active && runInAction(() => (FieldLoader.ServerLoadStatus.requested = requestedIds.length));
+ setTimeout(() => runInAction(() => (FieldLoader.ServerLoadStatus.requested = requestedIds.length)));
const serializedFields = await Utils.EmitCallback(_socket, MessageStore.GetRefFields, requestedIds);
// 3) when the serialized RefFields have been received, go head and begin deserializing them into objects.
@@ -383,7 +403,7 @@ export namespace DocServer {
console.log('deserializing ' + serializedFields.length + ' fields');
for (const field of serializedFields) {
processed++;
- if (FieldLoader.active && processed % 150 === 0) {
+ if (processed % 150 === 0) {
runInAction(() => (FieldLoader.ServerLoadStatus.retrieved = processed));
await new Promise(res => setTimeout(res)); // force loading to yield to splash screen rendering to update progress
}
@@ -469,13 +489,14 @@ export namespace DocServer {
* @param field the [RefField] to be serialized and sent to the server to be stored in the database
*/
export function CreateField(field: RefField) {
+ CacheNeedsUpdate = true;
_CreateField(field);
}
function _CreateFieldImpl(field: RefField) {
_cache[field[Id]] = field;
const initialState = SerializationHelper.Serialize(field);
- Utils.Emit(_socket, MessageStore.CreateField, initialState);
+ Doc.CurrentUserEmail !== 'guest' && Utils.Emit(_socket, MessageStore.CreateField, initialState);
}
let _CreateField: (field: RefField) => void = errorFunc;
@@ -495,7 +516,7 @@ export namespace DocServer {
}
function _UpdateFieldImpl(id: string, diff: any) {
- !DocServer.Control.isReadOnly() && Utils.Emit(_socket, MessageStore.UpdateField, { id, diff });
+ !DocServer.Control.isReadOnly() && Doc.CurrentUserEmail !== 'guest' && Utils.Emit(_socket, MessageStore.UpdateField, { id, diff });
}
let _UpdateField: (id: string, diff: any) => void = errorFunc;
@@ -532,11 +553,11 @@ export namespace DocServer {
}
export function DeleteDocument(id: string) {
- Utils.Emit(_socket, MessageStore.DeleteField, id);
+ Doc.CurrentUserEmail !== 'guest' && Utils.Emit(_socket, MessageStore.DeleteField, id);
}
export function DeleteDocuments(ids: string[]) {
- Utils.Emit(_socket, MessageStore.DeleteFields, ids);
+ Doc.CurrentUserEmail !== 'guest' && Utils.Emit(_socket, MessageStore.DeleteFields, ids);
}
function _respondToDeleteImpl(ids: string | string[]) {
diff --git a/src/client/Network.ts b/src/client/Network.ts
index 19eff3b3b..39bf69e32 100644
--- a/src/client/Network.ts
+++ b/src/client/Network.ts
@@ -2,6 +2,11 @@ import { Utils } from '../Utils';
import requestPromise = require('request-promise');
import { Upload } from '../server/SharedMediaTypes';
+/**
+ * Networking is repsonsible for connecting the client to the server. Networking
+ * mainly provides methods that the client can use to begin the process of
+ * interacting with the server, such as fetching or uploading files.
+ */
export namespace Networking {
export async function FetchFromServer(relativeRoute: string) {
return (await fetch(relativeRoute)).text();
@@ -18,20 +23,32 @@ export namespace Networking {
}
/**
+ * FileGuidPair attaches a guid to a file that is being uploaded,
+ * allowing the client to track the upload progress.
+ *
+ * When files are dragged to the canvas, the overWriteDoc's ID is
+ * used as the guid. Otherwise, a new guid is generated.
+ */
+ export interface FileGuidPair {
+ file: File;
+ guid?: string;
+ }
+ /**
* Handles uploading basic file types to server and makes the API call to "/uploadFormData" endpoint
- * with the mapping of GUID to filem as parameters.
+ * with the mapping of guid to files as parameters.
*
- * @param files the files to be uploaded to the server
+ * @param fileguidpairs the files and corresponding guids to be uploaded to the server
+ * @param browndash whether the endpoint should be invoked on the browndash server
* @returns the response as a json from the server
*/
- export async function UploadFilesToServer<T extends Upload.FileInformation = Upload.FileInformation>(files: File | File[]): Promise<Upload.FileResponse<T>[]> {
+ export async function UploadFilesToServer<T extends Upload.FileInformation = Upload.FileInformation>(fileguidpairs: FileGuidPair | FileGuidPair[], browndash?: boolean): Promise<Upload.FileResponse<T>[]> {
const formData = new FormData();
- if (Array.isArray(files)) {
- if (!files.length) {
+ if (Array.isArray(fileguidpairs)) {
+ if (!fileguidpairs.length) {
return [];
}
const maxFileSize = 50000000;
- if (files.some(f => f.size > maxFileSize)) {
+ if (fileguidpairs.some(f => f.file.size > maxFileSize)) {
return new Promise<any>(res =>
res([
{
@@ -41,15 +58,19 @@ export namespace Networking {
])
);
}
- files.forEach(file => formData.append(Utils.GenerateGuid(), file));
+ // If the fileguidpair has a guid to use (From the overwriteDoc) use that guid. Otherwise, generate a new guid.
+ fileguidpairs.forEach(fileguidpair => formData.append(fileguidpair.guid ?? Utils.GenerateGuid(), fileguidpair.file));
} else {
- formData.append(Utils.GenerateGuid(), files);
+ // Handle the case where fileguidpairs is a single file.
+ formData.append(fileguidpairs.guid ?? Utils.GenerateGuid(), fileguidpairs.file);
}
const parameters = {
method: 'POST',
body: formData,
};
- const response = await fetch('/uploadFormData', parameters);
+
+ const endpoint = browndash ? '[insert endpoint allowing local => browndash]' : '/uploadFormData';
+ const response = await fetch(endpoint, parameters);
return response.json();
}
diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts
index 2cfd9e680..f8d129e79 100644
--- a/src/client/documents/DocumentTypes.ts
+++ b/src/client/documents/DocumentTypes.ts
@@ -2,7 +2,7 @@ export enum DocumentType {
NONE = 'none',
// core data types
- RTF = 'rtf',
+ RTF = 'rich text',
IMG = 'image',
WEB = 'web',
COL = 'collection',
@@ -14,36 +14,31 @@ export enum DocumentType {
INK = 'ink',
SCREENSHOT = 'screenshot',
FONTICON = 'fonticonbox',
- FILTER = 'filter',
SEARCH = 'search', // search query
LABEL = 'label', // simple text label
BUTTON = 'button', // onClick button
WEBCAM = 'webcam', // webcam
- MARKER = 'marker', // generic marker document not intended to be viewed independently of its context (e.g., for text selections in PDF/Web/RTF)
- DATE = 'date', // calendar view of a date
+ CONFIG = 'config', // configuration document intended to specify a view layout configuration, but not be directly rendered (e.g., for saving the page# of a PDF, or view transform of a collection)
SCRIPTING = 'script', // script editor
EQUATION = 'equation', // equation editor
FUNCPLOT = 'funcplot', // function plotter
MAP = 'map',
DATAVIZ = 'dataviz',
LOADING = 'loading',
-
+ SIMULATION = 'simulation', //physics simulation
// special purpose wrappers that either take no data or are compositions of lower level types
LINK = 'link',
- LINKANCHOR = 'linkanchor',
IMPORT = 'import',
SLIDER = 'slider',
PRES = 'presentation',
PRESELEMENT = 'preselement',
COLOR = 'color',
YOUTUBE = 'youtube',
- SEARCHITEM = 'searchitem',
COMPARISON = 'comparison',
GROUP = 'group',
PUSHPIN = "pushpin",
- LINKDB = 'linkdb', // database of links ??? why do we have this
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 ca62028c1..af4cf2243 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1,8 +1,9 @@
import { IconProp } from '@fortawesome/fontawesome-svg-core';
-import { action, runInAction } from 'mobx';
+import { action, reaction, runInAction } from 'mobx';
import { basename } from 'path';
import { DateField } from '../../fields/DateField';
-import { Doc, DocListCast, Field, Initializing, Opt, updateCachedAcls } from '../../fields/Doc';
+import { Doc, DocListCast, Field, Opt, updateCachedAcls } from '../../fields/Doc';
+import { Initializing } from '../../fields/DocSymbols';
import { Id } from '../../fields/FieldSymbols';
import { HtmlField } from '../../fields/HtmlField';
import { InkField, PointData } from '../../fields/InkField';
@@ -24,7 +25,7 @@ import { DirectoryImportBox } from '../util/Import & Export/DirectoryImportBox';
import { FollowLinkScript } from '../util/LinkFollower';
import { LinkManager } from '../util/LinkManager';
import { ScriptingGlobals } from '../util/ScriptingGlobals';
-import { undoBatch, UndoManager } from '../util/UndoManager';
+import { undoable, UndoManager } from '../util/UndoManager';
import { CollectionDockingView } from '../views/collections/CollectionDockingView';
import { DimUnit } from '../views/collections/collectionMulticolumn/CollectionMulticolumnView';
import { CollectionView } from '../views/collections/CollectionView';
@@ -33,7 +34,7 @@ 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/button/FontIconBox';
+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';
@@ -50,6 +51,7 @@ 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';
import { ScreenshotBox } from '../views/nodes/ScreenshotBox';
import { ScriptingBox } from '../views/nodes/ScriptingBox';
@@ -109,18 +111,28 @@ class DocInfo extends FInfo {
}
}
class DimInfo extends FInfo {
- fieldType? = 'DimUnit';
+ fieldType? = 'enumeration';
values? = [DimUnit.Pixel, DimUnit.Ratio];
- readOnly = true;
+ readOnly = false;
}
class PEInfo extends FInfo {
- fieldType? = 'pointerEvents';
+ fieldType? = 'enumeration';
values? = ['all', 'none'];
- readOnly = true;
+ readOnly = false;
}
class DAInfo extends FInfo {
- fieldType? = 'dropActionType';
+ fieldType? = 'enumeration';
values? = ['embed', 'copy', 'move', 'same', 'proto', 'none'];
+ readOnly = false;
+}
+class CTypeInfo extends FInfo {
+ fieldType? = 'enumeration';
+ values? = Array.from(Object.keys(CollectionViewType));
+ readOnly = false;
+}
+class DTypeInfo extends FInfo {
+ fieldType? = 'enumeration';
+ values? = Array.from(Object.keys(DocumentType));
readOnly = true;
}
class DateInfo extends FInfo {
@@ -133,276 +145,281 @@ type STRt = StrInfo | string;
type DOCt = DocInfo | Doc;
type DIMt = DimInfo | typeof DimUnit.Pixel | typeof DimUnit.Ratio;
type PEVt = PEInfo | 'none' | 'all';
+type COLLt = CTypeInfo | CollectionViewType;
type DROPt = DAInfo | dropActionType;
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]);
- isSystem?: BOOLt = new BoolInfo('is this a system created/owned doc', true);
- type?: STRt = new StrInfo('type of document', true, Array.from(Object.keys(DocumentType)));
- title?: string;
- creationDate?: DATEt = new DateInfo('date the document was created', true);
- _dropAction?: DROPt = new DAInfo("what should happen to this document when it's dropped somewhere else");
- allowOverlayDrop?: BOOLt = new BoolInfo('can documents be dropped onto this document without using dragging title bar or holding down embed key (ctrl)?', true);
- childDropAction?: DROPt = new DAInfo("what should happen to the source document when it's dropped onto a child of a collection ");
- targetDropAction?: DROPt = new DAInfo('what should happen to the source document when ??? ');
- userColor?: STRt = new StrInfo('color associated with a Dash user (seen in header fields of shared documents)');
- color?: STRt = new StrInfo('foreground color data doc');
- backgroundColor?: STRt = new StrInfo('background color for data doc');
- _layout_autoHeight?: BOOLt = new BoolInfo('whether document automatically resizes vertically to display contents');
- _headerHeight?: NUMt = new NumInfo('height of document header used for displaying title');
- _headerFontSize?: NUMt = new NumInfo('font size of header of custom notes');
- _headerPointerEvents?: PEVt = new PEInfo('types of events the header of a custom text document can consume');
- _freeform_panX?: NUMt = new NumInfo('horizontal pan location of a freeform view');
- _freeform_panY?: NUMt = new NumInfo('vertical pan location of a freeform view');
+ _dimMagnitude?: NUMt = new NumInfo("magnitude of collectionMulti{row,col} element's width or height");
+ _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');
_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');
_nativeHeightUnfrozen?: BOOLt = new BoolInfo('native height can be changed independent of width by dragging decoration resizers');
- _dimMagnitude?: NUMt = new NumInfo("magnitude of collectionMulti{row,col} element's width or height");
- _dimUnit?: DIMt = new DimInfo("units of collectionMulti{row,col} element's width or height - 'px' or '*' for pixels or relative units");
+
+ 'acl-Guest'?: string; // public permissions
+ '_acl-Guest'?: string; // public permissions
+ 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');
+ 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');
+ 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');
+ _undoIgnoreFields?: List<string>; //'fields that should not be added to the undo stack (opacity for Undo/Redo/and sidebar) AND whether modifications to document are undoable (true for linearview menu buttons to prevent open/close from entering undo stack)'
+ undoIgnoreFields?: List<string>; //'fields that should not be added to the undo stack (opacity for Undo/Redo/and sidebar) AND whether modifications to document are undoable (true for linearview menu buttons to prevent open/close from entering undo stack)'
+ _headerHeight?: NUMt = new NumInfo('height of document header used for displaying title');
+ _headerFontSize?: NUMt = new NumInfo('font size of header of custom notes');
+ _headerPointerEvents?: PEVt = new PEInfo('types of events the header of a custom text document can consume');
+ _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_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');
+ layout_hideDocumentButtonBar?: BOOLt = new BoolInfo('whether to hide the document decorations lower button bar when selected');
+ layout_hideLinkAnchors?: BOOLt = new BoolInfo('suppresses link anchor dots from being displayed');
+ layout_hideAllLinks?: BOOLt = new BoolInfo('whether all individual blue anchor dots should be hidden');
+ layout_hideResizeHandles?: BOOLt = new BoolInfo('whether to hide the resize handles when selected');
+ layout_hideLinkButton?: BOOLt = new BoolInfo('whether the blue link counter button should be hidden');
+ 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_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_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)');
- _layoutFitContentsToBox?: BOOLt = new BoolInfo('whether a freeformview should zoom/scale to create a shrinkwrapped view of its content');
- _contentBounds?: List<number>; // the (forced) bounds of the document to display. format is: [left, top, right, bottom]
- _lockedPosition?: boolean; // lock the x,y coordinates of the document so that it can't be dragged
- _lockedTransform?: boolean; // lock the freeform_panx,freeform_pany and scale parameters of the document so that it be panned/zoomed
- _followLinkToggle?: boolean; // whether document, when clicked, toggles display of its link target
+ _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_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_altContentUI?: boolean; // whether to show alternate content button
- _isLightbox?: boolean; // whether a collection acts as a lightbox by opening lightbox links by hiding all other documents in collection besides link target
+ _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
- _layoutScrollTop?: number; // scroll location for pdfs
- _noAutoscroll?: boolean; // whether collections autoscroll when this item is dragged
- _chromeHidden?: boolean; // whether the editing chrome for a document is hidden
- _searchDoc?: boolean; // is this a search document (used to change UI for search results in schema view)
- _forceActive?: boolean; // flag to handle pointer events when not selected (or otherwise active)
- enableDragWhenActive?: boolean; // allow dragging even if document contentts are active (e.g., tree, groups)
- _stayInCollection?: boolean; // whether the document should remain in its collection when someone tries to drag and drop it elsewhere
- _raiseWhenDragged?: boolean; // whether a document is brought to front when dragged.
- _hideContextMenu?: boolean; // whether the context menu can be shown
- _viewType?: string; // sub type of a collection
- viewType?: string; // sub type of a collection
- _gridGap?: number; // gap between items in masonry view
- _freeform_scale?: number; // how much a freeform view has been scaled (zoomed)
- _overflow?: string; // set overflow behavior
- _xMargin?: number; // gap between left edge of document and start of masonry/stacking layouts
- _yMargin?: number; // gap between top edge of dcoument and start of masonry/stacking layouts
- _xPadding?: number;
- _yPadding?: number;
- _itemIndex?: number; // which item index the carousel viewer is showing
- _layout_showSidebar?: boolean; //whether an annotationsidebar should be displayed for text docuemnts
+
+ _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');
_singleLine?: boolean; // whether label box is restricted to one line of text
_createDocOnCR?: boolean; // whether carriage returns and tabs create new text documents
- _minFontSize?: number; // minimum font size for labelBoxes
- _maxFontSize?: number; // maximum font size for labelBoxes
- _columnWidth?: number;
- _columnsHideIfEmpty?: boolean; // whether stacking view column headings should be hidden
- _fontSize?: string;
- _fontFamily?: string;
- _fontWeight?: string;
- _pivotField?: string; // field key used to determine headings for sections in stacking, masonry, pivot views
- _layout_curPage?: number; // current page of a PDF or other? paginated document
- _layout_currentTimecode?: number; // the current timecode of a time-based document (e.g., current time of a video) value is in seconds
- _currentFrame?: number; // the current frame of a frame-based collection (e.g., progressive slide)
- _timecodeToShow?: number; // the time that a document should be displayed (e.g., when an annotation shows up as a video plays)
- _timecodeToHide?: number; // the time that a document should be hidden
- _timelineLabel?: boolean; // whether the document exists on a timeline
+ _columnWidth?: NUMt = new NumInfo('width of table column');
+ _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);
+ _text_fontSize?: string;
+ _text_fontFamily?: string;
+ _text_fontWeight?: string;
+ _pivotField?: string; // field key used to determine headings for sections in stacking, masonry, pivot views
+
+ 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"');
+ 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.');
dragFactory_count?: NUMt = new NumInfo('number of items created from a drag button (used for setting title with incrementing index)', true);
openFactoryLocation?: string; // an OpenWhere value to place the factory created document
openFactoryAsDelegate?: boolean; //
- lat?: NUMt = new NumInfo('latitude of a mapping view');
- lng?: NUMt = new NumInfo('longitude of a mapping view');
zoom?: NUMt = new NumInfo('zoom of a mapping view');
- infoWindowOpen?: boolean;
- author?: string;
- _layout_fieldKey?: string;
- fieldValues?: List<any>; // possible field values used by fieldInfos
- fieldType?: string; // type of afield used by fieldInfos
- unrendered?: boolean; // denotes an annotation that is not rendered with a DocumentView (e.g, rtf/pdf text selections and links to scroll locations in web/pdf)
- 'acl-Public'?: string; // public permissions
- '_acl-Public'?: string; // public permissions
- version?: string; // version identifier for a document
- label?: string;
- hidden?: boolean;
- _hidden?: boolean;
- pointerEvents?: string; // pointer events that the documentview should have
- mediaState?: string; // status of audio/video media document: "pendingRecording", "recording", "paused", "playing"
- recording?: boolean; // whether WebCam is recording or not
- autoPlayAnchors?: boolean; // whether to play audio/video when an anchor is clicked in a stackedTimeline.
- dontPlayLinkOnSelect?: boolean; // whether an audio/video should start playing when a link is followed to it.
- linkSource?: Doc; // the source document for a collection of backlinks
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
- expertMode?: boolean; // something available only in expert (not novice) mode
+ expertMode?: BOOLt = new BoolInfo('something available only in expert (not novice) mode');
+
+ contentPointerEvents?: string; // pointer events allowed for content of a document view. eg. set to "none" in menuSidebar for sharedDocs so that you can select a document, but not interact with its contents
contextMenuFilters?: List<ScriptField>;
contextMenuScripts?: List<ScriptField>;
contextMenuLabels?: List<string>;
contextMenuIcons?: List<string>;
- defaultDoubleClick?: 'ignore' | 'default'; // ignore double clicks, or deafult (undefined) means open document full screen
- waitForDoubleClickToClick?: 'always' | 'never' | 'default'; // whether a click function wait for double click to expire. 'default' undefined = wait only if there's a click handler, "never" = never wait, "always" = alway wait
- dontUndo?: boolean; // whether button clicks should be undoable (this is set to true for Undo/Redo/and sidebar buttons that open the siebar panel)
- layout?: string | Doc; // default layout string for a document
- contentPointerEvents?: string; // pointer events allowed for content of a document view. eg. set to "none" in menuSidebar for sharedDocs so that you can select a document, but not interact with its contents
- childLimitHeight?: number; // whether to limit the height of collection children. 0 - means height can be no bigger than width
+ 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');
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?: boolean; // whether child documents are active when parent is document active
- childDontRegisterViews?: boolean;
- childHideLinkButton?: boolean; // hide link buttons on all children
+ childDocumentsActive?: BOOLt = new BoolInfo('whether child documents are active when parent is document active');
+ childDontRegisterViews?: BOOLt = new BoolInfo('whether child document views should be registered so that they can be found when following links, etc');
+ childHideLinkButton?: BOOLt = new BoolInfo('hide link buttons on all children');
childContextMenuFilters?: List<ScriptField>;
childContextMenuScripts?: List<ScriptField>;
childContextMenuLabels?: List<string>;
childContextMenuIcons?: List<string>;
- followLinkZoom?: boolean; // whether to zoom to the target of a link
- layout_hideLinkButton?: boolean; // whether the blue link counter button should be hidden
- disableDocBrushing?: boolean; // whether to suppress border highlighting
- layout_hideDecorationTitle?: boolean;
- hideOpenButton?: boolean;
- layout_hideResizeHandles?: boolean;
- hideDocumentButtonBar?: boolean;
- hideAllLinks?: boolean; // whether all individual blue anchor dots should be hidden
- isTemplateForField?: string; // the field key for which the containing document is a rendering template
- isTemplateDoc?: boolean;
targetScriptKey?: string; // where to write a template script (used by collections with click templates which need to target onClick, onDoubleClick, etc)
- templates?: List<string>;
- hero?: ImageField; // primary image that best represents a compound document (e.g., for a buxton device document that has multiple images)
- caption?: RichTextField;
- opacity?: number;
- defaultBackgroundColor?: string;
- _layout_autoMoveAnchors?: boolean; // whether link endpoint should move around the edges of a document to make shortest path to other link endpoint
- layout_hideLinkAnchors?: boolean; // suppresses link anchor dots from being displayed
- isFolder?: boolean;
- lastFrame?: number; // the last frame of a frame-based collection (e.g., progressive slide)
- activeFrame?: number; // the active frame of a document in a frame base collection
- appearFrame?: number; // the frame in which the document appears
- viewTransitionTime?: number; // transition duration for view parameters
- presPanX?: number; // panX saved as a view spec
- presPanY?: number; // panY saved as a view spec
+
+ 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)');
+
+ isSystem?: BOOLt = new BoolInfo('is this a system created/owned doc', true);
+ isBaseProto?: BOOLt = new BoolInfo('is doc a base level prototype for data documents as opposed to data documents which are prototypes for layout documents. base protos are not cloned during a deep');
+ isTemplateForField?: string; // the field key for which the containing document is a rendering template
+ isTemplateDoc?: BOOLt = new BoolInfo('is the document a template for creating other documents');
+ isGroup?: BOOLt = new BoolInfo('should collection use a grouping UI behavior');
+ isFolder?: BOOLt = new BoolInfo('is document a tree view folder');
+ _isTimelineLabel?: BOOLt = new BoolInfo('is document a timeline label');
+ _isLightbox?: BOOLt = new BoolInfo('whether a collection acts as a lightbox by opening lightbox links by hiding all other documents in collection besides link target');
+
+ presPanX?: NUMt = new NumInfo('panX saved as a view spec');
+ presPanY?: NUMt = new NumInfo('panY saved as a view spec');
+ presViewScale?: NUMt = new NumInfo('viewScale saved as a view Spec');
+ presTransition?: NUMt = new NumInfo('the time taken for the transition TO a document');
+ presDuration?: NUMt = new NumInfo('the duration of the slide in presentation view');
+ presZoomText?: BOOLt = new BoolInfo('whether text anchors should shown in a larger box when following links to make them stand out');
+
presLat?: NUMt = new NumInfo('latitude of a map'); // latitude of a map
presLong?: NUMt = new NumInfo('longitude of map'); // longitude of map
presZoom?: NUMt = new NumInfo('zoom of map'); // zoom of map
presMapType?:string;
- presViewScale?: number; // viewScale saved as a view Spec
- presTransition?: number; //the time taken for the transition TO a document
- presDuration?: number; //the duration of the slide in presentation view
- presZoomText?: boolean; // whether text anchors should shown in a larger box when following links to make them stand out
- borderRounding?: string;
- boxShadow?: string; // box-shadow css string OR "standard" to use dash standard box shadow
data?: any;
- isBaseProto?: boolean; // this Doc is 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
- dontRegisterView?: boolean;
- lookupField?: ScriptField; // script that returns the value of a field. This script is passed the rootDoc, layoutDoc, field, and container of the document. see PresBox.
+ 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?: string;
- annotationOn?: Doc;
- followLinkToggle?: boolean;
- isGroup?: boolean; // whether a collection should use a grouping UI behavior
- _removeDropProperties?: List<string>; // list of properties that should be removed from a document when it is dropped. e.g., a creator button may be forceActive to allow it be dragged, but the forceActive property can be removed from the dropped document
+ dockingConfig?: STRt = new StrInfo('configuration of golden layout windows (applies only if doc is rendered as a CollectionDockingView)');
+ icon?: string; // icon used by fonticonbox to render button
noteType?: string;
- // BACKGROUND GRID
+
+ // freeform properties
_freeform_backgroundGrid?: boolean;
+ _freeform_scale?: NUMt = new NumInfo('how much a freeform view has been scaled (zoomed)');
+ _freeform_panX?: NUMt = new NumInfo('horizontal pan location of a freeform view');
+ _freeform_panY?: NUMt = new NumInfo('vertical pan location of a freeform view');
+ _freeform_noAutoPan?: BOOLt = new BoolInfo('disables autopanning when this item is dragged');
+ _freeform_noZoom?: BOOLt = new BoolInfo('disables zooming');
//BUTTONS
buttonText?: string;
- iconShape?: string; // shapes of the fonticon border
btnType?: string;
btnList?: List<string>;
docColorBtn?: string;
userColorBtn?: string;
- canClick?: string;
script?: ScriptField;
- numBtnMax?: number;
- numBtnMin?: number;
+ numBtnMax?: NUMt = new NumInfo('maximum value of a number button');
+ numBtnMin?: NUMt = new NumInfo('minimum value of a number button');
switchToggle?: boolean;
badgeValue?: ScriptField;
//LINEAR VIEW
- linearViewIsExpanded?: boolean; // is linear view expanded
- linearViewExpandable?: boolean; // can linear view be expanded
- linearViewToggleButton?: string; // button to open close linear view group
- linearViewSubMenu?: boolean;
- linearBtnWidth?: number;
- flexGap?: number; // Linear view flex gap
+ linearView_IsOpen?: BOOLt = new BoolInfo('is linear view open');
+ linearView_Expandable?: BOOLt = new BoolInfo('can linear view be expanded');
+ linearView_Dropdown?: BOOLt = new BoolInfo('can linear view be opened as a dropdown');
+ linearView_SubMenu?: BOOLt = new BoolInfo('is doc a sub menu of more linear views');
+ flexGap?: NUMt = new NumInfo('Linear view flex gap');
flexDirection?: 'unset' | 'row' | 'column' | 'row-reverse' | 'column-reverse';
- layout_linkView?: Doc; // view template for a link document
- layout_keyValue?: string; // view tempalte for key value docs
link_description?: string; // added for links
link_relationship?: string; // type of relatinoship a link represents
- layout_linkDisplay?: boolean; // whether a link line should be dipslayed between the two link anchors
- layout_linkDisplayArrow?: boolean; // whether to display link's directional arrowhead
+ link_displayLine?: BOOLt = new BoolInfo('whether a link line should be dipslayed between the two link anchors');
+ link_displayArrow?: BOOLt = new BoolInfo("whether to display link's directional arrowhead");
link_anchor_1?: Doc;
link_anchor_2?: Doc;
- link_anchor_1_useLinkSmallAnchor?: boolean; // whether link_anchor_1 of a link should use a miniature anchor dot (as when the anchor is a text selection)
- link_anchor_2_useLinkSmallAnchor?: boolean; // whether link_anchor_1 of a link should use a miniature anchor dot (as when the anchor is a text selection)
- ignoreClick?: boolean;
+ link_autoMoveAnchors?: BOOLt = new BoolInfo('whether link endpoint should move around the edges of a document to make shortest path to other link endpoint');
+ link_anchor_1_useSmallAnchor?: BOOLt = new BoolInfo('whether link_anchor_1 of a link should use a miniature anchor dot (as when the anchor is a text selection)');
+ link_anchor_2_useSmallAnchor?: BOOLt = new BoolInfo('whether link_anchor_1 of a link should use a miniature anchor dot (as when the anchor is a text selection)');
+ link_relationshipList?: List<string>; // for storing different link relationships (when set by user in the link editor)
+ link_relationshipSizes?: List<number>; //stores number of links contained in each relationship
+ link_colorList?: List<string>; // colors of links corresponding to specific link relationships
+ followLinkZoom?: BOOLt = new BoolInfo('whether to zoom to the target of a link');
+ followLinkToggle?: BOOLt = new BoolInfo('whether target of link should be toggled on and off when following a link to it');
+ followLinkLocation?: STRt = new StrInfo('where to open link target when following link');
+ followLinkAnimEffect?: STRt = new StrInfo('animation effect triggered on target of link');
+ followLinkAnimDirection?: STRt = new StrInfo('direction modifier for animation effect');
+
+ ignoreClick?: BOOLt = new BoolInfo('whether clicks on document should be ignored');
onClick?: ScriptField;
onDoubleClick?: ScriptField;
onChildClick?: ScriptField; // script given to children of a collection to execute when they are clicked
onChildDoubleClick?: ScriptField; // script given to children of a collection to execute when they are double clicked
+ onClickScriptDisable?: STRt = new StrInfo('"always" disable click script, "never" disable click script, or default');
+ defaultDoubleClick?: 'ignore' | 'default'; // ignore double clicks, or deafult (undefined) means open document full screen
+ 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.');
+ childDragAction?: DROPt = new DAInfo('what should happen to the child documents when they are dragged from the collection');
dropConverter?: ScriptField; // script to run when documents are dropped on this Document.
+ dropAction?: DROPt = new DAInfo("what should happen to this document when it's dropped somewhere else");
+ _dropAction?: DROPt = new DAInfo("what should happen to this document when it's dropped somewhere else");
+ _dropPropertiesToRemove?: List<string>; // list of properties that should be removed from a document when it is dropped. e.g., a creator button may be forceActive to allow it be dragged, but the forceActive property can be removed from the dropped document
+ cloneFieldFilter?: List<string>; // fields not to copy when the document is clonedclipboard?: Doc;
+ dragWhenActive?: BOOLt = new BoolInfo('should document drag when it is active - e.g., pileView, group');
+ dragAction?: DROPt = new DAInfo('how to drag document when it is active (e.g., tree, groups)');
+ dragFactory_count?: NUMt = new NumInfo('number of items created from a drag button (used for setting title with incrementing index)', true);
dragFactory?: Doc; // document to create when dragging with a suitable onDragStart script
clickFactory?: Doc; // document to create when clicking on a button with a suitable onClick script
onDragStart?: ScriptField; //script to execute at start of drag operation -- e.g., when a "creator" button is dragged this script generates a different document to drop
- cloneFieldFilter?: List<string>; // fields not to copy when the document is clonedclipboard?: Doc;
- filterBoolean?: string;
- data_useCors?: boolean;
- icon?: string;
- target?: Doc; // available for use in scripts as the primary target document
- sourcePanel?: Doc; // panel to display in 'targetContainer' as the result of a button onClick script
- targetContainer?: Doc; // document whose proto will be set to 'panel' as the result of a onClick click script
- searchFileTypes?: List<string>; // file types allowed in a search query
- stroke_width?: number;
- freezeChildren?: string; // whether children are now allowed to be added and or removed from a collection
- treeViewHideTitle?: boolean; // whether to hide the top document title of a tree view
- treeViewHideUnrendered?: boolean; // tells tree view not to display documents that have an 'unrendered' tag unless they also have a treeViewFieldKey tag (presBox)
- treeViewHideHeaderIfTemplate?: boolean; // whether to hide the header for a document in a tree view only if a childLayoutTemplate is provided (presBox)
- treeViewHideHeader?: boolean; // whether to hide the header for a document in a tree view
- treeViewHideHeaderFields?: boolean; // whether to hide the drop down options for tree view items.
+ 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)
+
+ treeViewHideTitle?: BOOLt = new BoolInfo('whether to hide the top document title of a tree view');
+ treeViewHideUnrendered?: BOOLt = new BoolInfo("tells tree view not to display documents that have an 'layout_unrendered' tag unless they also have a treeViewFieldKey tag (presBox)");
+ treeViewHideHeaderIfTemplate?: BOOLt = new BoolInfo('whether to hide the header for a document in a tree view only if a childLayoutTemplate is provided (presBox)');
+ treeViewHideHeader?: BOOLt = new BoolInfo('whether to hide the header for a document in a tree view');
+ treeViewHideHeaderFields?: BOOLt = new BoolInfo('whether to hide the drop down options for tree view items.');
treeViewChildDoubleClick?: ScriptField; //
- // Action Button
- buttonMenu?: boolean; // whether a action button should be displayed
- buttonMenuDoc?: Doc;
- explainer?: string;
-
- treeViewOpenIsTransient?: boolean; // ignores the treeViewOpen Doc flag, allowing a treeViewItem's expand/collapse state to be independent of other views of the same document in the same or any other tree view
- _treeViewOpen?: boolean; // whether this document is expanded in a tree view (note: need _ and regular versions since this can be specified for both proto and layout docs)
- treeViewOpen?: boolean; // whether this document is expanded in a tree view
+ treeViewOpenIsTransient?: BOOLt = new BoolInfo("ignores the treeViewOpen Doc flag, allowing a treeViewItem's expand/collapse state to be independent of other views of the same document in the same or any other tree view");
+ treeViewOpen?: BOOLt = new BoolInfo('whether this document is expanded in a tree view');
treeViewExpandedView?: string; // which field/thing is displayed when this item is opened in tree view
- treeViewExpandedViewLock?: boolean; // whether the expanded view can be changed
+ treeViewExpandedViewLock?: BOOLt = new BoolInfo('whether the expanded view can be changed');
treeViewChecked?: ScriptField; // script to call when a tree view checkbox is checked
- treeViewTruncateTitleWidth?: number;
- treeViewHasOverlay?: boolean; // whether the treeview has an overlay for freeform annotations
+ treeViewTruncateTitleWidth?: NUMt = new NumInfo('maximum width of a treew view title before truncation');
+ treeViewHasOverlay?: BOOLt = new BoolInfo('whether the treeview has an overlay for freeform annotations');
treeViewType?: string; // whether treeview is a Slide, file system, or (default) collection hierarchy
- sidebarColor?: string; // background color of text sidebar
- sidebarViewType?: string; // collection type of text sidebar
- docMaxAutoHeight?: number; // maximum height for newly created (eg, from pasting) text documents
+ treeViewFreezeChildren?: STRt = new StrInfo('set (add, remove, add|remove) to disable adding, removing or both from collection');
+
+ sidebar_color?: string; // background color of text sidebar
+ sidebar_collectionType?: string; // collection type of text sidebar
+
+ data_dashboards?: List<any>; // list of dashboards used in shareddocs;
text?: string;
- textTransform?: string; // is linear view expanded
- letterSpacing?: string; // is linear view expanded
+ textTransform?: string;
+ letterSpacing?: string;
iconTemplate?: string; // name of icon template style
- selectedIndex?: number; // which item in a linear view has been selected using the "thumb doc" ui
+ selectedIndex?: NUMt = new NumInfo("which item in a linear view has been selected using the 'thumb doc' ui");
+
+ fieldValues?: List<any>; // possible values a field can have (used by FieldInfo's only)
+ fieldType?: string; // display type of a field, e.g. string, number, enumeration (used by FieldInfo's only)
+
clipboard?: Doc;
- searchQuery?: string; // for quersyBox
- useLinkSmallAnchor?: boolean; // whether links to this document should use a miniature linkAnchorBox
- border?: string; //for searchbox
hoverBackgroundColor?: string; // background color of a label when hovered
- link_relationshipList?: List<string>; // for storing different link relationships (when set by user in the link editor)
- link_relationshipSizes?: List<number>; //stores number of links contained in each relationship
- linkColorList?: List<string>; // colors of links corresponding to specific link relationships
+ userBackgroundColor?: STRt = new StrInfo('background color associated with a Dash user (seen in header fields of shared documents)');
+ userColor?: STRt = new StrInfo('color associated with a Dash user (seen in header fields of shared documents)');
}
export namespace Docs {
export let newAccount: boolean = false;
@@ -434,6 +451,7 @@ export namespace Docs {
nativeHeightUnfrozen: true,
layout_forceReflow: true,
defaultDoubleClick: 'ignore',
+ systemIcon: 'BsFileEarmarkTextFill',
},
},
],
@@ -455,21 +473,21 @@ export namespace Docs {
DocumentType.IMG,
{
layout: { view: ImageBox, dataField: defaultDataKey },
- options: {},
+ options: { freeform: '', systemIcon: 'BsFileEarmarkImageFill' },
},
],
[
DocumentType.WEB,
{
layout: { view: WebBox, dataField: defaultDataKey },
- options: { _height: 300, _layout_fitWidth: true, nativeDimModifiable: true, nativeHeightUnfrozen: true, waitForDoubleClickToClick: 'always' },
+ options: { _height: 300, _layout_fitWidth: true, nativeDimModifiable: true, nativeHeightUnfrozen: true, waitForDoubleClickToClick: 'always', systemIcon: 'BsGlobe' },
},
],
[
DocumentType.COL,
{
layout: { view: CollectionView, dataField: defaultDataKey },
- options: { _layout_fitWidth: true, _freeform_panX: 0, _freeform_panY: 0, _freeform_scale: 1 },
+ options: { _layout_fitWidth: true, freeform: '', _freeform_panX: 0, _freeform_panY: 0, _freeform_scale: 1, systemIcon: 'BsFillCollectionFill' },
},
],
[
@@ -483,35 +501,35 @@ export namespace Docs {
DocumentType.VID,
{
layout: { view: VideoBox, dataField: defaultDataKey },
- options: { _layout_currentTimecode: 0 },
+ options: { _layout_currentTimecode: 0, systemIcon: 'BsFileEarmarkPlayFill' },
},
],
[
DocumentType.AUDIO,
{
layout: { view: AudioBox, dataField: defaultDataKey },
- options: { _height: 100, layout_fitWidth: true, layout_forceReflow: true, nativeDimModifiable: true },
+ options: { _height: 100, layout_fitWidth: true, layout_forceReflow: true, nativeDimModifiable: true, systemIcon: 'BsFillVolumeUpFill' },
},
],
[
DocumentType.REC,
{
layout: { view: VideoBox, dataField: defaultDataKey },
- options: { _height: 100, backgroundColor: 'pink' },
+ options: { _height: 100, backgroundColor: 'pink', systemIcon: 'BsFillMicFill' },
},
],
[
DocumentType.PDF,
{
layout: { view: PDFBox, dataField: defaultDataKey },
- options: { _layout_curPage: 1, _layout_fitWidth: true, nativeDimModifiable: true, nativeHeightUnfrozen: true },
+ options: { _layout_curPage: 1, _layout_fitWidth: true, nativeDimModifiable: true, nativeHeightUnfrozen: true, systemIcon: 'BsFileEarmarkPdfFill' },
},
],
[
DocumentType.MAP,
{
layout: { view: MapBox, dataField: defaultDataKey },
- options: { _height: 600, _width: 800, nativeDimModifiable: true },
+ options: { _height: 600, _width: 800, nativeDimModifiable: true, systemIcon: 'BsFillPinMapFill' },
},
],
[
@@ -534,31 +552,23 @@ export namespace Docs {
link_description: '',
layout_showCaption: 'link_description',
backgroundColor: 'lightblue', // lightblue is default color for linking dot and link documents text comment area
- _removeDropProperties: new List(['onClick']),
+ _dropPropertiesToRemove: new List(['onClick']),
},
},
],
[
- DocumentType.LINKDB,
- {
- data: new List<Doc>(),
- layout: { view: EmptyBox, dataField: defaultDataKey },
- options: { childDropAction: 'embed', title: 'Global Link Database' },
- },
- ],
- [
DocumentType.SCRIPTDB,
{
data: new List<Doc>(),
layout: { view: EmptyBox, dataField: defaultDataKey },
- options: { childDropAction: 'embed', title: 'Global Script Database' },
+ options: { title: 'Global Script Database' },
},
],
[
DocumentType.SCRIPTING,
{
layout: { view: ScriptingBox, dataField: defaultDataKey },
- options: {},
+ options: { systemIcon: 'BsFileEarmarkCodeFill' },
},
],
[
@@ -578,7 +588,7 @@ export namespace Docs {
DocumentType.EQUATION,
{
layout: { view: EquationBox, dataField: 'text' },
- options: { nativeDimModifiable: true, fontSize: '14px', layout_hideResizeHandles: true, layout_hideDecorationTitle: true },
+ options: { nativeDimModifiable: true, fontSize: '14px', layout_hideResizeHandles: true, layout_hideDecorationTitle: true, systemIcon: 'BsCalculatorFill' }, ///systemIcon: 'BsSuperscript' + BsSubscript
},
],
[
@@ -613,14 +623,14 @@ export namespace Docs {
DocumentType.FONTICON,
{
layout: { view: FontIconBox, dataField: 'icon' },
- options: { defaultDoubleClick: 'ignore', waitForDoubleClickToClick: 'never', enableDragWhenActive: true, layout_hideLinkButton: true, _width: 40, _height: 40 },
+ options: { defaultDoubleClick: 'ignore', waitForDoubleClickToClick: 'never', layout_hideLinkButton: true, _width: 40, _height: 40 },
},
],
[
DocumentType.WEBCAM,
{
layout: { view: RecordingBox, dataField: defaultDataKey },
- options: {},
+ options: { systemIcon: 'BsFillCameraVideoFill' },
},
],
[
@@ -631,10 +641,10 @@ export namespace Docs {
},
],
[
- DocumentType.MARKER,
+ DocumentType.CONFIG,
{
layout: { view: CollectionView, dataField: defaultDataKey },
- options: { layout_hideLinkButton: true, pointerEvents: 'none' },
+ options: { layout_hideLinkButton: true, layout_unrendered: true },
},
],
[
@@ -642,14 +652,14 @@ export namespace Docs {
{
// NOTE: this is unused!! ink fields are filled in directly within the InkDocument() method
layout: { view: InkingStroke, dataField: 'stroke' },
- options: {},
+ options: { systemIcon: 'BsFillPencilFill' },
},
],
[
DocumentType.SCREENSHOT,
{
layout: { view: ScreenshotBox, dataField: defaultDataKey },
- options: { nativeDimModifiable: true, nativeHeightUnfrozen: true },
+ options: { nativeDimModifiable: true, nativeHeightUnfrozen: true, systemIcon: 'BsCameraFill' },
},
],
[
@@ -657,15 +667,14 @@ export namespace Docs {
{
data: '',
layout: { view: ComparisonBox, dataField: defaultDataKey },
- options: { nativeDimModifiable: true, backgroundColor: 'gray', targetDropAction: 'embed' },
+ options: { backgroundColor: 'gray', dropAction: 'move', waitForDoubleClickToClick: 'always', systemIcon: 'BsLayoutSplit' },
},
],
[
DocumentType.GROUPDB,
{
- data: new List<Doc>(),
layout: { view: EmptyBox, dataField: defaultDataKey },
- options: { childDropAction: 'embed', title: 'Global Group Database' },
+ options: { title: 'Global Group Database' },
},
],
[
@@ -690,6 +699,29 @@ export namespace Docs {
},
],
[
+ DocumentType.SIMULATION,
+ {
+ data: '',
+ layout: { view: PhysicsSimulationBox, dataField: defaultDataKey, _width: 1000, _height: 800 },
+ options: {
+ _height: 100,
+ layout_forceReflow: true,
+ nativeHeightUnfrozen: true,
+ mass1: '',
+ mass2: '',
+ nativeDimModifiable: true,
+ position: '',
+ acceleration: '',
+ pendulum: '',
+ spring: '',
+ wedge: '',
+ simulation: '',
+ review: '',
+ systemIcon: 'BsShareFill',
+ },
+ },
+ ],
+ [
DocumentType.PUSHPIN,
{
layout: { view: MapPushpinBox, dataField: defaultDataKey },
@@ -701,7 +733,7 @@ export namespace Docs {
const suffix = 'Proto';
/**
- * This function loads or initializes the prototype for each docment type.
+ * This function loads or initializes the prototype for each document type.
*
* This is an asynchronous function because it has to attempt
* to fetch the prototype documents from the server.
@@ -717,7 +749,7 @@ export namespace Docs {
.filter(type => type !== DocumentType.NONE)
.map(type => type + suffix);
// fetch the actual prototype documents from the server
- const actualProtos = Docs.newAccount ? {} : await DocServer.GetRefFields(prototypeIds);
+ const actualProtos = await DocServer.GetRefFields(prototypeIds);
// update this object to include any default values: DocumentOptions for all prototypes
prototypeIds.map(id => {
const existing = actualProtos[id] as Doc;
@@ -728,6 +760,10 @@ export namespace Docs {
// an entry dedicated to the given DocumentType)
target && PrototypeMap.set(type, target);
});
+ reaction(
+ () => (proto => StrCast(proto?.BROADCAST_MESSAGE))(DocServer.GetCachedRefField('rtfProto') as Doc),
+ msg => msg && alert(msg)
+ );
}
/**
@@ -742,13 +778,6 @@ export namespace Docs {
}
/**
- * A collection of all links in the database. Ideally, this would be a search, but for now all links are cached here.
- */
- export function MainLinkDocument() {
- return Prototypes.get(DocumentType.LINKDB);
- }
-
- /**
* A collection of all scripts in the database
*/
export function MainScriptDocument() {
@@ -795,6 +824,7 @@ export namespace Docs {
x: 0,
y: 0,
_width: 300,
+ 'acl-Guest': SharingPermissions.View,
...(template.options || {}),
layout: layout.view?.LayoutString(layout.dataField),
data: template.data,
@@ -836,15 +866,14 @@ export namespace Docs {
const { omit: dataProps, extract: viewProps } = OmitKeys(options, viewKeys, '^_');
// dataProps['acl-Override'] = SharingPermissions.Unset;
- dataProps['acl-Public'] = options['acl-Public'] ? options['acl-Public'] : Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Augment;
-
+ dataProps['acl-Guest'] = options['acl-Guest'] ?? (Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.View);
dataProps.isSystem = viewProps.isSystem;
dataProps.isDataDoc = true;
dataProps.author = Doc.CurrentUserEmail;
- dataProps.creationDate = new DateField();
+ dataProps.author_date = new DateField();
if (fieldKey) {
dataProps[`${fieldKey}_modificationDate`] = new DateField();
- dataProps[fieldKey] = data;
+ dataProps[fieldKey] = options.data ?? data;
// so that the list of annotations is already initialised, prevents issues in addonly.
// without this, if a doc has no annotations but the user has AddOnly privileges, they won't be able to add an annotation because they would have needed to create the field's list which they don't have permissions to do.
@@ -859,26 +888,25 @@ export namespace Docs {
dataDoc.proto = proto;
}
- const viewFirstProps: { [id: string]: any } = {};
- viewFirstProps['acl-Public'] = options['_acl-Public'] ? options['_acl-Public'] : Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Augment;
- // viewFirstProps['acl-Override'] = SharingPermissions.Unset;
- viewFirstProps.author = Doc.CurrentUserEmail;
+ const viewFirstProps: { [id: string]: any } = { author: Doc.CurrentUserEmail };
+ viewFirstProps['acl-Guest'] = options['_acl-Guest'] ?? (Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.View);
let viewDoc: Doc;
// determines whether viewDoc should be created using placeholder Doc or default
if (placeholderDoc) {
placeholderDoc._height = options._height !== undefined ? Number(options._height) : undefined;
placeholderDoc._width = options._width !== undefined ? Number(options._width) : undefined;
viewDoc = Doc.assign(placeholderDoc, viewFirstProps, true, true);
+ Array.from(Object.keys(placeholderDoc))
+ .filter(key => key.startsWith('acl'))
+ .forEach(key => (dataDoc[key] = viewDoc[key] = placeholderDoc[key]));
} else {
viewDoc = Doc.assign(Doc.MakeDelegate(dataDoc, delegId), viewFirstProps, true, true);
}
Doc.assign(viewDoc, viewProps, true, true);
- if (![DocumentType.LINK, DocumentType.MARKER, DocumentType.LABEL].includes(viewDoc.type as any)) {
+ if (![DocumentType.LINK, DocumentType.CONFIG, DocumentType.LABEL].includes(viewDoc.type as any)) {
DocUtils.MakeLinkToActiveAudio(() => viewDoc);
}
- Doc.AddFileOrphan(dataDoc);
-
updateCachedAcls(dataDoc);
updateCachedAcls(viewDoc);
@@ -988,7 +1016,7 @@ export namespace Docs {
I.layout = InkingStroke.LayoutString('stroke');
I.layout_fitWidth = true;
I.layout_hideDecorationTitle = true; // don't show title when selected
- // I.hideOpenButton = true; // don't show open full screen button when selected
+ // I.layout_hideOpenButton = true; // don't show open full screen button when selected
I.color = color;
I.fillColor = fillColor;
I.stroke = new InkField(points);
@@ -1004,15 +1032,15 @@ export namespace Docs {
I.y = options.y as number;
I._width = options._width as number;
I._height = options._height as number;
- I._fontFamily = 'cursive';
I.author = Doc.CurrentUserEmail;
I.rotation = 0;
I.defaultDoubleClick = 'click';
- I.creationDate = new DateField();
- I['acl-Public'] = Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Augment;
+ I.author_date = new DateField();
+ I['acl-Guest'] = Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.View;
//I['acl-Override'] = SharingPermissions.Unset;
I[Initializing] = false;
- return I;
+
+ return InstanceFromProto(I, '', options);
}
export function PdfDocument(url: string, options: DocumentOptions = {}, overwriteDoc?: Doc) {
@@ -1051,75 +1079,67 @@ export namespace Docs {
// }
export function FreeformDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
- const inst = InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _xPadding: 20, _yPadding: 20, ...options, _viewType: CollectionViewType.Freeform }, id);
- documents.map(d => (d.embedContainer = inst));
+ const inst = InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _xPadding: 20, _yPadding: 20, ...options, _type_collection: CollectionViewType.Freeform }, id);
+ documents.forEach(d => Doc.SetContainer(d, inst));
return inst;
}
- export function WebanchorDocument(options: DocumentOptions = {}, id?: string) {
- return InstanceFromProto(Prototypes.get(DocumentType.MARKER), undefined, options, id);
+ export function ConfigDocument(options: DocumentOptions, id?: string) {
+ return InstanceFromProto(Prototypes.get(DocumentType.CONFIG), options?.data, options, id);
}
- export function CollectionAnchorDocument(options: DocumentOptions = {}, id?: string) {
- return InstanceFromProto(Prototypes.get(DocumentType.MARKER), options?.data, options, id);
+ 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 TextanchorDocument(options: DocumentOptions = {}, id?: string) {
- return InstanceFromProto(Prototypes.get(DocumentType.MARKER), options?.data, options, 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 ImageanchorDocument(options: DocumentOptions = {}, id?: string) {
- return InstanceFromProto(Prototypes.get(DocumentType.MARKER), options?.data, options, id);
+ export function PileDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
+ return InstanceFromProto(
+ Prototypes.get(DocumentType.COL),
+ new List(documents),
+ { backgroundColor: 'transparent', dropAction: 'move', _forceActive: true, _freeform_noZoom: true, _freeform_noAutoPan: true, ...options, _type_collection: CollectionViewType.Pile },
+ id
+ );
}
-
export function MapanchorDocument(options: DocumentOptions = {}, id?: string) {
return InstanceFromProto(Prototypes.get(DocumentType.MARKER), options?.data, options, id);
}
- export function InkAnchorDocument(options: DocumentOptions, id?: string) {
- return InstanceFromProto(Prototypes.get(DocumentType.MARKER), options?.data, options, id);
- }
-
- export function HTMLAnchorDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
- return InstanceFromProto(Prototypes.get(DocumentType.MARKER), new List(documents), options, id);
- }
-
- export function PileDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
- return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _overflow: 'visible', enableDragWhenActive: true, _forceActive: true, _noAutoscroll: true, ...options, _viewType: CollectionViewType.Pile }, id);
- }
-
export function LinearDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
- return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Linear }, id);
+ return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Linear }, id);
}
export function MapCollectionDocument(documents: Array<Doc>, options: DocumentOptions = {}) {
- return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Map });
+ return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Map });
}
export function CarouselDocument(documents: Array<Doc>, options: DocumentOptions) {
- return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Carousel });
+ return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Carousel });
}
export function Carousel3DDocument(documents: Array<Doc>, options: DocumentOptions) {
- return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Carousel3D });
+ return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Carousel3D });
}
export function SchemaDocument(schemaHeaders: SchemaHeaderField[], documents: Array<Doc>, options: DocumentOptions) {
- return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { schemaHeaders: new List(schemaHeaders), ...options, _viewType: CollectionViewType.Schema });
+ return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { schemaHeaders: new List(schemaHeaders), ...options, _type_collection: CollectionViewType.Schema });
}
export function TreeDocument(documents: Array<Doc>, options: DocumentOptions, id?: string, protoId?: string) {
- return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _xMargin: 5, _yMargin: 5, ...options, _viewType: CollectionViewType.Tree }, id, undefined, protoId);
+ return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _xMargin: 5, _yMargin: 5, ...options, _type_collection: CollectionViewType.Tree }, id, undefined, protoId);
}
export function StackingDocument(documents: Array<Doc>, options: DocumentOptions, id?: string, protoId?: string) {
- return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Stacking }, id, undefined, protoId);
+ return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Stacking }, id, undefined, protoId);
}
export function NoteTakingDocument(documents: Array<Doc>, options: DocumentOptions, id?: string, protoId?: string) {
return InstanceFromProto(
Prototypes.get(DocumentType.COL),
new List(documents),
- { columnHeaders: new List<SchemaHeaderField>([new SchemaHeaderField('Untitled')]), ...options, _viewType: CollectionViewType.NoteTaking },
+ { columnHeaders: new List<SchemaHeaderField>([new SchemaHeaderField('Untitled')]), ...options, _type_collection: CollectionViewType.NoteTaking },
id,
undefined,
protoId
@@ -1127,22 +1147,22 @@ export namespace Docs {
}
export function MulticolumnDocument(documents: Array<Doc>, options: DocumentOptions) {
- return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Multicolumn });
+ return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Multicolumn });
}
export function MultirowDocument(documents: Array<Doc>, options: DocumentOptions) {
- return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Multirow });
+ return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Multirow });
}
export function MasonryDocument(documents: Array<Doc>, options: DocumentOptions) {
- return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Masonry });
+ return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Masonry });
}
export function LabelDocument(options?: DocumentOptions) {
return InstanceFromProto(Prototypes.get(DocumentType.LABEL), undefined, { ...(options || {}) });
}
- export function EquationDocument(options?: DocumentOptions) {
- return InstanceFromProto(Prototypes.get(DocumentType.EQUATION), undefined, { ...(options || {}) }, undefined, 'text');
+ export function EquationDocument(text?: string, options?: DocumentOptions) {
+ return InstanceFromProto(Prototypes.get(DocumentType.EQUATION), text, { ...(options || {}) }, undefined, 'text');
}
export function FunctionPlotDocument(documents: Array<Doc>, options?: DocumentOptions) {
@@ -1160,9 +1180,6 @@ export namespace Docs {
export function FontIconDocument(options?: DocumentOptions) {
return InstanceFromProto(Prototypes.get(DocumentType.FONTICON), undefined, { ...(options || {}) });
}
- export function FilterDocument(options?: DocumentOptions) {
- return InstanceFromProto(Prototypes.get(DocumentType.FILTER), undefined, { ...(options || {}) });
- }
export function PresElementBoxDocument() {
return Prototypes.get(DocumentType.PRESELEMENT);
@@ -1173,7 +1190,9 @@ export namespace Docs {
}
export function DockDocument(documents: Array<Doc>, config: string, options: DocumentOptions, id?: string) {
- return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { freezeChildren: 'remove|add', ...options, viewType: CollectionViewType.Docking, _viewType: CollectionViewType.Docking, dockingConfig: config }, id);
+ const ret = InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { treeViewFreezeChildren: 'remove|add', ...options, type_collection: CollectionViewType.Docking, dockingConfig: config }, id);
+ documents.map(c => Doc.SetContainer(c, ret));
+ return ret;
}
export function DirectoryImportDocument(options: DocumentOptions = {}) {
@@ -1195,36 +1214,45 @@ export namespace Docs {
},
],
};
- return DockDocument(
+ const doc = DockDocument(
configs.map(c => c.doc),
JSON.stringify(layoutConfig),
- options,
+ Doc.CurrentUserEmail === 'guest' ? options : { 'acl-Guest': SharingPermissions.View, ...options },
id
);
+ configs.map(c => {
+ Doc.SetContainer(c.doc, doc);
+ inheritParentAcls(doc, c.doc, false);
+ });
+ return doc;
}
export function DelegateDocument(proto: Doc, options: DocumentOptions = {}) {
return InstanceFromProto(proto, undefined, options);
}
+
+ export function SimulationDocument(options?: DocumentOptions) {
+ return InstanceFromProto(Prototypes.get(DocumentType.SIMULATION), undefined, { ...(options || {}) });
+ }
}
}
export namespace DocUtils {
/**
* @param docs
- * @param docFilters
- * @param docRangeFilters
+ * @param childFilters
+ * @param childFiltersByRanges
* @param parentCollection
- * Given a list of docs and docFilters, @returns the list of Docs that match those filters
+ * Given a list of docs and childFilters, @returns the list of Docs that match those filters
*/
- export function FilterDocs(childDocs: Doc[], docFilters: string[], docRangeFilters: string[], parentCollection?: Doc) {
- if (!docFilters?.length && !docRangeFilters?.length) {
+ export function FilterDocs(childDocs: Doc[], childFilters: string[], childFiltersByRanges: string[], parentCollection?: Doc) {
+ if (!childFilters?.length && !childFiltersByRanges?.length) {
return childDocs.filter(d => !d.cookies); // remove documents that need a cookie if there are no filters to provide one
}
const filterFacets: { [key: string]: { [value: string]: string } } = {}; // maps each filter key to an object with value=>modifier fields
- docFilters.forEach(filter => {
- const fields = filter.split(':');
+ childFilters.forEach(filter => {
+ const fields = filter.split(Doc.FilterSep);
const key = fields[0];
const value = fields[1];
const modifiers = fields[2];
@@ -1234,7 +1262,7 @@ export namespace DocUtils {
filterFacets[key][value] = modifiers;
});
- const filteredDocs = docFilters.length
+ const filteredDocs = childFilters.length
? childDocs.filter(d => {
if (d.z) return true;
// if the document needs a cookie but no filter provides the cookie, then the document does not pass the filter
@@ -1242,7 +1270,7 @@ export namespace DocUtils {
return false;
}
- for (const facetKey of Object.keys(filterFacets).filter(fkey => fkey !== 'cookies' && fkey !== Utils.noDragsDocFilter.split(':')[0])) {
+ for (const facetKey of Object.keys(filterFacets).filter(fkey => fkey !== 'cookies' && fkey !== Utils.noDragsDocFilter.split(Doc.FilterSep)[0])) {
const facet = filterFacets[facetKey];
// facets that match some value in the field of the document (e.g. some text field)
@@ -1278,7 +1306,7 @@ export namespace DocUtils {
return Field.toString(d[facetKey] as Field).includes(value);
});
// if we're ORing them together, the default return is false, and we return true for a doc if it satisfies any one set of criteria
- if (parentCollection?.filterBoolean === 'OR') {
+ if (parentCollection?.childFilters_boolean === 'OR') {
if (satisfiesUnsetsFacets && satisfiesExistsFacets && satisfiesCheckFacets && !failsNotEqualFacets && satisfiesMatchFacets) return true;
}
// if we're ANDing them together, the default return is true, and we return false for a doc if it doesn't satisfy any set of criteria
@@ -1286,14 +1314,14 @@ export namespace DocUtils {
if (!satisfiesUnsetsFacets || !satisfiesExistsFacets || !satisfiesCheckFacets || failsNotEqualFacets || (matches.length && !satisfiesMatchFacets)) return false;
}
}
- return (parentCollection?.currentFilter as Doc)?.filterBoolean === 'OR' ? false : true;
+ return parentCollection?.childFilters_boolean === 'OR' ? false : true;
})
: childDocs;
const rangeFilteredDocs = filteredDocs.filter(d => {
- for (let i = 0; i < docRangeFilters.length; i += 3) {
- const key = docRangeFilters[i];
- const min = Number(docRangeFilters[i + 1]);
- const max = Number(docRangeFilters[i + 2]);
+ for (let i = 0; i < childFiltersByRanges.length; i += 3) {
+ const key = childFiltersByRanges[i];
+ const min = Number(childFiltersByRanges[i + 1]);
+ const max = Number(childFiltersByRanges[i + 2]);
const val = typeof d[key] === 'string' ? (Number(StrCast(d[key])).toString() === StrCast(d[key]) ? Number(StrCast(d[key])) : undefined) : Cast(d[key], 'number', null);
if (val === undefined) {
//console.log("Should 'undefined' pass range filter or not?")
@@ -1310,11 +1338,11 @@ export namespace DocUtils {
broadcastEvent && runInAction(() => (DocumentManager.Instance.RecordingEvent = DocumentManager.Instance.RecordingEvent + 1));
return DocUtils.ActiveRecordings.map(audio => {
const sourceDoc = getSourceDoc();
- return sourceDoc && DocUtils.MakeLink(sourceDoc, audio.getAnchor(true) || audio.props.Document, { layout_linkDisplay: false, link_relationship: 'recording annotation:linked recording', link_description: 'recording timeline' });
+ return sourceDoc && DocUtils.MakeLink(sourceDoc, audio.getAnchor(true) || audio.props.Document, { link_displayLine: false, link_relationship: 'recording annotation:linked recording', link_description: 'recording timeline' });
});
}
- export function MakeLink(source: Doc, target: Doc, linkSettings: { link_relationship?: string; link_description?: string; layout_linkDisplay?: boolean }, id?: string, showPopup?: number[]) {
+ 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;
@@ -1355,15 +1383,15 @@ export namespace DocUtils {
source,
target,
{
+ 'acl-Guest': SharingPermissions.Augment,
+ '_acl-Guest': SharingPermissions.Augment,
title: ComputedField.MakeFunction('generateLinkTitle(self)') as any,
- link_anchor_1_useLinkSmallAnchor: source.useLinkSmallAnchor ? true : undefined,
- link_anchor_2_useLinkSmallAnchor: target.useLinkSmallAnchor ? true : undefined,
- 'acl-Public': SharingPermissions.Augment,
- '_acl-Public': SharingPermissions.Augment,
- layout_linkDisplay: linkSettings.layout_linkDisplay,
+ link_anchor_1_useSmallAnchor: source.useSmallAnchor ? true : undefined,
+ link_anchor_2_useSmallAnchor: target.useSmallAnchor ? true : undefined,
+ link_displayLine: linkSettings.link_displayLine,
link_relationship: linkSettings.link_relationship,
link_description: linkSettings.link_description,
- _layout_autoMoveAnchors: true,
+ link_autoMoveAnchors: true,
_layout_showCaption: 'link_description',
_layout_showTitle: 'link_relationship',
},
@@ -1530,9 +1558,9 @@ export namespace DocUtils {
!simpleMenu &&
ContextMenu.Instance.addItem({
description: 'Quick Notes',
- subitems: DocListCast((Doc.UserDoc()['template-notes'] as Doc).data).map((note, i) => ({
+ subitems: DocListCast((Doc.UserDoc()['template_notes'] as Doc).data).map((note, i) => ({
description: ':' + StrCast(note.title),
- event: undoBatch((args: { x: number; y: number }) => {
+ event: undoable((args: { x: number; y: number }) => {
const textDoc = Docs.Create.TextDocument('', {
_width: 200,
x,
@@ -1546,7 +1574,7 @@ export namespace DocUtils {
textDoc[pivotField] = pivotValue;
}
docTextAdder(textDoc);
- }),
+ }, 'create quick note'),
icon: StrCast(note.icon) as IconProp,
})) as ContextMenuProps[],
icon: 'sticky-note',
@@ -1557,7 +1585,7 @@ export namespace DocUtils {
.filter(doc => doc && doc !== Doc.UserDoc().emptyTrail && doc !== Doc.UserDoc().emptyDataViz)
.map((dragDoc, i) => ({
description: ':' + StrCast(dragDoc.title).replace('Untitled ', ''),
- event: undoBatch((args: { x: number; y: number }) => {
+ event: undoable((args: { x: number; y: number }) => {
const newDoc = DocUtils.copyDragFactory(dragDoc);
if (newDoc) {
newDoc.author = Doc.CurrentUserEmail;
@@ -1570,7 +1598,7 @@ export namespace DocUtils {
}
docAdder?.(newDoc);
}
- }),
+ }, StrCast(dragDoc.title)),
icon: Doc.toIcon(dragDoc),
})) as ContextMenuProps[];
ContextMenu.Instance.addItem({
@@ -1590,10 +1618,10 @@ export namespace DocUtils {
}
export function findTemplate(templateName: string, type: string, signature: string) {
let docLayoutTemplate: Opt<Doc>;
- const iconViews = DocListCast(Cast(Doc.UserDoc()['template-icons'], Doc, null)?.data);
- const templBtns = DocListCast(Cast(Doc.UserDoc()['template-buttons'], Doc, null)?.data);
- const noteTypes = DocListCast(Cast(Doc.UserDoc()['template-notes'], Doc, null)?.data);
- const clickFuncs = DocListCast(Cast(Doc.UserDoc()['template-clickFuncs'], Doc, null)?.data);
+ const iconViews = DocListCast(Cast(Doc.UserDoc()['template_icons'], Doc, null)?.data);
+ const templBtns = DocListCast(Cast(Doc.UserDoc()['template_buttons'], Doc, null)?.data);
+ const noteTypes = DocListCast(Cast(Doc.UserDoc()['template_notes'], Doc, null)?.data);
+ const clickFuncs = DocListCast(Cast(Doc.UserDoc()['template_clickFuncs'], Doc, null)?.data);
const allTemplates = iconViews
.concat(templBtns)
.concat(noteTypes)
@@ -1675,11 +1703,10 @@ export namespace DocUtils {
});
});
if (create) {
- const newCollection = Docs.Create.PileDocument(docList, { title: 'pileup', x: (x || 0) - size, y: (y || 0) - size, _width: size * 2, _height: size * 2 });
+ const newCollection = Docs.Create.PileDocument(docList, { title: 'pileup', x: (x || 0) - size, y: (y || 0) - size, _width: size * 2, _height: size * 2, dragWhenActive: true });
newCollection.x = NumCast(newCollection.x) + NumCast(newCollection._width) / 2 - size;
newCollection.y = NumCast(newCollection.y) + NumCast(newCollection._height) / 2 - size;
newCollection._width = newCollection._height = size * 2;
- newCollection._jitterRotation = 10;
return newCollection;
}
}
@@ -1691,14 +1718,14 @@ export namespace DocUtils {
if (context && !hasContextAnchor && (context.type === DocumentType.VID || context.type === DocumentType.WEB || context.type === DocumentType.PDF || context.type === DocumentType.IMG)) {
const pushpin = Docs.Create.FontIconDocument({
title: 'pushpin',
- label: '',
+ icon_label: '',
annotationOn: Cast(doc.annotationOn, Doc, null),
followLinkToggle: true,
icon: 'map-pin',
x: Cast(doc.x, 'number', null),
y: Cast(doc.y, 'number', null),
backgroundColor: '#ACCEF7',
- hideAllLinks: true,
+ layout_hideAllLinks: true,
_width: 15,
_height: 15,
_xPadding: 0,
@@ -1746,7 +1773,7 @@ export namespace DocUtils {
return;
}
const full = { ...options, _width: 400, title: name };
- const pathname = Utils.prepend(result.accessPaths.agnostic.client);
+ const pathname = result.accessPaths.agnostic.client;
const doc = await DocUtils.DocumentFromType(type, pathname, full, overwriteDoc);
if (doc) {
const proto = Doc.GetProto(doc);
@@ -1789,7 +1816,7 @@ export namespace DocUtils {
_xMargin: noMargins ? 0 : undefined,
_yMargin: noMargins ? 0 : undefined,
annotationOn,
- docMaxAutoHeight: maxHeight,
+ layout_maxAutoHeight: maxHeight,
backgroundColor,
_width: width || 200,
_height: 35,
@@ -1797,7 +1824,7 @@ export namespace DocUtils {
y: y,
_layout_fitWidth: true,
_layout_autoHeight: true,
- _layout_altContentUI: BoolCast(Doc.UserDoc().defaultToFlashcards),
+ _layout_enableAltContentUI: BoolCast(Doc.UserDoc().defaultToFlashcards),
title,
});
const template = Doc.UserDoc().defaultTextLayout;
@@ -1826,9 +1853,21 @@ export namespace DocUtils {
});
}
+ /**
+ * uploadFilesToDocs will take in an array of Files, and creates documents for the
+ * new files.
+ *
+ * @param files an array of files that will be uploaded
+ * @param options options to use while uploading
+ * @returns
+ */
export async function uploadFilesToDocs(files: File[], options: DocumentOptions) {
const generatedDocuments: Doc[] = [];
- const upfiles = await Networking.UploadFilesToServer(files);
+
+ // These files do not have overwriteDocs, so we do not set the guid and let the client generate one.
+ const fileNoGuidPairs: Networking.FileGuidPair[] = files.map(file => ({ file }));
+
+ const upfiles = await Networking.UploadFilesToServer(fileNoGuidPairs);
for (const {
source: { name, type },
result,
@@ -1840,7 +1879,8 @@ export namespace DocUtils {
export function uploadFileToDoc(file: File, options: DocumentOptions, overwriteDoc: Doc) {
const generatedDocuments: Doc[] = [];
- Networking.UploadFilesToServer([file]).then(upfiles => {
+ // Since this file has an overwriteDoc, we can set the client tracking guid to the overwriteDoc's guid.
+ Networking.UploadFilesToServer([{ file, guid: overwriteDoc[Id] }]).then(upfiles => {
const {
source: { name, type },
result,
@@ -1858,14 +1898,11 @@ export namespace DocUtils {
export function copyDragFactory(dragFactory: Doc) {
if (!dragFactory) return undefined;
const ndoc = dragFactory.isTemplateDoc ? Doc.ApplyTemplate(dragFactory) : Doc.MakeCopy(dragFactory, true);
- ndoc && Doc.AddFileOrphan(Doc.GetProto(ndoc));
if (ndoc && dragFactory['dragFactory_count'] !== undefined) {
dragFactory['dragFactory_count'] = NumCast(dragFactory['dragFactory_count']) + 1;
Doc.SetInPlace(ndoc, 'title', ndoc.title + ' ' + NumCast(dragFactory['dragFactory_count']).toString(), true);
}
- if (ndoc && Doc.ActiveDashboard) inheritParentAcls(Doc.ActiveDashboard, ndoc);
-
return ndoc;
}
export function delegateDragFactory(dragFactory: Doc) {
diff --git a/src/client/goldenLayout.js b/src/client/goldenLayout.js
index 9cb20d834..843b8bb5f 100644
--- a/src/client/goldenLayout.js
+++ b/src/client/goldenLayout.js
@@ -4740,7 +4740,7 @@
newstack._$init();
newstack.addChild(this.contentItems[0]);
}
- correctRowOrCol.addChild(newstack, insertBefore ? 0 : undefined, true);
+ correctRowOrCol.addChild(newstack, !insertBefore ? 0 : undefined, true);
newstack.config[dimension] = 50;
contentItem.config[dimension] = 50;
correctRowOrCol.callDownwards('setSize');
diff --git a/src/fields/ListSpec.ts b/src/client/theme.ts
index e69de29bb..e69de29bb 100644
--- a/src/fields/ListSpec.ts
+++ b/src/client/theme.ts
diff --git a/src/client/util/CaptureManager.scss b/src/client/util/CaptureManager.scss
index a5024247e..11e31fe2e 100644
--- a/src/client/util/CaptureManager.scss
+++ b/src/client/util/CaptureManager.scss
@@ -155,21 +155,3 @@
}
}
-.close-button {
- position: absolute;
- top: 10;
- right: 10;
- background:transparent;
- border-radius:100%;
- width: 25px;
- height: 25px;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: 0.2s;
-}
-
-.close-button:hover {
- background: rgba(0,0,0,0.1);
-}
-
diff --git a/src/client/util/CaptureManager.tsx b/src/client/util/CaptureManager.tsx
index d68761ba7..f42336ee7 100644
--- a/src/client/util/CaptureManager.tsx
+++ b/src/client/util/CaptureManager.tsx
@@ -58,7 +58,6 @@ export class CaptureManager extends React.Component<{}> {
)
);
}
-
return (
<div className="capture-block">
<div className="capture-block-title">Links</div>
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 5cfd77937..c9a5175bb 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -1,4 +1,4 @@
-import { reaction } from "mobx";
+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";
@@ -12,7 +12,7 @@ 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 } from "../../Utils";
+import { OmitKeys, Utils, addStyleSheetRule } from "../../Utils";
import { DocServer } from "../DocServer";
import { Docs, DocumentOptions, DocUtils, FInfo } from "../documents/Documents";
import { CollectionViewType, DocumentType } from "../documents/DocumentTypes";
@@ -20,16 +20,17 @@ 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/button/FontIconBox";
+import { ButtonType } from "../views/nodes/FontIconBox/FontIconBox";
import { OpenWhere } from "../views/nodes/DocumentView";
import { OverlayView } from "../views/OverlayView";
-import { DragManager } from "./DragManager";
+import { DragManager, dropActionType } from "./DragManager";
import { MakeTemplate } from "./DropConverter";
import { FollowLinkScript } from "./LinkFollower";
import { LinkManager } from "./LinkManager";
import { ScriptingGlobals } from "./ScriptingGlobals";
-import { ColorScheme } from "./SettingsManager";
+import { ColorScheme, SettingsManager } from "./SettingsManager";
import { UndoManager } from "./UndoManager";
+import { ImportElementBox } from "../views/nodes/importBox/ImportElementBox";
interface Button {
// DocumentOptions fields a button can set
@@ -68,7 +69,7 @@ export class CurrentUserUtils {
template: (opts:DocumentOptions) => Docs.Create.MultirowDocument(
[
Docs.Create.MulticolumnDocument([], { title: "data", _height: 200, isSystem: true }),
- Docs.Create.TextDocument("", { title: "text", _layout_fitWidth:true, _height: 100, isSystem: true, _fontFamily: StrCast(Doc.UserDoc()._fontFamily), _fontSize: StrCast(Doc.UserDoc()._fontSize) })
+ Docs.Create.TextDocument("", { title: "text", _layout_fitWidth:true, _height: 100, isSystem: true, _text_fontFamily: StrCast(Doc.UserDoc().fontFamily), _text_fontSize: StrCast(Doc.UserDoc().fontSize) })
], opts)
},
{
@@ -97,7 +98,7 @@ export class CurrentUserUtils {
const reqdOpts:DocumentOptions = {
title: "Experimental Tools", _xMargin: 0, _layout_showTitle: "title", _chromeHidden: true,
- _stayInCollection: true, _hideContextMenu: true, _forceActive: true, isSystem: true,
+ _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, _forceActive: true, isSystem: true,
_layout_autoHeight: true, _width: 500, _height: 300, _layout_fitWidth: true, _columnWidth: 35, ignoreClick: true, _lockedPosition: true,
};
const reqdScripts = { dropConverter : "convertToButtons(dragData)" };
@@ -123,7 +124,7 @@ export class CurrentUserUtils {
}
/// Initializes templates for editing click funcs of a document
- static setupClickEditorTemplates(doc: Doc, field = "template-clickFuncs") {
+ static setupClickEditorTemplates(doc: Doc, field = "template_clickFuncs") {
const tempClicks = DocCast(doc[field]);
const reqdClickOpts:DocumentOptions = { _width: 300, _height:200, isSystem: true};
const reqdTempOpts:{opts:DocumentOptions, script: string}[] = [
@@ -136,7 +137,7 @@ export class CurrentUserUtils {
const reqdClickList = reqdTempOpts.map(opts => {
const allOpts = {...reqdClickOpts, ...opts.opts};
const clickDoc = tempClicks ? DocListCast(tempClicks.data).find(doc => doc.title === opts.opts.title): undefined;
- return DocUtils.AssignOpts(clickDoc, allOpts) ?? MakeTemplate(Docs.Create.ScriptingDocument(ScriptField.MakeScript(opts.script, {heading:Doc.name, checked:"boolean", containingTreeView:Doc.name}), allOpts), true, opts.opts.title);
+ return DocUtils.AssignOpts(clickDoc, allOpts) ?? MakeTemplate(Docs.Create.ScriptingDocument(ScriptField.MakeScript(opts.script, {heading:Doc.name, checked:"boolean", containingTreeView:Doc.name}), allOpts), true, opts.opts.title?.toString());
});
const reqdOpts:DocumentOptions = {title: "click editor templates", _height:75, isSystem: true};
@@ -144,14 +145,14 @@ export class CurrentUserUtils {
}
/// Initializes templates that can be applied to notes
- static setupNoteTemplates(doc: Doc, field="template-notes") {
+ static setupNoteTemplates(doc: Doc, field="template_notes") {
const tempNotes = DocCast(doc[field]);
const reqdTempOpts:DocumentOptions[] = [
{ noteType: "Note", backgroundColor: "yellow", icon: "sticky-note"},
{ noteType: "Idea", backgroundColor: "pink", icon: "lightbulb" },
{ noteType: "Topic", backgroundColor: "lightblue", icon: "book-open" }];
const reqdNoteList = reqdTempOpts.map(opts => {
- const reqdOpts = {...opts, title: "text", width:200, layout_autoHeight: true, layout_fitWidth: true};
+ const reqdOpts = {...opts, isSystem:true, title: "text", width:200, layout_autoHeight: true, layout_fitWidth: true};
const noteType = tempNotes ? DocListCast(tempNotes.data).find(doc => doc.noteType === opts.noteType): undefined;
return DocUtils.AssignOpts(noteType, reqdOpts) ?? MakeTemplate(Docs.Create.TextDocument("",reqdOpts), true, opts.noteType??"Note");
});
@@ -174,7 +175,7 @@ export class CurrentUserUtils {
}
// setup templates for different document types when they are iconified from Document Decorations
- static setupDefaultIconTemplates(doc: Doc, field="template-icons") {
+ static setupDefaultIconTemplates(doc: Doc, field="template_icons") {
const reqdOpts = { title: "icon templates", _height: 75, isSystem: true };
const templateIconsDoc = DocUtils.AssignOpts(DocCast(doc[field]), reqdOpts) ?? (doc[field] = Docs.Create.TreeDocument([], reqdOpts));
@@ -192,7 +193,7 @@ export class CurrentUserUtils {
{onClick:"deiconifyView(documentView)", onDoubleClick: "deiconifyViewToLightbox(documentView)", });
};
const labelBox = (opts: DocumentOptions, data?:string) => Docs.Create.LabelDocument({
- textTransform: "unset", letterSpacing: "unset", _singleLine: false, _minFontSize: 14, _maxFontSize: 24, borderRounding: "5px", _width: 150, _height: 70, _xPadding: 10, _yPadding: 10, ...opts
+ textTransform: "unset", letterSpacing: "unset", _singleLine: false, _label_minFontSize: 14, _label_maxFontSize: 24, layout_borderRounding: "5px", _width: 150, _height: 70, _xPadding: 10, _yPadding: 10, ...opts
});
const imageBox = (opts: DocumentOptions, url?:string) => Docs.Create.ImageDocument(url ?? "http://www.cs.brown.edu/~bcz/noImage.png", { "icon_nativeWidth": 360 / 4, "icon_nativeHeight": 270 / 4, iconTemplate:DocumentType.IMG, _width: 360 / 4, _height: 270 / 4, _layout_showTitle: "title", ...opts });
const fontBox = (opts:DocumentOptions, data?:string) => Docs.Create.FontIconDocument({ _nativeHeight: 30, _nativeWidth: 30, _width: 30, _height: 30, ...opts });
@@ -201,7 +202,7 @@ export class CurrentUserUtils {
makeIconTemplate(DocumentType.AUDIO, "title", { iconTemplate:DocumentType.LABEL, backgroundColor: "lightgreen"}),
makeIconTemplate(DocumentType.PDF, "title", { iconTemplate:DocumentType.LABEL, backgroundColor: "pink"}),
makeIconTemplate(DocumentType.WEB, "title", { iconTemplate:DocumentType.LABEL, backgroundColor: "brown"}),
- makeIconTemplate(DocumentType.RTF, "text", { iconTemplate:DocumentType.LABEL, _layout_showTitle: "creationDate"}),
+ makeIconTemplate(DocumentType.RTF, "text", { iconTemplate:DocumentType.LABEL, _layout_showTitle: "author_date"}),
makeIconTemplate(DocumentType.IMG, "data", { iconTemplate:DocumentType.IMG, _height: undefined}),
makeIconTemplate(DocumentType.COL, "icon", { iconTemplate:DocumentType.IMG}),
makeIconTemplate(DocumentType.COL, "icon", { iconTemplate:DocumentType.IMG}),
@@ -231,7 +232,7 @@ export class CurrentUserUtils {
marks: [{ type: "strong" }]
}, {
type: "dashField",
- attrs: { fieldKey: "creationDate", docId: "", hideKey: false },
+ attrs: { fieldKey: "author_date", docId: "", hideKey: false },
marks: [{ type: "strong" }]
}]
}]
@@ -260,12 +261,14 @@ export class CurrentUserUtils {
const emptyThings:{key:string, // the field name where the empty thing will be stored
opts:DocumentOptions, // the document options that are required for the empty thing
funcs?:{[key:string]: any}, // computed fields that are rquired for the empth thing
+ scripts?:{[key:string]: any},
creator:(opts:DocumentOptions)=> any // how to create the empty thing if it doesn't exist
}[] = [
{key: "Note", creator: opts => Docs.Create.TextDocument("", opts), opts: { _width: 200, _layout_autoHeight: true }},
- {key: "Flashcard", creator: opts => Docs.Create.TextDocument("", opts), opts: { _width: 200, _layout_autoHeight: true, _layout_altContentUI: true}},
- {key: "Equation", creator: opts => Docs.Create.EquationDocument(opts), opts: { _width: 300, _height: 35, }},
+ {key: "Flashcard", creator: opts => Docs.Create.TextDocument("", opts), opts: { _width: 200, _layout_autoHeight: true, _layout_enableAltContentUI: true}},
+ {key: "Equation", creator: opts => Docs.Create.EquationDocument("",opts), opts: { _width: 300, _height: 35, }},
{key: "Noteboard", creator: opts => Docs.Create.NoteTakingDocument([], opts), opts: { _width: 250, _height: 200, _layout_fitWidth: true}},
+ {key: "Simulation", creator: opts => Docs.Create.SimulationDocument(opts), opts: { _width: 300, _height: 300, }},
{key: "Collection", creator: opts => Docs.Create.FreeformDocument([], opts), opts: { _width: 150, _height: 100, _layout_fitWidth: true }},
{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 }},
@@ -273,43 +276,46 @@ export class CurrentUserUtils {
{key: "Map", creator: opts => Docs.Create.MapDocument([], opts), opts: { _width: 800, _height: 600, _layout_fitWidth: true, _layout_showSidebar: 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, onClick: FollowLinkScript()}},
+ {key: "Button", creator: Docs.Create.ButtonDocument, opts: { _width: 150, _height: 50, _xPadding: 10, _yPadding: 10}, scripts: {onClick: FollowLinkScript()?.script.originalScript ?? ""}},
{key: "Script", creator: opts => Docs.Create.ScriptingDocument(null, opts), opts: { _width: 200, _height: 250, }},
{key: "DataViz", creator: opts => Docs.Create.DataVizDocument("/users/rz/Downloads/addresses.csv", opts), opts: { _width: 300, _height: 300 }},
{key: "Header", creator: headerTemplate, opts: { _width: 300, _height: 70, _headerPointerEvents: "all", _headerHeight: 12, _headerFontSize: 9, _layout_autoHeight: true, treeViewHideUnrendered: true}},
- {key: "Trail", creator: Docs.Create.PresDocument, opts: { _width: 400, _height: 30, _viewType: CollectionViewType.Stacking, targetDropAction: "embed" as any, treeViewHideTitle: true, _layout_fitWidth:true, _chromeHidden: true, boxShadow: "0 0" }},
+ {key: "Trail", creator: Docs.Create.PresDocument, opts: { _width: 400, _height: 30, _type_collection: CollectionViewType.Stacking, dropAction: "embed" as dropActionType, treeViewHideTitle: true, _layout_fitWidth:true, _chromeHidden: true, layout_boxShadow: "0 0" }},
{key: "Tab", creator: opts => Docs.Create.FreeformDocument([], opts), opts: { _width: 500, _height: 800, _layout_fitWidth: true, _freeform_backgroundGrid: true, }},
- {key: "Slide", creator: opts => Docs.Create.TreeDocument([], opts), opts: { _width: 300, _height: 200, _viewType: CollectionViewType.Tree,
- treeViewHasOverlay: true, _fontSize: "20px", _layout_autoHeight: true,
- allowOverlayDrop: true, treeViewType: TreeViewType.outline,
+ {key: "Slide", creator: opts => Docs.Create.TreeDocument([], opts), opts: { _width: 300, _height: 200, _type_collection: CollectionViewType.Tree,
+ treeViewHasOverlay: true, _text_fontSize: "20px", _layout_autoHeight: true,
+ dropAction:'move', treeViewType: TreeViewType.outline,
backgroundColor: "white", _xMargin: 0, _yMargin: 0, _createDocOnCR: true
}, funcs: {title: 'self.text?.Text'}},
];
- emptyThings.forEach(thing => DocUtils.AssignDocField(doc, "empty"+thing.key, (opts) => thing.creator(opts), {...standardOps(thing.key), ...thing.opts}, undefined, undefined, thing.funcs));
+ emptyThings.forEach(thing => DocUtils.AssignDocField(doc, "empty"+thing.key, (opts) => thing.creator(opts), {...standardOps(thing.key), ...thing.opts}, undefined, thing.scripts, thing.funcs));
return [
- { toolTip: "Tap or drag to create a note", title: "Note", icon: "sticky-note", dragFactory: doc.emptyNote as Doc, clickFactory: DocCast(doc.emptyNote)},
- { toolTip: "Tap or drag to create a flashcard", title: "Flashcard", icon: "id-card", dragFactory: doc.emptyFlashcard as Doc, clickFactory: DocCast(doc.emptyFlashcard)},
- { toolTip: "Tap or drag to create an equation", title: "Math", icon: "calculator", dragFactory: doc.emptyEquation as Doc, clickFactory: DocCast(doc.emptyEquation)},
- { toolTip: "Tap or drag to create a note board", title: "Notes", icon: "folder", dragFactory: doc.emptyNoteboard as Doc, clickFactory: DocCast(doc.emptyNoteboard)},
- { toolTip: "Tap or drag to create a collection", title: "Col", icon: "folder", dragFactory: doc.emptyCollection as Doc,clickFactory: DocCast(doc.emptyTab)},
- { toolTip: "Tap or drag to create a webpage", title: "Web", icon: "globe-asia", dragFactory: doc.emptyWebpage as Doc, clickFactory: DocCast(doc.emptyWebpage)},
- { toolTip: "Tap or drag to create a comparison box", title: "Compare", icon: "columns", dragFactory: doc.emptyComparison as Doc,clickFactory: DocCast(doc.emptyComparison)},
- { toolTip: "Tap or drag to create an audio recorder", title: "Audio", icon: "microphone", dragFactory: doc.emptyAudio as Doc, clickFactory: DocCast(doc.emptyAudio), openFactoryLocation: OpenWhere.overlay},
- { toolTip: "Tap or drag to create a map", title: "Map", icon: "map-marker-alt", dragFactory: doc.emptyMap as Doc, clickFactory: DocCast(doc.emptyMap)},
- { toolTip: "Tap or drag to create a screen grabber", title: "Grab", icon: "photo-video", dragFactory: doc.emptyScreengrab as Doc,clickFactory: DocCast(doc.emptyScreengrab), openFactoryLocation: OpenWhere.overlay},
- { toolTip: "Tap or drag to create a WebCam recorder", title: "WebCam", icon: "photo-video", dragFactory: doc.emptyWebCam as Doc, clickFactory: DocCast(doc.emptyWebCam), openFactoryLocation: OpenWhere.overlay},
- { toolTip: "Tap or drag to create a button", title: "Button", icon: "bolt", dragFactory: doc.emptyButton as Doc, clickFactory: DocCast(doc.emptyButton)},
- { toolTip: "Tap or drag to create a scripting box", title: "Script", icon: "terminal", dragFactory: doc.emptyScript as Doc, clickFactory: DocCast(doc.emptyScript)},
- { toolTip: "Tap or drag to create a data viz node", title: "DataViz", icon: "file", dragFactory: doc.emptyDataViz as Doc, clickFactory: DocCast(doc.emptyDataViz)},
- { toolTip: "Tap or drag to create a bullet slide", title: "PPT Slide", icon: "file", dragFactory: doc.emptySlide as Doc, clickFactory: DocCast(doc.emptySlide), openFactoryLocation: OpenWhere.overlay},
- { toolTip: "Tap or drag to create a data note", title: "DataNote", icon: "window-maximize", dragFactory: doc.emptyHeader as Doc, clickFactory: DocCast(doc.emptyHeader), openFactoryAsDelegate: true },
- { toolTip: "Toggle a Calculator REPL", title: "repl", icon: "calculator", clickFactory: 'repl' as any, openFactoryLocation: OpenWhere.overlay},
+ { toolTip: "Tap or drag to create a note", title: "Note", icon: "sticky-note", dragFactory: doc.emptyNote as Doc, clickFactory: DocCast(doc.emptyNote)},
+ { toolTip: "Tap or drag to create a flashcard", title: "Flashcard", icon: "id-card", dragFactory: doc.emptyFlashcard as Doc, clickFactory: DocCast(doc.emptyFlashcard)},
+ { toolTip: "Tap or drag to create an equation", title: "Math", icon: "calculator", dragFactory: doc.emptyEquation as Doc, clickFactory: DocCast(doc.emptyEquation)},
+ { toolTip: "Tap or drag to create a physics simulation", title: "Simulation", icon: "atom", dragFactory: doc.emptySimulation as Doc, funcs: { hidden: "IsNoviceMode()"}},
+ { toolTip: "Tap or drag to create a note board", title: "Notes", icon: "folder", dragFactory: doc.emptyNoteboard as Doc, clickFactory: DocCast(doc.emptyNoteboard)},
+ { toolTip: "Tap or drag to create a collection", title: "Col", icon: "folder", dragFactory: doc.emptyCollection as Doc, clickFactory: DocCast(doc.emptyTab)},
+ { toolTip: "Tap or drag to create a webpage", title: "Web", icon: "globe-asia", dragFactory: doc.emptyWebpage as Doc, clickFactory: DocCast(doc.emptyWebpage)},
+ { toolTip: "Tap or drag to create a comparison box", title: "Compare", icon: "columns", dragFactory: doc.emptyComparison as Doc, clickFactory: DocCast(doc.emptyComparison)},
+ { toolTip: "Tap or drag to create an audio recorder", title: "Audio", icon: "microphone", dragFactory: doc.emptyAudio as Doc, clickFactory: DocCast(doc.emptyAudio), openFactoryLocation: OpenWhere.overlay},
+ { toolTip: "Tap or drag to create a map", title: "Map", icon: "map-marker-alt", dragFactory: doc.emptyMap as Doc, clickFactory: DocCast(doc.emptyMap)},
+ { toolTip: "Tap or drag to create a screen grabber", title: "Grab", icon: "photo-video", dragFactory: doc.emptyScreengrab as Doc, clickFactory: DocCast(doc.emptyScreengrab), openFactoryLocation: OpenWhere.overlay},
+ { toolTip: "Tap or drag to create a WebCam recorder", title: "WebCam", icon: "photo-video", dragFactory: doc.emptyWebCam as Doc, clickFactory: DocCast(doc.emptyWebCam), openFactoryLocation: OpenWhere.overlay, funcs: { hidden: "IsNoviceMode()"}},
+ { toolTip: "Tap or drag to create a button", title: "Button", icon: "bolt", dragFactory: doc.emptyButton as Doc, clickFactory: DocCast(doc.emptyButton)},
+ { toolTip: "Tap or drag to create a scripting box", title: "Script", icon: "terminal", dragFactory: doc.emptyScript as Doc, clickFactory: DocCast(doc.emptyScript), funcs: { hidden: "IsNoviceMode()"}},
+ { toolTip: "Tap or drag to create a data viz node", title: "DataViz", icon: "file", dragFactory: doc.emptyDataViz as Doc, clickFactory: DocCast(doc.emptyDataViz)},
+ { toolTip: "Tap or drag to create a bullet slide", title: "PPT Slide", icon: "file", dragFactory: doc.emptySlide as Doc, clickFactory: DocCast(doc.emptySlide), openFactoryLocation: OpenWhere.overlay, funcs: { hidden: "IsNoviceMode()"}},
+ { toolTip: "Tap or drag to create a data note", title: "DataNote", icon: "window-maximize",dragFactory: doc.emptyHeader as Doc,clickFactory: DocCast(doc.emptyHeader), openFactoryAsDelegate: true , funcs: { hidden: "IsNoviceMode()"}},
+ { toolTip: "Toggle a Calculator REPL", title: "replviewer", icon: "calculator", clickFactory: '<ScriptingRepl />' as any, openFactoryLocation: OpenWhere.overlay, funcs: { hidden: "IsNoviceMode()"}}, // hack: clickFactory is not a Doc but will get interpreted as a custom UI by the openDoc() onClick script
+ { toolTip: "Toggle an UndoStack", title: "undostacker", icon: "calculator", clickFactory: "<UndoStack>" as any, openFactoryLocation: OpenWhere.overlay, funcs: { hidden: "IsNoviceMode()"}},
].map(tuple => (
{ openFactoryLocation: OpenWhere.addRight,
scripts: { onClick: 'openDoc(copyDragFactory(this.clickFactory,this.openFactoryAsDelegate), this.openFactoryLocation)',
onDragStart: '{ return copyDragFactory(this.dragFactory,this.openFactoryAsDelegate); }'},
+ funcs: tuple.funcs,
...tuple, }))
}
@@ -318,60 +324,58 @@ export class CurrentUserUtils {
const creatorBtns = CurrentUserUtils.creatorBtnDescriptors(doc).map((reqdOpts) => {
const btn = dragCreatorDoc ? DocListCast(dragCreatorDoc.data).find(doc => doc.title === reqdOpts.title): undefined;
const opts:DocumentOptions = {...OmitKeys(reqdOpts, ["funcs", "scripts", "backgroundColor"]).omit,
- _width: 35, _height: 35, _hideContextMenu: true, _stayInCollection: true,
+ _width: 60, _height: 60, _layout_hideContextMenu: true, _dragOnlyWithinContainer: true,
btnType: ButtonType.ToolButton, backgroundColor: reqdOpts.backgroundColor ?? Colors.DARK_GRAY, color: Colors.WHITE, isSystem: true,
- _removeDropProperties: new List<string>(["_stayInCollection"]),
};
return DocUtils.AssignScripts(DocUtils.AssignOpts(btn, opts) ?? Docs.Create.FontIconDocument(opts), reqdOpts.scripts, reqdOpts.funcs);
});
const reqdOpts:DocumentOptions = {
- title: "Basic Item Creators", _layout_showTitle: "title", _xMargin: 0, _stayInCollection: true, _hideContextMenu: true, _chromeHidden: true, isSystem: true,
+ title: "Basic Item Creators", _layout_showTitle: "title", _xMargin: 0, _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, _chromeHidden: true, isSystem: true,
_layout_autoHeight: true, _width: 500, _height: 300, _layout_fitWidth: true, _columnWidth: 40, ignoreClick: true, _lockedPosition: true, _forceActive: true,
- childDropAction: 'embed'
+ childDragAction: 'embed'
};
const reqdScripts = { dropConverter: "convertToButtons(dragData)" };
return DocUtils.AssignScripts(DocUtils.AssignOpts(dragCreatorDoc, reqdOpts, creatorBtns) ?? Docs.Create.MasonryDocument(creatorBtns, reqdOpts), reqdScripts);
}
/// returns descriptions needed to buttons for the left sidebar to open up panes displaying different collections of documents
- static leftSidebarMenuBtnDescriptions(doc: Doc):{title:string, target:Doc, icon:string, scripts:{[key:string]:any}, funcs?:{[key:string]:any}}[] {
+ static leftSidebarMenuBtnDescriptions(doc: Doc):{title:string, target:Doc, icon:string, toolTip: string, scripts:{[key:string]:any}, funcs?:{[key:string]:any}, hidden?: boolean}[] {
const badgeValue = "((len) => len && len !== '0' ? len: undefined)(docList(self.target.data).filter(doc => !docList(self.target.viewed).includes(doc)).length.toString())";
const getActiveDashTrails = "Doc.ActiveDashboard?.myTrails";
return [
- { title: "Dashboards", target: this.setupDashboards(doc, "myDashboards"), icon: "desktop", funcs: {hidden: "IsNoviceMode()"} },
- { title: "Search", target: this.setupSearcher(doc, "mySearcher"), icon: "search", },
- { title: "Files", target: this.setupFilesystem(doc, "myFilesystem"), icon: "folder-open", },
- { title: "Tools", target: this.setupToolsBtnPanel(doc, "myTools"), icon: "wrench", funcs: {hidden: "IsNoviceMode()"} },
- { title: "Imports", target: this.setupImportSidebar(doc, "myImports"), icon: "upload", },
- { title: "Recently Closed", target: this.setupRecentlyClosed(doc, "myRecentlyClosed"), icon: "archive", },
- { title: "Shared Docs", target: Doc.MySharedDocs, icon: "users", funcs: {badgeValue:badgeValue}},
- { title: "Trails", target: Doc.UserDoc(), icon: "pres-trail", funcs: {target: getActiveDashTrails}},
- { title: "User Doc View", target: this.setupUserDocView(doc, "myUserDocView"), icon: "address-card",funcs: {hidden: "IsNoviceMode()"} },
+ { title: "Dashboards", toolTip: "Dashboards", target: this.setupDashboards(doc, "myDashboards"), ignoreClick: true, icon: "desktop", funcs: {hidden: "IsNoviceMode()"} },
+ { title: "Search", toolTip: "Search ⌘F", target: this.setupSearcher(doc, "mySearcher"), ignoreClick: true, icon: "search", },
+ { title: "Files", toolTip: "Files", target: this.setupFilesystem(doc, "myFilesystem"), ignoreClick: true, icon: "folder-open", },
+ { title: "Tools", toolTip: "Tools", target: this.setupToolsBtnPanel(doc, "myTools"), ignoreClick: true, icon: "wrench", },
+ { title: "Imports", toolTip: "Imports ⌘I", target: this.setupImportSidebar(doc, "myImports"), ignoreClick:false, icon: "upload", },
+ { title: "Closed", toolTip: "Recently Closed", target: this.setupRecentlyClosed(doc, "myRecentlyClosed"), ignoreClick: true, icon: "archive", hidden: true }, // this doc is hidden from the Sidebar, but it's still being used in MyFilesystem which ignores the hidden field
+ { title: "Shared", toolTip: "Shared Docs", target: Doc.MySharedDocs, ignoreClick: true, icon: "users", funcs: {badgeValue: badgeValue}},
+ { title: "Trails", toolTip: "Trails ⌘R", target: Doc.UserDoc(), ignoreClick: true, icon: "pres-trail", funcs: {target: getActiveDashTrails}},
+ { title: "User Doc", toolTip: "User Doc", target: this.setupUserDocView(doc, "myUserDocView"), ignoreClick: true, icon: "address-card",funcs: {hidden: "IsNoviceMode()"} },
].map(tuple => ({...tuple, scripts:{onClick: 'selectMainMenu(self)'}}));
}
/// the empty panel that is filled with whichever left menu button's panel has been selected
static setupLeftSidebarPanel(doc: Doc, field="myLeftSidebarPanel") {
- DocUtils.AssignDocField(doc, field, (opts) => ((doc:Doc) => {doc.isSystem = true; return doc;})(new Doc()), {isSystem:true});
+ DocUtils.AssignDocField(doc, field, (opts) => Doc.assign(new Doc(), opts as any), {title:"leftSidebarPanel", isSystem:true, undoIgnoreFields: new List<string>(['proto'])});
}
/// Initializes the left sidebar menu buttons and the panels they open up
static setupLeftSidebarMenu(doc: Doc, field="myLeftSidebarMenu") {
this.setupLeftSidebarPanel(doc);
const myLeftSidebarMenu = DocCast(doc[field]);
- const menuBtns = CurrentUserUtils.leftSidebarMenuBtnDescriptions(doc).map(({ title, target, icon, scripts, funcs }) => {
+ const menuBtns = CurrentUserUtils.leftSidebarMenuBtnDescriptions(doc).map(({ title, target, icon, toolTip, hidden, scripts, funcs }) => {
const btnDoc = myLeftSidebarMenu ? DocListCast(myLeftSidebarMenu.data).find(doc => doc.title === title) : undefined;
const reqdBtnOpts:DocumentOptions = {
- title, icon, target, btnType: ButtonType.MenuButton, isSystem: true, dontUndo: true, dontRegisterView: true,
- _width: 60, _height: 60, _stayInCollection: true, _hideContextMenu: true,
- _removeDropProperties: new List<string>(["_stayInCollection"]),
+ title, icon, target, toolTip, hidden, btnType: ButtonType.MenuButton, isSystem: true, undoIgnoreFields: new List<string>(['height', 'data_columnHeaders']), dontRegisterView: true,
+ _width: 60, _height: 60, _dragOnlyWithinContainer: true, _layout_hideContextMenu: true,
};
return DocUtils.AssignScripts(DocUtils.AssignOpts(btnDoc, reqdBtnOpts) ?? Docs.Create.FontIconDocument(reqdBtnOpts), scripts, funcs);
});
const reqdStackOpts:DocumentOptions ={
- title: "menuItemPanel", childDropAction: "same", backgroundColor: Colors.DARK_GRAY, boxShadow: "rgba(0,0,0,0)", dontRegisterView: true, ignoreClick: true,
+ title: "menuItemPanel", childDragAction: "same", layout_boxShadow: "rgba(0,0,0,0)", dontRegisterView: true, ignoreClick: true,
_chromeHidden: true, _gridGap: 0, _yMargin: 0, _yPadding: 0, _xMargin: 0, _layout_autoHeight: false, _width: 60, _columnWidth: 60, _lockedPosition: true, isSystem: true,
};
return DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.StackingDocument(items??[], opts), reqdStackOpts, menuBtns, { dropConverter: "convertToButtons(dragData)" });
@@ -410,26 +414,26 @@ export class CurrentUserUtils {
static mobileButton = (opts: DocumentOptions, docs: Doc[]) => Docs.Create.MulticolumnDocument(docs, {
...opts,
_nativeWidth: 900, _nativeHeight: 250, _width: 900, _height: 250, _yMargin: 15,
- borderRounding: "5px", boxShadow: "0 0", isSystem: true
+ layout_borderRounding: "5px", layout_boxShadow: "0 0", isSystem: true
}) as any as Doc
// sets up the text container for the information contained within the mobile button
static mobileTextContainer = (opts: DocumentOptions, docs: Doc[]) => Docs.Create.MultirowDocument(docs, {
...opts,
_nativeWidth: 450, _nativeHeight: 250, _width: 450, _height: 250, _yMargin: 25,
- backgroundColor: "rgba(0,0,0,0)", borderRounding: "0", boxShadow: "0 0", ignoreClick: true, isSystem: true
+ backgroundColor: "rgba(0,0,0,0)", layout_borderRounding: "0", layout_boxShadow: "0 0", ignoreClick: true, isSystem: true
}) as any as Doc
// Sets up the title of the button
static mobileButtonText = (opts: DocumentOptions, buttonTitle: string) => Docs.Create.TextDocument(buttonTitle, {
...opts,
- title: buttonTitle, _fontSize: "37px", _xMargin: 0, _yMargin: 0, ignoreClick: true, backgroundColor: "rgba(0,0,0,0)", isSystem: true
+ title: buttonTitle, _text_fontSize: "37px", _xMargin: 0, _yMargin: 0, ignoreClick: true, backgroundColor: "rgba(0,0,0,0)", isSystem: true
}) as any as Doc
// Sets up the description of the button
static mobileButtonInfo = (opts: DocumentOptions, buttonInfo: string) => Docs.Create.TextDocument(buttonInfo, {
...opts,
- title: "info", _fontSize: "25px", _xMargin: 0, _yMargin: 0, ignoreClick: true, backgroundColor: "rgba(0,0,0,0)", _dimMagnitude: 2, isSystem: true
+ title: "info", _text_fontSize: "25px", _xMargin: 0, _yMargin: 0, ignoreClick: true, backgroundColor: "rgba(0,0,0,0)", _dimMagnitude: 2, isSystem: true
}) as any as Doc
@@ -454,8 +458,8 @@ export class CurrentUserUtils {
/// Search option on the left side button panel
static setupSearcher(doc: Doc, field:string) {
return DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.SearchDocument(opts), {
- dontRegisterView: true, backgroundColor: "dimgray", ignoreClick: true, title: "Search Panel", isSystem: true, childDropAction: "embed",
- _lockedPosition: true, _viewType: CollectionViewType.Schema, _searchDoc: true, });
+ dontRegisterView: true, backgroundColor: "dimgray", ignoreClick: true, title: "Search Panel", isSystem: true, childDragAction: "embed",
+ _lockedPosition: true, _type_collection: CollectionViewType.Schema });
}
/// Initializes the panel of draggable tools that is opened from the left sidebar.
@@ -464,8 +468,8 @@ export class CurrentUserUtils {
const creatorBtns = CurrentUserUtils.setupCreatorButtons(doc, DocListCast(myTools?.data)?.length ? DocListCast(myTools.data)[0]:undefined);
const templateBtns = CurrentUserUtils.setupExperimentalTemplateButtons(doc,DocListCast(myTools?.data)?.length > 1 ? DocListCast(myTools.data)[1]:undefined);
const reqdToolOps:DocumentOptions = {
- title: "My Tools", isSystem: true, ignoreClick: true, boxShadow: "0 0",
- _layout_showTitle: "title", _width: 500, _yMargin: 20, _lockedPosition: true, _forceActive: true, _stayInCollection: true, _hideContextMenu: true, _chromeHidden: true,
+ title: "My Tools", isSystem: true, ignoreClick: true, layout_boxShadow: "0 0",
+ _layout_showTitle: "title", _width: 500, _yMargin: 20, _lockedPosition: true, _forceActive: true, _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, _chromeHidden: true,
};
return DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.StackingDocument(items??[], opts), reqdToolOps, [creatorBtns, templateBtns]);
}
@@ -477,10 +481,10 @@ export class CurrentUserUtils {
const toggleDarkTheme = `this.colorScheme = this.colorScheme ? undefined : "${ColorScheme.Dark}"`;
const newDashboard = `createNewDashboard()`;
- const reqdBtnOpts:DocumentOptions = { _forceActive: true, _width: 30, _height: 30, _stayInCollection: true, _hideContextMenu: true,
+ const reqdBtnOpts:DocumentOptions = { _forceActive: true, _width: 30, _height: 30, _dragOnlyWithinContainer: true, _layout_hideContextMenu: true,
title: "new dashboard", btnType: ButtonType.ClickButton, toolTip: "Create new dashboard", buttonText: "New trail", icon: "plus", isSystem: true };
const reqdBtnScript = {onClick: newDashboard,}
- const newDashboardButton = DocUtils.AssignScripts(DocUtils.AssignOpts(DocCast(myDashboards?.buttonMenuDoc), reqdBtnOpts) ?? Docs.Create.FontIconDocument(reqdBtnOpts), reqdBtnScript);
+ const newDashboardButton = DocUtils.AssignScripts(DocUtils.AssignOpts(DocCast(myDashboards?.layout_headerButton), reqdBtnOpts) ?? Docs.Create.FontIconDocument(reqdBtnOpts), reqdBtnScript);
const contextMenuScripts = [/*newDashboard*/] as string[];
const contextMenuLabels = [/*"Create New Dashboard"*/] as string[];
@@ -490,15 +494,15 @@ export class CurrentUserUtils {
const childContextMenuLabels = ["Toggle Dark Theme", "Toggle Comic Mode", "Snapshot Dashboard", "Share Dashboard", "Remove Dashboard", "Reset Dashboard"];// entries must be kept in synch with childContextMenuScripts, childContextMenuIcons, and childContextMenuFilters
const childContextMenuIcons = ["chalkboard", "tv", "camera", "users", "times", "trash"]; // entries must be kept in synch with childContextMenuScripts, childContextMenuLabels, and childContextMenuFilters
const reqdOpts:DocumentOptions = {
- title: "My Dashboards", childHideLinkButton: true, freezeChildren: "remove|add", treeViewHideTitle: true, boxShadow: "0 0", childDontRegisterViews: true,
- targetDropAction: "same", treeViewType: TreeViewType.fileSystem, isFolder: true, isSystem: true, treeViewTruncateTitleWidth: 350, ignoreClick: true,
- buttonMenu: true, buttonMenuDoc: newDashboardButton, childDropAction: "embed",
+ title: "My Dashboards", childHideLinkButton: true, treeViewFreezeChildren: "remove|add", treeViewHideTitle: true, layout_boxShadow: "0 0", childDontRegisterViews: true,
+ dropAction: "same", treeViewType: TreeViewType.fileSystem, isFolder: true, isSystem: true, treeViewTruncateTitleWidth: 350, ignoreClick: true,
+ layout_headerButton: newDashboardButton, childDragAction: "none",
_layout_showTitle: "title", _height: 400, _gridGap: 5, _forceActive: true, _lockedPosition: true,
contextMenuLabels:new List<string>(contextMenuLabels),
contextMenuIcons:new List<string>(contextMenuIcons),
childContextMenuLabels:new List<string>(childContextMenuLabels),
childContextMenuIcons:new List<string>(childContextMenuIcons),
- explainer: "This is your collection of dashboards. A dashboard represents the tab configuration of your workspace. To manage documents as folders, go to the Files."
+ layout_explainer: "This is your collection of dashboards. A dashboard represents the tab configuration of your workspace. To manage documents as folders, go to the Files."
};
myDashboards = DocUtils.AssignDocField(doc, field, (opts) => Docs.Create.TreeDocument([], opts), reqdOpts);
if (Cast(myDashboards.contextMenuScripts, listSpec(ScriptField), null)?.length !== contextMenuScripts.length) {
@@ -516,25 +520,25 @@ export class CurrentUserUtils {
/// initializes the left sidebar File system pane
static setupFilesystem(doc: Doc, field:string) {
var myFilesystem = DocCast(doc[field]);
- const myFileOrphans = DocUtils.AssignDocField(doc, "myFileOrphans", (opts) => Docs.Create.TreeDocument([], opts), { title: "Unfiled", _stayInCollection: true, isSystem: true, isFolder: true });
const newFolder = `TreeView_addNewFolder()`;
const newFolderOpts: DocumentOptions = {
- _forceActive: true, _stayInCollection: true, _hideContextMenu: true, _width: 30, _height: 30,
- title: "New folder", btnType: ButtonType.ClickButton, toolTip: "Create new folder", buttonText: "New folder", icon: "folder-plus", isSystem: true
+ _forceActive: true, _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, _width: 30, _height: 30, undoIgnoreFields:new List<string>(['treeViewSortCriterion']),
+ title: "New folder", color: Colors.BLACK, btnType: ButtonType.ClickButton, toolTip: "Create new folder", buttonText: "New folder", icon: "folder-plus", isSystem: true
};
const newFolderScript = { onClick: newFolder};
- const newFolderButton = DocUtils.AssignScripts(DocUtils.AssignOpts(DocCast(myFilesystem?.buttonMenuDoc), newFolderOpts) ?? Docs.Create.FontIconDocument(newFolderOpts), newFolderScript);
+ const newFolderButton = DocUtils.AssignScripts(DocUtils.AssignOpts(DocCast(myFilesystem?.layout_headerButton), newFolderOpts) ?? Docs.Create.FontIconDocument(newFolderOpts), newFolderScript);
const reqdOpts:DocumentOptions = { _layout_showTitle: "title", _height: 100, _gridGap: 5, _forceActive: true, _lockedPosition: true,
- title: "My Documents", buttonMenu: true, buttonMenuDoc: newFolderButton, treeViewHideTitle: true, targetDropAction: "proto", isSystem: true,
- isFolder: true, treeViewType: TreeViewType.fileSystem, childHideLinkButton: true, boxShadow: "0 0", childDontRegisterViews: true,
- treeViewTruncateTitleWidth: 350, ignoreClick: true, childDropAction: "embed",
+ title: "My Documents", layout_headerButton: newFolderButton, treeViewHideTitle: true, dropAction: 'add', isSystem: true,
+ isFolder: true, treeViewType: TreeViewType.fileSystem, childHideLinkButton: true, layout_boxShadow: "0 0", childDontRegisterViews: true,
+ treeViewTruncateTitleWidth: 350, ignoreClick: true, childDragAction: "embed",
childContextMenuLabels: new List<string>(["Create new folder"]),
childContextMenuIcons: new List<string>(["plus"]),
- explainer: "This is your file manager where you can create folders to keep track of documents independently of your dashboard."
+ layout_explainer: "This is your file manager where you can create folders to keep track of documents independently of your dashboard."
};
- myFilesystem = DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.TreeDocument(items??[], opts), reqdOpts, [myFileOrphans]);
+ const fileFolders = new Set(DocListCast(DocCast(doc[field])?.data));
+ myFilesystem = DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.TreeDocument(items??[], opts), reqdOpts, Array.from(fileFolders));
const childContextMenuScripts = [newFolder];
if (Cast(myFilesystem.childContextMenuScripts, listSpec(ScriptField), null)?.length !== childContextMenuScripts.length) {
myFilesystem.childContextMenuScripts = new List<ScriptField>(childContextMenuScripts.map(script => ScriptField.MakeFunction(script)!));
@@ -544,23 +548,21 @@ export class CurrentUserUtils {
/// initializes the panel displaying docs that have been recently closed
static setupRecentlyClosed(doc: Doc, field:string) {
- const reqdOpts:DocumentOptions = { _layout_showTitle: "title", _lockedPosition: true, _gridGap: 5, _forceActive: true,
- title: "My Recently Closed", buttonMenu: true, childHideLinkButton: true, treeViewHideTitle: true, childDropAction: "embed", isSystem: true,
- treeViewTruncateTitleWidth: 350, ignoreClick: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same",
+ const reqdOpts:DocumentOptions = { _layout_showTitle: "title", _lockedPosition: true, _gridGap: 5, _forceActive: true, isFolder: true,
+ title: "My Recently Closed", childHideLinkButton: true, treeViewHideTitle: true, childDragAction: "move", isSystem: true,
+ treeViewTruncateTitleWidth: 350, ignoreClick: true, layout_boxShadow: "0 0", childDontRegisterViews: true, dropAction: "same",
contextMenuLabels: new List<string>(["Empty recently closed"]),
contextMenuIcons:new List<string>(["trash"]),
- explainer: "Recently closed documents appear in this menu. They will only be deleted if you explicity empty this list."
+ layout_explainer: "Recently closed documents appear in this menu. They will only be deleted if you explicity empty this list."
};
const recentlyClosed = DocUtils.AssignDocField(doc, field, (opts) => Docs.Create.TreeDocument([], opts), reqdOpts);
const clearAll = (target:string) => `getProto(${target}).data = new List([])`;
- const clearBtnsOpts:DocumentOptions = { _width: 30, _height: 30, _forceActive: true, _stayInCollection: true, _hideContextMenu: true,
- title: "Empty", target: recentlyClosed, btnType: ButtonType.ClickButton, buttonText: "Empty", icon: "trash", isSystem: true,
+ const clearBtnsOpts:DocumentOptions = { _width: 30, _height: 30, _forceActive: true, _dragOnlyWithinContainer: true, _layout_hideContextMenu: true,
+ title: "Empty", target: recentlyClosed, btnType: ButtonType.ClickButton, color: Colors.BLACK, buttonText: "Empty", icon: "trash", isSystem: true,
toolTip: "Empty recently closed",};
- const clearDocsButton = DocUtils.AssignDocField(recentlyClosed, "clearDocsBtn", (opts) => Docs.Create.FontIconDocument(opts), clearBtnsOpts, undefined, {onClick: clearAll("self.target")});
+ DocUtils.AssignDocField(recentlyClosed, "layout_headerButton", (opts) => Docs.Create.FontIconDocument(opts), clearBtnsOpts, undefined, {onClick: clearAll("self.target")});
- if (recentlyClosed.buttonMenuDoc !== clearDocsButton) Doc.GetProto(recentlyClosed).buttonMenuDoc = clearDocsButton;
-
if (!Cast(recentlyClosed.contextMenuScripts, listSpec(ScriptField),null)?.find((script) => script.script.originalScript === clearAll("self"))) {
recentlyClosed.contextMenuScripts = new List<ScriptField>([ScriptField.MakeScript(clearAll("self"))!])
}
@@ -571,7 +573,7 @@ export class CurrentUserUtils {
static setupUserDocView(doc: Doc, field:string) {
const reqdOpts:DocumentOptions = {
_lockedPosition: true, _gridGap: 5, _forceActive: true, title: Doc.CurrentUserEmail +"-view",
- boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", ignoreClick: true, isSystem: true,
+ layout_boxShadow: "0 0", childDontRegisterViews: true, dropAction: "same", ignoreClick: true, isSystem: true,
treeViewHideTitle: true, treeViewTruncateTitleWidth: 350
};
if (!doc[field]) DocUtils.AssignOpts(doc, {treeViewOpen: true, treeViewExpandedView: "fields" });
@@ -579,14 +581,19 @@ export class CurrentUserUtils {
}
static linearButtonList = (opts: DocumentOptions, docs: Doc[]) => Docs.Create.LinearDocument(docs, {
- ...opts, _gridGap: 0, _xMargin: 5, _yMargin: 5, boxShadow: "0 0", _forceActive: true,
+ ...opts, _gridGap: 0, _xMargin: 5, _yMargin: 5, layout_boxShadow: "0 0", _forceActive: true,
+ dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }),
+ _lockedPosition: true, isSystem: true, flexDirection: "row"
+ })
+ static multiToggleList = (opts: DocumentOptions, docs: Doc[]) => Docs.Create.FontIconDocument({
+ ...opts, data:docs, _gridGap: 0, _xMargin: 5, _yMargin: 5, layout_boxShadow: "0 0", _forceActive: true,
dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }),
_lockedPosition: true, isSystem: true, flexDirection: "row"
})
static createToolButton = (opts: DocumentOptions) => Docs.Create.FontIconDocument({
- btnType: ButtonType.ToolButton, _forceActive: true, _hideContextMenu: true,
- _removeDropProperties: new List<string>([ "_hideContextMenu", "stayInCollection"]),
+ btnType: ButtonType.ToolButton, _forceActive: true, _layout_hideContextMenu: true,
+ _dropPropertiesToRemove: new List<string>([ "_layout_hideContextMenu"]),
_nativeWidth: 40, _nativeHeight: 40, _width: 40, _height: 40, isSystem: true, ...opts,
})
@@ -598,18 +605,19 @@ export class CurrentUserUtils {
CurrentUserUtils.createToolButton(opts), scripts, funcs);
const btnDescs = [// setup reactions to change the highlights on the undo/redo buttons -- would be better to encode this in the undo/redo buttons, but the undo/redo stacks are not wired up that way yet
- { scripts: { onClick: "undo()"}, opts: { title: "undo", icon: "undo-alt", toolTip: "Click to undo" }},
- { scripts: { onClick: "redo()"}, opts: { title: "redo", icon: "redo-alt", toolTip: "Click to redo" }},
- { scripts: { }, opts: { title: "linker", icon: "linkui", toolTip: "link started"}},
- { scripts: { }, opts: { title: "currently playing", icon: "currentlyplayingui", toolTip: "currently playing media"}},
+ { scripts: { onClick: "undo()"}, opts: { title: "Undo", icon: "undo-alt", toolTip: "Undo ⌘Z" }},
+ { scripts: { onClick: "redo()"}, opts: { title: "Redo", icon: "redo-alt", toolTip: "Redo ⌘⇧Z" }},
+ { scripts: { }, opts: { title: "undoStack", layout: "<UndoStack>", toolTip: "Undo/Redo Stack"}}, // note: layout fields are hacks -- they don't actually run through the JSX parser (yet)
+ { scripts: { }, opts: { title: "linker", layout: "<LinkingUI>", toolTip: "link started"}},
+ { scripts: { }, opts: { title: "currently playing", layout: "<CurrentlyPlayingUI>", toolTip: "currently playing media"}},
];
- const btns = btnDescs.map(desc => dockBtn({_width: 30, _height: 30, dontUndo: true, _stayInCollection: true, ...desc.opts}, desc.scripts));
+ const btns = btnDescs.map(desc => dockBtn({_width: 30, _height: 30, defaultDoubleClick: 'ignore', undoIgnoreFields: new List<string>(['opacity']), _dragOnlyWithinContainer: true, ...desc.opts}, desc.scripts));
const dockBtnsReqdOpts:DocumentOptions = {
- title: "docked buttons", _height: 40, flexGap: 0, boxShadow: "standard", childDropAction: 'embed',
- childDontRegisterViews: true, linearViewIsExpanded: true, linearViewExpandable: true, ignoreClick: true
+ title: "docked buttons", _height: 40, flexGap: 0, layout_boxShadow: "standard", childDragAction: 'move',
+ childDontRegisterViews: true, linearView_IsOpen: true, linearView_Expandable: true, ignoreClick: true
};
- reaction(() => UndoManager.redoStack.slice(), () => Doc.GetProto(btns.find(btn => btn.title === "redo")!).opacity = UndoManager.CanRedo() ? 1 : 0.4, { fireImmediately: true });
- reaction(() => UndoManager.undoStack.slice(), () => Doc.GetProto(btns.find(btn => btn.title === "undo")!).opacity = UndoManager.CanUndo() ? 1 : 0.4, { fireImmediately: true });
+ reaction(() => UndoManager.redoStack.slice(), () => Doc.GetProto(btns.find(btn => btn.title === "Redo")!).opacity = UndoManager.CanRedo() ? 1 : 0.4, { fireImmediately: true });
+ reaction(() => UndoManager.undoStack.slice(), () => Doc.GetProto(btns.find(btn => btn.title === "Undo")!).opacity = UndoManager.CanUndo() ? 1 : 0.4, { fireImmediately: true });
return DocUtils.AssignDocField(doc, field, (opts, items) => this.linearButtonList(opts, items??[]), dockBtnsReqdOpts, btns);
}
@@ -622,31 +630,35 @@ export class CurrentUserUtils {
}
static viewTools(): Button[] {
return [
- { title: "Snap\xA0Lines", icon: "th", toolTip: "Show Snap Lines", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"snaplines", funcs: {}, scripts: { onClick: 'showFreeform(self.toolType, _readOnly_)'}}, // Only when floating document is selected in freeform
- { title: "Grid", icon: "border-all", toolTip: "Show Grid", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"grid", funcs: {}, scripts: { onClick: 'showFreeform(self.toolType, _readOnly_)'}}, // Only when floating document is selected in freeform
- { title: "View\xA0All", icon: "object-group", toolTip: "Fit all Docs to View",btnType: ButtonType.ToggleButton, expertMode: false, toolType:"viewAll", funcs: {}, scripts: { onClick: 'showFreeform(self.toolType, _readOnly_)'}}, // Only when floating document is selected in freeform
- { title: "Clusters", icon: "braille", toolTip: "Show Doc Clusters", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"clusters", funcs: {}, scripts: { onClick: 'showFreeform(self.toolType, _readOnly_)'}}, // Only when floating document is selected in freeform
- { title: "Arrange",icon: "arrow-down-short-wide",toolTip: "Toggle Auto Arrange",btnType: ButtonType.ToggleButton, expertMode: false, toolType:"arrange", funcs: {hidden: 'IsNoviceMode()'}, scripts: { onClick: 'showFreeform(self.toolType, _readOnly_)'}}, // Only when floating document is selected in freeform
- { title: "Reset", icon: "check", toolTip: "Reset View", btnType: ButtonType.ClickButton, expertMode: false, backgroundColor:"transparent", scripts: { onClick: 'resetView()'}}, // Only when floating document is selected in freeform
+ { title: "Snap", icon: "th", toolTip: "Show Snap Lines", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"snaplines", funcs: {}, scripts: { onClick: '{ return showFreeform(self.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
+ { title: "Grid", icon: "border-all", toolTip: "Show Grid", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"grid", funcs: {}, scripts: { onClick: '{ return showFreeform(self.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
+ { title: "View All", icon: "object-group", toolTip: "Fit all Docs to View", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"viewAll", funcs: {}, scripts: { onClick: '{ return showFreeform(self.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
+ { title: "Clusters", icon: "braille", toolTip: "Show Doc Clusters", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"clusters", funcs: {}, scripts: { onClick: '{ return showFreeform(self.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
+ { title: "Cards", icon: "brain", toolTip: "Flashcards", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"flashcards", funcs: {}, scripts: { onClick: '{ return showFreeform(self.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
+ { title: "Arrange",icon:"arrow-down-short-wide",toolTip:"Toggle Auto Arrange", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"arrange", funcs: {hidden: 'IsNoviceMode()'}, scripts: { onClick: '{ return showFreeform(self.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
]
}
static textTools():Button[] {
return [
- { title: "Font", toolTip: "Font", width: 100, btnType: ButtonType.DropdownList, toolType:"font", ignoreClick: true, scripts: {script: '{ return setFontAttr(self.toolType, value, _readOnly_);}'},
+ { title: "Font", toolTip: "Font", width: 100, btnType: ButtonType.DropdownList, toolType:"font", ignoreClick: true, scripts: {script: '{ return setFontAttr(self.toolType, value, _readOnly_);}'},
btnList: new List<string>(["Roboto", "Roboto Mono", "Nunito", "Times New Roman", "Arial", "Georgia", "Comic Sans MS", "Tahoma", "Impact", "Crimson Text"]) },
- { title: "Size", toolTip: "Font size (%size)", btnType: ButtonType.NumberDropdownButton, width: 75, toolType:"fontSize", ignoreClick: true, scripts: {script: '{ return setFontAttr(self.toolType, value, _readOnly_);}'}, numBtnMax: 200, numBtnMin: 0 },
- { title: "Color", toolTip: "Font color (%color)", btnType: ButtonType.ColorButton, icon: "font", toolType:"fontColor",ignoreClick: true, scripts: {script: '{ return setFontAttr(self.toolType, value, _readOnly_);}'}},
- { title: "Highlight",toolTip:"Font highlight", btnType: ButtonType.ColorButton, icon: "highlighter", toolType:"highlight",ignoreClick: true, scripts: {script: '{ return setFontAttr(self.toolType, value, _readOnly_);}'},funcs: {hidden: "IsNoviceMode()"} },
- { title: "Bold", toolTip: "Bold (Ctrl+B)", btnType: ButtonType.ToggleButton, icon: "bold", toolType:"bold", scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
- { title: "Italic", toolTip: "Italic (Ctrl+I)", btnType: ButtonType.ToggleButton, icon: "italic", toolType:"italics", scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
- { title: "Under", toolTip: "Underline (Ctrl+U)", btnType: ButtonType.ToggleButton, icon: "underline", toolType:"underline", scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
- { title: "Bullets", toolTip: "Bullet List", btnType: ButtonType.ToggleButton, icon: "list", toolType:"bullet", scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
- { title: "#", toolTip: "Number List", btnType: ButtonType.ToggleButton, icon: "list-ol", toolType:"decimal", scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
- { title: "Left", toolTip: "Left align (Cmd-[)", btnType: ButtonType.ToggleButton, icon: "align-left", toolType:"left", scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}' }},
- { title: "Center", toolTip: "Center align (Cmd-\\)",btnType: ButtonType.ToggleButton, icon: "align-center",toolType:"center", scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
- { title: "Right", toolTip: "Right align (Cmd-])", btnType: ButtonType.ToggleButton, icon: "align-right", toolType:"right", scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
- { title: "Dictate", toolTip: "Dictate", btnType: ButtonType.ToggleButton, icon: "microphone", toolType:"dictation", scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'}},
- { title: "NoLink", toolTip: "Auto Link", btnType: ButtonType.ToggleButton, icon: "link", toolType:"noAutoLink", expertMode:true, scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'}, funcs: {hidden: 'IsNoviceMode()'}},
+ { title: "Font Size",toolTip: "Font size (%size)", btnType: ButtonType.NumberDropdownButton, toolType:"fontSize", ignoreClick: true, scripts: {script: '{ return setFontAttr(self.toolType, value, _readOnly_);}'}, numBtnMax: 200, numBtnMin: 6 },
+ { title: "Color", toolTip: "Font color (%color)", btnType: ButtonType.ColorButton, icon: "font", toolType:"fontColor",ignoreClick: true, scripts: {script: '{ return setFontAttr(self.toolType, value, _readOnly_);}'}},
+ { title: "Highlight",toolTip: "Font highlight", btnType: ButtonType.ColorButton, icon: "highlighter", toolType:"highlight",ignoreClick: true, scripts: {script: '{ return setFontAttr(self.toolType, value, _readOnly_);}'},funcs: {hidden: "IsNoviceMode()"} },
+ { title: "Bold", toolTip: "Bold (Ctrl+B)", btnType: ButtonType.ToggleButton, icon: "bold", toolType:"bold", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
+ { title: "Italic", toolTip: "Italic (Ctrl+I)", btnType: ButtonType.ToggleButton, icon: "italic", toolType:"italics", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
+ { title: "Under", toolTip: "Underline (Ctrl+U)", btnType: ButtonType.ToggleButton, icon: "underline", toolType:"underline",ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
+ { title: "Bullets", toolTip: "Bullet List", btnType: ButtonType.ToggleButton, icon: "list", toolType:"bullet", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
+ { title: "#", toolTip: "Number List", btnType: ButtonType.ToggleButton, icon: "list-ol", toolType:"decimal", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
+ { title: "Align", toolTip: "Alignment", btnType: ButtonType.MultiToggleButton, toolType:"alignment", ignoreClick: true,
+ subMenu: [
+ { title: "Left", toolTip: "Left align (Cmd-[)", btnType: ButtonType.ToggleButton, icon: "align-left", toolType:"left", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}' }},
+ { title: "Center", toolTip: "Center align (Cmd-\\)",btnType: ButtonType.ToggleButton, icon: "align-center",toolType:"center",ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
+ { title: "Right", toolTip: "Right align (Cmd-])", btnType: ButtonType.ToggleButton, icon: "align-right", toolType:"right", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
+ ]
+ },
+ { title: "Dictate", toolTip: "Dictate", btnType: ButtonType.ToggleButton, icon: "microphone", toolType:"dictation", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'}},
+ { title: "NoLink", toolTip: "Auto Link", btnType: ButtonType.ToggleButton, icon: "link", toolType:"noAutoLink", expertMode:true, scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'}, funcs: {hidden: 'IsNoviceMode()'}},
// { title: "Strikethrough", tooltip: "Strikethrough", btnType: ButtonType.ToggleButton, icon: "strikethrough", scripts: {onClick:: 'toggleStrikethrough()'}},
// { title: "Superscript", tooltip: "Superscript", btnType: ButtonType.ToggleButton, icon: "superscript", scripts: {onClick:: 'toggleSuperscript()'}},
// { title: "Subscript", tooltip: "Subscript", btnType: ButtonType.ToggleButton, icon: "subscript", scripts: {onClick:: 'toggleSubscript()'}},
@@ -690,19 +702,19 @@ export class CurrentUserUtils {
CollectionViewType.Carousel3D, CollectionViewType.Linear, CollectionViewType.Map,
CollectionViewType.Grid, CollectionViewType.NoteTaking]),
title: "Perspective", toolTip: "View", btnType: ButtonType.DropdownList, ignoreClick: true, width: 100, scripts: { script: 'setView(value, _readOnly_)'}},
- { title: "Pin", icon: "map-pin", toolTip: "Pin View to Trail", btnType: ButtonType.ClickButton, expertMode: false, width: 20, scripts: { onClick: 'pinWithView(altKey)'}},
- { title: "Fill", icon: "fill-drip", toolTip: "Background Fill Color",btnType: ButtonType.ColorButton, expertMode: false, ignoreClick: true, width: 20, scripts: { script: 'return setBackgroundColor(value, _readOnly_)'}}, // Only when a document is selected
- { title: "Header", icon: "heading", toolTip: "Header Color", btnType: ButtonType.ColorButton, expertMode: false, ignoreClick: true, scripts: { script: 'return setHeaderColor(value, _readOnly_)'}},
- { title: "Overlay", icon: "layer-group", toolTip: "Overlay", btnType: ButtonType.ToggleButton, expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode, true)'}, scripts: { onClick: 'toggleOverlay(_readOnly_)'}}, // Only when floating document is selected in freeform
- { title: "Back", icon: "chevron-left", toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode)'}, width: 20, scripts: { onClick: 'prevKeyFrame(_readOnly_)'}},
- { title: "Num", icon:"",toolTip: "Frame Number (click to toggle edit mode)",btnType: ButtonType.TextButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode)', buttonText: 'selectedDocs()?.lastElement()?.currentFrame?.toString()'}, width: 20, scripts: { onClick: '{ return curKeyFrame(_readOnly_);}'}},
- { title: "Fwd", icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode)'}, width: 20, scripts: { onClick: 'nextKeyFrame(_readOnly_)'}},
- { title: "Text", icon: "Text", toolTip: "Text functions", subMenu: CurrentUserUtils.textTools(), expertMode: false, toolType:DocumentType.RTF, funcs: { linearViewIsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Always available
- { title: "Ink", icon: "Ink", toolTip: "Ink functions", subMenu: CurrentUserUtils.inkTools(), expertMode: false, toolType:DocumentType.INK, funcs: { linearViewIsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`}, scripts: { onClick: 'setInkToolDefaults()'} }, // Always available
- { title: "Doc", icon: "Doc", toolTip: "Freeform Doc tools", subMenu: CurrentUserUtils.freeTools(), expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode, true)`, linearViewIsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Always available
- { title: "View", icon: "View", toolTip: "View tools", subMenu: CurrentUserUtils.viewTools(), expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode)`, linearViewIsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Always available
- { title: "Web", icon: "Web", toolTip: "Web functions", subMenu: CurrentUserUtils.webTools(), expertMode: false, toolType:DocumentType.WEB, funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode)`, linearViewIsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Only when Web is selected
- { title: "Schema", icon: "Schema",linearBtnWidth:58,toolTip: "Schema functions", subMenu: CurrentUserUtils.schemaTools(), expertMode: false, toolType:CollectionViewType.Schema, funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode)`, linearViewIsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} } // Only when Schema is selected
+ { title: "Pin", icon: "map-pin", toolTip: "Pin View to Trail", btnType: ButtonType.ClickButton, expertMode: false, width: 30, scripts: { onClick: 'pinWithView(altKey)'}, funcs: {hidden: "IsNoneSelected()"}},
+ { title: "Fill", icon: "fill-drip", toolTip: "Background Fill Color",btnType: ButtonType.ColorButton, expertMode: false, ignoreClick: true, width: 30, scripts: { script: 'return setBackgroundColor(value, _readOnly_)'}, funcs: {hidden: "IsNoneSelected()"}}, // Only when a document is selected
+ { title: "Header", icon: "heading", toolTip: "Header Color", btnType: ButtonType.ColorButton, expertMode: true, ignoreClick: true, scripts: { script: 'return setHeaderColor(value, _readOnly_)'}, funcs: {hidden: "IsNoneSelected()"}},
+ { title: "Overlay", icon: "layer-group", toolTip: "Overlay", btnType: ButtonType.ToggleButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode, true)'}, scripts: { onClick: 'return { toggleOverlay(_readOnly_); }'}}, // Only when floating document is selected in freeform
+ { title: "Back", icon: "chevron-left", toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode)'}, width: 30, scripts: { onClick: 'prevKeyFrame(_readOnly_)'}},
+ { title: "Num", icon:"", toolTip: "Frame Number (click to toggle edit mode)", btnType: ButtonType.TextButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode)', buttonText: 'selectedDocs()?.lastElement()?.currentFrame?.toString()'}, width: 20, scripts: { onClick: '{ return curKeyFrame(_readOnly_);}'}},
+ { title: "Fwd", icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode)'}, width: 30, scripts: { onClick: 'nextKeyFrame(_readOnly_)'}},
+ { title: "Text", icon: "Text", toolTip: "Text functions", subMenu: CurrentUserUtils.textTools(), expertMode: false, toolType:DocumentType.RTF, funcs: { linearView_IsOpen: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Always available
+ { title: "Ink", icon: "Ink", toolTip: "Ink functions", subMenu: CurrentUserUtils.inkTools(), expertMode: false, toolType:DocumentType.INK, funcs: { linearView_IsOpen: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`}, scripts: { onClick: 'setInkToolDefaults()'} }, // Always available
+ { title: "Doc", icon: "Doc", toolTip: "Freeform Doc tools", subMenu: CurrentUserUtils.freeTools(), expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode, true)`, linearView_IsOpen: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Always available
+ { title: "View", icon: "View", toolTip: "View tools", subMenu: CurrentUserUtils.viewTools(), expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode)`, linearView_IsOpen: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Always available
+ { title: "Web", icon: "Web", toolTip: "Web functions", subMenu: CurrentUserUtils.webTools(), expertMode: false, toolType:DocumentType.WEB, funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode)`, linearView_IsOpen: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Only when Web is selected
+ { title: "Schema", icon: "Schema",linearBtnWidth:58,toolTip: "Schema functions",subMenu: CurrentUserUtils.schemaTools(), expertMode: false, toolType:CollectionViewType.Schema, funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode)`, linearView_IsOpen: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} } // Only when Schema is selected
];
}
@@ -710,40 +722,42 @@ export class CurrentUserUtils {
static setupContextMenuButton(params:Button, btnDoc?:Doc) {
const reqdOpts:DocumentOptions = {
...OmitKeys(params, ["scripts", "funcs", "subMenu"]).omit,
- backgroundColor: params.backgroundColor ??"transparent", /// a bit hacky. if an onClick is specified, then assume a toggle uses onClick to get the backgroundColor (see below). Otherwise, assume a transparent background
- color: Colors.WHITE, isSystem: true, dontUndo: true,
+ color: Colors.WHITE, isSystem: true,
_nativeWidth: params.width ?? 30, _width: params.width ?? 30,
_height: 30, _nativeHeight: 30, linearBtnWidth: params.linearBtnWidth,
toolType: params.toolType, expertMode: params.expertMode,
- _stayInCollection: true, _hideContextMenu: true, _lockedPosition: true,
- _removeDropProperties: new List<string>([ "_stayInCollection"]),
+ _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, _lockedPosition: true,
};
const reqdFuncs:{[key:string]:any} = {
...params.funcs,
- backgroundColor: params.btnType === ButtonType.ToggleButton ? params.scripts?.onClick:undefined /// a bit hacky. if onClick is set, then we assume it returns a color value when queried with '_readOnly_'. This will be true for toggle buttons, but not generally
}
return DocUtils.AssignScripts(DocUtils.AssignOpts(btnDoc, reqdOpts) ?? Docs.Create.FontIconDocument(reqdOpts), params.scripts, reqdFuncs);
}
+ static setupContextMenuBtn(params:Button, menuDoc:Doc):Doc {
+ const menuBtnDoc = DocListCast(menuDoc?.data).find(doc => doc.title === params.title);
+ const subMenu = params.subMenu;
+ if (!subMenu) { // button does not have a sub menu
+ return this.setupContextMenuButton(params, menuBtnDoc);
+ }
+ // linear view
+ const reqdSubMenuOpts = { ...OmitKeys(params, ["scripts", "funcs", "subMenu"]).omit, undoIgnoreFields: new List<string>(['width', "linearView_IsOpen"]),
+ childDontRegisterViews: true, flexGap: 0, _height: 30, ignoreClick: params.scripts?.onClick ? false : true,
+ linearView_SubMenu: true, linearView_Expandable: params.btnType !== ButtonType.MultiToggleButton};
+
+ const items = (menuBtnDoc?:Doc) => !menuBtnDoc ? [] : subMenu.map(sub => this.setupContextMenuBtn(sub, menuBtnDoc) );
+ const creator = params.btnType === ButtonType.MultiToggleButton ? this.multiToggleList : this.linearButtonList;
+ const btnDoc = DocUtils.AssignScripts( DocUtils.AssignDocField(menuDoc, StrCast(params.title),
+ (opts) => creator(opts, items(menuBtnDoc)), reqdSubMenuOpts, items(menuBtnDoc)), params.scripts, params.funcs);
+ if (!menuBtnDoc) Doc.GetProto(btnDoc).data = new List<Doc>(items(btnDoc));
+ return btnDoc;
+ }
+
/// Initializes all the default buttons for the top bar context menu
static setupContextMenuButtons(doc: Doc, field="myContextMenuBtns") {
- const reqdCtxtOpts:DocumentOptions = { title: "context menu buttons", flexGap: 0, childDropAction: 'embed', childDontRegisterViews: true, linearViewIsExpanded: true, ignoreClick: true, linearViewExpandable: false, _height: 35 };
+ const reqdCtxtOpts:DocumentOptions = { title: "context menu buttons", undoIgnoreFields:new List<string>(['width', "linearView_IsOpen"]), flexGap: 0, childDragAction: 'embed', childDontRegisterViews: true, linearView_IsOpen: true, ignoreClick: true, linearView_Expandable: false, _height: 35 };
const ctxtMenuBtnsDoc = DocUtils.AssignDocField(doc, field, (opts, items) => this.linearButtonList(opts, items??[]), reqdCtxtOpts, undefined);
- const ctxtMenuBtns = CurrentUserUtils.contextMenuTools().map(params => {
- const menuBtnDoc = DocListCast(ctxtMenuBtnsDoc?.data).find(doc => doc.title === params.title);
- if (!params.subMenu) {
- return this.setupContextMenuButton(params, menuBtnDoc);
- } else {
- const reqdSubMenuOpts = { ...OmitKeys(params, ["scripts", "funcs", "subMenu"]).omit,
- childDontRegisterViews: true, flexGap: 0, _height: 30, ignoreClick: params.scripts?.onClick ? false : true,
- linearViewSubMenu: true, linearViewExpandable: true, };
- const items = params.subMenu?.map(sub =>
- this.setupContextMenuButton(sub, DocListCast(menuBtnDoc?.data).find(doc => doc.title === sub.title))
- );
- return DocUtils.AssignScripts(
- DocUtils.AssignDocField(ctxtMenuBtnsDoc, StrCast(params.title), (opts) => this.linearButtonList(opts, items??[]), reqdSubMenuOpts, items), params.scripts, params.funcs);
- }
- });
+ const ctxtMenuBtns = CurrentUserUtils.contextMenuTools().map(params => this.setupContextMenuBtn(params, ctxtMenuBtnsDoc) );
return DocUtils.AssignOpts(ctxtMenuBtnsDoc, reqdCtxtOpts, ctxtMenuBtns);
}
@@ -762,8 +776,9 @@ export class CurrentUserUtils {
const linkDocs = new Doc(linkDatabaseId, true);
linkDocs.title = "LINK DATABASE: " + Doc.CurrentUserEmail;
linkDocs.author = Doc.CurrentUserEmail;
+ linkDocs.isSystem = true;
linkDocs.data = new List<Doc>([]);
- linkDocs["acl-Public"] = SharingPermissions.Augment;
+ linkDocs["acl-Guest"] = SharingPermissions.Augment;
doc.myLinkDatabase = new PrefetchProxy(linkDocs);
}
}
@@ -773,43 +788,42 @@ export class CurrentUserUtils {
// When the user views one of these documents, it will be added to the sharing documents 'viewed' list field
// The sharing document also stores the user's color value which helps distinguish shared documents from personal documents
static setupSharedDocs(doc: Doc, sharingDocumentId: string) {
- const addToDashboards = ScriptField.MakeScript(`addToDashboards(self)`);
- const dashboardFilter = ScriptField.MakeFunction(`doc._viewType === '${CollectionViewType.Docking}'`, { doc: Doc.name });
const dblClkScript = "{scriptContext.openLevel(documentView); addDocToList(scriptContext.props.treeView.props.Document, 'viewed', documentView.rootDoc);}";
const sharedScripts = { treeViewChildDoubleClick: dblClkScript, }
const sharedDocOpts:DocumentOptions = {
title: "My Shared Docs",
userColor: "rgb(202, 202, 202)",
- isFolder:true,
- childContextMenuFilters: new List<ScriptField>([dashboardFilter!,]),
- childContextMenuScripts: new List<ScriptField>([addToDashboards!,]),
- childContextMenuLabels: new List<string>(["Add to Dashboards",]),
- childContextMenuIcons: new List<string>(["user-plus",]),
- "acl-Public": SharingPermissions.Augment, "_acl-Public": SharingPermissions.Augment,
- childDropAction: "embed", isSystem: true, contentPointerEvents: "all", childLimitHeight: 0, _yMargin: 0, _gridGap: 15, childDontRegisterViews:true,
+ isFolder:true, undoIgnoreFields:new List<string>(['treeViewSortCriterion']),
+ // childContextMenuFilters: new List<ScriptField>([dashboardFilter!,]),
+ // childContextMenuScripts: new List<ScriptField>([addToDashboards!,]),
+ // childContextMenuLabels: new List<string>(["Add to Dashboards",]),
+ // childContextMenuIcons: new List<string>(["user-plus",]),
+ "acl-Guest": SharingPermissions.Augment, "_acl-Guest": SharingPermissions.Augment,
+ childDragAction: "embed", isSystem: true, contentPointerEvents: "all", childLimitHeight: 0, _yMargin: 0, _gridGap: 15, childDontRegisterViews:true,
// NOTE: treeViewHideTitle & _layout_showTitle is for a TreeView's editable title, _layout_showTitle is for DocumentViews title bar
- _layout_showTitle: "title", treeViewHideTitle: true, ignoreClick: true, _lockedPosition: true, boxShadow: "0 0", _chromeHidden: true, dontRegisterView: true,
- explainer: "This is where documents or dashboards that other users have shared with you will appear. To share a document or dashboard right click and select 'Share'"
+ _layout_showTitle: "title", treeViewHideTitle: true, ignoreClick: true, _lockedPosition: true, layout_boxShadow: "0 0", _chromeHidden: true, dontRegisterView: true,
+ layout_explainer: "This is where documents or dashboards that other users have shared with you will appear. To share a document or dashboard right click and select 'Share'"
};
DocUtils.AssignDocField(doc, "mySharedDocs", opts => Docs.Create.TreeDocument([], opts, sharingDocumentId + "layout", sharingDocumentId), sharedDocOpts, undefined, sharedScripts);
+ if (!Doc.GetProto(DocCast(doc.mySharedDocs)).data_dashboards) Doc.GetProto(DocCast(doc.mySharedDocs)).data_dashboards = new List<Doc>();
}
/// Import option on the left side button panel
- static setupImportSidebar(doc: Doc, field:string) {
+ static setupImportSidebar(doc: Doc, field:string) {
const reqdOpts:DocumentOptions = {
- title: "My Imports", _forceActive: true, buttonMenu: true, ignoreClick: true, _layout_showTitle: "title",
- _stayInCollection: true, _hideContextMenu: true, childLimitHeight: 0,
- childDropAction: "copy", _layout_autoHeight: true, _yMargin: 50, _gridGap: 15, boxShadow: "0 0", _lockedPosition: true, isSystem: true, _chromeHidden: true,
- dontRegisterView: true, explainer: "This is where documents that are Imported into Dash will go."
+ title: "My Imports", _forceActive: true, _layout_showTitle: "title", childLayoutString: ImportElementBox.LayoutString('data'),
+ _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, childLimitHeight: 0, onClickScriptDisable:"never",
+ childDragAction: "copy", _layout_autoHeight: true, _yMargin: 50, _gridGap: 15, layout_boxShadow: "0 0", _lockedPosition: true, isSystem: true, _chromeHidden: true,
+ dontRegisterView: true, layout_explainer: "This is where documents that are Imported into Dash will go."
};
- const myImports = DocUtils.AssignDocField(doc, field, (opts) => Docs.Create.StackingDocument([], opts), reqdOpts);
+ const myImports = DocUtils.AssignDocField(doc, field, (opts) => Docs.Create.MasonryDocument([], opts), reqdOpts, undefined, {onClick: "deselectAll()"});
const reqdBtnOpts:DocumentOptions = { _forceActive: true, toolTip: "Import from computer",
- _width: 30, _height: 30, _stayInCollection: true, _hideContextMenu: true, title: "Import", btnType: ButtonType.ClickButton,
+ _width: 30, _height: 30, color: Colors.BLACK, _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, title: "Import", btnType: ButtonType.ClickButton,
buttonText: "Import", icon: "upload", isSystem: true };
- DocUtils.AssignDocField(myImports, "buttonMenuDoc", (opts) => Docs.Create.FontIconDocument(opts), reqdBtnOpts, undefined, { onClick: "importDocument()" });
+ DocUtils.AssignDocField(myImports, "layout_headerButton", (opts) => Docs.Create.FontIconDocument(opts), reqdBtnOpts, undefined, { onClick: "importDocument()" });
return myImports;
}
/// Updates the UserDoc to have all required fields, docs, etc. No changes should need to be
@@ -821,7 +835,7 @@ export class CurrentUserUtils {
async () => {
const groups = await DocListCastAsync(DocCast(doc.globalGroupDatabase).data);
const mygroups = groups?.filter(group => JSON.parse(StrCast(group.members)).includes(Doc.CurrentUserEmail)) || [];
- SetCachedGroups(["Public", ...mygroups?.map(g => StrCast(g.title))]);
+ SetCachedGroups(["Guest", ...mygroups?.map(g => StrCast(g.title))]);
}, { fireImmediately: true });
doc.isSystem ?? (doc.isSystem = true);
doc.title ?? (doc.title = Doc.CurrentUserEmail);
@@ -845,8 +859,14 @@ export class CurrentUserUtils {
doc.fontHighlight ?? (doc.fontHighlight = "");
doc.defaultAclPrivate ?? (doc.defaultAclPrivate = false);
doc.savedFilters ?? (doc.savedFilters = new List<Doc>());
+ doc.userBackgroundColor ?? (doc.userBackgroundColor = Colors.DARK_GRAY);
+ addStyleSheetRule(SettingsManager._settingsStyle, 'lm_header', { background: `${doc.userBackgroundColor} !important` });
+ doc.userVariantColor ?? (doc.userVariantColor = Colors.MEDIUM_BLUE);
+ doc.userColor ?? (doc.userColor = Colors.LIGHT_GRAY);
+ doc.userTheme ?? (doc.userTheme = ColorScheme.Dark);
doc.filterDocCount = 0;
- doc.freezeChildren = "remove|add";
+ doc.treeViewFreezeChildren = "remove|add";
+ doc.activePage = doc.activeDashboard === undefined ? 'home': doc.activePage;
this.setupLinkDocs(doc, linkDatabaseId);
this.setupSharedDocs(doc, sharingDocumentId); // sets up the right sidebar collection for mobile upload documents and sharing
this.setupDefaultIconTemplates(doc); // creates a set of icon templates triggered by the document deoration icon
@@ -857,11 +877,13 @@ export class CurrentUserUtils {
this.setupDockedButtons(doc); // the bottom bar of font icons
this.setupLeftSidebarMenu(doc); // the left-side column of buttons that open their contents in a flyout panel on the left
this.setupDocTemplates(doc); // sets up the template menu of templates
- this.setupFieldInfos(doc); // sets up the collection of field info descriptions for each possible DocumentOption
+ //this.setupFieldInfos(doc); // sets up the collection of field info descriptions for each possible DocumentOption
DocUtils.AssignDocField(doc, "globalScriptDatabase", (opts) => Docs.Prototypes.MainScriptDocument(), {});
- DocUtils.AssignDocField(doc, "myHeaderBar", (opts) => Docs.Create.MulticolumnDocument([], opts), { title: "header bar", isSystem: true }); // drop down panel at top of dashboard for stashing documents
+ DocUtils.AssignDocField(doc, "myHeaderBar", (opts) => Docs.Create.MulticolumnDocument([], opts), { title: "My Header Bar", isSystem: true, childDocumentsActive:false, dropAction: 'move'}); // drop down panel at top of dashboard for stashing documents
+ Doc.AddDocToList(Doc.MyFilesystem, undefined, Doc.MyDashboards)
Doc.AddDocToList(Doc.MyFilesystem, undefined, Doc.MySharedDocs)
+ Doc.AddDocToList(Doc.MyFilesystem, undefined, Doc.MyRecentlyClosed)
if (doc.activeDashboard instanceof Doc) {
// undefined means ColorScheme.Light until all CSS is updated with values for each color scheme (e.g., see MainView.scss, DocumentDecorations.scss)
@@ -869,7 +891,8 @@ export class CurrentUserUtils {
}
new LinkManager();
- setTimeout(DocServer.UPDATE_SERVER_CACHE, 2500);
+ DocServer.CacheNeedsUpdate && setTimeout(DocServer.UPDATE_SERVER_CACHE, 2500);
+ setInterval(DocServer.UPDATE_SERVER_CACHE, 120000);
return doc;
}
static setupFieldInfos(doc:Doc, field="fieldInfos") {
@@ -891,22 +914,22 @@ export class CurrentUserUtils {
});
}
+ @observable public static ServerVersion: string = ';'
public static async loadCurrentUser() {
return rp.get(Utils.prepend("/getCurrentUser")).then(async response => {
if (response) {
- const result: { id: string, email: string, cacheDocumentIds: string } = JSON.parse(response);
+ const result: { version: string, userDocumentId: string, sharingDocumentId: string, linkDatabaseId: string, email: string, cacheDocumentIds: string, resolvedPorts: string } = JSON.parse(response);
+ runInAction(() => CurrentUserUtils.ServerVersion = result.version);
Doc.CurrentUserEmail = result.email;
- resolvedPorts = JSON.parse(await (await fetch("/resolvedPorts")).text());
+ resolvedPorts = result.resolvedPorts as any;
DocServer.init(window.location.protocol, window.location.hostname, resolvedPorts.socket, result.email);
if (result.cacheDocumentIds)
{
const ids = result.cacheDocumentIds.split(";");
const batch = 30000;
- FieldLoader.active = true;
for (let i = 0; i < ids.length; i = Math.min(ids.length, i+batch)) {
await DocServer.GetRefFields(ids.slice(i, i+batch));
}
- FieldLoader.active = false;
}
return result;
} else {
@@ -915,27 +938,24 @@ export class CurrentUserUtils {
});
}
- public static async loadUserDocument(id: string) {
- await rp.get(Utils.prepend("/getUserDocumentIds")).then(ids => {
- const { userDocumentId, sharingDocumentId, linkDatabaseId } = JSON.parse(ids);
- if (userDocumentId) {
- return DocServer.GetRefField(userDocumentId).then(async field => {
- Docs.newAccount = !(field instanceof Doc);
- await Docs.Prototypes.initialize();
- const userDoc = Docs.newAccount ? new Doc(userDocumentId, true) : field as Doc;
- this.updateUserDocument(Doc.SetUserDoc(userDoc), sharingDocumentId, linkDatabaseId);
- if (Docs.newAccount) {
- if (Doc.CurrentUserEmail === "guest") {
- DashboardView.createNewDashboard(undefined, "guest dashboard");
- } else {
- userDoc.activePage = "home";
- }
- }
- return userDoc;
- });
- } else {
- throw new Error("There should be a user id! Why does Dash think there isn't one?");
+ public static async loadUserDocument(info:{
+ userDocumentId: string;
+ sharingDocumentId: string;
+ linkDatabaseId: string;
+ }) {
+ return DocServer.GetRefField(info.userDocumentId).then(async field => {
+ Docs.newAccount = !(field instanceof Doc);
+ await Docs.Prototypes.initialize();
+ const userDoc = Docs.newAccount ? new Doc(info.userDocumentId, true) : field as Doc;
+ this.updateUserDocument(Doc.SetUserDoc(userDoc), info.sharingDocumentId, info.linkDatabaseId);
+ if (Docs.newAccount) {
+ if (Doc.CurrentUserEmail === "guest") {
+ DashboardView.createNewDashboard(undefined, "guest dashboard");
+ } else {
+ userDoc.activePage = "home";
+ }
}
+ return userDoc;
});
}
@@ -946,10 +966,8 @@ export class CurrentUserUtils {
input.multiple = true;
input.accept = ".zip, application/pdf, video/*, image/*, audio/*";
input.onchange = async _e => {
- const upload = Utils.prepend("/uploadDoc");
- const formData = new FormData();
const file = input.files?.[0];
- if (file?.type === 'application/zip') {
+ if (file?.type === 'application/zip' || file?.type === 'application/x-zip-compressed') {
const doc = await Doc.importDocument(file);
// NOT USING SOLR, so need to replace this with something else // if (doc instanceof Doc) {
// setTimeout(() => SearchUtil.Search(`{!join from=id to=proto_i}id:link*`, true, {}).then(docs =>
diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts
index 1cce21034..717473aa1 100644
--- a/src/client/util/DictationManager.ts
+++ b/src/client/util/DictationManager.ts
@@ -5,7 +5,7 @@ import { Doc, Opt } from '../../fields/Doc';
import { List } from '../../fields/List';
import { RichTextField } from '../../fields/RichTextField';
import { listSpec } from '../../fields/Schema';
-import { Cast, CastCtor } from '../../fields/Types';
+import { Cast, CastCtor, DocCast } from '../../fields/Types';
import { AudioField, ImageField } from '../../fields/URLField';
import { Utils } from '../../Utils';
import { Docs } from '../documents/Documents';
@@ -328,7 +328,7 @@ export namespace DictationManager {
{
action: (target: DocumentView) => {
const newBox = Docs.Create.TextDocument('', { _width: 400, _height: 200, title: 'My Outline', _layout_autoHeight: true });
- const proto = newBox.proto!;
+ const proto = DocCast(newBox.proto);
const prompt = 'Press alt + r to start dictating here...';
const head = 3;
const anchor = head + prompt.length;
@@ -372,7 +372,7 @@ export namespace DictationManager {
expression: /view as (freeform|stacking|masonry|schema|tree)/g,
action: (target: DocumentView, matches: RegExpExecArray) => {
const mode = matches[1];
- mode && (target.props.Document._viewType = mode);
+ mode && (target.props.Document._type_collection = mode);
},
restrictTo: [DocumentType.COL],
}
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 6eed4dc86..42132c2d7 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -1,9 +1,11 @@
import { action, computed, observable, ObservableSet } from 'mobx';
-import { AnimationSym, Doc, Opt } from '../../fields/Doc';
+import { Doc, DocListCast, Opt } from '../../fields/Doc';
+import { AclAdmin, AclEdit, Animation } from '../../fields/DocSymbols';
import { Id } from '../../fields/FieldSymbols';
import { listSpec } from '../../fields/Schema';
import { Cast, DocCast, StrCast } from '../../fields/Types';
import { AudioField } from '../../fields/URLField';
+import { GetEffectiveAcl } from '../../fields/util';
import { CollectionViewType } from '../documents/DocumentTypes';
import { CollectionDockingView } from '../views/collections/CollectionDockingView';
import { TabDocView } from '../views/collections/TabDocView';
@@ -24,7 +26,13 @@ export class DocumentManager {
@observable public RecordingEvent = 0;
@observable public LinkedDocumentViews: { a: DocumentView; b: DocumentView; l: Doc }[] = [];
@computed public get DocumentViews() {
- return Array.from(this._documentViews).filter(view => !(view.ComponentView instanceof KeyValueBox));
+ return Array.from(this._documentViews).filter(view => !(view.ComponentView instanceof KeyValueBox) && (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(view.docViewPath)));
+ }
+ public AddDocumentView(dv: DocumentView) {
+ this._documentViews.add(dv);
+ }
+ public DeleteDocumentView(dv: DocumentView) {
+ this._documentViews.delete(dv);
}
private static _instance: DocumentManager;
@@ -67,7 +75,7 @@ export class DocumentManager {
@action
public AddView = (view: DocumentView) => {
- //console.log("MOUNT " + view.props.Document.title + "/" + view.props.LayoutTemplateString);
+ if (view.props.LayoutTemplateString?.includes(KeyValueBox.name)) return;
if (view.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) {
const viewAnchorIndex = view.props.LayoutTemplateString.includes('link_anchor_2') ? 'link_anchor_2' : 'link_anchor_1';
const link = view.rootDoc;
@@ -82,7 +90,7 @@ export class DocumentManager {
// this.LinkedDocumentViews.forEach(view => console.log(" LV = " + view.a.props.Document.title + "/" + view.a.props.LayoutTemplateString + " --> " +
// view.b.props.Document.title + "/" + view.b.props.LayoutTemplateString));
} else {
- this._documentViews.add(view);
+ this.AddDocumentView(view);
}
this.callAddViewFuncs(view);
};
@@ -100,7 +108,7 @@ export class DocumentManager {
const index = this.LinkAnchorBoxViews.indexOf(view);
this.LinkAnchorBoxViews.splice(index, 1);
} else {
- this._documentViews.delete(view);
+ this.DeleteDocumentView(view);
}
SelectionManager.DeselectView(view);
});
@@ -115,8 +123,7 @@ export class DocumentManager {
});
if (toReturn.length === 0) {
DocumentManager.Instance.DocumentViews.forEach(view => {
- const doc = view.rootDoc.proto;
- if (doc && doc[Id] && doc[Id] === id) {
+ if (Doc.GetProto(view.rootDoc)?.[Id] === id) {
toReturn.push(view);
}
});
@@ -143,7 +150,7 @@ export class DocumentManager {
(pass, toReturn) =>
toReturn ??
docViewArray.filter(view => view.rootDoc === doc).find(view => !pass || view.props.docViewPath().lastElement() === preferredCollection) ??
- docViewArray.filter(view => Doc.GetProto(view.rootDoc) === doc).find(view => !pass || view.props.docViewPath().lastElement() === preferredCollection),
+ docViewArray.filter(view => Doc.AreProtosEqual(view.rootDoc, doc)).find(view => !pass || view.props.docViewPath().lastElement() === preferredCollection),
undefined as Opt<DocumentView>
);
}
@@ -188,8 +195,8 @@ export class DocumentManager {
var containerDocContext = srcContext ? [srcContext, doc] : [doc];
while (
containerDocContext.length &&
- containerDocContext[0]?.embedContainer &&
- DocCast(containerDocContext[0].embedContainer)?.viewType !== CollectionViewType.Docking &&
+ DocCast(containerDocContext[0]?.embedContainer) &&
+ DocCast(containerDocContext[0].embedContainer)?._type_collection !== CollectionViewType.Docking &&
(includeExistingViews || !DocumentManager.Instance.getDocumentView(containerDocContext[0]))
) {
containerDocContext = [Cast(containerDocContext[0].embedContainer, Doc, null), ...containerDocContext];
@@ -228,7 +235,7 @@ export class DocumentManager {
public showDocumentView = async (targetDocView: DocumentView, options: DocFocusOptions) => {
const docViewPath = targetDocView.docViewPath.slice();
let rootContextView = docViewPath.shift();
- await (rootContextView && this.focusViewsInPath(rootContextView, options, async () => ({ childDocView: docViewPath.shift(), viewSpec: undefined })));
+ await (rootContextView && this.focusViewsInPath(rootContextView, options, async () => ({ childDocView: docViewPath.shift(), viewSpec: undefined, focused: false })));
if (options.toggleTarget && (!options.didMove || targetDocView.rootDoc.hidden)) targetDocView.rootDoc.hidden = !targetDocView.rootDoc.hidden;
else if (options.openLocation?.startsWith(OpenWhere.toggle) && !options.didMove && rootContextView) DocumentViewInternal.addDocTabFunc(rootContextView.rootDoc, options.openLocation);
};
@@ -242,17 +249,23 @@ export class DocumentManager {
public showDocument = async (
targetDoc: Doc, // document to display
options: DocFocusOptions, // options for how to navigate to target
- finished?: () => void
+ finished?: (changed: boolean) => void // func called after focusing on target with flag indicating whether anything needed to be done.
) => {
+ Doc.RemoveDocFromList(Doc.MyRecentlyClosed, undefined, targetDoc);
const docContextPath = DocumentManager.GetContextPath(targetDoc, true);
if (docContextPath.some(doc => doc.hidden)) options.toggleTarget = false;
- let rootContextView = await new Promise<DocumentView>(res => {
- const viewIndex = docContextPath.findIndex(doc => this.getDocumentView(doc));
- if (viewIndex !== -1) return res(this.getDocumentView(docContextPath[viewIndex])!);
- options.didMove = true;
- docContextPath.some(doc => TabDocView.Activate(doc)) || DocumentViewInternal.addDocTabFunc(docContextPath[0], options.openLocation ?? OpenWhere.addRight);
- this.AddViewRenderedCb(docContextPath[0], dv => res(dv));
- });
+ let rootContextView =
+ docContextPath.length &&
+ (await new Promise<DocumentView>(res => {
+ const viewIndex = docContextPath.findIndex(doc => this.getDocumentView(doc));
+ if (viewIndex !== -1) {
+ viewIndex && docContextPath.splice(0, viewIndex);
+ return res(this.getDocumentView(docContextPath[0])!);
+ }
+ options.didMove = true;
+ docContextPath.some(doc => TabDocView.Activate(doc)) || DocumentViewInternal.addDocTabFunc(docContextPath[0], options.openLocation ?? OpenWhere.addRight);
+ this.AddViewRenderedCb(docContextPath[0], dv => res(dv));
+ }));
if (options.openLocation === OpenWhere.lightbox) {
// even if we found the document view, if the target is a lightbox, we try to open it in the lightbox to preserve lightbox semantics (eg, there's only one active doc in the lightbox)
const target = DocCast(targetDoc.annotationOn, targetDoc);
@@ -264,21 +277,29 @@ export class DocumentManager {
docContextPath.shift();
const childViewIterator = async (docView: DocumentView) => {
const innerDoc = docContextPath.shift();
- return { viewSpec: innerDoc, childDocView: innerDoc && !innerDoc.unrendered ? (await docView.ComponentView?.getView?.(innerDoc)) ?? this.getDocumentView(innerDoc) : undefined };
+ return { focused: false, viewSpec: innerDoc, childDocView: innerDoc && !innerDoc.layout_unrendered ? (await docView.ComponentView?.getView?.(innerDoc)) ?? this.getDocumentView(innerDoc) : undefined };
};
- const target = await this.focusViewsInPath(rootContextView, options, childViewIterator);
- this.restoreDocView(target.viewSpec, target.docView, options, target.contextView ?? target.docView, targetDoc);
- finished?.();
+ if (rootContextView) {
+ const target = await this.focusViewsInPath(rootContextView, options, childViewIterator);
+ this.restoreDocView(target.viewSpec, target.docView, options, target.contextView ?? target.docView, targetDoc);
+ finished?.(target.focused);
+ } else finished?.(false);
};
- focusViewsInPath = async (docView: DocumentView, options: DocFocusOptions, iterator: (docView: DocumentView) => Promise<{ viewSpec: Opt<Doc>; childDocView: Opt<DocumentView> }>) => {
+ focusViewsInPath = async (
+ docView: DocumentView, //
+ options: DocFocusOptions,
+ iterator: (docView: DocumentView) => Promise<{ viewSpec: Opt<Doc>; childDocView: Opt<DocumentView>; focused: boolean }>
+ ) => {
let contextView: DocumentView | undefined; // view containing context that contains target
+ let focused = false;
while (true) {
docView.rootDoc.layout_fieldKey === 'layout_icon' ? await new Promise<void>(res => docView.iconify(res)) : undefined;
- docView.props.focus(docView.rootDoc, options); // focus the view within its container
+ const nextFocus = docView.props.focus(docView.rootDoc, options); // focus the view within its container
+ focused = focused || (nextFocus === undefined ? false : true); // keep track of whether focusing on a view needed to actually change anything
const { childDocView, viewSpec } = await iterator(docView);
- if (!childDocView) return { viewSpec: options.anchorDoc ?? viewSpec ?? docView.rootDoc, docView, contextView };
+ if (!childDocView) return { viewSpec: options.anchorDoc ?? viewSpec ?? docView.rootDoc, docView, contextView, focused };
contextView = docView;
docView = childDocView;
}
@@ -292,7 +313,7 @@ export class DocumentManager {
Doc.linkFollowHighlight(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[AnimationSym] = options.effect;
+ if (options.effect) docView.rootDoc[Animation] = options.effect;
if (options.zoomTextSelections && Doc.UnhighlightTimer && contextView && viewSpec.textHtml) {
// if the docView is a text anchor, the contextView is the PDF/Web/Text doc
@@ -301,7 +322,7 @@ export class DocumentManager {
DocumentManager._overlayViews.add(contextView);
}
Doc.AddUnHighlightWatcher(() => {
- docView.rootDoc[AnimationSym] = undefined;
+ docView.rootDoc[Animation] = undefined;
DocumentManager.removeOverlayViews();
contextView && (contextView.htmlOverlayEffect = '');
});
@@ -315,7 +336,7 @@ export function DocFocusOrOpen(doc: Doc, options: DocFocusOptions = { willZoomCe
if (dv && (!containingDoc || dv.props.docViewPath().lastElement()?.Document === containingDoc)) {
DocumentManager.Instance.showDocumentView(dv, options).then(() => dv && Doc.linkFollowHighlight(dv.rootDoc));
} else {
- const container = DocCast(containingDoc ?? doc.embedContainer);
+ const container = DocCast(containingDoc ?? doc.embedContainer ?? Doc.BestEmbedding(doc));
const showDoc = !Doc.IsSystem(container) ? container : doc;
options.toggleTarget = undefined;
DocumentManager.Instance.showDocument(showDoc, options, () => DocumentManager.Instance.showDocument(doc, { ...options, openLocation: undefined })).then(() => {
@@ -325,6 +346,9 @@ export function DocFocusOrOpen(doc: Doc, options: DocFocusOptions = { willZoomCe
});
}
};
+ if (Doc.IsDataProto(doc) && DocListCast(doc.proto_embeddings).some(embed => embed.hidden && [AclAdmin, AclEdit].includes(GetEffectiveAcl(embed)))) {
+ doc = DocListCast(doc.proto_embeddings).find(embed => embed.hidden && [AclAdmin, AclEdit].includes(GetEffectiveAcl(embed)))!;
+ }
if (doc.hidden) {
doc.hidden = false;
options.toggleTarget = false;
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 070e0f918..f4ff38515 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -15,42 +15,36 @@ import { SelectionManager } from './SelectionManager';
import { SnappingManager } from './SnappingManager';
import { UndoManager } from './UndoManager';
-export type dropActionType = 'embed' | 'copy' | 'move' | 'same' | 'proto' | 'none' | undefined; // undefined = move, "same" = move but don't call removeDropProperties
+export type dropActionType = 'embed' | 'copy' | 'move' | 'add' | 'same' | 'proto' | 'none' | undefined; // undefined = move, "same" = move but don't call dropPropertiesToRemove
/**
* Initialize drag
* @param _reference: The HTMLElement that is being dragged
* @param docFunc: The Dash document being moved
- * @param moveFunc: The function called when the document is moved
- * @param dropAction: What to do with the document when it is dropped
- * @param dragStarted: Method to call when the drag is started
*/
-export function SetupDrag(_reference: React.RefObject<HTMLElement>, docFunc: () => Doc | Promise<Doc | undefined> | undefined, moveFunc?: DragManager.MoveFunction, dropAction?: dropActionType, dragStarted?: () => void) {
- const onRowMove = async (e: PointerEvent) => {
+export function SetupDrag(_reference: React.RefObject<HTMLElement>, docFunc: () => Doc | undefined) {
+ const onRowMove = (e: PointerEvent) => {
e.stopPropagation();
e.preventDefault();
document.removeEventListener('pointermove', onRowMove);
document.removeEventListener('pointerup', onRowUp);
- const doc = await docFunc();
+ const doc = docFunc();
if (doc) {
const dragData = new DragManager.DocumentDragData([doc]);
- dragData.dropAction = dropAction;
- dragData.moveDocument = moveFunc;
DragManager.StartDocumentDrag([_reference.current!], dragData, e.x, e.y);
- dragStarted?.();
}
};
const onRowUp = (): void => {
document.removeEventListener('pointermove', onRowMove);
document.removeEventListener('pointerup', onRowUp);
};
- const onItemDown = async (e: React.PointerEvent) => {
+ const onItemDown = (e: React.PointerEvent) => {
if (e.button === 0) {
e.stopPropagation();
if (e.shiftKey) {
e.persist();
- const dragDoc = await docFunc();
+ const dragDoc = docFunc();
dragDoc && DragManager.StartWindowDrag?.(e, [dragDoc]);
} else {
document.addEventListener('pointermove', onRowMove);
@@ -132,7 +126,7 @@ export namespace DragManager {
userDropAction: dropActionType; // the user requested drop action -- this will be honored as specified by modifier keys
defaultDropAction?: dropActionType; // an optionally specified default drop action when there is no user drop actionl - this will be honored if there is no user drop action
dropAction: dropActionType; // a drop action request by the initiating code. the actual drop action may be different -- eg, if the request is 'embed', but the document is dropped within the same collection, the drop action will be switched to 'move'
- removeDropProperties?: string[];
+ dropPropertiesToRemove?: string[];
moveDocument?: MoveFunction;
removeDocument?: RemoveFunction;
isDocDecorationMove?: boolean; // Flags that Document decorations are used to drag document which allows suppression of onDragStart scripts
@@ -174,6 +168,13 @@ export namespace DragManager {
userDropAction: dropActionType;
}
+ let defaultPreDropFunc = (e: Event, de: DragManager.DropEvent, targetAction: dropActionType) => {
+ if (de.complete.docDragData) {
+ targetAction && (de.complete.docDragData.dropAction = targetAction);
+ e.stopPropagation();
+ }
+ };
+
export function MakeDropTarget(element: HTMLElement, dropFunc: (e: Event, de: DropEvent) => void, doc?: Doc, preDropFunc?: (e: Event, de: DropEvent, targetAction: dropActionType) => void): DragDropDisposer {
if ('canDrop' in element.dataset) {
throw new Error("Element is already droppable, can't make it droppable again");
@@ -182,7 +183,7 @@ export namespace DragManager {
const handler = (e: Event) => dropFunc(e, (e as CustomEvent<DropEvent>).detail);
const preDropHandler = (e: Event) => {
const de = (e as CustomEvent<DropEvent>).detail;
- preDropFunc?.(e, de, StrCast(doc?.targetDropAction) as dropActionType);
+ (preDropFunc ?? defaultPreDropFunc)(e, de, StrCast(doc?.dropAction) as dropActionType);
};
element.addEventListener('dashOnDrop', handler);
doc && element.addEventListener('dashPreDrop', preDropHandler);
@@ -196,7 +197,7 @@ export namespace DragManager {
// drag a document and drop it (or make an embed/copy on drop)
export function StartDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions, dropEvent?: () => any) {
const addAudioTag = (dropDoc: any) => {
- dropDoc && !dropDoc.creationDate && (dropDoc.creationDate = new DateField());
+ dropDoc && !dropDoc.author_date && (dropDoc.author_date = new DateField());
dropDoc instanceof Doc && DocUtils.MakeLinkToActiveAudio(() => dropDoc);
return dropDoc;
};
@@ -212,6 +213,8 @@ export namespace DragManager {
? addAudioTag(ScriptCast(d.onDragStart).script.run({ this: d }).result)
: docDragData.dropAction === 'embed'
? Doc.BestEmbedding(d)
+ : docDragData.dropAction === 'add'
+ ? d
: docDragData.dropAction === 'proto'
? Doc.GetProto(d)
: docDragData.dropAction === 'copy'
@@ -223,11 +226,13 @@ export namespace DragManager {
)
).filter(d => d);
!['same', 'proto'].includes(docDragData.dropAction as any) &&
- docDragData.droppedDocuments.forEach((drop: Doc, i: number) => {
- const dragProps = StrListCast(dragData.draggedDocuments[i].removeDropProperties);
- const remProps = (dragData?.removeDropProperties || []).concat(Array.from(dragProps));
- remProps.map(prop => (drop[prop] = undefined));
- });
+ docDragData.droppedDocuments
+ // .filter(drop => !drop.dragOnlyWithinContainer || ['embed', 'copy'].includes(docDragData.dropAction as any))
+ .forEach((drop: Doc, i: number) => {
+ const dragProps = StrListCast(dragData.draggedDocuments[i].dropPropertiesToRemove);
+ const remProps = (dragData?.dropPropertiesToRemove || []).concat(Array.from(dragProps));
+ [...remProps, 'dropPropertiesToRemove'].map(prop => (drop[prop] = undefined));
+ });
}
return e;
};
@@ -263,7 +268,7 @@ export namespace DragManager {
// drags a column from a schema view
export function StartColumnDrag(ele: HTMLElement[], dragData: ColumnDragData, downX: number, downY: number, options?: DragOptions) {
- StartDrag(ele, dragData, downX, downY, options);
+ StartDrag(ele, dragData, downX, downY, options, undefined, 'Drag Column');
}
export function SetSnapLines(horizLines: number[], vertLines: number[]) {
@@ -325,10 +330,10 @@ export namespace DragManager {
export let docsBeingDragged: Doc[] = observable([] as Doc[]);
export let CanEmbed = false;
export let DocDragData: DocumentDragData | undefined;
- export function StartDrag(eles: HTMLElement[], dragData: { [id: string]: any }, downX: number, downY: number, options?: DragOptions, finishDrag?: (dropData: DragCompleteEvent) => void) {
+ export function StartDrag(eles: HTMLElement[], dragData: { [id: string]: any }, downX: number, downY: number, options?: DragOptions, finishDrag?: (dropData: DragCompleteEvent) => void, dragUndoName?: string) {
if (dragData.dropAction === 'none') return;
DocDragData = dragData as DocumentDragData;
- const batch = UndoManager.StartBatch('dragging');
+ const batch = UndoManager.StartBatch(dragUndoName ?? 'document drag');
eles = eles.filter(e => e);
CanEmbed = dragData.canEmbed || false;
if (!dragDiv) {
@@ -510,7 +515,7 @@ export namespace DragManager {
const target = document.elementFromPoint(e.x, e.y);
- if (target && !Doc.UserDoc()._noAutoscroll && !options?.noAutoscroll && !dragData.draggedDocuments?.some((d: any) => d._noAutoscroll)) {
+ if (target && !Doc.UserDoc()._noAutoscroll && !options?.noAutoscroll && !dragData.draggedDocuments?.some((d: any) => d._freeform_noAutoPan)) {
const autoScrollHandler = () => {
target.dispatchEvent(
new CustomEvent<React.DragEvent>('dashDragAutoScroll', {
@@ -579,6 +584,7 @@ export namespace DragManager {
async function dispatchDrag(target: Element, e: PointerEvent, complete: DragCompleteEvent, pos: { x: number; y: number }, finishDrag?: (e: DragCompleteEvent) => void, options?: DragOptions, endDrag?: () => void) {
const dropArgs = {
+ cancelable: true, // allows preventDefault() to be called to cancel the drop
bubbles: true,
detail: {
...pos,
@@ -591,8 +597,9 @@ export namespace DragManager {
},
};
target.dispatchEvent(new CustomEvent<DropEvent>('dashPreDrop', dropArgs));
+ UndoManager.StartTempBatch(); // run drag/drop in temp batch in case drop is not allowed (so we can undo any intermediate changes)
await finishDrag?.(complete);
- target.dispatchEvent(new CustomEvent<DropEvent>('dashOnDrop', dropArgs));
+ UndoManager.EndTempBatch(target.dispatchEvent(new CustomEvent<DropEvent>('dashOnDrop', dropArgs))); // event return val is true unless the event preventDefault() is called
options?.dragComplete?.(complete);
endDrag?.();
}
@@ -607,8 +614,8 @@ ScriptingGlobals.add(function toggleRaiseOnDrag(forAllDocs: boolean, readOnly?:
? 'transparent'
: DragManager.GetRaiseWhenDragged()
? Colors.MEDIUM_BLUE_ALT
- : Colors.PINK;
- return DragManager.GetRaiseWhenDragged() ? Colors.PINK : 'transparent';
+ : Colors.LIGHT_BLUE;
+ return DragManager.GetRaiseWhenDragged() ? Colors.MEDIUM_BLUE_ALT : 'transparent';
}
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());
diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts
index f46ea393a..f235be192 100644
--- a/src/client/util/DropConverter.ts
+++ b/src/client/util/DropConverter.ts
@@ -9,7 +9,7 @@ import { RichTextField } from '../../fields/RichTextField';
import { ImageField } from '../../fields/URLField';
import { ScriptingGlobals } from './ScriptingGlobals';
import { listSpec } from '../../fields/Schema';
-import { ButtonType } from '../views/nodes/button/FontIconBox';
+import { ButtonType } from '../views/nodes/FontIconBox/FontIconBox';
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
@@ -57,11 +57,11 @@ export function convertDropDataToButtons(data: DragManager.DocumentDragData) {
let dbox = doc;
// bcz: isButtonBar is intended to allow a collection of linear buttons to be dropped and nested into another collection of buttons... it's not being used yet, and isn't very elegant
if (doc.type === DocumentType.FONTICON || StrCast(Doc.Layout(doc).layout).includes('FontIconBox')) {
- if (data.removeDropProperties || dbox.removeDropProperties) {
+ if (data.dropPropertiesToRemove || dbox.dropPropertiesToRemove) {
//dbox = Doc.MakeEmbedding(doc); // don't need to do anything if dropping an icon doc onto an icon bar since there should be no layout data for an icon
dbox = Doc.MakeEmbedding(dbox);
- const dragProps = Cast(dbox.removeDropProperties, listSpec('string'), []);
- const remProps = (data.removeDropProperties || []).concat(Array.from(dragProps));
+ const dragProps = Cast(dbox.dropPropertiesToRemove, listSpec('string'), []);
+ const remProps = (data.dropPropertiesToRemove || []).concat(Array.from(dragProps));
remProps.map(prop => (dbox[prop] = undefined));
}
} else if (!doc.onDragStart && !doc.isButtonBar) {
@@ -81,7 +81,7 @@ export function convertDropDataToButtons(data: DragManager.DocumentDragData) {
icon: layoutDoc.isTemplateDoc ? 'font' : 'bolt',
});
dbox.dragFactory = layoutDoc;
- dbox.removeDropProperties = doc.removeDropProperties instanceof ObjectField ? ObjectField.MakeCopy(doc.removeDropProperties) : undefined;
+ dbox.dropPropertiesToRemove = doc.dropPropertiesToRemove instanceof ObjectField ? ObjectField.MakeCopy(doc.dropPropertiesToRemove) : undefined;
dbox.onDragStart = ScriptField.MakeFunction('makeDelegate(this.dragFactory)');
} else if (doc.isButtonBar) {
dbox.ignoreClick = true;
diff --git a/src/client/util/GroupManager.scss b/src/client/util/GroupManager.scss
index 9438bdd72..673af16ee 100644
--- a/src/client/util/GroupManager.scss
+++ b/src/client/util/GroupManager.scss
@@ -1,38 +1,54 @@
.group-interface {
width: 380px;
height: 300px;
+ position: relative;
.dialogue-box {
.group-create {
display: flex;
flex-direction: column;
- height: 90%;
+ overflow: visible;
+ height: 100%;
+ width: 98.5%;
+ padding: 5px;
justify-content: space-between;
margin-left: 5px;
- input {
- border-radius: 5px;
- padding: 8px;
- min-width: 100%;
- border: 1px solid hsl(0, 0%, 80%);
- outline: none;
- height: 30;
-
- &:focus {
- border: 2.5px solid #2684FF;
- }
- }
-
p {
font-size: 20px;
text-align: left;
- color: black;
}
+ }
- button {
- align-self: flex-end;
+ .group-input {
+
+ input {
+ padding: 8px;
+ width: 100%;
+ outline: none;
+ border: none;
+ min-width: 100%;
+ border: 2px solid;
+ border-radius: 4px;
+ border-color: inherit;
+ background: white;
+ color: black;
+ height: 30;
}
}
+
+ .select-users {
+ margin-right: 3;
+ max-height: 30;
+ width: 100%;
+ display: inline-flex;
+ flex-direction: row;
+ border: 2px solid;
+ border-radius: 4px;
+ border-color: inherit;
+ background: white;
+ color: black;
+ }
}
@@ -54,10 +70,14 @@
.group-interface {
display: flex;
flex-direction: column;
+ overflow: hidden;
+ padding: 10px;
.overlay {
- transform: translate(-20px, -20px);
- border-radius: 10px;
+ transform: translate(-10px, -10px);
+ width: 400px;
+ height: 320px;
+ overflow: hidden;
}
.delete-button {
@@ -66,10 +86,8 @@
.close-button {
position: absolute;
- right: 1em;
- top: 1em;
- cursor: pointer;
- z-index: 999;
+ right: 2px;
+ top: 2px;
}
.group-heading {
@@ -81,7 +99,6 @@
font-size: 20px;
text-align: left;
margin-right: 15px;
- color: black;
}
}
@@ -94,18 +111,24 @@
margin-left: 5;
width: 50px;
cursor: pointer;
+ display: flex;
+ flex-direction: row;
+ }
+
+ .style-divider {
+ width: 100%;
+ height: 1px;
}
.group-body {
justify-content: space-between;
- height: 220;
- background-color: #e8e8e8;
+ height: 225;
padding-right: 1em;
justify-content: space-around;
text-align: left;
- overflow-y: auto;
+ overflow-y: hidden;
width: 100%;
.group-row {
@@ -117,7 +140,6 @@
.group-name {
max-width: 65%;
margin: 0 10;
- color: black;
}
.group-info {
diff --git a/src/client/util/GroupManager.tsx b/src/client/util/GroupManager.tsx
index da947aba6..f35844020 100644
--- a/src/client/util/GroupManager.tsx
+++ b/src/client/util/GroupManager.tsx
@@ -15,6 +15,8 @@ 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';
/**
* Interface for options for the react-select component
@@ -280,47 +282,59 @@ export class GroupManager extends React.Component<{}> {
*/
private get groupCreationModal() {
const contents = (
- <div className="group-create">
+ <div className="group-create" style={{ background: SettingsManager.Instance.userBackgroundColor, color: SettingsManager.Instance.userColor }}>
<div className="group-heading" style={{ marginBottom: 0 }}>
<p>
<b>New Group</b>
</p>
- <div
- className={'close-button'}
- onClick={action(() => {
- this.createGroupModalOpen = false;
- TaskCompletionBox.taskCompleted = false;
- })}>
- <FontAwesomeIcon icon={'times'} color={'black'} size={'lg'} />
+ <div className="close-button">
+ <Button
+ icon={<FontAwesomeIcon icon={'times'} size={'lg'} />}
+ onClick={action(() => {
+ this.createGroupModalOpen = false;
+ TaskCompletionBox.taskCompleted = false;
+ })}
+ color={StrCast(Doc.UserDoc().userColor)}
+ />
</div>
</div>
- <input className="group-input" ref={this.inputRef} onKeyDown={this.handleKeyDown} autoFocus type="text" placeholder="Group name" onChange={action(() => (this.buttonColour = this.inputRef.current?.value ? 'black' : '#979797'))} />
- <Select
- isMulti
- options={this.options}
- onChange={this.handleChange}
- placeholder={'Select users'}
- value={this.selectedUsers}
- closeMenuOnSelect={false}
- styles={{
- dropdownIndicator: (base, state) => ({
- ...base,
- transition: '0.5s all ease',
- transform: state.selectProps.menuIsOpen ? 'rotate(180deg)' : undefined,
- }),
- multiValue: base => ({
- ...base,
- maxWidth: '50%',
-
- '&:hover': {
- maxWidth: 'unset',
- },
- }),
- }}
- />
- <button ref={this.createGroupButtonRef} onClick={this.createGroup} style={{ background: this.buttonColour }} disabled={this.buttonColour === '#979797'}>
- Create
- </button>
+ <div className="group-input" style={{ border: StrCast(Doc.UserDoc().userColor) }}>
+ <input ref={this.inputRef} onKeyDown={this.handleKeyDown} autoFocus type="text" placeholder="Group name" onChange={action(() => (this.buttonColour = this.inputRef.current?.value ? 'black' : '#979797'))} />
+ </div>
+ <div style={{ border: StrCast(Doc.UserDoc().userColor) }}>
+ <Select
+ className="select-users"
+ isMulti
+ options={this.options}
+ onChange={this.handleChange}
+ placeholder={'Select users'}
+ value={this.selectedUsers}
+ closeMenuOnSelect={false}
+ styles={{
+ control: () => ({
+ display: 'inline-flex',
+ width: '100%',
+ }),
+ indicatorSeparator: () => ({
+ display: 'inline-flex',
+ visibility: 'hidden',
+ }),
+ indicatorsContainer: () => ({
+ display: 'inline-flex',
+ textDecorationColor: 'black',
+ }),
+ valueContainer: () => ({
+ display: 'inline-flex',
+ fontStyle: StrCast(Doc.UserDoc().userColor),
+ color: StrCast(Doc.UserDoc().userColor),
+ width: '100%',
+ }),
+ }}
+ />
+ </div>
+ <div className={'create-button'}>
+ <Button text={'Create'} type={Type.TERT} color={StrCast(Doc.UserDoc().userColor)} onClick={this.createGroup} />
+ </div>
</div>
);
@@ -353,37 +367,30 @@ 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">
+ <div className="group-interface" style={{ background: SettingsManager.Instance.userBackgroundColor, color: SettingsManager.Instance.userColor }}>
{this.groupCreationModal}
{this.currentGroup ? <GroupMemberView group={this.currentGroup} onCloseButtonClick={action(() => (this.currentGroup = undefined))} /> : null}
<div className="group-heading">
<p>
<b>Manage Groups</b>
</p>
- <button onClick={action(() => (this.createGroupModalOpen = true))}>
- <FontAwesomeIcon icon={'plus'} size={'sm'} /> Create Group
- </button>
- <div className={'close-button'} onClick={this.close}>
- <FontAwesomeIcon icon={'times'} color={'black'} size={'lg'} />
+ <Button icon={<FontAwesomeIcon icon={'plus'} />} iconPlacement={'left'} text={'Create Group'} type={Type.TERT} color={StrCast(Doc.UserDoc().userColor)} onClick={action(() => (this.createGroupModalOpen = true))} />
+ <div className={'close-button'}>
+ <Button icon={<FontAwesomeIcon icon={'times'} size={'lg'} />} onClick={this.close} color={StrCast(Doc.UserDoc().userColor)} />
</div>
</div>
<div className="main-container">
<div className="sort-groups" onClick={action(() => (this.groupSort = this.groupSort === 'ascending' ? 'descending' : this.groupSort === 'descending' ? 'none' : 'ascending'))}>
- Name{' '}
- {this.groupSort === 'ascending' ? (
- <FontAwesomeIcon icon={'caret-up'} size={'xs'} />
- ) : this.groupSort === 'descending' ? (
- <FontAwesomeIcon icon={'caret-down'} size={'xs'} />
- ) : (
- <FontAwesomeIcon icon={'caret-right'} size={'xs'} />
- )}
+ Name
+ <IconButton icon={<FontAwesomeIcon icon={this.groupSort === 'ascending' ? 'caret-up' : this.groupSort === 'descending' ? 'caret-down' : 'caret-right'} />} size={Size.XSMALL} color={StrCast(Doc.UserDoc().userColor)} />
</div>
- <div className="group-body">
+ <div className={'style-divider'} style={{ background: StrCast(Doc.UserDoc().userColor) }} />
+ <div className="group-body" style={{ background: StrCast(Doc.UserDoc().userBackgroundColor), color: StrCast(Doc.UserDoc().userColor) }}>
{groups.map(group => (
<div className="group-row" key={StrCast(group.title || group.groupName)}>
<div className="group-name">{StrCast(group.title || group.groupName)}</div>
<div className="group-info" onClick={action(() => (this.currentGroup = group))}>
- <FontAwesomeIcon icon={'info-circle'} color={'#e8e8e8'} size={'sm'} style={{ backgroundColor: '#1e89d7', borderRadius: '100%', border: '1px solid #1e89d7' }} />
+ <IconButton icon={<FontAwesomeIcon icon={'info-circle'} />} size={Size.XSMALL} color={StrCast(Doc.UserDoc().userColor)} onClick={action(() => (this.currentGroup = group))} />
</div>
</div>
))}
diff --git a/src/client/util/GroupMemberView.scss b/src/client/util/GroupMemberView.scss
index 2eb164988..d50569b26 100644
--- a/src/client/util/GroupMemberView.scss
+++ b/src/client/util/GroupMemberView.scss
@@ -1,6 +1,9 @@
.editing-interface {
width: 100%;
height: 100%;
+ padding: 10px;
+ float: right;
+ overflow: visible;
hr {
margin-top: 20;
@@ -10,7 +13,6 @@
outline: none;
border-radius: 5px;
border: 0px;
- color: #fcfbf7;
text-transform: none;
letter-spacing: 2px;
font-size: 75%;
@@ -37,8 +39,6 @@
text-align: center;
border: none;
outline: none;
- color: black;
- margin-top: -5;
height: 20;
text-overflow: ellipsis;
background: none;
@@ -61,21 +61,36 @@
margin-bottom: 25;
.add-member-dropdown {
- width: 65%;
- margin: 0 5;
+ margin-right: 3;
+ max-height: 30;
+ width: 250px;
+ display: inline-flex;
+ flex-direction: row;
+ border: 2px solid;
+ border-radius: 4px;
+ border-color: inherit;
+ background: white;
+ color: black;
+ }
- input {
- height: 30;
- }
+ .delete-button {
+ align-self: center;
+ background: inherit;
}
}
}
+ .style-divider {
+ width: 98%;
+ height: 1px;
+ margin-top: 20px;
+ margin-bottom: 20px;
+ }
+
.editing-contents {
overflow-y: auto;
height: 62%;
width: 100%;
- color: black;
margin-top: -15px;
.editing-row {
diff --git a/src/client/util/GroupMemberView.tsx b/src/client/util/GroupMemberView.tsx
index b7f89794d..535d8ccc2 100644
--- a/src/client/util/GroupMemberView.tsx
+++ b/src/client/util/GroupMemberView.tsx
@@ -1,13 +1,15 @@
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, observable } from "mobx";
-import { observer } from "mobx-react";
-import * as React from "react";
-import Select from "react-select";
-import { Doc } from "../../fields/Doc";
-import { StrCast } from "../../fields/Types";
-import { MainViewModal } from "../views/MainViewModal";
-import { GroupManager, UserOptions } from "./GroupManager";
-import "./GroupMemberView.scss";
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { action, observable } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import Select from 'react-select';
+import { Doc } from '../../fields/Doc';
+import { StrCast } from '../../fields/Types';
+import { MainViewModal } from '../views/MainViewModal';
+import { GroupManager, UserOptions } from './GroupManager';
+import './GroupMemberView.scss';
+import { Button, IconButton, Size, Type } from 'browndash-components';
+import { SettingsManager } from './SettingsManager';
interface GroupMemberViewProps {
group: Doc;
@@ -16,93 +18,87 @@ interface GroupMemberViewProps {
@observer
export class GroupMemberView extends React.Component<GroupMemberViewProps> {
-
- @observable private memberSort: "ascending" | "descending" | "none" = "none";
+ @observable private memberSort: 'ascending' | 'descending' | 'none' = 'none';
private get editingInterface() {
let members: string[] = this.props.group ? JSON.parse(StrCast(this.props.group.members)) : [];
- members = this.memberSort === "ascending" ? members.sort() : this.memberSort === "descending" ? members.sort().reverse() : members;
+ members = this.memberSort === 'ascending' ? members.sort() : this.memberSort === 'descending' ? members.sort().reverse() : members;
const options: UserOptions[] = this.props.group ? GroupManager.Instance.options.filter(option => !(JSON.parse(StrCast(this.props.group.members)) as string[]).includes(option.value)) : [];
const hasEditAccess = GroupManager.Instance.hasEditAccess(this.props.group);
- return (!this.props.group ? null :
- <div className="editing-interface">
+ return !this.props.group ? null : (
+ <div className="editing-interface" style={{ background: SettingsManager.Instance.userBackgroundColor, color: SettingsManager.Instance.userColor }}>
<div className="editing-header">
<input
className="group-title"
- style={{ marginLeft: !hasEditAccess ? "-14%" : 0 }}
+ style={{ marginLeft: !hasEditAccess ? '-14%' : 0 }}
value={StrCast(this.props.group.title || this.props.group.groupName)}
- onChange={e => this.props.group.title = e.currentTarget.value}
- disabled={!hasEditAccess}
- >
- </input>
- <div className={"memberView-closeButton"} onClick={action(this.props.onCloseButtonClick)}>
- <FontAwesomeIcon icon={"times"} color={"black"} size={"lg"} />
+ onChange={e => (this.props.group.title = e.currentTarget.value)}
+ disabled={!hasEditAccess}></input>
+ <div className={'memberView-closeButton'}>
+ <Button icon={<FontAwesomeIcon icon={'times'} size={'lg'} />} onClick={action(this.props.onCloseButtonClick)} color={StrCast(Doc.UserDoc().userColor)} />
</div>
- {GroupManager.Instance.hasEditAccess(this.props.group) ?
+ {GroupManager.Instance.hasEditAccess(this.props.group) ? (
<div className="group-buttons">
- <div className="add-member-dropdown">
+ <div style={{ border: StrCast(Doc.UserDoc().userColor) }}>
<Select
+ className="add-member-dropdown"
isSearchable={true}
options={options}
onChange={selectedOption => GroupManager.Instance.addMemberToGroup(this.props.group, (selectedOption as UserOptions).value)}
- placeholder={"Add members"}
+ placeholder={'Add members'}
value={null}
styles={{
- dropdownIndicator: (base, state) => ({
- ...base,
- transition: '0.5s all ease',
- transform: state.selectProps.menuIsOpen ? 'rotate(180deg)' : undefined
- })
+ control: () => ({
+ display: 'inline-flex',
+ width: '100%',
+ }),
+ indicatorSeparator: () => ({
+ display: 'inline-flex',
+ visibility: 'hidden',
+ }),
+ indicatorsContainer: () => ({
+ display: 'inline-flex',
+ textDecorationColor: 'black',
+ }),
+ valueContainer: () => ({
+ display: 'inline-flex',
+ fontStyle: StrCast(Doc.UserDoc().userColor),
+ color: StrCast(Doc.UserDoc().userColor),
+ width: '100%',
+ }),
}}
/>
</div>
- <button onClick={() => GroupManager.Instance.deleteGroup(this.props.group)}>Delete group</button>
- </div> :
- null}
- <div
- className="sort-emails"
- style={{ paddingTop: hasEditAccess ? 0 : 35 }}
- onClick={action(() => this.memberSort = this.memberSort === "ascending" ? "descending" : this.memberSort === "descending" ? "none" : "ascending")}>
- Emails {this.memberSort === "ascending" ? "↑" : this.memberSort === "descending" ? "↓" : ""} {/* → */}
+ <div className={'delete-button'}>
+ <Button text={'Delete Group'} type={Type.TERT} color={StrCast(Doc.UserDoc().userColor)} onClick={() => GroupManager.Instance.deleteGroup(this.props.group)} />
+ </div>
+ </div>
+ ) : null}
+ <div className="sort-emails" style={{ paddingTop: hasEditAccess ? 0 : 35 }} onClick={action(() => (this.memberSort = this.memberSort === 'ascending' ? 'descending' : this.memberSort === 'descending' ? 'none' : 'ascending'))}>
+ Emails {this.memberSort === 'ascending' ? '↑' : this.memberSort === 'descending' ? '↓' : ''} {/* → */}
</div>
</div>
- <hr />
- <div className="editing-contents"
- style={{ height: hasEditAccess ? "62%" : "85%" }}
- >
+ <div className={'style-divider'} style={{ background: StrCast(Doc.UserDoc().userColor) }} />
+ <div className="editing-contents" style={{ height: hasEditAccess ? '62%' : '85%' }}>
{members.map(member => (
- <div
- className="editing-row"
- key={member}
- >
- <div className="user-email">
- {member}
- </div>
- {hasEditAccess ?
- <div className={"remove-button"} onClick={() => GroupManager.Instance.removeMemberFromGroup(this.props.group, member)}>
- <FontAwesomeIcon icon={"trash-alt"} size={"sm"} />
+ <div className="editing-row" key={member}>
+ <div className="user-email">{member}</div>
+ {hasEditAccess ? (
+ <div className={'remove-button'} onClick={() => GroupManager.Instance.removeMemberFromGroup(this.props.group, member)}>
+ <IconButton icon={<FontAwesomeIcon icon={'trash-alt'} />} size={Size.XSMALL} color={StrCast(Doc.UserDoc().userColor)} onClick={() => GroupManager.Instance.removeMemberFromGroup(this.props.group, member)} />
</div>
- : null}
+ ) : null}
</div>
))}
</div>
</div>
);
-
}
render() {
- return <MainViewModal
- isDisplayed={true}
- interactive={true}
- contents={this.editingInterface}
- dialogueBoxStyle={{ width: 400, height: 250 }}
- closeOnExternalClick={this.props.onCloseButtonClick}
- />;
+ return <MainViewModal isDisplayed={true} interactive={true} contents={this.editingInterface} dialogueBoxStyle={{ width: 400, height: 250 }} closeOnExternalClick={this.props.onCloseButtonClick} />;
}
-
-
-} \ No newline at end of file
+}
diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index b9bb22564..1a4c2450e 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx
@@ -112,7 +112,7 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> {
sizes.push(file.size);
modifiedDates.push(file.lastModified);
});
- collector.push(...(await Networking.UploadFilesToServer<Upload.ImageInformation>(batch)));
+ collector.push(...(await Networking.UploadFilesToServer<Upload.ImageInformation>(batch.map(file =>({file})))));
runInAction(() => (this.completed += batch.length));
});
diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx
index 043f0f1f3..d0f459291 100644
--- a/src/client/util/InteractionUtils.tsx
+++ b/src/client/util/InteractionUtils.tsx
@@ -178,7 +178,7 @@ export namespace InteractionUtils {
filter: mask ? `url(#mask${defGuid})` : undefined,
opacity: 1.0,
// opacity: strokeWidth !== width ? 0.5 : undefined,
- pointerEvents: pevents as any,
+ pointerEvents: (pevents as any) === 'all' ? 'visiblepainted' : (pevents as any),
stroke: color ?? 'rgb(0, 0, 0)',
strokeWidth: strokeWidth,
strokeLinecap: lineCap as any,
diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts
index 246f5bb8c..b8fea340f 100644
--- a/src/client/util/LinkFollower.ts
+++ b/src/client/util/LinkFollower.ts
@@ -28,9 +28,9 @@ export class LinkFollower {
// if the target isn't onscreen, then it will open up the target in the lightbox, or in place
// depending on the followLinkLocation property of the source (or the link itself as a fallback);
public static FollowLink = (linkDoc: Opt<Doc>, sourceDoc: Doc, altKey: boolean) => {
- const batch = UndoManager.StartBatch('follow link click');
+ const batch = UndoManager.StartBatch('Follow Link');
runInAction(() => (LinkFollower.IsFollowing = true)); // turn off decoration bounds while following links since animations may occur, and DocDecorations is based on screenToLocal which is not always an observable value
- LinkFollower.traverseLink(
+ return LinkFollower.traverseLink(
linkDoc,
sourceDoc,
action(() => {
@@ -42,17 +42,22 @@ export class LinkFollower {
};
public static traverseLink(link: Opt<Doc>, sourceDoc: Doc, finished?: () => void, traverseBacklink?: boolean) {
+ const getView = (doc: Doc) => DocumentManager.Instance.getFirstDocumentView(DocCast(doc.layout_unrendered ? doc.annotationOn : doc));
+ const isAnchor = (sourceDoc: Doc, anchor: Doc) => Doc.AreProtosEqual(anchor, sourceDoc) || Doc.AreProtosEqual(anchor.annotationOn as Doc, sourceDoc);
const linkDocs = link ? [link] : LinkManager.Links(sourceDoc);
- const firstDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.link_anchor_1 as Doc, sourceDoc) || Doc.AreProtosEqual((linkDoc.link_anchor_1 as Doc).annotationOn as Doc, sourceDoc)); // link docs where 'doc' is link_anchor_1
- const secondDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.link_anchor_2 as Doc, sourceDoc) || Doc.AreProtosEqual((linkDoc.link_anchor_2 as Doc).annotationOn as Doc, sourceDoc)); // link docs where 'doc' is link_anchor_2
- const fwdLinkWithoutTargetView = firstDocs.find(d => DocumentManager.Instance.getDocumentViews((d.link_anchor_2 as Doc).type === DocumentType.MARKER ? DocCast((d.link_anchor_2 as Doc).annotationOn) : (d.link_anchor_2 as Doc)).length === 0);
- const backLinkWithoutTargetView = secondDocs.find(d => DocumentManager.Instance.getDocumentViews((d.link_anchor_1 as Doc).type === DocumentType.MARKER ? DocCast((d.link_anchor_1 as Doc).annotationOn) : (d.link_anchor_1 as Doc)).length === 0);
- const linkWithoutTargetDoc = traverseBacklink === undefined ? fwdLinkWithoutTargetView || backLinkWithoutTargetView : traverseBacklink ? backLinkWithoutTargetView : fwdLinkWithoutTargetView;
- const linkDocList = linkWithoutTargetDoc && !sourceDoc.followAllLinks ? [linkWithoutTargetDoc] : traverseBacklink === undefined ? firstDocs.concat(secondDocs) : traverseBacklink ? secondDocs : firstDocs;
+ const fwdLinks = linkDocs.filter(l => isAnchor(sourceDoc, l.link_anchor_1 as Doc)); // link docs where 'sourceDoc' is link_anchor_1
+ const backLinks = linkDocs.filter(l => isAnchor(sourceDoc, l.link_anchor_2 as Doc)); // link docs where 'sourceDoc' is link_anchor_2
+ const fwdLinkWithoutTargetView = fwdLinks.find(l => !getView(DocCast(l.link_anchor_2)));
+ const backLinkWithoutTargetView = backLinks.find(l => !getView(DocCast(l.link_anchor_1)));
+ const linkWithoutTargetDoc = traverseBacklink === undefined ? fwdLinkWithoutTargetView ?? backLinkWithoutTargetView : traverseBacklink ? backLinkWithoutTargetView : fwdLinkWithoutTargetView;
+ const linkDocList = linkWithoutTargetDoc && !sourceDoc.followAllLinks ? [linkWithoutTargetDoc] : traverseBacklink === undefined ? fwdLinks.concat(backLinks) : traverseBacklink ? backLinks : fwdLinks;
const followLinks = sourceDoc.followLinkToggle || sourceDoc.followAllLinks ? linkDocList : linkDocList.slice(0, 1);
var count = 0;
const allFinished = () => ++count === followLinks.length && finished?.();
- if (!followLinks.length) finished?.();
+ if (!followLinks.length) {
+ finished?.();
+ return false;
+ }
followLinks.forEach(async linkDoc => {
const target = (
sourceDoc === linkDoc.link_anchor_1
@@ -101,7 +106,7 @@ export class LinkFollower {
Doc.AddDocToList(sourceDocParent, Doc.LayoutFieldKey(sourceDocParent), target);
movedTarget = true;
}
- target.embedContainer = sourceDocParent;
+ Doc.SetContainer(target, sourceDocParent);
const moveTo = [NumCast(sourceDoc.x) + NumCast(sourceDoc.followLinkXoffset), NumCast(sourceDoc.y) + NumCast(sourceDoc.followLinkYoffset)];
if (sourceDoc.followLinkXoffset !== undefined && moveTo[0] !== target.x) {
target.x = moveTo[0];
@@ -118,17 +123,18 @@ export class LinkFollower {
allFinished();
}
});
+ return true;
}
}
ScriptingGlobals.add(function followLink(doc: Doc, altKey: boolean) {
SelectionManager.DeselectAll();
- LinkFollower.FollowLink(undefined, doc, altKey);
+ return LinkFollower.FollowLink(undefined, doc, altKey) ? undefined : { select: true };
});
export function FollowLinkScript() {
- return ScriptField.MakeScript('followLink(this,altKey)', { altKey: 'boolean' });
+ return ScriptField.MakeScript('return followLink(this,altKey)', { altKey: 'boolean' });
}
export function IsFollowLinkScript(field: FieldResult<Field>) {
- return ScriptCast(field)?.script.originalScript.includes('followLink(');
+ return ScriptCast(field)?.script.originalScript.includes('return followLink(');
}
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index 5e6107e5f..ef4b21b05 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -1,9 +1,12 @@
-import { action, observable, observe } from 'mobx';
+import { action, observable, observe, runInAction } from 'mobx';
import { computedFn } from 'mobx-utils';
-import { DirectLinksSym, Doc, DocListCast, DocListCastAsync, Field, Opt } from '../../fields/Doc';
+import { Doc, DocListCast, DocListCastAsync, Field, Opt } from '../../fields/Doc';
+import { DirectLinks } from '../../fields/DocSymbols';
+import { FieldLoader } from '../../fields/FieldLoader';
import { List } from '../../fields/List';
import { ProxyField } from '../../fields/Proxy';
import { Cast, DocCast, PromiseValue, StrCast } from '../../fields/Types';
+import { DocServer } from '../DocServer';
import { ScriptingGlobals } from './ScriptingGlobals';
/*
* link doc:
@@ -44,38 +47,41 @@ export class LinkManager {
LinkManager._instance = this;
this.createlink_relationshipLists();
LinkManager.userLinkDBs = [];
- const addLinkToDoc = (link: Doc) => {
- Promise.all([link]).then(linkdoc => {
- const link = DocCast(linkdoc[0]);
- Promise.all([link.proto]).then(linkproto => {
- Promise.all([link.link_anchor_1, link.link_anchor_2]).then(linkdata => {
- const a1 = DocCast(linkdata[0]);
- const a2 = DocCast(linkdata[1]);
- a1 &&
- a2 &&
- Promise.all([Doc.GetProto(a1), Doc.GetProto(a2)]).then(
- action(protos => {
- (protos[0] as Doc)?.[DirectLinksSym].add(link);
- (protos[1] as Doc)?.[DirectLinksSym].add(link);
+ // since this is an action, not a reaction, we get only one shot to add this link to the Anchor docs
+ // Thus make sure all promised values are resolved from link -> link.proto -> link.link_anchor_[1,2] -> link.link_anchor_[1,2].proto
+ // Then add the link to the anchor protos.
+ const addLinkToDoc = (lprom: Doc) =>
+ PromiseValue(lprom).then((link: Opt<Doc>) =>
+ PromiseValue(link?.proto as Doc).then((lproto: Opt<Doc>) =>
+ Promise.all([lproto?.link_anchor_1 as Doc, lproto?.link_anchor_2 as Doc].map(PromiseValue)).then((lAnchs: Opt<Doc>[]) =>
+ Promise.all(lAnchs.map(lAnch => PromiseValue(lAnch?.proto as Doc))).then((lAnchProtos: Opt<Doc>[]) =>
+ Promise.all(lAnchProtos.map(lAnchProto => PromiseValue(lAnchProto?.proto as Doc))).then(
+ action(lAnchProtoProtos => {
+ link && lAnchs[0] && Doc.GetProto(lAnchs[0])[DirectLinks].add(link);
+ link && lAnchs[1] && Doc.GetProto(lAnchs[1])[DirectLinks].add(link);
})
- );
- });
- });
- });
- };
- const remLinkFromDoc = (link: Doc) => {
- const a1 = link?.link_anchor_1;
- const a2 = link?.link_anchor_2;
- Promise.all([a1, a2]).then(
- action(() => {
- if (a1 instanceof Doc && a2 instanceof Doc && ((a1.author !== undefined && a2.author !== undefined) || link.author === Doc.CurrentUserEmail)) {
- Doc.GetProto(a1)[DirectLinksSym].delete(link);
- Doc.GetProto(a2)[DirectLinksSym].delete(link);
- Doc.GetProto(link)[DirectLinksSym].delete(link);
- }
- })
+ )
+ )
+ )
+ )
);
- };
+
+ const remLinkFromDoc = (lprom: Doc) =>
+ PromiseValue(lprom).then((link: Opt<Doc>) =>
+ PromiseValue(link?.proto as Doc).then((lproto: Opt<Doc>) =>
+ Promise.all([lproto?.link_anchor_1 as Doc, lproto?.link_anchor_2 as Doc].map(PromiseValue)).then((lAnchs: Opt<Doc>[]) =>
+ Promise.all(lAnchs.map(lAnch => PromiseValue(lAnch?.proto as Doc))).then((lAnchProtos: Opt<Doc>[]) =>
+ Promise.all(lAnchProtos.map(lAnchProto => PromiseValue(lAnchProto?.proto as Doc))).then(
+ action(lAnchProtoProtos => {
+ link && lAnchs[0] && Doc.GetProto(lAnchs[0])[DirectLinks].delete(link);
+ link && lAnchs[1] && Doc.GetProto(lAnchs[1])[DirectLinks].delete(link);
+ })
+ )
+ )
+ )
+ )
+ );
+
const watchUserLinkDB = (userLinkDBDoc: Doc) => {
LinkManager._links.push(...DocListCast(userLinkDBDoc.data));
const toRealField = (field: Field) => (field instanceof ProxyField ? field.value : field); // see List.ts. data structure is not a simple list of Docs, but a list of ProxyField/Fields
@@ -127,19 +133,21 @@ export class LinkManager {
},
true
);
+ runInAction(() => (FieldLoader.ServerLoadStatus.message = 'links'));
LinkManager.addLinkDB(Doc.LinkDBDoc());
}
public createlink_relationshipLists = () => {
//create new lists for link relations and their associated colors if the lists don't already exist
!Doc.UserDoc().link_relationshipList && (Doc.UserDoc().link_relationshipList = new List<string>());
- !Doc.UserDoc().linkColorList && (Doc.UserDoc().linkColorList = new List<string>());
+ !Doc.UserDoc().link_ColorList && (Doc.UserDoc().link_ColorList = new List<string>());
!Doc.UserDoc().link_relationshipSizes && (Doc.UserDoc().link_relationshipSizes = new List<number>());
};
public addLink(linkDoc: Doc, checkExists = false) {
if (!checkExists || !DocListCast(Doc.LinkDBDoc().data).includes(linkDoc)) {
Doc.AddDocToList(Doc.LinkDBDoc(), 'data', linkDoc);
+ setTimeout(DocServer.UPDATE_SERVER_CACHE, 100);
}
}
public deleteLink(linkDoc: Doc) {
@@ -153,7 +161,7 @@ export class LinkManager {
return this.relatedLinker(anchor);
} // finds all links that contain the given anchor
public getAllDirectLinks(anchor: Doc): Doc[] {
- return Array.from(Doc.GetProto(anchor)[DirectLinksSym] ?? []);
+ return Array.from(Doc.GetProto(anchor)[DirectLinks]);
} // finds all links that contain the given anchor
relatedLinker = computedFn(function relatedLinker(this: any, anchor: Doc): Doc[] {
@@ -161,7 +169,7 @@ export class LinkManager {
console.log('WAITING FOR DOC/PROTO IN LINKMANAGER');
return [];
}
- const dirLinks = Doc.GetProto(anchor)[DirectLinksSym];
+ const dirLinks = Doc.GetProto(anchor)[DirectLinks];
const annos = DocListCast(anchor[Doc.LayoutFieldKey(anchor) + '_annotations']);
if (!annos) debugger;
return annos.reduce((list, anno) => [...list, ...LinkManager.Instance.relatedLinker(anno)], Array.from(dirLinks).slice());
@@ -191,10 +199,8 @@ export class LinkManager {
public static getOppositeAnchor(linkDoc: Doc, anchor: Doc): Doc | undefined {
const a1 = Cast(linkDoc.link_anchor_1, Doc, null);
const a2 = Cast(linkDoc.link_anchor_2, Doc, null);
- if (Doc.AreProtosEqual(anchor, a1)) return a2;
- if (Doc.AreProtosEqual(anchor, a2)) return a1;
- if (Doc.AreProtosEqual(anchor, a1.annotationOn as Doc)) return a2;
- if (Doc.AreProtosEqual(anchor, a2.annotationOn as Doc)) return a1;
+ if (Doc.AreProtosEqual(DocCast(anchor.annotationOn, anchor), DocCast(a1.annotationOn, a1))) return a2;
+ if (Doc.AreProtosEqual(DocCast(anchor.annotationOn, anchor), DocCast(a2.annotationOn, a2))) return a1;
if (Doc.AreProtosEqual(anchor, linkDoc)) return linkDoc;
}
}
diff --git a/src/client/util/PingManager.ts b/src/client/util/PingManager.ts
new file mode 100644
index 000000000..4dd2fcd35
--- /dev/null
+++ b/src/client/util/PingManager.ts
@@ -0,0 +1,39 @@
+import { action, observable, runInAction } from 'mobx';
+import { Networking } from '../Network';
+import { CurrentUserUtils } from './CurrentUserUtils';
+export class PingManager {
+ // create static instance and getter for global use
+ @observable static _instance: PingManager;
+ static get Instance(): PingManager {
+ return PingManager._instance;
+ }
+
+ @observable IsBeating = true;
+ private setIsBeating = action((status: boolean) => {
+ this.IsBeating = status;
+ setTimeout(this.showAlert, 100);
+ });
+
+ showAlert = () => {
+ alert(PingManager.Instance.IsBeating ? 'The server connection is active' : 'The server connection has been interrupted.NOTE: Any changes made will appear to persist but will be lost after a browser refreshes.');
+ };
+ sendPing = async (): Promise<void> => {
+ try {
+ const res = await Networking.PostToServer('/ping', { date: new Date() });
+ runInAction(() => (CurrentUserUtils.ServerVersion = res.message));
+ !this.IsBeating && this.setIsBeating(true);
+ } catch {
+ if (this.IsBeating) {
+ this.setIsBeating(false);
+ }
+ }
+ };
+
+ // not used now, but may need to clear interval
+ private _interval: NodeJS.Timeout | null = null;
+ INTERVAL_SECONDS = 1;
+ constructor() {
+ PingManager._instance = this;
+ this._interval = setInterval(this.sendPing, this.INTERVAL_SECONDS * 1000);
+ }
+}
diff --git a/src/client/util/RTFMarkup.tsx b/src/client/util/RTFMarkup.tsx
index 247267710..c2f121e1f 100644
--- a/src/client/util/RTFMarkup.tsx
+++ b/src/client/util/RTFMarkup.tsx
@@ -30,7 +30,7 @@ export class RTFMarkup extends React.Component<{}> {
*/
@computed get cheatSheet() {
return (
- <div style={{ textAlign: 'initial', height: '100%' }}>
+ <div style={{ background: 'white', textAlign: 'initial', height: '100%' }}>
<p>
<b style={{ fontSize: 'larger' }}>{`wiki:phrase`}</b>
{` display wikipedia page for entered text (terminate with carriage return)`}
@@ -52,7 +52,7 @@ export class RTFMarkup extends React.Component<{}> {
{` add a sidebar text document inline`}
</p>
<p>
- <b style={{ fontSize: 'larger' }}>{`\`\` `}</b>
+ <b style={{ fontSize: 'larger' }}>{`\`\`\` `}</b>
{` create a code snippet block`}
</p>
<p>
diff --git a/src/client/util/ReplayMovements.ts b/src/client/util/ReplayMovements.ts
index cbc465d6a..d99630f82 100644
--- a/src/client/util/ReplayMovements.ts
+++ b/src/client/util/ReplayMovements.ts
@@ -187,7 +187,6 @@ export class ReplayMovements {
} else {
// tab wasn't open - open it and play the movement
const openedColFFView = this.openTab(movement.doc);
- console.log('openedColFFView', openedColFFView);
openedColFFView && this.zoomAndPan(movement, openedColFFView);
}
diff --git a/src/client/util/ReportManager.scss b/src/client/util/ReportManager.scss
deleted file mode 100644
index 5a2f2fcad..000000000
--- a/src/client/util/ReportManager.scss
+++ /dev/null
@@ -1,88 +0,0 @@
-@import '../views/global/globalCssVariables';
-
-.issue-list-wrapper {
- position: relative;
- min-width: 250px;
- background-color: $light-blue;
- overflow-y: scroll;
-}
-
-.issue-list {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 5px;
- margin: 5px;
- border-radius: 5px;
- border: 1px solid grey;
- background-color: lightgoldenrodyellow;
-}
-
-// issue should pop up when the user hover over the issue
-.issue-list:hover {
- box-shadow: 2px;
- cursor: pointer;
- border: 3px solid #252b33;
-}
-
-.issue-content {
- background-color: white;
- padding: 10px;
- flex: 1 1 auto;
- overflow-y: scroll;
-}
-
-.issue-title {
- font-size: 20px;
- font-weight: 600;
- color: black;
-}
-
-.issue-body {
- padding: 0 10px;
- width: 100%;
- text-align: left;
-}
-
-.issue-body > * {
- margin-top: 5px;
-}
-
-.issue-body img,
-.issue-body video {
- display: block;
- max-width: 100%;
-}
-
-.report-issue-fab {
- position: fixed;
- bottom: 20px;
- right: 20px;
- display: flex;
- align-items: center;
- justify-content: center;
- cursor: pointer;
-}
-
-.loading-center {
- margin: auto 0;
-}
-
-.settings-content label {
- margin-top: 10px;
-}
-
-.report-disclaimer {
- font-size: 8px;
- color: grey;
- padding-right: 50px;
- font-style: italic;
- text-align: left;
-}
-
-.flex-select {
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 10px;
-}
diff --git a/src/client/util/ReportManager.tsx b/src/client/util/ReportManager.tsx
deleted file mode 100644
index 51742d455..000000000
--- a/src/client/util/ReportManager.tsx
+++ /dev/null
@@ -1,297 +0,0 @@
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, observable, runInAction } from 'mobx';
-import { observer } from 'mobx-react';
-import * as React from 'react';
-import { ColorState, SketchPicker } from 'react-color';
-import { Doc } from '../../fields/Doc';
-import { Id } from '../../fields/FieldSymbols';
-import { BoolCast, Cast, StrCast } from '../../fields/Types';
-import { addStyleSheet, addStyleSheetRule, Utils } from '../../Utils';
-import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager';
-import { DocServer } from '../DocServer';
-import { Networking } from '../Network';
-import { MainViewModal } from '../views/MainViewModal';
-import { FontIconBox } from '../views/nodes/button/FontIconBox';
-import { DragManager } from './DragManager';
-import { GroupManager } from './GroupManager';
-import './SettingsManager.scss';
-import './ReportManager.scss';
-import { undoBatch } from './UndoManager';
-import { Octokit } from "@octokit/core";
-import { CheckBox } from '../views/search/CheckBox';
-import ReactLoading from 'react-loading';
-import ReactMarkdown from 'react-markdown';
-import rehypeRaw from 'rehype-raw';
-import remarkGfm from 'remark-gfm';
-const higflyout = require('@hig/flyout');
-export const { anchorPoints } = higflyout;
-export const Flyout = higflyout.default;
-
-@observer
-export class ReportManager extends React.Component<{}> {
- public static Instance: ReportManager;
- @observable private isOpen = false;
-
- private octokit: Octokit;
-
- @observable public issues: any[] = [];
- @action setIssues = action((issues: any[]) => { this.issues = issues; });
-
- // undefined is the default - null is if the user is making an issue
- @observable public selectedIssue: any = undefined;
- @action setSelectedIssue = action((issue: any) => { this.selectedIssue = issue; });
-
- // only get the open issues
- @observable public shownIssues = this.issues.filter(issue => issue.state === 'open');
-
- public updateIssueSearch = action((query: string = '') => {
- if (query === '') {
- this.shownIssues = this.issues.filter(issue => issue.state === 'open');
- return;
- }
- this.shownIssues = this.issues.filter(issue => issue.title.toLowerCase().includes(query.toLowerCase()));
- });
-
- constructor(props: {}) {
- super(props);
- ReportManager.Instance = this;
-
- this.octokit = new Octokit({
- auth: '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'
- });
- }
-
- public close = action(() => (this.isOpen = false));
- public open = action(() => {
- if (this.issues.length === 0) {
- // load in the issues if not already loaded
- this.getAllIssues()
- .then(issues => {
- this.setIssues(issues);
- this.updateIssueSearch();
- })
- .catch(err => console.log(err));
- }
- (this.isOpen = true)
- });
-
- @observable private bugTitle = '';
- @action setBugTitle = action((title: string) => { this.bugTitle = title; });
- @observable private bugDescription = '';
- @action setBugDescription = action((description: string) => { this.bugDescription = description; });
- @observable private bugType = '';
- @action setBugType = action((type: string) => { this.bugType = type; });
- @observable private bugPriority = '';
- @action setBugPriority = action((priortiy: string) => { this.bugPriority = priortiy; });
-
- // private toGithub = false;
- // will always be set to true - no alterntive option yet
- private toGithub = true;
-
- private formatTitle = (title: string, userEmail: string) => `${title} - ${userEmail.replace('@brown.edu', '')}`;
-
- public async getAllIssues() : Promise<any[]> {
- const res = await this.octokit.request('GET /repos/{owner}/{repo}/issues', {
- owner: 'brown-dash',
- repo: 'Dash-Web',
- });
-
- // 200 status means success
- if (res.status === 200) {
- return res.data;
- } else {
- throw new Error('Error getting issues');
- }
- }
-
- // turns an upload link into a servable link
- // ex:
- // C: /Users/dash/Documents/GitHub/Dash-Web/src/server/public/files/images/upload_8008dbc4b6424fbff14da7345bb32eb2.png
- // -> http://localhost:1050/files/images/upload_8008dbc4b6424fbff14da7345bb32eb2_l.png
- private fileLinktoServerLink = (fileLink: string) => {
- const serverUrl = 'https://browndash.com/';
-
- const regex = 'public'
- const publicIndex = fileLink.indexOf(regex) + regex.length;
-
- const finalUrl = `${serverUrl}${fileLink.substring(publicIndex + 1).replace('.', '_l.')}`;
- return finalUrl;
- }
-
- public async reportIssue() {
- if (this.bugTitle === '' || this.bugDescription === ''
- || this.bugType === '' || this.bugPriority === '') {
- alert('Please fill out all required fields to report an issue.');
- return;
- }
-
- if (this.toGithub) {
-
- const formattedLinks = (this.fileLinks ?? []).map(this.fileLinktoServerLink)
-
- const req = await this.octokit.request('POST /repos/{owner}/{repo}/issues', {
- owner: 'brown-dash',
- repo: 'Dash-Web',
- title: this.formatTitle(this.bugTitle, Doc.CurrentUserEmail),
- body: `${this.bugDescription} \n\nfiles:\n${formattedLinks.join('\n')}`,
- labels: [
- 'from-dash-app',
- this.bugType,
- this.bugPriority
- ]
- });
-
- // 201 status means success
- if (req.status !== 201) {
- alert('Error creating issue on github.');
- // on error, don't close the modal
- return;
- }
- }
- else {
- // if not going to github issues, not sure what to do yet...
- }
-
- // if we're down here, then we're good to go. reset the fields.
- this.setBugTitle('');
- this.setBugDescription('');
- // this.toGithub = false;
- this.setFileLinks([]);
- this.setBugType('');
- this.setBugPriority('');
- this.close();
- }
-
- @observable public fileLinks: any = [];
- @action setFileLinks = action((links: any) => { this.fileLinks = links; });
-
- private getServerPath = (link: any) => { return link.result.accessPaths.agnostic.server }
-
- private uploadFiles = (input: any) => {
- // keep null while uploading
- this.setFileLinks(null);
- // upload the files to the server
- if (input.files && input.files.length !== 0) {
- const fileArray: File[] = Array.from(input.files);
- (Networking.UploadFilesToServer(fileArray)).then(links => {
- console.log('finshed uploading', links.map(this.getServerPath));
- this.setFileLinks((links ?? []).map(this.getServerPath));
- })
- }
-
- }
-
-
- private renderIssue = (issue: any) => {
-
- const isReportingIssue = issue === null;
-
- return isReportingIssue ?
- // report issue
- (<div className="settings-content">
- <h3 style={{ 'textDecoration': 'underline'}}>Report an Issue</h3>
- <label>Please leave a title for the bug.</label><br />
- <input type="text" placeholder='title' onChange={(e) => this.setBugTitle(e.target.value)} required/>
- <br />
- <label>Please leave a description for the bug and how it can be recreated.</label>
- <textarea placeholder='description' onChange={(e) => this.setBugDescription(e.target.value)} required/>
- <br />
- {/* {<label>Send to github issues? </label>
- <input type="checkbox" onChange={(e) => this.toGithub = e.target.checked} />
- <br /> } */}
-
- <label>Please label the issue</label>
- <div className='flex-select'>
- <select name="bugType" onChange={e => this.bugType = e.target.value}>
- <option value="" disabled selected>Type</option>
- <option value="bug">Bug</option>
- <option value="cosmetic">Poor Design or Cosmetic</option>
- <option value="documentation">Poor Documentation</option>
- </select>
-
- <select name="bigPriority" onChange={e => this.bugPriority = e.target.value}>
- <option value="" disabled selected>Priority</option>
- <option value="priority-low">Low</option>
- <option value="priority-medium">Medium</option>
- <option value="priority-high">High</option>
- </select>
- </div>
-
-
- <div>
- <label>Upload media that shows the bug (optional)</label>
- <input type="file" name="file" multiple accept='audio/*, video/*, image/*' onChange={e => this.uploadFiles(e.target)}/>
- </div>
- <br />
-
- <button onClick={() => this.reportIssue()} disabled={this.fileLinks === null} style={{ backgroundColor: this.fileLinks === null ? 'grey' : '' }}>{this.fileLinks === null ? 'Uploading...' : 'Submit'}</button>
- </div>)
- :
- // view issue
- (
- <div className='issue-container'>
- <h5 style={{'textAlign': "left"}}><a href={issue.html_url} target="_blank">Issue #{issue.number}</a></h5>
- <div className='issue-title'>
- {issue.title}
- </div>
- <ReactMarkdown children={issue.body} className='issue-body' linkTarget={"_blank"} remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]} />
- </div>
- );
- }
-
- private showReportIssueScreen = () => {
- this.setSelectedIssue(null);
- }
-
- private closeReportIssueScreen = () => {
- this.setSelectedIssue(undefined);
- }
-
- private get reportInterface() {
-
- const isReportingIssue = this.selectedIssue === null;
-
- return (
- <div className="settings-interface">
- <div className='issue-list-wrapper'>
- <h3>Current Issues</h3>
- <input type="text" placeholder='search issues' onChange={(e => this.updateIssueSearch(e.target.value))}></input><br />
- {this.issues.length === 0 ? <ReactLoading className='loading-center'/> : this.shownIssues.map(issue => <div className='issue-list' key={issue.number} onClick={() => this.setSelectedIssue(issue)}>{issue.title}</div>)}
-
- {/* <div className="settings-user">
- <button onClick={() => this.getAllIssues().then(issues => this.issues = issues)}>Poll Issues</button>
- </div> */}
- </div>
-
- <div className="close-button" onClick={this.close}>
- <FontAwesomeIcon icon={'times'} color="black" size={'lg'} />
- </div>
-
- <div className="issue-content" style={{'paddingTop' : this.selectedIssue === undefined ? '50px' : 'inherit'}}>
- {this.selectedIssue === undefined ? "no issue selected" : this.renderIssue(this.selectedIssue)}
- </div>
-
- <div className='report-issue-fab'>
- <span className='report-disclaimer' hidden={!isReportingIssue}>Note: issue reporting is not anonymous.</span>
- <button
- onClick={() => isReportingIssue ? this.closeReportIssueScreen() : this.showReportIssueScreen()}
- >{isReportingIssue ? 'Cancel' : 'Report New Issue'}</button>
- </div>
-
-
- </div>
- );
- }
-
- render() {
- return (
- <MainViewModal
- contents={this.reportInterface}
- isDisplayed={this.isOpen}
- interactive={true}
- closeOnExternalClick={this.close}
- dialogueBoxStyle={{ width: 'auto', height: '500px', background: Cast(Doc.SharingDoc().userColor, 'string', null) }}
- />
- );
- }
-}
diff --git a/src/client/util/ScriptManager.ts b/src/client/util/ScriptManager.ts
index 42a6493ea..87509f2ea 100644
--- a/src/client/util/ScriptManager.ts
+++ b/src/client/util/ScriptManager.ts
@@ -1,12 +1,11 @@
-import { Doc, DocListCast } from "../../fields/Doc";
-import { List } from "../../fields/List";
-import { listSpec } from "../../fields/Schema";
-import { Cast, StrCast } from "../../fields/Types";
-import { Docs } from "../documents/Documents";
-import { ScriptingGlobals } from "./ScriptingGlobals";
+import { Doc, DocListCast } from '../../fields/Doc';
+import { List } from '../../fields/List';
+import { listSpec } from '../../fields/Schema';
+import { Cast, StrCast } from '../../fields/Types';
+import { Docs } from '../documents/Documents';
+import { ScriptingGlobals } from './ScriptingGlobals';
export class ScriptManager {
-
static _initialized = false;
private static _instance: ScriptManager;
public static get Instance(): ScriptManager {
@@ -24,11 +23,7 @@ export class ScriptManager {
}
public getAllScripts(): Doc[] {
const sdoc = ScriptManager.Instance.ScriptManagerDoc;
- if (sdoc) {
- const docs = DocListCast(sdoc.data);
- return docs;
- }
- return [];
+ return sdoc ? DocListCast(sdoc.data) : [];
}
public addScript(scriptDoc: Doc): boolean {
@@ -59,36 +54,35 @@ export class ScriptManager {
}
public static addScriptToGlobals(scriptDoc: Doc): void {
-
ScriptingGlobals.removeGlobal(StrCast(scriptDoc.name));
- const params = Cast(scriptDoc["data-params"], listSpec("string"), []);
+ const params = Cast(scriptDoc['data-params'], listSpec('string'), []);
const paramNames = params.reduce((o: string, p: string) => {
if (params.indexOf(p) === params.length - 1) {
- o = o + p.split(":")[0].trim();
+ o = o + p.split(':')[0].trim();
} else {
- o = o + p.split(":")[0].trim() + ",";
+ o = o + p.split(':')[0].trim() + ',';
}
return o;
- }, "" as string);
+ }, '' as string);
const f = new Function(paramNames, StrCast(scriptDoc.script));
Object.defineProperty(f, 'name', { value: StrCast(scriptDoc.name), writable: false });
- let parameters = "(";
+ let parameters = '(';
params.forEach((element: string, i: number) => {
if (i === params.length - 1) {
- parameters = parameters + element + ")";
+ parameters = parameters + element + ')';
} else {
- parameters = parameters + element + ", ";
+ parameters = parameters + element + ', ';
}
});
- if (parameters === "(") {
+ if (parameters === '(') {
ScriptingGlobals.add(f, StrCast(scriptDoc.description));
} else {
ScriptingGlobals.add(f, StrCast(scriptDoc.description), parameters);
}
}
-} \ No newline at end of file
+}
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index bfad93334..4be9448b3 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -2,7 +2,7 @@ import { action, observable, ObservableMap } from 'mobx';
import { computedFn } from 'mobx-utils';
import { Doc, Opt } from '../../fields/Doc';
import { DocCast } from '../../fields/Types';
-import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
+import { CollectionViewType } from '../documents/DocumentTypes';
import { DocumentView } from '../views/nodes/DocumentView';
import { LinkManager } from './LinkManager';
import { ScriptingGlobals } from './ScriptingGlobals';
@@ -21,7 +21,7 @@ export namespace SelectionManager {
@action
SelectView(docView: DocumentView, ctrlPressed: boolean): void {
// if doc is not in SelectedDocuments, add it
- if (!manager.SelectedViewsMap.get(docView) && docView.props.Document.type !== DocumentType.MARKER) {
+ if (!manager.SelectedViewsMap.get(docView)) {
if (!ctrlPressed) {
if (LinkManager.currentLink && !LinkManager.Links(docView.rootDoc).includes(LinkManager.currentLink) && docView.rootDoc !== LinkManager.currentLink) {
LinkManager.currentLink = undefined;
@@ -103,14 +103,14 @@ export namespace SelectionManager {
export function Views(): Array<DocumentView> {
return manager.SelectedViews;
- // Array.from(manager.SelectedViewsMap.keys()); //.filter(dv => manager.SelectedViews.get(dv)?._viewType !== CollectionViewType.Docking);
+ // Array.from(manager.SelectedViewsMap.keys()); //.filter(dv => manager.SelectedViews.get(dv)?._type_collection !== CollectionViewType.Docking);
}
export function SelectedSchemaDoc(): Doc | undefined {
return manager.SelectedSchemaDocument;
}
export function Docs(): Doc[] {
- return manager.SelectedViews.map(dv => dv.rootDoc).filter(doc => doc?._viewType !== CollectionViewType.Docking);
- // Array.from(manager.SelectedViewsMap.values()).filter(doc => doc?._viewType !== CollectionViewType.Docking);
+ return manager.SelectedViews.map(dv => dv.rootDoc).filter(doc => doc?._type_collection !== CollectionViewType.Docking);
+ // Array.from(manager.SelectedViewsMap.values()).filter(doc => doc?._type_collection !== CollectionViewType.Docking);
}
}
ScriptingGlobals.add(function SelectionManager_selectedDocType(type: string, expertMode: boolean, checkContext?: boolean) {
@@ -118,6 +118,9 @@ ScriptingGlobals.add(function SelectionManager_selectedDocType(type: string, exp
if (type === 'tab') {
return SelectionManager.Views().lastElement()?.props.renderDepth === 0;
}
- let selected = (sel => (checkContext ? DocCast(sel?.context) : sel))(SelectionManager.SelectedSchemaDoc() ?? SelectionManager.Docs().lastElement());
- return selected?.type === type || selected?.viewType === type || !type;
+ let selected = (sel => (checkContext ? DocCast(sel?.embedContainer) : sel))(SelectionManager.SelectedSchemaDoc() ?? SelectionManager.Docs().lastElement());
+ return selected?.type === type || selected?.type_collection === type || !type;
+});
+ScriptingGlobals.add(function deselectAll() {
+ SelectionManager.DeselectAll();
});
diff --git a/src/client/util/ServerStats.tsx b/src/client/util/ServerStats.tsx
index f84ad8598..3c7c35a7e 100644
--- a/src/client/util/ServerStats.tsx
+++ b/src/client/util/ServerStats.tsx
@@ -3,6 +3,10 @@ import { observer } from 'mobx-react';
import * as React from 'react';
import { MainViewModal } from '../views/MainViewModal';
import './SharingManager.scss';
+import { PingManager } from './PingManager';
+import { StrCast } from '../../fields/Types';
+import { Doc } from '../../fields/Doc';
+import { SettingsManager } from './SettingsManager';
@observer
export class ServerStats extends React.Component<{}> {
@@ -39,11 +43,23 @@ export class ServerStats extends React.Component<{}> {
*/
@computed get sharingInterface() {
return (
- <div>
- <span>Active users:{this._stats?.socketMap.length}</span>
- {this._stats?.socketMap.map((user: any) => (
- <p>{user.username}</p>
- ))}
+ <div
+ style={{
+ display: 'flex',
+ height: 300,
+ width: 400,
+ background: SettingsManager.Instance?.userBackgroundColor,
+ opacity: 0.6,
+ }}>
+ <div style={{ width: 300, height: 100, margin: 'auto', display: 'flex', flexDirection: 'column' }}>
+ {PingManager.Instance.IsBeating ? 'The server connection is active' : 'The server connection has been interrupted.NOTE: Any changes made will appear to persist but will be lost after a browser refreshes.'}
+
+ <br />
+ <span>Active users:{this._stats?.socketMap.length}</span>
+ {this._stats?.socketMap.map((user: any) => (
+ <p>{user.username}</p>
+ ))}
+ </div>
</div>
);
}
diff --git a/src/client/util/SettingsManager.scss b/src/client/util/SettingsManager.scss
index 1289ca2b4..bca649bc3 100644
--- a/src/client/util/SettingsManager.scss
+++ b/src/client/util/SettingsManager.scss
@@ -2,41 +2,20 @@
.settings-interface {
//background-color: whitesmoke !important;
- color: grey;
width: 450px;
-
- button {
- background: #315a96;
- outline: none;
- border-radius: 5px;
- border: 0px;
- color: #fcfbf7;
- text-transform: uppercase;
- letter-spacing: 2px;
- font-size: 75%;
- padding: 10px;
- margin: 10px;
- transition: transform 0.2s;
- margin: 2px;
- }
}
.settings-title {
font-size: 25px;
font-weight: bold;
padding-right: 10px;
- color: black;
}
.settings-username {
font-size: 12px;
padding-right: 15px;
- color: black;
margin-top: 10px;
margin-bottom: 10px;
- /* right: 135; */
- // position: absolute;
- // left: 243;
}
.settings-section {
@@ -49,7 +28,6 @@
font-size: 16;
font-weight: bold;
text-align: left;
- color: black;
width: 80;
margin-right: 50px;
}
@@ -61,40 +39,10 @@
.password-content {
display: flex;
+ width: 100%;
flex-direction: column;
-
- .password-content-inputs {
- width: 100;
- // margin-bottom: 10px;
- font-size: 10px;
-
- .password-inputs {
- border: 1px solid rgb(160, 160, 160);
- margin-bottom: 8px;
- width: 130;
- color: black;
- border-radius: 5px;
- padding: 7px;
- }
- }
-
- .password-content-buttons {
- //margin-left: 84px;
- //width: 100;
- padding: 7px;
-
- .password-submit {
- //margin-left: 85px;
- margin-top: 5px;
- }
-
- .password-forgot {
- //margin-left: 65px;
- //margin-top: -20px;
- font-size: 12px;
- white-space: nowrap;
- }
- }
+ align-items: flex-start;
+ gap: 5px;
}
.accounts-content {
@@ -103,7 +51,6 @@
.modes-content {
display: flex;
- margin-left: 10px;
font-size: 12px;
.modes-select {
@@ -111,7 +58,6 @@
width: 80%;
height: 35px;
margin-right: 10px;
- color: black;
border-radius: 5px;
&:hover {
@@ -135,12 +81,6 @@
}
}
- .playground-text {
- color: black;
- margin-right: 10px;
- margin-top: 2;
- }
-
.acl-text {
color: black;
margin-top: 2;
@@ -172,12 +112,11 @@
.appearances-content {
display: flex;
- margin-top: 4px;
- color: black;
font-size: 10px;
.preferences-color {
display: flex;
+ align-items: center;
margin-top: 2px;
.preferences-color-text {
@@ -197,7 +136,6 @@
margin-top: 2px;
.preferences-font-text {
- color: black;
margin-top: 4;
margin-right: 4;
margin-bottom: 2px;
@@ -212,7 +150,6 @@
.font-select {
height: 35px;
- color: black;
font-size: 9;
margin-right: 6;
border-radius: 5px;
@@ -225,7 +162,6 @@
.size-select {
height: 35px;
- color: black;
font-size: 9;
border-radius: 5px;
width: 30%;
@@ -237,7 +173,6 @@
}
.preferences-check {
- color: black;
margin-right: 4;
margin-bottom: -3;
margin-left: 5;
@@ -252,40 +187,17 @@
display: flex;
flex-direction: column;
- button {
- width: auto;
- align-self: center;
- background: #252b33;
- margin-right: 15px;
-
- //margin-top: 4px;
-
- &:hover {
- background: $medium-gray;
- }
- }
-
- // .delete-button {
- // background: rgb(227, 86, 86);
- // }
.close-button {
position: absolute;
- right: 1em;
- top: 1em;
- cursor: pointer;
+ right: 2px;
+ top: 2px;
}
- // .logout-button {
- // right: 355;
- // position: absolute;
- // }
.settings-content {
- background: #e4e4e4;
- //border-radius: 6px;
padding: 10px;
- //width: 560px;
+ width: 500px;
flex: 1 1 auto;
}
@@ -296,11 +208,8 @@
.error-text {
color: #c40233;
- width: 300;
- margin-left: -20;
font-size: 10;
margin-bottom: 4;
- margin-top: -3;
}
.success-text {
@@ -317,7 +226,6 @@
}
h1 {
- color: #121721;
text-transform: uppercase;
letter-spacing: 2px;
font-size: 19;
@@ -346,7 +254,6 @@
.padding {
padding: 0 0 0 20px;
- color: black;
}
}
}
@@ -357,21 +264,19 @@
min-height: 250px;
height: 100%;
width: 100%;
-
- .settings-content {
- background-color: $off-white;
- }
}
.settings-panel {
position: relative;
min-width: 150px;
- background-color: $light-blue;
.settings-user {
position: absolute;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-direction: column;
bottom: 10px;
- text-align: center;
left: 0;
right: 0;
@@ -388,16 +293,11 @@
.settings-tabs {
// font-size: 16px;
font-weight: 600;
- color: black;
.tab-control {
padding: 10px;
border-bottom: 1px solid #9f9e9e;
cursor: pointer;
-
- &.active {
- background-color: #fdfdfd;
- }
}
}
@@ -416,20 +316,31 @@
.tab-content {
display: flex;
+ flex-flow: row wrap;
+ gap: 10px;
+ overflow: hidden;
.tab-column {
- flex: 0 0 50%;
+ flex: 0 0 calc(50% - 10px);
+ flex-direction: column;
+ display: flex;
+ justify-content: flex-start;
+ align-items: flex-start;
.tab-column-title {
- color: black;
font-size: 16px;
font-weight: bold;
- margin-bottom: 16px;
+ margin-bottom: 10px;
}
.tab-column-title,
.tab-column-content {
- padding-left: 16px;
+ display: flex;
+ justify-content: center;
+ align-items: flex-start;
+ flex-direction: column;
+ gap: 10px;
+ width: 100%;
}
}
}
diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx
index a3a924b75..b2b5be070 100644
--- a/src/client/util/SettingsManager.tsx
+++ b/src/client/util/SettingsManager.tsx
@@ -1,18 +1,19 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Button, ColorPicker, Dropdown, DropdownType, EditableText, Group, NumberDropdown, Size, Toggle, ToggleType, Type } from 'browndash-components';
import { action, computed, observable, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { ColorState, SketchPicker } from 'react-color';
+import { BsGoogle } from 'react-icons/bs';
+import { FaFillDrip, FaPalette } from 'react-icons/fa';
import { Doc } from '../../fields/Doc';
-import { Id } from '../../fields/FieldSymbols';
+import { DashVersion } from '../../fields/DocSymbols';
import { BoolCast, Cast, StrCast } from '../../fields/Types';
import { addStyleSheet, addStyleSheetRule, Utils } from '../../Utils';
import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager';
import { DocServer } from '../DocServer';
import { Networking } from '../Network';
import { MainViewModal } from '../views/MainViewModal';
-import { FontIconBox } from '../views/nodes/button/FontIconBox';
-import { DragManager } from './DragManager';
+import { FontIconBox } from '../views/nodes/FontIconBox/FontIconBox';
import { GroupManager } from './GroupManager';
import './SettingsManager.scss';
import { undoBatch } from './UndoManager';
@@ -21,9 +22,12 @@ export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
export enum ColorScheme {
- Dark = '-Dark',
- Light = '-Light',
- System = '-MatchSystem',
+ Dark = 'Dark',
+ Light = 'Light',
+ CoolBlue = 'Cool Blue',
+ Cupcake = 'Cupcake',
+ System = 'Match System',
+ Custom = 'Custom',
}
export enum freeformScrollMode {
@@ -50,8 +54,8 @@ export class SettingsManager extends React.Component<{}> {
@computed get backgroundColor() {
return Doc.UserDoc().activeCollectionBackground;
}
- @computed get colorScheme() {
- return Doc.ActiveDashboard?.colorScheme;
+ @computed get userTheme() {
+ return Doc.UserDoc().userTheme;
}
constructor(props: {}) {
@@ -73,14 +77,31 @@ export class SettingsManager extends React.Component<{}> {
}
};
- @undoBatch selectUserMode = action((e: React.ChangeEvent) => (Doc.noviceMode = (e.currentTarget as any)?.value === 'Novice'));
+ @computed get userColor() {
+ return StrCast(Doc.UserDoc().userColor);
+ }
+
+ @computed get userVariantColor() {
+ return StrCast(Doc.UserDoc().userVariantColor);
+ }
+
+ @computed get userBackgroundColor() {
+ return StrCast(Doc.UserDoc().userBackgroundColor);
+ }
+
+ @undoBatch selectUserMode = action((mode: string) => (Doc.noviceMode = mode === 'Novice'));
@undoBatch changelayout_showTitle = action((e: React.ChangeEvent) => (Doc.UserDoc().layout_showTitle = (e.currentTarget as any).value ? 'title' : undefined));
- @undoBatch changeFontFamily = action((e: React.ChangeEvent) => (Doc.UserDoc().fontFamily = (e.currentTarget as any).value));
- @undoBatch changeFontSize = action((e: React.ChangeEvent) => (Doc.UserDoc().fontSize = (e.currentTarget as any).value));
- @undoBatch switchActiveBackgroundColor = action((color: ColorState) => (Doc.UserDoc().activeCollectionBackground = String(color.hex)));
- @undoBatch switchUserColor = action((color: ColorState) => {
- Doc.SharingDoc().userColor = undefined;
- Doc.GetProto(Doc.SharingDoc()).userColor = String(color.hex);
+ @undoBatch changeFontFamily = action((font: string) => (Doc.UserDoc().fontFamily = font));
+ @undoBatch changeFontSize = action((val: number) => (Doc.UserDoc().fontSize = val));
+ @undoBatch switchUserBackgroundColor = action((color: string) => {
+ 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 playgroundModeToggle = action(() => {
this.playgroundMode = !this.playgroundMode;
@@ -92,82 +113,67 @@ export class SettingsManager extends React.Component<{}> {
@undoBatch
@action
- changeColorScheme = action((e: React.ChangeEvent) => {
- const activeDashboard = Doc.ActiveDashboard;
- if (!activeDashboard) return;
- const scheme: ColorScheme = (e.currentTarget as any).value;
+ changeColorScheme = action((scheme: string) => {
+ Doc.UserDoc().userTheme = scheme;
switch (scheme) {
case ColorScheme.Light:
- activeDashboard.colorScheme = undefined; // undefined means ColorScheme.Light until all CSS is updated with values for each color scheme (e.g., see MainView.scss, DocumentDecorations.scss)
- addStyleSheetRule(SettingsManager._settingsStyle, 'lm_header', { background: '#d3d3d3 !important' });
+ this.switchUserColor('#323232');
+ this.switchUserBackgroundColor('#DFDFDF');
+ this.switchUserVariantColor('#BDDDF5');
break;
case ColorScheme.Dark:
- activeDashboard.colorScheme = ColorScheme.Dark;
- addStyleSheetRule(SettingsManager._settingsStyle, 'lm_header', { background: 'black !important' });
+ this.switchUserColor('#DFDFDF');
+ this.switchUserBackgroundColor('#323232');
+ this.switchUserVariantColor('#4476F7');
+ break;
+ case ColorScheme.CoolBlue:
+ this.switchUserColor('#ADEAFF');
+ this.switchUserBackgroundColor('#060A15');
+ this.switchUserVariantColor('#3C51FF');
+ break;
+ case ColorScheme.Cupcake:
+ this.switchUserColor('#3BC7FF');
+ this.switchUserBackgroundColor('#fffdf7');
+ this.switchUserVariantColor('#FFD7F3');
+ break;
+ case ColorScheme.Custom:
break;
case ColorScheme.System:
default:
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
- activeDashboard.colorScheme = e.matches ? ColorScheme.Dark : undefined; // undefined means ColorScheme.Light until all CSS is updated with values for each color scheme (e.g., see MainView.scss, DocumentDecorations.scss)
+ e.matches ? ColorScheme.Dark : ColorScheme.Light; // undefined means ColorScheme.Light until all CSS is updated with values for each color scheme (e.g., see MainView.scss, DocumentDecorations.scss)
});
break;
}
});
@computed get colorsContent() {
- const colorBox = (func: (color: ColorState) => void) => (
- <SketchPicker
- onChange={func}
- color={StrCast(this.backgroundColor)}
- presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb', 'transparent']}
- />
- );
-
- const colorFlyout = (
- <div className="colorFlyout">
- <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={colorBox(this.switchActiveBackgroundColor)}>
- <div className="colorFlyout-button" style={{ backgroundColor: StrCast(this.backgroundColor) }} onPointerDown={e => e.stopPropagation()}>
- <FontAwesomeIcon icon="palette" size="sm" color={StrCast(this.backgroundColor)} />
- </div>
- </Flyout>
- </div>
- );
- const userColorFlyout = (
- <div className="colorFlyout">
- <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={colorBox(this.switchUserColor)}>
- <div className="colorFlyout-button" style={{ backgroundColor: StrCast(this.backgroundColor) }} onPointerDown={e => e.stopPropagation()}>
- <FontAwesomeIcon icon="palette" size="sm" color={StrCast(this.backgroundColor)} />
- </div>
- </Flyout>
- </div>
- );
-
- const colorSchemes = [ColorScheme.Light, ColorScheme.Dark, ColorScheme.System];
- const schemeMap = ['Light', 'Dark', 'Match system'];
-
+ const colorSchemes = [ColorScheme.Light, ColorScheme.Dark, ColorScheme.Cupcake, ColorScheme.CoolBlue, ColorScheme.Custom, ColorScheme.System];
+ const schemeMap = ['Light', 'Dark', 'Cupcake', 'Cool Blue', 'Custom', 'Match System'];
+ const userTheme = StrCast(Doc.UserDoc().userTheme);
return (
- <div className="colors-content">
- <div className="preferences-color">
- <div className="preferences-color-text">Background Color</div>
- {colorFlyout}
- </div>
- <div className="preferences-color">
- <div className="preferences-color-text">Border/Header Color</div>
- {userColorFlyout}
- </div>
- <div className="preferences-colorScheme">
- <div className="preferences-color-text">Color Scheme</div>
- <div className="preferences-color-controls">
- <select className="scheme-select" onChange={this.changeColorScheme} defaultValue={StrCast(Doc.ActiveDashboard?.colorScheme)}>
- {colorSchemes.map((scheme, i) => (
- <option key={scheme} value={scheme}>
- {' '}
- {schemeMap[i]}{' '}
- </option>
- ))}
- </select>
- </div>
- </div>
+ <div style={{ width: '100%' }}>
+ <Dropdown
+ formLabel="Theme"
+ size={Size.SMALL}
+ type={Type.TERT}
+ selectedVal={userTheme}
+ setSelectedVal={scheme => this.changeColorScheme(scheme as string)}
+ items={colorSchemes.map((scheme, i) => ({
+ text: schemeMap[i],
+ val: scheme,
+ }))}
+ dropdownType={DropdownType.SELECT}
+ color={this.userColor}
+ fillWidth
+ />
+ {userTheme === ColorScheme.Custom && (
+ <Group formLabel="Custom Theme">
+ <ColorPicker tooltip={'User Color'} color={this.userColor} type={Type.SEC} icon={<FaFillDrip />} selectedColor={this.userColor} setSelectedColor={this.switchUserColor} />
+ <ColorPicker tooltip={'User Background Color'} color={this.userColor} type={Type.SEC} icon={<FaPalette />} selectedColor={this.userBackgroundColor} setSelectedColor={this.switchUserBackgroundColor} />
+ <ColorPicker tooltip={'User Variant Color'} color={this.userColor} type={Type.SEC} icon={<FaPalette />} selectedColor={this.userVariantColor} setSelectedColor={this.switchUserVariantColor} />
+ </Group>
+ )}
</div>
);
}
@@ -175,30 +181,60 @@ export class SettingsManager extends React.Component<{}> {
@computed get formatsContent() {
return (
<div className="prefs-content">
- <div>
- <input type="checkbox" onChange={e => (Doc.UserDoc().layout_showTitle = Doc.UserDoc().layout_showTitle ? undefined : 'creationDate')} checked={Doc.UserDoc().layout_showTitle !== undefined} />
- <div className="preferences-check">Show doc header</div>
- </div>
- <div>
- <input type="checkbox" onChange={e => (Doc.UserDoc()['documentLinksButton-fullMenu'] = !Doc.UserDoc()['documentLinksButton-fullMenu'])} checked={BoolCast(Doc.UserDoc()['documentLinksButton-fullMenu'])} />
- <div className="preferences-check">Show full toolbar</div>
- </div>
- <div>
- <input type="checkbox" onChange={e => FontIconBox.SetShowLabels(!FontIconBox.GetShowLabels())} checked={FontIconBox.GetShowLabels()} />
- <div className="preferences-check">Show button labels</div>
- </div>
- <div>
- <input type="checkbox" onChange={e => FontIconBox.SetRecognizeGestures(!FontIconBox.GetRecognizeGestures())} checked={FontIconBox.GetRecognizeGestures()} />
- <div className="preferences-check">Recognize ink Gestures</div>
- </div>
- <div>
- <input type="checkbox" onChange={e => (Doc.UserDoc().activeInkHideTextLabels = !Doc.UserDoc().activeInkHideTextLabels)} checked={BoolCast(Doc.UserDoc().activeInkHideTextLabels)} />
- <div className="preferences-check">Hide Labels In Ink Shapes</div>
- </div>
- <div>
- <input type="checkbox" onChange={e => (Doc.UserDoc().openInkInLightbox = !Doc.UserDoc().openInkInLightbox)} checked={BoolCast(Doc.UserDoc().openInkInLightbox)} />
- <div className="preferences-check">Open Ink Docs in Lightbox</div>
- </div>
+ <Toggle
+ formLabel={'Show document header'}
+ formLabelPlacement={'right'}
+ toggleType={ToggleType.SWITCH}
+ 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}
+ />
+ <Toggle
+ formLabel={'Show Full Toolbar'}
+ formLabelPlacement={'right'}
+ toggleType={ToggleType.SWITCH}
+ onClick={e => (Doc.UserDoc()['documentLinksButton-fullMenu'] = !Doc.UserDoc()['documentLinksButton-fullMenu'])}
+ toggleStatus={BoolCast(Doc.UserDoc()['documentLinksButton-fullMenu'])}
+ size={Size.XSMALL}
+ color={this.userColor}
+ />
+ <Toggle
+ formLabel={'Show Button Labels'}
+ formLabelPlacement={'right'}
+ toggleType={ToggleType.SWITCH}
+ onClick={e => FontIconBox.SetShowLabels(!FontIconBox.GetShowLabels())}
+ toggleStatus={FontIconBox.GetShowLabels()}
+ size={Size.XSMALL}
+ color={this.userColor}
+ />
+ <Toggle
+ formLabel={'Recognize Ink Gestures'}
+ formLabelPlacement={'right'}
+ toggleType={ToggleType.SWITCH}
+ onClick={e => FontIconBox.SetRecognizeGestures(!FontIconBox.GetRecognizeGestures())}
+ toggleStatus={FontIconBox.GetRecognizeGestures()}
+ size={Size.XSMALL}
+ color={this.userColor}
+ />
+ <Toggle
+ formLabel={'Hide Labels In Ink Shapes'}
+ formLabelPlacement={'right'}
+ toggleType={ToggleType.SWITCH}
+ onClick={e => (Doc.UserDoc().activeInkHideTextLabels = !Doc.UserDoc().activeInkHideTextLabels)}
+ toggleStatus={BoolCast(Doc.UserDoc().activeInkHideTextLabels)}
+ size={Size.XSMALL}
+ color={this.userColor}
+ />
+ <Toggle
+ formLabel={'Open Ink Docs in Lightbox'}
+ formLabelPlacement={'right'}
+ toggleType={ToggleType.SWITCH}
+ onClick={e => (Doc.UserDoc().openInkInLightbox = !Doc.UserDoc().openInkInLightbox)}
+ toggleStatus={BoolCast(Doc.UserDoc().openInkInLightbox)}
+ size={Size.XSMALL}
+ color={this.userColor}
+ />
</div>
);
}
@@ -224,25 +260,32 @@ export class SettingsManager extends React.Component<{}> {
return (
<div className="tab-content appearances-content">
- <div className="preferences-font">
- <div className="preferences-font-text">Default Font</div>
- <div className="preferences-font-controls">
- <select className="size-select" onChange={this.changeFontSize} value={StrCast(Doc.UserDoc().fontSize, '7px')}>
- {fontSizes.map(size => (
- <option key={size} value={size} defaultValue={StrCast(Doc.UserDoc().fontSize)}>
- {' '}
- {size}{' '}
- </option>
- ))}
- </select>
- <select className="font-select" onChange={this.changeFontFamily} value={StrCast(Doc.UserDoc().fontFamily, 'Times New Roman')}>
- {fontFamilies.map(font => (
- <option key={font} value={font} defaultValue={StrCast(Doc.UserDoc().fontFamily)}>
- {' '}
- {font}{' '}
- </option>
- ))}
- </select>
+ <div className="tab-column">
+ <div className="tab-column-title">Text</div>
+ <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={() => {}} />
+ <Dropdown
+ items={fontFamilies.map(val => {
+ return {
+ text: val,
+ val: val,
+ style: {
+ fontFamily: val,
+ },
+ };
+ })}
+ dropdownType={DropdownType.SELECT}
+ type={Type.TERT}
+ selectedVal={StrCast(Doc.UserDoc().fontFamily)}
+ setSelectedVal={val => {
+ this.changeFontFamily(val as string);
+ }}
+ color={this.userColor}
+ fillWidth
+ />
+ </Group>
</div>
</div>
</div>
@@ -250,8 +293,7 @@ export class SettingsManager extends React.Component<{}> {
}
@action
- changeVal = (e: React.ChangeEvent, pass: string) => {
- const value = (e.target as any).value;
+ changeVal = (value: string, pass: string) => {
switch (pass) {
case 'curr':
this.curr_password = value;
@@ -268,20 +310,12 @@ export class SettingsManager extends React.Component<{}> {
@computed get passwordContent() {
return (
<div className="password-content">
- <div className="password-content-inputs">
- <input className="password-inputs" type="password" placeholder="current password" onChange={e => this.changeVal(e, 'curr')} />
- <input className="password-inputs" type="password" placeholder="new password" onChange={e => this.changeVal(e, 'new')} />
- <input className="password-inputs" type="password" placeholder="confirm new password" onChange={e => this.changeVal(e, 'conf')} />
- </div>
- <div className="password-content-buttons">
- {!this.passwordResultText ? null : <div className={`${this.passwordResultText.startsWith('Error') ? 'error' : 'success'}-text`}>{this.passwordResultText}</div>}
- <a className="password-forgot" href="/forgotPassword">
- forgot password?
- </a>
- <button className="password-submit" onClick={this.changePassword}>
- submit
- </button>
- </div>
+ <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 />
+ {!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} />
</div>
);
}
@@ -289,9 +323,7 @@ export class SettingsManager extends React.Component<{}> {
@computed get accountOthersContent() {
return (
<div className="account-others-content">
- <button onClick={this.googleAuthorize} value="data">
- Authorize Google Acc
- </button>
+ <Button type={Type.TERT} text={'Connect to Google'} iconPlacement="left" icon={<BsGoogle />} onClick={() => this.googleAuthorize()} />
</div>
);
}
@@ -311,7 +343,7 @@ export class SettingsManager extends React.Component<{}> {
);
}
- setFreeformScrollMode = (mode: freeformScrollMode) => {
+ setFreeformScrollMode = (mode: string) => {
Doc.UserDoc().freeformScrollMode = mode;
};
@@ -321,45 +353,65 @@ export class SettingsManager extends React.Component<{}> {
<div className="tab-column">
<div className="tab-column-title">Modes</div>
<div className="tab-column-content">
- <select className="modes-select" onChange={this.selectUserMode} defaultValue={Doc.noviceMode ? 'Novice' : 'Developer'}>
- <option key={'Novice'} value={'Novice'}>
- {' '}
- Novice{' '}
- </option>
- <option key={'Developer'} value={'Developer'}>
- {' '}
- Developer
- </option>
- </select>
- <div className="modes-playground">
- <input className="playground-check" type="checkbox" checked={this.playgroundMode} onChange={this.playgroundModeToggle} />
- <div className="playground-text">Playground Mode</div>
- </div>
+ <Dropdown
+ formLabel={'Mode'}
+ items={[
+ {
+ text: 'Novice',
+ description: 'Novice mode is a user-friendly setting designed to cater to those who are new to Dash',
+ val: 'Novice',
+ },
+ {
+ text: 'Developer',
+ description:
+ 'Developer mode is an advanced setting that grants you greater control and access to the underlying mechanics and tools of a software or system. Developer mode is still under development as there are experimental features.',
+ val: 'Developer',
+ },
+ ]}
+ selectedVal={Doc.noviceMode ? 'Novice' : 'Developer'}
+ setSelectedVal={val => {
+ this.selectUserMode(val as string);
+ }}
+ dropdownType={DropdownType.SELECT}
+ type={Type.TERT}
+ placement="bottom-start"
+ color={this.userColor}
+ fillWidth
+ />
+ <Toggle formLabel={'Playground Mode'} toggleType={ToggleType.SWITCH} toggleStatus={this.playgroundMode} onClick={this.playgroundModeToggle} color={this.userColor} />
</div>
- <div className="tab-column-title" style={{ marginTop: 10, marginBottom: 0 }}>
- Freeform scrolling
+ <div className="tab-column-title" style={{ marginTop: 20, marginBottom: 10 }}>
+ Freeform Navigation
</div>
<div className="tab-column-content">
- <button style={{ backgroundColor: Doc.UserDoc().freeformScrollMode === freeformScrollMode.Pan ? 'blue' : '' }} onClick={() => this.setFreeformScrollMode(freeformScrollMode.Pan)}>
- Scroll to pan
- </button>
- <div>
- <div>Scrolling pans canvas, shift + scrolling zooms</div>
- </div>
- <button style={{ backgroundColor: Doc.UserDoc().freeformScrollMode === freeformScrollMode.Zoom ? 'blue' : '' }} onClick={() => this.setFreeformScrollMode(freeformScrollMode.Zoom)}>
- Scroll to zoom
- </button>
- <div>Scrolling zooms canvas</div>
+ <Dropdown
+ formLabel={'Scroll Mode'}
+ items={[
+ {
+ text: 'Scroll to Pan',
+ description: 'Scrolling pans canvas, shift + scrolling zooms',
+ val: freeformScrollMode.Pan,
+ },
+ {
+ text: 'Scroll to Zoom',
+ description: 'Scrolling zooms canvas',
+ val: freeformScrollMode.Zoom,
+ },
+ ]}
+ selectedVal={StrCast(Doc.UserDoc().freeformScrollMode)}
+ setSelectedVal={val => this.setFreeformScrollMode(val as string)}
+ dropdownType={DropdownType.SELECT}
+ type={Type.TERT}
+ placement="bottom-start"
+ color={this.userColor}
+ />
</div>
</div>
<div className="tab-column">
<div className="tab-column-title">Permissions</div>
<div className="tab-column-content">
- <button onClick={() => GroupManager.Instance?.open()}>Manage groups</button>
- <div className="default-acl">
- <input className="acl-check" type="checkbox" checked={BoolCast(Doc.defaultAclPrivate)} onChange={action(() => (Doc.defaultAclPrivate = !Doc.defaultAclPrivate))} />
- <div className="acl-text">Default access private</div>
- </div>
+ <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))} />
</div>
</div>
</div>
@@ -376,31 +428,41 @@ export class SettingsManager extends React.Component<{}> {
{ title: 'Appearance', ele: this.appearanceContent },
{ title: 'Text', ele: this.textContent },
];
-
return (
<div className="settings-interface">
- <div className="settings-panel">
+ <div className="settings-panel" style={{ background: this.userColor }}>
<div className="settings-tabs">
- {tabs.map(tab => (
- <div key={tab.title} className={'tab-control ' + (this.activeTab === tab.title ? 'active' : 'inactive')} onClick={action(() => (this.activeTab = tab.title))}>
- {tab.title}
- </div>
- ))}
+ {tabs.map(tab => {
+ const isActive = this.activeTab === tab.title;
+ return (
+ <div
+ key={tab.title}
+ style={{
+ background: isActive ? this.userBackgroundColor : this.userColor,
+ color: isActive ? this.userColor : this.userBackgroundColor,
+ }}
+ className={'tab-control ' + (isActive ? 'active' : 'inactive')}
+ onClick={action(() => (this.activeTab = tab.title))}>
+ {tab.title}
+ </div>
+ );
+ })}
</div>
<div className="settings-user">
- <div className="settings-username">{Doc.CurrentUserEmail}</div>
- <button className="logout-button" onClick={() => window.location.assign(Utils.prepend('/logout'))}>
- {Doc.GuestDashboard ? 'Exit' : 'Log Out'}
- </button>
+ <div style={{ color: 'black' }}>{DashVersion}</div>
+ <div className="settings-username" style={{ color: this.userBackgroundColor }}>
+ {Doc.CurrentUserEmail}
+ </div>
+ <Button text={Doc.GuestDashboard ? 'Exit' : 'Log Out'} type={Type.TERT} color={this.userVariantColor} onClick={() => window.location.assign(Utils.prepend('/logout'))} />
</div>
</div>
- <div className="close-button" onClick={this.close}>
- <FontAwesomeIcon icon={'times'} color="black" size={'lg'} />
+ <div className="close-button">
+ <Button icon={<FontAwesomeIcon icon={'times'} size={'lg'} />} onClick={this.close} color={this.userColor} />
</div>
- <div className="settings-content">
+ <div className="settings-content" style={{ color: this.userColor, background: this.userBackgroundColor }}>
{tabs.map(tab => (
<div key={tab.title} className={'tab-section ' + (this.activeTab === tab.title ? 'active' : 'inactive')}>
{tab.ele}
@@ -418,7 +480,7 @@ export class SettingsManager extends React.Component<{}> {
isDisplayed={this.isOpen}
interactive={true}
closeOnExternalClick={this.close}
- dialogueBoxStyle={{ width: '500px', height: '300px', background: Cast(Doc.SharingDoc().userColor, 'string', null) }}
+ dialogueBoxStyle={{ width: 'fit-content', height: '300px', background: Cast(Doc.UserDoc().userColor, 'string', null) }}
/>
);
}
diff --git a/src/client/util/SharingManager.scss b/src/client/util/SharingManager.scss
index 932e94664..c9f86f0e1 100644
--- a/src/client/util/SharingManager.scss
+++ b/src/client/util/SharingManager.scss
@@ -2,39 +2,61 @@
width: 600px;
// height: 360px;
- .overlay {
- transform: translate(-20px, -20px);
- }
-
- select {
+ .select {
text-align: justify;
text-align-last: end
}
.sharing-contents {
+ padding: 10px;
display: flex;
flex-direction: column;
.close-button {
position: absolute;
- right: 1em;
- top: 1em;
- cursor: pointer;
- z-index: 999;
+ right: 2px;
+ top: 2px;
+ }
+
+ .share-title {
+ display: inline-flex;
+ gap: 5px;
+
+ .share-info {
+ align-self: center;
+ cursor: pointer;
+ }
+ }
+
+ .share-copy-link {
+ border-radius: 4px;
+ margin-top: -15px;
+ margin-bottom: 15px;
+ width: fit-content;
}
.share-container {
+
.share-setup {
display: flex;
margin-bottom: 20px;
align-items: center;
- height: 36;
+ max-height: 28;
.user-search {
+ max-height: 30;
width: 90%;
+ display: inline-flex;
+ flex-direction: row;
+ border: 2px solid;
+ border-radius: 4px;
+ border-color: inherit;
+ background: white;
+ color: black;
input {
- height: 30;
+ height: 20px;
+ width: 100px;
}
}
@@ -44,12 +66,14 @@
outline: none;
text-align: justify; // for Edge
text-align-last: end;
+ font-size: 13px;
+ min-width: 90px;
+ height: 28;
+ margin-left: 2px;
}
.share-button {
- height: 105%;
- margin-left: 2%;
- background-color: black;
+ margin-left: 3%;
}
}
@@ -76,15 +100,16 @@
float: right;
align-items: baseline;
margin-top: -12;
+ margin-bottom: 10;
.layoutDoc-acls,
.myDocs-acls {
flex-direction: column;
- margin-right: 12;
label {
font-weight: normal;
font-style: italic;
+ padding-right: 12;
}
input {
@@ -102,7 +127,9 @@
.group-container {
width: 50%;
display: flex;
+ top:0;
flex-direction: column;
+ border-radius: 4px;
.user-sort {
text-align: left;
@@ -119,10 +146,12 @@
.groups-list,
.users-list {
font-style: italic;
- background: #e8e8e8;
+ border: 2px solid;
padding-left: 10px;
padding-right: 10px;
- width: 100%;
+ width: 97%;
+ border-bottom-left-radius: 4px;
+ border-bottom-right-radius: 4px;
overflow-y: scroll;
overflow-x: hidden;
text-align: left;
@@ -131,7 +160,6 @@
align-items: center;
text-align: center;
justify-content: center;
- color: black;
height: 250px;
margin: 0 2;
@@ -146,7 +174,6 @@
outline: none;
border-radius: 5px;
border: 0px;
- color: #fcfbf7;
text-transform: none;
letter-spacing: 2px;
font-size: 75%;
@@ -165,11 +192,10 @@
font-size: 20px;
text-align: left;
margin: 0 0 20px 0;
- color: black;
}
.hr-substitute {
- border: solid black 0.5px;
+ border: solid 0.5px;
margin-top: 20px;
}
@@ -190,88 +216,186 @@
}
}
+ .title-individual{
+ height: 25px;
+ padding-left: 2;
+ width: 97%;
+ border-radius: 4px;
+ margin-top: 10px;
+ margin-left: -8px;
+ font-size: 14;
+ margin-bottom: -4;
+ border: 2px solid;
+ border-bottom: none;
+ align-items: center;
+ display: flex;
+ }
+
+ .title-group{
+ height: 25px;
+ padding-left: 2;
+ width: 97%;
+ border-radius: 4px;
+ margin-top: 10px;
+ margin-left: -8px;
+ font-size: 14;
+ margin-bottom: -4;
+ border: 2px solid;
+ border-bottom: none;
+ align-items: center;
+ display: flex;
+ }
+
.container {
display: flex;
position: relative;
margin-top: 5px;
- margin-bottom: 10px;
+ margin-left: -5px;
font-size: 22px;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
- width: 100%;
+ width: 97%;
text-align: left;
font-style: normal;
- font-size: 14;
+ font-size: 12.5;
font-weight: normal;
- padding: 0;
- align-items: center;
+
+ padding: 3px;
+ border-bottom: 0.5px solid;
.group-info {
cursor: pointer;
}
&:hover .padding {
+ overflow-x: unset;
white-space: unset;
+ overflow-wrap: break-word;
}
.padding {
- padding: 0 10px 0 0;
- color: black;
+ max-width: 150px;
+ overflow-x: hidden;
+ display: inline-block;
text-overflow: ellipsis;
- overflow: hidden;
white-space: nowrap;
- max-width: 40%;
}
.permissions-dropdown {
- border: none;
- height: 25;
- background-color: #e8e8e8;
+ display: flex;
+ align-items: flex-end;
+ text-align: right;
+ margin-left: auto;
+ margin-right: -12px;
+
}
.edit-actions {
display: flex;
position: absolute;
- right: -10;
+ align-items: flex-end;
+ right: -10;
}
+ }
+
+ .permissions-dropdown-None{
+ height: 100%;
+ min-width: 85px;
+ text-align: right;
+ margin-right: -12px;
+ padding: 0px;
+ padding-left: 3px;
+ background: grey;
+ color: rgb(71, 71, 71);
+ border-radius: 6px;
+ border: 1px solid rgb(71, 71, 71);
+ }
+ .permissions-dropdown-Edit,
+ .permissions-dropdown-Admin {
+ height: 100%;
+ min-width: 85px;
+ text-align: right;
+ margin-right: -12px;
+ padding: 0px;
+ padding-left: 3px;
+ background: rgb(254, 254, 199);
+ color: rgb(75, 75, 5);
+ border-radius: 6px;
+ border: 1px solid rgb(75, 75, 5);
+ }
+ .permissions-dropdown-Augment{
+ height: 100%;
+ min-width: 85px;
+ text-align: right;
+ margin-right: -12px;
+ padding: 0px;
+ padding-left: 3px;
+ background: rgb(208, 255, 208);
+ color:rgb(19, 80, 19);
+ border-radius: 6px;
+ border: 1px solid rgb(19, 80, 19);
}
+ .permissions-dropdown-View{
+ height: 100%;
+ min-width: 85px;
+ text-align: right;
+ margin-right: -12px;
+ padding: 0px;
+ padding-left: 3px;
+ background: rgb(213, 213, 255);
+ color: rgb(25, 25, 101);
+ border-radius: 6px;
+ border: 1px solid rgb(25, 25, 101);
+ }
+ .permissions-dropdown-Not-Shared{
+ height: 100%;
+ min-width: 85px;
+ text-align: right;
+ margin-right: -12px;
+ padding: 0px;
+ padding-left: 3px;
+ background: rgb(255, 207, 207);
+ color: rgb(138, 47, 47);
+ border-radius: 6px;
+ border: 1px solid rgb(138, 47, 47);
+ }
.no-users {
margin-top: 20px;
}
- .link-container {
- display: flex;
- flex-direction: row;
- margin-bottom: 10px;
- margin-left: auto;
- margin-right: auto;
-
- .link-box,
- .copy {
- padding: 10px;
- border-radius: 10px;
- padding: 10px;
- border: solid black 1px;
- }
-
- .link-box {
- background: white;
- color: blue;
- text-decoration: underline;
- }
-
- .copy {
- margin-left: 20px;
- cursor: alias;
- border-radius: 50%;
- width: 42px;
- height: 42px;
- transition: 1.5s all ease;
- padding-top: 12px;
- }
- }
+ // .link-container {
+ // display: flex;
+ // flex-direction: row;
+ // margin-bottom: 10px;
+ // margin-left: auto;
+ // margin-right: auto;
+
+ // .link-box,
+ // .copy {
+ // padding: 10px;
+ // border-radius: 10px;
+ // padding: 10px;
+ // border: solid black 1px;
+ // }
+
+ // .link-box {
+ // background: white;
+ // color: blue;
+ // text-decoration: underline;
+ // }
+
+ // .copy {
+ // margin-left: 20px;
+ // cursor: alias;
+ // border-radius: 50%;
+ // width: 42px;
+ // height: 42px;
+ // transition: 1.5s all ease;
+ // padding-top: 12px;
+ // }
+ // }
} \ No newline at end of file
diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx
index 0eecb31cf..6171c01d7 100644
--- a/src/client/util/SharingManager.tsx
+++ b/src/client/util/SharingManager.tsx
@@ -1,14 +1,16 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { intersection } from 'lodash';
+import { Button, IconButton, Size, Type } from 'browndash-components';
+import { concat, intersection } from 'lodash';
import { action, computed, observable, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import Select from 'react-select';
import * as RequestPromise from 'request-promise';
-import { AclAdmin, AclPrivate, AclSym, AclUnset, DataSym, Doc, DocListCast, DocListCastAsync, HierarchyMapping } from '../../fields/Doc';
+import { Doc, DocListCast, DocListCastAsync, HierarchyMapping, ReverseHierarchyMap } from '../../fields/Doc';
+import { AclAdmin, AclPrivate, DocAcl, DocData } from '../../fields/DocSymbols';
+import { FieldLoader } from '../../fields/FieldLoader';
import { Id } from '../../fields/FieldSymbols';
-import { List } from '../../fields/List';
-import { NumCast, StrCast } from '../../fields/Types';
+import { StrCast } from '../../fields/Types';
import { distributeAcls, GetEffectiveAcl, normalizeEmail, SharingPermissions, TraceMobx } from '../../fields/util';
import { Utils } from '../../Utils';
import { DocServer } from '../DocServer';
@@ -20,8 +22,11 @@ import { SearchBox } from '../views/search/SearchBox';
import { DocumentManager } from './DocumentManager';
import { GroupManager, UserOptions } from './GroupManager';
import { GroupMemberView } from './GroupMemberView';
+import { LinkManager } from './LinkManager';
import { SelectionManager } from './SelectionManager';
+import { SettingsManager } from './SettingsManager';
import './SharingManager.scss';
+import { undoable } from './UndoManager';
export interface User {
email: string;
@@ -46,6 +51,7 @@ const indType = '!indType/';
const groupType = '!groupType/';
const storage = 'data';
+const dashStorage = 'data_dashboards';
/**
* A user who also has a sharing doc.
@@ -72,13 +78,14 @@ export class SharingManager extends React.Component<{}> {
@observable private individualSort: 'ascending' | 'descending' | 'none' = 'none'; // sorting options for the list of individuals
@observable private groupSort: 'ascending' | 'descending' | 'none' = 'none'; // sorting options for the list of groups
private shareDocumentButtonRef: React.RefObject<HTMLButtonElement> = React.createRef(); // ref for the share button, used for the position of the popup
- private distributeAclsButtonRef: React.RefObject<HTMLButtonElement> = React.createRef(); // ref for the distribute button, used for the position of the popup
// if both showUserOptions and showGroupOptions are false then both are displayed
@observable private showUserOptions: boolean = false; // whether to show individuals as options when sharing (in the react-select component)
@observable private showGroupOptions: boolean = false; // // whether to show groups as options when sharing (in the react-select component)
private populating: boolean = false; // whether the list of users is populating or not
+ @observable private upgradeNested: boolean = false; // whether child docs in a collection/dashboard should be changed to be less private - initially selected so default is upgrade all
@observable private layoutDocAcls: boolean = false; // whether the layout doc or data doc's acls are to be used
@observable private myDocAcls: boolean = false; // whether the My Docs checkbox is selected or not
+ @observable private _buttonDown = false;
// private get linkVisible() {
// return this.targetDoc ? this.targetDoc["acl-" + PublicKey] !== SharingPermissions.None : false;
@@ -92,6 +99,7 @@ export class SharingManager extends React.Component<{}> {
DictationOverlay.Instance.hasActiveModal = true;
this.isOpen = this.targetDoc !== undefined;
this.permissions = SharingPermissions.Augment;
+ this.upgradeNested = true;
});
};
@@ -107,6 +115,7 @@ export class SharingManager extends React.Component<{}> {
}),
500
);
+ this.layoutDocAcls = false;
});
constructor(props: {}) {
@@ -129,6 +138,7 @@ export class SharingManager extends React.Component<{}> {
this.populating = true;
const userList = await RequestPromise.get(Utils.prepend('/getUsers'));
const raw = (JSON.parse(userList) as User[]).filter(user => user.email !== 'guest' && user.email !== Doc.CurrentUserEmail);
+ runInAction(() => (FieldLoader.ServerLoadStatus.message = 'users'));
const docs = await DocServer.GetRefFields(raw.reduce((list, user) => [...list, user.sharingDocumentId, user.linkDatabaseId], [] as string[]));
raw.map(
action((newUser: User) => {
@@ -137,7 +147,7 @@ export class SharingManager extends React.Component<{}> {
if (sharingDoc instanceof Doc && linkDatabase instanceof Doc) {
if (!this.users.find(user => user.user.email === newUser.email)) {
this.users.push({ user: newUser, sharingDoc, linkDatabase, userColor: StrCast(sharingDoc.userColor) });
- // LinkManager.addLinkDB(sharer.linkDatabase);
+ //LinkManager.addLinkDB(linkDatabase);
}
}
})
@@ -149,79 +159,44 @@ export class SharingManager extends React.Component<{}> {
/**
* Shares the document with a user.
*/
- setInternalSharing = (recipient: ValidatedUser, permission: string, targetDoc?: Doc) => {
+ setInternalSharing = undoable((recipient: ValidatedUser, permission: string, targetDoc: Doc | undefined) => {
const { user, sharingDoc } = recipient;
const target = targetDoc || this.targetDoc!;
const acl = `acl-${normalizeEmail(user.email)}`;
- const myAcl = `acl-${Doc.CurrentUserEmailNormalized}`;
- const isDashboard = DocListCast(Doc.MyDashboards.data).indexOf(target) !== -1;
-
- const docs = SelectionManager.Views().length < 2 ? [target] : SelectionManager.Views().map(docView => docView.props.Document);
- return !docs
- .map(doc => (this.layoutDocAcls ? doc : doc[DataSym]))
- .map(doc => {
- doc.author === Doc.CurrentUserEmail && !doc[myAcl] && distributeAcls(myAcl, SharingPermissions.Admin, doc, undefined, undefined, isDashboard);
-
- if (permission === SharingPermissions.None) {
- if (doc[acl] && doc[acl] !== SharingPermissions.None) doc.numUsersShared = NumCast(doc.numUsersShared, 1) - 1;
- } else {
- if (!doc[acl] || doc[acl] === SharingPermissions.None) doc.numUsersShared = NumCast(doc.numUsersShared, 0) + 1;
- }
-
- distributeAcls(acl, permission as SharingPermissions, doc, undefined, undefined, isDashboard);
-
- this.setDashboardBackground(doc, permission as SharingPermissions);
- if (permission !== SharingPermissions.None) return Doc.AddDocToList(sharingDoc, storage, doc);
- return GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, storage, (doc.createdFrom as Doc) || doc);
- })
- .some(success => !success);
- };
+ const docs = SelectionManager.Views().length < 2 ? [target] : SelectionManager.Views().map(docView => docView.rootDoc);
+ docs.map(doc => (this.layoutDocAcls || doc.dockingConfig ? doc : Doc.GetProto(doc))).forEach(doc => {
+ distributeAcls(acl, permission as SharingPermissions, doc, undefined, this.upgradeNested ? true : undefined);
+ if (permission !== SharingPermissions.None) {
+ Doc.AddDocToList(sharingDoc, doc.dockingConfig ? dashStorage : storage, doc);
+ } else GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, ((doc.createdFrom as Doc) || doc).dockingConfig ? dashStorage : storage, (doc.createdFrom as Doc) || doc);
+ });
+ }, 'set Doc permissions');
/**
* Sets the permission on the target for the group.
* @param group
* @param permission
*/
- setInternalGroupSharing = (group: Doc | { title: string }, permission: string, targetDoc?: Doc) => {
+ setInternalGroupSharing = undoable((group: Doc | { title: string }, permission: string, targetDoc?: Doc) => {
const target = targetDoc || this.targetDoc!;
- const key = normalizeEmail(StrCast(group.title));
- const acl = `acl-${key}`;
- const isDashboard = DocListCast(Doc.MyDashboards.data).indexOf(target) !== -1;
-
- const docs = SelectionManager.Views().length < 2 ? [target] : SelectionManager.Views().map(docView => docView.props.Document);
-
- // ! ensures it returns true if document has been shared successfully, false otherwise
- return !docs
- .map(doc => (this.layoutDocAcls ? doc : doc[DataSym]))
- .map(doc => {
- doc.author === Doc.CurrentUserEmail && !doc[`acl-${Doc.CurrentUserEmailNormalized}`] && distributeAcls(`acl-${Doc.CurrentUserEmailNormalized}`, SharingPermissions.Admin, doc, undefined, undefined, isDashboard);
-
- if (permission === SharingPermissions.None) {
- if (doc[acl] && doc[acl] !== SharingPermissions.None) doc.numGroupsShared = NumCast(doc.numGroupsShared, 1) - 1;
- } else {
- if (!doc[acl] || doc[acl] === SharingPermissions.None) doc.numGroupsShared = NumCast(doc.numGroupsShared, 0) + 1;
- }
+ const acl = `acl-${normalizeEmail(StrCast(group.title))}`;
- distributeAcls(acl, permission as SharingPermissions, doc, undefined, undefined, isDashboard);
- this.setDashboardBackground(doc, permission as SharingPermissions);
+ const docs = SelectionManager.Views().length < 2 ? [target] : SelectionManager.Views().map(docView => docView.rootDoc);
+ docs.map(doc => (this.layoutDocAcls || doc.dockingConfig ? doc : Doc.GetProto(doc))).forEach(doc => {
+ distributeAcls(acl, permission as SharingPermissions, doc, undefined, this.upgradeNested ? true : undefined);
- if (group instanceof Doc) {
- const members: string[] = JSON.parse(StrCast(group.members));
- const users: ValidatedUser[] = this.users.filter(({ user: { email } }) => members.includes(email));
+ if (group instanceof Doc) {
+ Doc.AddDocToList(group, 'docsShared', doc);
- // if documents have been shared, add the doc to that list if it doesn't already exist, otherwise create a new list with the doc
- group.docsShared ? Doc.IndexOf(doc, DocListCast(group.docsShared)) === -1 && (group.docsShared as List<Doc>).push(doc) : (group.docsShared = new List<Doc>([doc]));
-
- return users
- .map(({ user, sharingDoc }) => {
- if (permission !== SharingPermissions.None) return Doc.AddDocToList(sharingDoc, storage, doc); // add the doc to the sharingDoc if it hasn't already been added
- else return GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, storage, (doc.createdFrom as Doc) || doc); // remove the doc from the list if it already exists
- })
- .some(success => !success);
- }
- })
- .some(success => success);
- };
+ this.users
+ .filter(({ user: { email } }) => JSON.parse(StrCast(group.members)).includes(email))
+ .forEach(({ user, sharingDoc }) => {
+ if (permission !== SharingPermissions.None) Doc.AddDocToList(sharingDoc, doc.dockingConfig ? dashStorage : storage, doc); // add the doc to the sharingDoc if it hasn't already been added
+ else GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, ((doc.createdFrom as Doc) || doc).dockingConfig ? dashStorage : storage, (doc.createdFrom as Doc) || doc); // remove the doc from the list if it already exists
+ });
+ }
+ });
+ }, 'set group permissions');
/**
* Shares the documents shared with a group with a new user who has been added to that group.
@@ -236,7 +211,13 @@ export class SharingManager extends React.Component<{}> {
else {
DocListCastAsync(user.sharingDoc[storage]).then(userdocs =>
DocListCastAsync(group.docsShared).then(dl => {
- const filtered = dl?.filter(doc => !userdocs?.includes(doc));
+ const filtered = dl?.filter(doc => !doc.dockingConfig && !userdocs?.includes(doc));
+ filtered && userdocs?.push(...filtered);
+ })
+ );
+ DocListCastAsync(user.sharingDoc[dashStorage]).then(userdocs =>
+ DocListCastAsync(group.docsShared).then(dl => {
+ const filtered = dl?.filter(doc => doc.dockingConfig && !userdocs?.includes(doc));
filtered && userdocs?.push(...filtered);
})
);
@@ -247,44 +228,23 @@ export class SharingManager extends React.Component<{}> {
/**
* Called from the properties sidebar to change permissions of a user.
*/
- shareFromPropertiesSidebar = (shareWith: string, permission: SharingPermissions, docs: Doc[]) => {
- if (shareWith !== 'Public' && shareWith !== 'Override') {
+ shareFromPropertiesSidebar = undoable((shareWith: string, permission: SharingPermissions, docs: Doc[], layout: boolean) => {
+ if (layout) this.layoutDocAcls = true;
+ if (shareWith !== 'Guest') {
const user = this.users.find(({ user: { email } }) => email === (shareWith === 'Me' ? Doc.CurrentUserEmail : shareWith));
docs.forEach(doc => {
if (user) this.setInternalSharing(user, permission, doc);
- else this.setInternalGroupSharing(GroupManager.Instance.getGroup(shareWith)!, permission, doc);
+ else this.setInternalGroupSharing(GroupManager.Instance.getGroup(shareWith)!, permission, doc, undefined, true);
});
} else {
- const dashboards = DocListCast(Doc.MyDashboards.data);
docs.forEach(doc => {
- const isDashboard = dashboards.indexOf(doc) !== -1;
- if (GetEffectiveAcl(doc) === AclAdmin) distributeAcls(`acl-${shareWith}`, permission, doc, undefined, undefined, isDashboard);
- this.setDashboardBackground(doc, permission as SharingPermissions);
- });
- }
- };
-
- /**
- * Sets the background of the Dashboard if it has been shared as a visual indicator
- */
- setDashboardBackground = (doc: Doc, permission: SharingPermissions) => {
- if (Doc.IndexOf(doc, DocListCast(Doc.MyDashboards.data)) !== -1) {
- if (permission !== SharingPermissions.None) {
- doc.isShared = true;
- doc.backgroundColor = 'green';
- } else {
- const acls = doc[DataSym][AclSym];
- if (
- Object.keys(acls)
- .filter(key => key !== `acl-${Doc.CurrentUserEmailNormalized}` && key !== 'acl-Me')
- .every(key => [AclUnset, AclPrivate].includes(acls[key]))
- ) {
- doc.isShared = undefined;
- doc.backgroundColor = undefined;
+ if (GetEffectiveAcl(doc) === AclAdmin) {
+ distributeAcls(`acl-${shareWith}`, permission, doc, undefined);
}
- }
+ });
}
- };
+ this.layoutDocAcls = false;
+ }, 'sidebar set permissions');
/**
* Removes the documents shared with a user through a group when the user is removed from the group.
@@ -294,13 +254,19 @@ export class SharingManager extends React.Component<{}> {
removeMember = (group: Doc, emailId: string) => {
const user: ValidatedUser = this.users.find(({ user: { email } }) => email === emailId)!;
- if (group.docsShared) {
+ if (group.docsShared && user) {
DocListCastAsync(user.sharingDoc[storage]).then(userdocs =>
DocListCastAsync(group.docsShared).then(dl => {
const remaining = userdocs?.filter(doc => !dl?.includes(doc)) || [];
userdocs?.splice(0, userdocs.length, ...remaining);
})
);
+ DocListCastAsync(user.sharingDoc[dashStorage]).then(userdocs =>
+ DocListCastAsync(group.docsShared).then(dl => {
+ const remaining = userdocs?.filter(doc => !dl?.includes(doc)) || [];
+ userdocs?.splice(0, userdocs.length, ...remaining);
+ })
+ );
}
};
@@ -310,11 +276,9 @@ export class SharingManager extends React.Component<{}> {
*/
removeGroup = (group: Doc) => {
if (group.docsShared) {
- const dashboards = DocListCast(Doc.MyDashboards.data);
DocListCast(group.docsShared).forEach(doc => {
const acl = `acl-${StrCast(group.title)}`;
- const isDashboard = dashboards.indexOf(doc) !== -1;
- distributeAcls(acl, SharingPermissions.None, doc, undefined, undefined, isDashboard);
+ distributeAcls(acl, SharingPermissions.None, doc);
const members: string[] = JSON.parse(StrCast(group.members));
const users: ValidatedUser[] = this.users.filter(({ user: { email } }) => members.includes(email));
@@ -330,37 +294,26 @@ export class SharingManager extends React.Component<{}> {
// return;
// }
// targetDoc["acl-" + PublicKey] = permission;
- // }
-
- // private get sharingUrl() {
- // if (!this.targetDoc) {
- // return undefined;
- // }
- // const baseUrl = Utils.prepend("/doc/" + this.targetDoc[Id]);
- // return `${baseUrl}?sharing=true`;
- // }
+ // }s
- // copy = action(() => {
- // if (this.sharingUrl) {
- // Utils.CopyText(this.sharingUrl);
- // this.copied = true;
- // }
- // });
+ /**
+ * Copies the Public sharing url to the user's clipboard.
+ */
+ private copyURL = (e: any) => {
+ Utils.CopyText(Utils.shareUrl(this.targetDoc![Id]));
+ };
/**
* Returns the SharingPermissions (Admin, Can Edit etc) access that's used to share
*/
- private sharingOptions(uniform: boolean, override?: boolean) {
- const dropdownValues: string[] = Object.values(SharingPermissions);
+ private sharingOptions(uniform: boolean, showGuestOptions?: boolean) {
+ const dropdownValues: string[] = showGuestOptions ? [SharingPermissions.None, SharingPermissions.View] : Object.values(SharingPermissions);
if (!uniform) dropdownValues.unshift('-multiple-');
- if (!override) dropdownValues.splice(dropdownValues.indexOf(SharingPermissions.Unset), 1);
- return dropdownValues
- .filter(permission => !Doc.noviceMode || ![SharingPermissions.SelfEdit].includes(permission as any))
- .map(permission => (
- <option key={permission} value={permission}>
- {permission}
- </option>
- ));
+ return dropdownValues.map(permission => (
+ <option key={permission} value={permission}>
+ {concat(ReverseHierarchyMap.get(permission)?.image, ' ', permission)}
+ </option>
+ ));
}
private focusOn = (contents: string) => {
@@ -405,38 +358,43 @@ export class SharingManager extends React.Component<{}> {
/**
* Handles changes in the permission chosen to share with someone with
*/
- @action
- handlePermissionsChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
- this.permissions = event.currentTarget.value as SharingPermissions;
- };
+ handlePermissionsChange = undoable(
+ action((event: React.ChangeEvent<HTMLSelectElement>) => {
+ this.permissions = event.currentTarget.value as SharingPermissions;
+ }),
+ 'permission change'
+ );
/**
* Calls the relevant method for sharing, displays the popup, and resets the relevant variables.
*/
- @action
- share = () => {
- if (this.selectedUsers) {
- this.selectedUsers.forEach(user => {
- if (user.value.includes(indType)) {
- this.setInternalSharing(this.users.find(u => u.user.email === user.label)!, this.permissions);
- } else {
- this.setInternalGroupSharing(GroupManager.Instance.getGroup(user.label)!, this.permissions);
- }
- });
-
- const { left, width, top, height } = this.shareDocumentButtonRef.current!.getBoundingClientRect();
- TaskCompletionBox.popupX = left - 1.5 * width;
- TaskCompletionBox.popupY = top - 1.5 * height;
- TaskCompletionBox.textDisplayed = 'Document shared!';
- TaskCompletionBox.taskCompleted = true;
- setTimeout(
- action(() => (TaskCompletionBox.taskCompleted = false)),
- 2000
- );
+ share = undoable(
+ action(() => {
+ if (this.selectedUsers) {
+ this.selectedUsers.forEach(user => {
+ if (user.value.includes(indType)) {
+ this.setInternalSharing(this.users.find(u => u.user.email === user.label)!, this.permissions, undefined);
+ } else {
+ this.setInternalGroupSharing(GroupManager.Instance.getGroup(user.label)!, this.permissions);
+ }
+ });
+
+ const { left, width, top, height } = this.shareDocumentButtonRef.current!.getBoundingClientRect();
+ TaskCompletionBox.popupX = left - 1.5 * width;
+ TaskCompletionBox.popupY = top - 1.5 * height;
+ TaskCompletionBox.textDisplayed = 'Document shared!';
+ TaskCompletionBox.taskCompleted = true;
+ setTimeout(
+ action(() => (TaskCompletionBox.taskCompleted = false)),
+ 2000
+ );
- this.selectedUsers = null;
- }
- };
+ this.layoutDocAcls = false;
+ this.selectedUsers = null;
+ }
+ }),
+ 'share Doc'
+ );
/**
* Sorting algorithm to sort users.
@@ -463,6 +421,7 @@ export class SharingManager extends React.Component<{}> {
if (!this.targetDoc) return null;
TraceMobx();
const groupList = GroupManager.Instance?.allGroups || [];
+
const sortedUsers = this.users
.slice()
.sort(this.sortUsers)
@@ -484,8 +443,7 @@ export class SharingManager extends React.Component<{}> {
const users = this.individualSort === 'ascending' ? this.users.slice().sort(this.sortUsers) : this.individualSort === 'descending' ? this.users.slice().sort(this.sortUsers).reverse() : this.users;
const groups = this.groupSort === 'ascending' ? groupList.slice().sort(this.sortGroups) : this.groupSort === 'descending' ? groupList.slice().sort(this.sortGroups).reverse() : groupList;
- // handles the case where multiple documents are selected
- let docs = SelectionManager.Views().length < 2 ? [this.layoutDocAcls ? this.targetDoc : this.targetDoc?.[DataSym]] : SelectionManager.Views().map(docView => (this.layoutDocAcls ? docView.props.Document : docView.props.Document?.[DataSym]));
+ let docs = SelectionManager.Views().length < 2 ? [this.targetDoc] : SelectionManager.Views().map(docView => docView.rootDoc);
if (this.myDocAcls) {
const newDocs: Doc[] = [];
@@ -493,33 +451,39 @@ export class SharingManager extends React.Component<{}> {
docs = newDocs.filter(doc => GetEffectiveAcl(doc) === AclAdmin);
}
- const targetDoc = this.layoutDocAcls ? docs[0] : docs[0]?.[DataSym];
+ const targetDoc = this.layoutDocAcls ? docs[0] : docs[0]?.[DocData];
// tslint:disable-next-line: no-unnecessary-callback-wrapper
const effectiveAcls = docs.map(doc => GetEffectiveAcl(doc));
const admin = this.myDocAcls ? Boolean(docs.length) : effectiveAcls.every(acl => acl === AclAdmin);
// users in common between all docs
- const commonKeys = intersection(...docs.map(doc => (this.layoutDocAcls ? doc : doc[DataSym])).map(doc => doc?.[AclSym] && Object.keys(doc[AclSym])));
+ const commonKeys = intersection(docs).reduce((list, doc) => (doc?.[DocAcl] ? [...list, ...Object.keys(doc[DocAcl])] : list), [] as string[]);
// the list of users shared with
- const userListContents: (JSX.Element | null)[] = users
- .filter(({ user }) => (docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(user.email)}`) : docs[0]?.author !== user.email))
+ const userListContents = users
+ // .filter(({ user }) => (docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(user.email)}`) : docs[0]?.author !== user.email))
+ .filter(({ user }) => docs[0]?.author !== user.email)
.map(({ user, linkDatabase, sharingDoc, userColor }) => {
const userKey = `acl-${normalizeEmail(user.email)}`;
- const uniform = docs.map(doc => (this.layoutDocAcls ? doc : doc[DataSym])).every(doc => doc?.[AclSym]?.[userKey] === docs[0]?.[AclSym]?.[userKey]);
- const permissions = uniform ? StrCast(targetDoc?.[userKey]) : '-multiple-';
+ const uniform = docs.every(doc => doc?.[DocAcl]?.[userKey] === docs[0]?.[DocAcl]?.[userKey]);
+ // const permissions = uniform ? StrCast(targetDoc?.[userKey]) : '-multiple-';
+ let permissions = targetDoc[DocAcl][userKey] ? HierarchyMapping.get(targetDoc[DocAcl][userKey])?.name : StrCast(targetDoc[userKey]);
+ permissions = uniform ? StrCast(targetDoc?.[userKey]) : '-multiple-';
return !permissions ? null : (
<div key={userKey} className={'container'}>
<span className={'padding'}>{user.email}</span>
<div className="edit-actions">
{admin || this.myDocAcls ? (
- <select className={'permissions-dropdown'} value={permissions} onChange={e => this.setInternalSharing({ user, linkDatabase, sharingDoc, userColor }, e.currentTarget.value)}>
+ <select className={`permissions-dropdown-${permissions}`} value={permissions} onChange={e => this.setInternalSharing({ user, linkDatabase, sharingDoc, userColor }, e.currentTarget.value, undefined)}>
{this.sharingOptions(uniform)}
</select>
) : (
- <div className={'permissions-dropdown'}>{permissions}</div>
+ <div className={`permissions-dropdown-${permissions}`}>
+ {concat(ReverseHierarchyMap.get(permissions)?.image, ' ', permissions)}
+ &nbsp;
+ </div>
)}
</div>
</div>
@@ -530,10 +494,13 @@ export class SharingManager extends React.Component<{}> {
const sameAuthor = docs.every(doc => doc?.author === docs[0]?.author);
// the owner of the doc and the current user are placed at the top of the user list.
+ const userKey = `acl-${normalizeEmail(Doc.CurrentUserEmail)}`;
+ const curUserPermission = StrCast(targetDoc[userKey]);
+ // const curUserPermission = HierarchyMapping.get(effectiveAcls[0])!.name
userListContents.unshift(
sameAuthor ? (
<div key={'owner'} className={'container'}>
- <span className={'padding'}>{targetDoc?.author === Doc.CurrentUserEmail ? 'Me' : targetDoc?.author}</span>
+ <span className="padding">{targetDoc?.author === Doc.CurrentUserEmail ? 'Me' : StrCast(targetDoc?.author)}</span>
<div className="edit-actions">
<div className={'permissions-dropdown'}>Owner</div>
</div>
@@ -543,7 +510,10 @@ export class SharingManager extends React.Component<{}> {
<div key={'me'} className={'container'}>
<span className={'padding'}>Me</span>
<div className="edit-actions">
- <div className={'permissions-dropdown'}>{effectiveAcls.every(acl => acl === effectiveAcls[0]) ? HierarchyMapping.get(effectiveAcls[0])!.name : '-multiple-'}</div>
+ <div className={`permissions-dropdown-${curUserPermission}`}>
+ {effectiveAcls.every(acl => acl === effectiveAcls[0]) ? concat(ReverseHierarchyMap.get(curUserPermission!)?.image, ' ', curUserPermission) : '-multiple-'}
+ &nbsp;
+ </div>
</div>
</div>
) : null
@@ -551,29 +521,27 @@ export class SharingManager extends React.Component<{}> {
// the list of groups shared with
const groupListMap: (Doc | { title: string })[] = groups.filter(({ title }) => (docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(StrCast(title))}`) : true));
- groupListMap.unshift({ title: 'Public' }); //, { title: "ALL" });
+ groupListMap.unshift({ title: 'Guest' }); //, { title: "ALL" });
const groupListContents = groupListMap.map(group => {
- const groupKey = `acl-${StrCast(group.title)}`;
- const uniform = docs
- .map(doc => (this.layoutDocAcls ? doc : doc[DataSym]))
- .every(doc => (this.layoutDocAcls ? doc?.[AclSym]?.[groupKey] === docs[0]?.[AclSym]?.[groupKey] : doc?.[DataSym]?.[AclSym]?.[groupKey] === docs[0]?.[DataSym]?.[AclSym]?.[groupKey]));
- const permissions = uniform ? StrCast(targetDoc?.[`acl-${StrCast(group.title)}`]) : '-multiple-';
+ let groupKey = `acl-${StrCast(group.title)}`;
+ const uniform = docs.every(doc => doc?.[DocAcl]?.[groupKey] === docs[0]?.[DocAcl]?.[groupKey]);
+ const permissions = uniform ? StrCast(targetDoc?.[groupKey]) : '-multiple-';
return !permissions ? null : (
- <div key={groupKey} className={'container'}>
+ <div key={groupKey} className={'container'} style={{ background: SettingsManager.Instance.userBackgroundColor, color: SettingsManager.Instance.userColor }}>
<div className={'padding'}>{StrCast(group.title)}</div>
- {group instanceof Doc ? (
- <div className="group-info" onClick={action(() => (GroupManager.Instance.currentGroup = group))}>
- <FontAwesomeIcon icon={'info-circle'} color={'#e8e8e8'} size={'sm'} style={{ backgroundColor: '#1e89d7', borderRadius: '100%', border: '1px solid #1e89d7' }} />
- </div>
- ) : null}
- <div className="edit-actions">
+ &nbsp;
+ {group instanceof Doc ? <IconButton icon={<FontAwesomeIcon icon={'info-circle'} />} size={Size.XSMALL} color={SettingsManager.Instance.userColor} onClick={action(() => (GroupManager.Instance.currentGroup = group))} /> : null}
+ <div className={'edit-actions'}>
{admin || this.myDocAcls ? (
- <select className={'permissions-dropdown'} value={permissions} onChange={e => this.setInternalGroupSharing(group, e.currentTarget.value)}>
- {this.sharingOptions(uniform, group.title === 'Override')}
+ <select className={`permissions-dropdown-${permissions}`} value={permissions} onChange={e => this.setInternalGroupSharing(group, e.currentTarget.value)}>
+ {this.sharingOptions(uniform, group.title === 'Guest')}
</select>
) : (
- <div className={'permissions-dropdown'}>{permissions}</div>
+ <div className={`permissions-dropdown-${permissions}`}>
+ {concat(ReverseHierarchyMap.get(permissions)?.image, ' ', permissions)}
+ &nbsp;
+ </div>
)}
</div>
</div>
@@ -582,22 +550,28 @@ export class SharingManager extends React.Component<{}> {
return (
<div className="sharing-interface">
{GroupManager.Instance?.currentGroup ? <GroupMemberView group={GroupManager.Instance.currentGroup} onCloseButtonClick={action(() => (GroupManager.Instance.currentGroup = undefined))} /> : null}
- <div className="sharing-contents">
- <p className={'share-title'}>
+ <div
+ className="sharing-contents"
+ style={{
+ background: SettingsManager.Instance.userBackgroundColor,
+ color: StrCast(Doc.UserDoc().userColor),
+ }}>
+ <p className="share-title" style={{ color: StrCast(Doc.UserDoc().userColor) }}>
+ <div className="share-info" onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/features/collaboration/', '_blank')}>
+ <FontAwesomeIcon icon={'question-circle'} size={'sm'} onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/features/collaboration/', '_blank')} />
+ </div>
<b>Share </b>
{this.focusOn(docs.length < 2 ? StrCast(targetDoc?.title, 'this document') : '-multiple-')}
</p>
- <div className={'close-button'} onClick={this.close}>
- <FontAwesomeIcon icon={'times'} color={'black'} size={'lg'} />
+ <div className="share-copy-link">
+ <Button type={Type.TERT} color={StrCast(Doc.UserDoc().userColor)} icon={<FontAwesomeIcon icon={'copy'} size="sm" />} iconPlacement={'left'} text={'Copy Guest URL'} onClick={this.copyURL} />
</div>
- {/* {this.linkVisible ?
- <div>
- {this.sharingUrl}
- </div> :
- (null)} */}
- {
+ <div className="close-button">
+ <Button icon={<FontAwesomeIcon icon={'times'} size={'lg'} />} onClick={this.close} color={StrCast(Doc.UserDoc().userColor)} />
+ </div>
+ {admin ? (
<div className="share-container">
- <div className="share-setup">
+ <div className="share-setup" style={{ border: StrCast(Doc.UserDoc().userColor) }}>
<Select
className="user-search"
placeholder="Enter user or group name..."
@@ -609,17 +583,34 @@ export class SharingManager extends React.Component<{}> {
onChange={this.handleUsersChange}
value={this.selectedUsers}
styles={{
+ control: () => ({
+ display: 'inline-flex',
+ width: '100%',
+ }),
indicatorSeparator: () => ({
+ display: 'inline-flex',
visibility: 'hidden',
}),
+ indicatorsContainer: () => ({
+ display: 'inline-flex',
+ textDecorationColor: 'black',
+ }),
+ valueContainer: () => ({
+ display: 'inline-flex',
+ fontStyle: StrCast(Doc.UserDoc().userColor),
+ color: StrCast(Doc.UserDoc().userColor),
+ width: '100%',
+ }),
}}
/>
- <select className="permissions-select" onChange={this.handlePermissionsChange} value={this.permissions}>
- {this.sharingOptions(true)}
- </select>
- <button ref={this.shareDocumentButtonRef} className="share-button" onClick={this.share}>
- Share
- </button>
+ <div className="permissions-select">
+ <select className={`permissions-dropdown-${this.permissions}`} onChange={this.handlePermissionsChange} value={this.permissions}>
+ {this.sharingOptions(true)}
+ </select>
+ </div>
+ <div className="share-button">
+ <Button text={'SHARE'} type={Type.TERT} color={StrCast(Doc.UserDoc().userColor)} onClick={this.share} />
+ </div>
</div>
<div className="sort-checkboxes">
<input type="checkbox" onChange={action(() => (this.showUserOptions = !this.showUserOptions))} /> <label style={{ marginRight: 10 }}>Individuals</label>
@@ -629,36 +620,46 @@ export class SharingManager extends React.Component<{}> {
<div className="acl-container">
{Doc.noviceMode ? null : (
<div className="layoutDoc-acls">
+ <input type="checkbox" onChange={action(() => (this.upgradeNested = !this.upgradeNested))} checked={this.upgradeNested} /> <label>Upgrade Nested </label>
<input type="checkbox" onChange={action(() => (this.layoutDocAcls = !this.layoutDocAcls))} checked={this.layoutDocAcls} /> <label>Layout</label>
</div>
)}
</div>
</div>
- }
- <div className="main-container">
+ ) : (
+ <div className="share-container">
+ <div className="acl-container">
+ <div className="layoutDoc-acls">
+ <input type="checkbox" onChange={action(() => (this.layoutDocAcls = !this.layoutDocAcls))} checked={this.layoutDocAcls} /> <label>Layout</label>
+ </div>
+ </div>
+ </div>
+ )}
+ <div className="main-container" style={{ color: StrCast(Doc.UserDoc().userColor), border: StrCast(Doc.UserDoc().userColor) }}>
<div className={'individual-container'}>
<div className="user-sort" onClick={action(() => (this.individualSort = this.individualSort === 'ascending' ? 'descending' : this.individualSort === 'descending' ? 'none' : 'ascending'))}>
- Individuals{' '}
- {this.individualSort === 'ascending' ? (
- <FontAwesomeIcon icon={'caret-up'} size={'xs'} />
- ) : this.individualSort === 'descending' ? (
- <FontAwesomeIcon icon={'caret-down'} size={'xs'} />
- ) : (
- <FontAwesomeIcon icon={'caret-right'} size={'xs'} />
- )}
+ <div className="title-individual">
+ Individuals
+ <IconButton
+ icon={<FontAwesomeIcon icon={this.individualSort === 'ascending' ? 'caret-up' : this.individualSort === 'descending' ? 'caret-down' : 'caret-right'} />}
+ size={Size.XSMALL}
+ color={StrCast(Doc.UserDoc().userColor)}
+ />
+ </div>
</div>
- <div className={'users-list'}>{userListContents}</div>
+ <div className="users-list">{userListContents}</div>
</div>
<div className={'group-container'}>
<div className="user-sort" onClick={action(() => (this.groupSort = this.groupSort === 'ascending' ? 'descending' : this.groupSort === 'descending' ? 'none' : 'ascending'))}>
- Groups{' '}
- {this.groupSort === 'ascending' ? (
- <FontAwesomeIcon icon={'caret-up'} size={'xs'} />
- ) : this.groupSort === 'descending' ? (
- <FontAwesomeIcon icon={'caret-down'} size={'xs'} />
- ) : (
- <FontAwesomeIcon icon={'caret-right'} size={'xs'} />
- )}
+ <div className="title-group">
+ Groups
+ <IconButton icon={<FontAwesomeIcon icon={'info-circle'} />} size={Size.XSMALL} color={StrCast(Doc.UserDoc().userColor)} onClick={action(() => GroupManager.Instance.open())} />
+ <IconButton
+ icon={<FontAwesomeIcon icon={this.groupSort === 'ascending' ? 'caret-up' : this.groupSort === 'descending' ? 'caret-down' : 'caret-right'} />}
+ size={Size.XSMALL}
+ color={StrCast(Doc.UserDoc().userColor)}
+ />
+ </div>
</div>
<div className={'groups-list'}>{groupListContents}</div>
</div>
diff --git a/src/client/util/TrackMovements.ts b/src/client/util/TrackMovements.ts
index a755c5dc9..0e56ee1bc 100644
--- a/src/client/util/TrackMovements.ts
+++ b/src/client/util/TrackMovements.ts
@@ -3,6 +3,7 @@ import { NumCast } from '../../fields/Types';
import { Doc, DocListCast } from '../../fields/Doc';
import { CollectionDockingView } from '../views/collections/CollectionDockingView';
import { Id } from '../../fields/FieldSymbols';
+import { CollectionViewType } from '../documents/DocumentTypes';
export type Movement = {
time: number;
@@ -89,7 +90,7 @@ export class TrackMovements {
if (this.recordingFFViews === null) return;
// so that the size comparisons are correct, we must filter to only the FFViews
- const isFFView = (doc: Doc) => doc && 'viewType' in doc && doc.viewType === 'freeform';
+ const isFFView = (doc: Doc) => doc && doc._type_collection === CollectionViewType.Freeform;
const tabbedFFViews = new Set<Doc>();
for (const DashDoc of tabbedDocs) {
if (isFFView(DashDoc)) tabbedFFViews.add(DashDoc);
diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts
index d0aec45a6..9a6719ea5 100644
--- a/src/client/util/UndoManager.ts
+++ b/src/client/util/UndoManager.ts
@@ -1,9 +1,11 @@
import { observable, action, runInAction } from 'mobx';
+import { Field } from '../../fields/Doc';
+import { RichTextField } from '../../fields/RichTextField';
import { Without } from '../../Utils';
function getBatchName(target: any, key: string | symbol): string {
const keyName = key.toString();
- if (target && target.constructor && target.constructor.name) {
+ if (target?.constructor?.name) {
return `${target.constructor.name}.${keyName}`;
}
return keyName;
@@ -34,6 +36,17 @@ function propertyDecorator(target: any, key: string | symbol) {
});
}
+export function undoable(fn: (...args: any[]) => any, batchName: string): (...args: any[]) => any {
+ return function () {
+ const batch = UndoManager.StartBatch(batchName);
+ try {
+ return fn.apply(undefined, arguments as any);
+ } finally {
+ batch.end();
+ }
+ };
+}
+
export function undoBatch(target: any, key: string | symbol, descriptor?: TypedPropertyDescriptor<any>): any;
export function undoBatch(fn: (...args: any[]) => any): (...args: any[]) => any;
export function undoBatch(target: any, key?: string | symbol, descriptor?: TypedPropertyDescriptor<any>): any {
@@ -73,15 +86,24 @@ export namespace UndoManager {
}
type UndoBatch = UndoEvent[];
+ export let undoStackNames: string[] = observable([]);
+ export let redoStackNames: string[] = observable([]);
export let undoStack: UndoBatch[] = observable([]);
export let redoStack: UndoBatch[] = observable([]);
let currentBatch: UndoBatch | undefined;
- export let batchCounter = 0;
+ export let batchCounter = observable.box(0);
let undoing = false;
- let tempEvents: UndoEvent[] | undefined = undefined;
+ export let tempEvents: UndoEvent[] | undefined = undefined;
- export function AddEvent(event: UndoEvent): void {
- if (currentBatch && batchCounter && !undoing) {
+ export function AddEvent(event: UndoEvent, value?: any): void {
+ if (currentBatch && batchCounter.get() && !undoing) {
+ console.log(
+ ' '.slice(0, batchCounter.get()) +
+ 'UndoEvent : ' +
+ event.prop +
+ ' = ' +
+ (value instanceof RichTextField ? value.Text : value instanceof Array ? value.map(val => Field.toScriptString(val)).join(',') : Field.toScriptString(value))
+ );
currentBatch.push(event);
tempEvents?.push(event);
}
@@ -135,11 +157,13 @@ export namespace UndoManager {
private dispose = (cancel: boolean) => {
if (this.disposed) {
- throw new Error('Cannot dispose an already disposed batch');
+ console.log('WARNING: undo batch already disposed');
+ return false;
+ } else {
+ this.disposed = true;
+ openBatches.splice(openBatches.indexOf(this));
+ return EndBatch(this.batchName, cancel);
}
- this.disposed = true;
- openBatches.splice(openBatches.indexOf(this));
- return EndBatch(cancel);
};
end = () => this.dispose(false);
@@ -147,22 +171,23 @@ export namespace UndoManager {
}
export function StartBatch(batchName: string): Batch {
- // console.log("Start " + batchCounter + " " + batchName);
- batchCounter++;
- if (batchCounter > 0 && currentBatch === undefined) {
+ console.log(' '.slice(0, batchCounter.get()) + 'Start ' + batchCounter + ' ' + batchName);
+ runInAction(() => batchCounter.set(batchCounter.get() + 1));
+ if (currentBatch === undefined) {
currentBatch = [];
}
return new Batch(batchName);
}
- const EndBatch = action((cancel: boolean = false) => {
- batchCounter--;
- // console.log("End " + batchCounter);
- if (batchCounter === 0 && currentBatch?.length) {
- // console.log("------ended----")
+ const EndBatch = action((batchName: string, cancel: boolean = false) => {
+ runInAction(() => batchCounter.set(batchCounter.get() - 1));
+ console.log(' '.slice(0, batchCounter.get()) + 'End ' + batchName + ' (' + currentBatch?.length + ')');
+ if (batchCounter.get() === 0 && currentBatch?.length) {
if (!cancel) {
undoStack.push(currentBatch);
+ undoStackNames.push(batchName ?? '???');
}
+ redoStackNames.length = 0;
redoStack.length = 0;
currentBatch = undefined;
return true;
@@ -170,15 +195,11 @@ export namespace UndoManager {
return false;
});
- export function RunInTempBatch<T>(fn: () => T) {
+ export function StartTempBatch() {
tempEvents = [];
- try {
- const success = runInAction(fn);
- if (!success) UndoManager.UndoTempBatch();
- return success;
- } finally {
- tempEvents = undefined;
- }
+ }
+ export function EndTempBatch<T>(success: boolean) {
+ UndoManager.UndoTempBatch(success);
}
//TODO Make this return the return value
export function RunInBatch<T>(fn: () => T, batchName: string) {
@@ -189,10 +210,11 @@ export namespace UndoManager {
batch.end();
}
}
- export const UndoTempBatch = action(() => {
- if (tempEvents) {
+ export const UndoTempBatch = action((success: any) => {
+ if (tempEvents && !success) {
undoing = true;
for (let i = tempEvents.length - 1; i >= 0; i--) {
+ currentBatch?.includes(tempEvents[i]) && currentBatch.splice(currentBatch.indexOf(tempEvents[i]));
tempEvents[i].undo();
}
undoing = false;
@@ -204,6 +226,7 @@ export namespace UndoManager {
return;
}
+ const names = undoStackNames.pop();
const commands = undoStack.pop();
if (!commands) {
return;
@@ -215,6 +238,7 @@ export namespace UndoManager {
}
undoing = false;
+ redoStackNames.push(names ?? '???');
redoStack.push(commands);
});
@@ -223,6 +247,7 @@ export namespace UndoManager {
return;
}
+ const names = redoStackNames.pop();
const commands = redoStack.pop();
if (!commands) {
return;
@@ -234,6 +259,7 @@ export namespace UndoManager {
}
undoing = false;
+ undoStackNames.push(names ?? '???');
undoStack.push(commands);
});
}
diff --git a/src/client/util/reportManager/ReportManager.scss b/src/client/util/reportManager/ReportManager.scss
new file mode 100644
index 000000000..cd6a1d934
--- /dev/null
+++ b/src/client/util/reportManager/ReportManager.scss
@@ -0,0 +1,364 @@
+@import '../../views/global/globalCssVariables';
+
+// header
+
+.report-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ .header-btns {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ }
+
+ h2 {
+ margin: 0;
+ padding: 0;
+ font-size: 24px;
+ }
+}
+
+.report-header-vertical {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 4px;
+
+ h2 {
+ margin: 0;
+ padding: 0;
+ padding-bottom: 8px;
+ font-size: 24px;
+ }
+}
+
+// Report
+
+.report-issue {
+ width: 450px;
+ min-width: 300px;
+ padding: 16px;
+ padding-top: 32px;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ // background-color: #ffffff;
+ text-align: left;
+ position: relative;
+
+ .report-label {
+ font-size: 14px;
+ font-weight: 400;
+ }
+
+ .report-section {
+ display: flex;
+ flex-direction: column;
+ }
+
+ .report-textarea {
+ border: none;
+ outline: none;
+ width: 100%;
+ height: 80px;
+ padding: 8px;
+ resize: vertical;
+ background: transparent;
+ transition: border 0.3s ease;
+ }
+
+ .report-selects {
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+ gap: 16px;
+ background-color: transparent;
+
+ .report-select {
+ padding: 8px;
+ background-color: transparent;
+
+ .report-opt {
+ padding: 8px;
+ }
+ }
+ }
+}
+
+.report-input {
+ border: none;
+ outline: none;
+ border-bottom: 1px solid;
+ padding: 8px;
+ padding-left: 0;
+ transition: all 0.2s ease;
+ background: transparent;
+
+ &:hover {
+ // border-bottom-color: $text-gray;
+ }
+ &:focus {
+ // border-bottom-color: #4476f7;
+ }
+}
+
+// View issues
+
+.view-issues {
+ width: 75vw;
+ min-width: 500px;
+ display: flex;
+ gap: 16px;
+ height: 100%;
+ overflow-x: auto;
+
+ video::-webkit-media-controls {
+ display: flex !important;
+ }
+
+ .left {
+ flex: 1;
+ height: 100%;
+ padding: 16px;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ text-align: left;
+ position: relative;
+
+ .issues {
+ padding-top: 24px;
+ position: relative;
+ flex-grow: 1;
+ overflow-y: auto;
+ overflow-x: hidden;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ }
+ }
+
+ .right {
+ position: relative;
+ flex: 1;
+ padding: 16px;
+ min-width: 300px;
+ height: 100%;
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ }
+}
+
+// Issue
+
+.issue-card {
+ cursor: pointer;
+ padding: 16px;
+ border: 1px solid;
+ transition: all 0.1s ease;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ border-radius: 8px;
+ transition: all 0.2s ease;
+
+ .issue-top {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ padding-bottom: 8px;
+ }
+
+ .issue-label {
+ cursor: pointer;
+ font-size: 14px;
+ font-weight: 400;
+ padding: 0;
+ margin: 0;
+ }
+
+ .issue-title {
+ font-size: 16px;
+ font-weight: 500;
+ padding: 0;
+ margin: 0;
+ }
+}
+
+// Dropzone
+
+.dropzone {
+ padding: 2rem;
+ border-radius: 0.5rem;
+ border: 2px dashed;
+
+ .dropzone-instructions {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 16px;
+
+ p {
+ text-align: center;
+ }
+ }
+}
+
+.file-list {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+ font-size: 14px;
+ width: 100%;
+ overflow-x: auto;
+ list-style-type: none;
+ display: flex;
+ align-items: center;
+ gap: 16px;
+
+ .file-name {
+ padding: 8px 12px;
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ white-space: nowrap;
+ }
+}
+
+// Detailed issue view
+
+.issue-view {
+ height: 100%;
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ text-align: left;
+ position: relative;
+ overflow: auto;
+
+ .issue-label {
+ .issue-link {
+ cursor: pointer;
+ color: #4476f7;
+ }
+ }
+
+ .issue-title {
+ font-size: 24px;
+ margin: 0;
+ padding: 0;
+ }
+
+ .issue-date {
+ font-size: 14px;
+ }
+
+ .issue-content {
+ font-size: 14px;
+ }
+}
+
+// tags flex lists
+
+.issues-filters {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+
+ .issues-filter {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+ white-space: nowrap;
+ overflow-x: auto;
+ }
+}
+
+.issue-tags {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+ white-space: nowrap;
+ overflow-x: auto;
+}
+
+// Media previews
+
+.report-media-wrapper {
+ position: relative;
+ cursor: pointer;
+
+ .close-btn {
+ position: absolute;
+ top: 2px;
+ right: 2px;
+ opacity: 0;
+ }
+
+ .report-media-content {
+ position: relative;
+ display: inline block;
+
+ video::-webkit-media-controls {
+ display: flex !important;
+ }
+ }
+
+ .report-media-content::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(0, 0, 0, 0.5); /* Adjust the opacity as desired */
+ opacity: 0;
+ transition: opacity 0.3s ease; /* Transition for smooth effect */
+ pointer-events: none;
+
+ video::-webkit-media-controls {
+ pointer-events: all;
+ }
+ }
+
+ &:hover {
+ .report-media-content::after {
+ opacity: 1;
+ }
+
+ .close-btn {
+ opacity: 1;
+ }
+ }
+}
+
+.report-audio-wrapper {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+@media (max-width: 1100px) {
+ .report-header {
+ flex-direction: column;
+ align-items: stretch;
+ gap: 2rem;
+ }
+}
+
+// Tag styling
+
+.report-tag {
+ box-sizing: border-box;
+ padding: 4px 10px;
+ font-size: 10px;
+ border-radius: 32px;
+ transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease;
+}
diff --git a/src/client/util/reportManager/ReportManager.tsx b/src/client/util/reportManager/ReportManager.tsx
new file mode 100644
index 000000000..7aad0f2b1
--- /dev/null
+++ b/src/client/util/reportManager/ReportManager.tsx
@@ -0,0 +1,406 @@
+import * as React from 'react';
+import v4 = require('uuid/v4');
+import '.././SettingsManager.scss';
+import './ReportManager.scss';
+import Dropzone from 'react-dropzone';
+import ReactLoading from 'react-loading';
+import { action, observable } from 'mobx';
+import { BsX, BsArrowsAngleExpand, BsArrowsAngleContract } from 'react-icons/bs';
+import { CgClose } from 'react-icons/cg';
+import { AiOutlineUpload } from 'react-icons/ai';
+import { HiOutlineArrowLeft } from 'react-icons/hi';
+import { Issue } from './reportManagerSchema';
+import { observer } from 'mobx-react';
+import { Doc } from '../../../fields/Doc';
+import { MainViewModal } from '../../views/MainViewModal';
+import { Octokit } from '@octokit/core';
+import { Button, Dropdown, DropdownType, IconButton, Type } from 'browndash-components';
+import { BugType, FileData, Priority, ReportForm, ViewState, bugDropdownItems, darkColors, emptyReportForm, formatTitle, getAllIssues, isDarkMode, lightColors, passesTagFilter, priorityDropdownItems, uploadFilesToServer } from './reportManagerUtils';
+import { Filter, FormInput, FormTextArea, IssueCard, IssueView, Tag } from './ReportManagerComponents';
+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.
+ */
+@observer
+export class ReportManager extends React.Component<{}> {
+ public static Instance: ReportManager;
+ @observable private isOpen = false;
+
+ @observable private query = '';
+ @action private setQuery = (q: string) => {
+ this.query = q;
+ };
+
+ private octokit: Octokit;
+
+ @observable viewState: ViewState = ViewState.VIEW;
+ @action private setViewState = (state: ViewState) => {
+ this.viewState = state;
+ };
+ @observable submitting: boolean = false;
+ @action private setSubmitting = (submitting: boolean) => {
+ this.submitting = submitting;
+ };
+
+ @observable fetchingIssues: boolean = false;
+ @action private setFetchingIssues = (fetching: boolean) => {
+ this.fetchingIssues = fetching;
+ };
+
+ @observable
+ public shownIssues: Issue[] = [];
+ @action setShownIssues = action((issues: Issue[]) => {
+ this.shownIssues = issues;
+ });
+
+ @observable
+ public priorityFilter: Priority | null = null;
+ @action setPriorityFilter = action((priority: Priority | null) => {
+ this.priorityFilter = priority;
+ });
+
+ @observable
+ public bugFilter: BugType | null = null;
+ @action setBugFilter = action((bug: BugType | null) => {
+ this.bugFilter = bug;
+ });
+
+ @observable selectedIssue: Issue | undefined = undefined;
+ @action setSelectedIssue = action((issue: Issue | undefined) => {
+ this.selectedIssue = issue;
+ });
+
+ @observable rightExpanded: boolean = false;
+ @action setRightExpanded = action((expanded: boolean) => {
+ this.rightExpanded = expanded;
+ });
+
+ // Form state
+ @observable private formData: ReportForm = emptyReportForm;
+ @action setFormData = action((newData: ReportForm) => {
+ this.formData = newData;
+ });
+
+ public close = action(() => (this.isOpen = false));
+ public open = action(async () => {
+ this.isOpen = true;
+ if (this.shownIssues.length === 0) {
+ this.updateIssues();
+ }
+ });
+
+ @action updateIssues = action(async () => {
+ this.setFetchingIssues(true);
+ try {
+ const issues = (await getAllIssues(this.octokit)) as Issue[];
+ this.setShownIssues(issues.filter(issue => issue.state === 'open' && !issue.pull_request));
+ } catch (err) {
+ console.log(err);
+ }
+ this.setFetchingIssues(false);
+ });
+
+ constructor(props: {}) {
+ super(props);
+ ReportManager.Instance = this;
+
+ // initializing Github connection
+ this.octokit = new Octokit({
+ auth: process.env.GITHUB_ACCESS_TOKEN,
+ });
+ }
+
+ /**
+ * Sends a request to Github to report a new issue with the form data.
+ * @returns nothing
+ */
+ public async reportIssue(): Promise<void> {
+ if (this.formData.title === '' || this.formData.description === '') {
+ alert('Please fill out all required fields to report an issue.');
+ return;
+ }
+ this.setSubmitting(true);
+ let formattedLinks: string[] = [];
+ if (this.formData.mediaFiles.length > 0) {
+ const links = await uploadFilesToServer(this.formData.mediaFiles);
+ if (links) {
+ formattedLinks = links;
+ }
+ }
+
+ const req = await this.octokit.request('POST /repos/{owner}/{repo}/issues', {
+ owner: 'brown-dash',
+ repo: 'Dash-Web',
+ title: formatTitle(this.formData.title, Doc.CurrentUserEmail),
+ body: `${this.formData.description} ${formattedLinks.length > 0 ? `\n\nFiles:\n${formattedLinks.join('\n')}` : ''}`,
+ labels: ['from-dash-app', this.formData.type, this.formData.priority],
+ });
+
+ // 201 status means success
+ if (req.status !== 201) {
+ alert('Error creating issue on github.');
+ } else {
+ await this.updateIssues();
+ alert('Successfully submitted issue.');
+ }
+ this.setFormData(emptyReportForm);
+ this.setSubmitting(false);
+ }
+
+ /**
+ * Handles file upload.
+ *
+ * @param files uploaded files
+ */
+ private onDrop = (files: File[]) => {
+ this.setFormData({ ...this.formData, mediaFiles: [...this.formData.mediaFiles, ...files.map(file => ({ _id: v4(), file }))] });
+ };
+
+ /**
+ * Gets a JSX element to render a media preview
+ * @param fileData file data
+ * @returns JSX element of a piece of media (image, video, audio)
+ */
+ private getMediaPreview = (fileData: FileData): JSX.Element => {
+ const file = fileData.file;
+ const mimeType = file.type;
+ const preview = URL.createObjectURL(file);
+
+ if (mimeType.startsWith('image/')) {
+ return (
+ <div key={fileData._id} className="report-media-wrapper">
+ <div className="report-media-content">
+ <img height={100} alt={`Preview of ${file.name}`} src={preview} style={{ display: 'block' }} />
+ </div>
+ <div className="close-btn">
+ <IconButton icon={<BsX color="#ffffff" />} onClick={() => this.setFormData({ ...this.formData, mediaFiles: this.formData.mediaFiles.filter(f => f._id !== fileData._id) })} />
+ </div>
+ </div>
+ );
+ } else if (mimeType.startsWith('video/')) {
+ return (
+ <div key={fileData._id} className="report-media-wrapper">
+ <div className="report-media-content">
+ <video className="report-default-video" controls style={{ height: '100px', width: 'auto', display: 'block' }}>
+ <source src={preview} type="video/mp4" />
+ Your browser does not support the video tag.
+ </video>
+ </div>
+ <div className="close-btn">
+ <IconButton icon={<BsX color="#ffffff" />} onClick={() => this.setFormData({ ...this.formData, mediaFiles: this.formData.mediaFiles.filter(f => f._id !== fileData._id) })} />
+ </div>
+ </div>
+ );
+ } else if (mimeType.startsWith('audio/')) {
+ return (
+ <div key={fileData._id} className="report-audio-wrapper">
+ <audio src={preview} controls />
+ <div className="close-btn">
+ <IconButton icon={<BsX color="#ffffff" />} onClick={() => this.setFormData({ ...this.formData, mediaFiles: this.formData.mediaFiles.filter(f => f._id !== fileData._id) })} />
+ </div>
+ </div>
+ );
+ }
+ return <></>;
+ };
+
+ /**
+ * @returns the component that dispays all issues
+ */
+ private viewIssuesComponent = () => {
+ const darkMode = isDarkMode(SettingsManager.Instance.userBackgroundColor);
+ const colors = darkMode ? darkColors : lightColors;
+
+ return (
+ <div className="view-issues" style={{ backgroundColor: SettingsManager.Instance.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>
+ <div className="header-btns">
+ <IconButton color={StrCast(Doc.UserDoc().userColor)} tooltip="refresh" icon={<MdRefresh size="16px" />} onClick={this.updateIssues} />
+ <Button
+ type={Type.TERT}
+ color={StrCast(Doc.UserDoc().userVariantColor)}
+ text="Report Issue"
+ onClick={() => {
+ this.setViewState(ViewState.CREATE);
+ }}
+ />
+ </div>
+ </div>
+ <FormInput value={this.query} placeholder="Filter by query..." onChange={this.setQuery} />
+ <div className="issues-filters">
+ <Filter items={Object.values(Priority)} activeValue={this.priorityFilter} setActiveValue={p => this.setPriorityFilter(p)} />
+ <Filter items={Object.values(BugType)} activeValue={this.bugFilter} setActiveValue={b => this.setBugFilter(b)} />
+ </div>
+ <div className="issues">
+ {this.fetchingIssues ? (
+ <div style={{ flexGrow: 1, display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
+ <ReactLoading type="spin" color={StrCast(Doc.UserDoc().userColor)} width={50} height={50} />
+ </div>
+ ) : (
+ this.shownIssues
+ .filter(issue => issue.title.toLowerCase().includes(this.query))
+ .filter(issue => passesTagFilter(issue, this.priorityFilter, this.bugFilter))
+ .map(issue => (
+ <IssueCard
+ key={issue.number}
+ issue={issue}
+ onSelect={() => {
+ this.setSelectedIssue(issue);
+ }}
+ />
+ ))
+ )}
+ </div>
+ </div>
+ <div className="right">{this.selectedIssue ? <IssueView key={this.selectedIssue.number} issue={this.selectedIssue} /> : <div>No issue selected</div>} </div>
+ <div style={{ position: 'absolute', top: '8px', right: '8px', display: 'flex', gap: '16px' }}>
+ <IconButton
+ color={StrCast(Doc.UserDoc().userColor)}
+ tooltip={this.rightExpanded ? 'Minimize right side' : 'Expand right side'}
+ icon={this.rightExpanded ? <BsArrowsAngleContract size="16px" /> : <BsArrowsAngleExpand size="16px" />}
+ onClick={e => {
+ e.stopPropagation();
+ this.setRightExpanded(!this.rightExpanded);
+ }}
+ />
+ <IconButton color={StrCast(Doc.UserDoc().userColor)} tooltip="close" icon={<CgClose size="16px" />} onClick={this.close} />
+ </div>
+ </div>
+ );
+ };
+
+ /**
+ * @returns the form component for submitting issues
+ */
+ private reportIssueComponent = () => {
+ const darkMode = isDarkMode(StrCast(Doc.UserDoc().userBackgroundColor));
+ const colors = darkMode ? darkColors : lightColors;
+
+ return (
+ <div className="report-issue" style={{ color: colors.text }}>
+ <div className="report-header-vertical">
+ <Button
+ type={Type.PRIM}
+ color={StrCast(Doc.UserDoc().userColor)}
+ text="back to view"
+ icon={<HiOutlineArrowLeft />}
+ iconPlacement="left"
+ onClick={() => {
+ this.setViewState(ViewState.VIEW);
+ }}
+ />
+ <h2>Report an Issue</h2>
+ </div>
+ <div className="report-section">
+ <label className="report-label">Please provide a title for the bug</label>
+ <FormInput value={this.formData.title} placeholder="Title..." onChange={val => this.setFormData({ ...this.formData, title: val })} />
+ </div>
+ <div className="report-section">
+ <label className="report-label">Please leave a description for the bug and how it can be recreated</label>
+ <FormTextArea value={this.formData.description} placeholder="Description..." onChange={val => this.setFormData({ ...this.formData, description: val })} />
+ </div>
+ <div className="report-selects">
+ <Dropdown
+ color={StrCast(Doc.UserDoc().userColor)}
+ formLabel={'Type'}
+ items={bugDropdownItems}
+ selectedVal={this.formData.type}
+ setSelectedVal={val => {
+ if (typeof val === 'string') this.setFormData({ ...this.formData, type: val as BugType });
+ }}
+ dropdownType={DropdownType.SELECT}
+ type={Type.TERT}
+ fillWidth
+ />
+ <Dropdown
+ color={StrCast(Doc.UserDoc().userColor)}
+ formLabel={'Priority'}
+ items={priorityDropdownItems}
+ selectedVal={this.formData.priority}
+ setSelectedVal={val => {
+ if (typeof val === 'string') this.setFormData({ ...this.formData, priority: val as Priority });
+ }}
+ dropdownType={DropdownType.SELECT}
+ type={Type.TERT}
+ fillWidth
+ />
+ </div>
+ <Dropzone
+ onDrop={this.onDrop}
+ accept={{
+ 'image/*': ['.png', '.jpg', '.jpeg', '.gif'],
+ 'video/*': ['.mp4', '.mpeg', '.webm', '.mov'],
+ 'audio/mpeg': ['.mp3'],
+ 'audio/wav': ['.wav'],
+ 'audio/ogg': ['.ogg'],
+ }}>
+ {({ getRootProps, getInputProps }) => (
+ <div {...getRootProps({ className: 'dropzone' })} style={{ borderColor: isDarkMode(StrCast(Doc.UserDoc().userBackgroundColor)) ? darkColors.border : lightColors.border }}>
+ <input {...getInputProps()} />
+ <div className="dropzone-instructions">
+ <AiOutlineUpload size={25} />
+ <p>Drop or select media that shows the bug (optional)</p>
+ </div>
+ </div>
+ )}
+ </Dropzone>
+ {this.formData.mediaFiles.length > 0 && <ul className="file-list">{this.formData.mediaFiles.map(file => this.getMediaPreview(file))}</ul>}
+ {this.submitting ? (
+ <Button
+ text="Submit"
+ type={Type.TERT}
+ color={StrCast(Doc.UserDoc().userVariantColor)}
+ icon={<ReactLoading type="spin" color={'#ffffff'} width={20} height={20} />}
+ iconPlacement="right"
+ onClick={() => {
+ this.reportIssue();
+ }}
+ />
+ ) : (
+ <Button
+ text="Submit"
+ type={Type.TERT}
+ color={StrCast(Doc.UserDoc().userVariantColor)}
+ onClick={() => {
+ this.reportIssue();
+ }}
+ />
+ )}
+ <div style={{ position: 'absolute', top: '4px', right: '4px' }}>
+ <IconButton color={StrCast(Doc.UserDoc().userColor)} tooltip="close" icon={<CgClose size={'16px'} />} onClick={this.close} />
+ </div>
+ </div>
+ );
+ };
+
+ /**
+ * @returns the component rendered to the modal
+ */
+ private reportComponent = () => {
+ if (this.viewState === ViewState.VIEW) {
+ return this.viewIssuesComponent();
+ } else {
+ return this.reportIssueComponent();
+ }
+ };
+
+ render() {
+ return (
+ <MainViewModal
+ contents={this.reportComponent()}
+ isDisplayed={this.isOpen}
+ interactive={true}
+ closeOnExternalClick={this.close}
+ dialogueBoxStyle={{ width: 'auto', minWidth: '300px', height: '85vh', maxHeight: '90vh', background: StrCast(Doc.UserDoc().userBackgroundColor), borderRadius: '8px' }}
+ />
+ );
+ }
+}
diff --git a/src/client/util/reportManager/ReportManagerComponents.tsx b/src/client/util/reportManager/ReportManagerComponents.tsx
new file mode 100644
index 000000000..e870c073d
--- /dev/null
+++ b/src/client/util/reportManager/ReportManagerComponents.tsx
@@ -0,0 +1,381 @@
+import * as React from 'react';
+import { Issue } from './reportManagerSchema';
+import { darkColors, dashBlue, getLabelColors, isDarkMode, lightColors } from './reportManagerUtils';
+import ReactMarkdown from 'react-markdown';
+import rehypeRaw from 'rehype-raw';
+import remarkGfm from 'remark-gfm';
+import { StrCast } from '../../../fields/Types';
+import { Doc } from '../../../fields/Doc';
+
+/**
+ * Mini helper components for the report component.
+ */
+
+interface FilterProps<T> {
+ items: T[];
+ activeValue: T | null;
+ setActiveValue: (val: T | null) => void;
+}
+
+// filter ui for issues (horizontal list of tags)
+export const Filter = <T extends string>({ items, activeValue, setActiveValue }: FilterProps<T>) => {
+ // establishing theme
+ const darkMode = isDarkMode(StrCast(Doc.UserDoc().userBackgroundColor));
+ const colors = darkMode ? darkColors : lightColors;
+ const isTagDarkMode = isDarkMode(StrCast(Doc.UserDoc().userColor));
+ const activeTagTextColor = isTagDarkMode ? darkColors.text : lightColors.text;
+
+ return (
+ <div className="issues-filter">
+ <Tag
+ text={'All'}
+ onClick={() => {
+ setActiveValue(null);
+ }}
+ fontSize="12px"
+ backgroundColor={activeValue === null ? StrCast(Doc.UserDoc().userColor) : 'transparent'}
+ color={activeValue === null ? activeTagTextColor : colors.textGrey}
+ borderColor={activeValue === null ? StrCast(Doc.UserDoc().userColor) : colors.border}
+ border
+ />
+ {items.map(item => {
+ return (
+ <Tag
+ key={item}
+ text={item}
+ onClick={() => {
+ setActiveValue(item);
+ }}
+ fontSize="12px"
+ backgroundColor={activeValue === item ? StrCast(Doc.UserDoc().userColor) : 'transparent'}
+ color={activeValue === item ? activeTagTextColor : colors.textGrey}
+ border
+ borderColor={activeValue === item ? StrCast(Doc.UserDoc().userColor) : colors.border}
+ />
+ );
+ })}
+ </div>
+ );
+};
+
+interface IssueCardProps {
+ issue: Issue;
+ onSelect: () => void;
+}
+
+// Component for the issue cards list on the left
+export const IssueCard = ({ issue, onSelect }: IssueCardProps) => {
+ const [textColor, setTextColor] = React.useState('');
+ const [bgColor, setBgColor] = React.useState('transparent');
+ const [borderColor, setBorderColor] = React.useState('transparent');
+
+ const resetColors = () => {
+ const darkMode = isDarkMode(StrCast(Doc.UserDoc().userBackgroundColor));
+ const colors = darkMode ? darkColors : lightColors;
+ setTextColor(colors.text);
+ setBorderColor(colors.border);
+ setBgColor('transparent');
+ };
+
+ const handlePointerOver = () => {
+ const darkMode = isDarkMode(StrCast(Doc.UserDoc().userColor));
+ setTextColor(darkMode ? darkColors.text : lightColors.text);
+ setBorderColor(StrCast(Doc.UserDoc().userColor));
+ setBgColor(StrCast(Doc.UserDoc().userColor));
+ };
+
+ React.useEffect(() => {
+ resetColors();
+ }, []);
+
+ return (
+ <div className="issue-card" onClick={onSelect} style={{ color: textColor, backgroundColor: bgColor, borderColor: borderColor }} onPointerOver={handlePointerOver} onPointerOut={resetColors}>
+ <div className="issue-top">
+ <label className="issue-label">#{issue.number}</label>
+ <div className="issue-tags">
+ {issue.labels.map(label => {
+ const labelString = typeof label === 'string' ? label : label.name ?? '';
+ const colors = getLabelColors(labelString);
+ return <Tag key={labelString} text={labelString} backgroundColor={colors[0]} color={colors[1]} />;
+ })}
+ </div>
+ </div>
+ <h3 className="issue-title">{issue.title}</h3>
+ </div>
+ );
+};
+
+interface IssueViewProps {
+ issue: Issue;
+}
+
+// Detailed issue view that displays on the right
+export const IssueView = ({ issue }: IssueViewProps) => {
+ const [issueBody, setIssueBody] = React.useState('');
+
+ // Parses the issue body into a formatted markdown (main functionality is replacing urls with tags)
+ const parseBody = async (body: string) => {
+ const imgTagRegex = /<img\b[^>]*\/?>/;
+ const videoTagRegex = /<video\b[^>]*\/?>/;
+ const audioTagRegex = /<audio\b[^>]*\/?>/;
+
+ const fileRegex = /https:\/\/browndash\.com\/files/;
+ const localRegex = /http:\/\/localhost:1050\/files/;
+ const parts = body.split('\n');
+
+ const modifiedParts = await Promise.all(
+ parts.map(async part => {
+ if (imgTagRegex.test(part) || videoTagRegex.test(part) || audioTagRegex.test(part)) {
+ return `\n${await parseFileTag(part)}\n`;
+ } else if (fileRegex.test(part)) {
+ const tag = await parseDashFiles(part);
+ return tag;
+ } else if (localRegex.test(part)) {
+ const tag = await parseLocalFiles(part);
+ return tag;
+ } else {
+ return part;
+ }
+ })
+ );
+
+ setIssueBody(modifiedParts.join('\n'));
+ };
+
+ // Extracts the src from an image tag and either returns the raw url if not accessible or a new image tag
+ const parseFileTag = async (tag: string): Promise<string> => {
+ const regex = /src="([^"]+)"/;
+ let url = '';
+ const match = tag.match(regex);
+ if (!match) return tag;
+ url = match[1];
+ if (!url) return tag;
+
+ const mimeType = url.split('.').pop();
+ if (!mimeType) return tag;
+
+ switch (mimeType) {
+ // image
+ case '.jpg':
+ case '.png':
+ case '.jpeg':
+ case '.gif':
+ return await getDisplayedFile(url, 'image');
+ // video
+ case '.mp4':
+ case '.mpeg':
+ case '.webm':
+ case '.mov':
+ return await getDisplayedFile(url, 'video');
+ //audio
+ case '.mp3':
+ case '.wav':
+ case '.ogg':
+ return await getDisplayedFile(url, 'audio');
+ }
+ return tag;
+ };
+
+ // Returns the corresponding HTML tag for a src url
+ const parseDashFiles = async (url: string) => {
+ const dashImgRegex = /https:\/\/browndash\.com\/files[/\\]images/;
+ const dashVideoRegex = /https:\/\/browndash\.com\/files[/\\]videos/;
+ const dashAudioRegex = /https:\/\/browndash\.com\/files[/\\]audio/;
+
+ if (dashImgRegex.test(url)) {
+ return await getDisplayedFile(url, 'image');
+ } else if (dashVideoRegex.test(url)) {
+ return await getDisplayedFile(url, 'video');
+ } else if (dashAudioRegex.test(url)) {
+ return await getDisplayedFile(url, 'audio');
+ } else {
+ return url;
+ }
+ };
+
+ // Returns the corresponding HTML tag for a src url
+ const parseLocalFiles = async (url: string) => {
+ const imgRegex = /http:\/\/localhost:1050\/files[/\\]images/;
+ const dashVideoRegex = /http:\/\/localhost:1050\.com\/files[/\\]videos/;
+ const dashAudioRegex = /http:\/\/localhost:1050\.com\/files[/\\]audio/;
+
+ if (imgRegex.test(url)) {
+ return await getDisplayedFile(url, 'image');
+ } else if (dashVideoRegex.test(url)) {
+ return await getDisplayedFile(url, 'video');
+ } else if (dashAudioRegex.test(url)) {
+ return await getDisplayedFile(url, 'audio');
+ } else {
+ return url;
+ }
+ };
+
+ const getDisplayedFile = async (url: string, fileType: 'image' | 'video' | 'audio'): Promise<string> => {
+ switch (fileType) {
+ case 'image':
+ const imgValid = await isImgValid(url);
+ if (!imgValid) return `\n${url} (This image could not be loaded)\n`;
+ return `\n${url}\n<img width="100%" alt="Issue asset" src=${url} />\n`;
+ case 'video':
+ const videoValid = await isVideoValid(url);
+ if (!videoValid) return `\n${url} (This video could not be loaded)\n`;
+ return `\n${url}\n<video class="report-default-video" width="100%" controls alt="Issue asset" src=${url} />\n`;
+ case 'audio':
+ const audioValid = await isAudioValid(url);
+ if (!audioValid) return `\n${url} (This audio could not be loaded)\n`;
+ return `\n${url}\n<audio src=${url} controls />\n`;
+ }
+ };
+
+ // Loads an image and returns a promise that resolves as whether the image is valid or not
+ const isImgValid = (src: string): Promise<boolean> => {
+ const imgElement = document.createElement('img');
+ const validPromise: Promise<boolean> = new Promise(resolve => {
+ imgElement.addEventListener('load', () => resolve(true));
+ imgElement.addEventListener('error', () => resolve(false));
+ });
+ imgElement.src = src;
+ return validPromise;
+ };
+
+ // Loads a video and returns a promise that resolves as whether the video is valid or not
+ const isVideoValid = (src: string): Promise<boolean> => {
+ const videoElement = document.createElement('video');
+ const validPromise: Promise<boolean> = new Promise(resolve => {
+ videoElement.addEventListener('loadeddata', () => resolve(true));
+ videoElement.addEventListener('error', () => resolve(false));
+ });
+ videoElement.src = src;
+ return validPromise;
+ };
+
+ // Loads audio and returns a promise that resolves as whether the audio is valid or not
+ const isAudioValid = (src: string): Promise<boolean> => {
+ const audioElement = document.createElement('audio');
+ const validPromise: Promise<boolean> = new Promise(resolve => {
+ audioElement.addEventListener('loadeddata', () => resolve(true));
+ audioElement.addEventListener('error', () => resolve(false));
+ });
+ audioElement.src = src;
+ return validPromise;
+ };
+
+ // Called on mount to parse the body
+ React.useEffect(() => {
+ setIssueBody('Loading...');
+ parseBody((issue.body as string) ?? '');
+ }, [issue]);
+
+ return (
+ <div className="issue-view">
+ <span className="issue-label">
+ Issue{' '}
+ <a className="issue-link" href={issue.html_url} target="_blank">
+ #{issue.number}
+ </a>
+ </span>
+ <h2 className="issue-title">{issue.title}</h2>
+ <div className="issue-date">
+ Opened on {new Date(issue.created_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })} {issue.user?.login && `by ${issue.user?.login}`}
+ </div>
+ {issue.labels.length > 0 && (
+ <div>
+ <div className="issue-tags">
+ {issue.labels.map(label => {
+ const labelString = typeof label === 'string' ? label : label.name ?? '';
+ const colors = getLabelColors(labelString);
+ return <Tag key={labelString} text={labelString} backgroundColor={colors[0]} color={colors[1]} fontSize="12px" />;
+ })}
+ </div>
+ </div>
+ )}
+ <ReactMarkdown children={issueBody} className="issue-content" linkTarget={'_blank'} remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]} />
+ </div>
+ );
+};
+
+interface TagProps {
+ text: string;
+ fontSize?: string;
+ color?: string;
+ backgroundColor?: string;
+ borderColor?: string;
+ border?: boolean;
+ onClick?: () => void;
+}
+
+// Small tag for labels of the issue
+export const Tag = ({ text, color, backgroundColor, fontSize, border, borderColor, onClick }: TagProps) => {
+ return (
+ <div
+ onClick={onClick ?? (() => {})}
+ className="report-tag"
+ style={{ color: color ?? '#ffffff', backgroundColor: backgroundColor ?? '#347bff', cursor: onClick ? 'pointer' : 'auto', fontSize: fontSize ?? '10px', border: border ? '1px solid' : 'none', borderColor: borderColor ?? '#94a3b8' }}>
+ {text}
+ </div>
+ );
+};
+
+interface FormInputProps {
+ value: string;
+ placeholder: string;
+ onChange: (val: string) => void;
+}
+export const FormInput = ({ value, placeholder, onChange }: FormInputProps) => {
+ const [inputBorderColor, setInputBorderColor] = React.useState('');
+
+ return (
+ <input
+ className="report-input"
+ style={{ borderBottom: `1px solid ${inputBorderColor}` }}
+ value={value}
+ type="text"
+ placeholder={placeholder}
+ onChange={e => onChange(e.target.value)}
+ required
+ onPointerOver={() => {
+ if (inputBorderColor === dashBlue) return;
+ setInputBorderColor(isDarkMode(StrCast(Doc.UserDoc().userBackgroundColor)) ? darkColors.textGrey : lightColors.textGrey);
+ }}
+ onPointerOut={() => {
+ if (inputBorderColor === dashBlue) return;
+ setInputBorderColor(isDarkMode(StrCast(Doc.UserDoc().userBackgroundColor)) ? darkColors.border : lightColors.border);
+ }}
+ onFocus={() => {
+ setInputBorderColor(dashBlue);
+ }}
+ onBlur={() => {
+ setInputBorderColor(isDarkMode(StrCast(Doc.UserDoc().userBackgroundColor)) ? darkColors.border : lightColors.border);
+ }}
+ />
+ );
+};
+
+export const FormTextArea = ({ value, placeholder, onChange }: FormInputProps) => {
+ const [textAreaBorderColor, setTextAreaBorderColor] = React.useState('');
+
+ return (
+ <textarea
+ className="report-textarea"
+ value={value}
+ placeholder={placeholder}
+ onChange={e => onChange(e.target.value)}
+ required
+ style={{ border: `1px solid ${textAreaBorderColor}` }}
+ onPointerOver={() => {
+ if (textAreaBorderColor === dashBlue) return;
+ setTextAreaBorderColor(isDarkMode(StrCast(Doc.UserDoc().userBackgroundColor)) ? darkColors.textGrey : lightColors.textGrey);
+ }}
+ onPointerOut={() => {
+ if (textAreaBorderColor === dashBlue) return;
+ setTextAreaBorderColor(isDarkMode(StrCast(Doc.UserDoc().userBackgroundColor)) ? darkColors.border : lightColors.border);
+ }}
+ onFocus={() => {
+ setTextAreaBorderColor(dashBlue);
+ }}
+ onBlur={() => {
+ setTextAreaBorderColor(isDarkMode(StrCast(Doc.UserDoc().userBackgroundColor)) ? darkColors.border : lightColors.border);
+ }}
+ />
+ );
+};
diff --git a/src/client/util/reportManager/reportManagerSchema.ts b/src/client/util/reportManager/reportManagerSchema.ts
new file mode 100644
index 000000000..9a1c7c3e9
--- /dev/null
+++ b/src/client/util/reportManager/reportManagerSchema.ts
@@ -0,0 +1,877 @@
+/**
+ * Issue interface schema from Github.
+ */
+export interface Issue {
+ active_lock_reason?: null | string;
+ assignee: null | PurpleSimpleUser;
+ assignees?: AssigneeElement[] | null;
+ /**
+ * How the author is associated with the repository.
+ */
+ author_association: AuthorAssociation;
+ /**
+ * Contents of the issue
+ */
+ body?: null | string;
+ body_html?: string;
+ body_text?: string;
+ closed_at: Date | null;
+ closed_by?: null | FluffySimpleUser;
+ comments: number;
+ comments_url: string;
+ created_at: Date;
+ draft?: boolean;
+ events_url: string;
+ html_url: string;
+ id: number;
+ /**
+ * Labels to associate with this issue; pass one or more label names to replace the set of
+ * labels on this issue; send an empty array to clear all labels from the issue; note that
+ * the labels are silently dropped for users without push access to the repository
+ */
+ labels: Array<LabelObject | string>;
+ labels_url: string;
+ locked: boolean;
+ milestone: null | Milestone;
+ node_id: string;
+ /**
+ * Number uniquely identifying the issue within its repository
+ */
+ number: number;
+ performed_via_github_app?: null | GitHubApp;
+ pull_request?: PullRequest;
+ reactions?: ReactionRollup;
+ /**
+ * A repository on GitHub.
+ */
+ repository?: Repository;
+ repository_url: string;
+ /**
+ * State of the issue; either 'open' or 'closed'
+ */
+ state: string;
+ /**
+ * The reason for the current state
+ */
+ state_reason?: StateReason | null;
+ timeline_url?: string;
+ /**
+ * Title of the issue
+ */
+ title: string;
+ updated_at: Date;
+ /**
+ * URL for the issue
+ */
+ url: string;
+ user: null | TentacledSimpleUser;
+ [property: string]: any;
+}
+
+/**
+ * A GitHub user.
+ */
+export interface PurpleSimpleUser {
+ avatar_url: string;
+ email?: null | string;
+ events_url: string;
+ followers_url: string;
+ following_url: string;
+ gists_url: string;
+ gravatar_id: null | string;
+ html_url: string;
+ id: number;
+ login: string;
+ name?: null | string;
+ node_id: string;
+ organizations_url: string;
+ received_events_url: string;
+ repos_url: string;
+ site_admin: boolean;
+ starred_at?: string;
+ starred_url: string;
+ subscriptions_url: string;
+ type: string;
+ url: string;
+ [property: string]: any;
+}
+
+/**
+ * A GitHub user.
+ */
+export interface AssigneeElement {
+ avatar_url: string;
+ email?: null | string;
+ events_url: string;
+ followers_url: string;
+ following_url: string;
+ gists_url: string;
+ gravatar_id: null | string;
+ html_url: string;
+ id: number;
+ login: string;
+ name?: null | string;
+ node_id: string;
+ organizations_url: string;
+ received_events_url: string;
+ repos_url: string;
+ site_admin: boolean;
+ starred_at?: string;
+ starred_url: string;
+ subscriptions_url: string;
+ type: string;
+ url: string;
+ [property: string]: any;
+}
+
+/**
+ * How the author is associated with the repository.
+ */
+export enum AuthorAssociation {
+ Collaborator = 'COLLABORATOR',
+ Contributor = 'CONTRIBUTOR',
+ FirstTimeContributor = 'FIRST_TIME_CONTRIBUTOR',
+ FirstTimer = 'FIRST_TIMER',
+ Mannequin = 'MANNEQUIN',
+ Member = 'MEMBER',
+ None = 'NONE',
+ Owner = 'OWNER',
+}
+
+/**
+ * A GitHub user.
+ */
+export interface FluffySimpleUser {
+ avatar_url: string;
+ email?: null | string;
+ events_url: string;
+ followers_url: string;
+ following_url: string;
+ gists_url: string;
+ gravatar_id: null | string;
+ html_url: string;
+ id: number;
+ login: string;
+ name?: null | string;
+ node_id: string;
+ organizations_url: string;
+ received_events_url: string;
+ repos_url: string;
+ site_admin: boolean;
+ starred_at?: string;
+ starred_url: string;
+ subscriptions_url: string;
+ type: string;
+ url: string;
+ [property: string]: any;
+}
+
+export interface LabelObject {
+ color?: null | string;
+ default?: boolean;
+ description?: null | string;
+ id?: number;
+ name?: string;
+ node_id?: string;
+ url?: string;
+ [property: string]: any;
+}
+
+/**
+ * A collection of related issues and pull requests.
+ */
+export interface Milestone {
+ closed_at: Date | null;
+ closed_issues: number;
+ created_at: Date;
+ creator: null | MilestoneSimpleUser;
+ description: null | string;
+ due_on: Date | null;
+ html_url: string;
+ id: number;
+ labels_url: string;
+ node_id: string;
+ /**
+ * The number of the milestone.
+ */
+ number: number;
+ open_issues: number;
+ /**
+ * The state of the milestone.
+ */
+ state: State;
+ /**
+ * The title of the milestone.
+ */
+ title: string;
+ updated_at: Date;
+ url: string;
+ [property: string]: any;
+}
+
+/**
+ * A GitHub user.
+ */
+export interface MilestoneSimpleUser {
+ avatar_url: string;
+ email?: null | string;
+ events_url: string;
+ followers_url: string;
+ following_url: string;
+ gists_url: string;
+ gravatar_id: null | string;
+ html_url: string;
+ id: number;
+ login: string;
+ name?: null | string;
+ node_id: string;
+ organizations_url: string;
+ received_events_url: string;
+ repos_url: string;
+ site_admin: boolean;
+ starred_at?: string;
+ starred_url: string;
+ subscriptions_url: string;
+ type: string;
+ url: string;
+ [property: string]: any;
+}
+
+/**
+ * The state of the milestone.
+ */
+export enum State {
+ Closed = 'closed',
+ Open = 'open',
+}
+
+/**
+ * GitHub apps are a new way to extend GitHub. They can be installed directly on
+ * organizations and user accounts and granted access to specific repositories. They come
+ * with granular permissions and built-in webhooks. GitHub apps are first class actors
+ * within GitHub.
+ */
+export interface GitHubApp {
+ client_id?: string;
+ client_secret?: string;
+ created_at: Date;
+ description: null | string;
+ /**
+ * The list of events for the GitHub app
+ */
+ events: string[];
+ external_url: string;
+ html_url: string;
+ /**
+ * Unique identifier of the GitHub app
+ */
+ id: number;
+ /**
+ * The number of installations associated with the GitHub app
+ */
+ installations_count?: number;
+ /**
+ * The name of the GitHub app
+ */
+ name: string;
+ node_id: string;
+ owner: null | GitHubAppSimpleUser;
+ pem?: string;
+ /**
+ * The set of permissions for the GitHub app
+ */
+ permissions: GitHubAppPermissions;
+ /**
+ * The slug name of the GitHub app
+ */
+ slug?: string;
+ updated_at: Date;
+ webhook_secret?: null | string;
+ [property: string]: any;
+}
+
+/**
+ * A GitHub user.
+ */
+export interface GitHubAppSimpleUser {
+ avatar_url: string;
+ email?: null | string;
+ events_url: string;
+ followers_url: string;
+ following_url: string;
+ gists_url: string;
+ gravatar_id: null | string;
+ html_url: string;
+ id: number;
+ login: string;
+ name?: null | string;
+ node_id: string;
+ organizations_url: string;
+ received_events_url: string;
+ repos_url: string;
+ site_admin: boolean;
+ starred_at?: string;
+ starred_url: string;
+ subscriptions_url: string;
+ type: string;
+ url: string;
+ [property: string]: any;
+}
+
+/**
+ * The set of permissions for the GitHub app
+ */
+export interface GitHubAppPermissions {
+ checks?: string;
+ contents?: string;
+ deployments?: string;
+ issues?: string;
+ metadata?: string;
+}
+
+export interface PullRequest {
+ diff_url: null | string;
+ html_url: null | string;
+ merged_at?: Date | null;
+ patch_url: null | string;
+ url: null | string;
+ [property: string]: any;
+}
+
+export interface ReactionRollup {
+ '+1': number;
+ '-1': number;
+ confused: number;
+ eyes: number;
+ heart: number;
+ hooray: number;
+ laugh: number;
+ rocket: number;
+ total_count: number;
+ url: string;
+ [property: string]: any;
+}
+
+/**
+ * A repository on GitHub.
+ */
+export interface Repository {
+ /**
+ * Whether to allow Auto-merge to be used on pull requests.
+ */
+ allow_auto_merge?: boolean;
+ /**
+ * Whether to allow forking this repo
+ */
+ allow_forking?: boolean;
+ /**
+ * Whether to allow merge commits for pull requests.
+ */
+ allow_merge_commit?: boolean;
+ /**
+ * Whether to allow rebase merges for pull requests.
+ */
+ allow_rebase_merge?: boolean;
+ /**
+ * Whether to allow squash merges for pull requests.
+ */
+ allow_squash_merge?: boolean;
+ /**
+ * Whether or not a pull request head branch that is behind its base branch can always be
+ * updated even if it is not required to be up to date before merging.
+ */
+ allow_update_branch?: boolean;
+ /**
+ * Whether anonymous git access is enabled for this repository
+ */
+ anonymous_access_enabled?: boolean;
+ archive_url: string;
+ /**
+ * Whether the repository is archived.
+ */
+ archived: boolean;
+ assignees_url: string;
+ blobs_url: string;
+ branches_url: string;
+ clone_url: string;
+ collaborators_url: string;
+ comments_url: string;
+ commits_url: string;
+ compare_url: string;
+ contents_url: string;
+ contributors_url: string;
+ created_at: Date | null;
+ /**
+ * The default branch of the repository.
+ */
+ default_branch: string;
+ /**
+ * Whether to delete head branches when pull requests are merged
+ */
+ delete_branch_on_merge?: boolean;
+ deployments_url: string;
+ description: null | string;
+ /**
+ * Returns whether or not this repository disabled.
+ */
+ disabled: boolean;
+ downloads_url: string;
+ events_url: string;
+ fork: boolean;
+ forks: number;
+ forks_count: number;
+ forks_url: string;
+ full_name: string;
+ git_commits_url: string;
+ git_refs_url: string;
+ git_tags_url: string;
+ git_url: string;
+ /**
+ * Whether discussions are enabled.
+ */
+ has_discussions?: boolean;
+ /**
+ * Whether downloads are enabled.
+ */
+ has_downloads: boolean;
+ /**
+ * Whether issues are enabled.
+ */
+ has_issues: boolean;
+ has_pages: boolean;
+ /**
+ * Whether projects are enabled.
+ */
+ has_projects: boolean;
+ /**
+ * Whether the wiki is enabled.
+ */
+ has_wiki: boolean;
+ homepage: null | string;
+ hooks_url: string;
+ html_url: string;
+ /**
+ * Unique identifier of the repository
+ */
+ id: number;
+ /**
+ * Whether this repository acts as a template that can be used to generate new repositories.
+ */
+ is_template?: boolean;
+ issue_comment_url: string;
+ issue_events_url: string;
+ issues_url: string;
+ keys_url: string;
+ labels_url: string;
+ language: null | string;
+ languages_url: string;
+ license: null | LicenseSimple;
+ master_branch?: string;
+ /**
+ * The default value for a merge commit message.
+ *
+ * - `PR_TITLE` - default to the pull request's title.
+ * - `PR_BODY` - default to the pull request's body.
+ * - `BLANK` - default to a blank commit message.
+ */
+ merge_commit_message?: MergeCommitMessage;
+ /**
+ * The default value for a merge commit title.
+ *
+ * - `PR_TITLE` - default to the pull request's title.
+ * - `MERGE_MESSAGE` - default to the classic title for a merge message (e.g., Merge pull
+ * request #123 from branch-name).
+ */
+ merge_commit_title?: MergeCommitTitle;
+ merges_url: string;
+ milestones_url: string;
+ mirror_url: null | string;
+ /**
+ * The name of the repository.
+ */
+ name: string;
+ network_count?: number;
+ node_id: string;
+ notifications_url: string;
+ open_issues: number;
+ open_issues_count: number;
+ organization?: null | RepositorySimpleUser;
+ /**
+ * A GitHub user.
+ */
+ owner: OwnerObject;
+ permissions?: RepositoryPermissions;
+ /**
+ * Whether the repository is private or public.
+ */
+ private: boolean;
+ pulls_url: string;
+ pushed_at: Date | null;
+ releases_url: string;
+ /**
+ * The size of the repository. Size is calculated hourly. When a repository is initially
+ * created, the size is 0.
+ */
+ size: number;
+ /**
+ * The default value for a squash merge commit message:
+ *
+ * - `PR_BODY` - default to the pull request's body.
+ * - `COMMIT_MESSAGES` - default to the branch's commit messages.
+ * - `BLANK` - default to a blank commit message.
+ */
+ squash_merge_commit_message?: SquashMergeCommitMessage;
+ /**
+ * The default value for a squash merge commit title:
+ *
+ * - `PR_TITLE` - default to the pull request's title.
+ * - `COMMIT_OR_PR_TITLE` - default to the commit's title (if only one commit) or the pull
+ * request's title (when more than one commit).
+ */
+ squash_merge_commit_title?: SquashMergeCommitTitle;
+ ssh_url: string;
+ stargazers_count: number;
+ stargazers_url: string;
+ starred_at?: string;
+ statuses_url: string;
+ subscribers_count?: number;
+ subscribers_url: string;
+ subscription_url: string;
+ svn_url: string;
+ tags_url: string;
+ teams_url: string;
+ temp_clone_token?: string;
+ template_repository?: null | TemplateRepository;
+ topics?: string[];
+ trees_url: string;
+ updated_at: Date | null;
+ url: string;
+ /**
+ * Whether a squash merge commit can use the pull request title as default. **This property
+ * has been deprecated. Please use `squash_merge_commit_title` instead.
+ */
+ use_squash_pr_title_as_default?: boolean;
+ /**
+ * The repository visibility: public, private, or internal.
+ */
+ visibility?: string;
+ watchers: number;
+ watchers_count: number;
+ /**
+ * Whether to require contributors to sign off on web-based commits
+ */
+ web_commit_signoff_required?: boolean;
+ [property: string]: any;
+}
+
+/**
+ * License Simple
+ */
+export interface LicenseSimple {
+ html_url?: string;
+ key: string;
+ name: string;
+ node_id: string;
+ spdx_id: null | string;
+ url: null | string;
+ [property: string]: any;
+}
+
+/**
+ * The default value for a merge commit message.
+ *
+ * - `PR_TITLE` - default to the pull request's title.
+ * - `PR_BODY` - default to the pull request's body.
+ * - `BLANK` - default to a blank commit message.
+ */
+export enum MergeCommitMessage {
+ Blank = 'BLANK',
+ PRBody = 'PR_BODY',
+ PRTitle = 'PR_TITLE',
+}
+
+/**
+ * The default value for a merge commit title.
+ *
+ * - `PR_TITLE` - default to the pull request's title.
+ * - `MERGE_MESSAGE` - default to the classic title for a merge message (e.g., Merge pull
+ * request #123 from branch-name).
+ */
+export enum MergeCommitTitle {
+ MergeMessage = 'MERGE_MESSAGE',
+ PRTitle = 'PR_TITLE',
+}
+
+/**
+ * A GitHub user.
+ */
+export interface RepositorySimpleUser {
+ avatar_url: string;
+ email?: null | string;
+ events_url: string;
+ followers_url: string;
+ following_url: string;
+ gists_url: string;
+ gravatar_id: null | string;
+ html_url: string;
+ id: number;
+ login: string;
+ name?: null | string;
+ node_id: string;
+ organizations_url: string;
+ received_events_url: string;
+ repos_url: string;
+ site_admin: boolean;
+ starred_at?: string;
+ starred_url: string;
+ subscriptions_url: string;
+ type: string;
+ url: string;
+ [property: string]: any;
+}
+
+/**
+ * A GitHub user.
+ */
+export interface OwnerObject {
+ avatar_url: string;
+ email?: null | string;
+ events_url: string;
+ followers_url: string;
+ following_url: string;
+ gists_url: string;
+ gravatar_id: null | string;
+ html_url: string;
+ id: number;
+ login: string;
+ name?: null | string;
+ node_id: string;
+ organizations_url: string;
+ received_events_url: string;
+ repos_url: string;
+ site_admin: boolean;
+ starred_at?: string;
+ starred_url: string;
+ subscriptions_url: string;
+ type: string;
+ url: string;
+ [property: string]: any;
+}
+
+export interface RepositoryPermissions {
+ admin: boolean;
+ maintain?: boolean;
+ pull: boolean;
+ push: boolean;
+ triage?: boolean;
+ [property: string]: any;
+}
+
+/**
+ * The default value for a squash merge commit message:
+ *
+ * - `PR_BODY` - default to the pull request's body.
+ * - `COMMIT_MESSAGES` - default to the branch's commit messages.
+ * - `BLANK` - default to a blank commit message.
+ */
+export enum SquashMergeCommitMessage {
+ Blank = 'BLANK',
+ CommitMessages = 'COMMIT_MESSAGES',
+ PRBody = 'PR_BODY',
+}
+
+/**
+ * The default value for a squash merge commit title:
+ *
+ * - `PR_TITLE` - default to the pull request's title.
+ * - `COMMIT_OR_PR_TITLE` - default to the commit's title (if only one commit) or the pull
+ * request's title (when more than one commit).
+ */
+export enum SquashMergeCommitTitle {
+ CommitOrPRTitle = 'COMMIT_OR_PR_TITLE',
+ PRTitle = 'PR_TITLE',
+}
+
+export interface TemplateRepository {
+ allow_auto_merge?: boolean;
+ allow_merge_commit?: boolean;
+ allow_rebase_merge?: boolean;
+ allow_squash_merge?: boolean;
+ allow_update_branch?: boolean;
+ archive_url?: string;
+ archived?: boolean;
+ assignees_url?: string;
+ blobs_url?: string;
+ branches_url?: string;
+ clone_url?: string;
+ collaborators_url?: string;
+ comments_url?: string;
+ commits_url?: string;
+ compare_url?: string;
+ contents_url?: string;
+ contributors_url?: string;
+ created_at?: string;
+ default_branch?: string;
+ delete_branch_on_merge?: boolean;
+ deployments_url?: string;
+ description?: string;
+ disabled?: boolean;
+ downloads_url?: string;
+ events_url?: string;
+ fork?: boolean;
+ forks_count?: number;
+ forks_url?: string;
+ full_name?: string;
+ git_commits_url?: string;
+ git_refs_url?: string;
+ git_tags_url?: string;
+ git_url?: string;
+ has_downloads?: boolean;
+ has_issues?: boolean;
+ has_pages?: boolean;
+ has_projects?: boolean;
+ has_wiki?: boolean;
+ homepage?: string;
+ hooks_url?: string;
+ html_url?: string;
+ id?: number;
+ is_template?: boolean;
+ issue_comment_url?: string;
+ issue_events_url?: string;
+ issues_url?: string;
+ keys_url?: string;
+ labels_url?: string;
+ language?: string;
+ languages_url?: string;
+ /**
+ * The default value for a merge commit message.
+ *
+ * - `PR_TITLE` - default to the pull request's title.
+ * - `PR_BODY` - default to the pull request's body.
+ * - `BLANK` - default to a blank commit message.
+ */
+ merge_commit_message?: MergeCommitMessage;
+ /**
+ * The default value for a merge commit title.
+ *
+ * - `PR_TITLE` - default to the pull request's title.
+ * - `MERGE_MESSAGE` - default to the classic title for a merge message (e.g., Merge pull
+ * request #123 from branch-name).
+ */
+ merge_commit_title?: MergeCommitTitle;
+ merges_url?: string;
+ milestones_url?: string;
+ mirror_url?: string;
+ name?: string;
+ network_count?: number;
+ node_id?: string;
+ notifications_url?: string;
+ open_issues_count?: number;
+ owner?: Owner;
+ permissions?: TemplateRepositoryPermissions;
+ private?: boolean;
+ pulls_url?: string;
+ pushed_at?: string;
+ releases_url?: string;
+ size?: number;
+ /**
+ * The default value for a squash merge commit message:
+ *
+ * - `PR_BODY` - default to the pull request's body.
+ * - `COMMIT_MESSAGES` - default to the branch's commit messages.
+ * - `BLANK` - default to a blank commit message.
+ */
+ squash_merge_commit_message?: SquashMergeCommitMessage;
+ /**
+ * The default value for a squash merge commit title:
+ *
+ * - `PR_TITLE` - default to the pull request's title.
+ * - `COMMIT_OR_PR_TITLE` - default to the commit's title (if only one commit) or the pull
+ * request's title (when more than one commit).
+ */
+ squash_merge_commit_title?: SquashMergeCommitTitle;
+ ssh_url?: string;
+ stargazers_count?: number;
+ stargazers_url?: string;
+ statuses_url?: string;
+ subscribers_count?: number;
+ subscribers_url?: string;
+ subscription_url?: string;
+ svn_url?: string;
+ tags_url?: string;
+ teams_url?: string;
+ temp_clone_token?: string;
+ topics?: string[];
+ trees_url?: string;
+ updated_at?: string;
+ url?: string;
+ use_squash_pr_title_as_default?: boolean;
+ visibility?: string;
+ watchers_count?: number;
+ [property: string]: any;
+}
+
+export interface Owner {
+ avatar_url?: string;
+ events_url?: string;
+ followers_url?: string;
+ following_url?: string;
+ gists_url?: string;
+ gravatar_id?: string;
+ html_url?: string;
+ id?: number;
+ login?: string;
+ node_id?: string;
+ organizations_url?: string;
+ received_events_url?: string;
+ repos_url?: string;
+ site_admin?: boolean;
+ starred_url?: string;
+ subscriptions_url?: string;
+ type?: string;
+ url?: string;
+ [property: string]: any;
+}
+
+export interface TemplateRepositoryPermissions {
+ admin?: boolean;
+ maintain?: boolean;
+ pull?: boolean;
+ push?: boolean;
+ triage?: boolean;
+ [property: string]: any;
+}
+
+export enum StateReason {
+ Completed = 'completed',
+ NotPlanned = 'not_planned',
+ Reopened = 'reopened',
+}
+
+/**
+ * A GitHub user.
+ */
+export interface TentacledSimpleUser {
+ avatar_url: string;
+ email?: null | string;
+ events_url: string;
+ followers_url: string;
+ following_url: string;
+ gists_url: string;
+ gravatar_id: null | string;
+ html_url: string;
+ id: number;
+ login: string;
+ name?: null | string;
+ node_id: string;
+ organizations_url: string;
+ received_events_url: string;
+ repos_url: string;
+ site_admin: boolean;
+ starred_at?: string;
+ starred_url: string;
+ subscriptions_url: string;
+ type: string;
+ url: string;
+ [property: string]: any;
+}
diff --git a/src/client/util/reportManager/reportManagerUtils.ts b/src/client/util/reportManager/reportManagerUtils.ts
new file mode 100644
index 000000000..b95417aa1
--- /dev/null
+++ b/src/client/util/reportManager/reportManagerUtils.ts
@@ -0,0 +1,254 @@
+// Final file url reference: "https://browndash.com/files/images/upload_cb31bc0fda59c96ca14193ec494f80cf_o.jpg" />
+
+import { Octokit } from '@octokit/core';
+import { Networking } from '../../Network';
+import { Issue } from './reportManagerSchema';
+
+// enums and interfaces
+
+export enum ViewState {
+ VIEW,
+ CREATE,
+}
+
+export enum Priority {
+ HIGH = 'priority-high',
+ MEDIUM = 'priority-medium',
+ LOW = 'priority-low',
+}
+
+export enum BugType {
+ BUG = 'bug',
+ COSMETIC = 'cosmetic',
+ DOCUMENTATION = 'documentation',
+ ENHANCEMENT = 'enhancement',
+}
+
+export interface FileData {
+ _id: string;
+ file: File;
+}
+
+export interface ReportForm {
+ title: string;
+ description: string;
+ type: BugType;
+ priority: Priority;
+ mediaFiles: FileData[];
+}
+
+export type ReportFormKey = keyof ReportForm;
+
+export const emptyReportForm = {
+ title: '',
+ description: '',
+ type: BugType.BUG,
+ priority: Priority.MEDIUM,
+ mediaFiles: [],
+};
+
+// interfacing with Github
+
+/**
+ * Fetches issues from Github.
+ * @returns array of all issues
+ */
+export const getAllIssues = async (octokit: Octokit): Promise<any[]> => {
+ const res = await octokit.request('GET /repos/{owner}/{repo}/issues', {
+ owner: 'brown-dash',
+ repo: 'Dash-Web',
+ per_page: 80,
+ });
+
+ // 200 status means success
+ if (res.status === 200) {
+ return res.data;
+ } else {
+ throw new Error('Error getting issues');
+ }
+};
+
+/**
+ * Formats issue title.
+ *
+ * @param title title of issue
+ * @param userEmail email of issue submitter
+ * @returns formatted title
+ */
+export const formatTitle = (title: string, userEmail: string): string => `${title} - ${userEmail.replace('@brown.edu', '')}`;
+
+// uploading
+
+// turns an upload link -> server link
+// ex:
+// C: /Users/dash/Documents/GitHub/Dash-Web/src/server/public/files/images/upload_8008dbc4b6424fbff14da7345bb32eb2.png
+// -> https://browndash.com/files/images/upload_8008dbc4b6424fbff14da7345bb32eb2_l.png
+export const fileLinktoServerLink = (fileLink: string): string => {
+ const serverUrl = window.location.href.includes('browndash') ? 'https://browndash.com/' : 'http://localhost:1050/';
+
+ const regex = 'public';
+ const publicIndex = fileLink.indexOf(regex) + regex.length;
+
+ const finalUrl = `${serverUrl}${fileLink.substring(publicIndex + 1).replace('.', '_l.')}`;
+ return finalUrl;
+};
+
+/**
+ * Gets the server file path.
+ *
+ * @param link response from file upload
+ * @returns server file path
+ */
+export const getServerPath = (link: any): string => {
+ return link.result.accessPaths.agnostic.server as string;
+};
+
+/**
+ * Uploads media files to the server.
+ * @returns the server paths or undefined on error
+ */
+export const uploadFilesToServer = async (mediaFiles: FileData[]): Promise<string[] | undefined> => {
+ try {
+ // need to always upload to browndash
+ const links = await Networking.UploadFilesToServer(mediaFiles.map(file => ({ file: file.file })));
+ return (links ?? []).map(getServerPath).map(fileLinktoServerLink);
+ } catch (err) {
+ if (err instanceof Error) {
+ alert(err.message);
+ } else {
+ alert(err);
+ }
+ }
+};
+
+// helper functions
+
+/**
+ * Returns when the issue passes the current filters.
+ *
+ * @param issue issue to check
+ * @returns boolean indicating whether the issue passes the current filters
+ */
+export const passesTagFilter = (issue: Issue, priorityFilter: string | null, bugFilter: string | null) => {
+ let passesPriority = true;
+ let passesBug = true;
+ if (priorityFilter) {
+ passesPriority = issue.labels.some(label => {
+ if (typeof label === 'string') {
+ return label === priorityFilter;
+ } else {
+ return label.name === priorityFilter;
+ }
+ });
+ }
+ if (bugFilter) {
+ passesBug = issue.labels.some(label => {
+ if (typeof label === 'string') {
+ return label === bugFilter;
+ } else {
+ return label.name === bugFilter;
+ }
+ });
+ }
+ return passesPriority && passesBug;
+};
+
+// sets and lists
+
+export const prioritySet = new Set(Object.values(Priority));
+export const bugSet = new Set(Object.values(BugType));
+
+export const priorityDropdownItems = [
+ {
+ text: 'Low',
+ val: Priority.LOW,
+ },
+ {
+ text: 'Medium',
+ val: Priority.MEDIUM,
+ },
+ {
+ text: 'High',
+ val: Priority.HIGH,
+ },
+];
+
+export const bugDropdownItems = [
+ {
+ text: 'Bug',
+ val: BugType.BUG,
+ },
+ {
+ text: 'Poor Design or Cosmetic',
+ val: BugType.COSMETIC,
+ },
+ {
+ text: 'Documentation',
+ val: BugType.DOCUMENTATION,
+ },
+ {
+ text: 'New feature or request',
+ val: BugType.ENHANCEMENT,
+ },
+];
+
+// colors
+
+// [bgColor, color]
+export const priorityColors: { [key: string]: string[] } = {
+ 'priority-low': ['#d4e0ff', '#000000'],
+ 'priority-medium': ['#6a91f6', '#ffffff'],
+ 'priority-high': ['#003cd5', '#ffffff'],
+};
+
+// [bgColor, color]
+export const bugColors: { [key: string]: string[] } = {
+ bug: ['#fe6d6d', '#ffffff'],
+ cosmetic: ['#c650f4', '#ffffff'],
+ documentation: ['#36acf0', '#ffffff'],
+ enhancement: ['#36d4f0', '#ffffff'],
+};
+
+export const getLabelColors = (label: string): string[] => {
+ if (prioritySet.has(label as Priority)) {
+ return priorityColors[label];
+ } else if (bugSet.has(label as BugType)) {
+ return bugColors[label];
+ }
+ return ['#0f73f6', '#ffffff'];
+};
+
+const hexToRgb = (hex: string) => {
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
+ return result
+ ? {
+ r: parseInt(result[1], 16),
+ g: parseInt(result[2], 16),
+ b: parseInt(result[3], 16),
+ }
+ : {
+ r: 0,
+ g: 0,
+ b: 0,
+ };
+};
+
+// function that returns whether text should be light on the given bg color
+export const isDarkMode = (bgHex: string): boolean => {
+ const { r, g, b } = hexToRgb(bgHex);
+ return r * 0.299 + g * 0.587 + b * 0.114 <= 186;
+};
+
+export const lightColors = {
+ text: '#000000',
+ textGrey: '#5c5c5c',
+ border: '#b8b8b8',
+};
+
+export const darkColors = {
+ text: '#ffffff',
+ textGrey: '#d6d6d6',
+ border: '#717171',
+};
+
+export const dashBlue = '#4476f7';
diff --git a/src/client/views/.DS_Store b/src/client/views/.DS_Store
index 5e39387b8..5fa69f28c 100644
--- a/src/client/views/.DS_Store
+++ b/src/client/views/.DS_Store
Binary files differ
diff --git a/src/client/views/AntimodeMenu.scss b/src/client/views/AntimodeMenu.scss
index 8a0e5480e..b205a0f1e 100644
--- a/src/client/views/AntimodeMenu.scss
+++ b/src/client/views/AntimodeMenu.scss
@@ -5,11 +5,15 @@
position: absolute;
z-index: 10001;
height: $antimodemenu-height;
- background: $dark-gray;
- border-bottom: $standard-border;
+ width: fit-content;
+ border-radius: $standard-border-radius;
+ overflow: hidden;
// box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25);
// border-radius: 0px 6px 6px 6px;
display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 3px;
&.with-rows {
flex-direction: column
@@ -20,30 +24,6 @@
height: 35px;
}
- .antimodeMenu-button {
- background-color: transparent;
- width: 35px;
- height: 35px;
- padding: 5;
- text-align: center;
- display: flex;
- justify-content: center;
- align-items: center;
- position: relative;
-
- .svg {
- margin: 0;
- }
-
- &.active {
- background-color: #121212;
- }
- }
-
- .antimodeMenu-button:hover {
- background-color: rgba(0, 0, 0, 0.4);
- }
-
.antimodeMenu-dragger {
height: 100%;
transition: width .2s;
diff --git a/src/client/views/AntimodeMenu.tsx b/src/client/views/AntimodeMenu.tsx
index de1207ce4..c41ea7053 100644
--- a/src/client/views/AntimodeMenu.tsx
+++ b/src/client/views/AntimodeMenu.tsx
@@ -1,6 +1,8 @@
import React = require('react');
import { observable, action, runInAction } from 'mobx';
import './AntimodeMenu.scss';
+import { StrCast } from '../../fields/Types';
+import { Doc } from '../../fields/Doc';
export interface AntimodeMenuProps {}
/**
@@ -148,6 +150,7 @@ export abstract class AntimodeMenu<T extends AntimodeMenuProps> extends React.Co
left: this._left,
top: this._top,
opacity: this._opacity,
+ background: StrCast(Doc.UserDoc().userBackgroundColor),
transitionProperty: this._transitionProperty,
transitionDuration: this._transitionDuration,
transitionDelay: this._transitionDelay,
@@ -173,6 +176,7 @@ export abstract class AntimodeMenu<T extends AntimodeMenuProps> extends React.Co
height: 'inherit',
width: 200,
opacity: this._opacity,
+ background: StrCast(Doc.UserDoc().userBackgroundColor),
transitionProperty: this._transitionProperty,
transitionDuration: this._transitionDuration,
transitionDelay: this._transitionDelay,
@@ -195,6 +199,7 @@ export abstract class AntimodeMenu<T extends AntimodeMenuProps> extends React.Co
left: this._left,
top: this._top,
opacity: this._opacity,
+ background: StrCast(Doc.UserDoc().userBackgroundColor),
transitionProperty: this._transitionProperty,
transitionDuration: this._transitionDuration,
transitionDelay: this._transitionDelay,
diff --git a/src/client/views/AudioWaveform.tsx b/src/client/views/AudioWaveform.tsx
index 14c922526..c779ce8c4 100644
--- a/src/client/views/AudioWaveform.tsx
+++ b/src/client/views/AudioWaveform.tsx
@@ -3,7 +3,7 @@ import axios from 'axios';
import { action, computed, IReactionDisposer, reaction } from 'mobx';
import { observer } from 'mobx-react';
import Waveform from 'react-audio-waveform';
-import { Doc } from '../../fields/Doc';
+import { Doc, NumListCast } from '../../fields/Doc';
import { List } from '../../fields/List';
import { listSpec } from '../../fields/Schema';
import { Cast } from '../../fields/Types';
@@ -54,7 +54,7 @@ export class AudioWaveform extends React.Component<AudioWaveformProps> {
}
@computed get audioBuckets() {
- return Cast(this.props.layoutDoc[this.audioBucketField(this.clipStart, this.clipEnd, this.zoomFactor)], listSpec('number'), []);
+ return NumListCast(this.props.layoutDoc[this.audioBucketField(this.clipStart, this.clipEnd, this.zoomFactor)]);
}
audioBucketField = (start: number, end: number, zoomFactor: number) => this.props.fieldKey + '_audioBuckets/' + '/' + start.toFixed(2).replace('.', '_') + '/' + end.toFixed(2).replace('.', '_') + '/' + zoomFactor * 10;
@@ -102,7 +102,7 @@ export class AudioWaveform extends React.Component<AudioWaveformProps> {
render() {
return (
<div className="audioWaveform">
- <Waveform color={Colors.MEDIUM_BLUE_ALT} height={this.waveHeight} barWidth={200 / this.audioBuckets.length} pos={this.props.duration} duration={this.props.duration} peaks={this.audioBuckets} progressColor={Colors.MEDIUM_BLUE_ALT} />
+ <Waveform color={Colors.MEDIUM_BLUE_ALT} height={this.waveHeight} barWidth={200 / this.audioBuckets.length} pos={this.props.duration} duration={this.props.duration} peaks={Array.from(this.audioBuckets)} progressColor={Colors.MEDIUM_BLUE_ALT} />
</div>
);
}
diff --git a/src/client/views/ContextMenu.scss b/src/client/views/ContextMenu.scss
index cbe14060a..588eff1d1 100644
--- a/src/client/views/ContextMenu.scss
+++ b/src/client/views/ContextMenu.scss
@@ -7,6 +7,7 @@
box-shadow: 0px 3px 4px rgba(0, 0, 0, 30%);
flex-direction: column;
background: whitesmoke;
+ color: black;
border-radius: 3px;
}
@@ -51,13 +52,29 @@
user-select: none;
transition: all 0.1s;
border-style: none;
- // padding: 10px 0px 10px 0px;
+ position: relative;
white-space: nowrap;
font-size: 13px;
letter-spacing: 2px;
text-transform: uppercase;
padding-right: 30px;
+ .contextMenu-item-background {
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ top: 0;
+ left: 0;
+ z-index: 0;
+ filter: opacity(0);
+ }
+
+ &:hover {
+ .contextMenu-item-background {
+ filter: opacity(0.2) !important;
+ }
+ }
+
.contextMenu-item-icon-background {
pointer-events: all;
background-color: transparent;
@@ -132,11 +149,6 @@
// border-top: solid 1px; //TODO:glr clean
}
-.contextMenu-item:hover {
- transition: all 0.1s ease;
- background: $light-blue;
-}
-
.contextMenu-description {
margin-left: 5px;
text-align: left;
diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx
index e4c3e864b..8412a9aae 100644
--- a/src/client/views/ContextMenu.tsx
+++ b/src/client/views/ContextMenu.tsx
@@ -5,6 +5,8 @@ import { observer } from 'mobx-react';
import './ContextMenu.scss';
import { ContextMenuItem, ContextMenuProps, OriginalMenuProps } from './ContextMenuItem';
import { Utils } from '../../Utils';
+import { StrCast } from '../../fields/Types';
+import { Doc } from '../../fields/Doc';
@observer
export class ContextMenu extends React.Component {
@@ -190,7 +192,11 @@ export class ContextMenu extends React.Component {
}
return this.filteredItems.map((value, index) =>
Array.isArray(value) ? (
- <div className="contextMenu-group">
+ <div
+ className="contextMenu-group"
+ style={{
+ background: StrCast(Doc.UserDoc().userVariantColor),
+ }}>
<div className="contextMenu-description">{value.join(' -> ')}</div>
</div>
) : (
@@ -213,13 +219,18 @@ export class ContextMenu extends React.Component {
this._height = Number(getComputedStyle(r).height.replace('px', ''));
}
})}
- style={{ left: this.pageX, ...(this._yRelativeToTop ? { top: this.pageY } : { bottom: this.pageY }) }}>
+ style={{
+ left: this.pageX,
+ ...(this._yRelativeToTop ? { top: this.pageY } : { bottom: this.pageY }),
+ background: StrCast(Doc.UserDoc().userBackgroundColor),
+ color: StrCast(Doc.UserDoc().userColor),
+ }}>
{!this.itemsNeedSearch ? null : (
<span className={'search-icon'}>
<span className="icon-background">
<FontAwesomeIcon icon="search" size="lg" />
</span>
- <input className="contextMenu-item contextMenu-description search" type="text" placeholder="Filter Menu..." value={this._searchString} onKeyDown={this.onKeyDown} onChange={this.onChange} autoFocus />
+ <input style={{ color: 'black' }} className="contextMenu-item contextMenu-description search" type="text" placeholder="Filter Menu..." value={this._searchString} onKeyDown={this.onKeyDown} onChange={this.onChange} autoFocus />
</span>
)}
{this.menuItems}
diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx
index e87d2046b..daa2c152a 100644
--- a/src/client/views/ContextMenuItem.tsx
+++ b/src/client/views/ContextMenuItem.tsx
@@ -4,6 +4,8 @@ import { observer } from 'mobx-react';
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { UndoManager } from '../util/UndoManager';
+import { Doc } from '../../fields/Doc';
+import { StrCast } from '../../fields/Types';
export interface OriginalMenuProps {
description: string;
@@ -40,10 +42,7 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select
handleEvent = async (e: React.MouseEvent<HTMLDivElement>) => {
if ('event' in this.props) {
this.props.closeMenu?.();
- let batch: UndoManager.Batch | undefined;
- if (this.props.undoable !== false) {
- batch = UndoManager.StartBatch(`Context menu event: ${this.props.description}`);
- }
+ const batch = this.props.undoable !== false ? UndoManager.StartBatch(`Click Menu item: ${this.props.description}`) : undefined;
await this.props.event({ x: e.clientX, y: e.clientY });
batch?.end();
}
@@ -93,6 +92,11 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select
</span>
) : null}
<div className="contextMenu-description">{this.props.description.replace(':', '')}</div>
+ <div className={`contextMenu-item-background`}
+ style={{
+ background: StrCast(Doc.UserDoc().userColor)
+ }}
+ />
</div>
);
} else if ('subitems' in this.props) {
@@ -106,6 +110,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)
}}>
{this._items.map(prop => (
<ContextMenuItem {...prop} key={prop.description} closeMenu={this.props.closeMenu} />
@@ -136,6 +141,11 @@ 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`}
+ style={{
+ background: StrCast(Doc.UserDoc().userColor)
+ }}
+ />
{submenu}
</div>
);
diff --git a/src/client/views/DashboardView.scss b/src/client/views/DashboardView.scss
index b8a6f6c05..6be2133ef 100644
--- a/src/client/views/DashboardView.scss
+++ b/src/client/views/DashboardView.scss
@@ -1,12 +1,15 @@
-@import "./global/globalCssVariables";
-
+@import './global/globalCssVariables';
.dashboard-view {
- padding: 50px;
- display: flex;
- flex-direction: row;
- width: 100%;
- position: absolute;
+ padding: 50px;
+ display: flex;
+ flex-direction: row;
+ width: 100%;
+ position: absolute;
+ height: 100%;
+ width:100%;
+ padding-right: 0px;
+ overflow: auto;
.left-menu {
display: flex;
@@ -14,26 +17,28 @@
flex-direction: column;
width: 250px;
min-width: 250px;
+ gap: 5px;
}
- .all-dashboards {
- display: flex;
- flex-direction: row;
- flex-wrap: wrap;
- overflow-y: scroll;
- }
+ .all-dashboards {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ overflow-y: auto;
+ width: 100%;
+ }
}
.text-button {
cursor: pointer;
- padding: 3px 0;
- &:hover {
- font-weight: 500;
- }
+ padding: 3px 0;
+ &:hover {
+ font-weight: 500;
+ }
- &.selected {
- font-weight: 700;
- }
+ &.selected {
+ font-weight: 700;
+ }
}
.new-dashboard-button {
@@ -56,15 +61,26 @@
display: flex;
justify-content: center;
align-items: center;
+ position: relative;
&:hover {
color: $light-blue;
border: solid 2px $light-blue;
}
+
+ .background {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ left: 0;
+ top: 0;
+ z-index: -1;
+ }
}
.dashboard-container {
border-radius: 10px;
+ position: relative;
cursor: pointer;
width: 250px;
height: 200px;
@@ -74,35 +90,55 @@
margin: 0 0px 30px 30px;
overflow: hidden;
- &:hover{
+ &:hover {
outline: solid 2px $light-blue;
- }
+ }
- .title {
- margin: 10px;
- font-weight: 500;
- }
+ .title {
+ margin: 10px;
+ font-weight: 500;
+ }
- img {
- width: auto;
- height: 80%;
- }
+ img {
+ width: auto;
+ height: 80%;
+ }
- .info {
- display: flex;
- flex-direction: row;
- justify-content: space-between;
- align-items: center;
- padding: 0px 10px;
- }
+ .info {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+ padding: 0px 10px;
+ }
+ .dashboard-status,
+ .dashboard-status-shared {
+ font-size: 9;
+ left: 10%;
+ position: relative;
+ top: -5;
+ }
+ .dashboard-status-shared {
+ background: 'lightgreen';
+ }
.more {
z-index: 100;
}
+
+ .background {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ left: 0;
+ top: 0;
+ z-index: -1;
+ }
}
.new-dashboard {
color: $dark-gray;
+ padding: 10px;
display: flex;
width: 100%;
height: 100%;
@@ -136,4 +172,4 @@
flex-direction: row;
justify-content: flex-end;
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/DashboardView.tsx b/src/client/views/DashboardView.tsx
index 02c5085a2..21808d6e0 100644
--- a/src/client/views/DashboardView.tsx
+++ b/src/client/views/DashboardView.tsx
@@ -1,15 +1,17 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Button, ColorPicker, FontSize, IconButton, Size } from 'browndash-components';
+import { Button, ColorPicker, EditableText, FontSize, IconButton, Size, Type } from 'browndash-components';
import { action, computed, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { DataSym, Doc, DocListCast, DocListCastAsync } from '../../fields/Doc';
+import { FaPlus } from 'react-icons/fa';
+import { Doc, DocListCast, DocListCastAsync } from '../../fields/Doc';
+import { AclPrivate, DocAcl } from '../../fields/DocSymbols';
import { Id } from '../../fields/FieldSymbols';
import { List } from '../../fields/List';
import { PrefetchProxy } from '../../fields/Proxy';
import { listSpec } from '../../fields/Schema';
import { ScriptField } from '../../fields/ScriptField';
-import { Cast, DocCast, ImageCast, StrCast } from '../../fields/Types';
+import { Cast, ImageCast, StrCast } from '../../fields/Types';
import { DocServer } from '../DocServer';
import { Docs, DocumentOptions, DocUtils } from '../documents/Documents';
import { CollectionViewType } from '../documents/DocumentTypes';
@@ -23,8 +25,7 @@ import { ContextMenu } from './ContextMenu';
import './DashboardView.scss';
import { Colors } from './global/globalEnums';
import { MainViewModal } from './MainViewModal';
-import { ButtonType } from './nodes/button/FontIconBox';
-import { FaPlus } from 'react-icons/fa';
+import { ButtonType } from './nodes/FontIconBox/FontIconBox';
enum DashboardGroup {
MyDashboards,
@@ -42,13 +43,16 @@ export class DashboardView extends React.Component {
@observable private selectedDashboardGroup = DashboardGroup.MyDashboards;
@observable private newDashboardName: string | undefined = undefined;
- @observable private newDashboardColor: string | undefined = undefined;
+ @observable private newDashboardColor: string | undefined = '#AFAFAF';
@action abortCreateNewDashboard = () => {
this.newDashboardName = undefined;
};
@action setNewDashboardName(name: string) {
this.newDashboardName = name;
}
+ @action setNewDashboardColor(color: string) {
+ this.newDashboardColor = color;
+ }
@action
selectDashboardGroup = (group: DashboardGroup) => {
@@ -56,28 +60,30 @@ export class DashboardView extends React.Component {
};
clickDashboard = (e: React.MouseEvent, dashboard: Doc) => {
- Doc.AddDocToList(Doc.MySharedDocs, 'viewed', dashboard);
- Doc.ActiveDashboard = dashboard;
+ if (this.selectedDashboardGroup === DashboardGroup.SharedDashboards) {
+ DashboardView.openSharedDashboard(dashboard);
+ } else {
+ Doc.ActiveDashboard = dashboard;
+ }
Doc.ActivePage = 'dashboard';
};
- getDashboards = () => {
+ getDashboards = (whichGroup: DashboardGroup) => {
const allDashboards = DocListCast(Doc.MyDashboards.data);
- if (this.selectedDashboardGroup === DashboardGroup.MyDashboards) {
+ if (whichGroup === DashboardGroup.MyDashboards) {
return allDashboards.filter(dashboard => Doc.GetProto(dashboard).author === Doc.CurrentUserEmail);
- } else {
- const sharedDashboards = DocListCast(Doc.MySharedDocs.data).filter(doc => doc._viewType === CollectionViewType.Docking);
- return sharedDashboards;
}
+ const sharedDashboards = DocListCast(Doc.MySharedDocs.data_dashboards).filter(doc => doc.dockingConfig);
+ return sharedDashboards;
};
isUnviewedSharedDashboard = (dashboard: Doc): boolean => {
- // const sharedDashboards = DocListCast(Doc.MySharedDocs.data).filter(doc => doc._viewType === CollectionViewType.Docking);
+ // const sharedDashboards = DocListCast(Doc.MySharedDocs.data_dashboards).filter(doc => doc._type_collection === CollectionViewType.Docking);
return !DocListCast(Doc.MySharedDocs.viewed).includes(dashboard);
};
getSharedDashboards = () => {
- const sharedDashs = DocListCast(Doc.MySharedDocs.data).filter(doc => doc._viewType === CollectionViewType.Docking);
+ const sharedDashs = DocListCast(Doc.MySharedDocs.data_dashboards).filter(doc => doc._type_collection === CollectionViewType.Docking);
return sharedDashs.filter(dashboard => !DocListCast(Doc.MySharedDocs.viewed).includes(dashboard));
};
@@ -94,23 +100,18 @@ export class DashboardView extends React.Component {
const dashboardCount = DocListCast(Doc.MyDashboards.data).length + 1;
const placeholder = `Dashboard ${dashboardCount}`;
return (
- <div className="new-dashboard">
+ <div
+ className="new-dashboard"
+ style={{
+ background: StrCast(Doc.UserDoc().userBackgroundColor),
+ color: StrCast(Doc.UserDoc().userColor),
+ }}>
<div className="header">Create New Dashboard</div>
- <div className="title-input">
- Title
- <input className="input" placeholder={placeholder} onChange={e => this.setNewDashboardName((e.target as any).value)} />
- </div>
- <div className="color-picker">
- Background
- <ColorPicker
- onChange={color => {
- this.newDashboardColor = color;
- }}
- />
- </div>
+ <EditableText formLabel="Title" placeholder={placeholder} type={Type.SEC} color={StrCast(Doc.UserDoc().userColor)} setVal={val => this.setNewDashboardName(val as string)} fillWidth />
+ <ColorPicker formLabel="Background" colorPickerType="github" type={Type.TERT} selectedColor={this.newDashboardColor} setSelectedColor={this.setNewDashboardColor} />
<div className="button-bar">
- <Button text="Cancel" onClick={this.abortCreateNewDashboard} />
- <Button text="Create" onClick={() => this.createNewDashboard(this.newDashboardName!, this.newDashboardColor)} />
+ <Button text="Cancel" color={StrCast(Doc.UserDoc().userColor)} onClick={this.abortCreateNewDashboard} />
+ <Button type={Type.TERT} text="Create" color={StrCast(Doc.UserDoc().userVariantColor)} onClick={() => this.createNewDashboard(this.newDashboardName!, this.newDashboardColor)} />
</div>
</div>
);
@@ -149,32 +150,43 @@ export class DashboardView extends React.Component {
};
render() {
+ const color = StrCast(Doc.UserDoc().userColor);
+ const variant = StrCast(Doc.UserDoc().userVariantColor);
return (
<>
<div className="dashboard-view">
<div className="left-menu">
- <div className="new-dashboard-button">
- <Button icon={<FaPlus />} size={Size.MEDIUM} text="New" onClick={() => this.setNewDashboardName('')} />
- </div>
- <div className={`text-button ${this.selectedDashboardGroup === DashboardGroup.MyDashboards && 'selected'}`} onClick={() => this.selectDashboardGroup(DashboardGroup.MyDashboards)}>
- My Dashboards
- </div>
- <div className={`text-button ${this.selectedDashboardGroup === DashboardGroup.SharedDashboards && 'selected'}`} onClick={() => this.selectDashboardGroup(DashboardGroup.SharedDashboards)}>
- Shared Dashboards
- </div>
+ <Button text={'My Dashboards'} active={this.selectedDashboardGroup === DashboardGroup.MyDashboards} color={color} align={'flex-start'} onClick={() => this.selectDashboardGroup(DashboardGroup.MyDashboards)} fillWidth />
+ <Button
+ text={'Shared Dashboards' + ' (' + this.getDashboards(DashboardGroup.SharedDashboards).length + ')'}
+ active={this.selectedDashboardGroup === DashboardGroup.SharedDashboards}
+ color={this.getDashboards(DashboardGroup.SharedDashboards).some(dash => !DocListCast(Doc.MySharedDocs.viewed).includes(dash)) ? 'green' : color}
+ align={'flex-start'}
+ 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().map(dashboard => {
+ {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]} onContextMenu={e => this.onContextMenu(dashboard, e)} onClick={e => this.clickDashboard(e, dashboard)}>
+ <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">
- <input style={{ border: 'unset' }} className="input" onClick={e => e.stopPropagation()} defaultValue={StrCast(dashboard.title)} onChange={e => (Doc.GetProto(dashboard).title = (e.target as any).value)} />
+ <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"
@@ -187,9 +199,17 @@ export class DashboardView extends React.Component {
e.stopPropagation();
this.onContextMenu(dashboard, e);
}}>
- <Button size={Size.SMALL} icon={<FontAwesomeIcon color="black" size="lg" icon="bars" />} />
+ <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>
);
})}
@@ -199,6 +219,13 @@ export class DashboardView extends React.Component {
this.setNewDashboardName('');
}}>
+
+ <div
+ className={`background`}
+ style={{
+ background: StrCast(Doc.UserDoc().userColor),
+ filter: 'opacity(0.2)',
+ }}
+ />
</div>
</div>
</div>
@@ -220,6 +247,11 @@ export class DashboardView extends React.Component {
return CollectionDockingView.TakeSnapshot(Doc.ActiveDashboard);
}
+ public static openSharedDashboard = (dashboard: Doc) => {
+ Doc.AddDocToList(Doc.MySharedDocs, 'viewed', dashboard);
+ DashboardView.openDashboard(Doc.BestEmbedding(dashboard));
+ };
+
/// opens a dashboard as the ActiveDashboard (and adds the dashboard to the users list of dashboards if it's not already there).
/// this also sets the readonly state of the dashboard based on the current mode of dash (from its url)
public static openDashboard = (doc: Doc | undefined, fromHistory = false) => {
@@ -346,13 +378,13 @@ export class DashboardView extends React.Component {
},
],
};
- Doc.SetInPlace(dashboard, 'dockingConfig', JSON.stringify(reset), true);
+ if (dashboard.dockingConfig && dashboard.dockingConfig !== Doc.GetProto(dashboard).dockingConfig) dashboard.dockingConfig = JSON.stringify(reset);
+ else Doc.SetInPlace(dashboard, 'dockingConfig', JSON.stringify(reset), true);
return reset;
};
public static createNewDashboard = (id?: string, name?: string, background?: string) => {
- const dashboards = Doc.MyDashboards;
- const dashboardCount = DocListCast(dashboards.data).length + 1;
+ const dashboardCount = DocListCast(Doc.MyDashboards.data).length + 1;
const freeformOptions: DocumentOptions = {
x: 0,
y: 400,
@@ -366,14 +398,10 @@ export class DashboardView extends React.Component {
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');
- freeformDoc.embedContainer = dashboardDoc;
- // switching the tabs from the datadoc to the regular doc
- const dashboardTabs = DocListCast(dashboardDoc[DataSym].data);
- dashboardDoc.data = new List<Doc>(dashboardTabs);
dashboardDoc['pane-count'] = 1;
- Doc.AddDocToList(dashboards, 'data', dashboardDoc);
+ Doc.AddDocToList(Doc.MyDashboards, 'data', dashboardDoc);
DashboardView.SetupDashboardTrails(dashboardDoc);
@@ -389,10 +417,11 @@ export class DashboardView extends React.Component {
_forceActive: true,
_width: 30,
_height: 30,
- _stayInCollection: true,
- _hideContextMenu: true,
+ _dragOnlyWithinContainer: true,
+ _layout_hideContextMenu: true,
title: 'New trail',
toolTip: 'Create new trail',
+ color: Colors.BLACK,
btnType: ButtonType.ClickButton,
buttonText: 'New trail',
icon: 'plus',
@@ -412,19 +441,18 @@ export class DashboardView extends React.Component {
_layout_fitWidth: true,
_gridGap: 5,
_forceActive: true,
- childDropAction: 'embed',
+ childDragAction: 'embed',
treeViewTruncateTitleWidth: 150,
ignoreClick: true,
- buttonMenu: true,
- buttonMenuDoc: myTrailsBtn,
+ layout_headerButton: myTrailsBtn,
contextMenuIcons: new List<string>(['plus']),
contextMenuLabels: new List<string>(['Create New Trail']),
_lockedPosition: true,
- boxShadow: '0 0',
+ layout_boxShadow: '0 0',
childDontRegisterViews: true,
- targetDropAction: 'same',
+ dropAction: 'same',
isSystem: true,
- explainer: 'All of the trails that you have created will appear here.',
+ layout_explainer: 'All of the trails that you have created will appear here.',
};
const myTrails = DocUtils.AssignScripts(Docs.Create.TreeDocument([], reqdOpts), { treeViewChildDoubleClick: 'openPresentation(documentView.rootDoc)' });
dashboardDoc.myTrails = new PrefetchProxy(myTrails);
@@ -436,10 +464,6 @@ export class DashboardView extends React.Component {
}
}
-export function AddToList(MySharedDocs: Doc, arg1: string, dash: any) {
- throw new Error('Function not implemented.');
-}
-
ScriptingGlobals.add(function createNewDashboard() {
return DashboardView.createNewDashboard();
}, 'creates a new dashboard when called');
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index ffb2d92f6..e076e69ca 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -1,15 +1,14 @@
import { action, computed, observable } from 'mobx';
import { DateField } from '../../fields/DateField';
-import { AclAdmin, AclAugment, AclEdit, AclPrivate, AclReadonly, AclSym, DataSym, Doc, DocListCast, Opt } from '../../fields/Doc';
-import { InkTool } from '../../fields/InkField';
+import { Doc, DocListCast, HierarchyMapping, Opt, ReverseHierarchyMap } from '../../fields/Doc';
+import { AclAdmin, AclAugment, AclEdit, AclPrivate, AclReadonly, DocAcl, DocData } from '../../fields/DocSymbols';
import { List } from '../../fields/List';
-import { Cast, ScriptCast } from '../../fields/Types';
-import { denormalizeEmail, distributeAcls, GetEffectiveAcl, inheritParentAcls, SharingPermissions } from '../../fields/util';
+import { Cast, DocCast, StrCast } from '../../fields/Types';
+import { distributeAcls, GetEffectiveAcl, inheritParentAcls, SharingPermissions } from '../../fields/util';
import { returnFalse } from '../../Utils';
import { DocUtils } from '../documents/Documents';
import { DocumentType } from '../documents/DocumentTypes';
import { InteractionUtils } from '../util/InteractionUtils';
-import { UndoManager } from '../util/UndoManager';
import { DocumentView } from './nodes/DocumentView';
import { Touchable } from './Touchable';
@@ -36,7 +35,7 @@ export function DocComponent<P extends DocComponentProps>() {
}
// This is the data part of a document -- ie, the data that is constant across all views of the document
@computed get dataDoc() {
- return this.props.Document[DataSym] as Doc;
+ return this.props.Document[DocData] as Doc;
}
// key where data is stored
@computed get fieldKey() {
@@ -74,7 +73,7 @@ export function ViewBoxBaseComponent<P extends ViewBoxBaseProps>() {
}
// This is the data part of a document -- ie, the data that is constant across all views of the document
@computed get dataDoc() {
- return this.props.DataDoc && (this.props.Document.isTemplateForField || this.props.Document.isTemplateDoc) ? this.props.DataDoc : this.props.Document[DataSym];
+ return this.props.DataDoc && (this.props.Document.isTemplateForField || this.props.Document.isTemplateDoc) ? this.props.DataDoc : this.props.Document[DocData];
}
// key where data is stored
@computed get fieldKey() {
@@ -118,7 +117,7 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>()
}
// This is the data part of a document -- ie, the data that is constant across all views of the document
@computed get dataDoc() {
- return this.props.DataDoc && (this.props.Document.isTemplateForField || this.props.Document.isTemplateDoc) ? this.props.DataDoc : this.props.Document[DataSym];
+ return this.props.DataDoc && (this.props.Document.isTemplateForField || this.props.Document.isTemplateDoc) ? this.props.DataDoc : this.props.Document[DocData];
}
// key where data is stored
@@ -128,8 +127,6 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>()
isAnyChildContentActive = () => this._isAnyChildContentActive;
- lookupField = (field: string) => ScriptCast((this.layoutDoc as any).lookupField)?.script.run({ self: this.layoutDoc, data: this.rootDoc, field: field }).result;
-
protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
@computed public get annotationKey() {
@@ -148,13 +145,13 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>()
const toRemove = value.filter(v => docs.includes(v));
if (toRemove.length !== 0) {
- const recent = Doc.MyRecentlyClosed;
+ const recent = this.rootDoc !== Doc.MyRecentlyClosed ? Doc.MyRecentlyClosed : undefined;
toRemove.forEach(doc => {
leavePushpin && DocUtils.LeavePushpin(doc, annotationKey ?? this.annotationKey);
Doc.RemoveDocFromList(targetDataDoc, annotationKey ?? this.annotationKey, doc);
+ Doc.RemoveDocFromList(Doc.GetProto(doc), 'proto_embeddings', doc);
doc.embedContainer = undefined;
if (recent) {
- Doc.RemoveDocFromList(recent, 'data', doc);
doc.type !== DocumentType.LOADING && Doc.AddDocToList(recent, 'data', doc, undefined, true, true);
}
});
@@ -174,8 +171,8 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>()
return true;
}
const first = doc instanceof Doc ? doc : doc[0];
- if (!first?._stayInCollection && addDocument !== returnFalse) {
- return UndoManager.RunInTempBatch(() => this.removeDocument(doc, annotationKey, true) && addDocument(doc, annotationKey));
+ if (!first?._dragOnlyWithinContainer && addDocument !== returnFalse) {
+ return this.removeDocument(doc, annotationKey, false) && addDocument(doc, annotationKey);
}
return false;
};
@@ -185,7 +182,7 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>()
if (this.props.filterAddDocument?.(docs) === false || docs.find(doc => Doc.AreProtosEqual(doc, this.props.Document) && Doc.LayoutField(doc) === Doc.LayoutField(this.props.Document))) {
return false;
}
- const targetDataDoc = this.props.Document[DataSym];
+ const targetDataDoc = this.rootDoc[DocData];
const effectiveAcl = GetEffectiveAcl(targetDataDoc);
if (effectiveAcl === AclPrivate || effectiveAcl === AclReadonly) {
@@ -193,36 +190,16 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>()
}
const added = docs;
if (added.length) {
- const aclKeys = Object.keys(this.props.Document[AclSym] ?? {});
- aclKeys.forEach(key =>
- added.forEach(d => {
- if (d.author === denormalizeEmail(key.substring(4)) && !d.createdFrom) {
- distributeAcls(key, SharingPermissions.Admin, d);
- }
- })
- );
-
- if (effectiveAcl === AclAugment) {
- added.map(doc => {
- if ([AclAdmin, AclEdit].includes(GetEffectiveAcl(doc)) && Doc.ActiveDashboard) inheritParentAcls(Doc.ActiveDashboard, doc);
- doc.embedContainer = this.props.Document;
- if (annotationKey ?? this._annotationKeySuffix()) Doc.GetProto(doc).annotationOn = this.props.Document;
- Doc.AddDocToList(targetDataDoc, annotationKey ?? this.annotationKey, doc);
+ if ([AclAugment, AclEdit, AclAdmin].includes(effectiveAcl)) {
+ added.forEach(doc => {
+ doc._dragOnlyWithinContainer = undefined;
+ if (annotationKey ?? this._annotationKeySuffix()) Doc.GetProto(doc).annotationOn = this.rootDoc;
+ Doc.SetContainer(doc, this.rootDoc);
+ inheritParentAcls(targetDataDoc, doc, true);
});
- } else {
- added
- .filter(doc => [AclAdmin, AclEdit].includes(GetEffectiveAcl(doc)))
- .map(doc => {
- // only make a pushpin if we have acl's to edit the document
- //DocUtils.LeavePushpin(doc);
- doc._stayInCollection = undefined;
- doc.embedContainer = this.props.Document;
- if (annotationKey ?? this._annotationKeySuffix()) Doc.GetProto(doc).annotationOn = this.rootDoc;
-
- Doc.ActiveDashboard && inheritParentAcls(Doc.ActiveDashboard, doc);
- });
+
const annoDocs = targetDataDoc[annotationKey ?? this.annotationKey] as List<Doc>;
- if (annoDocs instanceof List) annoDocs.push(...added);
+ if (annoDocs instanceof List) annoDocs.push(...added.filter(add => !annoDocs.includes(add)));
else targetDataDoc[annotationKey ?? this.annotationKey] = new List<Doc>(added);
targetDataDoc[(annotationKey ?? this.annotationKey) + '_modificationDate'] = new DateField(new Date(Date.now()));
}
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 547d844ca..6f5e9f5c0 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -464,6 +464,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
action(() => {
this._isRecording = false;
this._stopFunc();
+ b.end();
}),
emptyFunction
);
@@ -588,8 +589,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
<div style={{ position: 'absolute', zIndex: 1000 }}>
<LinkPopup
key="popup"
- showPopup={this._showLinkPopup}
- linkCreated={link => (link.layout_linkDisplay = !IsFollowLinkScript(this.props.views().lastElement()?.rootDoc.onClick))}
+ linkCreated={link => (link.link_displayLine = !IsFollowLinkScript(this.props.views().lastElement()?.rootDoc.onClick))}
linkCreateAnchor={() => this.props.views().lastElement()?.ComponentView?.getAnchor?.(true)}
linkFrom={() => this.props.views().lastElement()?.rootDoc}
/>
diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index ccac5ffe4..ca3610cc0 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -112,6 +112,33 @@ $resizeHandler: 8px;
}
}
+ .documentDecorations-lockButton {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: grey;
+ border: solid 1.5px rgb(72, 71, 71);
+ color: grey;
+ transition: 0.1s ease;
+ opacity: 1;
+ pointer-events: all;
+ width: 20px;
+ height: 20px;
+ min-width: 20px;
+ border-radius: 100%;
+ opacity: 0.5;
+ cursor: pointer;
+
+ &:hover {
+ color: rgb(72, 71, 71);
+ opacity: 1;
+ }
+
+ > svg {
+ margin: 0;
+ }
+ }
+
.documentDecorations-minimizeButton {
display: flex;
align-items: center;
@@ -152,6 +179,7 @@ $resizeHandler: 8px;
display: flex;
height: 20px;
border-radius: 8px;
+ gap: 2px;
outline: none;
border: none;
opacity: 0.3;
@@ -186,6 +214,79 @@ $resizeHandler: 8px;
}
}
+ .documentDecorations-share {
+ background: none;
+ opacity: 1;
+ grid-column: 3;
+ pointer-events: auto;
+ min-width: fit-content;
+ text-align: center;
+ display: flex;
+ height: 21px;
+ opacity: 0.3;
+ &:hover {
+ opacity: 1;
+ }
+
+
+ .checkbox{
+ display: inline;
+
+ .checkbox-box{
+ display: inline;
+ position: relative;
+ top: -2.5;
+ left: 35;
+ zoom: .7;
+ }
+
+ & .checkbox-text{
+ display: inline;
+ position: relative;
+ top: 1.5;
+ font-size: 8px;
+ }
+ }
+
+ .documentDecorations-shareNone{
+ width: calc(100% + 10px);
+ background: grey;
+ color: rgb(71, 71, 71);
+ border-radius: 8px;
+ border: 2px solid rgb(71, 71, 71);
+ }
+ .documentDecorations-shareEdit,
+ .documentDecorations-shareAdmin{
+ width: calc(100% + 10px);
+ background: rgb(254, 254, 199);
+ color: rgb(75, 75, 5);
+ border-radius: 8px;
+ border: 2px solid rgb(75, 75, 5);
+ }
+ .documentDecorations-shareAugment{
+ width: calc(100% + 10px);
+ background: rgb(208, 255, 208);
+ color:rgb(19, 80, 19);
+ border-radius: 8px;
+ border: 2px solid rgb(19, 80, 19);
+
+ }
+ .documentDecorations-shareView{
+ width: calc(100% + 10px);
+ background: rgb(213, 213, 255);
+ color: rgb(25, 25, 101);
+ border-radius: 8px;
+ border: 2px solid rgb(25, 25, 101);
+ }
+ .documentDecorations-shareNot-Shared{
+ width: calc(100% + 10px);
+ background: rgb(255, 207, 207);
+ color: rgb(146, 58, 58);
+ border-radius: 8px;
+ border: 2px solid rgb(146, 58, 58);
+ }
+ }
+
.documentDecorations-centerCont {
grid-column: 2;
background: none;
@@ -264,7 +365,7 @@ $resizeHandler: 8px;
.documentDecorations-lock {
position: relative;
background: black;
- color: gray;
+ color: rgb(145, 144, 144);
height: 14;
width: 14;
pointer-events: all;
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 39073d763..f3daf3ffa 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -2,23 +2,26 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tooltip } from '@material-ui/core';
import { IconButton } from 'browndash-components';
-import { action, computed, observable, reaction, runInAction } from 'mobx';
+import { action, computed, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import { FaUndo } from 'react-icons/fa';
import { DateField } from '../../fields/DateField';
-import { AclAdmin, AclEdit, DataSym, Doc, DocListCast, Field, HeightSym, WidthSym } from '../../fields/Doc';
-import { Document } from '../../fields/documentSchemas';
+import { Doc, DocListCast, Field, HierarchyMapping, ReverseHierarchyMap } from '../../fields/Doc';
+import { AclAdmin, AclAugment, AclEdit, DocData, Height, Width } from '../../fields/DocSymbols';
import { InkField } from '../../fields/InkField';
+import { RichTextField } from '../../fields/RichTextField';
import { ScriptField } from '../../fields/ScriptField';
-import { Cast, NumCast, StrCast } from '../../fields/Types';
+import { Cast, DocCast, NumCast, StrCast } from '../../fields/Types';
import { GetEffectiveAcl } from '../../fields/util';
import { aggregateBounds, emptyFunction, numberValue, returnFalse, setupMoveUpEvents, Utils } from '../../Utils';
import { Docs } from '../documents/Documents';
import { DocumentType } from '../documents/DocumentTypes';
+import { DocumentManager } from '../util/DocumentManager';
import { DragManager } from '../util/DragManager';
+import { LinkFollower } from '../util/LinkFollower';
import { SelectionManager } from '../util/SelectionManager';
import { SnappingManager } from '../util/SnappingManager';
-import { undoBatch, UndoManager } from '../util/UndoManager';
+import { UndoManager } from '../util/UndoManager';
import { CollectionDockingView } from './collections/CollectionDockingView';
import { CollectionFreeFormView } from './collections/collectionFreeForm';
import { DocumentButtonBar } from './DocumentButtonBar';
@@ -27,15 +30,11 @@ import { Colors } from './global/globalEnums';
import { InkingStroke } from './InkingStroke';
import { InkStrokeProperties } from './InkStrokeProperties';
import { LightboxView } from './LightboxView';
-import { DocumentView, OpenWhere, OpenWhereMod } from './nodes/DocumentView';
+import { DocumentView, OpenWhereMod } from './nodes/DocumentView';
import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
import { ImageBox } from './nodes/ImageBox';
import React = require('react');
-import { RichTextField } from '../../fields/RichTextField';
-import { LinkFollower } from '../util/LinkFollower';
import _ = require('lodash');
-import { DocumentManager } from '../util/DocumentManager';
-import { isUndefined } from 'lodash';
@observer
export class DocumentDecorations extends React.Component<{ PanelWidth: number; PanelHeight: number; boundsLeft: number; boundsTop: number }, { value: string }> {
@@ -65,6 +64,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
@observable private _isRotating: boolean = false;
@observable private _isRounding: boolean = false;
@observable private _isResizing: boolean = false;
+ @observable private showLayoutAcl: boolean = false;
constructor(props: any) {
super(props);
@@ -150,7 +150,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
Doc.SetInPlace(d.rootDoc, titleFieldKey, titleField, true);
}
}),
- 'title blur'
+ 'edit title'
);
}
};
@@ -163,35 +163,50 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
};
@action onContainerDown = (e: React.PointerEvent): void => {
- setupMoveUpEvents(
- this,
- e,
- e => this.onBackgroundMove(true, e),
- e => {},
- emptyFunction
- );
+ const first = SelectionManager.Views()[0];
+ const effectiveLayoutAcl = GetEffectiveAcl(first.rootDoc);
+ if (effectiveLayoutAcl == AclAdmin || effectiveLayoutAcl == AclEdit || effectiveLayoutAcl == AclAugment) {
+ setupMoveUpEvents(
+ this,
+ e,
+ e => this.onBackgroundMove(true, e),
+ e => {},
+ emptyFunction
+ );
+ }
};
@action onTitleDown = (e: React.PointerEvent): void => {
- setupMoveUpEvents(
- this,
- e,
- e => this.onBackgroundMove(true, e),
- e => {},
- action(e => {
- !this._editingTitle && (this._accumulatedTitle = this._titleControlString.startsWith('#') ? this.selectionTitle : this._titleControlString);
- this._editingTitle = true;
- this._keyinput.current && setTimeout(this._keyinput.current.focus);
- })
- );
+ const first = SelectionManager.Views()[0];
+ const effectiveLayoutAcl = GetEffectiveAcl(first.rootDoc);
+ if (effectiveLayoutAcl == AclAdmin || effectiveLayoutAcl == AclEdit || effectiveLayoutAcl == AclAugment) {
+ setupMoveUpEvents(
+ this,
+ e,
+ e => this.onBackgroundMove(true, e),
+ e => {},
+ action(e => {
+ !this._editingTitle && (this._accumulatedTitle = this._titleControlString.startsWith('#') ? this.selectionTitle : this._titleControlString);
+ this._editingTitle = true;
+ this._keyinput.current && setTimeout(this._keyinput.current.focus);
+ })
+ );
+ }
};
onBackgroundDown = (e: React.PointerEvent) => setupMoveUpEvents(this, e, e => this.onBackgroundMove(false, e), emptyFunction, emptyFunction);
@action
onBackgroundMove = (dragTitle: boolean, e: PointerEvent): boolean => {
+ const first = SelectionManager.Views()[0];
+ const effectiveLayoutAcl = GetEffectiveAcl(first.rootDoc);
+ if (effectiveLayoutAcl != AclAdmin && effectiveLayoutAcl != AclEdit && effectiveLayoutAcl != AclAugment) {
+ return false;
+ }
const dragDocView = SelectionManager.Views()[0];
- if (DocListCast(Doc.MyOverlayDocs.data).includes(dragDocView.rootDoc)) return false;
+ const containers = new Set<Doc | undefined>();
+ SelectionManager.Views().forEach(v => containers.add(DocCast(v.rootDoc.embedContainer)));
+ if (containers.size > 1) return false;
const { left, top } = dragDocView.getBounds() || { left: 0, top: 0 };
const dragData = new DragManager.DocumentDragData(
SelectionManager.Views().map(dv => dv.props.Document),
@@ -227,11 +242,6 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
.filter(v => v && v.props.renderDepth > 0);
if (forceDeleteOrIconify === false && this._iconifyBatch) return;
this._deleteAfterIconify = forceDeleteOrIconify || this._iconifyBatch ? true : false;
- if (!this._iconifyBatch) {
- this._iconifyBatch = UndoManager.StartBatch('iconifying');
- } else {
- forceDeleteOrIconify = false; // can't force immediate close in the middle of iconifying -- have to wait until iconifying completes
- }
var iconifyingCount = views.length;
const finished = action((force?: boolean) => {
if ((force || --iconifyingCount === 0) && this._iconifyBatch) {
@@ -250,6 +260,12 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
this._iconifyBatch = undefined;
}
});
+ if (!this._iconifyBatch) {
+ this._iconifyBatch = UndoManager.StartBatch(forceDeleteOrIconify ? 'delete selected docs' : 'iconifying');
+ } else {
+ forceDeleteOrIconify = false; // can't force immediate close in the middle of iconifying -- have to wait until iconifying completes
+ }
+
if (forceDeleteOrIconify) finished(forceDeleteOrIconify);
else if (!this._deleteAfterIconify) views.forEach(dv => dv.iconify(finished));
};
@@ -273,19 +289,19 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
if (selectedDocs.length) {
if (e.ctrlKey) {
// open an embedding in a new tab with Ctrl Key
- CollectionDockingView.AddSplit(Doc.BestEmbedding(selectedDocs[0].props.Document), OpenWhereMod.right);
+ CollectionDockingView.AddSplit(Doc.BestEmbedding(selectedDocs[0].rootDoc), OpenWhereMod.right);
} else if (e.shiftKey) {
// open centered in a new workspace with Shift Key
- const embedding = Doc.MakeEmbedding(selectedDocs[0].props.Document);
+ const embedding = Doc.MakeEmbedding(selectedDocs[0].rootDoc);
embedding.embedContainer = undefined;
- embedding.x = -embedding[WidthSym]() / 2;
- embedding.y = -embedding[HeightSym]() / 2;
+ embedding.x = -embedding[Width]() / 2;
+ embedding.y = -embedding[Height]() / 2;
CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([embedding], { title: 'Tab for ' + embedding.title }), OpenWhereMod.right);
} else if (e.altKey) {
// open same document in new tab
- CollectionDockingView.ToggleSplit(selectedDocs[0].props.Document, OpenWhereMod.right);
+ CollectionDockingView.ToggleSplit(selectedDocs[0].rootDoc, OpenWhereMod.right);
} else {
- var openDoc = selectedDocs[0].props.Document;
+ var openDoc = selectedDocs[0].rootDoc;
if (openDoc.layout_fieldKey === 'layout_icon') {
openDoc = DocListCast(openDoc.proto_embeddings).find(embedding => !embedding.embedContainer) ?? Doc.MakeEmbedding(openDoc);
Doc.deiconifyView(openDoc);
@@ -293,7 +309,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
LightboxView.SetLightboxDoc(
openDoc,
undefined,
- selectedDocs.slice(1).map(view => view.props.Document)
+ selectedDocs.slice(1).map(view => view.rootDoc)
);
}
}
@@ -330,7 +346,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
const docMax = Math.min(NumCast(doc.width) / 2, NumCast(doc.height) / 2);
const ratio = dist / maxDist;
const radius = Math.min(1, ratio) * docMax;
- doc.borderRounding = `${radius}px`;
+ doc.layout_borderRounding = `${radius}px`;
});
return false;
}, // moveEvent
@@ -397,7 +413,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
onRotateDown = (e: React.PointerEvent): void => {
this._isRotating = true;
const rcScreen = { X: this.rotCenter[0], Y: this.rotCenter[1] };
- const rotateUndo = UndoManager.StartBatch('rotatedown');
+ const rotateUndo = UndoManager.StartBatch('drag rotation');
const selectedInk = SelectionManager.Views().filter(i => i.ComponentView instanceof InkingStroke);
const centerPoint = this.rotCenter.slice();
const infos = new Map<Doc, { unrotatedDocPos: { x: number; y: number }; startRotCtr: { x: number; y: number }; accumRot: number }>();
@@ -465,7 +481,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
const bounds = e.currentTarget.getBoundingClientRect();
this._offX = this._resizeHdlId.toLowerCase().includes('left') ? bounds.right - e.clientX : bounds.left - e.clientX;
this._offY = this._resizeHdlId.toLowerCase().includes('top') ? bounds.bottom - e.clientY : bounds.top - e.clientY;
- this._resizeUndo = UndoManager.StartBatch('DocDecs resize');
+ this._resizeUndo = UndoManager.StartBatch('drag resizing');
this._snapX = e.pageX;
this._snapY = e.pageY;
const ffviewSet = new Set<CollectionFreeFormView>();
@@ -479,6 +495,8 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
onPointerMove = (e: PointerEvent, down: number[], move: number[]): boolean => {
const first = SelectionManager.Views()[0];
+ const effectiveAcl = GetEffectiveAcl(first.rootDoc);
+ if (!(effectiveAcl == AclAdmin || effectiveAcl == AclEdit || effectiveAcl == AclAugment)) return false;
if (!first) return false;
let thisPt = { x: e.clientX - this._offX, y: e.clientY - this._offY };
var fixedAspect = Doc.NativeAspect(first.layoutDoc);
@@ -586,7 +604,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
let dW = docwidth * (dWin / refWidth);
let dH = docheight * (dHin / refHeight);
const scale = docView.props.ScreenToLocalTransform().Scale;
- const modifyNativeDim = (e.ctrlKey || doc.layout_forceReflow) && doc.nativeDimModifiable && ((!dragBottom && !dragTop) || e.ctrlKey || doc.nativeHeightUnfrozen);
+ const modifyNativeDim = (e.ctrlKey && doc.nativeDimModifiable) || (doc.layout_forceReflow && !dragBottom && !dragTop) || (doc.nativeHeightUnfrozen && (dragBottom || dragTop || e.ctrlKey));
if (nwidth && nheight) {
if (nwidth / nheight !== docwidth / docheight && !dragBottom && !dragTop) {
docheight = (nheight / nwidth) * docwidth;
@@ -599,9 +617,9 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
}
let actualdW = Math.max(docwidth + dW * scale, 20);
let actualdH = Math.max(docheight + dH * scale, 20);
- let dX = !dWin ? 0 : scale * refCent[0] * (1 - (1 + dWin / refWidth));
- let dY = !dHin ? 0 : scale * refCent[1] * (1 - (1 + dHin / refHeight));
- const preserveNativeDim = doc._nativeHeightUnfrozen === false && doc._nativeDimModifiable === false;
+ let dX = !dWin ? 0 : (scale * refCent[0] * -dWin) / refWidth;
+ let dY = !dHin ? 0 : (scale * refCent[1] * -dHin) / refHeight;
+ const preserveNativeDim = !doc._nativeHeightUnfrozen && !doc._nativeDimModifiable;
const fixedAspect = nwidth && nheight && (!doc._layout_fitWidth || preserveNativeDim || e.ctrlKey || doc.nativeHeightUnfrozen || doc.nativeDimModifiable);
if (fixedAspect) {
if ((Math.abs(dW) > Math.abs(dH) && ((!dragBottom && !dragTop) || !modifyNativeDim)) || dragRight) {
@@ -612,6 +630,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
} else {
if (!doc._layout_fitWidth || preserveNativeDim) {
actualdH = (nheight / nwidth) * actualdW;
+ dYin && (dY = -dW * scale * (nheight / nwidth));
doc._height = actualdH;
} else if (!modifyNativeDim || dragBotRight) {
doc._height = actualdH;
@@ -628,13 +647,14 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
} else {
if (!doc._layout_fitWidth || preserveNativeDim) {
actualdW = (nwidth / nheight) * actualdH;
+ dXin && (dX = -dH * scale * (nwidth / nheight));
doc._width = actualdW;
} else if (!modifyNativeDim || dragBotRight) {
doc._width = actualdW;
}
}
if (!modifyNativeDim) {
- actualdH = Math.min((nheight / nwidth) * docwidth, actualdH);
+ actualdH = (nheight / nwidth) * NumCast(doc._width); //, actualdH);
}
doc._height = actualdH;
}
@@ -744,17 +764,25 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
setTimeout(action(() => (this._showNothing = true)));
return null;
}
+
+ // sharing
+ const acl = GetEffectiveAcl(!this.showLayoutAcl ? Doc.GetProto(seldocview.rootDoc) : seldocview.rootDoc);
+ const docShareMode = HierarchyMapping.get(acl)!.name;
+ const shareMode = StrCast(docShareMode);
+ var shareSymbolIcon = ReverseHierarchyMap.get(shareMode)?.image;
+
// hide the decorations if the parent chooses to hide it or if the document itself hides it
const hideDecorations = seldocview.props.hideDecorations || seldocview.rootDoc.hideDecorations;
- const hideResizers = hideDecorations || seldocview.props.hideResizeHandles || seldocview.rootDoc.layout_hideResizeHandles || this._isRounding || this._isRotating;
+ const hideResizers =
+ ![AclAdmin, AclEdit, AclAugment].includes(GetEffectiveAcl(seldocview.rootDoc)) || hideDecorations || seldocview.props.hideResizeHandles || seldocview.rootDoc.layout_hideResizeHandles || this._isRounding || this._isRotating;
const hideTitle = hideDecorations || seldocview.props.hideDecorationTitle || seldocview.rootDoc.layout_hideDecorationTitle || this._isRounding || this._isRotating;
- const hideDocumentButtonBar = hideDecorations || seldocview.props.hideDocumentButtonBar || seldocview.rootDoc.hideDocumentButtonBar || this._isRounding || this._isRotating;
+ const hideDocumentButtonBar = hideDecorations || seldocview.props.hideDocumentButtonBar || seldocview.rootDoc.layout_hideDocumentButtonBar || this._isRounding || this._isRotating;
// if multiple documents have been opened at the same time, then don't show open button
const hideOpenButton =
hideDecorations ||
seldocview.props.hideOpenButton ||
- seldocview.rootDoc.hideOpenButton ||
- SelectionManager.Views().some(docView => docView.props.Document._stayInCollection || docView.props.Document.isGroup || docView.props.Document.hideOpenButton) ||
+ seldocview.rootDoc.layout_hideOpenButton ||
+ SelectionManager.Views().some(docView => docView.rootDoc._dragOnlyWithinContainer || docView.rootDoc.isGroup || docView.rootDoc.layout_hideOpenButton) ||
this._isRounding ||
this._isRotating;
const hideDeleteButton =
@@ -764,26 +792,12 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
seldocview.props.hideDeleteButton ||
seldocview.rootDoc.hideDeleteButton ||
SelectionManager.Views().some(docView => {
- const collectionAcl = docView.props.docViewPath()?.lastElement() ? GetEffectiveAcl(docView.props.docViewPath().lastElement().rootDoc[DataSym]) : AclEdit;
- return (docView.rootDoc.stayInCollection && !docView.rootDoc.timelineLabel) || (collectionAcl !== AclAdmin && collectionAcl !== AclEdit && GetEffectiveAcl(docView.rootDoc) !== AclAdmin);
+ const collectionAcl = docView.props.docViewPath()?.lastElement() ? GetEffectiveAcl(docView.props.docViewPath().lastElement().rootDoc[DocData]) : AclEdit;
+ return collectionAcl !== AclAdmin && collectionAcl !== AclEdit && GetEffectiveAcl(docView.rootDoc) !== AclAdmin;
});
-
const topBtn = (key: string, icon: string, pointerDown: undefined | ((e: React.PointerEvent) => void), click: undefined | ((e: any) => void), title: string) => (
<Tooltip key={key} title={<div className="dash-tooltip">{title}</div>} placement="top">
- <div
- className={`documentDecorations-${key}Button`}
- onContextMenu={e => e.preventDefault()}
- onPointerDown={
- pointerDown ??
- (e =>
- setupMoveUpEvents(
- this,
- e,
- returnFalse,
- emptyFunction,
- undoBatch(e => click!(e))
- ))
- }>
+ <div className={`documentDecorations-${key}Button`} onContextMenu={e => e.preventDefault()} onPointerDown={pointerDown ?? (e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, e => click!(e)))}>
<FontAwesomeIcon icon={icon as any} />
</div>
</Tooltip>
@@ -807,12 +821,33 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
// Radius constants
const useRounding = seldocview.ComponentView instanceof ImageBox || seldocview.ComponentView instanceof FormattedTextBox || seldocview.ComponentView instanceof CollectionFreeFormView;
- const borderRadius = numberValue(StrCast(seldocview.rootDoc.borderRounding));
+ const borderRadius = numberValue(StrCast(seldocview.rootDoc.layout_borderRounding));
const docMax = Math.min(NumCast(seldocview.rootDoc.width) / 2, NumCast(seldocview.rootDoc.height) / 2);
const maxDist = Math.min((this.Bounds.r - this.Bounds.x) / 2, (this.Bounds.b - this.Bounds.y) / 2);
const radiusHandle = (borderRadius / docMax) * maxDist;
const radiusHandleLocation = Math.min(radiusHandle, maxDist);
+ const sharingMenu = docShareMode ? (
+ <div className="documentDecorations-share">
+ <div className={`documentDecorations-share${shareMode}`}>
+ &nbsp;
+ {shareSymbolIcon + ' ' + shareMode}
+ &nbsp;
+ {/* {!Doc.noviceMode ? (
+ <div className="checkbox">
+ <div className="checkbox-box">
+ <input type="checkbox" checked={this.showLayoutAcl} onChange={action(() => (this.showLayoutAcl = !this.showLayoutAcl))} />
+ </div>
+ <div className="checkbox-text"> Layout </div>
+ </div>
+ ) : null}
+ &nbsp; */}
+ </div>
+ </div>
+ ) : (
+ <div />
+ );
+
const titleArea = this._editingTitle ? (
<input
ref={this._keyinput}
@@ -827,8 +862,18 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
onPointerDown={e => e.stopPropagation()}
/>
) : (
- <div className="documentDecorations-title" key="title" onPointerDown={this.onTitleDown}>
- <span className={`documentDecorations-titleSpan${colorScheme}`}>{`${hideTitle ? '' : this.selectionTitle}`}</span>
+ <div
+ className="documentDecorations-title"
+ key="title"
+ onPointerDown={e => {
+ e.stopPropagation;
+ }}>
+ {hideTitle ? null : (
+ <span className={`documentDecorations-titleSpan${colorScheme}`} onPointerDown={this.onTitleDown}>
+ {this.selectionTitle}
+ </span>
+ )}
+ {sharingMenu}
{!useLock ? null : (
<Tooltip key="lock" title={<div className="dash-tooltip">toggle ability to interact with document</div>} placement="top">
<div className="documentDecorations-lock" style={{ color: seldocview.rootDoc._lockedPosition ? 'red' : undefined }} onPointerDown={this.onLockDown} onContextMenu={e => e.preventDefault()}>
@@ -838,6 +883,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
)}
</div>
);
+
return (
<div className={`documentDecorations${colorScheme}`} style={{ opacity: this._showNothing ? 0.1 : undefined }}>
<div
@@ -867,11 +913,17 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
width: bounds.r - bounds.x + this._resizeBorderWidth + 'px',
height: bounds.b - bounds.y + this._resizeBorderWidth + this._titleHeight + 'px',
}}>
- <div className="documentDecorations-topbar" style={{ display: hideDeleteButton && hideTitle && hideOpenButton ? 'none' : undefined }} onPointerDown={this.onContainerDown}>
+ <div
+ className="documentDecorations-topbar"
+ style={{
+ color: 'black',
+ display: hideDeleteButton && hideTitle && hideOpenButton ? 'none' : undefined,
+ }}
+ onPointerDown={this.onContainerDown}>
{hideDeleteButton ? null : topBtn('close', 'times', undefined, e => this.onCloseClick(true), 'Close')}
{hideResizers || hideDeleteButton ? null : topBtn('minimize', 'window-maximize', undefined, e => this.onCloseClick(undefined), 'Minimize')}
- {hideTitle ? null : titleArea}
- {hideOpenButton ? null : topBtn('open', 'external-link-alt', this.onMaximizeDown, undefined, 'Open in Lightbox (ctrl: as new embedding, shift: in new collection)')}
+ {titleArea}
+ {hideOpenButton ? <div /> : topBtn('open', 'external-link-alt', this.onMaximizeDown, undefined, 'Open in Lightbox (ctrl: as alias, shift: in new collection)')}
</div>
{hideResizers ? null : (
<>
@@ -927,7 +979,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
}}>
{this._isRotating ? null : (
<Tooltip enterDelay={750} title={<div className="dash-tooltip">tap to set rotate center, drag to rotate</div>}>
- <div className="documentDecorations-rotation" style={{ pointerEvents: 'all' }} onPointerDown={this.onRotateDown} onContextMenu={e => e.preventDefault()}>
+ <div className="documentDecorations-rotation" style={{ pointerEvents: 'all', color: 'blue' }} onPointerDown={this.onRotateDown} onContextMenu={e => e.preventDefault()}>
<IconButton icon={<FaUndo />} color={Colors.LIGHT_GRAY} />
</div>
</Tooltip>
diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx
index 6b4132814..d60617020 100644
--- a/src/client/views/EditableView.tsx
+++ b/src/client/views/EditableView.tsx
@@ -6,6 +6,7 @@ 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 {
/**
@@ -156,12 +157,14 @@ export class EditableView extends React.Component<EditableProps> {
break;
case ':':
if (this.props.menuCallback) {
+ e.stopPropagation();
this.props.menuCallback(e.currentTarget.getBoundingClientRect().x, e.currentTarget.getBoundingClientRect().y);
break;
}
default:
if (this.props.textCallback?.(e.key)) {
+ e.stopPropagation();
this._editing = false;
this.props.isEditingCallback?.(false);
}
@@ -229,7 +232,7 @@ export class EditableView extends React.Component<EditableProps> {
onChange: this.props.autosuggestProps.onChange,
}}
/>
- ) : this.props.oneLine !== false && this.props.GetValue()?.toString().indexOf('\n') === -1 ? (
+ ) : (
<input
className="editableView-input"
ref={r => (this._inputref = r)}
@@ -245,23 +248,24 @@ export class EditableView extends React.Component<EditableProps> {
onClick={this.stopPropagation}
onPointerUp={this.stopPropagation}
/>
- ) : (
- <textarea
- className="editableView-input"
- ref={r => (this._inputref = r)}
- style={{ display: this.props.display, overflow: 'auto', fontSize: this.props.fontSize, minHeight: `min(100%, ${(this.props.GetValue()?.split('\n').length || 1) * 15})`, minWidth: 20, background: this.props.background }}
- placeholder={this.props.placeholder}
- onBlur={e => this.finalizeEdit(e.currentTarget.value, false, true, false)}
- defaultValue={this.props.GetValue()}
- autoFocus={true}
- onChange={this.onChange}
- onKeyDown={this.onKeyDown}
- onKeyPress={this.stopPropagation}
- onPointerDown={this.stopPropagation}
- onClick={this.stopPropagation}
- onPointerUp={this.stopPropagation}
- />
);
+ // ) : (
+ // <textarea
+ // className="editableView-input"
+ // ref={r => (this._inputref = r)}
+ // style={{ display: this.props.display, overflow: 'auto', fontSize: this.props.fontSize, minHeight: `min(100%, ${(this.props.GetValue()?.split('\n').length || 1) * 15})`, minWidth: 20, background: this.props.background }}
+ // placeholder={this.props.placeholder}
+ // onBlur={e => this.finalizeEdit(e.currentTarget.value, false, true, false)}
+ // defaultValue={this.props.GetValue()}
+ // autoFocus={true}
+ // onChange={this.onChange}
+ // onKeyDown={this.onKeyDown}
+ // onKeyPress={this.stopPropagation}
+ // onPointerDown={this.stopPropagation}
+ // onClick={this.stopPropagation}
+ // onPointerUp={this.stopPropagation}
+ // />
+ // );
}
render() {
diff --git a/src/client/views/FilterPanel.scss b/src/client/views/FilterPanel.scss
index c903f29ee..4f0460659 100644
--- a/src/client/views/FilterPanel.scss
+++ b/src/client/views/FilterPanel.scss
@@ -156,7 +156,7 @@
right: 0;
top: 0;
z-index: 1;
- background-color: #9f9f9f;
+ // background-color: #9f9f9f;
.filterBox-tree {
z-index: 0;
@@ -182,6 +182,7 @@
}
.filterBox-tree {
+ padding-top: 8px;
display: inline-block;
width: 100%;
margin-bottom: 10px;
@@ -189,3 +190,23 @@
overflow: auto;
}
}
+
+
+
+.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;
+ }
+
+}
+
+
+
diff --git a/src/client/views/FilterPanel.tsx b/src/client/views/FilterPanel.tsx
index 75e0e7c4c..63bd01b19 100644
--- a/src/client/views/FilterPanel.tsx
+++ b/src/client/views/FilterPanel.tsx
@@ -10,6 +10,9 @@ import { UserOptions } from '../util/GroupManager';
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';
interface filterProps {
rootDoc: Doc;
@@ -31,7 +34,7 @@ export class FilterPanel extends React.Component<filterProps> {
return targetView?.ComponentView?.annotationKey ?? targetView?.ComponentView?.fieldKey ?? 'data';
}
@computed get targetDocChildren() {
- return DocListCast(this.targetDoc?.[this.targetDocChildKey] || Doc.ActiveDashboard?.data);
+ return [...DocListCast(this.targetDoc?.[this.targetDocChildKey] || Doc.ActiveDashboard?.data), ...DocListCast(this.targetDoc[Doc.LayoutFieldKey(this.targetDoc) + '_sidebar'])];
}
@computed get allDocs() {
@@ -63,14 +66,14 @@ export class FilterPanel extends React.Component<filterProps> {
* The current attributes selected to filter based on
*/
@computed get activeFilters() {
- return StrListCast(this.targetDoc?._docFilters);
+ return StrListCast(this.targetDoc?._childFilters);
}
/**
* @returns a string array of the current attributes
*/
@computed get currentFacets() {
- return this.activeFilters.map(filter => filter.split(':')[0]);
+ return this.activeFilters.map(filter => filter.split(Doc.FilterSep)[0]);
}
gatherFieldValues(childDocs: Doc[], facetKey: string) {
@@ -89,6 +92,7 @@ export class FilterPanel extends React.Component<filterProps> {
const fieldKey = Doc.LayoutFieldKey(t);
const annos = !Field.toString(Doc.LayoutField(t) as Field).includes('CollectionView');
DocListCast(t[annos ? fieldKey + '_annotations' : fieldKey]).forEach(newdoc => newarray.push(newdoc));
+ annos && DocListCast(t[fieldKey + '_sidebar']).forEach(newdoc => newarray.push(newdoc));
});
subDocs = newarray;
}
@@ -106,8 +110,8 @@ export class FilterPanel extends React.Component<filterProps> {
@observable _chosenFacets = new ObservableMap<string, 'text' | 'checkbox' | 'slider' | 'range'>();
@computed get activeFacets() {
const facets = new Map<string, 'text' | 'checkbox' | 'slider' | 'range'>(this._chosenFacets);
- StrListCast(this.targetDoc?._docFilters).map(filter => facets.set(filter.split(':')[0], filter.split(':')[2] === 'match' ? 'text' : 'checkbox'));
- setTimeout(() => StrListCast(this.targetDoc?._docFilters).map(action(filter => this._chosenFacets.set(filter.split(':')[0], filter.split(':')[2] === 'match' ? 'text' : 'checkbox'))));
+ StrListCast(this.targetDoc?._childFilters).map(filter => facets.set(filter.split(Doc.FilterSep)[0], filter.split(Doc.FilterSep)[2] === 'match' ? 'text' : 'checkbox'));
+ setTimeout(() => StrListCast(this.targetDoc?._childFilters).map(action(filter => this._chosenFacets.set(filter.split(Doc.FilterSep)[0], filter.split(Doc.FilterSep)[2] === 'match' ? 'text' : 'checkbox'))));
return facets;
}
/**
@@ -171,21 +175,31 @@ export class FilterPanel extends React.Component<filterProps> {
<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} />
</div>
- <div className="filterBox-select-bool">
- <select className="filterBox-selection" onChange={action(e => this.targetDoc && (this.targetDoc._filterBoolean = (e.target as any).value))} defaultValue={StrCast(this.targetDoc?.filterBoolean)}>
+ {/* THE FOLLOWING CODE SHOULD BE DEVELOPER FOR BOOLEAN EXPRESSION (AND / OR) */}
+ {/* <div className="filterBox-select-bool">
+ <select className="filterBox-selection" onChange={action(e => this.targetDoc && (this.targetDoc._childFilters_boolean = (e.target as any).value))} defaultValue={StrCast(this.targetDoc?.childFilters_boolean)}>
{['AND', 'OR'].map(bool => (
<option value={bool} key={bool}>
{bool}
</option>
))}
</select>
- </div>{' '}
+ </div>{' '} */}
</div>
<div className="filterBox-tree" key="tree">
{Array.from(this.activeFacets.keys()).map(facetHeader => (
<div>
- {facetHeader}
+ <div className="filterBox-facetHeader">
+ <div className="filterBox-facetHeader-Header"> </div>
+ {facetHeader.charAt(0).toUpperCase() + facetHeader.slice(1)}
+
+ <div className="filterBox-facetHeader-collapse">
+ <AiOutlineMinusSquare />
+ {/* <CiCircleRemove/> */}
+ </div>
+ </div>
+
{this.displayFacetValueFilterUIs(this.activeFacets.get(facetHeader), facetHeader)}
</div>
))}
@@ -200,11 +214,12 @@ export class FilterPanel extends React.Component<filterProps> {
return (
<input
placeholder={
- StrListCast(this.targetDoc._docFilters)
- .find(filter => filter.split(':')[0] === facetHeader)
- ?.split(':')[1] ?? '-empty-'
+ StrListCast(this.targetDoc._childFilters)
+ .find(filter => filter.split(Doc.FilterSep)[0] === facetHeader)
+ ?.split(Doc.FilterSep)[1] ?? '-empty-'
}
- onKeyDown={e => e.key === 'Enter' && Doc.setDocFilter(this.targetDoc, facetHeader, e.currentTarget.value, !e.currentTarget.value ? 'remove' : 'match')}
+ onBlur={undoable(e => Doc.setDocFilter(this.targetDoc, facetHeader, e.currentTarget.value, !e.currentTarget.value ? 'remove' : 'match'), 'set text filter')}
+ onKeyDown={e => e.key === 'Enter' && undoable(e => Doc.setDocFilter(this.targetDoc, facetHeader, e.currentTarget.value, !e.currentTarget.value ? 'remove' : 'match'), 'set text filter')(e)}
/>
);
case 'checkbox':
@@ -215,12 +230,12 @@ export class FilterPanel extends React.Component<filterProps> {
<input
style={{ width: 20, marginLeft: 20 }}
checked={
- StrListCast(this.targetDoc._docFilters)
- .find(filter => filter.split(':')[0] === facetHeader && filter.split(':')[1] == facetValue)
- ?.split(':')[2] === 'check'
+ StrListCast(this.targetDoc._childFilters)
+ .find(filter => filter.split(Doc.FilterSep)[0] === facetHeader && filter.split(Doc.FilterSep)[1] == facetValue)
+ ?.split(Doc.FilterSep)[2] === 'check'
}
type={type}
- onChange={e => Doc.setDocFilter(this.targetDoc, facetHeader, fval, e.target.checked ? 'check' : 'remove')}
+ onChange={undoable(e => Doc.setDocFilter(this.targetDoc, facetHeader, fval, e.target.checked ? 'check' : 'remove'), 'set filter')}
/>
{facetValue}
</div>
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index 141e99c66..35d6d73e4 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -2,20 +2,19 @@ import React = require('react');
import * as fitCurve from 'fit-curve';
import { action, computed, observable, runInAction } from 'mobx';
import { observer } from 'mobx-react';
+import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnTrue, setupMoveUpEvents } from '../../Utils';
import { Doc, Opt } from '../../fields/Doc';
import { InkData, InkTool } from '../../fields/InkField';
-import { ScriptField } from '../../fields/ScriptField';
-import { Cast, FieldValue, NumCast } from '../../fields/Types';
+import { NumCast } from '../../fields/Types';
import MobileInkOverlay from '../../mobile/MobileInkOverlay';
import { GestureUtils } from '../../pen-gestures/GestureUtils';
import { MobileInkOverlayContent } from '../../server/Message';
-import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnTrue, setupMoveUpEvents } from '../../Utils';
import { CognitiveServices } from '../cognitive_services/CognitiveServices';
-import { Docs, DocUtils } from '../documents/Documents';
import { InteractionUtils } from '../util/InteractionUtils';
import { ScriptingGlobals } from '../util/ScriptingGlobals';
import { Transform } from '../util/Transform';
import './GestureOverlay.scss';
+import { InkTranscription } from './InkTranscription';
import {
ActiveArrowEnd,
ActiveArrowScale,
@@ -31,13 +30,11 @@ import {
SetActiveInkColor,
SetActiveInkWidth,
} from './InkingStroke';
-import { InkTranscription } from './InkTranscription';
-import { checkInksToGroup } from './nodes/button/FontIconBox';
+import TouchScrollableMenu, { TouchScrollableMenuItem } from './TouchScrollableMenu';
+import { Touchable } from './Touchable';
+import { checkInksToGroup } from './global/globalScripts';
import { DocumentView } from './nodes/DocumentView';
import { RadialMenu } from './nodes/RadialMenu';
-import HorizontalPalette from './Palette';
-import { Touchable } from './Touchable';
-import TouchScrollableMenu, { TouchScrollableMenuItem } from './TouchScrollableMenu';
interface GestureOverlayProps {
isActive: boolean;
@@ -79,7 +76,6 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
private _overlayRef = React.createRef<HTMLDivElement>();
private _d1: Doc | undefined;
private _inkToTextDoc: Doc | undefined;
- private _thumbDoc: Doc | undefined;
private thumbIdentifier?: number;
private pointerIdentifier?: number;
private _hands: Map<number, React.Touch[]> = new Map<number, React.Touch[]>();
@@ -93,78 +89,12 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
GestureOverlay.Instances.push(this);
}
- static setupThumbButtons(doc: Doc) {
- const docProtoData: { title: string; icon: string; drag?: string; toolType?: string; ignoreClick?: boolean; pointerDown?: string; pointerUp?: string; clipboard?: Doc; backgroundColor?: string; dragFactory?: Doc }[] = [
- { title: 'use pen', icon: 'pen-nib', pointerUp: 'resetPen()', pointerDown: 'setPen(2, this.backgroundColor)', backgroundColor: 'blue' },
- { title: 'use highlighter', icon: 'highlighter', pointerUp: 'resetPen()', pointerDown: 'setPen(20, this.backgroundColor)', backgroundColor: 'yellow' },
- {
- title: 'notepad',
- icon: 'clipboard',
- pointerUp: 'GestureOverlay.Instance.closeFloatingDoc()',
- pointerDown: 'GestureOverlay.Instance.openFloatingDoc(this.clipboard)',
- clipboard: Docs.Create.FreeformDocument([], { _width: 300, _height: 300, isSystem: true }),
- backgroundColor: 'orange',
- },
- { title: 'interpret text', icon: 'font', toolType: 'inktotext', pointerUp: "setToolglass('none')", pointerDown: 'setToolglass(self.toolType)', backgroundColor: 'orange' },
- { title: 'ignore gestures', icon: 'signature', toolType: 'ignoregesture', pointerUp: "setToolglass('none')", pointerDown: 'setToolglass(self.toolType)', backgroundColor: 'green' },
- ];
- return docProtoData.map(data =>
- Docs.Create.FontIconDocument({
- _nativeWidth: 10,
- _nativeHeight: 10,
- _width: 10,
- _height: 10,
- title: data.title,
- icon: data.icon,
- toolType: data.toolType,
- _dropAction: data.pointerDown ? 'copy' : undefined,
- ignoreClick: data.ignoreClick,
- onDragStart: data.drag ? ScriptField.MakeFunction(data.drag) : undefined,
- clipboard: data.clipboard,
- onPointerUp: data.pointerUp ? ScriptField.MakeScript(data.pointerUp) : undefined,
- onPointerDown: data.pointerDown ? ScriptField.MakeScript(data.pointerDown) : undefined,
- backgroundColor: data.backgroundColor,
- dragFactory: data.dragFactory,
- isSystem: true,
- })
- );
- }
-
- static setupThumbDoc(userDoc: Doc) {
- if (!userDoc.thumbDoc) {
- const thumbDoc = Docs.Create.LinearDocument(GestureOverlay.setupThumbButtons(userDoc), {
- _width: 100,
- _height: 50,
- ignoreClick: true,
- _lockedPosition: true,
- title: 'buttons',
- _layout_autoHeight: true,
- _yMargin: 5,
- linearViewIsExpanded: true,
- backgroundColor: 'white',
- isSystem: true,
- });
- thumbDoc.inkToTextDoc = Docs.Create.LinearDocument([], {
- _width: 300,
- _height: 25,
- _layout_autoHeight: true,
- linearViewIsExpanded: true,
- flexDirection: 'column',
- isSystem: true,
- });
- userDoc.thumbDoc = thumbDoc;
- }
- return Cast(userDoc.thumbDoc, Doc);
- }
-
componentWillUnmount() {
GestureOverlay.Instances.splice(GestureOverlay.Instances.indexOf(this), 1);
GestureOverlay.Instance = GestureOverlay.Instances.lastElement();
}
componentDidMount = () => {
GestureOverlay.Instance = this;
- this._thumbDoc = FieldValue(Cast(GestureOverlay.setupThumbDoc(Doc.UserDoc()), Doc));
- this._inkToTextDoc = FieldValue(Cast(this._thumbDoc?.inkToTextDoc, Doc));
};
// TODO: nda - add dragging groups with one finger drag and have to click into group to scroll within the group
@@ -395,24 +325,6 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
this.thumbIdentifier = thumb?.identifier;
this._hands.set(thumb.identifier, fingers);
- const others = fingers.filter(f => f !== thumb);
- const minX = Math.min(...others.map(f => f.clientX));
- const minY = Math.min(...others.map(f => f.clientY));
-
- // load up the palette collection around the thumb
- const thumbDoc = await Cast(GestureOverlay.setupThumbDoc(Doc.UserDoc()), Doc);
- if (thumbDoc) {
- runInAction(() => {
- RadialMenu.Instance._display = false;
- this._inkToTextDoc = FieldValue(Cast(thumbDoc.inkToTextDoc, Doc));
- this._thumbDoc = thumbDoc;
- this._thumbX = thumb.clientX;
- this._thumbY = thumb.clientY;
- this._menuX = thumb.clientX + 50;
- this._menuY = thumb.clientY;
- this._palette = <HorizontalPalette key="palette" x={minX} y={minY} thumb={[thumb.clientX, thumb.clientY]} thumbDoc={thumbDoc} />;
- });
- }
this.removeMoveListeners();
document.removeEventListener('touchmove', this.handleHandMove);
@@ -462,11 +374,6 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
if (Math.abs(pt.clientY - this._thumbY) > 10 * window.devicePixelRatio) {
this._selectedIndex = Math.min(Math.max(-1, -Math.ceil((pt.clientY - this._thumbY) / (10 * window.devicePixelRatio)) - 1), this._possibilities.length - 1);
}
- } else if (this._thumbDoc) {
- if (Math.abs(pt.clientX - this._thumbX) > 15 * window.devicePixelRatio) {
- this._thumbDoc.selectedIndex = Math.max(-1, NumCast(this._thumbDoc.selectedIndex) - Math.sign(pt.clientX - this._thumbX));
- this._thumbX = pt.clientX;
- }
}
}
}
@@ -485,7 +392,6 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
if (this.thumbIdentifier) this._hands.delete(this.thumbIdentifier);
this._palette = undefined;
this.thumbIdentifier = undefined;
- this._thumbDoc = undefined;
// this chunk of code is for handling the ink to text toolglass
let scriptWorked = false;
@@ -1013,8 +919,8 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
focus={emptyFunction}
whenChildContentsActiveChanged={emptyFunction}
bringToFront={emptyFunction}
- docRangeFilters={returnEmptyFilter}
- docFilters={returnEmptyFilter}
+ childFiltersByRanges={returnEmptyFilter}
+ childFilters={returnEmptyFilter}
searchFilterDocs={returnEmptyDoclist}
/>
);
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index b9b92dd2b..7b693c8da 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -1,22 +1,18 @@
import { random } from 'lodash';
import { action, runInAction } from 'mobx';
-import { DateField } from '../../fields/DateField';
import { Doc, DocListCast } from '../../fields/Doc';
import { Id } from '../../fields/FieldSymbols';
import { InkTool } from '../../fields/InkField';
-import { List } from '../../fields/List';
import { ScriptField } from '../../fields/ScriptField';
import { Cast, PromiseValue } from '../../fields/Types';
import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager';
-import { DocServer } from '../DocServer';
-import { DocumentType } from '../documents/DocumentTypes';
import { DragManager } from '../util/DragManager';
import { GroupManager } from '../util/GroupManager';
import { SelectionManager } from '../util/SelectionManager';
import { SettingsManager } from '../util/SettingsManager';
import { SharingManager } from '../util/SharingManager';
import { SnappingManager } from '../util/SnappingManager';
-import { undoBatch, UndoManager } from '../util/UndoManager';
+import { UndoManager } from '../util/UndoManager';
import { CollectionDockingView } from './collections/CollectionDockingView';
import { CollectionFreeFormView } from './collections/collectionFreeForm';
import { CollectionFreeFormViewChrome } from './collections/CollectionMenu';
@@ -161,12 +157,10 @@ export class KeyManager {
case 'delete':
case 'backspace':
if (document.activeElement?.tagName !== 'INPUT' && document.activeElement?.tagName !== 'TEXTAREA') {
- UndoManager.RunInBatch(() => {
- if (LightboxView.LightboxDoc) {
- LightboxView.SetLightboxDoc(undefined);
- SelectionManager.DeselectAll();
- } else DocumentDecorations.Instance.onCloseClick(true);
- }, 'backspace');
+ if (LightboxView.LightboxDoc) {
+ LightboxView.SetLightboxDoc(undefined);
+ SelectionManager.DeselectAll();
+ } else DocumentDecorations.Instance.onCloseClick(true);
return { stopPropagation: true, preventDefault: true };
}
break;
@@ -267,6 +261,18 @@ export class KeyManager {
case 't':
PromiseValue(Cast(Doc.UserDoc()['tabs-button-tools'], Doc)).then(pv => pv && (pv.onClick as ScriptField).script.run({ this: pv }));
break;
+ case 'i':
+ const importBtn = DocListCast(Doc.MyLeftSidebarMenu.data).find(d => d.target === Doc.MyImports);
+ if (importBtn) {
+ MainView.Instance.selectMenu(importBtn);
+ }
+ break;
+ case 's':
+ const trailsBtn = DocListCast(Doc.MyLeftSidebarMenu.data).find(d => d.target === Doc.MyTrails);
+ if (trailsBtn) {
+ MainView.Instance.selectMenu(trailsBtn);
+ }
+ break;
case 'f':
if (SelectionManager.Views().length === 1 && SelectionManager.Views()[0].ComponentView?.search) {
SelectionManager.Views()[0].ComponentView?.search?.('', false, false);
@@ -278,26 +284,26 @@ export class KeyManager {
}
break;
case 'e':
- Doc.ActiveTool = InkTool.Eraser;
+ Doc.ActiveTool = (Doc.ActiveTool === InkTool.Eraser ? InkTool.None : InkTool.Eraser);
break;
case 'p':
- Doc.ActiveTool = InkTool.Pen;
- break;
- case 'o':
- const target = SelectionManager.Docs().lastElement();
- target && CollectionDockingView.OpenFullScreen(target);
+ Doc.ActiveTool = (Doc.ActiveTool === InkTool.Pen ? InkTool.None : InkTool.Pen);
break;
case 'r':
preventDefault = false;
break;
case 'y':
- SelectionManager.DeselectAll();
- UndoManager.Redo();
+ if (Doc.ActivePage !== 'home') {
+ SelectionManager.DeselectAll();
+ UndoManager.Redo();
+ }
stopPropagation = false;
break;
case 'z':
- SelectionManager.DeselectAll();
- UndoManager.Undo();
+ if (Doc.ActivePage !== 'home') {
+ SelectionManager.DeselectAll();
+ UndoManager.Undo();
+ }
stopPropagation = false;
break;
case 'a':
@@ -352,38 +358,13 @@ export class KeyManager {
});
public paste(e: ClipboardEvent) {
- const plain = e.clipboardData?.getData('text/plain');
- const clone = plain?.startsWith('__DashCloneId(');
- if (plain && (plain.startsWith('__DashDocId(') || clone)) {
- const first = SelectionManager.Views().length ? SelectionManager.Views()[0] : undefined;
- if (first?.props.Document.type === DocumentType.COL) {
- const docids = plain.split(':');
- let count = 1;
- const list: Doc[] = [];
- const targetDataDoc = Doc.GetProto(first.props.Document);
- const fieldKey = first.LayoutFieldKey;
- const docList = DocListCast(targetDataDoc[fieldKey]);
- docids.map(
- (did, i) =>
- i &&
- DocServer.GetRefField(did).then(async doc => {
- count++;
- if (doc instanceof Doc) {
- list.push(doc);
- }
- if (count === docids.length) {
- const added = await Promise.all(list.filter(d => !docList.includes(d)).map(async d => (clone ? (await Doc.MakeClone(d, true)).clone : d)));
- if (added.length) {
- added.map(doc => (doc.embedContainer = targetDataDoc));
- undoBatch(() => {
- targetDataDoc[fieldKey] = new List<Doc>([...docList, ...added]);
- targetDataDoc[fieldKey + '_modificationDate'] = new DateField(new Date(Date.now()));
- })();
- }
- }
- })
- );
- }
+ const plain = e.clipboardData?.getData('text/plain'); // list of document ids, separated by ':'s
+ if (!plain) return;
+ const clone = plain.startsWith('__DashCloneId(');
+ const docids = plain.split(':'); // hack! docids[0] is the top left of the selection rectangle
+ const addDocument = SelectionManager.Views().lastElement()?.ComponentView?.addDocument;
+ if (addDocument && (plain.startsWith('__DashDocId(') || clone)) {
+ Doc.Paste(docids.slice(1), clone, addDocument);
}
}
diff --git a/src/client/views/InkControlPtHandles.tsx b/src/client/views/InkControlPtHandles.tsx
index 9447b2e72..07e3270b1 100644
--- a/src/client/views/InkControlPtHandles.tsx
+++ b/src/client/views/InkControlPtHandles.tsx
@@ -182,8 +182,8 @@ export interface InkEndProps {
inkDoc: Doc;
inkView: InkingStroke;
screenSpaceLineWidth: number;
- startPt: PointData;
- endPt: PointData;
+ startPt: () => PointData;
+ endPt: () => PointData;
}
@observer
export class InkEndPtHandles extends React.Component<InkEndProps> {
@@ -191,29 +191,31 @@ export class InkEndPtHandles extends React.Component<InkEndProps> {
@observable _overEnd: boolean = false;
@action
- dragRotate = (e: React.PointerEvent, p1: () => { X: number; Y: number }, p2: () => { X: number; Y: number }) => {
+ dragRotate = (e: React.PointerEvent, pt1: () => { X: number; Y: number }, pt2: () => { X: number; Y: number }) => {
setupMoveUpEvents(
this,
e,
action(e => {
if (!this.props.inkView.controlUndo) this.props.inkView.controlUndo = UndoManager.StartBatch('stretch ink');
// compute stretch factor by finding scaling along axis between start and end points
- const v1 = { X: p1().X - p2().X, Y: p1().Y - p2().Y };
- const v2 = { X: e.clientX - p2().X, Y: e.clientY - p2().Y };
+ const p1 = pt1();
+ const p2 = pt2();
+ const v1 = { X: p1.X - p2.X, Y: p1.Y - p2.Y };
+ const v2 = { X: e.clientX - p2.X, Y: e.clientY - p2.Y };
const v1len = Math.sqrt(v1.X * v1.X + v1.Y * v1.Y);
const v2len = Math.sqrt(v2.X * v2.X + v2.Y * v2.Y);
const scaling = v2len / v1len;
const v1n = { X: v1.X / v1len, Y: v1.Y / v1len };
const v2n = { X: v2.X / v2len, Y: v2.Y / v2len };
const angle = Math.acos(v1n.X * v2n.X + v1n.Y * v2n.Y) * Math.sign(v1.X * v2.Y - v2.X * v1.Y);
- InkStrokeProperties.Instance.stretchInk(SelectionManager.Views(), scaling, p2(), v1n, e.shiftKey);
- InkStrokeProperties.Instance.rotateInk(SelectionManager.Views(), angle, p2());
+ InkStrokeProperties.Instance.stretchInk(SelectionManager.Views(), scaling, p2, v1n, e.shiftKey);
+ InkStrokeProperties.Instance.rotateInk(SelectionManager.Views(), angle, pt2()); // bcz: call pt2() func here because pt2 will have changed from previous stretchInk call
return false;
}),
action(() => {
this.props.inkView.controlUndo?.end();
this.props.inkView.controlUndo = undefined;
- UndoManager.FilterBatches(['data', 'x', 'y', 'width', 'height']);
+ UndoManager.FilterBatches(['stroke', 'x', 'y', 'width', 'height']);
}),
returnFalse
);
@@ -237,20 +239,8 @@ export class InkEndPtHandles extends React.Component<InkEndProps> {
);
return (
<svg>
- {hdl('start', this.props.startPt, (e: React.PointerEvent) =>
- this.dragRotate(
- e,
- () => this.props.startPt,
- () => this.props.endPt
- )
- )}
- {hdl('end', this.props.endPt, (e: React.PointerEvent) =>
- this.dragRotate(
- e,
- () => this.props.endPt,
- () => this.props.startPt
- )
- )}
+ {hdl('start', this.props.startPt(), (e: React.PointerEvent) => this.dragRotate(e, this.props.startPt, this.props.endPt))}
+ {hdl('end', this.props.endPt(), (e: React.PointerEvent) => this.dragRotate(e, this.props.endPt, this.props.startPt))}
</svg>
);
}
diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts
index d28981e17..abc4381a6 100644
--- a/src/client/views/InkStrokeProperties.ts
+++ b/src/client/views/InkStrokeProperties.ts
@@ -202,7 +202,6 @@ export class InkStrokeProperties {
@action
rotateInk = (inkStrokes: DocumentView[], angle: number, scrpt: PointData) => {
this.applyFunction(inkStrokes, (view: DocumentView, ink: InkData, xScale: number, yScale: number, inkStrokeWidth: number) => {
- view.rootDoc.rotation = NumCast(view.rootDoc.rotation) + angle;
const inkCenterPt = view.ComponentView?.ptFromScreen?.(scrpt);
return !inkCenterPt
? ink
diff --git a/src/client/views/InkTranscription.tsx b/src/client/views/InkTranscription.tsx
index d6a6a64bf..6c213f40f 100644
--- a/src/client/views/InkTranscription.tsx
+++ b/src/client/views/InkTranscription.tsx
@@ -1,7 +1,8 @@
import * as iink from 'iink-js';
import { action, observable } from 'mobx';
import * as React from 'react';
-import { Doc, DocListCast, HeightSym, WidthSym } from '../../fields/Doc';
+import { Doc, DocListCast } from '../../fields/Doc';
+import { Height, Width } from '../../fields/DocSymbols';
import { InkData, InkField, InkTool } from '../../fields/InkField';
import { Cast, DateCast, NumCast } from '../../fields/Types';
import { aggregateBounds } from '../../Utils';
@@ -104,7 +105,7 @@ export class InkTranscription extends React.Component {
: null;
}
- r.addEventListener('exported', (e: any) => this.exportInk(e, this._textRef));
+ r?.addEventListener('exported', (e: any) => this.exportInk(e, this._textRef));
return (this._textRef = r);
};
@@ -123,12 +124,12 @@ export class InkTranscription extends React.Component {
const strokes: InkData[] = [];
const times: number[] = [];
validInks
- .filter(i => Cast(i.data, InkField))
+ .filter(i => Cast(i[Doc.LayoutFieldKey(i)], InkField))
.forEach(i => {
- const d = Cast(i.data, InkField, null);
+ const d = Cast(i[Doc.LayoutFieldKey(i)], InkField, null);
const inkStroke = DocumentManager.Instance.getDocumentView(i)?.ComponentView as InkingStroke;
strokes.push(d.inkData.map(pd => inkStroke.ptToScreen({ X: pd.X, Y: pd.Y })));
- times.push(DateCast(i.creationDate).getDate().getTime());
+ times.push(DateCast(i.author_date).getDate().getTime());
});
this.currGroup = groupDoc;
@@ -221,7 +222,7 @@ export class InkTranscription extends React.Component {
const docList = DocListCast(this.currGroup.data);
docList.forEach((inkDoc: Doc) => {
// just having the times match up and be a unique value (actual timestamp doesn't matter)
- const ms = DateCast(inkDoc.creationDate).getDate().getTime() + 14400000;
+ const ms = DateCast(inkDoc.author_date).getDate().getTime() + 14400000;
const word = timestampWord.get(ms);
if (!word) {
return;
@@ -283,8 +284,8 @@ export class InkTranscription extends React.Component {
action(d => {
const x = NumCast(d.x);
const y = NumCast(d.y);
- const width = d[WidthSym]();
- const height = d[HeightSym]();
+ const width = d[Width]();
+ const height = d[Height]();
bounds.push({ x, y, width, height });
})
);
@@ -323,8 +324,8 @@ export class InkTranscription extends React.Component {
// Gets a collection based on the selected nodes using a marquee view ref
const newCollection = marqViewRef?.getCollection(selected, undefined, true);
if (newCollection) {
- newCollection.height = newCollection[HeightSym]();
- newCollection.width = newCollection[WidthSym]();
+ newCollection.height = newCollection[Height]();
+ newCollection.width = newCollection[Width]();
// if the grouping we are creating is an individual word
if (word) {
newCollection.title = word;
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 14c5dc58d..d0210d63b 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -21,9 +21,10 @@
Most of the operations that can be performed on an InkStroke (eg delete a point, rotate, stretch) are implemented in the InkStrokeProperties helper class
*/
import React = require('react');
-import { action, IReactionDisposer, observable, reaction } from 'mobx';
+import { action, computed, IReactionDisposer, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
-import { Doc, HeightSym, WidthSym } from '../../fields/Doc';
+import { Doc } from '../../fields/Doc';
+import { Height, Width } from '../../fields/DocSymbols';
import { InkData, InkField } from '../../fields/InkField';
import { BoolCast, Cast, NumCast, RTFCast, StrCast } from '../../fields/Types';
import { TraceMobx } from '../../fields/util';
@@ -87,7 +88,13 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
if (!addAsAnnotation && !pinProps) return this.rootDoc;
- const anchor = Docs.Create.InkAnchorDocument({ title: 'Ink anchor:' + this.rootDoc.title, presDuration: 1100, presTransition: 1000, unrendered: true, annotationOn: this.rootDoc });
+ const anchor = Docs.Create.ConfigDocument({
+ title: 'Ink anchor:' + this.rootDoc.title,
+ // set presentation timing for restoring shape
+ presDuration: 1100,
+ presTransition: 1000,
+ annotationOn: this.rootDoc,
+ });
if (anchor) {
anchor.backgroundColor = 'transparent';
// /* addAsAnnotation &&*/ this.addDocument(anchor);
@@ -290,6 +297,18 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
*/
nearestScreenPt = () => this._nearestScrPt;
+ @computed get screenCtrlPts() {
+ const { inkData, inkScaleX, inkScaleY, inkStrokeWidth, inkTop, inkLeft } = this.inkScaledData();
+ return inkData
+ .map(point =>
+ this.screenToLocal()
+ .inverse()
+ .transformPoint((point.X - inkLeft - inkStrokeWidth / 2) * inkScaleX + inkStrokeWidth / 2, (point.Y - inkTop - inkStrokeWidth / 2) * inkScaleY + inkStrokeWidth / 2)
+ )
+ .map(p => ({ X: p[0], Y: p[1] }));
+ }
+ startPt = () => this.screenCtrlPts[0];
+ endPt = () => this.screenCtrlPts.lastElement();
/**
* @param boundsLeft the screen space left coordinate of the ink stroke
* @param boundsTop the screen space top coordinate of the ink stroke
@@ -297,39 +316,31 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
*/
componentUI = (boundsLeft: number, boundsTop: number) => {
const inkDoc = this.props.Document;
- const { inkData, inkScaleX, inkScaleY, inkStrokeWidth, inkTop, inkLeft } = this.inkScaledData();
+ const { inkData, inkStrokeWidth } = this.inkScaledData();
const screenSpaceCenterlineStrokeWidth = Math.min(3, inkStrokeWidth * this.screenToLocal().inverse().Scale); // the width of the blue line widget that shows the centerline of the ink stroke
const screenInkWidth = this.screenToLocal().inverse().transformDirection(inkStrokeWidth, inkStrokeWidth);
- const screenPts = inkData
- .map(point =>
- this.screenToLocal()
- .inverse()
- .transformPoint((point.X - inkLeft - inkStrokeWidth / 2) * inkScaleX + inkStrokeWidth / 2, (point.Y - inkTop - inkStrokeWidth / 2) * inkScaleY + inkStrokeWidth / 2)
- )
- .map(p => ({ X: p[0], Y: p[1] }));
- const screenHdlPts = screenPts;
const startMarker = StrCast(this.layoutDoc.stroke_startMarker);
const endMarker = StrCast(this.layoutDoc.stroke_endMarker);
- const markerScale = NumCast(this.layoutDoc.strokeMarkerScale);
+ const markerScale = NumCast(this.layoutDoc.stroke_markerScale);
return SnappingManager.GetIsDragging() ? null : !InkStrokeProperties.Instance._controlButton ? (
!this.props.isSelected() || InkingStroke.IsClosed(inkData) ? null : (
<div className="inkstroke-UI" style={{ clip: `rect(${boundsTop}px, 10000px, 10000px, ${boundsLeft}px)` }}>
- <InkEndPtHandles inkView={this} inkDoc={inkDoc} startPt={screenPts[0]} endPt={screenPts.lastElement()} screenSpaceLineWidth={screenSpaceCenterlineStrokeWidth} />
+ <InkEndPtHandles inkView={this} inkDoc={inkDoc} startPt={this.startPt} endPt={this.endPt} screenSpaceLineWidth={screenSpaceCenterlineStrokeWidth} />
</div>
)
) : (
<div className="inkstroke-UI" style={{ clip: `rect(${boundsTop}px, 10000px, 10000px, ${boundsLeft}px)` }}>
{InteractionUtils.CreatePolyline(
- screenPts,
+ this.screenCtrlPts,
0,
0,
Colors.MEDIUM_BLUE,
screenInkWidth[0],
screenSpaceCenterlineStrokeWidth,
- StrCast(inkDoc.strokeLineJoin),
- StrCast(this.layoutDoc.strokeLineCap),
+ StrCast(inkDoc.stroke_lineJoin),
+ StrCast(this.layoutDoc.stroke_lineCap),
StrCast(inkDoc.stroke_bezier),
'none',
startMarker,
@@ -343,8 +354,8 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
1.0,
false
)}
- <InkControlPtHandles inkView={this} inkDoc={inkDoc} inkCtrlPoints={inkData} screenCtrlPoints={screenHdlPts} nearestScreenPt={this.nearestScreenPt} screenSpaceLineWidth={screenSpaceCenterlineStrokeWidth} />
- <InkTangentHandles inkView={this} inkDoc={inkDoc} screenCtrlPoints={screenHdlPts} screenSpaceLineWidth={screenSpaceCenterlineStrokeWidth} ScreenToLocalTransform={this.screenToLocal} />
+ <InkControlPtHandles inkView={this} inkDoc={inkDoc} inkCtrlPoints={inkData} screenCtrlPoints={this.screenCtrlPts} nearestScreenPt={this.nearestScreenPt} screenSpaceLineWidth={screenSpaceCenterlineStrokeWidth} />
+ <InkTangentHandles inkView={this} inkDoc={inkDoc} screenCtrlPoints={this.screenCtrlPts} screenSpaceLineWidth={screenSpaceCenterlineStrokeWidth} ScreenToLocalTransform={this.screenToLocal} />
</div>
);
};
@@ -357,17 +368,17 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
const startMarker = StrCast(this.layoutDoc.stroke_startMarker);
const endMarker = StrCast(this.layoutDoc.stroke_endMarker);
- const markerScale = NumCast(this.layoutDoc.strokeMarkerScale, 1);
+ const markerScale = NumCast(this.layoutDoc.stroke_markerScale, 1);
const closed = InkingStroke.IsClosed(inkData);
const isInkMask = BoolCast(this.layoutDoc.stroke_isInkMask);
const fillColor = isInkMask ? DashColor(StrCast(this.layoutDoc.fillColor, 'transparent')).blacken(0).rgb().toString() : this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.FillColor) ?? 'transparent';
const strokeColor = !closed && fillColor && fillColor !== 'transparent' ? fillColor : this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color) ?? StrCast(this.layoutDoc.color);
// bcz: Hack!! Not really sure why, but having fractional values for width/height of mask ink strokes causes the dragging clone (see DragManager) to be offset from where it should be.
- if (isInkMask && (this.layoutDoc[WidthSym]() !== Math.round(this.layoutDoc[WidthSym]()) || this.layoutDoc[HeightSym]() !== Math.round(this.layoutDoc[HeightSym]()))) {
+ if (isInkMask && (this.layoutDoc[Width]() !== Math.round(this.layoutDoc[Width]()) || this.layoutDoc[Height]() !== Math.round(this.layoutDoc[Height]()))) {
setTimeout(() => {
- this.layoutDoc._width = Math.round(NumCast(this.layoutDoc[WidthSym]()));
- this.layoutDoc._height = Math.round(NumCast(this.layoutDoc[HeightSym]()));
+ this.layoutDoc._width = Math.round(NumCast(this.layoutDoc[Width]()));
+ this.layoutDoc._height = Math.round(NumCast(this.layoutDoc[Height]()));
});
}
@@ -379,8 +390,8 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
strokeColor,
inkStrokeWidth,
inkStrokeWidth,
- StrCast(this.layoutDoc.strokeLineJoin),
- StrCast(this.layoutDoc.strokeLineCap),
+ StrCast(this.layoutDoc.stroke_lineJoin),
+ StrCast(this.layoutDoc.stroke_lineCap),
StrCast(this.layoutDoc.stroke_bezier),
!closed ? 'none' : fillColor === 'transparent' ? 'none' : fillColor,
startMarker,
@@ -396,20 +407,23 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
);
const highlight = !this.controlUndo && this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Highlighting);
const highlightIndex = highlight?.highlightIndex;
- const highlightColor = highlight?.highlightIndex ? highlight?.highlightColor : StrCast(this.layoutDoc.strokeOutlineColor, !closed && fillColor && fillColor !== 'transparent' ? StrCast(this.layoutDoc.color, 'transparent') : 'transparent');
+ const highlightColor =
+ (!this.props.isSelected() || !isInkMask) && highlight?.highlightIndex
+ ? highlight?.highlightColor
+ : StrCast(this.layoutDoc.stroke_outlineColor, !closed && fillColor && fillColor !== 'transparent' ? StrCast(this.layoutDoc.color, 'transparent') : 'transparent');
// Invisible polygonal line that enables the ink to be selected by the user.
- const clickableLine = (downHdlr?: (e: React.PointerEvent) => void, suppressFill: boolean = false, mask: boolean = false) =>
+ const clickableLine = (downHdlr?: (e: React.PointerEvent) => void, mask: boolean = false) =>
InteractionUtils.CreatePolyline(
inkData,
inkLeft,
inkTop,
- highlightColor,
+ mask && highlightColor === 'transparent' ? strokeColor : highlightColor,
inkStrokeWidth,
inkStrokeWidth + (fillColor ? (closed ? 2 : (highlightIndex ?? 0) + 2) : 2),
- StrCast(this.layoutDoc.strokeLineJoin),
- StrCast(this.layoutDoc.strokeLineCap),
+ StrCast(this.layoutDoc.stroke_lineJoin),
+ StrCast(this.layoutDoc.stroke_lineCap),
StrCast(this.layoutDoc.stroke_bezier),
- !closed ? 'none' : !isInkMask && (fillColor === 'transparent' || suppressFill) ? 'none' : fillColor,
+ !closed || !fillColor || DashColor(fillColor).alpha() === 0 ? 'none' : fillColor,
'',
'',
markerScale,
@@ -423,7 +437,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
downHdlr,
mask
);
- const fsize = +StrCast(this.props.Document.fontSize, '12px').replace('px', '');
+ const fsize = +StrCast(this.props.Document._text_fontSize, '12px').replace('px', '');
// bootsrap 3 style sheet sets line height to be 20px for default 14 point font size.
// this attempts to figure out the lineHeight ratio by inquiring the body's lineHeight and dividing by the fontsize which should yield 1.428571429
// see: https://bibwild.wordpress.com/2019/06/10/bootstrap-3-to-4-changes-in-how-font-size-line-height-and-spacing-is-done-or-what-happened-to-line-height-computed/
@@ -449,7 +463,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
cursor: this.props.isSelected() ? 'default' : undefined,
}}
{...interactions}>
- {clickableLine(this.onPointerDown, undefined, isInkMask)}
+ {clickableLine(this.onPointerDown, isInkMask)}
{isInkMask ? null : inkLine}
</svg>
{!closed || (!RTFCast(this.rootDoc.text)?.Text && (!this.props.isSelected() || Doc.UserDoc().activeInkHideTextLabels)) ? null : (
@@ -458,7 +472,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
style={{
color: StrCast(this.layoutDoc.textColor, 'black'),
pointerEvents: this.props.isDocumentActive?.() ? 'all' : undefined,
- width: this.layoutDoc[WidthSym](),
+ width: this.layoutDoc[Width](),
transform: `scale(${this.props.NativeDimScaling?.() || 1})`,
transformOrigin: 'top left',
//top: (this.props.PanelHeight() - (lineHeightGuess * fsize + 20) * (this.props.NativeDimScaling?.() || 1)) / 2,
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index 1b309a6be..f79a30ad3 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -43,7 +43,7 @@ export class LightboxView extends React.Component<LightboxViewProps> {
@observable private static _layoutTemplateString: Opt<string>;
@observable private static _doc: Opt<Doc>;
@observable private static _docTarget: Opt<Doc>;
- @observable private static _docFilters: string[] = []; // filters
+ @observable private static _childFilters: string[] = []; // filters
private static _savedState: Opt<LightboxSavedState>;
private static _history: Opt<{ doc: Doc; target?: Doc }[]> = [];
@observable private static _future: Opt<Doc[]> = [];
@@ -58,7 +58,7 @@ export class LightboxView extends React.Component<LightboxViewProps> {
this.LightboxDoc.layout_fieldKey = this._savedState.layout_fieldKey;
}
if (!doc) {
- this._docFilters && (this._docFilters.length = 0);
+ this._childFilters && (this._childFilters.length = 0);
this._future = this._history = [];
Doc.ActiveTool = InkTool.None;
MainView.Instance._exploreMode = false;
@@ -134,7 +134,7 @@ export class LightboxView extends React.Component<LightboxViewProps> {
@action
public static SetCookie(cookie: string) {
if (this.LightboxDoc && cookie) {
- this._docFilters = (f => (this._docFilters ? [this._docFilters.push(f) as any, this._docFilters][1] : [f]))(`cookies:${cookie}:provide`);
+ this._childFilters = (f => (this._childFilters ? [this._childFilters.push(f) as any, this._childFilters][1] : [f]))(`cookies:${cookie}:provide`);
}
}
public static AddDocTab = (doc: Doc, location: OpenWhere, layoutTemplate?: Doc | string) => {
@@ -148,7 +148,7 @@ export class LightboxView extends React.Component<LightboxViewProps> {
layoutTemplate
);
};
- docFilters = () => LightboxView._docFilters || [];
+ childFilters = () => LightboxView._childFilters || [];
addDocTab = LightboxView.AddDocTab;
@action public static Next() {
const doc = LightboxView._doc!;
@@ -261,8 +261,8 @@ export class LightboxView extends React.Component<LightboxViewProps> {
renderDepth={0}
rootSelected={returnTrue}
docViewPath={returnEmptyDoclist}
- docFilters={this.docFilters}
- docRangeFilters={returnEmptyFilter}
+ childFilters={this.childFilters}
+ childFiltersByRanges={returnEmptyFilter}
searchFilterDocs={returnEmptyDoclist}
addDocument={undefined}
removeDocument={undefined}
@@ -313,8 +313,10 @@ export class LightboxView extends React.Component<LightboxViewProps> {
className="lightboxView-tabBtn"
title="open in tab"
onClick={e => {
+ const lightdoc = LightboxView._docTarget || LightboxView._doc!;
e.stopPropagation();
- CollectionDockingView.AddSplit(LightboxView._docTarget || LightboxView._doc!, OpenWhereMod.none);
+ Doc.RemoveDocFromList(Doc.MyRecentlyClosed, 'data', lightdoc);
+ CollectionDockingView.AddSplit(lightdoc, OpenWhereMod.none);
SelectionManager.DeselectAll();
LightboxView.SetLightboxDoc(undefined);
}}>
diff --git a/src/client/views/Main.scss b/src/client/views/Main.scss
index c7a7614ac..a403a10e3 100644
--- a/src/client/views/Main.scss
+++ b/src/client/views/Main.scss
@@ -11,6 +11,7 @@ body {
height: 100%;
overflow: hidden;
font-family: $sans-serif;
+ font-size: $body-text;
margin: 0;
position: absolute;
top: 0;
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index 6b18caed0..6dd1d53ee 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -6,19 +6,18 @@ import * as React from 'react';
import * as ReactDOM from 'react-dom/client';
import { AssignAllExtensions } from '../../extensions/General/Extensions';
import { FieldLoader } from '../../fields/FieldLoader';
-import { DocServer } from '../DocServer';
-import { Docs } from '../documents/Documents';
import { CurrentUserUtils } from '../util/CurrentUserUtils';
-import { LinkManager } from '../util/LinkManager'; // this must come before importing Docs and CurrentUserUtils
import { ReplayMovements } from '../util/ReplayMovements';
import { TrackMovements } from '../util/TrackMovements';
import { CollectionView } from './collections/CollectionView';
import { MainView } from './MainView';
import * as dotenv from 'dotenv'; // see https://github.com/motdotla/dotenv#how-do-i-use-dotenv-with-import
+import { PingManager } from '../util/PingManager';
+import './global/globalScripts';
dotenv.config();
AssignAllExtensions();
-FieldLoader.ServerLoadStatus = { requested: 0, retrieved: 0 }; // bcz: not sure why this is needed to get the code loaded properly...
+FieldLoader.ServerLoadStatus = { requested: 0, retrieved: 0, message: 'cache' }; // bcz: not sure why this is needed to get the code loaded properly...
(async () => {
MainView.Live = window.location.search.includes('live');
@@ -26,8 +25,12 @@ FieldLoader.ServerLoadStatus = { requested: 0, retrieved: 0 }; // bcz: not sure
root.render(<FieldLoader />);
window.location.search.includes('safe') && CollectionView.SetSafeMode(true);
const info = await CurrentUserUtils.loadCurrentUser();
- if (info.email === 'guest') DocServer.Control.makeReadOnly();
- await CurrentUserUtils.loadUserDocument(info.id);
+ // if (info.email === 'guest') DocServer.Control.makeReadOnly();
+ if (!info.userDocumentId) {
+ alert('Fatal Error: user not found in database');
+ return;
+ }
+ await CurrentUserUtils.loadUserDocument(info);
setTimeout(() => {
document.getElementById('root')!.addEventListener(
'wheel',
@@ -45,9 +48,9 @@ FieldLoader.ServerLoadStatus = { requested: 0, retrieved: 0 }; // bcz: not sure
d.setTime(d.getTime() + 100 * 24 * 60 * 60 * 1000);
const expires = 'expires=' + d.toUTCString();
document.cookie = `loadtime=${loading};${expires};path=/`;
- new LinkManager();
new TrackMovements();
new ReplayMovements();
+ new PingManager();
root.render(<MainView />);
}, 0);
})();
diff --git a/src/client/views/MainView.scss b/src/client/views/MainView.scss
index b95ce0e99..b3faff442 100644
--- a/src/client/views/MainView.scss
+++ b/src/client/views/MainView.scss
@@ -47,6 +47,10 @@ h1,
align-items: center;
justify-content: space-between;
gap: 10px;
+ background: rgb(0, 0, 0);
+ border-radius: 8px;
+ padding-left: 5px;
+ padding-right: 5px;
}
.mainView-snapLines {
@@ -68,10 +72,6 @@ h1,
left: 0;
z-index: 1;
touch-action: none;
-
- .searchBox-container {
- background: $light-gray;
- }
}
.mainView-container,
@@ -118,10 +118,6 @@ h1,
background: $light-gray;
}
- .searchBox-container {
- background: $dark-gray;
- }
-
.contextMenu-cont,
.contextMenu-item {
background: $dark-gray;
@@ -198,10 +194,10 @@ h1,
left: 0;
position: absolute;
z-index: 2;
- background-color: $light-gray;
+ background-color: linen; //$light-gray;
.editable-title {
- background-color: $light-gray;
+ background-color: linen; //$light-gray;
}
}
}
@@ -250,7 +246,6 @@ h1,
.mainView-leftMenuPanel {
min-width: var(--menuPanelWidth);
- background-color: $dark-gray;
border-right: $standard-border;
.collectionStackingView {
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index ce29b63cd..dc236b603 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -4,13 +4,13 @@ import * as far from '@fortawesome/free-regular-svg-icons';
import * as fa from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import 'browndash-components/dist/styles/global.min.css';
-import { action, computed, configure, observable, reaction, runInAction } from 'mobx';
+import { action, computed, configure, observable, runInAction } from 'mobx';
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 { StrCast } from '../../fields/Types';
+import { DocCast, StrCast } from '../../fields/Types';
import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, returnZero, setupMoveUpEvents, Utils } from '../../Utils';
import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager';
import { DocServer } from '../DocServer';
@@ -21,7 +21,7 @@ import { DocumentManager } from '../util/DocumentManager';
import { GroupManager } from '../util/GroupManager';
import { HistoryUtil } from '../util/History';
import { Hypothesis } from '../util/HypothesisUtils';
-import { ReportManager } from '../util/ReportManager';
+import { ReportManager } from '../util/reportManager/ReportManager';
import { RTFMarkup } from '../util/RTFMarkup';
import { ScriptingGlobals } from '../util/ScriptingGlobals';
import { SelectionManager } from '../util/SelectionManager';
@@ -81,7 +81,7 @@ export class MainView extends React.Component {
@observable private _sidebarContent: any = Doc.MyLeftSidebarPanel;
@observable private _leftMenuFlyoutWidth: number = 0;
@computed get _hideUI() {
- return this.mainDoc && this.mainDoc._viewType !== CollectionViewType.Docking;
+ return this.mainDoc && this.mainDoc._type_collection !== CollectionViewType.Docking;
}
@computed private get dashboardTabHeight() {
@@ -153,46 +153,35 @@ export class MainView extends React.Component {
}
this._sidebarContent.proto = undefined;
if (!MainView.Live) {
- DocServer.setPlaygroundFields([
+ DocServer.setLivePlaygroundFields([
'dataTransition',
'viewTransition',
'treeViewOpen',
- 'layout_showSidebar',
+ 'treeViewExpandedView',
+ 'carousel_index',
'itemIndex', // for changing slides in presentations
'layout_sidebarWidthPercent',
'layout_currentTimecode',
'layout_timelineHeightPercent',
+ 'layout_hideMinimap',
+ 'layout_showSidebar',
+ 'layout_scrollTop',
+ 'layout_fitWidth',
+ 'layout_curPage',
'presStatus',
'freeform_panX',
'freeform_panY',
+ 'freeform_scale',
'overlayX',
'overlayY',
- 'layout_fitWidth',
- 'nativeWidth',
- 'nativeHeight',
'text_scrollHeight',
'text_height',
- 'layout_hideMinimap',
- 'freeform_scale',
- 'layout_scrollTop',
'hidden',
- 'layout_curPage',
- 'viewType',
+ //'type_collection',
'chromeHidden',
'currentFrame',
- 'width',
- 'height',
- 'nativeWidth',
]); // can play with these fields on someone else's
}
- DocServer.GetRefField('rtfProto').then(
- proto =>
- proto instanceof Doc &&
- reaction(
- () => StrCast(proto.BROADCAST_MESSAGE),
- msg => msg && alert(msg)
- )
- );
const tag = document.createElement('script');
tag.src = 'https://www.youtube.com/iframe_api';
@@ -217,7 +206,7 @@ export class MainView extends React.Component {
window.removeEventListener('keydown', KeyManager.Instance.handle);
window.removeEventListener('pointerdown', this.globalPointerDown, true);
window.removeEventListener('pointermove', this.globalPointerMove, true);
- window.removeEventListener('mouseclick', this.globalPointerClick, true);
+ window.removeEventListener('pointerup', this.globalPointerClick, true);
window.removeEventListener('paste', KeyManager.Instance.paste as any);
document.removeEventListener('linkAnnotationToDash', Hypothesis.linkListener);
}
@@ -236,7 +225,7 @@ export class MainView extends React.Component {
if (pathname.length > 1 && pathname[0] === 'doc') {
DocServer.GetRefField(pathname[1]).then(
action(field => {
- if (field instanceof Doc && field._viewType !== CollectionViewType.Docking) {
+ if (field instanceof Doc && field._type_collection !== CollectionViewType.Docking) {
Doc.GuestTarget = field;
}
})
@@ -487,6 +476,8 @@ export class MainView extends React.Component {
fa.faSquareRootAlt,
fa.faVolumeMute,
fa.faUserCircle,
+ fa.faHeart,
+ fa.faHeartBroken,
fa.faHighlighter,
fa.faRemoveFormat,
fa.faHandPointUp,
@@ -528,12 +519,13 @@ export class MainView extends React.Component {
});
initEventListeners = () => {
+ 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('mouseclick', this.globalPointerClick, true);
+ document.addEventListener('pointerup', this.globalPointerClick, true);
document.addEventListener(
'click',
(e: MouseEvent) => {
@@ -569,7 +561,7 @@ export class MainView extends React.Component {
@action
createNewFolder = async () => {
- const folder = Docs.Create.TreeDocument([], { title: 'Untitled folder', _stayInCollection: true, isFolder: true });
+ const folder = Docs.Create.TreeDocument([], { title: 'Untitled folder', _dragOnlyWithinContainer: true, isFolder: true });
Doc.AddDocToList(Doc.MyFilesystem, 'data', folder);
};
@@ -580,6 +572,8 @@ export class MainView extends React.Component {
waitForDoubleClick = () => (this._exploreMode ? 'never' : undefined);
headerBarScreenXf = () => new Transform(-this.leftScreenOffsetOfMainDocView - this.leftMenuFlyoutWidth(), -this.headerBarDocHeight(), 1);
mainScreenToLocalXf = () => new Transform(-this.leftScreenOffsetOfMainDocView - this.leftMenuFlyoutWidth(), -this.topOfMainDocContent, 1);
+ addHeaderDoc = (doc: Doc | Doc[], annotationKey?: string) => (doc instanceof Doc ? [doc] : doc).reduce((done, doc) => Doc.AddDocToList(this.headerBarDoc, 'data', doc), true);
+ removeHeaderDoc = (doc: Doc | Doc[], annotationKey?: string) => (doc instanceof Doc ? [doc] : doc).reduce((done, doc) => Doc.RemoveDocFromList(this.headerBarDoc, 'data', doc), true);
@computed get headerBarDocView() {
return (
<div className="mainView-headerBar" style={{ height: this.headerBarDocHeight() }}>
@@ -587,18 +581,19 @@ export class MainView extends React.Component {
key="headerBarDoc"
Document={this.headerBarDoc}
DataDoc={undefined}
- addDocument={undefined}
addDocTab={DocumentViewInternal.addDocTabFunc}
pinToPres={emptyFunction}
docViewPath={returnEmptyDoclist}
styleProvider={DefaultStyleProvider}
rootSelected={returnTrue}
- removeDocument={returnFalse}
+ addDocument={this.addHeaderDoc}
+ removeDocument={this.removeHeaderDoc}
fitContentsToBox={returnTrue}
isDocumentActive={returnTrue} // headerBar is always documentActive (ie, the docView gets pointer events)
isContentActive={returnTrue} // headerBar is awlays contentActive which means its items are always documentActive
ScreenToLocalTransform={this.headerBarScreenXf}
childHideResizeHandles={returnTrue}
+ childDragAction="move"
dontRegisterView={true}
hideResizeHandles={true}
PanelWidth={this.headerBarDocWidth}
@@ -607,8 +602,8 @@ export class MainView extends React.Component {
focus={emptyFunction}
whenChildContentsActiveChanged={emptyFunction}
bringToFront={emptyFunction}
- docFilters={returnEmptyFilter}
- docRangeFilters={returnEmptyFilter}
+ childFilters={returnEmptyFilter}
+ childFiltersByRanges={returnEmptyFilter}
searchFilterDocs={returnEmptyDoclist}
/>
</div>
@@ -636,8 +631,8 @@ export class MainView extends React.Component {
focus={emptyFunction}
whenChildContentsActiveChanged={emptyFunction}
bringToFront={emptyFunction}
- docFilters={returnEmptyFilter}
- docRangeFilters={returnEmptyFilter}
+ childFilters={returnEmptyFilter}
+ childFiltersByRanges={returnEmptyFilter}
searchFilterDocs={returnEmptyDoclist}
suppressSetHeight={true}
renderDepth={this._hideUI ? 0 : -1}
@@ -693,15 +688,13 @@ export class MainView extends React.Component {
sidebarScreenToLocal = () => new Transform(0, -this.topOfSidebarDoc, 1);
mainContainerXf = () => this.sidebarScreenToLocal().translate(-this.leftScreenOffsetOfMainDocView, 0);
static addDocTabFunc_impl = (doc: Doc, location: OpenWhere): boolean => {
- const whereFields = doc._viewType === CollectionViewType.Docking ? [OpenWhere.dashboard] : location.split(':');
+ const whereFields = location.split(':');
const keyValue = whereFields[1]?.includes('KeyValue');
const whereMods: OpenWhereMod = whereFields.length > 1 ? (whereFields[1].replace('KeyValue', '') as OpenWhereMod) : OpenWhereMod.none;
- if (doc.dockingConfig) return DashboardView.openDashboard(doc);
+ if (doc.dockingConfig && !keyValue) return DashboardView.openDashboard(doc);
// prettier-ignore
switch (whereFields[0]) {
case OpenWhere.lightbox: return LightboxView.AddDocTab(doc, location);
- case OpenWhere.dashboard: return DashboardView.openDashboard(doc);
- case OpenWhere.fullScreen: return CollectionDockingView.OpenFullScreen(doc);
case OpenWhere.close: return CollectionDockingView.CloseSplit(doc, whereMods);
case OpenWhere.toggle: return CollectionDockingView.ToggleSplit(doc, whereMods);
case OpenWhere.add:default:return CollectionDockingView.AddSplit(doc, whereMods, undefined, undefined, keyValue);
@@ -735,8 +728,8 @@ export class MainView extends React.Component {
focus={emptyFunction}
whenChildContentsActiveChanged={emptyFunction}
bringToFront={emptyFunction}
- docFilters={returnEmptyFilter}
- docRangeFilters={returnEmptyFilter}
+ childFilters={returnEmptyFilter}
+ childFiltersByRanges={returnEmptyFilter}
searchFilterDocs={returnEmptyDoclist}
/>
</div>
@@ -747,7 +740,7 @@ export class MainView extends React.Component {
@computed get leftMenuPanel() {
return (
- <div key="menu" className="mainView-leftMenuPanel" style={{ display: LightboxView.LightboxDoc ? 'none' : undefined }}>
+ <div key="menu" className="mainView-leftMenuPanel" style={{ background: StrCast(Doc.UserDoc().userBackgroundColor), display: LightboxView.LightboxDoc ? 'none' : undefined }}>
<DocumentView
Document={Doc.MyLeftSidebarMenu}
DataDoc={undefined}
@@ -766,8 +759,8 @@ export class MainView extends React.Component {
isContentActive={returnTrue}
whenChildContentsActiveChanged={emptyFunction}
bringToFront={emptyFunction}
- docFilters={returnEmptyFilter}
- docRangeFilters={returnEmptyFilter}
+ childFilters={returnEmptyFilter}
+ childFiltersByRanges={returnEmptyFilter}
searchFilterDocs={returnEmptyDoclist}
scriptContext={this}
/>
@@ -802,14 +795,17 @@ export class MainView extends React.Component {
{this._hideUI ? null : this.leftMenuPanel}
<div key="inner" className={`mainView-innerContent${this.colorScheme}`}>
{this.flyout}
- <div className="mainView-libraryHandle" style={{ left: leftMenuFlyoutWidth - 10 /* ~half width of handle */, display: !this._leftMenuFlyoutWidth ? 'none' : undefined }} onPointerDown={this.onFlyoutPointerDown}>
- <FontAwesomeIcon icon="chevron-left" color={this.colorScheme === ColorScheme.Dark ? 'white' : 'black'} style={{ opacity: '50%' }} size="sm" />
+ <div
+ className="mainView-libraryHandle"
+ style={{ background: StrCast(Doc.UserDoc().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" />
</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 }}>
+ <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>
)}
@@ -853,7 +849,7 @@ export class MainView extends React.Component {
this._leftMenuFlyoutWidth = this._leftMenuFlyoutWidth || 250;
//setTimeout(action(() => (this._leftMenuFlyoutWidth += 0.5)));
- this._sidebarContent.proto = button.target as any;
+ this._sidebarContent.proto = DocCast(button.target);
this.LastButton = button;
});
@@ -864,7 +860,7 @@ export class MainView extends React.Component {
this._leftMenuFlyoutWidth = 0;
});
- remButtonDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg: boolean, doc) => flg && Doc.RemoveDocFromList(Doc.MyDockedBtns, 'data', doc), true);
+ remButtonDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg: boolean, doc) => flg && !doc.dragOnlyWithinContainer && Doc.RemoveDocFromList(Doc.MyDockedBtns, 'data', doc), true);
moveButtonDoc = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => this.remButtonDoc(doc) && addDocument(doc);
addButtonDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg: boolean, doc) => flg && Doc.AddDocToList(Doc.MyDockedBtns, 'data', doc), true);
@@ -876,7 +872,7 @@ export class MainView extends React.Component {
@computed get docButtons() {
return !Doc.MyDockedBtns ? null : (
- <div className="mainView-docButtons" ref={this._docBtnRef}>
+ <div className="mainView-docButtons" style={{ background: StrCast(Doc.UserDoc().userBackgroundColor), color: StrCast(Doc.UserDoc().userColor) }} ref={this._docBtnRef}>
<CollectionLinearView
Document={Doc.MyDockedBtns}
DataDoc={undefined}
@@ -902,8 +898,8 @@ export class MainView extends React.Component {
renderDepth={0}
focus={emptyFunction}
whenChildContentsActiveChanged={emptyFunction}
- docFilters={returnEmptyFilter}
- docRangeFilters={returnEmptyFilter}
+ childFilters={returnEmptyFilter}
+ childFiltersByRanges={returnEmptyFilter}
searchFilterDocs={returnEmptyDoclist}
/>
{['watching', 'recording'].includes(String(this.userDoc?.presentationMode) ?? '') ? <div style={{ border: '.5rem solid green', padding: '5px' }}>{StrCast(this.userDoc?.presentationMode)}</div> : <></>}
@@ -914,11 +910,11 @@ export class MainView extends React.Component {
return !SelectionManager.Views().some(dv => dv.rootDoc.freeform_snapLines) ? null : (
<div className="mainView-snapLines">
<svg style={{ width: '100%', height: '100%' }}>
- {SnappingManager.horizSnapLines().map(l => (
- <line x1="0" y1={l} x2="2000" y2={l} stroke="black" opacity={0.3} strokeWidth={0.5} strokeDasharray={'1 1'} />
+ {SnappingManager.horizSnapLines().map((l, i) => (
+ <line key={i} x1="0" y1={l} x2="2000" y2={l} stroke="black" opacity={0.3} strokeWidth={0.5} strokeDasharray={'1 1'} />
))}
- {SnappingManager.vertSnapLines().map(l => (
- <line y1="0" x1={l} y2="2000" x2={l} stroke="black" opacity={0.3} strokeWidth={0.5} strokeDasharray={'1 1'} />
+ {SnappingManager.vertSnapLines().map((l, i) => (
+ <line key={i} y1="0" x1={l} y2="2000" x2={l} stroke="black" opacity={0.3} strokeWidth={0.5} strokeDasharray={'1 1'} />
))}
</svg>
</div>
@@ -957,7 +953,11 @@ export class MainView extends React.Component {
render() {
return (
<div
- className={`mainView-container${this.colorScheme}`}
+ className={`mainView-container ${this.colorScheme}`}
+ style={{
+ color: StrCast(Doc.UserDoc().userColor),
+ background: StrCast(Doc.UserDoc().userBackgroundColor),
+ }}
onScroll={() => (ele => (ele.scrollTop = ele.scrollLeft = 0))(document.getElementById('root')!)}
ref={r => {
r &&
@@ -1012,6 +1012,7 @@ export class MainView extends React.Component {
<InkTranscription />
{this.snapLines}
<LightboxView key="lightbox" PanelWidth={this._windowWidth} PanelHeight={this._windowHeight} maxBorder={[200, 50]} />
+ {/* <NewLightboxView key="newLightbox" PanelWidth={this._windowWidth} PanelHeight={this._windowHeight} maxBorder={[200, 50]} /> */}
</div>
);
}
diff --git a/src/client/views/MainViewModal.scss b/src/client/views/MainViewModal.scss
index 0648e31c5..4bf9eb79f 100644
--- a/src/client/views/MainViewModal.scss
+++ b/src/client/views/MainViewModal.scss
@@ -4,22 +4,23 @@
z-index: 10000;
width: 100%;
height: 100%;
+ box-shadow: #00000044 5px 5px 10px;
.dialogue-box {
- padding: 10px;
position: absolute;
z-index: 1000;
text-align: center;
justify-content: center;
align-self: center;
align-content: center;
- background: white;
// border-radius: 10px;
box-shadow: #00000044 5px 5px 10px;
transform: translate(-50%, -50%);
top: 50%;
left: 50%;
transition: 0.5s all ease;
+ border-radius: 10px;
+ overflow: hidden;
}
.overlay {
@@ -28,6 +29,7 @@
position: absolute;
z-index: 999;
transition: 0.5s all ease;
+ backdrop-filter: blur(5px);
}
} \ No newline at end of file
diff --git a/src/client/views/MainViewModal.tsx b/src/client/views/MainViewModal.tsx
index 32997a944..42df99864 100644
--- a/src/client/views/MainViewModal.tsx
+++ b/src/client/views/MainViewModal.tsx
@@ -1,6 +1,10 @@
import * as React from 'react';
import './MainViewModal.scss';
import { observer } from 'mobx-react';
+import { Doc } from '../../fields/Doc';
+import { StrCast } from '../../fields/Types';
+import { isDark } from 'browndash-components';
+import { Colors } from './global/globalEnums';
export interface MainViewOverlayProps {
isDisplayed: boolean;
@@ -41,9 +45,8 @@ export class MainViewModal extends React.Component<MainViewOverlayProps> {
className="overlay"
onClick={this.props?.closeOnExternalClick}
style={{
- backgroundColor: 'black',
+ backgroundColor: isDark(StrCast(Doc.UserDoc().userColor)) ? "#DFDFDF30" : "#32323230",
...(p.overlayStyle || {}),
- opacity: p.isDisplayed ? overlayOpacity : 0,
}}
/>
</div>
diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx
index eb0fd0c7a..a4a2c1df9 100644
--- a/src/client/views/MarqueeAnnotator.tsx
+++ b/src/client/views/MarqueeAnnotator.tsx
@@ -1,6 +1,7 @@
import { action, observable, ObservableMap, runInAction } from 'mobx';
import { observer } from 'mobx-react';
-import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DataSym, Doc, Opt } from '../../fields/Doc';
+import { Doc, Opt } from '../../fields/Doc';
+import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DocData } from '../../fields/DocSymbols';
import { Id } from '../../fields/FieldSymbols';
import { List } from '../../fields/List';
import { NumCast } from '../../fields/Types';
@@ -125,7 +126,7 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
if (!e.aborted && e.linkDocument) {
Doc.GetProto(e.linkDocument).link_relationship = 'cropped image';
Doc.GetProto(e.linkDocument).title = 'crop: ' + this.props.docView.rootDoc.title;
- Doc.GetProto(e.linkDocument).layout_linkDisplay = false;
+ Doc.GetProto(e.linkDocument).link_displayLine = false;
}
},
});
@@ -153,8 +154,8 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
annotationOn: this.props.rootDoc,
title: 'Annotation on ' + this.props.rootDoc.title,
});
- marqueeAnno.x = NumCast(this.props.docView.props.Document.freeform_panXMin) + (parseInt(anno.style.left || '0') - containerOffset[0]) / scale / NumCast(this.props.docView.props.Document._freeform_scale, 1);
- marqueeAnno.y = NumCast(this.props.docView.props.Document.freeform_panYMin) + (parseInt(anno.style.top || '0') - containerOffset[1]) / scale / NumCast(this.props.docView.props.Document._freeform_scale, 1) + NumCast(this.props.scrollTop);
+ marqueeAnno.x = NumCast(this.props.docView.props.Document.freeform_panX_min) + (parseInt(anno.style.left || '0') - containerOffset[0]) / scale / NumCast(this.props.docView.props.Document._freeform_scale, 1);
+ marqueeAnno.y = NumCast(this.props.docView.props.Document.freeform_panY_min) + (parseInt(anno.style.top || '0') - containerOffset[1]) / scale / NumCast(this.props.docView.props.Document._freeform_scale, 1) + NumCast(this.props.scrollTop);
marqueeAnno._height = parseInt(anno.style.height || '0') / scale / NumCast(this.props.docView.props.Document._freeform_scale, 1);
marqueeAnno._width = parseInt(anno.style.width || '0') / scale / NumCast(this.props.docView.props.Document._freeform_scale, 1);
anno.remove();
@@ -162,7 +163,7 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
return marqueeAnno;
}
- const textRegionAnno = Docs.Create.HTMLAnchorDocument([], {
+ const textRegionAnno = Docs.Create.HTMLMarkerDocument([], {
annotationOn: this.props.rootDoc,
text: this.props.selectionText(),
backgroundColor: 'transparent',
@@ -207,7 +208,7 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
@action
highlight = (color: string, isLinkButton: boolean, savedAnnotations?: ObservableMap<number, HTMLDivElement[]>, addAsAnnotation?: boolean, summarize?: boolean) => {
// creates annotation documents for current highlights
- const effectiveAcl = GetEffectiveAcl(this.props.rootDoc[DataSym]);
+ const effectiveAcl = GetEffectiveAcl(this.props.rootDoc[DocData]);
const annotationDoc = [AclAugment, AclSelfEdit, AclEdit, AclAdmin].includes(effectiveAcl) && this.makeAnnotationDocument(color, isLinkButton, savedAnnotations);
addAsAnnotation && !savedAnnotations && annotationDoc && this.props.addDocument(annotationDoc);
return (annotationDoc as Doc) ?? undefined;
@@ -243,7 +244,7 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
this._top = Math.min(this._startY, this._startY + this._height);
this._width = Math.abs(this._width);
this._height = Math.abs(this._height);
- e.stopPropagation();
+ //e.stopPropagation(); // overlay documents are all 'active', yet they can be dragged. if we stop propagation, then they can be marqueed but not dragged. if we don't stop, then they will be marqueed and dragged, but the marquee will be zero width since the doc will move along with the cursor.
};
onSelectEnd = (e: PointerEvent) => {
diff --git a/src/client/views/MetadataEntryMenu.tsx b/src/client/views/MetadataEntryMenu.tsx
index 82ec5a5b3..bcbdd3ccb 100644
--- a/src/client/views/MetadataEntryMenu.tsx
+++ b/src/client/views/MetadataEntryMenu.tsx
@@ -1,5 +1,5 @@
import * as React from 'react';
-import "./MetadataEntryMenu.scss";
+import './MetadataEntryMenu.scss';
import { observer } from 'mobx-react';
import { observable, action, runInAction, trace, computed, IReactionDisposer, reaction } from 'mobx';
import { KeyValueBox } from './nodes/KeyValueBox';
@@ -16,9 +16,9 @@ export interface MetadataEntryProps {
}
@observer
-export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{
- @observable private _currentKey: string = "";
- @observable private _currentValue: string = "";
+export class MetadataEntryMenu extends React.Component<MetadataEntryProps> {
+ @observable private _currentKey: string = '';
+ @observable private _currentValue: string = '';
private _addChildren: boolean = false;
@observable _allSuggestions: string[] = [];
_suggestionDispser: IReactionDisposer | undefined;
@@ -32,7 +32,7 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{
if (!this.userModified) {
this.previewValue();
}
- }
+ };
previewValue = async () => {
let field: Field | undefined | null = null;
@@ -45,30 +45,30 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{
if (field === null) {
field = v;
} else if (v !== field) {
- value = "multiple values";
+ value = 'multiple values';
}
}
if (value === undefined) {
if (field !== null && field !== undefined) {
- value = (onProto ? "" : "= ") + Field.toScriptString(field);
+ value = (onProto ? '' : '= ') + Field.toScriptString(field);
} else {
- value = "";
+ value = '';
}
}
const s = value;
- runInAction(() => this._currentValue = s);
- }
+ runInAction(() => (this._currentValue = s));
+ };
@action
onValueChange = (e: React.ChangeEvent<HTMLInputElement>) => {
this._currentValue = e.target.value;
- this.userModified = e.target.value.trim() !== "";
- }
+ this.userModified = e.target.value.trim() !== '';
+ };
@undoBatch
@action
onValueKeyDown = async (e: React.KeyboardEvent) => {
- if (e.key === "Enter") {
+ if (e.key === 'Enter') {
e.stopPropagation();
const script = KeyValueBox.CompileKVPScript(this._currentValue);
if (!script) return;
@@ -95,18 +95,18 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{
this.clearInputs();
}
}
- }
+ };
@action
clearInputs = () => {
- this._currentKey = "";
- this._currentValue = "";
+ this._currentKey = '';
+ this._currentValue = '';
this.userModified = false;
if (this.autosuggestRef.current) {
const input: HTMLInputElement = (this.autosuggestRef.current as any).input;
input && input.focus();
}
- }
+ };
getKeySuggestions = (value: string) => {
value = value.toLowerCase();
@@ -114,16 +114,18 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{
const keys = new Set<string>();
docs.forEach(doc => Doc.allKeys(doc).forEach(key => keys.add(key)));
return Array.from(keys).filter(key => key.toLowerCase().startsWith(value));
- }
+ };
getSuggestionValue = (suggestion: string) => suggestion;
renderSuggestion = (suggestion: string) => {
- return (null);
- }
+ return null;
+ };
componentDidMount() {
- this._suggestionDispser = reaction(() => this._currentKey,
- () => this._allSuggestions = this.getKeySuggestions(this._currentKey),
- { fireImmediately: true });
+ this._suggestionDispser = reaction(
+ () => this._currentKey,
+ () => (this._allSuggestions = this.getKeySuggestions(this._currentKey)),
+ { fireImmediately: true }
+ );
}
componentWillUnmount() {
this._suggestionDispser && this._suggestionDispser();
@@ -131,49 +133,64 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{
onClick = (e: React.ChangeEvent<HTMLInputElement>) => {
this._addChildren = !this._addChildren;
- }
+ };
private get considerChildOptions() {
- if (!this.props.docs.every(doc => doc._viewType !== undefined)) {
+ if (!this.props.docs.every(doc => doc._type_collection !== undefined)) {
return null;
}
return (
- <div style={{ display: "flex" }}>
+ <div style={{ display: 'flex' }}>
Children:
- <input type="checkbox" onChange={this.onClick} ></input>
+ <input type="checkbox" onChange={this.onClick}></input>
</div>
);
}
_ref = React.createRef<HTMLInputElement>();
render() {
- return (<div className="metadataEntry-outerDiv" id="metadataEntry-outer" onPointerDown={e => e.stopPropagation()}>
- <div className="metadataEntry-inputArea">
- <div style={{ display: "flex", flexDirection: "row" }}>
- <span>Key:</span>
- <div className="metadataEntry-autoSuggester" onClick={e => this.autosuggestRef.current!.input?.focus()} >
- <Autosuggest inputProps={{ value: this._currentKey, onChange: this.onKeyChange }}
- getSuggestionValue={this.getSuggestionValue}
- suggestions={emptyPath}
- alwaysRenderSuggestions={false}
- renderSuggestion={this.renderSuggestion}
- onSuggestionsFetchRequested={emptyFunction}
- onSuggestionsClearRequested={emptyFunction}
- ref={this.autosuggestRef} />
+ return (
+ <div className="metadataEntry-outerDiv" id="metadataEntry-outer" onPointerDown={e => e.stopPropagation()}>
+ <div className="metadataEntry-inputArea">
+ <div style={{ display: 'flex', flexDirection: 'row' }}>
+ <span>Key:</span>
+ <div className="metadataEntry-autoSuggester" onClick={e => this.autosuggestRef.current!.input?.focus()}>
+ <Autosuggest
+ inputProps={{ value: this._currentKey, onChange: this.onKeyChange }}
+ getSuggestionValue={this.getSuggestionValue}
+ suggestions={emptyPath}
+ alwaysRenderSuggestions={false}
+ renderSuggestion={this.renderSuggestion}
+ onSuggestionsFetchRequested={emptyFunction}
+ onSuggestionsClearRequested={emptyFunction}
+ ref={this.autosuggestRef}
+ />
+ </div>
</div>
+ <div style={{ display: 'flex', flexDirection: 'row' }}>
+ <span>Value:</span>
+ <input className="metadataEntry-input" ref={this._ref} value={this._currentValue} onClick={e => this._ref.current!.focus()} onChange={this.onValueChange} onKeyDown={this.onValueKeyDown} />
+ </div>
+ {this.considerChildOptions}
</div>
- <div style={{ display: "flex", flexDirection: "row" }}>
- <span>Value:</span>
- <input className="metadataEntry-input" ref={this._ref} value={this._currentValue} onClick={e => this._ref.current!.focus()} onChange={this.onValueChange} onKeyDown={this.onValueKeyDown} />
+ <div className="metadataEntry-keys">
+ <ul>
+ {this._allSuggestions
+ .slice()
+ .sort()
+ .map(s => (
+ <li
+ key={s}
+ onClick={action(() => {
+ this._currentKey = s;
+ this.previewValue();
+ })}>
+ {s}
+ </li>
+ ))}
+ </ul>
</div>
- {this.considerChildOptions}
- </div>
- <div className="metadataEntry-keys" >
- <ul>
- {this._allSuggestions.slice().sort().map(s => <li key={s} onClick={action(() => { this._currentKey = s; this.previewValue(); })} >{s}</li>)}
- </ul>
</div>
- </div>
);
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/OverlayView.scss b/src/client/views/OverlayView.scss
index 033cdf1f7..5362bf9f0 100644
--- a/src/client/views/OverlayView.scss
+++ b/src/client/views/OverlayView.scss
@@ -5,7 +5,6 @@
width: 100vw;
height: 100vh;
/* background-color: pink; */
- z-index: 100000;
}
.overlayWindow-outerDiv {
@@ -55,8 +54,8 @@
}
.overlayView-doc {
- z-index: 9002; //so that it appears above chroma
+ z-index: 9002; //so that it appears above chroma
position: absolute;
top: 0;
left: 0;
-} \ No newline at end of file
+}
diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx
index 9baedae6d..339507f65 100644
--- a/src/client/views/OverlayView.tsx
+++ b/src/client/views/OverlayView.tsx
@@ -3,7 +3,8 @@ import { observer } from 'mobx-react';
import { computedFn } from 'mobx-utils';
import * as React from 'react';
import ReactLoading from 'react-loading';
-import { Doc, DocListCast, HeightSym, WidthSym } from '../../fields/Doc';
+import { Doc, DocListCast } from '../../fields/Doc';
+import { Height, Width } from '../../fields/DocSymbols';
import { Id } from '../../fields/FieldSymbols';
import { NumCast } from '../../fields/Types';
import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, setupMoveUpEvents, Utils } from '../../Utils';
@@ -159,7 +160,7 @@ export class OverlayView extends React.Component {
}
removeOverlayDoc = (doc: Doc | Doc[]) => {
- (doc instanceof Doc ? [doc] : doc).forEach(doc => Doc.RemoveDocFromList(Doc.MyOverlayDocs, undefined, doc));
+ (doc instanceof Doc ? [doc] : doc).forEach(Doc.RemFromMyOverlay);
return true;
};
@@ -177,6 +178,7 @@ export class OverlayView extends React.Component {
offsety = 0;
const dref = React.createRef<HTMLDivElement>();
const onPointerMove = action((e: PointerEvent, down: number[]) => {
+ if (e.cancelBubble) return false; // if the overlay doc processed the move event (e.g., to pan its contents), then the event should be marked as canceled since propagation can't be stopped
if (e.buttons === 1) {
d.overlayX = e.clientX + offsetx;
d.overlayY = e.clientY + offsety;
@@ -196,7 +198,7 @@ export class OverlayView extends React.Component {
});
const onPointerDown = (e: React.PointerEvent) => {
- setupMoveUpEvents(this, e, onPointerMove, emptyFunction, emptyFunction);
+ setupMoveUpEvents(this, e, onPointerMove, emptyFunction, emptyFunction, false);
offsetx = NumCast(d.overlayX) - e.clientX;
offsety = NumCast(d.overlayY) - e.clientY;
};
@@ -213,8 +215,8 @@ export class OverlayView extends React.Component {
bringToFront={emptyFunction}
addDocument={undefined}
removeDocument={this.removeOverlayDoc}
- PanelWidth={d[WidthSym]}
- PanelHeight={d[HeightSym]}
+ PanelWidth={d[Width]}
+ PanelHeight={d[Height]}
ScreenToLocalTransform={this.docScreenToLocalXf(d)}
renderDepth={1}
hideDecorations={true}
@@ -226,8 +228,8 @@ export class OverlayView extends React.Component {
docViewPath={returnEmptyDoclist}
addDocTab={DocumentViewInternal.addDocTabFunc}
pinToPres={emptyFunction}
- docFilters={returnEmptyFilter}
- docRangeFilters={returnEmptyFilter}
+ childFilters={returnEmptyFilter}
+ childFiltersByRanges={returnEmptyFilter}
searchFilterDocs={returnEmptyDoclist}
/>
</div>
diff --git a/src/client/views/Palette.scss b/src/client/views/Palette.scss
deleted file mode 100644
index 0ec879288..000000000
--- a/src/client/views/Palette.scss
+++ /dev/null
@@ -1,30 +0,0 @@
-.palette-container {
- .palette-thumb {
- touch-action: pan-x;
- position: absolute;
- height: 70px;
- overflow: hidden;
-
- .palette-thumbContent {
- transition: transform .3s;
- width: max-content;
- overflow: hidden;
-
- .collectionView {
- overflow: visible;
-
- .collectionLinearView-outer {
- overflow: visible;
- }
- }
- }
-
- .palette-cover {
- width: 50px;
- height: 50px;
- position: absolute;
- bottom: 0;
- border: 1px solid black;
- }
- }
-} \ No newline at end of file
diff --git a/src/client/views/Palette.tsx b/src/client/views/Palette.tsx
deleted file mode 100644
index 3ad28c418..000000000
--- a/src/client/views/Palette.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-import { IReactionDisposer, observable, reaction } from 'mobx';
-import { observer } from 'mobx-react';
-import * as React from 'react';
-import { Doc } from '../../fields/Doc';
-import { NumCast } from '../../fields/Types';
-import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnTrue, emptyPath } from '../../Utils';
-import { Transform } from '../util/Transform';
-import { DocumentView } from './nodes/DocumentView';
-import './Palette.scss';
-
-export interface PaletteProps {
- x: number;
- y: number;
- thumb: number[];
- thumbDoc: Doc;
-}
-
-@observer
-export default class Palette extends React.Component<PaletteProps> {
- private _selectedDisposer?: IReactionDisposer;
- @observable private _selectedIndex: number = 0;
-
- componentDidMount = () => {
- this._selectedDisposer = reaction(
- () => NumCast(this.props.thumbDoc.selectedIndex),
- i => (this._selectedIndex = i),
- { fireImmediately: true }
- );
- };
-
- componentWillUnmount = () => {
- this._selectedDisposer?.();
- };
-
- render() {
- return (
- <div className="palette-container" style={{ transform: `translate(${this.props.x}px, ${this.props.y}px)` }}>
- <div className="palette-thumb" style={{ transform: `translate(${this.props.thumb[0] - this.props.x}px, ${this.props.thumb[1] - this.props.y}px)` }}>
- <div className="palette-thumbContent" style={{ transform: `translate(-${this._selectedIndex * 50 + 10}px, 0px)` }}>
- <DocumentView
- Document={this.props.thumbDoc}
- DataDoc={undefined}
- addDocument={undefined}
- addDocTab={returnFalse}
- rootSelected={returnTrue}
- pinToPres={emptyFunction}
- removeDocument={undefined}
- ScreenToLocalTransform={Transform.Identity}
- PanelWidth={() => window.screen.width}
- PanelHeight={() => window.screen.height}
- renderDepth={0}
- isDocumentActive={returnTrue}
- isContentActive={emptyFunction}
- focus={emptyFunction}
- docViewPath={returnEmptyDoclist}
- styleProvider={returnEmptyString}
- whenChildContentsActiveChanged={emptyFunction}
- bringToFront={emptyFunction}
- docFilters={returnEmptyFilter}
- docRangeFilters={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.tsx b/src/client/views/PreviewCursor.tsx
index f7fb443c0..82d2bff56 100644
--- a/src/client/views/PreviewCursor.tsx
+++ b/src/client/views/PreviewCursor.tsx
@@ -2,9 +2,8 @@ import { action, observable, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { Doc } from '../../fields/Doc';
-import { Cast, NumCast, StrCast } from '../../fields/Types';
+import { StrCast } from '../../fields/Types';
import { returnFalse } from '../../Utils';
-import { DocServer } from '../DocServer';
import { Docs, DocumentOptions, DocUtils } from '../documents/Documents';
import { ImageUtils } from '../util/Import & Export/ImageUtils';
import { Transform } from '../util/Transform';
@@ -51,38 +50,28 @@ export class PreviewCursor extends React.Component<{}> {
PreviewCursor._slowLoadDocuments?.(plain.split('v=')[1].split('&')[0], options, generatedDocuments, '', undefined, PreviewCursor._addDocument).then(batch.end);
} else if (re.test(plain)) {
const url = plain;
- undoBatch(() =>
- PreviewCursor._addDocument(
- Docs.Create.WebDocument(url, {
- title: url,
- _width: 500,
- _height: 300,
- data_useCors: true,
- x: newPoint[0],
- y: newPoint[1],
- })
- )
- )();
+ if (url.startsWith(window.location.href)) {
+ undoBatch(() =>
+ PreviewCursor._addDocument(
+ Docs.Create.WebDocument(url, {
+ title: url,
+ _width: 500,
+ _height: 300,
+ data_useCors: true,
+ x: newPoint[0],
+ y: newPoint[1],
+ })
+ )
+ )();
+ } else alert('cannot paste dash into itself');
} else if (plain.startsWith('__DashDocId(') || plain.startsWith('__DashCloneId(')) {
const clone = plain.startsWith('__DashCloneId(');
const docids = plain.split(':');
- const strs = docids[0].split(',');
+ const strs = docids[0].split(','); // hack! docids[0] is the top left of the selection rectangle
const ptx = Number(strs[0].substring((clone ? '__DashCloneId(' : '__DashDocId(').length));
const pty = Number(strs[1].substring(0, strs[1].length - 1));
+ Doc.Paste(docids.slice(1), clone, PreviewCursor._addDocument, ptx, pty, newPoint);
- const batch = UndoManager.StartBatch('cloning');
- {
- const toCopy = await Promise.all(docids.slice(1).map(async did => Cast(await DocServer.GetRefField(did), Doc, null)));
- const docs = clone ? (await Promise.all(Doc.MakeClones(toCopy, false))).map(res => res.clone) : toCopy;
- const firstx = docs.length ? NumCast(docs[0].x) + ptx - newPoint[0] : 0;
- const firsty = docs.length ? NumCast(docs[0].y) + pty - newPoint[1] : 0;
- docs.map(doc => {
- doc.x = NumCast(doc.x) - firstx;
- doc.y = NumCast(doc.y) - firsty;
- });
- PreviewCursor._addDocument(docs);
- }
- batch.end();
e.stopPropagation();
} else {
FormattedTextBox.PasteOnLoad = e;
diff --git a/src/client/views/PropertiesButtons.scss b/src/client/views/PropertiesButtons.scss
index 36b2df73e..b801b3abf 100644
--- a/src/client/views/PropertiesButtons.scss
+++ b/src/client/views/PropertiesButtons.scss
@@ -20,22 +20,31 @@ $linkGap : 3px;
.propertiesButtons-linkButton-empty,
.propertiesButtons-linkButton-nonempty {
- height: 25px;
- width: 29px;
- border-radius: 6px;
- pointer-events: auto;
- background-color: $dark-gray;
- color: #fcfbf7;
- text-transform: uppercase;
- letter-spacing: 2px;
- font-size: 75%;
- transition: transform 0.2s;
- text-align: center;
+ // margin-right: 7px;
+ // margin-left: 8px;
+ height: 28px;
+ // width: 226px;//29px;
display: flex;
- justify-content: center;
align-items: center;
- margin-right: 10px;
- margin-left: 4px;
+ // height: 25px;
+ // width: 230px;//29px;
+ // display: flex;
+ // align-items: center;
+ // border-radius: 6px;
+ pointer-events: auto;
+ // background-color: $dark-gray;
+ // color: #fcfbf7;
+ // text-transform: uppercase;
+ // letter-spacing: 2px;
+ // font-size: 75%;
+ transition: transform 0.2s;
+ // text-align: center;
+
+
+ // justify-content: center;
+
+ // margin-right: 10px;
+ // margin-left: 4px;
&:hover {
background: $medium-gray;
@@ -46,25 +55,34 @@ $linkGap : 3px;
.propertiesButtons-linkButton-empty.toggle-on {
background-color: $medium-blue;
color: $white;
+ width:100%
}
.propertiesButtons-linkButton-empty.toggle-hover {
background-color: $light-blue;
color: $black;
+ width:100%
}
.propertiesButtons-linkButton-empty.toggle-off {
- background-color: $dark-gray;
- color: white;
+ background-color: white;//$dark-gray;
+ color: black; //white;
+ width:100%
+}
+
+.propertiesButtons-icon {
+ margin-left:8px;
}
.propertiesButtons {
- margin-top: 3px;
- grid-column: 1/4;
+ position:relative;
width: 100%;
- height: auto;
- display: flex;
- flex-direction: row;
- flex-wrap: wrap;
- padding-bottom: 5.5px;
+ // margin-top: 3px;
+// // grid-column: 1/4;
+// width: 100%;
+// height: auto;
+// display: flex;
+// // flex-direction: row;
+// // flex-wrap: wrap;
+// padding-bottom: 5.5px;
}
.onClickFlyout-editScript {
@@ -80,8 +98,8 @@ $linkGap : 3px;
.propertiesButtons-button {
pointer-events: auto;
- padding-right: 5px;
- width: 25px;
+ padding-right: 8px;//5px;
+ width: 100%;//width: 25px;
border-radius: 5px;
margin-right: 20px;
margin-bottom: 8px;
@@ -110,18 +128,24 @@ $linkGap : 3px;
}
}
-.propertiesButtons-title {
- background: $dark-gray;
- color: $white;
- font-size: 6px;
- width: 37px;
- padding: 3px;
- height: 12px;
- border-radius: 7px;
+.propertiesButtons-label {
text-transform: uppercase;
- text-align: center;
- margin-top: -4px;
-}
+ margin-left: 8px;
+ // margin-right: 50 px;
+}
+
+// .propertiesButtons-title {
+// background: pink; //$dark-gray;
+// color: $white;
+// font-size: 6px;
+// width: 37px;
+// padding: 3px;
+// height: 12px;
+// border-radius: 7px;
+// text-transform: uppercase;
+// text-align: center;
+// margin-top: -4px;
+// }
.propertiesButtons-linker {
height: 25px;
diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx
index 86230ca78..8cae34d7d 100644
--- a/src/client/views/PropertiesButtons.tsx
+++ b/src/client/views/PropertiesButtons.tsx
@@ -1,27 +1,37 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Tooltip } from '@material-ui/core';
+import { Icon, Tooltip } from '@material-ui/core';
import { action, computed, observable } from 'mobx';
import { observer } from 'mobx-react';
import { Doc, DocListCast, Opt } from '../../fields/Doc';
import { Id } from '../../fields/FieldSymbols';
import { InkField } from '../../fields/InkField';
import { RichTextField } from '../../fields/RichTextField';
-import { ScriptField } from '../../fields/ScriptField';
import { BoolCast, ScriptCast, StrCast } from '../../fields/Types';
import { ImageField } from '../../fields/URLField';
import { Utils } from '../../Utils';
import { DocUtils } from '../documents/Documents';
import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
+import { IsFollowLinkScript } from '../util/LinkFollower';
import { LinkManager } from '../util/LinkManager';
import { SelectionManager } from '../util/SelectionManager';
-import { undoBatch } from '../util/UndoManager';
+import { undoable, undoBatch } from '../util/UndoManager';
import { Colors } from './global/globalEnums';
import { InkingStroke } from './InkingStroke';
import { DocumentView, OpenWhere } from './nodes/DocumentView';
import { pasteImageBitmap } from './nodes/WebBoxRenderer';
import './PropertiesButtons.scss';
import React = require('react');
-import { IsFollowLinkScript } from '../util/LinkFollower';
+import { JsxElement } from 'typescript';
+import { FaBraille, FaHighlighter, FaLock, FaLockOpen, FaThumbtack } from 'react-icons/fa';
+import { AiOutlineApple, AiOutlineColumnWidth, AiOutlinePicture } from 'react-icons/ai';
+import { MdClosedCaption, MdClosedCaptionDisabled, MdGridOff, MdGridOn, MdSubtitles, MdSubtitlesOff, MdTouchApp } from 'react-icons/md';
+import { TbEditCircle, TbEditCircleOff, TbHandOff, TbHandStop, TbHighlight, TbHighlightOff } from 'react-icons/tb';
+import { BiHide, BiShow } from 'react-icons/bi';
+import { BsGrid3X3GapFill } from 'react-icons/bs';
+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;
@@ -31,6 +41,7 @@ enum UtilityButtonState {
OpenRight,
OpenExternally,
}
+
@observer
export class PropertiesButtons extends React.Component<{}, {}> {
@observable public static Instance: PropertiesButtons;
@@ -42,251 +53,354 @@ export class PropertiesButtons extends React.Component<{}, {}> {
return !SelectionManager.SelectedSchemaDoc() && SelectionManager.Views().lastElement()?.topMost;
}
- propertyToggleBtn = (label: string, property: string, tooltip: (on?: any) => string, icon: (on: boolean) => string, onClick?: (dv: Opt<DocumentView>, doc: Doc, property: string) => void, useUserDoc?: boolean) => {
+ propertyToggleBtn = (label: (on?: any) => string, property: string, tooltip: (on?: any) => string, icon: (on?: any) => any, onClick?: (dv: Opt<DocumentView>, doc: Doc, property: string) => void, useUserDoc?: boolean) => {
const targetDoc = useUserDoc ? Doc.UserDoc() : this.selectedDoc;
const onPropToggle = (dv: Opt<DocumentView>, doc: Doc, prop: string) => ((dv?.layoutDoc || doc)[prop] = (dv?.layoutDoc || doc)[prop] ? false : true);
return !targetDoc ? null : (
- <Tooltip title={<div className={`dash-tooltip`}>{tooltip(targetDoc?.[property])} </div>} placement="top">
- <div>
- <div
- className={`propertiesButtons-linkButton-empty toggle-${StrCast(targetDoc[property]).includes(':hover') ? 'hover' : targetDoc[property] ? 'on' : 'off'}`}
- onPointerDown={e => e.stopPropagation()}
- onClick={undoBatch(() => {
- if (SelectionManager.Views().length > 1) {
- SelectionManager.Views().forEach(dv => (onClick ?? onPropToggle)(dv, dv.rootDoc, property));
- } else if (targetDoc) (onClick ?? onPropToggle)(undefined, targetDoc, property);
- })}>
- <FontAwesomeIcon className="documentdecorations-icon" size="lg" icon={icon(BoolCast(targetDoc?.[property])) as any} />
- </div>
- <div className="propertiesButtons-title">{label}</div>
- </div>
- </Tooltip>
+ <Toggle
+ toggleStatus={BoolCast(targetDoc[property])}
+ text={label(targetDoc?.[property])}
+ color={StrCast(Doc.UserDoc().userColor)}
+ icon={icon(targetDoc?.[property] as any)}
+ iconPlacement={'left'}
+ align={'flex-start'}
+ fillWidth={true}
+ toggleType={ToggleType.BUTTON}
+ onClick={undoable(() => {
+ if (SelectionManager.Views().length > 1) {
+ SelectionManager.Views().forEach(dv => (onClick ?? onPropToggle)(dv, dv.rootDoc, property));
+ } else if (targetDoc) (onClick ?? onPropToggle)(undefined, targetDoc, property);
+ }, property)}
+ />
);
};
+
+ // this implments a container pattern by marking the targetDoc (collection) as a lightbox
+ // that always fits its contents to its container and that hides all other documents when
+ // a link is followed that targets a 'lightbox' destination
+ @computed get isLightboxButton() {
+ return this.propertyToggleBtn(
+ on => 'Lightbox',
+ 'isLightbox',
+ on => `${on ? 'Set' : 'Remove'} lightbox flag`,
+ on => 'window-restore',
+ onClick => {
+ SelectionManager.Views().forEach(dv => {
+ const containerDoc = dv.rootDoc;
+ //containerDoc.followAllLinks =
+ // containerDoc.noShadow =
+ // containerDoc.layout_disableBrushing =
+ // containerDoc._forceActive =
+ //containerDoc._freeform_fitContentsToBox =
+ containerDoc._isLightbox = !containerDoc._isLightbox;
+ //containerDoc._xPadding = containerDoc._yPadding = containerDoc._isLightbox ? 10 : undefined;
+ const containerContents = DocListCast(dv.dataDoc[dv.props.fieldKey ?? Doc.LayoutFieldKey(containerDoc)]);
+ //dv.rootDoc.onClick = ScriptField.MakeScript('{self.data = undefined; documentView.select(false)}', { documentView: 'any' });
+ containerContents.forEach(doc => LinkManager.Links(doc).forEach(link => (link.link_displayLine = false)));
+ });
+ }
+ );
+ }
+
+ @computed get titleButton() {
+ return this.propertyToggleBtn(
+ on => (!on ? 'SHOW TITLE' : this.selectedDoc?.['_layout_showTitle'] === 'title:hover' ? 'HIDE TITLE' : 'HOVER TITLE'),
+ '_layout_showTitle',
+ on => 'Switch between title styles',
+ on => (on ? <MdSubtitlesOff /> : <MdSubtitles />), // {currentIcon}, //(on ? <MdSubtitles/> :) , //,'text-width', on ? <MdSubtitles/> : <MdSubtitlesOff/>,
+ (dv, doc) => {
+ const tdoc = dv?.rootDoc || doc;
+ const newtitle = !tdoc._layout_showTitle ? 'title' : tdoc._layout_showTitle === 'title' ? 'title:hover' : '';
+ tdoc._layout_showTitle = newtitle ? newtitle : undefined;
+ }
+ );
+ }
+
@computed get lockButton() {
return this.propertyToggleBtn(
- 'No\xA0Drag',
+ on => (on ? 'UNLOCK' : 'LOCK'), // 'No\xA0Drag',
'_lockedPosition',
on => `${on ? 'Unlock' : 'Lock'} position to prevent dragging`,
- on => 'thumbtack'
+ on => (on ? <FaLockOpen /> : <FaLock />)
+ // on => 'thumbtack'
);
}
+
@computed get maskButton() {
+ //highlight text while going down and reading through
return this.propertyToggleBtn(
- 'Mask',
+ on => (on ? 'PLAIN INK' : 'HIGHLIGHTER MASK'),
'stroke_isInkMask',
on => (on ? 'Make plain ink' : 'Make highlight mask'),
- on => 'paint-brush',
+ on => (on ? <TbHighlightOff /> : <TbHighlight />), // <FaHighlighter/>,// 'paint-brush',
(dv, doc) => InkingStroke.toggleMask(dv?.layoutDoc || doc)
);
}
+
@computed get hideImageButton() {
+ // put in developer -- can trace on top of object and drawing is still there
return this.propertyToggleBtn(
- 'Background',
+ on => (on ? 'SHOW BACKGROUND IMAGE' : 'HIDE BACKGROUND IMAGE'), //'Background',
'_hideImage',
on => (on ? 'Show Image' : 'Show Background'),
- on => 'portrait'
+ on => (on ? <BiShow /> : <BiHide />) //'portrait'
);
}
+
@computed get clustersButton() {
return this.propertyToggleBtn(
- 'Clusters',
+ on => (on ? 'DISABLE CLUSTERS' : 'HIGHLIGHT CLUSTERS'),
'_freeform_useClusters',
on => `${on ? 'Hide' : 'Show'} clusters`,
- on => 'braille'
+ on => <FaBraille />
);
}
@computed get panButton() {
return this.propertyToggleBtn(
- 'Lock\xA0View',
+ on => (on ? 'ENABLE PANNING' : 'DISABLE PANNING'), //'Lock\xA0View',
'_lockedTransform',
on => `${on ? 'Unlock' : 'Lock'} panning of view`,
- on => 'lock'
+ on => (on ? <TbHandStop /> : <TbHandOff />) //'lock'
);
}
+
@computed get forceActiveButton() {
+ //select text
return this.propertyToggleBtn(
- 'Active',
+ on => (on ? 'INACTIVE INTERACTION' : 'ACTIVE INTERACTION'),
'_forceActive',
on => `${on ? 'Select to activate' : 'Contents always active'} `,
- on => 'eye'
+ on => <MdTouchApp /> // 'eye'
);
}
+
@computed get fitContentButton() {
return this.propertyToggleBtn(
- 'View All',
+ on => (on ? 'PREVIOUS VIEW' : 'VIEW ALL'), //'View All',
'_freeform_fitContentsToBox',
on => `${on ? "Don't" : 'Do'} fit content to container visible area`,
- on => 'object-group'
- );
- }
- // this implments a container pattern by marking the targetDoc (collection) as a lightbox
- // that always fits its contents to its container and that hides all other documents when
- // a link is followed that targets a 'lightbox' destination
- @computed get isLightboxButton() {
- return this.propertyToggleBtn(
- 'Lightbox',
- 'isLightbox',
- on => `${on ? 'Set' : 'Remove'} lightbox flag`,
- on => 'window-restore',
- onClick => {
- SelectionManager.Views().forEach(dv => {
- const containerDoc = dv.rootDoc;
- //containerDoc.followAllLinks =
- // containerDoc.noShadow =
- // containerDoc.disableDocBrushing =
- // containerDoc._forceActive =
- //containerDoc._freeform_fitContentsToBox =
- containerDoc._isLightbox = !containerDoc._isLightbox;
- //containerDoc._xPadding = containerDoc._yPadding = containerDoc._isLightbox ? 10 : undefined;
- const containerContents = DocListCast(dv.dataDoc[dv.props.fieldKey ?? Doc.LayoutFieldKey(containerDoc)]);
- //dv.rootDoc.onClick = ScriptField.MakeScript('{self.data = undefined; documentView.select(false)}', { documentView: 'any' });
- containerContents.forEach(doc => LinkManager.Links(doc).forEach(link => (link.layout_linkDisplay = false)));
- });
- }
+ on => (on ? <CiGrid31 /> : <BsGrid3X3GapFill />) //'object-group'
);
}
+
+ // // this implments a container pattern by marking the targetDoc (collection) as a lightbox
+ // // that always fits its contents to its container and that hides all other documents when
+ // // a link is followed that targets a 'lightbox' destination
+ // @computed get isLightboxButton() { // developer
+ // return this.propertyToggleBtn(
+ // on => 'Lightbox',
+ // 'isLightbox',
+ // on => `${on ? 'Set' : 'Remove'} lightbox flag`,
+ // on => 'window-restore',
+ // onClick => {
+ // SelectionManager.Views().forEach(dv => {
+ // const containerDoc = dv.rootDoc;
+ // //containerDoc.followAllLinks =
+ // // containerDoc.noShadow =
+ // // containerDoc.disableDocBrushing =
+ // // containerDoc._forceActive =
+ // //containerDoc._freeform_fitContentsToBox =
+ // containerDoc._isLightbox = !containerDoc._isLightbox;
+ // //containerDoc._xPadding = containerDoc._yPadding = containerDoc._isLightbox ? 10 : undefined;
+ // const containerContents = DocListCast(dv.dataDoc[dv.props.fieldKey ?? Doc.LayoutFieldKey(containerDoc)]);
+ // //dv.rootDoc.onClick = ScriptField.MakeScript('{self.data = undefined; documentView.select(false)}', { documentView: 'any' });
+ // containerContents.forEach(doc => LinkManager.Links(doc).forEach(link => (link.layout_linkDisplay = false)));
+ // });
+ // }
+ // );
+ // }
+
@computed get layout_fitWidthButton() {
return this.propertyToggleBtn(
- 'Fit\xA0Width',
+ on => (on ? 'RESTRICT WIDTH' : 'FIT WIDTH'), //'Fit\xA0Width',
'_layout_fitWidth',
on => `${on ? "Don't" : 'Do'} fit content to width of container`,
- on => 'arrows-alt-h'
+ on => (on ? <AiOutlineColumnWidth /> : <RxWidth />) // 'arrows-alt-h'
);
}
+
@computed get captionButton() {
return this.propertyToggleBtn(
- 'Caption',
+ //DEVELOPER
+ on => (on ? 'HIDE CAPTION' : 'SHOW CAPTION'), //'Caption',
'_layout_showCaption',
on => `${on ? 'Hide' : 'Show'} caption footer`,
- on => 'closed-captioning',
+ on => (on ? <MdClosedCaptionDisabled /> : <MdClosedCaption />), //'closed-captioning',
(dv, doc) => ((dv?.rootDoc || doc)._layout_showCaption = (dv?.rootDoc || doc)._layout_showCaption === undefined ? 'caption' : undefined)
);
}
+
@computed get chromeButton() {
+ // developer -- removing UI decoration
return this.propertyToggleBtn(
- 'Controls',
+ on => (on ? 'ENABLE UI CONTROLS' : 'DISABLE UI CONTROLS'),
'_chromeHidden',
on => `${on ? 'Show' : 'Hide'} editing UI`,
- on => 'edit',
+ on => (on ? <TbEditCircle /> : <TbEditCircleOff />), // 'edit',
(dv, doc) => ((dv?.rootDoc || doc)._chromeHidden = !(dv?.rootDoc || doc)._chromeHidden)
);
}
- @computed get titleButton() {
- return this.propertyToggleBtn(
- 'Title',
- '_layout_showTitle',
- on => 'Switch between title styles',
- on => 'text-width',
- (dv, doc) => {
- const tdoc = dv?.rootDoc || doc;
- const newtitle = !tdoc._layout_showTitle ? 'title' : tdoc._layout_showTitle === 'title' ? 'title:hover' : '';
- tdoc._layout_showTitle = newtitle;
- }
- );
- }
+
@computed get layout_autoHeightButton() {
+ // store previous dimensions to store old values
return this.propertyToggleBtn(
- 'Auto\xA0Size',
+ on => (on ? 'AUTO\xA0SIZE' : 'FIXED SIZE'),
'_layout_autoHeight',
on => `Automatical vertical sizing to show all content`,
- on => 'arrows-alt-v'
+ on => <FontAwesomeIcon icon="arrows-alt-v" size="lg" />
);
}
+
@computed get gridButton() {
return this.propertyToggleBtn(
- 'Grid',
+ on => (on ? 'HIDE GRID' : 'DISPLAY GRID'),
'_freeform_backgroundGrid',
on => `Display background grid in collection`,
- on => 'border-all'
- );
- }
- @computed get groupButton() {
- return this.propertyToggleBtn(
- 'Group',
- 'isGroup',
- on => `Display collection as a Group`,
- on => 'object-group',
- (dv, doc) => {
- doc.isGroup = !doc.isGroup;
- doc.forceActive = doc.isGroup;
- }
- );
- }
- @computed get freezeThumb() {
- return this.propertyToggleBtn(
- 'FreezeThumb',
- '_thumb-frozen',
- on => `${on ? 'Freeze' : 'Unfreeze'} thumbnail`,
- on => 'snowflake',
- (dv, doc) => {
- if (doc['thumb-frozen']) doc['thumb-frozen'] = undefined;
- else {
- document.body.focus(); // so that we can access the clipboard without an error
- setTimeout(() =>
- pasteImageBitmap((data_url: any, error: any) => {
- error && console.log(error);
- data_url && Utils.convertDataUri(data_url, doc[Id] + '-thumb-frozen', true).then(returnedfilename => (doc['thumb-frozen'] = new ImageField(returnedfilename)));
- })
- );
- }
- }
+ on => (on ? <MdGridOff /> : <MdGridOn />) //'border-all'
);
}
+
+ // @computed get groupButton() { //developer
+ // return this.propertyToggleBtn(
+ // on => 'Group',
+ // 'isGroup',
+ // on => `Display collection as a Group`,
+ // on => 'object-group',
+ // (dv, doc) => {
+ // doc.isGroup = !doc.isGroup;
+ // doc.forceActive = doc.isGroup;
+ // }
+ // );
+ // }
+
+ // @computed get freezeThumb() {
+ // return this.propertyToggleBtn(
+ // 'FreezeThumb',
+ // '_thumb-frozen',
+ // on => `${on ? 'Freeze' : 'Unfreeze'} thumbnail`,
+ // on => 'snowflake',
+ // (dv, doc) => {
+ // if (doc['thumb-frozen']) doc['thumb-frozen'] = undefined;
+ // else {
+ // document.body.focus(); // so that we can access the clipboard without an error
+ // setTimeout(() =>
+ // pasteImageBitmap((data_url: any, error: any) => {
+ // error && console.log(error);
+ // data_url && Utils.convertDataUri(data_url, doc[Id] + '-thumb-frozen', true).then(returnedfilename => (doc['thumb-frozen'] = new ImageField(returnedfilename)));
+ // })
+ // );
+ // }
+ // }
+ // );
+ // }
@computed get snapButton() {
+ // THESE ARE NOT COMING
return this.propertyToggleBtn(
- 'Snap\xA0Lines',
+ on => (on ? 'HIDE SNAP LINES' : 'SHOW SNAP LINES'),
'freeform_snapLines',
on => `Display snapping lines when objects are dragged`,
- on => 'th',
- undefined,
- true
+ on => <TfiBarChart />, //'th',
+ undefined
);
}
- @computed
- get onClickButton() {
- return !this.selectedDoc ? null : (
- <Tooltip title={<div className="dash-tooltip">Choose onClick behavior</div>} placement="top">
- <div>
- <div className="propertiesButtons-linkFlyout">
- <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={this.onClickFlyout}>
- <div className={'propertiesButtons-linkButton-empty'} onPointerDown={e => e.stopPropagation()}>
- <FontAwesomeIcon className="documentdecorations-icon" icon="mouse-pointer" size="lg" />
- </div>
- </Flyout>
- </div>
- <div className="propertiesButtons-title"> onclick </div>
- </div>
- </Tooltip>
- );
- }
- @computed
- get perspectiveButton() {
- return !this.selectedDoc ? null : (
- <Tooltip title={<div className="dash-tooltip">Choose view perspective</div>} placement="top">
- <div>
- <div className="propertiesButtons-linkFlyout">
- <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={this.onPerspectiveFlyout}>
- <div className={'propertiesButtons-linkButton-empty'} onPointerDown={e => e.stopPropagation()}>
- <FontAwesomeIcon className="documentdecorations-icon" icon="mouse-pointer" size="lg" />
- </div>
- </Flyout>
- </div>
- <div className="propertiesButtons-title"> Perspective </div>
- </div>
- </Tooltip>
- );
- }
+ // @computed
+ // get onClickButton() {
+ // return !this.selectedDoc ? null : (
+ // <Tooltip title={<div className="dash-tooltip">Choose onClick behavior</div>} placement="top">
+ // <div>
+ // <div className="propertiesButtons-linkFlyout">
+ // <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={this.onClickFlyout}>
+ // <div className={'propertiesButtons-linkButton-empty'} onPointerDown={e => e.stopPropagation()}>
+ // <FontAwesomeIcon className="documentdecorations-icon" icon="mouse-pointer" size="lg" />
+ // </div>
+ // </Flyout>
+ // </div>
+ // <div className="propertiesButtons-title"> onclick </div>
+ // </div>
+ // </Tooltip>
+ // );
+ // }
+ // @computed
+ // get perspectiveButton() { // gone
+ // return !this.selectedDoc ? null : (
+ // <Tooltip title={<div className="dash-tooltip">Choose view perspective</div>} placement="top">
+ // <div>
+ // <div className="propertiesButtons-linkFlyout">
+ // <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={this.onPerspectiveFlyout}>
+ // <div className={'propertiesButtons-linkButton-empty'} onPointerDown={e => e.stopPropagation()}>
+ // <FontAwesomeIcon className="documentdecorations-icon" icon="mouse-pointer" size="lg" />
+ // </div>
+ // </Flyout>
+ // </div>
+ // <div className="propertiesButtons-title"> Perspective </div>
+ // </div>
+ // </Tooltip>
+ // );
+ // }
@undoBatch
handlePerspectiveChange = (e: any) => {
- this.selectedDoc && (this.selectedDoc._viewType = e.target.value);
+ this.selectedDoc && (this.selectedDoc._type_collection = e.target.value);
SelectionManager.Views()
.filter(dv => dv.docView)
.map(dv => dv.docView!)
- .forEach(docView => (docView.layoutDoc._viewType = e.target.value));
+ .forEach(docView => (docView.layoutDoc._type_collection = e.target.value));
};
+ @computed get onClickVal() {
+ const linkButton = IsFollowLinkScript(this.selectedDoc.onClick);
+ const followLoc = this.selectedDoc._followLinkLocation;
+ const linkedToLightboxView = () => LinkManager.Links(this.selectedDoc).some(link => LinkManager.getOppositeAnchor(link, this.selectedDoc)?._isLightbox);
+
+ if (followLoc === OpenWhere.lightbox && !linkedToLightboxView()) return 'linkInPlace';
+ else if (linkButton && followLoc === OpenWhere.addRight) return 'linkOnRight';
+ else if (linkButton && this.selectedDoc._followLinkLocation === OpenWhere.lightbox && linkedToLightboxView()) return 'enterPortal';
+ else if (ScriptCast(this.selectedDoc.onClick)?.script.originalScript.includes('toggleDetail')) return 'toggleDetail';
+ else return 'nothing';
+ }
+
+ @computed
+ get onClickButton() {
+ const buttonList = [
+ ['nothing', 'Select Document'],
+ ['enterPortal', 'Enter Portal'],
+ ['toggleDetail', 'Toggle Detail'],
+ ['linkInPlace', 'Open Link in Lightbox'],
+ ['linkOnRight', 'Open Link on Right'],
+ ];
+
+ const items: IListItemProps[] = buttonList.map(value => {
+ return {
+ text: value[1],
+ val: value[1],
+ };
+ });
+ return !this.selectedDoc ? null : (
+ <Dropdown
+ tooltip={'Choose onClick behavior'}
+ items={items}
+ selectedVal={this.onClickVal}
+ setSelectedVal={val => this.handleOptionChange(val as string)}
+ title={'Choose onClick behaviour'}
+ color={StrCast(Doc.UserDoc().userColor)}
+ dropdownType={DropdownType.SELECT}
+ type={Type.SEC}
+ fillWidth
+ />
+ // <Tooltip title={<div className="dash-tooltip">Choose onClick behavior</div>} placement="top">
+ // <div>
+ // <div className="propertiesButtons-linkFlyout">
+ // <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={this.onClickFlyout}>
+ // <div className={'propertiesButtons-linkButton-empty'} onPointerDown={e => e.stopPropagation()}>
+ // <FontAwesomeIcon className="documentdecorations-icon" icon="mouse-pointer" size="lg" />
+ // </div>
+ // </Flyout>
+ // </div>
+ // <div className="propertiesButtons-title"> onclick </div>
+ // </div>
+ // </Tooltip>
+ );
+ }
@undoBatch
@action
@@ -366,26 +480,6 @@ export class PropertiesButtons extends React.Component<{}, {}> {
</div>
);
}
- @computed
- get onPerspectiveFlyout() {
- const excludedViewTypes = [CollectionViewType.Invalid, CollectionViewType.Docking, CollectionViewType.Pile, CollectionViewType.StackedTimeline, CollectionViewType.Linear];
-
- const makeLabel = (value: string, label: string) => (
- <div className="radio" key={label}>
- <label>
- <input type="radio" value={value} checked={(this.selectedDoc?._viewType ?? 'invalid') === value} onChange={this.handlePerspectiveChange} />
- {label}
- </label>
- </div>
- );
- return (
- <form>
- {Object.values(CollectionViewType)
- .filter(type => !excludedViewTypes.includes(type))
- .map(type => makeLabel(type, type))}
- </form>
- );
- }
render() {
const layoutField = this.selectedDoc?.[Doc.LayoutFieldKey(this.selectedDoc)];
@@ -395,9 +489,9 @@ export class PropertiesButtons extends React.Component<{}, {}> {
const isMap = this.selectedDoc?.type === DocumentType.MAP;
const isCollection = this.selectedDoc?.type === DocumentType.COL;
//TODO: will likely need to create separate note-taking view type here
- const isStacking = this.selectedDoc?._viewType === CollectionViewType.Stacking || this.selectedDoc?._viewType === CollectionViewType.NoteTaking;
- const isFreeForm = this.selectedDoc?._viewType === CollectionViewType.Freeform;
- const isTree = this.selectedDoc?._viewType === CollectionViewType.Tree;
+ const isStacking = this.selectedDoc?._type_collection === CollectionViewType.Stacking || this.selectedDoc?._type_collection === CollectionViewType.NoteTaking;
+ const isFreeForm = this.selectedDoc?._type_collection === CollectionViewType.Freeform;
+ const isTree = this.selectedDoc?._type_collection === CollectionViewType.Tree;
const isTabView = this.selectedTabView;
const toggle = (ele: JSX.Element | null, style?: React.CSSProperties) => (
<div className="propertiesButtons-button" style={style}>
@@ -411,22 +505,21 @@ export class PropertiesButtons extends React.Component<{}, {}> {
{toggle(this.titleButton)}
{toggle(this.captionButton)}
{toggle(this.lockButton)}
- {toggle(this.onClickButton)}
+ {/* {toggle(this.onClickButton)} */}
{toggle(this.layout_fitWidthButton)}
- {toggle(this.freezeThumb)}
- {toggle(this.forceActiveButton, { display: !isFreeForm && !isMap ? 'none' : '' })}
+ {/* {toggle(this.freezeThumb)} */}
+ {toggle(this.forceActiveButton)}
{toggle(this.fitContentButton, { display: !isFreeForm && !isMap ? 'none' : '' })}
- {toggle(this.isLightboxButton, { display: !isFreeForm && !isMap ? 'none' : '' })}
+ {/* {toggle(this.isLightboxButton, { display: !isFreeForm && !isMap ? 'none' : '' })} */}
{toggle(this.layout_autoHeightButton, { display: !isText && !isStacking && !isTree ? 'none' : '' })}
{toggle(this.maskButton, { display: !isInk ? 'none' : '' })}
{toggle(this.hideImageButton, { display: !isImage ? 'none' : '' })}
{toggle(this.chromeButton, { display: !isCollection || isNovice ? 'none' : '' })}
{toggle(this.gridButton, { display: !isCollection ? 'none' : '' })}
- {toggle(this.groupButton, { display: isTabView || !isCollection ? 'none' : '' })}
+ {/* {toggle(this.groupButton, { display: isTabView || !isCollection ? 'none' : '' })} */}
{toggle(this.snapButton, { display: !isCollection ? 'none' : '' })}
{toggle(this.clustersButton, { display: !isFreeForm ? 'none' : '' })}
{toggle(this.panButton, { display: !isFreeForm ? 'none' : '' })}
- {toggle(this.perspectiveButton, { display: !isCollection || isNovice ? 'none' : '' })}
</div>
);
}
diff --git a/src/client/views/PropertiesDocBacklinksSelector.scss b/src/client/views/PropertiesDocBacklinksSelector.scss
index 4d2a61c3b..5c53acf48 100644
--- a/src/client/views/PropertiesDocBacklinksSelector.scss
+++ b/src/client/views/PropertiesDocBacklinksSelector.scss
@@ -1,4 +1,4 @@
-.propertiesView-contexts-content {
+.propertiesDocBacklinksSelector {
.linkMenu {
position: relative;
}
diff --git a/src/client/views/PropertiesDocBacklinksSelector.tsx b/src/client/views/PropertiesDocBacklinksSelector.tsx
index 46e6fd188..da4438481 100644
--- a/src/client/views/PropertiesDocBacklinksSelector.tsx
+++ b/src/client/views/PropertiesDocBacklinksSelector.tsx
@@ -19,19 +19,19 @@ type PropertiesDocBacklinksSelectorProps = {
@observer
export class PropertiesDocBacklinksSelector extends React.Component<PropertiesDocBacklinksSelectorProps> {
getOnClick = (link: Doc) => {
- const linkSource = this.props.Document;
- const other = LinkManager.getOppositeAnchor(link, linkSource);
+ const linkAnchor = this.props.Document;
+ const other = LinkManager.getOppositeAnchor(link, linkAnchor);
const otherdoc = !other ? undefined : other.annotationOn && other.type !== DocumentType.RTF ? Cast(other.annotationOn, Doc, null) : other;
if (otherdoc) {
otherdoc.hidden = false;
- this.props.addDocTab(Doc.IsDocDataProto(otherdoc) ? Doc.MakeDelegate(otherdoc) : otherdoc, OpenWhere.toggleRight);
+ this.props.addDocTab(Doc.IsDataProto(otherdoc) ? Doc.MakeDelegate(otherdoc) : otherdoc, OpenWhere.toggleRight);
}
};
render() {
return !SelectionManager.Views().length ? null : (
- <div className="preroptiesDocBacklinksSelector">
+ <div className="propertiesDocBacklinksSelector">
{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/PropertiesDocContextSelector.tsx b/src/client/views/PropertiesDocContextSelector.tsx
index 12a09196e..d157e7b1c 100644
--- a/src/client/views/PropertiesDocContextSelector.tsx
+++ b/src/client/views/PropertiesDocContextSelector.tsx
@@ -3,11 +3,10 @@ import { observer } from 'mobx-react';
import * as React from 'react';
import { Doc, DocListCast } from '../../fields/Doc';
import { Id } from '../../fields/FieldSymbols';
-import { Cast, NumCast, StrCast } from '../../fields/Types';
-import { CollectionViewType } from '../documents/DocumentTypes';
+import { Cast, StrCast } from '../../fields/Types';
import { DocFocusOrOpen } from '../util/DocumentManager';
import { CollectionDockingView } from './collections/CollectionDockingView';
-import { DocumentView, OpenWhere, OpenWhereMod } from './nodes/DocumentView';
+import { DocumentView, OpenWhere } from './nodes/DocumentView';
import './PropertiesDocContextSelector.scss';
type PropertiesDocContextSelectorProps = {
@@ -39,6 +38,7 @@ export class PropertiesDocContextSelector extends React.Component<PropertiesDocC
}, new Set<Doc>())
.keys()
);
+
return doclayouts
.filter(doc => !Doc.AreProtosEqual(doc, CollectionDockingView.Instance?.props.Document))
.filter(doc => !Doc.IsSystem(doc))
@@ -48,11 +48,12 @@ export class PropertiesDocContextSelector extends React.Component<PropertiesDocC
getOnClick = (col: Doc, target: Doc) => {
if (!this.props.DocView) return;
- col = Doc.IsDocDataProto(col) ? Doc.MakeDelegate(col) : col;
+ col = Doc.IsDataProto(col) ? Doc.MakeDelegate(col) : col;
DocFocusOrOpen(Doc.GetProto(this.props.DocView.props.Document), undefined, col);
};
render() {
+ if (this._docs.length < 1) return undefined;
return (
<div>
{this.props.hideTitle ? null : <p key="contexts">Contexts:</p>}
diff --git a/src/client/views/PropertiesSection.scss b/src/client/views/PropertiesSection.scss
new file mode 100644
index 000000000..321b6300c
--- /dev/null
+++ b/src/client/views/PropertiesSection.scss
@@ -0,0 +1,24 @@
+@import './global/globalCssVariables.scss';
+
+.propertiesView-section {
+
+ .propertiesView-content {
+ padding: 10px;
+ }
+
+ .propertiesView-sectionTitle {
+ text-align: center;
+ display: flex;
+ padding: 3px 10px;
+ font-size: 14px;
+ font-weight: bold;
+ justify-content: space-between;
+ align-items: center;
+
+ .propertiesView-sectionTitle-icon {
+ width: 20px;
+ height: 20px;
+ align-items: flex-end;
+ }
+ }
+}
diff --git a/src/client/views/PropertiesSection.tsx b/src/client/views/PropertiesSection.tsx
new file mode 100644
index 000000000..6fab0168b
--- /dev/null
+++ b/src/client/views/PropertiesSection.tsx
@@ -0,0 +1,64 @@
+import React = require('react');
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { action, computed, observable } from 'mobx';
+import { observer } from 'mobx-react';
+import './PropertiesSection.scss';
+import { Doc } from '../../fields/Doc';
+import { StrCast } from '../../fields/Types';
+
+export interface PropertiesSectionProps {
+ title: string;
+ content?: JSX.Element | string | null;
+ isOpen: boolean;
+ setIsOpen: (bool: boolean) => any;
+ inSection?: boolean;
+ setInSection?: (bool: boolean) => any;
+ onDoubleClick?: () => void;
+}
+
+@observer
+export class PropertiesSection extends React.Component<PropertiesSectionProps> {
+ @computed get color() {
+ return StrCast(Doc.UserDoc().userColor);
+ }
+
+ @computed get backgroundColor() {
+ return StrCast(Doc.UserDoc().userBackgroundColor);
+ }
+
+ @computed get variantColor() {
+ return StrCast(Doc.UserDoc().userVariantColor);
+ }
+
+ @observable isDouble: boolean = false;
+
+ render() {
+ if (this.props.content === undefined || this.props.content === null) return null;
+ else
+ return (
+ <div className="propertiesView-section" onPointerEnter={action(() => this.props.setInSection && this.props.setInSection(true))} onPointerLeave={action(() => this.props.setInSection && this.props.setInSection(false))}>
+ <div
+ className="propertiesView-sectionTitle"
+ onDoubleClick={action(e => {
+ this.isDouble = true;
+ this.props.onDoubleClick && this.props.onDoubleClick();
+ this.props.setIsOpen(true);
+ setTimeout(() => (this.isDouble = false), 300);
+ })}
+ onClick={action(e => {
+ this.props.setIsOpen(!this.props.isOpen);
+ })}
+ style={{
+ background: this.props.isOpen ? this.variantColor : this.backgroundColor,
+ color: this.color,
+ }}>
+ {this.props.title}
+ <div className="propertiesView-sectionTitle-icon">
+ <FontAwesomeIcon icon={this.props.isOpen ? 'caret-down' : 'caret-right'} size="lg" />
+ </div>
+ </div>
+ {!this.props.isOpen ? null : <div className="propertiesView-content">{this.props.content}</div>}
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/PropertiesView.scss b/src/client/views/PropertiesView.scss
index 897be9a32..b79486167 100644
--- a/src/client/views/PropertiesView.scss
+++ b/src/client/views/PropertiesView.scss
@@ -6,122 +6,31 @@
font-family: 'Roboto';
font-size: 12px;
cursor: auto;
+ border-left: $standard-border;
.slider-text {
font-size: 8px;
}
- overflow-x: hidden;
- overflow-y: auto;
-
.propertiesView-title {
- text-align: center;
- padding-top: 12px;
- padding-bottom: 12px;
- display: flex;
- font-size: 18px;
+ padding: 10px;
+ font-size: 24px;
font-weight: bold;
- justify-content: center;
-
- .propertiesView-title-icon {
- width: 20px;
- height: 20px;
- padding-left: 38px;
- margin-top: -5px;
- align-items: flex-end;
- margin-left: auto;
- margin-right: 10px;
-
- &:hover {
- color: grey;
- cursor: pointer;
- }
- }
}
- .propertiesView-name {
- border-bottom: 1px solid black;
- padding: 8.5px;
- font-size: 12.5px;
+ overflow-x: hidden;
+ overflow-y: auto;
- &:hover {
- cursor: text;
- }
+ .propertiesView-name,
+ .propertiesView-type {
+ padding: 5px 10px;
}
- .propertiesView-settings {
- //border-bottom: 1px solid black;
- //padding: 8.5px;
- font-size: 12.5px;
- font-weight: bold;
-
- .propertiesView-settings-title {
- font-weight: bold;
- font-size: 12.5px;
- padding: 4px;
- display: flex;
- color: white;
- padding-left: 8px;
- background-color: rgb(51, 51, 51);
-
- &:hover {
- cursor: pointer;
- }
-
- .propertiesView-settings-title-icon {
- float: right;
- justify-items: right;
- align-items: flex-end;
- margin-left: auto;
- margin-right: 9px;
-
- &:hover {
- cursor: pointer;
- }
- }
- }
-
- .propertiesView-settings-content {
- margin-left: 12px;
- padding-bottom: 10px;
- padding-top: 8px;
- }
- }
.propertiesView-sharing {
//border-bottom: 1px solid black;
//padding: 8.5px;
- .propertiesView-sharing-title {
- font-weight: bold;
- font-size: 12.5px;
- padding: 4px;
- display: flex;
- color: white;
- padding-left: 8px;
- background-color: rgb(51, 51, 51);
-
- &:hover {
- cursor: pointer;
- }
-
- .propertiesView-sharing-title-icon {
- float: right;
- justify-items: right;
- align-items: flex-end;
- margin-left: auto;
- margin-right: 9px;
-
- &:hover {
- cursor: pointer;
- }
- }
- }
-
- .propertiesView-sharing-content {
- font-size: 10px;
- padding: 10px;
- margin-left: 5px;
.propertiesView-buttonContainer {
float: right;
@@ -133,16 +42,6 @@
padding: 0;
margin-top: -5;
}
-
- .propertiesView-acls-checkbox {
- margin-top: -20px;
-
- .propertiesView-acls-checkbox-text {
- font-size: 7px;
- margin-top: -10px;
- margin-left: 6px;
- }
- }
}
.change-buttons {
@@ -157,6 +56,63 @@
width: 100%;
}
}
+ }
+
+ .propertiesView-acls-checkbox {
+ float: right;
+ margin-left: 50px;
+ }
+
+ .propertiesView-shareDropDown{
+ margin-right: 10px;
+ min-width: 65px;
+
+ & .propertiesView-shareDropDownNone{
+ padding: 0px;
+ padding-left: 3px;
+ padding-right: 3px;
+ background: grey;
+ color: rgb(71, 71, 71);
+ border-radius: 6px;
+ border: 1px solid rgb(71, 71, 71);
+ }
+ & .propertiesView-shareDropDownEdit,
+ .propertiesView-shareDropDownAdmin{
+ padding: 0px;
+ padding-left: 3px;
+ padding-right: 3px;
+ background: rgb(254, 254, 199);
+ color: rgb(75, 75, 5);
+ border-radius: 6px;
+ border: 1px solid rgb(75, 75, 5);
+ }
+ & .propertiesView-shareDropDownAugment{
+ padding: 0px;
+ padding-left: 3px;
+ padding-right: 3px;
+ background: rgb(208, 255, 208);
+ color:rgb(19, 80, 19);
+ border-radius: 6px;
+ border: 1px solid rgb(19, 80, 19);
+
+ }
+ & .propertiesView-shareDropDownView{
+ padding: 0px;
+ padding-left: 3px;
+ padding-right: 3px;
+ background: rgb(213, 213, 255);
+ color: rgb(25, 25, 101);
+ border-radius: 6px;
+ border: 1px solid rgb(25, 25, 101);
+ }
+ & .propertiesView-shareDropDownNot-Shared{
+ padding: 0px;
+ padding-left: 3px;
+ padding-right: 3px;
+ background: rgb(255, 207, 207);
+ color: rgb(138, 47, 47);
+ border-radius: 6px;
+ border: 1px solid rgb(138, 47, 47);
}
}
@@ -164,33 +120,7 @@
//border-bottom: 1px solid black;
//padding: 8.5px;
- .propertiesView-filters-title {
- font-weight: bold;
- font-size: 12.5px;
- padding: 4px;
- display: flex;
- color: white;
- padding-left: 8px;
- background-color: rgb(51, 51, 51);
-
- &:hover {
- cursor: pointer;
- }
-
- .propertiesView-filters-title-icon {
- float: right;
- justify-items: right;
- align-items: flex-end;
- margin-left: auto;
- margin-right: 9px;
-
- &:hover {
- cursor: pointer;
- }
- }
- }
-
- .propertiesView-filters-content {
+ .propertiesView-content {
font-size: 10px;
padding: 10px;
margin-left: 5px;
@@ -219,85 +149,10 @@
}
}
- .propertiesView-appearance {
- //border-bottom: 1px solid black;
- //padding: 8.5px;
-
- .propertiesView-appearance-title {
- font-weight: bold;
- font-size: 12.5px;
- padding: 4px;
- display: flex;
- color: white;
- padding-left: 8px;
- background-color: rgb(51, 51, 51);
-
- &:hover {
- cursor: pointer;
- }
-
- .propertiesView-appearance-title-icon {
- float: right;
- justify-items: right;
- align-items: flex-end;
- margin-left: auto;
- margin-right: 9px;
-
- &:hover {
- cursor: pointer;
- }
- }
- }
-
- .propertiesView-appearance-content {
- font-size: 10px;
- padding: 10px;
- margin-left: 5px;
- }
- }
-
- .propertiesView-transform {
- //border-bottom: 1px solid black;
- //padding: 8.5px;
-
- .propertiesView-transform-title {
- font-weight: bold;
- font-size: 12.5px;
- padding: 4px;
- display: flex;
- color: white;
- padding-left: 8px;
- background-color: rgb(51, 51, 51);
-
- &:hover {
- cursor: pointer;
- }
-
- .propertiesView-transform-title-icon {
- float: right;
- justify-items: right;
- align-items: flex-end;
- margin-left: auto;
- margin-right: 9px;
-
- &:hover {
- cursor: pointer;
- }
- }
- }
-
- .propertiesView-transform-content {
- font-size: 10px;
- padding: 10px;
- margin-left: 5px;
- }
- }
-
.notify-button {
padding: 2px;
width: 12px;
height: 12px;
- background-color: black;
border-radius: 10px;
padding-left: 2px;
padding-right: 2px;
@@ -317,19 +172,7 @@
}
.expansion-button {
- margin-left: -20;
-
- .expansion-button-icon {
- width: 11px;
- height: 11px;
- color: black;
- margin-left: 27px;
-
- &:hover {
- color: rgb(131, 131, 131);
- cursor: pointer;
- }
- }
+ margin-right: 10px;
}
.propertiesView-sharingTable {
@@ -340,18 +183,13 @@
padding: 5px; // remove when adding buttons
border-radius: 6px; // remove when adding buttons
margin-right: 10px; // remove when adding buttons
- // width: 100%;
- // display: inline-table;
background-color: #ececec;
- max-height: 130px;
- overflow-y: auto;
- width: 92%;
+ width: 97%;
.propertiesView-sharingTable-item {
display: flex;
- // padding: 5px;
padding: 3px;
- align-items: center;
+ align-items: right;
border-bottom: 0.5px solid grey;
&:hover .propertiesView-sharingTable-item-name {
@@ -372,19 +210,9 @@
.propertiesView-sharingTable-item-permission {
display: flex;
align-items: flex-end;
+ text-align: right;
margin-left: auto;
-
- .permissions-select {
- border: none;
- background-color: inherit;
- width: 87px;
- text-align: justify; // for Edge
- text-align-last: end;
-
- &:hover {
- cursor: pointer;
- }
- }
+ margin-right: -12px;
}
&:last-child {
@@ -393,57 +221,9 @@
}
}
- .propertiesView-fields {
- //border-bottom: 1px solid black;
- //padding: 8.5px;
-
- .propertiesView-fields-title {
- font-weight: bold;
- font-size: 12.5px;
- padding: 4px;
- display: flex;
- color: white;
- padding-left: 8px;
- background-color: rgb(51, 51, 51);
-
- &:hover {
- cursor: pointer;
- }
-
- .propertiesView-fields-title-icon {
- float: right;
- justify-items: right;
- align-items: flex-end;
- margin-left: auto;
- margin-right: 9px;
-
- &:hover {
- cursor: pointer;
- }
- }
- }
-
- .propertiesView-fields-checkbox {
- float: right;
- height: 20px;
- margin-top: -9px;
-
- .propertiesView-fields-checkbox-text {
- font-size: 7px;
- margin-top: -10px;
- margin-left: 6px;
- }
- }
-
- .propertiesView-fields-content {
- font-size: 10px;
- margin-left: 2px;
- padding: 10px;
-
- &:hover {
- cursor: pointer;
- }
- }
+ .propertiesView-permissions-select {
+ background: inherit;
+ border: none;
}
.propertiesView-field {
@@ -466,109 +246,14 @@
}
}
- .propertiesView-contexts {
- .propertiesView-contexts-title {
- font-weight: bold;
- font-size: 12.5px;
- padding: 4px;
- display: flex;
- color: white;
- padding-left: 8px;
- background-color: rgb(51, 51, 51);
-
- &:hover {
- cursor: pointer;
- }
-
- .propertiesView-contexts-title-icon {
- float: right;
- justify-items: right;
- align-items: flex-end;
- margin-left: auto;
- margin-right: 9px;
-
- &:hover {
- cursor: pointer;
- }
- }
- }
-
- .propertiesView-contexts-content {
- overflow: hidden;
- padding: 10px;
- }
- }
-
- .propertiesView-layout {
- .propertiesView-layout-title {
- font-weight: bold;
- font-size: 12.5px;
- padding: 4px;
- display: flex;
- color: white;
- padding-left: 8px;
- background-color: rgb(51, 51, 51);
-
- &:hover {
- cursor: pointer;
- }
-
- .propertiesView-layout-title-icon {
- float: right;
- justify-items: right;
- align-items: flex-end;
- margin-left: auto;
- margin-right: 9px;
-
- &:hover {
- cursor: pointer;
- }
- }
- }
-
- .propertiesView-layout-content {
- overflow: hidden;
- padding: 10px;
- }
}
.propertiesView-presTrails {
//border-bottom: 1px solid black;
//padding: 8.5px;
- .propertiesView-presTrails-title {
- font-weight: bold;
- font-size: 12.5px;
- padding: 4px;
- display: flex;
- color: white;
- padding-left: 8px;
- background-color: rgb(51, 51, 51);
-
- &:hover {
- cursor: pointer;
- }
-
- .propertiesView-presTrails-title-icon {
- float: right;
- justify-items: right;
- align-items: flex-end;
- margin-left: auto;
- margin-right: 9px;
-
- &:hover {
- cursor: pointer;
- }
- }
- }
-
- .propertiesView-presTrails-content {
- font-size: 10px;
- padding: 10px;
- margin-left: 5px;
- }
+
}
-}
.inking-button {
display: flex;
@@ -830,8 +515,16 @@
}
}
+.propertiesView-wordTitle{
+ color:darkslategray;
+ font-weight:200;
+}
+
.editable-title {
border: solid 1px #323232;
+ padding-left: 5px;
+ padding-top: 4px;
+ border-radius: 4px;
height: fit-content;
&:hover {
@@ -839,6 +532,26 @@
}
}
+.propertiesView-wordType{
+ color:darkslategray;
+ font-weight:200;
+}
+
+.currentType{
+ text-decoration: underline;
+ display: flex;
+ align-items:center;
+ // border: solid 1px #323232;
+ // padding-left: 5px;
+ // padding-top: 4px;
+ // border-radius: 4px;
+ // height: fit-content;
+}
+
+.currentType-icon{
+ margin-right:5px;
+}
+
.properties-flyout {
grid-column: 2/4;
}
@@ -847,10 +560,6 @@
padding: 5%;
}
-.propertiesView-section {
- padding-left: 20px;
-}
-
.propertiesView-input {
padding: 4px 8px;
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 19c138a21..872f1c6ab 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -3,37 +3,42 @@ import { IconLookup } from '@fortawesome/fontawesome-svg-core';
import { faAnchor, faArrowRight, faWindowMaximize } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Checkbox, Tooltip } from '@material-ui/core';
-import { intersection } from 'lodash';
-import { action, computed, Lambda, observable } from 'mobx';
+import { Button, Colors, EditableText, IconButton, NumberInput, Size, Slider, Type } from 'browndash-components';
+import { concat } from 'lodash';
+import { Lambda, action, computed, observable } from 'mobx';
import { observer } from 'mobx-react';
import { ColorState, SketchPicker } from 'react-color';
-import { AclAdmin, AclSym, DataSym, Doc, Field, FieldResult, HeightSym, HierarchyMapping, NumListCast, Opt, StrListCast, WidthSym } from '../../fields/Doc';
+import * as Icons from 'react-icons/bs'; //{BsCollectionFill, BsFillFileEarmarkImageFill} from "react-icons/bs"
+import { GrCircleInformation } from 'react-icons/gr';
+import { Utils, emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, setupMoveUpEvents } from '../../Utils';
+import { Doc, DocListCast, Field, FieldResult, HierarchyMapping, NumListCast, Opt, ReverseHierarchyMap, StrListCast } from '../../fields/Doc';
+import { AclAdmin, DocAcl, DocData, Height, Width } from '../../fields/DocSymbols';
import { Id } from '../../fields/FieldSymbols';
import { InkField } from '../../fields/InkField';
import { List } from '../../fields/List';
import { ComputedField } from '../../fields/ScriptField';
import { Cast, DocCast, NumCast, StrCast } from '../../fields/Types';
-import { denormalizeEmail, GetEffectiveAcl, SharingPermissions } from '../../fields/util';
-import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, setupMoveUpEvents } from '../../Utils';
+import { GetEffectiveAcl, SharingPermissions, normalizeEmail } from '../../fields/util';
import { DocumentType } from '../documents/DocumentTypes';
import { DocumentManager } from '../util/DocumentManager';
+import { GroupManager } from '../util/GroupManager';
import { LinkManager } from '../util/LinkManager';
import { SelectionManager } from '../util/SelectionManager';
import { SharingManager } from '../util/SharingManager';
import { Transform } from '../util/Transform';
-import { undoBatch, UndoManager } from '../util/UndoManager';
+import { UndoManager, undoBatch, undoable } from '../util/UndoManager';
import { EditableView } from './EditableView';
import { FilterPanel } from './FilterPanel';
-import { Colors } from './global/globalEnums';
import { InkStrokeProperties } from './InkStrokeProperties';
-import { DocumentView, OpenWhere, StyleProviderFunc } from './nodes/DocumentView';
-import { KeyValueBox } from './nodes/KeyValueBox';
-import { PresBox, PresEffect, PresEffectDirection } from './nodes/trails';
import { PropertiesButtons } from './PropertiesButtons';
import { PropertiesDocBacklinksSelector } from './PropertiesDocBacklinksSelector';
import { PropertiesDocContextSelector } from './PropertiesDocContextSelector';
+import { PropertiesSection } from './PropertiesSection';
import './PropertiesView.scss';
import { DefaultStyleProvider } from './StyleProvider';
+import { DocumentView, OpenWhere, StyleProviderFunc } from './nodes/DocumentView';
+import { KeyValueBox } from './nodes/KeyValueBox';
+import { PresBox, PresEffect, PresEffectDirection } from './nodes/trails';
const higflyout = require('@hig/flyout');
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
@@ -69,7 +74,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
return this.selectedDoc?.type === DocumentType.LINK;
}
@computed get dataDoc() {
- return this.selectedDoc?.[DataSym];
+ return this.selectedDoc?.[DocData];
}
@observable layoutFields: boolean = false;
@@ -116,19 +121,18 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
return this.selectedDoc?.type === DocumentType.INK;
}
- rtfWidth = () => (!this.selectedDoc ? 0 : Math.min(this.selectedDoc?.[WidthSym](), this.props.width - 20));
- rtfHeight = () => (!this.selectedDoc ? 0 : this.rtfWidth() <= this.selectedDoc?.[WidthSym]() ? Math.min(this.selectedDoc?.[HeightSym](), this.MAX_EMBED_HEIGHT) : this.MAX_EMBED_HEIGHT);
+ rtfWidth = () => (!this.selectedDoc ? 0 : Math.min(this.selectedDoc?.[Width](), this.props.width - 20));
+ rtfHeight = () => (!this.selectedDoc ? 0 : this.rtfWidth() <= this.selectedDoc?.[Width]() ? Math.min(this.selectedDoc?.[Height](), this.MAX_EMBED_HEIGHT) : this.MAX_EMBED_HEIGHT);
@action
docWidth = () => {
if (this.selectedDoc) {
const layoutDoc = this.selectedDoc;
const aspect = Doc.NativeAspect(layoutDoc, undefined, !layoutDoc._layout_fitWidth);
- if (aspect) return Math.min(layoutDoc[WidthSym](), Math.min(this.MAX_EMBED_HEIGHT * aspect, this.props.width - 20));
- return Doc.NativeWidth(layoutDoc) ? Math.min(layoutDoc[WidthSym](), this.props.width - 20) : this.props.width - 20;
- } else {
- return 0;
+ if (aspect) return Math.min(layoutDoc[Width](), Math.min(this.MAX_EMBED_HEIGHT * aspect, this.props.width - 20));
+ return Doc.NativeWidth(layoutDoc) ? Math.min(layoutDoc[Width](), this.props.width - 20) : this.props.width - 20;
}
+ return 0;
};
@action
@@ -186,7 +190,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
});
rows.push(
- <div className="propertiesView-field" key="newKeyValue" style={{ marginTop: '3px' }}>
+ <div className="propertiesView-field" key="newKeyValue" style={{ marginTop: '3px', backgroundColor: 'white', textAlign: 'center' }}>
<EditableView key="editableView" oneLine contents={'add key:value or #tags'} height={13} fontSize={10} GetValue={() => ''} SetValue={this.setKeyValue} />
</div>
);
@@ -199,13 +203,13 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
}
@computed get noviceFields() {
- const noviceReqFields = ['author', 'creationDate', 'tags', '_layout_curPage'];
+ const noviceReqFields = ['author', 'author_date', 'tags', '_layout_curPage'];
return this.editableFields(key => key.indexOf('modificationDate') !== -1 || (key[0] === key[0].toUpperCase() && !key.startsWith('acl')), noviceReqFields);
}
@undoBatch
setKeyValue = (value: string) => {
- const docs = SelectionManager.Views().length < 2 && this.selectedDoc ? [this.layoutFields ? Doc.Layout(this.selectedDoc) : this.dataDoc] : SelectionManager.Views().map(dv => (this.layoutFields ? dv.layoutDoc : dv.dataDoc));
+ const docs = SelectionManager.Views().length < 2 && this.selectedDoc ? [this.layoutFields ? Doc.Layout(this.selectedDoc) : this.dataDoc!] : SelectionManager.Views().map(dv => (this.layoutFields ? dv.layoutDoc : dv.dataDoc));
docs.forEach(doc => {
if (value.indexOf(':') !== -1) {
const newVal = value[0].toUpperCase() + value.substring(1, value.length);
@@ -220,7 +224,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
const tags = StrListCast(doc.tags);
if (!tags.includes(value)) {
tags.push(value);
- doc[DataSym].tags = tags.length ? new List<string>(tags) : undefined;
+ doc[DocData].tags = tags.length ? new List<string>(tags) : undefined;
}
return true;
}
@@ -244,11 +248,30 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
return !this.selectedDoc ? null : <PropertiesDocContextSelector DocView={this.selectedDocumentView} hideTitle={true} addDocTab={this.props.addDocTab} />;
}
+ @computed get contextCount() {
+ if (this.selectedDocumentView) {
+ const target = this.selectedDocumentView.props.Document;
+ const embeddings = DocListCast(target.proto_embeddings);
+ return embeddings.length - 1;
+ } else {
+ return 0;
+ }
+ }
+
@computed get links() {
const selAnchor = this.selectedDocumentView?.anchorViewDoc ?? LinkManager.currentLinkAnchor ?? this.selectedDoc;
return !selAnchor ? null : <PropertiesDocBacklinksSelector Document={selAnchor} hideTitle={true} addDocTab={this.props.addDocTab} />;
}
+ @computed get linkCount() {
+ const selAnchor = this.selectedDocumentView?.anchorViewDoc ?? LinkManager.currentLinkAnchor ?? this.selectedDoc;
+ var counter = 0;
+
+ LinkManager.Links(selAnchor).forEach((l, i) => counter++);
+
+ return counter;
+ }
+
@computed get layoutPreview() {
if (SelectionManager.Views().length > 1) {
return '-- multiple selected --';
@@ -276,8 +299,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
PanelHeight={panelHeight}
focus={emptyFunction}
ScreenToLocalTransform={this.getTransform}
- docFilters={returnEmptyFilter}
- docRangeFilters={returnEmptyFilter}
+ childFilters={returnEmptyFilter}
+ childFiltersByRanges={returnEmptyFilter}
searchFilterDocs={returnEmptyDoclist}
addDocument={returnFalse}
moveDocument={undefined}
@@ -287,7 +310,6 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
pinToPres={emptyFunction}
bringToFront={returnFalse}
dontRegisterView={true}
- dropAction={undefined}
/>
</div>
);
@@ -301,27 +323,23 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
*/
@undoBatch
changePermissions = (e: any, user: string) => {
- const docs = (SelectionManager.Views().length < 2 ? [this.selectedDoc] : SelectionManager.Views().map(dv => dv.props.Document)).filter(doc => doc).map(doc => (this.layoutDocAcls ? doc : DocCast(doc)[DataSym]));
- SharingManager.Instance.shareFromPropertiesSidebar(user, e.currentTarget.value as SharingPermissions, docs);
+ const docs = (SelectionManager.Views().length < 2 ? [this.selectedDoc] : SelectionManager.Views().map(dv => dv.props.Document)).filter(doc => doc).map(doc => (this.layoutDocAcls ? doc! : DocCast(doc)[DocData]));
+ SharingManager.Instance.shareFromPropertiesSidebar(user, e.currentTarget.value as SharingPermissions, docs, this.layoutDocAcls);
};
/**
* @returns the options for the permissions dropdown.
*/
- getPermissionsSelect(user: string, permission: string) {
- const dropdownValues: string[] = Object.values(SharingPermissions);
+ getPermissionsSelect(user: string, permission: string, showGuestOptions: boolean) {
+ const dropdownValues: string[] = showGuestOptions ? [SharingPermissions.None, SharingPermissions.View] : Object.values(SharingPermissions);
if (permission === '-multiple-') dropdownValues.unshift(permission);
- if (user !== 'Override') dropdownValues.splice(dropdownValues.indexOf(SharingPermissions.Unset), 1);
return (
- <select className="permissions-select" value={permission} onChange={e => this.changePermissions(e, user)}>
- {dropdownValues
- .filter(permission => !Doc.noviceMode || ![SharingPermissions.View, SharingPermissions.SelfEdit].includes(permission as any))
- .map(permission => (
- <option key={permission} value={permission}>
- {' '}
- {permission}{' '}
- </option>
- ))}
+ <select className="propertiesView-permissions-select" value={permission} onChange={e => this.changePermissions(e, user)}>
+ {dropdownValues.map(permission => (
+ <option className="propertiesView-permisssions-select" key={permission} value={permission}>
+ {concat(ReverseHierarchyMap.get(permission)?.image, ' ', permission)}
+ </option>
+ ))}
</select>
);
}
@@ -333,7 +351,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
return (
<Tooltip title={<div className="dash-tooltip">Notify with message</div>}>
<div className="notify-button">
- <FontAwesomeIcon className="notify-button-icon" icon="bell" color="white" size="sm" />
+ <FontAwesomeIcon className="notify-button-icon" icon="bell" size="sm" />
</div>
</Tooltip>
);
@@ -344,17 +362,18 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
*/
@computed get expansionIcon() {
return (
- <Tooltip title={<div className="dash-tooltip">Show more permissions</div>}>
- <div
- className="expansion-button"
- onPointerDown={() => {
+ <div className="expansion-button">
+ <IconButton
+ icon={<FontAwesomeIcon icon={'ellipsis-h'} />}
+ size={Size.XSMALL}
+ color={StrCast(Doc.UserDoc().userColor)}
+ onClick={action(() => {
if (this.selectedDocumentView || this.selectedDoc) {
SharingManager.Instance.open(this.selectedDocumentView?.props.Document === this.selectedDoc ? this.selectedDocumentView : undefined, this.selectedDoc);
}
- }}>
- <FontAwesomeIcon className="expansion-button-icon" icon="ellipsis-h" color="black" size="sm" />
- </div>
- </Tooltip>
+ })}
+ />
+ </div>
);
}
@@ -362,6 +381,9 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
* @returns a row of the permissions panel
*/
sharingItem(name: string, admin: boolean, permission: string, showExpansionIcon?: boolean) {
+ if (name == Doc.CurrentUserEmail) {
+ name = 'Me';
+ }
return (
<div
className="propertiesView-sharingTable-item"
@@ -375,92 +397,208 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
</div>
{/* {name !== "Me" ? this.notifyIcon : null} */}
<div className="propertiesView-sharingTable-item-permission">
- {admin && permission !== 'Owner' ? this.getPermissionsSelect(name, permission) : permission}
- {permission === 'Owner' || showExpansionIcon ? this.expansionIcon : null}
+ {this.colorACLDropDown(name, admin, permission, false)}
+ {(permission === 'Owner' && name == 'Me') || showExpansionIcon ? this.expansionIcon : null}
+ </div>
+ </div>
+ );
+ }
+
+ /**
+ * @returns a colored dropdown bar reflective of the permission
+ */
+ colorACLDropDown(name: string, admin: boolean, permission: string, showGuestOptions: boolean) {
+ var shareImage = ReverseHierarchyMap.get(permission)?.image;
+ return (
+ <div>
+ <div className={'propertiesView-shareDropDown'}>
+ <div className={`propertiesView-shareDropDown${permission}`}>
+ <div>{admin && permission !== 'Owner' ? this.getPermissionsSelect(name, permission, showGuestOptions) : concat(shareImage, ' ', permission)}</div>
+ </div>
</div>
</div>
);
}
/**
+ * Sorting algorithm to sort users.
+ */
+ sortUsers = (u1: String, u2: String) => {
+ return u1 > u2 ? -1 : u1 === u2 ? 0 : 1;
+ };
+
+ /**
+ * Sorting algorithm to sort groups.
+ */
+ sortGroups = (group1: Doc, group2: Doc) => {
+ const g1 = StrCast(group1.title);
+ const g2 = StrCast(group2.title);
+ return g1 > g2 ? -1 : g1 === g2 ? 0 : 1;
+ };
+
+ /**
* @returns the sharing and permissions panel.
*/
@computed get sharingTable() {
// all selected docs
- const docs =
- SelectionManager.Views().length < 2 ? [this.layoutDocAcls ? this.selectedDoc : this.selectedDoc?.[DataSym]] : SelectionManager.Views().map(docView => (this.layoutDocAcls ? docView.props.Document : docView.props.Document[DataSym]));
-
+ const docs = SelectionManager.Views().length < 2 && this.selectedDoc ? [this.selectedDoc] : SelectionManager.Views().map(docView => docView.rootDoc);
const target = docs[0];
- // tslint:disable-next-line: no-unnecessary-callback-wrapper
- const effectiveAcls = docs.map(doc => GetEffectiveAcl(doc));
- const showAdmin = effectiveAcls.every(acl => acl === AclAdmin);
+ const showAdmin = GetEffectiveAcl(target) == AclAdmin;
+ const individualTableEntries = [];
+ const usersAdded: string[] = []; // all shared users being added - organized by denormalized email
+
+ const seldoc = this.layoutDocAcls || !this.selectedDoc ? this.selectedDoc : Doc.GetProto(this.selectedDoc);
+ // adds each user to usersAdded
+ SharingManager.Instance.users.forEach(eachUser => {
+ var userOnDoc = true;
+ if (seldoc) {
+ if (Doc.GetT(seldoc, 'acl-' + normalizeEmail(eachUser.user.email), 'string', true) === '' || Doc.GetT(seldoc, 'acl-' + normalizeEmail(eachUser.user.email), 'string', true) === undefined) {
+ userOnDoc = false;
+ }
+ }
+ if (userOnDoc && !usersAdded.includes(eachUser.user.email) && eachUser.user.email !== 'guest' && eachUser.user.email != target.author) {
+ usersAdded.push(eachUser.user.email);
+ }
+ });
- // users in common between all docs
- const commonKeys: string[] = intersection(...docs.map(doc => doc?.[AclSym] && Object.keys(doc[AclSym]).filter(key => key !== 'acl-Me')));
+ // sorts and then adds each user to the table
+ usersAdded.sort(this.sortUsers);
+ usersAdded.map(userEmail => {
+ const userKey = `acl-${normalizeEmail(userEmail)}`;
+ var aclField = Doc.GetT(this.layoutDocAcls ? target : Doc.GetProto(target), userKey, 'string', true);
+ var permission = StrCast(aclField);
+ individualTableEntries.unshift(this.sharingItem(userEmail, showAdmin, permission!, false)); // adds each user
+ });
- const tableEntries = [];
+ // adds current user
+ var userEmail = Doc.CurrentUserEmail;
+ if (userEmail == 'guest') userEmail = 'Guest';
+ const userKey = `acl-${normalizeEmail(userEmail)}`;
+ if (!usersAdded.includes(userEmail) && userEmail !== 'Guest' && userEmail != target.author) {
+ var permission;
+ if (this.layoutDocAcls) {
+ if (target[DocAcl][userKey]) permission = HierarchyMapping.get(target[DocAcl][userKey])?.name;
+ else if (target['embedContainer']) permission = StrCast(Doc.GetProto(DocCast(target['embedContainer']))[userKey]);
+ else permission = StrCast(Doc.GetProto(target)?.[userKey]);
+ } else permission = StrCast(target[userKey]);
+ individualTableEntries.unshift(this.sharingItem(userEmail, showAdmin, permission!, false)); // adds each user
+ }
- // DocCastAsync(Doc.UserDoc().sidebarUsersDisplayed).then(sidebarUsersDisplayed => {
- if (commonKeys.length) {
- for (const key of commonKeys) {
- const name = denormalizeEmail(key.substring(4));
- const uniform = docs.every(doc => doc?.[AclSym]?.[key] === docs[0]?.[AclSym]?.[key]);
- if (name !== Doc.CurrentUserEmail && name !== target.author && name !== 'Public' && name !== 'Override' /* && sidebarUsersDisplayed![name] !== false*/) {
- tableEntries.push(this.sharingItem(name, showAdmin, uniform ? HierarchyMapping.get(target[AclSym][key])!.name : '-multiple-'));
+ // shift owner to top
+ individualTableEntries.unshift(this.sharingItem(StrCast(target.author), showAdmin, 'Owner'), false);
+
+ // adds groups
+ const groupTableEntries: JSX.Element[] = [];
+ const groupList = GroupManager.Instance?.allGroups || [];
+ groupList.sort(this.sortGroups);
+ groupList.map(group => {
+ if (group.title != 'Guest' && this.selectedDoc) {
+ const groupKey = 'acl-' + normalizeEmail(StrCast(group.title));
+ if (this.selectedDoc[groupKey] != '' && this.selectedDoc[groupKey] != undefined) {
+ var permission;
+ if (this.layoutDocAcls) {
+ if (target[DocAcl][groupKey]) {
+ permission = HierarchyMapping.get(target[DocAcl][groupKey])?.name;
+ } else if (target['embedContainer']) permission = StrCast(Doc.GetProto(DocCast(target['embedContainer']))[groupKey]);
+ else permission = StrCast(Doc.GetProto(target)?.[groupKey]);
+ } else permission = StrCast(target[groupKey]);
+ groupTableEntries.unshift(this.sharingItem(StrCast(group.title), showAdmin, permission!, false));
}
}
- }
+ });
- const ownerSame = Doc.CurrentUserEmail !== target.author && docs.filter(doc => doc).every(doc => doc.author === docs[0].author);
- // shifts the current user, owner, public to the top of the doc.
- // tableEntries.unshift(this.sharingItem("Override", showAdmin, docs.filter(doc => doc).every(doc => doc["acl-Override"] === docs[0]["acl-Override"]) ? (AclMap.get(target[AclSym]?.["acl-Override"]) || "None") : "-multiple-"));
- if (ownerSame) tableEntries.unshift(this.sharingItem(StrCast(target.author), showAdmin, 'Owner'));
- tableEntries.unshift(this.sharingItem('Public', showAdmin, docs.filter(doc => doc).every(doc => doc['acl-Public'] === target['acl-Public']) ? target['acl-Public'] || SharingPermissions.None : '-multiple-'));
- tableEntries.unshift(
- this.sharingItem(
- 'Me',
- showAdmin,
- docs.filter(doc => doc).every(doc => doc.author === Doc.CurrentUserEmail) ? 'Owner' : effectiveAcls.every(acl => acl === effectiveAcls[0]) ? HierarchyMapping.get(effectiveAcls[0])!.name : '-multiple-',
- !ownerSame
- )
- );
+ // guest permission
+ const guestPermission = StrCast((this.layoutDocAcls ? target : Doc.GetProto(target))['acl-Guest']);
- return <div className="propertiesView-sharingTable">{tableEntries}</div>;
+ return (
+ <div>
+ <div>
+ <br></br> Individuals with Access to this Document
+ </div>
+ <div className="propertiesView-sharingTable" style={{ background: StrCast(Doc.UserDoc().userBackgroundColor), color: StrCast(Doc.UserDoc().userColor) }}>
+ {<div> {individualTableEntries}</div>}
+ </div>
+ {groupTableEntries.length > 0 ? (
+ <div>
+ <div>
+ <br></br> Groups with Access to this Document
+ </div>
+ <div className="propertiesView-sharingTable" style={{ background: StrCast(Doc.UserDoc().userBackgroundColor), color: StrCast(Doc.UserDoc().userColor) }}>
+ {<div> {groupTableEntries}</div>}
+ </div>
+ </div>
+ ) : null}
+ <br></br> Guest
+ <div>{this.colorACLDropDown('Guest', showAdmin, guestPermission!, true)}</div>
+ </div>
+ );
}
@computed get fieldsCheckbox() {
+ // color= "primary"
return <Checkbox color="primary" onChange={this.toggleCheckbox} checked={this.layoutFields} />;
}
@action
- toggleCheckbox = () => {
- this.layoutFields = !this.layoutFields;
- };
+ toggleCheckbox = () => (this.layoutFields = !this.layoutFields);
+
+ @computed get color() {
+ return StrCast(Doc.UserDoc().userColor);
+ }
+
+ @computed get backgroundColor() {
+ return StrCast(Doc.UserDoc().userBackgroundColor);
+ }
+
+ @computed get variantColor() {
+ return StrCast(Doc.UserDoc().userVariantColor);
+ }
@computed get editableTitle() {
const titles = new Set<string>();
- SelectionManager.Views().forEach(dv => titles.add(StrCast(dv.rootDoc.title)));
const title = Array.from(titles.keys()).length > 1 ? '--multiple selected--' : StrCast(this.selectedDoc?.title);
+ SelectionManager.Views().forEach(dv => titles.add(StrCast(dv.rootDoc.title)));
+ return <EditableText val={title} setVal={this.setTitle} color={this.color} type={Type.SEC} formLabel={'Title'} fillWidth />;
+ }
+
+ @computed get currentType() {
+ const documentType = StrCast(this.selectedDoc?.type);
+ var currentType: string = documentType;
+ var capitalizedDocType = Utils.cleanDocumentType(currentType as DocumentType);
+
return (
- <div className="editable-title">
- <EditableView key="editableView" contents={title} height={25} fontSize={14} GetValue={() => title} SetValue={this.setTitle} />
+ <div>
+ <div className="propertiesView-wordType">Type</div>
+ <div className="currentType">
+ <div className="currentType-icon">{this.currentComponent}</div>
+
+ {capitalizedDocType}
+ </div>
</div>
);
}
+ @computed get currentComponent() {
+ var iconName = StrCast(this.selectedDoc?.systemIcon);
+
+ if (iconName) {
+ const Icon = Icons[iconName as keyof typeof Icons];
+ return <Icon />;
+ } else {
+ return <Icons.BsFillCollectionFill />;
+ }
+ }
+
@undoBatch
@action
- setTitle = (value: string) => {
+ setTitle = (value: string | number) => {
if (SelectionManager.Views().length > 1) {
SelectionManager.Views().map(dv => Doc.SetInPlace(dv.rootDoc, 'title', value, true));
- return true;
} else if (this.dataDoc) {
if (this.selectedDoc) Doc.SetInPlace(this.selectedDoc, 'title', value, true);
- else KeyValueBox.SetField(this.dataDoc, 'title', value, true);
- return true;
+ else KeyValueBox.SetField(this.dataDoc, 'title', value as string, true);
}
- return false;
};
@undoBatch
@@ -518,7 +656,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
className="inking-button-points"
style={{ backgroundColor: InkStrokeProperties.Instance._controlButton ? 'black' : '' }}
onPointerDown={action(() => (InkStrokeProperties.Instance._controlButton = !InkStrokeProperties.Instance._controlButton))}>
- <FontAwesomeIcon icon="bezier-curve" color="white" size="lg" />
+ <FontAwesomeIcon icon="bezier-curve" size="lg" />
</div>
</Tooltip>
</div>
@@ -534,23 +672,13 @@ 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} 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" color="white" size="sm" />
+ <FontAwesomeIcon icon="caret-up" size="sm" />
</div>
<div className="inputbox-Button-down" key="down2" onPointerDown={undoBatch(action(() => this.upDownButtons('down', key)))}>
- <FontAwesomeIcon icon="caret-down" color="white" size="sm" />
+ <FontAwesomeIcon icon="caret-down" size="sm" />
</div>
</div>
</div>
@@ -568,55 +696,50 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
@action
upDownButtons = (dirs: string, field: string) => {
+ const selDoc = this.selectedDoc;
+ if (!selDoc) return;
+ //prettier-ignore
switch (field) {
- case 'Xps':
- this.selectedDoc && (this.selectedDoc.x = NumCast(this.selectedDoc?.x) + (dirs === 'up' ? 10 : -10));
- break;
- case 'Yps':
- this.selectedDoc && (this.selectedDoc.y = NumCast(this.selectedDoc?.y) + (dirs === 'up' ? 10 : -10));
- break;
- case 'stk':
- this.selectedDoc && (this.selectedDoc.stroke_width = NumCast(this.selectedDoc?.stroke_width) + (dirs === 'up' ? 0.1 : -0.1));
- break;
+ case 'Xps': selDoc.x = NumCast(this.selectedDoc?.x) + (dirs === 'up' ? 10 : -10); break;
+ case 'Yps': selDoc.y = NumCast(this.selectedDoc?.y) + (dirs === 'up' ? 10 : -10); break;
+ case 'stk': selDoc.stroke_width = NumCast(this.selectedDoc?.stroke_width) + (dirs === 'up' ? 0.1 : -0.1); break;
case 'wid':
- const oldWidth = NumCast(this.selectedDoc?._width);
- const oldHeight = NumCast(this.selectedDoc?._height);
- const oldX = NumCast(this.selectedDoc?.x);
- const oldY = NumCast(this.selectedDoc?.y);
- this.selectedDoc && (this.selectedDoc._width = oldWidth + (dirs === 'up' ? 10 : -10));
- const doc = this.selectedDoc;
- if (doc?.type === DocumentType.INK && doc.x && doc.y && doc._height && doc._width) {
- const ink = Cast(doc.data, InkField)?.inkData;
+ const oldWidth = NumCast(selDoc._width);
+ const oldHeight = NumCast(selDoc._height);
+ const oldX = NumCast(selDoc.x);
+ const oldY = NumCast(selDoc.y);
+ selDoc._width = oldWidth + (dirs === 'up' ? 10 : -10);
+ if (selDoc.type === DocumentType.INK && selDoc.x && selDoc.y && selDoc._height && selDoc._width) {
+ const ink = Cast(selDoc.data, InkField)?.inkData;
if (ink) {
const newPoints: { X: number; Y: number }[] = [];
for (var j = 0; j < ink.length; j++) {
// (new x — oldx) + (oldxpoint * newWidt)/oldWidth
- const newX = NumCast(doc.x) - oldX + (ink[j].X * NumCast(doc._width)) / oldWidth;
- const newY = NumCast(doc.y) - oldY + (ink[j].Y * NumCast(doc._height)) / oldHeight;
+ const newX = NumCast(selDoc.x) - oldX + (ink[j].X * NumCast(selDoc._width)) / oldWidth;
+ const newY = NumCast(selDoc.y) - oldY + (ink[j].Y * NumCast(selDoc._height)) / oldHeight;
newPoints.push({ X: newX, Y: newY });
}
- doc.data = new InkField(newPoints);
+ selDoc.data = new InkField(newPoints);
}
}
break;
case 'hgt':
- const oWidth = NumCast(this.selectedDoc?._width);
- const oHeight = NumCast(this.selectedDoc?._height);
- const oX = NumCast(this.selectedDoc?.x);
- const oY = NumCast(this.selectedDoc?.y);
- this.selectedDoc && (this.selectedDoc._height = oHeight + (dirs === 'up' ? 10 : -10));
- const docu = this.selectedDoc;
- if (docu?.type === DocumentType.INK && docu.x && docu.y && docu._height && docu._width) {
- const ink = Cast(docu.data, InkField)?.inkData;
+ const oWidth = NumCast(selDoc._width);
+ const oHeight = NumCast(selDoc._height);
+ const oX = NumCast(selDoc.x);
+ const oY = NumCast(selDoc.y);
+ selDoc._height = oHeight + (dirs === 'up' ? 10 : -10);
+ if (selDoc.type === DocumentType.INK && selDoc.x && selDoc.y && selDoc._height && selDoc._width) {
+ const ink = Cast(selDoc.data, InkField)?.inkData;
if (ink) {
const newPoints: { X: number; Y: number }[] = [];
for (var j = 0; j < ink.length; j++) {
// (new x — oldx) + (oldxpoint * newWidt)/oldWidth
- const newX = NumCast(docu.x) - oX + (ink[j].X * NumCast(docu._width)) / oWidth;
- const newY = NumCast(docu.y) - oY + (ink[j].Y * NumCast(docu._height)) / oHeight;
+ const newX = NumCast(selDoc.x) - oX + (ink[j].X * NumCast(selDoc._width)) / oWidth;
+ const newY = NumCast(selDoc.y) - oY + (ink[j].Y * NumCast(selDoc._height)) / oHeight;
newPoints.push({ X: newX, Y: newY });
}
- docu.data = new InkField(newPoints);
+ selDoc.data = new InkField(newPoints);
}
}
break;
@@ -656,21 +779,11 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
return this.inputBoxDuo(
'hgt',
this.shapeHgt,
- (val: string) => {
- if (!isNaN(Number(val))) {
- this.shapeHgt = val;
- }
- return true;
- },
+ undoable((val: string) => !isNaN(Number(val)) && (this.shapeHgt = val), 'set height'),
'H:',
'wid',
this.shapeWid,
- (val: string) => {
- if (!isNaN(Number(val))) {
- this.shapeWid = val;
- }
- return true;
- },
+ undoable((val: string) => !isNaN(Number(val)) && (this.shapeWid = val), 'set width'),
'W:'
);
}
@@ -678,21 +791,11 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
return this.inputBoxDuo(
'Xps',
this.shapeXps,
- (val: string) => {
- if (val !== '0' && !isNaN(Number(val))) {
- this.shapeXps = val;
- }
- return true;
- },
+ undoable((val: string) => val !== '0' && !isNaN(Number(val)) && (this.shapeXps = val), 'set x coord'),
'X:',
'Yps',
this.shapeYps,
- (val: string) => {
- if (val !== '0' && !isNaN(Number(val))) {
- this.shapeYps = val;
- }
- return true;
- },
+ undoable((val: string) => val !== '0' && !isNaN(Number(val)) && (this.shapeYps = val), 'set y coord'),
'Y:'
);
}
@@ -700,36 +803,24 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
@observable private _fillBtn = false;
@observable private _lineBtn = false;
- private _lastFill = '#D0021B';
- private _lastLine = '#D0021B';
private _lastDash: any = '2';
@computed get colorFil() {
- const ccol = this.getField('fillColor') || '';
- ccol && (this._lastFill = ccol);
- return ccol;
+ return StrCast(this.selectedDoc?.fillColor);
}
@computed get colorStk() {
- const ccol = this.getField('color') || '';
- ccol && (this._lastLine = ccol);
- return ccol;
+ return StrCast(this.selectedDoc?.color);
}
set colorFil(value) {
- value && (this._lastFill = value);
this.selectedDoc && (this.selectedDoc.fillColor = value ? value : undefined);
}
set colorStk(value) {
- value && (this._lastLine = value);
this.selectedDoc && (this.selectedDoc.color = value ? value : undefined);
}
- colorButton(value: string, type: string, setter: () => {}) {
- // return <div className="properties-flyout" onPointerEnter={e => this.changeScrolling(false)}
- // onPointerLeave={e => this.changeScrolling(true)}>
- // <Flyout anchorPoint={anchorPoints.LEFT_TOP}
- // content={type === "fill" ? this.fillPicker : this.linePicker}>
+ colorButton(value: string, type: string, setter: () => void) {
return (
- <div className="color-button" key="color" onPointerDown={undoBatch(action(e => setter()))}>
+ <div className="color-button" key="color" onPointerDown={action(e => setter())}>
<div
className="color-button-preview"
style={{
@@ -742,31 +833,17 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
{value === '' || value === 'transparent' ? <p style={{ fontSize: 25, color: 'red', marginTop: -14 }}>☒</p> : ''}
</div>
);
- // </Flyout>
- // </div>;
}
- @undoBatch
- @action
- switchStk = (color: ColorState) => {
- const val = String(color.hex);
- this.colorStk = val;
- return true;
- };
- @undoBatch
- @action
- switchFil = (color: ColorState) => {
- const val = String(color.hex);
- this.colorFil = val;
- return true;
- };
-
- colorPicker(setter: (color: string) => {}, type: string) {
+ colorPicker(color: string, setter: (color: string) => void) {
return (
<SketchPicker
- onChange={type === 'stk' ? this.switchStk : this.switchFil}
+ onChange={undoable(
+ action((color: ColorState) => setter(color.hex)),
+ 'set stroke color property'
+ )}
presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb', 'transparent']}
- color={type === 'stk' ? this.colorStk : this.colorFil}
+ color={color}
/>
);
}
@@ -775,22 +852,20 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
return this.colorButton(this.colorFil, 'fill', () => {
this._fillBtn = !this._fillBtn;
this._lineBtn = false;
- return true;
});
}
@computed get lineButton() {
return this.colorButton(this.colorStk, 'line', () => {
this._lineBtn = !this._lineBtn;
this._fillBtn = false;
- return true;
});
}
@computed get fillPicker() {
- return this.colorPicker((color: string) => (this.colorFil = color), 'fil');
+ return this.colorPicker(this.colorFil, (color: string) => (this.colorFil = color));
}
@computed get linePicker() {
- return this.colorPicker((color: string) => (this.colorStk = color), 'stk');
+ return this.colorPicker(this.colorStk, (color: string) => (this.colorStk = color));
}
@computed get strokeAndFill() {
@@ -812,20 +887,14 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
);
}
- @computed get solidStk() {
- return this.selectedDoc?.color && (!this.selectedDoc?.stroke_dash || this.selectedDoc?.stroke_dash === '0') ? true : false;
- }
@computed get dashdStk() {
return this.selectedDoc?.stroke_dash || '';
}
- @computed get unStrokd() {
- return this.selectedDoc?.color ? true : false;
- }
@computed get widthStk() {
return this.getField('stroke_width') || '1';
}
@computed get markScal() {
- return Number(this.getField('strokeMakerScale') || '1');
+ return Number(this.getField('stroke_markerScale') || '1');
}
@computed get markHead() {
return this.getField('stroke_startMarker') || '';
@@ -833,23 +902,16 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
@computed get markTail() {
return this.getField('stroke_endMarker') || '';
}
- set solidStk(value) {
- this.dashdStk = '';
- this.unStrokd = !value;
- }
set dashdStk(value) {
- value && (this._lastDash = value) && (this.unStrokd = false);
+ value && (this._lastDash = value);
this.selectedDoc && (this.selectedDoc.stroke_dash = value ? this._lastDash : undefined);
}
set markScal(value) {
- this.selectedDoc && (this.selectedDoc.strokeMarkerScale = Number(value));
+ this.selectedDoc && (this.selectedDoc.stroke_markerScale = Number(value));
}
set widthStk(value) {
this.selectedDoc && (this.selectedDoc.stroke_width = Number(value));
}
- set unStrokd(value) {
- this.colorStk = value ? '' : this._lastLine;
- }
set markHead(value) {
this.selectedDoc && (this.selectedDoc.stroke_startMarker = value);
}
@@ -870,16 +932,28 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
<input className="inputBox-input" type="text" value={value} 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" color="white" size="sm" />
+ <FontAwesomeIcon icon="caret-up" size="sm" />
</div>
<div className="inputbox-Button-down" key="down2" onPointerDown={undoBatch(action(() => this.upDownButtons('down', key)))}>
- <FontAwesomeIcon icon="caret-down" color="white" size="sm" />
+ <FontAwesomeIcon icon="caret-down" size="sm" />
</div>
</div>
</div>
);
};
+ @action
+ onDoubleClick = () => {
+ this.openContexts = false;
+ this.openLinks = false;
+ this.openOptions = false;
+ this.openTransform = false;
+ this.openFields = false;
+ this.openSharing = false;
+ this.openLayout = false;
+ this.openFilters = false;
+ };
+
@computed get widthAndDash() {
return (
<div className="widthAndDash">
@@ -959,195 +1033,177 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
);
}
+ getNumber = (label: string, unit: string, min: number, max: number, number: number, setNumber: any) => {
+ return (
+ <div>
+ <NumberInput formLabel={label} formLabelPlacement={'left'} type={Type.SEC} unit={unit} fillWidth color={this.color} number={number} setNumber={setNumber} min={min} max={max} />
+ <Slider multithumb={false} color={this.color} size={Size.XSMALL} min={min} max={max} unit={unit} number={number} setNumber={setNumber} fillWidth />
+ </div>
+ );
+ };
+
@computed get transformEditor() {
return (
<div className="transform-editor">
{this.isInk ? this.controlPointsButton : null}
- {this.hgtInput}
- {this.XpsInput}
+ {this.getNumber(
+ 'Height',
+ ' px',
+ 0,
+ 1000,
+ Number(this.shapeHgt),
+ undoable((val: string) => !isNaN(Number(val)) && (this.shapeHgt = val), 'set height')
+ )}
+ {this.getNumber(
+ 'Width',
+ ' px',
+ 0,
+ 1000,
+ Number(this.shapeWid),
+ undoable((val: string) => !isNaN(Number(val)) && (this.shapeWid = val), 'set width')
+ )}
+ {this.getNumber(
+ 'X Coordinate',
+ ' px',
+ -2000,
+ 2000,
+ Number(this.shapeXps),
+ undoable((val: string) => !isNaN(Number(val)) && (this.shapeXps = val), 'set x coord')
+ )}
+ {this.getNumber(
+ 'Y Coordinate',
+ ' px',
+ -2000,
+ 2000,
+ Number(this.shapeYps),
+ undoable((val: string) => !isNaN(Number(val)) && (this.shapeYps = val), 'set y coord')
+ )}
</div>
);
}
@computed get optionsSubMenu() {
return (
- <div className="propertiesView-settings" onPointerEnter={action(() => (this.inOptions = true))} onPointerLeave={action(() => (this.inOptions = false))}>
- <div className="propertiesView-settings-title" onPointerDown={action(() => (this.openOptions = !this.openOptions))} style={{ backgroundColor: this.openOptions ? 'black' : '' }}>
- Options
- <div className="propertiesView-settings-title-icon">
- <FontAwesomeIcon icon={this.openOptions ? 'caret-down' : 'caret-right'} size="lg" color="white" />
- </div>
- </div>
- {!this.openOptions ? null : (
- <div className="propertiesView-settings-content">
- <PropertiesButtons />
- </div>
- )}
- </div>
+ <PropertiesSection
+ title="Options"
+ content={<PropertiesButtons />}
+ inSection={this.inOptions}
+ isOpen={this.openOptions}
+ setInSection={bool => (this.inOptions = bool)}
+ setIsOpen={bool => (this.openOptions = bool)}
+ onDoubleClick={() => this.onDoubleClick()}
+ />
);
}
@computed get sharingSubMenu() {
return (
- <div className="propertiesView-sharing">
- <div className="propertiesView-sharing-title" onPointerDown={action(() => (this.openSharing = !this.openSharing))} style={{ backgroundColor: this.openSharing ? 'black' : '' }}>
- Sharing {'&'} Permissions
- <div className="propertiesView-sharing-title-icon">
- <FontAwesomeIcon icon={this.openSharing ? 'caret-down' : 'caret-right'} size="lg" color="white" />
- </div>
- </div>
- {!this.openSharing ? null : (
- <div className="propertiesView-sharing-content">
- <div className="propertiesView-buttonContainer">
- {!Doc.noviceMode ? (
- <div className="propertiesView-acls-checkbox">
- <Checkbox color="primary" onChange={action(() => (this.layoutDocAcls = !this.layoutDocAcls))} checked={this.layoutDocAcls} />
- <div className="propertiesView-acls-checkbox-text">Layout</div>
- </div>
- ) : null}
- {/* <Tooltip title={<><div className="dash-tooltip">{"Re-distribute sharing settings"}</div></>}>
- <button onPointerDown={() => SharingManager.Instance.distributeOverCollection(this.selectedDoc!)}>
- <FontAwesomeIcon icon="redo-alt" color="white" size="1x" />
- </button>
- </Tooltip> */}
+ <PropertiesSection
+ title="Sharing & Permissions"
+ content={
+ <>
+ {/* <div className="propertiesView-buttonContainer"> */}
+ <div className="propertiesView-acls-checkbox">
+ Layout Permissions
+ <Checkbox color="primary" onChange={action(() => (this.layoutDocAcls = !this.layoutDocAcls))} checked={this.layoutDocAcls} />
</div>
+ {/* <Tooltip title={<><div className="dash-tooltip">{"Re-distribute sharing settings"}</div></>}>
+ <button onPointerDown={() => SharingManager.Instance.distributeOverCollection(this.selectedDoc!)}>
+ <FontAwesomeIcon icon="redo-alt" size="1x" />
+ </button>
+ </Tooltip> */}
+ {/* </div> */}
{this.sharingTable}
- </div>
- )}
- </div>
+ </>
+ }
+ isOpen={this.openSharing}
+ setIsOpen={bool => (this.openSharing = bool)}
+ onDoubleClick={() => this.onDoubleClick()}
+ />
);
}
/**
- * Updates this.filterDoc's currentFilter and saves the docFilters on the currentFilter
+ * Updates this.filterDoc's currentFilter and saves the childFilters on the currentFilter
*/
updateFilterDoc = (doc: Doc) => {
if (this.selectedDoc) {
if (doc === this.selectedDoc.currentFilter) return; // causes problems if you try to reapply the same doc
- const savedDocFilters = doc._docFiltersList;
- const currentDocFilters = this.selectedDoc._docFilters;
- this.selectedDoc._docFilters = new List<string>();
- (this.selectedDoc.currentFilter as Doc)._docFiltersList = currentDocFilters;
+ const savedDocFilters = doc._childFiltersList;
+ const currentDocFilters = this.selectedDoc._childFilters;
+ this.selectedDoc._childFilters = new List<string>();
+ (this.selectedDoc.currentFilter as Doc)._childFiltersList = currentDocFilters;
this.selectedDoc.currentFilter = doc;
- doc._docFiltersList = new List<string>();
- this.selectedDoc._docFilters = savedDocFilters;
+ doc._childFiltersList = new List<string>();
+ this.selectedDoc._childFilters = savedDocFilters;
- const savedDocRangeFilters = doc._docRangeFiltersList;
- const currentDocRangeFilters = this.selectedDoc._docRangeFilters;
- this.selectedDoc._docRangeFilters = new List<string>();
- (this.selectedDoc.currentFilter as Doc)._docRangeFiltersList = currentDocRangeFilters;
+ const savedDocRangeFilters = doc._childFiltersByRangesList;
+ const currentDocRangeFilters = this.selectedDoc._childFiltersByRanges;
+ this.selectedDoc._childFiltersByRanges = new List<string>();
+ (this.selectedDoc.currentFilter as Doc)._childFiltersByRangesList = currentDocRangeFilters;
this.selectedDoc.currentFilter = doc;
- doc._docRangeFiltersList = new List<string>();
- this.selectedDoc._docRangeFilters = savedDocRangeFilters;
+ doc._childFiltersByRangesList = new List<string>();
+ this.selectedDoc._childFiltersByRanges = savedDocRangeFilters;
}
};
@computed get filtersSubMenu() {
return (
- <div className="propertiesView-filters">
- <div className="propertiesView-filters-title" onPointerDown={action(() => (this.openFilters = !this.openFilters))} style={{ backgroundColor: this.openFilters ? 'black' : '' }}>
- Filters
- <div className="propertiesView-filters-title-icon">
- <FontAwesomeIcon icon={this.openFilters ? 'caret-down' : 'caret-right'} size="lg" color="white" />
- </div>
- </div>
- {!this.openFilters ? null : (
- <div className="propertiesView-filters-content" style={{ position: 'relative', height: 'auto' }}>
+ <PropertiesSection
+ title="Filters"
+ content={
+ <div className="propertiesView-content filters" style={{ position: 'relative', height: 'auto' }}>
<FilterPanel rootDoc={this.selectedDoc ?? Doc.ActiveDashboard!} />
</div>
- )}
- </div>
+ }
+ isOpen={this.openFilters}
+ setIsOpen={bool => (this.openFilters = bool)}
+ onDoubleClick={() => this.onDoubleClick()}
+ />
);
}
@computed get inkSubMenu() {
+ let isDouble = false;
+
return (
<>
- {!this.isInk ? null : (
- <div className="propertiesView-appearance">
- <div className="propertiesView-appearance-title" onPointerDown={action(() => (this.openAppearance = !this.openAppearance))} style={{ backgroundColor: this.openAppearance ? 'black' : '' }}>
- Appearance
- <div className="propertiesView-appearance-title-icon">
- <FontAwesomeIcon icon={this.openAppearance ? 'caret-down' : 'caret-right'} size="lg" color="white" />
- </div>
- </div>
- {!this.openAppearance ? null : <div className="propertiesView-appearance-content">{this.appearanceEditor}</div>}
- </div>
- )}
-
- <div className="propertiesView-transform">
- <div className="propertiesView-transform-title" onPointerDown={action(() => (this.openTransform = !this.openTransform))} style={{ backgroundColor: this.openTransform ? 'black' : '' }}>
- Transform
- <div className="propertiesView-transform-title-icon">
- <FontAwesomeIcon icon={this.openTransform ? 'caret-down' : 'caret-right'} size="lg" color="white" />
- </div>
- </div>
- {this.openTransform ? <div className="propertiesView-transform-content">{this.transformEditor}</div> : null}
- </div>
+ <PropertiesSection title="Appearance" content={this.isInk ? this.appearanceEditor : null} isOpen={this.openAppearance} setIsOpen={bool => (this.openAppearance = bool)} onDoubleClick={() => this.onDoubleClick()} />
+ <PropertiesSection title="Transform" content={this.transformEditor} isOpen={this.openTransform} setIsOpen={bool => (this.openTransform = bool)} onDoubleClick={() => this.onDoubleClick()} />
</>
);
}
@computed get fieldsSubMenu() {
return (
- <div className="propertiesView-fields">
- <div className="propertiesView-fields-title" onPointerDown={action(() => (this.openFields = !this.openFields))} style={{ backgroundColor: this.openFields ? 'black' : '' }}>
- Fields {'&'} Tags
- <div className="propertiesView-fields-title-icon">
- <FontAwesomeIcon icon={this.openFields ? 'caret-down' : 'caret-right'} size="lg" color="white" />
- </div>
- </div>
- {!Doc.noviceMode && this.openFields ? (
- <div className="propertiesView-fields-checkbox">
- {this.fieldsCheckbox}
- <div className="propertiesView-fields-checkbox-text">Layout</div>
- </div>
- ) : null}
- {!this.openFields ? null : <div className="propertiesView-fields-content">{Doc.noviceMode ? this.noviceFields : this.expandedField}</div>}
- </div>
+ <PropertiesSection
+ title="Fields & Tags"
+ content={<div className="propertiesView-content fields">{Doc.noviceMode ? this.noviceFields : this.expandedField}</div>}
+ isOpen={this.openFields}
+ setIsOpen={bool => (this.openFields = bool)}
+ onDoubleClick={() => this.onDoubleClick()}
+ />
);
}
@computed get contextsSubMenu() {
return (
- <div className="propertiesView-contexts">
- <div className="propertiesView-contexts-title" onPointerDown={action(() => (this.openContexts = !this.openContexts))} style={{ backgroundColor: this.openContexts ? 'black' : '' }}>
- Other Contexts
- <div className="propertiesView-contexts-title-icon">
- <FontAwesomeIcon icon={this.openContexts ? 'caret-down' : 'caret-right'} size="lg" color="white" />
- </div>
- </div>
- {this.openContexts ? <div className="propertiesView-contexts-content">{this.contexts}</div> : null}
- </div>
+ <PropertiesSection
+ title="Other Contexts"
+ content={this.contextCount > 0 ? this.contexts : 'There are no other contexts.'}
+ isOpen={this.openContexts}
+ setIsOpen={bool => (this.openContexts = bool)}
+ onDoubleClick={() => this.onDoubleClick()}
+ />
);
}
@computed get linksSubMenu() {
- return (
- <div className="propertiesView-contexts">
- <div className="propertiesView-contexts-title" onPointerDown={action(() => (this.openLinks = !this.openLinks))} style={{ backgroundColor: this.openLinks ? 'black' : '' }}>
- Linked To
- <div className="propertiesView-contexts-title-icon">
- <FontAwesomeIcon icon={this.openLinks ? 'caret-down' : 'caret-right'} size="lg" color="white" />
- </div>
- </div>
- {this.openLinks ? <div className="propertiesView-contexts-content">{this.links}</div> : null}
- </div>
- );
+ return <PropertiesSection title="Linked To" content={this.linkCount > 0 ? this.links : 'There are no current links.'} isOpen={this.openLinks} setIsOpen={bool => (this.openLinks = bool)} onDoubleClick={() => this.onDoubleClick()} />;
}
@computed get layoutSubMenu() {
- return (
- <div className="propertiesView-layout">
- <div className="propertiesView-layout-title" onPointerDown={action(() => (this.openLayout = !this.openLayout))} style={{ backgroundColor: this.openLayout ? 'black' : '' }}>
- Layout
- <div className="propertiesView-layout-title-icon">
- <FontAwesomeIcon icon={this.openLayout ? 'caret-down' : 'caret-right'} size="lg" color="white" />
- </div>
- </div>
- {this.openLayout ? <div className="propertiesView-layout-content">{this.layoutPreview}</div> : null}
- </div>
- );
+ return <PropertiesSection title="Layout" content={this.layoutPreview} isOpen={this.openLayout} setIsOpen={bool => (this.openLayout = bool)} onDoubleClick={() => this.onDoubleClick()} />;
}
@computed get description() {
@@ -1162,27 +1218,28 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
// handleDescriptionChange = (e: React.ChangeEvent<HTMLInputElement>) => { this.link_description = e.target.value; }
// handleRelationshipChange = (e: React.ChangeEvent<HTMLInputElement>) => { this.link_relationship = e.target.value; }
- @undoBatch
- handleDescriptionChange = action((value: string) => {
- if (LinkManager.currentLink && this.selectedDoc) {
- this.setDescripValue(value);
- return true;
- }
- });
-
- @undoBatch
- handlelinkRelationshipChange = action((value: string) => {
- if (LinkManager.currentLink && this.selectedDoc) {
- this.setlinkRelationshipValue(value);
- return true;
- }
- });
+ handleDescriptionChange = undoable(
+ action((value: string) => {
+ if (LinkManager.currentLink && this.selectedDoc) {
+ this.setDescripValue(value);
+ }
+ }),
+ 'change link description'
+ );
+
+ handlelinkRelationshipChange = undoable(
+ action((value: string) => {
+ if (LinkManager.currentLink && this.selectedDoc) {
+ this.setlinkRelationshipValue(value);
+ }
+ }),
+ 'change link relationship'
+ );
@undoBatch
setDescripValue = action((value: string) => {
if (LinkManager.currentLink) {
Doc.GetProto(LinkManager.currentLink).link_description = value;
- return true;
}
});
@@ -1194,7 +1251,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
Doc.GetProto(LinkManager.currentLink).link_relationship = value;
const linkRelationshipList = StrListCast(Doc.UserDoc().link_relationshipList);
const linkRelationshipSizes = NumListCast(Doc.UserDoc().link_relationshipSizes);
- const linkColorList = StrListCast(Doc.UserDoc().linkColorList);
+ const linkColorList = StrListCast(Doc.UserDoc().link_ColorList);
// if the relationship does not exist in the list, add it and a corresponding unique randomly generated color
if (!linkRelationshipList?.includes(value)) {
@@ -1228,8 +1285,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
}
});
- @undoBatch
- changeFollowBehavior = action((follow: Opt<string>) => this.sourceAnchor && (this.sourceAnchor.followLinkLocation = follow));
+ changeFollowBehavior = undoable((loc: Opt<string>) => this.sourceAnchor && (this.sourceAnchor.followLinkLocation = loc), 'change follow behavior');
@undoBatch
changeAnimationBehavior = action((behavior: string) => this.sourceAnchor && (this.sourceAnchor.followLinkAnimEffect = behavior));
@@ -1331,7 +1387,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
autoComplete="off"
style={{ textAlign: 'left' }}
id="link_description_input"
- value={Field.toString(LinkManager.currentLink?.link_description as any as Field)}
+ value={StrCast(LinkManager.currentLink?.link_description)}
onKeyDown={this.onDescriptionKey}
onBlur={this.onSelectOutDesc}
onChange={e => this.handleDescriptionChange(e.currentTarget.value)}
@@ -1349,6 +1405,227 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
this.sourceAnchor && (this.sourceAnchor.followLinkZoomScale = scale);
};
+ @computed get linkProperties() {
+ const zoom = Number((NumCast(this.sourceAnchor?.followLinkZoomScale, 1) * 100).toPrecision(3));
+ const targZoom = this.sourceAnchor?.followLinkZoom;
+ const indent = 30;
+ const hasSelectedAnchor = LinkManager.Links(this.sourceAnchor).includes(LinkManager.currentLink!);
+
+ return (
+ <>
+ <div className="propertiesView-section" style={{ background: 'darkgray' }}>
+ <div className="propertiesView-input first" style={{ display: 'grid', gridTemplateColumns: '84px auto' }}>
+ <p>Relationship</p>
+ {this.editRelationship}
+ </div>
+ <div className="propertiesView-input" style={{ display: 'grid', gridTemplateColumns: '84px auto' }}>
+ <p>Description</p>
+ {this.editDescription}
+ </div>
+ <div className="propertiesView-input inline">
+ <p>Show link</p>
+ <button
+ style={{ background: !LinkManager.currentLink?.link_displayLine ? '' : '#4476f7', borderRadius: 3 }}
+ onPointerDown={e => this.toggleLinkProp(e, 'link_displayLine')}
+ onClick={e => e.stopPropagation()}
+ className="propertiesButton">
+ <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" />
+ </button>
+ </div>
+ <div className="propertiesView-input inline" style={{ marginLeft: 10 }}>
+ <p>Auto-move anchors</p>
+ <button
+ style={{ background: !LinkManager.currentLink?.link_autoMoveAnchors ? '' : '#4476f7', borderRadius: 3 }}
+ onPointerDown={e => this.toggleLinkProp(e, 'link_autoMoveAnchors')}
+ onClick={e => e.stopPropagation()}
+ className="propertiesButton">
+ <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" />
+ </button>
+ </div>
+ <div className="propertiesView-input inline" style={{ marginLeft: 10 }}>
+ <p>Display arrow</p>
+ <button
+ style={{ background: !LinkManager.currentLink?.link_displayArrow ? '' : '#4476f7', borderRadius: 3 }}
+ onPointerDown={e => this.toggleLinkProp(e, 'link_displayArrow')}
+ onClick={e => e.stopPropagation()}
+ className="propertiesButton">
+ <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" />
+ </button>
+ </div>
+ </div>
+ {!hasSelectedAnchor ? null : (
+ <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)}>
+ <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>
+ <option value={OpenWhere.replaceLeft}>Replacing left tab</option>
+ <option value={OpenWhere.replaceRight}>Replacing right tab</option>
+ <option value={OpenWhere.lightbox}>Opening in lightbox</option>
+ <option value={OpenWhere.add}>Opening in new tab</option>
+ <option value={OpenWhere.replace}>Replacing current tab</option>
+ <option value={OpenWhere.inParent}>Opening in same collection</option>
+ {LinkManager.currentLink?.linksToAnnotation ? <option value="openExternal">Open in external page</option> : null}
+ </select>
+ </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')}>
+ <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()}>
+ {effect.toString()}
+ </option>
+ ))}
+ </select>
+ <div className="effectDirection" style={{ marginLeft: '10px', display: 'grid', width: 40, height: 36, gridColumn: 3, gridTemplateRows: '12px 12px 12px' }}>
+ {this.animationDirection(PresEffectDirection.Left, 'angle-right', 1, 2, {})}
+ {this.animationDirection(PresEffectDirection.Right, 'angle-left', 3, 2, {})}
+ {this.animationDirection(PresEffectDirection.Top, 'angle-down', 2, 1, {})}
+ {this.animationDirection(PresEffectDirection.Bottom, 'angle-up', 2, 3, {})}
+ {this.animationDirection(PresEffectDirection.Center, '', 2, 2, { width: 10, height: 10, alignSelf: 'center' })}
+ </div>
+ </div>
+ {PresBox.inputter(
+ '0.1',
+ '0.1',
+ '10',
+ NumCast(this.sourceAnchor?.followLinkTransitionTime) / 1000,
+ true,
+ (val: string) => PresBox.SetTransitionTime(val, (timeInMS: number) => this.sourceAnchor && (this.sourceAnchor.followLinkTransitionTime = timeInMS)),
+ indent
+ )}{' '}
+ <div
+ className={'slider-headers'}
+ style={{
+ display: 'grid',
+ justifyContent: 'space-between',
+ width: `calc(100% - ${indent * 2}px)`,
+ marginLeft: indent,
+ marginRight: indent,
+ gridTemplateColumns: 'auto auto',
+ borderTop: 'solid',
+ }}>
+ <div className="slider-text">Fast</div>
+ <div className="slider-text">Slow</div>
+ </div>{' '}
+ <div className="propertiesView-input inline">
+ <p>Play Target Audio</p>
+ <button
+ style={{ background: !this.sourceAnchor?.followLinkAudio ? '' : '#4476f7', borderRadius: 3 }}
+ onPointerDown={e => this.toggleAnchorProp(e, 'followLinkAudio', this.sourceAnchor)}
+ onClick={e => e.stopPropagation()}
+ className="propertiesButton">
+ <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" />
+ </button>
+ </div>
+ <div className="propertiesView-input inline">
+ <p>Zoom Text Selections</p>
+ <button
+ style={{ background: !this.sourceAnchor?.followLinkZoomText ? '' : '#4476f7', borderRadius: 3 }}
+ onPointerDown={e => this.toggleAnchorProp(e, 'followLinkZoomText', this.sourceAnchor)}
+ onClick={e => e.stopPropagation()}
+ className="propertiesButton">
+ <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" />
+ </button>
+ </div>
+ <div className="propertiesView-input inline">
+ <p>Toggle Follow to Outer Context</p>
+ <button
+ style={{ background: !this.sourceAnchor?.followLinkToOuterContext ? '' : '#4476f7', borderRadius: 3 }}
+ onPointerDown={e => this.toggleAnchorProp(e, 'followLinkToOuterContext', this.sourceAnchor)}
+ onClick={e => e.stopPropagation()}
+ className="propertiesButton">
+ <FontAwesomeIcon className="fa-icon" icon={faWindowMaximize as IconLookup} size="lg" />
+ </button>
+ </div>
+ <div className="propertiesView-input inline">
+ <p>Toggle Target (Show/Hide)</p>
+ <button
+ style={{ background: !this.sourceAnchor?.followLinkToggle ? '' : '#4476f7', borderRadius: 3 }}
+ onPointerDown={e => this.toggleAnchorProp(e, 'followLinkToggle', this.sourceAnchor)}
+ onClick={e => e.stopPropagation()}
+ className="propertiesButton">
+ <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" />
+ </button>
+ </div>
+ <div className="propertiesView-input inline">
+ <p>Ease Transitions</p>
+ <button
+ style={{ background: this.sourceAnchor?.followLinkEase === 'linear' ? '' : '#4476f7', borderRadius: 3 }}
+ onPointerDown={e => this.toggleAnchorProp(e, 'followLinkEase', this.sourceAnchor, 'ease', 'linear')}
+ onClick={e => e.stopPropagation()}
+ className="propertiesButton">
+ <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" />
+ </button>
+ </div>
+ <div className="propertiesView-input inline">
+ <p>Capture Offset to Target</p>
+ <button
+ style={{ background: this.sourceAnchor?.followLinkXoffset === undefined ? '' : '#4476f7', borderRadius: 3 }}
+ onPointerDown={e => {
+ this.toggleAnchorProp(e, 'followLinkXoffset', this.sourceAnchor, NumCast(this.destinationAnchor?.x) - NumCast(this.sourceAnchor?.x), undefined);
+ this.toggleAnchorProp(e, 'followLinkYoffset', this.sourceAnchor, NumCast(this.destinationAnchor?.y) - NumCast(this.sourceAnchor?.y), undefined);
+ }}
+ onClick={e => e.stopPropagation()}
+ className="propertiesButton">
+ <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" />
+ </button>
+ </div>
+ <div className="propertiesView-input inline">
+ <p>Center Target (no zoom)</p>
+ <button
+ style={{ background: this.sourceAnchor?.followLinkZoom ? '' : '#4476f7', borderRadius: 3 }}
+ onPointerDown={e => this.toggleAnchorProp(e, 'followLinkZoom', this.sourceAnchor)}
+ onClick={e => e.stopPropagation()}
+ className="propertiesButton">
+ <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" />
+ </button>
+ </div>
+ <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} />
+ <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'} />
+ </div>
+ <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setZoom(String(zoom), -0.1))}>
+ <FontAwesomeIcon icon={'caret-down'} />
+ </div>
+ </div>
+ </div>
+ <button
+ style={{ background: !targZoom || this.sourceAnchor?.followLinkZoomScale === 0 ? '' : '#4476f7', borderRadius: 3, gridColumn: 3 }}
+ onPointerDown={e => this.toggleAnchorProp(e, 'followLinkZoom', this.sourceAnchor)}
+ onClick={e => e.stopPropagation()}
+ className="propertiesButton">
+ <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" />
+ </button>
+ </div>
+ {!targZoom ? null : PresBox.inputter('0', '1', '100', zoom, true, this.setZoom, 30)}
+ <div
+ className={'slider-headers'}
+ style={{
+ display: !targZoom ? 'none' : 'grid',
+ justifyContent: 'space-between',
+ width: `calc(100% - ${indent * 2}px)`,
+ marginLeft: indent,
+ marginRight: indent,
+ gridTemplateColumns: 'auto auto',
+ borderTop: 'solid',
+ }}>
+ <div className="slider-text">0%</div>
+ <div className="slider-text">100%</div>
+ </div>{' '}
+ </div>
+ )}
+ </>
+ );
+ }
+
/**
* Handles adding and removing members from the sharing panel
*/
@@ -1362,9 +1639,6 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
render() {
const isNovice = Doc.noviceMode;
- const zoom = Number((NumCast(this.sourceAnchor?.followLinkZoomScale, 1) * 100).toPrecision(3));
- const targZoom = this.sourceAnchor?.followLinkZoom;
- const indent = 30;
const hasSelectedAnchor = LinkManager.Links(this.sourceAnchor).includes(LinkManager.currentLink!);
if (!this.selectedDoc && !this.isPres) {
return (
@@ -1380,260 +1654,49 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
<div
className="propertiesView"
style={{
+ background: StrCast(Doc.UserDoc().userBackgroundColor),
+ color: StrCast(Doc.UserDoc().userColor),
width: this.props.width,
minWidth: this.props.width,
- //overflowY: this.scrolling ? "scroll" : "visible"
}}>
- <div className="propertiesView-title" style={{ width: this.props.width }}>
- Properties
+ <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>
</div>
- <div className="propertiesView-name">{this.editableTitle}</div>
+ <div className="propertiesView-name">{this.editableTitle}</div>
+ <div className="propertiesView-type"> {this.currentType} </div>
{this.contextsSubMenu}
-
{this.linksSubMenu}
- {!this.selectedDoc || !LinkManager.currentLink || (!hasSelectedAnchor && this.selectedDoc !== LinkManager.currentLink) ? null : (
- <>
- <div className="propertiesView-section" style={{ background: 'darkgray' }}>
- <div className="propertiesView-input first" style={{ display: 'grid', gridTemplateColumns: '84px auto' }}>
- <p>Relationship</p>
- {this.editRelationship}
- </div>
- <div className="propertiesView-input" style={{ display: 'grid', gridTemplateColumns: '84px auto' }}>
- <p>Description</p>
- {this.editDescription}
- </div>
- <div className="propertiesView-input inline">
- <p>Show link</p>
- <button
- style={{ background: !LinkManager.currentLink?.layout_linkDisplay ? '' : '#4476f7', borderRadius: 3 }}
- onPointerDown={e => this.toggleLinkProp(e, 'layout_linkDisplay')}
- onClick={e => e.stopPropagation()}
- className="propertiesButton">
- <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" />
- </button>
- </div>
- <div className="propertiesView-input inline" style={{ marginLeft: 10 }}>
- <p>Auto-move anchors</p>
- <button
- style={{ background: !LinkManager.currentLink?.layout_autoMoveAnchors ? '' : '#4476f7', borderRadius: 3 }}
- onPointerDown={e => this.toggleLinkProp(e, 'layout_autoMoveAnchors')}
- onClick={e => e.stopPropagation()}
- className="propertiesButton">
- <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" />
- </button>
- </div>
- <div className="propertiesView-input inline" style={{ marginLeft: 10 }}>
- <p>Display arrow</p>
- <button
- style={{ background: !LinkManager.currentLink?.layout_linkDisplayArrow ? '' : '#4476f7', borderRadius: 3 }}
- onPointerDown={e => this.toggleLinkProp(e, 'layout_linkDisplayArrow')}
- onClick={e => e.stopPropagation()}
- className="propertiesButton">
- <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" />
- </button>
- </div>
- </div>
- {!hasSelectedAnchor ? null : (
- <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)}>
- <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>
- <option value={OpenWhere.replaceLeft}>Replacing left tab</option>
- <option value={OpenWhere.replaceRight}>Replacing right tab</option>
- <option value={OpenWhere.fullScreen}>Overlaying current tab</option>
- <option value={OpenWhere.lightbox}>Opening in lightbox</option>
- <option value={OpenWhere.add}>Opening in new tab</option>
- <option value={OpenWhere.replace}>Replacing current tab</option>
- <option value={OpenWhere.inParent}>Opening in same collection</option>
- {LinkManager.currentLink?.linksToAnnotation ? <option value="openExternal">Open in external page</option> : null}
- </select>
- </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')}>
- <option value="default">Default</option>
- {[PresEffect.None, PresEffect.Zoom, PresEffect.Lightspeed, PresEffect.Fade, PresEffect.Flip, PresEffect.Rotate, PresEffect.Bounce, PresEffect.Roll].map(effect => (
- <option value={effect.toString()}>{effect.toString()}</option>
- ))}
- </select>
- <div className="effectDirection" style={{ marginLeft: '10px', display: 'grid', width: 40, height: 36, gridColumn: 3, gridTemplateRows: '12px 12px 12px' }}>
- {this.animationDirection(PresEffectDirection.Left, 'angle-right', 1, 2, {})}
- {this.animationDirection(PresEffectDirection.Right, 'angle-left', 3, 2, {})}
- {this.animationDirection(PresEffectDirection.Top, 'angle-down', 2, 1, {})}
- {this.animationDirection(PresEffectDirection.Bottom, 'angle-up', 2, 3, {})}
- {this.animationDirection(PresEffectDirection.Center, '', 2, 2, { width: 10, height: 10, alignSelf: 'center' })}
- </div>
- </div>
- {PresBox.inputter(
- '0.1',
- '0.1',
- '10',
- NumCast(this.sourceAnchor?.followLinkTransitionTime) / 1000,
- true,
- (val: string) => PresBox.SetTransitionTime(val, (timeInMS: number) => this.sourceAnchor && (this.sourceAnchor.followLinkTransitionTime = timeInMS)),
- indent
- )}{' '}
- <div
- className={'slider-headers'}
- style={{
- display: 'grid',
- justifyContent: 'space-between',
- width: `calc(100% - ${indent * 2}px)`,
- marginLeft: indent,
- marginRight: indent,
- gridTemplateColumns: 'auto auto',
- borderTop: 'solid',
- }}>
- <div className="slider-text">Fast</div>
- <div className="slider-text">Slow</div>
- </div>{' '}
- <div className="propertiesView-input inline">
- <p>Play Target Audio</p>
- <button
- style={{ background: !this.sourceAnchor?.followLinkAudio ? '' : '#4476f7', borderRadius: 3 }}
- onPointerDown={e => this.toggleAnchorProp(e, 'followLinkAudio', this.sourceAnchor)}
- onClick={e => e.stopPropagation()}
- className="propertiesButton">
- <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" />
- </button>
- </div>
- <div className="propertiesView-input inline">
- <p>Zoom Text Selections</p>
- <button
- style={{ background: !this.sourceAnchor?.followLinkZoomText ? '' : '#4476f7', borderRadius: 3 }}
- onPointerDown={e => this.toggleAnchorProp(e, 'followLinkZoomText', this.sourceAnchor)}
- onClick={e => e.stopPropagation()}
- className="propertiesButton">
- <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" />
- </button>
- </div>
- <div className="propertiesView-input inline">
- <p>Toggle Follow to Outer Context</p>
- <button
- style={{ background: !this.sourceAnchor?.followLinkToOuterContext ? '' : '#4476f7', borderRadius: 3 }}
- onPointerDown={e => this.toggleAnchorProp(e, 'followLinkToOuterContext', this.sourceAnchor)}
- onClick={e => e.stopPropagation()}
- className="propertiesButton">
- <FontAwesomeIcon className="fa-icon" icon={faWindowMaximize as IconLookup} size="lg" />
- </button>
- </div>
- <div className="propertiesView-input inline">
- <p>Toggle Target (Show/Hide)</p>
- <button
- style={{ background: !this.sourceAnchor?.followLinkToggle ? '' : '#4476f7', borderRadius: 3 }}
- onPointerDown={e => this.toggleAnchorProp(e, 'followLinkToggle', this.sourceAnchor)}
- onClick={e => e.stopPropagation()}
- className="propertiesButton">
- <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" />
- </button>
- </div>
- <div className="propertiesView-input inline">
- <p>Ease Transitions</p>
- <button
- style={{ background: this.sourceAnchor?.followLinkEase === 'linear' ? '' : '#4476f7', borderRadius: 3 }}
- onPointerDown={e => this.toggleAnchorProp(e, 'followLinkEase', this.sourceAnchor, 'ease', 'linear')}
- onClick={e => e.stopPropagation()}
- className="propertiesButton">
- <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" />
- </button>
- </div>
- <div className="propertiesView-input inline">
- <p>Capture Offset to Target</p>
- <button
- style={{ background: this.sourceAnchor?.followLinkXoffset === undefined ? '' : '#4476f7', borderRadius: 3 }}
- onPointerDown={e => {
- this.toggleAnchorProp(e, 'followLinkXoffset', this.sourceAnchor, NumCast(this.destinationAnchor?.x) - NumCast(this.sourceAnchor?.x), undefined);
- this.toggleAnchorProp(e, 'followLinkYoffset', this.sourceAnchor, NumCast(this.destinationAnchor?.y) - NumCast(this.sourceAnchor?.y), undefined);
- }}
- onClick={e => e.stopPropagation()}
- className="propertiesButton">
- <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" />
- </button>
- </div>
- <div className="propertiesView-input inline">
- <p>Center Target (no zoom)</p>
- <button
- style={{ background: this.sourceAnchor?.followLinkZoom ? '' : '#4476f7', borderRadius: 3 }}
- onPointerDown={e => this.toggleAnchorProp(e, 'followLinkZoom', this.sourceAnchor)}
- onClick={e => e.stopPropagation()}
- className="propertiesButton">
- <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" />
- </button>
- </div>
- <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%' }} 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'} />
- </div>
- <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setZoom(String(zoom), -0.1))}>
- <FontAwesomeIcon icon={'caret-down'} />
- </div>
- </div>
- </div>
- <button
- style={{ background: !targZoom || this.sourceAnchor?.followLinkZoomScale === 0 ? '' : '#4476f7', borderRadius: 3, gridColumn: 3 }}
- onPointerDown={e => this.toggleAnchorProp(e, 'followLinkZoom', this.sourceAnchor)}
- onClick={e => e.stopPropagation()}
- className="propertiesButton">
- <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" />
- </button>
- </div>
- {!targZoom ? null : PresBox.inputter('0', '1', '100', zoom, true, this.setZoom, 30)}
- <div
- className={'slider-headers'}
- style={{
- display: !targZoom ? 'none' : 'grid',
- justifyContent: 'space-between',
- width: `calc(100% - ${indent * 2}px)`,
- marginLeft: indent,
- marginRight: indent,
- gridTemplateColumns: 'auto auto',
- borderTop: 'solid',
- }}>
- <div className="slider-text">0%</div>
- <div className="slider-text">100%</div>
- </div>{' '}
- </div>
- )}
- </>
- )}
-
+ {!this.selectedDoc || !LinkManager.currentLink || (!hasSelectedAnchor && this.selectedDoc !== LinkManager.currentLink) ? null : this.linkProperties}
{this.inkSubMenu}
-
{this.optionsSubMenu}
-
{this.fieldsSubMenu}
-
{isNovice ? null : this.sharingSubMenu}
-
{isNovice ? null : this.filtersSubMenu}
-
{isNovice ? null : this.layoutSubMenu}
</div>
);
}
- if (this.isPres) {
- const selectedItem: boolean = PresBox.Instance?.selectedArray.size > 0;
+ if (this.isPres && PresBox.Instance) {
+ const selectedItem: boolean = PresBox.Instance.selectedArray.size > 0;
const type = [DocumentType.AUDIO, DocumentType.VID].includes(DocCast(PresBox.Instance.activeItem?.annotationOn)?.type as any as DocumentType)
? (DocCast(PresBox.Instance.activeItem?.annotationOn)?.type as any as DocumentType)
: PresBox.targetRenderedDoc(PresBox.Instance.activeItem)?.type;
return (
<div className="propertiesView" style={{ width: this.props.width }}>
- <div className="propertiesView-title" style={{ width: this.props.width }}>
+ <div className="propertiesView-sectionTitle" style={{ width: this.props.width }}>
Presentation
</div>
<div className="propertiesView-name" style={{ borderBottom: 0 }}>
{this.editableTitle}
<div className="propertiesView-presSelected">
- <div className="propertiesView-selectedCount">{PresBox.Instance?.selectedArray.size} selected</div>
- <div className="propertiesView-selectedList">{PresBox.Instance?.listOfSelected}</div>
+ <div className="propertiesView-selectedCount">{PresBox.Instance.selectedArray.size} selected</div>
+ <div className="propertiesView-selectedList">{PresBox.Instance.listOfSelected}</div>
</div>
</div>
{!selectedItem ? null : (
@@ -1641,7 +1704,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
<div className="propertiesView-presTrails-title" onPointerDown={action(() => (this.openPresTransitions = !this.openPresTransitions))} style={{ backgroundColor: this.openPresTransitions ? 'black' : '' }}>
&nbsp; <FontAwesomeIcon style={{ alignSelf: 'center' }} icon={'rocket'} /> &nbsp; Transitions
<div className="propertiesView-presTrails-title-icon">
- <FontAwesomeIcon icon={this.openPresTransitions ? 'caret-down' : 'caret-right'} size="lg" color="white" />
+ <FontAwesomeIcon icon={this.openPresTransitions ? 'caret-down' : 'caret-right'} size="lg" />
</div>
</div>
{this.openPresTransitions ? <div className="propertiesView-presTrails-content">{PresBox.Instance.transitionDropdown}</div> : null}
@@ -1655,7 +1718,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
style={{ backgroundColor: this.openPresTransitions ? 'black' : '' }}>
&nbsp; <FontAwesomeIcon style={{ alignSelf: 'center' }} icon={'rocket'} /> &nbsp; Visibilty
<div className="propertiesView-presTrails-title-icon">
- <FontAwesomeIcon icon={this.openPresVisibilityAndDuration ? 'caret-down' : 'caret-right'} size="lg" color="white" />
+ <FontAwesomeIcon icon={this.openPresVisibilityAndDuration ? 'caret-down' : 'caret-right'} size="lg" />
</div>
</div>
{this.openPresVisibilityAndDuration ? <div className="propertiesView-presTrails-content">{PresBox.Instance.visibiltyDurationDropdown}</div> : null}
@@ -1666,7 +1729,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
<div className="propertiesView-presTrails-title" onPointerDown={action(() => (this.openPresProgressivize = !this.openPresProgressivize))} style={{ backgroundColor: this.openPresTransitions ? 'black' : '' }}>
&nbsp; <FontAwesomeIcon style={{ alignSelf: 'center' }} icon={'rocket'} /> &nbsp; Progressivize
<div className="propertiesView-presTrails-title-icon">
- <FontAwesomeIcon icon={this.openPresProgressivize ? 'caret-down' : 'caret-right'} size="lg" color="white" />
+ <FontAwesomeIcon icon={this.openPresProgressivize ? 'caret-down' : 'caret-right'} size="lg" />
</div>
</div>
{this.openPresProgressivize ? <div className="propertiesView-presTrails-content">{PresBox.Instance.progressivizeDropdown}</div> : null}
@@ -1677,25 +1740,12 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
<div className="propertiesView-presTrails-title" onPointerDown={action(() => (this.openSlideOptions = !this.openSlideOptions))} style={{ backgroundColor: this.openSlideOptions ? 'black' : '' }}>
&nbsp; <FontAwesomeIcon style={{ alignSelf: 'center' }} icon={type === DocumentType.AUDIO ? 'file-audio' : 'file-video'} /> &nbsp; {type === DocumentType.AUDIO ? 'Audio Options' : 'Video Options'}
<div className="propertiesView-presTrails-title-icon">
- <FontAwesomeIcon icon={this.openSlideOptions ? 'caret-down' : 'caret-right'} size="lg" color="white" />
+ <FontAwesomeIcon icon={this.openSlideOptions ? 'caret-down' : 'caret-right'} size="lg" />
</div>
</div>
{this.openSlideOptions ? <div className="propertiesView-presTrails-content">{PresBox.Instance.mediaOptionsDropdown}</div> : null}
</div>
)}
- {/* <div className="propertiesView-presTrails">
- <div className="propertiesView-presTrails-title"
- onPointerDown={action(() => { this.openAddSlide = !this.openAddSlide; })}
- style={{ backgroundColor: this.openAddSlide ? "black" : "" }}>
- &nbsp; <FontAwesomeIcon icon={"plus"} /> &nbsp; Add new slide
- <div className="propertiesView-presTrails-title-icon">
- <FontAwesomeIcon icon={this.openAddSlide ? "caret-down" : "caret-right"} size="lg" color="white" />
- </div>
- </div>
- {this.openAddSlide ? <div className="propertiesView-presTrails-content">
- {PresBox.Instance.newDocumentDropdown}
- </div> : null}
- </div> */}
</div>
);
}
diff --git a/src/client/views/ScriptingRepl.tsx b/src/client/views/ScriptingRepl.tsx
index 5dfe10def..2bc2d5e6b 100644
--- a/src/client/views/ScriptingRepl.tsx
+++ b/src/client/views/ScriptingRepl.tsx
@@ -5,6 +5,7 @@ import * as React from 'react';
import { DocumentManager } from '../util/DocumentManager';
import { CompileScript, Transformer, ts } from '../util/Scripting';
import { ScriptingGlobals } from '../util/ScriptingGlobals';
+import { undoable } from '../util/UndoManager';
import { DocumentIconContainer } from './nodes/DocumentIcon';
import { OverlayView } from './OverlayView';
import './ScriptingRepl.scss';
@@ -161,7 +162,7 @@ export class ScriptingRepl extends React.Component {
this.commands.push({ command: this.commandString, result: script.errors });
return;
}
- const result = script.run({ args: this.args }, e => this.commands.push({ command: this.commandString, result: e.toString() }));
+ const result = undoable(() => script.run({ args: this.args }, e => this.commands.push({ command: this.commandString, result: e.toString() })), 'run:' + this.commandString)();
if (result.success) {
this.commands.push({ command: this.commandString, result: result.result });
this.commandsHistory.push(this.commandString);
diff --git a/src/client/views/SidebarAnnos.scss b/src/client/views/SidebarAnnos.scss
index a0506cb3a..d7de2b641 100644
--- a/src/client/views/SidebarAnnos.scss
+++ b/src/client/views/SidebarAnnos.scss
@@ -4,22 +4,26 @@
overflow: auto;
flex-flow: row;
flex-wrap: wrap;
- .sidebarAnnos-filterTag, .sidebarAnnos-filterTag-active,
- .sidebarAnnos-filterUser, .sidebarAnnos-filterUser-active {
+ .sidebarAnnos-filterTag,
+ .sidebarAnnos-filterTag-active,
+ .sidebarAnnos-filterUser,
+ .sidebarAnnos-filterUser-active {
font-weight: bold;
font-size: 10px;
padding-left: 5;
padding-right: 5;
box-shadow: black 1px 1px 3px;
border-radius: 5;
- margin: 2;
+ margin: 2;
height: 15;
background-color: lightgrey;
}
- .sidebarAnnos-filterUser, .sidebarAnnos-filterUser-active {
+ .sidebarAnnos-filterUser,
+ .sidebarAnnos-filterUser-active {
border-radius: 15px;
}
+ .sidebarAnnos-filterUser-active,
.sidebarAnnos-filterTag-active {
background-color: white;
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx
index 42993bfc4..cd50865fb 100644
--- a/src/client/views/SidebarAnnos.tsx
+++ b/src/client/views/SidebarAnnos.tsx
@@ -49,17 +49,25 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
);
return keys;
}
+ @computed get allHashtags() {
+ const keys = new Set<string>();
+ DocListCast(this.props.rootDoc[this.sidebarKey]).forEach(doc => StrListCast(doc.tags).forEach(tag => keys.add(tag)));
+ return Array.from(keys.keys())
+ .filter(key => key[0])
+ .filter(key => !key.startsWith('_') && (key[0] === '#' || key[0] === key[0].toUpperCase()))
+ .sort();
+ }
@computed get allUsers() {
const keys = new Set<string>();
DocListCast(this.props.rootDoc[this.sidebarKey]).forEach(doc => keys.add(StrCast(doc.author)));
return Array.from(keys.keys()).sort();
}
get filtersKey() {
- return '_' + this.sidebarKey + '-docFilters';
+ return '_' + this.sidebarKey + '_childFilters';
}
anchorMenuClick = (anchor: Doc) => {
- const startup = StrListCast(this.props.rootDoc.docFilters)
+ const startup = StrListCast(this.props.rootDoc.childFilters)
.map(filter => filter.split(':')[0])
.join(' ');
const target = Docs.Create.TextDocument(startup, {
@@ -69,15 +77,15 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
_height: 50,
_layout_fitWidth: true,
_layout_autoHeight: true,
- _fontSize: StrCast(Doc.UserDoc().fontSize),
- _fontFamily: StrCast(Doc.UserDoc().fontFamily),
+ _text_fontSize: StrCast(Doc.UserDoc().fontSize),
+ _text_fontFamily: StrCast(Doc.UserDoc().fontFamily),
});
FormattedTextBox.SelectOnLoad = target[Id];
FormattedTextBox.DontSelectInitialText = true;
const link = DocUtils.MakeLink(anchor, target, { link_relationship: 'inline comment:comment on' });
- link && (link.layout_linkDisplay = false);
+ link && (link.link_displayLine = false);
- const taggedContent = this.docFilters()
+ const taggedContent = this.childFilters()
.filter(data => data.split(':')[0])
.map(data => {
const key = data.split(':')[0];
@@ -141,7 +149,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
return target;
};
makeDocUnfiltered = (doc: Doc) => {
- if (DocListCast(this.props.rootDoc[this.sidebarKey]).find(anno => Doc.AreProtosEqual(doc.unrendered ? DocCast(doc.annotationOn) : doc, anno))) {
+ if (DocListCast(this.props.rootDoc[this.sidebarKey]).find(anno => Doc.AreProtosEqual(doc.layout_unrendered ? DocCast(doc.annotationOn) : doc, anno))) {
if (this.props.layoutDoc[this.filtersKey]) {
this.props.layoutDoc[this.filtersKey] = new List<string>();
}
@@ -169,7 +177,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
addDocument = (doc: Doc | Doc[]) => this.props.sidebarAddDocument(doc, this.sidebarKey);
moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => this.props.moveDocument(doc, targetCollection, addDocument, this.sidebarKey);
removeDocument = (doc: Doc | Doc[]) => this.props.removeDocument(doc, this.sidebarKey);
- docFilters = () => [...StrListCast(this.props.layoutDoc._docFilters), ...StrListCast(this.props.layoutDoc[this.filtersKey])];
+ childFilters = () => [...StrListCast(this.props.layoutDoc._childFilters), ...StrListCast(this.props.layoutDoc[this.filtersKey])];
layout_showTitle = () => 'title';
setHeightCallback = (height: number) => this.props.setHeight?.(height + this.filtersHeight());
sortByLinkAnchorY = (a: Doc, b: Doc) => {
@@ -179,9 +187,9 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
};
render() {
const renderTag = (tag: string) => {
- const active = StrListCast(this.props.rootDoc[this.filtersKey]).includes(`${tag}:${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, tag, tag, 'check', true, this.sidebarKey, e.shiftKey)}>
+ <div key={tag} className={`sidebarAnnos-filterTag${active ? '-active' : ''}`} onClick={e => Doc.setDocFilter(this.props.rootDoc, 'tags', tag, 'check', true, this.sidebarKey, e.shiftKey)}>
{tag}
</div>
);
@@ -216,6 +224,7 @@ 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())
.sort()
.map(key => renderMeta(key, this.allMetadata.get(key)))}
@@ -232,7 +241,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
ref={this._stackRef}
PanelHeight={this.panelHeight}
PanelWidth={this.panelWidth}
- docFilters={this.docFilters}
+ childFilters={this.childFilters}
sortFunc={this.sortByLinkAnchorY}
setHeight={this.setHeightCallback}
isAnnotationOverlay={false}
@@ -248,7 +257,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
addDocument={this.addDocument}
ScreenToLocalTransform={this.screenToLocalTransform}
renderDepth={this.props.renderDepth + 1}
- viewType={CollectionViewType.Stacking}
+ type_collection={CollectionViewType.Stacking}
fieldKey={this.sidebarKey}
pointerEvents={returnAll}
/>
diff --git a/src/client/views/StyleProvider.scss b/src/client/views/StyleProvider.scss
index 80c878386..c06bb287e 100644
--- a/src/client/views/StyleProvider.scss
+++ b/src/client/views/StyleProvider.scss
@@ -31,8 +31,8 @@
.styleProvider-treeView-icon,
.styleProvider-treeView-icon-active {
- margin-left: 0.25rem;
- margin-right: 0.25rem;
+ margin-left: 0;
+ margin-right: 0;
}
.styleProvider-treeView-icon {
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index e852d9297..63ff348e3 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -1,7 +1,7 @@
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tooltip } from '@material-ui/core';
-import { Shadows } from 'browndash-components';
+import { IconButton, Shadows, Size } from 'browndash-components';
import { action, runInAction } from 'mobx';
import { extname } from 'path';
import { Doc, Opt, StrListCast } from '../../fields/Doc';
@@ -18,13 +18,13 @@ import { TreeSort } from './collections/TreeView';
import { Colors } from './global/globalEnums';
import { InkingStroke } from './InkingStroke';
import { MainView } from './MainView';
-import { DocumentViewProps, OpenWhere } from './nodes/DocumentView';
+import { 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');
-import { SchemaRowBox } from './collections/collectionSchema/SchemaRowBox';
export enum StyleProp {
TreeViewIcon = 'treeViewIcon',
@@ -37,14 +37,13 @@ export enum StyleProp {
BackgroundColor = 'backgroundColor', // background color of a document view
FillColor = 'fillColor', // fill color of an ink stroke or shape
WidgetColor = 'widgetColor', // color to display UI widgets on a document view -- used for the sidebar divider dragger on a text note
- HideLinkButton = 'hideLinkButton', // hides the blue-dot link button. used when a document acts like a button
+ HideLinkBtn = 'hideLinkButton', // hides the blue-dot link button. used when a document acts like a button
PointerEvents = 'pointerEvents', // pointer events for DocumentView -- inherits pointer events if not specified
Decorations = 'decorations', // additional decoration to display above a DocumentView -- currently only used to display a Lock for making things background
HeaderMargin = 'headerMargin', // margin at top of documentview, typically for displaying a title -- doc contents will start below that
- showCaption = 'layout_showCaption',
+ ShowCaption = 'layout_showCaption',
TitleHeight = 'titleHeight', // Height of Title area
- layout_showTitle = 'layout_showTitle', // whether to display a title on a Document (optional :hover suffix)
- JitterRotation = 'jitterRotation', // whether documents should be randomly rotated
+ ShowTitle = 'layout_showTitle', // whether to display a title on a Document (optional :hover suffix)
BorderPath = 'customBorder', // border path for document view
FontSize = 'fontSize', // size of text font
FontFamily = 'fontFamily', // font family of text
@@ -84,20 +83,21 @@ export function wavyBorderPath(pw: number, ph: number, inset: number = 0.05) {
// a preliminary implementation of a dash style sheet for setting rendering properties of documents nested within a Tab
//
export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string): any {
- const remoteDocHeader = 'author;creationDate;noMargin';
+ const remoteDocHeader = 'author;author_date;noMargin';
const docProps = testDocProps(props) ? props : undefined;
const selected = property.includes(':selected');
const isCaption = property.includes(':caption');
const isAnchor = property.includes(':anchor');
const isAnnotated = property.includes(':annotated');
const isOpen = property.includes(':open');
+ const isEmpty = property.includes(':empty');
const boxBackground = property.includes(':box');
const fieldKey = props?.fieldKey ? props.fieldKey + '_' : isCaption ? 'caption_' : '';
const lockedPosition = () => doc && BoolCast(doc._lockedPosition);
const backgroundCol = () => props?.styleProvider?.(doc, props, StyleProp.BackgroundColor);
const opacity = () => props?.styleProvider?.(doc, props, StyleProp.Opacity);
- const layout_showTitle = () => props?.styleProvider?.(doc, props, StyleProp.layout_showTitle);
- const random = (min: number, max: number, x: number, y: number) => /* min should not be equal to max */ min + (((Math.abs(x * y) * 9301 + 49297) % 233280) / 233280) * (max - min);
+ const layout_showTitle = () => props?.styleProvider?.(doc, props, StyleProp.ShowTitle);
+ // prettier-ignore
switch (property.split(':')[0]) {
case StyleProp.TreeViewIcon:
const img = ImageCast(doc?.icon, ImageCast(doc?.data));
@@ -106,17 +106,17 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
const url = doc?.icon ? img.url.href : img.url.href.replace(ext, '_s' + ext);
return <img src={url} width={20} height={15} style={{ margin: 'auto', display: 'block', objectFit: 'contain' }} />;
}
- return Doc.toIcon(doc, isOpen);
-
+ return Doc.toIcon(doc, isEmpty ? undefined : isOpen);
case StyleProp.TreeViewSortings:
- const allSorts: { [key: string]: { color: string; label: string } | undefined } = {};
- allSorts[TreeSort.Down] = { color: 'blue', label: '↓' };
- allSorts[TreeSort.Up] = { color: 'crimson', label: '↑' };
- if (doc?._viewType === CollectionViewType.Freeform) allSorts[TreeSort.Zindex] = { color: 'green', label: 'z' };
- allSorts[TreeSort.None] = { color: 'darkgray', label: '\u00A0\u00A0\u00A0' };
+ const allSorts: { [key: string]: { color: string; icon: JSX.Element | string } | undefined } = {};
+ allSorts[TreeSort.Down] = { color: Colors.MEDIUM_BLUE, icon: <BsArrowDown/> };
+ allSorts[TreeSort.Up] = { color: 'crimson', icon: <BsArrowUp/> };
+ if (doc?._type_collection === CollectionViewType.Freeform) allSorts[TreeSort.Zindex] = { color: 'green', icon: 'Z' };
+ allSorts[TreeSort.None] = { color: 'darkgray', icon: <BsArrowDownUp/> };
return allSorts;
case StyleProp.Highlighting:
- if (doc && !doc.disableDocBrushing && !props?.disableDocBrushing) {
+ if (doc && (Doc.IsSystem(doc) || doc.type === DocumentType.FONTICON)) return undefined;
+ if (doc && !doc.layout_disableBrushing && !props?.disableBrushing) {
const selected = SelectionManager.Views().some(dv => dv.rootDoc === doc);
const highlightIndex = Doc.isBrushedHighlightedDegree(doc) || (selected ? Doc.DocBrushStatus.selfBrushed : 0);
const highlightColor = ['transparent', 'rgb(68, 118, 247)', selected ? 'black' : 'rgb(68, 118, 247)', 'orange', 'lightBlue'][highlightIndex];
@@ -131,21 +131,17 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
}
}
return undefined;
- case StyleProp.DocContents:
- return undefined;
- case StyleProp.WidgetColor:
- return isAnnotated ? Colors.LIGHT_BLUE : darkScheme() ? 'lightgrey' : 'dimgrey';
- case StyleProp.Opacity:
- return props?.LayoutTemplateString?.includes(KeyValueBox.name) ? 1 : Cast(doc?._opacity, 'number', Cast(doc?.opacity, 'number', null));
- case StyleProp.HideLinkButton:
- return props?.hideLinkButton || (!selected && doc?.layout_hideLinkButton);
- case StyleProp.FontSize:
- return StrCast(doc?.[fieldKey + 'fontSize'], StrCast(doc?._fontSize, StrCast(Doc.UserDoc().fontSize)));
- case StyleProp.FontFamily:
- return StrCast(doc?.[fieldKey + 'fontFamily'], StrCast(doc?._fontFamily, StrCast(Doc.UserDoc().fontFamily)));
- case StyleProp.FontWeight:
- return StrCast(doc?.[fieldKey + 'fontWeight'], StrCast(doc?._fontWeight, StrCast(Doc.UserDoc().fontWeight)));
- case StyleProp.layout_showTitle:
+ case StyleProp.DocContents:return undefined;
+ case StyleProp.WidgetColor:return isAnnotated ? Colors.LIGHT_BLUE : darkScheme() ? 'lightgrey' : 'dimgrey';
+ case StyleProp.Opacity: return props?.LayoutTemplateString?.includes(KeyValueBox.name) ? 1 : doc?.textInlineAnnotations ? 0 : Cast(doc?._opacity, "number", Cast(doc?.opacity, 'number', null));
+ case StyleProp.HideLinkBtn:return props?.hideLinkButton || (!selected && doc?.layout_hideLinkButton);
+ case StyleProp.FontSize: return StrCast(doc?.[fieldKey + 'fontSize'], StrCast(doc?._text_fontSize, StrCast(Doc.UserDoc().fontSize)));
+ case StyleProp.FontFamily: return StrCast(doc?.[fieldKey + 'fontFamily'], StrCast(doc?._text_fontFamily, StrCast(Doc.UserDoc().fontFamily)));
+ case StyleProp.FontWeight: return StrCast(doc?.[fieldKey + 'fontWeight'], StrCast(doc?._text_fontWeight, StrCast(Doc.UserDoc().fontWeight)));
+ case StyleProp.FillColor: return StrCast(doc?._fillColor, StrCast(doc?.fillColor, 'transparent'));
+ case StyleProp.ShowCaption:return doc?._type_collection === CollectionViewType.Carousel || props?.hideCaptions ? undefined : StrCast(doc?._layout_showCaption);
+ case StyleProp.TitleHeight:return 15;
+ case StyleProp.ShowTitle:
return (
(doc &&
!doc.presentationTargetDoc &&
@@ -162,10 +158,10 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
)) ||
''
);
- case StyleProp.FillColor:
- return Cast(doc?._fillColor, 'string', Cast(doc?.fillColor, 'string', 'transparent'));
case StyleProp.Color:
- if (MainView.Instance.LastButton === doc) return Colors.DARK_GRAY;
+ if (MainView.Instance.LastButton === doc) return SettingsManager.Instance.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));
if (docColor) return docColor;
const docView = props?.DocumentView?.();
@@ -173,9 +169,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
if (!backColor) return undefined;
return lightOrDark(backColor);
case StyleProp.BorderRounding:
- return StrCast(doc?.[fieldKey + 'borderRounding'], StrCast(doc?.borderRounding, doc?._viewType === CollectionViewType.Pile ? '50%' : ''));
- case StyleProp.TitleHeight:
- return 15;
+ return StrCast(doc?.[fieldKey + 'borderRounding'], StrCast(doc?.layout_borderRounding, doc?._type_collection === CollectionViewType.Pile ? '50%' : ''));
case StyleProp.BorderPath:
const borderPath = Doc.IsComicStyle(doc) &&
props?.renderDepth &&
@@ -192,12 +186,8 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
</div>
),
};
- case StyleProp.JitterRotation:
- return Doc.IsComicStyle(doc) ? random(-1, 1, NumCast(doc?.x), NumCast(doc?.y)) * ((props?.PanelWidth() || 0) > (props?.PanelHeight() || 0) ? 5 : 10) : 0;
- case StyleProp.showCaption:
- return doc?._viewType === CollectionViewType.Carousel || props?.hideCaptions ? undefined : StrCast(doc?._layout_showCaption);
case StyleProp.HeaderMargin:
- return ([CollectionViewType.Stacking, CollectionViewType.NoteTaking, CollectionViewType.Masonry, CollectionViewType.Tree].includes(doc?._viewType as any) ||
+ return ([CollectionViewType.Stacking, CollectionViewType.NoteTaking, CollectionViewType.Masonry, CollectionViewType.Tree].includes(doc?._type_collection as any) ||
(doc?.type === DocumentType.RTF && !layout_showTitle()?.includes('noMargin')) ||
doc?.type === DocumentType.LABEL) &&
layout_showTitle() &&
@@ -205,108 +195,77 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
? 15
: 0;
case StyleProp.BackgroundColor: {
- if (MainView.Instance.LastButton === doc) return Colors.LIGHT_GRAY;
+ if (MainView.Instance.LastButton === 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) {
- case DocumentType.PRESELEMENT:
- docColor = docColor || (darkScheme() ? '' : '');
- break;
- case DocumentType.PRES:
- docColor = docColor || (darkScheme() ? 'transparent' : 'transparent');
- break;
- case DocumentType.FONTICON:
- docColor = boxBackground ? undefined : docColor || Colors.DARK_GRAY;
- break;
- case DocumentType.RTF:
- docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : Colors.LIGHT_GRAY);
- break;
- case DocumentType.FILTER:
- docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : 'rgba(105, 105, 105, 0.432)');
- break;
- case DocumentType.INK:
- docColor = doc?.stroke_isInkMask ? 'rgba(0,0,0,0.7)' : undefined;
- break;
- case DocumentType.SLIDER:
- break;
- case DocumentType.EQUATION:
- docColor = docColor || 'transparent';
- break;
- case DocumentType.LABEL:
- docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : Colors.LIGHT_GRAY);
- break;
- case DocumentType.BUTTON:
- docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : Colors.LIGHT_GRAY);
- break;
- case DocumentType.LINKANCHOR:
- docColor = isAnchor ? Colors.LIGHT_BLUE : 'transparent';
- break;
- case DocumentType.LINK:
- docColor = (isAnchor ? docColor : '') || 'transparent';
- break;
+ case DocumentType.SLIDER: break;
+ case DocumentType.PRESELEMENT: docColor = docColor || (darkScheme() ? '' : ''); break;
+ case DocumentType.PRES: docColor = docColor || (darkScheme() ? 'transparent' : 'transparent'); break;
+ case DocumentType.FONTICON: docColor = boxBackground ? undefined : docColor || Colors.DARK_GRAY; break;
+ case DocumentType.RTF: docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : Colors.LIGHT_GRAY); break;
+ case DocumentType.INK: docColor = doc?.stroke_isInkMask ? 'rgba(0,0,0,0.7)' : undefined; break;
+ case DocumentType.EQUATION: docColor = docColor || 'transparent'; break;
+ case DocumentType.LABEL: docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : Colors.LIGHT_GRAY); break;
+ case DocumentType.BUTTON: docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : Colors.LIGHT_GRAY); break;
+ case DocumentType.LINK: docColor = (isAnchor ? docColor : '') || 'transparent'; break;
case DocumentType.IMG:
case DocumentType.WEB:
case DocumentType.PDF:
case DocumentType.MAP:
case DocumentType.SCREENSHOT:
- case DocumentType.VID:
- docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : Colors.LIGHT_GRAY);
- break;
+ case DocumentType.VID: docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : Colors.LIGHT_GRAY); break;
case DocumentType.COL:
if (StrCast(Doc.LayoutField(doc)).includes(SliderBox.name)) break;
- docColor =
- docColor ||
- (Doc.IsSystem(doc)
- ? darkScheme()
- ? Colors.DARK_GRAY
- : Colors.LIGHT_GRAY // system docs (seen in treeView) get a grayish background
+ docColor = docColor || (Doc.IsSystem(doc)
+ ? SettingsManager.Instance.userBackgroundColor
: doc.annotationOn
? '#00000010' // faint interior for collections on PDFs, images, etc
: doc?._isGroup
? undefined
- : Cast((props?.renderDepth || 0) > 0 ? Doc.UserDoc().activeCollectionNestedBackground : Doc.UserDoc().activeCollectionBackground, 'string') ?? (darkScheme() ? Colors.BLACK : 'linear-gradient(#065fff, #85c1f9)'));
- break;
- //if (doc._viewType !== CollectionViewType.Freeform && doc._viewType !== CollectionViewType.Time) return "rgb(62,62,62)";
- default:
- docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : Colors.WHITE);
+ : doc._type_collection === CollectionViewType.Stacking ?
+ (darkScheme() ? Colors.MEDIUM_GRAY : Colors.DARK_GRAY)
+ : Cast((props?.renderDepth || 0) > 0 ? Doc.UserDoc().activeCollectionNestedBackground : Doc.UserDoc().activeCollectionBackground, 'string') ?? (darkScheme() ? Colors.BLACK : Colors.MEDIUM_GRAY));
break;
+ //if (doc._type_collection !== CollectionViewType.Freeform && doc._type_collection !== CollectionViewType.Time) return "rgb(62,62,62)";
+ default: docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : Colors.WHITE);
}
- if (docColor && !doc) docColor = DashColor(docColor).fade(0.5).toString();
- return docColor;
+ return (docColor && !doc) ? DashColor(docColor).fade(0.5).toString() : docColor;
}
case StyleProp.BoxShadow: {
if (!doc || opacity() === 0 || doc.noShadow) return undefined; // if it's not visible, then no shadow)
- if (doc.boxShadow === 'standard') return Shadows.STANDARD_SHADOW;
+ if (doc.layout_boxShadow === 'standard') return Shadows.STANDARD_SHADOW;
if (IsFollowLinkScript(doc?.onClick) && LinkManager.Links(doc).length && ![DocumentType.LINK, DocumentType.INK].includes(doc.type as any)) return StrCast(doc?._linkButtonShadow, 'lightblue 0em 0em 1em');
switch (doc?.type) {
case DocumentType.COL:
return StrCast(
- doc?.boxShadow,
- doc?._viewType === CollectionViewType.Pile
+ doc?.layout_borderRounding,
+ doc?._type_collection === CollectionViewType.Pile
? '4px 4px 10px 2px'
: lockedPosition() || doc?._isGroup || docProps?.LayoutTemplateString
? undefined // groups have no drop shadow -- they're supposed to be "invisible". LayoutString's imply collection is being rendered as something else (e.g., title of a Slide)
- : `${darkScheme() ? Colors.DARK_GRAY : Colors.MEDIUM_GRAY} ${StrCast(doc.boxShadow, '0.2vw 0.2vw 0.8vw')}`
+ : `${darkScheme() ? Colors.DARK_GRAY : Colors.MEDIUM_GRAY} ${StrCast(doc.layout_borderRounding, '0.2vw 0.2vw 0.8vw')}`
);
case DocumentType.LABEL:
if (doc?.annotationOn !== undefined) return 'black 2px 2px 1px';
default:
return doc.z
- ? `#9c9396 ${StrCast(doc?.boxShadow, '10px 10px 0.9vw')}` // if it's a floating doc, give it a big shadow
+ ? `#9c9396 ${StrCast(doc?.layout_boxShadow, '10px 10px 0.9vw')}` // if it's a floating doc, give it a big shadow
: props?.docViewPath().lastElement()?.rootDoc._freeform_useClusters
- ? `${backgroundCol()} ${StrCast(doc.boxShadow, `0vw 0vw ${(lockedPosition() ? 100 : 50) / (docProps?.NativeDimScaling?.() || 1)}px`)}` // if it's just in a cluster, make the shadown roughly match the cluster border extent
+ ? `${backgroundCol()} ${StrCast(doc.layout_boxShadow, `0vw 0vw ${(lockedPosition() ? 100 : 50) / (docProps?.NativeDimScaling?.() || 1)}px`)}` // if it's just in a cluster, make the shadown roughly match the cluster border extent
: NumCast(doc.group, -1) !== -1
- ? `gray ${StrCast(doc.boxShadow, `0vw 0vw ${(lockedPosition() ? 100 : 50) / (docProps?.NativeDimScaling?.() || 1)}px`)}` // if it's just in a cluster, make the shadown roughly match the cluster border extent
+ ? `gray ${StrCast(doc.layout_boxShadow, `0vw 0vw ${(lockedPosition() ? 100 : 50) / (docProps?.NativeDimScaling?.() || 1)}px`)}` // if it's just in a cluster, make the shadown roughly match the cluster border extent
: lockedPosition()
? undefined // if it's a background & has a cluster color, make the shadow spread really big
- : StrCast(doc.boxShadow, '');
+ : StrCast(doc.layout_boxShadow, '');
}
}
case StyleProp.PointerEvents:
const isInk = doc && StrCast(Doc.Layout(doc).layout).includes(InkingStroke.name) && !props?.LayoutTemplateString;
+ if (StrCast(doc?.pointerEvents) && !props?.LayoutTemplateString?.includes(KeyValueBox.name)) return StrCast(doc!.pointerEvents); // honor pointerEvents field (set by lock button usually) if it's not a keyValue view of the Doc
if (docProps?.DocumentView?.().ComponentView?.overridePointerEvents?.() !== undefined) return docProps?.DocumentView?.().ComponentView?.overridePointerEvents?.();
- if (MainView.Instance._exploreMode || doc?.unrendered) return isInk ? 'visiblePainted' : 'all';
- if (doc?.pointerEvents) return StrCast(doc.pointerEvents);
+ if (MainView.Instance._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';
@@ -314,19 +273,19 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
return undefined; // fixes problem with tree view elements getting pointer events when the tree view is not active
case StyleProp.Decorations:
const lock = () => {
- if (props?.docViewPath().lastElement()?.rootDoc?._viewType === CollectionViewType.Freeform) {
+ if (props?.docViewPath().lastElement()?.rootDoc?._type_collection === CollectionViewType.Freeform) {
return doc?.pointerEvents !== 'none' ? null : (
<div className="styleProvider-lock" onClick={() => toggleLockedPosition(doc)}>
- <FontAwesomeIcon icon={'lock'} style={{ color: 'red' }} size="lg" />
+ <FontAwesomeIcon icon='lock' style={{ color: 'red' }} size="lg" />
</div>
);
}
};
const filter = () => {
const showFilterIcon =
- StrListCast(doc?._docFilters).length || StrListCast(doc?._docRangeFilters).length
+ StrListCast(doc?._childFilters).length || StrListCast(doc?._childFiltersByRanges).length
? '#18c718bd' //'hasFilter'
- : docProps?.docFilters?.().filter(f => Utils.IsRecursiveFilter(f) && f !== Utils.noDragsDocFilter).length || docProps?.docRangeFilters().length
+ : docProps?.childFilters?.().filter(f => Utils.IsRecursiveFilter(f) && f !== Utils.noDragsDocFilter).length || docProps?.childFiltersByRanges().length
? 'orange' //'inheritsFilter'
: undefined;
return !showFilterIcon ? null : (
@@ -359,18 +318,19 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
}
export function DashboardToggleButton(doc: Doc, field: string, onIcon: IconProp, offIcon: IconProp, clickFunc?: () => void) {
+ const color = StrCast(Doc.UserDoc().userColor);
return (
- <div
- title={field}
- className={`styleProvider-treeView-icon${doc[field] ? '-active' : ''}`}
+ <IconButton
+ size={Size.XSMALL}
+ color={color}
+ icon={<FontAwesomeIcon icon={(doc[field] ? (onIcon as any) : offIcon) as IconProp} />}
onClick={undoBatch(
action((e: React.MouseEvent) => {
e.stopPropagation();
clickFunc ? clickFunc() : (doc[field] = doc[field] ? undefined : true);
})
- )}>
- <FontAwesomeIcon icon={(doc[field] ? (onIcon as any) : offIcon) as IconProp} size="sm" />
- </div>
+ )}
+ />
);
}
/**
@@ -378,13 +338,9 @@ export function DashboardToggleButton(doc: Doc, field: string, onIcon: IconProp,
*/
export function DashboardStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps | DocumentViewProps>, property: string) {
if (doc && property.split(':')[0] === StyleProp.Decorations) {
- return doc._viewType === CollectionViewType.Docking ? null : (
- <>
- {DashboardToggleButton(doc, 'hidden', 'eye-slash', 'eye', () => {
- DocFocusOrOpen(doc, { toggleTarget: true, willZoomCentered: true, zoomScale: 0 }, DocCast(doc?.embedContainer ?? doc?.annotationOn));
- })}
- </>
- );
+ return doc._type_collection === CollectionViewType.Docking || Doc.IsSystem(doc)
+ ? null
+ : DashboardToggleButton(doc, 'hidden', 'eye-slash', 'eye', () => DocFocusOrOpen(doc, { toggleTarget: true, willZoomCentered: true, zoomScale: 0 }, DocCast(doc?.embedContainer ?? doc?.annotationOn)));
}
return DefaultStyleProvider(doc, props, property);
}
diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx
index 814a95ab8..5c2ab3f70 100644
--- a/src/client/views/TemplateMenu.tsx
+++ b/src/client/views/TemplateMenu.tsx
@@ -97,8 +97,8 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
TraceMobx();
const firstDoc = this.props.docViews[0].props.Document;
const templateName = StrCast(firstDoc.layout_fieldKey, 'layout').replace('layout_', '');
- const noteTypes = DocListCast(Cast(Doc.UserDoc()['template-notes'], Doc, null)?.data);
- const addedTypes = DocListCast(Cast(Doc.UserDoc()['template-clickFuncs'], Doc, null)?.data);
+ const noteTypes = DocListCast(Cast(Doc.UserDoc()['template_notes'], Doc, null)?.data);
+ const addedTypes = DocListCast(Cast(Doc.UserDoc()['template_clickFuncs'], Doc, null)?.data);
const templateMenu: Array<JSX.Element> = [];
this.props.templates?.forEach((checked, template) => templateMenu.push(<TemplateToggle key={template} template={template} checked={checked} toggle={this.toggleTemplate} />));
templateMenu.push(<OtherToggle key={'default'} name={'Default'} checked={templateName === 'layout'} toggle={this.toggleDefault} />);
@@ -116,13 +116,12 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
styleProvider={DefaultStyleProvider}
setHeight={returnFalse}
docViewPath={returnEmptyDoclist}
- docFilters={returnEmptyFilter}
- docRangeFilters={returnEmptyFilter}
+ childFilters={returnEmptyFilter}
+ childFiltersByRanges={returnEmptyFilter}
searchFilterDocs={returnEmptyDoclist}
rootSelected={returnFalse}
onCheckedClick={this.scriptField}
onChildClick={this.scriptField}
- dropAction={undefined}
isAnyChildContentActive={returnFalse}
isContentActive={returnTrue}
bringToFront={emptyFunction}
diff --git a/src/client/views/UndoStack.scss b/src/client/views/UndoStack.scss
index ab21e6d7e..192b99a12 100644
--- a/src/client/views/UndoStack.scss
+++ b/src/client/views/UndoStack.scss
@@ -2,9 +2,10 @@
height: 100%;
display: flex;
flex-direction: column;
+ justify-content: center;
+ align-items: center;
position: relative;
pointer-events: all;
- padding-left: 4px;
}
.undoStack-resultContainer {
@@ -21,9 +22,11 @@
}
.undoStack-commandsContainer {
- background-color: whitesmoke;
flex: 1 1 auto;
overflow-y: scroll;
- height: 30px;
+ height: fit-content;
+ width: 200px;
border-radius: 5px;
+ padding: 5px;
+ max-height: 200px;
}
diff --git a/src/client/views/UndoStack.tsx b/src/client/views/UndoStack.tsx
index f5af09e5b..a551e5332 100644
--- a/src/client/views/UndoStack.tsx
+++ b/src/client/views/UndoStack.tsx
@@ -3,6 +3,11 @@ import { observer } from 'mobx-react';
import * as React from 'react';
import { UndoManager } from '../util/UndoManager';
import './UndoStack.scss';
+import { StrCast } from '../../fields/Types';
+import { Doc } from '../../fields/Doc';
+import { Popup, Type, isDark } from 'browndash-components';
+import { Colors } from './global/globalEnums';
+import { SettingsManager } from '../util/SettingsManager';
interface UndoStackProps {
width?: number;
@@ -14,33 +19,39 @@ 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;
return this.props.inline && UndoStack.HideInline ? null : (
- <div
- className="undoStack-outerContainer"
- style={{ width: this.props.width, height: this.props.height ? (UndoStack.Expand ? 4 : 1) * this.props.height : undefined, top: UndoStack.Expand && this.props.height ? -this.props.height * 3 : undefined }}
- onClick={action(e => (UndoStack.Expand = !UndoStack.Expand))}
- onDoubleClick={action(e => (UndoStack.Expand = UndoStack.HideInline = false))}>
- <div className="undoStack-commandsContainer" ref={r => r?.scroll({ behavior: 'auto', top: r?.scrollHeight + 20 })} style={{ background: UndoManager.batchCounter.get() ? 'yellow' : undefined }}>
- <div className="undoStack-resultContainer" key={0}>
- <div className="undoStack-commandString" style={{ fontWeight: 'bold', textAlign: 'center' }}>
- Undo/Redo Stack
- </div>
- </div>
- {UndoManager.undoStackNames.map((name, i) => (
- <div className="undoStack-resultContainer" key={i}>
- <div className="undoStack-commandString">{name.replace(/[^\.]*\./, '')}</div>
- </div>
- ))}
- {Array.from(UndoManager.redoStackNames)
- .reverse()
- .map((name, i) => (
- <div className="undoStack-resultContainer" key={i}>
- <div className="undoStack-commandString" style={{ fontWeight: 'bold', color: 'red' }}>
- {name.replace(/[^\.]*\./, '')}
+ <div className="undoStack-outerContainer">
+ <Popup
+ text={'Undo/Redo Stack'}
+ color={UndoManager.batchCounter.get() ? 'yellow' : StrCast(Doc.UserDoc().userVariantColor)}
+ placement={`top-start`}
+ type={Type.TERT}
+ popup={
+ <div
+ 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,
+ }}>
+ {UndoManager.undoStackNames.map((name, i) => (
+ <div className="undoStack-resultContainer" key={i}>
+ <div className="undoStack-commandString">{name.replace(/[^\.]*\./, '')}</div>
</div>
- </div>
- ))}
- </div>
+ ))}
+ {Array.from(UndoManager.redoStackNames)
+ .reverse()
+ .map((name, i) => (
+ <div className="undoStack-resultContainer" key={i}>
+ <div className="undoStack-commandString" style={{ fontWeight: 'bold', color: 'red' }}>
+ {name.replace(/[^\.]*\./, '')}
+ </div>
+ </div>
+ ))}
+ </div>
+ }
+ />
</div>
);
}
diff --git a/src/client/views/animationtimeline/Keyframe.tsx b/src/client/views/animationtimeline/Keyframe.tsx
index 3465a5283..addc00c85 100644
--- a/src/client/views/animationtimeline/Keyframe.tsx
+++ b/src/client/views/animationtimeline/Keyframe.tsx
@@ -123,7 +123,7 @@ export type RegionData = makeInterface<[typeof RegionDataSchema]>;
export const RegionData = makeInterface(RegionDataSchema);
interface IProps {
- node: Doc;
+ animatedDoc: Doc;
RegionData: Doc;
collection: Doc;
tickSpacing: number;
@@ -167,7 +167,7 @@ export class Keyframe extends React.Component<IProps> {
return RegionData(this.props.RegionData);
}
@computed private get regions() {
- return DocListCast(this.props.node.regions);
+ return DocListCast(this.props.animatedDoc.regions);
}
@computed private get keyframes() {
return DocListCast(this.regiondata.keyframes);
@@ -375,7 +375,7 @@ export class Keyframe extends React.Component<IProps> {
*/
@action
makeRegionMenu = (kf: Doc, e: MouseEvent) => {
- TimelineMenu.Instance.addItem('button', 'Remove Region', () => Cast(this.props.node.regions, listSpec(Doc))?.splice(this.regions.indexOf(this.props.RegionData), 1)),
+ TimelineMenu.Instance.addItem('button', 'Remove Region', () => Cast(this.props.animatedDoc.regions, listSpec(Doc))?.splice(this.regions.indexOf(this.props.RegionData), 1)),
TimelineMenu.Instance.addItem('input', `fadeIn: ${this.regiondata.fadeIn}ms`, val => {
runInAction(() => {
let cannotMove: boolean = false;
@@ -461,7 +461,7 @@ export class Keyframe extends React.Component<IProps> {
e.stopPropagation();
const div = ref.current!;
div.style.opacity = '1';
- Doc.BrushDoc(this.props.node);
+ Doc.BrushDoc(this.props.animatedDoc);
};
/**
@@ -473,7 +473,7 @@ export class Keyframe extends React.Component<IProps> {
e.stopPropagation();
const div = ref.current!;
div.style.opacity = '0';
- Doc.UnBrushDoc(this.props.node);
+ Doc.UnBrushDoc(this.props.animatedDoc);
};
///////////////////////UI STUFF /////////////////////////
diff --git a/src/client/views/animationtimeline/Timeline.tsx b/src/client/views/animationtimeline/Timeline.tsx
index adc97bbb4..7ca13756a 100644
--- a/src/client/views/animationtimeline/Timeline.tsx
+++ b/src/client/views/animationtimeline/Timeline.tsx
@@ -535,7 +535,7 @@ export class Timeline extends React.Component<FieldViewProps> {
{this.children.map(doc => (
<Track
ref={ref => this.mapOfTracks.push(ref)}
- node={doc}
+ animatedDoc={doc}
currentBarX={this._currentBarX}
changeCurrentBarX={this.changeCurrentBarX}
transform={this.props.ScreenToLocalTransform()}
diff --git a/src/client/views/animationtimeline/Track.tsx b/src/client/views/animationtimeline/Track.tsx
index 2349ba786..1010332f5 100644
--- a/src/client/views/animationtimeline/Track.tsx
+++ b/src/client/views/animationtimeline/Track.tsx
@@ -12,7 +12,7 @@ import { Keyframe, KeyframeFunc, RegionData } from './Keyframe';
import './Track.scss';
interface IProps {
- node: Doc;
+ animatedDoc: Doc;
currentBarX: number;
transform: Transform;
collection: Doc;
@@ -36,23 +36,23 @@ export class Track extends React.Component<IProps> {
private objectWhitelist = ['data'];
@computed private get regions() {
- return DocListCast(this.props.node.regions);
+ return DocListCast(this.props.animatedDoc.regions);
}
@computed private get time() {
return NumCast(KeyframeFunc.convertPixelTime(this.props.currentBarX, 'mili', 'time', this.props.tickSpacing, this.props.tickIncrement));
}
async componentDidMount() {
- const regions = await DocListCastAsync(this.props.node.regions);
- if (!regions) this.props.node.regions = new List<Doc>(); //if there is no region, then create new doc to store stuff
+ const regions = await DocListCastAsync(this.props.animatedDoc.regions);
+ if (!regions) this.props.animatedDoc.regions = new List<Doc>(); //if there is no region, then create new doc to store stuff
//these two lines are exactly same from timeline.tsx
const relativeHeight = window.innerHeight / 20;
runInAction(() => (this._trackHeight = relativeHeight < this.MAX_TITLE_HEIGHT ? relativeHeight : this.MAX_TITLE_HEIGHT)); //for responsiveness
this._timelineVisibleReaction = this.timelineVisibleReaction();
this._currentBarXReaction = this.currentBarXReaction();
- if (DocListCast(this.props.node.regions).length === 0) this.createRegion(this.time);
- this.props.node.hidden = false;
- this.props.node.opacity = 1;
+ if (DocListCast(this.props.animatedDoc.regions).length === 0) this.createRegion(this.time);
+ this.props.animatedDoc.hidden = false;
+ this.props.animatedDoc.opacity = 1;
// this.autoCreateKeyframe();
}
@@ -127,13 +127,13 @@ export class Track extends React.Component<IProps> {
*/
@action
autoCreateKeyframe = () => {
- const objects = this.objectWhitelist.map(key => this.props.node[key]);
- intercept(this.props.node, change => {
+ const objects = this.objectWhitelist.map(key => this.props.animatedDoc[key]);
+ intercept(this.props.animatedDoc, change => {
return change;
});
return reaction(
() => {
- return [...this.primitiveWhitelist.map(key => this.props.node[key]), ...objects];
+ return [...this.primitiveWhitelist.map(key => this.props.animatedDoc[key]), ...objects];
},
(changed, reaction) => {
//check for region
@@ -171,14 +171,14 @@ export class Track extends React.Component<IProps> {
() => {
const regiondata = this.findRegion(this.time);
if (regiondata) {
- this.props.node.hidden = false;
+ this.props.animatedDoc.hidden = false;
// if (!this._autoKfReaction) {
// // this._autoKfReaction = this.autoCreateKeyframe();
// }
this.timeChange();
} else {
- this.props.node.hidden = true;
- this.props.node.opacity = 0;
+ this.props.animatedDoc.hidden = true;
+ this.props.animatedDoc.opacity = 0;
//if (this._autoKfReaction) this._autoKfReaction();
}
}
@@ -250,10 +250,10 @@ export class Track extends React.Component<IProps> {
private applyKeys = async (kf: Doc) => {
this.primitiveWhitelist.forEach(key => {
if (!kf[key]) {
- this.props.node[key] = undefined;
+ this.props.animatedDoc[key] = undefined;
} else {
const stored = kf[key];
- this.props.node[key] = stored instanceof ObjectField ? stored[Copy]() : stored;
+ this.props.animatedDoc[key] = stored instanceof ObjectField ? stored[Copy]() : stored;
}
});
};
@@ -282,11 +282,11 @@ export class Track extends React.Component<IProps> {
const dif = NumCast(right[key]) - NumCast(left[key]);
const deltaLeft = this.time - NumCast(left.time);
const ratio = deltaLeft / (NumCast(right.time) - NumCast(left.time));
- this.props.node[key] = NumCast(left[key]) + dif * ratio;
+ this.props.animatedDoc[key] = NumCast(left[key]) + dif * ratio;
} else {
// case data
const stored = left[key];
- this.props.node[key] = stored instanceof ObjectField ? stored[Copy]() : stored;
+ this.props.animatedDoc[key] = stored instanceof ObjectField ? stored[Copy]() : stored;
}
});
};
@@ -326,7 +326,7 @@ export class Track extends React.Component<IProps> {
regiondata.duration = rightRegion.position - regiondata.position;
}
if (this.regions.length === 0 || !rightRegion || (rightRegion && rightRegion.position - regiondata.position >= NumCast(regiondata.fadeIn) + NumCast(regiondata.fadeOut))) {
- Cast(this.props.node.regions, listSpec(Doc))?.push(regiondata);
+ Cast(this.props.animatedDoc.regions, listSpec(Doc))?.push(regiondata);
this._newKeyframe = true;
this.saveStateRegion = regiondata;
return regiondata;
@@ -360,7 +360,7 @@ export class Track extends React.Component<IProps> {
@action
copyDocDataToKeyFrame = (doc: Doc) => {
this.primitiveWhitelist.map(key => {
- const originalVal = this.props.node[key];
+ const originalVal = this.props.animatedDoc[key];
doc[key] = originalVal instanceof ObjectField ? originalVal[Copy]() : originalVal;
});
};
@@ -377,8 +377,8 @@ export class Track extends React.Component<IProps> {
ref={this._inner}
style={{ height: `${this._trackHeight}px` }}
onDoubleClick={this.onInnerDoubleClick}
- onPointerOver={() => Doc.BrushDoc(this.props.node)}
- onPointerOut={() => Doc.UnBrushDoc(this.props.node)}>
+ onPointerOver={() => Doc.BrushDoc(this.props.animatedDoc)}
+ onPointerOut={() => Doc.UnBrushDoc(this.props.animatedDoc)}>
{this.regions?.map((region, i) => {
return <Keyframe key={`${i}`} {...this.props} RegionData={region} makeKeyData={this.makeKeyData} />;
})}
diff --git a/src/client/views/collections/CollectionCarousel3DView.scss b/src/client/views/collections/CollectionCarousel3DView.scss
index 5c8b491eb..6bd1d9f5f 100644
--- a/src/client/views/collections/CollectionCarousel3DView.scss
+++ b/src/client/views/collections/CollectionCarousel3DView.scss
@@ -1,3 +1,4 @@
+@import '../global/globalCssVariables';
.collectionCarousel3DView-outer {
height: 100%;
position: relative;
@@ -7,8 +8,8 @@
.carousel-wrapper {
display: flex;
position: absolute;
- top: 15%;
- height: 60%;
+ top: $CAROUSEL3D_TOP * 1%;
+ height: ($CAROUSEL3D_SIDE_SCALE * 100) * 1%;
align-items: center;
transition: transform 0.3s cubic-bezier(0.455, 0.03, 0.515, 0.955);
@@ -17,10 +18,17 @@
flex: 1;
transition: opacity 0.3s linear, transform 0.5s cubic-bezier(0.455, 0.03, 0.515, 0.955);
pointer-events: none;
+ opacity: 0.5;
+ z-index: 1;
+ transform: scale($CAROUSEL3D_SIDE_SCALE);
+ user-select: none;
}
.collectionCarousel3DView-item-active {
pointer-events: unset;
+ opacity: 1;
+ z-index: 2;
+ transform: scale($CAROUSEL3D_CENTER_SCALE);
}
}
@@ -105,4 +113,4 @@
.carousel3DView-back-scroll:hover,
.carousel3DView-fwd-scroll:hover {
background: lightgray;
-} \ No newline at end of file
+}
diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx
index a266c9207..d94e552b4 100644
--- a/src/client/views/collections/CollectionCarousel3DView.tsx
+++ b/src/client/views/collections/CollectionCarousel3DView.tsx
@@ -7,6 +7,8 @@ import { Id } from '../../../fields/FieldSymbols';
import { NumCast, ScriptCast, StrCast } from '../../../fields/Types';
import { returnFalse, returnZero, Utils } from '../../../Utils';
import { DragManager } from '../../util/DragManager';
+import { SelectionManager } from '../../util/SelectionManager';
+import { CAROUSEL3D_CENTER_SCALE, CAROUSEL3D_SIDE_SCALE, CAROUSEL3D_TOP } from '../global/globalCssVariables.scss';
import { DocumentView } from '../nodes/DocumentView';
import { StyleProp } from '../StyleProvider';
import './CollectionCarousel3DView.scss';
@@ -32,27 +34,34 @@ export class CollectionCarousel3DView extends CollectionSubView() {
}
};
+ centerScale = Number(CAROUSEL3D_CENTER_SCALE);
panelWidth = () => this.props.PanelWidth() / 3;
- panelHeight = () => this.props.PanelHeight() * 0.6;
+ panelHeight = () => this.props.PanelHeight() * Number(CAROUSEL3D_SIDE_SCALE);
onChildDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick);
isContentActive = () => this.props.isSelected() || this.props.isContentActive() || this.props.isAnyChildContentActive();
isChildContentActive = () => (this.isContentActive() ? true : false);
+ childScreenToLocal = () =>
+ this.props // document's left is the panel shifted by the doc's index * panelWidth/#docs. But it scales by centerScale around its center, so it's left moves left by the distance of the left from the center (panelwidth/2) * the scale delta (centerScale-1)
+ .ScreenToLocalTransform() // the top behaves the same way ecept it's shifted by the 'top' amount specified for the panel in css and then by the scale factor.
+ .translate(-this.panelWidth() + ((this.centerScale - 1) * this.panelWidth()) / 2, -((Number(CAROUSEL3D_TOP) / 100) * this.props.PanelHeight()) + ((this.centerScale - 1) * this.panelHeight()) / 2)
+ .scale(1 / this.centerScale);
@computed get content() {
- const currentIndex = NumCast(this.layoutDoc._itemIndex);
+ const currentIndex = NumCast(this.layoutDoc._carousel_index);
const displayDoc = (childPair: { layout: Doc; data: Doc }) => {
return (
<DocumentView
{...this.props}
NativeWidth={returnZero}
NativeHeight={returnZero}
- suppressSetHeight={true}
+ //suppressSetHeight={true}
onDoubleClick={this.onChildDoubleClick}
renderDepth={this.props.renderDepth + 1}
LayoutTemplate={this.props.childLayoutTemplate}
LayoutTemplateString={this.props.childLayoutString}
Document={childPair.layout}
DataDoc={childPair.data}
+ ScreenToLocalTransform={this.childScreenToLocal}
isContentActive={this.isChildContentActive}
isDocumentActive={this.props.childDocumentsActive?.() || this.Document._childDocumentsActive ? this.props.isDocumentActive : this.isContentActive}
PanelWidth={this.panelWidth}
@@ -64,10 +73,7 @@ export class CollectionCarousel3DView extends CollectionSubView() {
return this.childLayoutPairs.map((childPair, index) => {
return (
- <div
- key={childPair.layout[Id]}
- className={`collectionCarousel3DView-item${index === currentIndex ? '-active' : ''} ${index}`}
- style={index === currentIndex ? { opacity: '1', transform: 'scale(1.3)', width: this.panelWidth() } : { opacity: '0.5', transform: 'scale(0.6)', userSelect: 'none', width: this.panelWidth() }}>
+ <div key={childPair.layout[Id]} className={`collectionCarousel3DView-item${index === currentIndex ? '-active' : ''} ${index}`} style={{ width: this.panelWidth() }}>
{displayDoc(childPair)}
</div>
);
@@ -75,7 +81,8 @@ export class CollectionCarousel3DView extends CollectionSubView() {
}
changeSlide = (direction: number) => {
- this.layoutDoc._itemIndex = (NumCast(this.layoutDoc._itemIndex) + direction + this.childLayoutPairs.length) % this.childLayoutPairs.length;
+ SelectionManager.DeselectAll();
+ this.layoutDoc._carousel_index = (NumCast(this.layoutDoc._carousel_index) + direction + this.childLayoutPairs.length) % this.childLayoutPairs.length;
};
onArrowClick = (e: React.MouseEvent, direction: number) => {
@@ -114,10 +121,10 @@ export class CollectionCarousel3DView extends CollectionSubView() {
return (
<div className="arrow-buttons">
<div key="back" className="carousel3DView-back" style={{ background: `${StrCast(this.props.Document.backgroundColor)}` }} onClick={e => this.onArrowClick(e, -1)}>
- <FontAwesomeIcon icon={'angle-left'} size={'2x'} />
+ <FontAwesomeIcon icon="angle-left" size={'2x'} />
</div>
<div key="fwd" className="carousel3DView-fwd" style={{ background: `${StrCast(this.props.Document.backgroundColor)}` }} onClick={e => this.onArrowClick(e, 1)}>
- <FontAwesomeIcon icon={'angle-right'} size={'2x'} />
+ <FontAwesomeIcon icon="angle-right" size={'2x'} />
</div>
{this.autoScrollButton}
</div>
@@ -139,13 +146,14 @@ export class CollectionCarousel3DView extends CollectionSubView() {
}
@computed get dots() {
- return this.childLayoutPairs.map((_child, index) => <div key={Utils.GenerateGuid()} className={`dot${index === NumCast(this.layoutDoc._itemIndex) ? '-active' : ''}`} onClick={() => (this.layoutDoc._itemIndex = index)} />);
+ return this.childLayoutPairs.map((_child, index) => <div key={Utils.GenerateGuid()} className={`dot${index === NumCast(this.layoutDoc._carousel_index) ? '-active' : ''}`} onClick={() => (this.layoutDoc._carousel_index = index)} />);
+ }
+ @computed get translateX() {
+ const index = NumCast(this.layoutDoc._carousel_index);
+ return this.panelWidth() * (1 - index);
}
render() {
- const index = NumCast(this.layoutDoc._itemIndex);
- const translateX = this.panelWidth() * (1 - index);
-
return (
<div
className="collectionCarousel3DView-outer"
@@ -154,7 +162,7 @@ export class CollectionCarousel3DView extends CollectionSubView() {
background: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor),
color: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color),
}}>
- <div className="carousel-wrapper" style={{ transform: `translateX(${translateX}px)` }}>
+ <div className="carousel-wrapper" style={{ transform: `translateX(${this.translateX}px)` }}>
{this.content}
</div>
{this.props.Document._chromeHidden ? null : this.buttons}
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index 9ec78155c..ea02bcd4c 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -30,11 +30,11 @@ export class CollectionCarouselView extends CollectionSubView() {
advance = (e: React.MouseEvent) => {
e.stopPropagation();
- this.layoutDoc._itemIndex = (NumCast(this.layoutDoc._itemIndex) + 1) % this.childLayoutPairs.length;
+ this.layoutDoc._carousel_index = (NumCast(this.layoutDoc._carousel_index) + 1) % this.childLayoutPairs.length;
};
goback = (e: React.MouseEvent) => {
e.stopPropagation();
- this.layoutDoc._itemIndex = (NumCast(this.layoutDoc._itemIndex) - 1 + this.childLayoutPairs.length) % this.childLayoutPairs.length;
+ this.layoutDoc._carousel_index = (NumCast(this.layoutDoc._carousel_index) - 1 + this.childLayoutPairs.length) % this.childLayoutPairs.length;
};
captionStyleProvider = (doc: Doc | undefined, captionProps: Opt<DocumentViewProps>, property: string): any => {
// first look for properties on the document in the carousel, then fallback to properties on the container
@@ -45,7 +45,7 @@ export class CollectionCarouselView extends CollectionSubView() {
onContentDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick);
onContentClick = () => ScriptCast(this.layoutDoc.onChildClick);
@computed get content() {
- const index = NumCast(this.layoutDoc._itemIndex);
+ const index = NumCast(this.layoutDoc._carousel_index);
const curDoc = this.childLayoutPairs?.[index];
const captionProps = { ...this.props, fieldKey: 'caption', setHeight: undefined };
const marginX = NumCast(this.layoutDoc['caption_xMargin']);
@@ -60,6 +60,8 @@ export class CollectionCarouselView extends CollectionSubView() {
NativeHeight={returnZero}
onDoubleClick={this.onContentDoubleClick}
onClick={this.onContentClick}
+ isDocumentActive={this.props.childDocumentsActive?.() ? this.props.isDocumentActive : this.props.isContentActive}
+ isContentActive={this.props.childContentsActive ?? this.props.isContentActive() === false ? returnFalse : emptyFunction}
hideCaptions={show_captions ? true : false}
renderDepth={this.props.renderDepth + 1}
LayoutTemplate={this.props.childLayoutTemplate}
diff --git a/src/client/views/collections/CollectionDockingView.scss b/src/client/views/collections/CollectionDockingView.scss
index 78e44dfa2..d93015506 100644
--- a/src/client/views/collections/CollectionDockingView.scss
+++ b/src/client/views/collections/CollectionDockingView.scss
@@ -15,6 +15,12 @@
cursor: grab;
color: $black;
}
+.collectiondockingview-container .lm_splitter {
+ opacity: 0.2;
+ &:hover {
+ opacity: 1;
+ }
+}
.lm_title.focus-visible {
-webkit-appearance: none;
@@ -89,6 +95,13 @@
position: relative;
}
+.lm_maximised .lm_header {
+ background-color: #000000;
+}
+.lm_maximised .lm_tab {
+ width: 100%;
+}
+
.lm_stack {
position: relative;
}
@@ -163,6 +176,7 @@
display: flex;
align-content: center;
justify-content: center;
+ background: $dark-gray;
}
.lm_controls > li {
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 3a4691484..0052c4196 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -6,9 +6,9 @@ import { Doc, DocListCast, Opt } from '../../../fields/Doc';
import { Id } from '../../../fields/FieldSymbols';
import { InkTool } from '../../../fields/InkField';
import { List } from '../../../fields/List';
-import { Cast, ImageCast, NumCast, StrCast } from '../../../fields/Types';
+import { ImageCast, NumCast, StrCast } from '../../../fields/Types';
import { ImageField } from '../../../fields/URLField';
-import { inheritParentAcls } from '../../../fields/util';
+import { GetEffectiveAcl, inheritParentAcls } from '../../../fields/util';
import { emptyFunction, incrementTitleCopy } from '../../../Utils';
import { DocServer } from '../../DocServer';
import { Docs } from '../../documents/Documents';
@@ -20,14 +20,17 @@ import { SelectionManager } from '../../util/SelectionManager';
import { undoBatch, UndoManager } from '../../util/UndoManager';
import { DashboardView } from '../DashboardView';
import { LightboxView } from '../LightboxView';
+import { OpenWhere, OpenWhereMod } from '../nodes/DocumentView';
+import { OverlayView } from '../OverlayView';
+import { ScriptingRepl } from '../ScriptingRepl';
+import { UndoStack } from '../UndoStack';
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');
-import { OpenWhere, OpenWhereMod } from '../nodes/DocumentView';
-import { OverlayView } from '../OverlayView';
-import { ScriptingRepl } from '../ScriptingRepl';
const _global = (window /* browser */ || global) /* node */ as any;
@observer
@@ -85,6 +88,7 @@ export class CollectionDockingView extends CollectionSubView() {
tabItemDropped = () => DragManager.CompleteWindowDrag?.(false);
tabDragStart = (proxy: any, finishDrag?: (aborted: boolean) => void) => {
+ this._flush = this._flush ?? UndoManager.StartBatch('tab move');
const dashDoc = proxy?._contentItem?.tab?.DashDoc as Doc;
dashDoc && (DragManager.DocDragData = new DragManager.DocumentDragData([proxy._contentItem.tab.DashDoc]));
DragManager.CompleteWindowDrag = (aborted: boolean) => {
@@ -92,12 +96,12 @@ export class CollectionDockingView extends CollectionSubView() {
proxy._dragListener.AbortDrag();
if (this._flush) {
this._flush.cancel(); // cancel the undo change being logged
- this._flush = undefined;
this.setupGoldenLayout(); // restore golden layout to where it was before the drag (this is a no-op when using StartOtherDrag because the proxy dragged item was never in the golden layout)
}
DragManager.CompleteWindowDrag = undefined;
}
finishDrag?.(aborted);
+ setTimeout(this.endUndoBatch, 100);
};
};
@undoBatch
@@ -123,28 +127,6 @@ export class CollectionDockingView extends CollectionSubView() {
}
@undoBatch
- public static OpenFullScreen(doc: Doc) {
- SelectionManager.DeselectAll();
- const instance = CollectionDockingView.Instance;
- if (instance) {
- if (doc._viewType === CollectionViewType.Docking && doc.layout_fieldKey === 'layout') {
- return DashboardView.openDashboard(doc);
- }
- const newItemStackConfig = {
- type: 'stack',
- content: [CollectionDockingView.makeDocumentConfig(Doc.MakeEmbedding(doc))],
- };
- const docconfig = instance._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, instance._goldenLayout);
- instance._goldenLayout.root.contentItems[0].addChild(docconfig);
- docconfig.callDownwards('_$init');
- instance._goldenLayout._$maximiseItem(docconfig);
- instance._goldenLayout.emit('stateChanged');
- instance.stateChanged();
- }
- return true;
- }
-
- @undoBatch
@action
public static ReplaceTab(document: Doc, panelName: OpenWhereMod, stack: any, addToSplit?: boolean, keyValue?: boolean): boolean {
const instance = CollectionDockingView.Instance;
@@ -172,20 +154,17 @@ export class CollectionDockingView extends CollectionSubView() {
@undoBatch
public static ToggleSplit(doc: Doc, location: OpenWhereMod, stack?: any, panelName?: string, keyValue?: boolean) {
- return CollectionDockingView.Instance && Array.from(CollectionDockingView.Instance.tabMap.keys()).findIndex(tab => tab.DashDoc === doc) !== -1
- ? CollectionDockingView.CloseSplit(doc)
- : CollectionDockingView.AddSplit(doc, location, stack, panelName, keyValue);
+ return Array.from(CollectionDockingView.Instance?.tabMap.keys() ?? []).findIndex(tab => tab.DashDoc === doc) !== -1 ? CollectionDockingView.CloseSplit(doc) : CollectionDockingView.AddSplit(doc, location, stack, panelName, keyValue);
}
//
// Creates a split on any side of the docking view based on the passed input pullSide and then adds the Document to the requested side
//
- @undoBatch
@action
public static AddSplit(document: Doc, pullSide: OpenWhereMod, stack?: any, panelName?: string, keyValue?: boolean) {
- if (document?._viewType === CollectionViewType.Docking) return DashboardView.openDashboard(document);
+ if (document?._type_collection === CollectionViewType.Docking && !keyValue) return DashboardView.openDashboard(document);
if (!CollectionDockingView.Instance) return false;
- const tab = Array.from(CollectionDockingView.Instance.tabMap).find(tab => tab.DashDoc === document && !keyValue);
+ const tab = Array.from(CollectionDockingView.Instance.tabMap).find(tab => tab.DashDoc === document && !tab.contentItem.config.props.keyValue && !keyValue);
if (tab) {
tab.header.parent.setActiveContentItem(tab.contentItem);
return true;
@@ -195,6 +174,8 @@ export class CollectionDockingView extends CollectionSubView() {
if (!instance) return false;
const docContentConfig = CollectionDockingView.makeDocumentConfig(document, panelName, undefined, keyValue);
+ CollectionDockingView.Instance._flush = CollectionDockingView.Instance._flush ?? UndoManager.StartBatch('Add Split');
+ setTimeout(CollectionDockingView.Instance.endUndoBatch, 100);
if (!pullSide && stack) {
stack.addChild(docContentConfig, undefined);
setTimeout(() => stack.setActiveContentItem(stack.contentItems[stack.contentItems.length - 1]));
@@ -358,6 +339,7 @@ export class CollectionDockingView extends CollectionSubView() {
} catch (e) {}
this._goldenLayout?.destroy();
window.removeEventListener('resize', this.onResize);
+ window.removeEventListener('mouseup', this.onPointerUp);
this._reactionDisposer?.();
this._lightboxReactionDisposer?.();
@@ -370,16 +352,35 @@ export class CollectionDockingView extends CollectionSubView() {
!LightboxView.LightboxDoc && cur && this._goldenLayout?.updateSize(cur.getBoundingClientRect().width, cur.getBoundingClientRect().height);
};
+ endUndoBatch = () => {
+ const json = JSON.stringify(this._goldenLayout.toConfig());
+ const matches = json.match(/\"documentId\":\"[a-z0-9-]+\"/g);
+ const docids = matches?.map(m => m.replace('"documentId":"', '').replace('"', ''));
+ const docs = !docids
+ ? []
+ : docids
+ .map(id => DocServer.GetCachedRefField(id))
+ .filter(f => f)
+ .map(f => f as Doc);
+ const changesMade = this.props.Document.dockingConfig !== json;
+ if (changesMade) {
+ if (![AclAdmin, AclEdit].includes(GetEffectiveAcl(this.dataDoc))) {
+ this.layoutDoc.dockingConfig = json;
+ this.layoutDoc.data = new List<Doc>(docs);
+ } else {
+ Doc.SetInPlace(this.rootDoc, 'dockingConfig', json, true);
+ Doc.SetInPlace(this.rootDoc, 'data', new List<Doc>(docs), true);
+ }
+ }
+ this._flush?.end();
+ this._flush = undefined;
+ };
+
@action
onPointerUp = (e: MouseEvent): void => {
window.removeEventListener('pointerup', this.onPointerUp);
- const flush = this._flush;
- this._flush = undefined;
- if (flush) {
- DragManager.CompleteWindowDrag = undefined;
- if (!this.stateChanged()) flush.cancel();
- else flush.end();
- }
+ DragManager.CompleteWindowDrag = undefined;
+ setTimeout(this.endUndoBatch, 100);
};
@action
@@ -393,9 +394,14 @@ export class CollectionDockingView extends CollectionSubView() {
window.addEventListener('mouseup', this.onPointerUp);
if (!htmlTarget.closest('*.lm_content') && (htmlTarget.closest('*.lm_tab') || htmlTarget.closest('*.lm_stack'))) {
const className = typeof htmlTarget.className === 'string' ? htmlTarget.className : '';
- if (!className.includes('lm_close') && !className.includes('lm_maximise')) {
- this._flush = UndoManager.StartBatch('golden layout edit');
- DocServer.UPDATE_SERVER_CACHE();
+ if (className.includes('lm_maximise')) this._flush = UndoManager.StartBatch('tab maximize');
+ else {
+ const tabTarget = (e.target as HTMLElement)?.parentElement?.className.includes('lm_tab') ? (e.target as HTMLElement).parentElement : (e.target as HTMLElement);
+ const map = Array.from(this.tabMap).find(tab => tab.element[0] === tabTarget);
+ if (map?.DashDoc && DocumentManager.Instance.getFirstDocumentView(map.DashDoc)) {
+ SelectionManager.SelectView(DocumentManager.Instance.getFirstDocumentView(map.DashDoc), false);
+ }
+ if (!className.includes('lm_close')) DocServer.UPDATE_SERVER_CACHE();
}
}
}
@@ -438,7 +444,7 @@ export class CollectionDockingView extends CollectionSubView() {
const newtabdocs = origtabdocs.map(origtabdoc => Doc.MakeEmbedding(origtabdoc));
if (newtabdocs.length) {
Doc.GetProto(newtab).data = new List<Doc>(newtabdocs);
- newtabdocs.forEach(ntab => (ntab.embedContainer = newtab));
+ newtabdocs.forEach(ntab => Doc.SetContainer(ntab, newtab));
}
json = json.replace(origtab[Id], newtab[Id]);
return newtab;
@@ -452,28 +458,18 @@ export class CollectionDockingView extends CollectionSubView() {
stateChanged = () => {
this._ignoreStateChange = JSON.stringify(this._goldenLayout.toConfig());
const json = JSON.stringify(this._goldenLayout.toConfig());
- const matches = json.match(/\"documentId\":\"[a-z0-9-]+\"/g);
- const docids = matches?.map(m => m.replace('"documentId":"', '').replace('"', ''));
- const docs = !docids
- ? []
- : docids
- .map(id => DocServer.GetCachedRefField(id))
- .filter(f => f)
- .map(f => f as Doc);
const changesMade = this.props.Document.dockingConfig !== json;
- if (changesMade && !this._flush) {
- UndoManager.RunInBatch(() => {
- this.props.Document.dockingConfig = json;
- this.props.Document.data = new List<Doc>(docs);
- }, 'state changed');
- }
return changesMade;
};
tabDestroyed = (tab: any) => {
- if (tab.DashDoc && ![DocumentType.KVP, DocumentType.PRES].includes(tab.DashDoc?.type)) {
+ this._flush = this._flush ?? UndoManager.StartBatch('tab movement');
+ if (tab.DashDoc && ![DocumentType.PRES].includes(tab.DashDoc?.type) && !tab.contentItem.config.props.keyValue) {
Doc.AddDocToList(Doc.MyHeaderBar, 'data', tab.DashDoc);
- Doc.AddDocToList(Doc.MyRecentlyClosed, 'data', tab.DashDoc, undefined, true, true);
+ // if you close a tab that is not embedded somewhere else (an embedded Doc can be opened simultaneously in a tab), then add the tab to recently closed
+ if (tab.DashDoc.embedContainer === this.rootDoc) tab.DashDoc.embedContainer = undefined;
+ if (!tab.DashDoc.embedContainer) Doc.AddDocToList(Doc.MyRecentlyClosed, 'data', tab.DashDoc, undefined, true, true);
+ Doc.RemoveDocFromList(Doc.GetProto(tab.DashDoc), 'proto_embeddings', tab.DashDoc);
}
if (CollectionDockingView.Instance) {
const dview = CollectionDockingView.Instance.props.Document;
@@ -502,7 +498,7 @@ export class CollectionDockingView extends CollectionSubView() {
_layout_fitWidth: true,
title: `Untitled Tab ${NumCast(dashboard['pane-count'])}`,
});
- this.props.Document.isShared && inheritParentAcls(this.props.Document, docToAdd);
+ inheritParentAcls(this.rootDoc, docToAdd, false);
CollectionDockingView.AddSplit(docToAdd, OpenWhereMod.none, stack);
}
});
@@ -545,7 +541,7 @@ export class CollectionDockingView extends CollectionSubView() {
_freeform_backgroundGrid: true,
title: `Untitled Tab ${NumCast(dashboard['pane-count'])}`,
});
- this.props.Document.isShared && inheritParentAcls(this.props.Document, docToAdd);
+ inheritParentAcls(this.dataDoc, docToAdd, false);
CollectionDockingView.AddSplit(docToAdd, OpenWhereMod.none, stack);
}
})
@@ -586,8 +582,12 @@ ScriptingGlobals.add(
case OpenWhere.addRight:
return CollectionDockingView.AddSplit(doc, OpenWhereMod.right);
case OpenWhere.overlay:
- if (doc === 'repl') OverlayView.Instance.addWindow(<ScriptingRepl />, { x: 300, y: 100, width: 200, height: 200, title: 'Scripting REPL' });
- else Doc.AddDocToList(Doc.MyOverlayDocs, undefined, doc);
+ // prettier-ignore
+ switch (doc) {
+ case '<ScriptingRepl />': return OverlayView.Instance.addWindow(<ScriptingRepl />, { x: 300, y: 100, width: 200, height: 200, title: 'Scripting REPL' });
+ case "<UndoStack>": return OverlayView.Instance.addWindow(<UndoStack />, { x: 300, y: 100, width: 200, height: 200, title: 'Scripting REPL' });
+ }
+ Doc.AddToMyOverlay(doc);
}
},
'opens up document in location specified',
diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
index 528781991..06522b85e 100644
--- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
+++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
@@ -2,12 +2,12 @@ import React = require('react');
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed, observable, runInAction } from 'mobx';
import { observer } from 'mobx-react';
-import { DataSym, Doc, DocListCast } from '../../../fields/Doc';
+import { Doc, DocListCast } from '../../../fields/Doc';
+import { DocData } from '../../../fields/DocSymbols';
import { Id } from '../../../fields/FieldSymbols';
import { PastelSchemaPalette, SchemaHeaderField } from '../../../fields/SchemaHeaderField';
import { ScriptField } from '../../../fields/ScriptField';
-import { NumCast, StrCast } from '../../../fields/Types';
-import { emptyFunction, numberRange, returnEmptyString, setupMoveUpEvents } from '../../../Utils';
+import { emptyFunction, numberRange, returnEmptyString, returnFalse, setupMoveUpEvents } from '../../../Utils';
import { Docs } from '../../documents/Documents';
import { DragManager } from '../../util/DragManager';
import { CompileScript } from '../../util/Scripting';
@@ -97,14 +97,15 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
rowDrop = action((e: Event, de: DragManager.DropEvent) => {
this._createEmbeddingSelected = false;
if (de.complete.docDragData) {
- this.props.parent.Document.dropConverter instanceof ScriptField && this.props.parent.Document.dropConverter.script.run({ dragData: de.complete.docDragData });
const key = this.props.pivotField;
const castedValue = this.getValue(this.heading);
const onLayoutDoc = this.onLayoutDoc(key);
- de.complete.docDragData.droppedDocuments.forEach(d => Doc.SetInPlace(d, key, castedValue, !onLayoutDoc));
- this.props.parent.onInternalDrop(e, de);
- e.stopPropagation();
+ if (this.props.parent.onInternalDrop(e, de)) {
+ de.complete.docDragData.droppedDocuments.forEach(d => Doc.SetInPlace(d, key, castedValue, !onLayoutDoc));
+ }
+ return true;
}
+ return false;
});
getValue = (value: string): any => {
@@ -156,7 +157,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
const onLayoutDoc = this.onLayoutDoc(key);
FormattedTextBox.SelectOnLoad = newDoc[Id];
FormattedTextBox.SelectOnLoadChar = value;
- (onLayoutDoc ? newDoc : newDoc[DataSym])[key] = this.getValue(this.props.heading);
+ (onLayoutDoc ? newDoc : newDoc[DocData])[key] = this.getValue(this.props.heading);
const docs = this.props.parent.childDocList;
return docs ? (docs.splice(0, 0, newDoc) ? true : false) : this.props.parent.props.addDocument?.(newDoc) || false; // should really extend addDocument to specify insertion point (at beginning of list)
};
@@ -250,10 +251,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
return (
<div className="collectionStackingView-optionPicker">
<div className="optionOptions">
- <div className={'optionPicker' + (selected === true ? ' active' : '')} onClick={this.toggleEmbedding}>
- Create Embedding
- </div>
- <div className={'optionPicker' + (selected === true ? ' active' : '')} onClick={this.deleteRow}>
+ <div className={'optionPicker' + (selected === true ? ' active' : '')} onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, this.deleteRow)}>
Delete
</div>
</div>
@@ -271,13 +269,16 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
const stackPad = showChrome ? `0px ${this.props.parent.xMargin}px` : `${this.props.parent.yMargin}px ${this.props.parent.xMargin}px 0px ${this.props.parent.xMargin}px `;
return this.collapsed ? null : (
<div style={{ position: 'relative' }}>
+ {this.props.showHandle && this.props.parent.props.isContentActive() ? this.props.parent.columnDragger : null}
{showChrome ? (
<div
className="collectionStackingView-addDocumentButton"
- style={{
- //width: style.columnWidth / style.numGroupColumns,
- padding: `${NumCast(this.props.parent.layoutDoc._yPadding, this.props.parent.yMargin)}px 0px 0px 0px`,
- }}>
+ style={
+ {
+ //width: style.columnWidth / style.numGroupColumns,
+ //padding: `${NumCast(this.props.parent.layoutDoc._yPadding, this.props.parent.yMargin)}px 0px 0px 0px`,
+ }
+ }>
<EditableView GetValue={returnEmptyString} SetValue={this.addDocument} textCallback={this.textCallback} contents={'+ NEW'} />
</div>
) : null}
@@ -286,12 +287,12 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
ref={this._contRef}
style={{
padding: stackPad,
+ minHeight: this.props.showHandle && this.props.parent.props.isContentActive() ? '10px' : undefined,
width: this.props.parent.NodeWidth,
gridGap: this.props.parent.gridGap,
gridTemplateColumns: numberRange(rows).reduce((list: string, i: any) => list + ` ${this.props.parent.columnWidth}px`, ''),
}}>
{this.props.parent.children(this.props.docList)}
- {this.props.showHandle && this.props.parent.props.isContentActive() ? this.props.parent.columnDragger : null}
</div>
</div>
);
@@ -311,23 +312,33 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
onPointerDown={this.headerDown}
title={evContents === `NO ${key.toUpperCase()} VALUE` ? `Documents that don't have a ${key} value will go here. This column cannot be removed.` : ''}
style={{ background: evContents !== `NO ${key.toUpperCase()} VALUE` ? this.color : 'lightgrey' }}>
- {noChrome ? evContents : editableHeaderView}
+ {noChrome ? evContents : <div>{editableHeaderView}</div>}
{noChrome || evContents === `NO ${key.toUpperCase()} VALUE` ? null : (
<div className="collectionStackingView-sectionColor">
- <button className="collectionStackingView-sectionColorButton" onClick={action(e => (this._paletteOn = !this._paletteOn))}>
+ <button
+ className="collectionStackingView-sectionColorButton"
+ onPointerDown={e =>
+ setupMoveUpEvents(
+ this,
+ e,
+ returnFalse,
+ emptyFunction,
+ action(e => (this._paletteOn = !this._paletteOn))
+ )
+ }>
<FontAwesomeIcon icon="palette" size="lg" />
</button>
{this._paletteOn ? this.renderColorPicker() : null}
</div>
)}
{noChrome ? null : (
- <button className="collectionStackingView-sectionDelete" onClick={noChrome ? undefined : this.collapseSection}>
+ <button className="collectionStackingView-sectionDelete" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, noChrome ? emptyFunction : this.collapseSection)}>
<FontAwesomeIcon icon={this.collapsed ? 'chevron-down' : 'chevron-up'} size="lg" />
</button>
)}
{noChrome || evContents === `NO ${key.toUpperCase()} VALUE` ? null : (
- <div className="collectionStackingView-sectionOptions">
- <Flyout anchorPoint={anchorPoints.TOP_CENTER} content={this.renderMenu()}>
+ <div className="collectionStackingView-sectionOptions" onPointerDown={e => e.stopPropagation()}>
+ <Flyout anchorPoint={anchorPoints.RIGHT_TOP} content={this.renderMenu()}>
<button className="collectionStackingView-sectionOptionButton">
<FontAwesomeIcon icon="ellipsis-v" size="lg" />
</button>
diff --git a/src/client/views/collections/CollectionMenu.scss b/src/client/views/collections/CollectionMenu.scss
index c35f088a6..6eeccc94e 100644
--- a/src/client/views/collections/CollectionMenu.scss
+++ b/src/client/views/collections/CollectionMenu.scss
@@ -8,652 +8,15 @@
background-color: $dark-gray;
height: 35px;
border-bottom: $standard-border;
- padding-right: 5px;
+ padding: 0 10px;
align-items: center;
+ overflow-x: scroll;
+ &::-webkit-scrollbar {
+ display: none;
+ }
- .collectionMenu-hardCodedButton {
- cursor: pointer;
- color: $white;
- width: 25px;
- height: 25px;
- padding: 5;
- text-align: center;
+ .hardCodedButtons {
display: flex;
- justify-content: center;
- align-items: center;
- position: relative;
- transition: 0.2s;
- border-radius: 3px;
-
- &:hover {
- background-color: rgba(0, 0, 0, 0.2);
- }
+ flex-direction: row;
}
-}
-
-// .collectionMenu-cont {
-// position: relative;
-// display: inline-flex;
-// width: 100%;
-// opacity: 0.9;
-// z-index: 901;
-// transition: top .5s;
-// background: $dark-gray;
-// color: $white;
-// transform-origin: top left;
-// top: 0;
-// width: 100%;
-
-// .recordButtonOutline {
-// border-radius: 100%;
-// width: 18px;
-// height: 18px;
-// border: solid 1px $white;
-// display: flex;
-// align-items: center;
-// justify-content: center;
-// }
-
-// .recordButtonInner {
-// border-radius: 100%;
-// width: 70%;
-// height: 70%;
-// background: $white;
-// }
-
-// .collectionMenu {
-// display: flex;
-// height: 100%;
-// overflow: visible;
-// z-index: 901;
-// border: unset;
-
-// .collectionMenu-divider {
-// height: 100%;
-// margin-left: 3px;
-// margin-right: 3px;
-// width: 2px;
-// background-color: $medium-gray;
-// }
-
-// .collectionViewBaseChrome {
-// display: flex;
-// align-items: center;
-
-// .collectionViewBaseChrome-viewPicker {
-// font-size: $small-text;
-// outline-color: $black;
-// color: $white;
-// border: none;
-// background: $dark-gray;
-// }
-
-// .collectionViewBaseChrome-viewPicker:focus {
-// outline: none;
-// border: none;
-// }
-
-// .collectionViewBaseChrome-viewPicker:active {
-// outline-color: $black;
-// }
-
-// .collectionViewBaseChrome-button {
-// font-size: $small-text;
-// text-transform: uppercase;
-// letter-spacing: 2px;
-// background: $white;
-// color: $pink;
-// outline-color: $black;
-// border: none;
-// padding: 12px 10px 11px 10px;
-// margin-left: 10px;
-// }
-
-// .collectionViewBaseChrome-cmdPicker {
-// margin-left: 3px;
-// margin-right: 0px;
-// font-size: $small-text;
-// text-transform: capitalize;
-// color: $white;
-// border: none;
-// background: $dark-gray;
-// }
-
-// .collectionViewBaseChrome-cmdPicker:focus {
-// border: none;
-// outline: none;
-// }
-
-// .commandEntry-outerDiv {
-// pointer-events: all;
-// background-color: transparent;
-// display: flex;
-// flex-direction: row;
-// align-items: center;
-// justify-content: center;
-// height: 100%;
-// overflow: hidden;
-
-// .commandEntry-drop {
-// color: $white;
-// width: 30px;
-// margin-top: auto;
-// margin-bottom: auto;
-// }
-// }
-
-// .commandEntry-outerDiv:hover{
-// background-color: $drop-shadow;
-
-// .collectionViewBaseChrome-viewPicker,
-// .collectionViewBaseChrome-cmdPicker{
-// background: $dark-gray;
-// }
-// }
-
-// .collectionViewBaseChrome-collapse {
-// transition: all .5s, opacity 0.3s;
-// position: absolute;
-// width: 30px;
-// transform-origin: top left;
-// pointer-events: all;
-// // margin-top: 10px;
-// }
-
-// @media only screen and (max-device-width: 480px) {
-// .collectionViewBaseChrome-collapse {
-// display: none;
-// }
-// }
-
-// .collectionViewBaseChrome-template,
-// .collectionViewBaseChrome-viewModes {
-// align-items: center;
-// height: 100%;
-// display: flex;
-// background: transparent;
-// color: $medium-gray;
-// justify-content: center;
-// }
-
-// .collectionViewBaseChrome-viewSpecs {
-// margin-left: 5px;
-// display: grid;
-// border: none;
-// border-right: solid $medium-gray 1px;
-
-// .collectionViewBaseChrome-filterIcon {
-// position: relative;
-// display: flex;
-// margin: auto;
-// background: $dark-gray;
-// color: $white;
-// width: 30px;
-// height: 30px;
-// align-items: center;
-// justify-content: center;
-// border: none;
-// border-right: solid $medium-gray 1px;
-// }
-
-// .collectionViewBaseChrome-viewSpecsInput {
-// padding: 12px 10px 11px 10px;
-// border: 0px;
-// color: $medium-gray;
-// text-align: center;
-// letter-spacing: 2px;
-// outline-color: $black;
-// font-size: $small-text;
-// background: $white;
-// height: 100%;
-// width: 75px;
-// }
-
-// .collectionViewBaseChrome-viewSpecsMenu {
-// overflow: hidden;
-// transition: height .5s, display .5s;
-// position: absolute;
-// top: 60px;
-// z-index: 100;
-// display: flex;
-// flex-direction: column;
-// background: $white;
-// box-shadow: $medium-gray 2px 2px 4px;
-
-// .qs-datepicker {
-// left: unset;
-// right: 0;
-// }
-
-// .collectionViewBaseChrome-viewSpecsMenu-row {
-// display: grid;
-// grid-template-columns: 150px 200px 150px;
-// margin-top: 10px;
-// margin-right: 10px;
-
-// .collectionViewBaseChrome-viewSpecsMenu-rowLeft,
-// .collectionViewBaseChrome-viewSpecsMenu-rowMiddle,
-// .collectionViewBaseChrome-viewSpecsMenu-rowRight {
-// font-size: $small-text;
-// letter-spacing: 2px;
-// color: $medium-gray;
-// margin-left: 10px;
-// padding: 5px;
-// border: none;
-// outline-color: $black;
-// }
-// }
-
-// .collectionViewBaseChrome-viewSpecsMenu-lastRow {
-// display: grid;
-// grid-template-columns: 1fr 1fr 1fr;
-// grid-gap: 10px;
-// margin: 10px;
-// }
-// }
-// }
-// }
-
-// .collectionStackingViewChrome-cont,
-// .collectionTreeViewChrome-cont,
-// .collection3DCarouselViewChrome-cont {
-// display: flex;
-// justify-content: space-between;
-// }
-
-// .collectionGridViewChrome-cont {
-// display: flex;
-// margin-left: 10;
-
-// .collectionGridViewChrome-viewPicker {
-// font-size: $small-text;
-// //text-transform: uppercase;
-// //letter-spacing: 2px;
-// background: $dark-gray;
-// color: $white;
-// outline-color: $black;
-// color: $white;
-// border: none;
-// border-right: solid $medium-gray 1px;
-// }
-
-// .collectionGridViewChrome-viewPicker:active {
-// outline-color: $black;
-// }
-
-// .grid-control {
-// align-self: center;
-// display: flex;
-// flex-direction: row;
-// margin-right: 5px;
-
-// .grid-icon {
-// margin-right: 5px;
-// align-self: center;
-// }
-
-// .flexLabel {
-// margin-bottom: 0;
-// }
-
-// .collectionGridViewChrome-entryBox {
-// width: 50%;
-// color: $black;
-// }
-
-// .collectionGridViewChrome-columnButton {
-// color: $black;
-// }
-// }
-// }
-
-// .collectionStackingViewChrome-sort,
-// .collectionTreeViewChrome-sort {
-// display: flex;
-// align-items: center;
-// justify-content: space-between;
-
-// .collectionStackingViewChrome-sortIcon,
-// .collectionTreeViewChrome-sortIcon {
-// transition: transform .5s;
-// margin-left: 10px;
-// }
-// }
-
-// button:hover {
-// transform: scale(1);
-// }
-
-
-// .collectionStackingViewChrome-pivotField-cont,
-// .collectionTreeViewChrome-pivotField-cont,
-// .collection3DCarouselViewChrome-scrollSpeed-cont {
-// justify-self: right;
-// align-items: center;
-// display: flex;
-// grid-auto-columns: auto;
-// font-size: $small-text;
-// letter-spacing: 2px;
-
-// .collectionStackingViewChrome-pivotField-label,
-// .collectionTreeViewChrome-pivotField-label,
-// .collection3DCarouselViewChrome-scrollSpeed-label {
-// grid-column: 1;
-// margin-right: 7px;
-// user-select: none;
-// font-family: $sans-serif;
-// letter-spacing: normal;
-// }
-
-// .collectionStackingViewChrome-sortIcon {
-// transition: transform .5s;
-// grid-column: 3;
-// text-align: center;
-// display: flex;
-// justify-content: center;
-// align-items: center;
-// cursor: pointer;
-// width: 25px;
-// height: 25px;
-// border-radius: 100%;
-// }
-
-// .collectionStackingViewChrome-sortIcon:hover {
-// background-color: $drop-shadow;
-// }
-
-// .collectionStackingViewChrome-pivotField,
-// .collectionTreeViewChrome-pivotField,
-// .collection3DCarouselViewChrome-scrollSpeed {
-// color: $white;
-// grid-column: 2;
-// grid-row: 1;
-// width: 90%;
-// min-width: 100px;
-// display: flex;
-// height: 80%;
-// border-radius: 7px;
-// align-items: center;
-// background: $white;
-
-// .editable-view-input,
-// input,
-// .editableView-container-editing-oneLine,
-// .editableView-container-editing {
-// margin: auto;
-// border: 0px;
-// color: $light-gray !important;
-// text-align: center;
-// letter-spacing: 2px;
-// outline-color: $black;
-// height: 100%;
-// }
-
-// .react-autosuggest__container {
-// margin: 0;
-// color: $medium-gray;
-// padding: 0px;
-// }
-// }
-// }
-
-// .collectionStackingViewChrome-pivotField:hover,
-// .collectionTreeViewChrome-pivotField:hover,
-// .collection3DCarouselViewChrome-scrollSpeed:hover {
-// cursor: text;
-// }
-
-// }
-// }
-
-// .collectionMenu-webUrlButtons {
-// margin-left: 44;
-// background: lightGray;
-// display: flex;
-// }
-
-// .webBox-urlEditor {
-// position: relative;
-// opacity: 0.9;
-// z-index: 901;
-// transition: top .5s;
-
-// .urlEditor {
-// display: grid;
-// grid-template-columns: 1fr auto;
-// padding-bottom: 10px;
-// overflow: hidden;
-// margin-top: 5px;
-// height: 35px;
-
-// .editorBase {
-// display: flex;
-
-// .editor-collapse {
-// transition: all .5s, opacity 0.3s;
-// position: absolute;
-// width: 40px;
-// transform-origin: top left;
-// }
-
-// .switchToText {
-// color: $medium-gray;
-// }
-
-// .switchToText:hover {
-// color: $dark-gray;
-// }
-// }
-
-// button:hover {
-// transform: scale(1);
-// }
-// }
-// }
-
-// .collectionMenu-urlInput {
-// padding: 12px 10px 11px 10px;
-// border: 0px;
-// color: $black;
-// font-size: $small-text;
-// letter-spacing: 2px;
-// outline-color: $black;
-// background: $white;
-// width: 100%;
-// min-width: 350px;
-// margin-right: 10px;
-// height: 100%;
-// }
-
-// .collectionFreeFormMenu-cont {
-// display: inline-flex;
-// position: relative;
-// align-items: center;
-// height: 100%;
-
-// .color-previewI {
-// width: 60%;
-// top: 80%;
-// position: absolute;
-// height: 4px;
-// }
-
-// .color-previewII {
-// width: 80%;
-// height: 80%;
-// margin-left: 10%;
-// position: absolute;
-// bottom: 5;
-// }
-
-// .btn-group {
-// display: grid;
-// grid-template-columns: auto auto auto auto;
-// margin: auto;
-// /* Make the buttons appear below each other */
-// }
-
-// .btn-draw {
-// display: inline-flex;
-// margin: auto;
-// /* Make the buttons appear below each other */
-// }
-
-// .fwdKeyframe,
-// .numKeyframe,
-// .backKeyframe {
-// cursor: pointer;
-// position: relative;
-// width: 20;
-// height: 30;
-// bottom: 0;
-// background: $dark-gray;
-// display: inline-flex;
-// align-items: center;
-// color: $white;
-// }
-
-// .backKeyframe {
-// svg {
-// display: block;
-// margin: auto;
-// }
-// }
-
-
-// .numKeyframe {
-// flex-direction: column;
-// padding-top: 5px;
-// }
-
-// .fwdKeyframe {
-// svg {
-// display: block;
-// margin: auto;
-// }
-
-// border-right: solid $medium-gray 1px;
-// }
-// }
-
-// .collectionSchemaViewChrome-cont {
-// display: flex;
-// font-size: $small-text;
-
-// .collectionSchemaViewChrome-toggle {
-// display: flex;
-// margin-left: 10px;
-// }
-
-// .collectionSchemaViewChrome-label {
-// text-transform: uppercase;
-// letter-spacing: 2px;
-// margin-right: 5px;
-// display: flex;
-// flex-direction: column;
-// justify-content: center;
-// }
-
-// .collectionSchemaViewChrome-toggler {
-// width: 100px;
-// height: 35px;
-// background-color: $black;
-// position: relative;
-// }
-
-// .collectionSchemaViewChrome-togglerButton {
-// width: 47px;
-// height: 30px;
-// background-color: $light-gray;
-// // position: absolute;
-// transition: all 0.5s ease;
-// // top: 3px;
-// margin-top: 3px;
-// color: $medium-gray;
-// letter-spacing: 2px;
-// text-transform: uppercase;
-// display: flex;
-// flex-direction: column;
-// justify-content: center;
-// text-align: center;
-
-// &.on {
-// margin-left: 3px;
-// }
-
-// &.off {
-// margin-left: 50px;
-// }
-// }
-// }
-
-
-// .commandEntry-outerDiv {
-// display: flex;
-// flex-direction: column;
-// height: 40px;
-// }
-
-// .commandEntry-inputArea {
-// display: flex;
-// flex-direction: row;
-// width: 150px;
-// margin: auto auto auto auto;
-// }
-
-// .react-autosuggest__container {
-// position: relative;
-// width: 100%;
-// margin-left: 5px;
-// margin-right: 5px;
-// }
-
-// .react-autosuggest__input {
-// border: 1px solid $light-gray;
-// border-radius: 4px;
-// width: 100%;
-// }
-
-// .react-autosuggest__input--focused {
-// outline: none;
-// }
-
-// .react-autosuggest__input--open {
-// border-bottom-left-radius: 0;
-// border-bottom-right-radius: 0;
-// }
-
-// .react-autosuggest__suggestions-container {
-// display: none;
-// }
-
-// .react-autosuggest__suggestions-container--open {
-// display: block;
-// position: fixed;
-// overflow-y: auto;
-// max-height: 400px;
-// width: 180px;
-// border: 1px solid $light-gray;
-// background-color: $white;
-// font-family: $sans-serif;
-// font-weight: 300;
-// font-size: $large-header;
-// border-bottom-left-radius: 4px;
-// border-bottom-right-radius: 4px;
-// z-index: 2;
-// }
-
-// .react-autosuggest__suggestions-list {
-// margin: 0;
-// padding: 0;
-// list-style-type: none;
-// }
-
-// .react-autosuggest__suggestion {
-// cursor: pointer;
-// padding: 10px 20px;
-// }
-
-// .react-autosuggest__suggestion--highlighted {
-// background-color: $light-gray;
-// } \ No newline at end of file
+} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx
index 615014c4a..f65e8698f 100644
--- a/src/client/views/collections/CollectionMenu.tsx
+++ b/src/client/views/collections/CollectionMenu.tsx
@@ -2,23 +2,23 @@ import React = require('react');
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon, FontAwesomeIconProps } from '@fortawesome/react-fontawesome';
import { Tooltip } from '@material-ui/core';
-import { action, computed, Lambda, observable, reaction, runInAction } from 'mobx';
+import { Toggle, ToggleType, Type } from 'browndash-components';
+import { Lambda, action, computed, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import { ColorState } from 'react-color';
+import { Utils, emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, setupMoveUpEvents } from '../../../Utils';
import { Doc, DocListCast, Opt } from '../../../fields/Doc';
-import { Document } from '../../../fields/documentSchemas';
import { Id } from '../../../fields/FieldSymbols';
import { InkTool } from '../../../fields/InkField';
import { List } from '../../../fields/List';
import { ObjectField } from '../../../fields/ObjectField';
import { RichTextField } from '../../../fields/RichTextField';
import { listSpec } from '../../../fields/Schema';
-import { ScriptField } from '../../../fields/ScriptField';
-import { BoolCast, Cast, NumCast, StrCast } from '../../../fields/Types';
+import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../fields/Types';
+import { Document } from '../../../fields/documentSchemas';
import { GestureUtils } from '../../../pen-gestures/GestureUtils';
-import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, setupMoveUpEvents, Utils } from '../../../Utils';
-import { Docs } from '../../documents/Documents';
import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
+import { Docs } from '../../documents/Documents';
import { DragManager } from '../../util/DragManager';
import { ScriptingGlobals } from '../../util/ScriptingGlobals';
import { SelectionManager } from '../../util/SelectionManager';
@@ -28,20 +28,19 @@ import { undoBatch } from '../../util/UndoManager';
import { AntimodeMenu } from '../AntimodeMenu';
import { EditableView } from '../EditableView';
import { GestureOverlay } from '../GestureOverlay';
-import { Colors } from '../global/globalEnums';
import { ActiveFillColor, ActiveInkColor, SetActiveArrowEnd, SetActiveArrowStart, SetActiveBezierApprox, SetActiveFillColor, SetActiveInkColor, SetActiveInkWidth } from '../InkingStroke';
import { LightboxView } from '../LightboxView';
+import { MainView } from '../MainView';
+import { DefaultStyleProvider } from '../StyleProvider';
import { CollectionFreeFormDocumentView } from '../nodes/CollectionFreeFormDocumentView';
-import { DocumentView, OpenWhereMod } from '../nodes/DocumentView';
-import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
+import { DocumentView, DocumentViewInternal, OpenWhereMod } from '../nodes/DocumentView';
import { RichTextMenu } from '../nodes/formattedText/RichTextMenu';
-import { DefaultStyleProvider } from '../StyleProvider';
import { CollectionDockingView } from './CollectionDockingView';
-import { CollectionLinearView } from './collectionLinear';
import './CollectionMenu.scss';
import { COLLECTION_BORDER_WIDTH } from './CollectionView';
import { TabDocView } from './TabDocView';
import { CollectionFreeFormView } from './collectionFreeForm';
+import { CollectionLinearView } from './collectionLinear';
interface CollectionMenuProps {
panelHeight: () => number;
@@ -95,6 +94,15 @@ export class CollectionMenu extends AntimodeMenu<CollectionMenuProps> {
}
};
+ @action
+ toggleProperties = () => {
+ if (MainView.Instance.propertiesWidth() > 0) {
+ SettingsManager.propertiesWidth = 0;
+ } else {
+ SettingsManager.propertiesWidth = 300;
+ }
+ };
+
buttonBarXf = () => {
if (!this._docBtnRef.current) return Transform.Identity();
const { scale, translateX, translateY } = Utils.GetScreenTransform(this._docBtnRef.current);
@@ -121,7 +129,7 @@ export class CollectionMenu extends AntimodeMenu<CollectionMenuProps> {
docViewPath={returnEmptyDoclist}
moveDocument={returnFalse}
addDocument={returnFalse}
- addDocTab={returnFalse}
+ addDocTab={DocumentViewInternal.addDocTabFunc}
pinToPres={emptyFunction}
removeDocument={returnFalse}
ScreenToLocalTransform={this.buttonBarXf}
@@ -130,8 +138,8 @@ export class CollectionMenu extends AntimodeMenu<CollectionMenuProps> {
renderDepth={0}
focus={emptyFunction}
whenChildContentsActiveChanged={emptyFunction}
- docFilters={returnEmptyFilter}
- docRangeFilters={returnEmptyFilter}
+ childFilters={returnEmptyFilter}
+ childFiltersByRanges={returnEmptyFilter}
searchFilterDocs={returnEmptyDoclist}
/>
</div>
@@ -139,23 +147,45 @@ export class CollectionMenu extends AntimodeMenu<CollectionMenuProps> {
}
render() {
- const propIcon = SettingsManager.headerBarHeight > 0 ? 'angle-double-up' : 'angle-double-down';
- const propTitle = SettingsManager.headerBarHeight > 0 ? 'Close Header Bar' : 'Open Header Bar';
-
- const prop = (
- <Tooltip title={<div className="dash-tooltip">{propTitle}</div>} key="topar" placement="bottom">
- <div className="collectionMenu-hardCodedButton" style={{ backgroundColor: SettingsManager.propertiesWidth > 0 ? Colors.MEDIUM_BLUE : undefined }} onPointerDown={this.toggleTopBar}>
- <FontAwesomeIcon icon={propIcon} size="lg" />
- </div>
- </Tooltip>
+ const headerIcon = SettingsManager.headerBarHeight > 0 ? 'angle-double-up' : 'angle-double-down';
+ const headerTitle = SettingsManager.headerBarHeight > 0 ? 'Close Header Bar' : 'Open Header Bar';
+ const propIcon = SettingsManager.propertiesWidth > 0 ? 'angle-double-right' : 'angle-double-left';
+ const propTitle = SettingsManager.propertiesWidth > 0 ? 'Close Properties' : 'Open Properties';
+
+ const hardCodedButtons = (
+ <div className={`hardCodedButtons`}>
+ <Toggle
+ toggleType={ToggleType.BUTTON}
+ type={Type.PRIM}
+ color={StrCast(Doc.UserDoc().userColor)}
+ onClick={this.toggleTopBar}
+ toggleStatus={SettingsManager.headerBarHeight > 0}
+ icon={<FontAwesomeIcon icon={headerIcon} size="lg" />}
+ tooltip={headerTitle}
+ />
+ <Toggle
+ toggleType={ToggleType.BUTTON}
+ type={Type.PRIM}
+ color={StrCast(Doc.UserDoc().userColor)}
+ onClick={this.toggleProperties}
+ toggleStatus={SettingsManager.propertiesWidth > 0}
+ icon={<FontAwesomeIcon icon={propIcon} size="lg" />}
+ tooltip={propTitle}
+ />
+ </div>
);
// NEW BUTTONS
//dash col linear view buttons
const contMenuButtons = (
- <div className="collectionMenu-container">
+ <div
+ className="collectionMenu-container"
+ style={{
+ background: SettingsManager.Instance.userBackgroundColor,
+ // borderColor: StrCast(Doc.UserDoc().userColor)
+ }}>
{this.contMenuButtons}
- {prop}
+ {hardCodedButtons}
</div>
);
@@ -172,7 +202,7 @@ export class CollectionMenu extends AntimodeMenu<CollectionMenuProps> {
// [<CollectionViewBaseChrome key="chrome"
// docView={this.SelectedCollection}
// fieldKey={this.SelectedCollection.LayoutFieldKey}
- // type={StrCast(this.SelectedCollection?.props.Document._viewType, CollectionViewType.Invalid) as CollectionViewType} />,
+ // type={StrCast(this.SelectedCollection?.props.Document._type_collection, CollectionViewType.Invalid) as CollectionViewType} />,
// prop,
// /*button*/]);
}
@@ -277,16 +307,16 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu
_saveFilterCommand = {
params: ['target'],
title: 'save filter',
- script: `self.target._docFilters = compareLists(self['target-docFilters'],self.target._docFilters) ? undefined : copyField(self['target-docFilters']);
+ script: `self.target._childFilters = compareLists(self['target-childFilters'],self.target._childFilters) ? undefined : copyField(self['target-childFilters']);
self.target._searchFilterDocs = compareLists(self['target-searchFilterDocs'],self.target._searchFilterDocs) ? undefined: copyField(self['target-searchFilterDocs']);`,
immediate: undoBatch((source: Doc[]) => {
- this.target._docFilters = undefined;
+ this.target._childFilters = undefined;
this.target._searchFilterDocs = undefined;
}),
initialize: (button: Doc) => {
const activeDash = Doc.ActiveDashboard;
if (activeDash) {
- button['target-docFilters'] = (Doc.MySearcher._docFilters || activeDash._docFilters) instanceof ObjectField ? ObjectField.MakeCopy((Doc.MySearcher._docFilters || activeDash._docFilters) as any as ObjectField) : undefined;
+ button['target-childFilters'] = (Doc.MySearcher._childFilters || activeDash._childFilters) instanceof ObjectField ? ObjectField.MakeCopy((Doc.MySearcher._childFilters || activeDash._childFilters) as any as ObjectField) : undefined;
button['target-searchFilterDocs'] = activeDash._searchFilterDocs instanceof ObjectField ? ObjectField.MakeCopy(activeDash._searchFilterDocs as any as ObjectField) : undefined;
}
},
@@ -347,9 +377,9 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu
@undoBatch
viewChanged = (e: React.ChangeEvent) => {
- const target = this.document !== Doc.MyLeftSidebarPanel ? this.document : (this.document.proto as Doc);
+ const target = this.document !== Doc.MyLeftSidebarPanel ? this.document : DocCast(this.document.proto);
//@ts-ignore
- target._viewType = e.target.selectedOptions[0].value;
+ target._type_collection = e.target.selectedOptions[0].value;
};
commandChanged = (e: React.ChangeEvent) => {
@@ -427,8 +457,9 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu
if (docDragData?.draggedDocuments.length) {
this._buttonizableCommands?.filter(c => c.title === this._currentKey).map(c => c.immediate(docDragData.draggedDocuments || []));
e.stopPropagation();
+ return true;
}
- return true;
+ return false;
}
dragViewDown = (e: React.PointerEvent) => {
@@ -440,8 +471,8 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu
const c = {
params: ['target'],
title: vtype,
- script: `this.target._viewType = '${StrCast(this.props.type)}'`,
- immediate: (source: Doc[]) => (this.document._viewType = Doc.getDocTemplate(source?.[0])),
+ script: `this.target._type_collection = '${StrCast(this.props.type)}'`,
+ immediate: (source: Doc[]) => (this.document._type_collection = Doc.getDocTemplate(source?.[0])),
initialize: emptyFunction,
};
DragManager.StartButtonDrag([this._viewRef.current!], c.script, StrCast(c.title), { target: this.document }, c.params, c.initialize, e.clientX, e.clientY);
@@ -549,7 +580,6 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu
@action
startRecording = () => {
const doc = Docs.Create.ScreenshotDocument({ title: 'screen recording', _layout_fitWidth: true, _width: 400, _height: 200, mediaState: 'pendingRecording' });
- //Doc.AddDocToList(Doc.MyOverlayDocs, undefined, doc);
CollectionDockingView.AddSplit(doc, OpenWhereMod.right);
};
@@ -639,8 +669,8 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu
key="float"
style={{
backgroundColor: this.props.docView.layoutDoc.z ? '121212' : undefined,
- pointerEvents: this.props.docView.props.docViewPath().lastElement()?.rootDoc?._viewType !== CollectionViewType.Freeform ? 'none' : undefined,
- color: this.props.docView.props.docViewPath().lastElement()?.rootDoc?._viewType !== CollectionViewType.Freeform ? 'dimgrey' : undefined,
+ pointerEvents: this.props.docView.props.docViewPath().lastElement()?.rootDoc?._type_collection !== CollectionViewType.Freeform ? 'none' : undefined,
+ color: this.props.docView.props.docViewPath().lastElement()?.rootDoc?._type_collection !== CollectionViewType.Freeform ? 'dimgrey' : undefined,
}}
onClick={undoBatch(() => this.props.docView.props.CollectionFreeFormDocumentView?.().float())}>
<FontAwesomeIcon icon={['fab', 'buffer']} size={'lg'} />
@@ -1005,14 +1035,12 @@ export class CollectionStackingViewChrome extends React.Component<CollectionView
if (Doc.noviceMode) {
if (docs instanceof Doc) {
- const keys = Object.keys(docs).filter(key => key.indexOf('title') >= 0 || key.indexOf('author') >= 0 || key.indexOf('creationDate') >= 0 || key.indexOf('modificationDate') >= 0 || (key[0].toUpperCase() === key[0] && key[0] !== '_'));
+ const keys = Object.keys(docs).filter(key => key.indexOf('title') >= 0 || key.indexOf('author') >= 0 || key.indexOf('author_date') >= 0 || key.indexOf('modificationDate') >= 0 || (key[0].toUpperCase() === key[0] && key[0] !== '_'));
return keys.filter(key => key.toLowerCase().indexOf(val) > -1);
}
const keys = new Set<string>();
docs.forEach(doc => Doc.allKeys(doc).forEach(key => keys.add(key)));
- const noviceKeys = Array.from(keys).filter(
- key => key.indexOf('title') >= 0 || key.indexOf('author') >= 0 || key.indexOf('creationDate') >= 0 || key.indexOf('modificationDate') >= 0 || (key[0]?.toUpperCase() === key[0] && key[0] !== '_')
- );
+ const noviceKeys = Array.from(keys).filter(key => key.indexOf('title') >= 0 || key.indexOf('author') >= 0 || key.indexOf('author_date') >= 0 || key.indexOf('modificationDate') >= 0 || (key[0]?.toUpperCase() === key[0] && key[0] !== '_'));
return noviceKeys.filter(key => key.toLowerCase().indexOf(val) > -1);
}
@@ -1124,13 +1152,13 @@ export class CollectionNoteTakingViewChrome extends React.Component<CollectionVi
if (Doc.UserDoc().noviceMode) {
if (docs instanceof Doc) {
- const keys = Object.keys(docs).filter(key => key.indexOf('title') >= 0 || key.indexOf('author') >= 0 || key.indexOf('creationDate') >= 0 || key.indexOf('modificationDate') >= 0 || (key[0].toUpperCase() === key[0] && key[0] !== '_'));
+ const keys = Object.keys(docs).filter(key => key.indexOf('title') >= 0 || key.indexOf('author') >= 0 || key.indexOf('author_date') >= 0 || key.indexOf('modificationDate') >= 0 || (key[0].toUpperCase() === key[0] && key[0] !== '_'));
return keys.filter(key => key.toLowerCase().indexOf(val) > -1);
} else {
const keys = new Set<string>();
docs.forEach(doc => Doc.allKeys(doc).forEach(key => keys.add(key)));
const noviceKeys = Array.from(keys).filter(
- key => key.indexOf('title') >= 0 || key.indexOf('author') >= 0 || key.indexOf('creationDate') >= 0 || key.indexOf('modificationDate') >= 0 || (key[0]?.toUpperCase() === key[0] && key[0] !== '_')
+ key => key.indexOf('title') >= 0 || key.indexOf('author') >= 0 || key.indexOf('author_date') >= 0 || key.indexOf('modificationDate') >= 0 || (key[0]?.toUpperCase() === key[0] && key[0] !== '_')
);
return noviceKeys.filter(key => key.toLowerCase().indexOf(val) > -1);
}
diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx
index a65e23911..53a42d2a6 100644
--- a/src/client/views/collections/CollectionNoteTakingView.tsx
+++ b/src/client/views/collections/CollectionNoteTakingView.tsx
@@ -2,14 +2,15 @@ import React = require('react');
import { CursorProperty } from 'csstype';
import { action, computed, IReactionDisposer, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
-import { DataSym, Doc, Field, HeightSym, Opt, WidthSym } from '../../../fields/Doc';
+import { Doc, Field, Opt } from '../../../fields/Doc';
+import { DocData, Height, Width } from '../../../fields/DocSymbols';
import { Id } from '../../../fields/FieldSymbols';
import { List } from '../../../fields/List';
import { listSpec } from '../../../fields/Schema';
import { SchemaHeaderField } from '../../../fields/SchemaHeaderField';
import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
import { TraceMobx } from '../../../fields/util';
-import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, returnZero, smoothScroll, Utils } from '../../../Utils';
+import { emptyFunction, returnFalse, returnZero, smoothScroll, Utils } from '../../../Utils';
import { Docs, DocUtils } from '../../documents/Documents';
import { DragManager, dropActionType } from '../../util/DragManager';
import { SnappingManager } from '../../util/SnappingManager';
@@ -46,7 +47,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
@observable _cursor: CursorProperty = 'grab';
@observable _scroll = 0;
@computed get chromeHidden() {
- return BoolCast(this.layoutDoc.chromeHidden);
+ return BoolCast(this.layoutDoc.chromeHidden) || this.props.onBrowseClick?.() ? true : false;
}
// columnHeaders returns the list of SchemaHeaderFields currently being used by the layout doc to render the columns
@computed get colHeaderData() {
@@ -191,7 +192,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
if (found) {
const top = found.getBoundingClientRect().top;
const localTop = this.props.ScreenToLocalTransform().transformPoint(0, top);
- if (Math.floor(localTop[1]) !== 0) {
+ if (Math.floor(localTop[1]) !== 0 && Math.ceil(this.props.PanelHeight()) < (this._mainCont?.scrollHeight || 0)) {
let focusSpeed = options.zoomTime ?? 500;
smoothScroll(focusSpeed, this._mainCont!, localTop[1] + this._mainCont!.scrollTop, options.easeFunc);
return focusSpeed;
@@ -200,13 +201,17 @@ export class CollectionNoteTakingView extends CollectionSubView() {
};
styleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string) => {
- if (property === StyleProp.BoxShadow && doc && DragManager.docsBeingDragged.includes(doc)) {
- return `#9c9396 ${StrCast(doc?.boxShadow, '10px 10px 0.9vw')}`;
- }
- if (property === StyleProp.Opacity && doc) {
- if (this.props.childOpacity) {
- return this.props.childOpacity();
- }
+ switch (property) {
+ case StyleProp.BoxShadow:
+ if (doc && DragManager.docsBeingDragged.includes(doc)) {
+ return `#9c9396 ${StrCast(doc?.layout_boxShadow, '10px 10px 0.9vw')}`;
+ }
+ break;
+ case StyleProp.Opacity:
+ if (doc && this.props.childOpacity) {
+ return this.props.childOpacity();
+ }
+ break;
}
return this.props.styleProvider?.(doc, props, property);
};
@@ -225,7 +230,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
ref={r => (dref = r || undefined)}
Document={doc}
pointerEvents={this.blockPointerEventsWhenDragging}
- DataDoc={dataDoc || (!Doc.AreProtosEqual(doc[DataSym], doc) && doc[DataSym])}
+ DataDoc={dataDoc ?? (!Doc.AreProtosEqual(doc[DocData], doc) ? doc[DocData] : undefined)}
renderDepth={this.props.renderDepth + 1}
PanelWidth={width}
PanelHeight={height}
@@ -245,16 +250,17 @@ export class CollectionNoteTakingView extends CollectionSubView() {
dontRegisterView={dataDoc ? true : BoolCast(this.layoutDoc.childDontRegisterViews, this.props.dontRegisterView)}
rootSelected={this.rootSelected}
layout_showTitle={this.props.childlayout_showTitle}
- dropAction={StrCast(this.layoutDoc.childDropAction) as dropActionType}
+ dragAction={StrCast(this.layoutDoc.childDragAction) as dropActionType}
onClick={this.onChildClickHandler}
+ onBrowseClick={this.props.onBrowseClick}
onDoubleClick={this.onChildDoubleClickHandler}
ScreenToLocalTransform={noteTakingDocTransform}
focus={this.focusDocument}
- docFilters={this.childDocFilters}
+ childFilters={this.childDocFilters}
hideDecorationTitle={this.props.childHideDecorationTitle?.()}
hideResizeHandles={this.props.childHideResizeHandles?.()}
hideTitle={this.props.childHideTitle?.()}
- docRangeFilters={this.childDocRangeFilters}
+ childFiltersByRanges={this.childDocRangeFilters}
searchFilterDocs={this.searchFilterDocs}
addDocument={this.props.addDocument}
moveDocument={this.props.moveDocument}
@@ -284,7 +290,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
const existingHeader = this.colHeaderData.find(sh => sh.heading === heading);
const existingWidth = existingHeader?.width ? existingHeader.width : 0;
const maxWidth = existingWidth > 0 ? existingWidth * this.availableWidth : this.maxColWidth;
- const width = d.layout_fitWidth ? maxWidth : d[WidthSym]();
+ const width = d.layout_fitWidth ? maxWidth : d[Width]();
return Math.min(maxWidth - CollectionNoteTakingViewColumn.ColumnMargin, width < maxWidth ? width : maxWidth);
}
@@ -294,8 +300,8 @@ export class CollectionNoteTakingView extends CollectionSubView() {
const childLayoutDoc = Doc.Layout(d, this.props.childLayoutTemplate?.());
const childDataDoc = !d.isTemplateDoc && !d.isTemplateForField ? undefined : this.props.DataDoc;
const maxHeight = (lim => (lim === 0 ? this.props.PanelWidth() : lim === -1 ? 10000 : lim))(NumCast(this.layoutDoc.childLimitHeight, -1));
- const nw = Doc.NativeWidth(childLayoutDoc, childDataDoc) || (!(childLayoutDoc._layout_fitWidth || this.props.childLayoutFitWidth?.(d)) ? d[WidthSym]() : 0);
- const nh = Doc.NativeHeight(childLayoutDoc, childDataDoc) || (!(childLayoutDoc._layout_fitWidth || this.props.childLayoutFitWidth?.(d)) ? d[HeightSym]() : 0);
+ const nw = Doc.NativeWidth(childLayoutDoc, childDataDoc) || (!(childLayoutDoc._layout_fitWidth || this.props.childLayoutFitWidth?.(d)) ? d[Width]() : 0);
+ const nh = Doc.NativeHeight(childLayoutDoc, childDataDoc) || (!(childLayoutDoc._layout_fitWidth || this.props.childLayoutFitWidth?.(d)) ? d[Height]() : 0);
if (nw && nh) {
const docWid = this.getDocWidth(d);
return Math.min(maxHeight, (docWid * nh) / nw);
@@ -436,23 +442,24 @@ export class CollectionNoteTakingView extends CollectionSubView() {
docs.splice(previousDocIndex + 1, 0, ...newDocs);
}
}
+ return true;
}
} else if (de.complete.linkDragData?.dragDocument.embedContainer === this.props.Document && de.complete.linkDragData?.linkDragView?.props.CollectionFreeFormDocumentView?.()) {
const source = Docs.Create.TextDocument('', { _width: 200, _height: 75, _layout_fitWidth: true, title: 'dropped annotation' });
- this.props.addDocument?.(source);
+ if (!this.props.addDocument?.(source)) e.preventDefault();
de.complete.linkDocument = DocUtils.MakeLink(source, de.complete.linkDragData.linkSourceGetAnchor(), { link_relationship: 'doc annotation' }); // TODODO this is where in text links get passed
e.stopPropagation();
- } else if (de.complete.annoDragData?.dragDocument && super.onInternalDrop(e, de)) return this.internalAnchorAnnoDrop(e, de.complete.annoDragData);
+ return true;
+ } else if (de.complete.annoDragData?.dragDocument && super.onInternalDrop(e, de)) {
+ return this.internalAnchorAnnoDrop(e, de.complete.annoDragData);
+ }
return false;
};
@undoBatch
internalAnchorAnnoDrop(e: Event, annoDragData: DragManager.AnchorAnnoDragData) {
const dropCreator = annoDragData.dropDocCreator;
- annoDragData.dropDocCreator = (annotationOn: Doc | undefined) => {
- const dropDoc = dropCreator(annotationOn);
- return dropDoc || this.rootDoc;
- };
+ annoDragData.dropDocCreator = (annotationOn: Doc | undefined) => dropCreator(annotationOn) || this.rootDoc;
return true;
}
@@ -602,42 +609,6 @@ export class CollectionNoteTakingView extends CollectionSubView() {
return eles;
}
- @computed get buttonMenu() {
- const menuDoc: Doc = Cast(this.rootDoc.buttonMenuDoc, Doc, null);
- if (menuDoc) {
- const width = NumCast(menuDoc._width, 30);
- const height = NumCast(menuDoc._height, 30);
- return (
- <div className="buttonMenu-docBtn" style={{ width, height }}>
- <DocumentView
- Document={menuDoc}
- DataDoc={menuDoc}
- isContentActive={this.props.isContentActive}
- isDocumentActive={returnTrue}
- addDocument={this.props.addDocument}
- moveDocument={this.props.moveDocument}
- addDocTab={this.props.addDocTab}
- pinToPres={emptyFunction}
- rootSelected={this.props.isSelected}
- removeDocument={this.props.removeDocument}
- ScreenToLocalTransform={Transform.Identity}
- PanelWidth={() => 35}
- PanelHeight={() => 35}
- renderDepth={this.props.renderDepth}
- focus={emptyFunction}
- styleProvider={this.props.styleProvider}
- docViewPath={returnEmptyDoclist}
- whenChildContentsActiveChanged={emptyFunction}
- bringToFront={emptyFunction}
- docFilters={this.props.docFilters}
- docRangeFilters={this.props.docRangeFilters}
- searchFilterDocs={this.props.searchFilterDocs}
- />
- </div>
- );
- }
- }
-
@computed get nativeWidth() {
return Doc.NativeWidth(this.layoutDoc);
}
@@ -650,42 +621,32 @@ export class CollectionNoteTakingView extends CollectionSubView() {
}
@computed get backgroundEvents() {
- return SnappingManager.GetIsDragging();
+ return this.props.isContentActive() === false ? 'none' : undefined;
}
observer: any;
render() {
TraceMobx();
- const buttonMenu = this.rootDoc.buttonMenu;
- const noviceExplainer = StrCast(this.rootDoc.explainer);
return (
- <>
- {buttonMenu || noviceExplainer ? (
- <div className="documentButtonMenu" key="buttons">
- {buttonMenu ? this.buttonMenu : null}
- {Doc.UserDoc().noviceMode && noviceExplainer ? <div className="documentExplanation">{noviceExplainer}</div> : null}
- </div>
- ) : null}
- <div
- className="collectionNoteTakingView"
- ref={this.createRef}
- key="notes"
- style={{
- overflowY: this.props.isContentActive() ? 'auto' : 'hidden',
- background: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor),
- pointerEvents: this.backgroundEvents ? 'all' : undefined,
- }}
- onScroll={action(e => (this._scroll = e.currentTarget.scrollTop))}
- onPointerLeave={action(e => (this.docsDraggedRowCol.length = 0))}
- onPointerMove={e => e.buttons && this.onPointerMove(false, e.clientX, e.clientY)}
- onDragOver={e => this.onPointerMove(true, e.clientX, e.clientY)}
- onDrop={this.onExternalDrop.bind(this)}
- onContextMenu={this.onContextMenu}
- onWheel={e => this.props.isContentActive(true) && e.stopPropagation()}>
- {this.renderedSections}
- </div>
- </>
+ <div
+ className="collectionNoteTakingView"
+ ref={this.createRef}
+ key="notes"
+ style={{
+ overflowY: this.props.isContentActive() ? 'auto' : 'hidden',
+ background: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor),
+ pointerEvents: this.backgroundEvents ? 'all' : undefined,
+ }}
+ onScroll={action(e => (this._scroll = e.currentTarget.scrollTop))}
+ onPointerLeave={action(e => (this.docsDraggedRowCol.length = 0))}
+ onPointerMove={e => e.buttons && this.onPointerMove(false, e.clientX, e.clientY)}
+ onDragOver={e => this.onPointerMove(true, e.clientX, e.clientY)}
+ onDrop={this.onExternalDrop.bind(this)}
+ onContextMenu={this.onContextMenu}
+ onWheel={e => this.props.isContentActive(true) && e.stopPropagation()}>
+ {this.renderedSections}
+ </div>
);
}
}
diff --git a/src/client/views/collections/CollectionNoteTakingViewColumn.tsx b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx
index 63becac1e..3286d60bd 100644
--- a/src/client/views/collections/CollectionNoteTakingViewColumn.tsx
+++ b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx
@@ -92,6 +92,7 @@ export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColu
columnDrop = action((e: Event, de: DragManager.DropEvent) => {
const drop = { docs: de.complete.docDragData?.droppedDocuments, val: this.getValue(this._heading) };
drop.docs?.forEach(d => Doc.SetInPlace(d, this.props.pivotField, drop.val, false));
+ return true;
});
getValue = (value: string): any => {
@@ -121,7 +122,8 @@ export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColu
@action pointerEntered = () => SnappingManager.GetIsDragging() && (this._background = '#b4b4b4');
@action pointerLeave = () => (this._background = 'inherit');
- textCallback = (char: string) => this.addNewTextDoc('-typed text-', false, true);
+ @undoBatch
+ addTextNote = (char: string) => this.addNewTextDoc('-typed text-', false, true);
// addNewTextDoc is called when a user starts typing in a column to create a new node
@action
@@ -272,7 +274,7 @@ export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColu
{!this.props.chromeHidden ? (
<div className="collectionNoteTakingView-DocumentButtons" style={{ marginBottom: 10 }}>
<div key={`${heading}-add-document`} className="collectionNoteTakingView-addDocumentButton">
- <EditableView GetValue={returnEmptyString} SetValue={this.addNewTextDoc} textCallback={this.textCallback} placeholder={"Type ':' for commands"} contents={'+ New Node'} menuCallback={this.menuCallback} />
+ <EditableView GetValue={returnEmptyString} SetValue={this.addNewTextDoc} textCallback={this.addTextNote} placeholder={"Type ':' for commands"} contents={'+ New Node'} menuCallback={this.menuCallback} />
</div>
<div key={`${this.props.Document[Id]}-addGroup`} className="collectionNoteTakingView-addDocumentButton">
<EditableView {...this.props.editableViewProps()} />
diff --git a/src/client/views/collections/CollectionPileView.tsx b/src/client/views/collections/CollectionPileView.tsx
index ea0fbbc54..bbd528e13 100644
--- a/src/client/views/collections/CollectionPileView.tsx
+++ b/src/client/views/collections/CollectionPileView.tsx
@@ -1,6 +1,7 @@
import { action, computed, IReactionDisposer, reaction } from 'mobx';
import { observer } from 'mobx-react';
-import { Doc, DocListCast, HeightSym, WidthSym } from '../../../fields/Doc';
+import { Doc, DocListCast } from '../../../fields/Doc';
+import { Height, Width } from '../../../fields/DocSymbols';
import { NumCast, StrCast } from '../../../fields/Types';
import { emptyFunction, returnFalse, returnTrue, setupMoveUpEvents } from '../../../Utils';
import { DocUtils } from '../../documents/Documents';
@@ -22,7 +23,7 @@ export class CollectionPileView extends CollectionSubView() {
componentDidMount() {
if (this.layoutEngine() !== computePassLayout.name && this.layoutEngine() !== computeStarburstLayout.name) {
- this.Document._pileLayoutEngine = computePassLayout.name;
+ this.Document._freeform_pileEngine = computePassLayout.name;
}
this._originalChrome = this.layoutDoc._chromeHidden;
this.layoutDoc._chromeHidden = true;
@@ -32,7 +33,7 @@ export class CollectionPileView extends CollectionSubView() {
Object.values(this._disposers).forEach(disposer => disposer?.());
}
- layoutEngine = () => StrCast(this.Document._pileLayoutEngine);
+ layoutEngine = () => StrCast(this.Document._freeform_pileEngine);
@undoBatch
addPileDoc = (doc: Doc | Doc[]) => {
@@ -51,20 +52,25 @@ export class CollectionPileView extends CollectionSubView() {
@computed get toggleIcon() {
return ScriptField.MakeScript('documentView.iconify()', { documentView: 'any' });
}
+ @computed get contentEvents() {
+ const isStarburst = this.layoutEngine() === computeStarburstLayout.name;
+ return this.props.isContentActive() && isStarburst ? undefined : 'none';
+ }
// returns the contents of the pileup in a CollectionFreeFormView
@computed get contents() {
- const isStarburst = this.layoutEngine() === computeStarburstLayout.name;
return (
- <div className="collectionPileView-innards" style={{ pointerEvents: isStarburst || SnappingManager.GetIsDragging() ? undefined : 'none' }}>
+ <div className="collectionPileView-innards" style={{ pointerEvents: this.contentEvents }}>
<CollectionFreeFormView
- {...this.props}
- childContentsActive={returnFalse}
+ {...this.props} //
layoutEngine={this.layoutEngine}
addDocument={this.addPileDoc}
- childCanEmbedOnDrag={true}
- childClickScript={this.toggleIcon}
moveDocument={this.removePileDoc}
+ // pile children never have their contents active, but will be document active whenever the entire pile is.
+ childContentsActive={returnFalse}
+ childDocumentsActive={this.props.isDocumentActive}
+ childDragAction="move"
+ childClickScript={this.toggleIcon}
/>
</div>
);
@@ -72,28 +78,30 @@ export class CollectionPileView extends CollectionSubView() {
// toggles the pileup between starburst to compact
toggleStarburst = action(() => {
+ this.layoutDoc._freeform_scale = undefined;
if (this.layoutEngine() === computeStarburstLayout.name) {
- if (this.rootDoc[WidthSym]() !== NumCast(this.rootDoc._starburstDiameter, 500)) {
- this.rootDoc._starburstDiameter = this.rootDoc[WidthSym]();
+ if (this.rootDoc[Width]() !== NumCast(this.rootDoc._starburstDiameter, 500)) {
+ this.rootDoc._starburstDiameter = this.rootDoc[Width]();
}
const defaultSize = 110;
- this.rootDoc.x = NumCast(this.rootDoc.x) + this.layoutDoc[WidthSym]() / 2 - NumCast(this.layoutDoc._starburstPileWidth, defaultSize) / 2;
- this.rootDoc.y = NumCast(this.rootDoc.y) + this.layoutDoc[HeightSym]() / 2 - NumCast(this.layoutDoc._starburstPileHeight, defaultSize) / 2;
- this.layoutDoc._width = NumCast(this.layoutDoc._starburstPileWidth, defaultSize);
- this.layoutDoc._height = NumCast(this.layoutDoc._starburstPileHeight, defaultSize);
+ this.rootDoc.x = NumCast(this.rootDoc.x) + this.layoutDoc[Width]() / 2 - NumCast(this.layoutDoc._freeform_pileWidth, defaultSize) / 2;
+ this.rootDoc.y = NumCast(this.rootDoc.y) + this.layoutDoc[Height]() / 2 - NumCast(this.layoutDoc._freeform_pileHeight, defaultSize) / 2;
+ this.layoutDoc._width = NumCast(this.layoutDoc._freeform_pileWidth, defaultSize);
+ this.layoutDoc._height = NumCast(this.layoutDoc._freeform_pileHeight, defaultSize);
DocUtils.pileup(this.childDocs, undefined, undefined, NumCast(this.layoutDoc._width) / 2, false);
this.layoutDoc._freeform_panX = 0;
this.layoutDoc._freeform_panY = -10;
- this.props.Document._pileLayoutEngine = computePassLayout.name;
+ this.props.Document._freeform_pileEngine = computePassLayout.name;
} else {
- const defaultSize = NumCast(this.rootDoc._starburstDiameter, 500);
- this.rootDoc.x = NumCast(this.rootDoc.x) + this.layoutDoc[WidthSym]() / 2 - defaultSize / 2;
- this.rootDoc.y = NumCast(this.rootDoc.y) + this.layoutDoc[HeightSym]() / 2 - defaultSize / 2;
- this.layoutDoc._starburstPileWidth = this.layoutDoc[WidthSym]();
- this.layoutDoc._starburstPileHeight = this.layoutDoc[HeightSym]();
+ const defaultSize = NumCast(this.rootDoc._starburstDiameter, 400);
+ this.rootDoc.x = NumCast(this.rootDoc.x) + this.layoutDoc[Width]() / 2 - defaultSize / 2;
+ this.rootDoc.y = NumCast(this.rootDoc.y) + this.layoutDoc[Height]() / 2 - defaultSize / 2;
+ this.layoutDoc._freeform_pileWidth = this.layoutDoc[Width]();
+ this.layoutDoc._freeform_pileHeight = this.layoutDoc[Height]();
this.layoutDoc._freeform_panX = this.layoutDoc._freeform_panY = 0;
this.layoutDoc._width = this.layoutDoc._height = defaultSize;
- this.props.Document._pileLayoutEngine = computeStarburstLayout.name;
+ this.layoutDoc.background;
+ this.props.Document._freeform_pileEngine = computeStarburstLayout.name;
}
});
@@ -101,7 +109,6 @@ export class CollectionPileView extends CollectionSubView() {
_undoBatch: UndoManager.Batch | undefined;
pointerDown = (e: React.PointerEvent) => {
let dist = 0;
- SnappingManager.SetIsDragging(true);
setupMoveUpEvents(
this,
e,
@@ -124,7 +131,6 @@ export class CollectionPileView extends CollectionSubView() {
() => {
this._undoBatch?.end();
this._undoBatch = undefined;
- SnappingManager.SetIsDragging(false);
},
emptyFunction,
e.shiftKey && this.layoutEngine() === computePassLayout.name,
diff --git a/src/client/views/collections/CollectionStackedTimeline.scss b/src/client/views/collections/CollectionStackedTimeline.scss
index a55b70e22..a19d8e696 100644
--- a/src/client/views/collections/CollectionStackedTimeline.scss
+++ b/src/client/views/collections/CollectionStackedTimeline.scss
@@ -104,7 +104,7 @@
.collectionStackedTimeline-left-resizer,
.collectionStackedTimeline-resizer {
- background: $medium-gray;
+ background: $dark-gray;
position: absolute;
top: 0;
height: 100%;
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index 6b4c8a3e9..ad84d859d 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -369,22 +369,22 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
// handles dragging and dropping markers in timeline
@action
internalDocDrop(e: Event, de: DragManager.DropEvent, docDragData: DragManager.DocumentDragData, xp: number) {
- if (!de.embedKey && this.props.Document._isGroup) return false;
- if (!super.onInternalDrop(e, de)) return false;
-
- // determine x coordinate of drop and assign it to the documents being dragged --- see internalDocDrop of collectionFreeFormView.tsx for how it's done when dropping onto a 2D freeform view
- const localPt = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y);
- const x = localPt[0] - docDragData.offset[0];
- const timelinePt = this.toTimeline(x + this._scroll, this.timelineContentWidth);
- docDragData.droppedDocuments.forEach(drop => {
- const anchorEnd = this.anchorEnd(drop);
- if (anchorEnd !== undefined) {
- Doc.SetInPlace(drop, drop._timecodeToHide === undefined ? this.props.endTag : 'timecodeToHide', timelinePt + anchorEnd - this.anchorStart(drop), false);
- }
- Doc.SetInPlace(drop, drop._timecodeToShow === undefined ? this.props.startTag : 'timecodeToShow', timelinePt, false);
- });
+ if (super.onInternalDrop(e, de)) {
+ // determine x coordinate of drop and assign it to the documents being dragged --- see internalDocDrop of collectionFreeFormView.tsx for how it's done when dropping onto a 2D freeform view
+ const localPt = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y);
+ const x = localPt[0] - docDragData.offset[0];
+ const timelinePt = this.toTimeline(x + this._scroll, this.timelineContentWidth);
+ docDragData.droppedDocuments.forEach(drop => {
+ const anchorEnd = this.anchorEnd(drop);
+ if (anchorEnd !== undefined) {
+ Doc.SetInPlace(drop, drop._timecodeToHide === undefined ? this.props.endTag : 'timecodeToHide', timelinePt + anchorEnd - this.anchorStart(drop), false);
+ }
+ Doc.SetInPlace(drop, drop._timecodeToShow === undefined ? this.props.startTag : 'timecodeToShow', timelinePt, false);
+ });
- return true;
+ return true;
+ }
+ return false;
}
onInternalDrop = (e: Event, de: DragManager.DropEvent) => {
@@ -403,15 +403,15 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
docAnchor ??
Docs.Create.LabelDocument({
title: ComputedField.MakeFunction(`self["${endTag}"] ? "#" + formatToTime(self["${startTag}"]) + "-" + formatToTime(self["${endTag}"]) : "#" + formatToTime(self["${startTag}"])`) as any,
- _minFontSize: 12,
- _maxFontSize: 24,
- _stayInCollection: true,
+ _label_minFontSize: 12,
+ _label_maxFontSize: 24,
+ _dragOnlyWithinContainer: true,
backgroundColor: 'rgba(128, 128, 128, 0.5)',
layout_hideLinkButton: true,
onClick: FollowLinkScript(),
annotationOn: rootDoc,
- _timelineLabel: true,
- borderRounding: anchorEndTime === undefined ? '100%' : undefined,
+ _isTimelineLabel: true,
+ layout_borderRounding: anchorEndTime === undefined ? '100%' : undefined,
});
Doc.GetProto(anchor)[startTag] = anchorStartTime;
Doc.GetProto(anchor)[endTag] = anchorEndTime;
@@ -527,6 +527,9 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
);
}
+ @computed get timelineEvents() {
+ return this.props.isContentActive() ? 'all' : this.props.isContentActive() === false ? 'none' : undefined;
+ }
render() {
const overlaps: {
anchorStartTime: number;
@@ -539,7 +542,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
}));
const maxLevel = overlaps.reduce((m, o) => Math.max(m, o.level), 0) + 2;
return this.clipDuration === 0 ? null : (
- <div ref={this.createDashEventsTarget} style={{ pointerEvents: SnappingManager.GetIsDragging() ? 'all' : undefined }}>
+ <div ref={this.createDashEventsTarget} style={{ pointerEvents: this.timelineEvents }}>
<div
className="collectionStackedTimeline-timelineContainer"
style={{ width: this.props.PanelWidth(), cursor: SnappingManager.GetIsDragging() ? 'grab' : '' }}
@@ -751,7 +754,7 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps>
if (timelineOnly) {
if (!left && time !== undefined && time <= NumCast(anchor[this.props.startTag])) time = undefined;
Doc.SetInPlace(anchor, left ? this.props.startTag : this.props.endTag, time, true);
- if (!left) Doc.SetInPlace(anchor, 'borderRounding', time !== undefined ? undefined : '100%', true);
+ if (!left) Doc.SetInPlace(anchor, 'layout_borderRounding', time !== undefined ? undefined : '100%', true);
} else {
anchor[left ? '_timecodeToShow' : '_timecodeToHide'] = time;
}
@@ -819,8 +822,8 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps>
focus={focusFunc}
isContentActive={returnFalse}
searchFilterDocs={returnEmptyDoclist}
- docFilters={returnEmptyFilter}
- docRangeFilters={returnEmptyFilter}
+ childFilters={returnEmptyFilter}
+ childFiltersByRanges={returnEmptyFilter}
rootSelected={returnFalse}
onClick={script}
onDoubleClick={this.props.layoutDoc.autoPlayAnchors ? undefined : doublescript}
diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss
index 99a68e94b..255bc3889 100644
--- a/src/client/views/collections/CollectionStackingView.scss
+++ b/src/client/views/collections/CollectionStackingView.scss
@@ -128,6 +128,7 @@
height: 15;
position: absolute;
margin-left: -5;
+ z-index: 10;
}
// Documents in stacking view
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 404ca5d65..e4a0d6dad 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -1,22 +1,23 @@
import React = require('react');
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { CursorProperty } from 'csstype';
-import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
+import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
-import { DataSym, Doc, HeightSym, Opt, WidthSym } from '../../../fields/Doc';
+import { Doc, Opt } from '../../../fields/Doc';
+import { DocData, Height, Width } from '../../../fields/DocSymbols';
import { Id } from '../../../fields/FieldSymbols';
import { List } from '../../../fields/List';
import { listSpec } from '../../../fields/Schema';
import { SchemaHeaderField } from '../../../fields/SchemaHeaderField';
import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
import { TraceMobx } from '../../../fields/util';
-import { emptyFunction, returnEmptyDoclist, returnFalse, returnNone, returnZero, setupMoveUpEvents, smoothScroll, Utils } from '../../../Utils';
+import { emptyFunction, lightOrDark, returnEmptyDoclist, returnFalse, returnNone, returnZero, setupMoveUpEvents, smoothScroll, Utils } from '../../../Utils';
import { Docs, DocUtils } from '../../documents/Documents';
import { CollectionViewType } from '../../documents/DocumentTypes';
import { DragManager, dropActionType } from '../../util/DragManager';
import { SnappingManager } from '../../util/SnappingManager';
import { Transform } from '../../util/Transform';
-import { undoBatch } from '../../util/UndoManager';
+import { undoBatch, UndoManager } from '../../util/UndoManager';
import { ContextMenu } from '../ContextMenu';
import { ContextMenuProps } from '../ContextMenuItem';
import { EditableView } from '../EditableView';
@@ -36,7 +37,7 @@ export type collectionStackingViewProps = {
sortFunc?: (a: Doc, b: Doc) => number;
chromeHidden?: boolean;
// view type is stacking
- viewType?: CollectionViewType;
+ type_collection?: CollectionViewType;
NativeWidth?: () => number;
NativeHeight?: () => number;
};
@@ -57,7 +58,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
// map of node headers to their heights. Used in Masonry
@observable _heightMap = new Map<string, number>();
// Assuming that this is the current css cursor style
- @observable _cursor: CursorProperty = 'grab';
+ @observable _cursor: CursorProperty = 'ew-resize';
// gets reset whenever we scroll. Not sure what it is
@observable _scroll = 0; // used to force the document decoration to update when scrolling
// does this mean whether the browser is hidden? Or is chrome something else entirely?
@@ -83,7 +84,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.HeaderMargin);
}
@computed get xMargin() {
- return NumCast(this.layoutDoc._xMargin, Math.min(3, 0.05 * this.props.PanelWidth()));
+ return NumCast(this.layoutDoc._xMargin, Math.max(3, 0.05 * this.props.PanelWidth()));
}
@computed get yMargin() {
return this.props.yPadding || NumCast(this.layoutDoc._yMargin, Math.min(5, 0.05 * this.props.PanelWidth()));
@@ -94,7 +95,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
}
// are we stacking or masonry?
@computed get isStackingView() {
- return (this.props.viewType ?? this.layoutDoc._viewType) === CollectionViewType.Stacking;
+ return (this.props.type_collection ?? this.layoutDoc._type_collection) === CollectionViewType.Stacking;
}
// this is the number of StackingViewFieldColumns that we have
@computed get numGroupColumns() {
@@ -285,7 +286,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
const layout_fieldKey = StrCast(docView.LayoutFieldKey);
const newDoc = Doc.MakeCopy(docView.rootDoc, true);
const dataField = docView.rootDoc[Doc.LayoutFieldKey(newDoc)];
- newDoc[DataSym][Doc.LayoutFieldKey(newDoc)] = dataField === undefined || Cast(dataField, listSpec(Doc), null)?.length !== undefined ? new List<Doc>([]) : undefined;
+ newDoc[DocData][Doc.LayoutFieldKey(newDoc)] = dataField === undefined || Cast(dataField, listSpec(Doc), null)?.length !== undefined ? new List<Doc>([]) : undefined;
if (layout_fieldKey !== 'layout' && docView.rootDoc[layout_fieldKey] instanceof Doc) {
newDoc[layout_fieldKey] = docView.rootDoc[layout_fieldKey];
}
@@ -294,7 +295,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
return this.addDocument?.(newDoc);
}
};
- isContentActive = () => (this.props.isSelected() || this.props.isContentActive() ? true : this.props.isSelected() === false || this.props.isContentActive() === false ? false : undefined);
+ isContentActive = () => (this.props.isContentActive() ? true : this.props.isSelected() === false || this.props.isContentActive() === false ? false : undefined);
@observable _renderCount = 5;
isChildContentActive = () =>
@@ -306,21 +307,20 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
? false
: undefined;
isChildButtonContentActive = () => (this.props.childDocumentsActive?.() === false || this.rootDoc.childDocumentsActive === false ? false : undefined);
+ @observable docRefs = new ObservableMap<Doc, DocumentView>();
// this is what renders the document that you see on the screen
// called in Children: this actually adds a document to our children list
getDisplayDoc(doc: Doc, width: () => number, count: number) {
const dataDoc = !doc.isTemplateDoc && !doc.isTemplateForField ? undefined : this.props.DataDoc;
const height = () => this.getDocHeight(doc);
- let dref: Opt<DocumentView>;
- const stackedDocTransform = () => this.getDocTransform(doc, dref);
+ const stackedDocTransform = () => this.getDocTransform(doc);
this._docXfs.push({ stackedDocTransform, width, height });
- //DocumentView is how the node will be rendered
return count > this._renderCount ? null : (
<DocumentView
- ref={r => (dref = r || undefined)}
+ ref={action((r: DocumentView) => r?.ContentDiv && this.docRefs.set(doc, r))}
Document={doc}
- DataDoc={dataDoc || (!Doc.AreProtosEqual(doc[DataSym], doc) && doc[DataSym])}
+ DataDoc={dataDoc ?? (!Doc.AreProtosEqual(doc[DocData], doc) ? doc[DocData] : undefined)}
renderDepth={this.props.renderDepth + 1}
PanelWidth={width}
PanelHeight={height}
@@ -340,16 +340,16 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
dontRegisterView={BoolCast(this.layoutDoc.childDontRegisterViews, this.props.dontRegisterView)} // used to be true if DataDoc existed, but template textboxes won't layout_autoHeight resize if dontRegisterView is set, but they need to.
rootSelected={this.rootSelected}
layout_showTitle={this.props.childlayout_showTitle}
- dropAction={StrCast(this.layoutDoc.childDropAction) as dropActionType}
+ dragAction={(this.layoutDoc.childDragAction ?? this.props.childDragAction) as dropActionType}
onClick={this.onChildClickHandler}
onDoubleClick={this.onChildDoubleClickHandler}
ScreenToLocalTransform={stackedDocTransform}
focus={this.focusDocument}
- docFilters={this.childDocFilters}
+ childFilters={this.childDocFilters}
hideDecorationTitle={this.props.childHideDecorationTitle?.()}
hideResizeHandles={this.props.childHideResizeHandles?.()}
hideTitle={this.props.childHideTitle?.()}
- docRangeFilters={this.childDocRangeFilters}
+ childFiltersByRanges={this.childDocRangeFilters}
searchFilterDocs={this.searchFilterDocs}
xPadding={NumCast(this.layoutDoc._childXPadding, this.props.childXPadding)}
yPadding={NumCast(this.layoutDoc._childYPadding, this.props.childYPadding)}
@@ -366,9 +366,10 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
);
}
- getDocTransform(doc: Doc, dref?: DocumentView) {
- const y = this._scroll; // required for document decorations to update when the text box container is scrolled
- const { scale, translateX, translateY } = Utils.GetScreenTransform(dref?.ContentDiv || undefined);
+ getDocTransform(doc: Doc) {
+ const dref = this.docRefs.get(doc);
+ this._scroll; // must be referenced for document decorations to update when the text box container is scrolled
+ const { translateX, translateY } = Utils.GetScreenTransform(dref?.ContentDiv);
// the document view may center its contents and if so, will prepend that onto the screenToLocalTansform. so we have to subtract that off
return new Transform(-translateX + (dref?.centeringX || 0), -translateY + (dref?.centeringY || 0), 1).scale(this.props.ScreenToLocalTransform().Scale);
}
@@ -377,7 +378,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
const childLayoutDoc = Doc.Layout(d, this.props.childLayoutTemplate?.());
const maxWidth = this.columnWidth / this.numGroupColumns;
if (!this.layoutDoc._columnsFill && !(childLayoutDoc._layout_fitWidth || this.props.childLayoutFitWidth?.(d))) {
- return Math.min(d[WidthSym](), maxWidth);
+ return Math.min(d[Width](), maxWidth);
}
return maxWidth;
}
@@ -386,8 +387,8 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
const childLayoutDoc = Doc.Layout(d, this.props.childLayoutTemplate?.());
const childDataDoc = !d.isTemplateDoc && !d.isTemplateForField ? undefined : this.props.DataDoc;
const maxHeight = (lim => (lim === 0 ? this.props.PanelWidth() : lim === -1 ? 10000 : lim))(NumCast(this.layoutDoc.childLimitHeight, -1));
- const nw = Doc.NativeWidth(childLayoutDoc, childDataDoc) || (!(childLayoutDoc._layout_fitWidth || this.props.childLayoutFitWidth?.(d)) ? d[WidthSym]() : 0);
- const nh = Doc.NativeHeight(childLayoutDoc, childDataDoc) || (!(childLayoutDoc._layout_fitWidth || this.props.childLayoutFitWidth?.(d)) ? d[HeightSym]() : 0);
+ const nw = Doc.NativeWidth(childLayoutDoc, childDataDoc) || (!(childLayoutDoc._layout_fitWidth || this.props.childLayoutFitWidth?.(d)) ? d[Width]() : 0);
+ const nh = Doc.NativeHeight(childLayoutDoc, childDataDoc) || (!(childLayoutDoc._layout_fitWidth || this.props.childLayoutFitWidth?.(d)) ? d[Height]() : 0);
if (nw && nh) {
const colWid = this.columnWidth / (this.isStackingView ? this.numGroupColumns : 1);
const docWid = this.layoutDoc._columnsFill ? colWid : Math.min(this.getDocWidth(d), colWid);
@@ -401,11 +402,15 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
// This following three functions must be from the view Mehek showed
columnDividerDown = (e: React.PointerEvent) => {
runInAction(() => (this._cursor = 'grabbing'));
+ const batch = UndoManager.StartBatch('stacking width');
setupMoveUpEvents(
this,
e,
this.onDividerMove,
- action(() => (this._cursor = 'grab')),
+ action(() => {
+ this._cursor = 'ew-resize';
+ batch.end();
+ }),
emptyFunction
);
};
@@ -417,7 +422,11 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
@computed get columnDragger() {
return (
- <div className="collectionStackingView-columnDragger" onPointerDown={this.columnDividerDown} ref={this._draggerRef} style={{ cursor: this._cursor, left: `${this.columnWidth + this.xMargin}px`, top: `${Math.max(0, this.yMargin - 9)}px` }}>
+ <div
+ className="collectionStackingView-columnDragger"
+ onPointerDown={this.columnDividerDown}
+ ref={this._draggerRef}
+ style={{ cursor: this._cursor, color: StrCast(Doc.UserDoc().userColor), left: `${this.columnWidth + this.xMargin}px`, top: `${Math.max(0, this.yMargin - 9)}px` }}>
<FontAwesomeIcon icon={'arrows-alt-h'} />
</div>
);
@@ -454,18 +463,24 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
const docs = this.childDocList;
// still figuring out where to add the document
if (docs && newDocs.length) {
+ newDocs.forEach(newdoc => docs.indexOf(newdoc) !== -1 && docs.splice(docs.indexOf(newdoc), 1));
const insertInd = dropInd === -1 ? docs.length : dropInd + dropAfter;
const offset = newDocs.reduce((off, ndoc) => (this.filteredChildren.find((fdoc, i) => ndoc === fdoc && i < insertInd) ? off + 1 : off), 0);
newDocs.filter(ndoc => docs.indexOf(ndoc) !== -1).forEach(ndoc => docs.splice(docs.indexOf(ndoc), 1));
docs.splice(insertInd - offset, 0, ...newDocs);
}
+ return true;
}
} else if (de.complete.linkDragData?.dragDocument.embedContainer === this.props.Document && de.complete.linkDragData?.linkDragView?.props.CollectionFreeFormDocumentView?.()) {
const source = Docs.Create.TextDocument('', { _width: 200, _height: 75, _layout_fitWidth: true, title: 'dropped annotation' });
- this.props.addDocument?.(source);
+ if (!this.props.addDocument?.(source)) e.preventDefault();
de.complete.linkDocument = DocUtils.MakeLink(source, de.complete.linkDragData.linkSourceGetAnchor(), { link_relationship: 'doc annotation' }); // TODODO this is where in text links get passed
e.stopPropagation();
- } else if (de.complete.annoDragData?.dragDocument && super.onInternalDrop(e, de)) return this.internalAnchorAnnoDrop(e, de.complete.annoDragData);
+ return true;
+ } else if (de.complete.annoDragData?.dragDocument && super.onInternalDrop(e, de)) {
+ return this.internalAnchorAnnoDrop(e, de.complete.annoDragData);
+ }
+ e.preventDefault();
return false;
};
@@ -583,7 +598,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
action((entries: any) => {
if (this.layoutDoc._layout_autoHeight && ref && this.refList.length && !SnappingManager.GetIsDragging()) {
const height = this.refList.reduce((p, r) => p + Number(getComputedStyle(r).height.replace('px', '')), 0);
- this.props.setHeight?.(this.headerMargin + height);
+ this.props.setHeight?.(2 * this.headerMargin + height); // bcz: added 2x for header to fix problem with scrollbars appearing in Tools panel
}
})
);
@@ -649,12 +664,11 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
return35 = () => 35;
@computed get buttonMenu() {
- const menuDoc: Doc = Cast(this.rootDoc.buttonMenuDoc, Doc, null);
+ const menuDoc: Doc = Cast(this.rootDoc.layout_headerButton, Doc, null);
// TODO:glr Allow support for multiple buttons
if (menuDoc) {
const width: number = NumCast(menuDoc._width, 30);
const height: number = NumCast(menuDoc._height, 30);
- console.log(menuDoc.title, width, height);
return (
<div className="buttonMenu-docBtn" style={{ width: width, height: height }}>
<DocumentView
@@ -678,8 +692,8 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
docViewPath={returnEmptyDoclist}
whenChildContentsActiveChanged={emptyFunction}
bringToFront={emptyFunction}
- docFilters={this.props.docFilters}
- docRangeFilters={this.props.docRangeFilters}
+ childFilters={this.props.childFilters}
+ childFiltersByRanges={this.props.childFiltersByRanges}
searchFilterDocs={this.props.searchFilterDocs}
/>
</div>
@@ -699,7 +713,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
}
@computed get backgroundEvents() {
- return SnappingManager.GetIsDragging();
+ return this.props.isContentActive() === false ? 'none' : undefined;
}
observer: any;
render() {
@@ -709,8 +723,8 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
SetValue: this.addGroup,
contents: '+ ADD A GROUP',
};
- const buttonMenu = this.rootDoc.buttonMenu;
- const noviceExplainer = this.rootDoc.explainer;
+ const buttonMenu = this.rootDoc.layout_headerButton;
+ const noviceExplainer = this.rootDoc.layout_explainer;
return (
<>
{buttonMenu || noviceExplainer ? (
@@ -726,7 +740,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
style={{
overflowY: this.isContentActive() ? 'auto' : 'hidden',
background: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor),
- pointerEvents: (this.props.pointerEvents?.() as any) ?? (this.backgroundEvents ? 'all' : undefined),
+ pointerEvents: (this.props.pointerEvents?.() as any) ?? this.backgroundEvents,
}}
onScroll={action(e => (this._scroll = e.currentTarget.scrollTop))}
onDrop={this.onExternalDrop.bind(this)}
diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
index 243550c0b..ebb4ba5a1 100644
--- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
+++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
@@ -94,6 +94,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
columnDrop = action((e: Event, de: DragManager.DropEvent) => {
const drop = { docs: de.complete.docDragData?.droppedDocuments, val: this.getValue(this._heading) };
this.props.pivotField && drop.docs?.forEach(d => Doc.SetInPlace(d, this.props.pivotField, drop.val, false));
+ return true;
});
getValue = (value: string): any => {
const parsed = parseInt(value);
@@ -128,7 +129,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
@action pointerEntered = () => SnappingManager.GetIsDragging() && (this._background = '#b4b4b4');
@action pointerLeave = () => (this._background = 'inherit');
- textCallback = (char: string) => this.addNewTextDoc('-typed text-', false, true);
+ @undoBatch typedNote = (char: string) => this.addNewTextDoc('-typed text-', false, true);
@action
addNewTextDoc = (value: string, shiftDown?: boolean, forceEmptyNote?: boolean) => {
@@ -363,7 +364,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
<EditableView
GetValue={returnEmptyString}
SetValue={this.addNewTextDoc}
- textCallback={this.textCallback}
+ textCallback={this.typedNote}
placeholder={"Type ':' for commands"}
contents={<FontAwesomeIcon icon={'plus'} />}
menuCallback={this.menuCallback}
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 008de1944..c189ef126 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -1,11 +1,11 @@
import { action, computed, observable } from 'mobx';
import * as rp from 'request-promise';
import CursorField from '../../../fields/CursorField';
-import { AclPrivate, Doc, DocListCast, Field, Opt, StrListCast } from '../../../fields/Doc';
+import { Doc, DocListCast, Field, Opt, StrListCast } from '../../../fields/Doc';
+import { AclPrivate } from '../../../fields/DocSymbols';
import { Id } from '../../../fields/FieldSymbols';
import { List } from '../../../fields/List';
import { listSpec } from '../../../fields/Schema';
-import { ScriptField } from '../../../fields/ScriptField';
import { Cast, ScriptCast, StrCast } from '../../../fields/Types';
import { WebField } from '../../../fields/URLField';
import { GestureUtils } from '../../../pen-gestures/GestureUtils';
@@ -28,8 +28,8 @@ export function CollectionSubView<X>(moreProps?: X) {
private gestureDisposer?: GestureUtils.GestureEventDisposer;
protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
protected _mainCont?: HTMLDivElement;
- @observable _focusFilters: Opt<string[]>; // docFilters that are overridden when previewing a link to an anchor which has docFilters set on it
- @observable _focusRangeFilters: Opt<string[]>; // docRangeFilters that are overridden when previewing a link to an anchor which has docRangeFilters set on it
+ @observable _focusFilters: Opt<string[]>; // childFilters that are overridden when previewing a link to an anchor which has childFilters set on it
+ @observable _focusRangeFilters: Opt<string[]>; // childFiltersByRanges that are overridden when previewing a link to an anchor which has childFiltersByRanges set on it
protected createDashEventsTarget = (ele: HTMLDivElement | null) => {
//used for stacking and masonry view
this.dropDisposer?.();
@@ -65,11 +65,7 @@ export function CollectionSubView<X>(moreProps?: X) {
// to its children which may be templates.
// If 'annotationField' is specified, then all children exist on that field of the extension document, otherwise, they exist directly on the data document under 'fieldKey'
@computed get dataField() {
- if (this.layoutDoc[this.props.fieldKey]) return this.layoutDoc[this.props.fieldKey];
- // sets the dataDoc's data field to an empty list if the data field is undefined - prevents issues with addonly
- // setTimeout changes it outside of the @computed section
- !this.dataDoc[this.props.fieldKey] && setTimeout(() => !this.dataDoc[this.props.fieldKey] && (this.dataDoc[this.props.fieldKey] = new List<Doc>()));
- return this.dataDoc[this.props.fieldKey];
+ return this.layoutDoc[this.props.fieldKey];
}
get childLayoutPairs(): { layout: Doc; data: Doc }[] {
@@ -85,13 +81,13 @@ export function CollectionSubView<X>(moreProps?: X) {
get childDocList() {
return Cast(this.dataField, listSpec(Doc));
}
- collectionFilters = () => this._focusFilters ?? StrListCast(this.props.Document._docFilters);
- collectionRangeDocFilters = () => this._focusRangeFilters ?? Cast(this.props.Document._docRangeFilters, listSpec('string'), []);
+ collectionFilters = () => this._focusFilters ?? StrListCast(this.props.Document._childFilters);
+ collectionRangeDocFilters = () => this._focusRangeFilters ?? Cast(this.props.Document._childFiltersByRanges, listSpec('string'), []);
// child filters apply to the descendants of the documents in this collection
- childDocFilters = () => [...(this.props.docFilters?.().filter(f => Utils.IsRecursiveFilter(f)) || []), ...this.collectionFilters()];
+ childDocFilters = () => [...(this.props.childFilters?.().filter(f => Utils.IsRecursiveFilter(f)) || []), ...this.collectionFilters()];
// unrecursive filters apply to the documents in the collection, but no their children. See Utils.noRecursionHack
- unrecursiveDocFilters = () => [...(this.props.docFilters?.().filter(f => !Utils.IsRecursiveFilter(f)) || [])];
- childDocRangeFilters = () => [...(this.props.docRangeFilters?.() || []), ...this.collectionRangeDocFilters()];
+ unrecursiveDocFilters = () => [...(this.props.childFilters?.().filter(f => !Utils.IsRecursiveFilter(f)) || [])];
+ childDocRangeFilters = () => [...(this.props.childFiltersByRanges?.() || []), ...this.collectionRangeDocFilters()];
searchFilterDocs = () => this.props.searchFilterDocs?.() ?? DocListCast(this.props.Document._searchFilterDocs);
@computed.struct get childDocs() {
TraceMobx();
@@ -110,38 +106,41 @@ export function CollectionSubView<X>(moreProps?: X) {
rawdocs = rootDoc && !this.props.isAnnotationOverlay ? [Doc.GetProto(rootDoc)] : [];
}
- const childDocs = rawdocs.filter(d => !(d instanceof Promise) && GetEffectiveAcl(Doc.GetProto(d)) !== AclPrivate && (this.props.ignoreUnrendered || !d.unrendered)).map(d => d as Doc);
+ const childDocs = rawdocs.filter(d => !(d instanceof Promise) && GetEffectiveAcl(Doc.GetProto(d)) !== AclPrivate && (this.props.ignoreUnrendered || !d.layout_unrendered)).map(d => d as Doc);
const childDocFilters = this.childDocFilters();
- const docRangeFilters = this.childDocRangeFilters();
+ const childFiltersByRanges = this.childDocRangeFilters();
const searchDocs = this.searchFilterDocs();
- if (this.props.Document.dontRegisterView || (!childDocFilters.length && !this.unrecursiveDocFilters().length && !docRangeFilters.length && !searchDocs.length)) {
+ if (this.props.Document.dontRegisterView || (!childDocFilters.length && !this.unrecursiveDocFilters().length && !childFiltersByRanges.length && !searchDocs.length)) {
return childDocs.filter(cd => !cd.cookies); // remove any documents that require a cookie if there are no filters to provide one
}
const docsforFilter: Doc[] = [];
childDocs.forEach(d => {
// dragging facets
- const dragged = this.props.docFilters?.().some(f => f.includes(Utils.noDragsDocFilter));
+ const dragged = this.props.childFilters?.().some(f => f.includes(Utils.noDragsDocFilter));
if (dragged && DragManager.docsBeingDragged.includes(d)) return false;
- let notFiltered = d.z || Doc.IsSystem(d) || DocUtils.FilterDocs([d], this.unrecursiveDocFilters(), docRangeFilters, this.props.Document).length > 0;
+ let notFiltered = d.z || Doc.IsSystem(d) || DocUtils.FilterDocs([d], this.unrecursiveDocFilters(), childFiltersByRanges, this.props.Document).length > 0;
if (notFiltered) {
- notFiltered = (!searchDocs.length || searchDocs.includes(d)) && DocUtils.FilterDocs([d], childDocFilters, docRangeFilters, this.props.Document).length > 0;
+ notFiltered = (!searchDocs.length || searchDocs.includes(d)) && DocUtils.FilterDocs([d], childDocFilters, childFiltersByRanges, this.props.Document).length > 0;
const fieldKey = Doc.LayoutFieldKey(d);
const annos = !Field.toString(Doc.LayoutField(d) as Field).includes(CollectionView.name);
const data = d[annos ? fieldKey + '_annotations' : fieldKey];
- if (data !== undefined) {
- let subDocs = DocListCast(data);
+ const side = annos && d[fieldKey + '_sidebar'];
+ if (data !== undefined || side !== undefined) {
+ let subDocs = [...DocListCast(data), ...DocListCast(side)];
if (subDocs.length > 0) {
let newarray: Doc[] = [];
- notFiltered = notFiltered || (!searchDocs.length && DocUtils.FilterDocs(subDocs, childDocFilters, docRangeFilters, d).length);
+ notFiltered = notFiltered || (!searchDocs.length && DocUtils.FilterDocs(subDocs, childDocFilters, childFiltersByRanges, d).length);
while (subDocs.length > 0 && !notFiltered) {
newarray = [];
subDocs.forEach(t => {
const fieldKey = Doc.LayoutFieldKey(t);
const annos = !Field.toString(Doc.LayoutField(t) as Field).includes(CollectionView.name);
- notFiltered = notFiltered || ((!searchDocs.length || searchDocs.includes(t)) && ((!childDocFilters.length && !docRangeFilters.length) || DocUtils.FilterDocs([t], childDocFilters, docRangeFilters, d).length));
+ notFiltered =
+ notFiltered || ((!searchDocs.length || searchDocs.includes(t)) && ((!childDocFilters.length && !childFiltersByRanges.length) || DocUtils.FilterDocs([t], childDocFilters, childFiltersByRanges, d).length));
DocListCast(t[annos ? fieldKey + '_annotations' : fieldKey]).forEach(newdoc => newarray.push(newdoc));
+ annos && DocListCast(t[fieldKey + '_sidebar']).forEach(newdoc => newarray.push(newdoc));
});
subDocs = newarray;
}
@@ -186,12 +185,14 @@ export function CollectionSubView<X>(moreProps?: X) {
@undoBatch
protected onGesture(e: Event, ge: GestureUtils.GestureEvent) {}
- protected onInternalPreDrop(e: Event, de: DragManager.DropEvent, targetAction: dropActionType) {
+ protected onInternalPreDrop(e: Event, de: DragManager.DropEvent) {
if (de.complete.docDragData) {
- // if targetDropAction is, say 'embed', but we're just dragging within a collection, we want to ignore the targetAction.
- // otherwise, the targetAction should become the actual action (which can still be overridden by the userDropAction -eg, shift/ctrl keys)
- if (targetAction && !de.complete.docDragData.draggedDocuments.some(d => d.embedContainer === this.props.Document && this.childDocs.includes(d))) {
- de.complete.docDragData.dropAction = targetAction;
+ // override the dropEvent's dropAction
+ const dropAction = this.layoutDoc.dropAction as dropActionType;
+ // if the dropEvent's dragAction is, say 'embed', but we're just dragging within a collection, we may not actually want to make an embedding.
+ // so we check if our collection has a dropAction set on it and if so, we use that instead.
+ if (dropAction && !de.complete.docDragData.draggedDocuments.some(d => d.embedContainer === this.props.Document && this.childDocs.includes(d))) {
+ de.complete.docDragData.dropAction = dropAction;
}
e.stopPropagation();
}
@@ -204,7 +205,7 @@ export function CollectionSubView<X>(moreProps?: X) {
protected onInternalDrop(e: Event, de: DragManager.DropEvent): boolean {
const docDragData = de.complete.docDragData;
if (docDragData) {
- let added = false;
+ let added = undefined;
const dropAction = docDragData.dropAction || docDragData.userDropAction;
const targetDocments = DocListCast(this.dataDoc[this.props.fieldKey]);
const someMoved = !dropAction && docDragData.draggedDocuments.some(drag => targetDocments.includes(drag));
@@ -213,21 +214,21 @@ export function CollectionSubView<X>(moreProps?: X) {
const movedDocs = docDragData.droppedDocuments.filter((d, i) => docDragData.draggedDocuments[i] === d);
const addedDocs = docDragData.droppedDocuments.filter((d, i) => docDragData.draggedDocuments[i] !== d);
if (movedDocs.length) {
- const canAdd = this.props.Document._viewType === CollectionViewType.Pile || de.embedKey || this.props.Document.allowOverlayDrop || Doc.AreProtosEqual(Cast(movedDocs[0].annotationOn, Doc, null), this.props.Document);
- added = docDragData.moveDocument(movedDocs, this.props.Document, canAdd ? this.addDocument : returnFalse);
+ const canAdd = de.embedKey || dropAction || Doc.AreProtosEqual(Cast(movedDocs[0].annotationOn, Doc, null), this.rootDoc);
+ const moved = docDragData.moveDocument(movedDocs, this.rootDoc, canAdd ? this.addDocument : returnFalse);
+ added = canAdd || moved ? moved : undefined;
} else {
- ScriptCast(this.props.Document.dropConverter)?.script.run({ dragData: docDragData });
+ ScriptCast(this.rootDoc.dropConverter)?.script.run({ dragData: docDragData });
added = addedDocs.length ? this.addDocument(addedDocs) : true;
}
- added && e.stopPropagation();
- return added;
} else {
- ScriptCast(this.props.Document.dropConverter)?.script.run({ dragData: docDragData });
+ ScriptCast(this.rootDoc.dropConverter)?.script.run({ dragData: docDragData });
added = this.addDocument(docDragData.droppedDocuments);
+ !added && alert('You cannot perform this move');
}
- !added && alert('You cannot perform this move');
- e.stopPropagation();
- return added;
+ added === false && !this.props.isAnnotationOverlay && e.preventDefault();
+ added === true && e.stopPropagation();
+ return added ? true : false;
} else if (de.complete.annoDragData) {
const dropCreator = de.complete.annoDragData.dropDocCreator;
de.complete.annoDragData.dropDocCreator = () => {
@@ -448,7 +449,7 @@ export function CollectionSubView<X>(moreProps?: X) {
}
if (generatedDocuments.length) {
// Creating a dash document
- const isFreeformView = this.props.Document._viewType === CollectionViewType.Freeform;
+ const isFreeformView = this.props.Document._type_collection === CollectionViewType.Freeform;
const set = !isFreeformView
? generatedDocuments
: generatedDocuments.length > 1
diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx
index 6135154c3..192d48c64 100644
--- a/src/client/views/collections/CollectionTimeView.tsx
+++ b/src/client/views/collections/CollectionTimeView.tsx
@@ -34,8 +34,6 @@ export class CollectionTimeView extends CollectionSubView() {
async componentDidMount() {
this.props.setContentView?.(this);
- //const detailView = (await DocCastAsync(this.props.Document.childClickedOpenTemplateView)) || DocUtils.findTemplate("detailView", StrCast(this.rootDoc.type), "");
- ///const childText = "const embedding = getEmbedding(self); switchView(embedding, detailView); embedding.dropAction='embed'; useRightSplit(embedding, shiftKey); ";
runInAction(() => {
this._childClickedScript = ScriptField.MakeScript('openInLightbox(self)', { this: Doc.name });
this._viewDefDivClick = ScriptField.MakeScript('pivotColumnClick(this,payload)', { payload: 'any' });
@@ -47,11 +45,11 @@ export class CollectionTimeView extends CollectionSubView() {
}
getAnchor = (addAsAnnotation: boolean) => {
- const anchor = Docs.Create.HTMLAnchorDocument([], {
+ const anchor = Docs.Create.HTMLMarkerDocument([], {
title: ComputedField.MakeFunction(`"${this.pivotField}"])`) as any,
annotationOn: this.rootDoc,
});
- PresBox.pinDocView(anchor, { pinData: { viewType: true, pivot: true, filters: true } }, this.rootDoc);
+ PresBox.pinDocView(anchor, { pinData: { type_collection: true, pivot: true, filters: true } }, this.rootDoc);
if (addAsAnnotation) {
// when added as an annotation, links to anchors can be found as links to the document even if the anchors are not rendered
@@ -125,8 +123,8 @@ export class CollectionTimeView extends CollectionSubView() {
goTo = (prevFilterIndex: number) => {
this.layoutDoc._pivotField = this.layoutDoc['_prevPivotFields' + prevFilterIndex];
- this.layoutDoc._docFilters = ObjectField.MakeCopy(this.layoutDoc['_prevDocFilter' + prevFilterIndex] as ObjectField);
- this.layoutDoc._docRangeFilters = ObjectField.MakeCopy(this.layoutDoc['_prevDocRangeFilters' + prevFilterIndex] as ObjectField);
+ this.layoutDoc._childFilters = ObjectField.MakeCopy(this.layoutDoc['_prevDocFilter' + prevFilterIndex] as ObjectField);
+ this.layoutDoc._childFiltersByRanges = ObjectField.MakeCopy(this.layoutDoc['_prevDocRangeFilters' + prevFilterIndex] as ObjectField);
this.layoutDoc._prevFilterIndex = prevFilterIndex;
};
@@ -136,7 +134,7 @@ export class CollectionTimeView extends CollectionSubView() {
if (prevFilterIndex > 0) {
this.goTo(prevFilterIndex - 1);
} else {
- this.layoutDoc._docFilters = new List([]);
+ this.layoutDoc._childFilters = new List([]);
}
};
@@ -145,7 +143,7 @@ export class CollectionTimeView extends CollectionSubView() {
<div className="collectionTimeView-innards" key="timeline" style={{ pointerEvents: this.props.isContentActive() ? undefined : 'none' }} onClick={this.contentsDown}>
<CollectionFreeFormView
{...this.props}
- engineProps={{ pivotField: this.pivotField, docFilters: this.childDocFilters, docRangeFilters: this.childDocRangeFilters }}
+ engineProps={{ pivotField: this.pivotField, childFilters: this.childDocFilters, childFiltersByRanges: this.childDocRangeFilters }}
fitContentsToBox={returnTrue}
childClickScript={this._childClickedScript}
viewDefDivClick={this.layoutEngine() === computeTimelineLayout.name ? undefined : this._viewDefDivClick}
@@ -276,12 +274,12 @@ export class CollectionTimeView extends CollectionSubView() {
ScriptingGlobals.add(function pivotColumnClick(pivotDoc: Doc, bounds: ViewDefBounds) {
const pivotField = StrCast(pivotDoc._pivotField, 'author');
let prevFilterIndex = NumCast(pivotDoc._prevFilterIndex);
- const originalFilter = StrListCast(ObjectField.MakeCopy(pivotDoc._docFilters as ObjectField));
- pivotDoc['_prevDocFilter' + prevFilterIndex] = ObjectField.MakeCopy(pivotDoc._docFilters as ObjectField);
- pivotDoc['_prevDocRangeFilters' + prevFilterIndex] = ObjectField.MakeCopy(pivotDoc._docRangeFilters as ObjectField);
+ const originalFilter = StrListCast(ObjectField.MakeCopy(pivotDoc._childFilters as ObjectField));
+ pivotDoc['_prevDocFilter' + prevFilterIndex] = ObjectField.MakeCopy(pivotDoc._childFilters as ObjectField);
+ pivotDoc['_prevDocRangeFilters' + prevFilterIndex] = ObjectField.MakeCopy(pivotDoc._childFiltersByRanges as ObjectField);
pivotDoc['_prevPivotFields' + prevFilterIndex] = pivotField;
pivotDoc._prevFilterIndex = ++prevFilterIndex;
- pivotDoc._docFilters = new List();
+ pivotDoc._childFilters = new List();
setTimeout(
action(() => {
const filterVals = bounds.payload as string[];
@@ -292,7 +290,7 @@ ScriptingGlobals.add(function pivotColumnClick(pivotDoc: Doc, bounds: ViewDefBou
pivotDoc._pivotField = filterVals[0];
}
}
- const newFilters = StrListCast(pivotDoc._docFilters);
+ const newFilters = StrListCast(pivotDoc._childFilters);
if (newFilters.length && originalFilter.length && newFilters.lastElement() === originalFilter.lastElement()) {
pivotDoc._prevFilterIndex = --prevFilterIndex;
pivotDoc['_prevDocFilter' + prevFilterIndex] = undefined;
diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss
index 273b08247..2bf649caf 100644
--- a/src/client/views/collections/CollectionTreeView.scss
+++ b/src/client/views/collections/CollectionTreeView.scss
@@ -13,7 +13,7 @@
width: 100%;
position: relative;
top: 0;
- background: $light-gray;
+ // background: $light-gray;
font-size: 13px;
overflow: auto;
user-select: none;
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 095e34c39..ea3b5065f 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -1,8 +1,8 @@
import { action, computed, IReactionDisposer, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
-import { DataSym, Doc, DocListCast, HeightSym, Opt, StrListCast, WidthSym } from '../../../fields/Doc';
+import { Doc, DocListCast, Opt, StrListCast } from '../../../fields/Doc';
+import { DocData, Height, Width } from '../../../fields/DocSymbols';
import { Id } from '../../../fields/FieldSymbols';
-import { InkTool } from '../../../fields/InkField';
import { listSpec } from '../../../fields/Schema';
import { ScriptField } from '../../../fields/ScriptField';
import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
@@ -89,8 +89,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
// these should stay in synch with counterparts in DocComponent.ts ViewBoxAnnotatableComponent
@observable _isAnyChildContentActive = false;
whenChildContentsActiveChanged = action((isActive: boolean) => this.props.whenChildContentsActiveChanged((this._isAnyChildContentActive = isActive)));
- isContentActive = (outsideReaction?: boolean) =>
- Doc.ActiveTool !== InkTool.None || this.props.isContentActive?.() || this.props.Document.forceActive || this.props.isSelected(outsideReaction) || this._isAnyChildContentActive || this.props.rootSelected(outsideReaction) ? true : false;
+ isContentActive = (outsideReaction?: boolean) => (this._isAnyChildContentActive ? true : this.props.isContentActive() ? true : false);
componentWillUnmount() {
this._isDisposing = true;
@@ -141,11 +140,17 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
if ((this._mainEle = ele)) this._treedropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.doc, this.onInternalPreDrop.bind(this));
};
- protected onInternalPreDrop = (e: Event, de: DragManager.DropEvent, targetAction: dropActionType) => {
+ protected onInternalPreDrop = (e: Event, de: DragManager.DropEvent) => {
+ const dropAction = this.layoutDoc.dropAction as dropActionType;
const dragData = de.complete.docDragData;
if (dragData) {
- const isInTree = () => Doc.AreProtosEqual(dragData.treeViewDoc, this.props.Document) || dragData.draggedDocuments.some(d => d.embedContainer === this.doc && this.childDocs.includes(d));
- dragData.dropAction = targetAction && !isInTree() ? targetAction : this.doc === dragData?.treeViewDoc ? 'same' : dragData.dropAction;
+ const sameTree = Doc.AreProtosEqual(dragData.treeViewDoc, this.rootDoc) ? true : false;
+ const isAlreadyInTree = () => sameTree || dragData.draggedDocuments.some(d => d.embedContainer === this.doc && this.childDocs.includes(d));
+ if (isAlreadyInTree() !== sameTree) {
+ console.log('WHAAAT');
+ }
+ dragData.dropAction = dropAction && !isAlreadyInTree() ? dropAction : sameTree ? 'same' : dragData.dropAction;
+ e.stopPropagation();
}
};
@@ -154,15 +159,15 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
@action
remove = (doc: Doc | Doc[]): boolean => {
const docs = doc instanceof Doc ? [doc] : doc;
- const targetDataDoc = this.doc[DataSym];
+ const targetDataDoc = this.doc[DocData];
const value = DocListCast(targetDataDoc[this.props.fieldKey]);
const result = value.filter(v => !docs.includes(v));
if ((doc instanceof Doc ? [doc] : doc).some(doc => SelectionManager.Views().some(dv => Doc.AreProtosEqual(dv.rootDoc, doc)))) SelectionManager.DeselectAll();
- if (result.length !== value.length) {
- const ind = targetDataDoc[this.props.fieldKey].indexOf(doc);
- const prev = ind && targetDataDoc[this.props.fieldKey][ind - 1];
+ if (result.length !== value.length && doc instanceof Doc) {
+ const ind = DocListCast(targetDataDoc[this.props.fieldKey]).indexOf(doc);
+ const prev = ind && DocListCast(targetDataDoc[this.props.fieldKey])[ind - 1];
this.props.removeDocument?.(doc);
- if (ind > 0) {
+ if (ind > 0 && prev) {
FormattedTextBox.SelectOnLoad = prev[Id];
DocumentManager.Instance.getDocumentView(prev, this.props.DocumentView?.())?.select(false);
}
@@ -175,8 +180,8 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
addDoc = (docs: Doc | Doc[], relativeTo: Opt<Doc>, before?: boolean): boolean => {
const doAddDoc = (doc: Doc | Doc[]) =>
(doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => {
- const res = flg && Doc.AddDocToList(this.doc[DataSym], this.props.fieldKey, doc, relativeTo, before);
- res && (doc.embedContainer = this.props.Document);
+ const res = flg && Doc.AddDocToList(this.doc[DocData], this.props.fieldKey, doc, relativeTo, before);
+ res && Doc.SetContainer(doc, this.props.Document);
return res;
}, true);
if (this.doc.resolvedDataDoc instanceof Promise) return false;
@@ -239,8 +244,8 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
PanelHeight={this.documentTitleHeight}
NativeDimScaling={returnOne}
onKey={this.onKey}
- docFilters={returnEmptyFilter}
- docRangeFilters={returnEmptyFilter}
+ childFilters={returnEmptyFilter}
+ childFiltersByRanges={returnEmptyFilter}
searchFilterDocs={returnEmptyDoclist}
addDocument={returnFalse}
moveDocument={returnFalse}
@@ -260,7 +265,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
@observable _renderCount = 1;
@computed get treeViewElements() {
TraceMobx();
- const dropAction = StrCast(this.doc.childDropAction) as dropActionType;
+ const dragAction = StrCast(this.doc.childDragAction) as dropActionType;
const addDoc = (doc: Doc | Doc[], relativeTo?: Doc, before?: boolean) => this.addDoc(doc, relativeTo, before);
const moveDoc = (d: Doc | Doc[], target: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => this.props.moveDocument?.(d, target, addDoc) || false;
if (this._renderCount < this.treeChildren.length) setTimeout(action(() => (this._renderCount = Math.min(this.treeChildren.length, this._renderCount + 20))));
@@ -275,7 +280,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
addDoc,
this.remove,
moveDoc,
- dropAction,
+ dragAction,
this.props.addDocTab,
this.props.styleProvider,
this.screenToLocalTransform,
@@ -313,12 +318,12 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
}
@computed get noviceExplainer() {
- return !Doc.noviceMode || !this.rootDoc.explainer ? null : <div className="documentExplanation"> {StrCast(this.rootDoc.explainer)} </div>;
+ return !Doc.noviceMode || !this.rootDoc.layout_explainer ? null : <div className="documentExplanation"> {StrCast(this.rootDoc.layout_explainer)} </div>;
}
return35 = () => 35;
@computed get buttonMenu() {
- const menuDoc = Cast(this.rootDoc.buttonMenuDoc, Doc, null);
+ const menuDoc = Cast(this.rootDoc.layout_headerButton, Doc, null);
// To create a multibutton menu add a CollectionLinearView
return !menuDoc ? null : (
<div className="buttonMenu-docBtn" style={{ width: NumCast(menuDoc._width, 30), height: NumCast(menuDoc._height, 30) }}>
@@ -342,8 +347,8 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
docViewPath={returnEmptyDoclist}
whenChildContentsActiveChanged={emptyFunction}
bringToFront={emptyFunction}
- docFilters={this.props.docFilters}
- docRangeFilters={this.props.docRangeFilters}
+ childFilters={this.props.childFilters}
+ childFiltersByRanges={this.props.childFiltersByRanges}
searchFilterDocs={this.props.searchFilterDocs}
/>
</div>
@@ -368,8 +373,8 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
marginX = () => NumCast(this.doc._xMargin);
marginTop = () => NumCast(this.doc._yMargin);
marginBot = () => NumCast(this.doc._yMargin);
- documentTitleWidth = () => Math.min(this.layoutDoc?.[WidthSym](), this.panelWidth());
- documentTitleHeight = () => (this.layoutDoc?.[HeightSym]() || 0) - NumCast(this.layoutDoc.layout_autoHeightMargins);
+ documentTitleWidth = () => Math.min(this.layoutDoc?.[Width](), this.panelWidth());
+ documentTitleHeight = () => (this.layoutDoc?.[Height]() || 0) - NumCast(this.layoutDoc.layout_autoHeightMargins);
truncateTitleWidth = () => this.treeViewtruncateTitleWidth;
onChildClick = () => this.props.onChildClick?.() || ScriptCast(this.doc.onChildClick);
panelWidth = () => Math.max(0, this.props.PanelWidth() - 2 * this.marginX() * (this.props.NativeDimScaling?.() || 1));
@@ -382,7 +387,8 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
@observable _headerHeight = 0;
@computed get content() {
const background = () => this.props.styleProvider?.(this.doc, this.props, StyleProp.BackgroundColor);
- const pointerEvents = () => (!this.props.isContentActive() && !SnappingManager.GetIsDragging() ? 'none' : undefined);
+ const color = () => this.props.styleProvider?.(this.doc, this.props, StyleProp.Color);
+ const pointerEvents = () => (this.props.isContentActive() === false ? 'none' : undefined);
const titleBar = this.props.treeViewHideTitle || this.doc.treeViewHideTitle ? null : this.titleBar;
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%', pointerEvents: 'all' }}>
@@ -395,8 +401,10 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
<div
className="collectionTreeView-contents"
key="tree"
+ ref={r => !this.doc.treeViewHasOverlay && r && this.createTreeDropTarget(r)}
style={{
...(!titleBar ? { marginLeft: this.marginX(), paddingTop: this.marginTop() } : {}),
+ color: color(),
overflow: 'auto',
width: '100%',
height: '100%',
@@ -418,8 +426,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
minHeight: '100%',
}}
onWheel={e => e.stopPropagation()}
- onDrop={this.onTreeDrop}
- ref={r => !this.doc.treeViewHasOverlay && r && this.createTreeDropTarget(r)}>
+ onDrop={this.onTreeDrop}>
<ul className={`no-indent${this.outlineMode ? '-outline' : ''}`}>{this.treeViewElements}</ul>
</div>
</div>
@@ -439,12 +446,12 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
setContentView={emptyFunction}
NativeWidth={returnZero}
NativeHeight={returnZero}
- pointerEvents={SnappingManager.GetIsDragging() ? returnAll : returnNone}
+ pointerEvents={this.props.isContentActive() && SnappingManager.GetIsDragging() ? returnAll : returnNone}
isAnnotationOverlay={true}
isAnnotationOverlayScrollable={true}
childDocumentsActive={this.props.isDocumentActive}
fieldKey={this.props.fieldKey + '_annotations'}
- dropAction={'move'}
+ dropAction="move"
select={emptyFunction}
addDocument={this.addAnnotationDocument}
removeDocument={this.remAnnotationDocument}
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index dd7bf1740..c33519afb 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -9,6 +9,7 @@ import { TraceMobx } from '../../../fields/util';
import { returnEmptyString } from '../../../Utils';
import { DocUtils } from '../../documents/Documents';
import { CollectionViewType } from '../../documents/DocumentTypes';
+import { dropActionType } from '../../util/DragManager';
import { ImageUtils } from '../../util/Import & Export/ImageUtils';
import { InteractionUtils } from '../../util/InteractionUtils';
import { ContextMenu } from '../ContextMenu';
@@ -55,7 +56,7 @@ interface CollectionViewProps_ extends FieldViewProps {
childHideDecorationTitle?: () => boolean;
childHideResizeHandles?: () => boolean;
childLayoutTemplate?: () => Doc | undefined; // specify a layout Doc template to use for children of the collection
- childCanEmbedOnDrag?: boolean;
+ childDragAction?: dropActionType;
childXPadding?: number;
childYPadding?: number;
childLayoutString?: string;
@@ -87,7 +88,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
}
get collectionViewType(): CollectionViewType | undefined {
- const viewField = StrCast(this.layoutDoc._viewType);
+ const viewField = StrCast(this.layoutDoc._type_collection);
if (CollectionView._safeMode) {
switch (viewField) {
case CollectionViewType.Freeform:
@@ -127,8 +128,8 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
}
};
- setupViewTypes(category: string, func: (viewType: CollectionViewType) => Doc) {
- if (!Doc.IsSystem(this.rootDoc) && this.rootDoc._viewType !== CollectionViewType.Docking && !this.rootDoc.isGroup && !this.rootDoc.annotationOn) {
+ setupViewTypes(category: string, func: (type_collection: CollectionViewType) => Doc) {
+ if (!Doc.IsSystem(this.rootDoc) && this.rootDoc._type_collection !== CollectionViewType.Docking && !this.rootDoc.isGroup && !this.rootDoc.annotationOn) {
// prettier-ignore
const subItems: ContextMenuProps[] = [
{ description: 'Freeform', event: () => func(CollectionViewType.Freeform), icon: 'signature' },
@@ -160,7 +161,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
!Doc.noviceMode &&
this.setupViewTypes('UI Controls...', vtype => {
const newRendition = Doc.MakeEmbedding(this.rootDoc);
- newRendition._viewType = vtype;
+ newRendition._type_collection = vtype;
this.props.addDocTab(newRendition, OpenWhere.addRight);
return newRendition;
});
@@ -220,7 +221,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
childHideResizeHandles = () => this.props.childHideResizeHandles?.() ?? BoolCast(this.Document.childHideResizeHandles);
childHideDecorationTitle = () => this.props.childHideDecorationTitle?.() ?? BoolCast(this.Document.childHideDecorationTitle);
childLayoutTemplate = () => this.props.childLayoutTemplate?.() || Cast(this.rootDoc.childLayoutTemplate, Doc, null);
- isContentActive = (outsideReaction?: boolean) => this.props.isContentActive() || this.isAnyChildContentActive();
+ isContentActive = (outsideReaction?: boolean) => (this.isAnyChildContentActive() ? true : this.props.isContentActive());
render() {
TraceMobx();
@@ -244,7 +245,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
<div
className="collectionView"
onContextMenu={this.onContextMenu}
- style={{ pointerEvents: this.props.DocumentView?.()?.props.docViewPath().lastElement()?.rootDoc?._viewType === CollectionViewType.Freeform && this.rootDoc._lockedPosition ? 'none' : undefined }}>
+ style={{ pointerEvents: this.props.DocumentView?.()?.props.docViewPath().lastElement()?.rootDoc?._type_collection === CollectionViewType.Freeform && this.rootDoc._lockedPosition ? 'none' : undefined }}>
{this.renderSubView(this.collectionViewType, props)}
</div>
);
diff --git a/src/client/views/collections/TabDocView.scss b/src/client/views/collections/TabDocView.scss
index 58605c3f4..13bb3a577 100644
--- a/src/client/views/collections/TabDocView.scss
+++ b/src/client/views/collections/TabDocView.scss
@@ -65,11 +65,7 @@ input.lm_title {
}
.miniMap {
- position: absolute;
overflow: hidden;
- right: 15;
- bottom: 15;
- border: solid 1px;
width: 100%;
height: 100%;
transition: all 0.5s;
@@ -90,18 +86,5 @@ input.lm_title {
cursor: pointer;
position: absolute;
bottom: 5;
- display: flex;
right: 5;
- width: 25px;
- height: 25px;
- border-radius: 3px;
- padding: 2px;
- justify-content: center;
- align-items: center;
- align-content: center;
- background-color: $light-gray;
-
- &:hover {
- box-shadow: none;
- }
}
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index 69963736b..d787f5262 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -2,25 +2,24 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tooltip } from '@material-ui/core';
import { clamp } from 'lodash';
-import { action, computed, IReactionDisposer, observable, reaction } from 'mobx';
+import { action, computed, IReactionDisposer, observable, ObservableSet, reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as ReactDOM from 'react-dom/client';
-import { DataSym, Doc, HeightSym, Opt, WidthSym } from '../../../fields/Doc';
+import { Doc, Opt } from '../../../fields/Doc';
+import { DocData, Height, Width } from '../../../fields/DocSymbols';
import { Id } from '../../../fields/FieldSymbols';
import { List } from '../../../fields/List';
import { FieldId } from '../../../fields/RefField';
-import { listSpec } from '../../../fields/Schema';
import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../fields/Types';
import { emptyFunction, lightOrDark, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents, simulateMouseClick, Utils } from '../../../Utils';
import { DocServer } from '../../DocServer';
-import { DocUtils } from '../../documents/Documents';
import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
import { DocumentManager } from '../../util/DocumentManager';
import { DragManager, dropActionType } from '../../util/DragManager';
import { SelectionManager } from '../../util/SelectionManager';
import { SnappingManager } from '../../util/SnappingManager';
import { Transform } from '../../util/Transform';
-import { undoBatch, UndoManager } from '../../util/UndoManager';
+import { undoable, UndoManager } from '../../util/UndoManager';
import { DashboardView } from '../DashboardView';
import { Colors, Shadows } from '../global/globalEnums';
import { LightboxView } from '../LightboxView';
@@ -35,6 +34,7 @@ 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 {
@@ -44,12 +44,21 @@ interface TabDocViewProps {
}
@observer
export class TabDocView extends React.Component<TabDocViewProps> {
+ static _allTabs = new ObservableSet<TabDocView>();
_mainCont: HTMLDivElement | null = null;
_tabReaction: IReactionDisposer | undefined;
@observable _activated: boolean = false;
@observable _panelWidth = 0;
@observable _panelHeight = 0;
+ @observable _hovering = false;
@observable _isActive: boolean = false;
+ @observable _isAnyChildContentActive = false;
+ @computed get _isUserActivated() {
+ return SelectionManager.Views().some(view => view.rootDoc === this._document) || this._isAnyChildContentActive;
+ }
+ @computed get _isContentActive() {
+ return this._isUserActivated || this._hovering;
+ }
@observable _document: Doc | undefined;
@observable _view: DocumentView | undefined;
@@ -62,9 +71,7 @@ export class TabDocView extends React.Component<TabDocViewProps> {
return 'transparent';
}
@computed get tabColor() {
- let tabColor = StrCast(this._document?._backgroundColor, StrCast(this._document?.backgroundColor, DefaultStyleProvider(this._document, undefined, StyleProp.BackgroundColor)));
- if (tabColor === 'transparent') return 'black';
- return tabColor;
+ return this._isUserActivated ? Colors.WHITE : this._hovering ? Colors.LIGHT_GRAY : Colors.MEDIUM_GRAY;
}
@computed get tabTextColor() {
return this._document?.type === DocumentType.PRES ? 'black' : StrCast(this._document?._color, StrCast(this._document?.color, DefaultStyleProvider(this._document, undefined, StyleProp.Color)));
@@ -116,15 +123,13 @@ export class TabDocView extends React.Component<TabDocViewProps> {
titleEle.size = StrCast(doc.title).length + 3;
titleEle.value = doc.title;
- titleEle.onkeydown = (e: KeyboardEvent) => {
- e.stopPropagation();
- };
- titleEle.onchange = undoBatch(
- action((e: any) => {
+ titleEle.onkeydown = (e: KeyboardEvent) => e.stopPropagation();
+ titleEle.onchange = (e: any) => {
+ undoable(() => {
titleEle.size = e.currentTarget.value.length + 3;
Doc.GetProto(doc).title = e.currentTarget.value;
- })
- );
+ }, 'edit tab title')();
+ };
if (tab.element[0].children[1].children.length === 1) {
iconWrap.className = 'lm_iconWrap lm_moreInfo';
@@ -194,11 +199,13 @@ export class TabDocView extends React.Component<TabDocViewProps> {
}
});
tab._disposers.selectionDisposer = reaction(
- () => SelectionManager.Views().some(v => v.topMost && v.props.Document === doc),
+ () => SelectionManager.Views().some(view => view.rootDoc === this._document),
action(selected => {
if (selected) this._activated = true;
const toggle = tab.element[0].children[2].children[0] as HTMLInputElement;
- selected && tab.contentItem !== tab.header.parent.getActiveContentItem() && UndoManager.RunInBatch(() => tab.header.parent.setActiveContentItem(tab.contentItem), 'tab switch');
+ if (selected && tab.contentItem !== tab.header.parent.getActiveContentItem()) {
+ undoable(() => tab.header.parent.setActiveContentItem(tab.contentItem), 'tab switch')();
+ }
toggle.style.fontWeight = selected ? 'bold' : '';
// toggle.style.textTransform = selected ? "uppercase" : "";
}),
@@ -234,7 +241,7 @@ export class TabDocView extends React.Component<TabDocViewProps> {
public static PinDoc(docs: Doc | Doc[], pinProps: PinProps) {
const docList = docs instanceof Doc ? [docs] : docs;
- const batch = UndoManager.StartBatch('pinning doc');
+ const batch = UndoManager.StartBatch('Pin doc to pres trail');
const curPres = Doc.ActivePresentation ?? Doc.MakeCopy(Doc.UserDoc().emptyTrail as Doc, true);
if (!Doc.ActivePresentation) {
@@ -256,7 +263,7 @@ export class TabDocView extends React.Component<TabDocViewProps> {
pinDoc.presMovement = doc.type === DocumentType.SCRIPTING || pinProps?.pinDocLayout ? PresMovement.None : PresMovement.Zoom;
pinDoc.presDuration = pinDoc.presDuration ?? 1000;
pinDoc.groupWithUp = false;
- pinDoc.embedContainer = curPres;
+ Doc.SetContainer(pinDoc, curPres);
// these should potentially all be props passed down by the CollectionTreeView to the TreeView elements. That way the PresBox could configure all of its children at render time
pinDoc.treeViewRenderAsBulletHeader = true; // forces a tree view to render the document next to the bullet in the header area
pinDoc.treeViewHeaderWidth = '100%'; // forces the header to grow to be the same size as its largest sibling.
@@ -294,18 +301,19 @@ export class TabDocView extends React.Component<TabDocViewProps> {
pinDoc && PresBox.Instance?.addToSelectedArray(pinDoc); //Update selected array
});
if (
+ // open the presentation trail if it's not already opened
!Array.from(CollectionDockingView.Instance?.tabMap ?? [])
.map(d => d.DashDoc)
.includes(curPres)
) {
- const docs = Cast(Doc.MyOverlayDocs.data, listSpec(Doc), []);
- if (docs.includes(curPres)) docs.splice(docs.indexOf(curPres), 1);
+ if (Doc.IsInMyOverlay(curPres)) Doc.RemFromMyOverlay(curPres);
CollectionDockingView.AddSplit(curPres, OpenWhereMod.right);
setTimeout(() => DocumentManager.Instance.showDocument(docList.lastElement(), { willPan: true }), 100); // keeps the pinned doc in view since the sidebar shifts things
}
setTimeout(batch.end, 500); // need to wait until dockingview (goldenlayout) updates all its structurs
}
+ @action
componentDidMount() {
new _global.ResizeObserver(
action((entries: any) => {
@@ -320,14 +328,17 @@ export class TabDocView extends React.Component<TabDocViewProps> {
// this._tabReaction = reaction(() => ({ selected: this.active(), title: this.tab?.titleElement[0] }),
// ({ selected, title }) => title && (title.style.backgroundColor = selected ? "white" : ""),
// { fireImmediately: true });
+ TabDocView._allTabs.add(this);
}
componentDidUpdate() {
this._view && DocumentManager.Instance.AddView(this._view);
}
+ @action
componentWillUnmount() {
this._tabReaction?.();
this._view && DocumentManager.Instance.RemoveView(this._view);
+ TabDocView._allTabs.delete(this);
this.props.glContainer.layoutManager.off('activeContentItemChanged', this.onActiveContentItemChanged);
}
@@ -336,6 +347,7 @@ export class TabDocView extends React.Component<TabDocViewProps> {
private onActiveContentItemChanged(contentItem: any) {
if (!contentItem || (this.stack === contentItem.parent && ((contentItem?.tab === this.tab && !this._isActive) || (contentItem?.tab !== this.tab && this._isActive)))) {
this._activated = this._isActive = !contentItem || contentItem?.tab === this.tab;
+ if (!this._view) setTimeout(() => SelectionManager.SelectView(this._view, false));
!this._isActive && this._document && Doc.UnBrushDoc(this._document); // bcz: bad -- trying to simulate a pointer leave event when a new tab is opened up on top of an existing one.
}
}
@@ -349,10 +361,10 @@ export class TabDocView extends React.Component<TabDocViewProps> {
// lightbox - will add the document to any collection along the path from the document to the docking view that has a field isLightbox. if none is found, it adds to the full screen lightbox
addDocTab = (doc: Doc, location: OpenWhere) => {
SelectionManager.DeselectAll();
- const whereFields = doc._viewType === CollectionViewType.Docking ? [OpenWhere.dashboard] : location.split(':');
+ const whereFields = location.split(':');
const keyValue = whereFields[1]?.includes('KeyValue');
const whereMods: OpenWhereMod = whereFields.length > 1 ? (whereFields[1].replace('KeyValue', '') as OpenWhereMod) : OpenWhereMod.none;
- if (doc.dockingConfig) return DashboardView.openDashboard(doc);
+ if (doc.dockingConfig && !keyValue) return DashboardView.openDashboard(doc);
// prettier-ignore
switch (whereFields[0]) {
case undefined:
@@ -365,8 +377,6 @@ export class TabDocView extends React.Component<TabDocViewProps> {
}
}
return LightboxView.AddDocTab(doc, location);
- case OpenWhere.dashboard: return DashboardView.openDashboard(doc);
- case OpenWhere.fullScreen: return CollectionDockingView.OpenFullScreen(doc);
case OpenWhere.close: return CollectionDockingView.CloseSplit(doc, whereMods);
case OpenWhere.replace: return CollectionDockingView.ReplaceTab(doc, whereMods, this.stack, undefined, keyValue);
case OpenWhere.toggle: return CollectionDockingView.ToggleSplit(doc, whereMods, this.stack, undefined, keyValue);
@@ -383,10 +393,10 @@ export class TabDocView extends React.Component<TabDocViewProps> {
};
getCurrentFrame = () => {
- return NumCast(Cast(PresBox.Instance.childDocs[PresBox.Instance.itemIndex].presentationTargetDoc, Doc, null)._currentFrame);
+ return NumCast(Cast(PresBox.Instance.activeItem.presentationTargetDoc, Doc, null)._currentFrame);
};
static Activate = (tabDoc: Doc) => {
- const tab = Array.from(CollectionDockingView.Instance?.tabMap!).find(tab => tab.DashDoc === tabDoc);
+ const tab = Array.from(CollectionDockingView.Instance?.tabMap!).find(tab => tab.DashDoc === tabDoc && !tab.contentItem.config.props.keyValue);
tab?.header.parent.setActiveContentItem(tab.contentItem); // glr: Panning does not work when this is set - (this line is for trying to make a tab that is not topmost become topmost)
return tab !== undefined;
};
@@ -406,11 +416,11 @@ export class TabDocView extends React.Component<TabDocViewProps> {
};
PanelWidth = () => this._panelWidth;
PanelHeight = () => this._panelHeight;
- miniMapColor = () => this.tabColor;
+ miniMapColor = () => Colors.MEDIUM_GRAY;
tabView = () => this._view;
- disableMinimap = () => !this._document || this._document.layout !== CollectionView.LayoutString(Doc.LayoutFieldKey(this._document)) || this._document?._viewType !== CollectionViewType.Freeform;
- hideMinimap = () => this.disableMinimap() || BoolCast(this._document?.layout_hideMinimap);
-
+ 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;
@computed get docView() {
return !this._activated || !this._document ? null : (
<>
@@ -425,16 +435,16 @@ export class TabDocView extends React.Component<TabDocViewProps> {
LayoutTemplateString={this.props.keyValue ? KeyValueBox.LayoutString() : undefined}
hideTitle={this.props.keyValue}
Document={this._document}
- DataDoc={!Doc.AreProtosEqual(this._document[DataSym], this._document) ? this._document[DataSym] : undefined}
+ DataDoc={!Doc.AreProtosEqual(this._document[DocData], this._document) ? this._document[DocData] : undefined}
onBrowseClick={MainView.Instance.exploreMode}
waitForDoubleClickToClick={MainView.Instance.waitForDoubleClick}
- isContentActive={returnTrue}
+ isContentActive={this.isContentActive}
isDocumentActive={returnFalse}
PanelWidth={this.PanelWidth}
PanelHeight={this.PanelHeight}
styleProvider={DefaultStyleProvider}
- docFilters={CollectionDockingView.Instance?.childDocFilters ?? returnEmptyDoclist}
- docRangeFilters={CollectionDockingView.Instance?.childDocRangeFilters ?? returnEmptyDoclist}
+ childFilters={CollectionDockingView.Instance?.childDocFilters ?? returnEmptyDoclist}
+ childFiltersByRanges={CollectionDockingView.Instance?.childDocRangeFilters ?? returnEmptyDoclist}
searchFilterDocs={CollectionDockingView.Instance?.searchFilterDocs ?? returnEmptyDoclist}
addDocument={undefined}
removeDocument={this.remDocTab}
@@ -442,30 +452,15 @@ export class TabDocView extends React.Component<TabDocViewProps> {
ScreenToLocalTransform={this.ScreenToLocalTransform}
dontCenter={'y'}
rootSelected={returnTrue}
- whenChildContentsActiveChanged={emptyFunction}
+ whenChildContentsActiveChanged={this.whenChildContentActiveChanges}
focus={this.focusFunc}
docViewPath={returnEmptyDoclist}
bringToFront={emptyFunction}
pinToPres={TabDocView.PinDoc}
/>
- <TabMinimapView key="minimap" hideMinimap={this.hideMinimap} addDocTab={this.addDocTab} PanelHeight={this.PanelHeight} PanelWidth={this.PanelWidth} background={this.miniMapColor} document={this._document} tabView={this.tabView} />
- <Tooltip key="ttip" title={<div className="dash-tooltip">{this._document.layout_hideMinimap ? 'Open minimap' : 'Close minimap'}</div>}>
- <div
- className="miniMap-hidden"
- style={{
- display: this.disableMinimap() || this._document._viewType !== 'freeform' ? 'none' : undefined,
- color: this._document.layout_hideMinimap ? Colors.BLACK : Colors.WHITE,
- backgroundColor: this._document.layout_hideMinimap ? Colors.LIGHT_GRAY : Colors.MEDIUM_BLUE,
- boxShadow: this._document.layout_hideMinimap ? Shadows.STANDARD_SHADOW : undefined,
- }}
- onPointerDown={e => e.stopPropagation()}
- onClick={action(e => {
- e.stopPropagation();
- this._document!.layout_hideMinimap = !this._document!.layout_hideMinimap;
- })}>
- <FontAwesomeIcon icon={'globe-asia'} size="lg" />
- </div>
- </Tooltip>
+ {this.disableMinimap() || this._document._type_collection !== CollectionViewType.Freeform ? null : (
+ <TabMinimapView key="minimap" addDocTab={this.addDocTab} PanelHeight={this.PanelHeight} PanelWidth={this.PanelWidth} background={this.miniMapColor} document={this._document} tabView={this.tabView} />
+ )}
</>
);
}
@@ -477,6 +472,10 @@ export class TabDocView extends React.Component<TabDocViewProps> {
style={{
fontFamily: Doc.UserDoc().renderStyle === 'comic' ? 'Comic Sans MS' : undefined,
}}
+ onPointerEnter={action(() => (this._hovering = true))}
+ onPointerLeave={action(() => (this._hovering = false))}
+ onDragOver={action(() => (this._hovering = true))}
+ onDragLeave={action(() => (this._hovering = false))}
ref={ref => {
if ((this._mainCont = ref)) {
if (this._lastTab) {
@@ -496,7 +495,6 @@ export class TabDocView extends React.Component<TabDocViewProps> {
interface TabMinimapViewProps {
document: Doc;
- hideMinimap: () => boolean;
tabView: () => DocumentView | undefined;
addDocTab: (doc: Doc, where: OpenWhere) => boolean;
PanelWidth: () => number;
@@ -535,7 +533,7 @@ export class TabMinimapView extends React.Component<TabMinimapViewProps> {
return 'gray';
}
})(doc.type as DocumentType);
- return !background ? undefined : <div style={{ width: doc[WidthSym](), height: doc[HeightSym](), position: 'absolute', display: 'block', background }} />;
+ return !background ? undefined : <div style={{ width: doc[Width](), height: doc[Height](), position: 'absolute', display: 'block', background }} />;
}
}
};
@@ -571,46 +569,56 @@ export class TabMinimapView extends React.Component<TabMinimapViewProps> {
if (!this.renderBounds) return null;
const miniWidth = (this.props.PanelWidth() / NumCast(this.props.document._freeform_scale, 1) / this.renderBounds.dim) * 100;
const miniHeight = (this.props.PanelHeight() / NumCast(this.props.document._freeform_scale, 1) / this.renderBounds.dim) * 100;
- const miniLeft = 50 + ((NumCast(this.props.document._freeform_) - this.renderBounds.cx) / this.renderBounds.dim) * 100 - miniWidth / 2;
+ const miniLeft = 50 + ((NumCast(this.props.document._freeform_panX) - this.renderBounds.cx) / this.renderBounds.dim) * 100 - miniWidth / 2;
const miniTop = 50 + ((NumCast(this.props.document._freeform_panY) - this.renderBounds.cy) / this.renderBounds.dim) * 100 - miniHeight / 2;
const miniSize = this.returnMiniSize();
- return this.props.hideMinimap() ? null : (
- <div className="miniMap" style={{ width: miniSize, height: miniSize, background: this.props.background() }}>
- <CollectionFreeFormView
- Document={this.props.document}
- docViewPath={returnEmptyDoclist}
- childLayoutTemplate={this.childLayoutTemplate} // bcz: Ugh .. should probably be rendering a CollectionView or the minimap should be part of the collectionFreeFormView to avoid having to set stuff like this.
- noOverlay={true} // don't render overlay Docs since they won't scale
- setHeight={returnFalse}
- isContentActive={emptyFunction}
- isAnyChildContentActive={returnFalse}
- select={emptyFunction}
- dropAction={undefined}
- isSelected={returnFalse}
- dontRegisterView={true}
- fieldKey={Doc.LayoutFieldKey(this.props.document)}
- bringToFront={emptyFunction}
- rootSelected={returnTrue}
- addDocument={returnFalse}
- moveDocument={returnFalse}
- removeDocument={returnFalse}
- PanelWidth={this.returnMiniSize}
- PanelHeight={this.returnMiniSize}
- ScreenToLocalTransform={Transform.Identity}
- renderDepth={0}
- whenChildContentsActiveChanged={emptyFunction}
- focus={emptyFunction}
- styleProvider={TabMinimapView.miniStyleProvider}
- addDocTab={this.props.addDocTab}
- pinToPres={TabDocView.PinDoc}
- docFilters={CollectionDockingView.Instance?.childDocFilters ?? returnEmptyDoclist}
- docRangeFilters={CollectionDockingView.Instance?.childDocRangeFilters ?? returnEmptyDoclist}
- searchFilterDocs={CollectionDockingView.Instance?.searchFilterDocs ?? returnEmptyDoclist}
- fitContentsToBox={returnTrue}
+ return (
+ <div className="miniMap-hidden">
+ <Popup
+ icon={<FontAwesomeIcon icon="globe-asia" size="lg" />}
+ color={StrCast(Doc.UserDoc().userVariantColor, Colors.MEDIUM_BLUE)}
+ type={Type.TERT}
+ onPointerDown={e => e.stopPropagation()}
+ placement={'top-end'}
+ popup={
+ <div className="miniMap" style={{ width: miniSize, height: miniSize, background: this.props.background() }}>
+ <CollectionFreeFormView
+ Document={this.props.document}
+ docViewPath={returnEmptyDoclist}
+ childLayoutTemplate={this.childLayoutTemplate} // bcz: Ugh .. should probably be rendering a CollectionView or the minimap should be part of the collectionFreeFormView to avoid having to set stuff like this.
+ noOverlay={true} // don't render overlay Docs since they won't scale
+ setHeight={returnFalse}
+ isContentActive={emptyFunction}
+ isAnyChildContentActive={returnFalse}
+ select={emptyFunction}
+ isSelected={returnFalse}
+ dontRegisterView={true}
+ fieldKey={Doc.LayoutFieldKey(this.props.document)}
+ bringToFront={emptyFunction}
+ rootSelected={returnTrue}
+ addDocument={returnFalse}
+ moveDocument={returnFalse}
+ removeDocument={returnFalse}
+ PanelWidth={this.returnMiniSize}
+ PanelHeight={this.returnMiniSize}
+ ScreenToLocalTransform={Transform.Identity}
+ renderDepth={0}
+ whenChildContentsActiveChanged={emptyFunction}
+ focus={emptyFunction}
+ styleProvider={TabMinimapView.miniStyleProvider}
+ addDocTab={this.props.addDocTab}
+ pinToPres={TabDocView.PinDoc}
+ childFilters={CollectionDockingView.Instance?.childDocFilters ?? returnEmptyDoclist}
+ childFiltersByRanges={CollectionDockingView.Instance?.childDocRangeFilters ?? returnEmptyDoclist}
+ searchFilterDocs={CollectionDockingView.Instance?.searchFilterDocs ?? returnEmptyDoclist}
+ fitContentsToBox={returnTrue}
+ />
+ <div className="miniOverlay" onPointerDown={this.miniDown}>
+ <div className="miniThumb" style={{ width: `${miniWidth}% `, height: `${miniHeight}% `, left: `${miniLeft}% `, top: `${miniTop}% ` }} />
+ </div>
+ </div>
+ }
/>
- <div className="miniOverlay" onPointerDown={this.miniDown}>
- <div className="miniThumb" style={{ width: `${miniWidth}% `, height: `${miniHeight}% `, left: `${miniLeft}% `, top: `${miniTop}% ` }} />
- </div>
</div>
);
}
diff --git a/src/client/views/collections/TreeView.scss b/src/client/views/collections/TreeView.scss
index 7eab03e1d..d22e85880 100644
--- a/src/client/views/collections/TreeView.scss
+++ b/src/client/views/collections/TreeView.scss
@@ -13,7 +13,7 @@
.treeView-container-active {
.bullet-outline {
position: relative;
- width: $TREE_BULLET_WIDTH;
+ width: fit-content;
color: $medium-gray;
transform: scale(0.5);
display: inline-flex;
@@ -21,37 +21,20 @@
}
.treeView-bulletIcons {
- // width: $TREE_BULLET_WIDTH;
width: 100%;
height: 100%;
- position: absolute;
-
- .treeView-expandIcon {
- display: none;
- left: -10px;
- position: absolute;
- }
-
- .treeView-checkIcon {
- left: 3.5px;
- top: 2px;
- position: absolute;
- }
-
- &:hover {
- .treeView-expandIcon {
- display: unset;
- }
- }
+ position: relative;
+ display: flex;
+ flex-direction: row;
}
- .treeView-bulletIcons:hover img {
- left: 14px;
- position: absolute;
- transform-origin: center left;
- transform: scale(6);
- pointer-events: none;
- }
+ // .treeView-bulletIcons:hover img {
+ // left: 14px;
+ // position: absolute;
+ // transform-origin: center left;
+ // transform: scale(6);
+ // pointer-events: none;
+ // }
.bullet {
grid-column: 1;
@@ -59,11 +42,12 @@
justify-content: center;
align-items: center;
position: relative;
- width: $TREE_BULLET_WIDTH;
+ width: fit-content;
min-height: 20px;
color: $medium-gray;
border: #80808030 1px solid;
border-radius: 5px;
+ z-index: 1;
}
}
@@ -71,9 +55,6 @@
position: absolute;
height: max-content;
pointer-events: none;
- color: white;
- border-radius: 4px;
- font-size: 10px;
}
.treeView-container-active {
@@ -121,10 +102,26 @@
align-items: center;
width: max-content;
border-radius: 5px;
+ overflow: hidden;
+ position: relative;
+ z-index: 1;
+
+ .treeView-background {
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ top: 0;
+ left: 0;
+ z-index: 0;
+ filter: opacity(0);
+ }
&:hover {
- background-color: #bdddf5;
+ .treeView-background {
+ filter: opacity(0.2) !important;
+ }
}
+
//align-items: center;
::-webkit-scrollbar {
@@ -157,6 +154,7 @@
opacity: 0.75;
pointer-events: all;
cursor: pointer;
+ z-index: 1;
> svg {
margin-left: 0.25rem;
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index 92b70cb5a..25a547066 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -2,7 +2,8 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed, IReactionDisposer, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
-import { DataSym, Doc, DocListCast, Field, HeightSym, Opt, StrListCast, WidthSym } from '../../../fields/Doc';
+import { Doc, DocListCast, Field, Opt, StrListCast } from '../../../fields/Doc';
+import { DocData, Height, Width } from '../../../fields/DocSymbols';
import { Id } from '../../../fields/FieldSymbols';
import { List } from '../../../fields/List';
import { RichTextField } from '../../../fields/RichTextField';
@@ -10,7 +11,7 @@ import { listSpec } from '../../../fields/Schema';
import { ComputedField, ScriptField } from '../../../fields/ScriptField';
import { BoolCast, Cast, DocCast, FieldValue, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
import { TraceMobx } from '../../../fields/util';
-import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnTrue, returnZero, simulateMouseClick, Utils } from '../../../Utils';
+import { emptyFunction, lightOrDark, return18, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnTrue, returnZero, simulateMouseClick, Utils } from '../../../Utils';
import { Docs, DocUtils } from '../../documents/Documents';
import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
import { DocumentManager } from '../../util/DocumentManager';
@@ -20,7 +21,7 @@ import { ScriptingGlobals } from '../../util/ScriptingGlobals';
import { SelectionManager } from '../../util/SelectionManager';
import { SnappingManager } from '../../util/SnappingManager';
import { Transform } from '../../util/Transform';
-import { undoBatch, UndoManager } from '../../util/UndoManager';
+import { undoable, undoBatch, UndoManager } from '../../util/UndoManager';
import { EditableView } from '../EditableView';
import { TREE_BULLET_WIDTH } from '../global/globalCssVariables.scss';
import { DocumentView, DocumentViewInternal, DocumentViewProps, OpenWhere, StyleProviderFunc } from '../nodes/DocumentView';
@@ -33,6 +34,7 @@ import { CollectionTreeView, TreeViewType } from './CollectionTreeView';
import { CollectionView } from './CollectionView';
import './TreeView.scss';
import React = require('react');
+import { IconButton, Size } from 'browndash-components';
export interface TreeViewProps {
treeView: CollectionTreeView;
@@ -44,7 +46,7 @@ export interface TreeViewProps {
dataDoc?: Doc;
treeViewParent: Doc;
renderDepth: number;
- dropAction: dropActionType;
+ dragAction: dropActionType;
addDocTab: (doc: Doc, where: OpenWhere) => boolean;
panelWidth: () => number;
panelHeight: () => number;
@@ -102,7 +104,7 @@ export class TreeView extends React.Component<TreeViewProps> {
private _treedropDisposer?: DragManager.DragDropDisposer;
get treeViewOpenIsTransient() {
- return this.props.treeView.doc.treeViewOpenIsTransient || Doc.IsDocDataProto(this.doc);
+ return this.props.treeView.doc.treeViewOpenIsTransient || Doc.IsDataProto(this.doc);
}
set treeViewOpen(c: boolean) {
if (this.treeViewOpenIsTransient) this._transientOpenState = c;
@@ -118,14 +120,14 @@ export class TreeView extends React.Component<TreeViewProps> {
return 'TreeView(' + this.props.document.title + ')';
} // this makes mobx trace() statements more descriptive
get defaultExpandedView() {
- return this.doc.viewType === CollectionViewType.Docking
+ return this.doc._type_collection === CollectionViewType.Docking
? this.fieldKey
: this.props.treeView.dashboardMode
? this.fieldKey
: this.props.treeView.fileSysMode
? this.doc.isFolder
? this.fieldKey
- : 'embeddings' // for displaying
+ : 'data' // file system folders display their contents (data). used to be they displayed their embeddings but now its a tree structure and not a flat list
: this.props.treeView.outlineMode || this.childDocs
? this.fieldKey
: Doc.noviceMode
@@ -146,7 +148,7 @@ export class TreeView extends React.Component<TreeViewProps> {
return NumCast(this.props.treeViewParent.maxEmbedHeight, 200);
}
@computed get dataDoc() {
- return this.props.document.treeViewChildrenOnRoot ? this.doc : this.doc[DataSym];
+ return this.props.document.treeViewChildrenOnRoot ? this.doc : this.doc[DocData];
}
@computed get layoutDoc() {
return Doc.Layout(this.doc);
@@ -191,10 +193,10 @@ export class TreeView extends React.Component<TreeViewProps> {
};
@undoBatch @action remove = (doc: Doc | Doc[], key: string) => {
this.props.treeView.props.select(false);
- const ind = this.dataDoc[key].indexOf(doc);
+ const ind = DocListCast(this.dataDoc[key]).indexOf(doc instanceof Doc ? doc : doc.lastElement());
const res = (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && Doc.RemoveDocFromList(this.dataDoc, key, doc), true);
- res && ind > 0 && DocumentManager.Instance.getDocumentView(this.dataDoc[key][ind - 1], this.props.treeView.props.DocumentView?.())?.select(false);
+ res && ind > 0 && DocumentManager.Instance.getDocumentView(DocListCast(this.dataDoc[key])[ind - 1], this.props.treeView.props.DocumentView?.())?.select(false);
return res;
};
@@ -220,12 +222,8 @@ export class TreeView extends React.Component<TreeViewProps> {
this.treeViewOpen = !this.treeViewOpen;
} else {
// choose an appropriate embedding or make one. --- choose the first embedding that (1) user owns, (2) has no context field ... otherwise make a new embedding
- const bestEmbedding =
- docView.props.Document.author === Doc.CurrentUserEmail && !Doc.IsDocDataProto(docView.props.Document)
- ? docView.props.Document
- : DocListCast(this.props.document.proto_embeddings).find(doc => !doc.embedContainer && doc.author === Doc.CurrentUserEmail);
- const nextBestEmbedding = DocListCast(this.props.document.proto_embeddings).find(doc => doc.author === Doc.CurrentUserEmail);
- this.props.addDocTab(bestEmbedding ?? nextBestEmbedding ?? Doc.MakeEmbedding(this.props.document), OpenWhere.lightbox);
+ const bestEmbedding = docView.rootDoc.author === Doc.CurrentUserEmail && !Doc.IsDataProto(docView.props.Document) ? docView.rootDoc : Doc.BestEmbedding(docView.rootDoc);
+ this.props.addDocTab(bestEmbedding, OpenWhere.lightbox);
}
};
@@ -275,7 +273,7 @@ export class TreeView extends React.Component<TreeViewProps> {
};
onPointerEnter = (e: React.PointerEvent): void => {
this.props.isContentActive(true) && Doc.BrushDoc(this.dataDoc);
- if (e.buttons === 1 && SnappingManager.GetIsDragging()) {
+ if (e.buttons === 1 && SnappingManager.GetIsDragging() && this.props.isContentActive()) {
this._header.current!.className = 'treeView-header';
document.removeEventListener('pointermove', this.onDragMove, true);
document.removeEventListener('pointerup', this.onDragUp, true);
@@ -312,7 +310,7 @@ export class TreeView extends React.Component<TreeViewProps> {
title: '-title-',
treeViewExpandedViewLock: true,
treeViewExpandedView: 'data',
- _viewType: CollectionViewType.Tree,
+ _type_collection: CollectionViewType.Tree,
layout_hideLinkButton: true,
_layout_showSidebar: true,
_layout_fitWidth: true,
@@ -340,13 +338,12 @@ export class TreeView extends React.Component<TreeViewProps> {
};
makeFolder = () => {
- const folder = Docs.Create.TreeDocument([], { title: 'Untitled folder', _stayInCollection: true, isFolder: true });
+ const folder = Docs.Create.TreeDocument([], { title: 'Untitled folder', _dragOnlyWithinContainer: true, isFolder: true });
TreeView._editTitleOnLoad = { id: folder[Id], parent: this.props.parentTreeView };
return this.props.addDocument(folder);
};
- deleteItem = () => this.props.removeDoc?.(this.doc);
- preTreeDrop = (e: Event, de: DragManager.DropEvent, targetAction: dropActionType) => {
+ preTreeDrop = (e: Event, de: DragManager.DropEvent) => {
const dragData = de.complete.docDragData;
dragData && (dragData.dropAction = this.props.treeView.props.Document === dragData.treeViewDoc ? 'same' : dragData.dropAction);
};
@@ -354,7 +351,7 @@ export class TreeView extends React.Component<TreeViewProps> {
@undoBatch
treeDrop = (e: Event, de: DragManager.DropEvent) => {
const pt = [de.x, de.y];
- if (!this._header.current) return;
+ if (!this._header.current) return false;
const rect = this._header.current.getBoundingClientRect();
const before = pt[1] < rect.top + rect.height / 2;
const inside = this.props.treeView.fileSysMode && !this.doc.isFolder ? false : pt[0] > Math.min(rect.left + 75, rect.left + rect.width * 0.75) || (!before && this.treeViewOpen && this.childDocs?.length ? true : false);
@@ -363,14 +360,25 @@ export class TreeView extends React.Component<TreeViewProps> {
const destDoc = this.doc;
DocUtils.MakeLink(sourceDoc, destDoc, { link_relationship: 'tree link' });
e.stopPropagation();
+ return true;
}
const docDragData = de.complete.docDragData;
if (docDragData && pt[0] < rect.left + rect.width) {
if (docDragData.draggedDocuments[0] === this.doc) return true;
- if (this.dropDocuments(docDragData.droppedDocuments, before, inside, docDragData.dropAction, docDragData.removeDocument, docDragData.moveDocument, docDragData.treeViewDoc === this.props.treeView.props.Document)) {
- e.stopPropagation();
- }
+ const added = this.dropDocuments(
+ docDragData.droppedDocuments, //
+ before,
+ inside,
+ docDragData.dropAction,
+ docDragData.removeDocument,
+ docDragData.moveDocument,
+ docDragData.treeViewDoc === this.props.treeView.props.Document
+ );
+ e.stopPropagation();
+ !added && e.preventDefault();
+ return added;
}
+ return false;
};
dropping: boolean = false;
@@ -380,17 +388,17 @@ export class TreeView extends React.Component<TreeViewProps> {
const innerAdd = (doc: Doc) => {
const dataIsComputed = ComputedField.WithoutComputed(() => FieldValue(this.dataDoc[this.fieldKey])) instanceof ComputedField;
const added = (!dataIsComputed || (this.dropping && this.moving)) && Doc.AddDocToList(this.dataDoc, this.fieldKey, doc);
- dataIsComputed && (doc.embedContainer = this.doc.embedContainer);
+ dataIsComputed && Doc.SetContainer(doc, DocCast(this.doc.embedContainer));
return added;
};
return (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && innerAdd(doc), true as boolean);
};
const addDoc = inside ? localAdd : parentAddDoc;
const move = (!dropAction || dropAction === 'proto' || dropAction === 'move' || dropAction === 'same') && moveDocument;
- const canAdd = (!this.props.treeView.outlineMode && !StrCast((inside ? this.props.document : this.props.treeViewParent)?.freezeChildren).includes('add')) || forceAdd;
+ const canAdd = (!this.props.treeView.outlineMode && !StrCast((inside ? this.props.document : this.props.treeViewParent)?.treeViewFreezeChildren).includes('add')) || forceAdd;
if (canAdd) {
this.props.parentTreeView instanceof TreeView && (this.props.parentTreeView.dropping = true);
- const res = UndoManager.RunInTempBatch(() => droppedDocuments.reduce((added, d) => (move ? move(d, undefined, addDoc) || (dropAction === 'proto' ? addDoc(d) : false) : addDoc(d)) || added, false));
+ const res = droppedDocuments.reduce((added, d) => (move ? move(d, undefined, addDoc) || (dropAction === 'proto' ? addDoc(d) : false) : addDoc(d)) || added, false);
this.props.parentTreeView instanceof TreeView && (this.props.parentTreeView.dropping = false);
return res;
}
@@ -410,7 +418,7 @@ export class TreeView extends React.Component<TreeViewProps> {
embeddedPanelHeight = () => {
const layoutDoc = (temp => temp && Doc.expandTemplateLayout(temp, this.props.document))(this.props.treeView.props.childLayoutTemplate?.()) || this.layoutDoc;
return Math.min(
- layoutDoc[HeightSym](),
+ layoutDoc[Height](),
this.MAX_EMBED_HEIGHT,
(() => {
const aspect = Doc.NativeAspect(layoutDoc);
@@ -419,7 +427,7 @@ export class TreeView extends React.Component<TreeViewProps> {
? !Doc.NativeHeight(layoutDoc)
? NumCast(layoutDoc._height)
: Math.min((this.embeddedPanelWidth() * NumCast(layoutDoc.scrollHeight, Doc.NativeHeight(layoutDoc))) / (Doc.NativeWidth(layoutDoc) || NumCast(this.props.treeViewParent._height)))
- : (this.embeddedPanelWidth() * layoutDoc[HeightSym]()) / layoutDoc[WidthSym]();
+ : (this.embeddedPanelWidth() * layoutDoc[Height]()) / layoutDoc[Width]();
})()
);
};
@@ -443,7 +451,7 @@ export class TreeView extends React.Component<TreeViewProps> {
const innerAdd = (doc: Doc) => {
const dataIsComputed = ComputedField.WithoutComputed(() => FieldValue(this.dataDoc[key])) instanceof ComputedField;
const added = (!dataIsComputed || (this.dropping && this.moving)) && Doc.AddDocToList(this.dataDoc, key, doc, addBefore, before, false, true);
- dataIsComputed && (doc.embedContainer = this.doc.embedContainer);
+ dataIsComputed && Doc.SetContainer(doc, DocCast(this.doc.embedContainer));
return added;
};
return (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && innerAdd(doc), true as boolean);
@@ -459,7 +467,7 @@ export class TreeView extends React.Component<TreeViewProps> {
addDoc,
remDoc,
moveDoc,
- this.props.dropAction,
+ this.props.dragAction,
this.props.addDocTab,
this.titleStyleProvider,
this.props.ScreenToLocalTransform,
@@ -528,7 +536,7 @@ export class TreeView extends React.Component<TreeViewProps> {
@computed get renderContent() {
TraceMobx();
const expandKey = this.treeViewExpandedView;
- const sortings = (this.props.styleProvider?.(this.doc, this.props.treeView.props, StyleProp.TreeViewSortings) as { [key: string]: { color: string; label: string } }) ?? {};
+ const sortings = (this.props.styleProvider?.(this.doc, this.props.treeView.props, StyleProp.TreeViewSortings) as { [key: string]: { color: string; icon: JSX.Element | string } }) ?? {};
if (['links', 'annotations', 'embeddings', this.fieldKey].includes(expandKey)) {
const sorting = StrCast(this.doc.treeViewSortCriterion, TreeSort.None);
const sortKeys = Object.keys(sortings);
@@ -551,7 +559,7 @@ export class TreeView extends React.Component<TreeViewProps> {
}
const dataIsComputed = ComputedField.WithoutComputed(() => FieldValue(this.dataDoc[key])) instanceof ComputedField;
const added = (!dataIsComputed || (this.dropping && this.moving)) && Doc.AddDocToList(this.dataDoc, key, doc, addBefore, before, false, true);
- !dataIsComputed && added && (doc.embedContainer = this.doc.embedContainer);
+ !dataIsComputed && added && Doc.SetContainer(doc, DocCast(this.doc.embedContainer));
return added;
};
@@ -568,10 +576,25 @@ 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'} style={{ background: sortings[sorting]?.color }}>
- {sortings[sorting]?.label}
+ <div className={'treeView-sorting'}>
+ <IconButton
+ color={sortings[sorting]?.color}
+ size={Size.XSMALL}
+ icon={sortings[sorting]?.icon}
+ onPointerDown={e => {
+ downX = e.clientX;
+ downY = e.clientY;
+ e.stopPropagation();
+ }}
+ onClick={undoable(e => {
+ if (this.props.isContentActive() && Math.abs(e.clientX - downX) < 3 && Math.abs(e.clientY - downY) < 3) {
+ !this.props.treeView.outlineMode && (this.doc.treeViewSortCriterion = sortKeys[(curSortIndex + 1) % sortKeys.length]);
+ e.stopPropagation();
+ }
+ }, 'sort order')}
+ />
</div>
)}
<ul
@@ -584,12 +607,12 @@ export class TreeView extends React.Component<TreeViewProps> {
downY = e.clientY;
e.stopPropagation();
}}
- onClick={e => {
+ onClick={undoable(e => {
if (this.props.isContentActive() && Math.abs(e.clientX - downX) < 3 && Math.abs(e.clientY - downY) < 3) {
!this.props.treeView.outlineMode && (this.doc.treeViewSortCriterion = sortKeys[(curSortIndex + 1) % sortKeys.length]);
e.stopPropagation();
}
- }}>
+ }, 'sort order')}>
{!docs
? null
: TreeView.GetChildElements(
@@ -603,7 +626,7 @@ export class TreeView extends React.Component<TreeViewProps> {
addDoc,
remDoc,
moveDoc,
- StrCast(this.doc.childDropAction, this.props.dropAction) as dropActionType,
+ StrCast(this.doc.childDragAction, this.props.dragAction) as dropActionType,
this.props.addDocTab,
this.titleStyleProvider,
this.props.ScreenToLocalTransform,
@@ -628,7 +651,7 @@ export class TreeView extends React.Component<TreeViewProps> {
this._renderCount
)}
</ul>
- </>
+ </div>
);
} else if (this.treeViewExpandedView === 'fields') {
return (
@@ -639,6 +662,7 @@ export class TreeView extends React.Component<TreeViewProps> {
}
return (
<ul
+ style={{}}
onPointerDown={e => {
e.preventDefault();
e.stopPropagation();
@@ -672,7 +696,8 @@ export class TreeView extends React.Component<TreeViewProps> {
@computed get renderBullet() {
TraceMobx();
- const iconType = this.props.treeView.props.styleProvider?.(this.doc, this.props.treeView.props, StyleProp.TreeViewIcon + (this.treeViewOpen ? ':open' : '')) || 'question';
+ const iconType = this.props.treeView.props.styleProvider?.(this.doc, this.props.treeView.props, StyleProp.TreeViewIcon + (this.treeViewOpen ? ':open' : !this.childDocs.length ? ':empty' : '')) || 'question';
+ const color = StrCast(Doc.UserDoc().userColor);
const checked = this.onCheckedClick ? this.doc.treeViewChecked ?? 'unchecked' : undefined;
return (
<div
@@ -693,14 +718,19 @@ export class TreeView extends React.Component<TreeViewProps> {
}>
{this.props.treeView.outlineMode ? (
!(this.doc.text as RichTextField)?.Text ? null : (
- <FontAwesomeIcon size="sm" icon={[this.childDocs?.length && !this.treeViewOpen ? 'fas' : 'far', 'circle']} />
+ <IconButton color={color} icon={<FontAwesomeIcon icon={[this.childDocs?.length && !this.treeViewOpen ? 'fas' : 'far', 'circle']} />} size={Size.XSMALL} />
)
) : (
<div className="treeView-bulletIcons" style={{ color: Doc.IsSystem(DocCast(this.doc.proto)) ? 'red' : undefined }}>
- <div className={`treeView-${this.onCheckedClick ? 'checkIcon' : 'expandIcon'}`}>
- <FontAwesomeIcon size="sm" icon={checked === 'check' ? 'check' : checked === 'x' ? 'times' : checked === 'unchecked' ? 'square' : !this.treeViewOpen ? 'caret-right' : 'caret-down'} />
- </div>
- {this.onCheckedClick ? null : typeof iconType === 'string' ? <FontAwesomeIcon icon={iconType as IconProp} /> : iconType}
+ {this.onCheckedClick ? (
+ <IconButton
+ color={color}
+ icon={<FontAwesomeIcon size="sm" icon={checked === 'check' ? 'check' : checked === 'x' ? 'times' : checked === 'unchecked' ? 'square' : !this.treeViewOpen ? 'caret-right' : 'caret-down'} />}
+ size={Size.XSMALL}
+ />
+ ) : (
+ <IconButton color={color} icon={<FontAwesomeIcon icon={iconType as IconProp} />} size={Size.XSMALL} />
+ )}
</div>
)}
</div>
@@ -713,7 +743,7 @@ export class TreeView extends React.Component<TreeViewProps> {
const data = () => (this.childDocs || this.props.treeView.dashboardMode ? this.fieldKey : '');
const embeddings = () => (this.props.treeView.dashboardMode ? '' : 'embeddings');
const fields = () => (Doc.noviceMode ? '' : 'fields');
- const layout = Doc.noviceMode || this.doc.viewType === CollectionViewType.Docking ? [] : ['layout'];
+ const layout = Doc.noviceMode || this.doc._type_collection === CollectionViewType.Docking ? [] : ['layout'];
return [data(), ...layout, ...(this.props.treeView.fileSysMode ? [embeddings(), links(), annos()] : []), fields()].filter(m => m);
}
@action
@@ -728,21 +758,19 @@ export class TreeView extends React.Component<TreeViewProps> {
@observable headerEleWidth = 0;
@computed get titleButtons() {
const customHeaderButtons = this.props.styleProvider?.(this.doc, this.props.treeView.props, StyleProp.Decorations);
+ const color = StrCast(Doc.UserDoc().userColor);
return this.props.treeViewHideHeaderFields() || this.doc.treeViewHideHeaderFields ? null : (
<>
{customHeaderButtons} {/* e.g.,. hide button is set by dashboardStyleProvider */}
- {this.doc.hideContextMenu ? null : (
- <FontAwesomeIcon
- title="context menu"
- key="bars"
- icon="bars"
- size="sm"
- onClick={e => {
- this.showContextMenu(e);
- e.stopPropagation();
- }}
- />
- )}
+ <IconButton
+ color={color}
+ icon={<FontAwesomeIcon icon="bars" />}
+ size={Size.XSMALL}
+ onClick={e => {
+ this.showContextMenu(e);
+ e.stopPropagation();
+ }}
+ />
{Doc.noviceMode ? null : this.doc.treeViewExpandedViewLock || Doc.IsSystem(this.doc) ? null : (
<span className="collectionTreeView-keyHeader" title="type of expanded data" key={this.treeViewExpandedView} onPointerDown={this.expandNextviewType}>
{this.treeViewExpandedView}
@@ -759,10 +787,10 @@ export class TreeView extends React.Component<TreeViewProps> {
};
contextMenuItems = () => {
const makeFolder = { script: ScriptField.MakeFunction(`scriptContext.makeFolder()`, { scriptContext: 'any' })!, icon: 'folder-plus', label: 'New Folder' };
- const deleteItem = { script: ScriptField.MakeFunction(`scriptContext.deleteItem()`, { scriptContext: 'any' })!, icon: 'folder-plus', label: 'Delete' };
const folderOp = this.childDocs?.length ? [makeFolder] : [];
- const openEmbedding = { script: ScriptField.MakeFunction(`openDoc(getEmbedding(self), ${OpenWhere.addRight})`)!, icon: 'copy', label: 'Open Embedding' };
+ const openEmbedding = { script: ScriptField.MakeFunction(`openDoc(getEmbedding(self), "${OpenWhere.addRight}")`)!, icon: 'copy', label: 'Open New Embedding' };
const focusDoc = { script: ScriptField.MakeFunction(`DocFocusOrOpen(self)`)!, icon: 'eye', label: 'Focus or Open' };
+ const reopenDoc = { script: ScriptField.MakeFunction(`DocFocusOrOpen(self)`)!, icon: 'eye', label: 'Reopen' };
return [
...(this.props.contextMenuItems ?? []).filter(mi => (!mi.filter ? true : mi.filter.script.run({ doc: this.doc })?.result)),
...(this.doc.isFolder
@@ -771,9 +799,11 @@ export class TreeView extends React.Component<TreeViewProps> {
? []
: this.props.treeView.fileSysMode && this.doc === Doc.GetProto(this.doc)
? [openEmbedding, makeFolder]
- : this.doc.viewType === CollectionViewType.Docking
+ : this.doc._type_collection === CollectionViewType.Docking
? []
- : [deleteItem, openEmbedding, focusDoc]),
+ : this.props.treeView.rootDoc === Doc.MyRecentlyClosed
+ ? [reopenDoc]
+ : [openEmbedding, focusDoc]),
];
};
childContextMenuItems = () => {
@@ -856,7 +886,6 @@ export class TreeView extends React.Component<TreeViewProps> {
};
titleWidth = () => Math.max(20, Math.min(this.props.treeView.truncateTitleWidth(), this.props.panelWidth())) / (this.props.treeView.props.NativeDimScaling?.() || 1) - this.headerEleWidth - treeBulletWidth();
- return18 = () => 18;
/**
* Renders the EditableView title element for placement into the tree.
*/
@@ -874,6 +903,7 @@ export class TreeView extends React.Component<TreeViewProps> {
height={12}
sizeToContent={true}
fontSize={12}
+ isEditingCallback={action(e => (this._editTitle = e))}
GetValue={() => StrCast(this.doc.title)}
OnTab={undoBatch((shift?: boolean) => {
if (!shift) this.props.indentDocument?.(true);
@@ -904,7 +934,6 @@ export class TreeView extends React.Component<TreeViewProps> {
hideDecorationTitle={this.props.treeView.outlineMode}
hideResizeHandles={this.props.treeView.outlineMode}
styleProvider={this.titleStyleProvider}
- enableDragWhenActive={true}
onClickScriptDisable="never" // tree docViews have a script to show fields, etc.
docViewPath={this.props.treeView.props.docViewPath}
treeViewDoc={this.props.treeView.props.Document}
@@ -914,28 +943,29 @@ export class TreeView extends React.Component<TreeViewProps> {
pinToPres={emptyFunction}
onClick={this.onChildClick}
onDoubleClick={this.onChildDoubleClick}
- dropAction={this.props.dropAction}
+ dragAction={this.props.dragAction}
moveDocument={this.move}
removeDocument={this.props.removeDoc}
ScreenToLocalTransform={this.getTransform}
- NativeHeight={this.return18}
+ NativeHeight={return18}
NativeWidth={returnZero}
+ shouldNotScale={returnTrue}
PanelWidth={this.titleWidth}
- PanelHeight={this.return18}
+ PanelHeight={return18}
contextMenuItems={this.contextMenuItems}
renderDepth={1}
- isContentActive={this.props.isContentActive}
+ isContentActive={emptyFunction} //this.props.isContentActive}
isDocumentActive={this.props.isContentActive}
focus={this.refocus}
whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged}
bringToFront={emptyFunction}
- disableDocBrushing={this.props.treeView.props.disableDocBrushing}
+ disableBrushing={this.props.treeView.props.disableBrushing}
hideLinkButton={BoolCast(this.props.treeView.props.Document.childHideLinkButton)}
dontRegisterView={BoolCast(this.props.treeView.props.Document.childDontRegisterViews, this.props.dontRegisterView)}
xPadding={NumCast(this.props.treeView.props.Document.childXPadding, this.props.treeView.props.childXPadding)}
yPadding={NumCast(this.props.treeView.props.Document.childYPadding, this.props.treeView.props.childYPadding)}
- docFilters={returnEmptyFilter}
- docRangeFilters={returnEmptyFilter}
+ childFilters={returnEmptyFilter}
+ childFiltersByRanges={returnEmptyFilter}
searchFilterDocs={returnEmptyDoclist}
/>
);
@@ -946,10 +976,12 @@ export class TreeView extends React.Component<TreeViewProps> {
ref={this._tref}
title="click to edit title. Double Click or Drag to Open"
style={{
+ backgroundColor: Doc.IsSystem(this.props.document) || this.props.document.isFolder ? StrCast(Doc.UserDoc().userVariantColor) : undefined,
+ color: Doc.IsSystem(this.props.document) || this.props.document.isFolder ? lightOrDark(StrCast(Doc.UserDoc().userVariantColor)) : undefined,
fontWeight: Doc.IsSearchMatch(this.doc) !== undefined ? 'bold' : undefined,
textDecoration: Doc.GetT(this.doc, 'title', 'string', true) ? 'underline' : undefined,
outline: this.doc === Doc.ActiveDashboard ? 'dashed 1px #06123232' : undefined,
- pointerEvents: !this.props.isContentActive() && !SnappingManager.GetIsDragging() ? 'none' : undefined,
+ pointerEvents: !this.props.isContentActive() ? 'none' : undefined,
}}>
{view}
</div>
@@ -971,6 +1003,12 @@ export class TreeView extends React.Component<TreeViewProps> {
onPointerDown={this.ignoreEvent}
onPointerEnter={this.onPointerEnter}
onPointerLeave={this.onPointerLeave}>
+ <div
+ className="treeView-background"
+ style={{
+ background: StrCast(Doc.UserDoc().userColor),
+ }}
+ />
{contents}
</div>
{this.renderBorder}
@@ -1009,8 +1047,8 @@ export class TreeView extends React.Component<TreeViewProps> {
treeViewDoc={this.props.treeView?.props.Document}
rootSelected={returnTrue}
docViewPath={this.props.treeView.props.docViewPath}
- docFilters={returnEmptyFilter}
- docRangeFilters={returnEmptyFilter}
+ childFilters={returnEmptyFilter}
+ childFiltersByRanges={returnEmptyFilter}
searchFilterDocs={returnEmptyDoclist}
addDocument={this.props.addDocument}
moveDocument={this.move}
@@ -1020,7 +1058,7 @@ export class TreeView extends React.Component<TreeViewProps> {
yPadding={NumCast(this.props.treeView.props.Document.childYPadding, this.props.treeView.props.childYPadding)}
addDocTab={this.props.addDocTab}
pinToPres={this.props.treeView.props.pinToPres}
- disableDocBrushing={this.props.treeView.props.disableDocBrushing}
+ disableBrushing={this.props.treeView.props.disableBrushing}
bringToFront={returnFalse}
scriptContext={this}
/>
@@ -1030,7 +1068,7 @@ export class TreeView extends React.Component<TreeViewProps> {
// renders the text version of a document as the header. This is used in the file system mode and in other vanilla tree views.
@computed get renderTitleAsHeader() {
- return this.props.treeView.Document.treeViewHideUnrendered && this.doc.unrendered && !this.doc.treeViewFieldKey ? (
+ return this.props.treeView.Document.treeViewHideUnrendered && this.doc.layout_unrendered && !this.doc.treeViewFieldKey ? (
<div></div>
) : (
<>
@@ -1131,7 +1169,7 @@ export class TreeView extends React.Component<TreeViewProps> {
add: (doc: Doc | Doc[], relativeTo?: Doc, before?: boolean) => boolean,
remove: undefined | ((doc: Doc | Doc[]) => boolean),
move: DragManager.MoveFunction,
- dropAction: dropActionType,
+ dragAction: dropActionType,
addDocTab: (doc: Doc, where: OpenWhere) => boolean,
styleProvider: undefined | StyleProviderFunc,
screenToLocalXf: () => Transform,
@@ -1181,7 +1219,7 @@ export class TreeView extends React.Component<TreeViewProps> {
TreeView._editTitleOnLoad = editTitle ? { id: child[Id], parent } : undefined;
Doc.AddDocToList(newParent, fieldKey, child, addAfter, false);
newParent.treeViewOpen = true;
- child.embedContainer = treeView.Document;
+ Doc.SetContainer(child, treeView.Document);
}
};
const indent = i === 0 ? undefined : (editTitle: boolean) => dentDoc(editTitle, docs[i - 1], undefined, treeViewRefs.get(docs[i - 1]));
@@ -1190,7 +1228,7 @@ export class TreeView extends React.Component<TreeViewProps> {
const childLayout = Doc.Layout(pair.layout);
const rowHeight = () => {
const aspect = Doc.NativeAspect(childLayout);
- return aspect ? Math.min(childLayout[WidthSym](), rowWidth()) / aspect : childLayout[HeightSym]();
+ return aspect ? Math.min(childLayout[Width](), rowWidth()) / aspect : childLayout[Height]();
};
return (
<TreeView
@@ -1210,14 +1248,14 @@ export class TreeView extends React.Component<TreeViewProps> {
onCheckedClick={onCheckedClick}
onChildClick={onChildClick}
renderDepth={renderDepth}
- removeDoc={StrCast(treeViewParent.freezeChildren).includes('remove') ? undefined : remove}
+ removeDoc={StrCast(treeViewParent.treeViewFreezeChildren).includes('remove') ? undefined : remove}
addDocument={addDocument}
styleProvider={styleProvider}
panelWidth={rowWidth}
panelHeight={rowHeight}
dontRegisterView={dontRegisterView}
moveDocument={move}
- dropAction={dropAction}
+ dragAction={dragAction}
addDocTab={addDocTab}
ScreenToLocalTransform={screenToLocalXf}
isContentActive={isContentActive}
@@ -1238,6 +1276,6 @@ export class TreeView extends React.Component<TreeViewProps> {
ScriptingGlobals.add(function TreeView_addNewFolder() {
TreeView._editTitleOnLoad = { id: Utils.GenerateGuid(), parent: undefined };
- const opts = { title: 'Untitled folder', _stayInCollection: true, isFolder: true };
+ const opts = { title: 'Untitled folder', _dragOnlyWithinContainer: true, isFolder: true };
return Doc.AddDocToList(Doc.MyFilesystem, 'data', Docs.Create.TreeDocument([], opts, TreeView._editTitleOnLoad.id));
});
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
index fee4705e6..1e76d373c 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
@@ -1,4 +1,5 @@
-import { Doc, Field, FieldResult, HeightSym, WidthSym } from '../../../../fields/Doc';
+import { Doc, Field, FieldResult } from '../../../../fields/Doc';
+import { Height, Width } from '../../../../fields/DocSymbols';
import { Id, ToString } from '../../../../fields/FieldSymbols';
import { ObjectField } from '../../../../fields/ObjectField';
import { RefField } from '../../../../fields/RefField';
@@ -90,8 +91,8 @@ export function computePassLayout(poolData: Map<string, PoolData>, pivotDoc: Doc
docMap.set(layout[Id], {
x: NumCast(layout.x),
y: NumCast(layout.y),
- width: layout[WidthSym](),
- height: layout[HeightSym](),
+ width: layout[Width](),
+ height: layout[Height](),
pair: { layout, data },
transition: 'all .3s',
replica: '',
@@ -105,8 +106,8 @@ export function computeStarburstLayout(poolData: Map<string, PoolData>, pivotDoc
const burstDiam = [NumCast(pivotDoc._width), NumCast(pivotDoc._height)];
const burstScale = NumCast(pivotDoc._starburstDocScale, 1);
childPairs.forEach(({ layout, data }, i) => {
- const aspect = layout[HeightSym]() / layout[WidthSym]();
- const docSize = Math.min(Math.min(400, layout[WidthSym]()), Math.min(400, layout[WidthSym]()) / aspect) * burstScale;
+ const aspect = layout[Height]() / layout[Width]();
+ const docSize = Math.min(Math.min(400, layout[Width]()), Math.min(400, layout[Width]()) / aspect) * burstScale;
const deg = (i / childPairs.length) * Math.PI * 2;
docMap.set(layout[Id], {
x: Math.min(burstDiam[0] / 2 - docSize, Math.max(-burstDiam[0] / 2, (Math.cos(deg) * burstDiam[0]) / 2 - docSize / 2)),
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index 68ba3d368..fb8ec93b2 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -1,6 +1,7 @@
import { action, computed, IReactionDisposer, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
-import { CssSym, Doc, Field } from '../../../../fields/Doc';
+import { Doc, Field } from '../../../../fields/Doc';
+import { DocCss } from '../../../../fields/DocSymbols';
import { Id } from '../../../../fields/FieldSymbols';
import { List } from '../../../../fields/List';
import { Cast, NumCast, StrCast } from '../../../../fields/Types';
@@ -35,10 +36,10 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
() => [
this.props.A.props.ScreenToLocalTransform(),
Cast(Cast(Cast(this.props.A.rootDoc, Doc, null)?.link_anchor_1, Doc, null)?.annotationOn, Doc, null)?.layout_scrollTop,
- Cast(Cast(Cast(this.props.A.rootDoc, Doc, null)?.link_anchor_1, Doc, null)?.annotationOn, Doc, null)?.[CssSym],
+ Cast(Cast(Cast(this.props.A.rootDoc, Doc, null)?.link_anchor_1, Doc, null)?.annotationOn, Doc, null)?.[DocCss],
this.props.B.props.ScreenToLocalTransform(),
Cast(Cast(Cast(this.props.A.rootDoc, Doc, null)?.link_anchor_2, Doc, null)?.annotationOn, Doc, null)?.layout_scrollTop,
- Cast(Cast(Cast(this.props.A.rootDoc, Doc, null)?.link_anchor_2, Doc, null)?.annotationOn, Doc, null)?.[CssSym],
+ Cast(Cast(Cast(this.props.A.rootDoc, Doc, null)?.link_anchor_2, Doc, null)?.annotationOn, Doc, null)?.[DocCss],
],
action(() => {
this._start = Date.now();
@@ -58,7 +59,7 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
0
); // since the render code depends on querying the Dom through getBoudndingClientRect, we need to delay triggering render()
setTimeout(
- action(() => (!LinkDocs.length || !linkDoc.layout_linkDisplay) && (this._opacity = 0.05)),
+ action(() => (!LinkDocs.length || !linkDoc.link_displayLine) && (this._opacity = 0.05)),
750
); // this will unhighlight the link line.
const a = A.ContentDiv.getBoundingClientRect();
@@ -76,7 +77,7 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
const targetBhyperlink = Array.from(window.document.getElementsByClassName((linkDoc.link_anchor_2 as Doc)[Id])).lastElement();
if ((!targetAhyperlink && !a.width) || (!targetBhyperlink && !b.width)) return;
if (!targetAhyperlink) {
- if (linkDoc.layout_autoMoveAnchors) {
+ if (linkDoc.link_autoMoveAnchors) {
linkDoc.link_anchor_1_x = ((apt.point.x - aleft) / awidth) * 100;
linkDoc.link_anchor_1_y = ((apt.point.y - atop) / aheight) * 100;
}
@@ -91,7 +92,7 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
else linkDoc.opacity = 1;
}
if (!targetBhyperlink) {
- if (linkDoc.layout_autoMoveAnchors) {
+ if (linkDoc.link_autoMoveAnchors) {
linkDoc.link_anchor_2_x = ((bpt.point.x - bleft) / bwidth) * 100;
linkDoc.link_anchor_2_y = ((bpt.point.y - btop) / bheight) * 100;
}
@@ -234,6 +235,9 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
bActive,
textX,
textY,
+ // pt1,
+ // pt2,
+ // this code adds space between links
pt1: [pt1[0] + pt1normalized[0] * 13, pt1[1] + pt1normalized[1] * 13],
pt2: [pt2[0] + pt2normalized[0] * 13, pt2[1] + pt2normalized[1] * 13],
};
@@ -246,7 +250,7 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
const { a, b, pt1norm, pt2norm, aActive, bActive, textX, textY, pt1, pt2 } = this.renderData;
const linkRelationship = Field.toString(link?.link_relationship as any as Field); //get string representing relationship
const linkRelationshipList = Doc.UserDoc().link_relationshipList as List<string>;
- const linkColorList = Doc.UserDoc().linkColorList as List<string>;
+ const linkColorList = Doc.UserDoc().link_ColorList as List<string>;
const linkRelationshipSizes = Doc.UserDoc().link_relationshipSizes as List<number>;
const currRelationshipIndex = linkRelationshipList.indexOf(linkRelationship);
const linkDescription = Field.toString(link.link_description as any as Field);
@@ -261,11 +265,11 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
//thickness varies linearly from 3px to 12px for increasing link count
const strokeWidth = linkSize === -1 ? '3px' : Math.floor(2 + 10 * (linkSize / Math.max(...linkRelationshipSizes))) + 'px';
- if (link.layout_linkDisplayArrow === undefined) {
- link.layout_linkDisplayArrow = false;
+ if (link.link_displayArrow === undefined) {
+ link.link_displayArrow = false;
}
- return link.opacity === 0 || !a.width || !b.width || (!link.layout_linkDisplay && !aActive && !bActive) ? null : (
+ return link.opacity === 0 || !a.width || !b.width || (!link.link_displayLine && !aActive && !bActive) ? null : (
<>
<defs>
<marker id={`${link[Id] + 'arrowhead'}`} markerWidth="4" markerHeight="3" refX="0" refY="1.5" orient="auto">
@@ -294,7 +298,7 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
style={{ pointerEvents: 'visibleStroke', opacity: this._opacity, stroke, strokeWidth }}
onClick={this.onClickLine}
d={`M ${pt1[0]} ${pt1[1]} C ${pt1[0] + pt1norm[0]} ${pt1[1] + pt1norm[1]}, ${pt2[0] + pt2norm[0]} ${pt2[1] + pt2norm[1]}, ${pt2[0]} ${pt2[1]}`}
- markerEnd={link.layout_linkDisplayArrow ? `url(#${link[Id] + 'arrowhead'})` : ''}
+ markerEnd={link.link_displayArrow ? `url(#${link[Id] + 'arrowhead'})` : ''}
/>
{textX === undefined || !linkDescription ? null : (
<text filter={`url(#${link[Id] + 'background'})`} className="collectionfreeformlinkview-linkText" x={textX} y={textY} onPointerDown={this.pointerDown}>
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss
index cb5cef29c..4ada1731f 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss
@@ -1,11 +1,13 @@
-.collectionfreeformlinksview-svgCanvas{
+// TODO: change z-index to -1 when a modal is active?
+
+.collectionfreeformlinksview-svgCanvas {
position: absolute;
top: 0;
left: 0;
- width: 100%;
+ width: 100%;
height: 100%;
pointer-events: none;
- }
- .collectionfreeformlinksview-container {
+}
+.collectionfreeformlinksview-container {
pointer-events: none;
- } \ No newline at end of file
+}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 4d18ebeea..e1455525e 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -4,7 +4,8 @@ import { action, computed, IReactionDisposer, observable, reaction, runInAction,
import { observer } from 'mobx-react';
import { computedFn } from 'mobx-utils';
import { DateField } from '../../../../fields/DateField';
-import { DataSym, Doc, DocListCast, HeightSym, Opt, WidthSym } from '../../../../fields/Doc';
+import { Doc, DocListCast, Opt } from '../../../../fields/Doc';
+import { DocData, Height, Width } from '../../../../fields/DocSymbols';
import { Id } from '../../../../fields/FieldSymbols';
import { InkData, InkField, InkTool, PointData, Segment } from '../../../../fields/InkField';
import { List } from '../../../../fields/List';
@@ -417,15 +418,16 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
@undoBatch
internalLinkDrop(e: Event, de: DragManager.DropEvent, linkDragData: DragManager.LinkDragData, xp: number, yp: number) {
if (linkDragData.linkDragView.props.docViewPath().includes(this.props.docViewPath().lastElement())) {
- // dragged document is a child of this collection
- if (!linkDragData.linkDragView.props.CollectionFreeFormDocumentView?.() || linkDragData.dragDocument.embedContainer !== this.props.Document) {
- // if the source doc view's embedContainer isn't this same freeformcollectionlinkDragData.dragDocument.embedContainer === this.props.Document
+ let added = false;
+ // do nothing if link is dropped into any freeform view parent of dragged document
+ if (!linkDragData.dragDocument.embedContainer || linkDragData.dragDocument.embedContainer !== this.rootDoc) {
const source = Docs.Create.TextDocument('', { _width: 200, _height: 75, x: xp, y: yp, title: 'dropped annotation' });
- this.props.addDocument?.(source);
+ added = this.props.addDocument?.(source) ? true : false;
de.complete.linkDocument = DocUtils.MakeLink(linkDragData.linkSourceGetAnchor(), source, { link_relationship: 'annotated by:annotation of' }); // TODODO this is where in text links get passed
}
- e.stopPropagation(); // do nothing if link is dropped into any freeform view parent of dragged document
- return true;
+ e.stopPropagation();
+ !added && e.preventDefault();
+ return added;
}
return false;
}
@@ -483,7 +485,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
return false;
}
- @undoBatch
@action
updateClusters(_freeform_useClusters: boolean) {
this.props.Document._freeform_useClusters = _freeform_useClusters;
@@ -534,7 +535,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
}
}
- @undoBatch
@action
updateCluster(doc: Doc) {
const childLayouts = this.childLayoutPairs.map(pair => pair.layout);
@@ -637,7 +637,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
case InkTool.None:
if (!(this.props.layoutEngine?.() || StrCast(this.layoutDoc._layoutEngine))) {
this._hitCluster = this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY));
- setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, emptyFunction, this._hitCluster !== -1 ? true : false);
+ setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, emptyFunction, this._hitCluster !== -1 ? true : false, false);
}
break;
}
@@ -709,7 +709,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
case GestureUtils.Gestures.Rectangle:
if (this._inkToTextStartX && this._inkToTextStartY) {
const end = this.getTransform().transformPoint(Math.max(...ge.points.map(p => p.X)), Math.max(...ge.points.map(p => p.Y)));
- const setDocs = this.getActiveDocuments().filter(s => s.proto?.type === 'rtf' && s.color);
+ const setDocs = this.getActiveDocuments().filter(s => DocCast(s.proto)?.type === DocumentType.RTF && s.color);
const sets = setDocs.map(sd => {
return Cast(sd.text, RichTextField)?.Text as string;
});
@@ -721,9 +721,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
const inks = this.getActiveDocuments().filter(doc => {
if (doc.type === 'ink') {
const l = NumCast(doc.x);
- const r = l + doc[WidthSym]();
+ const r = l + doc[Width]();
const t = NumCast(doc.y);
- const b = t + doc[HeightSym]();
+ const b = t + doc[Height]();
const pass = !(this._inkToTextStartX! > r || end[0] < l || this._inkToTextStartY! > b || end[1] < t);
return pass;
}
@@ -870,9 +870,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
Doc.ActiveTool = InkTool.None;
} else {
if (this.tryDragCluster(e, this._hitCluster)) {
+ e.stopPropagation(); // we're moving a cluster, so stop propagation and return true to end panning and let the document drag take over
return true;
}
- this.pan(e);
+ // pan the view if this is a regular collection, or it's an overlay and the overlay is zoomed (otherwise, there's nothing to pan)
+ if (!this.props.isAnnotationOverlay || 1 - NumCast(this.rootDoc._freeform_scale_min, 1) / this.getLocalTransform().inverse().Scale) {
+ this.pan(e);
+ e.stopPropagation(); // if we are actually panning, stop propagation -- this will preven things like the overlayView from dragging the document while we're panning
+ }
}
return false;
};
@@ -1017,7 +1022,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
@action
zoom = (pointX: number, pointY: number, deltaY: number): void => {
- if (this.Document._isGroup) return;
+ if (this.Document._isGroup || this.Document._freeform_noZoom) return;
let deltaScale = deltaY > 0 ? 1 / 1.05 : 1.05;
if (deltaScale < 0) deltaScale = -deltaScale;
const [x, y] = this.getTransform().transformPoint(pointX, pointY);
@@ -1025,12 +1030,12 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
if (deltaScale * invTransform.Scale > 20) {
deltaScale = 20 / invTransform.Scale;
}
- if (deltaScale < 1 && invTransform.Scale <= NumCast(this.rootDoc._freeform_scaleMin, 1) && this.isAnnotationOverlay) {
+ if (deltaScale < 1 && invTransform.Scale <= NumCast(this.rootDoc._freeform_scale_min, 1) && this.isAnnotationOverlay) {
this.setPan(0, 0);
return;
}
- if (deltaScale * invTransform.Scale < NumCast(this.rootDoc._freeform_scaleMin, 1) && this.isAnnotationOverlay) {
- deltaScale = NumCast(this.rootDoc._freeform_scaleMin, 1) / invTransform.Scale;
+ if (deltaScale * invTransform.Scale < NumCast(this.rootDoc._freeform_scale_min, 1) && this.isAnnotationOverlay) {
+ deltaScale = NumCast(this.rootDoc._freeform_scale_min, 1) / invTransform.Scale;
}
const localTransform = invTransform.scaleAbout(deltaScale, x, y);
@@ -1045,33 +1050,22 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
onPointerWheel = (e: React.WheelEvent): void => {
if (this.Document._isGroup || !this.isContentActive()) return; // group style collections neither pan nor zoom
PresBox.Instance?.pauseAutoPres();
- if (this.layoutDoc._Transform || DocListCast(Doc.MyOverlayDocs?.data).includes(this.props.Document) || this.props.Document.treeViewOutlineMode === TreeViewType.outline) return;
+ if (this.layoutDoc._Transform || this.props.Document.treeViewOutlineMode === TreeViewType.outline) return;
e.stopPropagation();
+ const docHeight = NumCast(this.rootDoc[Doc.LayoutFieldKey(this.rootDoc) + '_nativeHeight'], this.nativeHeight);
+ const scrollable = NumCast(this.layoutDoc[this.scaleFieldKey], 1) === 1 && docHeight > this.props.PanelHeight() / this.nativeDimScaling + 1e-4;
switch (!e.ctrlKey ? Doc.UserDoc().freeformScrollMode : freeformScrollMode.Pan) {
case freeformScrollMode.Pan:
// if ctrl is selected then zoom
- if (e.ctrlKey) {
- if (this.props.isContentActive(true)) {
- if (this.props.isAnnotationOverlayScrollable) {
- // bcz: zooming on a webbox doesn't get the correct coordinates here for unknown reasons.
- // so better to do nothing than having things jump around.
- } else this.zoom(e.screenX, e.screenY, e.deltaY);
- }
- } // otherwise pan
- else if (this.props.isContentActive(true)) {
- const dx = -e.deltaX;
- const dy = -e.deltaY;
- if (e.shiftKey) {
- !this.props.isAnnotationOverlayScrollable && this.scrollPan({ deltaX: dy, deltaY: 0 });
- } else {
- !this.props.isAnnotationOverlayScrollable && this.scrollPan({ deltaX: dx, deltaY: dy });
- }
+ if (!e.ctrlKey && this.props.isContentActive(true)) {
+ this.scrollPan({ deltaX: -e.deltaX, deltaY: e.shiftKey ? 0 : -Math.max(-1, Math.min(1, e.deltaY)) });
+ break;
}
- break;
default:
case freeformScrollMode.Zoom:
- if (this.props.isContentActive(true)) {
- !this.props.isAnnotationOverlayScrollable && this.zoom(e.clientX, e.clientY, e.deltaY); // if (!this.props.isAnnotationOverlay) // bcz: do we want to zoom in on images/videos/etc?
+ if ((e.ctrlKey || !scrollable) && this.props.isContentActive(true)) {
+ this.zoom(e.clientX, e.clientY, Math.max(-1, Math.min(1, e.deltaY))); // if (!this.props.isAnnotationOverlay) // bcz: do we want to zoom in on images/videos/etc?
+ e.preventDefault();
}
break;
}
@@ -1114,26 +1108,27 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
else if (ranges.yrange.max <= panY - panelHgtMin / 2) panY = ranges.yrange.min - (this.props.originTopLeft ? panelHgtMax / 2 : panelHgtMin / 2);
}
}
- if (!this.layoutDoc._lockedTransform || LightboxView.LightboxDoc || DocListCast(Doc.MyOverlayDocs?.data).includes(this.Document)) {
+ if (!this.layoutDoc._lockedTransform || LightboxView.LightboxDoc) {
this.setPanZoomTransition(panTime);
- const scale = this.getLocalTransform().inverse().Scale;
- const minScale = NumCast(this.rootDoc._freeform_scaleMin, 1);
- const minPanX = NumCast(this.rootDoc._freeform_panXMin, 0);
- const minPanY = NumCast(this.rootDoc._freeform_panYMin, 0);
- const newPanX = Math.min(minPanX + (1 - minScale / scale) * NumCast(this.rootDoc._freeform_panXMax, this.nativeWidth), Math.max(minPanX, panX));
+ const minScale = NumCast(this.rootDoc._freeform_scale_min, 1);
+ const scale = 1 - minScale / this.getLocalTransform().inverse().Scale;
+ const minPanX = NumCast(this.rootDoc._freeform_panX_min, 0);
+ const minPanY = NumCast(this.rootDoc._freeform_panY_min, 0);
+ const maxPanX = NumCast(this.rootDoc._freeform_panX_max, this.nativeWidth);
+ const newPanX = Math.min(minPanX + scale * maxPanX, Math.max(minPanX, panX));
const fitYscroll = (((this.nativeHeight / this.nativeWidth) * this.props.PanelWidth() - this.props.PanelHeight()) * this.props.ScreenToLocalTransform().Scale) / minScale;
const nativeHeight = (this.props.PanelHeight() / this.props.PanelWidth() / (this.nativeHeight / this.nativeWidth)) * this.nativeHeight;
const maxScrollTop = this.nativeHeight / this.props.ScreenToLocalTransform().Scale - this.props.PanelHeight();
const maxPanY =
minPanY + // minPanY + scrolling introduced by view scaling + scrolling introduced by layout_fitWidth
- (1 - minScale / scale) * NumCast(this.rootDoc._panYMax, nativeHeight) +
+ scale * NumCast(this.rootDoc._panY_max, nativeHeight) +
(!this.props.getScrollHeight?.() ? fitYscroll : 0); // when not zoomed, scrolling is handled via a scrollbar, not panning
let newPanY = Math.max(minPanY, Math.min(maxPanY, panY));
if (false && NumCast(this.rootDoc.layout_scrollTop) && NumCast(this.rootDoc._freeform_scale, minScale) !== minScale) {
const relTop = NumCast(this.rootDoc.layout_scrollTop) / maxScrollTop;
this.rootDoc.layout_scrollTop = undefined;
newPanY = minPanY + relTop * (maxPanY - minPanY);
- } else if (fitYscroll && this.rootDoc.layout_scrollTop === undefined && NumCast(this.rootDoc._freeform_scale, minScale) === minScale) {
+ } else if (fitYscroll > 2 && this.rootDoc.layout_scrollTop === undefined && NumCast(this.rootDoc._freeform_scale, minScale) === minScale) {
const maxPanY = minPanY + fitYscroll;
const relTop = (panY - minPanY) / (maxPanY - minPanY);
setTimeout(() => (this.rootDoc.layout_scrollTop = relTop * maxScrollTop), 10);
@@ -1147,7 +1142,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
@action
nudge = (x: number, y: number, nudgeTime: number = 500) => {
const collectionDoc = this.props.docViewPath().lastElement().rootDoc;
- if (collectionDoc?._viewType !== CollectionViewType.Freeform || collectionDoc._freeform_ !== undefined) {
+ if (collectionDoc?._type_collection !== CollectionViewType.Freeform || collectionDoc._freeform_ !== undefined) {
this.setPan(
NumCast(this.layoutDoc[this.panXFieldKey]) + ((this.props.PanelWidth() / 2) * x) / this.zoomScaling(), // nudge x,y as a function of panel dimension and scale
NumCast(this.layoutDoc[this.panYFieldKey]) + ((this.props.PanelHeight() / 2) * -y) / this.zoomScaling(),
@@ -1208,7 +1203,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
calculatePanIntoView = (doc: Doc, xf: Transform, scale?: number) => {
const layoutdoc = Doc.Layout(doc);
const pt = xf.transformPoint(NumCast(doc.x), NumCast(doc.y));
- const pt2 = xf.transformPoint(NumCast(doc.x) + layoutdoc[WidthSym](), NumCast(doc.y) + layoutdoc[HeightSym]());
+ const pt2 = xf.transformPoint(NumCast(doc.x) + layoutdoc[Width](), NumCast(doc.y) + layoutdoc[Height]());
const bounds = { left: pt[0], right: pt2[0], top: pt[1], bot: pt2[1], width: pt2[0] - pt[0], height: pt2[1] - pt[1] };
if (scale !== undefined) {
@@ -1246,7 +1241,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
};
};
- isContentActive = () => this.props.isSelected() || this.props.isContentActive();
+ isContentActive = () => this.props.isContentActive();
@undoBatch
@action
@@ -1258,7 +1253,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
const layout_fieldKey = StrCast(docView.LayoutFieldKey);
const newDoc = Doc.MakeCopy(docView.rootDoc, true);
const dataField = docView.rootDoc[Doc.LayoutFieldKey(newDoc)];
- newDoc[DataSym][Doc.LayoutFieldKey(newDoc)] = dataField === undefined || Cast(dataField, listSpec(Doc), null)?.length !== undefined ? new List<Doc>([]) : undefined;
+ newDoc[DocData][Doc.LayoutFieldKey(newDoc)] = dataField === undefined || Cast(dataField, listSpec(Doc), null)?.length !== undefined ? new List<Doc>([]) : undefined;
if (below) newDoc.y = NumCast(docView.rootDoc.y) + NumCast(docView.rootDoc._height) + 10;
else newDoc.x = NumCast(docView.rootDoc.x) + NumCast(docView.rootDoc._width) + 10;
if (layout_fieldKey !== 'layout' && docView.rootDoc[layout_fieldKey] instanceof Doc) {
@@ -1269,13 +1264,15 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
return this.addDocument?.(newDoc);
}
};
- pointerEvents = () => {
+ @computed get _pointerEvents() {
const engine = this.props.layoutEngine?.() || StrCast(this.props.Document._layoutEngine);
const pointerEvents = DocumentDecorations.Instance.Interacting
? 'none'
- : this.props.childPointerEvents ?? (this.props.viewDefDivClick || (engine === computePassLayout.name && !this.props.isSelected(true)) ? 'none' : this.props.pointerEvents?.());
+ : this.props.childPointerEvents ?? (this.props.viewDefDivClick || (engine === computePassLayout.name && !this.props.isSelected(true)) || this.isContentActive() === false ? 'none' : this.props.pointerEvents?.());
return pointerEvents;
- };
+ }
+ pointerEvents = () => this._pointerEvents;
+ childContentsActive = () => (this.props.childContentsActive ?? this.isContentActive() === false ? returnFalse : emptyFunction)();
getChildDocView(entry: PoolData) {
const childLayout = entry.pair.layout;
const childData = entry.pair.data;
@@ -1298,13 +1295,13 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
onDoubleClick={this.onChildDoubleClickHandler}
onBrowseClick={this.onBrowseClickHandler}
ScreenToLocalTransform={childLayout.z ? this.getContainerTransform : this.getTransform}
- PanelWidth={childLayout[WidthSym]}
- PanelHeight={childLayout[HeightSym]}
- docFilters={this.childDocFilters}
- docRangeFilters={this.childDocRangeFilters}
+ PanelWidth={childLayout[Width]}
+ PanelHeight={childLayout[Height]}
+ childFilters={this.childDocFilters}
+ childFiltersByRanges={this.childDocRangeFilters}
searchFilterDocs={this.searchFilterDocs}
- isDocumentActive={this.props.childDocumentsActive?.() ? this.props.isDocumentActive : this.isContentActive}
- isContentActive={this.props.childContentsActive ?? emptyFunction}
+ isDocumentActive={this.props.childDocumentsActive?.() || this.rootDoc._isGroup ? this.props.isDocumentActive : this.isContentActive}
+ isContentActive={this.childContentsActive}
focus={this.Document._isGroup ? this.groupFocus : this.isAnnotationOverlay ? this.props.focus : this.focus}
addDocTab={this.addDocTab}
addDocument={this.props.addDocument}
@@ -1314,16 +1311,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged}
docViewPath={this.props.docViewPath}
styleProvider={this.getClusterColor}
- canEmbedOnDrag={this.props.childCanEmbedOnDrag}
+ dragAction={(this.rootDoc.childDragAction ?? this.props.childDragAction) as dropActionType}
dataProvider={this.childDataProvider}
sizeProvider={this.childSizeProvider}
- dropAction={StrCast(this.props.Document.childDropAction) as dropActionType}
bringToFront={this.bringToFront}
layout_showTitle={this.props.childlayout_showTitle}
dontRegisterView={this.props.dontRenderDocuments || this.props.dontRegisterView}
pointerEvents={this.pointerEvents}
- //rotation={this.props.styleProvider?.(childLayout, this.props, StyleProp.JitterRotation) || 0}
- //fitContentsToBox={this.props.fitContentsToBox || BoolCast(this.props.freezeChildDimensions)} // bcz: check this
+ //fitContentsToBox={this.props.fitContentsToBox || BoolCast(this.props.treeViewFreezeChildDimensions)} // bcz: check this
/>
);
}
@@ -1362,6 +1357,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
@observable _lightboxDoc: Opt<Doc>;
getCalculatedPositions(params: { pair: { layout: Doc; data?: Doc }; index: number; collection: Doc }): PoolData {
+ const random = (min: number, max: number, x: number, y: number) => /* min should not be equal to max */ min + (((Math.abs(x * y) * 9301 + 49297) % 233280) / 233280) * (max - min);
const childDoc = params.pair.layout;
const childDocLayout = Doc.Layout(childDoc);
const layoutFrameNumber = Cast(this.Document._currentFrame, 'number'); // frame number that container is at which determines layout frame values
@@ -1372,11 +1368,15 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
layoutFrameNumber === undefined
? { _width: Cast(childDocLayout._width, 'number'), _height: Cast(childDocLayout._height, 'number'), _rotation: Cast(childDocLayout._rotation, 'number'), x: childDoc.x, y: childDoc.y, opacity: this.props.childOpacity?.() }
: CollectionFreeFormDocumentView.getValues(childDoc, layoutFrameNumber);
+ // prettier-ignore
+ const rotation = Cast(_rotation,'number',
+ !this.layoutDoc._rotation_jitter ? null
+ : NumCast(this.layoutDoc._rotation_jitter) * random(-1, 1, NumCast(x), NumCast(y)) );
return {
x: Number.isNaN(NumCast(x)) ? 0 : NumCast(x),
y: Number.isNaN(NumCast(y)) ? 0 : NumCast(y),
z: Cast(z, 'number'),
- rotation: Cast(_rotation, 'number'),
+ rotation: rotation,
color: Cast(color, 'string') ? StrCast(color) : this.props.styleProvider?.(childDoc, this.props, StyleProp.Color),
backgroundColor: Cast(backgroundColor, 'string') ? StrCast(backgroundColor) : this.getClusterColor(childDoc, this.props, StyleProp.BackgroundColor),
opacity: !_width ? 0 : this._keyframeEditing ? 1 : Cast(opacity, 'number') ?? this.props.styleProvider?.(childDoc, this.props, StyleProp.Opacity),
@@ -1441,21 +1441,17 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
}.bind(this)
);
- childPositionProviderUnmemoized = (doc: Doc, replica: string) => {
- return this._layoutPoolData.get(doc[Id] + (replica || ''));
- };
+ childPositionProviderUnmemoized = (doc: Doc, replica: string) => this._layoutPoolData.get(doc[Id] + (replica || ''));
childDataProvider = computedFn(
function childDataProvider(this: any, doc: Doc, replica: string) {
- return this._layoutPoolData.get(doc[Id] + (replica || ''));
+ return this.childPositionProviderUnmemoized(doc, replica);
}.bind(this)
);
- childSizeProviderUnmemoized = (doc: Doc, replica: string) => {
- return this._layoutSizeData.get(doc[Id] + (replica || ''));
- };
+ childSizeProviderUnmemoized = (doc: Doc, replica: string) => this._layoutSizeData.get(doc[Id] + (replica || ''));
childSizeProvider = computedFn(
function childSizeProvider(this: any, doc: Doc, replica: string) {
- return this._layoutSizeData.get(doc[Id] + (replica || ''));
+ return this.childSizeProviderUnmemoized(doc, replica);
}.bind(this)
);
@@ -1541,8 +1537,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
// create an anchor that saves information about the current state of the freeform view (pan, zoom, view type)
- const anchor = Docs.Create.CollectionAnchorDocument({ title: 'ViewSpec - ' + StrCast(this.layoutDoc._viewType), unrendered: true, presTransition: 500, annotationOn: this.rootDoc });
- PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), pannable: !this.Document._isGroup, viewType: true, filters: true } }, this.rootDoc);
+ const anchor = Docs.Create.ConfigDocument({ title: 'ViewSpec - ' + StrCast(this.layoutDoc._type_collection), layout_unrendered: true, presTransition: 500, annotationOn: this.rootDoc });
+ PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), pannable: !this.Document._isGroup, type_collection: true, filters: true } }, this.rootDoc);
if (addAsAnnotation) {
if (Cast(this.dataDoc[this.props.fieldKey + '_annotations'], listSpec(Doc), null) !== undefined) {
@@ -1565,14 +1561,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
this._disposers.groupBounds = reaction(
() => {
if (this.Document._isGroup && this.childDocs.length === this.childDocList?.length) {
- const clist = this.childDocs.map(cd => ({ x: NumCast(cd.x), y: NumCast(cd.y), width: cd[WidthSym](), height: cd[HeightSym]() }));
+ const clist = this.childDocs.map(cd => ({ x: NumCast(cd.x), y: NumCast(cd.y), width: cd[Width](), height: cd[Height]() }));
return aggregateBounds(clist, NumCast(this.layoutDoc._xPadding), NumCast(this.layoutDoc._yPadding));
}
return undefined;
},
cbounds => {
if (cbounds) {
- const c = [NumCast(this.layoutDoc.x) + this.layoutDoc[WidthSym]() / 2, NumCast(this.layoutDoc.y) + this.layoutDoc[HeightSym]() / 2];
+ const c = [NumCast(this.layoutDoc.x) + this.layoutDoc[Width]() / 2, NumCast(this.layoutDoc.y) + this.layoutDoc[Height]() / 2];
const p = [NumCast(this.layoutDoc[this.panXFieldKey]), NumCast(this.layoutDoc[this.panYFieldKey])];
const pbounds = {
x: cbounds.x - p[0] + c[0],
@@ -1638,8 +1634,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
CollectionFreeFormView.UpdateIcon(
this.layoutDoc[Id] + '-icon' + new Date().getTime(),
this.props.docViewPath().lastElement().ContentDiv!,
- this.layoutDoc[WidthSym](),
- this.layoutDoc[HeightSym](),
+ this.layoutDoc[Width](),
+ this.layoutDoc[Height](),
this.props.PanelWidth(),
this.props.PanelHeight(),
0,
@@ -1698,7 +1694,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
if ((e as any).handlePan || this.props.isAnnotationOverlay) return;
(e as any).handlePan = true;
- if (!this.layoutDoc._noAutoscroll && !this.props.renderDepth && this._marqueeRef) {
+ if (!this.layoutDoc._freeform_noAutoPan && !this.props.renderDepth && this._marqueeRef) {
const dragX = e.detail.clientX;
const dragY = e.detail.clientY;
const bounds = this._marqueeRef?.getBoundingClientRect();
@@ -1763,12 +1759,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
return;
}
!Doc.noviceMode && Doc.UserDoc().defaultTextLayout && appearanceItems.push({ description: 'Reset default note style', event: () => (Doc.UserDoc().defaultTextLayout = undefined), icon: 'eye' });
- appearanceItems.push({ description: (Doc.UserDoc().defaultToFlashcards ? 'Disable' : 'Enable') + ' Flashcard Notes', event: () => (Doc.UserDoc().defaultToFlashcards = !Doc.UserDoc().defaultToFlashcards), icon: 'eye' });
- appearanceItems.push({
- description: `${this.fitContentsToBox ? 'Make Zoomable' : 'Scale to Window'}`,
- event: () => (this.Document._freeform_fitContentsToBox = !this.fitContentsToBox),
- icon: !this.fitContentsToBox ? 'expand-arrows-alt' : 'compress-arrows-alt',
- });
appearanceItems.push({ description: `Pin View`, event: () => TabDocView.PinDoc(this.rootDoc, { pinViewport: MarqueeView.CurViewBounds(this.rootDoc, this.props.PanelWidth(), this.props.PanelHeight()) }), icon: 'map-pin' });
!Doc.noviceMode && appearanceItems.push({ description: `update icon`, event: this.updateIcon, icon: 'compress-arrows-alt' });
this.props.renderDepth && appearanceItems.push({ description: 'Ungroup collection', event: this.promoteCollection, icon: 'table' });
@@ -1851,10 +1841,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
incrementalRender = action(() => {
if (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath())) {
- const unrendered = this.childDocs.filter(doc => !this._renderCutoffData.get(doc[Id]));
+ const layout_unrendered = this.childDocs.filter(doc => !this._renderCutoffData.get(doc[Id]));
const loadIncrement = 5;
- for (var i = 0; i < Math.min(unrendered.length, loadIncrement); i++) {
- this._renderCutoffData.set(unrendered[i][Id] + '', true);
+ for (var i = 0; i < Math.min(layout_unrendered.length, loadIncrement); i++) {
+ this._renderCutoffData.set(layout_unrendered[i][Id] + '', true);
}
}
this.childDocs.some(doc => !this._renderCutoffData.get(doc[Id])) && setTimeout(this.incrementalRender, 1);
@@ -1973,6 +1963,12 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
lightboxPanelWidth = () => Math.max(0, this.props.PanelWidth() - 30);
lightboxPanelHeight = () => Math.max(0, this.props.PanelHeight() - 30);
lightboxScreenToLocal = () => this.props.ScreenToLocalTransform().translate(-15, -15);
+ onPassiveWheel = (e: WheelEvent) => {
+ const docHeight = NumCast(this.rootDoc[Doc.LayoutFieldKey(this.rootDoc) + '_nativeHeight'], this.nativeHeight);
+ const scrollable = NumCast(this.layoutDoc[this.scaleFieldKey], 1) === 1 && docHeight > this.props.PanelHeight() / this.nativeDimScaling;
+ this.props.isSelected() && !scrollable && e.preventDefault();
+ };
+ _oldWheel: any;
render() {
TraceMobx();
return (
@@ -1980,8 +1976,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
className="collectionfreeformview-container"
ref={r => {
this.createDashEventsTarget(r);
+ this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel);
+ this._oldWheel = r;
// prevent wheel events from passivly propagating up through containers
- !this.props.isAnnotationOverlay && r?.addEventListener('wheel', (e: WheelEvent) => this.props.isSelected() && e.preventDefault(), { passive: false });
+ r?.addEventListener('wheel', this.onPassiveWheel, { passive: false });
}}
onWheel={this.onPointerWheel}
onClick={this.onClick}
@@ -1991,12 +1989,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
onDragOver={e => e.preventDefault()}
onContextMenu={this.onContextMenu}
style={{
- pointerEvents:
- this.props.Document.type === DocumentType.MARKER
- ? 'none' // bcz: ugh.. this is here to prevent markers, which render as freeform views, from grabbing events -- need a better approach.
- : SnappingManager.GetIsDragging() && this.childDocs.includes(DragManager.docsBeingDragged.lastElement())
- ? 'all'
- : (this.props.pointerEvents?.() as any),
+ pointerEvents: this.props.isContentActive() && SnappingManager.GetIsDragging() ? 'all' : (this.props.pointerEvents?.() as any),
textAlign: this.isAnnotationOverlay ? 'initial' : undefined,
transform: `scale(${this.nativeDimScaling || 1})`,
width: `${100 / (this.nativeDimScaling || 1)}%`,
@@ -2016,8 +2009,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
onKey={this.onKeyDown}
onDoubleClick={this.onChildDoubleClickHandler}
onBrowseClick={this.onBrowseClickHandler}
- docFilters={this.childDocFilters}
- docRangeFilters={this.childDocRangeFilters}
+ childFilters={this.childDocFilters}
+ childFiltersByRanges={this.childDocRangeFilters}
searchFilterDocs={this.searchFilterDocs}
isDocumentActive={this.props.childDocumentsActive?.() ? this.props.isDocumentActive : this.isContentActive}
isContentActive={this.props.childContentsActive ?? emptyFunction}
@@ -2265,22 +2258,20 @@ class CollectionFreeFormBackgroundGrid extends React.Component<CollectionFreeFor
export function CollectionBrowseClick(dv: DocumentView, clientX: number, clientY: number) {
const browseTransitionTime = 500;
SelectionManager.DeselectAll();
- if (
- dv.props.focus(dv.props.Document, {
- willZoomCentered: true,
- zoomScale: 0.8,
- zoomTime: browseTransitionTime,
- }) === undefined
- ) {
- const selfFfview = !dv.rootDoc._isGroup && dv.ComponentView instanceof CollectionFreeFormView ? dv.ComponentView : undefined;
- let parFfview = dv.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView;
- while (parFfview?.rootDoc._isGroup) parFfview = parFfview.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView;
- const ffview = selfFfview && selfFfview.rootDoc[selfFfview.scaleFieldKey] !== 0.5 ? selfFfview : parFfview; // if focus doc is a freeform that is not at it's default 0.5 scale, then zoom out on it. Otherwise, zoom out on the parent ffview
- ffview?.zoomSmoothlyAboutPt(ffview.getTransform().transformPoint(clientX, clientY), 0.5, browseTransitionTime);
- Doc.linkFollowHighlight(dv?.props.Document, false);
- } else {
- DocumentManager.Instance.showDocument(dv.rootDoc, { zoomScale: 0.8, willZoomCentered: true });
- }
+ DocumentManager.Instance.showDocument(dv.rootDoc, { zoomScale: 0.8, willZoomCentered: true }, (focused: boolean) => {
+ if (!focused) {
+ const selfFfview = !dv.rootDoc._isGroup && dv.ComponentView instanceof CollectionFreeFormView ? dv.ComponentView : undefined;
+ let containers = dv.props.docViewPath();
+ let parFfview = dv.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView;
+ for (var cont of containers) {
+ parFfview = parFfview ?? cont.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView;
+ }
+ while (parFfview?.rootDoc._isGroup) parFfview = parFfview.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView;
+ const ffview = selfFfview && selfFfview.rootDoc[selfFfview.scaleFieldKey] !== 0.5 ? selfFfview : parFfview; // if focus doc is a freeform that is not at it's default 0.5 scale, then zoom out on it. Otherwise, zoom out on the parent ffview
+ ffview?.zoomSmoothlyAboutPt(ffview.getTransform().transformPoint(clientX, clientY), 0.5, browseTransitionTime);
+ Doc.linkFollowHighlight(dv?.props.Document, false);
+ }
+ });
}
ScriptingGlobals.add(CollectionBrowseClick);
ScriptingGlobals.add(function nextKeyFrame(readOnly: boolean) {
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
index c9168d40a..0f51fe6ff 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
@@ -4,6 +4,10 @@ import { Tooltip } from '@material-ui/core';
import { observer } from 'mobx-react';
import { unimplementedFunction } from '../../../../Utils';
import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu';
+import { IconButton } from 'browndash-components';
+import { StrCast } from '../../../../fields/Types';
+import { Doc } from '../../../../fields/Doc';
+import { computed } from 'mobx';
@observer
export class MarqueeOptionsMenu extends AntimodeMenu<AntimodeMenuProps> {
@@ -22,39 +26,44 @@ export class MarqueeOptionsMenu extends AntimodeMenu<AntimodeMenuProps> {
MarqueeOptionsMenu.Instance = this;
}
+ @computed get userColor() {
+ return StrCast(Doc.UserDoc().userColor)
+ }
+
render() {
- const presPinWithViewIcon = <img src="/assets/pinWithView.png" style={{ margin: 'auto', width: 19, transform: 'translate(-2px, -2px)' }} />;
+ const presPinWithViewIcon = <img src="/assets/pinWithView.png" style={{ width: 19 }} />;
const buttons = (
<>
- <Tooltip key="collect" title={<div className="dash-tooltip">Create a Collection</div>} placement="bottom">
- <button className="antimodeMenu-button" onPointerDown={this.createCollection}>
- <FontAwesomeIcon icon="object-group" size="lg" />
- </button>
- </Tooltip>
- ,
- <Tooltip key="group" title={<div className="dash-tooltip">Create a Grouping</div>} placement="bottom">
- <button className="antimodeMenu-button" onPointerDown={e => this.createCollection(e, true)}>
- <FontAwesomeIcon icon="layer-group" size="lg" />
- </button>
- </Tooltip>
- ,
- <Tooltip key="summarize" title={<div className="dash-tooltip">Summarize Documents</div>} placement="bottom">
- <button className="antimodeMenu-button" onPointerDown={this.summarize}>
- <FontAwesomeIcon icon="compress-arrows-alt" size="lg" />
- </button>
- </Tooltip>
- ,
- <Tooltip key="delete" title={<div className="dash-tooltip">Delete Documents</div>} placement="bottom">
- <button className="antimodeMenu-button" onPointerDown={this.delete}>
- <FontAwesomeIcon icon="trash-alt" size="lg" />
- </button>
- </Tooltip>
- ,
- <Tooltip key="pinWithView" title={<div className="dash-tooltip">Pin selected region to trail</div>} placement="bottom">
- <button className="antimodeMenu-button" onPointerDown={this.pinWithView}>
- {presPinWithViewIcon}
- </button>
- </Tooltip>
+ <IconButton
+ tooltip={"Create a Collection"}
+ onPointerDown={this.createCollection}
+ icon={<FontAwesomeIcon icon="object-group"/>}
+ color={this.userColor}
+ />
+ <IconButton
+ tooltip={"Create a Grouping"}
+ onPointerDown={e => this.createCollection(e, true)}
+ icon={<FontAwesomeIcon icon="layer-group"/>}
+ color={this.userColor}
+ />
+ <IconButton
+ tooltip={"Summarize Documents"}
+ onPointerDown={this.summarize}
+ icon={<FontAwesomeIcon icon="compress-arrows-alt"/>}
+ color={this.userColor}
+ />
+ <IconButton
+ tooltip={"Delete Documents"}
+ onPointerDown={this.delete}
+ icon={<FontAwesomeIcon icon="trash-alt"/>}
+ color={this.userColor}
+ />
+ <IconButton
+ tooltip={"Pin selected region"}
+ onPointerDown={this.pinWithView}
+ icon={presPinWithViewIcon}
+ color={this.userColor}
+ />
</>
);
return this.getElement(buttons);
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index fdff13b54..7c53bfdbe 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -1,6 +1,7 @@
import { action, computed, observable } from 'mobx';
import { observer } from 'mobx-react';
-import { AclAdmin, AclAugment, AclEdit, DataSym, Doc, Opt } from '../../../../fields/Doc';
+import { Doc, Opt } from '../../../../fields/Doc';
+import { AclAdmin, AclAugment, AclEdit, DocData } from '../../../../fields/DocSymbols';
import { Id } from '../../../../fields/FieldSymbols';
import { InkData, InkField, InkTool } from '../../../../fields/InkField';
import { List } from '../../../../fields/List';
@@ -8,7 +9,7 @@ import { RichTextField } from '../../../../fields/RichTextField';
import { SchemaHeaderField } from '../../../../fields/SchemaHeaderField';
import { Cast, DocCast, FieldValue, NumCast, StrCast } from '../../../../fields/Types';
import { ImageField } from '../../../../fields/URLField';
-import { GetEffectiveAcl } from '../../../../fields/util';
+import { distributeAcls, GetEffectiveAcl, SharingPermissions } from '../../../../fields/util';
import { intersectRect, returnFalse, Utils } from '../../../../Utils';
import { CognitiveServices } from '../../../cognitive_services/CognitiveServices';
import { Docs, DocumentOptions, DocUtils } from '../../../documents/Documents';
@@ -102,7 +103,6 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
this._lassoPts = [];
};
- @undoBatch
@action
onKeyPress = (e: KeyboardEvent) => {
//make textbox and add it to this collection
@@ -111,7 +111,6 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
const [x, y] = this.Transform.transformPoint(this._downX, this._downY);
if (e.key === '?') {
cm.setDefaultItem('?', (str: string) => this.props.addDocTab(Docs.Create.WebDocument(`https://bing.com/search?q=${str}`, { _width: 400, x, y, _height: 512, _nativeWidth: 850, title: 'bing', data_useCors: true }), OpenWhere.addRight));
-
cm.displayMenu(this._downX, this._downY, undefined, true);
e.stopPropagation();
} else if (e.key === 'u' && this.props.ungroup) {
@@ -162,7 +161,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
});
})
);
- } else if (e.key === 's' && e.ctrlKey) {
+ } /* else if (e.key === 's' && e.ctrlKey) {
e.preventDefault();
const slide = DocUtils.copyDragFactory(DocCast(Doc.UserDoc().emptySlide))!;
slide.x = x;
@@ -171,9 +170,9 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
TreeView._editTitleOnLoad = { id: slide[Id], parent: undefined };
this.props.addDocument?.(slide);
e.stopPropagation();
- } else if (!e.ctrlKey && !e.metaKey && SelectionManager.Views().length < 2) {
+ }*/ else if (!e.ctrlKey && !e.metaKey && SelectionManager.Views().length < 2) {
FormattedTextBox.SelectOnLoadChar = Doc.UserDoc().defaultTextLayout && !this.props.childLayoutString ? e.key : '';
- FormattedTextBox.LiveTextUndo = UndoManager.StartBatch('live text batch');
+ FormattedTextBox.LiveTextUndo = UndoManager.StartBatch('type new note');
this.props.addLiveTextDocument(DocUtils.GetNewTextDoc('-typed text-', x, y, 200, 100, this.props.xPadding === 0));
e.stopPropagation();
}
@@ -327,7 +326,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
} else {
this._downX = x;
this._downY = y;
- const effectiveAcl = GetEffectiveAcl(this.props.Document[DataSym]);
+ const effectiveAcl = GetEffectiveAcl(this.props.Document[DocData]);
if ([AclAdmin, AclEdit, AclAugment].includes(effectiveAcl)) {
PreviewCursor.Show(x, y, this.onKeyPress, this.props.addLiveTextDocument, this.props.getTransform, this.props.addDocument, this.props.nudge, this.props.slowLoadDocuments);
}
@@ -338,7 +337,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
@action
onClick = (e: React.MouseEvent): void => {
if (this.props.pointerEvents?.() === 'none') return;
- if (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) {
+ if (Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, Date.now())) {
if (Doc.ActiveTool === InkTool.None) {
if (!(e.nativeEvent as any).marqueeHit) {
(e.nativeEvent as any).marqueeHit = true;
@@ -378,7 +377,6 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
: ((doc: Doc) => {
Doc.GetProto(doc).data = new List<Doc>(selected);
Doc.GetProto(doc).title = makeGroup ? 'grouping' : 'nested freeform';
- !this.props.isAnnotationOverlay && Doc.AddFileOrphan(Doc.GetProto(doc));
doc._freeform_panX = doc._freeform_panY = 0;
return doc;
})(Doc.MakeCopy(Doc.UserDoc().emptyCollection as Doc, true));
@@ -386,16 +384,17 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
newCollection._width = this.Bounds.width;
newCollection._height = this.Bounds.height;
newCollection._isGroup = makeGroup;
+ newCollection._dragWhenActive = makeGroup;
newCollection.forceActive = makeGroup;
- newCollection.enableDragWhenActive = makeGroup;
newCollection.x = this.Bounds.left;
newCollection.y = this.Bounds.top;
newCollection.layout_fitWidth = true;
- selected.forEach(d => (d.embedContainer = newCollection));
+ selected.forEach(d => Doc.SetContainer(d, newCollection));
this.hideMarquee();
return newCollection;
});
+ @undoBatch
@action
pileup = (e: KeyboardEvent | React.PointerEvent | undefined) => {
const selected = this.marqueeSelect(false);
@@ -432,8 +431,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
}
const newCollection = this.getCollection(selected, (e as KeyboardEvent)?.key === 't' ? Docs.Create.StackingDocument : undefined, group);
- newCollection._panX = this.Bounds.left + this.Bounds.width / 2;
- newCollection._panY = this.Bounds.top + this.Bounds.height / 2;
+ newCollection._freeform_panX = this.Bounds.left + this.Bounds.width / 2;
+ newCollection._freeform_panY = this.Bounds.top + this.Bounds.height / 2;
newCollection._currentFrame = activeFrame;
this.props.addDocument?.(newCollection);
this.props.selectDocuments([newCollection]);
@@ -508,8 +507,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
};
@undoBatch
- @action
- summary = (e: KeyboardEvent | React.PointerEvent | undefined) => {
+ summary = action((e: KeyboardEvent | React.PointerEvent | undefined) => {
const selected = this.marqueeSelect(false).map(d => {
this.props.removeDocument?.(d);
d.x = NumCast(d.x) - this.Bounds.left;
@@ -523,7 +521,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
followLinkToggle: true,
_width: 200,
_height: 200,
- _layoutFitContentsToBox: true,
+ _layout_fitContentsToBox: true,
_layout_showSidebar: true,
title: 'overview',
});
@@ -534,19 +532,10 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
this.props.addDocument?.(portal);
this.props.addLiveTextDocument(summary);
MarqueeOptionsMenu.Instance.fadeOut(true);
- };
+ });
@action
- background = (e: KeyboardEvent | React.PointerEvent | undefined) => {
- const newCollection = this.getCollection([], undefined, undefined);
- this.props.addDocument?.(newCollection);
- MarqueeOptionsMenu.Instance.fadeOut(true);
- this.hideMarquee();
- setTimeout(() => this.props.selectDocuments([newCollection]));
- };
-
- @undoBatch
- marqueeCommand = action((e: KeyboardEvent) => {
+ marqueeCommand = (e: KeyboardEvent) => {
if (this._commandExecuted || (e as any).propagationIsStopped) {
return;
}
@@ -557,7 +546,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
this.delete(e, e.key === 'h');
e.stopPropagation();
}
- if ('cbtsSpg'.indexOf(e.key) !== -1) {
+ if ('ctsSpg'.indexOf(e.key) !== -1) {
this._commandExecuted = true;
e.stopPropagation();
e.preventDefault();
@@ -565,7 +554,6 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
if (e.key === 'g') this.collection(e, true);
if (e.key === 'c' || e.key === 't') this.collection(e);
if (e.key === 's' || e.key === 'S') this.summary(e);
- if (e.key === 'b') this.background(e);
if (e.key === 'p') this.pileup(e);
this.cleanupInteractions(false);
}
@@ -575,7 +563,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
e.preventDefault();
this._lassoFreehand = !this._lassoFreehand;
}
- });
+ };
touchesLine(r1: { left: number; top: number; width: number; height: number }) {
for (const lassoPt of this._lassoPts) {
diff --git a/src/client/views/collections/collectionGrid/CollectionGridView.tsx b/src/client/views/collections/collectionGrid/CollectionGridView.tsx
index e8ae88ae5..cd8b7a0cc 100644
--- a/src/client/views/collections/collectionGrid/CollectionGridView.tsx
+++ b/src/client/views/collections/collectionGrid/CollectionGridView.tsx
@@ -368,7 +368,7 @@ export class CollectionGridView extends CollectionSubView() {
<div
className="collectionGridView-contents"
ref={this.createDashEventsTarget}
- style={{ pointerEvents: !this.props.isContentActive() && !SnappingManager.GetIsDragging() ? 'none' : undefined }}
+ style={{ pointerEvents: !this.props.isContentActive() ? 'none' : undefined }}
onContextMenu={this.onContextMenu}
onPointerDown={this.onPointerDown}
onDrop={this.onExternalDrop}>
diff --git a/src/client/views/collections/collectionLinear/CollectionLinearView.scss b/src/client/views/collections/collectionLinear/CollectionLinearView.scss
index 3e3709827..6b3318bf3 100644
--- a/src/client/views/collections/collectionLinear/CollectionLinearView.scss
+++ b/src/client/views/collections/collectionLinear/CollectionLinearView.scss
@@ -17,12 +17,7 @@
.collectionLinearView-menuOpener {
user-select: none;
}
-
- &.true {
- border-left: $standard-border;
- background-color: $medium-blue-alt;
- }
-
+
> 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 efd73a927..2254b2e5f 100644
--- a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
+++ b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
@@ -1,11 +1,12 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tooltip } from '@material-ui/core';
-import { action, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
+import { action, IReactionDisposer, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { Doc, HeightSym, Opt, WidthSym } from '../../../../fields/Doc';
+import { Doc, Opt } from '../../../../fields/Doc';
+import { Height, Width } from '../../../../fields/DocSymbols';
import { Id } from '../../../../fields/FieldSymbols';
-import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
+import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
import { emptyFunction, returnEmptyDoclist, returnTrue, StopEvent, Utils } from '../../../../Utils';
import { CollectionViewType } from '../../../documents/DocumentTypes';
import { DocumentManager } from '../../../util/DocumentManager';
@@ -15,14 +16,17 @@ import { DocumentLinksButton } from '../../nodes/DocumentLinksButton';
import { DocumentView } from '../../nodes/DocumentView';
import { LinkDescriptionPopup } from '../../nodes/LinkDescriptionPopup';
import { StyleProp } from '../../StyleProvider';
+import { UndoStack } from '../../UndoStack';
import { CollectionStackedTimeline } from '../CollectionStackedTimeline';
import { CollectionSubView } from '../CollectionSubView';
import './CollectionLinearView.scss';
+import { Button, Toggle, ToggleType, Type } from 'browndash-components';
+import { Colors } from '../../global/globalEnums';
/**
* CollectionLinearView is the class for rendering the horizontal collection
* of documents, it useful for horizontal menus. It can either be expandable
- * or not using the linearViewExpandable field.
+ * or not using the linearView_Expandable field.
* It is used in the following locations:
* - It is used in the popup menu on the bottom left (see docButtons() in MainView.tsx)
* - It is used for the context sensitive toolbar at the top (see contMenuButtons() in CollectionMenu.tsx)
@@ -39,43 +43,19 @@ export class CollectionLinearView extends CollectionSubView() {
this._dropDisposer?.();
this._widthDisposer?.();
this._selectedDisposer?.();
- this.childLayoutPairs.map((pair, ind) => ScriptCast(pair.layout.proto?.onPointerUp)?.script.run({ this: pair.layout.proto }, console.log));
+ this.childLayoutPairs.map((pair, ind) => ScriptCast(DocCast(pair.layout.proto)?.onPointerUp)?.script.run({ this: pair.layout.proto }, console.log));
}
componentDidMount() {
this._widthDisposer = reaction(
- () => 5 + NumCast(this.rootDoc.linearBtnWidth, this.dimension()) + (this.layoutDoc.linearViewIsExpanded ? this.childDocs.filter(doc => !doc.hidden).reduce((tot, doc) => (doc[WidthSym]() || this.dimension()) + tot + 4, 0) : 0),
+ () => 5 + NumCast(this.rootDoc.linearBtnWidth, this.dimension()) + (this.layoutDoc.linearView_IsOpen ? this.childDocs.filter(doc => !doc.hidden).reduce((tot, doc) => (doc[Width]() || this.dimension()) + tot + 4, 0) : 0),
width => this.childDocs.length && (this.layoutDoc._width = width),
{ fireImmediately: true }
);
-
- this._selectedDisposer = reaction(
- () => NumCast(this.layoutDoc.selectedIndex),
- i =>
- runInAction(() => {
- this._selectedIndex = i;
- let selected: any = undefined;
- this.childLayoutPairs.map(async (pair, ind) => {
- const isSelected = this._selectedIndex === ind;
- if (isSelected) {
- selected = pair;
- } else {
- ScriptCast(pair.layout.proto?.onPointerUp)?.script.run({ this: pair.layout.proto }, console.log);
- }
- });
- if (selected && selected.layout) {
- ScriptCast(selected.layout.proto?.onPointerDown)?.script.run({ this: selected.layout.proto }, console.log);
- }
- }),
- { fireImmediately: true }
- );
}
protected createDashEventsTarget = (ele: HTMLDivElement | null) => {
- //used for stacking and masonry view
- this._dropDisposer && this._dropDisposer();
- if (ele) {
- this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.layoutDoc);
- }
+ this._dropDisposer?.();
+ if (ele) this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.layoutDoc);
};
dimension = () => NumCast(this.rootDoc._height);
@@ -87,12 +67,8 @@ export class CollectionLinearView extends CollectionSubView() {
@action
exitLongLinks = () => {
- if (DocumentLinksButton.StartLink) {
- if (DocumentLinksButton.StartLink.Document) {
- action((e: React.PointerEvent<HTMLDivElement>) => {
- Doc.UnBrushDoc(DocumentLinksButton.StartLink?.Document as Doc);
- });
- }
+ if (DocumentLinksButton.StartLink?.Document) {
+ action((e: React.PointerEvent<HTMLDivElement>) => Doc.UnBrushDoc(DocumentLinksButton.StartLink?.Document as Doc));
}
DocumentLinksButton.StartLink = undefined;
DocumentLinksButton.StartLinkView = undefined;
@@ -161,11 +137,17 @@ export class CollectionLinearView extends CollectionSubView() {
</span>
);
};
+
getDisplayDoc = (doc: Doc, preview: boolean = false) => {
- if (doc.icon === 'linkui') return this.getLinkUI();
- if (doc.icon === 'currentlyplayingui') return this.getCurrentlyPlayingUI();
+ // hack to avoid overhead of making UndoStack,etc into DocumentView style Boxes. If the UndoStack is ever intended to become part of the persisten state of the dashboard, then this would have to change.
+ // prettier-ignore
+ switch (doc.layout) {
+ case '<LinkingUI>': return this.getLinkUI();
+ case '<CurrentlyPlayingUI>': return this.getCurrentlyPlayingUI();
+ case '<UndoStack>': return <UndoStack key={doc[Id]} width={200} height={40} inline={true} />;
+ }
- const nested = doc._viewType === CollectionViewType.Linear;
+ const nested = doc._type_collection === CollectionViewType.Linear;
const hidden = doc.hidden === true;
let dref: Opt<HTMLDivElement>;
@@ -193,12 +175,12 @@ export class CollectionLinearView extends CollectionSubView() {
moveDocument={this.props.moveDocument}
addDocTab={this.props.addDocTab}
pinToPres={emptyFunction}
- dropAction={StrCast(this.layoutDoc.childDropAction) as dropActionType}
+ dragAction={(this.layoutDoc.childDragAction ?? this.props.childDragAction) as dropActionType}
rootSelected={this.props.isSelected}
removeDocument={this.props.removeDocument}
ScreenToLocalTransform={docXf}
- PanelWidth={doc[WidthSym]}
- PanelHeight={nested || doc._height ? doc[HeightSym] : this.dimension}
+ PanelWidth={doc[Width]}
+ PanelHeight={nested || doc._height ? doc[Height] : this.dimension}
renderDepth={this.props.renderDepth + 1}
dontRegisterView={BoolCast(this.rootDoc.childDontRegisterViews)}
focus={emptyFunction}
@@ -206,8 +188,8 @@ export class CollectionLinearView extends CollectionSubView() {
docViewPath={returnEmptyDoclist}
whenChildContentsActiveChanged={emptyFunction}
bringToFront={emptyFunction}
- docFilters={this.props.docFilters}
- docRangeFilters={this.props.docRangeFilters}
+ childFilters={this.props.childFilters}
+ childFiltersByRanges={this.props.childFiltersByRanges}
searchFilterDocs={this.props.searchFilterDocs}
hideResizeHandles={true}
/>
@@ -218,54 +200,45 @@ export class CollectionLinearView extends CollectionSubView() {
render() {
const flexDir = StrCast(this.Document.flexDirection); // Specify direction of linear view content
const flexGap = NumCast(this.Document.flexGap); // Specify the gap between linear view content
- const isExpanded = BoolCast(this.layoutDoc.linearViewIsExpanded);
+ const isExpanded = BoolCast(this.layoutDoc.linearView_IsOpen);
const menuOpener = (
- <label
- className={`collectionlinearView-label${isExpanded ? '-expanded' : ''}`}
- htmlFor={this.Document[Id] + '-input'}
- style={{ boxShadow: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BoxShadow) }}
- onPointerDown={StopEvent}>
- <div className="collectionLinearView-menuOpener">{Cast(this.props.Document.icon, 'string', null) ?? <FontAwesomeIcon icon={isExpanded ? 'minus' : 'plus'} />}</div>
- </label>
+ <Toggle
+ text={Cast(this.props.Document.icon, 'string', null)}
+ icon={Cast(this.props.Document.icon, 'string', null) ? undefined : <FontAwesomeIcon icon={isExpanded ? 'minus' : 'plus'} />}
+ type={Type.TERT}
+ color={StrCast(Doc.UserDoc().userVariantColor, Colors.MEDIUM_BLUE)}
+ onPointerDown={e => e.stopPropagation()}
+ toggleType={ToggleType.BUTTON}
+ toggleStatus={BoolCast(this.layoutDoc.linearView_IsOpen)}
+ onClick={() => {
+ this.layoutDoc.linearView_IsOpen = !isExpanded;
+ }}
+ tooltip={isExpanded ? 'Close' : 'Open'}
+ fillWidth={true}
+ align={'center'}
+ />
);
return (
- <div className={`collectionLinearView-outer ${this.layoutDoc.linearViewSubMenu}`} style={{ backgroundColor: this.layoutDoc.linearViewIsExpanded ? undefined : 'transparent' }}>
- <div className="collectionLinearView" ref={this.createDashEventsTarget} onContextMenu={this.myContextMenu} style={{ minHeight: this.dimension() }}>
- {!this.props.Document.linearViewExpandable ? null : (
- <Tooltip title={<div className="dash-tooltip">{isExpanded ? 'Close' : 'Open'}</div>} placement="top">
+ <div className={`collectionLinearView-outer ${this.layoutDoc.linearView_SubMenu}`} style={{ backgroundColor: this.layoutDoc.linearView_IsOpen ? undefined : 'transparent' }}>
+ <div className="collectionLinearView" ref={this.createDashEventsTarget} onContextMenu={this.myContextMenu} style={{ minHeight: this.dimension(), pointerEvents: 'all' }}>
+ {
+ <>
{menuOpener}
- </Tooltip>
- )}
- <input
- id={this.Document[Id] + '-input'}
- type="checkbox"
- checked={isExpanded}
- ref={this.addMenuToggle}
- onChange={action(e => {
- ScriptCast(this.Document.onClick)?.script.run({
- this: this.layoutDoc,
- self: this.rootDoc,
- _readOnly_: false,
- scriptContext: this.props.scriptContext,
- documentView: this.props.DocumentView?.(),
- });
- this.layoutDoc.linearViewIsExpanded = this.addMenuToggle.current!.checked;
- })}
- />
-
- {!this.layoutDoc.linearViewIsExpanded ? null : (
- <div
- className="collectionLinearView-content"
- style={{
- height: this.dimension(),
- flexDirection: flexDir as any,
- gap: flexGap,
- }}>
- {this.childLayoutPairs.map(pair => this.getDisplayDoc(pair.layout))}
- </div>
- )}
+ {!this.layoutDoc.linearView_IsOpen ? null : (
+ <div
+ className="collectionLinearView-content"
+ style={{
+ height: this.dimension(),
+ flexDirection: flexDir as any,
+ gap: flexGap,
+ }}>
+ {this.childLayoutPairs.map(pair => this.getDisplayDoc(pair.layout))}
+ </div>
+ )}
+ </>
+ }
</div>
</div>
);
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss
index 821c8d804..f87a06033 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss
@@ -8,6 +8,11 @@
display: flex;
flex-direction: column;
width: 100%;
+ align-items: center;
+
+ .contentFittingDocumentView {
+ width: unset;
+ }
.label-wrapper {
display: flex;
@@ -15,7 +20,6 @@
justify-content: center;
height: 20px;
}
-
}
.multiColumnResizer {
@@ -30,5 +34,4 @@
transition: 0.5s background-color ease;
}
}
-
-} \ No newline at end of file
+}
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
index 78d3d1b6e..10532b9d9 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
@@ -5,7 +5,6 @@ import { Doc, DocListCast } from '../../../../fields/Doc';
import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
import { returnFalse } from '../../../../Utils';
import { DragManager, dropActionType } from '../../../util/DragManager';
-import { SnappingManager } from '../../../util/SnappingManager';
import { Transform } from '../../../util/Transform';
import { undoBatch } from '../../../util/UndoManager';
import { DocumentView } from '../../nodes/DocumentView';
@@ -194,11 +193,7 @@ export class CollectionMulticolumnView extends CollectionSubView() {
let dropInd = -1;
if (de.complete.docDragData && this._mainCont) {
let curInd = -1;
- de.complete.docDragData?.droppedDocuments.forEach(
- action((d: Doc) => {
- curInd = this.childDocs.indexOf(d);
- })
- );
+ de.complete.docDragData?.droppedDocuments.forEach(d => (curInd = this.childDocs.indexOf(d)));
Array.from(this._mainCont.children).forEach((child, index) => {
const brect = child.getBoundingClientRect();
if (brect.x < de.x && brect.x + brect.width > de.x) {
@@ -225,6 +220,7 @@ export class CollectionMulticolumnView extends CollectionSubView() {
}
})
);
+ return true;
}
}
return false;
@@ -234,8 +230,15 @@ export class CollectionMulticolumnView extends CollectionSubView() {
onChildDoubleClickHandler = () => ScriptCast(this.Document.onChildDoubleClick);
isContentActive = () => this.props.isSelected() || this.props.isContentActive() || this.props.isAnyChildContentActive();
- isChildContentActive = () => (((this.props.childDocumentsActive?.() || this.Document._childDocumentsActive) && this.props.isDocumentActive?.() && SnappingManager.GetIsDragging()) || this.isContentActive() ? true : false);
- getDisplayDoc = (layout: Doc, dxf: () => Transform, width: () => number, height: () => number) => {
+ isChildContentActive = () => {
+ const childDocsActive = this.props.childDocumentsActive?.() ?? this.rootDoc.childDocumentsActive;
+ return this.props.isContentActive?.() === false || childDocsActive === false
+ ? false //
+ : this.props.isDocumentActive?.() && childDocsActive
+ ? true
+ : undefined;
+ };
+ getDisplayDoc = (layout: Doc, dxf: () => Transform, width: () => number, height: () => number, shouldNotScale: () => boolean) => {
return (
<DocumentView
Document={layout}
@@ -247,8 +250,9 @@ export class CollectionMulticolumnView extends CollectionSubView() {
renderDepth={this.props.renderDepth + 1}
PanelWidth={width}
PanelHeight={height}
+ shouldNotScale={shouldNotScale}
rootSelected={this.rootSelected}
- dropAction={StrCast(this.props.Document.childDropAction) as dropActionType}
+ dragAction={(this.props.Document.childDragAction ?? this.props.childDragAction) as dropActionType}
onClick={this.onChildClickHandler}
onDoubleClick={this.onChildDoubleClickHandler}
suppressSetHeight={true}
@@ -259,8 +263,8 @@ export class CollectionMulticolumnView extends CollectionSubView() {
hideDecorationTitle={this.props.childHideDecorationTitle?.()}
fitContentsToBox={this.props.fitContentsToBox}
focus={this.props.focus}
- docFilters={this.childDocFilters}
- docRangeFilters={this.childDocRangeFilters}
+ childFilters={this.childDocFilters}
+ childFiltersByRanges={this.childDocRangeFilters}
searchFilterDocs={this.searchFilterDocs}
dontRegisterView={this.props.dontRegisterView}
addDocument={this.props.addDocument}
@@ -284,15 +288,19 @@ export class CollectionMulticolumnView extends CollectionSubView() {
const collector: JSX.Element[] = [];
for (let i = 0; i < childLayoutPairs.length; i++) {
const { layout } = childLayoutPairs[i];
+ const aspect = Doc.NativeAspect(layout, undefined, true);
+ const width = () => this.lookupPixels(layout);
+ const height = () => PanelHeight() - 2 * NumCast(Document._yMargin) - (BoolCast(Document.showWidthLabels) ? 20 : 0);
+ const docwidth = () => (layout._layout_forceReflow ? width() : Math.min(height() * aspect, width()));
+ const docheight = () => Math.min(docwidth() / aspect, height());
const dxf = () =>
this.lookupIndividualTransform(layout)
- .translate(-NumCast(Document._xMargin), -NumCast(Document._yMargin))
+ .translate(-NumCast(Document._xMargin) - (width() - docwidth()) / 2, -NumCast(Document._yMargin))
.scale(this.props.NativeDimScaling?.() || 1);
- const width = () => this.lookupPixels(layout);
- const height = () => PanelHeight() - 2 * NumCast(Document._yMargin) - (BoolCast(Document.showWidthLabels) ? 20 : 0);
+ const shouldNotScale = () => this.props.fitContentsToBox?.() || BoolCast(layout.freeform_fitContentsToBox);
collector.push(
- <div className={'document-wrapper'} key={'wrapper' + i} style={{ width: width() }}>
- {this.getDisplayDoc(layout, dxf, width, height)}
+ <div className="document-wrapper" key={'wrapper' + i} style={{ width: width() }}>
+ {this.getDisplayDoc(layout, dxf, docwidth, docheight, shouldNotScale)}
<WidthLabel layout={layout} collectionDoc={Document} />
</div>,
<ResizeBar
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.scss b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.scss
index 79fb195e8..ec7200a03 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.scss
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.scss
@@ -9,6 +9,12 @@
display: flex;
flex-direction: row;
height: 100%;
+ align-items: center;
+ margin: auto;
+
+ .contentFittingDocumentView {
+ height: unset;
+ }
.label-wrapper {
display: flex;
@@ -16,7 +22,6 @@
justify-content: center;
height: 20px;
}
-
}
.multiRowResizer {
@@ -31,5 +36,4 @@
transition: 0.5s background-color ease;
}
}
-
-} \ No newline at end of file
+}
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
index 4d61dc272..04cfc5456 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
@@ -5,7 +5,6 @@ import { Doc, DocListCast } from '../../../../fields/Doc';
import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
import { returnFalse } from '../../../../Utils';
import { DragManager, dropActionType } from '../../../util/DragManager';
-import { SnappingManager } from '../../../util/SnappingManager';
import { Transform } from '../../../util/Transform';
import { undoBatch } from '../../../util/UndoManager';
import { DocumentView } from '../../nodes/DocumentView';
@@ -194,11 +193,7 @@ export class CollectionMultirowView extends CollectionSubView() {
let dropInd = -1;
if (de.complete.docDragData && this._mainCont) {
let curInd = -1;
- de.complete.docDragData?.droppedDocuments.forEach(
- action((d: Doc) => {
- curInd = this.childDocs.indexOf(d);
- })
- );
+ de.complete.docDragData?.droppedDocuments.forEach(d => (curInd = this.childDocs.indexOf(d)));
Array.from(this._mainCont.children).forEach((child, index) => {
const brect = child.getBoundingClientRect();
if (brect.y < de.y && brect.y + brect.height > de.y) {
@@ -225,6 +220,7 @@ export class CollectionMultirowView extends CollectionSubView() {
}
})
);
+ return true;
}
}
return false;
@@ -234,8 +230,15 @@ export class CollectionMultirowView extends CollectionSubView() {
onChildDoubleClickHandler = () => ScriptCast(this.Document.onChildDoubleClick);
isContentActive = () => this.props.isSelected() || this.props.isContentActive() || this.props.isAnyChildContentActive();
- isChildContentActive = () => (((this.props.childDocumentsActive?.() || this.Document._childDocumentsActive) && this.props.isDocumentActive?.() && SnappingManager.GetIsDragging()) || this.isContentActive() ? true : false);
- getDisplayDoc = (layout: Doc, dxf: () => Transform, width: () => number, height: () => number) => {
+ isChildContentActive = () => {
+ const childDocsActive = this.props.childDocumentsActive?.() ?? this.rootDoc.childDocumentsActive;
+ return this.props.isContentActive?.() === false || childDocsActive === false
+ ? false //
+ : this.props.isDocumentActive?.() && childDocsActive
+ ? true
+ : undefined;
+ };
+ getDisplayDoc = (layout: Doc, dxf: () => Transform, width: () => number, height: () => number, shouldNotScale: () => boolean) => {
return (
<DocumentView
Document={layout}
@@ -247,8 +250,9 @@ export class CollectionMultirowView extends CollectionSubView() {
renderDepth={this.props.renderDepth + 1}
PanelWidth={width}
PanelHeight={height}
+ shouldNotScale={shouldNotScale}
rootSelected={this.rootSelected}
- dropAction={StrCast(this.props.Document.childDropAction) as dropActionType}
+ dropAction={StrCast(this.rootDoc.childDragAction) as dropActionType}
onClick={this.onChildClickHandler}
onDoubleClick={this.onChildDoubleClickHandler}
ScreenToLocalTransform={dxf}
@@ -257,9 +261,10 @@ export class CollectionMultirowView extends CollectionSubView() {
hideResizeHandles={this.props.childHideResizeHandles?.()}
hideDecorationTitle={this.props.childHideDecorationTitle?.()}
fitContentsToBox={this.props.fitContentsToBox}
+ dragAction={this.props.childDragAction}
focus={this.props.focus}
- docFilters={this.childDocFilters}
- docRangeFilters={this.childDocRangeFilters}
+ childFilters={this.childDocFilters}
+ childFiltersByRanges={this.childDocRangeFilters}
searchFilterDocs={this.searchFilterDocs}
dontRegisterView={this.props.dontRegisterView}
addDocument={this.props.addDocument}
@@ -283,15 +288,19 @@ export class CollectionMultirowView extends CollectionSubView() {
const collector: JSX.Element[] = [];
for (let i = 0; i < childLayoutPairs.length; i++) {
const { layout } = childLayoutPairs[i];
+ const aspect = Doc.NativeAspect(layout, undefined, true);
+ const height = () => this.lookupPixels(layout);
+ const width = () => PanelWidth() - 2 * NumCast(Document._xMargin) - (BoolCast(Document.showWidthLabels) ? 20 : 0);
+ const docheight = () => Math.min(width() / aspect, height());
+ const docwidth = () => (layout._layout_forceReflow ? width() : Math.min(width(), docheight() * aspect));
const dxf = () =>
this.lookupIndividualTransform(layout)
- .translate(-NumCast(Document._xMargin), -NumCast(Document._yMargin))
+ .translate(-NumCast(Document._xMargin) - (width() - docwidth()) / 2, -NumCast(Document._yMargin) - (height() - docheight()) / 2)
.scale(this.props.NativeDimScaling?.() || 1);
- const height = () => this.lookupPixels(layout);
- const width = () => PanelWidth() - 2 * NumCast(Document._xMargin) - (BoolCast(Document.showWidthLabels) ? 20 : 0);
+ const shouldNotScale = () => this.props.fitContentsToBox?.() || BoolCast(layout.freeform_fitContentsToBox);
collector.push(
- <div className={'document-wrapper'} style={{ height: height() }} key={'wrapper' + i}>
- {this.getDisplayDoc(layout, dxf, width, height)}
+ <div className="document-wrapper" style={{ height: height() }} key={'wrapper' + i}>
+ {this.getDisplayDoc(layout, dxf, docwidth, docheight, shouldNotScale)}
<HeightLabel layout={layout} collectionDoc={Document} />
</div>,
<ResizeBar
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.scss b/src/client/views/collections/collectionSchema/CollectionSchemaView.scss
index 3a0c2c85c..52ebb7763 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaView.scss
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.scss
@@ -104,7 +104,7 @@
.schema-header-row {
cursor: grab;
- justify-content: flex-end;
+ //justify-content: flex-end;
.row-menu {
display: flex;
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
index bcc2ca129..babe5c810 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
@@ -8,17 +8,16 @@ import { Id } from '../../../../fields/FieldSymbols';
import { List } from '../../../../fields/List';
import { listSpec } from '../../../../fields/Schema';
import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types';
-import { emptyFunction, returnDefault, returnEmptyDoclist, returnEmptyString, returnFalse, returnIgnore, returnNever, returnTrue, setupMoveUpEvents, smoothScroll } from '../../../../Utils';
+import { emptyFunction, returnEmptyDoclist, returnEmptyString, returnFalse, returnIgnore, returnNever, returnTrue, setupMoveUpEvents, smoothScroll } from '../../../../Utils';
import { Docs, DocumentOptions, DocUtils, FInfo } from '../../../documents/Documents';
import { DocumentManager } from '../../../util/DocumentManager';
import { DragManager } from '../../../util/DragManager';
import { SelectionManager } from '../../../util/SelectionManager';
-import { undoBatch } from '../../../util/UndoManager';
+import { undoable, undoBatch } from '../../../util/UndoManager';
import { ContextMenu } from '../../ContextMenu';
import { EditableView } from '../../EditableView';
import { Colors } from '../../global/globalEnums';
import { DocFocusOptions, DocumentView } from '../../nodes/DocumentView';
-import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
import { KeyValueBox } from '../../nodes/KeyValueBox';
import { DefaultStyleProvider } from '../../StyleProvider';
import { CollectionSubView } from '../CollectionSubView';
@@ -33,6 +32,7 @@ export enum ColumnType {
Date,
Image,
RTF,
+ Enumeration,
Any,
}
@@ -43,9 +43,10 @@ export const FInfotoColType: { [key: string]: ColumnType } = {
date: ColumnType.Date,
image: ColumnType.Image,
rtf: ColumnType.RTF,
+ enumeration: ColumnType.Enumeration,
};
-const defaultColumnKeys: string[] = ['title', 'type', 'author', 'creationDate', 'text'];
+const defaultColumnKeys: string[] = ['title', 'type', 'author', 'author_date', 'text'];
@observer
export class CollectionSchemaView extends CollectionSubView() {
@@ -55,6 +56,7 @@ export class CollectionSchemaView extends CollectionSubView() {
private _makeNewColumn: boolean = false;
private _documentOptions: DocumentOptions = new DocumentOptions();
private _tableContentRef: HTMLDivElement | null = null;
+ private _menuTarget = React.createRef<HTMLDivElement>();
static _rowHeight: number = 50;
static _rowSingleLineHeight: number = 32;
@@ -78,6 +80,11 @@ export class CollectionSchemaView extends CollectionSubView() {
@observable _filterSearchValue: string = '';
@observable _selectedCell: [Doc, number] | undefined;
+ // target HTMLelement portal for showing a popup menu to edit cell values.
+ public get MenuTarget() {
+ return this._menuTarget.current;
+ }
+
@computed get _selectedDocs() {
return SelectionManager.Docs().filter(doc => Doc.AreProtosEqual(DocCast(doc.embedContainer), this.rootDoc));
}
@@ -224,7 +231,7 @@ export class CollectionSchemaView extends CollectionSubView() {
@undoBatch
@action
- setSort = (field: string | undefined, desc: boolean = false) => {
+ setColumnSort = (field: string | undefined, desc: boolean = false) => {
this.layoutDoc.sortField = field;
this.layoutDoc.sortDesc = desc;
};
@@ -337,9 +344,7 @@ export class CollectionSchemaView extends CollectionSubView() {
dragColumn = (e: PointerEvent, index: number) => {
const dragData = new DragManager.ColumnDragData(index);
const dragEles = [this._colEles[index]];
- this.childDocs.forEach(doc => {
- dragEles.push(this._rowEles.get(doc).children[1].children[index]);
- });
+ this.childDocs.forEach(doc => dragEles.push(this._rowEles.get(doc).children[1].children[index]));
DragManager.StartColumnDrag(dragEles, dragData, e.x, e.y);
document.removeEventListener('pointermove', this.highlightDropColumn);
@@ -353,24 +358,28 @@ export class CollectionSchemaView extends CollectionSubView() {
return true;
};
- @action
- highlightDropColumn = (e: PointerEvent) => {
- e.stopPropagation();
- const mouseX = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY)[0];
+ findDropIndex = (mouseX: number) => {
let index: number | undefined;
this.displayColumnWidths.reduce((total, curr, i) => {
if (total <= mouseX && total + curr >= mouseX) {
- if (mouseX <= total + curr / 2) index = i;
+ if (mouseX <= total + curr) index = i;
else index = i + 1;
}
return total + curr;
}, CollectionSchemaView._rowMenuWidth);
+ return index;
+ };
+ @action
+ highlightDropColumn = (e: PointerEvent) => {
+ e.stopPropagation();
+ const mouseX = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY)[0];
+ const index = this.findDropIndex(mouseX);
this._colEles.forEach((colRef, i) => {
let leftStyle = '';
let rightStyle = '';
- if (i + 1 === index) rightStyle = `solid 2px ${Colors.MEDIUM_BLUE}`;
- if (i === index && i === 0) leftStyle = `solid 2px ${Colors.MEDIUM_BLUE}`;
+ if (i + 1 === index) rightStyle = `solid 12px ${Colors.MEDIUM_BLUE}`;
+ if (i === index && i === 0) leftStyle = `solid 12px ${Colors.MEDIUM_BLUE}`;
colRef.style.borderLeft = leftStyle;
colRef.style.borderRight = rightStyle;
this.childDocs.forEach(doc => {
@@ -425,17 +434,9 @@ export class CollectionSchemaView extends CollectionSubView() {
@action
onInternalDrop = (e: Event, de: DragManager.DropEvent) => {
if (de.complete.columnDragData) {
- e.stopPropagation();
const mouseX = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y)[0];
- let index = de.complete.columnDragData.colIndex;
- this.displayColumnWidths.reduce((total, curr, i) => {
- if (total <= mouseX && total + curr >= mouseX) {
- if (mouseX <= total + curr / 2) index = i;
- else index = i + 1;
- }
- return total + curr;
- }, CollectionSchemaView._rowMenuWidth);
- this.moveColumn(de.complete.columnDragData.colIndex, index);
+ const index = this.findDropIndex(mouseX);
+ this.moveColumn(de.complete.columnDragData.colIndex, index ?? de.complete.columnDragData.colIndex);
this._colEles.forEach((colRef, i) => {
colRef.style.borderLeft = '';
@@ -446,6 +447,7 @@ export class CollectionSchemaView extends CollectionSubView() {
});
});
+ e.stopPropagation();
return true;
}
const draggedDocs = de.complete.docDragData?.draggedDocuments;
@@ -460,7 +462,6 @@ export class CollectionSchemaView extends CollectionSubView() {
if (draggedView) DocumentManager.Instance.RemoveView(draggedView);
DocumentManager.Instance.AddViewRenderedCb(doc, dv => dv.select(true));
});
- e.stopPropagation();
return true;
}
return false;
@@ -484,24 +485,11 @@ export class CollectionSchemaView extends CollectionSubView() {
return false;
};
- @action
- addNewTextDoc = (value: string, shiftDown?: boolean, forceEmptyNote?: boolean) => {
- if (!value && !forceEmptyNote) return false;
- const newDoc = Docs.Create.TextDocument(value, { title: value, _layout_autoHeight: true });
- FormattedTextBox.SelectOnLoad = newDoc[Id];
- FormattedTextBox.SelectOnLoadChar = forceEmptyNote ? '' : ' ';
- return this.addRow(newDoc) || false;
- };
-
menuCallback = (x: number, y: number) => {
ContextMenu.Instance.clearItems();
- DocUtils.addDocumentCreatorMenuItems(doc => this.addRow(doc), this.addRow, x, y, true);
+ DocUtils.addDocumentCreatorMenuItems(this.addRow, this.addRow, x, y, true);
- ContextMenu.Instance.setDefaultItem('::', (name: string): void => {
- Doc.GetProto(this.props.Document)[name] = '';
- this.addRow(Docs.Create.TextDocument('', { title: name, _layout_autoHeight: true }));
- });
ContextMenu.Instance.displayMenu(x, y, undefined, true);
};
@@ -621,7 +609,7 @@ export class CollectionSchemaView extends CollectionSubView() {
this._menuKeys = this.documentKeys.filter(value => value.toLowerCase().includes(this._menuValue.toLowerCase()));
};
- getFieldFilters = (field: string) => StrListCast(this.Document._docFilters).filter(filter => filter.split(':')[0] == field);
+ getFieldFilters = (field: string) => StrListCast(this.Document._childFilters).filter(filter => filter.split(':')[0] == field);
removeFieldFilters = (field: string) => {
this.getFieldFilters(field).forEach(filter => Doc.setDocFilter(this.Document, field, filter.split(':')[1], 'remove'));
@@ -697,6 +685,12 @@ export class CollectionSchemaView extends CollectionSubView() {
);
}
+ onPassiveWheel = (e: WheelEvent) => {
+ // 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)
+ if (!this._oldWheel.scrollTop && e.deltaY <= 0) e.preventDefault();
+ e.stopPropagation();
+ };
+ _oldWheel: any;
@computed get keysDropdown() {
return (
<div className="schema-key-search">
@@ -710,16 +704,11 @@ export class CollectionSchemaView extends CollectionSubView() {
</div>
<div
className="schema-key-list"
- 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 }
- )
- }>
+ ref={r => {
+ this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel);
+ this._oldWheel = r;
+ r?.addEventListener('wheel', this.onPassiveWheel, { passive: false });
+ }}>
{this._menuKeys.map(key => (
<div
className="schema-search-result"
@@ -773,7 +762,7 @@ export class CollectionSchemaView extends CollectionSubView() {
}
});
- const filters = StrListCast(this.Document._docFilters);
+ const filters = StrListCast(this.Document._childFilters);
return keyOptions.map(key => {
let bool = false;
if (filters !== undefined) {
@@ -821,7 +810,6 @@ export class CollectionSchemaView extends CollectionSubView() {
}
@computed get sortedDocs() {
- trace();
const field = StrCast(this.layoutDoc.sortField);
const desc = BoolCast(this.layoutDoc.sortDesc);
const docs = !field
@@ -845,6 +833,7 @@ export class CollectionSchemaView extends CollectionSubView() {
render() {
return (
<div className="collectionSchemaView" ref={(ele: HTMLDivElement | null) => this.createDashEventsTarget(ele)} onDrop={this.onExternalDrop.bind(this)}>
+ <div ref={this._menuTarget} style={{ background: 'red', top: 0, left: 0, position: 'absolute', zIndex: 10000 }}></div>
<div
className="schema-table"
onWheel={e => this.props.isContentActive() && e.stopPropagation()}
@@ -866,20 +855,28 @@ export class CollectionSchemaView extends CollectionSubView() {
columnWidths={this.displayColumnWidths}
sortField={this.sortField}
sortDesc={this.sortDesc}
- setSort={this.setSort}
+ setSort={this.setColumnSort}
rowHeight={this.rowHeightFunc}
removeColumn={this.removeColumn}
resizeColumn={this.startResize}
openContextMenu={this.openContextMenu}
dragColumn={this.dragColumn}
setColRef={this.setColRef}
+ isContentActive={this.props.isContentActive}
/>
))}
</div>
{this._columnMenuIndex !== undefined && this.renderColumnMenu}
{this._filterColumnIndex !== undefined && this.renderFilterMenu}
<CollectionSchemaViewDocs schema={this} childDocs={this.sortedDocsFunc} rowHeight={this.rowHeightFunc} setRef={(ref: HTMLDivElement | null) => (this._tableContentRef = ref)} />
- <EditableView GetValue={returnEmptyString} SetValue={this.addNewTextDoc} placeholder={"Type ':' for commands"} contents={'+ New Node'} menuCallback={this.menuCallback} height={CollectionSchemaView._newNodeInputHeight} />
+ <EditableView
+ GetValue={returnEmptyString}
+ SetValue={undoable(value => (value ? this.addRow(Docs.Create.TextDocument(value, { title: value, _layout_autoHeight: true })) : false), 'add text doc')}
+ placeholder={"Type ':' for commands"}
+ contents={'+ New Node'}
+ menuCallback={this.menuCallback}
+ height={CollectionSchemaView._newNodeInputHeight}
+ />
</div>
{this.previewWidth > 0 && <div className="schema-preview-divider" style={{ width: CollectionSchemaView._previewDividerWidth }} onPointerDown={this.onDividerDown}></div>}
{this.previewWidth > 0 && (
@@ -900,8 +897,8 @@ export class CollectionSchemaView extends CollectionSubView() {
isContentActive={returnTrue}
isDocumentActive={returnFalse}
ScreenToLocalTransform={this.screenToLocal}
- docFilters={this.childDocFilters}
- docRangeFilters={this.childDocRangeFilters}
+ childFilters={this.childDocFilters}
+ childFiltersByRanges={this.childDocRangeFilters}
searchFilterDocs={this.searchFilterDocs}
styleProvider={DefaultStyleProvider}
docViewPath={returnEmptyDoclist}
@@ -946,17 +943,18 @@ class CollectionSchemaViewDocs extends React.Component<CollectionSchemaViewDocsP
LayoutTemplateString={SchemaRowBox.LayoutString(this.props.schema.props.fieldKey)}
Document={doc}
DataDoc={dataDoc}
+ yPadding={index}
renderDepth={this.props.schema.props.renderDepth + 1}
PanelWidth={this.tableWidthFunc}
PanelHeight={this.props.rowHeight}
styleProvider={DefaultStyleProvider}
waitForDoubleClickToClick={returnNever}
defaultDoubleClick={returnIgnore}
- enableDragWhenActive={true}
+ dragAction="move"
onClickScriptDisable="always"
focus={this.props.schema.focusDocument}
- docFilters={this.props.schema.childDocFilters}
- docRangeFilters={this.props.schema.childDocRangeFilters}
+ childFilters={this.props.schema.childDocFilters}
+ childFiltersByRanges={this.props.schema.childDocRangeFilters}
searchFilterDocs={this.props.schema.searchFilterDocs}
rootSelected={this.props.schema.rootSelected}
ScreenToLocalTransform={this.childScreenToLocal(index)}
@@ -970,7 +968,6 @@ class CollectionSchemaViewDocs extends React.Component<CollectionSchemaViewDocsP
hideLinkAnchors={true}
layout_fitWidth={returnTrue}
scriptContext={this}
- canEmbedOnDrag={true}
/>
</div>
);
diff --git a/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx b/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx
index 7da3c042c..65e47f441 100644
--- a/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx
@@ -5,8 +5,6 @@ import { observer } from 'mobx-react';
import { emptyFunction, setupMoveUpEvents } from '../../../../Utils';
import { Colors } from '../../global/globalEnums';
import './CollectionSchemaView.scss';
-import { SnappingManager } from '../../../util/SnappingManager';
-import { DragManager } from '../../../util/DragManager';
export interface SchemaColumnHeaderProps {
columnKeys: string[];
@@ -14,6 +12,7 @@ export interface SchemaColumnHeaderProps {
columnIndex: number;
sortField: string;
sortDesc: boolean;
+ isContentActive: (outsideReaction?: boolean | undefined) => boolean | undefined;
setSort: (field: string | undefined, desc?: boolean) => void;
removeColumn: (index: number) => void;
rowHeight: () => number;
@@ -46,7 +45,7 @@ export class SchemaColumnHeader extends React.Component<SchemaColumnHeaderProps>
@action
onPointerDown = (e: React.PointerEvent) => {
- setupMoveUpEvents(this, e, e => this.props.dragColumn(e, this.props.columnIndex), emptyFunction, emptyFunction, false);
+ this.props.isContentActive(true) && setupMoveUpEvents(this, e, e => this.props.dragColumn(e, this.props.columnIndex), emptyFunction, emptyFunction, false);
};
render() {
diff --git a/src/client/views/collections/collectionSchema/SchemaRowBox.tsx b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx
index 45bfe4f77..e8e606030 100644
--- a/src/client/views/collections/collectionSchema/SchemaRowBox.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx
@@ -7,7 +7,7 @@ import { Doc } from '../../../../fields/Doc';
import { BoolCast } from '../../../../fields/Types';
import { DragManager } from '../../../util/DragManager';
import { SnappingManager } from '../../../util/SnappingManager';
-import { undoBatch } from '../../../util/UndoManager';
+import { undoable } from '../../../util/UndoManager';
import { ViewBoxBaseComponent } from '../../DocComponent';
import { Colors } from '../../global/globalEnums';
import { OpenWhere } from '../../nodes/DocumentView';
@@ -15,6 +15,7 @@ import { FieldView, FieldViewProps } from '../../nodes/FieldView';
import { CollectionSchemaView } from './CollectionSchemaView';
import './CollectionSchemaView.scss';
import { SchemaTableCell } from './SchemaTableCell';
+import { Transform } from '../../../util/Transform';
@observer
export class SchemaRowBox extends ViewBoxBaseComponent<FieldViewProps>() {
@@ -52,13 +53,13 @@ export class SchemaRowBox extends ViewBoxBaseComponent<FieldViewProps>() {
};
onPointerEnter = (e: any) => {
- if (!SnappingManager.GetIsDragging()) return;
- document.removeEventListener('pointermove', this.onPointerMove);
- document.addEventListener('pointermove', this.onPointerMove);
+ if (SnappingManager.GetIsDragging() && this.props.isContentActive()) {
+ document.removeEventListener('pointermove', this.onPointerMove);
+ document.addEventListener('pointermove', this.onPointerMove);
+ }
};
onPointerMove = (e: any) => {
- if (!SnappingManager.GetIsDragging()) return;
const dragIsRow = DragManager.docsBeingDragged.some(doc => doc.embedContainer === this.schemaDoc); // this.schemaView?._selectedDocs.has(doc) ?? false;
if (this._ref && dragIsRow) {
@@ -111,18 +112,18 @@ export class SchemaRowBox extends ViewBoxBaseComponent<FieldViewProps>() {
}}>
<div
className="schema-row-button"
- onPointerDown={undoBatch(e => {
+ onPointerDown={undoable(e => {
e.stopPropagation();
this.props.removeDocument?.(this.rootDoc);
- })}>
+ }, 'Delete Row')}>
<FontAwesomeIcon icon="times" />
</div>
<div
className="schema-row-button"
- onPointerDown={e => {
+ onPointerDown={undoable(e => {
e.stopPropagation();
this.props.addDocTab(this.rootDoc, OpenWhere.addRight);
- }}>
+ }, 'Open Doc on Right')}>
<FontAwesomeIcon icon="external-link-alt" />
</div>
</div>
@@ -143,6 +144,13 @@ export class SchemaRowBox extends ViewBoxBaseComponent<FieldViewProps>() {
selectedCell={this.selectedCell}
setColumnValues={this.setColumnValues}
oneLine={BoolCast(this.schemaDoc?._singleLine)}
+ menuTarget={this.schemaView.MenuTarget}
+ transform={() => {
+ const ind = index === this.schemaView.columnKeys.length - 1 ? this.schemaView.columnKeys.length - 3 : index;
+ const x = this.schemaView?.displayColumnWidths.reduce((p, c, i) => (i <= ind ? p + c : p), 0);
+ const y = (this.props.yPadding ?? 0) * this.props.PanelHeight();
+ return new Transform(x + CollectionSchemaView._rowMenuWidth, y, 1);
+ }}
/>
))}
</div>
diff --git a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
index 42bf32475..1c9c0de53 100644
--- a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
@@ -1,4 +1,5 @@
-import React = require('react');
+import * as React from 'react';
+import Select, { MenuPlacement } from 'react-select';
import { action, computed, observable } from 'mobx';
import { observer } from 'mobx-react';
import { extname } from 'path';
@@ -6,14 +7,13 @@ import DatePicker from 'react-datepicker';
import { DateField } from '../../../../fields/DateField';
import { Doc, DocListCast, Field } from '../../../../fields/Doc';
import { RichTextField } from '../../../../fields/RichTextField';
-import { BoolCast, Cast, DateCast, FieldValue } from '../../../../fields/Types';
+import { BoolCast, Cast, DateCast, DocCast, FieldValue, StrCast } from '../../../../fields/Types';
import { ImageField } from '../../../../fields/URLField';
import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnZero, Utils } from '../../../../Utils';
import { FInfo } from '../../../documents/Documents';
import { DocFocusOrOpen } from '../../../util/DocumentManager';
-import { dropActionType } from '../../../util/DragManager';
import { Transform } from '../../../util/Transform';
-import { undoBatch } from '../../../util/UndoManager';
+import { undoable, undoBatch } from '../../../util/UndoManager';
import { EditableView } from '../../EditableView';
import { Colors } from '../../global/globalEnums';
import { OpenWhere } from '../../nodes/DocumentView';
@@ -21,7 +21,7 @@ import { FieldView, FieldViewProps } from '../../nodes/FieldView';
import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
import { KeyValueBox } from '../../nodes/KeyValueBox';
import { DefaultStyleProvider } from '../../StyleProvider';
-import { ColumnType, FInfotoColType } from './CollectionSchemaView';
+import { CollectionSchemaView, ColumnType, FInfotoColType } from './CollectionSchemaView';
import './CollectionSchemaView.scss';
export interface SchemaTableCellProps {
@@ -41,6 +41,9 @@ export interface SchemaTableCellProps {
oneLine?: boolean; // whether all input should fit on one line vs allowing textare multiline inputs
allowCRs?: boolean; // allow carriage returns in text input (othewrise CR ends the edit)
finishEdit?: () => void; // notify container that edit is over (eg. to hide view in DashFieldView)
+ options?: string[];
+ menuTarget: HTMLDivElement | null;
+ transform: () => Transform;
}
@observer
@@ -58,14 +61,14 @@ export class SchemaTableCell extends React.Component<SchemaTableCellProps> {
break;
}
protoCount++;
- doc = doc.proto;
+ doc = DocCast(doc.proto);
}
const parenCount = Math.max(0, protoCount - 1);
const color = protoCount === 0 || (fieldKey.startsWith('_') && Document[fieldKey] === undefined) ? 'black' : 'blue';
const textDecoration = color !== 'black' && parenCount ? 'underline' : '';
const fieldProps: FieldViewProps = {
- docFilters: returnEmptyFilter,
- docRangeFilters: returnEmptyFilter,
+ childFilters: returnEmptyFilter,
+ childFiltersByRanges: returnEmptyFilter,
searchFilterDocs: returnEmptyDoclist,
styleProvider: DefaultStyleProvider,
docViewPath: returnEmptyDoclist,
@@ -73,7 +76,7 @@ export class SchemaTableCell extends React.Component<SchemaTableCellProps> {
isSelected: returnFalse,
setHeight: returnFalse,
select: emptyFunction,
- dropAction: 'embed',
+ dragAction: 'move',
bringToFront: emptyFunction,
renderDepth: 1,
isContentActive: returnFalse,
@@ -115,29 +118,29 @@ export class SchemaTableCell extends React.Component<SchemaTableCellProps> {
contents={<FieldView {...fieldProps} />}
editing={this.selected ? undefined : false}
GetValue={() => Field.toKeyValueString(this.props.Document, this.props.fieldKey)}
- SetValue={undoBatch((value: string, shiftDown?: boolean, enterKey?: boolean) => {
+ SetValue={undoable((value: string, shiftDown?: boolean, enterKey?: boolean) => {
if (shiftDown && enterKey) {
this.props.setColumnValues(this.props.fieldKey.replace(/^_/, ''), value);
}
const ret = KeyValueBox.SetField(this.props.Document, this.props.fieldKey.replace(/^_/, ''), value);
this.props.finishEdit?.();
return ret;
- })}
+ }, 'edit schema cell')}
/>
</div>
);
}
get getCellType() {
+ const columnTypeStr = this.props.getFinfo(this.props.fieldKey)?.fieldType;
const cellValue = this.props.Document[this.props.fieldKey];
if (cellValue instanceof ImageField) return ColumnType.Image;
if (cellValue instanceof DateField) return ColumnType.Date;
if (cellValue instanceof RichTextField) return ColumnType.RTF;
if (typeof cellValue === 'number') return ColumnType.Any;
- if (typeof cellValue === 'string') return ColumnType.Any;
+ if (typeof cellValue === 'string' && columnTypeStr !== 'enumeration') return ColumnType.Any;
if (typeof cellValue === 'boolean') return ColumnType.Boolean;
- const columnTypeStr = this.props.getFinfo(this.props.fieldKey)?.fieldType;
if (columnTypeStr && columnTypeStr in FInfotoColType) {
return FInfotoColType[columnTypeStr];
}
@@ -152,6 +155,7 @@ export class SchemaTableCell extends React.Component<SchemaTableCellProps> {
case ColumnType.Image: return <SchemaImageCell {...this.props} />;
case ColumnType.Boolean: return <SchemaBoolCell {...this.props} />;
case ColumnType.RTF: return <SchemaRTFCell {...this.props} />;
+ case ColumnType.Enumeration: return <SchemaEnumerationCell {...this.props} options={this.props.getFinfo(this.props.fieldKey)?.values?.map(val => val.toString())} />;
case ColumnType.Date: // return <SchemaDateCell {...this.props} />;
default: return this.defaultCellContent;
}
@@ -268,7 +272,7 @@ export class SchemaRTFCell extends React.Component<SchemaTableCellProps> {
fieldProps.isContentActive = this.selectedFunc;
return (
<div className="schemaRTFCell" style={{ display: 'flex', fontStyle: this.selected ? undefined : 'italic', width: '100%', height: '100%', position: 'relative', color, textDecoration, cursor, pointerEvents }}>
- {this.selected ? <FormattedTextBox allowScroll={true} {...fieldProps} /> : (field => (field ? Field.toString(field) : ''))(FieldValue(fieldProps.Document[fieldProps.fieldKey]))}
+ {this.selected ? <FormattedTextBox allowScroll={true} {...fieldProps} DataDoc={this.props.Document} /> : (field => (field ? Field.toString(field) : ''))(FieldValue(fieldProps.Document[fieldProps.fieldKey]))}
</div>
);
}
@@ -311,3 +315,38 @@ export class SchemaBoolCell extends React.Component<SchemaTableCellProps> {
);
}
}
+@observer
+export class SchemaEnumerationCell extends React.Component<SchemaTableCellProps> {
+ @computed get selected() {
+ const selected: [Doc, number] | undefined = this.props.selectedCell();
+ return this.props.isRowActive() && selected?.[0] === this.props.Document && selected[1] === this.props.col;
+ }
+ render() {
+ const { color, textDecoration, fieldProps, cursor, pointerEvents } = SchemaTableCell.renderProps(this.props);
+ const options = this.props.options?.map(facet => ({ value: facet, label: facet }));
+ return (
+ <div className="schemaSelectionCell" style={{ display: 'flex', color, textDecoration, cursor, pointerEvents }}>
+ <div style={{ width: '100%' }}>
+ <Select
+ styles={{
+ menuPortal: base => ({
+ ...base,
+ left: 0,
+ top: 0,
+ transform: `translate(${this.props.transform().TranslateX}px, ${this.props.transform().TranslateY}px)`,
+ width: Number(base.width) * this.props.transform().Scale,
+ zIndex: 9999,
+ }),
+ }}
+ menuPortalTarget={this.props.menuTarget}
+ menuPosition={'absolute'}
+ placeholder={StrCast(this.props.Document[this.props.fieldKey], 'select...')}
+ options={options}
+ isMulti={false}
+ onChange={val => KeyValueBox.SetField(this.props.Document, this.props.fieldKey.replace(/^_/, ''), `"${val?.value ?? ''}"`)}
+ />
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/global/globalCssVariables.scss b/src/client/views/global/globalCssVariables.scss
index 3496bb835..7b2ac5713 100644
--- a/src/client/views/global/globalCssVariables.scss
+++ b/src/client/views/global/globalCssVariables.scss
@@ -36,8 +36,8 @@ $icon-size: 28px;
// fonts
$sans-serif: 'Roboto', sans-serif;
$large-header: 16px;
-$body-text: 12px;
-$small-text: 9px;
+$body-text: 13px;
+$small-text: 10px;
// $sans-serif: "Roboto Slab", sans-serif;
// misc values
@@ -70,6 +70,10 @@ $DFLT_IMAGE_NATIVE_DIM: 900px;
$LEFT_MENU_WIDTH: 60px;
$TREE_BULLET_WIDTH: 20px;
+$CAROUSEL3D_CENTER_SCALE: 1.3;
+$CAROUSEL3D_SIDE_SCALE: 0.3;
+$CAROUSEL3D_TOP: 15;
+
:export {
contextMenuZindex: $contextMenu-zindex;
SCHEMA_DIVIDER_WIDTH: $SCHEMA_DIVIDER_WIDTH;
@@ -84,4 +88,7 @@ $TREE_BULLET_WIDTH: 20px;
TREE_BULLET_WIDTH: $TREE_BULLET_WIDTH;
INK_MASK_SIZE: $INK_MASK_SIZE;
MEDIUM_GRAY: $medium-gray;
+ CAROUSEL3D_CENTER_SCALE: $CAROUSEL3D_CENTER_SCALE;
+ CAROUSEL3D_SIDE_SCALE: $CAROUSEL3D_SIDE_SCALE;
+ CAROUSEL3D_TOP: $CAROUSEL3D_TOP;
}
diff --git a/src/client/views/global/globalCssVariables.scss.d.ts b/src/client/views/global/globalCssVariables.scss.d.ts
index 537ea1e5d..efb702564 100644
--- a/src/client/views/global/globalCssVariables.scss.d.ts
+++ b/src/client/views/global/globalCssVariables.scss.d.ts
@@ -12,6 +12,9 @@ interface IGlobalScss {
TREE_BULLET_WIDTH: string;
INK_MASK_SIZE: number;
MEDIUM_GRAY: string;
+ CAROUSEL3D_CENTER_SCALE: string;
+ CAROUSEL3D_SIDE_SCALE: string;
+ CAROUSEL3D_TOP: string;
}
declare const globalCssVariables: IGlobalScss;
diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts
new file mode 100644
index 000000000..256377758
--- /dev/null
+++ b/src/client/views/global/globalScripts.ts
@@ -0,0 +1,416 @@
+import { Colors } from 'browndash-components';
+import { runInAction, action } from 'mobx';
+import { aggregateBounds } from '../../../Utils';
+import { Doc } from '../../../fields/Doc';
+import { Width, Height } from '../../../fields/DocSymbols';
+import { InkTool } from '../../../fields/InkField';
+import { Cast, StrCast, NumCast, BoolCast } from '../../../fields/Types';
+import { WebField } from '../../../fields/URLField';
+import { GestureUtils } from '../../../pen-gestures/GestureUtils';
+import { LinkManager } from '../../util/LinkManager';
+import { ScriptingGlobals } from '../../util/ScriptingGlobals';
+import { SelectionManager } from '../../util/SelectionManager';
+import { undoable, UndoManager } from '../../util/UndoManager';
+import { GestureOverlay } from '../GestureOverlay';
+import { InkTranscription } from '../InkTranscription';
+import { ActiveFillColor, SetActiveFillColor, ActiveIsInkMask, SetActiveIsInkMask, ActiveInkWidth, SetActiveInkWidth, ActiveInkColor, SetActiveInkColor } from '../InkingStroke';
+import { CollectionFreeFormView } from '../collections/collectionFreeForm';
+import { CollectionFreeFormDocumentView } from '../nodes/CollectionFreeFormDocumentView';
+import { WebBox } from '../nodes/WebBox';
+import { RichTextMenu } from '../nodes/formattedText/RichTextMenu';
+import { DocumentType } from '../../documents/DocumentTypes';
+
+ScriptingGlobals.add(function IsNoneSelected() {
+ return SelectionManager.Views().length <= 0;
+}, 'are no document selected');
+
+// toggle: Set overlay status of selected document
+ScriptingGlobals.add(function setView(view: string) {
+ const selected = SelectionManager.Docs().lastElement();
+ selected ? (selected._type_collection = view) : console.log('[FontIconBox.tsx] changeView failed');
+});
+
+// toggle: Set overlay status of selected document
+ScriptingGlobals.add(function setBackgroundColor(color?: string, checkResult?: boolean) {
+ const selectedViews = SelectionManager.Views();
+ if (Doc.ActiveTool !== InkTool.None) {
+ if (checkResult) {
+ return ActiveFillColor();
+ }
+ SetActiveFillColor(color ?? 'transparent');
+ } else if (selectedViews.length) {
+ if (checkResult) {
+ const selView = selectedViews.lastElement();
+ const fieldKey = selView.rootDoc.type === DocumentType.INK ? 'fillColor' : 'backgroundColor';
+ const layoutFrameNumber = Cast(selView.props.docViewPath().lastElement()?.rootDoc?._currentFrame, 'number'); // frame number that container is at which determines layout frame values
+ const contentFrameNumber = Cast(selView.rootDoc?._currentFrame, 'number', layoutFrameNumber ?? null); // frame number that content is at which determines what content is displayed
+ return CollectionFreeFormDocumentView.getStringValues(selView?.rootDoc, contentFrameNumber)[fieldKey] ?? 'transparent';
+ }
+ selectedViews.forEach(dv => {
+ const fieldKey = dv.rootDoc.type === DocumentType.INK ? 'fillColor' : 'backgroundColor';
+ const layoutFrameNumber = Cast(dv.props.docViewPath().lastElement()?.rootDoc?._currentFrame, 'number'); // frame number that container is at which determines layout frame values
+ const contentFrameNumber = Cast(dv.rootDoc?._currentFrame, 'number', layoutFrameNumber ?? null); // frame number that content is at which determines what content is displayed
+ if (contentFrameNumber !== undefined) {
+ CollectionFreeFormDocumentView.setStringValues(contentFrameNumber, dv.rootDoc, { fieldKey: color });
+ } else {
+ dv.rootDoc['_' + fieldKey] = color;
+ }
+ });
+ } else {
+ const selected = SelectionManager.Docs().length ? SelectionManager.Docs() : LinkManager.currentLink ? [LinkManager.currentLink] : [];
+ if (checkResult) {
+ return selected.lastElement()?._backgroundColor ?? 'transparent';
+ }
+ selected.forEach(doc => (doc._backgroundColor = color));
+ }
+});
+
+// toggle: Set overlay status of selected document
+ScriptingGlobals.add(function setHeaderColor(color?: string, checkResult?: boolean) {
+ if (checkResult) {
+ return Doc.SharingDoc().headingColor;
+ }
+ Doc.SharingDoc().headingColor = undefined;
+ Doc.GetProto(Doc.SharingDoc()).headingColor = color;
+ Doc.UserDoc().layout_showTitle = color === 'transparent' ? undefined : StrCast(Doc.UserDoc().layout_showTitle, 'author_date');
+});
+
+// toggle: Set overlay status of selected document
+ScriptingGlobals.add(function toggleOverlay(checkResult?: boolean) {
+ const selected = SelectionManager.Views().length ? SelectionManager.Views()[0] : undefined;
+ if (checkResult) {
+ if (NumCast(selected?.Document.z) >= 1) return true;
+ return false;
+ }
+ selected ? selected.props.CollectionFreeFormDocumentView?.().float() : console.log('[FontIconBox.tsx] toggleOverlay failed');
+});
+
+ScriptingGlobals.add(function showFreeform(attr: 'flashcards' | 'grid' | 'snaplines' | 'clusters' | 'arrange' | 'viewAll', checkResult?: boolean) {
+ const selected = SelectionManager.Docs().lastElement();
+ // prettier-ignore
+ const map: Map<'flashcards' | 'grid' | 'snaplines' | 'clusters' | 'arrange'| 'viewAll', { waitForRender?: boolean, checkResult: (doc:Doc) => any; setDoc: (doc:Doc) => void;}> = new Map([
+ ['grid', {
+ checkResult: (doc:Doc) => BoolCast(doc._freeform_backgroundGrid, false),
+ setDoc: (doc:Doc) => doc._freeform_backgroundGrid = !doc._freeform_backgroundGrid,
+ }],
+ ['snaplines', {
+ checkResult: (doc:Doc) => BoolCast(doc._freeform_snapLines, false),
+ setDoc: (doc:Doc) => doc._freeform_snapLines = !doc._freeform_snapLines,
+ }],
+ ['viewAll', {
+ checkResult: (doc:Doc) => BoolCast(doc._freeform_fitContentsToBox, false),
+ setDoc: (doc:Doc) => doc._freeform_fitContentsToBox = !doc._freeform_fitContentsToBox,
+ }],
+ ['clusters', {
+ waitForRender: true, // flags that undo batch should terminate after a re-render giving the script the chance to fire
+ checkResult: (doc:Doc) => BoolCast(doc._freeform_useClusters, false),
+ setDoc: (doc:Doc) => doc._freeform_useClusters = !doc._freeform_useClusters,
+ }],
+ ['arrange', {
+ waitForRender: true, // flags that undo batch should terminate after a re-render giving the script the chance to fire
+ checkResult: (doc:Doc) => BoolCast(doc._autoArrange, false),
+ setDoc: (doc:Doc) => doc._autoArrange = !doc._autoArrange,
+ }],
+ ['flashcards', {
+ checkResult: (doc:Doc) => BoolCast(Doc.UserDoc().defaultToFlashcards, false),
+ setDoc: (doc:Doc) => Doc.UserDoc().defaultToFlashcards = !Doc.UserDoc().defaultToFlashcards,
+ }],
+ ]);
+
+ if (checkResult) {
+ return map.get(attr)?.checkResult(selected);
+ }
+ const batch = map.get(attr)?.waitForRender ? UndoManager.StartBatch('set freeform attribute') : { end: () => {} };
+ SelectionManager.Docs().map(dv => map.get(attr)?.setDoc(dv));
+ setTimeout(() => batch.end(), 100);
+});
+
+ScriptingGlobals.add(function setFontAttr(attr: 'font' | 'fontColor' | 'highlight' | 'fontSize' | 'alignment', value: any, checkResult?: boolean) {
+ const editorView = RichTextMenu.Instance?.TextView?.EditorView;
+ const selected = SelectionManager.Docs().lastElement();
+ // prettier-ignore
+ const map: Map<'font'|'fontColor'|'highlight'|'fontSize'|'alignment', { checkResult: () => any; setDoc: () => void;}> = new Map([
+ ['font', {
+ checkResult: () => RichTextMenu.Instance?.fontFamily,
+ setDoc: () => value && RichTextMenu.Instance.setFontFamily(value),
+ }],
+ ['highlight', {
+ checkResult: () =>(selected ?? Doc.UserDoc())._fontHighlight,
+ setDoc: () => value && RichTextMenu.Instance.setHighlight(value),
+ }],
+ ['fontColor', {
+ checkResult: () => RichTextMenu.Instance?.fontColor,
+ setDoc: () => value && RichTextMenu.Instance.setColor(value),
+ }],
+ ['alignment', {
+ checkResult: () => RichTextMenu.Instance.textAlign,
+ setDoc: () => value && editorView?.state ? RichTextMenu.Instance.align(editorView, editorView.dispatch, value):(Doc.UserDoc().textAlign = value),
+ }],
+ ['fontSize', {
+ checkResult: () => RichTextMenu.Instance?.fontSize.replace('px', ''),
+ setDoc: () => {
+ if (typeof value === 'number') value = value.toString();
+ if (value && Number(value).toString() === value) value += 'px';
+ RichTextMenu.Instance.setFontSize(value);
+ },
+ }],
+ ]);
+
+ if (checkResult) {
+ return map.get(attr)?.checkResult();
+ }
+ map.get(attr)?.setDoc?.();
+});
+
+type attrname = 'noAutoLink' | 'dictation' | 'bold' | 'italics' | 'underline' | 'left' | 'center' | 'right' | 'bullet' | 'decimal';
+type attrfuncs = [attrname, { checkResult: () => boolean; toggle: () => any }];
+
+ScriptingGlobals.add(function toggleCharStyle(charStyle: attrname, checkResult?: boolean) {
+ const textView = RichTextMenu.Instance?.TextView;
+ const editorView = textView?.EditorView;
+ // prettier-ignore
+ const alignments:attrfuncs[] = (['left','right','center'] as ("left"|"center"|"right")[]).map((where) =>
+ [ where, { checkResult: () =>(editorView ? (RichTextMenu.Instance.textAlign ===where): (Doc.UserDoc().textAlign ===where) ? true:false),
+ toggle: () => (editorView?.state ? RichTextMenu.Instance.align(editorView, editorView.dispatch, where):(Doc.UserDoc().textAlign = where))}]);
+ // prettier-ignore
+ const listings:attrfuncs[] = (['bullet','decimal'] as attrname[]).map(list =>
+ [ list, { checkResult: () => (editorView ? RichTextMenu.Instance.getActiveListStyle() === list:false),
+ toggle: () => editorView?.state && RichTextMenu.Instance.changeListType(list) }]);
+ // prettier-ignore
+ const attrs:attrfuncs[] = [
+ ['dictation', { checkResult: () => textView?._recording ? true:false,
+ toggle: () => textView && runInAction(() => (textView._recording = !textView._recording)) }],
+ ['noAutoLink',{ checkResult: () => (editorView ? RichTextMenu.Instance.noAutoLink : false),
+ toggle: () => editorView && RichTextMenu.Instance?.toggleNoAutoLinkAnchor()}],
+ ['bold', { checkResult: () => (editorView ? RichTextMenu.Instance.bold : (Doc.UserDoc().fontWeight === 'bold') ? true:false),
+ toggle: editorView ? RichTextMenu.Instance.toggleBold : () => (Doc.UserDoc().fontWeight = Doc.UserDoc().fontWeight === 'bold' ? undefined : 'bold')}],
+ ['italics', { checkResult: () => (editorView ? RichTextMenu.Instance.italics : (Doc.UserDoc().fontStyle === 'italics') ? true:false),
+ toggle: editorView ? RichTextMenu.Instance.toggleItalics : () => (Doc.UserDoc().fontStyle = Doc.UserDoc().fontStyle === 'italics' ? undefined : 'italics')}],
+ ['underline', { checkResult: () => (editorView ? RichTextMenu.Instance.underline : (Doc.UserDoc().textDecoration === 'underline') ? true:false),
+ toggle: editorView ? RichTextMenu.Instance.toggleUnderline : () => (Doc.UserDoc().textDecoration = Doc.UserDoc().textDecoration === 'underline' ? undefined : 'underline') }]]
+
+ const map = new Map(attrs.concat(alignments).concat(listings));
+ if (checkResult) {
+ return map.get(charStyle)?.checkResult();
+ }
+ undoable(() => map.get(charStyle)?.toggle(), 'toggle ' + charStyle)();
+});
+
+export function checkInksToGroup() {
+ if (Doc.ActiveTool === InkTool.Write) {
+ CollectionFreeFormView.collectionsWithUnprocessedInk.forEach(ffView => {
+ // TODO: nda - will probably want to go through ffView unprocessed docs and then see if any of the inksToGroup docs are in it and only use those
+ // find all inkDocs in ffView.unprocessedDocs that are within 200 pixels of each other
+ const inksToGroup = ffView.unprocessedDocs.filter(inkDoc => {
+ // console.log(inkDoc.x, inkDoc.y);
+ });
+ });
+ }
+}
+
+export function createInkGroup(inksToGroup?: Doc[], isSubGroup?: boolean) {
+ // TODO nda - if document being added to is a inkGrouping then we can just add to that group
+ if (Doc.ActiveTool === InkTool.Write) {
+ CollectionFreeFormView.collectionsWithUnprocessedInk.forEach(ffView => {
+ // TODO: nda - will probably want to go through ffView unprocessed docs and then see if any of the inksToGroup docs are in it and only use those
+ const selected = ffView.unprocessedDocs;
+ // loop through selected an get the bound
+ const bounds: { x: number; y: number; width?: number; height?: number }[] = [];
+
+ selected.map(
+ action(d => {
+ const x = NumCast(d.x);
+ const y = NumCast(d.y);
+ const width = d[Width]();
+ const height = d[Height]();
+ bounds.push({ x, y, width, height });
+ })
+ );
+
+ const aggregBounds = aggregateBounds(bounds, 0, 0);
+ const marqViewRef = ffView._marqueeViewRef.current;
+
+ // set the vals for bounds in marqueeView
+ if (marqViewRef) {
+ marqViewRef._downX = aggregBounds.x;
+ marqViewRef._downY = aggregBounds.y;
+ marqViewRef._lastX = aggregBounds.r;
+ marqViewRef._lastY = aggregBounds.b;
+ }
+
+ selected.map(
+ action(d => {
+ const dx = NumCast(d.x);
+ const dy = NumCast(d.y);
+ delete d.x;
+ delete d.y;
+ delete d.activeFrame;
+ delete d._timecodeToShow; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection
+ delete d._timecodeToHide; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection
+ // calculate pos based on bounds
+ if (marqViewRef?.Bounds) {
+ d.x = dx - marqViewRef.Bounds.left - marqViewRef.Bounds.width / 2;
+ d.y = dy - marqViewRef.Bounds.top - marqViewRef.Bounds.height / 2;
+ }
+ return d;
+ })
+ );
+ ffView.props.removeDocument?.(selected);
+ // TODO: nda - this is the code to actually get a new grouped collection
+ const newCollection = marqViewRef?.getCollection(selected, undefined, true);
+ if (newCollection) {
+ newCollection.height = newCollection[Height]();
+ newCollection.width = newCollection[Width]();
+ }
+
+ // nda - bug: when deleting a stroke before leaving writing mode, delete the stroke from unprocessed ink docs
+ newCollection && ffView.props.addDocument?.(newCollection);
+ // TODO: nda - will probably need to go through and only remove the unprocessed selected docs
+ ffView.unprocessedDocs = [];
+
+ InkTranscription.Instance.transcribeInk(newCollection, selected, false);
+ });
+ }
+ CollectionFreeFormView.collectionsWithUnprocessedInk.clear();
+}
+
+function setActiveTool(tool: InkTool | GestureUtils.Gestures, keepPrim: boolean, checkResult?: boolean) {
+ InkTranscription.Instance?.createInkGroup();
+ if (checkResult) {
+ return (Doc.ActiveTool === tool && !GestureOverlay.Instance?.InkShape) || GestureOverlay.Instance?.InkShape === tool
+ ? GestureOverlay.Instance?.KeepPrimitiveMode || ![GestureUtils.Gestures.Circle, GestureUtils.Gestures.Line, GestureUtils.Gestures.Rectangle].includes(tool as GestureUtils.Gestures)
+ ? true
+ : true
+ : false;
+ }
+ runInAction(() => {
+ if (GestureOverlay.Instance) {
+ GestureOverlay.Instance.KeepPrimitiveMode = keepPrim;
+ }
+ if (Object.values(GestureUtils.Gestures).includes(tool as any)) {
+ if (GestureOverlay.Instance.InkShape === tool && !keepPrim) {
+ Doc.ActiveTool = InkTool.None;
+ GestureOverlay.Instance.InkShape = undefined;
+ } else {
+ Doc.ActiveTool = InkTool.Pen;
+ GestureOverlay.Instance.InkShape = tool as GestureUtils.Gestures;
+ }
+ } else if (tool) {
+ // pen or eraser
+ if (Doc.ActiveTool === tool && !GestureOverlay.Instance.InkShape && !keepPrim) {
+ Doc.ActiveTool = InkTool.None;
+ } else {
+ Doc.ActiveTool = tool as any;
+ GestureOverlay.Instance.InkShape = undefined;
+ }
+ } else {
+ Doc.ActiveTool = InkTool.None;
+ }
+ });
+}
+
+ScriptingGlobals.add(setActiveTool, 'sets the active ink tool mode');
+
+// toggle: Set overlay status of selected document
+ScriptingGlobals.add(function setInkProperty(option: 'inkMask' | 'fillColor' | 'strokeWidth' | 'strokeColor', value: any, checkResult?: boolean) {
+ const selected = SelectionManager.Docs().lastElement();
+ // prettier-ignore
+ const map: Map<'inkMask' | 'fillColor' | 'strokeWidth' | 'strokeColor', { checkResult: () => any; setInk: (doc: Doc) => void; setMode: () => void }> = new Map([
+ ['inkMask', {
+ checkResult: () => ((selected?.type === DocumentType.INK ? BoolCast(selected.stroke_isInkMask) : ActiveIsInkMask())),
+ setInk: (doc: Doc) => (doc.stroke_isInkMask = !doc.stroke_isInkMask),
+ setMode: () => selected?.type !== DocumentType.INK && SetActiveIsInkMask(!ActiveIsInkMask()),
+ }],
+ ['fillColor', {
+ checkResult: () => (selected?.type === DocumentType.INK ? StrCast(selected.fillColor) : ActiveFillColor() ?? "transparent"),
+ setInk: (doc: Doc) => (doc.fillColor = StrCast(value)),
+ setMode: () => SetActiveFillColor(StrCast(value)),
+ }],
+ [ 'strokeWidth', {
+ checkResult: () => (selected?.type === DocumentType.INK ? NumCast(selected.stroke_width) : ActiveInkWidth()),
+ setInk: (doc: Doc) => (doc.stroke_width = NumCast(value)),
+ setMode: () => SetActiveInkWidth(value.toString()),
+ }],
+ ['strokeColor', {
+ checkResult: () => (selected?.type === DocumentType.INK ? StrCast(selected.color) : ActiveInkColor()),
+ setInk: (doc: Doc) => (doc.color = String(value)),
+ setMode: () => SetActiveInkColor(StrCast(value)),
+ }],
+ ]);
+
+ if (checkResult) {
+ return map.get(option)?.checkResult();
+ }
+ map.get(option)?.setMode();
+ SelectionManager.Docs()
+ .filter(doc => doc.type === DocumentType.INK)
+ .map(doc => map.get(option)?.setInk(doc));
+});
+
+/** WEB
+ * webSetURL
+ **/
+ScriptingGlobals.add(function webSetURL(url: string, checkResult?: boolean) {
+ const selected = SelectionManager.Views().lastElement();
+ if (selected?.rootDoc.type === DocumentType.WEB) {
+ if (checkResult) {
+ return StrCast(selected.rootDoc.data, Cast(selected.rootDoc.data, WebField, null)?.url?.href);
+ }
+ selected.ComponentView?.setData?.(url);
+ //selected.rootDoc.data = new WebField(url);
+ }
+});
+ScriptingGlobals.add(function webForward(checkResult?: boolean) {
+ const selected = SelectionManager.Views().lastElement()?.ComponentView as WebBox;
+ if (checkResult) {
+ return selected?.forward(checkResult) ? undefined : 'lightGray';
+ }
+ selected?.forward();
+});
+ScriptingGlobals.add(function webBack(checkResult?: boolean) {
+ const selected = SelectionManager.Views().lastElement()?.ComponentView as WebBox;
+ if (checkResult) {
+ return selected?.back(checkResult) ? undefined : 'lightGray';
+ }
+ selected?.back();
+});
+
+/** Schema
+ * toggleSchemaPreview
+ **/
+ScriptingGlobals.add(function toggleSchemaPreview(checkResult?: boolean) {
+ const selected = SelectionManager.Docs().lastElement();
+ if (checkResult && selected) {
+ const result: boolean = NumCast(selected.schema_previewWidth) > 0;
+ if (result) return Colors.MEDIUM_BLUE;
+ else return 'transparent';
+ } else if (selected) {
+ if (NumCast(selected.schema_previewWidth) > 0) {
+ selected.schema_previewWidth = 0;
+ } else {
+ selected.schema_previewWidth = 200;
+ }
+ }
+});
+ScriptingGlobals.add(function toggleSingleLineSchema(checkResult?: boolean) {
+ const selected = SelectionManager.Docs().lastElement();
+ if (checkResult && selected) {
+ return NumCast(selected._schema_singleLine) > 0 ? Colors.MEDIUM_BLUE : 'transparent';
+ }
+ if (selected) {
+ selected._schema_singleLine = !selected._schema_singleLine;
+ }
+});
+
+/** STACK
+ * groupBy
+ */
+ScriptingGlobals.add(function setGroupBy(key: string, checkResult?: boolean) {
+ SelectionManager.Docs().map(doc => (doc._text_fontFamily = key));
+ const editorView = RichTextMenu.Instance.TextView?.EditorView;
+ if (checkResult) {
+ return StrCast((editorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily);
+ }
+ if (editorView) RichTextMenu.Instance.setFontFamily(key);
+ else Doc.UserDoc().fontFamily = key;
+});
diff --git a/src/client/views/linking/LinkMenu.tsx b/src/client/views/linking/LinkMenu.tsx
index 3f6369898..65d13a6c3 100644
--- a/src/client/views/linking/LinkMenu.tsx
+++ b/src/client/views/linking/LinkMenu.tsx
@@ -49,7 +49,7 @@ export class LinkMenu extends React.Component<Props> {
<LinkMenuGroup key={group[0]} itemHandler={this.props.itemHandler} docView={this.props.docView} sourceDoc={this.props.docView.props.Document} group={group[1]} groupType={group[0]} clearLinkEditor={this.clear} />
));
- return linkItems.length ? linkItems : this.props.style ? [<></>] : [<p key="">No links have been created yet. Drag the linking button onto another document to create a link.</p>];
+ return linkItems.length ? linkItems : this.props.style ? [] : [<p key="none">No links have been created yet. Drag the linking button onto another document to create a link.</p>];
};
render() {
diff --git a/src/client/views/linking/LinkMenuGroup.tsx b/src/client/views/linking/LinkMenuGroup.tsx
index b9d56541a..8324a97d9 100644
--- a/src/client/views/linking/LinkMenuGroup.tsx
+++ b/src/client/views/linking/LinkMenuGroup.tsx
@@ -25,7 +25,7 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> {
getBackgroundColor = (): string => {
const link_relationshipList = StrListCast(Doc.UserDoc().link_relationshipList);
- const linkColorList = StrListCast(Doc.UserDoc().linkColorList);
+ const linkColorList = StrListCast(Doc.UserDoc().link_ColorList);
let color = 'white';
// if this link's relationship property is not default "link", set its color
if (link_relationshipList) {
diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index 5af05e491..737d675aa 100644
--- a/src/client/views/linking/LinkMenuItem.tsx
+++ b/src/client/views/linking/LinkMenuItem.tsx
@@ -78,7 +78,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
e,
e => {
const dragData = new DragManager.DocumentDragData([this.props.linkDoc], 'embed');
- dragData.removeDropProperties = ['hidden'];
+ dragData.dropPropertiesToRemove = ['hidden'];
DragManager.StartDocumentDrag([this._editRef.current!], dragData, e.x, e.y);
return true;
},
@@ -170,7 +170,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
linkSrc: this.props.sourceDoc,
linkDoc: this.props.linkDoc,
showHeader: false,
- location: [this._drag.current?.getBoundingClientRect().right ?? 100, this._drag.current?.getBoundingClientRect().top ?? e.clientY],
+ location: [(this._drag.current?.getBoundingClientRect().left ?? 100) + 40, (this._drag.current?.getBoundingClientRect().top ?? e.clientY) + 25],
noPreview: false,
})
}>
diff --git a/src/client/views/linking/LinkPopup.scss b/src/client/views/linking/LinkPopup.scss
index b20ad9476..4bfb4b0b9 100644
--- a/src/client/views/linking/LinkPopup.scss
+++ b/src/client/views/linking/LinkPopup.scss
@@ -4,7 +4,6 @@
top: 0;
height: 200px;
width: 200px;
- position: absolute;
// padding: 15px;
border-radius: 3px;
@@ -37,8 +36,4 @@
margin: auto;
}
}
-
- .searchBox-container {
- background: pink;
- }
}
diff --git a/src/client/views/linking/LinkPopup.tsx b/src/client/views/linking/LinkPopup.tsx
index 5f2d4a7b6..6895c0746 100644
--- a/src/client/views/linking/LinkPopup.tsx
+++ b/src/client/views/linking/LinkPopup.tsx
@@ -14,7 +14,6 @@ import React = require('react');
import { OpenWhere } from '../nodes/DocumentView';
interface LinkPopupProps {
- showPopup: boolean;
linkFrom?: () => Doc | undefined;
linkCreateAnchor?: () => Doc | undefined;
linkCreated?: (link: Doc) => void;
@@ -44,10 +43,9 @@ export class LinkPopup extends React.Component<LinkPopupProps> {
getPHeight = () => 500;
render() {
- const popupVisibility = this.props.showPopup ? 'block' : 'none';
const linkDoc = this.props.linkFrom ? this.props.linkFrom : undefined;
return (
- <div className="linkPopup-container" style={{ display: popupVisibility }}>
+ <div className="linkPopup-container">
{/* <div className="linkPopup-url-container">
<input autoComplete="off" type="text" value={this.linkURL} placeholder="Enter URL..." onChange={this.onLinkChange} />
<button onPointerDown={e => this.makeLinkToURL(this.linkURL, "add:right")}
@@ -70,7 +68,6 @@ export class LinkPopup extends React.Component<LinkPopupProps> {
linkSearch={true}
linkCreated={this.props.linkCreated}
fieldKey="data"
- dropAction="move"
isSelected={returnTrue}
isContentActive={returnTrue}
select={returnTrue}
@@ -89,8 +86,8 @@ export class LinkPopup extends React.Component<LinkPopupProps> {
docViewPath={returnEmptyDoclist}
whenChildContentsActiveChanged={emptyFunction}
bringToFront={emptyFunction}
- docFilters={returnEmptyFilter}
- docRangeFilters={returnEmptyFilter}
+ childFilters={returnEmptyFilter}
+ childFiltersByRanges={returnEmptyFilter}
searchFilterDocs={returnEmptyDoclist}
/>
</div>
diff --git a/src/client/views/newlightbox/ButtonMenu/ButtonMenu.scss b/src/client/views/newlightbox/ButtonMenu/ButtonMenu.scss
new file mode 100644
index 000000000..74fbfbb2c
--- /dev/null
+++ b/src/client/views/newlightbox/ButtonMenu/ButtonMenu.scss
@@ -0,0 +1,15 @@
+@import '../NewLightboxStyles.scss';
+
+.newLightboxButtonMeny-container {
+ width: 100vw;
+ height: 100vh;
+
+ &.dark {
+ background: $black;
+ }
+
+ &.light,
+ &.default {
+ background: $white;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx b/src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx
new file mode 100644
index 000000000..ff17e5c12
--- /dev/null
+++ b/src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx
@@ -0,0 +1,50 @@
+import './ButtonMenu.scss';
+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';
+
+export const ButtonMenu = (props: IButtonMenu) => {
+ return (
+ <div className={`newLightboxButtonMenu-container`}>
+ <div
+ className="newLightboxView-navBtn"
+ title="toggle fit width"
+ onClick={e => {
+ e.stopPropagation();
+ NewLightboxView.LightboxDoc!._fitWidth = !NewLightboxView.LightboxDoc!._fitWidth;
+ }}></div>
+ <div
+ className="newLightboxView-tabBtn"
+ title="open in tab"
+ onClick={e => {
+ e.stopPropagation();
+ CollectionDockingView.AddSplit(NewLightboxView.LightboxDoc || NewLightboxView.LightboxDoc!, OpenWhereMod.none);
+ SelectionManager.DeselectAll();
+ NewLightboxView.SetNewLightboxDoc(undefined);
+ }}></div>
+ <div
+ className="newLightboxView-penBtn"
+ title="toggle pen annotation"
+ style={{ background: Doc.ActiveTool === InkTool.Pen ? 'white' : undefined }}
+ onClick={e => {
+ e.stopPropagation();
+ Doc.ActiveTool = Doc.ActiveTool === InkTool.Pen ? InkTool.None : InkTool.Pen;
+ }}></div>
+ <div
+ className="newLightboxView-exploreBtn"
+ title="toggle explore mode to navigate among documents only"
+ style={{ background: MainView.Instance._exploreMode ? 'white' : undefined }}
+ onClick={action(e => {
+ e.stopPropagation();
+ MainView.Instance._exploreMode = !MainView.Instance._exploreMode;
+ })}></div>
+ </div>
+ );
+};
diff --git a/src/client/views/newlightbox/ButtonMenu/index.ts b/src/client/views/newlightbox/ButtonMenu/index.ts
new file mode 100644
index 000000000..f53a8c729
--- /dev/null
+++ b/src/client/views/newlightbox/ButtonMenu/index.ts
@@ -0,0 +1 @@
+export * from './ButtonMenu' \ No newline at end of file
diff --git a/src/client/views/newlightbox/ButtonMenu/utils.ts b/src/client/views/newlightbox/ButtonMenu/utils.ts
new file mode 100644
index 000000000..096ea87ad
--- /dev/null
+++ b/src/client/views/newlightbox/ButtonMenu/utils.ts
@@ -0,0 +1,3 @@
+export interface IButtonMenu {
+
+} \ No newline at end of file
diff --git a/src/client/views/newlightbox/ExploreView/ExploreView.scss b/src/client/views/newlightbox/ExploreView/ExploreView.scss
new file mode 100644
index 000000000..5a8ab2f87
--- /dev/null
+++ b/src/client/views/newlightbox/ExploreView/ExploreView.scss
@@ -0,0 +1,44 @@
+@import '../NewLightboxStyles.scss';
+
+.exploreView-container {
+ width: 100%;
+ height: 100%;
+ border-radius: 20px;
+ position: relative;
+ // transform: scale(1);
+ background: $gray-l1;
+ border-top: $standard-border;
+ border-color: $gray-l2;
+ border-radius: 0px 0px 20px 20px;
+ transform-origin: 50% 50%;
+ overflow: hidden;
+
+ &.dark {
+ background: $black;
+ }
+
+ &.light,
+ &.default {
+ background: $gray-l1;
+ }
+
+ .exploreView-doc {
+ width: 60px;
+ height: 80px;
+ position: absolute;
+ background: $blue-l2;
+ // opacity: 0.8;
+ transform-origin: 50% 50%;
+ transform: translate(-50%, -50%) scale(1);
+ cursor: pointer;
+ transition: 0.2s ease;
+ overflow: hidden;
+ font-size: 9px;
+ padding: 10px;
+ border-radius: 5px;
+
+ &:hover {
+ transform: translate(calc(-50% * 1.125), calc(-50% * 1.125)) scale(1.5);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/client/views/newlightbox/ExploreView/ExploreView.tsx b/src/client/views/newlightbox/ExploreView/ExploreView.tsx
new file mode 100644
index 000000000..a1d6375c4
--- /dev/null
+++ b/src/client/views/newlightbox/ExploreView/ExploreView.tsx
@@ -0,0 +1,32 @@
+import './ExploreView.scss';
+import { IBounds, IExploreView, emptyBounds } from './utils';
+import { IRecommendation } from '../components';
+import * as React from 'react';
+import { NewLightboxView } from '../NewLightboxView';
+import { StrCast } from '../../../../fields/Types';
+
+export const ExploreView = (props: IExploreView) => {
+ const { recs, bounds = emptyBounds } = props;
+
+ return (
+ <div className={`exploreView-container`}>
+ {recs &&
+ recs.map(rec => {
+ const x_bound: number = Math.max(Math.abs(bounds.max_x), Math.abs(bounds.min_x));
+ const y_bound: number = Math.max(Math.abs(bounds.max_y), Math.abs(bounds.min_y));
+ if (rec.embedding) {
+ const x = (rec.embedding.x / x_bound) * 50;
+ const y = (rec.embedding.y / y_bound) * 50;
+ return (
+ <div className={`exploreView-doc`} onClick={() => {}} style={{ top: `calc(50% + ${y}%)`, left: `calc(50% + ${x}%)` }}>
+ {rec.title}
+ </div>
+ );
+ } else return null;
+ })}
+ <div className={`exploreView-doc`} style={{ top: `calc(50% + ${0}%)`, left: `calc(50% + ${0}%)`, background: '#073763', color: 'white' }}>
+ {StrCast(NewLightboxView.LightboxDoc?.title)}
+ </div>
+ </div>
+ );
+};
diff --git a/src/client/views/newlightbox/ExploreView/index.ts b/src/client/views/newlightbox/ExploreView/index.ts
new file mode 100644
index 000000000..bf94eedcd
--- /dev/null
+++ b/src/client/views/newlightbox/ExploreView/index.ts
@@ -0,0 +1 @@
+export * from './ExploreView' \ No newline at end of file
diff --git a/src/client/views/newlightbox/ExploreView/utils.ts b/src/client/views/newlightbox/ExploreView/utils.ts
new file mode 100644
index 000000000..7d9cf226d
--- /dev/null
+++ b/src/client/views/newlightbox/ExploreView/utils.ts
@@ -0,0 +1,20 @@
+import { IRecommendation } from "../components";
+
+export interface IExploreView {
+ recs?: IRecommendation[],
+ bounds?: IBounds
+}
+
+export const emptyBounds = {
+ max_x: 0,
+ max_y: 0,
+ min_x: 0,
+ min_y: 0
+}
+
+export interface IBounds {
+ max_x: number,
+ max_y: number,
+ min_x: number,
+ min_y: number
+} \ No newline at end of file
diff --git a/src/client/views/newlightbox/Header/LightboxHeader.scss b/src/client/views/newlightbox/Header/LightboxHeader.scss
new file mode 100644
index 000000000..a9e60ea98
--- /dev/null
+++ b/src/client/views/newlightbox/Header/LightboxHeader.scss
@@ -0,0 +1,71 @@
+@import '../NewLightboxStyles.scss';
+
+.newLightboxHeader-container {
+ width: 100%;
+ height: 100%;
+ background: $gray-l1;
+ border-radius: 20px 20px 0px 0px;
+ padding: 20px;
+ display: grid;
+ grid-template-columns: 70% 30%;
+ grid-template-rows: 50% 50%;
+
+ .title-container,
+ .type-container {
+ display: flex;
+ flex-direction: row;
+ gap: 5px;
+ justify-content: flex-start;
+ align-items: center;
+ }
+
+ .title-container {
+ grid-column: 1;
+ grid-row: 1;
+ }
+
+ .type-container {
+ grid-column: 1;
+ grid-row: 2;
+ .type {
+ padding: 2px 7px !important;
+ background: $gray-l2;
+ }
+ }
+
+ .lb-label {
+ color: $gray-l3;
+ font-weight: $h1-weight;
+ }
+
+ .lb-button {
+ border: solid 1.5px black;
+ padding: 3px 5px;
+ cursor: pointer;
+ display: flex;
+ flex-direction: row;
+ justify-content: space-evenly;
+ align-items: center;
+ transition: 0.2s ease;
+ gap: 5px;
+ font-size: $body-size;
+ height: fit-content;
+
+ &:hover {
+ background: $gray-l2;
+ }
+
+ &.true {
+ background: $blue-l1;
+ }
+ }
+
+ &.dark {
+ background: $black;
+ }
+
+ &.light,
+ &.default {
+ background: $white;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/newlightbox/Header/LightboxHeader.tsx b/src/client/views/newlightbox/Header/LightboxHeader.tsx
new file mode 100644
index 000000000..a272ce294
--- /dev/null
+++ b/src/client/views/newlightbox/Header/LightboxHeader.tsx
@@ -0,0 +1,62 @@
+import './LightboxHeader.scss';
+import * as React from 'react';
+import { INewLightboxHeader } from "./utils";
+import { NewLightboxView } from '../NewLightboxView';
+import { StrCast } from '../../../../fields/Types';
+import { EditableText } from '../components/EditableText';
+import { getType } from '../utils';
+import { Button, IconButton, Size, Type } from 'browndash-components';
+import { MdExplore, MdTravelExplore } from 'react-icons/md'
+import { BsBookmark, BsBookmarkFill } from 'react-icons/bs'
+import { Doc } from '../../../../fields/Doc';
+import { LightboxView } from '../../LightboxView';
+import { Colors } from '../../global/globalEnums';
+
+
+export const NewLightboxHeader = (props: INewLightboxHeader) => {
+ const {height = 100, width} = props;
+ const [doc, setDoc] = React.useState<Doc | undefined>(LightboxView.LightboxDoc)
+ const [editing, setEditing] = React.useState<boolean>(false)
+ const [title, setTitle] = React.useState<JSX.Element | null>(
+ (null)
+ )
+ React.useEffect(() => {
+ let lbDoc = LightboxView.LightboxDoc
+ setDoc(lbDoc)
+ if (lbDoc) {
+ setTitle(
+ <EditableText
+ editing={editing}
+ text={StrCast(lbDoc.title)}
+ onEdit={(newText: string) => {
+ if(lbDoc) lbDoc.title = newText;
+ }}
+ setEditing={setEditing}
+ />)
+ }
+ }, [LightboxView.LightboxDoc])
+
+ const [saved, setSaved] = React.useState<boolean>(false)
+
+ if (!doc) return null
+ else return <div className={`newLightboxHeader-container`} onPointerDown={(e) => e.stopPropagation()} style={{ minHeight: height, height: height, width: width }}>
+ <div className={`title-container`}>
+ <div className={`lb-label`}>Title</div>
+ {title}
+ </div>
+ <div className={`type-container`}>
+ <div className={`lb-label`}>Type</div>
+ <div className={`type`}>{getType(StrCast(doc.type))}</div>
+ </div>
+ <div style={{gridColumn: 2, gridRow: 1, height: '100%', display: 'flex', justifyContent: 'flex-end', alignItems: 'center'}}>
+ <IconButton size={Size.XSMALL} onClick={() => setSaved(!saved)} color={Colors.DARK_GRAY} icon={saved ? <BsBookmarkFill/> : <BsBookmark/>}/>
+ <IconButton size={Size.XSMALL} onClick={() => setSaved(!saved)} color={Colors.DARK_GRAY} icon={saved ? <BsBookmarkFill/> : <BsBookmark/>}/>
+ </div>
+ <div style={{gridColumn: 2, gridRow: 2, height: '100%', display: 'flex', justifyContent: 'flex-end', alignItems: 'center'}}>
+ <Button onClick={() => {
+ console.log(NewLightboxView.ExploreMode)
+ NewLightboxView.SetExploreMode(!NewLightboxView.ExploreMode)
+ }} size={Size.XSMALL} color={Colors.DARK_GRAY} type={Type.SEC} text={"t-SNE 2D Embeddings"} icon={<MdTravelExplore/>}/>
+ </div>
+ </div>
+} \ No newline at end of file
diff --git a/src/client/views/newlightbox/Header/index.ts b/src/client/views/newlightbox/Header/index.ts
new file mode 100644
index 000000000..090677c16
--- /dev/null
+++ b/src/client/views/newlightbox/Header/index.ts
@@ -0,0 +1 @@
+export * from './LightboxHeader' \ No newline at end of file
diff --git a/src/client/views/newlightbox/Header/utils.ts b/src/client/views/newlightbox/Header/utils.ts
new file mode 100644
index 000000000..22e0487c2
--- /dev/null
+++ b/src/client/views/newlightbox/Header/utils.ts
@@ -0,0 +1,4 @@
+export interface INewLightboxHeader {
+ height?: number
+ width?: number
+} \ No newline at end of file
diff --git a/src/client/views/newlightbox/NewLightboxStyles.scss b/src/client/views/newlightbox/NewLightboxStyles.scss
new file mode 100644
index 000000000..ff4a6c971
--- /dev/null
+++ b/src/client/views/newlightbox/NewLightboxStyles.scss
@@ -0,0 +1,73 @@
+$white: white;
+$black: black;
+
+// gray
+$gray-l1: rgba(230, 230, 230, 1);
+$gray-l2: rgb(201, 201, 201);
+$gray-l3: rgba(87, 87, 87, 1);
+
+// blue
+$blue-l1: #cfe2f3;
+$blue-l2: #6fa8dc;
+$blue-l3: #0b5394;
+$blue-l4: #073763;
+
+// view backgrounds
+$background-dm: black;
+$background-lm: white;
+$header-dm: $gray-l3;
+$header-lm: $gray-l1;
+
+// border
+$standard-border: solid 2px;
+
+// standard shadow
+
+
+$text-color-dm: $gray-l1;
+$text-color-lm: $gray-l3;
+
+
+// text / font
+$title-size: 2rem;
+$title-weight: 700;
+
+$h1-size: 15px;
+$h1-weight: 700;
+
+$h2-size: 13px;
+$h2-weight: 600;
+
+
+$body-size: 10px;
+$body-weight: 400;
+
+// header
+$header-height: 40px;
+
+@keyframes skeleton-loading-l3 {
+ 0% {
+ background-color: rgba(128, 128, 128, 1);
+ }
+ 100% {
+ background-color: rgba(128, 128, 128, 0.5);
+ }
+}
+
+@keyframes skeleton-loading-l2 {
+ 0% {
+ background-color: rgba(182, 182, 182, 1);
+ }
+ 100% {
+ background-color: rgba(182, 182, 182, 0.5);
+ }
+}
+
+@keyframes skeleton-loading-l1 {
+ 0% {
+ background-color: rgba(230, 230, 230, 1);
+ }
+ 100% {
+ background-color: rgba(230, 230, 230, 0.5);
+ }
+} \ No newline at end of file
diff --git a/src/client/views/newlightbox/NewLightboxView.scss b/src/client/views/newlightbox/NewLightboxView.scss
new file mode 100644
index 000000000..76c34bcf9
--- /dev/null
+++ b/src/client/views/newlightbox/NewLightboxView.scss
@@ -0,0 +1,34 @@
+@import './NewLightboxStyles.scss';
+
+.newLightboxView-frame {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: #474545bb;
+ backdrop-filter: blur(4px);
+ z-index: 1000;
+
+ .app-document {
+ width: 100%;
+ height: 100%;
+ display: grid;
+ }
+
+ .explore {
+ width: 100%;
+ height: 100%;
+ display: grid;
+ }
+
+ .newLightboxView-contents {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+
+ .newLightboxView-doc {
+ position: relative;
+ }
+ }
+}
diff --git a/src/client/views/newlightbox/NewLightboxView.tsx b/src/client/views/newlightbox/NewLightboxView.tsx
new file mode 100644
index 000000000..3acbd1a32
--- /dev/null
+++ b/src/client/views/newlightbox/NewLightboxView.tsx
@@ -0,0 +1,388 @@
+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 { InkTool } from '../../../fields/InkField';
+import { Cast, NumCast, StrCast } from '../../../fields/Types';
+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 { DocumentView, OpenWhere } from '../nodes/DocumentView';
+import { ExploreView } from './ExploreView';
+import { IBounds, emptyBounds } 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"
+}
+
+interface LightboxViewProps {
+ PanelWidth: number;
+ PanelHeight: number;
+ maxBorder: number[];
+}
+
+type LightboxSavedState = {
+ panX: Opt<number>;
+ panY: Opt<number>;
+ scale: Opt<number>;
+ scrollTop: Opt<number>;
+ layout_fieldKey: Opt<string>;
+};
+@observer
+export class NewLightboxView extends React.Component<LightboxViewProps> {
+ @computed public static get LightboxDoc() {
+ return this._doc;
+ }
+ private static LightboxDocTemplate = () => NewLightboxView._layoutTemplate;
+ @observable private static _layoutTemplate: Opt<Doc>;
+ @observable private static _layoutTemplateString: Opt<string>;
+ @observable private static _doc: Opt<Doc>;
+ @observable private static _docTarget: Opt<Doc>;
+ @observable private static _docFilters: string[] = []; // filters
+ private static _savedState: Opt<LightboxSavedState>;
+ private static _history: Opt<{ doc: Doc; target?: Doc }[]> = [];
+ @observable private static _future: Opt<Doc[]> = [];
+ @observable private static _docView: Opt<DocumentView>;
+
+ // keywords
+ @observable private static _keywords: string[] = []
+ @action public static SetKeywords(kw: string[]) {
+ this._keywords = kw
+ }
+ @computed public static get Keywords() {
+ return this._keywords
+ }
+
+ // query
+ @observable private static _query: string = ''
+ @action public static SetQuery(query: string) {
+ this._query = query
+ }
+ @computed public static get Query() {
+ return this._query
+ }
+
+ // keywords
+ @observable private static _recs: IRecommendation[] = []
+ @action public static SetRecs(recs: IRecommendation[]) {
+ this._recs = recs
+ }
+ @computed public static get Recs() {
+ return this._recs
+ }
+
+ // bounds
+ @observable private static _bounds: IBounds = emptyBounds;
+ @action public static SetBounds(bounds: IBounds) {
+ this._bounds = bounds;
+ }
+ @computed public static get Bounds() {
+ return this._bounds;
+ }
+
+ // explore
+ @observable private static _explore: Opt<boolean> = false;
+ @action public static SetExploreMode(status: Opt<boolean>) {
+ this._explore = status;
+ }
+ @computed public static get ExploreMode() {
+ return this._explore;
+ }
+
+ // newLightbox sidebar status
+ @observable private static _sidebarStatus: Opt<string> = "";
+ @action public static SetSidebarStatus(sidebarStatus: Opt<string>) {
+ this._sidebarStatus = sidebarStatus;
+ }
+ @computed public static get SidebarStatus() {
+ return this._sidebarStatus;
+ }
+
+ static path: { doc: Opt<Doc>; target: Opt<Doc>; history: Opt<{ doc: Doc; target?: Doc }[]>; future: Opt<Doc[]>; saved: Opt<LightboxSavedState> }[] = [];
+ @action public static SetNewLightboxDoc(doc: Opt<Doc>, target?: Doc, future?: Doc[], layoutTemplate?: Doc | string) {
+ if (this.LightboxDoc && this.LightboxDoc !== doc && this._savedState) {
+ if (this._savedState.panX !== undefined) this.LightboxDoc._freeform_panX = this._savedState.panX;
+ if (this._savedState.panY !== undefined) this.LightboxDoc._freeform_panY = this._savedState.panY;
+ if (this._savedState.scrollTop !== undefined) this.LightboxDoc._layout_scrollTop = this._savedState.scrollTop;
+ if (this._savedState.scale !== undefined) this.LightboxDoc._freeform_scale = this._savedState.scale;
+ this.LightboxDoc.layout_fieldKey = this._savedState.layout_fieldKey;
+ }
+ if (!doc) {
+ this._docFilters && (this._docFilters.length = 0);
+ this._future = this._history = [];
+ Doc.ActiveTool = InkTool.None;
+ MainView.Instance._exploreMode = false;
+ } else {
+ const l = DocUtils.MakeLinkToActiveAudio(() => doc).lastElement();
+ l && (Cast(l.link_anchor_2, Doc, null).backgroundColor = 'lightgreen');
+ CollectionStackedTimeline.CurrentlyPlaying?.forEach(dv => dv.ComponentView?.Pause?.());
+ //TabDocView.PinDoc(doc, { hidePresBox: true });
+ this._history ? this._history.push({ doc, target }) : (this._history = [{ doc, target }]);
+ if (doc !== LightboxView.LightboxDoc) {
+ this._savedState = {
+ layout_fieldKey: StrCast(doc.layout_fieldKey),
+ panX: Cast(doc.freeform_panX, 'number', null),
+ panY: Cast(doc.freeform_panY, 'number', null),
+ scale: Cast(doc.freeform_scale, 'number', null),
+ scrollTop: Cast(doc.layout_scrollTop, 'number', null),
+ };
+ }
+ }
+ if (future) {
+ this._future = [
+ ...(this._future ?? []),
+ ...(this.LightboxDoc ? [this.LightboxDoc] : []),
+ ...future
+ .slice()
+ .sort((a, b) => NumCast(b._timecodeToShow) - NumCast(a._timecodeToShow))
+ .sort((a, b) => LinkManager.Links(a).length - LinkManager.Links(b).length),
+ ];
+ }
+ this._doc = doc;
+ this._layoutTemplate = layoutTemplate instanceof Doc ? layoutTemplate : undefined;
+ if (doc && (typeof layoutTemplate === 'string' ? layoutTemplate : undefined)) {
+ doc.layout_fieldKey = layoutTemplate;
+ }
+ this._docTarget = target || doc;
+
+ return true;
+ }
+ public static IsNewLightboxDocView(path: DocumentView[]) {
+ return (path ?? []).includes(this._docView!);
+ }
+ @computed get leftBorder() {
+ return Math.min(this.props.PanelWidth / 4, this.props.maxBorder[0]);
+ }
+ @computed get topBorder() {
+ return Math.min(this.props.PanelHeight / 4, this.props.maxBorder[1]);
+ }
+ newLightboxWidth = () => this.props.PanelWidth - 420;
+ newLightboxHeight = () => this.props.PanelHeight - 140;
+ newLightboxScreenToLocal = () => new Transform(-this.leftBorder, -this.topBorder, 1);
+ navBtn = (left: Opt<string | number>, bottom: Opt<number>, top: number, icon: string, display: () => string, click: (e: React.MouseEvent) => void, color?: string) => {
+ return (
+ <div
+ className="newLightboxView-navBtn-frame"
+ style={{
+ display: display(),
+ left,
+ width: bottom !== undefined ? undefined : Math.min(this.props.PanelWidth / 4, this.props.maxBorder[0]),
+ bottom,
+ }}>
+ <div className="newLightboxView-navBtn" title={color} style={{ top, color: color ? 'red' : 'white', background: color ? 'white' : undefined }} onClick={click}>
+ <div style={{ height: 10 }}>{color}</div>
+ <FontAwesomeIcon icon={icon as any} size="3x" />
+ </div>
+ </div>
+ );
+ };
+ public static GetSavedState(doc: Doc) {
+ return this.LightboxDoc === doc && this._savedState ? this._savedState : undefined;
+ }
+
+ // adds a cookie to the newLightbox view - the cookie becomes part of a filter which will display any documents whose cookie metadata field matches this cookie
+ @action
+ public static SetCookie(cookie: string) {
+ if (this.LightboxDoc && cookie) {
+ this._docFilters = (f => (this._docFilters ? [this._docFilters.push(f) as any, this._docFilters][1] : [f]))(`cookies:${cookie}:provide`);
+ }
+ }
+ public static AddDocTab = (doc: Doc, location: OpenWhere, layoutTemplate?: Doc | string) => {
+ SelectionManager.DeselectAll();
+ return NewLightboxView.SetNewLightboxDoc(
+ doc,
+ undefined,
+ [...DocListCast(doc[Doc.LayoutFieldKey(doc)]), ...DocListCast(doc[Doc.LayoutFieldKey(doc) + '_annotations']).filter(anno => anno.annotationOn !== doc), ...(NewLightboxView._future ?? [])].sort(
+ (a: Doc, b: Doc) => NumCast(b._timecodeToShow) - NumCast(a._timecodeToShow)
+ ),
+ layoutTemplate
+ );
+ };
+ docFilters = () => NewLightboxView._docFilters || [];
+ addDocTab = NewLightboxView.AddDocTab;
+ @action public static Next() {
+ const doc = NewLightboxView._doc!;
+ const target = (NewLightboxView._docTarget = this._future?.pop());
+ const targetDocView = target && DocumentManager.Instance.getLightboxDocumentView(target);
+ if (targetDocView && target) {
+ const l = DocUtils.MakeLinkToActiveAudio(() => targetDocView.ComponentView?.getAnchor?.(true) || target).lastElement();
+ l && (Cast(l.link_anchor_2, Doc, null).backgroundColor = 'lightgreen');
+ DocumentManager.Instance.showDocument(target, { willZoomCentered: true, zoomScale: 0.9 });
+ if (NewLightboxView._history?.lastElement().target !== target) NewLightboxView._history?.push({ doc, target });
+ } else {
+ if (!target && NewLightboxView.path.length) {
+ const saved = NewLightboxView._savedState;
+ if (LightboxView.LightboxDoc && saved) {
+ LightboxView.LightboxDoc._freeform_panX = saved.panX;
+ LightboxView.LightboxDoc._freeform_panY = saved.panY;
+ LightboxView.LightboxDoc._freeform_scale = saved.scale;
+ LightboxView.LightboxDoc._layout_scrollTop = saved.scrollTop;
+ }
+ const pop = NewLightboxView.path.pop();
+ if (pop) {
+ NewLightboxView._doc = pop.doc;
+ NewLightboxView._docTarget = pop.target;
+ NewLightboxView._future = pop.future;
+ NewLightboxView._history = pop.history;
+ NewLightboxView._savedState = pop.saved;
+ }
+ } else {
+ NewLightboxView.SetNewLightboxDoc(target);
+ }
+ }
+ }
+
+ @action public static Previous() {
+ const previous = NewLightboxView._history?.pop();
+ if (!previous || !NewLightboxView._history?.length) {
+ NewLightboxView.SetNewLightboxDoc(undefined);
+ return;
+ }
+ const { doc, target } = NewLightboxView._history?.lastElement();
+ const docView = DocumentManager.Instance.getLightboxDocumentView(target || doc);
+ if (docView) {
+ NewLightboxView._docTarget = target;
+ target && DocumentManager.Instance.showDocument(target, { willZoomCentered: true, zoomScale: 0.9 });
+ } else {
+ NewLightboxView.SetNewLightboxDoc(doc, target);
+ }
+ if (NewLightboxView._future?.lastElement() !== previous.target || previous.doc) NewLightboxView._future?.push(previous.target || previous.doc);
+ }
+ @action
+ stepInto = () => {
+ NewLightboxView.path.push({
+ doc: LightboxView.LightboxDoc,
+ target: NewLightboxView._docTarget,
+ future: NewLightboxView._future,
+ history: NewLightboxView._history,
+ saved: NewLightboxView._savedState,
+ });
+ const coll = NewLightboxView._docTarget;
+ if (coll) {
+ const fieldKey = Doc.LayoutFieldKey(coll);
+ const contents = [...DocListCast(coll[fieldKey]), ...DocListCast(coll[fieldKey + '_annotations'])];
+ const links = LinkManager.Links(coll)
+ .map(link => LinkManager.getOppositeAnchor(link, coll))
+ .filter(doc => doc)
+ .map(doc => doc!);
+ NewLightboxView.SetNewLightboxDoc(coll, undefined, contents.length ? contents : links);
+ }
+ };
+
+ @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>)
+ }
+
+ future = () => NewLightboxView._future;
+ render() {
+ let newLightboxHeaderHeight = 100;
+ let downx = 0,
+ downy = 0;
+ return !LightboxView.LightboxDoc ? null : (
+ <div
+ className="newLightboxView-frame"
+ onPointerDown={e => {
+ downx = e.clientX;
+ downy = e.clientY;
+ }}
+ onClick={e => {
+ if (Math.abs(downx - e.clientX) < 4 && Math.abs(downy - e.clientY) < 4) {
+ 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,
+ }}>
+ <NewLightboxHeader height={newLightboxHeaderHeight} width={this.newLightboxWidth()} />
+ {!NewLightboxView._explore ?
+ <div className="newLightboxView-doc" style={{height: this.newLightboxHeight()}}>
+ {this.documentView}
+ </div>
+ :
+ <div className={`explore`}>
+ <ExploreView recs={NewLightboxView.Recs} bounds={NewLightboxView.Bounds}/>
+ </div>
+ }
+ </div>
+ <RecommendationList keywords={NewLightboxView.Keywords}/>
+ </div>
+
+ </div>
+ );
+ }
+}
+interface NewLightboxTourBtnProps {
+ navBtn: (left: Opt<string | number>, bottom: Opt<number>, top: number, icon: string, display: () => string, click: (e: React.MouseEvent) => void, color?: string) => JSX.Element;
+ future: () => Opt<Doc[]>;
+ stepInto: () => void;
+}
+@observer
+export class NewLightboxTourBtn extends React.Component<NewLightboxTourBtnProps> {
+ render() {
+ return this.props.navBtn(
+ '50%',
+ 0,
+ 0,
+ 'chevron-down',
+ () => (LightboxView.LightboxDoc /*&& this.props.future()?.length*/ ? '' : 'none'),
+ e => {
+ e.stopPropagation();
+ this.props.stepInto();
+ },
+ ''
+ );
+ }
+}
diff --git a/src/client/views/newlightbox/RecommendationList/RecommendationList.scss b/src/client/views/newlightbox/RecommendationList/RecommendationList.scss
new file mode 100644
index 000000000..40dd47e47
--- /dev/null
+++ b/src/client/views/newlightbox/RecommendationList/RecommendationList.scss
@@ -0,0 +1,117 @@
+@import '../NewLightboxStyles.scss';
+
+.recommendationlist-container {
+ height: calc(100% - 40px);
+ margin: 20px;
+ border-radius: 20px;
+ overflow-y: scroll;
+
+ .recommendations {
+ height: fit-content;
+ padding: 20px;
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+ background: $gray-l1;
+ border-radius: 0px 0px 20px 20px;
+ }
+
+ .header {
+ top: 0px;
+ position: sticky;
+ background: $gray-l1;
+ border-bottom: $standard-border;
+ border-color: $gray-l2;
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ align-items: flex-start;
+ border-radius: 20px 20px 0px 0px;
+ padding: 20px;
+ z-index: 2;
+ gap: 10px;
+ color: $text-color-lm;
+
+ .lb-label {
+ color: $gray-l3;
+ font-weight: $h1-weight;
+ font-size: $body-size;
+ }
+
+ .lb-caret {
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-end;
+ align-items: center;
+ gap: 5px;
+ cursor: pointer;
+ width: 100%;
+ user-select: none;
+ font-size: $body-size;
+ }
+
+ .more {
+ width: 100%;
+ }
+
+ &.dark {
+ color: $text-color-dm;
+ }
+
+ .title {
+ height: 30px;
+ min-height: 30px;
+ font-size: $h1-size;
+ font-weight: $h1-weight;
+ text-align: left;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ width: 100%;
+ }
+
+ .keywords {
+ display: flex;
+ flex-flow: row wrap;
+ gap: 5px;
+
+ .keyword-input {
+ padding: 3px 7px;
+ background: $gray-l2;
+ outline: none;
+ border: none;
+ height: 21.5px;
+ color: $text-color-lm;
+ }
+
+ .keyword {
+ padding: 3px 7px;
+ width: fit-content;
+ background: $gray-l2;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-direction: row;
+ gap: 10px;
+ font-size: $body-size;
+ font-weight: $body-weight;
+
+ &.loading {
+ animation: skeleton-loading-l2 1s linear infinite alternate;
+ min-width: 70px;
+ height: 21.5px;
+ }
+ }
+
+ }
+ }
+
+ &.dark {
+ background: $black;
+ }
+
+ &.light,
+ &.default {
+ background: $gray-l1;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/newlightbox/RecommendationList/RecommendationList.tsx b/src/client/views/newlightbox/RecommendationList/RecommendationList.tsx
new file mode 100644
index 000000000..9f3c32e4e
--- /dev/null
+++ b/src/client/views/newlightbox/RecommendationList/RecommendationList.tsx
@@ -0,0 +1,196 @@
+import { GrClose } from 'react-icons/gr';
+import { IRecommendation, Recommendation } from "../components";
+import './RecommendationList.scss';
+import * as React from 'react';
+import { IRecommendationList } from "./utils";
+import { NewLightboxView } from '../NewLightboxView';
+import { DocCast, StrCast } from '../../../../fields/Types';
+import { FaCaretDown, FaCaretUp } from 'react-icons/fa';
+import { Doc, DocListCast, StrListCast } from '../../../../fields/Doc';
+import { IDocRequest, fetchKeywords, fetchRecommendations } from '../utils';
+import { IBounds } from '../ExploreView/utils';
+import { List } from '../../../../fields/List';
+import { Id } from '../../../../fields/FieldSymbols';
+import { LightboxView } from '../../LightboxView';
+import { IconButton, Size, Type } from 'browndash-components';
+import { Colors } from '../../global/globalEnums';
+
+export const RecommendationList = (props: IRecommendationList) => {
+ const {loading, keywords} = props
+ const [loadingKeywords, setLoadingKeywords] = React.useState<boolean>(true)
+ const [showMore, setShowMore] = React.useState<boolean>(false)
+ const [keywordsLoc, setKeywordsLoc] = React.useState<string[]>([])
+ const [update, setUpdate] = React.useState<boolean>(true)
+ const initialRecs: IRecommendation[] = [
+ {loading: true},
+ {loading: true},
+ {loading: true},
+ {loading: true},
+ {loading: true}
+ ];
+ const [recs, setRecs] = React.useState<IRecommendation[]>(initialRecs)
+
+ React.useEffect(() => {
+ const getKeywords = async () => {
+ let text = StrCast(LightboxView.LightboxDoc?.text)
+ console.log('[1] fetching keywords')
+ const response = await fetchKeywords(text, 5, true)
+ console.log('[2] response:', response)
+ const kw = response.keywords;
+ console.log(kw);
+ NewLightboxView.SetKeywords(kw);
+ if (LightboxView.LightboxDoc) {
+ console.log('setting keywords on doc')
+ LightboxView.LightboxDoc.keywords = new List<string>(kw);
+ setKeywordsLoc(NewLightboxView.Keywords);
+ }
+ setLoadingKeywords(false)
+ }
+ let keywordsList = StrListCast(LightboxView.LightboxDoc!.keywords)
+ if (!keywordsList || keywordsList.length < 2) {
+ setLoadingKeywords(true)
+ getKeywords()
+ setUpdate(!update)
+ } else {
+ setKeywordsLoc(keywordsList)
+ setLoadingKeywords(false)
+ setUpdate(!update)
+ }
+ }, [NewLightboxView.LightboxDoc])
+
+ // terms: vannevar bush, information spaces,
+ React.useEffect(() => {
+ const getRecommendations = async () => {
+ console.log('fetching recommendations')
+ let query = 'undefined'
+ if (keywordsLoc) query = keywordsLoc.join(',')
+ let src = StrCast(NewLightboxView.LightboxDoc?.text)
+ let dashDocs:IDocRequest[] = [];
+ // get linked docs
+ let linkedDocs = DocListCast(NewLightboxView.LightboxDoc?.links)
+ console.log("linked docs", linkedDocs)
+ // get context docs (docs that are also in the collection)
+ // let contextDocs: Doc[] = DocListCast(DocCast(LightboxView.LightboxDoc?.context).data)
+ // let docId = LightboxView.LightboxDoc && LightboxView.LightboxDoc[Id]
+ // console.log("context docs", contextDocs)
+ // contextDocs.forEach((doc: Doc) => {
+ // if (docId !== doc[Id]){
+ // dashDocs.push({
+ // title: StrCast(doc.title),
+ // text: StrCast(doc.text),
+ // id: doc[Id],
+ // type: StrCast(doc.type)
+ // })
+ // }
+ // })
+ console.log("dash docs", dashDocs)
+ if (query !== undefined) {
+ const response = await fetchRecommendations(src, query, [], true)
+ const num_recs = response.num_recommendations
+ const recs = response.recommendations
+ const keywords = response.keywords
+ const response_bounds: IBounds = {
+ max_x: response.max_x,
+ max_y: response.max_y,
+ min_x: response.min_x,
+ min_y: response.min_y
+ }
+ // if (NewLightboxView.NewLightboxDoc) {
+ // NewLightboxView.NewLightboxDoc.keywords = new List<string>(keywords);
+ // setKeywordsLoc(NewLightboxView.Keywords);
+ // }
+ // console.log(response_bounds)
+ NewLightboxView.SetBounds(response_bounds)
+ const recommendations: IRecommendation[] = [];
+ for (const key in recs) {
+ console.log(key)
+ const title = recs[key].title;
+ const url = recs[key].url
+ const type = recs[key].type
+ const text = recs[key].text
+ const transcript = recs[key].transcript
+ const previewUrl = recs[key].previewUrl
+ const embedding = recs[key].embedding
+ const distance = recs[key].distance
+ const source = recs[key].source
+ const related_concepts = recs[key].related_concepts
+ const docId = recs[key].doc_id
+ related_concepts.length >= 1 && recommendations.push({
+ title: title,
+ data: url,
+ type: type,
+ text: text,
+ transcript: transcript,
+ previewUrl: previewUrl,
+ embedding: embedding,
+ distance: Math.round(distance * 100) / 100,
+ source: source,
+ related_concepts: related_concepts,
+ docId: docId
+ })
+ }
+ recommendations.sort((a, b) => {
+ if (a.distance && b.distance) {
+ return a.distance - b.distance
+ } else return 0
+ })
+ console.log("[rec]: ", recommendations)
+ NewLightboxView.SetRecs(recommendations)
+ setRecs(recommendations)
+ }
+ }
+ getRecommendations();
+ }, [update])
+
+
+
+ return <div className={`recommendationlist-container`} onPointerDown={(e) => {e.stopPropagation()}}>
+ <div className={`header`}>
+ <div className={`title`}>
+ Recommendations
+ </div>
+ {NewLightboxView.LightboxDoc && <div style={{fontSize: 10}}>
+ The recommendations are produced based on the text in the document <b><u>{StrCast(NewLightboxView.LightboxDoc.title)}</u></b>. The following keywords are used to fetch the recommendations.
+ </div>}
+ <div className={`lb-label`}>Keywords</div>
+ {loadingKeywords ? <div className={`keywords`}>
+ <div className={`keyword ${loadingKeywords && 'loading'}`}/>
+ <div className={`keyword ${loadingKeywords && 'loading'}`}/>
+ <div className={`keyword ${loadingKeywords && 'loading'}`}/>
+ <div className={`keyword ${loadingKeywords && 'loading'}`}/>
+ </div>
+ :
+ <div className={`keywords`}>
+ {keywordsLoc && keywordsLoc.map((word, ind) => {
+ return <div className={`keyword`}>
+ {word}
+ <IconButton type={Type.PRIM} size={Size.XSMALL} color={Colors.DARK_GRAY} icon={<GrClose/>} onClick={() => {
+ let kw = keywordsLoc
+ kw.splice(ind)
+ NewLightboxView.SetKeywords(kw)
+ }}/>
+ </div>
+ })}
+ </div>
+ }
+ {!showMore ?
+ <div className={`lb-caret`} onClick={() => {setShowMore(true)}}>
+ More <FaCaretDown/>
+ </div>
+ :
+ <div className={`more`}>
+ <div className={`lb-caret`} onClick={() => {setShowMore(false)}}>
+ Less <FaCaretUp/>
+ </div>
+ <div className={`lb-label`}>Type</div>
+ <div className={`lb-label`}>Sources</div>
+ </div>
+ }
+ </div>
+ <div className={`recommendations`}>
+ {recs && recs.map((rec: IRecommendation) => {
+ return <Recommendation {...rec} />
+ })}
+ </div>
+ </div>
+} \ No newline at end of file
diff --git a/src/client/views/newlightbox/RecommendationList/index.ts b/src/client/views/newlightbox/RecommendationList/index.ts
new file mode 100644
index 000000000..f4555c1f2
--- /dev/null
+++ b/src/client/views/newlightbox/RecommendationList/index.ts
@@ -0,0 +1 @@
+export * from './RecommendationList' \ No newline at end of file
diff --git a/src/client/views/newlightbox/RecommendationList/utils.ts b/src/client/views/newlightbox/RecommendationList/utils.ts
new file mode 100644
index 000000000..cdfff3258
--- /dev/null
+++ b/src/client/views/newlightbox/RecommendationList/utils.ts
@@ -0,0 +1,9 @@
+import { IRecommendation } from "../components";
+
+export interface IRecommendationList {
+ loading?: boolean,
+ keywords?: string[],
+ recs?: IRecommendation[]
+ getRecs?: any
+}
+
diff --git a/src/client/views/newlightbox/components/EditableText/EditableText.scss b/src/client/views/newlightbox/components/EditableText/EditableText.scss
new file mode 100644
index 000000000..7828538ab
--- /dev/null
+++ b/src/client/views/newlightbox/components/EditableText/EditableText.scss
@@ -0,0 +1,34 @@
+@import '../../NewLightboxStyles.scss';
+
+.lb-editableText,
+.lb-displayText {
+ padding: 4px 7px !important;
+ border: $standard-border !important;
+ border-color: $gray-l2 !important;
+}
+
+.lb-editableText {
+ -webkit-appearance: none;
+ overflow: hidden;
+ font-size: inherit;
+ border: none;
+ outline: none;
+ width: 100%;
+ margin: 0px;
+ padding: 0px;
+ box-shadow: none !important;
+ background: none;
+
+ &:focus {
+ outline: none;
+ background-color: $blue-l1;
+ }
+}
+
+.lb-displayText {
+ cursor: text !important;
+ width: 100%;
+ display: flex;
+ align-items: center;
+ font-size: inherit;
+} \ No newline at end of file
diff --git a/src/client/views/newlightbox/components/EditableText/EditableText.tsx b/src/client/views/newlightbox/components/EditableText/EditableText.tsx
new file mode 100644
index 000000000..e9e7ca264
--- /dev/null
+++ b/src/client/views/newlightbox/components/EditableText/EditableText.tsx
@@ -0,0 +1,65 @@
+import * as React from 'react'
+import './EditableText.scss'
+import { Size } from 'browndash-components'
+
+export interface IEditableTextProps {
+ text: string
+ placeholder?: string
+ editing: boolean
+ onEdit: (newText: string) => void
+ setEditing: (editing: boolean) => void
+ backgroundColor?: string
+ size?: Size
+ height?: number
+}
+
+/**
+ * Editable Text is used for inline renaming of some text.
+ * It appears as normal UI text but transforms into a text input field when the user clicks on or focuses it.
+ * @param props
+ * @returns
+ */
+export const EditableText = (props: IEditableTextProps) => {
+ const {
+ editing,
+ height,
+ size,
+ text,
+ onEdit,
+ setEditing,
+ backgroundColor,
+ placeholder,
+ } = props
+
+ const handleOnChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+ onEdit(event.target.value)
+ }
+
+ return editing ? (
+ <input
+ style={{ background: backgroundColor, height: height }}
+ placeholder={placeholder}
+ size={1}
+ className="lb-editableText"
+ autoFocus
+ onChange={handleOnChange}
+ onBlur={() => setEditing(false)}
+ defaultValue={text}
+ ></input>
+ ) : (
+ <input
+ style={{ background: backgroundColor, height: height }}
+ placeholder={placeholder}
+ size={1}
+ className="lb-editableText"
+ autoFocus
+ onChange={handleOnChange}
+ onBlur={() => setEditing(false)}
+ defaultValue={text}
+ ></input>
+ // <div className="lb-displayText" onClick={(e) => {
+ // e.stopPropagation()
+ // setEditing(true)
+ // }}>{text}</div>
+ )
+}
diff --git a/src/client/views/newlightbox/components/EditableText/index.ts b/src/client/views/newlightbox/components/EditableText/index.ts
new file mode 100644
index 000000000..e3367b175
--- /dev/null
+++ b/src/client/views/newlightbox/components/EditableText/index.ts
@@ -0,0 +1 @@
+export * from './EditableText'
diff --git a/src/client/views/newlightbox/components/Recommendation/Recommendation.scss b/src/client/views/newlightbox/components/Recommendation/Recommendation.scss
new file mode 100644
index 000000000..c86c63ba0
--- /dev/null
+++ b/src/client/views/newlightbox/components/Recommendation/Recommendation.scss
@@ -0,0 +1,176 @@
+@import '../../NewLightboxStyles.scss';
+
+.recommendation-container {
+ width: 100%;
+ height: fit-content;
+ min-height: 180px;
+ border-radius: 20px;
+ display: grid;
+ grid-template-columns: 0% 100%;
+ grid-template-rows: auto auto auto auto auto;
+ gap: 5px 0px;
+ padding: 10px;
+ cursor: pointer;
+ transition: 0.2s ease;
+ border: $standard-border;
+ border-color: $gray-l2;
+ background: white;
+
+ &:hover {
+ // background: white !important;
+ transform: scale(1.02);
+ z-index: 0;
+
+ .title {
+ text-decoration: underline;
+ }
+ }
+
+ &.previewUrl {
+ grid-template-columns: calc(30% - 10px) 70%;
+ grid-template-rows: auto auto auto auto auto;
+ gap: 5px 10px;
+ }
+
+ &.loading {
+ animation: skeleton-loading-l2 1s linear infinite alternate;
+ border: none;
+ grid-template-columns: calc(30% - 10px) 70%;
+ grid-template-rows: auto auto auto auto auto;
+ gap: 5px 10px;
+
+ .image-container,
+ .title,
+ .info,
+ .source,
+ .explainer,
+ .hide-rec {
+ animation: skeleton-loading-l3 1s linear infinite alternate;
+ }
+
+ .title {
+ border-radius: 20px;
+ }
+ }
+
+ .distance-container,
+ .type-container,
+ .source-container {
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-start;
+ align-items: center;
+ gap: 5px;
+ }
+
+ .image-container {
+ grid-row: 2/5;
+ grid-column: 1;
+ border-radius: 20px;
+ overflow: hidden;
+
+ .image {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ }
+ }
+
+ .title {
+ grid-row: 1;
+ grid-column: 1/3;
+ border-radius: 20px;
+ font-size: $h2-size;
+ font-weight: $h2-weight;
+ overflow: hidden;
+ border-radius: 0px;
+ min-height: 30px;
+ }
+
+ .info {
+ grid-row: 2;
+ grid-column: 2;
+ border-radius: 20px;
+ display: flex;
+ flex-direction: row;
+ gap: 5px;
+ font-size: $body-size;
+
+ .lb-type {
+ padding: 2px 7px !important;
+ background: $gray-l2;
+ }
+ }
+
+ .lb-label {
+ color: $gray-l3;
+ font-weight: $h1-weight;
+ font-size: $body-size;
+ }
+
+ .source {
+ grid-row: 3;
+ grid-column: 2;
+ border-radius: 20px;
+ font-size: $body-size;
+ display: flex;
+ justify-content: flex-start;
+ align-items: center;
+
+ .lb-source {
+ padding: 2px 7px !important;
+ background: $gray-l2;
+ border-radius: 10px;
+ white-space: nowrap;
+ max-width: 130px;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ }
+ }
+
+ .explainer {
+ grid-row: 4;
+ grid-column: 2;
+ border-radius: 20px;
+ font-size: 10px;
+ width: 100%;
+ background: $blue-l1;
+ border-radius: 0;
+ padding: 10px;
+
+ .concepts-container {
+ display: flex;
+ flex-flow: row wrap;
+ margin-top: 3px;
+ gap: 3px;
+ .concept {
+ padding: 2px 7px !important;
+ background: $gray-l2;
+ }
+ }
+ }
+
+ .hide-rec {
+ grid-row: 5;
+ grid-column: 2;
+ border-radius: 20px;
+ font-size: $body-size;
+ display: flex;
+ align-items: center;
+ margin-top: 5px;
+ gap: 5px;
+ justify-content: flex-end;
+ text-transform: underline;
+ }
+
+ &.dark {
+ background: $black;
+ border-color: $white;
+ }
+
+ &.light,
+ &.default {
+ background: $white;
+ border-color: $white;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/newlightbox/components/Recommendation/Recommendation.tsx b/src/client/views/newlightbox/components/Recommendation/Recommendation.tsx
new file mode 100644
index 000000000..2c2f04b9f
--- /dev/null
+++ b/src/client/views/newlightbox/components/Recommendation/Recommendation.tsx
@@ -0,0 +1,102 @@
+import * as React from 'react';
+import { IRecommendation } from './utils';
+import './Recommendation.scss';
+import { getType } from '../../utils';
+import { FaEyeSlash } from 'react-icons/fa';
+import { NewLightboxView } from '../../NewLightboxView';
+import { DocumentManager } from '../../../../util/DocumentManager';
+import { Doc } from '../../../../../fields/Doc';
+import { Docs } from '../../../../documents/Documents';
+
+export const Recommendation = (props: IRecommendation) => {
+ const { title, data, type, text, transcript, loading, source, previewUrl, related_concepts, distance, docId } = props;
+
+ return (
+ <div
+ className={`recommendation-container ${loading && 'loading'} ${previewUrl && 'previewUrl'}`}
+ onClick={() => {
+ let doc: Doc | null = null;
+ if (source == 'Dash' && docId) {
+ const docView = DocumentManager.Instance.getDocumentViewsById(docId).lastElement();
+ if (docView) {
+ doc = docView.rootDoc;
+ }
+ } else if (data) {
+ switch (type) {
+ case 'YouTube':
+ console.log('create ', type, 'document');
+ doc = Docs.Create.VideoDocument(data, { title: title, _width: 400, _height: 315, transcript: transcript });
+ break;
+ case 'Video':
+ console.log('create ', type, 'document');
+ doc = Docs.Create.VideoDocument(data, { title: title, _width: 400, _height: 315, transcript: transcript });
+ break;
+ case 'Webpage':
+ console.log('create ', type, 'document');
+ doc = Docs.Create.WebDocument(data, { title: title, text: text });
+ break;
+ case 'HTML':
+ console.log('create ', type, 'document');
+ doc = Docs.Create.WebDocument(data, { title: title, text: text });
+ break;
+ case 'Text':
+ console.log('create ', type, 'document');
+ doc = Docs.Create.TextDocument(data, { title: title, text: text });
+ break;
+ case 'PDF':
+ console.log('create ', type, 'document');
+ doc = Docs.Create.PdfDocument(data, { title: title, text: text });
+ break;
+ }
+ }
+ if (doc !== null) NewLightboxView.SetNewLightboxDoc(doc);
+ }}>
+ {loading ? <div className={`image-container`}></div> : previewUrl ? <div className={`image-container`}>{<img className={`image`} src={previewUrl}></img>}</div> : null}
+ <div className={`title`}>{title}</div>
+ <div className={`info`}>
+ {!loading && (
+ <div className={`type-container`}>
+ <div className={`lb-label`}>Type</div>
+ <div className={`lb-type`}>{getType(type!)}</div>
+ </div>
+ )}
+ {!loading && (
+ <div className={`distance-container`}>
+ <div className={`lb-label`}>Distance</div>
+ <div className={`lb-distance`}>{distance}</div>
+ </div>
+ )}
+ </div>
+ <div className={`source`}>
+ {!loading && (
+ <div className={`source-container`}>
+ <div className={`lb-label`}>Source</div>
+ <div className={`lb-source`}>{source}</div>
+ </div>
+ )}
+ </div>
+ <div className={`explainer`}>
+ {!loading && (
+ <div>
+ You are seeing this recommendation because this document also explores
+ <div className={`concepts-container`}>
+ {related_concepts?.map(val => {
+ return <div className={'concept'}>{val}</div>;
+ })}
+ </div>
+ </div>
+ )}
+ </div>
+ <div className={`hide-rec`}>
+ {!loading && (
+ <>
+ <div>Hide Recommendation</div>
+ <div style={{ fontSize: 15, paddingRight: 5 }}>
+ <FaEyeSlash />
+ </div>
+ </>
+ )}
+ </div>
+ </div>
+ );
+};
diff --git a/src/client/views/newlightbox/components/Recommendation/index.ts b/src/client/views/newlightbox/components/Recommendation/index.ts
new file mode 100644
index 000000000..12ebf9d6e
--- /dev/null
+++ b/src/client/views/newlightbox/components/Recommendation/index.ts
@@ -0,0 +1,2 @@
+export * from './utils'
+export * from './Recommendation' \ No newline at end of file
diff --git a/src/client/views/newlightbox/components/Recommendation/utils.ts b/src/client/views/newlightbox/components/Recommendation/utils.ts
new file mode 100644
index 000000000..796ce0eb0
--- /dev/null
+++ b/src/client/views/newlightbox/components/Recommendation/utils.ts
@@ -0,0 +1,23 @@
+import { DocumentType } from "../../../../documents/DocumentTypes"
+
+export interface IRecommendation {
+ loading?: boolean
+ type?: DocumentType | string,
+ data?: string,
+ title?: string,
+ text?: string,
+ source?: string,
+ previewUrl?: string,
+ transcript?: {
+ text: string,
+ start: number,
+ duration: number
+ }[],
+ embedding?: {
+ x: number,
+ y: number
+ },
+ distance?: number,
+ related_concepts?: string[],
+ docId?: string
+} \ No newline at end of file
diff --git a/src/client/views/newlightbox/components/SkeletonDoc/SkeletonDoc.scss b/src/client/views/newlightbox/components/SkeletonDoc/SkeletonDoc.scss
new file mode 100644
index 000000000..e541e3f3c
--- /dev/null
+++ b/src/client/views/newlightbox/components/SkeletonDoc/SkeletonDoc.scss
@@ -0,0 +1,82 @@
+@import '../../NewLightboxStyles.scss';
+
+.skeletonDoc-container {
+ display: flex;
+ flex-direction: column;
+ height: calc(100% - 40px);
+ margin: 20px;
+ gap: 20px;
+
+ .header {
+ width: calc(100% - 20px);
+ height: 80px;
+ background: $gray-l2;
+ animation: skeleton-loading-l2 1s linear infinite alternate;
+ display: grid;
+ grid-template-rows: 60% 40%;
+ padding: 10px;
+ grid-template-columns: auto auto auto auto;
+ border-radius: 20px;
+
+ .title {
+ grid-row: 1;
+ grid-column: 1 / 5;
+ display: flex;
+ width: fit-content;
+ height: 100%;
+ min-width: 500px;
+ font-size: $title-size;
+ animation: skeleton-loading-l3 1s linear infinite alternate;
+ border-radius: 20px;
+ }
+
+ .type {
+ display: flex;
+ padding: 3px 7px;
+ width: fit-content;
+ height: fit-content;
+ margin-top: 8px;
+ min-height: 15px;
+ min-width: 60px;
+ grid-row: 2;
+ grid-column: 1;
+ animation: skeleton-loading-l3 1s linear infinite alternate;
+ border-radius: 20px;
+ }
+
+ .buttons-container {
+ grid-row: 1 / 3;
+ grid-column: 5;
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ gap: 10px;
+
+ .button {
+ width: 50px;
+ height: 50px;
+ border-radius: 100%;
+ animation: skeleton-loading-l3 1s linear infinite alternate;
+ }
+ }
+
+ }
+
+ .content {
+ width: 100%;
+ flex: 1;
+ -webkit-flex: 1; /* Chrome */
+ background: $gray-l2;
+ animation: skeleton-loading-l2 1s linear infinite alternate;
+ border-radius: 20px;
+ }
+
+ // &.dark {
+ // background: $black;
+ // }
+
+ // &.light,
+ // &.default {
+ // background: $white;
+ // }
+} \ No newline at end of file
diff --git a/src/client/views/newlightbox/components/SkeletonDoc/SkeletonDoc.tsx b/src/client/views/newlightbox/components/SkeletonDoc/SkeletonDoc.tsx
new file mode 100644
index 000000000..50cee893f
--- /dev/null
+++ b/src/client/views/newlightbox/components/SkeletonDoc/SkeletonDoc.tsx
@@ -0,0 +1,22 @@
+import './SkeletonDoc.scss';
+import { ISkeletonDoc } from "./utils";
+import * as React from 'react';
+
+export const SkeletonDoc = (props: ISkeletonDoc) => {
+ const { type, data } = props
+
+ return <div className={`skeletonDoc-container`}>
+ <div className={`header`}>
+ <div className={`title`}></div>
+ <div className={`type`}></div>
+ <div className={`tags`}></div>
+ <div className={`buttons-container`}>
+ <div className={`button`}></div>
+ <div className={`button`}></div>
+ </div>
+ </div>
+ <div className={`content`}>
+ {data}
+ </div>
+ </div>
+} \ No newline at end of file
diff --git a/src/client/views/newlightbox/components/SkeletonDoc/index.ts b/src/client/views/newlightbox/components/SkeletonDoc/index.ts
new file mode 100644
index 000000000..396b7272b
--- /dev/null
+++ b/src/client/views/newlightbox/components/SkeletonDoc/index.ts
@@ -0,0 +1 @@
+export * from './SkeletonDoc' \ No newline at end of file
diff --git a/src/client/views/newlightbox/components/SkeletonDoc/utils.ts b/src/client/views/newlightbox/components/SkeletonDoc/utils.ts
new file mode 100644
index 000000000..81c32c328
--- /dev/null
+++ b/src/client/views/newlightbox/components/SkeletonDoc/utils.ts
@@ -0,0 +1,5 @@
+import { IRecommendation } from "../Recommendation";
+
+export interface ISkeletonDoc extends IRecommendation {
+
+} \ No newline at end of file
diff --git a/src/client/views/newlightbox/components/Template/Template.scss b/src/client/views/newlightbox/components/Template/Template.scss
new file mode 100644
index 000000000..5b72ddaf9
--- /dev/null
+++ b/src/client/views/newlightbox/components/Template/Template.scss
@@ -0,0 +1,15 @@
+@import '../../NewLightboxStyles.scss';
+
+.template-container {
+ width: 100vw;
+ height: 100vh;
+
+ &.dark {
+ background: $black;
+ }
+
+ &.light,
+ &.default {
+ background: $white;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/newlightbox/components/Template/Template.tsx b/src/client/views/newlightbox/components/Template/Template.tsx
new file mode 100644
index 000000000..9c6f0f59c
--- /dev/null
+++ b/src/client/views/newlightbox/components/Template/Template.tsx
@@ -0,0 +1,10 @@
+import './Template.scss';
+import * as React from 'react';
+import { ITemplate } from "./utils";
+
+export const Template = (props: ITemplate) => {
+
+ return <div className={`template-container`}>
+
+ </div>
+} \ No newline at end of file
diff --git a/src/client/views/newlightbox/components/Template/index.ts b/src/client/views/newlightbox/components/Template/index.ts
new file mode 100644
index 000000000..36b5f3f46
--- /dev/null
+++ b/src/client/views/newlightbox/components/Template/index.ts
@@ -0,0 +1 @@
+export * from './Template' \ No newline at end of file
diff --git a/src/client/views/newlightbox/components/Template/utils.ts b/src/client/views/newlightbox/components/Template/utils.ts
new file mode 100644
index 000000000..965e653ec
--- /dev/null
+++ b/src/client/views/newlightbox/components/Template/utils.ts
@@ -0,0 +1,3 @@
+export interface ITemplate {
+
+} \ No newline at end of file
diff --git a/src/client/views/newlightbox/components/index.ts b/src/client/views/newlightbox/components/index.ts
new file mode 100644
index 000000000..3f9128690
--- /dev/null
+++ b/src/client/views/newlightbox/components/index.ts
@@ -0,0 +1,3 @@
+export * from './Template'
+export * from './Recommendation'
+export * from './SkeletonDoc' \ No newline at end of file
diff --git a/src/client/views/newlightbox/utils.ts b/src/client/views/newlightbox/utils.ts
new file mode 100644
index 000000000..6016abca4
--- /dev/null
+++ b/src/client/views/newlightbox/utils.ts
@@ -0,0 +1,121 @@
+import { DocumentType } from "../../documents/DocumentTypes";
+import { IRecommendation } from "./components";
+
+export interface IDocRequest {
+ id: string,
+ title: string,
+ text: string,
+ type: string
+}
+
+export const fetchRecommendations = async (src: string, query: string, docs?: IDocRequest[], dummy?: boolean) => {
+ console.log("[rec] making request")
+ if (dummy) {
+ return {
+ "recommendations": dummyRecs,
+ "keywords": dummyKeywords,
+ "num_recommendations": 4,
+ "max_x": 100,
+ "max_y": 100,
+ "min_x": 0,
+ "min_y": 0
+
+ };
+ }
+ const response = await fetch('http://127.0.0.1:8000/recommend', {
+ method: 'POST',
+ headers: {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ "src": src,
+ "query": query,
+ "docs": docs
+ })
+ })
+ const data = await response.json();
+
+ return data;
+}
+
+export const fetchKeywords = async (text: string, n: number, dummy?: boolean) => {
+ console.log("[fetchKeywords]")
+ if (dummy) {
+ return {
+ "keywords": dummyKeywords
+ };
+ }
+ const response = await fetch('http://127.0.0.1:8000/keywords', {
+ method: 'POST',
+ headers: {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ "text": text,
+ "n": n
+ })
+ })
+ const data = await response.json()
+ return data;
+}
+
+export const getType = (type: DocumentType | string) => {
+ switch(type) {
+ case DocumentType.AUDIO:
+ return "Audio"
+ case DocumentType.VID:
+ return "Video"
+ case DocumentType.PDF:
+ return "PDF"
+ case DocumentType.WEB:
+ return "Webpage"
+ case "YouTube":
+ return "Video"
+ case "HTML":
+ return "Webpage"
+ default:
+ return "Unknown: " + type
+ }
+}
+
+const dummyRecs = {
+ "a": {
+ title: 'Vannevar Bush - American Engineer',
+ previewUrl: 'https://cdn.britannica.com/98/23598-004-1E6A382E/Vannevar-Bush-Differential-Analyzer-1935.jpg',
+ type: 'web',
+ distance: 2.3,
+ source: 'www.britannica.com',
+ related_concepts: ['vannevar bush', 'knowledge'],
+ embedding: {
+ x: 0,
+ y: 0
+ }
+ },
+ "b": {
+ title: "From Memex to hypertext: Vannevar Bush and the mind's machine",
+ type: 'pdf',
+ distance: 5.4,
+ source: 'Google Scholar',
+ related_concepts: ['memex', 'vannevar bush', 'hypertext'],
+ },
+ "c": {
+ title: 'How the hyperlink changed everything | Small Thing Big Idea, a TED series',
+ previewUrl: 'https://pi.tedcdn.com/r/talkstar-photos.s3.amazonaws.com/uploads/b17d043f-2642-4117-a913-52204505513f/MargaretGouldStewart_2018V-embed.jpg?u%5Br%5D=2&u%5Bs%5D=0.5&u%5Ba%5D=0.8&u%5Bt%5D=0.03&quality=82w=640',
+ type: 'youtube',
+ distance: 5.3,
+ source: 'www.youtube.com',
+ related_concepts: ['User Control', 'Explanations']
+ },
+ "d": {
+ title: 'Recommender Systems: Behind the Scenes of Machine Learning-Based Personalization',
+ previewUrl: 'https://sloanreview.mit.edu/wp-content/uploads/2018/10/MAG-Ransbotham-Ratings-Recommendations-1200X627-1200x627.jpg',
+ type: 'pdf',
+ distance: 9.3,
+ source: 'www.altexsoft.com',
+ related_concepts: ['User Control', 'Explanations']
+ }
+}
+
+const dummyKeywords = ['user control', 'vannevar bush', 'hypermedia', 'hypertext'] \ No newline at end of file
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 8e83cf121..6558d215a 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -233,7 +233,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
this.dataDoc[this.fieldKey + '-recordingStart'] = new DateField();
DocUtils.ActiveRecordings.push(this);
this._recorder.ondataavailable = async (e: any) => {
- const [{ result }] = await Networking.UploadFilesToServer(e.data);
+ const [{ result }] = await Networking.UploadFilesToServer({file: e.data});
if (!(result instanceof Error)) {
this.props.Document[this.fieldKey] = new AudioField(result.accessPaths.agnostic.client);
}
@@ -362,10 +362,10 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
Doc.GetProto(newDoc).recordingSource = this.dataDoc;
Doc.GetProto(newDoc).recordingStart = ComputedField.MakeFunction(`self.recordingSource["${this.fieldKey}-recordingStart"]`);
Doc.GetProto(newDoc).mediaState = ComputedField.MakeFunction('self.recordingSource.mediaState');
- if (DocListCast(Doc.MyOverlayDocs?.data).includes(this.rootDoc)) {
+ if (Doc.IsInMyOverlay(this.rootDoc)) {
newDoc.overlayX = this.rootDoc.x;
newDoc.overlayY = NumCast(this.rootDoc.y) + NumCast(this.rootDoc._height);
- Doc.AddDocToList(Doc.MyOverlayDocs, undefined, newDoc);
+ Doc.AddToMyOverlay(newDoc);
} else {
this.props.addDocument?.(newDoc);
}
@@ -447,7 +447,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
timelineScreenToLocal = () => this.props.ScreenToLocalTransform().translate(0, -AudioBox.topControlsHeight);
- setPlayheadTime = (time: number) => (this._ele!.currentTime = this.layoutDoc._layout_currentTimecode = time);
+ setPlayheadTime = (time: number) => (this._ele!.currentTime /*= this.layoutDoc._layout_currentTimecode*/ = time);
playing = () => this.mediaState === media_state.Playing;
diff --git a/src/client/views/nodes/ColorBox.tsx b/src/client/views/nodes/ColorBox.tsx
index 330cc3971..aae759702 100644
--- a/src/client/views/nodes/ColorBox.tsx
+++ b/src/client/views/nodes/ColorBox.tsx
@@ -2,7 +2,8 @@ import React = require('react');
import { action } from 'mobx';
import { observer } from 'mobx-react';
import { ColorState, SketchPicker } from 'react-color';
-import { Doc, HeightSym, WidthSym } from '../../../fields/Doc';
+import { Doc } from '../../../fields/Doc';
+import { Height, Width } from '../../../fields/DocSymbols';
import { InkTool } from '../../../fields/InkField';
import { StrCast } from '../../../fields/Types';
import { DocumentType } from '../../documents/DocumentTypes';
@@ -48,7 +49,7 @@ export class ColorBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
render() {
- const scaling = Math.min(this.layoutDoc.layout_fitWidth ? 10000 : this.props.PanelHeight() / this.rootDoc[HeightSym](), this.props.PanelWidth() / this.rootDoc[WidthSym]());
+ const scaling = Math.min(this.layoutDoc.layout_fitWidth ? 10000 : this.props.PanelHeight() / this.rootDoc[Height](), this.props.PanelWidth() / this.rootDoc[Width]());
return (
<div
className={`colorBox-container${this.props.isContentActive() ? '-interactive' : ''}`}
diff --git a/src/client/views/nodes/ComparisonBox.scss b/src/client/views/nodes/ComparisonBox.scss
index 660045a6f..a12f1c12b 100644
--- a/src/client/views/nodes/ComparisonBox.scss
+++ b/src/client/views/nodes/ComparisonBox.scss
@@ -93,4 +93,4 @@
display: flex;
}
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index 3be2d9a77..ca5ec9389 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -2,11 +2,10 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed, observable } from 'mobx';
import { observer } from 'mobx-react';
import { Doc, Opt } from '../../../fields/Doc';
-import { Cast, NumCast, StrCast } from '../../../fields/Types';
+import { DocCast, NumCast, StrCast } from '../../../fields/Types';
import { emptyFunction, returnFalse, returnNone, returnZero, setupMoveUpEvents } from '../../../Utils';
-import { Docs } from '../../documents/Documents';
+import { Docs, DocUtils } from '../../documents/Documents';
import { DragManager } from '../../util/DragManager';
-import { SnappingManager } from '../../util/SnappingManager';
import { undoBatch } from '../../util/UndoManager';
import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent';
import { StyleProp } from '../StyleProvider';
@@ -32,48 +31,56 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
get clipWidthKey() {
return '_' + this.props.fieldKey + '_clipWidth';
}
-
componentDidMount() {
this.props.setContentView?.(this);
}
-
protected createDropTarget = (ele: HTMLDivElement | null, fieldKey: string, disposerId: number) => {
this._disposers[disposerId]?.();
if (ele) {
- // create disposers identified by disposerId to remove drag & drop listeners
- this._disposers[disposerId] = DragManager.MakeDropTarget(ele, (e, dropEvent) => this.dropHandler(e, dropEvent, fieldKey), this.layoutDoc);
+ this._disposers[disposerId] = DragManager.MakeDropTarget(ele, (e, dropEvent) => this.internalDrop(e, dropEvent, fieldKey), this.layoutDoc);
}
};
@undoBatch
- private dropHandler = (event: Event, dropEvent: DragManager.DropEvent, fieldKey: string) => {
+ private internalDrop = (e: Event, dropEvent: DragManager.DropEvent, fieldKey: string) => {
if (dropEvent.complete.docDragData) {
- event.stopPropagation(); // prevent parent Doc from registering new position so that it snaps back into place
const droppedDocs = dropEvent.complete.docDragData?.droppedDocuments;
- if (droppedDocs?.length) {
- this.dataDoc[fieldKey] = droppedDocs[0];
- }
+ const added = dropEvent.complete.docDragData.moveDocument?.(droppedDocs, this.rootDoc, (doc: Doc | Doc[]) => this.addDoc(doc instanceof Doc ? doc : doc.lastElement(), fieldKey));
+ Doc.SetContainer(droppedDocs.lastElement(), this.dataDoc);
+ !added && e.preventDefault();
+ e.stopPropagation(); // prevent parent Doc from registering new position so that it snaps back into place
+ return added;
}
};
private registerSliding = (e: React.PointerEvent<HTMLDivElement>, targetWidth: number) => {
- e.button !== 2 &&
+ if (e.button !== 2) {
setupMoveUpEvents(
this,
e,
this.onPointerMove,
emptyFunction,
+ action((e, doubleTap) => {
+ if (doubleTap) {
+ this._isAnyChildContentActive = true;
+ if (!this.dataDoc[this.fieldKey + '_1']) this.dataDoc[this.fieldKey + '_1'] = DocUtils.copyDragFactory(Doc.UserDoc().emptyNote as Doc);
+ if (!this.dataDoc[this.fieldKey + '_2']) this.dataDoc[this.fieldKey + '_2'] = DocUtils.copyDragFactory(Doc.UserDoc().emptyNote as Doc);
+ }
+ }),
+ false,
+ undefined,
action(() => {
- // on click, animate slider movement to the targetWidth
+ if (this._isAnyChildContentActive) return;
this._animating = 'all 200ms';
+ // on click, animate slider movement to the targetWidth
this.layoutDoc[this.clipWidthKey] = (targetWidth * 100) / this.props.PanelWidth();
setTimeout(
action(() => (this._animating = '')),
200
);
- }),
- false
+ })
);
+ }
};
@action
@@ -86,7 +93,12 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
};
getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
- const anchor = Docs.Create.ImageanchorDocument({ title: 'ImgAnchor:' + this.rootDoc.title, presTransition: 1000, unrendered: true, annotationOn: this.rootDoc });
+ const anchor = Docs.Create.ConfigDocument({
+ title: 'CompareAnchor:' + this.rootDoc.title,
+ // set presentation timing properties for restoring view
+ presTransition: 1000,
+ annotationOn: this.rootDoc,
+ });
if (anchor) {
if (!addAsAnnotation) anchor.backgroundColor = 'transparent';
/* addAsAnnotation &&*/ this.addDocument(anchor);
@@ -101,12 +113,29 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
e.stopPropagation; // prevent click event action (slider movement) in registerSliding
delete this.dataDoc[fieldKey];
};
+ moveDoc = (doc: Doc, addDocument: (document: Doc | Doc[]) => boolean, which: string) => this.remDoc(doc, which) && addDocument(doc);
+ addDoc = (doc: Doc, which: string) => {
+ this.dataDoc[which] = doc;
+ return true;
+ };
+ remDoc = (doc: Doc, which: string) => {
+ if (this.dataDoc[which] === doc) {
+ this.dataDoc[which] = undefined;
+ return true;
+ }
+ return false;
+ };
+
+ whenChildContentsActiveChanged = action((isActive: boolean) => (this._isAnyChildContentActive = isActive));
docStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string): any => {
if (property === StyleProp.PointerEvents) return 'none';
return this.props.styleProvider?.(doc, props, property);
};
-
+ moveDoc1 = (doc: Doc | Doc[], targetCol: Doc | undefined, addDoc: any) => (doc instanceof Doc ? [doc] : doc).reduce((res, doc: Doc) => res && this.moveDoc(doc, addDoc, this.fieldKey + '_1'), true);
+ moveDoc2 = (doc: Doc | Doc[], targetCol: Doc | undefined, addDoc: any) => (doc instanceof Doc ? [doc] : doc).reduce((res, doc: Doc) => res && this.moveDoc(doc, addDoc, this.fieldKey + '_2'), true);
+ remDoc1 = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((res, doc) => res && this.remDoc(doc, this.fieldKey + '_1'), true);
+ remDoc2 = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((res, doc) => res && this.remDoc(doc, this.fieldKey + '_2'), true);
render() {
const clearButton = (which: string) => {
return (
@@ -114,36 +143,35 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
className={`clear-button ${which}`}
onPointerDown={e => e.stopPropagation()} // prevent triggering slider movement in registerSliding
onClick={e => this.clearDoc(e, which)}>
- <FontAwesomeIcon className={`clear-button ${which}`} icon={'times'} size="sm" />
+ <FontAwesomeIcon className={`clear-button ${which}`} icon="times" size="sm" />
</div>
);
};
const displayDoc = (which: string) => {
- const whichDoc = Cast(this.dataDoc[which], Doc, null);
- // if (whichDoc?.type === DocumentType.MARKER) whichDoc = Cast(whichDoc.annotationOn, Doc, null);
- const targetDoc = Cast(whichDoc?.annotationOn, Doc, null) ?? whichDoc;
- return whichDoc ? (
+ const whichDoc = DocCast(this.dataDoc[which]);
+ const targetDoc = DocCast(whichDoc?.annotationOn, whichDoc);
+ return targetDoc ? (
<>
<DocumentView
- ref={r => {
- //whichDoc !== targetDoc && r?.focus(whichDoc, { instant: true });
- }}
{...this.props}
+ Document={targetDoc}
+ DataDoc={undefined}
+ moveDocument={which.endsWith('1') ? this.moveDoc1 : this.moveDoc2}
+ removeDocument={which.endsWith('1') ? this.remDoc1 : this.remDoc2}
NativeWidth={returnZero}
NativeHeight={returnZero}
- isContentActive={returnFalse}
+ isContentActive={emptyFunction}
isDocumentActive={returnFalse}
- styleProvider={this.docStyleProvider}
- Document={targetDoc}
- DataDoc={undefined}
+ whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
+ styleProvider={this._isAnyChildContentActive ? this.props.styleProvider : this.docStyleProvider}
hideLinkButton={true}
- pointerEvents={returnNone}
+ pointerEvents={this._isAnyChildContentActive ? undefined : returnNone}
/>
{clearButton(which)}
</> // placeholder image if doc is missing
) : (
<div className="placeholder">
- <FontAwesomeIcon className="upload-icon" icon={'cloud-upload-alt'} size="lg" />
+ <FontAwesomeIcon className="upload-icon" icon="cloud-upload-alt" size="lg" />
</div>
);
};
@@ -156,7 +184,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
};
return (
- <div className={`comparisonBox${this.props.isContentActive() || SnappingManager.GetIsDragging() ? '-interactive' : ''}` /* change className to easily disable/enable pointer events in CSS */}>
+ <div className={`comparisonBox${this.props.isContentActive() ? '-interactive' : ''}` /* change className to easily disable/enable pointer events in CSS */}>
{displayBox(`${this.fieldKey}_2`, 1, this.props.PanelWidth() - 3)}
<div className="clip-div" style={{ width: this.clipWidth + '%', transition: this._animating, background: StrCast(this.layoutDoc._backgroundColor, 'gray') }}>
{displayBox(`${this.fieldKey}_1`, 0, 0)}
@@ -168,7 +196,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
left: `calc(${this.clipWidth + '%'} - 0.5px)`,
cursor: this.clipWidth < 5 ? 'e-resize' : this.clipWidth / 100 > (this.props.PanelWidth() - 5) / this.props.PanelWidth() ? 'w-resize' : undefined,
}}
- onPointerDown={e => this.registerSliding(e, this.props.PanelWidth() / 2)} /* if clicked, return slide-bar to center */
+ onPointerDown={e => !this._isAnyChildContentActive && this.registerSliding(e, this.props.PanelWidth() / 2)} /* if clicked, return slide-bar to center */
>
<div className="slide-handle" />
</div>
diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
index 5876efb01..d548ab9f1 100644
--- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx
+++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
@@ -71,14 +71,14 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
};
getAnchor = (addAsAnnotation?: boolean, pinProps?: PinProps) => {
- const anchor =
- this._chartRenderer?.getAnchor(pinProps) ??
- Docs.Create.TextanchorDocument({
- unrendered: true,
- // 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)
- /*put in some options*/
- });
+ const anchor = !pinProps
+ ? this.rootDoc
+ : this._chartRenderer?.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)
+ /*put in some options*/
+ });
anchor.presDataVizView = this.dataVizView;
anchor.presDataVizAxes = this.axes.length ? new List<string>(this.axes) : undefined;
diff --git a/src/client/views/nodes/DataVizBox/components/LineChart.tsx b/src/client/views/nodes/DataVizBox/components/LineChart.tsx
index 11f62a61f..6b564b0c9 100644
--- a/src/client/views/nodes/DataVizBox/components/LineChart.tsx
+++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx
@@ -163,7 +163,10 @@ export class LineChart extends React.Component<LineChartProps> {
// create a document anchor that stores whatever is needed to reconstruct the viewing state (selection,zoom,etc)
getAnchor = (pinProps?: PinProps) => {
- const anchor = Docs.Create.TextanchorDocument({ title: 'line doc selection' + this._currSelected?.x, unrendered: true });
+ const anchor = Docs.Create.ConfigDocument({
+ //
+ title: 'line doc selection' + this._currSelected?.x,
+ });
PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this.props.dataDoc);
anchor.presDataVizSelection = this._currSelected ? new List<number>([this._currSelected.x, this._currSelected.y]) : undefined;
return anchor;
diff --git a/src/client/views/nodes/DataVizBox/components/TableBox.tsx b/src/client/views/nodes/DataVizBox/components/TableBox.tsx
index 3816bddfa..d84e34d52 100644
--- a/src/client/views/nodes/DataVizBox/components/TableBox.tsx
+++ b/src/client/views/nodes/DataVizBox/components/TableBox.tsx
@@ -1,7 +1,7 @@
import { action, computed } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { AnimationSym, Doc } from '../../../../../fields/Doc';
+import { Doc } from '../../../../../fields/Doc';
import { Id } from '../../../../../fields/FieldSymbols';
import { List } from '../../../../../fields/List';
import { emptyFunction, returnFalse, setupMoveUpEvents, Utils } from '../../../../../Utils';
@@ -57,7 +57,7 @@ export class TableBox extends React.Component<TableBoxProps> {
DragManager.StartAnchorAnnoDrag([header.current!], 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.layout_linkDisplay = true;
+ e.linkDocument.link_displayLine = true;
// e.annoDragData.linkSourceDoc.followLinkToggle = e.annoDragData.dropDocument.annotationOn === this.props.rootDoc;
// e.annoDragData.linkSourceDoc.followLinkZoom = false;
}
diff --git a/src/client/views/nodes/DataVizBox/utils/D3Utils.ts b/src/client/views/nodes/DataVizBox/utils/D3Utils.ts
index e1ff6f8eb..10bfb0c64 100644
--- a/src/client/views/nodes/DataVizBox/utils/D3Utils.ts
+++ b/src/client/views/nodes/DataVizBox/utils/D3Utils.ts
@@ -34,7 +34,6 @@ export const createLineGenerator = (xScale: d3.ScaleLinear<number, number, never
};
export const xAxisCreator = (g: d3.Selection<SVGGElement, unknown, null, undefined>, height: number, xScale: d3.ScaleLinear<number, number, never>) => {
- console.log('x axis creator being called');
g.attr('class', 'x-axis').attr('transform', `translate(0,${height})`).call(d3.axisBottom(xScale).tickSize(15));
};
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index 9546a6d38..8ac9d6804 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -1,6 +1,7 @@
import { computed } from 'mobx';
import { observer } from 'mobx-react';
-import { AclPrivate, Doc, Opt } from '../../../fields/Doc';
+import { Doc, Opt } from '../../../fields/Doc';
+import { AclPrivate } from '../../../fields/DocSymbols';
import { ScriptField } from '../../../fields/ScriptField';
import { Cast, StrCast } from '../../../fields/Types';
import { GetEffectiveAcl, TraceMobx } from '../../../fields/util';
@@ -17,7 +18,6 @@ import { SearchBox } from '../search/SearchBox';
import { DashWebRTCVideo } from '../webcam/DashWebRTCVideo';
import { YoutubeBox } from './../../apis/youtube/YoutubeBox';
import { AudioBox } from './AudioBox';
-import { FontIconBox } from './button/FontIconBox';
import { ColorBox } from './ColorBox';
import { ComparisonBox } from './ComparisonBox';
import { DataVizBox } from './DataVizBox/DataVizBox';
@@ -25,9 +25,11 @@ import { DocumentViewProps } from './DocumentView';
import './DocumentView.scss';
import { EquationBox } from './EquationBox';
import { FieldView, FieldViewProps } from './FieldView';
+import { FontIconBox } from './FontIconBox/FontIconBox';
import { FormattedTextBox } from './formattedText/FormattedTextBox';
import { FunctionPlotBox } from './FunctionPlotBox';
import { ImageBox } from './ImageBox';
+import { ImportElementBox } from './importBox/ImportElementBox';
import { KeyValueBox } from './KeyValueBox';
import { LabelBox } from './LabelBox';
import { LinkAnchorBox } from './LinkAnchorBox';
@@ -35,6 +37,7 @@ import { LinkBox } from './LinkBox';
import { LoadingBox } from './LoadingBox';
import { MapBox } from './MapBox/MapBox';
import { PDFBox } from './PDFBox';
+import { PhysicsSimulationBox } from './PhysicsBox/PhysicsSimulationBox';
import { RecordingBox } from './RecordingBox';
import { ScreenshotBox } from './ScreenshotBox';
import { ScriptingBox } from './ScriptingBox';
@@ -266,7 +269,9 @@ export class DocumentContentsView extends React.Component<
HTMLtag,
ComparisonBox,
LoadingBox,
+ PhysicsSimulationBox,
SchemaRowBox,
+ ImportElementBox,
MapPushpinBox,
}}
bindings={bindings}
diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx
index d5ca30957..7723a088d 100644
--- a/src/client/views/nodes/DocumentLinksButton.tsx
+++ b/src/client/views/nodes/DocumentLinksButton.tsx
@@ -9,7 +9,7 @@ import { DocUtils } from '../../documents/Documents';
import { DragManager } from '../../util/DragManager';
import { Hypothesis } from '../../util/HypothesisUtils';
import { LinkManager } from '../../util/LinkManager';
-import { undoBatch, UndoManager } from '../../util/UndoManager';
+import { undoable, undoBatch, UndoManager } from '../../util/UndoManager';
import './DocumentLinksButton.scss';
import { DocumentView } from './DocumentView';
import { LinkDescriptionPopup } from './LinkDescriptionPopup';
@@ -71,14 +71,15 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
e,
this.onLinkButtonMoved,
emptyFunction,
- action((e, doubleTap) => doubleTap && DocumentView.showBackLinks(this.props.View.rootDoc)),
+ action((e, doubleTap) => {
+ doubleTap && DocumentView.showBackLinks(this.props.View.rootDoc);
+ }),
undefined,
undefined,
action(() => (DocumentLinksButton.LinkEditorDocView = this.props.View))
);
};
- @undoBatch
onLinkButtonDown = (e: React.PointerEvent): void => {
setupMoveUpEvents(
this,
@@ -123,17 +124,14 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
e,
returnFalse,
emptyFunction,
- undoBatch(
- action(e => {
- DocumentLinksButton.finishLinkClick(e.clientX, e.clientY, DocumentLinksButton.StartLink, this.props.View.props.Document, true, this.props.View);
- })
- )
+ action(e => DocumentLinksButton.finishLinkClick(e.clientX, e.clientY, DocumentLinksButton.StartLink, this.props.View.props.Document, true, this.props.View))
);
};
- public static finishLinkClick = undoBatch(
- action((screenX: number, screenY: number, startLink: Doc, endLink: Doc, startIsAnnotation: boolean, endLinkView?: DocumentView, pinProps?: PinProps) => {
- if (startLink === endLink) {
+ @undoBatch
+ public static finishLinkClick(screenX: number, screenY: number, startLink: Doc | undefined, endLink: Doc, startIsAnnotation: boolean, endLinkView?: DocumentView, pinProps?: PinProps) {
+ runInAction(() => {
+ if (startLink === endLink || !startLink) {
DocumentLinksButton.StartLink = undefined;
DocumentLinksButton.StartLinkView = undefined;
DocumentLinksButton.AnnotationId = undefined;
@@ -185,8 +183,8 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
);
}
}
- })
- );
+ });
+ }
@action clearLinks() {
DocumentLinksButton.StartLink = undefined;
@@ -195,7 +193,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
@computed get filteredLinks() {
const results = [] as Doc[];
- const filters = this.props.View.props.docFilters();
+ const filters = this.props.View.props.childFilters();
Array.from(new Set<Doc>(this.props.View.allLinks)).forEach(link => {
if (DocUtils.FilterDocs([link], filters, []).length || DocUtils.FilterDocs([link.link_anchor_2 as Doc], filters, []).length || DocUtils.FilterDocs([link.link_anchor_1 as Doc], filters, []).length) {
results.push(link);
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index 1265651ad..b25540dd3 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -24,7 +24,7 @@
width: 100%;
height: 100%;
border-radius: inherit;
- transition: outline 0.3s linear;
+ // transition: outline 0.3s linear;
// background: $white; //overflow: hidden;
transform-origin: center;
@@ -115,7 +115,10 @@
width: 100%;
height: 100%;
transition: inherit;
-
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
.sharingIndicator {
height: 30px;
width: 30px;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index d8494e58e..bb9f45bdd 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -3,10 +3,12 @@ import { action, computed, IReactionDisposer, observable, reaction, runInAction
import { observer } from 'mobx-react';
import { computedFn } from 'mobx-utils';
import { Bounce, Fade, Flip, LightSpeed, Roll, Rotate, Zoom } from 'react-reveal';
-import { AclPrivate, AnimationSym, DataSym, Doc, DocListCast, Field, Opt, StrListCast, WidthSym } from '../../../fields/Doc';
+import { Doc, DocListCast, Field, Opt, StrListCast } from '../../../fields/Doc';
+import { AclPrivate, Animation, DocData, Width } from '../../../fields/DocSymbols';
import { Id } from '../../../fields/FieldSymbols';
import { InkTool } from '../../../fields/InkField';
import { List } from '../../../fields/List';
+import { RefField } from '../../../fields/RefField';
import { listSpec } from '../../../fields/Schema';
import { ScriptField } from '../../../fields/ScriptField';
import { BoolCast, Cast, DocCast, ImageCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
@@ -35,8 +37,10 @@ import { ContextMenuProps } from '../ContextMenuItem';
import { DocComponent } from '../DocComponent';
import { EditableView } from '../EditableView';
import { GestureOverlay } from '../GestureOverlay';
+import { InkingStroke } from '../InkingStroke';
import { LightboxView } from '../LightboxView';
import { StyleProp } from '../StyleProvider';
+import { UndoStack } from '../UndoStack';
import { CollectionFreeFormDocumentView } from './CollectionFreeFormDocumentView';
import { DocumentContentsView, ObserverJsxParser } from './DocumentContentsView';
import { DocumentLinksButton } from './DocumentLinksButton';
@@ -47,8 +51,7 @@ import { LinkAnchorBox } from './LinkAnchorBox';
import { PresEffect, PresEffectDirection } from './trails';
import { PinProps, PresBox } from './trails/PresBox';
import React = require('react');
-import { InkingStroke } from '../InkingStroke';
-import { RefField } from '../../../fields/RefField';
+import { SettingsManager } from '../../util/SettingsManager';
const { Howl } = require('howler');
interface Window {
@@ -66,9 +69,7 @@ export enum OpenWhere {
addLeft = 'add:left',
addRight = 'add:right',
addBottom = 'add:bottom',
- dashboard = 'dashboard',
close = 'close',
- fullScreen = 'fullScreen',
toggle = 'toggle',
toggleRight = 'toggle:right',
replace = 'replace',
@@ -115,6 +116,7 @@ export interface DocComponentView {
brushView?: (view: { width: number; height: number; panX: number; panY: number }) => void;
getView?: (doc: Doc) => Promise<Opt<DocumentView>>; // returns a nested DocumentView for the specified doc or undefined
addDocTab?: (doc: Doc, where: OpenWhere) => boolean; // determines how to add a document - used in following links to open the target ina local lightbox
+ addDocument?: (doc: Doc | Doc[], annotationKey?: string) => boolean; // add a document (used only by collections)
reverseNativeScaling?: () => boolean; // DocumentView's setup screenToLocal based on the doc having a nativeWidth/Height. However, some content views (e.g., FreeFormView w/ fitContentsToBox set) may ignore the native dimensions so this flags the DocumentView to not do Nativre scaling.
shrinkWrap?: () => void; // requests a document to display all of its contents with no white space. currently only implemented (needed?) for freeform views
select?: (ctrlKey: boolean, shiftKey: boolean) => void;
@@ -157,16 +159,18 @@ export interface DocumentViewSharedProps {
CollectionFreeFormDocumentView?: () => CollectionFreeFormDocumentView;
PanelWidth: () => number;
PanelHeight: () => number;
+ shouldNotScale?: () => boolean;
docViewPath: () => DocumentView[];
childHideDecorationTitle?: () => boolean;
childHideResizeHandles?: () => boolean;
+ childDragAction?: dropActionType; // allows child documents to be dragged out of collection without holding the embedKey or dragging the doc decorations title bar.
dataTransition?: string; // specifies animation transition - used by collectionPile and potentially other layout engines when changing the size of documents so that the change won't be abrupt
styleProvider: Opt<StyleProviderFunc>;
setTitleFocus?: () => void;
focus: DocFocusFunc;
layout_fitWidth?: (doc: Doc) => boolean | undefined;
- docFilters: () => string[];
- docRangeFilters: () => string[];
+ childFilters: () => string[];
+ childFiltersByRanges: () => string[];
searchFilterDocs: () => Doc[];
layout_showTitle?: () => string;
whenChildContentsActiveChanged: (isActive: boolean) => void;
@@ -179,7 +183,7 @@ export interface DocumentViewSharedProps {
pinToPres: (document: Doc, pinProps: PinProps) => void;
ScreenToLocalTransform: () => Transform;
bringToFront: (doc: Doc, sendToBack?: boolean) => void;
- canEmbedOnDrag?: boolean;
+ dragAction?: dropActionType;
treeViewDoc?: Doc;
xPadding?: number;
yPadding?: number;
@@ -189,9 +193,8 @@ export interface DocumentViewSharedProps {
hideCaptions?: boolean;
ignoreAutoHeight?: boolean;
forceAutoHeight?: boolean;
- disableDocBrushing?: boolean; // should highlighting for this view be disabled when same document in another view is hovered over.
+ disableBrushing?: boolean; // should highlighting for this view be disabled when same document in another view is hovered over.
onClickScriptDisable?: 'never' | 'always'; // undefined = only when selected
- enableDragWhenActive?: boolean;
waitForDoubleClickToClick?: () => 'never' | 'always' | undefined;
defaultDoubleClick?: () => 'default' | 'ignore' | undefined;
pointerEvents?: () => Opt<string>;
@@ -276,7 +279,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
return Doc.LayoutFieldKey(this.layoutDoc);
}
@computed get layout_showTitle() {
- return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.layout_showTitle) as Opt<string>;
+ return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.ShowTitle) as Opt<string>;
}
@computed get NativeDimScaling() {
return this.props.NativeDimScaling?.() || 1;
@@ -306,7 +309,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
return this.props?.styleProvider?.(this.layoutDoc, this.props, StyleProp.HeaderMargin) || 0;
}
@computed get layout_showCaption() {
- return this.props?.styleProvider?.(this.layoutDoc, this.props, StyleProp.showCaption) || 0;
+ return this.props?.styleProvider?.(this.layoutDoc, this.props, StyleProp.ShowCaption) || 0;
}
@computed get titleHeight() {
return this.props?.styleProvider?.(this.layoutDoc, this.props, StyleProp.TitleHeight) || 0;
@@ -329,8 +332,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
return (
DocumentView.LongPress ||
onScriptDisable === 'always' ||
- (onScriptDisable !== 'never' && (this.rootSelected() || this.props.isSelected())) ||
- this._componentView?.isAnyChildContentActive?.()
+ (onScriptDisable !== 'never' && (this.rootSelected() || this._componentView?.isAnyChildContentActive?.()))
);
}
@computed get onClickHandler() {
@@ -352,11 +354,17 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
componentDidMount() {
this.setupHandlers();
}
-
+ preDropFunc = (e: Event, de: DragManager.DropEvent) => {
+ const dropAction = this.layoutDoc.dropAction as dropActionType;
+ if (de.complete.docDragData && this.isContentActive() && !this.props.treeViewDoc) {
+ dropAction && (de.complete.docDragData.dropAction = dropAction);
+ e.stopPropagation();
+ }
+ };
setupHandlers() {
this.cleanupHandlers(false);
if (this._mainCont.current) {
- this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, this.drop.bind(this), this.props.Document);
+ this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, this.drop.bind(this), this.props.Document, this.preDropFunc);
this._multiTouchDisposer = InteractionUtils.MakeMultiTouchTarget(this._mainCont.current, this.onTouchStart.bind(this));
this._holdDisposer = InteractionUtils.MakeHoldTouchTarget(this._mainCont.current, this.handle1PointerHoldStart.bind(this));
}
@@ -384,7 +392,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
dragData.treeViewDoc = this.props.treeViewDoc;
dragData.removeDocument = this.props.removeDocument;
dragData.moveDocument = this.props.moveDocument;
- dragData.canEmbed = this.props.canEmbedOnDrag;
+ dragData.canEmbed = this.rootDoc.dragAction ?? this.props.dragAction ? true : false;
const ffview = this.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView;
ffview && runInAction(() => (ffview.ChildDrag = this.props.DocumentView()));
DragManager.StartDocumentDrag(
@@ -402,7 +410,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
defaultRestoreTargetView = (docView: DocumentView, anchor: Doc, focusSpeed: number, options: DocFocusOptions) => {
const targetMatch =
Doc.AreProtosEqual(anchor, this.rootDoc) || // anchor is this document, so anchor's properties apply to this document
- (DocCast(anchor)?.unrendered && Doc.AreProtosEqual(DocCast(anchor.annotationOn), this.rootDoc)) // the anchor is an unrendered annotation on this document, so anchor properties apply to this document
+ (DocCast(anchor)?.layout_unrendered && Doc.AreProtosEqual(DocCast(anchor.annotationOn), this.rootDoc)) // the anchor is an layout_unrendered annotation on this document, so anchor properties apply to this document
? true
: false;
return targetMatch && PresBox.restoreTargetDocView(docView, anchor, focusSpeed) ? focusSpeed : undefined;
@@ -456,27 +464,26 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
// instead of in the global lightbox
const oldFunc = DocumentViewInternal.addDocTabFunc;
DocumentViewInternal.addDocTabFunc = this.props.addDocTab;
- const res =
- this.onClickHandler?.script.run(
- {
- this: this.layoutDoc,
- self: this.rootDoc,
- _readOnly_: false,
- scriptContext: this.props.scriptContext,
- documentView: this.props.DocumentView(),
- clientX,
- clientY,
- shiftKey,
- altKey,
- metaKey,
- },
- console.log
- ).result?.select === true
- ? this.props.select(false)
- : '';
+ this.onClickHandler?.script.run(
+ {
+ this: this.layoutDoc,
+ self: this.rootDoc,
+ _readOnly_: false,
+ scriptContext: this.props.scriptContext,
+ documentView: this.props.DocumentView(),
+ clientX,
+ clientY,
+ shiftKey,
+ altKey,
+ metaKey,
+ },
+ console.log
+ ).result?.select === true
+ ? this.props.select(false)
+ : '';
DocumentViewInternal.addDocTabFunc = oldFunc;
};
- clickFunc = () => (this.props.Document.dontUndo ? func() : UndoManager.RunInBatch(func, 'on click'));
+ clickFunc = () => UndoManager.RunInBatch(func, 'click ' + this.rootDoc.title);
} else {
// onDragStart implies a button doc that we don't want to select when clicking. RootDocument & isTemplateForField implies we're clicking on part of a template instance and we want to select the whole template, not the part
if ((this.layoutDoc.onDragStart || this.props.Document.rootDocument) && !(e.ctrlKey || e.button > 0)) {
@@ -484,13 +491,18 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
}
preventDefault = false;
}
-
- this._singleClickFunc = clickFunc ?? (() => this._componentView?.select?.(e.ctrlKey || e.metaKey, e.shiftKey) ?? this.props.select(e.ctrlKey || e.metaKey || e.shiftKey));
+ const sendToBack = e.altKey;
+ this._singleClickFunc =
+ clickFunc ??
+ (() =>
+ sendToBack
+ ? this.props.DocumentView().props.CollectionFreeFormDocumentView?.().props.bringToFront(this.rootDoc, true)
+ : this._componentView?.select?.(e.ctrlKey || e.metaKey, e.shiftKey) ?? this.props.select(e.ctrlKey || e.metaKey || e.shiftKey));
const waitFordblclick = this.props.waitForDoubleClickToClick?.() ?? this.Document.waitForDoubleClickToClick;
if ((clickFunc && waitFordblclick !== 'never') || waitFordblclick === 'always') {
this._doubleClickTimeout && clearTimeout(this._doubleClickTimeout);
this._doubleClickTimeout = setTimeout(this._singleClickFunc, 300);
- } else {
+ } else if (!DocumentView.LongPress) {
this._singleClickFunc();
this._singleClickFunc = undefined;
}
@@ -502,7 +514,15 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
@action
onPointerDown = (e: React.PointerEvent): void => {
- this._longPressSelector = setTimeout(() => DocumentView.LongPress && this.props.select(false), 1000);
+ this._longPressSelector = setTimeout(() => {
+ if (DocumentView.LongPress) {
+ if (this.rootDoc.undoIgnoreFields) {
+ runInAction(() => (UndoStack.HideInline = !UndoStack.HideInline));
+ } else {
+ this.props.select(false);
+ }
+ }
+ }, 1000);
if (!GestureOverlay.DownDocView) GestureOverlay.DownDocView = this.props.DocumentView();
this._downX = e.clientX;
@@ -513,19 +533,19 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
// if this is part of a template, let the event go up to the template root unless right/ctrl clicking
if (
// prettier-ignore
- this.props.isDocumentActive?.() &&
+ (this.props.isDocumentActive?.() || this.props.isContentActive?.()) &&
!this.props.onBrowseClick?.() &&
!this.Document.ignoreClick &&
e.button === 0 &&
this.pointerEvents !== 'none' &&
- !DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc)
+ !Doc.IsInMyOverlay(this.layoutDoc)
) {
e.stopPropagation();
// don't preventDefault anymore. Goldenlayout, PDF text selection and RTF text selection all need it to go though
- //if (this.props.isSelected(true) && this.rootDoc.type !== DocumentType.PDF && this.layoutDoc._viewType !== CollectionViewType.Docking) e.preventDefault();
+ //if (this.props.isSelected(true) && this.rootDoc.type !== DocumentType.PDF && this.layoutDoc._type_collection !== CollectionViewType.Docking) e.preventDefault();
// listen to move events if document content isn't active or document is draggable
- if (!this.layoutDoc._lockedPosition && (!this.isContentActive() || this.props.enableDragWhenActive || this.rootDoc.enableDragWhenActive)) {
+ if (!this.layoutDoc._lockedPosition && (!this.isContentActive() || BoolCast(this.rootDoc._dragWhenActive))) {
document.addEventListener('pointermove', this.onPointerMove);
}
}
@@ -539,7 +559,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
if (!Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, Date.now())) {
this.cleanupPointerEvents();
- this.startDragging(this._downX, this._downY, ((e.ctrlKey || e.altKey) && 'embed') || ((this.Document.dropAction || this.props.dropAction || undefined) as dropActionType));
+ this._longPressSelector && clearTimeout(this._longPressSelector);
+ this.startDragging(this._downX, this._downY, ((e.ctrlKey || e.altKey) && 'embed') || ((this.Document.dragAction || this.props.dragAction || undefined) as dropActionType));
}
};
@@ -557,8 +578,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
if (this.onPointerUpHandler?.script) {
this.onPointerUpHandler.script.run({ self: this.rootDoc, this: this.layoutDoc }, console.log);
} else if (e.button === 0 && Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, this._downTime)) {
- this._doubleTap = Date.now() - this._lastTap < Utils.CLICK_TIME;
- if (!this.props.isSelected(true)) this._lastTap = Date.now(); // don't want to process the start of a double tap if the doucment is selected
+ this._doubleTap = (this.onDoubleClickHandler?.script || this.rootDoc.defaultDoubleClick !== 'ignore') && Date.now() - this._lastTap < Utils.CLICK_TIME;
+ if (!this.isContentActive()) this._lastTap = Date.now(); // don't want to process the start of a double tap if the doucment is selected
}
if (DocumentView.LongPress) e.preventDefault();
};
@@ -598,16 +619,17 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
@undoBatch
@action
drop = (e: Event, de: DragManager.DropEvent) => {
- if (this.props.dontRegisterView || this.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) return;
+ if (this.props.dontRegisterView || this.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) return false;
if (this.props.Document === Doc.ActiveDashboard) {
+ e.stopPropagation();
+ e.preventDefault();
alert((e.target as any)?.closest?.('*.lm_content') ? "You can't perform this move most likely because you don't have permission to modify the destination." : 'Linking to document tabs not yet supported. Drop link on document content.');
- return;
+ return true;
}
const linkdrag = de.complete.annoDragData ?? de.complete.linkDragData;
if (linkdrag) {
linkdrag.linkSourceDoc = linkdrag.linkSourceGetAnchor();
if (linkdrag.linkSourceDoc) {
- e.stopPropagation();
if (de.complete.annoDragData && !de.complete.annoDragData.dropDocument) {
de.complete.annoDragData.dropDocument = de.complete.annoDragData.dropDocCreator(undefined);
}
@@ -615,8 +637,11 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
const dropDoc = de.complete.annoDragData?.dropDocument ?? this._componentView?.getAnchor?.(true) ?? this.rootDoc;
de.complete.linkDocument = DocUtils.MakeLink(linkdrag.linkSourceDoc, dropDoc, {}, undefined, [de.x, de.y - 50]);
}
+ e.stopPropagation();
+ return true;
}
}
+ return false;
};
@undoBatch
@@ -654,7 +679,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
@action
onContextMenu = (e?: React.MouseEvent, pageX?: number, pageY?: number) => {
- if (e && this.rootDoc._hideContextMenu && Doc.noviceMode) {
+ if (e && this.rootDoc._layout_hideContextMenu && Doc.noviceMode) {
e.preventDefault();
e.stopPropagation();
//!this.props.isSelected(true) && SelectionManager.SelectView(this.props.DocumentView(), false);
@@ -702,10 +727,14 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
const templateDoc = Cast(this.props.Document[StrCast(this.props.Document.layout_fieldKey)], Doc, null);
const appearance = cm.findByDescription('UI Controls...');
const appearanceItems: ContextMenuProps[] = appearance && 'subitems' in appearance ? appearance.subitems : [];
+
+ if (this.props.renderDepth === 0) {
+ appearanceItems.push({ description: 'Open in Lightbox', event: () => LightboxView.SetLightboxDoc(this.rootDoc), icon: 'hand-point-right' });
+ }
!Doc.noviceMode && templateDoc && appearanceItems.push({ description: 'Open Template ', event: () => this.props.addDocTab(templateDoc, OpenWhere.addRight), icon: 'eye' });
!appearance && appearanceItems.length && cm.addItem({ description: 'UI Controls...', subitems: appearanceItems, icon: 'compass' });
- if (!Doc.IsSystem(this.rootDoc) && this.rootDoc.type !== DocumentType.PRES && ![CollectionViewType.Docking, CollectionViewType.Tree].includes(this.rootDoc._viewType as any)) {
+ if (!Doc.IsSystem(this.rootDoc) && this.rootDoc.type !== DocumentType.PRES && ![CollectionViewType.Docking, CollectionViewType.Tree].includes(this.rootDoc._type_collection as any)) {
const existingOnClick = cm.findByDescription('OnClick...');
const onClicks: ContextMenuProps[] = existingOnClick && 'subitems' in existingOnClick ? existingOnClick.subitems : [];
@@ -725,18 +754,20 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
onClicks.push({ description: 'Enter Portal', event: this.makeIntoPortal, icon: 'window-restore' });
!Doc.noviceMode && onClicks.push({ description: 'Toggle Detail', event: this.setToggleDetail, icon: 'concierge-bell' });
- if (!this.Document.annotationOn) {
- const options = cm.findByDescription('Options...');
- const optionItems: ContextMenuProps[] = options && 'subitems' in options ? options.subitems : [];
- !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'compass' });
-
- onClicks.push({ description: this.onClickHandler ? 'Remove Click Behavior' : 'Follow Link', event: () => this.toggleFollowLink(false, false), icon: 'link' });
- onClicks.push({ description: 'Edit onClick Script', event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.props.Document, undefined, 'onClick'), 'edit onClick'), icon: 'terminal' });
- !existingOnClick && cm.addItem({ description: 'OnClick...', noexpand: true, subitems: onClicks, icon: 'mouse-pointer' });
- } else if (LinkManager.Links(this.Document).length) {
- onClicks.push({ description: 'Select on Click', event: () => this.noOnClick(), icon: 'link' });
- onClicks.push({ description: 'Follow Link on Click', event: () => this.followLinkOnClick(), icon: 'link' });
- !existingOnClick && cm.addItem({ description: 'OnClick...', subitems: onClicks, icon: 'mouse-pointer' });
+ if (!this.props.treeViewDoc) {
+ if (!this.Document.annotationOn) {
+ const options = cm.findByDescription('Options...');
+ const optionItems: ContextMenuProps[] = options && 'subitems' in options ? options.subitems : [];
+ !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'compass' });
+
+ onClicks.push({ description: this.onClickHandler ? 'Remove Click Behavior' : 'Follow Link', event: () => this.toggleFollowLink(false, false), icon: 'link' });
+ onClicks.push({ description: 'Edit onClick Script', event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.props.Document, undefined, 'onClick'), 'edit onClick'), icon: 'terminal' });
+ !existingOnClick && cm.addItem({ description: 'OnClick...', noexpand: true, subitems: onClicks, icon: 'mouse-pointer' });
+ } else if (LinkManager.Links(this.Document).length) {
+ onClicks.push({ description: 'Select on Click', event: () => this.noOnClick(), icon: 'link' });
+ onClicks.push({ description: 'Follow Link on Click', event: () => this.followLinkOnClick(), icon: 'link' });
+ !existingOnClick && cm.addItem({ description: 'OnClick...', subitems: onClicks, icon: 'mouse-pointer' });
+ }
}
}
@@ -767,23 +798,22 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
!more && moreItems.length && cm.addItem({ description: 'More...', subitems: moreItems, icon: 'compass' });
}
const constantItems: ContextMenuProps[] = [];
- constantItems.push({ description: 'Show Metadata', event: () => this.props.addDocTab(this.props.Document, (OpenWhere.addRight.toString() + 'KeyValue') as OpenWhere), icon: 'layer-group' });
if (!Doc.IsSystem(this.rootDoc)) {
- constantItems.push({ description: 'Export as Zip file', icon: 'download', event: async () => Doc.Zip(this.props.Document) });
- constantItems.push({ description: 'Import Zipped file', icon: 'upload', event: ({ x, y }) => this.importDocument() });
- (this.rootDoc._viewType !== CollectionViewType.Docking || !Doc.noviceMode) && constantItems.push({ description: 'Share', event: () => SharingManager.Instance.open(this.props.DocumentView()), icon: 'users' });
+ constantItems.push({ description: 'Zip Export', icon: 'download', event: async () => Doc.Zip(this.props.Document) });
+ (this.rootDoc._type_collection !== CollectionViewType.Docking || !Doc.noviceMode) && constantItems.push({ description: 'Share', event: () => SharingManager.Instance.open(this.props.DocumentView()), icon: 'users' });
if (this.props.removeDocument && Doc.ActiveDashboard !== this.props.Document) {
// need option to gray out menu items ... preferably with a '?' that explains why they're grayed out (eg., no permissions)
constantItems.push({ description: 'Close', event: this.deleteClicked, icon: 'times' });
}
}
- cm.addItem({ description: 'General...', noexpand: !Doc.IsSystem(this.rootDoc), subitems: constantItems, icon: 'question' });
+ constantItems.push({ description: 'Show Metadata', event: () => this.props.addDocTab(this.props.Document, (OpenWhere.addRight.toString() + 'KeyValue') as OpenWhere), icon: 'layer-group' });
+ cm.addItem({ description: 'General...', noexpand: false, subitems: constantItems, icon: 'question' });
const help = cm.findByDescription('Help...');
const helpItems: ContextMenuProps[] = help && 'subitems' in help ? help.subitems : [];
!Doc.noviceMode && helpItems.push({ description: 'Text Shortcuts Ctrl+/', event: () => this.props.addDocTab(Docs.Create.PdfDocument('/assets/cheat-sheet.pdf', { _width: 300, _height: 300 }), OpenWhere.addRight), icon: 'keyboard' });
!Doc.noviceMode && helpItems.push({ description: 'Print Document in Console', event: () => console.log(this.props.Document), icon: 'hand-point-right' });
- !Doc.noviceMode && helpItems.push({ description: 'Print DataDoc in Console', event: () => console.log(this.props.Document[DataSym]), icon: 'hand-point-right' });
+ !Doc.noviceMode && helpItems.push({ description: 'Print DataDoc in Console', event: () => console.log(this.props.Document[DocData]), icon: 'hand-point-right' });
let documentationDescription: string | undefined = undefined;
let documentationLink: string | undefined = undefined;
@@ -818,7 +848,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
break;
}
// Add link to help documentation
- if (documentationDescription && documentationLink) {
+ if (!this.props.treeViewDoc && documentationDescription && documentationLink) {
helpItems.push({
description: documentationDescription,
event: () => window.open(documentationLink, '_blank'),
@@ -832,44 +862,50 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
cm.displayMenu((e?.pageX || pageX || 0) - 15, (e?.pageY || pageY || 0) - 15);
};
- rootSelected = (outsideReaction?: boolean) => this.props.isSelected(outsideReaction) || (this.props.Document.rootDocument && this.props.rootSelected?.(outsideReaction)) || false;
+ @computed get _rootSelected() {
+ return this.props.isSelected(false) || (this.props.Document.rootDocument && this.props.rootSelected?.(false)) || false;
+ }
+ rootSelected = (outsideReaction?: boolean) => this._rootSelected;
panelHeight = () => this.props.PanelHeight() - this.headerMargin;
screenToLocal = () => this.props.ScreenToLocalTransform().translate(0, -this.headerMargin);
onClickFunc: any = () => (this.disableClickScriptFunc ? undefined : this.onClickHandler);
setHeight = (height: number) => (this.layoutDoc._height = height);
setContentView = action((view: { getAnchor?: (addAsAnnotation: boolean) => Doc; forward?: () => boolean; back?: () => boolean }) => (this._componentView = view));
- isContentActive = (outsideReaction?: boolean): boolean | undefined => {
- return this.props.isContentActive() === false
+ @computed get _isContentActive() {
+ // true - if the document has been activated directly or indirectly (by having its children selected)
+ // false - if its pointer events are explicitly turned off or if it's container tells it that it's inactive
+ // undefined - it is not active, but it should be responsive to actions that might active it or its contents (eg clicking)
+ return this.props.isContentActive() === false || this.props.pointerEvents?.() === 'none'
? false
- : Doc.ActiveTool !== InkTool.None ||
- SnappingManager.GetIsDragging() ||
- this.rootSelected() ||
- this.rootDoc.forceActive ||
- this.props.isSelected(outsideReaction) ||
- this._componentView?.isAnyChildContentActive?.() ||
- this.props.isContentActive()
+ : Doc.ActiveTool !== InkTool.None || SnappingManager.GetIsDragging() || this.rootSelected() || this.rootDoc.forceActive || this._componentView?.isAnyChildContentActive?.() || this.props.isContentActive()
? true
: undefined;
- };
+ }
+ isContentActive = (): boolean | undefined => this._isContentActive;
@observable _retryThumb = 1;
- thumbShown = () => {
- const childHighlighted = () =>
- Array.from(Doc.highlightedDocs.keys())
- .concat(Array.from(Doc.brushManager.BrushedDoc.keys()))
- .some(doc => Doc.AreProtosEqual(DocCast(doc.annotationOn), this.rootDoc));
+ @computed get _thumbShown() {
+ const childHighlighted = () => false;
+ // Array.from(Doc.highlightedDocs.keys())
+ // .concat(Array.from(Doc.brushManager.BrushedDoc.keys()))
+ // .some(doc => Doc.AreProtosEqual(DocCast(doc.annotationOn), this.rootDoc));
const childOverlayed = () => Array.from(DocumentManager._overlayViews).some(view => Doc.AreProtosEqual(view.rootDoc, this.rootDoc));
return !this.props.LayoutTemplateString &&
- !this.props.isSelected() &&
+ !this.isContentActive() &&
LightboxView.LightboxDoc !== this.rootDoc &&
this.thumb &&
!Doc.AreProtosEqual(DocumentLinksButton.StartLink, this.rootDoc) &&
- ((!childHighlighted() && !childOverlayed() && !Doc.isBrushedHighlightedDegree(this.rootDoc)) || this.rootDoc._viewType === CollectionViewType.Docking) &&
- !this._componentView?.isAnyChildContentActive?.()
+ ((!childHighlighted() && !childOverlayed() && !Doc.isBrushedHighlightedDegree(this.rootDoc)) || this.rootDoc._type_collection === CollectionViewType.Docking)
? true
: false;
- };
- docFilters = () => [...this.props.docFilters(), ...StrListCast(this.layoutDoc.docFilters)];
- contentPointerEvents = () => (!this.disableClickScriptFunc && this.onClickHandler ? 'none' : this.pointerEvents);
+ }
+ thumbShown = () => this._thumbShown;
+ childFilters = () => [...this.props.childFilters(), ...StrListCast(this.layoutDoc.childFilters)];
+
+ /// disable pointer events on content when there's an enabled onClick script (but not the browse script) and the contents aren't forced active, or if contents are marked inactive
+ @computed get _contentPointerEvents() {
+ return (!this.disableClickScriptFunc && this.onClickHandler && !this.props.onBrowseClick?.() && this.isContentActive() !== true) || this.isContentActive() === false ? 'none' : this.pointerEvents;
+ }
+ contentPointerEvents = () => this._contentPointerEvents;
@computed get contents() {
TraceMobx();
const isInk = StrCast(this.layoutDoc.layout).includes(InkingStroke.name) && !this.props.LayoutTemplateString;
@@ -877,7 +913,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
<div
className="documentView-contentsView"
style={{
- pointerEvents: (isInk ? 'none' : this.pointerEvents) ?? 'all',
+ pointerEvents: (isInk ? 'none' : this.contentPointerEvents()) ?? 'all',
height: this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined,
}}>
{!this._retryThumb || !this.thumbShown() ? null : (
@@ -900,7 +936,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
docViewPath={this.props.viewPath}
thumbShown={this.thumbShown}
setContentView={this.setContentView}
- docFilters={this.docFilters}
+ childFilters={this.childFilters}
NativeDimScaling={this.props.NativeDimScaling}
PanelHeight={this.panelHeight}
setHeight={!this.props.suppressSetHeight ? this.setHeight : undefined}
@@ -912,7 +948,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
setTitleFocus={this.setTitleFocus}
layout_fieldKey={this.finalLayoutKey}
/>
- {this.layoutDoc.hideAllLinks ? null : this.allLinkEndpoints}
+ {this.layoutDoc.layout_hideAllLinks ? null : this.allLinkEndpoints}
</div>
);
}
@@ -922,14 +958,14 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
anchorStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string): any => {
// prettier-ignore
switch (property.split(':')[0]) {
- case StyleProp.layout_showTitle: return '';
+ case StyleProp.ShowTitle: return '';
case StyleProp.PointerEvents: return 'none';
case StyleProp.Highlighting: return undefined;
}
return this.props.styleProvider?.(doc, props, property);
};
// We need to use allrelatedLinks to get not just links to the document as a whole, but links to
- // anchors that are not rendered as DocumentViews (marked as 'unrendered' with their 'annotationOn' set to this document). e.g.,
+ // anchors that are not rendered as DocumentViews (marked as 'layout_unrendered' with their 'annotationOn' set to this document). e.g.,
// - PDF text regions are rendered as an Annotations without generating a DocumentView, '
// - RTF selections are rendered via Prosemirror and have a mark which contains the Document ID for the annotation link
// - and links to PDF/Web docs at a certain scroll location never create an explicit view.
@@ -940,20 +976,20 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
link =>
Doc.AreProtosEqual(link.link_anchor_1 as Doc, this.rootDoc) ||
Doc.AreProtosEqual(link.link_anchor_2 as Doc, this.rootDoc) ||
- ((link.link_anchor_1 as Doc)?.unrendered && Doc.AreProtosEqual((link.link_anchor_1 as Doc)?.annotationOn as Doc, this.rootDoc)) ||
- ((link.link_anchor_2 as Doc)?.unrendered && Doc.AreProtosEqual((link.link_anchor_2 as Doc)?.annotationOn as Doc, this.rootDoc))
+ ((link.link_anchor_1 as Doc)?.layout_unrendered && Doc.AreProtosEqual((link.link_anchor_1 as Doc)?.annotationOn as Doc, this.rootDoc)) ||
+ ((link.link_anchor_2 as Doc)?.layout_unrendered && Doc.AreProtosEqual((link.link_anchor_2 as Doc)?.annotationOn as Doc, this.rootDoc))
);
}
@computed get allLinks() {
TraceMobx();
return LinkManager.Instance.getAllRelatedLinks(this.rootDoc);
}
- hideLink = computedFn((link: Doc) => () => (link.layout_linkDisplay = false));
+ hideLink = computedFn((link: Doc) => () => (link.link_displayLine = false));
@computed get allLinkEndpoints() {
// the small blue dots that mark the endpoints of links
TraceMobx();
- if (this.props.hideLinkAnchors || this.layoutDoc.layout_hideLinkAnchors || this.props.dontRegisterView || this.layoutDoc.unrendered) return null;
- const filtered = DocUtils.FilterDocs(this.directLinks, this.props.docFilters?.() ?? [], []).filter(d => d.layout_linkDisplay);
+ if (this.props.hideLinkAnchors || this.layoutDoc.layout_hideLinkAnchors || this.props.dontRegisterView || this.layoutDoc.layout_unrendered) return null;
+ const filtered = DocUtils.FilterDocs(this.directLinks, this.props.childFilters?.() ?? [], []).filter(d => d.link_displayLine);
return filtered.map(link => (
<div className="documentView-anchorCont" key={link[Id]}>
<DocumentView
@@ -1001,7 +1037,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
gumStream = stream;
recorder = new MediaRecorder(stream);
recorder.ondataavailable = async (e: any) => {
- const [{ result }] = await Networking.UploadFilesToServer(e.data);
+ const [{ result }] = await Networking.UploadFilesToServer({ file: e.data });
if (!(result instanceof Error)) {
const audioField = new AudioField(result.accessPaths.agnostic.client);
const audioAnnos = Cast(dataDoc[field + '-audioAnnotations'], listSpec(AudioField), null);
@@ -1046,13 +1082,13 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
@computed get innards() {
TraceMobx();
const ffscale = () => this.props.DocumentView().props.CollectionFreeFormDocumentView?.().props.ScreenToLocalTransform().Scale || 1;
- const layout_showTitle = this.layout_showTitle?.split(':')[0];
- const layout_showTitleHover = this.layout_showTitle?.includes(':hover');
+ const showTitle = this.layout_showTitle?.split(':')[0];
+ const showTitleHover = this.layout_showTitle?.includes(':hover');
const captionView = !this.layout_showCaption ? null : (
<div
className="documentView-captionWrapper"
style={{
- pointerEvents: this.Document.ignoreClick ? 'none' : this.isContentActive() || this.props.isDocumentActive?.() ? 'all' : undefined,
+ pointerEvents: this.rootDoc.ignoreClick ? 'none' : this.isContentActive() || this.props.isDocumentActive?.() ? 'all' : undefined,
minWidth: 50 * ffscale(),
maxHeight: `max(100%, ${20 * ffscale()}px)`,
}}>
@@ -1071,27 +1107,27 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
/>
</div>
);
- const targetDoc = layout_showTitle?.startsWith('_') ? this.layoutDoc : this.rootDoc;
+ const targetDoc = showTitle?.startsWith('_') ? this.layoutDoc : this.rootDoc;
const background = StrCast(
- SharingManager.Instance.users.find(users => users.user.email === this.dataDoc.author)?.sharingDoc.userColor,
- Doc.UserDoc().layout_showTitle && [DocumentType.RTF, DocumentType.COL].includes(this.rootDoc.type as any) ? StrCast(Doc.SharingDoc().userColor) : 'rgba(0,0,0,0.4)'
+ SharingManager.Instance.users.find(u => u.user.email === this.dataDoc.author)?.sharingDoc.headingColor,
+ Doc.UserDoc().layout_showTitle && [DocumentType.RTF, DocumentType.COL].includes(this.rootDoc.type as any) ? StrCast(Doc.SharingDoc().headingColor) : SettingsManager.Instance.userVariantColor
);
- const layout_sidebarWidthPercent = +StrCast(this.layoutDoc.layout_sidebarWidthPercent).replace('%', '');
- const titleView = !layout_showTitle ? null : (
+ const sidebarWidthPercent = +StrCast(this.layoutDoc.layout_sidebarWidthPercent).replace('%', '');
+ const titleView = !showTitle ? null : (
<div
- className={`documentView-titleWrapper${layout_showTitleHover ? '-hover' : ''}`}
+ className={`documentView-titleWrapper${showTitleHover ? '-hover' : ''}`}
key="title"
style={{
position: this.headerMargin ? 'relative' : 'absolute',
height: this.titleHeight,
- width: !this.headerMargin ? `calc(${layout_sidebarWidthPercent || 100}% - 18px)` : (layout_sidebarWidthPercent || 100) + '%', // leave room for annotation button
+ width: !this.headerMargin ? `calc(${sidebarWidthPercent || 100}% - 18px)` : (sidebarWidthPercent || 100) + '%', // leave room for annotation button
color: lightOrDark(background),
background,
pointerEvents: (!this.disableClickScriptFunc && this.onClickHandler) || this.Document.ignoreClick ? 'none' : this.isContentActive() || this.props.isDocumentActive?.() ? 'all' : undefined,
}}>
<EditableView
ref={this._titleRef}
- contents={layout_showTitle
+ contents={showTitle
.split(';')
.map(field => field.trim())
.map(field => targetDoc[field]?.toString())
@@ -1100,27 +1136,27 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
fontSize={10}
GetValue={() => {
this.props.select(false);
- return layout_showTitle.split(';').length === 1 ? layout_showTitle + '=' + Field.toString(targetDoc[layout_showTitle.split(';')[0]] as any as Field) : '#' + layout_showTitle;
+ return showTitle.split(';').length === 1 ? showTitle + '=' + Field.toString(targetDoc[showTitle.split(';')[0]] as any as Field) : '#' + showTitle;
}}
SetValue={undoBatch((input: string) => {
if (input?.startsWith('#')) {
if (this.props.layout_showTitle) {
this.rootDoc._layout_showTitle = input?.substring(1) ? input.substring(1) : undefined;
} else {
- Doc.UserDoc().layout_showTitle = input?.substring(1) ? input.substring(1) : 'creationDate';
+ Doc.UserDoc().layout_showTitle = input?.substring(1) ? input.substring(1) : 'author_date';
}
} else {
- var value = input.replace(new RegExp(layout_showTitle + '='), '') as string | number;
- if (layout_showTitle !== 'title' && Number(value).toString() === value) value = Number(value);
- if (layout_showTitle.includes('Date') || layout_showTitle === 'author') return true;
- Doc.SetInPlace(targetDoc, layout_showTitle, value, true);
+ var value = input.replace(new RegExp(showTitle + '='), '') as string | number;
+ if (showTitle !== 'title' && Number(value).toString() === value) value = Number(value);
+ if (showTitle.includes('Date') || showTitle === 'author') return true;
+ Doc.SetInPlace(targetDoc, showTitle, value, true);
}
return true;
})}
/>
</div>
);
- return this.props.hideTitle || (!layout_showTitle && !this.layout_showCaption) ? (
+ return this.props.hideTitle || (!showTitle && !this.layout_showCaption) ? (
this.contents
) : (
<div className="documentView-styleWrapper">
@@ -1135,7 +1171,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
renderDoc = (style: object) => {
TraceMobx();
- return !DocCast(this.Document) || GetEffectiveAcl(this.Document[DataSym]) === AclPrivate
+ return !DocCast(this.Document) || GetEffectiveAcl(this.Document[DocData]) === AclPrivate
? null
: this.docContents ?? (
<div
@@ -1147,8 +1183,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
opacity: this.opacity,
cursor: Doc.ActiveTool === InkTool.None ? 'grab' : 'crosshair',
color: StrCast(this.layoutDoc.color, 'inherit'),
- fontFamily: StrCast(this.Document._fontFamily, 'inherit'),
- fontSize: Cast(this.Document._fontSize, 'string', null),
+ fontFamily: StrCast(this.Document._text_fontFamily, 'inherit'),
+ fontSize: Cast(this.Document._text_fontSize, 'string', null),
transform: this._animateScalingTo ? `scale(${this._animateScalingTo})` : undefined,
transition: !this._animateScalingTo ? StrCast(this.Document.dataTransition) : `transform ${this.animateScaleTime / 1000}s ease-${this._animateScalingTo < 1 ? 'in' : 'out'}`,
}}>
@@ -1187,16 +1223,22 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
case PresEffect.Lightspeed: return <LightSpeed {...effectProps}>{renderDoc}</LightSpeed>;
}
}
+ @computed get highlighting() {
+ return this.props.styleProvider?.(this.props.Document, this.props, StyleProp.Highlighting);
+ }
+ @computed get borderPath() {
+ return this.props.styleProvider?.(this.props.Document, this.props, StyleProp.BorderPath);
+ }
render() {
TraceMobx();
- const highlighting = this.props.styleProvider?.(this.props.Document, this.props, StyleProp.Highlighting);
- const borderPath = this.props.styleProvider?.(this.props.Document, this.props, StyleProp.BorderPath);
+ const highlighting = this.highlighting;
+ const borderPath = this.borderPath;
const boxShadow =
this.props.treeViewDoc || !highlighting
? this.boxShadow
: highlighting && this.borderRounding && highlighting.highlightStyle !== 'dashed'
? `0 0 0 ${highlighting.highlightIndex}px ${highlighting.highlightColor}`
- : this.boxShadow || (this.props.Document.isTemplateForField ? 'black 0.2vw 0.2vw 0.8vw' : undefined);
+ : this.boxShadow || (this.rootDoc.isTemplateForField ? 'black 0.2vw 0.2vw 0.8vw' : undefined);
const renderDoc = this.renderDoc({
borderRadius: this.borderRounding,
outline: highlighting && !this.borderRounding && !highlighting.highlightStroke ? `${highlighting.highlightColor} ${highlighting.highlightStyle} ${highlighting.highlightIndex}px` : 'solid 0px',
@@ -1212,15 +1254,15 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
onContextMenu={this.onContextMenu}
onPointerDown={this.onPointerDown}
onClick={this.onClick}
- onPointerEnter={e => (!SnappingManager.GetIsDragging() || DragManager.CanEmbed) && Doc.BrushDoc(this.props.Document)}
- onPointerOver={e => (!SnappingManager.GetIsDragging() || DragManager.CanEmbed) && Doc.BrushDoc(this.props.Document)}
- onPointerLeave={e => !isParentOf(this.ContentDiv, document.elementFromPoint(e.nativeEvent.x, e.nativeEvent.y)) && Doc.UnBrushDoc(this.props.Document)}
+ onPointerEnter={e => (!SnappingManager.GetIsDragging() || DragManager.CanEmbed) && Doc.BrushDoc(this.rootDoc)}
+ onPointerOver={e => (!SnappingManager.GetIsDragging() || DragManager.CanEmbed) && Doc.BrushDoc(this.rootDoc)}
+ onPointerLeave={e => !isParentOf(this.ContentDiv, document.elementFromPoint(e.nativeEvent.x, e.nativeEvent.y)) && Doc.UnBrushDoc(this.rootDoc)}
style={{
borderRadius: this.borderRounding,
pointerEvents: this.pointerEvents === 'visiblePainted' ? 'none' : this.pointerEvents,
}}>
<>
- {DocumentViewInternal.AnimationEffect(renderDoc, this.rootDoc[AnimationSym], this.rootDoc)}
+ {DocumentViewInternal.AnimationEffect(renderDoc, this.rootDoc[Animation], this.rootDoc)}
{borderPath?.jsx}
</>
</div>
@@ -1254,8 +1296,8 @@ export class DocumentView extends React.Component<DocumentViewProps> {
public setAnimEffect = (presEffect: Doc, timeInMs: number, afterTrans?: () => void) => {
this.AnimEffectTimer && clearTimeout(this.AnimEffectTimer);
- this.rootDoc[AnimationSym] = presEffect;
- this.AnimEffectTimer = setTimeout(() => (this.rootDoc[AnimationSym] = undefined), timeInMs);
+ this.rootDoc[Animation] = presEffect;
+ this.AnimEffectTimer = setTimeout(() => (this.rootDoc[Animation] = undefined), timeInMs);
};
public setViewTransition = (transProp: string, timeInMs: number, afterTrans?: () => void, dataTrans = false) => {
this.rootDoc._viewTransition = `${transProp} ${timeInMs}ms`;
@@ -1284,13 +1326,13 @@ export class DocumentView extends React.Component<DocumentViewProps> {
}
// shows a stacking view collection (by default, but the user can change) of all documents linked to the source
- public static showBackLinks(linkSource: Doc) {
- const docId = Doc.CurrentUserEmail + Doc.GetProto(linkSource)[Id] + '-pivotish';
+ public static showBackLinks(linkAnchor: Doc) {
+ const docId = Doc.CurrentUserEmail + Doc.GetProto(linkAnchor)[Id] + '-pivotish';
// prettier-ignore
- DocServer.GetRefField(docId).then(docx => docx instanceof Doc &&
+ DocServer.GetRefField(docId).then(docx =>
LightboxView.SetLightboxDoc(
- docx || // reuse existing pivot view of documents, or else create a new collection
- Docs.Create.StackingDocument([], { title: linkSource.title + '-pivot', _width: 500, _height: 500, linkSource, updateContentsScript: ScriptField.MakeScript('updateLinkCollection(self)') }, docId)
+ (docx as Doc) ?? // reuse existing pivot view of documents, or else create a new collection
+ Docs.Create.StackingDocument([], { title: linkAnchor.title + '-pivot', _width: 500, _height: 500, target: linkAnchor, updateContentsScript: ScriptField.MakeScript('updateLinkCollection(self, self.target)') }, docId)
)
);
}
@@ -1326,7 +1368,7 @@ export class DocumentView extends React.Component<DocumentViewProps> {
return this.props.LayoutTemplateString?.includes('link_anchor_2') ? DocCast(this.rootDoc['link_anchor_2']) : this.props.LayoutTemplateString?.includes('link_anchor_1') ? DocCast(this.rootDoc['link_anchor_1']) : undefined;
}
@computed get hideLinkButton() {
- return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.HideLinkButton + (this.isSelected() ? ':selected' : ''));
+ return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.HideLinkBtn + (this.isSelected() ? ':selected' : ''));
}
@computed get linkCountView() {
const hideCount = this.props.renderDepth === -1 || SnappingManager.GetIsDragging() || (this.isSelected() && this.props.renderDepth) || !this._isHovering || this.hideLinkButton;
@@ -1345,7 +1387,7 @@ export class DocumentView extends React.Component<DocumentViewProps> {
return this.docView?._componentView?.reverseNativeScaling?.() ? 0 : returnVal(this.props.NativeHeight?.(), Doc.NativeHeight(this.layoutDoc, this.props.DataDoc, !this.layout_fitWidth));
}
@computed get shouldNotScale() {
- return (this.layout_fitWidth && !this.nativeWidth) || [CollectionViewType.Docking].includes(this.Document._viewType as any);
+ return this.props.shouldNotScale?.() || (this.layout_fitWidth && !this.nativeWidth) || [CollectionViewType.Docking].includes(this.Document._type_collection as any);
}
@computed get effectiveNativeWidth() {
return this.shouldNotScale ? 0 : this.nativeWidth || NumCast(this.layoutDoc.width);
@@ -1478,8 +1520,8 @@ export class DocumentView extends React.Component<DocumentViewProps> {
// increase max auto height if document has been resized to be greater than current max
() => NumCast(this.layoutDoc._height),
action(height => {
- const docMax = NumCast(this.layoutDoc.docMaxAutoHeight);
- if (docMax && docMax < height) this.layoutDoc.docMaxAutoHeight = height;
+ const docMax = NumCast(this.layoutDoc.layout_maxAutoHeight);
+ if (docMax && docMax < height) this.layoutDoc.layout_maxAutoHeight = height;
})
);
!BoolCast(this.props.Document.dontRegisterView, this.props.dontRegisterView) && DocumentManager.Instance.AddView(this);
@@ -1565,22 +1607,23 @@ ScriptingGlobals.add(function toggleDetail(dv: DocumentView, detailLayoutKeySuff
else dv.switchViews(true, detailLayoutKeySuffix, undefined, true);
});
-ScriptingGlobals.add(function updateLinkCollection(linkCollection: Doc) {
- const linkSource = Cast(linkCollection.linkSource, Doc, null);
+ScriptingGlobals.add(function updateLinkCollection(linkCollection: Doc, linkSource: Doc) {
const collectedLinks = DocListCast(Doc.GetProto(linkCollection).data);
- let wid = linkSource[WidthSym]();
+ let wid = linkSource[Width]();
+ let embedding: Doc | undefined;
const links = LinkManager.Links(linkSource);
links.forEach(link => {
const other = LinkManager.getOppositeAnchor(link, linkSource);
- const otherdoc = !other ? undefined : other.annotationOn ? Cast(other.annotationOn, Doc, null) : other;
+ const otherdoc = DocCast(other?.annotationOn ?? other);
if (otherdoc && !collectedLinks?.some(d => Doc.AreProtosEqual(d, otherdoc))) {
- const embedding = Doc.MakeEmbedding(otherdoc);
+ embedding = Doc.MakeEmbedding(otherdoc);
embedding.x = wid;
embedding.y = 0;
embedding._lockedPosition = false;
- wid += otherdoc[WidthSym]();
+ wid += otherdoc[Width]();
Doc.AddDocToList(Doc.GetProto(linkCollection), 'data', embedding);
}
});
+ embedding && DocServer.UPDATE_SERVER_CACHE(); // if a new embedding was made, update the client's server cache so that it will not come back as a promise
return links;
});
diff --git a/src/client/views/nodes/EquationBox.scss b/src/client/views/nodes/EquationBox.scss
index 9714e1bd0..f5871db22 100644
--- a/src/client/views/nodes/EquationBox.scss
+++ b/src/client/views/nodes/EquationBox.scss
@@ -1,8 +1,9 @@
-@import "../global/globalCssVariables.scss";
+@import '../global/globalCssVariables.scss';
.equationBox-cont {
- transform-origin: top left;
+ transform-origin: center;
+ background-color: #e7e7e7;
> span {
- width: 100%;
+ width: 100%;
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/nodes/EquationBox.tsx b/src/client/views/nodes/EquationBox.tsx
index f17ab06e7..a77e4bdd1 100644
--- a/src/client/views/nodes/EquationBox.tsx
+++ b/src/client/views/nodes/EquationBox.tsx
@@ -2,7 +2,7 @@ import EquationEditor from 'equation-editor-react';
import { action, reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { WidthSym } from '../../../fields/Doc';
+import { Width } from '../../../fields/DocSymbols';
import { Id } from '../../../fields/FieldSymbols';
import { NumCast, StrCast } from '../../../fields/Types';
import { TraceMobx } from '../../../fields/util';
@@ -26,7 +26,8 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() {
this.props.select(false);
this._ref.current!.mathField.focus();
- this._ref.current!.mathField.select();
+ this.rootDoc.text === 'x' && this._ref.current!.mathField.select();
+ EquationBox.SelectOnLoad = '';
}
reaction(
() => StrCast(this.dataDoc.text),
@@ -35,6 +36,7 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() {
this._ref.current!.mathField.latex(text);
}
}
+ //{ fireImmediately: true }
);
reaction(
() => this.props.isSelected(),
@@ -53,9 +55,8 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() {
const _height = Number(getComputedStyle(this._ref.current!.element.current).height.replace('px', ''));
const _width = Number(getComputedStyle(this._ref.current!.element.current).width.replace('px', ''));
if (e.key === 'Enter') {
- const nextEq = Docs.Create.EquationDocument({
+ const nextEq = Docs.Create.EquationDocument(e.shiftKey ? StrCast(this.dataDoc.text) : 'x', {
title: '# math',
- text: StrCast(this.dataDoc.text),
_width,
_height: 25,
x: NumCast(this.layoutDoc.x),
@@ -67,7 +68,7 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
if (e.key === 'Tab') {
const graph = Docs.Create.FunctionPlotDocument([this.rootDoc], {
- x: NumCast(this.layoutDoc.x) + this.layoutDoc[WidthSym](),
+ x: NumCast(this.layoutDoc.x) + this.layoutDoc[Width](),
y: NumCast(this.layoutDoc.y),
_width: 400,
_height: 300,
@@ -101,10 +102,10 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() {
width: 'fit-content', // `${100 / scale}%`,
height: `${100 / scale}%`,
pointerEvents: !this.props.isSelected() ? 'none' : undefined,
- fontSize: StrCast(this.rootDoc._fontSize),
+ fontSize: StrCast(this.rootDoc._text_fontSize),
}}
onKeyDown={e => e.stopPropagation()}>
- <EquationEditor ref={this._ref} value={this.dataDoc.text || 'x'} spaceBehavesLikeTab={true} onChange={this.onChange} autoCommands="pi theta sqrt sum prod alpha beta gamma rho" autoOperatorNames="sin cos tan" />
+ <EquationEditor ref={this._ref} value={StrCast(this.dataDoc.text, 'x')} spaceBehavesLikeTab={true} onChange={this.onChange} autoCommands="pi theta sqrt sum prod alpha beta gamma rho" autoOperatorNames="sin cos tan" />
</div>
);
}
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 85dd779fc..4ebf22ddf 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -45,7 +45,7 @@ export interface FieldViewProps extends DocumentViewSharedProps {
@observer
export class FieldView extends React.Component<FieldViewProps> {
public static LayoutString(fieldType: { name: string }, fieldStr: string) {
- return `<${fieldType.name} {...props} fieldKey={'${fieldStr}'}/>`; //e.g., "<ImageBox {...props} fieldKey={"data} />"
+ return `<${fieldType.name} {...props} fieldKey={'${fieldStr}'}/>`; //e.g., "<ImageBox {...props} fieldKey={'data'} />"
}
@computed
diff --git a/src/client/views/nodes/button/ButtonInterface.ts b/src/client/views/nodes/FontIconBox/ButtonInterface.ts
index 0aa2ac8e1..0aa2ac8e1 100644
--- a/src/client/views/nodes/button/ButtonInterface.ts
+++ b/src/client/views/nodes/FontIconBox/ButtonInterface.ts
diff --git a/src/client/views/nodes/button/FontIconBadge.scss b/src/client/views/nodes/FontIconBox/FontIconBadge.scss
index 78f506e57..2ff5c651f 100644
--- a/src/client/views/nodes/button/FontIconBadge.scss
+++ b/src/client/views/nodes/FontIconBox/FontIconBadge.scss
@@ -1,11 +1,12 @@
.fontIconBadge {
- background: red;
+ background: lightgreen;
width: 15px;
height: 15px;
top: 8px;
+ color: black;
display: block;
position: absolute;
right: 5;
border-radius: 50%;
text-align: center;
-} \ No newline at end of file
+}
diff --git a/src/client/views/nodes/FontIconBox/FontIconBadge.tsx b/src/client/views/nodes/FontIconBox/FontIconBadge.tsx
new file mode 100644
index 000000000..b50588ce2
--- /dev/null
+++ b/src/client/views/nodes/FontIconBox/FontIconBadge.tsx
@@ -0,0 +1,37 @@
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import './FontIconBadge.scss';
+
+interface FontIconBadgeProps {
+ value: string | undefined;
+}
+
+@observer
+export class FontIconBadge extends React.Component<FontIconBadgeProps> {
+ _notifsRef = React.createRef<HTMLDivElement>();
+
+ // onPointerDown = (e: React.PointerEvent) => {
+ // setupMoveUpEvents(this, e,
+ // (e: PointerEvent) => {
+ // const dragData = new DragManager.DocumentDragData([this.props.collection!]);
+ // DragManager.StartDocumentDrag([this._notifsRef.current!], dragData, e.x, e.y);
+ // return true;
+ // },
+ // returnFalse, emptyFunction, false);
+ // }
+
+ render() {
+ if (this.props.value === undefined) return null;
+ return (
+ <div className="fontIconBadge-container" ref={this._notifsRef}>
+ <div
+ className="fontIconBadge"
+ style={{ display: 'initial' }}
+ // onPointerDown={this.onPointerDown}
+ >
+ {this.props.value}
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/nodes/button/FontIconBox.scss b/src/client/views/nodes/FontIconBox/FontIconBox.scss
index f3b43501b..9d9fa26b0 100644
--- a/src/client/views/nodes/button/FontIconBox.scss
+++ b/src/client/views/nodes/FontIconBox/FontIconBox.scss
@@ -18,7 +18,7 @@
.fontIconBox-label {
color: $white;
- bottom: 0;
+ bottom: -1;
position: absolute;
text-align: center;
font-size: 7px;
@@ -27,7 +27,7 @@
border-radius: 8px;
padding: 0;
width: 100%;
- font-family: 'ROBOTO';
+ font-family: 'system-ui';
text-transform: uppercase;
font-weight: bold;
transition: 0.15s;
diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
new file mode 100644
index 000000000..91eac675f
--- /dev/null
+++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
@@ -0,0 +1,403 @@
+import { IconProp } from '@fortawesome/fontawesome-svg-core';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Button, MultiToggle, ColorPicker, Dropdown, DropdownType, EditableText, IconButton, IListItemProps, NumberDropdown, NumberDropdownType, Popup, Size, Toggle, ToggleType, Type } from 'browndash-components';
+import { action, computed, observable } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { Doc, DocListCast, StrListCast } from '../../../../fields/Doc';
+import { ScriptField } from '../../../../fields/ScriptField';
+import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
+import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
+import { SelectionManager } from '../../../util/SelectionManager';
+import { undoable, UndoManager } from '../../../util/UndoManager';
+import { ContextMenu } from '../../ContextMenu';
+import { DocComponent } from '../../DocComponent';
+import { EditableView } from '../../EditableView';
+import { Colors } from '../../global/globalEnums';
+import { StyleProp } from '../../StyleProvider';
+import { FieldView, FieldViewProps } from '../FieldView';
+import { OpenWhere } from '../DocumentView';
+import { RichTextMenu } from '../formattedText/RichTextMenu';
+import './FontIconBox.scss';
+import { SelectedDocView } from '../../selectedDoc';
+import { Utils } from '../../../../Utils';
+import { FaAlignCenter, FaAlignJustify, FaAlignLeft, FaAlignRight } from 'react-icons/fa';
+
+export enum ButtonType {
+ TextButton = 'textBtn',
+ MenuButton = 'menuBtn',
+ DropdownList = 'dropdownList',
+ DropdownButton = 'dropdownBtn',
+ ClickButton = 'clickBtn',
+ ToggleButton = 'toggleBtn',
+ ColorButton = 'colorBtn',
+ ToolButton = 'toolBtn',
+ MultiToggleButton = 'multiToggleBtn',
+ NumberSliderButton = 'numSliderBtn',
+ NumberDropdownButton = 'numDropdownBtn',
+ NumberInlineButton = 'numInlineBtn',
+ EditableText = 'editableText',
+}
+
+export interface ButtonProps extends FieldViewProps {
+ type?: ButtonType;
+}
+@observer
+export class FontIconBox extends DocComponent<ButtonProps>() {
+ public static LayoutString(fieldKey: string) {
+ return FieldView.LayoutString(FontIconBox, fieldKey);
+ }
+ @observable noTooltip = false;
+ showTemplate = (): void => {
+ const dragFactory = Cast(this.layoutDoc.dragFactory, Doc, null);
+ dragFactory && this.props.addDocTab(dragFactory, OpenWhere.addRight);
+ };
+ dragAsTemplate = (): void => {
+ this.layoutDoc.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)');
+ };
+ useAsPrototype = (): void => {
+ this.layoutDoc.onDragStart = ScriptField.MakeFunction('makeDelegate(this.dragFactory, true)');
+ };
+
+ specificContextMenu = (): void => {
+ if (!Doc.noviceMode) {
+ const cm = ContextMenu.Instance;
+ cm.addItem({ description: 'Show Template', event: this.showTemplate, icon: 'tag' });
+ cm.addItem({ description: 'Use as Render Template', event: this.dragAsTemplate, icon: 'tag' });
+ cm.addItem({ description: 'Use as Prototype', event: this.useAsPrototype, icon: 'tag' });
+ }
+ };
+
+ 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));
+ }
+ Icon = (color: string, iconFalse?: boolean) => {
+ let icon;
+ if (iconFalse) {
+ icon = StrCast(this.dataDoc[this.fieldKey ?? 'iconFalse'] ?? this.dataDoc.icon, 'user') as any;
+ if (icon) return <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={icon} color={color} />;
+ 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} />;
+ };
+ @computed get dropdown() {
+ return BoolCast(this.rootDoc.dropDownOpen);
+ }
+ @computed get buttonList() {
+ return StrListCast(this.rootDoc.btnList);
+ }
+ @computed get type() {
+ return StrCast(this.rootDoc.btnType);
+ }
+
+ /**
+ * Types of buttons in dash:
+ * - Main menu button (LHS)
+ * - Tool button
+ * - Expandable button (CollectionLinearView)
+ * - Button inside of CollectionLinearView vs. outside of CollectionLinearView
+ * - Action button
+ * - Dropdown button
+ * - Color button
+ * - Dropdown list
+ * - Number button
+ **/
+
+ _batch: UndoManager.Batch | undefined = undefined;
+ /**
+ * Number button
+ */
+ @computed get numberDropdown() {
+ let type: NumberDropdownType;
+ switch (this.type) {
+ case ButtonType.NumberDropdownButton:
+ type = 'dropdown';
+ break;
+ case ButtonType.NumberInlineButton:
+ type = 'input';
+ break;
+ case ButtonType.NumberSliderButton:
+ default:
+ type = 'slider';
+ break;
+ }
+ const numScript = (value?: number) => ScriptCast(this.rootDoc.script).script.run({ this: this.layoutDoc, self: this.rootDoc, value, _readOnly_: value === undefined });
+ 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}
+ numberDropdownType={type}
+ showPlusMinus={false}
+ tooltip={this.label}
+ type={Type.PRIM}
+ min={NumCast(this.rootDoc.numBtnMin, 0)}
+ max={NumCast(this.rootDoc.numBtnMax, 100)}
+ number={checkResult}
+ setNumber={undoable(value => numScript(value), `${this.rootDoc.title} button set from list`)}
+ fillWidth
+ />
+ );
+ }
+
+ /**
+ * Dropdown button
+ */
+ @computed get dropdownButton() {
+ 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);
+ return (
+ <div
+ className={`menuButton ${this.type} ${active}`}
+ style={{ color: color, backgroundColor: 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 : (
+ <div className="fontIconBox-label" style={{ color: color, backgroundColor: backgroundColor }}>
+ {' '}
+ {this.label}{' '}
+ </div>
+ )}
+ <div className="menuButton-dropdown" style={{ borderBottomRightRadius: this.dropdown ? 0 : undefined }}>
+ <FontAwesomeIcon icon={'caret-down'} color={color} size="sm" />
+ </div>
+ {this.rootDoc.dropDownOpen ? <div className="menuButton-dropdownBox">{/* DROPDOWN BOX CONTENTS */}</div> : null}
+ </div>
+ );
+ }
+
+ /**
+ * 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[] = [];
+ let text: string | undefined;
+ let dropdown = true;
+ let getStyle: (val: string) => any = () => {};
+ let icon: IconProp = 'caret-down';
+ let isViewDropdown: boolean = script?.script.originalScript.startsWith('setView');
+ try {
+ if (isViewDropdown) {
+ const selectedDocs: Doc[] = SelectionManager.Docs();
+ const selected = SelectionManager.Docs().lastElement();
+ if (selected) {
+ if (StrCast(selected.type) === DocumentType.COL) {
+ text = StrCast(selected._type_collection);
+ } else {
+ dropdown = false;
+ if (selectedDocs.length > 1) {
+ text = selectedDocs.length + ' selected';
+ } else {
+ 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 />;
+ }
+ } else {
+ dropdown = false;
+ return <Button text={`None Selected`} type={Type.TERT} color={color} fillWidth inactive />;
+ }
+ noviceList = [CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Stacking, CollectionViewType.NoteTaking];
+ } else {
+ text = StrCast((RichTextMenu.Instance?.TextView?.EditorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily);
+ getStyle = (val: string) => {
+ return { fontFamily: val };
+ };
+ }
+ } catch (e) {
+ console.log(e);
+ }
+
+ // Get items to place into the list
+ const list: IListItemProps[] = this.buttonList
+ .filter(value => !Doc.noviceMode || !noviceList.length || noviceList.includes(value))
+ .map(value => ({
+ text: value.charAt(0).toUpperCase() + value.slice(1),
+ val: value,
+ style: getStyle(value),
+ onClick: undoable(() => script.script.run({ this: this.layoutDoc, self: this.rootDoc, value }), value),
+ // shortcut: '#',
+ }));
+
+ 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
+ />
+ );
+ }
+
+ @computed get colorScript() {
+ return ScriptCast(this.rootDoc.script);
+ }
+
+ /**
+ * Color button
+ */
+ @computed get colorButton() {
+ const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color);
+ const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor);
+ const curColor = this.colorScript?.script.run({ this: this.layoutDoc, self: this.rootDoc, value: undefined, _readOnly_: true }).result ?? 'transparent';
+ const tooltip: string = StrCast(this.rootDoc.toolTip);
+
+ return (
+ <ColorPicker
+ setSelectedColor={value => {
+ const s = this.colorScript;
+ s && undoable(() => s.script.run({ this: this.layoutDoc, self: this.rootDoc, value: value, _readOnly_: false }).result, `Set ${tooltip} to ${value}`)();
+ }}
+ selectedColor={curColor}
+ type={Type.PRIM}
+ color={color}
+ icon={this.Icon(color)!}
+ tooltip={tooltip}
+ label={this.label}
+ />
+ );
+ }
+ @computed get multiToggleButton() {
+ // Determine the type of toggle button
+ const tooltip: string = StrCast(this.rootDoc.toolTip);
+
+ const script = ScriptCast(this.rootDoc.onClick);
+ const toggleStatus = script ? script.script.run({ this: this.layoutDoc, self: this.rootDoc, value: undefined, _readOnly_: true }).result : false;
+ // Colors
+ const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color);
+ const items = DocListCast(this.rootDoc.data);
+ return (
+ <MultiToggle
+ tooltip={`Toggle ${tooltip}`}
+ type={Type.PRIM}
+ color={color}
+ 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} />,
+ tooltip: StrCast(item.toolTip),
+ val: StrCast(item.toolType),
+ }))}
+ selectedVal={StrCast(items.find(itemDoc => ScriptCast(itemDoc.onClick).script.run({ this: itemDoc, self: itemDoc, value: undefined, _readOnly_: true }).result)?.toolType)}
+ setSelectedVal={(val: string | number) => {
+ const itemDoc = items.find(item => item.toolType === val);
+ itemDoc && ScriptCast(itemDoc.onClick).script.run({ this: itemDoc, self: itemDoc, value: val, _readOnly_: false });
+ }}
+ />
+ );
+ }
+
+ @computed get toggleButton() {
+ // Determine the type of toggle button
+ const buttonText: string = StrCast(this.rootDoc.buttonText);
+ const tooltip: string = StrCast(this.rootDoc.toolTip);
+
+ const script = ScriptCast(this.rootDoc.onClick);
+ const toggleStatus = script ? script.script.run({ this: this.layoutDoc, self: this.rootDoc, value: undefined, _readOnly_: true }).result : false;
+ // Colors
+ const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color);
+ const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor);
+
+ return (
+ <Toggle
+ tooltip={`Toggle ${tooltip}`}
+ toggleType={ToggleType.BUTTON}
+ type={Type.PRIM}
+ toggleStatus={toggleStatus}
+ text={buttonText}
+ color={color}
+ icon={this.Icon(color)!}
+ label={this.label}
+ onPointerDown={() => script.script.run({ this: this.layoutDoc, self: this.rootDoc, value: !toggleStatus, _readOnly_: false })}
+ />
+ );
+ }
+
+ /**
+ * Default
+ */
+ @computed get defaultButton() {
+ const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color);
+ const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor);
+ const tooltip: string = StrCast(this.rootDoc.toolTip);
+
+ return <IconButton tooltip={tooltip} icon={this.Icon(color)!} label={this.label} />;
+ }
+
+ @computed get editableText() {
+ // Script for running the toggle
+ const script = ScriptCast(this.rootDoc.script);
+ // Function to run the script
+ const checkResult = script?.script.run({ this: this.layoutDoc, self: this.rootDoc, value: '', _readOnly_: true }).result;
+
+ const setValue = (value: string, shiftDown?: boolean): boolean => script?.script.run({ this: this.layoutDoc, self: this.rootDoc, value, _readOnly_: false }).result;
+
+ return <EditableText editing={false} setEditing={(editing: boolean) => {}} />;
+
+ return (
+ <div className="menuButton editableText">
+ <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={'lock'} />
+ <div style={{ width: 'calc(100% - .875em)', paddingLeft: '4px' }}>
+ <EditableView GetValue={() => script?.script.run({ this: this.layoutDoc, self: this.rootDoc, value: '', _readOnly_: true }).result} SetValue={setValue} oneLine={true} contents={checkResult} />
+ </div>
+ </div>
+ );
+ }
+
+ render() {
+ const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color);
+ const tooltip = StrCast(this.rootDoc.toolTip);
+ const scriptFunc = () => ScriptCast(this.rootDoc.onClick)?.script.run({ this: this.layoutDoc, self: this.rootDoc, _readOnly_: false });
+ const btnProps = { tooltip, icon: this.Icon(color)!, label: this.label };
+ // prettier-ignore
+ switch (this.type) {
+ case ButtonType.NumberDropdownButton:
+ case ButtonType.NumberInlineButton:
+ case ButtonType.NumberSliderButton: return this.numberDropdown;
+ case ButtonType.EditableText: return this.editableText;
+ case ButtonType.DropdownList: return this.dropdownListButton;
+ case ButtonType.ColorButton: return this.colorButton;
+ case ButtonType.DropdownButton: return this.dropdownButton;
+ case ButtonType.MultiToggleButton: return this.multiToggleButton;
+ 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} />;
+ }
+ return this.defaultButton;
+ }
+}
diff --git a/src/client/views/nodes/FunctionPlotBox.tsx b/src/client/views/nodes/FunctionPlotBox.tsx
index db72209ab..61711417f 100644
--- a/src/client/views/nodes/FunctionPlotBox.tsx
+++ b/src/client/views/nodes/FunctionPlotBox.tsx
@@ -43,7 +43,10 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>
);
}
getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
- const anchor = Docs.Create.TextanchorDocument({ annotationOn: this.rootDoc, unrendered: true });
+ const anchor = Docs.Create.ConfigDocument({
+ //
+ annotationOn: this.rootDoc,
+ });
PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), datarange: true } }, this.rootDoc);
anchor.presXRange = new List<number>(Array.from(this._plot.options.xAxis.domain));
anchor.presYRange = new List<number>(Array.from(this._plot.options.yAxis.domain));
@@ -77,9 +80,10 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>
@undoBatch
drop = (e: Event, de: DragManager.DropEvent) => {
if (de.complete.docDragData?.droppedDocuments.length) {
+ const added = de.complete.docDragData.droppedDocuments.reduce((res, doc) => res && Doc.AddDocToList(this.dataDoc, this.props.fieldKey, doc), true);
+ !added && e.preventDefault();
e.stopPropagation(); // prevent parent Doc from registering new position so that it snaps back into place
- de.complete.docDragData.droppedDocuments.map(doc => Doc.AddDocToList(this.dataDoc, this.props.fieldKey, doc));
- return false;
+ return added;
}
return false;
};
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 7d1d50cc7..d763753a5 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -3,7 +3,8 @@ import { Tooltip } from '@material-ui/core';
import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import { extname } from 'path';
-import { DataSym, Doc, DocListCast, Opt, WidthSym } from '../../../fields/Doc';
+import { Doc, DocListCast, Opt } from '../../../fields/Doc';
+import { DocData, Width } from '../../../fields/DocSymbols';
import { Id } from '../../../fields/FieldSymbols';
import { InkTool } from '../../../fields/InkField';
import { List } from '../../../fields/List';
@@ -77,13 +78,12 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
const anchor =
this._getAnchor?.(this._savedAnnotations, false) ?? // use marquee anchor, otherwise, save zoom/pan as anchor
- Docs.Create.ImageanchorDocument({
+ Docs.Create.ConfigDocument({
title: 'ImgAnchor:' + this.rootDoc.title,
presPanX: NumCast(this.layoutDoc._freeform_panX),
presPanY: NumCast(this.layoutDoc._freeform_panY),
presViewScale: Cast(this.layoutDoc._freeform_scale, 'number', null),
presTransition: 1000,
- unrendered: true,
annotationOn: this.rootDoc,
});
if (anchor) {
@@ -107,7 +107,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
);
const layoutDoc = this.layoutDoc;
this._disposers.path = reaction(
- () => ({ nativeSize: this.nativeSize, width: this.layoutDoc[WidthSym]() }),
+ () => ({ nativeSize: this.nativeSize, width: this.layoutDoc[Width]() }),
({ nativeSize, width }) => {
if (layoutDoc === this.layoutDoc || !this.layoutDoc._height) {
this.layoutDoc._height = (width * nativeSize.nativeHeight) / nativeSize.nativeWidth;
@@ -135,31 +135,32 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
@action
drop = (e: Event, de: DragManager.DropEvent) => {
if (de.complete.docDragData) {
+ let added: boolean | undefined = undefined;
const targetIsBullseye = (ele: HTMLElement): boolean => {
if (!ele) return false;
if (ele === this._overlayIconRef.current) return true;
return targetIsBullseye(ele.parentElement as HTMLElement);
};
if (de.metaKey || targetIsBullseye(e.target as HTMLElement)) {
- de.complete.docDragData.droppedDocuments.forEach(
- action((drop: Doc) => {
- Doc.AddDocToList(this.dataDoc, this.fieldKey + '-alternates', drop);
- this.rootDoc[this.fieldKey + '_usePath'] = 'alternate:hover';
- e.stopPropagation();
- })
- );
+ added = de.complete.docDragData.droppedDocuments.reduce((last: boolean, drop: Doc) => {
+ this.rootDoc[this.fieldKey + '_usePath'] = 'alternate:hover';
+ return last && Doc.AddDocToList(this.dataDoc, this.fieldKey + '-alternates', drop);
+ }, true);
} else if (de.altKey || !this.dataDoc[this.fieldKey]) {
const layoutDoc = de.complete.docDragData?.draggedDocuments[0];
const targetField = Doc.LayoutFieldKey(layoutDoc);
- const targetDoc = layoutDoc[DataSym];
+ const targetDoc = layoutDoc[DocData];
if (targetDoc[targetField] instanceof ImageField) {
this.dataDoc[this.fieldKey] = ObjectField.MakeCopy(targetDoc[targetField] as ImageField);
Doc.SetNativeWidth(this.dataDoc, Doc.NativeWidth(targetDoc), this.fieldKey);
Doc.SetNativeHeight(this.dataDoc, Doc.NativeHeight(targetDoc), this.fieldKey);
- e.stopPropagation();
}
}
+ added === false && e.preventDefault();
+ added !== undefined && e.stopPropagation();
+ return added;
}
+ return false;
};
@undoBatch
@@ -169,14 +170,13 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
setNativeSize = action(() => {
const scaling = (this.props.DocumentView?.().props.ScreenToLocalTransform().Scale || 1) / NumCast(this.rootDoc._freeform_scale, 1);
const nscale = NumCast(this.props.PanelWidth()) / scaling;
- const nh = nscale / NumCast(this.dataDoc[this.fieldKey + '_nativeHeight']);
const nw = nscale / NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']);
- this.dataDoc[this.fieldKey + '_nativeHeight'] = NumCast(this.dataDoc[this.fieldKey + '_nativeHeight']) * nh;
- this.dataDoc[this.fieldKey + '_nativeWidth'] = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']) * nh;
- this.rootDoc._freeform_panX = nh * NumCast(this.rootDoc._freeform_panX);
- this.rootDoc._freeform_panY = nh * NumCast(this.rootDoc._freeform_panY);
- this.dataDoc._freeform_panXMax = this.dataDoc._freeform_panXMax ? nh * NumCast(this.dataDoc._freeform_panXMax) : undefined;
- this.dataDoc._freeform_panXMin = this.dataDoc._freeform_panXMin ? nh * NumCast(this.dataDoc._freeform_panXMin) : undefined;
+ this.dataDoc[this.fieldKey + '_nativeHeight'] = NumCast(this.dataDoc[this.fieldKey + '_nativeHeight']) * nw;
+ this.dataDoc[this.fieldKey + '_nativeWidth'] = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']) * nw;
+ this.rootDoc._freeform_panX = nw * NumCast(this.rootDoc._freeform_panX);
+ this.rootDoc._freeform_panY = nw * NumCast(this.rootDoc._freeform_panY);
+ this.dataDoc._freeform_panXMax = this.dataDoc._freeform_panXMax ? nw * NumCast(this.dataDoc._freeform_panXMax) : undefined;
+ this.dataDoc._freeform_panXMin = this.dataDoc._freeform_panXMin ? nw * NumCast(this.dataDoc._freeform_panXMin) : undefined;
this.dataDoc._freeform_panYMax = this.dataDoc._freeform_panYMax ? nw * NumCast(this.dataDoc._freeform_panYMax) : undefined;
this.dataDoc._freeform_panYMin = this.dataDoc._freeform_panYMin ? nw * NumCast(this.dataDoc._freeform_panYMin) : undefined;
});
@@ -231,7 +231,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
croppingProto.freeform_panYMax = anchh / viewScale;
if (addCrop) {
DocUtils.MakeLink(region, cropping, { link_relationship: 'cropped image' });
- cropping.x = NumCast(this.rootDoc.x) + this.rootDoc[WidthSym]();
+ cropping.x = NumCast(this.rootDoc.x) + this.rootDoc[Width]();
cropping.y = NumCast(this.rootDoc.y);
this.props.addDocTab(cropping, OpenWhere.inParent);
}
@@ -295,7 +295,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
choosePath(url: URL) {
const lower = url.href.toLowerCase();
if (url.protocol === 'data') return url.href;
- if (url.href.indexOf(window.location.origin) === -1) return Utils.CorsProxy(url.href);
+ if (url.href.indexOf(window.location.origin) === -1 && url.href.indexOf("dashblobstore") === -1) return Utils.CorsProxy(url.href);
if (!/\.(png|jpg|jpeg|gif|webp)$/.test(lower)) return `/assets/unknown-file-icon-hi.png`;
const ext = extname(url.href);
@@ -303,7 +303,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
}
considerGooglePhotosLink = () => {
- const remoteUrl = this.dataDoc.googlePhotosUrl;
+ const remoteUrl = StrCast(this.dataDoc.googlePhotosUrl); // bcz: StrCast or URLCast???
return !remoteUrl ? null : <img draggable={false} style={{ transformOrigin: 'bottom right' }} id={'google-photos'} src={'/assets/google_photos.png'} onClick={() => window.open(remoteUrl)} />;
};
@@ -387,7 +387,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
ref={this._overlayIconRef}
onPointerDown={e => setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, e => (this.rootDoc[`_${this.fieldKey}_usePath`] = usePath === undefined ? 'alternate' : usePath === 'alternate' ? 'alternate:hover' : undefined))}
style={{
- display: (SnappingManager.GetIsDragging() && DragManager.DocDragData?.canEmbed) || DocListCast(this.dataDoc[this.fieldKey + '-alternates']).length ? 'block' : 'none',
+ display: (this.props.isContentActive() !== false && DragManager.DocDragData?.canEmbed) || DocListCast(this.dataDoc[this.fieldKey + '-alternates']).length ? 'block' : 'none',
width: 'min(10%, 25px)',
height: 'min(10%, 25px)',
background: usePath === undefined ? 'white' : usePath === 'alternate' ? 'black' : 'gray',
@@ -505,7 +505,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
}
})}
style={{
- display: !SnappingManager.GetIsDragging() && this.props.thumbShown?.() ? 'none' : undefined,
+ display: !this.props.isContentActive() && this.props.thumbShown?.() ? 'none' : undefined,
width: this.props.PanelWidth() ? undefined : `100%`,
height: this.props.PanelWidth() ? undefined : `100%`,
pointerEvents: this.layoutDoc._lockedPosition ? 'none' : undefined,
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index 88a82e8e6..a6712a3db 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -84,7 +84,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
public static ApplyKVPScript(doc: Doc, key: string, kvpScript: KVPScript, forceOnDelegate?: boolean): boolean {
const { script, type, onDelegate } = kvpScript;
//const target = onDelegate ? Doc.Layout(doc.layout) : Doc.GetProto(doc); // bcz: TODO need to be able to set fields on layout templates
- const target = forceOnDelegate || onDelegate || key.startsWith('_') ? doc : doc.proto || doc;
+ const target = forceOnDelegate || onDelegate || key.startsWith('_') ? doc : DocCast(doc.proto, doc);
let field: Field;
if (type === 'computed') {
field = new ComputedField(script);
@@ -213,18 +213,18 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
document.addEventListener('pointerup', this.onDividerUp);
};
- getFieldView = async () => {
+ getFieldView = () => {
const rows = this.rows.filter(row => row.isChecked);
if (rows.length > 1) {
- const parent = Docs.Create.StackingDocument([], { _layout_autoHeight: true, _width: 300, title: `field views for ${DocCast(this.props.Document.data).title}`, _chromeHidden: true });
+ const parent = Docs.Create.StackingDocument([], { _layout_autoHeight: true, _width: 300, title: `field views for ${DocCast(this.props.Document).title}`, _chromeHidden: true });
for (const row of rows) {
- const field = this.createFieldView(DocCast(this.props.Document.data), row);
+ const field = this.createFieldView(DocCast(this.props.Document), row);
field && Doc.AddDocToList(parent, 'data', field);
row.uncheck();
}
return parent;
}
- return rows.length ? this.createFieldView(DocCast(this.props.Document.data), rows.lastElement()) : undefined;
+ return rows.length ? this.createFieldView(DocCast(this.props.Document), rows.lastElement()) : undefined;
};
createFieldView = (templateDoc: Doc, row: KeyValuePair) => {
diff --git a/src/client/views/nodes/KeyValuePair.scss b/src/client/views/nodes/KeyValuePair.scss
index 2b4867321..57d36932e 100644
--- a/src/client/views/nodes/KeyValuePair.scss
+++ b/src/client/views/nodes/KeyValuePair.scss
@@ -19,7 +19,6 @@
position: relative;
margin: 0;
}
- .keyValuePair-keyFieldMod,
.keyValuePair-keyField {
width: 100%;
margin-left: 20px;
@@ -28,9 +27,6 @@
overflow: auto;
display: inline;
}
- .keyValuePair-keyFieldMod {
- margin-left: 35px;
- }
}
}
.keyValuePair-td-value {
diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx
index 509bb667e..01acdccb7 100644
--- a/src/client/views/nodes/KeyValuePair.tsx
+++ b/src/client/views/nodes/KeyValuePair.tsx
@@ -13,6 +13,7 @@ import { KeyValueBox } from './KeyValueBox';
import './KeyValueBox.scss';
import './KeyValuePair.scss';
import React = require('react');
+import { DocCast } from '../../../fields/Types';
// Represents one row in a key value plane
@@ -56,8 +57,8 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
const props: FieldViewProps = {
Document: this.props.doc,
DataDoc: this.props.doc,
- docFilters: returnEmptyFilter,
- docRangeFilters: returnEmptyFilter,
+ childFilters: returnEmptyFilter,
+ childFiltersByRanges: returnEmptyFilter,
searchFilterDocs: returnEmptyDoclist,
styleProvider: DefaultStyleProvider,
docViewPath: returnEmptyDoclist,
@@ -66,7 +67,6 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
isSelected: returnFalse,
setHeight: returnFalse,
select: emptyFunction,
- dropAction: 'embed',
bringToFront: emptyFunction,
renderDepth: 1,
isContentActive: returnFalse,
@@ -87,7 +87,7 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
break;
}
protoCount++;
- doc = doc.proto;
+ doc = DocCast(doc.proto);
}
const parenCount = Math.max(0, protoCount - 1);
const keyStyle = protoCount === 0 ? 'black' : 'blue';
@@ -104,12 +104,12 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
onClick={undoBatch(() => {
if (Object.keys(props.Document).indexOf(props.fieldKey) !== -1) {
delete props.Document[props.fieldKey];
- } else delete props.Document.proto![props.fieldKey];
+ } else delete DocCast(props.Document.proto)?.[props.fieldKey];
})}>
X
</button>
<input className="keyValuePair-td-key-check" type="checkbox" style={hover} onChange={this.handleCheck} ref={this.checkbox} />
- <div className={`keyValuePair-keyField${props.fieldKey.includes('_') ? 'Mod' : ''}`} style={{ color: keyStyle }}>
+ <div className="keyValuePair-keyField" style={{ marginLeft: 20 * (props.fieldKey.match(/_/g)?.length || 0), color: keyStyle }}>
{'('.repeat(parenCount)}
{props.fieldKey}
{')'.repeat(parenCount)}
diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx
index 32026ea9c..4439be0cd 100644
--- a/src/client/views/nodes/LabelBox.tsx
+++ b/src/client/views/nodes/LabelBox.tsx
@@ -74,7 +74,9 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps & LabelBoxProp
if (docDragData && missingParams?.includes((e.target as any).textContent)) {
this.paramsDoc[(e.target as any).textContent] = new List<Doc>(docDragData.droppedDocuments.map((d, i) => (d.onDragStart ? docDragData.draggedDocuments[i] : d)));
e.stopPropagation();
+ return true;
}
+ return false;
};
@observable _mouseOver = false;
@@ -87,8 +89,8 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps & LabelBoxProp
const params = {
rotateText: null,
fontSizeFactor: 1,
- minimumFontSize: NumCast(this.rootDoc._minFontSize, 8),
- maximumFontSize: NumCast(this.rootDoc._maxFontSize, 1000),
+ minimumFontSize: NumCast(this.rootDoc._label_minFontSize, 8),
+ maximumFontSize: NumCast(this.rootDoc._label_maxFontSize, 1000),
limitingDimension: 'both',
horizontalAlign: 'center',
verticalAlign: 'center',
@@ -130,9 +132,9 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps & LabelBoxProp
className="labelBox-mainButton"
style={{
backgroundColor: this.hoverColor,
- fontSize: StrCast(this.layoutDoc._fontSize),
+ fontSize: StrCast(this.layoutDoc._text_fontSize),
color: StrCast(this.layoutDoc._color),
- fontFamily: StrCast(this.layoutDoc._fontFamily) || 'inherit',
+ fontFamily: StrCast(this.layoutDoc._text_fontFamily) || 'inherit',
letterSpacing: StrCast(this.layoutDoc.letterSpacing),
textTransform: StrCast(this.layoutDoc.textTransform) as any,
paddingLeft: NumCast(this.rootDoc._xPadding),
diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx
index f38ef634c..9bcd04cf5 100644
--- a/src/client/views/nodes/LinkAnchorBox.tsx
+++ b/src/client/views/nodes/LinkAnchorBox.tsx
@@ -47,13 +47,13 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() {
if (separation > 100) {
const dragData = new DragManager.DocumentDragData([this.rootDoc]);
dragData.dropAction = 'embed';
- dragData.removeDropProperties = ['link_anchor_1_x', 'link_anchor_1_y', 'link_anchor_2_x', 'link_anchor_2_y', 'onClick'];
+ dragData.dropPropertiesToRemove = ['link_anchor_1_x', 'link_anchor_1_y', 'link_anchor_2_x', 'link_anchor_2_y', 'onClick'];
DragManager.StartDocumentDrag([this._ref.current!], dragData, pt[0], pt[1]);
return true;
} else {
this.rootDoc[this.fieldKey + '_x'] = ((pt[0] - bounds.left) / bounds.width) * 100;
this.rootDoc[this.fieldKey + '_y'] = ((pt[1] - bounds.top) / bounds.height) * 100;
- this.rootDoc.layout_autoMoveAnchors = false;
+ this.rootDoc.link_autoMoveAnchors = false;
}
}
return false;
@@ -68,7 +68,7 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() {
const y = NumCast(this.rootDoc[this.fieldKey + '_y'], 100);
const background = this.props.styleProvider?.(this.dataDoc, this.props, StyleProp.BackgroundColor + ':anchor');
const anchor = this.fieldKey === 'link_anchor_1' ? 'link_anchor_2' : 'link_anchor_1';
- const anchorScale = !this.dataDoc[this.fieldKey + '_useLinkSmallAnchor'] && (x === 0 || x === 100 || y === 0 || y === 100) ? 1 : 0.25;
+ const anchorScale = !this.dataDoc[this.fieldKey + '_useSmallAnchor'] && (x === 0 || x === 100 || y === 0 || y === 100) ? 1 : 0.25;
const targetTitle = StrCast((this.dataDoc[anchor] as Doc)?.title);
const selView = SelectionManager.Views().lastElement()?.props.LayoutTemplateString?.includes('link_anchor_1')
? 'link_anchor_1'
diff --git a/src/client/views/nodes/LinkBox.scss b/src/client/views/nodes/LinkBox.scss
index b5b8e660f..767f0291b 100644
--- a/src/client/views/nodes/LinkBox.scss
+++ b/src/client/views/nodes/LinkBox.scss
@@ -1,3 +1,7 @@
-.linkBox-container-interactive {
+.linkBox-container-interactive {
pointer-events: all;
-} \ No newline at end of file
+ width: 100%;
+}
+.linkBox-container {
+ width: 100%;
+}
diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx
index c6172ee01..d69009415 100644
--- a/src/client/views/nodes/LinkDocPreview.tsx
+++ b/src/client/views/nodes/LinkDocPreview.tsx
@@ -3,11 +3,12 @@ import { Tooltip } from '@material-ui/core';
import { action, computed, observable } from 'mobx';
import { observer } from 'mobx-react';
import wiki from 'wikijs';
-import { Doc, DocCastAsync, HeightSym, Opt, WidthSym } from '../../../fields/Doc';
+import { Doc, DocCastAsync, Opt } from '../../../fields/Doc';
+import { DirectLinks, 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';
-import { Docs, DocUtils } from '../../documents/Documents';
+import { Docs } from '../../documents/Documents';
import { DocumentType } from '../../documents/DocumentTypes';
import { DragManager } from '../../util/DragManager';
import { LinkFollower } from '../../util/LinkFollower';
@@ -123,7 +124,6 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
this._markerTargetDoc = linkTarget;
this._targetDoc = /*linkTarget?.type === DocumentType.MARKER &&*/ linkTarget?.annotationOn ? Cast(linkTarget.annotationOn, Doc, null) ?? linkTarget : linkTarget;
}
- this._toolTipText = 'link to ' + this._targetDoc?.title;
if (LinkDocPreview.LinkInfo?.noPreview || this._linkSrc?.followLinkToggle || this._markerTargetDoc?.type === DocumentType.PRES) this.followLink();
}
})
@@ -184,17 +184,17 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
width = () => {
if (!this._targetDoc) return 225;
- if (this._targetDoc[WidthSym]() < this._targetDoc?.[HeightSym]()) {
- return (Math.min(225, this._targetDoc[HeightSym]()) * this._targetDoc[WidthSym]()) / this._targetDoc[HeightSym]();
+ if (this._targetDoc[Width]() < this._targetDoc?.[Height]()) {
+ return (Math.min(225, this._targetDoc[Height]()) * this._targetDoc[Width]()) / this._targetDoc[Height]();
}
- return Math.min(225, NumCast(this._targetDoc?.[WidthSym](), 225));
+ return Math.min(225, NumCast(this._targetDoc?.[Width](), 225));
};
height = () => {
if (!this._targetDoc) return 225;
- if (this._targetDoc[WidthSym]() > this._targetDoc?.[HeightSym]()) {
- return (Math.min(225, this._targetDoc[WidthSym]()) * this._targetDoc[HeightSym]()) / this._targetDoc[WidthSym]();
+ if (this._targetDoc[Width]() > this._targetDoc?.[Height]()) {
+ return (Math.min(225, this._targetDoc[Width]()) * this._targetDoc[Height]()) / this._targetDoc[Width]();
}
- return Math.min(225, NumCast(this._targetDoc?.[HeightSym](), 225));
+ return Math.min(225, NumCast(this._targetDoc?.[Height](), 225));
};
@computed get previewHeader() {
return !this._linkDoc || !this._markerTargetDoc || !this._targetDoc || !this._linkSrc ? null : (
@@ -268,8 +268,8 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
addDocTab={returnFalse}
pinToPres={returnFalse}
dontRegisterView={true}
- docFilters={returnEmptyFilter}
- docRangeFilters={returnEmptyFilter}
+ childFilters={returnEmptyFilter}
+ childFiltersByRanges={returnEmptyFilter}
searchFilterDocs={returnEmptyDoclist}
renderDepth={0}
suppressSetHeight={true}
@@ -296,7 +296,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
className="linkDocPreview"
ref={this._linkDocRef}
onPointerDown={this.followLinkPointerDown}
- style={{ display: !this._toolTipText ? 'none' : undefined, left: this.props.location[0], top: this.props.location[1], width: this.width() + borders, height: this.height() + borders + (this.props.showHeader ? 37 : 0) }}>
+ style={{ left: this.props.location[0], top: this.props.location[1], width: this.width() + borders, height: this.height() + borders + (this.props.showHeader ? 37 : 0) }}>
{this.docPreview}
</div>
);
diff --git a/src/client/views/nodes/LoadingBox.scss b/src/client/views/nodes/LoadingBox.scss
index 4c3b8dabe..d4a7e18f2 100644
--- a/src/client/views/nodes/LoadingBox.scss
+++ b/src/client/views/nodes/LoadingBox.scss
@@ -12,6 +12,10 @@
text-overflow: ellipsis;
max-width: 80%;
text-align: center;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ align-items: center;
}
}
diff --git a/src/client/views/nodes/LoadingBox.tsx b/src/client/views/nodes/LoadingBox.tsx
index 8c5255f80..fcbd0128d 100644
--- a/src/client/views/nodes/LoadingBox.tsx
+++ b/src/client/views/nodes/LoadingBox.tsx
@@ -8,6 +8,7 @@ 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
@@ -43,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.title));
+ 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/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx
index bfb0a6161..15cf22953 100644
--- a/src/client/views/nodes/MapBox/MapBox.tsx
+++ b/src/client/views/nodes/MapBox/MapBox.tsx
@@ -62,6 +62,12 @@ const mapOptions = {
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 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
*/
@@ -473,7 +479,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
return StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%');
}
@computed get sidebarColor() {
- return StrCast(this.layoutDoc.sidebarColor, StrCast(this.layoutDoc[this.props.fieldKey + '_backgroundColor'], '#e4e4e4'));
+ return StrCast(this.layoutDoc.sidebar_color, StrCast(this.layoutDoc[this.props.fieldKey + '_backgroundColor'], '#e4e4e4'));
}
/**
@@ -544,9 +550,8 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
return this.addDocument(doc, annotationKey);
};
- pointerEvents = () => {
- return this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' && !MarqueeOptionsMenu.Instance.isShown() ? 'all' : SnappingManager.GetIsDragging() ? undefined : 'none';
- };
+ pointerEvents = () => (this.props.isContentActive() && !MarqueeOptionsMenu.Instance.isShown() ? 'all' : 'none');
+
@computed get annotationLayer() {
return (
<div className="mapBox-annotationLayer" style={{ height: Doc.NativeHeight(this.Document) || undefined }} ref={this._annotationLayer}>
@@ -583,8 +588,8 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
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));
- transparentFilter = () => [...this.props.docFilters(), Utils.IsTransparentFilter()];
- opaqueFilter = () => [...this.props.docFilters(), Utils.IsOpaqueFilter()];
+ transparentFilter = () => [...this.props.childFilters(), Utils.IsTransparentFilter()];
+ opaqueFilter = () => [...this.props.childFilters(), Utils.IsOpaqueFilter()];
infoWidth = () => this.props.PanelWidth() / 5;
infoHeight = () => this.props.PanelHeight() / 5;
anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick;
@@ -905,7 +910,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
}
render() {
- const renderAnnotations = (docFilters?: () => string[]) => null;
+ const renderAnnotations = (childFilters?: () => string[]) => null;
return (
<div className="mapBox" ref={this._ref}>
<div
diff --git a/src/client/views/nodes/MapBox/MapBox2.tsx b/src/client/views/nodes/MapBox/MapBox2.tsx
index f75b3a6a6..643aeaee0 100644
--- a/src/client/views/nodes/MapBox/MapBox2.tsx
+++ b/src/client/views/nodes/MapBox/MapBox2.tsx
@@ -3,7 +3,8 @@ import { Autocomplete, GoogleMap, GoogleMapProps, Marker } from '@react-google-m
import { action, computed, IReactionDisposer, observable, ObservableMap, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { Doc, DocListCast, Opt, WidthSym } from '../../../../fields/Doc';
+import { Doc, DocListCast, Opt } from '../../../../fields/Doc';
+import { Width } from '../../../../fields/DocSymbols';
import { Id } from '../../../../fields/FieldSymbols';
import { InkTool } from '../../../../fields/InkField';
import { NumCast, StrCast } from '../../../../fields/Types';
@@ -367,7 +368,7 @@ export class MapBox2 extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
.ScreenToLocalTransform()
.scale(this.props.NativeDimScaling?.() || 1)
.transformDirection(delta[0], delta[1]);
- const fullWidth = this.layoutDoc[WidthSym]();
+ const fullWidth = this.layoutDoc[Width]();
const mapWidth = fullWidth - this.sidebarWidth();
if (this.sidebarWidth() + localDelta[0] > 0) {
this._showSidebar = true;
@@ -390,7 +391,7 @@ export class MapBox2 extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
return StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%');
}
@computed get sidebarColor() {
- return StrCast(this.layoutDoc.sidebarColor, StrCast(this.layoutDoc[this.props.fieldKey + '_backgroundColor'], '#e4e4e4'));
+ return StrCast(this.layoutDoc.sidebar_color, StrCast(this.layoutDoc[this.props.fieldKey + '_backgroundColor'], '#e4e4e4'));
}
/**
@@ -509,7 +510,7 @@ export class MapBox2 extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
};
pointerEvents = () => {
- return this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' && !MarqueeOptionsMenu.Instance.isShown() ? 'all' : SnappingManager.GetIsDragging() ? undefined : 'none';
+ return this.props.isContentActive() === false ? 'none' : this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' && !MarqueeOptionsMenu.Instance.isShown() ? 'all' : SnappingManager.GetIsDragging() ? undefined : 'none';
};
@computed get annotationLayer() {
return (
@@ -546,8 +547,8 @@ export class MapBox2 extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
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));
- transparentFilter = () => [...this.props.docFilters(), Utils.IsTransparentFilter()];
- opaqueFilter = () => [...this.props.docFilters(), Utils.IsOpaqueFilter()];
+ transparentFilter = () => [...this.props.childFilters(), Utils.IsTransparentFilter()];
+ opaqueFilter = () => [...this.props.childFilters(), Utils.IsOpaqueFilter()];
infoWidth = () => this.props.PanelWidth() / 5;
infoHeight = () => this.props.PanelHeight() / 5;
anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick;
@@ -557,7 +558,7 @@ export class MapBox2 extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
return (window as any).Microsoft.Maps;
}
render() {
- const renderAnnotations = (docFilters?: () => string[]) => null;
+ const renderAnnotations = (childFilters?: () => string[]) => null;
return (
<div className="MapBox2" ref={this._ref}>
<div
diff --git a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx
index 04cb12ade..66c47d131 100644
--- a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx
+++ b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx
@@ -61,7 +61,7 @@ export class MapBoxInfoWindow extends React.Component<MapBoxInfoWindowProps & Vi
fieldKey="data"
NativeWidth={returnZero}
NativeHeight={returnZero}
- docFilters={returnEmptyFilter}
+ childFilters={returnEmptyFilter}
setHeight={emptyFunction}
isAnnotationOverlay={false}
select={emptyFunction}
@@ -76,7 +76,7 @@ export class MapBoxInfoWindow extends React.Component<MapBoxInfoWindowProps & Vi
removeDocument={this.removeDoc}
addDocument={this.addDoc}
renderDepth={this.props.renderDepth + 1}
- viewType={CollectionViewType.Stacking}
+ type_collection={CollectionViewType.Stacking}
pointerEvents={returnAll}
/>
</div>
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index edeaba322..fd4c6366b 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -3,7 +3,8 @@ import { action, computed, IReactionDisposer, observable, reaction, runInAction
import { observer } from 'mobx-react';
import * as Pdfjs from 'pdfjs-dist';
import 'pdfjs-dist/web/pdf_viewer.css';
-import { Doc, DocListCast, HeightSym, Opt, WidthSym } from '../../../fields/Doc';
+import { Doc, DocListCast, Opt } from '../../../fields/Doc';
+import { Height, Width } from '../../../fields/DocSymbols';
import { Id } from '../../../fields/FieldSymbols';
import { InkTool } from '../../../fields/InkField';
import { ComputedField } from '../../../fields/ScriptField';
@@ -63,7 +64,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
super(props);
const nw = Doc.NativeWidth(this.Document, this.dataDoc) || 927;
const nh = Doc.NativeHeight(this.Document, this.dataDoc) || 1200;
- !this.Document._layout_fitWidth && (this.Document._height = this.Document[WidthSym]() * (nh / nw));
+ !this.Document._layout_fitWidth && (this.Document._height = this.Document[Width]() * (nh / nw));
if (this.pdfUrl) {
if (PDFBox.pdfcache.get(this.pdfUrl.url.href)) runInAction(() => (this._pdf = PDFBox.pdfcache.get(this.pdfUrl!.url.href)));
else if (PDFBox.pdfpromise.get(this.pdfUrl.url.href)) PDFBox.pdfpromise.get(this.pdfUrl.url.href)?.then(action((pdf: any) => (this._pdf = pdf)));
@@ -104,15 +105,15 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
const docViewContent = this.props.docViewPath().lastElement().ContentDiv!;
const newDiv = docViewContent.cloneNode(true) as HTMLDivElement;
- newDiv.style.width = this.layoutDoc[WidthSym]().toString();
- newDiv.style.height = this.layoutDoc[HeightSym]().toString();
+ newDiv.style.width = this.layoutDoc[Width]().toString();
+ newDiv.style.height = this.layoutDoc[Height]().toString();
this.replaceCanvases(docViewContent, newDiv);
const htmlString = this._pdfViewer?._mainCont.current && new XMLSerializer().serializeToString(newDiv);
const anchx = NumCast(cropping.x);
const anchy = NumCast(cropping.y);
- const anchw = cropping[WidthSym]() * (this.props.NativeDimScaling?.() || 1);
- const anchh = cropping[HeightSym]() * (this.props.NativeDimScaling?.() || 1);
+ const anchw = cropping[Width]() * (this.props.NativeDimScaling?.() || 1);
+ const anchh = cropping[Height]() * (this.props.NativeDimScaling?.() || 1);
const viewScale = 1;
cropping.title = 'crop: ' + this.rootDoc.title;
cropping.x = NumCast(this.rootDoc.x) + NumCast(this.rootDoc._width);
@@ -169,8 +170,8 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
CollectionFreeFormView.UpdateIcon(
filename,
docViewContent,
- this.layoutDoc[WidthSym](),
- this.layoutDoc[HeightSym](),
+ this.layoutDoc[Width](),
+ this.layoutDoc[Height](),
this.props.PanelWidth(),
this.props.PanelHeight(),
NumCast(this.layoutDoc._layout_scrollTop),
@@ -237,14 +238,11 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
ele = document.createElement('div');
ele.append(this._pdfViewer.selectionContent()!);
}
- const docAnchor = () => {
- const anchor = Docs.Create.TextanchorDocument({
+ const docAnchor = () =>
+ Docs.Create.ConfigDocument({
title: StrCast(this.rootDoc.title + '@' + NumCast(this.layoutDoc._layout_scrollTop)?.toFixed(0)),
- unrendered: true,
annotationOn: this.rootDoc,
});
- return anchor;
- };
const annoAnchor = this._pdfViewer?._getAnchor(this._pdfViewer.savedAnnotations(), true);
const anchor = annoAnchor ?? docAnchor();
PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), scrollable: true, pannable: true } }, this.rootDoc);
@@ -261,8 +259,8 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
this.dataDoc[this.props.fieldKey + '_numPages'] = np;
Doc.SetNativeWidth(this.dataDoc, Math.max(Doc.NativeWidth(this.dataDoc), (nw * 96) / 72));
Doc.SetNativeHeight(this.dataDoc, (nh * 96) / 72);
- this.layoutDoc._height = this.layoutDoc[WidthSym]() / (Doc.NativeAspect(this.dataDoc) || 1);
- !this.Document._layout_fitWidth && (this.Document._height = this.Document[WidthSym]() * (nh / nw));
+ this.layoutDoc._height = this.layoutDoc[Width]() / (Doc.NativeAspect(this.dataDoc) || 1);
+ !this.Document._layout_fitWidth && (this.Document._height = this.Document[Width]() * (nh / nw));
};
public search = action((searchString: string, bwd?: boolean, clear: boolean = false) => {
@@ -337,7 +335,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
const ratio = (curNativeWidth + ((onButton ? 1 : -1) * localDelta[0]) / (this.props.NativeDimScaling?.() || 1)) / nativeWidth;
if (ratio >= 1) {
this.layoutDoc.nativeWidth = nativeWidth * ratio;
- onButton && (this.layoutDoc._width = this.layoutDoc[WidthSym]() + localDelta[0]);
+ onButton && (this.layoutDoc._width = this.layoutDoc[Width]() + localDelta[0]);
this.layoutDoc._show_sidebar = nativeWidth !== this.layoutDoc._nativeWidth;
}
return false;
@@ -358,11 +356,11 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
const curNativeWidth = NumCast(this.layoutDoc.nativeWidth, nativeWidth);
if (preview) {
this._previewNativeWidth = nativeWidth * sideratio;
- this._previewWidth = (this.layoutDoc[WidthSym]() * nativeWidth * sideratio) / curNativeWidth;
+ this._previewWidth = (this.layoutDoc[Width]() * nativeWidth * sideratio) / curNativeWidth;
this._showSidebar = true;
} else {
this.layoutDoc.nativeWidth = nativeWidth * pdfratio;
- this.layoutDoc._width = (this.layoutDoc[WidthSym]() * nativeWidth * pdfratio) / curNativeWidth;
+ this.layoutDoc._width = (this.layoutDoc[Width]() * nativeWidth * pdfratio) / curNativeWidth;
this.layoutDoc._show_sidebar = nativeWidth !== this.layoutDoc._nativeWidth;
}
});
@@ -441,7 +439,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
return PDFBox.sidebarResizerWidth + nativeDiff * (this.props.NativeDimScaling?.() || 1);
};
@undoBatch
- toggleSidebarType = () => (this.rootDoc.sidebarViewType = this.rootDoc.sidebarViewType === CollectionViewType.Freeform ? CollectionViewType.Stacking : CollectionViewType.Freeform);
+ toggleSidebarType = () => (this.rootDoc.sidebar_collectionType = this.rootDoc.sidebar_collectionType === CollectionViewType.Freeform ? CollectionViewType.Stacking : CollectionViewType.Freeform);
specificContextMenu = (e: React.MouseEvent): void => {
const cm = ContextMenu.Instance;
const options = cm.findByDescription('Options...');
@@ -557,11 +555,10 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
};
return (
<div className={'formattedTextBox-sidebar' + (Doc.ActiveTool !== InkTool.None ? '-inking' : '')} style={{ width: '100%', right: 0, backgroundColor: `white` }}>
- {renderComponent(StrCast(this.layoutDoc.sidebarViewType))}
+ {renderComponent(StrCast(this.layoutDoc.sidebar_collectionType))}
</div>
);
}
- isPdfContentActive = () => this.isAnyChildContentActive() || this.props.isSelected() || (this.props.renderDepth === 0 && LightboxView.IsLightboxDocView(this.props.docViewPath()));
@computed get renderPdfView() {
TraceMobx();
const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1;
@@ -594,7 +591,6 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
pdf={this._pdf}
focus={this.focus}
url={this.pdfUrl!.url.pathname}
- isContentActive={this.isPdfContentActive}
anchorMenuClick={this.anchorMenuClick}
loaded={!Doc.NativeAspect(this.dataDoc) ? this.loaded : undefined}
setPdfViewer={this.setPdfViewer}
diff --git a/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.tsx b/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.tsx
index 2c06282ed..cd1ff17dd 100644
--- a/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.tsx
+++ b/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.tsx
@@ -1,11 +1,11 @@
-// import ArrowLeftIcon from '@mui/icons-material/ArrowLeft';
-// import ArrowRightIcon from '@mui/icons-material/ArrowRight';
-// import PauseIcon from '@mui/icons-material/Pause';
-// import PlayArrowIcon from '@mui/icons-material/PlayArrow';
-// import QuestionMarkIcon from '@mui/icons-material/QuestionMark';
-// import ReplayIcon from '@mui/icons-material/Replay';
-// import { Box, Button, Checkbox, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, FormControl, FormControlLabel, FormGroup, IconButton, LinearProgress, Stack } from '@mui/material';
-// import Typography from '@mui/material/Typography';
+import ArrowLeftIcon from '@mui/icons-material/ArrowLeft';
+import ArrowRightIcon from '@mui/icons-material/ArrowRight';
+import PauseIcon from '@mui/icons-material/Pause';
+import PlayArrowIcon from '@mui/icons-material/PlayArrow';
+import QuestionMarkIcon from '@mui/icons-material/QuestionMark';
+import ReplayIcon from '@mui/icons-material/Replay';
+import { Box, Button, Checkbox, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, FormControl, FormControlLabel, FormGroup, IconButton, LinearProgress, Stack } from '@mui/material';
+import Typography from '@mui/material/Typography';
import { action, computed, IReactionDisposer, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import { NumListCast } from '../../../../fields/Doc';
@@ -928,12 +928,12 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
<Stack direction="row" spacing={1}>
{this.dataDoc.simulation_paused && this.simulationMode != 'Tutorial' && (
<IconButton onClick={() => (this.dataDoc.simulation_paused = false)}>
- {/* <PlayArrowIcon /> */}
+ <PlayArrowIcon />
</IconButton>
)}
{!this.dataDoc.simulation_paused && this.simulationMode != 'Tutorial' && (
<IconButton onClick={() => (this.dataDoc.simulation_paused = true)}>
- {/* <PauseIcon /> */}
+ <PauseIcon />
</IconButton>
)}
{this.dataDoc.simulation_paused && this.simulationMode != 'Tutorial' && (
@@ -1180,7 +1180,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
this.dataDoc.simulation_showForceMagnitudes = this.tutorial.steps[step].showMagnitude;
}}
disabled={this.dataDoc.tutorial_stepNumber == 0}>
- {/* <ArrowLeftIcon /> */}
+ <ArrowLeftIcon />
</IconButton>
<div>
<h3>
@@ -1199,7 +1199,7 @@ export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewP
this.dataDoc.simulation_showForceMagnitudes = this.tutorial.steps[step].showMagnitude;
}}
disabled={this.dataDoc.tutorial_stepNumber === this.tutorial.steps.length - 1}>
- {/* <ArrowRightIcon /> */}
+ <ArrowRightIcon />
</IconButton>
</div>
<div>
diff --git a/src/client/views/nodes/QueryBox.scss b/src/client/views/nodes/QueryBox.scss
deleted file mode 100644
index b5f90aa1e..000000000
--- a/src/client/views/nodes/QueryBox.scss
+++ /dev/null
@@ -1,5 +0,0 @@
-.queryBox, .queryBox-dragging {
- width: 100%;
- height: 100%;
- position: absolute;
-} \ No newline at end of file
diff --git a/src/client/views/nodes/QueryBox.tsx b/src/client/views/nodes/QueryBox.tsx
deleted file mode 100644
index 1b6056be6..000000000
--- a/src/client/views/nodes/QueryBox.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-// import React = require("react");
-// import { IReactionDisposer } from "mobx";
-// import { observer } from "mobx-react";
-// import { documentSchema } from "../../../new_fields/documentSchemas";
-// import { Id } from '../../../new_fields/FieldSymbols';
-// import { makeInterface, listSpec } from "../../../new_fields/Schema";
-// import { StrCast, Cast } from "../../../new_fields/Types";
-// import { ViewBoxAnnotatableComponent } from '../DocComponent';
-// import { SearchBox } from "../search/SearchBox";
-// import { FieldView, FieldViewProps } from './FieldView';
-// import "./QueryBox.scss";
-// import { List } from "../../../new_fields/List";
-// import { SnappingManager } from "../../util/SnappingManager";
-
-// type QueryDocument = makeInterface<[typeof documentSchema]>;
-// const QueryDocument = makeInterface(documentSchema);
-
-// @observer
-// export class QueryBox extends ViewBoxAnnotatableComponent<FieldViewProps, QueryDocument>(QueryDocument) {
-// public static LayoutString(fieldKey: string) { return FieldView.LayoutString(QueryBox, fieldKey); }
-// _docListChangedReaction: IReactionDisposer | undefined;
-// componentDidMount() {
-// }
-
-// componentWillUnmount() {
-// this._docListChangedReaction?.();
-// }
-
-// render() {
-// const dragging = !SnappingManager.GetIsDragging() ? "" : "-dragging";
-// return <div className={`queryBox${dragging}`} onWheel={(e) => e.stopPropagation()} >
-
-// <SearchBox Document={this.props.Document} />
-// </div >;
-// }
-// }
-
-// //<SearchBox id={this.props.Document[Id]} sideBar={side} Document={this.props.Document} searchQuery={StrCast(this.dataDoc.searchQuery)} filterQuery={this.dataDoc.filterQuery} />
diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
index 80b12b96e..04f11a5df 100644
--- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
@@ -11,6 +11,7 @@ import { DocumentType } from '../../../documents/DocumentTypes';
import { Presentation } from '../../../util/TrackMovements';
import { Doc } from '../../../../fields/Doc';
import { Id } from '../../../../fields/FieldSymbols';
+import { DocCast } from '../../../../fields/Types';
@observer
export class RecordingBox extends ViewBoxBaseComponent() {
@@ -57,7 +58,7 @@ export class RecordingBox extends ViewBoxBaseComponent() {
render() {
return (
<div className="recordingBox" ref={this._ref}>
- {!this.result && <RecordingView setResult={this.setResult} setDuration={this.setVideoDuration} id={this.rootDoc.proto?.[Id] || ''} />}
+ {!this.result && <RecordingView setResult={this.setResult} setDuration={this.setVideoDuration} id={DocCast(this.rootDoc.proto)?.[Id] || ''} />}
</div>
);
}
diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx
index 424ebc384..51eb774e2 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx
@@ -67,7 +67,7 @@ export function RecordingView(props: IRecordingViewProps) {
const videoFiles = videos.map((vid, i) => new File(vid.videoChunks, `segvideo${i}.mkv`, { type: vid.videoChunks[0].type, lastModified: Date.now() }));
// upload the segments to the server and get their server access paths
- const serverPaths: string[] = (await Networking.UploadFilesToServer(videoFiles)).map(res => (res.result instanceof Error ? '' : res.result.accessPaths.agnostic.server));
+ const serverPaths: string[] = (await Networking.UploadFilesToServer(videoFiles.map(file => ({file})))).map(res => (res.result instanceof Error ? '' : res.result.accessPaths.agnostic.server));
// concat the segments together using post call
const result: Upload.AccessPathInfo | Error = await Networking.PostToServer('/concatVideos', serverPaths);
diff --git a/src/client/views/nodes/ScreenshotBox.scss b/src/client/views/nodes/ScreenshotBox.scss
index 6fb5ea7b3..1e9b64a0b 100644
--- a/src/client/views/nodes/ScreenshotBox.scss
+++ b/src/client/views/nodes/ScreenshotBox.scss
@@ -1,6 +1,5 @@
.screenshotBox {
transform-origin: top left;
- background: white;
color: black;
// .screenshotBox-viewer {
// opacity: 0.99; // hack! overcomes some kind of Chrome weirdness where buttons (e.g., snapshot) disappear at some point as the video is resized larger
@@ -12,46 +11,39 @@
#CANCAN {
canvas {
- width:100% !important;
+ width: 100% !important;
height: 100% !important;
}
}
-.screenshotBox-content, .screenshotBox-content-interactive, .screenshotBox-cont-fullScreen {
+.screenshotBox-content,
+.screenshotBox-content-interactive,
+.screenshotBox-cont-fullScreen {
width: 100%;
z-index: -1; // 0; // logically this should be 0 (or unset) which would give us transparent brush strokes over videos. However, this makes Chrome crawl to a halt
position: absolute;
}
-.screenshotBox-content, .screenshotBox-content-interactive, .screenshotBox-content-fullScreen {
- height: Auto;
+.screenshotBox-content,
+.screenshotBox-content-interactive,
+.screenshotBox-content-fullScreen {
+ height: Auto;
}
.screenshotBox-uiButtons {
- background:dimgray;
- border: orange solid 1px;
position: absolute;
right: 25;
top: 0;
- width:25;
+ width: 22;
height: 25;
- .screenshotBox-snapshot{
- color : white;
- top :0px;
- right : 5px;
- position: absolute;
- background-color:rgba(50, 50, 50, 0.2);
- transform-origin: left top;
- pointer-events:all;
- }
- .screenshotBox-recorder{
- color : white;
- top :0px;
+ .screenshotBox-recorder {
+ color: white;
+ top: 4px;
left: 5px;
position: absolute;
- background-color:rgba(50, 50, 50, 0.2);
+ background-color: rgba(50, 50, 50, 0.2);
transform-origin: left top;
- pointer-events:all;
+ pointer-events: all;
}
}
diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx
index 7bf765042..271ff3cf8 100644
--- a/src/client/views/nodes/ScreenshotBox.tsx
+++ b/src/client/views/nodes/ScreenshotBox.tsx
@@ -5,10 +5,11 @@ import { computed, observable, runInAction } from 'mobx';
import { observer } from 'mobx-react';
// import { BufferAttribute, Camera, Vector2, Vector3 } from 'three';
import { DateField } from '../../../fields/DateField';
-import { Doc, HeightSym, WidthSym } from '../../../fields/Doc';
+import { Doc } from '../../../fields/Doc';
+import { Height, Width } from '../../../fields/DocSymbols';
import { Id } from '../../../fields/FieldSymbols';
import { ComputedField } from '../../../fields/ScriptField';
-import { Cast, NumCast } from '../../../fields/Types';
+import { Cast, DocCast, NumCast } from '../../../fields/Types';
import { AudioField, VideoField } from '../../../fields/URLField';
import { TraceMobx } from '../../../fields/util';
import { emptyFunction, returnFalse, returnOne, returnZero } from '../../../Utils';
@@ -24,6 +25,8 @@ import { FieldView, FieldViewProps } from './FieldView';
import { FormattedTextBox } from './formattedText/FormattedTextBox';
import './ScreenshotBox.scss';
import { VideoBox } from './VideoBox';
+import { SettingsManager } from '../../util/SettingsManager';
+
declare class MediaRecorder {
constructor(e: any, options?: any); // whatever MediaRecorder has
}
@@ -140,7 +143,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
if (!nativeWidth || !nativeHeight) {
if (!nativeWidth) Doc.SetNativeWidth(this.dataDoc, 1200);
Doc.SetNativeHeight(this.dataDoc, (nativeWidth || 1200) / aspect);
- this.layoutDoc._height = (this.layoutDoc[WidthSym]() || 0) / aspect;
+ this.layoutDoc._height = (this.layoutDoc[Width]() || 0) / aspect;
}
};
@@ -222,7 +225,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
const aud_chunks: any = [];
this._audioRec.ondataavailable = (e: any) => aud_chunks.push(e.data);
this._audioRec.onstop = async (e: any) => {
- const [{ result }] = await Networking.UploadFilesToServer(aud_chunks);
+ const [{ result }] = await Networking.UploadFilesToServer(aud_chunks.map((file: any) => ({ file })));
if (!(result instanceof Error)) {
this.dataDoc[this.props.fieldKey + '-audio'] = new AudioField(result.accessPaths.agnostic.client);
}
@@ -233,9 +236,8 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
this._videoRec.onstart = () => (this.dataDoc[this.props.fieldKey + '-recordingStart'] = new DateField(new Date()));
this._videoRec.ondataavailable = (e: any) => vid_chunks.push(e.data);
this._videoRec.onstop = async (e: any) => {
- console.log('screenshotbox: upload');
const file = new File(vid_chunks, `${this.rootDoc[Id]}.mkv`, { type: vid_chunks[0].type, lastModified: Date.now() });
- const [{ result }] = await Networking.UploadFilesToServer(file);
+ const [{ result }] = await Networking.UploadFilesToServer({ file });
this.dataDoc[this.fieldKey + '_duration'] = (new Date().getTime() - this.recordingStart!) / 1000;
if (!(result instanceof Error)) {
// convert this screenshotBox into normal videoBox
@@ -277,7 +279,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
dictationTextProto.mediaState = ComputedField.MakeFunction('self.recordingSource.mediaState');
this.dataDoc[this.fieldKey + '-dictation'] = dictationText;
};
- videoPanelHeight = () => (NumCast(this.dataDoc[this.fieldKey + '_nativeHeight'], this.layoutDoc[HeightSym]()) / NumCast(this.dataDoc[this.fieldKey + '_nativeWidth'], this.layoutDoc[WidthSym]())) * this.props.PanelWidth();
+ videoPanelHeight = () => (NumCast(this.dataDoc[this.fieldKey + '_nativeHeight'], this.layoutDoc[Height]()) / NumCast(this.dataDoc[this.fieldKey + '_nativeWidth'], this.layoutDoc[Width]())) * this.props.PanelWidth();
formattedPanelHeight = () => Math.max(0, this.props.PanelHeight() - this.videoPanelHeight());
render() {
TraceMobx();
@@ -311,11 +313,11 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
</>
</CollectionFreeFormView>
</div>
- <div style={{ background: 'white', position: 'relative', height: this.formattedPanelHeight() }}>
+ <div style={{ background: SettingsManager.Instance.userColor, position: 'relative', height: this.formattedPanelHeight() }}>
{!(this.dataDoc[this.fieldKey + '-dictation'] instanceof Doc) ? null : (
<FormattedTextBox
{...this.props}
- Document={this.dataDoc[this.fieldKey + '-dictation']}
+ Document={DocCast(this.dataDoc[this.fieldKey + '-dictation'])}
fieldKey={'text'}
PanelHeight={this.formattedPanelHeight}
select={emptyFunction}
@@ -333,8 +335,8 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
</div>
</div>
{!this.props.isSelected() ? null : (
- <div className="screenshotBox-uiButtons">
- <div className="screenshotBox-recorder" key="snap" onPointerDown={this.toggleRecording}>
+ <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}>
<FontAwesomeIcon icon="file" size="lg" />
</div>
</div>
diff --git a/src/client/views/nodes/ScriptingBox.tsx b/src/client/views/nodes/ScriptingBox.tsx
index 37fda14fc..7c8a1849e 100644
--- a/src/client/views/nodes/ScriptingBox.tsx
+++ b/src/client/views/nodes/ScriptingBox.tsx
@@ -270,8 +270,12 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable
// sets field of the corresponding field key (param name) to be dropped document
@action
onDrop = (e: Event, de: DragManager.DropEvent, fieldKey: string) => {
- Doc.SetInPlace(this.rootDoc, fieldKey, de.complete.docDragData?.droppedDocuments[0], true);
- e.stopPropagation();
+ if (de.complete.docDragData) {
+ de.complete.docDragData.droppedDocuments.forEach(doc => Doc.SetInPlace(this.rootDoc, fieldKey, doc, true));
+ e.stopPropagation();
+ return true;
+ }
+ return false;
};
// deletes a param from all areas in which it is stored
@@ -606,7 +610,6 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable
' ': {
dataProvider: (token: any) => this.handleToken(token),
component: (blob: any) => {
- console.log('Blob', blob);
return this.renderFuncListElement(blob.entity);
},
output: (item: any, trigger: any) => {
@@ -617,7 +620,6 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable
'.': {
dataProvider: (token: any) => this.handleToken(token),
component: (blob: any) => {
- console.log('Blob', blob);
return this.renderFuncListElement(blob.entity);
},
output: (item: any, trigger: any) => {
diff --git a/src/client/views/nodes/SliderBox.tsx b/src/client/views/nodes/SliderBox.tsx
index b96977f32..430b20eb5 100644
--- a/src/client/views/nodes/SliderBox.tsx
+++ b/src/client/views/nodes/SliderBox.tsx
@@ -12,50 +12,56 @@ import { FieldView, FieldViewProps } from './FieldView';
import { Handle, Tick, TooltipRail, Track } from './SliderBox-components';
import './SliderBox.scss';
-
@observer
export class SliderBox extends ViewBoxBaseComponent<FieldViewProps>() {
- public static LayoutString(fieldKey: string) { return FieldView.LayoutString(SliderBox, fieldKey); }
+ public static LayoutString(fieldKey: string) {
+ return FieldView.LayoutString(SliderBox, fieldKey);
+ }
- get minThumbKey() { return this.fieldKey + "-minThumb"; }
- get maxThumbKey() { return this.fieldKey + "-maxThumb"; }
- get minKey() { return this.fieldKey + "-min"; }
- get maxKey() { return this.fieldKey + "-max"; }
+ get minThumbKey() {
+ return this.fieldKey + '-minThumb';
+ }
+ get maxThumbKey() {
+ return this.fieldKey + '-maxThumb';
+ }
+ get minKey() {
+ return this.fieldKey + '-min';
+ }
+ get maxKey() {
+ return this.fieldKey + '-max';
+ }
specificContextMenu = (e: React.MouseEvent): void => {
const funcs: ContextMenuProps[] = [];
- funcs.push({ description: "Edit Thumb Change Script", icon: "edit", event: (obj: any) => ScriptBox.EditButtonScript("On Thumb Change ...", this.props.Document, "onThumbChange", obj.x, obj.y) });
- ContextMenu.Instance.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" });
- }
- onChange = (values: readonly number[]) => runInAction(() => {
- this.dataDoc[this.minThumbKey] = values[0];
- this.dataDoc[this.maxThumbKey] = values[1];
- ScriptCast(this.layoutDoc.onThumbChanged, null)?.script.run({
- self: this.rootDoc,
- scriptContext: this.props.scriptContext, range: values, this: this.layoutDoc
+ funcs.push({ description: 'Edit Thumb Change Script', icon: 'edit', event: (obj: any) => ScriptBox.EditButtonScript('On Thumb Change ...', this.props.Document, 'onThumbChange', obj.x, obj.y) });
+ ContextMenu.Instance.addItem({ description: 'Options...', subitems: funcs, icon: 'asterisk' });
+ };
+ onChange = (values: readonly number[]) =>
+ runInAction(() => {
+ this.dataDoc[this.minThumbKey] = values[0];
+ this.dataDoc[this.maxThumbKey] = values[1];
+ ScriptCast(this.layoutDoc.onThumbChanged, null)?.script.run({
+ self: this.rootDoc,
+ scriptContext: this.props.scriptContext,
+ range: values,
+ this: this.layoutDoc,
+ });
});
- })
render() {
const domain = [NumCast(this.layoutDoc[this.minKey]), NumCast(this.layoutDoc[this.maxKey])];
const defaultValues = [NumCast(this.dataDoc[this.minThumbKey]), NumCast(this.dataDoc[this.maxThumbKey])];
- return domain[1] <= domain[0] ? (null) : (
- <div className="sliderBox-outerDiv" onContextMenu={this.specificContextMenu} onPointerDown={e => e.stopPropagation()}
- style={{ boxShadow: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BoxShadow) }}>
- <div className="sliderBox-mainButton"
- onContextMenu={this.specificContextMenu} style={{
+ return domain[1] <= domain[0] ? null : (
+ <div className="sliderBox-outerDiv" onContextMenu={this.specificContextMenu} onPointerDown={e => e.stopPropagation()} style={{ boxShadow: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BoxShadow) }}>
+ <div
+ className="sliderBox-mainButton"
+ onContextMenu={this.specificContextMenu}
+ style={{
background: StrCast(this.layoutDoc.backgroundColor),
- color: StrCast(this.layoutDoc.color, "black"),
- fontSize: StrCast(this.layoutDoc._fontSize), letterSpacing: StrCast(this.layoutDoc.letterSpacing)
- }} >
- <Slider
- mode={2}
- step={Math.min(1, .1 * (domain[1] - domain[0]))}
- domain={domain}
- rootStyle={{ position: "relative", width: "100%" }}
- onChange={this.onChange}
- values={defaultValues}
- >
-
+ color: StrCast(this.layoutDoc.color, 'black'),
+ fontSize: StrCast(this.layoutDoc._text_fontSize),
+ letterSpacing: StrCast(this.layoutDoc.letterSpacing),
+ }}>
+ <Slider mode={2} step={Math.min(1, 0.1 * (domain[1] - domain[0]))} domain={domain} rootStyle={{ position: 'relative', width: '100%' }} onChange={this.onChange} values={defaultValues}>
<Rail>{railProps => <TooltipRail {...railProps} />}</Rail>
<Handles>
{({ handles, activeHandleID, getHandleProps }) => (
@@ -64,13 +70,7 @@ export class SliderBox extends ViewBoxBaseComponent<FieldViewProps>() {
const value = i === 0 ? defaultValues[0] : defaultValues[1];
return (
<div title={String(value)}>
- <Handle
- key={handle.id}
- handle={handle}
- domain={domain}
- isActive={handle.id === activeHandleID}
- getHandleProps={getHandleProps}
- />
+ <Handle key={handle.id} handle={handle} domain={domain} isActive={handle.id === activeHandleID} getHandleProps={getHandleProps} />
</div>
);
})}
@@ -81,13 +81,7 @@ export class SliderBox extends ViewBoxBaseComponent<FieldViewProps>() {
{({ tracks, getTrackProps }) => (
<div className="slider-tracks">
{tracks.map(({ id, source, target }) => (
- <Track
- key={id}
- source={source}
- target={target}
- disabled={false}
- getTrackProps={getTrackProps}
- />
+ <Track key={id} source={source} target={target} disabled={false} getTrackProps={getTrackProps} />
))}
</div>
)}
@@ -95,13 +89,8 @@ export class SliderBox extends ViewBoxBaseComponent<FieldViewProps>() {
<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()}
- />
+ {ticks.map(tick => (
+ <Tick key={tick.id} tick={tick} count={ticks.length} format={(val: number) => val.toString()} />
))}
</div>
)}
@@ -111,4 +100,4 @@ export class SliderBox extends ViewBoxBaseComponent<FieldViewProps>() {
</div>
);
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index e00cb8618..1f52c2d92 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -3,7 +3,8 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction, untracked } from 'mobx';
import { observer } from 'mobx-react';
import { basename } from 'path';
-import { Doc, HeightSym, StrListCast, WidthSym } from '../../../fields/Doc';
+import { Doc, StrListCast } from '../../../fields/Doc';
+import { Height, Width } from '../../../fields/DocSymbols';
import { InkTool } from '../../../fields/InkField';
import { List } from '../../../fields/List';
import { ObjectField } from '../../../fields/ObjectField';
@@ -14,6 +15,7 @@ import { Docs, DocUtils } from '../../documents/Documents';
import { DocumentType } from '../../documents/DocumentTypes';
import { Networking } from '../../Network';
import { DocumentManager } from '../../util/DocumentManager';
+import { FollowLinkScript } from '../../util/LinkFollower';
import { LinkManager } from '../../util/LinkManager';
import { ReplayMovements } from '../../util/ReplayMovements';
import { SelectionManager } from '../../util/SelectionManager';
@@ -33,8 +35,6 @@ import { FieldView, FieldViewProps } from './FieldView';
import { RecordingBox } from './RecordingBox';
import { PinProps, PresBox } from './trails';
import './VideoBox.scss';
-import { ScriptField } from '../../../fields/ScriptField';
-import { FollowLinkScript } from '../../util/LinkFollower';
const path = require('path');
/**
@@ -111,7 +111,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
// returns the presentation data if it exists, null otherwise
@computed get presentation() {
const data = this.dataDoc[this.fieldKey + '-presentation'];
- return data ? JSON.parse(data) : null;
+ return data ? JSON.parse(StrCast(data)) : null;
}
@computed private get timeline() {
@@ -354,8 +354,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
updateIcon = () => {
const makeIcon = (returnedfilename: string) => {
this.dataDoc.icon = new ImageField(returnedfilename);
- this.dataDoc.icon_nativeWidth = this.layoutDoc[WidthSym]();
- this.dataDoc.icon_nativeHeight = this.layoutDoc[HeightSym]();
+ this.dataDoc.icon_nativeWidth = this.layoutDoc[Width]();
+ this.dataDoc.icon_nativeHeight = this.layoutDoc[Height]();
};
this.Snapshot(undefined, undefined, makeIcon);
};
@@ -465,7 +465,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
{ fireImmediately: true }
);
- (!this.dataDoc[this.fieldKey + '_thumbnails'] || this.dataDoc[this.fieldKey + '_thumbnails'].length != VideoBox.numThumbnails) && this.getVideoThumbnails();
+ (!this.dataDoc[this.fieldKey + '_thumbnails'] || StrListCast(this.dataDoc[this.fieldKey + '_thumbnails']).length != VideoBox.numThumbnails) && this.getVideoThumbnails();
}
};
@@ -870,6 +870,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
}),
returnFalse,
() => MarqueeAnnotator.clearAnnotations(this._savedAnnotations),
+ false,
false
);
}
@@ -912,7 +913,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
marqueeFitScaling = () => ((this.props.NativeDimScaling?.() || 1) * this.heightPercent) / 100;
marqueeOffset = () => [((this.panelWidth() / 2) * (1 - this.heightPercent / 100)) / (this.heightPercent / 100), 0];
- timelineDocFilter = () => [`_timelineLabel:true,${Utils.noRecursionHack}:x`];
+ timelineDocFilter = () => [`_isTimelineLabel:true,${Utils.noRecursionHack}:x`];
// renders video controls
componentUI = (boundsLeft: number, boundsTop: number) => {
@@ -1026,13 +1027,13 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
croppingProto.videoCrop = true;
croppingProto.layout_currentTimecode = this.layoutDoc._layout_currentTimecode;
croppingProto.freeform_scale = viewScale;
- croppingProto.freeform_scaleMin = viewScale;
+ croppingProto.freeform_scale_min = viewScale;
croppingProto.freeform_ = anchx / viewScale;
croppingProto.freeform_panY = anchy / viewScale;
- croppingProto.freeform_panXMin = anchx / viewScale;
- croppingProto.freeform_panXMax = anchw / viewScale;
- croppingProto.freeform_panYMin = anchy / viewScale;
- croppingProto.freeform_panYMax = anchh / viewScale;
+ croppingProto.freeform_panX_min = anchx / viewScale;
+ croppingProto.freeform_panX_max = anchw / viewScale;
+ croppingProto.freeform_panY_min = anchy / viewScale;
+ croppingProto.freeform_panY_max = anchh / viewScale;
if (addCrop) {
DocUtils.MakeLink(region, cropping, { link_relationship: 'cropped image' });
}
@@ -1081,8 +1082,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
PanelHeight={this.panelHeight}
isAnyChildContentActive={returnFalse}
ScreenToLocalTransform={this.screenToLocalTransform}
- docFilters={this.timelineDocFilter}
+ childFilters={this.timelineDocFilter}
select={emptyFunction}
+ focus={emptyFunction}
NativeDimScaling={returnOne}
whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
removeDocument={this.removeDocument}
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 3da5d8f17..f5df42161 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -2,16 +2,18 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as WebRequest from 'web-request';
-import { Doc, DocListCast, Field, HeightSym, Opt, WidthSym } from '../../../fields/Doc';
+import { Doc, DocListCast, Field, Opt } from '../../../fields/Doc';
+import { Height, Width } from '../../../fields/DocSymbols';
import { Id } from '../../../fields/FieldSymbols';
import { HtmlField } from '../../../fields/HtmlField';
import { InkTool } from '../../../fields/InkField';
import { List } from '../../../fields/List';
+import { RefField } from '../../../fields/RefField';
import { listSpec } from '../../../fields/Schema';
import { Cast, ImageCast, NumCast, StrCast, WebCast } from '../../../fields/Types';
import { ImageField, WebField } from '../../../fields/URLField';
import { TraceMobx } from '../../../fields/util';
-import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, getWordAtPoint, removeStyleSheetRule, returnFalse, returnOne, returnTrue, returnZero, setupMoveUpEvents, smoothScroll, StopEvent, Utils } from '../../../Utils';
+import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, getWordAtPoint, returnFalse, returnOne, returnZero, setupMoveUpEvents, smoothScroll, Utils } from '../../../Utils';
import { Docs, DocUtils } from '../../documents/Documents';
import { DocumentManager } from '../../util/DocumentManager';
import { DragManager } from '../../util/DragManager';
@@ -37,7 +39,6 @@ import { LinkDocPreview } from './LinkDocPreview';
import { PinProps, PresBox } from './trails';
import './WebBox.scss';
import React = require('react');
-import { RefField } from '../../../fields/RefField';
const { CreateImage } = require('./WebBoxRenderer');
const _global = (window /* browser */ || global) /* node */ as any;
const htmlToText = require('html-to-text');
@@ -84,7 +85,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
return this._url ? WebBox.urlHash(this._url) + '' : '';
}
@computed get scrollHeight() {
- return Math.max(this.layoutDoc[HeightSym](), this._scrollHeight);
+ return Math.max(this.layoutDoc[Height](), this._scrollHeight);
}
@computed get allAnnotations() {
return DocListCast(this.dataDoc[this.annotationKey]);
@@ -124,11 +125,15 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
this._searchRef.current?.setRangeText(searchString);
});
}
- if (clear) {
- this._iframe?.contentWindow?.getSelection()?.empty();
- }
- if (searchString) {
- (this._iframe?.contentWindow as any)?.find(searchString, false, bwd, true);
+ try {
+ if (clear) {
+ this._iframe?.contentWindow?.getSelection()?.empty();
+ }
+ if (searchString) {
+ (this._iframe?.contentWindow as any)?.find(searchString, false, bwd, true);
+ }
+ } catch (e) {
+ console.log("WebBox search error", e)
}
return true;
};
@@ -214,7 +219,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
if (!nativeWidth || !nativeHeight || Math.abs(nativeWidth / nativeHeight - youtubeaspect) > 0.05) {
if (!nativeWidth) Doc.SetNativeWidth(this.layoutDoc, 600);
Doc.SetNativeHeight(this.layoutDoc, (nativeWidth || 600) / youtubeaspect);
- this.layoutDoc._height = this.layoutDoc[WidthSym]() / youtubeaspect;
+ this.layoutDoc._height = this.layoutDoc[Width]() / youtubeaspect;
}
} // else it's an HTMLfield
} else if (this.webField && !this.dataDoc.text) {
@@ -281,7 +286,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
focus = (anchor: Doc, options: DocFocusOptions) => {
if (anchor !== this.rootDoc && this._outerRef.current) {
const windowHeight = this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1);
- const scrollTo = Utils.scrollIntoView(NumCast(anchor.y), anchor[HeightSym](), NumCast(this.layoutDoc._layout_scrollTop), windowHeight, windowHeight * 0.1, Math.max(NumCast(anchor.y) + anchor[HeightSym](), this._scrollHeight));
+ const scrollTo = Utils.scrollIntoView(NumCast(anchor.y), anchor[Height](), NumCast(this.layoutDoc._layout_scrollTop), windowHeight, windowHeight * 0.1, Math.max(NumCast(anchor.y) + anchor[Height](), this._scrollHeight));
if (scrollTo !== undefined) {
if (this._initialScroll === undefined) {
const focusTime = options.zoomTime ?? 500;
@@ -296,8 +301,10 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
@action
getView = (doc: Doc) => {
+ if (Doc.AreProtosEqual(doc, this.rootDoc)) return new Promise<Opt<DocumentView>>(res => res(this.props.DocumentView?.()));
if (this.rootDoc.layout_fieldKey === 'layout_icon') this.props.DocumentView?.().iconify();
- if (this._url && WebCast(doc.presData).url.href !== this._url) this.setData(WebCast(doc.presData).url.href);
+ const webUrl = WebCast(doc.presData)?.url;
+ if (this._url && webUrl && webUrl.href !== this._url) this.setData(webUrl.href);
if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) this.toggleSidebar(false);
return new Promise<Opt<DocumentView>>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv)));
};
@@ -320,10 +327,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
} catch (e) {}
const anchor =
this._getAnchor(this._savedAnnotations, false) ??
- Docs.Create.WebanchorDocument({
+ Docs.Create.ConfigDocument({
title: StrCast(this.rootDoc.title + ' ' + this.layoutDoc._layout_scrollTop),
y: NumCast(this.layoutDoc._layout_scrollTop),
- unrendered: true,
annotationOn: this.rootDoc,
});
PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), scrollable: pinProps?.pinData ? true : false, pannable: true } }, this.rootDoc);
@@ -459,7 +465,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
this._scrollHeight = Math.max(this._scrollHeight, (iframeContent.body.children[0] as any)?.scrollHeight || 0);
if (this._scrollHeight) {
this.rootDoc.nativeHeight = Math.min(NumCast(this.rootDoc.nativeHeight), this._scrollHeight);
- this.layoutDoc.height = Math.min(this.layoutDoc[HeightSym](), (this.layoutDoc[WidthSym]() * NumCast(this.rootDoc.nativeHeight)) / NumCast(this.rootDoc.nativeWidth));
+ this.layoutDoc.height = Math.min(this.layoutDoc[Height](), (this.layoutDoc[Width]() * NumCast(this.rootDoc.nativeHeight)) / NumCast(this.rootDoc.nativeWidth));
}
};
initHeights();
@@ -472,13 +478,13 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
'click',
undoBatch(
action((e: MouseEvent) => {
- const batch = UndoManager.StartBatch('webclick');
let href = '';
for (let ele = e.target as any; ele; ele = ele.parentElement) {
href = (typeof ele.href === 'string' ? ele.href : ele.href?.baseVal) || ele.parentElement?.href || href;
}
const origin = this.webField?.origin;
if (href && origin) {
+ const batch = UndoManager.StartBatch('webclick');
e.stopPropagation();
setTimeout(() => {
this.setData(href.replace(Utils.prepend(''), origin));
@@ -509,7 +515,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
if (this._innerCollectionView) {
this._innerCollectionView.zoom(e.screenX, e.screenY, e.deltaY);
const offset = e.clientY - NumCast(this.layoutDoc._layout_scrollTop);
- this.layoutDoc.panY = offset - offset / NumCast(this.layoutDoc._freeform_scale) + NumCast(this.layoutDoc._layout_scrollTop) - NumCast(this.layoutDoc._layout_scrollTop) / NumCast(this.layoutDoc._freeform_scale);
+ this.layoutDoc.freeform_panY = offset - offset / NumCast(this.layoutDoc._freeform_scale) + NumCast(this.layoutDoc._layout_scrollTop) - NumCast(this.layoutDoc._layout_scrollTop) / NumCast(this.layoutDoc._freeform_scale);
}
e.preventDefault();
}
@@ -744,6 +750,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
ref={action((r: HTMLIFrameElement | null) => (this._iframe = r))}
src={url}
onLoad={this.iframeLoaded}
+ scrolling="no" // ugh.. on windows, I get an inner scroll bar for the iframe's body even though the scrollHeight should be set to the full height of the document.
// the 'allow-top-navigation' and 'allow-top-navigation-by-user-activation' attributes are left out to prevent iframes from redirecting the top-level Dash page
// sandbox={"allow-forms allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin allow-scripts"} />;
sandbox={`${this.layoutDoc.allowScripts ? 'allow-scripts' : ''} allow-forms allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin`}
@@ -782,7 +789,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
if (ratio >= 1) {
this.layoutDoc.nativeWidth = nativeWidth * ratio;
this.layoutDoc.nativeHeight = nativeHeight * (1 + ratio);
- onButton && (this.layoutDoc._width = this.layoutDoc[WidthSym]() + localDelta[0]);
+ onButton && (this.layoutDoc._width = this.layoutDoc[Width]() + localDelta[0]);
this.layoutDoc._layout_showSidebar = nativeWidth !== this.layoutDoc._nativeWidth;
}
return false;
@@ -818,9 +825,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
toggleSidebar = action((preview: boolean = false) => {
var nativeWidth = NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth']);
if (!nativeWidth) {
- const defaultNativeWidth = this.rootDoc[this.fieldKey] instanceof WebField ? 850 : this.Document[WidthSym]();
+ const defaultNativeWidth = this.rootDoc[this.fieldKey] instanceof WebField ? 850 : this.Document[Width]();
Doc.SetNativeWidth(this.dataDoc, Doc.NativeWidth(this.dataDoc) || defaultNativeWidth);
- Doc.SetNativeHeight(this.dataDoc, Doc.NativeHeight(this.dataDoc) || (this.Document[HeightSym]() / this.Document[WidthSym]()) * defaultNativeWidth);
+ Doc.SetNativeHeight(this.dataDoc, Doc.NativeHeight(this.dataDoc) || (this.Document[Height]() / this.Document[Width]()) * defaultNativeWidth);
nativeWidth = NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth']);
}
const sideratio = ((!this.layoutDoc.nativeWidth || this.layoutDoc.nativeWidth === nativeWidth ? WebBox.openSidebarWidth : 0) + nativeWidth) / nativeWidth;
@@ -828,11 +835,11 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
const curNativeWidth = NumCast(this.layoutDoc.nativeWidth, nativeWidth);
if (preview) {
this._previewNativeWidth = nativeWidth * sideratio;
- this._previewWidth = (this.layoutDoc[WidthSym]() * nativeWidth * sideratio) / curNativeWidth;
+ this._previewWidth = (this.layoutDoc[Width]() * nativeWidth * sideratio) / curNativeWidth;
this._showSidebar = true;
} else {
this.layoutDoc._layout_showSidebar = !this.layoutDoc._layout_showSidebar;
- this.layoutDoc._width = (this.layoutDoc[WidthSym]() * nativeWidth * pdfratio) / curNativeWidth;
+ this.layoutDoc._width = (this.layoutDoc[Width]() * nativeWidth * pdfratio) / curNativeWidth;
if (!this.layoutDoc._layout_showSidebar && !(this.dataDoc[this.fieldKey] instanceof WebField)) {
this.layoutDoc.nativeWidth = this.dataDoc[this.fieldKey + '_nativeWidth'] = undefined;
} else {
@@ -897,7 +904,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1;
const pointerEvents = this.layoutDoc._lockedPosition ? 'none' : (this.props.pointerEvents?.() as any);
const scale = previewScale * (this.props.NativeDimScaling?.() || 1);
- const renderAnnotations = (docFilters: () => string[]) => (
+ const renderAnnotations = (childFilters: () => string[]) => (
<CollectionFreeFormView
{...this.props}
setContentView={this.setInnerContent}
@@ -915,8 +922,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
ScreenToLocalTransform={this.scrollXf}
NativeDimScaling={returnOne}
focus={this.focus}
- dropAction="embed"
- docFilters={docFilters}
+ childFilters={childFilters}
select={emptyFunction}
isAnyChildContentActive={returnFalse}
bringToFront={emptyFunction}
@@ -924,7 +930,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
removeDocument={this.removeDocument}
moveDocument={this.moveDocument}
- addDocument={this.addDocument}
+ addDocument={this.addDocumentWrapper}
childPointerEvents={this.props.isContentActive() ? 'all' : undefined}
pointerEvents={this.annotationPointerEvents}
/>
@@ -992,16 +998,19 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
panelHeight = () => this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1);
scrollXf = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._layout_scrollTop));
anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick;
- transparentFilter = () => [...this.props.docFilters(), Utils.IsTransparentFilter()];
- opaqueFilter = () => [...this.props.docFilters(), Utils.noDragsDocFilter, ...(DragManager.docsBeingDragged.length ? [] : [Utils.IsOpaqueFilter()])];
+ transparentFilter = () => [...this.props.childFilters(), Utils.IsTransparentFilter()];
+ opaqueFilter = () => [...this.props.childFilters(), Utils.noDragsDocFilter, ...(DragManager.docsBeingDragged.length ? [] : [Utils.IsOpaqueFilter()])];
childStyleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string): any => {
if (doc instanceof Doc && property === StyleProp.PointerEvents) {
- if (doc.textInlineAnnotations) return 'none';
+ if (this.inlineTextAnnotations.includes(doc)) return 'none';
}
return this.props.styleProvider?.(doc, props, property);
};
- pointerEvents = () => (!this._draggingSidebar && this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' && !MarqueeOptionsMenu.Instance?.isShown() ? 'all' : SnappingManager.GetIsDragging() ? undefined : 'none');
- annotationPointerEvents = () => (this._isAnnotating || SnappingManager.GetIsDragging() || Doc.ActiveTool !== InkTool.None ? 'all' : 'none');
+ pointerEvents = () =>
+ !this._draggingSidebar && this.props.isContentActive() && !MarqueeOptionsMenu.Instance?.isShown()
+ ? 'all' //
+ : 'none';
+ annotationPointerEvents = () => (this.props.isContentActive() && (this._isAnnotating || SnappingManager.GetIsDragging() || Doc.ActiveTool !== InkTool.None) ? 'all' : 'none');
render() {
const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1;
const pointerEvents = this.layoutDoc._lockedPosition ? 'none' : (this.props.pointerEvents?.() as any);
@@ -1010,7 +1019,11 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
<div
className="webBox"
ref={this._mainCont}
- style={{ pointerEvents: this.pointerEvents(), position: SnappingManager.GetIsDragging() ? 'absolute' : undefined, display: !SnappingManager.GetIsDragging() && this.props.thumbShown?.() ? 'none' : undefined }}>
+ style={{
+ pointerEvents: this.pointerEvents(), //
+ position: SnappingManager.GetIsDragging() ? 'absolute' : undefined,
+ display: !this.props.isContentActive() && this.props.thumbShown?.() ? 'none' : undefined,
+ }}>
<div className="webBox-background" style={{ backgroundColor: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor) }} />
<div
className="webBox-container"
diff --git a/src/client/views/nodes/button/FontIconBadge.tsx b/src/client/views/nodes/button/FontIconBadge.tsx
deleted file mode 100644
index 3b5aac221..000000000
--- a/src/client/views/nodes/button/FontIconBadge.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import { observer } from "mobx-react";
-import * as React from "react";
-import { AclPrivate, Doc, DocListCast } from "../../../../fields/Doc";
-import { GetEffectiveAcl } from "../../../../fields/util";
-import { emptyFunction, returnFalse, setupMoveUpEvents } from "../../../../Utils";
-import { DragManager } from "../../../util/DragManager";
-import "./FontIconBadge.scss";
-
-interface FontIconBadgeProps {
- value: string | undefined;
-}
-
-@observer
-export class FontIconBadge extends React.Component<FontIconBadgeProps> {
- _notifsRef = React.createRef<HTMLDivElement>();
-
- // onPointerDown = (e: React.PointerEvent) => {
- // setupMoveUpEvents(this, e,
- // (e: PointerEvent) => {
- // const dragData = new DragManager.DocumentDragData([this.props.collection!]);
- // DragManager.StartDocumentDrag([this._notifsRef.current!], dragData, e.x, e.y);
- // return true;
- // },
- // returnFalse, emptyFunction, false);
- // }
-
- render() {
- if (this.props.value === undefined) return (null);
- return <div className="fontIconBadge-container" ref={this._notifsRef}>
- <div className="fontIconBadge" style={{ "display": "initial" }}
- // onPointerDown={this.onPointerDown}
- >
- {this.props.value}
- </div>
- </div>;
- }
-} \ No newline at end of file
diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx
deleted file mode 100644
index 5e615f2c1..000000000
--- a/src/client/views/nodes/button/FontIconBox.tsx
+++ /dev/null
@@ -1,960 +0,0 @@
-import { IconProp } from '@fortawesome/fontawesome-svg-core';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Tooltip } from '@material-ui/core';
-import { action, computed, observable, runInAction } from 'mobx';
-import { observer } from 'mobx-react';
-import * as React from 'react';
-import { ColorState, SketchPicker } from 'react-color';
-import { Doc, HeightSym, StrListCast, WidthSym } from '../../../../fields/Doc';
-import { InkTool } from '../../../../fields/InkField';
-import { ScriptField } from '../../../../fields/ScriptField';
-import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
-import { WebField } from '../../../../fields/URLField';
-import { GestureUtils } from '../../../../pen-gestures/GestureUtils';
-import { aggregateBounds, Utils } from '../../../../Utils';
-import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
-import { LinkManager } from '../../../util/LinkManager';
-import { ScriptingGlobals } from '../../../util/ScriptingGlobals';
-import { SelectionManager } from '../../../util/SelectionManager';
-import { undoBatch, UndoManager } from '../../../util/UndoManager';
-import { CollectionFreeFormView } from '../../collections/collectionFreeForm';
-import { ContextMenu } from '../../ContextMenu';
-import { DocComponent } from '../../DocComponent';
-import { EditableView } from '../../EditableView';
-import { GestureOverlay } from '../../GestureOverlay';
-import { Colors } from '../../global/globalEnums';
-import { ActiveFillColor, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, SetActiveFillColor, SetActiveInkColor, SetActiveInkWidth, SetActiveIsInkMask } from '../../InkingStroke';
-import { InkTranscription } from '../../InkTranscription';
-import { StyleProp } from '../../StyleProvider';
-import { FieldView, FieldViewProps } from '.././FieldView';
-import { CollectionFreeFormDocumentView } from '../CollectionFreeFormDocumentView';
-import { OpenWhere } from '../DocumentView';
-import { RichTextMenu } from '../formattedText/RichTextMenu';
-import { WebBox } from '../WebBox';
-import { FontIconBadge } from './FontIconBadge';
-import './FontIconBox.scss';
-
-export enum ButtonType {
- TextButton = 'textBtn',
- MenuButton = 'menuBtn',
- DropdownList = 'drpdownList',
- DropdownButton = 'drpdownBtn',
- ClickButton = 'clickBtn',
- DoubleButton = 'dblBtn',
- ToggleButton = 'tglBtn',
- ColorButton = 'colorBtn',
- ToolButton = 'toolBtn',
- NumberSliderButton = 'numSliderBtn',
- NumberDropdownButton = 'numDropdownBtn',
- NumberInlineButton = 'numInlineBtn',
- EditableText = 'editableText',
-}
-
-export interface ButtonProps extends FieldViewProps {
- type?: ButtonType;
-}
-@observer
-export class FontIconBox extends DocComponent<ButtonProps>() {
- public static LayoutString(fieldKey: string) {
- return FieldView.LayoutString(FontIconBox, fieldKey);
- }
- @observable noTooltip = false;
- showTemplate = (): void => {
- const dragFactory = Cast(this.layoutDoc.dragFactory, Doc, null);
- dragFactory && this.props.addDocTab(dragFactory, OpenWhere.addRight);
- };
- dragAsTemplate = (): void => {
- this.layoutDoc.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)');
- };
- useAsPrototype = (): void => {
- this.layoutDoc.onDragStart = ScriptField.MakeFunction('makeDelegate(this.dragFactory, true)');
- };
-
- specificContextMenu = (): void => {
- if (!Doc.noviceMode) {
- const cm = ContextMenu.Instance;
- cm.addItem({ description: 'Show Template', event: this.showTemplate, icon: 'tag' });
- cm.addItem({ description: 'Use as Render Template', event: this.dragAsTemplate, icon: 'tag' });
- cm.addItem({ description: 'Use as Prototype', event: this.useAsPrototype, icon: 'tag' });
- }
- };
-
- 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.label, StrCast(this.rootDoc.title));
- }
- Icon = (color: string) => {
- const 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} />;
- };
- @computed get dropdown() {
- return BoolCast(this.rootDoc.dropDownOpen);
- }
- @computed get buttonList() {
- return StrListCast(this.rootDoc.btnList);
- }
- @computed get type() {
- return StrCast(this.rootDoc.btnType);
- }
-
- /**
- * Types of buttons in dash:
- * - Main menu button (LHS)
- * - Tool button
- * - Expandable button (CollectionLinearView)
- * - Button inside of CollectionLinearView vs. outside of CollectionLinearView
- * - Action button
- * - Dropdown button
- * - Color button
- * - Dropdown list
- * - Number button
- **/
-
- _batch: UndoManager.Batch | undefined = undefined;
- /**
- * Number button
- */
- @computed get numberSliderButton() {
- const numScript = ScriptCast(this.rootDoc.script);
- const setValue = (value: number) => UndoManager.RunInBatch(() => numScript?.script.run({ self: this.rootDoc, value, _readOnly_: false }), 'set num value');
-
- // Script for checking the outcome of the toggle
- const checkResult = Number(numScript?.script.run({ self: this.rootDoc, value: 0, _readOnly_: true }).result ?? 0).toPrecision(NumCast(this.dataDoc.numPrecision, 3));
-
- const label = !FontIconBox.GetShowLabels() ? null : <div className="fontIconBox-label">{this.label}</div>;
-
- const dropdown = (
- <div className="menuButton-dropdownBox" onPointerDown={e => e.stopPropagation()}>
- <input
- type="range"
- step="1"
- min={NumCast(this.rootDoc.numBtnMin, 0)}
- max={NumCast(this.rootDoc.numBtnMax, 100)}
- value={checkResult}
- className="menu-slider"
- onPointerDown={() => (this._batch = UndoManager.StartBatch('presDuration'))}
- onPointerUp={() => this._batch?.end()}
- onChange={e => {
- e.stopPropagation();
- setValue(Number(e.target.value));
- }}
- />
- </div>
- );
- return (
- <div
- className="menuButton numBtn slider"
- onPointerDown={e => e.stopPropagation()}
- onClick={action(() => {
- this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen;
- this.noTooltip = this.rootDoc.dropDownOpen;
- Doc.UnBrushAllDocs();
- })}>
- {checkResult}
- {label}
- {this.rootDoc.dropDownOpen ? dropdown : null}
- </div>
- );
- }
- /**
- * Number button
- */
- @computed get numberDropdownButton() {
- const numScript = ScriptCast(this.rootDoc.script);
- const setValue = (value: number) => UndoManager.RunInBatch(() => numScript?.script.run({ self: this.rootDoc, value, _readOnly_: false }), 'set num value');
-
- // Script for checking the outcome of the toggle
- const checkResult = Number(numScript?.script.run({ self: this.rootDoc, value: 0, _readOnly_: true }).result ?? 0).toPrecision(NumCast(this.dataDoc.numPrecision, 3));
-
- const label = !FontIconBox.GetShowLabels() ? null : <div className="fontIconBox-label">{this.label}</div>;
-
- const items: number[] = [];
- for (let i = 0; i < 100; i++) {
- if (i % 2 === 0) {
- items.push(i);
- }
- }
- const list = items.map(value => {
- return (
- <div
- className="list-item"
- key={`${value}`}
- style={{
- backgroundColor: value.toString() === checkResult ? Colors.LIGHT_BLUE : undefined,
- }}
- onClick={() => setValue(value)}>
- {value}
- </div>
- );
- });
- return (
- <div className="menuButton numBtn list">
- <div className="button" onClick={action(e => setValue(Number(checkResult) - 1))}>
- <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={'minus'} />
- </div>
- <div
- className={`button ${'number'}`}
- onPointerDown={e => {
- e.stopPropagation();
- e.preventDefault();
- }}
- onClick={action(() => {
- this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen;
- this.noTooltip = this.rootDoc.dropDownOpen;
- Doc.UnBrushAllDocs();
- })}>
- <input style={{ width: 30 }} className="button-input" type="number" value={checkResult} onChange={undoBatch(action(e => setValue(Number(e.target.value))))} />
- </div>
- <div className={`button`} onClick={action(e => setValue(Number(checkResult) + 1))}>
- <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={'plus'} />
- </div>
-
- {this.rootDoc.dropDownOpen ? (
- <div>
- <div className="menuButton-dropdownList" style={{ left: '25%' }}>
- {list}
- </div>
- <div
- className="dropbox-background"
- onClick={action(e => {
- e.stopPropagation();
- this.rootDoc.dropDownOpen = false;
- this.noTooltip = false;
- Doc.UnBrushAllDocs();
- })}
- />
- </div>
- ) : null}
- </div>
- );
- }
- /**
- * Number button
- */
- @computed get numberInlineButton() {
- return <div />;
- }
-
- /**
- * Dropdown button
- */
- @computed get dropdownButton() {
- 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);
- return (
- <div
- className={`menuButton ${this.type} ${active}`}
- style={{ color: color, backgroundColor: 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 : (
- <div className="fontIconBox-label" style={{ color: color, backgroundColor: backgroundColor }}>
- {' '}
- {this.label}{' '}
- </div>
- )}
- <div className="menuButton-dropdown" style={{ borderBottomRightRadius: this.dropdown ? 0 : undefined }}>
- <FontAwesomeIcon icon={'caret-down'} color={color} size="sm" />
- </div>
- {this.rootDoc.dropDownOpen ? <div className="menuButton-dropdownBox">{/* DROPDOWN BOX CONTENTS */}</div> : null}
- </div>
- );
- }
-
- /**
- * 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[] = [];
- let text: string | undefined;
- let dropdown = true;
- let icon: IconProp = 'caret-down';
- try {
- if (script?.script.originalScript.startsWith('setView')) {
- const selected = SelectionManager.Docs().lastElement();
- if (selected) {
- if (StrCast(selected.type) === DocumentType.COL) {
- text = StrCast(selected._viewType);
- } else {
- dropdown = false;
- text = selected.type === DocumentType.RTF ? 'Text' : StrCast(selected.type);
- icon = Doc.toIcon(selected);
- }
- } else {
- dropdown = false;
- icon = 'globe-asia';
- text = 'User Default';
- }
- noviceList = [CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Stacking, CollectionViewType.NoteTaking];
- } else text = StrCast((RichTextMenu.Instance?.TextView?.EditorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily);
- } catch (e) {
- console.log(e);
- }
-
- // Get items to place into the list
- const list = this.buttonList
- .filter(value => !Doc.noviceMode || !noviceList.length || noviceList.includes(value))
- .map(value => (
- <div
- className="list-item"
- key={`${value}`}
- style={{
- fontFamily: script.script.originalScript.startsWith('{ return setFont') ? value : undefined,
- backgroundColor: value === text ? Colors.LIGHT_BLUE : undefined,
- }}
- onClick={undoBatch(() => script.script.run({ self: this.rootDoc, value }))}>
- {value[0].toUpperCase() + value.slice(1)}
- </div>
- ));
-
- const label =
- !this.label || !FontIconBox.GetShowLabels() ? null : (
- <div className="fontIconBox-label" style={{ bottom: 0, position: 'absolute', color: color, backgroundColor: backgroundColor }}>
- {this.label}
- </div>
- );
-
- return (
- <div
- className={`menuButton ${this.type} ${active}`}
- style={{ backgroundColor: this.rootDoc.dropDownOpen ? Colors.MEDIUM_BLUE : backgroundColor, color: color, display: dropdown ? undefined : 'flex' }}
- onClick={
- dropdown
- ? action(() => {
- this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen;
- this.noTooltip = this.rootDoc.dropDownOpen;
- Doc.UnBrushAllDocs();
- })
- : undefined
- }>
- {dropdown ? null : <FontAwesomeIcon style={{ marginLeft: 5 }} className={`fontIconBox-icon-${this.type}`} icon={icon} color={color} />}
- <div className="menuButton-dropdown-header">{text && text[0].toUpperCase() + text.slice(1)}</div>
- {label}
- {!dropdown ? null : (
- <div className="menuButton-dropDown">
- <FontAwesomeIcon icon={icon} color={color} size="sm" />
- </div>
- )}
- {this.rootDoc.dropDownOpen ? (
- <div>
- <div className="menuButton-dropdownList" style={{ left: 0 }}>
- {list}
- </div>
- <div
- className="dropbox-background"
- onClick={action(e => {
- e.stopPropagation();
- this.rootDoc.dropDownOpen = false;
- this.noTooltip = false;
- Doc.UnBrushAllDocs();
- })}
- />
- </div>
- ) : null}
- </div>
- );
- }
-
- @observable colorPickerClosed: boolean = true;
- @computed get colorScript() {
- return ScriptCast(this.rootDoc.script);
- }
-
- colorPicker = (curColor: string) => {
- const change = (value: ColorState, ev: MouseEvent) => {
- ev.preventDefault();
- ev.stopPropagation();
- const s = this.colorScript;
- s && undoBatch(() => s.script.run({ self: this.rootDoc, value: Utils.colorString(value), _readOnly_: false }).result)();
- };
- const presets = ['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb', 'transparent'];
- return <SketchPicker onChange={change as any /* SketchPicker passes the mouse event to the callback, but the type system doesn't know that */} color={curColor} presetColors={presets} />;
- };
- /**
- * Color button
- */
- @computed get colorButton() {
- const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color);
- const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor);
- const curColor = this.colorScript?.script.run({ self: this.rootDoc, value: undefined, _readOnly_: true }).result ?? 'transparent';
-
- const label =
- !this.label || !FontIconBox.GetShowLabels() ? null : (
- <div className="fontIconBox-label" style={{ color, backgroundColor }}>
- {this.label}
- </div>
- );
-
- return (
- <div
- className={`menuButton ${this.type + (FontIconBox.GetShowLabels() ? 'Label' : '')} ${this.colorPickerClosed}`}
- style={{ color: color, borderBottomLeftRadius: this.dropdown ? 0 : undefined }}
- onClick={action(e => {
- this.colorPickerClosed = !this.colorPickerClosed;
- this.noTooltip = !this.colorPickerClosed;
- setTimeout(() => Doc.UnBrushAllDocs());
- e.stopPropagation();
- })}
- onPointerDown={e => e.stopPropagation()}>
- {this.Icon(color)}
- <div className="colorButton-color" style={{ backgroundColor: curColor }} />
- {label}
- {/* {dropdownCaret} */}
- {this.colorPickerClosed ? null : (
- <div>
- <div className="menuButton-dropdownBox" onPointerDown={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onClick={e => e.stopPropagation()}>
- {this.colorPicker(curColor)}
- </div>
- <div
- className="dropbox-background"
- onPointerDown={action(e => {
- e.preventDefault();
- e.stopPropagation();
- this.colorPickerClosed = true;
- this.noTooltip = false;
- Doc.UnBrushAllDocs();
- })}
- />
- </div>
- )}
- </div>
- );
- }
-
- @computed get toggleButton() {
- // Determine the type of toggle button
- const switchToggle: boolean = BoolCast(this.rootDoc.switchToggle);
- const buttonText: string = StrCast(this.rootDoc.buttonText);
- // Colors
- const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color);
- const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor);
-
- // Button label
- const label =
- !this.label || !FontIconBox.GetShowLabels() ? null : (
- <div className="fontIconBox-label" style={{ color, backgroundColor }}>
- {this.label}
- </div>
- );
-
- if (switchToggle) {
- return (
- <div className={`menuButton ${this.type} ${'switch'}`}>
- {buttonText ? buttonText : null}
- <label className="switch">
- <input type="checkbox" checked={backgroundColor === Colors.MEDIUM_BLUE} />
- <span className="slider round" />
- </label>
- </div>
- );
- } else {
- return (
- <div className={`menuButton ${this.type + (FontIconBox.GetShowLabels() ? 'Label' : '')}`} style={{ opacity: 1, backgroundColor, color }}>
- {this.Icon(color)}
- {label}
- </div>
- );
- }
- }
-
- /**
- * Default
- */
- @computed get defaultButton() {
- const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color);
- const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor);
- return (
- <div className={`menuButton ${this.type}`} onContextMenu={this.specificContextMenu} style={{ backgroundColor: 'transparent', borderBottomLeftRadius: this.dropdown ? 0 : undefined }}>
- <div className="menuButton-wrap">
- {this.Icon(color)}
- {!this.label || !FontIconBox.GetShowLabels() ? null : (
- <div className="fontIconBox-label" style={{ color: color, backgroundColor: backgroundColor }}>
- {' '}
- {this.label}{' '}
- </div>
- )}
- </div>
- </div>
- );
- }
-
- @computed get editableText() {
- // Script for running the toggle
- const script = ScriptCast(this.rootDoc.script);
- // Function to run the script
- const checkResult = script?.script.run({ value: '', _readOnly_: true }).result;
-
- const setValue = (value: string, shiftDown?: boolean): boolean => script?.script.run({ value, _readOnly_: false }).result;
- return (
- <div className="menuButton editableText">
- <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={'lock'} />
- <div style={{ width: 'calc(100% - .875em)', paddingLeft: '4px' }}>
- <EditableView GetValue={() => script?.script.run({ value: '', _readOnly_: true }).result} SetValue={setValue} oneLine={true} contents={checkResult} />
- </div>
- </div>
- );
- }
-
- render() {
- const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color);
- const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor);
- const label = (noBackground: boolean = false) =>
- !this.label || !FontIconBox.GetShowLabels() ? null : (
- <div className="fontIconBox-label" style={{ color, backgroundColor: noBackground ? 'transparent' : backgroundColor }}>
- {this.label}
- </div>
- );
- // TODO:glr Add label of button type
- let button: JSX.Element = this.defaultButton;
-
- // prettier-ignore
- switch (this.type) {
- case ButtonType.EditableText: return this.editableText;
- case ButtonType.DropdownList: button = this.dropdownListButton; break;
- case ButtonType.ColorButton: button = this.colorButton; break;
- case ButtonType.NumberDropdownButton: button = this.numberDropdownButton; break;
- case ButtonType.NumberInlineButton: button = this.numberInlineButton; break;
- case ButtonType.NumberSliderButton: button = this.numberSliderButton; break;
- case ButtonType.DropdownButton: button = this.dropdownButton; break;
- case ButtonType.ToggleButton: button = this.toggleButton; break;
- case ButtonType.TextButton:
- // Script for checking the outcome of the toggle
- const script = ScriptCast(this.rootDoc.script);
- const checkResult = script?.script.run({ _readOnly_: true }).result;
- button = (
- <div className={`menuButton ${this.type}`} style={{ color, backgroundColor:checkResult ?? backgroundColor, opacity: 1, gridAutoColumns: `${NumCast(this.rootDoc._height)} auto` }}>
- {this.Icon(color)}
- {StrCast(this.rootDoc.buttonText) ? <div className="button-text">{StrCast(this.rootDoc.buttonText)}</div> : null}
- {label()}
- </div>
- );
- break;
- case ButtonType.ClickButton:
- case ButtonType.ToolButton: button = (
- <div className={`menuButton ${this.type + (FontIconBox.GetShowLabels() ? 'Label' : '')}`} style={{ backgroundColor, color, opacity: 1 }}>
- {this.Icon(color)}
- {label()}
- </div>
- );
- break;
- case ButtonType.MenuButton: button = (
- <div className={`menuButton ${this.type}`} style={{ color, backgroundColor }}>
- {this.Icon(color)}
- {label(true)}
- <FontIconBadge value={Cast(this.Document.badgeValue, 'string', null)} />
- </div>
- );
- break;
- }
-
- return !this.layoutDoc.toolTip || this.noTooltip ? button : <Tooltip title={<div className="dash-tooltip">{StrCast(this.layoutDoc.toolTip)}</div>}>{button}</Tooltip>;
- }
-}
-
-// toggle: Set overlay status of selected document
-ScriptingGlobals.add(function setView(view: string) {
- const selected = SelectionManager.Docs().lastElement();
- selected ? (selected._viewType = view) : console.log('[FontIconBox.tsx] changeView failed');
-});
-
-// toggle: Set overlay status of selected document
-ScriptingGlobals.add(function setBackgroundColor(color?: string, checkResult?: boolean) {
- const selectedViews = SelectionManager.Views();
- if (Doc.ActiveTool !== InkTool.None) {
- if (checkResult) {
- return ActiveFillColor();
- }
- SetActiveFillColor(color ?? 'transparent');
- } else if (selectedViews.length) {
- if (checkResult) {
- const selView = selectedViews.lastElement();
- const fieldKey = selView.rootDoc.type === DocumentType.INK ? 'fillColor' : 'backgroundColor';
- const layoutFrameNumber = Cast(selView.props.docViewPath().lastElement()?.rootDoc?._currentFrame, 'number'); // frame number that container is at which determines layout frame values
- const contentFrameNumber = Cast(selView.rootDoc?._currentFrame, 'number', layoutFrameNumber ?? null); // frame number that content is at which determines what content is displayed
- return CollectionFreeFormDocumentView.getStringValues(selView?.rootDoc, contentFrameNumber)[fieldKey] ?? 'transparent';
- }
- selectedViews.forEach(dv => {
- const fieldKey = dv.rootDoc.type === DocumentType.INK ? 'fillColor' : 'backgroundColor';
- const layoutFrameNumber = Cast(dv.props.docViewPath().lastElement()?.rootDoc?._currentFrame, 'number'); // frame number that container is at which determines layout frame values
- const contentFrameNumber = Cast(dv.rootDoc?._currentFrame, 'number', layoutFrameNumber ?? null); // frame number that content is at which determines what content is displayed
- if (contentFrameNumber !== undefined) {
- CollectionFreeFormDocumentView.setStringValues(contentFrameNumber, dv.rootDoc, { fieldKey: color });
- } else {
- dv.rootDoc['_' + fieldKey] = color;
- }
- });
- } else {
- const selected = SelectionManager.Docs().length ? SelectionManager.Docs() : LinkManager.currentLink ? [LinkManager.currentLink] : [];
- if (checkResult) {
- return selected.lastElement()?._backgroundColor ?? 'transparent';
- }
- selected.forEach(doc => (doc._backgroundColor = color));
- }
-});
-
-// toggle: Set overlay status of selected document
-ScriptingGlobals.add(function setHeaderColor(color?: string, checkResult?: boolean) {
- if (checkResult) {
- return Doc.SharingDoc().userColor;
- }
- Doc.SharingDoc().userColor = undefined;
- Doc.GetProto(Doc.SharingDoc()).userColor = color;
- Doc.UserDoc().layout_showTitle = color === 'transparent' ? undefined : StrCast(Doc.UserDoc().layout_showTitle, 'creationDate');
-});
-
-// toggle: Set overlay status of selected document
-ScriptingGlobals.add(function toggleOverlay(checkResult?: boolean) {
- const selected = SelectionManager.Views().length ? SelectionManager.Views()[0] : undefined;
- if (checkResult) {
- if (NumCast(selected?.Document.z) >= 1) return Colors.MEDIUM_BLUE;
- return 'transparent';
- }
- selected ? selected.props.CollectionFreeFormDocumentView?.().float() : console.log('[FontIconBox.tsx] toggleOverlay failed');
-});
-
-ScriptingGlobals.add(function showFreeform(attr: 'grid' | 'snaplines' | 'clusters' | 'arrange' | 'viewAll', checkResult?: boolean) {
- const selected = SelectionManager.Docs().lastElement();
- // prettier-ignore
- const map: Map<'grid' | 'snaplines' | 'clusters' | 'arrange'| 'viewAll', { undo: boolean, checkResult: (doc:Doc) => any; setDoc: (doc:Doc) => void;}> = new Map([
- ['grid', {
- undo: false,
- checkResult: (doc:Doc) => doc._freeform_backgroundGrid,
- setDoc: (doc:Doc) => doc._freeform_backgroundGrid = !doc._freeform_backgroundGrid,
- }],
- ['snaplines', {
- undo: false,
- checkResult: (doc:Doc) => doc._freeform_snapLines,
- setDoc: (doc:Doc) => doc._freeform_snapLines = !doc._freeform_snapLines,
- }],
- ['viewAll', {
- undo: false,
- checkResult: (doc:Doc) => doc._freeform_fitContentsToBox,
- setDoc: (doc:Doc) => doc._freeform_fitContentsToBox = !doc._freeform_fitContentsToBox,
- }],
- ['clusters', {
- undo: false,
- checkResult: (doc:Doc) => doc._freeform_useClusters,
- setDoc: (doc:Doc) => doc._freeform_useClusters = !doc._freeform_useClusters,
- }],
- ['arrange', {
- undo: true,
- checkResult: (doc:Doc) => doc._autoArrange,
- setDoc: (doc:Doc) => doc._autoArrange = !doc._autoArrange,
- }],
- ]);
-
- if (checkResult) {
- return map.get(attr)?.checkResult(selected) ? Colors.MEDIUM_BLUE : 'transparent';
- }
- const batch = map.get(attr)?.undo ? UndoManager.StartBatch('set feature') : { end: () => {} };
- SelectionManager.Docs().map(dv => map.get(attr)?.setDoc(dv));
- setTimeout(() => batch.end(), 100);
-});
-ScriptingGlobals.add(function setFontAttr(attr: 'font' | 'fontColor' | 'highlight' | 'fontSize', value: any, checkResult?: boolean) {
- const editorView = RichTextMenu.Instance?.TextView?.EditorView;
- const selected = SelectionManager.Docs().lastElement();
- // prettier-ignore
- const map: Map<'font'|'fontColor'|'highlight'|'fontSize', { checkResult: () => any; setDoc: () => void;}> = new Map([
- ['font', {
- checkResult: () => RichTextMenu.Instance?.fontFamily,
- setDoc: () => value && RichTextMenu.Instance.setFontFamily(value),
- }],
- ['highlight', {
- checkResult: () =>(selected ?? Doc.UserDoc())._fontHighlight,
- setDoc: () => value && RichTextMenu.Instance.setHighlight(value),
- }],
- ['fontColor', {
- checkResult: () => RichTextMenu.Instance?.fontColor,
- setDoc: () => value && RichTextMenu.Instance.setColor(value),
- }],
- ['fontSize', {
- checkResult: () => RichTextMenu.Instance?.fontSize.replace('px', ''),
- setDoc: () => {
- if (typeof value === 'number') value = value.toString();
- if (value && Number(value).toString() === value) value += 'px';
- RichTextMenu.Instance.setFontSize(value);
- },
- }],
- ]);
-
- if (checkResult) {
- return map.get(attr)?.checkResult();
- }
- map.get(attr)?.setDoc?.();
-});
-
-type attrname = 'noAutoLink' | 'dictation' | 'bold' | 'italics' | 'underline' | 'left' | 'center' | 'right' | 'bullet' | 'decimal';
-type attrfuncs = [attrname, { checkResult: () => boolean; toggle: () => any }];
-ScriptingGlobals.add(function toggleCharStyle(charStyle: attrname, checkResult?: boolean) {
- const textView = RichTextMenu.Instance?.TextView;
- const editorView = textView?.EditorView;
- // prettier-ignore
- const alignments:attrfuncs[] = (['left','right','center'] as ("left"|"center"|"right")[]).map((where) =>
- [ where, { checkResult: () =>(editorView ? (RichTextMenu.Instance.textAlign ===where): (Doc.UserDoc().textAlign ===where) ? true:false),
- toggle: () => (editorView?.state ? RichTextMenu.Instance.align(editorView, editorView.dispatch, where):(Doc.UserDoc().textAlign = where))}]);
- // prettier-ignore
- const listings:attrfuncs[] = (['bullet','decimal'] as attrname[]).map(list =>
- [ list, { checkResult: () => (editorView ? RichTextMenu.Instance.getActiveListStyle() === list:false),
- toggle: () => editorView?.state && RichTextMenu.Instance.changeListType(list) }]);
- // prettier-ignore
- const attrs:attrfuncs[] = [
- ['dictation', { checkResult: () => textView?._recording ? true:false,
- toggle: () => textView && runInAction(() => (textView._recording = !textView._recording)) }],
- ['noAutoLink',{ checkResult: () => (editorView ? RichTextMenu.Instance.noAutoLink : false),
- toggle: () => editorView && RichTextMenu.Instance?.toggleNoAutoLinkAnchor()}],
- ['bold', { checkResult: () => (editorView ? RichTextMenu.Instance.bold : (Doc.UserDoc().fontWeight === 'bold') ? true:false),
- toggle: editorView ? RichTextMenu.Instance.toggleBold : () => (Doc.UserDoc().fontWeight = Doc.UserDoc().fontWeight === 'bold' ? undefined : 'bold')}],
- ['italics', { checkResult: () => (editorView ? RichTextMenu.Instance.italics : (Doc.UserDoc().fontStyle === 'italics') ? true:false),
- toggle: editorView ? RichTextMenu.Instance.toggleItalics : () => (Doc.UserDoc().fontStyle = Doc.UserDoc().fontStyle === 'italics' ? undefined : 'italics')}],
- ['underline', { checkResult: () => (editorView ? RichTextMenu.Instance.underline : (Doc.UserDoc().textDecoration === 'underline') ? true:false),
- toggle: editorView ? RichTextMenu.Instance.toggleUnderline : () => (Doc.UserDoc().textDecoration = Doc.UserDoc().textDecoration === 'underline' ? undefined : 'underline') }]]
-
- const map = new Map(attrs.concat(alignments).concat(listings));
- if (checkResult) return map.get(charStyle)?.checkResult() ? Colors.MEDIUM_BLUE : 'transparent';
- map.get(charStyle)?.toggle();
-});
-
-export function checkInksToGroup() {
- // console.log("getting here to inks group");
- if (Doc.ActiveTool === InkTool.Write) {
- CollectionFreeFormView.collectionsWithUnprocessedInk.forEach(ffView => {
- // TODO: nda - will probably want to go through ffView unprocessed docs and then see if any of the inksToGroup docs are in it and only use those
- // find all inkDocs in ffView.unprocessedDocs that are within 200 pixels of each other
- const inksToGroup = ffView.unprocessedDocs.filter(inkDoc => {
- // console.log(inkDoc.x, inkDoc.y);
- });
- });
- }
-}
-
-export function createInkGroup(inksToGroup?: Doc[], isSubGroup?: boolean) {
- // TODO nda - if document being added to is a inkGrouping then we can just add to that group
- if (Doc.ActiveTool === InkTool.Write) {
- CollectionFreeFormView.collectionsWithUnprocessedInk.forEach(ffView => {
- // TODO: nda - will probably want to go through ffView unprocessed docs and then see if any of the inksToGroup docs are in it and only use those
- const selected = ffView.unprocessedDocs;
- // loop through selected an get the bound
- const bounds: { x: number; y: number; width?: number; height?: number }[] = [];
-
- selected.map(
- action(d => {
- const x = NumCast(d.x);
- const y = NumCast(d.y);
- const width = d[WidthSym]();
- const height = d[HeightSym]();
- bounds.push({ x, y, width, height });
- })
- );
-
- const aggregBounds = aggregateBounds(bounds, 0, 0);
- const marqViewRef = ffView._marqueeViewRef.current;
-
- // set the vals for bounds in marqueeView
- if (marqViewRef) {
- marqViewRef._downX = aggregBounds.x;
- marqViewRef._downY = aggregBounds.y;
- marqViewRef._lastX = aggregBounds.r;
- marqViewRef._lastY = aggregBounds.b;
- }
-
- selected.map(
- action(d => {
- const dx = NumCast(d.x);
- const dy = NumCast(d.y);
- delete d.x;
- delete d.y;
- delete d.activeFrame;
- delete d._timecodeToShow; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection
- delete d._timecodeToHide; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection
- // calculate pos based on bounds
- if (marqViewRef?.Bounds) {
- d.x = dx - marqViewRef.Bounds.left - marqViewRef.Bounds.width / 2;
- d.y = dy - marqViewRef.Bounds.top - marqViewRef.Bounds.height / 2;
- }
- return d;
- })
- );
- ffView.props.removeDocument?.(selected);
- // TODO: nda - this is the code to actually get a new grouped collection
- const newCollection = marqViewRef?.getCollection(selected, undefined, true);
- if (newCollection) {
- newCollection.height = newCollection[HeightSym]();
- newCollection.width = newCollection[WidthSym]();
- }
-
- // nda - bug: when deleting a stroke before leaving writing mode, delete the stroke from unprocessed ink docs
- newCollection && ffView.props.addDocument?.(newCollection);
- // TODO: nda - will probably need to go through and only remove the unprocessed selected docs
- ffView.unprocessedDocs = [];
-
- InkTranscription.Instance.transcribeInk(newCollection, selected, false);
- });
- }
- CollectionFreeFormView.collectionsWithUnprocessedInk.clear();
-}
-
-function setActiveTool(tool: InkTool | GestureUtils.Gestures, keepPrim: boolean, checkResult?: boolean) {
- InkTranscription.Instance?.createInkGroup();
- if (checkResult) {
- return (Doc.ActiveTool === tool && !GestureOverlay.Instance?.InkShape) || GestureOverlay.Instance?.InkShape === tool
- ? GestureOverlay.Instance?.KeepPrimitiveMode || ![GestureUtils.Gestures.Circle, GestureUtils.Gestures.Line, GestureUtils.Gestures.Rectangle].includes(tool as GestureUtils.Gestures)
- ? Colors.MEDIUM_BLUE
- : Colors.MEDIUM_BLUE_ALT
- : 'transparent';
- }
- runInAction(() => {
- if (GestureOverlay.Instance) {
- GestureOverlay.Instance.KeepPrimitiveMode = keepPrim;
- }
- if (Object.values(GestureUtils.Gestures).includes(tool as any)) {
- if (GestureOverlay.Instance.InkShape === tool && !keepPrim) {
- Doc.ActiveTool = InkTool.None;
- GestureOverlay.Instance.InkShape = undefined;
- } else {
- Doc.ActiveTool = InkTool.Pen;
- GestureOverlay.Instance.InkShape = tool as GestureUtils.Gestures;
- }
- } else if (tool) {
- // pen or eraser
- if (Doc.ActiveTool === tool && !GestureOverlay.Instance.InkShape && !keepPrim) {
- Doc.ActiveTool = InkTool.None;
- } else {
- Doc.ActiveTool = tool as any;
- GestureOverlay.Instance.InkShape = undefined;
- }
- } else {
- Doc.ActiveTool = InkTool.None;
- }
- });
-}
-
-ScriptingGlobals.add(setActiveTool, 'sets the active ink tool mode');
-
-// toggle: Set overlay status of selected document
-ScriptingGlobals.add(function setInkProperty(option: 'inkMask' | 'fillColor' | 'strokeWidth' | 'strokeColor', value: any, checkResult?: boolean) {
- const selected = SelectionManager.Docs().lastElement();
- // prettier-ignore
- const map: Map<'inkMask' | 'fillColor' | 'strokeWidth' | 'strokeColor', { checkResult: () => any; setInk: (doc: Doc) => void; setMode: () => void }> = new Map([
- ['inkMask', {
- checkResult: () => ((selected?.type === DocumentType.INK ? BoolCast(selected.stroke_isInkMask) : ActiveIsInkMask()) ? Colors.MEDIUM_BLUE : 'transparent'),
- setInk: (doc: Doc) => (doc.stroke_isInkMask = !doc.stroke_isInkMask),
- setMode: () => selected?.type !== DocumentType.INK && SetActiveIsInkMask(!ActiveIsInkMask()),
- }],
- ['fillColor', {
- checkResult: () => (selected?.type === DocumentType.INK ? StrCast(selected.fillColor) : ActiveFillColor() ?? "transparent"),
- setInk: (doc: Doc) => (doc.fillColor = StrCast(value)),
- setMode: () => SetActiveFillColor(StrCast(value)),
- }],
- [ 'strokeWidth', {
- checkResult: () => (selected?.type === DocumentType.INK ? NumCast(selected.stroke_width) : ActiveInkWidth()),
- setInk: (doc: Doc) => (doc.stroke_width = NumCast(value)),
- setMode: () => SetActiveInkWidth(value.toString()),
- }],
- ['strokeColor', {
- checkResult: () => (selected?.type === DocumentType.INK ? StrCast(selected.color) : ActiveInkColor()),
- setInk: (doc: Doc) => (doc.color = String(value)),
- setMode: () => SetActiveInkColor(StrCast(value)),
- }],
- ]);
-
- if (checkResult) {
- return map.get(option)?.checkResult();
- }
- map.get(option)?.setMode();
- SelectionManager.Docs()
- .filter(doc => doc.type === DocumentType.INK)
- .map(doc => map.get(option)?.setInk(doc));
-});
-
-/** WEB
- * webSetURL
- **/
-ScriptingGlobals.add(function webSetURL(url: string, checkResult?: boolean) {
- const selected = SelectionManager.Views().lastElement();
- if (selected?.rootDoc.type === DocumentType.WEB) {
- if (checkResult) {
- return StrCast(selected.rootDoc.data, Cast(selected.rootDoc.data, WebField, null)?.url?.href);
- }
- selected.ComponentView?.setData?.(url);
- //selected.rootDoc.data = new WebField(url);
- }
-});
-ScriptingGlobals.add(function webForward(checkResult?: boolean) {
- const selected = SelectionManager.Views().lastElement()?.ComponentView as WebBox;
- if (checkResult) {
- return selected?.forward(checkResult) ? undefined : 'lightGray';
- }
- selected?.forward();
-});
-ScriptingGlobals.add(function webBack(checkResult?: boolean) {
- const selected = SelectionManager.Views().lastElement()?.ComponentView as WebBox;
- if (checkResult) {
- return selected?.back(checkResult) ? undefined : 'lightGray';
- }
- selected?.back();
-});
-
-/** Schema
- * toggleSchemaPreview
- **/
-ScriptingGlobals.add(function toggleSchemaPreview(checkResult?: boolean) {
- const selected = SelectionManager.Docs().lastElement();
- if (checkResult && selected) {
- const result: boolean = NumCast(selected.schema_previewWidth) > 0;
- if (result) return Colors.MEDIUM_BLUE;
- else return 'transparent';
- } else if (selected) {
- if (NumCast(selected.schema_previewWidth) > 0) {
- selected.schema_previewWidth = 0;
- } else {
- selected.schema_previewWidth = 200;
- }
- }
-});
-ScriptingGlobals.add(function toggleSingleLineSchema(checkResult?: boolean) {
- const selected = SelectionManager.Docs().lastElement();
- if (checkResult && selected) {
- return NumCast(selected._schema_singleLine) > 0 ? Colors.MEDIUM_BLUE : 'transparent';
- }
- if (selected) {
- selected._schema_singleLine = !selected._schema_singleLine;
- }
-});
-
-/** STACK
- * groupBy
- */
-ScriptingGlobals.add(function setGroupBy(key: string, checkResult?: boolean) {
- SelectionManager.Docs().map(doc => (doc._fontFamily = key));
- const editorView = RichTextMenu.Instance.TextView?.EditorView;
- if (checkResult) {
- return StrCast((editorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily);
- }
- if (editorView) RichTextMenu.Instance.setFontFamily(key);
- else Doc.UserDoc().fontFamily = key;
-});
diff --git a/src/client/views/nodes/button/colorDropdown/ColorDropdown.tsx b/src/client/views/nodes/button/colorDropdown/ColorDropdown.tsx
deleted file mode 100644
index 74c3c563c..000000000
--- a/src/client/views/nodes/button/colorDropdown/ColorDropdown.tsx
+++ /dev/null
@@ -1,77 +0,0 @@
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import React, { Component } from 'react';
-import { BoolCast, StrCast } from '../../../../../fields/Types';
-import { IButtonProps } from '../ButtonInterface';
-import { ColorState, SketchPicker } from 'react-color';
-import { ScriptField } from '../../../../../fields/ScriptField';
-import { Doc } from '../../../../../fields/Doc';
-import { FontIconBox } from '../FontIconBox';
-
-export class ColorDropdown extends Component<IButtonProps> {
- render() {
- const active: string = StrCast(this.props.rootDoc.dropDownOpen);
-
- const script: string = StrCast(this.props.rootDoc.script);
- const scriptCheck: string = script + '(undefined, true)';
- const boolResult = ScriptField.MakeScript(scriptCheck)?.script.run().result;
-
- const stroke: boolean = false;
- // if (script === "setStrokeColor") {
- // stroke = true;
- // const checkWidth = ScriptField.MakeScript("setStrokeWidth(0, true)")?.script.run().result;
- // const width = 20 + (checkWidth / 100) * 70;
- // const height = 20 + (checkWidth / 100) * 70;
- // strokeIcon = (<div style={{ borderRadius: "100%", width: width + '%', height: height + '%', backgroundColor: boolResult ? boolResult : "#FFFFFF" }} />);
- // }
-
- const colorOptions: string[] = ['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb'];
-
- const colorBox = (func: (color: ColorState) => void) => <SketchPicker disableAlpha={!stroke} onChange={func} color={boolResult ? boolResult : '#FFFFFF'} presetColors={colorOptions} />;
- const label =
- !this.props.label || !FontIconBox.GetShowLabels() ? null : (
- <div className="fontIconBox-label" style={{ color: this.props.color, backgroundColor: this.props.backgroundColor, position: 'absolute' }}>
- {this.props.label}
- </div>
- );
-
- const dropdownCaret = (
- <div className="menuButton-dropDown" style={{ borderBottomRightRadius: active ? 0 : undefined }}>
- <FontAwesomeIcon icon={'caret-down'} color={this.props.color} size="sm" />
- </div>
- );
-
- const click = (value: ColorState) => {
- const hex: string = value.hex;
- const s = ScriptField.MakeScript(script + '("' + hex + '", false)');
- if (s) {
- s.script.run().result;
- }
- };
- return (
- <div
- className={`menuButton ${this.props.type} ${active}`}
- style={{ color: this.props.color, borderBottomLeftRadius: active ? 0 : undefined }}
- onClick={() => (this.props.rootDoc.dropDownOpen = !this.props.rootDoc.dropDownOpen)}
- onPointerDown={e => e.stopPropagation()}>
- <FontAwesomeIcon className={`fontIconBox-icon-${this.props.type}`} icon={this.props.icon} color={this.props.color} />
- <div className="colorButton-color" style={{ backgroundColor: boolResult ? boolResult : '#FFFFFF' }} />
- {label}
- {/* {dropdownCaret} */}
- {this.props.rootDoc.dropDownOpen ? (
- <div>
- <div className="menuButton-dropdownBox" onPointerDown={e => e.stopPropagation()} onClick={e => e.stopPropagation()}>
- {colorBox(click)}
- </div>
- <div
- className="dropbox-background"
- onClick={e => {
- e.stopPropagation();
- this.props.rootDoc.dropDownOpen = false;
- }}
- />
- </div>
- ) : null}
- </div>
- );
- }
-}
diff --git a/src/client/views/nodes/button/colorDropdown/index.ts b/src/client/views/nodes/button/colorDropdown/index.ts
deleted file mode 100644
index 1147d6457..000000000
--- a/src/client/views/nodes/button/colorDropdown/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from './ColorDropdown'; \ No newline at end of file
diff --git a/src/client/views/nodes/button/textButton/TextButton.tsx b/src/client/views/nodes/button/textButton/TextButton.tsx
deleted file mode 100644
index 5d7d55863..000000000
--- a/src/client/views/nodes/button/textButton/TextButton.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import React, { Component } from 'react';
-import { BoolCast } from '../../../../../fields/Types';
-import { IButtonProps } from '../ButtonInterface';
-
-export class TextButton extends Component<IButtonProps> {
- render() {
- const type = this.props.type;
- // Determine the type of toggle button
- const buttonText: boolean = BoolCast(this.props.rootDoc.switchToggle);
-
- return (
- <div
- className={`menuButton ${this.props.type}`}
- style={{
- opacity: 1,
- backgroundColor: this.props.backgroundColor,
- color: this.props.color,
- }}
- >
- <FontAwesomeIcon
- className={`fontIconBox-icon-${this.props.type}`}
- icon={this.props.icon}
- color={this.props.color}
- />
- {this.props.label}
- </div>
- );
- }
-}
diff --git a/src/client/views/nodes/button/textButton/index.ts b/src/client/views/nodes/button/textButton/index.ts
deleted file mode 100644
index 01d62eb7e..000000000
--- a/src/client/views/nodes/button/textButton/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from './TextButton'; \ No newline at end of file
diff --git a/src/client/views/nodes/button/toggleButton/ToggleButton.tsx b/src/client/views/nodes/button/toggleButton/ToggleButton.tsx
deleted file mode 100644
index dca6487d8..000000000
--- a/src/client/views/nodes/button/toggleButton/ToggleButton.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import React, { Component } from 'react';
-import { BoolCast } from '../../../../../fields/Types';
-import { Colors } from '../../../global/globalEnums';
-import { IButtonProps } from '../ButtonInterface';
-
-export class ToggleButton extends Component<IButtonProps> {
- render() {
- const type = this.props.type;
- // Determine the type of toggle button
- const switchToggle: boolean = BoolCast(this.props.rootDoc.switchToggle);
-
- if (switchToggle) {
- return (
- <div className={`menuButton ${type} ${'switch'}`}>
- <label className="switch">
- <input type="checkbox"
- checked={this.props.backgroundColor === Colors.MEDIUM_BLUE}
- />
- <span className="slider round"></span>
- </label>
- </div>
- );
- } else {
- return (
- <div className={`menuButton ${type}`}
- style={{ opacity: 1, backgroundColor: this.props.backgroundColor, color: this.props.color }}>
- <FontAwesomeIcon className={`fontIconBox-icon-${type}`} icon={this.props.icon} color={this.props.color} />
- {this.props.label}
- </div>
- );
- }
- }
-} \ No newline at end of file
diff --git a/src/client/views/nodes/button/toggleButton/index.ts b/src/client/views/nodes/button/toggleButton/index.ts
deleted file mode 100644
index cdb9c527c..000000000
--- a/src/client/views/nodes/button/toggleButton/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from './ToggleButton'; \ No newline at end of file
diff --git a/src/client/views/nodes/formattedText/DashDocView.tsx b/src/client/views/nodes/formattedText/DashDocView.tsx
index c929b7ff3..48f4c2afd 100644
--- a/src/client/views/nodes/formattedText/DashDocView.tsx
+++ b/src/client/views/nodes/formattedText/DashDocView.tsx
@@ -2,8 +2,9 @@ import { action, IReactionDisposer, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import { NodeSelection } from 'prosemirror-state';
import * as ReactDOM from 'react-dom/client';
-import { Doc, HeightSym, WidthSym } from '../../../../fields/Doc';
-import { Cast, NumCast, StrCast } from '../../../../fields/Types';
+import { Doc } from '../../../../fields/Doc';
+import { Height, Width } from '../../../../fields/DocSymbols';
+import { NumCast, StrCast } from '../../../../fields/Types';
import { emptyFunction, returnFalse, Utils } from '../../../../Utils';
import { DocServer } from '../../../DocServer';
import { Docs, DocUtils } from '../../../documents/Documents';
@@ -210,14 +211,14 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> {
addDocTab={this._textBox.props.addDocTab}
pinToPres={returnFalse}
renderDepth={this._textBox.props.renderDepth + 1}
- PanelWidth={this._finalLayout[WidthSym]}
- PanelHeight={this._finalLayout[HeightSym]}
+ PanelWidth={this._finalLayout[Width]}
+ PanelHeight={this._finalLayout[Height]}
focus={this.outerFocus}
whenChildContentsActiveChanged={returnFalse}
bringToFront={emptyFunction}
dontRegisterView={false}
- docFilters={this.props.tbox?.props.docFilters}
- docRangeFilters={this.props.tbox?.props.docRangeFilters}
+ childFilters={this.props.tbox?.props.childFilters}
+ childFiltersByRanges={this.props.tbox?.props.childFiltersByRanges}
searchFilterDocs={this.props.tbox?.props.searchFilterDocs}
/>
</div>
diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx
index 6c61f6709..d5ad128fe 100644
--- a/src/client/views/nodes/formattedText/DashFieldView.tsx
+++ b/src/client/views/nodes/formattedText/DashFieldView.tsx
@@ -3,7 +3,7 @@ import { Tooltip } from '@material-ui/core';
import { action, computed, IReactionDisposer, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as ReactDOM from 'react-dom/client';
-import { DataSym, Doc, Field } from '../../../../fields/Doc';
+import { Doc } from '../../../../fields/Doc';
import { List } from '../../../../fields/List';
import { listSpec } from '../../../../fields/Schema';
import { SchemaHeaderField } from '../../../../fields/SchemaHeaderField';
@@ -17,6 +17,7 @@ import { OpenWhere } from '../DocumentView';
import './DashFieldView.scss';
import { FormattedTextBox } from './FormattedTextBox';
import React = require('react');
+import { Transform } from '../../../util/Transform';
export class DashFieldView {
dom: HTMLDivElement; // container for label and value
@@ -113,6 +114,7 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna
componentWillUnmount() {
this._reactionDisposer?.();
}
+ return100 = () => 100;
// set the display of the field's value (checkbox for booleans, span of text for strings)
@computed get fieldValueContent() {
@@ -123,7 +125,7 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna
col={0}
deselectCell={emptyFunction}
selectCell={emptyFunction}
- maxWidth={this.props.hideKey ? undefined : () => 100}
+ maxWidth={this.props.hideKey ? undefined : this.return100}
columnWidth={this.props.hideKey ? () => this.props.tbox.props.PanelWidth() - 20 : returnZero}
selectedCell={() => [this._dashDoc!, 0]}
fieldKey={this._fieldKey}
@@ -135,6 +137,8 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna
allowCRs={true}
oneLine={!this._expanded}
finishEdit={action(() => (this._expanded = false))}
+ transform={Transform.Identity}
+ menuTarget={null}
/>
</div>
);
@@ -144,7 +148,7 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna
let container = this.props.tbox.props.DocumentView?.().props.docViewPath().lastElement();
if (container) {
const embedding = Doc.MakeEmbedding(container.rootDoc);
- embedding._viewType = CollectionViewType.Time;
+ embedding._type_collection = CollectionViewType.Time;
const colHdrKey = '_' + container.LayoutFieldKey + '_columnHeaders';
let list = Cast(embedding[colHdrKey], listSpec(SchemaHeaderField));
if (!list) {
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss
index 109b62e6f..348bdd79e 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.scss
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss
@@ -84,7 +84,6 @@ audiotag:hover {
height: 11;
}
-.formattedTextBox-outer-selected,
.formattedTextBox-outer {
position: relative;
overflow: auto;
@@ -92,9 +91,6 @@ audiotag:hover {
width: 100%;
height: unset;
}
-.formattedTextBox-outer-selected {
- cursor: text;
-}
.formattedTextBox-sidebar-handle {
position: absolute;
@@ -148,10 +144,8 @@ audiotag:hover {
}
.formattedTextBox-inner-rounded,
-.formattedTextBox-inner-rounded-selected,
.formattedTextBox-inner,
-.formattedTextBox-inner-minimal,
-.formattedTextBox-inner-selected {
+.formattedTextBox-inner-minimal {
height: 100%;
white-space: pre-wrap;
.ProseMirror:hover {
@@ -169,17 +163,6 @@ audiotag:hover {
border-width: 1px;
}
}
-.formattedTextBox-inner-rounded-selected,
-.formattedTextBox-inner-selected {
- > .ProseMirror {
- padding: 10px;
- }
-}
-.formattedTextBox-outer-selected {
- > .ProseMirror:hover {
- background: unset;
- }
-}
.gpt-typing-wrapper {
padding: 10px;
@@ -640,7 +623,6 @@ footnote::before {
}
}
- .formattedTextBox-outer-selected,
.formattedTextBox-outer {
position: relative;
overflow: auto;
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 023814a9d..1dcc445e8 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -12,9 +12,11 @@ import { Fragment, Mark, Node, Slice } from 'prosemirror-model';
import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from 'prosemirror-state';
import { EditorView } from 'prosemirror-view';
import { DateField } from '../../../../fields/DateField';
-import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, CssSym, Doc, DocListCast, Field, ForceServerWrite, HeightSym, Opt, StrListCast, UpdatingFromServer, WidthSym } from '../../../../fields/Doc';
+import { Doc, DocListCast, StrListCast, Field, Opt } from '../../../../fields/Doc';
+import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DocCss, Height, Width, ForceServerWrite, UpdatingFromServer } from '../../../../fields/DocSymbols';
import { Id } from '../../../../fields/FieldSymbols';
import { InkTool } from '../../../../fields/InkField';
+import { List } from '../../../../fields/List';
import { PrefetchProxy } from '../../../../fields/Proxy';
import { RichTextField } from '../../../../fields/RichTextField';
import { RichTextUtils } from '../../../../fields/RichTextUtils';
@@ -32,8 +34,8 @@ import { DictationManager } from '../../../util/DictationManager';
import { DocumentManager } from '../../../util/DocumentManager';
import { DragManager } from '../../../util/DragManager';
import { MakeTemplate } from '../../../util/DropConverter';
-import { IsFollowLinkScript } from '../../../util/LinkFollower';
import { LinkManager } from '../../../util/LinkManager';
+import { RTFMarkup } from '../../../util/RTFMarkup';
import { SelectionManager } from '../../../util/SelectionManager';
import { SnappingManager } from '../../../util/SnappingManager';
import { undoBatch, UndoManager } from '../../../util/UndoManager';
@@ -68,8 +70,6 @@ import { schema } from './schema_rts';
import { SummaryView } from './SummaryView';
import applyDevTools = require('prosemirror-dev-tools');
import React = require('react');
-import { RTFMarkup } from '../../../util/RTFMarkup';
-import { List } from '../../../../fields/List';
const translateGoogleApi = require('translate-google-api');
export const GoogleRef = 'googleDocId';
type PullHandler = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => void;
@@ -96,6 +96,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
private _scrollRef: React.RefObject<HTMLDivElement> = React.createRef();
private _editorView: Opt<EditorView>;
public _applyingChange: string = '';
+ private _finishingLink = false;
private _searchIndex = 0;
private _lastTimedMark: Mark | undefined = undefined;
private _cachedLinks: Doc[] = [];
@@ -131,7 +132,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
return this._showSidebar ? '20%' : StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%');
}
@computed get sidebarColor() {
- return StrCast(this.layoutDoc.sidebarColor, StrCast(this.layoutDoc[this.fieldKey + '_backgroundColor'], '#e4e4e4'));
+ return StrCast(this.layoutDoc.sidebar_color, StrCast(this.layoutDoc[this.fieldKey + '_backgroundColor'], '#e4e4e4'));
}
@computed get layout_autoHeight() {
return (this.props.forceAutoHeight || this.layoutDoc._layout_autoHeight) && !this.props.ignoreAutoHeight;
@@ -243,9 +244,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
if (!pinProps && this._editorView?.state.selection.empty) return this.rootDoc;
- const anchor = Docs.Create.TextanchorDocument({ annotationOn: this.rootDoc, unrendered: true });
+ const anchor = Docs.Create.ConfigDocument({ title: StrCast(this.rootDoc.title), annotationOn: this.rootDoc });
this.addDocument(anchor);
+ this._finishingLink = true;
this.makeLinkAnchor(anchor, OpenWhere.addRight, undefined, 'Anchored Selection', false, addAsAnnotation);
+ this._finishingLink = false;
PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), scrollable: true } }, this.rootDoc);
return anchor;
};
@@ -296,21 +299,21 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
};
dispatchTransaction = (tx: Transaction) => {
- if (this._editorView) {
+ if (this._editorView && (this._editorView as any).docView) {
const state = this._editorView.state.apply(tx);
this._editorView.updateState(state);
const dataDoc = Doc.IsDelegateField(DocCast(this.layoutDoc.proto), this.fieldKey) ? DocCast(this.layoutDoc.proto) : this.dataDoc;
- const curText = state.doc.textBetween(0, state.doc.content.size, ' \n');
- const curTemp = this.layoutDoc.resolvedDataDoc ? Cast(this.layoutDoc[this.fieldKey], RichTextField) : undefined; // the actual text in the text box
- const curProto = Cast(Cast(dataDoc.proto, Doc, null)?.[this.fieldKey], RichTextField, null); // the default text inherited from a prototype
- const curLayout = this.rootDoc !== this.layoutDoc ? Cast(this.layoutDoc[this.fieldKey], RichTextField, null) : undefined; // the default text stored in a layout template
- const json = JSON.stringify(state.toJSON());
+ const newText = state.doc.textBetween(0, state.doc.content.size, ' \n');
+ const newJson = JSON.stringify(state.toJSON());
+ const prevData = Cast(this.layoutDoc[this.fieldKey], RichTextField, null); // the actual text in the text box
+ const templateData = this.rootDoc !== this.layoutDoc ? prevData : undefined; // the default text stored in a layout template
+ const protoData = Cast(Cast(dataDoc.proto, Doc, null)?.[this.fieldKey], RichTextField, null); // the default text inherited from a prototype
const effectiveAcl = GetEffectiveAcl(dataDoc);
- const removeSelection = (json: string | undefined) => (json?.indexOf('"storedMarks"') === -1 ? json?.replace(/"selection":.*/, '') : json?.replace(/"selection":"\"storedMarks\""/, '"storedMarks"'));
+ const removeSelection = (json: string | undefined) => json?.replace(/"selection":.*/, '');
- if ([AclEdit, AclAdmin, AclSelfEdit].includes(effectiveAcl)) {
+ if ([AclEdit, AclAdmin, AclSelfEdit, AclAugment].includes(effectiveAcl)) {
const accumTags = [] as string[];
state.tr.doc.nodesBetween(0, state.doc.content.size, (node: any, pos: number, parent: any) => {
if (node.type === schema.nodes.dashField && node.attrs.fieldKey.startsWith('#')) {
@@ -320,29 +323,25 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
dataDoc.tags = accumTags.length ? new List<string>(Array.from(new Set<string>(accumTags))) : undefined;
let unchanged = true;
- if (this._applyingChange !== this.fieldKey && removeSelection(json) !== removeSelection(curProto?.Data)) {
+ if (this._applyingChange !== this.fieldKey && removeSelection(newJson) !== removeSelection(prevData?.Data)) {
this._applyingChange = this.fieldKey;
- const textChange = curText !== Cast(dataDoc[this.fieldKey], RichTextField)?.Text;
+ const textChange = newText !== prevData?.Text;
textChange && (dataDoc[this.fieldKey + '_modificationDate'] = new DateField(new Date(Date.now())));
- if ((!curTemp && !curProto) || curText || json.includes('dash')) {
+ if ((!prevData && !protoData) || newText || (!newText && !templateData)) {
// if no template, or there's text that didn't come from the layout template, write it to the document. (if this is driven by a template, then this overwrites the template text which is intended)
- if (removeSelection(json) !== removeSelection(curLayout?.Data)) {
+ if ((this._finishingLink || this.props.isContentActive()) && removeSelection(newJson) !== removeSelection(prevData?.Data)) {
const numstring = NumCast(dataDoc[this.fieldKey], null);
- if (numstring !== undefined) {
- dataDoc[this.fieldKey] = Number(curText);
- } else {
- dataDoc[this.fieldKey] = new RichTextField(json, curText);
- }
- dataDoc[this.fieldKey + '_noTemplate'] = true; //(curTemp?.Text || "") !== curText; // mark the data field as being split from the template if it has been edited
- textChange && ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, self: this.rootDoc, text: curText });
+ dataDoc[this.fieldKey] = numstring !== undefined ? Number(newText) : new RichTextField(newJson, newText);
+ dataDoc[this.fieldKey + '_noTemplate'] = true; // mark the data field as being split from the template if it has been edited
+ textChange && ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, self: this.rootDoc, text: newText });
unchanged = false;
}
} else {
// if we've deleted all the text in a note driven by a template, then restore the template data
dataDoc[this.fieldKey] = undefined;
- this._editorView.updateState(EditorState.fromJSON(this.config, JSON.parse((curProto || curTemp).Data)));
+ this._editorView.updateState(EditorState.fromJSON(this.config, JSON.parse((protoData || prevData).Data)));
dataDoc[this.fieldKey + '_noTemplate'] = undefined; // mark the data field as not being split from any template it might have
- ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, self: this.rootDoc, text: curText });
+ ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, self: this.rootDoc, text: newText });
unchanged = false;
}
this._applyingChange = '';
@@ -418,6 +417,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
DocListCast(Doc.MyPublishedDocs.data).forEach(term => (tr = this.hyperlinkTerm(tr, term, newAutoLinks)));
tr = tr.setSelection(isNodeSel && false ? new NodeSelection(tr.doc.resolve(f)) : new TextSelection(tr.doc.resolve(f), tr.doc.resolve(t)));
this._editorView?.dispatch(tr);
+ this.prepareForTyping();
}
oldAutoLinks.filter(oldLink => !newAutoLinks.has(oldLink) && oldLink.link_anchor_2 !== this.rootDoc).forEach(LinkManager.Instance.deleteLink);
};
@@ -460,8 +460,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
if (node.firstChild === null && !node.marks.find((m: Mark) => m.type.name === schema.marks.noAutoLinkAnchor.name) && node.marks.find((m: Mark) => m.type.name === schema.marks.splitter.name)) {
alink =
alink ??
- (LinkManager.Links(this.Document).find(link => Doc.AreProtosEqual(Cast(link.link_anchor_1, Doc, null), this.rootDoc) && Doc.AreProtosEqual(Cast(link.link_anchor_2, Doc, null), target)) ||
- DocUtils.MakeLink(this.props.Document, target, { link_relationship: LinkManager.AutoKeywords })!);
+ (LinkManager.Links(this.rootDoc).find(
+ link =>
+ Doc.AreProtosEqual(Cast(link.link_anchor_1, Doc, null), this.rootDoc) && //
+ Doc.AreProtosEqual(Cast(link.link_anchor_2, Doc, null), target)
+ ) ||
+ DocUtils.MakeLink(this.rootDoc, target, { link_relationship: LinkManager.AutoKeywords })!);
newAutoLinks.add(alink);
const allAnchors = [{ href: Doc.localServerPath(target), title: 'a link', anchorId: this.props.Document[Id] }];
allAnchors.push(...(node.marks.find((m: Mark) => m.type.name === schema.marks.autoLinkAnchor.name)?.attrs.allAnchors ?? []));
@@ -534,36 +538,50 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
@undoBatch
@action
drop = (e: Event, de: DragManager.DropEvent) => {
- if (de.complete.annoDragData) de.complete.annoDragData.dropDocCreator = () => this.getAnchor(true);
+ if (de.complete.annoDragData) {
+ de.complete.annoDragData.dropDocCreator = () => this.getAnchor(true);
+ e.stopPropagation();
+ return true;
+ }
const dragData = de.complete.docDragData;
if (dragData) {
- const draggedDoc = dragData.draggedDocuments.length && dragData.draggedDocuments[0];
- // replace text contents whend dragging with Alt
- if (draggedDoc && draggedDoc.type === DocumentType.RTF && !Doc.AreProtosEqual(draggedDoc, this.props.Document) && de.altKey) {
- if (draggedDoc.data instanceof RichTextField) {
- Doc.GetProto(this.dataDoc)[this.fieldKey] = new RichTextField(draggedDoc.data.Data, draggedDoc.data.Text);
- e.stopPropagation();
- }
- // embed document when dragg marked as embed
- } else if (de.embedKey) {
- const target = dragData.droppedDocuments[0];
- const node = schema.nodes.dashDoc.create({
- width: target[WidthSym](),
- height: target[HeightSym](),
- title: 'dashDoc',
- docId: target[Id],
- float: 'unset',
- });
- if (!['embed', 'copy'].includes((dragData.dropAction ?? '') as any)) {
- dragData.removeDocument?.(dragData.draggedDocuments[0]);
+ const dataDoc = Doc.IsDelegateField(DocCast(this.layoutDoc.proto), this.fieldKey) ? DocCast(this.layoutDoc.proto) : this.dataDoc;
+ const effectiveAcl = GetEffectiveAcl(dataDoc);
+ let added = [AclEdit, AclAdmin, AclSelfEdit].includes(effectiveAcl);
+ const draggedDoc = dragData.draggedDocuments.lastElement();
+ if (added) {
+ // replace text contents when dragging with Alt
+ if (de.altKey) {
+ const fieldKey = Doc.LayoutFieldKey(draggedDoc);
+ if (draggedDoc[fieldKey] instanceof RichTextField && !Doc.AreProtosEqual(draggedDoc, this.props.Document)) {
+ Doc.GetProto(this.dataDoc)[this.fieldKey] = Field.Copy(draggedDoc[fieldKey]);
+ }
+
+ // embed document when drag marked as embed
+ } else if (de.embedKey) {
+ const node = schema.nodes.dashDoc.create({
+ width: draggedDoc[Width](),
+ height: draggedDoc[Height](),
+ title: 'dashDoc',
+ docId: draggedDoc[Id],
+ float: 'unset',
+ });
+ if (!['embed', 'copy'].includes((dragData.dropAction ?? '') as any)) {
+ added = dragData.removeDocument?.(draggedDoc) ? true : false;
+ }
+ if (added) {
+ draggedDoc._freeform_fitContentsToBox = true;
+ 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));
+ }
}
- target._freeform_fitContentsToBox = true;
- target.embedContainer = this.rootDoc;
- const view = this._editorView!;
- view.dispatch(view.state.tr.insert(view.posAtCoords({ left: de.x, top: de.y })!.pos, node));
- e.stopPropagation();
} // otherwise, fall through to outer collection to handle drop
+ !added && e.preventDefault();
+ e.stopPropagation();
+ return added;
}
+ return false;
};
getNodeEndpoints(context: Node, node: Node): { from: number; to: number } | null {
@@ -650,7 +668,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
const hr = Math.round(Date.now() / 1000 / 60 / 60);
numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-hr-' + (hr - i), { opacity: ((10 - i - 1) / 10).toString() }));
}
- this.layoutDoc[CssSym] = this.layoutDoc[CssSym] + 1; // css changes happen outside of react/mobx. so we need to set a flag that will notify anyone intereted in layout changes triggered by css changes (eg., CollectionLinkView)
+ this.layoutDoc[DocCss] = this.layoutDoc[DocCss] + 1; // css changes happen outside of react/mobx. so we need to set a flag that will notify anyone intereted in layout changes triggered by css changes (eg., CollectionLinkView)
};
@observable _showSidebar = false;
@@ -667,7 +685,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
this.layoutDoc._width = !preview && this.SidebarShown ? NumCast(this.layoutDoc._width) * 2 : Math.max(20, NumCast(this.layoutDoc._width) * prevWidth);
};
sidebarDown = (e: React.PointerEvent) => {
- const batch = UndoManager.StartBatch('sidebar');
+ const batch = UndoManager.StartBatch('toggle sidebar');
setupMoveUpEvents(
this,
e,
@@ -686,7 +704,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
.scale(this.props.NativeDimScaling?.() || 1)
.transformDirection(delta[0], delta[1]);
const sidebarWidth = (NumCast(this.layoutDoc._width) * Number(this.layout_sidebarWidthPercent.replace('%', ''))) / 100;
- const width = this.layoutDoc[WidthSym]() + localDelta[0];
+ const width = this.layoutDoc[Width]() + localDelta[0];
this.layoutDoc._layout_sidebarWidthPercent = Math.max(0, (sidebarWidth + localDelta[0]) / width) * 100 + '%';
this.layoutDoc.width = width;
this.layoutDoc._layout_showSidebar = this.layoutDoc._layout_sidebarWidthPercent !== '0%';
@@ -694,13 +712,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
return false;
};
- @undoBatch
deleteAnnotation = (anchor: Doc) => {
+ const batch = UndoManager.StartBatch('delete link');
LinkManager.Instance.deleteLink(LinkManager.Links(anchor)[0]);
// const docAnnotations = DocListCast(this.props.dataDoc[this.fieldKey]);
// this.props.dataDoc[this.fieldKey] = new List<Doc>(docAnnotations.filter(a => a !== this.annoTextRegion));
// AnchorMenu.Instance.fadeOut(true);
this.props.select(false);
+ setTimeout(batch.end); // wait for reaction to remove link from document
};
@undoBatch
@@ -733,17 +752,22 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
.split(' ')
.filter(h => h);
const anchorDoc = Array.from(hrefs).lastElement().replace(Doc.localServerPath(), '').split('?')[0];
+ const deleteMarkups = undoBatch(() => {
+ const sel = editor.state.selection;
+ editor.dispatch(editor.state.tr.removeMark(sel.from, sel.to, editor.state.schema.marks.linkAnchor));
+ });
e.persist();
anchorDoc &&
DocServer.GetRefField(anchorDoc).then(
action(anchor => {
+ anchor && SelectionManager.SelectSchemaViewDoc(anchor as Doc);
AnchorMenu.Instance.Status = 'annotation';
- AnchorMenu.Instance.Delete = () => this.deleteAnnotation(anchor as Doc);
+ AnchorMenu.Instance.Delete = !anchor && editor.state.selection.empty ? returnFalse : !anchor ? deleteMarkups : () => this.deleteAnnotation(anchor as Doc);
AnchorMenu.Instance.Pinned = false;
- AnchorMenu.Instance.PinToPres = () => this.pinToPres(anchor as Doc);
- AnchorMenu.Instance.MakeTargetToggle = () => this.makeTargetToggle(anchor as Doc);
- AnchorMenu.Instance.ShowTargetTrail = () => this.showTargetTrail(anchor as Doc);
- AnchorMenu.Instance.IsTargetToggler = () => this.isTargetToggler(anchor as Doc);
+ AnchorMenu.Instance.PinToPres = !anchor ? returnFalse : () => this.pinToPres(anchor as Doc);
+ AnchorMenu.Instance.MakeTargetToggle = !anchor ? returnFalse : () => this.makeTargetToggle(anchor as Doc);
+ AnchorMenu.Instance.ShowTargetTrail = !anchor ? returnFalse : () => this.showTargetTrail(anchor as Doc);
+ AnchorMenu.Instance.IsTargetToggler = !anchor ? returnFalse : () => this.isTargetToggler(anchor as Doc);
AnchorMenu.Instance.jumpTo(e.clientX, e.clientY, true);
})
);
@@ -808,9 +832,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
icon: !this.Document._layout_noSidebar ? 'eye-slash' : 'eye',
});
uicontrols.push({
- description: (this.Document._layout_altContentUI ? 'Hide' : 'Show') + ' Alt Content UI',
- event: () => (this.layoutDoc._layout_altContentUI = !this.layoutDoc._layout_altContentUI),
- icon: !this.Document._layout_altContentUI ? 'eye-slash' : 'eye',
+ description: (this.Document._layout_enableAltContentUI ? 'Hide' : 'Show') + ' Alt Content UI',
+ event: () => (this.layoutDoc._layout_enableAltContentUI = !this.layoutDoc._layout_enableAltContentUI),
+ icon: !this.Document._layout_enableAltContentUI ? 'eye-slash' : 'eye',
});
uicontrols.push({ description: 'Show Highlights...', noexpand: true, subitems: highlighting, icon: 'hand-point-right' });
!Doc.noviceMode &&
@@ -823,7 +847,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
const appearance = cm.findByDescription('Appearance...');
const appearanceItems = appearance && 'subitems' in appearance ? appearance.subitems : [];
- appearanceItems.push({ description: 'Change Perspective...', noexpand: true, subitems: changeItems, icon: 'external-link-alt' });
+ appearanceItems.push({ description: 'Change Style...', noexpand: true, subitems: changeItems, icon: 'external-link-alt' });
// this.rootDoc.isTemplateDoc && appearanceItems.push({ description: "Make Default Layout", event: async () => Doc.UserDoc().defaultTextLayout = new PrefetchProxy(this.rootDoc), icon: "eye" });
!Doc.noviceMode &&
@@ -862,11 +886,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
const optionItems = options && 'subitems' in options ? options.subitems : [];
optionItems.push({ description: `Generate Dall-E Image`, event: () => this.generateImage(), icon: 'star' });
optionItems.push({ description: `Ask GPT-3`, event: () => this.askGPT(), icon: 'lightbulb' });
- optionItems.push({
- description: !this.Document._createDocOnCR ? 'Create New Doc on Carriage Return' : 'Allow Carriage Returns',
- event: () => (this.layoutDoc._createDocOnCR = !this.layoutDoc._createDocOnCR),
- icon: !this.Document._createDocOnCR ? 'grip-lines' : 'bars',
- });
+ this.props.renderDepth &&
+ optionItems.push({
+ description: !this.Document._createDocOnCR ? 'Create New Doc on Carriage Return' : 'Allow Carriage Returns',
+ event: () => (this.layoutDoc._createDocOnCR = !this.layoutDoc._createDocOnCR),
+ icon: !this.Document._createDocOnCR ? 'grip-lines' : 'bars',
+ });
!Doc.noviceMode &&
optionItems.push({
description: `${this.Document._layout_autoHeight ? 'Lock' : 'Auto'} Height`,
@@ -906,7 +931,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
let image_url = await gptImageCall((this.dataDoc.text as RichTextField)?.Text);
if (image_url) {
const [result] = await Networking.PostToServer('/uploadRemoteImage', { sources: [image_url] });
- const source = Utils.prepend(result.accessPaths.agnostic.client);
+ const source = result.accessPaths.agnostic.client;
const newDoc = Docs.Create.ImageDocument(source, {
x: NumCast(this.rootDoc.x) + NumCast(this.layoutDoc._width) + 10,
y: NumCast(this.rootDoc.y),
@@ -915,10 +940,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
data_nativeWidth: result.nativeWidth,
data_nativeHeight: result.nativeHeight,
});
- if (DocListCast(Doc.MyOverlayDocs?.data).includes(this.rootDoc)) {
+ if (Doc.IsInMyOverlay(this.rootDoc)) {
newDoc.overlayX = this.rootDoc.x;
newDoc.overlayY = NumCast(this.rootDoc.y) + NumCast(this.rootDoc._height);
- Doc.AddDocToList(Doc.MyOverlayDocs, undefined, newDoc);
+ Doc.AddToMyOverlay(newDoc);
} else {
this.props.addDocument?.(newDoc);
}
@@ -960,7 +985,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
if (this._editorView && this._recordingStart) {
if (this._break) {
const textanchorFunc = () => {
- const tanch = Docs.Create.TextanchorDocument({ title: 'dictation anchor', unrendered: true });
+ const tanch = Docs.Create.ConfigDocument({ title: 'dictation anchor' });
return this.addDocument(tanch) ? tanch : undefined;
};
const link = DocUtils.MakeLinkToActiveAudio(textanchorFunc, false).lastElement();
@@ -998,7 +1023,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
const splitter = state.schema.marks.splitter.create({ id: Utils.GenerateGuid() });
let tr = state.tr.addMark(sel.from, sel.to, splitter);
if (sel.from !== sel.to) {
- const anchor = anchorDoc ?? Docs.Create.TextanchorDocument({ title: '#' + this._editorView?.state.doc.textBetween(sel.from, sel.to), annotationOn: this.dataDoc, unrendered: true });
+ const anchor =
+ anchorDoc ??
+ Docs.Create.ConfigDocument({
+ //
+ title: 'text(' + this._editorView?.state.doc.textBetween(sel.from, sel.to) + ')',
+ annotationOn: this.dataDoc,
+ });
const href = targetHref ?? Doc.localServerPath(anchor);
if (anchor !== anchorDoc && addAsAnnotation) this.addDocument(anchor);
tr.doc.nodesBetween(sel.from, sel.to, (node: any, pos: number, parent: any) => {
@@ -1014,6 +1045,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
this._editorView!.dispatch(tr.removeMark(sel.from, sel.to, splitter));
this.dataDoc[UpdatingFromServer] = this.dataDoc[ForceServerWrite] = false;
anchor.text = selectedText;
+ anchor.title = selectedText.substring(0, 30);
return anchor;
}
return anchorDoc ?? this.rootDoc;
@@ -1022,7 +1054,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
}
getView = async (doc: Doc) => {
- if (DocListCast(this.rootDoc[this.SidebarKey]).find(anno => Doc.AreProtosEqual(doc.unrendered ? DocCast(doc.annotationOn) : doc, anno))) {
+ if (DocListCast(this.rootDoc[this.SidebarKey]).find(anno => Doc.AreProtosEqual(doc.layout_unrendered ? DocCast(doc.annotationOn) : doc, anno))) {
!this.SidebarShown && this.toggleSidebar(false);
setTimeout(() => this._sidebarRef?.current?.makeDocUnfiltered(doc));
}
@@ -1129,11 +1161,17 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
() => ({ sidebarHeight: this.sidebarHeight, textHeight: this.textHeight, layout_autoHeight: this.layout_autoHeight, marginsHeight: this.layout_autoHeightMargins }),
({ sidebarHeight, textHeight, layout_autoHeight, marginsHeight }) => {
const newHeight = this.contentScaling * (marginsHeight + Math.max(sidebarHeight, textHeight));
- if (layout_autoHeight && newHeight && newHeight !== this.rootDoc.height && !this.props.dontRegisterView) {
+ if (
+ (!Array.from(FormattedTextBox._globalHighlights).includes('Bold Text') || this.props.isSelected()) && //
+ layout_autoHeight &&
+ newHeight &&
+ newHeight !== this.rootDoc.height &&
+ !this.props.dontRegisterView
+ ) {
this.props.setHeight?.(newHeight);
}
},
- { fireImmediately: true }
+ { fireImmediately: !Array.from(FormattedTextBox._globalHighlights).includes('Bold Text') }
);
this._disposers.links = reaction(
() => LinkManager.Links(this.dataDoc), // if a link is deleted, then remove all hyperlinks that reference it from the text's marks
@@ -1155,7 +1193,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
() => {
const dataDoc = Doc.IsDelegateField(DocCast(this.layoutDoc?.proto), this.fieldKey) ? DocCast(this.layoutDoc?.proto) : this?.dataDoc;
const whichDoc = !this.dataDoc || !this.layoutDoc ? undefined : dataDoc?.[this.fieldKey + '_noTemplate'] || !this.layoutDoc[this.fieldKey] ? dataDoc : this.layoutDoc;
- return !whichDoc ? undefined : { data: Cast(whichDoc[this.fieldKey], RichTextField, null), str: Field.toString(whichDoc[this.fieldKey]) };
+ return !whichDoc ? undefined : { data: Cast(whichDoc[this.fieldKey], RichTextField, null), str: Field.toString(DocCast(whichDoc[this.fieldKey])) };
},
incomingValue => {
if (this._editorView && this._applyingChange !== this.fieldKey) {
@@ -1199,15 +1237,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
this._disposers.selected = reaction(
() => this.props.isSelected(),
action(selected => {
+ selected && this.prepareForTyping();
if (FormattedTextBox._globalHighlights.has('Bold Text')) {
- this.layoutDoc[CssSym] = this.layoutDoc[CssSym] + 1; // css change happens outside of mobx/react, so this will notify anyone interested in the layout that it has changed
+ this.layoutDoc[DocCss] = this.layoutDoc[DocCss] + 1; // css change happens outside of mobx/react, so this will notify anyone interested in the layout that it has changed
}
if (RichTextMenu.Instance?.view === this._editorView && !selected) {
RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined);
}
if (this._editorView && selected) {
RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this.props);
- this.autoLink();
+ setTimeout(this.autoLink, 20);
}
// Accessing editor and text doc for gpt assisted text edits
if (this._editorView && selected) {
@@ -1474,11 +1513,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
}
const selectOnLoad = this.rootDoc[Id] === FormattedTextBox.SelectOnLoad && (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath()));
- if (selectOnLoad && !this.props.dontRegisterView && !this.props.dontSelectOnLoad && this.isActiveTab(this.ProseRef)) {
+ if (this._editorView && selectOnLoad && !this.props.dontRegisterView && !this.props.dontSelectOnLoad && this.isActiveTab(this.ProseRef)) {
const selLoadChar = FormattedTextBox.SelectOnLoadChar;
FormattedTextBox.SelectOnLoad = '';
this.props.select(false);
- if (selLoadChar && this._editorView) {
+ if (selLoadChar) {
const $from = this._editorView.state.selection.anchor ? this._editorView.state.doc.resolve(this._editorView.state.selection.anchor - 1) : undefined;
const mark = schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) });
const curMarks = this._editorView.state.storedMarks ?? $from?.marksAcross(this._editorView.state.selection.$head) ?? [];
@@ -1488,30 +1527,18 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
.insertText(FormattedTextBox.SelectOnLoadChar, this._editorView.state.doc.content.size - 1, this._editorView.state.doc.content.size)
.setStoredMarks(storedMarks);
this._editorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size))));
- } else if (this._editorView && curText && !FormattedTextBox.DontSelectInitialText) {
+ } else if (curText && !FormattedTextBox.DontSelectInitialText) {
selectAll(this._editorView.state, this._editorView?.dispatch);
- this.startUndoTypingBatch();
- } else if (this._editorView) {
- this._editorView.dispatch(this._editorView.state.tr.addStoredMark(schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) })));
}
}
selectOnLoad && this._editorView!.focus();
- // add user mark for any first character that was typed since the user mark that gets set in KeyPress won't have been called yet.
+ if (this.props.isContentActive()) this.prepareForTyping();
if (this._editorView) {
const tr = this._editorView.state.tr;
const { from, to } = tr.selection;
- // for some reason, the selection is sometimes lost in the sidebar view when prosemirror syncs the seledtion with the dom, so reset the selectoin after the document has ben fully instantiated.
+ // for some reason, the selection is sometimes lost in the sidebar view when prosemirror syncs the seledtion with the dom, so reset the selection after the document has ben fully instantiated.
if (FormattedTextBox.DontSelectInitialText) setTimeout(() => this._editorView?.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(from), tr.doc.resolve(to)))), 250);
- this._editorView.state.storedMarks = [
- ...(this._editorView.state.storedMarks ?? []),
- ...(!this._editorView.state.storedMarks?.some(mark => mark.type === schema.marks.user_mark) ? [schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) })] : []),
- ...(Doc.UserDoc().fontColor !== 'transparent' && Doc.UserDoc().fontColor ? [schema.mark(schema.marks.pFontColor, { color: StrCast(Doc.UserDoc().fontColor) })] : []),
- ...(Doc.UserDoc().fontStyle === 'italics' ? [schema.mark(schema.marks.em)] : []),
- ...(Doc.UserDoc().textDecoration === 'underline' ? [schema.mark(schema.marks.underline)] : []),
- ...(Doc.UserDoc().fontFamily ? [schema.mark(schema.marks.pFontFamily, { family: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.FontFamily) })] : []),
- ...(Doc.UserDoc().fontSize ? [schema.mark(schema.marks.pFontSize, { fontSize: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.FontSize) })] : []),
- ...(Doc.UserDoc().fontWeight === 'bold' ? [schema.mark(schema.marks.strong)] : []),
- ];
+
if (FormattedTextBox.PasteOnLoad) {
const pdfAnchorId = FormattedTextBox.PasteOnLoad.clipboardData?.getData('dash/pdfAnchor');
FormattedTextBox.PasteOnLoad = undefined;
@@ -1521,9 +1548,31 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
FormattedTextBox.DontSelectInitialText = false;
}
+ // add user mark for any first character that was typed since the user mark that gets set in KeyPress won't have been called yet.
+ prepareForTyping = () => {
+ this._editorView?.dispatch(
+ this._editorView?.state.tr.setStoredMarks([
+ ...(this._editorView.state.storedMarks?.filter(mark => ![schema.marks.em, schema.marks.underline, schema.marks.pFontFamily, schema.marks.pFontSize, schema.marks.strong, schema.marks.pFontColor].includes(mark.type)) ?? []),
+ ...[schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) })],
+ ...(Doc.UserDoc().fontColor !== 'transparent' && Doc.UserDoc().fontColor ? [schema.mark(schema.marks.pFontColor, { color: StrCast(Doc.UserDoc().fontColor) })] : []),
+ ...(Doc.UserDoc().fontStyle === 'italics' ? [schema.mark(schema.marks.em)] : []),
+ ...(Doc.UserDoc().textDecoration === 'underline' ? [schema.mark(schema.marks.underline)] : []),
+ ...(Doc.UserDoc().fontFamily ? [schema.mark(schema.marks.pFontFamily, { family: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.FontFamily) })] : []),
+ ...(Doc.UserDoc().fontSize ? [schema.mark(schema.marks.pFontSize, { fontSize: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.FontSize) })] : []),
+ ...(Doc.UserDoc().fontWeight === 'bold' ? [schema.mark(schema.marks.strong)] : []),
+ ])
+ );
+ };
+
+ @action
componentWillUnmount() {
+ if (this._recording) {
+ this._recording = !this._recording;
+ }
Object.values(this._disposers).forEach(disposer => disposer?.());
this.endUndoTypingBatch();
+ FormattedTextBox.LiveTextUndo?.end();
+ FormattedTextBox.LiveTextUndo = undefined;
this.unhighlightSearchTerms();
this._editorView?.destroy();
RichTextMenu.Instance?.TextView === this && RichTextMenu.Instance.updateMenu(undefined, undefined, undefined);
@@ -1558,8 +1607,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
}
if (this._recording && !e.ctrlKey && e.button === 0) {
this.breakupDictation();
- e.preventDefault();
- e.stopPropagation();
}
this._downX = e.clientX;
this._downY = e.clientY;
@@ -1624,7 +1671,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
onFocused = (e: React.FocusEvent): void => {
//applyDevTools.applyDevTools(this._editorView);
this.ProseRef?.children[0] === e.nativeEvent.target && this._editorView && RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this.props);
- this.startUndoTypingBatch();
+ e.stopPropagation();
};
onClick = (e: React.MouseEvent): void => {
@@ -1699,13 +1746,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
}
}
startUndoTypingBatch() {
- !this._undoTyping && (this._undoTyping = UndoManager.StartBatch('undoTyping'));
+ !this._undoTyping && (this._undoTyping = UndoManager.StartBatch('text edits on ' + this.rootDoc.title));
}
public endUndoTypingBatch() {
- const wasUndoing = this._undoTyping;
this._undoTyping?.end();
this._undoTyping = undefined;
- return wasUndoing;
}
@action
@@ -1733,7 +1778,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
const state = this._editorView!.state;
const curText = state.doc.textBetween(0, state.doc.content.size, ' \n');
- if (this.layoutDoc.sidebarViewType === 'translation' && !this.fieldKey.includes('translation') && curText.endsWith(' ') && curText !== this._lastText) {
+ if (this.layoutDoc.sidebar_collectionType === 'translation' && !this.fieldKey.includes('translation') && curText.endsWith(' ') && curText !== this._lastText) {
try {
translateGoogleApi(curText, { from: 'en', to: 'es' }).then((result1: any) => {
setTimeout(
@@ -1799,8 +1844,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
default:
if (this._lastTimedMark?.attrs.userid === Doc.CurrentUserEmail) break;
case ' ':
- [AclEdit, AclAdmin, AclSelfEdit].includes(GetEffectiveAcl(this.dataDoc)) &&
- this._editorView!.dispatch(this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark.create({})).addStoredMark(schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) })));
+ if (e.code !== 'Space') {
+ [AclEdit, AclAugment, AclAdmin].includes(GetEffectiveAcl(this.rootDoc)) &&
+ this._editorView!.dispatch(this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark).addStoredMark(schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) })));
+ }
+ break;
}
this.startUndoTypingBatch();
};
@@ -1822,17 +1870,18 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
tryUpdateScrollHeight = () => {
const margins = 2 * NumCast(this.layoutDoc._yMargin, this.props.yPadding || 0);
const children = this.ProseRef?.children.length ? Array.from(this.ProseRef.children[0].children) : undefined;
- if (children) {
+ if (children && !SnappingManager.GetIsDragging()) {
const toNum = (val: string) => Number(val.replace('px', '').replace('auto', '0'));
const toHgt = (node: Element) => {
const { height, marginTop, marginBottom } = getComputedStyle(node);
return toNum(height) + Math.max(0, toNum(marginTop)) + Math.max(0, toNum(marginBottom));
};
const proseHeight = !this.ProseRef ? 0 : children.reduce((p, child) => p + toHgt(child), margins);
- const scrollHeight = this.ProseRef && Math.min(NumCast(this.layoutDoc.docMaxAutoHeight, proseHeight), proseHeight);
+ const scrollHeight = this.ProseRef && Math.min(NumCast(this.layoutDoc.layout_maxAutoHeight, proseHeight), proseHeight);
if (this.props.setHeight && scrollHeight && !this.props.dontRegisterView) {
// if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation
const setScrollHeight = () => (this.rootDoc[this.fieldKey + '_scrollHeight'] = scrollHeight);
+
if (this.rootDoc === this.layoutDoc || this.layoutDoc.resolvedDataDoc) {
setScrollHeight();
} else {
@@ -1941,25 +1990,25 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
fitContentsToBox={this.fitContentsToBox}
noSidebar={true}
treeViewHideTitle={true}
- fieldKey={this.layoutDoc.sidebarViewType === 'translation' ? `${this.fieldKey}_translation` : `${this.fieldKey}_sidebar`}
+ fieldKey={this.layoutDoc.sidebar_collectionType === 'translation' ? `${this.fieldKey}_translation` : `${this.fieldKey}_sidebar`}
/>
</div>
);
};
return (
<div className={'formattedTextBox-sidebar' + (Doc.ActiveTool !== InkTool.None ? '-inking' : '')} style={{ width: `${this.layout_sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}>
- {renderComponent(StrCast(this.layoutDoc.sidebarViewType))}
+ {renderComponent(StrCast(this.layoutDoc.sidebar_collectionType))}
</div>
);
}
cycleAlternateText = () => {
- if (this.layoutDoc._layout_altContentUI) {
- const usePath = this.rootDoc[`${this.props.fieldKey}_usePath`];
+ if (this.layoutDoc._layout_enableAltContentUI) {
+ const usePath = this.rootDoc[`_${this.props.fieldKey}_usePath`];
this.rootDoc[`_${this.props.fieldKey}_usePath`] = usePath === undefined ? 'alternate' : usePath === 'alternate' ? 'alternate:hover' : undefined;
}
};
@computed get overlayAlternateIcon() {
- const usePath = this.rootDoc[`${this.props.fieldKey}_usePath`];
+ const usePath = this.rootDoc[`_${this.props.fieldKey}_usePath`];
return (
<Tooltip
title={
@@ -1995,62 +2044,69 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
return this.props.fieldKey + (usePath && (!usePath.includes(':hover') || this._isHovering) ? `_${usePath.replace(':hover', '')}` : '');
}
@observable _isHovering = false;
+ onPassiveWheel = (e: WheelEvent) => {
+ // 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)
+ if (this.props.isContentActive() && !this.props.allowScroll) {
+ if (!NumCast(this.layoutDoc._layout_scrollTop) && e.deltaY <= 0) e.preventDefault();
+ e.stopPropagation();
+ }
+ };
+ _oldWheel: any;
+ @computed get fontColor() {
+ return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color);
+ }
+ @computed get fontSize() {
+ return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontSize);
+ }
+ @computed get fontFamily() {
+ return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontFamily);
+ }
+ @computed get fontWeight() {
+ return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontWeight);
+ }
render() {
TraceMobx();
- const active = this.props.isContentActive() || this.props.isSelected();
- const selected = active;
const scale = (this.props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._freeform_scale, 1);
- const rounded = StrCast(this.layoutDoc.borderRounding) === '100%' ? '-rounded' : '';
- const interactive = (Doc.ActiveTool === InkTool.None || SnappingManager.GetIsDragging()) && (this.layoutDoc.z || !this.layoutDoc._lockedPosition);
- if (!selected && FormattedTextBoxComment.textBox === this) setTimeout(FormattedTextBoxComment.Hide);
- const minimal = this.props.ignoreAutoHeight;
+ const rounded = StrCast(this.layoutDoc.layout_borderRounding) === '100%' ? '-rounded' : '';
+ setTimeout(() => !this.props.isContentActive() && FormattedTextBoxComment.textBox === this && FormattedTextBoxComment.Hide);
const paddingX = NumCast(this.layoutDoc._xMargin, this.props.xPadding || 0);
const paddingY = NumCast(this.layoutDoc._yMargin, this.props.yPadding || 0);
- const selPad = (selected && !this.layoutDoc._createDocOnCR) || minimal ? Math.min(paddingY, Math.min(paddingX, 10)) : 0;
- const selPaddingClass = selected && !this.layoutDoc._createDocOnCR && paddingY >= 10 ? '-selected' : '';
const styleFromLayoutString = Doc.styleFromLayoutString(this.rootDoc, this.layoutDoc, this.props, scale); // this converts any expressions in the format string to style props. e.g., <FormattedTextBox height='{this._headerHeight}px' >
return styleFromLayoutString?.height === '0px' ? null : (
<div
className="formattedTextBox"
onPointerEnter={action(() => (this._isHovering = true))}
onPointerLeave={action(() => (this._isHovering = false))}
- 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 (this.props.isContentActive() && !this.props.allowScroll) {
- if (!NumCast(this.layoutDoc._layout_scrollTop) && e.deltaY <= 0) e.preventDefault();
- e.stopPropagation();
- }
- },
- { passive: false }
- )
- }
+ ref={r => {
+ this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel);
+ this._oldWheel = r;
+ r?.addEventListener('wheel', this.onPassiveWheel, { passive: false });
+ }}
style={{
...(this.props.dontScale
? {}
: {
transform: `scale(${scale})`,
- transformOrigin: 'top left',
width: `${100 / scale}%`,
height: `${100 / scale}%`,
}),
- display: !SnappingManager.GetIsDragging() && this.props.thumbShown?.() ? 'none' : undefined,
+ // display: !this.props.isContentActive() && this.props.thumbShown?.() ? 'none' : undefined,
transition: 'inherit',
// overflowY: this.layoutDoc._layout_autoHeight ? "hidden" : undefined,
- color: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color),
- fontSize: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontSize),
- fontFamily: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontFamily),
- fontWeight: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontWeight),
+ color: this.fontColor,
+ fontSize: this.fontSize,
+ fontFamily: this.fontFamily,
+ fontWeight: this.fontWeight,
...styleFromLayoutString,
}}>
<div
className="formattedTextBox-cont"
ref={this._ref}
style={{
+ cursor: this.props.isContentActive() ? 'text' : undefined,
overflow: this.layout_autoHeight && this.props.CollectionFreeFormDocumentView?.() ? 'hidden' : undefined, //x this breaks viewing an layout_autoHeight doc in its own tab, or in the lightbox
height: this.props.height || (this.layout_autoHeight && this.props.renderDepth && !this.props.suppressSetHeight ? 'max-content' : undefined),
- pointerEvents: interactive ? undefined : 'none',
+ pointerEvents: Doc.ActiveTool === InkTool.None && !this.props.onBrowseClick?.() ? undefined : 'none',
}}
onContextMenu={this.specificContextMenu}
onKeyDown={this.onKeyDown}
@@ -2062,32 +2118,30 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
onPointerDown={this.onPointerDown}
onDoubleClick={this.onDoubleClick}>
<div
- className={`formattedTextBox-outer${selected ? '-selected' : ''}`}
+ className={`formattedTextBox-outer`}
ref={this._scrollRef}
style={{
width: this.props.dontSelectOnLoad ? '100%' : `calc(100% - ${this.layout_sidebarWidthPercent})`,
- pointerEvents: !active && !SnappingManager.GetIsDragging() ? 'none' : undefined,
overflow: this.layoutDoc._createDocOnCR ? 'hidden' : this.layoutDoc._layout_autoHeight ? 'visible' : undefined,
}}
onScroll={this.onScroll}
onDrop={this.ondrop}>
<div
- className={minimal ? 'formattedTextBox-minimal' : `formattedTextBox-inner${rounded}${selPaddingClass}`}
+ className={`formattedTextBox-inner${rounded}`}
ref={this.createDropTarget}
style={{
padding: StrCast(this.layoutDoc._textBoxPadding),
- paddingLeft: StrCast(this.layoutDoc._textBoxPaddingX, `${paddingX - selPad}px`),
- paddingRight: StrCast(this.layoutDoc._textBoxPaddingX, `${paddingX - selPad}px`),
- paddingTop: StrCast(this.layoutDoc._textBoxPaddingY, `${paddingY - selPad}px`),
- paddingBottom: StrCast(this.layoutDoc._textBoxPaddingY, `${paddingY - selPad}px`),
- pointerEvents: !active && !SnappingManager.GetIsDragging() ? (IsFollowLinkScript(this.layoutDoc.onClick) ? 'none' : undefined) : undefined,
+ paddingLeft: StrCast(this.layoutDoc._textBoxPaddingX, `${paddingX}px`),
+ paddingRight: StrCast(this.layoutDoc._textBoxPaddingX, `${paddingX}px`),
+ paddingTop: StrCast(this.layoutDoc._textBoxPaddingY, `${paddingY}px`),
+ paddingBottom: StrCast(this.layoutDoc._textBoxPaddingY, `${paddingY}px`),
}}
/>
</div>
{this.noSidebar || this.props.dontSelectOnLoad || !this.SidebarShown || this.layout_sidebarWidthPercent === '0%' ? null : this.sidebarCollection}
{this.noSidebar || this.Document._layout_noSidebar || this.props.dontSelectOnLoad || this.Document._createDocOnCR ? null : this.sidebarHandle}
{this.audioHandle}
- {this.layoutDoc._layout_altContentUI ? this.overlayAlternateIcon : null}
+ {this.layoutDoc._layout_enableAltContentUI ? this.overlayAlternateIcon : null}
</div>
</div>
);
diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
index 4dfe07b24..112a0d87e 100644
--- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
+++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
@@ -4,7 +4,7 @@ import { Schema } from 'prosemirror-model';
import { splitListItem, wrapInList } from 'prosemirror-schema-list';
import { EditorState, NodeSelection, TextSelection, Transaction } from 'prosemirror-state';
import { liftTarget } from 'prosemirror-transform';
-import { AclAugment, AclSelfEdit, Doc } from '../../../../fields/Doc';
+import { AclAdmin, AclAugment, AclEdit } from '../../../../fields/DocSymbols';
import { GetEffectiveAcl } from '../../../../fields/util';
import { Utils } from '../../../../Utils';
import { Docs } from '../../../documents/Documents';
@@ -12,6 +12,7 @@ import { RTFMarkup } from '../../../util/RTFMarkup';
import { SelectionManager } from '../../../util/SelectionManager';
import { OpenWhere } from '../DocumentView';
import { liftListItem, sinkListItem } from './prosemirrorPatches.js';
+import { Doc } from '../../../../fields/Doc';
const mac = typeof navigator !== 'undefined' ? /Mac/.test(navigator.platform) : false;
@@ -48,15 +49,11 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
const canEdit = (state: any) => {
switch (GetEffectiveAcl(props.DataDoc)) {
case AclAugment:
- return false;
- case AclSelfEdit:
- for (var i = state.selection.from; i < state.selection.to; i++) {
- const marks = state.doc.resolve(i)?.marks?.();
- if (marks?.some((mark: any) => mark.type === schema.marks.user_mark && mark.attrs.userid !== Doc.CurrentUserEmail)) {
- return false;
- }
+ const prevNode = state.selection.$cursor.nodeBefore;
+ const prevUser = !prevNode ? Doc.CurrentUserEmail : prevNode.marks[prevNode.marks.length - 1].attrs.userid;
+ if (prevUser != Doc.CurrentUserEmail) {
+ return false;
}
- break;
}
return true;
};
@@ -337,7 +334,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
//Command to create a blank space
bind('Space', (state: EditorState, dispatch: (tx: Transaction) => void) => {
- if (!canEdit(state)) return true;
+ if (GetEffectiveAcl(props.DataDoc) != AclEdit && GetEffectiveAcl(props.DataDoc) != AclAugment && GetEffectiveAcl(props.DataDoc) != AclAdmin) return true;
const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
dispatch(splitMetadata(marks, state.tr));
return false;
diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx
index 5e0041b84..7c3e4baad 100644
--- a/src/client/views/nodes/formattedText/RichTextMenu.tsx
+++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx
@@ -138,7 +138,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
this.activeListType = this.getActiveListStyle();
this._activeAlignment = this.getActiveAlignment();
- this._activeFontFamily = !activeFamilies.length ? StrCast(this.TextView?.Document.fontFamily, StrCast(Doc.UserDoc().fontFamily, 'Arial')) : activeFamilies.length === 1 ? String(activeFamilies[0]) : 'various';
+ this._activeFontFamily = !activeFamilies.length ? StrCast(this.TextView?.Document._text_fontFamily, StrCast(Doc.UserDoc().fontFamily, 'Arial')) : activeFamilies.length === 1 ? String(activeFamilies[0]) : 'various';
this._activeFontSize = !activeSizes.length ? StrCast(this.TextView?.Document.fontSize, StrCast(Doc.UserDoc().fontSize, '10px')) : activeSizes[0];
this._activeFontColor = !activeColors.length ? StrCast(this.TextView?.Document.fontColor, StrCast(Doc.UserDoc().fontColor, 'black')) : activeColors.length > 0 ? String(activeColors[0]) : '...';
this._activeHighlightColor = !activeHighlights.length ? '' : activeHighlights.length > 0 ? String(activeHighlights[0]) : '...';
@@ -221,7 +221,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
m.type === state.schema.marks.marker && activeHighlights.add(String(m.attrs.highlight));
});
} else if (SelectionManager.Views().some(dv => dv.ComponentView instanceof EquationBox)) {
- SelectionManager.Views().forEach(dv => StrCast(dv.rootDoc._fontSize) && activeSizes.add(StrCast(dv.rootDoc._fontSize)));
+ SelectionManager.Views().forEach(dv => StrCast(dv.rootDoc._text_fontSize) && activeSizes.add(StrCast(dv.rootDoc._text_fontSize)));
}
return { activeFamilies: Array.from(activeFamilies), activeSizes: Array.from(activeSizes), activeColors: Array.from(activeColors), activeHighlights: Array.from(activeHighlights) };
}
@@ -345,8 +345,8 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
this.view.focus();
}
} else if (SelectionManager.Views().some(dv => dv.ComponentView instanceof EquationBox)) {
- SelectionManager.Views().forEach(dv => (dv.rootDoc._fontSize = fontSize));
- } else Doc.UserDoc()._fontSize = fontSize;
+ SelectionManager.Views().forEach(dv => (dv.rootDoc._text_fontSize = fontSize));
+ } else Doc.UserDoc().fontSize = fontSize;
this.updateMenu(this.view, undefined, this.props);
};
@@ -355,7 +355,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
const fmark = this.view.state.schema.marks.pFontFamily.create({ family: family });
this.setMark(fmark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(fmark)), true);
this.view.focus();
- } else Doc.UserDoc()._fontFamily = family;
+ } else Doc.UserDoc().fontFamily = family;
this.updateMenu(this.view, undefined, this.props);
};
@@ -758,11 +758,11 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
// <div className="collectionMenu-divider" key="divider 3" />
// {[this.createMarksDropdown(this.activeFontSize, this.fontSizeOptions, "font size", action((val: string) => {
// this.activeFontSize = val;
- // SelectionManager.Views().map(dv => dv.props.Document._fontSize = val);
+ // SelectionManager.Views().map(dv => dv.props.Document._text_fontSize = val);
// })),
// this.createMarksDropdown(this.activeFontFamily, this.fontFamilyOptions, "font family", action((val: string) => {
// this.activeFontFamily = val;
- // SelectionManager.Views().map(dv => dv.props.Document._fontFamily = val);
+ // SelectionManager.Views().map(dv => dv.props.Document._text_fontFamily = val);
// })),
// <div className="collectionMenu-divider" key="divider 4" />,
// this.createNodesDropdown(this.activeListType, this.listTypeOptions, "list type", () => ({})),
diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts
index 104aed058..8bafc2cef 100644
--- a/src/client/views/nodes/formattedText/RichTextRules.ts
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -1,11 +1,11 @@
import { ellipsis, emDash, InputRule, smartQuotes, textblockTypeInputRule } from 'prosemirror-inputrules';
import { NodeSelection, TextSelection } from 'prosemirror-state';
-import { DataSym, Doc, StrListCast } from '../../../../fields/Doc';
+import { Doc, StrListCast } from '../../../../fields/Doc';
+import { DocData } from '../../../../fields/DocSymbols';
import { Id } from '../../../../fields/FieldSymbols';
import { List } from '../../../../fields/List';
import { ComputedField } from '../../../../fields/ScriptField';
-import { NumCast, StrCast } from '../../../../fields/Types';
-import { normalizeEmail } from '../../../../fields/util';
+import { NumCast } from '../../../../fields/Types';
import { Utils } from '../../../../Utils';
import { DocServer } from '../../../DocServer';
import { Docs, DocUtils } from '../../../documents/Documents';
@@ -76,12 +76,21 @@ export class RichTextRules {
//Create annotation to a field on the text document
new InputRule(new RegExp(/>>$/), (state, match, start, end) => {
- const textDoc = this.Document[DataSym];
+ const textDoc = this.Document[DocData];
const numInlines = NumCast(textDoc.inlineTextCount);
textDoc.inlineTextCount = numInlines + 1;
const inlineFieldKey = 'inline' + numInlines; // which field on the text document this annotation will write to
const inlineLayoutKey = 'layout_' + inlineFieldKey; // the field holding the layout string that will render the inline annotation
- const textDocInline = Docs.Create.TextDocument('', { _layout_fieldKey: inlineLayoutKey, _width: 75, _height: 35, annotationOn: textDoc, _layout_fitWidth: true, _layout_autoHeight: true, _fontSize: '9px', title: 'inline comment' });
+ const textDocInline = Docs.Create.TextDocument('', {
+ _layout_fieldKey: inlineLayoutKey,
+ _width: 75,
+ _height: 35,
+ annotationOn: textDoc,
+ _layout_fitWidth: true,
+ _layout_autoHeight: true,
+ _text_fontSize: '9px',
+ title: 'inline comment',
+ });
textDocInline.title = inlineFieldKey; // give the annotation its own title
textDocInline.title_custom = true; // And make sure that it's 'custom' so that editing text doesn't change the title of the containing doc
textDocInline.isTemplateForField = inlineFieldKey; // this is needed in case the containing text doc is converted to a template at some point
@@ -234,13 +243,13 @@ export class RichTextRules {
// create a text display of a metadata field on this or another document, or create a hyperlink portal to another document
// [[<fieldKey> : <Doc>]]
- // [[:Doc]] => hyperlink
+ // [[:docTitle]] => hyperlink
// [[fieldKey]] => show field
// [[fieldKey=value]] => show field and also set its value
- // [[fieldKey:Doc]] => show field of doc
+ // [[fieldKey:docTitle]] => show field of doc
new InputRule(new RegExp(/\[\[([a-zA-Z_\? \-0-9]*)(=[a-zA-Z_@\? /\-0-9]*)?(:[a-zA-Z_@:\.\? \-0-9]+)?\]\]$/), (state, match, start, end) => {
const fieldKey = match[1];
- const docId = match[3]?.replace(':', '');
+ const docTitle = match[3]?.replace(':', '');
const value = match[2]?.substring(1);
const linkToDoc = (target: Doc) => {
const rstate = this.TextBox.EditorView?.state;
@@ -257,21 +266,25 @@ export class RichTextRules {
}
};
if (!fieldKey) {
- if (docId) {
- const target = DocServer.QUERY_SERVER_CACHE(docId);
- if (target) setTimeout(() => linkToDoc(target));
- else DocServer.GetRefField(docId).then(docx => linkToDoc((docx instanceof Doc && docx) || Docs.Create.FreeformDocument([], { title: docId + '(auto)', _width: 500, _height: 500 }, docId)));
-
- return state.tr.deleteRange(end - 1, end).deleteRange(start, start + 3);
+ if (docTitle) {
+ const target = DocServer.FindDocByTitle(docTitle);
+ if (target) {
+ setTimeout(() => linkToDoc(target));
+ return state.tr.deleteRange(end - 1, end).deleteRange(start, start + 3);
+ }
}
return state.tr;
}
if (value !== '' && value !== undefined) {
const num = value.match(/^[0-9.]$/);
- this.Document[DataSym][fieldKey] = value === 'true' ? true : value === 'false' ? false : num ? Number(value) : value;
+ this.Document[DocData][fieldKey] = value === 'true' ? true : value === 'false' ? false : num ? Number(value) : value;
+ }
+ const target = DocServer.FindDocByTitle(docTitle);
+ if (target) {
+ const fieldView = state.schema.nodes.dashField.create({ fieldKey, docId: target[Id], hideKey: false });
+ return state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))).replaceSelectionWith(fieldView, true);
}
- const fieldView = state.schema.nodes.dashField.create({ fieldKey, docId, hideKey: false });
- return state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))).replaceSelectionWith(fieldView, true);
+ return state.tr;
}),
// create a text display of a metadata field on this or another document, or create a hyperlink portal to another document
@@ -303,11 +316,11 @@ export class RichTextRules {
new InputRule(new RegExp(/#([a-zA-Z_\-]+[a-zA-Z_\-0-9]*)\s$/), (state, match, start, end) => {
const tag = match[1];
if (!tag) return state.tr;
- //this.Document[DataSym]['#' + tag] = '#' + tag;
- const tags = StrListCast(this.Document[DataSym].tags);
+ //this.Document[DocData]['#' + tag] = '#' + tag;
+ const tags = StrListCast(this.Document[DocData].tags);
if (!tags.includes(tag)) {
tags.push(tag);
- this.Document[DataSym].tags = new List<string>(tags);
+ this.Document[DocData].tags = new List<string>(tags);
}
const fieldView = state.schema.nodes.dashField.create({ fieldKey: '#' + tag });
return state.tr
diff --git a/src/client/views/nodes/importBox/ImportElementBox.tsx b/src/client/views/nodes/importBox/ImportElementBox.tsx
new file mode 100644
index 000000000..58f0b29e4
--- /dev/null
+++ b/src/client/views/nodes/importBox/ImportElementBox.tsx
@@ -0,0 +1,38 @@
+import { computed } from 'mobx';
+import { observer } from 'mobx-react';
+import { Doc } from '../../../../fields/Doc';
+import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnOne, returnTrue } from '../../../../Utils';
+import { Transform } from '../../../util/Transform';
+import { ViewBoxBaseComponent } from '../../DocComponent';
+import { DefaultStyleProvider } from '../../StyleProvider';
+import { DocumentView, DocumentViewInternal } from '../DocumentView';
+import { FieldView, FieldViewProps } from '../FieldView';
+import React = require('react');
+
+@observer
+export class ImportElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
+ public static LayoutString(fieldKey: string) {
+ return FieldView.LayoutString(ImportElementBox, fieldKey);
+ }
+
+ screenToLocalXf = () => this.props.ScreenToLocalTransform().scale(1 * (this.props.NativeDimScaling?.() || 1));
+ @computed get mainItem() {
+ return (
+ <div style={{ backgroundColor: 'pink' }}>
+ <DocumentView
+ {...this.props} //
+ LayoutTemplateString={undefined}
+ Document={this.rootDoc}
+ isContentActive={returnFalse}
+ DataDoc={undefined}
+ addDocument={returnFalse}
+ ScreenToLocalTransform={this.screenToLocalXf}
+ hideResizeHandles={true}
+ />
+ </div>
+ );
+ }
+ render() {
+ return !(this.rootDoc instanceof Doc) ? null : this.mainItem;
+ }
+}
diff --git a/src/client/views/nodes/trails/PresBox.scss b/src/client/views/nodes/trails/PresBox.scss
index eb91c82f3..bf56b4d9e 100644
--- a/src/client/views/nodes/trails/PresBox.scss
+++ b/src/client/views/nodes/trails/PresBox.scss
@@ -951,6 +951,7 @@
margin-right: unset;
height: 100%;
position: relative;
+ user-select: none;
}
select {
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 0a907c958..c0cd3ab70 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -3,7 +3,8 @@ 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 { AnimationSym, Doc, DocListCast, Field, FieldResult, Opt, StrListCast } from '../../../../fields/Doc';
+import { Doc, DocListCast, Field, FieldResult, Opt, StrListCast } from '../../../../fields/Doc';
+import { Animation } from '../../../../fields/DocSymbols';
import { Copy, Id } from '../../../../fields/FieldSymbols';
import { InkField } from '../../../../fields/InkField';
import { List } from '../../../../fields/List';
@@ -41,8 +42,7 @@ export interface pinDataTypes {
scrollable?: boolean;
dataviz?: number[];
pannable?: boolean;
- map?:boolean;
- viewType?: boolean;
+ type_collection?: boolean;
inkable?: boolean;
filters?: boolean;
pivot?: boolean;
@@ -104,10 +104,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@observable _presKeyEvents: boolean = false;
@observable _forceKeyEvents: boolean = false;
@computed get isTreeOrStack() {
- return [CollectionViewType.Tree, CollectionViewType.Stacking].includes(StrCast(this.layoutDoc._viewType) as any);
+ return [CollectionViewType.Tree, CollectionViewType.Stacking].includes(StrCast(this.layoutDoc._type_collection) as any);
}
@computed get isTree() {
- return this.layoutDoc._viewType === CollectionViewType.Tree;
+ return this.layoutDoc._type_collection === CollectionViewType.Tree;
}
@computed get presFieldKey() {
return StrCast(this.layoutDoc.presFieldKey, 'data');
@@ -129,14 +129,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
public static targetRenderedDoc = (doc: Doc) => {
const targetDoc = Cast(doc?.presentationTargetDoc, Doc, null);
- return targetDoc?.unrendered ? DocCast(targetDoc.annotationOn) : targetDoc;
+ return targetDoc?.layout_unrendered ? DocCast(targetDoc.annotationOn) : targetDoc;
};
@computed get scrollable() {
- if ([DocumentType.PDF, DocumentType.WEB, DocumentType.RTF].includes(this.targetDoc.type as DocumentType) || this.targetDoc._viewType === CollectionViewType.Stacking) return true;
+ if ([DocumentType.PDF, DocumentType.WEB, DocumentType.RTF].includes(this.targetDoc.type as DocumentType) || this.targetDoc._type_collection === CollectionViewType.Stacking) return true;
return false;
}
@computed get panable() {
- if ((this.targetDoc.type === DocumentType.COL && this.targetDoc._viewType === CollectionViewType.Freeform) || this.targetDoc.type === DocumentType.IMG) return true;
+ if ((this.targetDoc.type === DocumentType.COL && this.targetDoc._type_collection === CollectionViewType.Freeform) || this.targetDoc.type === DocumentType.IMG) return true;
return false;
}
@computed get selectedDocumentView() {
@@ -197,7 +197,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
this.turnOffEdit(true);
this._disposers.selection = reaction(
() => SelectionManager.Views(),
- views => views.some(view => view.props.Document === this.rootDoc) && this.updateCurrentPresentation()
+ views => (!PresBox.Instance || views.some(view => view.props.Document === this.rootDoc)) && this.updateCurrentPresentation(),
+ { fireImmediately: true }
);
this._disposers.editing = reaction(
() => this.layoutDoc.presStatus === PresStatus.Edit,
@@ -275,7 +276,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
const targetList = PresBox.targetRenderedDoc(doc);
if (doc.presIndexed !== undefined && targetList) {
const listItems = (Cast(targetList[Doc.LayoutFieldKey(targetList)], listSpec(Doc), null)?.filter(d => d instanceof Doc) as Doc[]) ?? DocListCast(targetList[Doc.LayoutFieldKey(targetList) + '_annotations']);
- return listItems.filter(doc => !doc.unrendered);
+ return listItems.filter(doc => !doc.layout_unrendered);
}
};
// Called when the user activates 'next' - to move to the next part of the pres. trail
@@ -299,8 +300,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
listItemDoc.presTransition = 500;
targetView?.setAnimEffect(listItemDoc, 500);
if (targetView?.docView && this.activeItem.presBulletExpand) {
- targetView.docView._animateScalingTo = 1.1;
- Doc.AddUnHighlightWatcher(() => (targetView!.docView!._animateScalingTo = 0));
+ targetView.docView._animateScalingTo = 1.2;
+ targetView.docView._animateScaleTime = 400;
+ Doc.AddUnHighlightWatcher(() => {
+ targetView.docView!._animateScaleTime = undefined;
+ targetView!.docView!._animateScalingTo = 0;
+ });
}
listItemDoc.opacity = undefined;
this.activeItem.presIndexed = presIndexed + 1;
@@ -331,7 +336,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@action
back = () => {
const activeItem: Doc = this.activeItem;
- const prevItem = Cast(this.childDocs[Math.max(0, this.itemIndex - 1)], Doc, null);
let prevSelected = this.itemIndex;
// Functionality for group with up
let didZoom = activeItem.presMovement;
@@ -377,26 +381,26 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
static pinDataTypes(target?: Doc): pinDataTypes {
const targetType = target?.type as any;
const inkable = [DocumentType.INK].includes(targetType);
- const scrollable = [DocumentType.PDF, DocumentType.RTF, DocumentType.WEB].includes(targetType) || target?._viewType === CollectionViewType.Stacking;
- const pannable = [DocumentType.IMG, DocumentType.PDF].includes(targetType) || (targetType === DocumentType.COL && target?._viewType === CollectionViewType.Freeform);
+ 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);
const dataview = [DocumentType.INK, DocumentType.COL, DocumentType.IMG, DocumentType.RTF].includes(targetType) && target?.activeFrame === undefined;
const poslayoutview = [DocumentType.COL].includes(targetType) && target?.activeFrame === undefined;
- const viewType = targetType === DocumentType.COL;
+ const type_collection = targetType === DocumentType.COL;
const filters = true;
const pivot = true;
const dataannos = false;
- return { scrollable, pannable, inkable, viewType, pivot, map, filters, temporal, clippable, dataview, datarange, poslayoutview, dataannos };
+ return { scrollable, pannable, inkable, type_collection, pivot, map, filters, temporal, clippable, dataview, datarange, poslayoutview, dataannos };
}
@action
playAnnotation = (anno: AudioField) => {};
@action
static restoreTargetDocView(bestTargetView: Opt<DocumentView>, activeItem: Doc, transTime: number, pinDocLayout: boolean = BoolCast(activeItem.presPinLayout), pinDataTypes?: pinDataTypes, targetDoc?: Doc) {
- const bestTarget = bestTargetView?.rootDoc ?? (targetDoc?.unrendered ? DocCast(targetDoc?.annotationOn) : targetDoc);
+ const bestTarget = bestTargetView?.rootDoc ?? (targetDoc?.layout_unrendered ? DocCast(targetDoc?.annotationOn) : targetDoc);
if (!bestTarget || activeItem === bestTarget) return;
let changed = false;
if (pinDocLayout) {
@@ -499,16 +503,16 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
changed = true;
}
}
- if ((pinDataTypes?.viewType && activeItem.presViewType !== undefined) || (!pinDataTypes && activeItem.presViewType !== undefined)) {
- if (bestTarget._viewType !== activeItem.presViewType) {
- bestTarget._viewType = activeItem.presViewType;
+ if ((pinDataTypes?.type_collection && activeItem.presViewType !== undefined) || (!pinDataTypes && activeItem.presViewType !== undefined)) {
+ if (bestTarget._type_collection !== activeItem.presViewType) {
+ bestTarget._type_collection = activeItem.presViewType;
changed = true;
}
}
if ((pinDataTypes?.filters && activeItem.presDocFilters !== undefined) || (!pinDataTypes && activeItem.presDocFilters !== undefined)) {
- if (bestTarget.docFilters !== activeItem.presDocFilters) {
- bestTarget.docFilters = ObjectField.MakeCopy(activeItem.presDocFilters as ObjectField) || new List<string>([]);
+ if (bestTarget.childFilters !== activeItem.presDocFilters) {
+ bestTarget.childFilters = ObjectField.MakeCopy(activeItem.presDocFilters as ObjectField) || new List<string>([]);
changed = true;
}
}
@@ -537,13 +541,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
if (pinDataTypes?.dataannos || (!pinDataTypes && activeItem.presAnnotations !== undefined)) {
const fkey = Doc.LayoutFieldKey(bestTarget);
- const oldItems = DocListCast(bestTarget[fkey + '_annotations']).filter(doc => doc.unrendered);
+ const oldItems = DocListCast(bestTarget[fkey + '_annotations']).filter(doc => doc.layout_unrendered);
const newItems = DocListCast(activeItem.presAnnotations).map(doc => {
doc.hidden = false;
return doc;
});
const hiddenItems = DocListCast(bestTarget[fkey + '_annotations'])
- .filter(doc => !doc.unrendered && !newItems.includes(doc))
+ .filter(doc => !doc.layout_unrendered && !newItems.includes(doc))
.map(doc => {
doc.hidden = true;
return doc;
@@ -623,7 +627,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
pinProps.pinData.scrollable ||
pinProps.pinData.temporal ||
pinProps.pinData.pannable ||
- pinProps.pinData.viewType ||
+ pinProps.pinData.type_collection ||
pinProps.pinData.clippable ||
pinProps.pinData.datarange ||
pinProps.pinData.dataview ||
@@ -636,7 +640,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
if (pinProps.pinData.dataannos) {
const fkey = Doc.LayoutFieldKey(targetDoc);
- pinDoc.presAnnotations = new List<Doc>(DocListCast(Doc.GetProto(targetDoc)[fkey + '_annotations']).filter(doc => !doc.unrendered));
+ pinDoc.presAnnotations = new List<Doc>(DocListCast(Doc.GetProto(targetDoc)[fkey + '_annotations']).filter(doc => !doc.layout_unrendered));
}
if (pinProps.pinData.inkable) {
pinDoc.presFillColor = targetDoc.fillColor;
@@ -673,8 +677,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
})
)
);
- if (pinProps.pinData.viewType) pinDoc.presViewType = targetDoc._viewType;
- if (pinProps.pinData.filters) pinDoc.presDocFilters = ObjectField.MakeCopy(targetDoc.docFilters as ObjectField);
+ if (pinProps.pinData.type_collection) pinDoc.presViewType = targetDoc._type_collection;
+ if (pinProps.pinData.filters) pinDoc.presDocFilters = ObjectField.MakeCopy(targetDoc.childFilters as ObjectField);
if (pinProps.pinData.pivot) pinDoc.presPivotField = targetDoc._pivotField;
if (pinProps.pinData.pannable) {
pinDoc.presPanX = NumCast(targetDoc._freeform_panX);
@@ -711,7 +715,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
const finished = () => {
afterNav?.();
console.log('Finish Slide Nav: ' + targetDoc.title);
- targetDoc[AnimationSym] = undefined;
+ targetDoc[Animation] = undefined;
};
const selViewCache = Array.from(this.selectedArray);
const dragViewCache = Array.from(this._dragArray);
@@ -757,7 +761,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
}
if (targetDoc) {
- if (activeItem.presentationTargetDoc instanceof Doc) activeItem.presentationTargetDoc[AnimationSym] = undefined;
+ if (activeItem.presentationTargetDoc instanceof Doc) activeItem.presentationTargetDoc[Animation] = undefined;
DocumentManager.Instance.AddViewRenderedCb(LightboxView.LightboxDoc, dv => {
// if target or the doc it annotates is not in the lightbox, then close the lightbox
@@ -821,7 +825,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
const savedStates = docs.map(doc => {
switch (doc.type) {
case DocumentType.COL:
- if (doc._viewType === CollectionViewType.Freeform) return { type: CollectionViewType.Freeform, doc, x: NumCast(doc.freeform_panX), y: NumCast(doc.freeform_panY), s: NumCast(doc.freeform_scale) };
+ if (doc._type_collection === CollectionViewType.Freeform) return { type: CollectionViewType.Freeform, doc, x: NumCast(doc.freeform_panX), y: NumCast(doc.freeform_panY), s: NumCast(doc.freeform_scale) };
break;
case DocumentType.INK:
if (doc.data instanceof InkField) {
@@ -839,8 +843,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
case CollectionViewType.Freeform:
{
const { x, y, s, doc } = savedState!;
- doc._panX = x;
- doc._panY = y;
+ doc._freeform_panX = x;
+ doc._freeform_panY = y;
doc._freeform_scale = s;
}
break;
@@ -857,7 +861,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
});
LightboxView.SetLightboxDoc(undefined);
- Doc.RemoveDocFromList(Doc.MyOverlayDocs, undefined, this.rootDoc);
+ Doc.RemFromMyOverlay(this.rootDoc);
return PresStatus.Edit;
};
};
@@ -953,8 +957,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
return PresBox.OpenPresMinimized(this.rootDoc, [pt[0] + (this.props.PanelWidth() - 250), pt[1] + 10]);
};
exitMinimize = () => {
- if (DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc)) {
- Doc.RemoveDocFromList(Doc.MyOverlayDocs, undefined, this.rootDoc);
+ if (Doc.IsInMyOverlay(this.layoutDoc)) {
+ Doc.RemFromMyOverlay(this.rootDoc);
CollectionDockingView.AddSplit(this.rootDoc, OpenWhereMod.right);
}
return PresStatus.Edit;
@@ -966,7 +970,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
doc.overlayY = pt[1];
doc._height = 30;
doc._width = PresBox.minimizedWidth;
- Doc.AddDocToList(Doc.MyOverlayDocs, undefined, doc);
+ Doc.AddToMyOverlay(doc);
PresBox.Instance?.initializePresState(PresBox.Instance.itemIndex);
return (doc.presStatus = PresStatus.Manual);
}
@@ -978,11 +982,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@undoBatch
viewChanged = action((e: React.ChangeEvent) => {
//@ts-ignore
- const viewType = e.target.selectedOptions[0].value as CollectionViewType;
- this.layoutDoc.presFieldKey = this.fieldKey + (viewType === CollectionViewType.Tree ? '-linearized' : '');
+ const type_collection = e.target.selectedOptions[0].value as CollectionViewType;
+ this.layoutDoc.presFieldKey = this.fieldKey + (type_collection === CollectionViewType.Tree ? '-linearized' : '');
// pivot field may be set by the user in timeline view (or some other way) -- need to reset it here
- [CollectionViewType.Tree || CollectionViewType.Stacking].includes(viewType) && (this.rootDoc._pivotField = undefined);
- this.rootDoc._viewType = viewType;
+ [CollectionViewType.Tree || CollectionViewType.Stacking].includes(type_collection) && (this.rootDoc._pivotField = undefined);
+ this.rootDoc._type_collection = type_collection;
if (this.isTreeOrStack) {
this.layoutDoc._gridGap = 0;
}
@@ -1044,13 +1048,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
});
return true;
};
- childLayoutTemplate = () => (!this.isTreeOrStack ? undefined : DocCast(Doc.UserDoc().presElement));
+ childLayoutTemplate = () => (!this.isTreeOrStack ? DocCast(Doc.UserDoc().presElement) : DocCast(Doc.UserDoc().presElement));
removeDocument = (doc: Doc) => Doc.RemoveDocFromList(this.rootDoc, this.fieldKey, doc);
getTransform = () => this.props.ScreenToLocalTransform().translate(-5, -65); // listBox padding-left and pres-box-cont minHeight
panelHeight = () => this.props.PanelHeight() - 40;
- isContentActive = (outsideReaction?: boolean) => this.props.isContentActive(outsideReaction);
- //.ActiveTool === InkTool.None && !this.layoutDoc._lockedPosition && (this.layoutDoc.forceActive || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0) ? true : false;
-
/**
* For sorting the array so that the order is maintained when it is dropped.
*/
@@ -1183,7 +1184,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
break;
case 'Escape':
- if (DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc)) {
+ if (Doc.IsInMyOverlay(this.layoutDoc)) {
this.exitClicked();
} else if (this.layoutDoc.presStatus === PresStatus.Edit) {
this.clearSelectedArray();
@@ -1452,6 +1453,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
min={min}
max={max}
value={value}
+ readOnly={true}
style={{ marginLeft: hmargin, marginRight: hmargin, width: `calc(100% - ${2 * (hmargin ?? 0)}px)` }}
className={`toolbar-slider ${active ? '' : 'none'}`}
onPointerDown={e => {
@@ -1525,7 +1527,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
<div className="ribbon-doubleButton">
<div className="presBox-subheading">Slide Duration</div>
<div className="ribbon-property">
- <input className="presBox-input" type="number" value={duration} onKeyDown={e => e.stopPropagation()} onChange={e => this.updateDurationTime(e.target.value)} /> s
+ <input className="presBox-input" type="number" readOnly={true} value={duration} onKeyDown={e => e.stopPropagation()} onChange={e => this.updateDurationTime(e.target.value)} /> s
</div>
<div className="ribbon-propertyUpDown">
<div className="ribbon-propertyUpDownItem" onClick={() => this.updateDurationTime(String(duration), 1000)}>
@@ -1678,7 +1680,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
<div className="ribbon-doubleButton" style={{ display: activeItem.presMovement === PresMovement.Zoom ? 'inline-flex' : 'none' }}>
<div className="presBox-subheading">Zoom (% screen filled)</div>
<div className="ribbon-property">
- <input className="presBox-input" type="number" value={zoom} onChange={e => this.updateZoom(e.target.value)} />%
+ <input className="presBox-input" type="number" readOnly={true} value={zoom} onChange={e => this.updateZoom(e.target.value)} />%
</div>
<div className="ribbon-propertyUpDown">
<div className="ribbon-propertyUpDownItem" onClick={() => this.updateZoom(String(zoom), 0.1)}>
@@ -1693,7 +1695,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
<div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}>
<div className="presBox-subheading">Transition Time</div>
<div className="ribbon-property">
- <input className="presBox-input" type="number" value={transitionSpeed} onKeyDown={e => e.stopPropagation()} onChange={action(e => this.updateTransitionTime(e.target.value))} /> s
+ <input className="presBox-input" type="number" readOnly={true} value={transitionSpeed} onKeyDown={e => e.stopPropagation()} onChange={action(e => this.updateTransitionTime(e.target.value))} /> s
</div>
<div className="ribbon-propertyUpDown">
<div className="ribbon-propertyUpDownItem" onClick={() => this.updateTransitionTime(String(transitionSpeed), 1000)}>
@@ -1780,6 +1782,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
className="presBox-input"
style={{ textAlign: 'center', width: '100%', height: 15, fontSize: 10 }}
type="number"
+ readOnly={true}
value={NumCast(activeItem.presStartTime).toFixed(2)}
onKeyDown={e => e.stopPropagation()}
onChange={action((e: React.ChangeEvent<HTMLInputElement>) => {
@@ -1806,6 +1809,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
onKeyDown={e => e.stopPropagation()}
style={{ textAlign: 'center', width: '100%', height: 15, fontSize: 10 }}
type="number"
+ readOnly={true}
value={NumCast(activeItem.presEndTime).toFixed(2)}
onChange={action((e: React.ChangeEvent<HTMLInputElement>) => {
activeItem.presEndTime = Number(e.target.value);
@@ -2088,20 +2092,20 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
createTemplate = (layout: string, input?: string) => {
const x = this.activeItem && this.targetDoc ? NumCast(this.targetDoc.x) : 0;
const y = this.activeItem && this.targetDoc ? NumCast(this.targetDoc.y) + NumCast(this.targetDoc._height) + 20 : 0;
- const title = () => Docs.Create.TextDocument('Click to change title', { title: 'Slide title', _width: 380, _height: 60, x: 10, y: 58, _fontSize: '24pt' });
- const subtitle = () => Docs.Create.TextDocument('Click to change subtitle', { title: 'Slide subtitle', _width: 380, _height: 50, x: 10, y: 118, _fontSize: '16pt' });
- const header = () => Docs.Create.TextDocument('Click to change header', { title: 'Slide header', _width: 380, _height: 65, x: 10, y: 80, _fontSize: '20pt' });
- const contentTitle = () => Docs.Create.TextDocument('Click to change title', { title: 'Slide title', _width: 380, _height: 60, x: 10, y: 10, _fontSize: '24pt' });
- const content = () => Docs.Create.TextDocument('Click to change text', { title: 'Slide text', _width: 380, _height: 145, x: 10, y: 70, _fontSize: '14pt' });
- const content1 = () => Docs.Create.TextDocument('Click to change text', { title: 'Column 1', _width: 185, _height: 140, x: 10, y: 80, _fontSize: '14pt' });
- const content2 = () => Docs.Create.TextDocument('Click to change text', { title: 'Column 2', _width: 185, _height: 140, x: 205, y: 80, _fontSize: '14pt' });
+ const title = () => Docs.Create.TextDocument('Click to change title', { title: 'Slide title', _width: 380, _height: 60, x: 10, y: 58, _text_fontSize: '24pt' });
+ const subtitle = () => Docs.Create.TextDocument('Click to change subtitle', { title: 'Slide subtitle', _width: 380, _height: 50, x: 10, y: 118, _text_fontSize: '16pt' });
+ const header = () => Docs.Create.TextDocument('Click to change header', { title: 'Slide header', _width: 380, _height: 65, x: 10, y: 80, _text_fontSize: '20pt' });
+ const contentTitle = () => Docs.Create.TextDocument('Click to change title', { title: 'Slide title', _width: 380, _height: 60, x: 10, y: 10, _text_fontSize: '24pt' });
+ const content = () => Docs.Create.TextDocument('Click to change text', { title: 'Slide text', _width: 380, _height: 145, x: 10, y: 70, _text_fontSize: '14pt' });
+ const content1 = () => Docs.Create.TextDocument('Click to change text', { title: 'Column 1', _width: 185, _height: 140, x: 10, y: 80, _text_fontSize: '14pt' });
+ const content2 = () => Docs.Create.TextDocument('Click to change text', { title: 'Column 2', _width: 185, _height: 140, x: 205, y: 80, _text_fontSize: '14pt' });
// prettier-ignore
switch (layout) {
case 'blank': return Docs.Create.FreeformDocument([], { title: input ? input : 'Blank slide', _width: 400, _height: 225, x, y });
- case 'title': return Docs.Create.FreeformDocument([title(), subtitle()], { title: input ? input : 'Title slide', _width: 400, _height: 225, _layoutFitContentsToBox: true, x, y });
- case 'header': return Docs.Create.FreeformDocument([header()], { title: input ? input : 'Section header', _width: 400, _height: 225, _layoutFitContentsToBox: true, x, y });
- case 'content': return Docs.Create.FreeformDocument([contentTitle(), content()], { title: input ? input : 'Title and content', _width: 400, _height: 225, _layoutFitContentsToBox: true, x, y });
- case 'twoColumns': return Docs.Create.FreeformDocument([contentTitle(), content1(), content2()], { title: input ? input : 'Title and two columns', _width: 400, _height: 225, _layoutFitContentsToBox: true, x, y })
+ case 'title': return Docs.Create.FreeformDocument([title(), subtitle()], { title: input ? input : 'Title slide', _width: 400, _height: 225, _layout_fitContentsToBox: true, x, y });
+ case 'header': return Docs.Create.FreeformDocument([header()], { title: input ? input : 'Section header', _width: 400, _height: 225, _layout_fitContentsToBox: true, x, y });
+ case 'content': return Docs.Create.FreeformDocument([contentTitle(), content()], { title: input ? input : 'Title and content', _width: 400, _height: 225, _layout_fitContentsToBox: true, x, y });
+ case 'twoColumns': return Docs.Create.FreeformDocument([contentTitle(), content1(), content2()], { title: input ? input : 'Title and two columns', _width: 400, _height: 225, _layout_fitContentsToBox: true, x, y })
}
};
@@ -2150,12 +2154,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@computed get toolbar() {
const propIcon = SettingsManager.propertiesWidth > 0 ? 'angle-double-right' : 'angle-double-left';
const propTitle = SettingsManager.propertiesWidth > 0 ? 'Close Presentation Panel' : 'Open Presentation Panel';
- const mode = StrCast(this.rootDoc._viewType) as CollectionViewType;
+ const mode = StrCast(this.rootDoc._type_collection) as CollectionViewType;
const isMini: boolean = this.toolbarWidth <= 100;
- const inOverlay = DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc);
const activeColor = Colors.LIGHT_BLUE;
const inactiveColor = Colors.WHITE;
- return mode === CollectionViewType.Carousel3D || inOverlay ? null : (
+ return mode === CollectionViewType.Carousel3D || Doc.IsInMyOverlay(this.rootDoc) ? null : (
<div id="toolbarContainer" className={'presBox-toolbar'}>
{/* <Tooltip title={<><div className="dash-tooltip">{"Add new slide"}</div></>}><div className={`toolbar-button ${this.newDocumentTools ? "active" : ""}`} onClick={action(() => this.newDocumentTools = !this.newDocumentTools)}>
<FontAwesomeIcon icon={"plus"} />
@@ -2194,11 +2197,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
* presentPanel: The button to start the presentation / open minimized view of the presentation
*/
@computed get topPanel() {
- const mode = StrCast(this.rootDoc._viewType) as CollectionViewType;
+ const mode = StrCast(this.rootDoc._type_collection) as CollectionViewType;
const isMini: boolean = this.toolbarWidth <= 100;
- const inOverlay = DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc);
return (
- <div className={`presBox-buttons${inOverlay ? ' inOverlay' : ''}`} style={{ background: Doc.ActivePresentation === this.rootDoc ? Colors.LIGHT_BLUE : undefined, display: !this.rootDoc._chromeHidden ? 'none' : undefined }}>
+ <div
+ className={`presBox-buttons${Doc.IsInMyOverlay(this.rootDoc) ? ' inOverlay' : ''}`}
+ style={{ background: Doc.ActivePresentation === this.rootDoc ? Colors.LIGHT_BLUE : undefined, display: !this.rootDoc._chromeHidden ? 'none' : undefined }}>
{isMini ? null : (
<select className="presBox-viewPicker" style={{ display: this.layoutDoc.presStatus === 'edit' ? 'block' : 'none' }} onPointerDown={e => e.stopPropagation()} onChange={this.viewChanged} value={mode}>
<option onPointerDown={StopEvent} value={CollectionViewType.Stacking}>
@@ -2248,7 +2252,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@computed get playButtons() {
const presEnd = !this.layoutDoc.presLoop && this.itemIndex === this.childDocs.length - 1 && (this.activeItem.presIndexed === undefined || NumCast(this.activeItem.presIndexed) === (this.progressivizedItems(this.activeItem)?.length ?? 0));
const presStart: boolean = !this.layoutDoc.presLoop && this.itemIndex === 0;
- const inOverlay = DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc);
+ const inOverlay = Doc.IsInMyOverlay(this.rootDoc);
// Case 1: There are still other frames and should go through all frames before going to next slide
return (
<div className="presPanelOverlay" style={{ display: this.layoutDoc.presStatus !== 'edit' ? 'inline-flex' : 'none' }}>
@@ -2419,10 +2423,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
render() {
// needed to ensure that the childDocs are loaded for looking up fields
this.childDocs.slice();
- const mode = StrCast(this.rootDoc._viewType) as CollectionViewType;
+ const mode = StrCast(this.rootDoc._type_collection) as CollectionViewType;
const presEnd = !this.layoutDoc.presLoop && this.itemIndex === this.childDocs.length - 1 && (this.activeItem.presIndexed === undefined || NumCast(this.activeItem.presIndexed) === (this.progressivizedItems(this.activeItem)?.length ?? 0));
const presStart = !this.layoutDoc.presLoop && this.itemIndex === 0;
- const inOverlay = DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc);
return this.props.addDocTab === returnFalse ? ( // bcz: hack!! - addDocTab === returnFalse only when this is being rendered by the OverlayView which means the doc is a mini player
<div className="miniPres" onClick={e => e.stopPropagation()} onPointerEnter={action(e => (this._forceKeyEvents = true))}>
<div
@@ -2465,7 +2468,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
</div>
</div>
) : (
- <div className="presBox-cont" style={{ minWidth: inOverlay ? PresBox.minimizedWidth : undefined }}>
+ <div className="presBox-cont" style={{ minWidth: Doc.IsInMyOverlay(this.rootDoc) ? PresBox.minimizedWidth : undefined }}>
{this.topPanel}
{this.toolbar}
{this.newDocumentToolbarDropdown}
@@ -2479,9 +2482,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
childIgnoreNativeSize={true}
moveDocument={returnFalse}
ignoreUnrendered={true}
+ childDragAction="move"
//childLayoutFitWidth={returnTrue}
childOpacity={returnOne}
- //childLayoutString={PresElementBox.LayoutString('data')}
childClickScript={PresBox.navigateToDocScript}
childLayoutTemplate={this.childLayoutTemplate}
childXPadding={Doc.IsComicStyle(this.rootDoc) ? 20 : undefined}
diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx
index e48d2d674..f31cf6147 100644
--- a/src/client/views/nodes/trails/PresElementBox.tsx
+++ b/src/client/views/nodes/trails/PresElementBox.tsx
@@ -1,19 +1,20 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tooltip } from '@material-ui/core';
-import { action, computed, IReactionDisposer, observable, reaction } from 'mobx';
+import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
-import { Doc, DocListCast, HeightSym, Opt, WidthSym } from '../../../../fields/Doc';
+import { Doc, DocListCast, Opt } from '../../../../fields/Doc';
+import { Height, Width } from '../../../../fields/DocSymbols';
import { Id } from '../../../../fields/FieldSymbols';
import { List } from '../../../../fields/List';
import { Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types';
import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents } from '../../../../Utils';
-import { Docs, DocUtils } from '../../../documents/Documents';
+import { Docs } from '../../../documents/Documents';
import { CollectionViewType } from '../../../documents/DocumentTypes';
import { DocumentManager } from '../../../util/DocumentManager';
import { DragManager } from '../../../util/DragManager';
import { SettingsManager } from '../../../util/SettingsManager';
import { Transform } from '../../../util/Transform';
-import { undoBatch } from '../../../util/UndoManager';
+import { undoable, undoBatch } from '../../../util/UndoManager';
import { ViewBoxBaseComponent } from '../../DocComponent';
import { EditableView } from '../../EditableView';
import { Colors } from '../../global/globalEnums';
@@ -106,8 +107,8 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
ScreenToLocalTransform={Transform.Identity}
renderDepth={this.props.renderDepth + 1}
docViewPath={returnEmptyDoclist}
- docFilters={this.props.docFilters}
- docRangeFilters={this.props.docRangeFilters}
+ childFilters={this.props.childFilters}
+ childFiltersByRanges={this.props.childFiltersByRanges}
searchFilterDocs={this.props.searchFilterDocs}
rootSelected={returnTrue}
addDocument={returnFalse}
@@ -191,13 +192,14 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
const dragArray = this.presBoxView?._dragArray ?? [];
const dragData = new DragManager.DocumentDragData(this.presBoxView?.sortArray() ?? []);
if (!dragData.draggedDocuments.length) dragData.draggedDocuments.push(this.rootDoc);
- dragData.dropAction = 'move';
- dragData.treeViewDoc = this.presBox?._viewType === CollectionViewType.Tree ? this.presBox : undefined; // this.props.DocumentView?.()?.props.treeViewDoc;
+ dragData.treeViewDoc = this.presBox?._type_collection === CollectionViewType.Tree ? this.presBox : undefined; // this.props.DocumentView?.()?.props.treeViewDoc;
dragData.moveDocument = this.props.moveDocument;
const dragItem: HTMLElement[] = [];
+ const classesToRestore = new Map<HTMLElement, string>();
if (dragArray.length === 1) {
const doc = this._itemRef.current || dragArray[0];
if (doc) {
+ classesToRestore.set(doc, doc.className);
doc.className = miniView ? 'presItem-miniSlide' : 'presItem-slide';
dragItem.push(doc);
}
@@ -211,16 +213,19 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
dragItem.push(doc);
}
- // const dropEvent = () => runInAction(() => this._dragging = false);
if (activeItem) {
+ runInAction(() => (this._dragging = true));
DragManager.StartDocumentDrag(
dragItem.map(ele => ele),
dragData,
e.clientX,
e.clientY,
- undefined
+ undefined,
+ action(() => {
+ Array.from(classesToRestore).forEach(pair => (pair[0].className = pair[1]));
+ this._dragging = false;
+ })
);
- // runInAction(() => this._dragging = true);
return true;
}
return false;
@@ -263,16 +268,15 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
};
- @undoBatch
- removeItem = action((e: React.MouseEvent) => {
+ removePresentationItem = undoable((e: React.MouseEvent) => {
e.stopPropagation();
if (this.presBox && this.indexInPres < (this.presBoxView?.itemIndex || 0)) {
- this.presBox.itemIndex = (this.presBoxView?.itemIndex || 0) - 1;
+ runInAction(() => (this.presBox!.itemIndex = (this.presBoxView?.itemIndex || 0) - 1));
}
this.props.removeDocument?.(this.rootDoc);
this.presBoxView?.removeFromSelectedArray(this.rootDoc);
this.removeAllRecordingInOverlay();
- });
+ }, 'Remove doc from pres trail');
// set the value/title of the individual pres element
@undoBatch
@@ -323,11 +327,9 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
};
removeAllRecordingInOverlay = () => {
- DocListCast(Doc.MyOverlayDocs.data).forEach(doc => {
- if (doc.slides === this.rootDoc) {
- Doc.RemoveDocFromList(Doc.MyOverlayDocs, undefined, doc);
- }
- });
+ DocListCast(Doc.MyOverlayDocs.data)
+ .filter(doc => doc.slides === this.rootDoc)
+ .forEach(Doc.RemFromMyOverlay);
};
static removeEveryExistingRecordingInOverlay = () => {
@@ -335,9 +337,9 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
DocListCast(Doc.MyOverlayDocs.data).forEach(doc => {
if (doc.slides !== null) {
// if it's a recording video, don't remove from overlay (user can lose data)
- if (!PresElementBox.videoIsRecorded(DocCast(doc.slides))) return;
-
- Doc.RemoveDocFromList(Doc.MyOverlayDocs, undefined, doc);
+ if (PresElementBox.videoIsRecorded(DocCast(doc.slides))) {
+ Doc.RemFromMyOverlay(doc);
+ }
}
});
};
@@ -357,7 +359,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
if (!iconClick) PresElementBox.removeEveryExistingRecordingInOverlay();
if (activeItem.recording) {
- Doc.AddDocToList(Doc.MyOverlayDocs, undefined, DocCast(activeItem.recording));
+ Doc.AddToMyOverlay(DocCast(activeItem.recording));
}
};
@@ -369,7 +371,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
// if we already have an existing recording
this.showRecording(activeItem, true);
// // if we already have an existing recording
- // Doc.AddDocToList(Doc.MyOverlayDocs, undefined, Cast(activeItem.recording, Doc, null));
+ // Doc.AddToMyOverlay(Cast(activeItem.recording, Doc, null));
} else {
// Remove every recording that already exists in overlay view
// this is a design decision to clear to focus in on the recoding mode
@@ -379,9 +381,9 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
const recording = Docs.Create.WebCamDocument('', {
_width: 384,
_height: 216,
- hideDocumentButtonBar: true,
+ layout_hideDocumentButtonBar: true,
layout_hideDecorationTitle: true,
- hideOpenButton: true,
+ layout_hideOpenButton: true,
// hideDeleteButton: true,
cloneFieldFilter: new List<string>(['isSystem']),
});
@@ -391,9 +393,9 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
activeItem.recording = recording;
// make recording box appear in the bottom right corner of the screen
- recording.overlayX = window.innerWidth - recording[WidthSym]() - 20;
- recording.overlayY = window.innerHeight - recording[HeightSym]() - 20;
- Doc.AddDocToList(Doc.MyOverlayDocs, undefined, recording);
+ recording.overlayX = window.innerWidth - recording[Width]() - 20;
+ recording.overlayY = window.innerHeight - recording[Height]() - 20;
+ Doc.AddToMyOverlay(recording);
}
};
@@ -476,7 +478,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
);
items.push(
<Tooltip key="trash" title={<div className="dash-tooltip">Remove from presentation</div>}>
- <div className={'slideButton'} onClick={this.removeItem}>
+ <div className={'slideButton'} onClick={this.removePresentationItem}>
<FontAwesomeIcon icon={'trash'} onPointerDown={e => e.stopPropagation()} />
</div>
</Tooltip>
@@ -524,7 +526,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
style={{
display: 'infline-block',
backgroundColor: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor),
- //boxShadow: presBoxColor && presBoxColor !== 'white' && presBoxColor !== 'transparent' ? (isCurrent ? '0 0 0px 1.5px' + presBoxColor : undefined) : undefined,
+ //layout_boxShadow: presBoxColor && presBoxColor !== 'white' && presBoxColor !== 'transparent' ? (isCurrent ? '0 0 0px 1.5px' + presBoxColor : undefined) : undefined,
border: presBoxColor && presBoxColor !== 'white' && presBoxColor !== 'transparent' ? (isCurrent ? presBoxColor + ' solid 2.5px' : undefined) : undefined,
}}>
<div
@@ -538,7 +540,6 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
<div
className="presItem-number"
title="select without navigation"
- style={{ pointerEvents: this.presBoxView?.isContentActive() ? 'all' : undefined }}
onPointerDown={e => {
e.stopPropagation();
if (this._itemRef.current && this._dragRef.current) {
@@ -546,7 +547,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
}}
onClick={e => e.stopPropagation()}>{`${this.indexInPres + 1}. `}</div>
- <EditableView ref={this._titleRef} editing={!isSelected ? false : undefined} contents={activeItem.title} overflow={'ellipsis'} GetValue={() => StrCast(activeItem.title)} SetValue={this.onSetValue} />
+ <EditableView ref={this._titleRef} oneLine={true} editing={!isSelected ? false : undefined} contents={activeItem.title} overflow={'ellipsis'} GetValue={() => StrCast(activeItem.title)} SetValue={this.onSetValue} />
</div>
{/* <Tooltip title={<><div className="dash-tooltip">{"Movement speed"}</div></>}><div className="presItem-time" style={{ display: showMore ? "block" : "none" }}>{this.transition}</div></Tooltip> */}
{/* <Tooltip title={<><div className="dash-tooltip">{"Duration"}</div></>}><div className="presItem-time" style={{ display: showMore ? "block" : "none" }}>{this.duration}</div></Tooltip> */}
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx
index d6dddf71a..e35e011e2 100644
--- a/src/client/views/pdf/AnchorMenu.tsx
+++ b/src/client/views/pdf/AnchorMenu.tsx
@@ -15,6 +15,9 @@ 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';
@observer
export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
@@ -42,7 +45,6 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
];
@observable private highlightColor: string = 'rgba(245, 230, 95, 0.616)';
- @observable private _showLinkPopup: boolean = false;
@observable public Status: 'marquee' | 'annotation' | '' = '';
@@ -131,7 +133,6 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
() => this._opacity,
opacity => {
if (!opacity) {
- this._showLinkPopup = false;
this.setGPTPopupVis(false);
this.setGPTPopupText('');
}
@@ -141,7 +142,6 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
this._disposer = reaction(
() => SelectionManager.Views().slice(),
selected => {
- this._showLinkPopup = false;
this.setGPTPopupVis(false);
this.setGPTPopupText('');
AnchorMenu.Instance.fadeOut(true);
@@ -253,49 +253,22 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
AnchorMenu.Instance.fadeOut(true);
};
- @action
- toggleLinkPopup = (e: React.MouseEvent) => {
- //ignore the potential null type error because this method cannot be called unless the user selects text and clicks the link button
- //change popup visibility field to visible
- this._showLinkPopup = !this._showLinkPopup;
- };
-
@computed get highlighter() {
- const button = (
- <button className="antimodeMenu-button anchor-color-preview-button" title="" key="highlighter-button" onClick={this.highlightClicked}>
- <div className="anchor-color-preview">
- <FontAwesomeIcon icon="highlighter" size="lg" style={{ transition: 'transform 0.1s', transform: 'rotate(-45deg)' }} />
- <div className="color-preview" style={{ backgroundColor: this.highlightColor }}></div>
- </div>
- </button>
- );
-
- const dropdownContent = (
- <div className="dropdown">
- <p>Change highlighter color:</p>
- <div className="color-wrapper">
- {this._palette.map(color => {
- if (color) {
- return this.highlightColor === color ? (
- <button className="color-button active" key={`active ${color}`} style={{ backgroundColor: color }} onPointerDown={e => this.changeHighlightColor(color, e)}></button>
- ) : (
- <button className="color-button" key={`inactive ${color}`} style={{ backgroundColor: color }} onPointerDown={e => this.changeHighlightColor(color, e)}></button>
- );
- }
- })}
- </div>
- </div>
- );
return (
- <Tooltip key="highlighter" title={<div className="dash-tooltip">{'Click to Highlight'}</div>}>
- <div className="anchorMenu-highlighter">
- <ButtonDropdown key={'highlighter'} button={button} dropdownContent={dropdownContent} pdf={true} />
- </div>
- </Tooltip>
+ <Group>
+ <IconButton
+ icon={<FontAwesomeIcon icon="highlighter" style={{ transition: 'transform 0.1s', transform: 'rotate(-45deg)' }} />}
+ tooltip={'Click to Highlight'}
+ onClick={this.highlightClicked}
+ colorPicker={this.highlightColor}
+ color={StrCast(Doc.UserDoc().userColor)}
+ />
+ <ColorPicker selectedColor={this.highlightColor} setSelectedColor={this.changeHighlightColor} size={Size.XSMALL} />
+ </Group>
);
}
- @action changeHighlightColor = (color: string, e: React.PointerEvent) => {
+ @action changeHighlightColor = (color: string) => {
const col: ColorState = {
hex: color,
hsl: { a: 0, h: 0, s: 0, l: 0, source: '' },
@@ -304,8 +277,6 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
oldHue: 0,
source: '',
};
- e.preventDefault();
- e.stopPropagation();
this.highlightColor = Utils.colorString(col);
};
@@ -317,7 +288,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
canSummarize = (): boolean => {
const docs = SelectionManager.Docs();
if (docs.length > 0) {
- return docs.some(doc => doc.type === 'pdf' || doc.type === 'web');
+ return docs.some(doc => doc.type === DocumentType.PDF || doc.type === DocumentType.WEB);
}
return false;
};
@@ -339,18 +310,20 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
this.Status === 'marquee' ? (
<>
{this.highlighter}
- <Tooltip key="annotate" title={<div className="dash-tooltip">Drag to Place Annotation</div>}>
- <button className="antimodeMenu-button annotate" ref={this._commentCont} onPointerDown={this.pointerDown} style={{ cursor: 'grab' }}>
- <FontAwesomeIcon icon="comment-alt" size="lg" />
- </button>
- </Tooltip>
+ <IconButton
+ tooltip="Drag to Place Annotation" //
+ onPointerDown={this.pointerDown}
+ icon={<FontAwesomeIcon icon="comment-alt" />}
+ color={StrCast(Doc.UserDoc().userColor)}
+ />
{/* GPT Summarize icon only shows up when text is highlighted, not on marquee selection*/}
{AnchorMenu.Instance.StartCropDrag === unimplementedFunction && this.canSummarize() && (
- <Tooltip key="gpt" title={<div className="dash-tooltip">Summarize with AI</div>}>
- <button className="antimodeMenu-button annotate" onPointerDown={this.gptSummarize} style={{ cursor: 'grab' }}>
- <FontAwesomeIcon icon="comment-dots" size="lg" />
- </button>
- </Tooltip>
+ <IconButton
+ tooltip="Summarize with AI" //
+ onPointerDown={this.gptSummarize}
+ icon={<FontAwesomeIcon icon="comment-dots" size="lg" />}
+ color={StrCast(Doc.UserDoc().userColor)}
+ />
)}
<GPTPopup
key="gptpopup"
@@ -364,56 +337,74 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
mode={this.GPTMode}
/>
{AnchorMenu.Instance.OnAudio === unimplementedFunction ? null : (
- <Tooltip key="annoaudiotate" title={<div className="dash-tooltip">Click to Record Annotation</div>}>
- <button className="antimodeMenu-button annotate" onPointerDown={this.audioDown} style={{ cursor: 'grab' }}>
- <FontAwesomeIcon icon="microphone" size="lg" />
- </button>
- </Tooltip>
+ <IconButton
+ tooltip="Click to Record Annotation" //
+ onPointerDown={this.audioDown}
+ icon={<FontAwesomeIcon icon="microphone" />}
+ color={StrCast(Doc.UserDoc().userColor)}
+ />
)}
{this.canEdit() && (
- <Tooltip key="gpttextedit" title={<div className="dash-tooltip">AI edit suggestions</div>}>
- <button className="antimodeMenu-button annotate" onPointerDown={this.gptEdit} style={{ cursor: 'grab' }}>
- <FontAwesomeIcon icon="pencil-alt" size="lg" />
- </button>
- </Tooltip>
+ <IconButton
+ tooltip="AI edit suggestions" //
+ onPointerDown={this.gptEdit}
+ icon={<FontAwesomeIcon icon="pencil-alt" />}
+ color={StrCast(Doc.UserDoc().userColor)}
+ />
)}
- <Tooltip key="link" title={<div className="dash-tooltip">Find document to link to selected text</div>}>
- <button className="antimodeMenu-button link" onPointerDown={this.toggleLinkPopup}>
- <FontAwesomeIcon style={{ position: 'absolute', transform: 'scale(1.5)' }} icon={'search'} size="lg" />
- <FontAwesomeIcon style={{ position: 'absolute', transform: 'scale(0.5)', transformOrigin: 'top left', top: 12, left: 12 }} icon={'link'} size="lg" />
- </button>
- </Tooltip>
- <LinkPopup key="popup" showPopup={this._showLinkPopup} linkCreateAnchor={this.onMakeAnchor} />,
+ <Popup
+ tooltip="Find document to link to selected text" //
+ type={Type.PRIM}
+ icon={<FontAwesomeIcon icon={'search'} />}
+ popup={<LinkPopup key="popup" linkCreateAnchor={this.onMakeAnchor} />}
+ color={StrCast(Doc.UserDoc().userColor)}
+ />
{AnchorMenu.Instance.StartCropDrag === unimplementedFunction ? null : (
- <Tooltip key="crop" title={<div className="dash-tooltip">Click/Drag to create cropped image</div>}>
- <button className="antimodeMenu-button annotate" onPointerDown={this.cropDown} style={{ cursor: 'grab' }}>
- <FontAwesomeIcon icon="image" size="lg" />
- </button>
- </Tooltip>
+ <IconButton
+ tooltip="Click/Drag to create cropped image" //
+ onPointerDown={this.cropDown}
+ icon={<FontAwesomeIcon icon="image" />}
+ color={StrCast(Doc.UserDoc().userColor)}
+ />
)}
</>
) : (
<>
- <Tooltip key="trash" title={<div className="dash-tooltip">Remove Link Anchor</div>}>
- <button className="antimodeMenu-button" onPointerDown={this.Delete}>
- <FontAwesomeIcon icon="trash-alt" size="lg" />
- </button>
- </Tooltip>
- <Tooltip key="Pin" title={<div className="dash-tooltip">Pin to Presentation</div>}>
- <button className="antimodeMenu-button" onPointerDown={this.PinToPres}>
- <FontAwesomeIcon icon="map-pin" size="lg" />
- </button>
- </Tooltip>
- <Tooltip key="trail" title={<div className="dash-tooltip">Show Linked Trail</div>}>
- <button className="antimodeMenu-button" onPointerDown={this.ShowTargetTrail}>
- <FontAwesomeIcon icon="taxi" size="lg" />
- </button>
- </Tooltip>
- <Tooltip key="toggle" title={<div className="dash-tooltip">make target visibility toggle on click</div>}>
- <button className="antimodeMenu-button" style={{ color: this.IsTargetToggler() ? 'black' : 'white', backgroundColor: this.IsTargetToggler() ? 'white' : 'black' }} onPointerDown={this.MakeTargetToggle}>
- <FontAwesomeIcon icon="thumbtack" size="lg" />
- </button>
- </Tooltip>
+ {this.Delete !== returnFalse && (
+ <IconButton
+ tooltip="Remove Link Anchor" //
+ onPointerDown={this.Delete}
+ icon={<FontAwesomeIcon icon="trash-alt" />}
+ color={StrCast(Doc.UserDoc().userColor)}
+ />
+ )}
+ {this.PinToPres !== returnFalse && (
+ <IconButton
+ tooltip="Pin to Presentation" //
+ onPointerDown={this.PinToPres}
+ icon={<FontAwesomeIcon icon="map-pin" />}
+ color={StrCast(Doc.UserDoc().userColor)}
+ />
+ )}
+ {this.ShowTargetTrail !== returnFalse && (
+ <IconButton
+ tooltip="Show Linked Trail" //
+ onPointerDown={this.ShowTargetTrail}
+ icon={<FontAwesomeIcon icon="taxi" />}
+ 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)}
+ />
+ )}
</>
);
diff --git a/src/client/views/pdf/PDFViewer.scss b/src/client/views/pdf/PDFViewer.scss
index 470aa3eb1..cfe07f6cb 100644
--- a/src/client/views/pdf/PDFViewer.scss
+++ b/src/client/views/pdf/PDFViewer.scss
@@ -26,15 +26,9 @@
.textLayer {
opacity: unset;
mix-blend-mode: multiply; // bcz: makes text fuzzy!
-
- // span {
- // padding-right: 5px;
- // padding-bottom: 4px;
- // }
}
-
.textLayer ::selection {
- background: #accef7;
+ background: #accef76a;
}
// should match the backgroundColor in createAnnotation()
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 40b745cc4..1319a236d 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -2,7 +2,8 @@ import { action, computed, IReactionDisposer, observable, ObservableMap, reactio
import { observer } from 'mobx-react';
import * as Pdfjs from 'pdfjs-dist';
import 'pdfjs-dist/web/pdf_viewer.css';
-import { Doc, DocListCast, Field, HeightSym, Opt } from '../../../fields/Doc';
+import { Doc, DocListCast, Field, Opt } from '../../../fields/Doc';
+import { Height } from '../../../fields/DocSymbols';
import { Id } from '../../../fields/FieldSymbols';
import { InkTool } from '../../../fields/InkField';
import { Cast, NumCast, StrCast } from '../../../fields/Types';
@@ -25,6 +26,7 @@ import { Annotation } from './Annotation';
import './PDFViewer.scss';
import React = require('react');
import { GPTPopup } from './GPTPopup/GPTPopup';
+import { InkingStroke } from '../InkingStroke';
const PDFJSViewer = require('pdfjs-dist/web/pdf_viewer');
const pdfjsLib = require('pdfjs-dist');
const _global = (window /* browser */ || global) /* node */ as any;
@@ -90,7 +92,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
@observable isAnnotating = false;
// key where data is stored
@computed get allAnnotations() {
- return DocUtils.FilterDocs(DocListCast(this.props.dataDoc[this.props.fieldKey + '_annotations']), this.props.docFilters(), this.props.docRangeFilters());
+ return DocUtils.FilterDocs(DocListCast(this.props.dataDoc[this.props.fieldKey + '_annotations']), this.props.childFilters(), this.props.childFiltersByRanges());
}
@computed get inlineTextAnnotations() {
return this.allAnnotations.filter(a => a.textInlineAnnotations);
@@ -181,7 +183,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
let focusSpeed: Opt<number>;
if (doc !== this.props.rootDoc && mainCont) {
const windowHeight = this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1);
- const scrollTo = Utils.scrollIntoView(scrollTop, doc[HeightSym](), NumCast(this.props.layoutDoc._layout_scrollTop), windowHeight, windowHeight * 0.1, this._scrollHeight);
+ const scrollTo = Utils.scrollIntoView(scrollTop, doc[Height](), NumCast(this.props.layoutDoc._layout_scrollTop), windowHeight, windowHeight * 0.1, this._scrollHeight);
if (scrollTo !== undefined && scrollTo !== this.props.layoutDoc._layout_scrollTop) {
if (!this._pdfViewer) this._initialScroll = { loc: scrollTo, easeFunc: options.easeFunc };
else if (!options.instant) this._scrollStopper = smoothScroll((focusSpeed = options.zoomTime ?? 500), mainCont, scrollTo, options.easeFunc, this._scrollStopper);
@@ -482,16 +484,17 @@ export class PDFViewer extends React.Component<IViewerProps> {
}
};
- pointerEvents = () => (this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' && !MarqueeOptionsMenu.Instance.isShown() ? 'all' : SnappingManager.GetIsDragging() ? undefined : 'none');
+ pointerEvents = () =>
+ this.props.isContentActive() && !MarqueeOptionsMenu.Instance.isShown()
+ ? 'all' //
+ : 'none';
@computed get annotationLayer() {
+ const inlineAnnos = this.inlineTextAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y)).filter(anno => !anno.hidden);
return (
<div className="pdfViewerDash-annotationLayer" style={{ height: Doc.NativeHeight(this.props.Document), transform: `scale(${NumCast(this.props.layoutDoc._freeform_scale, 1)})` }} ref={this._annotationLayer}>
- {this.inlineTextAnnotations
- .sort((a, b) => NumCast(a.y) - NumCast(b.y))
- .filter(anno => !anno.hidden)
- .map(anno => (
- <Annotation {...this.props} fieldKey={this.props.fieldKey + '_annotations'} pointerEvents={this.pointerEvents} showInfo={this.showInfo} dataDoc={this.props.dataDoc} anno={anno} key={`${anno[Id]}-annotation`} />
- ))}
+ {inlineAnnos.map(anno => (
+ <Annotation {...this.props} fieldKey={this.props.fieldKey + '_annotations'} pointerEvents={this.pointerEvents} showInfo={this.showInfo} dataDoc={this.props.dataDoc} anno={anno} key={`${anno[Id]}-annotation`} />
+ ))}
</div>
);
}
@@ -500,7 +503,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
return !this._overlayAnnoInfo ? null : (
<div className="pdfViewerDash-overlayAnno" style={{ top: NumCast(this._overlayAnnoInfo.y), left: NumCast(this._overlayAnnoInfo.x) }}>
<div className="pdfViewerDash-overlayAnno" style={{ right: -50, background: SharingManager.Instance.users.find(users => users.user.email === this._overlayAnnoInfo!.author)?.userColor }}>
- {this._overlayAnnoInfo.author + ' ' + Field.toString(this._overlayAnnoInfo.creationDate as Field)}
+ {this._overlayAnnoInfo.author + ' ' + Field.toString(this._overlayAnnoInfo.author_date as Field)}
</div>
</div>
);
@@ -512,17 +515,18 @@ export class PDFViewer extends React.Component<IViewerProps> {
overlayTransform = () => this.scrollXf().scale(1 / NumCast(this.props.layoutDoc._freeform_scale, 1));
panelWidth = () => this.props.PanelWidth() / (this.props.NativeDimScaling?.() || 1);
panelHeight = () => this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1);
- transparentFilter = () => [...this.props.docFilters(), Utils.IsTransparentFilter()];
- opaqueFilter = () => [...this.props.docFilters(), Utils.noDragsDocFilter, ...(DragManager.docsBeingDragged.length ? [] : [Utils.IsOpaqueFilter()])];
+ transparentFilter = () => [...this.props.childFilters(), Utils.IsTransparentFilter()];
+ opaqueFilter = () => [...this.props.childFilters(), Utils.noDragsDocFilter, ...(DragManager.docsBeingDragged.length && this.props.isContentActive() ? [] : [Utils.IsOpaqueFilter()])];
childStyleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string): any => {
if (doc instanceof Doc && property === StyleProp.PointerEvents) {
- if (doc.textInlineAnnotations) return 'none';
- return 'all';
+ if (this.inlineTextAnnotations.includes(doc) || this.props.isContentActive() === false) return 'none';
+ const isInk = doc && StrCast(Doc.Layout(doc).layout).includes(InkingStroke.name) && !props?.LayoutTemplateString;
+ return isInk ? 'visiblePainted' : 'all';
}
return this.props.styleProvider?.(doc, props, property);
};
- renderAnnotations = (docFilters: () => string[], mixBlendMode?: any, display?: string) => (
+ renderAnnotations = (childFilters: () => string[], mixBlendMode?: any, display?: string) => (
<div
className="pdfViewerDash-overlay"
style={{
@@ -535,8 +539,8 @@ export class PDFViewer extends React.Component<IViewerProps> {
NativeWidth={returnZero}
NativeHeight={returnZero}
setContentView={emptyFunction} // override setContentView to do nothing
- pointerEvents={SnappingManager.GetIsDragging() ? returnAll : returnNone} // freeform view doesn't get events unless something is being dragged onto it.
- childPointerEvents="all" // but freeform children need to get events to allow text editing, etc
+ pointerEvents={this.props.isContentActive() && (SnappingManager.GetIsDragging() || Doc.ActiveTool !== InkTool.None) ? returnAll : returnNone} // freeform view doesn't get events unless something is being dragged onto it.
+ childPointerEvents={this.props.isContentActive() !== false ? 'all' : 'none'} // but freeform children need to get events to allow text editing, etc
renderDepth={this.props.renderDepth + 1}
isAnnotationOverlay={true}
fieldKey={this.props.fieldKey + '_annotations'}
@@ -548,8 +552,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
ScreenToLocalTransform={this.overlayTransform}
isAnyChildContentActive={returnFalse}
isAnnotationOverlayScrollable={true}
- dropAction="embed"
- docFilters={docFilters}
+ childFilters={childFilters}
select={emptyFunction}
bringToFront={emptyFunction}
styleProvider={this.childStyleProvider}
@@ -557,14 +560,15 @@ export class PDFViewer extends React.Component<IViewerProps> {
</div>
);
@computed get overlayTransparentAnnotations() {
- return this.renderAnnotations(this.transparentFilter, 'multiply', DragManager.docsBeingDragged.length ? 'none' : undefined);
+ const transparentChildren = DocUtils.FilterDocs(DocListCast(this.props.dataDoc[this.props.fieldKey + '_annotations']), this.transparentFilter(), []);
+ return !transparentChildren.length ? null : this.renderAnnotations(this.transparentFilter, 'multiply', DragManager.docsBeingDragged.length && this.props.isContentActive() ? 'none' : undefined);
}
@computed get overlayOpaqueAnnotations() {
return this.renderAnnotations(this.opaqueFilter, this.allAnnotations.some(anno => anno.mixBlendMode) ? 'hard-light' : undefined);
}
@computed get overlayLayer() {
return (
- <div style={{ pointerEvents: SnappingManager.GetIsDragging() ? 'all' : 'none' }}>
+ <div style={{ pointerEvents: this.props.isContentActive() && SnappingManager.GetIsDragging() ? 'all' : 'none' }}>
{this.overlayTransparentAnnotations}
{this.overlayOpaqueAnnotations}
</div>
diff --git a/src/client/views/search/SearchBox.scss b/src/client/views/search/SearchBox.scss
index e8865b918..a439aea3e 100644
--- a/src/client/views/search/SearchBox.scss
+++ b/src/client/views/search/SearchBox.scss
@@ -13,12 +13,16 @@
.searchBox-bar {
width: 100%;
- height: 35px;
+ height: fit-content;
display: flex;
justify-content: center;
align-items: center;
background-color: none;
padding: 5px;
+ top: 0px;
+ position: sticky;
+ overflow-y: scroll;
+ border-bottom: $standard-border;
.searchBox-type {
display: block;
@@ -42,34 +46,66 @@
}
}
- .searchBox-results-container {
+ .section-header {
+
+ .section-title {
+ font-size: $body-text;
+ font-weight: 600;
+ }
+
+ .section-subtitle {
+ display: flex;
+ color: $light-gray;
+ }
+
+ padding: 5px 10px;
+ display: flex;
+ flex-direction: column;
+ gap: 3px;
+ background: $medium-blue;
+ color: white;
+ }
+
+ .searchBox-recommendations-container {
display: flex;
flex-direction: column;
width: 100%;
- height: 100%;
+ height: fit-content;
justify-content: "center";
-
- .searchBox-results-count {
+
+ .searchBox-recommendations-view {
+ margin-top: 10px;
display: flex;
- color: gray;
- margin-left: 5px;
+ width: 100%;
+ height: fit-content;
+ flex-direction: column;
+ gap: 10px;
+ padding: 0px 10px;
+
+
}
+ }
+
+ .searchBox-results-container {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ height: fit-content;
+ justify-content: "center";
- .searchBox-results-scroll-view {
- margin-top: 10px;
+ .searchBox-results-view {
display: inline-block;
width: 100%;
- height: calc(100% - 55px);
- overflow-y: scroll;
+ height: fit-content;
.searchBox-results-scroll-view-result {
display: inline-block;
vertical-align: middle;
width: 100%;
- height: 50px;
+ height: fit-content;
cursor: pointer;
font-size: 15px;
- padding: 11px;
+ padding: 10px;
&.searchBox-results-scroll-view-result-selected {
background: #999;
@@ -81,6 +117,8 @@
width: calc(100% - 45px);
text-align: left;
overflow: hidden;
+ max-height: 2.4em;
+ line-height: 1.2em;
text-overflow: ellipsis;
}
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index 536ec7c75..1ceea697a 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -2,7 +2,8 @@ import { Tooltip } from '@material-ui/core';
import { action, computed, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { DirectLinksSym, Doc, DocListCast, DocListCastAsync, Field, Opt } from '../../../fields/Doc';
+import { Doc, DocListCast, DocListCastAsync, Field, Opt } from '../../../fields/Doc';
+import { DirectLinks } from '../../../fields/DocSymbols';
import { Id } from '../../../fields/FieldSymbols';
import { DocCast, StrCast } from '../../../fields/Types';
import { DocUtils } from '../../documents/Documents';
@@ -14,6 +15,10 @@ import { CollectionDockingView } from '../collections/CollectionDockingView';
import { ViewBoxBaseComponent } from '../DocComponent';
import { FieldView, FieldViewProps } from '../nodes/FieldView';
import './SearchBox.scss';
+import { fetchRecommendations } from '../newlightbox/utils';
+import { IRecommendation, Recommendation } from '../newlightbox/components';
+import { Colors } from '../global/globalEnums';
+import { SettingsManager } from '../../util/SettingsManager';
const DAMPENING_FACTOR = 0.9;
const MAX_ITERATIONS = 25;
@@ -42,6 +47,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
@observable _searchString = '';
@observable _docTypeString = 'all';
@observable _results: Map<Doc, string[]> = new Map<Doc, string[]>();
+ @observable _recommendations: IRecommendation[] = [];
@observable _pageRanks: Map<Doc, number> = new Map<Doc, number>();
@observable _linkedDocsOut: Map<Doc, Set<Doc>> = new Map<Doc, Set<Doc>>();
@observable _linkedDocsIn: Map<Doc, Set<Doc>> = new Map<Doc, Set<Doc>>();
@@ -141,6 +147,8 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
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;
@@ -211,7 +219,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
}
@action
static staticSearchCollection(rootDoc: Opt<Doc>, query: string) {
- const blockedTypes = [DocumentType.PRESELEMENT, DocumentType.MARKER, DocumentType.KVP, DocumentType.FILTER, DocumentType.SEARCH, DocumentType.SEARCHITEM, DocumentType.FONTICON, DocumentType.BUTTON, DocumentType.SCRIPTING];
+ const blockedTypes = [DocumentType.PRESELEMENT, DocumentType.CONFIG, DocumentType.KVP, DocumentType.SEARCH, DocumentType.FONTICON, DocumentType.BUTTON, DocumentType.SCRIPTING];
const blockedKeys = [
'x',
'y',
@@ -219,7 +227,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
'width',
'layout_autoHeight',
'acl-Override',
- 'acl-Public',
+ 'acl-Guest',
'embedContainer',
'zIndex',
'height',
@@ -240,7 +248,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
'layout',
'layout_keyValue',
'layout_fitWidth',
- 'viewType',
+ 'type_collection',
'title_custom',
'freeform_panX',
'freeform_panY',
@@ -290,7 +298,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
this._results.forEach((_, doc) => {
this._pageRanks.set(doc, 1.0 / this._results.size);
- if (Doc.GetProto(doc)[DirectLinksSym].size === 0) {
+ if (Doc.GetProto(doc)[DirectLinks].size === 0) {
this._linkedDocsOut.set(doc, new Set(this._results.keys()));
this._results.forEach((_, linkedDoc) => {
@@ -299,7 +307,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
} else {
const linkedDocSet = new Set<Doc>();
- Doc.GetProto(doc)[DirectLinksSym].forEach(link => {
+ Doc.GetProto(doc)[DirectLinks].forEach(link => {
const d1 = link?.link_anchor_1 as Doc;
const d2 = link?.link_anchor_2 as Doc;
if (doc === d1 && this._results.has(d2)) {
@@ -391,6 +399,37 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
if (query) {
this.searchCollection(query);
+ const response = await fetchRecommendations('', query, [], true);
+ const recs = response.recommendations;
+ const recommendations: IRecommendation[] = [];
+ for (const key in recs) {
+ const title = recs[key].title;
+ const url = recs[key].url;
+ const type = recs[key].type;
+ const text = recs[key].text;
+ const transcript = recs[key].transcript;
+ const previewUrl = recs[key].previewUrl;
+ const embedding = recs[key].embedding;
+ const distance = recs[key].distance;
+ const source = recs[key].source;
+ const related_concepts = recs[key].related_concepts;
+ const docId = recs[key].doc_id;
+ recommendations.push({
+ title: title,
+ data: url,
+ type: type,
+ text: text,
+ transcript: transcript,
+ previewUrl: previewUrl,
+ embedding: embedding,
+ distance: Math.round(distance * 100) / 100,
+ source: source,
+ related_concepts: related_concepts,
+ docId: docId,
+ });
+ }
+ const setRecommendations = action(() => (this._recommendations = recommendations));
+ setRecommendations();
}
};
@@ -400,6 +439,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
*/
resetSearch = action(() => {
this._results.forEach((_, doc) => {
+ DocumentManager.Instance.getFirstDocumentView(doc)?.ComponentView?.search?.('', undefined, true);
Doc.UnBrushDoc(doc);
Doc.UnHighlightDoc(doc);
Doc.ClearSearchMatches();
@@ -422,7 +462,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
*/
@computed
public get selectOptions() {
- const selectValues = ['all', 'rtf', 'image', 'pdf', 'web', 'video', 'audio', 'collection'];
+ const selectValues = ['all', DocumentType.RTF, DocumentType.IMG, DocumentType.PDF, DocumentType.WEB, DocumentType.VID, DocumentType.AUDIO, DocumentType.COL];
return selectValues.map(value => (
<option key={value} value={value}>
@@ -478,15 +518,19 @@ 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">{result[1].join(', ')}</div>
+ <div className="searchBox-result-keys" style={{ color: SettingsManager.Instance.userVariantColor }}>
+ {result[1].join(', ')}
+ </div>
</div>
</Tooltip>
);
}
});
+ const recommendationsJSX: JSX.Element[] = this._recommendations.map(props => <Recommendation {...props} />);
+
return (
- <div style={{ pointerEvents: 'all' }} className="searchBox-container">
+ <div className="searchBox-container" style={{ pointerEvents: 'all', color: SettingsManager.Instance.userColor, background: SettingsManager.Instance.userBackgroundColor }}>
<div className="searchBox-bar">
{isLinkSearch ? null : (
<select name="type" id="searchBox-type" className="searchBox-type" onChange={this.onSelectChange}>
@@ -509,10 +553,24 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
ref={this._inputRef}
/>
</div>
- <div className="searchBox-results-container">
- <div className="searchBox-results-count">{`${validResults}` + ' result' + (validResults === 1 ? '' : 's')}</div>
- <div className="searchBox-results-scroll-view">{resultsJSX}</div>
- </div>
+ {resultsJSX.length > 0 && (
+ <div className="searchBox-results-container">
+ <div className="section-header" style={{ background: StrCast(Doc.UserDoc().userVariantColor, Colors.MEDIUM_BLUE) }}>
+ <div className="section-title">Results</div>
+ <div className="section-subtitle">{`${validResults}` + ' result' + (validResults === 1 ? '' : 's')}</div>
+ </div>
+ <div className="searchBox-results-view">{resultsJSX}</div>
+ </div>
+ )}
+ {recommendationsJSX.length > 0 && (
+ <div className="searchBox-recommendations-container">
+ <div className="section-header" style={{ background: StrCast(Doc.UserDoc().userVariantColor, Colors.MEDIUM_BLUE) }}>
+ <div className="section-title">Recommendations</div>
+ <div className="section-subtitle">{`${validResults}` + ' result' + (validResults === 1 ? '' : 's')}</div>
+ </div>
+ <div className="searchBox-recommendations-view">{recommendationsJSX}</div>
+ </div>
+ )}
</div>
);
}
diff --git a/src/client/views/selectedDoc/SelectedDocView.scss b/src/client/views/selectedDoc/SelectedDocView.scss
new file mode 100644
index 000000000..156dfc37b
--- /dev/null
+++ b/src/client/views/selectedDoc/SelectedDocView.scss
@@ -0,0 +1,3 @@
+.selectedDocView-container {
+
+} \ No newline at end of file
diff --git a/src/client/views/selectedDoc/SelectedDocView.tsx b/src/client/views/selectedDoc/SelectedDocView.tsx
new file mode 100644
index 000000000..955a4a174
--- /dev/null
+++ b/src/client/views/selectedDoc/SelectedDocView.tsx
@@ -0,0 +1,47 @@
+import React = require('react');
+import { Doc } from "../../../fields/Doc";
+import { observer } from "mobx-react";
+import { computed } from "mobx";
+import { StrCast } from "../../../fields/Types";
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Colors, ListBox } from 'browndash-components';
+import { DocumentManager } from '../../util/DocumentManager';
+import { DocFocusOptions } from '../nodes/DocumentView';
+
+export interface SelectedDocViewProps {
+ selectedDocs: Doc[];
+}
+
+@observer
+export class SelectedDocView extends React.Component<SelectedDocViewProps> {
+
+ @computed get selectedDocs() {
+ return this.props.selectedDocs;
+ }
+
+
+ render() {
+ return <div className={`selectedDocView-container`}>
+ <ListBox
+ items={this.selectedDocs.map((doc) => {
+ const icon = Doc.toIcon(doc);
+ const iconEle = <FontAwesomeIcon size={'1x'} icon={icon} />;
+ const text = StrCast(doc.title)
+ const finished = () => {
+
+ };
+ const options: DocFocusOptions = {
+ playAudio: false,
+ };
+ return {
+ text: text,
+ val: StrCast(doc._id),
+ icon: iconEle,
+ onClick: () => {DocumentManager.Instance.showDocument(doc, options, finished);}
+ }
+ })}
+ color={StrCast(Doc.UserDoc().userColor)}
+ />
+ </div>
+ }
+} \ No newline at end of file
diff --git a/src/client/views/selectedDoc/index.ts b/src/client/views/selectedDoc/index.ts
new file mode 100644
index 000000000..1f1db91f6
--- /dev/null
+++ b/src/client/views/selectedDoc/index.ts
@@ -0,0 +1 @@
+export * from './SelectedDocView' \ No newline at end of file
diff --git a/src/client/views/topbar/TopBar.scss b/src/client/views/topbar/TopBar.scss
index a1131b92e..2237d5ac1 100644
--- a/src/client/views/topbar/TopBar.scss
+++ b/src/client/views/topbar/TopBar.scss
@@ -1,243 +1,240 @@
-@import "../global/globalCssVariables";
+@import '../global/globalCssVariables';
.topbar-container {
- flex-direction: column;
- font-size: 10px;
- line-height: 1;
- overflow-y: auto;
- overflow-x: visible;
- background: $dark-gray;
- overflow: visible;
- z-index: 1000;
- align-items: center;
- height: $topbar-height;
- background-color: $dark-gray;
- border-bottom: $standard-border;
- padding: 0px 10px;
- cursor: default;
- display: flex;
- justify-content: center;
+ flex-direction: column;
+ font-size: 10px;
+ line-height: 1;
+ overflow-y: auto;
+ overflow-x: visible;
+ background: $dark-gray;
+ overflow: visible;
+ z-index: 1000;
+ align-items: center;
+ height: $topbar-height;
+ background-color: $dark-gray;
+ border-bottom: $standard-border;
+ padding: 0px 10px;
+ cursor: default;
+ display: flex;
+ justify-content: center;
+ width: 100%;
+
+ .topbar-inner-container {
+ display: flex;
+ flex-direction: row;
+ position: relative;
+ display: grid;
+ grid-auto-columns: 33.3% 33.3% 33.3%;
width: 100%;
+ align-items: center;
- .topbar-inner-container {
- display: flex;
- flex-direction: row;
- position: relative;
- display: grid;
- grid-auto-columns: 33.3% 33.3% 33.3%;
- width: 100%;
- align-items: center;
-
- // &:first-child {
- // height: 20px;
- // }
- }
+ // &:first-child {
+ // height: 20px;
+ // }
+ }
- .topbar-button-text {
- color: $white;
- padding: 10px;
- size: 15;
+ .topbar-button-text {
+ color: $white;
+ padding: 10px;
+ size: 15;
- &:hover {
- font-weight: 500;
- }
- }
+ &:hover {
+ font-weight: 500;
+ }
+ }
- .topbar-button-icon {
- cursor: pointer;
- width: fit-content;
+ .topbar-button-icon {
+ cursor: pointer;
+ width: fit-content;
+ display: flex;
+ justify-content: center;
+ gap: 4px;
+ align-items: center;
+ justify-self: center;
+ align-self: center;
+ padding: 5px;
+ transition: linear 0.2s;
+ color: $white;
+
+ &:hover {
+ background-color: darken($color: $light-gray, $amount: 20);
+ font-weight: 500;
+ }
+ }
+
+ .topbar-title {
+ color: $white;
+ font-size: 17;
+ font-weight: 500;
+ }
+
+ .topbar-center {
+ grid-column: 2;
+ display: inline-flex;
+ justify-content: center;
+ align-items: center;
+ gap: 5px;
+
+ .topbar-dashboard-header {
+ font-weight: 600;
+ }
+ }
+
+ .topbar-right {
+ grid-column: 3;
+ position: relative;
+ display: flex;
+ justify-content: flex-end;
+ gap: 5px;
+ margin-right: 5px;
+ }
+
+ .topbar-left {
+ grid-column: 1;
+ color: black;
+ font-family: 'Roboto';
+ position: relative;
+ display: flex;
+ width: fit-content;
+ gap: 5px;
+
+ .logo-container {
+ font-size: 15;
display: flex;
+ flex-direction: row;
justify-content: center;
- gap: 4px;
align-items: center;
- justify-self: center;
- align-self: center;
- padding: 5px;
- transition: linear 0.2s;
- color: $white;
-
- &:hover {
- background-color: darken($color: $light-gray, $amount: 20);
- font-weight: 500;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -o-user-select: none;
+ user-select: none;
+
+ .logo {
+ background-color: transparent;
+ width: 25px;
+ height: 25px;
+ margin-right: 5px;
}
- }
+ }
- .topbar-title {
- color: $white;
- font-size: 17;
- font-weight: 500;
- }
-
- .topbar-center {
- grid-column: 2;
- display: inline-flex;
- justify-content: center;
- align-items: center;
- gap: 5px;
-
- .topbar-dashboard-header {
- font-weight: 600;
- }
- }
+ .topBar-icon:hover {
+ background-color: $close-red;
+ }
-
- .topbar-right {
- grid-column: 3;
- position: relative;
- display: flex;
- justify-content: flex-end;
- gap: 5px;
- margin-right: 5px;
- }
-
- .topbar-left {
- grid-column: 1;
- color: black;
+ .topbar-lozenge-user,
+ .topbar-lozenge {
+ height: 23;
+ font-size: 12;
+ color: white;
font-family: 'Roboto';
- position: relative;
+ font-weight: 400;
+ padding: 4px;
+ align-self: center;
+ margin-left: 7px;
display: flex;
- width: fit-content;
- gap: 5px;
-
- .logo-container {
- font-size: 15;
- display: flex;
- flex-direction: row;
- justify-content: center;
- align-items: center;
- -webkit-user-select: none;
- -khtml-user-select: none;
- -moz-user-select: none;
- -o-user-select: none;
- user-select: none;
-
- .logo {
- background-color: transparent;
- width: 25px;
- height: 25px;
- margin-right: 5px;
-
- }
- }
+ align-items: center;
- .topBar-icon:hover {
- background-color: $close-red;
- }
+ .topbar-dashSelect {
+ border: none;
+ background-color: transparent;
+ color: black;
+ font-family: 'Roboto';
+ font-size: 17;
+ font-weight: 500;
- .topbar-lozenge-user,
- .topbar-lozenge {
- height: 23;
- font-size: 12;
- color: white;
- font-family: 'Roboto';
- font-weight: 400;
- padding: 4px;
- align-self: center;
- margin-left: 7px;
- display: flex;
- align-items: center;
-
- .topbar-dashSelect {
- border: none;
- background-color: transparent;
- color: black;
- font-family: 'Roboto';
- font-size: 17;
- font-weight: 500;
-
- &:hover {
- cursor: pointer;
- }
- }
+ &:hover {
+ cursor: pointer;
+ }
}
+ }
+
+ .topbar-logoff {
+ border-radius: 3px;
+ background: olivedrab;
+ color: white;
+ display: none;
+ margin-left: 5px;
+ padding: 1px 2px 1px 2px;
+ cursor: pointer;
+ }
- .topbar-logoff {
- border-radius: 3px;
- background: olivedrab;
- color: white;
- display: none;
- margin-left: 5px;
- padding: 1px 2px 1px 2px;
- cursor: pointer;
- }
+ .topbar-logoff {
+ background: red;
+ }
+ .topbar-lozenge-user:hover {
.topbar-logoff {
- background: red;
+ display: inline-block;
}
-
- .topbar-lozenge-user:hover {
- .topbar-logoff {
- display: inline-block;
- }
- }
- }
-
- .topbar-barChild {
-
- &.topbar-collection {
- flex: 0 1 auto;
- margin-left: 2px;
- margin-right: 2px
- }
-
- &.topbar-input {
- margin: 5px;
- border-radius: 20px;
- border: $dark-gray;
- display: block;
- width: 130px;
- -webkit-transition: width 0.4s;
- transition: width 0.4s;
- /* align-self: stretch; */
- outline: none;
-
- &:focus {
- width: 500px;
- outline: none;
- }
+ }
+ }
+
+ .topbar-barChild {
+ &.topbar-collection {
+ flex: 0 1 auto;
+ margin-left: 2px;
+ margin-right: 2px;
+ }
+
+ &.topbar-input {
+ margin: 5px;
+ border-radius: 20px;
+ border: $dark-gray;
+ display: block;
+ width: 130px;
+ -webkit-transition: width 0.4s;
+ transition: width 0.4s;
+ /* align-self: stretch; */
+ outline: none;
+
+ &:focus {
+ width: 500px;
+ outline: none;
}
+ }
- &.topbar-filter {
- align-self: stretch;
+ &.topbar-filter {
+ align-self: stretch;
- button {
- transform: none;
+ button {
+ transform: none;
- &:hover {
- transform: none;
- }
- }
+ &:hover {
+ transform: none;
+ }
}
+ }
- &.topbar-submit {
- margin-left: 2px;
- margin-right: 2px
- }
+ &.topbar-submit {
+ margin-left: 2px;
+ margin-right: 2px;
+ }
- &.topbar-close {
- color: $white;
- max-height: $topbar-height;
- }
- }
+ &.topbar-close {
+ color: $white;
+ max-height: $topbar-height;
+ }
+ }
}
.topbar-results {
- display: flex;
- flex-direction: column;
- top: 300px;
- display: flex;
- flex-direction: column;
- height: 100%;
- overflow: visible;
-
- .no-result {
- width: 500px;
- background: $light-gray;
- padding: 10px;
- height: 50px;
- text-transform: uppercase;
- text-align: left;
- font-weight: bold;
- }
-} \ No newline at end of file
+ display: flex;
+ flex-direction: column;
+ top: 300px;
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ overflow: visible;
+
+ .no-result {
+ width: 500px;
+ background: $light-gray;
+ padding: 10px;
+ height: 50px;
+ text-transform: uppercase;
+ text-align: left;
+ font-weight: bold;
+ }
+}
diff --git a/src/client/views/topbar/TopBar.tsx b/src/client/views/topbar/TopBar.tsx
index 9bd2ba5ce..c194ede32 100644
--- a/src/client/views/topbar/TopBar.tsx
+++ b/src/client/views/topbar/TopBar.tsx
@@ -1,24 +1,27 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Button, IconButton, Size } from 'browndash-components';
-import { action, computed, observable } from 'mobx';
+import { Button, IconButton, Size, Type, isDark } 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 { AclAdmin, Doc } from '../../../fields/Doc';
+import { Doc, DocListCast } from '../../../fields/Doc';
+import { AclAdmin, DashVersion } from '../../../fields/DocSymbols';
import { StrCast } from '../../../fields/Types';
import { GetEffectiveAcl } from '../../../fields/util';
import { DocumentManager } from '../../util/DocumentManager';
-import { ReportManager } from '../../util/ReportManager';
+import { PingManager } from '../../util/PingManager';
+import { ReportManager } from '../../util/reportManager/ReportManager';
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 { Colors } from '../global/globalEnums';
import { MainView } from '../MainView';
+import { CollectionDockingView } from '../collections/CollectionDockingView';
+import { Colors } from '../global/globalEnums';
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
@@ -33,8 +36,22 @@ export class TopBar extends React.Component {
});
};
- @observable textColor: string = Colors.LIGHT_GRAY;
- @observable backgroundColor: string = Colors.DARK_GRAY;
+ @computed get color() {
+ return StrCast(Doc.UserDoc().userColor, Colors.LIGHT_GRAY);
+ }
+ @computed get variantColor() {
+ return StrCast(Doc.UserDoc().userVariantColor, Colors.MEDIUM_BLUE);
+ }
+ @computed get backgroundColor() {
+ return PingManager.Instance.IsBeating ? SettingsManager.Instance.userBackgroundColor : Colors.MEDIUM_GRAY;
+ }
+
+ @observable happyHeart: boolean = PingManager.Instance.IsBeating;
+ setHappyHeart = action((status: boolean) => (this.happyHeart = status));
+ dispose = reaction(
+ () => PingManager.Instance.IsBeating,
+ isBeating => this.setHappyHeart(isBeating)
+ );
/**
* Returns the left hand side of the topbar.
@@ -47,16 +64,20 @@ export class TopBar extends React.Component {
return (
<div className="topbar-left">
{Doc.ActiveDashboard ? (
- <IconButton onClick={this.navigateToHome} icon={<FontAwesomeIcon icon="home" />} color={this.textColor} />
+ <IconButton
+ onClick={this.navigateToHome}
+ icon={<FontAwesomeIcon icon={DocListCast(Doc.MySharedDocs.data_dashboards).some(dash => !DocListCast(Doc.MySharedDocs.viewed).includes(dash)) ? 'portrait' : 'home'} />}
+ color={this.color}
+ />
) : (
<div className="logo-container">
<img className="logo" src="/assets/medium-blue-light-blue-circle.png" alt="dash logo"></img>
- <span style={{ color: Colors.LIGHT_GRAY, fontWeight: 200 }}>brown</span>
- <span style={{ color: Colors.LIGHT_BLUE, fontWeight: 500 }}>dash</span>
+ <span style={{ color: isDark(this.backgroundColor) ? Colors.LIGHT_GRAY : Colors.DARK_GRAY, fontWeight: 200 }}>brown</span>
+ <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.textColor} onClick={action(() => (MainView.Instance._exploreMode = !MainView.Instance._exploreMode))} />
+ <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))} />
)}
</div>
);
@@ -83,36 +104,21 @@ export class TopBar extends React.Component {
<div className="topbar-center">
<Button
text={StrCast(Doc.ActiveDashboard.title)}
- tooltip="Browsing mode for directly navigating to documents"
+ tooltip="Open Dashboards"
size={Size.SMALL}
- color={'white'}
+ color={this.color}
+ style={{ fontWeight: 700, fontSize: '1rem' }}
onClick={(e: React.MouseEvent) => {
const dashView = Doc.ActiveDashboard && DocumentManager.Instance.getDocumentView(Doc.ActiveDashboard);
ContextMenu.Instance.addItem({ description: 'Open Dashboard View', event: this.navigateToHome, icon: 'edit' });
- ContextMenu.Instance.addItem({
- description: 'Snapshot Dashboard',
- event: async () => {
- const batch = UndoManager.StartBatch('snapshot');
- await DashboardView.snapshotDashboard();
- batch.end();
- },
- icon: 'edit',
- });
dashView?.showContextMenu(e.clientX + 20, e.clientY + 30);
}}
/>
- <Button
- text={GetEffectiveAcl(Doc.GetProto(Doc.ActiveDashboard)) === AclAdmin ? 'Share' : 'View Original'}
- onClick={() => {
- SharingManager.Instance.open(undefined, Doc.ActiveDashboard);
- }}
- size={Size.SMALL}
- />
{!Doc.noviceMode && (
<IconButton
tooltip="Work on a copy of the dashboard layout"
size={Size.SMALL}
- color={this.textColor}
+ color={this.color}
onClick={async () => {
const batch = UndoManager.StartBatch('snapshot');
await DashboardView.snapshotDashboard();
@@ -131,13 +137,31 @@ export class TopBar extends React.Component {
* and allows the user to access their account settings etc.
*/
@computed get topbarRight() {
+ const upToDate = DashVersion === CurrentUserUtils.ServerVersion;
return (
<div className="topbar-right">
- <IconButton size={Size.SMALL} color={Colors.LIGHT_GRAY} onClick={ServerStats.Instance.open} icon={<FaStamp />} />
- <IconButton size={Size.SMALL} color={Colors.LIGHT_GRAY} onClick={ReportManager.Instance.open} icon={<FaBug />} />
- <IconButton size={Size.SMALL} color={Colors.LIGHT_GRAY} onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/', '_blank')} icon={<FontAwesomeIcon icon="question-circle" />} />
- <IconButton size={Size.SMALL} color={Colors.LIGHT_GRAY} onClick={SettingsManager.Instance.open} icon={<FontAwesomeIcon icon="cog" />} />
- {/* <Button text={'Logout'} borderRadius={5} hoverStyle={'gray'} backgroundColor={Colors.DARK_GRAY} color={this.textColor} fontSize={FontSize.SECONDARY} onClick={() => window.location.assign(Utils.prepend('/logout'))} /> */}
+ {Doc.ActiveDashboard ? (
+ <Button
+ text={GetEffectiveAcl(Doc.ActiveDashboard) === AclAdmin ? 'Share' : 'View Original'}
+ type={Type.TERT}
+ color={this.variantColor}
+ onClick={() => {
+ SharingManager.Instance.open(undefined, Doc.ActiveDashboard);
+ }}
+ />
+ ) : null}
+ <IconButton tooltip={'Issue Reporter ⌘I'} size={Size.SMALL} color={this.color} onClick={ReportManager.Instance.open} icon={<FaBug />} />
+ <IconButton tooltip={'Documentation ⌘D'} size={Size.SMALL} color={this.color} onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/', '_blank')} icon={<FontAwesomeIcon icon="question-circle" />} />
+ <IconButton tooltip={'Settings ⌘⇧S'} size={Size.SMALL} color={this.color} onClick={SettingsManager.Instance.open} icon={<FontAwesomeIcon icon="cog" />} />
+ <IconButton
+ size={Size.SMALL}
+ onClick={ServerStats.Instance.open}
+ type={Type.TERT}
+ tooltip={'Server is ' + (PingManager.Instance.IsBeating ? '' : 'NOT ') + 'running ' + (upToDate ? DashVersion : 'out of date version:' + DashVersion)}
+ color={this.happyHeart ? (upToDate ? Colors.LIGHT_BLUE : Colors.YELLOW) : Colors.ERROR_RED}
+ icon={<FontAwesomeIcon icon={this.happyHeart ? 'heart' : 'heart-broken'} />}
+ />
+ {/* <Button text={'Logout'} borderRadius={5} hoverStyle={'gray'} backgroundColor={Colors.DARK_GRAY} color={this.color} fontSize={FontSize.SECONDARY} onClick={() => window.location.assign(Utils.prepend('/logout'))} /> */}
</div>
);
}
@@ -145,13 +169,15 @@ export class TopBar extends React.Component {
render() {
return (
//TODO:glr Add support for light / dark mode
- <div style={{ pointerEvents: 'all' }} className="topbar-container">
- <div
- className="topbar-inner-container"
- style={{
- color: this.textColor,
- background: this.backgroundColor,
- }}>
+ <div
+ style={{
+ pointerEvents: 'all',
+ color: this.color,
+ background: this.backgroundColor,
+ // borderColor: this.color
+ }}
+ className="topbar-container">
+ <div className="topbar-inner-container">
{this.topbarLeft}
{this.topbarCenter}
{this.topbarRight}
diff --git a/src/fields/CursorField.ts b/src/fields/CursorField.ts
index a8a2859d2..46f5a8e1c 100644
--- a/src/fields/CursorField.ts
+++ b/src/fields/CursorField.ts
@@ -1,44 +1,43 @@
-import { ObjectField } from "./ObjectField";
-import { observable } from "mobx";
-import { Deserializable } from "../client/util/SerializationHelper";
-import { serializable, createSimpleSchema, object, date } from "serializr";
-import { OnUpdate, ToScriptString, ToString, Copy } from "./FieldSymbols";
+import { ObjectField } from './ObjectField';
+import { observable } from 'mobx';
+import { Deserializable } from '../client/util/SerializationHelper';
+import { serializable, createSimpleSchema, object, date } from 'serializr';
+import { FieldChanged, ToScriptString, ToString, Copy } from './FieldSymbols';
export type CursorPosition = {
- x: number,
- y: number
+ x: number;
+ y: number;
};
export type CursorMetadata = {
- id: string,
- identifier: string,
- timestamp: number
+ id: string;
+ identifier: string;
+ timestamp: number;
};
export type CursorData = {
- metadata: CursorMetadata,
- position: CursorPosition
+ metadata: CursorMetadata;
+ position: CursorPosition;
};
const PositionSchema = createSimpleSchema({
x: true,
- y: true
+ y: true,
});
const MetadataSchema = createSimpleSchema({
id: true,
identifier: true,
- timestamp: true
+ timestamp: true,
});
const CursorSchema = createSimpleSchema({
metadata: object(MetadataSchema),
- position: object(PositionSchema)
+ position: object(PositionSchema),
});
-@Deserializable("cursor")
+@Deserializable('cursor')
export default class CursorField extends ObjectField {
-
@serializable(object(CursorSchema))
readonly data: CursorData;
@@ -50,7 +49,7 @@ export default class CursorField extends ObjectField {
setPosition(position: CursorPosition) {
this.data.position = position;
this.data.metadata.timestamp = Date.now();
- this[OnUpdate]?.();
+ this[FieldChanged]?.();
}
[Copy]() {
@@ -58,9 +57,9 @@ export default class CursorField extends ObjectField {
}
[ToScriptString]() {
- return "invalid";
+ return 'invalid';
}
[ToString]() {
- return "invalid";
+ return 'invalid';
}
-} \ No newline at end of file
+}
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 0cf4256d6..4ed7ccb61 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -9,11 +9,37 @@ 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 { UndoManager } from '../client/util/UndoManager';
+import { undoable, UndoManager } from '../client/util/UndoManager';
import { decycle } from '../decycler/decycler';
+import * as JSZipUtils from '../JSZipUtils';
import { DashColor, incrementTitleCopy, intersectRect, Utils } from '../Utils';
import { DateField } from './DateField';
-import { Copy, HandleUpdate, Id, OnUpdate, Parent, Self, SelfProxy, ToScriptString, ToString, Update } from './FieldSymbols';
+import {
+ AclAdmin,
+ AclAugment,
+ AclEdit,
+ AclPrivate,
+ AclReadonly,
+ Animation,
+ CachedUpdates,
+ DirectLinks,
+ DocAcl,
+ DocCss,
+ DocData,
+ DocFields,
+ DocLayout,
+ FieldKeys,
+ FieldTuples,
+ ForceServerWrite,
+ Height,
+ Highlight,
+ Initializing,
+ Self,
+ SelfProxy,
+ UpdatingFromServer,
+ Width,
+} from './DocSymbols';
+import { Copy, FieldChanged, HandleUpdate, Id, Parent, ToScriptString, ToString } from './FieldSymbols';
import { InkField, InkTool } from './InkField';
import { List, ListFieldName } from './List';
import { ObjectField } from './ObjectField';
@@ -24,9 +50,8 @@ import { listSpec } from './Schema';
import { ComputedField, ScriptField } from './ScriptField';
import { 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, updateFunction } from './util';
+import { deleteProperty, GetEffectiveAcl, getField, getter, makeEditable, makeReadOnly, normalizeEmail, setter, SharingPermissions, containedFieldChangedHandler } from './util';
import JSZip = require('jszip');
-import * as JSZipUtils from '../JSZipUtils';
export namespace Field {
export function toKeyValueString(doc: Doc, key: string): string {
const onDelegate = Object.keys(doc).includes(key.replace(/^_/, ''));
@@ -93,63 +118,38 @@ export function DocListCast(field: FieldResult, defaultVal: Doc[] = []) {
return Cast(field, listSpec(Doc), defaultVal).filter(d => d instanceof Doc) as Doc[];
}
-export const WidthSym = Symbol('Width');
-export const HeightSym = Symbol('Height');
-export const AnimationSym = Symbol('Animation');
-export const HighlightSym = Symbol('Highlight');
-export const DataSym = Symbol('Data');
-export const LayoutSym = Symbol('Layout');
-export const FieldsSym = Symbol('Fields');
-export const CssSym = Symbol('Css');
-export const AclSym = Symbol('Acl');
-export const DirectLinksSym = Symbol('DirectLinks');
-export const AclUnset = Symbol('AclUnset');
-export const AclPrivate = Symbol('AclOwnerOnly');
-export const AclReadonly = Symbol('AclReadOnly');
-export const AclAugment = Symbol('AclAugment');
-export const AclSelfEdit = Symbol('AclSelfEdit');
-export const AclEdit = Symbol('AclEdit');
-export const AclAdmin = Symbol('AclAdmin');
-export const UpdatingFromServer = Symbol('UpdatingFromServer');
-export const Initializing = Symbol('Initializing');
-export const ForceServerWrite = Symbol('ForceServerWrite');
-export const CachedUpdates = Symbol('Cached updates');
-
export enum aclLevel {
unset = -1,
unshared = 0,
viewable = 1,
augmentable = 2,
- selfEditable = 2.5,
editable = 3,
admin = 4,
}
// prettier-ignore
-export const HierarchyMapping: Map<symbol, { level:aclLevel; name: SharingPermissions }> = new Map([
- [AclPrivate, { level: aclLevel.unshared, name: SharingPermissions.None }],
- [AclReadonly, { level: aclLevel.viewable, name: SharingPermissions.View }],
- [AclAugment, { level: aclLevel.augmentable, name: SharingPermissions.Augment}],
- [AclSelfEdit, { level: aclLevel.selfEditable, name: SharingPermissions.SelfEdit }],
- [AclEdit, { level: aclLevel.editable, name: SharingPermissions.Edit }],
- [AclAdmin, { level: aclLevel.admin, name: SharingPermissions.Admin }],
- [AclUnset, { level: aclLevel.unset, name: SharingPermissions.Unset }],
+export const HierarchyMapping: Map<symbol, { level:aclLevel; name: SharingPermissions; image: string }> = new Map([
+ [AclPrivate, { level: aclLevel.unshared, name: SharingPermissions.None, image: 'â–²' }],
+ [AclReadonly, { level: aclLevel.viewable, name: SharingPermissions.View, image: '♦' }],
+ [AclAugment, { level: aclLevel.augmentable, name: SharingPermissions.Augment, image: '⬟' }],
+ [AclEdit, { level: aclLevel.editable, name: SharingPermissions.Edit, image: '⬢' }],
+ [AclAdmin, { level: aclLevel.admin, name: SharingPermissions.Admin, image: '⬢' }],
]);
-export const ReverseHierarchyMap: Map<string, { level: aclLevel; acl: symbol }> = new Map(Array.from(HierarchyMapping.entries()).map(value => [value[1].name, { level: value[1].level, acl: value[0] }]));
+export const ReverseHierarchyMap: Map<string, { level: aclLevel; acl: symbol; image: string }> = new Map(Array.from(HierarchyMapping.entries()).map(value => [value[1].name, { level: value[1].level, acl: value[0], image: value[1].image }]));
// caches the document access permissions for the current user.
// this recursively updates all protos as well.
export function updateCachedAcls(doc: Doc) {
if (!doc) return;
- const target = (doc as any)?.__fields ?? doc;
+ const target = (doc as any)?.__fieldTuples ?? doc;
const permissions: { [key: string]: symbol } = !target.author || target.author === Doc.CurrentUserEmail ? { 'acl-Me': AclAdmin } : {};
Object.keys(target).filter(key => key.startsWith('acl') && (permissions[key] = ReverseHierarchyMap.get(StrCast(target[key]))!.acl));
- if (Object.keys(permissions).length || doc[AclSym]?.length) {
- runInAction(() => (doc[AclSym] = permissions));
+ if (Object.keys(permissions).length || doc[DocAcl]?.length) {
+ runInAction(() => (doc[DocAcl] = permissions));
}
if (doc.proto instanceof Promise) {
- doc.proto.then(updateCachedAcls);
+ doc.proto.then(proto => updateCachedAcls(DocCast(proto)));
return doc.proto;
}
}
@@ -211,6 +211,15 @@ export class Doc extends RefField {
public static get MyTrails() {
return DocCast(Doc.ActiveDashboard?.myTrails);
}
+ public static IsInMyOverlay(doc: Doc) {
+ return DocListCast(Doc.MyOverlayDocs?.data).includes(doc);
+ }
+ public static AddToMyOverlay(doc: Doc) {
+ Doc.AddDocToList(Doc.MyOverlayDocs, undefined, doc);
+ }
+ public static RemFromMyOverlay(doc: Doc) {
+ Doc.RemoveDocFromList(Doc.MyOverlayDocs, undefined, doc);
+ }
public static get MyOverlayDocs() {
return DocCast(Doc.UserDoc().myOverlayDocs);
}
@@ -229,22 +238,6 @@ export class Doc extends RefField {
public static get MyFilesystem() {
return DocCast(Doc.UserDoc().myFilesystem);
}
- public static get MyFileOrphans() {
- return DocCast(Doc.UserDoc().myFileOrphans);
- }
- public static AddFileOrphan(doc: Doc) {
- if (
- doc &&
- Doc.MyFileOrphans instanceof Doc &&
- Doc.IsDocDataProto(doc) &&
- !Doc.IsSystem(doc) &&
- ![DocumentType.MARKER, DocumentType.KVP, DocumentType.LINK, DocumentType.LINKANCHOR].includes(doc.type as any) &&
- !doc.isFolder &&
- !doc.annotationOn
- ) {
- Doc.AddDocToList(Doc.MyFileOrphans, undefined, doc);
- }
- }
public static get MyTools() {
return DocCast(Doc.UserDoc().myTools);
}
@@ -286,25 +279,20 @@ export class Doc extends RefField {
set: setter,
get: getter,
// getPrototypeOf: (target) => Cast(target[SelfProxy].proto, Doc) || null, // TODO this might be able to replace the proto logic in getter
- has: (target, key) => GetEffectiveAcl(target) !== AclPrivate && key in target.__fields,
+ has: (target, key) => GetEffectiveAcl(target) !== AclPrivate && key in target.__fieldTuples,
ownKeys: target => {
- const obj = {} as any;
- if (GetEffectiveAcl(target) !== AclPrivate) Object.assign(obj, target.___fieldKeys);
- runInAction(() => (obj.__LAYOUT__ = target.__LAYOUT__));
- return Object.keys(obj);
+ const keys = GetEffectiveAcl(target) !== AclPrivate ? Object.keys(target[FieldKeys]) : [];
+ return [...keys, '__LAYOUT__'];
},
getOwnPropertyDescriptor: (target, prop) => {
- if (prop.toString() === '__LAYOUT__') {
+ if (prop.toString() === '__LAYOUT__' || !(prop in target[FieldKeys])) {
return Reflect.getOwnPropertyDescriptor(target, prop);
}
- if (prop in target.__fieldKeys) {
- return {
- configurable: true, //TODO Should configurable be true?
- enumerable: true,
- value: 0, //() => target.__fields[prop])
- };
- }
- return Reflect.getOwnPropertyDescriptor(target, prop);
+ return {
+ configurable: true, //TODO Should configurable be true?
+ enumerable: true,
+ value: 0, //() => target.__fieldTuples[prop])
+ };
},
deleteProperty: deleteProperty,
defineProperty: () => {
@@ -318,67 +306,64 @@ export class Doc extends RefField {
return docProxy;
}
- proto: Opt<Doc>;
[key: string]: FieldResult;
@serializable(alias('fields', map(autoObject(), { afterDeserialize: afterDocDeserialize })))
- private get __fields() {
- return this.___fields;
+ private get __fieldTuples() {
+ return this[FieldTuples];
}
- private set __fields(value) {
- this.___fields = value;
+ private set __fieldTuples(value) {
+ // called by deserializer to set all fields in one shot
+ this[FieldTuples] = value;
for (const key in value) {
const field = value[key];
- field !== undefined && (this.__fieldKeys[key] = true);
- if (!(field instanceof ObjectField)) continue;
- field[Parent] = this[Self];
- field[OnUpdate] = updateFunction(this[Self], key, field, this[SelfProxy]);
+ field !== undefined && (this[FieldKeys][key] = true);
+ if (field instanceof ObjectField) {
+ field[Parent] = this[Self];
+ field[FieldChanged] = containedFieldChangedHandler(this[SelfProxy], key, field);
+ }
}
}
- private get __fieldKeys() {
- return this.___fieldKeys;
- }
- private set __fieldKeys(value) {
- this.___fieldKeys = value;
- }
- @observable private ___fields: any = {};
- @observable private ___fieldKeys: any = {};
+ @observable private [FieldTuples]: any = {};
+ @observable private [FieldKeys]: any = {};
/// all of the raw acl's that have been set on this document. Use GetEffectiveAcl to determine the actual ACL of the doc for editing
- @observable public [AclSym]: { [key: string]: symbol } = {};
- @observable public [CssSym]: number = 0; // incrementer denoting a change to CSS layout
- @observable public [DirectLinksSym]: Set<Doc> = new Set();
- @observable public [AnimationSym]: Opt<Doc>;
- @observable public [HighlightSym]: boolean = false;
+ @observable public [DocAcl]: { [key: string]: symbol } = {};
+ @observable public [DocCss]: number = 0; // incrementer denoting a change to CSS layout
+ @observable public [DirectLinks] = new ObservableSet<Doc>();
+ @observable public [Animation]: Opt<Doc>;
+ @observable public [Highlight]: boolean = false;
static __Anim(Doc: Doc) {
// for debugging to print AnimationSym field easily.
- return Doc[AnimationSym];
+ return Doc[Animation];
}
private [UpdatingFromServer]: boolean = false;
private [ForceServerWrite]: boolean = false;
public [Initializing]: boolean = false;
- private [Update] = (diff: any) => {
- (!this[UpdatingFromServer] || this[ForceServerWrite]) && DocServer.UpdateField(this[Id], diff);
- };
-
private [Self] = this;
private [SelfProxy]: any;
- public [FieldsSym] = () => this[Self].___fields; // Object.keys(this).reduce((fields, key) => { fields[key] = this[key]; return fields; }, {} as any);
- public [WidthSym] = () => NumCast(this[SelfProxy]._width);
- public [HeightSym] = () => NumCast(this[SelfProxy]._height);
+ public [FieldChanged] = (diff: undefined | { op: '$addToSet' | '$remFromSet' | '$set'; items: Field[] | undefined; length: number | undefined; hint?: any }, serverOp: any) => {
+ if (!this[UpdatingFromServer] || this[ForceServerWrite]) {
+ DocServer.UpdateField(this[Id], serverOp);
+ }
+ };
+ public [DocFields] = () => this[Self][FieldTuples]; // Object.keys(this).reduce((fields, key) => { fields[key] = this[key]; return fields; }, {} as any);
+ public [Width] = () => NumCast(this[SelfProxy]._width);
+ public [Height] = () => NumCast(this[SelfProxy]._height);
public [ToScriptString] = () => `idToDoc("${this[Self][Id]}")`;
public [ToString] = () => `Doc(${GetEffectiveAcl(this[SelfProxy]) === AclPrivate ? '-inaccessible-' : this[SelfProxy].title})`;
- public get [LayoutSym]() {
+ public get [DocLayout]() {
return this[SelfProxy].__LAYOUT__;
}
- public get [DataSym]() {
+ public get [DocData](): Doc {
const self = this[SelfProxy];
return self.resolvedDataDoc && !self.isTemplateForField ? self : Doc.GetProto(Cast(Doc.Layout(self).resolvedDataDoc, Doc, null) || self);
}
@computed get __LAYOUT__(): Doc | undefined {
- const templateLayoutDoc = Cast(Doc.LayoutField(this[SelfProxy]), Doc, null);
+ const self = this[SelfProxy];
+ const templateLayoutDoc = Cast(Doc.LayoutField(self), Doc, null);
if (templateLayoutDoc) {
let renderFieldKey: any;
const layoutField = templateLayoutDoc[StrCast(templateLayoutDoc.layout_fieldKey, 'layout')];
@@ -387,7 +372,7 @@ export class Doc extends RefField {
} else {
return Cast(layoutField, Doc, null);
}
- return Cast(this[SelfProxy][renderFieldKey + '-layout[' + templateLayoutDoc[Id] + ']'], Doc, null) || templateLayoutDoc;
+ return Cast(self[renderFieldKey + '_layout[' + templateLayoutDoc[Id] + ']'], Doc, null) || templateLayoutDoc;
}
return undefined;
}
@@ -476,6 +461,9 @@ export namespace Doc {
// });
// }
+ export function SetContainer(doc: Doc, container: Doc) {
+ doc.embedContainer = container;
+ }
export function RunCachedUpdate(doc: Doc, field: string) {
const update = doc[CachedUpdates][field];
if (update) {
@@ -510,7 +498,7 @@ export namespace Doc {
export function GetT<T extends Field>(doc: Doc, key: string, ctor: ToConstructor<T>, ignoreProto: boolean = false): FieldResult<T> {
return Cast(Get(doc, key, ignoreProto), ctor) as FieldResult<T>;
}
- export function IsDocDataProto(doc: Doc) {
+ export function IsDataProto(doc: Doc) {
return GetT(doc, 'isDataDoc', 'boolean', true);
}
export function IsBaseProto(doc: Doc) {
@@ -533,17 +521,17 @@ export namespace Doc {
if (key.startsWith('_')) key = key.substring(1);
const hasProto = Doc.GetProto(doc) !== doc ? Doc.GetProto(doc) : undefined;
const onDeleg = Object.getOwnPropertyNames(doc).indexOf(key) !== -1;
- const onProto = hasProto && Object.getOwnPropertyNames(doc.proto).indexOf(key) !== -1;
+ const onProto = hasProto && Object.getOwnPropertyNames(hasProto).indexOf(key) !== -1;
if (onDeleg || !hasProto || (!onProto && !defaultProto)) {
doc[key] = value;
- } else doc.proto![key] = value;
+ } else hasProto[key] = value;
}
export function GetAllPrototypes(doc: Doc): Doc[] {
const protos: Doc[] = [];
let d: Opt<Doc> = doc;
while (d) {
protos.push(d);
- d = FieldValue(d.proto);
+ d = DocCast(FieldValue(d.proto));
}
return protos;
}
@@ -576,14 +564,14 @@ export namespace Doc {
// compare whether documents or their protos match
export function AreProtosEqual(doc?: Doc, other?: Doc) {
- return doc && other && Doc.GetProto(doc) === Doc.GetProto(other);
+ return doc && other && (doc === other || Doc.GetProto(doc) === Doc.GetProto(other));
}
// Gets the data document for the document. Note: this is mis-named -- it does not specifically
// return the doc's proto, but rather recursively searches through the proto inheritance chain
// and returns the document who's proto is undefined or whose proto is marked as a data doc ('isDataDoc').
export function GetProto(doc: Doc): Doc {
- const proto = doc && (Doc.GetT(doc, 'isDataDoc', 'boolean', true) ? doc : doc.proto || doc);
+ const proto = doc && (Doc.GetT(doc, 'isDataDoc', 'boolean', true) ? doc : DocCast(doc.proto, doc));
return proto === doc ? proto : Doc.GetProto(proto);
}
export function GetDataDoc(doc: Doc): Doc {
@@ -597,7 +585,7 @@ export namespace Doc {
let proto: Doc | undefined = doc;
while (proto) {
Object.keys(proto).forEach(key => results.add(key));
- proto = proto.proto;
+ proto = DocCast(FieldValue(proto.proto));
}
return Array.from(results);
@@ -674,7 +662,7 @@ export namespace Doc {
const bounds = docList.reduce(
(bounds, doc) => {
const [sptX, sptY] = [NumCast(doc.x), NumCast(doc.y)];
- const [bptX, bptY] = [sptX + doc[WidthSym](), sptY + doc[HeightSym]()];
+ const [bptX, bptY] = [sptX + doc[Width](), sptY + doc[Height]()];
return {
x: Math.min(sptX, bounds.x),
y: Math.min(sptY, bounds.y),
@@ -694,17 +682,18 @@ export namespace Doc {
Doc.SetLayout(embedding, Doc.MakeEmbedding(layout));
}
embedding.createdFrom = doc;
- embedding.proto_embeddingId = Doc.GetProto(doc).proto_embeddingId = NumCast(Doc.GetProto(doc).proto_embeddingId) + 1;
+ embedding.proto_embeddingId = Doc.GetProto(doc).proto_embeddingId = DocListCast(Doc.GetProto(doc).proto_embeddings).length - 1;
embedding.title = ComputedField.MakeFunction(`renameEmbedding(this)`);
embedding.author = Doc.CurrentUserEmail;
- Doc.AddDocToList(Doc.GetProto(doc)[DataSym], 'proto_embeddings', embedding);
+ Doc.AddDocToList(Doc.GetProto(doc)[DocData], 'proto_embeddings', embedding);
return embedding;
}
export function BestEmbedding(doc: Doc) {
- const bestEmbedding = Doc.GetProto(doc) ? DocListCast(doc.proto_embeddings).find(doc => !doc.embedContainer && doc.author === Doc.CurrentUserEmail) : doc;
+ const bestEmbedding = Doc.GetProto(doc) ? [doc, ...DocListCast(doc.proto_embeddings)].find(doc => !doc.embedContainer && doc.author === Doc.CurrentUserEmail) : doc;
+ bestEmbedding && Doc.AddDocToList(Doc.GetProto(doc), 'protoEmbeddings', doc);
return bestEmbedding ?? Doc.MakeEmbedding(doc);
}
@@ -749,7 +738,9 @@ export namespace Doc {
}
};
const docAtKey = doc[key];
- if (docAtKey instanceof Doc) {
+ if (key === 'author') {
+ assignKey(Doc.CurrentUserEmail);
+ } else if (docAtKey instanceof Doc) {
if (pruneDocs.includes(docAtKey)) {
// prune doc and do nothing
} else if (!Doc.IsSystem(docAtKey) && (key.startsWith('layout') || ['embedContainer', 'annotationOn', 'proto'].includes(key) || ((key === 'link_anchor_1' || key === 'link_anchor_2') && doc.author === Doc.CurrentUserEmail))) {
@@ -770,7 +761,7 @@ export namespace Doc {
}
})
);
- Array.from(doc[DirectLinksSym]).forEach(async link => {
+ Array.from(doc[DirectLinks]).forEach(async link => {
if (
cloneLinks ||
((cloneMap.has(DocCast(link.link_anchor_1)?.[Id]) || cloneMap.has(DocCast(DocCast(link.link_anchor_1)?.annotationOn)?.[Id])) &&
@@ -779,11 +770,10 @@ export namespace Doc {
linkMap.set(link[Id], await Doc.makeClone(link, cloneMap, linkMap, rtfs, exclusions, pruneDocs, cloneLinks));
}
});
- Doc.SetInPlace(copy, 'title', 'CLONE: ' + doc.title, true);
+ Doc.SetInPlace(copy, 'title', '>:' + doc.title, true);
copy.cloneOf = doc;
cloneMap.set(doc[Id], copy);
- Doc.AddFileOrphan(copy);
return copy;
}
export function repairClone(clone: Doc, cloneMap: Map<string, Doc>, visited: Set<Doc>) {
@@ -918,7 +908,7 @@ export namespace Doc {
// If it doesn't find the expanded layout, then it makes a delegate of the template layout and
// saves it on the data doc indexed by the template layout's id.
//
- const expandedLayoutFieldKey = templateField + '-layout[' + templateLayoutDoc[Id] + ']';
+ const expandedLayoutFieldKey = templateField + '_layout[' + templateLayoutDoc[Id] + ']';
let expandedTemplateLayout = targetDoc?.[expandedLayoutFieldKey];
if (templateLayoutDoc.resolvedDataDoc instanceof Promise) {
@@ -928,7 +918,7 @@ export namespace Doc {
if (templateLayoutDoc.resolvedDataDoc === (targetDoc.rootDocument || Doc.GetProto(targetDoc))) {
expandedTemplateLayout = templateLayoutDoc; // reuse an existing template layout if its for the same document with the same params
} else {
- templateLayoutDoc.resolvedDataDoc && (templateLayoutDoc = Cast(templateLayoutDoc.proto, Doc, null) || templateLayoutDoc); // if the template has already been applied (ie, a nested template), then use the template's prototype
+ templateLayoutDoc.resolvedDataDoc && (templateLayoutDoc = DocCast(templateLayoutDoc.proto, templateLayoutDoc)); // if the template has already been applied (ie, a nested template), then use the template's prototype
if (!targetDoc[expandedLayoutFieldKey]) {
_pendingMap.set(targetDoc[Id] + expandedLayoutFieldKey, true);
setTimeout(
@@ -937,6 +927,7 @@ export namespace Doc {
newLayoutDoc.rootDocument = targetDoc;
const dataDoc = Doc.GetProto(targetDoc);
newLayoutDoc.resolvedDataDoc = dataDoc;
+ newLayoutDoc['acl-Guest'] = SharingPermissions.Edit;
if (dataDoc[templateField] === undefined && templateLayoutDoc[templateField] instanceof List && (templateLayoutDoc[templateField] as any).length) {
dataDoc[templateField] = ComputedField.MakeFunction(`ObjectField.MakeCopy(templateLayoutDoc["${templateField}"] as List)`, { templateLayoutDoc: Doc.name }, { templateLayoutDoc });
}
@@ -967,7 +958,7 @@ export namespace Doc {
const field = ProxyField.WithoutProxy(() => doc[key]);
if (key === 'proto' && copyProto) {
if (doc.proto instanceof Doc && overwrite.proto instanceof Doc) {
- overwrite[key] = Doc.Overwrite(doc[key]!, overwrite.proto);
+ overwrite[key] = Doc.Overwrite(doc.proto, overwrite.proto);
}
} else {
if (field instanceof RefField) {
@@ -985,49 +976,99 @@ export namespace Doc {
return overwrite;
}
- export function MakeCopy(doc: Doc, copyProto: boolean = false, copyProtoId?: string, retitle = false): Doc {
- const copy = new Doc(copyProtoId, true);
- updateCachedAcls(copy);
- const exclude = [...Cast(doc.cloneFieldFilter, listSpec('string'), []), 'dragFactory_count', 'cloneFieldFilter'];
+ export function FindReferences(infield: Doc | List<any>, references: Set<Doc>, system: boolean | undefined) {
+ if (infield instanceof List<any>) {
+ infield.forEach(val => (val instanceof Doc || val instanceof List) && FindReferences(val, references, system));
+ return;
+ }
+ const doc = infield as Doc;
+ if (references.has(doc)) {
+ references.add(doc);
+ return;
+ }
+ const excludeLists = doc.title === 'My Recently Closed' || doc.title === 'My Header Bar' || doc.title === 'My Dashboards';
+ if (system !== undefined && ((system && !Doc.IsSystem(doc)) || (!system && Doc.IsSystem(doc)))) return;
+ references.add(doc);
Object.keys(doc).forEach(key => {
- if (exclude.includes(key)) return;
- const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
- const field = key === 'author' ? Doc.CurrentUserEmail : ProxyField.WithoutProxy(() => doc[key]);
- if (key === 'proto' && copyProto) {
- if (doc[key] instanceof Doc) {
- copy[key] = Doc.MakeCopy(doc[key]!, false);
+ if (key === 'proto') {
+ if (doc.proto instanceof Doc) {
+ Doc.FindReferences(doc.proto, references, system);
}
} else {
+ const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
+ const field = key === 'author' ? Doc.CurrentUserEmail : ProxyField.WithoutProxy(() => doc[key]);
if (field instanceof RefField) {
- copy[key] = field;
+ if (field instanceof Doc) {
+ if (key === 'myLinkDatabase') {
+ field instanceof Doc && references.add(field);
+ // skip docs that have been closed and are scheduled for garbage collection
+ } else {
+ Doc.FindReferences(field, references, system);
+ }
+ }
} else if (cfield instanceof ComputedField) {
- copy[key] = cfield[Copy](); // ComputedField.MakeFunction(cfield.script.originalScript);
} else if (field instanceof ObjectField) {
- copy[key] =
- doc[key] instanceof Doc
- ? key.includes('layout[')
- ? undefined
- : doc[key] // reference documents except remove documents that are expanded teplate fields
- : ObjectField.MakeCopy(field);
+ if (field instanceof Doc) {
+ Doc.FindReferences(field, references, system);
+ } else if (field instanceof List) {
+ !excludeLists && Doc.FindReferences(field, references, system);
+ } else if (field instanceof ProxyField) {
+ if (key === 'myLinkDatabase') {
+ field instanceof Doc && references.add(field);
+ // skip docs that have been closed and are scheduled for garbage collection
+ } else {
+ Doc.FindReferences(field.value, references, system);
+ }
+ } else if (field instanceof PrefetchProxy) {
+ Doc.FindReferences(field.value, references, system);
+ }
} else if (field instanceof Promise) {
debugger; //This shouldn't happend...
- } else {
- copy[key] = field;
}
}
});
+ }
+
+ export function MakeCopy(doc: Doc, copyProto: boolean = false, copyProtoId?: string, retitle = false): Doc {
+ const copy = new Doc(copyProtoId, true);
+ updateCachedAcls(copy);
+ const exclude = [...StrListCast(doc.cloneFieldFilter), 'dragFactory_count', 'cloneFieldFilter'];
+ Object.keys(doc)
+ .filter(key => !exclude.includes(key))
+ .forEach(key => {
+ if (key === 'proto' && copyProto) {
+ if (doc.proto instanceof Doc) {
+ copy[key] = Doc.MakeCopy(doc.proto, false);
+ }
+ } else {
+ const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
+ const field = key === 'author' ? Doc.CurrentUserEmail : ProxyField.WithoutProxy(() => doc[key]);
+ if (field instanceof RefField) {
+ copy[key] = field;
+ } else if (cfield instanceof ComputedField) {
+ copy[key] = cfield[Copy](); // ComputedField.MakeFunction(cfield.script.originalScript);
+ } else if (field instanceof ObjectField) {
+ copy[key] =
+ doc[key] instanceof Doc && key.includes('layout[')
+ ? undefined // remove expanded template field documents
+ : ObjectField.MakeCopy(field);
+ } else if (field instanceof Promise) {
+ debugger; //This shouldn't happend...
+ } else {
+ copy[key] = field;
+ }
+ }
+ });
if (copyProto) {
Doc.GetProto(copy).embedContainer = undefined;
Doc.GetProto(copy).proto_embeddings = new List<Doc>([copy]);
} else {
- Doc.AddDocToList(Doc.GetProto(copy)[DataSym], 'proto_embeddings', copy);
+ Doc.AddDocToList(Doc.GetProto(copy)[DocData], 'proto_embeddings', copy);
}
copy.embedContainer = undefined;
- Doc.defaultAclPrivate && (copy['acl-Public'] = 'Not Shared');
if (retitle) {
copy.title = incrementTitleCopy(StrCast(copy.title));
}
- Doc.AddFileOrphan(copy);
return copy;
}
@@ -1043,10 +1084,9 @@ export namespace Doc {
Object.keys(doc)
.filter(key => key.startsWith('acl'))
.forEach(key => (delegate[key] = doc[key]));
- if (!Doc.IsSystem(doc)) Doc.AddDocToList(doc[DataSym], 'proto_embeddings', delegate);
+ if (!Doc.IsSystem(doc)) Doc.AddDocToList(doc[DocData], 'proto_embeddings', delegate);
title && (delegate.title = title);
delegate[Initializing] = false;
- Doc.AddFileOrphan(delegate);
return delegate;
}
return undefined;
@@ -1067,7 +1107,7 @@ export namespace Doc {
delegate[Initializing] = true;
delegate.proto = delegateProto;
delegate.author = Doc.CurrentUserEmail;
- Doc.AddDocToList(delegateProto[DataSym], 'proto_embeddings', delegate);
+ Doc.AddDocToList(delegateProto[DocData], 'proto_embeddings', delegate);
delegate[Initializing] = false;
delegateProto[Initializing] = false;
return delegate;
@@ -1083,7 +1123,6 @@ export namespace Doc {
const applied = ApplyTemplateTo(templateDoc, target, targetKey, templateDoc.title + '(...' + _applyCount++ + ')');
target.layout_fieldKey = targetKey;
applied && (Doc.GetProto(applied).type = templateDoc.type);
- Doc.defaultAclPrivate && (applied['acl-Public'] = 'Not Shared');
return applied;
}
return undefined;
@@ -1094,7 +1133,7 @@ export namespace Doc {
target[targetKey] = new PrefetchProxy(templateDoc);
} else {
titleTarget && (Doc.GetProto(target).title = titleTarget);
- const setDoc = [AclAdmin, AclEdit].includes(GetEffectiveAcl(Doc.GetProto(target))) ? Doc.GetProto(target) : target;
+ const setDoc = [AclAdmin, AclEdit, AclAugment].includes(GetEffectiveAcl(Doc.GetProto(target))) ? Doc.GetProto(target) : target;
setDoc[targetKey] = new PrefetchProxy(templateDoc);
}
}
@@ -1171,7 +1210,7 @@ export namespace Doc {
}
export const brushManager = new DocBrush();
- export class DocData {
+ export class UserDocData {
@observable _user_doc: Doc = undefined!;
@observable _sharing_doc: Doc = undefined!;
@observable _searchQuery: string = '';
@@ -1180,8 +1219,8 @@ export namespace Doc {
// the document containing the view layout information - will be the Document itself unless the Document has
// a layout field or 'layout' is given.
export function Layout(doc: Doc, layout?: Doc): Doc {
- const overrideLayout = layout && Cast(doc[`${StrCast(layout.isTemplateForField, 'data')}-layout[` + layout[Id] + ']'], Doc, null);
- return overrideLayout || doc[LayoutSym] || doc;
+ const overrideLayout = layout && Cast(doc[`${StrCast(layout.isTemplateForField, 'data')}_layout[` + layout[Id] + ']'], Doc, null);
+ return overrideLayout || doc[DocLayout] || doc;
}
export function SetLayout(doc: Doc, layout: Doc | string) {
doc[StrCast(doc.layout_fieldKey, 'layout')] = layout;
@@ -1196,12 +1235,12 @@ export namespace Doc {
return Doc.NativeWidth(doc, dataDoc, useDim) / (Doc.NativeHeight(doc, dataDoc, useDim) || 1);
}
export function NativeWidth(doc?: Doc, dataDoc?: Doc, useWidth?: boolean) {
- return !doc ? 0 : NumCast(doc._nativeWidth, NumCast((dataDoc || doc)[Doc.LayoutFieldKey(doc) + '_nativeWidth'], useWidth ? doc[WidthSym]() : 0));
+ return !doc ? 0 : NumCast(doc._nativeWidth, NumCast((dataDoc || doc)[Doc.LayoutFieldKey(doc) + '_nativeWidth'], useWidth ? doc[Width]() : 0));
}
export function NativeHeight(doc?: Doc, dataDoc?: Doc, useHeight?: boolean) {
if (!doc) return 0;
- const nheight = (Doc.NativeWidth(doc, dataDoc, useHeight) * doc[HeightSym]()) / doc[WidthSym]();
- const dheight = NumCast((dataDoc || doc)[Doc.LayoutFieldKey(doc) + '_nativeHeight'], useHeight ? doc[HeightSym]() : 0);
+ const nheight = (Doc.NativeWidth(doc, dataDoc, useHeight) * doc[Height]()) / doc[Width]();
+ const dheight = NumCast((dataDoc || doc)[Doc.LayoutFieldKey(doc) + '_nativeHeight'], useHeight ? doc[Height]() : 0);
return NumCast(doc._nativeHeight, nheight || dheight);
}
export function SetNativeWidth(doc: Doc, width: number | undefined, fieldKey?: string) {
@@ -1211,7 +1250,7 @@ export namespace Doc {
doc[(fieldKey ?? Doc.LayoutFieldKey(doc)) + '_nativeHeight'] = height;
}
- const manager = new DocData();
+ const manager = new UserDocData();
export function SearchQuery(): string {
return manager._searchQuery;
}
@@ -1312,7 +1351,7 @@ export namespace Doc {
}
export function LinkEndpoint(linkDoc: Doc, anchorDoc: Doc) {
- if (linkDoc.link_anchor_2 === anchorDoc || (linkDoc.link_anchor_2 as Doc).annotationOn) return '2';
+ if (Doc.AreProtosEqual(linkDoc.link_anchor_2 as Doc, anchorDoc) || Doc.AreProtosEqual((linkDoc.link_anchor_2 as Doc).annotationOn as Doc, anchorDoc)) return '2';
return Doc.AreProtosEqual(anchorDoc, (linkDoc.link_anchor_1 as Doc).annotationOn as Doc) || Doc.AreProtosEqual(anchorDoc, linkDoc.link_anchor_1 as Doc) ? '1' : '2';
}
@@ -1346,16 +1385,16 @@ export namespace Doc {
export var highlightedDocs = new ObservableSet<Doc>();
export function IsHighlighted(doc: Doc) {
if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(Doc.GetProto(doc)) === AclPrivate || doc.opacity === 0) return false;
- return doc[HighlightSym] || Doc.GetProto(doc)[HighlightSym];
+ return doc[Highlight] || Doc.GetProto(doc)[Highlight];
}
export function HighlightDoc(doc: Doc, dataAndDisplayDocs = true, presEffect?: Doc) {
runInAction(() => {
highlightedDocs.add(doc);
- doc[HighlightSym] = true;
- doc[AnimationSym] = presEffect;
+ doc[Highlight] = true;
+ doc[Animation] = presEffect;
if (dataAndDisplayDocs) {
highlightedDocs.add(Doc.GetProto(doc));
- Doc.GetProto(doc)[HighlightSym] = true;
+ Doc.GetProto(doc)[Highlight] = true;
}
});
}
@@ -1365,8 +1404,8 @@ export namespace Doc {
(doc ? [doc] : Array.from(highlightedDocs)).forEach(doc => {
highlightedDocs.delete(doc);
highlightedDocs.delete(Doc.GetProto(doc));
- doc[HighlightSym] = Doc.GetProto(doc)[HighlightSym] = false;
- doc[AnimationSym] = undefined;
+ doc[Highlight] = Doc.GetProto(doc)[Highlight] = false;
+ doc[Animation] = undefined;
});
});
}
@@ -1419,55 +1458,57 @@ export namespace Doc {
}
export function setDocRangeFilter(container: Opt<Doc>, key: string, range?: number[]) {
if (!container) return;
- const docRangeFilters = Cast(container._docRangeFilters, listSpec('string'), []);
- for (let i = 0; i < docRangeFilters.length; i += 3) {
- if (docRangeFilters[i] === key) {
- docRangeFilters.splice(i, 3);
+ const childFiltersByRanges = Cast(container._childFiltersByRanges, listSpec('string'), []);
+ for (let i = 0; i < childFiltersByRanges.length; i += 3) {
+ if (childFiltersByRanges[i] === key) {
+ childFiltersByRanges.splice(i, 3);
break;
}
}
if (range !== undefined) {
- docRangeFilters.push(key);
- docRangeFilters.push(range[0].toString());
- docRangeFilters.push(range[1].toString());
- container._docRangeFilters = new List<string>(docRangeFilters);
+ childFiltersByRanges.push(key);
+ childFiltersByRanges.push(range[0].toString());
+ childFiltersByRanges.push(range[1].toString());
+ container._childFiltersByRanges = new List<string>(childFiltersByRanges);
}
}
+ export const FilterSep = '::';
+
// filters document in a container collection:
// all documents with the specified value for the specified key are included/excluded
// based on the modifiers :"check", "x", undefined
export function setDocFilter(container: Opt<Doc>, key: string, value: any, modifiers: 'remove' | 'match' | 'check' | 'x' | 'exists' | 'unset', toggle?: boolean, fieldPrefix?: string, append: boolean = true) {
if (!container) return;
- const filterField = '_' + (fieldPrefix ? fieldPrefix + '_' : '') + 'docFilters';
- const docFilters = Cast(container[filterField], listSpec('string'), []);
+ const filterField = '_' + (fieldPrefix ? fieldPrefix + '_' : '') + 'childFilters';
+ const childFilters = StrListCast(container[filterField]);
runInAction(() => {
- for (let i = 0; i < docFilters.length; i++) {
- const fields = docFilters[i].split(':'); // split key:value:modifier
+ 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 (toggle) modifiers = 'remove';
else return;
}
- docFilters.splice(i, 1);
- container[filterField] = new List<string>(docFilters);
+ childFilters.splice(i, 1);
+ container[filterField] = new List<string>(childFilters);
break;
}
}
- if (!docFilters.length && modifiers === 'match' && value === undefined) {
+ if (!childFilters.length && modifiers === 'match' && value === undefined) {
container[filterField] = undefined;
} else if (modifiers !== 'remove') {
- !append && (docFilters.length = 0);
- docFilters.push(key + ':' + value + ':' + modifiers);
- container[filterField] = new List<string>(docFilters);
+ !append && (childFilters.length = 0);
+ childFilters.push(key + FilterSep + value + FilterSep + modifiers);
+ container[filterField] = new List<string>(childFilters);
}
});
}
export function readDocRangeFilter(doc: Doc, key: string) {
- const docRangeFilters = Cast(doc._docRangeFilters, listSpec('string'), []);
- for (let i = 0; i < docRangeFilters.length; i += 3) {
- if (docRangeFilters[i] === key) {
- return [Number(docRangeFilters[i + 1]), Number(docRangeFilters[i + 2])];
+ const childFiltersByRanges = Cast(doc._childFiltersByRanges, listSpec('string'), []);
+ for (let i = 0; i < childFiltersByRanges.length; i += 3) {
+ if (childFiltersByRanges[i] === key) {
+ return [Number(childFiltersByRanges[i + 1]), Number(childFiltersByRanges[i + 2])];
}
}
}
@@ -1518,15 +1559,33 @@ export namespace Doc {
return style;
}
+ export function Paste(docids: string[], clone: boolean, addDocument: (doc: Doc | Doc[]) => boolean, ptx?: number, pty?: number, newPoint?: number[]) {
+ DocServer.GetRefFields(docids).then(async fieldlist => {
+ const list = Array.from(Object.values(fieldlist))
+ .map(d => DocCast(d))
+ .filter(d => d);
+ const docs = clone ? (await Promise.all(Doc.MakeClones(list, false))).map(res => res.clone) : list;
+ if (ptx !== undefined && pty !== undefined && newPoint !== undefined) {
+ const firstx = list.length ? NumCast(list[0].x) + ptx - newPoint[0] : 0;
+ const firsty = list.length ? NumCast(list[0].y) + pty - newPoint[1] : 0;
+ docs.map(doc => {
+ doc.x = NumCast(doc.x) - firstx;
+ doc.y = NumCast(doc.y) - firsty;
+ });
+ }
+ undoable(addDocument, 'Paste Doc')(docs); // embedContainer gets set in addDocument
+ });
+ }
+
// prettier-ignore
export function toIcon(doc?: Doc, isOpen?: boolean) {
- switch (StrCast(doc?.type)) {
+ switch (isOpen !== undefined ? DocumentType.COL: StrCast(doc?.type)) {
case DocumentType.IMG: return 'image';
case DocumentType.COMPARISON: return 'columns';
case DocumentType.RTF: return 'sticky-note';
case DocumentType.COL:
- const folder: IconProp = isOpen ? 'folder-open' : 'folder';
- const chevron: IconProp = isOpen ? 'chevron-down' : 'chevron-right';
+ const folder: IconProp = isOpen === true ? 'folder-open' : isOpen === false ? 'folder' : 'question';
+ const chevron: IconProp = isOpen === true ? 'chevron-down' : isOpen === false ? 'chevron-right' : 'question';
return !doc?.isFolder ? folder : chevron;
case DocumentType.WEB: return 'globe-asia';
case DocumentType.SCREENSHOT: return 'photo-video';
@@ -1752,6 +1811,16 @@ 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();
diff --git a/src/fields/DocSymbols.ts b/src/fields/DocSymbols.ts
index 65decc147..dc9d1084b 100644
--- a/src/fields/DocSymbols.ts
+++ b/src/fields/DocSymbols.ts
@@ -1,4 +1,4 @@
-export const Update = Symbol('DocUpdate');
+export const DocUpdated = Symbol('DocUpdated');
export const Self = Symbol('DocSelf');
export const SelfProxy = Symbol('DocSelfProxy');
export const FieldKeys = Symbol('DocFieldKeys');
@@ -13,7 +13,6 @@ export const DocFields = Symbol('DocFields');
export const DocCss = Symbol('DocCss');
export const DocAcl = Symbol('DocAcl');
export const DirectLinks = Symbol('DocDirectLinks');
-export const AclUnset = Symbol('DocAclUnset');
export const AclPrivate = Symbol('DocAclOwnerOnly');
export const AclReadonly = Symbol('DocAclReadOnly');
export const AclAugment = Symbol('DocAclAugment');
@@ -24,3 +23,5 @@ export const UpdatingFromServer = Symbol('DocUpdatingFromServer');
export const Initializing = Symbol('DocInitializing');
export const ForceServerWrite = Symbol('DocForceServerWrite');
export const CachedUpdates = Symbol('DocCachedUpdates');
+
+export const DashVersion = 'v0.5.4';
diff --git a/src/fields/FieldLoader.tsx b/src/fields/FieldLoader.tsx
index 2a7b936f7..a5a71833c 100644
--- a/src/fields/FieldLoader.tsx
+++ b/src/fields/FieldLoader.tsx
@@ -6,10 +6,9 @@ import './FieldLoader.scss';
@observer
export class FieldLoader extends React.Component {
- @observable public static ServerLoadStatus = { requested: 0, retrieved: 0 };
- public static active = false;
+ @observable public static ServerLoadStatus = { requested: 0, retrieved: 0, message: '' };
render() {
- return <div className="fieldLoader">{`Requested: ${FieldLoader.ServerLoadStatus.requested} ... ${FieldLoader.ServerLoadStatus.retrieved} `}</div>;
+ return <div className="fieldLoader">{`${FieldLoader.ServerLoadStatus.message} request: ${FieldLoader.ServerLoadStatus.requested} ... ${FieldLoader.ServerLoadStatus.retrieved} `}</div>;
}
}
diff --git a/src/fields/FieldSymbols.ts b/src/fields/FieldSymbols.ts
index e50c2856f..0dbeb064b 100644
--- a/src/fields/FieldSymbols.ts
+++ b/src/fields/FieldSymbols.ts
@@ -1,12 +1,9 @@
-export const Update = Symbol('Update');
-export const Self = Symbol('Self');
-export const SelfProxy = Symbol('SelfProxy');
-export const HandleUpdate = Symbol('HandleUpdate');
-export const Id = Symbol('Id');
-export const OnUpdate = Symbol('OnUpdate');
-export const Parent = Symbol('Parent');
-export const Copy = Symbol('Copy');
-export const ToValue = Symbol('ToValue');
-export const ToScriptString = Symbol('ToScriptString');
-export const ToPlainText = Symbol('ToPlainText');
-export const ToString = Symbol('ToString');
+export const HandleUpdate = Symbol('FieldHandleUpdate');
+export const Id = Symbol('FieldId');
+export const FieldChanged = Symbol('FieldChanged');
+export const Parent = Symbol('FieldParent');
+export const Copy = Symbol('FieldCopy');
+export const ToValue = Symbol('FieldToValue');
+export const ToScriptString = Symbol('FieldToScriptString');
+export const ToPlainText = Symbol('FieldToPlainText');
+export const ToString = Symbol('FieldToString');
diff --git a/src/fields/IconField.ts b/src/fields/IconField.ts
deleted file mode 100644
index 76c4ddf1b..000000000
--- a/src/fields/IconField.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-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/List.ts b/src/fields/List.ts
index dfd24cf7a..f3fcc87f7 100644
--- a/src/fields/List.ts
+++ b/src/fields/List.ts
@@ -1,218 +1,17 @@
-import { action, observable } from 'mobx';
+import { action, computed, observable } from 'mobx';
import { alias, list, serializable } from 'serializr';
import { DocServer } from '../client/DocServer';
import { ScriptingGlobals } from '../client/util/ScriptingGlobals';
import { afterDocDeserialize, autoObject, Deserializable } from '../client/util/SerializationHelper';
import { Field } from './Doc';
-import { Copy, OnUpdate, Parent, Self, SelfProxy, ToScriptString, ToString, Update } from './FieldSymbols';
+import { FieldTuples, Self, SelfProxy } from './DocSymbols';
+import { Copy, FieldChanged, Parent, ToScriptString, ToString } from './FieldSymbols';
import { ObjectField } from './ObjectField';
import { ProxyField } from './Proxy';
import { RefField } from './RefField';
import { listSpec } from './Schema';
import { Cast } from './Types';
-import { deleteProperty, getter, setter, updateFunction } from './util';
-
-const listHandlers: any = {
- /// Mutator methods
- copyWithin() {
- throw new Error('copyWithin not supported yet');
- },
- fill(value: any, start?: number, end?: number) {
- if (value instanceof RefField) {
- throw new Error('fill with RefFields not supported yet');
- }
- const res = this[Self].__fields.fill(value, start, end);
- this[Update]();
- return res;
- },
- pop(): any {
- const field = toRealField(this[Self].__fields.pop());
- this[Update]();
- return field;
- },
- push: action(function (this: any, ...items: any[]) {
- items = items.map(toObjectField);
-
- const list = this[Self];
- const length = list.__fields.length;
- for (let i = 0; i < items.length; i++) {
- const item = items[i];
- //TODO Error checking to make sure parent doesn't already exist
- if (item instanceof ObjectField) {
- item[Parent] = list;
- item[OnUpdate] = updateFunction(list, i + length, item, this);
- }
- }
- const res = list.__fields.push(...items);
- this[Update]({ op: '$addToSet', items, length: length + items.length });
- return res;
- }),
- reverse() {
- const res = this[Self].__fields.reverse();
- this[Update]();
- return res;
- },
- shift() {
- const res = toRealField(this[Self].__fields.shift());
- this[Update]();
- return res;
- },
- sort(cmpFunc: any) {
- this[Self].__realFields(); // coerce retrieving entire array
- const res = this[Self].__fields.sort(cmpFunc ? (first: any, second: any) => cmpFunc(toRealField(first), toRealField(second)) : undefined);
- this[Update]();
- return res;
- },
- splice: action(function (this: any, start: number, deleteCount: number, ...items: any[]) {
- this[Self].__realFields(); // coerce retrieving entire array
- items = items.map(toObjectField);
- const list = this[Self];
- const removed = list.__fields.filter((item: any, i: number) => i >= start && i < start + deleteCount);
- for (let i = 0; i < items.length; i++) {
- const item = items[i];
- //TODO Error checking to make sure parent doesn't already exist
- //TODO Need to change indices of other fields in array
- if (item instanceof ObjectField) {
- item[Parent] = list;
- item[OnUpdate] = updateFunction(list, i + start, item, this);
- }
- }
- let hintArray: {val : any, index : number}[] = [];
- for(let i = start; i < start + deleteCount; i++) {
- hintArray.push({val : list.__fields[i], index : i});
- }
- const res = list.__fields.splice(start, deleteCount, ...items);
- // the hint object sends the starting index of the slice and the number
- // of elements to delete.
- this[Update](
- items.length === 0 && deleteCount
- ? { op: '$remFromSet', items: removed, hint : { start : start, deleteCount : deleteCount }, length: list.__fields.length }
- : items.length && !deleteCount && start === list.__fields.length
- ? { op: '$addToSet', items, length: list.__fields.length }
- : undefined
- );
- return res.map(toRealField);
- }),
- unshift(...items: any[]) {
- items = items.map(toObjectField);
- const list = this[Self];
- const length = list.__fields.length;
- for (let i = 0; i < items.length; i++) {
- const item = items[i];
- //TODO Error checking to make sure parent doesn't already exist
- //TODO Need to change indices of other fields in array
- if (item instanceof ObjectField) {
- item[Parent] = list;
- item[OnUpdate] = updateFunction(list, i, item, this);
- }
- }
- const res = this[Self].__fields.unshift(...items);
- this[Update]();
- return res;
- },
- /// Accessor methods
- concat: action(function (this: any, ...items: any[]) {
- this[Self].__realFields();
- return this[Self].__fields.map(toRealField).concat(...items);
- }),
- includes(valueToFind: any, fromIndex: number) {
- if (valueToFind instanceof RefField) {
- return this[Self].__realFields().includes(valueToFind, fromIndex);
- } else {
- return this[Self].__fields.includes(valueToFind, fromIndex);
- }
- },
- indexOf(valueToFind: any, fromIndex: number) {
- if (valueToFind instanceof RefField) {
- return this[Self].__realFields().indexOf(valueToFind, fromIndex);
- } else {
- return this[Self].__fields.indexOf(valueToFind, fromIndex);
- }
- },
- join(separator: any) {
- this[Self].__realFields();
- return this[Self].__fields.map(toRealField).join(separator);
- },
- lastElement() {
- return this[Self].__realFields().lastElement();
- },
- lastIndexOf(valueToFind: any, fromIndex: number) {
- if (valueToFind instanceof RefField) {
- return this[Self].__realFields().lastIndexOf(valueToFind, fromIndex);
- } else {
- return this[Self].__fields.lastIndexOf(valueToFind, fromIndex);
- }
- },
- slice(begin: number, end: number) {
- this[Self].__realFields();
- return this[Self].__fields.slice(begin, end).map(toRealField);
- },
-
- /// Iteration methods
- entries() {
- return this[Self].__realFields().entries();
- },
- every(callback: any, thisArg: any) {
- return this[Self].__realFields().every(callback, thisArg);
- // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
- // If we don't want to support the array parameter, we should use this version instead
- // return this[Self].__fields.every((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
- },
- filter(callback: any, thisArg: any) {
- return this[Self].__realFields().filter(callback, thisArg);
- // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
- // If we don't want to support the array parameter, we should use this version instead
- // return this[Self].__fields.filter((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
- },
- find(callback: any, thisArg: any) {
- return this[Self].__realFields().find(callback, thisArg);
- // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
- // If we don't want to support the array parameter, we should use this version instead
- // return this[Self].__fields.find((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
- },
- findIndex(callback: any, thisArg: any) {
- return this[Self].__realFields().findIndex(callback, thisArg);
- // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
- // If we don't want to support the array parameter, we should use this version instead
- // return this[Self].__fields.findIndex((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
- },
- forEach(callback: any, thisArg: any) {
- return this[Self].__realFields().forEach(callback, thisArg);
- // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
- // If we don't want to support the array parameter, we should use this version instead
- // return this[Self].__fields.forEach((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
- },
- map(callback: any, thisArg: any) {
- return this[Self].__realFields().map(callback, thisArg);
- // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
- // If we don't want to support the array parameter, we should use this version instead
- // return this[Self].__fields.map((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
- },
- reduce(callback: any, initialValue: any) {
- return this[Self].__realFields().reduce(callback, initialValue);
- // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
- // If we don't want to support the array parameter, we should use this version instead
- // return this[Self].__fields.reduce((acc:any, element:any, index:number, array:any) => callback(acc, toRealField(element), index, array), initialValue);
- },
- reduceRight(callback: any, initialValue: any) {
- return this[Self].__realFields().reduceRight(callback, initialValue);
- // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
- // If we don't want to support the array parameter, we should use this version instead
- // return this[Self].__fields.reduceRight((acc:any, element:any, index:number, array:any) => callback(acc, toRealField(element), index, array), initialValue);
- },
- some(callback: any, thisArg: any) {
- return this[Self].__realFields().some(callback, thisArg);
- // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
- // If we don't want to support the array parameter, we should use this version instead
- // return this[Self].__fields.some((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
- },
- values() {
- return this[Self].__realFields().values();
- },
- [Symbol.iterator]() {
- return this[Self].__realFields().values();
- },
-};
+import { deleteProperty, getter, setter, containedFieldChangedHandler } from './util';
function toObjectField(field: Field) {
return field instanceof RefField ? new ProxyField(field) : field;
@@ -222,41 +21,224 @@ function toRealField(field: Field) {
return field instanceof ProxyField ? field.value : field;
}
-function listGetter(target: any, prop: string | symbol, receiver: any): any {
- if (listHandlers.hasOwnProperty(prop)) {
- return listHandlers[prop];
- }
- return getter(target, prop, receiver);
-}
-
-interface ListSpliceUpdate<T> {
- type: 'splice';
- index: number;
- added: T[];
- removedCount: number;
-}
-
-interface ListIndexUpdate<T> {
- type: 'update';
- index: number;
- newValue: T;
-}
-
-type ListUpdate<T> = ListSpliceUpdate<T> | ListIndexUpdate<T>;
-
type StoredType<T extends Field> = T extends RefField ? ProxyField<T> : T;
-export const ListFieldName="fields";
+export const ListFieldName = 'fields';
@Deserializable('list')
class ListImpl<T extends Field> extends ObjectField {
+ static listHandlers: any = {
+ /// Mutator methods
+ copyWithin() {
+ throw new Error('copyWithin not supported yet');
+ },
+ fill(value: any, start?: number, end?: number) {
+ if (value instanceof RefField) {
+ throw new Error('fill with RefFields not supported yet');
+ }
+ const res = this[Self].__fieldTuples.fill(value, start, end);
+ this[SelfProxy][FieldChanged]?.();
+ return res;
+ },
+ pop(): any {
+ const field = toRealField(this[Self].__fieldTuples.pop());
+ this[SelfProxy][FieldChanged]?.();
+ return field;
+ },
+ push: action(function (this: ListImpl<any>, ...items: any[]) {
+ items = items.map(toObjectField);
+
+ const list = this[Self];
+ const length = list.__fieldTuples.length;
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ //TODO Error checking to make sure parent doesn't already exist
+ if (item instanceof ObjectField) {
+ item[Parent] = list;
+ item[FieldChanged] = containedFieldChangedHandler(this[SelfProxy], i + length, item);
+ }
+ }
+ const res = list.__fieldTuples.push(...items);
+ this[SelfProxy][FieldChanged]?.({ op: '$addToSet', items, length: length + items.length });
+ return res;
+ }),
+ reverse() {
+ const res = this[Self].__fieldTuples.reverse();
+ this[SelfProxy][FieldChanged]?.();
+ return res;
+ },
+ shift() {
+ const res = toRealField(this[Self].__fieldTuples.shift());
+ this[SelfProxy][FieldChanged]?.();
+ return res;
+ },
+ sort(cmpFunc: any) {
+ this[Self].__realFields; // coerce retrieving entire array
+ const res = this[Self].__fieldTuples.sort(cmpFunc ? (first: any, second: any) => cmpFunc(toRealField(first), toRealField(second)) : undefined);
+ this[SelfProxy][FieldChanged]?.();
+ return res;
+ },
+ splice: action(function (this: any, start: number, deleteCount: number, ...items: any[]) {
+ this[Self].__realFields; // coerce retrieving entire array
+ items = items.map(toObjectField);
+ const list = this[Self];
+ const removed = list.__fieldTuples.filter((item: any, i: number) => i >= start && i < start + deleteCount);
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ //TODO Error checking to make sure parent doesn't already exist
+ //TODO Need to change indices of other fields in array
+ if (item instanceof ObjectField) {
+ item[Parent] = list;
+ item[FieldChanged] = containedFieldChangedHandler(this, i + start, item);
+ }
+ }
+ let hintArray: { val: any; index: number }[] = [];
+ for (let i = start; i < start + deleteCount; i++) {
+ hintArray.push({ val: list.__fieldTuples[i], index: i });
+ }
+ const res = list.__fieldTuples.splice(start, deleteCount, ...items);
+ // the hint object sends the starting index of the slice and the number
+ // of elements to delete.
+ this[SelfProxy][FieldChanged]?.(
+ items.length === 0 && deleteCount
+ ? { op: '$remFromSet', items: removed, hint: { start, deleteCount }, length: list.__fieldTuples.length }
+ : items.length && !deleteCount && start === list.__fieldTuples.length
+ ? { op: '$addToSet', items, length: list.__fieldTuples.length }
+ : undefined
+ );
+ return res.map(toRealField);
+ }),
+ unshift(...items: any[]) {
+ items = items.map(toObjectField);
+ const list = this[Self];
+ for (let i = 0; i < items.length; i++) {
+ const item = items[i];
+ //TODO Error checking to make sure parent doesn't already exist
+ //TODO Need to change indices of other fields in array
+ if (item instanceof ObjectField) {
+ item[Parent] = list;
+ item[FieldChanged] = containedFieldChangedHandler(this, i, item);
+ }
+ }
+ const res = this[Self].__fieldTuples.unshift(...items);
+ this[SelfProxy][FieldChanged]?.();
+ return res;
+ },
+ /// Accessor methods
+ concat: action(function (this: any, ...items: any[]) {
+ this[Self].__realFields;
+ return this[Self].__fieldTuples.map(toRealField).concat(...items);
+ }),
+ includes(valueToFind: any, fromIndex: number) {
+ if (valueToFind instanceof RefField) {
+ return this[Self].__realFields.includes(valueToFind, fromIndex);
+ } else {
+ return this[Self].__fieldTuples.includes(valueToFind, fromIndex);
+ }
+ },
+ indexOf(valueToFind: any, fromIndex: number) {
+ if (valueToFind instanceof RefField) {
+ return this[Self].__realFields.indexOf(valueToFind, fromIndex);
+ }
+ return this[Self].__fieldTuples.indexOf(valueToFind, fromIndex);
+ },
+ join(separator: any) {
+ this[Self].__realFields;
+ return this[Self].__fieldTuples.map(toRealField).join(separator);
+ },
+ lastElement() {
+ return this[Self].__realFields.lastElement();
+ },
+ lastIndexOf(valueToFind: any, fromIndex: number) {
+ if (valueToFind instanceof RefField) {
+ return this[Self].__realFields.lastIndexOf(valueToFind, fromIndex);
+ } else {
+ return this[Self].__fieldTuples.lastIndexOf(valueToFind, fromIndex);
+ }
+ },
+ slice(begin: number, end: number) {
+ this[Self].__realFields;
+ return this[Self].__fieldTuples.slice(begin, end).map(toRealField);
+ },
+
+ /// Iteration methods
+ entries() {
+ return this[Self].__realFields.entries();
+ },
+ every(callback: any, thisArg: any) {
+ return this[Self].__realFields.every(callback, thisArg);
+ // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
+ // If we don't want to support the array parameter, we should use this version instead
+ // return this[Self].__fieldTuples.every((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
+ },
+ filter(callback: any, thisArg: any) {
+ return this[Self].__realFields.filter(callback, thisArg);
+ // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
+ // If we don't want to support the array parameter, we should use this version instead
+ // return this[Self].__fieldTuples.filter((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
+ },
+ find(callback: any, thisArg: any) {
+ return this[Self].__realFields.find(callback, thisArg);
+ // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
+ // If we don't want to support the array parameter, we should use this version instead
+ // return this[Self].__fieldTuples.find((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
+ },
+ findIndex(callback: any, thisArg: any) {
+ return this[Self].__realFields.findIndex(callback, thisArg);
+ // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
+ // If we don't want to support the array parameter, we should use this version instead
+ // return this[Self].__fieldTuples.findIndex((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
+ },
+ forEach(callback: any, thisArg: any) {
+ return this[Self].__realFields.forEach(callback, thisArg);
+ // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
+ // If we don't want to support the array parameter, we should use this version instead
+ // return this[Self].__fieldTuples.forEach((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
+ },
+ map(callback: any, thisArg: any) {
+ return this[Self].__realFields.map(callback, thisArg);
+ // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
+ // If we don't want to support the array parameter, we should use this version instead
+ // return this[Self].__fieldTuples.map((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
+ },
+ reduce(callback: any, initialValue: any) {
+ return this[Self].__realFields.reduce(callback, initialValue);
+ // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
+ // If we don't want to support the array parameter, we should use this version instead
+ // return this[Self].__fieldTuples.reduce((acc:any, element:any, index:number, array:any) => callback(acc, toRealField(element), index, array), initialValue);
+ },
+ reduceRight(callback: any, initialValue: any) {
+ return this[Self].__realFields.reduceRight(callback, initialValue);
+ // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
+ // If we don't want to support the array parameter, we should use this version instead
+ // return this[Self].__fieldTuples.reduceRight((acc:any, element:any, index:number, array:any) => callback(acc, toRealField(element), index, array), initialValue);
+ },
+ some(callback: any, thisArg: any) {
+ return this[Self].__realFields.some(callback, thisArg);
+ // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
+ // If we don't want to support the array parameter, we should use this version instead
+ // return this[Self].__fieldTuples.some((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
+ },
+ values() {
+ return this[Self].__realFields.values();
+ },
+ [Symbol.iterator]() {
+ return this[Self].__realFields.values();
+ },
+ };
+ static listGetter(target: any, prop: string | symbol, receiver: any): any {
+ if (ListImpl.listHandlers.hasOwnProperty(prop)) {
+ return ListImpl.listHandlers[prop];
+ }
+ return getter(target, prop, receiver);
+ }
constructor(fields?: T[]) {
super();
const list = new Proxy<this>(this, {
set: setter,
- get: listGetter,
- ownKeys: target => Object.keys(target.__fields),
+ get: ListImpl.listGetter,
+ ownKeys: target => Object.keys(target.__fieldTuples),
getOwnPropertyDescriptor: (target, prop) => {
- if (prop in target.__fields) {
+ if (prop in target[FieldTuples]) {
return {
configurable: true, //TODO Should configurable be true?
enumerable: true,
@@ -269,9 +251,9 @@ class ListImpl<T extends Field> extends ObjectField {
throw new Error("Currently properties can't be defined on documents using Object.defineProperty");
},
});
- this[SelfProxy] = list;
+ this[SelfProxy] = list as any as List<Field>; // bcz: ugh .. don't know how to convince typesecript that list is a List
if (fields) {
- (list as any).push(...fields);
+ this[SelfProxy].push(...fields);
}
return list;
}
@@ -280,8 +262,8 @@ class ListImpl<T extends Field> extends ObjectField {
// this requests all ProxyFields at the same time to avoid the overhead
// of separate network requests and separate updates to the React dom.
- private __realFields() {
- const unrequested = this.__fields.filter(f => f instanceof ProxyField && f.needsRequesting).map(f => f as ProxyField<RefField>);
+ @computed private get __realFields() {
+ const unrequested = this[FieldTuples].filter(f => f instanceof ProxyField && f.needsRequesting).map(f => f as ProxyField<RefField>);
// if we find any ProxyFields that don't have a current value, then
// start the server request for all of them
if (unrequested.length) {
@@ -293,45 +275,36 @@ class ListImpl<T extends Field> extends ObjectField {
// will await the batch request and return the requested field value.
unrequested.forEach(p => p.setExternalValuePromise(allSetPromise));
}
- return this.__fields.map(toRealField);
+ return this[FieldTuples].map(toRealField);
}
- public static FieldDataName = 'fields';
@serializable(alias(ListFieldName, list(autoObject(), { afterDeserialize: afterDocDeserialize })))
- private get __fields() {
- return this.___fields;
+ private get __fieldTuples() {
+ return this[FieldTuples];
}
- private set __fields(value) {
- this.___fields = value;
+ private set __fieldTuples(value) {
+ this[FieldTuples] = value;
for (const key in value) {
- const field = value[key];
- if (field instanceof ObjectField) {
- field[Parent] = this[Self];
- field[OnUpdate] = updateFunction(this[Self], key, field, this[SelfProxy]);
+ const item = value[key];
+ if (item instanceof ObjectField) {
+ item[Parent] = this[Self];
+ item[FieldChanged] = containedFieldChangedHandler(this[SelfProxy], Number(key), item);
}
}
}
[Copy]() {
- const copiedData = this[Self].__fields.map(f => (f instanceof ObjectField ? f[Copy]() : f));
+ const copiedData = this[Self].__fieldTuples.map(f => (f instanceof ObjectField ? f[Copy]() : f));
const deepCopy = new ListImpl<T>(copiedData as any);
return deepCopy;
}
// @serializable(alias("fields", list(autoObject())))
@observable
- private ___fields: StoredType<T>[] = [];
-
- private [Update] = (diff: any) => {
- // console.log(diff);
- const update = this[OnUpdate];
- // update && update(diff);
- update?.(diff);
- };
-
+ private [FieldTuples]: StoredType<T>[] = [];
private [Self] = this;
- private [SelfProxy]: any;
+ private [SelfProxy]: List<Field>; // also used in utils.ts even though it won't be found using find all references
[ToScriptString]() {
return `new List([${(this as any).map((field: any) => Field.toScriptString(field))}])`;
diff --git a/src/fields/ObjectField.ts b/src/fields/ObjectField.ts
index daa8a7777..b5bc2952a 100644
--- a/src/fields/ObjectField.ts
+++ b/src/fields/ObjectField.ts
@@ -1,9 +1,14 @@
-import { RefField } from "./RefField";
-import { OnUpdate, Parent, Copy, ToScriptString, ToString } from "./FieldSymbols";
-import { ScriptingGlobals } from "../client/util/ScriptingGlobals";
+import { RefField } from './RefField';
+import { FieldChanged, Parent, Copy, ToScriptString, ToString } from './FieldSymbols';
+import { ScriptingGlobals } from '../client/util/ScriptingGlobals';
+import { Field } from './Doc';
export abstract class ObjectField {
- public [OnUpdate]?: (diff?: any) => void;
+ // prettier-ignore
+ public [FieldChanged]?: (diff?: { op: '$addToSet' | '$remFromSet' | '$set';
+ items: Field[] | undefined;
+ length: number | undefined;
+ hint?: any }, serverOp?: any) => void;
public [Parent]?: RefField | ObjectField;
abstract [Copy](): ObjectField;
@@ -17,4 +22,4 @@ export namespace ObjectField {
}
}
-ScriptingGlobals.add(ObjectField); \ No newline at end of file
+ScriptingGlobals.add(ObjectField);
diff --git a/src/fields/PresField.ts b/src/fields/PresField.ts
deleted file mode 100644
index f236a04fd..000000000
--- a/src/fields/PresField.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-//insert code here
-import { ObjectField } from "./ObjectField";
-
-export abstract class PresField extends ObjectField {
-
-} \ No newline at end of file
diff --git a/src/fields/Proxy.ts b/src/fields/Proxy.ts
index 55d1d9ea4..c076f5fe1 100644
--- a/src/fields/Proxy.ts
+++ b/src/fields/Proxy.ts
@@ -1,5 +1,5 @@
import { Deserializable } from '../client/util/SerializationHelper';
-import { FieldWaiting, Opt } from './Doc';
+import { Field, FieldWaiting, Opt } from './Doc';
import { primitive, serializable } from 'serializr';
import { observable, action, runInAction, computed } from 'mobx';
import { DocServer } from '../client/DocServer';
@@ -39,7 +39,7 @@ export class ProxyField<T extends RefField> extends ObjectField {
}
[ToScriptString]() {
- return 'invalid';
+ return Field.toScriptString(this[ToValue](undefined)?.value); // not sure this is quite right since it doesn't recreate a proxy field, but better than 'invalid' ?
}
[ToString]() {
return 'ProxyField';
diff --git a/src/fields/Schema.ts b/src/fields/Schema.ts
index 7ad376a28..f035eeb0d 100644
--- a/src/fields/Schema.ts
+++ b/src/fields/Schema.ts
@@ -1,13 +1,13 @@
-import { Interface, ToInterface, Cast, ToConstructor, HasTail, Head, Tail, ListSpec, ToType, DefaultFieldConstructor } from "./Types";
-import { Doc, Field } from "./Doc";
-import { ObjectField } from "./ObjectField";
-import { RefField } from "./RefField";
-import { SelfProxy } from "./FieldSymbols";
-import { List } from "./List";
+import { Interface, ToInterface, Cast, ToConstructor, HasTail, Head, Tail, ListSpec, ToType, DefaultFieldConstructor } from './Types';
+import { Doc, Field } from './Doc';
+import { ObjectField } from './ObjectField';
+import { RefField } from './RefField';
+import { SelfProxy } from './DocSymbols';
+import { List } from './List';
type AllToInterface<T extends Interface[]> = {
- 1: ToInterface<Head<T>> & AllToInterface<Tail<T>>,
- 0: ToInterface<Head<T>>
+ 1: ToInterface<Head<T>> & AllToInterface<Tail<T>>;
+ 0: ToInterface<Head<T>>;
}[HasTail<T> extends true ? 1 : 0];
export const emptySchema = createSchema({});
@@ -28,36 +28,40 @@ export function makeInterface<T extends Interface[]>(...schemas: T): InterfaceFu
schema[key] = s[key];
}
}
- const proto = new Proxy({}, {
- get(target: any, prop, receiver) {
- const field = receiver.doc?.[prop];
- if (prop in schema) {
- const desc = prop === "proto" ? Doc : (schema as any)[prop]; // bcz: proto doesn't appear in schemas ... maybe it should?
- if (typeof desc === "object" && "defaultVal" in desc && "type" in desc) {//defaultSpec
- return Cast(field, desc.type, desc.defaultVal);
- }
- if (typeof desc === "function" && !ObjectField.isPrototypeOf(desc) && !RefField.isPrototypeOf(desc)) {
- const doc = Cast(field, Doc);
- if (doc === undefined) {
- return undefined;
+ const proto = new Proxy(
+ {},
+ {
+ get(target: any, prop, receiver) {
+ const field = receiver.doc?.[prop];
+ if (prop in schema) {
+ const desc = prop === 'proto' ? Doc : (schema as any)[prop]; // bcz: proto doesn't appear in schemas ... maybe it should?
+ if (typeof desc === 'object' && 'defaultVal' in desc && 'type' in desc) {
+ //defaultSpec
+ return Cast(field, desc.type, desc.defaultVal);
}
- if (doc instanceof Doc) {
- return desc(doc);
+ if (typeof desc === 'function' && !ObjectField.isPrototypeOf(desc) && !RefField.isPrototypeOf(desc)) {
+ const doc = Cast(field, Doc);
+ if (doc === undefined) {
+ return undefined;
+ }
+ if (doc instanceof Doc) {
+ return desc(doc);
+ }
+ return doc.then(doc => doc && desc(doc));
}
- return doc.then(doc => doc && desc(doc));
+ return Cast(field, desc);
}
- return Cast(field, desc);
- }
- return field;
- },
- set(target: any, prop, value, receiver) {
- receiver.doc && (receiver.doc[prop] = value); // receiver.doc may be undefined as the result of a change in acls
- return true;
+ return field;
+ },
+ set(target: any, prop, value, receiver) {
+ receiver.doc && (receiver.doc[prop] = value); // receiver.doc may be undefined as the result of a change in acls
+ return true;
+ },
}
- });
+ );
// !(doc instanceof Doc) && (throw new Error("Currently wrapping a schema in another schema isn't supported"));
const fn = (doc: Doc) => Object.create(proto, { doc: { value: doc[SelfProxy], writable: false } });
- return ((doc?: Doc | Doc[]) => (doc instanceof List ? doc : undefined)?.map?.(fn) ?? fn((doc as Doc) ?? new Doc)) as InterfaceFunc<T>;
+ return ((doc?: Doc | Doc[]) => (doc instanceof List ? doc : undefined)?.map?.(fn) ?? fn((doc as Doc) ?? new Doc())) as InterfaceFunc<T>;
}
export type makeStrictInterface<T extends Interface> = Partial<ToInterface<T>>;
@@ -75,8 +79,8 @@ export function makeStrictInterface<T extends Interface>(schema: T): (doc: Doc)
this.__doc[key] = value;
return;
}
- throw new TypeError("Expected type " + type);
- }
+ throw new TypeError('Expected type ' + type);
+ },
});
}
return function (doc: any) {
@@ -95,12 +99,12 @@ export function createSchema<T extends Interface>(schema: T): T & { proto: ToCon
}
export function listSpec<U extends ToConstructor<Field>>(type: U): ListSpec<ToType<U>> {
- return { List: type as any };//TODO Types
+ return { List: type as any }; //TODO Types
}
export function defaultSpec<T extends ToConstructor<Field>>(type: T, defaultVal: ToType<T>): DefaultFieldConstructor<ToType<T>> {
return {
type: type as any,
- defaultVal
+ defaultVal,
};
-} \ No newline at end of file
+}
diff --git a/src/fields/SchemaHeaderField.ts b/src/fields/SchemaHeaderField.ts
index 4b1855cb0..6dde2e5aa 100644
--- a/src/fields/SchemaHeaderField.ts
+++ b/src/fields/SchemaHeaderField.ts
@@ -1,7 +1,7 @@
import { Deserializable } from '../client/util/SerializationHelper';
import { serializable, primitive } from 'serializr';
import { ObjectField } from './ObjectField';
-import { Copy, ToScriptString, ToString, OnUpdate } from './FieldSymbols';
+import { Copy, ToScriptString, ToString, FieldChanged } from './FieldSymbols';
import { scriptingGlobal, ScriptingGlobals } from '../client/util/ScriptingGlobals';
import { ColumnType } from '../client/views/collections/collectionSchema/CollectionSchemaView';
@@ -82,32 +82,32 @@ export class SchemaHeaderField extends ObjectField {
setHeading(heading: string) {
this.heading = heading;
- this[OnUpdate]?.();
+ this[FieldChanged]?.();
}
setColor(color: string) {
this.color = color;
- this[OnUpdate]?.();
+ this[FieldChanged]?.();
}
setType(type: ColumnType) {
this.type = type;
- this[OnUpdate]?.();
+ this[FieldChanged]?.();
}
setWidth(width: number) {
this.width = width;
- this[OnUpdate]?.();
+ this[FieldChanged]?.();
}
setDesc(desc: boolean | undefined) {
this.desc = desc;
- this[OnUpdate]?.();
+ this[FieldChanged]?.();
}
setCollapsed(collapsed: boolean | undefined) {
this.collapsed = collapsed;
- this[OnUpdate]?.();
+ this[FieldChanged]?.();
}
[Copy]() {
diff --git a/src/fields/Types.ts b/src/fields/Types.ts
index 251b1149d..69dbe9756 100644
--- a/src/fields/Types.ts
+++ b/src/fields/Types.ts
@@ -1,11 +1,10 @@
-import { Field, Opt, FieldResult, Doc } from './Doc';
+import { DateField } from './DateField';
+import { Doc, Field, FieldResult, Opt } from './Doc';
import { List } from './List';
import { RefField } from './RefField';
-import { DateField } from './DateField';
-import { ScriptField } from './ScriptField';
-import { URLField, WebField, ImageField, CsvField } from './URLField';
-import { TextField } from '@material-ui/core';
import { RichTextField } from './RichTextField';
+import { ScriptField } from './ScriptField';
+import { CsvField, ImageField, WebField } from './URLField';
export type ToType<T extends InterfaceValue> = T extends 'string'
? string
diff --git a/src/fields/documentSchemas.ts b/src/fields/documentSchemas.ts
index 7b2fd74d0..76b287be7 100644
--- a/src/fields/documentSchemas.ts
+++ b/src/fields/documentSchemas.ts
@@ -9,7 +9,7 @@ export const documentSchema = createSchema({
type: 'string', // enumerated type of document -- should be template-specific (ie, start with an '_')
title: 'string', // document title (can be on either data document or layout)
isTemplateForField: 'string', // if specified, it indicates the document is a template that renders the specified field
- creationDate: DateField, // when the document was created
+ author_date: DateField, // when the document was created
links: listSpec(Doc), // computed (readonly) list of links associated with this document
// "Location" properties in a very general sense
@@ -49,14 +49,14 @@ export const documentSchema = createSchema({
_columnsHideIfEmpty: 'boolean', // whether empty stacking view column headings should be hidden
// _columnHeaders: listSpec(SchemaHeaderField), // header descriptions for stacking/masonry
// _schemaHeaders: listSpec(SchemaHeaderField), // header descriptions for schema views
- _fontSize: 'string',
- _fontFamily: 'string',
+ _text_fontSize: 'string',
+ _text_fontFamily: 'string',
_layout_sidebarWidthPercent: 'string', // percent of text window width taken up by sidebar
// appearance properties on the data document
backgroundColor: 'string', // background color of document
- borderRounding: 'string', // border radius rounding of document
- boxShadow: 'string', // the amount of shadow around the perimeter of a document
+ layout_borderRounding: 'string', // border radius rounding of document
+ layout_boxShadow: 'string', // the amount of shadow around the perimeter of a document
color: 'string', // foreground color of document
freeform_fitContentsToBox: 'boolean', // whether freeform view contents should be zoomed/panned to fill the area of the document view box
fontSize: 'string',
@@ -86,21 +86,21 @@ export const documentSchema = createSchema({
onDragStart: ScriptField, // script to run when document is dragged (without being selected). the script should return the Doc to be dropped.
followLinkLocation: 'string', // flag for where to place content when following a click interaction (e.g., add:right, lightbox, default, )
hideLinkButton: 'boolean', // whether the blue link counter button should be hidden
- hideAllLinks: 'boolean', // whether all individual blue anchor dots should be hidden
- layout_linkDisplay: 'boolean', // whether a link connection should be shown between link anchor endpoints.
+ layout_hideAllLinks: 'boolean', // whether all individual blue anchor dots should be hidden
+ link_displayLine: 'boolean', // whether a link connection should be shown between link anchor endpoints.
isLightbox: 'boolean', // whether the marked object will display addDocTab() calls that target "lightbox" destinations
layers: listSpec('string'), // which layers the document is part of
_lockedPosition: 'boolean', // whether the document can be moved (dragged)
_lockedTransform: 'boolean', // whether a freeformview can pan/zoom
- layout_linkDisplayArrow: 'boolean', // toggles directed arrows
+ link_displayArrow: 'boolean', // toggles directed arrows
// drag drop properties
- _stayInCollection: 'boolean', // whether document can be dropped into a different collection
+ _dragOnlyWithinContainer: 'boolean', // whether document can be dropped into a different collection
dragFactory: Doc, // the document that serves as the "template" for the onDragStart script. ie, to drag out copies of the dragFactory document.
- dropAction: 'string', // override specifying what should happen when this document is dropped (can be "embed", "copy", "move")
- targetDropAction: 'string', // allows the target of a drop event to specify the dropAction ("embed", "copy", "move") NOTE: if the document is dropped within the same collection, the dropAction is coerced to 'move'
- childDropAction: 'string', // specify the override for what should happen when the child of a collection is dragged from it and dropped (can be "embed" or "copy")
- removeDropProperties: listSpec('string'), // properties that should be removed from the embed/copy/etc of this document when it is dropped
+ dropAction: 'string', // override specifying what should happen when something is dropped on this document (can be "embed", "copy", "move")
+ dragAction: 'string', // override specifying what should happen when this document s dragged (can be "embed", "copy", "move")
+ childDragAction: 'string', // specify the override for what should happen when the child of a collection is dragged from it and dropped (can be "embed" or "copy")
+ dropPropertiesToRemove: listSpec('string'), // properties that should be removed from the embed/copy/etc of this document when it is dropped
});
export const collectionSchema = createSchema({
diff --git a/src/fields/util.ts b/src/fields/util.ts
index 2a6caaaa3..28db77c65 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -1,34 +1,13 @@
import { $mobx, action, observable, runInAction, trace } from 'mobx';
import { computedFn } from 'mobx-utils';
import { DocServer } from '../client/DocServer';
-import { CollectionViewType } from '../client/documents/DocumentTypes';
import { LinkManager } from '../client/util/LinkManager';
import { SerializationHelper } from '../client/util/SerializationHelper';
import { UndoManager } from '../client/util/UndoManager';
import { returnZero } from '../Utils';
-import CursorField from './CursorField';
-import {
- AclAdmin,
- AclEdit,
- aclLevel,
- AclPrivate,
- AclSelfEdit,
- AclSym,
- DataSym,
- Doc,
- DocListCast,
- DocListCastAsync,
- ForceServerWrite,
- HeightSym,
- HierarchyMapping,
- Initializing,
- LayoutSym,
- ReverseHierarchyMap,
- updateCachedAcls,
- UpdatingFromServer,
- WidthSym,
-} from './Doc';
-import { Id, OnUpdate, Parent, SelfProxy, ToValue, Update } from './FieldSymbols';
+import { aclLevel, Doc, DocListCast, Field, FieldResult, HierarchyMapping, ReverseHierarchyMap, StrListCast, updateCachedAcls } from './Doc';
+import { AclAdmin, AclAugment, AclEdit, AclPrivate, DocAcl, DocData, DocLayout, FieldKeys, ForceServerWrite, Height, Initializing, SelfProxy, UpdatingFromServer, Width } from './DocSymbols';
+import { FieldChanged, Id, Parent, ToValue } from './FieldSymbols';
import { List } from './List';
import { ObjectField } from './ObjectField';
import { PrefetchProxy, ProxyField } from './Proxy';
@@ -36,7 +15,7 @@ import { RefField } from './RefField';
import { RichTextField } from './RichTextField';
import { SchemaHeaderField } from './SchemaHeaderField';
import { ComputedField } from './ScriptField';
-import { ScriptCast, StrCast } from './Types';
+import { DocCast, ScriptCast, StrCast } from './Types';
function _readOnlySetter(): never {
throw new Error("Documents can't be modified in read-only mode");
@@ -53,10 +32,9 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number
return true;
}
- if (value !== undefined) {
- value = value[SelfProxy] || value;
- }
- const curValue = target.__fields[prop];
+ value = value?.[SelfProxy] ?? value; // convert any Doc type values to Proxy's
+
+ const curValue = target.__fieldTuples[prop];
if (curValue === value || (curValue instanceof ProxyField && value instanceof RefField && curValue.fieldId === value[Id])) {
// TODO This kind of checks correctly in the case that curValue is a ProxyField and value is a RefField, but technically
// curValue should get filled in with value if it isn't already filled in, in case we fetched the referenced field some other way
@@ -65,16 +43,17 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number
if (value instanceof RefField) {
value = new ProxyField(value);
}
+
if (value instanceof ObjectField) {
if (value[Parent] && value[Parent] !== receiver && !(value instanceof PrefetchProxy)) {
throw new Error("Can't put the same object in multiple documents at the same time");
}
value[Parent] = receiver;
- value[OnUpdate] = updateFunction(target, prop, value, receiver);
+ value[FieldChanged] = containedFieldChangedHandler(receiver, prop, value);
}
if (curValue instanceof ObjectField) {
delete curValue[Parent];
- delete curValue[OnUpdate];
+ delete curValue[FieldChanged];
}
const effectiveAcl = GetEffectiveAcl(target);
@@ -82,43 +61,54 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number
const writeMode = DocServer.getFieldWriteMode(prop as string);
const fromServer = target[UpdatingFromServer];
const sameAuthor = fromServer || receiver.author === Doc.CurrentUserEmail;
- const writeToDoc = sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAdmin || writeMode !== DocServer.WriteMode.LiveReadonly;
- const writeToServer = (sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAdmin || (effectiveAcl === AclSelfEdit && value instanceof RichTextField)) && !DocServer.Control.isReadOnly();
+ const writeToDoc =
+ sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAdmin || writeMode === DocServer.WriteMode.Playground || writeMode === DocServer.WriteMode.LivePlayground || (effectiveAcl === AclAugment && value instanceof RichTextField);
+ const writeToServer =
+ !DocServer.Control.isReadOnly() && //
+ (sameAuthor || effectiveAcl === AclEdit || effectiveAcl === AclAdmin || (effectiveAcl === AclAugment && value instanceof RichTextField));
if (writeToDoc) {
if (value === undefined) {
- target.__fieldKeys && delete target.__fieldKeys[prop];
- delete target.__fields[prop];
+ target[FieldKeys] && delete target[FieldKeys][prop]; // Lists don't have a FieldKeys field
+ delete target.__fieldTuples[prop];
} else {
- target.__fieldKeys && (target.__fieldKeys[prop] = true);
- target.__fields[prop] = value;
+ // bcz: uncomment to see if server is being updated
+ // console.log(prop + ' = ' + value + '(' + curValue + ')');
+ target[FieldKeys] && (target[FieldKeys][prop] = true); // Lists don't have a FieldKeys field
+ target.__fieldTuples[prop] = value;
}
if (writeToServer) {
- if (value === undefined) target[Update]({ $unset: { ['fields.' + prop]: '' } });
- else target[Update]({ $set: { ['fields.' + prop]: value instanceof ObjectField ? SerializationHelper.Serialize(value) : value === undefined ? null : value } });
+ // prettier-ignore
+ if (value === undefined)
+ (target as Doc|ObjectField)[FieldChanged]?.(undefined, { $unset: { ['fields.' + prop]: '' } });
+ else (target as Doc|ObjectField)[FieldChanged]?.(undefined, { $set: { ['fields.' + prop]: value instanceof ObjectField ? SerializationHelper.Serialize(value) :value}});
if (prop === 'author' || prop.toString().startsWith('acl')) updateCachedAcls(target);
} else {
DocServer.registerDocWithCachedUpdate(receiver, prop as string, curValue);
}
!receiver[Initializing] &&
+ !StrListCast(receiver.undoIgnoreFields).includes(prop.toString()) &&
(!receiver[UpdatingFromServer] || receiver[ForceServerWrite]) &&
- UndoManager.AddEvent({
- redo: () => (receiver[prop] = value),
- undo: () => {
- const wasUpdate = receiver[UpdatingFromServer];
- const wasForce = receiver[ForceServerWrite];
- receiver[ForceServerWrite] = true; // needed since writes aren't propagated to server if UpdatingFromServerIsSet
- receiver[UpdatingFromServer] = true; // needed if the event caused ACL's to change such that the doc is otherwise no longer editable.
- receiver[prop] = curValue;
- receiver[ForceServerWrite] = wasForce;
- receiver[UpdatingFromServer] = wasUpdate;
+ UndoManager.AddEvent(
+ {
+ redo: () => (receiver[prop] = value),
+ undo: () => {
+ const wasUpdate = receiver[UpdatingFromServer];
+ const wasForce = receiver[ForceServerWrite];
+ receiver[ForceServerWrite] = true; // needed since writes aren't propagated to server if UpdatingFromServerIsSet
+ receiver[UpdatingFromServer] = true; // needed if the event caused ACL's to change such that the doc is otherwise no longer editable.
+ receiver[prop] = curValue;
+ receiver[ForceServerWrite] = wasForce;
+ receiver[UpdatingFromServer] = wasUpdate;
+ },
+ prop: prop?.toString(),
},
- prop: prop?.toString(),
- });
+ value
+ );
return true;
}
- return false;
+ return true;
});
let _setter: (target: any, prop: string | symbol | number, value: any, receiver: any) => boolean = _setterImpl;
@@ -141,14 +131,18 @@ export function denormalizeEmail(email: string) {
/**
* Copies parent's acl fields to the child
*/
-export function inheritParentAcls(parent: Doc, child: Doc) {
- return;
- const dataDoc = parent[DataSym];
- for (const key of Object.keys(dataDoc)) {
- // if the default acl mode is private, then don't inherit the acl-Public permission, but set it to private.
- const permission = key === 'acl-Public' && Doc.defaultAclPrivate ? AclPrivate : dataDoc[key];
- key.startsWith('acl') && distributeAcls(key, permission, child);
- }
+export function inheritParentAcls(parent: Doc, child: Doc, layoutOnly: boolean) {
+ [...Object.keys(parent), ...(Doc.CurrentUserEmail !== parent.author ? ['acl-Owner'] : [])]
+ .filter(key => key.startsWith('acl'))
+ .forEach(key => {
+ // if the default acl mode is private, then don't inherit the acl-guest permission, but set it to private.
+ // const permission: string = key === 'acl-guest' && Doc.defaultAclPrivate ? AclPrivate : parent[key];
+ const parAcl = ReverseHierarchyMap.get(StrCast(key === 'acl-Owner' ? (Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Edit) : parent[key]))?.acl;
+ if (parAcl) {
+ const sharePermission = HierarchyMapping.get(parAcl)?.name;
+ sharePermission && distributeAcls(key === 'acl-Owner' ? `acl-${normalizeEmail(StrCast(parent.author))}` : key, sharePermission, child, undefined, false, layoutOnly);
+ }
+ });
}
/**
@@ -167,13 +161,11 @@ export function inheritParentAcls(parent: Doc, child: Doc) {
* Unset: Remove a sharing permission (eg., used )
*/
export enum SharingPermissions {
- Unset = 'None',
Admin = 'Admin',
Edit = 'Edit',
- SelfEdit = 'Self Edit',
Augment = 'Augment',
View = 'View',
- None = 'Not Shared',
+ None = 'Not-Shared',
}
// return acl from cache or cache the acl and return.
@@ -186,11 +178,11 @@ const getEffectiveAclCache = computedFn(function (target: any, user?: string) {
*/
export function GetEffectiveAcl(target: any, user?: string): symbol {
if (!target) return AclPrivate;
- if (target[UpdatingFromServer]) return AclAdmin;
+ if (target[UpdatingFromServer] || Doc.CurrentUserEmail === 'guest') return AclAdmin;
return getEffectiveAclCache(target, user); // all changes received from the server must be processed as Admin. return this directly so that the acls aren't cached (UpdatingFromServer is not observable)
}
-function getPropAcl(target: any, prop: string | symbol | number) {
+export function GetPropAcl(target: any, prop: string | symbol | number) {
if (typeof prop === 'symbol' || target[UpdatingFromServer]) return AclAdmin; // requesting the UpdatingFromServer prop or AclSym must always go through to keep the local DB consistent
if (prop && DocServer.IsPlaygroundField(prop.toString())) return AclEdit; // playground props are always editable
return GetEffectiveAcl(target);
@@ -207,7 +199,7 @@ export function SetCachedGroups(groups: string[]) {
runInAction(() => cachedGroups.push(...groups));
}
function getEffectiveAcl(target: any, user?: string): symbol {
- const targetAcls = target[AclSym];
+ const targetAcls = target[DocAcl];
if (targetAcls?.['acl-Me'] === AclAdmin || GetCachedGroupByName('Admin')) return AclAdmin;
const userChecked = user || Doc.CurrentUserEmail; // if the current user is the author of the document / the current user is a member of the admin group
@@ -217,105 +209,99 @@ function getEffectiveAcl(target: any, user?: string): symbol {
// there are issues with storing fields with . in the name, so they are replaced with _ during creation
// as a result we need to restore them again during this comparison.
const entity = denormalizeEmail(key.substring(4)); // an individual or a group
- if (HierarchyMapping.get(value as symbol)!.level > HierarchyMapping.get(effectiveAcl)!.level) {
- if (GetCachedGroupByName(entity) || userChecked === entity || entity === 'Me') {
+ if (GetCachedGroupByName(entity) || userChecked === entity || entity === 'Me') {
+ if (HierarchyMapping.get(value as symbol)!.level > HierarchyMapping.get(effectiveAcl)!.level) {
effectiveAcl = value as symbol;
}
}
}
- // if there's an overriding acl set through the properties panel or sharing menu, that's what's returned if the user isn't an admin of the document
- //const override = targetAcls['acl-Override'];
- // if (override !== AclUnset && override !== undefined) effectiveAcl = override;
-
- // if we're in playground mode, return AclEdit (or AclAdmin if that's the user's effectiveAcl)
return DocServer?.Control?.isReadOnly?.() && HierarchyMapping.get(effectiveAcl)!.level < aclLevel.editable ? AclEdit : effectiveAcl;
}
// authored documents are private until an ACL is set.
- const targetAuthor = target.__fields?.author || target.author; // target may be a Doc of Proxy, so check __fields.author and .author
+ const targetAuthor = target.__fieldTuples?.author || target.author; // target may be a Doc of Proxy, so check __fieldTuples.author and .author
if (targetAuthor && targetAuthor !== userChecked) return AclPrivate;
return AclAdmin;
}
+
/**
* Recursively distributes the access right for a user across the children of a document and its annotations.
* @param key the key storing the access right (e.g. acl-groupname)
* @param acl the access right being stored (e.g. "Can Edit")
* @param target the document on which this access right is being set
- * @param inheritingFromCollection whether the target is being assigned rights after being dragged into a collection (and so is inheriting the acls from the collection)
- * inheritingFromCollection is not currently being used but could be used if acl assignment defaults change
+ * @param visited list of Doc's already distributed to.
+ * @param allowUpgrade whether permissions can be made less restrictive
+ * @param layoutOnly just sets the layout doc's ACL (unless the data doc has no entry for the ACL, in which case it will be set as well)
*/
-export function distributeAcls(key: string, acl: SharingPermissions, target: Doc, inheritingFromCollection?: boolean, visited?: Doc[], isDashboard?: boolean) {
+export function distributeAcls(key: string, acl: SharingPermissions, target: Doc, visited?: Doc[], allowUpgrade?: boolean, layoutOnly = false) {
+ const selfKey = `acl-${Doc.CurrentUserEmailNormalized}`;
if (!visited) visited = [] as Doc[];
- if (!target || visited.includes(target)) return;
-
- if ((target._viewType === CollectionViewType.Docking && visited.length > 1) || Doc.GetProto(visited[0]) !== Doc.GetProto(target)) {
- target[key] = acl;
- if (target !== Doc.GetProto(target)) {
- //apparently we can't call updateCachedAcls twice (once for the main dashboard, and again for the nested dashboard...???)
- updateCachedAcls(target);
- }
- return;
- }
+ if (!target || visited.includes(target) || key === selfKey) return;
visited.push(target);
- let layoutDocChanged = false; // determines whether fetchProto should be called or not (i.e. is there a change that should be reflected in target[AclSym])
- // if it is inheriting from a collection, it only inherits if A) the key doesn't already exist or B) the right being inherited is more restrictive
- if (GetEffectiveAcl(target) === AclAdmin && (!inheritingFromCollection || !target[key] || ReverseHierarchyMap.get(StrCast(target[key]))!.level > ReverseHierarchyMap.get(acl)!.level)) {
- target[key] = acl;
- layoutDocChanged = true;
-
- if (isDashboard) {
- DocListCastAsync(target[Doc.LayoutFieldKey(target)]).then(docs => {
- docs?.forEach(d => distributeAcls(key, acl, d, inheritingFromCollection, visited));
- });
- }
- }
-
let dataDocChanged = false;
- const dataDoc = target[DataSym];
- if (dataDoc && (!inheritingFromCollection || !dataDoc[key] || ReverseHierarchyMap.get(StrCast(dataDoc[key]))! > ReverseHierarchyMap.get(acl)!)) {
- if (GetEffectiveAcl(dataDoc) === AclAdmin) {
- dataDoc[key] = acl;
- dataDocChanged = true;
- }
+ const dataDoc = target[DocData];
+ const curVal = ReverseHierarchyMap.get(StrCast(dataDoc[key]))?.level ?? 0;
+ const aclVal = ReverseHierarchyMap.get(acl)?.level ?? 0;
+ if (!layoutOnly && dataDoc && (allowUpgrade !== false || !dataDoc[key] || curVal > aclVal)) {
+ // propagate ACLs to links, children, and annotations
- // maps over the links of the document
- LinkManager.Links(dataDoc).forEach(link => distributeAcls(key, acl, link, inheritingFromCollection, visited));
+ LinkManager.Links(dataDoc).forEach(link => distributeAcls(key, acl, link, visited, allowUpgrade ? true : false));
- // maps over the children of the document
DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc)]).forEach(d => {
- distributeAcls(key, acl, d, inheritingFromCollection, visited);
- distributeAcls(key, acl, d[DataSym], inheritingFromCollection, visited);
+ distributeAcls(key, acl, d, visited, allowUpgrade ? true : false);
+ d !== d[DocData] && distributeAcls(key, acl, d[DocData], visited, allowUpgrade ? true : false);
});
- // maps over the annotations of the document
DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc) + '_annotations']).forEach(d => {
- distributeAcls(key, acl, d, inheritingFromCollection, visited);
- distributeAcls(key, acl, d[DataSym], inheritingFromCollection, visited);
+ distributeAcls(key, acl, d, visited, allowUpgrade ? true : false);
+ d !== d[DocData] && distributeAcls(key, acl, d[DocData], visited, allowUpgrade ? true : false);
});
+
+ Object.keys(target) // share expanded layout templates (eg, for presElementBox'es )
+ .filter(lkey => lkey.includes('layout[') && DocCast(target[lkey]))
+ .map(lkey => {
+ distributeAcls(key, acl, DocCast(target[lkey]), visited, allowUpgrade ? true : false);
+ });
+
+ if (GetEffectiveAcl(dataDoc) === AclAdmin) {
+ dataDoc[key] = acl;
+ dataDocChanged = true;
+ }
+ }
+
+ let layoutDocChanged = false; // determines whether fetchProto should be called or not (i.e. is there a change that should be reflected in target[AclSym])
+ // if it is inheriting from a collection, it only inherits if A) allowUpgrade is set B) the key doesn't already exist or c) the right being inherited is more restrictive
+ if (GetEffectiveAcl(target) === AclAdmin && (allowUpgrade || !Doc.GetT(target, key, 'boolean', true) || ReverseHierarchyMap.get(StrCast(target[key]))!.level > aclVal)) {
+ target[key] = acl;
+ layoutDocChanged = true;
+ if (dataDoc[key] === undefined) dataDoc[key] = acl;
}
layoutDocChanged && updateCachedAcls(target); // updates target[AclSym] when changes to acls have been made
dataDocChanged && updateCachedAcls(dataDoc);
}
+//
+// target should be either a Doc or ListImpl. receiver should be a Proxy<Doc> Or List.
+//
export function setter(target: any, in_prop: string | symbol | number, value: any, receiver: any): boolean {
let prop = in_prop;
- const effectiveAcl = in_prop === 'constructor' || typeof in_prop === 'symbol' ? AclAdmin : getPropAcl(target, prop);
- if (effectiveAcl !== AclEdit && effectiveAcl !== AclAdmin && !(effectiveAcl === AclSelfEdit && value instanceof RichTextField)) return true;
+ const effectiveAcl = in_prop === 'constructor' || typeof in_prop === 'symbol' ? AclAdmin : GetPropAcl(target, prop);
+ if (effectiveAcl !== AclEdit && effectiveAcl !== AclAugment && effectiveAcl !== AclAdmin) return true;
// if you're trying to change an acl but don't have Admin access / you're trying to change it to something that isn't an acceptable acl, you can't
if (typeof prop === 'string' && prop.startsWith('acl') && (effectiveAcl !== AclAdmin || ![...Object.values(SharingPermissions), undefined].includes(value))) return true;
- if (typeof prop === 'string' && prop !== '__id' && prop !== '__fields' && prop.startsWith('_')) {
+ if (typeof prop === 'string' && prop !== '__id' && prop !== '__fieldTuples' && prop.startsWith('_')) {
if (!prop.startsWith('__')) prop = prop.substring(1);
if (target.__LAYOUT__) {
target.__LAYOUT__[prop] = value;
return true;
}
}
- if (target.__fields[prop] instanceof ComputedField) {
- if (target.__fields[prop].setterscript && value !== undefined && !(value instanceof ComputedField)) {
- return ScriptCast(target.__fields[prop])?.setterscript?.run({ self: target[SelfProxy], this: target[SelfProxy], value }).success ? true : false;
+ if (target.__fieldTuples[prop] instanceof ComputedField) {
+ if (target.__fieldTuples[prop].setterscript && value !== undefined && !(value instanceof ComputedField)) {
+ return ScriptCast(target.__fieldTuples[prop])?.setterscript?.run({ self: target[SelfProxy], this: target[SelfProxy], value }).success ? true : false;
}
}
return _setter(target, prop, value, receiver);
@@ -325,14 +311,13 @@ export function getter(target: any, prop: string | symbol, proxy: any): any {
// prettier-ignore
switch (prop) {
case 'then' : return undefined;
- case '__fields' : case '__id':
- case 'constructor': case 'toString': case 'valueOf':
- case 'factory': case 'serializeInfo':
+ case 'constructor': case 'toString': case 'valueOf':
+ case 'serializeInfo': case 'factory':
return target[prop];
- case AclSym : return target[AclSym];
- case $mobx: return target.__fields[prop];
- case LayoutSym: return target.__LAYOUT__;
- case HeightSym: case WidthSym: if (GetEffectiveAcl(target) === AclPrivate) return returnZero;
+ case DocAcl : return target[DocAcl];
+ case $mobx: return target.__fieldTuples[prop];
+ case DocLayout: return target.__LAYOUT__;
+ case Height: case Width: if (GetEffectiveAcl(target) === AclPrivate) return returnZero;
default :
if (typeof prop === 'symbol') return target[prop];
if (prop.startsWith('isMobX')) return target[prop];
@@ -346,7 +331,7 @@ export function getter(target: any, prop: string | symbol, proxy: any): any {
}
function getFieldImpl(target: any, prop: string | number, proxy: any, ignoreProto: boolean = false): any {
- const field = target.__fields[prop];
+ const field = target.__fieldTuples[prop];
const value = field?.[ToValue]?.(proxy); // converts ComputedFields to values, or unpacks ProxyFields into Proxys
if (value) return value.value;
if (field === undefined && !ignoreProto && prop !== 'proto') {
@@ -370,82 +355,104 @@ export function deleteProperty(target: any, prop: string | number | symbol) {
return true;
}
-export function updateFunction(target: any, prop: any, value: any, receiver: any) {
- let lastValue = ObjectField.MakeCopy(value);
- return (diff?: any) => {
- const op =
- diff?.op === '$addToSet'
- ? { $addToSet: { ['fields.' + prop]: SerializationHelper.Serialize(new List<Doc>(diff.items)) } }
- : diff?.op === '$remFromSet'
- ? { $remFromSet: { ['fields.' + prop]: SerializationHelper.Serialize(new List<Doc>(diff.items)), hint: diff.hint } }
- : { $set: { ['fields.' + prop]: SerializationHelper.Serialize(value) } };
- !op.$set && ((op as any).length = diff.length);
- const prevValue = ObjectField.MakeCopy(lastValue as List<any>);
- lastValue = ObjectField.MakeCopy(value);
- const newValue = ObjectField.MakeCopy(value);
-
- if (!(value instanceof CursorField) && !value?.some?.((v: any) => v instanceof CursorField)) {
- !receiver[UpdatingFromServer] &&
+// this function creates a function that can be used to setup Undo for whenever an ObjectField changes.
+// the idea is that the Doc field setter can only setup undo at the granularity of an entire field and won't even be called if
+// just a part of a field (eg. field within an ObjectField) changes. This function returns a function that can be called
+// whenever an internal ObjectField field changes. It should be passed a 'diff' specification describing the change. Currently,
+// List's are the only true ObjectFields that can be partially modified (ignoring SchemaHeaderFields which should go away).
+// The 'diff' specification that a list can send is limited to indicating that something was added, removed, or that the list contents
+// were replaced. Based on this specification, an Undo event is setup that will save enough information about the ObjectField to be
+// able to undo and redo the partial change.
+//
+export function containedFieldChangedHandler(container: List<Field> | Doc, prop: string | number, liveContainedField: ObjectField) {
+ let lastValue: FieldResult = liveContainedField instanceof ObjectField ? ObjectField.MakeCopy(liveContainedField) : liveContainedField;
+ return (diff?: { op: '$addToSet' | '$remFromSet' | '$set'; items: Field[] | undefined; length: number | undefined; hint?: any }, dummyServerOp?: any) => {
+ const serializeItems = () => ({ __type: 'list', fields: diff?.items?.map((item: Field) => SerializationHelper.Serialize(item)) });
+ // prettier-ignore
+ const serverOp = diff?.op === '$addToSet'
+ ? { $addToSet: { ['fields.' + prop]: serializeItems() }, length: diff.length }
+ : diff?.op === '$remFromSet'
+ ? { $remFromSet: { ['fields.' + prop]: serializeItems(), hint: diff.hint}, length: diff.length }
+ : { $set: { ['fields.' + prop]: liveContainedField ? SerializationHelper.Serialize(liveContainedField) : undefined } };
+
+ if (!(container instanceof Doc) || !container[UpdatingFromServer]) {
+ const prevValue = ObjectField.MakeCopy(lastValue as List<any>);
+ lastValue = ObjectField.MakeCopy(liveContainedField);
+ const newValue = ObjectField.MakeCopy(liveContainedField);
+ if (diff?.op === '$addToSet') {
UndoManager.AddEvent(
- diff?.op === '$addToSet'
- ? {
- redo: () => {
- receiver[prop].push(...diff.items.map((item: any) => item.value ?? item));
- lastValue = ObjectField.MakeCopy(receiver[prop]);
- },
- undo: action(() => {
- // console.log("undo $add: " + prop, diff.items) // bcz: uncomment to log undo
- diff.items.forEach((item: any) => {
- if (item instanceof SchemaHeaderField) {
- const ind = receiver[prop].findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading);
- ind !== -1 && receiver[prop].splice(ind, 1);
- } else {
- const ind = receiver[prop].indexOf(item.value ?? item);
- ind !== -1 && receiver[prop].splice(ind, 1);
- }
- });
- lastValue = ObjectField.MakeCopy(receiver[prop]);
- }),
- prop: '',
- }
- : diff?.op === '$remFromSet'
- ? {
- redo: action(() => {
- diff.items.forEach((item: any) => {
- const ind = item instanceof SchemaHeaderField ? receiver[prop].findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading) : receiver[prop].indexOf(item.value ?? item);
- ind !== -1 && receiver[prop].splice(ind, 1);
- });
- lastValue = ObjectField.MakeCopy(receiver[prop]);
- }),
- undo: () => {
- // console.log("undo $rem: " + prop, diff.items) // bcz: uncomment to log undo
- diff.items.forEach((item: any) => {
- if (item instanceof SchemaHeaderField) {
- const ind = (prevValue as List<any>).findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading);
- ind !== -1 && receiver[prop].findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading) === -1 && receiver[prop].splice(ind, 0, item);
- } else {
- const ind = (prevValue as List<any>).indexOf(item.value ?? item);
- ind !== -1 && receiver[prop].indexOf(item.value ?? item) === -1 && receiver[prop].splice(ind, 0, item);
- }
- });
- lastValue = ObjectField.MakeCopy(receiver[prop]);
- },
- prop: '',
- }
- : {
- redo: () => {
- receiver[prop] = ObjectField.MakeCopy(newValue as List<any>);
- lastValue = ObjectField.MakeCopy(receiver[prop]);
- },
- undo: () => {
- // console.log("undo list: " + prop, receiver[prop]) // bcz: uncomment to log undo
- receiver[prop] = ObjectField.MakeCopy(prevValue as List<any>);
- lastValue = ObjectField.MakeCopy(receiver[prop]);
- },
- prop: '',
- }
+ {
+ redo: () => {
+ //console.log('redo $add: ' + prop, diff.items); // bcz: uncomment to log undo
+ (container as any)[prop as any]?.push(...(diff.items || [])?.map((item: any) => item.value ?? item));
+ lastValue = ObjectField.MakeCopy((container as any)[prop as any]);
+ },
+ undo: action(() => {
+ // console.log('undo $add: ' + prop, diff.items); // bcz: uncomment to log undo
+ diff.items?.forEach((item: any) => {
+ const ind =
+ item instanceof SchemaHeaderField //
+ ? (container as any)[prop as any]?.findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading)
+ : (container as any)[prop as any]?.indexOf(item.value ?? item);
+ ind !== undefined && ind !== -1 && (container as any)[prop as any]?.splice(ind, 1);
+ });
+ lastValue = ObjectField.MakeCopy((container as any)[prop as any]);
+ }),
+ prop: 'add ' + diff.items?.length + ' items to list',
+ },
+ diff?.items
);
+ } else if (diff?.op === '$remFromSet') {
+ UndoManager.AddEvent(
+ {
+ redo: action(() => {
+ // console.log('redo $rem: ' + prop, diff.items); // bcz: uncomment to log undo
+ diff.items?.forEach((item: any) => {
+ const ind =
+ item instanceof SchemaHeaderField //
+ ? (container as any)[prop as any]?.findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading)
+ : (container as any)[prop as any]?.indexOf(item.value ?? item);
+ ind !== undefined && ind !== -1 && (container as any)[prop as any]?.splice(ind, 1);
+ });
+ lastValue = ObjectField.MakeCopy((container as any)[prop as any]);
+ }),
+ undo: () => {
+ // console.log('undo $rem: ' + prop, diff.items); // bcz: uncomment to log undo
+ diff.items?.forEach((item: any) => {
+ if (item instanceof SchemaHeaderField) {
+ const ind = (prevValue as List<any>).findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading);
+ ind !== -1 && (container as any)[prop as any].findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading) === -1 && (container as any)[prop as any].splice(ind, 0, item);
+ } else {
+ const ind = (prevValue as List<any>).indexOf(item.value ?? item);
+ ind !== -1 && (container as any)[prop as any].indexOf(item.value ?? item) === -1 && (container as any)[prop as any].splice(ind, 0, item);
+ }
+ });
+ lastValue = ObjectField.MakeCopy((container as any)[prop as any]);
+ },
+ prop: 'remove ' + diff.items?.length + ' items from list',
+ },
+ diff?.items
+ );
+ } else {
+ const setFieldVal = (val: Field | undefined) => (container instanceof Doc ? (container[prop as string] = val) : (container[prop as number] = val as Field));
+ UndoManager.AddEvent(
+ {
+ redo: () => {
+ // console.log('redo list: ' + prop, fieldVal()); // bcz: uncomment to log undo
+ setFieldVal(newValue instanceof ObjectField ? ObjectField.MakeCopy(newValue) : undefined);
+ lastValue = ObjectField.MakeCopy((container as any)[prop as any]);
+ },
+ undo: () => {
+ // console.log('undo list: ' + prop, fieldVal()); // bcz: uncomment to log undo
+ setFieldVal(prevValue instanceof ObjectField ? ObjectField.MakeCopy(prevValue) : undefined);
+ lastValue = ObjectField.MakeCopy((container as any)[prop as any]);
+ },
+ prop: 'set list field',
+ },
+ diff?.items
+ );
+ }
}
- target[Update](op);
+ container[FieldChanged]?.(undefined, serverOp);
};
}
diff --git a/src/mobile/AudioUpload.tsx b/src/mobile/AudioUpload.tsx
index c3423e2a5..7a1dde9fb 100644
--- a/src/mobile/AudioUpload.tsx
+++ b/src/mobile/AudioUpload.tsx
@@ -25,8 +25,8 @@ export class AudioUpload extends React.Component {
title: 'mobile audio',
_width: 300,
_height: 300,
- _layoutFitContentsToBox: true,
- boxShadow: '0 0',
+ _layout_fitContentsToBox: true,
+ layout_boxShadow: '0 0',
}),
Doc
)
@@ -54,7 +54,7 @@ export class AudioUpload extends React.Component {
Doc
) as Doc,
],
- { title: 'mobile audio', _width: 300, _height: 300, _layoutFitContentsToBox: true, boxShadow: '0 0' }
+ { title: 'mobile audio', _width: 300, _height: 300, _layout_fitContentsToBox: true, layout_boxShadow: '0 0' }
),
Doc
)
@@ -110,8 +110,8 @@ export class AudioUpload extends React.Component {
pinToPres={emptyFunction}
rootSelected={returnTrue}
removeDocument={undefined}
- docFilters={returnEmptyFilter}
- docRangeFilters={returnEmptyFilter}
+ childFilters={returnEmptyFilter}
+ childFiltersByRanges={returnEmptyFilter}
searchFilterDocs={returnEmptyDoclist}
ScreenToLocalTransform={Transform.Identity}
PanelWidth={() => 600}
diff --git a/src/mobile/ImageUpload.tsx b/src/mobile/ImageUpload.tsx
index f910d765e..da38fcaee 100644
--- a/src/mobile/ImageUpload.tsx
+++ b/src/mobile/ImageUpload.tsx
@@ -42,7 +42,7 @@ export class Uploader extends React.Component<ImageUploadProps> {
this.process = "Uploading Files";
for (let index = 0; index < files.length; ++index) {
const file = files[index];
- const res = await Networking.UploadFilesToServer(file);
+ const res = await Networking.UploadFilesToServer({file});
this.setOpacity(3, "1"); // Slab 3
// For each item that the user has selected
res.map(async ({ result }) => {
diff --git a/src/mobile/MobileInterface.tsx b/src/mobile/MobileInterface.tsx
index a543c2f70..584a7b432 100644
--- a/src/mobile/MobileInterface.tsx
+++ b/src/mobile/MobileInterface.tsx
@@ -260,7 +260,7 @@ export class MobileInterface extends React.Component {
@action
componentDidMount = () => {
// if the home menu is in list view -> adjust the menu toggle appropriately
- this._menuListView = this._homeDoc._viewType === 'stacking' ? true : false;
+ this._menuListView = this._homeDoc._type_collection === 'stacking' ? true : false;
Doc.ActiveTool = InkTool.None; // ink should intially be set to none
Doc.UserDoc().activeMobile = this._homeDoc; // active mobile set to home
AudioBox.Enabled = true;
@@ -402,8 +402,8 @@ export class MobileInterface extends React.Component {
docViewPath={returnEmptyDoclist}
whenChildContentsActiveChanged={emptyFunction}
bringToFront={emptyFunction}
- docFilters={returnEmptyFilter}
- docRangeFilters={returnEmptyFilter}
+ childFilters={returnEmptyFilter}
+ childFiltersByRanges={returnEmptyFilter}
searchFilterDocs={returnEmptyDoclist}
/>
</div>
@@ -614,7 +614,7 @@ export class MobileInterface extends React.Component {
// The static ink menu that appears at the top
@computed get inkMenu() {
- return this._activeDoc._viewType !== CollectionViewType.Docking || !this._ink ? null : <div className="colorSelector">{/* <CollectionFreeFormViewChrome /> */}</div>;
+ return this._activeDoc._type_collection !== CollectionViewType.Docking || !this._ink ? null : <div className="colorSelector">{/* <CollectionFreeFormViewChrome /> */}</div>;
}
// DocButton that uses UndoManager and handles the opacity change if CanUndo is true
@@ -657,7 +657,7 @@ export class MobileInterface extends React.Component {
// DocButton for switching into ink mode
@computed get drawInk() {
- return !this.mainContainer || this._activeDoc._viewType !== CollectionViewType.Docking ? null : (
+ return !this.mainContainer || this._activeDoc._type_collection !== CollectionViewType.Docking ? null : (
<div className="docButton" id="inkButton" title={Doc.isDocPinned(this._activeDoc) ? 'Pen on' : 'Pen off'} onClick={this.onSwitchInking}>
<FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="pen-nib" />
</div>
@@ -666,7 +666,7 @@ export class MobileInterface extends React.Component {
// DocButton: Button that appears on the bottom of the screen to initiate image upload
@computed get uploadImageButton() {
- if (this._activeDoc.type === DocumentType.COL && this._activeDoc !== this._homeDoc && this._activeDoc._viewType !== CollectionViewType.Docking && this._activeDoc.title !== 'WORKSPACES') {
+ if (this._activeDoc.type === DocumentType.COL && this._activeDoc !== this._homeDoc && this._activeDoc._type_collection !== CollectionViewType.Docking && this._activeDoc.title !== 'WORKSPACES') {
return (
<div className="docButton" id="imageButton" title={Doc.isDocPinned(this._activeDoc) ? 'Pen on' : 'Pen off'} onClick={this.toggleUpload}>
<FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="upload" />
@@ -722,9 +722,9 @@ export class MobileInterface extends React.Component {
// Logic for switching the menu into the icons
@action
changeToIconView = () => {
- if ((this._homeDoc._viewType = 'stacking')) {
+ if ((this._homeDoc._type_collection = 'stacking')) {
this._menuListView = false;
- this._homeDoc._viewType = 'masonry';
+ this._homeDoc._type_collection = 'masonry';
this._homeDoc.columnWidth = 300;
this._homeDoc._columnWidth = 300;
const menuButtons = DocListCast(this._homeDoc.data);
@@ -742,8 +742,8 @@ export class MobileInterface extends React.Component {
// Logic for switching the menu into the stacking view
@action
changeToListView = () => {
- if ((this._homeDoc._viewType = 'masonry')) {
- this._homeDoc._viewType = 'stacking';
+ if ((this._homeDoc._type_collection = 'masonry')) {
+ this._homeDoc._type_collection = 'stacking';
this._menuListView = true;
const menuButtons = DocListCast(this._homeDoc.data);
menuButtons.map(doc => {
@@ -800,7 +800,7 @@ export class MobileInterface extends React.Component {
// Radial menu can only be used if it is a colleciton and it is not a homeDoc
// (and cannot be used on Dashboard to avoid pin to presentation opening on right)
@computed get displayRadialMenu() {
- return this._activeDoc.type === 'collection' && this._activeDoc !== this._homeDoc && this._activeDoc._viewType !== CollectionViewType.Docking ? <RadialMenu /> : null;
+ return this._activeDoc.type === 'collection' && this._activeDoc !== this._homeDoc && this._activeDoc._type_collection !== CollectionViewType.Docking ? <RadialMenu /> : null;
}
onDragOver = (e: React.DragEvent) => {
diff --git a/src/mobile/MobileMain.tsx b/src/mobile/MobileMain.tsx
index 6cbf86f77..dc3a73def 100644
--- a/src/mobile/MobileMain.tsx
+++ b/src/mobile/MobileMain.tsx
@@ -12,7 +12,7 @@ AssignAllExtensions();
const info = await CurrentUserUtils.loadCurrentUser();
DocServer.init(window.location.protocol, window.location.hostname, 4321, info.email + ' (mobile)');
await Docs.Prototypes.initialize();
- await CurrentUserUtils.loadUserDocument(info.id);
+ await CurrentUserUtils.loadUserDocument(info);
document.getElementById('root')!.addEventListener(
'wheel',
event => {
diff --git a/src/scraping/buxton/narratives/Theme - Chord Kbds.docx b/src/scraping/buxton/narratives/Theme - Chord Kbds.docx
deleted file mode 100644
index 439a7d975..000000000
--- a/src/scraping/buxton/narratives/Theme - Chord Kbds.docx
+++ /dev/null
Binary files differ
diff --git a/src/server/ApiManagers/AzureManager.ts b/src/server/ApiManagers/AzureManager.ts
new file mode 100644
index 000000000..12bb98ad0
--- /dev/null
+++ b/src/server/ApiManagers/AzureManager.ts
@@ -0,0 +1,67 @@
+import { ContainerClient, BlobServiceClient } from "@azure/storage-blob";
+import * as fs from "fs";
+import { Readable, Stream } from "stream";
+const AZURE_STORAGE_CONNECTION_STRING = process.env.AZURE_STORAGE_CONNECTION_STRING;
+
+export class AzureManager {
+ private _containerClient: ContainerClient;
+ private _blobServiceClient: BlobServiceClient;
+ private static _instance: AzureManager | undefined;
+
+ public static CONTAINER_NAME = "dashmedia";
+ public static STORAGE_ACCOUNT_NAME = "dashblobstore";
+
+ constructor() {
+ if (!AZURE_STORAGE_CONNECTION_STRING) {
+ throw new Error("Azure Storage Connection String Not Found");
+ }
+ this._blobServiceClient = BlobServiceClient.fromConnectionString(AZURE_STORAGE_CONNECTION_STRING);
+ this._containerClient = this.BlobServiceClient.getContainerClient(AzureManager.CONTAINER_NAME);
+ }
+
+ public static get Instance() {
+ return this._instance = this._instance ?? new AzureManager();
+ }
+
+ public get BlobServiceClient() {
+ return this._blobServiceClient;
+ }
+
+ public get ContainerClient() {
+ return this._containerClient;
+ }
+
+ public static UploadBlob(filename: string, filepath: string, filetype: string) {
+ const blockBlobClient = this.Instance.ContainerClient.getBlockBlobClient(filename);
+ const blobOptions = { blobHTTPHeaders: { blobContentType: filetype }};
+ const stream = fs.createReadStream(filepath);
+ return blockBlobClient.uploadStream(stream, undefined, undefined, blobOptions);
+ }
+
+ public static UploadBlobStream(stream: Readable, filename: string, filetype: string) {
+ const blockBlobClient = this.Instance.ContainerClient.getBlockBlobClient(filename);
+ const blobOptions = { blobHTTPHeaders: { blobContentType: filetype }};
+ return blockBlobClient.uploadStream(stream, undefined, undefined, blobOptions);
+ }
+
+ public static DeleteBlob(filename: string) {
+ const blockBlobClient = this.Instance.ContainerClient.getBlockBlobClient(filename);
+ return blockBlobClient.deleteIfExists();
+ }
+
+ public static async GetBlobs() {
+ const foundBlobs = [];
+ for await (const blob of this.Instance.ContainerClient.listBlobsFlat()) {
+ console.log(`${blob.name}`);
+
+ const blobItem = {
+ url : `https://${AzureManager.STORAGE_ACCOUNT_NAME}.blob.core.windows.net/${AzureManager.CONTAINER_NAME}/${blob.name}`,
+ name : blob.name
+ }
+
+ foundBlobs.push(blobItem);
+ }
+
+ return foundBlobs;
+ }
+}
diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts
index 74c06b4a6..ebc9deab7 100644
--- a/src/server/ApiManagers/UploadManager.ts
+++ b/src/server/ApiManagers/UploadManager.ts
@@ -12,6 +12,7 @@ import { AcceptableMedia, Upload } from '../SharedMediaTypes';
import ApiManager, { Registration } from './ApiManager';
import { SolrManager } from './SearchManager';
import v4 = require('uuid/v4');
+import { DashVersion } from '../../fields/DocSymbols';
const AdmZip = require('adm-zip');
const imageDataUri = require('image-data-uri');
const fs = require('fs');
@@ -43,6 +44,14 @@ export default class UploadManager extends ApiManager {
protected initialize(register: Registration): void {
register({
method: Method.POST,
+ subscription: '/ping',
+ secureHandler: async ({ req, res }) => {
+ _success(res, { message: DashVersion, date: new Date() });
+ },
+ });
+
+ register({
+ method: Method.POST,
subscription: '/concatVideos',
secureHandler: async ({ req, res }) => {
// req.body contains the array of server paths to the videos
@@ -75,7 +84,7 @@ export default class UploadManager extends ApiManager {
for (const key in files) {
const f = files[key];
if (!Array.isArray(f)) {
- const result = await DashUploadUtils.upload(f);
+ const result = await DashUploadUtils.upload(f, key); // key is the guid used by the client to track upload progress.
result && !(result.result instanceof Error) && results.push(result);
}
}
diff --git a/src/server/ApiManagers/UserManager.ts b/src/server/ApiManagers/UserManager.ts
index c3dadd821..8b7994eac 100644
--- a/src/server/ApiManagers/UserManager.ts
+++ b/src/server/ApiManagers/UserManager.ts
@@ -5,7 +5,8 @@ import { msToTime } from '../ActionUtilities';
import * as bcrypt from 'bcrypt-nodejs';
import { Opt } from '../../fields/Doc';
import { WebSocket } from '../websocket';
-import { DashStats } from '../DashStats';
+import { resolvedPorts } from '../server_Initialization';
+import { DashVersion } from '../../fields/DocSymbols';
export const timeMap: { [id: string]: number } = {};
interface ActivityUnit {
@@ -68,7 +69,18 @@ export default class UserManager extends ApiManager {
register({
method: Method.GET,
subscription: '/getCurrentUser',
- secureHandler: ({ res, user: { _id, email, cacheDocumentIds } }) => res.send(JSON.stringify({ id: _id, email, cacheDocumentIds })),
+ secureHandler: ({ res, user }) =>
+ res.send(
+ JSON.stringify({
+ version: DashVersion,
+ userDocumentId: user.userDocumentId,
+ linkDatabaseId: user.linkDatabaseId,
+ sharingDocumentId: user.sharingDocumentId,
+ email: user.email,
+ cacheDocumentIds: user.cacheDocumentIds,
+ resolvedPorts,
+ })
+ ),
publicHandler: ({ res }) => res.send(JSON.stringify({ id: '__guest__', email: 'guest' })),
});
diff --git a/src/server/DashSession/Session/utilities/session_config.ts b/src/server/DashSession/Session/utilities/session_config.ts
index bde98e9d2..266759929 100644
--- a/src/server/DashSession/Session/utilities/session_config.ts
+++ b/src/server/DashSession/Session/utilities/session_config.ts
@@ -120,7 +120,7 @@ export const defaultConfig: Configuration = {
color: "green"
}
},
- ports: { server: 3000 },
+ ports: { server: 1050 },
polling: {
route: "/",
intervalSeconds: 30,
diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts
index 11523a9d8..337bb812f 100644
--- a/src/server/DashUploadUtils.ts
+++ b/src/server/DashUploadUtils.ts
@@ -6,7 +6,7 @@ import { createReadStream, createWriteStream, existsSync, readFileSync, rename,
import * as path from 'path';
import { basename } from 'path';
import * as sharp from 'sharp';
-import { Stream } from 'stream';
+import { Readable, Stream } from 'stream';
import { filesDirectory, publicDirectory } from '.';
import { Opt } from '../fields/Doc';
import { ParsedPDF } from '../server/PdfTypes';
@@ -17,6 +17,8 @@ import { resolvedServerUrl } from './server_Initialization';
import { AcceptableMedia, Upload } from './SharedMediaTypes';
import request = require('request-promise');
import formidable = require('formidable');
+import { AzureManager } from './ApiManagers/AzureManager';
+import axios from 'axios';
const spawn = require('child_process').spawn;
const { exec } = require('child_process');
const parse = require('pdf-parse');
@@ -42,6 +44,10 @@ function isLocal() {
return /Dash-Web[0-9]*[\\\/]src[\\\/]server[\\\/]public[\\\/](.*)/;
}
+function usingAzure() {
+ return process.env.USE_AZURE === 'true';
+}
+
export namespace DashUploadUtils {
export interface Size {
width: number;
@@ -61,6 +67,9 @@ export namespace DashUploadUtils {
const size = 'content-length';
const type = 'content-type';
+ const BLOBSTORE_URL = process.env.BLOBSTORE_URL;
+ const RESIZE_FUNCTION_URL = process.env.RESIZE_FUNCTION_URL;
+
const { imageFormats, videoFormats, applicationFormats, audioFormats } = AcceptableMedia; //TODO:glr
export async function concatVideos(filePaths: string[]): Promise<Upload.AccessPathInfo> {
@@ -134,7 +143,6 @@ export namespace DashUploadUtils {
export function uploadYoutube(videoId: string): Promise<Upload.FileResponse> {
return new Promise<Upload.FileResponse<Upload.FileInformation>>((res, rej) => {
- console.log('Uploading YouTube video: ' + videoId);
const name = videoId;
const path = name.replace(/^-/, '__') + '.mp4';
const finalPath = serverPathToFile(Directory.videos, path);
@@ -181,9 +189,11 @@ export namespace DashUploadUtils {
});
}
- export async function upload(file: File): Promise<Upload.FileResponse> {
+ export async function upload(file: File, overwriteGuid?: string): Promise<Upload.FileResponse> {
+ const isAzureOn = usingAzure();
const { type, path, name } = file;
const types = type?.split('/') ?? [];
+ uploadProgress.set(overwriteGuid ?? name, 'uploading'); // If the client sent a guid it uses to track upload progress, use that guid. Otherwise, use the file's name.
const category = types[0];
let format = `.${types[1]}`;
@@ -477,17 +487,51 @@ export namespace DashUploadUtils {
};
}
+ /**
+ * UploadInspectedImage() takes an image with its metadata. If Azure is being used, this method will call the Azure function
+ * to execute the resizing. If Azure is not used, the function will begin to resize the image.
+ *
+ * @param metadata metadata object from InspectImage()
+ * @param filename the name of the file
+ * @param prefix the prefix to use, which will be set to '' if none is provided.
+ * @param cleanUp a boolean indicating if the files should be deleted after upload. True by default.
+ * @returns the accessPaths for the resized files.
+ */
export const UploadInspectedImage = async (metadata: Upload.InspectionResults, filename?: string, prefix = '', cleanUp = true): Promise<Upload.ImageInformation> => {
const { requestable, source, ...remaining } = metadata;
const resolved = filename || `${prefix}upload_${Utils.GenerateGuid()}.${remaining.contentType.split('/')[1].toLowerCase()}`;
const { images } = Directory;
const information: Upload.ImageInformation = {
accessPaths: {
- agnostic: getAccessPaths(images, resolved),
+ agnostic: usingAzure()
+ ? {
+ client: BLOBSTORE_URL + `/${resolved}`,
+ server: BLOBSTORE_URL + `/${resolved}`,
+ }
+ : getAccessPaths(images, resolved),
},
...metadata,
};
- const writtenFiles = await outputResizedImages(() => request(requestable), resolved, pathToDirectory(Directory.images));
+ let writtenFiles: { [suffix: string]: string };
+
+ if (usingAzure()) {
+ if (!RESIZE_FUNCTION_URL) {
+ throw new Error('Resize function URL not provided.');
+ }
+
+ try {
+ const response = await axios.post(RESIZE_FUNCTION_URL, {
+ url: requestable,
+ filename: resolved,
+ });
+ writtenFiles = response.data.writtenFiles;
+ } catch (err) {
+ console.error(err);
+ writtenFiles = {};
+ }
+ } else {
+ writtenFiles = await outputResizedImages(() => request(requestable), resolved, pathToDirectory(Directory.images));
+ }
for (const suffix of Object.keys(writtenFiles)) {
information.accessPaths[suffix] = getAccessPaths(images, writtenFiles[suffix]);
}
@@ -532,6 +576,15 @@ export namespace DashUploadUtils {
force: true,
};
+ /**
+ * outputResizedImages takes in a readable stream and resizes the images according to the sizes defined at the top of this file.
+ *
+ * The new images will be saved to the server with the corresponding prefixes.
+ * @param streamProvider a Stream of the image to process, taken from the /parsed_files location
+ * @param outputFileName the basename (No suffix) of the outputted file.
+ * @param outputDirectory the directory to output to, usually Directory.Images
+ * @returns a map with suffixes as keys and resized filenames as values.
+ */
export async function outputResizedImages(streamProvider: () => Stream | Promise<Stream>, outputFileName: string, outputDirectory: string) {
const writtenFiles: { [suffix: string]: string } = {};
for (const { resizer, suffix } of resizers(path.extname(outputFileName))) {
@@ -548,11 +601,16 @@ export namespace DashUploadUtils {
return writtenFiles;
}
+ /**
+ * define the resizers to use
+ * @param ext the extension
+ * @returns an array of resizer functions from sharp
+ */
function resizers(ext: string): DashUploadUtils.ImageResizer[] {
return [
{ suffix: SizeSuffix.Original },
...Object.values(DashUploadUtils.Sizes).map(({ suffix, width }) => {
- let initial: sharp.Sharp | undefined = sharp().resize(width, undefined, { withoutEnlargement: true });
+ let initial: sharp.Sharp | undefined = sharp({ failOnError: false }).resize(width, undefined, { withoutEnlargement: true });
if (pngs.includes(ext)) {
initial = initial.png(pngOptions);
} else if (jpgs.includes(ext)) {
diff --git a/src/server/server_Initialization.ts b/src/server/server_Initialization.ts
index 805da1d43..354f809e0 100644
--- a/src/server/server_Initialization.ts
+++ b/src/server/server_Initialization.ts
@@ -100,7 +100,7 @@ function buildWithMiddleware(server: express.Express) {
passport.session(),
(req: express.Request, res: express.Response, next: express.NextFunction) => {
res.locals.user = req.user;
- if (req.originalUrl.endsWith('.png') /*|| req.originalUrl.endsWith(".js")*/ && req.method === 'GET' && (res as any)._contentLength) {
+ if ((req.originalUrl.endsWith('.png') || req.originalUrl.endsWith('.jpg') || (process.env.RELEASE === 'true' && req.originalUrl.endsWith('.js'))) && req.method === 'GET') {
const period = 30000;
res.set('Cache-control', `public, max-age=${period}`);
} else {
@@ -149,23 +149,15 @@ function registerAuthenticationRoutes(server: express.Express) {
function registerCorsProxy(server: express.Express) {
server.use('/corsProxy', async (req, res) => {
- const referer = req.headers.referer ? decodeURIComponent(req.headers.referer) : '';
- let requrlraw = decodeURIComponent(req.url.substring(1));
- const qsplit = requrlraw.split('?q=');
- const newqsplit = requrlraw.split('&q=');
+ //const referer = req.headers.referer ? decodeURIComponent(req.headers.referer) : '';
+ let requrl = decodeURIComponent(req.url.substring(1));
+ const qsplit = requrl.split('?q=');
+ const newqsplit = requrl.split('&q=');
if (qsplit.length > 1 && newqsplit.length > 1) {
const lastq = newqsplit[newqsplit.length - 1];
- requrlraw = qsplit[0] + '?q=' + lastq.split('&')[0] + '&' + qsplit[1].split('&')[1];
- }
- const requrl = requrlraw.startsWith('/') ? referer + requrlraw : requrlraw;
- // cors weirdness here...
- // if the referer is a cors page and the cors() route (I think) redirected to /corsProxy/<path> and the requested url path was relative,
- // then we redirect again to the cors referer and just add the relative path.
- if (!requrl.startsWith('http') && req.originalUrl.startsWith('/corsProxy') && referer?.includes('corsProxy')) {
- res.redirect(referer + (referer.endsWith('/') ? '' : '/') + requrl);
- } else {
- proxyServe(req, requrl, res);
+ requrl = qsplit[0] + '?q=' + lastq.split('&')[0] + '&' + qsplit[1].split('&')[1];
}
+ proxyServe(req, requrl, res);
});
}
@@ -184,7 +176,9 @@ function proxyServe(req: any, requrl: string, response: any) {
const htmlText = htmlInputText
.toString('utf8')
.replace('<head>', '<head> <style>[id ^= "google"] { display: none; } </style>')
- .replace(/href="https?([^"]*)"/g, httpsToCors)
+ .replace('<script', '<noscript')
+ .replace('</script', '</noscript')
+ // .replace(/href="https?([^"]*)"/g, httpsToCors)
.replace(/data-srcset="[^"]*"/g, '')
.replace(/srcset="[^"]*"/g, '')
.replace(/target="_blank"/g, '');
@@ -234,8 +228,10 @@ function proxyServe(req: any, requrl: string, response: any) {
function registerEmbeddedBrowseRelativePathHandler(server: express.Express) {
server.use('*', (req, res) => {
const relativeUrl = req.originalUrl;
- if (!req.user) res.redirect('/home'); // When no user is logged in, we interpret a relative URL as being a reference to something they don't have access to and redirect to /home
- else if (!res.headersSent && req.headers.referer?.includes('corsProxy')) {
+ // if (req.originalUrl === '/css/main.css' || req.originalUrl === '/favicon.ico') res.end();
+ // else
+ if (!res.headersSent && req.headers.referer?.includes('corsProxy')) {
+ if (!req.user) res.redirect('/home'); // When no user is logged in, we interpret a relative URL as being a reference to something they don't have access to and redirect to /home
// a request for something by a proxied referrer means it must be a relative reference. So construct a proxied absolute reference here.
try {
const proxiedRefererUrl = decodeURIComponent(req.headers.referer); // (e.g., http://localhost:<port>/corsProxy/https://en.wikipedia.org/wiki/Engelbart)