aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/.DS_Storebin10244 -> 10244 bytes
-rw-r--r--src/JSZipUtils.js142
-rw-r--r--src/Utils.ts91
-rw-r--r--src/client/DocServer.ts173
-rw-r--r--src/client/Network.ts34
-rw-r--r--src/client/apis/google_docs/GooglePhotosClientUtils.ts88
-rw-r--r--src/client/apis/gpt/GPT.ts87
-rw-r--r--src/client/documents/DocumentTypes.ts5
-rw-r--r--src/client/documents/Documents.ts1016
-rw-r--r--src/client/documents/Gitlike.ts226
-rw-r--r--src/client/goldenLayout.js3
-rw-r--r--src/client/util/CaptureManager.tsx39
-rw-r--r--src/client/util/CurrentUserUtils.ts479
-rw-r--r--src/client/util/DictationManager.ts22
-rw-r--r--src/client/util/DocumentManager.ts479
-rw-r--r--src/client/util/DragManager.ts144
-rw-r--r--src/client/util/DropConverter.ts22
-rw-r--r--src/client/util/GroupManager.tsx8
-rw-r--r--src/client/util/HypothesisUtils.ts2
-rw-r--r--src/client/util/Import & Export/DirectoryImportBox.tsx342
-rw-r--r--src/client/util/Import & Export/ImageUtils.ts32
-rw-r--r--src/client/util/InteractionUtils.tsx10
-rw-r--r--src/client/util/LinkFollower.ts190
-rw-r--r--src/client/util/LinkManager.ts204
-rw-r--r--src/client/util/RTFMarkup.tsx137
-rw-r--r--src/client/util/ReplayMovements.ts167
-rw-r--r--src/client/util/ReportManager.tsx2
-rw-r--r--src/client/util/Scripting.ts40
-rw-r--r--src/client/util/SearchUtil.ts94
-rw-r--r--src/client/util/SelectionManager.ts64
-rw-r--r--src/client/util/SerializationHelper.ts97
-rw-r--r--src/client/util/ServerStats.tsx54
-rw-r--r--src/client/util/SettingsManager.scss16
-rw-r--r--src/client/util/SettingsManager.tsx46
-rw-r--r--src/client/util/SharingManager.tsx114
-rw-r--r--src/client/util/SnappingManager.ts4
-rw-r--r--src/client/util/TrackMovements.ts43
-rw-r--r--src/client/util/UndoManager.ts73
-rw-r--r--src/client/util/request-image-size.ts (renamed from src/client/util/request-image-size.js)22
-rw-r--r--src/client/views/.DS_Storebin10244 -> 10244 bytes
-rw-r--r--src/client/views/AntimodeMenu.tsx143
-rw-r--r--src/client/views/AudioWaveform.tsx124
-rw-r--r--src/client/views/ContextMenu.tsx12
-rw-r--r--src/client/views/ContextMenuItem.tsx6
-rw-r--r--src/client/views/DashboardView.tsx59
-rw-r--r--src/client/views/DocComponent.tsx146
-rw-r--r--src/client/views/DocumentButtonBar.scss49
-rw-r--r--src/client/views/DocumentButtonBar.tsx301
-rw-r--r--src/client/views/DocumentDecorations.tsx356
-rw-r--r--src/client/views/EditableView.scss11
-rw-r--r--src/client/views/EditableView.tsx109
-rw-r--r--src/client/views/FilterPanel.scss (renamed from src/client/views/nodes/FilterBox.scss)24
-rw-r--r--src/client/views/FilterPanel.tsx234
-rw-r--r--src/client/views/GestureOverlay.tsx133
-rw-r--r--src/client/views/GlobalKeyHandler.ts90
-rw-r--r--src/client/views/InkControlPtHandles.tsx262
-rw-r--r--src/client/views/InkStroke.scss3
-rw-r--r--src/client/views/InkStrokeProperties.ts30
-rw-r--r--src/client/views/InkTangentHandles.tsx117
-rw-r--r--src/client/views/InkTranscription.tsx23
-rw-r--r--src/client/views/InkingStroke.tsx206
-rw-r--r--src/client/views/LightboxView.tsx178
-rw-r--r--src/client/views/Main.tsx6
-rw-r--r--src/client/views/MainView.scss7
-rw-r--r--src/client/views/MainView.tsx209
-rw-r--r--src/client/views/MainViewModal.tsx37
-rw-r--r--src/client/views/MarqueeAnnotator.tsx109
-rw-r--r--src/client/views/MetadataEntryMenu.tsx119
-rw-r--r--src/client/views/OverlayView.tsx149
-rw-r--r--src/client/views/Palette.scss30
-rw-r--r--src/client/views/Palette.tsx70
-rw-r--r--src/client/views/PreviewCursor.tsx55
-rw-r--r--src/client/views/PropertiesButtons.tsx155
-rw-r--r--src/client/views/PropertiesDocBacklinksSelector.tsx61
-rw-r--r--src/client/views/PropertiesDocContextSelector.tsx25
-rw-r--r--src/client/views/PropertiesView.scss23
-rw-r--r--src/client/views/PropertiesView.tsx1123
-rw-r--r--src/client/views/ScriptingRepl.scss5
-rw-r--r--src/client/views/ScriptingRepl.tsx113
-rw-r--r--src/client/views/SidebarAnnos.scss14
-rw-r--r--src/client/views/SidebarAnnos.tsx129
-rw-r--r--src/client/views/StyleProvider.scss15
-rw-r--r--src/client/views/StyleProvider.tsx297
-rw-r--r--src/client/views/TemplateMenu.tsx19
-rw-r--r--src/client/views/UndoStack.scss29
-rw-r--r--src/client/views/UndoStack.tsx47
-rw-r--r--src/client/views/animationtimeline/Keyframe.tsx418
-rw-r--r--src/client/views/animationtimeline/Timeline.tsx14
-rw-r--r--src/client/views/animationtimeline/Track.tsx261
-rw-r--r--src/client/views/collections/CollectionCarousel3DView.scss14
-rw-r--r--src/client/views/collections/CollectionCarousel3DView.tsx174
-rw-r--r--src/client/views/collections/CollectionCarouselView.tsx112
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx166
-rw-r--r--src/client/views/collections/CollectionMasonryViewFieldRow.tsx349
-rw-r--r--src/client/views/collections/CollectionMenu.tsx151
-rw-r--r--src/client/views/collections/CollectionNoteTakingView.scss20
-rw-r--r--src/client/views/collections/CollectionNoteTakingView.tsx282
-rw-r--r--src/client/views/collections/CollectionNoteTakingViewColumn.tsx88
-rw-r--r--src/client/views/collections/CollectionPileView.tsx183
-rw-r--r--src/client/views/collections/CollectionStackedTimeline.scss4
-rw-r--r--src/client/views/collections/CollectionStackedTimeline.tsx265
-rw-r--r--src/client/views/collections/CollectionStackingView.scss23
-rw-r--r--src/client/views/collections/CollectionStackingView.tsx234
-rw-r--r--src/client/views/collections/CollectionStackingViewFieldColumn.tsx93
-rw-r--r--src/client/views/collections/CollectionSubView.tsx148
-rw-r--r--src/client/views/collections/CollectionTimeView.tsx399
-rw-r--r--src/client/views/collections/CollectionTreeView.scss3
-rw-r--r--src/client/views/collections/CollectionTreeView.tsx129
-rw-r--r--src/client/views/collections/CollectionView.tsx130
-rw-r--r--src/client/views/collections/TabDocView.scss6
-rw-r--r--src/client/views/collections/TabDocView.tsx223
-rw-r--r--src/client/views/collections/TreeView.scss3
-rw-r--r--src/client/views/collections/TreeView.tsx387
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx61
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss9
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx138
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx5
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss52
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx1240
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx62
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx137
-rw-r--r--src/client/views/collections/collectionGrid/CollectionGridView.tsx11
-rw-r--r--src/client/views/collections/collectionLinear/CollectionLinearView.scss10
-rw-r--r--src/client/views/collections/collectionLinear/CollectionLinearView.tsx131
-rw-r--r--src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx36
-rw-r--r--src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx37
-rw-r--r--src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx639
-rw-r--r--src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx513
-rw-r--r--src/client/views/collections/collectionSchema/CollectionSchemaMovableColumn.tsx138
-rw-r--r--src/client/views/collections/collectionSchema/CollectionSchemaMovableRow.tsx151
-rw-r--r--src/client/views/collections/collectionSchema/CollectionSchemaView.scss716
-rw-r--r--src/client/views/collections/collectionSchema/CollectionSchemaView.tsx1352
-rw-r--r--src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx79
-rw-r--r--src/client/views/collections/collectionSchema/SchemaRowBox.tsx160
-rw-r--r--src/client/views/collections/collectionSchema/SchemaTable.tsx693
-rw-r--r--src/client/views/collections/collectionSchema/SchemaTableCell.tsx352
-rw-r--r--src/client/views/global/globalCssVariables.scss11
-rw-r--r--src/client/views/global/globalCssVariables.scss.d.ts6
-rw-r--r--src/client/views/linking/LinkEditor.scss334
-rw-r--r--src/client/views/linking/LinkEditor.tsx394
-rw-r--r--src/client/views/linking/LinkMenu.scss8
-rw-r--r--src/client/views/linking/LinkMenu.tsx49
-rw-r--r--src/client/views/linking/LinkMenuGroup.tsx80
-rw-r--r--src/client/views/linking/LinkMenuItem.scss30
-rw-r--r--src/client/views/linking/LinkMenuItem.tsx143
-rw-r--r--src/client/views/linking/LinkPopup.scss5
-rw-r--r--src/client/views/linking/LinkPopup.tsx22
-rw-r--r--src/client/views/linking/LinkRelationshipSearch.tsx52
-rw-r--r--src/client/views/nodes/AudioBox.tsx110
-rw-r--r--src/client/views/nodes/CollectionFreeFormDocumentView.scss4
-rw-r--r--src/client/views/nodes/CollectionFreeFormDocumentView.tsx107
-rw-r--r--src/client/views/nodes/ColorBox.tsx9
-rw-r--r--src/client/views/nodes/ComparisonBox.scss2
-rw-r--r--src/client/views/nodes/ComparisonBox.tsx137
-rw-r--r--src/client/views/nodes/DataViz.tsx20
-rw-r--r--src/client/views/nodes/DataVizBox/DataVizBox.scss4
-rw-r--r--src/client/views/nodes/DataVizBox/DataVizBox.tsx180
-rw-r--r--src/client/views/nodes/DataVizBox/DrawHelper.ts247
-rw-r--r--src/client/views/nodes/DataVizBox/HistogramBox.scss18
-rw-r--r--src/client/views/nodes/DataVizBox/HistogramBox.tsx159
-rw-r--r--src/client/views/nodes/DataVizBox/TableBox.tsx37
-rw-r--r--src/client/views/nodes/DataVizBox/components/Chart.scss41
-rw-r--r--src/client/views/nodes/DataVizBox/components/LineChart.tsx322
-rw-r--r--src/client/views/nodes/DataVizBox/components/TableBox.scss (renamed from src/client/views/nodes/DataVizBox/TableBox.scss)0
-rw-r--r--src/client/views/nodes/DataVizBox/components/TableBox.tsx105
-rw-r--r--src/client/views/nodes/DataVizBox/utils/D3Utils.ts67
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx33
-rw-r--r--src/client/views/nodes/DocumentIcon.tsx56
-rw-r--r--src/client/views/nodes/DocumentLinksButton.tsx85
-rw-r--r--src/client/views/nodes/DocumentView.scss33
-rw-r--r--src/client/views/nodes/DocumentView.tsx1489
-rw-r--r--src/client/views/nodes/EquationBox.tsx34
-rw-r--r--src/client/views/nodes/FieldView.tsx55
-rw-r--r--src/client/views/nodes/FilterBox.tsx588
-rw-r--r--src/client/views/nodes/FunctionPlotBox.tsx40
-rw-r--r--src/client/views/nodes/ImageBox.scss30
-rw-r--r--src/client/views/nodes/ImageBox.tsx254
-rw-r--r--src/client/views/nodes/KeyValueBox.tsx95
-rw-r--r--src/client/views/nodes/KeyValuePair.scss64
-rw-r--r--src/client/views/nodes/KeyValuePair.tsx82
-rw-r--r--src/client/views/nodes/LabelBox.tsx145
-rw-r--r--src/client/views/nodes/LinkAnchorBox.tsx126
-rw-r--r--src/client/views/nodes/LinkBox.tsx55
-rw-r--r--src/client/views/nodes/LinkDescriptionPopup.tsx2
-rw-r--r--src/client/views/nodes/LinkDocPreview.tsx159
-rw-r--r--src/client/views/nodes/LoadingBox.scss15
-rw-r--r--src/client/views/nodes/LoadingBox.tsx3
-rw-r--r--src/client/views/nodes/MapBox/MapBox.tsx233
-rw-r--r--src/client/views/nodes/MapBox/MapBox2.tsx642
-rw-r--r--src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx16
-rw-r--r--src/client/views/nodes/PDFBox.tsx306
-rw-r--r--src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.scss90
-rw-r--r--src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.tsx1987
-rw-r--r--src/client/views/nodes/PhysicsBox/PhysicsSimulationInputField.tsx179
-rw-r--r--src/client/views/nodes/PhysicsBox/PhysicsSimulationQuestions.json161
-rw-r--r--src/client/views/nodes/PhysicsBox/PhysicsSimulationTutorial.json600
-rw-r--r--src/client/views/nodes/PhysicsBox/PhysicsSimulationWall.tsx34
-rw-r--r--src/client/views/nodes/PhysicsBox/PhysicsSimulationWeight.tsx990
-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.tsx107
-rw-r--r--src/client/views/nodes/RecordingBox/RecordingView.tsx181
-rw-r--r--src/client/views/nodes/ScreenshotBox.tsx54
-rw-r--r--src/client/views/nodes/ScriptingBox.tsx45
-rw-r--r--src/client/views/nodes/SliderBox.tsx99
-rw-r--r--src/client/views/nodes/VideoBox.tsx242
-rw-r--r--src/client/views/nodes/WebBox.scss2
-rw-r--r--src/client/views/nodes/WebBox.tsx589
-rw-r--r--src/client/views/nodes/WebBoxRenderer.js8
-rw-r--r--src/client/views/nodes/button/ButtonScripts.ts16
-rw-r--r--src/client/views/nodes/button/FontIconBadge.tsx30
-rw-r--r--src/client/views/nodes/button/FontIconBox.scss30
-rw-r--r--src/client/views/nodes/button/FontIconBox.tsx634
-rw-r--r--src/client/views/nodes/button/colorDropdown/ColorDropdown.tsx57
-rw-r--r--src/client/views/nodes/formattedText/DashDocCommentView.tsx26
-rw-r--r--src/client/views/nodes/formattedText/DashDocView.tsx78
-rw-r--r--src/client/views/nodes/formattedText/DashFieldView.scss10
-rw-r--r--src/client/views/nodes/formattedText/DashFieldView.tsx247
-rw-r--r--src/client/views/nodes/formattedText/EquationView.tsx14
-rw-r--r--src/client/views/nodes/formattedText/FootnoteView.tsx70
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.scss76
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx890
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx14
-rw-r--r--src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts84
-rw-r--r--src/client/views/nodes/formattedText/RichTextMenu.tsx60
-rw-r--r--src/client/views/nodes/formattedText/RichTextRules.ts126
-rw-r--r--src/client/views/nodes/formattedText/SummaryView.tsx2
-rw-r--r--src/client/views/nodes/formattedText/marks_rts.ts24
-rw-r--r--src/client/views/nodes/formattedText/nodes_rts.ts10
-rw-r--r--src/client/views/nodes/trails/PresBox.scss29
-rw-r--r--src/client/views/nodes/trails/PresBox.tsx2547
-rw-r--r--src/client/views/nodes/trails/PresElementBox.scss6
-rw-r--r--src/client/views/nodes/trails/PresElementBox.tsx285
-rw-r--r--src/client/views/nodes/trails/PresEnums.ts5
-rw-r--r--src/client/views/pdf/AnchorMenu.tsx338
-rw-r--r--src/client/views/pdf/Annotation.tsx35
-rw-r--r--src/client/views/pdf/GPTPopup/GPTPopup.scss132
-rw-r--r--src/client/views/pdf/GPTPopup/GPTPopup.tsx188
-rw-r--r--src/client/views/pdf/PDFViewer.tsx169
-rw-r--r--src/client/views/search/SearchBox.tsx117
-rw-r--r--src/client/views/topbar/TopBar.tsx84
-rw-r--r--src/decycler/decycler.d.ts2
-rw-r--r--src/decycler/decycler.js51
-rw-r--r--src/fields/DateField.ts14
-rw-r--r--src/fields/Doc.ts914
-rw-r--r--src/fields/DocSymbols.ts26
-rw-r--r--src/fields/FieldLoader.scss15
-rw-r--r--src/fields/FieldLoader.tsx15
-rw-r--r--src/fields/FieldSymbols.ts21
-rw-r--r--src/fields/IconField.ts26
-rw-r--r--src/fields/InkField.ts5
-rw-r--r--src/fields/List.ts113
-rw-r--r--src/fields/ListSpec.ts0
-rw-r--r--src/fields/PresField.ts6
-rw-r--r--src/fields/Proxy.ts85
-rw-r--r--src/fields/RichTextField.ts26
-rw-r--r--src/fields/RichTextUtils.ts16
-rw-r--r--src/fields/Schema.ts78
-rw-r--r--src/fields/SchemaHeaderField.ts7
-rw-r--r--src/fields/ScriptField.ts65
-rw-r--r--src/fields/Types.ts96
-rw-r--r--src/fields/URLField.ts9
-rw-r--r--src/fields/documentSchemas.ts71
-rw-r--r--src/fields/util.ts299
-rw-r--r--src/mobile/AudioUpload.tsx86
-rw-r--r--src/mobile/ImageUpload.tsx2
-rw-r--r--src/mobile/MobileInterface.tsx28
-rw-r--r--src/server/ApiManagers/DataVizManager.ts26
-rw-r--r--src/server/ApiManagers/SearchManager.ts151
-rw-r--r--src/server/ApiManagers/UploadManager.ts90
-rw-r--r--src/server/ApiManagers/UserManager.ts1
-rw-r--r--src/server/DashStats.ts293
-rw-r--r--src/server/DashUploadUtils.ts26
-rw-r--r--src/server/DataVizUtils.ts26
-rw-r--r--src/server/Message.ts2
-rw-r--r--src/server/index.ts28
-rw-r--r--src/server/server_Initialization.ts29
-rw-r--r--src/server/updateProtos.ts24
-rw-r--r--src/server/websocket.ts109
279 files changed, 23732 insertions, 19610 deletions
diff --git a/src/.DS_Store b/src/.DS_Store
index 4ed785983..be99aa5af 100644
--- a/src/.DS_Store
+++ b/src/.DS_Store
Binary files differ
diff --git a/src/JSZipUtils.js b/src/JSZipUtils.js
new file mode 100644
index 000000000..5ce1bd471
--- /dev/null
+++ b/src/JSZipUtils.js
@@ -0,0 +1,142 @@
+var JSZipUtils = {};
+// just use the responseText with xhr1, response with xhr2.
+// The transformation doesn't throw away high-order byte (with responseText)
+// because JSZip handles that case. If not used with JSZip, you may need to
+// do it, see https://developer.mozilla.org/En/Using_XMLHttpRequest#Handling_binary_data
+JSZipUtils._getBinaryFromXHR = function (xhr) {
+ // for xhr.responseText, the 0xFF mask is applied by JSZip
+ return xhr.response || xhr.responseText;
+};
+
+// taken from jQuery
+function createStandardXHR() {
+ try {
+ return new window.XMLHttpRequest();
+ } catch (e) {}
+}
+
+function createActiveXHR() {
+ try {
+ return new window.ActiveXObject('Microsoft.XMLHTTP');
+ } catch (e) {}
+}
+
+// Create the request object
+var createXHR =
+ typeof window !== 'undefined' && window.ActiveXObject
+ ? /* Microsoft failed to properly
+ * implement the XMLHttpRequest in IE7 (can't request local files),
+ * so we use the ActiveXObject when it is available
+ * Additionally XMLHttpRequest can be disabled in IE7/IE8 so
+ * we need a fallback.
+ */
+ function () {
+ return createStandardXHR() || createActiveXHR();
+ }
+ : // For all other browsers, use the standard XMLHttpRequest object
+ createStandardXHR;
+
+/**
+ * @param {string} path The path to the resource to GET.
+ * @param {function|{callback: function, progress: function}} options
+ * @return {Promise|undefined} If no callback is passed then a promise is returned
+ */
+JSZipUtils.getBinaryContent = function (path, options) {
+ var promise, resolve, reject;
+ var callback;
+
+ if (!options) {
+ options = {};
+ }
+
+ // backward compatible callback
+ if (typeof options === 'function') {
+ callback = options;
+ options = {};
+ } else if (typeof options.callback === 'function') {
+ // callback inside options object
+ callback = options.callback;
+ }
+
+ if (!callback && typeof Promise !== 'undefined') {
+ promise = new Promise(function (_resolve, _reject) {
+ resolve = _resolve;
+ reject = _reject;
+ });
+ } else {
+ resolve = function (data) {
+ callback(null, data);
+ };
+ reject = function (err) {
+ callback(err, null);
+ };
+ }
+
+ /*
+ * Here is the tricky part : getting the data.
+ * In firefox/chrome/opera/... setting the mimeType to 'text/plain; charset=x-user-defined'
+ * is enough, the result is in the standard xhr.responseText.
+ * cf https://developer.mozilla.org/En/XMLHttpRequest/Using_XMLHttpRequest#Receiving_binary_data_in_older_browsers
+ * In IE <= 9, we must use (the IE only) attribute responseBody
+ * (for binary data, its content is different from responseText).
+ * In IE 10, the 'charset=x-user-defined' trick doesn't work, only the
+ * responseType will work :
+ * http://msdn.microsoft.com/en-us/library/ie/hh673569%28v=vs.85%29.aspx#Binary_Object_upload_and_download
+ *
+ * I'd like to use jQuery to avoid this XHR madness, but it doesn't support
+ * the responseType attribute : http://bugs.jquery.com/ticket/11461
+ */
+ try {
+ var xhr = createXHR();
+
+ xhr.open('GET', path, true);
+
+ // recent browsers
+ if ('responseType' in xhr) {
+ xhr.responseType = 'arraybuffer';
+ }
+
+ // older browser
+ if (xhr.overrideMimeType) {
+ xhr.overrideMimeType('text/plain; charset=x-user-defined');
+ }
+
+ xhr.onreadystatechange = function (event) {
+ // use `xhr` and not `this`... thanks IE
+ if (xhr.readyState === 4) {
+ if (xhr.status === 200 || xhr.status === 0) {
+ try {
+ resolve(JSZipUtils._getBinaryFromXHR(xhr));
+ } catch (err) {
+ reject(new Error(err));
+ }
+ } else {
+ reject(new Error('Ajax error for ' + path + ' : ' + this.status + ' ' + this.statusText));
+ }
+ }
+ };
+
+ if (options.progress) {
+ xhr.onprogress = function (e) {
+ options.progress({
+ path: path,
+ originalEvent: e,
+ percent: (e.loaded / e.total) * 100,
+ loaded: e.loaded,
+ total: e.total,
+ });
+ };
+ }
+
+ xhr.send();
+ } catch (e) {
+ reject(new Error(e), null);
+ }
+
+ // returns a promise or undefined depending on whether a callback was
+ // provided
+ return promise;
+};
+
+// export
+module.exports = JSZipUtils;
diff --git a/src/Utils.ts b/src/Utils.ts
index 5e0514bc6..8c56896c5 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -1,14 +1,19 @@
import v4 = require('uuid/v4');
import v5 = require('uuid/v5');
import { ColorState } from 'react-color';
+import * as rp from 'request-promise';
import { Socket } from 'socket.io';
import { Colors } from './client/views/global/globalEnums';
import { Message } from './server/Message';
import Color = require('color');
export namespace Utils {
+ export let CLICK_TIME = 300;
export let DRAG_THRESHOLD = 4;
export let SNAP_THRESHOLD = 10;
+ export function isClick(x: number, y: number, downX: number, downY: number, downTime: number) {
+ return Date.now() - downTime < Utils.CLICK_TIME && Math.abs(x - downX) < Utils.DRAG_THRESHOLD && Math.abs(y - downY) < Utils.DRAG_THRESHOLD;
+ }
export function readUploadedFileAsText(inputFile: File) {
const temporaryFileReader = new FileReader();
@@ -34,6 +39,31 @@ export namespace Utils {
return v5(seed, v5.URL);
}
+ /**
+ * Uploads an image buffer to the server and stores with specified filename. by default the image
+ * is stored at multiple resolutions each retrieved by using the filename appended with _o, _s, _m, _l (indicating original, small, medium, or large)
+ * @param imageUri the bytes of the image
+ * @param returnedFilename the base filename to store the image on the server
+ * @param nosuffix optionally suppress creating multiple resolution images
+ */
+ export async function convertDataUri(imageUri: string, returnedFilename: string, nosuffix = false, replaceRootFilename?: string) {
+ try {
+ const posting = Utils.prepend('/uploadURI');
+ const returnedUri = await rp.post(posting, {
+ body: {
+ uri: imageUri,
+ name: returnedFilename,
+ nosuffix,
+ replaceRootFilename,
+ },
+ json: true,
+ });
+ return returnedUri;
+ } catch (e) {
+ console.log('VideoBox :' + e);
+ }
+ }
+
export function GetScreenTransform(ele?: HTMLElement): { scale: number; translateX: number; translateY: number } {
if (!ele) {
return { scale: 1, translateX: 1, translateY: 1 };
@@ -218,6 +248,7 @@ export namespace Utils {
}
export function scrollIntoView(targetY: number, targetHgt: number, scrollTop: number, contextHgt: number, minSpacing: number, scrollHeight: number) {
+ if (!targetHgt) return targetY; // if there's no height, then assume that
if (scrollTop + contextHgt < Math.min(scrollHeight, targetY + minSpacing + targetHgt)) {
return Math.ceil(targetY + minSpacing + targetHgt - contextHgt);
}
@@ -482,11 +513,29 @@ export function returnTrue() {
return true;
}
+export function returnIgnore(): 'ignore' {
+ return 'ignore';
+}
+export function returnAlways(): 'always' {
+ return 'always';
+}
+export function returnNever(): 'never' {
+ return 'never';
+}
+
+export function returnDefault(): 'default' {
+ return 'default';
+}
+
+export function return18() {
+ return 18;
+}
+
export function returnFalse() {
return false;
}
-export function returnAll() {
+export function returnAll(): 'all' {
return 'all';
}
@@ -495,7 +544,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() {
@@ -558,9 +607,13 @@ export namespace JSONUtils {
}
}
-const easeInOutQuad = (currentTime: number, start: number, change: number, duration: number) => {
- let newCurrentTime = currentTime / (duration / 2);
+const easeFunc = (transition: 'ease' | 'linear' | undefined, currentTime: number, start: number, change: number, duration: number) => {
+ if (transition === 'linear') {
+ let newCurrentTime = currentTime / duration; // currentTime / (duration / 2);
+ return start + newCurrentTime * change;
+ }
+ let newCurrentTime = currentTime / (duration / 2);
if (newCurrentTime < 1) {
return (change / 2) * newCurrentTime * newCurrentTime + start;
}
@@ -569,23 +622,28 @@ const easeInOutQuad = (currentTime: number, start: number, change: number, durat
return (-change / 2) * (newCurrentTime * (newCurrentTime - 2) - 1) + start;
};
-export function smoothScroll(duration: number, element: HTMLElement | HTMLElement[], to: number) {
+export function smoothScroll(duration: number, element: HTMLElement | HTMLElement[], to: number, transition: 'ease' | 'linear' | undefined, stopper?: () => void) {
+ stopper?.();
const elements = element instanceof HTMLElement ? [element] : element;
const starts = elements.map(element => element.scrollTop);
const startDate = new Date().getTime();
-
+ let _stop = false;
+ const stop = () => (_stop = true);
const animateScroll = () => {
const currentDate = new Date().getTime();
const currentTime = currentDate - startDate;
- elements.map((element, i) => (element.scrollTop = easeInOutQuad(currentTime, starts[i], to - starts[i], duration)));
+ elements.map((element, i) => (element.scrollTop = easeFunc(transition, currentTime, starts[i], to - starts[i], duration)));
- if (currentTime < duration) {
- requestAnimationFrame(animateScroll);
- } else {
- elements.forEach(element => (element.scrollTop = to));
+ if (!_stop) {
+ if (currentTime < duration) {
+ requestAnimationFrame(animateScroll);
+ } else {
+ elements.forEach(element => (element.scrollTop = to));
+ }
}
};
animateScroll();
+ return stop;
}
export function smoothScrollHorizontal(duration: number, element: HTMLElement | HTMLElement[], to: number) {
@@ -596,7 +654,7 @@ export function smoothScrollHorizontal(duration: number, element: HTMLElement |
const animateScroll = () => {
const currentDate = new Date().getTime();
const currentTime = currentDate - startDate;
- elements.map((element, i) => (element.scrollLeft = easeInOutQuad(currentTime, starts[i], to - starts[i], duration)));
+ elements.map((element, i) => (element.scrollLeft = easeFunc('ease', currentTime, starts[i], to - starts[i], duration)));
if (currentTime < duration) {
requestAnimationFrame(animateScroll);
@@ -677,8 +735,9 @@ export function DashColor(color: string) {
try {
return color ? Color(color.toLowerCase()) : Color('transparent');
} catch (e) {
- console.log('COLOR error:', e);
- return Color('red');
+ if (color.includes('gradient')) console.log("using color 'white' in place of :" + color);
+ else console.log('COLOR error:', e);
+ return Color('white');
}
}
@@ -807,7 +866,7 @@ export function setupMoveUpEvents(
(target as any)._noClick = clickEvent(e, (target as any)._doubleTap);
}
document.removeEventListener('pointermove', _moveEvent);
- document.removeEventListener('pointerup', _upEvent);
+ document.removeEventListener('pointerup', _upEvent, true);
};
const _clickEvent = (e: MouseEvent): void => {
if ((target as any)._noClick) e.stopPropagation();
@@ -818,6 +877,6 @@ export function setupMoveUpEvents(
e.preventDefault();
}
document.addEventListener('pointermove', _moveEvent);
- document.addEventListener('pointerup', _upEvent);
+ document.addEventListener('pointerup', _upEvent, true);
document.addEventListener('click', _clickEvent, true);
}
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
index 5a34fcf11..2a7f5a09b 100644
--- a/src/client/DocServer.ts
+++ b/src/client/DocServer.ts
@@ -1,7 +1,9 @@
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, 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';
@@ -28,16 +30,34 @@ import { GestureOverlay } from './views/GestureOverlay';
export namespace DocServer {
let _cache: { [id: string]: RefField | Promise<Opt<RefField>> } = {};
+ export function QUERY_SERVER_CACHE(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;
+ }
+ let lastCacheUpdate = 0;
export function UPDATE_SERVER_CACHE(print: boolean = false) {
- 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)));
+ 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;
});
- print && strings.sort().forEach((str, i) => console.log(i.toString() + ' ' + str));
+ if (filtered.length === lastCacheUpdate) return;
+ lastCacheUpdate = filtered.length;
+
rp.post(Utils.prepend('/setCacheDocumentIds'), {
body: {
- cacheDocumentIds: Array.from(Object.keys(_cache)).join(';'),
+ cacheDocumentIds: filtered.join(';'),
},
json: true,
});
@@ -225,7 +245,7 @@ export namespace DocServer {
* the server if the document has not been cached.
* @param id the id of the requested document
*/
- const _GetRefFieldImpl = async (id: string, force: boolean = false): Promise<Opt<RefField>> => {
+ const _GetRefFieldImpl = (id: string, force: boolean = false): Promise<Opt<RefField>> => {
// an initial pass through the cache to determine whether the document needs to be fetched,
// is already in the process of being fetched or already exists in the
// cache
@@ -300,8 +320,7 @@ export namespace DocServer {
}
export async function getYoutubeChannels() {
- const apiKey = await Utils.EmitCallback(_socket, MessageStore.YoutubeApiQuery, { type: YoutubeQueryTypes.Channels });
- return apiKey;
+ return await Utils.EmitCallback(_socket, MessageStore.YoutubeApiQuery, { type: YoutubeQueryTypes.Channels });
}
export function getYoutubeVideos(videoTitle: string, callBack: (videos: any[]) => void) {
@@ -320,10 +339,11 @@ export namespace DocServer {
*/
const _GetRefFieldsImpl = async (ids: string[]): Promise<{ [id: string]: Opt<RefField> }> => {
const requestedIds: string[] = [];
- const waitingIds: string[] = [];
- const promises: Promise<Opt<RefField>>[] = [];
- const map: { [id: string]: Opt<RefField> } = {};
+ const promises: Promise<any>[] = [];
+ let defaultRes: any = undefined;
+ const defaultPromise = new Promise<any>(res => (defaultRes = res));
+ let defaultPromises: { p: Promise<any>; id: string }[] = [];
// 1) an initial pass through the cache to determine
// i) which documents need to be fetched
// ii) which are already in the process of being fetched
@@ -331,6 +351,13 @@ export namespace DocServer {
for (const id of ids) {
const cached = _cache[id];
if (cached === undefined) {
+ defaultPromises.push({
+ id,
+ p: (_cache[id] = new Promise<any>(async res => {
+ await defaultPromise;
+ res(_cache[id]);
+ })),
+ });
// NOT CACHED => we'll have to send a request to the server
requestedIds.push(id);
} else if (cached instanceof Promise) {
@@ -338,10 +365,10 @@ export namespace DocServer {
// and requested one of the documents I'm looking for. Shouldn't fetch again, just
// wait until this promise is resolved (see 7)
promises.push(cached);
- waitingIds.push(id);
+ // waitingIds.push(id);
} else {
// CACHED => great, let's just add it to the field map
- map[id] = cached;
+ // map[id] = cached;
}
}
@@ -349,66 +376,65 @@ export namespace DocServer {
// 2) synchronously, we emit a single callback to the server requesting the serialized (i.e. represented by a string)
// 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
- const getSerializedFields: Promise<any> = Utils.EmitCallback(_socket, MessageStore.GetRefFields, requestedIds);
+ console.log('Requesting ' + requestedIds.length);
+ FieldLoader.active && 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.
// Here, once deserialized, we also invoke .proto to 'load' the documents' prototypes, which ensures that all
// future .proto calls on the Doc won't have to go farther than the cache to get their actual value.
- const deserializeFields = getSerializedFields.then(async fields => {
- const fieldMap: { [id: string]: RefField } = {};
- const proms: Promise<void>[] = [];
- runInAction(() => {
- for (const field of fields) {
- const cached = _cache[field.id];
- if (!cached) {
- // deserialize
- const prom = SerializationHelper.Deserialize(field).then(deserialized => {
- fieldMap[field.id] = deserialized;
-
- //overwrite or delete any promises (that we inserted as flags
- // to indicate that the field was in the process of being fetched). Now everything
- // should be an actual value within or entirely absent from the cache.
- if (deserialized !== undefined) {
- _cache[field.id] = deserialized;
- } else {
- delete _cache[field.id];
- }
- return deserialized;
- });
- // 4) here, for each of the documents we've requested *ourselves* (i.e. weren't promises or found in the cache)
- // we set the value at the field's id to a promise that will resolve to the field.
- // When we find that promises exist at keys in the cache, THIS is where they were set, just by some other caller (method).
- // The mapping in the .then call ensures that when other callers await these promises, they'll
- // get the resolved field
- _cache[field.id] = prom;
-
- // adds to a list of promises that will be awaited asynchronously
- proms.push(prom);
- } else if (cached instanceof Promise) {
- proms.push(cached as any);
- cached.then((f: any) => (fieldMap[field.id] = f));
- } else if (field) {
- proms.push(cached as any);
- fieldMap[field.id] = DocServer.GetCachedRefField(field.id) || field;
- }
- }
- });
- await Promise.all(proms);
- return fieldMap;
- });
+ let processed = 0;
+ console.log('deserializing ' + serializedFields.length + ' fields');
+ for (const field of serializedFields) {
+ processed++;
+ if (FieldLoader.active && 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
+ }
+ const cached = _cache[field.id];
+ if (!cached || (cached instanceof Promise && defaultPromises.some(dp => dp.p === cached))) {
+ // deserialize
+ // adds to a list of promises that will be awaited asynchronously
+ promises.push(
+ (_cache[field.id] = SerializationHelper.Deserialize(field).then(deserialized => {
+ //overwrite or delete any promises (that we inserted as flags
+ // to indicate that the field was in the process of being fetched). Now everything
+ // should be an actual value within or entirely absent from the cache.
+ if (deserialized !== undefined) {
+ _cache[field.id] = deserialized;
+ } else {
+ delete _cache[field.id];
+ }
+ const promInd = defaultPromises.findIndex(dp => dp.id === field.id);
+ promInd !== -1 && defaultPromises.splice(promInd, 1);
+ return deserialized;
+ }))
+ );
+ // 4) here, for each of the documents we've requested *ourselves* (i.e. weren't promises or found in the cache)
+ // we set the value at the field's id to a promise that will resolve to the field.
+ // When we find that promises exist at keys in the cache, THIS is where they were set, just by some other caller (method).
+ // The mapping in the .then call ensures that when other callers await these promises, they'll
+ // get the resolved field
+ } else if (cached instanceof Promise) {
+ console.log('.');
+ //promises.push(cached);
+ } else if (field) {
+ // console.log('-');
+ }
+ }
+ }
- // 5) at this point, all fields have a) been returned from the server and b) been deserialized into actual Field objects whose
- // prototype documents, if any, have also been fetched and cached.
- const fields = await deserializeFields;
+ await Promise.all(promises);
+ defaultPromises.forEach(df => delete _cache[df.id]);
+ defaultRes();
- // 6) with this confidence, we can now go through and update the cache at the ids of the fields that
- // we explicitly had to fetch. To finish it off, we add whatever value we've come up with for a given
- // id to the soon-to-be-returned field mapping.
- requestedIds.forEach(id => {
- const field = fields[id];
- map[id] = field;
- });
- }
+ // 5) at this point, all fields have a) been returned from the server and b) been deserialized into actual Field objects whose
+ // prototype documents, if any, have also been fetched and cached.
+ console.log('Deserialized ' + (requestedIds.length - defaultPromises.length) + ' fields');
+ // 6) with this confidence, we can now go through and update the cache at the ids of the fields that
+ // we explicitly had to fetch. To finish it off, we add whatever value we've come up with for a given
+ // id to the soon-to-be-returned field mapping.
+ //ids.forEach(id => (map[id] = _cache[id] as any));
// 7) those promises we encountered in the else if of 1), which represent
// other callers having already submitted a request to the server for (a) document(s)
@@ -417,16 +443,19 @@ export namespace DocServer {
//
// fortunately, those other callers will also hit their own version of 6) and clean up
// the shared cache when these promises resolve, so all we have to do is...
- const otherCallersFetching = await Promise.all(promises);
+ //const otherCallersFetching = await Promise.all(promises);
// ...extract the RefFields returned from the resolution of those promises and add them to our
// own map.
- waitingIds.forEach((id, index) => (map[id] = otherCallersFetching[index]));
+ // waitingIds.forEach((id, index) => (map[id] = otherCallersFetching[index]));
// now, we return our completed mapping from all of the ids that were passed into the method
// to their actual RefField | undefined values. This return value either becomes the input
// argument to the caller's promise (i.e. GetRefFields(["_id1_", "_id2_", "_id3_"]).then(map => //do something with map...))
// or it is the direct return result if the promise is awaited (i.e. let fields = await GetRefFields(["_id1_", "_id2_", "_id3_"])).
- return map;
+ return ids.reduce((map, id) => {
+ map[id] = _cache[id] as any;
+ return map;
+ }, {} as { [id: string]: Opt<RefField> });
};
let _GetRefFields: (ids: string[]) => Promise<{ [id: string]: Opt<RefField> }> = errorFunc;
diff --git a/src/client/Network.ts b/src/client/Network.ts
index 19eff3b3b..d606b9854 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,31 @@ 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
* @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[]): 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,9 +57,11 @@ 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',
diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
index ff9460b62..e8fd8fb8a 100644
--- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts
+++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
@@ -1,27 +1,26 @@
-import { AssertionError } from "assert";
-import { EditorState } from "prosemirror-state";
-import { Doc, DocListCastAsync, Opt } from "../../../fields/Doc";
-import { Id } from "../../../fields/FieldSymbols";
-import { RichTextField } from "../../../fields/RichTextField";
-import { RichTextUtils } from "../../../fields/RichTextUtils";
-import { Cast, StrCast } from "../../../fields/Types";
-import { ImageField } from "../../../fields/URLField";
-import { MediaItem, NewMediaItemResult } from "../../../server/apis/google/SharedTypes";
-import { Utils } from "../../../Utils";
-import { Docs, DocumentOptions, DocUtils } from "../../documents/Documents";
-import { Networking } from "../../Network";
-import { FormattedTextBox } from "../../views/nodes/formattedText/FormattedTextBox";
-import { GoogleAuthenticationManager } from "../GoogleAuthenticationManager";
+import { AssertionError } from 'assert';
+import { EditorState } from 'prosemirror-state';
+import { Doc, DocListCastAsync, Opt } from '../../../fields/Doc';
+import { Id } from '../../../fields/FieldSymbols';
+import { RichTextField } from '../../../fields/RichTextField';
+import { RichTextUtils } from '../../../fields/RichTextUtils';
+import { Cast, StrCast } from '../../../fields/Types';
+import { ImageField } from '../../../fields/URLField';
+import { MediaItem, NewMediaItemResult } from '../../../server/apis/google/SharedTypes';
+import { Utils } from '../../../Utils';
+import { Docs, DocumentOptions, DocUtils } from '../../documents/Documents';
+import { Networking } from '../../Network';
+import { FormattedTextBox } from '../../views/nodes/formattedText/FormattedTextBox';
+import { GoogleAuthenticationManager } from '../GoogleAuthenticationManager';
import Photos = require('googlephotos');
export namespace GooglePhotos {
-
const endpoint = async () => new Photos(await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken());
export enum MediaType {
ALL_MEDIA = 'ALL_MEDIA',
PHOTO = 'PHOTO',
- VIDEO = 'VIDEO'
+ VIDEO = 'VIDEO',
}
export type AlbumReference = { id: string } | { title: string };
@@ -58,11 +57,10 @@ export namespace GooglePhotos {
HOUSES: 'HOUSES',
GARDENS: 'GARDENS',
FLOWERS: 'FLOWERS',
- HOLIDAYS: 'HOLIDAYS'
+ HOLIDAYS: 'HOLIDAYS',
};
export namespace Export {
-
export interface AlbumCreationResult {
albumId: string;
mediaItems: MediaItem[];
@@ -82,20 +80,20 @@ export namespace GooglePhotos {
if (!images || !images.length) {
return undefined;
}
- const resolved = title ? title : (StrCast(collection.title) || `Dash Collection (${collection[Id]}`);
+ const resolved = title ? title : StrCast(collection.title) || `Dash Collection (${collection[Id]}`;
const { id, productUrl } = await Create.Album(resolved);
const response = await Transactions.UploadImages(images, { id }, descriptionKey);
if (response) {
const { results, failed } = response;
let index: Opt<number>;
while ((index = failed.pop()) !== undefined) {
- Doc.RemoveDocFromList(dataDocument, "data", images.splice(index, 1)[0]);
+ Doc.RemoveDocFromList(dataDocument, 'data', images.splice(index, 1)[0]);
}
const mediaItems: MediaItem[] = results.map(item => item.mediaItem);
if (mediaItems.length !== images.length) {
throw new AssertionError({ actual: mediaItems.length, expected: images.length });
}
- const idMapping = new Doc;
+ const idMapping = new Doc();
for (let i = 0; i < images.length; i++) {
const image = Doc.GetProto(images[i]);
const mediaItem = mediaItems[i];
@@ -117,38 +115,30 @@ export namespace GooglePhotos {
return { albumId: id, mediaItems };
}
};
-
}
export namespace Import {
-
export type CollectionConstructor = (data: Array<Doc>, options: DocumentOptions, ...args: any) => Doc;
export const CollectionFromSearch = async (constructor: CollectionConstructor, requested: Opt<Partial<Query.SearchOptions>>): Promise<Doc> => {
await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken();
const response = await Query.ContentSearch(requested);
const uploads = await Transactions.WriteMediaItemsToServer(response);
- const children = uploads.map((upload: Transactions.UploadInformation) => {
- const document = Docs.Create.ImageDocument(Utils.fileUrl(upload.fileNames.clean));
- document.contentSize = upload.contentSize;
- return document;
- });
+ const children = uploads.map((upload: Transactions.UploadInformation) => Docs.Create.ImageDocument(Utils.fileUrl(upload.fileNames.clean) /*, {"data_contentSize":upload.contentSize}*/));
const options = { _width: 500, _height: 500 };
return constructor(children, options);
};
-
}
export namespace Query {
-
- const delimiter = ", ";
- const comparator = (a: string, b: string) => (a < b) ? -1 : (a > b ? 1 : 0);
+ const delimiter = ', ';
+ const comparator = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0);
export const TagChildImages = async (collection: Doc) => {
await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken();
const idMapping = await Cast(collection.googlePhotosIdMapping, Doc);
if (!idMapping) {
- throw new Error("Appending image metadata requires that the targeted collection have already been mapped to an album!");
+ throw new Error('Appending image metadata requires that the targeted collection have already been mapped to an album!');
}
const tagMapping = new Map<string, string>();
const images = (await DocListCastAsync(collection.data))!.map(Doc.GetProto);
@@ -169,7 +159,7 @@ export namespace GooglePhotos {
const concatenated = tagMapping.get(image[Id])!;
const tags = concatenated.split(delimiter);
if (tags.length > 1) {
- const cleaned = concatenated.replace(ContentCategories.NONE + delimiter, "");
+ const cleaned = concatenated.replace(ContentCategories.NONE + delimiter, '');
image.googlePhotosTags = cleaned.split(delimiter).sort(comparator).join(delimiter);
} else {
image.googlePhotosTags = ContentCategories.NONE;
@@ -253,19 +243,15 @@ export namespace GooglePhotos {
export const GetImage = async (mediaItemId: string): Promise<Transactions.MediaItem> => {
return (await endpoint()).mediaItems.get(mediaItemId);
};
-
}
namespace Create {
-
export const Album = async (title: string) => {
return (await endpoint()).albums.create(title);
};
-
}
export namespace Transactions {
-
export interface UploadInformation {
mediaPaths: string[];
fileNames: { [key: string]: string };
@@ -295,18 +281,20 @@ export namespace GooglePhotos {
};
export const WriteMediaItemsToServer = async (body: { mediaItems: any[] }): Promise<UploadInformation[]> => {
- const uploads = await Networking.PostToServer("/googlePhotosMediaGet", body);
+ const uploads = await Networking.PostToServer('/googlePhotosMediaGet', body);
return uploads;
};
- export const UploadThenFetch = async (sources: Doc[], album?: AlbumReference, descriptionKey = "caption") => {
+ export const UploadThenFetch = async (sources: Doc[], album?: AlbumReference, descriptionKey = 'caption') => {
const response = await UploadImages(sources, album, descriptionKey);
if (!response) {
return undefined;
}
- const baseUrls: string[] = await Promise.all(response.results.map(item => {
- return new Promise<string>(resolve => Query.GetImage(item.mediaItem.id).then(item => resolve(item.baseUrl)));
- }));
+ const baseUrls: string[] = await Promise.all(
+ response.results.map(item => {
+ return new Promise<string>(resolve => Query.GetImage(item.mediaItem.id).then(item => resolve(item.baseUrl)));
+ })
+ );
return baseUrls;
};
@@ -315,9 +303,9 @@ export namespace GooglePhotos {
failed: number[];
}
- export const UploadImages = async (sources: Doc[], album?: AlbumReference, descriptionKey = "caption"): Promise<Opt<ImageUploadResults>> => {
+ export const UploadImages = async (sources: Doc[], album?: AlbumReference, descriptionKey = 'caption'): Promise<Opt<ImageUploadResults>> => {
await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken();
- if (album && "title" in album) {
+ if (album && 'title' in album) {
album = await Create.Album(album.title);
}
const media: MediaInput[] = [];
@@ -327,13 +315,13 @@ export namespace GooglePhotos {
return;
}
const url = data.url.href;
- const target = Doc.MakeAlias(source);
+ const target = Doc.MakeEmbedding(source);
const description = parseDescription(target, descriptionKey);
await DocUtils.makeCustomViewClicked(target, Docs.Create.FreeformDocument);
media.push({ url, description });
}
if (media.length) {
- const results = await Networking.PostToServer("/googlePhotosMediaPost", { media, album });
+ const results = await Networking.PostToServer('/googlePhotosMediaPost', { media, album });
return results;
}
};
@@ -341,14 +329,12 @@ export namespace GooglePhotos {
const parseDescription = (document: Doc, descriptionKey: string) => {
let description: string = Utils.prepend(`/doc/${document[Id]}?sharing=true`);
const target = document[descriptionKey];
- if (typeof target === "string") {
+ if (typeof target === 'string') {
description = target;
} else if (target instanceof RichTextField) {
description = RichTextUtils.ToPlainText(EditorState.fromJSON(FormattedTextBox.Instance.config, JSON.parse(target.Data)));
}
return description;
};
-
}
-
-} \ No newline at end of file
+}
diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
new file mode 100644
index 000000000..4b3960902
--- /dev/null
+++ b/src/client/apis/gpt/GPT.ts
@@ -0,0 +1,87 @@
+import { Configuration, OpenAIApi } from 'openai';
+
+enum GPTCallType {
+ SUMMARY = 'summary',
+ COMPLETION = 'completion',
+ EDIT = 'edit',
+}
+
+type GPTCallOpts = {
+ model: string;
+ maxTokens: number;
+ temp: number;
+ prompt: string;
+};
+
+const callTypeMap: { [type: string]: GPTCallOpts } = {
+ summary: { model: 'text-davinci-003', maxTokens: 256, temp: 0.5, prompt: 'Summarize this text briefly: ' },
+ edit: { model: 'text-davinci-003', maxTokens: 256, temp: 0.5, prompt: 'Reword this: ' },
+ completion: { model: 'text-davinci-003', maxTokens: 256, temp: 0.5, prompt: '' },
+};
+
+/**
+ * Calls the OpenAI API.
+ *
+ * @param inputText Text to process
+ * @returns AI Output
+ */
+const gptAPICall = async (inputText: string, callType: GPTCallType) => {
+ if (callType === GPTCallType.SUMMARY) inputText += '.';
+ const opts: GPTCallOpts = callTypeMap[callType];
+ try {
+ const configuration = new Configuration({
+ apiKey: process.env.OPENAI_KEY,
+ });
+ const openai = new OpenAIApi(configuration);
+ const response = await openai.createCompletion({
+ model: opts.model,
+ max_tokens: opts.maxTokens,
+ temperature: opts.temp,
+ prompt: `${opts.prompt}${inputText}`,
+ });
+ console.log(response.data.choices[0]);
+ return response.data.choices[0].text;
+ } catch (err) {
+ console.log(err);
+ return 'Error connecting with API.';
+ }
+};
+
+const gptImageCall = async (prompt: string) => {
+ try {
+ const configuration = new Configuration({
+ apiKey: process.env.OPENAI_KEY,
+ });
+ const openai = new OpenAIApi(configuration);
+ const response = await openai.createImage({
+ prompt: prompt,
+ n: 1,
+ size: '1024x1024',
+ });
+ return response.data.data[0].url;
+ } catch (err) {
+ console.error(err);
+ return;
+ }
+};
+
+// const gptEditCall = async (selectedText: string, fullText: string) => {
+// try {
+// const configuration = new Configuration({
+// apiKey: process.env.OPENAI_KEY,
+// });
+// const openai = new OpenAIApi(configuration);
+// const response = await openai.createCompletion({
+// model: 'text-davinci-003',
+// max_tokens: 256,
+// temperature: 0.1,
+// prompt: `Replace the phrase ${selectedText} inside of ${fullText}.`,
+// });
+// return response.data.choices[0].text.trim();
+// } catch (err) {
+// console.log(err);
+// return 'Error connecting with API.';
+// }
+// };
+
+export { gptAPICall, gptImageCall, GPTCallType };
diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts
index d99cd2dac..cdc8c275c 100644
--- a/src/client/documents/DocumentTypes.ts
+++ b/src/client/documents/DocumentTypes.ts
@@ -11,7 +11,7 @@ export enum DocumentType {
AUDIO = 'audio',
REC = 'recording',
PDF = 'pdf',
- INK = 'inks',
+ INK = 'ink',
SCREENSHOT = 'screenshot',
FONTICON = 'fonticonbox',
FILTER = 'filter',
@@ -19,7 +19,7 @@ export enum DocumentType {
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)
+ 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)
DATE = 'date', // calendar view of a date
SCRIPTING = 'script', // script editor
EQUATION = 'equation', // equation editor
@@ -27,6 +27,7 @@ export enum DocumentType {
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',
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index d789d4ee3..acd323eca 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -2,17 +2,17 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { action, runInAction } from 'mobx';
import { basename } from 'path';
import { DateField } from '../../fields/DateField';
-import { Doc, DocListCast, DocListCastAsync, 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';
import { List } from '../../fields/List';
-import { ProxyField } from '../../fields/Proxy';
import { RichTextField } from '../../fields/RichTextField';
import { SchemaHeaderField } from '../../fields/SchemaHeaderField';
import { ComputedField, ScriptField } from '../../fields/ScriptField';
-import { Cast, DocCast, FieldValue, NumCast, ScriptCast, StrCast } from '../../fields/Types';
-import { AudioField, CsvField, ImageField, MapField, PdfField, RecordingField, VideoField, WebField, YoutubeField } from '../../fields/URLField';
+import { BoolCast, Cast, DocCast, FieldValue, NumCast, ScriptCast, StrCast } from '../../fields/Types';
+import { AudioField, CsvField, ImageField, PdfField, VideoField, WebField, YoutubeField } from '../../fields/URLField';
import { inheritParentAcls, SharingPermissions } from '../../fields/util';
import { Upload } from '../../server/SharedMediaTypes';
import { aggregateBounds, OmitKeys, Utils } from '../../Utils';
@@ -22,9 +22,10 @@ import { Networking } from '../Network';
import { DocumentManager } from '../util/DocumentManager';
import { DragManager, dropActionType } from '../util/DragManager';
import { DirectoryImportBox } from '../util/Import & Export/DirectoryImportBox';
+import { FollowLinkScript } from '../util/LinkFollower';
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';
@@ -37,10 +38,8 @@ import { FontIconBox } from '../views/nodes/button/FontIconBox';
import { ColorBox } from '../views/nodes/ColorBox';
import { ComparisonBox } from '../views/nodes/ComparisonBox';
import { DataVizBox } from '../views/nodes/DataVizBox/DataVizBox';
-import { DocFocusOptions } from '../views/nodes/DocumentView';
import { EquationBox } from '../views/nodes/EquationBox';
import { FieldViewProps } from '../views/nodes/FieldView';
-import { FilterBox } from '../views/nodes/FilterBox';
import { FormattedTextBox } from '../views/nodes/formattedText/FormattedTextBox';
import { FunctionPlotBox } from '../views/nodes/FunctionPlotBox';
import { ImageBox } from '../views/nodes/ImageBox';
@@ -51,6 +50,7 @@ import { LinkDescriptionPopup } from '../views/nodes/LinkDescriptionPopup';
import { LoadingBox } from '../views/nodes/LoadingBox';
import { MapBox } from '../views/nodes/MapBox/MapBox';
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';
@@ -69,14 +69,16 @@ class EmptyBox {
return '';
}
}
-export abstract class FInfo {
+export class FInfo {
description: string = '';
- fieldType?: string;
+ readOnly: boolean = false;
+ fieldType?: string = '';
values?: Field[];
// format?: string; // format to display values (e.g, decimal places, $, etc)
// parse?: ScriptField; // parse a value from a string
- constructor(d: string) {
+ constructor(d: string, readOnly?: boolean) {
this.description = d;
+ this.readOnly = readOnly ?? false;
}
}
class BoolInfo extends FInfo {
@@ -86,16 +88,16 @@ class BoolInfo extends FInfo {
class NumInfo extends FInfo {
fieldType? = 'number';
values?: number[] = [];
- constructor(d: string, values?: number[]) {
- super(d);
+ constructor(d: string, readOnly?: boolean, values?: number[]) {
+ super(d, readOnly);
this.values = values;
}
}
class StrInfo extends FInfo {
fieldType? = 'string';
values?: string[] = [];
- constructor(d: string, values?: string[]) {
- super(d);
+ constructor(d: string, readOnly?: boolean, values?: string[]) {
+ super(d, readOnly);
this.values = values;
}
}
@@ -103,21 +105,38 @@ class DocInfo extends FInfo {
fieldType? = 'Doc';
values?: Doc[] = [];
constructor(d: string, values?: Doc[]) {
- super(d);
+ super(d, true);
this.values = values;
}
}
class DimInfo extends FInfo {
- fieldType? = 'DimUnit';
+ fieldType? = 'enumeration';
values? = [DimUnit.Pixel, DimUnit.Ratio];
+ readOnly = false;
}
class PEInfo extends FInfo {
- fieldType? = 'pointerEvents';
+ fieldType? = 'enumeration';
values? = ['all', 'none'];
+ readOnly = false;
}
class DAInfo extends FInfo {
- fieldType? = 'dropActionType';
- values? = ['alias', 'copy', 'move', 'same', 'proto', 'none'];
+ 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 {
+ fieldType? = 'date';
+ values?: DateField[] = [];
}
type BOOLt = BoolInfo | boolean;
type NUMt = NumInfo | number;
@@ -125,246 +144,268 @@ 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)', [1, 0]);
- system?: BOOLt = new BoolInfo('is this a system created/owned doc');
- type?: STRt = new StrInfo('type of document', Array.from(Object.keys(DocumentType)));
- title?: string;
- _dropAction?: DROPt = new DAInfo("what should happen to this document when it's dropped somewhere else");
- allowOverlayDrop?: BOOLt = new BoolInfo('can documents be dropped onto this document without using dragging title bar or holding down embed key (ctrl)?');
- childDropAction?: DROPt = new DAInfo("what should happen to the source document when it's dropped onto a child of a collection ");
- targetDropAction?: DROPt = new DAInfo('what should happen to the source document when ??? ');
- 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');
- _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');
- _panX?: NUMt = new NumInfo('horizontal pan location of a freeform view');
- _panY?: NUMt = new NumInfo('vertical pan location of a freeform view');
+ z?: NUMt = new NumInfo('whether document is in overlay (1) or not (0)', false, [1, 0]);
+ _dimMagnitude?: NUMt = new NumInfo("magnitude of collectionMulti{row,col} element's width or height");
+ _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)');
- _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");
- _fitWidth?: BOOLt = new BoolInfo('whether document should scale its contents to fit its rendered width or not (e.g., for PDFviews)');
- _fitContentsToBox?: 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 panx,pany and scale parameters of the document so that it be panned/zoomed
- _isPushpin?: boolean; // whether document, when clicked, toggles display of its link target
- _showTitle?: string; // field name to display in header (:hover is an optional suffix)
- _showCaption?: string; // which field to display in the caption area. leave empty to have no caption
- _scrollTop?: number; // scroll location for pdfs
- _noAutoscroll?: boolean; // whether collections autoscroll when this item is dragged
- _chromeHidden?: boolean; // whether the editing chrome for a document is hidden
- _searchDoc?: boolean; // is this a search document (used to change UI for search results in schema view)
- _forceActive?: boolean; // flag to handle pointer events when not selected (or otherwise active)
- _stayInCollection?: boolean; // whether the document should remain in its collection when someone tries to drag and drop it elsewhere
- _raiseWhenDragged?: boolean; // whether a document is brought to front when dragged.
- _hideContextMenu?: boolean; // whether the context menu can be shown
- _viewType?: string; // sub type of a collection
- viewType?: string; // sub type of a collection
- _gridGap?: number; // gap between items in masonry view
- _viewScale?: number; // how much a freeform view has been scaled (zoomed)
- _overflow?: string; // set overflow behavior
- _xMargin?: number; // gap between left edge of document and start of masonry/stacking layouts
- _yMargin?: number; // gap between top edge of dcoument and start of masonry/stacking layouts
- _xPadding?: number;
- _yPadding?: number;
- _itemIndex?: number; // which item index the carousel viewer is showing
- _showSidebar?: boolean; //whether an annotationsidebar should be displayed for text docuemnts
- _singleLine?: boolean; // whether text document is restricted to a single line (carriage returns make new document)
- _minFontSize?: number; // minimum font size for labelBoxes
- _maxFontSize?: number; // maximum font size for labelBoxes
- _columnWidth?: number;
- _columnsHideIfEmpty?: boolean; // whether stacking view column headings should be hidden
- _fontSize?: string;
- _fontFamily?: string;
- _fontWeight?: string;
- _pivotField?: string; // field key used to determine headings for sections in stacking, masonry, pivot views
- _curPage?: number; // current page of a PDF or other? paginated document
- _currentTimecode?: number; // the current timecode of a time-based document (e.g., current time of a video) value is in seconds
- _currentFrame?: number; // the current frame of a frame-based collection (e.g., progressive slide)
- _timecodeToShow?: number; // the time that a document should be displayed (e.g., when an annotation shows up as a video plays)
- _timecodeToHide?: number; // the time that a document should be hidden
- _timelineLabel?: boolean; // whether the document exists on a timeline
- '_carousel-caption-xMargin'?: NUMt = new NumInfo('x margin of caption inside of a carouself collection');
- '_carousel-caption-yMargin'?: NUMt = new NumInfo('y margin of caption inside of a carouself collection');
- 'icon-nativeWidth'?: NUMt = new NumInfo('native width of icon view');
- 'icon-nativeHeight'?: NUMt = new NumInfo('native height of icon view');
- 'dragFactory-count'?: NUMt = new NumInfo('number of items created from a drag button (used for setting title with incrementing index)');
- lat?: number;
- lng?: number;
- infoWindowOpen?: boolean;
- author?: string;
- _layoutKey?: 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)
+ _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');
+
'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.
+ type?: DTYPEt = new DTypeInfo('type of document', true);
+ _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)');
+ _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_showSidebar?: BOOLt = new BoolInfo('whether an annotationsidebar should be displayed for text docuemnts');
+ _layout_showCaption?: string; // which field to display in the caption area. leave empty to have no caption
+
+ _chromeHidden?: BOOLt = new BoolInfo('whether the editing chrome for a document is hidden');
+ _gridGap?: NUMt = new NumInfo('gap between items in masonry view');
+ _xMargin?: NUMt = new NumInfo('gap between left edge of document and start of masonry/stacking layouts');
+ _yMargin?: NUMt = new NumInfo('gap between top edge of dcoument and start of masonry/stacking layouts');
+ _xPadding?: NUMt = new NumInfo('x padding');
+ _yPadding?: NUMt = new NumInfo('y padding');
+ _singleLine?: boolean; // whether label box is restricted to one line of text
+ _createDocOnCR?: boolean; // whether carriage returns and tabs create new text documents
+ _columnWidth?: NUMt = new NumInfo('width of table column');
+ _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.');
+ 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?: 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>;
- dontUndo?: boolean; // whether button clicks should be undoable (this is set to true for Undo/Redo/and sidebar buttons that open the siebar panel)
- description?: string; // added for links
- layout?: string | Doc; // default layout string for a document
- contentPointerEvents?: string; // pointer events allowed for content of a document view. eg. set to "none" in menuSidebar for sharedDocs so that you can select a document, but not interact with its contents
- childLimitHeight?: number; // whether to limit the height of collection children. 0 - means height can be no bigger than width
+ 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>;
- hideLinkButton?: boolean; // whether the blue link counter button should be hidden
- hideDecorationTitle?: boolean;
- hideOpenButton?: boolean;
- hideResizeHandles?: boolean;
- hideDocumentButtonBar?: boolean;
- hideAllLinks?: boolean; // whether all individual blue anchor dots should be hidden
- isTemplateForField?: string; // the field key for which the containing document is a rendering template
- isTemplateDoc?: boolean;
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;
- _isLinkButton?: boolean; // marks a document as a button that will follow its primary link when clicked
- _linkAutoMove?: boolean; // whether link endpoint should move around the edges of a document to make shortest path to other link endpoint
- isFolder?: boolean;
- lastFrame?: number; // the last frame of a frame-based collection (e.g., progressive slide)
- activeFrame?: number; // the active frame of a document in a frame base collection
- appearFrame?: number; // the frame in which the document appears
- presTransition?: number; //the time taken for the transition TO a document
- presDuration?: number; //the duration of the slide in presentation view
- presProgressivize?: boolean;
- borderRounding?: string;
- boxShadow?: string; // box-shadow css string OR "standard" to use dash standard box shadow
+
+ 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');
+
data?: any;
- baseProto?: boolean; // is this a base prototoype
- dontRegisterView?: boolean;
- lookupField?: ScriptField; // script that returns the value of a field. This script is passed the rootDoc, layoutDoc, field, and container of the document. see PresBox.
+ 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
- clipWidth?: number; // percent transition from before to after in comparisonBox
- dockingConfig?: string;
- annotationOn?: Doc;
- isPushpin?: boolean;
- isGroup?: boolean; // whether a collection should use a grouping UI behavior
- _removeDropProperties?: List<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
- _backgroundGridShow?: boolean;
+
+ // 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;
- numBtnType?: string;
- 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;
- flexGap?: number; // Linear view flex gap
+ linearView_IsExpanded?: BOOLt = new BoolInfo('is linear view expanded');
+ linearView_Expandable?: BOOLt = new BoolInfo('can linear view be expanded');
+ 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
- linkRelationship?: string; // type of relatinoship a link represents
- linkDisplay?: boolean; // whether a link line should be dipslayed between the two link anchors
- anchor1?: Doc;
- anchor2?: Doc;
- 'anchor1-useLinkSmallAnchor'?: boolean; // whether anchor1 of a link should use a miniature anchor dot (as when the anchor is a text selection)
- 'anchor2-useLinkSmallAnchor'?: boolean; // whether anchor1 of a link should use a miniature anchor dot (as when the anchor is a text selection)
- ignoreClick?: boolean;
+ link_description?: string; // added for links
+ link_relationship?: string; // type of relatinoship a link represents
+ 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_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
+ 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;
- 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
- strokeWidth?: number;
- freezeChildren?: string; // whether children are now allowed to be added and or removed from a collection
- treeViewHideTitle?: boolean; // whether to hide the top document title of a tree view
- treeViewHideHeaderIfTemplate?: boolean; // whether to hide the header for a document in a tree view only if a childLayoutTemplate is provided (presBox)
- treeViewHideHeader?: boolean; // whether to hide the header for a document in a tree view
- treeViewHideHeaderFields?: boolean; // whether to hide the drop down options for tree view items.
+ 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
+
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
- linkRelationshipList?: List<string>; // for storing different link relationships (when set by user in the link editor)
- linkRelationshipSizes?: List<number>; //stores number of links contained in each relationship
- linkColorList?: List<string>; // colors of links corresponding to specific link relationships
+ 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;
@@ -394,8 +435,8 @@ export namespace Docs {
_yMargin: 10,
nativeDimModifiable: true,
nativeHeightUnfrozen: true,
- forceReflow: true,
- links: '@links(self)',
+ layout_forceReflow: true,
+ defaultDoubleClick: 'ignore',
},
},
],
@@ -403,84 +444,77 @@ export namespace Docs {
DocumentType.SEARCH,
{
layout: { view: SearchBox, dataField: defaultDataKey },
- options: { _width: 400, links: '@links(self)' },
- },
- ],
- [
- DocumentType.FILTER,
- {
- layout: { view: FilterBox, dataField: defaultDataKey },
- options: { _width: 400, links: '@links(self)' },
+ options: { _width: 400 },
},
],
[
DocumentType.COLOR,
{
layout: { view: ColorBox, dataField: defaultDataKey },
- options: { _nativeWidth: 220, _nativeHeight: 300, links: '@links(self)' },
+ options: { _nativeWidth: 220, _nativeHeight: 300 },
},
],
[
DocumentType.IMG,
{
layout: { view: ImageBox, dataField: defaultDataKey },
- options: { links: '@links(self)' },
+ options: { freeform: '' },
},
],
[
DocumentType.WEB,
{
layout: { view: WebBox, dataField: defaultDataKey },
- options: { _height: 300, _fitWidth: true, nativeDimModifiable: true, nativeHeightUnfrozen: true, links: '@links(self)' },
+ options: { _height: 300, _layout_fitWidth: true, nativeDimModifiable: true, nativeHeightUnfrozen: true, waitForDoubleClickToClick: 'always' },
},
],
[
DocumentType.COL,
{
layout: { view: CollectionView, dataField: defaultDataKey },
- options: { _fitWidth: true, _panX: 0, _panY: 0, _viewScale: 1, links: '@links(self)' },
+ options: { _layout_fitWidth: true, freeform: '', _freeform_panX: 0, _freeform_panY: 0, _freeform_scale: 1 },
},
],
[
DocumentType.KVP,
{
layout: { view: KeyValueBox, dataField: defaultDataKey },
- options: { _fitWidth: true, _height: 150 },
+ options: { _layout_fitWidth: true, _height: 150 },
},
],
[
DocumentType.VID,
{
layout: { view: VideoBox, dataField: defaultDataKey },
- options: { _currentTimecode: 0, links: '@links(self)' },
+ options: { _layout_currentTimecode: 0 },
},
],
[
DocumentType.AUDIO,
{
layout: { view: AudioBox, dataField: defaultDataKey },
- options: { _height: 100, backgroundColor: 'lightGray', _fitWidth: true, forceReflow: true, nativeDimModifiable: true, links: '@links(self)' },
+ options: { _height: 100, layout_fitWidth: true, layout_forceReflow: true, nativeDimModifiable: true },
},
],
[
DocumentType.REC,
{
layout: { view: VideoBox, dataField: defaultDataKey },
- options: { _height: 100, backgroundColor: 'pink', links: '@links(self)' },
+ options: { _height: 100, backgroundColor: 'pink' },
},
],
[
DocumentType.PDF,
{
layout: { view: PDFBox, dataField: defaultDataKey },
- options: { _curPage: 1, _fitWidth: true, nativeDimModifiable: true, nativeHeightUnfrozen: true, links: '@links(self)' },
+ options: { _layout_curPage: 1, _layout_fitWidth: true, nativeDimModifiable: true, nativeHeightUnfrozen: true },
},
],
[
DocumentType.MAP,
{
layout: { view: MapBox, dataField: defaultDataKey },
- options: { _height: 600, _width: 800, nativeDimModifiable: true, links: '@links(self)' },
+ options: { _height: 600, _width: 800, nativeDimModifiable: true },
},
],
[
@@ -493,16 +527,17 @@ export namespace Docs {
[
DocumentType.LINK,
{
- layout: { view: LinkBox, dataField: defaultDataKey },
+ layout: { view: LinkBox, dataField: 'link' },
options: {
childDontRegisterViews: true,
- _isLinkButton: true,
+ onClick: FollowLinkScript(),
+ layout_hideLinkAnchors: true,
_height: 150,
- description: '',
- showCaption: 'description',
+ link: '',
+ link_description: '',
+ layout_showCaption: 'link_description',
backgroundColor: 'lightblue', // lightblue is default color for linking dot and link documents text comment area
- links: '@links(self)',
- _removeDropProperties: new List(['isLinkButton']),
+ _dropPropertiesToRemove: new List(['onClick']),
},
},
],
@@ -511,7 +546,7 @@ export namespace Docs {
{
data: new List<Doc>(),
layout: { view: EmptyBox, dataField: defaultDataKey },
- options: { childDropAction: 'alias', title: 'Global Link Database' },
+ options: { title: 'Global Link Database' },
},
],
[
@@ -519,14 +554,14 @@ export namespace Docs {
{
data: new List<Doc>(),
layout: { view: EmptyBox, dataField: defaultDataKey },
- options: { childDropAction: 'alias', title: 'Global Script Database' },
+ options: { title: 'Global Script Database' },
},
],
[
DocumentType.SCRIPTING,
{
layout: { view: ScriptingBox, dataField: defaultDataKey },
- options: { links: '@links(self)' },
+ options: {},
},
],
[
@@ -539,92 +574,93 @@ export namespace Docs {
DocumentType.LABEL,
{
layout: { view: LabelBox, dataField: defaultDataKey },
- options: { links: '@links(self)', _singleLine: true },
+ options: { _singleLine: true },
},
],
[
DocumentType.EQUATION,
{
- layout: { view: EquationBox, dataField: defaultDataKey },
- options: { links: '@links(self)', nativeDimModifiable: true, hideResizeHandles: true, hideDecorationTitle: true },
+ layout: { view: EquationBox, dataField: 'text' },
+ options: { nativeDimModifiable: true, fontSize: '14px', layout_hideResizeHandles: true, layout_hideDecorationTitle: true },
},
],
[
DocumentType.FUNCPLOT,
{
layout: { view: FunctionPlotBox, dataField: defaultDataKey },
- options: { nativeDimModifiable: true, links: '@links(self)' },
+ options: { nativeDimModifiable: true },
},
],
[
DocumentType.BUTTON,
{
layout: { view: LabelBox, dataField: 'onClick' },
- options: { links: '@links(self)' },
+ options: {},
},
],
[
DocumentType.SLIDER,
{
layout: { view: SliderBox, dataField: defaultDataKey },
- options: { links: '@links(self)' },
+ options: {},
},
],
[
DocumentType.PRES,
{
layout: { view: PresBox, dataField: defaultDataKey },
- options: { links: '@links(self)' },
+ options: { defaultDoubleClick: 'ignore', layout_hideLinkAnchors: true },
},
],
[
DocumentType.FONTICON,
{
- layout: { view: FontIconBox, dataField: defaultDataKey },
- options: { hideLinkButton: true, _width: 40, _height: 40, borderRounding: '100%', links: '@links(self)' },
+ layout: { view: FontIconBox, dataField: 'icon' },
+ options: { defaultDoubleClick: 'ignore', waitForDoubleClickToClick: 'never', layout_hideLinkButton: true, _width: 40, _height: 40 },
},
],
[
DocumentType.WEBCAM,
{
layout: { view: RecordingBox, dataField: defaultDataKey },
- options: { links: '@links(self)' },
+ options: {},
},
],
[
DocumentType.PRESELEMENT,
{
layout: { view: PresElementBox, dataField: defaultDataKey },
- options: { title: 'pres element template', _fitWidth: true, _xMargin: 0, isTemplateDoc: true, isTemplateForField: 'data' },
+ options: { title: 'pres element template', _layout_fitWidth: true, _xMargin: 0, isTemplateDoc: true, isTemplateForField: 'data' },
},
],
[
- DocumentType.MARKER,
+ DocumentType.CONFIG,
{
layout: { view: CollectionView, dataField: defaultDataKey },
- options: { links: '@links(self)', hideLinkButton: true, pointerEvents: 'none' },
+ options: { layout_hideLinkButton: true, pointerEvents: 'none', layout_unrendered: true },
},
],
[
DocumentType.INK,
{
// NOTE: this is unused!! ink fields are filled in directly within the InkDocument() method
- layout: { view: InkingStroke, dataField: defaultDataKey },
- options: { links: '@links(self)' },
+ layout: { view: InkingStroke, dataField: 'stroke' },
+ options: {},
},
],
[
DocumentType.SCREENSHOT,
{
layout: { view: ScreenshotBox, dataField: defaultDataKey },
- options: { links: '@links(self)' },
+ options: { nativeDimModifiable: true, nativeHeightUnfrozen: true },
},
],
[
DocumentType.COMPARISON,
{
+ data: '',
layout: { view: ComparisonBox, dataField: defaultDataKey },
- options: { clipWidth: 50, nativeDimModifiable: true, backgroundColor: 'gray', targetDropAction: 'alias', links: '@links(self)' },
+ options: { backgroundColor: 'gray', dropAction: 'move', waitForDoubleClickToClick: 'always' },
},
],
[
@@ -632,28 +668,36 @@ export namespace Docs {
{
data: new List<Doc>(),
layout: { view: EmptyBox, dataField: defaultDataKey },
- options: { childDropAction: 'alias', title: 'Global Group Database' },
+ options: { title: 'Global Group Database' },
},
],
[
DocumentType.GROUP,
{
layout: { view: EmptyBox, dataField: defaultDataKey },
- options: { links: '@links(self)' },
+ options: {},
},
],
[
DocumentType.DATAVIZ,
{
layout: { view: DataVizBox, dataField: defaultDataKey },
- options: { _fitWidth: true, nativeDimModifiable: true, links: '@links(self)' },
+ options: { _layout_fitWidth: true, nativeDimModifiable: true },
},
],
[
DocumentType.LOADING,
{
layout: { view: LoadingBox, dataField: '' },
- options: { _fitWidth: true, _fitHeight: true, nativeDimModifiable: true, links: '@links(self)' },
+ options: { _layout_fitWidth: true, _fitHeight: true, nativeDimModifiable: true },
+ },
+ ],
+ [
+ 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: '' },
},
],
]);
@@ -661,7 +705,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.
@@ -672,8 +716,6 @@ export namespace Docs {
* haven't been initialized, the newly initialized prototype document.
*/
export async function initialize(): Promise<void> {
- ProxyField.initPlugin();
- ComputedField.initPlugin();
// non-guid string ids for each document prototype
const prototypeIds = Object.values(DocumentType)
.filter(type => type !== DocumentType.NONE)
@@ -749,18 +791,17 @@ export namespace Docs {
// synthesize the default options, the type and title from computed values and
// whatever options pertain to this specific prototype
const options: DocumentOptions = {
- system: true,
- _layoutKey: 'layout',
+ isSystem: true,
+ _layout_fieldKey: 'layout',
title,
type,
- baseProto: true,
+ isBaseProto: true,
x: 0,
y: 0,
_width: 300,
...(template.options || {}),
layout: layout.view?.LayoutString(layout.dataField),
data: template.data,
- layout_keyValue: KeyValueBox.LayoutString(''),
};
Object.entries(options).map(pair => {
if (typeof pair[1] === 'string' && pair[1].startsWith('@')) {
@@ -795,24 +836,24 @@ export namespace Docs {
* main document.
*/
function InstanceFromProto(proto: Doc, data: Field | undefined, options: DocumentOptions, delegId?: string, fieldKey: string = 'data', protoId?: string, placeholderDoc?: Doc) {
- const viewKeys = ['x', 'y', 'system']; // keys that should be addded to the view document even though they don't begin with an "_"
+ const viewKeys = ['x', 'y', 'isSystem']; // keys that should be addded to the view document even though they don't begin with an "_"
const { omit: dataProps, extract: viewProps } = OmitKeys(options, viewKeys, '^_');
- dataProps['acl-Override'] = 'None';
+ // dataProps['acl-Override'] = SharingPermissions.Unset;
dataProps['acl-Public'] = options['acl-Public'] ? options['acl-Public'] : Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Augment;
- dataProps.system = viewProps.system;
- dataProps.isPrototype = true;
+ dataProps.isSystem = viewProps.isSystem;
+ dataProps.isDataDoc = true;
dataProps.author = Doc.CurrentUserEmail;
- dataProps.creationDate = new DateField();
+ dataProps.author_date = new DateField();
if (fieldKey) {
- dataProps[`${fieldKey}-lastModified`] = new DateField();
+ dataProps[`${fieldKey}_modificationDate`] = new DateField();
dataProps[fieldKey] = data;
// so that the list of annotations is already initialised, prevents issues in addonly.
// without this, if a doc has no annotations but the user has AddOnly privileges, they won't be able to add an annotation because they would have needed to create the field's list which they don't have permissions to do.
- dataProps[fieldKey + '-annotations'] = new List<Doc>();
- dataProps[fieldKey + '-sidebar'] = new List<Doc>();
+ dataProps[fieldKey + '_annotations'] = new List<Doc>();
+ dataProps[fieldKey + '_sidebar'] = new List<Doc>();
}
// users placeholderDoc as proto if it exists
@@ -824,7 +865,7 @@ export namespace Docs {
const viewFirstProps: { [id: string]: any } = {};
viewFirstProps['acl-Public'] = options['_acl-Public'] ? options['_acl-Public'] : Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Augment;
- viewFirstProps['acl-Override'] = 'None';
+ // viewFirstProps['acl-Override'] = SharingPermissions.Unset;
viewFirstProps.author = Doc.CurrentUserEmail;
let viewDoc: Doc;
// determines whether viewDoc should be created using placeholder Doc or default
@@ -836,13 +877,11 @@ export namespace Docs {
viewDoc = Doc.assign(Doc.MakeDelegate(dataDoc, delegId), viewFirstProps, true, true);
}
Doc.assign(viewDoc, viewProps, true, true);
- ![DocumentType.LINK, DocumentType.MARKER, DocumentType.LABEL].includes(viewDoc.type as any) && DocUtils.MakeLinkToActiveAudio(() => viewDoc);
+ if (![DocumentType.LINK, DocumentType.CONFIG, DocumentType.LABEL].includes(viewDoc.type as any)) {
+ DocUtils.MakeLinkToActiveAudio(() => viewDoc);
+ }
- !Doc.IsSystem(dataDoc) &&
- ![DocumentType.MARKER, DocumentType.KVP, DocumentType.LINK, DocumentType.LINKANCHOR].includes(proto.type as any) &&
- !dataDoc.isFolder &&
- !dataProps.annotationOn &&
- Doc.AddDocToList(Doc.MyFileOrphans, undefined, dataDoc);
+ Doc.AddFileOrphan(dataDoc);
updateCachedAcls(dataDoc);
updateCachedAcls(viewDoc);
@@ -852,7 +891,7 @@ export namespace Docs {
export function ImageDocument(url: string | ImageField, options: DocumentOptions = {}, overwriteDoc?: Doc) {
const imgField = url instanceof ImageField ? url : new ImageField(url);
- return InstanceFromProto(Prototypes.get(DocumentType.IMG), imgField, { title: basename(imgField.url.href), ...options }, undefined, undefined, undefined, overwriteDoc);
+ return InstanceFromProto(Prototypes.get(DocumentType.IMG), imgField, { _nativeDimModifiable: false, _nativeHeightUnfrozen: false, title: basename(imgField.url.href), ...options }, undefined, undefined, undefined, overwriteDoc);
}
export function PresDocument(options: DocumentOptions = {}) {
@@ -880,19 +919,11 @@ export namespace Docs {
}
export function ComparisonDocument(options: DocumentOptions = { title: 'Comparison Box' }) {
- return InstanceFromProto(Prototypes.get(DocumentType.COMPARISON), '', options);
+ return InstanceFromProto(Prototypes.get(DocumentType.COMPARISON), undefined, options);
}
export function AudioDocument(url: string, options: DocumentOptions = {}, overwriteDoc?: Doc) {
- return InstanceFromProto(
- Prototypes.get(DocumentType.AUDIO),
- new AudioField(url),
- { ...options, backgroundColor: ComputedField.MakeFunction("this._mediaState === 'playing' ? 'green':'gray'") as any },
- undefined,
- undefined,
- undefined,
- overwriteDoc
- );
+ return InstanceFromProto(Prototypes.get(DocumentType.AUDIO), new AudioField(url), options, undefined, undefined, undefined, overwriteDoc);
}
export function RecordingDocument(url: string, options: DocumentOptions = {}) {
@@ -936,16 +967,17 @@ export namespace Docs {
return InstanceFromProto(Prototypes.get(DocumentType.RTF), field, options, undefined, fieldKey);
}
- export function LinkDocument(source: { doc: Doc; ctx?: Doc }, target: { doc: Doc; ctx?: Doc }, options: DocumentOptions = {}, id?: string) {
+ export function LinkDocument(source: Doc, target: Doc, options: DocumentOptions = {}, id?: string) {
const linkDoc = InstanceFromProto(
Prototypes.get(DocumentType.LINK),
undefined,
{
- anchor1: source.doc,
- anchor2: target.doc,
+ link_anchor_1: source,
+ link_anchor_2: target,
...options,
},
- id
+ id,
+ 'link'
);
LinkManager.Instance.addLink(linkDoc);
@@ -953,49 +985,35 @@ export namespace Docs {
return linkDoc;
}
- export function InkDocument(
- color: string,
- tool: string,
- strokeWidth: number,
- strokeBezier: string,
- fillColor: string,
- arrowStart: string,
- arrowEnd: string,
- dash: string,
- points: PointData[],
- isInkMask: boolean,
- options: DocumentOptions = {}
- ) {
+ export function InkDocument(color: string, strokeWidth: number, stroke_bezier: string, fillColor: string, arrowStart: string, arrowEnd: string, dash: string, points: PointData[], isInkMask: boolean, options: DocumentOptions = {}) {
const I = new Doc();
I[Initializing] = true;
- I.isInkMask = isInkMask;
I.type = DocumentType.INK;
- I.layout = InkingStroke.LayoutString('data');
+ I.layout = InkingStroke.LayoutString('stroke');
+ I.layout_fitWidth = true;
+ I.layout_hideDecorationTitle = true; // don't show title when selected
+ // I.layout_hideOpenButton = true; // don't show open full screen button when selected
I.color = color;
- I.hideDecorationTitle = true; // don't show title when selected
- // I.hideOpenButton = true; // don't show open full screen button when selected
I.fillColor = fillColor;
- I.strokeWidth = strokeWidth;
- I.strokeBezier = strokeBezier;
- I.strokeStartMarker = arrowStart;
- I.strokeEndMarker = arrowEnd;
- I.strokeDash = dash;
- I.tool = tool;
- I.fitWidth = true;
- I['text-align'] = 'center';
+ I.stroke = new InkField(points);
+ I.stroke_width = strokeWidth;
+ I.stroke_bezier = stroke_bezier;
+ I.stroke_startMarker = arrowStart;
+ I.stroke_endMarker = arrowEnd;
+ I.stroke_dash = dash;
+ I.stroke_isInkMask = isInkMask;
+ I.text_align = 'center';
I.title = 'ink';
I.x = options.x as number;
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.data = new InkField(points);
- I.creationDate = new DateField();
+ I.defaultDoubleClick = 'click';
+ I.author_date = new DateField();
I['acl-Public'] = Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Augment;
- I['acl-Override'] = 'None';
- I.links = ComputedField.MakeFunction('links(self)');
+ //I['acl-Override'] = SharingPermissions.Unset;
I[Initializing] = false;
return I;
}
@@ -1026,69 +1044,65 @@ export namespace Docs {
return InstanceFromProto(Prototypes.get(DocumentType.MAP), new List(documents), options);
}
- export function MapMarkerDocument(lat: number, lng: number, infoWindowOpen: boolean, documents: Array<Doc>, options: DocumentOptions, id?: string) {
- return InstanceFromProto(Prototypes.get(DocumentType.MARKER), new List(documents), { lat, lng, infoWindowOpen, ...options }, id);
- }
-
- export function KVPDocument(document: Doc, options: DocumentOptions = {}) {
- return InstanceFromProto(Prototypes.get(DocumentType.KVP), document, { title: document.title + '.kvp', ...options });
- }
+ // shouldn't ever need to create a KVP document-- instead set the LayoutTemplateString to be a KeyValueBox for the DocumentView (see addDocTab in TabDocView)
+ // export function KVPDocument(document: Doc, options: DocumentOptions = {}) {
+ // return InstanceFromProto(Prototypes.get(DocumentType.KVP), document, { title: document.title + '.kvp', ...options });
+ // }
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.context = inst));
+ const inst = InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _xPadding: 20, _yPadding: 20, ...options, _type_collection: CollectionViewType.Freeform }, id);
+ documents.forEach(d => (d.embedContainer = inst));
return inst;
}
- export function WebanchorDocument(url?: string, options: DocumentOptions = {}, id?: string) {
- return InstanceFromProto(Prototypes.get(DocumentType.MARKER), url, options, id);
+ export function ConfigDocument(options: DocumentOptions, id?: string) {
+ return InstanceFromProto(Prototypes.get(DocumentType.CONFIG), options?.data, options, id);
}
- export function TextanchorDocument(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 HTMLAnchorDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
- return InstanceFromProto(Prototypes.get(DocumentType.MARKER), new List(documents), 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 PileDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
- return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _overflow: 'visible', _forceActive: true, _noAutoscroll: true, ...options, _viewType: CollectionViewType.Pile }, id);
+ return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { dropAction: 'move', _forceActive: true, _freeform_noZoom: true, _freeform_noAutoPan: true, ...options, _type_collection: CollectionViewType.Pile }, id);
}
export function LinearDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
- 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
@@ -1096,14 +1110,14 @@ 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) {
@@ -1111,7 +1125,7 @@ export namespace Docs {
}
export function EquationDocument(options?: DocumentOptions) {
- return InstanceFromProto(Prototypes.get(DocumentType.EQUATION), undefined, { ...(options || {}) });
+ return InstanceFromProto(Prototypes.get(DocumentType.EQUATION), undefined, { ...(options || {}) }, undefined, 'text');
}
export function FunctionPlotDocument(documents: Array<Doc>, options?: DocumentOptions) {
@@ -1142,7 +1156,7 @@ 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);
+ return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { treeViewFreezeChildren: 'remove|add', ...options, _type_collection: CollectionViewType.Docking, dockingConfig: config }, id);
}
export function DirectoryImportDocument(options: DocumentOptions = {}) {
@@ -1164,36 +1178,41 @@ export namespace Docs {
},
],
};
- return DockDocument(
+ const doc = DockDocument(
configs.map(c => c.doc),
JSON.stringify(layoutConfig),
options,
id
);
+ configs.map(c => (c.doc.embedContainer = doc));
+ 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 viewSpecScript
- * Given a list of docs and docFilters, @returns the list of Docs that match those filters
+ * @param childFilters
+ * @param childFiltersByRanges
+ * @param parentCollection
+ * Given a list of docs and childFilters, @returns the list of Docs that match those filters
*/
- export function FilterDocs(docs: Doc[], docFilters: string[], docRangeFilters: string[], viewSpecScript?: ScriptField, parentCollection?: Doc) {
- const childDocs = viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs;
- if (!docFilters?.length && !docRangeFilters?.length) {
+ 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 => {
+ childFilters.forEach(filter => {
const fields = filter.split(':');
const key = fields[0];
const value = fields[1];
@@ -1204,7 +1223,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
@@ -1239,7 +1258,7 @@ export namespace DocUtils {
? true
: matches.some(value => {
if (facetKey.startsWith('*')) {
- // fields starting with a '*' are used to match families of related fields. ie, *lastModified will match text-lastModified, data-lastModified, etc
+ // fields starting with a '*' are used to match families of related fields. ie, *modificationDate will match text_modificationDate, data_modificationDate, etc
const allKeys = Array.from(Object.keys(d));
allKeys.push(...Object.keys(Doc.GetProto(d)));
const keys = allKeys.filter(key => key.includes(facetKey.substring(1)));
@@ -1248,7 +1267,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?.currentFilter as Doc)?.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
@@ -1256,14 +1275,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?.currentFilter as Doc)?.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?")
@@ -1274,58 +1293,19 @@ export namespace DocUtils {
return rangeFilteredDocs;
}
- export function Publish(promoteDoc: Doc, targetID: string, addDoc: any, remDoc: any) {
- targetID = targetID.replace(/^-/, '').replace(/\([0-9]*\)$/, '');
- DocServer.GetRefField(targetID).then(doc => {
- if (promoteDoc !== doc) {
- let copy = doc as Doc;
- if (copy) {
- Doc.Overwrite(promoteDoc, copy, true);
- } else {
- copy = Doc.MakeCopy(promoteDoc, true, targetID);
- }
- !doc && (copy.title = undefined) && (Doc.GetProto(copy).title = targetID);
- addDoc && addDoc(copy);
- remDoc && remDoc(promoteDoc);
- if (!doc) {
- DocListCastAsync(promoteDoc.links).then(links => {
- links &&
- links.map(async link => {
- if (link) {
- const a1 = await Cast(link.anchor1, Doc);
- if (a1 && Doc.AreProtosEqual(a1, promoteDoc)) link.anchor1 = copy;
- const a2 = await Cast(link.anchor2, Doc);
- if (a2 && Doc.AreProtosEqual(a2, promoteDoc)) link.anchor2 = copy;
- LinkManager.Instance.deleteLink(link);
- LinkManager.Instance.addLink(link);
- }
- });
- });
- }
- }
- });
- }
-
- export function DefaultFocus(doc: Doc, options: DocFocusOptions) {
- options?.afterFocus?.(false);
- }
-
- export let ActiveRecordings: { props: FieldViewProps; getAnchor: () => Doc }[] = [];
+ export let ActiveRecordings: { props: FieldViewProps; getAnchor: (addAsAnnotation: boolean) => Doc }[] = [];
export function MakeLinkToActiveAudio(getSourceDoc: () => Doc | undefined, broadcastEvent = true) {
broadcastEvent && runInAction(() => (DocumentManager.Instance.RecordingEvent = DocumentManager.Instance.RecordingEvent + 1));
return DocUtils.ActiveRecordings.map(audio => {
const sourceDoc = getSourceDoc();
- const link = sourceDoc && DocUtils.MakeLink({ doc: sourceDoc }, { doc: audio.getAnchor() || audio.props.Document }, 'recording annotation:linked recording', 'recording timeline');
- link && (link.followLinkLocation = 'add:right');
- return link;
+ 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: Doc }, target: { doc: Doc }, linkRelationship: string = '', description: string = '', id?: string, allowParCollectionLink?: boolean, showPopup?: number[]) {
- if (!linkRelationship) linkRelationship = target.doc.type === DocumentType.RTF ? 'Commentary:Comments On' : 'link';
- const sv = DocumentManager.Instance.getDocumentView(source.doc);
- if (!allowParCollectionLink && sv?.props.ContainingCollectionDoc === target.doc) return;
+ export function MakeLink(source: Doc, target: Doc, linkSettings: { link_relationship?: string; link_description?: string; link_displayLine?: boolean }, id?: string, showPopup?: number[]) {
+ if (!linkSettings.link_relationship) linkSettings.link_relationship = target.type === DocumentType.RTF ? 'Commentary:Comments On' : 'link';
+ const sv = DocumentManager.Instance.getDocumentView(source);
if (target.doc === Doc.UserDoc()) return undefined;
const makeLink = action((linkDoc: Doc, showPopup?: number[]) => {
@@ -1364,18 +1344,17 @@ export namespace DocUtils {
source,
target,
{
- title: ComputedField.MakeFunction('generateLinkTitle(self)') as any,
- 'anchor1-useLinkSmallAnchor': source.doc.useLinkSmallAnchor ? true : undefined,
- 'anchor2-useLinkSmallAnchor': target.doc.useLinkSmallAnchor ? true : undefined,
'acl-Public': SharingPermissions.Augment,
'_acl-Public': SharingPermissions.Augment,
- linkDisplay: true,
- _hidden: true,
- _linkAutoMove: true,
- linkRelationship,
- _showCaption: 'description',
- _showTitle: 'linkRelationship',
- description,
+ title: ComputedField.MakeFunction('generateLinkTitle(self)') as any,
+ 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,
+ link_autoMoveAnchors: true,
+ _layout_showCaption: 'link_description',
+ _layout_showTitle: 'link_relationship',
},
id
),
@@ -1389,11 +1368,12 @@ export namespace DocUtils {
const script = scripts[key];
if (ScriptCast(doc[key])?.script.originalScript !== scripts[key] && script) {
doc[key] = ScriptField.MakeScript(script, {
+ self: Doc.name,
+ this: Doc.name,
dragData: DragManager.DocumentDragData.name,
value: 'any',
_readOnly_: 'boolean',
scriptContext: 'any',
- thisContainer: Doc.name,
documentView: Doc.name,
heading: Doc.name,
checked: 'boolean',
@@ -1405,12 +1385,15 @@ export namespace DocUtils {
}
});
funcs &&
- Object.keys(funcs).map(key => {
- const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
- if (ScriptCast(cfield)?.script.originalScript !== funcs[key]) {
- doc[key] = funcs[key] ? ComputedField.MakeFunction(funcs[key], { dragData: DragManager.DocumentDragData.name }, { _readOnly_: true }) : undefined;
- }
- });
+ Object.keys(funcs)
+ .filter(key => !key.endsWith('-setter'))
+ .map(key => {
+ const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
+ if (ScriptCast(cfield)?.script.originalScript !== funcs[key]) {
+ const setFunc = Cast(funcs[key + '-setter'], 'string', null);
+ doc[key] = funcs[key] ? ComputedField.MakeFunction(funcs[key], { dragData: DragManager.DocumentDragData.name }, { _readOnly_: true }, setFunc) : undefined;
+ }
+ });
return doc;
}
export function AssignOpts(doc: Doc | undefined, reqdOpts: DocumentOptions, items?: Doc[]) {
@@ -1438,41 +1421,33 @@ export namespace DocUtils {
export function DocumentFromField(target: Doc, fieldKey: string, proto?: Doc, options?: DocumentOptions): Doc | undefined {
let created: Doc | undefined;
- let layout: ((fieldKey: string) => string) | undefined;
const field = target[fieldKey];
- const resolved = options || {};
+ const resolved = options ?? {};
if (field instanceof ImageField) {
created = Docs.Create.ImageDocument(field.url.href, resolved);
- layout = ImageBox.LayoutString;
+ created.layout = ImageBox.LayoutString(fieldKey);
} else if (field instanceof Doc) {
created = field;
} else if (field instanceof VideoField) {
created = Docs.Create.VideoDocument(field.url.href, resolved);
- layout = VideoBox.LayoutString;
+ created.layout = VideoBox.LayoutString(fieldKey);
} else if (field instanceof PdfField) {
created = Docs.Create.PdfDocument(field.url.href, resolved);
- layout = PDFBox.LayoutString;
+ created.layout = PDFBox.LayoutString(fieldKey);
} else if (field instanceof AudioField) {
created = Docs.Create.AudioDocument(field.url.href, resolved);
- layout = AudioBox.LayoutString;
- } else if (field instanceof RecordingField) {
- created = Docs.Create.RecordingDocument(field.url.href, resolved);
- layout = RecordingBox.LayoutString;
+ created.layout = AudioBox.LayoutString(fieldKey);
} else if (field instanceof InkField) {
- created = Docs.Create.InkDocument(ActiveInkColor(), Doc.ActiveTool, ActiveInkWidth(), ActiveInkBezierApprox(), ActiveFillColor(), ActiveArrowStart(), ActiveArrowEnd(), ActiveDash(), field.inkData, ActiveIsInkMask(), resolved);
- layout = InkingStroke.LayoutString;
+ created = Docs.Create.InkDocument(ActiveInkColor(), ActiveInkWidth(), ActiveInkBezierApprox(), ActiveFillColor(), ActiveArrowStart(), ActiveArrowEnd(), ActiveDash(), field.inkData, ActiveIsInkMask(), resolved);
+ created.layout = InkingStroke.LayoutString(fieldKey);
} else if (field instanceof List && field[0] instanceof Doc) {
created = Docs.Create.StackingDocument(DocListCast(field), resolved);
- layout = CollectionView.LayoutString;
- } else if (field instanceof MapField) {
- created = Docs.Create.MapDocument(DocListCast(field), resolved);
- layout = MapBox.LayoutString;
+ created.layout = CollectionView.LayoutString(fieldKey);
} else {
- created = Docs.Create.TextDocument('', { ...{ _width: 200, _height: 25, _autoHeight: true }, ...resolved });
- layout = FormattedTextBox.LayoutString;
+ created = Docs.Create.TextDocument('', { ...{ _width: 200, _height: 25, _layout_autoHeight: true }, ...resolved });
+ created.layout = FormattedTextBox.LayoutString(fieldKey);
}
if (created) {
- created.layout = layout?.(fieldKey);
created.title = fieldKey;
proto && created.proto && (created.proto = Doc.GetProto(proto));
}
@@ -1523,12 +1498,12 @@ export namespace DocUtils {
const id = s[s.length - 1];
return DocServer.GetRefField(id).then(field => {
if (field instanceof Doc) {
- const alias = Doc.MakeAlias(field);
- alias.x = (options.x as number) || 0;
- alias.y = (options.y as number) || 0;
- alias._width = (options._width as number) || 300;
- alias._height = (options._height as number) || (options._width as number) || 300;
- return alias;
+ const embedding = Doc.MakeEmbedding(field);
+ embedding.x = (options.x as number) || 0;
+ embedding.y = (options.y as number) || 0;
+ embedding._width = (options._width as number) || 300;
+ embedding._height = (options._height as number) || (options._width as number) || 300;
+ return embedding;
}
return undefined;
});
@@ -1544,23 +1519,23 @@ 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,
y,
- _autoHeight: note._autoHeight !== false,
- title: StrCast(note.title) + '#' + (note.aliasCount = NumCast(note.aliasCount) + 1),
+ _layout_autoHeight: note._layout_autoHeight !== false,
+ title: StrCast(note.title) + '#' + (note.embeddingCount = NumCast(note.embeddingCount) + 1),
});
- textDoc.layoutKey = 'layout_' + note.title;
- textDoc[textDoc.layoutKey] = note;
+ textDoc.layout_fieldKey = 'layout_' + note.title;
+ textDoc[textDoc.layout_fieldKey] = note;
if (pivotField) {
textDoc[pivotField] = pivotValue;
}
docTextAdder(textDoc);
- }),
+ }, 'create quick note'),
icon: StrCast(note.icon) as IconProp,
})) as ContextMenuProps[],
icon: 'sticky-note',
@@ -1568,10 +1543,10 @@ export namespace DocUtils {
const documentList: ContextMenuProps[] = DocListCast(DocListCast(Doc.MyTools?.data)[0]?.data)
.filter(btnDoc => !btnDoc.hidden)
.map(btnDoc => Cast(btnDoc?.dragFactory, Doc, null))
- .filter(doc => doc && doc !== Doc.UserDoc().emptyTrail)
+ .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;
@@ -1584,7 +1559,7 @@ export namespace DocUtils {
}
docAdder?.(newDoc);
}
- }),
+ }, StrCast(dragDoc.title)),
icon: Doc.toIcon(dragDoc),
})) as ContextMenuProps[];
ContextMenu.Instance.addItem({
@@ -1596,7 +1571,7 @@ export namespace DocUtils {
export function makeCustomViewClicked(doc: Doc, creator: Opt<(documents: Array<Doc>, options: DocumentOptions, id?: string) => Doc>, templateSignature: string = 'custom', docLayoutTemplate?: Doc) {
const batch = UndoManager.StartBatch('makeCustomViewClicked');
runInAction(() => {
- doc.layoutKey = 'layout_' + templateSignature;
+ doc.layout_fieldKey = 'layout_' + templateSignature;
createCustomView(doc, creator, templateSignature, docLayoutTemplate);
});
batch.end();
@@ -1604,10 +1579,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().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)
@@ -1627,7 +1602,7 @@ export namespace DocUtils {
const customName = 'layout_' + templateSignature;
const _width = NumCast(doc._width);
const _height = NumCast(doc._height);
- const options = { title: 'data', backgroundColor: StrCast(doc.backgroundColor), _autoHeight: true, _width, x: -_width / 2, y: -_height / 2, _showSidebar: false };
+ const options = { title: 'data', backgroundColor: StrCast(doc.backgroundColor), _layout_autoHeight: true, _width, x: -_width / 2, y: -_height / 2, _layout_showSidebar: false };
if (docLayoutTemplate) {
if (docLayoutTemplate !== doc[customName]) {
@@ -1658,9 +1633,9 @@ export namespace DocUtils {
}
}
export function iconify(doc: Doc) {
- const layoutKey = Cast(doc.layoutKey, 'string', null);
+ const layout_fieldKey = Cast(doc.layout_fieldKey, 'string', null);
DocUtils.makeCustomViewClicked(doc, Docs.Create.StackingDocument, 'icon', undefined);
- if (layoutKey && layoutKey !== 'layout' && layoutKey !== 'layout_icon') doc.deiconifyLayout = layoutKey.replace('layout_', '');
+ if (layout_fieldKey && layout_fieldKey !== 'layout' && layout_fieldKey !== 'layout_icon') doc.deiconifyLayout = layout_fieldKey.replace('layout_', '');
}
export function pileup(docList: Doc[], x?: number, y?: number, size: number = 55, create: boolean = true) {
@@ -1689,7 +1664,7 @@ export namespace DocUtils {
});
});
if (create) {
- const newCollection = Docs.Create.PileDocument(docList, { title: 'pileup', x: (x || 0) - size, y: (y || 0) - size, _width: size * 2, _height: size * 2 });
+ const newCollection = Docs.Create.PileDocument(docList, { title: 'pileup', x: (x || 0) - size, y: (y || 0) - size, _width: size * 2, _height: size * 2, dragWhenActive: true });
newCollection.x = NumCast(newCollection.x) + NumCast(newCollection._width) / 2 - size;
newCollection.y = NumCast(newCollection.y) + NumCast(newCollection._height) / 2 - size;
newCollection._width = newCollection._height = size * 2;
@@ -1699,27 +1674,28 @@ export namespace DocUtils {
}
export function LeavePushpin(doc: Doc, annotationField: string) {
- if (doc.isPushpin) return undefined;
- const context = Cast(doc.context, Doc, null) ?? Cast(doc.annotationOn, Doc, null);
- const hasContextAnchor = DocListCast(doc.links).some(l => (l.anchor2 === doc && Cast(l.anchor1, Doc, null)?.annotationOn === context) || (l.anchor1 === doc && Cast(l.anchor2, Doc, null)?.annotationOn === context));
+ if (doc.followLinkToggle) return undefined;
+ const context = Cast(doc.embedContainer, Doc, null) ?? Cast(doc.annotationOn, Doc, null);
+ const hasContextAnchor = LinkManager.Links(doc).some(l => (l.link_anchor_2 === doc && Cast(l.link_anchor_1, Doc, null)?.annotationOn === context) || (l.link_anchor_1 === doc && Cast(l.link_anchor_2, Doc, null)?.annotationOn === context));
if (context && !hasContextAnchor && (context.type === DocumentType.VID || context.type === DocumentType.WEB || context.type === DocumentType.PDF || context.type === DocumentType.IMG)) {
const pushpin = Docs.Create.FontIconDocument({
title: 'pushpin',
- label: '',
+ icon_label: '',
annotationOn: Cast(doc.annotationOn, Doc, null),
- isPushpin: true,
+ followLinkToggle: true,
icon: 'map-pin',
x: Cast(doc.x, 'number', null),
y: Cast(doc.y, 'number', null),
backgroundColor: '#ACCEF7',
+ layout_hideAllLinks: true,
_width: 15,
_height: 15,
_xPadding: 0,
- _isLinkButton: true,
+ onClick: FollowLinkScript(),
_timecodeToShow: Cast(doc._timecodeToShow, 'number', null),
});
Doc.AddDocToList(context, annotationField, pushpin);
- const pushpinLink = DocUtils.MakeLink({ doc: pushpin }, { doc: doc }, 'pushpin', '');
+ const pushpinLink = DocUtils.MakeLink(pushpin, doc, { link_relationship: 'pushpin' }, '');
doc._timecodeToShow = undefined;
return pushpin;
}
@@ -1753,32 +1729,28 @@ export namespace DocUtils {
return dd;
}
- async function processFileupload(generatedDocuments: Doc[], name: string, type: string, result: Error | Upload.FileInformation, options: DocumentOptions, rootDoc?: Doc) {
+ async function processFileupload(generatedDocuments: Doc[], name: string, type: string, result: Error | Upload.FileInformation, options: DocumentOptions, overwriteDoc?: Doc) {
if (result instanceof Error) {
alert(`Upload failed: ${result.message}`);
return;
}
const full = { ...options, _width: 400, title: name };
const pathname = Utils.prepend(result.accessPaths.agnostic.client);
- const doc = await DocUtils.DocumentFromType(type, pathname, full, rootDoc);
+ const doc = await DocUtils.DocumentFromType(type, pathname, full, overwriteDoc);
if (doc) {
const proto = Doc.GetProto(doc);
proto.text = result.rawText;
- proto.fileUpload = pathname
- .replace(/.*\//, '')
- .replace('upload_', '')
- .replace(/\.[a-z0-9]*$/, '');
if (Upload.isImageInformation(result)) {
const maxNativeDim = Math.min(Math.max(result.nativeHeight, result.nativeWidth), defaultNativeImageDim);
const exifRotation = StrCast((result.exifData?.data as any)?.Orientation).toLowerCase();
proto['data-nativeOrientation'] = result.exifData?.data?.image?.Orientation ?? (exifRotation.includes('rotate 90') || exifRotation.includes('rotate 270') ? 5 : undefined);
- proto['data-nativeWidth'] = result.nativeWidth < result.nativeHeight ? (maxNativeDim * result.nativeWidth) / result.nativeHeight : maxNativeDim;
- proto['data-nativeHeight'] = result.nativeWidth < result.nativeHeight ? maxNativeDim : maxNativeDim / (result.nativeWidth / result.nativeHeight);
+ proto['data_nativeWidth'] = result.nativeWidth < result.nativeHeight ? (maxNativeDim * result.nativeWidth) / result.nativeHeight : maxNativeDim;
+ proto['data_nativeHeight'] = result.nativeWidth < result.nativeHeight ? maxNativeDim : maxNativeDim / (result.nativeWidth / result.nativeHeight);
if (NumCast(proto['data-nativeOrientation']) >= 5) {
- proto['data-nativeHeight'] = result.nativeWidth < result.nativeHeight ? (maxNativeDim * result.nativeWidth) / result.nativeHeight : maxNativeDim;
- proto['data-nativeWidth'] = result.nativeWidth < result.nativeHeight ? maxNativeDim : maxNativeDim / (result.nativeWidth / result.nativeHeight);
+ proto['data_nativeHeight'] = result.nativeWidth < result.nativeHeight ? (maxNativeDim * result.nativeWidth) / result.nativeHeight : maxNativeDim;
+ proto['data_nativeWidth'] = result.nativeWidth < result.nativeHeight ? maxNativeDim : maxNativeDim / (result.nativeWidth / result.nativeHeight);
}
- proto.contentSize = result.contentSize;
+ proto.data_contentSize = result.contentSize;
// exif gps data coordinates are stored in DMS (Degrees Minutes Seconds), the following operation converts that to decimal coordinates
const latitude = result.exifData?.data?.GPSLatitude;
const latitudeDirection = result.exifData?.data?.GPSLatitudeRef;
@@ -1790,10 +1762,12 @@ export namespace DocUtils {
}
}
if (Upload.isVideoInformation(result)) {
- proto['data-duration'] = result.duration;
+ proto.data_duration = result.duration;
}
- if (rootDoc) {
- Doc.removeCurrentlyLoading(rootDoc);
+ if (overwriteDoc) {
+ Doc.removeCurrentlyLoading(overwriteDoc);
+ // loading doc icons are just labels. so any icon views of loading docs need to be replaced with the proper icon view.
+ DocumentManager.Instance.getAllDocumentViews(overwriteDoc).forEach(dv => StrCast(dv.rootDoc.layout_fieldKey) === 'layout_icon' && dv.iconify(() => dv.iconify()));
}
generatedDocuments.push(doc);
}
@@ -1804,21 +1778,22 @@ export namespace DocUtils {
_xMargin: noMargins ? 0 : undefined,
_yMargin: noMargins ? 0 : undefined,
annotationOn,
- docMaxAutoHeight: maxHeight,
- backgroundColor: backgroundColor,
+ layout_maxAutoHeight: maxHeight,
+ backgroundColor,
_width: width || 200,
_height: 35,
x: x,
y: y,
- _fitWidth: true,
- _autoHeight: true,
+ _layout_fitWidth: true,
+ _layout_autoHeight: true,
+ _layout_enableAltContentUI: BoolCast(Doc.UserDoc().defaultToFlashcards),
title,
});
const template = Doc.UserDoc().defaultTextLayout;
if (template instanceof Doc) {
tbox._width = NumCast(template._width);
- tbox.layoutKey = 'layout_' + StrCast(template.title);
- Doc.GetProto(tbox)[StrCast(tbox.layoutKey)] = template;
+ tbox.layout_fieldKey = 'layout_' + StrCast(template.title);
+ Doc.GetProto(tbox)[StrCast(tbox.layout_fieldKey)] = template;
}
return tbox;
}
@@ -1840,9 +1815,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,
@@ -1854,7 +1841,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,
@@ -1872,10 +1860,10 @@ 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.AddDocToList(Doc.MyFileOrphans, 'data', 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);
+ 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);
@@ -1884,31 +1872,25 @@ export namespace DocUtils {
}
export function delegateDragFactory(dragFactory: Doc) {
const ndoc = Doc.MakeDelegateWithProto(dragFactory);
- if (ndoc && dragFactory['dragFactory-count'] !== undefined) {
- dragFactory['dragFactory-count'] = NumCast(dragFactory['dragFactory-count']) + 1;
- Doc.GetProto(ndoc).title = ndoc.title + ' ' + NumCast(dragFactory['dragFactory-count']).toString();
+ if (ndoc && dragFactory['dragFactory_count'] !== undefined) {
+ dragFactory['dragFactory_count'] = NumCast(dragFactory['dragFactory_count']) + 1;
+ Doc.GetProto(ndoc).title = ndoc.title + ' ' + NumCast(dragFactory['dragFactory_count']).toString();
}
return ndoc;
}
}
ScriptingGlobals.add('Docs', Docs);
-ScriptingGlobals.add(function copyDragFactory(dragFactory: Doc) {
- return DocUtils.copyDragFactory(dragFactory);
-});
-ScriptingGlobals.add(function delegateDragFactory(dragFactory: Doc) {
- return DocUtils.delegateDragFactory(dragFactory);
+ScriptingGlobals.add(function copyDragFactory(dragFactory: Doc, asDelegate?: boolean) {
+ return dragFactory instanceof Doc ? (asDelegate ? DocUtils.delegateDragFactory(dragFactory) : DocUtils.copyDragFactory(dragFactory)) : dragFactory;
});
ScriptingGlobals.add(function makeDelegate(proto: any) {
const d = Docs.Create.DelegateDocument(proto, { title: 'child of ' + proto.title });
return d;
});
ScriptingGlobals.add(function generateLinkTitle(self: Doc) {
- const anchor1title = self.anchor1 && self.anchor1 !== self ? Cast(self.anchor1, Doc, null)?.title : '<?>';
- const anchor2title = self.anchor2 && self.anchor2 !== self ? Cast(self.anchor2, Doc, null)?.title : '<?>';
- const relation = self.linkRelationship || 'to';
- return `${anchor1title} (${relation}) ${anchor2title}`;
-});
-ScriptingGlobals.add(function openTabAlias(tab: Doc) {
- CollectionDockingView.AddSplit(Doc.MakeAlias(tab), 'right');
+ const link_anchor_1title = self.link_anchor_1 && self.link_anchor_1 !== self ? Cast(self.link_anchor_1, Doc, null)?.title : '<?>';
+ const link_anchor_2title = self.link_anchor_2 && self.link_anchor_2 !== self ? Cast(self.link_anchor_2, Doc, null)?.title : '<?>';
+ const relation = self.link_relationship || 'to';
+ return `${link_anchor_1title} (${relation}) ${link_anchor_2title}`;
});
diff --git a/src/client/documents/Gitlike.ts b/src/client/documents/Gitlike.ts
index 575c984f5..f35f33f65 100644
--- a/src/client/documents/Gitlike.ts
+++ b/src/client/documents/Gitlike.ts
@@ -1,118 +1,118 @@
-import { Doc, DocListCast, DocListCastAsync, Field } from "../../fields/Doc";
-import { List } from "../../fields/List";
-import { Cast, DateCast } from "../../fields/Types";
-import { DateField } from "../../fields/DateField";
-import { Id } from "../../fields/FieldSymbols";
+// import { Doc, DocListCast, DocListCastAsync, Field } from "../../fields/Doc";
+// import { List } from "../../fields/List";
+// import { Cast, DateCast } from "../../fields/Types";
+// import { DateField } from "../../fields/DateField";
+// import { Id } from "../../fields/FieldSymbols";
-// synchs matching documents on the two branches that are being merged/pulled
-// currently this just synchs the main 'fieldKey' component of the data since
-// we don't have individual timestamps for all fields -- this is a problematic design issue.
-function GitlikeSynchDocs(bd: Doc, md: Doc) {
- const fieldKey = Doc.LayoutFieldKey(md);
- const bdate = DateCast(bd[`${fieldKey}-lastModified`])?.date;
- const mdate = DateCast(md[`${fieldKey}-lastModified`])?.date;
- const bdproto = bd && Doc.GetProto(bd);
- if (bdate !== mdate && bdate <= mdate) {
- if (bdproto && md) {
- bdproto[fieldKey] = Field.Copy(md[fieldKey]);
- bdproto[`${fieldKey}-lastModified`] = new DateField();
- }
- }
- const bldate = DateCast(bd._lastModified)?.date;
- const mldate = DateCast(md._lastModified)?.date;
- if (bldate === mldate || bldate > mldate) return;
- if (bdproto && md) {
- bd.x = Field.Copy(md.x);
- bd.y = Field.Copy(md.y);
- bd.width = Field.Copy(md.width);
- bd.height = Field.Copy(md.height);
- bdproto._lastModified = new DateField();
- }
-}
+// // synchs matching documents on the two branches that are being merged/pulled
+// // currently this just synchs the main 'fieldKey' component of the data since
+// // we don't have individual timestamps for all fields -- this is a problematic design issue.
+// function GitlikeSynchDocs(bd: Doc, md: Doc) {
+// const fieldKey = Doc.LayoutFieldKey(md);
+// const bdate = DateCast(bd[`${fieldKey}_modificationDate`])?.date;
+// const mdate = DateCast(md[`${fieldKey}_modificationDate`])?.date;
+// const bdproto = bd && Doc.GetProto(bd);
+// if (bdate !== mdate && bdate <= mdate) {
+// if (bdproto && md) {
+// bdproto[fieldKey] = Field.Copy(md[fieldKey]);
+// bdproto[`${fieldKey}_modificationDate`] = new DateField();
+// }
+// }
+// const bldate = DateCast(bd._layout_modificationDate)?.date;
+// const mldate = DateCast(md._layout_modificationDate)?.date;
+// if (bldate === mldate || bldate > mldate) return;
+// if (bdproto && md) {
+// bd.x = Field.Copy(md.x);
+// bd.y = Field.Copy(md.y);
+// bd.width = Field.Copy(md.width);
+// bd.height = Field.Copy(md.height);
+// bdproto._layout_modificationDate = new DateField();
+// }
+// }
-// pulls documents onto a branch from the branch's master
-// if a document exists on master but not on the branch, it is branched and added
-// NOTE: need to set a timestamp on the branch that is equal to the master's last merge timestamp.
-async function GitlikePullFromMaster(branch: Doc, suffix = "") {
- const masterMain = Cast(branch.branchOf, Doc, null);
- // get the set of documents on both the branch and master
- const masterMainDocs = masterMain && await DocListCastAsync(masterMain[Doc.LayoutFieldKey(masterMain) + suffix]);
- const branchMainDocs = await DocListCastAsync(branch[Doc.LayoutFieldKey(branch) + suffix]);
- // get the master documents that correspond to the branch documents
- const branchMasterMainDocs = branchMainDocs?.map(bd => Cast(bd.branchOf, Doc, null) || bd);
- const branchMasterMainDocProtos = branchMasterMainDocs?.map(doc => Doc.GetProto(doc));
- // get documents on master that don't have a corresponding master doc (form a branch doc), and ...
- const newDocsFromMaster = masterMainDocs?.filter(md => !branchMasterMainDocProtos?.includes(Doc.GetProto(md)));
- const oldDocsFromMaster = masterMainDocs?.filter(md => branchMasterMainDocProtos?.includes(Doc.GetProto(md)));
- oldDocsFromMaster?.forEach(md => {
- const bd = branchMainDocs?.find(bd => (Cast(bd.branchOf, Doc, null) || bd) === md);
- bd && GitlikeSynchDocs(bd, md);
- });
- const cloneMap = new Map<string, Doc>(); cloneMap.set(masterMain[Id], branch);
- // make branch clones of them, then add them to the branch
- const newlyBranchedDocs = await Promise.all(newDocsFromMaster?.map(async md => (await Doc.MakeClone(md, false, true, cloneMap)).clone) || []);
- newlyBranchedDocs.forEach(nd => {
- Doc.AddDocToList(branch, Doc.LayoutFieldKey(branch) + suffix, nd);
- nd.context = branch;
- });
- // if a branch doc's corresponding main branch doc doesn't have a context, then it was deleted.
- const remDocsFromMaster = branchMainDocs?.filter(bd => Cast(bd.branchOf, Doc, null) && !Cast(bd.branchOf, Doc, null)?.context);
- // so then remove all the deleted main docs from this branch.
- remDocsFromMaster?.forEach(rd => Doc.RemoveDocFromList(branch, Doc.LayoutFieldKey(branch) + suffix, rd));
-}
+// // pulls documents onto a branch from the branch's master
+// // if a document exists on master but not on the branch, it is branched and added
+// // NOTE: need to set a timestamp on the branch that is equal to the master's last merge timestamp.
+// async function GitlikePullFromMaster(branch: Doc, suffix = "") {
+// const masterMain = Cast(branch.branchOf, Doc, null);
+// // get the set of documents on both the branch and master
+// const masterMainDocs = masterMain && await DocListCastAsync(masterMain[Doc.LayoutFieldKey(masterMain) + suffix]);
+// const branchMainDocs = await DocListCastAsync(branch[Doc.LayoutFieldKey(branch) + suffix]);
+// // get the master documents that correspond to the branch documents
+// const branchMasterMainDocs = branchMainDocs?.map(bd => Cast(bd.branchOf, Doc, null) || bd);
+// const branchMasterMainDocProtos = branchMasterMainDocs?.map(doc => Doc.GetProto(doc));
+// // get documents on master that don't have a corresponding master doc (form a branch doc), and ...
+// const newDocsFromMaster = masterMainDocs?.filter(md => !branchMasterMainDocProtos?.includes(Doc.GetProto(md)));
+// const oldDocsFromMaster = masterMainDocs?.filter(md => branchMasterMainDocProtos?.includes(Doc.GetProto(md)));
+// oldDocsFromMaster?.forEach(md => {
+// const bd = branchMainDocs?.find(bd => (Cast(bd.branchOf, Doc, null) || bd) === md);
+// bd && GitlikeSynchDocs(bd, md);
+// });
+// const cloneMap = new Map<string, Doc>(); cloneMap.set(masterMain[Id], branch);
+// // make branch clones of them, then add them to the branch
+// const newlyBranchedDocs = await Promise.all(newDocsFromMaster?.map(async md => (await Doc.MakeClone(md, false, true, cloneMap)).clone) || []);
+// newlyBranchedDocs.forEach(nd => {
+// Doc.AddDocToList(branch, Doc.LayoutFieldKey(branch) + suffix, nd);
+// nd.embedContainer = branch;
+// });
+// // if a branch doc's corresponding main branch doc doesn't have a embedContainer, then it was deleted.
+// const remDocsFromMaster = branchMainDocs?.filter(bd => Cast(bd.branchOf, Doc, null) && !Cast(bd.branchOf, Doc, null)?.embedContainer);
+// // so then remove all the deleted main docs from this branch.
+// remDocsFromMaster?.forEach(rd => Doc.RemoveDocFromList(branch, Doc.LayoutFieldKey(branch) + suffix, rd));
+// }
-// merges all branches from the master branch by first merging the top-level collection of documents,
-// and then merging all the annotations on those documents.
-// TODO: need to add an incrementing timestamp whenever anything merges. don't allow a branch to merge if it's last pull timestamp isn't equal to the last merge timestamp.
-async function GitlikeMergeWithMaster(master: Doc, suffix = "") {
- const branches = await DocListCastAsync(master.branches);
- branches?.map(async branch => {
- const branchChildren = await DocListCastAsync(branch[Doc.LayoutFieldKey(branch) + suffix]);
- branchChildren && await Promise.all(branchChildren.map(async bd => {
- const cloneMap = new Map<string, Doc>(); cloneMap.set(master[Id], branch);
- // see if the branch's child exists on master.
- const masterChild = Cast(bd.branchOf, Doc, null) || (await Doc.MakeClone(bd, false, true, cloneMap)).clone;
- // if the branch's child didn't exist on master, we make a branch clone of the child to add to master.
- // however, since master is supposed to have the "main" clone, and branches, the "branch" clones, we have to reverse the fields
- // on the branch child and master clone.
- if (masterChild.branchOf) {
- const branchDocProto = Doc.GetProto(bd);
- const masterChildProto = Doc.GetProto(masterChild);
- const branchTitle = bd.title;
- branchDocProto.title = masterChildProto.title;
- masterChildProto.title = branchTitle;
- masterChildProto.branchOf = masterChild.branchOf = undefined; // the master child should not be a branch of the branch child, so unset 'branchOf'
- masterChildProto.branches = new List<Doc>([bd]); // the master child's branches needs to include the branch child
- Doc.RemoveDocFromList(branchDocProto, "branches", masterChildProto); // the branch child should not have the master child in its branch list.
- branchDocProto.branchOf = masterChild; // the branch child is now a branch of the master child
- }
- Doc.AddDocToList(master, Doc.LayoutFieldKey(master) + suffix, masterChild); // add the masterChild to master (if it's already there, this is a no-op)
- masterChild.context = master;
- GitlikeSynchDocs(masterChild, bd);//Doc.GetProto(masterChild), bd);
- }));
- const masterChildren = await DocListCastAsync(master[Doc.LayoutFieldKey(master) + suffix]);
- masterChildren?.forEach(mc => { // see if any master children
- if (!branchChildren?.find(bc => bc.branchOf === mc)) { // are not in the list of children for this branch.
- Doc.RemoveDocFromList(master, Doc.LayoutFieldKey(master) + suffix, mc); // if so, delete the master child since the branch has deleted it.
- mc.context = undefined; // NOTE if we merge a branch that didn't do a pull, it will look like the branch deleted documents -- need edit timestamps that prevent merging if branch isn't up-to-date with last edit timestamp
- }
- });
- });
-}
+// // merges all branches from the master branch by first merging the top-level collection of documents,
+// // and then merging all the annotations on those documents.
+// // TODO: need to add an incrementing timestamp whenever anything merges. don't allow a branch to merge if it's last pull timestamp isn't equal to the last merge timestamp.
+// async function GitlikeMergeWithMaster(master: Doc, suffix = "") {
+// const branches = await DocListCastAsync(master.branches);
+// branches?.map(async branch => {
+// const branchChildren = await DocListCastAsync(branch[Doc.LayoutFieldKey(branch) + suffix]);
+// branchChildren && await Promise.all(branchChildren.map(async bd => {
+// const cloneMap = new Map<string, Doc>(); cloneMap.set(master[Id], branch);
+// // see if the branch's child exists on master.
+// const masterChild = Cast(bd.branchOf, Doc, null) || (await Doc.MakeClone(bd, false, true, cloneMap)).clone;
+// // if the branch's child didn't exist on master, we make a branch clone of the child to add to master.
+// // however, since master is supposed to have the "main" clone, and branches, the "branch" clones, we have to reverse the fields
+// // on the branch child and master clone.
+// if (masterChild.branchOf) {
+// const branchDocProto = Doc.GetProto(bd);
+// const masterChildProto = Doc.GetProto(masterChild);
+// const branchTitle = bd.title;
+// branchDocProto.title = masterChildProto.title;
+// masterChildProto.title = branchTitle;
+// masterChildProto.branchOf = masterChild.branchOf = undefined; // the master child should not be a branch of the branch child, so unset 'branchOf'
+// masterChildProto.branches = new List<Doc>([bd]); // the master child's branches needs to include the branch child
+// Doc.RemoveDocFromList(branchDocProto, "branches", masterChildProto); // the branch child should not have the master child in its branch list.
+// branchDocProto.branchOf = masterChild; // the branch child is now a branch of the master child
+// }
+// Doc.AddDocToList(master, Doc.LayoutFieldKey(master) + suffix, masterChild); // add the masterChild to master (if it's already there, this is a no-op)
+// masterChild.embedContainer = master;
+// GitlikeSynchDocs(masterChild, bd);//Doc.GetProto(masterChild), bd);
+// }));
+// const masterChildren = await DocListCastAsync(master[Doc.LayoutFieldKey(master) + suffix]);
+// masterChildren?.forEach(mc => { // see if any master children
+// if (!branchChildren?.find(bc => bc.branchOf === mc)) { // are not in the list of children for this branch.
+// Doc.RemoveDocFromList(master, Doc.LayoutFieldKey(master) + suffix, mc); // if so, delete the master child since the branch has deleted it.
+// mc.embedContainer = undefined; // NOTE if we merge a branch that didn't do a pull, it will look like the branch deleted documents -- need edit timestamps that prevent merging if branch isn't up-to-date with last edit timestamp
+// }
+// });
+// });
+// }
-// performs a "git"-like task: pull or merge
-// if pull, then target is a specific branch document that will be updated from its associated master
-// if merge, then target is the master doc that will merge in all branches associated with it.
-// TODO: parameterize 'merge' to specify which branch(es) should be merged.
-// extend 'merge' to allow a specific branch to be merge target (not just master);
-// make pull/merge be recursive (ie, this func currently just operates on the main doc and its children)
-export async function BranchTask(target: Doc, action: "pull" | "merge") {
- const func = action === "pull" ? GitlikePullFromMaster : GitlikeMergeWithMaster;
- await func(target, "");
- await DocListCast(target[Doc.LayoutFieldKey(target)]).forEach(async targetChild => func(targetChild, "-annotations"));
- await DocListCast(target[Doc.LayoutFieldKey(target)]).forEach(async targetChild => func(targetChild, "-sidebar"));
-}
+// // performs a "git"-like task: pull or merge
+// // if pull, then target is a specific branch document that will be updated from its associated master
+// // if merge, then target is the master doc that will merge in all branches associated with it.
+// // TODO: parameterize 'merge' to specify which branch(es) should be merged.
+// // extend 'merge' to allow a specific branch to be merge target (not just master);
+// // make pull/merge be recursive (ie, this func currently just operates on the main doc and its children)
+// export async function BranchTask(target: Doc, action: "pull" | "merge") {
+// const func = action === "pull" ? GitlikePullFromMaster : GitlikeMergeWithMaster;
+// await func(target, "");
+// await DocListCast(target[Doc.LayoutFieldKey(target)]).forEach(async targetChild => func(targetChild, "_annotations"));
+// await DocListCast(target[Doc.LayoutFieldKey(target)]).forEach(async targetChild => func(targetChild, "_sidebar"));
+// }
-export async function BranchCreate(target: Doc) {
- return (await Doc.MakeClone(target, false, true)).clone;
-} \ No newline at end of file
+// export async function BranchCreate(target: Doc) {
+// return (await Doc.MakeClone(target, false, true)).clone;
+// }
diff --git a/src/client/goldenLayout.js b/src/client/goldenLayout.js
index bc08b4d0b..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');
@@ -5408,6 +5408,7 @@
* @returns {void}
*/
_destroy: function () {
+ this._reactComponent.unmount();
// ReactDOM.unmountComponentAtNode(this._container.getElement()[0]);
this._container.off('open', this._render, this);
this._container.off('destroy', this._destroy, this);
diff --git a/src/client/util/CaptureManager.tsx b/src/client/util/CaptureManager.tsx
index 0b5957fac..d68761ba7 100644
--- a/src/client/util/CaptureManager.tsx
+++ b/src/client/util/CaptureManager.tsx
@@ -1,19 +1,15 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, observable, runInAction } from 'mobx';
+import { action, computed, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { convertToObject } from 'typescript';
-import { Doc, DocListCast } from '../../fields/Doc';
-import { BoolCast, StrCast, Cast } from '../../fields/Types';
-import { addStyleSheet, addStyleSheetRule, Utils } from '../../Utils';
+import { Doc } from '../../fields/Doc';
+import { DocCast, StrCast } from '../../fields/Types';
+import { addStyleSheet } from '../../Utils';
import { LightboxView } from '../views/LightboxView';
import { MainViewModal } from '../views/MainViewModal';
import './CaptureManager.scss';
+import { LinkManager } from './LinkManager';
import { SelectionManager } from './SelectionManager';
-import { undoBatch } from './UndoManager';
-const higflyout = require('@hig/flyout');
-export const { anchorPoints } = higflyout;
-export const Flyout = higflyout.default;
@observer
export class CaptureManager extends React.Component<{}> {
@@ -53,23 +49,14 @@ export class CaptureManager extends React.Component<{}> {
const doc = this._document;
const order: JSX.Element[] = [];
if (doc) {
- console.log('title', doc.title);
- console.log('links', doc.links);
- const linkDocs = DocListCast(doc.links);
- const firstDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor1 as Doc, doc) || Doc.AreProtosEqual((linkDoc.anchor1 as Doc).annotationOn as Doc, doc)); // link docs where 'doc' is anchor1
- const secondDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor2 as Doc, doc) || Doc.AreProtosEqual((linkDoc.anchor2 as Doc).annotationOn as Doc, doc)); // link docs where 'doc' is anchor2
- linkDocs.forEach((l, i) => {
- if (l) {
- console.log(i, (l.anchor1 as Doc).title);
- console.log(i, (l.anchor2 as Doc).title);
- order.push(
- <div className="list-item">
- <div className="number">{i}</div>
- {StrCast((l.anchor1 as Doc).title)}
- </div>
- );
- }
- });
+ LinkManager.Links(doc).forEach((l, i) =>
+ order.push(
+ <div className="list-item">
+ <div className="number">{i}</div>
+ {StrCast(DocCast(l.link_anchor_1)?.title)}
+ </div>
+ )
+ );
}
return (
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 114dbac93..11a8dcaf6 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -1,15 +1,14 @@
-import { forOwn } from "lodash";
import { reaction } from "mobx";
import * as rp from 'request-promise';
import { Doc, DocListCast, DocListCastAsync, Opt } from "../../fields/Doc";
-import { Id } from "../../fields/FieldSymbols";
+import { FieldLoader } from "../../fields/FieldLoader";
import { InkTool } from "../../fields/InkField";
import { List } from "../../fields/List";
import { PrefetchProxy } from "../../fields/Proxy";
import { RichTextField } from "../../fields/RichTextField";
import { listSpec } from "../../fields/Schema";
-import { ComputedField, ScriptField } from "../../fields/ScriptField";
-import { Cast, DateCast, DocCast, PromiseValue, StrCast } from "../../fields/Types";
+import { ScriptField } from "../../fields/ScriptField";
+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";
@@ -21,10 +20,12 @@ import { TreeViewType } from "../views/collections/CollectionTreeView";
import { DashboardView } from "../views/DashboardView";
import { Colors } from "../views/global/globalEnums";
import { MainView } from "../views/MainView";
-import { ButtonType, NumButtonType } from "../views/nodes/button/FontIconBox";
+import { ButtonType } from "../views/nodes/button/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";
@@ -36,14 +37,18 @@ interface Button {
toolTip?: string;
icon?: string;
btnType?: ButtonType;
- numBtnType?: NumButtonType;
numBtnMin?: number;
numBtnMax?: number;
switchToggle?: boolean;
width?: number;
+ linearBtnWidth?: number;
+ toolType?: string; // type of pen tool
+ expertMode?: boolean;// available only in expert mode
btnList?: List<string>;
ignoreClick?: boolean;
buttonText?: string;
+ backgroundColor?: string;
+ waitForDoubleClickToClick?: boolean;
// fields that do not correspond to DocumentOption fields
scripts?: { script?: string; onClick?: string; onDoubleClick?: string }
@@ -59,11 +64,11 @@ export class CurrentUserUtils {
const requiredTypeNameFields:{btnOpts:DocumentOptions, templateOpts:DocumentOptions, template:(opts:DocumentOptions) => Doc}[] = [
{
btnOpts: { title: "slide", icon: "address-card" },
- templateOpts: { _width: 400, _height: 300, title: "slideView", childDocumentsActive: true, _xMargin: 3, _yMargin: 3, system: true },
+ templateOpts: { _width: 400, _height: 300, title: "slideView", _xMargin: 3, _yMargin: 3, isSystem: true },
template: (opts:DocumentOptions) => Docs.Create.MultirowDocument(
[
- Docs.Create.MulticolumnDocument([], { title: "data", _height: 200, system: true }),
- Docs.Create.TextDocument("", { title: "text", _fitWidth:true, _height: 100, system: true, _fontFamily: StrCast(Doc.UserDoc()._fontFamily), _fontSize: StrCast(Doc.UserDoc()._fontSize) })
+ Docs.Create.MulticolumnDocument([], { title: "data", _height: 200, isSystem: true }),
+ Docs.Create.TextDocument("", { title: "text", _layout_fitWidth:true, _height: 100, isSystem: true, _text_fontFamily: StrCast(Doc.UserDoc().fontFamily), _text_fontSize: StrCast(Doc.UserDoc().fontSize) })
], opts)
},
{
@@ -79,7 +84,7 @@ export class CurrentUserUtils {
];
const requiredTypes = requiredTypeNameFields.map(({ btnOpts, template, templateOpts }) => {
const tempBtn = DocListCast(tempDocs?.data)?.find(doc => doc.title === btnOpts.title);
- const reqdScripts = { onDragStart: '{ return copyDragFactory(this.dragFactory); }' };
+ const reqdScripts = { onDragStart: '{ return copyDragFactory(this.dragFactory,this.openFactoryAsDelegate); }' };
const assignBtnAndTempOpts = (templateBtn:Opt<Doc>, btnOpts:DocumentOptions, templateOptions:DocumentOptions) => {
if (templateBtn) {
DocUtils.AssignOpts(templateBtn,btnOpts);
@@ -91,9 +96,9 @@ export class CurrentUserUtils {
});
const reqdOpts:DocumentOptions = {
- title: "Experimental Tools", _xMargin: 0, _showTitle: "title", _chromeHidden: true,
- _stayInCollection: true, _hideContextMenu: true, _forceActive: true, system: true, childDocumentsActive: true,
- _autoHeight: true, _width: 500, _height: 300, _fitWidth: true, _columnWidth: 35, ignoreClick: true, _lockedPosition: true,
+ title: "Experimental Tools", _xMargin: 0, _layout_showTitle: "title", _chromeHidden: 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)" };
const reqdFuncs = { hidden: "IsNoviceMode()" };
@@ -103,55 +108,55 @@ export class CurrentUserUtils {
/// Initializes templates for editing click funcs of a document
static setupChildClickEditors(doc: Doc, field = "clickFuncs-child") {
const tempClicks = DocCast(doc[field]);
- const reqdClickOpts:DocumentOptions = {_width: 300, _height:200, system: true};
+ const reqdClickOpts:DocumentOptions = {_width: 300, _height:200, isSystem: true};
const reqdTempOpts:{opts:DocumentOptions, script: string}[] = [
- { opts: { title: "Open In Target", targetScriptKey: "onChildClick"}, script: "docCast(thisContainer.target).then((target) => target && (target.proto.data = new List([self])))"},
- { opts: { title: "Open Detail On Right", targetScriptKey: "onChildDoubleClick"}, script: "openOnRight(self.doubleClickView)"}];
+ { opts: { title: "Open In Target", targetScriptKey: "onChildClick"}, script: "docCast(documentView?.props.docViewPath().lastElement()?.rootDoc.target).then((target) => target && (target.proto.data = new List([self])))"},
+ { opts: { title: "Open Detail On Right", targetScriptKey: "onChildDoubleClick"}, script: `openDoc(self.doubleClickView.${OpenWhere.addRight})`}];
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) ?? Docs.Create.ScriptingDocument(ScriptField.MakeScript(opts.script, allOpts),allOpts);
});
- const reqdOpts:DocumentOptions = { title: "child click editors", _height:75, system: true};
+ const reqdOpts:DocumentOptions = { title: "child click editors", _height:75, isSystem: true};
return DocUtils.AssignOpts(tempClicks, reqdOpts, reqdClickList) ?? (doc[field] = Docs.Create.TreeDocument(reqdClickList, reqdOpts));
}
/// 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, system: true};
+ const reqdClickOpts:DocumentOptions = { _width: 300, _height:200, isSystem: true};
const reqdTempOpts:{opts:DocumentOptions, script: string}[] = [
- { opts: { title: "onClick"}, script: "console.log( 'click')"},
- { opts: { title: "onDoubleClick"}, script: "console.log( 'double click')"},
- { opts: { title: "onChildClick"}, script: "console.log( 'child click')"},
- { opts: { title: "onChildDoubleClick"}, script: "console.log( 'child double click')"},
- { opts: { title: "onCheckedClick"}, script: "console.log( heading, checked, containingTreeView)"},
+ { opts: { title: "onClick"}, script: "console.log('click')"},
+ { opts: { title: "onDoubleClick"}, script: "console.log('click')"},
+ { opts: { title: "onChildClick"}, script: "console.log('click')"},
+ { opts: { title: "onChildDoubleClick"}, script: "console.log('click')"},
+ { opts: { title: "onCheckedClick"}, script: "console.log(heading, checked, containingTreeView)"},
];
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, system: true};
+ const reqdOpts:DocumentOptions = {title: "click editor templates", _height:75, isSystem: true};
return DocUtils.AssignOpts(tempClicks, reqdOpts, reqdClickList) ?? (doc[field] = Docs.Create.TreeDocument(reqdClickList, reqdOpts));
}
/// 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, autoHeight: true, fitWidth: true, system: true};
+ const reqdOpts = {...opts, 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");
});
- const reqdOpts:DocumentOptions = { title: "Note Layouts", _height: 75, system: true };
+ const reqdOpts:DocumentOptions = { title: "Note Layouts", _height: 75, isSystem: true };
return DocUtils.AssignOpts(tempNotes, reqdOpts, reqdNoteList) ?? (doc[field] = Docs.Create.TreeDocument(reqdNoteList, reqdOpts));
}
@@ -163,14 +168,14 @@ export class CurrentUserUtils {
CurrentUserUtils.setupClickEditorTemplates(doc)
];
CurrentUserUtils.setupChildClickEditors(doc)
- const reqdOpts = { title: "template layouts", _xMargin: 0, system: true, };
+ const reqdOpts = { title: "template layouts", _xMargin: 0, isSystem: true, };
const reqdScripts = { dropConverter: "convertToButtons(dragData)" };
return DocUtils.AssignDocField(doc, field, (opts,items) => Docs.Create.TreeDocument(items??[], opts), reqdOpts, templates, reqdScripts);
}
// setup templates for different document types when they are iconified from Document Decorations
- static setupDefaultIconTemplates(doc: Doc, field="template-icons") {
- const reqdOpts = { title: "icon templates", _height: 75, system: true };
+ 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));
const makeIconTemplate = (type: DocumentType | undefined, templateField: string, opts:DocumentOptions) => {
@@ -181,22 +186,22 @@ export class CurrentUserUtils {
case DocumentType.IMG : creator = imageBox; break;
case DocumentType.FONTICON: creator = fontBox; break;
}
- const allopts = {system: true, ...opts};
+ const allopts = {isSystem: true, onClickScriptDisable: "never", ...opts};
return DocUtils.AssignScripts( (curIcon?.iconTemplate === opts.iconTemplate ?
DocUtils.AssignOpts(curIcon, allopts):undefined) ?? ((templateIconsDoc[iconFieldName] = MakeTemplate(creator(allopts), true, iconFieldName, templateField))),
- {onClick:"deiconifyView(documentView)"});
+ {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, _showTitle: "title", ...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 });
const iconTemplates = [
makeIconTemplate(undefined, "title", { iconTemplate:DocumentType.LABEL, backgroundColor: "dimgray"}),
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, _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}),
@@ -212,9 +217,9 @@ export class CurrentUserUtils {
/// initalizes the set of "empty<DocType>" versions of each document type with default fields. e.g.,. emptyNote, emptyTrail
static creatorBtnDescriptors(doc: Doc): {
title: string, toolTip: string, icon: string, ignoreClick?: boolean, dragFactory?: Doc,
- backgroundColor?: string, clickFactory?: Doc, scripts?: { onClick?: string, onDragStart?: string}, funcs?: {onDragStart?:string, hidden?: string},
+ backgroundColor?: string, openFactoryAsDelegate?:boolean, openFactoryLocation?:string, clickFactory?: Doc, scripts?: { onClick?: string, onDragStart?: string}, funcs?: {onDragStart?:string, hidden?: string},
}[] {
- const standardOps = (key:string) => ({ title : "Untitled "+ key, _fitWidth: true, system: true, "dragFactory-count": 0, cloneFieldFilter: new List<string>(["system"]) });
+ const standardOps = (key:string) => ({ title : "Untitled "+ key, _layout_fitWidth: false, isSystem: true, "dragFactory_count": 0, cloneFieldFilter: new List<string>(["isSystem"]) });
const json = {
doc: {
type: "doc",
@@ -222,11 +227,11 @@ export class CurrentUserUtils {
{
type: "paragraph", attrs: {}, content: [{
type: "dashField",
- attrs: { fieldKey: "author", docid: "", hideKey: false },
+ attrs: { fieldKey: "author", docId: "", hideKey: false },
marks: [{ type: "strong" }]
}, {
type: "dashField",
- attrs: { fieldKey: "creationDate", docid: "", hideKey: false },
+ attrs: { fieldKey: "author_date", docId: "", hideKey: false },
marks: [{ type: "strong" }]
}]
}]
@@ -241,7 +246,7 @@ export class CurrentUserUtils {
"<HTMLdiv transformOrigin='top left' width='{100/scale}%' height='{100/scale}%' transform='scale({scale})'>" +
` <FormattedTextBox {...props} dontScale='true' fieldKey={'text'} height='calc(100% - ${headerBtnHgt}px - {this._headerHeight||0}px)'/>` +
" <FormattedTextBox {...props} dontScale='true' fieldKey={'header'} dontSelectOnLoad='true' ignoreAutoHeight='true' fontSize='{this._headerFontSize||9}px' height='{(this._headerHeight||0)}px' backgroundColor='{this._headerColor || MySharedDocs().userColor||`lightGray`}' />" +
- ` <HTMLdiv fontSize='${headerBtnHgt - 1}px' height='${headerBtnHgt}px' backgroundColor='yellow' onClick={‘(this._headerHeight=scale*Math.min(Math.max(0,this._height-30),this._headerHeight===0?50:0)) + (this._autoHeightMargins=this._headerHeight ? this._headerHeight+${headerBtnHgt}:0)’} >Metadata</HTMLdiv>` +
+ ` <HTMLdiv fontSize='${headerBtnHgt - 1}px' height='${headerBtnHgt}px' backgroundColor='yellow' onClick={‘(this._headerHeight=scale*Math.min(Math.max(0,this._height-30),this._headerHeight===0?50:0)) + (this._layout_autoHeightMargins=this._headerHeight ? this._headerHeight+${headerBtnHgt}:0)’} >Metadata</HTMLdiv>` +
"</HTMLdiv>"
}, "header");
@@ -255,69 +260,78 @@ 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, _autoHeight: true }},
- {key: "Noteboard", creator: opts => Docs.Create.NoteTakingDocument([], opts), opts: { _width: 250, _height: 200 }},
- {key: "Collection", creator: opts => Docs.Create.FreeformDocument([], opts), opts: { _width: 150, _height: 100 }},
- {key: "Equation", creator: opts => Docs.Create.EquationDocument(opts), opts: { _width: 300, _height: 35, _fitWidth:false, _backgroundGridShow: true, }},
- {key: "Webpage", creator: opts => Docs.Create.WebDocument("",opts), opts: { _width: 400, _height: 512, _nativeWidth: 850, useCors: true, }},
+ {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_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 }},
{key: "Audio", creator: opts => Docs.Create.AudioDocument(nullAudio, opts),opts: { _width: 200, _height: 100, }},
- {key: "Map", creator: opts => Docs.Create.MapDocument([], opts), opts: { _width: 800, _height: 600, _showSidebar: true, }},
+ {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, system: true, cloneFieldFilter: new List<string>(["system"]) }},
- {key: "Button", creator: Docs.Create.ButtonDocument, opts: { _width: 150, _height: 50, _xPadding: 10, _yPadding: 10, }},
+ {key: "WebCam", creator: opts => Docs.Create.WebCamDocument("", opts), opts: { _width: 400, _height: 200, recording:true, isSystem: true, cloneFieldFilter: new List<string>(["isSystem"]) }},
+ {key: "Button", creator: Docs.Create.ButtonDocument, opts: { _width: 150, _height: 50, _xPadding: 10, _yPadding: 10}, scripts: {onClick: FollowLinkScript()?.script.originalScript ?? ""}},
{key: "Script", creator: opts => Docs.Create.ScriptingDocument(null, opts), opts: { _width: 200, _height: 250, }},
- // {key: "DataViz", creator: opts => Docs.Create.DataVizDocument(opts), opts: { _width: 300, _height: 300 }},
- {key: "Header", creator: headerTemplate, opts: { _width: 300, _height: 70, _headerPointerEvents: "all", _headerHeight: 12, _headerFontSize: 9, _autoHeight: true,}},
- {key: "Trail", creator: Docs.Create.PresDocument, opts: { _width: 400, _height: 30, _viewType: CollectionViewType.Stacking, targetDropAction: "alias" as any, treeViewHideTitle: true, _chromeHidden: true, boxShadow: "0 0" }},
- {key: "Tab", creator: opts => Docs.Create.FreeformDocument([], opts), opts: { _width: 500, _height: 800, _backgroundGridShow: true, }},
- {key: "Slide", creator: opts => Docs.Create.TreeDocument([], opts), opts: { _width: 300, _height: 200, _viewType: CollectionViewType.Tree,
- treeViewHasOverlay: true, _fontSize: "20px", _autoHeight: true,
- allowOverlayDrop: true, treeViewType: TreeViewType.outline,
- backgroundColor: "white", _xMargin: 0, _yMargin: 0, _singleLine: true
+ {key: "DataViz", creator: opts => Docs.Create.DataVizDocument("/users/rz/Downloads/addresses.csv", opts), opts: { _width: 300, _height: 300 }},
+ {key: "Header", creator: headerTemplate, opts: { _width: 300, _height: 70, _headerPointerEvents: "all", _headerHeight: 12, _headerFontSize: 9, _layout_autoHeight: true, treeViewHideUnrendered: true}},
+ {key: "Trail", creator: Docs.Create.PresDocument, opts: { _width: 400, _height: 30, _type_collection: CollectionViewType.Stacking, dropAction: "embed" as dropActionType, treeViewHideTitle: true, _layout_fitWidth:true, _chromeHidden: true, layout_boxShadow: "0 0" }},
+ {key: "Tab", creator: opts => Docs.Create.FreeformDocument([], opts), opts: { _width: 500, _height: 800, _layout_fitWidth: true, _freeform_backgroundGrid: true, }},
+ {key: "Slide", creator: opts => Docs.Create.TreeDocument([], opts), opts: { _width: 300, _height: 200, _type_collection: CollectionViewType.Tree,
+ treeViewHasOverlay: true, _text_fontSize: "20px", _layout_autoHeight: true,
+ 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, },
- { toolTip: "Tap or drag to create a note board", title: "Notes", icon: "folder", dragFactory: doc.emptyNoteboard as Doc, },
- { toolTip: "Tap or drag to create a collection", title: "Col", icon: "folder", dragFactory: doc.emptyCollection as Doc, clickFactory: DocCast(doc.emptyTab), scripts: { onClick: 'openOnRight(copyDragFactory(this.clickFactory))', onDragStart: '{ return copyDragFactory(this.dragFactory);}'}, },
- { toolTip: "Tap or drag to create an equation", title: "Math", icon: "calculator", dragFactory: doc.emptyEquation as Doc, },
- { toolTip: "Tap or drag to create a webpage", title: "Web", icon: "globe-asia", dragFactory: doc.emptyWebpage as Doc, },
- { toolTip: "Tap or drag to create a comparison box", title: "Compare", icon: "columns", dragFactory: doc.emptyComparison as Doc, },
- { toolTip: "Tap or drag to create an audio recorder", title: "Audio", icon: "microphone", dragFactory: doc.emptyAudio as Doc, scripts: { onClick: 'openInOverlay(copyDragFactory(this.dragFactory))', onDragStart: '{ return copyDragFactory(this.dragFactory);}'}, },
- { toolTip: "Tap or drag to create a map", title: "Map", icon: "map-marker-alt", dragFactory: doc.emptyMap as Doc, },
- { toolTip: "Tap or drag to create a screen grabber", title: "Grab", icon: "photo-video", dragFactory: doc.emptyScreengrab as Doc, scripts: { onClick: 'openInOverlay(copyDragFactory(this.dragFactory))', onDragStart: '{ return copyDragFactory(this.dragFactory);}'},funcs: { hidden: 'IsNoviceMode()'} },
- { toolTip: "Tap or drag to create a WebCam recorder", title: "WebCam", icon: "photo-video", dragFactory: doc.emptyWebCam as Doc, scripts: { onClick: 'openInOverlay(copyDragFactory(this.dragFactory))', onDragStart: '{ return copyDragFactory(this.dragFactory);}'},funcs: { hidden: 'IsNoviceMode()'}},
- { toolTip: "Tap or drag to create a button", title: "Button", icon: "bolt", dragFactory: doc.emptyButton as Doc, funcs: { hidden: 'IsNoviceMode()'} },
- { toolTip: "Tap or drag to create a scripting box", title: "Script", icon: "terminal", dragFactory: doc.emptyScript as Doc, funcs: { hidden: 'IsNoviceMode()'}},
- { toolTip: "Tap or drag to create a data viz node", title: "DataViz", icon: "file", dragFactory: doc.emptyDataViz as Doc, },
- { toolTip: "Tap or drag to create a bullet slide", title: "PPT Slide", icon: "file", dragFactory: doc.emptySlide as Doc, funcs: { hidden: 'IsNoviceMode()'}},
- { toolTip: "Tap or drag to create a data note", title: "DataNote", icon: "window-maximize", dragFactory: doc.emptyHeader as Doc,scripts: { onClick: 'openOnRight(delegateDragFactory(this.dragFactory))', onDragStart: '{ return delegateDragFactory(this.dragFactory);}'}, },
- { toolTip: "Toggle a Calculator REPL", title: "repl", icon: "calculator", scripts: { onClick: 'addOverlayWindow("ScriptingRepl", { x: 300, y: 100, width: 200, height: 200, title: "Scripting REPL" })' } },
- ].map(tuple => ({scripts: {onClick: 'openOnRight(copyDragFactory(this.dragFactory))', onDragStart: '{ return copyDragFactory(this.dragFactory);}'}, ...tuple, }))
+ { 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, },
+ { 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: "replviewer", icon: "calculator", clickFactory: '<ScriptingRepl />' as any, openFactoryLocation: OpenWhere.overlay}, // hack: clickFactory is not a Doc but will get interpreted as a custom UI by the openDoc() onClick script
+ { toolTip: "Toggle an UndoStack", title: "undostacker", icon: "calculator", clickFactory: "<UndoStack>" as any, openFactoryLocation: OpenWhere.overlay},
+ ].map(tuple => (
+ { openFactoryLocation: OpenWhere.addRight,
+ scripts: { onClick: 'openDoc(copyDragFactory(this.clickFactory,this.openFactoryAsDelegate), this.openFactoryLocation)',
+ onDragStart: '{ return copyDragFactory(this.dragFactory,this.openFactoryAsDelegate); }'},
+ ...tuple, }))
}
/// Initalizes the "creator" buttons for the sidebar-- eg. the default set of draggable document creation tools
static setupCreatorButtons(doc: Doc, dragCreatorDoc?:Doc):Doc {
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,
- _nativeWidth: 50, _nativeHeight: 50, _width: 35, _height: 35, _hideContextMenu: true, _stayInCollection: true, _dropAction: "alias",
- btnType: ButtonType.ToolButton, backgroundColor: reqdOpts.backgroundColor ?? Colors.DARK_GRAY, color: Colors.WHITE, system: true,
- _removeDropProperties: new List<string>(["_stayInCollection"]),
+ const opts:DocumentOptions = {...OmitKeys(reqdOpts, ["funcs", "scripts", "backgroundColor"]).omit,
+ _width: 35, _height: 35, _layout_hideContextMenu: true, _dragOnlyWithinContainer: true,
+ btnType: ButtonType.ToolButton, backgroundColor: reqdOpts.backgroundColor ?? Colors.DARK_GRAY, color: Colors.WHITE, isSystem: true,
};
return DocUtils.AssignScripts(DocUtils.AssignOpts(btn, opts) ?? Docs.Create.FontIconDocument(opts), reqdOpts.scripts, reqdOpts.funcs);
});
const reqdOpts:DocumentOptions = {
- title: "Basic Item Creators", _showTitle: "title", _xMargin: 0, _stayInCollection: true, _hideContextMenu: true, _chromeHidden: true, system: true,
- _autoHeight: true, _width: 500, _height: 300, _fitWidth: true, _columnWidth: 40, ignoreClick: true, _lockedPosition: true, _forceActive: true,
- childDocumentsActive: 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,
+ childDragAction: 'embed'
};
const reqdScripts = { dropConverter: "convertToButtons(dragData)" };
return DocUtils.AssignScripts(DocUtils.AssignOpts(dragCreatorDoc, reqdOpts, creatorBtns) ?? Docs.Create.MasonryDocument(creatorBtns, reqdOpts), reqdScripts);
@@ -342,7 +356,7 @@ export class CurrentUserUtils {
/// the empty panel that is filled with whichever left menu button's panel has been selected
static setupLeftSidebarPanel(doc: Doc, field="myLeftSidebarPanel") {
- DocUtils.AssignDocField(doc, field, (opts) => ((doc:Doc) => {doc.system = true; return doc;})(new Doc()), {system:true});
+ DocUtils.AssignDocField(doc, field, (opts) => Doc.assign(new Doc(), opts as any), {isSystem:true, undoIgnoreFields: new List<string>(['proto'])});
}
/// Initializes the left sidebar menu buttons and the panels they open up
@@ -352,23 +366,22 @@ export class CurrentUserUtils {
const menuBtns = CurrentUserUtils.leftSidebarMenuBtnDescriptions(doc).map(({ title, target, icon, scripts, funcs }) => {
const btnDoc = myLeftSidebarMenu ? DocListCast(myLeftSidebarMenu.data).find(doc => doc.title === title) : undefined;
const reqdBtnOpts:DocumentOptions = {
- title, icon, target, btnType: ButtonType.MenuButton, system: true, dontUndo: true, dontRegisterView: true,
- _width: 60, _height: 60, _stayInCollection: true, _hideContextMenu: true, _chromeHidden: true, _dropAction: "alias",
- _removeDropProperties: new List<string>(["dropAction", "_stayInCollection"]),
+ title, icon, target, btnType: ButtonType.MenuButton, isSystem: true, undoIgnoreFields: new List<string>(['height', 'data_columnHeaders']), dontRegisterView: true,
+ _width: 60, _height: 60, _dragOnlyWithinContainer: true, _layout_hideContextMenu: true,
};
return DocUtils.AssignScripts(DocUtils.AssignOpts(btnDoc, reqdBtnOpts) ?? Docs.Create.FontIconDocument(reqdBtnOpts), scripts, funcs);
});
const reqdStackOpts:DocumentOptions ={
- title: "menuItemPanel", childDropAction: "alias", backgroundColor: Colors.DARK_GRAY, boxShadow: "rgba(0,0,0,0)", dontRegisterView: true, ignoreClick: true,
- _chromeHidden: true, _gridGap: 0, _yMargin: 0, _yPadding: 0, _xMargin: 0, _autoHeight: false, _width: 60, _columnWidth: 60, _lockedPosition: true, system: true
+ title: "menuItemPanel", childDragAction: "same", backgroundColor: Colors.DARK_GRAY, layout_boxShadow: "rgba(0,0,0,0)", dontRegisterView: true, ignoreClick: true,
+ _chromeHidden: true, _gridGap: 0, _yMargin: 0, _yPadding: 0, _xMargin: 0, _layout_autoHeight: false, _width: 60, _columnWidth: 60, _lockedPosition: true, isSystem: true,
};
return DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.StackingDocument(items??[], opts), reqdStackOpts, menuBtns, { dropConverter: "convertToButtons(dragData)" });
}
// Sets up mobile menu if it is undefined creates a new one, otherwise returns existing menu
static setupActiveMobileMenu(doc: Doc, field="activeMobileMenu") {
- const reqdOpts = { _width: 980, ignoreClick: true, _lockedPosition: false, title: "home", _yMargin: 100, system: true, _chromeHidden: true,};
+ const reqdOpts = { _width: 980, ignoreClick: true, _lockedPosition: false, title: "home", _yMargin: 100, isSystem: true, _chromeHidden: true,};
DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.StackingDocument(this.setupMobileButtons(), opts), reqdOpts);
}
@@ -389,62 +402,62 @@ export class CurrentUserUtils {
title: data.title,
_lockedPosition: true,
onClick: data.click ? ScriptField.MakeScript(data.click) : undefined,
- backgroundColor: data.backgroundColor, system: true
+ backgroundColor: data.backgroundColor, isSystem: true
},
- [this.createToolButton({ ignoreClick: true, icon: data.icon, backgroundColor: "rgba(0,0,0,0)", system: true, btnType: ButtonType.ClickButton, }), this.mobileTextContainer({}, [this.mobileButtonText({}, data.title), this.mobileButtonInfo({}, data.info)])])
+ [this.createToolButton({ ignoreClick: true, icon: data.icon, backgroundColor: "rgba(0,0,0,0)", isSystem: true, btnType: ButtonType.ClickButton, }), this.mobileTextContainer({}, [this.mobileButtonText({}, data.title), this.mobileButtonInfo({}, data.info)])])
);
}
// sets up the main document for the mobile button
static mobileButton = (opts: DocumentOptions, docs: Doc[]) => Docs.Create.MulticolumnDocument(docs, {
...opts,
- _removeDropProperties: new List<string>(["dropAction"]), _nativeWidth: 900, _nativeHeight: 250, _width: 900, _height: 250, _yMargin: 15,
- borderRounding: "5px", boxShadow: "0 0", system: true
+ _nativeWidth: 900, _nativeHeight: 250, _width: 900, _height: 250, _yMargin: 15,
+ 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,
- _removeDropProperties: new List<string>(["dropAction"]), _nativeWidth: 450, _nativeHeight: 250, _width: 450, _height: 250, _yMargin: 25,
- backgroundColor: "rgba(0,0,0,0)", borderRounding: "0", boxShadow: "0 0", ignoreClick: true, system: true
+ _nativeWidth: 450, _nativeHeight: 250, _width: 450, _height: 250, _yMargin: 25,
+ 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)", system: 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, system: 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
static setupMobileInkingDoc(userDoc: Doc) {
- return Docs.Create.FreeformDocument([], { title: "Mobile Inking", backgroundColor: "white", system: true });
+ return Docs.Create.FreeformDocument([], { title: "Mobile Inking", backgroundColor: "white", isSystem: true });
}
static setupMobileUploadDoc(userDoc: Doc) {
// const addButton = Docs.Create.FontIconDocument({ onDragStart: ScriptField.MakeScript('addWebToMobileUpload()'), title: "Add Web Doc to Upload Collection", icon: "plus", backgroundColor: "black" })
const webDoc = Docs.Create.WebDocument("https://www.britannica.com/biography/Miles-Davis", {
- title: "Upload Images From the Web", _lockedPosition: true, system: true
+ title: "Upload Images From the Web", _lockedPosition: true, isSystem: true
});
const uploadDoc = Docs.Create.StackingDocument([], {
- title: "Mobile Upload Collection", backgroundColor: "white", _lockedPosition: true, system: true, _chromeHidden: true,
+ title: "Mobile Upload Collection", backgroundColor: "white", _lockedPosition: true, isSystem: true, _chromeHidden: true,
});
return Docs.Create.StackingDocument([webDoc, uploadDoc], {
- _width: screen.width, _lockedPosition: true, title: "Upload", _autoHeight: true, _yMargin: 80, backgroundColor: "lightgray", system: true, _chromeHidden: true,
+ _width: screen.width, _lockedPosition: true, title: "Upload", _layout_autoHeight: true, _yMargin: 80, backgroundColor: "lightgray", isSystem: true, _chromeHidden: true,
});
}
/// 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", system: true, childDropAction: "alias",
- _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.
@@ -453,8 +466,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", system: true, ignoreClick: true, boxShadow: "0 0",
- _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]);
}
@@ -466,10 +479,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,
- title: "new dashboard", btnType: ButtonType.ClickButton, toolTip: "Create new dashboard", buttonText: "New trail", icon: "plus", system: 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[];
@@ -479,15 +492,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, system: true, treeViewTruncateTitleWidth: 350, ignoreClick: true,
- buttonMenu: true, buttonMenuDoc: newDashboardButton, childDropAction: "alias",
- _showTitle: "title", _height: 400, _gridGap: 5, _forceActive: true, _lockedPosition: true,
+ 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: "embed",
+ _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) {
@@ -505,23 +518,23 @@ 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, system: true, isFolder: true });
+ const myFileOrphans = DocUtils.AssignDocField(doc, "myFileOrphans", (opts) => Docs.Create.TreeDocument([], opts), { title: "Unfiled", undoIgnoreFields:new List<string>(['treeViewSortCriterion']), _dragOnlyWithinContainer: true, isSystem: true, isFolder: true });
const newFolder = `TreeView_addNewFolder()`;
const newFolderOpts: DocumentOptions = {
- _forceActive: true, _stayInCollection: true, _hideContextMenu: true, _width: 30, _height: 30,
- title: "New folder", btnType: ButtonType.ClickButton, toolTip: "Create new folder", buttonText: "New folder", icon: "folder-plus", system: true
+ _forceActive: true, _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, _width: 30, _height: 30, undoIgnoreFields:new List<string>(['treeViewSortCriterion']),
+ title: "New folder", btnType: ButtonType.ClickButton, toolTip: "Create new folder", buttonText: "New folder", icon: "folder-plus", isSystem: true
};
const newFolderScript = { onClick: newFolder};
- const newFolderButton = DocUtils.AssignScripts(DocUtils.AssignOpts(DocCast(myFilesystem?.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 = { _showTitle: "title", _height: 100, _gridGap: 5, _forceActive: true, _lockedPosition: true,
- title: "My Documents", buttonMenu: true, buttonMenuDoc: newFolderButton, treeViewHideTitle: true, targetDropAction: "proto", system: true,
- isFolder: true, treeViewType: TreeViewType.fileSystem, childHideLinkButton: true, boxShadow: "0 0", childDontRegisterViews: true,
- treeViewTruncateTitleWidth: 350, ignoreClick: true, childDropAction: "alias",
+ const reqdOpts:DocumentOptions = { _layout_showTitle: "title", _height: 100, _gridGap: 5, _forceActive: true, _lockedPosition: true,
+ title: "My Documents", layout_headerButton: newFolderButton, treeViewHideTitle: true, dropAction: "proto", isSystem: true,
+ isFolder: true, treeViewType: TreeViewType.fileSystem, childHideLinkButton: true, layout_boxShadow: "0 0", childDontRegisterViews: true,
+ treeViewTruncateTitleWidth: 350, ignoreClick: true, 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 childContextMenuScripts = [newFolder];
@@ -533,22 +546,22 @@ export class CurrentUserUtils {
/// initializes the panel displaying docs that have been recently closed
static setupRecentlyClosed(doc: Doc, field:string) {
- const reqdOpts:DocumentOptions = { _showTitle: "title", _lockedPosition: true, _gridGap: 5, _forceActive: true,
- title: "My Recently Closed", buttonMenu: true, childHideLinkButton: true, treeViewHideTitle: true, childDropAction: "alias", system: true,
- treeViewTruncateTitleWidth: 350, ignoreClick: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same",
+ const reqdOpts:DocumentOptions = { _layout_showTitle: "title", _lockedPosition: true, _gridGap: 5, _forceActive: true,
+ title: "My Recently Closed", childHideLinkButton: true, treeViewHideTitle: true, childDragAction: "embed", isSystem: true,
+ treeViewTruncateTitleWidth: 350, ignoreClick: true, layout_boxShadow: "0 0", childDontRegisterViews: true, dropAction: "same",
contextMenuLabels: new List<string>(["Empty recently closed"]),
contextMenuIcons:new List<string>(["trash"]),
- 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", system: true,
+ const clearBtnsOpts:DocumentOptions = { _width: 30, _height: 30, _forceActive: true, _dragOnlyWithinContainer: true, _layout_hideContextMenu: true,
+ title: "Empty", target: recentlyClosed, btnType: ButtonType.ClickButton, buttonText: "Empty", icon: "trash", isSystem: true,
toolTip: "Empty recently closed",};
- 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 (recentlyClosed.layout_headerButton !== clearDocsButton) Doc.GetProto(recentlyClosed).layout_headerButton = clearDocsButton;
if (!Cast(recentlyClosed.contextMenuScripts, listSpec(ScriptField),null)?.find((script) => script.script.originalScript === clearAll("self"))) {
recentlyClosed.contextMenuScripts = new List<ScriptField>([ScriptField.MakeScript(clearAll("self"))!])
@@ -560,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, system: 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" });
@@ -568,15 +581,15 @@ 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, system: true, flexDirection: "row"
+ _lockedPosition: true, isSystem: true, flexDirection: "row"
})
static createToolButton = (opts: DocumentOptions) => Docs.Create.FontIconDocument({
- btnType: ButtonType.ToolButton, _forceActive: true, _dropAction: "alias", _hideContextMenu: true,
- _removeDropProperties: new List<string>(["_dropAction", "_hideContextMenu", "stayInCollection"]),
- _nativeWidth: 40, _nativeHeight: 40, _width: 40, _height: 40, system: true, ...opts,
+ 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,
})
/// initializes the required buttons in the expanding button menu at the bottom of the Dash window
@@ -587,72 +600,87 @@ 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 audio"}},
- // { scripts: { onClick: 'prevKeyFrame(_readOnly_)'}, opts: { title: "Back", icon: "chevron-left", toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton, width: 20}},
- // { scripts: { onClick:""}, opts: { title: "Num", icon: "", toolTip: "Frame Number", btnType: ButtonType.TextButton, width: 20}, funcs: { buttonText: 'selectedDocs()?.lastElement()?.currentFrame.toString()'}},
- // { scripts: { onClick: 'nextKeyFrame(_readOnly_)'}, opts:{title: "Fwd", icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton, width: 20,} },
- ];
- const btns = btnDescs.map(desc => dockBtn({_width: 30, _height: 30, dontUndo: true, _stayInCollection: true, ...desc.opts}, desc.scripts));
- const dockBtnsReqdOpts = {
- title: "docked buttons", _height: 40, flexGap: 0, boxShadow: "standard",
- childDontRegisterViews: true, linearViewIsExpanded: true, linearViewExpandable: true, ignoreClick: true
+ { 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: "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, defaultDoubleClick: 'ignore', undoIgnoreFields: new List<string>(['opacity']), _dragOnlyWithinContainer: true, ...desc.opts}, desc.scripts));
+ const dockBtnsReqdOpts:DocumentOptions = {
+ title: "docked buttons", _height: 40, flexGap: 0, layout_boxShadow: "standard", childDragAction: 'move',
+ childDontRegisterViews: true, linearView_IsExpanded: true, linearView_Expandable: true, ignoreClick: true
};
reaction(() => UndoManager.redoStack.slice(), () => Doc.GetProto(btns.find(btn => btn.title === "redo")!).opacity = UndoManager.CanRedo() ? 1 : 0.4, { fireImmediately: true });
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);
}
+ static freeTools(): Button[] {
+ return [
+ { title: "Bottom", icon: "arrows-down-to-line",toolTip: "Make doc topmost", btnType: ButtonType.ClickButton, expertMode: false, funcs: {}, scripts: { onClick: 'sendToBack()'}}, // Only when floating document is selected in freeform
+ { title: "Top", icon: "arrows-up-to-line", toolTip: "Make doc bottommost", btnType: ButtonType.ClickButton, expertMode: false, funcs: {}, scripts: { onClick: 'bringToFront()'}}, // Only when floating document is selected in freeform
+ { title: "Z order", icon: "z", toolTip: "Bring Forward on Drag (double click to set for all)",waitForDoubleClickToClick:true, btnType: ButtonType.ToggleButton, expertMode: false, funcs: {}, scripts: { onClick: 'toggleRaiseOnDrag(false, _readOnly_)', onDoubleClick:`{ return toggleRaiseOnDrag(true, _readOnly_)`}}, // Only when floating document is selected in freeform
+ ]
+ }
+ 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: "Cards", icon: "brain", toolTip: "Flashcards", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"flashcards", 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
+ ]
+ }
static textTools():Button[] {
return [
- { title: "Font", toolTip: "Font", width: 100, btnType: ButtonType.DropdownList, ignoreClick: true, scripts: {script: 'setFont(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", width: 75, btnType: ButtonType.NumberButton, ignoreClick: true, scripts: {script: '{ return setFontSize(value, _readOnly_);}'}, numBtnMax: 200, numBtnMin: 0, numBtnType: NumButtonType.DropdownOptions },
- { title: "Color", toolTip: "Font color", btnType: ButtonType.ColorButton, icon: "font", ignoreClick: true, scripts: {script: '{ return setFontColor(value, _readOnly_);}'}},
- { title: "Bold", toolTip: "Bold (Ctrl+B)", btnType: ButtonType.ToggleButton, icon: "bold", scripts: {onClick: '{ return toggleBold(_readOnly_); }'} },
- { title: "Italic", toolTip: "Italic (Ctrl+I)", btnType: ButtonType.ToggleButton, icon: "italic", scripts: {onClick: '{ return toggleItalic(_readOnly_);}'} },
- { title: "Under", toolTip: "Underline (Ctrl+U)", btnType: ButtonType.ToggleButton, icon: "underline", scripts: {onClick: '{ return toggleUnderline(_readOnly_);}'} },
- { title: "Bullets", toolTip: "Bullet List", btnType: ButtonType.ToggleButton, icon: "list", scripts: {onClick: '{ return setBulletList("bullet", _readOnly_);}'} },
- { title: "#", toolTip: "Number List", btnType: ButtonType.ToggleButton, icon: "list-ol", scripts: {onClick: '{ return setBulletList("decimal", _readOnly_);}'} },
-
+ { title: "Font 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: "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()'}},
- { title: "Left", toolTip: "Left align", btnType: ButtonType.ToggleButton, icon: "align-left", scripts: {onClick:'{ return setAlignment("left", _readOnly_);}' }},
- { title: "Center", toolTip: "Center align", btnType: ButtonType.ToggleButton, icon: "align-center", scripts: {onClick:'{ return setAlignment("center", _readOnly_);}'} },
- { title: "Right", toolTip: "Right align", btnType: ButtonType.ToggleButton, icon: "align-right", scripts: {onClick:'{ return setAlignment("right", _readOnly_);}'} },
- { title: "NoLink", toolTip: "Auto Link", btnType: ButtonType.ToggleButton, icon: "link", scripts: {onClick:'{ return toggleNoAutoLinkAnchor(_readOnly_);}'}, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()'}},
- { title: "Dictate",toolTip: "Dictate", btnType: ButtonType.ToggleButton, icon: "microphone", scripts: {onClick:'{ return toggleDictation(_readOnly_);}'}},
- ];
+ ];
}
static inkTools():Button[] {
return [
- { title: "Pen", toolTip: "Pen (Ctrl+P)", btnType: ButtonType.ToggleButton, icon: "pen-nib", scripts: {onClick:'{ return setActiveTool("pen", false, _readOnly_);}' }},
- { title: "Write", toolTip: "Write (Ctrl+Shift+P)", btnType: ButtonType.ToggleButton, icon: "pen", scripts: {onClick:'{ return setActiveTool("write", false, _readOnly_);}'} },
- { title: "Eraser", toolTip: "Eraser (Ctrl+E)", btnType: ButtonType.ToggleButton, icon: "eraser", scripts: {onClick:'{ return setActiveTool("eraser", false, _readOnly_);}' }},
- // { title: "Highlighter", toolTip: "Highlighter (Ctrl+H)", btnType: ButtonType.ToggleButton, icon: "highlighter", scripts:{onClick: 'setActiveTool("highlighter")'} },
- { title: "Circle", toolTip: "Circle (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "circle", scripts: {onClick:`{ return setActiveTool("${GestureUtils.Gestures.Circle}", false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool("${GestureUtils.Gestures.Circle}", true, _readOnly_);}`} },
- { title: "Square", toolTip: "Square (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "square", scripts: {onClick:`{ return setActiveTool("${GestureUtils.Gestures.Rectangle}", false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool("${GestureUtils.Gestures.Rectangle}", true, _readOnly_);}`} },
- { title: "Line", toolTip: "Line (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "minus", scripts: {onClick:`{ return setActiveTool("${GestureUtils.Gestures.Line}", false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool("${GestureUtils.Gestures.Line}", true, _readOnly_);}`} },
- { title: "Mask", toolTip: "Mask", btnType: ButtonType.ToggleButton, icon: "user-circle", scripts: {onClick:'{ return setIsInkMask(_readOnly_);}'} },
- { title: "Fill", toolTip: "Fill color", btnType: ButtonType.ColorButton, icon: "fill-drip",ignoreClick: true, scripts: {script: '{ return setFillColor(value, _readOnly_);}'} },
- { title: "Width", toolTip: "Stroke width", btnType: ButtonType.NumberButton, ignoreClick: true, scripts: {script: '{ return setStrokeWidth(value, _readOnly_);}'}, numBtnType: NumButtonType.Slider, numBtnMin: 1},
- { title: "Color", toolTip: "Stroke color", btnType: ButtonType.ColorButton, icon: "pen", ignoreClick: true, scripts: {script: '{ return setStrokeColor(value, _readOnly_);}'} },
+ { title: "Pen", toolTip: "Pen (Ctrl+P)", btnType: ButtonType.ToggleButton, icon: "pen-nib", toolType: "pen", scripts: {onClick:'{ return setActiveTool(self.toolType, false, _readOnly_);}' }},
+ { title: "Write", toolTip: "Write (Ctrl+Shift+P)", btnType: ButtonType.ToggleButton, icon: "pen", toolType: "write", scripts: {onClick:'{ return setActiveTool(self.toolType, false, _readOnly_);}'} },
+ { title: "Eraser", toolTip: "Eraser (Ctrl+E)", btnType: ButtonType.ToggleButton, icon: "eraser", toolType: "eraser", scripts: {onClick:'{ return setActiveTool(self.toolType, false, _readOnly_);}' }},
+ { title: "Circle", toolTip: "Circle (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "circle", toolType:GestureUtils.Gestures.Circle, scripts: {onClick:`{ return setActiveTool(self.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(self.toolType, true, _readOnly_);}`} },
+ { title: "Square", toolTip: "Square (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "square", toolType:GestureUtils.Gestures.Rectangle, scripts: {onClick:`{ return setActiveTool(self.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(self.toolType, true, _readOnly_);}`} },
+ { title: "Line", toolTip: "Line (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "minus", toolType:GestureUtils.Gestures.Line, scripts: {onClick:`{ return setActiveTool(self.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(self.toolType, true, _readOnly_);}`} },
+ { title: "Mask", toolTip: "Mask", btnType: ButtonType.ToggleButton, icon: "user-circle",toolType: "inkMask", scripts: {onClick:'{ return setInkProperty(self.toolType, value, _readOnly_);}'} },
+ { title: "Width", toolTip: "Stroke width", btnType: ButtonType.NumberSliderButton, toolType: "strokeWidth", ignoreClick: true, scripts: {script: '{ return setInkProperty(self.toolType, value, _readOnly_);}'}, numBtnMin: 1},
+ { title: "Ink", toolTip: "Stroke color", btnType: ButtonType.ColorButton, icon: "pen", toolType: "strokeColor", ignoreClick: true, scripts: {script: '{ return setInkProperty(self.toolType, value, _readOnly_);}'} },
];
}
static schemaTools():Button[] {
- return [{ title: "Show preview", toolTip: "Show preview of selected document", btnType: ButtonType.ToggleButton, buttonText: "Show Preview", icon: "eye", scripts:{ onClick: '{return toggleSchemaPreview(_readOnly_);}'}, }];
+ return [
+ {title: "Show preview", toolTip: "Show selection preview", btnType: ButtonType.ToggleButton, buttonText: "Show Preview", icon: "eye", scripts:{ onClick: '{ return toggleSchemaPreview(_readOnly_); }'} },
+ {title: "Single Lines", toolTip: "Single Line Rows", btnType: ButtonType.ToggleButton, buttonText: "Single Line", icon: "eye", scripts:{ onClick: '{ return toggleSingleLineSchema(_readOnly_); }'} },
+ ];
}
static webTools() {
return [
{ title: "Back", toolTip: "Go back", btnType: ButtonType.ClickButton, icon: "arrow-left", scripts: { onClick: '{ return webBack(_readOnly_); }' }},
{ title: "Forward", toolTip: "Go forward", btnType: ButtonType.ClickButton, icon: "arrow-right", scripts: { onClick: '{ return webForward(_readOnly_); }'}},
- //{ title: "Reload", toolTip: "Reload webpage", btnType: ButtonType.ClickButton, icon: "redo-alt", click: 'webReload()' },
{ title: "URL", toolTip: "URL", width: 250, btnType: ButtonType.EditableText, icon: "lock", ignoreClick: true, scripts: { script: '{ return webSetURL(value, _readOnly_); }'} },
];
}
@@ -664,18 +692,20 @@ export class CurrentUserUtils {
CollectionViewType.Multirow, CollectionViewType.Time, CollectionViewType.Carousel,
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, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "tab")'}, width: 20, scripts: { onClick: 'pinWithView(_readOnly_, altKey)'}},
- { title: "Back", icon: "chevron-left", toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()'}, width: 20, scripts: { onClick: 'prevKeyFrame(_readOnly_)'}},
- { title: "Num", icon: "", toolTip: "Frame Number", btnType: ButtonType.TextButton, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()', buttonText: 'selectedDocs()?.lastElement()?.currentFrame?.toString()'}, width: 20, scripts: {}},
- { title: "Fwd", icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()'}, width: 20, scripts: { onClick: 'nextKeyFrame(_readOnly_)'}},
- { title: "Fill", icon: "fill-drip", toolTip: "Background Fill Color",btnType: ButtonType.ColorButton, funcs: {hidden: '!SelectionManager_selectedDocType()'}, 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, funcs: {hidden: '!SelectionManager_selectedDocType()'}, ignoreClick: true, scripts: { script: 'return setHeaderColor(value, _readOnly_)'}},
- { title: "Overlay", icon: "layer-group", toolTip: "Overlay", btnType: ButtonType.ToggleButton, funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform", true)'}, scripts: { onClick: 'toggleOverlay(_readOnly_)'}}, // Only when floating document is selected in freeform
- { title: "Text", icon: "Text", toolTip: "Text functions", subMenu: CurrentUserUtils.textTools(), funcs: {hidden: 'false', linearViewIsExpanded: `SelectionManager_selectedDocType("${DocumentType.RTF}")`} }, // Always available
- { title: "Ink", icon: "Ink", toolTip: "Ink functions", subMenu: CurrentUserUtils.inkTools(), funcs: {hidden: 'false', linearViewIsExpanded: `SelectionManager_selectedDocType("${DocumentType.INK}")`}, scripts: { onClick: 'setInkToolDefaults()'} }, // Always available
- { title: "Web", icon: "Web", toolTip: "Web functions", subMenu: CurrentUserUtils.webTools(), funcs: {hidden: `!SelectionManager_selectedDocType("${DocumentType.WEB}")`, linearViewIsExpanded: `SelectionManager_selectedDocType("${DocumentType.WEB}")`, } }, // Only when Web is selected
- { title: "Schema", icon: "Schema", toolTip: "Schema functions", subMenu: CurrentUserUtils.schemaTools(), funcs: {hidden: `!SelectionManager_selectedDocType(undefined, "${CollectionViewType.Schema}")`, linearViewIsExpanded: `SelectionManager_selectedDocType(undefined, "${CollectionViewType.Schema}")`} } // Only when Schema is selected
+ 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: { linearView_IsExpanded: `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_IsExpanded: `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_IsExpanded: `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_IsExpanded: `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_IsExpanded: `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_IsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} } // Only when Schema is selected
];
}
@@ -683,12 +713,12 @@ export class CurrentUserUtils {
static setupContextMenuButton(params:Button, btnDoc?:Doc) {
const reqdOpts:DocumentOptions = {
...OmitKeys(params, ["scripts", "funcs", "subMenu"]).omit,
- backgroundColor: params.scripts?.onClick ? undefined: "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, system: true, dontUndo: true,
+ 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,
_nativeWidth: params.width ?? 30, _width: params.width ?? 30,
- _height: 30, _nativeHeight: 30,
- _stayInCollection: true, _hideContextMenu: true, _lockedPosition: true,
- _dropAction: "alias", _removeDropProperties: new List<string>(["dropAction", "_stayInCollection"]),
+ _height: 30, _nativeHeight: 30, linearBtnWidth: params.linearBtnWidth,
+ toolType: params.toolType, expertMode: params.expertMode,
+ _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, _lockedPosition: true,
};
const reqdFuncs:{[key:string]:any} = {
...params.funcs,
@@ -699,16 +729,16 @@ export class CurrentUserUtils {
/// Initializes all the default buttons for the top bar context menu
static setupContextMenuButtons(doc: Doc, field="myContextMenuBtns") {
- const reqdCtxtOpts = { title: "context menu buttons", flexGap: 0, childDontRegisterViews: true, linearViewIsExpanded: true, ignoreClick: true, linearViewExpandable: false, _height: 35 };
+ const reqdCtxtOpts:DocumentOptions = { title: "context menu buttons", undoIgnoreFields:new List<string>(['width', "linearView_IsExpanded"]), flexGap: 0, childDragAction: 'embed', childDontRegisterViews: true, linearView_IsExpanded: true, ignoreClick: true, linearView_Expandable: false, _height: 35 };
const ctxtMenuBtnsDoc = DocUtils.AssignDocField(doc, field, (opts, items) => this.linearButtonList(opts, items??[]), reqdCtxtOpts, undefined);
const ctxtMenuBtns = CurrentUserUtils.contextMenuTools().map(params => {
const menuBtnDoc = DocListCast(ctxtMenuBtnsDoc?.data).find(doc => doc.title === params.title);
if (!params.subMenu) {
return this.setupContextMenuButton(params, menuBtnDoc);
} else {
- const reqdSubMenuOpts = { ...OmitKeys(params, ["scripts", "funcs", "subMenu"]).omit,
+ const reqdSubMenuOpts = { ...OmitKeys(params, ["scripts", "funcs", "subMenu"]).omit, undoIgnoreFields: new List<string>(['width', "linearView_IsExpanded"]),
childDontRegisterViews: true, flexGap: 0, _height: 30, ignoreClick: params.scripts?.onClick ? false : true,
- linearViewSubMenu: true, linearViewExpandable: true, };
+ linearView_SubMenu: true, linearView_Expandable: true, };
const items = params.subMenu?.map(sub =>
this.setupContextMenuButton(sub, DocListCast(menuBtnDoc?.data).find(doc => doc.title === sub.title))
);
@@ -721,11 +751,11 @@ export class CurrentUserUtils {
/// collection of documents rendered in the overlay layer above all tabs and other UI
static setupOverlays(doc: Doc, field = "myOverlayDocs") {
- return DocUtils.AssignDocField(doc, field, (opts) => Docs.Create.FreeformDocument([], opts), { title: "overlay documents", backgroundColor: "#aca3a6", system: true });
+ return DocUtils.AssignDocField(doc, field, (opts) => Docs.Create.FreeformDocument([], opts), { title: "overlay documents", backgroundColor: "#aca3a6", isSystem: true });
}
static setupPublished(doc:Doc, field = "myPublishedDocs") {
- return DocUtils.AssignDocField(doc, field, (opts) => Docs.Create.TreeDocument([], opts), { title: "published docs", backgroundColor: "#aca3a6", system: true });
+ return DocUtils.AssignDocField(doc, field, (opts) => Docs.Create.TreeDocument([], opts), { title: "published docs", backgroundColor: "#aca3a6", isSystem: true });
}
/// The database of all links on all documents
@@ -746,22 +776,23 @@ export class CurrentUserUtils {
// 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 dashboardFilter = ScriptField.MakeFunction(`doc._type_collection === '${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, undoIgnoreFields:new List<string>(['treeViewSortCriterion']),
childContextMenuFilters: new List<ScriptField>([dashboardFilter!,]),
childContextMenuScripts: new List<ScriptField>([addToDashboards!,]),
childContextMenuLabels: new List<string>(["Add to Dashboards",]),
childContextMenuIcons: new List<string>(["user-plus",]),
"acl-Public": SharingPermissions.Augment, "_acl-Public": SharingPermissions.Augment,
- childDropAction: "alias", system: true, contentPointerEvents: "all", childLimitHeight: 0, _yMargin: 0, _gridGap: 15,
- // NOTE: treeViewHideTitle & _showTitle is for a TreeView's editable title, _showTitle is for DocumentViews title bar
- _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'"
+ childDragAction: "embed", isSystem: true, contentPointerEvents: "all", childLimitHeight: 0, _yMargin: 0, _gridGap: 15, childDontRegisterViews:true,
+ // NOTE: treeViewHideTitle & _layout_showTitle is for a TreeView's editable title, _layout_showTitle is for DocumentViews title bar
+ _layout_showTitle: "title", treeViewHideTitle: true, ignoreClick: true, _lockedPosition: true, layout_boxShadow: "0 0", _chromeHidden: true, dontRegisterView: true,
+ layout_explainer: "This is where documents or dashboards that other users have shared with you will appear. To share a document or dashboard right click and select 'Share'"
};
DocUtils.AssignDocField(doc, "mySharedDocs", opts => Docs.Create.TreeDocument([], opts, sharingDocumentId + "layout", sharingDocumentId), sharedDocOpts, undefined, sharedScripts);
@@ -770,17 +801,17 @@ export class CurrentUserUtils {
/// Import option on the left side button panel
static setupImportSidebar(doc: Doc, field:string) {
const reqdOpts:DocumentOptions = {
- title: "My Imports", _forceActive: true, buttonMenu: true, ignoreClick: true, _showTitle: "title",
- _stayInCollection: true, _hideContextMenu: true, childLimitHeight: 0,
- childDropAction: "copy", _autoHeight: true, _yMargin: 50, _gridGap: 15, boxShadow: "0 0", _lockedPosition: true, system: true, _chromeHidden: true,
- dontRegisterView: true, explainer: "This is where documents that are Imported into Dash will go."
+ title: "My Imports", _forceActive: true, ignoreClick: true, _layout_showTitle: "title",
+ _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, childLimitHeight: 0,
+ childDragAction: "copy", _layout_autoHeight: true, _yMargin: 50, _gridGap: 15, layout_boxShadow: "0 0", _lockedPosition: true, isSystem: true, _chromeHidden: true,
+ dontRegisterView: true, layout_explainer: "This is where documents that are Imported into Dash will go."
};
const myImports = DocUtils.AssignDocField(doc, field, (opts) => Docs.Create.StackingDocument([], opts), reqdOpts);
const reqdBtnOpts:DocumentOptions = { _forceActive: true, toolTip: "Import from computer",
- _width: 30, _height: 30, _stayInCollection: true, _hideContextMenu: true, title: "Import", btnType: ButtonType.ClickButton,
- buttonText: "Import", icon: "upload", system: true };
- DocUtils.AssignDocField(myImports, "buttonMenuDoc", (opts) => Docs.Create.FontIconDocument(opts), reqdBtnOpts, undefined, { onClick: "importDocument()" });
+ _width: 30, _height: 30, _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, title: "Import", btnType: ButtonType.ClickButton,
+ buttonText: "Import", icon: "upload", isSystem: true };
+ DocUtils.AssignDocField(myImports, "layout_headerButton", (opts) => Docs.Create.FontIconDocument(opts), reqdBtnOpts, undefined, { onClick: "importDocument()" });
return myImports;
}
/// Updates the UserDoc to have all required fields, docs, etc. No changes should need to be
@@ -788,20 +819,22 @@ export class CurrentUserUtils {
/// whether to revert to "default" values, or to leave them as the user/system last set them.
static updateUserDocument(doc: Doc, sharingDocumentId: string, linkDatabaseId: string) {
DocUtils.AssignDocField(doc, "globalGroupDatabase", () => Docs.Prototypes.MainGroupDocument(), {});
- reaction(() => DateCast(DocCast(doc.globalGroupDatabase)["data-lastModified"]),
+ reaction(() => DateCast(DocCast(doc.globalGroupDatabase)["data_modificationDate"]),
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))]);
}, { fireImmediately: true });
- doc.system ?? (doc.system = true);
+ doc.isSystem ?? (doc.isSystem = true);
doc.title ?? (doc.title = Doc.CurrentUserEmail);
Doc.noviceMode ?? (Doc.noviceMode = true);
doc._raiseWhenDragged ?? (doc._raiseWhenDragged = true);
doc._showLabel ?? (doc._showLabel = true);
doc.textAlign ?? (doc.textAlign = "left");
doc.activeTool = InkTool.None;
- doc.activeInkColor ?? (doc.activeInkColor = "rgb(0, 0, 0)");;
+ doc.openInkInLightbox ?? (doc.openInkInLightbox = false);
+ doc.activeInkHideTextLabels ?? (doc.activeInkHideTextLabels = false);
+ doc.activeInkColor ?? (doc.activeInkColor = "rgb(0, 0, 0)");
doc.activeInkWidth ?? (doc.activeInkWidth = 1);
doc.activeInkBezier ?? (doc.activeInkBezier = "0");
doc.activeFillColor ?? (doc.activeFillColor = "");
@@ -815,7 +848,7 @@ export class CurrentUserUtils {
doc.defaultAclPrivate ?? (doc.defaultAclPrivate = false);
doc.savedFilters ?? (doc.savedFilters = new List<Doc>());
doc.filterDocCount = 0;
- doc.freezeChildren = "remove|add";
+ doc.treeViewFreezeChildren = "remove|add";
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
@@ -828,8 +861,10 @@ export class CurrentUserUtils {
this.setupDocTemplates(doc); // sets up the template menu of templates
this.setupFieldInfos(doc); // sets up the collection of field info descriptions for each possible DocumentOption
DocUtils.AssignDocField(doc, "globalScriptDatabase", (opts) => Docs.Prototypes.MainScriptDocument(), {});
- DocUtils.AssignDocField(doc, "myHeaderBar", (opts) => Docs.Create.MulticolumnDocument([], opts), { title: "header bar", system: true }); // drop down panel at top of dashboard for stashing documents
+ DocUtils.AssignDocField(doc, "myHeaderBar", (opts) => Docs.Create.MulticolumnDocument([], opts), { title: "header bar", isSystem: true, childDocumentsActive:false, dropAction: 'move'}); // drop down panel at top of dashboard for stashing documents
+ Doc.AddDocToList(Doc.MyFilesystem, undefined, Doc.MySharedDocs)
+
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)
doc.activeDashboard.colorScheme = doc.activeDashboard.colorScheme === ColorScheme.Light ? undefined : doc.activeDashboard.colorScheme;
@@ -837,16 +872,17 @@ export class CurrentUserUtils {
new LinkManager();
setTimeout(DocServer.UPDATE_SERVER_CACHE, 2500);
+ setInterval(DocServer.UPDATE_SERVER_CACHE, 120000);
return doc;
}
static setupFieldInfos(doc:Doc, field="fieldInfos") {
- const fieldInfoOpts = { title: "Field Infos", system: true}; // bcz: all possible document options have associated field infos which are stored onn the FieldInfos document **except for title and system which are used as part of the definition of the fieldInfos object
+ const fieldInfoOpts = { title: "Field Infos", isSystem: true}; // bcz: all possible document options have associated field infos which are stored onn the FieldInfos document **except for title and system which are used as part of the definition of the fieldInfos object
const infos = DocUtils.AssignDocField(doc, field, opts => Doc.assign(new Doc(), opts as any), fieldInfoOpts);
const entries = Object.entries(new DocumentOptions());
entries.forEach(pair => {
if (!Array.from(Object.keys(fieldInfoOpts)).includes(pair[0])) {
const options = pair[1] as FInfo;
- const opts:DocumentOptions = { system: true, title: pair[0], ...OmitKeys(options, ["values"]).omit, fieldIsLayout: pair[0].startsWith("_")};
+ const opts:DocumentOptions = { isSystem: true, title: pair[0], ...OmitKeys(options, ["values"]).omit, fieldIsLayout: pair[0].startsWith("_")};
switch (options.fieldType) {
case "boolean": opts.fieldValues = new List<boolean>(options.values as any); break;
case "number": opts.fieldValues = new List<number>(options.values as any); break;
@@ -868,10 +904,12 @@ export class CurrentUserUtils {
if (result.cacheDocumentIds)
{
const ids = result.cacheDocumentIds.split(";");
- const batch = 10000;
+ 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 {
@@ -945,6 +983,5 @@ ScriptingGlobals.add(function toggleComicMode() { Doc.UserDoc().renderStyle = Do
ScriptingGlobals.add(function createNewPresentation() { return MainView.Instance.createNewPresentation(); }, "creates a new presentation when called");
ScriptingGlobals.add(function openPresentation(pres:Doc) { return MainView.Instance.openPresentation(pres); }, "creates a new presentation when called");
ScriptingGlobals.add(function createNewFolder() { return MainView.Instance.createNewFolder(); }, "creates a new folder in myFiles when called");
-ScriptingGlobals.add(function links(doc: any) { return new List(LinkManager.Instance.getAllRelatedLinks(doc)); }, "returns all the links to the document or its annotations", "(doc: any)");
ScriptingGlobals.add(function importDocument() { return CurrentUserUtils.importDocument(); }, "imports files from device directly into the import sidebar");
ScriptingGlobals.add(function setInkToolDefaults() { Doc.ActiveTool = InkTool.None; }); \ No newline at end of file
diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts
index 0a61f3478..717473aa1 100644
--- a/src/client/util/DictationManager.ts
+++ b/src/client/util/DictationManager.ts
@@ -5,13 +5,13 @@ 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';
import { DocumentType } from '../documents/DocumentTypes';
import { DictationOverlay } from '../views/DictationOverlay';
-import { DocumentView } from '../views/nodes/DocumentView';
+import { DocumentView, OpenWhere, OpenWhereMod } from '../views/nodes/DocumentView';
import { SelectionManager } from './SelectionManager';
import { UndoManager } from './UndoManager';
@@ -324,28 +324,18 @@ export namespace DictationManager {
],
[
- 'open fields',
- {
- action: (target: DocumentView) => {
- const kvp = Docs.Create.KVPDocument(target.props.Document, { _width: 300, _height: 300 });
- target.props.addDocTab(kvp, 'add:right');
- },
- },
- ],
-
- [
'new outline',
{
action: (target: DocumentView) => {
- const newBox = Docs.Create.TextDocument('', { _width: 400, _height: 200, title: 'My Outline', _autoHeight: true });
- const proto = newBox.proto!;
+ const newBox = Docs.Create.TextDocument('', { _width: 400, _height: 200, title: 'My Outline', _layout_autoHeight: true });
+ const proto = DocCast(newBox.proto);
const prompt = 'Press alt + r to start dictating here...';
const head = 3;
const anchor = head + prompt.length;
const proseMirrorState = `{"doc":{"type":"doc","content":[{"type":"ordered_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"type":"text","text":"${prompt}"}]}]}]}]},"selection":{"type":"text","anchor":${anchor},"head":${head}}}`;
proto.data = new RichTextField(proseMirrorState);
proto.backgroundColor = '#eeffff';
- target.props.addDocTab(newBox, 'add:right');
+ target.props.addDocTab(newBox, OpenWhere.addRight);
},
},
],
@@ -382,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 b046d950f..7e3302067 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -1,27 +1,32 @@
-import { action, observable, runInAction } from 'mobx';
-import { Doc, DocListCast, DocListCastAsync, Opt } from '../../fields/Doc';
+import { action, computed, observable, ObservableSet } from 'mobx';
+import { Doc, Opt } from '../../fields/Doc';
+import { Animation } from '../../fields/DocSymbols';
import { Id } from '../../fields/FieldSymbols';
-import { Cast } from '../../fields/Types';
-import { returnFalse } from '../../Utils';
-import { DocumentType } from '../documents/DocumentTypes';
+import { listSpec } from '../../fields/Schema';
+import { Cast, DocCast, StrCast } from '../../fields/Types';
+import { AudioField } from '../../fields/URLField';
+import { CollectionViewType } from '../documents/DocumentTypes';
+import { CollectionDockingView } from '../views/collections/CollectionDockingView';
+import { TabDocView } from '../views/collections/TabDocView';
import { LightboxView } from '../views/LightboxView';
-import { DocumentView, ViewAdjustment } from '../views/nodes/DocumentView';
+import { DocFocusOptions, DocumentView, DocumentViewInternal, OpenWhere, OpenWhereMod } from '../views/nodes/DocumentView';
+import { FormattedTextBox } from '../views/nodes/formattedText/FormattedTextBox';
+import { KeyValueBox } from '../views/nodes/KeyValueBox';
import { LinkAnchorBox } from '../views/nodes/LinkAnchorBox';
-import { CollectionDockingView } from '../views/collections/CollectionDockingView';
-import { CollectionFreeFormView } from '../views/collections/collectionFreeForm';
-import { CollectionView } from '../views/collections/CollectionView';
+import { PresBox } from '../views/nodes/trails';
import { ScriptingGlobals } from './ScriptingGlobals';
import { SelectionManager } from './SelectionManager';
-import { listSpec } from '../../fields/Schema';
-import { AudioField } from '../../fields/URLField';
const { Howl } = require('howler');
export class DocumentManager {
//global holds all of the nodes (regardless of which collection they're in)
- @observable public DocumentViews = new Set<DocumentView>();
+ @observable _documentViews = new Set<DocumentView>();
@observable public LinkAnchorBoxViews: DocumentView[] = [];
@observable public RecordingEvent = 0;
@observable public LinkedDocumentViews: { a: DocumentView; b: DocumentView; l: Doc }[] = [];
+ @computed public get DocumentViews() {
+ return Array.from(this._documentViews).filter(view => !(view.ComponentView instanceof KeyValueBox));
+ }
private static _instance: DocumentManager;
public static get Instance(): DocumentManager {
@@ -31,26 +36,56 @@ export class DocumentManager {
//private constructor so no other class can create a nodemanager
private constructor() {}
+ private _viewRenderedCbs: { doc: Doc; func: (dv: DocumentView) => any }[] = [];
+ public AddViewRenderedCb = (doc: Opt<Doc>, func: (dv: DocumentView) => any) => {
+ if (doc) {
+ const dv = this.getDocumentView(doc);
+ this._viewRenderedCbs.push({ doc, func });
+ if (dv) {
+ this.callAddViewFuncs(dv);
+ return true;
+ }
+ } else {
+ func(undefined as any);
+ }
+ return false;
+ };
+ callAddViewFuncs = (view: DocumentView) => {
+ const callFuncs = this._viewRenderedCbs.filter(vc => vc.doc === view.rootDoc);
+ if (callFuncs.length) {
+ this._viewRenderedCbs = this._viewRenderedCbs.filter(vc => !callFuncs.includes(vc));
+ const intTimer = setInterval(
+ () => {
+ if (!view.ComponentView?.incrementalRendering?.()) {
+ callFuncs.forEach(cf => cf.func(view));
+ clearInterval(intTimer);
+ }
+ },
+ view.ComponentView?.incrementalRendering?.() ? 0 : 100
+ );
+ }
+ };
+
@action
public AddView = (view: DocumentView) => {
//console.log("MOUNT " + view.props.Document.title + "/" + view.props.LayoutTemplateString);
if (view.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) {
- const viewAnchorIndex = view.props.LayoutTemplateString.includes('anchor2') ? 'anchor2' : 'anchor1';
- DocListCast(view.rootDoc.links).forEach(link => {
- this.LinkAnchorBoxViews?.filter(dv => Doc.AreProtosEqual(dv.rootDoc, link) && !dv.props.LayoutTemplateString?.includes(viewAnchorIndex)).forEach(otherView =>
- this.LinkedDocumentViews.push({
- a: viewAnchorIndex === 'anchor2' ? otherView : view,
- b: viewAnchorIndex === 'anchor2' ? view : otherView,
- l: link,
- })
- );
- });
+ const viewAnchorIndex = view.props.LayoutTemplateString.includes('link_anchor_2') ? 'link_anchor_2' : 'link_anchor_1';
+ const link = view.rootDoc;
+ this.LinkAnchorBoxViews?.filter(dv => Doc.AreProtosEqual(dv.rootDoc, link) && !dv.props.LayoutTemplateString?.includes(viewAnchorIndex)).forEach(otherView =>
+ this.LinkedDocumentViews.push({
+ a: viewAnchorIndex === 'link_anchor_2' ? otherView : view,
+ b: viewAnchorIndex === 'link_anchor_2' ? view : otherView,
+ l: link,
+ })
+ );
this.LinkAnchorBoxViews.push(view);
// 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._documentViews.add(view);
}
+ this.callAddViewFuncs(view);
};
public RemoveView = action((view: DocumentView) => {
this.LinkedDocumentViews.slice().forEach(
@@ -66,7 +101,7 @@ export class DocumentManager {
const index = this.LinkAnchorBoxViews.indexOf(view);
this.LinkAnchorBoxViews.splice(index, 1);
} else {
- this.DocumentViews.delete(view);
+ this._documentViews.delete(view);
}
SelectionManager.DeselectView(view);
});
@@ -74,15 +109,14 @@ export class DocumentManager {
//gets all views
public getDocumentViewsById(id: string) {
const toReturn: DocumentView[] = [];
- Array.from(DocumentManager.Instance.DocumentViews).map(view => {
+ DocumentManager.Instance.DocumentViews.forEach(view => {
if (view.rootDoc[Id] === id) {
toReturn.push(view);
}
});
if (toReturn.length === 0) {
- Array.from(DocumentManager.Instance.DocumentViews).map(view => {
- const doc = view.rootDoc.proto;
- if (doc && doc[Id] && doc[Id] === id) {
+ DocumentManager.Instance.DocumentViews.forEach(view => {
+ if (Doc.GetProto(view.rootDoc)?.[Id] === id) {
toReturn.push(view);
}
});
@@ -94,54 +128,52 @@ export class DocumentManager {
return this.getDocumentViewsById(doc[Id]);
}
- public getDocumentViewById(id: string, preferredCollection?: CollectionView): DocumentView | undefined {
- if (!id) return undefined;
- let toReturn: DocumentView | undefined;
- const passes = preferredCollection ? [preferredCollection, undefined] : [undefined];
-
- for (const pass of passes) {
- Array.from(DocumentManager.Instance.DocumentViews).map(view => {
- if (view.rootDoc[Id] === id && (!pass || view.props.ContainingCollectionView === preferredCollection)) {
- toReturn = view;
- return;
- }
- });
- if (!toReturn) {
- Array.from(DocumentManager.Instance.DocumentViews).map(view => {
- const doc = view.rootDoc.proto;
- if (doc && doc[Id] === id && (!pass || view.props.ContainingCollectionView === preferredCollection)) {
- toReturn = view;
- }
- });
- } else {
- break;
- }
- }
-
- return toReturn;
- }
-
- public getDocumentView(toFind: Doc, preferredCollection?: CollectionView): DocumentView | undefined {
- return this.getDocumentViewById(toFind[Id], preferredCollection);
+ public getDocumentView(toFind: Doc | undefined, preferredCollection?: DocumentView): DocumentView | undefined {
+ const doc =
+ // bcz: this was temporary code used to match documents by data url instead of by id. intended only for repairing the DB
+ // Array.from(DocumentManager.Instance.DocumentViews).find(
+ // dv =>
+ // ((dv.rootDoc.data as any)?.url?.href && (dv.rootDoc.data as any)?.url?.href === (toFind.data as any)?.url?.href) ||
+ // ((DocCast(dv.rootDoc.annotationOn)?.data as any)?.url?.href && (DocCast(dv.rootDoc.annotationOn)?.data as any)?.url?.href === (DocCast(toFind.annotationOn)?.data as any)?.url?.href)
+ // )?.rootDoc ??
+ toFind;
+ const docViewArray = DocumentManager.Instance.DocumentViews;
+ const passes = !doc ? [] : preferredCollection ? [preferredCollection, undefined] : [undefined];
+ return passes.reduce(
+ (pass, toReturn) =>
+ toReturn ??
+ docViewArray.filter(view => 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>
+ );
}
public getLightboxDocumentView = (toFind: Doc, originatingDoc: Opt<Doc> = undefined): DocumentView | undefined => {
const views: DocumentView[] = [];
- Array.from(DocumentManager.Instance.DocumentViews).map(view => LightboxView.IsLightboxDocView(view.docViewPath) && Doc.AreProtosEqual(view.rootDoc, toFind) && views.push(view));
- return views?.find(view => view.ContentDiv?.getBoundingClientRect().width && view.props.focus !== returnFalse) || views?.find(view => view.props.focus !== returnFalse) || (views.length ? views[0] : undefined);
+ DocumentManager.Instance.DocumentViews.forEach(view => LightboxView.IsLightboxDocView(view.docViewPath) && Doc.AreProtosEqual(view.rootDoc, toFind) && views.push(view));
+ return views?.find(view => view.ContentDiv?.getBoundingClientRect().width /*&& view.props.focus !== returnFalse) || views?.find(view => view.props.focus !== returnFalse*/) || (views.length ? views[0] : undefined);
};
public getFirstDocumentView = (toFind: Doc, originatingDoc: Opt<Doc> = undefined): DocumentView | undefined => {
- const views = this.getDocumentViews(toFind).filter(view => view.rootDoc !== originatingDoc);
- return views?.find(view => view.ContentDiv?.getBoundingClientRect().width && view.props.focus !== returnFalse) || views?.find(view => view.props.focus !== returnFalse) || (views.length ? views[0] : undefined);
+ if (LightboxView.LightboxDoc) return DocumentManager.Instance.getLightboxDocumentView(toFind, originatingDoc);
+ const views = this.getDocumentViews(toFind); //.filter(view => view.rootDoc !== originatingDoc);
+ return views?.find(view => view.ContentDiv?.getBoundingClientRect().width /*&& view.props.focus !== returnFalse) || views?.find(view => view.props.focus !== returnFalse*/) || (views.length ? views[0] : undefined);
};
- public getDocumentViews(toFind: Doc): DocumentView[] {
+ public getDocumentViews(toFindIn: Doc): DocumentView[] {
+ const toFind =
+ // Array.from(DocumentManager.Instance.DocumentViews).find(
+ // dv =>
+ // ((dv.rootDoc.data as any)?.url?.href && (dv.rootDoc.data as any)?.url?.href === (toFindIn.data as any)?.url?.href) ||
+ // ((DocCast(dv.rootDoc.annotationOn)?.data as any)?.url?.href && (DocCast(dv.rootDoc.annotationOn)?.data as any)?.url?.href === (DocCast(toFindIn.annotationOn)?.data as any)?.url?.href)
+ // )?.rootDoc ??
+ toFindIn;
+
const toReturn: DocumentView[] = [];
- const docViews = Array.from(DocumentManager.Instance.DocumentViews).filter(view => !LightboxView.IsLightboxDocView(view.docViewPath));
- const lightViews = Array.from(DocumentManager.Instance.DocumentViews).filter(view => LightboxView.IsLightboxDocView(view.docViewPath));
+ const docViews = DocumentManager.Instance.DocumentViews.filter(view => !LightboxView.IsLightboxDocView(view.docViewPath));
+ const lightViews = DocumentManager.Instance.DocumentViews.filter(view => LightboxView.IsLightboxDocView(view.docViewPath));
// heuristic to return the "best" documents first:
// choose a document in the lightbox first
- // choose an exact match over an alias match
+ // choose an exact match over an embedding match
lightViews.map(view => view.rootDoc === toFind && toReturn.push(view));
lightViews.map(view => view.rootDoc !== toFind && Doc.AreProtosEqual(view.rootDoc, toFind) && toReturn.push(view));
docViews.map(view => view.rootDoc === toFind && toReturn.push(view));
@@ -150,192 +182,159 @@ export class DocumentManager {
return toReturn;
}
+ static GetContextPath(doc: Opt<Doc>, includeExistingViews?: boolean) {
+ if (!doc) return [];
+ const srcContext = DocCast(doc.annotationOn, DocCast(doc.embedContainer));
+ var containerDocContext = srcContext ? [srcContext, doc] : [doc];
+ while (
+ containerDocContext.length &&
+ 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];
+ }
+ return containerDocContext;
+ }
+
+ static playAudioAnno(doc: Doc) {
+ const anno = Cast(doc[Doc.LayoutFieldKey(doc) + '-audioAnnotations'], listSpec(AudioField), null)?.lastElement();
+ if (anno) {
+ if (anno instanceof AudioField) {
+ new Howl({
+ src: [anno.url.href],
+ format: ['mp3'],
+ autoplay: true,
+ loop: false,
+ volume: 0.5,
+ });
+ }
+ }
+ }
+
+ public static removeOverlayViews() {
+ DocumentManager._overlayViews?.forEach(action(view => (view.textHtmlOverlay = undefined)));
+ DocumentManager._overlayViews?.clear();
+ }
+ static _overlayViews = new ObservableSet<DocumentView>();
static addView = (doc: Doc, finished?: () => void) => {
- CollectionDockingView.AddSplit(doc, 'right');
+ CollectionDockingView.AddSplit(doc, OpenWhereMod.right);
finished?.();
};
- public jumpToDocument = (
+
+ // shows a documentView by:
+ // traverses down through the viewPath of contexts to the view:
+ // focusing on each context
+ 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 })));
+ 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);
+ };
+
+ // shows a document by first:
+ // traversing down through the contexts that contain target until an existing view is found
+ // if no container view is found, create one by: opening an existing tab that has the top-level view, or showing the top-level context in the lightbox.
+ // once a containing view is found, it then traverses back down through the contexts to the target document by:
+ // focusing on each context
+ // and finally restoring the targetDoc to the viewSpec specified by the last document which may either be the targetDoc, or a viewSpec that describes the targetDoc configuration
+ public showDocument = async (
targetDoc: Doc, // document to display
- willZoom: boolean, // whether to zoom doc to take up most of screen
- createViewFunc = DocumentManager.addView, // how to create a view of the doc if it doesn't exist
- docContext: Doc[], // context to load that should contain the target
- linkDoc?: Doc, // link that's being followed
- closeContextIfNotFound: boolean = false, // after opening a context where the document should be, this determines whether the context should be closed if the Doc isn't actually there
- originatingDoc: Opt<Doc> = undefined, // doc that initiated the display of the target odoc
- finished?: () => void,
- originalTarget?: Doc,
- noSelect?: boolean,
- presZoomScale?: number
- ): void => {
- originalTarget = originalTarget ?? targetDoc;
- const getFirstDocView = LightboxView.LightboxDoc ? DocumentManager.Instance.getLightboxDocumentView : DocumentManager.Instance.getFirstDocumentView;
- const docView = getFirstDocView(targetDoc, originatingDoc);
- const annotatedDoc = Cast(targetDoc.annotationOn, Doc, null);
- const resolvedTarget = targetDoc.type === DocumentType.MARKER ? annotatedDoc ?? docView?.rootDoc ?? targetDoc : docView?.rootDoc ?? targetDoc; // if target is a marker, then focus toggling should apply to the document it's on since the marker itself doesn't have a hidden field
- var wasHidden = resolvedTarget.hidden;
- if (wasHidden) {
- runInAction(() => {
- resolvedTarget.hidden = false; // if the target is hidden, un-hide it here.
- docView?.props.bringToFront(resolvedTarget);
- });
- }
- const focusAndFinish = (didFocus: boolean) => {
- const finalTargetDoc = resolvedTarget;
- if (originatingDoc?.isPushpin) {
- if (!didFocus && !wasHidden) {
- // don't toggle the hidden state if the doc was already un-hidden as part of this document traversal
- finalTargetDoc.hidden = !finalTargetDoc.hidden;
- }
- } else {
- finalTargetDoc.hidden && (finalTargetDoc.hidden = undefined);
- !noSelect && docView?.select(false);
- if (originatingDoc?.followLinkAudio) {
- const anno = Cast(finalTargetDoc[Doc.LayoutFieldKey(finalTargetDoc) + '-audioAnnotations'], listSpec(AudioField), null).lastElement();
- if (anno) {
- if (anno instanceof AudioField) {
- new Howl({
- src: [anno.url.href],
- format: ['mp3'],
- autoplay: true,
- loop: false,
- volume: 0.5,
- });
- }
- }
+ options: DocFocusOptions, // options for how to navigate to target
+ finished?: () => void
+ ) => {
+ const docContextPath = DocumentManager.GetContextPath(targetDoc, true);
+ if (docContextPath.some(doc => doc.hidden)) options.toggleTarget = false;
+ 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);
+ const contextView = this.getDocumentView(DocCast(target.embedContainer));
+ if (contextView?.docView?._componentView?.addDocTab?.(target, OpenWhere.lightbox)) {
+ await new Promise<void>(waitres => setTimeout(() => waitres()));
}
- finished?.();
+ }
+ docContextPath.shift();
+ const childViewIterator = async (docView: DocumentView) => {
+ const innerDoc = docContextPath.shift();
+ return { viewSpec: innerDoc, childDocView: innerDoc && !innerDoc.layout_unrendered ? (await docView.ComponentView?.getView?.(innerDoc)) ?? this.getDocumentView(innerDoc) : undefined };
};
- const annoContainerView = (!wasHidden || resolvedTarget !== annotatedDoc) && annotatedDoc && getFirstDocView(annotatedDoc);
- const contextDocs = docContext.length ? DocListCast(docContext[0].data) : undefined;
- const contextDoc = contextDocs?.find(doc => Doc.AreProtosEqual(doc, targetDoc) || Doc.AreProtosEqual(doc, annotatedDoc)) ? docContext.lastElement() : undefined;
- const targetDocContext = contextDoc || annotatedDoc;
- const targetDocContextView = (targetDocContext && getFirstDocView(targetDocContext)) || (wasHidden && annoContainerView); // if we have an annotation container and the target was hidden, then try again because we just un-hid the document above
- const focusView = !docView && targetDoc.type === DocumentType.MARKER && annoContainerView ? annoContainerView : docView;
- if (annoContainerView) {
- if (annoContainerView.props.Document.layoutKey === 'layout_icon') {
- annoContainerView.iconify(() =>
- annoContainerView.focus(targetDoc, {
- originalTarget,
- willZoom,
- scale: presZoomScale,
- afterFocus: (didFocus: boolean) =>
- new Promise<ViewAdjustment>(res => {
- focusAndFinish(true);
- res(ViewAdjustment.doNothing);
- }),
- })
- );
- return;
- } else if (!docView && targetDoc.type !== DocumentType.MARKER) {
- annoContainerView.focus(targetDoc, {}); // this allows something like a PDF view to remove its doc filters to expose the target so that it can be found in the retry code below
- }
+ if (rootContextView) {
+ const target = await this.focusViewsInPath(rootContextView, options, childViewIterator);
+ this.restoreDocView(target.viewSpec, target.docView, options, target.contextView ?? target.docView, targetDoc);
}
- if (focusView) {
- !noSelect && Doc.linkFollowHighlight(focusView.rootDoc); //TODO:glr make this a setting in PresBox
- const doFocus = (forceDidFocus: boolean) =>
- focusView.focus(originalTarget ?? targetDoc, {
- originalTarget,
- willZoom,
- scale: presZoomScale,
- afterFocus: (didFocus: boolean) =>
- new Promise<ViewAdjustment>(res => {
- focusAndFinish(forceDidFocus || didFocus);
- res(ViewAdjustment.doNothing);
- }),
- });
- if (focusView.props.Document.layoutKey === 'layout_icon' && focusView.rootDoc.type !== DocumentType.SCRIPTING) {
- focusView.iconify(() => doFocus(true));
- } else {
- doFocus(false);
- }
- } else {
- if (!targetDocContext) {
- // we don't have a view and there's no context specified ... create a new view of the target using the dockFunc or default
- createViewFunc(Doc.BrushDoc(targetDoc), finished); // bcz: should we use this?: Doc.MakeAlias(targetDoc)));
- } else {
- // otherwise try to get a view of the context of the target
- if (targetDocContextView) {
- // we found a context view and aren't forced to create a new one ... focus on the context first..
- wasHidden = wasHidden || targetDocContextView.rootDoc.hidden;
- targetDocContextView.rootDoc.hidden = false; // make sure context isn't hidden
- targetDocContext._viewTransition = 'transform 500ms';
- targetDocContextView.props.focus(targetDocContextView.rootDoc, {
- willZoom,
- afterFocus: async () => {
- targetDocContext._viewTransition = undefined;
- if (targetDocContext.layoutKey === 'layout_icon') {
- targetDocContextView.iconify(() =>
- this.jumpToDocument(resolvedTarget ?? targetDoc, willZoom, createViewFunc, docContext, linkDoc, closeContextIfNotFound, originatingDoc, finished, originalTarget, noSelect, presZoomScale)
- );
- }
- return ViewAdjustment.doNothing;
- },
- });
+ finished?.();
+ };
- // now find the target document within the context
- if (targetDoc._timecodeToShow) {
- // if the target has a timecode, it should show up once the (presumed) video context scrubs to the display timecode;
- targetDocContext._currentTimecode = targetDoc.anchorTimecodeToShow;
- finished?.();
- } else {
- // no timecode means we need to find the context view and focus on our target
- const findView = (delay: number) => {
- const retryDocView = getFirstDocView(resolvedTarget); // test again for the target view snce we presumably created the context above by focusing on it
- if (retryDocView) {
- // we found the target in the context.
- Doc.linkFollowHighlight(retryDocView.rootDoc);
- retryDocView.focus(targetDoc, {
- willZoom,
- afterFocus: (didFocus: boolean) =>
- new Promise<ViewAdjustment>(res => {
- !noSelect && focusAndFinish(true);
- res(ViewAdjustment.doNothing);
- }),
- }); // focus on the target in the context
- } else if (delay > 1000) {
- // we didn't find the target, so it must have moved out of the context. Go back to just creating it.
- if (closeContextIfNotFound) targetDocContextView.props.removeDocument?.(targetDocContextView.rootDoc);
- if (targetDoc.layout) {
- // there will no layout for a TEXTANCHOR type document
- createViewFunc(Doc.BrushDoc(targetDoc), finished); // create a new view of the target
- }
- } else {
- setTimeout(() => findView(delay + 200), 200);
- }
- };
- setTimeout(() => findView(0), 0);
- }
- } else {
- if (docContext.length && docContext[0]?.layoutKey === 'layout_icon') {
- const docContextView = this.getFirstDocumentView(docContext[0]);
- if (docContextView) {
- return docContextView.iconify(() =>
- this.jumpToDocument(targetDoc, willZoom, createViewFunc, docContext.slice(1, docContext.length), linkDoc, closeContextIfNotFound, originatingDoc, finished, originalTarget, noSelect, presZoomScale)
- );
- }
- }
- // there's no context view so we need to create one first and try again when that finishes
- const finishFunc = () => this.jumpToDocument(targetDoc, true, createViewFunc, docContext, linkDoc, true /* if we don't find the target, we want to get rid of the context just created */, undefined, finished, originalTarget);
- createViewFunc(
- targetDocContext, // after creating the context, this calls the finish function that will retry looking for the target
- finishFunc
- );
- }
- }
+ focusViewsInPath = async (docView: DocumentView, options: DocFocusOptions, iterator: (docView: DocumentView) => Promise<{ viewSpec: Opt<Doc>; childDocView: Opt<DocumentView> }>) => {
+ let contextView: DocumentView | undefined; // view containing context that contains target
+ 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 { childDocView, viewSpec } = await iterator(docView);
+ if (!childDocView) return { viewSpec: options.anchorDoc ?? viewSpec ?? docView.rootDoc, docView, contextView };
+ contextView = docView;
+ docView = childDocView;
}
};
-}
-export function DocFocusOrOpen(doc: Doc, collectionDoc?: Doc) {
- const cv = collectionDoc && DocumentManager.Instance.getDocumentView(collectionDoc);
- const dv = DocumentManager.Instance.getDocumentView(doc, (cv?.ComponentView as CollectionFreeFormView)?.props.CollectionView);
- if (dv && Doc.AreProtosEqual(dv.props.Document, doc)) {
- dv.props.focus(dv.props.Document, { willZoom: true });
- Doc.linkFollowHighlight(dv?.props.Document, false);
- } else {
- const context = doc.context !== Doc.MyFilesystem && Cast(doc.context, Doc, null);
- const showDoc = context || doc;
- CollectionDockingView.AddSplit(Doc.BestAlias(showDoc), 'right') && context && setTimeout(() => DocumentManager.Instance.getDocumentView(Doc.GetProto(doc))?.focus(doc, {}));
+
+ @action
+ restoreDocView(viewSpec: Opt<Doc>, docView: DocumentView, options: DocFocusOptions, contextView: Opt<DocumentView>, targetDoc: Doc) {
+ if (viewSpec && docView) {
+ if (docView.ComponentView instanceof FormattedTextBox) docView.ComponentView?.focus(viewSpec, options);
+ PresBox.restoreTargetDocView(docView, viewSpec, options.zoomTime ?? 500);
+ Doc.linkFollowHighlight(docView.rootDoc, undefined, options.effect);
+ if (options.playAudio) DocumentManager.playAudioAnno(docView.rootDoc);
+ if (options.toggleTarget && (!options.didMove || docView.rootDoc.hidden)) docView.rootDoc.hidden = !docView.rootDoc.hidden;
+ if (options.effect) docView.rootDoc[Animation] = options.effect;
+
+ if (options.zoomTextSelections && Doc.UnhighlightTimer && contextView && viewSpec.textHtml) {
+ // if the docView is a text anchor, the contextView is the PDF/Web/Text doc
+ contextView.htmlOverlayEffect = StrCast(options?.effect?.presEffect, StrCast(options?.effect?.followLinkAnimEffect));
+ contextView.textHtmlOverlay = StrCast(targetDoc.textHtml);
+ DocumentManager._overlayViews.add(contextView);
+ }
+ Doc.AddUnHighlightWatcher(() => {
+ docView.rootDoc[Animation] = undefined;
+ DocumentManager.removeOverlayViews();
+ contextView && (contextView.htmlOverlayEffect = '');
+ });
+ }
}
}
+export function DocFocusOrOpen(doc: Doc, options: DocFocusOptions = { willZoomCentered: true, zoomScale: 0, openLocation: OpenWhere.toggleRight }, containingDoc?: Doc) {
+ const func = () => {
+ const cv = DocumentManager.Instance.getDocumentView(containingDoc);
+ const dv = DocumentManager.Instance.getDocumentView(doc, cv);
+ 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 ?? doc);
+ const showDoc = !Doc.IsSystem(container) ? container : doc;
+ options.toggleTarget = undefined;
+ DocumentManager.Instance.showDocument(showDoc, options, () => DocumentManager.Instance.showDocument(doc, { ...options, openLocation: undefined })).then(() => {
+ const cv = DocumentManager.Instance.getDocumentView(containingDoc);
+ const dv = DocumentManager.Instance.getDocumentView(doc, cv);
+ dv && Doc.linkFollowHighlight(dv.rootDoc);
+ });
+ }
+ };
+ if (doc.hidden) {
+ doc.hidden = false;
+ options.toggleTarget = false;
+ setTimeout(func);
+ } else func();
+}
ScriptingGlobals.add(DocFocusOrOpen);
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index d0690fa10..85101fcab 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -1,55 +1,50 @@
import { action, observable, runInAction } from 'mobx';
import { DateField } from '../../fields/DateField';
-import { Doc, Field, Opt } from '../../fields/Doc';
+import { Doc, Field, Opt, StrListCast } from '../../fields/Doc';
import { List } from '../../fields/List';
import { PrefetchProxy } from '../../fields/Proxy';
-import { listSpec } from '../../fields/Schema';
-import { SchemaHeaderField } from '../../fields/SchemaHeaderField';
import { ScriptField } from '../../fields/ScriptField';
-import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../fields/Types';
+import { BoolCast, ScriptCast, StrCast } from '../../fields/Types';
import { emptyFunction, Utils } from '../../Utils';
import { Docs, DocUtils } from '../documents/Documents';
import * as globalCssVariables from '../views/global/globalCssVariables.scss';
+import { Colors } from '../views/global/globalEnums';
import { DocumentView } from '../views/nodes/DocumentView';
+import { ScriptingGlobals } from './ScriptingGlobals';
+import { SelectionManager } from './SelectionManager';
import { SnappingManager } from './SnappingManager';
import { UndoManager } from './UndoManager';
-export type dropActionType = 'alias' | 'copy' | 'move' | 'same' | 'proto' | 'none' | undefined; // undefined = move, "same" = move but don't call removeDropProperties
+export type dropActionType = 'embed' | 'copy' | 'move' | 'same' | 'proto' | 'none' | undefined; // undefined = move, "same" = move but don't call dropPropertiesToRemove
/**
* Initialize drag
* @param _reference: The HTMLElement that is being dragged
* @param docFunc: The Dash document being moved
- * @param moveFunc: The function called when the document is moved
- * @param dropAction: What to do with the document when it is dropped
- * @param dragStarted: Method to call when the drag is started
*/
-export function SetupDrag(_reference: React.RefObject<HTMLElement>, docFunc: () => Doc | Promise<Doc | undefined> | undefined, moveFunc?: DragManager.MoveFunction, dropAction?: dropActionType, dragStarted?: () => void) {
- const onRowMove = async (e: PointerEvent) => {
+export function SetupDrag(_reference: React.RefObject<HTMLElement>, docFunc: () => Doc | undefined) {
+ const onRowMove = (e: PointerEvent) => {
e.stopPropagation();
e.preventDefault();
document.removeEventListener('pointermove', onRowMove);
document.removeEventListener('pointerup', onRowUp);
- const doc = await docFunc();
+ const doc = docFunc();
if (doc) {
const dragData = new DragManager.DocumentDragData([doc]);
- dragData.dropAction = dropAction;
- dragData.moveDocument = moveFunc;
DragManager.StartDocumentDrag([_reference.current!], dragData, e.x, e.y);
- dragStarted?.();
}
};
const onRowUp = (): void => {
document.removeEventListener('pointermove', onRowMove);
document.removeEventListener('pointerup', onRowUp);
};
- const onItemDown = async (e: React.PointerEvent) => {
+ const onItemDown = (e: React.PointerEvent) => {
if (e.button === 0) {
e.stopPropagation();
if (e.shiftKey) {
e.persist();
- const dragDoc = await docFunc();
+ const dragDoc = docFunc();
dragDoc && DragManager.StartWindowDrag?.(e, [dragDoc]);
} else {
document.addEventListener('pointermove', onRowMove);
@@ -130,8 +125,8 @@ export namespace DragManager {
canEmbed?: boolean;
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 'alias', but the document is dropped within the same collection, the drop action will be switched to 'move'
- removeDropProperties?: string[];
+ 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'
+ dropPropertiesToRemove?: string[];
moveDocument?: MoveFunction;
removeDocument?: RemoveFunction;
isDocDecorationMove?: boolean; // Flags that Document decorations are used to drag document which allows suppression of onDragStart scripts
@@ -149,10 +144,14 @@ export namespace DragManager {
linkDragView: DocumentView;
}
export class ColumnDragData {
- constructor(colKey: SchemaHeaderField) {
- this.colKey = colKey;
+ // constructor(colKey: SchemaHeaderField) {
+ // this.colKey = colKey;
+ // }
+ // colKey: SchemaHeaderField;
+ constructor(colIndex: number) {
+ this.colIndex = colIndex;
}
- colKey: SchemaHeaderField;
+ colIndex: number;
}
// used by PDFs,Text,Image,Video,Web to conditionally (if the drop completes) create a text annotation when dragging the annotate button from the AnchorMenu when a text/region selection has been made.
// this is pretty clunky and should be rethought out using linkDrag or DocumentDrag
@@ -162,7 +161,6 @@ export namespace DragManager {
this.dropDocCreator = dropDocCreator;
this.offset = [0, 0];
}
- linkSourceDoc?: Doc;
dropDocCreator: (annotationOn: Doc | undefined) => Doc;
dropDocument?: Doc;
offset: number[];
@@ -170,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");
@@ -178,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);
@@ -189,10 +194,10 @@ export namespace DragManager {
};
}
- // drag a document and drop it (or make an alias/copy on drop)
+ // 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;
};
@@ -201,27 +206,31 @@ export namespace DragManager {
dropEvent?.(); // glr: optional additional function to be called - in this case with presentation trails
if (docDragData && !docDragData.droppedDocuments.length) {
docDragData.dropAction = dragData.userDropAction || dragData.dropAction;
- docDragData.droppedDocuments = await Promise.all(
- dragData.draggedDocuments.map(async d =>
- !dragData.isDocDecorationMove && !dragData.userDropAction && ScriptCast(d.onDragStart)
- ? addAudioTag(ScriptCast(d.onDragStart).script.run({ this: d }).result)
- : docDragData.dropAction === 'alias'
- ? Doc.BestAlias(d)
- : docDragData.dropAction === 'proto'
- ? Doc.GetProto(d)
- : docDragData.dropAction === 'copy'
- ? (
- await Doc.MakeClone(d)
- ).clone
- : d
+ docDragData.droppedDocuments = (
+ await Promise.all(
+ dragData.draggedDocuments.map(async d =>
+ !dragData.isDocDecorationMove && !dragData.userDropAction && ScriptCast(d.onDragStart)
+ ? addAudioTag(ScriptCast(d.onDragStart).script.run({ this: d }).result)
+ : docDragData.dropAction === 'embed'
+ ? Doc.BestEmbedding(d)
+ : docDragData.dropAction === 'proto'
+ ? Doc.GetProto(d)
+ : docDragData.dropAction === 'copy'
+ ? (
+ await Doc.MakeClone(d)
+ ).clone
+ : d
+ )
)
- );
+ ).filter(d => d);
!['same', 'proto'].includes(docDragData.dropAction as any) &&
- docDragData.droppedDocuments.forEach((drop: Doc, i: number) => {
- const dragProps = Cast(dragData.draggedDocuments[i].removeDropProperties, listSpec('string'), []);
- 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;
};
@@ -251,13 +260,13 @@ export namespace DragManager {
}
// drags a linker button and creates a link on drop
- export function StartLinkDrag(ele: HTMLElement, sourceView: DocumentView, sourceDocGetAnchor: undefined | (() => Doc), downX: number, downY: number, options?: DragOptions) {
- StartDrag([ele], new DragManager.LinkDragData(sourceView, () => sourceDocGetAnchor?.() ?? sourceView.rootDoc), downX, downY, options);
+ export function StartLinkDrag(ele: HTMLElement, sourceView: DocumentView, sourceDocGetAnchor: undefined | ((addAsAnnotation: boolean) => Doc), downX: number, downY: number, options?: DragOptions) {
+ StartDrag([ele], new DragManager.LinkDragData(sourceView, () => sourceDocGetAnchor?.(true) ?? sourceView.rootDoc), downX, downY, options);
}
// 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);
+ export function StartColumnDrag(ele: HTMLElement[], dragData: ColumnDragData, downX: number, downY: number, options?: DragOptions) {
+ StartDrag(ele, dragData, downX, downY, options, undefined, 'Drag Column');
}
export function SetSnapLines(horizLines: number[], vertLines: number[]) {
@@ -319,10 +328,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) {
@@ -350,7 +359,7 @@ export namespace DragManager {
top: Number.MAX_SAFE_INTEGER,
bottom: Number.MIN_SAFE_INTEGER,
};
- let rot = 0;
+ let rot: number[] = [];
const docsToDrag = dragData instanceof DocumentDragData ? dragData.draggedDocuments : dragData instanceof AnchorAnnoDragData ? [dragData.dragDocument] : [];
const dragElements = eles.map(ele => {
// bcz: very hacky -- if dragged element is a freeForm view with a rotation, then extract the rotation in order to apply it to the dragged element
@@ -358,9 +367,10 @@ export namespace DragManager {
// if the parent isn't a freeform view, then the element's width and height are presumed to match the acutal doc's dimensions (eg, dragging from import sidebar menu)
if (ele?.parentElement?.parentElement?.parentElement?.className === 'collectionFreeFormDocumentView-container') {
ele = ele.parentElement.parentElement.parentElement;
- rot = Number(ele.style.transform.replace(/.*rotate\(([-0-9.e]*)deg\).*/, '$1') || 0);
+ rot.push(Number(ele.style.transform.replace(/.*rotate\(([-0-9.e]*)deg\).*/, '$1') || 0));
} else {
useDim = true;
+ rot.push(0);
}
if (!ele.parentNode) dragDiv.appendChild(ele);
const dragElement = ele.parentNode === dragDiv ? ele : (ele.cloneNode(true) as HTMLElement);
@@ -383,7 +393,7 @@ export namespace DragManager {
const rect = ele.getBoundingClientRect();
const w = ele.offsetWidth || rect.width;
const h = ele.offsetHeight || rect.height;
- const rotR = -((rot < 0 ? rot + 360 : rot) / 180) * Math.PI;
+ const rotR = -((rot.lastElement() < 0 ? rot.lastElement() + 360 : rot.lastElement()) / 180) * Math.PI;
const tl = [0, 0];
const tr = [Math.cos(rotR) * w, Math.sin(-rotR) * w];
const bl = [Math.sin(rotR) * h, Math.cos(-rotR) * h];
@@ -417,7 +427,7 @@ export namespace DragManager {
transformOrigin: '0 0',
width,
height,
- transform: `translate(${xs[0]}px, ${ys[0]}px) rotate(${rot}deg) scale(${scaling})`,
+ transform: `translate(${xs[0]}px, ${ys[0]}px) rotate(${rot.lastElement()}deg) scale(${scaling})`,
});
dragLabel.style.transform = `translate(${xs[0]}px, ${ys[0] - 20}px)`;
@@ -480,7 +490,7 @@ export namespace DragManager {
const moveHandler = (e: PointerEvent) => {
e.preventDefault(); // required or dragging text menu link item ends up dragging the link button as native drag/drop
if (dragData instanceof DocumentDragData) {
- dragData.userDropAction = e.ctrlKey && e.altKey ? 'copy' : e.ctrlKey ? 'alias' : dragData.defaultDropAction;
+ dragData.userDropAction = e.ctrlKey && e.altKey ? 'copy' : e.ctrlKey ? 'embed' : dragData.defaultDropAction;
}
if (((e.target as any)?.className === 'lm_tabs' || (e.target as any)?.className === 'lm_header' || e?.shiftKey) && dragData.draggedDocuments.length === 1) {
if (!startWindowDragTimer) {
@@ -503,7 +513,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', {
@@ -558,7 +568,7 @@ export namespace DragManager {
const moveVec = { x: x - lastPt.x, y: y - lastPt.y };
lastPt = { x, y };
- dragElements.map((dragElement, i) => (dragElement.style.transform = `translate(${(xs[i] += moveVec.x)}px, ${(ys[i] += moveVec.y)}px) rotate(${rot}deg) scale(${scalings[i]})`));
+ dragElements.map((dragElement, i) => (dragElement.style.transform = `translate(${(xs[i] += moveVec.x)}px, ${(ys[i] += moveVec.y)}px) rotate(${rot[i]}deg) scale(${scalings[i]})`));
dragLabel.style.transform = `translate(${xs[0]}px, ${ys[0] - 20}px)`;
};
const upHandler = (e: PointerEvent) => {
@@ -572,6 +582,7 @@ export namespace DragManager {
async function dispatchDrag(target: Element, e: PointerEvent, complete: DragCompleteEvent, pos: { x: number; y: number }, finishDrag?: (e: DragCompleteEvent) => void, options?: DragOptions, endDrag?: () => void) {
const dropArgs = {
+ cancelable: true, // allows preventDefault() to be called to cancel the drop
bubbles: true,
detail: {
...pos,
@@ -584,9 +595,26 @@ 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?.();
}
}
+
+ScriptingGlobals.add(function toggleRaiseOnDrag(forAllDocs: boolean, readOnly?: boolean) {
+ if (readOnly) {
+ if (SelectionManager.Views().length)
+ return SelectionManager.Views().some(dv => dv.rootDoc.raiseWhenDragged)
+ ? Colors.MEDIUM_BLUE
+ : SelectionManager.Views().some(dv => dv.rootDoc.raiseWhenDragged === false)
+ ? 'transparent'
+ : DragManager.GetRaiseWhenDragged()
+ ? Colors.MEDIUM_BLUE_ALT
+ : Colors.LIGHT_BLUE;
+ return DragManager.GetRaiseWhenDragged() ? Colors.MEDIUM_BLUE_ALT : 'transparent';
+ }
+ 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 7c209d1e0..47997cc5c 100644
--- a/src/client/util/DropConverter.ts
+++ b/src/client/util/DropConverter.ts
@@ -33,15 +33,7 @@ function makeTemplate(doc: Doc, first: boolean = true, rename: Opt<string> = und
let any = false;
docs.forEach(d => {
if (!StrCast(d.title).startsWith('-')) {
- const params = StrCast(d.title)
- .match(/\(([a-zA-Z0-9._\-]*)\)/)?.[1]
- .replace('()', '');
- if (params) {
- any = makeTemplate(d, false) || any;
- d.PARAMS = params;
- } else {
- any = Doc.MakeMetadataFieldTemplate(d, Doc.GetProto(layoutDoc)) || any;
- }
+ any = Doc.MakeMetadataFieldTemplate(d, Doc.GetProto(layoutDoc)) || any;
} else if (d.type === DocumentType.COL || d.data instanceof RichTextField) {
any = makeTemplate(d, false) || any;
}
@@ -65,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) {
- //dbox = Doc.MakeAlias(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.MakeAlias(dbox);
- const dragProps = Cast(dbox.removeDropProperties, listSpec('string'), []);
- const remProps = (data.removeDropProperties || []).concat(Array.from(dragProps));
+ 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.dropPropertiesToRemove, listSpec('string'), []);
+ const remProps = (data.dropPropertiesToRemove || []).concat(Array.from(dragProps));
remProps.map(prop => (dbox[prop] = undefined));
}
} else if (!doc.onDragStart && !doc.isButtonBar) {
@@ -89,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.tsx b/src/client/util/GroupManager.tsx
index c8b784390..da947aba6 100644
--- a/src/client/util/GroupManager.tsx
+++ b/src/client/util/GroupManager.tsx
@@ -155,7 +155,7 @@ export class GroupManager extends React.Component<{}> {
addGroup(groupDoc: Doc): boolean {
if (this.GroupManagerDoc) {
Doc.AddDocToList(this.GroupManagerDoc, 'data', groupDoc);
- this.GroupManagerDoc['data-lastModified'] = new DateField();
+ this.GroupManagerDoc['data_modificationDate'] = new DateField();
return true;
}
return false;
@@ -176,7 +176,7 @@ export class GroupManager extends React.Component<{}> {
const index = DocListCast(this.GroupManagerDoc.data).findIndex(grp => grp === group);
index !== -1 && Cast(this.GroupManagerDoc.data, listSpec(Doc), [])?.splice(index, 1);
}
- this.GroupManagerDoc['data-lastModified'] = new DateField();
+ this.GroupManagerDoc['data_modificationDate'] = new DateField();
if (group === this.currentGroup) {
this.currentGroup = undefined;
}
@@ -197,7 +197,7 @@ export class GroupManager extends React.Component<{}> {
!memberList.includes(email) && memberList.push(email);
groupDoc.members = JSON.stringify(memberList);
SharingManager.Instance.shareWithAddedMember(groupDoc, email);
- this.GroupManagerDoc && (this.GroupManagerDoc['data-lastModified'] = new DateField());
+ this.GroupManagerDoc && (this.GroupManagerDoc['data_modificationDate'] = new DateField());
}
}
@@ -214,7 +214,7 @@ export class GroupManager extends React.Component<{}> {
const user = memberList.splice(index, 1)[0];
groupDoc.members = JSON.stringify(memberList);
SharingManager.Instance.removeMember(groupDoc, email);
- this.GroupManagerDoc && (this.GroupManagerDoc['data-lastModified'] = new DateField());
+ this.GroupManagerDoc && (this.GroupManagerDoc['data_modificationDate'] = new DateField());
}
}
}
diff --git a/src/client/util/HypothesisUtils.ts b/src/client/util/HypothesisUtils.ts
index 297520d5d..151f18d6f 100644
--- a/src/client/util/HypothesisUtils.ts
+++ b/src/client/util/HypothesisUtils.ts
@@ -20,7 +20,7 @@ export namespace Hypothesis {
export const getSourceWebDoc = async (uri: string) => {
const result = await findWebDoc(uri);
console.log(result ? 'existing doc found' : 'existing doc NOT found');
- return result || Docs.Create.WebDocument(uri, { title: uri, _nativeWidth: 850, _height: 512, _width: 400, useCors: true }); // create and return a new Web doc with given uri if no matching docs are found
+ return result || Docs.Create.WebDocument(uri, { title: uri, _nativeWidth: 850, _height: 512, _width: 400, data_useCors: true }); // create and return a new Web doc with given uri if no matching docs are found
};
/**
diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index 37571ae01..1a4c2450e 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx
@@ -1,27 +1,29 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { BatchedArray } from "array-batcher";
-import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx";
-import { observer } from "mobx-react";
-import { extname } from "path";
-import Measure, { ContentRect } from "react-measure";
-import { Doc, DocListCast, DocListCastAsync, Opt } from "../../../fields/Doc";
-import { Id } from "../../../fields/FieldSymbols";
-import { List } from "../../../fields/List";
-import { listSpec } from "../../../fields/Schema";
-import { SchemaHeaderField } from "../../../fields/SchemaHeaderField";
-import { BoolCast, Cast, NumCast } from "../../../fields/Types";
-import { AcceptableMedia, Upload } from "../../../server/SharedMediaTypes";
-import { Utils } from "../../../Utils";
-import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils";
-import { Docs, DocumentOptions, DocUtils } from "../../documents/Documents";
-import { Networking } from "../../Network";
-import { FieldView, FieldViewProps } from "../../views/nodes/FieldView";
-import { DocumentManager } from "../DocumentManager";
-import "./DirectoryImportBox.scss";
-import ImportMetadataEntry, { keyPlaceholder, valuePlaceholder } from "./ImportMetadataEntry";
-import React = require("react");
+import { BatchedArray } from 'array-batcher';
+import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
+import { observer } from 'mobx-react';
+import { extname } from 'path';
+import Measure, { ContentRect } from 'react-measure';
+import { Doc, DocListCast, DocListCastAsync, Opt } from '../../../fields/Doc';
+import { Id } from '../../../fields/FieldSymbols';
+import { List } from '../../../fields/List';
+import { listSpec } from '../../../fields/Schema';
+import { SchemaHeaderField } from '../../../fields/SchemaHeaderField';
+import { BoolCast, Cast, NumCast } from '../../../fields/Types';
+import { AcceptableMedia, Upload } from '../../../server/SharedMediaTypes';
+import { Utils } from '../../../Utils';
+import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
+import { Docs, DocumentOptions, DocUtils } from '../../documents/Documents';
+import { DocumentType } from '../../documents/DocumentTypes';
+import { Networking } from '../../Network';
+import { FieldView, FieldViewProps } from '../../views/nodes/FieldView';
+import { DocumentManager } from '../DocumentManager';
+import './DirectoryImportBox.scss';
+import ImportMetadataEntry, { keyPlaceholder, valuePlaceholder } from './ImportMetadataEntry';
+import React = require('react');
+import _ = require('lodash');
-const unsupported = ["text/html", "text/plain"];
+const unsupported = ['text/html', 'text/plain'];
@observer
export class DirectoryImportBox extends React.Component<FieldViewProps> {
@@ -29,7 +31,7 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> {
@observable private top = 0;
@observable private left = 0;
private dimensions = 50;
- @observable private phase = "";
+ @observable private phase = '';
private disposer: Opt<IReactionDisposer>;
@observable private entries: ImportMetadataEntry[] = [];
@@ -40,7 +42,9 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> {
@observable private uploading = false;
@observable private removeHover = false;
- public static LayoutString(fieldKey: string) { return FieldView.LayoutString(DirectoryImportBox, fieldKey); }
+ public static LayoutString(fieldKey: string) {
+ return FieldView.LayoutString(DirectoryImportBox, fieldKey);
+ }
constructor(props: FieldViewProps) {
super(props);
@@ -71,7 +75,7 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> {
handleSelection = async (e: React.ChangeEvent<HTMLInputElement>) => {
runInAction(() => {
this.uploading = true;
- this.phase = "Initializing download...";
+ this.phase = 'Initializing download...';
});
const docs: Doc[] = [];
@@ -79,7 +83,7 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> {
const files = e.target.files;
if (!files || files.length === 0) return;
- const directory = (files.item(0) as any).webkitRelativePath.split("/", 1)[0];
+ const directory = (files.item(0) as any).webkitRelativePath.split('/', 1)[0];
const validated: File[] = [];
for (let i = 0; i < files.length; i++) {
@@ -100,7 +104,7 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> {
const sizes: number[] = [];
const modifiedDates: number[] = [];
- runInAction(() => this.phase = `Internal: uploading ${this.quota - this.completed} files to Dash...`);
+ runInAction(() => (this.phase = `Internal: uploading ${this.quota - this.completed} files to Dash...`));
const batched = BatchedArray.from(validated, { batchSize: 15 });
const uploads = await batched.batchedMapAsync<Upload.FileResponse<Upload.ImageInformation>>(async (batch, collector) => {
@@ -108,24 +112,29 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> {
sizes.push(file.size);
modifiedDates.push(file.lastModified);
});
- collector.push(...(await Networking.UploadFilesToServer<Upload.ImageInformation>(batch)));
- runInAction(() => this.completed += batch.length);
+ collector.push(...(await Networking.UploadFilesToServer<Upload.ImageInformation>(batch.map(file =>({file})))));
+ runInAction(() => (this.completed += batch.length));
});
- await Promise.all(uploads.map(async response => {
- const { source: { type }, result } = response;
- if (result instanceof Error) {
- return;
- }
- const { accessPaths, exifData } = result;
- const path = Utils.prepend(accessPaths.agnostic.client);
- const document = type && await DocUtils.DocumentFromType(type, path, { _width: 300 });
- const { data, error } = exifData;
- if (document) {
- Doc.GetProto(document).exif = error || Doc.Get.FromJson({ data });
- docs.push(document);
- }
- }));
+ await Promise.all(
+ uploads.map(async response => {
+ const {
+ source: { type },
+ result,
+ } = response;
+ if (result instanceof Error) {
+ return;
+ }
+ const { accessPaths, exifData } = result;
+ const path = Utils.prepend(accessPaths.agnostic.client);
+ const document = type && (await DocUtils.DocumentFromType(type, path, { _width: 300 }));
+ const { data, error } = exifData;
+ if (document) {
+ Doc.GetProto(document).exif = error || Doc.Get.FromJson({ data });
+ docs.push(document);
+ }
+ })
+ );
for (let i = 0; i < docs.length; i++) {
const doc = docs[i];
@@ -146,22 +155,22 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> {
_height: 500,
_chromeHidden: true,
x: NumCast(doc.x),
- y: NumCast(doc.y) + offset
+ y: NumCast(doc.y) + offset,
};
- const parent = this.props.ContainingCollectionView;
- if (parent) {
+ const parent = this.props.DocumentView?.().props.docViewPath().lastElement();
+ if (parent?.rootDoc.type === DocumentType.COL) {
let importContainer: Doc;
if (docs.length < 50) {
importContainer = Docs.Create.MasonryDocument(docs, options);
} else {
- const headers = [new SchemaHeaderField("title"), new SchemaHeaderField("size")];
+ const headers = [new SchemaHeaderField('title'), new SchemaHeaderField('size')];
importContainer = Docs.Create.SchemaDocument(headers, docs, options);
}
- runInAction(() => this.phase = 'External: uploading files to Google Photos...');
+ runInAction(() => (this.phase = 'External: uploading files to Google Photos...'));
await GooglePhotos.Export.CollectionToAlbum({ collection: importContainer });
- Doc.AddDocToList(Doc.GetProto(parent.props.Document), "data", importContainer);
+ Doc.AddDocToList(Doc.GetProto(parent.props.Document), 'data', importContainer);
!this.persistent && this.props.removeDocument && this.props.removeDocument(doc);
- DocumentManager.Instance.jumpToDocument(importContainer, true, undefined, []);
+ DocumentManager.Instance.showDocument(importContainer, { willZoomCentered: true });
}
runInAction(() => {
@@ -169,14 +178,14 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> {
this.quota = 1;
this.completed = 0;
});
- }
+ };
componentDidMount() {
- this.selector.current!.setAttribute("directory", "");
- this.selector.current!.setAttribute("webkitdirectory", "");
+ this.selector.current!.setAttribute('directory', '');
+ this.selector.current!.setAttribute('webkitdirectory', '');
this.disposer = reaction(
() => this.completed,
- completed => runInAction(() => this.phase = `Internal: uploading ${this.quota - completed} files to Dash...`)
+ completed => runInAction(() => (this.phase = `Internal: uploading ${this.quota - completed} files to Dash...`))
);
}
@@ -193,7 +202,7 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> {
const offset = this.dimensions / 2;
this.left = bounds.width / 2 - offset;
this.top = bounds.height / 2 - offset;
- }
+ };
@action
addMetadataEntry = async () => {
@@ -201,8 +210,8 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> {
entryDoc.checked = false;
entryDoc.key = keyPlaceholder;
entryDoc.value = valuePlaceholder;
- Doc.AddDocToList(this.props.Document, "data", entryDoc);
- }
+ Doc.AddDocToList(this.props.Document, 'data', entryDoc);
+ };
@action
remove = async (entry: ImportMetadataEntry) => {
@@ -217,7 +226,7 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> {
}
}
}
- }
+ };
render() {
const dimensions = 50;
@@ -228,193 +237,204 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> {
const uploading = this.uploading;
const showRemoveLabel = this.removeHover;
const persistent = this.persistent;
- let percent = `${completed / quota * 100}`;
- percent = percent.split(".")[0];
- percent = percent.startsWith("100") ? "99" : percent;
+ let percent = `${(completed / quota) * 100}`;
+ percent = percent.split('.')[0];
+ percent = percent.startsWith('100') ? '99' : percent;
const marginOffset = (percent.length === 1 ? 5 : 0) - 1.6;
- const message = <span className={"phase"}>{this.phase}</span>;
- const centerPiece = this.phase.includes("Google Photos") ?
- <img src={"/assets/google_photos.png"} style={{
- transition: "0.4s opacity ease",
- width: 30,
- height: 30,
- opacity: uploading ? 1 : 0,
- pointerEvents: "none",
- position: "absolute",
- left: 12,
- top: this.top + 10,
- fontSize: 18,
- color: "white",
- marginLeft: this.left + marginOffset
- }} />
- : <div
+ const message = <span className={'phase'}>{this.phase}</span>;
+ const centerPiece = this.phase.includes('Google Photos') ? (
+ <img
+ src={'/assets/google_photos.png'}
style={{
- transition: "0.4s opacity ease",
+ transition: '0.4s opacity ease',
+ width: 30,
+ height: 30,
opacity: uploading ? 1 : 0,
- pointerEvents: "none",
- position: "absolute",
+ pointerEvents: 'none',
+ position: 'absolute',
+ left: 12,
+ top: this.top + 10,
+ fontSize: 18,
+ color: 'white',
+ marginLeft: this.left + marginOffset,
+ }}
+ />
+ ) : (
+ <div
+ style={{
+ transition: '0.4s opacity ease',
+ opacity: uploading ? 1 : 0,
+ pointerEvents: 'none',
+ position: 'absolute',
left: 10,
top: this.top + 12.3,
fontSize: 18,
- color: "white",
- marginLeft: this.left + marginOffset
- }}>{percent}%</div>;
+ color: 'white',
+ marginLeft: this.left + marginOffset,
+ }}>
+ {percent}%
+ </div>
+ );
return (
<Measure offset onResize={this.preserveCentering}>
- {({ measureRef }) =>
- <div ref={measureRef} style={{ width: "100%", height: "100%", pointerEvents: "all" }} >
+ {({ measureRef }) => (
+ <div ref={measureRef} style={{ width: '100%', height: '100%', pointerEvents: 'all' }}>
{message}
<input
- id={"selector"}
+ id={'selector'}
ref={this.selector}
onChange={this.handleSelection}
type="file"
style={{
- position: "absolute",
- display: "none"
- }} />
+ position: 'absolute',
+ display: 'none',
+ }}
+ />
<label
- htmlFor={"selector"}
+ htmlFor={'selector'}
style={{
opacity: isEditing ? 0 : 1,
- pointerEvents: isEditing ? "none" : "all",
- transition: "0.4s ease opacity"
- }}
- >
- <div style={{
- width: dimensions,
- height: dimensions,
- borderRadius: "50%",
- background: "black",
- position: "absolute",
- left: this.left,
- top: this.top
- }} />
- <div style={{
- position: "absolute",
- left: this.left + 8,
- top: this.top + 10,
- opacity: uploading ? 0 : 1,
- transition: "0.4s opacity ease"
+ pointerEvents: isEditing ? 'none' : 'all',
+ transition: '0.4s ease opacity',
}}>
- <FontAwesomeIcon icon={"cloud-upload-alt"} color="#FFFFFF" size={"2x"} />
+ <div
+ style={{
+ width: dimensions,
+ height: dimensions,
+ borderRadius: '50%',
+ background: 'black',
+ position: 'absolute',
+ left: this.left,
+ top: this.top,
+ }}
+ />
+ <div
+ style={{
+ position: 'absolute',
+ left: this.left + 8,
+ top: this.top + 10,
+ opacity: uploading ? 0 : 1,
+ transition: '0.4s opacity ease',
+ }}>
+ <FontAwesomeIcon icon={'cloud-upload-alt'} color="#FFFFFF" size={'2x'} />
</div>
<img
style={{
width: 80,
height: 80,
- transition: "0.4s opacity ease",
+ transition: '0.4s opacity ease',
opacity: uploading ? 0.7 : 0,
- position: "absolute",
+ position: 'absolute',
top: this.top - 15,
- left: this.left - 15
+ left: this.left - 15,
}}
- src={"/assets/loading.gif"}></img>
+ src={'/assets/loading.gif'}></img>
</label>
<input
- type={"checkbox"}
- onChange={e => runInAction(() => this.persistent = e.target.checked)}
+ type={'checkbox'}
+ onChange={e => runInAction(() => (this.persistent = e.target.checked))}
style={{
margin: 0,
- position: "absolute",
+ position: 'absolute',
left: 10,
bottom: 10,
opacity: isEditing || uploading ? 0 : 1,
- transition: "0.4s opacity ease",
- pointerEvents: isEditing || uploading ? "none" : "all"
+ transition: '0.4s opacity ease',
+ pointerEvents: isEditing || uploading ? 'none' : 'all',
}}
checked={this.persistent}
- onPointerEnter={action(() => this.removeHover = true)}
- onPointerLeave={action(() => this.removeHover = false)}
+ onPointerEnter={action(() => (this.removeHover = true))}
+ onPointerLeave={action(() => (this.removeHover = false))}
/>
<p
style={{
- position: "absolute",
+ position: 'absolute',
left: 27,
bottom: 8.4,
fontSize: 12,
opacity: showRemoveLabel ? 1 : 0,
- transition: "0.4s opacity ease"
- }}>Template will be <span style={{ textDecoration: "underline", textDecorationColor: persistent ? "green" : "red", color: persistent ? "green" : "red" }}>{persistent ? "kept" : "removed"}</span> after upload</p>
+ transition: '0.4s opacity ease',
+ }}>
+ Template will be <span style={{ textDecoration: 'underline', textDecorationColor: persistent ? 'green' : 'red', color: persistent ? 'green' : 'red' }}>{persistent ? 'kept' : 'removed'}</span> after upload
+ </p>
{centerPiece}
<div
style={{
- position: "absolute",
+ position: 'absolute',
top: 10,
right: 10,
- borderRadius: "50%",
+ borderRadius: '50%',
width: 25,
height: 25,
- background: "black",
- pointerEvents: uploading ? "none" : "all",
+ background: 'black',
+ pointerEvents: uploading ? 'none' : 'all',
opacity: uploading ? 0 : 1,
- transition: "0.4s opacity ease"
+ transition: '0.4s opacity ease',
}}
- title={isEditing ? "Back to Upload" : "Add Metadata"}
- onClick={action(() => this.editingMetadata = !this.editingMetadata)}
+ title={isEditing ? 'Back to Upload' : 'Add Metadata'}
+ onClick={action(() => (this.editingMetadata = !this.editingMetadata))}
/>
<FontAwesomeIcon
style={{
- pointerEvents: "none",
- position: "absolute",
+ pointerEvents: 'none',
+ position: 'absolute',
right: isEditing ? 14 : 15,
top: isEditing ? 15.4 : 16,
opacity: uploading ? 0 : 1,
- transition: "0.4s opacity ease"
+ transition: '0.4s opacity ease',
}}
- icon={isEditing ? "cloud-upload-alt" : "tag"}
+ icon={isEditing ? 'cloud-upload-alt' : 'tag'}
color="#FFFFFF"
- size={"1x"}
+ size={'1x'}
/>
<div
style={{
- transition: "0.4s ease opacity",
- width: "100%",
- height: "100%",
- pointerEvents: isEditing ? "all" : "none",
+ transition: '0.4s ease opacity',
+ width: '100%',
+ height: '100%',
+ pointerEvents: isEditing ? 'all' : 'none',
opacity: isEditing ? 1 : 0,
- overflowY: "scroll"
- }}
- >
+ overflowY: 'scroll',
+ }}>
<div
style={{
- borderRadius: "50%",
+ borderRadius: '50%',
width: 25,
height: 25,
marginLeft: 10,
- position: "absolute",
+ position: 'absolute',
right: 41,
- top: 10
+ top: 10,
}}
- title={"Add Metadata Entry"}
- onClick={this.addMetadataEntry}
- >
+ title={'Add Metadata Entry'}
+ onClick={this.addMetadataEntry}>
<FontAwesomeIcon
style={{
- pointerEvents: "none",
+ pointerEvents: 'none',
marginLeft: 6.4,
- marginTop: 5.2
+ marginTop: 5.2,
}}
- icon={"plus"}
- size={"1x"}
+ icon={'plus'}
+ size={'1x'}
/>
</div>
- <p style={{ paddingLeft: 10, paddingTop: 8, paddingBottom: 7 }} >Add metadata to your import...</p>
- <hr style={{ margin: "6px 10px 12px 10px" }} />
- {entries.map(doc =>
+ <p style={{ paddingLeft: 10, paddingTop: 8, paddingBottom: 7 }}>Add metadata to your import...</p>
+ <hr style={{ margin: '6px 10px 12px 10px' }} />
+ {entries.map(doc => (
<ImportMetadataEntry
Document={doc}
key={doc[Id]}
remove={this.remove}
- ref={(el) => { if (el) this.entries.push(el); }}
+ ref={el => {
+ if (el) this.entries.push(el);
+ }}
next={this.addMetadataEntry}
/>
- )}
+ ))}
</div>
</div>
- }
+ )}
</Measure>
);
}
-
-} \ No newline at end of file
+}
diff --git a/src/client/util/Import & Export/ImageUtils.ts b/src/client/util/Import & Export/ImageUtils.ts
index 9bd92a316..55d37f544 100644
--- a/src/client/util/Import & Export/ImageUtils.ts
+++ b/src/client/util/Import & Export/ImageUtils.ts
@@ -1,12 +1,11 @@
-import { Doc } from "../../../fields/Doc";
-import { ImageField } from "../../../fields/URLField";
-import { Cast, StrCast, NumCast } from "../../../fields/Types";
-import { Networking } from "../../Network";
-import { Id } from "../../../fields/FieldSymbols";
-import { Utils } from "../../../Utils";
+import { Doc } from '../../../fields/Doc';
+import { ImageField } from '../../../fields/URLField';
+import { Cast, StrCast, NumCast } from '../../../fields/Types';
+import { Networking } from '../../Network';
+import { Id } from '../../../fields/FieldSymbols';
+import { Utils } from '../../../Utils';
export namespace ImageUtils {
-
export const ExtractExif = async (document: Doc): Promise<boolean> => {
const field = Cast(document.data, ImageField);
if (!field) {
@@ -17,23 +16,22 @@ export namespace ImageUtils {
contentSize,
nativeWidth,
nativeHeight,
- exifData: { error, data }
- } = await Networking.PostToServer("/inspectImage", { source });
+ exifData: { error, data },
+ } = await Networking.PostToServer('/inspectImage', { source });
document.exif = error || Doc.Get.FromJson({ data });
const proto = Doc.GetProto(document);
- nativeWidth && (document._height = NumCast(document._width) * nativeHeight / nativeWidth);
- proto["data-nativeWidth"] = nativeWidth;
- proto["data-nativeHeight"] = nativeHeight;
- proto["data-path"] = source;
- proto.contentSize = contentSize ? contentSize : undefined;
+ nativeWidth && (document._height = (NumCast(document._width) * nativeHeight) / nativeWidth);
+ proto['data_nativeWidth'] = nativeWidth;
+ proto['data_nativeHeight'] = nativeHeight;
+ proto['data-path'] = source;
+ proto.data_contentSize = contentSize ? contentSize : undefined;
return data !== undefined;
};
export const ExportHierarchyToFileSystem = async (collection: Doc): Promise<void> => {
- const a = document.createElement("a");
+ const a = document.createElement('a');
a.href = Utils.prepend(`/imageHierarchyExport/${collection[Id]}`);
a.download = `Dash Export [${StrCast(collection.title)}].zip`;
a.click();
};
-
-} \ No newline at end of file
+}
diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx
index 3cdf4dbd2..043f0f1f3 100644
--- a/src/client/util/InteractionUtils.tsx
+++ b/src/client/util/InteractionUtils.tsx
@@ -107,7 +107,8 @@ export namespace InteractionUtils {
pevents: string,
opacity: number,
nodefs: boolean,
- downHdlr?: (e: React.PointerEvent) => void
+ downHdlr?: (e: React.PointerEvent) => void,
+ mask?: boolean
) {
const pts = shape ? makePolygon(shape, points) : points;
@@ -133,6 +134,11 @@ export namespace InteractionUtils {
{/* setting the svg fill sets the arrowStart fill */}
{nodefs ? null : (
<defs>
+ {!mask ? null : (
+ <filter id={`mask${defGuid}`} x="-1" y="-1" width="500%" height="500%">
+ <feGaussianBlur result="blurOut" in="offOut" stdDeviation="5"></feGaussianBlur>
+ </filter>
+ )}
{arrowStart !== 'dot' && arrowEnd !== 'dot' ? null : (
<marker id={`dot${defGuid}`} orient="auto" markerUnits="userSpaceOnUse" refX={0} refY="0" overflow="visible">
<circle r={strokeWidth * arrowWidthFactor} fill="context-stroke" />
@@ -155,6 +161,7 @@ export namespace InteractionUtils {
<polygon
style={{ stroke: color }}
strokeLinejoin={lineJoin as any}
+ strokeDasharray={dashArray}
strokeWidth={(markerStrokeWidth * 2) / 3}
points={`0 ${-markerStrokeWidth * arrowWidthFactor}, ${markerStrokeWidth * arrowNotchFactor} 0, 0 ${markerStrokeWidth * arrowWidthFactor}, ${arrowLengthFactor * markerStrokeWidth} 0`}
/>
@@ -168,6 +175,7 @@ export namespace InteractionUtils {
style={{
// filter: drawHalo ? "url(#inkSelectionHalo)" : undefined,
fill: fill && fill !== 'transparent' ? fill : 'none',
+ filter: mask ? `url(#mask${defGuid})` : undefined,
opacity: 1.0,
// opacity: strokeWidth !== width ? 0.5 : undefined,
pointerEvents: pevents as any,
diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts
index f75ac24f5..f74409e42 100644
--- a/src/client/util/LinkFollower.ts
+++ b/src/client/util/LinkFollower.ts
@@ -1,19 +1,19 @@
-import { action, observable, observe } from 'mobx';
-import { computedFn } from 'mobx-utils';
-import { DirectLinksSym, Doc, DocListCast, DocListCastAsync, Field, Opt } from '../../fields/Doc';
-import { List } from '../../fields/List';
-import { ProxyField } from '../../fields/Proxy';
-import { BoolCast, Cast, StrCast } from '../../fields/Types';
-import { LightboxView } from '../views/LightboxView';
-import { DocumentViewSharedProps, ViewAdjustment } from '../views/nodes/DocumentView';
+import { action, observable, runInAction } from 'mobx';
+import { Doc, DocListCast, Field, FieldResult, Opt } from '../../fields/Doc';
+import { ScriptField } from '../../fields/ScriptField';
+import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../fields/Types';
+import { DocumentType } from '../documents/DocumentTypes';
+import { DocFocusOptions, OpenWhere } from '../views/nodes/DocumentView';
+import { PresBox } from '../views/nodes/trails';
import { DocumentManager } from './DocumentManager';
+import { LinkManager } from './LinkManager';
+import { ScriptingGlobals } from './ScriptingGlobals';
+import { SelectionManager } from './SelectionManager';
import { UndoManager } from './UndoManager';
-
-type CreateViewFunc = (doc: Doc, followLinkLocation: string, finished?: () => void) => void;
/*
* link doc:
- * - anchor1: doc
- * - anchor2: doc
+ * - link_anchor_1: doc
+ * - link_anchor_2: doc
*
* group doc:
* - type: string representing the group type/name/category
@@ -23,90 +23,114 @@ type CreateViewFunc = (doc: Doc, followLinkLocation: string, finished?: () => vo
* - user defined kvps
*/
export class LinkFollower {
+ @observable public static IsFollowing = false;
// follows a link - if the target is on screen, it highlights/pans to it.
// 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, docViewProps: DocumentViewSharedProps, altKey: boolean, zoom: boolean = false) => {
- const batch = UndoManager.StartBatch('follow link click');
- // open up target if it's not already in view ...
- const createViewFunc = (doc: Doc, followLoc: string, finished?: Opt<() => void>) => {
- const createTabForTarget = (didFocus: boolean) =>
- new Promise<ViewAdjustment>(res => {
- const where = LightboxView.LightboxDoc ? 'lightbox' : StrCast(sourceDoc.followLinkLocation, followLoc);
- docViewProps.addDocTab(doc, where);
- setTimeout(() => {
- const getFirstDocView = LightboxView.LightboxDoc ? DocumentManager.Instance.getLightboxDocumentView : DocumentManager.Instance.getFirstDocumentView;
- const targDocView = getFirstDocView(doc); // get first document view available within the lightbox if that's open, or anywhere otherwise.
- if (targDocView) {
- targDocView.props.focus(doc, {
- willZoom: BoolCast(sourceDoc.followLinkZoom, false),
- afterFocus: (didFocus: boolean) => {
- finished?.();
- res(ViewAdjustment.resetView);
- return new Promise<ViewAdjustment>(res2 => res2(ViewAdjustment.doNothing));
- },
- });
- } else {
- res(where !== 'inPlace' ? ViewAdjustment.resetView : ViewAdjustment.doNothing); // for 'inPlace' resetting the initial focus&zoom would negate the zoom into the target
- }
- });
- });
-
- if (!sourceDoc.followLinkZoom) {
- createTabForTarget(false);
- } else {
- // first focus & zoom onto this (the clicked document). Then execute the function to focus on the target
- docViewProps.focus(sourceDoc, { willZoom: BoolCast(sourceDoc.followLinkZoom, true), scale: 1, afterFocus: createTabForTarget });
- }
- };
- LinkFollower.traverseLink(linkDoc, sourceDoc, createViewFunc, BoolCast(sourceDoc.followLinkZoom, zoom), docViewProps.ContainingCollectionDoc, batch.end, altKey ? true : undefined);
+ public static FollowLink = (linkDoc: Opt<Doc>, sourceDoc: Doc, altKey: boolean) => {
+ 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(
+ linkDoc,
+ sourceDoc,
+ action(() => {
+ batch.end();
+ Doc.AddUnHighlightWatcher(action(() => (LinkFollower.IsFollowing = false)));
+ }),
+ altKey ? true : undefined
+ );
};
- public static traverseLink(link: Opt<Doc>, sourceDoc: Doc, createViewFunc: CreateViewFunc, zoom = false, currentContext?: Doc, finished?: () => void, traverseBacklink?: boolean) {
- const linkDocs = link ? [link] : DocListCast(sourceDoc.links);
- const firstDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor1 as Doc, sourceDoc) || Doc.AreProtosEqual((linkDoc.anchor1 as Doc).annotationOn as Doc, sourceDoc)); // link docs where 'doc' is anchor1
- const secondDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor2 as Doc, sourceDoc) || Doc.AreProtosEqual((linkDoc.anchor2 as Doc).annotationOn as Doc, sourceDoc)); // link docs where 'doc' is anchor2
- const fwdLinkWithoutTargetView = firstDocs.find(d => DocumentManager.Instance.getDocumentViews(d.anchor2 as Doc).length === 0);
- const backLinkWithoutTargetView = secondDocs.find(d => DocumentManager.Instance.getDocumentViews(d.anchor1 as Doc).length === 0);
- const linkWithoutTargetDoc = traverseBacklink === undefined ? fwdLinkWithoutTargetView || backLinkWithoutTargetView : traverseBacklink ? backLinkWithoutTargetView : fwdLinkWithoutTargetView;
- const linkDocList = linkWithoutTargetDoc ? [linkWithoutTargetDoc] : traverseBacklink === undefined ? firstDocs.concat(secondDocs) : traverseBacklink ? secondDocs : firstDocs;
- const followLinks = sourceDoc.isPushpin ? linkDocList : linkDocList.slice(0, 1);
+ 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 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?.();
followLinks.forEach(async linkDoc => {
- if (linkDoc) {
- const target = (
- sourceDoc === linkDoc.anchor1
- ? linkDoc.anchor2
- : sourceDoc === linkDoc.anchor2
- ? linkDoc.anchor1
- : Doc.AreProtosEqual(sourceDoc, linkDoc.anchor1 as Doc) || Doc.AreProtosEqual((linkDoc.anchor1 as Doc).annotationOn as Doc, sourceDoc)
- ? linkDoc.anchor2
- : linkDoc.anchor1
- ) as Doc;
- if (target) {
- if (target.TourMap) {
- const fieldKey = Doc.LayoutFieldKey(target);
- const tour = DocListCast(target[fieldKey]).reverse();
- LightboxView.SetLightboxDoc(currentContext, undefined, tour);
- setTimeout(LightboxView.Next);
- allFinished();
- } else {
- const containerAnnoDoc = Cast(target.annotationOn, Doc, null);
- const containerDoc = containerAnnoDoc || target;
- var containerDocContext = containerDoc?.context ? [Cast(containerDoc?.context, Doc, null)] : ([] as Doc[]);
- while (containerDocContext.length && !DocumentManager.Instance.getDocumentView(containerDocContext[0]) && containerDocContext[0].context) {
- containerDocContext = [Cast(containerDocContext[0].context, Doc, null), ...containerDocContext];
+ const target = (
+ sourceDoc === linkDoc.link_anchor_1
+ ? linkDoc.link_anchor_2
+ : sourceDoc === linkDoc.link_anchor_2
+ ? linkDoc.link_anchor_1
+ : Doc.AreProtosEqual(sourceDoc, linkDoc.link_anchor_1 as Doc) || Doc.AreProtosEqual((linkDoc.link_anchor_1 as Doc).annotationOn as Doc, sourceDoc)
+ ? linkDoc.link_anchor_2
+ : linkDoc.link_anchor_1
+ ) as Doc;
+ if (target) {
+ const doFollow = (canToggle?: boolean) => {
+ const toggleTarget = canToggle && BoolCast(sourceDoc.followLinkToggle);
+ const options: DocFocusOptions = {
+ playAudio: BoolCast(sourceDoc.followLinkAudio),
+ toggleTarget,
+ noSelect: true,
+ willPan: true,
+ willZoomCentered: BoolCast(sourceDoc.followLinkZoom, false),
+ zoomTime: NumCast(sourceDoc.followLinkTransitionTime, 500),
+ zoomScale: Cast(sourceDoc.followLinkZoomScale, 'number', null),
+ easeFunc: StrCast(sourceDoc.followLinkEase, 'ease') as any,
+ openLocation: StrCast(sourceDoc.followLinkLocation, OpenWhere.lightbox) as OpenWhere,
+ effect: sourceDoc,
+ zoomTextSelections: BoolCast(sourceDoc.followLinkZoomText),
+ };
+ if (target.type === DocumentType.PRES) {
+ const containerDocContext = DocumentManager.GetContextPath(sourceDoc, true); // gather all views that affect layout of sourceDoc so we can revert them after playing the rail
+ SelectionManager.DeselectAll();
+ if (!DocumentManager.Instance.AddViewRenderedCb(target, dv => containerDocContext.length && (dv.ComponentView as PresBox).PlayTrail(containerDocContext))) {
+ PresBox.OpenPresMinimized(target, [0, 0]);
}
- const targetContexts = LightboxView.LightboxDoc ? [containerAnnoDoc || containerDocContext[0]].filter(a => a) : containerDocContext;
- DocumentManager.Instance.jumpToDocument(target, zoom, (doc, finished) => createViewFunc(doc, StrCast(linkDoc.followLinkLocation, 'lightbox'), finished), targetContexts, linkDoc, undefined, sourceDoc, allFinished);
+ finished?.();
+ } else {
+ DocumentManager.Instance.showDocument(target, options, allFinished);
+ }
+ };
+ let movedTarget = false;
+ if (sourceDoc.followLinkLocation === OpenWhere.inParent) {
+ const sourceDocParent = DocCast(sourceDoc.embedContainer);
+ if (target.embedContainer instanceof Doc && target.embedContainer !== sourceDocParent) {
+ Doc.RemoveDocFromList(target.embedContainer, Doc.LayoutFieldKey(target.embedContainer), target);
+ movedTarget = true;
}
- } else {
- allFinished();
- }
+ if (!DocListCast(sourceDocParent[Doc.LayoutFieldKey(sourceDocParent)]).includes(target)) {
+ Doc.AddDocToList(sourceDocParent, Doc.LayoutFieldKey(sourceDocParent), target);
+ movedTarget = true;
+ }
+ target.embedContainer = 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];
+ movedTarget = true;
+ }
+ if (sourceDoc.followLinkYoffset !== undefined && moveTo[1] !== target.y) {
+ target.y = moveTo[1];
+ movedTarget = true;
+ }
+ if (movedTarget) setTimeout(doFollow);
+ else doFollow(true);
+ } else doFollow(true);
} else {
allFinished();
}
});
}
}
+
+ScriptingGlobals.add(function followLink(doc: Doc, altKey: boolean) {
+ SelectionManager.DeselectAll();
+ LinkFollower.FollowLink(undefined, doc, altKey);
+});
+
+export function FollowLinkScript() {
+ return ScriptField.MakeScript('followLink(this,altKey)', { altKey: 'boolean' });
+}
+export function IsFollowLinkScript(field: FieldResult<Field>) {
+ return ScriptCast(field)?.script.originalScript.includes('followLink(');
+}
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index 7a12a8580..ce422f849 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -1,13 +1,16 @@
import { action, observable, observe } 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 { List } from '../../fields/List';
import { ProxyField } from '../../fields/Proxy';
-import { Cast, StrCast } from '../../fields/Types';
+import { Cast, DocCast, PromiseValue, StrCast } from '../../fields/Types';
+import { DocServer } from '../DocServer';
+import { ScriptingGlobals } from './ScriptingGlobals';
/*
* link doc:
- * - anchor1: doc
- * - anchor2: doc
+ * - link_anchor_1: doc
+ * - link_anchor_2: doc
*
* group doc:
* - type: string representing the group type/name/category
@@ -19,85 +22,100 @@ import { Cast, StrCast } from '../../fields/Types';
export class LinkManager {
@observable static _instance: LinkManager;
@observable static userLinkDBs: Doc[] = [];
- public static currentLink: Opt<Doc>;
+ @observable public static currentLink: Opt<Doc>;
+ @observable public static currentLinkAnchor: Opt<Doc>;
public static get Instance() {
return LinkManager._instance;
}
- public static addLinkDB = (linkDb: any) => LinkManager.userLinkDBs.push(linkDb);
+
+ public static Links(doc: Doc | undefined) {
+ return doc ? LinkManager.Instance.getAllRelatedLinks(doc) : [];
+ }
+ public static addLinkDB = async (linkDb: any) => {
+ await Promise.all(
+ ((await DocListCastAsync(linkDb.data)) ?? []).map(link =>
+ // makes sure link anchors are loaded to avoid incremental updates to computedFns in LinkManager
+ [PromiseValue(link?.link_anchor_1), PromiseValue(link?.link_anchor_2)]
+ )
+ );
+ LinkManager.userLinkDBs.push(linkDb);
+ };
public static AutoKeywords = 'keywords:Usages';
- static links: Doc[] = [];
+ static _links: Doc[] = [];
constructor() {
LinkManager._instance = this;
- this.createLinkrelationshipLists();
+ this.createlink_relationshipLists();
LinkManager.userLinkDBs = [];
const addLinkToDoc = (link: Doc) => {
- const a1Prom = link?.anchor1;
- const a2Prom = link?.anchor2;
- Promise.all([a1Prom, a2Prom]).then(all => {
- const a1 = all[0];
- const a2 = all[1];
- const a1ProtoProm = (link?.anchor1 as Doc)?.proto;
- const a2ProtoProm = (link?.anchor2 as Doc)?.proto;
- Promise.all([a1ProtoProm, a2ProtoProm]).then(
- action(all => {
- if (a1 instanceof Doc && a2 instanceof Doc && ((a1.author !== undefined && a2.author !== undefined) || link.author === Doc.CurrentUserEmail)) {
- Doc.GetProto(a1)[DirectLinksSym].add(link);
- Doc.GetProto(a2)[DirectLinksSym].add(link);
- Doc.GetProto(link)[DirectLinksSym].add(link);
- }
- })
- );
+ 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)?.[DirectLinks].add(link);
+ (protos[1] as Doc)?.[DirectLinks].add(link);
+ })
+ );
+ });
+ });
});
};
const remLinkFromDoc = (link: Doc) => {
- const a1 = link?.anchor1;
- const a2 = link?.anchor2;
+ 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);
+ Doc.GetProto(a1)[DirectLinks].delete(link);
+ Doc.GetProto(a2)[DirectLinks].delete(link);
+ Doc.GetProto(link)[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
- observe(
- userLinkDBDoc.data as Doc,
- change => {
- // observe pushes/splices on a user link DB 'data' field (should only happen for local changes)
- switch (change.type as any) {
- case 'splice':
- (change as any).added.forEach((link: any) => addLinkToDoc(toRealField(link)));
- (change as any).removed.forEach((link: any) => remLinkFromDoc(toRealField(link)));
- break;
- case 'update': //let oldValue = change.oldValue;
- }
- },
- true
- );
- observe(
- userLinkDBDoc,
- 'data', // obsever when a new array of links is assigned as the link DB 'data' field (should happen whenever a remote user adds/removes a link)
- change => {
- switch (change.type as any) {
- case 'update':
- Promise.all([...((change.oldValue as any as Doc[]) || []), ...((change.newValue as any as Doc[]) || [])]).then(doclist => {
- const oldDocs = doclist.slice(0, ((change.oldValue as any as Doc[]) || []).length);
- const newDocs = doclist.slice(((change.oldValue as any as Doc[]) || []).length, doclist.length);
+ 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
+ if (userLinkDBDoc.data) {
+ observe(
+ userLinkDBDoc.data,
+ change => {
+ // observe pushes/splices on a user link DB 'data' field (should only happen for local changes)
+ switch (change.type as any) {
+ case 'splice':
+ (change as any).added.forEach((link: any) => addLinkToDoc(toRealField(link)));
+ (change as any).removed.forEach((link: any) => remLinkFromDoc(toRealField(link)));
+ break;
+ case 'update': //let oldValue = change.oldValue;
+ }
+ },
+ true
+ );
+ observe(
+ userLinkDBDoc,
+ 'data', // obsever when a new array of links is assigned as the link DB 'data' field (should happen whenever a remote user adds/removes a link)
+ change => {
+ switch (change.type as any) {
+ case 'update':
+ Promise.all([...((change.oldValue as any as Doc[]) || []), ...((change.newValue as any as Doc[]) || [])]).then(doclist => {
+ const oldDocs = doclist.slice(0, ((change.oldValue as any as Doc[]) || []).length);
+ const newDocs = doclist.slice(((change.oldValue as any as Doc[]) || []).length, doclist.length);
- const added = newDocs?.filter(link => !(oldDocs || []).includes(link));
- const removed = oldDocs?.filter(link => !(newDocs || []).includes(link));
- added?.forEach((link: any) => addLinkToDoc(toRealField(link)));
- removed?.forEach((link: any) => remLinkFromDoc(toRealField(link)));
- });
- }
- },
- true
- );
+ const added = newDocs?.filter(link => !(oldDocs || []).includes(link));
+ const removed = oldDocs?.filter(link => !(newDocs || []).includes(link));
+ added?.forEach((link: any) => addLinkToDoc(toRealField(link)));
+ removed?.forEach((link: any) => remLinkFromDoc(toRealField(link)));
+ });
+ }
+ },
+ true
+ );
+ }
};
observe(
LinkManager.userLinkDBs,
@@ -112,25 +130,19 @@ export class LinkManager {
true
);
LinkManager.addLinkDB(Doc.LinkDBDoc());
- DocListCastAsync(Doc.LinkDBDoc()?.data).then(dblist =>
- dblist?.forEach(async link => {
- // make sure anchors are loaded to avoid incremental updates to computedFn's in LinkManager
- const a1 = await Cast(link?.anchor1, Doc, null);
- const a2 = await Cast(link?.anchor2, Doc, null);
- })
- );
}
- public createLinkrelationshipLists = () => {
+ public createlink_relationshipLists = () => {
//create new lists for link relations and their associated colors if the lists don't already exist
- !Doc.UserDoc().linkRelationshipList && (Doc.UserDoc().linkRelationshipList = new List<string>());
- !Doc.UserDoc().linkColorList && (Doc.UserDoc().linkColorList = new List<string>());
- !Doc.UserDoc().linkRelationshipSizes && (Doc.UserDoc().linkRelationshipSizes = new List<number>());
+ !Doc.UserDoc().link_relationshipList && (Doc.UserDoc().link_relationshipList = 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) {
@@ -144,37 +156,27 @@ export class LinkManager {
return this.relatedLinker(anchor);
} // finds all links that contain the given anchor
public getAllDirectLinks(anchor: Doc): Doc[] {
- // FIXME:glr Why is Doc undefined?
- if (Doc.GetProto(anchor)[DirectLinksSym]) {
- return Array.from(Doc.GetProto(anchor)[DirectLinksSym]);
- } else {
- return [];
- }
+ return Array.from(Doc.GetProto(anchor)[DirectLinks] ?? []);
} // finds all links that contain the given anchor
relatedLinker = computedFn(function relatedLinker(this: any, anchor: Doc): Doc[] {
- const lfield = Doc.LayoutFieldKey(anchor);
if (!anchor || anchor instanceof Promise || Doc.GetProto(anchor) instanceof Promise) {
console.log('WAITING FOR DOC/PROTO IN LINKMANAGER');
return [];
}
- const dirLinks = Doc.GetProto(anchor)[DirectLinksSym];
- const annos = DocListCast(anchor[lfield + '-annotations']);
- const timelineAnnos = DocListCast(anchor[lfield + '-annotations-timeline']);
- if (!annos || !timelineAnnos) {
- debugger;
- }
- const related = [...annos, ...timelineAnnos].reduce((list, anno) => [...list, ...LinkManager.Instance.relatedLinker(anno)], Array.from(dirLinks).slice());
- return related;
+ 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());
}, true);
// returns map of group type to anchor's links in that group type
public getRelatedGroupedLinks(anchor: Doc): Map<string, Array<Doc>> {
const anchorGroups = new Map<string, Array<Doc>>();
this.relatedLinker(anchor).forEach(link => {
- if (link.linkRelationship && link.linkRelationship !== '-ungrouped-') {
- const relation = StrCast(link.linkRelationship);
- const anchorRelation = relation.indexOf(':') !== -1 ? relation.split(':')[Doc.AreProtosEqual(Cast(link.anchor1, Doc, null), anchor) ? 0 : 1] : relation;
+ if (link.link_relationship && link.link_relationship !== '-ungrouped-') {
+ const relation = StrCast(link.link_relationship);
+ const anchorRelation = relation.indexOf(':') !== -1 ? relation.split(':')[Doc.AreProtosEqual(Cast(link.link_anchor_1, Doc, null), anchor) ? 0 : 1] : relation;
const group = anchorGroups.get(anchorRelation);
anchorGroups.set(anchorRelation, group ? [...group, link] : [link]);
} else {
@@ -190,12 +192,18 @@ export class LinkManager {
//TODO This should probably return undefined if there isn't an opposite anchor
//TODO This should also await the return value of the anchor so we don't filter out promises
public static getOppositeAnchor(linkDoc: Doc, anchor: Doc): Doc | undefined {
- const a1 = Cast(linkDoc.anchor1, Doc, null);
- const a2 = Cast(linkDoc.anchor2, 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;
+ const a1 = Cast(linkDoc.link_anchor_1, Doc, null);
+ const a2 = Cast(linkDoc.link_anchor_2, Doc, null);
+ 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;
}
}
+
+ScriptingGlobals.add(
+ function links(doc: any) {
+ return new List(LinkManager.Links(doc));
+ },
+ 'returns all the links to the document or its annotations',
+ '(doc: any)'
+);
diff --git a/src/client/util/RTFMarkup.tsx b/src/client/util/RTFMarkup.tsx
new file mode 100644
index 000000000..247267710
--- /dev/null
+++ b/src/client/util/RTFMarkup.tsx
@@ -0,0 +1,137 @@
+import { action, computed, observable } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { MainViewModal } from '../views/MainViewModal';
+
+@observer
+export class RTFMarkup extends React.Component<{}> {
+ static Instance: RTFMarkup;
+ @observable private isOpen = false; // whether the SharingManager modal is open or not
+
+ // private get linkVisible() {
+ // return this.targetDoc ? this.targetDoc["acl-" + PublicKey] !== SharingPermissions.None : false;
+ // }
+
+ @action
+ public open = () => (this.isOpen = true);
+
+ @action
+ public close = () => (this.isOpen = false);
+
+ constructor(props: {}) {
+ super(props);
+ RTFMarkup.Instance = this;
+ }
+
+ @observable _stats: { [key: string]: any } | undefined;
+
+ /**
+ * @returns the main interface of the SharingManager.
+ */
+ @computed get cheatSheet() {
+ return (
+ <div style={{ textAlign: 'initial', height: '100%' }}>
+ <p>
+ <b style={{ fontSize: 'larger' }}>{`wiki:phrase`}</b>
+ {` display wikipedia page for entered text (terminate with carriage return)`}
+ </p>
+ <p>
+ <b style={{ fontSize: 'larger' }}>{`#tag `}</b>
+ {` add hashtag metadata to document. e.g, #idea`}
+ </p>
+ <p>
+ <b style={{ fontSize: 'larger' }}>{`#, ## ... ###### `}</b>
+ {` set heading style based on number of '#'s between 1 and 6`}
+ </p>
+ <p>
+ <b style={{ fontSize: 'larger' }}>{`#tag `}</b>
+ {` add hashtag metadata to document. e.g, #idea`}
+ </p>
+ <p>
+ <b style={{ fontSize: 'larger' }}>{`>> `}</b>
+ {` add a sidebar text document inline`}
+ </p>
+ <p>
+ <b style={{ fontSize: 'larger' }}>{`\`\` `}</b>
+ {` create a code snippet block`}
+ </p>
+ <p>
+ <b style={{ fontSize: 'larger' }}>{`cmd-f `}</b>
+ {` collapse to an inline footnote)`}
+ </p>
+ <p>
+ <b style={{ fontSize: 'larger' }}>{`cmd-e `}</b>
+ {` collapse to elided text`}
+ </p>
+ <p>
+ <b style={{ fontSize: 'larger' }}>{`cmd-[ `}</b>
+ {` left justify text`}
+ </p>
+ <p>
+ <b style={{ fontSize: 'larger' }}>{`cmd-\\ `}</b>
+ {` center text`}
+ </p>
+ <p>
+ <b style={{ fontSize: 'larger' }}>{`cmd-] `}</b>
+ {` right justify text`}
+ </p>
+ <p>
+ <b style={{ fontSize: 'larger' }}>{`%% `}</b>
+ {` restore default styling`}
+ </p>
+ <p>
+ <b style={{ fontSize: 'larger' }}>{`%color `}</b>
+ {` changes text color styling. e.g., %green.`}
+ </p>
+ <p>
+ <b style={{ fontSize: 'larger' }}>{`%num `}</b>
+ {` set font size. e.g., %10 for 10pt font`}
+ </p>
+ <p>
+ <b style={{ fontSize: 'larger' }}>{`%eq `}</b>
+ {` creates an equation block for typeset math`}
+ </p>
+ <p>
+ <b style={{ fontSize: 'larger' }}>{`%/ `}</b>
+ {` switch between primary and alternate text (see bottom right Button for hover options).`}
+ </p>
+ <p>
+ <b style={{ fontSize: 'larger' }}>{`%> `}</b>
+ {` create a bockquote section. Terminate with 2 carriage returns`}
+ </p>
+ <p>
+ <b style={{ fontSize: 'larger' }}>{`%q `}</b>
+ {` start a quoted block of text that’s indented on the left and right. Terminate with %q`}
+ </p>
+ <p>
+ <b style={{ fontSize: 'larger' }}>{`%d `}</b>
+ {` start a block text where the first line is indented`}
+ </p>
+ <p>
+ <b style={{ fontSize: 'larger' }}>{`%h `}</b>
+ {` start a block of text that begins with a hanging indent`}
+ </p>
+ <p>
+ <b style={{ fontSize: 'larger' }}>{`[:doctitle]] `}</b>
+ {` hyperlink to document specified by it’s title`}
+ </p>
+ <p>
+ <b style={{ fontSize: 'larger' }}>{`[[fieldname]] `}</b>
+ {` display value of fieldname`}
+ </p>
+ <p>
+ <b style={{ fontSize: 'larger' }}>{`[[fieldname=value]] `}</b>
+ {` assign value to fieldname of document and display it`}
+ </p>
+ <p>
+ <b style={{ fontSize: 'larger' }}>{`[[fieldname:doctitle]] `}</b>
+ {` show value of fieldname from doc specified by it’s title`}
+ </p>
+ </div>
+ );
+ }
+
+ render() {
+ return <MainViewModal contents={this.cheatSheet} isDisplayed={this.isOpen} interactive={true} closeOnExternalClick={this.close} />;
+ }
+}
diff --git a/src/client/util/ReplayMovements.ts b/src/client/util/ReplayMovements.ts
index 86bc4c5de..cbc465d6a 100644
--- a/src/client/util/ReplayMovements.ts
+++ b/src/client/util/ReplayMovements.ts
@@ -1,22 +1,23 @@
-import { CollectionFreeFormView } from "../views/collections/collectionFreeForm";
-import { IReactionDisposer, observable, observe, reaction } from "mobx";
-import { Doc } from "../../fields/Doc";
-import { VideoBox } from "../views/nodes/VideoBox";
-import { DocumentManager } from "./DocumentManager";
-import { CollectionDockingView } from "../views/collections/CollectionDockingView";
-import { DocServer } from "../DocServer";
-import { Movement, Presentation } from "./TrackMovements";
+import { IReactionDisposer, observable, reaction } from 'mobx';
+import { Doc, IdToDoc } from '../../fields/Doc';
+import { CollectionDockingView } from '../views/collections/CollectionDockingView';
+import { CollectionFreeFormView } from '../views/collections/collectionFreeForm';
+import { OpenWhereMod } from '../views/nodes/DocumentView';
+import { VideoBox } from '../views/nodes/VideoBox';
+import { DocumentManager } from './DocumentManager';
+import { Movement, Presentation } from './TrackMovements';
export class ReplayMovements {
- private timers: NodeJS.Timeout[] | null;
+ private timers: NodeJS.Timeout[] | null;
private videoBoxDisposeFunc: IReactionDisposer | null;
private videoBox: VideoBox | null;
private isPlaying: boolean;
-
// create static instance and getter for global use
@observable static _instance: ReplayMovements;
- static get Instance(): ReplayMovements { return ReplayMovements._instance }
+ static get Instance(): ReplayMovements {
+ return ReplayMovements._instance;
+ }
constructor() {
// init the global instance
ReplayMovements._instance = this;
@@ -37,127 +38,120 @@ export class ReplayMovements {
}
Doc.UserDoc().presentationMode = 'none';
- this.isPlaying = false
+ this.isPlaying = false;
// TODO: set userdoc presentMode to browsing
- this.timers?.map(timer => clearTimeout(timer))
- }
+ this.timers?.map(timer => clearTimeout(timer));
+ };
setVideoBox = async (videoBox: VideoBox) => {
// console.info('setVideoBox', videoBox);
- if (this.videoBox !== null) { console.warn('setVideoBox on already videoBox'); }
- if (this.videoBoxDisposeFunc !== null) { console.warn('setVideoBox on already videoBox dispose func'); this.videoBoxDisposeFunc(); }
-
+ if (this.videoBox !== null) {
+ console.warn('setVideoBox on already videoBox');
+ }
+ if (this.videoBoxDisposeFunc !== null) {
+ console.warn('setVideoBox on already videoBox dispose func');
+ this.videoBoxDisposeFunc();
+ }
const { presentation } = videoBox;
- if (presentation == null) { console.warn('setVideoBox on null videoBox presentation'); return; }
-
- let docIdtoDoc: Map<string, Doc> = new Map();
- try {
- docIdtoDoc = await this.loadPresentation(presentation);
- } catch {
- console.error('[recordingApi.ts] setVideoBox(): error loading presentation - no replay movements');
- throw 'error loading docs from server';
+ if (presentation == null) {
+ console.warn('setVideoBox on null videoBox presentation');
+ return;
}
-
- this.videoBoxDisposeFunc =
- reaction(() => ({ playing: videoBox._playing, timeViewed: videoBox.player?.currentTime || 0 }),
- ({ playing, timeViewed }) =>
- playing ? this.playMovements(presentation, docIdtoDoc, timeViewed) : this.pauseMovements()
- );
+ this.loadPresentation(presentation);
+
+ this.videoBoxDisposeFunc = reaction(
+ () => ({ playing: videoBox._playing, timeViewed: videoBox.player?.currentTime || 0 }),
+ ({ playing, timeViewed }) => (playing ? this.playMovements(presentation, timeViewed) : this.pauseMovements())
+ );
this.videoBox = videoBox;
- }
+ };
removeVideoBox = () => {
- if (this.videoBoxDisposeFunc == null) { console.warn('removeVideoBox on null videoBox'); return; }
+ if (this.videoBoxDisposeFunc == null) {
+ console.warn('removeVideoBox on null videoBox');
+ return;
+ }
this.videoBoxDisposeFunc();
this.videoBox = null;
this.videoBoxDisposeFunc = null;
- }
+ };
// should be called from interacting with the screen
pauseFromInteraction = () => {
this.videoBox?.Pause();
this.pauseMovements();
- }
+ };
- loadPresentation = async (presentation: Presentation) => {
+ loadPresentation = (presentation: Presentation) => {
const { movements } = presentation;
if (movements === null) {
throw '[recordingApi.ts] followMovements() failed: no presentation data';
}
- // generate a set of all unique docIds
- const docIds = new Set<string>();
- for (const {docId} of movements) {
- if (!docIds.has(docId)) docIds.add(docId);
- }
-
- const docIdtoDoc = new Map<string, Doc>();
-
- let refFields = await DocServer.GetRefFields([...docIds.keys()]);
- for (const docId in refFields) {
- if (!refFields[docId]) {
- throw `one field was undefined`;
+ movements.forEach((movement, i) => {
+ if (typeof movement.doc === 'string') {
+ movements[i].doc = IdToDoc(movement.doc);
+ if (!movements[i].doc) {
+ console.log('ERROR: tracked doc not found');
+ }
}
- docIdtoDoc.set(docId, refFields[docId] as Doc);
- }
- // console.info('loadPresentation refFields', refFields, docIdtoDoc);
-
- return docIdtoDoc;
- }
+ });
+ };
// returns undefined if the docView isn't open on the screen
- getCollectionFFView = (docId: string) => {
- const isInView = DocumentManager.Instance.getDocumentViewById(docId);
- if (isInView) { return isInView.ComponentView as CollectionFreeFormView; }
- }
+ getCollectionFFView = (doc: Doc) => {
+ const isInView = DocumentManager.Instance.getDocumentView(doc);
+ if (isInView) {
+ return isInView.ComponentView as CollectionFreeFormView;
+ }
+ };
// will open the doc in a tab then return the CollectionFFView that holds it
- openTab = (docId: string, docIdtoDoc: Map<string, Doc>) => {
- const doc = docIdtoDoc.get(docId);
- if (doc == undefined) {
- console.error(`docIdtoDoc did not contain docId ${docId}`)
+ openTab = (doc: Doc) => {
+ if (doc === undefined) {
+ console.error(`doc undefined`);
return undefined;
}
// console.log('openTab', docId, doc);
- CollectionDockingView.AddSplit(doc, 'right');
+ CollectionDockingView.AddSplit(doc, OpenWhereMod.right);
const docView = DocumentManager.Instance.getDocumentView(doc);
// BUG - this returns undefined if the doc is already open
return docView?.ComponentView as CollectionFreeFormView;
- }
+ };
// helper to replay a movement
zoomAndPan = (movement: Movement, document: CollectionFreeFormView) => {
const { panX, panY, scale } = movement;
scale !== 0 && document.zoomSmoothlyAboutPt([panX, panY], scale, 0);
- document.Document._panX = panX;
- document.Document._panY = panY;
- }
+ document.Document._freeform_panX = panX;
+ document.Document._freeform_panY = panY;
+ };
- getFirstMovements = (movements: Movement[]): Map<string, Movement> => {
+ getFirstMovements = (movements: Movement[]): Map<Doc, Movement> => {
if (movements === null) return new Map();
// generate a set of all unique docIds
- const docIdtoFirstMove = new Map();
+ const docIdtoFirstMove = new Map<Doc, Movement>();
for (const move of movements) {
- const { docId } = move;
- if (!docIdtoFirstMove.has(docId)) docIdtoFirstMove.set(docId, move);
+ if (!docIdtoFirstMove.has(move.doc)) docIdtoFirstMove.set(move.doc, move);
}
return docIdtoFirstMove;
- }
+ };
endPlayingPresentation = () => {
this.isPlaying = false;
Doc.UserDoc().presentationMode = 'none';
- }
+ };
- public playMovements = (presentation: Presentation, docIdtoDoc: Map<string, Doc>, timeViewed: number = 0) => {
+ public playMovements = (presentation: Presentation, timeViewed: number = 0) => {
// console.info('playMovements', presentation, timeViewed, docIdtoDoc);
- if (presentation.movements === null || presentation.movements.length === 0) { //|| this.playFFView === null) {
- return new Error('[recordingApi.ts] followMovements() failed: no presentation data')
+ if (presentation.movements === null || presentation.movements.length === 0) {
+ //|| this.playFFView === null) {
+ return new Error('[recordingApi.ts] followMovements() failed: no presentation data');
}
if (this.isPlaying) return;
@@ -165,35 +159,34 @@ export class ReplayMovements {
Doc.UserDoc().presentationMode = 'watching';
// only get the movements that are remaining in the video time left
- const filteredMovements = presentation.movements.filter(movement => movement.time > timeViewed * 1000)
+ const filteredMovements = presentation.movements.filter(movement => movement.time > timeViewed * 1000);
const handleFirstMovements = () => {
// if the first movement is a closed tab, open it
const firstMovement = filteredMovements[0];
- const isClosed = this.getCollectionFFView(firstMovement.docId) === undefined;
- if (isClosed) this.openTab(firstMovement.docId, docIdtoDoc);
+ const isClosed = this.getCollectionFFView(firstMovement.doc) === undefined;
+ if (isClosed) this.openTab(firstMovement.doc);
// for the open tabs, set it to the first move
const docIdtoFirstMove = this.getFirstMovements(filteredMovements);
- for (const [docId, firstMove] of docIdtoFirstMove) {
- const colFFView = this.getCollectionFFView(docId);
+ for (const [doc, firstMove] of docIdtoFirstMove) {
+ const colFFView = this.getCollectionFFView(doc);
if (colFFView) this.zoomAndPan(firstMove, colFFView);
}
- }
+ };
handleFirstMovements();
-
// make timers that will execute each movement at the correct replay time
this.timers = filteredMovements.map(movement => {
- const timeDiff = movement.time - timeViewed * 1000
+ const timeDiff = movement.time - timeViewed * 1000;
return setTimeout(() => {
- const collectionFFView = this.getCollectionFFView(movement.docId);
+ const collectionFFView = this.getCollectionFFView(movement.doc);
if (collectionFFView) {
this.zoomAndPan(movement, collectionFFView);
} else {
// tab wasn't open - open it and play the movement
- const openedColFFView = this.openTab(movement.docId, docIdtoDoc);
+ const openedColFFView = this.openTab(movement.doc);
console.log('openedColFFView', openedColFFView);
openedColFFView && this.zoomAndPan(movement, openedColFFView);
}
@@ -204,5 +197,5 @@ export class ReplayMovements {
}
}, timeDiff);
});
- }
+ };
}
diff --git a/src/client/util/ReportManager.tsx b/src/client/util/ReportManager.tsx
index 51742d455..4c1020455 100644
--- a/src/client/util/ReportManager.tsx
+++ b/src/client/util/ReportManager.tsx
@@ -173,7 +173,7 @@ export class ReportManager extends React.Component<{}> {
// 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 => {
+ (Networking.UploadFilesToServer(fileArray.map(file =>({file})))).then(links => {
console.log('finshed uploading', links.map(this.getServerPath));
this.setFileLinks((links ?? []).map(this.getServerPath));
})
diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts
index 6dcdcb71b..70c2e3842 100644
--- a/src/client/util/Scripting.ts
+++ b/src/client/util/Scripting.ts
@@ -7,7 +7,7 @@
import * as typescriptlib from '!!raw-loader!./type_decls.d';
import * as ts from 'typescript';
import { Doc, Field } from '../../fields/Doc';
-import { ObjectField } from '../../fields/ObjectField';
+import { RefField } from '../../fields/RefField';
import { ScriptField } from '../../fields/ScriptField';
import { scriptingGlobals, ScriptingGlobals } from './ScriptingGlobals';
export { ts };
@@ -58,7 +58,14 @@ function Run(script: string | undefined, customParams: string[], diagnostics: an
// let fieldTypes = [Doc, ImageField, PdfField, VideoField, AudioField, List, RichTextField, ScriptField, ComputedField, CompileScript];
// let paramNames = ["Docs", ...fieldTypes.map(fn => fn.name)];
// let params: any[] = [Docs, ...fieldTypes];
- const compiledFunction = new Function(...paramNames, `return ${script}`);
+ const compiledFunction = (() => {
+ try {
+ return new Function(...paramNames, `return ${script}`);
+ } catch {
+ return undefined;
+ }
+ })();
+ if (!compiledFunction) return { compiled: false, errors };
const { capturedVariables = {} } = options;
const run = (args: { [name: string]: any } = {}, onError?: (e: any) => void, errorVal?: any): ScriptResult => {
const argsArray: any[] = [];
@@ -153,7 +160,7 @@ export type Traverser = (node: ts.Node, indentation: string) => boolean | void;
export type TraverserParam = Traverser | { onEnter: Traverser; onLeave: Traverser };
export type Transformer = {
transformer: ts.TransformerFactory<ts.SourceFile>;
- getVars?: () => { capturedVariables: { [name: string]: Field } };
+ getVars?: () => { [name: string]: Field };
};
export interface ScriptOptions {
requiredType?: string; // does function required a typed return value
@@ -180,7 +187,11 @@ function forEachNode(node: ts.Node, onEnter: Traverser, onExit?: Traverser, inde
export function CompileScript(script: string, options: ScriptOptions = {}): CompileResult {
const captured = options.capturedVariables ?? {};
- const signature = Object.keys(captured).reduce((p, v) => p + `${v}=${captured[v] instanceof ObjectField ? 'XXX' : captured[v].toString()}`, '');
+ const signature = Object.keys(captured).reduce((p, v) => {
+ const formatCapture = (obj: any) => `${v}=${obj instanceof RefField ? 'XXX' : obj.toString()}`;
+ if (captured[v] instanceof Array) return p + (captured[v] as any).map(formatCapture);
+ return p + formatCapture(captured[v]);
+ }, '');
const found = ScriptField.GetScriptFieldCache(script + ':' + signature);
if (found) return found as CompiledScript;
const { requiredType = '', addReturn = false, params = {}, capturedVariables = {}, typecheck = true } = options;
@@ -199,16 +210,17 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp
if (options.transformer) {
const sourceFile = ts.createSourceFile('script.ts', script, ts.ScriptTarget.ES2015, true);
const result = ts.transform(sourceFile, [options.transformer.transformer]);
- if (options.transformer.getVars) {
- const newCaptures = options.transformer.getVars();
+ const newCaptures = options.transformer.getVars?.();
+ if (Object.keys(newCaptures ?? {}).length) {
// tslint:disable-next-line: prefer-object-spread
- options.capturedVariables = Object.assign(capturedVariables, newCaptures.capturedVariables) as any;
+ //options.capturedVariables = Object.assign(capturedVariables, newCaptures!) as any;
+
+ const transformed = result.transformed;
+ const printer = ts.createPrinter({
+ newLine: ts.NewLineKind.LineFeed,
+ });
+ script = printer.printFile(transformed[0]);
}
- const transformed = result.transformed;
- const printer = ts.createPrinter({
- newLine: ts.NewLineKind.LineFeed,
- });
- script = printer.printFile(transformed[0]);
result.dispose();
}
const paramNames: string[] = [];
@@ -248,8 +260,10 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp
ScriptingGlobals.resetScriptingGlobals();
}
!signature.includes('XXX') && ScriptField._scriptFieldCache.set(script + ':' + signature, result as CompiledScript);
- //console.log('COMPILED: ' + script + ':' + signature);
return result;
}
ScriptingGlobals.add(CompileScript);
+ScriptingGlobals.add(function runScript(self: Doc, script: ScriptField) {
+ return script?.script.run({ this: self, self: self }).result;
+});
diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts
index 79759a71d..d154c48a4 100644
--- a/src/client/util/SearchUtil.ts
+++ b/src/client/util/SearchUtil.ts
@@ -24,51 +24,57 @@ export namespace SearchUtil {
export interface SearchParams {
hl?: string;
- "hl.fl"?: string;
+ 'hl.fl'?: string;
start?: number;
rows?: number;
fq?: string;
sort?: string;
- allowAliases?: boolean;
- onlyAliases?: boolean;
- "facet"?: string;
- "facet.field"?: string;
+ allowEmbeddings?: boolean;
+ onlyEmbeddings?: boolean;
+ facet?: string;
+ 'facet.field'?: string;
}
export function Search(query: string, returnDocs: true, options?: SearchParams): Promise<DocSearchResult>;
export function Search(query: string, returnDocs: false, options?: SearchParams): Promise<IdSearchResult>;
export async function Search(query: string, returnDocs: boolean, options: SearchParams = {}) {
- query = query || "*"; //If we just have a filter query, search for * as the query
- const rpquery = Utils.prepend("/dashsearch");
+ query = query || '*'; //If we just have a filter query, search for * as the query
+ const rpquery = Utils.prepend('/dashsearch');
let replacedQuery = query.replace(/type_t:([^ )])/g, (substring, arg) => `{!join from=id to=proto_i}*:* AND ${arg}`);
- if (options.onlyAliases) {
- const header = query.match(/_[atnb]?:/) ? replacedQuery : "DEFAULT:" + replacedQuery;
+ if (options.onlyEmbeddings) {
+ const header = query.match(/_[atnb]?:/) ? replacedQuery : 'DEFAULT:' + replacedQuery;
replacedQuery = `{!join from=id to=proto_i}* AND ${header}`;
}
//console.log("Q: " + replacedQuery + " fq: " + options.fq);
const gotten = await rp.get(rpquery, { qs: { ...options, q: replacedQuery } });
- const result: IdSearchResult = gotten.startsWith("<") ? { ids: [], docs: [], numFound: 0, lines: [] } : JSON.parse(gotten);
+ const result: IdSearchResult = gotten.startsWith('<') ? { ids: [], docs: [], numFound: 0, lines: [] } : JSON.parse(gotten);
if (!returnDocs) {
return result;
}
const { ids, highlighting } = result;
- const txtresult = query !== "*" && JSON.parse(await rp.get(Utils.prepend("/textsearch"), {
- qs: { ...options, q: query.replace(/^[ \+\?\*\|]*/, "") }, // a leading '+' leads to a server crash since findInFiles doesn't handle regex failures
- }));
+ const txtresult =
+ query !== '*' &&
+ JSON.parse(
+ await rp.get(Utils.prepend('/textsearch'), {
+ qs: { ...options, q: query.replace(/^[ \+\?\*\|]*/, '') }, // a leading '+' leads to a server crash since findInFiles doesn't handle regex failures
+ })
+ );
const fileids = txtresult ? txtresult.ids : [];
const newIds: string[] = [];
const newLines: string[][] = [];
- if (fileids) {
- await Promise.all(fileids.map(async (tr: string, i: number) => {
- const docQuery = "fileUpload_t:" + tr.substr(0, 7); //If we just have a filter query, search for * as the query
- const docResult = JSON.parse(await rp.get(Utils.prepend("/dashsearch"), { qs: { ...options, q: docQuery } }));
- newIds.push(...docResult.ids);
- newLines.push(...docResult.ids.map((dr: any) => txtresult.lines[i]));
- }));
- }
-
+ // bcz: we stopped storing fileUpload id's, so this won't find anything
+ // if (fileids) {
+ // await Promise.all(
+ // fileids.map(async (tr: string, i: number) => {
+ // const docQuery = 'fileUpload_t:' + tr.substr(0, 7); //If we just have a filter query, search for * as the query
+ // const docResult = JSON.parse(await rp.get(Utils.prepend('/dashsearch'), { qs: { ...options, q: docQuery } }));
+ // newIds.push(...docResult.ids);
+ // newLines.push(...docResult.ids.map((dr: any) => txtresult.lines[i]));
+ // })
+ // );
+ // }
const theDocs: Doc[] = [];
const theLines: string[][] = [];
@@ -86,7 +92,7 @@ export namespace SearchUtil {
const docs = ids.map((id: string) => docMap[id]).map(doc => doc as Doc);
for (let i = 0; i < ids.length; i++) {
const testDoc = docs[i];
- if (testDoc instanceof Doc && testDoc.type !== DocumentType.KVP && (options.allowAliases || testDoc.proto === undefined || theDocs.findIndex(d => Doc.AreProtosEqual(d, testDoc)) === -1)) {
+ if (testDoc instanceof Doc && testDoc.type !== DocumentType.KVP && (options.allowEmbeddings || testDoc.proto === undefined || theDocs.findIndex(d => Doc.AreProtosEqual(d, testDoc)) === -1)) {
theDocs.push(testDoc);
theLines.push([]);
} else {
@@ -97,48 +103,46 @@ export namespace SearchUtil {
return { docs: theDocs, numFound: Math.max(0, result.numFound), highlighting, lines: theLines };
}
- export async function GetAliasesOfDocument(doc: Doc): Promise<Doc[]>;
- export async function GetAliasesOfDocument(doc: Doc, returnDocs: false): Promise<string[]>;
- export async function GetAliasesOfDocument(doc: Doc, returnDocs = true): Promise<Doc[] | string[]> {
+ export async function GetEmbeddingsOfDocument(doc: Doc): Promise<Doc[]>;
+ export async function GetEmbeddingsOfDocument(doc: Doc, returnDocs: false): Promise<string[]>;
+ export async function GetEmbeddingsOfDocument(doc: Doc, returnDocs = true): Promise<Doc[] | string[]> {
const proto = Doc.GetProto(doc);
const protoId = proto[Id];
if (returnDocs) {
- return (await Search("", returnDocs, { fq: `proto_i:"${protoId}"`, allowAliases: true })).docs;
+ return (await Search('', returnDocs, { fq: `proto_i:"${protoId}"`, allowEmbeddings: true })).docs;
} else {
- return (await Search("", returnDocs, { fq: `proto_i:"${protoId}"`, allowAliases: true })).ids;
+ return (await Search('', returnDocs, { fq: `proto_i:"${protoId}"`, allowEmbeddings: true })).ids;
}
// return Search(`{!join from=id to=proto_i}id:${protoId}`, true);
}
export async function GetViewsOfDocument(doc: Doc): Promise<Doc[]> {
- const results = await Search("", true, { fq: `proto_i:"${doc[Id]}"` });
+ const results = await Search('', true, { fq: `proto_i:"${doc[Id]}"` });
return results.docs;
}
- export async function GetContextsOfDocument(doc: Doc): Promise<{ contexts: Doc[], aliasContexts: Doc[] }> {
- const docContexts = (await Search("", true, { fq: `data_l:"${doc[Id]}"` })).docs;
- const aliases = await GetAliasesOfDocument(doc, false);
- const aliasContexts = (await Promise.all(aliases.map(doc => Search("", true, { fq: `data_l:"${doc}"` }))));
- const contexts = { contexts: docContexts, aliasContexts: [] as Doc[] };
- aliasContexts.forEach(result => contexts.aliasContexts.push(...result.docs));
+ export async function GetContextsOfDocument(doc: Doc): Promise<{ contexts: Doc[]; embeddingContexts: Doc[] }> {
+ const docContexts = (await Search('', true, { fq: `data_l:"${doc[Id]}"` })).docs;
+ const embeddings = await GetEmbeddingsOfDocument(doc, false);
+ const embeddingContexts = await Promise.all(embeddings.map(doc => Search('', true, { fq: `data_l:"${doc}"` })));
+ const contexts = { contexts: docContexts, embeddingContexts: [] as Doc[] };
+ embeddingContexts.forEach(result => contexts.embeddingContexts.push(...result.docs));
return contexts;
}
- export async function GetContextIdsOfDocument(doc: Doc): Promise<{ contexts: string[], aliasContexts: string[] }> {
- const docContexts = (await Search("", false, { fq: `data_l:"${doc[Id]}"` })).ids;
- const aliases = await GetAliasesOfDocument(doc, false);
- const aliasContexts = (await Promise.all(aliases.map(doc => Search("", false, { fq: `data_l:"${doc}"` }))));
- const contexts = { contexts: docContexts, aliasContexts: [] as string[] };
- aliasContexts.forEach(result => contexts.aliasContexts.push(...result.ids));
+ export async function GetContextIdsOfDocument(doc: Doc): Promise<{ contexts: string[]; embeddingContexts: string[] }> {
+ const docContexts = (await Search('', false, { fq: `data_l:"${doc[Id]}"` })).ids;
+ const embeddings = await GetEmbeddingsOfDocument(doc, false);
+ const embeddingContexts = await Promise.all(embeddings.map(doc => Search('', false, { fq: `data_l:"${doc}"` })));
+ const contexts = { contexts: docContexts, embeddingContexts: [] as string[] };
+ embeddingContexts.forEach(result => contexts.embeddingContexts.push(...result.ids));
return contexts;
}
export async function GetAllDocs() {
- const query = "*";
+ const query = '*';
const response = await rp.get(Utils.prepend('/dashsearch'), {
- qs:
- { start: 0, rows: 10000, q: query },
-
+ qs: { start: 0, rows: 10000, q: query },
});
const result: IdSearchResult = JSON.parse(response);
const { ids, numFound, highlighting } = result;
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index a3d6f5227..0125331ec 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -2,14 +2,16 @@ 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';
export namespace SelectionManager {
class Manager {
@observable IsDragging: boolean = false;
- SelectedViews: ObservableMap<DocumentView, Doc> = new ObservableMap();
+ SelectedViewsMap: ObservableMap<DocumentView, Doc> = new ObservableMap();
+ @observable SelectedViews: DocumentView[] = [];
@observable SelectedSchemaDocument: Doc | undefined;
@action
@@ -19,42 +21,51 @@ export namespace SelectionManager {
@action
SelectView(docView: DocumentView, ctrlPressed: boolean): void {
// if doc is not in SelectedDocuments, add it
- if (!manager.SelectedViews.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;
+ }
this.DeselectAll();
}
- manager.SelectedViews.set(docView, docView.rootDoc);
+ manager.SelectedViews.push(docView);
+ manager.SelectedViewsMap.set(docView, docView.rootDoc);
docView.props.whenChildContentsActiveChanged(true);
- } else if (!ctrlPressed && (Array.from(manager.SelectedViews.entries()).length > 1 || manager.SelectedSchemaDocument)) {
- Array.from(manager.SelectedViews.keys()).map(dv => dv !== docView && dv.props.whenChildContentsActiveChanged(false));
+ } else if (!ctrlPressed && (Array.from(manager.SelectedViewsMap.entries()).length > 1 || manager.SelectedSchemaDocument)) {
+ Array.from(manager.SelectedViewsMap.keys()).map(dv => dv !== docView && dv.props.whenChildContentsActiveChanged(false));
manager.SelectedSchemaDocument = undefined;
- manager.SelectedViews.clear();
- manager.SelectedViews.set(docView, docView.rootDoc);
+ manager.SelectedViews.length = 0;
+ manager.SelectedViewsMap.clear();
+ manager.SelectedViews.push(docView);
+ manager.SelectedViewsMap.set(docView, docView.rootDoc);
}
}
@action
- DeselectView(docView: DocumentView): void {
- if (manager.SelectedViews.get(docView)) {
- manager.SelectedViews.delete(docView);
+ DeselectView(docView?: DocumentView): void {
+ if (docView && manager.SelectedViewsMap.get(docView)) {
+ manager.SelectedViewsMap.delete(docView);
+ manager.SelectedViews.splice(manager.SelectedViews.indexOf(docView), 1);
docView.props.whenChildContentsActiveChanged(false);
}
}
@action
DeselectAll(): void {
manager.SelectedSchemaDocument = undefined;
- Array.from(manager.SelectedViews.keys()).forEach(dv => dv.props.whenChildContentsActiveChanged(false));
- manager.SelectedViews.clear();
+ Array.from(manager.SelectedViewsMap.keys()).forEach(dv => dv.props.whenChildContentsActiveChanged(false));
+ manager.SelectedViewsMap.clear();
+ manager.SelectedViews.length = 0;
}
}
const manager = new Manager();
- export function DeselectView(docView: DocumentView): void {
+ export function DeselectView(docView?: DocumentView): void {
manager.DeselectView(docView);
}
- export function SelectView(docView: DocumentView, ctrlPressed: boolean): void {
- manager.SelectView(docView, ctrlPressed);
+ export function SelectView(docView: DocumentView | undefined, ctrlPressed: boolean): void {
+ if (!docView) DeselectAll();
+ else manager.SelectView(docView, ctrlPressed);
}
export function SelectSchemaViewDoc(document: Opt<Doc>, deselectAllFirst?: boolean): void {
if (deselectAllFirst) manager.DeselectAll();
@@ -63,7 +74,7 @@ export namespace SelectionManager {
const IsSelectedCache = computedFn(function isSelected(doc: DocumentView) {
// wrapping get() in a computedFn only generates mobx() invalidations when the return value of the function for the specific get parameters has changed
- return manager.SelectedViews.get(doc) ? true : false;
+ return manager.SelectedViewsMap.get(doc) ? true : false;
});
// computed functions, such as used in IsSelected generate errors if they're called outside of a
// reaction context. Specifying the context with 'outsideReaction' allows an efficiency feature
@@ -72,7 +83,7 @@ export namespace SelectionManager {
return !doc
? false
: outsideReaction
- ? manager.SelectedViews.get(doc)
+ ? manager.SelectedViewsMap.get(doc)
? true
: false // get() accesses a hashtable -- setting anything in the hashtable generates a mobx invalidation for every get()
: IsSelectedCache(doc);
@@ -81,7 +92,7 @@ export namespace SelectionManager {
export function DeselectAll(except?: Doc): void {
let found: DocumentView | undefined = undefined;
if (except) {
- for (const view of Array.from(manager.SelectedViews.keys())) {
+ for (const view of Array.from(manager.SelectedViewsMap.keys())) {
if (view.props.Document === except) found = view;
}
}
@@ -91,19 +102,22 @@ export namespace SelectionManager {
}
export function Views(): Array<DocumentView> {
- return Array.from(manager.SelectedViews.keys()); //.filter(dv => manager.SelectedViews.get(dv)?._viewType !== CollectionViewType.Docking);
+ return manager.SelectedViews;
+ // 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 Array.from(manager.SelectedViews.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(docType?: DocumentType, colType?: CollectionViewType, checkContext?: boolean) {
- if (colType === ('tab' as any)) {
+ScriptingGlobals.add(function SelectionManager_selectedDocType(type: string, expertMode: boolean, checkContext?: boolean) {
+ if (Doc.noviceMode && expertMode) return false;
+ if (type === 'tab') {
return SelectionManager.Views().lastElement()?.props.renderDepth === 0;
}
- let selected = (sel => (checkContext ? DocCast(sel?.context) : sel))(SelectionManager.SelectedSchemaDoc() ?? SelectionManager.Docs().lastElement());
- return docType ? selected?.type === docType : colType ? selected?.viewType === colType : true;
+ let selected = (sel => (checkContext ? DocCast(sel?.embedContainer) : sel))(SelectionManager.SelectedSchemaDoc() ?? SelectionManager.Docs().lastElement());
+ return selected?.type === type || selected?.type_collection === type || !type;
});
diff --git a/src/client/util/SerializationHelper.ts b/src/client/util/SerializationHelper.ts
index 2d598c1ac..76037a7e9 100644
--- a/src/client/util/SerializationHelper.ts
+++ b/src/client/util/SerializationHelper.ts
@@ -1,6 +1,6 @@
-import { PropSchema, serialize, deserialize, custom, setDefaultModelSchema, getDefaultModelSchema } from "serializr";
-import { Field } from "../../fields/Doc";
-import { ClientUtils } from "./ClientUtils";
+import { PropSchema, serialize, deserialize, custom, setDefaultModelSchema, getDefaultModelSchema } from 'serializr';
+import { Field } from '../../fields/Doc';
+import { ClientUtils } from './ClientUtils';
let serializing = 0;
export function afterDocDeserialize(cb: (err: any, val: any) => void, err: any, newValue: any) {
@@ -25,7 +25,7 @@ export namespace SerializationHelper {
serializing++;
if (!(obj.constructor.name in reverseMap)) {
serializing--;
- throw Error("Error: " + `type '${obj.constructor.name}' not registered. Make sure you register it using a @Deserializable decorator`);
+ throw Error('Error: ' + `type '${obj.constructor.name}' not registered. Make sure you register it using a @Deserializable decorator`);
}
const json = serialize(obj);
@@ -44,12 +44,8 @@ export namespace SerializationHelper {
}
if (!obj.__type) {
- if (true || ClientUtils.RELEASE) {
- console.warn("No property 'type' found in JSON.");
- return undefined;
- } else {
- throw Error("No property 'type' found in JSON.");
- }
+ console.warn("No property 'type' found in JSON.");
+ return undefined;
}
if (!(obj.__type in serializationTypes)) {
@@ -58,87 +54,34 @@ export namespace SerializationHelper {
const type = serializationTypes[obj.__type];
const value = await new Promise(res => deserialize(type.ctor, obj, (err, result) => res(result)));
- if (type.afterDeserialize) {
- await type.afterDeserialize(value);
- }
+ type.afterDeserialize?.(value);
+
return value;
}
}
-const serializationTypes: { [name: string]: { ctor: { new(): any }, afterDeserialize?: (obj: any) => void | Promise<any> } } = {};
+const serializationTypes: { [name: string]: { ctor: { new (): any }; afterDeserialize?: (obj: any) => void | Promise<any> } } = {};
const reverseMap: { [ctor: string]: string } = {};
-export interface DeserializableOpts {
- (constructor: { new(...args: any[]): any }): void;
- withFields(fields: string[]): Function;
-}
-
-export function Deserializable(name: string, afterDeserialize?: (obj: any) => void | Promise<any>): DeserializableOpts;
-export function Deserializable(constructor: { new(...args: any[]): any }): void;
-export function Deserializable(constructor: { new(...args: any[]): any } | string, afterDeserialize?: (obj: any) => void): DeserializableOpts | void {
- function addToMap(name: string, ctor: { new(...args: any[]): any }) {
+export function Deserializable(className: string, afterDeserialize?: (obj: any) => void | Promise<any>, constructorArgs?: [string]): (constructor: { new (...args: any[]): any }) => void {
+ function addToMap(className: string, ctor: { new (...args: any[]): any }) {
const schema = getDefaultModelSchema(ctor) as any;
- if (schema.targetClass !== ctor) {
- const newSchema = { ...schema, factory: () => new ctor() };
- setDefaultModelSchema(ctor, newSchema);
+ if (schema.targetClass !== ctor || constructorArgs) {
+ setDefaultModelSchema(ctor, { ...schema, factory: (context: any) => new ctor(...(constructorArgs ?? [])?.map(arg => context.json[arg])) });
}
- if (!(name in serializationTypes)) {
- serializationTypes[name] = { ctor, afterDeserialize };
- reverseMap[ctor.name] = name;
+ if (!(className in serializationTypes)) {
+ serializationTypes[className] = { ctor, afterDeserialize };
+ reverseMap[ctor.name] = className;
} else {
- throw new Error(`Name ${name} has already been registered as deserializable`);
+ throw new Error(`Name ${className} has already been registered as deserializable`);
}
}
- if (typeof constructor === "string") {
- return Object.assign((ctor: { new(...args: any[]): any }) => {
- addToMap(constructor, ctor);
- }, { withFields: (fields: string[]) => Deserializable.withFields(fields, constructor, afterDeserialize) });
- }
- addToMap(constructor.name, constructor);
-}
-
-export namespace Deserializable {
- export function withFields(fields: string[], name?: string, afterDeserialize?: (obj: any) => void | Promise<any>) {
- return function (constructor: { new(...fields: any[]): any }) {
- Deserializable(name || constructor.name, afterDeserialize)(constructor);
- let schema = getDefaultModelSchema(constructor);
- if (schema) {
- schema.factory = context => {
- const args = fields.map(key => context.json[key]);
- return new constructor(...args);
- };
- // TODO A modified version of this would let us not reassign fields that we're passing into the constructor later on in deserializing
- // fields.forEach(field => {
- // if (field in schema.props) {
- // let propSchema = schema.props[field];
- // if (propSchema === false) {
- // return;
- // } else if (propSchema === true) {
- // propSchema = primitive();
- // }
- // schema.props[field] = custom(propSchema.serializer,
- // () => {
- // return SKIP;
- // });
- // }
- // });
- } else {
- schema = {
- props: {},
- factory: context => {
- const args = fields.map(key => context.json[key]);
- return new constructor(...args);
- }
- };
- setDefaultModelSchema(constructor, schema);
- }
- };
- }
+ return (ctor: { new (...args: any[]): any }) => addToMap(className, ctor);
}
export function autoObject(): PropSchema {
return custom(
- (s) => SerializationHelper.Serialize(s),
+ s => SerializationHelper.Serialize(s),
(json: any, context: any, oldValue: any, cb: (err: any, result: any) => void) => SerializationHelper.Deserialize(json).then(res => cb(null, res))
);
-} \ No newline at end of file
+}
diff --git a/src/client/util/ServerStats.tsx b/src/client/util/ServerStats.tsx
new file mode 100644
index 000000000..f84ad8598
--- /dev/null
+++ b/src/client/util/ServerStats.tsx
@@ -0,0 +1,54 @@
+import { action, computed, observable } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { MainViewModal } from '../views/MainViewModal';
+import './SharingManager.scss';
+
+@observer
+export class ServerStats extends React.Component<{}> {
+ public static Instance: ServerStats;
+ @observable private isOpen = false; // whether the SharingManager modal is open or not
+
+ // private get linkVisible() {
+ // return this.targetDoc ? this.targetDoc["acl-" + PublicKey] !== SharingPermissions.None : false;
+ // }
+
+ @action
+ public open = async () => {
+ /**
+ * Populates the list of users.
+ */
+ fetch('/stats').then((res: Response) => res.text().then(action(stats => (this._stats = JSON.parse(stats)))));
+
+ this.isOpen = true;
+ };
+
+ public close = action(() => {
+ this.isOpen = false;
+ });
+
+ constructor(props: {}) {
+ super(props);
+ ServerStats.Instance = this;
+ }
+
+ @observable _stats: { [key: string]: any } | undefined;
+
+ /**
+ * @returns the main interface of the SharingManager.
+ */
+ @computed get sharingInterface() {
+ return (
+ <div>
+ <span>Active users:{this._stats?.socketMap.length}</span>
+ {this._stats?.socketMap.map((user: any) => (
+ <p>{user.username}</p>
+ ))}
+ </div>
+ );
+ }
+
+ render() {
+ return <MainViewModal contents={this.sharingInterface} isDisplayed={this.isOpen} interactive={true} closeOnExternalClick={this.close} />;
+ }
+}
diff --git a/src/client/util/SettingsManager.scss b/src/client/util/SettingsManager.scss
index b7199f433..1289ca2b4 100644
--- a/src/client/util/SettingsManager.scss
+++ b/src/client/util/SettingsManager.scss
@@ -1,4 +1,4 @@
-@import "../views/global/globalCssVariables";
+@import '../views/global/globalCssVariables';
.settings-interface {
//background-color: whitesmoke !important;
@@ -59,7 +59,6 @@
}
}
-
.password-content {
display: flex;
flex-direction: column;
@@ -76,7 +75,6 @@
color: black;
border-radius: 5px;
padding: 7px;
-
}
}
@@ -148,7 +146,6 @@
margin-top: 2;
text-align: left;
}
-
}
}
@@ -297,9 +294,8 @@
margin-bottom: 10px;
}
-
.error-text {
- color: #C40233;
+ color: #c40233;
width: 300;
margin-left: -20;
font-size: 10;
@@ -313,7 +309,7 @@
font-size: 10;
margin-bottom: 4;
margin-top: -3;
- color: #009F6B;
+ color: #009f6b;
}
.focus-span {
@@ -352,7 +348,6 @@
padding: 0 0 0 20px;
color: black;
}
-
}
}
@@ -421,7 +416,6 @@
.tab-content {
display: flex;
- margin: 20px 0;
.tab-column {
flex: 0 0 50%;
@@ -437,9 +431,7 @@
.tab-column-content {
padding-left: 16px;
}
-
}
-
}
.tab-column button {
@@ -465,4 +457,4 @@
.settings-interface .settings-heading {
font-size: 25;
}
-} \ No newline at end of file
+}
diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx
index 5c1c836f7..f886ce2ca 100644
--- a/src/client/util/SettingsManager.tsx
+++ b/src/client/util/SettingsManager.tsx
@@ -28,7 +28,7 @@ export enum ColorScheme {
export enum freeformScrollMode {
Pan = 'pan',
- Zoom = 'zoom'
+ Zoom = 'zoom',
}
@observer
@@ -74,7 +74,7 @@ export class SettingsManager extends React.Component<{}> {
};
@undoBatch selectUserMode = action((e: React.ChangeEvent) => (Doc.noviceMode = (e.currentTarget as any)?.value === 'Novice'));
- @undoBatch changeShowTitle = action((e: React.ChangeEvent) => (Doc.UserDoc().showTitle = (e.currentTarget as any).value ? 'title' : undefined));
+ @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)));
@@ -176,7 +176,7 @@ export class SettingsManager extends React.Component<{}> {
return (
<div className="prefs-content">
<div>
- <input type="checkbox" onChange={e => (Doc.UserDoc().showTitle = Doc.UserDoc().showTitle ? undefined : 'creationDate')} checked={Doc.UserDoc().showTitle !== undefined} />
+ <input type="checkbox" onChange={e => (Doc.UserDoc().layout_showTitle = Doc.UserDoc().layout_showTitle ? undefined : 'author_date')} checked={Doc.UserDoc().layout_showTitle !== undefined} />
<div className="preferences-check">Show doc header</div>
</div>
<div>
@@ -184,16 +184,20 @@ export class SettingsManager extends React.Component<{}> {
<div className="preferences-check">Show full toolbar</div>
</div>
<div>
- <input type="checkbox" onChange={e => DragManager.SetRaiseWhenDragged(!DragManager.GetRaiseWhenDragged())} checked={DragManager.GetRaiseWhenDragged()} />
- <div className="preferences-check">Raise on drag</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.SetRecognizeGesturs(!FontIconBox.GetRecognizeGestures())} checked={FontIconBox.GetRecognizeGestures()} />
- <div className="preferences-check">Recognize ink Gesturs</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>
</div>
);
@@ -307,11 +311,9 @@ export class SettingsManager extends React.Component<{}> {
);
}
-
-
setFreeformScrollMode = (mode: freeformScrollMode) => {
Doc.UserDoc().freeformScrollMode = mode;
- }
+ };
@computed get modesContent() {
return (
@@ -334,12 +336,20 @@ export class SettingsManager extends React.Component<{}> {
<div className="playground-text">Playground Mode</div>
</div>
</div>
- <div className="tab-column-title">Freeform scroll mode</div>
- <div>
- <button onClick={() => this.setFreeformScrollMode(freeformScrollMode.Pan)}>Scroll to pan</button>
- <div>Scrolling pans around the freeform, holding shift and scrolling zooms in and out.</div>
- <button onClick={() => this.setFreeformScrollMode(freeformScrollMode.Zoom)}>Scroll to zoom</button>
- <div>Scrolling zooms in and out of canvas</div>
+ <div className="tab-column-title" style={{ marginTop: 10, marginBottom: 0 }}>
+ Freeform scrolling
+ </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>
</div>
</div>
<div className="tab-column">
diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx
index 4b0310e76..97e64ab71 100644
--- a/src/client/util/SharingManager.tsx
+++ b/src/client/util/SharingManager.tsx
@@ -5,13 +5,14 @@ import { observer } from 'mobx-react';
import * as React from 'react';
import Select from 'react-select';
import * as RequestPromise from 'request-promise';
-import { AclAugment, AclAdmin, AclEdit, AclPrivate, AclReadonly, AclSym, AclUnset, DataSym, Doc, DocListCast, DocListCastAsync, Opt, AclSelfEdit } from '../../fields/Doc';
+import { Doc, DocListCast, DocListCastAsync, HierarchyMapping } from '../../fields/Doc';
+import { AclAdmin, AclPrivate, DocAcl, AclUnset, DocData } from '../../fields/DocSymbols';
+import { Id } from '../../fields/FieldSymbols';
import { List } from '../../fields/List';
-import { Cast, NumCast, StrCast } from '../../fields/Types';
+import { NumCast, StrCast } from '../../fields/Types';
import { distributeAcls, GetEffectiveAcl, normalizeEmail, SharingPermissions, TraceMobx } from '../../fields/util';
import { Utils } from '../../Utils';
import { DocServer } from '../DocServer';
-import { CollectionView } from '../views/collections/CollectionView';
import { DictationOverlay } from '../views/DictationOverlay';
import { MainViewModal } from '../views/MainViewModal';
import { DocumentView } from '../views/nodes/DocumentView';
@@ -22,8 +23,6 @@ import { GroupManager, UserOptions } from './GroupManager';
import { GroupMemberView } from './GroupMemberView';
import { SelectionManager } from './SelectionManager';
import './SharingManager.scss';
-import { LinkManager } from './LinkManager';
-import { Id } from '../../fields/FieldSymbols';
export interface User {
email: string;
@@ -82,16 +81,6 @@ export class SharingManager extends React.Component<{}> {
@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
- // maps acl symbols to SharingPermissions
- private AclMap = new Map<symbol, string>([
- [AclPrivate, SharingPermissions.None],
- [AclReadonly, SharingPermissions.View],
- [AclAugment, SharingPermissions.Augment],
- [AclSelfEdit, SharingPermissions.SelfEdit],
- [AclEdit, SharingPermissions.Edit],
- [AclAdmin, SharingPermissions.Admin],
- ]);
-
// private get linkVisible() {
// return this.targetDoc ? this.targetDoc["acl-" + PublicKey] !== SharingPermissions.None : false;
// }
@@ -140,35 +129,21 @@ export class SharingManager extends React.Component<{}> {
if (!this.populating && Doc.UserDoc()[Id] !== '__guest__') {
this.populating = true;
const userList = await RequestPromise.get(Utils.prepend('/getUsers'));
- const raw = JSON.parse(userList) as User[];
- const sharingDocs: ValidatedUser[] = [];
- const evaluating = raw.map(async user => {
- const isCandidate = user.email !== Doc.CurrentUserEmail;
- if (isCandidate) {
- const sharingDoc = await DocServer.GetRefField(user.sharingDocumentId);
- const linkDatabase = await DocServer.GetRefField(user.linkDatabaseId);
+ const raw = (JSON.parse(userList) as User[]).filter(user => user.email !== 'guest' && user.email !== Doc.CurrentUserEmail);
+ const docs = await DocServer.GetRefFields(raw.reduce((list, user) => [...list, user.sharingDocumentId, user.linkDatabaseId], [] as string[]));
+ raw.map(
+ action((newUser: User) => {
+ const sharingDoc = docs[newUser.sharingDocumentId];
+ const linkDatabase = docs[newUser.linkDatabaseId];
if (sharingDoc instanceof Doc && linkDatabase instanceof Doc) {
- await DocListCastAsync(linkDatabase.data);
- (await DocListCastAsync(Cast(linkDatabase, Doc, null).data))?.forEach(async link => {
- // makes sure link anchors are loaded to avoid incremental updates to computedFns in LinkManager
- const a1 = await Cast(link?.anchor1, Doc, null);
- const a2 = await Cast(link?.anchor2, Doc, null);
- });
- sharingDocs.push({ user, sharingDoc, linkDatabase, userColor: StrCast(sharingDoc.userColor) });
- }
- }
- });
- return Promise.all(evaluating).then(() => {
- runInAction(async () => {
- for (const sharer of sharingDocs) {
- if (!this.users.find(user => user.user.email === sharer.user.email)) {
- this.users.push(sharer);
- LinkManager.addLinkDB(sharer.linkDatabase);
+ 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);
}
}
- });
- this.populating = false;
- });
+ })
+ );
+ this.populating = false;
}
};
@@ -184,6 +159,7 @@ export class SharingManager extends React.Component<{}> {
const docs = SelectionManager.Views().length < 2 ? [target] : SelectionManager.Views().map(docView => docView.props.Document);
return !docs
+ .map(doc => (this.layoutDocAcls ? doc : doc[DocData]))
.map(doc => {
doc.author === Doc.CurrentUserEmail && !doc[myAcl] && distributeAcls(myAcl, SharingPermissions.Admin, doc, undefined, undefined, isDashboard);
@@ -197,7 +173,7 @@ export class SharingManager extends React.Component<{}> {
this.setDashboardBackground(doc, permission as SharingPermissions);
if (permission !== SharingPermissions.None) return Doc.AddDocToList(sharingDoc, storage, doc);
- else return GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, storage, (doc.aliasOf as Doc) || doc);
+ return GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, storage, (doc.createdFrom as Doc) || doc);
})
.some(success => !success);
};
@@ -217,6 +193,7 @@ export class SharingManager extends React.Component<{}> {
// ! ensures it returns true if document has been shared successfully, false otherwise
return !docs
+ .map(doc => (this.layoutDocAcls ? doc : doc[DocData]))
.map(doc => {
doc.author === Doc.CurrentUserEmail && !doc[`acl-${Doc.CurrentUserEmailNormalized}`] && distributeAcls(`acl-${Doc.CurrentUserEmailNormalized}`, SharingPermissions.Admin, doc, undefined, undefined, isDashboard);
@@ -239,7 +216,7 @@ export class SharingManager extends React.Component<{}> {
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.aliasOf as Doc) || doc); // remove the doc from the list if it already exists
+ 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);
}
@@ -283,6 +260,7 @@ export class SharingManager extends React.Component<{}> {
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);
});
}
};
@@ -290,14 +268,18 @@ export class SharingManager extends React.Component<{}> {
/**
* Sets the background of the Dashboard if it has been shared as a visual indicator
*/
- setDashboardBackground = async (doc: Doc, permission: SharingPermissions) => {
+ 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).every(key => (key === `acl-${Doc.CurrentUserEmailNormalized}` ? true : [AclUnset, AclPrivate].includes(acls[key])))) {
+ const acls = doc[DocData][DocAcl];
+ 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;
}
@@ -372,7 +354,7 @@ export class SharingManager extends React.Component<{}> {
private sharingOptions(uniform: boolean, override?: boolean) {
const dropdownValues: string[] = Object.values(SharingPermissions);
if (!uniform) dropdownValues.unshift('-multiple-');
- if (override) dropdownValues.unshift('None');
+ if (!override) dropdownValues.splice(dropdownValues.indexOf(SharingPermissions.Unset), 1);
return dropdownValues
.filter(permission => !Doc.noviceMode || ![SharingPermissions.SelfEdit].includes(permission as any))
.map(permission => (
@@ -387,12 +369,11 @@ export class SharingManager extends React.Component<{}> {
const docs = SelectionManager.Views().length > 1 ? SelectionManager.Views().map(docView => docView.props.Document) : [this.targetDoc];
return (
<span
- className={'focus-span'}
+ className="focus-span"
title={title}
onClick={() => {
- let context: Opt<CollectionView>;
- if (this.targetDoc && this.targetDocView && docs.length === 1 && (context = this.targetDocView.props.ContainingCollectionView)) {
- DocumentManager.Instance.jumpToDocument(this.targetDoc, true, undefined, [context.props.Document]);
+ if (this.targetDoc && this.targetDocView && docs.length === 1) {
+ DocumentManager.Instance.showDocument(this.targetDoc, { willZoomCentered: true });
}
}}
onPointerEnter={action(() => {
@@ -458,17 +439,6 @@ export class SharingManager extends React.Component<{}> {
}
};
- // distributeOverCollection = (targetDoc?: Doc) => {
- // const target = targetDoc || this.targetDoc!;
-
- // const docs = SelectionManager.Views().length < 2 ? [target] : SelectionManager.Views().map(docView => docView.props.Document);
- // docs.forEach(doc => {
- // for (const [key, value] of Object.entries(doc[AclSym])) {
- // distributeAcls(key, this.AclMap.get(value)! as SharingPermissions, target);
- // }
- // });
- // }
-
/**
* Sorting algorithm to sort users.
*/
@@ -491,6 +461,7 @@ export class SharingManager extends React.Component<{}> {
* @returns the main interface of the SharingManager.
*/
@computed get sharingInterface() {
+ if (!this.targetDoc) return null;
TraceMobx();
const groupList = GroupManager.Instance?.allGroups || [];
const sortedUsers = this.users
@@ -515,7 +486,7 @@ export class SharingManager extends React.Component<{}> {
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.layoutDocAcls ? this.targetDoc : this.targetDoc?.[DocData]] : SelectionManager.Views().map(docView => (this.layoutDocAcls ? docView.props.Document : docView.props.Document?.[DocData]));
if (this.myDocAcls) {
const newDocs: Doc[] = [];
@@ -523,21 +494,21 @@ export class SharingManager extends React.Component<{}> {
docs = newDocs.filter(doc => GetEffectiveAcl(doc) === AclAdmin);
}
- const targetDoc = docs[0];
+ 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?.[AclSym] && Object.keys(doc[AclSym]) : doc?.[DataSym]?.[AclSym] && Object.keys(doc[DataSym][AclSym]))));
+ const commonKeys = intersection(...docs.map(doc => (this.layoutDocAcls ? doc : doc[DocData])).map(doc => doc?.[DocAcl] && Object.keys(doc[DocAcl])));
// 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))
.map(({ user, linkDatabase, sharingDoc, userColor }) => {
const userKey = `acl-${normalizeEmail(user.email)}`;
- const uniform = docs.every(doc => (this.layoutDocAcls ? doc?.[AclSym]?.[userKey] === docs[0]?.[AclSym]?.[userKey] : doc?.[DataSym]?.[AclSym]?.[userKey] === docs[0]?.[DataSym]?.[AclSym]?.[userKey]));
+ const uniform = docs.map(doc => (this.layoutDocAcls ? doc : doc[DocData])).every(doc => doc?.[DocAcl]?.[userKey] === docs[0]?.[DocAcl]?.[userKey]);
const permissions = uniform ? StrCast(targetDoc?.[userKey]) : '-multiple-';
return !permissions ? null : (
@@ -563,7 +534,7 @@ export class SharingManager extends React.Component<{}> {
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>
@@ -573,7 +544,7 @@ 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]) ? this.AclMap.get(effectiveAcls[0])! : '-multiple-'}</div>
+ <div className={'permissions-dropdown'}>{effectiveAcls.every(acl => acl === effectiveAcls[0]) ? HierarchyMapping.get(effectiveAcls[0])!.name : '-multiple-'}</div>
</div>
</div>
) : null
@@ -584,7 +555,9 @@ export class SharingManager extends React.Component<{}> {
groupListMap.unshift({ title: 'Public' }); //, { title: "ALL" });
const groupListContents = groupListMap.map(group => {
const groupKey = `acl-${StrCast(group.title)}`;
- const uniform = docs.every(doc => (this.layoutDocAcls ? doc?.[AclSym]?.[groupKey] === docs[0]?.[AclSym]?.[groupKey] : doc?.[DataSym]?.[AclSym]?.[groupKey] === docs[0]?.[DataSym]?.[AclSym]?.[groupKey]));
+ const uniform = docs
+ .map(doc => (this.layoutDocAcls ? doc : doc[DocData]))
+ .every(doc => (this.layoutDocAcls ? doc?.[DocAcl]?.[groupKey] === docs[0]?.[DocAcl]?.[groupKey] : doc?.[DocData]?.[DocAcl]?.[groupKey] === docs[0]?.[DocData]?.[DocAcl]?.[groupKey]));
const permissions = uniform ? StrCast(targetDoc?.[`acl-${StrCast(group.title)}`]) : '-multiple-';
return !permissions ? null : (
@@ -607,9 +580,8 @@ export class SharingManager extends React.Component<{}> {
</div>
);
});
-
return (
- <div className={'sharing-interface'}>
+ <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'}>
diff --git a/src/client/util/SnappingManager.ts b/src/client/util/SnappingManager.ts
index 2c0a1da8b..ed9819fc0 100644
--- a/src/client/util/SnappingManager.ts
+++ b/src/client/util/SnappingManager.ts
@@ -40,9 +40,9 @@ export namespace SnappingManager {
}
export function SetShowSnapLines(show: boolean) {
- runInAction(() => (Doc.UserDoc().showSnapLines = show));
+ runInAction(() => (Doc.UserDoc().freeform_snapLines = show));
}
export function GetShowSnapLines() {
- return Doc.UserDoc().showSnapLines;
+ return Doc.UserDoc().freeform_snapLines;
}
}
diff --git a/src/client/util/TrackMovements.ts b/src/client/util/TrackMovements.ts
index 4a2ccd706..0e56ee1bc 100644
--- a/src/client/util/TrackMovements.ts
+++ b/src/client/util/TrackMovements.ts
@@ -3,13 +3,14 @@ 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;
panX: number;
panY: number;
scale: number;
- docId: string;
+ doc: Doc;
};
export type Presentation = {
@@ -28,7 +29,7 @@ export class TrackMovements {
private tracking: boolean;
private absoluteStart: number;
// instance variable for holding the FFViews and their disposers
- private recordingFFViews: Map<string, IReactionDisposer> | null;
+ private recordingFFViews: Map<Doc, IReactionDisposer> | null;
private tabChangeDisposeFunc: IReactionDisposer | null;
// create static instance and getter for global use
@@ -55,33 +56,33 @@ export class TrackMovements {
return this.currentPresentation.movements === null;
}
- private addRecordingFFView(doc: Doc, key: string = doc[Id]): void {
+ private addRecordingFFView(doc: Doc): void {
// console.info('adding dispose func : docId', key, 'doc', doc);
if (this.recordingFFViews === null) {
console.warn('addFFView on null RecordingApi');
return;
}
- if (this.recordingFFViews.has(key)) {
- console.warn('addFFView : key already in map');
+ if (this.recordingFFViews.has(doc)) {
+ console.warn('addFFView : doc already in map');
return;
}
const disposeFunc = reaction(
- () => ({ x: NumCast(doc.panX, -1), y: NumCast(doc.panY, -1), scale: NumCast(doc.viewScale, 0) }),
- res => res.x !== -1 && res.y !== -1 && this.tracking && this.trackMovement(res.x, res.y, key, res.scale)
+ () => ({ x: NumCast(doc.freeform_panX, -1), y: NumCast(doc.freeform_panY, -1), scale: NumCast(doc.freeform_scale, 0) }),
+ res => res.x !== -1 && res.y !== -1 && this.tracking && this.trackMovement(res.x, res.y, doc, res.scale)
);
- this.recordingFFViews?.set(key, disposeFunc);
+ this.recordingFFViews?.set(doc, disposeFunc);
}
- private removeRecordingFFView = (key: string) => {
+ private removeRecordingFFView = (doc: Doc) => {
// console.info('removing dispose func : docId', key);
if (this.recordingFFViews === null) {
console.warn('removeFFView on null RecordingApi');
return;
}
- this.recordingFFViews.get(key)?.();
- this.recordingFFViews.delete(key);
+ this.recordingFFViews.get(doc)?.();
+ this.recordingFFViews.delete(doc);
};
// in the case where only one tab was changed (updates not across dashboards), set only one to true
@@ -89,16 +90,16 @@ 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 tabbedFFViews = new Set<string>();
+ 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[Id]);
+ if (isFFView(DashDoc)) tabbedFFViews.add(DashDoc);
}
// new tab was added - need to add it
if (tabbedFFViews.size > this.recordingFFViews.size) {
for (const DashDoc of tabbedDocs) {
- if (!this.recordingFFViews.has(DashDoc[Id])) {
+ if (!this.recordingFFViews.has(DashDoc)) {
if (isFFView(DashDoc)) {
this.addRecordingFFView(DashDoc);
@@ -110,9 +111,9 @@ export class TrackMovements {
}
// tab was removed - need to remove it from recordingFFViews
else if (tabbedFFViews.size < this.recordingFFViews.size) {
- for (const [key] of this.recordingFFViews) {
- if (!tabbedFFViews.has(key)) {
- this.removeRecordingFFView(key);
+ for (const [doc] of this.recordingFFViews) {
+ if (!tabbedFFViews.has(doc)) {
+ this.removeRecordingFFView(doc);
if (onlyOne) return;
}
}
@@ -214,7 +215,7 @@ export class TrackMovements {
}
};
- private trackMovement = (panX: number, panY: number, docId: string, scale: number = 0) => {
+ private trackMovement = (panX: number, panY: number, doc: Doc, scale: number = 0) => {
// ensure we are recording to track
if (!this.tracking) {
console.error('[recordingApi.ts] trackMovements(): tracking is false');
@@ -231,10 +232,10 @@ export class TrackMovements {
// get the time
const time = new Date().getTime() - this.absoluteStart;
// make new movement object
- const movement: Movement = { time, panX, panY, scale, docId };
+ const movement: Movement = { time, panX, panY, scale, doc };
// add that movement to the current presentation data's movement array
- this.currentPresentation.movements && this.currentPresentation.movements.push(movement);
+ this.currentPresentation.movements?.push(movement);
};
// method that concatenates an array of presentatations into one
diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts
index d0aec45a6..b59af6656 100644
--- a/src/client/util/UndoManager.ts
+++ b/src/client/util/UndoManager.ts
@@ -1,9 +1,10 @@
import { observable, action, runInAction } from 'mobx';
+import { Field } from '../../fields/Doc';
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 +35,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 +85,18 @@ 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 Array ? value.map(val => Field.toScriptString(val)).join(',') : Field.toScriptString(value)));
currentBatch.push(event);
tempEvents?.push(event);
}
@@ -135,11 +150,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 +164,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 +188,11 @@ export namespace UndoManager {
return false;
});
- export function RunInTempBatch<T>(fn: () => T) {
+ export function StartTempBatch() {
tempEvents = [];
- try {
- const success = runInAction(fn);
- if (!success) UndoManager.UndoTempBatch();
- return success;
- } finally {
- tempEvents = undefined;
- }
+ }
+ export function EndTempBatch<T>(success: boolean) {
+ UndoManager.UndoTempBatch(success);
}
//TODO Make this return the return value
export function RunInBatch<T>(fn: () => T, batchName: string) {
@@ -189,10 +203,11 @@ export namespace UndoManager {
batch.end();
}
}
- export const UndoTempBatch = action(() => {
- if (tempEvents) {
+ export const UndoTempBatch = action((success: any) => {
+ if (tempEvents && !success) {
undoing = true;
for (let i = tempEvents.length - 1; i >= 0; i--) {
+ currentBatch?.includes(tempEvents[i]) && currentBatch.splice(currentBatch.indexOf(tempEvents[i]));
tempEvents[i].undo();
}
undoing = false;
@@ -204,6 +219,7 @@ export namespace UndoManager {
return;
}
+ const names = undoStackNames.pop();
const commands = undoStack.pop();
if (!commands) {
return;
@@ -215,6 +231,7 @@ export namespace UndoManager {
}
undoing = false;
+ redoStackNames.push(names ?? '???');
redoStack.push(commands);
});
@@ -223,6 +240,7 @@ export namespace UndoManager {
return;
}
+ const names = redoStackNames.pop();
const commands = redoStack.pop();
if (!commands) {
return;
@@ -234,6 +252,7 @@ export namespace UndoManager {
}
undoing = false;
+ undoStackNames.push(names ?? '???');
undoStack.push(commands);
});
}
diff --git a/src/client/util/request-image-size.js b/src/client/util/request-image-size.ts
index 502e0fbac..57e8516ac 100644
--- a/src/client/util/request-image-size.js
+++ b/src/client/util/request-image-size.ts
@@ -13,7 +13,7 @@ const request = require('request');
const imageSize = require('image-size');
const HttpError = require('standard-http-error');
-module.exports = function requestImageSize(options) {
+module.exports = function requestImageSize(options: any) {
let opts = {
encoding: null,
};
@@ -36,28 +36,24 @@ module.exports = function requestImageSize(options) {
return new Promise((resolve, reject) => {
const req = request(opts);
- req.on('response', res => {
+ req.on('response', (res: any) => {
if (res.statusCode >= 400) {
return reject(new HttpError(res.statusCode, res.statusMessage));
}
let buffer = Buffer.from([]);
- let size;
+ let size: any;
- res.on('data', chunk => {
+ res.on('data', (chunk: any) => {
buffer = Buffer.concat([buffer, chunk]);
try {
size = imageSize(buffer);
- } catch (err) {
- reject(err);
- return req.abort();
- }
-
- if (size) {
- resolve(size);
- return req.abort();
- }
+ if (size) {
+ resolve(size);
+ return req.abort();
+ }
+ } catch (err) {}
});
res.on('error', reject);
diff --git a/src/client/views/.DS_Store b/src/client/views/.DS_Store
index e4ac87aad..5fa69f28c 100644
--- a/src/client/views/.DS_Store
+++ b/src/client/views/.DS_Store
Binary files differ
diff --git a/src/client/views/AntimodeMenu.tsx b/src/client/views/AntimodeMenu.tsx
index 0f1fc6b69..de1207ce4 100644
--- a/src/client/views/AntimodeMenu.tsx
+++ b/src/client/views/AntimodeMenu.tsx
@@ -1,8 +1,7 @@
-import React = require("react");
-import { observable, action, runInAction } from "mobx";
-import "./AntimodeMenu.scss";
-export interface AntimodeMenuProps {
-}
+import React = require('react');
+import { observable, action, runInAction } from 'mobx';
+import './AntimodeMenu.scss';
+export interface AntimodeMenuProps {}
/**
* This is an abstract class that serves as the base for a PDF-style or Marquee-style
@@ -17,15 +16,19 @@ export abstract class AntimodeMenu<T extends AntimodeMenuProps> extends React.Co
@observable protected _top: number = -300;
@observable protected _left: number = -300;
@observable protected _opacity: number = 0;
- @observable protected _transitionProperty: string = "opacity";
- @observable protected _transitionDuration: string = "0.5s";
- @observable protected _transitionDelay: string = "";
+ @observable protected _transitionProperty: string = 'opacity';
+ @observable protected _transitionDuration: string = '0.5s';
+ @observable protected _transitionDelay: string = '';
@observable protected _canFade: boolean = false;
@observable public Pinned: boolean = false;
- get width() { return this._mainCont.current ? this._mainCont.current.getBoundingClientRect().width : 0; }
- get height() { return this._mainCont.current ? this._mainCont.current.getBoundingClientRect().height : 0; }
+ get width() {
+ return this._mainCont.current ? this._mainCont.current.getBoundingClientRect().width : 0;
+ }
+ get height() {
+ return this._mainCont.current ? this._mainCont.current.getBoundingClientRect().height : 0;
+ }
@action
/**
@@ -36,12 +39,12 @@ export abstract class AntimodeMenu<T extends AntimodeMenuProps> extends React.Co
*/
public jumpTo = (x: number, y: number, forceJump: boolean = false) => {
if (!this.Pinned || forceJump) {
- this._transitionProperty = this._transitionDuration = this._transitionDelay = "";
+ this._transitionProperty = this._transitionDuration = this._transitionDelay = '';
this._opacity = 1;
this._left = x;
this._top = y;
}
- }
+ };
@action
/**
@@ -51,56 +54,56 @@ export abstract class AntimodeMenu<T extends AntimodeMenuProps> extends React.Co
public fadeOut = (forceOut: boolean) => {
if (!this.Pinned) {
if (this._opacity === 0.2) {
- this._transitionProperty = "opacity";
- this._transitionDuration = "0.1s";
+ this._transitionProperty = 'opacity';
+ this._transitionDuration = '0.1s';
}
if (forceOut) {
- this._transitionProperty = "";
- this._transitionDuration = "";
+ this._transitionProperty = '';
+ this._transitionDuration = '';
}
- this._transitionDelay = "";
+ this._transitionDelay = '';
this._opacity = 0;
this._left = this._top = -300;
}
- }
+ };
@action
protected pointerLeave = (e: React.PointerEvent) => {
if (!this.Pinned && this._canFade) {
- this._transitionProperty = "opacity";
- this._transitionDuration = "0.5s";
- this._transitionDelay = "1s";
+ this._transitionProperty = 'opacity';
+ this._transitionDuration = '0.5s';
+ this._transitionDelay = '1s';
this._opacity = 0.2;
setTimeout(() => this.fadeOut(false), 3000);
}
- }
+ };
@action
protected pointerEntered = (e: React.PointerEvent) => {
- this._transitionProperty = "opacity";
- this._transitionDuration = "0.1s";
- this._transitionDelay = "";
+ this._transitionProperty = 'opacity';
+ this._transitionDuration = '0.1s';
+ this._transitionDelay = '';
this._opacity = 1;
- }
+ };
@action
protected togglePin = (e: React.MouseEvent) => {
- runInAction(() => this.Pinned = !this.Pinned);
- }
+ runInAction(() => (this.Pinned = !this.Pinned));
+ };
protected dragStart = (e: React.PointerEvent) => {
- document.removeEventListener("pointermove", this.dragging);
- document.addEventListener("pointermove", this.dragging);
- document.removeEventListener("pointerup", this.dragEnd);
- document.addEventListener("pointerup", this.dragEnd);
+ document.removeEventListener('pointermove', this.dragging);
+ document.addEventListener('pointermove', this.dragging);
+ document.removeEventListener('pointerup', this.dragEnd);
+ document.addEventListener('pointerup', this.dragEnd);
this._offsetX = e.pageX - this._mainCont.current!.getBoundingClientRect().left;
this._offsetY = e.pageY - this._mainCont.current!.getBoundingClientRect().top;
e.stopPropagation();
e.preventDefault();
- }
+ };
@action
protected dragging = (e: PointerEvent) => {
@@ -115,32 +118,41 @@ export abstract class AntimodeMenu<T extends AntimodeMenuProps> extends React.Co
e.stopPropagation();
e.preventDefault();
- }
+ };
protected dragEnd = (e: PointerEvent) => {
- document.removeEventListener("pointermove", this.dragging);
- document.removeEventListener("pointerup", this.dragEnd);
+ document.removeEventListener('pointermove', this.dragging);
+ document.removeEventListener('pointerup', this.dragEnd);
e.stopPropagation();
e.preventDefault();
- }
+ };
protected handleContextMenu = (e: React.MouseEvent) => {
e.stopPropagation();
e.preventDefault();
- }
+ };
protected getDragger = () => {
- return <div className="antimodeMenu-dragger" key="dragger" onPointerDown={this.dragStart} style={{ width: "20px" }} />;
- }
+ return <div className="antimodeMenu-dragger" key="dragger" onPointerDown={this.dragStart} style={{ width: '20px' }} />;
+ };
- protected getElement(buttons: JSX.Element[]) {
+ protected getElement(buttons: JSX.Element) {
return (
- <div className="antimodeMenu-cont" onPointerLeave={this.pointerLeave} onPointerEnter={this.pointerEntered} ref={this._mainCont} onContextMenu={this.handleContextMenu}
+ <div
+ className="antimodeMenu-cont"
+ onPointerLeave={this.pointerLeave}
+ onPointerEnter={this.pointerEntered}
+ ref={this._mainCont}
+ onContextMenu={this.handleContextMenu}
style={{
- left: this._left, top: this._top, opacity: this._opacity, transitionProperty: this._transitionProperty, transitionDuration: this._transitionDuration, transitionDelay: this._transitionDelay,
- position: this.Pinned ? "unset" : undefined
+ left: this._left,
+ top: this._top,
+ opacity: this._opacity,
+ transitionProperty: this._transitionProperty,
+ transitionDuration: this._transitionDuration,
+ transitionDelay: this._transitionDelay,
+ position: this.Pinned ? 'unset' : undefined,
}}>
- {/* {this.getDragger} */}
{buttons}
</div>
);
@@ -148,34 +160,51 @@ export abstract class AntimodeMenu<T extends AntimodeMenuProps> extends React.Co
protected getElementVert(buttons: JSX.Element[]) {
return (
- <div className="antimodeMenu-cont" onPointerLeave={this.pointerLeave} onPointerEnter={this.pointerEntered} ref={this._mainCont} onContextMenu={this.handleContextMenu}
+ <div
+ className="antimodeMenu-cont"
+ onPointerLeave={this.pointerLeave}
+ onPointerEnter={this.pointerEntered}
+ ref={this._mainCont}
+ onContextMenu={this.handleContextMenu}
style={{
left: this.Pinned ? undefined : this._left,
top: this.Pinned ? 0 : this._top,
right: this.Pinned ? 0 : undefined,
- height: "inherit",
+ height: 'inherit',
width: 200,
- opacity: this._opacity, transitionProperty: this._transitionProperty, transitionDuration: this._transitionDuration, transitionDelay: this._transitionDelay,
- position: this.Pinned ? "absolute" : undefined
+ opacity: this._opacity,
+ transitionProperty: this._transitionProperty,
+ transitionDuration: this._transitionDuration,
+ transitionDelay: this._transitionDelay,
+ position: this.Pinned ? 'absolute' : undefined,
}}>
{buttons}
</div>
);
}
-
-
protected getElementWithRows(rows: JSX.Element[], numRows: number, hasDragger: boolean = true) {
return (
- <div className="antimodeMenu-cont with-rows" onPointerLeave={this.pointerLeave} onPointerEnter={this.pointerEntered} ref={this._mainCont} onContextMenu={this.handleContextMenu}
+ <div
+ className="antimodeMenu-cont with-rows"
+ onPointerLeave={this.pointerLeave}
+ onPointerEnter={this.pointerEntered}
+ ref={this._mainCont}
+ onContextMenu={this.handleContextMenu}
style={{
- left: this._left, top: this._top, opacity: this._opacity, transitionProperty: this._transitionProperty,
- transitionDuration: this._transitionDuration, transitionDelay: this._transitionDelay, height: "auto",
- flexDirection: this.Pinned ? "row" : undefined, position: this.Pinned ? "unset" : undefined
+ left: this._left,
+ top: this._top,
+ opacity: this._opacity,
+ transitionProperty: this._transitionProperty,
+ transitionDuration: this._transitionDuration,
+ transitionDelay: this._transitionDelay,
+ height: 'auto',
+ flexDirection: this.Pinned ? 'row' : undefined,
+ position: this.Pinned ? 'unset' : undefined,
}}>
- {hasDragger ? <div className="antimodeMenu-dragger" onPointerDown={this.dragStart} style={{ width: "20px" }} /> : (null)}
+ {hasDragger ? <div className="antimodeMenu-dragger" onPointerDown={this.dragStart} style={{ width: '20px' }} /> : null}
{rows}
</div>
);
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/AudioWaveform.tsx b/src/client/views/AudioWaveform.tsx
index 525c0ce5a..14c922526 100644
--- a/src/client/views/AudioWaveform.tsx
+++ b/src/client/views/AudioWaveform.tsx
@@ -1,27 +1,25 @@
-import React = require("react");
-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 { List } from "../../fields/List";
-import { listSpec } from "../../fields/Schema";
-import { Cast } from "../../fields/Types";
-import { numberRange } from "../../Utils";
-import "./AudioWaveform.scss";
-import { Colors } from "./global/globalEnums";
-
+import React = require('react');
+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 { List } from '../../fields/List';
+import { listSpec } from '../../fields/Schema';
+import { Cast } from '../../fields/Types';
+import { numberRange } from '../../Utils';
+import './AudioWaveform.scss';
+import { Colors } from './global/globalEnums';
/**
* AudioWaveform
- *
+ *
* Used in CollectionStackedTimeline to render a canvas with a visual of an audio waveform for AudioBox and VideoBox documents.
* Uses react-audio-waveform package.
* Bins the audio data into audioBuckets which are passed to package to render the lines.
* Calculates new buckets each time a new zoom factor or new set of trim bounds is created and stores it in a field on the layout doc with a title indicating the bounds and zoom for that list (see audioBucketField)
*/
-
export interface AudioWaveformProps {
duration: number; // length of media clip
rawDuration: number; // length of underlying media data
@@ -32,6 +30,7 @@ export interface AudioWaveformProps {
zoomFactor: number;
PanelHeight: number;
PanelWidth: number;
+ fieldKey: string;
}
@observer
@@ -40,80 +39,71 @@ export class AudioWaveform extends React.Component<AudioWaveformProps> {
_disposer: IReactionDisposer | undefined;
- @computed get waveHeight() { return Math.max(50, this.props.PanelHeight); }
-
- @computed get clipStart() { return this.props.clipStart; }
- @computed get clipEnd() { return this.props.clipEnd; }
- @computed get zoomFactor() { return this.props.zoomFactor; }
+ @computed get waveHeight() {
+ return Math.max(50, this.props.PanelHeight);
+ }
- @computed get audioBuckets() { return Cast(this.props.layoutDoc[this.audioBucketField(this.clipStart, this.clipEnd, this.zoomFactor)], listSpec("number"), []); }
- audioBucketField = (start: number, end: number, zoomFactor: number) => "audioBuckets/" + "/" + start.toFixed(2).replace(".", "_") + "/" + end.toFixed(2).replace(".", "_") + "/" + (zoomFactor * 10);
+ @computed get clipStart() {
+ return this.props.clipStart;
+ }
+ @computed get clipEnd() {
+ return this.props.clipEnd;
+ }
+ @computed get zoomFactor() {
+ return this.props.zoomFactor;
+ }
+ @computed get audioBuckets() {
+ return Cast(this.props.layoutDoc[this.audioBucketField(this.clipStart, this.clipEnd, this.zoomFactor)], listSpec('number'), []);
+ }
+ audioBucketField = (start: number, end: number, zoomFactor: number) => this.props.fieldKey + '_audioBuckets/' + '/' + start.toFixed(2).replace('.', '_') + '/' + end.toFixed(2).replace('.', '_') + '/' + zoomFactor * 10;
componentWillUnmount() {
this._disposer?.();
}
componentDidMount() {
- this._disposer = reaction(() => ({ clipStart: this.clipStart, clipEnd: this.clipEnd, fieldKey: this.audioBucketField(this.clipStart, this.clipEnd, this.zoomFactor), zoomFactor: this.props.zoomFactor }),
+ this._disposer = reaction(
+ () => ({ clipStart: this.clipStart, clipEnd: this.clipEnd, fieldKey: this.audioBucketField(this.clipStart, this.clipEnd, this.zoomFactor), zoomFactor: this.props.zoomFactor }),
({ clipStart, clipEnd, fieldKey, zoomFactor }) => {
if (!this.props.layoutDoc[fieldKey]) {
// setting these values here serves as a "lock" to prevent multiple attempts to create the waveform at nerly the same time.
- const waveform = Cast(this.props.layoutDoc[this.audioBucketField(0, this.props.rawDuration, 1)], listSpec("number"));
- this.props.layoutDoc[fieldKey] = waveform && new List<number>(waveform.slice(clipStart / this.props.rawDuration * waveform.length, clipEnd / this.props.rawDuration * waveform.length));
+ const waveform = Cast(this.props.layoutDoc[this.audioBucketField(0, this.props.rawDuration, 1)], listSpec('number'));
+ this.props.layoutDoc[fieldKey] = waveform && new List<number>(waveform.slice((clipStart / this.props.rawDuration) * waveform.length, (clipEnd / this.props.rawDuration) * waveform.length));
setTimeout(() => this.createWaveformBuckets(fieldKey, clipStart, clipEnd, zoomFactor));
}
- }, { fireImmediately: true });
+ },
+ { fireImmediately: true }
+ );
}
// decodes the audio file into peaks for generating the waveform
createWaveformBuckets = async (fieldKey: string, clipStart: number, clipEnd: number, zoomFactor: number) => {
- axios({ url: this.props.mediaPath, responseType: "arraybuffer" }).then(
- (response) => {
- const context = new window.AudioContext();
- context.decodeAudioData(
- response.data,
- action((buffer) => {
- const rawDecodedAudioData = buffer.getChannelData(0);
- const startInd = clipStart / this.props.rawDuration;
- const endInd = clipEnd / this.props.rawDuration;
- const decodedAudioData = rawDecodedAudioData.slice(Math.floor(startInd * rawDecodedAudioData.length), Math.floor(endInd * rawDecodedAudioData.length));
- const numBuckets = Math.floor(AudioWaveform.NUMBER_OF_BUCKETS * zoomFactor);
+ axios({ url: this.props.mediaPath, responseType: 'arraybuffer' }).then(response => {
+ const context = new window.AudioContext();
+ context.decodeAudioData(
+ response.data,
+ action(buffer => {
+ const rawDecodedAudioData = buffer.getChannelData(0);
+ const startInd = clipStart / this.props.rawDuration;
+ const endInd = clipEnd / this.props.rawDuration;
+ const decodedAudioData = rawDecodedAudioData.slice(Math.floor(startInd * rawDecodedAudioData.length), Math.floor(endInd * rawDecodedAudioData.length));
+ const numBuckets = Math.floor(AudioWaveform.NUMBER_OF_BUCKETS * zoomFactor);
- const bucketDataSize = Math.floor(
- decodedAudioData.length / numBuckets
- );
- const brange = Array.from(Array(bucketDataSize));
- const bucketList = numberRange(numBuckets).map(
- (i: number) =>
- brange.reduce(
- (p, x, j) =>
- Math.abs(
- Math.max(p, decodedAudioData[i * bucketDataSize + j])
- ),
- 0
- ) / 2
- );
- this.props.layoutDoc[fieldKey] = new List<number>(bucketList);
- })
- );
- }
- );
- }
+ const bucketDataSize = Math.floor(decodedAudioData.length / numBuckets);
+ const brange = Array.from(Array(bucketDataSize));
+ const bucketList = numberRange(numBuckets).map((i: number) => brange.reduce((p, x, j) => Math.abs(Math.max(p, decodedAudioData[i * bucketDataSize + j])), 0) / 2);
+ this.props.layoutDoc[fieldKey] = new List<number>(bucketList);
+ })
+ );
+ });
+ };
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={this.audioBuckets} progressColor={Colors.MEDIUM_BLUE_ALT} />
</div>
);
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx
index 6a530e3ae..e4c3e864b 100644
--- a/src/client/views/ContextMenu.tsx
+++ b/src/client/views/ContextMenu.tsx
@@ -102,13 +102,11 @@ export class ContextMenu extends React.Component {
}
}
@action
- moveAfter(item: ContextMenuProps, after: ContextMenuProps) {
- if (after && this.findByDescription(after.description)) {
- const curInd = this._items.findIndex(i => i.description === item.description);
- this._items.splice(curInd, 1);
- const afterInd = this._items.findIndex(i => i.description === after.description);
- this._items.splice(afterInd + 1, 0, item);
- }
+ moveAfter(item: ContextMenuProps, after?: ContextMenuProps) {
+ const curInd = this._items.findIndex(i => i.description === item.description);
+ this._items.splice(curInd, 1);
+ const afterInd = after && this.findByDescription(after.description) ? this._items.findIndex(i => i.description === after.description) : this._items.length;
+ this._items.splice(afterInd, 0, item);
}
@action
setDefaultItem(prefix: string, item: (name: string) => void) {
diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx
index dc9c2eb6c..33f250986 100644
--- a/src/client/views/ContextMenuItem.tsx
+++ b/src/client/views/ContextMenuItem.tsx
@@ -29,6 +29,7 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select
@observable private _items: Array<ContextMenuProps> = [];
@observable private overItem = false;
+ @action
componentDidMount() {
this._items.length = 0;
if ((this.props as SubmenuProps)?.subitems) {
@@ -39,10 +40,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();
}
diff --git a/src/client/views/DashboardView.tsx b/src/client/views/DashboardView.tsx
index 7ebe8d0e3..6aae302ac 100644
--- a/src/client/views/DashboardView.tsx
+++ b/src/client/views/DashboardView.tsx
@@ -3,7 +3,8 @@ import { Button, ColorPicker, FontSize, IconButton, Size } from 'browndash-compo
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 { Doc, DocListCast, DocListCastAsync } from '../../fields/Doc';
+import { DocData } from '../../fields/DocSymbols';
import { Id } from '../../fields/FieldSymbols';
import { List } from '../../fields/List';
import { PrefetchProxy } from '../../fields/Proxy';
@@ -66,18 +67,18 @@ export class DashboardView extends React.Component {
if (this.selectedDashboardGroup === 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);
+ const sharedDashboards = DocListCast(Doc.MySharedDocs.data).filter(doc => doc._type_collection === CollectionViewType.Docking);
return sharedDashboards;
}
};
isUnviewedSharedDashboard = (dashboard: Doc): boolean => {
- // const sharedDashboards = DocListCast(Doc.MySharedDocs.data).filter(doc => doc._viewType === CollectionViewType.Docking);
+ // const sharedDashboards = DocListCast(Doc.MySharedDocs.data).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).filter(doc => doc._type_collection === CollectionViewType.Docking);
return sharedDashs.filter(dashboard => !DocListCast(Doc.MySharedDocs.viewed).includes(dashboard));
};
@@ -109,17 +110,8 @@ export class DashboardView extends React.Component {
/>
</div>
<div className="button-bar">
- <Button text="Cancel" borderRadius={10} hoverStyle={'gray'} fontSize={FontSize.SECONDARY} onClick={this.abortCreateNewDashboard} />
- <Button
- text="Create"
- borderRadius={10}
- backgroundColor={Colors.LIGHT_BLUE}
- hoverStyle={'darken'}
- fontSize={FontSize.SECONDARY}
- onClick={() => {
- this.createNewDashboard(this.newDashboardName!, this.newDashboardColor);
- }}
- />
+ <Button text="Cancel" onClick={this.abortCreateNewDashboard} />
+ <Button text="Create" onClick={() => this.createNewDashboard(this.newDashboardName!, this.newDashboardColor)} />
</div>
</div>
);
@@ -163,7 +155,7 @@ export class DashboardView extends React.Component {
<div className="dashboard-view">
<div className="left-menu">
<div className="new-dashboard-button">
- <Button icon={<FaPlus />} hoverStyle="darken" backgroundColor={Colors.LIGHT_BLUE} size={Size.MEDIUM} fontSize={FontSize.HEADER} text="New" onClick={() => this.setNewDashboardName('')} borderRadius={50} />
+ <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
@@ -196,7 +188,7 @@ export class DashboardView extends React.Component {
e.stopPropagation();
this.onContextMenu(dashboard, e);
}}>
- <IconButton isCircle={true} size={Size.SMALL} hoverStyle="gray" icon={<FontAwesomeIcon color="black" size="lg" icon="bars" />} />
+ <Button size={Size.SMALL} icon={<FontAwesomeIcon color="black" size="lg" icon="bars" />} />
</div>
</div>
</div>
@@ -233,7 +225,6 @@ export class DashboardView extends React.Component {
/// 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) => {
if (!doc) return false;
- Doc.MainDocId = doc[Id];
Doc.AddDocToList(Doc.MyDashboards, 'data', doc);
// this has the side-effect of setting the main container since we're assigning the active/guest dashboard
@@ -368,18 +359,17 @@ export class DashboardView extends React.Component {
y: 400,
_width: 1500,
_height: 1000,
- _fitWidth: true,
- _backgroundGridShow: true,
+ _layout_fitWidth: true,
+ _freeform_backgroundGrid: true,
backgroundColor: background,
title: `Untitled Tab 1`,
};
const title = name ? name : `Dashboard ${dashboardCount}`;
const freeformDoc = Doc.GuestTarget || Docs.Create.FreeformDocument([], freeformOptions);
const dashboardDoc = Docs.Create.StandardCollectionDockingDocument([{ doc: freeformDoc, initialWidth: 600 }], { title: title }, id, 'row');
- freeformDoc.context = dashboardDoc;
// switching the tabs from the datadoc to the regular doc
- const dashboardTabs = DocListCast(dashboardDoc[DataSym].data);
+ const dashboardTabs = DocListCast(dashboardDoc[DocData].data);
dashboardDoc.data = new List<Doc>(dashboardTabs);
dashboardDoc['pane-count'] = 1;
@@ -399,14 +389,14 @@ 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',
btnType: ButtonType.ClickButton,
buttonText: 'New trail',
icon: 'plus',
- system: true,
+ isSystem: true,
};
const reqdBtnScript = { onClick: `createNewPresentation()` };
const myTrailsBtn = DocUtils.AssignScripts(Docs.Create.FontIconDocument(reqdBtnOpts), reqdBtnScript);
@@ -416,25 +406,24 @@ export class DashboardView extends React.Component {
// but we don't want to create the list of trails here-- but rather in createDashboard
const reqdOpts: DocumentOptions = {
title: 'My Trails',
- _showTitle: 'title',
+ _layout_showTitle: 'title',
_height: 100,
treeViewHideTitle: true,
- _fitWidth: true,
+ _layout_fitWidth: true,
_gridGap: 5,
_forceActive: true,
- childDropAction: 'alias',
+ 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',
- system: true,
- explainer: 'All of the trails that you have created will appear here.',
+ dropAction: 'same',
+ isSystem: true,
+ 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);
@@ -463,7 +452,7 @@ ScriptingGlobals.add(function resetDashboard(dashboard: Doc) {
DashboardView.resetDashboard(dashboard);
}, 'move all dashboard tabs to single stack');
ScriptingGlobals.add(function addToDashboards(dashboard: Doc) {
- DashboardView.openDashboard(Doc.MakeAlias(dashboard));
+ DashboardView.openDashboard(Doc.MakeEmbedding(dashboard));
}, 'adds Dashboard to set of Dashboards');
ScriptingGlobals.add(function snapshotDashboard() {
DashboardView.snapshotDashboard();
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index 043a83d16..3de40a640 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -1,7 +1,7 @@
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 { DocListCast, Opt, Doc } 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';
@@ -16,6 +16,7 @@ import { Touchable } from './Touchable';
/// DocComponent returns a generic React base class used by views that don't have 'fieldKey' props (e.g.,CollectionFreeFormDocumentView, DocumentView)
export interface DocComponentProps {
Document: Doc;
+ fieldKey?: string;
LayoutTemplate?: () => Opt<Doc>;
LayoutTemplateString?: string;
}
@@ -35,7 +36,11 @@ 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() {
+ return this.props.fieldKey;
}
protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
@@ -47,7 +52,6 @@ export function DocComponent<P extends DocComponentProps>() {
interface ViewBoxBaseProps {
Document: Doc;
DataDoc?: Doc;
- ContainingCollectionDoc: Opt<Doc>;
DocumentView?: () => DocumentView;
fieldKey: string;
isSelected: (outsideReaction?: boolean) => boolean;
@@ -70,19 +74,13 @@ 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() {
return this.props.fieldKey;
}
- isContentActive = (outsideReaction?: boolean) =>
- this.props.isContentActive?.() === false
- ? false
- : Doc.ActiveTool !== InkTool.None || this.props.isContentActive?.() || this.props.Document.forceActive || this.props.isSelected(outsideReaction) || this.props.rootSelected(outsideReaction)
- ? true
- : undefined;
protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
}
return Component;
@@ -120,7 +118,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
@@ -130,12 +128,10 @@ 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() {
- return this.fieldKey + (this._annotationKeySuffix() ? '-' + this._annotationKeySuffix() : '');
+ return this.fieldKey + (this._annotationKeySuffix() ? '_' + this._annotationKeySuffix() : '');
}
@action.bound
@@ -143,32 +139,25 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>()
const effectiveAcl = GetEffectiveAcl(this.dataDoc);
const indocs = doc instanceof Doc ? [doc] : doc;
const docs = indocs.filter(doc => [AclEdit, AclAdmin].includes(effectiveAcl) || GetEffectiveAcl(doc) === AclAdmin);
- if (docs.length) {
- setTimeout(() =>
- docs.map(doc => {
- // this allows 'addDocument' to see the annotationOn field in order to create a pushin
- Doc.SetInPlace(doc, 'isPushpin', undefined, true);
- doc.annotationOn === this.props.Document && Doc.SetInPlace(doc, 'annotationOn', undefined, true);
- })
- );
- const targetDataDoc = this.dataDoc;
- const value = DocListCast(targetDataDoc[annotationKey ?? this.annotationKey]);
- const toRemove = value.filter(v => docs.includes(v));
-
- if (toRemove.length !== 0) {
- const recent = Doc.MyRecentlyClosed;
- toRemove.forEach(doc => {
- leavePushpin && DocUtils.LeavePushpin(doc, annotationKey ?? this.annotationKey);
- Doc.RemoveDocFromList(targetDataDoc, annotationKey ?? this.annotationKey, doc);
- doc.context = undefined;
- if (recent) {
- Doc.RemoveDocFromList(recent, 'data', doc);
- doc.type !== DocumentType.LOADING && Doc.AddDocToList(recent, 'data', doc, undefined, true, true);
- }
- });
- this.isAnyChildContentActive() && this.props.select(false);
- return true;
- }
+
+ docs.forEach(doc => doc.annotationOn === this.props.Document && Doc.SetInPlace(doc, 'annotationOn', undefined, true));
+ const targetDataDoc = this.dataDoc;
+ const value = DocListCast(targetDataDoc[annotationKey ?? this.annotationKey]);
+ const toRemove = value.filter(v => docs.includes(v));
+
+ if (toRemove.length !== 0) {
+ const recent = Doc.MyRecentlyClosed;
+ toRemove.forEach(doc => {
+ leavePushpin && DocUtils.LeavePushpin(doc, annotationKey ?? this.annotationKey);
+ Doc.RemoveDocFromList(targetDataDoc, annotationKey ?? this.annotationKey, doc);
+ doc.embedContainer = undefined;
+ if (recent) {
+ Doc.RemoveDocFromList(recent, 'data', doc);
+ doc.type !== DocumentType.LOADING && Doc.AddDocToList(recent, 'data', doc, undefined, true, true);
+ }
+ });
+ this.isAnyChildContentActive() && this.props.select(false);
+ return true;
}
return false;
@@ -183,8 +172,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, true) && addDocument(doc, annotationKey);
}
return false;
};
@@ -194,47 +183,46 @@ 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 docList = DocListCast(targetDataDoc[annotationKey ?? this.annotationKey]);
- const added = docs.filter(d => !docList.includes(d));
+ const targetDataDoc = this.props.Document[DocData];
const effectiveAcl = GetEffectiveAcl(targetDataDoc);
+ if (effectiveAcl === AclPrivate || effectiveAcl === AclReadonly) {
+ return false;
+ }
+ const added = docs;
if (added.length) {
- if (effectiveAcl === AclPrivate || effectiveAcl === AclReadonly) {
- return false;
- } else {
- if (this.props.Document[AclSym] && Object.keys(this.props.Document[AclSym]).length) {
- added.forEach(d => {
- for (const [key, value] of Object.entries(this.props.Document[AclSym])) {
- if (d.author === denormalizeEmail(key.substring(4)) && !d.aliasOf) distributeAcls(key, SharingPermissions.Admin, d);
- }
- });
- }
+ const aclKeys = Object.keys(this.props.Document[DocAcl] ?? {});
+ 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.context = this.props.Document;
- if (annotationKey ?? this._annotationKeySuffix()) Doc.GetProto(doc).annotationOn = this.props.Document;
- Doc.AddDocToList(targetDataDoc, annotationKey ?? this.annotationKey, doc);
+ 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);
+ });
+ } 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._dragOnlyWithinContainer = undefined;
+ doc.embedContainer = this.props.Document;
+ if (annotationKey ?? this._annotationKeySuffix()) Doc.GetProto(doc).annotationOn = this.rootDoc;
+
+ Doc.ActiveDashboard && inheritParentAcls(Doc.ActiveDashboard, doc);
});
- } 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.context = this.props.Document;
- if (annotationKey ?? this._annotationKeySuffix()) Doc.GetProto(doc).annotationOn = this.props.Document;
-
- Doc.ActiveDashboard && inheritParentAcls(Doc.ActiveDashboard, doc);
- });
- const annoDocs = targetDataDoc[annotationKey ?? this.annotationKey] as List<Doc>;
- if (annoDocs instanceof List) annoDocs.push(...added);
- else targetDataDoc[annotationKey ?? this.annotationKey] = new List<Doc>(added);
- targetDataDoc[(annotationKey ?? this.annotationKey) + '-lastModified'] = new DateField(new Date(Date.now()));
- }
+ const annoDocs = targetDataDoc[annotationKey ?? this.annotationKey] as List<Doc>;
+ 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()));
}
}
return true;
diff --git a/src/client/views/DocumentButtonBar.scss b/src/client/views/DocumentButtonBar.scss
index 1e93ba5e2..1abf33cac 100644
--- a/src/client/views/DocumentButtonBar.scss
+++ b/src/client/views/DocumentButtonBar.scss
@@ -18,15 +18,62 @@ $linkGap: 3px;
cursor: pointer;
}
+.documentButtonBar-endLinkTypes,
+.documentButtonBar-linkTypes,
+.documentButtonBar-followTypes,
.documentButtonBar-pinTypes {
position: absolute;
- display: flex;
+ display: none;
width: 60px;
top: -14px;
background: black;
height: 20px;
align-items: center;
}
+.documentButtonBar-followTypes {
+ width: 20px;
+ display: none;
+}
+.documentButtonBar-followIcon {
+ align-items: center;
+ display: flex;
+ height: 100%;
+ &:hover {
+ background-color: lightblue;
+ }
+}
+.documentButtonBar-follow {
+ &:hover {
+ .documentButtonBar-followTypes {
+ display: flex;
+ }
+ }
+}
+.documentButtonBar-pin {
+ color: white;
+ &:hover {
+ .documentButtonBar-pinTypes {
+ display: flex;
+ }
+ }
+}
+.documentButtonBar-link {
+ color: white;
+ &:hover {
+ .documentButtonBar-linkTypes {
+ display: flex;
+ }
+ }
+}
+.documentButtonBar-endLink {
+ color: white;
+ &:hover {
+ .documentButtonBar-endLinkTypes {
+ display: flex;
+ }
+ }
+}
+
.documentButtonBar-pinIcon {
&:hover {
background-color: lightblue;
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index ce77b7446..35b0b22a8 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -5,25 +5,27 @@ import { action, computed, observable, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import { Doc } from '../../fields/Doc';
import { RichTextField } from '../../fields/RichTextField';
-import { Cast, NumCast } from '../../fields/Types';
-import { emptyFunction, setupMoveUpEvents, simulateMouseClick } from '../../Utils';
+import { Cast, DocCast, NumCast } from '../../fields/Types';
+import { emptyFunction, returnFalse, setupMoveUpEvents, simulateMouseClick } from '../../Utils';
import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager';
import { Pulls, Pushes } from '../apis/google_docs/GoogleApiClientUtils';
-import { Docs } from '../documents/Documents';
+import { Docs, DocUtils } from '../documents/Documents';
import { DragManager } from '../util/DragManager';
+import { IsFollowLinkScript } from '../util/LinkFollower';
import { SelectionManager } from '../util/SelectionManager';
-import { SettingsManager } from '../util/SettingsManager';
import { SharingManager } from '../util/SharingManager';
-import { undoBatch } from '../util/UndoManager';
+import { undoBatch, UndoManager } from '../util/UndoManager';
import { CollectionDockingView } from './collections/CollectionDockingView';
import { TabDocView } from './collections/TabDocView';
import './DocumentButtonBar.scss';
import { Colors } from './global/globalEnums';
+import { LinkPopup } from './linking/LinkPopup';
import { MetadataEntryMenu } from './MetadataEntryMenu';
import { DocumentLinksButton } from './nodes/DocumentLinksButton';
-import { DocumentView, DocumentViewInternal } from './nodes/DocumentView';
+import { DocumentView, DocumentViewInternal, OpenWhere, OpenWhereMod } from './nodes/DocumentView';
import { DashFieldView } from './nodes/formattedText/DashFieldView';
import { GoogleRef } from './nodes/formattedText/FormattedTextBox';
+import { PinProps } from './nodes/trails';
import { TemplateMenu } from './TemplateMenu';
import React = require('react');
const higflyout = require('@hig/flyout');
@@ -176,11 +178,11 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
e.preventDefault();
let googleDoc = await Cast(dataDoc.googleDoc, Doc);
if (!googleDoc) {
- const options = { _width: 600, _nativeWidth: 960, _nativeHeight: 800, useCors: false };
+ const options = { _width: 600, _nativeWidth: 960, _nativeHeight: 800, data_useCors: false };
googleDoc = Docs.Create.WebDocument(googleDocUrl, options);
dataDoc.googleDoc = googleDoc;
}
- CollectionDockingView.AddSplit(googleDoc, 'right');
+ CollectionDockingView.AddSplit(googleDoc, OpenWhereMod.right);
} else if (e.altKey) {
e.preventDefault();
window.open(googleDocUrl);
@@ -209,26 +211,128 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
</Tooltip>
);
}
+ @observable subFollow = '';
@computed
get followLinkButton() {
const targetDoc = this.view0?.props.Document;
+ const followBtn = (allDocs: boolean, click: (doc: Doc) => void, isSet: (doc?: Doc) => boolean, icon: IconProp) => {
+ const tooltip = `Follow ${this.subPin}documents`;
+ return !tooltip ? null : (
+ <Tooltip title={<div className="dash-tooltip">{tooltip}</div>}>
+ <div className="documentButtonBar-followIcon" style={{ backgroundColor: isSet(targetDoc) ? Colors.LIGHT_BLUE : Colors.DARK_GRAY, color: isSet(targetDoc) ? Colors.BLACK : Colors.WHITE }}>
+ <FontAwesomeIcon
+ className="documentdecorations-icon"
+ style={{ width: 20 }}
+ key={icon.toString()}
+ size="sm"
+ icon={icon}
+ onPointerEnter={action(e => (this.subPin = allDocs ? 'All ' : ''))}
+ onPointerLeave={action(e => (this.subPin = ''))}
+ onClick={e => {
+ this.props.views().forEach(dv => click(dv!.rootDoc));
+ e.stopPropagation();
+ }}
+ />
+ </div>
+ </Tooltip>
+ );
+ };
+ const followLink = IsFollowLinkScript(targetDoc?.onClick);
return !targetDoc ? null : (
- <Tooltip title={<div className="dash-tooltip">{'Set onClick to follow primary link'}</div>}>
+ <Tooltip title={<div className="dash-tooltip">Set onClick to follow primary link</div>}>
<div
- className="documentButtonBar-icon"
- style={{ backgroundColor: targetDoc.isLinkButton ? Colors.LIGHT_BLUE : Colors.DARK_GRAY, color: targetDoc.isLinkButton ? Colors.BLACK : Colors.WHITE }}
- onClick={undoBatch(e => this.props.views().map(view => view?.docView?.toggleFollowLink(undefined, undefined, false)))}>
+ className="documentButtonBar-icon documentButtonBar-follow"
+ style={{ backgroundColor: followLink ? Colors.LIGHT_BLUE : Colors.DARK_GRAY, color: followLink ? Colors.BLACK : Colors.WHITE }}
+ onClick={undoBatch(e => this.props.views().map(view => view?.docView?.toggleFollowLink(undefined, false)))}>
+ <div className="documentButtonBar-followTypes">
+ {followBtn(
+ true,
+ (doc: Doc) => (doc.followAllLinks = !doc.followAllLinks),
+ (doc?: Doc) => (doc?.followAllLinks ? true : false),
+ 'window-maximize'
+ )}
+ </div>
<FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="hand-point-right" />
</div>
</Tooltip>
);
}
- @observable expandPin = false;
+
+ @observable subLink = '';
+ @computed get linkButton() {
+ const targetDoc = this.view0?.props.Document;
+ return !targetDoc || !this.view0 ? null : (
+ <div className="documentButtonBar-icon documentButtonBar-link">
+ <div className="documentButtonBar-linkTypes">
+ <Tooltip title={<div>search for target</div>}>
+ <div className="documentButtonBar-button">
+ <button style={{ backgroundColor: 'transparent', width: 35, height: 35, display: 'flex', justifyContent: 'center', alignItems: 'center', position: 'relative' }} onPointerDown={this.toggleLinkSearch}>
+ <FontAwesomeIcon style={{ position: 'absolute', transform: 'scale(1.5)' }} icon={'search'} size="lg" />
+ <FontAwesomeIcon style={{ position: 'absolute', transform: 'scale(0.5)', transformOrigin: 'center', top: 9, left: 2 }} icon={'link'} size="lg" />
+ </button>
+ </div>
+ </Tooltip>
+ <Tooltip title={<div>open linked trail</div>}>
+ <div className="documentButtonBar-button">
+ <button style={{ backgroundColor: 'transparent', width: 35, height: 35, display: 'flex', justifyContent: 'center', alignItems: 'center', position: 'relative' }} onPointerDown={this.toggleTrail}>
+ <FontAwesomeIcon icon="taxi" size="lg" />
+ </button>
+ </div>
+ </Tooltip>
+ </div>
+ <div style={{ width: 25, height: 25 }}>
+ <DocumentLinksButton View={this.view0} AlwaysOn={true} InMenu={true} StartLink={true} />
+ </div>
+ </div>
+ );
+ }
+ @observable subEndLink = '';
+ @computed
+ get endLinkButton() {
+ const linkBtn = (pinLayout: boolean, pinContent: boolean, icon: IconProp) => {
+ const tooltip = `Finish Link and Save ${this.subEndLink} data`;
+ return !this.view0 ? null : (
+ <Tooltip title={<div className="dash-tooltip">{tooltip}</div>}>
+ <div className="documentButtonBar-pinIcon">
+ <FontAwesomeIcon
+ className="documentdecorations-icon"
+ style={{ width: 20 }}
+ key={icon.toString()}
+ size="sm"
+ icon={icon}
+ onPointerEnter={action(e => (this.subEndLink = (pinLayout ? 'Layout' : '') + (pinLayout && pinContent ? ' &' : '') + (pinContent ? ' Content' : '')))}
+ onPointerLeave={action(e => (this.subEndLink = ''))}
+ onClick={e => {
+ this.view0 &&
+ DocumentLinksButton.finishLinkClick(e.clientX, e.clientY, DocumentLinksButton.StartLink, this.view0.props.Document, true, this.view0, {
+ pinDocLayout: pinLayout,
+ pinData: !pinContent ? {} : { poslayoutview: true, dataannos: true, dataview: pinContent },
+ } as PinProps);
+
+ e.stopPropagation();
+ }}
+ />
+ </div>
+ </Tooltip>
+ );
+ };
+ return !this.view0 ? null : (
+ <div className="documentButtonBar-icon documentButtonBar-pin">
+ <div className="documentButtonBar-pinTypes">
+ {linkBtn(true, false, 'window-maximize')}
+ {linkBtn(false, true, 'address-card')}
+ {linkBtn(true, true, 'id-card')}
+ </div>
+ <DocumentLinksButton View={this.view0} AlwaysOn={true} InMenu={true} StartLink={false} />
+ </div>
+ );
+ }
+
@observable subPin = '';
@computed
get pinButton() {
const targetDoc = this.view0?.props.Document;
- const pinBtn = (pinDocLayout: boolean, pinDocContent: boolean, icon: IconProp) => {
+ const pinBtn = (pinLayoutView: boolean, pinContentView: boolean, icon: IconProp) => {
const tooltip = `Pin Document and Save ${this.subPin} to trail`;
return !tooltip ? null : (
<Tooltip title={<div className="dash-tooltip">{tooltip}</div>}>
@@ -242,10 +346,10 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
onPointerEnter={action(
e =>
(this.subPin =
- (pinDocLayout ? 'Layout' : '') +
- (pinDocLayout && pinDocContent ? ' &' : '') +
- (pinDocContent ? ' Content View' : '') +
- (pinDocLayout && pinDocContent ? '(shift+alt)' : pinDocLayout ? '(shift)' : pinDocContent ? '(alt)' : ''))
+ (pinLayoutView ? 'Layout' : '') +
+ (pinLayoutView && pinContentView ? ' &' : '') +
+ (pinContentView ? ' Content View' : '') +
+ (pinLayoutView && pinContentView ? '(shift+alt)' : pinLayoutView ? '(shift)' : pinContentView ? '(alt)' : ''))
)}
onPointerLeave={action(e => (this.subPin = ''))}
onClick={e => {
@@ -253,7 +357,13 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
.views()
.filter(v => v)
.map(dv => dv!.rootDoc);
- TabDocView.PinDoc(docs, { pinDocLayout, pinDocContent, activeFrame: Cast(docs.lastElement()?.activeFrame, 'number', null) });
+ TabDocView.PinDoc(docs, {
+ pinAudioPlay: true,
+ pinDocLayout: pinLayoutView,
+ pinData: { dataview: pinContentView },
+ activeFrame: Cast(docs.lastElement()?.activeFrame, 'number', null),
+ currentFrame: Cast(docs.lastElement()?.currentFrame, 'number', null),
+ });
e.stopPropagation();
}}
/>
@@ -264,25 +374,20 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
return !targetDoc ? null : (
<Tooltip title={<div className="dash-tooltip">{`Pin Document ${SelectionManager.Views().length > 1 ? 'multiple documents' : ''} to Trail`}</div>}>
<div
- className="documentButtonBar-icon"
- style={{ color: 'white' }}
- onPointerEnter={action(e => (this.expandPin = true))}
- onPointerLeave={action(e => (this.expandPin = false))}
+ className="documentButtonBar-icon documentButtonBar-pin"
onClick={e => {
const docs = this.props
.views()
.filter(v => v)
.map(dv => dv!.rootDoc);
- TabDocView.PinDoc(docs, { pinDocLayout: e.shiftKey, pinDocContent: e.altKey, activeFrame: Cast(docs.lastElement()?.activeFrame, 'number', null) });
+ TabDocView.PinDoc(docs, { pinAudioPlay: true, pinDocLayout: e.shiftKey, pinData: { dataview: e.altKey }, activeFrame: Cast(docs.lastElement()?.activeFrame, 'number', null) });
e.stopPropagation();
}}>
- {this.expandPin ? (
- <div className="documentButtonBar-pinTypes">
- {pinBtn(true, false, 'window-maximize')}
- {pinBtn(false, true, 'address-card')}
- {pinBtn(true, true, 'id-card')}
- </div>
- ) : null}
+ <div className="documentButtonBar-pinTypes">
+ {pinBtn(true, false, 'window-maximize')}
+ {pinBtn(false, true, 'address-card')}
+ {pinBtn(true, true, 'id-card')}
+ </div>
<FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="map-pin" />
</div>
</Tooltip>
@@ -293,12 +398,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
get shareButton() {
const targetDoc = this.view0?.props.Document;
return !targetDoc ? null : (
- <Tooltip
- title={
- <>
- <div className="dash-tooltip">{'Open Sharing Manager'}</div>
- </>
- }>
+ <Tooltip title={<div className="dash-tooltip">{'Open Sharing Manager'}</div>}>
<div className="documentButtonBar-icon" style={{ color: 'white' }} onClick={e => SharingManager.Instance.open(this.view0, targetDoc)}>
<FontAwesomeIcon className="documentdecorations-icon" icon="users" />
</div>
@@ -311,7 +411,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
const targetDoc = this.view0?.props.Document;
return !targetDoc ? null : (
<Tooltip title={<div className="dash-tooltip">{`Open Context Menu`}</div>}>
- <div className="documentButtonBar-icon" style={{ color: 'white', cursor: 'pointer' }} onClick={e => this.openContextMenu(e)}>
+ <div className="documentButtonBar-icon" style={{ color: 'white', cursor: 'pointer' }} onClick={this.openContextMenu}>
<FontAwesomeIcon className="documentdecorations-icon" icon="bars" />
</div>
</Tooltip>
@@ -321,12 +421,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
get metadataButton() {
const view0 = this.view0;
return !view0 ? null : (
- <Tooltip
- title={
- <>
- <div className="dash-tooltip">Show metadata panel</div>
- </>
- }>
+ <Tooltip title={<div className="dash-tooltip">Show metadata panel</div>}>
<div className="documentButtonBar-linkFlyout">
<Flyout
anchorPoint={anchorPoints.LEFT_TOP}
@@ -349,44 +444,46 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
}
@observable _isRecording = false;
+ _stopFunc: () => void = emptyFunction;
@computed
get recordButton() {
const targetDoc = this.view0?.props.Document;
return !targetDoc ? null : (
- <Tooltip title={<div className="dash-tooltip">{'Click to record 5 second annotation'}</div>}>
+ <Tooltip title={<div className="dash-tooltip">Press to record audio annotation</div>}>
<div
className="documentButtonBar-icon"
- style={{ backgroundColor: this._isRecording ? 'red' : targetDoc.isLinkButton ? Colors.LIGHT_BLUE : Colors.DARK_GRAY, color: targetDoc.isLinkButton ? Colors.BLACK : Colors.WHITE }}
- onClick={undoBatch(
- action(e => {
- this._isRecording = true;
- this.props.views().map(
- view =>
- view &&
- DocumentViewInternal.recordAudioAnnotation(
- view.dataDoc,
- view.LayoutFieldKey,
- action(() => (this._isRecording = false))
- )
- );
- })
- )}>
+ style={{ backgroundColor: this._isRecording ? Colors.ERROR_RED : Colors.DARK_GRAY, color: Colors.WHITE }}
+ onPointerDown={action((e: React.PointerEvent) => {
+ this._isRecording = true;
+ this.props.views().map(view => view && DocumentViewInternal.recordAudioAnnotation(view.dataDoc, view.LayoutFieldKey, stopFunc => (this._stopFunc = stopFunc), emptyFunction));
+ const b = UndoManager.StartBatch('Recording');
+ setupMoveUpEvents(
+ this,
+ e,
+ returnFalse,
+ action(() => {
+ this._isRecording = false;
+ this._stopFunc();
+ }),
+ emptyFunction
+ );
+ })}>
<FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="microphone" />
</div>
</Tooltip>
);
}
- @observable _aliasDown = false;
+ @observable _embedDown = false;
onTemplateButton = action((e: React.PointerEvent): void => {
this._tooltipOpen = false;
- setupMoveUpEvents(this, e, this.onAliasButtonMoved, emptyFunction, emptyFunction);
+ setupMoveUpEvents(this, e, this.onEmbedButtonMoved, emptyFunction, emptyFunction);
});
- onAliasButtonMoved = () => {
+ onEmbedButtonMoved = () => {
if (this._dragRef.current) {
const dragDocView = this.view0!;
const dragData = new DragManager.DocumentDragData([dragDocView.props.Document]);
const [left, top] = dragDocView.props.ScreenToLocalTransform().inverse().transformPoint(0, 0);
- dragData.defaultDropAction = 'alias';
+ dragData.defaultDropAction = 'embed';
dragData.canEmbed = true;
DragManager.StartDocumentDrag([dragDocView.ContentDiv!], dragData, left, top, { hideSource: false });
return true;
@@ -401,14 +498,14 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
const view0 = this.view0;
const views = this.props.views();
return !view0 ? null : (
- <Tooltip title={<div className="dash-tooltip">Tap to Customize Layout. Drag an embeddable alias</div>} open={this._tooltipOpen} onClose={action(() => (this._tooltipOpen = false))} placement="bottom">
+ <Tooltip title={<div className="dash-tooltip">Tap to Customize Layout. Drag an embedding</div>} open={this._tooltipOpen} onClose={action(() => (this._tooltipOpen = false))} placement="bottom">
<div className="documentButtonBar-linkFlyout" ref={this._dragRef} onPointerEnter={action(() => !this._ref.current?.getBoundingClientRect().width && (this._tooltipOpen = true))}>
<Flyout
anchorPoint={anchorPoints.LEFT_TOP}
- onOpen={action(() => (this._aliasDown = true))}
- onClose={action(() => (this._aliasDown = false))}
+ onOpen={action(() => (this._embedDown = true))}
+ onClose={action(() => (this._embedDown = false))}
content={
- !this._aliasDown ? null : (
+ !this._embedDown ? null : (
<div ref={this._ref}>
{' '}
<TemplateMenu docViews={views.filter(v => v).map(v => v as DocumentView)} />
@@ -416,7 +513,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
)
}>
<div className={'documentButtonBar-linkButton-empty'} ref={this._dragRef} onPointerDown={this.onTemplateButton}>
- {<FontAwesomeIcon className="documentdecorations-icon" icon="edit" size="sm" />}
+ <FontAwesomeIcon className="documentdecorations-icon" icon="edit" size="sm" />
</div>
</Flyout>
</div>
@@ -436,6 +533,45 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
simulateMouseClick(child, e.clientX, e.clientY - 30, e.screenX, e.screenY - 30);
};
+ @observable _showLinkPopup = false;
+ @action
+ toggleLinkSearch = (e: React.PointerEvent) => {
+ this._showLinkPopup = !this._showLinkPopup;
+ e.stopPropagation();
+ };
+
+ @observable _captureEndLinkLayout = false;
+ @action
+ captureEndLinkLayout = (e: React.PointerEvent) => {
+ this._captureEndLinkLayout = !this._captureEndLinkLayout;
+ };
+ @observable _captureEndLinkContent = false;
+ @action
+ captureEndLinkContent = (e: React.PointerEvent) => {
+ this._captureEndLinkContent = !this._captureEndLinkContent;
+ };
+
+ @action
+ captureEndLinkState = (e: React.PointerEvent) => {
+ this._captureEndLinkContent = this._captureEndLinkLayout = !this._captureEndLinkLayout;
+ };
+
+ @action
+ toggleTrail = (e: React.PointerEvent) => {
+ const rootView = this.props.views()[0];
+ const rootDoc = rootView?.rootDoc;
+ if (rootDoc) {
+ const anchor = rootView.ComponentView?.getAnchor?.(false) ?? rootDoc;
+ const trail = DocCast(anchor.presTrail) ?? Doc.MakeCopy(DocCast(Doc.UserDoc().emptyTrail), true);
+ if (trail !== anchor.presTrail) {
+ DocUtils.MakeLink(anchor, trail, { link_relationship: 'link trail' });
+ anchor.presTrail = trail;
+ }
+ Doc.ActivePresentation = trail;
+ this.props.views().lastElement()?.props.addDocTab(trail, OpenWhere.replaceRight);
+ }
+ e.stopPropagation();
+ };
render() {
if (!this.view0) return null;
@@ -448,23 +584,28 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
<div className="documentButtonBar-button">
<DocumentLinksButton View={this.view0} AlwaysOn={true} InMenu={true} ShowCount={true} />
</div>
- <div className="documentButtonBar-button">
- <DocumentLinksButton View={this.view0} AlwaysOn={true} InMenu={true} StartLink={true} />
- </div>
- {(DocumentLinksButton.StartLink || Doc.UserDoc()['documentLinksButton-fullMenu']) && DocumentLinksButton.StartLink !== doc ? (
- <div className="documentButtonBar-button">
- <DocumentLinksButton View={this.view0} AlwaysOn={true} InMenu={true} StartLink={false} />
+ {this._showLinkPopup ? (
+ <div style={{ position: 'absolute', zIndex: 1000 }}>
+ <LinkPopup
+ key="popup"
+ showPopup={this._showLinkPopup}
+ 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}
+ />
</div>
- ) : null}
- {Doc.noviceMode ? null : <div className="documentButtonBar-button">{this.recordButton}</div>}
+ ) : (
+ <div className="documentButtonBar-button">{this.linkButton}</div>
+ )}
+
+ {DocumentLinksButton.StartLink && DocumentLinksButton.StartLink !== doc ? <div className="documentButtonBar-button">{this.endLinkButton} </div> : null}
{
Doc.noviceMode ? null : <div className="documentButtonBar-button">{this.templateButton}</div>
- /*<div className="documentButtonBar-button">
- {this.metadataButton}
- </div> */
+ /*<div className="documentButtonBar-button"> {this.metadataButton} </div> */
}
{!SelectionManager.Views()?.some(v => v.allLinks.length) ? null : <div className="documentButtonBar-button">{this.followLinkButton}</div>}
<div className="documentButtonBar-button">{this.pinButton}</div>
+ <div className="documentButtonBar-button">{this.recordButton}</div>
{!Doc.UserDoc()['documentLinksButton-fullMenu'] ? null : <div className="documentButtonBar-button">{this.shareButton}</div>}
{!Doc.UserDoc()['documentLinksButton-fullMenu'] ? null : (
<div className="documentButtonBar-button" style={{ display: !considerPush ? 'none' : '' }}>
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 47347284c..41ce08d3c 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -6,19 +6,22 @@ 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 } from '../../fields/Doc';
+import { AclAdmin, 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 { emptyFunction, numberValue, returnFalse, setupMoveUpEvents, Utils } from '../../Utils';
+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,10 +30,11 @@ import { Colors } from './global/globalEnums';
import { InkingStroke } from './InkingStroke';
import { InkStrokeProperties } from './InkStrokeProperties';
import { LightboxView } from './LightboxView';
-import { DocumentView } 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 _ = require('lodash');
@observer
export class DocumentDecorations extends React.Component<{ PanelWidth: number; PanelHeight: number; boundsLeft: number; boundsTop: number }, { value: string }> {
@@ -66,12 +70,25 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
DocumentDecorations.Instance = this;
reaction(
() => SelectionManager.Views().slice(),
- action(docs => (this._editingTitle = false))
+ action(docs => {
+ this._showNothing = !DocumentView.LongPress && docs.length === 1; // show decorations if multiple docs are selected or we're long pressing
+ this._editingTitle = false;
+ })
+ );
+ document.addEventListener(
+ // show decorations whenever pointer moves outside of selection bounds.
+ 'pointermove',
+ action(e => {
+ if (this.Bounds.x !== Number.MAX_VALUE && (this.Bounds.x > e.clientX || this.Bounds.r < e.clientX || this.Bounds.y > e.clientY || this.Bounds.b < e.clientY)) {
+ this._showNothing = false;
+ }
+ })
);
}
@computed
get Bounds() {
+ if (LinkFollower.IsFollowing) return { x: 0, y: 0, r: 0, b: 0 };
const views = SelectionManager.Views();
return views
.filter(dv => dv.props.renderDepth > 0)
@@ -103,7 +120,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
titleFieldKey &&
SelectionManager.Views().forEach(d => {
if (titleFieldKey === 'title') {
- d.dataDoc['title-custom'] = !this._accumulatedTitle.startsWith('-');
+ d.dataDoc.title_custom = !this._accumulatedTitle.startsWith('-');
if (StrCast(d.rootDoc.title).startsWith('@') && !this._accumulatedTitle.startsWith('@')) {
Doc.RemoveDocFromList(Doc.MyPublishedDocs, undefined, d.rootDoc);
}
@@ -113,22 +130,26 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
}
//@ts-ignore
const titleField = +this._accumulatedTitle == this._accumulatedTitle ? +this._accumulatedTitle : this._accumulatedTitle;
- Doc.SetInPlace(d.rootDoc, titleFieldKey, titleField, true);
- if (d.rootDoc.syncLayoutFieldWithTitle) {
- const title = titleField.toString();
+ if (titleField.toString().startsWith('<this>')) {
+ const title = titleField.toString().replace(/<this>\.?/, '');
const curKey = Doc.LayoutFieldKey(d.rootDoc);
- if (curKey !== title && d.dataDoc[title] === undefined) {
- d.rootDoc.layout = FormattedTextBox.LayoutString(title);
- setTimeout(() => {
- const val = d.dataDoc[curKey];
- d.dataDoc[curKey] = undefined;
- d.dataDoc[title] = val;
- });
+ if (curKey !== title) {
+ if (title) {
+ if (d.dataDoc[title] === undefined || d.dataDoc[title] instanceof RichTextField || typeof d.dataDoc[title] === 'string') {
+ d.rootDoc.layout_fieldKey = `layout_${title}`;
+ d.rootDoc[`layout_${title}`] = FormattedTextBox.LayoutString(title);
+ d.rootDoc[`${title}_nativeWidth`] = d.rootDoc[`${title}_nativeHeight`] = 0;
+ }
+ } else {
+ d.rootDoc.layout_fieldKey = undefined;
+ }
}
+ } else {
+ Doc.SetInPlace(d.rootDoc, titleFieldKey, titleField, true);
}
}),
- 'title blur'
+ 'edit title'
);
}
};
@@ -169,7 +190,9 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
@action
onBackgroundMove = (dragTitle: boolean, e: PointerEvent): boolean => {
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),
@@ -180,7 +203,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
dragData.removeDocument = dragDocView.props.removeDocument;
dragData.isDocDecorationMove = true;
dragData.canEmbed = dragTitle;
- this._hidden = this.Interacting = true;
+ this._hidden = true;
DragManager.StartDocumentDrag(
SelectionManager.Views().map(dv => dv.ContentDiv!),
dragData,
@@ -189,7 +212,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
{
dragComplete: action(e => {
dragData.canEmbed && SelectionManager.DeselectAll();
- this._hidden = this.Interacting = false;
+ this._hidden = false;
}),
hideSource: true,
}
@@ -205,11 +228,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) {
@@ -228,6 +246,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));
};
@@ -250,28 +274,28 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
const selectedDocs = SelectionManager.Views();
if (selectedDocs.length) {
if (e.ctrlKey) {
- // open an alias in a new tab with Ctrl Key
- CollectionDockingView.AddSplit(Doc.BestAlias(selectedDocs[0].props.Document), 'right');
+ // open an embedding in a new tab with Ctrl Key
+ CollectionDockingView.AddSplit(Doc.BestEmbedding(selectedDocs[0].rootDoc), OpenWhereMod.right);
} else if (e.shiftKey) {
// open centered in a new workspace with Shift Key
- const alias = Doc.MakeAlias(selectedDocs[0].props.Document);
- alias.context = undefined;
- alias.x = -alias[WidthSym]() / 2;
- alias.y = -alias[HeightSym]() / 2;
- CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([alias], { title: 'Tab for ' + alias.title }), 'right');
+ const embedding = Doc.MakeEmbedding(selectedDocs[0].rootDoc);
+ embedding.embedContainer = undefined;
+ 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, 'right');
+ CollectionDockingView.ToggleSplit(selectedDocs[0].rootDoc, OpenWhereMod.right);
} else {
- var openDoc = selectedDocs[0].props.Document;
- if (openDoc.layoutKey === 'layout_icon') {
- openDoc = DocListCast(openDoc.aliases).find(alias => !alias.context) ?? Doc.MakeAlias(openDoc);
+ 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);
}
LightboxView.SetLightboxDoc(
openDoc,
undefined,
- selectedDocs.slice(1).map(view => view.props.Document)
+ selectedDocs.slice(1).map(view => view.rootDoc)
);
}
}
@@ -283,15 +307,14 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
SelectionManager.DeselectAll();
};
- onSelectorClick = () => SelectionManager.Views()?.[0]?.props.ContainingCollectionView?.props.select(false);
-
+ onSelectorClick = () => SelectionManager.Views()?.[0]?.props.docViewPath?.().lastElement()?.select(false);
/**
* Handles setting up events when user clicks on the border radius editor
* @param e PointerEvent
*/
@action
onRadiusDown = (e: React.PointerEvent): void => {
- this._isRounding = true;
+ this._isRounding = DocumentDecorations.Instance.Interacting = true;
this._resizeUndo = UndoManager.StartBatch('DocDecs set radius');
// Call util move event function
setupMoveUpEvents(
@@ -309,15 +332,16 @@ 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
action(e => {
- this._isRounding = false;
+ DocumentDecorations.Instance.Interacting = this._isRounding = false;
this._resizeUndo?.end();
}), // upEvent
- e => {} // clickEvent
+ e => {}, // clickEvent,
+ true
);
};
@@ -346,8 +370,8 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
const newloccentern = seldocview.props.ScreenToLocalTransform().transformPoint(rotCenter[0], rotCenter[1]);
const newlocenter = [newloccentern[0] - NumCast(seldocview.layoutDoc._width) / 2, newloccentern[1] - NumCast(seldocview.layoutDoc._height) / 2];
const final = Utils.rotPt(newlocenter[0], newlocenter[1], -(NumCast(seldocview.rootDoc._rotation) / 180) * Math.PI);
- seldocview.rootDoc.rotateCenterX = final.x / NumCast(seldocview.layoutDoc._width);
- seldocview.rootDoc.rotateCenterY = final.y / NumCast(seldocview.layoutDoc._height);
+ seldocview.rootDoc.rotation_centerX = final.x / NumCast(seldocview.layoutDoc._width);
+ seldocview.rootDoc.rotation_centerY = final.y / NumCast(seldocview.layoutDoc._height);
};
@action
@@ -364,8 +388,8 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
action(action(() => (this._isRotating = false))), // upEvent
action((e, doubleTap) => {
if (doubleTap) {
- seldocview.rootDoc.rotateCenterX = 0.5;
- seldocview.rootDoc.rotateCenterY = 0.5;
+ seldocview.rootDoc.rotation_centerX = 0.5;
+ seldocview.rootDoc.rotation_centerY = 0.5;
}
})
);
@@ -375,7 +399,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 }>();
@@ -437,24 +461,13 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
@action
onPointerDown = (e: React.PointerEvent): void => {
- const views = SelectionManager.Views().map(dv => dv.rootDoc);
- this._inkDragDocs = views
- .filter(doc => doc.type === DocumentType.INK)
- .map(doc => {
- if (InkStrokeProperties.Instance._lock) {
- Doc.SetNativeHeight(doc, NumCast(doc._height));
- Doc.SetNativeWidth(doc, NumCast(doc._width));
- }
- return { doc, x: NumCast(doc.x), y: NumCast(doc.y), width: NumCast(doc._width), height: NumCast(doc._height) };
- });
-
setupMoveUpEvents(this, e, this.onPointerMove, this.onPointerUp, emptyFunction);
this.Interacting = true; // turns off pointer events on things like youtube videos and web pages so that dragging doesn't get "stuck" when cursor moves over them
this._resizeHdlId = e.currentTarget.className;
const bounds = e.currentTarget.getBoundingClientRect();
this._offX = this._resizeHdlId.toLowerCase().includes('left') ? bounds.right - e.clientX : bounds.left - e.clientX;
this._offY = this._resizeHdlId.toLowerCase().includes('top') ? bounds.bottom - e.clientY : bounds.top - e.clientY;
- this._resizeUndo = UndoManager.StartBatch('DocDecs resize');
+ this._resizeUndo = UndoManager.StartBatch('drag resizing');
this._snapX = e.pageX;
this._snapY = e.pageY;
const ffviewSet = new Set<CollectionFreeFormView>();
@@ -471,10 +484,6 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
if (!first) return false;
let thisPt = { x: e.clientX - this._offX, y: e.clientY - this._offY };
var fixedAspect = Doc.NativeAspect(first.layoutDoc);
- InkStrokeProperties.Instance._lock &&
- SelectionManager.Views()
- .filter(dv => dv.rootDoc.type === DocumentType.INK)
- .forEach(dv => (fixedAspect = Doc.NativeAspect(dv.rootDoc)));
const resizeHdl = this._resizeHdlId.split(' ')[0];
if (fixedAspect && (resizeHdl === 'documentDecorations-bottomRightResizer' || resizeHdl === 'documentDecorations-topLeftResizer')) {
@@ -503,73 +512,86 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
dragRight = false,
dragBotRight = false,
dragTop = false;
- let dX = 0,
- dY = 0,
- dW = 0,
- dH = 0;
+ let dXin = 0,
+ dYin = 0,
+ dWin = 0,
+ dHin = 0;
switch (this._resizeHdlId.split(' ')[0]) {
case '':
break;
case 'documentDecorations-topLeftResizer':
- dX = -1;
- dY = -1;
- dW = -move[0];
- dH = -move[1];
+ dXin = -1;
+ dYin = -1;
+ dWin = -move[0];
+ dHin = -move[1];
break;
case 'documentDecorations-topRightResizer':
- dW = move[0];
- dY = -1;
- dH = -move[1];
+ dWin = move[0];
+ dYin = -1;
+ dHin = -move[1];
break;
case 'documentDecorations-topResizer':
- dY = -1;
- dH = -move[1];
+ dYin = -1;
+ dHin = -move[1];
dragTop = true;
break;
case 'documentDecorations-bottomLeftResizer':
- dX = -1;
- dW = -move[0];
- dH = move[1];
+ dXin = -1;
+ dWin = -move[0];
+ dHin = move[1];
break;
case 'documentDecorations-bottomRightResizer':
- dW = move[0];
- dH = move[1];
+ dWin = move[0];
+ dHin = move[1];
dragBotRight = true;
break;
case 'documentDecorations-bottomResizer':
- dH = move[1];
+ dHin = move[1];
dragBottom = true;
break;
case 'documentDecorations-leftResizer':
- dX = -1;
- dW = -move[0];
+ dXin = -1;
+ dWin = -move[0];
break;
case 'documentDecorations-rightResizer':
- dW = move[0];
+ dWin = move[0];
dragRight = true;
break;
}
- SelectionManager.Views().forEach(
+ const isGroup = first.rootDoc._isGroup ? first.rootDoc : undefined;
+ const scaleViews = isGroup ? DocListCast(isGroup.data).map(doc => DocumentManager.Instance.getFirstDocumentView(doc)!) : SelectionManager.Views();
+ const aggBounds = aggregateBounds(scaleViews.map(view => view.rootDoc) as any, 0, 0);
+ const refWidth = aggBounds.r - aggBounds.x;
+ const refHeight = aggBounds.b - aggBounds.y;
+ const scaleRefPt = first.props
+ .ScreenToLocalTransform()
+ .inverse()
+ .transformPoint(
+ NumCast(isGroup?._xPadding) + (dXin ? refWidth : 0), //
+ NumCast(isGroup?._yPadding) + (dYin ? refHeight : 0)
+ );
+ scaleViews.forEach(
action((docView: DocumentView) => {
if (e.ctrlKey && !Doc.NativeHeight(docView.props.Document)) docView.toggleNativeDimensions();
- if (dX !== 0 || dY !== 0 || dW !== 0 || dH !== 0) {
- const doc = Document(docView.rootDoc);
- if (doc.nativeHeightUnfrozen && !NumCast(doc.nativeHeight)) {
+ if (dXin !== 0 || dYin !== 0 || dWin !== 0 || dHin !== 0) {
+ const doc = docView.rootDoc;
+ const refCent = docView.props.ScreenToLocalTransform().transformPoint(scaleRefPt[0], scaleRefPt[1]);
+
+ if (doc.nativeHeightUnfrozen && !NumCast(doc.nativeHeight) && doc._nativeWidth !== undefined) {
doc._nativeHeight = (NumCast(doc._height) / NumCast(doc._width, 1)) * docView.nativeWidth;
}
const nwidth = docView.nativeWidth;
const nheight = docView.nativeHeight;
- let docheight = doc._height || 0;
- let docwidth = doc._width || 0;
- const width = docwidth;
- let height = docheight || (nheight / nwidth) * width;
- height = !height || isNaN(height) ? 20 : height;
+ const docwidth = NumCast(doc._width);
+ let docheight = (hgt => (!hgt || isNaN(hgt) ? 20 : hgt))(NumCast(doc._height) || (nheight / nwidth) * docwidth);
+ let dW = docwidth * (dWin / refWidth);
+ let dH = docheight * (dHin / refHeight);
const scale = docView.props.ScreenToLocalTransform().Scale;
- const modifyNativeDim = (e.ctrlKey || doc.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 !== width / height && !dragBottom && !dragTop) {
- height = (nheight / nwidth) * width;
+ if (nwidth / nheight !== docwidth / docheight && !dragBottom && !dragTop) {
+ docheight = (nheight / nwidth) * docwidth;
}
if (modifyNativeDim && !dragBottom && !dragTop) {
// ctrl key enables modification of the nativeWidth or nativeHeight durin the interaction
@@ -577,55 +599,64 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
else dW = (dH * nwidth) / nheight;
}
}
- let actualdW = Math.max(width + dW * scale, 20);
- let actualdH = Math.max(height + dH * scale, 20);
- const fixedAspect = nwidth && nheight && (!doc._fitWidth || e.ctrlKey || doc.nativeHeightUnfrozen || doc.nativeDimModifiable);
+ 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 && !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) {
if (dragRight && modifyNativeDim) {
if (Doc.NativeWidth(doc)) {
- doc._nativeWidth = (actualdW / (doc._width || 1)) * Doc.NativeWidth(doc);
+ doc._nativeWidth = (actualdW / (docwidth || 1)) * Doc.NativeWidth(doc);
}
} else {
- if (!doc._fitWidth) {
+ if (!doc._layout_fitWidth || preserveNativeDim) {
actualdH = (nheight / nwidth) * actualdW;
doc._height = actualdH;
- } else if (!modifyNativeDim || dragBotRight) doc._height = actualdH;
+ } else if (!modifyNativeDim || dragBotRight) {
+ doc._height = actualdH;
+ }
}
doc._width = actualdW;
} else {
- if ((dragBottom || dragTop) && (modifyNativeDim || (docView.layoutDoc.nativeHeightUnfrozen && docView.layoutDoc._fitWidth))) {
- // frozen web pages, PDFs, and some RTFS have frozen nativewidth/height. But they are marked to allow their nativeHeight to be explicitly modified with fitWidth and vertical resizing. (ie, with fitWidth they can't grow horizontally to match a vertical resize so it makes more sense to change their nativeheight even if the ctrl key isn't used)
- doc._nativeHeight = (actualdH / (doc._height || 1)) * Doc.NativeHeight(doc);
- doc._autoHeight = false;
+ if ((dragBottom || dragTop) && (modifyNativeDim || (docView.layoutDoc.nativeHeightUnfrozen && docView.layoutDoc._layout_fitWidth))) {
+ // frozen web pages, PDFs, and some RTFS have frozen nativewidth/height. But they are marked to allow their nativeHeight
+ // to be explicitly modified with fitWidth and vertical resizing. (ie, with fitWidth they can't grow horizontally to match
+ // a vertical resize so it makes more sense to change their nativeheight even if the ctrl key isn't used)
+ doc._nativeHeight = (actualdH / (docheight || 1)) * Doc.NativeHeight(doc);
+ doc._layout_autoHeight = false;
} else {
- if (!doc._fitWidth) {
+ if (!doc._layout_fitWidth || preserveNativeDim) {
actualdW = (nwidth / nheight) * actualdH;
doc._width = actualdW;
- } else if (!modifyNativeDim || dragBotRight) doc._width = actualdW;
+ } else if (!modifyNativeDim || dragBotRight) {
+ doc._width = actualdW;
+ }
}
if (!modifyNativeDim) {
- actualdH = Math.min((nheight / nwidth) * NumCast(doc._width), actualdH);
- doc._height = actualdH;
- } else doc._height = actualdH;
+ actualdH = (nheight / nwidth) * NumCast(doc._width); //, actualdH);
+ }
+ doc._height = actualdH;
}
} else {
- const rotCtr = [NumCast(doc._width) / 2, NumCast(doc._height) / 2];
+ const rotCtr = [docwidth / 2, docheight / 2];
const tlRotated = Utils.rotPt(-rotCtr[0], -rotCtr[1], (NumCast(doc._rotation) / 180) * Math.PI);
- const maxHeight = doc.nativHeightUnfrozen || !nheight ? 0 : Math.max(nheight, NumCast(doc.scrollHeight, NumCast(doc[docView.LayoutFieldKey + '-scrollHeight']))) * docView.NativeDimScaling();
+ const maxHeight = doc.nativeHeightUnfrozen || !nheight ? 0 : Math.max(nheight, NumCast(doc.scrollHeight, NumCast(doc[docView.LayoutFieldKey + '_scrollHeight']))) * docView.NativeDimScaling();
dH && (doc._height = actualdH > maxHeight && maxHeight ? maxHeight : actualdH);
dW && (doc._width = actualdW);
- dH && (doc._autoHeight = false);
+ dH && (doc._layout_autoHeight = false);
const rotCtr2 = [NumCast(doc._width) / 2, NumCast(doc._height) / 2];
const tlRotated2 = Utils.rotPt(-rotCtr2[0], -rotCtr2[1], (NumCast(doc._rotation) / 180) * Math.PI);
doc.x = NumCast(doc.x) + tlRotated.x + rotCtr[0] - (tlRotated2.x + rotCtr2[0]); // doc shifts by amount topleft moves because rotation is about center of doc
doc.y = NumCast(doc.y) + tlRotated.y + rotCtr[1] - (tlRotated2.y + rotCtr2[1]);
}
- doc.x = (doc.x || 0) + dX * (actualdW - docwidth);
- doc.y = (doc.y || 0) + (dragBottom ? 0 : dY * (actualdH - docheight));
- doc._lastModified = new DateField();
+ doc.x = NumCast(doc.x) + dX;
+ doc.y = NumCast(doc.y) + dY;
+ doc._layout_modificationDate = new DateField();
}
const val = this._dragHeights.get(docView.layoutDoc);
if (val) this._dragHeights.set(docView.layoutDoc, { start: val.start, lowest: Math.min(val.lowest, NumCast(docView.layoutDoc._height)) });
@@ -641,11 +672,11 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
this._resizeUndo?.end();
SnappingManager.clearSnapLines();
- // detect autoHeight gesture and apply
+ // detect layout_autoHeight gesture and apply
SelectionManager.Views()
.map(docView => ({ doc: docView.layoutDoc, hgts: this._dragHeights.get(docView.layoutDoc) }))
.filter(pair => pair.hgts && pair.hgts.lowest < pair.hgts.start && pair.hgts.lowest <= 20)
- .forEach(pair => (pair.doc._autoHeight = true));
+ .forEach(pair => (pair.doc._layout_autoHeight = true));
//need to change points for resize, or else rotation/control points will fail.
this._inkDragDocs
.map(oldbds => ({ oldbds, inkPts: Cast(oldbds.doc.data, InkField)?.inkData || [] }))
@@ -684,7 +715,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
}
@computed get hasIcons() {
- return SelectionManager.Views().some(docView => docView.rootDoc.layoutKey === 'layout_icon');
+ return SelectionManager.Views().some(docView => docView.rootDoc.layout_fieldKey === 'layout_icon');
}
@observable _showRotCenter = false;
@@ -693,8 +724,8 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
if (SelectionManager.Views().length) {
const seldocview = SelectionManager.Views()[0];
const loccenter = Utils.rotPt(
- NumCast(seldocview.rootDoc.rotateCenterX) * NumCast(seldocview.layoutDoc._width),
- NumCast(seldocview.rootDoc.rotateCenterY) * NumCast(seldocview.layoutDoc._height),
+ NumCast(seldocview.rootDoc.rotation_centerX) * NumCast(seldocview.layoutDoc._width),
+ NumCast(seldocview.rootDoc.rotation_centerY) * NumCast(seldocview.layoutDoc._height),
(NumCast(seldocview.rootDoc._rotation) / 180) * Math.PI
);
return seldocview.props
@@ -705,50 +736,43 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
return this._rotCenter;
}
+ @observable _showNothing = true;
+
render() {
- const { b, c, r, x, y } = this.Bounds;
- const bounds = { b, c, r, x, y };
- const seldocview = SelectionManager.Views().slice(-1)[0];
+ const { b, r, x, y } = this.Bounds;
+ const bounds = { b, r, x, y };
+ const seldocview = SelectionManager.Views().lastElement();
if (SnappingManager.GetIsDragging() || bounds.r - bounds.x < 1 || bounds.x === Number.MAX_VALUE || !seldocview || this._hidden || isNaN(bounds.r) || isNaN(bounds.b) || isNaN(bounds.x) || isNaN(bounds.y)) {
+ setTimeout(action(() => (this._showNothing = true)));
return null;
}
// hide the decorations if the parent chooses to hide it or if the document itself hides it
- const hideResizers = seldocview.props.hideResizeHandles || seldocview.rootDoc.hideResizeHandles || seldocview.rootDoc._isGroup || this._isRounding || this._isRotating;
- const hideTitle = seldocview.props.hideDecorationTitle || seldocview.rootDoc.hideDecorationTitle || this._isRounding || this._isRotating;
- const hideDocumentButtonBar = seldocview.props.hideDocumentButtonBar || seldocview.rootDoc.hideDocumentButtonBar || this._isRounding || this._isRotating;
+ const hideDecorations = seldocview.props.hideDecorations || seldocview.rootDoc.hideDecorations;
+ const hideResizers = 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.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 =
+ hideDecorations ||
this._isRounding ||
this._isRotating ||
seldocview.props.hideDeleteButton ||
seldocview.rootDoc.hideDeleteButton ||
SelectionManager.Views().some(docView => {
- const collectionAcl = docView.props.ContainingCollectionView ? GetEffectiveAcl(docView.props.ContainingCollectionDoc?.[DataSym]) : AclEdit;
- return docView.rootDoc.stayInCollection || (collectionAcl !== AclAdmin && collectionAcl !== AclEdit && GetEffectiveAcl(docView.rootDoc) !== AclAdmin);
+ const 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>
@@ -765,14 +789,14 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
bounds.b = Math.max(bounds.y, Math.max(topBounds, Math.min(window.innerHeight, bounds.b + this._resizeBorderWidth / 2 + this._linkBoxHeight) - this._resizeBorderWidth / 2 - this._linkBoxHeight));
const useLock = bounds.r - bounds.x > 135 && seldocview.props.CollectionFreeFormDocumentView;
- const useRotation = seldocview.rootDoc.type !== DocumentType.EQUATION; // when do we want an object to not rotate?
- const rotation = NumCast(seldocview.rootDoc._rotation);
+ const useRotation = !hideResizers && seldocview.rootDoc.type !== DocumentType.EQUATION && seldocview.props.CollectionFreeFormDocumentView; // when do we want an object to not rotate?
+ const rotation = SelectionManager.Views().length == 1 ? NumCast(seldocview.rootDoc._rotation) : 0;
const resizerScheme = colorScheme ? 'documentDecorations-resizer' + colorScheme : '';
// Radius constants
- const useRounding = seldocview.ComponentView instanceof ImageBox || seldocview.ComponentView instanceof FormattedTextBox;
- const borderRadius = numberValue(StrCast(seldocview.rootDoc.borderRounding));
+ const useRounding = seldocview.ComponentView instanceof ImageBox || seldocview.ComponentView instanceof FormattedTextBox || seldocview.ComponentView instanceof CollectionFreeFormView;
+ 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;
@@ -804,7 +828,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
</div>
);
return (
- <div className={`documentDecorations${colorScheme}`}>
+ <div className={`documentDecorations${colorScheme}`} style={{ opacity: this._showNothing ? 0.1 : undefined }}>
<div
className="documentDecorations-background"
style={{
@@ -813,7 +837,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
left: bounds.x - this._resizeBorderWidth / 2,
top: bounds.y - this._resizeBorderWidth / 2,
pointerEvents: DocumentDecorations.Instance.AddToSelection || this.Interacting ? 'none' : 'all',
- display: SelectionManager.Views().length <= 1 ? 'none' : undefined,
+ display: SelectionManager.Views().length <= 1 || hideDecorations ? 'none' : undefined,
}}
onPointerDown={this.onBackgroundDown}
onContextMenu={e => {
@@ -832,11 +856,11 @@ 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" onPointerDown={this.onContainerDown}>
- {hideDeleteButton ? <div /> : topBtn('close', 'times', undefined, e => this.onCloseClick(true), 'Close')}
- {hideResizers || hideDeleteButton ? <div /> : topBtn('minimize', 'window-maximize', undefined, e => this.onCloseClick(undefined), 'Minimize')}
- {titleArea}
- {hideOpenButton ? <div /> : topBtn('open', 'external-link-alt', this.onMaximizeDown, undefined, 'Open in Lightbox (ctrl: as alias, shift: in new collection)')}
+ <div className="documentDecorations-topbar" style={{ 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)')}
</div>
{hideResizers ? null : (
<>
@@ -850,7 +874,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
<div key="b" className={`documentDecorations-bottomResizer ${resizerScheme}`} onPointerDown={this.onPointerDown} onContextMenu={e => e.preventDefault()} />
<div key="br" className={`documentDecorations-bottomRightResizer ${resizerScheme}`} onPointerDown={this.onPointerDown} onContextMenu={e => e.preventDefault()} />
- {seldocview.props.renderDepth <= 1 || !seldocview.props.ContainingCollectionView ? null : topBtn('selector', 'arrow-alt-circle-up', undefined, this.onSelectorClick, 'tap to select containing document')}
+ {seldocview.props.renderDepth <= 1 || !seldocview.props.docViewPath().lastElement() ? null : topBtn('selector', 'arrow-alt-circle-up', undefined, this.onSelectorClick, 'tap to select containing document')}
</>
)}
{useRounding && (
@@ -891,9 +915,11 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
pointerEvents: 'none',
}}>
{this._isRotating ? null : (
- <div className="documentDecorations-rotation" style={{ pointerEvents: 'all' }} onPointerDown={this.onRotateDown} onContextMenu={e => e.preventDefault()}>
- <IconButton icon={<FaUndo />} isCircle={true} hoverStyle={'lighten'} backgroundColor={Colors.DARK_GRAY} color={Colors.LIGHT_GRAY} />
- </div>
+ <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()}>
+ <IconButton icon={<FaUndo />} color={Colors.LIGHT_GRAY} />
+ </div>
+ </Tooltip>
)}
</div>
{!this._showRotCenter ? null : (
diff --git a/src/client/views/EditableView.scss b/src/client/views/EditableView.scss
index ed7ec9dc1..0955ba8ff 100644
--- a/src/client/views/EditableView.scss
+++ b/src/client/views/EditableView.scss
@@ -10,12 +10,14 @@
.editableView-container-editing-oneLine {
width: 100%;
+ height: max-content;
span {
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- display: block;
+ p {
+ white-space: pre;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
}
input {
@@ -33,4 +35,3 @@
border: none;
outline: none;
}
- \ No newline at end of file
diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx
index 8036df471..7043edcee 100644
--- a/src/client/views/EditableView.tsx
+++ b/src/client/views/EditableView.tsx
@@ -1,9 +1,11 @@
import React = require('react');
-import { action, observable } from 'mobx';
+import { action, IReactionDisposer, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as Autosuggest from 'react-autosuggest';
import { ObjectField } from '../../fields/ObjectField';
import './EditableView.scss';
+import { DocumentIconContainer } from './nodes/DocumentIcon';
+import { OverlayView } from './OverlayView';
export interface EditableProps {
/**
@@ -37,15 +39,16 @@ export interface EditableProps {
onChange: (e: React.ChangeEvent, { newValue }: { newValue: string }) => void;
autosuggestProps: Autosuggest.AutosuggestProps<string, any>;
};
- oneLine?: boolean;
+ oneLine?: boolean; // whether to display the editable view as a single input line or as a textarea
+ allowCRs?: boolean; // can carriage returns be entered
editing?: boolean;
isEditingCallback?: (isEditing: boolean) => void;
menuCallback?: (x: number, y: number) => void;
textCallback?: (char: string) => boolean;
showMenuOnLoad?: boolean;
- toggle?: () => void;
background?: string | undefined;
placeholder?: string;
+ wrap?: string; // nowrap, pre-wrap, etc
}
/**
@@ -56,7 +59,9 @@ export interface EditableProps {
@observer
export class EditableView extends React.Component<EditableProps> {
private _ref = React.createRef<HTMLDivElement>();
- private _inputref = React.createRef<HTMLInputElement>();
+ private _inputref: HTMLInputElement | HTMLTextAreaElement | null = null;
+ _overlayDisposer?: () => void;
+ _editingDisposer?: IReactionDisposer;
@observable _editing: boolean = false;
constructor(props: EditableProps) {
@@ -64,21 +69,53 @@ export class EditableView extends React.Component<EditableProps> {
this._editing = this.props.editing ? true : false;
}
+ componentDidMount(): void {
+ this._editingDisposer = reaction(
+ () => this._editing,
+ editing => {
+ if (editing) {
+ setTimeout(() => {
+ if (this._inputref?.value.startsWith('=') || this._inputref?.value.startsWith(':=')) {
+ this._overlayDisposer?.();
+ this._overlayDisposer = OverlayView.Instance.addElement(<DocumentIconContainer />, { x: 0, y: 0 });
+ }
+ });
+ } else {
+ this._overlayDisposer?.();
+ this._overlayDisposer = undefined;
+ }
+ },
+ { fireImmediately: true }
+ );
+ }
+
@action
componentDidUpdate() {
if (this._editing && this.props.editing === false) {
- this._inputref.current?.value && this.finalizeEdit(this._inputref.current.value, false, true, false);
+ this._inputref?.value && this.finalizeEdit(this._inputref.value, false, true, false);
} else if (this.props.editing !== undefined) {
this._editing = this.props.editing;
}
}
componentWillUnmount() {
- this._inputref.current?.value && this.finalizeEdit(this._inputref.current.value, false, true, false);
+ this._overlayDisposer?.();
+ this._editingDisposer?.();
+ this._inputref?.value && this.finalizeEdit(this._inputref.value, false, true, false);
}
+ onChange = (e: React.ChangeEvent) => {
+ const targVal = (e.target as any).value;
+ if (!(targVal.startsWith(':=') || targVal.startsWith('='))) {
+ this._overlayDisposer?.();
+ this._overlayDisposer = undefined;
+ } else if (!this._overlayDisposer) {
+ this._overlayDisposer = OverlayView.Instance.addElement(<DocumentIconContainer />, { x: 0, y: 0 });
+ }
+ };
+
@action
- onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
+ onKeyDown = (e: React.KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => {
switch (e.key) {
case 'Tab':
e.stopPropagation();
@@ -90,13 +127,15 @@ export class EditableView extends React.Component<EditableProps> {
if (!e.currentTarget.value) this.props.OnEmpty?.();
break;
case 'Enter':
- e.stopPropagation();
- if (!e.ctrlKey) {
- this.finalizeEdit(e.currentTarget.value, e.shiftKey, false, true);
- } else if (this.props.OnFillDown) {
- this.props.OnFillDown(e.currentTarget.value);
- this._editing = false;
- this.props.isEditingCallback?.(false);
+ if (this.props.allowCRs !== true) {
+ e.stopPropagation();
+ if (!e.ctrlKey) {
+ this.finalizeEdit(e.currentTarget.value, e.shiftKey, false, true);
+ } else if (this.props.OnFillDown) {
+ this.props.OnFillDown(e.currentTarget.value);
+ this._editing = false;
+ this.props.isEditingCallback?.(false);
+ }
}
break;
case 'Escape':
@@ -104,16 +143,27 @@ export class EditableView extends React.Component<EditableProps> {
this._editing = false;
this.props.isEditingCallback?.(false);
break;
- case ':':
- this.props.menuCallback?.(e.currentTarget.getBoundingClientRect().x, e.currentTarget.getBoundingClientRect().y);
+ case 'ArrowUp':
+ case 'ArrowDown':
+ case 'ArrowLeft':
+ case 'ArrowRight':
+ e.stopPropagation();
break;
case 'Shift':
case 'Alt':
case 'Meta':
case 'Control':
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);
}
@@ -130,7 +180,7 @@ export class EditableView extends React.Component<EditableProps> {
this._editing = true;
this.props.isEditingCallback?.(true);
}
- e.stopPropagation();
+ // e.stopPropagation();
}
};
@@ -181,15 +231,32 @@ 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={this._inputref}
+ ref={r => (this._inputref = r)}
style={{ display: this.props.display, overflow: 'auto', fontSize: this.props.fontSize, 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}
@@ -215,8 +282,8 @@ export class EditableView extends React.Component<EditableProps> {
<div
className={`editableView-container-editing${this.props.oneLine ? '-oneLine' : ''}`}
ref={this._ref}
- style={{ display: this.props.display, textOverflow: this.props.overflow, minHeight: '10px', whiteSpace: 'nowrap', height: this.props.height || 'auto', maxHeight: this.props.maxHeight }}
- onPointerDown={e => e.stopPropagation()}
+ style={{ display: this.props.display, textOverflow: this.props.overflow, minHeight: '10px', whiteSpace: this.props.oneLine ? 'nowrap' : 'pre-line', height: this.props.height, maxHeight: this.props.maxHeight }}
+ //onPointerDown={this.stopPropagation}
onClick={this.onClick}
placeholder={this.props.placeholder}>
<span style={{ fontStyle: this.props.fontStyle, fontSize: this.props.fontSize }}>{this.props.contents ? this.props.contents?.valueOf() : this.props.placeholder?.valueOf()}</span>
diff --git a/src/client/views/nodes/FilterBox.scss b/src/client/views/FilterPanel.scss
index 107ad2e36..c903f29ee 100644
--- a/src/client/views/nodes/FilterBox.scss
+++ b/src/client/views/FilterPanel.scss
@@ -16,7 +16,6 @@
}
}
-
.filter-bookmark {
//display: flex;
@@ -34,12 +33,12 @@
// }
.filterBox-select {
- // width: 90%;
+ display: flex;
+ width: 100%;
margin-top: 5px;
- // margin-bottom: 15px;
+ background: white;
}
-
.filterBox-saveBookmark {
background-color: #e9e9e9;
border-radius: 11px;
@@ -61,7 +60,6 @@
margin-top: 4px;
margin-left: 2px;
}
-
}
.filterBox-select-scope,
@@ -153,13 +151,12 @@
.filterBox-treeView {
display: flex;
flex-direction: column;
- width: 200px;
- height: 100%;
- position: absolute;
+ width: 100%;
+ position: relative;
right: 0;
top: 0;
z-index: 1;
- background-color: #9F9F9F;
+ background-color: #9f9f9f;
.filterBox-tree {
z-index: 0;
@@ -177,8 +174,8 @@
cursor: pointer;
}
- >div,
- >div>div {
+ > div,
+ > div > div {
width: 100%;
height: 100%;
}
@@ -188,6 +185,7 @@
display: inline-block;
width: 100%;
margin-bottom: 10px;
- //height: calc(100% - 30px);
+ margin-left: 5px;
+ overflow: auto;
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/FilterPanel.tsx b/src/client/views/FilterPanel.tsx
new file mode 100644
index 000000000..fe42628cd
--- /dev/null
+++ b/src/client/views/FilterPanel.tsx
@@ -0,0 +1,234 @@
+import React = require('react');
+import { action, computed, observable, ObservableMap } from 'mobx';
+import { observer } from 'mobx-react';
+import Select from 'react-select';
+import { Doc, DocListCast, Field, StrListCast } from '../../fields/Doc';
+import { RichTextField } from '../../fields/RichTextField';
+import { StrCast } from '../../fields/Types';
+import { DocumentManager } from '../util/DocumentManager';
+import { UserOptions } from '../util/GroupManager';
+import './FilterPanel.scss';
+import { FieldView } from './nodes/FieldView';
+import { SearchBox } from './search/SearchBox';
+import { undoable } from '../util/UndoManager';
+
+interface filterProps {
+ rootDoc: Doc;
+}
+@observer
+export class FilterPanel extends React.Component<filterProps> {
+ public static LayoutString(fieldKey: string) {
+ return FieldView.LayoutString(FilterPanel, fieldKey);
+ }
+
+ /**
+ * @returns the relevant doc according to the value of FilterBox._filterScope i.e. either the Current Dashboard or the Current Collection
+ */
+ @computed get targetDoc() {
+ return this.props.rootDoc;
+ }
+ @computed get targetDocChildKey() {
+ const targetView = DocumentManager.Instance.getFirstDocumentView(this.targetDoc);
+ return targetView?.ComponentView?.annotationKey ?? targetView?.ComponentView?.fieldKey ?? 'data';
+ }
+ @computed get targetDocChildren() {
+ return [...DocListCast(this.targetDoc?.[this.targetDocChildKey] || Doc.ActiveDashboard?.data), ...DocListCast(this.targetDoc[Doc.LayoutFieldKey(this.targetDoc) + '_sidebar'])];
+ }
+
+ @computed get allDocs() {
+ const allDocs = new Set<Doc>();
+ const targetDoc = this.targetDoc;
+ if (targetDoc) {
+ SearchBox.foreachRecursiveDoc([this.targetDoc], (depth, doc) => allDocs.add(doc));
+ }
+ return Array.from(allDocs);
+ }
+
+ @computed get _allFacets() {
+ // trace();
+ const noviceReqFields = ['author', 'tags', 'text', 'type'];
+ const noviceLayoutFields: string[] = []; //["_layout_curPage"];
+ const noviceFields = [...noviceReqFields, ...noviceLayoutFields];
+
+ const keys = new Set<string>(noviceFields);
+ this.allDocs.forEach(doc => SearchBox.documentKeys(doc).filter(key => keys.add(key)));
+ const sortedKeys = Array.from(keys.keys())
+ .filter(key => key[0])
+ .filter(key => key.indexOf('modificationDate') !== -1 || (key[0] === key[0].toUpperCase() && !key.startsWith('_')) || noviceFields.includes(key) || !Doc.noviceMode)
+ .sort();
+ noviceFields.forEach(key => sortedKeys.splice(sortedKeys.indexOf(key), 1));
+ return [...noviceFields, ...sortedKeys];
+ }
+
+ /**
+ * The current attributes selected to filter based on
+ */
+ @computed get activeFilters() {
+ return StrListCast(this.targetDoc?._childFilters);
+ }
+
+ /**
+ * @returns a string array of the current attributes
+ */
+ @computed get currentFacets() {
+ return this.activeFilters.map(filter => filter.split(':')[0]);
+ }
+
+ gatherFieldValues(childDocs: Doc[], facetKey: string) {
+ const valueSet = new Set<string>();
+ let rtFields = 0;
+ let subDocs = childDocs;
+ if (subDocs.length > 0) {
+ let newarray: Doc[] = [];
+ while (subDocs.length > 0) {
+ newarray = [];
+ subDocs.forEach(t => {
+ const facetVal = t[facetKey];
+ if (facetVal instanceof RichTextField || typeof facetVal === 'string') rtFields++;
+ facetVal !== undefined && valueSet.add(Field.toString(facetVal as Field));
+ (facetVal === true || facetVal == false) && valueSet.add(Field.toString(!facetVal));
+ 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;
+ }
+ }
+ // }
+ // });
+ return { strings: Array.from(valueSet.keys()), rtFields };
+ }
+
+ public removeFilter = (filterName: string) => {
+ Doc.setDocFilter(this.targetDoc, filterName, undefined, 'remove');
+ Doc.setDocRangeFilter(this.targetDoc, filterName, undefined);
+ };
+
+ @observable _chosenFacets = new ObservableMap<string, 'text' | 'checkbox' | 'slider' | 'range'>();
+ @computed get activeFacets() {
+ const facets = new Map<string, 'text' | 'checkbox' | 'slider' | 'range'>(this._chosenFacets);
+ StrListCast(this.targetDoc?._childFilters).map(filter => facets.set(filter.split(':')[0], filter.split(':')[2] === 'match' ? 'text' : 'checkbox'));
+ setTimeout(() => StrListCast(this.targetDoc?._childFilters).map(action(filter => this._chosenFacets.set(filter.split(':')[0], filter.split(':')[2] === 'match' ? 'text' : 'checkbox'))));
+ return facets;
+ }
+ /**
+ * Responds to clicking the check box in the flyout menu
+ */
+ @action
+ facetClick = (facetHeader: string) => {
+ if (!this.targetDoc) return;
+ const allCollectionDocs = this.targetDocChildren;
+ const facetValues = this.gatherFieldValues(this.targetDocChildren, facetHeader);
+
+ let nonNumbers = 0;
+ let minVal = Number.MAX_VALUE,
+ maxVal = -Number.MAX_VALUE;
+ facetValues.strings.map(val => {
+ const num = val ? Number(val) : Number.NaN;
+ if (Number.isNaN(num)) {
+ val && nonNumbers++;
+ } else {
+ minVal = Math.min(num, minVal);
+ maxVal = Math.max(num, maxVal);
+ }
+ });
+ if (facetHeader === 'text' || (facetValues.rtFields / allCollectionDocs.length > 0.1 && facetValues.strings.length > 20)) {
+ this._chosenFacets.set(facetHeader, 'text');
+ } else if (facetHeader !== 'tags' && nonNumbers / facetValues.strings.length < 0.1) {
+ } else {
+ this._chosenFacets.set(facetHeader, 'checkbox');
+ }
+ };
+
+ facetValues = (facetHeader: string) => {
+ const allCollectionDocs = new Set<Doc>();
+ SearchBox.foreachRecursiveDoc(this.targetDocChildren, (depth: number, doc: Doc) => allCollectionDocs.add(doc));
+ const set = new Set<string>([String.fromCharCode(127) + '--undefined--']);
+ if (facetHeader === 'tags')
+ allCollectionDocs.forEach(child =>
+ StrListCast(child[facetHeader])
+ .filter(h => h)
+ .forEach(key => set.add(key))
+ );
+ else
+ allCollectionDocs.forEach(child => {
+ const fieldVal = child[facetHeader] as Field;
+ set.add(Field.toString(fieldVal));
+ (fieldVal === true || fieldVal === false) && set.add((!fieldVal).toString());
+ });
+ const facetValues = Array.from(set).filter(v => v);
+
+ let nonNumbers = 0;
+
+ facetValues.map(val => Number.isNaN(Number(val)) && nonNumbers++);
+ return nonNumbers / facetValues.length > 0.1 ? facetValues.sort() : facetValues.sort((n1: string, n2: string) => Number(n1) - Number(n2));
+ };
+
+ render() {
+ const options = this._allFacets.filter(facet => this.currentFacets.indexOf(facet) === -1).map(facet => ({ value: facet, label: facet }));
+ return (
+ <div className="filterBox-treeView">
+ <div className="filterBox-select">
+ <div style={{ width: '100%' }}>
+ <Select placeholder="Add a filter..." options={options} isMulti={false} onChange={val => this.facetClick((val as UserOptions).value)} onKeyDown={e => e.stopPropagation()} value={null} closeMenuOnSelect={true} />
+ </div>
+ <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 className="filterBox-tree" key="tree">
+ {Array.from(this.activeFacets.keys()).map(facetHeader => (
+ <div>
+ {facetHeader}
+ {this.displayFacetValueFilterUIs(this.activeFacets.get(facetHeader), facetHeader)}
+ </div>
+ ))}
+ </div>
+ </div>
+ );
+ }
+
+ private displayFacetValueFilterUIs(type: string | undefined, facetHeader: string): React.ReactNode {
+ switch (type) {
+ case 'text':
+ return (
+ <input
+ placeholder={
+ StrListCast(this.targetDoc._childFilters)
+ .find(filter => filter.split(':')[0] === facetHeader)
+ ?.split(':')[1] ?? '-empty-'
+ }
+ onBlur={undoable(e => Doc.setDocFilter(this.targetDoc, facetHeader, e.currentTarget.value, !e.currentTarget.value ? 'remove' : 'match'), 'set text filter')}
+ onKeyDown={e => e.key === 'Enter' && undoable(e => Doc.setDocFilter(this.targetDoc, facetHeader, e.currentTarget.value, !e.currentTarget.value ? 'remove' : 'match'), 'set text filter')(e)}
+ />
+ );
+ case 'checkbox':
+ return this.facetValues(facetHeader).map(fval => {
+ const facetValue = fval;
+ return (
+ <div>
+ <input
+ style={{ width: 20, marginLeft: 20 }}
+ checked={
+ StrListCast(this.targetDoc._childFilters)
+ .find(filter => filter.split(':')[0] === facetHeader && filter.split(':')[1] == facetValue)
+ ?.split(':')[2] === 'check'
+ }
+ type={type}
+ 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 a29073f14..54ad519de 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -1,11 +1,10 @@
import React = require('react');
import * as fitCurve from 'fit-curve';
-import { action, computed, observable, runInAction, trace } from 'mobx';
+import { action, computed, observable, runInAction } from 'mobx';
+import { observer } from 'mobx-react';
import { Doc, Opt } from '../../fields/Doc';
import { InkData, InkTool } from '../../fields/InkField';
-import { List } from '../../fields/List';
-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';
@@ -14,7 +13,6 @@ import { CognitiveServices } from '../cognitive_services/CognitiveServices';
import { Docs, DocUtils } from '../documents/Documents';
import { InteractionUtils } from '../util/InteractionUtils';
import { ScriptingGlobals } from '../util/ScriptingGlobals';
-import { SelectionManager } from '../util/SelectionManager';
import { Transform } from '../util/Transform';
import './GestureOverlay.scss';
import {
@@ -36,10 +34,8 @@ import { InkTranscription } from './InkTranscription';
import { checkInksToGroup } from './nodes/button/FontIconBox';
import { DocumentView } from './nodes/DocumentView';
import { RadialMenu } from './nodes/RadialMenu';
-import HorizontalPalette from './Palette';
import { Touchable } from './Touchable';
import TouchScrollableMenu, { TouchScrollableMenuItem } from './TouchScrollableMenu';
-import { observer } from 'mobx-react';
interface GestureOverlayProps {
isActive: boolean;
@@ -47,6 +43,7 @@ interface GestureOverlayProps {
@observer
export class GestureOverlay extends Touchable<GestureOverlayProps> {
static Instance: GestureOverlay;
+ static Instances: GestureOverlay[] = [];
@observable public InkShape: Opt<GestureUtils.Gestures>;
@observable public SavedColor?: string;
@@ -66,6 +63,8 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
@observable private _clipboardDoc?: JSX.Element;
@observable private _possibilities: JSX.Element[] = [];
+ public static DownDocView: DocumentView | undefined;
+
@computed private get height(): number {
return 2 * Math.max(this._pointerY && this._thumbY ? this._thumbY - this._pointerY : 100, 100);
}
@@ -78,7 +77,6 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
private _overlayRef = React.createRef<HTMLDivElement>();
private _d1: Doc | undefined;
private _inkToTextDoc: Doc | undefined;
- private _thumbDoc: Doc | undefined;
private thumbIdentifier?: number;
private pointerIdentifier?: number;
private _hands: Map<number, React.Touch[]> = new Map<number, React.Touch[]>();
@@ -89,75 +87,15 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
constructor(props: any) {
super(props);
- GestureOverlay.Instance = this;
+ GestureOverlay.Instances.push(this);
}
- static setupThumbButtons(doc: Doc) {
- const docProtoData: { title: string; icon: string; drag?: 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, system: true }),
- backgroundColor: 'orange',
- },
- { title: 'interpret text', icon: 'font', pointerUp: "setToolglass('none')", pointerDown: "setToolglass('inktotext')", backgroundColor: 'orange' },
- { title: 'ignore gestures', icon: 'signature', pointerUp: "setToolglass('none')", pointerDown: "setToolglass('ignoregesture')", backgroundColor: 'green' },
- ];
- return docProtoData.map(data =>
- Docs.Create.FontIconDocument({
- _nativeWidth: 10,
- _nativeHeight: 10,
- _width: 10,
- _height: 10,
- title: data.title,
- icon: data.icon,
- _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,
- _removeDropProperties: new List<string>(['dropAction']),
- dragFactory: data.dragFactory,
- system: 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',
- _autoHeight: true,
- _yMargin: 5,
- linearViewIsExpanded: true,
- backgroundColor: 'white',
- system: true,
- });
- thumbDoc.inkToTextDoc = Docs.Create.LinearDocument([], {
- _width: 300,
- _height: 25,
- _autoHeight: true,
- linearViewIsExpanded: true,
- flexDirection: 'column',
- system: true,
- });
- userDoc.thumbDoc = thumbDoc;
- }
- return Cast(userDoc.thumbDoc, Doc);
+ componentWillUnmount() {
+ GestureOverlay.Instances.splice(GestureOverlay.Instances.indexOf(this), 1);
+ GestureOverlay.Instance = GestureOverlay.Instances.lastElement();
}
componentDidMount = () => {
- this._thumbDoc = FieldValue(Cast(GestureOverlay.setupThumbDoc(Doc.UserDoc()), Doc));
- this._inkToTextDoc = FieldValue(Cast(this._thumbDoc?.inkToTextDoc, Doc));
+ GestureOverlay.Instance = this;
};
// TODO: nda - add dragging groups with one finger drag and have to click into group to scroll within the group
@@ -388,24 +326,6 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
this.thumbIdentifier = thumb?.identifier;
this._hands.set(thumb.identifier, fingers);
- const others = fingers.filter(f => f !== thumb);
- const minX = Math.min(...others.map(f => f.clientX));
- const minY = Math.min(...others.map(f => f.clientY));
-
- // load up the palette collection around the thumb
- const thumbDoc = await Cast(GestureOverlay.setupThumbDoc(Doc.UserDoc()), Doc);
- if (thumbDoc) {
- runInAction(() => {
- RadialMenu.Instance._display = false;
- this._inkToTextDoc = FieldValue(Cast(thumbDoc.inkToTextDoc, Doc));
- this._thumbDoc = thumbDoc;
- this._thumbX = thumb.clientX;
- this._thumbY = thumb.clientY;
- this._menuX = thumb.clientX + 50;
- this._menuY = thumb.clientY;
- this._palette = <HorizontalPalette key="palette" x={minX} y={minY} thumb={[thumb.clientX, thumb.clientY]} thumbDoc={thumbDoc} />;
- });
- }
this.removeMoveListeners();
document.removeEventListener('touchmove', this.handleHandMove);
@@ -455,11 +375,6 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
if (Math.abs(pt.clientY - this._thumbY) > 10 * window.devicePixelRatio) {
this._selectedIndex = Math.min(Math.max(-1, -Math.ceil((pt.clientY - this._thumbY) / (10 * window.devicePixelRatio)) - 1), this._possibilities.length - 1);
}
- } else if (this._thumbDoc) {
- if (Math.abs(pt.clientX - this._thumbX) > 15 * window.devicePixelRatio) {
- this._thumbDoc.selectedIndex = Math.max(-1, NumCast(this._thumbDoc.selectedIndex) - Math.sign(pt.clientX - this._thumbX));
- this._thumbX = pt.clientX;
- }
}
}
}
@@ -478,7 +393,6 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
if (this.thumbIdentifier) this._hands.delete(this.thumbIdentifier);
this._palette = undefined;
this.thumbIdentifier = undefined;
- this._thumbDoc = undefined;
// this chunk of code is for handling the ink to text toolglass
let scriptWorked = false;
@@ -628,6 +542,7 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
}
@action
onPointerUp = (e: PointerEvent) => {
+ GestureOverlay.DownDocView = undefined;
if (this._points.length > 1) {
const B = this.svgBounds;
const points = this._points.map(p => ({ X: p.X - B.left, Y: p.Y - B.top }));
@@ -650,9 +565,9 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
}
possibilities.push(...wR?.alternates?.map((a: any) => a.recognizedString));
}
- const r = Math.max(this.svgBounds.right, ...this._strokes.map(s => this.getBounds(s).right));
- const l = Math.min(this.svgBounds.left, ...this._strokes.map(s => this.getBounds(s).left));
- const t = Math.min(this.svgBounds.top, ...this._strokes.map(s => this.getBounds(s).top));
+ const r = Math.max(this.svgBounds.right, ...this._strokes.map(s => GestureOverlay.getBounds(s).right));
+ const l = Math.min(this.svgBounds.left, ...this._strokes.map(s => GestureOverlay.getBounds(s).left));
+ const t = Math.min(this.svgBounds.top, ...this._strokes.map(s => GestureOverlay.getBounds(s).top));
// if we receive any word results from cognitive services, display them
runInAction(() => {
@@ -883,7 +798,7 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
detail: {
points,
gesture,
- bounds: this.getBounds(points),
+ bounds: GestureOverlay.getBounds(points),
text,
},
})
@@ -891,7 +806,7 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
);
};
- getBounds = (stroke: InkData, pad?: boolean) => {
+ public static getBounds = (stroke: InkData, pad?: boolean) => {
const padding = pad ? [-20000, 20000] : [];
const xs = [...padding, ...stroke.map(p => p.X)];
const ys = [...padding, ...stroke.map(p => p.Y)];
@@ -903,12 +818,12 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
};
@computed get svgBounds() {
- return this.getBounds(this._points);
+ return GestureOverlay.getBounds(this._points);
}
@computed get elements() {
- const selView = SelectionManager.Views().lastElement();
- const width = (Number(ActiveInkWidth()) * NumCast(selView?.rootDoc._viewScale, 1)) / (selView?.props.ScreenToLocalTransform().Scale || 1);
+ const selView = GestureOverlay.DownDocView;
+ const width = Number(ActiveInkWidth()) * NumCast(selView?.rootDoc._freeform_scale, 1); // * (selView?.props.ScreenToLocalTransform().Scale || 1);
const rect = this._overlayRef.current?.getBoundingClientRect();
const B = { left: -20000, right: 20000, top: -20000, bottom: 20000, width: 40000, height: 40000 }; //this.getBounds(this._points, true);
B.left = B.left - width / 2;
@@ -1002,14 +917,12 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
renderDepth={0}
styleProvider={returnEmptyString}
docViewPath={returnEmptyDoclist}
- focus={DocUtils.DefaultFocus}
+ focus={emptyFunction}
whenChildContentsActiveChanged={emptyFunction}
bringToFront={emptyFunction}
- docRangeFilters={returnEmptyFilter}
- docFilters={returnEmptyFilter}
+ childFiltersByRanges={returnEmptyFilter}
+ childFilters={returnEmptyFilter}
searchFilterDocs={returnEmptyDoclist}
- ContainingCollectionView={undefined}
- ContainingCollectionDoc={undefined}
/>
);
};
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index 5a6caf995..47dcdd2e4 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -1,23 +1,20 @@
import { random } from 'lodash';
-import { action, observable, runInAction } from 'mobx';
-import { DateField } from '../../fields/DateField';
+import { action, runInAction } from 'mobx';
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';
import { CollectionStackedTimeline } from './collections/CollectionStackedTimeline';
import { ContextMenu } from './ContextMenu';
@@ -26,6 +23,7 @@ import { InkStrokeProperties } from './InkStrokeProperties';
import { LightboxView } from './LightboxView';
import { MainView } from './MainView';
import { DocumentLinksButton } from './nodes/DocumentLinksButton';
+import { OpenWhereMod } from './nodes/DocumentView';
import { AnchorMenu } from './pdf/AnchorMenu';
const modifiers = ['control', 'meta', 'shift', 'alt'];
@@ -102,6 +100,20 @@ export class KeyManager {
const groupings = SelectionManager.Views().slice();
const randomGroup = random(0, 1000);
+ const collectionView = groupings.reduce(
+ (col, g) => (col === null || g.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView === col ? g.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView : undefined),
+ null as null | undefined | CollectionFreeFormView
+ );
+ if (collectionView) {
+ UndoManager.RunInBatch(() => {
+ collectionView._marqueeViewRef.current?.collection(
+ e,
+ true,
+ groupings.map(g => g.rootDoc)
+ );
+ }, 'grouping');
+ break;
+ }
UndoManager.RunInBatch(() => groupings.map(dv => (dv.layoutDoc.group = randomGroup)), 'group');
SelectionManager.DeselectAll();
break;
@@ -145,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;
@@ -191,6 +201,16 @@ export class KeyManager {
case 'arrowdown':
UndoManager.RunInBatch(() => SelectionManager.Views().map(dv => dv.props.CollectionFreeFormDocumentView?.().nudge?.(0, 10)), 'nudge down');
break;
+ case 'g':
+ if (document.activeElement?.tagName === 'INPUT' || document.activeElement?.tagName === 'TEXTAREA') {
+ return { stopPropagation: false, preventDefault: false };
+ }
+
+ const groupings = SelectionManager.Views().slice();
+ const randomGroup = random(0, 1000);
+ UndoManager.RunInBatch(() => groupings.map(dv => (dv.layoutDoc.group = randomGroup)), 'group');
+ SelectionManager.DeselectAll();
+ break;
}
return {
@@ -225,7 +245,7 @@ export class KeyManager {
if (document.activeElement?.tagName === 'INPUT' || document.activeElement?.tagName === 'TEXTAREA') {
return { stopPropagation: false, preventDefault: false };
}
- MainView.Instance.mainFreeform && CollectionDockingView.AddSplit(MainView.Instance.mainFreeform, 'right');
+ MainView.Instance.mainFreeform && CollectionDockingView.AddSplit(MainView.Instance.mainFreeform, OpenWhereMod.right);
break;
case 'arrowleft':
if (document.activeElement?.tagName === 'INPUT' || document.activeElement?.tagName === 'TEXTAREA') {
@@ -245,7 +265,7 @@ export class KeyManager {
if (SelectionManager.Views().length === 1 && SelectionManager.Views()[0].ComponentView?.search) {
SelectionManager.Views()[0].ComponentView?.search?.('', false, false);
} else {
- const searchBtn = Doc.MySearcher;
+ const searchBtn = DocListCast(Doc.MyLeftSidebarMenu.data).find(d => d.target === Doc.MySearcher);
if (searchBtn) {
MainView.Instance.selectMenu(searchBtn);
}
@@ -326,38 +346,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)).clone : d)));
- if (added.length) {
- added.map(doc => (doc.context = targetDataDoc));
- undoBatch(() => {
- targetDataDoc[fieldKey] = new List<Doc>([...docList, ...added]);
- targetDataDoc[fieldKey + '-lastModified'] = 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);
}
}
@@ -373,6 +368,9 @@ export class KeyManager {
case 'z':
UndoManager.Redo();
break;
+ case 'p':
+ Doc.ActiveTool = InkTool.Write;
+ break;
}
return {
diff --git a/src/client/views/InkControlPtHandles.tsx b/src/client/views/InkControlPtHandles.tsx
index d036a636a..07e3270b1 100644
--- a/src/client/views/InkControlPtHandles.tsx
+++ b/src/client/views/InkControlPtHandles.tsx
@@ -1,23 +1,23 @@
-import React = require("react");
-import { action, observable } from "mobx";
-import { observer } from "mobx-react";
-import { Doc } from "../../fields/Doc";
-import { ControlPoint, InkData, PointData, InkField } from "../../fields/InkField";
-import { List } from "../../fields/List";
-import { listSpec } from "../../fields/Schema";
-import { Cast, NumCast } from "../../fields/Types";
-import { setupMoveUpEvents, returnFalse } from "../../Utils";
-import { Transform } from "../util/Transform";
-import { UndoManager } from "../util/UndoManager";
-import { Colors } from "./global/globalEnums";
-import { InkingStroke } from "./InkingStroke";
-import { InkStrokeProperties } from "./InkStrokeProperties";
-import { DocumentView } from "./nodes/DocumentView";
-import { SelectionManager } from "../util/SelectionManager";
+import React = require('react');
+import { action, observable } from 'mobx';
+import { observer } from 'mobx-react';
+import { Doc } from '../../fields/Doc';
+import { ControlPoint, InkData, PointData, InkField } from '../../fields/InkField';
+import { List } from '../../fields/List';
+import { listSpec } from '../../fields/Schema';
+import { Cast, NumCast } from '../../fields/Types';
+import { setupMoveUpEvents, returnFalse } from '../../Utils';
+import { Transform } from '../util/Transform';
+import { UndoManager } from '../util/UndoManager';
+import { Colors } from './global/globalEnums';
+import { InkingStroke } from './InkingStroke';
+import { InkStrokeProperties } from './InkStrokeProperties';
+import { DocumentView } from './nodes/DocumentView';
+import { SelectionManager } from '../util/SelectionManager';
export interface InkControlProps {
inkDoc: Doc;
- inkView: DocumentView;
+ inkView: InkingStroke;
inkCtrlPoints: InkData;
screenCtrlPoints: InkData;
screenSpaceLineWidth: number;
@@ -26,16 +26,16 @@ export interface InkControlProps {
@observer
export class InkControlPtHandles extends React.Component<InkControlProps> {
-
@observable private _overControl = -1;
-
- @observable controlUndo: UndoManager.Batch | undefined;
+ get docView() {
+ return this.props.inkView.props.docViewPath().lastElement();
+ }
componentDidMount() {
- document.addEventListener("keydown", this.onDelete, true);
+ document.addEventListener('keydown', this.onDelete, true);
}
componentWillUnmount() {
- document.removeEventListener("keydown", this.onDelete, true);
+ document.removeEventListener('keydown', this.onDelete, true);
}
/**
* Handles the movement of a selected control point when the user clicks and drags.
@@ -43,30 +43,32 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
*/
@action
onControlDown = (e: React.PointerEvent, controlIndex: number): void => {
- const ptFromScreen = this.props.inkView.ComponentView?.ptFromScreen;
+ const ptFromScreen = this.props.inkView.ptFromScreen;
if (ptFromScreen) {
const order = controlIndex % 4;
const handleIndexA = ((order === 3 ? controlIndex - 1 : controlIndex - 2) + this.props.inkCtrlPoints.length) % this.props.inkCtrlPoints.length;
const handleIndexB = (order === 3 ? controlIndex + 2 : controlIndex + 1) % this.props.inkCtrlPoints.length;
- const brokenIndices = Cast(this.props.inkDoc.brokenInkIndices, listSpec("number"));
+ const brokenIndices = Cast(this.props.inkDoc.brokenInkIndices, listSpec('number'));
const wasSelected = InkStrokeProperties.Instance._currentPoint === controlIndex;
if (!wasSelected) InkStrokeProperties.Instance._currentPoint = -1;
const origInk = this.props.inkCtrlPoints.slice();
- setupMoveUpEvents(this, e,
+ setupMoveUpEvents(
+ this,
+ e,
action((e: PointerEvent, down: number[], delta: number[]) => {
- if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("drag ink ctrl pt");
+ if (!this.props.inkView.controlUndo) this.props.inkView.controlUndo = UndoManager.StartBatch('drag ink ctrl pt');
const inkMoveEnd = ptFromScreen({ X: delta[0], Y: delta[1] });
const inkMoveStart = ptFromScreen({ X: 0, Y: 0 });
- InkStrokeProperties.Instance.moveControlPtHandle(this.props.inkView, inkMoveEnd.X - inkMoveStart.X, inkMoveEnd.Y - inkMoveStart.Y, controlIndex, origInk);
+ InkStrokeProperties.Instance.moveControlPtHandle(this.docView, inkMoveEnd.X - inkMoveStart.X, inkMoveEnd.Y - inkMoveStart.Y, controlIndex, origInk);
return false;
}),
action(() => {
- if (this.controlUndo) {
- InkStrokeProperties.Instance.snapControl(this.props.inkView, controlIndex);
+ if (this.props.inkView.controlUndo) {
+ InkStrokeProperties.Instance.snapControl(this.docView, controlIndex);
}
- this.controlUndo?.end();
- this.controlUndo = undefined;
- UndoManager.FilterBatches(["data", "x", "y", "width", "height"]);
+ this.props.inkView.controlUndo?.end();
+ this.props.inkView.controlUndo = undefined;
+ UndoManager.FilterBatches(['data', 'x', 'y', 'width', 'height']);
}),
action((e: PointerEvent, doubleTap: boolean | undefined) => {
const equivIndex = controlIndex === 0 ? this.props.inkCtrlPoints.length - 1 : controlIndex === this.props.inkCtrlPoints.length - 1 ? 0 : controlIndex;
@@ -76,44 +78,52 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
else this.props.inkDoc.brokenInkIndices = new List<number>([controlIndex]);
} else {
if (brokenIndices?.includes(equivIndex)) {
- if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("make smooth");
- InkStrokeProperties.Instance.snapHandleTangent(this.props.inkView, equivIndex, handleIndexA, handleIndexB);
+ if (!this.props.inkView.controlUndo) this.props.inkView.controlUndo = UndoManager.StartBatch('make smooth');
+ InkStrokeProperties.Instance.snapHandleTangent(this.docView, equivIndex, handleIndexA, handleIndexB);
}
if (equivIndex !== controlIndex && brokenIndices?.includes(controlIndex)) {
- if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("make smooth");
- InkStrokeProperties.Instance.snapHandleTangent(this.props.inkView, controlIndex, handleIndexA, handleIndexB);
+ if (!this.props.inkView.controlUndo) this.props.inkView.controlUndo = UndoManager.StartBatch('make smooth');
+ InkStrokeProperties.Instance.snapHandleTangent(this.docView, controlIndex, handleIndexA, handleIndexB);
}
}
- this.controlUndo?.end();
- this.controlUndo = undefined;
+ this.props.inkView.controlUndo?.end();
+ this.props.inkView.controlUndo = undefined;
}
this.changeCurrPoint(controlIndex);
- }), undefined, undefined, () => wasSelected && this.changeCurrPoint(-1));
+ }),
+ undefined,
+ undefined,
+ () => wasSelected && this.changeCurrPoint(-1)
+ );
}
- }
+ };
/**
* Updates whether a user has hovered over a particular control point or point that could be added
* on click.
*/
- @action onEnterControl = (i: number) => { this._overControl = i; };
- @action onLeaveControl = () => { this._overControl = -1; };
+ @action onEnterControl = (i: number) => {
+ this._overControl = i;
+ };
+ @action onLeaveControl = () => {
+ this._overControl = -1;
+ };
/**
* Deletes the currently selected point.
*/
@action
onDelete = (e: KeyboardEvent) => {
- if (["-", "Backspace", "Delete"].includes(e.key)) {
- InkStrokeProperties.Instance.deletePoints(this.props.inkView, e.shiftKey);
+ if (['-', 'Backspace', 'Delete'].includes(e.key)) {
+ InkStrokeProperties.Instance.deletePoints(this.docView, e.shiftKey);
e.stopPropagation();
}
- }
+ };
/**
* Changes the current selected control point.
*/
@action
- changeCurrPoint = (i: number) => InkStrokeProperties.Instance._currentPoint = i
+ changeCurrPoint = (i: number) => (InkStrokeProperties.Instance._currentPoint = i);
render() {
// Accessing the current ink's data and extracting all control points.
@@ -133,101 +143,105 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
const closed = InkingStroke.IsClosed(inkData);
const nearestScreenPt = this.props.nearestScreenPt();
- const TagType = (broken?: boolean) => broken ? "rect" : "circle";
- const hdl = (control: { X: number, Y: number, I: number }, scale: number, color: string) => {
- const broken = Cast(this.props.inkDoc.brokenInkIndices, listSpec("number"))?.includes(control.I);
+ const TagType = (broken?: boolean) => (broken ? 'rect' : 'circle');
+ const hdl = (control: { X: number; Y: number; I: number }, scale: number, color: string) => {
+ const broken = Cast(this.props.inkDoc.brokenInkIndices, listSpec('number'))?.includes(control.I);
const Tag = TagType((control.I === 0 || control.I === inkData.length - 1) && !closed) as keyof JSX.IntrinsicElements;
- return <Tag key={control.I.toString() + scale}
- x={control.X - this.props.screenSpaceLineWidth * 2 * scale}
- y={control.Y - this.props.screenSpaceLineWidth * 2 * scale}
- cx={control.X}
- cy={control.Y}
- r={this.props.screenSpaceLineWidth * 2 * scale}
- opacity={this.controlUndo ? 0.15 : 1}
- height={this.props.screenSpaceLineWidth * 4 * scale}
- width={this.props.screenSpaceLineWidth * 4 * scale}
- strokeWidth={this.props.screenSpaceLineWidth / 2}
- stroke={Colors.MEDIUM_BLUE}
- fill={broken ? Colors.MEDIUM_BLUE : color}
- onPointerDown={(e: React.PointerEvent) => this.onControlDown(e, control.I)}
- onMouseEnter={() => this.onEnterControl(control.I)}
- onMouseLeave={this.onLeaveControl}
- pointerEvents="all"
- cursor="default"
- />;
- };
- return (<svg>
- {!nearestScreenPt ? (null) :
- <circle key={"npt"}
- cx={nearestScreenPt.X}
- cy={nearestScreenPt.Y}
- r={this.props.screenSpaceLineWidth * 2}
- fill={"#00007777"}
- stroke={"#00007777"}
- strokeWidth={0}
- pointerEvents="none"
+ return (
+ <Tag
+ key={control.I.toString() + scale}
+ x={control.X - this.props.screenSpaceLineWidth * 2 * scale}
+ y={control.Y - this.props.screenSpaceLineWidth * 2 * scale}
+ cx={control.X}
+ cy={control.Y}
+ r={this.props.screenSpaceLineWidth * 2 * scale}
+ opacity={this.props.inkView.controlUndo ? 0.15 : 1}
+ height={this.props.screenSpaceLineWidth * 4 * scale}
+ width={this.props.screenSpaceLineWidth * 4 * scale}
+ strokeWidth={this.props.screenSpaceLineWidth / 2}
+ stroke={Colors.MEDIUM_BLUE}
+ fill={broken ? Colors.MEDIUM_BLUE : color}
+ onPointerDown={(e: React.PointerEvent) => this.onControlDown(e, control.I)}
+ onMouseEnter={() => this.onEnterControl(control.I)}
+ onMouseLeave={this.onLeaveControl}
+ pointerEvents="all"
+ cursor="default"
/>
- }
- {sreenCtrlPoints.map(control => hdl(control, this._overControl !== control.I ? 1 : 3 / 2, Colors.WHITE))}
- </svg>
+ );
+ };
+ return (
+ <svg>
+ {!nearestScreenPt ? null : <circle key={'npt'} cx={nearestScreenPt.X} cy={nearestScreenPt.Y} r={this.props.screenSpaceLineWidth * 2} fill={'#00007777'} stroke={'#00007777'} strokeWidth={0} pointerEvents="none" />}
+ {sreenCtrlPoints.map(control => hdl(control, this._overControl !== control.I ? 1 : 3 / 2, Colors.WHITE))}
+ </svg>
);
}
}
-
export interface InkEndProps {
inkDoc: Doc;
- inkView: DocumentView;
+ inkView: InkingStroke;
screenSpaceLineWidth: number;
- startPt: PointData;
- endPt: PointData;
+ startPt: () => PointData;
+ endPt: () => PointData;
}
@observer
export class InkEndPtHandles extends React.Component<InkEndProps> {
- @observable controlUndo: UndoManager.Batch | undefined;
@observable _overStart: boolean = false;
@observable _overEnd: boolean = false;
@action
- dragRotate = (e: React.PointerEvent, p1: () => { X: number, Y: number }, p2: () => { X: number, Y: number }) => {
- setupMoveUpEvents(this, e, (e) => {
- if (!this.controlUndo) this.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 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());
- return false;
- }, action(() => {
- this.controlUndo?.end();
- this.controlUndo = undefined;
- UndoManager.FilterBatches(["data", "x", "y", "width", "height"]);
- }), returnFalse);
- }
+ 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 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, 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(['stroke', 'x', 'y', 'width', 'height']);
+ }),
+ returnFalse
+ );
+ };
render() {
- const hdl = (key: string, pt: PointData, dragFunc: (e: React.PointerEvent) => void) => <circle key={key}
- cx={pt.X}
- cy={pt.Y}
- r={this.props.screenSpaceLineWidth * 2}
- fill={this._overStart ? "#aaaaaa" : "#99999977"}
- stroke={"#00007777"}
- strokeWidth={0}
- onPointerLeave={action(() => this._overStart = false)}
- onPointerEnter={action(() => this._overStart = true)}
- onPointerDown={dragFunc}
- pointerEvents="all"
- />;
- 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))}
- </svg>
+ const hdl = (key: string, pt: PointData, dragFunc: (e: React.PointerEvent) => void) => (
+ <circle
+ key={key}
+ cx={pt.X}
+ cy={pt.Y}
+ r={this.props.screenSpaceLineWidth * 2}
+ fill={this._overStart ? '#aaaaaa' : '#99999977'}
+ stroke={'#00007777'}
+ strokeWidth={0}
+ onPointerLeave={action(() => (this._overStart = false))}
+ onPointerEnter={action(() => (this._overStart = true))}
+ onPointerDown={dragFunc}
+ pointerEvents="all"
+ />
+ );
+ 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))}
+ </svg>
);
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/InkStroke.scss b/src/client/views/InkStroke.scss
index bed7caf7f..f504890a5 100644
--- a/src/client/views/InkStroke.scss
+++ b/src/client/views/InkStroke.scss
@@ -39,5 +39,8 @@
&:hover {
background: #9f9f9f0a;
}
+ > .formattedTextBox {
+ position: relative;
+ }
}
}
diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts
index d19a916f9..abc4381a6 100644
--- a/src/client/views/InkStrokeProperties.ts
+++ b/src/client/views/InkStrokeProperties.ts
@@ -19,7 +19,6 @@ export class InkStrokeProperties {
return this._Instance || new InkStrokeProperties();
}
- @observable _lock = false;
@observable _controlButton = false;
@observable _currentPoint = -1;
@@ -51,21 +50,21 @@ export class InkStrokeProperties {
if (!requireCurrPoint || this._currentPoint !== -1) {
const doc = inkView.rootDoc;
if (doc.type === DocumentType.INK && doc.width && doc.height) {
- const ink = Cast(doc.data, InkField)?.inkData;
+ const ink = Cast(doc.stroke, InkField)?.inkData;
if (ink) {
const oldXrange = (xs => ({ coord: NumCast(doc.x), min: Math.min(...xs), max: Math.max(...xs) }))(ink.map(p => p.X));
const oldYrange = (ys => ({ coord: NumCast(doc.y), min: Math.min(...ys), max: Math.max(...ys) }))(ink.map(p => p.Y));
- const ptsXscale = (NumCast(doc._width) - NumCast(doc.strokeWidth)) / (oldXrange.max - oldXrange.min || 1) || 1;
- const ptsYscale = (NumCast(doc._height) - NumCast(doc.strokeWidth)) / (oldYrange.max - oldYrange.min || 1) || 1;
- const newPoints = func(inkView, ink, ptsXscale, ptsYscale, NumCast(doc.strokeWidth));
+ const ptsXscale = (NumCast(doc._width) - NumCast(doc.stroke_width)) / (oldXrange.max - oldXrange.min || 1) || 1;
+ const ptsYscale = (NumCast(doc._height) - NumCast(doc.stroke_width)) / (oldYrange.max - oldYrange.min || 1) || 1;
+ const newPoints = func(inkView, ink, ptsXscale, ptsYscale, NumCast(doc.stroke_width));
if (newPoints) {
const newXrange = (xs => ({ min: Math.min(...xs), max: Math.max(...xs) }))(newPoints.map(p => p.X));
const newYrange = (ys => ({ min: Math.min(...ys), max: Math.max(...ys) }))(newPoints.map(p => p.Y));
- doc._width = (newXrange.max - newXrange.min) * ptsXscale + NumCast(doc.strokeWidth);
- doc._height = (newYrange.max - newYrange.min) * ptsYscale + NumCast(doc.strokeWidth);
+ doc._width = (newXrange.max - newXrange.min) * ptsXscale + NumCast(doc.stroke_width);
+ doc._height = (newYrange.max - newYrange.min) * ptsYscale + NumCast(doc.stroke_width);
doc.x = oldXrange.coord + (newXrange.min - oldXrange.min) * ptsXscale;
doc.y = oldYrange.coord + (newYrange.min - oldYrange.min) * ptsYscale;
- Doc.GetProto(doc).data = new InkField(newPoints);
+ Doc.GetProto(doc).stroke = new InkField(newPoints);
appliedFunc = true;
}
}
@@ -203,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
@@ -342,12 +340,19 @@ export class InkStrokeProperties {
if (ink) {
const screenDragPt = inkView.ComponentView?.ptToScreen?.(ink[controlIndex]);
if (screenDragPt) {
+ if (controlIndex === ink.length - 1) {
+ const firstPtScr = inkView.ComponentView?.ptToScreen?.(ink[0]);
+ if (firstPtScr && Math.sqrt((firstPtScr.X - screenDragPt.X) * (firstPtScr.X - screenDragPt.X) + (firstPtScr.Y - screenDragPt.Y) * (firstPtScr.Y - screenDragPt.Y)) < 7) {
+ const deltaX = ink[0].X - ink[controlIndex].X;
+ const deltaY = ink[0].Y - ink[controlIndex].Y;
+ return this.moveControlPtHandle(inkView, deltaX, deltaY, controlIndex, ink.slice());
+ }
+ }
const snapData = this.snapToAllCurves(screenDragPt, inkView, { nearestPt: { X: 0, Y: 0 }, distance: 10 }, ink, controlIndex);
if (snapData.distance < 10) {
const deltaX = snapData.nearestPt.X - ink[controlIndex].X;
const deltaY = snapData.nearestPt.Y - ink[controlIndex].Y;
- const res = this.moveControlPtHandle(inkView, deltaX, deltaY, controlIndex, ink.slice());
- return res;
+ return this.moveControlPtHandle(inkView, deltaX, deltaY, controlIndex, ink.slice());
}
}
}
@@ -367,10 +372,11 @@ export class InkStrokeProperties {
snapToAllCurves = (screenDragPt: { X: number; Y: number }, inkView: DocumentView, snapData: { nearestPt: { X: number; Y: number }; distance: number }, ink: InkData, controlIndex: number) => {
const containingCollection = inkView.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView;
+ const containingDocView = inkView.props.CollectionFreeFormDocumentView?.().props.DocumentView?.();
containingCollection?.childDocs
.filter(doc => doc.type === DocumentType.INK)
.forEach(doc => {
- const testInkView = DocumentManager.Instance.getDocumentView(doc, containingCollection?.props.CollectionView);
+ const testInkView = DocumentManager.Instance.getDocumentView(doc, containingDocView);
const snapped = testInkView?.ComponentView?.snapPt?.(screenDragPt, doc === inkView.rootDoc ? this.excludeSelfSnapSegs(ink, controlIndex) : []);
if (snapped && snapped.distance < snapData.distance) {
const snappedInkPt = doc === inkView.rootDoc ? snapped.nearestPt : inkView.ComponentView?.ptFromScreen?.(testInkView?.ComponentView?.ptToScreen?.(snapped.nearestPt) ?? { X: 0, Y: 0 }); // convert from snapped ink coordinate system to dragged ink coordinate system by converting to/from screen space
diff --git a/src/client/views/InkTangentHandles.tsx b/src/client/views/InkTangentHandles.tsx
index ae35bc980..71e0ff63c 100644
--- a/src/client/views/InkTangentHandles.tsx
+++ b/src/client/views/InkTangentHandles.tsx
@@ -1,21 +1,21 @@
-import React = require("react");
-import { action } from "mobx";
-import { observer } from "mobx-react";
-import { Doc } from "../../fields/Doc";
-import { HandleLine, HandlePoint, InkData } from "../../fields/InkField";
-import { List } from "../../fields/List";
-import { listSpec } from "../../fields/Schema";
-import { Cast } from "../../fields/Types";
-import { emptyFunction, setupMoveUpEvents } from "../../Utils";
-import { Transform } from "../util/Transform";
-import { UndoManager } from "../util/UndoManager";
-import { Colors } from "./global/globalEnums";
-import { InkingStroke } from "./InkingStroke";
-import { InkStrokeProperties } from "./InkStrokeProperties";
-import { DocumentView } from "./nodes/DocumentView";
+import React = require('react');
+import { action } from 'mobx';
+import { observer } from 'mobx-react';
+import { Doc } from '../../fields/Doc';
+import { HandleLine, HandlePoint, InkData } from '../../fields/InkField';
+import { List } from '../../fields/List';
+import { listSpec } from '../../fields/Schema';
+import { Cast } from '../../fields/Types';
+import { emptyFunction, setupMoveUpEvents } from '../../Utils';
+import { Transform } from '../util/Transform';
+import { UndoManager } from '../util/UndoManager';
+import { Colors } from './global/globalEnums';
+import { InkingStroke } from './InkingStroke';
+import { InkStrokeProperties } from './InkStrokeProperties';
+import { DocumentView } from './nodes/DocumentView';
export interface InkHandlesProps {
inkDoc: Doc;
- inkView: DocumentView;
+ inkView: InkingStroke;
screenCtrlPoints: InkData;
screenSpaceLineWidth: number;
ScreenToLocalTransform: () => Transform;
@@ -23,49 +23,52 @@ export interface InkHandlesProps {
@observer
export class InkTangentHandles extends React.Component<InkHandlesProps> {
+ get docView() {
+ return this.props.inkView.props.docViewPath().lastElement();
+ }
/**
* Handles the movement of a selected handle point when the user clicks and drags.
* @param handleNum The index of the currently selected handle point.
*/
onHandleDown = (e: React.PointerEvent, handleIndex: number): void => {
- const ptFromScreen = this.props.inkView.ComponentView?.ptFromScreen;
- if (!ptFromScreen) return;
- const screenScale = this.props.ScreenToLocalTransform().Scale;
- var controlUndo: UndoManager.Batch | undefined;
const order = handleIndex % 4;
const oppositeHandleRawIndex = order === 1 ? handleIndex - 3 : handleIndex + 3;
const oppositeHandleIndex = (oppositeHandleRawIndex < 0 ? this.props.screenCtrlPoints.length + oppositeHandleRawIndex : oppositeHandleRawIndex) % this.props.screenCtrlPoints.length;
const controlIndex = (order === 1 ? handleIndex - 1 : handleIndex + 2) % this.props.screenCtrlPoints.length;
- setupMoveUpEvents(this, e,
- (e: PointerEvent, down: number[], delta: number[]) => {
- if (!controlUndo) controlUndo = UndoManager.StartBatch("DocDecs move tangent");
+ setupMoveUpEvents(
+ this,
+ e,
+ action((e: PointerEvent, down: number[], delta: number[]) => {
+ if (!this.props.inkView.controlUndo) this.props.inkView.controlUndo = UndoManager.StartBatch('DocDecs move tangent');
if (e.altKey) this.onBreakTangent(controlIndex);
- const inkMoveEnd = ptFromScreen({ X: delta[0], Y: delta[1] });
- const inkMoveStart = ptFromScreen({ X: 0, Y: 0 });
- InkStrokeProperties.Instance.moveTangentHandle(this.props.inkView, -(inkMoveEnd.X - inkMoveStart.X), -(inkMoveEnd.Y - inkMoveStart.Y), handleIndex, oppositeHandleIndex, controlIndex);
+ const inkMoveEnd = this.props.inkView.ptFromScreen({ X: delta[0], Y: delta[1] });
+ const inkMoveStart = this.props.inkView.ptFromScreen({ X: 0, Y: 0 });
+ InkStrokeProperties.Instance.moveTangentHandle(this.docView, -(inkMoveEnd.X - inkMoveStart.X), -(inkMoveEnd.Y - inkMoveStart.Y), handleIndex, oppositeHandleIndex, controlIndex);
return false;
- }, () => {
- controlUndo?.end();
- UndoManager.FilterBatches(["data", "x", "y", "width", "height"]);
- }, emptyFunction
+ }),
+ action(() => {
+ this.props.inkView.controlUndo?.end();
+ this.props.inkView.controlUndo = undefined;
+ UndoManager.FilterBatches(['data', 'x', 'y', 'width', 'height']);
+ }),
+ emptyFunction
);
- }
+ };
/**
- * Breaks tangent handle movement when ‘Alt’ key is held down. Adds the current handle index and
+ * Breaks tangent handle movement when ‘Alt’ key is held down. Adds the current handle index and
* its matching (opposite) handle to a list of broken handle indices.
* @param handleNum The index of the currently selected handle point.
*/
@action
onBreakTangent = (controlIndex: number) => {
const closed = InkingStroke.IsClosed(this.props.screenCtrlPoints);
- const brokenIndices = Cast(this.props.inkDoc.brokenInkIndices, listSpec("number"));
- if (!brokenIndices?.includes(controlIndex) &&
- ((controlIndex > 0 && controlIndex < this.props.screenCtrlPoints.length - 1) || closed)) {
+ const brokenIndices = Cast(this.props.inkDoc.brokenInkIndices, listSpec('number'));
+ if (!brokenIndices?.includes(controlIndex) && ((controlIndex > 0 && controlIndex < this.props.screenCtrlPoints.length - 1) || closed)) {
if (brokenIndices) brokenIndices.push(controlIndex);
else this.props.inkDoc.brokenInkIndices = new List<number>([controlIndex]);
}
- }
+ };
render() {
// Accessing the current ink's data and extracting all handle points and handle lines.
@@ -81,10 +84,18 @@ export class InkTangentHandles extends React.Component<InkHandlesProps> {
// Adding first and last (single) handle lines.
if (closed) {
tangentLines.push({ X1: data[data.length - 2].X, Y1: data[data.length - 2].Y, X2: data[0].X, Y2: data[0].Y, X3: data[1].X, Y3: data[1].Y, dot1: 0, dot2: data.length - 1 });
- }
- else {
+ } else {
tangentLines.push({ X1: data[0].X, Y1: data[0].Y, X2: data[0].X, Y2: data[0].Y, X3: data[1].X, Y3: data[1].Y, dot1: 0, dot2: 0 });
- tangentLines.push({ X1: data[data.length - 2].X, Y1: data[data.length - 2].Y, X2: data[data.length - 1].X, Y2: data[data.length - 1].Y, X3: data[data.length - 1].X, Y3: data[data.length - 1].Y, dot1: data.length - 1, dot2: data.length - 1 });
+ tangentLines.push({
+ X1: data[data.length - 2].X,
+ Y1: data[data.length - 2].Y,
+ X2: data[data.length - 1].X,
+ Y2: data[data.length - 1].Y,
+ X3: data[data.length - 1].X,
+ Y3: data[data.length - 1].Y,
+ dot1: data.length - 1,
+ dot2: data.length - 1,
+ });
}
for (let i = 2; i < data.length - 4; i += 4) {
tangentLines.push({ X1: data[i].X, Y1: data[i].Y, X2: data[i + 1].X, Y2: data[i + 1].Y, X3: data[i + 3].X, Y3: data[i + 3].Y, dot1: i + 1, dot2: i + 2 });
@@ -94,7 +105,7 @@ export class InkTangentHandles extends React.Component<InkHandlesProps> {
return (
<>
- {tangentHandles.map((pts, i) =>
+ {tangentHandles.map((pts, i) => (
<svg height="10" width="10" key={`hdl${i}`}>
<circle
cx={pts.X}
@@ -106,25 +117,31 @@ export class InkTangentHandles extends React.Component<InkHandlesProps> {
onPointerDown={e => this.onHandleDown(e, pts.I)}
pointerEvents="all"
cursor="default"
- display={(pts.dot1 === InkStrokeProperties.Instance._currentPoint || pts.dot2 === InkStrokeProperties.Instance._currentPoint) ? "inherit" : "none"} />
- </svg>)}
+ display={pts.dot1 === InkStrokeProperties.Instance._currentPoint || pts.dot2 === InkStrokeProperties.Instance._currentPoint ? 'inherit' : 'none'}
+ />
+ </svg>
+ ))}
{tangentLines.map((pts, i) => {
- const tangentLine = (x1: number, y1: number, x2: number, y2: number) =>
+ const tangentLine = (x1: number, y1: number, x2: number, y2: number) => (
<line
x1={x1}
y1={y1}
x2={x2}
y2={y2}
stroke={Colors.MEDIUM_BLUE}
- strokeDasharray={"1 1"}
+ strokeDasharray={'1 1'}
strokeWidth={1}
- display={(pts.dot1 === InkStrokeProperties.Instance._currentPoint || pts.dot2 === InkStrokeProperties.Instance._currentPoint) ? "inherit" : "none"} />;
- return <svg height="100" width="100" key={`line${i}`}>
- {tangentLine(pts.X1, pts.Y1, pts.X2, pts.Y2)}
- {tangentLine(pts.X2, pts.Y2, pts.X3, pts.Y3)}
- </svg>;
+ display={pts.dot1 === InkStrokeProperties.Instance._currentPoint || pts.dot2 === InkStrokeProperties.Instance._currentPoint ? 'inherit' : 'none'}
+ />
+ );
+ return (
+ <svg height="100" width="100" key={`line${i}`}>
+ {tangentLine(pts.X1, pts.Y1, pts.X2, pts.Y2)}
+ {tangentLine(pts.X2, pts.Y2, pts.X3, pts.Y3)}
+ </svg>
+ );
})}
</>
);
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/InkTranscription.tsx b/src/client/views/InkTranscription.tsx
index bf0e8081d..c39fa79da 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';
@@ -67,7 +68,7 @@ export class InkTranscription extends React.Component {
: null;
}
- r.addEventListener('exported', (e: any) => this.exportInk(e, this._mathRef));
+ r?.addEventListener('exported', (e: any) => this.exportInk(e, this._mathRef));
return (this._mathRef = 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;
@@ -172,7 +173,7 @@ export class InkTranscription extends React.Component {
if (!selected) {
return;
}
- const ctx = await Cast(selected[0].context, Doc);
+ const ctx = await Cast(selected[0].embedContainer, Doc);
if (!ctx) {
return;
}
@@ -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 b83ba97e7..d0210d63b 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -6,7 +6,7 @@
point 1, tangent pt 1, tangent pt 2, point 2, point 3, tangent pt 3, ... (Note that segment endpoints are duplicated ie Point2 = Point 3)
brokenIndices - an array of indexes into the data field where the incoming and outgoing tangents are not constrained to be equal
text - a text field that will be centered within a closed ink stroke
- isInkMask - a flag that makes the ink stroke render as a mask over its collection where the stroke itself is mixBlendMode multiplied by
+ stroke_isInkMask - a flag that makes the ink stroke render as a mask over its collection where the stroke itself is mixBlendMode multiplied by
the underlying collection content, and everything outside the stroke is covered by a semi-opaque dark gray mask.
The coordinates of the ink data need to be mapped to the screen since ink points are not changed when the DocumentView is translated or scaled.
@@ -21,14 +21,16 @@
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';
-import { OmitKeys, returnFalse, setupMoveUpEvents } from '../../Utils';
+import { DashColor, returnFalse, setupMoveUpEvents } from '../../Utils';
import { CognitiveServices } from '../cognitive_services/CognitiveServices';
+import { Docs } from '../documents/Documents';
import { InteractionUtils } from '../util/InteractionUtils';
import { SnappingManager } from '../util/SnappingManager';
import { Transform } from '../util/Transform';
@@ -44,6 +46,7 @@ import { InkTangentHandles } from './InkTangentHandles';
import { DocComponentView } from './nodes/DocumentView';
import { FieldView, FieldViewProps } from './nodes/FieldView';
import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
+import { PinProps, PresBox } from './nodes/trails';
import { StyleProp } from './StyleProvider';
import Color = require('color');
@@ -54,7 +57,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
return FieldView.LayoutString(InkingStroke, fieldStr);
}
public static IsClosed(inkData: InkData) {
- return inkData && inkData.lastElement().X === inkData[0].X && inkData.lastElement().Y === inkData[0].Y;
+ return inkData?.length && inkData.lastElement().X === inkData[0].X && inkData.lastElement().Y === inkData[0].Y;
}
private _handledClick = false; // flag denoting whether ink stroke has handled a psuedo-click onPointerUp so that the real onClick event can be stopPropagated
private _disposers: { [key: string]: IReactionDisposer } = {};
@@ -78,14 +81,29 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
// fit within its panel (e.g., for content fitting views like Lightbox or multicolumn, etc)
screenToLocal = () => this.props.ScreenToLocalTransform().scale(this.props.NativeDimScaling?.() || 1);
- getAnchor = () => {
- console.log(document.activeElement);
- return this._subContentView?.getAnchor?.() || this.rootDoc;
- };
+ getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
+ if (this._subContentView) {
+ return this._subContentView.getAnchor?.(addAsAnnotation) || this.rootDoc;
+ }
- scrollFocus = (textAnchor: Doc, smooth: boolean) => {
- return this._subContentView?.scrollFocus?.(textAnchor, smooth);
+ if (!addAsAnnotation && !pinProps) return 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);
+ PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), inkable: true } }, this.rootDoc);
+ return anchor;
+ }
+ return this.rootDoc;
};
+
/**
* @returns the center of the ink stroke in the ink document's coordinate space (not screen space, and not the ink data coordinate space);
* DocumentDecorations calls getBounds() on DocumentViews which call getCenter() if defined - in the case of ink it needs to be defined since
@@ -123,12 +141,9 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
* dimmed by a semi-opaque overlay mask.
*/
public static toggleMask = action((inkDoc: Doc) => {
- inkDoc.isInkMask = !inkDoc.isInkMask;
- inkDoc._backgroundColor = inkDoc.isInkMask ? 'rgba(0,0,0,0.7)' : undefined;
- inkDoc.mixBlendMode = inkDoc.isInkMask ? 'hard-light' : undefined;
- inkDoc.color = '#9b9b9bff';
- inkDoc._stayInCollection = inkDoc.isInkMask ? true : undefined;
+ inkDoc.stroke_isInkMask = !inkDoc.stroke_isInkMask;
});
+ @observable controlUndo: UndoManager.Batch | undefined;
/**
* Drags the a simple bezier segment of the stroke.
* Also adds a control point when double clicking on the stroke.
@@ -148,15 +163,15 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
const { nearestSeg } = InkStrokeProperties.nearestPtToStroke(screenPts, { X: e.clientX, Y: e.clientY });
const controlIndex = nearestSeg;
const wasSelected = InkStrokeProperties.Instance._currentPoint === controlIndex;
- var controlUndo: UndoManager.Batch | undefined;
const isEditing = InkStrokeProperties.Instance._controlButton && this.props.isSelected();
+ this.controlUndo = undefined;
setupMoveUpEvents(
this,
e,
!isEditing
? returnFalse
: action((e: PointerEvent, down: number[], delta: number[]) => {
- if (!controlUndo) controlUndo = UndoManager.StartBatch('drag ink ctrl pt');
+ if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch('drag ink ctrl pt');
const inkMoveEnd = this.ptFromScreen({ X: delta[0], Y: delta[1] });
const inkMoveStart = this.ptFromScreen({ X: 0, Y: 0 });
InkStrokeProperties.Instance.moveControlPtHandle(inkView, inkMoveEnd.X - inkMoveStart.X, inkMoveEnd.Y - inkMoveStart.Y, controlIndex);
@@ -166,12 +181,11 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
!isEditing
? returnFalse
: action(() => {
- controlUndo?.end();
- controlUndo = undefined;
+ this.controlUndo?.end();
+ this.controlUndo = undefined;
UndoManager.FilterBatches(['data', 'x', 'y', 'width', 'height']);
}),
action((e: PointerEvent, doubleTap: boolean | undefined) => {
- doubleTap = doubleTap || this.props.docViewPath().lastElement()?.docView?._pendingDoubleClick;
if (doubleTap) {
InkStrokeProperties.Instance._controlButton = true;
InkStrokeProperties.Instance._currentPoint = -1;
@@ -234,7 +248,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
*/
inkScaledData = () => {
const inkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? [];
- const inkStrokeWidth = NumCast(this.rootDoc.strokeWidth, 1);
+ const inkStrokeWidth = NumCast(this.rootDoc.stroke_width, 1);
const inkTop = Math.min(...inkData.map(p => p.Y)) - inkStrokeWidth / 2;
const inkBottom = Math.max(...inkData.map(p => p.Y)) + inkStrokeWidth / 2;
const inkLeft = Math.min(...inkData.map(p => p.X)) - inkStrokeWidth / 2;
@@ -269,9 +283,13 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
.map(p => ({ X: p[0], Y: p[1] }));
const { distance, nearestT, nearestSeg, nearestPt } = InkStrokeProperties.nearestPtToStroke(screenPts, { X: e.clientX, Y: e.clientY });
- this._nearestT = nearestT;
- this._nearestSeg = nearestSeg;
- this._nearestScrPt = nearestPt;
+ if (distance < 40) {
+ this._nearestT = nearestT;
+ this._nearestSeg = nearestSeg;
+ this._nearestScrPt = nearestPt;
+ } else {
+ this._nearestT = this._nearestSeg = this._nearestScrPt = undefined;
+ }
};
/**
@@ -279,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
@@ -286,45 +316,37 @@ 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.strokeStartMarker);
- const endMarker = StrCast(this.layoutDoc.strokeEndMarker);
- const markerScale = NumCast(this.layoutDoc.strokeMarkerScale);
+ const startMarker = StrCast(this.layoutDoc.stroke_startMarker);
+ const endMarker = StrCast(this.layoutDoc.stroke_endMarker);
+ 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.props.docViewPath().lastElement()} 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.strokeBezier),
+ StrCast(inkDoc.stroke_lineJoin),
+ StrCast(this.layoutDoc.stroke_lineCap),
+ StrCast(inkDoc.stroke_bezier),
'none',
startMarker,
endMarker,
markerScale * Math.min(screenSpaceCenterlineStrokeWidth, screenInkWidth[0] / screenSpaceCenterlineStrokeWidth),
- StrCast(inkDoc.strokeDash),
+ StrCast(inkDoc.stroke_dash),
1,
1,
'',
@@ -332,15 +354,8 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
1.0,
false
)}
- <InkControlPtHandles
- inkView={this.props.docViewPath().lastElement()}
- inkDoc={inkDoc}
- inkCtrlPoints={inkData}
- screenCtrlPoints={screenHdlPts}
- nearestScreenPt={this.nearestScreenPt}
- screenSpaceLineWidth={screenSpaceCenterlineStrokeWidth}
- />
- <InkTangentHandles inkView={this.props.docViewPath().lastElement()} 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>
);
};
@@ -351,19 +366,19 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
TraceMobx();
const { inkData, inkStrokeWidth, inkLeft, inkTop, inkScaleX, inkScaleY, inkWidth, inkHeight } = this.inkScaledData();
- const startMarker = StrCast(this.layoutDoc.strokeStartMarker);
- const endMarker = StrCast(this.layoutDoc.strokeEndMarker);
- const markerScale = NumCast(this.layoutDoc.strokeMarkerScale, 1);
+ const startMarker = StrCast(this.layoutDoc.stroke_startMarker);
+ const endMarker = StrCast(this.layoutDoc.stroke_endMarker);
+ const markerScale = NumCast(this.layoutDoc.stroke_markerScale, 1);
const closed = InkingStroke.IsClosed(inkData);
- const isInkMask = BoolCast(this.layoutDoc.isInkMask);
- const fillColor = isInkMask ? '#aaaaaa' : StrCast(this.layoutDoc.fillColor, 'transparent');
+ 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]()));
});
}
@@ -375,14 +390,14 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
strokeColor,
inkStrokeWidth,
inkStrokeWidth,
- StrCast(this.layoutDoc.strokeLineJoin),
- StrCast(this.layoutDoc.strokeLineCap),
- StrCast(this.layoutDoc.strokeBezier),
+ StrCast(this.layoutDoc.stroke_lineJoin),
+ StrCast(this.layoutDoc.stroke_lineCap),
+ StrCast(this.layoutDoc.stroke_bezier),
!closed ? 'none' : fillColor === 'transparent' ? 'none' : fillColor,
startMarker,
endMarker,
markerScale,
- StrCast(this.layoutDoc.strokeDash),
+ StrCast(this.layoutDoc.stroke_dash),
inkScaleX,
inkScaleY,
'',
@@ -390,23 +405,25 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
1.0,
false
);
- const highlightIndex = /*BoolCast(this.props.Document.isLinkButton) && */ Doc.isBrushedHighlightedDegree(this.props.Document); // bcz: Argh!! need to identify a tree view doc better than a LayoutTemlatString
- const highlightColor = !highlightIndex
- ? StrCast(this.layoutDoc.strokeOutlineColor, !closed && fillColor && fillColor !== 'transparent' ? StrCast(this.layoutDoc.color, 'transparent') : 'transparent')
- : ['transparent', 'rgb(68, 118, 247)', 'rgb(68, 118, 247)', 'yellow', 'magenta', 'cyan', 'orange'][highlightIndex];
+ const highlight = !this.controlUndo && this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Highlighting);
+ const highlightIndex = highlight?.highlightIndex;
+ 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) =>
+ const clickableLine = (downHdlr?: (e: React.PointerEvent) => void, mask: boolean = false) =>
InteractionUtils.CreatePolyline(
inkData,
inkLeft,
inkTop,
- highlightColor,
+ mask && highlightColor === 'transparent' ? strokeColor : highlightColor,
inkStrokeWidth,
- Math.max(5, fillColor && closed && highlightIndex ? highlightIndex / 2 : inkStrokeWidth + (fillColor ? (closed ? 0 : highlightIndex + 2) : 0)),
- StrCast(this.layoutDoc.strokeLineJoin),
- StrCast(this.layoutDoc.strokeLineCap),
- StrCast(this.layoutDoc.strokeBezier),
- !closed ? 'none' : fillColor === 'transparent' || suppressFill ? 'none' : fillColor,
+ inkStrokeWidth + (fillColor ? (closed ? 2 : (highlightIndex ?? 0) + 2) : 2),
+ StrCast(this.layoutDoc.stroke_lineJoin),
+ StrCast(this.layoutDoc.stroke_lineCap),
+ StrCast(this.layoutDoc.stroke_bezier),
+ !closed || !fillColor || DashColor(fillColor).alpha() === 0 ? 'none' : fillColor,
'',
'',
markerScale,
@@ -414,12 +431,13 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
inkScaleX,
inkScaleY,
'',
- this.props.pointerEvents?.() ?? (this.rootDoc._lockedPosition ? 'none' : 'visiblepainted'),
+ this.props.pointerEvents?.() ?? 'visiblepainted',
0.0,
false,
- downHdlr
+ 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/
@@ -440,52 +458,40 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
<svg
className="inkStroke"
style={{
- transform: this.props.Document.isInkMask ? `translate(${InkingStroke.MaskDim / 2}px, ${InkingStroke.MaskDim / 2}px)` : undefined,
+ transform: isInkMask ? `rotate(-${NumCast(this.props.CollectionFreeFormDocumentView?.().Rot)}deg) translate(${InkingStroke.MaskDim / 2}px, ${InkingStroke.MaskDim / 2}px)` : undefined,
// mixBlendMode: this.layoutDoc.tool === InkTool.Highlighter ? 'multiply' : 'unset',
cursor: this.props.isSelected() ? 'default' : undefined,
}}
- {...(!closed ? interactions : {})}>
- {closed ? inkLine : clickableLine(this.onPointerDown)}
- {closed ? clickableLine(this.onPointerDown) : inkLine}
+ {...interactions}>
+ {clickableLine(this.onPointerDown, isInkMask)}
+ {isInkMask ? null : inkLine}
</svg>
- {!closed || (!RTFCast(this.rootDoc.text)?.Text && !this.props.isSelected()) ? null : (
+ {!closed || (!RTFCast(this.rootDoc.text)?.Text && (!this.props.isSelected() || Doc.UserDoc().activeInkHideTextLabels)) ? null : (
<div
className="inkStroke-text"
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,
}}>
<FormattedTextBox
- {...OmitKeys(this.props, ['children']).omit}
+ {...this.props}
+ setHeight={undefined}
setContentView={this.setSubContentView} // this makes the inkingStroke the "dominant" component - ie, it will show the inking UI when selected (not text)
yPadding={10}
xPadding={10}
- fieldKey={'text'}
+ fieldKey="text"
fontSize={fsize}
dontRegisterView={true}
noSidebar={true}
dontScale={true}
- isContentActive={this.isContentActive}
+ isContentActive={this.props.isContentActive}
/>
</div>
)}
- {!closed ? null : (
- <svg
- className="inkStroke"
- style={{
- transform: this.props.Document.isInkMask ? `translate(${InkingStroke.MaskDim / 2}px, ${InkingStroke.MaskDim / 2}px)` : undefined,
- mixBlendMode: 'unset',
- cursor: this.props.isSelected() ? 'default' : undefined,
- position: 'absolute',
- }}
- {...interactions}>
- {clickableLine(this.onPointerDown, true)}
- </svg>
- )}
</div>
);
}
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index 22b0380a2..286d39943 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -5,7 +5,7 @@ import * as React from 'react';
import { Doc, DocListCast, Opt } from '../../fields/Doc';
import { InkTool } from '../../fields/InkField';
import { Cast, NumCast, StrCast } from '../../fields/Types';
-import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from '../../Utils';
+import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnTrue } from '../../Utils';
import { DocUtils } from '../documents/Documents';
import { DocumentManager } from '../util/DocumentManager';
import { LinkManager } from '../util/LinkManager';
@@ -17,7 +17,7 @@ import { TabDocView } from './collections/TabDocView';
import { GestureOverlay } from './GestureOverlay';
import './LightboxView.scss';
import { MainView } from './MainView';
-import { DocumentView } from './nodes/DocumentView';
+import { DocumentView, OpenWhere, OpenWhereMod } from './nodes/DocumentView';
import { DefaultStyleProvider, wavyBorderPath } from './StyleProvider';
interface LightboxViewProps {
@@ -26,6 +26,13 @@ interface LightboxViewProps {
maxBorder: number[];
}
+type LightboxSavedState = {
+ panX: Opt<number>;
+ panY: Opt<number>;
+ scale: Opt<number>;
+ scrollTop: Opt<number>;
+ layout_fieldKey: Opt<string>;
+};
@observer
export class LightboxView extends React.Component<LightboxViewProps> {
@computed public static get LightboxDoc() {
@@ -33,47 +40,41 @@ export class LightboxView extends React.Component<LightboxViewProps> {
}
private static LightboxDocTemplate = () => LightboxView._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
- @observable private static _tourMap: Opt<Doc[]> = []; // list of all tours available from the current target
- private static _savedState: Opt<{ panX: Opt<number>; panY: Opt<number>; scale: Opt<number>; scrollTop: Opt<number> }>;
+ @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[]> = [];
@observable private static _docView: Opt<DocumentView>;
- private static openInTabFunc: any;
- static path: { doc: Opt<Doc>; target: Opt<Doc>; history: Opt<{ doc: Doc; target?: Doc }[]>; future: Opt<Doc[]>; saved: Opt<{ panX: Opt<number>; panY: Opt<number>; scale: Opt<number>; scrollTop: Opt<number> }> }[] = [];
- @action public static SetLightboxDoc(doc: Opt<Doc>, target?: Doc, future?: Doc[], layoutTemplate?: Doc) {
+ static path: { doc: Opt<Doc>; target: Opt<Doc>; history: Opt<{ doc: Doc; target?: Doc }[]>; future: Opt<Doc[]>; saved: Opt<LightboxSavedState> }[] = [];
+ @action public static SetLightboxDoc(doc: Opt<Doc>, target?: Doc, future?: Doc[], layoutTemplate?: Doc | string) {
if (this.LightboxDoc && this.LightboxDoc !== doc && this._savedState) {
- this.LightboxDoc._panX = this._savedState.panX;
- this.LightboxDoc._panY = this._savedState.panY;
- this.LightboxDoc._scrollTop = this._savedState.scrollTop;
- this.LightboxDoc._viewScale = this._savedState.scale;
- this.LightboxDoc._viewTransition = undefined;
+ 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._childFilters && (this._childFilters.length = 0);
this._future = this._history = [];
Doc.ActiveTool = InkTool.None;
MainView.Instance._exploreMode = false;
} else {
- if (doc) {
- const l = DocUtils.MakeLinkToActiveAudio(() => doc).lastElement();
- l && (Cast(l.anchor2, Doc, null).backgroundColor = 'lightgreen');
- }
- CollectionStackedTimeline.CurrentlyPlaying?.forEach(doc => {
- DocumentManager.Instance.getAllDocumentViews(doc).forEach(dv => {
- dv.ComponentView?.Pause?.();
- });
- });
+ 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 = {
- panX: Cast(doc._panX, 'number', null),
- panY: Cast(doc._panY, 'number', null),
- scale: Cast(doc._viewScale, 'number', null),
- scrollTop: Cast(doc._scrollTop, 'number', null),
+ 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),
};
}
}
@@ -84,19 +85,15 @@ export class LightboxView extends React.Component<LightboxViewProps> {
...future
.slice()
.sort((a, b) => NumCast(b._timecodeToShow) - NumCast(a._timecodeToShow))
- .sort((a, b) => DocListCast(a.links).length - DocListCast(b.links).length),
+ .sort((a, b) => LinkManager.Links(a).length - LinkManager.Links(b).length),
];
}
this._doc = doc;
- this._layoutTemplate = layoutTemplate;
+ this._layoutTemplate = layoutTemplate instanceof Doc ? layoutTemplate : undefined;
+ if (doc && (typeof layoutTemplate === 'string' ? layoutTemplate : undefined)) {
+ doc.layout_fieldKey = layoutTemplate;
+ }
this._docTarget = target || doc;
- this._tourMap = DocListCast(doc?.links)
- .map(link => {
- const opp = LinkManager.getOppositeAnchor(link, doc!);
- return opp?.TourMap ? opp : undefined;
- })
- .filter(m => m)
- .map(m => m!);
return true;
}
@@ -137,39 +134,39 @@ 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: string, layoutTemplate?: Doc, openInTabFunc?: any) => {
- LightboxView.openInTabFunc = openInTabFunc;
+ public static AddDocTab = (doc: Doc, location: OpenWhere, layoutTemplate?: Doc | string) => {
SelectionManager.DeselectAll();
return LightboxView.SetLightboxDoc(
doc,
undefined,
- [...DocListCast(doc[Doc.LayoutFieldKey(doc)]), ...DocListCast(doc[Doc.LayoutFieldKey(doc) + '-annotations']), ...(LightboxView._future ?? [])].sort((a: Doc, b: Doc) => NumCast(b._timecodeToShow) - NumCast(a._timecodeToShow)),
+ [...DocListCast(doc[Doc.LayoutFieldKey(doc)]), ...DocListCast(doc[Doc.LayoutFieldKey(doc) + '_annotations']).filter(anno => anno.annotationOn !== doc), ...(LightboxView._future ?? [])].sort(
+ (a: Doc, b: Doc) => NumCast(b._timecodeToShow) - NumCast(a._timecodeToShow)
+ ),
layoutTemplate
);
};
- docFilters = () => LightboxView._docFilters || [];
+ childFilters = () => LightboxView._childFilters || [];
addDocTab = LightboxView.AddDocTab;
@action public static Next() {
const doc = LightboxView._doc!;
const target = (LightboxView._docTarget = this._future?.pop());
const targetDocView = target && DocumentManager.Instance.getLightboxDocumentView(target);
if (targetDocView && target) {
- const l = DocUtils.MakeLinkToActiveAudio(() => targetDocView.ComponentView?.getAnchor?.() || target).lastElement();
- l && (Cast(l.anchor2, Doc, null).backgroundColor = 'lightgreen');
- targetDocView.focus(target, { originalTarget: target, willZoom: true, scale: 0.9 });
+ 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 (LightboxView._history?.lastElement().target !== target) LightboxView._history?.push({ doc, target });
} else {
if (!target && LightboxView.path.length) {
const saved = LightboxView._savedState;
if (LightboxView.LightboxDoc && saved) {
- LightboxView.LightboxDoc._panX = saved.panX;
- LightboxView.LightboxDoc._panY = saved.panY;
- LightboxView.LightboxDoc._viewScale = saved.scale;
- LightboxView.LightboxDoc._scrollTop = saved.scrollTop;
- LightboxView.LightboxDoc._viewTransition = undefined;
+ 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 = LightboxView.path.pop();
if (pop) {
@@ -183,13 +180,6 @@ export class LightboxView extends React.Component<LightboxViewProps> {
LightboxView.SetLightboxDoc(target);
}
}
- LightboxView._tourMap = DocListCast(LightboxView._docTarget?.links)
- .map(link => {
- const opp = LinkManager.getOppositeAnchor(link, LightboxView._docTarget!);
- return opp?.TourMap ? opp : undefined;
- })
- .filter(m => m)
- .map(m => m!);
}
@action public static Previous() {
@@ -202,19 +192,11 @@ export class LightboxView extends React.Component<LightboxViewProps> {
const docView = DocumentManager.Instance.getLightboxDocumentView(target || doc);
if (docView) {
LightboxView._docTarget = target;
- if (!target) docView.ComponentView?.shrinkWrap?.();
- else docView.focus(target, { willZoom: true, scale: 0.9 });
+ target && DocumentManager.Instance.showDocument(target, { willZoomCentered: true, zoomScale: 0.9 });
} else {
LightboxView.SetLightboxDoc(doc, target);
}
if (LightboxView._future?.lastElement() !== previous.target || previous.doc) LightboxView._future?.push(previous.target || previous.doc);
- LightboxView._tourMap = DocListCast(LightboxView._docTarget?.links)
- .map(link => {
- const opp = LinkManager.getOppositeAnchor(link, LightboxView._docTarget!);
- return opp?.TourMap ? opp : undefined;
- })
- .filter(m => m)
- .map(m => m!);
}
@action
stepInto = () => {
@@ -225,27 +207,19 @@ export class LightboxView extends React.Component<LightboxViewProps> {
history: LightboxView._history,
saved: LightboxView._savedState,
});
- const tours = LightboxView._tourMap;
- if (tours && tours.length) {
- const fieldKey = Doc.LayoutFieldKey(tours[0]);
- LightboxView._future?.push(...DocListCast(tours[0][fieldKey]).reverse());
- } else {
- const coll = LightboxView._docTarget;
- if (coll) {
- const fieldKey = Doc.LayoutFieldKey(coll);
- const contents = [...DocListCast(coll[fieldKey]), ...DocListCast(coll[fieldKey + '-annotations'])];
- const links = DocListCast(coll.links)
- .map(link => LinkManager.getOppositeAnchor(link, coll))
- .filter(doc => doc)
- .map(doc => doc!);
- LightboxView.SetLightboxDoc(coll, undefined, contents.length ? contents : links);
- TabDocView.PinDoc(coll, { hidePresBox: true });
- }
+ const coll = LightboxView._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!);
+ LightboxView.SetLightboxDoc(coll, undefined, contents.length ? contents : links);
}
};
future = () => LightboxView._future;
- tourMap = () => LightboxView._tourMap;
render() {
let downx = 0,
downy = 0;
@@ -274,36 +248,22 @@ export class LightboxView extends React.Component<LightboxViewProps> {
<GestureOverlay isActive={true}>
<DocumentView
- ref={action((r: DocumentView | null) => {
- LightboxView._docView = r !== null ? r : undefined;
- r &&
- setTimeout(
- action(() => {
- const target = LightboxView._docTarget;
- const doc = LightboxView._doc;
- //const targetView = target && DocumentManager.Instance.getLightboxDocumentView(target);
- if (doc === r.props.Document && (!target || target === doc)) r.ComponentView?.shrinkWrap?.();
- //else target?.focus(target, { willZoom: true, scale: 0.9, instant: true }); // bcz: why was this here? it breaks smooth navigation in lightbox using 'next' button
- })
- );
- })}
+ ref={action((r: DocumentView | null) => (LightboxView._docView = r !== null ? r : undefined))}
Document={LightboxView.LightboxDoc}
DataDoc={undefined}
PanelWidth={this.lightboxWidth}
PanelHeight={this.lightboxHeight}
LayoutTemplate={LightboxView.LightboxDocTemplate}
- isDocumentActive={returnFalse}
+ isDocumentActive={returnTrue} // without this being true, sidebar annotations need to be activated before text can be selected.
isContentActive={returnTrue}
styleProvider={DefaultStyleProvider}
ScreenToLocalTransform={this.lightboxScreenToLocal}
renderDepth={0}
rootSelected={returnTrue}
docViewPath={returnEmptyDoclist}
- docFilters={this.docFilters}
- docRangeFilters={returnEmptyFilter}
+ childFilters={this.childFilters}
+ childFiltersByRanges={returnEmptyFilter}
searchFilterDocs={returnEmptyDoclist}
- ContainingCollectionView={undefined}
- ContainingCollectionDoc={undefined}
addDocument={undefined}
removeDocument={undefined}
whenChildContentsActiveChanged={emptyFunction}
@@ -311,7 +271,7 @@ export class LightboxView extends React.Component<LightboxViewProps> {
pinToPres={TabDocView.PinDoc}
bringToFront={emptyFunction}
onBrowseClick={MainView.Instance.exploreMode}
- focus={DocUtils.DefaultFocus}
+ focus={emptyFunction}
/>
</GestureOverlay>
</div>
@@ -339,23 +299,22 @@ export class LightboxView extends React.Component<LightboxViewProps> {
},
this.future()?.length.toString()
)}
- <LightboxTourBtn navBtn={this.navBtn} future={this.future} stepInto={this.stepInto} tourMap={this.tourMap} />
+ <LightboxTourBtn navBtn={this.navBtn} future={this.future} stepInto={this.stepInto} />
<div
className="lightboxView-navBtn"
- title={'toggle fit width'}
+ title="toggle fit width"
onClick={e => {
e.stopPropagation();
- LightboxView.LightboxDoc!._fitWidth = !LightboxView.LightboxDoc!._fitWidth;
+ LightboxView.LightboxDoc!._layout_fitWidth = !LightboxView.LightboxDoc!._layout_fitWidth;
}}>
- <FontAwesomeIcon icon={LightboxView.LightboxDoc?._fitWidth ? 'arrows-alt-h' : 'arrows-alt-v'} size="2x" />
+ <FontAwesomeIcon icon={LightboxView.LightboxDoc?._layout_fitWidth ? 'arrows-alt-h' : 'arrows-alt-v'} size="2x" />
</div>
<div
className="lightboxView-tabBtn"
- title={'open in tab'}
+ title="open in tab"
onClick={e => {
e.stopPropagation();
- CollectionDockingView.AddSplit(LightboxView._docTarget || LightboxView._doc!, '');
- //LightboxView.openInTabFunc(LightboxView._docTarget || LightboxView._doc!, "inPlace");
+ CollectionDockingView.AddSplit(LightboxView._docTarget || LightboxView._doc!, OpenWhereMod.none);
SelectionManager.DeselectAll();
LightboxView.SetLightboxDoc(undefined);
}}>
@@ -387,7 +346,6 @@ export class LightboxView extends React.Component<LightboxViewProps> {
}
interface LightboxTourBtnProps {
navBtn: (left: Opt<string | number>, bottom: Opt<number>, top: number, icon: string, display: () => string, click: (e: React.MouseEvent) => void, color?: string) => JSX.Element;
- tourMap: () => Opt<Doc[]>;
future: () => Opt<Doc[]>;
stepInto: () => void;
}
@@ -404,7 +362,7 @@ export class LightboxTourBtn extends React.Component<LightboxTourBtnProps> {
e.stopPropagation();
this.props.stepInto();
},
- StrCast(this.props.tourMap()?.lastElement()?.TourMap)
+ ''
);
}
}
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index a1d221af2..b0b757388 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -5,6 +5,7 @@
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';
@@ -18,9 +19,12 @@ import { PingManager } from '../util/PingManager';
dotenv.config();
AssignAllExtensions();
+FieldLoader.ServerLoadStatus = { requested: 0, retrieved: 0 }; // bcz: not sure why this is needed to get the code loaded properly...
(async () => {
MainView.Live = window.location.search.includes('live');
+ const root = ReactDOM.createRoot(document.getElementById('root')!);
+ root.render(<FieldLoader />);
window.location.search.includes('safe') && CollectionView.SetSafeMode(true);
const info = await CurrentUserUtils.loadCurrentUser();
if (info.email === 'guest') DocServer.Control.makeReadOnly();
@@ -46,6 +50,6 @@ AssignAllExtensions();
new TrackMovements();
new ReplayMovements();
new PingManager();
- ReactDOM.createRoot(document.getElementById('root')!).render(<MainView />);
+ root.render(<MainView />);
}, 0);
})();
diff --git a/src/client/views/MainView.scss b/src/client/views/MainView.scss
index 069206126..b95ce0e99 100644
--- a/src/client/views/MainView.scss
+++ b/src/client/views/MainView.scss
@@ -1,5 +1,12 @@
@import 'global/globalCssVariables';
@import 'nodeModuleOverrides';
+html {
+ overscroll-behavior-x: none;
+}
+body {
+ overscroll-behavior-x: none;
+}
+
h1,
.h1 {
// reverts change to h1 made by normalize.css
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 5be5c3588..fbe44cf7a 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -3,18 +3,18 @@ import { faBuffer, faHireAHelper } from '@fortawesome/free-brands-svg-icons';
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 { observer } from 'mobx-react';
import 'normalize.css';
import * as React from 'react';
-import * as ReactDOM from 'react-dom/client';
import { Doc, DocListCast, Opt } from '../../fields/Doc';
import { ScriptField } from '../../fields/ScriptField';
import { DocCast, StrCast } from '../../fields/Types';
-import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, returnZero, setupMoveUpEvents, simulateMouseClick, Utils } from '../../Utils';
+import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, returnZero, setupMoveUpEvents, Utils } from '../../Utils';
import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager';
import { DocServer } from '../DocServer';
-import { Docs, DocUtils } from '../documents/Documents';
+import { Docs } from '../documents/Documents';
import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
import { CaptureManager } from '../util/CaptureManager';
import { DocumentManager } from '../util/DocumentManager';
@@ -22,12 +22,15 @@ import { GroupManager } from '../util/GroupManager';
import { HistoryUtil } from '../util/History';
import { Hypothesis } from '../util/HypothesisUtils';
import { ReportManager } from '../util/ReportManager';
+import { RTFMarkup } from '../util/RTFMarkup';
import { ScriptingGlobals } from '../util/ScriptingGlobals';
import { SelectionManager } from '../util/SelectionManager';
+import { ServerStats } from '../util/ServerStats';
import { ColorScheme, SettingsManager } from '../util/SettingsManager';
import { SharingManager } from '../util/SharingManager';
import { SnappingManager } from '../util/SnappingManager';
import { Transform } from '../util/Transform';
+import { UndoManager } from '../util/UndoManager';
import { TimelineMenu } from './animationtimeline/TimelineMenu';
import { CollectionDockingView } from './collections/CollectionDockingView';
import { MarqueeOptionsMenu } from './collections/collectionFreeForm/MarqueeOptionsMenu';
@@ -40,7 +43,7 @@ import { DashboardView } from './DashboardView';
import { DictationOverlay } from './DictationOverlay';
import { DocumentDecorations } from './DocumentDecorations';
import { GestureOverlay } from './GestureOverlay';
-import { DASHBOARD_SELECTOR_HEIGHT, LEFT_MENU_WIDTH } from './global/globalCssVariables.scss';
+import { LEFT_MENU_WIDTH, TOPBAR_HEIGHT } from './global/globalCssVariables.scss';
import { Colors } from './global/globalEnums';
import { KeyManager } from './GlobalKeyHandler';
import { InkTranscription } from './InkTranscription';
@@ -49,7 +52,7 @@ import { LinkMenu } from './linking/LinkMenu';
import './MainView.scss';
import { AudioBox } from './nodes/AudioBox';
import { DocumentLinksButton } from './nodes/DocumentLinksButton';
-import { DocumentView } from './nodes/DocumentView';
+import { DocumentView, DocumentViewInternal, OpenWhere, OpenWhereMod } from './nodes/DocumentView';
import { DashFieldViewMenu } from './nodes/formattedText/DashFieldView';
import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
import { RichTextMenu } from './nodes/formattedText/RichTextMenu';
@@ -57,8 +60,6 @@ import { LinkDescriptionPopup } from './nodes/LinkDescriptionPopup';
import { LinkDocPreview } from './nodes/LinkDocPreview';
import { RadialMenu } from './nodes/RadialMenu';
import { TaskCompletionBox } from './nodes/TaskCompletedBox';
-import { PresBox } from './nodes/trails';
-import { WebBox } from './nodes/WebBox';
import { OverlayView } from './OverlayView';
import { AnchorMenu } from './pdf/AnchorMenu';
import { PreviewCursor } from './PreviewCursor';
@@ -83,14 +84,14 @@ 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() {
return this._hideUI ? 0 : 27;
} // 27 comes form lm.config.defaultConfig.dimensions.headerHeight in goldenlayout.js
@computed private get topOfDashUI() {
- return this._hideUI || LightboxView.LightboxDoc ? 0 : Number(DASHBOARD_SELECTOR_HEIGHT.replace('px', ''));
+ return this._hideUI || LightboxView.LightboxDoc ? 0 : Number(TOPBAR_HEIGHT.replace('px', ''));
}
@computed private get topOfHeaderBarDoc() {
return this.topOfDashUI;
@@ -151,7 +152,7 @@ export class MainView extends React.Component {
if (ele && prog) {
// remove from DOM
setTimeout(() => {
- clearTimeout();
+ //clearTimeout();
prog.style.transition = '1s';
prog.style.width = '100%';
}, 0);
@@ -212,11 +213,11 @@ export class MainView extends React.Component {
document.addEventListener('dash', (e: any) => {
// event used by chrome plugin to tell Dash which document to focus on
const id = FormattedTextBox.GetDocFromUrl(e.detail);
- DocServer.GetRefField(id).then(doc => (doc instanceof Doc ? DocumentManager.Instance.jumpToDocument(doc, false, undefined, []) : null));
+ DocServer.GetRefField(id).then(doc => (doc instanceof Doc ? DocumentManager.Instance.showDocument(doc, { willPan: false }) : null));
});
document.addEventListener('linkAnnotationToDash', Hypothesis.linkListener);
this.initEventListeners();
- }
+ };
if (PingManager.Instance.isBeating) {
wrapper();
@@ -233,19 +234,21 @@ export class MainView extends React.Component {
}
);
}
-
}
componentWillUnMount() {
window.removeEventListener('keyup', KeyManager.Instance.unhandle);
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('paste', KeyManager.Instance.paste as any);
document.removeEventListener('linkAnnotationToDash', Hypothesis.linkListener);
}
constructor(props: Readonly<{}>) {
super(props);
+ DocumentViewInternal.addDocTabFunc = MainView.addDocTabFunc_impl;
MainView.Instance = this;
DashboardView._urlState = HistoryUtil.parseUrl(window.location) || ({} as any);
@@ -255,11 +258,9 @@ export class MainView extends React.Component {
if (window.location.pathname !== '/home') {
const pathname = window.location.pathname.substr(1).split('/');
if (pathname.length > 1 && pathname[0] === 'doc') {
- Doc.MainDocId = pathname[1];
- //!this.userDoc &&
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;
}
})
@@ -269,12 +270,16 @@ export class MainView extends React.Component {
library.add(
...[
+ fa.faExclamationCircle,
fa.faEdit,
+ fa.faArrowDownShortWide,
fa.faTrash,
fa.faTrashAlt,
fa.faShare,
+ fa.faTaxi,
fa.faDownload,
fa.faExpandArrowsAlt,
+ fa.faAmbulance,
fa.faLayerGroup,
fa.faExternalLinkAlt,
fa.faCalendar,
@@ -283,6 +288,7 @@ export class MainView extends React.Component {
fa.faConciergeBell,
fa.faWindowRestore,
fa.faFolder,
+ fa.faFolderOpen,
fa.faMapPin,
fa.faMapMarker,
fa.faFingerprint,
@@ -293,10 +299,12 @@ export class MainView extends React.Component {
fa.faLaptopCode,
fa.faMale,
fa.faCopy,
+ fa.faHome,
fa.faHandPointLeft,
fa.faHandPointRight,
fa.faCompass,
fa.faSnowflake,
+ fa.faStar,
fa.faMicrophone,
fa.faKeyboard,
fa.faQuestion,
@@ -334,6 +342,8 @@ export class MainView extends React.Component {
fa.faArrowUp,
fa.faBolt,
fa.faBullseye,
+ fa.faTurnUp,
+ fa.faTurnDown,
fa.faCaretUp,
fa.faCat,
fa.faCheck,
@@ -344,6 +354,7 @@ export class MainView extends React.Component {
fa.faClone,
fa.faCloudUploadAlt,
fa.faCommentAlt,
+ fa.faCommentDots,
fa.faCompressArrowsAlt,
fa.faCut,
fa.faEllipsisV,
@@ -502,12 +513,33 @@ export class MainView extends React.Component {
fa.faUserCircle,
fa.faHeart,
fa.faHeartBroken,
+ fa.faHighlighter,
+ fa.faRemoveFormat,
+ fa.faHandPointUp,
+ fa.faXRay,
+ fa.faZ,
+ fa.faArrowsUpToLine,
+ fa.faArrowsDownToLine,
]
);
- this.initAuthenticationRouters();
}
+ private longPressTimer: NodeJS.Timeout | undefined;
+ globalPointerClick = action((e: any) => {
+ this.longPressTimer && clearTimeout(this.longPressTimer);
+ DocumentView.LongPress = false;
+ });
+ globalPointerMove = action((e: PointerEvent) => {
+ if (e.movementX > 3 || e.movementY > 3) this.longPressTimer && clearTimeout(this.longPressTimer);
+ });
globalPointerDown = action((e: PointerEvent) => {
+ DocumentView.LongPress = false;
+ this.longPressTimer = setTimeout(
+ action(() => (DocumentView.LongPress = true)),
+ 1000
+ );
+ DocumentManager.removeOverlayViews();
+ Doc.linkFollowUnhighlight();
AudioBox.Enabled = true;
const targets = document.elementsFromPoint(e.x, e.y);
if (targets.length) {
@@ -522,10 +554,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(
'click',
(e: MouseEvent) => {
@@ -541,27 +576,10 @@ export class MainView extends React.Component {
document.oncontextmenu = () => false;
};
- initAuthenticationRouters = async () => {
- const received = Doc.MainDocId;
- if (received && !this.userDoc) {
- reaction(
- () => Doc.GuestTarget,
- target => target && DashboardView.createNewDashboard(),
- { fireImmediately: true }
- );
- }
- // else {
- // PromiseValue(this.userDoc.activeDashboard).then(dash => {
- // if (dash instanceof Doc) DashboardView.openDashboard(dash);
- // else Doc.createNewDashboard();
- // });
- // }
- };
-
@action
createNewPresentation = () => {
const pres = Doc.MakeCopy(Doc.UserDoc().emptyTrail as Doc, true);
- CollectionDockingView.AddSplit(pres, 'right');
+ CollectionDockingView.AddSplit(pres, OpenWhereMod.right);
Doc.MyTrails && Doc.AddDocToList(Doc.MyTrails, 'data', pres); // Doc.MyTrails should be created in createDashboard
Doc.ActivePresentation = pres;
};
@@ -569,18 +587,16 @@ export class MainView extends React.Component {
@action
openPresentation = (pres: Doc) => {
if (pres.type === DocumentType.PRES) {
- CollectionDockingView.AddSplit(pres, 'right');
+ CollectionDockingView.AddSplit(pres, OpenWhereMod.right);
Doc.MyTrails && (Doc.ActivePresentation = pres);
Doc.AddDocToList(Doc.MyTrails, 'data', pres);
this.closeFlyout();
- } else {
- PresBox.NavigateToDoc(DocCast(pres.presentationTargetDoc), pres);
}
};
@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);
};
@@ -588,8 +604,11 @@ export class MainView extends React.Component {
@computed get exploreMode() {
return () => (this._exploreMode ? ScriptField.MakeScript('CollectionBrowseClick(documentView, clientX, clientY)', { documentView: 'any', clientX: 'number', clientY: 'number' })! : undefined);
}
+ 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() }}>
@@ -597,31 +616,30 @@ export class MainView extends React.Component {
key="headerBarDoc"
Document={this.headerBarDoc}
DataDoc={undefined}
- addDocument={undefined}
- addDocTab={this.addDocTabFunc}
+ 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}
PanelHeight={this.headerBarDocHeight}
renderDepth={0}
- focus={DocUtils.DefaultFocus}
+ focus={emptyFunction}
whenChildContentsActiveChanged={emptyFunction}
bringToFront={emptyFunction}
- docFilters={returnEmptyFilter}
- docRangeFilters={returnEmptyFilter}
+ childFilters={returnEmptyFilter}
+ childFiltersByRanges={returnEmptyFilter}
searchFilterDocs={returnEmptyDoclist}
- ContainingCollectionView={undefined}
- ContainingCollectionDoc={undefined}
/>
</div>
);
@@ -629,13 +647,13 @@ export class MainView extends React.Component {
@computed get mainDocView() {
return (
<>
- {this._hideUI ? null : this.headerBarDocView}
+ {this._hideUI || !this.headerBarDocHeight?.() ? null : this.headerBarDocView}
<DocumentView
key="main"
Document={this.mainContainer!}
DataDoc={undefined}
addDocument={undefined}
- addDocTab={this.addDocTabFunc}
+ addDocTab={DocumentViewInternal.addDocTabFunc}
pinToPres={emptyFunction}
docViewPath={returnEmptyDoclist}
styleProvider={this._hideUI ? DefaultStyleProvider : undefined}
@@ -645,14 +663,12 @@ export class MainView extends React.Component {
ScreenToLocalTransform={this._hideUI ? this.mainScreenToLocalXf : Transform.Identity}
PanelWidth={this.mainDocViewWidth}
PanelHeight={this.mainDocViewHeight}
- focus={DocUtils.DefaultFocus}
+ focus={emptyFunction}
whenChildContentsActiveChanged={emptyFunction}
bringToFront={emptyFunction}
- docFilters={returnEmptyFilter}
- docRangeFilters={returnEmptyFilter}
+ childFilters={returnEmptyFilter}
+ childFiltersByRanges={returnEmptyFilter}
searchFilterDocs={returnEmptyDoclist}
- ContainingCollectionView={undefined}
- ContainingCollectionDoc={undefined}
suppressSetHeight={true}
renderDepth={this._hideUI ? 0 : -1}
/>
@@ -706,20 +722,18 @@ export class MainView extends React.Component {
sidebarScreenToLocal = () => new Transform(0, -this.topOfSidebarDoc, 1);
mainContainerXf = () => this.sidebarScreenToLocal().translate(-this.leftScreenOffsetOfMainDocView, 0);
- addDocTabFunc = (doc: Doc, location: string): boolean => {
- const locationFields = doc._viewType === CollectionViewType.Docking ? ['dashboard'] : location.split(':');
- const locationParams = locationFields.length > 1 ? locationFields[1] : '';
- if (doc.dockingConfig) return DashboardView.openDashboard(doc);
+ static addDocTabFunc_impl = (doc: Doc, location: OpenWhere): boolean => {
+ 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 && !keyValue) return DashboardView.openDashboard(doc);
// prettier-ignore
- switch (locationFields[0]) {
- default:
- case 'inPlace':
- case 'add': return CollectionDockingView.AddSplit(doc, locationParams);
- case 'dashboard': return DashboardView.openDashboard(doc);
- case 'close': return CollectionDockingView.CloseSplit(doc, locationParams);
- case 'fullScreen': return CollectionDockingView.OpenFullScreen(doc);
- case 'lightbox': return LightboxView.AddDocTab(doc, location);
- case 'toggle': return CollectionDockingView.ToggleSplit(doc, locationParams);
+ switch (whereFields[0]) {
+ case OpenWhere.lightbox: return LightboxView.AddDocTab(doc, location);
+ 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,7 +749,7 @@ export class MainView extends React.Component {
Document={this._sidebarContent.proto || this._sidebarContent}
DataDoc={undefined}
addDocument={undefined}
- addDocTab={this.addDocTabFunc}
+ addDocTab={DocumentViewInternal.addDocTabFunc}
pinToPres={emptyFunction}
docViewPath={returnEmptyDoclist}
styleProvider={this._sidebarContent.proto === Doc.MyDashboards || this._sidebarContent.proto === Doc.MyFilesystem ? DashboardStyleProvider : DefaultStyleProvider}
@@ -747,14 +761,12 @@ export class MainView extends React.Component {
renderDepth={0}
isContentActive={returnTrue}
scriptContext={CollectionDockingView.Instance?.props.Document}
- focus={DocUtils.DefaultFocus}
+ focus={emptyFunction}
whenChildContentsActiveChanged={emptyFunction}
bringToFront={emptyFunction}
- docFilters={returnEmptyFilter}
- docRangeFilters={returnEmptyFilter}
+ childFilters={returnEmptyFilter}
+ childFiltersByRanges={returnEmptyFilter}
searchFilterDocs={returnEmptyDoclist}
- ContainingCollectionView={undefined}
- ContainingCollectionDoc={undefined}
/>
</div>
{this.docButtons}
@@ -769,7 +781,7 @@ export class MainView extends React.Component {
Document={Doc.MyLeftSidebarMenu}
DataDoc={undefined}
addDocument={undefined}
- addDocTab={this.addDocTabFunc}
+ addDocTab={DocumentViewInternal.addDocTabFunc}
pinToPres={emptyFunction}
rootSelected={returnTrue}
removeDocument={returnFalse}
@@ -778,16 +790,14 @@ export class MainView extends React.Component {
PanelHeight={this.leftMenuHeight}
renderDepth={0}
docViewPath={returnEmptyDoclist}
- focus={DocUtils.DefaultFocus}
+ focus={emptyFunction}
styleProvider={DefaultStyleProvider}
isContentActive={returnTrue}
whenChildContentsActiveChanged={emptyFunction}
bringToFront={emptyFunction}
- docFilters={returnEmptyFilter}
- docRangeFilters={returnEmptyFilter}
+ childFilters={returnEmptyFilter}
+ childFiltersByRanges={returnEmptyFilter}
searchFilterDocs={returnEmptyDoclist}
- ContainingCollectionView={undefined}
- ContainingCollectionDoc={undefined}
scriptContext={this}
/>
</div>
@@ -833,7 +843,7 @@ export class MainView extends React.Component {
</div>
)}
<div className="properties-container" style={{ width: this.propertiesWidth() }}>
- {this.propertiesWidth() < 10 ? null : <PropertiesView styleProvider={DefaultStyleProvider} addDocTab={this.addDocTabFunc} width={this.propertiesWidth()} height={this.propertiesHeight()} />}
+ {this.propertiesWidth() < 10 ? null : <PropertiesView styleProvider={DefaultStyleProvider} addDocTab={DocumentViewInternal.addDocTabFunc} width={this.propertiesWidth()} height={this.propertiesHeight()} />}
</div>
</div>
</div>
@@ -872,7 +882,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;
});
@@ -883,7 +893,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);
@@ -895,12 +905,12 @@ export class MainView extends React.Component {
@computed get docButtons() {
return !Doc.MyDockedBtns ? null : (
- <div className="mainView-docButtons" ref={this._docBtnRef} style={{ height: !Doc.MyDockedBtns.linearViewIsExpanded ? '42px' : undefined }}>
+ <div className="mainView-docButtons" ref={this._docBtnRef}>
<CollectionLinearView
Document={Doc.MyDockedBtns}
DataDoc={undefined}
- fieldKey={'data'}
- dropAction={'alias'}
+ fieldKey="data"
+ dropAction="embed"
setHeight={returnFalse}
styleProvider={DefaultStyleProvider}
rootSelected={returnTrue}
@@ -911,36 +921,33 @@ export class MainView extends React.Component {
isSelected={returnFalse}
docViewPath={returnEmptyDoclist}
moveDocument={this.moveButtonDoc}
- CollectionView={undefined}
addDocument={this.addButtonDoc}
- addDocTab={this.addDocTabFunc}
+ addDocTab={DocumentViewInternal.addDocTabFunc}
pinToPres={emptyFunction}
removeDocument={this.remButtonDoc}
ScreenToLocalTransform={this.buttonBarXf}
PanelWidth={this.leftMenuFlyoutWidth}
PanelHeight={this.leftMenuFlyoutHeight}
renderDepth={0}
- focus={DocUtils.DefaultFocus}
+ focus={emptyFunction}
whenChildContentsActiveChanged={emptyFunction}
- docFilters={returnEmptyFilter}
- docRangeFilters={returnEmptyFilter}
+ childFilters={returnEmptyFilter}
+ childFiltersByRanges={returnEmptyFilter}
searchFilterDocs={returnEmptyDoclist}
- ContainingCollectionView={undefined}
- ContainingCollectionDoc={undefined}
/>
{['watching', 'recording'].includes(String(this.userDoc?.presentationMode) ?? '') ? <div style={{ border: '.5rem solid green', padding: '5px' }}>{StrCast(this.userDoc?.presentationMode)}</div> : <></>}
</div>
);
}
@computed get snapLines() {
- return !SnappingManager.GetShowSnapLines() ? null : (
+ 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>
@@ -972,6 +979,10 @@ export class MainView extends React.Component {
);
}
+ @computed get linkDocPreview() {
+ return LinkDocPreview.LinkInfo ? <LinkDocPreview {...LinkDocPreview.LinkInfo} /> : null;
+ }
+
render() {
return (
<div
@@ -989,6 +1000,8 @@ export class MainView extends React.Component {
{this.inkResources}
<DictationOverlay />
<SharingManager />
+ <ServerStats />
+ <RTFMarkup />
<SettingsManager />
<ReportManager />
<CaptureManager />
@@ -999,7 +1012,7 @@ export class MainView extends React.Component {
{this._hideUI ? null : <TopBar />}
{LinkDescriptionPopup.descriptionPopup ? <LinkDescriptionPopup /> : null}
{DocumentLinksButton.LinkEditorDocView ? <LinkMenu clearLinkEditor={action(() => (DocumentLinksButton.LinkEditorDocView = undefined))} docView={DocumentLinksButton.LinkEditorDocView} /> : null}
- {LinkDocPreview.LinkInfo ? <LinkDocPreview {...LinkDocPreview.LinkInfo} /> : null}
+ {this.linkDocPreview}
{((page: string) => {
// prettier-ignore
diff --git a/src/client/views/MainViewModal.tsx b/src/client/views/MainViewModal.tsx
index 55dee005d..32997a944 100644
--- a/src/client/views/MainViewModal.tsx
+++ b/src/client/views/MainViewModal.tsx
@@ -1,5 +1,5 @@
import * as React from 'react';
-import "./MainViewModal.scss";
+import './MainViewModal.scss';
import { observer } from 'mobx-react';
export interface MainViewOverlayProps {
@@ -15,31 +15,38 @@ export interface MainViewOverlayProps {
@observer
export class MainViewModal extends React.Component<MainViewOverlayProps> {
-
render() {
const p = this.props;
const dialogueOpacity = p.dialogueBoxDisplayedOpacity || 1;
const overlayOpacity = p.overlayDisplayedOpacity || 0.4;
- return !p.isDisplayed ? (null) : (
- <div className="mainViewModal-cont" style={{
- pointerEvents: p.isDisplayed && p.interactive ? "all" : "none"
- }}>
- <div className="dialogue-box" style={{
- borderColor: "black",
- ...(p.dialogueBoxStyle || {}),
- opacity: p.isDisplayed ? dialogueOpacity : 0
- }} >
+ return !p.isDisplayed ? null : (
+ <div
+ className="mainViewModal-cont"
+ style={{
+ pointerEvents: p.isDisplayed && p.interactive ? 'all' : 'none',
+ }}>
+ <div
+ className="dialogue-box"
+ style={{
+ borderColor: 'black',
+ height: 'max-content',
+ overflow: 'auto',
+ maxHeight: '80%',
+ ...(p.dialogueBoxStyle || {}),
+ opacity: p.isDisplayed ? dialogueOpacity : 0,
+ }}>
{p.contents}
</div>
- <div className="overlay"
+ <div
+ className="overlay"
onClick={this.props?.closeOnExternalClick}
style={{
- backgroundColor: "black",
+ backgroundColor: 'black',
...(p.overlayStyle || {}),
- opacity: p.isDisplayed ? overlayOpacity : 0
+ opacity: p.isDisplayed ? overlayOpacity : 0,
}}
/>
</div>
);
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx
index ce0e58d90..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';
@@ -8,7 +9,8 @@ import { GetEffectiveAcl } from '../../fields/util';
import { unimplementedFunction, Utils } from '../../Utils';
import { Docs, DocUtils } from '../documents/Documents';
import { DragManager } from '../util/DragManager';
-import { undoBatch } from '../util/UndoManager';
+import { FollowLinkScript } from '../util/LinkFollower';
+import { undoBatch, UndoManager } from '../util/UndoManager';
import './MarqueeAnnotator.scss';
import { DocumentView } from './nodes/DocumentView';
import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
@@ -18,7 +20,7 @@ const _global = (window /* browser */ || global) /* node */ as any;
export interface MarqueeAnnotatorProps {
rootDoc: Doc;
- down: number[];
+ down?: number[];
iframe?: () => undefined | HTMLIFrameElement;
scrollTop: number;
scaling?: () => number;
@@ -34,6 +36,7 @@ export interface MarqueeAnnotatorProps {
finishMarquee: (x?: number, y?: number, PointerEvent?: PointerEvent) => void;
anchorMenuClick?: () => undefined | ((anchor: Doc) => void);
anchorMenuCrop?: (anchor: Doc | undefined, addCrop: boolean) => Doc | undefined;
+ highlightDragSrcColor?: string;
}
@observer
export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
@@ -44,6 +47,21 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
@observable private _width: number = 0;
@observable private _height: number = 0;
+ constructor(props: any) {
+ super(props);
+
+ AnchorMenu.Instance.OnCrop = (e: PointerEvent) => {
+ if (this.props.anchorMenuCrop) {
+ UndoManager.RunInBatch(() => this.props.anchorMenuCrop?.(this.highlight('', true, undefined, false), true), 'cropping');
+ }
+ };
+ AnchorMenu.Instance.OnClick = (e: PointerEvent) => this.props.anchorMenuClick?.()?.(this.highlight(this.props.highlightDragSrcColor ?? 'rgba(173, 216, 230, 0.75)', true, undefined, true));
+ AnchorMenu.Instance.OnAudio = unimplementedFunction;
+ AnchorMenu.Instance.Highlight = this.highlight;
+ AnchorMenu.Instance.GetAnchor = (savedAnnotations?: ObservableMap<number, HTMLDivElement[]>, addAsAnnotation?: boolean) => this.highlight('rgba(173, 216, 230, 0.75)', true, savedAnnotations, true);
+ AnchorMenu.Instance.onMakeAnchor = () => AnchorMenu.Instance.GetAnchor(undefined, true);
+ }
+
@action
static clearAnnotations(savedAnnotations: ObservableMap<number, HTMLDivElement[]>) {
AnchorMenu.Instance.Status = 'marquee';
@@ -53,24 +71,21 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
savedAnnotations.clear();
}
- @action componentDidMount() {
- // set marquee x and y positions to the spatially transformed position
- const boundingRect = this.props.mainCont.getBoundingClientRect();
- this._startX = this._left = (this.props.down[0] - boundingRect.left) * (this.props.mainCont.offsetWidth / boundingRect.width);
- this._startY = this._top = (this.props.down[1] - boundingRect.top) * (this.props.mainCont.offsetHeight / boundingRect.height) + this.props.mainCont.scrollTop;
- this._height = this._width = 0;
+ @action gotDownPoint() {
+ if (!this._width && !this._height) {
+ const downPt = this.props.down!;
+ // set marquee x and y positions to the spatially transformed position
+ const boundingRect = this.props.mainCont.getBoundingClientRect();
+ this._startX = this._left = (downPt[0] - boundingRect.left) * (this.props.mainCont.offsetWidth / boundingRect.width);
+ this._startY = this._top = (downPt[1] - boundingRect.top) * (this.props.mainCont.offsetHeight / boundingRect.height) + this.props.mainCont.scrollTop;
+ }
const doc = this.props.iframe?.()?.contentDocument ?? document;
+ doc.removeEventListener('pointermove', this.onSelectMove);
+ doc.removeEventListener('pointerup', this.onSelectEnd);
doc.addEventListener('pointermove', this.onSelectMove);
doc.addEventListener('pointerup', this.onSelectEnd);
- AnchorMenu.Instance.OnCrop = (e: PointerEvent) => this.props.anchorMenuCrop?.(this.highlight('rgba(173, 216, 230, 0.75)', true), true);
- AnchorMenu.Instance.OnClick = (e: PointerEvent) => this.props.anchorMenuClick?.()?.(this.highlight('rgba(173, 216, 230, 0.75)', true));
- AnchorMenu.Instance.OnAudio = unimplementedFunction;
- AnchorMenu.Instance.Highlight = this.highlight;
- AnchorMenu.Instance.GetAnchor = (savedAnnotations?: ObservableMap<number, HTMLDivElement[]>) => this.highlight('rgba(173, 216, 230, 0.75)', true, savedAnnotations);
- AnchorMenu.Instance.onMakeAnchor = AnchorMenu.Instance.GetAnchor;
-
/**
* This function is used by the AnchorMenu to create an anchor highlight and a new linked text annotation.
* It also initiates a Drag/Drop interaction to place the text annotation.
@@ -78,11 +93,8 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
AnchorMenu.Instance.StartDrag = action((e: PointerEvent, ele: HTMLElement) => {
e.preventDefault();
e.stopPropagation();
- const sourceAnchorCreator = () => {
- const annoDoc = this.highlight('rgba(173, 216, 230, 0.75)', true); // hyperlink color
- annoDoc && this.props.addDocument(annoDoc);
- return annoDoc;
- };
+ const sourceAnchorCreator = () => this.highlight(this.props.highlightDragSrcColor ?? 'rgba(173, 216, 230, 0.75)', true, undefined, true); // hyperlink color
+
const targetCreator = (annotationOn: Doc | undefined) => {
const target = DocUtils.GetNewTextDoc('Note linked to ' + this.props.rootDoc.title, 0, 0, 100, 100, undefined, annotationOn, undefined, 'yellow');
FormattedTextBox.SelectOnLoad = target[Id];
@@ -91,7 +103,8 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.props.docView, sourceAnchorCreator, targetCreator), e.pageX, e.pageY, {
dragComplete: e => {
if (!e.aborted && e.annoDragData && e.annoDragData.linkSourceDoc && e.annoDragData.dropDocument && e.linkDocument) {
- e.annoDragData.linkSourceDoc.isPushpin = e.annoDragData.dropDocument.annotationOn === this.props.rootDoc;
+ e.annoDragData.linkSourceDoc.followLinkToggle = e.annoDragData.dropDocument.annotationOn === this.props.rootDoc;
+ e.annoDragData.linkSourceDoc.followLinkZoom = false;
}
},
});
@@ -106,23 +119,20 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
e.preventDefault();
e.stopPropagation();
var cropRegion: Doc | undefined;
- const sourceAnchorCreator = () => {
- cropRegion = this.highlight('rgba(173, 216, 230, 0.75)', true); // hyperlink color
- cropRegion && this.props.addDocument(cropRegion);
- return cropRegion;
- };
+ const sourceAnchorCreator = () => (cropRegion = this.highlight('', true, undefined, true)); // hyperlink color
const targetCreator = (annotationOn: Doc | undefined) => this.props.anchorMenuCrop!(cropRegion, false)!;
DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.props.docView, sourceAnchorCreator, targetCreator), e.pageX, e.pageY, {
dragComplete: e => {
if (!e.aborted && e.linkDocument) {
- Doc.GetProto(e.linkDocument).linkRelationship = 'cropped image';
+ Doc.GetProto(e.linkDocument).link_relationship = 'cropped image';
Doc.GetProto(e.linkDocument).title = 'crop: ' + this.props.docView.rootDoc.title;
+ Doc.GetProto(e.linkDocument).link_displayLine = false;
}
},
});
});
}
- componentWillUnmount() {
+ releaseDownPt() {
const doc = this.props.iframe?.()?.contentDocument ?? document;
doc.removeEventListener('pointermove', this.onSelectMove);
doc.removeEventListener('pointerup', this.onSelectEnd);
@@ -138,17 +148,30 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
const scale = this.props.scaling?.() || 1;
const anno = savedAnnos[0];
const containerOffset = this.props.containerOffset?.() || [0, 0];
- const marqueeAnno = Docs.Create.FreeformDocument([], { _isLinkButton: isLinkButton, backgroundColor: color, annotationOn: this.props.rootDoc, title: 'Annotation on ' + this.props.rootDoc.title });
- marqueeAnno.x = NumCast(this.props.docView.props.Document.panXMin) + (parseInt(anno.style.left || '0') - containerOffset[0]) / scale / NumCast(this.props.docView.props.Document._viewScale, 1);
- marqueeAnno.y = NumCast(this.props.docView.props.Document.panYMin) + (parseInt(anno.style.top || '0') - containerOffset[1]) / scale / NumCast(this.props.docView.props.Document._viewScale, 1) + NumCast(this.props.scrollTop);
- marqueeAnno._height = parseInt(anno.style.height || '0') / scale / NumCast(this.props.docView.props.Document._viewScale, 1);
- marqueeAnno._width = parseInt(anno.style.width || '0') / scale / NumCast(this.props.docView.props.Document._viewScale, 1);
+ const marqueeAnno = Docs.Create.FreeformDocument([], {
+ onClick: isLinkButton ? FollowLinkScript() : undefined,
+ backgroundColor: color,
+ annotationOn: this.props.rootDoc,
+ title: 'Annotation on ' + this.props.rootDoc.title,
+ });
+ 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();
savedAnnoMap.clear();
return marqueeAnno;
}
- const textRegionAnno = Docs.Create.HTMLAnchorDocument([], { annotationOn: this.props.rootDoc, text: this.props.selectionText(), backgroundColor: 'transparent', title: 'Selection on ' + this.props.rootDoc.title });
+ const textRegionAnno = Docs.Create.HTMLMarkerDocument([], {
+ annotationOn: this.props.rootDoc,
+ text: this.props.selectionText(),
+ backgroundColor: 'transparent',
+ presDuration: 2100,
+ presTransition: 500,
+ presZoomText: true,
+ title: 'Selection on ' + this.props.rootDoc.title,
+ });
let minX = Number.MAX_VALUE;
let maxX = -Number.MAX_VALUE;
let minY = Number.MAX_VALUE;
@@ -183,11 +206,11 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
return textRegionAnno;
};
@action
- highlight = (color: string, isLinkButton: boolean, savedAnnotations?: ObservableMap<number, HTMLDivElement[]>) => {
+ 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);
- !savedAnnotations && annotationDoc && this.props.addDocument(annotationDoc);
+ addAsAnnotation && !savedAnnotations && annotationDoc && this.props.addDocument(annotationDoc);
return (annotationDoc as Doc) ?? undefined;
};
@@ -221,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) => {
@@ -251,11 +274,8 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
AnchorMenu.Instance.jumpTo(cliX, cliY);
- if (AnchorMenu.Instance.Highlighting) {
- // when highlighter has been toggled when menu is pinned, we auto-highlight immediately on mouse up
- this.highlight('rgba(245, 230, 95, 0.75)', false); // yellowish highlight color for highlighted text (should match AnchorMenu's highlight color)
- }
this.props.finishMarquee(undefined, undefined, e);
+ runInAction(() => (this._width = this._height = 0));
} else {
runInAction(() => (this._width = this._height = 0));
this.props.finishMarquee(cliX, cliY, e);
@@ -263,8 +283,9 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
};
render() {
- return (
+ return !this.props.down ? null : (
<div
+ ref={r => (r ? this.gotDownPoint() : this.releaseDownPt())}
className="marqueeAnnotator-dragBox"
style={{
left: `${this._left}px`,
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.tsx b/src/client/views/OverlayView.tsx
index 6d7f2c037..339507f65 100644
--- a/src/client/views/OverlayView.tsx
+++ b/src/client/views/OverlayView.tsx
@@ -3,19 +3,20 @@ 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';
-import { DocUtils } from '../documents/Documents';
+import { DocumentType } from '../documents/DocumentTypes';
+import { DocumentManager } from '../util/DocumentManager';
import { DragManager } from '../util/DragManager';
-import { ScriptingGlobals } from '../util/ScriptingGlobals';
import { Transform } from '../util/Transform';
import { CollectionFreeFormLinksView } from './collections/collectionFreeForm/CollectionFreeFormLinksView';
import { LightboxView } from './LightboxView';
-import { DocumentView } from './nodes/DocumentView';
+import { MainView } from './MainView';
+import { DocumentView, DocumentViewInternal } from './nodes/DocumentView';
import './OverlayView.scss';
-import { ScriptingRepl } from './ScriptingRepl';
import { DefaultStyleProvider } from './StyleProvider';
export type OverlayDisposer = () => void;
@@ -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;
};
@@ -170,74 +171,70 @@ export class OverlayView extends React.Component {
);
@computed get overlayDocs() {
- return LightboxView.LightboxDoc
- ? null
- : DocListCast(Doc.MyOverlayDocs?.data).map(d => {
- let offsetx = 0,
- offsety = 0;
- const dref = React.createRef<HTMLDivElement>();
- const onPointerMove = action((e: PointerEvent, down: number[]) => {
- if (e.buttons === 1) {
- d.overlayX = e.clientX + offsetx;
- d.overlayY = e.clientY + offsety;
- }
- if (e.metaKey) {
- const dragData = new DragManager.DocumentDragData([d]);
- dragData.offset = [-offsetx, -offsety];
- dragData.dropAction = 'move';
- dragData.removeDocument = (doc: Doc | Doc[]) => {
- const docs = doc instanceof Doc ? [doc] : doc;
- docs.forEach(d => Doc.RemoveDocFromList(Doc.MyOverlayDocs, undefined, d));
- return true;
- };
- dragData.moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean): boolean => {
- return dragData.removeDocument!(doc) ? addDocument(doc) : false;
- };
- DragManager.StartDocumentDrag([dref.current!], dragData, down[0], down[1]);
- return true;
- }
- return false;
- });
-
- const onPointerDown = (e: React.PointerEvent) => {
- setupMoveUpEvents(this, e, onPointerMove, emptyFunction, emptyFunction);
- offsetx = NumCast(d.overlayX) - e.clientX;
- offsety = NumCast(d.overlayY) - e.clientY;
- };
- return (
- <div
- className="overlayView-doc"
- ref={dref}
- key={d[Id]}
- onPointerDown={onPointerDown}
- style={{ top: d.type === 'presentation' ? 0 : undefined, width: NumCast(d._width), height: NumCast(d._height), transform: `translate(${d.overlayX}px, ${d.overlayY}px)` }}>
- <DocumentView
- Document={d}
- rootSelected={returnTrue}
- bringToFront={emptyFunction}
- addDocument={undefined}
- removeDocument={this.removeOverlayDoc}
- PanelWidth={d[WidthSym]}
- PanelHeight={d[HeightSym]}
- ScreenToLocalTransform={this.docScreenToLocalXf(d)}
- renderDepth={1}
- isDocumentActive={returnTrue}
- isContentActive={returnTrue}
- whenChildContentsActiveChanged={emptyFunction}
- focus={DocUtils.DefaultFocus}
- styleProvider={DefaultStyleProvider}
- docViewPath={returnEmptyDoclist}
- addDocTab={returnFalse}
- pinToPres={emptyFunction}
- docFilters={returnEmptyFilter}
- docRangeFilters={returnEmptyFilter}
- searchFilterDocs={returnEmptyDoclist}
- ContainingCollectionView={undefined}
- ContainingCollectionDoc={undefined}
- />
- </div>
- );
- });
+ return DocListCast(Doc.MyOverlayDocs?.data)
+ .filter(d => !LightboxView.LightboxDoc || d.type === DocumentType.PRES)
+ .map(d => {
+ let offsetx = 0,
+ 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;
+ }
+ if (e.metaKey) {
+ const dragData = new DragManager.DocumentDragData([d]);
+ dragData.offset = [-offsetx, -offsety];
+ dragData.dropAction = 'move';
+ dragData.removeDocument = this.removeOverlayDoc;
+ dragData.moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean): boolean => {
+ return dragData.removeDocument!(doc) ? addDocument(doc) : false;
+ };
+ DragManager.StartDocumentDrag([dref.current!], dragData, down[0], down[1]);
+ return true;
+ }
+ return false;
+ });
+
+ const onPointerDown = (e: React.PointerEvent) => {
+ setupMoveUpEvents(this, e, onPointerMove, emptyFunction, emptyFunction, false);
+ offsetx = NumCast(d.overlayX) - e.clientX;
+ offsety = NumCast(d.overlayY) - e.clientY;
+ };
+ return (
+ <div
+ className="overlayView-doc"
+ ref={dref}
+ key={d[Id]}
+ onPointerDown={onPointerDown}
+ style={{ top: d.type === DocumentType.PRES ? 0 : undefined, width: NumCast(d._width), height: NumCast(d._height), transform: `translate(${d.overlayX}px, ${d.overlayY}px)` }}>
+ <DocumentView
+ Document={d}
+ rootSelected={returnTrue}
+ bringToFront={emptyFunction}
+ addDocument={undefined}
+ removeDocument={this.removeOverlayDoc}
+ PanelWidth={d[Width]}
+ PanelHeight={d[Height]}
+ ScreenToLocalTransform={this.docScreenToLocalXf(d)}
+ renderDepth={1}
+ hideDecorations={true}
+ isDocumentActive={returnTrue}
+ isContentActive={returnTrue}
+ whenChildContentsActiveChanged={emptyFunction}
+ focus={emptyFunction}
+ styleProvider={DefaultStyleProvider}
+ docViewPath={returnEmptyDoclist}
+ addDocTab={DocumentViewInternal.addDocTabFunc}
+ pinToPres={emptyFunction}
+ childFilters={returnEmptyFilter}
+ childFiltersByRanges={returnEmptyFilter}
+ searchFilterDocs={returnEmptyDoclist}
+ />
+ </div>
+ );
+ });
}
public static ShowSpinner() {
@@ -254,7 +251,3 @@ export class OverlayView extends React.Component {
);
}
}
-// bcz: ugh ... want to be able to pass ScriptingRepl as tag argument, but that doesn't seem to work.. runtime error
-ScriptingGlobals.add(function addOverlayWindow(type: string, options: OverlayElementOptions) {
- OverlayView.Instance.addWindow(<ScriptingRepl />, options);
-});
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 954529bc9..000000000
--- a/src/client/views/Palette.tsx
+++ /dev/null
@@ -1,70 +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}
- ContainingCollectionView={undefined}
- ContainingCollectionDoc={undefined} />
- <div className="palette-cover" style={{ transform: `translate(${Math.max(0, this._selectedIndex) * 50.75 + 23}px, 0px)` }}></div>
- </div>
- </div>
- </div>
- );
- }
-} \ No newline at end of file
diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx
index 119476210..b513fe245 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';
@@ -19,16 +18,7 @@ export class PreviewCursor extends React.Component<{}> {
static _addDocument: (doc: Doc | Doc[]) => boolean;
static _addLiveTextDoc: (doc: Doc) => void;
static _nudge?: undefined | ((x: number, y: number) => boolean);
- static _slowLoadDocuments?: (
- files: File[] | string,
- options: DocumentOptions,
- generatedDocuments: Doc[],
- text: string,
- completed: ((doc: Doc[]) => void) | undefined,
- clientX: number,
- clientY: number,
- addDocument: (doc: Doc | Doc[]) => boolean
- ) => Promise<void>;
+ static _slowLoadDocuments?: (files: File[] | string, options: DocumentOptions, generatedDocuments: Doc[], text: string, completed: ((doc: Doc[]) => void) | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => Promise<void>;
@observable static _clickPoint = [0, 0];
@observable public static Visible = false;
constructor(props: any) {
@@ -57,7 +47,7 @@ export class PreviewCursor extends React.Component<{}> {
x: newPoint[0],
y: newPoint[1],
};
- PreviewCursor._slowLoadDocuments?.(plain.split('v=')[1].split('&')[0], options, generatedDocuments, '', undefined, newPoint[0], newPoint[1], PreviewCursor._addDocument).then(batch.end);
+ 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(() =>
@@ -66,7 +56,7 @@ export class PreviewCursor extends React.Component<{}> {
title: url,
_width: 500,
_height: 300,
- useCors: true,
+ data_useCors: true,
x: newPoint[0],
y: newPoint[1],
})
@@ -75,34 +65,16 @@ export class PreviewCursor extends React.Component<{}> {
} 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 docs = await Promise.all(
- docids
- .filter((did, i) => i)
- .map(async did => {
- const doc = Cast(await DocServer.GetRefField(did), Doc, null);
- return clone ? (await Doc.MakeClone(doc)).clone : doc;
- })
- );
- 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 {
- // creates text document
FormattedTextBox.PasteOnLoad = e;
- UndoManager.RunInBatch(() => PreviewCursor._addLiveTextDoc(DocUtils.GetNewTextDoc('-pasted text-', newPoint[0], newPoint[1], 500, undefined, undefined, undefined, 750)), 'paste');
+ if (e.clipboardData.getData('dash/pdfAnchor')) e.preventDefault();
+ UndoManager.RunInBatch(() => PreviewCursor._addLiveTextDoc(DocUtils.GetNewTextDoc('', newPoint[0], newPoint[1], 500, undefined, undefined, undefined, 750)), 'paste');
}
}
//pasting in images
@@ -191,16 +163,7 @@ export class PreviewCursor extends React.Component<{}> {
getTransform: () => Transform,
addDocument: undefined | ((doc: Doc | Doc[]) => boolean),
nudge: undefined | ((nudgeX: number, nudgeY: number) => boolean),
- slowLoadDocuments: (
- files: File[] | string,
- options: DocumentOptions,
- generatedDocuments: Doc[],
- text: string,
- completed: ((doc: Doc[]) => void) | undefined,
- clientX: number,
- clientY: number,
- addDocument: (doc: Doc | Doc[]) => boolean
- ) => Promise<void>
+ slowLoadDocuments: (files: File[] | string, options: DocumentOptions, generatedDocuments: Doc[], text: string, completed: ((doc: Doc[]) => void) | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => Promise<void>
) {
this._clickPoint = [x, y];
this._onKeyPress = onKeyPress;
diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx
index 80c2c7705..74dd1c2f7 100644
--- a/src/client/views/PropertiesButtons.tsx
+++ b/src/client/views/PropertiesButtons.tsx
@@ -2,20 +2,22 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tooltip } from '@material-ui/core';
import { action, computed, observable } from 'mobx';
import { observer } from 'mobx-react';
-import { Doc, Opt } from '../../fields/Doc';
+import { Doc, DocListCast, Opt } from '../../fields/Doc';
import { Id } from '../../fields/FieldSymbols';
import { InkField } from '../../fields/InkField';
import { RichTextField } from '../../fields/RichTextField';
-import { BoolCast, StrCast } from '../../fields/Types';
+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 } from './nodes/DocumentView';
-import { VideoBox } from './nodes/VideoBox';
+import { DocumentView, OpenWhere } from './nodes/DocumentView';
import { pasteImageBitmap } from './nodes/WebBoxRenderer';
import './PropertiesButtons.scss';
import React = require('react');
@@ -48,11 +50,11 @@ export class PropertiesButtons extends React.Component<{}, {}> {
<div
className={`propertiesButtons-linkButton-empty toggle-${StrCast(targetDoc[property]).includes(':hover') ? 'hover' : targetDoc[property] ? 'on' : 'off'}`}
onPointerDown={e => e.stopPropagation()}
- onClick={undoBatch(() => {
+ 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)}>
<FontAwesomeIcon className="documentdecorations-icon" size="lg" icon={icon(BoolCast(targetDoc?.[property])) as any} />
</div>
<div className="propertiesButtons-title">{label}</div>
@@ -71,16 +73,24 @@ export class PropertiesButtons extends React.Component<{}, {}> {
@computed get maskButton() {
return this.propertyToggleBtn(
'Mask',
- 'isInkMask',
+ 'stroke_isInkMask',
on => (on ? 'Make plain ink' : 'Make highlight mask'),
on => 'paint-brush',
(dv, doc) => InkingStroke.toggleMask(dv?.layoutDoc || doc)
);
}
+ @computed get hideImageButton() {
+ return this.propertyToggleBtn(
+ 'Background',
+ '_hideImage',
+ on => (on ? 'Show Image' : 'Show Background'),
+ on => 'portrait'
+ );
+ }
@computed get clustersButton() {
return this.propertyToggleBtn(
'Clusters',
- '_useClusters',
+ '_freeform_useClusters',
on => `${on ? 'Hide' : 'Show'} clusters`,
on => 'braille'
);
@@ -93,18 +103,52 @@ export class PropertiesButtons extends React.Component<{}, {}> {
on => 'lock'
);
}
+ @computed get forceActiveButton() {
+ return this.propertyToggleBtn(
+ 'Active',
+ '_forceActive',
+ on => `${on ? 'Select to activate' : 'Contents always active'} `,
+ on => 'eye'
+ );
+ }
@computed get fitContentButton() {
return this.propertyToggleBtn(
'View All',
- '_fitContentsToBox',
+ '_freeform_fitContentsToBox',
on => `${on ? "Don't" : 'Do'} fit content to container visible area`,
- on => 'eye'
+ on => 'object-group'
);
}
- @computed get fitWidthButton() {
+ // 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.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 layout_fitWidthButton() {
return this.propertyToggleBtn(
'Fit\xA0Width',
- '_fitWidth',
+ '_layout_fitWidth',
on => `${on ? "Don't" : 'Do'} fit content to width of container`,
on => 'arrows-alt-h'
);
@@ -112,10 +156,10 @@ export class PropertiesButtons extends React.Component<{}, {}> {
@computed get captionButton() {
return this.propertyToggleBtn(
'Caption',
- '_showCaption',
+ '_layout_showCaption',
on => `${on ? 'Hide' : 'Show'} caption footer`,
on => 'closed-captioning',
- (dv, doc) => ((dv?.rootDoc || doc)._showCaption = (dv?.rootDoc || doc)._showCaption === undefined ? 'caption' : undefined)
+ (dv, doc) => ((dv?.rootDoc || doc)._layout_showCaption = (dv?.rootDoc || doc)._layout_showCaption === undefined ? 'caption' : undefined)
);
}
@computed get chromeButton() {
@@ -130,16 +174,20 @@ export class PropertiesButtons extends React.Component<{}, {}> {
@computed get titleButton() {
return this.propertyToggleBtn(
'Title',
- '_showTitle',
+ '_layout_showTitle',
on => 'Switch between title styles',
on => 'text-width',
- (dv, doc) => ((dv?.rootDoc || doc)._showTitle = !(dv?.rootDoc || doc)._showTitle ? 'title' : (dv?.rootDoc || doc)._showTitle === 'title' ? 'title:hover' : undefined)
+ (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 autoHeightButton() {
+ @computed get layout_autoHeightButton() {
return this.propertyToggleBtn(
'Auto\xA0Size',
- '_autoHeight',
+ '_layout_autoHeight',
on => `Automatical vertical sizing to show all content`,
on => 'arrows-alt-v'
);
@@ -147,7 +195,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
@computed get gridButton() {
return this.propertyToggleBtn(
'Grid',
- '_backgroundGridShow',
+ '_freeform_backgroundGrid',
on => `Display background grid in collection`,
on => 'border-all'
);
@@ -161,6 +209,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
(dv, doc) => {
doc.isGroup = !doc.isGroup;
doc.forceActive = doc.isGroup;
+ doc.dragWhenActive = doc.isGroup;
}
);
}
@@ -169,7 +218,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
'FreezeThumb',
'_thumb-frozen',
on => `${on ? 'Freeze' : 'Unfreeze'} thumbnail`,
- on => 'arrows-alt-h',
+ on => 'snowflake',
(dv, doc) => {
if (doc['thumb-frozen']) doc['thumb-frozen'] = undefined;
else {
@@ -177,7 +226,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
setTimeout(() =>
pasteImageBitmap((data_url: any, error: any) => {
error && console.log(error);
- data_url && VideoBox.convertDataUri(data_url, doc[Id] + '-thumb-frozen', true).then(returnedfilename => (doc['thumb-frozen'] = new ImageField(returnedfilename)));
+ data_url && Utils.convertDataUri(data_url, doc[Id] + '-thumb-frozen', true).then(returnedfilename => (doc['thumb-frozen'] = new ImageField(returnedfilename)));
})
);
}
@@ -187,9 +236,9 @@ export class PropertiesButtons extends React.Component<{}, {}> {
@computed get snapButton() {
return this.propertyToggleBtn(
'Snap\xA0Lines',
- 'showSnapLines',
+ 'freeform_snapLines',
on => `Display snapping lines when objects are dragged`,
- on => 'border-all',
+ on => 'th',
undefined,
true
);
@@ -232,21 +281,21 @@ export class PropertiesButtons extends React.Component<{}, {}> {
@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));
};
@undoBatch
@action
handleOptionChange = (onClick: string) => {
- this.selectedDoc && (this.selectedDoc.onClickBehavior = onClick);
SelectionManager.Views()
.filter(dv => dv.docView)
.map(dv => dv.docView!)
.forEach(docView => {
+ const linkButton = IsFollowLinkScript(docView.props.Document.onClick);
docView.noOnClick();
switch (onClick) {
case 'enterPortal':
@@ -256,10 +305,12 @@ export class PropertiesButtons extends React.Component<{}, {}> {
docView.setToggleDetail();
break;
case 'linkInPlace':
- docView.toggleFollowLink('inPlace', true, false);
+ docView.toggleFollowLink(false, false);
+ docView.props.Document.followLinkLocation = linkButton ? OpenWhere.lightbox : undefined;
break;
case 'linkOnRight':
- docView.toggleFollowLink('add:right', false, false);
+ docView.toggleFollowLink(false, false);
+ docView.props.Document.followLinkLocation = linkButton ? OpenWhere.addRight : undefined;
break;
}
});
@@ -277,24 +328,26 @@ export class PropertiesButtons extends React.Component<{}, {}> {
['nothing', 'Select Document'],
['enterPortal', 'Enter Portal'],
['toggleDetail', 'Toggle Detail'],
- ['linkInPlace', 'Follow Link'],
+ ['linkInPlace', 'Open Link in Lightbox'],
['linkOnRight', 'Open Link on Right'],
];
- const currentSelection = this.selectedDoc.onClickBehavior;
- // Get items to place into the list
-
const list = buttonList.map(value => {
- const click = () => {
- this.handleOptionChange(value[0]);
- };
+ const click = () => this.handleOptionChange(value[0]);
+ 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);
+
+ let active = false;
+ // prettier-ignore
+ switch (value[0]) {
+ case 'linkInPlace': active = linkButton && followLoc === OpenWhere.lightbox && !linkedToLightboxView(); break;
+ case 'linkOnRight': active = linkButton && followLoc === OpenWhere.addRight; break;
+ case 'enterPortal': active = linkButton && this.selectedDoc._followLinkLocation === OpenWhere.lightbox && linkedToLightboxView(); break;
+ case 'toggleDetail':active = ScriptCast(this.selectedDoc.onClick)?.script.originalScript.includes('toggleDetail'); break;
+ case 'nothing': active = !linkButton && this.selectedDoc.onClick === undefined;break;
+ }
return (
- <div
- className="list-item"
- key={`${value}`}
- style={{
- backgroundColor: value[0] === currentSelection ? Colors.LIGHT_BLUE : undefined,
- }}
- onClick={click}>
+ <div className="list-item" key={`${value}`} style={{ backgroundColor: active ? Colors.LIGHT_BLUE : undefined }} onClick={click}>
{value[1]}
</div>
);
@@ -320,7 +373,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
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} />
+ <input type="radio" value={value} checked={(this.selectedDoc?._type_collection ?? 'invalid') === value} onChange={this.handlePerspectiveChange} />
{label}
</label>
</div>
@@ -338,12 +391,13 @@ export class PropertiesButtons extends React.Component<{}, {}> {
const layoutField = this.selectedDoc?.[Doc.LayoutFieldKey(this.selectedDoc)];
const isText = layoutField instanceof RichTextField;
const isInk = layoutField instanceof InkField;
+ const isImage = layoutField instanceof ImageField;
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}>
@@ -358,11 +412,14 @@ export class PropertiesButtons extends React.Component<{}, {}> {
{toggle(this.captionButton)}
{toggle(this.lockButton)}
{toggle(this.onClickButton)}
- {toggle(this.fitWidthButton)}
+ {toggle(this.layout_fitWidthButton)}
{toggle(this.freezeThumb)}
+ {toggle(this.forceActiveButton)}
{toggle(this.fitContentButton, { display: !isFreeForm && !isMap ? 'none' : '' })}
- {toggle(this.autoHeightButton, { display: !isText && !isStacking && !isTree ? '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' : '' })}
diff --git a/src/client/views/PropertiesDocBacklinksSelector.tsx b/src/client/views/PropertiesDocBacklinksSelector.tsx
index 4ead8eaf0..3e69bcba6 100644
--- a/src/client/views/PropertiesDocBacklinksSelector.tsx
+++ b/src/client/views/PropertiesDocBacklinksSelector.tsx
@@ -1,53 +1,40 @@
-import { computed } from "mobx";
-import { observer } from "mobx-react";
-import * as React from "react";
-import { Doc, DocListCast } from "../../fields/Doc";
-import { Cast } from "../../fields/Types";
-import { emptyFunction } from "../../Utils";
-import { DocumentType } from "../documents/DocumentTypes";
-import { LinkManager } from "../util/LinkManager";
-import { SelectionManager } from "../util/SelectionManager";
-import { LinkMenu } from "./linking/LinkMenu";
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { Doc } from '../../fields/Doc';
+import { Cast } from '../../fields/Types';
+import { DocumentType } from '../documents/DocumentTypes';
+import { LinkManager } from '../util/LinkManager';
+import { SelectionManager } from '../util/SelectionManager';
+import { LinkMenu } from './linking/LinkMenu';
+import { OpenWhere, OpenWhereMod } from './nodes/DocumentView';
import './PropertiesDocBacklinksSelector.scss';
type PropertiesDocBacklinksSelectorProps = {
- Document: Doc,
- Stack?: any,
- hideTitle?: boolean,
- addDocTab(doc: Doc, location: string): void
+ Document: Doc;
+ Stack?: any;
+ hideTitle?: boolean;
+ addDocTab(doc: Doc, location: OpenWhere): void;
};
@observer
export class PropertiesDocBacklinksSelector extends React.Component<PropertiesDocBacklinksSelectorProps> {
- @computed get _docs() {
- const linkSource = this.props.Document;
- const links = DocListCast(linkSource.links);
- const collectedLinks = [] as Doc[];
- links.map(link => {
- const other = LinkManager.getOppositeAnchor(link, linkSource);
- const otherdoc = !other ? undefined : other.annotationOn && other.type !== DocumentType.RTF ? Cast(other.annotationOn, Doc, null) : other;
- if (otherdoc && !collectedLinks.some(d => Doc.AreProtosEqual(d, otherdoc))) {
- collectedLinks.push(otherdoc);
- }
- });
- return collectedLinks;
- }
-
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.IsPrototype(otherdoc) ? Doc.MakeDelegate(otherdoc) : otherdoc, "toggle:right");
+ this.props.addDocTab(Doc.IsDataProto(otherdoc) ? Doc.MakeDelegate(otherdoc) : otherdoc, OpenWhere.toggleRight);
}
- }
+ };
render() {
- return !SelectionManager.Views().length ? (null) : <div>
- {this.props.hideTitle ? (null) : <p key="contexts">Contexts:</p>}
- <LinkMenu docView={SelectionManager.Views().lastElement()} clearLinkEditor={emptyFunction} itemHandler={this.getOnClick} position={{ x: 0 }} />
- </div>;
+ return !SelectionManager.Views().length ? null : (
+ <div className="preroptiesDocBacklinksSelector">
+ {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>
+ );
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/PropertiesDocContextSelector.tsx b/src/client/views/PropertiesDocContextSelector.tsx
index 9d89ee036..e1279c9a7 100644
--- a/src/client/views/PropertiesDocContextSelector.tsx
+++ b/src/client/views/PropertiesDocContextSelector.tsx
@@ -7,14 +7,14 @@ import { Cast, NumCast, StrCast } from '../../fields/Types';
import { CollectionViewType } from '../documents/DocumentTypes';
import { DocFocusOrOpen } from '../util/DocumentManager';
import { CollectionDockingView } from './collections/CollectionDockingView';
-import { DocumentView } from './nodes/DocumentView';
+import { DocumentView, OpenWhere, OpenWhereMod } from './nodes/DocumentView';
import './PropertiesDocContextSelector.scss';
type PropertiesDocContextSelectorProps = {
DocView?: DocumentView;
Stack?: any;
hideTitle?: boolean;
- addDocTab(doc: Doc, location: string): void;
+ addDocTab(doc: Doc, location: OpenWhere): void;
};
@observer
@@ -22,15 +22,15 @@ export class PropertiesDocContextSelector extends React.Component<PropertiesDocC
@computed get _docs() {
if (!this.props.DocView) return [];
const target = this.props.DocView.props.Document;
- const targetContext = this.props.DocView.props.ContainingCollectionDoc;
- const aliases = DocListCast(target.aliases);
- const containerProtos = aliases.filter(alias => alias.context && alias.context instanceof Doc && Cast(alias.context, Doc, null) !== targetContext).reduce((set, alias) => set.add(Cast(alias.context, Doc, null)), new Set<Doc>());
- const containerSets = Array.from(containerProtos.keys()).map(container => DocListCast(container.aliases));
+ const targetContext = this.props.DocView.props.docViewPath().lastElement()?.rootDoc;
+ const embeddings = DocListCast(target.proto_embeddings);
+ const containerProtos = embeddings.filter(embedding => embedding.embedContainer && embedding.embedContainer instanceof Doc).reduce((set, embedding) => set.add(Cast(embedding.embedContainer, Doc, null)), new Set<Doc>());
+ const containerSets = Array.from(containerProtos.keys()).map(container => DocListCast(container.proto_embeddings));
const containers = containerSets.reduce((p, set) => {
set.map(s => p.add(s));
return p;
}, new Set<Doc>());
- const doclayoutSets = Array.from(containers.keys()).map(dp => DocListCast(dp.aliases));
+ const doclayoutSets = Array.from(containers.keys()).map(dp => DocListCast(dp.proto_embeddings));
const doclayouts = Array.from(
doclayoutSets
.reduce((p, set) => {
@@ -42,19 +42,14 @@ export class PropertiesDocContextSelector extends React.Component<PropertiesDocC
return doclayouts
.filter(doc => !Doc.AreProtosEqual(doc, CollectionDockingView.Instance?.props.Document))
.filter(doc => !Doc.IsSystem(doc))
+ .filter(doc => doc !== targetContext)
.map(doc => ({ col: doc, target }));
}
getOnClick = (col: Doc, target: Doc) => {
if (!this.props.DocView) return;
- col = Doc.IsPrototype(col) ? Doc.MakeDelegate(col) : col;
- if (col._viewType === CollectionViewType.Freeform) {
- col._panX = NumCast(target.x) + NumCast(target._width) / 2;
- col._panY = NumCast(target.y) + NumCast(target._height) / 2;
- }
- col.hidden = false;
- this.props.addDocTab(col, 'toggle:right');
- setTimeout(() => DocFocusOrOpen(Doc.GetProto(this.props.DocView!.props.Document), col), 100);
+ col = Doc.IsDataProto(col) ? Doc.MakeDelegate(col) : col;
+ DocFocusOrOpen(Doc.GetProto(this.props.DocView.props.Document), undefined, col);
};
render() {
diff --git a/src/client/views/PropertiesView.scss b/src/client/views/PropertiesView.scss
index 437df4739..897be9a32 100644
--- a/src/client/views/PropertiesView.scss
+++ b/src/client/views/PropertiesView.scss
@@ -1,11 +1,16 @@
-@import "./global/globalCssVariables.scss";
+@import './global/globalCssVariables.scss';
.propertiesView {
height: 100%;
width: 250;
- font-family: "Roboto";
+ font-family: 'Roboto';
+ font-size: 12px;
cursor: auto;
+ .slider-text {
+ font-size: 8px;
+ }
+
overflow-x: hidden;
overflow-y: auto;
@@ -843,7 +848,7 @@
}
.propertiesView-section {
- padding: 10px 0;
+ padding-left: 20px;
}
.propertiesView-input {
@@ -865,7 +870,15 @@
}
.propertiesButton {
- width: 4rem;
+ width: 2rem;
+ height: 2rem;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ > svg {
+ width: 15px;
+ height: 15px;
+ }
}
}
@@ -877,4 +890,4 @@
color: white;
padding-left: 8px;
background-color: rgb(51, 51, 51);
-} \ No newline at end of file
+}
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 842664402..09aac053a 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -1,13 +1,14 @@
import React = require('react');
import { IconLookup } from '@fortawesome/fontawesome-svg-core';
-import { faAnchor, faArrowRight } from '@fortawesome/free-solid-svg-icons';
+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, autorun, computed, Lambda, observable } from 'mobx';
+import { action, computed, Lambda, observable } from 'mobx';
import { observer } from 'mobx-react';
import { ColorState, SketchPicker } from 'react-color';
-import { AclAdmin, AclAugment, AclEdit, AclPrivate, AclReadonly, AclSelfEdit, AclSym, AclUnset, DataSym, Doc, DocListCast, Field, HeightSym, NumListCast, Opt, StrListCast, WidthSym } from '../../fields/Doc';
+import { Doc, Field, FieldResult, HierarchyMapping, NumListCast, Opt, 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';
@@ -21,19 +22,19 @@ 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 { undoable, undoBatch, UndoManager } from '../util/UndoManager';
import { EditableView } from './EditableView';
+import { FilterPanel } from './FilterPanel';
+import { Colors } from './global/globalEnums';
import { InkStrokeProperties } from './InkStrokeProperties';
-import { DocumentView, StyleProviderFunc } from './nodes/DocumentView';
-import { FilterBox } from './nodes/FilterBox';
+import { DocumentView, OpenWhere, StyleProviderFunc } from './nodes/DocumentView';
import { KeyValueBox } from './nodes/KeyValueBox';
-import { PresBox } from './nodes/trails';
+import { PresBox, PresEffect, PresEffectDirection } from './nodes/trails';
import { PropertiesButtons } from './PropertiesButtons';
import { PropertiesDocBacklinksSelector } from './PropertiesDocBacklinksSelector';
import { PropertiesDocContextSelector } from './PropertiesDocContextSelector';
import './PropertiesView.scss';
import { DefaultStyleProvider } from './StyleProvider';
-import { listSpec } from '../../fields/Schema';
const higflyout = require('@hig/flyout');
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
@@ -43,7 +44,7 @@ interface PropertiesViewProps {
width: number;
height: number;
styleProvider?: StyleProviderFunc;
- addDocTab: (doc: Doc, where: string) => boolean;
+ addDocTab: (doc: Doc, where: OpenWhere) => boolean;
}
@observer
@@ -69,20 +70,20 @@ 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;
@observable openOptions: boolean = true;
@observable openSharing: boolean = true;
- @observable openFields: boolean = true;
+ @observable openFields: boolean = false;
@observable openLayout: boolean = false;
@observable openContexts: boolean = true;
@observable openLinks: boolean = true;
@observable openAppearance: boolean = true;
@observable openTransform: boolean = true;
- @observable openFilters: boolean = true; // should be false
+ @observable openFilters: boolean = false;
/**
* autorun to set up the filter doc of a collection if that collection has been selected and the filters panel is open
@@ -94,18 +95,18 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
@observable layoutDocAcls: boolean = false;
//Pres Trails booleans:
- @observable openPresTransitions: boolean = false;
+ @observable openPresTransitions: boolean = true;
@observable openPresProgressivize: boolean = false;
+ @observable openPresVisibilityAndDuration: boolean = false;
@observable openAddSlide: boolean = false;
@observable openSlideOptions: boolean = false;
@observable inOptions: boolean = false;
@observable _controlButton: boolean = false;
- @observable _lock: boolean = false;
componentDidMount() {
this.selectedDocListenerDisposer?.();
- this.selectedDocListenerDisposer = autorun(() => this.openFilters && this.selectedDoc && this.checkFilterDoc());
+ // this.selectedDocListenerDisposer = autorun(() => this.openFilters && this.selectedDoc && this.checkFilterDoc());
}
componentWillUnmount() {
@@ -116,23 +117,18 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
return this.selectedDoc?.type === DocumentType.INK;
}
- rtfWidth = () => {
- return !this.selectedDoc ? 0 : Math.min(this.selectedDoc?.[WidthSym](), this.props.width - 20);
- };
- rtfHeight = () => {
- return !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._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;
+ const aspect = Doc.NativeAspect(layoutDoc, undefined, !layoutDoc._layout_fitWidth);
+ 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
@@ -145,10 +141,10 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
this.MAX_EMBED_HEIGHT,
Doc.NativeAspect(layoutDoc, undefined, true)
? this.docWidth() / Doc.NativeAspect(layoutDoc, undefined, true)
- : layoutDoc._fitWidth
+ : layoutDoc._layout_fitWidth
? !Doc.NativeHeight(this.dataDoc)
? NumCast(this.props.height)
- : Math.min((this.docWidth() * NumCast(layoutDoc.scrollHeight, Doc.NativeHeight(layoutDoc))) / Doc.NativeWidth(layoutDoc) || NumCast(this.props.height))
+ : Math.min((this.docWidth() * Doc.NativeHeight(layoutDoc)) / Doc.NativeWidth(layoutDoc) || NumCast(this.props.height))
: NumCast(layoutDoc._height) || 50
)
);
@@ -156,121 +152,60 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
return 0;
};
- @computed get expandedField() {
+ editableFields = (filter: (key: string) => boolean, reqdKeys: string[]) => {
+ const rows: JSX.Element[] = [];
if (this.dataDoc && this.selectedDoc) {
- const ids: { [key: string]: string } = {};
- const docs = SelectionManager.Views().length < 2 ? [this.layoutFields ? Doc.Layout(this.selectedDoc) : this.dataDoc] : SelectionManager.Views().map(dv => (this.layoutFields ? dv.layoutDoc : dv.dataDoc));
- docs.forEach(doc => Object.keys(doc).forEach(key => !(key in ids) && doc[key] !== ComputedField.undefined && (ids[key] = key)));
- const rows: JSX.Element[] = [];
- for (const key of Object.keys(ids).slice().sort()) {
- const docvals = new Set<any>();
- docs.forEach(doc => docvals.add(doc[key]));
- const contents = Array.from(docvals.keys()).length > 1 ? '-multiple' : docs[0][key];
- if (key[0] === '#') {
- rows.push(
- <div style={{ display: 'flex', overflowY: 'visible', marginBottom: '2px' }} key={key}>
- <span style={{ fontWeight: 'bold', whiteSpace: 'nowrap' }}>{key}</span>
- &nbsp;
- </div>
- );
- } else {
- const contentElement = (
- <EditableView
- key="editableView"
- contents={contents !== undefined ? Field.toString(contents as Field) : 'null'}
- height={13}
- fontSize={10}
- GetValue={() => (contents !== undefined ? Field.toString(contents as Field) : 'null')}
- SetValue={(value: string) => {
- docs.map(doc => KeyValueBox.SetField(doc, key, value, true));
- return true;
- }}
- />
- );
- rows.push(
- <div style={{ display: 'flex', overflowY: 'visible', marginBottom: '-1px' }} key={key}>
- <span style={{ fontWeight: 'bold', whiteSpace: 'nowrap' }}>{key + ':'}</span>
- &nbsp;
- {contentElement}
- </div>
- );
- }
- }
+ const ids = new Set<string>(reqdKeys);
+ const docs: Doc[] = SelectionManager.Views().length < 2 ? [this.layoutFields ? Doc.Layout(this.selectedDoc) : this.dataDoc] : SelectionManager.Views().map(dv => (this.layoutFields ? dv.layoutDoc : dv.dataDoc));
+ docs.forEach(doc => Object.keys(doc).forEach(key => doc[key] !== ComputedField.undefined && ids.add(key)));
+
+ // prettier-ignore
+ Array.from(ids).filter(filter).sort().map(key => {
+ const multiple = Array.from(docs.reduce((set,doc) => set.add(doc[key]), new Set<FieldResult>()).keys()).length > 1;
+ const editableContents = multiple ? '-multiple-' : Field.toKeyValueString(docs[0], key);
+ const displayContents = multiple ? '-multiple-' : Field.toString(docs[0][key] as Field);
+ const contentElement = (
+ <EditableView
+ key="editableView"
+ contents={displayContents}
+ height={13}
+ fontSize={10}
+ GetValue={() => editableContents}
+ SetValue={(value: string) => {
+ value !== '-multiple-' && docs.map(doc => KeyValueBox.SetField(doc, key, value, true));
+ return true;
+ }}
+ />);
+ rows.push(
+ <div style={{ display: 'flex', overflowY: 'visible', marginBottom: '-1px' }} key={key}>
+ <span style={{ fontWeight: 'bold', whiteSpace: 'nowrap' }}>{key + ':'}</span>
+ &nbsp;
+ {contentElement}
+ </div>
+ );
+ });
+
rows.push(
- <div className="propertiesView-field" key={'newKeyValue'} style={{ marginTop: '3px' }}>
+ <div className="propertiesView-field" key="newKeyValue" style={{ marginTop: '3px' }}>
<EditableView key="editableView" oneLine contents={'add key:value or #tags'} height={13} fontSize={10} GetValue={() => ''} SetValue={this.setKeyValue} />
</div>
);
- return rows;
}
+ return rows;
+ };
+
+ @computed get expandedField() {
+ return this.editableFields(returnTrue, []);
}
@computed get noviceFields() {
- if (this.dataDoc) {
- const ids: { [key: string]: string } = {};
- const docs = SelectionManager.Views().length < 2 ? [this.dataDoc] : SelectionManager.Views().map(dv => dv.dataDoc);
- docs.forEach(doc => Object.keys(doc).forEach(key => !(key in ids) && doc[key] !== ComputedField.undefined && (ids[key] = key)));
- const rows: JSX.Element[] = [];
- const noviceReqFields = ['author', 'creationDate', 'tags'];
- const noviceLayoutFields = ['_curPage'];
- const noviceKeys = [...Array.from(Object.keys(ids)).filter(key => key[0] === '#' || key.indexOf('lastModified') !== -1 || (key[0] === key[0].toUpperCase() && !key.startsWith('acl'))), ...noviceReqFields, ...noviceLayoutFields];
- for (const key of noviceKeys.sort()) {
- const docvals = new Set<any>();
- docs.forEach(doc => docvals.add(doc[key]));
- const contents = Array.from(docvals.keys()).length > 1 ? '-multiple' : docs[0][key];
- if (key[0] === '#') {
- rows.push(
- <div className="propertiesView-uneditable-field" key={key}>
- <span style={{ fontWeight: 'bold', whiteSpace: 'nowrap' }}>{key}</span>
- &nbsp;
- </div>
- );
- } else if (contents !== undefined) {
- const value = Field.toString(contents as Field);
- if (noviceReqFields.includes(key) || key.indexOf('lastModified') !== -1) {
- rows.push(
- <div className="propertiesView-uneditable-field" key={key}>
- <span style={{ fontWeight: 'bold', whiteSpace: 'nowrap' }}>{key + ': '}</span>
- <div style={{ whiteSpace: 'nowrap', overflowX: 'hidden' }}>{value}</div>
- </div>
- );
- } else {
- const contentElement = (
- <EditableView
- key="editableView"
- contents={value}
- height={13}
- fontSize={10}
- GetValue={() => (contents !== undefined ? Field.toString(contents as Field) : 'null')}
- SetValue={(value: string) => {
- docs.map(doc => KeyValueBox.SetField(doc, key, value, true));
- return true;
- }}
- />
- );
-
- rows.push(
- <div style={{ display: 'flex', overflowY: 'visible', marginBottom: '-1px' }} key={key}>
- <span style={{ fontWeight: 'bold', whiteSpace: 'nowrap' }}>{key + ':'}</span>
- &nbsp;
- {contentElement}
- </div>
- );
- }
- }
- }
- rows.push(
- <div className="propertiesView-field" key={'newKeyValue'} style={{ marginTop: '3px' }}>
- <EditableView key="editableView" oneLine contents={'add key:value or #tags'} height={13} fontSize={10} GetValue={() => ''} SetValue={this.setKeyValue} />
- </div>
- );
- return rows;
- }
+ 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);
@@ -282,11 +217,10 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
}
return true;
} else if (value[0] === '#') {
- const newVal = value + `:'${value}'`;
- doc[DataSym][value] = value;
- const tags = StrCast(doc.tags, ':');
- if (!tags.includes(`${value}:`)) {
- doc[DataSym].tags = `${tags + value + ':'}`;
+ const tags = StrListCast(doc.tags);
+ if (!tags.includes(value)) {
+ tags.push(value);
+ doc[DocData].tags = tags.length ? new List<string>(tags) : undefined;
}
return true;
}
@@ -311,7 +245,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
}
@computed get links() {
- return !this.selectedDoc ? null : <PropertiesDocBacklinksSelector Document={this.selectedDoc} hideTitle={true} addDocTab={this.props.addDocTab} />;
+ const selAnchor = this.selectedDocumentView?.anchorViewDoc ?? LinkManager.currentLinkAnchor ?? this.selectedDoc;
+ return !selAnchor ? null : <PropertiesDocBacklinksSelector Document={selAnchor} hideTitle={true} addDocTab={this.props.addDocTab} />;
}
@computed get layoutPreview() {
@@ -339,13 +274,11 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
NativeHeight={layoutDoc.type === DocumentType.RTF ? this.rtfHeight : undefined}
PanelWidth={panelWidth}
PanelHeight={panelHeight}
- focus={returnFalse}
+ focus={emptyFunction}
ScreenToLocalTransform={this.getTransform}
- docFilters={returnEmptyFilter}
- docRangeFilters={returnEmptyFilter}
+ childFilters={returnEmptyFilter}
+ childFiltersByRanges={returnEmptyFilter}
searchFilterDocs={returnEmptyDoclist}
- ContainingCollectionDoc={undefined}
- ContainingCollectionView={undefined}
addDocument={returnFalse}
moveDocument={undefined}
removeDocument={returnFalse}
@@ -354,7 +287,6 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
pinToPres={emptyFunction}
bringToFront={returnFalse}
dontRegisterView={true}
- dropAction={undefined}
/>
</div>
);
@@ -368,7 +300,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
*/
@undoBatch
changePermissions = (e: any, user: string) => {
- const docs = SelectionManager.Views().length < 2 ? (this.selectedDoc ? [this.selectedDoc] : []) : SelectionManager.Views().map(docView => docView.props.Document);
+ 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);
};
@@ -378,7 +310,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
getPermissionsSelect(user: string, permission: string) {
const dropdownValues: string[] = Object.values(SharingPermissions);
if (permission === '-multiple-') dropdownValues.unshift(permission);
- if (user === 'Override') dropdownValues.unshift('None');
+ if (user !== 'Override') dropdownValues.splice(dropdownValues.indexOf(SharingPermissions.Unset), 1);
return (
<select className="permissions-select" value={permission} onChange={e => this.changePermissions(e, user)}>
{dropdownValues
@@ -411,7 +343,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
*/
@computed get expansionIcon() {
return (
- <Tooltip title={<div className="dash-tooltip">{'Show more permissions'}</div>}>
+ <Tooltip title={<div className="dash-tooltip">Show more permissions</div>}>
<div
className="expansion-button"
onPointerDown={() => {
@@ -453,19 +385,9 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
* @returns the sharing and permissions panel.
*/
@computed get sharingTable() {
- const AclMap = new Map<symbol, string>([
- [AclUnset, 'None'],
- [AclPrivate, SharingPermissions.None],
- [AclReadonly, SharingPermissions.View],
- [AclAugment, SharingPermissions.Augment],
- [AclSelfEdit, SharingPermissions.SelfEdit],
- [AclEdit, SharingPermissions.Edit],
- [AclAdmin, SharingPermissions.Admin],
- ]);
-
// 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]));
+ SelectionManager.Views().length < 2 && this.selectedDoc ? [this.layoutDocAcls ? this.selectedDoc : this.dataDoc!] : SelectionManager.Views().map(docView => (this.layoutDocAcls ? docView.props.Document : docView.props.Document[DocData]));
const target = docs[0];
@@ -474,7 +396,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
const showAdmin = effectiveAcls.every(acl => acl === AclAdmin);
// users in common between all docs
- const commonKeys: string[] = intersection(...docs.map(doc => (this.layoutDocAcls ? doc?.[AclSym] && Object.keys(doc[AclSym]) : doc?.[DataSym][AclSym] && Object.keys(doc[DataSym][AclSym]))));
+ const commonKeys: string[] = intersection(...docs.map(doc => doc?.[DocAcl] && Object.keys(doc[DocAcl]).filter(key => key !== 'acl-Me')));
const tableEntries = [];
@@ -482,9 +404,9 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
if (commonKeys.length) {
for (const key of commonKeys) {
const name = denormalizeEmail(key.substring(4));
- const uniform = docs.every(doc => (this.layoutDocAcls ? doc?.[AclSym]?.[key] === docs[0]?.[AclSym]?.[key] : doc?.[DataSym]?.[AclSym]?.[key] === docs[0]?.[DataSym]?.[AclSym]?.[key]));
+ const uniform = docs.every(doc => doc?.[DocAcl]?.[key] === docs[0]?.[DocAcl]?.[key]);
if (name !== Doc.CurrentUserEmail && name !== target.author && name !== 'Public' && name !== 'Override' /* && sidebarUsersDisplayed![name] !== false*/) {
- tableEntries.push(this.sharingItem(name, showAdmin, uniform ? AclMap.get(this.layoutDocAcls ? target[AclSym][key] : target[DataSym][AclSym][key])! : '-multiple-'));
+ tableEntries.push(this.sharingItem(name, showAdmin, uniform ? HierarchyMapping.get(target[DocAcl][key])!.name : '-multiple-'));
}
}
}
@@ -492,11 +414,16 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
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-"));
- tableEntries.unshift(this.sharingItem('Public', showAdmin, docs.filter(doc => doc).every(doc => doc['acl-Public'] === docs[0]['acl-Public']) ? AclMap.get(target[AclSym]?.['acl-Public']) || SharingPermissions.None : '-multiple-'));
+ if (ownerSame) tableEntries.unshift(this.sharingItem(StrCast(target.author), showAdmin, 'Owner'));
+ tableEntries.unshift(this.sharingItem('Public', showAdmin, StrCast(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]) ? AclMap.get(effectiveAcls[0])! : '-multiple-', !ownerSame)
+ 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
+ )
);
- if (ownerSame) tableEntries.unshift(this.sharingItem(StrCast(target.author), showAdmin, 'Owner'));
return <div className="propertiesView-sharingTable">{tableEntries}</div>;
}
@@ -506,9 +433,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
}
@action
- toggleCheckbox = () => {
- this.layoutFields = !this.layoutFields;
- };
+ toggleCheckbox = () => (this.layoutFields = !this.layoutFields);
@computed get editableTitle() {
const titles = new Set<string>();
@@ -593,16 +518,6 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
<FontAwesomeIcon icon="bezier-curve" color="white" size="lg" />
</div>
</Tooltip>
- <Tooltip title={<div className="dash-tooltip">{InkStrokeProperties.Instance._lock ? 'Unlock ratio' : 'Lock ratio'}</div>}>
- <div className="inking-button-lock" onPointerDown={action(() => (InkStrokeProperties.Instance._lock = !InkStrokeProperties.Instance._lock))}>
- <FontAwesomeIcon icon={InkStrokeProperties.Instance._lock ? 'lock' : 'unlock'} color="white" size="lg" />
- </div>
- </Tooltip>
- <Tooltip title={<div className="dash-tooltip">{'Rotate 90Ëš'}</div>}>
- <div className="inking-button-rotate" onPointerDown={action(() => this.rotate(Math.PI / 2))}>
- <FontAwesomeIcon icon="undo" color="white" size="lg" />
- </div>
- </Tooltip>
</div>
);
}
@@ -616,17 +531,7 @@ 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" />
@@ -650,60 +555,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 'rot':
- this.rotate(dirs === 'up' ? 0.1 : -0.1);
- break;
- 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.strokeWidth = NumCast(this.selectedDoc?.strokeWidth) + (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));
- InkStrokeProperties.Instance._lock && this.selectedDoc && (this.selectedDoc._height = (NumCast(this.selectedDoc?._width) / oldWidth) * NumCast(this.selectedDoc?._height));
- 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));
- InkStrokeProperties.Instance._lock && this.selectedDoc && (this.selectedDoc._width = (NumCast(this.selectedDoc?._height) / oHeight) * NumCast(this.selectedDoc?._width));
- 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;
@@ -711,11 +606,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
};
getField(key: string) {
- //if (this.selectedDoc) {
return Field.toString(this.selectedDoc?.[key] as Field);
- // } else {
- // return undefined as Opt<string>;
- // }
}
@computed get shapeXps() {
@@ -724,9 +615,6 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
@computed get shapeYps() {
return this.getField('y');
}
- @computed get shapeRot() {
- return this.getField('rotation');
- }
@computed get shapeHgt() {
return this.getField('_height');
}
@@ -739,39 +627,22 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
set shapeYps(value) {
this.selectedDoc && (this.selectedDoc.y = Number(value));
}
- set shapeRot(value) {
- this.selectedDoc && (this.selectedDoc.rotation = Number(value));
- }
set shapeWid(value) {
- const oldWidth = NumCast(this.selectedDoc?._width);
this.selectedDoc && (this.selectedDoc._width = Number(value));
- InkStrokeProperties.Instance._lock && this.selectedDoc && (this.selectedDoc._height = (NumCast(this.selectedDoc?._width) * NumCast(this.selectedDoc?._height)) / oldWidth);
}
set shapeHgt(value) {
- const oldHeight = NumCast(this.selectedDoc?._height);
this.selectedDoc && (this.selectedDoc._height = Number(value));
- InkStrokeProperties.Instance._lock && this.selectedDoc && (this.selectedDoc._width = (NumCast(this.selectedDoc?._height) * NumCast(this.selectedDoc?._width)) / oldHeight);
}
@computed get hgtInput() {
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:'
);
}
@@ -779,82 +650,36 @@ 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:'
);
}
- @computed get rotInput() {
- return this.inputBoxDuo(
- 'rot',
- this.shapeRot,
- (val: string) => {
- if (!isNaN(Number(val))) {
- this.rotate(Number(val) - Number(this.shapeRot));
- this.shapeRot = val;
- }
- return true;
- },
- '∠:',
- 'rot',
- this.shapeRot,
- (val: string) => {
- if (!isNaN(Number(val))) {
- this.rotate(Number(val) - Number(this.shapeRot));
- this.shapeRot = val;
- }
- return true;
- },
- ''
- );
- }
@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={{
@@ -867,31 +692,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}
/>
);
}
@@ -900,22 +711,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() {
@@ -937,49 +746,36 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
);
}
- @computed get solidStk() {
- return this.selectedDoc?.color && (!this.selectedDoc?.strokeDash || this.selectedDoc?.strokeDash === '0') ? true : false;
- }
@computed get dashdStk() {
- return this.selectedDoc?.strokeDash || '';
- }
- @computed get unStrokd() {
- return this.selectedDoc?.color ? true : false;
+ return this.selectedDoc?.stroke_dash || '';
}
@computed get widthStk() {
- return this.getField('strokeWidth') || '1';
+ 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('strokeStartMarker') || '';
+ return this.getField('stroke_startMarker') || '';
}
@computed get markTail() {
- return this.getField('strokeEndMarker') || '';
- }
- set solidStk(value) {
- this.dashdStk = '';
- this.unStrokd = !value;
+ return this.getField('stroke_endMarker') || '';
}
set dashdStk(value) {
- value && (this._lastDash = value) && (this.unStrokd = false);
- this.selectedDoc && (this.selectedDoc.strokeDash = value ? this._lastDash : undefined);
+ 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.strokeWidth = Number(value));
- }
- set unStrokd(value) {
- this.colorStk = value ? '' : this._lastLine;
+ this.selectedDoc && (this.selectedDoc.stroke_width = Number(value));
}
set markHead(value) {
- this.selectedDoc && (this.selectedDoc.strokeStartMarker = value);
+ this.selectedDoc && (this.selectedDoc.stroke_startMarker = value);
}
set markTail(value) {
- this.selectedDoc && (this.selectedDoc.strokeEndMarker = value);
+ this.selectedDoc && (this.selectedDoc.stroke_endMarker = value);
}
@computed get stkInput() {
@@ -1087,10 +883,9 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
@computed get transformEditor() {
return (
<div className="transform-editor">
- {this.controlPointsButton}
+ {this.isInk ? this.controlPointsButton : null}
{this.hgtInput}
{this.XpsInput}
- {this.rotInput}
</div>
);
}
@@ -1145,62 +940,31 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
}
/**
- * Checks if a currentFilter (FilterDoc) exists on the current collection (if the Properties Panel + Filters submenu are open).
- * If it doesn't exist, it creates it.
- */
- checkFilterDoc() {
- if (!this.selectedDoc?.currentFilter) this.selectedDoc!.currentFilter = FilterBox.createFilterDoc();
- }
-
- /**
- * Creates a new currentFilter for this.filterDoc,
- */
- createNewFilterDoc = () => {
- if (this.selectedDoc) {
- const curFilterDoc = DocCast(this.selectedDoc.currentFilter);
- const currentDocFilters = this.selectedDoc._docFilters;
- const currentDocRangeFilters = this.selectedDoc._docRangeFilters;
- this.selectedDoc._docFilters = new List<string>();
- this.selectedDoc._docRangeFilters = new List<string>();
- if (DocListCast(Doc.UserDoc().savedFilters).includes(curFilterDoc)) {
- curFilterDoc._docFiltersList = currentDocFilters;
- curFilterDoc._docRangeFiltersList = currentDocRangeFilters;
- this.selectedDoc.currentFilter = FilterBox.createFilterDoc();
- } else {
- Doc.GetProto(curFilterDoc).data = undefined;
- Doc.GetProto(curFilterDoc).title = 'Unnamed Filter';
- curFilterDoc._docFiltersList = undefined;
- curFilterDoc._docRangeFiltersList = undefined;
- }
- }
- };
-
- /**
- * 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 !(this.selectedDoc?.currentFilter instanceof Doc) ? null : (
+ return (
<div className="propertiesView-filters">
<div className="propertiesView-filters-title" onPointerDown={action(() => (this.openFilters = !this.openFilters))} style={{ backgroundColor: this.openFilters ? 'black' : '' }}>
Filters
@@ -1209,35 +973,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
</div>
</div>
{!this.openFilters ? null : (
- <div className="propertiesView-filters-content" style={{ height: this.selectedDoc.currentFilter[HeightSym]() + 15 }}>
- <DocumentView
- Document={this.selectedDoc.currentFilter}
- DataDoc={undefined}
- addDocument={undefined}
- addDocTab={returnFalse}
- pinToPres={emptyFunction}
- rootSelected={returnTrue}
- removeDocument={returnFalse}
- ScreenToLocalTransform={this.getTransform}
- PanelWidth={() => this.props.width}
- PanelHeight={this.selectedDoc.currentFilter[HeightSym]}
- renderDepth={0}
- scriptContext={this.selectedDoc.currentFilter}
- focus={emptyFunction}
- styleProvider={DefaultStyleProvider}
- isContentActive={returnTrue}
- whenChildContentsActiveChanged={emptyFunction}
- bringToFront={emptyFunction}
- docFilters={returnEmptyFilter}
- docRangeFilters={returnEmptyFilter}
- searchFilterDocs={returnEmptyDoclist}
- ContainingCollectionView={undefined}
- ContainingCollectionDoc={undefined}
- createNewFilterDoc={this.createNewFilterDoc}
- updateFilterDoc={this.updateFilterDoc}
- docViewPath={returnEmptyDoclist}
- dontCenter="y"
- />
+ <div className="propertiesView-filters-content" style={{ position: 'relative', height: 'auto' }}>
+ <FilterPanel rootDoc={this.selectedDoc ?? Doc.ActiveDashboard!} />
</div>
)}
</div>
@@ -1259,17 +996,15 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
</div>
)}
- {this.isInk ? (
- <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 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>
- {this.openTransform ? <div className="propertiesView-transform-content">{this.transformEditor}</div> : null}
</div>
- ) : null}
+ {this.openTransform ? <div className="propertiesView-transform-content">{this.transformEditor}</div> : null}
+ </div>
</>
);
}
@@ -1336,49 +1071,52 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
);
}
- @observable description = Field.toString(LinkManager.currentLink?.description as any as Field);
- @observable relationship = StrCast(LinkManager.currentLink?.linkRelationship);
+ @computed get description() {
+ return Field.toString(LinkManager.currentLink?.link_description as any as Field);
+ }
+ @computed get relationship() {
+ return StrCast(LinkManager.currentLink?.link_relationship);
+ }
@observable private relationshipButtonColor: string = '';
// @action
- // handleDescriptionChange = (e: React.ChangeEvent<HTMLInputElement>) => { this.description = e.target.value; }
- // handleRelationshipChange = (e: React.ChangeEvent<HTMLInputElement>) => { this.relationship = e.target.value; }
-
- @undoBatch
- handleDescriptionChange = action((value: string) => {
- if (LinkManager.currentLink && this.selectedDoc) {
- this.selectedDoc.description = value;
- this.description = value;
- return true;
- }
- });
+ // handleDescriptionChange = (e: React.ChangeEvent<HTMLInputElement>) => { this.link_description = e.target.value; }
+ // handleRelationshipChange = (e: React.ChangeEvent<HTMLInputElement>) => { this.link_relationship = e.target.value; }
- @undoBatch
- handleLinkRelationshipChange = action((value: string) => {
- if (LinkManager.currentLink && this.selectedDoc) {
- this.selectedDoc.linkRelationship = value;
- this.relationship = 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).description = value;
- return true;
+ Doc.GetProto(LinkManager.currentLink).link_description = value;
}
});
@undoBatch
- setLinkRelationshipValue = action((value: string) => {
+ setlinkRelationshipValue = action((value: string) => {
if (LinkManager.currentLink) {
- const prevRelationship = LinkManager.currentLink.linkRelationship as string;
- LinkManager.currentLink.linkRelationship = value;
- Doc.GetProto(LinkManager.currentLink).linkRelationship = value;
- const linkRelationshipList = StrListCast(Doc.UserDoc().linkRelationshipList);
- const linkRelationshipSizes = NumListCast(Doc.UserDoc().linkRelationshipSizes);
- const linkColorList = StrListCast(Doc.UserDoc().linkColorList);
+ const prevRelationship = StrCast(LinkManager.currentLink.link_relationship);
+ LinkManager.currentLink.link_relationship = value;
+ 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().link_ColorList);
// if the relationship does not exist in the list, add it and a corresponding unique randomly generated color
if (!linkRelationshipList?.includes(value)) {
@@ -1412,20 +1150,34 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
}
});
+ changeFollowBehavior = undoable((loc: Opt<string>) => this.sourceAnchor && (this.sourceAnchor.followLinkLocation = loc), 'change follow behavior');
+
@undoBatch
- changeFollowBehavior = action((follow: string) => {
- if (LinkManager.currentLink && this.selectedDoc) {
- this.selectedDoc.followLinkLocation = follow;
- return true;
- }
- });
+ changeAnimationBehavior = action((behavior: string) => this.sourceAnchor && (this.sourceAnchor.followLinkAnimEffect = behavior));
+
+ @undoBatch
+ changeEffectDirection = action((effect: PresEffectDirection) => this.sourceAnchor && (this.sourceAnchor.followLinkAnimDirection = effect));
+
+ animationDirection = (direction: PresEffectDirection, icon: string, gridColumn: number, gridRow: number, opts: object) => {
+ const lanch = this.sourceAnchor;
+ const color = lanch?.followLinkAnimDirection === direction || (direction === PresEffectDirection.Center && !lanch?.followLinkAnimDirection) ? Colors.MEDIUM_BLUE : '';
+ return (
+ <Tooltip title={<div className="dash-tooltip">{direction}</div>}>
+ <div
+ style={{ ...opts, border: direction === PresEffectDirection.Center ? `solid 2px ${color}` : undefined, borderRadius: '20%', cursor: 'pointer', gridColumn, gridRow, justifySelf: 'center', background: color, color: 'black' }}
+ onClick={() => this.changeEffectDirection(direction)}>
+ {icon ? <FontAwesomeIcon icon={icon as any} /> : null}
+ </div>
+ </Tooltip>
+ );
+ };
onSelectOutDesc = () => {
this.setDescripValue(this.description);
document.getElementById('link_description_input')?.blur();
};
- onDescriptionKey = (e: React.KeyboardEvent<HTMLInputElement>) => {
+ onDescriptionKey = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter') {
this.setDescripValue(this.description);
document.getElementById('link_description_input')?.blur();
@@ -1433,30 +1185,48 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
};
onSelectOutRelationship = () => {
- this.setLinkRelationshipValue(this.relationship);
+ this.setlinkRelationshipValue(this.relationship);
document.getElementById('link_relationship_input')?.blur();
};
onRelationshipKey = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
- this.setLinkRelationshipValue(this.relationship);
+ this.setlinkRelationshipValue(this.relationship);
document.getElementById('link_relationship_input')?.blur();
}
};
- toggleAnchor = (e: React.PointerEvent) => {
- setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => this.selectedDoc && (this.selectedDoc.linkAutoMove = !this.selectedDoc.linkAutoMove))));
+ toggleLinkProp = (e: React.PointerEvent, prop: string) => {
+ setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => LinkManager.currentLink && (LinkManager.currentLink[prop] = !LinkManager.currentLink[prop]))));
};
- toggleArrow = (e: React.PointerEvent) => {
- setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => this.selectedDoc && (this.selectedDoc.displayArrow = !this.selectedDoc.displayArrow))));
- };
+ @computed get destinationAnchor() {
+ const ldoc = LinkManager.currentLink;
+ const lanch = this.selectedDocumentView?.anchorViewDoc ?? LinkManager.currentLinkAnchor;
+ if (ldoc && lanch) return LinkManager.getOppositeAnchor(ldoc, lanch) ?? lanch;
+ return ldoc ? DocCast(ldoc.link_anchor_2) : ldoc;
+ }
- toggleZoomToTarget1 = (e: React.PointerEvent) => {
- setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => this.selectedDoc && (DocCast(this.selectedDoc.anchor1).followLinkZoom = !DocCast(this.selectedDoc.anchor1).followLinkZoom))));
- };
- toggleZoomToTarget2 = (e: React.PointerEvent) => {
- setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => this.selectedDoc && (DocCast(this.selectedDoc.anchor2).followLinkZoom = !DocCast(this.selectedDoc.anchor2).followLinkZoom))));
+ @computed get sourceAnchor() {
+ const selAnchor = this.selectedDocumentView?.anchorViewDoc ?? LinkManager.currentLinkAnchor;
+
+ return selAnchor ?? (LinkManager.currentLink && this.destinationAnchor ? LinkManager.getOppositeAnchor(LinkManager.currentLink, this.destinationAnchor) : LinkManager.currentLink);
+ }
+
+ toggleAnchorProp = (e: React.PointerEvent, prop: string, anchor?: Doc, value: any = true, ovalue: any = false, cb: (val: any) => any = val => val) => {
+ anchor &&
+ setupMoveUpEvents(
+ this,
+ e,
+ returnFalse,
+ emptyFunction,
+ undoBatch(
+ action(() => {
+ anchor[prop] = anchor[prop] === value ? ovalue : value;
+ this.selectedDoc && cb(anchor[prop]);
+ })
+ )
+ );
};
@computed
@@ -1465,10 +1235,10 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
<input
autoComplete={'off'}
id="link_relationship_input"
- value={StrCast(this.selectedDoc?.linkRelationship)}
+ value={StrCast(LinkManager.currentLink?.link_relationship)}
onKeyDown={this.onRelationshipKey}
onBlur={this.onSelectOutRelationship}
- onChange={e => this.handleLinkRelationshipChange(e.currentTarget.value)}
+ onChange={e => this.handlelinkRelationshipChange(e.currentTarget.value)}
className="text"
type="text"
/>
@@ -1478,19 +1248,28 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
@computed
get editDescription() {
return (
- <input
- autoComplete={'off'}
+ <textarea
+ autoComplete="off"
+ style={{ textAlign: 'left' }}
id="link_description_input"
- value={StrCast(this.selectedDoc?.description)}
+ value={StrCast(LinkManager.currentLink?.link_description)}
onKeyDown={this.onDescriptionKey}
onBlur={this.onSelectOutDesc}
onChange={e => this.handleDescriptionChange(e.currentTarget.value)}
className="text"
- type="text"
/>
);
}
+ // Converts seconds to ms and updates presTransition
+ setZoom = (number: String, change?: number) => {
+ let scale = Number(number) / 100;
+ if (change) scale += change;
+ if (scale < 0.01) scale = 0.01;
+ if (scale > 1) scale = 1;
+ this.sourceAnchor && (this.sourceAnchor.followLinkZoomScale = scale);
+ };
+
/**
* Handles adding and removing members from the sharing panel
*/
@@ -1504,6 +1283,10 @@ 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 (
<div className="propertiesView" style={{ width: this.props.width }}>
@@ -1513,81 +1296,6 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
</div>
);
} else {
- if (this.selectedDoc && this.isLink) {
- return (
- <div className="propertiesView">
- <div className="propertiesView-title">Linking</div>
- <div className="propertiesView-section">
- <p className="propertiesView-label">Information</p>
- <div className="propertiesView-input first">
- <p>Link Relationship</p>
- {this.editRelationship}
- </div>
- <div className="propertiesView-input">
- <p>Description</p>
- {this.editDescription}
- </div>
- </div>
- <div className="propertiesView-section">
- <p className="propertiesView-label">Behavior</p>
- <div className="propertiesView-input inline first">
- <p>Follow</p>
- <select name="selectList" id="selectList" onChange={e => this.changeFollowBehavior(e.currentTarget.value)} value={StrCast(this.selectedDoc.followLinkLocation, 'default')}>
- <option value="default">Default</option>
- <option value="add:left">Open in new left pane</option>
- <option value="add:right">Open in new right pane</option>
- <option value="replace:left">Replace left tab</option>
- <option value="replace:right">Replace right tab</option>
- <option value="fullScreen">Open full screen</option>
- <option value="add">Open in new tab</option>
- <option value="replace">Replace current tab</option>
- {this.selectedDoc.linksToAnnotation ? <option value="openExternal">Open in external page</option> : null}
- </select>
- </div>
- <div className="propertiesView-input inline">
- <p>Auto-move anchor</p>
- <button
- style={{ background: this.selectedDoc.hidden ? 'gray' : !this.selectedDoc.linkAutoMove ? '' : '#4476f7', borderRadius: 3 }}
- onPointerDown={this.toggleAnchor}
- onClick={e => e.stopPropagation()}
- className="propertiesButton">
- <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" />
- </button>
- </div>
- <div className="propertiesView-input inline">
- <p>Display arrow</p>
- <button
- style={{ background: this.selectedDoc.hidden ? 'gray' : !this.selectedDoc.displayArrow ? '' : '#4476f7', borderRadius: 3 }}
- onPointerDown={this.toggleArrow}
- onClick={e => e.stopPropagation()}
- className="propertiesButton">
- <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" />
- </button>
- </div>
- <div className="propertiesView-input inline">
- <p>Zoom to target</p>
- <button
- style={{ background: this.selectedDoc.hidden ? 'gray' : !Cast(this.selectedDoc.anchor1, Doc, null).followLinkZoom ? '' : '#4476f7', borderRadius: 3 }}
- onPointerDown={this.toggleZoomToTarget1}
- onClick={e => e.stopPropagation()}
- className="propertiesButton">
- <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" />
- </button>
- </div>
- <div className="propertiesView-input inline">
- <p>Zoom to source</p>
- <button
- style={{ background: this.selectedDoc.hidden ? 'gray' : !Cast(this.selectedDoc.anchor2, Doc, null).followLinkZoom ? '' : '#4476f7', borderRadius: 3 }}
- onPointerDown={this.toggleZoomToTarget2}
- onClick={e => e.stopPropagation()}
- className="propertiesButton">
- <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" />
- </button>
- </div>
- </div>
- </div>
- );
- }
if (this.selectedDoc && !this.isPres) {
return (
<div
@@ -1605,6 +1313,220 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
{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?.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.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 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>
+ )}
+ </>
+ )}
{this.inkSubMenu}
@@ -1620,9 +1542,11 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
</div>
);
}
- if (this.isPres) {
- const selectedItem: boolean = PresBox.Instance?.selectedArray.size > 0;
- const type = PresBox.Instance.activeItem?.type;
+ 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 }}>
@@ -1631,18 +1555,13 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
<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 : (
<div className="propertiesView-presTrails">
- <div
- className="propertiesView-presTrails-title"
- onPointerDown={action(() => {
- this.openPresTransitions = !this.openPresTransitions;
- })}
- style={{ backgroundColor: this.openPresTransitions ? 'black' : '' }}>
+ <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" />
@@ -1651,27 +1570,34 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
{this.openPresTransitions ? <div className="propertiesView-presTrails-content">{PresBox.Instance.transitionDropdown}</div> : null}
</div>
)}
- {/* {!selectedItem || type === DocumentType.VID || type === DocumentType.AUDIO ? (null) : <div className="propertiesView-presTrails">
- <div className="propertiesView-presTrails-title"
- onPointerDown={action(() => this.openPresProgressivize = !this.openPresProgressivize)}
- style={{ backgroundColor: this.openPresProgressivize ? "black" : "" }}>
- &nbsp; <FontAwesomeIcon style={{ alignSelf: "center" }} icon={"tasks"} /> &nbsp; Progressivize
- <div className="propertiesView-presTrails-title-icon">
- <FontAwesomeIcon icon={this.openPresProgressivize ? "caret-down" : "caret-right"} size="lg" color="white" />
- </div>
- </div>
- {this.openPresProgressivize ? <div className="propertiesView-presTrails-content">
- {PresBox.Instance.progressivizeDropdown}
- </div> : null}
- </div>} */}
- {!selectedItem || (type !== DocumentType.VID && type !== DocumentType.AUDIO) ? null : (
+ {!selectedItem ? null : (
<div className="propertiesView-presTrails">
<div
className="propertiesView-presTrails-title"
- onPointerDown={action(() => {
- this.openSlideOptions = !this.openSlideOptions;
- })}
- style={{ backgroundColor: this.openSlideOptions ? 'black' : '' }}>
+ onPointerDown={action(() => (this.openPresVisibilityAndDuration = !this.openPresVisibilityAndDuration))}
+ 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" />
+ </div>
+ </div>
+ {this.openPresVisibilityAndDuration ? <div className="propertiesView-presTrails-content">{PresBox.Instance.visibiltyDurationDropdown}</div> : null}
+ </div>
+ )}
+ {!selectedItem ? null : (
+ <div className="propertiesView-presTrails">
+ <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" />
+ </div>
+ </div>
+ {this.openPresProgressivize ? <div className="propertiesView-presTrails-content">{PresBox.Instance.progressivizeDropdown}</div> : null}
+ </div>
+ )}
+ {!selectedItem || (type !== DocumentType.VID && type !== DocumentType.AUDIO) ? null : (
+ <div className="propertiesView-presTrails">
+ <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" />
@@ -1680,19 +1606,6 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
{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.scss b/src/client/views/ScriptingRepl.scss
index 778e9c445..adc82238e 100644
--- a/src/client/views/ScriptingRepl.scss
+++ b/src/client/views/ScriptingRepl.scss
@@ -31,6 +31,9 @@
border-radius: 25%;
padding: 2px;
}
+.documentIcon-outerDiv:hover {
+ opacity: 0.3;
+}
.scriptingObject-icon {
padding: 3px;
@@ -48,4 +51,4 @@
.scriptingObject-leaf {
margin-left: 15px;
-} \ No newline at end of file
+}
diff --git a/src/client/views/ScriptingRepl.tsx b/src/client/views/ScriptingRepl.tsx
index 4fecfa4d9..5dfe10def 100644
--- a/src/client/views/ScriptingRepl.tsx
+++ b/src/client/views/ScriptingRepl.tsx
@@ -10,34 +10,49 @@ import { OverlayView } from './OverlayView';
import './ScriptingRepl.scss';
@observer
-export class ScriptingObjectDisplay extends React.Component<{ scrollToBottom: () => void, value: { [key: string]: any }, name?: string }> {
+export class ScriptingObjectDisplay extends React.Component<{ scrollToBottom: () => void; value: { [key: string]: any }; name?: string }> {
@observable collapsed = true;
@action
toggle = () => {
this.collapsed = !this.collapsed;
this.props.scrollToBottom();
- }
+ };
render() {
const val = this.props.value;
const proto = Object.getPrototypeOf(val);
const name = (proto && proto.constructor && proto.constructor.name) || String(val);
- const title = this.props.name ? <><b>{this.props.name} : </b>{name}</> : name;
+ const title = this.props.name ? (
+ <>
+ <b>{this.props.name} : </b>
+ {name}
+ </>
+ ) : (
+ name
+ );
if (this.collapsed) {
return (
<div className="scriptingObject-collapsed">
- <span onClick={this.toggle} className="scriptingObject-icon scriptingObject-iconCollapsed"><FontAwesomeIcon icon="caret-right" size="sm" /></span>{title} (+{Object.keys(val).length})
+ <span onClick={this.toggle} className="scriptingObject-icon scriptingObject-iconCollapsed">
+ <FontAwesomeIcon icon="caret-right" size="sm" />
+ </span>
+ {title} (+{Object.keys(val).length})
</div>
);
} else {
return (
<div className="scriptingObject-open">
<div>
- <span onClick={this.toggle} className="scriptingObject-icon"><FontAwesomeIcon icon="caret-down" size="sm" /></span>{title}
+ <span onClick={this.toggle} className="scriptingObject-icon">
+ <FontAwesomeIcon icon="caret-down" size="sm" />
+ </span>
+ {title}
</div>
<div className="scriptingObject-fields">
- {Object.keys(val).map(key => <ScriptingValueDisplay {...this.props} name={key} />)}
+ {Object.keys(val).map(key => (
+ <ScriptingValueDisplay {...this.props} name={key} />
+ ))}
</div>
</div>
);
@@ -46,18 +61,32 @@ export class ScriptingObjectDisplay extends React.Component<{ scrollToBottom: ()
}
@observer
-export class ScriptingValueDisplay extends React.Component<{ scrollToBottom: () => void, value: any, name?: string }> {
+export class ScriptingValueDisplay extends React.Component<{ scrollToBottom: () => void; value: any; name?: string }> {
render() {
const val = this.props.name ? this.props.value[this.props.name] : this.props.value;
- if (typeof val === "object") {
+ if (typeof val === 'object') {
return <ScriptingObjectDisplay scrollToBottom={this.props.scrollToBottom} value={val} name={this.props.name} />;
- } else if (typeof val === "function") {
- const name = "[Function]";
- const title = this.props.name ? <><b>{this.props.name} : </b>{name}</> : name;
+ } else if (typeof val === 'function') {
+ const name = '[Function]';
+ const title = this.props.name ? (
+ <>
+ <b>{this.props.name} : </b>
+ {name}
+ </>
+ ) : (
+ name
+ );
return <div className="scriptingObject-leaf">{title}</div>;
} else {
const name = String(val);
- const title = this.props.name ? <><b>{this.props.name} : </b>{name}</> : name;
+ const title = this.props.name ? (
+ <>
+ <b>{this.props.name} : </b>
+ {name}
+ </>
+ ) : (
+ name
+ );
return <div className="scriptingObject-leaf">{title}</div>;
}
}
@@ -65,11 +94,11 @@ export class ScriptingValueDisplay extends React.Component<{ scrollToBottom: ()
@observer
export class ScriptingRepl extends React.Component {
- @observable private commands: { command: string, result: any }[] = [];
+ @observable private commands: { command: string; result: any }[] = [];
private commandsHistory: string[] = [];
- @observable private commandString: string = "";
- private commandBuffer: string = "";
+ @observable private commandString: string = '';
+ private commandBuffer: string = '';
@observable private historyIndex: number = -1;
@@ -82,7 +111,7 @@ export class ScriptingRepl extends React.Component {
transformer: context => {
const knownVars: { [name: string]: number } = {};
const usedDocuments: number[] = [];
- ScriptingGlobals.getGlobals().forEach((global: any) => knownVars[global] = 1);
+ ScriptingGlobals.getGlobals().forEach((global: any) => (knownVars[global] = 1));
return root => {
function visit(node: ts.Node) {
let skip = false;
@@ -105,7 +134,7 @@ export class ScriptingRepl extends React.Component {
const m = parseInt(match[1]);
usedDocuments.push(m);
} else {
- return ts.createPropertyAccess(ts.createIdentifier("args"), node);
+ return ts.createPropertyAccess(ts.createIdentifier('args'), node);
}
}
}
@@ -114,20 +143,20 @@ export class ScriptingRepl extends React.Component {
}
return ts.visitNode(root, visit);
};
- }
+ },
};
- }
+ };
@action
onKeyDown = (e: React.KeyboardEvent) => {
let stopProp = true;
switch (e.key) {
- case "Enter": {
+ case 'Enter': {
e.stopPropagation();
const docGlobals: { [name: string]: any } = {};
- Array.from(DocumentManager.Instance.DocumentViews).forEach((dv, i) => docGlobals[`d${i}`] = dv.props.Document);
+ DocumentManager.Instance.DocumentViews.forEach((dv, i) => (docGlobals[`d${i}`] = dv.props.Document));
const globals = ScriptingGlobals.makeMutableGlobalsCopy(docGlobals);
- const script = CompileScript(this.commandString, { typecheck: false, addReturn: true, editable: true, params: { args: "any" }, transformer: this.getTransformer(), globals });
+ const script = CompileScript(this.commandString, { typecheck: false, addReturn: true, editable: true, params: { args: 'any' }, transformer: this.getTransformer(), globals });
if (!script.compiled) {
this.commands.push({ command: this.commandString, result: script.errors });
return;
@@ -139,13 +168,13 @@ export class ScriptingRepl extends React.Component {
this.maybeScrollToBottom();
- this.commandString = "";
- this.commandBuffer = "";
+ this.commandString = '';
+ this.commandBuffer = '';
this.historyIndex = -1;
}
break;
}
- case "ArrowUp": {
+ case 'ArrowUp': {
if (this.historyIndex < this.commands.length - 1) {
this.historyIndex++;
if (this.historyIndex === 0) {
@@ -155,12 +184,12 @@ export class ScriptingRepl extends React.Component {
}
break;
}
- case "ArrowDown": {
+ case 'ArrowDown': {
if (this.historyIndex >= 0) {
this.historyIndex--;
if (this.historyIndex === -1) {
this.commandString = this.commandBuffer;
- this.commandBuffer = "";
+ this.commandBuffer = '';
} else {
this.commandString = this.commandsHistory[this.commands.length - 1 - this.historyIndex];
}
@@ -176,25 +205,25 @@ export class ScriptingRepl extends React.Component {
e.stopPropagation();
e.preventDefault();
}
- }
+ };
@action
onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
this.commandString = e.target.value;
- }
+ };
private shouldScroll: boolean = false;
private maybeScrollToBottom = () => {
const ele = this.commandsRef.current;
- if (ele && ele.scrollTop === (ele.scrollHeight - ele.offsetHeight)) {
+ if (ele && ele.scrollTop === ele.scrollHeight - ele.offsetHeight) {
this.shouldScroll = true;
this.forceUpdate();
}
- }
+ };
private scrollToBottom() {
const ele = this.commandsRef.current;
- ele && ele.scroll({ behavior: "auto", top: ele.scrollHeight });
+ ele && ele.scroll({ behavior: 'auto', top: ele.scrollHeight });
}
componentDidUpdate() {
@@ -206,15 +235,11 @@ export class ScriptingRepl extends React.Component {
overlayDisposer?: () => void;
onFocus = () => {
- if (this.overlayDisposer) {
- this.overlayDisposer();
- }
+ this.overlayDisposer?.();
this.overlayDisposer = OverlayView.Instance.addElement(<DocumentIconContainer />, { x: 0, y: 0 });
- }
+ };
- onBlur = () => {
- this.overlayDisposer?.();
- }
+ onBlur = () => this.overlayDisposer?.();
render() {
return (
@@ -229,14 +254,8 @@ export class ScriptingRepl extends React.Component {
);
})}
</div>
- <input
- className="scriptingRepl-commandInput"
- onFocus={this.onFocus}
- onBlur={this.onBlur}
- value={this.commandString}
- onChange={this.onChange}
- onKeyDown={this.onKeyDown}></input>
+ <input className="scriptingRepl-commandInput" onFocus={this.onFocus} onBlur={this.onBlur} value={this.commandString} onChange={this.onChange} onKeyDown={this.onKeyDown}></input>
</div>
);
}
-} \ No newline at end of file
+}
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 78480da03..db273cc88 100644
--- a/src/client/views/SidebarAnnos.tsx
+++ b/src/client/views/SidebarAnnos.tsx
@@ -3,10 +3,12 @@ import { observer } from 'mobx-react';
import { Doc, DocListCast, Field, FieldResult, StrListCast } from '../../fields/Doc';
import { Id } from '../../fields/FieldSymbols';
import { List } from '../../fields/List';
+import { RichTextField } from '../../fields/RichTextField';
import { DocCast, NumCast, StrCast } from '../../fields/Types';
-import { emptyFunction, OmitKeys, returnAll, returnOne, returnTrue, returnZero } from '../../Utils';
+import { emptyFunction, returnAll, returnFalse, returnOne, returnTrue, returnZero } from '../../Utils';
import { Docs, DocUtils } from '../documents/Documents';
import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
+import { LinkManager } from '../util/LinkManager';
import { Transform } from '../util/Transform';
import { CollectionStackingView } from './collections/CollectionStackingView';
import { FieldViewProps } from './nodes/FieldView';
@@ -15,7 +17,6 @@ import { SearchBox } from './search/SearchBox';
import './SidebarAnnos.scss';
import { StyleProp } from './StyleProvider';
import React = require('react');
-import { RichTextField } from '../../fields/RichTextField';
interface ExtraProps {
fieldKey: string;
@@ -25,6 +26,7 @@ interface ExtraProps {
// usePanelWidth: boolean;
showSidebar: boolean;
nativeWidth: number;
+ usePanelWidth?: boolean;
whenChildContentsActiveChanged: (isActive: boolean) => void;
ScreenToLocalTransform: () => Transform;
sidebarAddDocument: (doc: Doc | Doc[], suffix: string) => boolean;
@@ -47,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, {
@@ -65,17 +75,17 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
annotationOn: this.props.rootDoc,
_width: 200,
_height: 50,
- _fitWidth: true,
- _autoHeight: true,
- _isLinkButton: true,
- _fontSize: StrCast(Doc.UserDoc().fontSize),
- _fontFamily: StrCast(Doc.UserDoc().fontFamily),
+ _layout_fitWidth: true,
+ _layout_autoHeight: true,
+ _text_fontSize: StrCast(Doc.UserDoc().fontSize),
+ _text_fontFamily: StrCast(Doc.UserDoc().fontFamily),
});
FormattedTextBox.SelectOnLoad = target[Id];
FormattedTextBox.DontSelectInitialText = true;
- DocUtils.MakeLink({ doc: anchor }, { doc: target }, 'inline comment:comment on');
+ const link = DocUtils.MakeLink(anchor, target, { link_relationship: 'inline comment:comment on' });
+ 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];
@@ -83,21 +93,36 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
Doc.GetProto(target)[key] = val;
return {
type: 'dashField',
- attrs: { fieldKey: key, docid: '', hideKey: false, editable: true },
- marks: [
- { type: 'pFontSize', attrs: { fontSize: '12px' } },
- { type: 'pFontFamily', attrs: { family: 'Arial' } },
- { type: 'pFontColor', attrs: { color: 'black' } },
- { type: 'strong' },
- { type: 'user_mark', attrs: { userid: Doc.CurrentUserEmail, modified: 0 } },
- ],
+ attrs: { fieldKey: key, docId: '', hideKey: false, editable: true },
+ marks: [{ type: 'pFontSize', attrs: { fontSize: '12px' } }, { type: 'strong' }, { type: 'user_mark', attrs: { userid: Doc.CurrentUserEmail, modified: 0 } }],
};
});
- const textLines = [
+
+ if (!anchor.text) Doc.GetProto(anchor).text = '-selection-';
+ const textLines: any = [
{
type: 'paragraph',
attrs: { align: null, color: null, id: null, indent: null, inset: null, lineSpacing: null, paddingBottom: null, paddingTop: null },
- content: [{ type: 'dashField', attrs: { fieldKey: 'text', docid: anchor[Id], hideKey: true, editable: false } }],
+ content: [
+ {
+ type: 'dashField',
+ marks: [
+ {
+ type: 'linkAnchor',
+ attrs: {
+ allAnchors: [{ href: `/doc/${target[Id]}`, title: 'Anchored Selection', anchorId: `${target[Id]}` }],
+ location: 'add:right',
+ title: 'Anchored Selection',
+ noPreview: true,
+ docref: false,
+ },
+ },
+ { type: 'pFontSize', attrs: { fontSize: '8px' } },
+ { type: 'em' },
+ ],
+ attrs: { fieldKey: 'text', docId: anchor[Id], hideKey: true, editable: false },
+ },
+ ],
},
{ type: 'paragraph', attrs: { align: null, color: null, id: null, indent: null, inset: null, lineSpacing: null, paddingBottom: null, paddingTop: null } },
];
@@ -107,22 +132,24 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
content: taggedContent,
};
if (taggedContent.length) textLines.push(metadatatext);
- Doc.GetProto(target).text = new RichTextField(
- JSON.stringify({
- doc: {
- type: 'doc',
- content: textLines,
- },
- selection: { type: 'text', anchor: 4, head: 4 }, // set selection to middle paragraph
- }),
- ''
- );
+ if (textLines.length) {
+ Doc.GetProto(target).text = new RichTextField(
+ JSON.stringify({
+ doc: {
+ type: 'doc',
+ content: textLines,
+ },
+ selection: { type: 'text', anchor: 4, head: 4 }, // set selection to middle paragraph
+ }),
+ ''
+ );
+ }
this.addDocument(target);
- this._stackRef.current?.focusDocument(target, {});
+ setTimeout(() => this._stackRef.current?.focusDocument(target, {}));
return target;
};
makeDocUnfiltered = (doc: Doc) => {
- if (DocListCast(this.props.rootDoc[this.sidebarKey]).includes(doc)) {
+ 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>();
}
@@ -132,7 +159,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
};
get sidebarKey() {
- return this.props.fieldKey + '-sidebar';
+ return this.props.fieldKey + '_sidebar';
}
filtersHeight = () => 38;
screenToLocalTransform = () =>
@@ -140,32 +167,29 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
.ScreenToLocalTransform()
.translate(Doc.NativeWidth(this.props.dataDoc), 0)
.scale(this.props.NativeDimScaling?.() || 1);
- // panelWidth = () => !this.props.layoutDoc._showSidebar ? 0 :
- // this.props.usePanelWidth ? this.props.PanelWidth() :
- // (NumCast(this.props.layoutDoc.nativeWidth) - Doc.NativeWidth(this.props.dataDoc)) * this.props.PanelWidth() / NumCast(this.props.layoutDoc.nativeWidth);
panelWidth = () =>
!this.props.showSidebar
? 0
- : this.props.layoutDoc.type === DocumentType.RTF || this.props.layoutDoc.type === DocumentType.MAP
+ : this.props.usePanelWidth // [DocumentType.RTF, DocumentType.MAP].includes(this.props.layoutDoc.type as any)
? this.props.PanelWidth() / (this.props.NativeDimScaling?.() || 1)
: ((NumCast(this.props.nativeWidth) - Doc.NativeWidth(this.props.dataDoc)) * this.props.PanelWidth()) / NumCast(this.props.nativeWidth);
panelHeight = () => this.props.PanelHeight() - this.filtersHeight();
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])];
- showTitle = () => 'title';
+ 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) => {
- const ay = DocListCast(a.links).length && DocCast(DocListCast(a.links)[0].anchor1).y;
- const by = DocListCast(b.links).length && DocCast(DocListCast(b.links)[0].anchor1).y;
+ const ay = LinkManager.Links(a).length && DocCast(LinkManager.Links(a)[0].link_anchor_1).y;
+ const by = LinkManager.Links(b).length && DocCast(LinkManager.Links(b)[0].link_anchor_1).y;
return NumCast(ay) - NumCast(by);
};
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>
);
@@ -192,7 +216,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
style={{
position: 'absolute',
pointerEvents: this.props.isContentActive() ? 'all' : undefined,
- top: this.props.rootDoc.type !== DocumentType.RTF && StrCast(this.props.rootDoc._showTitle) === 'title' ? 15 : 0,
+ top: this.props.rootDoc.type !== DocumentType.RTF && StrCast(this.props.rootDoc._layout_showTitle) === 'title' ? 15 : 0,
right: 0,
background: this.props.styleProvider?.(this.props.rootDoc, this.props, StyleProp.WidgetColor),
width: `100%`,
@@ -200,36 +224,37 @@ 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)))}
</div>
<div style={{ width: '100%', height: `calc(100% - 38px)`, position: 'relative' }}>
<CollectionStackingView
- {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
- ref={this._stackRef}
+ {...this.props}
+ setContentView={emptyFunction}
NativeWidth={returnZero}
NativeHeight={returnZero}
+ ref={this._stackRef}
PanelHeight={this.panelHeight}
PanelWidth={this.panelWidth}
- docFilters={this.docFilters}
- scaleField={this.sidebarKey + '-scale'}
+ childFilters={this.childFilters}
sortFunc={this.sortByLinkAnchorY}
setHeight={this.setHeightCallback}
isAnnotationOverlay={false}
select={emptyFunction}
NativeDimScaling={returnOne}
- childShowTitle={this.showTitle}
+ //childlayout_showTitle={this.layout_showTitle}
+ isAnyChildContentActive={returnFalse}
childDocumentsActive={this.props.isContentActive}
whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged}
childHideDecorationTitle={returnTrue}
removeDocument={this.removeDocument}
moveDocument={this.moveDocument}
addDocument={this.addDocument}
- CollectionView={undefined}
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 b1c97164a..80c878386 100644
--- a/src/client/views/StyleProvider.scss
+++ b/src/client/views/StyleProvider.scss
@@ -1,20 +1,31 @@
+.styleProvider-filter,
+.styleProvider-audio,
.styleProvider-lock {
font-size: 10;
width: 15;
height: 15;
position: absolute;
- right: -0;
+ right: -15;
top: 0;
background: black;
pointer-events: all;
opacity: 0.3;
display: flex;
+ flex-direction: column;
color: gold;
border-radius: 3px;
justify-content: center;
cursor: default;
}
-.styleProvider-lock:hover {
+.styleProvider-filter {
+ right: 0;
+}
+.styleProvider-audio {
+ right: 15;
+}
+.styleProvider-lock:hover,
+.styleProvider-audio:hover,
+.styleProvider-filter:hover {
opacity: 1;
}
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index 5abf4bde2..330ccc583 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -1,13 +1,18 @@
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 { action, runInAction } from 'mobx';
import { extname } from 'path';
-import { Doc, Opt } from '../../fields/Doc';
+import { Doc, Opt, StrListCast } from '../../fields/Doc';
import { BoolCast, Cast, DocCast, ImageCast, NumCast, StrCast } from '../../fields/Types';
-import { DashColor, emptyFunction, lightOrDark } from '../../Utils';
+import { DashColor, lightOrDark, Utils } from '../../Utils';
import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
-import { DocFocusOrOpen } from '../util/DocumentManager';
-import { ColorScheme } from '../util/SettingsManager';
+import { DocFocusOrOpen, DocumentManager } from '../util/DocumentManager';
+import { IsFollowLinkScript } from '../util/LinkFollower';
+import { LinkManager } from '../util/LinkManager';
+import { SelectionManager } from '../util/SelectionManager';
+import { ColorScheme, SettingsManager } from '../util/SettingsManager';
import { undoBatch, UndoManager } from '../util/UndoManager';
import { TreeSort } from './collections/TreeView';
import { Colors } from './global/globalEnums';
@@ -15,29 +20,29 @@ import { InkingStroke } from './InkingStroke';
import { MainView } from './MainView';
import { DocumentViewProps } from './nodes/DocumentView';
import { FieldViewProps } from './nodes/FieldView';
+import { KeyValueBox } from './nodes/KeyValueBox';
import { SliderBox } from './nodes/SliderBox';
import './StyleProvider.scss';
import React = require('react');
-import { Shadows } from 'browndash-components';
export enum StyleProp {
TreeViewIcon = 'treeViewIcon',
TreeViewSortings = 'treeViewSortings', // options for how to sort tree view items
DocContents = 'docContents', // when specified, the JSX returned will replace the normal rendering of the document view
Opacity = 'opacity', // opacity of the document view
- Hidden = 'hidden', // whether the document view should not be isplayed
BoxShadow = 'boxShadow', // box shadow - used for making collections standout and for showing clusters in free form views
BorderRounding = 'borderRounding', // border radius of the document view
Color = 'color', // foreground color of Document view items
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
- LinkSource = 'linkSource', // source document of a link -- used by LinkAnchorBox
+ 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',
TitleHeight = 'titleHeight', // Height of Title area
- ShowTitle = 'showTitle', // whether to display a title on a Document (optional :hover suffix)
+ ShowTitle = 'layout_showTitle', // whether to display a title on a Document (optional :hover suffix)
JitterRotation = 'jitterRotation', // whether documents should be randomly rotated
BorderPath = 'customBorder', // border path for document view
FontSize = 'fontSize', // size of text font
@@ -78,19 +83,22 @@ 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 fieldKey = props?.fieldKey ? props.fieldKey + '-' : isCaption ? 'caption-' : '';
- const isBackground = () => doc && BoolCast(doc._lockedPosition);
+ 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 showTitle = () => props?.styleProvider?.(doc, props, StyleProp.ShowTitle);
+ const layout_showTitle = () => props?.styleProvider?.(doc, props, StyleProp.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);
+ // prettier-ignore
switch (property.split(':')[0]) {
case StyleProp.TreeViewIcon:
const img = ImageCast(doc?.icon, ImageCast(doc?.data));
@@ -99,54 +107,52 @@ 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' };
+ if (doc?._type_collection === CollectionViewType.Freeform) allSorts[TreeSort.Zindex] = { color: 'green', label: 'z' };
allSorts[TreeSort.None] = { color: 'darkgray', label: '\u00A0\u00A0\u00A0' };
return allSorts;
case StyleProp.Highlighting:
- if (doc) {
- const highlightIndex = Doc.isBrushedHighlightedDegree(doc);
- const highlightColor = ['transparent', 'rgb(68, 118, 247)', 'rgb(68, 118, 247)', 'orange', 'lightBlue'][highlightIndex];
+ 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];
const highlightStyle = ['solid', 'dashed', 'solid', 'solid', 'solid'][highlightIndex];
- const excludeTypes = !props?.treeViewDoc ? [DocumentType.FONTICON, DocumentType.INK] : [DocumentType.FONTICON];
- let highlighting = !props?.disableDocBrushing && highlightIndex && !excludeTypes.includes(doc.type as any) && doc._viewType !== CollectionViewType.Linear; // bcz: hack to turn off highlighting onsidebar panel documents. need to flag a document as not highlightable in a more direct way
- if (highlighting && props?.focus !== emptyFunction && StrCast(doc.title) !== '[pres element template]') {
- return { highlightStyle, highlightColor, highlightIndex };
+ if (highlightIndex) {
+ return {
+ highlightStyle,
+ highlightColor,
+ highlightIndex,
+ highlightStroke: doc.type === DocumentType.INK,
+ };
}
}
return undefined;
- case StyleProp.DocContents:
- return undefined;
- case StyleProp.WidgetColor:
- return isAnnotated ? Colors.LIGHT_BLUE : darkScheme() ? 'lightgrey' : 'dimgrey';
- // return doc = dragManager.dragDocument ? props.dragEffects.opacity??CastofOpacityonline94 : Cast())
- // same idea for background Color
- case StyleProp.Opacity:
- return Cast(doc?._opacity, 'number', Cast(doc?.opacity, 'number', null));
- case StyleProp.HideLinkButton:
- return props?.hideLinkButton || (!selected && (doc?.isLinkButton || doc?.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.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.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 &&
- props?.showTitle?.() !== '' &&
+ !props?.LayoutTemplateString?.includes(KeyValueBox.name) &&
+ props?.layout_showTitle?.() !== '' &&
StrCast(
- doc._showTitle,
- props?.showTitle?.() ||
+ doc._layout_showTitle,
+ props?.layout_showTitle?.() ||
(!Doc.IsSystem(doc) && [DocumentType.COL, DocumentType.FUNCPLOT, DocumentType.LABEL, DocumentType.RTF, DocumentType.IMG, DocumentType.VID].includes(doc.type as any)
? doc.author === Doc.CurrentUserEmail
- ? StrCast(Doc.UserDoc().showTitle)
+ ? StrCast(Doc.UserDoc().layout_showTitle)
: remoteDocHeader
: '')
)) ||
@@ -157,143 +163,159 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
const docColor: Opt<string> = StrCast(doc?.[fieldKey + 'color'], StrCast(doc?._color));
if (docColor) return docColor;
const docView = props?.DocumentView?.();
- const backColor = backgroundCol() || docView?.props.styleProvider?.(docView.props.treeViewDoc, docView.props, 'backgroundColor');
+ const backColor = backgroundCol() || docView?.props.styleProvider?.(docView.props.treeViewDoc, docView.props, StyleProp.BackgroundColor);
if (!backColor) return undefined;
return lightOrDark(backColor);
- case StyleProp.Hidden:
- return BoolCast(doc?.hidden);
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:
- return Doc.IsComicStyle(doc) && props?.renderDepth && doc?.type !== DocumentType.INK
- ? { path: wavyBorderPath(props?.PanelWidth?.() || 0, props?.PanelHeight?.() || 0), fill: wavyBorderPath(props?.PanelWidth?.() || 0, props?.PanelHeight?.() || 0, 0.08), width: 3 }
- : { path: undefined, width: 0 };
+ const borderPath = Doc.IsComicStyle(doc) &&
+ props?.renderDepth &&
+ doc?.type !== DocumentType.INK && { path: wavyBorderPath(props?.PanelWidth?.() || 0, props?.PanelHeight?.() || 0), fill: wavyBorderPath(props?.PanelWidth?.() || 0, props?.PanelHeight?.() || 0, 0.08), width: 3 };
+ return !borderPath
+ ? null
+ : {
+ clipPath: `path('${borderPath.path}')`,
+ jsx: (
+ <div key="border2" className="documentView-customBorder" style={{ pointerEvents: 'none' }}>
+ <svg style={{ overflow: 'visible', height: '100%' }} viewBox={`0 0 ${props.PanelWidth()} ${props.PanelHeight()}`}>
+ <path d={borderPath.path} style={{ stroke: 'black', fill: 'transparent', strokeWidth: borderPath.width }} />
+ </svg>
+ </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.HeaderMargin:
- return ([CollectionViewType.Stacking, CollectionViewType.NoteTaking, CollectionViewType.Masonry, CollectionViewType.Tree].includes(doc?._viewType as any) ||
- (doc?.type === DocumentType.RTF && !showTitle()?.includes('noMargin')) ||
+ 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) &&
- showTitle() &&
- !StrCast(doc?.showTitle).includes(':hover')
+ layout_showTitle() &&
+ !StrCast(doc?.layout_showTitle).includes(':hover')
? 15
: 0;
case StyleProp.BackgroundColor: {
if (MainView.Instance.LastButton === doc) return Colors.LIGHT_GRAY;
- let docColor: Opt<string> = StrCast(doc?.[fieldKey + 'backgroundColor'], StrCast(doc?._backgroundColor, isCaption ? 'rgba(0,0,0,0.4)' : ''));
+ let docColor: Opt<string> = StrCast(doc?.[fieldKey + '_backgroundColor'], StrCast(doc?._backgroundColor, isCaption ? 'rgba(0,0,0,0.4)' : ''));
+ // prettier-ignore
switch (doc?.type) {
- case DocumentType.PRESELEMENT:
- docColor = docColor || (darkScheme() ? '' : '');
- break;
- case DocumentType.PRES:
- docColor = docColor || (darkScheme() ? 'transparent' : 'transparent');
- break;
- case DocumentType.FONTICON:
- docColor = 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?.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 || (doc.annotationOn !== undefined ? 'rgba(128, 128, 128, 0.18)' : undefined) || (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.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.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.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?._viewType === CollectionViewType.Pile || Doc.IsSystem(doc)
+ docColor = docColor || (Doc.IsSystem(doc)
? darkScheme()
? Colors.DARK_GRAY
: Colors.LIGHT_GRAY // system docs (seen in treeView) get a grayish background
: doc.annotationOn
- ? '#00000015' // faint interior for collections on PDFs, images, etc
+ ? '#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);
- 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) return undefined; // if it's not visible, then no shadow)
- if (doc.boxShadow === 'standard') return Shadows.STANDARD_SHADOW;
- if (doc?.isLinkButton && ![DocumentType.LINK, DocumentType.INK].includes(doc.type as any)) return StrCast(doc?._linkButtonShadow, 'lightblue 0em 0em 1em');
-
+ if (!doc || opacity() === 0 || doc.noShadow) return undefined; // if it's not visible, then no 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'
- : isBackground() || doc?._isGroup || docProps?.LayoutTemplateString
+ : 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
- : props?.ContainingCollectionDoc?._useClusters && doc.type !== DocumentType.INK
- ? `${backgroundCol()} ${StrCast(doc.boxShadow, `0vw 0vw ${(isBackground() ? 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 && doc.type !== DocumentType.INK
- ? `gray ${StrCast(doc.boxShadow, `0vw 0vw ${(isBackground() ? 100 : 50) / (docProps?.NativeDimScaling?.() || 1)}px`)}` // if it's just in a cluster, make the shadown roughly match the cluster border extent
- : isBackground()
+ ? `#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.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.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:
- if (doc?.pointerEvents) return StrCast(doc.pointerEvents);
+ const isInk = doc && StrCast(Doc.Layout(doc).layout).includes(InkingStroke.name) && !props?.LayoutTemplateString;
+ if (StrCast(doc?.pointerEvents) && !props?.LayoutTemplateString?.includes(KeyValueBox.name)) return StrCast(doc!.pointerEvents); // honor pointerEvents field (set by lock button usually) if it's not a keyValue view of the Doc
+ if (docProps?.DocumentView?.().ComponentView?.overridePointerEvents?.() !== undefined) return docProps?.DocumentView?.().ComponentView?.overridePointerEvents?.();
+ if (MainView.Instance._exploreMode || doc?.layout_unrendered) return isInk ? 'visiblePainted' : 'all';
+ if (props?.contentPointerEvents) return StrCast(props.contentPointerEvents);
if (props?.pointerEvents?.() === 'none') return 'none';
- const isInk = doc && StrCast(Doc.Layout(doc).layout).includes(InkingStroke.name);
- if (opacity() === 0 || (isInk && !docProps?.treeViewDoc) || doc?.isInkMask) return 'none';
- if (!isInk) return props?.isDocumentActive?.() ? 'all' : undefined; // fixes problem with tree view elements getting pointer events when the tree view is not active
- return undefined;
+ if (opacity() === 0) return 'none';
+ if (props?.isDocumentActive?.()) return isInk ? 'visiblePainted' : 'all';
+ return undefined; // fixes problem with tree view elements getting pointer events when the tree view is not active
case StyleProp.Decorations:
- if (props?.ContainingCollectionDoc?._viewType === CollectionViewType.Freeform || doc?.x !== undefined || doc?.y !== undefined) {
- return doc && isBackground() && !Doc.IsSystem(doc) && (props?.renderDepth || 0) > 0 ? (
- <div className="styleProvider-lock" onClick={() => toggleLockedPosition(doc)}>
- <FontAwesomeIcon icon={isBackground() ? 'lock' : 'unlock'} style={{ color: isBackground() ? 'red' : undefined }} size="lg" />
+ const lock = () => {
+ 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" />
+ </div>
+ );
+ }
+ };
+ const filter = () => {
+ const showFilterIcon =
+ StrListCast(doc?._childFilters).length || StrListCast(doc?._childFiltersByRanges).length
+ ? '#18c718bd' //'hasFilter'
+ : docProps?.childFilters?.().filter(f => Utils.IsRecursiveFilter(f) && f !== Utils.noDragsDocFilter).length || docProps?.childFiltersByRanges().length
+ ? 'orange' //'inheritsFilter'
+ : undefined;
+ return !showFilterIcon ? null : (
+ <div className="styleProvider-filter" onClick={action(() => (SettingsManager.propertiesWidth = 250))}>
+ <FontAwesomeIcon icon={'filter'} size="lg" style={{ position: 'absolute', top: '1%', right: '1%', cursor: 'pointer', padding: 1, color: showFilterIcon, zIndex: 1 }} />
</div>
- ) : null;
- }
+ );
+ };
+ const audio = () => {
+ const audioAnnoState = (doc: Doc) => StrCast(doc.audioAnnoState, 'stopped');
+ const audioAnnosCount = (doc: Doc) => StrListCast(doc[Doc.LayoutFieldKey(doc) + '-audioAnnotations']).length;
+ if (!doc || props?.renderDepth === -1 || (!audioAnnosCount(doc) && audioAnnoState(doc) === 'stopped')) return null;
+ const audioIconColors: { [key: string]: string } = { recording: 'red', playing: 'green', stopped: 'blue' };
+ return (
+ <Tooltip title={<div>{StrListCast(doc[Doc.LayoutFieldKey(doc) + '-audioAnnotations-text']).lastElement()}</div>}>
+ <div className="styleProvider-audio" onPointerDown={() => DocumentManager.Instance.getFirstDocumentView(doc)?.docView?.playAnnotation()}>
+ <FontAwesomeIcon className="documentView-audioFont" style={{ color: audioIconColors[audioAnnoState(doc)] }} icon={'file-audio'} size="sm" />
+ </div>
+ </Tooltip>
+ );
+ };
+ return (
+ <>
+ {lock()}
+ {filter()}
+ {audio()}
+ </>
+ );
}
}
@@ -317,16 +339,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', () => {
- doc.hidden = doc.hidden ? undefined : true;
- if (!doc.hidden) {
- DocFocusOrOpen(doc, props?.ContainingCollectionDoc);
- }
- })}
- </>
- );
+ return doc._type_collection === CollectionViewType.Docking
+ ? 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 863829a51..5c2ab3f70 100644
--- a/src/client/views/TemplateMenu.tsx
+++ b/src/client/views/TemplateMenu.tsx
@@ -1,7 +1,6 @@
import { action, computed, observable, ObservableSet, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import { Doc, DocListCast } from '../../fields/Doc';
-import { List } from '../../fields/List';
import { ScriptField } from '../../fields/ScriptField';
import { Cast, StrCast } from '../../fields/Types';
import { TraceMobx } from '../../fields/util';
@@ -80,7 +79,7 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
};
componentDidMount() {
!this._addedKeys && (this._addedKeys = new ObservableSet());
- Array.from(Object.keys(Doc.GetProto(this.props.docViews[0].props.Document)))
+ [...Array.from(Object.keys(Doc.GetProto(this.props.docViews[0].props.Document))), ...Array.from(Object.keys(this.props.docViews[0].props.Document))]
.filter(key => key.startsWith('layout_'))
.map(key => runInAction(() => this._addedKeys.add(key.replace('layout_', ''))));
}
@@ -92,14 +91,14 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
}
templateIsUsed = (selDoc: Doc, templateDoc: Doc) => {
const template = StrCast(templateDoc.dragFactory ? Cast(templateDoc.dragFactory, Doc, null)?.title : templateDoc.title);
- return StrCast(selDoc.layoutKey) === 'layout_' + template ? 'check' : 'unchecked';
+ return StrCast(selDoc.layout_fieldKey) === 'layout_' + template ? 'check' : 'unchecked';
};
render() {
TraceMobx();
const firstDoc = this.props.docViews[0].props.Document;
- const templateName = StrCast(firstDoc.layoutKey, '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 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 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} />);
@@ -114,19 +113,15 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
{templateMenu}
<CollectionTreeView
Document={Doc.MyTemplates}
- CollectionView={undefined}
- ContainingCollectionDoc={undefined}
- ContainingCollectionView={undefined}
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
new file mode 100644
index 000000000..ab21e6d7e
--- /dev/null
+++ b/src/client/views/UndoStack.scss
@@ -0,0 +1,29 @@
+.undoStack-outerContainer {
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ position: relative;
+ pointer-events: all;
+ padding-left: 4px;
+}
+
+.undoStack-resultContainer {
+ border-radius: 5px;
+}
+
+.undoStack-commandInput {
+ width: 100%;
+}
+
+.undoStack-commandResult,
+.undoStack-commandString {
+ overflow-wrap: break-word;
+}
+
+.undoStack-commandsContainer {
+ background-color: whitesmoke;
+ flex: 1 1 auto;
+ overflow-y: scroll;
+ height: 30px;
+ border-radius: 5px;
+}
diff --git a/src/client/views/UndoStack.tsx b/src/client/views/UndoStack.tsx
new file mode 100644
index 000000000..f5af09e5b
--- /dev/null
+++ b/src/client/views/UndoStack.tsx
@@ -0,0 +1,47 @@
+import { action, observable } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { UndoManager } from '../util/UndoManager';
+import './UndoStack.scss';
+
+interface UndoStackProps {
+ width?: number;
+ height?: number;
+ inline?: boolean;
+}
+@observer
+export class UndoStack extends React.Component<UndoStackProps> {
+ @observable static HideInline: boolean;
+ @observable static Expand: boolean;
+ render() {
+ 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>
+ </div>
+ ))}
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/animationtimeline/Keyframe.tsx b/src/client/views/animationtimeline/Keyframe.tsx
index 92d3e2bed..3465a5283 100644
--- a/src/client/views/animationtimeline/Keyframe.tsx
+++ b/src/client/views/animationtimeline/Keyframe.tsx
@@ -1,38 +1,37 @@
-import { action, computed, observable, runInAction } from "mobx";
-import { observer } from "mobx-react";
-import * as React from "react";
-import { Doc, DocListCast, Opt } from "../../../fields/Doc";
-import { List } from "../../../fields/List";
-import { createSchema, defaultSpec, listSpec, makeInterface } from "../../../fields/Schema";
-import { Cast, NumCast } from "../../../fields/Types";
-import { Docs } from "../../documents/Documents";
-import { Transform } from "../../util/Transform";
-import { CollectionDockingView } from "../collections/CollectionDockingView";
-import "../global/globalCssVariables.scss";
-import "./Keyframe.scss";
-import "./Timeline.scss";
-import { TimelineMenu } from "./TimelineMenu";
-
+import { action, computed, observable, runInAction } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { Doc, DocListCast, Opt } from '../../../fields/Doc';
+import { List } from '../../../fields/List';
+import { createSchema, defaultSpec, listSpec, makeInterface } from '../../../fields/Schema';
+import { Cast, NumCast } from '../../../fields/Types';
+import { Docs } from '../../documents/Documents';
+import { Transform } from '../../util/Transform';
+import { CollectionDockingView } from '../collections/CollectionDockingView';
+import '../global/globalCssVariables.scss';
+import { OpenWhereMod } from '../nodes/DocumentView';
+import './Keyframe.scss';
+import './Timeline.scss';
+import { TimelineMenu } from './TimelineMenu';
/**
- * Useful static functions that you can use. Mostly for logic, but you can also add UI logic here also
+ * Useful static functions that you can use. Mostly for logic, but you can also add UI logic here also
*/
export namespace KeyframeFunc {
-
export enum KeyframeType {
- end = "end",
- fade = "fade",
- default = "default",
+ end = 'end',
+ fade = 'fade',
+ default = 'default',
}
export enum Direction {
- left = "left",
- right = "right"
+ left = 'left',
+ right = 'right',
}
- export const findAdjacentRegion = (dir: KeyframeFunc.Direction, currentRegion: Doc, regions: Doc[]): (RegionData | undefined) => {
- let leftMost: (RegionData | undefined) = undefined;
- let rightMost: (RegionData | undefined) = undefined;
+ export const findAdjacentRegion = (dir: KeyframeFunc.Direction, currentRegion: Doc, regions: Doc[]): RegionData | undefined => {
+ let leftMost: RegionData | undefined = undefined;
+ let rightMost: RegionData | undefined = undefined;
regions.forEach(region => {
const neighbor = RegionData(region);
if (currentRegion.position! > neighbor.position) {
@@ -52,11 +51,12 @@ export namespace KeyframeFunc {
}
};
- export const calcMinLeft = (region: Doc, currentBarX: number, ref?: Doc) => { //returns the time of the closet keyframe to the left
+ export const calcMinLeft = (region: Doc, currentBarX: number, ref?: Doc) => {
+ //returns the time of the closet keyframe to the left
let leftKf: Opt<Doc>;
let time: number = 0;
const keyframes = DocListCast(region.keyframes!);
- keyframes.map((kf) => {
+ keyframes.map(kf => {
let compTime = currentBarX;
if (ref) compTime = NumCast(ref.time);
if (NumCast(kf.time) < compTime && NumCast(kf.time) >= time) {
@@ -67,11 +67,11 @@ export namespace KeyframeFunc {
return leftKf;
};
-
- export const calcMinRight = (region: Doc, currentBarX: number, ref?: Doc) => { //returns the time of the closest keyframe to the right
+ export const calcMinRight = (region: Doc, currentBarX: number, ref?: Doc) => {
+ //returns the time of the closest keyframe to the right
let rightKf: Opt<Doc>;
let time: number = Infinity;
- DocListCast(region.keyframes!).forEach((kf) => {
+ DocListCast(region.keyframes!).forEach(kf => {
let compTime = currentBarX;
if (ref) compTime = NumCast(ref.time);
if (NumCast(kf.time) > compTime && NumCast(kf.time) <= NumCast(time)) {
@@ -93,27 +93,31 @@ export namespace KeyframeFunc {
return regiondata;
};
-
- export const convertPixelTime = (pos: number, unit: "mili" | "sec" | "min" | "hr", dir: "pixel" | "time", tickSpacing: number, tickIncrement: number) => {
- const time = dir === "pixel" ? (pos * tickSpacing) / tickIncrement : (pos / tickSpacing) * tickIncrement;
+ export const convertPixelTime = (pos: number, unit: 'mili' | 'sec' | 'min' | 'hr', dir: 'pixel' | 'time', tickSpacing: number, tickIncrement: number) => {
+ const time = dir === 'pixel' ? (pos * tickSpacing) / tickIncrement : (pos / tickSpacing) * tickIncrement;
switch (unit) {
- case "mili": return time;
- case "sec": return dir === "pixel" ? time / 1000 : time * 1000;
- case "min": return dir === "pixel" ? time / 60000 : time * 60000;
- case "hr": return dir === "pixel" ? time / 3600000 : time * 3600000;
- default: return time;
+ case 'mili':
+ return time;
+ case 'sec':
+ return dir === 'pixel' ? time / 1000 : time * 1000;
+ case 'min':
+ return dir === 'pixel' ? time / 60000 : time * 60000;
+ case 'hr':
+ return dir === 'pixel' ? time / 3600000 : time * 3600000;
+ default:
+ return time;
}
};
}
export const RegionDataSchema = createSchema({
- position: defaultSpec("number", 0),
- duration: defaultSpec("number", 0),
+ position: defaultSpec('number', 0),
+ duration: defaultSpec('number', 0),
keyframes: listSpec(Doc),
- fadeIn: defaultSpec("number", 0),
- fadeOut: defaultSpec("number", 0),
+ fadeIn: defaultSpec('number', 0),
+ fadeOut: defaultSpec('number', 0),
functions: listSpec(Doc),
- hasData: defaultSpec("boolean", false)
+ hasData: defaultSpec('boolean', false),
});
export type RegionData = makeInterface<[typeof RegionDataSchema]>;
export const RegionData = makeInterface(RegionDataSchema);
@@ -130,50 +134,63 @@ interface IProps {
makeKeyData: (region: RegionData, pos: number, kftype: KeyframeFunc.KeyframeType) => Doc;
}
-
/**
- *
+ *
* This class handles the green region stuff
* Key facts:
- *
+ *
* Structure looks like this
- *
+ *
* region as a whole
* <------------------------------REGION------------------------------->
- *
- * region broken down
- *
+ *
+ * region broken down
+ *
* <|---------|############ MAIN CONTENT #################|-----------|> .....followed by void.........
* (start) (Fade 2)
* (fade 1) (finish)
- *
- *
- * As you can see, this is different from After Effect and Premiere Pro, but this is how TAG worked.
- * If you want to checkout TAG, it's in the lockers, and the password is the usual lab door password. It's the blue laptop.
- * If you want to know the exact location of the computer, message me.
- *
- * @author Andrew Kim
+ *
+ *
+ * As you can see, this is different from After Effect and Premiere Pro, but this is how TAG worked.
+ * If you want to checkout TAG, it's in the lockers, and the password is the usual lab door password. It's the blue laptop.
+ * If you want to know the exact location of the computer, message me.
+ *
+ * @author Andrew Kim
*/
@observer
export class Keyframe extends React.Component<IProps> {
-
@observable private _bar = React.createRef<HTMLDivElement>();
@observable private _mouseToggled = false;
@observable private _doubleClickEnabled = false;
- @computed private get regiondata() { return RegionData(this.props.RegionData); }
- @computed private get regions() { return DocListCast(this.props.node.regions); }
- @computed private get keyframes() { return DocListCast(this.regiondata.keyframes); }
- @computed private get pixelPosition() { return KeyframeFunc.convertPixelTime(this.regiondata.position, "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement); }
- @computed private get pixelDuration() { return KeyframeFunc.convertPixelTime(this.regiondata.duration, "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement); }
- @computed private get pixelFadeIn() { return KeyframeFunc.convertPixelTime(this.regiondata.fadeIn, "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement); }
- @computed private get pixelFadeOut() { return KeyframeFunc.convertPixelTime(this.regiondata.fadeOut, "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement); }
+ @computed private get regiondata() {
+ return RegionData(this.props.RegionData);
+ }
+ @computed private get regions() {
+ return DocListCast(this.props.node.regions);
+ }
+ @computed private get keyframes() {
+ return DocListCast(this.regiondata.keyframes);
+ }
+ @computed private get pixelPosition() {
+ return KeyframeFunc.convertPixelTime(this.regiondata.position, 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement);
+ }
+ @computed private get pixelDuration() {
+ return KeyframeFunc.convertPixelTime(this.regiondata.duration, 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement);
+ }
+ @computed private get pixelFadeIn() {
+ return KeyframeFunc.convertPixelTime(this.regiondata.fadeIn, 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement);
+ }
+ @computed private get pixelFadeOut() {
+ return KeyframeFunc.convertPixelTime(this.regiondata.fadeOut, 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement);
+ }
constructor(props: any) {
super(props);
}
componentDidMount() {
- setTimeout(() => { //giving it a temporary 1sec delay...
+ setTimeout(() => {
+ //giving it a temporary 1sec delay...
if (!this.regiondata.keyframes) this.regiondata.keyframes = new List<Doc>();
const start = this.props.makeKeyData(this.regiondata, this.regiondata.position, KeyframeFunc.KeyframeType.end);
const fadeIn = this.props.makeKeyData(this.regiondata, this.regiondata.position + this.regiondata.fadeIn, KeyframeFunc.KeyframeType.fade);
@@ -202,12 +219,12 @@ export class Keyframe extends React.Component<IProps> {
this._doubleClickEnabled = false;
}, 200);
this._doubleClickEnabled = true;
- document.addEventListener("pointermove", this.onBarPointerMove);
- document.addEventListener("pointerup", (e: PointerEvent) => {
- document.removeEventListener("pointermove", this.onBarPointerMove);
+ document.addEventListener('pointermove', this.onBarPointerMove);
+ document.addEventListener('pointerup', (e: PointerEvent) => {
+ document.removeEventListener('pointermove', this.onBarPointerMove);
});
}
- }
+ };
@action
onBarPointerMove = (e: PointerEvent) => {
@@ -219,46 +236,46 @@ export class Keyframe extends React.Component<IProps> {
const left = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.left, this.regiondata, this.regions)!;
const right = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.right, this.regiondata, this.regions)!;
const prevX = this.regiondata.position;
- const futureX = this.regiondata.position + KeyframeFunc.convertPixelTime(e.movementX, "mili", "time", this.props.tickSpacing, this.props.tickIncrement);
+ const futureX = this.regiondata.position + KeyframeFunc.convertPixelTime(e.movementX, 'mili', 'time', this.props.tickSpacing, this.props.tickIncrement);
if (futureX <= 0) {
this.regiondata.position = 0;
- } else if ((left && left.position + left.duration >= futureX)) {
+ } else if (left && left.position + left.duration >= futureX) {
this.regiondata.position = left.position + left.duration;
- } else if ((right && right.position <= futureX + this.regiondata.duration)) {
+ } else if (right && right.position <= futureX + this.regiondata.duration) {
this.regiondata.position = right.position - this.regiondata.duration;
} else {
this.regiondata.position = futureX;
}
const movement = this.regiondata.position - prevX;
- this.keyframes.forEach(kf => kf.time = NumCast(kf.time) + movement);
- }
+ this.keyframes.forEach(kf => (kf.time = NumCast(kf.time) + movement));
+ };
@action
onResizeLeft = (e: React.PointerEvent) => {
e.preventDefault();
e.stopPropagation();
- document.addEventListener("pointermove", this.onDragResizeLeft);
- document.addEventListener("pointerup", () => {
- document.removeEventListener("pointermove", this.onDragResizeLeft);
+ document.addEventListener('pointermove', this.onDragResizeLeft);
+ document.addEventListener('pointerup', () => {
+ document.removeEventListener('pointermove', this.onDragResizeLeft);
});
- }
+ };
@action
onResizeRight = (e: React.PointerEvent) => {
e.preventDefault();
e.stopPropagation();
- document.addEventListener("pointermove", this.onDragResizeRight);
- document.addEventListener("pointerup", () => {
- document.removeEventListener("pointermove", this.onDragResizeRight);
+ document.addEventListener('pointermove', this.onDragResizeRight);
+ document.addEventListener('pointerup', () => {
+ document.removeEventListener('pointermove', this.onDragResizeRight);
});
- }
+ };
@action
onDragResizeLeft = (e: PointerEvent) => {
e.preventDefault();
e.stopPropagation();
const bar = this._bar.current!;
- const offset = KeyframeFunc.convertPixelTime(Math.round((e.clientX - bar.getBoundingClientRect().left) * this.props.transform.Scale), "mili", "time", this.props.tickSpacing, this.props.tickIncrement);
+ const offset = KeyframeFunc.convertPixelTime(Math.round((e.clientX - bar.getBoundingClientRect().left) * this.props.transform.Scale), 'mili', 'time', this.props.tickSpacing, this.props.tickIncrement);
const leftRegion = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.left, this.regiondata, this.regions);
if (leftRegion && this.regiondata.position + offset <= leftRegion.position + leftRegion.duration) {
this.regiondata.position = leftRegion.position + leftRegion.duration;
@@ -275,90 +292,91 @@ export class Keyframe extends React.Component<IProps> {
}
this.keyframes[0].time = this.regiondata.position;
this.keyframes[1].time = this.regiondata.position + this.regiondata.fadeIn;
- }
-
+ };
@action
onDragResizeRight = (e: PointerEvent) => {
e.preventDefault();
e.stopPropagation();
const bar = this._bar.current!;
- const offset = KeyframeFunc.convertPixelTime(Math.round((e.clientX - bar.getBoundingClientRect().right) * this.props.transform.Scale), "mili", "time", this.props.tickSpacing, this.props.tickIncrement);
+ const offset = KeyframeFunc.convertPixelTime(Math.round((e.clientX - bar.getBoundingClientRect().right) * this.props.transform.Scale), 'mili', 'time', this.props.tickSpacing, this.props.tickIncrement);
const rightRegion = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.right, this.regiondata, this.regions);
const fadeOutKeyframeTime = NumCast(this.keyframes[this.keyframes.length - 3].time);
- if (this.regiondata.position + this.regiondata.duration - this.regiondata.fadeOut + offset <= fadeOutKeyframeTime) { //case 1: when third to last keyframe is in the way
+ if (this.regiondata.position + this.regiondata.duration - this.regiondata.fadeOut + offset <= fadeOutKeyframeTime) {
+ //case 1: when third to last keyframe is in the way
this.regiondata.duration = fadeOutKeyframeTime - this.regiondata.position + this.regiondata.fadeOut;
- } else if (rightRegion && (this.regiondata.position + this.regiondata.duration + offset >= rightRegion.position)) {
+ } else if (rightRegion && this.regiondata.position + this.regiondata.duration + offset >= rightRegion.position) {
this.regiondata.duration = rightRegion.position - this.regiondata.position;
} else {
this.regiondata.duration += offset;
}
this.keyframes[this.keyframes.length - 2].time = this.regiondata.position + this.regiondata.duration - this.regiondata.fadeOut;
this.keyframes[this.keyframes.length - 1].time = this.regiondata.position + this.regiondata.duration;
- }
-
+ };
@action
createKeyframe = async (clientX: number) => {
this._mouseToggled = true;
const bar = this._bar.current!;
- const offset = KeyframeFunc.convertPixelTime(Math.round((clientX - bar.getBoundingClientRect().left) * this.props.transform.Scale), "mili", "time", this.props.tickSpacing, this.props.tickIncrement);
- if (offset > this.regiondata.fadeIn && offset < this.regiondata.duration - this.regiondata.fadeOut) { //make sure keyframe is not created inbetween fades and ends
+ const offset = KeyframeFunc.convertPixelTime(Math.round((clientX - bar.getBoundingClientRect().left) * this.props.transform.Scale), 'mili', 'time', this.props.tickSpacing, this.props.tickIncrement);
+ if (offset > this.regiondata.fadeIn && offset < this.regiondata.duration - this.regiondata.fadeOut) {
+ //make sure keyframe is not created inbetween fades and ends
const position = this.regiondata.position;
this.props.makeKeyData(this.regiondata, Math.round(position + offset), KeyframeFunc.KeyframeType.default);
this.regiondata.hasData = true;
- this.props.changeCurrentBarX(KeyframeFunc.convertPixelTime(Math.round(position + offset), "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement)); //first move the keyframe to the correct location and make a copy so the correct file gets coppied
-
+ this.props.changeCurrentBarX(KeyframeFunc.convertPixelTime(Math.round(position + offset), 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement)); //first move the keyframe to the correct location and make a copy so the correct file gets coppied
}
- }
-
+ };
@action
moveKeyframe = async (e: React.MouseEvent, kf: Doc) => {
e.preventDefault();
e.stopPropagation();
- this.props.changeCurrentBarX(KeyframeFunc.convertPixelTime(NumCast(kf.time!), "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement));
- }
+ this.props.changeCurrentBarX(KeyframeFunc.convertPixelTime(NumCast(kf.time!), 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement));
+ };
/**
* custom keyframe context menu items (when clicking on the keyframe circle)
*/
@action
makeKeyframeMenu = (kf: Doc, e: MouseEvent) => {
- TimelineMenu.Instance.addItem("button", "Toggle Fade Only", () => {
+ TimelineMenu.Instance.addItem('button', 'Toggle Fade Only', () => {
kf.type = kf.type === KeyframeFunc.KeyframeType.fade ? KeyframeFunc.KeyframeType.default : KeyframeFunc.KeyframeType.fade;
}),
- TimelineMenu.Instance.addItem("button", "Show Data", action(() => {
- const kvp = Docs.Create.KVPDocument(kf, { _width: 300, _height: 300 });
- CollectionDockingView.AddSplit(kvp, "right");
- })),
- TimelineMenu.Instance.addItem("button", "Delete", action(() => {
- (this.regiondata.keyframes as List<Doc>).splice(this.keyframes.indexOf(kf), 1);
- this.forceUpdate();
- })),
- TimelineMenu.Instance.addItem("input", "Move", action((val) => {
- let cannotMove: boolean = false;
- const kfIndex: number = this.keyframes.indexOf(kf);
- if (val < 0 || (val < NumCast(this.keyframes[kfIndex - 1].time) || val > NumCast(this.keyframes[kfIndex + 1].time))) {
- cannotMove = true;
- }
- if (!cannotMove) {
- this.keyframes[kfIndex].time = parseInt(val, 10);
- this.keyframes[1].time = this.regiondata.position + this.regiondata.fadeIn;
- }
- }));
- TimelineMenu.Instance.addMenu("Keyframe");
+ TimelineMenu.Instance.addItem(
+ 'button',
+ 'Delete',
+ action(() => {
+ (this.regiondata.keyframes as List<Doc>).splice(this.keyframes.indexOf(kf), 1);
+ this.forceUpdate();
+ })
+ ),
+ TimelineMenu.Instance.addItem(
+ 'input',
+ 'Move',
+ action(val => {
+ let cannotMove: boolean = false;
+ const kfIndex: number = this.keyframes.indexOf(kf);
+ if (val < 0 || val < NumCast(this.keyframes[kfIndex - 1].time) || val > NumCast(this.keyframes[kfIndex + 1].time)) {
+ cannotMove = true;
+ }
+ if (!cannotMove) {
+ this.keyframes[kfIndex].time = parseInt(val, 10);
+ this.keyframes[1].time = this.regiondata.position + this.regiondata.fadeIn;
+ }
+ })
+ );
+ TimelineMenu.Instance.addMenu('Keyframe');
TimelineMenu.Instance.openMenu(e.clientX, e.clientY);
- }
+ };
/**
- * context menu for region (anywhere on the green region).
+ * context menu for region (anywhere on the green region).
*/
@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("input", `fadeIn: ${this.regiondata.fadeIn}ms`, (val) => {
+ TimelineMenu.Instance.addItem('button', 'Remove Region', () => Cast(this.props.node.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;
if (val < 0 || val > NumCast(this.keyframes[2].time) - this.regiondata.position) {
@@ -370,7 +388,7 @@ export class Keyframe extends React.Component<IProps> {
}
});
}),
- TimelineMenu.Instance.addItem("input", `fadeOut: ${this.regiondata.fadeOut}ms`, (val) => {
+ TimelineMenu.Instance.addItem('input', `fadeOut: ${this.regiondata.fadeOut}ms`, val => {
runInAction(() => {
let cannotMove: boolean = false;
if (val < 0 || val > this.regiondata.position + this.regiondata.duration - NumCast(this.keyframes[this.keyframes.length - 3].time)) {
@@ -382,34 +400,38 @@ export class Keyframe extends React.Component<IProps> {
}
});
}),
- TimelineMenu.Instance.addItem("input", `position: ${this.regiondata.position}ms`, (val) => {
+ TimelineMenu.Instance.addItem('input', `position: ${this.regiondata.position}ms`, val => {
runInAction(() => {
const prevPosition = this.regiondata.position;
let cannotMove: boolean = false;
- this.regions.map(region => ({ pos: NumCast(region.position), dur: NumCast(region.duration) })).forEach(({ pos, dur }) => {
- if (pos !== this.regiondata.position) {
- if ((val < 0) || (val > pos && val < pos + dur || (this.regiondata.duration + val > pos && this.regiondata.duration + val < pos + dur))) {
- cannotMove = true;
+ this.regions
+ .map(region => ({ pos: NumCast(region.position), dur: NumCast(region.duration) }))
+ .forEach(({ pos, dur }) => {
+ if (pos !== this.regiondata.position) {
+ if (val < 0 || (val > pos && val < pos + dur) || (this.regiondata.duration + val > pos && this.regiondata.duration + val < pos + dur)) {
+ cannotMove = true;
+ }
}
- }
- });
+ });
if (!cannotMove) {
this.regiondata.position = parseInt(val, 10);
this.updateKeyframes(this.regiondata.position - prevPosition);
}
});
}),
- TimelineMenu.Instance.addItem("input", `duration: ${this.regiondata.duration}ms`, (val) => {
+ TimelineMenu.Instance.addItem('input', `duration: ${this.regiondata.duration}ms`, val => {
runInAction(() => {
let cannotMove: boolean = false;
- this.regions.map(region => ({ pos: NumCast(region.position), dur: NumCast(region.duration) })).forEach(({ pos, dur }) => {
- if (pos !== this.regiondata.position) {
- val += this.regiondata.position;
- if ((val < 0) || (val > pos && val < pos + dur)) {
- cannotMove = true;
+ this.regions
+ .map(region => ({ pos: NumCast(region.position), dur: NumCast(region.duration) }))
+ .forEach(({ pos, dur }) => {
+ if (pos !== this.regiondata.position) {
+ val += this.regiondata.position;
+ if (val < 0 || (val > pos && val < pos + dur)) {
+ cannotMove = true;
+ }
}
- }
- });
+ });
if (!cannotMove) {
this.regiondata.duration = parseInt(val, 10);
this.keyframes[this.keyframes.length - 1].time = this.regiondata.position + this.regiondata.duration;
@@ -417,9 +439,9 @@ export class Keyframe extends React.Component<IProps> {
}
});
}),
- TimelineMenu.Instance.addMenu("Region");
+ TimelineMenu.Instance.addMenu('Region');
TimelineMenu.Instance.openMenu(e.clientX, e.clientY);
- }
+ };
@action
updateKeyframes = (incr: number, filter: number[] = []) => {
@@ -428,7 +450,7 @@ export class Keyframe extends React.Component<IProps> {
kf.time = NumCast(kf.time) + incr;
}
});
- }
+ };
/**
* hovering effect when hovered (hidden div darkens)
@@ -438,9 +460,9 @@ export class Keyframe extends React.Component<IProps> {
e.preventDefault();
e.stopPropagation();
const div = ref.current!;
- div.style.opacity = "1";
+ div.style.opacity = '1';
Doc.BrushDoc(this.props.node);
- }
+ };
/**
* hovering effect when hovered out (hidden div becomes invisible)
@@ -450,14 +472,12 @@ export class Keyframe extends React.Component<IProps> {
e.preventDefault();
e.stopPropagation();
const div = ref.current!;
- div.style.opacity = "0";
+ div.style.opacity = '0';
Doc.UnBrushDoc(this.props.node);
- }
-
+ };
///////////////////////UI STUFF /////////////////////////
-
/**
* drawing keyframe. Handles both keyframe with a circle (one that you create by double clicking) and one without circle (fades)
* this probably needs biggest change, since everyone expected all keyframes to have a circle (and draggable)
@@ -465,32 +485,43 @@ export class Keyframe extends React.Component<IProps> {
drawKeyframes = () => {
const keyframeDivs: JSX.Element[] = [];
return DocListCast(this.regiondata.keyframes).map(kf => {
- if (kf.type as KeyframeFunc.KeyframeType !== KeyframeFunc.KeyframeType.end) {
- return <>
- <div className="keyframe" style={{ left: `${KeyframeFunc.convertPixelTime(NumCast(kf.time), "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement) - this.pixelPosition}px` }}>
- <div className="divider"></div>
- <div className="keyframeCircle keyframe-indicator"
- onPointerDown={(e) => { e.preventDefault(); e.stopPropagation(); this.moveKeyframe(e, kf); }}
- onContextMenu={(e: React.MouseEvent) => {
- e.preventDefault();
- e.stopPropagation();
- this.makeKeyframeMenu(kf, e.nativeEvent);
- }}
- onDoubleClick={(e) => { e.preventDefault(); e.stopPropagation(); }}>
+ if ((kf.type as KeyframeFunc.KeyframeType) !== KeyframeFunc.KeyframeType.end) {
+ return (
+ <>
+ <div className="keyframe" style={{ left: `${KeyframeFunc.convertPixelTime(NumCast(kf.time), 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement) - this.pixelPosition}px` }}>
+ <div className="divider"></div>
+ <div
+ className="keyframeCircle keyframe-indicator"
+ onPointerDown={e => {
+ e.preventDefault();
+ e.stopPropagation();
+ this.moveKeyframe(e, kf);
+ }}
+ onContextMenu={(e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ this.makeKeyframeMenu(kf, e.nativeEvent);
+ }}
+ onDoubleClick={e => {
+ e.preventDefault();
+ e.stopPropagation();
+ }}></div>
</div>
- </div>
- <div className="keyframe-information" />
- </>;
+ <div className="keyframe-information" />
+ </>
+ );
} else {
- return <div className="keyframe" style={{ left: `${KeyframeFunc.convertPixelTime(NumCast(kf.time), "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement) - this.pixelPosition}px` }}>
- <div className="divider" />
- </div>;
+ return (
+ <div className="keyframe" style={{ left: `${KeyframeFunc.convertPixelTime(NumCast(kf.time), 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement) - this.pixelPosition}px` }}>
+ <div className="divider" />
+ </div>
+ );
}
});
- }
+ };
/**
- * drawing the hidden divs that partition different intervals within a region.
+ * drawing the hidden divs that partition different intervals within a region.
*/
@action
drawKeyframeDividers = () => {
@@ -500,26 +531,36 @@ export class Keyframe extends React.Component<IProps> {
if (index !== this.keyframes.length - 1) {
const right = this.keyframes[index + 1];
const bodyRef = React.createRef<HTMLDivElement>();
- const kfPos = KeyframeFunc.convertPixelTime(NumCast(kf.time), "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement);
- const rightPos = KeyframeFunc.convertPixelTime(NumCast(right.time), "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement);
+ const kfPos = KeyframeFunc.convertPixelTime(NumCast(kf.time), 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement);
+ const rightPos = KeyframeFunc.convertPixelTime(NumCast(right.time), 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement);
keyframeDividers.push(
- <div ref={bodyRef} className="body-container" style={{ left: `${kfPos - this.pixelPosition}px`, width: `${rightPos - kfPos}px` }}
- onPointerOver={(e) => { e.preventDefault(); e.stopPropagation(); this.onContainerOver(e, bodyRef); }}
- onPointerOut={(e) => { e.preventDefault(); e.stopPropagation(); this.onContainerOut(e, bodyRef); }}
- onContextMenu={(e) => {
+ <div
+ ref={bodyRef}
+ className="body-container"
+ style={{ left: `${kfPos - this.pixelPosition}px`, width: `${rightPos - kfPos}px` }}
+ onPointerOver={e => {
+ e.preventDefault();
+ e.stopPropagation();
+ this.onContainerOver(e, bodyRef);
+ }}
+ onPointerOut={e => {
+ e.preventDefault();
+ e.stopPropagation();
+ this.onContainerOut(e, bodyRef);
+ }}
+ onContextMenu={e => {
e.preventDefault();
e.stopPropagation();
if (index !== 0 || index !== this.keyframes.length - 2) {
this._mouseToggled = true;
}
this.makeRegionMenu(kf, e.nativeEvent);
- }}>
- </div>
+ }}></div>
);
}
});
return keyframeDividers;
- }
+ };
/**
* rendering that green region
@@ -527,13 +568,18 @@ export class Keyframe extends React.Component<IProps> {
//154, 206, 223
render() {
return (
- <div className="bar" ref={this._bar} style={{
- transform: `translate(${this.pixelPosition}px)`,
- width: `${this.pixelDuration}px`,
- background: `linear-gradient(90deg, rgba(154, 206, 223, 0) 0%, rgba(154, 206, 223, 1) ${this.pixelFadeIn / this.pixelDuration * 100}%, rgba(154, 206, 223, 1) ${(this.pixelDuration - this.pixelFadeOut) / this.pixelDuration * 100}%, rgba(154, 206, 223, 0) 100% )`
- }}
+ <div
+ className="bar"
+ ref={this._bar}
+ style={{
+ transform: `translate(${this.pixelPosition}px)`,
+ width: `${this.pixelDuration}px`,
+ background: `linear-gradient(90deg, rgba(154, 206, 223, 0) 0%, rgba(154, 206, 223, 1) ${(this.pixelFadeIn / this.pixelDuration) * 100}%, rgba(154, 206, 223, 1) ${
+ ((this.pixelDuration - this.pixelFadeOut) / this.pixelDuration) * 100
+ }%, rgba(154, 206, 223, 0) 100% )`,
+ }}
onPointerDown={this.onBarPointerDown}>
- <div className="leftResize keyframe-indicator" onPointerDown={this.onResizeLeft} ></div>
+ <div className="leftResize keyframe-indicator" onPointerDown={this.onResizeLeft}></div>
{/* <div className="keyframe-information"></div> */}
<div className="rightResize keyframe-indicator" onPointerDown={this.onResizeRight}></div>
{/* <div className="keyframe-information"></div> */}
@@ -542,4 +588,4 @@ export class Keyframe extends React.Component<IProps> {
</div>
);
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/animationtimeline/Timeline.tsx b/src/client/views/animationtimeline/Timeline.tsx
index 7a393a4f7..adc97bbb4 100644
--- a/src/client/views/animationtimeline/Timeline.tsx
+++ b/src/client/views/animationtimeline/Timeline.tsx
@@ -76,7 +76,7 @@ export class Timeline extends React.Component<FieldViewProps> {
private get children(): Doc[] {
const annotatedDoc = [DocumentType.IMG, DocumentType.VID, DocumentType.PDF, DocumentType.MAP].includes(StrCast(this.props.Document.type) as any);
if (annotatedDoc) {
- return DocListCast(this.props.Document[Doc.LayoutFieldKey(this.props.Document) + '-annotations']);
+ return DocListCast(this.props.Document[Doc.LayoutFieldKey(this.props.Document) + '_annotations']);
}
return DocListCast(this.props.Document[this.props.fieldKey]);
}
@@ -290,15 +290,15 @@ export class Timeline extends React.Component<FieldViewProps> {
};
resetView(doc: Doc) {
- doc._panX = doc._customOriginX ?? 0;
- doc._panY = doc._customOriginY ?? 0;
- doc._viewScale = doc._customOriginScale ?? 1;
+ doc._freeform_panX = doc._customOriginX ?? 0;
+ doc._freeform_panY = doc._customOriginY ?? 0;
+ doc._freeform_scale = doc._customOriginScale ?? 1;
}
setView(doc: Doc) {
- doc._customOriginX = doc._panX;
- doc._customOriginY = doc._panY;
- doc._customOriginScale = doc._viewScale;
+ doc._customOriginX = doc._freeform_panX;
+ doc._customOriginY = doc._freeform_panY;
+ doc._customOriginScale = doc._freeform_scale;
}
/**
* zooming mechanism (increment and spacing changes)
diff --git a/src/client/views/animationtimeline/Track.tsx b/src/client/views/animationtimeline/Track.tsx
index 25c2e68e7..2349ba786 100644
--- a/src/client/views/animationtimeline/Track.tsx
+++ b/src/client/views/animationtimeline/Track.tsx
@@ -1,15 +1,15 @@
-import { action, computed, intercept, observable, reaction, runInAction } from "mobx";
-import { observer } from "mobx-react";
-import * as React from "react";
-import { Doc, DocListCast, Opt, DocListCastAsync } from "../../../fields/Doc";
-import { Copy } from "../../../fields/FieldSymbols";
-import { List } from "../../../fields/List";
-import { ObjectField } from "../../../fields/ObjectField";
-import { listSpec } from "../../../fields/Schema";
-import { Cast, NumCast } from "../../../fields/Types";
-import { Transform } from "../../util/Transform";
-import { Keyframe, KeyframeFunc, RegionData } from "./Keyframe";
-import "./Track.scss";
+import { action, computed, intercept, observable, reaction, runInAction } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { Doc, DocListCast, Opt, DocListCastAsync } from '../../../fields/Doc';
+import { Copy } from '../../../fields/FieldSymbols';
+import { List } from '../../../fields/List';
+import { ObjectField } from '../../../fields/ObjectField';
+import { listSpec } from '../../../fields/Schema';
+import { Cast, NumCast } from '../../../fields/Types';
+import { Transform } from '../../util/Transform';
+import { Keyframe, KeyframeFunc, RegionData } from './Keyframe';
+import './Track.scss';
interface IProps {
node: Doc;
@@ -32,27 +32,22 @@ export class Track extends React.Component<IProps> {
@observable private _newKeyframe: boolean = false;
private readonly MAX_TITLE_HEIGHT = 75;
@observable private _trackHeight = 0;
- private primitiveWhitelist = [
- "x",
- "y",
- "_width",
- "_height",
- "opacity",
- "_scrollTop"
- ];
- private objectWhitelist = [
- "data"
- ];
+ private primitiveWhitelist = ['x', 'y', '_width', '_height', 'opacity', '_layout_scrollTop'];
+ private objectWhitelist = ['data'];
- @computed private get regions() { return DocListCast(this.props.node.regions); }
- @computed private get time() { return NumCast(KeyframeFunc.convertPixelTime(this.props.currentBarX, "mili", "time", this.props.tickSpacing, this.props.tickIncrement)); }
+ @computed private get regions() {
+ return DocListCast(this.props.node.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
- //these two lines are exactly same from timeline.tsx
+ //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
+ 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);
@@ -71,7 +66,6 @@ export class Track extends React.Component<IProps> {
}
////////////////////////////////
-
getLastRegionTime = () => {
let lastTime: number = 0;
let lastRegion: Opt<Doc>;
@@ -83,11 +77,11 @@ export class Track extends React.Component<IProps> {
}
});
return lastRegion ? lastTime + NumCast(lastRegion.duration) : 0;
- }
+ };
/**
* keyframe save logic. Needs to be changed so it's more efficient
- *
+ *
*/
@action
saveKeyframe = async () => {
@@ -97,16 +91,18 @@ export class Track extends React.Component<IProps> {
if (this._newKeyframe) {
DocListCast(this.saveStateRegion?.keyframes).forEach((kf, index) => {
this.copyDocDataToKeyFrame(kf);
- kf.opacity = (index === 0 || index === 3) ? 0.1 : 1;
+ kf.opacity = index === 0 || index === 3 ? 0.1 : 1;
});
this._newKeyframe = false;
}
if (!kf) return;
- if (kf.type === KeyframeFunc.KeyframeType.default) { // only save for non-fades
+ if (kf.type === KeyframeFunc.KeyframeType.default) {
+ // only save for non-fades
this.copyDocDataToKeyFrame(kf);
const leftkf = KeyframeFunc.calcMinLeft(this.saveStateRegion!, this.time, kf); // lef keyframe, if it exists
- const rightkf = KeyframeFunc.calcMinRight(this.saveStateRegion!, this.time, kf); //right keyframe, if it exists
- if (leftkf?.type === KeyframeFunc.KeyframeType.fade) { //replicating this keyframe to fades
+ const rightkf = KeyframeFunc.calcMinRight(this.saveStateRegion!, this.time, kf); //right keyframe, if it exists
+ if (leftkf?.type === KeyframeFunc.KeyframeType.fade) {
+ //replicating this keyframe to fades
const edge = KeyframeFunc.calcMinLeft(this.saveStateRegion!, this.time, leftkf);
edge && this.copyDocDataToKeyFrame(edge);
leftkf && this.copyDocDataToKeyFrame(leftkf);
@@ -124,8 +120,7 @@ export class Track extends React.Component<IProps> {
keyframes[kfIndex] = kf;
this.saveStateKf = undefined;
this.saveStateRegion = undefined;
- }
-
+ };
/**
* autocreates keyframe
@@ -136,23 +131,27 @@ export class Track extends React.Component<IProps> {
intercept(this.props.node, change => {
return change;
});
- return reaction(() => {
- return [...this.primitiveWhitelist.map(key => this.props.node[key]), ...objects];
- }, (changed, reaction) => {
- //check for region
- const region = this.findRegion(this.time);
- if (region !== undefined) { //if region at scrub time exist
- const r = region as RegionData; //for some region is returning undefined... which is not the case
- if (DocListCast(r.keyframes).find(kf => kf.time === this.time) === undefined) { //basically when there is no additional keyframe at that timespot
- this.makeKeyData(r, this.time, KeyframeFunc.KeyframeType.default);
+ return reaction(
+ () => {
+ return [...this.primitiveWhitelist.map(key => this.props.node[key]), ...objects];
+ },
+ (changed, reaction) => {
+ //check for region
+ const region = this.findRegion(this.time);
+ if (region !== undefined) {
+ //if region at scrub time exist
+ const r = region as RegionData; //for some region is returning undefined... which is not the case
+ if (DocListCast(r.keyframes).find(kf => kf.time === this.time) === undefined) {
+ //basically when there is no additional keyframe at that timespot
+ this.makeKeyData(r, this.time, KeyframeFunc.KeyframeType.default);
+ }
}
- }
- }, { fireImmediately: false });
- }
-
-
+ },
+ { fireImmediately: false }
+ );
+ };
- // @observable private _storedState:(Doc | undefined) = undefined;
+ // @observable private _storedState:(Doc | undefined) = undefined;
// /**
// * reverting back to previous state before editing on AT
// */
@@ -161,54 +160,62 @@ export class Track extends React.Component<IProps> {
// if (this._storedState) this.applyKeys(this._storedState);
// }
-
/**
* Reaction when scrubber bar changes
* made into function so it's easier to dispose later
- */
+ */
@action
currentBarXReaction = () => {
- return reaction(() => this.props.currentBarX, () => {
- const regiondata = this.findRegion(this.time);
- if (regiondata) {
- this.props.node.hidden = false;
- // if (!this._autoKfReaction) {
- // // this._autoKfReaction = this.autoCreateKeyframe();
- // }
- this.timeChange();
- } else {
- this.props.node.hidden = true;
- this.props.node.opacity = 0;
- //if (this._autoKfReaction) this._autoKfReaction();
+ return reaction(
+ () => this.props.currentBarX,
+ () => {
+ const regiondata = this.findRegion(this.time);
+ if (regiondata) {
+ this.props.node.hidden = false;
+ // if (!this._autoKfReaction) {
+ // // this._autoKfReaction = this.autoCreateKeyframe();
+ // }
+ this.timeChange();
+ } else {
+ this.props.node.hidden = true;
+ this.props.node.opacity = 0;
+ //if (this._autoKfReaction) this._autoKfReaction();
+ }
}
- });
- }
+ );
+ };
/**
* when timeline is visible, reaction is ran so states are reverted
*/
@action
timelineVisibleReaction = () => {
- return reaction(() => {
- return this.props.timelineVisible;
- }, isVisible => {
- if (isVisible) {
- this.regions.filter(region => !region.hasData).forEach(region => {
- for (let i = 0; i < 4; i++) {
- this.copyDocDataToKeyFrame(DocListCast(region.keyframes)[i]);
- if (i === 0 || i === 3) { //manually inputing fades
- DocListCast(region.keyframes)[i].opacity = 0.1;
- }
- }
- });
- } else {
- //this.revertState();
+ return reaction(
+ () => {
+ return this.props.timelineVisible;
+ },
+ isVisible => {
+ if (isVisible) {
+ this.regions
+ .filter(region => !region.hasData)
+ .forEach(region => {
+ for (let i = 0; i < 4; i++) {
+ this.copyDocDataToKeyFrame(DocListCast(region.keyframes)[i]);
+ if (i === 0 || i === 3) {
+ //manually inputing fades
+ DocListCast(region.keyframes)[i].opacity = 0.1;
+ }
+ }
+ });
+ } else {
+ //this.revertState();
+ }
}
- });
- }
+ );
+ };
- @observable private saveStateKf: (Doc | undefined) = undefined;
- @observable private saveStateRegion: (Doc | undefined) = undefined;
+ @observable private saveStateKf: Doc | undefined = undefined;
+ @observable private saveStateRegion: Doc | undefined = undefined;
/**w
* when scrubber position changes. Need to edit the logic
@@ -222,9 +229,9 @@ export class Track extends React.Component<IProps> {
}
const regiondata = await this.findRegion(Math.round(this.time)); //finds a region that the scrubber is on
if (regiondata) {
- const leftkf: (Doc | undefined) = await KeyframeFunc.calcMinLeft(regiondata, this.time); // lef keyframe, if it exists
- const rightkf: (Doc | undefined) = await KeyframeFunc.calcMinRight(regiondata, this.time); //right keyframe, if it exists
- const currentkf: (Doc | undefined) = await this.calcCurrent(regiondata); //if the scrubber is on top of the keyframe
+ const leftkf: Doc | undefined = await KeyframeFunc.calcMinLeft(regiondata, this.time); // lef keyframe, if it exists
+ const rightkf: Doc | undefined = await KeyframeFunc.calcMinRight(regiondata, this.time); //right keyframe, if it exists
+ const currentkf: Doc | undefined = await this.calcCurrent(regiondata); //if the scrubber is on top of the keyframe
if (currentkf) {
await this.applyKeys(currentkf);
this.saveStateKf = currentkf;
@@ -233,10 +240,10 @@ export class Track extends React.Component<IProps> {
await this.interpolate(leftkf, rightkf);
}
}
- }
+ };
/**
- * applying changes (when saving the keyframe)
+ * applying changes (when saving the keyframe)
* need to change the logic here
*/
@action
@@ -249,73 +256,73 @@ export class Track extends React.Component<IProps> {
this.props.node[key] = stored instanceof ObjectField ? stored[Copy]() : stored;
}
});
- }
-
+ };
/**
* calculating current keyframe, if the scrubber is right on the keyframe
*/
@action
calcCurrent = (region: Doc) => {
- let currentkf: (Doc | undefined) = undefined;
+ let currentkf: Doc | undefined = undefined;
const keyframes = DocListCast(region.keyframes!);
- keyframes.forEach((kf) => {
+ keyframes.forEach(kf => {
if (NumCast(kf.time) === Math.round(this.time)) currentkf = kf;
});
return currentkf;
- }
-
+ };
/**
- * basic linear interpolation function
+ * basic linear interpolation function
*/
@action
interpolate = async (left: Doc, right: Doc) => {
this.primitiveWhitelist.forEach(key => {
- if (left[key] && right[key] && typeof (left[key]) === "number" && typeof (right[key]) === "number") { //if it is number, interpolate
+ if (left[key] && right[key] && typeof left[key] === 'number' && typeof right[key] === 'number') {
+ //if it is number, interpolate
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);
- } else { // case data
+ this.props.node[key] = NumCast(left[key]) + dif * ratio;
+ } else {
+ // case data
const stored = left[key];
this.props.node[key] = stored instanceof ObjectField ? stored[Copy]() : stored;
}
});
- }
+ };
/**
* finds region that corresponds to specific time (is there a region at this time?)
* linear O(n) (maybe possible to optimize this with other Data structures?)
*/
findRegion = (time: number) => {
- return this.regions?.find(rd => (time >= NumCast(rd.position) && time <= (NumCast(rd.position) + NumCast(rd.duration))));
- }
-
+ return this.regions?.find(rd => time >= NumCast(rd.position) && time <= NumCast(rd.position) + NumCast(rd.duration));
+ };
/**
- * double click on track. Signalling keyframe creation.
+ * double click on track. Signalling keyframe creation.
*/
@action
onInnerDoubleClick = (e: React.MouseEvent) => {
const inner = this._inner.current!;
const offsetX = Math.round((e.clientX - inner.getBoundingClientRect().left) * this.props.transform.Scale);
- this.createRegion(KeyframeFunc.convertPixelTime(offsetX, "mili", "time", this.props.tickSpacing, this.props.tickIncrement));
- }
-
+ this.createRegion(KeyframeFunc.convertPixelTime(offsetX, 'mili', 'time', this.props.tickSpacing, this.props.tickIncrement));
+ };
/**
- * creates a region (KEYFRAME.TSX stuff).
+ * creates a region (KEYFRAME.TSX stuff).
*/
@action
createRegion = (time: number) => {
- if (this.findRegion(time) === undefined) { //check if there is a region where double clicking (prevents phantom regions)
+ if (this.findRegion(time) === undefined) {
+ //check if there is a region where double clicking (prevents phantom regions)
const regiondata = KeyframeFunc.defaultKeyframe(); //create keyframe data
regiondata.position = time; //set position
const rightRegion = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.right, regiondata, this.regions);
- if (rightRegion && rightRegion.position - regiondata.position <= 4000) { //edge case when there is less than default 4000 duration space between this and right region
+ if (rightRegion && rightRegion.position - regiondata.position <= 4000) {
+ //edge case when there is less than default 4000 duration space between this and right region
regiondata.duration = rightRegion.position - regiondata.position;
}
if (this.regions.length === 0 || !rightRegion || (rightRegion && rightRegion.position - regiondata.position >= NumCast(regiondata.fadeIn) + NumCast(regiondata.fadeOut))) {
@@ -325,27 +332,30 @@ export class Track extends React.Component<IProps> {
return regiondata;
}
}
- }
+ };
@action
- makeKeyData = (regiondata: RegionData, time: number, type: KeyframeFunc.KeyframeType = KeyframeFunc.KeyframeType.default) => { //Kfpos is mouse offsetX, representing time
+ makeKeyData = (regiondata: RegionData, time: number, type: KeyframeFunc.KeyframeType = KeyframeFunc.KeyframeType.default) => {
+ //Kfpos is mouse offsetX, representing time
const trackKeyFrames = DocListCast(regiondata.keyframes);
const existingkf = trackKeyFrames.find(TK => TK.time === time);
if (existingkf) return existingkf;
- //else creates a new doc.
+ //else creates a new doc.
const newKeyFrame: Doc = new Doc();
newKeyFrame.time = time;
newKeyFrame.type = type;
this.copyDocDataToKeyFrame(newKeyFrame);
//assuming there are already keyframes (for keeping keyframes in order, sorted by time)
if (trackKeyFrames.length === 0) regiondata.keyframes!.push(newKeyFrame);
- trackKeyFrames.map(kf => NumCast(kf.time)).forEach((kfTime, index) => {
- if ((kfTime < time && index === trackKeyFrames.length - 1) || (kfTime < time && time < NumCast(trackKeyFrames[index + 1].time))) {
- regiondata.keyframes!.splice(index + 1, 0, newKeyFrame);
- }
- });
+ trackKeyFrames
+ .map(kf => NumCast(kf.time))
+ .forEach((kfTime, index) => {
+ if ((kfTime < time && index === trackKeyFrames.length - 1) || (kfTime < time && time < NumCast(trackKeyFrames[index + 1].time))) {
+ regiondata.keyframes!.splice(index + 1, 0, newKeyFrame);
+ }
+ });
return newKeyFrame;
- }
+ };
@action
copyDocDataToKeyFrame = (doc: Doc) => {
@@ -353,7 +363,7 @@ export class Track extends React.Component<IProps> {
const originalVal = this.props.node[key];
doc[key] = originalVal instanceof ObjectField ? originalVal[Copy]() : originalVal;
});
- }
+ };
/**
* UI sstuff here. Not really much to change
@@ -362,10 +372,13 @@ export class Track extends React.Component<IProps> {
return (
<div className="track-container">
<div className="track">
- <div className="inner" ref={this._inner} style={{ height: `${this._trackHeight}px` }}
+ <div
+ className="inner"
+ ref={this._inner}
+ style={{ height: `${this._trackHeight}px` }}
onDoubleClick={this.onInnerDoubleClick}
onPointerOver={() => Doc.BrushDoc(this.props.node)}
- onPointerOut={() => Doc.UnBrushDoc(this.props.node)} >
+ onPointerOut={() => Doc.UnBrushDoc(this.props.node)}>
{this.regions?.map((region, i) => {
return <Keyframe key={`${i}`} {...this.props} RegionData={region} makeKeyData={this.makeKeyData} />;
})}
@@ -374,4 +387,4 @@ export class Track extends React.Component<IProps> {
</div>
);
}
-} \ No newline at end of file
+}
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 01f41869e..d94e552b4 100644
--- a/src/client/views/collections/CollectionCarousel3DView.tsx
+++ b/src/client/views/collections/CollectionCarousel3DView.tsx
@@ -5,11 +5,13 @@ import * as React from 'react';
import { Doc } from '../../../fields/Doc';
import { Id } from '../../../fields/FieldSymbols';
import { NumCast, ScriptCast, StrCast } from '../../../fields/Types';
-import { OmitKeys, returnFalse, Utils } from '../../../Utils';
+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";
+import './CollectionCarousel3DView.scss';
import { CollectionSubView } from './CollectionSubView';
@observer
@@ -20,134 +22,152 @@ export class CollectionCarousel3DView extends CollectionSubView() {
private _dropDisposer?: DragManager.DragDropDisposer;
- componentWillUnmount() { this._dropDisposer?.(); }
+ componentWillUnmount() {
+ this._dropDisposer?.();
+ }
- protected createDashEventsTarget = (ele: HTMLDivElement | null) => { //used for stacking and masonry view
+ protected createDashEventsTarget = (ele: HTMLDivElement | null) => {
+ //used for stacking and masonry view
this._dropDisposer?.();
if (ele) {
this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.layoutDoc);
}
- }
+ };
+ 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 displayDoc = (childPair: { layout: Doc, data: Doc }) => {
- return <DocumentView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "childLayoutTemplate", "childLayoutString"]).omit}
- onDoubleClick={this.onChildDoubleClick}
- renderDepth={this.props.renderDepth + 1}
- LayoutTemplate={this.props.childLayoutTemplate}
- LayoutTemplateString={this.props.childLayoutString}
- Document={childPair.layout}
- DataDoc={childPair.data}
- PanelWidth={this.panelWidth}
- PanelHeight={this.panelHeight}
- bringToFront={returnFalse}
- />;
+ const currentIndex = NumCast(this.layoutDoc._carousel_index);
+ const displayDoc = (childPair: { layout: Doc; data: Doc }) => {
+ return (
+ <DocumentView
+ {...this.props}
+ NativeWidth={returnZero}
+ NativeHeight={returnZero}
+ //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}
+ PanelHeight={this.panelHeight}
+ bringToFront={returnFalse}
+ />
+ );
};
- return (this.childLayoutPairs.map((childPair, index) => {
+ 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>);
- }));
+ </div>
+ );
+ });
}
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) => {
e.stopPropagation();
this.changeSlide(direction);
- !this.layoutDoc.autoScrollOn && (this.layoutDoc.showScrollButton = (direction === 1) ? "fwd" : "back"); // while autoscroll is on, keep the other autoscroll button hidden
+ !this.layoutDoc.autoScrollOn && (this.layoutDoc.showScrollButton = direction === 1 ? 'fwd' : 'back'); // while autoscroll is on, keep the other autoscroll button hidden
!this.layoutDoc.autoScrollOn && this.fadeScrollButton(); // keep pause button visible while autoscroll is on
- }
+ };
interval?: number;
startAutoScroll = (direction: number) => {
this.interval = window.setInterval(() => {
this.changeSlide(direction);
}, this.scrollSpeed);
- }
+ };
stopAutoScroll = () => {
window.clearInterval(this.interval);
this.interval = undefined;
this.fadeScrollButton();
- }
+ };
toggleAutoScroll = (direction: number) => {
this.layoutDoc.autoScrollOn = this.layoutDoc.autoScrollOn ? false : true;
this.layoutDoc.autoScrollOn ? this.startAutoScroll(direction) : this.stopAutoScroll();
- }
+ };
fadeScrollButton = () => {
window.setTimeout(() => {
- !this.layoutDoc.autoScrollOn && (this.layoutDoc.showScrollButton = "none"); //fade away after 1.5s if it's not clicked.
+ !this.layoutDoc.autoScrollOn && (this.layoutDoc.showScrollButton = 'none'); //fade away after 1.5s if it's not clicked.
}, 1500);
- }
+ };
@computed get buttons() {
if (!this.props.isContentActive()) return null;
- 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"} />
- </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"} />
+ 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'} />
+ </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'} />
+ </div>
+ {this.autoScrollButton}
</div>
- {this.autoScrollButton}
- </div>;
+ );
}
@computed get autoScrollButton() {
const whichButton = this.layoutDoc.showScrollButton;
- return <>
- <div className={`carousel3DView-back-scroll${whichButton === "back" ? "" : "-hidden"}`} style={{ background: `${StrCast(this.props.Document.backgroundColor)}` }}
- onClick={() => this.toggleAutoScroll(-1)}>
- {this.layoutDoc.autoScrollOn ? <FontAwesomeIcon icon={"pause"} size={"1x"} /> : <FontAwesomeIcon icon={"angle-double-left"} size={"1x"} />}
- </div>
- <div className={`carousel3DView-fwd-scroll${whichButton === "fwd" ? "" : "-hidden"}`} style={{ background: `${StrCast(this.props.Document.backgroundColor)}` }}
- onClick={() => this.toggleAutoScroll(1)}>
- {this.layoutDoc.autoScrollOn ? <FontAwesomeIcon icon={"pause"} size={"1x"} /> : <FontAwesomeIcon icon={"angle-double-right"} size={"1x"} />}
- </div>
- </>;
+ return (
+ <>
+ <div className={`carousel3DView-back-scroll${whichButton === 'back' ? '' : '-hidden'}`} style={{ background: `${StrCast(this.props.Document.backgroundColor)}` }} onClick={() => this.toggleAutoScroll(-1)}>
+ {this.layoutDoc.autoScrollOn ? <FontAwesomeIcon icon={'pause'} size={'1x'} /> : <FontAwesomeIcon icon={'angle-double-left'} size={'1x'} />}
+ </div>
+ <div className={`carousel3DView-fwd-scroll${whichButton === 'fwd' ? '' : '-hidden'}`} style={{ background: `${StrCast(this.props.Document.backgroundColor)}` }} onClick={() => this.toggleAutoScroll(1)}>
+ {this.layoutDoc.autoScrollOn ? <FontAwesomeIcon icon={'pause'} size={'1x'} /> : <FontAwesomeIcon icon={'angle-double-right'} size={'1x'} />}
+ </div>
+ </>
+ );
}
@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" ref={this.createDashEventsTarget}
- style={{
- 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)` }}>
- {this.content}
- </div>
- {this.props.Document._chromeHidden ? (null) : this.buttons}
- <div className="dot-bar">
- {this.dots}
+ return (
+ <div
+ className="collectionCarousel3DView-outer"
+ ref={this.createDashEventsTarget}
+ style={{
+ 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(${this.translateX}px)` }}>
+ {this.content}
+ </div>
+ {this.props.Document._chromeHidden ? null : this.buttons}
+ <div className="dot-bar">{this.dots}</div>
</div>
- </div>;
+ );
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index abb4b6bc6..ea02bcd4c 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -4,59 +4,66 @@ import { observer } from 'mobx-react';
import * as React from 'react';
import { Doc, Opt } from '../../../fields/Doc';
import { NumCast, ScriptCast, StrCast } from '../../../fields/Types';
-import { OmitKeys, returnFalse } from '../../../Utils';
+import { emptyFunction, returnFalse, returnZero, StopEvent } from '../../../Utils';
import { DragManager } from '../../util/DragManager';
import { DocumentView, DocumentViewProps } from '../nodes/DocumentView';
import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
import { StyleProp } from '../StyleProvider';
-import "./CollectionCarouselView.scss";
+import './CollectionCarouselView.scss';
import { CollectionSubView } from './CollectionSubView';
@observer
export class CollectionCarouselView extends CollectionSubView() {
private _dropDisposer?: DragManager.DragDropDisposer;
- componentWillUnmount() { this._dropDisposer?.(); }
+ componentWillUnmount() {
+ this._dropDisposer?.();
+ }
- protected createDashEventsTarget = (ele: HTMLDivElement | null) => { //used for stacking and masonry view
+ protected createDashEventsTarget = (ele: HTMLDivElement | null) => {
+ //used for stacking and masonry view
this._dropDisposer?.();
if (ele) {
this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.layoutDoc);
}
- }
+ };
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;
- }
- captionStyleProvider = (doc: (Doc | undefined), captionProps: Opt<DocumentViewProps>, property: string): any => {
+ 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
- const childValue = doc?.["caption-" + property] ? this.props.styleProvider?.(doc, captionProps, property) : undefined;
+ const childValue = doc?.['caption-' + property] ? this.props.styleProvider?.(doc, captionProps, property) : undefined;
return childValue ?? this.props.styleProvider?.(this.layoutDoc, captionProps, property);
- }
- panelHeight = () => this.props.PanelHeight() - (StrCast(this.layoutDoc._showCaption) ? 50 : 0);
+ };
+ panelHeight = () => this.props.PanelHeight() - (StrCast(this.layoutDoc._layout_showCaption) ? 50 : 0);
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 = { ...OmitKeys(this.props, ["setHeight",]).omit, fieldKey: "caption" };
- const marginX = NumCast(this.layoutDoc["caption-xMargin"]);
- const marginY = NumCast(this.layoutDoc["caption-yMargin"]);
- const showCaptions = StrCast(this.layoutDoc._showCaption);
- return !(curDoc?.layout instanceof Doc) ? (null) :
+ const captionProps = { ...this.props, fieldKey: 'caption', setHeight: undefined };
+ const marginX = NumCast(this.layoutDoc['caption_xMargin']);
+ const marginY = NumCast(this.layoutDoc['caption_yMargin']);
+ const show_captions = StrCast(this.layoutDoc._layout_showCaption);
+ return !(curDoc?.layout instanceof Doc) ? null : (
<>
<div className="collectionCarouselView-image" key="image">
- <DocumentView {...OmitKeys(this.props, ["setHeight", "NativeWidth", "NativeHeight", "childLayoutTemplate", "childLayoutString"]).omit}
+ <DocumentView
+ {...this.props}
+ NativeWidth={returnZero}
+ NativeHeight={returnZero}
onDoubleClick={this.onContentDoubleClick}
onClick={this.onContentClick}
- hideCaptions={showCaptions ? true : false}
+ 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}
- ContainingCollectionView={this}
LayoutTemplate={this.props.childLayoutTemplate}
LayoutTemplateString={this.props.childLayoutString}
Document={curDoc.layout}
@@ -65,40 +72,47 @@ export class CollectionCarouselView extends CollectionSubView() {
bringToFront={returnFalse}
/>
</div>
- <div className="collectionCarouselView-caption" key="caption"
+ <div
+ className="collectionCarouselView-caption"
+ key="caption"
+ onWheel={StopEvent}
style={{
- display: showCaptions ? undefined : "none",
+ display: show_captions ? undefined : 'none',
borderRadius: this.props.styleProvider?.(this.layoutDoc, captionProps, StyleProp.BorderRounding),
- marginRight: marginX, marginLeft: marginX,
- width: `calc(100% - ${marginX * 2}px)`
+ marginRight: marginX,
+ marginLeft: marginX,
+ width: `calc(100% - ${marginX * 2}px)`,
}}>
- <FormattedTextBox key={index} {...captionProps}
- fieldKey={showCaptions}
- styleProvider={this.captionStyleProvider}
- Document={curDoc.layout}
- DataDoc={undefined} />
+ <FormattedTextBox key={index} {...captionProps} allowScroll={true} fieldKey={show_captions} styleProvider={this.captionStyleProvider} Document={curDoc.layout} DataDoc={undefined} />
</div>
- </>;
+ </>
+ );
}
@computed get buttons() {
- return <>
- <div key="back" className="carouselView-back" onClick={this.goback}>
- <FontAwesomeIcon icon={"chevron-left"} size={"2x"} />
- </div>
- <div key="fwd" className="carouselView-fwd" onClick={this.advance}>
- <FontAwesomeIcon icon={"chevron-right"} size={"2x"} />
- </div>
- </>;
+ return (
+ <>
+ <div key="back" className="carouselView-back" onClick={this.goback}>
+ <FontAwesomeIcon icon={'chevron-left'} size={'2x'} />
+ </div>
+ <div key="fwd" className="carouselView-fwd" onClick={this.advance}>
+ <FontAwesomeIcon icon={'chevron-right'} size={'2x'} />
+ </div>
+ </>
+ );
}
render() {
- return <div className="collectionCarouselView-outer" ref={this.createDashEventsTarget}
- style={{
- background: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor),
- color: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color),
- }}>
- {this.content}
- {this.props.Document._chromeHidden ? (null) : this.buttons}
- </div>;
+ return (
+ <div
+ className="collectionCarouselView-outer"
+ ref={this.createDashEventsTarget}
+ style={{
+ background: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor),
+ color: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color),
+ }}>
+ {this.content}
+ {this.props.Document._chromeHidden ? null : this.buttons}
+ </div>
+ );
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 92319d080..a5d7e7864 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -6,7 +6,7 @@ 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 { emptyFunction, incrementTitleCopy } from '../../../Utils';
@@ -20,6 +20,10 @@ 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';
@@ -30,7 +34,7 @@ const _global = (window /* browser */ || global) /* node */ as any;
@observer
export class CollectionDockingView extends CollectionSubView() {
@observable public static Instance: CollectionDockingView | undefined;
- public static makeDocumentConfig(document: Doc, panelName?: string, width?: number) {
+ public static makeDocumentConfig(document: Doc, panelName?: string, width?: number, keyValue?: boolean) {
return {
type: 'react-component',
component: 'DocumentFrameRenderer',
@@ -38,6 +42,7 @@ export class CollectionDockingView extends CollectionSubView() {
width: width,
props: {
documentId: document[Id],
+ keyValue,
panelName, // name of tab that can be used to close or replace its contents
},
};
@@ -81,6 +86,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) => {
@@ -88,12 +94,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,12 +129,12 @@ export class CollectionDockingView extends CollectionSubView() {
SelectionManager.DeselectAll();
const instance = CollectionDockingView.Instance;
if (instance) {
- if (doc._viewType === CollectionViewType.Docking && doc.layoutKey === 'layout') {
+ if (doc._type_collection === CollectionViewType.Docking && doc.layout_fieldKey === 'layout') {
return DashboardView.openDashboard(doc);
}
const newItemStackConfig = {
type: 'stack',
- content: [CollectionDockingView.makeDocumentConfig(Doc.MakeAlias(doc))],
+ content: [CollectionDockingView.makeDocumentConfig(Doc.MakeEmbedding(doc))],
};
const docconfig = instance._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, instance._goldenLayout);
instance._goldenLayout.root.contentItems[0].addChild(docconfig);
@@ -142,10 +148,10 @@ export class CollectionDockingView extends CollectionSubView() {
@undoBatch
@action
- public static ReplaceTab(document: Doc, panelName: string, stack: any, addToSplit?: boolean): boolean {
+ public static ReplaceTab(document: Doc, panelName: OpenWhereMod, stack: any, addToSplit?: boolean, keyValue?: boolean): boolean {
const instance = CollectionDockingView.Instance;
if (!instance) return false;
- const newConfig = CollectionDockingView.makeDocumentConfig(document, panelName);
+ const newConfig = CollectionDockingView.makeDocumentConfig(document, panelName, undefined, keyValue);
if (!panelName && stack) {
const activeContentItemIndex = stack.contentItems.findIndex((item: any) => item.config === stack._activeContentItem.config);
const newContentItem = stack.layoutManager.createContentItem(newConfig, instance._goldenLayout);
@@ -155,30 +161,30 @@ export class CollectionDockingView extends CollectionSubView() {
}
const tab = Array.from(instance.tabMap.keys()).find(tab => tab.contentItem.config.props.panelName === panelName);
if (tab) {
- tab.header.parent.addChild(newConfig, undefined);
const j = tab.header.parent.contentItems.indexOf(tab.contentItem);
- !addToSplit && j !== -1 && tab.header.parent.contentItems[j].remove();
- return instance.layoutChanged();
+ if (newConfig.props.documentId !== tab.header.parent.contentItems[j].config.props.documentId) {
+ tab.header.parent.addChild(newConfig, undefined);
+ !addToSplit && j !== -1 && tab.header.parent.contentItems[j].remove();
+ return instance.layoutChanged();
+ }
+ return false;
}
return CollectionDockingView.AddSplit(document, panelName, stack, panelName);
}
@undoBatch
- public static ToggleSplit(doc: Doc, location: string, stack?: any, panelName?: string) {
- return CollectionDockingView.Instance && Array.from(CollectionDockingView.Instance.tabMap.keys()).findIndex(tab => tab.DashDoc === doc) !== -1
- ? CollectionDockingView.CloseSplit(doc)
- : CollectionDockingView.AddSplit(doc, location, stack, panelName);
+ public static ToggleSplit(doc: Doc, location: OpenWhereMod, stack?: any, panelName?: string, keyValue?: boolean) {
+ 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: string, stack?: any, panelName?: string) {
- if (document?._viewType === CollectionViewType.Docking) return DashboardView.openDashboard(document);
+ public static AddSplit(document: Doc, pullSide: OpenWhereMod, stack?: any, panelName?: string, keyValue?: boolean) {
+ 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);
+ const tab = Array.from(CollectionDockingView.Instance.tabMap).find(tab => tab.DashDoc === document && !keyValue);
if (tab) {
tab.header.parent.setActiveContentItem(tab.contentItem);
return true;
@@ -186,11 +192,13 @@ export class CollectionDockingView extends CollectionSubView() {
const instance = CollectionDockingView.Instance;
const glayRoot = instance._goldenLayout.root;
if (!instance) return false;
- const docContentConfig = CollectionDockingView.makeDocumentConfig(document, panelName);
+ 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);
- stack.setActiveContentItem(stack.contentItems[stack.contentItems.length - 1]);
+ setTimeout(() => stack.setActiveContentItem(stack.contentItems[stack.contentItems.length - 1]));
} else {
const newContentItem = () => {
const newItem = glayRoot.layoutManager.createContentItem({ type: 'stack', content: [docContentConfig] }, instance._goldenLayout);
@@ -208,14 +216,15 @@ export class CollectionDockingView extends CollectionSubView() {
// if row
switch (pullSide) {
default:
- case 'right':
+ case OpenWhereMod.none:
+ case OpenWhereMod.right:
glayRoot.contentItems[0].addChild(newContentItem());
break;
- case 'left':
+ case OpenWhereMod.left:
glayRoot.contentItems[0].addChild(newContentItem(), 0);
break;
- case 'top':
- case 'bottom':
+ case OpenWhereMod.top:
+ case OpenWhereMod.bottom:
// if not going in a row layout, must add already existing content into column
const rowlayout = glayRoot.contentItems[0];
const newColumn = rowlayout.layoutManager.createContentItem({ type: 'column' }, instance._goldenLayout);
@@ -362,16 +371,30 @@ 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) {
+ this.props.Document.dockingConfig = json;
+ this.props.Document.data = new List<Doc>(docs);
+ }
+ 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
@@ -385,9 +408,8 @@ 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');
- }
+ if (className.includes('lm_maximise')) this._flush = UndoManager.StartBatch('tab maximize');
+ else if (!className.includes('lm_close')) DocServer.UPDATE_SERVER_CACHE();
}
}
if (!InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE) && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) {
@@ -401,11 +423,10 @@ export class CollectionDockingView extends CollectionSubView() {
const _width = Number(getComputedStyle(content).width.replace('px', ''));
const _height = Number(getComputedStyle(content).height.replace('px', ''));
return CollectionFreeFormView.UpdateIcon(this.layoutDoc[Id] + '-icon' + new Date().getTime(), content, _width, _height, _width, _height, 0, 1, true, this.layoutDoc[Id] + '-icon', (iconFile, _nativeWidth, _nativeHeight) => {
- const img = Docs.Create.ImageDocument(new ImageField(iconFile), { title: this.rootDoc.title + '-icon', _width, _height, _nativeWidth, _nativeHeight });
const proto = this.dataDoc; // Cast(img.proto, Doc, null)!;
- proto['thumb-nativeWidth'] = _width;
- proto['thumb-nativeHeight'] = _height;
- this.dataDoc.thumb = new ImageField(iconFile);
+ proto['thumb_nativeWidth'] = _width;
+ proto['thumb_nativeHeight'] = _height;
+ proto.thumb = new ImageField(iconFile);
});
}
}
@@ -426,11 +447,11 @@ export class CollectionDockingView extends CollectionSubView() {
.map(f => f as Doc);
const newtabs = origtabs.map(origtab => {
const origtabdocs = DocListCast(origtab.data);
- const newtab = origtabdocs.length ? Doc.MakeCopy(origtab, true, undefined, true) : Doc.MakeAlias(origtab);
- const newtabdocs = origtabdocs.map(origtabdoc => Doc.MakeAlias(origtabdoc));
+ const newtab = origtabdocs.length ? Doc.MakeCopy(origtab, true, undefined, true) : Doc.MakeEmbedding(origtab);
+ const newtabdocs = origtabdocs.map(origtabdoc => Doc.MakeEmbedding(origtabdoc));
if (newtabdocs.length) {
Doc.GetProto(newtab).data = new List<Doc>(newtabdocs);
- newtabdocs.forEach(ntab => (ntab.context = newtab));
+ newtabdocs.forEach(ntab => (ntab.embedContainer = newtab));
}
json = json.replace(origtab[Id], newtab[Id]);
return newtab;
@@ -444,26 +465,13 @@ 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.dockcingConfig !== json;
- if (changesMade && !this._flush) {
- UndoManager.RunInBatch(() => {
- this.props.Document.dockingConfig = json;
- this.props.Document.data = new List<Doc>(docs);
- }, 'state changed');
- }
+ const changesMade = this.props.Document.dockingConfig !== json;
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);
}
@@ -473,7 +481,6 @@ export class CollectionDockingView extends CollectionSubView() {
Doc.RemoveDocFromList(dview, fieldKey, tab.DashDoc);
this.tabMap.delete(tab);
tab._disposers && Object.values(tab._disposers).forEach((disposer: any) => disposer?.());
- //tab.reactComponents?.forEach((ele: any) => ReactDOM.unmountComponentAtNode(ele));
this.stateChanged();
}
};
@@ -491,12 +498,12 @@ export class CollectionDockingView extends CollectionSubView() {
const docToAdd = Docs.Create.FreeformDocument([], {
_width: this.props.PanelWidth(),
_height: this.props.PanelHeight(),
- _backgroundGridShow: true,
- _fitWidth: true,
+ _freeform_backgroundGrid: true,
+ _layout_fitWidth: true,
title: `Untitled Tab ${NumCast(dashboard['pane-count'])}`,
});
this.props.Document.isShared && inheritParentAcls(this.props.Document, docToAdd);
- CollectionDockingView.AddSplit(docToAdd, '', stack);
+ CollectionDockingView.AddSplit(docToAdd, OpenWhereMod.none, stack);
}
});
@@ -507,7 +514,12 @@ export class CollectionDockingView extends CollectionSubView() {
action(() => {
//if (confirm('really close this?')) {
if ((!stack.parent.isRoot && !stack.parent.parent.isRoot) || stack.parent.contentItems.length > 1) {
+ const batch = UndoManager.StartBatch('close stack');
stack.remove();
+ setTimeout(() => {
+ this.stateChanged();
+ batch.end();
+ });
} else {
alert('cant delete the last stack');
}
@@ -529,12 +541,12 @@ export class CollectionDockingView extends CollectionSubView() {
const docToAdd = Docs.Create.FreeformDocument([], {
_width: this.props.PanelWidth(),
_height: this.props.PanelHeight(),
- _fitWidth: true,
- _backgroundGridShow: true,
+ _layout_fitWidth: true,
+ _freeform_backgroundGrid: true,
title: `Untitled Tab ${NumCast(dashboard['pane-count'])}`,
});
this.props.Document.isShared && inheritParentAcls(this.props.Document, docToAdd);
- CollectionDockingView.AddSplit(docToAdd, '', stack);
+ CollectionDockingView.AddSplit(docToAdd, OpenWhereMod.none, stack);
}
})
);
@@ -563,25 +575,35 @@ export class CollectionDockingView extends CollectionSubView() {
ScriptingGlobals.add(
function openInLightbox(doc: any) {
- LightboxView.AddDocTab(doc, 'lightbox');
+ LightboxView.AddDocTab(doc, OpenWhere.lightbox);
},
'opens up document in a lightbox',
'(doc: any)'
);
ScriptingGlobals.add(
- function openOnRight(doc: any) {
- return CollectionDockingView.AddSplit(doc, 'right');
+ function openDoc(doc: any, where: OpenWhere) {
+ switch (where) {
+ case OpenWhere.addRight:
+ return CollectionDockingView.AddSplit(doc, OpenWhereMod.right);
+ case OpenWhere.overlay:
+ // 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 tab on right side of the screen',
+ 'opens up document in location specified',
'(doc: any)'
);
ScriptingGlobals.add(
- function openInOverlay(doc: any) {
- return Doc.AddDocToList(Doc.MyOverlayDocs, undefined, doc);
+ function openRepl() {
+ return 'openRepl';
},
'opens up document in screen overlay layer',
'(doc: any)'
);
-ScriptingGlobals.add(function useRightSplit(doc: any, shiftKey?: boolean) {
- CollectionDockingView.ReplaceTab(doc, 'right', undefined, shiftKey);
+ScriptingGlobals.add(function useRightSplit(doc: any, addToRightSplit?: boolean) {
+ CollectionDockingView.ReplaceTab(doc, OpenWhereMod.right, undefined, addToRightSplit);
});
diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
index a94e706eb..6f88f6727 100644
--- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
+++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
@@ -1,24 +1,25 @@
-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 { 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 { Docs } from "../../documents/Documents";
-import { DragManager } from "../../util/DragManager";
-import { CompileScript } from "../../util/Scripting";
-import { SnappingManager } from "../../util/SnappingManager";
-import { Transform } from "../../util/Transform";
-import { undoBatch } from "../../util/UndoManager";
-import { EditableView } from "../EditableView";
-import { FormattedTextBox } from "../nodes/formattedText/FormattedTextBox";
-import { CollectionStackingView } from "./CollectionStackingView";
-import "./CollectionStackingView.scss";
-const higflyout = require("@hig/flyout");
+import React = require('react');
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { action, computed, observable, runInAction } from 'mobx';
+import { observer } from 'mobx-react';
+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 { Docs } from '../../documents/Documents';
+import { DragManager } from '../../util/DragManager';
+import { CompileScript } from '../../util/Scripting';
+import { SnappingManager } from '../../util/SnappingManager';
+import { Transform } from '../../util/Transform';
+import { undoBatch } from '../../util/UndoManager';
+import { EditableView } from '../EditableView';
+import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
+import { CollectionStackingView } from './CollectionStackingView';
+import './CollectionStackingView.scss';
+const higflyout = require('@hig/flyout');
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
@@ -32,7 +33,7 @@ interface CMVFieldRowProps {
docList: Doc[];
parent: CollectionStackingView;
pivotField: string;
- type: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" | undefined;
+ type: 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function' | undefined;
createDropTarget: (ele: HTMLDivElement) => void;
screenToLocalTransform: () => Transform;
setDocHeight: (key: string, thisHeight: number) => void;
@@ -43,15 +44,21 @@ interface CMVFieldRowProps {
@observer
export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowProps> {
- @observable private _background = "inherit";
- @observable private _createAliasSelected: boolean = false;
- @observable private heading: string = "";
- @observable private color: string = "#f1efeb";
+ @observable private _background = 'inherit';
+ @observable private _createEmbeddingSelected: boolean = false;
+ @observable private heading: string = '';
+ @observable private color: string = '#f1efeb';
@observable private collapsed: boolean = false;
@observable private _paletteOn = false;
- private set _heading(value: string) { runInAction(() => this.props.headingObject && (this.props.headingObject.heading = this.heading = value)); }
- private set _color(value: string) { runInAction(() => this.props.headingObject && (this.props.headingObject.color = this.color = value)); }
- private set _collapsed(value: boolean) { runInAction(() => this.props.headingObject && (this.props.headingObject.collapsed = this.collapsed = value)); }
+ private set _heading(value: string) {
+ runInAction(() => this.props.headingObject && (this.props.headingObject.heading = this.heading = value));
+ }
+ private set _color(value: string) {
+ runInAction(() => this.props.headingObject && (this.props.headingObject.color = this.color = value));
+ }
+ private set _collapsed(value: boolean) {
+ runInAction(() => this.props.headingObject && (this.props.headingObject.collapsed = this.collapsed = value));
+ }
private _dropDisposer?: DragManager.DragDropDisposer;
private _headerRef: React.RefObject<HTMLDivElement> = React.createRef();
@@ -65,11 +72,11 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
this.props.observeHeight(ele);
this._dropDisposer = DragManager.MakeDropTarget(ele, this.rowDrop.bind(this));
}
- }
+ };
@action
componentDidMount() {
- this.heading = this.props.headingObject?.heading || "";
- this.color = this.props.headingObject?.color || "#f1efeb";
+ this.heading = this.props.headingObject?.heading || '';
+ this.color = this.props.headingObject?.color || '#f1efeb';
this.collapsed = this.props.headingObject?.collapsed || false;
}
componentWillUnmount() {
@@ -85,39 +92,39 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
const trueHeight = rawHeight * transformScale;
this.props.setDocHeight(this.heading, trueHeight);
}
- }
+ };
@undoBatch
rowDrop = action((e: Event, de: DragManager.DropEvent) => {
- this._createAliasSelected = false;
+ 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 => {
const parsed = parseInt(value);
if (!isNaN(parsed)) return parsed;
- if (value.toLowerCase().indexOf("true") > -1) return true;
- if (value.toLowerCase().indexOf("false") > -1) return false;
+ if (value.toLowerCase().indexOf('true') > -1) return true;
+ if (value.toLowerCase().indexOf('false') > -1) return false;
return value;
- }
+ };
@action
headingChanged = (value: string, shiftDown?: boolean) => {
- this._createAliasSelected = false;
+ this._createEmbeddingSelected = false;
const key = this.props.pivotField;
const castedValue = this.getValue(value);
if (castedValue) {
- if (this.props.parent.columnHeaders) {
- if (this.props.parent.columnHeaders.map(i => i.heading).indexOf(castedValue.toString()) > -1) {
+ if (this.props.parent.colHeaderData) {
+ if (this.props.parent.colHeaderData.map(i => i.heading).indexOf(castedValue.toString()) > -1) {
return false;
}
}
@@ -126,74 +133,76 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
return true;
}
return false;
- }
+ };
@action
changeColumnColor = (color: string) => {
- this._createAliasSelected = false;
+ this._createEmbeddingSelected = false;
this._color = color;
- }
+ };
- pointerEnteredRow = action(() => SnappingManager.GetIsDragging() && (this._background = "#b4b4b4"));
+ pointerEnteredRow = action(() => SnappingManager.GetIsDragging() && (this._background = '#b4b4b4'));
@action
pointerLeaveRow = () => {
- this._createAliasSelected = false;
- this._background = "inherit";
- }
+ this._createEmbeddingSelected = false;
+ this._background = 'inherit';
+ };
@action
addDocument = (value: string, shiftDown?: boolean, forceEmptyNote?: boolean) => {
if (!value && !forceEmptyNote) return false;
- this._createAliasSelected = false;
+ this._createEmbeddingSelected = false;
const key = this.props.pivotField;
- const newDoc = Docs.Create.TextDocument("", { _autoHeight: true, _width: 200, _fitWidth: true, title: value });
+ const newDoc = Docs.Create.TextDocument('', { _layout_autoHeight: true, _width: 200, _layout_fitWidth: true, title: value });
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)
- }
+ };
- deleteRow = undoBatch(action(() => {
- this._createAliasSelected = false;
- const key = this.props.pivotField;
- this.props.docList.forEach(d => Doc.SetInPlace(d, key, undefined, true));
- if (this.props.parent.columnHeaders && this.props.headingObject) {
- const index = this.props.parent.columnHeaders.indexOf(this.props.headingObject);
- this.props.parent.columnHeaders.splice(index, 1);
- }
- }));
+ deleteRow = undoBatch(
+ action(() => {
+ this._createEmbeddingSelected = false;
+ const key = this.props.pivotField;
+ this.props.docList.forEach(d => Doc.SetInPlace(d, key, undefined, true));
+ if (this.props.parent.colHeaderData && this.props.headingObject) {
+ const index = this.props.parent.colHeaderData.indexOf(this.props.headingObject);
+ this.props.parent.colHeaderData.splice(index, 1);
+ }
+ })
+ );
@action
collapseSection = (e: any) => {
- this._createAliasSelected = false;
+ this._createEmbeddingSelected = false;
this.toggleVisibility();
e.stopPropagation();
- }
+ };
headerMove = (e: PointerEvent) => {
- const alias = Doc.MakeAlias(this.props.Document);
+ const embedding = Doc.MakeEmbedding(this.props.Document);
const key = this.props.pivotField;
let value = this.getValue(this.heading);
- value = typeof value === "string" ? `"${value}"` : value;
+ value = typeof value === 'string' ? `"${value}"` : value;
const script = `return doc.${key} === ${value}`;
const compiled = CompileScript(script, { params: { doc: Doc.name } });
if (compiled.compiled) {
- alias.viewSpecScript = new ScriptField(compiled);
- DragManager.StartDocumentDrag([this._headerRef.current!], new DragManager.DocumentDragData([alias]), e.clientX, e.clientY);
+ embedding.viewSpecScript = new ScriptField(compiled);
+ DragManager.StartDocumentDrag([this._headerRef.current!], new DragManager.DocumentDragData([embedding]), e.clientX, e.clientY);
}
return true;
- }
+ };
@action
headerDown = (e: React.PointerEvent<HTMLDivElement>) => {
if (e.button === 0 && !e.ctrlKey) {
setupMoveUpEvents(this, e, this.headerMove, emptyFunction, e => !this.props.chromeHidden && this.collapseSection(e));
- this._createAliasSelected = false;
+ this._createEmbeddingSelected = false;
}
- }
+ };
/**
* Returns true if a key is on the layout doc of the documents in the collection.
@@ -203,143 +212,141 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
if (Doc.Get(doc, key, true)) return true;
});
return false;
- }
+ };
renderColorPicker = () => {
const selected = this.color;
- const pink = PastelSchemaPalette.get("pink2");
- const purple = PastelSchemaPalette.get("purple4");
- const blue = PastelSchemaPalette.get("bluegreen1");
- const yellow = PastelSchemaPalette.get("yellow4");
- const red = PastelSchemaPalette.get("red2");
- const green = PastelSchemaPalette.get("bluegreen7");
- const cyan = PastelSchemaPalette.get("bluegreen5");
- const orange = PastelSchemaPalette.get("orange1");
- const gray = "#f1efeb";
+ const pink = PastelSchemaPalette.get('pink2');
+ const purple = PastelSchemaPalette.get('purple4');
+ const blue = PastelSchemaPalette.get('bluegreen1');
+ const yellow = PastelSchemaPalette.get('yellow4');
+ const red = PastelSchemaPalette.get('red2');
+ const green = PastelSchemaPalette.get('bluegreen7');
+ const cyan = PastelSchemaPalette.get('bluegreen5');
+ const orange = PastelSchemaPalette.get('orange1');
+ const gray = '#f1efeb';
return (
<div className="collectionStackingView-colorPicker">
<div className="colorOptions">
- <div className={"colorPicker" + (selected === pink ? " active" : "")} style={{ backgroundColor: pink }} onClick={() => this.changeColumnColor(pink!)}></div>
- <div className={"colorPicker" + (selected === purple ? " active" : "")} style={{ backgroundColor: purple }} onClick={() => this.changeColumnColor(purple!)}></div>
- <div className={"colorPicker" + (selected === blue ? " active" : "")} style={{ backgroundColor: blue }} onClick={() => this.changeColumnColor(blue!)}></div>
- <div className={"colorPicker" + (selected === yellow ? " active" : "")} style={{ backgroundColor: yellow }} onClick={() => this.changeColumnColor(yellow!)}></div>
- <div className={"colorPicker" + (selected === red ? " active" : "")} style={{ backgroundColor: red }} onClick={() => this.changeColumnColor(red!)}></div>
- <div className={"colorPicker" + (selected === gray ? " active" : "")} style={{ backgroundColor: gray }} onClick={() => this.changeColumnColor(gray)}></div>
- <div className={"colorPicker" + (selected === green ? " active" : "")} style={{ backgroundColor: green }} onClick={() => this.changeColumnColor(green!)}></div>
- <div className={"colorPicker" + (selected === cyan ? " active" : "")} style={{ backgroundColor: cyan }} onClick={() => this.changeColumnColor(cyan!)}></div>
- <div className={"colorPicker" + (selected === orange ? " active" : "")} style={{ backgroundColor: orange }} onClick={() => this.changeColumnColor(orange!)}></div>
+ <div className={'colorPicker' + (selected === pink ? ' active' : '')} style={{ backgroundColor: pink }} onClick={() => this.changeColumnColor(pink!)}></div>
+ <div className={'colorPicker' + (selected === purple ? ' active' : '')} style={{ backgroundColor: purple }} onClick={() => this.changeColumnColor(purple!)}></div>
+ <div className={'colorPicker' + (selected === blue ? ' active' : '')} style={{ backgroundColor: blue }} onClick={() => this.changeColumnColor(blue!)}></div>
+ <div className={'colorPicker' + (selected === yellow ? ' active' : '')} style={{ backgroundColor: yellow }} onClick={() => this.changeColumnColor(yellow!)}></div>
+ <div className={'colorPicker' + (selected === red ? ' active' : '')} style={{ backgroundColor: red }} onClick={() => this.changeColumnColor(red!)}></div>
+ <div className={'colorPicker' + (selected === gray ? ' active' : '')} style={{ backgroundColor: gray }} onClick={() => this.changeColumnColor(gray)}></div>
+ <div className={'colorPicker' + (selected === green ? ' active' : '')} style={{ backgroundColor: green }} onClick={() => this.changeColumnColor(green!)}></div>
+ <div className={'colorPicker' + (selected === cyan ? ' active' : '')} style={{ backgroundColor: cyan }} onClick={() => this.changeColumnColor(cyan!)}></div>
+ <div className={'colorPicker' + (selected === orange ? ' active' : '')} style={{ backgroundColor: orange }} onClick={() => this.changeColumnColor(orange!)}></div>
</div>
</div>
);
- }
+ };
- toggleAlias = action(() => this._createAliasSelected = true);
- toggleVisibility = () => this._collapsed = !this.collapsed;
+ toggleEmbedding = action(() => (this._createEmbeddingSelected = true));
+ toggleVisibility = () => (this._collapsed = !this.collapsed);
renderMenu = () => {
- const selected = this._createAliasSelected;
- return (<div className="collectionStackingView-optionPicker">
- <div className="optionOptions">
- <div className={"optionPicker" + (selected === true ? " active" : "")} onClick={this.toggleAlias}>Create Alias</div>
- <div className={"optionPicker" + (selected === true ? " active" : "")} onClick={this.deleteRow}>Delete</div>
+ const selected = this._createEmbeddingSelected;
+ 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}>
+ Delete
+ </div>
+ </div>
</div>
- </div>);
- }
+ );
+ };
@action
textCallback = (char: string) => {
- return this.addDocument("", false);
- }
+ return this.addDocument('', false);
+ };
@computed get contentLayout() {
const rows = Math.max(1, Math.min(this.props.docList.length, Math.floor((this.props.parent.props.PanelWidth() - 2 * this.props.parent.xMargin) / (this.props.parent.columnWidth + this.props.parent.gridGap))));
const showChrome = !this.props.chromeHidden;
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" }}>
- {showChrome ?
- <div className="collectionStackingView-addDocumentButton"
+ return this.collapsed ? null : (
+ <div style={{ position: 'relative' }}>
+ {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`
+ 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"}
- toggle={this.toggleVisibility} />
- </div> : null
- }
- <div className={`collectionStackingView-masonryGrid`}
+ <EditableView GetValue={returnEmptyString} SetValue={this.addDocument} textCallback={this.textCallback} contents={'+ NEW'} />
+ </div>
+ ) : null}
+ <div
+ className={`collectionStackingView-masonryGrid`}
ref={this._contRef}
style={{
padding: stackPad,
width: this.props.parent.NodeWidth,
gridGap: this.props.parent.gridGap,
- gridTemplateColumns: numberRange(rows).reduce((list: string, i: any) => list + ` ${this.props.parent.columnWidth}px`, ""),
+ 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)}
+ {this.props.showHandle && this.props.parent.props.isContentActive() ? this.props.parent.columnDragger : null}
</div>
- </div>;
+ </div>
+ );
}
@computed get headingView() {
const noChrome = this.props.chromeHidden;
const key = this.props.pivotField;
- const evContents = this.heading ? this.heading : this.props.type && this.props.type === "number" ? "0" : `NO ${key.toUpperCase()} VALUE`;
- const editableHeaderView = <EditableView
- GetValue={() => evContents}
- SetValue={this.headingChanged}
- contents={evContents}
- oneLine={true}
- toggle={this.toggleVisibility} />;
- return this.props.Document.miniHeaders ?
- <div className="collectionStackingView-miniHeader">
- {editableHeaderView}
- </div> :
- !this.props.headingObject ? (null) :
- <div className="collectionStackingView-sectionHeader" ref={this._headerRef} >
- <div className="collectionStackingView-sectionHeader-subCont" 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 === `NO ${key.toUpperCase()} VALUE` ? (null) :
- <div className="collectionStackingView-sectionColor">
- <button className="collectionStackingView-sectionColorButton" onClick={action(e => this._paletteOn = !this._paletteOn)}>
- <FontAwesomeIcon icon="palette" size="lg" />
+ const evContents = this.heading ? this.heading : this.props.type && this.props.type === 'number' ? '0' : `NO ${key.toUpperCase()} VALUE`;
+ const editableHeaderView = <EditableView GetValue={() => evContents} SetValue={this.headingChanged} contents={evContents} oneLine={true} />;
+ return this.props.Document.miniHeaders ? (
+ <div className="collectionStackingView-miniHeader">{editableHeaderView}</div>
+ ) : !this.props.headingObject ? null : (
+ <div className="collectionStackingView-sectionHeader" ref={this._headerRef}>
+ <div
+ className="collectionStackingView-sectionHeader-subCont"
+ 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 === `NO ${key.toUpperCase()} VALUE` ? null : (
+ <div className="collectionStackingView-sectionColor">
+ <button className="collectionStackingView-sectionColorButton" onClick={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}>
+ <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()}>
+ <button className="collectionStackingView-sectionOptionButton">
+ <FontAwesomeIcon icon="ellipsis-v" size="lg" />
</button>
- {this._paletteOn ? this.renderColorPicker() : (null)}
- </div>
- }
- {noChrome ? (null) : <button className="collectionStackingView-sectionDelete" onClick={noChrome ? undefined : 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()}>
- <button className="collectionStackingView-sectionOptionButton">
- <FontAwesomeIcon icon="ellipsis-v" size="lg" />
- </button>
- </Flyout>
- </div>
- }
- </div>
- </div>;
+ </Flyout>
+ </div>
+ )}
+ </div>
+ </div>
+ );
}
render() {
const background = this._background;
- return <div className="collectionStackingView-masonrySection"
- style={{ width: this.props.parent.NodeWidth, background }}
- ref={this.createRowDropRef}
- onPointerEnter={this.pointerEnteredRow}
- onPointerLeave={this.pointerLeaveRow}
- >
- {this.headingView}
- {this.contentLayout}
- </div >;
+ return (
+ <div className="collectionStackingView-masonrySection" style={{ width: this.props.parent.NodeWidth, background }} ref={this.createRowDropRef} onPointerEnter={this.pointerEnteredRow} onPointerLeave={this.pointerLeaveRow}>
+ {this.headingView}
+ {this.contentLayout}
+ </div>
+ );
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx
index db81f28f6..7d71bce13 100644
--- a/src/client/views/collections/CollectionMenu.tsx
+++ b/src/client/views/collections/CollectionMenu.tsx
@@ -14,7 +14,8 @@ 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 { 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';
@@ -31,7 +32,7 @@ import { Colors } from '../global/globalEnums';
import { ActiveFillColor, ActiveInkColor, SetActiveArrowEnd, SetActiveArrowStart, SetActiveBezierApprox, SetActiveFillColor, SetActiveInkColor, SetActiveInkWidth } from '../InkingStroke';
import { LightboxView } from '../LightboxView';
import { CollectionFreeFormDocumentView } from '../nodes/CollectionFreeFormDocumentView';
-import { DocumentView } from '../nodes/DocumentView';
+import { DocumentView, OpenWhereMod } from '../nodes/DocumentView';
import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
import { RichTextMenu } from '../nodes/formattedText/RichTextMenu';
import { DefaultStyleProvider } from '../StyleProvider';
@@ -40,7 +41,7 @@ import { CollectionLinearView } from './collectionLinear';
import './CollectionMenu.scss';
import { COLLECTION_BORDER_WIDTH } from './CollectionView';
import { TabDocView } from './TabDocView';
-import { GestureUtils } from '../../../pen-gestures/GestureUtils';
+import { CollectionFreeFormView } from './collectionFreeForm';
interface CollectionMenuProps {
panelHeight: () => number;
@@ -107,8 +108,8 @@ export class CollectionMenu extends AntimodeMenu<CollectionMenuProps> {
<CollectionLinearView
Document={selDoc}
DataDoc={undefined}
- fieldKey={'data'}
- dropAction={'alias'}
+ fieldKey="data"
+ dropAction="embed"
setHeight={returnFalse}
styleProvider={DefaultStyleProvider}
rootSelected={returnTrue}
@@ -119,7 +120,6 @@ export class CollectionMenu extends AntimodeMenu<CollectionMenuProps> {
isSelected={returnFalse}
docViewPath={returnEmptyDoclist}
moveDocument={returnFalse}
- CollectionView={undefined}
addDocument={returnFalse}
addDocTab={returnFalse}
pinToPres={emptyFunction}
@@ -130,11 +130,9 @@ export class CollectionMenu extends AntimodeMenu<CollectionMenuProps> {
renderDepth={0}
focus={emptyFunction}
whenChildContentsActiveChanged={emptyFunction}
- docFilters={returnEmptyFilter}
- docRangeFilters={returnEmptyFilter}
+ childFilters={returnEmptyFilter}
+ childFiltersByRanges={returnEmptyFilter}
searchFilterDocs={returnEmptyDoclist}
- ContainingCollectionView={undefined}
- ContainingCollectionDoc={undefined}
/>
</div>
);
@@ -174,7 +172,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*/]);
}
@@ -245,67 +243,50 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu
immediate: undoBatch((source: Doc[]) => {}),
initialize: emptyFunction,
};
- _openLinkInCommand = {
- params: ['target', 'container'],
- title: 'link follow target',
- script: `{ if (self.container?.length) {
- getProto(self.target).linkContainer = self.container[0];
- getProto(self.target).isLinkButton = true;
- getProto(self.target).onClick = makeScript("getProto(self.linkContainer).data = new List([self.links[0]?.anchor2])");
- }}`,
- immediate: undoBatch((container: Doc[]) => {
- if (container.length) {
- Doc.GetProto(this.target).linkContainer = container[0];
- Doc.GetProto(this.target).isLinkButton = true;
- Doc.GetProto(this.target).onClick = ScriptField.MakeScript('getProto(self.linkContainer).data = new List([self.links[0]?.anchor2])');
- }
- }),
- initialize: emptyFunction,
- };
_viewCommand = {
params: ['target'],
title: 'bookmark view',
- script: "self.target._panX = self['target-panX']; self.target._panY = self['target-panY']; self.target._viewScale = self['target-viewScale']; gotoFrame(self.target, self['target-currentFrame']);",
+ script: "self.target._freeform_panX = self['target-freeform_panX']; self.target._freeform_panY = self['target-freeform_panY']; self.target._freeform_scale = self['target_freeform_scale']; gotoFrame(self.target, self['target-currentFrame']);",
immediate: undoBatch((source: Doc[]) => {
- this.target._panX = 0;
- this.target._panY = 0;
- this.target._viewScale = 1;
+ this.target._freeform_panX = 0;
+ this.target._freeform_panY = 0;
+ this.target._freeform_scale = 1;
this.target._currentFrame = this.target._currentFrame === undefined ? undefined : 0;
}),
initialize: (button: Doc) => {
- button['target-panX'] = this.target._panX;
- button['target-panY'] = this.target._panY;
- button['target-viewScale'] = this.target._viewScale;
+ button['target-panX'] = this.target._freeform_panX;
+ button['target-panY'] = this.target._freeform_panY;
+ button['target-_ayout_scale'] = this.target._freeform_scale;
button['target-currentFrame'] = this.target._currentFrame;
},
};
_clusterCommand = {
params: ['target'],
title: 'fit content',
- script: 'self.target._fitContentsToBox = !self.target._fitContentsToBox;',
- immediate: undoBatch((source: Doc[]) => (this.target._fitContentsToBox = !this.target._fitContentsToBox)),
+ script: 'self.target._freeform_fitContentsToBox = !self.target._freeform_fitContentsToBox;',
+ immediate: undoBatch((source: Doc[]) => (this.target._freeform_fitContentsToBox = !this.target._freeform_fitContentsToBox)),
initialize: emptyFunction,
};
_fitContentCommand = {
params: ['target'],
title: 'toggle clusters',
- script: 'self.target._useClusters = !self.target._useClusters;',
- immediate: undoBatch((source: Doc[]) => (this.target._useClusters = !this.target._useClusters)),
+ script: 'self.target._freeform_useClusters = !self.target._freeform_useClusters;',
+ immediate: undoBatch((source: Doc[]) => (this.target._freeform_useClusters = !this.target._freeform_useClusters)),
initialize: emptyFunction,
};
_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;
}
},
@@ -327,7 +308,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu
return Doc.noviceMode ? undefined : [this._templateCommand, this._narrativeCommand];
}
@computed get _doc_commands() {
- return Doc.noviceMode ? undefined : [this._openLinkInCommand, this._onClickCommand];
+ return Doc.noviceMode ? undefined : [this._onClickCommand];
}
@computed get _tree_commands() {
return undefined;
@@ -366,9 +347,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) => {
@@ -446,8 +427,9 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu
if (docDragData?.draggedDocuments.length) {
this._buttonizableCommands?.filter(c => c.title === this._currentKey).map(c => c.immediate(docDragData.draggedDocuments || []));
e.stopPropagation();
+ return true;
}
- return true;
+ return false;
}
dragViewDown = (e: React.PointerEvent) => {
@@ -459,8 +441,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);
@@ -567,9 +549,8 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu
@undoBatch
@action
startRecording = () => {
- const doc = Docs.Create.ScreenshotDocument({ title: 'screen recording', _fitWidth: true, _width: 400, _height: 200, mediaState: 'pendingRecording' });
- //Doc.AddDocToList(Doc.MyOverlayDocs, undefined, doc);
- CollectionDockingView.AddSplit(doc, 'right');
+ const doc = Docs.Create.ScreenshotDocument({ title: 'screen recording', _layout_fitWidth: true, _width: 400, _height: 200, mediaState: 'pendingRecording' });
+ CollectionDockingView.AddSplit(doc, OpenWhereMod.right);
};
@computed
@@ -587,29 +568,29 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu
}
@undoBatch
- onAlias = () => {
+ onEmbed = () => {
if (this.selectedDoc && this.selectedDocumentView) {
// const copy = Doc.MakeCopy(this.selectedDocumentView.props.Document, true);
// copy.x = NumCast(this.selectedDoc.x) + NumCast(this.selectedDoc._width);
// copy.y = NumCast(this.selectedDoc.y) + 30;
// this.selectedDocumentView.props.addDocument?.(copy);
- const alias = Doc.MakeAlias(this.selectedDoc);
- alias.x = NumCast(this.selectedDoc.x) + NumCast(this.selectedDoc._width);
- alias.y = NumCast(this.selectedDoc.y) + 30;
- this.selectedDocumentView.props.addDocument?.(alias);
+ const embedding = Doc.MakeEmbedding(this.selectedDoc);
+ embedding.x = NumCast(this.selectedDoc.x) + NumCast(this.selectedDoc._width);
+ embedding.y = NumCast(this.selectedDoc.y) + 30;
+ this.selectedDocumentView.props.addDocument?.(embedding);
}
};
- onAliasButtonDown = (e: React.PointerEvent): void => {
- setupMoveUpEvents(this, e, this.onAliasButtonMoved, emptyFunction, emptyFunction);
+ onEmbedButtonDown = (e: React.PointerEvent): void => {
+ setupMoveUpEvents(this, e, this.onEmbedButtonMoved, emptyFunction, emptyFunction);
};
@undoBatch
- onAliasButtonMoved = (e: PointerEvent) => {
+ onEmbedButtonMoved = (e: PointerEvent) => {
const contentDiv = this.selectedDocumentView?.ContentDiv;
if (contentDiv && this.selectedDoc) {
const dragData = new DragManager.DocumentDragData([this.selectedDoc]);
const offset = [e.clientX - contentDiv.getBoundingClientRect().x, e.clientY - contentDiv.getBoundingClientRect().y];
- dragData.defaultDropAction = 'alias';
+ dragData.defaultDropAction = 'embed';
dragData.canEmbed = true;
DragManager.StartDocumentDrag([contentDiv], dragData, e.clientX, e.clientY, {
offsetX: offset[0],
@@ -622,11 +603,11 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu
};
@computed
- get aliasButton() {
+ get embedButton() {
const targetDoc = this.selectedDoc;
return !targetDoc || targetDoc.type === DocumentType.PRES ? null : (
- <Tooltip title={<div className="dash-tooltip">{'Tap or Drag to create an alias'}</div>} placement="top">
- <button className="antimodeMenu-button" onPointerDown={this.onAliasButtonDown} onClick={this.onAlias} style={{ cursor: 'drag' }}>
+ <Tooltip title={<div className="dash-tooltip">{'Tap or Drag to create an embedding'}</div>} placement="top">
+ <button className="antimodeMenu-button" onPointerDown={this.onEmbedButtonDown} onClick={this.onEmbed} style={{ cursor: 'drag' }}>
<FontAwesomeIcon className="colMenu-icon" icon="copy" size="lg" />
</button>
</Tooltip>
@@ -658,8 +639,8 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu
key="float"
style={{
backgroundColor: this.props.docView.layoutDoc.z ? '121212' : undefined,
- pointerEvents: this.props.docView.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Freeform ? 'none' : undefined,
- color: this.props.docView.props.ContainingCollectionDoc?._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'} />
@@ -676,7 +657,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu
<div className="collectionViewBaseChrome">
{this.notACollection || this.props.type === CollectionViewType.Invalid ? null : this.viewModes}
<div className="collectionMenu-divider" key="divider1"></div>
- {this.aliasButton}
+ {this.embedButton}
{/* {this.pinButton} */}
{this.toggleOverlayButton}
<div className="collectionMenu-divider" key="divider2"></div>
@@ -710,7 +691,7 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionView
return this.props.docView.props.Document;
}
@computed get dataField() {
- return this.document[this.props.docView.LayoutFieldKey + (this.props.isOverlay ? '-annotations' : '')];
+ return this.document[this.props.docView.LayoutFieldKey + (this.props.isOverlay ? '_annotations' : '')];
}
@computed get childDocs() {
return DocListCast(this.dataField);
@@ -733,11 +714,12 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionView
doc._currentFrame = 0;
CollectionFreeFormDocumentView.setupKeyframes(childDocs, 0);
}
- CollectionFreeFormDocumentView.updateKeyframe(childDocs, currentFrame || 0);
+ CollectionFreeFormView.updateKeyframe(undefined, [...childDocs, doc], currentFrame || 0);
doc._currentFrame = newFrame === undefined ? 0 : Math.max(0, newFrame);
}
}
+ _keyTimer: NodeJS.Timeout | undefined;
@undoBatch
@action
nextKeyframe = (): void => {
@@ -746,7 +728,7 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionView
this.document._currentFrame = 0;
CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0);
}
- CollectionFreeFormDocumentView.updateKeyframe(this.childDocs, currentFrame || 0);
+ this._keyTimer = CollectionFreeFormView.updateKeyframe(this._keyTimer, [...this.childDocs, this.document], currentFrame || 0);
this.document._currentFrame = Math.max(0, (currentFrame || 0) + 1);
this.document.lastFrame = Math.max(NumCast(this.document._currentFrame), NumCast(this.document.lastFrame));
};
@@ -758,7 +740,7 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionView
this.document._currentFrame = 0;
CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0);
}
- CollectionFreeFormDocumentView.gotoKeyframe(this.childDocs.slice());
+ this._keyTimer = CollectionFreeFormView.gotoKeyframe(this._keyTimer, [...this.childDocs, this.document], 1000);
this.document._currentFrame = Math.max(0, (currentFrame || 0) - 1);
};
@@ -816,7 +798,7 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionView
if (doc.type === DocumentType.INK) {
switch (field) {
case 'width':
- doc.strokeWidth = Number(value);
+ doc.stroke_width = Number(value);
break;
case 'color':
doc.color = String(value);
@@ -825,7 +807,7 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionView
doc.fillColor = String(value);
break;
case 'dash':
- doc.strokeDash = value;
+ doc.stroke_dash = value;
}
}
})
@@ -1023,16 +1005,13 @@ 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('lastModified') >= 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('lastModified') >= 0 || (key[0]?.toUpperCase() === key[0] && key[0] !== '_')
- );
- return noviceKeys.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('author_date') >= 0 || key.indexOf('modificationDate') >= 0 || (key[0]?.toUpperCase() === key[0] && key[0] !== '_'));
+ return noviceKeys.filter(key => key.toLowerCase().indexOf(val) > -1);
}
if (docs instanceof Doc) {
@@ -1143,13 +1122,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('lastModified') >= 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('lastModified') >= 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);
}
@@ -1253,9 +1232,9 @@ export class CollectionSchemaViewChrome extends React.Component<CollectionViewMe
const dividerWidth = 4;
const borderWidth = Number(COLLECTION_BORDER_WIDTH);
const panelWidth = this.props.docView.props.PanelWidth();
- const previewWidth = NumCast(this.document.schemaPreviewWidth);
+ const previewWidth = NumCast(this.document.schema_previewWidth);
const tableWidth = panelWidth - 2 * borderWidth - dividerWidth - previewWidth;
- this.document.schemaPreviewWidth = previewWidth === 0 ? Math.min(tableWidth / 3, 200) : 0;
+ this.document.schema_previewWidth = previewWidth === 0 ? Math.min(tableWidth / 3, 200) : 0;
};
@undoBatch
@@ -1272,7 +1251,7 @@ export class CollectionSchemaViewChrome extends React.Component<CollectionViewMe
};
render() {
- const previewWidth = NumCast(this.document.schemaPreviewWidth);
+ const previewWidth = NumCast(this.document.schema_previewWidth);
const textWrapped = Cast(this.document.textwrappedSchemaRows, listSpec('string'), []).length > 0;
return (
@@ -1347,7 +1326,7 @@ export class Collection3DCarouselViewChrome extends React.Component<CollectionVi
return (
<div className="collection3DCarouselViewChrome-cont">
<div className="collection3DCarouselViewChrome-scrollSpeed-cont">
- {FormattedTextBox.Focused ? <RichTextMenu /> : null}
+ {/* {FormattedTextBox.Focused ? <RichTextMenu /> : null} */}
<div className="collectionStackingViewChrome-scrollSpeed-label">AUTOSCROLL SPEED:</div>
<div className="collection3DCarouselViewChrome-scrollSpeed">
<EditableView GetValue={() => StrCast(this.scrollSpeed)} oneLine SetValue={this.setValue} contents={this.scrollSpeed ? this.scrollSpeed : 1000} />
diff --git a/src/client/views/collections/CollectionNoteTakingView.scss b/src/client/views/collections/CollectionNoteTakingView.scss
index 4d1f18e54..be1800d81 100644
--- a/src/client/views/collections/CollectionNoteTakingView.scss
+++ b/src/client/views/collections/CollectionNoteTakingView.scss
@@ -51,10 +51,6 @@
display: flex;
}
-.collectionNoteTakingViewFieldColumn {
- display: flex;
- overflow: auto;
-}
.collectionNoteTakingViewFieldColumn:hover {
.collectionNoteTakingView-DocumentButtons {
display: flex;
@@ -98,6 +94,11 @@
position: relative;
display: block;
}
+ .collectionNoteTakingViewFieldColumn {
+ overflow: auto;
+ display: flex;
+ flex-direction: column;
+ }
.collectionSchemaView-previewDoc {
height: 100%;
@@ -111,14 +112,19 @@
height: auto;
}
+ .collectionNoteTakingView-columnStackContainer {
+ height: 100%;
+ overflow: auto;
+ width: 100%;
+ display: flex;
+ }
.collectionNoteTakingView-columnStack {
height: 100%;
width: 100%;
- display: inline-block;
+ display: table-cell;
}
.collectionNoteTakingView-Nodes {
width: 100%;
- height: 100%;
position: absolute;
display: flex;
flex-direction: column;
@@ -128,7 +134,6 @@
width: 100%;
position: absolute;
margin: auto;
- width: max-content;
height: max-content;
position: relative;
grid-auto-rows: 0px;
@@ -165,6 +170,7 @@
// Documents in NoteTaking view
.collectionNoteTakingView-columnDoc {
display: flex;
+ width: 100%;
// margin: auto; // Removed auto so that it is no longer center aligned - this could be something we change
position: relative;
diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx
index b0f64ed60..f1c4c2cdf 100644
--- a/src/client/views/collections/CollectionNoteTakingView.tsx
+++ b/src/client/views/collections/CollectionNoteTakingView.tsx
@@ -2,16 +2,16 @@ 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 { Copy, Id } from '../../../fields/FieldSymbols';
+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 { DocumentType } from '../../documents/DocumentTypes';
import { DragManager, dropActionType } from '../../util/DragManager';
import { SnappingManager } from '../../util/SnappingManager';
import { Transform } from '../../util/Transform';
@@ -19,7 +19,7 @@ import { undoBatch } from '../../util/UndoManager';
import { ContextMenu } from '../ContextMenu';
import { ContextMenuProps } from '../ContextMenuItem';
import { LightboxView } from '../LightboxView';
-import { DocFocusOptions, DocumentView, DocumentViewProps, ViewAdjustment } from '../nodes/DocumentView';
+import { DocFocusOptions, DocumentView, DocumentViewProps } from '../nodes/DocumentView';
import { FieldViewProps } from '../nodes/FieldView';
import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
import { StyleProp } from '../StyleProvider';
@@ -50,20 +50,20 @@ export class CollectionNoteTakingView extends CollectionSubView() {
return BoolCast(this.layoutDoc.chromeHidden);
}
// columnHeaders returns the list of SchemaHeaderFields currently being used by the layout doc to render the columns
- @computed get columnHeaders() {
- const columnHeaders = Cast(this.dataDoc.columnHeaders, listSpec(SchemaHeaderField), null);
- const needsUnsetCategory = this.childDocs.some(d => !d[this.notetakingCategoryField] && !columnHeaders?.find(sh => sh.heading === 'unset'));
- if (needsUnsetCategory || columnHeaders === undefined || columnHeaders.length === 0) {
+ @computed get colHeaderData() {
+ const colHeaderData = Cast(this.dataDoc[this.fieldKey + '_columnHeaders'], listSpec(SchemaHeaderField), null);
+ const needsUnsetCategory = this.childDocs.some(d => !d[this.notetakingCategoryField] && !colHeaderData?.find(sh => sh.heading === 'unset'));
+ if (needsUnsetCategory || colHeaderData === undefined || colHeaderData.length === 0) {
setTimeout(() => {
- const columnHeaders = Cast(this.dataDoc.columnHeaders, listSpec(SchemaHeaderField), null);
+ const columnHeaders = Array.from(Cast(this.dataDoc[this.fieldKey + '_columnHeaders'], listSpec(SchemaHeaderField), null) ?? []);
const needsUnsetCategory = this.childDocs.some(d => !d[this.notetakingCategoryField] && !columnHeaders?.find(sh => sh.heading === 'unset'));
- if (needsUnsetCategory || columnHeaders === undefined || columnHeaders.length === 0) {
- if (columnHeaders) columnHeaders.push(new SchemaHeaderField('unset', undefined, undefined, 1));
- else this.dataDoc.columnHeaders = new List<SchemaHeaderField>();
+ if (needsUnsetCategory || columnHeaders.length === 0) {
+ columnHeaders.push(new SchemaHeaderField('unset', undefined, undefined, 1));
+ this.resizeColumns(columnHeaders);
}
});
}
- return columnHeaders ?? ([] as SchemaHeaderField[]);
+ return colHeaderData ?? ([] as SchemaHeaderField[]);
}
@computed get headerMargin() {
return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.HeaderMargin);
@@ -79,7 +79,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
}
// numGroupColumns returns the number of columns
@computed get numGroupColumns() {
- return this.columnHeaders.length;
+ return this.colHeaderData.length;
}
// PanelWidth returns the size of the total available space the view occupies
@computed get PanelWidth() {
@@ -118,7 +118,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
// (2) documentView gets unmounted as you remove it from the list
@computed get Sections() {
TraceMobx();
- const columnHeaders = this.columnHeaders;
+ const columnHeaders = this.colHeaderData;
// filter out the currently dragged docs from the child docs, since we will insert them later
const docs = this.childDocs.filter(d => !DragManager.docsBeingDragged.includes(d));
const sections = new Map<SchemaHeaderField, Doc[]>(columnHeaders.map(sh => [sh, []] as [SchemaHeaderField, []]));
@@ -150,9 +150,10 @@ export class CollectionNoteTakingView extends CollectionSubView() {
componentDidMount() {
super.componentDidMount?.();
document.addEventListener('pointerup', this.removeDocDragHighlight, true);
- this._disposers.autoHeight = reaction(
- () => this.layoutDoc._autoHeight,
- autoHeight => autoHeight && this.props.setHeight?.(Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), this.headerMargin + Math.max(...this.refList.map(r => Number(getComputedStyle(r).height.replace('px', ''))))))
+ this._disposers.layout_autoHeight = reaction(
+ () => this.layoutDoc._layout_autoHeight,
+ layout_autoHeight =>
+ layout_autoHeight && this.props.setHeight?.(Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), this.headerMargin + Math.max(...this.refList.map(r => Number(getComputedStyle(r).height.replace('px', ''))))))
);
}
@@ -163,7 +164,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
}
@action
- moveDocument = (doc: Doc, targetCollection: Doc | undefined, addDocument: (document: Doc) => boolean): boolean => {
+ moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[], annotationKey?: string) => boolean, annotationKey?: string): boolean => {
return this.props.removeDocument?.(doc) && addDocument?.(doc) ? true : false;
};
@@ -180,55 +181,47 @@ export class CollectionNoteTakingView extends CollectionSubView() {
return () => this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick);
}
- addDocTab = (doc: Doc, where: string) => {
- if (where === 'inPlace' && this.layoutDoc.isInPlaceContainer) {
- this.dataDoc[this.props.fieldKey] = new List<Doc>([doc]);
- return true;
- }
- return this.props.addDocTab(doc, where);
- };
-
scrollToBottom = () => {
- smoothScroll(500, this._mainCont!, this._mainCont!.scrollHeight);
+ smoothScroll(500, this._mainCont!, this._mainCont!.scrollHeight, 'ease');
};
// let's dive in and get the actual document we want to drag/move around
focusDocument = (doc: Doc, options: DocFocusOptions) => {
Doc.BrushDoc(doc);
- let focusSpeed = 0;
const found = this._mainCont && Array.from(this._mainCont.getElementsByClassName('documentView-node')).find((node: any) => node.id === doc[Id]);
if (found) {
const top = found.getBoundingClientRect().top;
const localTop = this.props.ScreenToLocalTransform().transformPoint(0, top);
if (Math.floor(localTop[1]) !== 0) {
- smoothScroll((focusSpeed = NumCast(doc.focusSpeed, 500)), this._mainCont!, localTop[1] + this._mainCont!.scrollTop);
+ let focusSpeed = options.zoomTime ?? 500;
+ smoothScroll(focusSpeed, this._mainCont!, localTop[1] + this._mainCont!.scrollTop, options.easeFunc);
+ return focusSpeed;
}
}
- const endFocus = async (moved: boolean) => (options?.afterFocus ? options?.afterFocus(moved) : ViewAdjustment.doNothing);
- this.props.focus(this.rootDoc, {
- willZoom: options?.willZoom,
- scale: options?.scale,
- afterFocus: (didFocus: boolean) => new Promise<ViewAdjustment>(res => setTimeout(async () => res(await endFocus(didFocus)), focusSpeed)),
- });
};
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);
};
isContentActive = () => this.props.isSelected() || this.props.isContentActive();
+ blockPointerEventsWhenDragging = () => (this.docsDraggedRowCol.length ? 'none' : undefined);
// getDisplayDoc returns the rules for displaying a document in this view (ie. DocumentView)
getDisplayDoc(doc: Doc, width: () => number) {
- const dataDoc = !doc.isTemplateDoc && !doc.isTemplateForField && !doc.PARAMS ? undefined : this.props.DataDoc;
+ const dataDoc = !doc.isTemplateDoc && !doc.isTemplateForField ? undefined : this.props.DataDoc;
const height = () => this.getDocHeight(doc);
let dref: Opt<DocumentView>;
const noteTakingDocTransform = () => this.getDocTransform(doc, dref);
@@ -236,13 +229,14 @@ export class CollectionNoteTakingView extends CollectionSubView() {
<DocumentView
ref={r => (dref = r || undefined)}
Document={doc}
- DataDoc={dataDoc || (!Doc.AreProtosEqual(doc[DataSym], doc) && doc[DataSym])}
+ pointerEvents={this.blockPointerEventsWhenDragging}
+ DataDoc={dataDoc ?? (!Doc.AreProtosEqual(doc[DocData], doc) ? doc[DocData] : undefined)}
renderDepth={this.props.renderDepth + 1}
PanelWidth={width}
PanelHeight={height}
styleProvider={this.styleProvider}
docViewPath={this.props.docViewPath}
- fitWidth={this.props.childFitWidth}
+ layout_fitWidth={this.props.childLayoutFitWidth}
isContentActive={emptyFunction}
onKey={this.onKeyDown}
//TODO: change this from a prop to a parameter passed into a function
@@ -250,31 +244,29 @@ export class CollectionNoteTakingView extends CollectionSubView() {
isDocumentActive={this.isContentActive}
LayoutTemplate={this.props.childLayoutTemplate}
LayoutTemplateString={this.props.childLayoutString}
- NativeWidth={this.props.childIgnoreNativeSize ? returnZero : this.props.childFitWidth?.(doc) || (doc._fitWidth && !Doc.NativeWidth(doc)) ? width : undefined} // explicitly ignore nativeWidth/height if childIgnoreNativeSize is set- used by PresBox
- NativeHeight={this.props.childIgnoreNativeSize ? returnZero : this.props.childFitWidth?.(doc) || (doc._fitWidth && !Doc.NativeHeight(doc)) ? height : undefined}
+ NativeWidth={this.props.childIgnoreNativeSize ? returnZero : this.props.childLayoutFitWidth?.(doc) || (doc._layout_fitWidth && !Doc.NativeWidth(doc)) ? width : undefined} // explicitly ignore nativeWidth/height if childIgnoreNativeSize is set- used by PresBox
+ NativeHeight={this.props.childIgnoreNativeSize ? returnZero : this.props.childLayoutFitWidth?.(doc) || (doc._layout_fitWidth && !Doc.NativeHeight(doc)) ? height : undefined}
dontCenter={this.props.childIgnoreNativeSize ? 'xy' : undefined}
dontRegisterView={dataDoc ? true : BoolCast(this.layoutDoc.childDontRegisterViews, this.props.dontRegisterView)}
rootSelected={this.rootSelected}
- showTitle={this.props.childShowTitle}
- dropAction={StrCast(this.layoutDoc.childDropAction) as dropActionType}
+ layout_showTitle={this.props.childlayout_showTitle}
+ dragAction={StrCast(this.layoutDoc.childDragAction) as dropActionType}
onClick={this.onChildClickHandler}
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}
- ContainingCollectionDoc={this.props.CollectionView?.props.Document}
- ContainingCollectionView={this.props.CollectionView}
addDocument={this.props.addDocument}
moveDocument={this.props.moveDocument}
removeDocument={this.props.removeDocument}
contentPointerEvents={StrCast(this.layoutDoc.contentPointerEvents)}
whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged}
- addDocTab={this.addDocTab}
+ addDocTab={this.props.addDocTab}
bringToFront={returnFalse}
scriptContext={this.props.scriptContext}
pinToPres={this.props.pinToPres}
@@ -285,7 +277,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
// getDocTransform is used to get the coordinates of a document when we go from a view like freeform to columns
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);
+ const { translateX, translateY } = Utils.GetScreenTransform(dref?.ContentDiv || undefined);
// 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);
}
@@ -294,10 +286,10 @@ export class CollectionNoteTakingView extends CollectionSubView() {
// if a note doc. Otherwise, returns the normal width (for graphs, images, etc...)
getDocWidth(d: Doc) {
const heading = !d[this.notetakingCategoryField] ? 'unset' : Field.toString(d[this.notetakingCategoryField] as Field);
- const existingHeader = this.columnHeaders.find(sh => sh.heading === heading);
+ 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.fitWidth ? maxWidth : d[WidthSym]();
+ const width = d.layout_fitWidth ? maxWidth : d[Width]();
return Math.min(maxWidth - CollectionNoteTakingViewColumn.ColumnMargin, width < maxWidth ? width : maxWidth);
}
@@ -305,16 +297,16 @@ export class CollectionNoteTakingView extends CollectionSubView() {
getDocHeight(d?: Doc) {
if (!d || d.hidden) return 0;
const childLayoutDoc = Doc.Layout(d, this.props.childLayoutTemplate?.());
- const childDataDoc = !d.isTemplateDoc && !d.isTemplateForField && !d.PARAMS ? undefined : this.props.DataDoc;
+ 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._fitWidth || this.props.childFitWidth?.(d)) ? d[WidthSym]() : 0);
- const nh = Doc.NativeHeight(childLayoutDoc, childDataDoc) || (!(childLayoutDoc._fitWidth || this.props.childFitWidth?.(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);
}
const childHeight = NumCast(childLayoutDoc._height);
- const panelHeight = childLayoutDoc._fitWidth || this.props.childFitWidth?.(d) ? Number.MAX_SAFE_INTEGER : this.props.PanelHeight() - 2 * this.yMargin;
+ const panelHeight = childLayoutDoc._layout_fitWidth || this.props.childLayoutFitWidth?.(d) ? Number.MAX_SAFE_INTEGER : this.props.PanelHeight() - 2 * this.yMargin;
return Math.min(childHeight, maxHeight, panelHeight);
}
@@ -328,18 +320,16 @@ export class CollectionNoteTakingView extends CollectionSubView() {
// Removing example: column widths are [0.5, 0.30, 0.20] --> user deletes the final column --> column widths are [0.625, 0.375].
// Adding example: column widths are [0.6, 0.4] --> user adds column at end --> column widths are [0.4, 0.267, 0.33]
@action
- resizeColumns = (isAdd: boolean, colWidth: number, colIndex: number) => {
- const n = this.columnHeaders.length;
- if (n == 1) {
- this.columnHeaders[0].setWidth(1);
- return true;
- }
- const scaleFactor = isAdd ? 1 - colWidth : 1 / (1 - colWidth);
- this.columnHeaders.forEach((h, i) => {
- if (!(isAdd && i == colIndex)) {
- h.width < 0 ? h.setWidth(1 / n) : h.setWidth(h.width * scaleFactor);
- }
- });
+ resizeColumns = (headers: SchemaHeaderField[]) => {
+ const n = headers.length;
+ const curWidths = headers.reduce((sum, hdr) => sum + Math.abs(hdr.width), 0);
+ const scaleFactor = 1 / curWidths;
+ this.dataDoc[this.fieldKey + '_columnHeaders'] = new List<SchemaHeaderField>(
+ headers.map(h => {
+ h.setWidth(Math.abs(h.width) * scaleFactor);
+ return h;
+ })
+ );
return true;
};
@@ -368,7 +358,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
});
// we alter the pivot fields of the docs in case they are moved to a new column.
const colIndex = this.getColumnFromXCoord(xCoord);
- const colHeader = colIndex === undefined ? 'unset' : StrCast(this.columnHeaders[colIndex].heading);
+ const colHeader = colIndex === undefined ? 'unset' : StrCast(this.colHeaderData[colIndex].heading);
DragManager.docsBeingDragged.forEach(d => (d[this.notetakingCategoryField] = colHeader));
// used to notify sections to re-render
this.docsDraggedRowCol.length = 0;
@@ -381,12 +371,12 @@ export class CollectionNoteTakingView extends CollectionSubView() {
// This function is used to know which document a column SHOULD be in while it is being dragged.
getColumnFromXCoord = (xCoord: number): number | undefined => {
let colIndex: number | undefined = undefined;
- const numColumns = this.columnHeaders.length;
+ const numColumns = this.colHeaderData.length;
const coords = [];
let colStartXCoord = 0;
for (let i = 0; i < numColumns; i++) {
coords.push(colStartXCoord);
- colStartXCoord += this.columnHeaders[i].width * this.availableWidth + this.DividerWidth;
+ colStartXCoord += this.colHeaderData[i].width * this.availableWidth + this.DividerWidth;
}
coords.push(this.PanelWidth);
for (let i = 0; i < numColumns; i++) {
@@ -402,7 +392,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
getDocsFromXCoord = (xCoord: number): Doc[] => {
const docsMatchingHeader: Doc[] = [];
const colIndex = this.getColumnFromXCoord(xCoord);
- const colHeader = colIndex === undefined ? 'unset' : StrCast(this.columnHeaders[colIndex].heading);
+ const colHeader = colIndex === undefined ? 'unset' : StrCast(this.colHeaderData[colIndex].heading);
this.childDocs?.map(d => {
if (d instanceof Promise) return;
const sectionValue = (d[this.notetakingCategoryField] as object) ?? 'unset';
@@ -417,7 +407,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
@action
onKeyDown = (e: React.KeyboardEvent, fieldProps: FieldViewProps) => {
const docView = fieldProps.DocumentView?.();
- if (docView && (e.ctrlKey || docView.rootDoc._singleLine) && ['Enter'].includes(e.key)) {
+ if (docView && (e.ctrlKey || docView.rootDoc._createDocOnCR) && ['Enter'].includes(e.key)) {
e.stopPropagation?.();
const newDoc = Doc.MakeCopy(docView.rootDoc, true);
Doc.GetProto(newDoc).text = undefined;
@@ -451,23 +441,24 @@ export class CollectionNoteTakingView extends CollectionSubView() {
docs.splice(previousDocIndex + 1, 0, ...newDocs);
}
}
+ return true;
}
- } else if (de.complete.linkDragData?.dragDocument.context === this.props.Document && de.complete.linkDragData?.linkDragView?.props.CollectionFreeFormDocumentView?.()) {
- const source = Docs.Create.TextDocument('', { _width: 200, _height: 75, _fitWidth: true, title: 'dropped annotation' });
- this.props.addDocument?.(source);
- de.complete.linkDocument = DocUtils.MakeLink({ doc: source }, { doc: de.complete.linkDragData.linkSourceGetAnchor() }, 'doc annotation', ''); // TODODO this is where in text links get passed
+ } 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' });
+ 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;
}
@@ -484,7 +475,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
this.onPointerMove(true, e.clientX, e.clientY);
docus?.map((doc: Doc) => this.addDocument(doc));
const newDoc = this.childDocs.lastElement();
- const colHeader = colInd === undefined ? 'unset' : StrCast(this.columnHeaders[colInd].heading);
+ const colHeader = colInd === undefined ? 'unset' : StrCast(this.colHeaderData[colInd].heading);
newDoc[this.notetakingCategoryField] = colHeader;
const docs = this.childDocList;
if (docs && targInd !== -1) {
@@ -518,7 +509,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
this.refList.push(ref);
this.observer = new _global.ResizeObserver(
action((entries: any) => {
- if (this.layoutDoc._autoHeight && ref && this.refList.length && !SnappingManager.GetIsDragging()) {
+ if (this.layoutDoc._layout_autoHeight && ref && this.refList.length && !SnappingManager.GetIsDragging()) {
const height = this.headerMargin + Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), Math.max(...this.refList.map(r => Number(getComputedStyle(r).height.replace('px', '')))));
if (!LightboxView.IsLightboxDocView(this.props.docViewPath())) {
this.props.setHeight?.(height);
@@ -529,9 +520,10 @@ export class CollectionNoteTakingView extends CollectionSubView() {
this.observer.observe(ref);
}
}}
+ select={this.props.select}
addDocument={this.addDocument}
chromeHidden={this.chromeHidden}
- columnHeaders={this.columnHeaders}
+ colHeaderData={this.colHeaderData}
Document={this.props.Document}
DataDoc={this.props.DataDoc}
resizeColumns={this.resizeColumns}
@@ -539,10 +531,10 @@ export class CollectionNoteTakingView extends CollectionSubView() {
numGroupColumns={this.numGroupColumns}
gridGap={this.gridGap}
pivotField={this.notetakingCategoryField}
+ fieldKey={this.fieldKey}
dividerWidth={this.DividerWidth}
maxColWidth={this.maxColWidth}
availableWidth={this.availableWidth}
- PanelWidth={this.PanelWidth}
key={heading?.heading ?? 'unset'}
headings={this.headings}
heading={heading?.heading ?? 'unset'}
@@ -562,18 +554,18 @@ export class CollectionNoteTakingView extends CollectionSubView() {
@undoBatch
@action
addGroup = (value: string) => {
- if (this.columnHeaders) {
- for (const header of this.columnHeaders) {
- if (header.heading == value) {
+ if (this.colHeaderData) {
+ for (const header of this.colHeaderData) {
+ if (header.heading === value) {
alert('You cannot use an existing column name. Please try a new column name');
return value;
}
}
}
- const columnHeaders = Cast(this.props.Document.columnHeaders, listSpec(SchemaHeaderField), null);
+ const columnHeaders = Array.from(Cast(this.dataDoc[this.fieldKey + '_columnHeaders'], listSpec(SchemaHeaderField), null));
const newColWidth = 1 / (this.numGroupColumns + 1);
- value && columnHeaders?.push(new SchemaHeaderField(value, undefined, undefined, newColWidth)) && this.resizeColumns(true, newColWidth, this.columnHeaders.length - 1);
- this.dataDoc.columnHeaders = new List<SchemaHeaderField>(columnHeaders.map(header => header[Copy]()));
+ columnHeaders.push(new SchemaHeaderField(value, undefined, undefined, newColWidth));
+ value && this.resizeColumns(columnHeaders);
return true;
};
@@ -582,7 +574,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
if (!e.isPropagationStopped()) {
const subItems: ContextMenuProps[] = [];
subItems.push({ description: `${this.layoutDoc._columnsFill ? 'Variable Size' : 'Autosize'} Column`, event: () => (this.layoutDoc._columnsFill = !this.layoutDoc._columnsFill), icon: 'plus' });
- subItems.push({ description: `${this.layoutDoc._autoHeight ? 'Variable Height' : 'Auto Height'}`, event: () => (this.layoutDoc._autoHeight = !this.layoutDoc._autoHeight), icon: 'plus' });
+ subItems.push({ description: `${this.layoutDoc._layout_autoHeight ? 'Variable Height' : 'Auto Height'}`, event: () => (this.layoutDoc._layout_autoHeight = !this.layoutDoc._layout_autoHeight), icon: 'plus' });
subItems.push({ description: 'Clear All', event: () => (this.dataDoc.data = new List([])), icon: 'times' });
ContextMenu.Instance.addItem({ description: 'Options...', subitems: subItems, icon: 'eye' });
}
@@ -592,8 +584,8 @@ export class CollectionNoteTakingView extends CollectionSubView() {
@action
setColumnStartXCoords = (movementXScreen: number, colIndex: number) => {
const movementX = this.props.ScreenToLocalTransform().transformDirection(movementXScreen, 0)[0];
- const leftHeader = this.columnHeaders[colIndex];
- const rightHeader = this.columnHeaders[colIndex + 1];
+ const leftHeader = this.colHeaderData[colIndex];
+ const rightHeader = this.colHeaderData[colIndex + 1];
leftHeader.setWidth(leftHeader.width + movementX / this.availableWidth);
rightHeader.setWidth(rightHeader.width - movementX / this.availableWidth);
};
@@ -616,44 +608,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}
- ContainingCollectionView={undefined}
- ContainingCollectionDoc={undefined}
- />
- </div>
- );
- }
- }
-
@computed get nativeWidth() {
return Doc.NativeWidth(this.layoutDoc);
}
@@ -666,42 +620,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 84d1c0205..3286d60bd 100644
--- a/src/client/views/collections/CollectionNoteTakingViewColumn.tsx
+++ b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx
@@ -29,14 +29,16 @@ interface CSVFieldColumnProps {
docList: Doc[];
heading: string;
pivotField: string;
+ fieldKey: string | undefined;
chromeHidden?: boolean;
- columnHeaders: SchemaHeaderField[] | undefined;
+ colHeaderData: SchemaHeaderField[] | undefined;
headingObject: SchemaHeaderField | undefined;
yMargin: number;
numGroupColumns: number;
gridGap: number;
type: 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function' | undefined;
headings: () => object[];
+ select: (ctrlPressed: boolean) => void;
renderChildren: (docs: Doc[]) => JSX.Element[];
addDocument: (doc: Doc | Doc[]) => boolean;
createDropTarget: (ele: HTMLDivElement) => void;
@@ -44,8 +46,7 @@ interface CSVFieldColumnProps {
observeHeight: (myref: any) => void;
unobserveHeight: (myref: any) => void;
editableViewProps: () => any;
- resizeColumns: (isAdd: boolean, colWidth: number, colIndex: number) => boolean;
- PanelWidth: number;
+ resizeColumns: (headers: SchemaHeaderField[]) => boolean;
maxColWidth: number;
dividerWidth: number;
availableWidth: number;
@@ -61,9 +62,9 @@ export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColu
// columnWidth returns the width of a column in absolute pixels
@computed get columnWidth() {
- if (!this.props.columnHeaders || !this.props.headingObject || this.props.columnHeaders.length === 1) return '100%';
- const i = this.props.columnHeaders.indexOf(this.props.headingObject);
- return this.props.columnHeaders[i].width * 100 + '%';
+ if (!this.props.colHeaderData || !this.props.headingObject || this.props.colHeaderData.length === 1) return '100%';
+ const i = this.props.colHeaderData.indexOf(this.props.headingObject);
+ return this.props.colHeaderData[i].width * 100 + '%';
}
private dropDisposer?: DragManager.DragDropDisposer;
@@ -91,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 => {
@@ -105,7 +107,7 @@ export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColu
headingChanged = (value: string, shiftDown?: boolean) => {
const castedValue = this.getValue(value);
if (castedValue) {
- if (this.props.columnHeaders?.map(i => i.heading).indexOf(castedValue.toString()) !== -1) {
+ if (this.props.colHeaderData?.map(i => i.heading).indexOf(castedValue.toString()) !== -1) {
return false;
}
this.props.docList.forEach(d => (d[this.props.pivotField] = castedValue));
@@ -120,14 +122,15 @@ 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
addNewTextDoc = (value: string, shiftDown?: boolean, forceEmptyNote?: boolean) => {
if (!value && !forceEmptyNote) return false;
const key = this.props.pivotField;
- const newDoc = Docs.Create.TextDocument(value, { _height: 18, _width: 200, _fitWidth: true, title: value, _autoHeight: true });
+ const newDoc = Docs.Create.TextDocument(value, { _height: 18, _width: 200, _layout_fitWidth: true, title: value, _layout_autoHeight: true });
const colValue = this.getValue(this.props.heading);
newDoc[key] = colValue;
FormattedTextBox.SelectOnLoad = newDoc[Id];
@@ -141,18 +144,11 @@ export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColu
@undoBatch
@action
deleteColumn = () => {
- const acolumnHeaders = Cast(this.props.Document.columnHeaders, listSpec(SchemaHeaderField), null);
- if (acolumnHeaders && this.props.headingObject) {
- const index = acolumnHeaders.indexOf(this.props.headingObject);
- const columnHeaders = acolumnHeaders; // new List<SchemaHeaderField>(acolumnHeaders.map(header => header[Copy]())); // needed for undo to work properly. otherwise we end up changing field values in the undo stack since they are shared by reference
- const newColIndex = index > 0 ? index - 1 : 1;
- const newColHeader = this.props.columnHeaders ? this.props.columnHeaders[newColIndex] : undefined;
- const newHeading = newColHeader ? newColHeader.heading : 'unset';
- this.props.docList.forEach(d => (d[this.props.pivotField] = newHeading));
- const colWidth = this.props.columnHeaders ? this.props.columnHeaders[index].width : 0;
- columnHeaders.splice(index, 1);
- //Doc.GetProto(this.props.Document).columnHeaders = columnHeaders;
- this.props.resizeColumns(false, colWidth, index);
+ const colHdrData = Array.from(Cast(this.props.Document[this.props.fieldKey + '_columnHeaders'], listSpec(SchemaHeaderField), null));
+ if (this.props.headingObject) {
+ this.props.docList.forEach(d => (d[this.props.pivotField] = undefined));
+ colHdrData.splice(colHdrData.indexOf(this.props.headingObject), 1);
+ this.props.resizeColumns(colHdrData);
}
};
@@ -218,7 +214,7 @@ export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColu
!Doc.UserDoc().noviceMode && ContextMenu.Instance.addItem({ description: 'Containers ...', subitems: layoutItems, icon: 'eye' });
ContextMenu.Instance.setDefaultItem('::', (name: string): void => {
Doc.GetProto(this.props.Document)[name] = '';
- const created = Docs.Create.TextDocument('', { title: name, _width: 250, _autoHeight: true });
+ const created = Docs.Create.TextDocument('', { title: name, _width: 250, _layout_autoHeight: true });
if (created) {
if (this.props.Document.isTemplateDoc) {
Doc.MakeMetadataFieldTemplate(created, this.props.Document);
@@ -248,9 +244,9 @@ export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColu
className="collectionNoteTakingView-sectionHeader-subCont"
title={evContents === `No Value` ? `Documents that don't have a ${key} value will go here. This column cannot be removed.` : ''}
style={{ background: evContents !== `No Value` ? this._color : 'inherit' }}>
- <EditableView GetValue={() => evContents} SetValue={this.headingChanged} contents={evContents} oneLine={true} />
+ <EditableView GetValue={() => evContents} isEditingCallback={isEditing => isEditing && this.props.select(false)} SetValue={this.headingChanged} contents={evContents} oneLine={true} />
</div>
- {(this.props.columnHeaders?.length ?? 0) > 1 && (
+ {(this.props.colHeaderData?.length ?? 0) > 1 && (
<button className="collectionNoteTakingView-sectionDelete" onClick={this.deleteColumn}>
<FontAwesomeIcon icon="trash" size="lg" />
</button>
@@ -262,28 +258,30 @@ export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColu
return (
<>
{headingView}
- <div className="collectionNoteTakingView-columnStack">
- <div
- key={`${heading}-stack`}
- className={`collectionNoteTakingView-Nodes`}
- style={{
- padding: `${columnYMargin}px ${0}px ${this.props.yMargin}px ${0}px`,
- gridGap: this.props.gridGap,
- gridTemplateColumns: templatecols,
- }}>
- {this.props.renderChildren(this.props.docList)}
- </div>
+ <div className="collectionNoteTakingView-columnStackContainer">
+ <div className="collectionNoteTakingView-columnStack">
+ <div
+ key={`${heading}-stack`}
+ className={`collectionNoteTakingView-Nodes`}
+ style={{
+ padding: `${columnYMargin}px ${0}px ${this.props.yMargin}px ${0}px`,
+ gridGap: this.props.gridGap,
+ gridTemplateColumns: templatecols,
+ }}>
+ {this.props.renderChildren(this.props.docList)}
+ </div>
- {!this.props.chromeHidden && type !== DocumentType.PRES ? (
- <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} />
+ {!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.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()} />
+ </div>
</div>
- <div key={`${this.props.Document[Id]}-addGroup`} className="collectionNoteTakingView-addDocumentButton">
- <EditableView {...this.props.editableViewProps()} />
- </div>
- </div>
- ) : null}
+ ) : null}
+ </div>
</div>
</>
);
@@ -294,7 +292,7 @@ export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColu
const heading = this._heading;
return (
<div
- className={'collectionNoteTakingViewFieldColumn' + (SnappingManager.GetIsDragging() ? 'Dragging' : '')}
+ className="collectionNoteTakingViewFieldColumn"
key={heading}
style={{
width: this.columnWidth,
diff --git a/src/client/views/collections/CollectionPileView.tsx b/src/client/views/collections/CollectionPileView.tsx
index 4489601db..91be31289 100644
--- a/src/client/views/collections/CollectionPileView.tsx
+++ b/src/client/views/collections/CollectionPileView.tsx
@@ -1,96 +1,106 @@
-import { action, computed, IReactionDisposer, reaction } from "mobx";
-import { observer } from "mobx-react";
-import { Doc, HeightSym, WidthSym } from "../../../fields/Doc";
-import { NumCast, StrCast } from "../../../fields/Types";
-import { emptyFunction, returnFalse, returnTrue, setupMoveUpEvents } from "../../../Utils";
-import { DocUtils } from "../../documents/Documents";
-import { SelectionManager } from "../../util/SelectionManager";
-import { SnappingManager } from "../../util/SnappingManager";
-import { undoBatch, UndoManager } from "../../util/UndoManager";
-import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView";
-import "./CollectionPileView.scss";
-import { CollectionSubView } from "./CollectionSubView";
-import React = require("react");
-import { ScriptField } from "../../../fields/ScriptField";
+import { action, computed, IReactionDisposer, reaction } from 'mobx';
+import { observer } from 'mobx-react';
+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';
+import { SelectionManager } from '../../util/SelectionManager';
+import { SnappingManager } from '../../util/SnappingManager';
+import { undoBatch, UndoManager } from '../../util/UndoManager';
+import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView';
+import './CollectionPileView.scss';
+import { CollectionSubView } from './CollectionSubView';
+import React = require('react');
+import { ScriptField } from '../../../fields/ScriptField';
+import { OpenWhere } from '../nodes/DocumentView';
+import { computePassLayout, computeStarburstLayout } from './collectionFreeForm';
@observer
export class CollectionPileView extends CollectionSubView() {
- _originalChrome: any = "";
+ _originalChrome: any = '';
_disposers: { [name: string]: IReactionDisposer } = {};
componentDidMount() {
- if (this.layoutEngine() !== "pass" && this.layoutEngine() !== "starburst") {
- this.Document._pileLayoutEngine = "pass";
+ if (this.layoutEngine() !== computePassLayout.name && this.layoutEngine() !== computeStarburstLayout.name) {
+ this.Document._freeform_pileEngine = computePassLayout.name;
}
this._originalChrome = this.layoutDoc._chromeHidden;
this.layoutDoc._chromeHidden = true;
-
- // pileups are designed to go away when they are empty.
- this._disposers.selected = reaction(() => this.childDocs.length,
- (num) => !num && this.props.ContainingCollectionView?.removeDocument(this.props.Document));
}
componentWillUnmount() {
this.layoutDoc._chromeHidden = this._originalChrome;
Object.values(this._disposers).forEach(disposer => disposer?.());
}
- layoutEngine = () => StrCast(this.Document._pileLayoutEngine);
+ layoutEngine = () => StrCast(this.Document._freeform_pileEngine);
@undoBatch
addPileDoc = (doc: Doc | Doc[]) => {
- (doc instanceof Doc ? [doc] : doc).map((d) => DocUtils.iconify(d));
+ (doc instanceof Doc ? [doc] : doc).map(d => DocUtils.iconify(d));
return this.props.addDocument?.(doc) || false;
- }
+ };
@undoBatch
removePileDoc = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => {
- (doc instanceof Doc ? [doc] : doc).map(undoBatch((d) => Doc.deiconifyView(d)));
- return this.props.moveDocument?.(doc, targetCollection, addDoc) || false;
- }
+ (doc instanceof Doc ? [doc] : doc).forEach(d => Doc.deiconifyView(d));
+ const ret = this.props.moveDocument?.(doc, targetCollection, addDoc) || false;
+ if (ret && !DocListCast(this.rootDoc[this.fieldKey ?? 'data']).length) this.props.DocumentView?.().props.removeDocument?.(this.rootDoc);
+ return ret;
+ };
- toggleIcon = () => {
- return ScriptField.MakeScript("documentView.iconify()", { documentView: "any" });
+ @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() === "starburst";
- return <div className="collectionPileView-innards"
- style={{ pointerEvents: isStarburst || SnappingManager.GetIsDragging() ? undefined : "none" }} >
- <CollectionFreeFormView {...this.props}
- layoutEngine={this.layoutEngine}
- childDocumentsActive={isStarburst ? returnTrue : undefined}
- addDocument={this.addPileDoc}
- childClickScript={this.toggleIcon()}
- moveDocument={this.removePileDoc} />
- </div>;
+ return (
+ <div className="collectionPileView-innards" style={{ pointerEvents: this.contentEvents }}>
+ <CollectionFreeFormView
+ {...this.props} //
+ layoutEngine={this.layoutEngine}
+ addDocument={this.addPileDoc}
+ 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>
+ );
}
// toggles the pileup between starburst to compact
toggleStarburst = action(() => {
- if (this.layoutEngine() === 'starburst') {
+ this.layoutDoc._freeform_scale = undefined;
+ if (this.layoutEngine() === computeStarburstLayout.name) {
+ 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._panX = 0;
- this.layoutDoc._panY = -10;
- this.props.Document._pileLayoutEngine = 'pass';
+ this.layoutDoc._freeform_panX = 0;
+ this.layoutDoc._freeform_panY = -10;
+ this.props.Document._freeform_pileEngine = computePassLayout.name;
} else {
- const defaultSize = 25;
- !this.layoutDoc._starburstRadius && (this.layoutDoc._starburstRadius = 250);
- !this.layoutDoc._starburstDocScale && (this.layoutDoc._starburstDocScale = 2.5);
- if (this.layoutEngine() === 'pass') {
- 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]();
- }
- this.layoutDoc._panX = this.layoutDoc._panY = 0;
+ const defaultSize = NumCast(this.rootDoc._starburstDiameter, 500);
+ 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 = 'starburst';
+ this.props.Document._freeform_pileEngine = computeStarburstLayout.name;
}
});
@@ -98,28 +108,34 @@ export class CollectionPileView extends CollectionSubView() {
_undoBatch: UndoManager.Batch | undefined;
pointerDown = (e: React.PointerEvent) => {
let dist = 0;
- SnappingManager.SetIsDragging(true);
- setupMoveUpEvents(this, e, (e: PointerEvent, down: number[], delta: number[]) => {
- if (this.layoutEngine() === "pass" && this.childDocs.length && e.shiftKey) {
- dist += Math.sqrt(delta[0] * delta[0] + delta[1] * delta[1]);
- if (dist > 100) {
- if (!this._undoBatch) {
- this._undoBatch = UndoManager.StartBatch("layout pile");
+ setupMoveUpEvents(
+ this,
+ e,
+ (e: PointerEvent, down: number[], delta: number[]) => {
+ if (this.layoutEngine() === 'pass' && this.childDocs.length && e.shiftKey) {
+ dist += Math.sqrt(delta[0] * delta[0] + delta[1] * delta[1]);
+ if (dist > 100) {
+ if (!this._undoBatch) {
+ this._undoBatch = UndoManager.StartBatch('layout pile');
+ }
+ const doc = this.childDocs[0];
+ doc.x = e.clientX;
+ doc.y = e.clientY;
+ this.props.addDocTab(doc, OpenWhere.inParentFromScreen) && (this.props.removeDocument?.(doc) || false);
+ dist = 0;
}
- const doc = this.childDocs[0];
- doc.x = e.clientX;
- doc.y = e.clientY;
- this.props.addDocTab(doc, "inParent") && (this.props.removeDocument?.(doc) || false);
- dist = 0;
}
- }
- return false;
- }, () => {
- this._undoBatch?.end();
- this._undoBatch = undefined;
- SnappingManager.SetIsDragging(false);
- }, emptyFunction, e.shiftKey && this.layoutEngine() === "pass", this.layoutEngine() === "pass" && e.shiftKey); // this sets _doubleTap
- }
+ return false;
+ },
+ () => {
+ this._undoBatch?.end();
+ this._undoBatch = undefined;
+ },
+ emptyFunction,
+ e.shiftKey && this.layoutEngine() === computePassLayout.name,
+ this.layoutEngine() === computePassLayout.name && e.shiftKey
+ ); // this sets _doubleTap
+ };
// onClick for toggling the pileup view
@undoBatch
@@ -130,12 +146,13 @@ export class CollectionPileView extends CollectionSubView() {
this.toggleStarburst();
e.stopPropagation();
}
- }
+ };
render() {
- return <div className={`collectionPileView`} onClick={this.onClick} onPointerDown={this.pointerDown}
- style={{ width: this.props.PanelWidth(), height: "100%" }}>
- {this.contents}
- </div>;
+ return (
+ <div className={`collectionPileView`} onClick={this.onClick} onPointerDown={this.pointerDown} style={{ width: this.props.PanelWidth(), height: '100%' }}>
+ {this.contents}
+ </div>
+ );
}
}
diff --git a/src/client/views/collections/CollectionStackedTimeline.scss b/src/client/views/collections/CollectionStackedTimeline.scss
index 5a107d2ca..a19d8e696 100644
--- a/src/client/views/collections/CollectionStackedTimeline.scss
+++ b/src/client/views/collections/CollectionStackedTimeline.scss
@@ -97,14 +97,14 @@
top: 2.5%;
height: 95%;
border-radius: 4px;
- background: $light-gray;
+ //background: $light-gray;
&:hover {
opacity: 1;
}
.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 7bf798656..ad84d859d 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -2,18 +2,20 @@ import React = require('react');
import { action, computed, IReactionDisposer, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import { computedFn } from 'mobx-utils';
-import { Doc, DocListCast, Opt, StrListCast } from '../../../fields/Doc';
+import { Doc, Opt, StrListCast } from '../../../fields/Doc';
import { Id } from '../../../fields/FieldSymbols';
import { List } from '../../../fields/List';
import { listSpec } from '../../../fields/Schema';
import { ComputedField, ScriptField } from '../../../fields/ScriptField';
-import { Cast, NumCast } from '../../../fields/Types';
-import { emptyFunction, formatTime, OmitKeys, returnFalse, returnOne, returnTrue, setupMoveUpEvents, smoothScrollHorizontal, StopEvent } from '../../../Utils';
+import { Cast, NumCast, ScriptCast } from '../../../fields/Types';
+import { ImageField } from '../../../fields/URLField';
+import { emptyFunction, formatTime, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnNone, returnTrue, returnZero, setupMoveUpEvents, smoothScrollHorizontal, StopEvent } from '../../../Utils';
import { Docs } from '../../documents/Documents';
import { DocumentType } from '../../documents/DocumentTypes';
import { DocumentManager } from '../../util/DocumentManager';
import { DragManager } from '../../util/DragManager';
-import { LinkFollower } from '../../util/LinkFollower';
+import { FollowLinkScript, IsFollowLinkScript, LinkFollower } from '../../util/LinkFollower';
+import { LinkManager } from '../../util/LinkManager';
import { ScriptingGlobals } from '../../util/ScriptingGlobals';
import { SelectionManager } from '../../util/SelectionManager';
import { SnappingManager } from '../../util/SnappingManager';
@@ -21,15 +23,11 @@ import { Transform } from '../../util/Transform';
import { undoBatch, UndoManager } from '../../util/UndoManager';
import { AudioWaveform } from '../AudioWaveform';
import { CollectionSubView } from '../collections/CollectionSubView';
-import { Colors } from '../global/globalEnums';
import { LightboxView } from '../LightboxView';
-import { DocFocusFunc, DocFocusOptions, DocumentView, DocumentViewProps, DocumentViewSharedProps } from '../nodes/DocumentView';
+import { DocFocusFunc, DocFocusOptions, DocumentView, DocumentViewProps, OpenWhere } from '../nodes/DocumentView';
import { LabelBox } from '../nodes/LabelBox';
-import './CollectionStackedTimeline.scss';
import { VideoBox } from '../nodes/VideoBox';
-import { ImageField } from '../../../fields/URLField';
-import { StyleProp } from '../StyleProvider';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import './CollectionStackedTimeline.scss';
export type CollectionStackedTimelineProps = {
Play: () => void;
@@ -43,7 +41,9 @@ export type CollectionStackedTimelineProps = {
mediaPath: string;
dictationKey: string;
rawDuration: number;
+ dataFieldKey: string;
fieldKey: string;
+ thumbnails?: () => string[];
};
// trimming state: shows full clip, current trim bounds, or not trimming
@@ -56,7 +56,7 @@ export enum TrimScope {
@observer
export class CollectionStackedTimeline extends CollectionSubView<CollectionStackedTimelineProps>() {
@observable static SelectingRegion: CollectionStackedTimeline | undefined = undefined;
- @observable public static CurrentlyPlaying: Doc[];
+ @observable public static CurrentlyPlaying: DocumentView[];
static RangeScript: ScriptField;
static LabelScript: ScriptField;
@@ -82,6 +82,9 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
get minTrimLength() {
return Math.max(this._timeline?.getBoundingClientRect() ? 0.05 * this.clipDuration : 0, 0.5);
}
+ @computed get thumbnails() {
+ return this.props.thumbnails?.();
+ }
@computed get trimStart() {
return this.IsTrimming !== TrimScope.None ? this._trimStart : this.clipStart;
@@ -104,7 +107,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
}
@computed get currentTime() {
- return NumCast(this.layoutDoc._currentTimecode);
+ return NumCast(this.layoutDoc._layout_currentTimecode);
}
@computed get zoomFactor() {
@@ -181,18 +184,21 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
if (
// need to include range inputs because after dragging video time slider it becomes target element
!(e.target instanceof HTMLInputElement && !(e.target.type === 'range')) &&
- this.props.isSelected(true)
+ this.props.isContentActive()
) {
// if shift pressed scrub 1 second otherwise 1/10th
const jump = e.shiftKey ? 1 : 0.1;
switch (e.key) {
case ' ':
+ this.props.playing() ? this.props.Pause() : this.props.Play();
+ break;
+ case '^':
if (!CollectionStackedTimeline.SelectingRegion) {
this._markerStart = this._markerEnd = this.currentTime;
CollectionStackedTimeline.SelectingRegion = this;
} else {
this._markerEnd = this.currentTime;
- CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.props.fieldKey, this.props.startTag, this.props.endTag, this._markerStart, this._markerEnd);
+ CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.props.fieldKey, this._markerStart, this._markerEnd, undefined, true);
this._markerEnd = undefined;
CollectionStackedTimeline.SelectingRegion = undefined;
}
@@ -218,12 +224,12 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
};
getLinkData(l: Doc) {
- let la1 = l.anchor1 as Doc;
- let la2 = l.anchor2 as Doc;
+ let la1 = l.link_anchor_1 as Doc;
+ let la2 = l.link_anchor_2 as Doc;
const linkTime = NumCast(la2[this.props.startTag], NumCast(la1[this.props.startTag]));
if (Doc.AreProtosEqual(la1, this.dataDoc)) {
- la1 = l.anchor2 as Doc;
- la2 = l.anchor1 as Doc;
+ la1 = l.link_anchor_2 as Doc;
+ la2 = l.link_anchor_1 as Doc;
}
return { la1, la2, linkTime };
}
@@ -259,7 +265,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
this._markerEnd = tmp;
}
if (!isClick && Math.abs(movement[0]) > 15 && !this.IsTrimming) {
- const anchor = CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.props.fieldKey, this.props.startTag, this.props.endTag, this._markerStart, this._markerEnd);
+ const anchor = CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.props.fieldKey, this._markerStart, this._markerEnd, undefined, true);
setTimeout(() => DocumentManager.Instance.getDocumentView(anchor)?.select(false));
}
(!isClick || !wasSelecting) && (this._markerEnd = undefined);
@@ -275,7 +281,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
undefined,
() => {
if (shiftKey) {
- CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.props.fieldKey, this.props.startTag, this.props.endTag, this.currentTime);
+ CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.props.fieldKey, this.currentTime, undefined, undefined, true);
} else {
!wasPlaying && this.props.setTime(this.toTimeline(clientX - rect.x, rect.width));
}
@@ -291,10 +297,9 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
const clientX = e.clientX;
if (rect) {
this._hoverTime = this.toTimeline(clientX - rect.x, rect.width);
- if (this.dataDoc.thumbnails) {
+ if (this.thumbnails) {
const nearest = Math.floor((this._hoverTime / this.props.rawDuration) * VideoBox.numThumbnails);
- const thumbnails = StrListCast(this.dataDoc.thumbnails);
- const imgField = thumbnails?.length > 0 ? new ImageField(thumbnails[nearest]) : undefined;
+ const imgField = this.thumbnails.length > 0 ? new ImageField(this.thumbnails[nearest]) : undefined;
this._thumbnail = imgField?.url?.href ? imgField.url.href.replace('.png', '_m.png') : undefined;
}
}
@@ -364,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) => {
@@ -390,36 +395,39 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
// creates marker on timeline
@undoBatch
@action
- static createAnchor(rootDoc: Doc, dataDoc: Doc, fieldKey: string, startTag: string, endTag: string, anchorStartTime?: number, anchorEndTime?: number, docAnchor?: Doc) {
+ static createAnchor(rootDoc: Doc, dataDoc: Doc, fieldKey: string, anchorStartTime: Opt<number>, anchorEndTime: Opt<number>, docAnchor: Opt<Doc>, addAsAnnotation: boolean) {
if (anchorStartTime === undefined) return rootDoc;
+ const startTag = '_timecodeToShow';
+ const endTag = '_timecodeToHide';
const anchor =
docAnchor ??
Docs.Create.LabelDocument({
title: ComputedField.MakeFunction(`self["${endTag}"] ? "#" + formatToTime(self["${startTag}"]) + "-" + formatToTime(self["${endTag}"]) : "#" + formatToTime(self["${startTag}"])`) as any,
- _minFontSize: 12,
- _maxFontSize: 24,
- _singleLine: false,
- _stayInCollection: true,
- useLinkSmallAnchor: true,
- hideLinkButton: true,
- _isLinkButton: 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;
- if (Cast(dataDoc[fieldKey], listSpec(Doc), null)) {
- Cast(dataDoc[fieldKey], listSpec(Doc), []).push(anchor);
- } else {
- dataDoc[fieldKey] = new List<Doc>([anchor]);
+ if (addAsAnnotation) {
+ if (Cast(dataDoc[fieldKey], listSpec(Doc), null)) {
+ Cast(dataDoc[fieldKey], listSpec(Doc), []).push(anchor);
+ } else {
+ dataDoc[fieldKey] = new List<Doc>([anchor]);
+ }
}
return anchor;
}
@action
playOnClick = (anchorDoc: Doc, clientX: number) => {
- const seekTimeInSeconds = this.anchorStart(anchorDoc) - 0.25;
+ const seekTimeInSeconds = this.anchorStart(anchorDoc) - 0.05;
const endTime = this.anchorEnd(anchorDoc);
if (this.layoutDoc.autoPlayAnchors) {
if (this.props.playing()) this.props.Pause();
@@ -428,7 +436,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
this.scrollToTime(seekTimeInSeconds);
}
} else {
- if (seekTimeInSeconds < NumCast(this.layoutDoc._currentTimecode) && endTime > NumCast(this.layoutDoc._currentTimecode)) {
+ if (seekTimeInSeconds < NumCast(this.layoutDoc._layout_currentTimecode) && endTime > NumCast(this.layoutDoc._layout_currentTimecode)) {
if (!this.layoutDoc.autoPlayAnchors && this.props.playing()) {
this.props.Pause();
} else {
@@ -444,12 +452,12 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
@action
clickAnchor = (anchorDoc: Doc, clientX: number) => {
- if (anchorDoc.isLinkButton) {
- LinkFollower.FollowLink(undefined, anchorDoc, this.props, false);
+ if (IsFollowLinkScript(anchorDoc.onClick)) {
+ LinkFollower.FollowLink(undefined, anchorDoc, false);
}
- const seekTimeInSeconds = this.anchorStart(anchorDoc) - 0.25;
+ const seekTimeInSeconds = this.anchorStart(anchorDoc) - 0.05;
const endTime = this.anchorEnd(anchorDoc);
- if (seekTimeInSeconds < NumCast(this.layoutDoc._currentTimecode) + 1e-4 && endTime > NumCast(this.layoutDoc._currentTimecode) - 1e-4) {
+ if (seekTimeInSeconds < NumCast(this.layoutDoc._layout_currentTimecode) + 1e-4 && endTime > NumCast(this.layoutDoc._layout_currentTimecode) - 1e-4) {
if (this.props.playing()) this.props.Pause();
else if (this.layoutDoc.autoPlayAnchors) this.props.Play();
else if (!this.layoutDoc.autoPlayAnchors) {
@@ -505,37 +513,6 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
currentTimecode = () => this.currentTime;
- @computed get renderDictation() {
- const dictation = Cast(this.dataDoc[this.props.dictationKey], Doc, null);
- return !dictation ? null : (
- <div
- style={{
- position: 'absolute',
- height: '100%',
- top: this.timelineContentHeight,
- background: Colors.LIGHT_BLUE,
- }}>
- <DocumentView
- {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
- Document={dictation}
- PanelHeight={this.dictationHeight}
- isAnnotationOverlay={true}
- isDocumentActive={returnFalse}
- select={emptyFunction}
- NativeDimScaling={returnOne}
- xMargin={25}
- yMargin={10}
- ScreenToLocalTransform={this.dictationScreenToLocalTransform}
- whenChildContentsActiveChanged={emptyFunction}
- removeDocument={returnFalse}
- moveDocument={returnFalse}
- addDocument={returnFalse}
- CollectionView={undefined}
- renderDepth={this.props.renderDepth + 1}></DocumentView>
- </div>
- );
- }
-
// renders selection region on timeline
@computed get selectionContainer() {
const markerEnd = CollectionStackedTimeline.SelectingRegion === this ? this.currentTime : this._markerEnd;
@@ -550,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;
@@ -561,8 +541,8 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
anchor,
}));
const maxLevel = overlaps.reduce((m, o) => Math.max(m, o.level), 0) + 2;
- return (
- <div ref={this.createDashEventsTarget} style={{ pointerEvents: SnappingManager.GetIsDragging() ? 'all' : undefined }}>
+ return this.clipDuration === 0 ? null : (
+ <div ref={this.createDashEventsTarget} style={{ pointerEvents: this.timelineEvents }}>
<div
className="collectionStackedTimeline-timelineContainer"
style={{ width: this.props.PanelWidth(), cursor: SnappingManager.GetIsDragging() ? 'grab' : '' }}
@@ -594,10 +574,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
top,
width: `${width}px`,
height: `${height}px`,
- }}
- onClick={e => {
- this.props.playFrom(start, this.anchorEnd(d.anchor));
- e.stopPropagation();
+ pointerEvents: 'none',
}}>
<StackedTimelineAnchor
{...this.props}
@@ -610,7 +587,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
height={height}
toTimeline={this.toTimeline}
layoutDoc={this.layoutDoc}
- // isDocumentActive={this.props.childDocumentsActive ? this.props.isDocumentActive : this.isContentActive}
+ isDocumentActive={this.isContentActive}
currentTimecode={this.currentTimecode}
_timeline={this._timeline}
stackedTimeline={this}
@@ -621,18 +598,20 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
);
})}
{!this.IsTrimming && this.selectionContainer}
- <AudioWaveform
- rawDuration={this.props.rawDuration}
- duration={this.clipDuration}
- mediaPath={this.props.mediaPath}
- layoutDoc={this.layoutDoc}
- clipStart={this.clipStart}
- clipEnd={this.clipEnd}
- zoomFactor={this.zoomFactor}
- PanelHeight={this.timelineContentHeight}
- PanelWidth={this.timelineContentWidth}
- />
- {/* {this.renderDictation} */}
+ {!this.props.PanelHeight() ? null : (
+ <AudioWaveform
+ rawDuration={this.props.rawDuration}
+ fieldKey={this.props.dataFieldKey}
+ duration={this.clipDuration}
+ mediaPath={this.props.mediaPath}
+ layoutDoc={this.layoutDoc}
+ clipStart={this.clipStart}
+ clipEnd={this.clipEnd}
+ zoomFactor={this.zoomFactor}
+ PanelHeight={this.timelineContentHeight}
+ PanelWidth={this.timelineContentWidth}
+ />
+ )}
<div
className="collectionStackedTimeline-hover"
@@ -688,6 +667,8 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
interface StackedTimelineAnchorProps {
mark: Doc;
+ whenChildContentsActiveChanged: (isActive: boolean) => void;
+ addDocTab: (doc: Doc, where: OpenWhere) => boolean;
rangeClickScript: () => ScriptField;
rangePlayScript: () => ScriptField;
left: number;
@@ -702,7 +683,7 @@ interface StackedTimelineAnchorProps {
endTag: string;
renderDepth: number;
layoutDoc: Doc;
- isDocumentActive?: () => boolean;
+ isDocumentActive?: () => boolean | undefined;
ScreenToLocalTransform: () => Transform;
_timeline: HTMLDivElement | null;
focus: DocFocusFunc;
@@ -736,19 +717,19 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps>
() => this.props.currentTimecode(),
time => {
const dictationDoc = Cast(this.props.layoutDoc['data-dictation'], Doc, null);
- const isDictation = dictationDoc && DocListCast(this.props.mark.links).some(link => Cast(link.anchor1, Doc, null)?.annotationOn === dictationDoc);
+ const isDictation = dictationDoc && LinkManager.Links(this.props.mark).some(link => Cast(link.link_anchor_1, Doc, null)?.annotationOn === dictationDoc);
if (
!LightboxView.LightboxDoc &&
// bcz: when should links be followed? we don't want to move away from the video to follow a link but we can open it in a sidebar/etc. But we don't know that upfront.
// for now, we won't follow any links when the lightbox is oepn to avoid "losing" the video.
/*(isDictation || !Doc.AreProtosEqual(LightboxView.LightboxDoc, this.props.layoutDoc))*/
!this.props.layoutDoc.dontAutoFollowLinks &&
- DocListCast(this.props.mark.links).length &&
+ LinkManager.Links(this.props.mark).length &&
time > NumCast(this.props.mark[this.props.startTag]) &&
time < NumCast(this.props.mark[this.props.endTag]) &&
this._lastTimecode < NumCast(this.props.mark[this.props.startTag]) - 1e-5
) {
- LinkFollower.FollowLink(undefined, this.props.mark, this.props as any as DocumentViewProps, false, true);
+ LinkFollower.FollowLink(undefined, this.props.mark, false);
}
this._lastTimecode = time;
}
@@ -759,9 +740,11 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps>
this._disposer?.();
}
+ @observable noEvents = false;
// starting the drag event for anchor resizing
+ @action
onAnchorDown = (e: React.PointerEvent, anchor: Doc, left: boolean): void => {
- this.props._timeline?.setPointerCapture(e.pointerId);
+ //this.props._timeline?.setPointerCapture(e.pointerId);
const newTime = (e: PointerEvent) => {
const rect = (e.target as any).getBoundingClientRect();
return this.props.toTimeline(e.clientX - rect.x, rect.width);
@@ -771,14 +754,14 @@ 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;
}
return false;
};
+ this.noEvents = true;
var undo: UndoManager.Batch | undefined;
-
setupMoveUpEvents(
this,
e,
@@ -787,11 +770,11 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps>
this.props.setTime(newTime(e));
return changeAnchor(anchor, left, newTime(e));
},
- e => {
+ action(e => {
this.props.setTime(newTime(e));
- this.props._timeline?.releasePointerCapture(e.pointerId);
undo?.end();
- },
+ this.noEvents = false;
+ }),
emptyFunction
);
};
@@ -806,51 +789,41 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps>
return [resetTitle];
};
- innerStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string): any => {
- if (property === StyleProp.Decorations && doc && NumCast(doc.timecodeToHide) - NumCast(doc.timecodeToShow) < 0.0002) {
- return (
- <div className="styleProvider-lock">
- <FontAwesomeIcon
- icon={'camera'}
- style={{ color: 'red' }}
- onClick={e => {
- LinkFollower.FollowLink(undefined, doc, props as DocumentViewSharedProps, e.altKey);
- e.stopPropagation();
- }}
- size="lg"
- />
- </div>
- );
- }
- return this.props.styleProvider?.(doc, props, property);
- };
-
// renders anchor LabelBox
renderInner = computedFn(function (this: StackedTimelineAnchor, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), screenXf: () => Transform, width: () => number, height: () => number) {
const anchor = observable({ view: undefined as any });
- const focusFunc = (doc: Doc, options: DocFocusOptions) => {
+ const focusFunc = (doc: Doc, options: DocFocusOptions): number | undefined => {
this.props.playLink(mark);
- this.props.focus(doc, options);
+ return undefined;
};
return {
anchor,
view: (
<DocumentView
key="view"
- {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight']).omit}
+ {...this.props}
+ NativeWidth={returnZero}
+ NativeHeight={returnZero}
ref={action((r: DocumentView | null) => (anchor.view = r))}
Document={mark}
DataDoc={undefined}
- styleProvider={this.innerStyleProvider}
+ docViewPath={returnEmptyDoclist}
+ pointerEvents={this.noEvents ? returnNone : undefined}
+ styleProvider={this.props.styleProvider}
renderDepth={this.props.renderDepth + 1}
LayoutTemplate={undefined}
LayoutTemplateString={LabelBox.LayoutStringWithTitle('data', this.computeTitle())}
isDocumentActive={this.props.isDocumentActive}
PanelWidth={width}
PanelHeight={height}
- fitWidth={returnTrue}
+ layout_fitWidth={returnTrue}
ScreenToLocalTransform={screenXf}
+ pinToPres={emptyFunction}
focus={focusFunc}
+ isContentActive={returnFalse}
+ searchFilterDocs={returnEmptyDoclist}
+ childFilters={returnEmptyFilter}
+ childFiltersByRanges={returnEmptyFilter}
rootSelected={returnFalse}
onClick={script}
onDoubleClick={this.props.layoutDoc.autoPlayAnchors ? undefined : doublescript}
@@ -871,15 +844,15 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps>
render() {
const inner = this.renderInner(this.props.mark, this.props.rangeClickScript, this.props.rangePlayScript, this.anchorScreenToLocalXf, this.width, this.height);
return (
- <>
+ <div style={{ pointerEvents: this.noEvents ? 'none' : undefined }}>
{inner.view}
{!inner.anchor.view || !SelectionManager.IsSelected(inner.anchor.view) ? null : (
<>
- <div key="left" className="collectionStackedTimeline-left-resizer" onPointerDown={e => this.onAnchorDown(e, this.props.mark, true)} />
- <div key="right" className="collectionStackedTimeline-resizer" onPointerDown={e => this.onAnchorDown(e, this.props.mark, false)} />
+ <div key="left" className="collectionStackedTimeline-left-resizer" style={{ pointerEvents: this.noEvents ? 'none' : undefined }} onPointerDown={e => this.onAnchorDown(e, this.props.mark, true)} />
+ <div key="right" className="collectionStackedTimeline-resizer" style={{ pointerEvents: this.noEvents ? 'none' : undefined }} onPointerDown={e => this.onAnchorDown(e, this.props.mark, false)} />
</>
)}
- </>
+ </div>
);
}
}
diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss
index 7385f933b..99a68e94b 100644
--- a/src/client/views/collections/CollectionStackingView.scss
+++ b/src/client/views/collections/CollectionStackingView.scss
@@ -150,11 +150,14 @@
}
.collectionStackingView-collapseBar {
- margin-left: 2px;
- margin-right: 2px;
margin-top: 2px;
background: $medium-gray;
height: 5px;
+ width: 100%;
+ display: none;
+ position: absolute;
+ top: 0;
+ cursor: default;
&.active {
margin-left: 0;
@@ -236,7 +239,7 @@
.editableView-container-editing-oneLine,
.editableView-container-editing {
color: grey;
- padding: 10px;
+ //padding: 10px;
}
.editableView-input:hover,
@@ -333,7 +336,7 @@
.collectionStackingView-sectionDelete {
position: absolute;
- right: 25px;
+ right: 0px;
top: 0;
height: 100%;
display: none;
@@ -352,6 +355,10 @@
.collectionStackingView-sectionDelete {
display: unset;
}
+
+ .collectionStackingView-collapseBar {
+ display: block;
+ }
}
.collectionStackingView-addDocumentButton,
@@ -365,7 +372,8 @@
.editableView-container-editing-oneLine,
.editableView-container-editing {
color: grey;
- padding: 10px;
+ padding-top: 5px;
+ padding-bottom: 5px;
width: 100%;
}
@@ -380,7 +388,8 @@
letter-spacing: 2px;
color: grey;
border: 0px;
- padding: 12px 10px 11px 10px;
+ padding-top: 5px; // 12px 10px 11px 10px;
+ padding-bottom: 5px; // 12px 10px 11px 10px;
}
}
@@ -394,7 +403,7 @@
letter-spacing: 2px;
color: grey;
border: 0px;
- padding: 12px 10px 11px 10px;
+ padding-top: 10px; // : 12px 10px 11px 10px;
}
}
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 175051d5c..4756b4cd3 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -3,26 +3,27 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { CursorProperty } from 'csstype';
import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
-import { DataSym, Doc, DocListCast, 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, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
+import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
import { TraceMobx } from '../../../fields/util';
-import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, returnZero, setupMoveUpEvents, smoothScroll, Utils } from '../../../Utils';
+import { emptyFunction, 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';
import { LightboxView } from '../LightboxView';
import { CollectionFreeFormDocumentView } from '../nodes/CollectionFreeFormDocumentView';
-import { DocFocusOptions, DocumentView, DocumentViewProps, ViewAdjustment } from '../nodes/DocumentView';
+import { DocFocusOptions, DocumentView, DocumentViewProps } from '../nodes/DocumentView';
import { FieldViewProps } from '../nodes/FieldView';
import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
import { StyleProp } from '../StyleProvider';
@@ -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;
};
@@ -49,7 +50,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
// Not sure what a pivot field is. Seems like we cause reaction in MobX get rid of it once we exit this view
_pivotFieldDisposer?: IReactionDisposer;
// Seems like we cause reaction in MobX get rid of our height once we exit this view
- _autoHeightDisposer?: IReactionDisposer;
+ _layout_autoHeightDisposer?: IReactionDisposer;
// keeping track of documents. Updated on internal and external drops. What's the difference?
_docXfs: { height: () => number; width: () => number; stackedDocTransform: () => Transform }[] = [];
// Doesn't look like this field is being used anywhere. Obsolete?
@@ -65,8 +66,8 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
return this.props.chromeHidden || BoolCast(this.layoutDoc.chromeHidden);
}
// it looks like this gets the column headers that Mehek was showing just now
- @computed get columnHeaders() {
- return Cast(this.layoutDoc._columnHeaders, listSpec(SchemaHeaderField), null);
+ @computed get colHeaderData() {
+ return Cast(this.layoutDoc['_' + this.fieldKey + '_columnHeaders'], listSpec(SchemaHeaderField), null);
}
// Still not sure what a pivot is, but it appears that we can actually filter docs somehow?
@computed get pivotField() {
@@ -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() {
@@ -116,10 +117,10 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
constructor(props: any) {
super(props);
- if (this.columnHeaders === undefined) {
+ if (this.colHeaderData === undefined) {
// TODO: what is a layout doc? Is it literally how this document is supposed to be layed out?
// here we're making an empty list of column headers (again, what Mehek showed us)
- this.layoutDoc._columnHeaders = new List<SchemaHeaderField>();
+ this.layoutDoc['_' + this.fieldKey + '_columnHeaders'] = new List<SchemaHeaderField>();
}
}
@@ -129,6 +130,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
TraceMobx();
// appears that we are going to reset the _docXfs. TODO: what is Xfs?
this._docXfs.length = 0;
+ this._renderCount < docs.length && setTimeout(action(() => (this._renderCount = Math.min(docs.length, this._renderCount + 5))));
return docs.map((d, i) => {
const height = () => this.getDocHeight(d);
const width = () => this.getDocWidth(d);
@@ -139,7 +141,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
// So we're choosing whether we're going to render a column or a masonry doc
return (
<div className={`collectionStackingView-${this.isStackingView ? 'columnDoc' : 'masonryDoc'}`} key={d[Id]} style={style}>
- {this.getDisplayDoc(d, width)}
+ {this.getDisplayDoc(d, width, i)}
</div>
);
});
@@ -153,14 +155,14 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
//TODO: this seems important
get Sections() {
// appears that pivot field IS actually for sorting
- if (!this.pivotField || this.columnHeaders instanceof Promise) return new Map<SchemaHeaderField, Doc[]>();
+ if (!this.pivotField || this.colHeaderData instanceof Promise) return new Map<SchemaHeaderField, Doc[]>();
- if (this.columnHeaders === undefined) {
- setTimeout(() => (this.layoutDoc._columnHeaders = new List<SchemaHeaderField>()), 0);
+ if (this.colHeaderData === undefined) {
+ setTimeout(() => (this.layoutDoc['_' + this.fieldKey + '_columnHeaders'] = new List<SchemaHeaderField>()), 0);
return new Map<SchemaHeaderField, Doc[]>();
}
- const columnHeaders = Array.from(this.columnHeaders);
- const fields = new Map<SchemaHeaderField, Doc[]>(columnHeaders.map(sh => [sh, []] as [SchemaHeaderField, []]));
+ const colHeaderData = Array.from(this.colHeaderData);
+ const fields = new Map<SchemaHeaderField, Doc[]>(colHeaderData.map(sh => [sh, []] as [SchemaHeaderField, []]));
let changed = false;
this.filteredChildren.map(d => {
const sectionValue = (d[this.pivotField] ? d[this.pivotField] : `NO ${this.pivotField.toUpperCase()} VALUE`) as object;
@@ -169,13 +171,13 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
const castedSectionValue = !isNaN(parsed) ? parsed : sectionValue;
// look for if header exists already
- const existingHeader = columnHeaders.find(sh => sh.heading === (castedSectionValue ? castedSectionValue.toString() : `NO ${this.pivotField.toUpperCase()} VALUE`));
+ const existingHeader = colHeaderData.find(sh => sh.heading === (castedSectionValue ? castedSectionValue.toString() : `NO ${this.pivotField.toUpperCase()} VALUE`));
if (existingHeader) {
fields.get(existingHeader)!.push(d);
} else {
const newSchemaHeader = new SchemaHeaderField(castedSectionValue ? castedSectionValue.toString() : `NO ${this.pivotField.toUpperCase()} VALUE`);
fields.set(newSchemaHeader, [d]);
- columnHeaders.push(newSchemaHeader);
+ colHeaderData.push(newSchemaHeader);
changed = true;
}
});
@@ -186,13 +188,13 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
.filter(key => !fields.get(key)!.length)
.map(header => {
fields.delete(header);
- columnHeaders.splice(columnHeaders.indexOf(header), 1);
+ colHeaderData.splice(colHeaderData.indexOf(header), 1);
changed = true;
});
}
changed &&
setTimeout(
- action(() => this.columnHeaders?.splice(0, this.columnHeaders.length, ...columnHeaders)),
+ action(() => this.colHeaderData?.splice(0, this.colHeaderData.length, ...colHeaderData)),
0
);
return fields;
@@ -200,16 +202,17 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
componentDidMount() {
super.componentDidMount?.();
+ this.props.setContentView?.(this);
// reset section headers when a new filter is inputted
this._pivotFieldDisposer = reaction(
() => this.pivotField,
- () => (this.layoutDoc._columnHeaders = new List())
+ () => (this.layoutDoc['_' + this.fieldKey + '_columnHeaders'] = new List())
);
- this._autoHeightDisposer = reaction(
- () => this.layoutDoc._autoHeight,
- autoHeight =>
- autoHeight &&
+ this._layout_autoHeightDisposer = reaction(
+ () => this.layoutDoc._layout_autoHeight,
+ layout_autoHeight =>
+ layout_autoHeight &&
this.props.setHeight?.(
Math.min(
NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER),
@@ -222,11 +225,13 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
componentWillUnmount() {
super.componentWillUnmount();
this._pivotFieldDisposer?.();
- this._autoHeightDisposer?.();
+ this._layout_autoHeightDisposer?.();
}
+ isAnyChildContentActive = () => this.props.isAnyChildContentActive();
+
@action
- moveDocument = (doc: Doc, targetCollection: Doc | undefined, addDocument: (document: Doc) => boolean): boolean => {
+ moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean): boolean => {
return this.props.removeDocument?.(doc) && addDocument?.(doc) ? true : false;
};
createRef = (ele: HTMLDivElement | null) => {
@@ -234,44 +239,30 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
this.createDashEventsTarget(ele!); //so the whole grid is the drop target?
};
- @computed get onChildClickHandler() {
- return () => this.props.childClickScript || ScriptCast(this.Document.onChildClick);
- }
+ onChildClickHandler = () => this.props.childClickScript || ScriptCast(this.Document.onChildClick);
@computed get onChildDoubleClickHandler() {
return () => this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick);
}
- addDocTab = (doc: Doc, where: string) => {
- if (where === 'inPlace' && this.layoutDoc.isInPlaceContainer) {
- this.dataDoc[this.props.fieldKey] = new List<Doc>([doc]);
- return true;
- }
- return this.props.addDocTab(doc, where);
- };
-
scrollToBottom = () => {
- smoothScroll(500, this._mainCont!, this._mainCont!.scrollHeight);
+ smoothScroll(500, this._mainCont!, this._mainCont!.scrollHeight, 'ease');
};
// let's dive in and get the actual document we want to drag/move around
focusDocument = (doc: Doc, options: DocFocusOptions) => {
Doc.BrushDoc(doc);
- let focusSpeed = 0;
const found = this._mainCont && Array.from(this._mainCont.getElementsByClassName('documentView-node')).find((node: any) => node.id === doc[Id]);
if (found) {
const top = found.getBoundingClientRect().top;
const localTop = this.props.ScreenToLocalTransform().transformPoint(0, top);
if (Math.floor(localTop[1]) !== 0) {
- smoothScroll((focusSpeed = NumCast(doc.focusSpeed, 500)), this._mainCont!, localTop[1] + this._mainCont!.scrollTop);
+ let focusSpeed = options.zoomTime ?? 500;
+ smoothScroll(focusSpeed, this._mainCont!, localTop[1] + this._mainCont!.scrollTop, options.easeFunc);
+ return focusSpeed;
}
}
- const endFocus = async (moved: boolean) => options?.afterFocus?.(moved) ?? ViewAdjustment.doNothing;
- this.props.focus(this.rootDoc, {
- willZoom: options?.willZoom,
- scale: options?.scale,
- afterFocus: (didFocus: boolean) => new Promise<ViewAdjustment>(res => setTimeout(async () => res(await endFocus(didFocus)), focusSpeed)),
- });
+ return undefined;
};
styleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string) => {
@@ -292,68 +283,75 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
if (docView && ['Enter'].includes(e.key) && e.ctrlKey) {
e.stopPropagation?.();
const below = !e.altKey && e.key !== 'Tab';
- const layoutKey = StrCast(docView.LayoutFieldKey);
+ 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;
- if (layoutKey !== 'layout' && docView.rootDoc[layoutKey] instanceof Doc) {
- newDoc[layoutKey] = docView.rootDoc[layoutKey];
+ 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];
}
Doc.GetProto(newDoc).text = undefined;
FormattedTextBox.SelectOnLoad = newDoc[Id];
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 = () =>
- this.props.isDocumentActive?.() && (this.props.childDocumentsActive?.() || BoolCast(this.rootDoc.childDocumentsActive)) ? true : this.props.childDocumentsActive?.() === false || this.rootDoc.childDocumentsActive === false ? false : undefined;
+ this.props.isContentActive?.() === false
+ ? false
+ : this.props.isDocumentActive?.() && (this.props.childDocumentsActive?.() || BoolCast(this.rootDoc.childDocumentsActive))
+ ? true
+ : this.props.childDocumentsActive?.() === false || this.rootDoc.childDocumentsActive === false
+ ? false
+ : undefined;
isChildButtonContentActive = () => (this.props.childDocumentsActive?.() === false || this.rootDoc.childDocumentsActive === false ? false : undefined);
// 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) {
- const dataDoc = !doc.isTemplateDoc && !doc.isTemplateForField && !doc.PARAMS ? undefined : this.props.DataDoc;
+ 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);
this._docXfs.push({ stackedDocTransform, width, height });
//DocumentView is how the node will be rendered
- return (
+ return count > this._renderCount ? null : (
<DocumentView
ref={r => (dref = r || undefined)}
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}
+ pointerEvents={this.props.DocumentView?.().props.onClick?.() ? returnNone : undefined} // if the stack has an onClick, then we don't want the contents to be interactive (see CollectionPileView)
styleProvider={this.styleProvider}
docViewPath={this.props.docViewPath}
- fitWidth={this.props.childFitWidth}
- isContentActive={doc.isLinkButton ? this.isChildButtonContentActive : this.isChildContentActive}
+ layout_fitWidth={this.props.childLayoutFitWidth}
+ isContentActive={doc.onClick ? this.isChildButtonContentActive : this.isChildContentActive}
onKey={this.onKeyDown}
+ onBrowseClick={this.props.onBrowseClick}
isDocumentActive={this.isContentActive}
LayoutTemplate={this.props.childLayoutTemplate}
LayoutTemplateString={this.props.childLayoutString}
- NativeWidth={this.props.childIgnoreNativeSize ? returnZero : this.props.childFitWidth?.(doc) || (doc._fitWidth && !Doc.NativeWidth(doc)) ? width : undefined} // explicitly ignore nativeWidth/height if childIgnoreNativeSize is set- used by PresBox
- NativeHeight={this.props.childIgnoreNativeSize ? returnZero : this.props.childFitWidth?.(doc) || (doc._fitWidth && !Doc.NativeHeight(doc)) ? height : undefined}
+ NativeWidth={this.props.childIgnoreNativeSize ? returnZero : this.props.childLayoutFitWidth?.(doc) || (doc._layout_fitWidth && !Doc.NativeWidth(doc)) ? width : undefined} // explicitly ignore nativeWidth/height if childIgnoreNativeSize is set- used by PresBox
+ NativeHeight={this.props.childIgnoreNativeSize ? returnZero : this.props.childLayoutFitWidth?.(doc) || (doc._layout_fitWidth && !Doc.NativeHeight(doc)) ? height : undefined}
dontCenter={this.props.childIgnoreNativeSize ? 'xy' : undefined}
- dontRegisterView={BoolCast(this.layoutDoc.childDontRegisterViews, this.props.dontRegisterView)} // used to be true if DataDoc existed, but template textboxes won't autoHeight resize if dontRegisterView is set, but they need to.
+ 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}
- showTitle={this.props.childShowTitle}
- dropAction={StrCast(this.layoutDoc.childDropAction) as dropActionType}
+ layout_showTitle={this.props.childlayout_showTitle}
+ 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}
- ContainingCollectionDoc={this.props.CollectionView?.props.Document}
- ContainingCollectionView={this.props.CollectionView}
xPadding={NumCast(this.layoutDoc._childXPadding, this.props.childXPadding)}
yPadding={NumCast(this.layoutDoc._childYPadding, this.props.childYPadding)}
addDocument={this.props.addDocument}
@@ -361,7 +359,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
removeDocument={this.props.removeDocument}
contentPointerEvents={StrCast(this.layoutDoc.contentPointerEvents)}
whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged}
- addDocTab={this.addDocTab}
+ addDocTab={this.props.addDocTab}
bringToFront={returnFalse}
scriptContext={this.props.scriptContext}
pinToPres={this.props.pinToPres}
@@ -379,36 +377,40 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
if (!d) return 0;
const childLayoutDoc = Doc.Layout(d, this.props.childLayoutTemplate?.());
const maxWidth = this.columnWidth / this.numGroupColumns;
- if (!this.layoutDoc._columnsFill && !(childLayoutDoc._fitWidth || this.props.childFitWidth?.(d))) {
- return Math.min(d[WidthSym](), maxWidth);
+ if (!this.layoutDoc._columnsFill && !(childLayoutDoc._layout_fitWidth || this.props.childLayoutFitWidth?.(d))) {
+ return Math.min(d[Width](), maxWidth);
}
return maxWidth;
}
getDocHeight(d?: Doc) {
if (!d || d.hidden) return 0;
const childLayoutDoc = Doc.Layout(d, this.props.childLayoutTemplate?.());
- const childDataDoc = !d.isTemplateDoc && !d.isTemplateForField && !d.PARAMS ? undefined : this.props.DataDoc;
+ 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._fitWidth || this.props.childFitWidth?.(d)) ? d[WidthSym]() : 0);
- const nh = Doc.NativeHeight(childLayoutDoc, childDataDoc) || (!(childLayoutDoc._fitWidth || this.props.childFitWidth?.(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);
return Math.min(maxHeight, (docWid * nh) / nw);
}
const childHeight = NumCast(childLayoutDoc._height);
- const panelHeight = childLayoutDoc._fitWidth || this.props.childFitWidth?.(d) ? Number.MAX_SAFE_INTEGER : this.props.PanelHeight() - 2 * this.yMargin;
+ const panelHeight = childLayoutDoc._layout_fitWidth || this.props.childLayoutFitWidth?.(d) ? Number.MAX_SAFE_INTEGER : this.props.PanelHeight() - 2 * this.yMargin;
return Math.min(childHeight, maxHeight, panelHeight);
}
// This following three functions must be from the view Mehek showed
columnDividerDown = (e: React.PointerEvent) => {
runInAction(() => (this._cursor = 'grabbing'));
+ const batch = UndoManager.StartBatch('stacking width');
setupMoveUpEvents(
this,
e,
this.onDividerMove,
- action(() => (this._cursor = 'grab')),
+ action(() => {
+ this._cursor = 'grab';
+ batch.end();
+ }),
emptyFunction
);
};
@@ -457,18 +459,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.context === this.props.Document && de.complete.linkDragData?.linkDragView?.props.CollectionFreeFormDocumentView?.()) {
- const source = Docs.Create.TextDocument('', { _width: 200, _height: 75, _fitWidth: true, title: 'dropped annotation' });
- this.props.addDocument?.(source);
- de.complete.linkDocument = DocUtils.MakeLink({ doc: source }, { doc: de.complete.linkDragData.linkSourceGetAnchor() }, 'doc annotation', ''); // TODODO this is where in text links get passed
+ } 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' });
+ 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;
};
@@ -529,7 +537,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
this.refList.push(ref);
this.observer = new _global.ResizeObserver(
action((entries: any) => {
- if (this.layoutDoc._autoHeight && ref && this.refList.length && !SnappingManager.GetIsDragging()) {
+ if (this.layoutDoc._layout_autoHeight && ref && this.refList.length && !SnappingManager.GetIsDragging()) {
const height = this.headerMargin + Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), Math.max(...this.refList.map(r => Number(getComputedStyle(r).height.replace('px', '')))));
if (!LightboxView.IsLightboxDocView(this.props.docViewPath())) {
this.props.setHeight?.(height);
@@ -542,7 +550,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
}}
addDocument={this.addDocument}
chromeHidden={this.chromeHidden}
- columnHeaders={this.columnHeaders}
+ colHeaderData={this.colHeaderData}
Document={this.props.Document}
DataDoc={this.props.DataDoc}
renderChildren={this.children}
@@ -584,7 +592,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
this.refList.push(ref);
this.observer = new _global.ResizeObserver(
action((entries: any) => {
- if (this.layoutDoc._autoHeight && ref && this.refList.length && !SnappingManager.GetIsDragging()) {
+ 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);
}
@@ -611,9 +619,9 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
/// add a new group category (column) to the active set of note categories. (e.g., if the pivot field is 'transportation', groups might be 'car', 'plane', 'bike', etc)
@action
addGroup = (value: string) => {
- if (value && this.columnHeaders) {
+ if (value && this.colHeaderData) {
const schemaHdrField = new SchemaHeaderField(value);
- this.columnHeaders.push(schemaHdrField);
+ this.colHeaderData.push(schemaHdrField);
return true;
}
return false;
@@ -629,11 +637,13 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
onContextMenu = (e: React.MouseEvent): void => {
// need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout
if (!e.isPropagationStopped()) {
- const subItems: ContextMenuProps[] = [];
- subItems.push({ description: `${this.layoutDoc._columnsFill ? 'Variable Size' : 'Autosize'} Column`, event: () => (this.layoutDoc._columnsFill = !this.layoutDoc._columnsFill), icon: 'plus' });
- subItems.push({ description: `${this.layoutDoc._autoHeight ? 'Variable Height' : 'Auto Height'}`, event: () => (this.layoutDoc._autoHeight = !this.layoutDoc._autoHeight), icon: 'plus' });
- subItems.push({ description: 'Clear All', event: () => (this.dataDoc.data = new List([])), icon: 'times' });
- ContextMenu.Instance.addItem({ description: 'Options...', subitems: subItems, icon: 'eye' });
+ const cm = ContextMenu.Instance;
+ const options = cm.findByDescription('Options...');
+ const optionItems: ContextMenuProps[] = options && 'subitems' in options ? options.subitems : [];
+ optionItems.push({ description: `${this.layoutDoc._columnsFill ? 'Variable Size' : 'Autosize'} Column`, event: () => (this.layoutDoc._columnsFill = !this.layoutDoc._columnsFill), icon: 'plus' });
+ optionItems.push({ description: `${this.layoutDoc._layout_autoHeight ? 'Variable Height' : 'Auto Height'}`, event: () => (this.layoutDoc._layout_autoHeight = !this.layoutDoc._layout_autoHeight), icon: 'plus' });
+ optionItems.push({ description: 'Clear All', event: () => (this.dataDoc[this.fieldKey ?? 'data'] = new List([])), icon: 'times' });
+ !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'compass' });
}
};
@@ -648,8 +658,9 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
return sections.map((section, i) => (this.isStackingView ? this.sectionStacking(section[0], section[1]) : this.sectionMasonry(section[0], section[1], i === 0)));
}
+ 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);
@@ -660,28 +671,27 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
<DocumentView
Document={menuDoc}
DataDoc={menuDoc}
- isContentActive={this.props.isContentActive}
- isDocumentActive={returnTrue}
+ isContentActive={this.isContentActive}
+ isDocumentActive={this.isContentActive}
addDocument={this.props.addDocument}
moveDocument={this.props.moveDocument}
addDocTab={this.props.addDocTab}
+ onBrowseClick={this.props.onBrowseClick}
pinToPres={emptyFunction}
rootSelected={this.props.isSelected}
removeDocument={this.props.removeDocument}
ScreenToLocalTransform={Transform.Identity}
- PanelWidth={() => 35}
- PanelHeight={() => 35}
+ PanelWidth={this.return35}
+ PanelHeight={this.return35}
renderDepth={this.props.renderDepth}
focus={emptyFunction}
styleProvider={this.props.styleProvider}
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}
- ContainingCollectionView={undefined}
- ContainingCollectionDoc={undefined}
/>
</div>
);
@@ -700,7 +710,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
}
@computed get backgroundEvents() {
- return SnappingManager.GetIsDragging();
+ return this.props.isContentActive() === false ? 'none' : undefined;
}
observer: any;
render() {
@@ -710,8 +720,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 ? (
@@ -725,28 +735,20 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
className={this.isStackingView ? 'collectionStackingView' : 'collectionMasonryView'}
ref={this.createRef}
style={{
- overflowY: this.props.isContentActive() ? 'auto' : 'hidden',
+ overflowY: this.isContentActive() ? 'auto' : 'hidden',
background: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor),
- pointerEvents: 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)}
onContextMenu={this.onContextMenu}
- onWheel={e => this.props.isContentActive(true) && e.stopPropagation()}>
+ onWheel={e => this.isContentActive() && e.stopPropagation()}>
{this.renderedSections}
{!this.showAddAGroup ? null : (
<div key={`${this.props.Document[Id]}-addGroup`} className="collectionStackingView-addGroupButton" style={{ width: !this.isStackingView ? '100%' : this.columnWidth / this.numGroupColumns - 10, marginTop: 10 }}>
<EditableView {...editableViewProps} />
</div>
)}
- {/* {this.chromeHidden || !this.props.isSelected() ? (null) :
- <Switch
- onChange={this.onToggle}
- onClick={this.onToggle}
- defaultChecked={true}
- checkedChildren="edit"
- unCheckedChildren="view"
- />} */}
</div>
</div>
</>
diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
index 7b268cd49..ebb4ba5a1 100644
--- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
+++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
@@ -1,12 +1,12 @@
import React = require('react');
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, observable } from 'mobx';
+import { action, computed, IReactionDisposer, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import { Doc, DocListCast, Opt } from '../../../fields/Doc';
import { RichTextField } from '../../../fields/RichTextField';
import { PastelSchemaPalette, SchemaHeaderField } from '../../../fields/SchemaHeaderField';
import { ScriptField } from '../../../fields/ScriptField';
-import { Cast, NumCast, StrCast } from '../../../fields/Types';
+import { BoolCast, Cast, NumCast, StrCast } from '../../../fields/Types';
import { ImageField } from '../../../fields/URLField';
import { TraceMobx } from '../../../fields/util';
import { emptyFunction, setupMoveUpEvents, returnFalse, returnEmptyString } from '../../../Utils';
@@ -34,7 +34,7 @@ interface CSVFieldColumnProps {
heading: string;
pivotField: string;
chromeHidden?: boolean;
- columnHeaders: SchemaHeaderField[] | undefined;
+ colHeaderData: SchemaHeaderField[] | undefined;
headingObject: SchemaHeaderField | undefined;
yMargin: number;
columnWidth: number;
@@ -56,6 +56,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
@observable private _background = 'inherit';
private dropDisposer?: DragManager.DragDropDisposer;
+ private _disposers: { [name: string]: IReactionDisposer } = {};
private _headerRef: React.RefObject<HTMLDivElement> = React.createRef();
@observable _paletteOn = false;
@@ -75,7 +76,16 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
}
};
+ @action
+ componentDidMount() {
+ this._disposers.collapser = reaction(
+ () => this.props.headingObject?.collapsed,
+ collapsed => (this.collapsed = collapsed !== undefined ? BoolCast(collapsed) : false),
+ { fireImmediately: true }
+ );
+ }
componentWillUnmount() {
+ this._disposers.collapser?.();
this.props.unobserveHeight(this._ele);
}
@@ -83,7 +93,8 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
@undoBatch
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));
+ 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);
@@ -97,7 +108,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
headingChanged = (value: string, shiftDown?: boolean) => {
const castedValue = this.getValue(value);
if (castedValue) {
- if (this.props.columnHeaders?.map(i => i.heading).indexOf(castedValue.toString()) !== -1) {
+ if (this.props.colHeaderData?.map(i => i.heading).indexOf(castedValue.toString()) !== -1) {
return false;
}
this.props.docList.forEach(d => (d[this.props.pivotField] = castedValue));
@@ -118,13 +129,13 @@ 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) => {
if (!value && !forceEmptyNote) return false;
const key = this.props.pivotField;
- const newDoc = Docs.Create.TextDocument(value, { _height: 18, _width: 200, _fitWidth: true, title: value, _autoHeight: true });
+ const newDoc = Docs.Create.TextDocument(value, { _height: 18, _width: 200, _layout_fitWidth: true, title: value, _layout_autoHeight: true });
newDoc[key] = this.getValue(this.props.heading);
const maxHeading = this.props.docList.reduce((maxHeading, doc) => (NumCast(doc.heading) > maxHeading ? NumCast(doc.heading) : maxHeading), 0);
const heading = maxHeading === 0 || this.props.docList.length === 0 ? 1 : maxHeading === 1 ? 2 : 3;
@@ -137,31 +148,31 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
@action
deleteColumn = () => {
this.props.docList.forEach(d => (d[this.props.pivotField] = undefined));
- if (this.props.columnHeaders && this.props.headingObject) {
- const index = this.props.columnHeaders.indexOf(this.props.headingObject);
- this.props.columnHeaders.splice(index, 1);
+ if (this.props.colHeaderData && this.props.headingObject) {
+ const index = this.props.colHeaderData.indexOf(this.props.headingObject);
+ this.props.colHeaderData.splice(index, 1);
}
};
@action
collapseSection = () => {
this.props.headingObject?.setCollapsed(!this.props.headingObject.collapsed);
- this.toggleVisibility();
+ this.collapsed = BoolCast(this.props.headingObject?.collapsed);
};
headerDown = (e: React.PointerEvent<HTMLDivElement>) => setupMoveUpEvents(this, e, this.startDrag, emptyFunction, emptyFunction);
//TODO: I think this is where I'm supposed to edit stuff
startDrag = (e: PointerEvent, down: number[], delta: number[]) => {
- // is MakeAlias a way to make a copy of a doc without rendering it?
- const alias = Doc.MakeAlias(this.props.Document);
- alias._width = this.props.columnWidth / (this.props.columnHeaders?.length || 1);
- alias._pivotField = undefined;
+ // is MakeEmbedding a way to make a copy of a doc without rendering it?
+ const embedding = Doc.MakeEmbedding(this.props.Document);
+ embedding._width = this.props.columnWidth / (this.props.colHeaderData?.length || 1);
+ embedding._pivotField = undefined;
let value = this.getValue(this._heading);
value = typeof value === 'string' ? `"${value}"` : value;
- alias.viewSpecScript = ScriptField.MakeFunction(`doc.${this.props.pivotField} === ${value}`, { doc: Doc.name });
- if (alias.viewSpecScript) {
- DragManager.StartDocumentDrag([this._headerRef.current!], new DragManager.DocumentDragData([alias]), e.clientX, e.clientY);
+ embedding.viewSpecScript = ScriptField.MakeFunction(`doc.${this.props.pivotField} === ${value}`, { doc: Doc.name });
+ if (embedding.viewSpecScript) {
+ DragManager.StartDocumentDrag([this._headerRef.current!], new DragManager.DocumentDragData([embedding]), e.clientX, e.clientY);
return true;
}
return false;
@@ -256,7 +267,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
!Doc.noviceMode && ContextMenu.Instance.addItem({ description: 'Containers ...', subitems: layoutItems, icon: 'eye' });
ContextMenu.Instance.setDefaultItem('::', (name: string): void => {
Doc.GetProto(this.props.Document)[name] = '';
- const created = Docs.Create.TextDocument('', { title: name, _width: 250, _autoHeight: true });
+ const created = Docs.Create.TextDocument('', { title: name, _width: 250, _layout_autoHeight: true });
if (created) {
if (this.props.Document.isTemplateDoc) {
Doc.MakeMetadataFieldTemplate(created, this.props.Document);
@@ -275,7 +286,8 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
const heading = this._heading;
const columnYMargin = this.props.headingObject ? 0 : this.props.yMargin;
const uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx);
- const evContents = heading ? heading : this.props?.type === 'number' ? '0' : `NO ${key.toUpperCase()} VALUE`;
+ const noValueHeader = `NO ${key.toUpperCase()} VALUE`;
+ const evContents = heading ? heading : this.props?.type === 'number' ? '0' : noValueHeader;
const headingView = this.props.headingObject ? (
<div
key={heading}
@@ -285,29 +297,24 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
marginTop: this.props.yMargin,
width: this.props.columnWidth / (uniqueHeadings.length + (this.props.chromeHidden ? 0 : 1) || 1),
}}>
- <div className={'collectionStackingView-collapseBar' + (this.props.headingObject.collapsed === true ? ' active' : '')} onClick={this.collapseSection}></div>
{/* the default bucket (no key value) has a tooltip that describes what it is.
Further, it does not have a color and cannot be deleted. */}
<div
className="collectionStackingView-sectionHeader-subCont"
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 : 'inherit' }}>
- <EditableView GetValue={() => evContents} SetValue={this.headingChanged} contents={evContents} oneLine={true} toggle={this.toggleVisibility} />
- {evContents === `NO ${key.toUpperCase()} VALUE` ? null : (
- <div className="collectionStackingView-sectionColor">
- <button className="collectionStackingView-sectionColorButton" onClick={action(e => (this._paletteOn = !this._paletteOn))}>
- <FontAwesomeIcon icon="palette" size="lg" />
- </button>
- {this._paletteOn ? this.renderColorPicker() : null}
- </div>
- )}
- {
- <button className="collectionStackingView-sectionDelete" onClick={this.deleteColumn}>
- <FontAwesomeIcon icon="trash" size="lg" />
+ title={evContents === noValueHeader ? `Documents that don't have a ${key} value will go here. This column cannot be removed.` : ''}
+ style={{ background: evContents !== noValueHeader ? this._color : 'inherit' }}>
+ <EditableView GetValue={() => evContents} SetValue={this.headingChanged} contents={evContents} oneLine={true} />
+ <div className="collectionStackingView-sectionColor" style={{ display: evContents === noValueHeader ? 'none' : undefined }}>
+ <button className="collectionStackingView-sectionColorButton" onClick={action(e => (this._paletteOn = !this._paletteOn))}>
+ <FontAwesomeIcon icon="palette" size="lg" />
</button>
- }
- {evContents === `NO ${key.toUpperCase()} VALUE` ? null : (
+ {this._paletteOn ? this.renderColorPicker() : null}
+ </div>
+ <button className="collectionStackingView-sectionDelete" onClick={this.deleteColumn}>
+ <FontAwesomeIcon icon="trash" size="lg" />
+ </button>
+ {/* {evContents === noValueHeader ? null : (
<div className="collectionStackingView-sectionOptions">
<Flyout anchorPoint={anchorPoints.TOP_RIGHT} content={this.renderMenu()}>
<button className="collectionStackingView-sectionOptionButton">
@@ -315,8 +322,13 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
</button>
</Flyout>
</div>
- )}
+ )} */}
</div>
+ <div
+ className={'collectionStackingView-collapseBar' + (this.props.headingObject.collapsed === true ? ' active' : '')}
+ style={{ display: this.props.headingObject.collapsed === true ? 'block' : undefined }}
+ onClick={this.collapseSection}
+ />
</div>
) : null;
const templatecols = `${this.props.columnWidth / this.props.numGroupColumns}px `;
@@ -348,14 +360,13 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
//TODO: would be great if there was additional space beyond the frame, so that you can actually see your
// bottom note
//TODO: ok, so we are using a single column, and this is it!
- <div key={`${heading}-add-document`} className="collectionStackingView-addDocumentButton" style={{ width: this.props.columnWidth / this.props.numGroupColumns, marginBottom: 10, marginLeft: 25 }}>
+ <div key={`${heading}-add-document`} className="collectionStackingView-addDocumentButton" style={{ width: 'calc(100% - 25px)', maxWidth: this.props.columnWidth / this.props.numGroupColumns - 25, marginBottom: 10 }}>
<EditableView
GetValue={returnEmptyString}
SetValue={this.addNewTextDoc}
- textCallback={this.textCallback}
+ textCallback={this.typedNote}
placeholder={"Type ':' for commands"}
contents={<FontAwesomeIcon icon={'plus'} />}
- toggle={this.toggleVisibility}
menuCallback={this.menuCallback}
/>
</div>
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 7bc273d7d..39fb2db1e 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -1,12 +1,11 @@
import { action, computed, observable } from 'mobx';
-import ReactLoading from 'react-loading';
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';
@@ -20,7 +19,6 @@ import { DocComponent } from '../DocComponent';
import React = require('react');
export interface SubCollectionViewProps extends CollectionViewProps {
- CollectionView: Opt<CollectionView>;
isAnyChildContentActive: () => boolean;
}
@@ -30,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?.();
@@ -67,10 +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() {
- // 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 }[] {
@@ -79,20 +74,20 @@ export function CollectionSubView<X>(moreProps?: X) {
.map(doc => Doc.GetLayoutDataDocPair(Document, !this.props.isAnnotationOverlay ? DataDoc : undefined, doc))
.filter(pair => {
// filter out any documents that have a proto that we don't have permissions to
- return pair.layout && (!pair.layout.proto || (pair.layout.proto instanceof Doc && GetEffectiveAcl(pair.layout.proto) !== AclPrivate));
+ return !pair.layout?.hidden && pair.layout && (!pair.layout.proto || (pair.layout.proto instanceof Doc && GetEffectiveAcl(pair.layout.proto) !== AclPrivate));
});
return validPairs.map(({ data, layout }) => ({ data: data as Doc, layout: layout! })); // this mapping is a bit of a hack to coerce types
}
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();
@@ -111,41 +106,41 @@ export function CollectionSubView<X>(moreProps?: X) {
rawdocs = rootDoc && !this.props.isAnnotationOverlay ? [Doc.GetProto(rootDoc)] : [];
}
- const docs = rawdocs.filter(d => !(d instanceof Promise) && GetEffectiveAcl(Doc.GetProto(d)) !== AclPrivate).map(d => d as Doc);
- const viewSpecScript = Cast(this.props.Document.viewSpecScript, ScriptField);
- const childDocs = viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs;
+ 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, viewSpecScript, 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, viewSpecScript, 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');
- const data = d[annos ? fieldKey + '-annotations' : fieldKey];
- if (data !== undefined) {
- let subDocs = DocListCast(data);
+ const annos = !Field.toString(Doc.LayoutField(d) as Field).includes(CollectionView.name);
+ const data = d[annos ? fieldKey + '_annotations' : fieldKey];
+ 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, viewSpecScript, 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');
+ 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, viewSpecScript, d).length));
- DocListCast(t[annos ? fieldKey + '-annotations' : fieldKey]).forEach(newdoc => newarray.push(newdoc));
+ 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;
}
@@ -172,8 +167,6 @@ export function CollectionSubView<X>(moreProps?: X) {
// The following conditional detects a recurring bug we've seen on the server
if (proto[Id] === Docs.Prototypes.get(DocumentType.COL)[Id]) {
alert('COLLECTION PROTO CURSOR ISSUE DETECTED! Check console for more info...');
- console.log(doc);
- console.log(proto);
throw new Error(`AHA! You were trying to set a cursor on a collection's proto, which is the original collection proto! Look at the two previously printed lines for document values!`);
}
let cursors = Cast(proto.cursors, listSpec(CursorField));
@@ -192,19 +185,22 @@ 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 'alias', 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.context === 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();
}
}
- addDocument = (doc: Doc | Doc[]) => this.props.addDocument?.(doc) || false;
-
+ addDocument = (doc: Doc | Doc[], annotationKey?: string) => this.props.addDocument?.(doc, annotationKey) || false;
+ removeDocument = (doc: Doc | Doc[], annotationKey?: string) => this.props.removeDocument?.(doc, annotationKey) || false;
+ moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[], annotationKey?: string) => boolean, annotationKey?: string) => this.props.moveDocument?.(doc, targetCollection, addDocument);
@action
protected onInternalDrop(e: Event, de: DragManager.DropEvent): boolean {
const docDragData = de.complete.docDragData;
@@ -218,22 +214,22 @@ 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);
+ added = docDragData.moveDocument(movedDocs, this.rootDoc, canAdd ? this.addDocument : returnFalse);
} 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');
+ !added && e.preventDefault();
e.stopPropagation();
return added;
} else if (de.complete.annoDragData) {
+ e.stopPropagation();
const dropCreator = de.complete.annoDragData.dropDocCreator;
de.complete.annoDragData.dropDocCreator = () => {
const dropped = dropCreator(this.props.isAnnotationOverlay ? this.rootDoc : undefined);
@@ -271,10 +267,10 @@ export function CollectionSubView<X>(moreProps?: X) {
if (FormattedTextBox.IsFragment(html)) {
const href = FormattedTextBox.GetHref(html);
if (href) {
- const docid = FormattedTextBox.GetDocFromUrl(href);
- if (docid) {
+ const docId = FormattedTextBox.GetDocFromUrl(href);
+ if (docId) {
// prosemirror text containing link to dash document
- DocServer.GetRefField(docid).then(f => {
+ DocServer.GetRefField(docId).then(f => {
if (f instanceof Doc) {
if (options.x || options.y) {
f.x = options.x as number;
@@ -287,7 +283,7 @@ export function CollectionSubView<X>(moreProps?: X) {
addDocument(Docs.Create.WebDocument(href, { ...options, title: href }));
}
} else if (text) {
- addDocument(Docs.Create.TextDocument(text, { ...options, _showTitle: StrCast(Doc.UserDoc().showTitle), _width: 100, _height: 25 }));
+ addDocument(Docs.Create.TextDocument(text, { ...options, _layout_showTitle: StrCast(Doc.UserDoc().layout_showTitle), _width: 100, _height: 25 }));
}
return;
}
@@ -313,8 +309,8 @@ export function CollectionSubView<X>(moreProps?: X) {
} else {
const path = window.location.origin + '/doc/';
if (text.startsWith(path)) {
- const docid = text.replace(Doc.globalServerPath(), '').split('?')[0];
- DocServer.GetRefField(docid).then(f => {
+ const docId = text.replace(Doc.globalServerPath(), '').split('?')[0];
+ DocServer.GetRefField(docId).then(f => {
if (f instanceof Doc) {
if (options.x || options.y) {
f.x = options.x as number;
@@ -336,8 +332,8 @@ export function CollectionSubView<X>(moreProps?: X) {
const iframe = SelectionManager.Views()[0].ContentDiv?.getElementsByTagName('iframe')?.[0];
const focusNode = iframe?.contentDocument?.getSelection()?.focusNode as any;
if (focusNode) {
- const anchor = srcWeb?.ComponentView?.getAnchor?.();
- anchor && DocUtils.MakeLink({ doc: htmlDoc }, { doc: anchor });
+ const anchor = srcWeb?.ComponentView?.getAnchor?.(true);
+ anchor && DocUtils.MakeLink(htmlDoc, anchor, {});
}
}
}
@@ -350,7 +346,7 @@ export function CollectionSubView<X>(moreProps?: X) {
if ((uriList || text).includes('www.youtube.com/watch') || text.includes('www.youtube.com/embed')) {
const batch = UndoManager.StartBatch('youtube upload');
const generatedDocuments: Doc[] = [];
- this.slowLoadDocuments((uriList || text).split('v=')[1].split('&')[0], options, generatedDocuments, text, completed, e.clientX, e.clientY, addDocument).then(batch.end);
+ this.slowLoadDocuments((uriList || text).split('v=')[1].split('&')[0], options, generatedDocuments, text, completed, addDocument).then(batch.end);
return;
}
@@ -372,28 +368,17 @@ export function CollectionSubView<X>(moreProps?: X) {
// }
}
if (uriList) {
- // const existingWebDoc = await Hypothesis.findWebDoc(uriList);
- // if (existingWebDoc) {
- // const alias = Doc.MakeAlias(existingWebDoc);
- // alias.x = options.x;
- // alias.y = options.y;
- // alias._nativeWidth = 850;
- // alias._height = 512;
- // alias._width = 400;
- // addDocument(alias);
- // } else
- {
- const newDoc = Docs.Create.WebDocument(uriList.split('#annotations:')[0], {
+ addDocument(
+ Docs.Create.WebDocument(uriList.split('#annotations:')[0], {
// clean hypothes.is URLs that reference a specific annotation (eg. https://en.wikipedia.org/wiki/Cartoon#annotations:t7qAeNbCEeqfG5972KR2Ig)
...options,
title: uriList.split('#annotations:')[0],
_width: 400,
_height: 512,
_nativeWidth: 850,
- useCors: true,
- });
- addDocument(newDoc);
- }
+ data_useCors: true,
+ })
+ );
return;
}
@@ -433,25 +418,16 @@ export function CollectionSubView<X>(moreProps?: X) {
proto._height = 20;
return proto;
}),
- { _width: 150, _height: 600, title: 'across', backgroundColor: 'white', _singleLine: true }
+ { _width: 150, _height: 600, title: 'across', backgroundColor: 'white', _createDocOnCR: true }
)
);
});
}
}
- this.slowLoadDocuments(files, options, generatedDocuments, text, completed, e.clientX, e.clientY, addDocument).then(batch.end);
+ this.slowLoadDocuments(files, options, generatedDocuments, text, completed, addDocument).then(batch.end);
}
- slowLoadDocuments = async (
- files: File[] | string,
- options: DocumentOptions,
- generatedDocuments: Doc[],
- text: string,
- completed: ((doc: Doc[]) => void) | undefined,
- clientX: number,
- clientY: number,
- addDocument: (doc: Doc | Doc[]) => boolean
- ) => {
+ slowLoadDocuments = async (files: File[] | string, options: DocumentOptions, generatedDocuments: Doc[], text: string, completed: ((doc: Doc[]) => void) | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => {
// create placeholder docs
// inside placeholder docs have some func that
@@ -473,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 3dd9d2d84..192d48c64 100644
--- a/src/client/views/collections/CollectionTimeView.tsx
+++ b/src/client/views/collections/CollectionTimeView.tsx
@@ -1,123 +1,132 @@
-import { toUpper } from "lodash";
-import { action, computed, observable, runInAction } from "mobx";
-import { observer } from "mobx-react";
-import { Doc, Opt, StrListCast } from "../../../fields/Doc";
-import { List } from "../../../fields/List";
-import { ObjectField } from "../../../fields/ObjectField";
-import { RichTextField } from "../../../fields/RichTextField";
-import { listSpec } from "../../../fields/Schema";
-import { ComputedField, ScriptField } from "../../../fields/ScriptField";
-import { Cast, NumCast, StrCast } from "../../../fields/Types";
-import { emptyFunction, returnEmptyString, returnFalse, returnTrue, setupMoveUpEvents } from "../../../Utils";
-import { Docs } from "../../documents/Documents";
-import { DocumentType } from "../../documents/DocumentTypes";
-import { DocumentManager } from "../../util/DocumentManager";
-import { ScriptingGlobals } from "../../util/ScriptingGlobals";
-import { ContextMenu } from "../ContextMenu";
-import { ContextMenuProps } from "../ContextMenuItem";
-import { EditableView } from "../EditableView";
-import { ViewSpecPrefix } from "../nodes/DocumentView";
-import { ViewDefBounds } from "./collectionFreeForm/CollectionFreeFormLayoutEngines";
-import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView";
-import { CollectionSubView } from "./CollectionSubView";
-import "./CollectionTimeView.scss";
-import React = require("react");
+import { toUpper } from 'lodash';
+import { action, computed, observable, runInAction } from 'mobx';
+import { observer } from 'mobx-react';
+import { Doc, Opt, StrListCast } from '../../../fields/Doc';
+import { List } from '../../../fields/List';
+import { ObjectField } from '../../../fields/ObjectField';
+import { RichTextField } from '../../../fields/RichTextField';
+import { listSpec } from '../../../fields/Schema';
+import { ComputedField, ScriptField } from '../../../fields/ScriptField';
+import { Cast, NumCast, StrCast } from '../../../fields/Types';
+import { emptyFunction, returnEmptyString, returnFalse, returnTrue, setupMoveUpEvents } from '../../../Utils';
+import { Docs } from '../../documents/Documents';
+import { DocumentManager } from '../../util/DocumentManager';
+import { ScriptingGlobals } from '../../util/ScriptingGlobals';
+import { ContextMenu } from '../ContextMenu';
+import { ContextMenuProps } from '../ContextMenuItem';
+import { EditableView } from '../EditableView';
+import { DocFocusOptions, DocumentView } from '../nodes/DocumentView';
+import { PresBox } from '../nodes/trails';
+import { computePivotLayout, computeTimelineLayout, ViewDefBounds } from './collectionFreeForm/CollectionFreeFormLayoutEngines';
+import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView';
+import { CollectionSubView } from './CollectionSubView';
+import './CollectionTimeView.scss';
+import React = require('react');
@observer
export class CollectionTimeView extends CollectionSubView() {
_changing = false;
- @observable _layoutEngine = "pivot";
+ @observable _layoutEngine = computePivotLayout.name;
@observable _collapsed: boolean = false;
@observable _childClickedScript: Opt<ScriptField>;
@observable _viewDefDivClick: Opt<ScriptField>;
@observable _focusPivotField: Opt<string>;
- getAnchor = () => {
- const anchor = Docs.Create.HTMLAnchorDocument([], {
- title: ComputedField.MakeFunction(`"${this.pivotField}"])`) as any,
- annotationOn: this.rootDoc
- });
-
- // save view spec information for anchor
- const proto = Doc.GetProto(anchor);
- proto.pivotField = this.pivotField;
- proto.docFilters = ObjectField.MakeCopy(this.layoutDoc._docFilters as ObjectField) || new List<string>([]);
- proto.docRangeFilters = ObjectField.MakeCopy(this.layoutDoc._docRangeFilters as ObjectField) || new List<string>([]);
- proto[ViewSpecPrefix + "_viewType"] = this.layoutDoc._viewType;
-
- // store anchor in annotations list of document (not technically needed since these anchors are never drawn)
- if (Cast(this.dataDoc[this.props.fieldKey + "-annotations"], listSpec(Doc), null) !== undefined) {
- Cast(this.dataDoc[this.props.fieldKey + "-annotations"], listSpec(Doc), []).push(anchor);
- } else {
- this.dataDoc[this.props.fieldKey + "-annotations"] = new List<Doc>([anchor]);
- }
- return anchor;
- }
-
async componentDidMount() {
this.props.setContentView?.(this);
- //const detailView = (await DocCastAsync(this.props.Document.childClickedOpenTemplateView)) || DocUtils.findTemplate("detailView", StrCast(this.rootDoc.type), "");
- ///const childText = "const alias = getAlias(self); switchView(alias, detailView); alias.dropAction='alias'; alias.removeDropProperties=new List<string>(['dropAction']); useRightSplit(alias, shiftKey); ";
runInAction(() => {
- this._childClickedScript = ScriptField.MakeScript("openInLightbox(self)", { this: Doc.name });
- this._viewDefDivClick = ScriptField.MakeScript("pivotColumnClick(this,payload)", { payload: "any" });
+ this._childClickedScript = ScriptField.MakeScript('openInLightbox(self)', { this: Doc.name });
+ this._viewDefDivClick = ScriptField.MakeScript('pivotColumnClick(this,payload)', { payload: 'any' });
});
}
- get pivotField() { return this._focusPivotField || StrCast(this.layoutDoc._pivotField); }
- @action
- setViewSpec = (anchor: Doc, preview: boolean) => {
- if (preview) { // if in preview, then override document's fields with view spec
- this._focusFilters = StrListCast(Doc.GetProto(anchor).docFilters);
- this._focusRangeFilters = StrListCast(Doc.GetProto(anchor).docRangeFilters);
- this._focusPivotField = StrCast(anchor.pivotField);
- } else if (anchor.pivotField !== undefined) { // otherwise set document's fields based on anchor view spec
- this.layoutDoc._prevFilterIndex = 1;
- this.layoutDoc._pivotField = StrCast(anchor.pivotField);
- this.layoutDoc._docFilters = new List<string>(StrListCast(anchor.docFilters));
- this.layoutDoc._docRangeFilters = new List<string>(StrListCast(anchor.docRangeFilters));
- }
- return 0;
+ get pivotField() {
+ return this._focusPivotField || StrCast(this.layoutDoc._pivotField);
}
+ getAnchor = (addAsAnnotation: boolean) => {
+ const anchor = Docs.Create.HTMLMarkerDocument([], {
+ title: ComputedField.MakeFunction(`"${this.pivotField}"])`) as any,
+ annotationOn: 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
+ if (Cast(this.dataDoc[this.props.fieldKey + '_annotations'], listSpec(Doc), null) !== undefined) {
+ Cast(this.dataDoc[this.props.fieldKey + '_annotations'], listSpec(Doc), []).push(anchor);
+ } else {
+ this.dataDoc[this.props.fieldKey + '_annotations'] = new List<Doc>([anchor]);
+ }
+ }
+ return anchor;
+ };
+
+ @action
+ scrollPreview = (docView: DocumentView, anchor: Doc, focusSpeed: number, options: DocFocusOptions) => {
+ // if in preview, then override document's fields with view spec
+ this._focusFilters = StrListCast(anchor.presDocFilters);
+ this._focusRangeFilters = StrListCast(anchor.presPinDocRangeFilters);
+ this._focusPivotField = StrCast(anchor.presPivotField);
+ return undefined;
+ };
+
layoutEngine = () => this._layoutEngine;
- toggleVisibility = action(() => this._collapsed = !this._collapsed);
+ toggleVisibility = action(() => (this._collapsed = !this._collapsed));
onMinDown = (e: React.PointerEvent) => {
- setupMoveUpEvents(this, e, action((e: PointerEvent, down: number[], delta: number[]) => {
- const minReq = NumCast(this.props.Document[this.props.fieldKey + "-timelineMinReq"], NumCast(this.props.Document[this.props.fieldKey + "-timelineMin"], 0));
- const maxReq = NumCast(this.props.Document[this.props.fieldKey + "-timelineMaxReq"], NumCast(this.props.Document[this.props.fieldKey + "-timelineMax"], 10));
- this.props.Document[this.props.fieldKey + "-timelineMinReq"] = minReq + (maxReq - minReq) * delta[0] / this.props.PanelWidth();
- this.props.Document[this.props.fieldKey + "-timelineSpan"] = undefined;
- return false;
- }), returnFalse, emptyFunction);
- }
+ setupMoveUpEvents(
+ this,
+ e,
+ action((e: PointerEvent, down: number[], delta: number[]) => {
+ const minReq = NumCast(this.props.Document[this.props.fieldKey + '-timelineMinReq'], NumCast(this.props.Document[this.props.fieldKey + '-timelineMin'], 0));
+ const maxReq = NumCast(this.props.Document[this.props.fieldKey + '-timelineMaxReq'], NumCast(this.props.Document[this.props.fieldKey + '-timelineMax'], 10));
+ this.props.Document[this.props.fieldKey + '-timelineMinReq'] = minReq + ((maxReq - minReq) * delta[0]) / this.props.PanelWidth();
+ this.props.Document[this.props.fieldKey + '-timelineSpan'] = undefined;
+ return false;
+ }),
+ returnFalse,
+ emptyFunction
+ );
+ };
onMaxDown = (e: React.PointerEvent) => {
- setupMoveUpEvents(this, e, action((e: PointerEvent, down: number[], delta: number[]) => {
- const minReq = NumCast(this.props.Document[this.props.fieldKey + "-timelineMinReq"], NumCast(this.props.Document[this.props.fieldKey + "-timelineMin"], 0));
- const maxReq = NumCast(this.props.Document[this.props.fieldKey + "-timelineMaxReq"], NumCast(this.props.Document[this.props.fieldKey + "-timelineMax"], 10));
- this.props.Document[this.props.fieldKey + "-timelineMaxReq"] = maxReq + (maxReq - minReq) * delta[0] / this.props.PanelWidth();
- return false;
- }), returnFalse, emptyFunction);
- }
+ setupMoveUpEvents(
+ this,
+ e,
+ action((e: PointerEvent, down: number[], delta: number[]) => {
+ const minReq = NumCast(this.props.Document[this.props.fieldKey + '-timelineMinReq'], NumCast(this.props.Document[this.props.fieldKey + '-timelineMin'], 0));
+ const maxReq = NumCast(this.props.Document[this.props.fieldKey + '-timelineMaxReq'], NumCast(this.props.Document[this.props.fieldKey + '-timelineMax'], 10));
+ this.props.Document[this.props.fieldKey + '-timelineMaxReq'] = maxReq + ((maxReq - minReq) * delta[0]) / this.props.PanelWidth();
+ return false;
+ }),
+ returnFalse,
+ emptyFunction
+ );
+ };
onMidDown = (e: React.PointerEvent) => {
- setupMoveUpEvents(this, e, action((e: PointerEvent, down: number[], delta: number[]) => {
- const minReq = NumCast(this.props.Document[this.props.fieldKey + "-timelineMinReq"], NumCast(this.props.Document[this.props.fieldKey + "-timelineMin"], 0));
- const maxReq = NumCast(this.props.Document[this.props.fieldKey + "-timelineMaxReq"], NumCast(this.props.Document[this.props.fieldKey + "-timelineMax"], 10));
- this.props.Document[this.props.fieldKey + "-timelineMinReq"] = minReq - (maxReq - minReq) * delta[0] / this.props.PanelWidth();
- this.props.Document[this.props.fieldKey + "-timelineMaxReq"] = maxReq - (maxReq - minReq) * delta[0] / this.props.PanelWidth();
- return false;
- }), returnFalse, emptyFunction);
- }
+ setupMoveUpEvents(
+ this,
+ e,
+ action((e: PointerEvent, down: number[], delta: number[]) => {
+ const minReq = NumCast(this.props.Document[this.props.fieldKey + '-timelineMinReq'], NumCast(this.props.Document[this.props.fieldKey + '-timelineMin'], 0));
+ const maxReq = NumCast(this.props.Document[this.props.fieldKey + '-timelineMaxReq'], NumCast(this.props.Document[this.props.fieldKey + '-timelineMax'], 10));
+ this.props.Document[this.props.fieldKey + '-timelineMinReq'] = minReq - ((maxReq - minReq) * delta[0]) / this.props.PanelWidth();
+ this.props.Document[this.props.fieldKey + '-timelineMaxReq'] = maxReq - ((maxReq - minReq) * delta[0]) / this.props.PanelWidth();
+ return false;
+ }),
+ returnFalse,
+ emptyFunction
+ );
+ };
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._pivotField = this.layoutDoc['_prevPivotFields' + prevFilterIndex];
+ 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;
- }
+ };
@action
contentsDown = (e: React.MouseEvent) => {
@@ -125,39 +134,58 @@ export class CollectionTimeView extends CollectionSubView() {
if (prevFilterIndex > 0) {
this.goTo(prevFilterIndex - 1);
} else {
- this.layoutDoc._docFilters = new List([]);
+ this.layoutDoc._childFilters = new List([]);
}
- }
+ };
- dontScaleFilter = (doc: Doc) => doc.type === DocumentType.RTF;
@computed get contents() {
- return <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 }}
- fitContentsToBox={returnTrue}
- childClickScript={this._childClickedScript}
- viewDefDivClick={this._viewDefDivClick}
- //dontScaleFilter={this.dontScaleFilter}
- layoutEngine={this.layoutEngine} />
- </div>;
+ return (
+ <div className="collectionTimeView-innards" key="timeline" style={{ pointerEvents: this.props.isContentActive() ? undefined : 'none' }} onClick={this.contentsDown}>
+ <CollectionFreeFormView
+ {...this.props}
+ engineProps={{ pivotField: this.pivotField, childFilters: this.childDocFilters, childFiltersByRanges: this.childDocRangeFilters }}
+ fitContentsToBox={returnTrue}
+ childClickScript={this._childClickedScript}
+ viewDefDivClick={this.layoutEngine() === computeTimelineLayout.name ? undefined : this._viewDefDivClick}
+ layoutEngine={this.layoutEngine}
+ />
+ </div>
+ );
}
public static SyncTimelineToPresentation(doc: Doc) {
const fieldKey = Doc.LayoutFieldKey(doc);
- doc[fieldKey + "-timelineCur"] = ComputedField.MakeFunction("(activePresentationItem()[this._pivotField || 'year'] || 0)");
+ doc[fieldKey + '-timelineCur'] = ComputedField.MakeFunction("(activePresentationItem()[this._pivotField || 'year'] || 0)");
}
specificMenu = (e: React.MouseEvent) => {
const layoutItems: ContextMenuProps[] = [];
const doc = this.layoutDoc;
- layoutItems.push({ description: "Force Timeline", event: () => { doc._forceRenderEngine = "timeline"; }, icon: "compress-arrows-alt" });
- layoutItems.push({ description: "Force Pivot", event: () => { doc._forceRenderEngine = "pivot"; }, icon: "compress-arrows-alt" });
- layoutItems.push({ description: "Auto Time/Pivot layout", event: () => { doc._forceRenderEngine = undefined; }, icon: "compress-arrows-alt" });
- layoutItems.push({ description: "Sync with presentation", event: () => CollectionTimeView.SyncTimelineToPresentation(doc), icon: "compress-arrows-alt" });
+ layoutItems.push({
+ description: 'Force Timeline',
+ event: () => {
+ doc._forceRenderEngine = computeTimelineLayout.name;
+ },
+ icon: 'compress-arrows-alt',
+ });
+ layoutItems.push({
+ description: 'Force Pivot',
+ event: () => {
+ doc._forceRenderEngine = computePivotLayout.name;
+ },
+ icon: 'compress-arrows-alt',
+ });
+ layoutItems.push({
+ description: 'Auto Time/Pivot layout',
+ event: () => {
+ doc._forceRenderEngine = undefined;
+ },
+ icon: 'compress-arrows-alt',
+ });
+ layoutItems.push({ description: 'Sync with presentation', event: () => CollectionTimeView.SyncTimelineToPresentation(doc), icon: 'compress-arrows-alt' });
- ContextMenu.Instance.addItem({ description: "Options...", subitems: layoutItems, icon: "eye" });
- }
+ ContextMenu.Instance.addItem({ description: 'Options...', subitems: layoutItems, icon: 'eye' });
+ };
@computed get _allFacets() {
const facets = new Set<string>();
this.childDocs.forEach(child => Object.keys(Doc.GetProto(child)).forEach(key => facets.add(key)));
@@ -169,37 +197,39 @@ export class CollectionTimeView extends CollectionSubView() {
const docItems: ContextMenuProps[] = [];
const keySet: Set<string> = new Set();
- this.childLayoutPairs.map(pair => this._allFacets.filter(fieldKey =>
- pair.layout[fieldKey] instanceof RichTextField ||
- typeof (pair.layout[fieldKey]) === "number" ||
- typeof (pair.layout[fieldKey]) === "boolean" ||
- typeof (pair.layout[fieldKey]) === "string").filter(fieldKey => fieldKey[0] !== "_" && (fieldKey[0] !== "#" || fieldKey === "#") && (fieldKey === "tags" || fieldKey[0] === toUpper(fieldKey)[0])).map(fieldKey => keySet.add(fieldKey)));
- Array.from(keySet).map(fieldKey =>
- docItems.push({ description: ":" + fieldKey, event: () => this.layoutDoc._pivotField = fieldKey, icon: "compress-arrows-alt" }));
- docItems.push({ description: ":default", event: () => this.layoutDoc._pivotField = undefined, icon: "compress-arrows-alt" });
- ContextMenu.Instance.addItem({ description: "Pivot Fields ...", subitems: docItems, icon: "eye" });
+ this.childLayoutPairs.map(pair =>
+ this._allFacets
+ .filter(fieldKey => pair.layout[fieldKey] instanceof RichTextField || typeof pair.layout[fieldKey] === 'number' || typeof pair.layout[fieldKey] === 'boolean' || typeof pair.layout[fieldKey] === 'string')
+ .filter(fieldKey => fieldKey[0] !== '_' && (fieldKey === 'tags' || fieldKey[0] === toUpper(fieldKey)[0]))
+ .map(fieldKey => keySet.add(fieldKey))
+ );
+ Array.from(keySet).map(fieldKey => docItems.push({ description: ':' + fieldKey, event: () => (this.layoutDoc._pivotField = fieldKey), icon: 'compress-arrows-alt' }));
+ docItems.push({ description: ':default', event: () => (this.layoutDoc._pivotField = undefined), icon: 'compress-arrows-alt' });
+ ContextMenu.Instance.addItem({ description: 'Pivot Fields ...', subitems: docItems, icon: 'eye' });
const pt = this.props.ScreenToLocalTransform().inverse().transformPoint(x, y);
- ContextMenu.Instance.displayMenu(x, y, ":");
- }
+ ContextMenu.Instance.displayMenu(x, y, ':');
+ };
@computed get pivotKeyUI() {
- return <div className={"pivotKeyEntry"}>
- <EditableView
- GetValue={returnEmptyString}
- SetValue={(value: any) => {
- if (value?.length) {
- this.layoutDoc._pivotField = value;
- return true;
- }
- return false;
- }}
- toggle={this.toggleVisibility}
- background={"#f1efeb"} // this.props.headingObject ? this.props.headingObject.color : "#f1efeb";
- contents={":" + StrCast(this.layoutDoc._pivotField)}
- showMenuOnLoad={true}
- display={"inline"}
- menuCallback={this.menuCallback} />
- </div>;
+ return (
+ <div className={'pivotKeyEntry'}>
+ <EditableView
+ GetValue={returnEmptyString}
+ SetValue={(value: any) => {
+ if (value?.length) {
+ this.layoutDoc._pivotField = value;
+ return true;
+ }
+ return false;
+ }}
+ background={'#f1efeb'} // this.props.headingObject ? this.props.headingObject.color : "#f1efeb";
+ contents={':' + StrCast(this.layoutDoc._pivotField)}
+ showMenuOnLoad={true}
+ display={'inline'}
+ menuCallback={this.menuCallback}
+ />
+ </div>
+ );
}
render() {
@@ -211,55 +241,62 @@ export class CollectionTimeView extends CollectionSubView() {
}
});
const forceLayout = StrCast(this.layoutDoc._forceRenderEngine);
- const doTimeline = forceLayout ? (forceLayout === "timeline") : nonNumbers / this.childDocs.length < 0.1 && this.props.PanelWidth() / this.props.PanelHeight() > 6;
- if (doTimeline !== (this._layoutEngine === "timeline")) {
+ const doTimeline = forceLayout ? forceLayout === computeTimelineLayout.name : nonNumbers / this.childDocs.length < 0.1 && this.props.PanelWidth() / this.props.PanelHeight() > 6;
+ if (doTimeline !== (this._layoutEngine === computeTimelineLayout.name)) {
if (!this._changing) {
this._changing = true;
- setTimeout(action(() => {
- this._layoutEngine = doTimeline ? "timeline" : "pivot";
- this._changing = false;
- }), 0);
+ setTimeout(
+ action(() => {
+ this._layoutEngine = doTimeline ? computeTimelineLayout.name : computePivotLayout.name;
+ this._changing = false;
+ }),
+ 0
+ );
}
}
- return <div className={"collectionTimeView" + (doTimeline ? "" : "-pivot")} onContextMenu={this.specificMenu}
- style={{ width: this.props.PanelWidth(), height: "100%" }}>
- {this.pivotKeyUI}
- {this.contents}
- {!this.props.isSelected() || !doTimeline ? (null) : <>
- <div className="collectionTimeView-thumb-min collectionTimeView-thumb" key="min" onPointerDown={this.onMinDown} />
- <div className="collectionTimeView-thumb-max collectionTimeView-thumb" key="mid" onPointerDown={this.onMaxDown} />
- <div className="collectionTimeView-thumb-mid collectionTimeView-thumb" key="max" onPointerDown={this.onMidDown} />
- </>}
- </div>;
+ return (
+ <div className={'collectionTimeView' + (doTimeline ? '' : '-pivot')} onContextMenu={this.specificMenu} style={{ width: this.props.PanelWidth(), height: '100%' }}>
+ {this.pivotKeyUI}
+ {this.contents}
+ {!this.props.isSelected() || !doTimeline ? null : (
+ <>
+ <div className="collectionTimeView-thumb-min collectionTimeView-thumb" key="min" onPointerDown={this.onMinDown} />
+ <div className="collectionTimeView-thumb-max collectionTimeView-thumb" key="mid" onPointerDown={this.onMaxDown} />
+ <div className="collectionTimeView-thumb-mid collectionTimeView-thumb" key="max" onPointerDown={this.onMidDown} />
+ </>
+ )}
+ </div>
+ );
}
}
ScriptingGlobals.add(function pivotColumnClick(pivotDoc: Doc, bounds: ViewDefBounds) {
- const pivotField = StrCast(pivotDoc._pivotField) || "author";
+ 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);
- pivotDoc["_prevPivotFields" + prevFilterIndex] = pivotField;
+ 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();
- setTimeout(action(() => {
- const filterVals = (bounds.payload as string[]);
- filterVals.map(filterVal => Doc.setDocFilter(pivotDoc, pivotField, filterVal, "check"));
- const pivotView = DocumentManager.Instance.getDocumentView(pivotDoc);
- if (pivotDoc && pivotView?.ComponentView instanceof CollectionTimeView && filterVals.length === 1) {
- if (pivotView?.ComponentView.childDocs.length && pivotView.ComponentView.childDocs[0][filterVals[0]]) {
- pivotDoc._pivotField = filterVals[0];
+ pivotDoc._childFilters = new List();
+ setTimeout(
+ action(() => {
+ const filterVals = bounds.payload as string[];
+ filterVals.map(filterVal => Doc.setDocFilter(pivotDoc, pivotField, filterVal, 'check'));
+ const pivotView = DocumentManager.Instance.getDocumentView(pivotDoc);
+ if (pivotDoc && pivotView?.ComponentView instanceof CollectionTimeView && filterVals.length === 1) {
+ if (pivotView?.ComponentView.childDocs.length && pivotView.ComponentView.childDocs[0][filterVals[0]]) {
+ pivotDoc._pivotField = filterVals[0];
+ }
}
- }
- const newFilters = StrListCast(pivotDoc._docFilters);
- if (newFilters.length && originalFilter.length &&
- newFilters.lastElement() === originalFilter.lastElement()) {
- pivotDoc._prevFilterIndex = --prevFilterIndex;
- pivotDoc["_prevDocFilter" + prevFilterIndex] = undefined;
- pivotDoc["_prevDocRangeFilters" + prevFilterIndex] = undefined;
- pivotDoc["_prevPivotFields" + prevFilterIndex] = undefined;
- }
- }));
-}); \ No newline at end of file
+ const newFilters = StrListCast(pivotDoc._childFilters);
+ if (newFilters.length && originalFilter.length && newFilters.lastElement() === originalFilter.lastElement()) {
+ pivotDoc._prevFilterIndex = --prevFilterIndex;
+ pivotDoc['_prevDocFilter' + prevFilterIndex] = undefined;
+ pivotDoc['_prevDocRangeFilters' + prevFilterIndex] = undefined;
+ pivotDoc['_prevPivotFields' + prevFilterIndex] = undefined;
+ }
+ })
+ );
+});
diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss
index 3785b7d61..273b08247 100644
--- a/src/client/views/collections/CollectionTreeView.scss
+++ b/src/client/views/collections/CollectionTreeView.scss
@@ -67,7 +67,8 @@
font-style: italic;
font-size: 8pt;
margin-left: 3px;
- display: none;
+ opacity: 0;
+ pointer-events: none;
}
.collectionTreeView-contents {
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 0ff89c5a7..4cd3885f5 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -1,13 +1,13 @@
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';
import { TraceMobx } from '../../../fields/util';
-import { emptyFunction, OmitKeys, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnOne, returnTrue } from '../../../Utils';
+import { emptyFunction, returnAll, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnNone, returnOne, returnTrue, returnZero } from '../../../Utils';
import { DocUtils } from '../../documents/Documents';
import { DocumentManager } from '../../util/DocumentManager';
import { DragManager, dropActionType } from '../../util/DragManager';
@@ -38,8 +38,8 @@ export type collectionTreeViewProps = {
onCheckedClick?: () => ScriptField;
onChildClick?: () => ScriptField;
// TODO: [AL] add these fields
- AddToMap?: (treeViewDoc: Doc, index: number[]) => Doc[];
- RemFromMap?: (treeViewDoc: Doc, index: number[]) => Doc[];
+ AddToMap?: (treeViewDoc: Doc, index: number[]) => void;
+ RemFromMap?: (treeViewDoc: Doc, index: number[]) => void;
hierarchyIndex?: number[];
};
@@ -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;
@@ -104,7 +103,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
componentDidMount() {
//this.props.setContentView?.(this);
this._disposers.autoheight = reaction(
- () => this.rootDoc.autoHeight,
+ () => this.rootDoc.layout_autoHeight,
auto => auto && this.computeHeight(),
{ fireImmediately: true }
);
@@ -114,25 +113,25 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
if (!this._isDisposing) {
const titleHeight = !this._titleRef ? this.marginTop() : Number(getComputedStyle(this._titleRef).height.replace('px', ''));
const bodyHeight = Array.from(this.refList).reduce((p, r) => p + Number(getComputedStyle(r).height.replace('px', '')), this.marginBot()) + 6;
- this.layoutDoc._autoHeightMargins = bodyHeight;
- this.props.setHeight?.(bodyHeight + titleHeight);
+ this.layoutDoc._layout_autoHeightMargins = bodyHeight;
+ !this.props.dontRegisterView && this.props.setHeight?.(bodyHeight + titleHeight);
}
};
unobserveHeight = (ref: any) => {
this.refList.delete(ref);
- this.rootDoc.autoHeight && this.computeHeight();
+ this.rootDoc.layout_autoHeight && this.computeHeight();
};
observeHeight = (ref: any) => {
if (ref) {
this.refList.add(ref);
this.observer = new _global.ResizeObserver(
action((entries: any) => {
- if (this.rootDoc.autoHeight && ref && this.refList.size && !SnappingManager.GetIsDragging()) {
+ if (this.rootDoc.layout_autoHeight && ref && this.refList.size && !SnappingManager.GetIsDragging()) {
this.computeHeight();
}
})
);
- this.rootDoc.autoHeight && this.computeHeight();
+ this.rootDoc.layout_autoHeight && this.computeHeight();
this.observer.observe(ref);
}
};
@@ -141,11 +140,16 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
if ((this._mainEle = ele)) this._treedropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.doc, this.onInternalPreDrop.bind(this));
};
- protected onInternalPreDrop = (e: Event, de: DragManager.DropEvent, targetAction: dropActionType) => {
+ protected onInternalPreDrop = (e: Event, de: DragManager.DropEvent) => {
+ const dropAction = this.layoutDoc.dropAction as dropActionType;
const dragData = de.complete.docDragData;
if (dragData) {
- const isInTree = () => Doc.AreProtosEqual(dragData.treeViewDoc, this.props.Document) || dragData.draggedDocuments.some(d => d.context === 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;
}
};
@@ -154,17 +158,17 @@ 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.CollectionView)?.select(false);
+ DocumentManager.Instance.getDocumentView(prev, this.props.DocumentView?.())?.select(false);
}
return true;
}
@@ -175,8 +179,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.context = this.props.Document);
+ const res = flg && Doc.AddDocToList(this.doc[DocData], this.props.fieldKey, doc, relativeTo, before);
+ res && (doc.embedContainer = this.props.Document);
return res;
}, true);
if (this.doc.resolvedDataDoc instanceof Promise) return false;
@@ -229,21 +233,19 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
return (
<FormattedTextBox
{...this.props}
- fieldKey={'text'}
+ fieldKey="text"
renderDepth={this.props.renderDepth + 1}
isContentActive={this.isContentActive}
isDocumentActive={this.isContentActive}
rootSelected={returnTrue}
- forceAutoHeight={true} // needed to make the title resize even if the rest of the tree view is not autoHeight
+ forceAutoHeight={true} // needed to make the title resize even if the rest of the tree view is not layout_autoHeight
PanelWidth={this.documentTitleWidth}
PanelHeight={this.documentTitleHeight}
NativeDimScaling={returnOne}
onKey={this.onKey}
- docFilters={returnEmptyFilter}
- docRangeFilters={returnEmptyFilter}
+ childFilters={returnEmptyFilter}
+ childFiltersByRanges={returnEmptyFilter}
searchFilterDocs={returnEmptyDoclist}
- ContainingCollectionDoc={this.doc}
- ContainingCollectionView={this.props.CollectionView}
addDocument={returnFalse}
moveDocument={returnFalse}
removeDocument={returnFalse}
@@ -259,23 +261,25 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
return StrListCast(this.doc.childContextMenuLabels).map((label, i) => ({ script: customScripts[i], filter: customFilters[i], icon: icons[i], label }));
};
headerFields = () => this.props.treeViewHideHeaderFields || BoolCast(this.doc.treeViewHideHeaderFields);
+ @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))));
return TreeView.GetChildElements(
this.treeChildren,
this,
this,
this.doc,
this.props.DataDoc,
- this.props.ContainingCollectionDoc,
+ undefined,
undefined,
addDoc,
this.remove,
moveDoc,
- dropAction,
+ dragAction,
this.props.addDocTab,
this.props.styleProvider,
this.screenToLocalTransform,
@@ -296,7 +300,8 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
//TODO: [AL] add these
this.props.AddToMap,
this.props.RemFromMap,
- this.props.hierarchyIndex
+ this.props.hierarchyIndex,
+ this._renderCount
);
}
@computed get titleBar() {
@@ -312,12 +317,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) }}>
@@ -328,10 +333,10 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
isDocumentActive={returnTrue}
addDocument={this.props.addDocument}
moveDocument={this.props.moveDocument}
+ removeDocument={this.props.removeDocument}
addDocTab={this.props.addDocTab}
pinToPres={emptyFunction}
rootSelected={this.props.isSelected}
- removeDocument={this.props.removeDocument}
ScreenToLocalTransform={Transform.Identity}
PanelWidth={this.return35}
PanelHeight={this.return35}
@@ -341,11 +346,9 @@ 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}
- ContainingCollectionView={undefined}
- ContainingCollectionDoc={undefined}
/>
</div>
);
@@ -369,24 +372,24 @@ 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.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));
- addAnnotationDocument = (doc: Doc | Doc[]) => this.props.CollectionView?.addDocument(doc, `${this.props.fieldKey}-annotations`) || false;
- remAnnotationDocument = (doc: Doc | Doc[]) => this.props.CollectionView?.removeDocument(doc, `${this.props.fieldKey}-annotations`) || false;
+ addAnnotationDocument = (doc: Doc | Doc[]) => this.addDocument(doc, `${this.props.fieldKey}_annotations`) || false;
+ remAnnotationDocument = (doc: Doc | Doc[]) => this.removeDocument(doc, `${this.props.fieldKey}_annotations`) || false;
moveAnnotationDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[], annotationKey?: string) => boolean) =>
- this.props.CollectionView?.moveDocument(doc, targetCollection, addDocument, `${this.props.fieldKey}-annotations`) || false;
+ this.moveDocument(doc, targetCollection, addDocument, `${this.props.fieldKey}_annotations`) || false;
@observable _headerHeight = 0;
- contentFunc = () => {
+ @computed get content() {
const background = () => this.props.styleProvider?.(this.doc, this.props, StyleProp.BackgroundColor);
- const pointerEvents = () => (!this.props.isContentActive() && !SnappingManager.GetIsDragging() ? 'none' : undefined);
+ const pointerEvents = () => (this.props.isContentActive() === false ? 'none' : undefined);
const titleBar = this.props.treeViewHideTitle || this.doc.treeViewHideTitle ? null : this.titleBar;
- return [
- <div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
+ return (
+ <div style={{ display: 'flex', flexDirection: 'column', height: '100%', pointerEvents: 'all' }}>
{!this.buttonMenu && !this.noviceExplainer ? null : (
<div className="documentButtonMenu" ref={action((r: HTMLDivElement | null) => r && (this._headerHeight = Number(getComputedStyle(r).height.replace(/px/, ''))))}>
{this.buttonMenu}
@@ -396,6 +399,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
<div
className="collectionTreeView-contents"
key="tree"
+ ref={r => !this.doc.treeViewHasOverlay && r && this.createTreeDropTarget(r)}
style={{
...(!titleBar ? { marginLeft: this.marginX(), paddingTop: this.marginTop() } : {}),
overflow: 'auto',
@@ -419,39 +423,42 @@ 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>
</div>
- </div>,
- ];
- };
+ </div>
+ );
+ }
render() {
TraceMobx();
- const scale = (this.props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._viewScale, 1) || 1;
+ const scale = (this.props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._freeform_scale, 1) || 1;
return (
<div style={{ transform: `scale(${scale})`, transformOrigin: 'top left', width: `${100 / scale}%`, height: `${100 / scale}%` }}>
{!(this.doc instanceof Doc) || !this.treeChildren ? null : this.doc.treeViewHasOverlay ? (
<CollectionFreeFormView
- {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
+ {...this.props}
+ setContentView={emptyFunction}
+ NativeWidth={returnZero}
+ NativeHeight={returnZero}
+ pointerEvents={this.props.isContentActive() && SnappingManager.GetIsDragging() ? returnAll : returnNone}
isAnnotationOverlay={true}
isAnnotationOverlayScrollable={true}
childDocumentsActive={this.props.isDocumentActive}
- fieldKey={this.props.fieldKey + '-annotations'}
- dropAction={'move'}
+ fieldKey={this.props.fieldKey + '_annotations'}
+ dropAction="move"
select={emptyFunction}
addDocument={this.addAnnotationDocument}
removeDocument={this.remAnnotationDocument}
moveDocument={this.moveAnnotationDocument}
bringToFront={emptyFunction}
renderDepth={this.props.renderDepth + 1}>
- {this.contentFunc}
+ {this.content}
</CollectionFreeFormView>
) : (
- this.contentFunc()
+ this.content
)}
</div>
);
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 9f63a11aa..c33519afb 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -2,7 +2,6 @@ import { computed, observable, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { Doc, DocListCast } from '../../../fields/Doc';
-import { Id } from '../../../fields/FieldSymbols';
import { ObjectField } from '../../../fields/ObjectField';
import { ScriptField } from '../../../fields/ScriptField';
import { BoolCast, Cast, ScriptCast, StrCast } from '../../../fields/Types';
@@ -10,12 +9,13 @@ import { TraceMobx } from '../../../fields/util';
import { returnEmptyString } from '../../../Utils';
import { DocUtils } from '../../documents/Documents';
import { CollectionViewType } from '../../documents/DocumentTypes';
-import { BranchCreate, BranchTask } from '../../documents/Gitlike';
+import { dropActionType } from '../../util/DragManager';
import { ImageUtils } from '../../util/Import & Export/ImageUtils';
import { InteractionUtils } from '../../util/InteractionUtils';
import { ContextMenu } from '../ContextMenu';
import { ContextMenuProps } from '../ContextMenuItem';
import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent';
+import { OpenWhere } from '../nodes/DocumentView';
import { FieldView, FieldViewProps } from '../nodes/FieldView';
import { CollectionCarousel3DView } from './CollectionCarousel3DView';
import { CollectionCarouselView } from './CollectionCarouselView';
@@ -42,18 +42,21 @@ interface CollectionViewProps_ extends FieldViewProps {
layoutEngine?: () => string;
setPreviewCursor?: (func: (x: number, y: number, drag: boolean, hide: boolean) => void) => void;
setBrushViewer?: (func?: (view: { width: number; height: number; panX: number; panY: number }) => void) => void;
+ ignoreUnrendered?: boolean;
// property overrides for child documents
childDocuments?: Doc[]; // used to override the documents shown by the sub collection to an explicit list (see LinkBox)
- childDocumentsActive?: () => boolean; // whether child documents can be dragged if collection can be dragged (eg., in a when a Pile document is in startburst mode)
- childFitWidth?: (child: Doc) => boolean;
- childShowTitle?: () => string;
+ childDocumentsActive?: () => boolean | undefined; // whether child documents can be dragged if collection can be dragged (eg., in a when a Pile document is in startburst mode)
+ childContentsActive?: () => boolean | undefined;
+ childLayoutFitWidth?: (child: Doc) => boolean;
+ childlayout_showTitle?: () => string;
childOpacity?: () => number;
childContextMenuItems?: () => { script: ScriptField; label: string }[];
childHideTitle?: () => boolean; // whether to hide the documentdecorations title for children
childHideDecorationTitle?: () => boolean;
childHideResizeHandles?: () => boolean;
childLayoutTemplate?: () => Doc | undefined; // specify a layout Doc template to use for children of the collection
+ childDragAction?: dropActionType;
childXPadding?: number;
childYPadding?: number;
childLayoutString?: string;
@@ -61,8 +64,8 @@ interface CollectionViewProps_ extends FieldViewProps {
childClickScript?: ScriptField;
childDoubleClickScript?: ScriptField;
//TODO: [AL] add these fields
- AddToMap?: (treeViewDoc: Doc, index: number[]) => Doc[];
- RemFromMap?: (treeViewDoc: Doc, index: number[]) => Doc[];
+ AddToMap?: (treeViewDoc: Doc, index: number[]) => void;
+ RemFromMap?: (treeViewDoc: Doc, index: number[]) => void;
hierarchyIndex?: number[]; // hierarchical index of a document up to the rendering root (primarily used for tree views)
}
export interface CollectionViewProps extends React.PropsWithChildren<CollectionViewProps_> {}
@@ -85,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:
@@ -98,18 +101,6 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
return viewField as any as CollectionViewType;
}
- showIsTagged = () => {
- return null;
- // this section would display an icon in the bototm right of a collection to indicate that all
- // photos had been processed through Google's content analysis API and Google's tags had been
- // assigned to the documents googlePhotosTags field.
- // const children = DocListCast(this.rootDoc[this.props.fieldKey]);
- // const imageProtos = children.filter(doc => Cast(doc.data, ImageField)).map(Doc.GetProto);
- // const allTagged = imageProtos.length > 0 && imageProtos.every(image => image.googlePhotosTags);
- // return !allTagged ? (null) : <img id={"google-tags"} src={"/assets/google_tags.png"} />;
- //this.isContentActive();
- };
-
screenToLocalTransform = () => (this.props.renderDepth ? this.props.ScreenToLocalTransform() : this.props.ScreenToLocalTransform().scale(this.props.PanelWidth() / this.bodyPanelWidth()));
// prettier-ignore
private renderSubView = (type: CollectionViewType | undefined, props: SubCollectionViewProps) => {
@@ -137,26 +128,25 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
}
};
- setupViewTypes(category: string, func: (viewType: CollectionViewType) => Doc, addExtras: boolean) {
- const subItems: ContextMenuProps[] = [];
- subItems.push({ description: 'Freeform', event: () => func(CollectionViewType.Freeform), icon: 'signature' });
- if (addExtras && CollectionView._safeMode) {
- ContextMenu.Instance.addItem({ description: 'Test Freeform', event: () => func(CollectionViewType.Invalid), icon: 'project-diagram' });
- }
- subItems.push({ description: 'Schema', event: () => func(CollectionViewType.Schema), icon: 'th-list' });
- subItems.push({ description: 'Tree', event: () => func(CollectionViewType.Tree), icon: 'tree' });
- subItems.push({ description: 'Stacking', event: () => (func(CollectionViewType.Stacking)._autoHeight = true), icon: 'ellipsis-v' });
- subItems.push({ description: 'Notetaking', event: () => (func(CollectionViewType.NoteTaking)._autoHeight = true), icon: 'ellipsis-v' });
- subItems.push({ description: 'Multicolumn', event: () => func(CollectionViewType.Multicolumn), icon: 'columns' });
- subItems.push({ description: 'Multirow', event: () => func(CollectionViewType.Multirow), icon: 'columns' });
- subItems.push({ description: 'Masonry', event: () => func(CollectionViewType.Masonry), icon: 'columns' });
- subItems.push({ description: 'Carousel', event: () => func(CollectionViewType.Carousel), icon: 'columns' });
- subItems.push({ description: '3D Carousel', event: () => func(CollectionViewType.Carousel3D), icon: 'columns' });
- !Doc.noviceMode && subItems.push({ description: 'Pivot/Time', event: () => func(CollectionViewType.Time), icon: 'columns' });
- !Doc.noviceMode && subItems.push({ description: 'Map', event: () => func(CollectionViewType.Map), icon: 'globe-americas' });
- subItems.push({ description: 'Grid', event: () => func(CollectionViewType.Grid), icon: 'th-list' });
+ 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' },
+ { description: 'Schema', event: () => func(CollectionViewType.Schema), icon: 'th-list' },
+ { description: 'Tree', event: () => func(CollectionViewType.Tree), icon: 'tree' },
+ { description: 'Stacking', event: () => (func(CollectionViewType.Stacking)._layout_autoHeight = true), icon: 'ellipsis-v' },
+ { description: 'Notetaking', event: () => (func(CollectionViewType.NoteTaking)._layout_autoHeight = true), icon: 'ellipsis-v' },
+ { description: 'Multicolumn', event: () => func(CollectionViewType.Multicolumn), icon: 'columns' },
+ { description: 'Multirow', event: () => func(CollectionViewType.Multirow), icon: 'columns' },
+ { description: 'Masonry', event: () => func(CollectionViewType.Masonry), icon: 'columns' },
+ { description: 'Carousel', event: () => func(CollectionViewType.Carousel), icon: 'columns' },
+ { description: '3D Carousel', event: () => func(CollectionViewType.Carousel3D), icon: 'columns' },
+ { description: 'Pivot/Time', event: () => func(CollectionViewType.Time), icon: 'columns' },
+ { description: 'Map', event: () => func(CollectionViewType.Map), icon: 'globe-americas' },
+ { description: 'Grid', event: () => func(CollectionViewType.Grid), icon: 'th-list' },
+ ];
- if (!Doc.IsSystem(this.rootDoc) && this.rootDoc._viewType !== CollectionViewType.Docking && !this.rootDoc.isGroup && !this.rootDoc.annotationOn) {
const existingVm = ContextMenu.Instance.findByDescription(category);
const catItems = existingVm && 'subitems' in existingVm ? existingVm.subitems : [];
catItems.push({ description: 'Add a Perspective...', addDivider: true, noexpand: true, subitems: subItems, icon: 'eye' });
@@ -166,47 +156,26 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
onContextMenu = (e: React.MouseEvent): void => {
const cm = ContextMenu.Instance;
- if (cm && !e.isPropagationStopped() && this.rootDoc[Id] !== Doc.MainDocId) {
+ if (cm && !e.isPropagationStopped()) {
// need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
- this.setupViewTypes(
- 'UI Controls...',
- vtype => {
- const newRendition = Doc.MakeAlias(this.rootDoc);
- newRendition._viewType = vtype;
- this.props.addDocTab(newRendition, 'add:right');
+ !Doc.noviceMode &&
+ this.setupViewTypes('UI Controls...', vtype => {
+ const newRendition = Doc.MakeEmbedding(this.rootDoc);
+ newRendition._type_collection = vtype;
+ this.props.addDocTab(newRendition, OpenWhere.addRight);
return newRendition;
- },
- false
- );
+ });
const options = cm.findByDescription('Options...');
const optionItems = options && 'subitems' in options ? options.subitems : [];
!Doc.noviceMode ? optionItems.splice(0, 0, { description: `${this.rootDoc.forceActive ? 'Select' : 'Force'} Contents Active`, event: () => (this.rootDoc.forceActive = !this.rootDoc.forceActive), icon: 'project-diagram' }) : null;
if (this.rootDoc.childLayout instanceof Doc) {
- optionItems.push({ description: 'View Child Layout', event: () => this.props.addDocTab(this.rootDoc.childLayout as Doc, 'add:right'), icon: 'project-diagram' });
+ optionItems.push({ description: 'View Child Layout', event: () => this.props.addDocTab(this.rootDoc.childLayout as Doc, OpenWhere.addRight), icon: 'project-diagram' });
}
if (this.rootDoc.childClickedOpenTemplateView instanceof Doc) {
- optionItems.push({ description: 'View Child Detailed Layout', event: () => this.props.addDocTab(this.rootDoc.childClickedOpenTemplateView as Doc, 'add:right'), icon: 'project-diagram' });
- }
- !Doc.noviceMode && optionItems.push({ description: `${this.rootDoc.isInPlaceContainer ? 'Unset' : 'Set'} inPlace Container`, event: () => (this.rootDoc.isInPlaceContainer = !this.rootDoc.isInPlaceContainer), icon: 'project-diagram' });
-
- if (!Doc.noviceMode && false) {
- optionItems.push({
- description: 'Create Branch',
- event: async () => this.props.addDocTab(await BranchCreate(this.rootDoc), 'add:right'),
- icon: 'project-diagram',
- });
- optionItems.push({
- description: 'Pull Master',
- event: () => BranchTask(this.rootDoc, 'pull'),
- icon: 'project-diagram',
- });
- optionItems.push({
- description: 'Merge Branches',
- event: () => BranchTask(this.rootDoc, 'merge'),
- icon: 'project-diagram',
- });
+ optionItems.push({ description: 'View Child Detailed Layout', event: () => this.props.addDocTab(this.rootDoc.childClickedOpenTemplateView as Doc, OpenWhere.addRight), icon: 'project-diagram' });
}
+ !Doc.noviceMode && optionItems.push({ description: `${this.rootDoc._isLightbox ? 'Unset' : 'Set'} is Lightbox`, event: () => (this.rootDoc._isLightbox = !this.rootDoc._isLightbox), icon: 'project-diagram' });
!options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'hand-point-right' });
@@ -222,9 +191,9 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
description: `Edit ${func.name} script`,
icon: 'edit',
event: (obj: any) => {
- const alias = Doc.MakeAlias(this.rootDoc);
- DocUtils.makeCustomViewClicked(alias, undefined, func.key);
- this.props.addDocTab(alias, 'add:right');
+ const embedding = Doc.MakeEmbedding(this.rootDoc);
+ DocUtils.makeCustomViewClicked(embedding, undefined, func.key);
+ this.props.addDocTab(embedding, OpenWhere.addRight);
},
})
);
@@ -252,11 +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);
- @computed get childLayoutString() {
- return StrCast(this.rootDoc.childLayoutString);
- }
-
- isContentActive = (outsideReaction?: boolean) => this.props.isContentActive();
+ isContentActive = (outsideReaction?: boolean) => (this.isAnyChildContentActive() ? true : this.props.isContentActive());
render() {
TraceMobx();
@@ -272,14 +237,15 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
PanelHeight: this.props.PanelHeight,
ScreenToLocalTransform: this.screenToLocalTransform,
childLayoutTemplate: this.childLayoutTemplate,
- childLayoutString: this.childLayoutString,
+ childLayoutString: StrCast(this.rootDoc.childLayoutString, this.props.childLayoutString),
childHideResizeHandles: this.childHideResizeHandles,
childHideDecorationTitle: this.childHideDecorationTitle,
- CollectionView: this,
};
return (
- <div className={'collectionView'} onContextMenu={this.onContextMenu} style={{ pointerEvents: this.props.ContainingCollectionDoc?._viewType === CollectionViewType.Freeform && this.rootDoc._lockedPosition ? 'none' : undefined }}>
- {this.showIsTagged()}
+ <div
+ className="collectionView"
+ onContextMenu={this.onContextMenu}
+ 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 0d045bada..58605c3f4 100644
--- a/src/client/views/collections/TabDocView.scss
+++ b/src/client/views/collections/TabDocView.scss
@@ -1,5 +1,9 @@
-@import "../global/globalCssVariables.scss";
+@import '../global/globalCssVariables.scss';
+.tabDocView-content {
+ height: 100%;
+ width: 100%;
+}
input.lm_title:focus,
input.lm_title {
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index 31ed5a83b..97d4d989b 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -5,29 +5,28 @@ import { clamp } from 'lodash';
import { action, computed, IReactionDisposer, observable, 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 { ScriptField } from '../../../fields/ScriptField';
-import { BoolCast, Cast, NumCast, StrCast } from '../../../fields/Types';
+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';
import { MainView } from '../MainView';
-import { DocFocusOptions, DocumentView, DocumentViewProps } from '../nodes/DocumentView';
+import { DocFocusOptions, DocumentView, DocumentViewProps, OpenWhere, OpenWhereMod } from '../nodes/DocumentView';
import { DashFieldView } from '../nodes/formattedText/DashFieldView';
+import { KeyValueBox } from '../nodes/KeyValueBox';
import { PinProps, PresBox, PresMovement } from '../nodes/trails';
import { DefaultStyleProvider, StyleProp } from '../StyleProvider';
import { CollectionDockingView } from './CollectionDockingView';
@@ -39,6 +38,7 @@ const _global = (window /* browser */ || global) /* node */ as any;
interface TabDocViewProps {
documentId: FieldId;
+ keyValue?: boolean;
glContainer: any;
}
@observer
@@ -55,6 +55,11 @@ export class TabDocView extends React.Component<TabDocViewProps> {
@computed get layoutDoc() {
return this._document && Doc.Layout(this._document);
}
+ @computed get tabBorderColor() {
+ const highlight = DefaultStyleProvider(this._document, undefined, StyleProp.Highlighting);
+ if (highlight?.highlightIndex === Doc.DocBrushStatus.highlighted) return highlight.highlightColor;
+ 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';
@@ -110,15 +115,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';
@@ -147,17 +150,15 @@ export class TabDocView extends React.Component<TabDocViewProps> {
ReactDOM.createRoot(closeWrap).render(closeIcon);
tab.reactComponents = [iconWrap, closeWrap];
tab.element[0].prepend(iconWrap);
- tab._disposers.layerDisposer = reaction(
- () => ({ layer: tab.DashDoc.activeLayer, color: this.tabColor }),
- ({ layer, color }) => {
- // console.log("TabDocView: " + this.tabColor);
- // console.log("lightOrDark: " + lightOrDark(this.tabColor));
+ tab._disposers.color = reaction(
+ () => ({ color: this.tabColor, borderColor: this.tabBorderColor }),
+ coloring => {
const textColor = lightOrDark(this.tabColor); //not working with StyleProp.Color
titleEle.style.color = textColor;
- titleEle.style.backgroundColor = 'transparent';
+ titleEle.style.backgroundColor = coloring.borderColor;
iconWrap.style.color = textColor;
closeWrap.style.color = textColor;
- tab.element[0].style.background = !layer ? color : 'dimgrey';
+ tab.element[0].style.background = coloring.color;
},
{ fireImmediately: true }
);
@@ -193,11 +194,14 @@ export class TabDocView extends React.Component<TabDocViewProps> {
() => SelectionManager.Views().some(v => v.topMost && v.props.Document === doc),
action(selected => {
if (selected) this._activated = true;
- const toggle = tab.element[0].children[1].children[0] as HTMLInputElement;
- selected && tab.contentItem !== tab.header.parent.getActiveContentItem() && UndoManager.RunInBatch(() => tab.header.parent.setActiveContentItem(tab.contentItem), 'tab switch');
- // toggle.style.fontWeight = selected ? "bold" : "";
+ const toggle = tab.element[0].children[2].children[0] as HTMLInputElement;
+ 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" : "";
- })
+ }),
+ { fireImmediately: true }
);
// highlight the tab when the tab document is brushed in any part of the UI
@@ -226,10 +230,10 @@ export class TabDocView extends React.Component<TabDocViewProps> {
* Adds a document to the presentation view
**/
@action
- public static PinDoc(docs: Doc | Doc[], pinProps?: PinProps) {
+ 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) {
@@ -243,13 +247,15 @@ export class TabDocView extends React.Component<TabDocViewProps> {
alert('Cannot pin presentation document to itself');
return;
}
- const pinDoc = Doc.MakeAlias(doc);
- pinDoc.presentationTargetDoc = doc;
+ const anchorDoc = DocumentManager.Instance.getDocumentView(doc)?.ComponentView?.getAnchor?.(false, pinProps);
+ const pinDoc = Doc.MakeDelegate(anchorDoc && anchorDoc !== doc ? anchorDoc : doc);
+ pinDoc.presentationTargetDoc = anchorDoc ?? doc;
pinDoc.title = doc.title + ' - Slide';
- pinDoc.data = new List<Doc>(); // the children of the alias' layout are the presentation slide children. the alias' data field might be children of a collection, PDF data, etc -- in any case we don't want the tree view to "see" this data
+ pinDoc.data = new List<Doc>(); // the children of the embedding's layout are the presentation slide children. the embedding's data field might be children of a collection, PDF data, etc -- in any case we don't want the tree view to "see" this data
pinDoc.presMovement = doc.type === DocumentType.SCRIPTING || pinProps?.pinDocLayout ? PresMovement.None : PresMovement.Zoom;
+ pinDoc.presDuration = pinDoc.presDuration ?? 1000;
pinDoc.groupWithUp = false;
- pinDoc.context = curPres;
+ pinDoc.embedContainer = 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.
@@ -257,44 +263,44 @@ export class TabDocView extends React.Component<TabDocViewProps> {
pinDoc.treeViewFieldKey = 'data'; // tree view will treat the 'data' field as the field where the hierarchical children are located instead of using the document's layout string field
pinDoc.treeViewExpandedView = 'data'; // in case the data doc has an expandedView set, this will mask that field and use the 'data' field when expanding the tree view
pinDoc.treeViewHideHeaderIfTemplate = true; // this will force the document to render itself as the tree view header
- const presArray: Doc[] = PresBox.Instance?.sortArray();
- const size: number = PresBox.Instance?.selectedArray.size;
- const presSelected: Doc | undefined = presArray && size ? presArray[size - 1] : undefined;
- const duration = NumCast(doc[`${Doc.LayoutFieldKey(pinDoc)}-duration`], null);
+ const duration = NumCast(doc[`${Doc.LayoutFieldKey(pinDoc)}_duration`], null);
+ if (pinProps.pinViewport) PresBox.pinDocView(pinDoc, pinProps, anchorDoc ?? doc);
if (!pinProps?.audioRange && duration !== undefined) {
pinDoc.mediaStart = 'manual';
pinDoc.mediaStop = 'manual';
pinDoc.presStartTime = NumCast(doc.clipStart);
pinDoc.presEndTime = NumCast(doc.clipEnd, duration);
}
- PresBox.pinDocView(pinDoc, pinProps, doc);
- pinDoc.onClick = ScriptField.MakeFunction('navigateToDoc(self.presentationTargetDoc, self)');
- Doc.AddDocToList(curPres, 'data', pinDoc, presSelected);
- //save position
if (pinProps?.activeFrame !== undefined) {
pinDoc.presActiveFrame = pinProps?.activeFrame;
pinDoc.title = doc.title + ' (move)';
pinDoc.presMovement = PresMovement.Pan;
}
- if (pinDoc.isInkMask) {
+ if (pinProps?.currentFrame !== undefined) {
+ pinDoc.presCurrentFrame = pinProps?.currentFrame;
+ pinDoc.title = doc.title + ' (move)';
+ pinDoc.presMovement = PresMovement.Pan;
+ }
+ if (pinDoc.stroke_isInkMask) {
pinDoc.presHideAfter = true;
pinDoc.presHideBefore = true;
pinDoc.presMovement = PresMovement.None;
}
if (curPres.expandBoolean) pinDoc.presExpandInlineButton = true;
+ Doc.AddDocToList(curPres, 'data', pinDoc, PresBox.Instance?.sortArray()?.lastElement());
PresBox.Instance?.clearSelectedArray();
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);
- CollectionDockingView.AddSplit(curPres, 'right');
- setTimeout(() => DocumentManager.Instance.jumpToDocument(docList.lastElement(), false, undefined, []), 100); // keeps the pinned doc in view since the sidebar shifts things
+ 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
}
@@ -339,32 +345,30 @@ export class TabDocView extends React.Component<TabDocViewProps> {
// replace[:{left,right,top,bottom,<any string>}] - e.g., "replace" will replace the current stack contents,
// "replace:right" - will replace the stack on the right named "right" if it exists, or create a stack on the right with that name,
// "replace:monkeys" - will replace any tab that has the label 'monkeys', or a tab with that label will be created by default on the right
- // inPlace - will add the document to any collection along the path from the document to the docking view that has a field isInPlaceContainer. if none is found, inPlace adds a tab to current stack
- addDocTab = (doc: Doc, location: string) => {
+ // 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 locationFields = doc._viewType === CollectionViewType.Docking ? ['dashboard'] : location.split(':');
- const locationParams = locationFields.length > 1 ? locationFields[1] : '';
- switch (locationFields[0]) {
- case 'dashboard':
- return DashboardView.openDashboard(doc);
- case 'close':
- return CollectionDockingView.CloseSplit(doc, locationParams);
- case 'fullScreen':
- return CollectionDockingView.OpenFullScreen(doc);
- case 'replace':
- return CollectionDockingView.ReplaceTab(doc, locationParams, this.stack);
- // case "lightbox": {
- // // TabDocView.PinDoc(doc, { hidePresBox: true });
- // return LightboxView.AddDocTab(doc, location, undefined, this.addDocTab);
- // }
- case 'lightbox':
- return LightboxView.AddDocTab(doc, location, undefined, this.addDocTab);
- case 'toggle':
- return CollectionDockingView.ToggleSplit(doc, locationParams, this.stack);
- case 'inPlace':
- case 'add':
- default:
- return CollectionDockingView.AddSplit(doc, locationParams, this.stack);
+ 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 && !keyValue) return DashboardView.openDashboard(doc);
+ // prettier-ignore
+ switch (whereFields[0]) {
+ case undefined:
+ case OpenWhere.lightbox: if (this.layoutDoc?._isLightbox) {
+ const lightboxView = !doc.annotationOn && DocCast(doc.embedContainer) ? DocumentManager.Instance.getFirstDocumentView(DocCast(doc.embedContainer)) : undefined;
+ const data = lightboxView?.dataDoc[Doc.LayoutFieldKey(lightboxView.rootDoc)];
+ if (lightboxView && (!data || data instanceof List)) {
+ lightboxView.layoutDoc[Doc.LayoutFieldKey(lightboxView.rootDoc)] = new List<Doc>([doc]);
+ return true;
+ }
+ }
+ return LightboxView.AddDocTab(doc, location);
+ 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);
+ case OpenWhere.add:default:return CollectionDockingView.AddSplit(doc, whereMods, this.stack, undefined, keyValue);
}
};
remDocTab = (doc: Doc | Doc[]) => {
@@ -377,28 +381,19 @@ 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);
+ 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;
};
@action
focusFunc = (doc: Doc, options: DocFocusOptions) => {
- const shrinkwrap = options?.originalTarget === this._document && this.view?.ComponentView?.shrinkWrap;
- if (options?.willZoom !== false && shrinkwrap && this._document) {
- const focusSpeed = NumCast(this._document.focusSpeed, 500);
- shrinkwrap();
- this._document._viewTransition = `transform ${focusSpeed}ms`;
- setTimeout(
- action(() => {
- this._document!._viewTransition = undefined;
- options?.afterFocus?.(false);
- }),
- focusSpeed
- );
- } else {
- options?.afterFocus?.(false);
- }
if (!this.tab.header.parent._activeContentItem || this.tab.header.parent._activeContentItem !== this.tab.contentItem) {
this.tab.header.parent.setActiveContentItem(this.tab.contentItem); // glr: Panning does not work when this is set - (this line is for trying to make a tab that is not topmost become topmost)
}
+ return undefined;
};
active = () => this._isActive;
@observable _forceInvalidateScreenToLocal = 0;
@@ -411,8 +406,8 @@ export class TabDocView extends React.Component<TabDocViewProps> {
PanelHeight = () => this._panelHeight;
miniMapColor = () => this.tabColor;
tabView = () => this._view;
- disableMinimap = () => !this._document || this._document.layout !== CollectionView.LayoutString(Doc.LayoutFieldKey(this._document)) || this._document?._viewType !== CollectionViewType.Freeform;
- hideMinimap = () => this.disableMinimap() || BoolCast(this._document?.hideMinimap);
+ disableMinimap = () => !this._document || this._document.layout !== CollectionView.LayoutString(Doc.LayoutFieldKey(this._document)) || this._document?._type_collection !== CollectionViewType.Freeform;
+ hideMinimap = () => this.disableMinimap() || BoolCast(this._document?.layout_hideMinimap);
@computed get docView() {
return !this._activated || !this._document ? null : (
@@ -425,17 +420,19 @@ export class TabDocView extends React.Component<TabDocViewProps> {
this._lastView = this._view;
})}
renderDepth={0}
+ 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}
- ContainingCollectionView={undefined}
- ContainingCollectionDoc={undefined}
+ DataDoc={!Doc.AreProtosEqual(this._document[DocData], this._document) ? this._document[DocData] : undefined}
onBrowseClick={MainView.Instance.exploreMode}
+ waitForDoubleClickToClick={MainView.Instance.waitForDoubleClick}
isContentActive={returnTrue}
+ 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}
@@ -450,19 +447,19 @@ export class TabDocView extends React.Component<TabDocViewProps> {
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.hideMinimap ? 'Open minimap' : 'Close minimap'}</div>}>
+ <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.hideMinimap ? Colors.BLACK : Colors.WHITE,
- backgroundColor: this._document.hideMinimap ? Colors.LIGHT_GRAY : Colors.MEDIUM_BLUE,
- boxShadow: this._document.hideMinimap ? Shadows.STANDARD_SHADOW : undefined,
+ display: this.disableMinimap() || this._document._type_collection !== 'freeform' ? 'none' : undefined,
+ color: this._document.layout_hideMinimap ? Colors.BLACK : Colors.WHITE,
+ backgroundColor: this._document.layout_hideMinimap ? Colors.LIGHT_GRAY : Colors.MEDIUM_BLUE,
+ boxShadow: this._document.layout_hideMinimap ? Shadows.STANDARD_SHADOW : undefined,
}}
onPointerDown={e => e.stopPropagation()}
onClick={action(e => {
e.stopPropagation();
- this._document!.hideMinimap = !this._document!.hideMinimap;
+ this._document!.layout_hideMinimap = !this._document!.layout_hideMinimap;
})}>
<FontAwesomeIcon icon={'globe-asia'} size="lg" />
</div>
@@ -474,11 +471,9 @@ export class TabDocView extends React.Component<TabDocViewProps> {
render() {
return (
<div
- className="collectionDockingView-content"
+ className="tabDocView-content"
style={{
fontFamily: Doc.UserDoc().renderStyle === 'comic' ? 'Comic Sans MS' : undefined,
- height: '100%',
- width: '100%',
}}
ref={ref => {
if ((this._mainCont = ref)) {
@@ -501,7 +496,7 @@ interface TabMinimapViewProps {
document: Doc;
hideMinimap: () => boolean;
tabView: () => DocumentView | undefined;
- addDocTab: (doc: Doc, where: string) => boolean;
+ addDocTab: (doc: Doc, where: OpenWhere) => boolean;
PanelWidth: () => number;
PanelHeight: () => number;
background: () => string;
@@ -538,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 }} />;
}
}
};
@@ -562,8 +557,8 @@ export class TabMinimapView extends React.Component<TabMinimapViewProps> {
this,
e,
action((e: PointerEvent, down: number[], delta: number[]) => {
- doc._panX = clamp(NumCast(doc._panX) + (delta[0] / miniSize) * renderBounds.dim, renderBounds.l, renderBounds.l + renderBounds.dim);
- doc._panY = clamp(NumCast(doc._panY) + (delta[1] / miniSize) * renderBounds.dim, renderBounds.t, renderBounds.t + renderBounds.dim);
+ doc._freeform_panX = clamp(NumCast(doc._freeform_panX) + (delta[0] / miniSize) * renderBounds.dim, renderBounds.l, renderBounds.l + renderBounds.dim);
+ doc._freeform_panY = clamp(NumCast(doc._freeform_panY) + (delta[1] / miniSize) * renderBounds.dim, renderBounds.t, renderBounds.t + renderBounds.dim);
return false;
}),
emptyFunction,
@@ -572,18 +567,15 @@ export class TabMinimapView extends React.Component<TabMinimapViewProps> {
};
render() {
if (!this.renderBounds) return null;
- const miniWidth = (this.props.PanelWidth() / NumCast(this.props.document._viewScale, 1) / this.renderBounds.dim) * 100;
- const miniHeight = (this.props.PanelHeight() / NumCast(this.props.document._viewScale, 1) / this.renderBounds.dim) * 100;
- const miniLeft = 50 + ((NumCast(this.props.document._panX) - this.renderBounds.cx) / this.renderBounds.dim) * 100 - miniWidth / 2;
- const miniTop = 50 + ((NumCast(this.props.document._panY) - this.renderBounds.cy) / this.renderBounds.dim) * 100 - miniHeight / 2;
+ const 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 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}
- CollectionView={undefined}
- ContainingCollectionView={undefined}
- ContainingCollectionDoc={undefined}
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
@@ -591,7 +583,6 @@ export class TabMinimapView extends React.Component<TabMinimapViewProps> {
isContentActive={emptyFunction}
isAnyChildContentActive={returnFalse}
select={emptyFunction}
- dropAction={undefined}
isSelected={returnFalse}
dontRegisterView={true}
fieldKey={Doc.LayoutFieldKey(this.props.document)}
@@ -605,12 +596,12 @@ export class TabMinimapView extends React.Component<TabMinimapViewProps> {
ScreenToLocalTransform={Transform.Identity}
renderDepth={0}
whenChildContentsActiveChanged={emptyFunction}
- focus={DocUtils.DefaultFocus}
+ focus={emptyFunction}
styleProvider={TabMinimapView.miniStyleProvider}
addDocTab={this.props.addDocTab}
pinToPres={TabDocView.PinDoc}
- 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}
fitContentsToBox={returnTrue}
/>
diff --git a/src/client/views/collections/TreeView.scss b/src/client/views/collections/TreeView.scss
index 83fee013a..7eab03e1d 100644
--- a/src/client/views/collections/TreeView.scss
+++ b/src/client/views/collections/TreeView.scss
@@ -173,7 +173,8 @@
.treeView-header:hover {
.collectionTreeView-keyHeader {
- display: inherit;
+ opacity: unset;
+ pointer-events: unset;
}
.treeView-openRight {
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index ac8562d5a..8d8d895c6 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -1,28 +1,30 @@
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, IReactionDisposer, observable, ObservableMap, reaction } from 'mobx';
+import { action, computed, IReactionDisposer, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
-import { DataSym, Doc, DocListCast, DocListCastOrNull, 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';
import { listSpec } from '../../../fields/Schema';
import { ComputedField, ScriptField } from '../../../fields/ScriptField';
-import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
+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, 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';
import { DragManager, dropActionType } from '../../util/DragManager';
+import { LinkManager } from '../../util/LinkManager';
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, StyleProviderFunc } from '../nodes/DocumentView';
+import { DocumentView, DocumentViewInternal, DocumentViewProps, OpenWhere, StyleProviderFunc } from '../nodes/DocumentView';
import { FieldViewProps } from '../nodes/FieldView';
import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
import { RichTextMenu } from '../nodes/formattedText/RichTextMenu';
@@ -41,13 +43,13 @@ export interface TreeViewProps {
prevSibling?: Doc;
document: Doc;
dataDoc?: Doc;
- containerCollection: Doc;
+ treeViewParent: Doc;
renderDepth: number;
- dropAction: dropActionType;
- addDocTab: (doc: Doc, where: string) => boolean;
+ dragAction: dropActionType;
+ addDocTab: (doc: Doc, where: OpenWhere) => boolean;
panelWidth: () => number;
panelHeight: () => number;
- addDocument: (doc: Doc | Doc[], relativeTo?: Doc, before?: boolean) => boolean;
+ addDocument: (doc: Doc | Doc[], annotationKey?: string, relativeTo?: Doc, before?: boolean) => boolean;
removeDoc: ((doc: Doc | Doc[]) => boolean) | undefined;
moveDocument: DragManager.MoveFunction;
isContentActive: (outsideReaction?: boolean) => boolean;
@@ -65,8 +67,8 @@ export interface TreeViewProps {
skipFields?: string[];
firstLevel: boolean;
// TODO: [AL] add these
- AddToMap?: (treeViewDoc: Doc, index: number[]) => Doc[];
- RemFromMap?: (treeViewDoc: Doc, index: number[]) => Doc[];
+ AddToMap?: (treeViewDoc: Doc, index: number[]) => void;
+ RemFromMap?: (treeViewDoc: Doc, index: number[]) => void;
hierarchyIndex?: number[];
}
@@ -95,13 +97,13 @@ export class TreeView extends React.Component<TreeViewProps> {
private _header: React.RefObject<HTMLDivElement> = React.createRef();
private _tref = React.createRef<HTMLDivElement>();
@observable _docRef: Opt<DocumentView>;
- private _selDisposer: Opt<IReactionDisposer>;
+ private _disposers: { [name: string]: IReactionDisposer } = {};
private _editTitleScript: (() => ScriptField) | undefined;
private _openScript: (() => ScriptField) | undefined;
private _treedropDisposer?: DragManager.DragDropDisposer;
get treeViewOpenIsTransient() {
- return this.props.treeView.doc.treeViewOpenIsTransient || Doc.IsPrototype(this.doc);
+ return this.props.treeView.doc.treeViewOpenIsTransient || Doc.IsDataProto(this.doc);
}
set treeViewOpen(c: boolean) {
if (this.treeViewOpenIsTransient) this._transientOpenState = c;
@@ -117,14 +119,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
- : 'aliases' // for displaying
+ : 'embeddings' // for displaying
: this.props.treeView.outlineMode || this.childDocs
? this.fieldKey
: Doc.noviceMode
@@ -142,10 +144,10 @@ export class TreeView extends React.Component<TreeViewProps> {
return this.validExpandViewTypes.includes(StrCast(this.doc.treeViewExpandedView)) ? StrCast(this.doc.treeViewExpandedView) : this.defaultExpandedView;
}
@computed get MAX_EMBED_HEIGHT() {
- return NumCast(this.props.containerCollection.maxEmbedHeight, 200);
+ 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);
@@ -159,67 +161,53 @@ export class TreeView extends React.Component<TreeViewProps> {
@computed get childLinks() {
return this.childDocList('links');
}
- @computed get childAliases() {
- return this.childDocList('aliases');
+ @computed get childEmbeddings() {
+ return this.childDocList('proto_embeddings');
}
@computed get childAnnos() {
- return this.childDocList(this.fieldKey + '-annotations');
+ return this.childDocList(this.fieldKey + '_annotations');
}
@computed get selected() {
return SelectionManager.IsSelected(this._docRef);
}
- // SelectionManager.Views().lastElement()?.props.Document === this.props.document; }
-
- @observable _presTimer!: NodeJS.Timeout;
- @observable _presKeyEventsActive: boolean = false;
-
- @observable _selectedArray: ObservableMap = new ObservableMap<Doc, any>();
- // the selected item's index
- @computed get itemIndex() {
- return NumCast(this.doc._itemIndex);
- }
- // the item that's active
- @computed get activeItem() {
- return this.childDocs ? Cast(this.childDocs[NumCast(this.doc._itemIndex)], Doc, null) : undefined;
- }
- @computed get targetDoc() {
- return Cast(this.activeItem?.presentationTargetDoc, Doc, null);
- }
childDocList(field: string) {
const layout = Cast(Doc.LayoutField(this.doc), Doc, null);
- return (
- (this.props.dataDoc ? DocListCastOrNull(this.props.dataDoc[field]) : undefined) || // if there's a data doc for an expanded template, use it's data field
- (layout ? DocListCastOrNull(layout[field]) : undefined) || // else if there's a layout doc, display it's fields
- DocListCastOrNull(this.doc[field])
- ); // otherwise use the document's data field
+ return DocListCast(this.props.dataDoc?.[field], DocListCast(layout?.[field], DocListCast(this.doc[field])));
}
+ moving: boolean = false;
@undoBatch move = (doc: Doc | Doc[], target: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => {
if (this.doc !== target && addDoc !== returnFalse) {
+ const canAdd1 = (this.props.parentTreeView as any).dropping || !(ComputedField.WithoutComputed(() => FieldValue(this.props.parentTreeView?.doc.data)) instanceof ComputedField);
+
// bcz: this should all be running in a Temp undo batch instead of hackily testing for returnFalse
- if (this.props.removeDoc?.(doc) === true) {
- return addDoc(doc);
+ if (canAdd1 && this.props.removeDoc?.(doc) === true) {
+ this.props.parentTreeView instanceof TreeView && (this.props.parentTreeView.moving = true);
+ const res = addDoc(doc);
+ this.props.parentTreeView instanceof TreeView && (this.props.parentTreeView.moving = false);
+ return res;
}
}
return false;
};
@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.CollectionView)?.select(false);
+ res && ind > 0 && DocumentManager.Instance.getDocumentView(DocListCast(this.dataDoc[key])[ind - 1], this.props.treeView.props.DocumentView?.())?.select(false);
return res;
};
@action setEditTitle = (docView?: DocumentView) => {
- this._selDisposer?.();
+ this._disposers.selection?.();
if (!docView) {
this._editTitle = false;
} else if (docView.isSelected()) {
const doc = docView.Document;
SelectionManager.SelectSchemaViewDoc(doc);
this._editTitle = true;
- this._selDisposer = reaction(
+ this._disposers.selection = reaction(
() => SelectionManager.SelectedSchemaDoc(),
seldoc => seldoc !== doc && this.setEditTitle(undefined)
);
@@ -232,11 +220,13 @@ export class TreeView extends React.Component<TreeViewProps> {
if (this.props.document.isFolder || Doc.IsSystem(this.props.document)) {
this.treeViewOpen = !this.treeViewOpen;
} else {
- // choose an appropriate alias or make one. --- choose the first alias that (1) user owns, (2) has no context field ... otherwise make a new alias
- const bestAlias =
- docView.props.Document.author === Doc.CurrentUserEmail && !Doc.IsPrototype(docView.props.Document) ? docView.props.Document : DocListCast(this.props.document.aliases).find(doc => !doc.context && doc.author === Doc.CurrentUserEmail);
- const nextBestAlias = DocListCast(this.props.document.aliases).find(doc => doc.author === Doc.CurrentUserEmail);
- this.props.addDocTab(bestAlias ?? nextBestAlias ?? Doc.MakeAlias(this.props.document), 'lightbox');
+ // 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.IsDataProto(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);
}
};
@@ -259,7 +249,8 @@ export class TreeView extends React.Component<TreeViewProps> {
};
componentWillUnmount() {
- this._selDisposer?.();
+ this._renderTimer && clearTimeout(this._renderTimer);
+ Object.values(this._disposers).forEach(disposer => disposer?.());
this._treeEle && this.props.unobserveHeight(this._treeEle);
document.removeEventListener('pointermove', this.onDragMove, true);
document.removeEventListener('pointermove', this.onDragUp, true);
@@ -268,6 +259,10 @@ export class TreeView extends React.Component<TreeViewProps> {
}
componentDidUpdate() {
+ this._disposers.opening = reaction(
+ () => this.treeViewOpen,
+ open => !open && (this._renderCount = 20)
+ );
this.props.hierarchyIndex !== undefined && this.props.AddToMap?.(this.doc, this.props.hierarchyIndex);
}
@@ -281,7 +276,7 @@ export class TreeView extends React.Component<TreeViewProps> {
};
onPointerEnter = (e: React.PointerEvent): void => {
this.props.isContentActive(true) && Doc.BrushDoc(this.dataDoc);
- if (e.buttons === 1 && SnappingManager.GetIsDragging()) {
+ if (e.buttons === 1 && SnappingManager.GetIsDragging() && this.props.isContentActive()) {
this._header.current!.className = 'treeView-header';
document.removeEventListener('pointermove', this.onDragMove, true);
document.removeEventListener('pointerup', this.onDragUp, true);
@@ -313,28 +308,29 @@ export class TreeView extends React.Component<TreeViewProps> {
};
public static makeTextBullet() {
- const bullet = Docs.Create.TextDocument('-text-', {
+ const bullet = Docs.Create.TextDocument('', {
layout: CollectionView.LayoutString('data'),
title: '-title-',
treeViewExpandedViewLock: true,
treeViewExpandedView: 'data',
- _viewType: CollectionViewType.Tree,
- hideLinkButton: true,
- _showSidebar: true,
- _fitWidth: true,
+ _type_collection: CollectionViewType.Tree,
+ layout_hideLinkButton: true,
+ _layout_showSidebar: true,
+ _layout_fitWidth: true,
treeViewType: TreeViewType.outline,
x: 0,
y: 0,
_xMargin: 0,
_yMargin: 0,
- _autoHeight: true,
- _singleLine: true,
+ _layout_autoHeight: true,
+ _createDocOnCR: true,
_width: 1000,
_height: 10,
});
Doc.GetProto(bullet).title = ComputedField.MakeFunction('self.text?.Text');
Doc.GetProto(bullet).data = new List<Doc>([]);
- FormattedTextBox.SelectOnLoad = bullet[Id];
+ DocumentManager.Instance.AddViewRenderedCb(bullet, dv => dv.ComponentView?.setFocus?.());
+
return bullet;
}
@@ -345,13 +341,13 @@ export class TreeView extends React.Component<TreeViewProps> {
};
makeFolder = () => {
- const folder = Docs.Create.TreeDocument([], { title: 'Untitled folder', _stayInCollection: true, isFolder: true });
+ const folder = Docs.Create.TreeDocument([], { title: 'Untitled folder', _dragOnlyWithinContainer: true, isFolder: true });
TreeView._editTitleOnLoad = { id: folder[Id], parent: this.props.parentTreeView };
return this.props.addDocument(folder);
};
deleteItem = () => this.props.removeDoc?.(this.doc);
- preTreeDrop = (e: Event, de: DragManager.DropEvent, targetAction: dropActionType) => {
+ preTreeDrop = (e: Event, de: DragManager.DropEvent) => {
const dragData = de.complete.docDragData;
dragData && (dragData.dropAction = this.props.treeView.props.Document === dragData.treeViewDoc ? 'same' : dragData.dropAction);
};
@@ -359,33 +355,56 @@ 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);
if (de.complete.linkDragData) {
const sourceDoc = de.complete.linkDragData.linkSourceGetAnchor();
const destDoc = this.doc;
- DocUtils.MakeLink({ doc: sourceDoc }, { doc: destDoc }, 'tree link', '');
+ 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.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;
};
- dropDocuments(droppedDocuments: Doc[], before: boolean, inside: number | boolean, dropAction: dropActionType, moveDocument: DragManager.MoveFunction | undefined, forceAdd: boolean) {
- const parentAddDoc = (doc: Doc | Doc[]) => this.props.addDocument(doc, undefined, before);
- const canAdd = (!this.props.treeView.outlineMode && !StrCast((inside ? this.props.document : this.props.containerCollection)?.freezeChildren).includes('add')) || forceAdd;
- const localAdd = (doc: Doc) => (Doc.AddDocToList(this.dataDoc, this.fieldKey, doc) && ((doc.context = this.doc.context) || true) ? true : false);
- const addDoc = !inside ? parentAddDoc : (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && localAdd(doc), true as boolean);
+ dropping: boolean = false;
+ dropDocuments(droppedDocuments: Doc[], before: boolean, inside: number | boolean, dropAction: dropActionType, removeDocument: DragManager.RemoveFunction | undefined, moveDocument: DragManager.MoveFunction | undefined, forceAdd: boolean) {
+ const parentAddDoc = (doc: Doc | Doc[]) => this.props.addDocument(doc, undefined, undefined, before);
+ const localAdd = (doc: Doc | Doc[]) => {
+ const innerAdd = (doc: Doc) => {
+ const dataIsComputed = ComputedField.WithoutComputed(() => FieldValue(this.dataDoc[this.fieldKey])) instanceof ComputedField;
+ const added = (!dataIsComputed || (this.dropping && this.moving)) && Doc.AddDocToList(this.dataDoc, this.fieldKey, doc);
+ dataIsComputed && (doc.embedContainer = 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)?.treeViewFreezeChildren).includes('add')) || forceAdd;
if (canAdd) {
- return UndoManager.RunInTempBatch(() => 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 = true);
+ 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;
}
return false;
}
@@ -401,22 +420,21 @@ export class TreeView extends React.Component<TreeViewProps> {
getTransform = () => this.refTransform(this._tref.current);
embeddedPanelWidth = () => this.props.panelWidth() / (this.props.treeView.props.NativeDimScaling?.() || 1);
embeddedPanelHeight = () => {
- const layoutDoc = (temp => temp && Doc.expandTemplateLayout(temp, this.props.document, ''))(this.props.treeView.props.childLayoutTemplate?.()) || this.layoutDoc;
+ 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);
if (aspect) return this.embeddedPanelWidth() / (aspect || 1);
- return layoutDoc._fitWidth
+ return layoutDoc._layout_fitWidth
? !Doc.NativeHeight(layoutDoc)
- ? NumCast(layoutDoc._height) //this.props.containerCollection._height)
- : Math.min((this.embeddedPanelWidth() * NumCast(layoutDoc.scrollHeight, Doc.NativeHeight(layoutDoc))) / (Doc.NativeWidth(layoutDoc) || NumCast(this.props.containerCollection._height)))
- : (this.embeddedPanelWidth() * layoutDoc[HeightSym]()) / layoutDoc[WidthSym]();
+ ? NumCast(layoutDoc._height)
+ : Math.min((this.embeddedPanelWidth() * NumCast(layoutDoc.scrollHeight, Doc.NativeHeight(layoutDoc))) / (Doc.NativeWidth(layoutDoc) || NumCast(this.props.treeViewParent._height)))
+ : (this.embeddedPanelWidth() * layoutDoc[Height]()) / layoutDoc[Width]();
})()
);
};
-
@computed get expandedField() {
const ids: { [key: string]: string } = {};
const rows: JSX.Element[] = [];
@@ -428,31 +446,37 @@ export class TreeView extends React.Component<TreeViewProps> {
const contents = doc[key];
let contentElement: (JSX.Element | null)[] | JSX.Element = [];
+ let leftOffset = observable({ width: 0 });
+ const expandedWidth = () => this.props.panelWidth() - leftOffset.width;
if (contents instanceof Doc || (Cast(contents, listSpec(Doc)) && Cast(contents, listSpec(Doc))!.length && Cast(contents, listSpec(Doc))![0] instanceof Doc)) {
const remDoc = (doc: Doc | Doc[]) => this.remove(doc, key);
- const localAdd = (doc: Doc, addBefore?: Doc, before?: boolean) => {
- const added = Doc.AddDocToList(this.dataDoc, key, doc, addBefore, before, false, true);
- added && (doc.context = this.doc.context);
- return added;
+ const moveDoc = (doc: Doc | Doc[], target: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => this.move(doc, target, addDoc);
+ const addDoc = (doc: Doc | Doc[], addBefore?: Doc, before?: boolean) => {
+ 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);
+ return added;
+ };
+ return (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && innerAdd(doc), true as boolean);
};
- const addDoc = (doc: Doc | Doc[], addBefore?: Doc, before?: boolean) => (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && localAdd(doc, addBefore, before), true);
contentElement = TreeView.GetChildElements(
contents instanceof Doc ? [contents] : DocListCast(contents),
this.props.treeView,
this,
doc,
undefined,
- this.props.containerCollection,
+ this.props.treeViewParent,
this.props.prevSibling,
addDoc,
remDoc,
- this.move,
- this.props.dropAction,
+ moveDoc,
+ this.props.dragAction,
this.props.addDocTab,
this.titleStyleProvider,
this.props.ScreenToLocalTransform,
this.props.isContentActive,
- this.props.panelWidth,
+ expandedWidth,
this.props.renderDepth,
this.props.treeViewHideHeaderFields,
[...this.props.renderedIds, doc[Id]],
@@ -483,15 +507,21 @@ export class TreeView extends React.Component<TreeViewProps> {
);
}
rows.push(
- <div style={{ display: 'flex' }} key={key}>
- <span style={{ fontWeight: 'bold' }}>{key + ':'}</span>
- &nbsp;
+ <div style={{ display: 'flex', overflow: 'auto' }} key={key}>
+ <span
+ ref={action((r: any) => {
+ if (r) leftOffset.width = r.getBoundingClientRect().width;
+ })}
+ style={{ fontWeight: 'bold' }}>
+ {key + ':'}
+ &nbsp;
+ </span>
{contentElement}
</div>
);
}
rows.push(
- <div style={{ display: 'flex' }} key={'newKeyValue'}>
+ <div style={{ display: 'flex', overflow: 'auto' }} key={'newKeyValue'}>
<EditableView
key="editableView"
contents={'+key:value'}
@@ -505,11 +535,13 @@ export class TreeView extends React.Component<TreeViewProps> {
return rows;
}
+ _renderTimer: any;
+ @observable _renderCount = 1;
@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 } }) ?? {};
- if (['links', 'annotations', 'aliases', this.fieldKey].includes(expandKey)) {
+ if (['links', 'annotations', 'embeddings', this.fieldKey].includes(expandKey)) {
const sorting = StrCast(this.doc.treeViewSortCriterion, TreeSort.None);
const sortKeys = Object.keys(sortings);
const curSortIndex = Math.max(
@@ -518,6 +550,7 @@ export class TreeView extends React.Component<TreeViewProps> {
);
const key = (expandKey === 'annotations' ? `${this.fieldKey}-` : '') + expandKey;
const remDoc = (doc: Doc | Doc[]) => this.remove(doc, key);
+ const moveDoc = (doc: Doc | Doc[], target: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => this.move(doc, target, addDoc);
const localAdd = (doc: Doc, addBefore?: Doc, before?: boolean) => {
// if there's a sort ordering specified that can be modified on drop (eg, zorder can be modified, alphabetical can't),
// then the modification would be done here
@@ -528,14 +561,24 @@ export class TreeView extends React.Component<TreeViewProps> {
docs.push(doc);
docs.sort((a, b) => (NumCast(a.zIndex) > NumCast(b.zIndex) ? 1 : -1)).forEach((d, i) => (d.zIndex = i));
}
- const added = Doc.AddDocToList(this.dataDoc, key, doc, addBefore, before, false, true);
- added && (doc.context = this.doc.context);
+ 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);
+
return added;
};
const addDoc = (doc: Doc | Doc[], addBefore?: Doc, before?: boolean) => (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && localAdd(doc, addBefore, before), true);
- const docs = expandKey === 'aliases' ? this.childAliases : expandKey === 'links' ? this.childLinks : expandKey === 'annotations' ? this.childAnnos : this.childDocs;
+ const docs = expandKey === 'embeddings' ? this.childEmbeddings : expandKey === 'links' ? this.childLinks : expandKey === 'annotations' ? this.childAnnos : this.childDocs;
let downX = 0,
downY = 0;
+ if (docs?.length && this._renderCount < docs?.length) {
+ this._renderTimer && clearTimeout(this._renderTimer);
+ this._renderTimer = setTimeout(
+ action(() => {
+ this._renderCount = Math.min(docs!.length, this._renderCount + 20);
+ })
+ );
+ }
return (
<>
{!docs?.length || this.props.AddToMap /* hack to identify pres box trees */ ? null : (
@@ -544,6 +587,7 @@ export class TreeView extends React.Component<TreeViewProps> {
</div>
)}
<ul
+ style={{ cursor: 'inherit' }}
key={expandKey + 'more'}
title="click to change sort order"
className={''} //this.doc.treeViewHideTitle ? 'no-indent' : ''}
@@ -552,12 +596,12 @@ export class TreeView extends React.Component<TreeViewProps> {
downY = e.clientY;
e.stopPropagation();
}}
- onClick={e => {
+ onClick={undoable(e => {
if (this.props.isContentActive() && Math.abs(e.clientX - downX) < 3 && Math.abs(e.clientY - downY) < 3) {
!this.props.treeView.outlineMode && (this.doc.treeViewSortCriterion = sortKeys[(curSortIndex + 1) % sortKeys.length]);
e.stopPropagation();
}
- }}>
+ }, 'sort order')}>
{!docs
? null
: TreeView.GetChildElements(
@@ -566,12 +610,12 @@ export class TreeView extends React.Component<TreeViewProps> {
this,
this.layoutDoc,
this.dataDoc,
- this.props.containerCollection,
+ this.props.treeViewParent,
this.props.prevSibling,
addDoc,
remDoc,
- this.move,
- StrCast(this.doc.childDropAction, this.props.dropAction) as dropActionType,
+ moveDoc,
+ StrCast(this.doc.childDragAction, this.props.dragAction) as dropActionType,
this.props.addDocTab,
this.titleStyleProvider,
this.props.ScreenToLocalTransform,
@@ -592,14 +636,15 @@ export class TreeView extends React.Component<TreeViewProps> {
// TODO: [AL] add these
this.props.AddToMap,
this.props.RemFromMap,
- this.props.hierarchyIndex
+ this.props.hierarchyIndex,
+ this._renderCount
)}
</ul>
</>
);
} else if (this.treeViewExpandedView === 'fields') {
return (
- <ul key={this.doc[Id] + this.doc.title}>
+ <ul key={this.doc[Id] + this.doc.title} style={{ cursor: 'inherit' }}>
<div>{this.expandedField}</div>
</ul>
);
@@ -625,7 +670,7 @@ export class TreeView extends React.Component<TreeViewProps> {
this.onCheckedClick?.script.run(
{
this: this.doc.isTemplateForField && this.props.dataDoc ? this.props.dataDoc : this.doc,
- heading: this.props.containerCollection.title,
+ heading: this.props.treeViewParent.title,
checked: this.doc.treeViewChecked === 'check' ? 'x' : this.doc.treeViewChecked === 'x' ? 'remove' : 'check',
containingTreeView: this.props.treeView.props.Document,
},
@@ -639,12 +684,12 @@ 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 checked = this.onCheckedClick ? this.doc.treeViewChecked ?? 'unchecked' : undefined;
return (
<div
className={`bullet${this.props.treeView.outlineMode ? '-outline' : ''}`}
- key={'bullet'}
+ key="bullet"
title={this.childDocs?.length ? `click to see ${this.childDocs?.length} items` : 'view fields'}
onClick={this.bulletClick}
style={
@@ -675,13 +720,13 @@ export class TreeView extends React.Component<TreeViewProps> {
}
@computed get validExpandViewTypes() {
- const annos = () => (DocListCast(this.doc[this.fieldKey + '-annotations']).length && !this.props.treeView.dashboardMode ? 'annotations' : '');
- const links = () => (DocListCast(this.doc.links).length && !this.props.treeView.dashboardMode ? 'links' : '');
+ const annos = () => (DocListCast(this.doc[this.fieldKey + '_annotations']).length && !this.props.treeView.dashboardMode ? 'annotations' : '');
+ const links = () => (LinkManager.Links(this.doc).length && !this.props.treeView.dashboardMode ? 'links' : '');
const data = () => (this.childDocs || this.props.treeView.dashboardMode ? this.fieldKey : '');
- const aliases = () => (this.props.treeView.dashboardMode ? '' : 'aliases');
+ const embeddings = () => (this.props.treeView.dashboardMode ? '' : 'embeddings');
const fields = () => (Doc.noviceMode ? '' : 'fields');
- const layout = Doc.noviceMode || this.doc.viewType === CollectionViewType.Docking ? [] : ['layout'];
- return [data(), ...layout, ...(this.props.treeView.fileSysMode ? [aliases(), links(), annos()] : []), fields()].filter(m => m);
+ 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
expandNextviewType = () => {
@@ -692,10 +737,13 @@ export class TreeView extends React.Component<TreeViewProps> {
this.treeViewOpen = true;
};
- @computed get headerElements() {
+ @observable headerEleWidth = 0;
+ @computed get titleButtons() {
+ const customHeaderButtons = this.props.styleProvider?.(this.doc, this.props.treeView.props, StyleProp.Decorations);
return this.props.treeViewHideHeaderFields() || this.doc.treeViewHideHeaderFields ? null : (
<>
- {this.doc.hideContextMenu ? null : (
+ {customHeaderButtons} {/* e.g.,. hide button is set by dashboardStyleProvider */}
+ {this.doc._layout_hideContextMenu ? null : (
<FontAwesomeIcon
title="context menu"
key="bars"
@@ -725,7 +773,7 @@ export class TreeView extends React.Component<TreeViewProps> {
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 openAlias = { script: ScriptField.MakeFunction(`openOnRight(getAlias(self))`)!, icon: 'copy', label: 'Open Alias' };
+ 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' };
return [
...(this.props.contextMenuItems ?? []).filter(mi => (!mi.filter ? true : mi.filter.script.run({ doc: this.doc })?.result)),
@@ -734,10 +782,10 @@ export class TreeView extends React.Component<TreeViewProps> {
: Doc.IsSystem(this.doc)
? []
: this.props.treeView.fileSysMode && this.doc === Doc.GetProto(this.doc)
- ? [openAlias, makeFolder]
- : this.doc.viewType === CollectionViewType.Docking
+ ? [openEmbedding, makeFolder]
+ : this.doc._type_collection === CollectionViewType.Docking
? []
- : [deleteItem, openAlias, focusDoc]),
+ : [deleteItem, openEmbedding, focusDoc]),
];
};
childContextMenuItems = () => {
@@ -747,9 +795,7 @@ export class TreeView extends React.Component<TreeViewProps> {
return StrListCast(this.doc.childContextMenuLabels).map((label, i) => ({ script: customScripts[i], filter: customFilters[i], icon: icons[i], label }));
};
- onChildClick = () => {
- return this.props.onChildClick?.() ?? (this._editTitleScript?.() || ScriptField.MakeFunction(`DocFocusOrOpen(self)`)!);
- };
+ onChildClick = () => this.props.onChildClick?.() ?? (this._editTitleScript?.() || ScriptField.MakeFunction(`DocFocusOrOpen(self)`)!);
onChildDoubleClick = () => ScriptCast(this.props.treeView.Document.treeViewChildDoubleClick, !this.props.treeView.outlineMode ? this._openScript?.() : null);
@@ -769,7 +815,6 @@ export class TreeView extends React.Component<TreeViewProps> {
case StyleProp.Opacity: return this.props.treeView.outlineMode ? undefined : 1;
case StyleProp.BackgroundColor: return this.selected ? '#7089bb' : StrCast(doc._backgroundColor, StrCast(doc.backgroundColor));
case StyleProp.Highlighting: if (this.props.treeView.outlineMode) return undefined;
- case StyleProp.Hidden: return false;
case StyleProp.BoxShadow: return undefined;
case StyleProp.DocContents:
const highlightIndex = this.props.treeView.outlineMode ? Doc.DocBrushStatus.unbrushed : Doc.isBrushedHighlightedDegree(doc);
@@ -795,7 +840,6 @@ export class TreeView extends React.Component<TreeViewProps> {
};
embeddedStyleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string): any => {
if (property.startsWith(StyleProp.Decorations)) return null;
- if (property.startsWith(StyleProp.Hidden)) return false;
return this.props?.treeView?.props.styleProvider?.(doc, props, property); // properties are inherited from the CollectionTreeView, not the hierarchical parent in the treeView
};
onKeyDown = (e: React.KeyboardEvent, fieldProps: FieldViewProps) => {
@@ -822,9 +866,8 @@ export class TreeView extends React.Component<TreeViewProps> {
}
return false;
};
- titleWidth = () => Math.max(20, Math.min(this.props.treeView.truncateTitleWidth(), this.props.panelWidth())) / (this.props.treeView.props.NativeDimScaling?.() || 1) - 3 * treeBulletWidth();
+ 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.
*/
@@ -866,13 +909,14 @@ export class TreeView extends React.Component<TreeViewProps> {
}
})}
Document={this.doc}
- fitWidth={returnTrue}
+ layout_fitWidth={returnTrue}
DataDoc={undefined}
scriptContext={this}
hideDecorationTitle={this.props.treeView.outlineMode}
hideResizeHandles={this.props.treeView.outlineMode}
styleProvider={this.titleStyleProvider}
- docViewPath={returnEmptyDoclist}
+ onClickScriptDisable="never" // tree docViews have a script to show fields, etc.
+ docViewPath={this.props.treeView.props.docViewPath}
treeViewDoc={this.props.treeView.props.Document}
addDocument={undefined}
addDocTab={this.props.addDocTab}
@@ -880,35 +924,32 @@ 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}
- ContainingCollectionView={this.props.treeView.props.CollectionView}
- ContainingCollectionDoc={this.props.treeView.props.Document}
/>
);
-
- const buttons = this.props.styleProvider?.(this.doc, { ...this.props.treeView.props, ContainingCollectionDoc: this.props.parentTreeView?.doc }, StyleProp.Decorations + (Doc.IsSystem(this.props.containerCollection) ? ':afterHeader' : ''));
return (
<>
<div
@@ -919,13 +960,12 @@ export class TreeView extends React.Component<TreeViewProps> {
fontWeight: Doc.IsSearchMatch(this.doc) !== undefined ? 'bold' : undefined,
textDecoration: Doc.GetT(this.doc, 'title', 'string', true) ? 'underline' : undefined,
outline: this.doc === Doc.ActiveDashboard ? 'dashed 1px #06123232' : undefined,
- pointerEvents: !this.props.isContentActive() && !SnappingManager.GetIsDragging() ? 'none' : undefined,
+ pointerEvents: !this.props.isContentActive() ? 'none' : undefined,
}}>
{view}
</div>
- <div className="treeView-rightButtons">
- {buttons} {/* hide and lock buttons */}
- {this.headerElements}
+ <div className="treeView-rightButtons" ref={action((r: any) => r && (this.headerEleWidth = r.getBoundingClientRect().width))}>
+ {this.titleButtons}
</div>
</>
);
@@ -949,6 +989,7 @@ export class TreeView extends React.Component<TreeViewProps> {
);
};
+ fitWidthFilter = (doc: Doc) => (doc.type === DocumentType.IMG ? false : undefined);
renderEmbeddedDocument = (asText: boolean, isActive: () => boolean | undefined) => {
return (
<div style={{ height: this.embeddedPanelHeight(), width: this.embeddedPanelWidth() }}>
@@ -957,6 +998,7 @@ export class TreeView extends React.Component<TreeViewProps> {
ref={action((r: DocumentView | null) => (this._dref = r))}
Document={this.doc}
DataDoc={undefined}
+ layout_fitWidth={this.fitWidthFilter}
PanelWidth={this.embeddedPanelWidth}
PanelHeight={this.embeddedPanelHeight}
LayoutTemplateString={asText ? FormattedTextBox.LayoutString('text') : undefined}
@@ -978,11 +1020,9 @@ 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}
- ContainingCollectionDoc={this.props.containerCollection}
- ContainingCollectionView={undefined}
addDocument={this.props.addDocument}
moveDocument={this.move}
removeDocument={this.props.removeDoc}
@@ -991,7 +1031,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}
/>
@@ -1001,7 +1041,9 @@ 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 (
+ return this.props.treeView.Document.treeViewHideUnrendered && this.doc.layout_unrendered && !this.doc.treeViewFieldKey ? (
+ <div></div>
+ ) : (
<>
{this.renderBullet}
{this.renderTitle}
@@ -1035,7 +1077,7 @@ export class TreeView extends React.Component<TreeViewProps> {
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);
- const docs = this.props.treeView.onTreeDrop(de, (docs: Doc[]) => this.dropDocuments(docs, before, inside, 'copy', undefined, false));
+ const docs = this.props.treeView.onTreeDrop(de, (docs: Doc[]) => this.dropDocuments(docs, before, inside, 'copy', undefined, undefined, false));
};
render() {
@@ -1093,15 +1135,15 @@ export class TreeView extends React.Component<TreeViewProps> {
childDocs: Doc[],
treeView: CollectionTreeView,
parentTreeView: CollectionTreeView | TreeView | undefined,
- containerCollection: Doc,
+ treeViewParent: Doc,
dataDoc: Doc | undefined,
parentCollectionDoc: Doc | undefined,
containerPrevSibling: Doc | undefined,
add: (doc: Doc | Doc[], relativeTo?: Doc, before?: boolean) => boolean,
remove: undefined | ((doc: Doc | Doc[]) => boolean),
move: DragManager.MoveFunction,
- dropAction: dropActionType,
- addDocTab: (doc: Doc, where: string) => boolean,
+ dragAction: dropActionType,
+ addDocTab: (doc: Doc, where: OpenWhere) => boolean,
styleProvider: undefined | StyleProviderFunc,
screenToLocalXf: () => Transform,
isContentActive: (outsideReaction?: boolean) => boolean,
@@ -1119,22 +1161,24 @@ export class TreeView extends React.Component<TreeViewProps> {
unobserveHeight: (ref: any) => void,
contextMenuItems: { script: ScriptField; filter: ScriptField; label: string; icon: string }[],
// TODO: [AL] add these
- AddToMap?: (treeViewDoc: Doc, index: number[]) => Doc[],
- RemFromMap?: (treeViewDoc: Doc, index: number[]) => Doc[],
- hierarchyIndex?: number[]
+ AddToMap?: (treeViewDoc: Doc, index: number[]) => void,
+ RemFromMap?: (treeViewDoc: Doc, index: number[]) => void,
+ hierarchyIndex?: number[],
+ renderCount?: number
) {
- const viewSpecScript = Cast(containerCollection.viewSpecScript, ScriptField);
+ const viewSpecScript = Cast(treeViewParent.viewSpecScript, ScriptField);
if (viewSpecScript) {
childDocs = childDocs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result);
}
- const docs = TreeView.sortDocs(childDocs, StrCast(containerCollection.treeViewSortCriterion, TreeSort.None));
+ const docs = TreeView.sortDocs(childDocs, StrCast(treeViewParent.treeViewSortCriterion, TreeSort.None));
const rowWidth = () => panelWidth() - treeBulletWidth() * (treeView.props.NativeDimScaling?.() || 1);
const treeViewRefs = new Map<Doc, TreeView | undefined>();
return docs
.filter(child => child instanceof Doc)
.map((child, i) => {
- const pair = Doc.GetLayoutDataDocPair(containerCollection, dataDoc, child);
+ if (renderCount && i > renderCount) return null;
+ const pair = Doc.GetLayoutDataDocPair(treeViewParent, dataDoc, child);
if (!pair.layout || pair.data instanceof Promise) {
return null;
}
@@ -1148,19 +1192,16 @@ 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.context = treeView.Document;
+ child.embedContainer = treeView.Document;
}
};
const indent = i === 0 ? undefined : (editTitle: boolean) => dentDoc(editTitle, docs[i - 1], undefined, treeViewRefs.get(docs[i - 1]));
- const outdent =
- parentCollectionDoc?._viewType !== CollectionViewType.Tree
- ? undefined
- : (editTitle: boolean) => dentDoc(editTitle, parentCollectionDoc, containerPrevSibling, parentTreeView instanceof TreeView ? parentTreeView.props.parentTreeView : undefined);
- const addDocument = (doc: Doc | Doc[], relativeTo?: Doc, before?: boolean) => add(doc, relativeTo ?? docs[i], before !== undefined ? before : false);
+ const outdent = !parentCollectionDoc ? undefined : (editTitle: boolean) => dentDoc(editTitle, parentCollectionDoc, containerPrevSibling, parentTreeView instanceof TreeView ? parentTreeView.props.parentTreeView : undefined);
+ const addDocument = (doc: Doc | Doc[], annotationKey?: string, relativeTo?: Doc, before?: boolean) => add(doc, relativeTo ?? docs[i], before !== undefined ? before : false);
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
@@ -1168,7 +1209,7 @@ export class TreeView extends React.Component<TreeViewProps> {
ref={r => treeViewRefs.set(child, r ? r : undefined)}
document={pair.layout}
dataDoc={pair.data}
- containerCollection={containerCollection}
+ treeViewParent={treeViewParent}
prevSibling={docs[i]}
// TODO: [AL] add these
hierarchyIndex={hierarchyIndex ? [...hierarchyIndex, i + 1] : undefined}
@@ -1180,14 +1221,14 @@ export class TreeView extends React.Component<TreeViewProps> {
onCheckedClick={onCheckedClick}
onChildClick={onChildClick}
renderDepth={renderDepth}
- removeDoc={StrCast(containerCollection.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}
@@ -1208,6 +1249,6 @@ export class TreeView extends React.Component<TreeViewProps> {
ScriptingGlobals.add(function TreeView_addNewFolder() {
TreeView._editTitleOnLoad = { id: Utils.GenerateGuid(), parent: undefined };
- const opts = { title: 'Untitled folder', _stayInCollection: true, isFolder: true };
+ const opts = { title: 'Untitled folder', _dragOnlyWithinContainer: true, isFolder: true };
return Doc.AddDocToList(Doc.MyFilesystem, 'data', Docs.Create.TreeDocument([], opts, TreeView._editTitleOnLoad.id));
});
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
index 89cc22d07..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';
@@ -14,6 +15,7 @@ export interface ViewDefBounds {
x: number;
y: number;
z?: number;
+ rotation?: number;
text?: string;
zIndex?: number;
width?: number;
@@ -31,6 +33,7 @@ export interface PoolData {
x: number;
y: number;
z?: number;
+ rotation?: number;
zIndex?: number;
width?: number;
height?: number;
@@ -46,7 +49,7 @@ export interface PoolData {
export interface ViewDefResult {
ele: JSX.Element;
bounds?: ViewDefBounds;
- inkMask?: number; //sort elements into either the mask layer (which has a mixedBlendMode appropriate for transparent masks), or the regular documents layer; -1 = no mask, 0 = mask layer but stroke is transprent (hidden), >0 = mask layer and not hidden
+ inkMask?: number; //sort elements into either the mask layer (which has a mixedBlendMode appropriate for transparent masks), or the regular documents layer; -1 = no mask, 0 = mask layer but stroke is transparent (hidden, as in during a presentation when you want to smoothly animate it into being a mask), >0 = mask layer and not hidden
}
function toLabel(target: FieldResult<Field>) {
if (typeof target === 'number' || Number(target)) {
@@ -82,42 +85,45 @@ interface PivotColumn {
filters: string[];
}
-export function computerPassLayout(poolData: Map<string, PoolData>, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], engineProps: any) {
+export function computePassLayout(poolData: Map<string, PoolData>, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], engineProps: any) {
const docMap = new Map<string, PoolData>();
childPairs.forEach(({ layout, data }, i) => {
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: '',
});
});
return normalizeResults(panelDim, 12, docMap, poolData, viewDefsToJSX, [], 0, []);
}
-export function computerStarburstLayout(poolData: Map<string, PoolData>, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], engineProps: any) {
- const mustFit = pivotDoc[WidthSym]() !== panelDim[0]; // if a panel size is set that's not the same as the pivot doc's size, then assume this is in a panel for a content fitting view (like a grid) in which case everything must be scaled to stay within the panel
+export function computeStarburstLayout(poolData: Map<string, PoolData>, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], engineProps: any) {
const docMap = new Map<string, PoolData>();
- const docSize = mustFit ? panelDim[0] * 0.33 : 75; // assume an icon sized at 75
- const burstRadius = mustFit ? panelDim : [NumCast(pivotDoc._starburstRadius, panelDim[0]) - docSize, NumCast(pivotDoc._starburstRadius, panelDim[1]) - docSize];
- const scaleDim = [burstRadius[0] * 2 + docSize, burstRadius[1] * 2 + docSize];
+ const burstDiam = [NumCast(pivotDoc._width), NumCast(pivotDoc._height)];
+ const burstScale = NumCast(pivotDoc._starburstDocScale, 1);
childPairs.forEach(({ layout, data }, i) => {
- const docSize = layout.layoutKey === 'layout_icon' ? (mustFit ? panelDim[0] * 0.33 : 75) : 400; // assume a icon sized at 75
+ 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.cos(deg) * burstRadius[0] - docSize / 2,
- y: Math.sin(deg) * burstRadius[1] - (docSize * layout[HeightSym]()) / layout[WidthSym]() / 2,
- width: docSize, //layout[WidthSym](),
- height: (docSize * layout[HeightSym]()) / layout[WidthSym](),
+ x: Math.min(burstDiam[0] / 2 - docSize, Math.max(-burstDiam[0] / 2, (Math.cos(deg) * burstDiam[0]) / 2 - docSize / 2)),
+ y: Math.min(burstDiam[1] / 2 - docSize * aspect, Math.max(-burstDiam[1] / 2, (Math.sin(deg) * burstDiam[1]) / 2 - (docSize / 2) * aspect)),
+ width: docSize,
+ height: docSize * aspect,
zIndex: NumCast(layout.zIndex),
pair: { layout, data },
replica: '',
+ color: 'white',
+ backgroundColor: 'white',
+ transition: 'all 0.3s',
});
});
- const divider = { type: 'div', color: 'transparent', x: -burstRadius[0], y: 0, width: 15, height: 15, payload: undefined };
- return normalizeResults(scaleDim, 12, docMap, poolData, viewDefsToJSX, [], 0, [divider]);
+ const divider = { type: 'div', color: 'transparent', x: -burstDiam[0] / 2, y: -burstDiam[1] / 2, width: 15, height: 15, payload: undefined };
+ return normalizeResults(burstDiam, 12, docMap, poolData, viewDefsToJSX, [], 0, [divider]);
}
export function computePivotLayout(poolData: Map<string, PoolData>, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], engineProps: any) {
@@ -128,20 +134,15 @@ export function computePivotLayout(poolData: Map<string, PoolData>, pivotDoc: Do
let nonNumbers = 0;
const pivotFieldKey = toLabel(engineProps?.pivotField ?? pivotDoc._pivotField) || 'author';
childPairs.map(pair => {
- const lval =
- pivotFieldKey === '#' || pivotFieldKey === 'tags'
- ? Array.from(Object.keys(Doc.GetProto(pair.layout)))
- .filter(k => k.startsWith('#'))
- .map(k => k.substring(1))
- : Cast(pair.layout[pivotFieldKey], listSpec('string'), null);
+ const listValue = Cast(pair.layout[pivotFieldKey], listSpec('string'), null);
const num = toNumber(pair.layout[pivotFieldKey]);
if (num === undefined || Number.isNaN(num)) {
nonNumbers++;
}
const val = Field.toString(pair.layout[pivotFieldKey] as Field);
- if (lval) {
- lval.forEach((val, i) => {
+ if (listValue) {
+ listValue.forEach((val, i) => {
!pivotColumnGroups.get(val) && pivotColumnGroups.set(val, { docs: [], filters: [val], replicas: [] });
pivotColumnGroups.get(val)!.docs.push(pair.layout);
pivotColumnGroups.get(val)!.replicas.push(i.toString());
@@ -154,8 +155,8 @@ export function computePivotLayout(poolData: Map<string, PoolData>, pivotDoc: Do
docMap.set(pair.layout[Id], {
x: 0,
y: 0,
- zIndex: -99,
- width: 0,
+ zIndex: 0,
+ width: 0, // should make doc hidden in CollectionFreefromDocumentView
height: 0,
pair,
replica: '',
@@ -406,7 +407,7 @@ function normalizeResults(
.map(ele => {
const newPosRaw = ele[1];
if (newPosRaw) {
- const newPos = {
+ const newPos: PoolData = {
x: newPosRaw.x * scale,
y: newPosRaw.y * scale,
z: newPosRaw.z,
@@ -415,8 +416,12 @@ function normalizeResults(
zIndex: newPosRaw.zIndex,
width: (newPosRaw.width || 0) * scale,
height: newPosRaw.height! * scale,
+ backgroundColor: newPosRaw.backgroundColor,
+ opacity: newPosRaw.opacity,
+ color: newPosRaw.color,
pair: ele[1].pair,
};
+ if (newPosRaw.transition) newPos.transition = newPosRaw.transition;
poolData.set(newPos.pair.layout[Id] + (newPos.replica || ''), { transition: 'all 1s', ...newPos });
}
});
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
index 858719a08..b44acfce8 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
@@ -5,15 +5,8 @@
transition: opacity 0.5s ease-in;
fill: transparent;
}
-.collectionfreeformlinkview-linkCircle {
- stroke: rgb(0,0,0);
- opacity: 0.5;
- pointer-events: all;
- cursor: pointer;
-}
.collectionfreeformlinkview-linkText {
- stroke: rgb(0,0,0);
- opacity: 0.5;
+ stroke: rgb(0, 0, 0);
pointer-events: all;
cursor: move;
}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index bf9de6760..f1d98d22a 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -1,9 +1,10 @@
import { action, computed, IReactionDisposer, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import { Doc, Field } from '../../../../fields/Doc';
+import { DocCss } from '../../../../fields/DocSymbols';
import { Id } from '../../../../fields/FieldSymbols';
import { List } from '../../../../fields/List';
-import { Cast, NumCast } from '../../../../fields/Types';
+import { Cast, NumCast, StrCast } from '../../../../fields/Types';
import { emptyFunction, setupMoveUpEvents, Utils } from '../../../../Utils';
import { LinkManager } from '../../../util/LinkManager';
import { SelectionManager } from '../../../util/SelectionManager';
@@ -34,17 +35,17 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
this._anchorDisposer = reaction(
() => [
this.props.A.props.ScreenToLocalTransform(),
- Cast(Cast(Cast(this.props.A.rootDoc, Doc, null)?.anchor1, Doc, null)?.annotationOn, Doc, null)?.scrollTop,
- Cast(Cast(Cast(this.props.A.rootDoc, Doc, null)?.anchor1, Doc, null)?.annotationOn, Doc, null)?._highlights,
+ 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)?.[DocCss],
this.props.B.props.ScreenToLocalTransform(),
- Cast(Cast(Cast(this.props.A.rootDoc, Doc, null)?.anchor2, Doc, null)?.annotationOn, Doc, null)?.scrollTop,
- Cast(Cast(Cast(this.props.A.rootDoc, Doc, null)?.anchor2, Doc, null)?.annotationOn, Doc, null)?._highlights,
+ 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)?.[DocCss],
],
action(() => {
this._start = Date.now();
this._timeout && clearTimeout(this._timeout);
this._timeout = setTimeout(this.timeout, 25);
- setTimeout(this.placeAnchors);
+ setTimeout(this.placeAnchors, 10); // when docs are dragged, their transforms will update before a render has been performed. placeanchors needs to come after a render to find things in the dom. a 0 timeout will still come before the render
}),
{ fireImmediately: true }
);
@@ -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.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();
@@ -72,36 +73,36 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
// if there's an element in the DOM with a classname containing a link anchor's id,
// then that DOM element is a hyperlink source for the current anchor and we want to place our link box at it's top right
// otherwise, we just use the computed nearest point on the document boundary to the target Document
- const targetAhyperlink = Array.from(window.document.getElementsByClassName((linkDoc.anchor1 as Doc)[Id])).lastElement();
- const targetBhyperlink = Array.from(window.document.getElementsByClassName((linkDoc.anchor2 as Doc)[Id])).lastElement();
+ const targetAhyperlink = Array.from(window.document.getElementsByClassName((linkDoc.link_anchor_1 as Doc)[Id])).lastElement();
+ const targetBhyperlink = Array.from(window.document.getElementsByClassName((linkDoc.link_anchor_2 as Doc)[Id])).lastElement();
if ((!targetAhyperlink && !a.width) || (!targetBhyperlink && !b.width)) return;
if (!targetAhyperlink) {
- if (linkDoc.linkAutoMove) {
- linkDoc.anchor1_x = ((apt.point.x - aleft) / awidth) * 100;
- linkDoc.anchor1_y = ((apt.point.y - atop) / aheight) * 100;
+ if (linkDoc.link_autoMoveAnchors) {
+ linkDoc.link_anchor_1_x = ((apt.point.x - aleft) / awidth) * 100;
+ linkDoc.link_anchor_1_y = ((apt.point.y - atop) / aheight) * 100;
}
} else {
const m = targetAhyperlink.getBoundingClientRect();
const mp = A.props.ScreenToLocalTransform().transformPoint(m.right, m.top + 5);
const mpx = mp[0] / A.props.PanelWidth();
const mpy = mp[1] / A.props.PanelHeight();
- if (mpx >= 0 && mpx <= 1) linkDoc.anchor1_x = mpx * 100;
- if (mpy >= 0 && mpy <= 1) linkDoc.anchor1_y = mpy * 100;
+ if (mpx >= 0 && mpx <= 1) linkDoc.link_anchor_1_x = mpx * 100;
+ if (mpy >= 0 && mpy <= 1) linkDoc.link_anchor_1_y = mpy * 100;
if (getComputedStyle(targetAhyperlink).fontSize === '0px') linkDoc.opacity = 0;
else linkDoc.opacity = 1;
}
if (!targetBhyperlink) {
- if (linkDoc.linkAutoMove) {
- linkDoc.anchor2_x = ((bpt.point.x - bleft) / bwidth) * 100;
- linkDoc.anchor2_y = ((bpt.point.y - btop) / bheight) * 100;
+ if (linkDoc.link_autoMoveAnchors) {
+ linkDoc.link_anchor_2_x = ((bpt.point.x - bleft) / bwidth) * 100;
+ linkDoc.link_anchor_2_y = ((bpt.point.y - btop) / bheight) * 100;
}
} else {
const m = targetBhyperlink.getBoundingClientRect();
const mp = B.props.ScreenToLocalTransform().transformPoint(m.right, m.top + 5);
const mpx = mp[0] / B.props.PanelWidth();
const mpy = mp[1] / B.props.PanelHeight();
- if (mpx >= 0 && mpx <= 1) linkDoc.anchor2_x = mpx * 100;
- if (mpy >= 0 && mpy <= 1) linkDoc.anchor2_y = mpy * 100;
+ if (mpx >= 0 && mpx <= 1) linkDoc.link_anchor_2_x = mpx * 100;
+ if (mpy >= 0 && mpy <= 1) linkDoc.link_anchor_2_y = mpy * 100;
if (getComputedStyle(targetBhyperlink).fontSize === '0px') linkDoc.opacity = 0;
else linkDoc.opacity = 1;
}
@@ -112,17 +113,21 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
this,
e,
(e, down, delta) => {
- this.props.LinkDocs[0].linkOffsetX = NumCast(this.props.LinkDocs[0].linkOffsetX) + delta[0];
- this.props.LinkDocs[0].linkOffsetY = NumCast(this.props.LinkDocs[0].linkOffsetY) + delta[1];
+ this.props.LinkDocs[0].link_relationship_OffsetX = NumCast(this.props.LinkDocs[0].link_relationship_OffsetX) + delta[0];
+ this.props.LinkDocs[0].link_relationship_OffsetY = NumCast(this.props.LinkDocs[0].link_relationship_OffsetY) + delta[1];
return false;
},
emptyFunction,
- () => {
+ action(() => {
+ SelectionManager.DeselectAll();
+ SelectionManager.SelectSchemaViewDoc(this.props.LinkDocs[0], true);
+ LinkManager.currentLink = this.props.LinkDocs[0];
+ this.toggleProperties();
// OverlayView.Instance.addElement(
// <LinkEditor sourceDoc={this.props.A.props.Document} linkDoc={this.props.LinkDocs[0]}
// showLinks={action(() => { })}
// />, { x: 300, y: 300 });
- }
+ })
);
};
@@ -132,6 +137,7 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
height = rect.height;
var el = el.parentNode;
while (el && el !== document.body) {
+ if (el.className === 'tabDocView-content') break;
rect = el.getBoundingClientRect?.();
if (rect?.width) {
if (top <= rect.bottom === false && getComputedStyle(el).overflow === 'hidden') return rect.bottom;
@@ -163,15 +169,16 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
@action
toggleProperties = () => {
- if (SettingsManager.propertiesWidth > 0) {
- SettingsManager.propertiesWidth = 0;
- } else {
+ if ((SettingsManager.propertiesWidth ?? 0) < 100) {
SettingsManager.propertiesWidth = 250;
}
};
+ @action
onClickLine = () => {
+ SelectionManager.DeselectAll();
SelectionManager.SelectSchemaViewDoc(this.props.LinkDocs[0], true);
+ LinkManager.currentLink = this.props.LinkDocs[0];
this.toggleProperties();
};
@@ -199,15 +206,17 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
const bclipped = bleft !== b.left || btop !== b.top;
if (aclipped && bclipped) return undefined;
const clipped = aclipped || bclipped;
+ const pt1inside = NumCast(LinkDocs[0].link_anchor_1_x) % 100 !== 0 && NumCast(LinkDocs[0].link_anchor_1_y) % 100 !== 0;
+ const pt2inside = NumCast(LinkDocs[0].link_anchor_2_x) % 100 !== 0 && NumCast(LinkDocs[0].link_anchor_2_y) % 100 !== 0;
const pt1 = [aleft + a.width / 2, atop + a.height / 2];
const pt2 = [bleft + b.width / 2, btop + b.width / 2];
- const pt1vec = [(bDocBounds.left + bDocBounds.right) / 2 - pt1[0], (bDocBounds.top + bDocBounds.bottom) / 2 - pt1[1]];
- const pt2vec = [(aDocBounds.left + aDocBounds.right) / 2 - pt2[0], (aDocBounds.top + aDocBounds.bottom) / 2 - pt2[1]];
+ const pt2vec = pt2inside ? [-0.7071, 0.7071] : [(bDocBounds.left + bDocBounds.right) / 2 - pt2[0], (bDocBounds.top + bDocBounds.bottom) / 2 - pt2[1]];
+ const pt1vec = pt1inside ? [-0.7071, 0.7071] : [(aDocBounds.left + aDocBounds.right) / 2 - pt1[0], (aDocBounds.top + aDocBounds.bottom) / 2 - pt1[1]];
const pt1len = Math.sqrt(pt1vec[0] * pt1vec[0] + pt1vec[1] * pt1vec[1]);
const pt2len = Math.sqrt(pt2vec[0] * pt2vec[0] + pt2vec[1] * pt2vec[1]);
const ptlen = Math.sqrt((pt1[0] - pt2[0]) * (pt1[0] - pt2[0]) + (pt1[1] - pt2[1]) * (pt1[1] - pt2[1])) / 2;
- const pt1norm = clipped ? [0, 0] : [(pt1vec[0] / pt1len) * ptlen, (pt1vec[1] / pt1len) * ptlen];
- const pt2norm = clipped ? [0, 0] : [(pt2vec[0] / pt2len) * ptlen, (pt2vec[1] / pt2len) * ptlen];
+ const pt1norm = clipped ? [0, 0] : [-(pt1vec[0] / pt1len) * ptlen, -(pt1vec[1] / pt1len) * ptlen];
+ const pt2norm = clipped ? [0, 0] : [-(pt2vec[0] / pt2len) * ptlen, -(pt2vec[1] / pt2len) * ptlen];
const pt1normlen = Math.sqrt(pt1norm[0] * pt1norm[0] + pt1norm[1] * pt1norm[1]) || 1;
const pt2normlen = Math.sqrt(pt2norm[0] * pt2norm[0] + pt2norm[1] * pt2norm[1]) || 1;
const pt1normalized = [pt1norm[0] / pt1normlen, pt1norm[1] / pt1normlen];
@@ -215,53 +224,84 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
const aActive = A.isSelected() || Doc.IsBrushed(A.rootDoc);
const bActive = B.isSelected() || Doc.IsBrushed(B.rootDoc);
- const textX = (Math.min(pt1[0], pt2[0]) + Math.max(pt1[0], pt2[0])) / 2 + NumCast(LinkDocs[0].linkOffsetX);
- const textY = (pt1[1] + pt2[1]) / 2 + NumCast(LinkDocs[0].linkOffsetY);
- return { a, b, pt1norm, pt2norm, aActive, bActive, textX, textY, pt1: [pt1[0] + pt1normalized[0] * 13, pt1[1] + pt1normalized[1] * 13], pt2: [pt2[0] + pt2normalized[0] * 13, pt2[1] + pt2normalized[1] * 13] };
+ const textX = (Math.min(pt1[0], pt2[0]) + Math.max(pt1[0], pt2[0])) / 2 + NumCast(LinkDocs[0].link_relationship_OffsetX);
+ const textY = (pt1[1] + pt2[1]) / 2 + NumCast(LinkDocs[0].link_relationship_OffsetY);
+ return {
+ a,
+ b,
+ pt1norm,
+ pt2norm,
+ aActive,
+ bActive,
+ textX,
+ textY,
+ pt1: [pt1[0] + pt1normalized[0] * 13, pt1[1] + pt1normalized[1] * 13],
+ pt2: [pt2[0] + pt2normalized[0] * 13, pt2[1] + pt2normalized[1] * 13],
+ };
}
render() {
if (!this.renderData) return null;
+ const link = this.props.LinkDocs[0];
const { a, b, pt1norm, pt2norm, aActive, bActive, textX, textY, pt1, pt2 } = this.renderData;
- LinkManager.currentLink = this.props.LinkDocs[0];
- const linkRelationship = Field.toString(LinkManager.currentLink?.linkRelationship as any as Field); //get string representing relationship
- const linkRelationshipList = Doc.UserDoc().linkRelationshipList as List<string>;
- const linkColorList = Doc.UserDoc().linkColorList as List<string>;
- const linkRelationshipSizes = Doc.UserDoc().linkRelationshipSizes as List<number>;
+ const linkRelationship = Field.toString(link?.link_relationship as any as Field); //get string representing relationship
+ const linkRelationshipList = Doc.UserDoc().link_relationshipList as List<string>;
+ const linkColorList = Doc.UserDoc().link_ColorList as List<string>;
+ const linkRelationshipSizes = Doc.UserDoc().link_relationshipSizes as List<number>;
const currRelationshipIndex = linkRelationshipList.indexOf(linkRelationship);
+ const linkDescription = Field.toString(link.link_description as any as Field);
const linkSize = currRelationshipIndex === -1 || currRelationshipIndex >= linkRelationshipSizes.length ? -1 : linkRelationshipSizes[currRelationshipIndex];
//access stroke color using index of the relationship in the color list (default black)
- const stroke = currRelationshipIndex === -1 || currRelationshipIndex >= linkColorList.length ? 'black' : linkColorList[currRelationshipIndex];
+ const stroke = currRelationshipIndex === -1 || currRelationshipIndex >= linkColorList.length ? StrCast(link._backgroundColor, 'black') : linkColorList[currRelationshipIndex];
// const hexStroke = this.rgbToHex(stroke)
//calculate stroke width/thickness based on the relative importance of the relationshipship (i.e. how many links the relationship has)
//thickness varies linearly from 3px to 12px for increasing link count
const strokeWidth = linkSize === -1 ? '3px' : Math.floor(2 + 10 * (linkSize / Math.max(...linkRelationshipSizes))) + 'px';
- if (this.props.LinkDocs[0].displayArrow === undefined) {
- this.props.LinkDocs[0].displayArrow = false;
+ if (link.link_displayArrow === undefined) {
+ link.link_displayArrow = false;
}
- return this.props.LinkDocs[0].opacity === 0 || !a.width || !b.width || (!this.props.LinkDocs[0].linkDisplay && !aActive && !bActive) ? null : (
+ return link.opacity === 0 || !a.width || !b.width || (!link.link_displayLine && !aActive && !bActive) ? null : (
<>
<defs>
- <marker id="arrowhead" markerWidth="4" markerHeight="3" refX="0" refY="1.5" orient="auto">
- <polygon points="0 0, 3 1.5, 0 3" fill={Colors.DARK_GRAY} />
+ <marker id={`${link[Id] + 'arrowhead'}`} markerWidth="4" markerHeight="3" refX="0" refY="1.5" orient="auto">
+ <polygon points="0 0, 3 1.5, 0 3" fill={stroke} />
</marker>
+ <filter id="outline">
+ <feMorphology in="SourceAlpha" result="expanded" operator="dilate" radius="1" />
+ <feFlood floodColor={`${Colors.DARK_GRAY}`} />
+ <feComposite in2="expanded" operator="in" />
+ <feComposite in="SourceGraphic" />
+ </filter>
+ <filter x="0" y="0" width="1" height="1" id={`${link[Id] + 'background'}`}>
+ <feFlood floodColor={`${StrCast(link._backgroundColor, 'white')}`} result="bg" />
+ <feMerge>
+ <feMergeNode in="bg" />
+ <feMergeNode in="SourceGraphic" />
+ </feMerge>
+ </filter>
</defs>
<path
+ filter={LinkManager.currentLink === link ? 'url(#outline)' : ''}
+ fill="pink"
+ stroke="antiquewhite"
+ strokeWidth="4"
className="collectionfreeformlinkview-linkLine"
- style={{ pointerEvents: 'all', opacity: this._opacity, stroke: SelectionManager.SelectedSchemaDoc() === this.props.LinkDocs[0] ? Colors.MEDIUM_BLUE : stroke, strokeWidth }}
+ 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={this.props.LinkDocs[0].displayArrow ? 'url(#arrowhead)' : ''}
+ markerEnd={link.link_displayArrow ? `url(#${link[Id] + 'arrowhead'})` : ''}
/>
- {textX === undefined ? null : (
- <text className="collectionfreeformlinkview-linkText" x={textX} y={textY} onPointerDown={this.pointerDown}>
- {Field.toString(this.props.LinkDocs[0].description as any as Field)}
+ {textX === undefined || !linkDescription ? null : (
+ <text filter={`url(#${link[Id] + 'background'})`} className="collectionfreeformlinkview-linkText" x={textX} y={textY} onPointerDown={this.pointerDown}>
+ <tspan>&nbsp;</tspan>
+ <tspan dy="2">{linkDescription}</tspan>
+ <tspan dy="2">&nbsp;</tspan>
</text>
)}
</>
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
index b8344dc0c..420e6a318 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
@@ -2,13 +2,13 @@ import { computed } from 'mobx';
import { observer } from 'mobx-react';
import { Id } from '../../../../fields/FieldSymbols';
import { DocumentManager } from '../../../util/DocumentManager';
+import { LightboxView } from '../../LightboxView';
import './CollectionFreeFormLinksView.scss';
import { CollectionFreeFormLinkView } from './CollectionFreeFormLinkView';
import React = require('react');
-import { LightboxView } from '../../LightboxView';
@observer
-export class CollectionFreeFormLinksView extends React.Component<React.PropsWithChildren<{}>> {
+export class CollectionFreeFormLinksView extends React.Component {
@computed get uniqueConnections() {
return Array.from(new Set(DocumentManager.Instance.LinkedDocumentViews))
.filter(c => !LightboxView.LightboxDoc || (LightboxView.IsLightboxDocView(c.a.docViewPath) && LightboxView.IsLightboxDocView(c.b.docViewPath)))
@@ -19,7 +19,6 @@ export class CollectionFreeFormLinksView extends React.Component<React.PropsWith
return (
<div className="collectionfreeformlinksview-container">
<svg className="collectionfreeformlinksview-svgCanvas">{this.uniqueConnections}</svg>
- {this.props.children}
</div>
);
}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
index d80fcdfc3..c6419885b 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
@@ -33,7 +33,7 @@
}
.collectionfreeformview-mask {
mix-blend-mode: multiply;
- background-color: rgba(0, 0, 0, 0.7);
+ background-color: rgba(0, 0, 0, 0.8);
}
.collectionfreeformview-viewdef {
@@ -96,37 +96,6 @@
border-color: #69a5db;
}
-.progressivizeButton {
- position: absolute;
- display: grid;
- grid-template-columns: auto 20px auto;
- transform: translate(-105%, 0);
- align-items: center;
- border: black solid 1px;
- border-radius: 3px;
- justify-content: center;
- width: 40;
- z-index: 30000;
- height: 20;
- overflow: hidden;
- background-color: #d5dce2;
- transition: all 1s;
-
- .progressivizeButton-prev:hover {
- color: #5a9edd;
- }
-
- .progressivizeButton-frame {
- justify-self: center;
- text-align: center;
- width: 15px;
- }
-
- .progressivizeButton-next:hover {
- color: #5a9edd;
- }
-}
-
.resizable {
background: rgba(0, 0, 0, 0.2);
width: 100px;
@@ -178,25 +147,6 @@
}
}
-.progressivizeMove-frame {
- width: 20px;
- border-radius: 2px;
- z-index: 100000;
- color: white;
- text-align: center;
- background-color: #5a9edd;
- transform: translate(-110%, 110%);
-}
-
-.progressivizeButton:hover {
- box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.5);
-
- .progressivizeButton-frame {
- background-color: #5a9edd;
- color: white;
- }
-}
-
.collectionFreeform-customText {
position: absolute;
text-align: center;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 2f246e74f..e6f8f3071 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,27 +1,27 @@
import { Bezier } from 'bezier-js';
-import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
+import { Colors } from 'browndash-components';
+import { action, computed, IReactionDisposer, observable, reaction, runInAction, trace } from 'mobx';
import { observer } from 'mobx-react';
import { computedFn } from 'mobx-utils';
import { DateField } from '../../../../fields/DateField';
-import { DataSym, Doc, DocListCast, HeightSym, Opt, StrListCast, 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';
-import { ObjectField } from '../../../../fields/ObjectField';
import { RichTextField } from '../../../../fields/RichTextField';
import { listSpec } from '../../../../fields/Schema';
import { ScriptField } from '../../../../fields/ScriptField';
-import { BoolCast, Cast, FieldValue, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
+import { BoolCast, Cast, DocCast, FieldValue, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
import { ImageField } from '../../../../fields/URLField';
import { TraceMobx } from '../../../../fields/util';
import { GestureUtils } from '../../../../pen-gestures/GestureUtils';
-import { aggregateBounds, emptyFunction, intersectRect, returnFalse, setupMoveUpEvents, Utils } from '../../../../Utils';
+import { aggregateBounds, emptyFunction, intersectRect, returnFalse, returnNone, returnTrue, returnZero, setupMoveUpEvents, Utils } from '../../../../Utils';
import { CognitiveServices } from '../../../cognitive_services/CognitiveServices';
import { Docs, DocUtils } from '../../../documents/Documents';
import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
import { DocumentManager } from '../../../util/DocumentManager';
import { DragManager, dropActionType } from '../../../util/DragManager';
-import { HistoryUtil } from '../../../util/History';
import { InteractionUtils } from '../../../util/InteractionUtils';
import { ReplayMovements } from '../../../util/ReplayMovements';
import { ScriptingGlobals } from '../../../util/ScriptingGlobals';
@@ -33,34 +33,37 @@ import { undoBatch, UndoManager } from '../../../util/UndoManager';
import { COLLECTION_BORDER_WIDTH } from '../../../views/global/globalCssVariables.scss';
import { Timeline } from '../../animationtimeline/Timeline';
import { ContextMenu } from '../../ContextMenu';
+import { DocumentDecorations } from '../../DocumentDecorations';
import { GestureOverlay } from '../../GestureOverlay';
import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, InkingStroke, SetActiveInkColor, SetActiveInkWidth } from '../../InkingStroke';
import { LightboxView } from '../../LightboxView';
import { CollectionFreeFormDocumentView } from '../../nodes/CollectionFreeFormDocumentView';
-import { DocFocusOptions, DocumentView, DocumentViewProps, ViewAdjustment, ViewSpecPrefix } from '../../nodes/DocumentView';
+import { DocFocusOptions, DocumentView, DocumentViewProps, OpenWhere } from '../../nodes/DocumentView';
import { FieldViewProps } from '../../nodes/FieldView';
import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
-import { PresBox } from '../../nodes/trails/PresBox';
-import { VideoBox } from '../../nodes/VideoBox';
+import { PinProps, PresBox } from '../../nodes/trails/PresBox';
import { CreateImage } from '../../nodes/WebBoxRenderer';
import { StyleProp } from '../../StyleProvider';
import { CollectionSubView } from '../CollectionSubView';
import { TreeViewType } from '../CollectionTreeView';
import { TabDocView } from '../TabDocView';
-import { computePivotLayout, computerPassLayout, computerStarburstLayout, computeTimelineLayout, PoolData, ViewDefBounds, ViewDefResult } from './CollectionFreeFormLayoutEngines';
+import { computePassLayout, computePivotLayout, computeStarburstLayout, computeTimelineLayout, PoolData, ViewDefBounds, ViewDefResult } from './CollectionFreeFormLayoutEngines';
import { CollectionFreeFormRemoteCursors } from './CollectionFreeFormRemoteCursors';
import './CollectionFreeFormView.scss';
import { MarqueeView } from './MarqueeView';
import React = require('react');
export type collectionFreeformViewProps = {
+ NativeWidth?: () => number;
+ NativeHeight?: () => number;
+ originTopLeft?: boolean;
annotationLayerHostsContent?: boolean; // whether to force scaling of content (needed by ImageBox)
viewDefDivClick?: ScriptField;
childPointerEvents?: string;
- scaleField?: string;
+ viewField?: string;
noOverlay?: boolean; // used to suppress docs in the overlay (z) layer (ie, for minimap since overlay doesn't scale)
engineProps?: any;
- dontScaleFilter?: (doc: Doc) => boolean; // whether this collection should scale documents to fit their panel vs just scrolling them
+ getScrollHeight?: () => number | undefined;
dontRenderDocuments?: boolean; // used for annotation overlays which need to distribute documents into different freeformviews with different mixBlendModes depending on whether they are transparent or not.
// However, this screws up interactions since only the top layer gets events. so we render the freeformview a 3rd time with all documents in order to get interaction events (eg., marquee) but we don't actually want to display the documents.
};
@@ -71,7 +74,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
return 'CollectionFreeFormView(' + this.props.Document.title?.toString() + ')';
} // this makes mobx trace() statements more descriptive
- private _lastNudge: any;
+ @observable
+ public static ShowPresPaths = false;
+
+ private _panZoomTransitionTimer: any;
private _lastX: number = 0;
private _lastY: number = 0;
private _downX: number = 0;
@@ -91,24 +97,27 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
private _brushtimer: any;
private _brushtimer1: any;
- // private isWritingMode: boolean = true;
- // private writingModeDocs: Doc[] = [];
-
private get isAnnotationOverlay() {
return this.props.isAnnotationOverlay;
}
- private get scaleFieldKey() {
- return this.props.scaleField || '_viewScale';
+ public get scaleFieldKey() {
+ return (this.props.viewField ?? '') + '_freeform_scale';
+ }
+ private get panXFieldKey() {
+ return (this.props.viewField ?? '') + '_freeform_panX';
+ }
+ private get panYFieldKey() {
+ return (this.props.viewField ?? '') + '_freeform_panY';
}
private get borderWidth() {
return this.isAnnotationOverlay ? 0 : COLLECTION_BORDER_WIDTH;
}
@observable.shallow _layoutElements: ViewDefResult[] = []; // shallow because some layout items (eg pivot labels) are just generated 'divs' and can't be frozen as observables
- @observable _viewTransition: number = 0; // sets the pan/zoom transform ease time- used by nudge(), focus() etc to smoothly zoom/pan. set to 0 to use document's transition time or default of 0
+ @observable _panZoomTransition: number = 0; // sets the pan/zoom transform ease time- used by nudge(), focus() etc to smoothly zoom/pan. set to 0 to use document's transition time or default of 0
@observable _hLines: number[] | undefined;
@observable _vLines: number[] | undefined;
- @observable _firstRender = true; // this turns off rendering of the collection's content so that there's instant feedback when a tab is switched of what content will be shown.
+ @observable _firstRender = false; // this turns off rendering of the collection's content so that there's instant feedback when a tab is switched of what content will be shown. could be used for performance improvement
@observable _showAnimTimeline = false;
@observable _clusterSets: Doc[][] = [];
@observable _deleteList: DocumentView[] = [];
@@ -118,6 +127,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
@observable ChildDrag: DocumentView | undefined; // child document view being dragged. needed to update drop areas of groups when a group item is dragged.
@observable _brushedView = { width: 0, height: 0, panX: 0, panY: 0, opacity: 0 }; // highlighted region of freeform canvas used by presentations to indicate a region
+ constructor(props: any) {
+ super(props);
+ }
+
@computed get views() {
const viewsMask = this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z && ele.inkMask !== -1 && ele.inkMask !== undefined).map(ele => ele.ele);
const renderableEles = this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z && (ele.inkMask === -1 || ele.inkMask === undefined)).map(ele => ele.ele);
@@ -127,11 +140,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
@computed get fitToContentVals() {
return {
bounds: { ...this.contentBounds, cx: (this.contentBounds.x + this.contentBounds.r) / 2, cy: (this.contentBounds.y + this.contentBounds.b) / 2 },
- scale: !this.childDocs.length ? 1 : Math.min(this.props.PanelHeight() / (this.contentBounds.b - this.contentBounds.y), this.props.PanelWidth() / (this.contentBounds.r - this.contentBounds.x)),
+ scale:
+ !this.childDocs.length || !Number.isFinite(this.contentBounds.b - this.contentBounds.y) || !Number.isFinite(this.contentBounds.r - this.contentBounds.x)
+ ? 1
+ : Math.min(this.props.PanelHeight() / (this.contentBounds.b - this.contentBounds.y), this.props.PanelWidth() / (this.contentBounds.r - this.contentBounds.x)),
};
}
@computed get fitContentsToBox() {
- return (this.props.fitContentsToBox?.() || this.Document._fitContentsToBox || this.Document.isGroup) && !this.isAnnotationOverlay;
+ return (this.props.fitContentsToBox?.() || this.Document._freeform_fitContentsToBox) && !this.isAnnotationOverlay;
}
@computed get contentBounds() {
const cb = Cast(this.rootDoc.contentBounds, listSpec('number'));
@@ -139,27 +155,27 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
? { x: cb[0], y: cb[1], r: cb[2], b: cb[3] }
: this.props.contentBounds?.() ??
aggregateBounds(
- this._layoutElements.filter(e => e.bounds && !e.bounds.z).map(e => e.bounds!),
+ this._layoutElements.filter(e => e.bounds && e.bounds.width && !e.bounds.z).map(e => e.bounds!),
NumCast(this.layoutDoc._xPadding, 10),
NumCast(this.layoutDoc._yPadding, 10)
);
}
@computed get nativeWidth() {
- return this.fitContentsToBox ? 0 : Doc.NativeWidth(this.Document, Cast(this.Document.resolvedDataDoc, Doc, null));
+ return this.props.NativeWidth?.() || (this.fitContentsToBox ? 0 : Doc.NativeWidth(this.Document, Cast(this.Document.resolvedDataDoc, Doc, null)));
}
@computed get nativeHeight() {
- return this.fitContentsToBox ? 0 : Doc.NativeHeight(this.Document, Cast(this.Document.resolvedDataDoc, Doc, null));
+ return this.props.NativeHeight?.() || (this.fitContentsToBox ? 0 : Doc.NativeHeight(this.Document, Cast(this.Document.resolvedDataDoc, Doc, null)));
}
@computed get cachedCenteringShiftX(): number {
const scaling = this.fitContentsToBox || !this.nativeDimScaling ? 1 : this.nativeDimScaling;
- return this.props.isAnnotationOverlay ? 0 : this.props.PanelWidth() / 2 / scaling; // shift so pan position is at center of window for non-overlay collections
+ return this.props.isAnnotationOverlay || this.props.originTopLeft ? 0 : this.props.PanelWidth() / 2 / scaling; // shift so pan position is at center of window for non-overlay collections
}
@computed get cachedCenteringShiftY(): number {
const dv = this.props.DocumentView?.();
const scaling = this.fitContentsToBox || !this.nativeDimScaling ? 1 : this.nativeDimScaling;
// if freeform has a native aspect, then the panel height needs to be adjusted to match it
- const aspect = dv?.nativeWidth && dv?.nativeHeight && !dv.layoutDoc.fitWidth ? dv.nativeHeight / dv.nativeWidth : this.props.PanelHeight() / this.props.PanelWidth();
- return this.props.isAnnotationOverlay ? 0 : (aspect * this.props.PanelWidth()) / 2 / scaling; // shift so pan position is at center of window for non-overlay collections
+ const aspect = dv?.nativeWidth && dv?.nativeHeight && !dv.layoutDoc.layout_fitWidth ? dv.nativeHeight / dv.nativeWidth : this.props.PanelHeight() / this.props.PanelWidth();
+ return this.props.isAnnotationOverlay || this.props.originTopLeft ? 0 : (aspect * this.props.PanelWidth()) / 2 / scaling; // shift so pan position is at center of window for non-overlay collections
}
@computed get cachedGetLocalTransform(): Transform {
return Transform.Identity()
@@ -173,6 +189,32 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
return this.getContainerTransform().translate(-this.cachedCenteringShiftX, -this.cachedCenteringShiftY).transform(this.cachedGetLocalTransform);
}
+ public static gotoKeyframe(timer: NodeJS.Timeout | undefined, docs: Doc[], duration: number) {
+ if (timer) clearTimeout(timer);
+ return DocumentView.SetViewTransition(docs, 'all', duration, undefined, true);
+ }
+ public static updateKeyframe(timer: NodeJS.Timeout | undefined, docs: Doc[], time: number) {
+ if (timer) clearTimeout(timer);
+ const newTimer = DocumentView.SetViewTransition(docs, 'all', 1000, undefined, true);
+ const timecode = Math.round(time);
+ docs.forEach(doc => {
+ CollectionFreeFormDocumentView.animFields.forEach(val => {
+ const findexed = Cast(doc[`${val.key}-indexed`], listSpec('number'), null);
+ findexed?.length <= timecode + 1 && findexed.push(undefined as any as number);
+ });
+ CollectionFreeFormDocumentView.animStringFields.forEach(val => {
+ const findexed = Cast(doc[`${val}-indexed`], listSpec('string'), null);
+ findexed?.length <= timecode + 1 && findexed.push(undefined as any as string);
+ });
+ CollectionFreeFormDocumentView.animDataFields(doc).forEach(val => {
+ const findexed = Cast(doc[`${val}-indexed`], listSpec(InkField), null);
+ findexed?.length <= timecode + 1 && findexed.push(undefined as any);
+ });
+ });
+ return newTimer;
+ }
+
+ _keyTimer: NodeJS.Timeout | undefined; // timer for turning off transition flag when key frame change has completed. Need to clear this if you do a second navigation before first finishes, or else first timer can go off during second naviation.
changeKeyFrame = (back = false) => {
const currentFrame = Cast(this.Document._currentFrame, 'number', null);
if (currentFrame === undefined) {
@@ -180,14 +222,17 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0);
}
if (back) {
- CollectionFreeFormDocumentView.gotoKeyframe(this.childDocs.slice());
+ this._keyTimer = CollectionFreeFormView.gotoKeyframe(this._keyTimer, [...this.childDocs, this.layoutDoc], 1000);
this.Document._currentFrame = Math.max(0, (currentFrame || 0) - 1);
} else {
- CollectionFreeFormDocumentView.updateKeyframe(this.childDocs, currentFrame || 0);
+ this._keyTimer = CollectionFreeFormView.updateKeyframe(this._keyTimer, [...this.childDocs, this.layoutDoc], currentFrame || 0);
this.Document._currentFrame = Math.max(0, (currentFrame || 0) + 1);
this.Document.lastFrame = Math.max(NumCast(this.Document._currentFrame), NumCast(this.Document.lastFrame));
}
};
+ @observable _keyframeEditing = false;
+ @action setKeyFrameEditing = (set: boolean) => (this._keyframeEditing = set);
+ getKeyFrameEditing = () => this._keyframeEditing;
onBrowseClickHandler = () => this.props.onBrowseClick?.() || ScriptCast(this.layoutDoc.onBrowseClick);
onChildClickHandler = () => this.props.childClickScript || ScriptCast(this.Document.onChildClick);
onChildDoubleClickHandler = () => this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick);
@@ -195,21 +240,19 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
shrinkWrap = () => {
if (this.props.DocumentView?.().nativeWidth) return;
const vals = this.fitToContentVals;
- this.layoutDoc._panX = vals.bounds.cx;
- this.layoutDoc._panY = vals.bounds.cy;
- this.layoutDoc._viewScale = vals.scale;
+ this.layoutDoc._freeform_panX = vals.bounds.cx;
+ this.layoutDoc._freeform_panY = vals.bounds.cy;
+ this.layoutDoc._freeform_scale = vals.scale;
};
freeformData = (force?: boolean) => (!this._firstRender && (this.fitContentsToBox || force) ? this.fitToContentVals : undefined);
reverseNativeScaling = () => (this.fitContentsToBox ? true : false);
- // panx, pany, zoomscale all attempt to get values first from the layout controller, then from the layout/dataDoc (or template layout doc), and finally from the resolved template data document.
+ // freeform_panx, freeform_pany, freeform_scale all attempt to get values first from the layout controller, then from the layout/dataDoc (or template layout doc), and finally from the resolved template data document.
// this search order, for example, allows icons of cropped images to find the panx/pany/zoom on the cropped image's data doc instead of the usual layout doc because the zoom/panX/panY define the cropped image
- panX = () => this.freeformData()?.bounds.cx ?? NumCast(this.Document._panX, NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.panX, 1));
- panY = () => this.freeformData()?.bounds.cy ?? NumCast(this.Document._panY, NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.panY, 1));
+ panX = () => this.freeformData()?.bounds.cx ?? NumCast(this.Document[this.panXFieldKey], NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.freeform_panX, 1));
+ panY = () => this.freeformData()?.bounds.cy ?? NumCast(this.Document[this.panYFieldKey], NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.freeform_panY, 1));
zoomScaling = () => this.freeformData()?.scale ?? NumCast(Doc.Layout(this.Document)[this.scaleFieldKey], NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.[this.scaleFieldKey], 1));
contentTransform = () =>
- !this.cachedCenteringShiftX && !this.cachedCenteringShiftY && this.zoomScaling() === 1
- ? ''
- : `translate(${this.cachedCenteringShiftX}px, ${this.cachedCenteringShiftY}px) scale(${this.zoomScaling()}) translate(${-this.panX()}px, ${-this.panY()}px)`;
+ this.props.isAnnotationOverlay && this.zoomScaling() === 1 ? `` : `translate(${this.cachedCenteringShiftX}px, ${this.cachedCenteringShiftY}px) scale(${this.zoomScaling()}) translate(${-this.panX()}px, ${-this.panY()}px)`;
getTransform = () => this.cachedGetTransform.copy();
getLocalTransform = () => this.cachedGetLocalTransform.copy();
getContainerTransform = () => this.cachedGetContainerTransform.copy();
@@ -221,7 +264,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
};
selectDocuments = (docs: Doc[]) => {
SelectionManager.DeselectAll();
- docs.map(doc => DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView)).map(dv => dv && SelectionManager.SelectView(dv, true));
+ docs.map(doc => DocumentManager.Instance.getDocumentView(doc, this.props.DocumentView?.())).map(dv => dv && SelectionManager.SelectView(dv, true));
};
addDocument = (newBox: Doc | Doc[]) => {
let retVal = false;
@@ -255,10 +298,44 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
isCurrent(doc: Doc) {
const dispTime = NumCast(doc._timecodeToShow, -1);
const endTime = NumCast(doc._timecodeToHide, dispTime + 1.5);
- const curTime = NumCast(this.Document._currentTimecode, -1);
- return dispTime === -1 || (curTime - dispTime >= -1e-4 && curTime <= endTime);
+ const curTime = NumCast(this.Document._layout_currentTimecode, -1);
+ return dispTime === -1 || curTime === -1 || (curTime - dispTime >= -1e-4 && curTime <= endTime);
}
+ groupFocus = (anchor: Doc, options: DocFocusOptions) => {
+ options.docTransform = new Transform(-NumCast(this.rootDoc[this.panXFieldKey]) + NumCast(anchor.x), -NumCast(this.rootDoc[this.panYFieldKey]) + NumCast(anchor.y), 1);
+ const res = this.props.focus(this.rootDoc, options);
+ options.docTransform = undefined;
+ return res;
+ };
+
+ focus = (anchor: Doc, options: DocFocusOptions) => {
+ if (this._lightboxDoc) return;
+ const xfToCollection = options?.docTransform ?? Transform.Identity();
+ const savedState = { panX: NumCast(this.Document[this.panXFieldKey]), panY: NumCast(this.Document[this.panYFieldKey]), scale: options?.willZoomCentered ? this.Document[this.scaleFieldKey] : undefined };
+ const cantTransform = this.fitContentsToBox || ((this.rootDoc._isGroup || this.layoutDoc._lockedTransform) && !LightboxView.LightboxDoc);
+ const { panX, panY, scale } = cantTransform || (!options.willPan && !options.willZoomCentered) ? savedState : this.calculatePanIntoView(anchor, xfToCollection, options?.willZoomCentered ? options?.zoomScale ?? 0.75 : undefined);
+
+ // focus on the document in the collection
+ const didMove = !cantTransform && !anchor.z && (panX !== savedState.panX || panY !== savedState.panY || scale !== savedState.scale);
+ if (didMove) options.didMove = true;
+ // glr: freeform transform speed can be set by adjusting presTransition field - needs a way of knowing when presentation is not active...
+ if (didMove) {
+ const focusTime = options?.instant ? 0 : options.zoomTime ?? 500;
+ (options.zoomScale ?? options.willZoomCentered) && scale && (this.Document[this.scaleFieldKey] = scale);
+ this.setPan(panX, panY, focusTime, true); // docs that are floating in their collection can't be panned to from their collection -- need to propagate the pan to a parent freeform somehow
+ return focusTime;
+ }
+ };
+
+ getView = async (doc: Doc): Promise<Opt<DocumentView>> => {
+ return new Promise<Opt<DocumentView>>(res => {
+ if (doc.hidden && this._lightboxDoc !== doc) doc.hidden = false;
+ const findDoc = (finish: (dv: DocumentView) => void) => DocumentManager.Instance.AddViewRenderedCb(doc, dv => finish(dv));
+ findDoc(dv => res(dv));
+ });
+ };
+
@action
internalDocDrop(e: Event, de: DragManager.DropEvent, docDragData: DragManager.DocumentDragData, xp: number, yp: number) {
if (!de.embedKey && !this.ChildDrag && this.rootDoc._isGroup) return false;
@@ -272,9 +349,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
.map(pair => pair.layout)
.slice()
.sort((doc1, doc2) => NumCast(doc1.zIndex) - NumCast(doc2.zIndex));
- zsorted.forEach((doc, index) => (doc.zIndex = doc.isInkMask ? 5000 : index + 1));
+ zsorted.forEach((doc, index) => (doc.zIndex = doc.stroke_isInkMask ? 5000 : index + 1));
const dvals = CollectionFreeFormDocumentView.getValues(refDoc, NumCast(refDoc.activeFrame, 1000));
const dropPos = this.Document._currentFrame !== undefined ? [NumCast(dvals.x), NumCast(dvals.y)] : [NumCast(refDoc.x), NumCast(refDoc.y)];
+
for (let i = 0; i < docDragData.droppedDocuments.length; i++) {
const d = docDragData.droppedDocuments[i];
const layoutDoc = Doc.Layout(d);
@@ -289,13 +367,35 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
d.x = x + NumCast(d.x) - dropPos[0];
d.y = y + NumCast(d.y) - dropPos[1];
}
- d._lastModified = new DateField();
+ d._layout_modificationDate = new DateField();
const nd = [Doc.NativeWidth(layoutDoc), Doc.NativeHeight(layoutDoc)];
layoutDoc._width = NumCast(layoutDoc._width, 300);
layoutDoc._height = NumCast(layoutDoc._height, nd[0] && nd[1] ? (nd[1] / nd[0]) * NumCast(layoutDoc._width) : 300);
(d._raiseWhenDragged === undefined ? DragManager.GetRaiseWhenDragged() : d._raiseWhenDragged) && (d.zIndex = zsorted.length + 1 + i); // bringToFront
}
+ if (this.layoutDoc._autoArrange || de.metaKey) {
+ const sorted = this.childLayoutPairs.slice().sort((a, b) => NumCast(a.layout.y) - NumCast(b.layout.y));
+ sorted.splice(
+ sorted.findIndex(pair => pair.layout === refDoc),
+ 1
+ );
+ if (sorted.length && refDoc && NumCast(sorted[0].layout.y) < NumCast(refDoc.y)) {
+ const topIndexed = NumCast(refDoc.y) < NumCast(sorted[0].layout.y) + NumCast(sorted[0].layout._height) / 2;
+ const deltay = sorted.length > 1 ? NumCast(refDoc.y) - (NumCast(sorted[0].layout.y) + (topIndexed ? 0 : NumCast(sorted[0].layout._height))) : 0;
+ const deltax = sorted.length > 1 ? NumCast(refDoc.x) - NumCast(sorted[0].layout.x) : 0;
+
+ let lastx = NumCast(refDoc.x);
+ let lasty = NumCast(refDoc.y) + (topIndexed ? 0 : NumCast(refDoc._height));
+ runInAction(() =>
+ sorted.slice(1).forEach((pair, i) => {
+ lastx = pair.layout.x = lastx + deltax;
+ lasty = (pair.layout.y = lasty + deltay) + (topIndexed ? 0 : NumCast(pair.layout._height));
+ })
+ );
+ }
+ }
+
(docDragData.droppedDocuments.length === 1 || de.shiftKey) && this.updateClusterDocs(docDragData.droppedDocuments);
return true;
}
@@ -318,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.context !== this.props.Document) {
- // if the source doc view's context isn't this same freeformcollectionlinkDragData.dragDocument.context === 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);
- de.complete.linkDocument = DocUtils.MakeLink({ doc: linkDragData.linkSourceGetAnchor() }, { doc: source }, 'annotated by:annotation of', ''); // TODODO this is where in text links get passed
+ 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;
}
@@ -347,13 +448,13 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
return this.childLayoutPairs
.map(pair => pair.layout)
.reduce((cluster, cd) => {
- const grouping = this.props.Document._useClusters ? NumCast(cd.cluster, -1) : NumCast(cd.group, -1);
+ const grouping = this.props.Document._freeform_useClusters ? NumCast(cd.layout_cluster, -1) : NumCast(cd.group, -1);
if (grouping !== -1) {
const layoutDoc = Doc.Layout(cd);
- const cx = NumCast(cd.x) - this._clusterDistance;
- const cy = NumCast(cd.y) - this._clusterDistance;
- const cw = NumCast(layoutDoc._width) + 2 * this._clusterDistance;
- const ch = NumCast(layoutDoc._height) + 2 * this._clusterDistance;
+ const cx = NumCast(cd.x) - this._clusterDistance / 2;
+ const cy = NumCast(cd.y) - this._clusterDistance / 2;
+ const cw = NumCast(layoutDoc._width) + this._clusterDistance;
+ const ch = NumCast(layoutDoc._height) + this._clusterDistance;
return !layoutDoc.z && intersectRect({ left: cx, top: cy, width: cw, height: ch }, { left: probe[0], top: probe[1], width: 1, height: 1 }) ? grouping : cluster;
}
return cluster;
@@ -364,10 +465,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
if (cluster !== -1) {
const ptsParent = e instanceof PointerEvent ? e : e.targetTouches.item(0);
if (ptsParent) {
- const eles = this.childLayoutPairs.map(pair => pair.layout).filter(cd => (this.props.Document._useClusters ? NumCast(cd.cluster) : NumCast(cd.group, -1)) === cluster);
- const clusterDocs = eles.map(ele => DocumentManager.Instance.getDocumentView(ele, this.props.CollectionView)!);
+ const eles = this.childLayoutPairs.map(pair => pair.layout).filter(cd => (this.props.Document._freeform_useClusters ? NumCast(cd.layout_cluster) : NumCast(cd.group, -1)) === cluster);
+ const clusterDocs = eles.map(ele => DocumentManager.Instance.getDocumentView(ele, this.props.DocumentView?.())!);
const { left, top } = clusterDocs[0].getBounds() || { left: 0, top: 0 };
- const de = new DragManager.DocumentDragData(eles, e.ctrlKey || e.altKey ? 'alias' : undefined);
+ const de = new DragManager.DocumentDragData(eles, e.ctrlKey || e.altKey ? 'embed' : undefined);
de.moveDocument = this.props.moveDocument;
de.offset = this.getTransform().transformDirection(ptsParent.clientX - left, ptsParent.clientY - top);
DragManager.StartDocumentDrag(
@@ -384,10 +485,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
return false;
}
- @undoBatch
@action
- updateClusters(_useClusters: boolean) {
- this.props.Document._useClusters = _useClusters;
+ updateClusters(_freeform_useClusters: boolean) {
+ this.props.Document._freeform_useClusters = _freeform_useClusters;
this._clusterSets.length = 0;
this.childLayoutPairs.map(pair => pair.layout).map(c => this.updateCluster(c));
}
@@ -395,70 +495,74 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
@action
updateClusterDocs(docs: Doc[]) {
const childLayouts = this.childLayoutPairs.map(pair => pair.layout);
- if (this.props.Document._useClusters) {
+ if (this.props.Document._freeform_useClusters) {
const docFirst = docs[0];
docs.map(doc => this._clusterSets.map(set => Doc.IndexOf(doc, set) !== -1 && set.splice(Doc.IndexOf(doc, set), 1)));
- const preferredInd = NumCast(docFirst.cluster);
- docs.map(doc => (doc.cluster = -1));
+ const preferredInd = NumCast(docFirst.layout_cluster);
+ docs.map(doc => (doc.layout_cluster = -1));
docs.map(doc =>
this._clusterSets.map((set, i) =>
set.map(member => {
- if (docFirst.cluster === -1 && Doc.IndexOf(member, childLayouts) !== -1 && Doc.overlapping(doc, member, this._clusterDistance)) {
- docFirst.cluster = i;
+ if (docFirst.layout_cluster === -1 && Doc.IndexOf(member, childLayouts) !== -1 && Doc.overlapping(doc, member, this._clusterDistance)) {
+ docFirst.layout_cluster = i;
}
})
)
);
- if (docFirst.cluster === -1 && preferredInd !== -1 && this._clusterSets.length > preferredInd && (!this._clusterSets[preferredInd] || !this._clusterSets[preferredInd].filter(member => Doc.IndexOf(member, childLayouts) !== -1).length)) {
- docFirst.cluster = preferredInd;
+ if (
+ docFirst.layout_cluster === -1 &&
+ preferredInd !== -1 &&
+ this._clusterSets.length > preferredInd &&
+ (!this._clusterSets[preferredInd] || !this._clusterSets[preferredInd].filter(member => Doc.IndexOf(member, childLayouts) !== -1).length)
+ ) {
+ docFirst.layout_cluster = preferredInd;
}
this._clusterSets.map((set, i) => {
- if (docFirst.cluster === -1 && !set.filter(member => Doc.IndexOf(member, childLayouts) !== -1).length) {
- docFirst.cluster = i;
+ if (docFirst.layout_cluster === -1 && !set.filter(member => Doc.IndexOf(member, childLayouts) !== -1).length) {
+ docFirst.layout_cluster = i;
}
});
- if (docFirst.cluster === -1) {
+ if (docFirst.layout_cluster === -1) {
docs.map(doc => {
- doc.cluster = this._clusterSets.length;
+ doc.layout_cluster = this._clusterSets.length;
this._clusterSets.push([doc]);
});
} else if (this._clusterSets.length) {
- for (let i = this._clusterSets.length; i <= NumCast(docFirst.cluster); i++) !this._clusterSets[i] && this._clusterSets.push([]);
- docs.map(doc => this._clusterSets[(doc.cluster = NumCast(docFirst.cluster))].push(doc));
+ for (let i = this._clusterSets.length; i <= NumCast(docFirst.layout_cluster); i++) !this._clusterSets[i] && this._clusterSets.push([]);
+ docs.map(doc => this._clusterSets[(doc.layout_cluster = NumCast(docFirst.layout_cluster))].push(doc));
}
- childLayouts.map(child => !this._clusterSets.some((set, i) => Doc.IndexOf(child, set) !== -1 && child.cluster === i) && this.updateCluster(child));
+ childLayouts.map(child => !this._clusterSets.some((set, i) => Doc.IndexOf(child, set) !== -1 && child.layout_cluster === i) && this.updateCluster(child));
}
}
- @undoBatch
@action
updateCluster(doc: Doc) {
const childLayouts = this.childLayoutPairs.map(pair => pair.layout);
- if (this.props.Document._useClusters) {
+ if (this.props.Document._freeform_useClusters) {
this._clusterSets.forEach(set => Doc.IndexOf(doc, set) !== -1 && set.splice(Doc.IndexOf(doc, set), 1));
- const preferredInd = NumCast(doc.cluster);
- doc.cluster = -1;
+ const preferredInd = NumCast(doc.layout_cluster);
+ doc.layout_cluster = -1;
this._clusterSets.forEach((set, i) =>
set.forEach(member => {
- if (doc.cluster === -1 && Doc.IndexOf(member, childLayouts) !== -1 && Doc.overlapping(doc, member, this._clusterDistance)) {
- doc.cluster = i;
+ if (doc.layout_cluster === -1 && Doc.IndexOf(member, childLayouts) !== -1 && Doc.overlapping(doc, member, this._clusterDistance)) {
+ doc.layout_cluster = i;
}
})
);
- if (doc.cluster === -1 && preferredInd !== -1 && this._clusterSets.length > preferredInd && (!this._clusterSets[preferredInd] || !this._clusterSets[preferredInd].filter(member => Doc.IndexOf(member, childLayouts) !== -1).length)) {
- doc.cluster = preferredInd;
+ if (doc.layout_cluster === -1 && preferredInd !== -1 && this._clusterSets.length > preferredInd && (!this._clusterSets[preferredInd] || !this._clusterSets[preferredInd].filter(member => Doc.IndexOf(member, childLayouts) !== -1).length)) {
+ doc.layout_cluster = preferredInd;
}
this._clusterSets.forEach((set, i) => {
- if (doc.cluster === -1 && !set.filter(member => Doc.IndexOf(member, childLayouts) !== -1).length) {
- doc.cluster = i;
+ if (doc.layout_cluster === -1 && !set.filter(member => Doc.IndexOf(member, childLayouts) !== -1).length) {
+ doc.layout_cluster = i;
}
});
- if (doc.cluster === -1) {
- doc.cluster = this._clusterSets.length;
+ if (doc.layout_cluster === -1) {
+ doc.layout_cluster = this._clusterSets.length;
this._clusterSets.push([doc]);
} else if (this._clusterSets.length) {
- for (let i = this._clusterSets.length; i <= doc.cluster; i++) !this._clusterSets[i] && this._clusterSets.push([]);
- this._clusterSets[doc.cluster].push(doc);
+ for (let i = this._clusterSets.length; i <= doc.layout_cluster; i++) !this._clusterSets[i] && this._clusterSets.push([]);
+ this._clusterSets[doc.layout_cluster ?? 0].push(doc);
}
}
}
@@ -467,8 +571,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
let styleProp = this.props.styleProvider?.(doc, props, property); // bcz: check 'props' used to be renderDepth + 1
switch (property) {
case StyleProp.BackgroundColor:
- const cluster = NumCast(doc?.cluster);
- if (this.Document._useClusters) {
+ const cluster = NumCast(doc?.layout_cluster);
+ if (this.Document._freeform_useClusters && doc?.type !== DocumentType.IMG) {
if (this._clusterSets.length <= cluster) {
setTimeout(() => doc && this.updateCluster(doc));
} else {
@@ -481,6 +585,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
}
}
break;
+ case StyleProp.FillColor:
+ if (doc && this.Document._currentFrame !== undefined) {
+ return CollectionFreeFormDocumentView.getStringValues(doc, NumCast(this.Document._currentFrame))?.fillColor;
+ }
}
return styleProp;
};
@@ -488,7 +596,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
trySelectCluster = (addToSel: boolean) => {
if (this._hitCluster !== -1) {
!addToSel && SelectionManager.DeselectAll();
- const eles = this.childLayoutPairs.map(pair => pair.layout).filter(cd => (this.props.Document._useClusters ? NumCast(cd.cluster) : NumCast(cd.group, -1)) === this._hitCluster);
+ const eles = this.childLayoutPairs.map(pair => pair.layout).filter(cd => (this.props.Document._freeform_useClusters ? NumCast(cd.layout_cluster) : NumCast(cd.group, -1)) === this._hitCluster);
this.selectDocuments(eles);
return true;
}
@@ -517,14 +625,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
!InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) &&
!InteractionUtils.IsType(e, InteractionUtils.PENTYPE)
) {
+ // prettier-ignore
switch (Doc.ActiveTool) {
- case InkTool.Highlighter:
- break;
- // TODO: nda - this where we want to create the new "writingDoc" collection that we add strokes to
- case InkTool.Write:
- break;
- case InkTool.Pen:
- break; // the GestureOverlay handles ink stroke input -- either as gestures, or drying as ink strokes that are added to document views
+ case InkTool.Highlighter: break;
+ case InkTool.Write: break;
+ case InkTool.Pen: break; // the GestureOverlay handles ink stroke input -- either as gestures, or drying as ink strokes that are added to document views
case InkTool.Eraser:
this._batch = UndoManager.StartBatch('collectionErase');
setupMoveUpEvents(this, e, this.onEraserMove, this.onEraserUp, emptyFunction);
@@ -532,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, false);
+ setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, emptyFunction, this._hitCluster !== -1 ? true : false, false);
}
break;
}
@@ -578,7 +683,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
const B = this.getTransform().transformBounds(ge.bounds.left, ge.bounds.top, ge.bounds.width, ge.bounds.height);
const inkDoc = Docs.Create.InkDocument(
ActiveInkColor(),
- Doc.ActiveTool,
ActiveInkWidth() * this.props.ScreenToLocalTransform().Scale,
ActiveInkBezierApprox(),
ActiveFillColor(),
@@ -589,8 +693,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
ActiveIsInkMask(),
{
title: 'ink stroke',
- x: B.x - ActiveInkWidth() / 2,
- y: B.y - ActiveInkWidth() / 2,
+ x: B.x - (ActiveInkWidth() * this.props.ScreenToLocalTransform().Scale) / 2,
+ y: B.y - (ActiveInkWidth() * this.props.ScreenToLocalTransform().Scale) / 2,
_width: B.width + ActiveInkWidth() * this.props.ScreenToLocalTransform().Scale,
_height: B.height + ActiveInkWidth() * this.props.ScreenToLocalTransform().Scale,
}
@@ -605,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;
});
@@ -617,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;
}
@@ -680,7 +784,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
this._batch?.end();
};
+ @action
onClick = (e: React.MouseEvent) => {
+ if (this._lightboxDoc) this._lightboxDoc = undefined;
if (this.onBrowseClickHandler()) {
if (this.props.DocumentView?.()) {
this.onBrowseClickHandler().script.run({ documentView: this.props.DocumentView(), clientX: e.clientX, clientY: e.clientY });
@@ -688,7 +794,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
e.stopPropagation();
e.preventDefault();
} else if (Math.abs(e.pageX - this._downX) < 3 && Math.abs(e.pageY - this._downY) < 3) {
- if (e.shiftKey) {
+ if (e.shiftKey && (this.props.renderDepth === 0 || this.isContentActive())) {
if (Date.now() - this._lastTap < 300) {
// reset zoom of freeform view to 1-to-1 on a shift + double click
this.zoomSmoothlyAboutPt(this.getTransform().transformPoint(e.clientX, e.clientY), 1);
@@ -702,19 +808,23 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
@action
scrollPan = (e: WheelEvent | { deltaX: number; deltaY: number }): void => {
+ PresBox.Instance?.pauseAutoPres();
const dx = e.deltaX;
const dy = e.deltaY;
- this.setPan(NumCast(this.Document._panX) - dx, NumCast(this.Document._panY) - dy, 0, true);
+ this.setPan(NumCast(this.Document[this.panXFieldKey]) - dx, NumCast(this.Document[this.panYFieldKey]) - dy, 0, true);
};
@action
pan = (e: PointerEvent | React.Touch | { clientX: number; clientY: number }): void => {
+ PresBox.Instance?.pauseAutoPres();
+ this.props.DocumentView?.().clearViewTransition();
const [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY);
- this.setPan(NumCast(this.Document._panX) - dx, NumCast(this.Document._panY) - dy, 0, true);
+ this.setPan(NumCast(this.Document[this.panXFieldKey]) - dx, NumCast(this.Document[this.panYFieldKey]) - dy, 0, true);
this._lastX = e.clientX;
this._lastY = e.clientY;
};
+ _eraserLock = 0;
/**
* Erases strokes by intersecting them with an invisible "eraser stroke".
* By default this iterates through all intersected ink strokes, determines their segmentation, draws back the non-intersected segments,
@@ -724,19 +834,23 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
@action
onEraserMove = (e: PointerEvent, down: number[], delta: number[]) => {
const currPoint = { X: e.clientX, Y: e.clientY };
+ if (this._eraserLock) return false; // bcz: should be fixed by putting it on a queue to be processed after the last eraser movement is processed.
this.getEraserIntersections({ X: currPoint.X - delta[0], Y: currPoint.Y - delta[1] }, currPoint).forEach(intersect => {
if (!this._deleteList.includes(intersect.inkView)) {
this._deleteList.push(intersect.inkView);
- SetActiveInkWidth(StrCast(intersect.inkView.rootDoc.strokeWidth?.toString()) || '1');
+ SetActiveInkWidth(StrCast(intersect.inkView.rootDoc.stroke_width?.toString()) || '1');
SetActiveInkColor(StrCast(intersect.inkView.rootDoc.color?.toString()) || 'black');
// create a new curve by appending all curves of the current segment together in order to render a single new stroke.
if (!e.shiftKey) {
+ this._eraserLock++;
this.segmentInkStroke(intersect.inkView, intersect.t).forEach(segment =>
- GestureOverlay.Instance.dispatchGesture(
+ this.forceStrokeGesture(
+ e,
GestureUtils.Gestures.Stroke,
segment.reduce((data, curve) => [...data, ...curve.points.map(p => intersect.inkView.ComponentView?.ptToScreen?.({ X: p.x, Y: p.y }) ?? { X: 0, Y: 0 })], [] as PointData[])
)
);
+ setTimeout(() => this._eraserLock--);
}
// Lower ink opacity to give the user a visual indicator of deletion.
intersect.inkView.layoutDoc.opacity = 0.5;
@@ -745,17 +859,25 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
});
return false;
};
+ forceStrokeGesture = (e: PointerEvent, gesture: GestureUtils.Gestures, points: InkData, text?: any) => {
+ this.onGesture(e, new GestureUtils.GestureEvent(gesture, points, GestureOverlay.getBounds(points), text));
+ };
@action
onPointerMove = (e: PointerEvent): boolean => {
if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) return false;
if (InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) {
Doc.ActiveTool = InkTool.None;
- if (this.props.isContentActive(true)) e.stopPropagation();
- } else if (!e.cancelBubble) {
+ } 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;
- } else 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;
};
@@ -769,7 +891,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
const eraserMax = { X: Math.max(lastPoint.X, currPoint.X), Y: Math.max(lastPoint.Y, currPoint.Y) };
return this.childDocs
- .map(doc => DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView))
+ .map(doc => DocumentManager.Instance.getDocumentView(doc, this.props.DocumentView?.()))
.filter(inkView => inkView?.ComponentView instanceof InkingStroke)
.map(inkView => ({ inkViewBounds: inkView!.getBounds(), inkStroke: inkView!.ComponentView as InkingStroke, inkView: inkView! }))
.filter(
@@ -792,9 +914,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
p2: { x: currPointInkSpace.X, y: currPointInkSpace.Y },
});
const intersects = Array.from(new Set(rawIntersects as (number | string)[])); // convert to more manageable union array type
- if (intersects.length) {
- console.log();
- }
// return tuples of the inkingStroke intersected, and the t value of the intersection
intersections.push(...intersects.map(t => ({ inkView, t: +t + Math.floor(i / 4) }))); // convert string t's to numbers and add start of curve segment to convert from local t value to t value along complete curve
}
@@ -826,7 +945,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
const docCurveTVal = t + Math.floor(i / 4);
if (excludeT < startSegmentT || excludeT > docCurveTVal) {
const localStartTVal = startSegmentT - Math.floor(i / 4);
- segment.push(inkSegment.split(localStartTVal < 0 ? 0 : localStartTVal, t));
+ t !== (localStartTVal < 0 ? 0 : localStartTVal) && segment.push(inkSegment.split(localStartTVal < 0 ? 0 : localStartTVal, t));
segment.length && segments.push(segment);
}
// start a new segment from the intersection t value
@@ -866,7 +985,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
this.childDocs
.filter(doc => doc.type === DocumentType.INK && !doc.dontIntersect)
.forEach(doc => {
- const otherInk = DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView)?.ComponentView as InkingStroke;
+ const otherInk = DocumentManager.Instance.getDocumentView(doc, this.props.DocumentView?.())?.ComponentView as InkingStroke;
const { inkData: otherInkData } = otherInk?.inkScaledData() ?? { inkData: [] };
const otherScreenPts = otherInkData.map(point => otherInk.ptToScreen(point));
const otherCtrlPts = otherScreenPts.map(spt => (ink.ComponentView as InkingStroke).ptFromScreen(spt));
@@ -876,106 +995,26 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
if (ink?.Document === otherInk.props.Document && neighboringSegment) continue;
const otherCurve = new Bezier(otherCtrlPts.slice(j, j + 4).map(p => ({ x: p.X, y: p.Y })));
+ const c0 = otherCurve.get(0);
+ const c1 = otherCurve.get(1);
+ const apt = curve.project(c0);
+ const bpt = curve.project(c1);
+ if (apt.d !== undefined && apt.d < 1 && apt.t !== undefined && !tVals.includes(apt.t)) {
+ tVals.push(apt.t);
+ }
this.bintersects(curve, otherCurve).forEach((val: string | number, i: number) => {
// Converting the Bezier.js Split type to a t-value number.
const t = +val.toString().split('/')[0];
if (i % 2 === 0 && !tVals.includes(t)) tVals.push(t); // bcz: Hack! don't know why but intersection points are doubled from bezier.js (but not identical).
});
+ if (bpt.d !== undefined && bpt.d < 1 && bpt.t !== undefined && !tVals.includes(bpt.t)) {
+ tVals.push(bpt.t);
+ }
}
});
return tVals;
};
- handle1PointerMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => {
- if (!e.cancelBubble) {
- const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true);
- if (myTouches[0]) {
- if (Doc.ActiveTool === InkTool.None) {
- if (this.tryDragCluster(e, this._hitCluster)) {
- e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers
- e.preventDefault();
- document.removeEventListener('pointermove', this.onPointerMove);
- return;
- }
- // TODO: nda - this allows us to pan collections with finger -> only want to do this when collection is selected'
- this.pan(myTouches[0]);
- }
- }
- // e.stopPropagation();
- e.preventDefault();
- }
- };
-
- handle2PointersMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => {
- // pinch zooming
- if (!e.cancelBubble) {
- const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true);
- const pt1 = myTouches[0];
- const pt2 = myTouches[1];
-
- if (this.prevPoints.size === 2) {
- const oldPoint1 = this.prevPoints.get(pt1.identifier);
- const oldPoint2 = this.prevPoints.get(pt2.identifier);
- if (oldPoint1 && oldPoint2) {
- const dir = InteractionUtils.Pinching(pt1, pt2, oldPoint1, oldPoint2);
-
- // if zooming, zoom
- if (dir !== 0) {
- const d1 = Math.sqrt(Math.pow(pt1.clientX - oldPoint1.clientX, 2) + Math.pow(pt1.clientY - oldPoint1.clientY, 2));
- const d2 = Math.sqrt(Math.pow(pt2.clientX - oldPoint2.clientX, 2) + Math.pow(pt2.clientY - oldPoint2.clientY, 2));
- const centerX = Math.min(pt1.clientX, pt2.clientX) + Math.abs(pt2.clientX - pt1.clientX) / 2;
- const centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2;
-
- // calculate the raw delta value
- const rawDelta = dir * (d1 + d2);
-
- // this floors and ceils the delta value to prevent jitteriness
- const delta = Math.sign(rawDelta) * Math.min(Math.abs(rawDelta), 8);
- this.zoom(centerX, centerY, delta * window.devicePixelRatio);
- this.prevPoints.set(pt1.identifier, pt1);
- this.prevPoints.set(pt2.identifier, pt2);
- }
- // this is not zooming. derive some form of panning from it.
- else {
- // use the centerx and centery as the "new mouse position"
- const centerX = Math.min(pt1.clientX, pt2.clientX) + Math.abs(pt2.clientX - pt1.clientX) / 2;
- const centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2;
- // const transformed = this.getTransform().inverse().transformPoint(centerX, centerY);
-
- this._lastX = centerX;
- this._lastY = centerY;
- }
- }
- }
- // e.stopPropagation();
- e.preventDefault();
- }
- };
-
- @action
- handle2PointersDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>) => {
- if (this.props.isContentActive(true)) {
- // const pt1: React.Touch | null = e.targetTouches.item(0);
- // const pt2: React.Touch | null = e.targetTouches.item(1);
- // // if (!pt1 || !pt2) return;
- const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true);
- const pt1 = myTouches[0];
- const pt2 = myTouches[1];
- if (pt1 && pt2) {
- const centerX = Math.min(pt1.clientX, pt2.clientX) + Math.abs(pt2.clientX - pt1.clientX) / 2;
- const centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2;
- this._lastX = centerX;
- this._lastY = centerY;
-
- this.removeMoveListeners();
- this.addMoveListeners();
- this.removeEndListeners();
- this.addEndListeners();
- e.stopPropagation();
- }
- }
- };
-
cleanUpInteractions = () => {
this.removeMoveListeners();
this.removeEndListeners();
@@ -983,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);
@@ -991,51 +1030,42 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
if (deltaScale * invTransform.Scale > 20) {
deltaScale = 20 / invTransform.Scale;
}
- if (deltaScale * invTransform.Scale < NumCast(this.rootDoc._viewScaleMin, 1) && this.isAnnotationOverlay) {
- deltaScale = NumCast(this.rootDoc._viewScaleMin, 1) / invTransform.Scale;
+ 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_scale_min, 1) && this.isAnnotationOverlay) {
+ deltaScale = NumCast(this.rootDoc._freeform_scale_min, 1) / invTransform.Scale;
}
- const localTransform = this.getLocalTransform().inverse().scaleAbout(deltaScale, x, y);
+ const localTransform = invTransform.scaleAbout(deltaScale, x, y);
if (localTransform.Scale >= 0.05 || localTransform.Scale > this.zoomScaling()) {
const safeScale = Math.min(Math.max(0.05, localTransform.Scale), 20);
this.props.Document[this.scaleFieldKey] = Math.abs(safeScale);
- this.setPan(-localTransform.TranslateX / safeScale, -localTransform.TranslateY / safeScale);
+ this.setPan(-localTransform.TranslateX / safeScale, (this.props.originTopLeft ? undefined : NumCast(this.props.Document.layout_scrollTop) * safeScale) || -localTransform.TranslateY / safeScale);
}
};
@action
onPointerWheel = (e: React.WheelEvent): void => {
- if (this.layoutDoc._Transform || DocListCast(Doc.MyOverlayDocs?.data).includes(this.props.Document) || this.props.Document.treeViewOutlineMode === TreeViewType.outline) return;
+ if (this.Document._isGroup || !this.isContentActive()) return; // group style collections neither pan nor zoom
+ PresBox.Instance?.pauseAutoPres();
+ if (this.layoutDoc._Transform || this.props.Document.treeViewOutlineMode === TreeViewType.outline) return;
e.stopPropagation();
- e.preventDefault();
- switch (Doc.UserDoc().freeformScrollMode) {
+ 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;
+ switch (!e.ctrlKey ? Doc.UserDoc().freeformScrollMode : freeformScrollMode.Pan) {
case freeformScrollMode.Pan:
- // if shift is selected then zoom
- if (e.ctrlKey) {
- if (!e.ctrlKey && this.props.Document.scrollHeight !== undefined) {
- // things that can scroll vertically should do that instead of zooming
- } else if (this.props.isContentActive(true) && !this.Document._isGroup) {
- !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?
- }
- // otherwise pan
- } else if (!e.ctrlKey && this.props.Document.scrollHeight !== undefined) {
- // things that can scroll vertically should do that instead of zooming
- } else if (this.props.isContentActive(true) && !this.Document._isGroup) {
- 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 ctrl is selected then zoom
+ 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 (!e.ctrlKey && this.props.Document.scrollHeight !== undefined) {
- // things that can scroll vertically should do that instead of zooming
- } else if (this.props.isContentActive(true) && !this.Document._isGroup) {
- !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;
}
@@ -1063,40 +1093,61 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
yrange: { min: Math.min(yrange.min, pos.y), max: Math.max(yrange.max, pos.y + (size.height || 0)) },
}),
{
- xrange: { min: Number.MAX_VALUE, max: -Number.MAX_VALUE },
- yrange: { min: Number.MAX_VALUE, max: -Number.MAX_VALUE },
+ xrange: { min: this.props.originTopLeft ? 0 : Number.MAX_VALUE, max: -Number.MAX_VALUE },
+ yrange: { min: this.props.originTopLeft ? 0 : Number.MAX_VALUE, max: -Number.MAX_VALUE },
}
);
- const panelDim = [this.props.PanelWidth() / this.zoomScaling(), this.props.PanelHeight() / this.zoomScaling()];
- if (ranges.xrange.min >= panX + panelDim[0] / 2) panX = ranges.xrange.max + panelDim[0] / 2; // snaps pan position of range of content goes out of bounds
- else if (ranges.xrange.max <= panX - panelDim[0] / 2) panX = ranges.xrange.min - panelDim[0] / 2;
- if (ranges.yrange.min >= panY + panelDim[1] / 2) panY = ranges.yrange.max + panelDim[1] / 2;
- else if (ranges.yrange.max <= panY - panelDim[1] / 2) panY = ranges.yrange.min - panelDim[1] / 2;
+ const panelWidMax = (this.props.PanelWidth() / this.zoomScaling()) * (this.props.originTopLeft ? 2 / this.nativeDimScaling : 1);
+ const panelWidMin = (this.props.PanelWidth() / this.zoomScaling()) * (this.props.originTopLeft ? 0 : 1);
+ const panelHgtMax = (this.props.PanelHeight() / this.zoomScaling()) * (this.props.originTopLeft ? 2 / this.nativeDimScaling : 1);
+ const panelHgtMin = (this.props.PanelHeight() / this.zoomScaling()) * (this.props.originTopLeft ? 0 : 1);
+ if (ranges.xrange.min >= panX + panelWidMax / 2) panX = ranges.xrange.max + (this.props.originTopLeft ? 0 : panelWidMax / 2);
+ else if (ranges.xrange.max <= panX - panelWidMin / 2) panX = ranges.xrange.min - (this.props.originTopLeft ? panelWidMax / 2 : panelWidMin / 2);
+ if (ranges.yrange.min >= panY + panelHgtMax / 2) panY = ranges.yrange.max + (this.props.originTopLeft ? 0 : panelHgtMax / 2);
+ 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)) {
- this._viewTransition = panTime;
- const scale = this.getLocalTransform().inverse().Scale;
- const minScale = NumCast(this.rootDoc._viewScaleMin, 1);
- const minPanX = NumCast(this.rootDoc._panXMin, 0);
- const minPanY = NumCast(this.rootDoc._panYMin, 0);
- const newPanX = Math.min(minPanX + (1 - minScale / scale) * NumCast(this.rootDoc._panXMax, this.nativeWidth), Math.max(minPanX, panX));
- const newPanY = Math.min(this.props.Document.scrollHeight !== undefined ? NumCast(this.Document.scrollHeight) : minPanY + (1 - minScale / scale) * NumCast(this.rootDoc._panYMax, this.nativeHeight), Math.max(minPanY, panY));
- !this.Document._verticalScroll && (this.Document._panX = this.isAnnotationOverlay ? newPanX : panX);
- !this.Document._horizontalScroll && (this.Document._panY = this.isAnnotationOverlay ? newPanY : panY);
+ if (!this.layoutDoc._lockedTransform || LightboxView.LightboxDoc) {
+ this.setPanZoomTransition(panTime);
+ 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
+ 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 > 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);
+ newPanY = minPanY;
+ }
+ !this.Document._verticalScroll && (this.Document[this.panXFieldKey] = this.isAnnotationOverlay ? newPanX : panX);
+ !this.Document._horizontalScroll && (this.Document[this.panYFieldKey] = this.isAnnotationOverlay ? newPanY : panY);
}
}
@action
nudge = (x: number, y: number, nudgeTime: number = 500) => {
- if (this.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Freeform || this.props.ContainingCollectionDoc._panX !== undefined) {
- // bcz: this isn't ideal, but want to try it out...
- this.setPan(NumCast(this.layoutDoc._panX) + ((this.props.PanelWidth() / 2) * x) / this.zoomScaling(), NumCast(this.layoutDoc._panY) + ((this.props.PanelHeight() / 2) * -y) / this.zoomScaling(), nudgeTime, true);
- this._lastNudge && clearTimeout(this._lastNudge);
- this._lastNudge = setTimeout(
- action(() => (this._viewTransition = 0)),
- nudgeTime
+ const collectionDoc = this.props.docViewPath().lastElement().rootDoc;
+ 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(),
+ nudgeTime,
+ true
);
return true;
}
@@ -1106,115 +1157,61 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
@action
bringToFront = (doc: Doc, sendToBack?: boolean) => {
if (sendToBack) {
- doc.zIndex = 0;
- } else if (doc.isInkMask) {
+ const docs = this.childLayoutPairs.map(pair => pair.layout).slice();
+ docs.sort((doc1, doc2) => NumCast(doc1.zIndex) - NumCast(doc2.zIndex));
+ let zfirst = docs.length ? NumCast(docs[0].zIndex) : 0;
+ doc.zIndex = zfirst - 1;
+ } else if (doc.stroke_isInkMask) {
doc.zIndex = 5000;
} else {
const docs = this.childLayoutPairs.map(pair => pair.layout).slice();
docs.sort((doc1, doc2) => NumCast(doc1.zIndex) - NumCast(doc2.zIndex));
- let zlast = docs.length ? Math.max(docs.length, NumCast(docs[docs.length - 1].zIndex)) : 1;
- if (zlast - docs.length > 100) {
- for (let i = 0; i < docs.length; i++) doc.zIndex = i + 1;
- zlast = docs.length + 1;
+ let zlast = docs.length ? Math.max(docs.length, NumCast(docs.lastElement().zIndex)) : 1;
+ if (docs.lastElement() !== doc) {
+ if (zlast - docs.length > 100) {
+ for (let i = 0; i < docs.length; i++) doc.zIndex = i + 1;
+ zlast = docs.length + 1;
+ }
+ doc.zIndex = zlast + 1;
}
- doc.zIndex = zlast + 1;
}
};
@action
+ setPanZoomTransition = (transitionTime: number) => {
+ this._panZoomTransition = transitionTime;
+ this._panZoomTransitionTimer && clearTimeout(this._panZoomTransitionTimer);
+ this._panZoomTransitionTimer = setTimeout(
+ action(() => (this._panZoomTransition = 0)),
+ transitionTime
+ );
+ };
+
+ @action
zoomSmoothlyAboutPt(docpt: number[], scale: number, transitionTime = 500) {
if (this.Document._isGroup) return;
- this._viewTransition = transitionTime;
+ this.setPanZoomTransition(transitionTime);
const screenXY = this.getTransform().inverse().transformPoint(docpt[0], docpt[1]);
this.layoutDoc[this.scaleFieldKey] = scale;
const newScreenXY = this.getTransform().inverse().transformPoint(docpt[0], docpt[1]);
const scrDelta = { x: screenXY[0] - newScreenXY[0], y: screenXY[1] - newScreenXY[1] };
const newpan = this.getTransform().transformDirection(scrDelta.x, scrDelta.y);
- this.layoutDoc._panX = NumCast(this.layoutDoc._panX) - newpan[0];
- this.layoutDoc._panY = NumCast(this.layoutDoc._panY) - newpan[1];
- return new Promise<number>(res => setTimeout(() => res(runInAction(() => (this._viewTransition = 0))), this._viewTransition)); // set transition to be smooth, then reset
+ this.layoutDoc[this.panXFieldKey] = NumCast(this.layoutDoc[this.panXFieldKey]) - newpan[0];
+ this.layoutDoc[this.panYFieldKey] = NumCast(this.layoutDoc[this.panYFieldKey]) - newpan[1];
}
- focusDocument = (doc: Doc, options: DocFocusOptions) => {
- const state = HistoryUtil.getState();
-
- // TODO This technically isn't correct if type !== "doc", as
- // currently nothing is done, but we should probably push a new state
- if (state.type === 'doc' && this.Document._panX !== undefined && this.Document._panY !== undefined) {
- const init = state.initializers![this.Document[Id]];
- if (!init) {
- state.initializers![this.Document[Id]] = { panX: NumCast(this.Document._panX), panY: NumCast(this.Document._panY) };
- HistoryUtil.pushState(state);
- } else if (init.panX !== this.Document._panX || init.panY !== this.Document._panY) {
- init.panX = NumCast(this.Document._panX);
- init.panY = NumCast(this.Document._panY);
- HistoryUtil.pushState(state);
- }
- }
- // if (SelectionManager.Views().length !== 1 || SelectionManager.Views()[0].Document !== doc) {
- // SelectionManager.DeselectAll();
- // }
- if (this.props.Document.scrollHeight || this.props.Document.scrollTop !== undefined || this.props.Document.currentTimecode !== undefined) {
- this.props.focus(doc, options);
- } else {
- const xfToCollection = options?.docTransform ?? Transform.Identity();
- const savedState = { panX: NumCast(this.Document._panX), panY: NumCast(this.Document._panY), scale: options?.willZoom ? this.Document[this.scaleFieldKey] : undefined };
- const newState = HistoryUtil.getState();
- const cantTransform = (this.rootDoc._isGroup || this.layoutDoc._lockedTransform) && !LightboxView.LightboxDoc;
- const { panX, panY, scale } = cantTransform ? savedState : this.calculatePanIntoView(doc, xfToCollection, options?.willZoom ? options?.scale || 0.75 : undefined);
- if (!cantTransform) {
- // only pan and zoom to focus on a document if the document is not an annotation in an annotation overlay collection
- newState.initializers![this.Document[Id]] = { panX, panY };
- HistoryUtil.pushState(newState);
- }
- // focus on the document in the collection
- const didMove = !cantTransform && !doc.z && (panX !== savedState.panX || panY !== savedState.panY || scale !== savedState.scale);
- const focusSpeed = options?.instant ? 0 : didMove ? NumCast(doc.focusSpeed, 500) : 0;
- // glr: freeform transform speed can be set by adjusting presTransition field - needs a way of knowing when presentation is not active...
- if (didMove) {
- scale && (this.Document[this.scaleFieldKey] = scale);
- this.setPan(panX, panY, focusSpeed, true); // docs that are floating in their collection can't be panned to from their collection -- need to propagate the pan to a parent freeform somehow
- }
-
- const startTime = Date.now();
- // focus on this collection within its parent view. the parent view after focusing determines whether to reset the view change within the collection
- const endFocus = async (moved: boolean) => {
- doc.hidden && Doc.UnHighlightDoc(doc);
- const resetView = options?.afterFocus ? await options?.afterFocus(moved) : ViewAdjustment.doNothing;
- if (resetView) {
- const restoreState = (!LightboxView.LightboxDoc || LightboxView.LightboxDoc === this.props.Document) && savedState;
- if (typeof restoreState !== 'boolean') {
- this.Document._panX = restoreState.panX;
- this.Document._panY = restoreState.panY;
- this.Document[this.scaleFieldKey] = restoreState.scale;
- }
- }
- runInAction(() => (this._viewTransition = 0));
- return resetView;
- };
- const xf = !cantTransform
- ? Transform.Identity()
- : this.props.isAnnotationOverlay
- ? new Transform(NumCast(this.rootDoc.x), NumCast(this.rootDoc.y), this.rootDoc[WidthSym]() / Doc.NativeWidth(this.rootDoc))
- : new Transform(NumCast(this.rootDoc.x) + this.rootDoc[WidthSym]() / 2 - NumCast(this.rootDoc._panX), NumCast(this.rootDoc.y) + this.rootDoc[HeightSym]() / 2 - NumCast(this.rootDoc._panY), 1);
-
- this.props.focus(!cantTransform ? this.rootDoc : doc, {
- ...options,
- docTransform: xf,
- afterFocus: (didFocus: boolean) => new Promise<ViewAdjustment>(res => setTimeout(async () => res(await endFocus(didMove || didFocus)), Math.max(0, focusSpeed - (Date.now() - startTime)))),
- });
- }
- };
-
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) {
+ if (scale !== undefined) {
const maxZoom = 5; // sets the limit for how far we will zoom. this is useful for preventing small text boxes from filling the screen. So probably needs to be more sophisticated to consider more about the target and context
- const newScale = Math.min(maxZoom, (1 / (this.nativeDimScaling || 1)) * scale * Math.min(this.props.PanelWidth() / Math.abs(bounds.width), this.props.PanelHeight() / Math.abs(bounds.height)));
+ const newScale =
+ scale === 0
+ ? NumCast(this.layoutDoc[this.scaleFieldKey])
+ : Math.min(maxZoom, (1 / (this.nativeDimScaling || 1)) * scale * Math.min(this.props.PanelWidth() / Math.abs(bounds.width), this.props.PanelHeight() / Math.abs(bounds.height)));
return {
panX: this.props.isAnnotationOverlay ? bounds.left - (Doc.NativeWidth(this.layoutDoc) / newScale - bounds.width) / 2 : (bounds.left + bounds.right) / 2,
panY: this.props.isAnnotationOverlay ? bounds.top - (Doc.NativeHeight(this.layoutDoc) / newScale - bounds.height) / 2 : (bounds.top + bounds.bot) / 2,
@@ -1224,11 +1221,13 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
const panelWidth = this.props.isAnnotationOverlay ? this.nativeWidth : this.props.PanelWidth();
const panelHeight = this.props.isAnnotationOverlay ? this.nativeHeight : this.props.PanelHeight();
- const pw = panelWidth / NumCast(this.layoutDoc._viewScale, 1);
- const ph = panelHeight / NumCast(this.layoutDoc._viewScale, 1);
- const cx = NumCast(this.layoutDoc._panX) + (this.props.isAnnotationOverlay ? pw / 2 : 0);
- const cy = NumCast(this.layoutDoc._panY) + (this.props.isAnnotationOverlay ? ph / 2 : 0);
+ const pw = panelWidth / NumCast(this.layoutDoc._freeform_scale, 1);
+ const ph = panelHeight / NumCast(this.layoutDoc._freeform_scale, 1);
+ const cx = NumCast(this.layoutDoc[this.panXFieldKey]) + (this.props.isAnnotationOverlay ? pw / 2 : 0);
+ const cy = NumCast(this.layoutDoc[this.panYFieldKey]) + (this.props.isAnnotationOverlay ? ph / 2 : 0);
const screen = { left: cx - pw / 2, right: cx + pw / 2, top: cy - ph / 2, bot: cy + ph / 2 };
+ const maxYShift = Math.max(0, screen.bot - screen.top - (bounds.bot - bounds.top));
+ const phborder = bounds.top < screen.top || bounds.bot > screen.bot ? Math.min(ph / 10, maxYShift / 2) : 0;
if (screen.right - screen.left < bounds.right - bounds.left || screen.bot - screen.top < bounds.bot - bounds.top) {
return {
panX: (bounds.left + bounds.right) / 2,
@@ -1237,28 +1236,28 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
};
}
return {
- panX: (this.props.isAnnotationOverlay ? NumCast(this.layoutDoc._panX) : cx) + Math.min(0, bounds.left - pw / 10 - screen.left) + Math.max(0, bounds.right + pw / 10 - screen.right),
- panY: (this.props.isAnnotationOverlay ? NumCast(this.layoutDoc._panY) : cy) + Math.min(0, bounds.top - ph / 10 - screen.top) + Math.max(0, bounds.bot + ph / 10 - screen.bot),
+ panX: (this.props.isAnnotationOverlay ? NumCast(this.layoutDoc[this.panXFieldKey]) : cx) + Math.min(0, bounds.left - pw / 10 - screen.left) + Math.max(0, bounds.right + pw / 10 - screen.right),
+ panY: (this.props.isAnnotationOverlay ? NumCast(this.layoutDoc[this.panYFieldKey]) : cy) + Math.min(0, bounds.top - phborder - screen.top) + Math.max(0, bounds.bot + phborder - screen.bot),
};
};
- isContentActive = () => this.props.isSelected() || this.props.isContentActive();
+ isContentActive = () => this.props.isContentActive();
@undoBatch
@action
onKeyDown = (e: React.KeyboardEvent, fieldProps: FieldViewProps) => {
const docView = fieldProps.DocumentView?.();
- if (docView && (e.metaKey || e.ctrlKey || e.altKey || docView.rootDoc._singleLine) && ['Tab', 'Enter'].includes(e.key)) {
+ if (docView && (e.metaKey || e.ctrlKey || e.altKey || docView.rootDoc._createDocOnCR) && ['Tab', 'Enter'].includes(e.key)) {
e.stopPropagation?.();
const below = !e.altKey && e.key !== 'Tab';
- const layoutKey = StrCast(docView.LayoutFieldKey);
+ 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 (layoutKey !== 'layout' && docView.rootDoc[layoutKey] instanceof Doc) {
- newDoc[layoutKey] = docView.rootDoc[layoutKey];
+ if (layout_fieldKey !== 'layout' && docView.rootDoc[layout_fieldKey] instanceof Doc) {
+ newDoc[layout_fieldKey] = docView.rootDoc[layout_fieldKey];
}
Doc.GetProto(newDoc).text = undefined;
FormattedTextBox.SelectOnLoad = newDoc[Id];
@@ -1267,7 +1266,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
};
pointerEvents = () => {
const engine = this.props.layoutEngine?.() || StrCast(this.props.Document._layoutEngine);
- const pointerEvents = this.props.isContentActive() === false ? 'none' : this.props.childPointerEvents ?? (this.props.viewDefDivClick || (engine === 'pass' && !this.props.isSelected(true)) ? 'none' : this.props.pointerEvents?.());
+ const pointerEvents = DocumentDecorations.Instance.Interacting
+ ? 'none'
+ : this.props.childPointerEvents ?? (this.props.viewDefDivClick || (engine === computePassLayout.name && !this.props.isSelected(true)) || this.isContentActive() === false ? 'none' : this.props.pointerEvents?.());
return pointerEvents;
};
getChildDocView(entry: PoolData) {
@@ -1282,25 +1283,24 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
replica={entry.replica}
suppressSetHeight={this.layoutEngine ? true : false}
renderCutoffProvider={this.renderCutoffProvider}
- ContainingCollectionView={this.props.CollectionView}
- ContainingCollectionDoc={this.props.Document}
CollectionFreeFormView={this}
LayoutTemplate={childLayout.z ? undefined : this.props.childLayoutTemplate}
LayoutTemplateString={childLayout.z ? undefined : this.props.childLayoutString}
rootSelected={childData ? this.rootSelected : returnFalse}
+ waitForDoubleClickToClick={this.props.waitForDoubleClickToClick}
onClick={this.onChildClickHandler}
onKey={this.onKeyDown}
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={emptyFunction}
- focus={this.focusDocument}
+ isDocumentActive={this.props.childDocumentsActive?.() || this.rootDoc._isGroup ? this.props.isDocumentActive : this.isContentActive}
+ isContentActive={this.props.childContentsActive ?? this.isContentActive() === false ? returnFalse : emptyFunction}
+ focus={this.Document._isGroup ? this.groupFocus : this.isAnnotationOverlay ? this.props.focus : this.focus}
addDocTab={this.addDocTab}
addDocument={this.props.addDocument}
removeDocument={this.props.removeDocument}
@@ -1309,52 +1309,74 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged}
docViewPath={this.props.docViewPath}
styleProvider={this.getClusterColor}
+ 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}
- showTitle={this.props.childShowTitle}
- dontScaleFilter={this.props.dontScaleFilter}
+ 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
+ //rotation={this.props.styleProvider?.(childLayout, this.props, StyleProp.JitterRotation) || 0}
+ //fitContentsToBox={this.props.fitContentsToBox || BoolCast(this.props.treeViewFreezeChildDimensions)} // bcz: check this
/>
);
}
- addDocTab = action((doc: Doc, where: string) => {
- if (where === 'inParent') {
- (doc instanceof Doc ? [doc] : doc).forEach(doc => {
- const pt = this.getTransform().transformPoint(NumCast(doc.x), NumCast(doc.y));
- doc.x = pt[0];
- doc.y = pt[1];
- });
- return this.props.addDocument?.(doc) || false;
- }
- if (where === 'inPlace' && this.layoutDoc.isInPlaceContainer) {
- this.dataDoc[this.props.fieldKey] = doc instanceof Doc ? doc : new List<Doc>(doc as any as Doc[]);
- return true;
+ addDocTab = action((doc: Doc, where: OpenWhere) => {
+ if (this.props.isAnnotationOverlay) return this.props.addDocTab(doc, where);
+ switch (where) {
+ case OpenWhere.inParent:
+ return this.props.addDocument?.(doc) || false;
+ case OpenWhere.inParentFromScreen:
+ const docContext = DocCast((doc instanceof Doc ? doc : doc?.[0])?.embedContainer);
+ return (
+ (this.addDocument?.(
+ (doc instanceof Doc ? [doc] : doc).map(doc => {
+ const pt = this.getTransform().transformPoint(NumCast(doc.x), NumCast(doc.y));
+ doc.x = pt[0];
+ doc.y = pt[1];
+ return doc;
+ })
+ ) &&
+ (!docContext || this.props.removeDocument?.(docContext))) ||
+ false
+ );
+ case undefined:
+ case OpenWhere.lightbox:
+ if (this.layoutDoc._isLightbox) {
+ this._lightboxDoc = doc;
+ return true;
+ }
+ if (this.childDocList?.includes(doc)) {
+ if (doc.hidden) doc.hidden = false;
+ return true;
+ }
}
return this.props.addDocTab(doc, where);
});
+ @observable _lightboxDoc: Opt<Doc>;
getCalculatedPositions(params: { pair: { layout: Doc; data?: Doc }; index: number; collection: Doc }): PoolData {
- const curFrame = Cast(this.Document._currentFrame, 'number');
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
+ const contentFrameNumber = Cast(childDocLayout._currentFrame, 'number', layoutFrameNumber ?? null); // frame number that content is at which determines what content is displayed
const { z, zIndex } = childDoc;
- const { backgroundColor, color } = curFrame === undefined ? { backgroundColor: undefined, color: undefined } : CollectionFreeFormDocumentView.getStringValues(childDoc, curFrame);
- const { x, y, opacity } = curFrame === undefined ? { x: childDoc.x, y: childDoc.y, opacity: this.props.childOpacity?.() } : CollectionFreeFormDocumentView.getValues(childDoc, curFrame);
+ const { backgroundColor, color } = contentFrameNumber === undefined ? { backgroundColor: undefined, color: undefined } : CollectionFreeFormDocumentView.getStringValues(childDoc, contentFrameNumber);
+ const { x, y, _width, _height, opacity, _rotation } =
+ 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);
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'),
color: Cast(color, 'string') ? StrCast(color) : this.props.styleProvider?.(childDoc, this.props, StyleProp.Color),
- backgroundColor: Cast(backgroundColor, 'string') ? StrCast(backgroundColor) : this.props.styleProvider?.(childDoc, this.props, StyleProp.BackgroundColor),
- opacity: Cast(opacity, 'number') ?? this.props.styleProvider?.(childDoc, this.props, StyleProp.Opacity),
+ 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),
zIndex: Cast(zIndex, 'number'),
- width: Cast(childDocLayout._width, 'number'),
- height: Cast(childDocLayout._height, 'number'),
+ width: _width,
+ height: _height,
transition: StrCast(childDocLayout.dataTransition),
pair: params.pair,
replica: '',
@@ -1451,10 +1473,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
const newPool = new Map<string, PoolData>();
// prettier-ignore
switch (this.layoutEngine) {
- case 'pass': return { newPool, computedElementData: this.doEngineLayout(newPool, computerPassLayout) };
- case 'timeline': return { newPool, computedElementData: this.doEngineLayout(newPool, computeTimelineLayout) };
- case 'pivot': return { newPool, computedElementData: this.doEngineLayout(newPool, computePivotLayout) };
- case 'starburst': return { newPool, computedElementData: this.doEngineLayout(newPool, computerStarburstLayout) };
+ case computePassLayout.name : return { newPool, computedElementData: this.doEngineLayout(newPool, computePassLayout) };
+ case computeTimelineLayout.name: return { newPool, computedElementData: this.doEngineLayout(newPool, computeTimelineLayout) };
+ case computePivotLayout.name: return { newPool, computedElementData: this.doEngineLayout(newPool, computePivotLayout) };
+ case computeStarburstLayout.name: return { newPool, computedElementData: this.doEngineLayout(newPool, computeStarburstLayout) };
}
return { newPool, computedElementData: this.doFreeformLayout(newPool) };
}
@@ -1475,6 +1497,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
newPos.x !== lastPos.x ||
newPos.y !== lastPos.y ||
newPos.z !== lastPos.z ||
+ newPos.rotation !== lastPos.rotation ||
newPos.zIndex !== lastPos.zIndex ||
newPos.transition !== lastPos.transition
) {
@@ -1490,88 +1513,75 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
const elements = computedElementData.slice();
Array.from(newPool.entries())
.filter(entry => this.isCurrent(entry[1].pair.layout))
- .forEach((entry, i) =>
+ .forEach((entry, i) => {
+ const childData: ViewDefBounds = this.childDataProvider(entry[1].pair.layout, entry[1].replica);
+ const childSize = this.childSizeProvider(entry[1].pair.layout, entry[1].replica);
elements.push({
ele: this.getChildDocView(entry[1]),
- bounds: this.childDataProvider(entry[1].pair.layout, entry[1].replica),
- inkMask: BoolCast(entry[1].pair.layout.isInkMask) ? NumCast(entry[1].pair.layout.opacity) : -1,
- })
- );
+ bounds: childData.opacity === 0 ? { ...childData, width: 0, height: 0 } : { ...childData, width: childSize.width, height: childSize.height },
+ inkMask: BoolCast(entry[1].pair.layout.stroke_isInkMask) ? NumCast(entry[1].pair.layout.opacity, 1) : -1,
+ });
+ });
if (this.props.isAnnotationOverlay && this.props.Document[this.scaleFieldKey]) {
// don't zoom out farther than 1-1 if it's a bounded item (image, video, pdf), otherwise don't allow zooming in closer than 1-1 if it's a text sidebar
- if (this.props.scaleField) this.props.Document[this.scaleFieldKey] = Math.min(1, this.zoomScaling());
+ if (this.props.viewField) this.props.Document[this.scaleFieldKey] = Math.min(1, this.zoomScaling());
else this.props.Document[this.scaleFieldKey] = Math.max(1, this.zoomScaling()); // NumCast(this.props.Document[this.scaleFieldKey]));
}
- this.Document._useClusters && !this._clusterSets.length && this.childDocs.length && this.updateClusters(true);
+ this.Document._freeform_useClusters && !this._clusterSets.length && this.childDocs.length && this.updateClusters(true);
return elements;
}
- @action
- setViewSpec = (anchor: Doc, preview: boolean) => {
- if (preview) {
- this._focusFilters = StrListCast(Doc.GetProto(anchor).docFilters);
- this._focusRangeFilters = StrListCast(Doc.GetProto(anchor).docRangeFilters);
- } else {
- if (anchor.docFilters) {
- this.layoutDoc._docFilters = new List<string>(StrListCast(anchor.docFilters));
- }
- if (anchor.docRangeFilters) {
- this.layoutDoc._docRangeFilters = new List<string>(StrListCast(anchor.docRangeFilters));
- }
- }
- return 0;
- };
+ 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.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);
- getAnchor = () => {
- if (this.props.Document.annotationOn) {
- return this.rootDoc;
- }
- const anchor = Docs.Create.TextanchorDocument({ title: 'ViewSpec - ' + StrCast(this.layoutDoc._viewType), annotationOn: this.rootDoc });
- const proto = Doc.GetProto(anchor);
- proto[ViewSpecPrefix + '_viewType'] = this.layoutDoc._viewType;
- proto.docFilters = ObjectField.MakeCopy(this.layoutDoc.docFilters as ObjectField) || new List<string>([]);
- if (Cast(this.dataDoc[this.props.fieldKey + '-annotations'], listSpec(Doc), null) !== undefined) {
- Cast(this.dataDoc[this.props.fieldKey + '-annotations'], listSpec(Doc), []).push(anchor);
- } else {
- this.dataDoc[this.props.fieldKey + '-annotations'] = new List<Doc>([anchor]);
+ if (addAsAnnotation) {
+ if (Cast(this.dataDoc[this.props.fieldKey + '_annotations'], listSpec(Doc), null) !== undefined) {
+ Cast(this.dataDoc[this.props.fieldKey + '_annotations'], listSpec(Doc), []).push(anchor);
+ } else {
+ this.dataDoc[this.props.fieldKey + '_annotations'] = new List<Doc>([anchor]);
+ }
}
return anchor;
};
@action
componentDidMount() {
- super.componentDidMount?.();
this.props.setContentView?.(this);
+ super.componentDidMount?.();
this.props.setBrushViewer?.(this.brushView);
setTimeout(
action(() => {
this._firstRender = false;
this._disposers.groupBounds = reaction(
() => {
- if (this.props.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]() }));
+ 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[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 p = [NumCast(this.layoutDoc._panX), NumCast(this.layoutDoc._panY)];
+ 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],
y: cbounds.y - p[1] + c[1],
r: cbounds.r - p[0] + c[0],
b: cbounds.b - p[1] + c[1],
};
- this.layoutDoc._width = pbounds.r - pbounds.x;
- this.layoutDoc._height = pbounds.b - pbounds.y;
- this.layoutDoc._panX = (cbounds.r + cbounds.x) / 2;
- this.layoutDoc._panY = (cbounds.b + cbounds.y) / 2;
- this.layoutDoc.x = pbounds.x;
- this.layoutDoc.y = pbounds.y;
+ if (Number.isFinite(pbounds.r - pbounds.x) && Number.isFinite(pbounds.b - pbounds.y)) {
+ this.layoutDoc._width = pbounds.r - pbounds.x;
+ this.layoutDoc._height = pbounds.b - pbounds.y;
+ this.layoutDoc[this.panXFieldKey] = (cbounds.r + cbounds.x) / 2;
+ this.layoutDoc[this.panYFieldKey] = (cbounds.b + cbounds.y) / 2;
+ this.layoutDoc.x = pbounds.x;
+ this.layoutDoc.y = pbounds.y;
+ }
}
},
{ fireImmediately: true }
@@ -1601,7 +1611,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
} else {
const canvas = oldDiv;
const img = document.createElement('img'); // create a Image Element
- img.src = canvas.toDataURL(); //image source
+ try {
+ img.src = canvas.toDataURL(); //image source
+ } catch (e) {
+ console.log(e);
+ }
img.style.width = canvas.style.width;
img.style.height = canvas.style.height;
const newCan = newDiv as HTMLCanvasElement;
@@ -1618,8 +1632,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,
@@ -1628,8 +1642,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
'',
(iconFile, nativeWidth, nativeHeight) => {
this.dataDoc.icon = new ImageField(iconFile);
- this.dataDoc['icon-nativeWidth'] = nativeWidth;
- this.dataDoc['icon-nativeHeight'] = nativeHeight;
+ this.dataDoc['icon_nativeWidth'] = nativeWidth;
+ this.dataDoc['icon_nativeHeight'] = nativeHeight;
}
);
@@ -1655,7 +1669,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
const nativeHeight = height;
return CreateImage(Utils.prepend(''), document.styleSheets, htmlString, nativeWidth, (nativeWidth * panelHeight) / panelWidth, (scrollTop * panelHeight) / realNativeHeight)
.then(async (data_url: any) => {
- const returnedFilename = await VideoBox.convertDataUri(data_url, filename, noSuffix, replaceRootFilename);
+ const returnedFilename = await Utils.convertDataUri(data_url, filename, noSuffix, replaceRootFilename);
cb(returnedFilename as string, nativeWidth, nativeHeight);
})
.catch(function (error: any) {
@@ -1678,7 +1692,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();
@@ -1686,8 +1700,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
const deltaX = dragX - bounds.left < 25 ? -(25 + (bounds.left - dragX)) : bounds.right - dragX < 25 ? 25 - (bounds.right - dragX) : 0;
const deltaY = dragY - bounds.top < 25 ? -(25 + (bounds.top - dragY)) : bounds.bottom - dragY < 25 ? 25 - (bounds.bottom - dragY) : 0;
if (deltaX !== 0 || deltaY !== 0) {
- this.Document._panY = NumCast(this.Document._panY) + deltaY / 2;
- this.Document._panX = NumCast(this.Document._panX) + deltaX / 2;
+ this.Document[this.panYFieldKey] = NumCast(this.Document[this.panYFieldKey]) + deltaY / 2;
+ this.Document[this.panXFieldKey] = NumCast(this.Document[this.panXFieldKey]) + deltaX / 2;
}
}
e.stopPropagation();
@@ -1701,8 +1715,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
doc.x = scr?.[0];
doc.y = scr?.[1];
});
- this.props.addDocTab(childDocs as any as Doc, 'inParent');
- this.props.ContainingCollectionView?.removeDocument(this.props.Document);
+ this.props.addDocTab(childDocs as any as Doc, OpenWhere.inParentFromScreen);
};
@undoBatch
@@ -1712,8 +1725,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
const height = Math.max(...docs.map(doc => NumCast(doc._height))) + 20;
const dim = Math.ceil(Math.sqrt(docs.length));
docs.forEach((doc, i) => {
- doc.x = NumCast(this.Document._panX) + (i % dim) * width - (width * dim) / 2;
- doc.y = NumCast(this.Document._panY) + Math.floor(i / dim) * height - (height * dim) / 2;
+ doc.x = NumCast(this.Document[this.panXFieldKey]) + (i % dim) * width - (width * dim) / 2;
+ doc.y = NumCast(this.Document[this.panYFieldKey]) + Math.floor(i / dim) * height - (height * dim) / 2;
});
};
@@ -1721,27 +1734,32 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
toggleNativeDimensions = () => Doc.toggleNativeDimensions(this.layoutDoc, 1, this.nativeWidth, this.nativeHeight);
onContextMenu = (e: React.MouseEvent) => {
- if (this.props.isAnnotationOverlay || this.props.Document.annotationOn || !ContextMenu.Instance) return;
+ if (this.props.isAnnotationOverlay || !ContextMenu.Instance) return;
const appearance = ContextMenu.Instance.findByDescription('Appearance...');
const appearanceItems = appearance && 'subitems' in appearance ? appearance.subitems : [];
appearanceItems.push({
description: 'Reset View',
event: () => {
- this.props.Document._panX = this.props.Document._panY = 0;
+ this.props.Document[this.panXFieldKey] = this.props.Document[this.panYFieldKey] = 0;
this.props.Document[this.scaleFieldKey] = 1;
},
icon: 'compress-arrows-alt',
});
+ !Doc.noviceMode &&
+ appearanceItems.push({
+ description: 'Toggle auto arrange',
+ event: () => (this.layoutDoc._autoArrange = !this.layoutDoc._autoArrange),
+ icon: 'compress-arrows-alt',
+ });
+ if (this.props.setContentView === emptyFunction) {
+ !appearance && ContextMenu.Instance.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'eye' });
+ return;
+ }
!Doc.noviceMode && Doc.UserDoc().defaultTextLayout && appearanceItems.push({ description: 'Reset default note style', event: () => (Doc.UserDoc().defaultTextLayout = undefined), icon: 'eye' });
- appearanceItems.push({
- description: `${this.fitContentsToBox ? 'Make Zoomable' : 'Scale to Window'}`,
- event: () => (this.Document._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' });
- //appearanceItems.push({ description: `update icon`, event: this.updateIcon, icon: "compress-arrows-alt" });
- this.props.ContainingCollectionView && appearanceItems.push({ description: 'Ungroup collection', event: this.promoteCollection, icon: 'table' });
+ !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' });
this.props.Document._isGroup && this.Document.transcription && appearanceItems.push({ description: 'Ink to text', event: () => this.transcribeStrokes(false), icon: 'font' });
@@ -1755,7 +1773,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
!Doc.noviceMode
? viewCtrlItems.push({ description: (SnappingManager.GetShowSnapLines() ? 'Hide' : 'Show') + ' Snap Lines', event: () => SnappingManager.SetShowSnapLines(!SnappingManager.GetShowSnapLines()), icon: 'compress-arrows-alt' })
: null;
- !Doc.noviceMode ? viewCtrlItems.push({ description: (this.Document._useClusters ? 'Hide' : 'Show') + ' Clusters', event: () => this.updateClusters(!this.Document._useClusters), icon: 'braille' }) : null;
+ !Doc.noviceMode ? viewCtrlItems.push({ description: (this.Document._freeform_useClusters ? 'Hide' : 'Show') + ' Clusters', event: () => this.updateClusters(!this.Document._freeform_useClusters), icon: 'braille' }) : null;
!viewctrls && ContextMenu.Instance.addItem({ description: 'UI Controls...', subitems: viewCtrlItems, icon: 'eye' });
const options = ContextMenu.Instance.findByDescription('Options...');
@@ -1770,31 +1788,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
!options && ContextMenu.Instance.addItem({ description: 'Options...', subitems: optionItems, icon: 'eye' });
const mores = ContextMenu.Instance.findByDescription('More...');
const moreItems = mores && 'subitems' in mores ? mores.subitems : [];
- if (!Doc.noviceMode) {
- e.persist();
- moreItems.push({ description: 'Export collection', icon: 'download', event: async () => Doc.Zip(this.props.Document) });
- moreItems.push({ description: 'Import exported collection', icon: 'upload', event: ({ x, y }) => this.importDocument(e.clientX, e.clientY) });
- }
!mores && ContextMenu.Instance.addItem({ description: 'More...', subitems: moreItems, icon: 'eye' });
};
- importDocument = (x: number, y: number) => {
- const input = document.createElement('input');
- input.type = 'file';
- input.accept = '.zip';
- input.onchange = _e => {
- input.files &&
- Doc.importDocument(input.files[0]).then(doc => {
- if (doc instanceof Doc) {
- const [xx, yy] = this.getTransform().transformPoint(x, y);
- (doc.x = xx), (doc.y = yy);
- this.props.addDocument?.(doc);
- }
- });
- };
- input.click();
- };
-
@undoBatch
@action
transcribeStrokes = (math: boolean) => {
@@ -1805,7 +1801,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
const lines = text.split('\n');
const height = 30 + 15 * lines.length;
- this.props.ContainingCollectionView?.addDocument(Docs.Create.TextDocument(text, { title: lines[0], x: NumCast(this.layoutDoc.x) + NumCast(this.layoutDoc._width) + 20, y: NumCast(this.layoutDoc.y), _width: 200, _height: height }));
+ this.addDocument(Docs.Create.TextDocument(text, { title: lines[0], x: NumCast(this.layoutDoc.x) + NumCast(this.layoutDoc._width) + 20, y: NumCast(this.layoutDoc.y), _width: 200, _height: height }));
}
}
};
@@ -1839,22 +1835,24 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
DragManager.SetSnapLines(horizLines, vertLines);
};
+ incrementalRendering = () => this.childDocs.filter(doc => !this._renderCutoffData.get(doc[Id])).length !== 0;
+
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);
});
- children = () => {
+ get children() {
this.incrementalRender();
- const children = typeof this.props.children === 'function' ? ((this.props.children as any)() as JSX.Element[]) : [];
+ const children = typeof this.props.children === 'function' ? ((this.props.children as any)() as JSX.Element[]) : this.props.children ? [this.props.children] : [];
return [...children, ...this.views, <CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" />];
- };
+ }
@computed get placeholder() {
return (
@@ -1864,6 +1862,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
);
}
+ showPresPaths = () => (CollectionFreeFormView.ShowPresPaths ? PresBox.Instance.getPaths(this.rootDoc) : null);
+
@computed get marqueeView() {
TraceMobx();
return (
@@ -1889,13 +1889,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
r?.addEventListener('dashDragAutoScroll', this.onDragAutoScroll as any);
}}
style={{ opacity: this.props.dontRenderDocuments ? 0.7 : undefined }}>
- {this.layoutDoc._backgroundGridShow ? (
+ {this.layoutDoc._freeform_backgroundGrid ? (
<div>
<CollectionFreeFormBackgroundGrid // bcz : UGHH don't know why, but if we don't wrap in a div, then PDF's don't render whenn taking snapshot of a dashboard and the background grid is on!!?
PanelWidth={this.props.PanelWidth}
PanelHeight={this.props.PanelHeight}
panX={this.panX}
panY={this.panY}
+ nativeDimScaling={this.nativeDim}
zoomScaling={this.zoomScaling}
layoutDoc={this.layoutDoc}
isAnnotationOverlay={this.isAnnotationOverlay}
@@ -1910,10 +1911,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
isAnnotationOverlayScrollable={this.props.isAnnotationOverlayScrollable}
transform={this.contentTransform}
zoomScaling={this.zoomScaling}
- presPaths={BoolCast(this.Document.presPathView)}
- progressivize={BoolCast(this.Document.editProgressivize)}
+ presPaths={this.showPresPaths}
presPinView={BoolCast(this.Document.presPinView)}
- transition={this._viewTransition ? `transform ${this._viewTransition}ms` : Cast(this.layoutDoc._viewTransition, 'string', null)}
+ transition={this._panZoomTransition ? `transform ${this._panZoomTransition}ms` : Cast(this.layoutDoc._viewTransition, 'string', Cast(this.props.DocumentView?.()?.rootDoc._viewTransition, 'string', null))}
viewDefDivClick={this.props.viewDefDivClick}>
{this.children}
</CollectionFreeFormViewPannableContents>
@@ -1929,8 +1929,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
const nh = this.nativeHeight;
const hscale = nh ? this.props.PanelHeight() / nh : 1;
const wscale = nw ? this.props.PanelWidth() / nw : 1;
- return wscale < hscale || this.layoutDoc.fitWidth ? wscale : hscale;
+ return wscale < hscale || this.layoutDoc.layout_fitWidth ? wscale : hscale;
}
+ nativeDim = () => this.nativeDimScaling;
private groupDropDisposer?: DragManager.DragDropDisposer;
protected createGroupEventsTarget = (ele: HTMLDivElement) => {
@@ -1957,13 +1958,27 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
1000
);
};
-
+ 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 (
<div
- className={'collectionfreeformview-container'}
- ref={this.createDashEventsTarget}
+ 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
+ r?.addEventListener('wheel', this.onPassiveWheel, { passive: false });
+ }}
onWheel={this.onPointerWheel}
onClick={this.onClick}
onPointerDown={this.onPointerDown}
@@ -1972,48 +1987,71 @@ 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)}%`,
- height: this.isAnnotationOverlay && this.Document.scrollHeight ? NumCast(this.Document.scrollHeight) : `${100 / (this.nativeDimScaling || 1)}%`, // : this.isAnnotationOverlay ? (this.Document.scrollHeight ? this.Document.scrollHeight : "100%") : this.props.PanelHeight()
+ height: this.props.getScrollHeight?.() ?? `${100 / (this.nativeDimScaling || 1)}%`,
}}>
- {this._firstRender ? this.placeholder : this.marqueeView}
- {this.props.noOverlay ? null : <CollectionFreeFormOverlayView elements={this.elementFunc} />}
-
- {
- // uncomment to show snap lines
- <div className="snapLines" style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', pointerEvents: 'none' }}>
- <svg style={{ width: '100%', height: '100%' }}>
- {this._hLines?.map(l => (
- <line x1="0" y1={l} x2="1000" y2={l} stroke="black" />
- ))}
- {this._vLines?.map(l => (
- <line y1="0" x1={l} y2="1000" x2={l} stroke="black" />
- ))}
- </svg>
+ {this._lightboxDoc ? (
+ <div style={{ padding: 15, width: '100%', height: '100%' }}>
+ <DocumentView
+ {...this.props}
+ Document={this._lightboxDoc}
+ DataDoc={undefined}
+ PanelWidth={this.lightboxPanelWidth}
+ PanelHeight={this.lightboxPanelHeight}
+ NativeWidth={returnZero}
+ NativeHeight={returnZero}
+ onClick={this.onChildClickHandler}
+ onKey={this.onKeyDown}
+ onDoubleClick={this.onChildDoubleClickHandler}
+ onBrowseClick={this.onBrowseClickHandler}
+ childFilters={this.childDocFilters}
+ childFiltersByRanges={this.childDocRangeFilters}
+ searchFilterDocs={this.searchFilterDocs}
+ isDocumentActive={this.props.childDocumentsActive?.() ? this.props.isDocumentActive : this.isContentActive}
+ isContentActive={this.props.childContentsActive ?? emptyFunction}
+ addDocTab={this.addDocTab}
+ ScreenToLocalTransform={this.lightboxScreenToLocal}
+ fitContentsToBox={undefined}
+ focus={this.focus}
+ />
</div>
- }
+ ) : (
+ <>
+ {this._firstRender ? this.placeholder : this.marqueeView}
+ {this.props.noOverlay ? null : <CollectionFreeFormOverlayView elements={this.elementFunc} />}
+
+ {/* // uncomment to show snap lines */}
+ <div className="snapLines" style={{ position: 'absolute', top: 0, left: 0, width: '100%', height: '100%', pointerEvents: 'none' }}>
+ <svg style={{ width: '100%', height: '100%' }}>
+ {this._hLines?.map(l => (
+ <line x1="0" y1={l} x2="1000" y2={l} stroke="black" />
+ ))}
+ {this._vLines?.map(l => (
+ <line y1="0" x1={l} y2="1000" x2={l} stroke="black" />
+ ))}
+ </svg>
+ </div>
- {this.props.Document._isGroup && SnappingManager.GetIsDragging() && this.ChildDrag ? (
- <div
- className="collectionFreeForm-groupDropper"
- ref={this.createGroupEventsTarget}
- style={{
- width: this.ChildDrag ? '10000' : '100%',
- height: this.ChildDrag ? '10000' : '100%',
- left: this.ChildDrag ? '-5000' : 0,
- top: this.ChildDrag ? '-5000' : 0,
- position: 'absolute',
- background: '#0009930',
- pointerEvents: 'all',
- }}
- />
- ) : null}
+ {this.props.Document._isGroup && SnappingManager.GetIsDragging() && this.ChildDrag ? (
+ <div
+ className="collectionFreeForm-groupDropper"
+ ref={this.createGroupEventsTarget}
+ style={{
+ width: this.ChildDrag ? '10000' : '100%',
+ height: this.ChildDrag ? '10000' : '100%',
+ left: this.ChildDrag ? '-5000' : 0,
+ top: this.ChildDrag ? '-5000' : 0,
+ position: 'absolute',
+ background: '#0009930',
+ pointerEvents: 'all',
+ }}
+ />
+ ) : null}
+ </>
+ )}
</div>
);
}
@@ -2037,10 +2075,10 @@ interface CollectionFreeFormViewPannableContentsProps {
transform: () => string;
zoomScaling: () => number;
viewDefDivClick?: ScriptField;
- children: () => JSX.Element[];
+ children?: React.ReactNode | undefined;
+ //children: () => JSX.Element[];
transition?: string;
- presPaths?: boolean;
- progressivize?: boolean;
+ presPaths: () => JSX.Element | null;
presPinView?: boolean;
isAnnotationOverlay: boolean | undefined;
isAnnotationOverlayScrollable: boolean | undefined;
@@ -2093,42 +2131,11 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF
return true;
};
- // scale: NumCast(targetDoc._viewScale),
- @computed get zoomProgressivizeContainer() {
- const activeItem = PresBox.Instance.activeItem;
- // const targetDoc = PresBox.Instance.targetDoc;
- if (activeItem && activeItem.presPinView && activeItem.id) {
- const left = NumCast(activeItem.presPinViewX);
- const top = NumCast(activeItem.presPinViewY);
- const width = 100;
- const height = 100;
- return !this.props.presPinView ? null : (
- <div key="resizable" className="resizable" onPointerDown={this.onPointerDown} style={{ width, height, top, left, position: 'absolute' }}>
- <div className="resizers" key={'resizer' + activeItem.id}>
- <div className="resizer top-left" onPointerDown={this.onPointerDown} />
- <div className="resizer top-right" onPointerDown={this.onPointerDown} />
- <div className="resizer bottom-left" onPointerDown={this.onPointerDown} />
- <div className="resizer bottom-right" onPointerDown={this.onPointerDown} />
- </div>
- </div>
- );
- }
- }
-
- @computed get zoomProgressivize() {
- return PresBox.Instance?.activeItem?.presPinView && PresBox.Instance.layoutDoc.presStatus === 'edit' ? this.zoomProgressivizeContainer : null;
- }
-
- @computed get progressivize() {
- return PresBox.Instance && this.props.progressivize ? PresBox.Instance.progressivizeChildDocs : null;
- }
-
@computed get presPaths() {
- const presPaths = 'presPaths' + (this.props.presPaths ? '' : '-hidden');
- return !PresBox.Instance || !this.props.presPaths ? null : (
+ return !this.props.presPaths() ? null : (
<>
- <div key="presorder">{PresBox.Instance.order}</div>
- <svg key="svg" className={presPaths}>
+ <div key="presorder">{PresBox.Instance?.order}</div>
+ <svg key="svg" className="presPaths">
<defs>
<marker id="markerSquare" markerWidth="3" markerHeight="3" refX="1.5" refY="1.5" orient="auto" overflow="visible">
<rect x="0" y="0" width="3" height="3" stroke="#69a6db" strokeWidth="1" fill="white" fillOpacity="0.8" />
@@ -2140,7 +2147,7 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF
<path d="M2,2 L2,6 L6,4 L2,2 Z" stroke="#69a6db" strokeLinejoin="round" strokeWidth="1" fill="white" fillOpacity="0.8" />
</marker>
</defs>
- {PresBox.Instance.paths}
+ {this.props.presPaths()}
</svg>
</>
);
@@ -2163,7 +2170,7 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF
width: this.props.isAnnotationOverlay ? undefined : 0, // if not an overlay, then this will be the size of the collection, but panning and zooming will move it outside the visible border of the collection and make it selectable. This problem shows up after zooming/panning on a background collection -- you can drag the collection by clicking on apparently empty space outside the collection
//willChange: "transform"
}}>
- {this.props.children()}
+ {this.props.children}
{!this.props.brushView.width ? null : (
<div
className="collectionFreeFormView-brushView"
@@ -2180,8 +2187,6 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF
/>
)}
{this.presPaths}
- {this.progressivize}
- {this.zoomProgressivize}
</div>
);
}
@@ -2193,6 +2198,7 @@ interface CollectionFreeFormViewBackgroundGridProps {
PanelWidth: () => number;
PanelHeight: () => number;
isAnnotationOverlay?: boolean;
+ nativeDimScaling: () => number;
zoomScaling: () => number;
layoutDoc: Doc;
cachedCenteringShiftX: number;
@@ -2210,10 +2216,10 @@ class CollectionFreeFormBackgroundGrid extends React.Component<CollectionFreeFor
const shiftX = (this.props.isAnnotationOverlay ? 0 : (-this.props.panX() % gridSpace) - gridSpace) * this.props.zoomScaling();
const shiftY = (this.props.isAnnotationOverlay ? 0 : (-this.props.panY() % gridSpace) - gridSpace) * this.props.zoomScaling();
const renderGridSpace = gridSpace * this.props.zoomScaling();
- const w = this.props.PanelWidth() + 2 * renderGridSpace;
- const h = this.props.PanelHeight() + 2 * renderGridSpace;
+ const w = this.props.PanelWidth() / this.props.nativeDimScaling() + 2 * renderGridSpace;
+ const h = this.props.PanelHeight() / this.props.nativeDimScaling() + 2 * renderGridSpace;
const strokeStyle = Doc.ActiveDashboard?.colorScheme === ColorScheme.Dark ? 'rgba(255,255,255,0.5)' : 'rgba(0, 0,0,0.5)';
- return (
+ return !this.props.nativeDimScaling() ? null : (
<canvas
className="collectionFreeFormView-grid"
width={w}
@@ -2248,20 +2254,24 @@ class CollectionFreeFormBackgroundGrid extends React.Component<CollectionFreeFor
}
export function CollectionBrowseClick(dv: DocumentView, clientX: number, clientY: number) {
+ const browseTransitionTime = 500;
SelectionManager.DeselectAll();
- dv.props.focus(dv.props.Document, {
- willZoom: true,
- afterFocus: async didMove => {
- if (!didMove) {
- const selfFfview = dv.ComponentView instanceof CollectionFreeFormView ? dv.ComponentView : undefined;
- const parFfview = dv.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView;
- const ffview = selfFfview && selfFfview.rootDoc[selfFfview.props.scaleField || '_viewScale'] !== 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
- await ffview?.zoomSmoothlyAboutPt(ffview.getTransform().transformPoint(clientX, clientY), 0.5);
- }
- return ViewAdjustment.doNothing;
- },
- });
- Doc.linkFollowHighlight(dv?.props.Document, false);
+ 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 });
+ }
}
ScriptingGlobals.add(CollectionBrowseClick);
ScriptingGlobals.add(function nextKeyFrame(readOnly: boolean) {
@@ -2270,6 +2280,32 @@ ScriptingGlobals.add(function nextKeyFrame(readOnly: boolean) {
ScriptingGlobals.add(function prevKeyFrame(readOnly: boolean) {
!readOnly && (SelectionManager.Views()[0].ComponentView as CollectionFreeFormView)?.changeKeyFrame(true);
});
-ScriptingGlobals.add(function pinWithView(readOnly: boolean, pinDocContent: boolean) {
- !readOnly && SelectionManager.Views().forEach(view => TabDocView.PinDoc(view.rootDoc, { pinDocContent, pinViewport: MarqueeView.CurViewBounds(view.rootDoc, view.props.PanelWidth(), view.props.PanelHeight()) }));
+ScriptingGlobals.add(function curKeyFrame(readOnly: boolean) {
+ const selView = SelectionManager.Views();
+ if (readOnly) return selView[0].ComponentView?.getKeyFrameEditing?.() ? Colors.MEDIUM_BLUE : 'transparent';
+ runInAction(() => selView[0].ComponentView?.setKeyFrameEditing?.(!selView[0].ComponentView?.getKeyFrameEditing?.()));
+});
+ScriptingGlobals.add(function pinWithView(pinContent: boolean) {
+ SelectionManager.Views().forEach(view =>
+ view.props.pinToPres(view.rootDoc, {
+ currentFrame: Cast(view.rootDoc.currentFrame, 'number', null),
+ pinData: {
+ poslayoutview: pinContent,
+ dataview: pinContent,
+ },
+ pinViewport: MarqueeView.CurViewBounds(view.rootDoc, view.props.PanelWidth(), view.props.PanelHeight()),
+ })
+ );
+});
+ScriptingGlobals.add(function bringToFront() {
+ SelectionManager.Views().forEach(view => view.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView.bringToFront(view.rootDoc));
+});
+ScriptingGlobals.add(function sendToBack(doc: Doc) {
+ SelectionManager.Views().forEach(view => view.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView.bringToFront(view.rootDoc, true));
+});
+ScriptingGlobals.add(function resetView() {
+ SelectionManager.Docs().forEach(doc => {
+ doc._freeform_panX = doc._freeform_panY = 0;
+ doc._freeform_scale = 1;
+ });
});
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
index 488f51d77..c9168d40a 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
@@ -3,8 +3,6 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tooltip } from '@material-ui/core';
import { observer } from 'mobx-react';
import { unimplementedFunction } from '../../../../Utils';
-import { DocumentType } from '../../../documents/DocumentTypes';
-import { SelectionManager } from '../../../util/SelectionManager';
import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu';
@observer
@@ -26,33 +24,39 @@ export class MarqueeOptionsMenu extends AntimodeMenu<AntimodeMenuProps> {
render() {
const presPinWithViewIcon = <img src="/assets/pinWithView.png" style={{ margin: 'auto', width: 19, transform: 'translate(-2px, -2px)' }} />;
- 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>,
- ];
+ 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>
+ </>
+ );
return this.getElement(buttons);
}
}
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index a020b67cd..5febbe83e 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';
@@ -17,9 +18,8 @@ import { SelectionManager } from '../../../util/SelectionManager';
import { Transform } from '../../../util/Transform';
import { undoBatch, UndoManager } from '../../../util/UndoManager';
import { ContextMenu } from '../../ContextMenu';
+import { OpenWhere } from '../../nodes/DocumentView';
import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
-import { PresBox } from '../../nodes/trails/PresBox';
-import { VideoBox } from '../../nodes/VideoBox';
import { pasteImageBitmap } from '../../nodes/WebBoxRenderer';
import { PreviewCursor } from '../../PreviewCursor';
import { SubCollectionViewProps } from '../CollectionSubView';
@@ -40,16 +40,7 @@ interface MarqueeViewProps {
nudge?: (x: number, y: number, nudgeTime?: number) => boolean;
ungroup?: () => void;
setPreviewCursor?: (func: (x: number, y: number, drag: boolean, hide: boolean) => void) => void;
- slowLoadDocuments: (
- files: File[] | string,
- options: DocumentOptions,
- generatedDocuments: Doc[],
- text: string,
- completed: ((doc: Doc[]) => void) | undefined,
- clientX: number,
- clientY: number,
- addDocument: (doc: Doc | Doc[]) => boolean
- ) => Promise<void>;
+ slowLoadDocuments: (files: File[] | string, options: DocumentOptions, generatedDocuments: Doc[], text: string, completed: ((doc: Doc[]) => void) | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => Promise<void>;
}
export interface MarqueeViewBounds {
@@ -62,8 +53,8 @@ export interface MarqueeViewBounds {
@observer
export class MarqueeView extends React.Component<SubCollectionViewProps & MarqueeViewProps> {
public static CurViewBounds(pinDoc: Doc, panelWidth: number, panelHeight: number) {
- const ps = NumCast(pinDoc._viewScale, 1);
- return { left: NumCast(pinDoc._panX) - panelWidth / 2 / ps, top: NumCast(pinDoc._panY) - panelHeight / 2 / ps, width: panelWidth / ps, height: panelHeight / ps };
+ const ps = NumCast(pinDoc._freeform_scale, 1);
+ return { left: NumCast(pinDoc._freeform_panX) - panelWidth / 2 / ps, top: NumCast(pinDoc._freeform_panY) - panelHeight / 2 / ps, width: panelWidth / ps, height: panelHeight / ps };
}
private _commandExecuted = false;
@@ -112,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
@@ -120,8 +110,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
const cm = ContextMenu.Instance;
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', useCors: true }), 'add:right'));
-
+ 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) {
@@ -167,7 +156,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
pasteImageBitmap((data: any, error: any) => {
error && console.log(error);
data &&
- VideoBox.convertDataUri(data, this.props.Document[Id] + '-thumb-frozen').then(returnedfilename => {
+ Utils.convertDataUri(data, this.props.Document[Id] + '-thumb-frozen').then(returnedfilename => {
this.props.Document['thumb-frozen'] = new ImageField(returnedfilename);
});
})
@@ -183,7 +172,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
e.stopPropagation();
} 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();
}
@@ -206,7 +195,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
const rowProto = new Doc();
rowProto.title = rowProto.Id;
rowProto._width = 200;
- rowProto.isPrototype = true;
+ rowProto.isDataDoc = true;
for (let i = 1; i < ns.length - 1; i++) {
const values = ns[i].split('\t');
if (values.length === 1 && columns.length > 1) {
@@ -214,7 +203,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
continue;
}
const docDataProto = Doc.MakeDelegate(rowProto);
- docDataProto.isPrototype = true;
+ docDataProto.isDataDoc = true;
columns.forEach((col, i) => (docDataProto[columns[i]] = values.length > i ? (values[i].indexOf(Number(values[i]).toString()) !== -1 ? Number(values[i]) : values[i]) : undefined));
if (groupAttr) {
docDataProto._group = groupAttr;
@@ -238,12 +227,13 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
@action
onPointerDown = (e: React.PointerEvent): void => {
+ // if (this.props.pointerEvents?.() === 'none') return;
this._downX = this._lastX = e.clientX;
this._downY = this._lastY = e.clientY;
if (!(e.nativeEvent as any).marqueeHit) {
(e.nativeEvent as any).marqueeHit = true;
// allow marquee if right click OR alt+left click OR in adding presentation slide & left key drag mode
- if (e.button === 2 || (e.button === 0 && e.altKey) || (PresBox.startMarquee && e.button === 0)) {
+ if (e.button === 2 || (e.button === 0 && e.altKey)) {
// if (e.altKey || (MarqueeView.DragMarquee && this.props.active(true))) {
this.setPreviewCursor(e.clientX, e.clientY, true, false);
// (!e.altKey) && e.stopPropagation(); // bcz: removed so that you can alt-click on button in a collection to switch link following behaviors.
@@ -272,9 +262,6 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
this.cleanupInteractions(true); // stop listening for events if another lower-level handle (e.g. another Marquee) has stopPropagated this
}
e.altKey && e.preventDefault();
- if (PresBox.startMarquee) {
- e.stopPropagation();
- }
};
@action
@@ -295,11 +282,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
document.removeEventListener('pointerdown', hideMarquee, true);
document.removeEventListener('wheel', hideMarquee, true);
};
- if (PresBox.startMarquee) {
- this.pinWithView();
- PresBox.startMarquee = false;
- }
- if (!this._commandExecuted && Math.abs(this.Bounds.height * this.Bounds.width) > 100 && !PresBox.startMarquee) {
+ if (!this._commandExecuted && Math.abs(this.Bounds.height * this.Bounds.width) > 100) {
MarqueeOptionsMenu.Instance.createCollection = this.collection;
MarqueeOptionsMenu.Instance.delete = this.delete;
MarqueeOptionsMenu.Instance.summarize = this.summary;
@@ -343,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);
}
@@ -353,7 +336,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
@action
onClick = (e: React.MouseEvent): void => {
- if (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) {
+ if (this.props.pointerEvents?.() === 'none') return;
+ 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;
@@ -377,10 +361,10 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
@undoBatch
@action
- delete = () => {
+ delete = (e?: React.PointerEvent<Element> | KeyboardEvent | undefined, hide?: boolean) => {
const selected = this.marqueeSelect(false);
SelectionManager.DeselectAll();
- selected.forEach(doc => this.props.removeDocument?.(doc));
+ selected.forEach(doc => (hide ? (doc.hidden = true) : this.props.removeDocument?.(doc)));
this.cleanupInteractions(false);
MarqueeOptionsMenu.Instance.fadeOut(true);
@@ -393,22 +377,25 @@ 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.AddDocToList(Doc.MyFileOrphans, undefined, Doc.GetProto(doc));
- doc._panX = doc._panY = 0;
+ !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));
- newCollection.system = undefined;
+ newCollection.isSystem = undefined;
newCollection._width = this.Bounds.width;
newCollection._height = this.Bounds.height;
newCollection._isGroup = makeGroup;
+ newCollection._dragWhenActive = makeGroup;
newCollection.forceActive = makeGroup;
newCollection.x = this.Bounds.left;
newCollection.y = this.Bounds.top;
- selected.forEach(d => (d.context = newCollection));
+ newCollection.layout_fitWidth = true;
+ selected.forEach(d => (d.embedContainer = newCollection));
this.hideMarquee();
return newCollection;
});
+ @undoBatch
@action
pileup = (e: KeyboardEvent | React.PointerEvent | undefined) => {
const selected = this.marqueeSelect(false);
@@ -429,36 +416,25 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
*/
@undoBatch
@action
- pinWithView = async () => {
- const doc = this.props.Document;
- TabDocView.PinDoc(doc, { pinViewport: this.Bounds });
+ pinWithView = () => {
+ TabDocView.PinDoc(this.props.Document, { pinViewport: this.Bounds });
MarqueeOptionsMenu.Instance.fadeOut(true);
this.hideMarquee();
};
@undoBatch
@action
- collection = (e: KeyboardEvent | React.PointerEvent | undefined, group?: boolean) => {
- const selected = this.marqueeSelect(false);
+ collection = (e: KeyboardEvent | React.PointerEvent | undefined, group?: boolean, selection?: Doc[]) => {
+ const selected = selection ?? this.marqueeSelect(false);
+ const activeFrame = selected.reduce((v, d) => v ?? Cast(d._activeFrame, 'number', null), undefined as number | undefined);
if (e instanceof KeyboardEvent ? 'cg'.includes(e.key) : true) {
- 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
- d.x = dx - this.Bounds.left - this.Bounds.width / 2;
- d.y = dy - this.Bounds.top - this.Bounds.height / 2;
- return d;
- })
- );
this.props.removeDocument?.(selected);
}
- // TODO: nda - this is the code to actually get a new grouped collection
+
const newCollection = this.getCollection(selected, (e as KeyboardEvent)?.key === 't' ? Docs.Create.StackingDocument : undefined, group);
+ 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]);
MarqueeOptionsMenu.Instance.fadeOut(true);
@@ -532,46 +508,46 @@ 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;
d.y = NumCast(d.y) - this.Bounds.top;
return d;
});
- const summary = Docs.Create.TextDocument('', { backgroundColor: '#e2ad32', x: this.Bounds.left, y: this.Bounds.top, isPushpin: true, _width: 200, _height: 200, _fitContentsToBox: true, _showSidebar: true, title: 'overview' });
+ const summary = Docs.Create.TextDocument('', {
+ backgroundColor: '#e2ad32',
+ x: this.Bounds.left,
+ y: this.Bounds.top,
+ followLinkToggle: true,
+ _width: 200,
+ _height: 200,
+ _layout_fitContentsToBox: true,
+ _layout_showSidebar: true,
+ title: 'overview',
+ });
const portal = Docs.Create.FreeformDocument(selected, { x: this.Bounds.left + 200, y: this.Bounds.top, isGroup: true, backgroundColor: 'transparent' });
- DocUtils.MakeLink({ doc: summary }, { doc: portal }, 'summary of:summarized by', '');
+ DocUtils.MakeLink(summary, portal, { link_relationship: 'summary of:summarized by' });
portal.hidden = true;
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;
}
- if (e.key === 'Backspace' || e.key === 'Delete' || e.key === 'd') {
+ if (e.key === 'Backspace' || e.key === 'Delete' || e.key === 'd' || e.key === 'h') {
this._commandExecuted = true;
e.stopPropagation();
(e as any).propagationIsStopped = true;
- this.delete();
+ 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();
@@ -579,7 +555,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);
}
@@ -589,7 +564,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) {
@@ -626,7 +601,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
return false;
}
- marqueeSelect(selectBackgrounds: boolean = true) {
+ marqueeSelect(selectBackgrounds: boolean = false) {
const selection: Doc[] = [];
const selectFunc = (doc: Doc) => {
const layoutDoc = Doc.Layout(doc);
@@ -685,7 +660,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
className="marqueeView"
style={{
overflow: StrCast(this.props.Document._overflow),
- cursor: [InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool) || this._visible || PresBox.startMarquee ? 'crosshair' : 'pointer',
+ cursor: [InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool) || this._visible ? 'crosshair' : 'pointer',
}}
onDragOver={e => e.preventDefault()}
onScroll={e => (e.currentTarget.scrollTop = e.currentTarget.scrollLeft = 0)}
diff --git a/src/client/views/collections/collectionGrid/CollectionGridView.tsx b/src/client/views/collections/collectionGrid/CollectionGridView.tsx
index 9468c5f06..cd8b7a0cc 100644
--- a/src/client/views/collections/collectionGrid/CollectionGridView.tsx
+++ b/src/client/views/collections/collectionGrid/CollectionGridView.tsx
@@ -4,7 +4,7 @@ import * as React from 'react';
import { Doc, Opt } from '../../../../fields/Doc';
import { Id } from '../../../../fields/FieldSymbols';
import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
-import { emptyFunction, OmitKeys, returnFalse, setupMoveUpEvents } from '../../../../Utils';
+import { emptyFunction, returnFalse, returnZero, setupMoveUpEvents } from '../../../../Utils';
import { Docs } from '../../../documents/Documents';
import { DragManager } from '../../../util/DragManager';
import { SnappingManager } from '../../../util/SnappingManager';
@@ -186,7 +186,10 @@ export class CollectionGridView extends CollectionSubView() {
getDisplayDoc(layout: Doc, dxf: () => Transform, width: () => number, height: () => number) {
return (
<DocumentView
- {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight']).omit}
+ {...this.props}
+ NativeWidth={returnZero}
+ NativeHeight={returnZero}
+ setContentView={emptyFunction}
Document={layout}
DataDoc={layout.resolvedDataDoc as Doc}
isContentActive={this.isChildContentActive}
@@ -196,7 +199,7 @@ export class CollectionGridView extends CollectionSubView() {
whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged}
onClick={this.onChildClickHandler}
renderDepth={this.props.renderDepth + 1}
- dontCenter={this.props.Document.centerY ? '' : 'y'}
+ dontCenter={this.props.Document.centerY ? undefined : 'y'}
/>
);
}
@@ -365,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 521bcda1e..3e3709827 100644
--- a/src/client/views/collections/collectionLinear/CollectionLinearView.scss
+++ b/src/client/views/collections/collectionLinear/CollectionLinearView.scss
@@ -1,9 +1,13 @@
@import '../../global/globalCssVariables';
@import '../../_nodeModuleOverrides';
+.collectionLinearView {
+ width: 100%;
+}
.collectionLinearView-label {
color: black;
background-color: $light-gray;
+ width: 100%;
}
.collectionLinearView-outer {
overflow: visible;
@@ -15,8 +19,6 @@
}
&.true {
- padding-left: 5px;
- padding-right: 5px;
border-left: $standard-border;
background-color: $medium-blue-alt;
}
@@ -29,7 +31,6 @@
display: flex;
height: 100%;
align-items: center;
- gap: 5px;
.collectionView {
overflow: visible !important;
@@ -101,13 +102,12 @@
background-color: $medium-blue;
padding: 5;
border-radius: 2px;
- height: 25;
+ height: 100%;
min-width: 25;
margin: 0;
color: $white;
display: flex;
font-weight: 100;
- width: fit-content;
transition: transform 0.2s;
align-items: center;
justify-content: center;
diff --git a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
index 9778fc4fe..8f90e4444 100644
--- a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
+++ b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
@@ -1,21 +1,22 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { StylesProvider, Tooltip } from '@material-ui/core';
-import { action, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
+import { Tooltip } from '@material-ui/core';
+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';
-import { DragManager } from '../../../util/DragManager';
+import { DragManager, dropActionType } from '../../../util/DragManager';
import { Transform } from '../../../util/Transform';
-import { Colors, Shadows } from '../../global/globalEnums';
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';
@@ -23,7 +24,7 @@ import './CollectionLinearView.scss';
/**
* 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)
@@ -40,46 +41,22 @@ 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 + (this.layoutDoc.linearViewIsExpanded ? this.childDocs.length * this.rootDoc[HeightSym]() : 10),
+ () => 5 + NumCast(this.rootDoc.linearBtnWidth, this.dimension()) + (this.layoutDoc.linearView_IsExpanded ? 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); // 2 * the padding
+ dimension = () => NumCast(this.rootDoc._height);
getTransform = (ele: Opt<HTMLDivElement>) => {
if (!ele) return Transform.Identity();
const { scale, translateX, translateY } = Utils.GetScreenTransform(ele);
@@ -88,12 +65,8 @@ export class CollectionLinearView extends CollectionSubView() {
@action
exitLongLinks = () => {
- if (DocumentLinksButton.StartLink) {
- if (DocumentLinksButton.StartLink.Document) {
- action((e: React.PointerEvent<HTMLDivElement>) => {
- Doc.UnBrushDoc(DocumentLinksButton.StartLink?.Document as Doc);
- });
- }
+ if (DocumentLinksButton.StartLink?.Document) {
+ action((e: React.PointerEvent<HTMLDivElement>) => Doc.UnBrushDoc(DocumentLinksButton.StartLink?.Document as Doc));
}
DocumentLinksButton.StartLink = undefined;
DocumentLinksButton.StartLinkView = undefined;
@@ -150,19 +123,29 @@ export class CollectionLinearView extends CollectionSubView() {
<span className="bottomPopup-text">
Currently playing:
{CollectionStackedTimeline.CurrentlyPlaying.map((clip, i) => (
- <span className="audio-title" onPointerDown={() => DocumentManager.Instance.jumpToDocument(clip, true, undefined, [])}>
- {clip.title + (i === CollectionStackedTimeline.CurrentlyPlaying.length - 1 ? '' : ',')}
- </span>
+ <>
+ <span className="audio-title" onPointerDown={() => DocumentManager.Instance.showDocument(clip.rootDoc, { willZoomCentered: true })}>
+ {clip.rootDoc.title + (i === CollectionStackedTimeline.CurrentlyPlaying.length - 1 ? ' ' : ',')}
+ </span>
+ <FontAwesomeIcon icon={!clip.ComponentView?.IsPlaying?.() ? 'play' : 'pause'} size="lg" onPointerDown={() => clip.ComponentView?.TogglePause?.()} />{' '}
+ <FontAwesomeIcon icon="times" size="lg" onPointerDown={() => clip.ComponentView?.Pause?.()} />{' '}
+ </>
))}
</span>
</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>;
@@ -175,10 +158,10 @@ export class CollectionLinearView extends CollectionSubView() {
ref={r => (dref = r || undefined)}
style={{
pointerEvents: 'all',
- width: nested ? undefined : NumCast(doc._width),
- height: nested ? undefined : NumCast(doc._height),
- marginLeft: !nested ? 2.5 : 0,
- marginRight: !nested ? 2.5 : 0,
+ width: NumCast(doc._width),
+ height: NumCast(doc._height),
+ marginLeft: 2,
+ marginRight: 2,
// width: NumCast(pair.layout._width),
// height: NumCast(pair.layout._height),
}}>
@@ -190,11 +173,12 @@ export class CollectionLinearView extends CollectionSubView() {
moveDocument={this.props.moveDocument}
addDocTab={this.props.addDocTab}
pinToPres={emptyFunction}
+ dragAction={(this.layoutDoc.childDragAction ?? this.props.childDragAction) as dropActionType}
rootSelected={this.props.isSelected}
removeDocument={this.props.removeDocument}
ScreenToLocalTransform={docXf}
- PanelWidth={nested ? doc[WidthSym] : this.dimension}
- 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}
@@ -202,11 +186,9 @@ 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}
- ContainingCollectionView={undefined}
- ContainingCollectionDoc={undefined}
hideResizeHandles={true}
/>
</div>
@@ -216,7 +198,7 @@ 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_IsExpanded);
const menuOpener = (
<label
@@ -229,9 +211,9 @@ export class CollectionLinearView extends CollectionSubView() {
);
return (
- <div className={`collectionLinearView-outer ${this.layoutDoc.linearViewSubMenu}`} style={{ backgroundColor: this.layoutDoc.linearViewIsExpanded ? undefined : 'transparent' }}>
- <div className="collectionLinearView" ref={this.createDashEventsTarget} onContextMenu={this.myContextMenu}>
- {!this.props.Document.linearViewExpandable ? null : (
+ <div className={`collectionLinearView-outer ${this.layoutDoc.linearView_SubMenu}`} style={{ backgroundColor: this.layoutDoc.linearView_IsExpanded ? undefined : 'transparent' }}>
+ <div className="collectionLinearView" ref={this.createDashEventsTarget} onContextMenu={this.myContextMenu} style={{ minHeight: this.dimension() }}>
+ {!this.props.Document.linearView_Expandable ? null : (
<Tooltip title={<div className="dash-tooltip">{isExpanded ? 'Close' : 'Open'}</div>} placement="top">
{menuOpener}
</Tooltip>
@@ -247,22 +229,23 @@ export class CollectionLinearView extends CollectionSubView() {
self: this.rootDoc,
_readOnly_: false,
scriptContext: this.props.scriptContext,
- thisContainer: this.props.ContainingCollectionDoc,
- documentView: this.props.docViewPath().lastElement(),
+ documentView: this.props.DocumentView?.(),
});
- this.layoutDoc.linearViewIsExpanded = this.addMenuToggle.current!.checked;
+ this.layoutDoc.linearView_IsExpanded = this.addMenuToggle.current!.checked;
})}
/>
- <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_IsExpanded ? 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.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
index 465dbfe6d..6dcd2d422 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
@@ -2,11 +2,9 @@ import { action, computed } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { Doc, DocListCast } from '../../../../fields/Doc';
-import { List } from '../../../../fields/List';
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';
@@ -195,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) {
@@ -226,6 +220,7 @@ export class CollectionMulticolumnView extends CollectionSubView() {
}
})
);
+ return true;
}
}
return false;
@@ -234,16 +229,15 @@ export class CollectionMulticolumnView extends CollectionSubView() {
onChildClickHandler = () => ScriptCast(this.Document.onChildClick);
onChildDoubleClickHandler = () => ScriptCast(this.Document.onChildDoubleClick);
- addDocTab = (doc: Doc, where: string) => {
- if (where === 'inPlace' && this.layoutDoc.isInPlaceContainer) {
- this.dataDoc[this.props.fieldKey] = new List<Doc>([doc]);
- return true;
- }
- return this.props.addDocTab(doc, where);
+ isContentActive = () => this.props.isSelected() || this.props.isContentActive() || this.props.isAnyChildContentActive();
+ isChildContentActive = () => {
+ const childDocsActive = this.props.childDocumentsActive?.() ?? this.rootDoc.childDocumentsActive;
+ return this.props.isContentActive?.() === false || childDocsActive === false
+ ? false //
+ : this.props.isDocumentActive?.() && childDocsActive
+ ? true
+ : undefined;
};
- isContentActive = () => this.props.isSelected() || this.props.isContentActive();
- isChildContentActive = () =>
- ((this.props.childDocumentsActive?.() || this.Document._childDocumentsActive) && this.props.isDocumentActive?.() && SnappingManager.GetIsDragging()) || this.props.isSelected() || this.props.isAnyChildContentActive() ? true : false;
getDisplayDoc = (layout: Doc, dxf: () => Transform, width: () => number, height: () => number) => {
return (
<DocumentView
@@ -257,7 +251,7 @@ export class CollectionMulticolumnView extends CollectionSubView() {
PanelWidth={width}
PanelHeight={height}
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}
@@ -268,17 +262,15 @@ 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}
- ContainingCollectionDoc={this.props.CollectionView?.props.Document}
- ContainingCollectionView={this.props.CollectionView}
dontRegisterView={this.props.dontRegisterView}
addDocument={this.props.addDocument}
moveDocument={this.props.moveDocument}
removeDocument={this.props.removeDocument}
whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged}
- addDocTab={this.addDocTab}
+ addDocTab={this.props.addDocTab}
pinToPres={this.props.pinToPres}
bringToFront={returnFalse}
/>
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
index f8de4e5de..4df5dad5a 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
@@ -2,11 +2,9 @@ import { action, computed } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { Doc, DocListCast } from '../../../../fields/Doc';
-import { List } from '../../../../fields/List';
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';
@@ -195,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) {
@@ -226,6 +220,7 @@ export class CollectionMultirowView extends CollectionSubView() {
}
})
);
+ return true;
}
}
return false;
@@ -234,16 +229,15 @@ export class CollectionMultirowView extends CollectionSubView() {
onChildClickHandler = () => ScriptCast(this.Document.onChildClick);
onChildDoubleClickHandler = () => ScriptCast(this.Document.onChildDoubleClick);
- addDocTab = (doc: Doc, where: string) => {
- if (where === 'inPlace' && this.layoutDoc.isInPlaceContainer) {
- this.dataDoc[this.props.fieldKey] = new List<Doc>([doc]);
- return true;
- }
- return this.props.addDocTab(doc, where);
+ isContentActive = () => this.props.isSelected() || this.props.isContentActive() || this.props.isAnyChildContentActive();
+ isChildContentActive = () => {
+ const childDocsActive = this.props.childDocumentsActive?.() ?? this.rootDoc.childDocumentsActive;
+ return this.props.isContentActive?.() === false || childDocsActive === false
+ ? false //
+ : this.props.isDocumentActive?.() && childDocsActive
+ ? true
+ : undefined;
};
- isContentActive = () => this.props.isSelected() || this.props.isContentActive();
- isChildContentActive = () =>
- ((this.props.childDocumentsActive?.() || this.Document._childDocumentsActive) && this.props.isDocumentActive?.() && SnappingManager.GetIsDragging()) || this.props.isSelected() || this.props.isAnyChildContentActive() ? true : false;
getDisplayDoc = (layout: Doc, dxf: () => Transform, width: () => number, height: () => number) => {
return (
<DocumentView
@@ -257,7 +251,7 @@ export class CollectionMultirowView extends CollectionSubView() {
PanelWidth={width}
PanelHeight={height}
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}
@@ -266,18 +260,17 @@ 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}
- ContainingCollectionDoc={this.props.CollectionView?.props.Document}
- ContainingCollectionView={this.props.CollectionView}
dontRegisterView={this.props.dontRegisterView}
addDocument={this.props.addDocument}
moveDocument={this.props.moveDocument}
removeDocument={this.props.removeDocument}
whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged}
- addDocTab={this.addDocTab}
+ addDocTab={this.props.addDocTab}
pinToPres={this.props.pinToPres}
bringToFront={returnFalse}
/>
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx
deleted file mode 100644
index adcd9e1e3..000000000
--- a/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx
+++ /dev/null
@@ -1,639 +0,0 @@
-import React = require("react");
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, computed, observable } from "mobx";
-import { observer } from "mobx-react";
-import { extname } from "path";
-import DatePicker from "react-datepicker";
-import { CellInfo } from "react-table";
-import { DateField } from "../../../../fields/DateField";
-import { Doc, DocListCast, Field, Opt } from "../../../../fields/Doc";
-import { Id } from "../../../../fields/FieldSymbols";
-import { List } from "../../../../fields/List";
-import { SchemaHeaderField } from "../../../../fields/SchemaHeaderField";
-import { ComputedField } from "../../../../fields/ScriptField";
-import { BoolCast, Cast, DateCast, FieldValue, StrCast } from "../../../../fields/Types";
-import { ImageField } from "../../../../fields/URLField";
-import { emptyFunction, Utils } from "../../../../Utils";
-import { Docs } from "../../../documents/Documents";
-import { DocumentType } from "../../../documents/DocumentTypes";
-import { DocumentManager } from "../../../util/DocumentManager";
-import { DragManager } from "../../../util/DragManager";
-import { KeyCodes } from "../../../util/KeyCodes";
-import { CompileScript } from "../../../util/Scripting";
-import { SearchUtil } from "../../../util/SearchUtil";
-import { SnappingManager } from "../../../util/SnappingManager";
-import { undoBatch } from "../../../util/UndoManager";
-import '../../../views/DocumentDecorations.scss';
-import { EditableView } from "../../EditableView";
-import { MAX_ROW_HEIGHT } from '../../global/globalCssVariables.scss';
-import { DocumentIconContainer } from "../../nodes/DocumentIcon";
-import { OverlayView } from "../../OverlayView";
-import { CollectionView } from "../CollectionView";
-import "./CollectionSchemaView.scss";
-
-// intialize cell properties
-export interface CellProps {
- row: number;
- col: number;
- rowProps: CellInfo;
- // currently unused
- CollectionView: Opt<CollectionView>;
- // currently unused
- ContainingCollection: Opt<CollectionView>;
- Document: Doc;
- // column name
- fieldKey: string;
- // currently unused
- renderDepth: number;
- // called when a button is pressed on the node itself
- addDocTab: (document: Doc, where: string) => boolean;
- pinToPres: (document: Doc) => void;
- moveDocument?: (document: Doc | Doc[], targetCollection: Doc | undefined,
- addDocument: (document: Doc | Doc[]) => boolean) => boolean;
- isFocused: boolean;
- changeFocusedCellByIndex: (row: number, col: number) => void;
- // set whether the cell is in the isEditing mode
- setIsEditing: (isEditing: boolean) => void;
- isEditable: boolean;
- setPreviewDoc: (doc: Doc) => void;
- setComputed: (script: string, doc: Doc, field: string, row: number, col: number) => boolean;
- getField: (row: number, col?: number) => void;
- // currnetly unused
- showDoc: (doc: Doc | undefined, dataDoc?: any, screenX?: number, screenY?: number) => void;
-}
-
-@observer
-export class CollectionSchemaCell extends React.Component<CellProps> {
- // return a field key that is corrected for whether it COMMENT
- public static resolvedFieldKey(column: string, rowDoc: Doc) {
- const fieldKey = column;
- if (fieldKey.startsWith("*")) {
- const rootKey = fieldKey.substring(1);
- const allKeys = [...Array.from(Object.keys(rowDoc)), ...Array.from(Object.keys(Doc.GetProto(rowDoc)))];
- const matchedKeys = allKeys.filter(key => key.includes(rootKey));
- if (matchedKeys.length) return matchedKeys[0];
- }
- return fieldKey;
- }
- @observable protected _isEditing: boolean = false;
- protected _focusRef = React.createRef<HTMLDivElement>();
- protected _rowDoc = this.props.rowProps.original;
- // Gets the serialized data in proto form of the base proto that this document's proto inherits from
- protected _rowDataDoc = Doc.GetProto(this.props.rowProps.original);
- // methods for dragging and dropping
- protected _dropDisposer?: DragManager.DragDropDisposer;
- @observable contents: string = "";
-
- componentDidMount() { document.addEventListener("keydown", this.onKeyDown); }
- componentWillUnmount() { document.removeEventListener("keydown", this.onKeyDown); }
-
- @action
- onKeyDown = (e: KeyboardEvent): void => {
- // If a cell is editable and clicked, hitting enter shoudl allow the user to edit it
- if (this.props.isFocused && this.props.isEditable && e.keyCode === KeyCodes.ENTER) {
- document.removeEventListener("keydown", this.onKeyDown);
- this._isEditing = true;
- this.props.setIsEditing(true);
- }
- }
-
- @action
- isEditingCallback = (isEditing: boolean): void => {
- // a general method that takes a boolean that determines whether the cell should be in
- // is-editing mode
- // remove the event listener if it's there
- document.removeEventListener("keydown", this.onKeyDown);
- // it's not already in is-editing mode, re-add the event listener
- isEditing && document.addEventListener("keydown", this.onKeyDown);
- this._isEditing = isEditing;
- this.props.setIsEditing(isEditing);
- this.props.changeFocusedCellByIndex(this.props.row, this.props.col);
- }
-
- @action
- onPointerDown = async (e: React.PointerEvent): Promise<void> => {
- // pan to the cell
- this.onItemDown(e);
- // focus on it
- this.props.changeFocusedCellByIndex(this.props.row, this.props.col);
- this.props.setPreviewDoc(this.props.rowProps.original);
-
- let url: string;
- if (url = StrCast(this.props.rowProps.row.href)) {
- // opens up the the doc in a new window, blurring the old one
- try {
- new URL(url);
- const temp = window.open(url)!;
- temp.blur();
- window.focus();
- } catch { }
- }
-
- const doc = Cast(this._rowDoc[this.renderFieldKey], Doc, null);
- doc && this.props.setPreviewDoc(doc);
- }
-
- @undoBatch
- applyToDoc = (doc: Doc, row: number, col: number, run: (args?: { [name: string]: any }) => any) => {
- // apply a specified change to the cell
- const res = run({ this: doc, $r: row, $c: col, $: (r: number = 0, c: number = 0) => this.props.getField(r + row, c + col) });
- if (!res.success) return false;
- // change what is rendered to this new changed cell content
- doc[this.renderFieldKey] = res.result;
- return true;
- // return whether the change was successful
- }
-
- private drop = (e: Event, de: DragManager.DropEvent) => {
- // if the drag has data at its completion
- if (de.complete.docDragData) {
- // if only one doc was dragged
- if (de.complete.docDragData.draggedDocuments.length === 1) {
- // update the renderFieldKey
- this._rowDataDoc[this.renderFieldKey] = de.complete.docDragData.draggedDocuments[0];
- }
- else {
- // create schema document reflecting the new column arrangement
- const coll = Docs.Create.SchemaDocument([new SchemaHeaderField("title", "#f1efeb")], de.complete.docDragData.draggedDocuments, {});
- this._rowDataDoc[this.renderFieldKey] = coll;
- }
- e.stopPropagation();
- }
- }
-
- protected dropRef = (ele: HTMLElement | null) => {
- // if the drop disposer is not undefined, run its function
- this._dropDisposer?.();
- // if ele is not null, give ele a non-undefined drop disposer
- ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this)));
- }
-
- returnHighlights(contents: string, positions?: number[]) {
- if (positions) {
- const results = [];
- StrCast(this.props.Document._searchString);
- const length = StrCast(this.props.Document._searchString).length;
- const color = contents ? "black" : "grey";
-
- results.push(<span key="-1" style={{ color }}>{contents?.slice(0, positions[0])}</span>);
- positions.forEach((num, cur) => {
- results.push(<span key={"start" + cur} style={{ backgroundColor: "#FFFF00", color }}>{contents?.slice(num, num + length)}</span>);
- let end = 0;
- cur === positions.length - 1 ? end = contents.length : end = positions[cur + 1];
- results.push(<span key={"end" + cur} style={{ color }}>{contents?.slice(num + length, end)}</span>);
- }
- );
- return results;
- }
- return <span style={{ color: contents ? "black" : "grey" }}>{contents ? contents?.valueOf() : "undefined"}</span>;
- }
-
- @computed get renderFieldKey() {
- // gets the resolved field key of this cell
- return CollectionSchemaCell.resolvedFieldKey(this.props.rowProps.column.id!, this.props.rowProps.original);
- }
-
- onItemDown = async (e: React.PointerEvent) => {
- // if the document is a document used to change UI for search results in schema view
- if (this.props.Document._searchDoc) {
- const aliasdoc = await SearchUtil.GetAliasesOfDocument(this._rowDataDoc);
- const targetContext = aliasdoc.length <= 0 ? undefined : Cast(aliasdoc[0].context, Doc, null);
- // Jump to the this document
- DocumentManager.Instance.jumpToDocument(this._rowDoc, false, emptyFunction, targetContext ? [targetContext] : [],
- undefined, undefined, undefined, () => this.props.setPreviewDoc(this._rowDoc));
- }
- }
-
- renderCellWithType(type: string | undefined) {
- const dragRef: React.RefObject<HTMLDivElement> = React.createRef();
-
- // the column
- const fieldKey = this.renderFieldKey;
- // the exact cell
- const field = this._rowDoc[fieldKey];
-
- const onPointerEnter = (e: React.PointerEvent): void => {
- // e.buttons === 1 means the left moue pointer is down
- if (e.buttons === 1 && SnappingManager.GetIsDragging() && (type === "document" || type === undefined)) {
- dragRef.current!.className = "collectionSchemaView-cellContainer doc-drag-over";
- }
- };
- const onPointerLeave = (e: React.PointerEvent): void => {
- // change the class name to indicate that the cell is no longer being dragged
- dragRef.current!.className = "collectionSchemaView-cellContainer";
- };
-
- let contents = Field.toString(field as Field);
- // display 2 hyphens instead of a blank box for empty cells
- contents = contents === "" ? "--" : contents;
-
- // classname reflects the tatus of the cell
- let className = "collectionSchemaView-cellWrapper";
- if (this._isEditing) className += " editing";
- if (this.props.isFocused && this.props.isEditable) className += " focused";
- if (this.props.isFocused && !this.props.isEditable) className += " inactive";
-
- const positions = [];
- if (StrCast(this.props.Document._searchString).toLowerCase() !== "") {
- // term is ...promise pending... if the field is a Promise, otherwise it is the cell's contents
- let term = (field instanceof Promise) ? "...promise pending..." : contents.toLowerCase();
- const search = StrCast(this.props.Document._searchString).toLowerCase();
- let start = term.indexOf(search);
- let tally = 0;
- // if search is found in term
- if (start !== -1) {
- positions.push(start);
- }
- // if search is found in term, continue finding all instances of search in term
- while (start < contents?.length && start !== -1) {
- term = term.slice(start + search.length + 1);
- tally += start + search.length + 1;
- start = term.indexOf(search);
- positions.push(tally + start);
- }
- // remove the last position
- if (positions.length > 1) {
- positions.pop();
- }
- }
- const placeholder = type === "number" ? "0" : contents === "" ? "--" : "undefined";
- return (
- <div className="collectionSchemaView-cellContainer" style={{ cursor: field instanceof Doc ? "grab" : "auto" }}
- ref={dragRef} onPointerDown={this.onPointerDown} onClick={action(e => this._isEditing = true)} onPointerEnter={onPointerEnter} onPointerLeave={onPointerLeave}>
- <div className={className} ref={this._focusRef} tabIndex={-1}>
- <div className="collectionSchemaView-cellContents" ref={type === undefined || type === "document" ? this.dropRef : null}>
- {!this.props.Document._searchDoc ?
- <EditableView
- editing={this._isEditing}
- isEditingCallback={this.isEditingCallback}
- display={"inline"}
- contents={contents}
- height={"auto"}
- maxHeight={Number(MAX_ROW_HEIGHT)}
- placeholder={placeholder}
- GetValue={() => {
- const cfield = ComputedField.WithoutComputed(() => FieldValue(field));
- const cscript = cfield instanceof ComputedField ? cfield.script.originalScript : undefined;
- const cfinalScript = cscript?.split("return")[cscript.split("return").length - 1];
- return cscript ? (cfinalScript?.endsWith(";") ? `:=${cfinalScript?.substring(0, cfinalScript.length - 2)}` : cfinalScript) :
- Field.IsField(cfield) ? Field.toScriptString(cfield) : "";
- }}
- SetValue={action((value: string) => {
- // sets what is displayed after the user makes an input
- let retVal = false;
- if (value.startsWith(":=") || value.startsWith("=:=")) {
- // decides how to compute a value when given either of the above strings
- const script = value.substring(value.startsWith("=:=") ? 3 : 2);
- retVal = this.props.setComputed(script, value.startsWith(":=") ? this._rowDataDoc : this._rowDoc, this.renderFieldKey, this.props.row, this.props.col);
- } else {
- // check if the input is a number
- let inputIsNum = true;
- for (const s of value) {
- if (isNaN(parseInt(s)) && !(s === ".") && !(s === ",")) {
- inputIsNum = false;
- }
- }
- // check if the input is a boolean
- const inputIsBool: boolean = value === "false" || value === "true";
- // what to do in the case
- if (!inputIsNum && !inputIsBool && !value.startsWith("=")) {
- // if it's not a number, it's a string, and should be processed as such
- // strips the string of quotes when it is edited to prevent quotes form being added to the text automatically
- // after each edit
- let valueSansQuotes = value;
- if (this._isEditing) {
- const vsqLength = valueSansQuotes.length;
- // get rid of outer quotes
- valueSansQuotes = valueSansQuotes.substring(value.startsWith("\"") ? 1 : 0,
- valueSansQuotes.charAt(vsqLength - 1) === "\"" ? vsqLength - 1 : vsqLength);
- }
- let inputAsString = '"';
- // escape any quotes in the string
- for (const i of valueSansQuotes) {
- if (i === '"') {
- inputAsString += '\\"';
- } else {
- inputAsString += i;
- }
- }
- // add a closing quote
- inputAsString += '"';
- //two options here: we can strip off outer quotes or we can figure out what's going on with the script
- const script = CompileScript(inputAsString, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } });
- const changeMade = inputAsString.length !== value.length || inputAsString.length - 2 !== value.length;
- // change it if a change is made, otherwise, just compile using the old cell conetnts
- script.compiled && (retVal = this.applyToDoc(changeMade ? this._rowDoc : this._rowDataDoc, this.props.row, this.props.col, script.run));
- // handle numbers and expressions
- } else if (inputIsNum || value.startsWith("=")) {
- //TODO: make accept numbers
- const inputscript = value.substring(value.startsWith("=") ? 1 : 0);
- // if commas are not stripped, the parser only considers the numbers after the last comma
- let inputSansCommas = "";
- for (const s of inputscript) {
- if (!(s === ",")) {
- inputSansCommas += s;
- }
- }
- const script = CompileScript(inputSansCommas, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } });
- const changeMade = value.length - 2 !== value.length;
- script.compiled && (retVal = this.applyToDoc(changeMade ? this._rowDoc : this._rowDataDoc, this.props.row, this.props.col, script.run));
- // handle booleans
- } else if (inputIsBool) {
- const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } });
- const changeMade = value.length - 2 !== value.length;
- script.compiled && (retVal = this.applyToDoc(changeMade ? this._rowDoc : this._rowDataDoc, this.props.row, this.props.col, script.run));
- }
- }
- if (retVal) {
- this._isEditing = false; // need to set this here. otherwise, the assignment of the field will invalidate & cause render() to be called with the wrong value for 'editing'
- this.props.setIsEditing(false);
- }
- return retVal;
- })}
- OnFillDown={async (value: string) => {
- // computes all of the value preceded by :=
- const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } });
- script.compiled && DocListCast(this.props.Document[this.props.fieldKey]).
- forEach((doc, i) => value.startsWith(":=") ?
- this.props.setComputed(value.substring(2), Doc.GetProto(doc), this.renderFieldKey, i, this.props.col) :
- this.applyToDoc(Doc.GetProto(doc), i, this.props.col, script.run));
- }}
- />
- :
- this.returnHighlights(contents, positions)
- }
- </div >
- </div>
- </div>
- );
- }
-
- render() { return this.renderCellWithType(undefined); }
-}
-
-@observer
-export class CollectionSchemaNumberCell extends CollectionSchemaCell { render() { return this.renderCellWithType("number"); } }
-
-@observer
-export class CollectionSchemaBooleanCell extends CollectionSchemaCell { render() { return this.renderCellWithType("boolean"); } }
-
-@observer
-export class CollectionSchemaStringCell extends CollectionSchemaCell { render() { return this.renderCellWithType("string"); } }
-
-@observer
-export class CollectionSchemaDateCell extends CollectionSchemaCell {
- @computed get _date(): Opt<DateField> {
- // if the cell is a date field, cast then contents to a date. Otherrwwise, make the contents undefined.
- return this._rowDoc[this.renderFieldKey] instanceof DateField ? DateCast(this._rowDoc[this.renderFieldKey]) : undefined;
- }
-
- @action
- handleChange = (date: any) => {
- // const script = CompileScript(date.toString(), { requiredType: "Date", addReturn: true, params: { this: Doc.name } });
- // if (script.compiled) {
- // this.applyToDoc(this._document, this.props.row, this.props.col, script.run);
- // } else {
- // ^ DateCast is always undefined for some reason, but that is what the field should be set to
- this._rowDoc[this.renderFieldKey] = new DateField(date as Date);
- //}
- }
-
- render() {
- return !this.props.isFocused ? <span onPointerDown={this.onPointerDown}>{this._date ? Field.toString(this._date as Field) : "--"}</span> :
- <DatePicker
- selected={this._date?.date || new Date}
- onSelect={date => this.handleChange(date)}
- onChange={date => this.handleChange(date)}
- />;
- }
-}
-
-@observer
-export class CollectionSchemaDocCell extends CollectionSchemaCell {
-
- _overlayDisposer?: () => void;
-
- @computed get _doc() { return FieldValue(Cast(this._rowDoc[this.renderFieldKey], Doc)); }
-
- @action
- onSetValue = (value: string) => {
- this._doc && (Doc.GetProto(this._doc).title = value);
-
- const script = CompileScript(value, {
- addReturn: true,
- typecheck: true,
- transformer: DocumentIconContainer.getTransformer()
- });
- // compile the script
- const results = script.compiled && script.run();
- // if the script was compiled and run
- if (results && results.success) {
- this._rowDoc[this.renderFieldKey] = results.result;
- return true;
- }
- return false;
- }
-
- componentWillUnmount() { this.onBlur(); }
-
- onBlur = () => { this._overlayDisposer?.(); };
- onFocus = () => {
- this.onBlur();
- this._overlayDisposer = OverlayView.Instance.addElement(<DocumentIconContainer />, { x: 0, y: 0 });
- }
-
- @action
- isEditingCallback = (isEditing: boolean): void => {
- // the isEditingCallback from a general CollectionSchemaCell
- document.removeEventListener("keydown", this.onKeyDown);
- isEditing && document.addEventListener("keydown", this.onKeyDown);
- this._isEditing = isEditing;
- this.props.setIsEditing(isEditing);
- this.props.changeFocusedCellByIndex(this.props.row, this.props.col);
- }
-
- render() {
- // if there's a doc, render it
- return !this._doc ? this.renderCellWithType("document") :
- <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1}
- onPointerDown={this.onPointerDown}
- >
- <div className="collectionSchemaView-cellContents-document"
- style={{ padding: "5.9px" }}
- ref={this.dropRef}
- onFocus={this.onFocus}
- onBlur={this.onBlur}
- >
- <EditableView
- editing={this._isEditing}
- isEditingCallback={this.isEditingCallback}
- display={"inline"}
- contents={this._doc.title || "--"}
- height={"auto"}
- maxHeight={Number(MAX_ROW_HEIGHT)}
- GetValue={() => StrCast(this._doc?.title)}
- SetValue={action((value: string) => {
- this.onSetValue(value);
- return true;
- })}
- />
- </div >
- <div onClick={() => this._doc && this.props.addDocTab(this._doc, "add:right")} className="collectionSchemaView-cellContents-docButton">
- <FontAwesomeIcon icon="external-link-alt" size="lg" />
- </div>
- </div>;
- }
-}
-
-@observer
-export class CollectionSchemaImageCell extends CollectionSchemaCell {
-
- choosePath(url: URL) {
- if (url.protocol === "data") return url.href; // if the url ises the data protocol, just return the href
- if (url.href.indexOf(window.location.origin) === -1) return Utils.CorsProxy(url.href); // otherwise, put it through the cors proxy erver
- if (!/\.(png|jpg|jpeg|gif|webp)$/.test(url.href.toLowerCase())) return url.href;//Why is this here — good question
-
- const ext = extname(url.href);
- return url.href.replace(ext, "_o" + ext);
- }
-
- render() {
- const field = Cast(this._rowDoc[this.renderFieldKey], ImageField, null); // retrieve the primary image URL that is being rendered from the data doc
- const alts = DocListCast(this._rowDoc[this.renderFieldKey + "-alternates"]); // retrieve alternate documents that may be rendered as alternate images
- const altpaths = alts.map(doc => Cast(doc[Doc.LayoutFieldKey(doc)], ImageField, null)?.url).filter(url => url).map(url => this.choosePath(url)); // access the primary layout data of the alternate documents
- const paths = field ? [this.choosePath(field.url), ...altpaths] : altpaths;
- // If there is a path, follow it; otherwise, follow a link to a default image icon
- const url = paths.length ? paths : [Utils.CorsProxy("http://www.cs.brown.edu/~bcz/noImage.png")];
-
- const aspect = Doc.NativeAspect(this._rowDoc); // aspect ratio
- let width = Math.min(75, this.props.rowProps.width); // get a with that is no smaller than 75px
- const height = Math.min(75, width / aspect); // get a height either proportional to that or 75 px
- width = height * aspect; // increase the width of the image if necessary to maintain proportionality
-
- const reference = React.createRef<HTMLDivElement>();
- return <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1} onPointerDown={this.onPointerDown}>
- <div className="collectionSchemaView-cellContents" key={this._rowDoc[Id]} ref={reference}>
- <img src={url[0]}
- width={paths.length ? width : "20px"}
- height={paths.length ? height : "20px"} />
- </div >
- </div>;
- }
-}
-
-
-@observer
-export class CollectionSchemaListCell extends CollectionSchemaCell {
- _overlayDisposer?: () => void;
-
- @computed get _field() { return this._rowDoc[this.renderFieldKey]; }
- @computed get _optionsList() { return this._field as List<any>; }
- @observable private _opened = false; // whether the list is opened
- @observable private _text = "select an item";
- @observable private _selectedNum = 0; // the index of the list item selected
-
- @action
- onSetValue = (value: string) => {
- // change if it's a document
- this._optionsList[this._selectedNum] = this._text = value;
-
- (this._field as List<any>).splice(this._selectedNum, 1, value);
- }
-
- @action
- onSelected = (element: string, index: number) => {
- // if an item is selected, the private variables should update to reflect this
- this._text = element;
- this._selectedNum = index;
- }
-
- onFocus = () => {
- this._overlayDisposer?.();
- this._overlayDisposer = OverlayView.Instance.addElement(<DocumentIconContainer />, { x: 0, y: 0 });
- }
-
- render() {
- const link = false;
- const reference = React.createRef<HTMLDivElement>();
-
- // if the list is not opened, don't display it; otherwise, do.
- if (this._optionsList?.length) {
- const options = !this._opened ? (null) :
- <div>
- {this._optionsList.map((element, index) => {
- const val = Field.toString(element);
- return <div className="collectionSchemaView-dropdownOption" key={index} style={{ padding: "6px" }} onPointerDown={(e) => this.onSelected(StrCast(element), index)} >
- {val}
- </div>;
- })}
- </div>;
-
- const plainText = <div style={{ padding: "5.9px" }}>{this._text}</div>;
- const textarea = <div className="collectionSchemaView-cellContents" key={this._rowDoc[Id]} style={{ padding: "5.9px" }} ref={this.dropRef} >
- <EditableView
- editing={this._isEditing}
- isEditingCallback={this.isEditingCallback}
- display={"inline"}
- contents={this._text}
- height={"auto"}
- maxHeight={Number(MAX_ROW_HEIGHT)}
- GetValue={() => this._text}
- SetValue={action((value: string) => {
- // add special for params
- this.onSetValue(value);
- return true;
- })}
- />
- </div >;
-
- //☰
- return (
- <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1} onPointerDown={this.onPointerDown}>
- <div className="collectionSchemaView-cellContents" key={this._rowDoc[Id]} ref={reference}>
- <div className="collectionSchemaView-dropDownWrapper">
- <button type="button" className="collectionSchemaView-dropdownButton" style={{ right: "length", position: "relative" }}
- onClick={action(e => this._opened = !this._opened)} >
- <FontAwesomeIcon icon={this._opened ? "caret-up" : "caret-down"} size="sm" />
- </button>
- <div className="collectionSchemaView-dropdownText"> {link ? plainText : textarea} </div>
- </div>
- {options}
- </div >
- </div>
- );
- }
- return this.renderCellWithType("list");
- }
-}
-
-
-@observer
-export class CollectionSchemaCheckboxCell extends CollectionSchemaCell {
- @computed get _isChecked() { return BoolCast(this._rowDoc[this.renderFieldKey]); }
-
- render() {
- const reference = React.createRef<HTMLDivElement>();
- return (
- <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1} onPointerDown={this.onPointerDown}>
- <input type="checkbox" checked={this._isChecked} onChange={e => this._rowDoc[this.renderFieldKey] = e.target.checked} />
- </div>
- );
- }
-}
-
-
-@observer
-export class CollectionSchemaButtons extends CollectionSchemaCell {
- // the navigation buttons for schema view when it is used for search.
- render() {
- return !this.props.Document._searchDoc || ![DocumentType.PDF, DocumentType.RTF].includes(StrCast(this._rowDoc.type) as DocumentType) ? <></> :
- <div style={{ paddingTop: 8, paddingLeft: 3 }} >
- <button style={{ padding: 2, left: 77 }} onClick={() => Doc.SearchMatchNext(this._rowDoc, true)}>
- <FontAwesomeIcon icon="arrow-up" size="sm" />
- </button>
- <button style={{ padding: 2 }} onClick={() => Doc.SearchMatchNext(this._rowDoc, false)} >
- <FontAwesomeIcon icon="arrow-down" size="sm" />
- </button>
- </div>;
- }
-}
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx
deleted file mode 100644
index 9653f2808..000000000
--- a/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx
+++ /dev/null
@@ -1,513 +0,0 @@
-import React = require("react");
-import { IconProp } from "@fortawesome/fontawesome-svg-core";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, computed, observable, runInAction, trace } from "mobx";
-import { observer } from "mobx-react";
-import { Doc, DocListCast, Opt, StrListCast } from "../../../../fields/Doc";
-import { listSpec } from "../../../../fields/Schema";
-import { PastelSchemaPalette, SchemaHeaderField } from "../../../../fields/SchemaHeaderField";
-import { ScriptField } from "../../../../fields/ScriptField";
-import { Cast, StrCast } from "../../../../fields/Types";
-import { undoBatch } from "../../../util/UndoManager";
-import { CollectionView } from "../CollectionView";
-import { ColumnType } from "./CollectionSchemaView";
-import "./CollectionSchemaView.scss";
-
-const higflyout = require("@hig/flyout");
-export const { anchorPoints } = higflyout;
-export const Flyout = higflyout.default;
-
-
-export interface AddColumnHeaderProps {
- createColumn: () => void;
-}
-
-@observer
-export class CollectionSchemaAddColumnHeader extends React.Component<AddColumnHeaderProps> {
- // the button that allows the user to add a column
- render() {
- return <button className="add-column" onClick={() => this.props.createColumn()}>
- <FontAwesomeIcon icon="plus" size="sm" />
- </button>;
- }
-}
-
-export interface ColumnMenuProps {
- columnField: SchemaHeaderField;
- // keyValue: string;
- possibleKeys: string[];
- existingKeys: string[];
- // keyType: ColumnType;
- typeConst: boolean;
- menuButtonContent: JSX.Element;
- addNew: boolean;
- onSelect: (oldKey: string, newKey: string, addnew: boolean) => void;
- setIsEditing: (isEditing: boolean) => void;
- deleteColumn: (column: string) => void;
- onlyShowOptions: boolean;
- setColumnType: (column: SchemaHeaderField, type: ColumnType) => void;
- setColumnSort: (column: SchemaHeaderField, desc: boolean | undefined) => void;
- anchorPoint?: any;
- setColumnColor: (column: SchemaHeaderField, color: string) => void;
-}
-@observer
-export class CollectionSchemaColumnMenu extends React.Component<ColumnMenuProps> {
- @observable private _isOpen: boolean = false;
- @observable private _node: HTMLDivElement | null = null;
-
- componentDidMount() { document.addEventListener("pointerdown", this.detectClick); }
-
- componentWillUnmount() { document.removeEventListener("pointerdown", this.detectClick); }
-
- @action
- detectClick = (e: PointerEvent) => {
- !this._node?.contains(e.target as Node) && this.props.setIsEditing(this._isOpen = false);
- }
-
- @action
- toggleIsOpen = (): void => {
- this.props.setIsEditing(this._isOpen = !this._isOpen);
- }
-
- changeColumnType = (type: ColumnType) => {
- this.props.setColumnType(this.props.columnField, type);
- }
-
- changeColumnSort = (desc: boolean | undefined) => {
- this.props.setColumnSort(this.props.columnField, desc);
- }
-
- changeColumnColor = (color: string) => {
- this.props.setColumnColor(this.props.columnField, color);
- }
-
- @action
- setNode = (node: HTMLDivElement): void => {
- if (node) {
- this._node = node;
- }
- }
-
- renderTypes = () => {
- if (this.props.typeConst) return (null);
-
- const type = this.props.columnField.type;
- return (
- <div className="collectionSchema-headerMenu-group">
- <label>Column type:</label>
- <div className="columnMenu-types">
- <div className={"columnMenu-option" + (type === ColumnType.Any ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.Any)}>
- <FontAwesomeIcon icon={"align-justify"} size="sm" />
- Any
- </div>
- <div className={"columnMenu-option" + (type === ColumnType.Number ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.Number)}>
- <FontAwesomeIcon icon={"hashtag"} size="sm" />
- Number
- </div>
- <div className={"columnMenu-option" + (type === ColumnType.String ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.String)}>
- <FontAwesomeIcon icon={"font"} size="sm" />
- Text
- </div>
- <div className={"columnMenu-option" + (type === ColumnType.Boolean ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.Boolean)}>
- <FontAwesomeIcon icon={"check-square"} size="sm" />
- Checkbox
- </div>
- <div className={"columnMenu-option" + (type === ColumnType.List ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.List)}>
- <FontAwesomeIcon icon={"list-ul"} size="sm" />
- List
- </div>
- <div className={"columnMenu-option" + (type === ColumnType.Doc ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.Doc)}>
- <FontAwesomeIcon icon={"file"} size="sm" />
- Document
- </div>
- <div className={"columnMenu-option" + (type === ColumnType.Image ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.Image)}>
- <FontAwesomeIcon icon={"image"} size="sm" />
- Image
- </div>
- <div className={"columnMenu-option" + (type === ColumnType.Date ? " active" : "")} onClick={() => this.changeColumnType(ColumnType.Date)}>
- <FontAwesomeIcon icon={"calendar"} size="sm" />
- Date
- </div>
- </div>
- </div >
- );
- }
-
- renderSorting = () => {
- const sort = this.props.columnField.desc;
- return (
- <div className="collectionSchema-headerMenu-group">
- <label>Sort by:</label>
- <div className="columnMenu-sort">
- <div className={"columnMenu-option" + (sort === true ? " active" : "")} onClick={() => this.changeColumnSort(true)}>
- <FontAwesomeIcon icon="sort-amount-down" size="sm" />
- Sort descending
- </div>
- <div className={"columnMenu-option" + (sort === false ? " active" : "")} onClick={() => this.changeColumnSort(false)}>
- <FontAwesomeIcon icon="sort-amount-up" size="sm" />
- Sort ascending
- </div>
- <div className="columnMenu-option" onClick={() => this.changeColumnSort(undefined)}>
- <FontAwesomeIcon icon="times" size="sm" />
- Clear sorting
- </div>
- </div>
- </div>
- );
- }
-
- renderColors = () => {
- const selected = this.props.columnField.color;
-
- const pink = PastelSchemaPalette.get("pink2");
- const purple = PastelSchemaPalette.get("purple2");
- const blue = PastelSchemaPalette.get("bluegreen1");
- const yellow = PastelSchemaPalette.get("yellow4");
- const red = PastelSchemaPalette.get("red2");
- const gray = "#f1efeb";
-
- return (
- <div className="collectionSchema-headerMenu-group">
- <label>Color:</label>
- <div className="columnMenu-colors">
- <div className={"columnMenu-colorPicker" + (selected === pink ? " active" : "")} style={{ backgroundColor: pink }} onClick={() => this.changeColumnColor(pink!)}></div>
- <div className={"columnMenu-colorPicker" + (selected === purple ? " active" : "")} style={{ backgroundColor: purple }} onClick={() => this.changeColumnColor(purple!)}></div>
- <div className={"columnMenu-colorPicker" + (selected === blue ? " active" : "")} style={{ backgroundColor: blue }} onClick={() => this.changeColumnColor(blue!)}></div>
- <div className={"columnMenu-colorPicker" + (selected === yellow ? " active" : "")} style={{ backgroundColor: yellow }} onClick={() => this.changeColumnColor(yellow!)}></div>
- <div className={"columnMenu-colorPicker" + (selected === red ? " active" : "")} style={{ backgroundColor: red }} onClick={() => this.changeColumnColor(red!)}></div>
- <div className={"columnMenu-colorPicker" + (selected === gray ? " active" : "")} style={{ backgroundColor: gray }} onClick={() => this.changeColumnColor(gray)}></div>
- </div>
- </div>
- );
- }
-
- renderContent = () => {
- return (
- <div className="collectionSchema-header-menuOptions">
- {this.props.onlyShowOptions ? <></> :
- <>
- {this.renderTypes()}
- {this.renderSorting()}
- {this.renderColors()}
- <div className="collectionSchema-headerMenu-group">
- <button onClick={() => this.props.deleteColumn(this.props.columnField.heading)}>Hide Column</button>
- </div>
- </>
- }
- </div>
- );
- }
-
- render() {
- return (
- <div className="collectionSchema-header-menu" ref={this.setNode}>
- <Flyout anchorPoint={this.props.anchorPoint ? this.props.anchorPoint : anchorPoints.TOP_CENTER} content={this.renderContent()}>
- <div className="collectionSchema-header-toggler" onClick={() => this.toggleIsOpen()}>{this.props.menuButtonContent}</div>
- </ Flyout >
- </div>
- );
- }
-}
-
-
-export interface KeysDropdownProps {
- keyValue: string;
- possibleKeys: string[];
- existingKeys: string[];
- canAddNew: boolean;
- addNew: boolean;
- onSelect: (oldKey: string, newKey: string, addnew: boolean, filter?: string) => void;
- setIsEditing: (isEditing: boolean) => void;
- width?: string;
- docs?: Doc[];
- Document: Doc;
- dataDoc: Doc | undefined;
- fieldKey: string;
- ContainingCollectionDoc: Doc | undefined;
- ContainingCollectionView: Opt<CollectionView>;
- active?: (outsideReaction?: boolean) => boolean | undefined;
- openHeader: (column: any, screenx: number, screeny: number) => void;
- col: SchemaHeaderField;
- icon: IconProp;
-}
-@observer
-export class KeysDropdown extends React.Component<KeysDropdownProps> {
- @observable private _key: string = this.props.keyValue;
- @observable private _searchTerm: string = this.props.keyValue + ":";
- @observable private _isOpen: boolean = false;
- @observable private _node: HTMLDivElement | null = null;
- @observable private _inputRef: React.RefObject<HTMLInputElement> = React.createRef();
-
- @action setSearchTerm = (value: string): void => { this._searchTerm = value; };
- @action setKey = (key: string): void => { this._key = key; };
- @action setIsOpen = (isOpen: boolean): void => { this._isOpen = isOpen; };
-
- @action
- onSelect = (key: string): void => {
- this.props.onSelect(this._key, key, this.props.addNew);
- this.setKey(key);
- this._isOpen = false;
- this.props.setIsEditing(false);
- }
-
- @action
- setNode = (node: HTMLDivElement): void => {
- if (node) {
- this._node = node;
- }
- }
-
- componentDidMount() {
- document.addEventListener("pointerdown", this.detectClick);
- const filters = Cast(this.props.Document._docFilters, listSpec("string"));
- if (filters?.some(filter => filter.split(":")[0] === this._key)) {
- runInAction(() => this.closeResultsVisibility = "contents");
- }
- }
-
- @action
- detectClick = (e: PointerEvent): void => {
- if (this._node && this._node.contains(e.target as Node)) {
- } else {
- this._isOpen = false;
- this.props.setIsEditing(false);
- }
- }
-
- private tempfilter: string = "";
- @undoBatch
- onKeyDown = (e: React.KeyboardEvent): void => {
- if (e.key === "Enter") {
- e.stopPropagation();
- if (this._searchTerm.includes(":")) {
- const colpos = this._searchTerm.indexOf(":");
- const temp = this._searchTerm.slice(colpos + 1, this._searchTerm.length);
- if (temp === "") {
- Doc.setDocFilter(this.props.Document, this._key, this.tempfilter, "remove");
- this.updateFilter();
- }
- else {
- Doc.setDocFilter(this.props.Document, this._key, this.tempfilter, "remove");
- this.tempfilter = temp;
- Doc.setDocFilter(this.props.Document, this._key, temp, "check");
- this.props.col.setColor("green");
- this.closeResultsVisibility = "contents";
- }
- }
- else {
- Doc.setDocFilter(this.props.Document, this._key, this.tempfilter, "remove");
- this.updateFilter();
- if (this.showKeys.length) {
- this.onSelect(this.showKeys[0]);
- } else if (this._searchTerm !== "" && this.props.canAddNew) {
- this.setSearchTerm(this._searchTerm || this._key);
- this.onSelect(this._searchTerm);
- }
- }
- }
- }
-
- onChange = (val: string): void => {
- this.setSearchTerm(val);
- }
-
- @action
- onFocus = (e: React.FocusEvent): void => {
- this._isOpen = true;
- this.props.setIsEditing(true);
- }
-
- @computed get showKeys() {
- const whitelistKeys = ["context", "author", "*lastModified", "text", "data", "tags", "creationDate"];
- const keyOptions = this._searchTerm === "" ? this.props.possibleKeys : this.props.possibleKeys.filter(key => key.toUpperCase().indexOf(this._searchTerm.toUpperCase()) > -1);
- const showKeys = new Set<string>();
- [...keyOptions, ...whitelistKeys].forEach(key => (!Doc.noviceMode ||
- whitelistKeys.includes(key)
- || ((!key.startsWith("_") && key[0] === key[0].toUpperCase()) || key[0] === "#")) ? showKeys.add(key) : null);
- return Array.from(showKeys.keys()).filter(key => !this._searchTerm || key.includes(this._searchTerm));
- }
-
- @computed get renderOptions() {
- if (!this._isOpen) {
- this.defaultMenuHeight = 0;
- return (null);
- }
- const options = this.showKeys.map(key => {
- return <div key={key} className="key-option" style={{
- border: "1px solid lightgray",
- width: this.props.width, maxWidth: this.props.width, overflowX: "hidden", background: "white",
- }}
- onPointerDown={e => {
- e.stopPropagation();
- }}
- onClick={() => {
- this.onSelect(key);
- this.setSearchTerm("");
- }}>{key}</div>;
- });
-
- // if search term does not already exist as a group type, give option to create new group type
-
- if (this._key !== this._searchTerm.slice(0, this._key.length)) {
- if (this._searchTerm !== "" && this.props.canAddNew) {
- options.push(<div key={""} className="key-option" style={{
- border: "1px solid lightgray", width: this.props.width, maxWidth: this.props.width, overflowX: "hidden", background: "white",
- }}
- onClick={() => { this.onSelect(this._searchTerm); this.setSearchTerm(""); }}>
- Create "{this._searchTerm}" key</div>);
- }
- }
-
- if (options.length === 0) {
- this.defaultMenuHeight = 0;
- }
- else {
- if (this.props.docs) {
- const panesize = this.props.docs.length * 30;
- options.length * 20 + 8 - 10 > panesize ? this.defaultMenuHeight = panesize : this.defaultMenuHeight = options.length * 20 + 8;
- }
- else {
- options.length > 5 ? this.defaultMenuHeight = 108 : this.defaultMenuHeight = options.length * 20 + 8;
- }
- }
- return options;
- }
-
- @computed get docSafe() { return DocListCast(this.props.dataDoc?.[this.props.fieldKey]); }
-
- @computed get renderFilterOptions() {
- if (!this._isOpen || !this.props.dataDoc) {
- this.defaultMenuHeight = 0;
- return (null);
- }
- const keyOptions: string[] = [];
- const colpos = this._searchTerm.indexOf(":");
- const temp = this._searchTerm.slice(colpos + 1, this._searchTerm.length);
- this.docSafe.forEach(doc => {
- const key = StrCast(doc[this._key]);
- if (keyOptions.includes(key) === false && key.includes(temp) && key !== "") {
- keyOptions.push(key);
- }
- });
-
- const filters = StrListCast(this.props.Document._docFilters);
- if (filters.some(filter => filter.split(":")[0] === this._key) === false) {
- this.props.col.setColor("rgb(241, 239, 235)");
- this.closeResultsVisibility = "none";
- }
- for (let i = 0; i < (filters?.length ?? 0) - 1; i++) {
- if (filters[i] === this.props.col.heading && keyOptions.includes(filters[i].split(":")[1]) === false) {
- keyOptions.push(filters[i + 1]);
- }
- }
- const options = keyOptions.map(key => {
- let bool = false;
- if (filters !== undefined) {
- const ind = filters.findIndex(filter => filter.split(":")[1] === key);
- const fields = ind === -1 ? undefined : filters[ind].split(":");
- bool = fields ? fields[2] === "check" : false;
- }
- return <div key={key} className="key-option" style={{
- paddingLeft: 5, textAlign: "left",
- width: this.props.width, maxWidth: this.props.width, overflowX: "hidden", background: "white", backgroundColor: "white",
- }}
- >
- <input type="checkbox"
- onPointerDown={e => e.stopPropagation()}
- onClick={e => e.stopPropagation()}
- onChange={action(e => {
- if (e.target.checked) {
- Doc.setDocFilter(this.props.Document, this._key, key, "check");
- this.closeResultsVisibility = "contents";
- this.props.col.setColor("green");
- } else {
- Doc.setDocFilter(this.props.Document, this._key, key, "remove");
- this.updateFilter();
- }
- })}
- checked={bool}
- />
- <span style={{ paddingLeft: 4 }}>
- {key}
- </span>
-
- </div>;
- });
- if (options.length === 0) {
- this.defaultMenuHeight = 0;
- }
- else {
- if (this.props.docs) {
- const panesize = this.props.docs.length * 30;
- options.length * 20 + 8 - 10 > panesize ? this.defaultMenuHeight = panesize : this.defaultMenuHeight = options.length * 20 + 8;
- }
- else {
- options.length > 5 ? this.defaultMenuHeight = 108 : this.defaultMenuHeight = options.length * 20 + 8;
- }
-
- }
- return options;
- }
-
- @observable defaultMenuHeight = 0;
-
-
- updateFilter() {
- const filters = Cast(this.props.Document._docFilters, listSpec("string"));
- if (filters === undefined || filters.length === 0 || filters.some(filter => filter.split(":")[0] === this._key) === false) {
- this.props.col.setColor("rgb(241, 239, 235)");
- this.closeResultsVisibility = "none";
- }
- }
-
- @computed get scriptField() {
- const scriptText = "setDocFilter(containingTreeView, heading, this.title, checked)";
- const script = ScriptField.MakeScript(scriptText, { this: Doc.name, heading: "string", checked: "string", containingTreeView: Doc.name });
- return script ? () => script : undefined;
- }
- filterBackground = () => "rgba(105, 105, 105, 0.432)";
- @observable filterOpen: boolean | undefined = undefined;
- closeResultsVisibility: string = "none";
-
- removeFilters = (e: React.PointerEvent): void => {
- const keyOptions: string[] = [];
- this.docSafe.forEach(doc => {
- const key = StrCast(doc[this._key]);
- if (keyOptions.includes(key) === false) {
- keyOptions.push(key);
- }
- });
-
- Doc.setDocFilter(this.props.Document, this._key, "", "remove");
- this.props.col.setColor("rgb(241, 239, 235)");
- this.closeResultsVisibility = "none";
- }
- render() {
- return (
- <div style={{ display: "flex", width: '100%', alignContent: 'center', alignItems: 'center' }} ref={this.setNode}>
- <div className="schema-icon" onClick={e => { this.props.openHeader(this.props.col, e.clientX, e.clientY); e.stopPropagation(); }}>
- <FontAwesomeIcon icon={this.props.icon} size="lg" style={{ display: "inline" }} />
- </div>
-
- <div className="keys-dropdown" style={{ zIndex: 1, width: this.props.width, maxWidth: this.props.width }}>
- <input className="keys-search" style={{ width: "100%" }}
- ref={this._inputRef} type="text"
- value={this._searchTerm} placeholder="Column key"
- onKeyDown={this.onKeyDown}
- onChange={e => this.onChange(e.target.value)}
- onClick={(e) => { e.stopPropagation(); this._inputRef.current?.focus(); }}
- onFocus={this.onFocus} ></input>
- <div style={{ display: this.closeResultsVisibility }}>
- <FontAwesomeIcon onPointerDown={this.removeFilters} icon={"times-circle"} size="lg"
- style={{ cursor: "hand", color: "grey", padding: 2, left: -20, top: -1, height: 15, position: "relative" }} />
- </div>
- {!this._isOpen ? (null) : <div className="keys-options-wrapper" style={{
- width: this.props.width, maxWidth: this.props.width, height: "auto",
- }}>
- {this._searchTerm.includes(":") ? this.renderFilterOptions : this.renderOptions}
- </div>}
- </div >
- </div>
- );
- }
-}
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaMovableColumn.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaMovableColumn.tsx
deleted file mode 100644
index 28d2e6ab1..000000000
--- a/src/client/views/collections/collectionSchema/CollectionSchemaMovableColumn.tsx
+++ /dev/null
@@ -1,138 +0,0 @@
-import React = require('react');
-import { action } from 'mobx';
-import { SchemaHeaderField } from '../../../../fields/SchemaHeaderField';
-import { DragManager } from '../../../util/DragManager';
-import { SnappingManager } from '../../../util/SnappingManager';
-import { Transform } from '../../../util/Transform';
-import './CollectionSchemaView.scss';
-
-export interface MovableColumnProps {
- columnRenderer: React.ReactNode;
- columnValue: SchemaHeaderField;
- allColumns: SchemaHeaderField[];
- reorderColumns: (toMove: SchemaHeaderField, relativeTo: SchemaHeaderField, before: boolean, columns: SchemaHeaderField[]) => void;
- ScreenToLocalTransform: () => Transform;
-}
-export class MovableColumn extends React.Component<MovableColumnProps> {
- // The header of the column
- private _header?: React.RefObject<HTMLDivElement> = React.createRef();
- // The container of the function that is responsible for moving the column over to a new plac
- private _colDropDisposer?: DragManager.DragDropDisposer;
- // initial column position
- private _startDragPosition: { x: number; y: number } = { x: 0, y: 0 };
- // sensitivity to being dragged, in pixels
- private _sensitivity: number = 16;
- // Column reference ID
- private _dragRef: React.RefObject<HTMLDivElement> = React.createRef();
-
- onPointerEnter = (e: React.PointerEvent): void => {
- // if the column is left-clicked and it is being dragged
- if (e.buttons === 1 && SnappingManager.GetIsDragging()) {
- this._header!.current!.className = 'collectionSchema-col-wrapper';
- document.addEventListener('pointermove', this.onDragMove, true);
- }
- };
-
- onPointerLeave = (e: React.PointerEvent): void => {
- this._header!.current!.className = 'collectionSchema-col-wrapper';
- document.removeEventListener('pointermove', this.onDragMove, true);
- !e.buttons && document.removeEventListener('pointermove', this.onPointerMove);
- };
-
- onDragMove = (e: PointerEvent): void => {
- // only take into account the horizonal direction when a column is dragged
- const x = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY);
- const rect = this._header!.current!.getBoundingClientRect();
- // Now store the point at the top center of the column when it was in its original position
- const bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left + (rect.right - rect.left) / 2, rect.top);
- // to be compared with its new horizontal position
- const before = x[0] < bounds[0];
- this._header!.current!.className = 'collectionSchema-col-wrapper';
- if (before) this._header!.current!.className += ' col-before';
- if (!before) this._header!.current!.className += ' col-after';
- e.stopPropagation();
- };
-
- createColDropTarget = (ele: HTMLDivElement) => {
- this._colDropDisposer?.();
- if (ele) {
- this._colDropDisposer = DragManager.MakeDropTarget(ele, this.colDrop.bind(this));
- }
- };
-
- colDrop = (e: Event, de: DragManager.DropEvent) => {
- document.removeEventListener('pointermove', this.onDragMove, true);
- // we only care about whether the column is shifted to the side
- const x = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y);
- // get the dimensions of the smallest rectangle that bounds the header
- const rect = this._header!.current!.getBoundingClientRect();
- const bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left + (rect.right - rect.left) / 2, rect.top);
- // get whether the column was dragged before or after where it is now
- const before = x[0] < bounds[0];
- const colDragData = de.complete.columnDragData;
- // if there is colDragData, which happen when the drag is complete, reorder the columns according to the established variables
- if (colDragData) {
- e.stopPropagation();
- this.props.reorderColumns(colDragData.colKey, this.props.columnValue, before, this.props.allColumns);
- return true;
- }
- return false;
- };
-
- onPointerMove = (e: PointerEvent) => {
- const onRowMove = (e: PointerEvent) => {
- e.stopPropagation();
- e.preventDefault();
-
- document.removeEventListener('pointermove', onRowMove);
- document.removeEventListener('pointerup', onRowUp);
- const dragData = new DragManager.ColumnDragData(this.props.columnValue);
- DragManager.StartColumnDrag(this._dragRef.current!, dragData, e.x, e.y);
- };
- const onRowUp = (): void => {
- document.removeEventListener('pointermove', onRowMove);
- document.removeEventListener('pointerup', onRowUp);
- };
- // if the left mouse button is the one being held
- if (e.buttons === 1) {
- const [dx, dy] = this.props.ScreenToLocalTransform().transformDirection(e.clientX - this._startDragPosition.x, e.clientY - this._startDragPosition.y);
- // If the movemnt of the drag exceeds the sensitivity value
- if (Math.abs(dx) + Math.abs(dy) > this._sensitivity) {
- document.removeEventListener('pointermove', this.onPointerMove);
- e.stopPropagation();
-
- document.addEventListener('pointermove', onRowMove);
- document.addEventListener('pointerup', onRowUp);
- }
- }
- };
-
- onPointerUp = (e: React.PointerEvent) => {
- document.removeEventListener('pointermove', this.onPointerMove);
- };
-
- @action
- onPointerDown = (e: React.PointerEvent, ref: React.RefObject<HTMLDivElement>) => {
- this._dragRef = ref;
- const [dx, dy] = this.props.ScreenToLocalTransform().transformDirection(e.clientX, e.clientY);
- // If the cell thing dragged is not being edited
- if (!(e.target as any)?.tagName.includes('INPUT')) {
- this._startDragPosition = { x: dx, y: dy };
- document.addEventListener('pointermove', this.onPointerMove);
- }
- };
-
- render() {
- const reference = React.createRef<HTMLDivElement>();
-
- return (
- <div className="collectionSchema-col" ref={this.createColDropTarget}>
- <div className="collectionSchema-col-wrapper" ref={this._header} onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}>
- <div className="col-dragger" ref={reference} onPointerDown={e => this.onPointerDown(e, reference)} onPointerUp={this.onPointerUp}>
- {this.props.columnRenderer}
- </div>
- </div>
- </div>
- );
- }
-}
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaMovableRow.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaMovableRow.tsx
deleted file mode 100644
index f872637e5..000000000
--- a/src/client/views/collections/collectionSchema/CollectionSchemaMovableRow.tsx
+++ /dev/null
@@ -1,151 +0,0 @@
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action } from 'mobx';
-import * as React from 'react';
-import { ReactTableDefaults, RowInfo } from 'react-table';
-import { Doc } from '../../../../fields/Doc';
-import { Cast, FieldValue, StrCast } from '../../../../fields/Types';
-import { DocumentManager } from '../../../util/DocumentManager';
-import { DragManager, dropActionType, SetupDrag } from '../../../util/DragManager';
-import { SnappingManager } from '../../../util/SnappingManager';
-import { Transform } from '../../../util/Transform';
-import { undoBatch } from '../../../util/UndoManager';
-import { ContextMenu } from '../../ContextMenu';
-import './CollectionSchemaView.scss';
-
-export interface MovableRowProps {
- rowInfo: RowInfo;
- ScreenToLocalTransform: () => Transform;
- addDoc: (doc: Doc | Doc[], relativeTo?: Doc, before?: boolean) => boolean;
- removeDoc: (doc: Doc | Doc[]) => boolean;
- rowFocused: boolean;
- textWrapRow: (doc: Doc) => void;
- rowWrapped: boolean;
- dropAction: string;
- addDocTab: any;
-}
-
-export class MovableRow extends React.Component<React.PropsWithChildren<MovableRowProps>> {
- private _header?: React.RefObject<HTMLDivElement> = React.createRef();
- private _rowDropDisposer?: DragManager.DragDropDisposer;
-
- // Event listeners are only necessary when the user is hovering over the table
- // Create one when the mouse starts hovering...
- onPointerEnter = (e: React.PointerEvent): void => {
- if (e.buttons === 1 && SnappingManager.GetIsDragging()) {
- this._header!.current!.className = 'collectionSchema-row-wrapper';
- document.addEventListener('pointermove', this.onDragMove, true);
- }
- };
- // ... and delete it when the mouse leaves
- onPointerLeave = (e: React.PointerEvent): void => {
- this._header!.current!.className = 'collectionSchema-row-wrapper';
- document.removeEventListener('pointermove', this.onDragMove, true);
- };
- // The method for the event listener, reorders columns when dragged to their new locations.
- onDragMove = (e: PointerEvent): void => {
- const x = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY);
- const rect = this._header!.current!.getBoundingClientRect();
- const bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left, rect.top + rect.height / 2);
- const before = x[1] < bounds[1];
- this._header!.current!.className = 'collectionSchema-row-wrapper';
- if (before) this._header!.current!.className += ' row-above';
- if (!before) this._header!.current!.className += ' row-below';
- e.stopPropagation();
- };
- componentWillUnmount() {
- this._rowDropDisposer?.();
- }
- //
- createRowDropTarget = (ele: HTMLDivElement) => {
- this._rowDropDisposer?.();
- if (ele) {
- this._rowDropDisposer = DragManager.MakeDropTarget(ele, this.rowDrop.bind(this));
- }
- };
- // Controls what hppens when a row is dragged and dropped
- rowDrop = (e: Event, de: DragManager.DropEvent) => {
- this.onPointerLeave(e as any);
- const rowDoc = FieldValue(Cast(this.props.rowInfo.original, Doc));
- if (!rowDoc) return false;
-
- const x = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y);
- const rect = this._header!.current!.getBoundingClientRect();
- const bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left, rect.top + rect.height / 2);
- const before = x[1] < bounds[1];
-
- const docDragData = de.complete.docDragData;
- if (docDragData) {
- e.stopPropagation();
- if (docDragData.draggedDocuments[0] === rowDoc) return true;
- const addDocument = (doc: Doc | Doc[]) => this.props.addDoc(doc, rowDoc, before);
- const movedDocs = docDragData.draggedDocuments;
- return docDragData.dropAction || docDragData.userDropAction
- ? docDragData.droppedDocuments.reduce((added: boolean, d) => this.props.addDoc(d, rowDoc, before) || added, false)
- : docDragData.moveDocument
- ? movedDocs.reduce((added: boolean, d) => docDragData.moveDocument?.(d, rowDoc, addDocument) || added, false)
- : docDragData.droppedDocuments.reduce((added: boolean, d) => this.props.addDoc(d, rowDoc, before), false);
- }
- return false;
- };
-
- onRowContextMenu = (e: React.MouseEvent): void => {
- const description = this.props.rowWrapped ? 'Unwrap text on row' : 'Text wrap row';
- ContextMenu.Instance.addItem({ description: description, event: () => this.props.textWrapRow(this.props.rowInfo.original), icon: 'file-pdf' });
- };
-
- @undoBatch
- @action
- move: DragManager.MoveFunction = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDoc) => {
- const targetView = targetCollection && DocumentManager.Instance.getDocumentView(targetCollection);
- return doc !== targetCollection && doc !== targetView?.props.ContainingCollectionDoc && this.props.removeDoc(doc) && addDoc(doc);
- };
-
- @action
- onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
- console.log('yes');
- if (e.key === 'Backspace' || e.key === 'Delete') {
- undoBatch(() => this.props.removeDoc(this.props.rowInfo.original));
- }
- };
-
- render() {
- const { children = null, rowInfo } = this.props;
-
- if (!rowInfo) {
- return <ReactTableDefaults.TrComponent>{children}</ReactTableDefaults.TrComponent>;
- }
-
- const { original } = rowInfo;
- const doc = FieldValue(Cast(original, Doc));
-
- if (!doc) return null;
-
- const reference = React.createRef<HTMLDivElement>();
- const onItemDown = SetupDrag(reference, () => doc, this.move, StrCast(this.props.dropAction) as dropActionType);
-
- let className = 'collectionSchema-row';
- if (this.props.rowFocused) className += ' row-focused';
- if (this.props.rowWrapped) className += ' row-wrapped';
-
- return (
- <div className={className} onKeyPress={this.onKeyDown} ref={this.createRowDropTarget} onContextMenu={this.onRowContextMenu}>
- <div className="collectionSchema-row-wrapper" onKeyPress={this.onKeyDown} ref={this._header} onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}>
- <ReactTableDefaults.TrComponent onKeyPress={this.onKeyDown}>
- <div className="row-dragger">
- <div className="row-option" onClick={undoBatch(() => this.props.removeDoc(this.props.rowInfo.original))}>
- <FontAwesomeIcon icon="trash" size="sm" />
- </div>
- <div className="row-option" style={{ cursor: 'grab' }} ref={reference} onPointerDown={onItemDown}>
- <FontAwesomeIcon icon="grip-vertical" size="sm" />
- </div>
- <div className="row-option" onClick={() => this.props.addDocTab(this.props.rowInfo.original, 'add:right')}>
- <FontAwesomeIcon icon="external-link-alt" size="sm" />
- </div>
- </div>
- {children}
- </ReactTableDefaults.TrComponent>
- </div>
- </div>
- );
- }
-}
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.scss b/src/client/views/collections/collectionSchema/CollectionSchemaView.scss
index 19401c7f0..52ebb7763 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaView.scss
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.scss
@@ -1,599 +1,239 @@
@import '../../global/globalCssVariables.scss';
-@import '../../../../../node_modules/react-table/react-table.css';
-.collectionSchemaView-container {
- border-width: $COLLECTION_BORDER_WIDTH;
- border-color: $medium-gray;
- border-style: solid;
- border-radius: $border-radius;
- box-sizing: border-box;
- position: relative;
- top: 0;
- width: 100%;
- height: 100%;
- margin-top: 0;
- transition: top 0.5s;
- display: flex;
- justify-content: space-between;
- flex-wrap: nowrap;
- touch-action: none;
- div {
- touch-action: none;
- }
- .collectionSchemaView-tableContainer {
- width: 100%;
- height: 100%;
- }
- .collectionSchemaView-dividerDragger {
- position: relative;
- height: 100%;
- width: $SCHEMA_DIVIDER_WIDTH;
- z-index: 20;
- right: 0;
- top: 0;
- background: gray;
- cursor: col-resize;
- }
- // .documentView-node:first-child {
- // background: $white;
- // }
-}
-.collectionSchemaView-searchContainer {
- border-width: $COLLECTION_BORDER_WIDTH;
- border-color: $medium-gray;
- border-style: solid;
- border-radius: $border-radius;
- box-sizing: border-box;
- position: relative;
- top: 0;
- width: 100%;
+.collectionSchemaView {
+ cursor: default;
height: 100%;
- margin-top: 0;
- transition: top 0.5s;
display: flex;
- justify-content: space-between;
- flex-wrap: nowrap;
- touch-action: none;
- padding: 2px;
- div {
- touch-action: none;
- }
- .collectionSchemaView-tableContainer {
- width: 100%;
- height: 100%;
- }
- .collectionSchemaView-dividerDragger {
- position: relative;
- height: 100%;
- width: 20px;
- z-index: 20;
- right: 0;
- top: 0;
- background: gray;
- cursor: col-resize;
- }
- // .documentView-node:first-child {
- // background: $white;
- // }
-}
+ flex-direction: row;
-.ReactTable {
- width: 100%;
- background: white;
- box-sizing: border-box;
- border: none !important;
- float: none !important;
- .rt-table {
- height: 100%;
- display: -webkit-inline-box;
- direction: ltr;
- overflow: visible;
- }
- .rt-noData {
- display: none;
- }
- .rt-thead {
- width: 100%;
- z-index: 100;
- overflow-y: visible;
- &.-header {
- font-size: 12px;
- height: 30px;
- box-shadow: none;
- z-index: 100;
- overflow-y: visible;
- }
- .rt-resizable-header-content {
- height: 100%;
- overflow: visible;
- }
- .rt-th {
- padding: 0;
- border-left: solid 1px $light-gray;
- }
- }
- .rt-th {
- font-size: 13px;
- text-align: center;
- &:last-child {
- overflow: visible;
- }
- }
- .rt-tbody {
- width: 100%;
- direction: rtl;
- overflow: visible;
- .rt-td {
- border-right: 1px solid rgba(0, 0, 0, 0.2);
- }
- }
- .rt-tr-group {
- direction: ltr;
- flex: 0 1 auto;
- min-height: 30px;
- border: 0 !important;
- }
- .rt-tr-group:nth-of-type(even) {
- direction: ltr;
- flex: 0 1 auto;
- min-height: 30px;
- border: 0 !important;
- background-color: red;
- }
- .rt-tr {
- width: 100%;
- min-height: 30px;
- }
- .rt-td {
- padding: 0;
- font-size: 13px;
- text-align: center;
- white-space: nowrap;
- display: flex;
- align-items: center;
- .imageBox-cont {
- position: relative;
- max-height: 100%;
- }
- .imageBox-cont img {
- object-fit: contain;
- max-width: 100%;
- height: 100%;
- }
- .videoBox-cont {
- object-fit: contain;
- width: auto;
- height: 100%;
- }
- }
- .rt-td.rt-expandable {
- display: flex;
- align-items: center;
- height: inherit;
- }
- .rt-resizer {
- width: 8px;
- right: -4px;
- }
- .rt-resizable-header {
- padding: 0;
- height: 30px;
- }
- .rt-resizable-header:last-child {
- overflow: visible;
- .rt-resizer {
- width: 5px !important;
- }
- }
-}
+ .schema-table {
+ background-color: $white;
+ cursor: grab;
-.documentView-node-topmost {
- text-align: left;
- transform-origin: center top;
- display: inline-block;
-}
+ .schema-table-content {
+ overflow: overlay;
+ scroll-behavior: smooth;
+ }
-.collectionSchema-col {
- height: 100%;
-}
+ .schema-column-menu,
+ .schema-filter-menu {
+ background: $light-gray;
+ position: absolute;
+ min-width: 200px;
+ max-width: 400px;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ z-index: 1;
-.collectionSchema-header-menu {
- height: auto;
- z-index: 100;
- position: absolute;
- background: white;
- padding: 5px;
- position: fixed;
- background: white;
- border: black 1px solid;
- .collectionSchema-header-toggler {
- z-index: 100;
- width: 100%;
- height: 100%;
- padding: 4px;
- letter-spacing: 2px;
- text-transform: uppercase;
- svg {
- margin-right: 4px;
- }
- }
-}
+ .schema-key-search-input {
+ width: calc(100% - 20px);
+ margin: 10px;
+ }
-.collectionSchemaView-header {
- height: 100%;
- color: gray;
- z-index: 100;
- overflow-y: visible;
- display: flex;
- justify-content: space-between;
- flex-wrap: wrap;
-}
+ .schema-search-result {
+ cursor: pointer;
+ padding: 5px 10px;
+ width: 100%;
-button.add-column {
- width: 28px;
-}
+ &:hover {
+ background-color: $medium-gray;
+ }
-.collectionSchemaView-menuOptions-wrapper {
- background: rgb(241, 239, 235);
- display: flex;
- cursor: default;
- height: 100%;
- align-content: center;
- align-items: center;
-}
+ .schema-search-result-type,
+ .schema-search-result-desc {
+ color: $dark-gray;
+ font-size: $body-text;
+ }
+ }
-.collectionSchema-header-menuOptions {
- color: black;
- width: 180px;
- text-align: left;
- .collectionSchema-headerMenu-group {
- padding: 7px 0;
- border-bottom: 1px solid lightgray;
- cursor: pointer;
- &:first-child {
- padding-top: 0;
- }
- &:last-child {
- border: none;
- text-align: center;
- padding: 12px 0 0 0;
- }
- }
- label {
- color: $medium-gray;
- font-weight: normal;
- letter-spacing: 2px;
- text-transform: uppercase;
- }
- input {
- color: black;
- width: 100%;
- }
- .columnMenu-option {
- cursor: pointer;
- padding: 3px;
- background-color: white;
- transition: background-color 0.2s;
- &:hover {
- background-color: $light-gray;
- }
- &.active {
- font-weight: bold;
- border: 2px solid $light-gray;
- }
- svg {
- color: gray;
- margin-right: 5px;
- width: 10px;
- }
- }
+ .schema-key-search,
+ .schema-new-key-options {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ }
- .keys-dropdown {
- position: relative;
- //width: 100%;
- background-color: white;
- input {
- border: 2px solid $light-gray;
- padding: 3px;
- height: 28px;
- font-weight: bold;
- letter-spacing: '2px';
- text-transform: 'uppercase';
- &:focus {
- font-weight: normal;
+ .schema-new-key-options {
+ margin: 10px;
+ .schema-key-warning {
+ color: red;
+ font-weight: normal;
+ align-self: center;
+ }
}
- }
- }
- .columnMenu-colors {
- display: flex;
- justify-content: space-between;
- flex-wrap: wrap;
- .columnMenu-colorPicker {
- cursor: pointer;
- width: 20px;
- height: 20px;
- border-radius: 10px;
- &.active {
- border: 2px solid white;
- box-shadow: 0 0 0 2px lightgray;
+
+ .schema-key-list {
+ width: 100%;
+ max-height: 300px;
+ overflow-y: auto;
}
- }
- }
-}
-.schema-icon {
- cursor: pointer;
- width: 25px;
- height: 25px;
- display: flex;
- align-items: center;
- justify-content: center;
- align-content: center;
- background-color: $medium-blue;
- color: white;
- margin-right: 5px;
- font-size: 10px;
- border-radius: 3px;
-}
+ .schema-key-type-option {
+ margin: 2px 0px;
-.keys-options-wrapper {
- position: absolute;
- text-align: left;
- height: fit-content;
- top: 100%;
- z-index: 21;
- background-color: #ffffff;
- box-shadow: 0px 3px 4px rgba(0, 0, 0, 30%);
- padding: 1px;
- .key-option {
- cursor: pointer;
- color: #000000;
- width: 100%;
- height: 25px;
- font-weight: 400;
- display: flex;
- justify-content: left;
- align-items: center;
- padding-left: 5px;
- &:hover {
- background-color: $light-gray;
- }
- }
-}
+ input {
+ margin-right: 5px;
+ }
+ }
-.collectionSchema-row {
- height: 100%;
- background-color: white;
- &.row-focused .rt-td {
- background-color: $light-blue; //$light-gray;
- overflow: visible;
- }
- &.row-wrapped {
- .rt-td {
- white-space: normal;
- }
- }
- .row-dragger {
- display: flex;
- justify-content: space-evenly;
- width: 58px;
- position: absolute;
- /* max-width: 50px; */
- min-height: 30px;
- align-items: center;
- color: lightgray;
- background-color: white;
- transition: color 0.1s ease;
- .row-option {
- color: black;
- cursor: pointer;
- position: relative;
- transition: color 0.1s ease;
- display: flex;
- flex-direction: column;
- justify-content: center;
- z-index: 2;
- border-radius: 3px;
- padding: 3px;
- &:hover {
- background-color: $light-gray;
+ .schema-key-default-val {
+ margin: 5px 0;
+ }
+
+ .schema-column-menu-button {
+ cursor: pointer;
+ padding: 2px 5px;
+ background: $medium-blue;
+ border-radius: 9999px;
+ color: $white;
+ width: fit-content;
+ margin: 5px;
+ align-self: center;
}
}
}
- .collectionSchema-row-wrapper {
- &.row-above {
- border-top: 1px solid $medium-blue;
- }
- &.row-below {
- border-bottom: 1px solid $medium-blue;
- }
- &.row-inside {
- border: 2px dashed $medium-blue;
- }
- .row-dragging {
- background-color: blue;
- }
+
+ .schema-preview-divider {
+ height: 100%;
+ background: black;
+ cursor: ew-resize;
}
}
-.collectionSchemaView-cellContainer {
- width: 100%;
- height: unset;
-}
+.schema-header-row {
+ cursor: grab;
+ //justify-content: flex-end;
-.collectionSchemaView-cellContents {
- width: 100%;
+ .row-menu {
+ display: flex;
+ justify-content: flex-end;
+ }
}
-.collectionSchemaView-cellWrapper {
+.schema-column-header {
+ background-color: $light-gray;
+ font-weight: bold;
display: flex;
- height: 100%;
- text-align: left;
- padding-left: 19px;
- position: relative;
+ flex-direction: row;
+ justify-content: space-between;
align-items: center;
- align-content: center;
- &:focus {
- outline: none;
- }
- &.editing {
- padding: 0;
- box-shadow: 0px 3px 4px rgba(0, 0, 0, 0.3);
- transform: scale(1.1);
- z-index: 40;
- input {
- outline: 0;
- border: none;
- background-color: $white;
- width: 100%;
- height: fit-content;
- min-height: 26px;
- }
- }
- &.focused {
+ padding: 0;
+ z-index: 1;
+ border: 1px solid $medium-gray;
+ //overflow: hidden;
+
+ .schema-column-title {
+ flex-grow: 2;
+ margin: 5px;
overflow: hidden;
- &.inactive {
- border: none;
- }
- }
- p {
- width: 100%;
- height: 100%;
- }
- &:hover .collectionSchemaView-cellContents-docExpander {
- display: block;
- }
- .collectionSchemaView-cellContents-document {
- display: inline-block;
+ min-width: 20%;
}
- .collectionSchemaView-cellContents-docButton {
- float: right;
- width: '15px';
- height: '15px';
+
+ .schema-header-menu {
+ margin: 5px;
}
- .collectionSchemaView-dropdownWrapper {
- border: grey;
- border-style: solid;
- border-width: 1px;
- height: 30px;
- .collectionSchemaView-dropdownButton {
- //display: inline-block;
- float: left;
- height: 100%;
- }
- .collectionSchemaView-dropdownText {
- display: inline-block;
- //float: right;
- height: 100%;
- display: 'flex';
- font-size: 13;
- justify-content: 'center';
- align-items: 'center';
+
+ .schema-column-resizer {
+ height: 100%;
+ width: 3px;
+ cursor: ew-resize;
+
+ &:hover {
+ background-color: $light-blue;
}
}
- .collectionSchemaView-dropdownContainer {
- position: absolute;
- border: 1px solid rgba(0, 0, 0, 0.04);
- box-shadow: 0 16px 24px 2px rgba(0, 0, 0, 0.14);
- .collectionSchemaView-dropdownOption:hover {
- background-color: rgba(0, 0, 0, 0.14);
- cursor: pointer;
- }
+
+ .schema-column-resizer.left {
+ min-width: 5px;
+ transform: translate(-3px, 0px);
+ align-self: flex-start;
+ background-color: $medium-gray;
}
}
-.collectionSchemaView-cellContents-docExpander {
- height: 30px;
- width: 30px;
- display: none;
- position: absolute;
- top: 0;
- right: 0;
- background-color: lightgray;
+.schema-header-menu {
+ display: flex;
+ flex-direction: row;
}
-.doc-drag-over {
- background-color: red;
+.schema-row-wrapper {
+ overflow: hidden;
}
-.collectionSchemaView-toolbar {
- z-index: 100;
+.schema-header-row {
+ background-color: $light-gray;
+ overflow: hidden;
}
-.collectionSchemaView-toolbar {
- height: 30px;
+.schema-header-row,
+.schema-row {
display: flex;
- justify-content: flex-end;
- padding: 0 10px;
- border-bottom: 2px solid gray;
- .collectionSchemaView-toolbar-item {
- display: flex;
- flex-direction: column;
- justify-content: center;
- }
+ flex-direction: row;
+ height: 100%;
+ overflow: auto;
}
-#preview-schema-checkbox-div {
- margin-left: 20px;
- font-size: 12px;
+.schema-header-row > .schema-column-header:nth-child(2) > .left {
+ display: none;
}
-.collectionSchemaView-table {
- width: 100%;
- height: 100%;
- overflow: auto;
- padding: 3px;
+.schema-table-cell,
+.row-menu {
+ border: 1px solid $medium-gray;
+ overflow-x: hidden;
+ overflow-y: auto;
+ padding: 5px;
+ display: inline-block;
}
-.rt-td.rt-expandable {
- overflow: visible;
- position: relative;
- height: 100%;
- z-index: 1;
-}
+.schema-row {
+ cursor: grab;
+ justify-content: flex-end;
+ background: white;
-.reactTable-sub {
- background-color: rgb(252, 252, 252);
- width: 100%;
- .rt-thead {
- display: none;
- }
- .row-dragger {
- background-color: rgb(252, 252, 252);
- }
- .rt-table {
- background-color: rgb(252, 252, 252);
+ .row-menu {
+ display: flex;
+ flex-direction: row;
+ min-width: 50px;
+ justify-content: flex-end;
}
- .collectionSchemaView-table {
- width: 100%;
- border: solid 1px;
- overflow: visible;
- padding: 0px;
+
+ .row-cells {
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-end;
}
}
-.collectionSchemaView-expander {
- height: 100%;
- min-height: 30px;
- position: absolute;
- color: gray;
- width: 20;
- height: auto;
- left: 55;
+.schema-row-button,
+.schema-header-button {
+ color: $dark-gray;
+ margin: 3px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
svg {
- position: absolute;
- top: 50%;
- left: 10;
- transform: translate(-50%, -50%);
+ width: 15px;
}
}
-.collectionSchemaView-addRow {
- color: gray;
- letter-spacing: 2px;
- text-transform: uppercase;
+.schema-sort-button {
+ width: 17px;
+ height: 17px;
+ border-radius: 30%;
+ background-color: $dark-gray;
+ color: white;
+ margin: 3px;
cursor: pointer;
- font-size: 10.5px;
- margin-left: 50px;
- margin-top: 10px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ svg {
+ width: 12px;
+ }
}
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
index c4ee1805f..ee5bf82ed 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
@@ -1,646 +1,978 @@
import React = require('react');
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, observable, untracked } from 'mobx';
+import { action, computed, observable, ObservableMap, observe, trace } from 'mobx';
import { observer } from 'mobx-react';
-import Measure from 'react-measure';
-import { Resize } from 'react-table';
-import { Doc, Opt } from '../../../../fields/Doc';
+import { computedFn } from 'mobx-utils';
+import { Doc, DocListCast, Field, NumListCast, StrListCast } from '../../../../fields/Doc';
+import { Id } from '../../../../fields/FieldSymbols';
import { List } from '../../../../fields/List';
import { listSpec } from '../../../../fields/Schema';
-import { PastelSchemaPalette, SchemaHeaderField } from '../../../../fields/SchemaHeaderField';
-import { Cast, NumCast } from '../../../../fields/Types';
-import { TraceMobx } from '../../../../fields/util';
-import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents } from '../../../../Utils';
-import { DocUtils } from '../../../documents/Documents';
+import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types';
+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 { SnappingManager } from '../../../util/SnappingManager';
-import { Transform } from '../../../util/Transform';
-import { undoBatch } from '../../../util/UndoManager';
+import { undoable, undoBatch } from '../../../util/UndoManager';
import { ContextMenu } from '../../ContextMenu';
-import { ContextMenuProps } from '../../ContextMenuItem';
-import { COLLECTION_BORDER_WIDTH, SCHEMA_DIVIDER_WIDTH } from '../../global/globalCssVariables.scss';
-import { DocumentView } from '../../nodes/DocumentView';
+import { EditableView } from '../../EditableView';
+import { Colors } from '../../global/globalEnums';
+import { DocFocusOptions, DocumentView } from '../../nodes/DocumentView';
+import { KeyValueBox } from '../../nodes/KeyValueBox';
import { DefaultStyleProvider } from '../../StyleProvider';
import { CollectionSubView } from '../CollectionSubView';
import './CollectionSchemaView.scss';
-import { SchemaTable } from './SchemaTable';
-// bcz: need to add drag and drop of rows and columns. This seems like it might work for rows: https://codesandbox.io/s/l94mn1q657
+import { SchemaColumnHeader } from './SchemaColumnHeader';
+import { SchemaRowBox } from './SchemaRowBox';
export enum ColumnType {
- Any,
Number,
String,
Boolean,
- Doc,
- Image,
- List,
Date,
+ Image,
+ RTF,
+ Enumeration,
+ Any,
}
-// this map should be used for keys that should have a const type of value
-const columnTypes: Map<string, ColumnType> = new Map([
- ['title', ColumnType.String],
- ['x', ColumnType.Number],
- ['y', ColumnType.Number],
- ['_width', ColumnType.Number],
- ['_height', ColumnType.Number],
- ['_nativeWidth', ColumnType.Number],
- ['_nativeHeight', ColumnType.Number],
- ['isPrototype', ColumnType.Boolean],
- ['_curPage', ColumnType.Number],
- ['_currentTimecode', ColumnType.Number],
- ['zIndex', ColumnType.Number],
-]);
+
+export const FInfotoColType: { [key: string]: ColumnType } = {
+ string: ColumnType.String,
+ number: ColumnType.Number,
+ boolean: ColumnType.Boolean,
+ date: ColumnType.Date,
+ image: ColumnType.Image,
+ rtf: ColumnType.RTF,
+ enumeration: ColumnType.Enumeration,
+};
+
+const defaultColumnKeys: string[] = ['title', 'type', 'author', 'author_date', 'text'];
@observer
export class CollectionSchemaView extends CollectionSubView() {
- private _previewCont?: HTMLDivElement;
-
- @observable _previewDoc: Doc | undefined = undefined;
- @observable _focusedTable: Doc = this.props.Document;
- @observable _col: any = '';
- @observable _menuWidth = 0;
- @observable _headerOpen = false;
- @observable _headerIsEditing = false;
- @observable _menuHeight = 0;
- @observable _pointerX = 0;
- @observable _pointerY = 0;
- @observable _openTypes: boolean = false;
+ private _keysDisposer: any;
+ private _closestDropIndex: number = 0;
+ private _previewRef: HTMLDivElement | null = null;
+ 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;
+ public static _minColWidth: number = 25;
+ public static _rowMenuWidth: number = 60;
+ public static _previewDividerWidth: number = 4;
+ public static _newNodeInputHeight: number = 30;
+ public fieldInfos = new ObservableMap<string, FInfo>();
+
+ @observable _menuKeys: string[] = [];
+ @observable _rowEles: ObservableMap = new ObservableMap<Doc, HTMLDivElement>();
+ @observable _colEles: HTMLDivElement[] = [];
+ @observable _displayColumnWidths: number[] | undefined;
+ @observable _columnMenuIndex: number | undefined;
+ @observable _newFieldWarning: string = '';
+ @observable _makeNewField: boolean = false;
+ @observable _newFieldDefault: any = 0;
+ @observable _newFieldType: ColumnType = ColumnType.Number;
+ @observable _menuValue: string = '';
+ @observable _filterColumnIndex: number | undefined;
+ @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 previewWidth() {
- return () => NumCast(this.props.Document.schemaPreviewWidth);
+ @computed get _selectedDocs() {
+ return SelectionManager.Docs().filter(doc => Doc.AreProtosEqual(DocCast(doc.embedContainer), this.rootDoc));
}
- @computed get previewHeight() {
- return () => this.props.PanelHeight() - 2 * this.borderWidth;
+
+ @computed get documentKeys() {
+ return Array.from(this.fieldInfos.keys());
+ }
+
+ @computed get previewWidth() {
+ return NumCast(this.layoutDoc.schema_previewWidth);
}
+
@computed get tableWidth() {
- return this.props.PanelWidth() - 2 * this.borderWidth - Number(SCHEMA_DIVIDER_WIDTH) - this.previewWidth();
+ return this.props.PanelWidth() - this.previewWidth - (this.previewWidth === 0 ? 0 : CollectionSchemaView._previewDividerWidth);
+ }
+
+ @computed get columnKeys() {
+ return Cast(this.layoutDoc.schema_columnKeys, listSpec('string'), defaultColumnKeys);
}
- @computed get borderWidth() {
- return Number(COLLECTION_BORDER_WIDTH);
+
+ @computed get storedColumnWidths() {
+ const widths = NumListCast(
+ this.layoutDoc.schema_columnWidths,
+ this.columnKeys.map(() => (this.tableWidth - CollectionSchemaView._rowMenuWidth) / this.columnKeys.length)
+ );
+
+ const totalWidth = widths.reduce((sum, width) => sum + width, 0);
+ if (totalWidth !== this.tableWidth - CollectionSchemaView._rowMenuWidth) {
+ return widths.map(w => (w / totalWidth) * (this.tableWidth - CollectionSchemaView._rowMenuWidth));
+ }
+ return widths;
}
- @computed get scale() {
- return this.props.ScreenToLocalTransform().Scale;
+
+ @computed get displayColumnWidths() {
+ return this._displayColumnWidths ?? this.storedColumnWidths;
}
- @computed get columns() {
- return Cast(this.props.Document._schemaHeaders, listSpec(SchemaHeaderField), []);
+
+ @computed get sortField() {
+ return StrCast(this.layoutDoc.sortField);
}
- set columns(columns: SchemaHeaderField[]) {
- this.props.Document._schemaHeaders = new List<SchemaHeaderField>(columns);
+
+ @computed get sortDesc() {
+ return BoolCast(this.layoutDoc.sortDesc);
}
- @computed get menuCoordinates() {
- let searchx = 0;
- let searchy = 0;
- if (this.props.Document._searchDoc) {
- const el = document.getElementsByClassName('collectionSchemaView-searchContainer')[0];
- if (el !== undefined) {
- const rect = el.getBoundingClientRect();
- searchx = rect.x;
- searchy = rect.y;
- }
- }
- const x = Math.max(0, Math.min(document.body.clientWidth - this._menuWidth, this._pointerX)) - searchx;
- const y = Math.max(0, Math.min(document.body.clientHeight - this._menuHeight, this._pointerY)) - searchy;
- return this.props.ScreenToLocalTransform().transformPoint(x, y);
+ @action
+ componentDidMount() {
+ this.props.setContentView?.(this);
+ document.addEventListener('keydown', this.onKeyDown);
+
+ Object.entries(this._documentOptions).forEach((pair: [string, FInfo]) => this.fieldInfos.set(pair[0], pair[1]));
+ this._keysDisposer = observe(
+ this.rootDoc[this.fieldKey ?? 'data'] as List<Doc>,
+ change => {
+ switch (change.type as any) {
+ case 'splice':
+ // prettier-ignore
+ (change as any).added.forEach((doc: Doc) => // for each document added
+ Doc.GetAllPrototypes(doc.value as Doc).forEach(proto => // for all of its prototypes (and itself)
+ Object.keys(proto).forEach(action(key => // check if any of its keys are new, and add them
+ !this.fieldInfos.get(key) && this.fieldInfos.set(key, new FInfo(''))))));
+ break;
+ case 'update': //let oldValue = change.oldValue; // fill this in if the entire child list will ever be reassigned with a new list
+ }
+ },
+ true
+ );
}
- get documentKeys() {
- const docs = this.childDocs;
- const keys: { [key: string]: boolean } = {};
- // bcz: ugh. this is untracked since otherwise a large collection of documents will blast the server for all their fields.
- // then as each document's fields come back, we update the documents _proxies. Each time we do this, the whole schema will be
- // invalidated and re-rendered. This workaround will inquire all of the document fields before the options button is clicked.
- // then by the time the options button is clicked, all of the fields should be in place. If a new field is added while this menu
- // is displayed (unlikely) it won't show up until something else changes.
- //TODO Types
- untracked(() => docs.map(doc => Doc.GetAllPrototypes(doc).map(proto => Object.keys(proto).forEach(key => (keys[key] = false)))));
-
- this.columns.forEach(key => (keys[key.heading] = true));
- return Array.from(Object.keys(keys));
+ componentWillUnmount() {
+ this._keysDisposer?.();
+ document.removeEventListener('keydown', this.onKeyDown);
}
- @action setHeaderIsEditing = (isEditing: boolean) => (this._headerIsEditing = isEditing);
+ rowIndex = (doc: Doc) => this.sortedDocs.docs.indexOf(doc);
- @undoBatch
- setColumnType = action((columnField: SchemaHeaderField, type: ColumnType): void => {
- this._openTypes = false;
- if (columnTypes.get(columnField.heading)) return;
-
- const columns = this.columns;
- const index = columns.indexOf(columnField);
- if (index > -1) {
- columnField.setType(NumCast(type));
- columns[index] = columnField;
- this.columns = columns;
+ @action
+ onKeyDown = (e: KeyboardEvent) => {
+ if (this._selectedDocs.length > 0) {
+ switch (e.key) {
+ case 'ArrowDown':
+ {
+ const lastDoc = this._selectedDocs.lastElement();
+ const lastIndex = this.rowIndex(lastDoc);
+ const curDoc = this.sortedDocs.docs[lastIndex];
+ if (lastIndex >= 0 && lastIndex < this.childDocs.length - 1) {
+ !e.shiftKey && this.clearSelection();
+ const newDoc = this.sortedDocs.docs[lastIndex + 1];
+ if (this._selectedDocs.includes(newDoc)) {
+ SelectionManager.DeselectView(DocumentManager.Instance.getFirstDocumentView(curDoc));
+ } else {
+ this.addDocToSelection(newDoc, e.shiftKey, lastIndex + 1);
+ this._selectedCell && (this._selectedCell[0] = newDoc);
+ this.scrollToDoc(newDoc, {});
+ }
+ }
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ break;
+ case 'ArrowUp':
+ {
+ const firstDoc = this._selectedDocs.lastElement();
+ const firstIndex = this.rowIndex(firstDoc);
+ const curDoc = this.sortedDocs.docs[firstIndex];
+ if (firstIndex > 0 && firstIndex < this.childDocs.length) {
+ !e.shiftKey && this.clearSelection();
+ const newDoc = this.sortedDocs.docs[firstIndex - 1];
+ if (this._selectedDocs.includes(newDoc)) SelectionManager.DeselectView(DocumentManager.Instance.getFirstDocumentView(curDoc));
+ else {
+ this.addDocToSelection(newDoc, e.shiftKey, firstIndex - 1);
+ this._selectedCell && (this._selectedCell[0] = newDoc);
+ this.scrollToDoc(newDoc, {});
+ }
+ }
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ break;
+ case 'ArrowRight':
+ if (this._selectedCell) {
+ this._selectedCell[1] = Math.min(this._selectedCell[1] + 1, this.columnKeys.length - 1);
+ } else if (this._selectedDocs.length > 0) {
+ this.selectCell(this._selectedDocs[0], 0);
+ }
+ break;
+ case 'ArrowLeft':
+ if (this._selectedCell) {
+ this._selectedCell[1] = Math.max(this._selectedCell[1] - 1, 0);
+ } else if (this._selectedDocs.length > 0) {
+ this.selectCell(this._selectedDocs[0], 0);
+ }
+ break;
+ case 'Backspace': {
+ this.removeDocument(this._selectedDocs);
+ break;
+ }
+ case 'Escape': {
+ this.deselectCell();
+ }
+ }
}
- });
+ };
@undoBatch
- setColumnColor = (columnField: SchemaHeaderField, color: string): void => {
- const columns = this.columns;
- const index = columns.indexOf(columnField);
- if (index > -1) {
- columnField.setColor(color);
- columns[index] = columnField;
- this.columns = columns; // need to set the columns to trigger rerender
- }
+ @action
+ setColumnSort = (field: string | undefined, desc: boolean = false) => {
+ this.layoutDoc.sortField = field;
+ this.layoutDoc.sortDesc = desc;
+ };
+
+ addRow = (doc: Doc | Doc[]) => {
+ const result: boolean = this.addDocument(doc);
+ return result;
};
@undoBatch
@action
- setColumnSort = (columnField: SchemaHeaderField, descending: boolean | undefined) => {
- const columns = this.columns;
- columns.forEach(col => col.setDesc(undefined));
+ changeColumnKey = (index: number, newKey: string, defaultVal?: any) => {
+ if (!this.documentKeys.includes(newKey)) {
+ this.addNewKey(newKey, defaultVal);
+ }
- const index = columns.findIndex(c => c.heading === columnField.heading);
- const column = columns[index];
- column.setDesc(descending);
- columns[index] = column;
- this.columns = columns;
+ let currKeys = [...this.columnKeys];
+ currKeys[index] = newKey;
+ this.layoutDoc.schema_columnKeys = new List<string>(currKeys);
};
- renderTypes = (col: any) => {
- if (columnTypes.get(col.heading)) return null;
+ @undoBatch
+ @action
+ addColumn = (key: string, defaultVal?: any) => {
+ if (!this.documentKeys.includes(key)) {
+ this.addNewKey(key, defaultVal);
+ }
- const type = col.type;
+ const newColWidth = this.tableWidth / (this.storedColumnWidths.length + 1);
+ const currWidths = this.storedColumnWidths.slice();
+ currWidths.splice(0, 0, newColWidth);
+ const newDesiredTableWidth = currWidths.reduce((w, cw) => w + cw, 0);
+ this.layoutDoc.schema_columnWidths = new List<number>(currWidths.map(w => (w / newDesiredTableWidth) * (this.tableWidth - CollectionSchemaView._rowMenuWidth)));
- const anyType = (
- <div className={'columnMenu-option' + (type === ColumnType.Any ? ' active' : '')} onClick={() => this.setColumnType(col, ColumnType.Any)}>
- <FontAwesomeIcon icon={'align-justify'} size="sm" />
- Any
- </div>
- );
+ let currKeys = this.columnKeys.slice();
+ currKeys.splice(0, 0, key);
+ this.layoutDoc.schema_columnKeys = new List<string>(currKeys);
+ };
- const numType = (
- <div className={'columnMenu-option' + (type === ColumnType.Number ? ' active' : '')} onClick={() => this.setColumnType(col, ColumnType.Number)}>
- <FontAwesomeIcon icon={'hashtag'} size="sm" />
- Number
- </div>
- );
+ @action
+ addNewKey = (key: string, defaultVal: any) => this.childDocs.forEach(doc => (doc[key] = defaultVal));
- const textType = (
- <div className={'columnMenu-option' + (type === ColumnType.String ? ' active' : '')} onClick={() => this.setColumnType(col, ColumnType.String)}>
- <FontAwesomeIcon icon={'font'} size="sm" />
- Text
- </div>
- );
+ @undoBatch
+ @action
+ removeColumn = (index: number) => {
+ if (this.columnKeys.length === 1) return;
+ const currWidths = this.storedColumnWidths.slice();
+ currWidths.splice(index, 1);
+ const newDesiredTableWidth = currWidths.reduce((w, cw) => w + cw, 0);
+ this.layoutDoc.schema_columnWidths = new List<number>(currWidths.map(w => (w / newDesiredTableWidth) * (this.tableWidth - CollectionSchemaView._rowMenuWidth)));
+
+ let currKeys = this.columnKeys.slice();
+ currKeys.splice(index, 1);
+ this.layoutDoc.schema_columnKeys = new List<string>(currKeys);
+ };
- const boolType = (
- <div className={'columnMenu-option' + (type === ColumnType.Boolean ? ' active' : '')} onClick={() => this.setColumnType(col, ColumnType.Boolean)}>
- <FontAwesomeIcon icon={'check-square'} size="sm" />
- Checkbox
- </div>
- );
+ @action
+ startResize = (e: any, index: number) => {
+ this._displayColumnWidths = this.storedColumnWidths;
+ setupMoveUpEvents(this, e, (e, delta) => this.resizeColumn(e, index), this.finishResize, emptyFunction);
+ };
- const listType = (
- <div className={'columnMenu-option' + (type === ColumnType.List ? ' active' : '')} onClick={() => this.setColumnType(col, ColumnType.List)}>
- <FontAwesomeIcon icon={'list-ul'} size="sm" />
- List
- </div>
- );
+ @action
+ resizeColumn = (e: PointerEvent, index: number) => {
+ if (this._displayColumnWidths) {
+ let shrinking;
+ let growing;
- const docType = (
- <div className={'columnMenu-option' + (type === ColumnType.Doc ? ' active' : '')} onClick={() => this.setColumnType(col, ColumnType.Doc)}>
- <FontAwesomeIcon icon={'file'} size="sm" />
- Document
- </div>
- );
+ let change = e.movementX;
- const imageType = (
- <div className={'columnMenu-option' + (type === ColumnType.Image ? ' active' : '')} onClick={() => this.setColumnType(col, ColumnType.Image)}>
- <FontAwesomeIcon icon={'image'} size="sm" />
- Image
- </div>
- );
+ if (index !== 0) {
+ growing = change < 0 ? index : index - 1;
+ shrinking = change < 0 ? index - 1 : index;
+ }
- const dateType = (
- <div className={'columnMenu-option' + (type === ColumnType.Date ? ' active' : '')} onClick={() => this.setColumnType(col, ColumnType.Date)}>
- <FontAwesomeIcon icon={'calendar'} size="sm" />
- Date
- </div>
- );
+ if (shrinking === undefined || growing === undefined) return true;
- const allColumnTypes = (
- <div className="columnMenu-types">
- {anyType}
- {numType}
- {textType}
- {boolType}
- {listType}
- {docType}
- {imageType}
- {dateType}
- </div>
- );
+ change = Math.abs(change);
+ if (this._displayColumnWidths[shrinking] - change < CollectionSchemaView._minColWidth) {
+ change = this._displayColumnWidths[shrinking] - CollectionSchemaView._minColWidth;
+ }
- const justColType =
- type === ColumnType.Any
- ? anyType
- : type === ColumnType.Number
- ? numType
- : type === ColumnType.String
- ? textType
- : type === ColumnType.Boolean
- ? boolType
- : type === ColumnType.List
- ? listType
- : type === ColumnType.Doc
- ? docType
- : type === ColumnType.Date
- ? dateType
- : imageType;
+ this._displayColumnWidths[shrinking] -= change * this.props.ScreenToLocalTransform().Scale;
+ this._displayColumnWidths[growing] += change * this.props.ScreenToLocalTransform().Scale;
- return (
- <div className="collectionSchema-headerMenu-group" onClick={action(() => (this._openTypes = !this._openTypes))}>
- <div>
- <label style={{ cursor: 'pointer' }}>Column type:</label>
- <FontAwesomeIcon icon={'caret-down'} size="lg" style={{ float: 'right', transform: `rotate(${this._openTypes ? '180deg' : 0})`, transition: '0.2s all ease' }} />
- </div>
- {this._openTypes ? allColumnTypes : justColType}
- </div>
- );
+ return false;
+ }
+ return true;
};
- renderSorting = (col: any) => {
- const sort = col.desc;
- return (
- <div className="collectionSchema-headerMenu-group">
- <label>Sort by:</label>
- <div className="columnMenu-sort">
- <div className={'columnMenu-option' + (sort === true ? ' active' : '')} onClick={() => this.setColumnSort(col, true)}>
- <FontAwesomeIcon icon="sort-amount-down" size="sm" />
- Sort descending
- </div>
- <div className={'columnMenu-option' + (sort === false ? ' active' : '')} onClick={() => this.setColumnSort(col, false)}>
- <FontAwesomeIcon icon="sort-amount-up" size="sm" />
- Sort ascending
- </div>
- <div className="columnMenu-option" onClick={() => this.setColumnSort(col, undefined)}>
- <FontAwesomeIcon icon="times" size="sm" />
- Clear sorting
- </div>
- </div>
- </div>
- );
+ @action
+ finishResize = () => {
+ this.layoutDoc.schema_columnWidths = new List<number>(this._displayColumnWidths);
+ this._displayColumnWidths = undefined;
};
- renderColors = (col: any) => {
- const selected = col.color;
-
- const pink = PastelSchemaPalette.get('pink2');
- const purple = PastelSchemaPalette.get('purple2');
- const blue = PastelSchemaPalette.get('bluegreen1');
- const yellow = PastelSchemaPalette.get('yellow4');
- const red = PastelSchemaPalette.get('red2');
- const gray = '#f1efeb';
-
- return (
- <div className="collectionSchema-headerMenu-group">
- <label>Color:</label>
- <div className="columnMenu-colors">
- <div className={'columnMenu-colorPicker' + (selected === pink ? ' active' : '')} style={{ backgroundColor: pink }} onClick={() => this.setColumnColor(col, pink!)}></div>
- <div className={'columnMenu-colorPicker' + (selected === purple ? ' active' : '')} style={{ backgroundColor: purple }} onClick={() => this.setColumnColor(col, purple!)}></div>
- <div className={'columnMenu-colorPicker' + (selected === blue ? ' active' : '')} style={{ backgroundColor: blue }} onClick={() => this.setColumnColor(col, blue!)}></div>
- <div className={'columnMenu-colorPicker' + (selected === yellow ? ' active' : '')} style={{ backgroundColor: yellow }} onClick={() => this.setColumnColor(col, yellow!)}></div>
- <div className={'columnMenu-colorPicker' + (selected === red ? ' active' : '')} style={{ backgroundColor: red }} onClick={() => this.setColumnColor(col, red!)}></div>
- <div className={'columnMenu-colorPicker' + (selected === gray ? ' active' : '')} style={{ backgroundColor: gray }} onClick={() => this.setColumnColor(col, gray)}></div>
- </div>
- </div>
- );
+ @undoBatch
+ @action
+ moveColumn = (fromIndex: number, toIndex: number) => {
+ let currKeys = this.columnKeys.slice();
+ currKeys.splice(toIndex, 0, currKeys.splice(fromIndex, 1)[0]);
+ this.layoutDoc.schema_columnKeys = new List<string>(currKeys);
+
+ let currWidths = this.storedColumnWidths.slice();
+ currWidths.splice(toIndex, 0, currWidths.splice(fromIndex, 1)[0]);
+ this.layoutDoc.schema_columnWidths = new List<number>(currWidths);
};
- @undoBatch
@action
- changeColumns = (oldKey: string, newKey: string, addNew: boolean, filter?: string) => {
- const columns = this.columns;
- if (columns === undefined) {
- this.columns = new List<SchemaHeaderField>([new SchemaHeaderField(newKey, 'f1efeb')]);
- } else {
- if (addNew) {
- columns.push(new SchemaHeaderField(newKey, 'f1efeb'));
- this.columns = columns;
- } else {
- const index = columns.map(c => c.heading).indexOf(oldKey);
- if (index > -1) {
- const column = columns[index];
- column.setHeading(newKey);
- columns[index] = column;
- this.columns = columns;
- if (filter) {
- Doc.setDocFilter(this.props.Document, newKey, filter, 'match');
- } else {
- this.props.Document._docFilters = undefined;
- }
- }
+ 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]));
+ DragManager.StartColumnDrag(dragEles, dragData, e.x, e.y);
+
+ document.removeEventListener('pointermove', this.highlightDropColumn);
+ document.addEventListener('pointermove', this.highlightDropColumn);
+ let stopHighlight = (e: PointerEvent) => {
+ document.removeEventListener('pointermove', this.highlightDropColumn);
+ document.removeEventListener('pointerup', stopHighlight);
+ };
+ document.addEventListener('pointerup', stopHighlight);
+
+ return true;
+ };
+
+ findDropIndex = (mouseX: number) => {
+ let index: number | undefined;
+ this.displayColumnWidths.reduce((total, curr, i) => {
+ if (total <= mouseX && total + curr >= mouseX) {
+ if (mouseX <= total + curr) index = i;
+ else index = i + 1;
}
- }
+ return total + curr;
+ }, CollectionSchemaView._rowMenuWidth);
+ return index;
};
@action
- openHeader = (col: any, screenx: number, screeny: number) => {
- this._col = col;
- this._headerOpen = true;
- this._pointerX = screenx;
- this._pointerY = screeny;
+ 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 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 => {
+ this._rowEles.get(doc).children[1].children[i].style.borderLeft = leftStyle;
+ this._rowEles.get(doc).children[1].children[i].style.borderRight = rightStyle;
+ });
+ });
};
@action
- closeHeader = () => {
- this._headerOpen = false;
- };
+ addRowRef = (doc: Doc, ref: HTMLDivElement) => this._rowEles.set(doc, ref);
- @undoBatch
@action
- deleteColumn = (key: string) => {
- const columns = this.columns;
- if (columns === undefined) {
- this.columns = new List<SchemaHeaderField>([]);
+ setColRef = (index: number, ref: HTMLDivElement) => {
+ if (this._colEles.length <= index) {
+ this._colEles.push(ref);
} else {
- const index = columns.map(c => c.heading).indexOf(key);
- if (index > -1) {
- columns.splice(index, 1);
- this.columns = columns;
- }
+ this._colEles[index] = ref;
}
- this.closeHeader();
};
- getPreviewTransform = (): Transform => {
- return this.props.ScreenToLocalTransform().translate(-this.borderWidth - NumCast(COLLECTION_BORDER_WIDTH) - this.tableWidth, -this.borderWidth);
+ @action
+ addDocToSelection = (doc: Doc, extendSelection: boolean, index: number) => {
+ const rowDocView = DocumentManager.Instance.getDocumentView(doc);
+ if (rowDocView) SelectionManager.SelectView(rowDocView, extendSelection);
};
@action
- onHeaderClick = (e: React.PointerEvent) => {
- e.stopPropagation();
+ clearSelection = () => SelectionManager.DeselectAll();
+
+ selectRows = (rootDoc: Doc, lastSelected: Doc) => {
+ const index = this.rowIndex(rootDoc);
+ const lastSelectedRow = this.rowIndex(lastSelected);
+ const startRow = Math.min(lastSelectedRow, index);
+ const endRow = Math.max(lastSelectedRow, index);
+ for (let i = startRow; i <= endRow; i++) {
+ const currDoc = this.sortedDocs.docs[i];
+ if (!this._selectedDocs.includes(currDoc)) this.addDocToSelection(currDoc, true, i);
+ }
};
@action
- onWheel(e: React.WheelEvent) {
- const scale = this.props.ScreenToLocalTransform().Scale;
- this.props.isContentActive(true) && e.stopPropagation();
- }
+ selectCell = (doc: Doc, index: number) => (this._selectedCell = [doc, index]);
- @computed get renderMenuContent() {
- TraceMobx();
- return (
- <div className="collectionSchema-header-menuOptions">
- {this.renderTypes(this._col)}
- {this.renderColors(this._col)}
- <div className="collectionSchema-headerMenu-group">
- <button
- onClick={() => {
- this.deleteColumn(this._col.heading);
- }}>
- Hide Column
- </button>
- </div>
- </div>
- );
- }
+ @action
+ deselectCell = () => (this._selectedCell = undefined);
- private createTarget = (ele: HTMLDivElement) => {
- this._previewCont = ele;
- super.CreateDropTarget(ele);
- };
+ sortedSelectedDocs = () => this.sortedDocs.docs.filter(doc => this._selectedDocs.includes(doc));
- isFocused = (doc: Doc, outsideReaction: boolean): boolean => this.props.isSelected(outsideReaction) && doc === this._focusedTable;
+ setDropIndex = (index: number) => (this._closestDropIndex = index);
- @action setFocused = (doc: Doc) => (this._focusedTable = doc);
+ @action
+ onInternalDrop = (e: Event, de: DragManager.DropEvent) => {
+ if (de.complete.columnDragData) {
+ const mouseX = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y)[0];
+ const index = this.findDropIndex(mouseX);
+ this.moveColumn(de.complete.columnDragData.colIndex, index ?? de.complete.columnDragData.colIndex);
+
+ this._colEles.forEach((colRef, i) => {
+ colRef.style.borderLeft = '';
+ colRef.style.borderRight = '';
+ this.childDocs.forEach(doc => {
+ this._rowEles.get(doc).children[1].children[i].style.borderLeft = '';
+ this._rowEles.get(doc).children[1].children[i].style.borderRight = '';
+ });
+ });
- @action setPreviewDoc = (doc: Opt<Doc>) => {
- SelectionManager.SelectSchemaViewDoc(doc);
- this._previewDoc = doc;
+ e.stopPropagation();
+ return true;
+ }
+ const draggedDocs = de.complete.docDragData?.draggedDocuments;
+ if (draggedDocs && super.onInternalDrop(e, de) && !this.sortField) {
+ const pushedDocs = this.childDocs.filter((doc, index) => index >= this._closestDropIndex && !draggedDocs.includes(doc));
+ const pushedAndDraggedDocs = [...pushedDocs, ...draggedDocs];
+ const removed = this.childDocs.slice().filter(doc => !pushedAndDraggedDocs.includes(doc));
+ this.dataDoc[this.fieldKey ?? 'data'] = new List<Doc>([...removed, ...draggedDocs, ...pushedDocs]);
+ this.clearSelection();
+ draggedDocs.forEach(doc => {
+ const draggedView = DocumentManager.Instance.getFirstDocumentView(doc);
+ if (draggedView) DocumentManager.Instance.RemoveView(draggedView);
+ DocumentManager.Instance.AddViewRenderedCb(doc, dv => dv.select(true));
+ });
+ return true;
+ }
+ return false;
};
- //toggles preview side-panel of schema
@action
- toggleExpander = () => {
- this.props.Document.schemaPreviewWidth = this.previewWidth() === 0 ? Math.min(this.tableWidth / 3, 200) : 0;
+ onExternalDrop = async (e: React.DragEvent): Promise<void> => {
+ super.onExternalDrop(e, {}, undoBatch(action(docus => docus.map((doc: Doc) => this.addDocument(doc)))));
};
- onDividerDown = (e: React.PointerEvent) => {
- setupMoveUpEvents(this, e, this.onDividerMove, emptyFunction, this.toggleExpander);
- };
+ onDividerDown = (e: React.PointerEvent) => setupMoveUpEvents(this, e, this.onDividerMove, emptyFunction, emptyFunction);
+
@action
onDividerMove = (e: PointerEvent, down: number[], delta: number[]) => {
- const nativeWidth = this._previewCont!.getBoundingClientRect();
+ const nativeWidth = this._previewRef!.getBoundingClientRect();
const minWidth = 40;
const maxWidth = 1000;
const movedWidth = this.props.ScreenToLocalTransform().transformDirection(nativeWidth.right - e.clientX, 0)[0];
const width = movedWidth < minWidth ? minWidth : movedWidth > maxWidth ? maxWidth : movedWidth;
- this.props.Document.schemaPreviewWidth = width;
+ this.layoutDoc.schema_previewWidth = width;
return false;
};
- onPointerDown = (e: React.PointerEvent): void => {
- if (e.button === 0 && !e.altKey && !e.ctrlKey && !e.metaKey) {
- if (this.props.isSelected(true)) e.stopPropagation();
- else this.props.select(false);
- }
- };
+ menuCallback = (x: number, y: number) => {
+ ContextMenu.Instance.clearItems();
- @computed
- get previewDocument(): Doc | undefined {
- return this._previewDoc;
- }
+ DocUtils.addDocumentCreatorMenuItems(this.addRow, this.addRow, x, y, true);
- @computed
- get dividerDragger() {
- return this.previewWidth() === 0 ? null : (
- <div className="collectionSchemaView-dividerDragger" onPointerDown={this.onDividerDown}>
- <div className="collectionSchemaView-dividerDragger" />
- </div>
- );
- }
+ ContextMenu.Instance.displayMenu(x, y, undefined, true);
+ };
- @computed
- get previewPanel() {
- return (
- <div ref={this.createTarget} style={{ width: `${this.previewWidth()}px` }}>
- {!this.previewDocument ? null : (
- <DocumentView
- Document={this.previewDocument}
- DataDoc={undefined}
- fitContentsToBox={returnTrue}
- dontCenter={'y'}
- focus={DocUtils.DefaultFocus}
- renderDepth={this.props.renderDepth}
- rootSelected={this.rootSelected}
- PanelWidth={this.previewWidth}
- PanelHeight={this.previewHeight}
- isContentActive={returnTrue}
- isDocumentActive={returnFalse}
- ScreenToLocalTransform={this.getPreviewTransform}
- docFilters={this.childDocFilters}
- docRangeFilters={this.childDocRangeFilters}
- searchFilterDocs={this.searchFilterDocs}
- styleProvider={DefaultStyleProvider}
- docViewPath={returnEmptyDoclist}
- ContainingCollectionDoc={this.props.CollectionView?.props.Document}
- ContainingCollectionView={this.props.CollectionView}
- moveDocument={this.props.moveDocument}
- addDocument={this.props.addDocument}
- removeDocument={this.props.removeDocument}
- whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged}
- addDocTab={this.props.addDocTab}
- pinToPres={this.props.pinToPres}
- bringToFront={returnFalse}
- />
- )}
- </div>
- );
- }
+ focusDocument = (doc: Doc, options: DocFocusOptions) => {
+ Doc.BrushDoc(doc);
+ this.scrollToDoc(doc, options);
+ return undefined;
+ };
- @computed
- get schemaTable() {
- return (
- <SchemaTable
- Document={this.props.Document}
- PanelHeight={this.props.PanelHeight}
- PanelWidth={this.props.PanelWidth}
- childDocs={this.childDocs}
- CollectionView={this.props.CollectionView}
- ContainingCollectionView={this.props.ContainingCollectionView}
- ContainingCollectionDoc={this.props.ContainingCollectionDoc}
- fieldKey={this.props.fieldKey}
- renderDepth={this.props.renderDepth}
- moveDocument={this.props.moveDocument}
- ScreenToLocalTransform={this.props.ScreenToLocalTransform}
- active={this.props.isContentActive}
- onDrop={this.onExternalDrop}
- addDocTab={this.props.addDocTab}
- pinToPres={this.props.pinToPres}
- isSelected={this.props.isSelected}
- isFocused={this.isFocused}
- setFocused={this.setFocused}
- setPreviewDoc={this.setPreviewDoc}
- deleteDocument={this.props.removeDocument}
- addDocument={this.props.addDocument}
- dataDoc={this.props.DataDoc}
- columns={this.columns}
- documentKeys={this.documentKeys}
- headerIsEditing={this._headerIsEditing}
- openHeader={this.openHeader}
- onClick={this.onTableClick}
- onPointerDown={emptyFunction}
- onResizedChange={this.onResizedChange}
- setColumns={this.setColumns}
- reorderColumns={this.reorderColumns}
- changeColumns={this.changeColumns}
- setHeaderIsEditing={this.setHeaderIsEditing}
- changeColumnSort={this.setColumnSort}
- />
- );
- }
+ scrollToDoc = (doc: Doc, options: DocFocusOptions) => {
+ const found = this._tableContentRef && Array.from(this._tableContentRef.getElementsByClassName('documentView-node')).find((node: any) => node.id === doc[Id]);
+ if (found) {
+ const rect = found.getBoundingClientRect();
+ const localRect = this.props.ScreenToLocalTransform().transformBounds(rect.left, rect.top, rect.width, rect.height);
+ if (localRect.y < this.rowHeightFunc() || localRect.y + localRect.height > this.props.PanelHeight()) {
+ let focusSpeed = options.zoomTime ?? 50;
+ smoothScroll(focusSpeed, this._tableContentRef!, localRect.y + this._tableContentRef!.scrollTop - this.rowHeightFunc(), options.easeFunc);
+ return focusSpeed;
+ }
+ }
+ };
- @computed
- public get schemaToolbar() {
- return (
- <div className="collectionSchemaView-toolbar">
- <div className="collectionSchemaView-toolbar-item">
- <div id="preview-schema-checkbox-div">
- <input type="checkbox" key={'Show Preview'} checked={this.previewWidth() !== 0} onChange={this.toggleExpander} />
- Show Preview
- </div>
- </div>
- </div>
- );
+ @computed get fieldDefaultInput() {
+ switch (this._newFieldType) {
+ case ColumnType.Number:
+ return <input type="number" name="" id="" value={this._newFieldDefault ?? 0} onPointerDown={e => e.stopPropagation()} onChange={action(e => (this._newFieldDefault = e.target.value))} />;
+ case ColumnType.Boolean:
+ return (
+ <>
+ <input type="checkbox" name="" id="" value={this._newFieldDefault} onPointerDown={e => e.stopPropagation()} onChange={action(e => (this._newFieldDefault = e.target.checked))} />
+ {this._newFieldDefault ? 'true' : 'false'}
+ </>
+ );
+ case ColumnType.String:
+ return <input type="text" name="" id="" value={this._newFieldDefault ?? ''} onPointerDown={e => e.stopPropagation()} onChange={action(e => (this._newFieldDefault = e.target.value))} />;
+ }
}
- onSpecificMenu = (e: React.MouseEvent) => {
- if ((e.target as any)?.className?.includes?.('collectionSchemaView-cell') || e.target instanceof HTMLSpanElement) {
- const cm = ContextMenu.Instance;
- const options = cm.findByDescription('Options...');
- const optionItems: ContextMenuProps[] = options && 'subitems' in options ? options.subitems : [];
- optionItems.push({ description: 'remove', event: () => this._previewDoc && this.props.removeDocument?.(this._previewDoc), icon: 'trash' });
- !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'compass' });
- cm.displayMenu(e.clientX, e.clientY);
- (e.nativeEvent as any).SchemaHandled = true; // not sure why this is needed, but if you right-click quickly on a cell, the Document/Collection contextMenu handlers still fire without this.
- e.stopPropagation();
+ onSearchKeyDown = (e: React.KeyboardEvent) => {
+ switch (e.key) {
+ case 'Enter':
+ this._menuKeys.length > 0 && this._menuValue.length > 0 ? this.setKey(this._menuKeys[0]) : action(() => (this._makeNewField = true))();
+ break;
+ case 'Escape':
+ this.closeColumnMenu();
+ break;
}
};
@action
- onTableClick = (e: React.MouseEvent): void => {
- if (!(e.target as any)?.className?.includes?.('collectionSchemaView-cell') && !(e.target instanceof HTMLSpanElement)) {
- this.setPreviewDoc(undefined);
+ setKey = (key: string, defaultVal?: any) => {
+ if (this._makeNewColumn) {
+ this.addColumn(key, defaultVal);
} else {
- e.stopPropagation();
+ this.changeColumnKey(this._columnMenuIndex!, key, defaultVal);
}
- this.setFocused(this.props.Document);
- this.closeHeader();
+ this.closeColumnMenu();
+ };
+
+ setColumnValues = (key: string, value: string) => {
+ let success: boolean = true;
+ this.childDocs.forEach(doc => success && KeyValueBox.SetField(doc, key, value));
+ return success;
+ };
+
+ @action
+ openColumnMenu = (index: number, newCol: boolean) => {
+ this._makeNewColumn = false;
+ this._columnMenuIndex = index;
+ this._menuValue = '';
+ this._menuKeys = this.documentKeys;
+ this._makeNewField = false;
+ this._newFieldWarning = '';
+ this._makeNewField = false;
+ this._makeNewColumn = newCol;
};
- onResizedChange = (newResized: Resize[], event: any) => {
- const columns = this.columns;
- newResized.forEach(resized => {
- const index = columns.findIndex(c => c.heading === resized.id);
- const column = columns[index];
- column.setWidth(resized.value);
- columns[index] = column;
+ @action
+ closeColumnMenu = () => (this._columnMenuIndex = undefined);
+
+ @action
+ openFilterMenu = (index: number) => {
+ this._filterColumnIndex = index;
+ this._filterSearchValue = '';
+ };
+
+ @action
+ closeFilterMenu = () => {
+ this._filterColumnIndex = undefined;
+ };
+
+ openContextMenu = (x: number, y: number, index: number) => {
+ this.closeColumnMenu();
+ this.closeFilterMenu();
+ ContextMenu.Instance.clearItems();
+ ContextMenu.Instance.addItem({
+ description: 'Change field',
+ event: () => this.openColumnMenu(index, false),
+ icon: 'pencil-alt',
+ });
+ ContextMenu.Instance.addItem({
+ description: 'Filter field',
+ event: () => this.openFilterMenu(index),
+ icon: 'filter',
});
- this.columns = columns;
+ ContextMenu.Instance.addItem({
+ description: 'Delete column',
+ event: () => this.removeColumn(index),
+ icon: 'trash',
+ });
+ ContextMenu.Instance.displayMenu(x, y, undefined, false);
};
@action
- setColumns = (columns: SchemaHeaderField[]) => (this.columns = columns);
+ updateKeySearch = (e: React.ChangeEvent<HTMLInputElement>) => {
+ this._menuValue = e.target.value;
+ this._menuKeys = this.documentKeys.filter(value => value.toLowerCase().includes(this._menuValue.toLowerCase()));
+ };
- @undoBatch
- reorderColumns = (toMove: SchemaHeaderField, relativeTo: SchemaHeaderField, before: boolean, columnsValues: SchemaHeaderField[]) => {
- const columns = [...columnsValues];
- const oldIndex = columns.indexOf(toMove);
- const relIndex = columns.indexOf(relativeTo);
- const newIndex = oldIndex > relIndex && !before ? relIndex + 1 : oldIndex < relIndex && before ? relIndex - 1 : relIndex;
+ getFieldFilters = (field: string) => StrListCast(this.Document._childFilters).filter(filter => filter.split(':')[0] == field);
- if (oldIndex === newIndex) return;
+ removeFieldFilters = (field: string) => {
+ this.getFieldFilters(field).forEach(filter => Doc.setDocFilter(this.Document, field, filter.split(':')[1], 'remove'));
+ };
- columns.splice(newIndex, 0, columns.splice(oldIndex, 1)[0]);
- this.columns = columns;
+ onFilterKeyDown = (e: React.KeyboardEvent) => {
+ switch (e.key) {
+ case 'Enter':
+ case 'Escape':
+ this.closeFilterMenu();
+ break;
+ }
};
- onZoomMenu = (e: React.WheelEvent) => this.props.isContentActive(true) && e.stopPropagation();
+ @action
+ updateFilterSearch = (e: React.ChangeEvent<HTMLInputElement>) => (this._filterSearchValue = e.target.value);
- render() {
- TraceMobx();
- if (!this.props.isContentActive()) setTimeout(() => this.closeHeader(), 0);
- const menuContent = this.renderMenuContent;
- const menu = (
- <div className="collectionSchema-header-menu" onWheel={e => this.onZoomMenu(e)} onPointerDown={e => this.onHeaderClick(e)} style={{ transform: `translate(${this.menuCoordinates[0]}px, ${this.menuCoordinates[1]}px)` }}>
- <Measure
- offset
- onResize={action((r: any) => {
- const dim = this.props.ScreenToLocalTransform().inverse().transformDirection(r.offset.width, r.offset.height);
- this._menuWidth = dim[0];
- this._menuHeight = dim[1];
+ @computed get newFieldMenu() {
+ return (
+ <div className="schema-new-key-options">
+ <div className="schema-key-type-option">
+ <input
+ type="radio"
+ name="newFieldType"
+ checked={this._newFieldType == ColumnType.Number}
+ onChange={action(() => {
+ this._newFieldType = ColumnType.Number;
+ this._newFieldDefault = 0;
+ })}
+ />
+ number
+ </div>
+ <div className="schema-key-type-option">
+ <input
+ type="radio"
+ name="newFieldType"
+ checked={this._newFieldType == ColumnType.Boolean}
+ onChange={action(() => {
+ this._newFieldType = ColumnType.Boolean;
+ this._newFieldDefault = false;
+ })}
+ />
+ boolean
+ </div>
+ <div className="schema-key-type-option">
+ <input
+ type="radio"
+ name="newFieldType"
+ checked={this._newFieldType == ColumnType.String}
+ onChange={action(() => {
+ this._newFieldType = ColumnType.String;
+ this._newFieldDefault = '';
+ })}
+ />
+ string
+ </div>
+ <div className="schema-key-default-val">value: {this.fieldDefaultInput}</div>
+ <div className="schema-key-warning">{this._newFieldWarning}</div>
+ <div
+ className="schema-column-menu-button"
+ onPointerDown={action(e => {
+ if (this.documentKeys.includes(this._menuValue)) {
+ this._newFieldWarning = 'Field already exists';
+ } else if (this._menuValue.length === 0) {
+ this._newFieldWarning = 'Field cannot be an empty string';
+ } else {
+ this.setKey(this._menuValue, this._newFieldDefault);
+ }
})}>
- {({ measureRef }) => <div ref={measureRef}> {menuContent} </div>}
- </Measure>
+ done
+ </div>
+ </div>
+ );
+ }
+
+ 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">
+ <div
+ className="schema-column-menu-button"
+ onPointerDown={action(e => {
+ e.stopPropagation();
+ this._makeNewField = true;
+ })}>
+ + new field
+ </div>
+ <div
+ className="schema-key-list"
+ 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"
+ onPointerDown={e => {
+ e.stopPropagation();
+ this.setKey(key);
+ }}>
+ <p>
+ <span className="schema-search-result-key">
+ {key}
+ {this.fieldInfos.get(key)!.fieldType ? ', ' : ''}
+ </span>
+ <span className="schema-search-result-type" style={{ color: this.fieldInfos.get(key)!.readOnly ? 'red' : 'inherit' }}>
+ {this.fieldInfos.get(key)!.fieldType}
+ </span>
+ </p>
+ <p className="schema-search-result-desc">{this.fieldInfos.get(key)!.description}</p>
+ </div>
+ ))}
+ </div>
</div>
);
+ }
+
+ @computed get renderColumnMenu() {
+ const x = this._columnMenuIndex! == -1 ? 0 : this.displayColumnWidths.reduce((total, curr, index) => total + (index < this._columnMenuIndex! ? curr : 0), CollectionSchemaView._rowMenuWidth);
return (
- <div
- className={'collectionSchemaView' + (this.props.Document._searchDoc ? '-searchContainer' : '-container')}
- style={{
- overflow: this.props.scrollOverflow === true ? 'scroll' : undefined,
- backgroundColor: 'white',
- pointerEvents: this.props.Document._searchDoc !== undefined && !this.props.isContentActive() && !SnappingManager.GetIsDragging() ? 'none' : undefined,
- width: this.props.PanelWidth() || '100%',
- height: this.props.PanelHeight() || '100%',
- position: 'relative',
- }}>
+ <div className="schema-column-menu" style={{ left: x, minWidth: CollectionSchemaView._minColWidth }}>
+ <input className="schema-key-search-input" type="text" value={this._menuValue} onKeyDown={this.onSearchKeyDown} onChange={this.updateKeySearch} onPointerDown={e => e.stopPropagation()} />
+ {this._makeNewField ? this.newFieldMenu : this.keysDropdown}
<div
- className="collectionSchemaView-tableContainer"
- style={{ width: `calc(100% - ${this.previewWidth()}px)` }}
- onContextMenu={this.onSpecificMenu}
- onPointerDown={this.onPointerDown}
- onWheel={e => this.props.isContentActive(true) && e.stopPropagation()}
- onDrop={e => this.onExternalDrop(e, {})}
- ref={this.createTarget}>
- {this.schemaTable}
+ className="schema-column-menu-button"
+ onPointerDown={action(e => {
+ e.stopPropagation();
+ this.closeColumnMenu();
+ })}>
+ cancel
</div>
- {this.dividerDragger}
- {!this.previewWidth() ? null : this.previewPanel}
- {this._headerOpen && this.props.isContentActive() ? menu : null}
+ </div>
+ );
+ }
+
+ @computed get renderFilterOptions() {
+ const keyOptions: string[] = [];
+ const columnKey = this.columnKeys[this._filterColumnIndex!];
+ const allDocs = DocListCast(this.dataDoc[this.props.fieldKey]);
+ allDocs.forEach(doc => {
+ const value = StrCast(doc[columnKey]);
+ if (!keyOptions.includes(value) && value !== '' && (this._filterSearchValue === '' || value.includes(this._filterSearchValue))) {
+ keyOptions.push(value);
+ }
+ });
+
+ const filters = StrListCast(this.Document._childFilters);
+ return keyOptions.map(key => {
+ let bool = false;
+ if (filters !== undefined) {
+ const ind = filters.findIndex(filter => filter.split(':')[1] === key);
+ const fields = ind === -1 ? undefined : filters[ind].split(':');
+ bool = fields ? fields[2] === 'check' : false;
+ }
+ return (
+ <div key={key} className="schema-filter-option">
+ <input
+ type="checkbox"
+ onPointerDown={e => e.stopPropagation()}
+ onClick={e => e.stopPropagation()}
+ onChange={action(e => {
+ if (e.target.checked) {
+ Doc.setDocFilter(this.props.Document, columnKey, key, 'check');
+ } else {
+ Doc.setDocFilter(this.props.Document, columnKey, key, 'remove');
+ }
+ })}
+ checked={bool}
+ />
+ <span style={{ paddingLeft: 4 }}>{key}</span>
+ </div>
+ );
+ });
+ }
+
+ @computed get renderFilterMenu() {
+ const x = this.displayColumnWidths.reduce((total, curr, index) => total + (index < this._filterColumnIndex! ? curr : 0), CollectionSchemaView._rowMenuWidth);
+ return (
+ <div className="schema-filter-menu" style={{ left: x, minWidth: CollectionSchemaView._minColWidth }}>
+ <input className="schema-filter-input" type="text" value={this._filterSearchValue} onKeyDown={this.onFilterKeyDown} onChange={this.updateFilterSearch} onPointerDown={e => e.stopPropagation()} />
+ {this.renderFilterOptions}
+ <div
+ className="schema-column-menu-button"
+ onPointerDown={action(e => {
+ e.stopPropagation();
+ this.closeFilterMenu();
+ })}>
+ done
+ </div>
+ </div>
+ );
+ }
+
+ @computed get sortedDocs() {
+ trace();
+ const field = StrCast(this.layoutDoc.sortField);
+ const desc = BoolCast(this.layoutDoc.sortDesc);
+ const docs = !field
+ ? this.childDocs
+ : [...this.childDocs].sort((docA, docB) => {
+ const aStr = Field.toString(docA[field] as Field);
+ const bStr = Field.toString(docB[field] as Field);
+ var out = 0;
+ if (aStr < bStr) out = -1;
+ if (aStr > bStr) out = 1;
+ if (desc) out *= -1;
+ return out;
+ });
+ return { docs };
+ }
+ rowHeightFunc = () => (BoolCast(this.layoutDoc._schema_singleLine) ? CollectionSchemaView._rowSingleLineHeight : CollectionSchemaView._rowHeight);
+ sortedDocsFunc = () => this.sortedDocs;
+ isContentActive = () => this.props.isSelected() || this.props.isContentActive();
+ screenToLocal = () => this.props.ScreenToLocalTransform().translate(-this.tableWidth, 0);
+ previewWidthFunc = () => this.previewWidth;
+ 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()}
+ ref={r => {
+ // prevent wheel events from passively propagating up through containers
+ r?.addEventListener('wheel', (e: WheelEvent) => {}, { passive: false });
+ }}>
+ <div className="schema-header-row" style={{ height: this.rowHeightFunc() }}>
+ <div className="row-menu" style={{ width: CollectionSchemaView._rowMenuWidth }}>
+ <div className="schema-header-button" onPointerDown={e => (this._columnMenuIndex === -1 ? this.closeColumnMenu() : this.openColumnMenu(-1, true))}>
+ <FontAwesomeIcon icon="plus" />
+ </div>
+ </div>
+ {this.columnKeys.map((key, index) => (
+ <SchemaColumnHeader
+ key={index}
+ columnIndex={index}
+ columnKeys={this.columnKeys}
+ columnWidths={this.displayColumnWidths}
+ sortField={this.sortField}
+ sortDesc={this.sortDesc}
+ 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={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 && (
+ <div style={{ width: `${this.previewWidth}px` }} ref={ref => (this._previewRef = ref)}>
+ {Array.from(this._selectedDocs).lastElement() && (
+ <DocumentView
+ Document={Array.from(this._selectedDocs).lastElement()}
+ DataDoc={undefined}
+ fitContentsToBox={returnTrue}
+ dontCenter={'y'}
+ onClickScriptDisable="always"
+ focus={emptyFunction}
+ defaultDoubleClick={returnIgnore}
+ renderDepth={this.props.renderDepth + 1}
+ rootSelected={this.rootSelected}
+ PanelWidth={this.previewWidthFunc}
+ PanelHeight={this.props.PanelHeight}
+ isContentActive={returnTrue}
+ isDocumentActive={returnFalse}
+ ScreenToLocalTransform={this.screenToLocal}
+ childFilters={this.childDocFilters}
+ childFiltersByRanges={this.childDocRangeFilters}
+ searchFilterDocs={this.searchFilterDocs}
+ styleProvider={DefaultStyleProvider}
+ docViewPath={returnEmptyDoclist}
+ moveDocument={this.props.moveDocument}
+ addDocument={this.addRow}
+ removeDocument={this.props.removeDocument}
+ whenChildContentsActiveChanged={returnFalse}
+ addDocTab={this.props.addDocTab}
+ pinToPres={this.props.pinToPres}
+ bringToFront={returnFalse}
+ />
+ )}
+ </div>
+ )}
+ </div>
+ );
+ }
+}
+
+interface CollectionSchemaViewDocsProps {
+ schema: CollectionSchemaView;
+ setRef: (ref: HTMLDivElement | null) => void;
+ childDocs: () => { docs: Doc[] };
+ rowHeight: () => number;
+}
+
+@observer
+class CollectionSchemaViewDocs extends React.Component<CollectionSchemaViewDocsProps> {
+ tableWidthFunc = () => this.props.schema.tableWidth;
+ childScreenToLocal = computedFn((index: number) => () => this.props.schema.props.ScreenToLocalTransform().translate(0, -this.props.rowHeight() - index * this.props.rowHeight()));
+ render() {
+ return (
+ <div className="schema-table-content" ref={this.props.setRef} style={{ height: `calc(100% - ${CollectionSchemaView._newNodeInputHeight + this.props.rowHeight()}px)` }}>
+ {this.props.childDocs().docs.map((doc: Doc, index: number) => {
+ const dataDoc = !doc.isTemplateDoc && !doc.isTemplateForField ? undefined : this.props.schema.props.DataDoc;
+ return (
+ <div className="schema-row-wrapper" style={{ height: this.props.rowHeight() }}>
+ <DocumentView
+ key={doc[Id]}
+ {...this.props.schema.props}
+ LayoutTemplate={this.props.schema.props.childLayoutTemplate}
+ 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}
+ dragAction="move"
+ onClickScriptDisable="always"
+ focus={this.props.schema.focusDocument}
+ childFilters={this.props.schema.childDocFilters}
+ childFiltersByRanges={this.props.schema.childDocRangeFilters}
+ searchFilterDocs={this.props.schema.searchFilterDocs}
+ rootSelected={this.props.schema.rootSelected}
+ ScreenToLocalTransform={this.childScreenToLocal(index)}
+ bringToFront={emptyFunction}
+ isDocumentActive={this.props.schema.props.childDocumentsActive?.() ? this.props.schema.props.isDocumentActive : this.props.schema.isContentActive}
+ isContentActive={emptyFunction}
+ whenChildContentsActiveChanged={this.props.schema.props.whenChildContentsActiveChanged}
+ hideDecorations={true}
+ hideTitle={true}
+ hideDocumentButtonBar={true}
+ hideLinkAnchors={true}
+ layout_fitWidth={returnTrue}
+ scriptContext={this}
+ />
+ </div>
+ );
+ })}
</div>
);
}
diff --git a/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx b/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx
new file mode 100644
index 000000000..65e47f441
--- /dev/null
+++ b/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx
@@ -0,0 +1,79 @@
+import React = require('react');
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { action, computed, observable } from 'mobx';
+import { observer } from 'mobx-react';
+import { emptyFunction, setupMoveUpEvents } from '../../../../Utils';
+import { Colors } from '../../global/globalEnums';
+import './CollectionSchemaView.scss';
+
+export interface SchemaColumnHeaderProps {
+ columnKeys: string[];
+ columnWidths: number[];
+ 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;
+ resizeColumn: (e: any, index: number) => void;
+ dragColumn: (e: any, index: number) => boolean;
+ openContextMenu: (x: number, y: number, index: number) => void;
+ setColRef: (index: number, ref: HTMLDivElement) => void;
+}
+
+@observer
+export class SchemaColumnHeader extends React.Component<SchemaColumnHeaderProps> {
+ @observable _ref: HTMLDivElement | null = null;
+
+ @computed get fieldKey() {
+ return this.props.columnKeys[this.props.columnIndex];
+ }
+
+ @action
+ sortClicked = (e: React.PointerEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
+ if (this.props.sortField == this.fieldKey && this.props.sortDesc) {
+ this.props.setSort(undefined);
+ } else if (this.props.sortField == this.fieldKey) {
+ this.props.setSort(this.fieldKey, true);
+ } else {
+ this.props.setSort(this.fieldKey, false);
+ }
+ };
+
+ @action
+ onPointerDown = (e: React.PointerEvent) => {
+ this.props.isContentActive(true) && setupMoveUpEvents(this, e, e => this.props.dragColumn(e, this.props.columnIndex), emptyFunction, emptyFunction, false);
+ };
+
+ render() {
+ return (
+ <div
+ className="schema-column-header"
+ style={{
+ width: this.props.columnWidths[this.props.columnIndex],
+ }}
+ onPointerDown={this.onPointerDown}
+ ref={col => {
+ if (col) {
+ this._ref = col;
+ this.props.setColRef(this.props.columnIndex, col);
+ }
+ }}>
+ <div className="schema-column-resizer left" onPointerDown={e => this.props.resizeColumn(e, this.props.columnIndex)}></div>
+ <div className="schema-column-title">{this.fieldKey}</div>
+
+ <div className="schema-header-menu">
+ <div className="schema-header-button" onPointerDown={e => this.props.openContextMenu(e.clientX, e.clientY, this.props.columnIndex)}>
+ <FontAwesomeIcon icon="ellipsis-h" />
+ </div>
+ <div className="schema-sort-button" onPointerDown={this.sortClicked} style={this.props.sortField == this.fieldKey ? { backgroundColor: Colors.MEDIUM_BLUE } : {}}>
+ <FontAwesomeIcon icon="caret-right" style={this.props.sortField == this.fieldKey ? { transform: `rotate(${this.props.sortDesc ? '270deg' : '90deg'})` } : {}} />
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/collections/collectionSchema/SchemaRowBox.tsx b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx
new file mode 100644
index 000000000..e8e606030
--- /dev/null
+++ b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx
@@ -0,0 +1,160 @@
+import React = require('react');
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { computed } from 'mobx';
+import { observer } from 'mobx-react';
+import { computedFn } from 'mobx-utils';
+import { Doc } from '../../../../fields/Doc';
+import { BoolCast } from '../../../../fields/Types';
+import { DragManager } from '../../../util/DragManager';
+import { SnappingManager } from '../../../util/SnappingManager';
+import { undoable } from '../../../util/UndoManager';
+import { ViewBoxBaseComponent } from '../../DocComponent';
+import { Colors } from '../../global/globalEnums';
+import { OpenWhere } from '../../nodes/DocumentView';
+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>() {
+ public static LayoutString(fieldKey: string) {
+ return FieldView.LayoutString(SchemaRowBox, fieldKey);
+ }
+
+ private _ref: HTMLDivElement | null = null;
+
+ bounds = () => this._ref?.getBoundingClientRect();
+
+ @computed get schemaView() {
+ return this.props.DocumentView?.().props.docViewPath().lastElement()?.ComponentView as CollectionSchemaView;
+ }
+
+ @computed get schemaDoc() {
+ return this.props.DocumentView?.().props.docViewPath().lastElement()?.rootDoc;
+ }
+
+ @computed get rowIndex() {
+ return this.schemaView?.rowIndex(this.rootDoc) ?? -1;
+ }
+
+ componentDidMount(): void {
+ this.props.setContentView?.(this);
+ }
+
+ select = (ctrlKey: boolean, shiftKey: boolean) => {
+ if (!this.schemaView) return;
+ const lastSelected = Array.from(this.schemaView._selectedDocs).lastElement();
+ if (shiftKey && lastSelected) this.schemaView.selectRows(this.rootDoc, lastSelected);
+ else {
+ this.props.select?.(ctrlKey);
+ }
+ };
+
+ onPointerEnter = (e: any) => {
+ if (SnappingManager.GetIsDragging() && this.props.isContentActive()) {
+ document.removeEventListener('pointermove', this.onPointerMove);
+ document.addEventListener('pointermove', this.onPointerMove);
+ }
+ };
+
+ onPointerMove = (e: any) => {
+ const dragIsRow = DragManager.docsBeingDragged.some(doc => doc.embedContainer === this.schemaDoc); // this.schemaView?._selectedDocs.has(doc) ?? false;
+
+ if (this._ref && dragIsRow) {
+ const rect = this._ref.getBoundingClientRect();
+ const y = e.clientY - rect.top; //y position within the element.
+ const height = this._ref.clientHeight;
+ const halfLine = height / 2;
+ if (y <= halfLine) {
+ this._ref.style.borderTop = `solid 2px ${Colors.MEDIUM_BLUE}`;
+ this._ref.style.borderBottom = '0px';
+ this.schemaView?.setDropIndex(this.rowIndex);
+ } else if (y > halfLine) {
+ this._ref.style.borderTop = '0px';
+ this._ref.style.borderBottom = `solid 2px ${Colors.MEDIUM_BLUE}`;
+ this.schemaView?.setDropIndex(this.rowIndex + 1);
+ }
+ }
+ };
+
+ onPointerLeave = (e: any) => {
+ if (this._ref) {
+ this._ref.style.borderTop = '0px';
+ this._ref.style.borderBottom = '0px';
+ }
+ document.removeEventListener('pointermove', this.onPointerMove);
+ };
+
+ getFinfo = computedFn((fieldKey: string) => this.schemaView?.fieldInfos.get(fieldKey));
+ selectCell = (doc: Doc, col: number) => this.schemaView?.selectCell(doc, col);
+ deselectCell = () => this.schemaView?.deselectCell();
+ selectedCell = () => this.schemaView?._selectedCell;
+ setColumnValues = (field: any, value: any) => this.schemaView?.setColumnValues(field, value) ?? false;
+ columnWidth = computedFn((index: number) => () => this.schemaView?.displayColumnWidths[index] ?? CollectionSchemaView._minColWidth);
+ render() {
+ return (
+ <div
+ className="schema-row"
+ style={{ height: this.props.PanelHeight(), backgroundColor: this.props.isSelected() ? Colors.LIGHT_BLUE : undefined }}
+ onPointerEnter={this.onPointerEnter}
+ onPointerLeave={this.onPointerLeave}
+ ref={(row: HTMLDivElement | null) => {
+ row && this.schemaView?.addRowRef?.(this.rootDoc, row);
+ this._ref = row;
+ }}>
+ <div
+ className="row-menu"
+ style={{
+ width: CollectionSchemaView._rowMenuWidth,
+ pointerEvents: !this.props.isContentActive() ? 'none' : undefined,
+ }}>
+ <div
+ className="schema-row-button"
+ onPointerDown={undoable(e => {
+ e.stopPropagation();
+ this.props.removeDocument?.(this.rootDoc);
+ }, 'Delete Row')}>
+ <FontAwesomeIcon icon="times" />
+ </div>
+ <div
+ className="schema-row-button"
+ onPointerDown={undoable(e => {
+ e.stopPropagation();
+ this.props.addDocTab(this.rootDoc, OpenWhere.addRight);
+ }, 'Open Doc on Right')}>
+ <FontAwesomeIcon icon="external-link-alt" />
+ </div>
+ </div>
+ <div className="row-cells">
+ {this.schemaView?.columnKeys?.map((key, index) => (
+ <SchemaTableCell
+ key={key}
+ Document={this.rootDoc}
+ col={index}
+ fieldKey={key}
+ allowCRs={false} // to enter text with new lines, must use \n
+ columnWidth={this.columnWidth(index)}
+ rowHeight={this.schemaView.rowHeightFunc}
+ isRowActive={this.props.isContentActive}
+ getFinfo={this.getFinfo}
+ selectCell={this.selectCell}
+ deselectCell={this.deselectCell}
+ 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>
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/collections/collectionSchema/SchemaTable.tsx b/src/client/views/collections/collectionSchema/SchemaTable.tsx
deleted file mode 100644
index fafea5ce3..000000000
--- a/src/client/views/collections/collectionSchema/SchemaTable.tsx
+++ /dev/null
@@ -1,693 +0,0 @@
-import { IconProp } from '@fortawesome/fontawesome-svg-core';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, observable } from 'mobx';
-import { observer } from 'mobx-react';
-import * as React from 'react';
-import ReactTable, { CellInfo, Column, ComponentPropsGetterR, Resize, SortingRule } from 'react-table';
-import { DateField } from '../../../../fields/DateField';
-import { AclPrivate, AclReadonly, DataSym, Doc, DocListCast, Field, Opt } from '../../../../fields/Doc';
-import { Id } from '../../../../fields/FieldSymbols';
-import { List } from '../../../../fields/List';
-import { listSpec } from '../../../../fields/Schema';
-import { SchemaHeaderField } from '../../../../fields/SchemaHeaderField';
-import { ComputedField } from '../../../../fields/ScriptField';
-import { Cast, FieldValue, NumCast, StrCast } from '../../../../fields/Types';
-import { ImageField } from '../../../../fields/URLField';
-import { GetEffectiveAcl } from '../../../../fields/util';
-import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from '../../../../Utils';
-import { Docs, DocumentOptions, DocUtils } from '../../../documents/Documents';
-import { DocumentType } from '../../../documents/DocumentTypes';
-import { CompileScript, Transformer, ts } from '../../../util/Scripting';
-import { Transform } from '../../../util/Transform';
-import { undoBatch } from '../../../util/UndoManager';
-import '../../../views/DocumentDecorations.scss';
-import { ContextMenu } from '../../ContextMenu';
-import { COLLECTION_BORDER_WIDTH, SCHEMA_DIVIDER_WIDTH } from '../../global/globalCssVariables.scss';
-import { DocumentView } from '../../nodes/DocumentView';
-import { DefaultStyleProvider } from '../../StyleProvider';
-import { CollectionView } from '../CollectionView';
-import {
- CellProps,
- CollectionSchemaButtons,
- CollectionSchemaCell,
- CollectionSchemaCheckboxCell,
- CollectionSchemaDateCell,
- CollectionSchemaDocCell,
- CollectionSchemaImageCell,
- CollectionSchemaListCell,
- CollectionSchemaNumberCell,
- CollectionSchemaStringCell,
-} from './CollectionSchemaCells';
-import { CollectionSchemaAddColumnHeader, KeysDropdown } from './CollectionSchemaHeaders';
-import { MovableColumn } from './CollectionSchemaMovableColumn';
-import { MovableRow } from './CollectionSchemaMovableRow';
-import './CollectionSchemaView.scss';
-
-enum ColumnType {
- Any,
- Number,
- String,
- Boolean,
- Doc,
- Image,
- List,
- Date,
-}
-
-// this map should be used for keys that should have a const type of value
-const columnTypes: Map<string, ColumnType> = new Map([
- ['title', ColumnType.String],
- ['x', ColumnType.Number],
- ['y', ColumnType.Number],
- ['_width', ColumnType.Number],
- ['_height', ColumnType.Number],
- ['_nativeWidth', ColumnType.Number],
- ['_nativeHeight', ColumnType.Number],
- ['isPrototype', ColumnType.Boolean],
- ['_curPage', ColumnType.Number],
- ['_currentTimecode', ColumnType.Number],
- ['zIndex', ColumnType.Number],
-]);
-
-export interface SchemaTableProps {
- Document: Doc; // child doc
- dataDoc?: Doc;
- PanelHeight: () => number;
- PanelWidth: () => number;
- childDocs?: Doc[];
- CollectionView: Opt<CollectionView>;
- ContainingCollectionView: Opt<CollectionView>;
- ContainingCollectionDoc: Opt<Doc>;
- fieldKey: string;
- renderDepth: number;
- deleteDocument?: (document: Doc | Doc[]) => boolean;
- addDocument?: (document: Doc | Doc[]) => boolean;
- moveDocument?: (document: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => boolean;
- ScreenToLocalTransform: () => Transform;
- active: (outsideReaction: boolean | undefined) => boolean | undefined;
- onDrop: (e: React.DragEvent<Element>, options: DocumentOptions, completed?: (() => void) | undefined) => void;
- addDocTab: (document: Doc, where: string) => boolean;
- pinToPres: (document: Doc) => void;
- isSelected: (outsideReaction?: boolean) => boolean;
- isFocused: (document: Doc, outsideReaction: boolean) => boolean;
- setFocused: (document: Doc) => void;
- setPreviewDoc: (document: Opt<Doc>) => void;
- columns: SchemaHeaderField[];
- documentKeys: any[];
- headerIsEditing: boolean;
- openHeader: (column: any, screenx: number, screeny: number) => void;
- onClick: (e: React.MouseEvent) => void;
- onPointerDown: (e: React.PointerEvent) => void;
- onResizedChange: (newResized: Resize[], event: any) => void;
- setColumns: (columns: SchemaHeaderField[]) => void;
- reorderColumns: (toMove: SchemaHeaderField, relativeTo: SchemaHeaderField, before: boolean, columnsValues: SchemaHeaderField[]) => void;
- changeColumns: (oldKey: string, newKey: string, addNew: boolean) => void;
- setHeaderIsEditing: (isEditing: boolean) => void;
- changeColumnSort: (columnField: SchemaHeaderField, descending: boolean | undefined) => void;
-}
-
-@observer
-export class SchemaTable extends React.Component<SchemaTableProps> {
- @observable _cellIsEditing: boolean = false;
- @observable _focusedCell: { row: number; col: number } = { row: 0, col: 0 };
- @observable _openCollections: Set<number> = new Set();
-
- @observable _showDoc: Doc | undefined;
- @observable _showDataDoc: any = '';
- @observable _showDocPos: number[] = [];
-
- @observable _showTitleDropdown: boolean = false;
-
- @computed get previewWidth() {
- return () => NumCast(this.props.Document.schemaPreviewWidth);
- }
- @computed get previewHeight() {
- return () => this.props.PanelHeight() - 2 * this.borderWidth;
- }
- @computed get tableWidth() {
- return this.props.PanelWidth() - 2 * this.borderWidth - Number(SCHEMA_DIVIDER_WIDTH) - this.previewWidth();
- }
-
- @computed get childDocs() {
- if (this.props.childDocs) return this.props.childDocs;
-
- const doc = this.props.dataDoc ? this.props.dataDoc : this.props.Document;
- return DocListCast(doc[this.props.fieldKey]);
- }
- set childDocs(docs: Doc[]) {
- const doc = this.props.dataDoc ? this.props.dataDoc : this.props.Document;
- doc[this.props.fieldKey] = new List<Doc>(docs);
- }
-
- @computed get textWrappedRows() {
- return Cast(this.props.Document.textwrappedSchemaRows, listSpec('string'), []);
- }
- set textWrappedRows(textWrappedRows: string[]) {
- this.props.Document.textwrappedSchemaRows = new List<string>(textWrappedRows);
- }
-
- @computed get resized(): { id: string; value: number }[] {
- return this.props.columns.reduce((resized, shf) => {
- shf.width > -1 && resized.push({ id: shf.heading, value: shf.width });
- return resized;
- }, [] as { id: string; value: number }[]);
- }
- @computed get sorted(): SortingRule[] {
- return this.props.columns.reduce((sorted, shf) => {
- shf.desc !== undefined && sorted.push({ id: shf.heading, desc: shf.desc });
- return sorted;
- }, [] as SortingRule[]);
- }
-
- @action
- changeSorting = (col: any) => {
- this.props.changeColumnSort(col, col.desc === true ? false : col.desc === false ? undefined : true);
- };
-
- @action
- changeTitleMode = () => (this._showTitleDropdown = !this._showTitleDropdown);
-
- @computed get borderWidth() {
- return Number(COLLECTION_BORDER_WIDTH);
- }
- @computed get tableColumns(): Column<Doc>[] {
- const possibleKeys = this.props.documentKeys.filter(key => this.props.columns.findIndex(existingKey => existingKey.heading.toUpperCase() === key.toUpperCase()) === -1);
- const columns: Column<Doc>[] = [];
- const tableIsFocused = this.props.isFocused(this.props.Document, false);
- const focusedRow = this._focusedCell.row;
- const focusedCol = this._focusedCell.col;
- const isEditable = !this.props.headerIsEditing;
-
- columns.push({
- expander: true,
- Header: '',
- width: 58,
- Expander: rowInfo => {
- return rowInfo.original.type !== DocumentType.COL ? null : (
- <div className="collectionSchemaView-expander" onClick={action(() => this._openCollections[rowInfo.isExpanded ? 'delete' : 'add'](rowInfo.viewIndex))}>
- <FontAwesomeIcon icon={rowInfo.isExpanded ? 'caret-down' : 'caret-right'} size="lg" />
- </div>
- );
- },
- });
- columns.push(
- ...this.props.columns.map(col => {
- const icon: IconProp =
- this.getColumnType(col) === ColumnType.Number
- ? 'hashtag'
- : this.getColumnType(col) === ColumnType.String
- ? 'font'
- : this.getColumnType(col) === ColumnType.Boolean
- ? 'check-square'
- : this.getColumnType(col) === ColumnType.Doc
- ? 'file'
- : this.getColumnType(col) === ColumnType.Image
- ? 'image'
- : this.getColumnType(col) === ColumnType.List
- ? 'list-ul'
- : this.getColumnType(col) === ColumnType.Date
- ? 'calendar'
- : 'align-justify';
-
- const keysDropdown = (
- <KeysDropdown
- keyValue={col.heading}
- possibleKeys={possibleKeys}
- existingKeys={this.props.columns.map(c => c.heading)}
- canAddNew={true}
- addNew={false}
- onSelect={this.props.changeColumns}
- setIsEditing={this.props.setHeaderIsEditing}
- docs={this.props.childDocs}
- Document={this.props.Document}
- dataDoc={this.props.dataDoc}
- fieldKey={this.props.fieldKey}
- ContainingCollectionDoc={this.props.ContainingCollectionDoc}
- ContainingCollectionView={this.props.ContainingCollectionView}
- active={this.props.active}
- openHeader={this.props.openHeader}
- icon={icon}
- col={col}
- // try commenting this out
- width={'100%'}
- />
- );
-
- const sortIcon = col.desc === undefined ? 'caret-right' : col.desc === true ? 'caret-down' : 'caret-up';
- const header = (
- <div className="collectionSchemaView-menuOptions-wrapper" style={{ background: col.color, padding: '2px', display: 'flex', cursor: 'default', height: '100%' }}>
- {keysDropdown}
- <div onClick={e => this.changeSorting(col)} style={{ width: 21, padding: 1, display: 'inline', zIndex: 1, background: 'inherit', cursor: 'pointer' }}>
- <FontAwesomeIcon icon={sortIcon} size="lg" />
- </div>
- {/* {this.props.Document._chromeHidden || this.props.addDocument == returnFalse ? undefined : <div className="collectionSchemaView-addRow" onClick={this.createRow}>+ new</div>} */}
- </div>
- );
-
- return {
- Header: <MovableColumn columnRenderer={header} columnValue={col} allColumns={this.props.columns} reorderColumns={this.props.reorderColumns} ScreenToLocalTransform={this.props.ScreenToLocalTransform} />,
- accessor: (doc: Doc) => (doc ? Field.toString(doc[col.heading] as Field) : 0),
- id: col.heading,
- Cell: (rowProps: CellInfo) => {
- const rowIndex = rowProps.index;
- const columnIndex = this.props.columns.map(c => c.heading).indexOf(rowProps.column.id!);
- const isFocused = focusedRow === rowIndex && focusedCol === columnIndex && tableIsFocused;
-
- const props: CellProps = {
- row: rowIndex,
- col: columnIndex,
- rowProps: rowProps,
- isFocused: isFocused,
- changeFocusedCellByIndex: this.changeFocusedCellByIndex,
- CollectionView: this.props.CollectionView,
- ContainingCollection: this.props.ContainingCollectionView,
- Document: this.props.Document,
- fieldKey: this.props.fieldKey,
- renderDepth: this.props.renderDepth,
- addDocTab: this.props.addDocTab,
- pinToPres: this.props.pinToPres,
- moveDocument: this.props.moveDocument,
- setIsEditing: this.setCellIsEditing,
- isEditable: isEditable,
- setPreviewDoc: this.props.setPreviewDoc,
- setComputed: this.setComputed,
- getField: this.getField,
- showDoc: this.showDoc,
- };
-
- switch (this.getColumnType(col, rowProps.original, rowProps.column.id)) {
- case ColumnType.Number:
- return <CollectionSchemaNumberCell {...props} />;
- case ColumnType.String:
- return <CollectionSchemaStringCell {...props} />;
- case ColumnType.Boolean:
- return <CollectionSchemaCheckboxCell {...props} />;
- case ColumnType.Doc:
- return <CollectionSchemaDocCell {...props} />;
- case ColumnType.Image:
- return <CollectionSchemaImageCell {...props} />;
- case ColumnType.List:
- return <CollectionSchemaListCell {...props} />;
- case ColumnType.Date:
- return <CollectionSchemaDateCell {...props} />;
- default:
- return <CollectionSchemaCell {...props} />;
- }
- },
- minWidth: 200,
- };
- })
- );
- columns.push({
- Header: <CollectionSchemaAddColumnHeader createColumn={this.createColumn} />,
- accessor: (doc: Doc) => 0,
- id: 'add',
- Cell: (rowProps: CellInfo) => {
- const rowIndex = rowProps.index;
- const columnIndex = this.props.columns.map(c => c.heading).indexOf(rowProps.column.id!);
- const isFocused = focusedRow === rowIndex && focusedCol === columnIndex && tableIsFocused;
- return (
- <CollectionSchemaButtons
- {...{
- row: rowProps.index,
- col: columnIndex,
- rowProps: rowProps,
- isFocused: isFocused,
- changeFocusedCellByIndex: this.changeFocusedCellByIndex,
- CollectionView: this.props.CollectionView,
- ContainingCollection: this.props.ContainingCollectionView,
- Document: this.props.Document,
- fieldKey: this.props.fieldKey,
- renderDepth: this.props.renderDepth,
- addDocTab: this.props.addDocTab,
- pinToPres: this.props.pinToPres,
- moveDocument: this.props.moveDocument,
- setIsEditing: this.setCellIsEditing,
- isEditable: isEditable,
- setPreviewDoc: this.props.setPreviewDoc,
- setComputed: this.setComputed,
- getField: this.getField,
- showDoc: this.showDoc,
- }}
- />
- );
- },
- width: 28,
- resizable: false,
- });
- return columns;
- }
-
- constructor(props: SchemaTableProps) {
- super(props);
- if (this.props.Document._schemaHeaders === undefined) {
- this.props.Document._schemaHeaders = new List<SchemaHeaderField>([
- new SchemaHeaderField('title', '#f1efeb'),
- new SchemaHeaderField('author', '#f1efeb'),
- new SchemaHeaderField('*lastModified', '#f1efeb', ColumnType.Date),
- new SchemaHeaderField('text', '#f1efeb', ColumnType.String),
- new SchemaHeaderField('type', '#f1efeb'),
- new SchemaHeaderField('context', '#f1efeb', ColumnType.Doc),
- ]);
- }
- }
-
- componentDidMount() {
- document.addEventListener('keydown', this.onKeyDown);
- }
-
- componentWillUnmount() {
- document.removeEventListener('keydown', this.onKeyDown);
- }
-
- tableAddDoc = (doc: Doc, relativeTo?: Doc, before?: boolean) => {
- const tableDoc = this.props.Document[DataSym];
- const effectiveAcl = GetEffectiveAcl(tableDoc);
-
- if (effectiveAcl !== AclPrivate && effectiveAcl !== AclReadonly) {
- doc.context = this.props.Document;
- tableDoc[this.props.fieldKey + '-lastModified'] = new DateField(new Date(Date.now()));
- return Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, relativeTo, before);
- }
- return false;
- };
-
- private getTrProps: ComponentPropsGetterR = (state, rowInfo) => {
- return !rowInfo
- ? {}
- : {
- ScreenToLocalTransform: this.props.ScreenToLocalTransform,
- addDoc: this.tableAddDoc,
- removeDoc: this.props.deleteDocument,
- rowInfo,
- rowFocused: !this.props.headerIsEditing && rowInfo.index === this._focusedCell.row && this.props.isFocused(this.props.Document, true),
- textWrapRow: this.toggleTextWrapRow,
- rowWrapped: this.textWrappedRows.findIndex(id => rowInfo.original[Id] === id) > -1,
- dropAction: StrCast(this.props.Document.childDropAction),
- addDocTab: this.props.addDocTab,
- };
- };
-
- private getTdProps: ComponentPropsGetterR = (state, rowInfo, column, instance) => {
- if (!rowInfo || column) return {};
-
- const row = rowInfo.index;
- //@ts-ignore
- const col = this.columns.map(c => c.heading).indexOf(column!.id);
- const isFocused = this._focusedCell.row === row && this._focusedCell.col === col && this.props.isFocused(this.props.Document, true);
- // TODO: editing border doesn't work :(
- return {
- style: { border: !this.props.headerIsEditing && isFocused ? '2px solid rgb(255, 160, 160)' : '1px solid #f1efeb' },
- };
- };
-
- @action setCellIsEditing = (isEditing: boolean) => (this._cellIsEditing = isEditing);
-
- @action
- onKeyDown = (e: KeyboardEvent): void => {
- if (!this._cellIsEditing && !this.props.headerIsEditing && this.props.isFocused(this.props.Document, true)) {
- // && this.props.isSelected(true)) {
- const direction = e.key === 'Tab' ? 'tab' : e.which === 39 ? 'right' : e.which === 37 ? 'left' : e.which === 38 ? 'up' : e.which === 40 ? 'down' : '';
- this._focusedCell = this.changeFocusedCellByDirection(direction, this._focusedCell.row, this._focusedCell.col);
-
- if (direction) {
- const pdoc = FieldValue(this.childDocs[this._focusedCell.row]);
- pdoc && this.props.setPreviewDoc(pdoc);
- e.stopPropagation();
- }
- } else if (e.keyCode === 27) {
- this.props.setPreviewDoc(undefined);
- e.stopPropagation(); // stopPropagation for left/right arrows
- }
- };
-
- changeFocusedCellByDirection = (direction: string, curRow: number, curCol: number) => {
- switch (direction) {
- case 'tab':
- return { row: curRow + 1 === this.childDocs.length ? 0 : curRow + 1, col: curCol + 1 === this.props.columns.length ? 0 : curCol + 1 };
- case 'right':
- return { row: curRow, col: curCol + 1 === this.props.columns.length ? curCol : curCol + 1 };
- case 'left':
- return { row: curRow, col: curCol === 0 ? curCol : curCol - 1 };
- case 'up':
- return { row: curRow === 0 ? curRow : curRow - 1, col: curCol };
- case 'down':
- return { row: curRow + 1 === this.childDocs.length ? curRow : curRow + 1, col: curCol };
- }
- return this._focusedCell;
- };
-
- @action
- changeFocusedCellByIndex = (row: number, col: number): void => {
- if (this._focusedCell.row !== row || this._focusedCell.col !== col) {
- this._focusedCell = { row: row, col: col };
- }
- this.props.setFocused(this.props.Document);
- };
-
- @undoBatch
- createRow = action(() => {
- this.props.addDocument?.(Docs.Create.TextDocument('', { title: '', _width: 100, _height: 30 }));
- this._focusedCell = { row: this.childDocs.length, col: this._focusedCell.col };
- });
-
- @undoBatch
- @action
- createColumn = () => {
- const newFieldName = (index: number) => `New field${index ? ` (${index})` : ''}`;
- for (let index = 0; index < 100; index++) {
- if (this.props.columns.findIndex(col => col.heading === newFieldName(index)) === -1) {
- this.props.columns.push(new SchemaHeaderField(newFieldName(index), '#f1efeb'));
- break;
- }
- }
- };
-
- @action
- getColumnType = (column: SchemaHeaderField, doc?: Doc, field?: string): ColumnType => {
- if (doc && field && column.type === ColumnType.Any) {
- const val = doc[CollectionSchemaCell.resolvedFieldKey(field, doc)];
- if (val instanceof ImageField) return ColumnType.Image;
- if (val instanceof Doc) return ColumnType.Doc;
- if (val instanceof DateField) return ColumnType.Date;
- if (val instanceof List) return ColumnType.List;
- }
- if (column.type && column.type !== 0) {
- return column.type;
- }
- if (columnTypes.get(column.heading)) {
- return (column.type = columnTypes.get(column.heading)!);
- }
- return (column.type = ColumnType.Any);
- };
-
- @undoBatch
- @action
- toggleTextwrap = async () => {
- const textwrappedRows = Cast(this.props.Document.textwrappedSchemaRows, listSpec('string'), []);
- if (textwrappedRows.length) {
- this.props.Document.textwrappedSchemaRows = new List<string>([]);
- } else {
- const docs = DocListCast(this.props.Document[this.props.fieldKey]);
- const allRows = docs instanceof Doc ? [docs[Id]] : docs.map(doc => doc[Id]);
- this.props.Document.textwrappedSchemaRows = new List<string>(allRows);
- }
- };
-
- @action
- toggleTextWrapRow = (doc: Doc): void => {
- const textWrapped = this.textWrappedRows;
- const index = textWrapped.findIndex(id => doc[Id] === id);
-
- index > -1 ? textWrapped.splice(index, 1) : textWrapped.push(doc[Id]);
-
- this.textWrappedRows = textWrapped;
- };
-
- @computed
- get reactTable() {
- const children = this.childDocs;
- const hasCollectionChild = children.reduce((found, doc) => found || doc.type === DocumentType.COL, false);
- const expanded: { [name: string]: any } = {};
- Array.from(this._openCollections.keys()).map(col => (expanded[col.toString()] = true));
- const rerender = [...this.textWrappedRows]; // TODO: get component to rerender on text wrap change without needign to console.log :((((
-
- return (
- <ReactTable
- style={{ position: 'relative' }}
- data={children}
- page={0}
- pageSize={children.length}
- showPagination={false}
- columns={this.tableColumns}
- getTrProps={this.getTrProps}
- getTdProps={this.getTdProps}
- sortable={false}
- TrComponent={MovableRow}
- sorted={this.sorted}
- expanded={expanded}
- resized={this.resized}
- onResizedChange={this.props.onResizedChange}
- // if it has a child, render another table with the children
- SubComponent={
- !hasCollectionChild
- ? undefined
- : row =>
- row.original.type !== DocumentType.COL ? null : (
- <div style={{ paddingLeft: 57 + 'px' }} className="reactTable-sub">
- <SchemaTable {...this.props} Document={row.original} dataDoc={undefined} childDocs={undefined} />
- </div>
- )
- }
- />
- );
- }
-
- onContextMenu = (e: React.MouseEvent): void => {
- ContextMenu.Instance.addItem({ description: 'Toggle text wrapping', event: this.toggleTextwrap, icon: 'table' });
- };
-
- getField = (row: number, col?: number) => {
- const docs = this.childDocs;
-
- row = row % docs.length;
- while (row < 0) row += docs.length;
- const columns = this.props.columns;
- const doc = docs[row];
- if (col === undefined) {
- return doc;
- }
- if (col >= 0 && col < columns.length) {
- const column = this.props.columns[col].heading;
- return doc[column];
- }
- return undefined;
- };
-
- createTransformer = (row: number, col: number): Transformer => {
- const self = this;
- const captures: { [name: string]: Field } = {};
-
- const transformer: ts.TransformerFactory<ts.SourceFile> = context => {
- return root => {
- function visit(node: ts.Node) {
- node = ts.visitEachChild(node, visit, context);
- if (ts.isIdentifier(node)) {
- const isntPropAccess = !ts.isPropertyAccessExpression(node.parent) || node.parent.expression === node;
- const isntPropAssign = !ts.isPropertyAssignment(node.parent) || node.parent.name !== node;
- if (isntPropAccess && isntPropAssign) {
- if (node.text === '$r') {
- return ts.createNumericLiteral(row.toString());
- } else if (node.text === '$c') {
- return ts.createNumericLiteral(col.toString());
- } else if (node.text === '$') {
- if (ts.isCallExpression(node.parent)) {
- // captures.doc = self.props.Document;
- // captures.key = self.props.fieldKey;
- }
- }
- }
- }
-
- return node;
- }
- return ts.visitNode(root, visit);
- };
- };
-
- // const getVars = () => {
- // return { capturedVariables: captures };
- // };
-
- return { transformer /*getVars*/ };
- };
-
- setComputed = (script: string, doc: Doc, field: string, row: number, col: number): boolean => {
- script = `const $ = (row:number, col?:number) => {
- const rval = (doc as any)[key][row + ${row}];
- return col === undefined ? rval : rval[(doc as any)._schemaHeaders[col + ${col}].heading];
- }
- return ${script}`;
- const compiled = CompileScript(script, { params: { this: Doc.name }, capturedVariables: { doc: this.props.Document, key: this.props.fieldKey }, typecheck: false, transformer: this.createTransformer(row, col) });
- if (compiled.compiled) {
- doc[field] = new ComputedField(compiled);
- return true;
- }
- return false;
- };
-
- @action
- showDoc = (doc: Doc | undefined, dataDoc?: Doc, screenX?: number, screenY?: number) => {
- this._showDoc = doc;
- if (dataDoc && screenX && screenY) {
- this._showDocPos = this.props.ScreenToLocalTransform().transformPoint(screenX, screenY);
- }
- };
-
- onOpenClick = () => {
- this._showDoc && this.props.addDocTab(this._showDoc, 'add:right');
- };
-
- getPreviewTransform = (): Transform => {
- return this.props.ScreenToLocalTransform().translate(-this.borderWidth - 4 - this.tableWidth, -this.borderWidth);
- };
-
- render() {
- const preview = '';
- return (
- <div
- className="collectionSchemaView-table"
- onPointerDown={this.props.onPointerDown}
- onClick={this.props.onClick}
- onWheel={e => this.props.active(true) && e.stopPropagation()}
- onDrop={e => this.props.onDrop(e, {})}
- onContextMenu={this.onContextMenu}>
- {this.reactTable}
- {this.props.Document._chromeHidden || this.props.addDocument === returnFalse ? undefined : (
- <div className="collectionSchemaView-addRow" onClick={this.createRow}>
- + new
- </div>
- )}
- {!this._showDoc ? null : (
- <div
- className="collectionSchemaView-documentPreview"
- ref="overlay"
- style={{
- position: 'absolute',
- width: 150,
- height: 150,
- background: 'dimgray',
- display: 'block',
- top: 0,
- left: 0,
- transform: `translate(${this._showDocPos[0]}px, ${this._showDocPos[1] - 180}px)`,
- }}>
- <DocumentView
- Document={this._showDoc}
- DataDoc={this._showDataDoc}
- styleProvider={DefaultStyleProvider}
- docViewPath={returnEmptyDoclist}
- focus={DocUtils.DefaultFocus}
- renderDepth={this.props.renderDepth}
- rootSelected={returnFalse}
- isContentActive={returnTrue}
- isDocumentActive={returnFalse}
- PanelWidth={() => 150}
- PanelHeight={() => 150}
- ScreenToLocalTransform={this.getPreviewTransform}
- docFilters={returnEmptyFilter}
- docRangeFilters={returnEmptyFilter}
- searchFilterDocs={returnEmptyDoclist}
- ContainingCollectionDoc={this.props.CollectionView?.props.Document}
- ContainingCollectionView={this.props.CollectionView}
- moveDocument={this.props.moveDocument}
- whenChildContentsActiveChanged={emptyFunction}
- addDocTab={this.props.addDocTab}
- pinToPres={this.props.pinToPres}
- bringToFront={returnFalse}></DocumentView>
- </div>
- )}
- </div>
- );
- }
-}
diff --git a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
new file mode 100644
index 000000000..a958f53ea
--- /dev/null
+++ b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
@@ -0,0 +1,352 @@
+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';
+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, 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 { Transform } from '../../../util/Transform';
+import { undoable, undoBatch } from '../../../util/UndoManager';
+import { EditableView } from '../../EditableView';
+import { Colors } from '../../global/globalEnums';
+import { OpenWhere } from '../../nodes/DocumentView';
+import { FieldView, FieldViewProps } from '../../nodes/FieldView';
+import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
+import { KeyValueBox } from '../../nodes/KeyValueBox';
+import { DefaultStyleProvider } from '../../StyleProvider';
+import { CollectionSchemaView, ColumnType, FInfotoColType } from './CollectionSchemaView';
+import './CollectionSchemaView.scss';
+
+export interface SchemaTableCellProps {
+ Document: Doc;
+ col: number;
+ deselectCell: () => void;
+ selectCell: (doc: Doc, col: number) => void;
+ selectedCell: () => [Doc, number] | undefined;
+ fieldKey: string;
+ maxWidth?: () => number;
+ columnWidth: () => number;
+ rowHeight: () => number;
+ padding?: number; // default is 5 -- see scss
+ isRowActive: () => boolean | undefined;
+ getFinfo: (fieldKey: string) => FInfo | undefined;
+ setColumnValues: (field: string, value: string) => boolean;
+ 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
+export class SchemaTableCell extends React.Component<SchemaTableCellProps> {
+ static addFieldDoc = (doc: Doc, where: OpenWhere) => {
+ DocFocusOrOpen(doc);
+ return true;
+ };
+ public static renderProps(props: SchemaTableCellProps) {
+ const { Document, fieldKey, getFinfo, columnWidth, isRowActive } = props;
+ let protoCount = 0;
+ let doc: Doc | undefined = Document;
+ while (doc) {
+ if (Object.keys(doc).includes(fieldKey.replace(/^_/, ''))) {
+ break;
+ }
+ protoCount++;
+ 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 = {
+ childFilters: returnEmptyFilter,
+ childFiltersByRanges: returnEmptyFilter,
+ searchFilterDocs: returnEmptyDoclist,
+ styleProvider: DefaultStyleProvider,
+ docViewPath: returnEmptyDoclist,
+ rootSelected: returnFalse,
+ isSelected: returnFalse,
+ setHeight: returnFalse,
+ select: emptyFunction,
+ dragAction: 'move',
+ bringToFront: emptyFunction,
+ renderDepth: 1,
+ isContentActive: returnFalse,
+ whenChildContentsActiveChanged: emptyFunction,
+ ScreenToLocalTransform: Transform.Identity,
+ focus: emptyFunction,
+ addDocTab: SchemaTableCell.addFieldDoc,
+ pinToPres: returnZero,
+ Document,
+ fieldKey: fieldKey,
+ PanelWidth: columnWidth,
+ PanelHeight: props.rowHeight,
+ };
+ const readOnly = getFinfo(fieldKey)?.readOnly ?? false;
+ const cursor = !readOnly ? 'text' : 'default';
+ const pointerEvents: 'all' | 'none' = !readOnly && isRowActive() ? 'all' : 'none';
+ return { color, textDecoration, fieldProps, cursor, pointerEvents };
+ }
+
+ @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;
+ }
+
+ @computed get defaultCellContent() {
+ const { color, textDecoration, fieldProps } = SchemaTableCell.renderProps(this.props);
+
+ return (
+ <div
+ className="schemacell-edit-wrapper"
+ style={{
+ color,
+ textDecoration,
+ width: '100%',
+ }}>
+ <EditableView
+ oneLine={this.props.oneLine}
+ allowCRs={this.props.allowCRs}
+ contents={<FieldView {...fieldProps} />}
+ editing={this.selected ? undefined : false}
+ GetValue={() => Field.toKeyValueString(this.props.Document, this.props.fieldKey)}
+ 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' && columnTypeStr !== 'enumeration') return ColumnType.Any;
+ if (typeof cellValue === 'boolean') return ColumnType.Boolean;
+
+ if (columnTypeStr && columnTypeStr in FInfotoColType) {
+ return FInfotoColType[columnTypeStr];
+ }
+
+ return ColumnType.Any;
+ }
+
+ get content() {
+ const cellType: ColumnType = this.getCellType;
+ // prettier-ignore
+ switch (cellType) {
+ 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;
+ }
+ }
+
+ render() {
+ return (
+ <div
+ className="schema-table-cell"
+ onPointerDown={action(e => !this.selected && this.props.selectCell(this.props.Document, this.props.col))}
+ style={{ padding: this.props.padding, maxWidth: this.props.maxWidth?.(), width: this.props.columnWidth() || undefined, border: this.selected ? `solid 2px ${Colors.MEDIUM_BLUE}` : undefined }}>
+ {this.content}
+ </div>
+ );
+ }
+}
+
+// mj: most of this is adapted from old schema code so I'm not sure what it does tbh
+@observer
+export class SchemaImageCell extends React.Component<SchemaTableCellProps> {
+ @observable _previewRef: HTMLImageElement | undefined;
+
+ choosePath(url: URL) {
+ if (url.protocol === 'data') return url.href; // if the url ises the data protocol, just return the href
+ if (url.href.indexOf(window.location.origin) === -1) return Utils.CorsProxy(url.href); // otherwise, put it through the cors proxy erver
+ if (!/\.(png|jpg|jpeg|gif|webp)$/.test(url.href.toLowerCase())) return url.href; //Why is this here — good question
+
+ const ext = extname(url.href);
+ return url.href.replace(ext, '_s' + ext);
+ }
+
+ get url() {
+ const field = Cast(this.props.Document[this.props.fieldKey], ImageField, null); // retrieve the primary image URL that is being rendered from the data doc
+ const alts = DocListCast(this.props.Document[this.props.fieldKey + '-alternates']); // retrieve alternate documents that may be rendered as alternate images
+ const altpaths = alts
+ .map(doc => Cast(doc[Doc.LayoutFieldKey(doc)], ImageField, null)?.url)
+ .filter(url => url)
+ .map(url => this.choosePath(url)); // access the primary layout data of the alternate documents
+ const paths = field ? [this.choosePath(field.url), ...altpaths] : altpaths;
+ // If there is a path, follow it; otherwise, follow a link to a default image icon
+ const url = paths.length ? paths : [Utils.CorsProxy('http://www.cs.brown.edu/~bcz/noImage.png')];
+ return url[0];
+ }
+
+ @action
+ showHoverPreview = (e: React.PointerEvent) => {
+ this._previewRef = document.createElement('img');
+ document.body.appendChild(this._previewRef);
+ const ext = extname(this.url);
+ this._previewRef.src = this.url.replace('_s' + ext, '_m' + ext);
+ this._previewRef.style.position = 'absolute';
+ this._previewRef.style.left = e.clientX + 10 + 'px';
+ this._previewRef.style.top = e.clientY + 10 + 'px';
+ this._previewRef.style.zIndex = '1000';
+ };
+
+ @action
+ moveHoverPreview = (e: React.PointerEvent) => {
+ if (!this._previewRef) return;
+ this._previewRef.style.left = e.clientX + 10 + 'px';
+ this._previewRef.style.top = e.clientY + 10 + 'px';
+ };
+
+ @action
+ removeHoverPreview = (e: React.PointerEvent) => {
+ if (!this._previewRef) return;
+ document.body.removeChild(this._previewRef);
+ };
+
+ render() {
+ const aspect = Doc.NativeAspect(this.props.Document); // aspect ratio
+ // let width = Math.max(75, this.props.columnWidth); // get a with that is no smaller than 75px
+ // const height = Math.max(75, width / aspect); // get a height either proportional to that or 75 px
+ const height = this.props.rowHeight() ? this.props.rowHeight() - (this.props.padding || 6) * 2 : undefined;
+ const width = height ? height * aspect : undefined; // increase the width of the image if necessary to maintain proportionality
+
+ return <img src={this.url} width={width} height={height} style={{}} draggable="false" onPointerEnter={this.showHoverPreview} onPointerMove={this.moveHoverPreview} onPointerLeave={this.removeHoverPreview} />;
+ }
+}
+
+@observer
+export class SchemaDateCell extends React.Component<SchemaTableCellProps> {
+ @observable _pickingDate: boolean = false;
+
+ @computed get date(): DateField {
+ // if the cell is a date field, cast then contents to a date. Otherrwwise, make the contents undefined.
+ return DateCast(this.props.Document[this.props.fieldKey]);
+ }
+
+ @action
+ handleChange = (date: any) => {
+ // const script = CompileScript(date.toString(), { requiredType: "Date", addReturn: true, params: { this: Doc.name } });
+ // if (script.compiled) {
+ // this.applyToDoc(this._document, this.props.row, this.props.col, script.run);
+ // } else {
+ // ^ DateCast is always undefined for some reason, but that is what the field should be set to
+ this.props.Document[this.props.fieldKey] = new DateField(date as Date);
+ //}
+ };
+
+ render() {
+ return <DatePicker dateFormat={'Pp'} selected={this.date.date} onChange={(date: any) => this.handleChange(date)} />;
+ }
+}
+@observer
+export class SchemaRTFCell 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;
+ }
+ selectedFunc = () => this.selected;
+ render() {
+ const { color, textDecoration, fieldProps, cursor, pointerEvents } = SchemaTableCell.renderProps(this.props);
+ 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]))}
+ </div>
+ );
+ }
+}
+@observer
+export class SchemaBoolCell 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);
+ return (
+ <div className="schemaBoolCell" style={{ display: 'flex', color, textDecoration, cursor, pointerEvents }}>
+ <input
+ style={{ marginRight: 4 }}
+ type="checkbox"
+ checked={BoolCast(this.props.Document[this.props.fieldKey])}
+ onChange={undoBatch((value: React.ChangeEvent<HTMLInputElement> | undefined) => {
+ if ((value?.nativeEvent as any).shiftKey) {
+ this.props.setColumnValues(this.props.fieldKey.replace(/^_/, ''), (color === 'black' ? '=' : '') + value?.target?.checked.toString());
+ }
+ KeyValueBox.SetField(this.props.Document, this.props.fieldKey.replace(/^_/, ''), (color === 'black' ? '=' : '') + value?.target?.checked.toString());
+ })}
+ />
+ <EditableView
+ 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) => {
+ if (shiftDown && enterKey) {
+ this.props.setColumnValues(this.props.fieldKey.replace(/^_/, ''), value);
+ }
+ const set = KeyValueBox.SetField(this.props.Document, this.props.fieldKey.replace(/^_/, ''), value);
+ this.props.finishEdit?.();
+ return set;
+ })}
+ />
+ </div>
+ );
+ }
+}
+@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 e68f9abe3..422dae15b 100644
--- a/src/client/views/global/globalCssVariables.scss
+++ b/src/client/views/global/globalCssVariables.scss
@@ -59,7 +59,6 @@ $standard-border-radius: 3px;
// shadow
$standard-box-shadow: 0px 3px 4px rgba(0, 0, 0, 0.3);
-$dashboardselector-height: 32px;
$mainTextInput-zindex: 999; // then text input overlay so that it's context menu will appear over decorations, etc
$docDecorations-zindex: 998; // then doc decorations appear over everything else
$remoteCursors-zindex: 997; // ... not sure what level the remote cursors should go -- is this right?
@@ -71,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;
@@ -79,9 +82,13 @@ $TREE_BULLET_WIDTH: 20px;
MAX_ROW_HEIGHT: $MAX_ROW_HEIGHT;
SEARCH_THUMBNAIL_SIZE: $search-thumnail-size;
ANTIMODEMENU_HEIGHT: $antimodemenu-height;
- DASHBOARD_SELECTOR_HEIGHT: $dashboardselector-height;
+ TOPBAR_HEIGHT: $topbar-height;
DFLT_IMAGE_NATIVE_DIM: $DFLT_IMAGE_NATIVE_DIM;
LEFT_MENU_WIDTH: $LEFT_MENU_WIDTH;
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 76259113c..efb702564 100644
--- a/src/client/views/global/globalCssVariables.scss.d.ts
+++ b/src/client/views/global/globalCssVariables.scss.d.ts
@@ -6,11 +6,15 @@ interface IGlobalScss {
MAX_ROW_HEIGHT: string;
SEARCH_THUMBNAIL_SIZE: string;
ANTIMODEMENU_HEIGHT: string;
- DASHBOARD_SELECTOR_HEIGHT: string;
+ TOPBAR_HEIGHT: string;
DFLT_IMAGE_NATIVE_DIM: string;
LEFT_MENU_WIDTH: string;
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/linking/LinkEditor.scss b/src/client/views/linking/LinkEditor.scss
deleted file mode 100644
index b0ee4e46d..000000000
--- a/src/client/views/linking/LinkEditor.scss
+++ /dev/null
@@ -1,334 +0,0 @@
-@import '../global/globalCssVariables';
-
-.linkEditor {
- width: 100%;
- height: auto;
- font-size: 13px; // TODO
- user-select: none;
- max-width: 280px;
-}
-
-.linkEditor-button-back {
- //margin-bottom: 6px;
- border-radius: 10px;
- width: 18px;
- height: 18px;
- padding: 0;
-
- &:hover {
- cursor: pointer;
- }
-}
-
-.linkEditor-info {
- padding-top: 12px;
- padding-left: 5px;
- padding-bottom: 3px;
- //margin-bottom: 6px;
- display: flex;
- justify-content: space-between;
- color: black;
-
- .linkEditor-linkedTo {
- width: calc(100% - 46px);
- overflow: hidden;
- position: relative;
- text-overflow: ellipsis;
- white-space: pre;
-
- .linkEditor-downArrow {
- &:hover {
- cursor: pointer;
- }
- }
- }
-}
-
-.linkEditor-moreInfo {
- margin-left: 12px;
- padding-left: 13px;
- padding-right: 6.5px;
- padding-bottom: 4px;
- font-size: 9px;
- //font-style: italic;
- text-decoration-color: grey;
-
- .button {
- color: black;
-
- &:hover {
- cursor: pointer;
- }
- }
-}
-
-.linkEditor-zoomFollow {
- padding-left: 26px;
- padding-right: 6.5px;
- padding-bottom: 3.5px;
- display: flex;
-
- .linkEditor-zoomFollow-label {
- text-decoration-color: black;
- color: black;
- line-height: 1.7;
- }
-
- .linkEditor-zoomFollow-input {
- display: block;
- width: 20px;
- }
-}
-.linkEditor-deleteBtn {
- padding-left: 3px;
-}
-
-.linkEditor-description {
- padding-left: 26px;
- padding-bottom: 3.5px;
- display: flex;
-
- .linkEditor-description-label {
- text-decoration-color: black;
- color: black;
- }
-
- .linkEditor-description-input {
- display: flex;
-
- .linkEditor-description-editing {
- min-width: 85%;
- //border: 1px solid grey;
- //border-radius: 4px;
- padding-left: 2px;
- //margin-right: 4px;
- color: black;
- text-decoration-color: grey;
- }
-
- .linkEditor-description-add-button {
- display: inline;
- border-radius: 7px;
- font-size: 9px;
- background: black;
- height: 80%;
- color: white;
- padding: 3px;
- margin-left: 3px;
-
- &:hover {
- cursor: pointer;
- background: grey;
- }
- }
- }
-}
-
-.linkEditor-relationship-dropdown {
- position: absolute;
- width: 154px;
- max-height: 90px;
- overflow: auto;
- background: white;
-
- p {
- padding: 3px;
- cursor: pointer;
- border: 1px solid $medium-gray;
- }
-
- p:hover {
- background: $light-blue;
- }
-}
-
-.linkEditor-followingDropdown {
- padding-left: 26px;
- padding-right: 6.5px;
- padding-bottom: 15px;
- display: flex;
-
- &:hover {
- cursor: pointer;
- }
-
- .linkEditor-followingDropdown-label {
- color: black;
- padding-right: 3px;
- }
-
- .linkEditor-followingDropdown-dropdown {
- .linkEditor-followingDropdown-header {
- border: 1px solid grey;
- border-radius: 4px;
- //background-color: rgb(236, 236, 236);
- padding-left: 2px;
- padding-right: 2px;
- text-decoration-color: black;
- color: rgb(94, 94, 94);
-
- .linkEditor-followingDropdown-icon {
- float: right;
- color: black;
- }
- }
-
- .linkEditor-followingDropdown-optionsList {
- padding-left: 3px;
- padding-right: 3px;
-
- &:last-child {
- border-bottom: none;
- }
-
- .linkEditor-followingDropdown-option {
- border: 0.25px solid grey;
- //background-color: rgb(236, 236, 236);
- padding-left: 2px;
- padding-right: 2px;
- color: grey;
- text-decoration-color: grey;
- font-size: 9px;
- border-top: none;
-
- &:hover {
- background-color: rgb(187, 220, 231);
- }
- }
- }
- }
-}
-
-.linkEditor-button,
-.linkEditor-addbutton {
- width: 15%;
- border-radius: 7px;
- font-size: 9px;
- background: black;
- padding: 3px;
- height: 80%;
- color: white;
- text-align: center;
- margin: auto;
- margin-left: 3px;
- > svg {
- margin: auto;
- }
- &:disabled {
- background-color: gray;
- }
-}
-
-.linkEditor-addbutton {
- margin-left: 0px;
-}
-
-.linkEditor-groupsLabel {
- display: flex;
- justify-content: space-between;
-}
-
-.linkEditor-group {
- background-color: $light-gray;
- padding: 6px;
- margin: 3px 0;
- border-radius: 3px;
-
- .linkEditor-group-row {
- display: flex;
- margin-bottom: 3px;
- }
-
- .linkEditor-group-row-label {
- margin-right: 6px;
- display: inline-block;
- }
-
- .linkEditor-metadata-row {
- display: flex;
- justify-content: space-between;
- margin-bottom: 6px;
-
- .linkEditor-error {
- border-color: red;
- }
-
- input {
- width: calc(50% - 16px);
- height: 20px;
- }
-
- button {
- width: 20px;
- height: 20px;
- margin-left: 3px;
- padding: 0;
- font-size: 10px;
- }
- }
-}
-
-.linkEditor-dropdown {
- width: 100%;
- position: relative;
- z-index: 999;
-
- input {
- width: 100%;
- }
-
- .linkEditor-options-wrapper {
- width: 100%;
- position: absolute;
- top: 19px;
- left: 0;
- display: flex;
- flex-direction: column;
- }
-
- .linkEditor-option {
- background-color: $light-gray;
- border: 1px solid $medium-gray;
- border-top: 0;
- padding: 3px;
- cursor: pointer;
-
- &:hover {
- background-color: lightgray;
- }
-
- &.onDown {
- background-color: gray;
- }
- }
-}
-
-.linkEditor-typeButton {
- background-color: transparent;
- color: $dark-gray;
- height: 20px;
- padding: 0 3px;
- padding-bottom: 2px;
- text-align: left;
- text-transform: none;
- letter-spacing: normal;
- font-size: 12px;
- font-weight: bold;
- display: inline-block;
- width: calc(100% - 40px);
-
- &:hover {
- background-color: $white;
- }
-}
-
-.linkEditor-group-buttons {
- height: 20px;
- display: flex;
- justify-content: flex-end;
- margin-top: 5px;
-
- .linkEditor-button {
- margin-left: 3px;
- }
-}
diff --git a/src/client/views/linking/LinkEditor.tsx b/src/client/views/linking/LinkEditor.tsx
deleted file mode 100644
index 1697062f4..000000000
--- a/src/client/views/linking/LinkEditor.tsx
+++ /dev/null
@@ -1,394 +0,0 @@
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Tooltip } from '@material-ui/core';
-import { action, computed, observable } from 'mobx';
-import { observer } from 'mobx-react';
-import { Doc, NumListCast, StrListCast, Field } from '../../../fields/Doc';
-import { DateCast, StrCast, Cast, BoolCast } from '../../../fields/Types';
-import { LinkManager } from '../../util/LinkManager';
-import { undoBatch } from '../../util/UndoManager';
-import './LinkEditor.scss';
-import { LinkRelationshipSearch } from './LinkRelationshipSearch';
-import React = require('react');
-import { emptyFunction, returnFalse, setupMoveUpEvents } from '../../../Utils';
-
-interface LinkEditorProps {
- sourceDoc: Doc;
- linkDoc: Doc;
- showLinks?: () => void;
- hideback?: boolean;
-}
-@observer
-export class LinkEditor extends React.Component<LinkEditorProps> {
- @observable description = Field.toString(LinkManager.currentLink?.description as any as Field);
- @observable relationship = StrCast(LinkManager.currentLink?.linkRelationship);
- @observable zoomFollow = BoolCast(this.props.sourceDoc.followLinkZoom);
- @observable audioFollow = BoolCast(this.props.sourceDoc.followLinkAudio);
- @observable openDropdown: boolean = false;
- @observable private buttonColor: string = '';
- @observable private relationshipButtonColor: string = '';
- @observable private relationshipSearchVisibility: string = 'none';
- @observable private searchIsActive: boolean = false;
-
- //@observable description = this.props.linkDoc.description ? StrCast(this.props.linkDoc.description) : "DESCRIPTION";
-
- @undoBatch
- setRelationshipValue = action((value: string) => {
- if (LinkManager.currentLink) {
- const prevRelationship = LinkManager.currentLink.linkRelationship as string;
- LinkManager.currentLink.linkRelationship = value;
- Doc.GetProto(LinkManager.currentLink).linkRelationship = value;
- const linkRelationshipList = StrListCast(Doc.UserDoc().linkRelationshipList);
- const linkRelationshipSizes = NumListCast(Doc.UserDoc().linkRelationshipSizes);
- const linkColorList = StrListCast(Doc.UserDoc().linkColorList);
-
- // if the relationship does not exist in the list, add it and a corresponding unique randomly generated color
- if (!linkRelationshipList?.includes(value)) {
- linkRelationshipList.push(value);
- linkRelationshipSizes.push(1);
- const randColor = 'rgb(' + Math.floor(Math.random() * 255) + ',' + Math.floor(Math.random() * 255) + ',' + Math.floor(Math.random() * 255) + ')';
- linkColorList.push(randColor);
- // if the relationship is already in the list AND the new rel is different from the prev rel, update the rel sizes
- } else if (linkRelationshipList && value !== prevRelationship) {
- const index = linkRelationshipList.indexOf(value);
- //increment size of new relationship size
- if (index !== -1 && index < linkRelationshipSizes.length) {
- const pvalue = linkRelationshipSizes[index];
- linkRelationshipSizes[index] = pvalue === undefined || !Number.isFinite(pvalue) ? 1 : pvalue + 1;
- }
- //decrement the size of the previous relationship if it already exists (i.e. not default 'link' relationship upon link creation)
- if (linkRelationshipList.includes(prevRelationship)) {
- const pindex = linkRelationshipList.indexOf(prevRelationship);
- if (pindex !== -1 && pindex < linkRelationshipSizes.length) {
- const pvalue = linkRelationshipSizes[pindex];
- linkRelationshipSizes[pindex] = Math.max(0, pvalue === undefined || !Number.isFinite(pvalue) ? 1 : pvalue - 1);
- }
- }
- }
- this.relationshipButtonColor = 'rgb(62, 133, 55)';
- setTimeout(
- action(() => (this.relationshipButtonColor = '')),
- 750
- );
- return true;
- }
- });
-
- /**
- * returns list of strings with possible existing relationships that contain what is currently in the input field
- */
- @action
- getRelationshipResults = () => {
- const query = this.relationship; //current content in input box
- const linkRelationshipList = StrListCast(Doc.UserDoc().linkRelationshipList);
- if (linkRelationshipList) {
- return linkRelationshipList.filter(rel => rel.includes(query));
- }
- };
-
- /**
- * toggles visibility of the relationship search results when the input field is focused on
- */
- @action
- toggleRelationshipResults = () => {
- this.relationshipSearchVisibility = this.relationshipSearchVisibility === 'none' ? 'block' : 'none';
- };
-
- @undoBatch
- setDescripValue = action((value: string) => {
- if (LinkManager.currentLink) {
- Doc.GetProto(LinkManager.currentLink).description = value;
- this.buttonColor = 'rgb(62, 133, 55)';
- setTimeout(
- action(() => (this.buttonColor = '')),
- 750
- );
- return true;
- }
- });
-
- onDescriptionKey = (e: React.KeyboardEvent<HTMLInputElement>) => {
- if (e.key === 'Enter') {
- this.setDescripValue(this.description);
- document.getElementById('input')?.blur();
- }
- e.stopPropagation();
- };
-
- onRelationshipKey = (e: React.KeyboardEvent<HTMLInputElement>) => {
- if (e.key === 'Enter') {
- this.setRelationshipValue(this.relationship);
- document.getElementById('input')?.blur();
- }
- e.stopPropagation();
- };
-
- onDescriptionDown = () => this.setDescripValue(this.description);
- onRelationshipDown = () => this.setRelationshipValue(this.relationship);
-
- onBlur = () => {
- //only hide the search results if the user clicks out of the input AND not on any of the search results
- // i.e. if search is not active
- if (!this.searchIsActive) {
- this.toggleRelationshipResults();
- }
- };
- onFocus = () => {
- this.toggleRelationshipResults();
- };
- toggleSearchIsActive = () => {
- this.searchIsActive = !this.searchIsActive;
- };
-
- @action
- handleDescriptionChange = (e: React.ChangeEvent<HTMLInputElement>) => {
- this.description = e.target.value;
- };
- @action
- handleRelationshipChange = (e: React.ChangeEvent<HTMLInputElement>) => {
- this.relationship = e.target.value;
- };
- @action
- handleZoomFollowChange = () => {
- this.props.sourceDoc.followLinkZoom = !this.props.sourceDoc.followLinkZoom;
- };
- @action
- handleAudioFollowChange = () => {
- this.props.sourceDoc.followLinkAudio = !this.props.sourceDoc.followLinkAudio;
- };
- @action
- handleRelationshipSearchChange = (result: string) => {
- this.setRelationshipValue(result);
- this.toggleRelationshipResults();
- this.relationship = result;
- };
- @computed
- get editRelationship() {
- //NOTE: confusingly, the classnames for the following relationship JSX elements are the same as the for the description elements for shared CSS
- return (
- <div className="linkEditor-description">
- <div className="linkEditor-description-label">Relationship:</div>
- <div className="linkEditor-description-input">
- <div className="linkEditor-description-editing">
- <input
- style={{ width: '100%' }}
- id="input"
- value={this.relationship}
- autoComplete={'off'}
- placeholder={'Enter link relationship'}
- onKeyDown={this.onRelationshipKey}
- onChange={this.handleRelationshipChange}
- onFocus={this.onFocus}
- onBlur={this.onBlur}></input>
- <LinkRelationshipSearch results={this.getRelationshipResults()} display={this.relationshipSearchVisibility} handleRelationshipSearchChange={this.handleRelationshipSearchChange} toggleSearch={this.toggleSearchIsActive} />
- </div>
- <div className="linkEditor-description-add-button" style={{ background: this.relationshipButtonColor }} onPointerDown={this.onRelationshipDown}>
- Set
- </div>
- </div>
- </div>
- );
- }
- @computed
- get editZoomFollow() {
- //NOTE: confusingly, the classnames for the following relationship JSX elements are the same as the for the description elements for shared CSS
- return (
- <div className="linkEditor-zoomFollow">
- <div className="linkEditor-zoomFollow-label">Zoom To Link Target:</div>
- <div className="linkEditor-zoomFollow-input">
- <div className="linkEditor-zoomFollow-editing">
- <input type="checkbox" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, this.handleZoomFollowChange)} defaultChecked={this.zoomFollow} />
- </div>
- </div>
- </div>
- );
- }
-
- @computed
- get editAudioFollow() {
- //NOTE: confusingly, the classnames for the following relationship JSX elements are the same as the for the description elements for shared CSS
- console.log('AudioFollow:' + this.audioFollow);
- return (
- <div className="linkEditor-zoomFollow">
- <div className="linkEditor-zoomFollow-label">Play Target Audio:</div>
- <div className="linkEditor-zoomFollow-input">
- <div className="linkEditor-zoomFollow-editing">
- <input type="checkbox" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, this.handleAudioFollowChange)} defaultChecked={this.audioFollow} />
- </div>
- </div>
- </div>
- );
- }
-
- @computed
- get editDescription() {
- return (
- <div className="linkEditor-description">
- <div className="linkEditor-description-label">Description:</div>
- <div className="linkEditor-description-input">
- <div className="linkEditor-description-editing">
- <input style={{ width: '100%' }} autoComplete={'off'} id="input" value={this.description} placeholder={'Enter link description'} onKeyDown={this.onDescriptionKey} onChange={this.handleDescriptionChange}></input>
- </div>
- <div className="linkEditor-description-add-button" style={{ background: this.buttonColor }} onPointerDown={this.onDescriptionDown}>
- Set
- </div>
- </div>
- </div>
- );
- }
-
- @action
- changeDropdown = () => {
- this.openDropdown = !this.openDropdown;
- };
-
- @undoBatch
- changeFollowBehavior = action((follow: string) => {
- this.openDropdown = false;
- Doc.GetProto(this.props.linkDoc).followLinkLocation = follow;
- });
-
- @computed
- get followingDropdown() {
- return (
- <div className="linkEditor-followingDropdown">
- <div className="linkEditor-followingDropdown-label">Follow Behavior:</div>
- <div className="linkEditor-followingDropdown-dropdown">
- <div className="linkEditor-followingDropdown-header" onPointerDown={this.changeDropdown}>
- {StrCast(this.props.linkDoc.followLinkLocation, 'default')}
- <FontAwesomeIcon className="linkEditor-followingDropdown-icon" icon={this.openDropdown ? 'chevron-up' : 'chevron-down'} size={'lg'} />
- </div>
- <div className="linkEditor-followingDropdown-optionsList" style={{ display: this.openDropdown ? '' : 'none' }}>
- <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior('default')}>
- Default
- </div>
- <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior('add:left')}>
- Always open in new left pane
- </div>
- <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior('add:right')}>
- Always open in new right pane
- </div>
- <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior('replace:right')}>
- Always replace right tab
- </div>
- <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior('replace:left')}>
- Always replace left tab
- </div>
- <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior('fullScreen')}>
- Always open full screen
- </div>
- <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior('add')}>
- Always open in a new tab
- </div>
- <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior('replace')}>
- Replace Tab
- </div>
- {this.props.linkDoc.linksToAnnotation ? (
- <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior('openExternal')}>
- Always open in external page
- </div>
- ) : null}
- </div>
- </div>
- </div>
- );
- }
-
- autoMove = (e: React.PointerEvent) => {
- setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => (this.props.linkDoc.linkAutoMove = !this.props.linkDoc.linkAutoMove))));
- };
-
- showAnchor = (e: React.PointerEvent) => {
- setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => (this.props.linkDoc.hidden = !this.props.linkDoc.hidden))));
- };
-
- showLink = (e: React.PointerEvent) => {
- setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => (this.props.linkDoc.linkDisplay = !this.props.linkDoc.linkDisplay))));
- };
-
- deleteLink = (e: React.PointerEvent): void => {
- setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => LinkManager.Instance.deleteLink(this.props.linkDoc))));
- };
-
- render() {
- const destination = LinkManager.getOppositeAnchor(this.props.linkDoc, this.props.sourceDoc);
-
- return !destination ? null : (
- <div className="linkEditor" tabIndex={0} onKeyDown={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}>
- <div className="linkEditor-info">
- {!this.props.showLinks ? null : (
- <Tooltip title={<div className="dash-tooltip">Return to link menu</div>} placement="top">
- <button className="linkEditor-button-back" style={{ display: this.props.hideback ? 'none' : '' }} onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => this.props.showLinks?.())}>
- <FontAwesomeIcon icon="arrow-left" size="sm" />{' '}
- </button>
- </Tooltip>
- )}
- <p className="linkEditor-linkedTo">
- Editing Link to: <b>{StrCast(destination.proto?.title, StrCast(destination.title, 'untitled'))}</b>
- </p>
- <Tooltip title={<div className="dash-tooltip">Delete Link</div>}>
- <div className="linkEditor-deleteBtn" onPointerDown={this.deleteLink} onClick={e => e.stopPropagation()}>
- <FontAwesomeIcon className="fa-icon" icon="trash" size="sm" />
- </div>
- </Tooltip>
- </div>
- <div className="linkEditor-moreInfo">
- {this.props.linkDoc.author ? (
- <>
- {' '}
- <b>Author:</b> {StrCast(this.props.linkDoc.author)}
- </>
- ) : null}
- {this.props.linkDoc.creationDate ? (
- <>
- {' '}
- <b>Creation Date:</b>
- {DateCast(this.props.linkDoc.creationDate).toString()}
- </>
- ) : null}
- </div>
-
- {this.editDescription}
- {this.editRelationship}
- {this.editZoomFollow}
- {this.editAudioFollow}
- <div className="linkEditor-description">
- Show Anchor:
- <Tooltip title={<div className="dash-tooltip">{this.props.linkDoc.hidden ? 'Show Link Anchor' : 'Hide Link Anchor'}</div>}>
- <div
- className="linkEditor-button"
- style={{ background: this.props.linkDoc.hidden ? 'gray' : '#4476f7' /* $medium-blue */ }}
- onPointerDown={this.showAnchor}
- onClick={e => e.stopPropagation()}>
- <FontAwesomeIcon className="fa-icon" icon={'eye'} size="sm" />
- </div>
- </Tooltip>
- </div>
- <div className="linkEditor-description">
- Show Link Line:
- <Tooltip title={<div className="dash-tooltip">{this.props.linkDoc.linkDisplay ? 'Hide Link Line' : 'Show Link Line'}</div>}>
- <div
- className="linkEditor-button"
- style={{ background: this.props.linkDoc.hidden ? 'gray' : this.props.linkDoc.linkDisplay ? '#4476f7' /* $medium-blue */ : '' }}
- onPointerDown={this.showLink}
- onClick={e => e.stopPropagation()}>
- <FontAwesomeIcon className="fa-icon" icon={'project-diagram'} size="sm" />
- </div>
- </Tooltip>
- </div>
- <div className="linkEditor-description">
- Freeze Anchor:
- <Tooltip title={<div className="dash-tooltip">{this.props.linkDoc.linkAutoMove ? 'Click to freeze link anchor position' : 'Click to auto move link anchor'}</div>}>
- <div
- className="linkEditor-button"
- style={{ background: this.props.linkDoc.hidden ? 'gray' : this.props.linkDoc.linkAutoMove ? '' : '#4476f7' /* $medium-blue */ }}
- onPointerDown={this.autoMove}
- onClick={e => e.stopPropagation()}>
- <FontAwesomeIcon className="fa-icon" icon={'play'} size="sm" />
- </div>
- </Tooltip>
- </div>
- {this.followingDropdown}
- </div>
- );
- }
-}
diff --git a/src/client/views/linking/LinkMenu.scss b/src/client/views/linking/LinkMenu.scss
index 77c16a28f..80cf93ed8 100644
--- a/src/client/views/linking/LinkMenu.scss
+++ b/src/client/views/linking/LinkMenu.scss
@@ -1,4 +1,4 @@
-@import "../global/globalCssVariables";
+@import '../global/globalCssVariables';
.linkMenu {
width: auto;
@@ -13,7 +13,6 @@
border: 1px solid #e4e4e4;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23);
background: white;
- min-width: 170px;
max-height: 230px;
overflow-y: scroll;
z-index: 10;
@@ -22,7 +21,7 @@
.linkMenu-list {
white-space: nowrap;
overflow-x: hidden;
- width: 240px;
+ width: 100%;
scrollbar-color: white;
&:last-child {
@@ -39,7 +38,6 @@
border-bottom: 0.5px solid lightgray;
//@extend: 5px 0;
-
&:last-child {
border-bottom: none;
}
@@ -76,4 +74,4 @@
display: none;
}
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/linking/LinkMenu.tsx b/src/client/views/linking/LinkMenu.tsx
index 0096a58bd..65d13a6c3 100644
--- a/src/client/views/linking/LinkMenu.tsx
+++ b/src/client/views/linking/LinkMenu.tsx
@@ -1,19 +1,18 @@
-import { action, computed, observable } from 'mobx';
+import { action, observable } from 'mobx';
import { observer } from 'mobx-react';
import { Doc } from '../../../fields/Doc';
import { LinkManager } from '../../util/LinkManager';
import { DocumentView } from '../nodes/DocumentView';
import { LinkDocPreview } from '../nodes/LinkDocPreview';
-import { LinkEditor } from './LinkEditor';
import './LinkMenu.scss';
import { LinkMenuGroup } from './LinkMenuGroup';
import React = require('react');
interface Props {
docView: DocumentView;
- position?: { x?: number; y?: number };
+ style?: { left: number; top: number };
itemHandler?: (doc: Doc) => void;
- clearLinkEditor: () => void;
+ clearLinkEditor?: () => void;
}
/**
@@ -22,23 +21,15 @@ interface Props {
@observer
export class LinkMenu extends React.Component<Props> {
_editorRef = React.createRef<HTMLDivElement>();
- @observable _editingLink?: Doc;
@observable _linkMenuRef = React.createRef<HTMLDivElement>();
- @computed get position() {
- return this.props.position ?? (dv => ({ x: dv?.left || 0, y: (dv?.bottom || 0) + 15 }))(this.props.docView.getBounds());
- }
-
- clear = action(() => {
- this.props.clearLinkEditor();
- this._editingLink = undefined;
- });
+ clear = () => this.props.clearLinkEditor?.();
componentDidMount() {
- document.addEventListener('pointerdown', this.onPointerDown, true);
+ this.props.clearLinkEditor && document.addEventListener('pointerdown', this.onPointerDown, true);
}
componentWillUnmount() {
- document.removeEventListener('pointerdown', this.onPointerDown, true);
+ this.props.clearLinkEditor && document.removeEventListener('pointerdown', this.onPointerDown, true);
}
onPointerDown = action((e: PointerEvent) => {
@@ -55,32 +46,20 @@ export class LinkMenu extends React.Component<Props> {
*/
renderAllGroups = (groups: Map<string, Array<Doc>>): Array<JSX.Element> => {
const linkItems = Array.from(groups.entries()).map(group => (
- <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}
- showEditor={action(linkDoc => (this._editingLink = linkDoc))}
- />
+ <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.position ? [<></>] : [<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() {
- const sourceDoc = this.props.docView.props.Document;
+ const sourceDoc = this.props.docView.rootDoc;
+ const sourceAnchor = this.props.docView.anchorViewDoc ?? sourceDoc;
+ const style = this.props.style ?? (dv => ({ left: dv?.left || 0, top: this.props.docView.topMost ? undefined : (dv?.bottom || 0) + 15, bottom: this.props.docView.topMost ? 20 : undefined, maxWidth: 200 }))(this.props.docView.getBounds());
+
return (
- <div className="linkMenu" ref={this._linkMenuRef} style={{ left: this.position.x, top: this.props.docView.topMost ? undefined : this.position.y, bottom: this.props.docView.topMost ? 20 : undefined }}>
- {this._editingLink ? (
- <div className="linkMenu-listEditor">
- <LinkEditor sourceDoc={sourceDoc} linkDoc={this._editingLink} showLinks={action(() => (this._editingLink = undefined))} />
- </div>
- ) : (
- <div className="linkMenu-list">{this.renderAllGroups(LinkManager.Instance.getRelatedGroupedLinks(sourceDoc))}</div>
- )}
+ <div className="linkMenu" ref={this._linkMenuRef} style={{ ...style }}>
+ <div className="linkMenu-list">{this.renderAllGroups(LinkManager.Instance.getRelatedGroupedLinks(sourceAnchor))}</div>
</div>
);
}
diff --git a/src/client/views/linking/LinkMenuGroup.tsx b/src/client/views/linking/LinkMenuGroup.tsx
index fa6a2f506..8324a97d9 100644
--- a/src/client/views/linking/LinkMenuGroup.tsx
+++ b/src/client/views/linking/LinkMenuGroup.tsx
@@ -1,20 +1,20 @@
-import { observer } from "mobx-react";
-import { observable, action } from "mobx";
-import { Doc, StrListCast } from "../../../fields/Doc";
-import { Id } from "../../../fields/FieldSymbols";
-import { Cast } from "../../../fields/Types";
-import { LinkManager } from "../../util/LinkManager";
-import { DocumentView } from "../nodes/DocumentView";
+import { observer } from 'mobx-react';
+import { observable, action } from 'mobx';
+import { Doc, StrListCast } from '../../../fields/Doc';
+import { Id } from '../../../fields/FieldSymbols';
+import { Cast, DocCast } from '../../../fields/Types';
+import { LinkManager } from '../../util/LinkManager';
+import { DocumentView } from '../nodes/DocumentView';
import './LinkMenu.scss';
-import { LinkMenuItem } from "./LinkMenuItem";
-import React = require("react");
+import { LinkMenuItem } from './LinkMenuItem';
+import React = require('react');
+import { DocumentType } from '../../documents/DocumentTypes';
interface LinkMenuGroupProps {
sourceDoc: Doc;
group: Doc[];
groupType: string;
- clearLinkEditor: () => void;
- showEditor: (linkDoc: Doc) => void;
+ clearLinkEditor?: () => void;
docView: DocumentView;
itemHandler?: (doc: Doc) => void;
}
@@ -24,51 +24,63 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> {
private _menuRef = React.createRef<HTMLDivElement>();
getBackgroundColor = (): string => {
- const linkRelationshipList = StrListCast(Doc.UserDoc().linkRelationshipList);
- const linkColorList = StrListCast(Doc.UserDoc().linkColorList);
- let color = "white";
+ const link_relationshipList = StrListCast(Doc.UserDoc().link_relationshipList);
+ const linkColorList = StrListCast(Doc.UserDoc().link_ColorList);
+ let color = 'white';
// if this link's relationship property is not default "link", set its color
- if (linkRelationshipList) {
- const relationshipIndex = linkRelationshipList.indexOf(this.props.groupType);
+ if (link_relationshipList) {
+ const relationshipIndex = link_relationshipList.indexOf(this.props.groupType);
const RGBcolor: string = linkColorList[relationshipIndex];
if (RGBcolor) {
//set opacity to 0.25 by modifiying the rgb string
- color = RGBcolor.slice(0, RGBcolor.length - 1) + ", 0.25)";
+ color = RGBcolor.slice(0, RGBcolor.length - 1) + ', 0.25)';
}
}
return color;
- }
+ };
@observable _collapsed = false;
render() {
const set = new Set<Doc>(this.props.group);
const groupItems = Array.from(set.keys()).map(linkDoc => {
- const destination = LinkManager.getOppositeAnchor(linkDoc, this.props.sourceDoc) ||
- LinkManager.getOppositeAnchor(linkDoc, Cast(linkDoc.anchor2, Doc, null).annotationOn === this.props.sourceDoc ? Cast(linkDoc.anchor2, Doc, null) : Cast(linkDoc.anchor1, Doc, null));
- if (destination && this.props.sourceDoc) {
- return <LinkMenuItem key={linkDoc[Id]}
+ const sourceDoc =
+ this.props.docView.anchorViewDoc ??
+ (this.props.docView.rootDoc.type === DocumentType.LINK //
+ ? this.props.docView.props.LayoutTemplateString?.includes('link_anchor_1')
+ ? DocCast(linkDoc.link_anchor_1)
+ : DocCast(linkDoc.link_anchor_2)
+ : this.props.sourceDoc);
+ const destDoc = !sourceDoc
+ ? undefined
+ : this.props.docView.rootDoc.type === DocumentType.LINK
+ ? this.props.docView.props.LayoutTemplateString?.includes('link_anchor_1')
+ ? DocCast(linkDoc.link_anchor_2)
+ : DocCast(linkDoc.link_anchor_1)
+ : LinkManager.getOppositeAnchor(linkDoc, sourceDoc) ||
+ LinkManager.getOppositeAnchor(linkDoc, Cast(linkDoc.link_anchor_2, Doc, null).annotationOn === sourceDoc ? Cast(linkDoc.link_anchor_2, Doc, null) : Cast(linkDoc.link_anchor_1, Doc, null));
+ return !destDoc || !sourceDoc ? null : (
+ <LinkMenuItem
+ key={linkDoc[Id]}
itemHandler={this.props.itemHandler}
groupType={this.props.groupType}
docView={this.props.docView}
linkDoc={linkDoc}
- sourceDoc={this.props.sourceDoc}
- destinationDoc={destination}
+ sourceDoc={sourceDoc}
+ destinationDoc={destDoc}
clearLinkEditor={this.props.clearLinkEditor}
- showEditor={this.props.showEditor}
- menuRef={this._menuRef} />;
- }
+ menuRef={this._menuRef}
+ />
+ );
});
return (
<div className="linkMenu-group" ref={this._menuRef}>
- <div className="linkMenu-group-name" onClick={action(() => this._collapsed = !this._collapsed)} style={{ background: this.getBackgroundColor() }}>
- <p className={this.props.groupType === "*" || this.props.groupType === "" ? "" : "expand-one"}> {this.props.groupType}:</p>
+ <div className="linkMenu-group-name" onClick={action(() => (this._collapsed = !this._collapsed))} style={{ background: this.getBackgroundColor() }}>
+ <p className={this.props.groupType === '*' || this.props.groupType === '' ? '' : 'expand-one'}> {this.props.groupType}:</p>
</div>
- {this._collapsed ? (null) : <div className="linkMenu-group-wrapper">
- {groupItems}
- </div>}
- </div >
+ {this._collapsed ? null : <div className="linkMenu-group-wrapper">{groupItems}</div>}
+ </div>
);
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/linking/LinkMenuItem.scss b/src/client/views/linking/LinkMenuItem.scss
index 8333aa374..806a2c381 100644
--- a/src/client/views/linking/LinkMenuItem.scss
+++ b/src/client/views/linking/LinkMenuItem.scss
@@ -1,7 +1,7 @@
-@import "../global/globalCssVariables";
+@import '../global/globalCssVariables';
.linkMenu-item {
- // border-top: 0.5px solid $medium-gray;
+ // border-top: 0.5px solid $medium-gray;
position: relative;
display: flex;
border-top: 0.5px solid #cdcdcd;
@@ -14,7 +14,6 @@
background-color: white;
-
.linkMenu-name {
position: relative;
width: auto;
@@ -22,9 +21,7 @@
display: flex;
.linkMenu-text {
-
- // padding: 4px 2px;
- //display: inline;
+ width: 100%;
.linkMenu-source-title {
text-decoration: none;
@@ -35,9 +32,7 @@
margin-left: 20px;
}
-
.linkMenu-title-wrapper {
-
display: flex;
align-items: center;
min-height: 20px;
@@ -59,7 +54,7 @@
.linkMenu-destination-title {
text-decoration: none;
- color: #4476F7;
+ color: #4476f7;
font-size: 13px;
line-height: 0.9;
padding-bottom: 2px;
@@ -84,7 +79,6 @@
font-size: 9px;
line-height: 0.9;
margin-left: 20px;
- max-width: 125px;
height: auto;
white-space: break-spaces;
}
@@ -96,9 +90,7 @@
//overflow-wrap: break-word;
user-select: none;
}
-
}
-
}
.linkMenu-item-content {
@@ -114,15 +106,13 @@
}
&:hover {
-
background-color: rgb(201, 239, 252);
.linkMenu-item-buttons {
- display: flex;
+ opacity: 1;
}
.linkMenu-item-content {
-
.linkMenu-destination-title {
text-decoration: underline;
color: rgb(60, 90, 156);
@@ -135,11 +125,9 @@
.linkMenu-item-buttons {
//@extend: right;
- position: absolute;
- top: 50%;
- right: 0;
- transform: translateY(-50%);
- display: none;
+ position: relative;
+ display: flex;
+ opacity: 0;
.button {
width: 20px;
@@ -172,4 +160,4 @@
cursor: pointer;
}
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index 3f9db2612..737d675aa 100644
--- a/src/client/views/linking/LinkMenuItem.tsx
+++ b/src/client/views/linking/LinkMenuItem.tsx
@@ -1,18 +1,20 @@
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tooltip } from '@material-ui/core';
-import { action, observable } from 'mobx';
+import { action, computed, observable } from 'mobx';
import { observer } from 'mobx-react';
-import { Doc, DocListCast } from '../../../fields/Doc';
-import { Cast, StrCast } from '../../../fields/Types';
+import { Doc } from '../../../fields/Doc';
+import { Cast, DocCast, StrCast } from '../../../fields/Types';
import { WebField } from '../../../fields/URLField';
-import { emptyFunction, setupMoveUpEvents } from '../../../Utils';
+import { emptyFunction, returnFalse, setupMoveUpEvents } from '../../../Utils';
import { DocumentType } from '../../documents/DocumentTypes';
-import { DocumentManager } from '../../util/DocumentManager';
import { DragManager } from '../../util/DragManager';
import { LinkFollower } from '../../util/LinkFollower';
import { LinkManager } from '../../util/LinkManager';
-import { DocumentView } from '../nodes/DocumentView';
+import { SelectionManager } from '../../util/SelectionManager';
+import { SettingsManager } from '../../util/SettingsManager';
+import { undoBatch } from '../../util/UndoManager';
+import { DocumentView, DocumentViewInternal, OpenWhere } from '../nodes/DocumentView';
import { LinkDocPreview } from '../nodes/LinkDocPreview';
import './LinkMenuItem.scss';
import React = require('react');
@@ -23,15 +25,14 @@ interface LinkMenuItemProps {
docView: DocumentView;
sourceDoc: Doc;
destinationDoc: Doc;
- clearLinkEditor: () => void;
- showEditor: (linkDoc: Doc) => void;
+ clearLinkEditor?: () => void;
menuRef: React.Ref<HTMLDivElement>;
itemHandler?: (doc: Doc) => void;
}
-// drag links and drop link targets (aliasing them if needed)
+// drag links and drop link targets (embedding them if needed)
export async function StartLinkTargetsDrag(dragEle: HTMLElement, docView: DocumentView, downX: number, downY: number, sourceDoc: Doc, specificLinks?: Doc[]) {
- const draggedDocs = (specificLinks ? specificLinks : DocListCast(sourceDoc.links)).map(link => LinkManager.getOppositeAnchor(link, sourceDoc)).filter(l => l) as Doc[];
+ const draggedDocs = (specificLinks ? specificLinks : LinkManager.Links(sourceDoc)).map(link => LinkManager.getOppositeAnchor(link, sourceDoc)).filter(l => l) as Doc[];
if (draggedDocs.length) {
const moddrag: Doc[] = [];
@@ -41,25 +42,10 @@ export async function StartLinkTargetsDrag(dragEle: HTMLElement, docView: Docume
}
const dragData = new DragManager.DocumentDragData(moddrag.length ? moddrag : draggedDocs);
- dragData.moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean): boolean => {
- docView.props.removeDocument?.(doc);
- addDocument(doc);
- return true;
- };
- const containingView = docView.props.ContainingCollectionView;
- const finishDrag = (e: DragManager.DragCompleteEvent) =>
- e.docDragData &&
- (e.docDragData.droppedDocuments = dragData.draggedDocuments.reduce((droppedDocs, d) => {
- const dvs = DocumentManager.Instance.getDocumentViews(d).filter(dv => dv.props.ContainingCollectionView === containingView);
- if (dvs.length) {
- dvs.forEach(dv => droppedDocs.push(dv.props.Document));
- } else {
- droppedDocs.push(Doc.MakeAlias(d));
- }
- return droppedDocs;
- }, [] as Doc[]));
+ dragData.canEmbed = true;
+ dragData.dropAction = 'embed';
- DragManager.StartDrag([dragEle], dragData, downX, downY, undefined, finishDrag);
+ DragManager.StartDocumentDrag([dragEle], dragData, downX, downY, undefined);
}
}
@@ -68,7 +54,6 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
private _drag = React.createRef<HTMLDivElement>();
_editRef = React.createRef<HTMLDivElement>();
- _buttonRef = React.createRef<HTMLDivElement>();
@observable private _showMore: boolean = false;
@action toggleShowMore(e: React.PointerEvent) {
@@ -76,19 +61,40 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
this._showMore = !this._showMore;
}
- onEdit = (e: React.PointerEvent): void => {
- LinkManager.currentLink = this.props.linkDoc;
+ @computed get sourceAnchor() {
+ const ldoc = this.props.linkDoc;
+ if (this.props.sourceDoc !== ldoc.link_anchor_1 && this.props.sourceDoc !== ldoc.link_anchor_2) {
+ if (Doc.AreProtosEqual(DocCast(DocCast(ldoc.link_anchor_1).annotationOn), this.props.sourceDoc)) return DocCast(ldoc.link_anchor_1);
+ if (Doc.AreProtosEqual(DocCast(DocCast(ldoc.link_anchor_2).annotationOn), this.props.sourceDoc)) return DocCast(ldoc.link_anchor_2);
+ }
+ return this.props.sourceDoc;
+ }
+ @action
+ onEdit = (e: React.PointerEvent) => {
+ LinkManager.currentLink = this.props.linkDoc === LinkManager.currentLink ? undefined : this.props.linkDoc;
+ LinkManager.currentLinkAnchor = this.sourceAnchor;
setupMoveUpEvents(
this,
e,
e => {
- const dragData = new DragManager.DocumentDragData([this.props.linkDoc], 'alias');
- dragData.removeDropProperties = ['hidden'];
+ const dragData = new DragManager.DocumentDragData([this.props.linkDoc], 'embed');
+ dragData.dropPropertiesToRemove = ['hidden'];
DragManager.StartDocumentDrag([this._editRef.current!], dragData, e.x, e.y);
return true;
},
emptyFunction,
- () => this.props.showEditor(this.props.linkDoc)
+ action(() => {
+ const trail = DocCast(this.props.docView.rootDoc.presTrail);
+ if (trail) {
+ Doc.ActivePresentation = trail;
+ DocumentViewInternal.addDocTabFunc(trail, OpenWhere.replaceRight);
+ } else {
+ SelectionManager.SelectView(this.props.docView, false);
+ if ((SettingsManager.propertiesWidth ?? 0) < 100) {
+ SettingsManager.propertiesWidth = 250;
+ }
+ }
+ })
);
};
@@ -100,37 +106,36 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
const eleClone: any = this._drag.current!.cloneNode(true);
eleClone.style.transform = `translate(${e.x}px, ${e.y}px)`;
StartLinkTargetsDrag(eleClone, this.props.docView, e.x, e.y, this.props.sourceDoc, [this.props.linkDoc]);
- this.props.clearLinkEditor();
+ this.props.clearLinkEditor?.();
return true;
},
emptyFunction,
() => {
- this.props.clearLinkEditor();
+ this.props.clearLinkEditor?.();
if (this.props.itemHandler) {
this.props.itemHandler?.(this.props.linkDoc);
} else {
const focusDoc =
- Cast(this.props.linkDoc.anchor1, Doc, null)?.annotationOn === this.props.sourceDoc
- ? Cast(this.props.linkDoc.anchor1, Doc, null)
- : Cast(this.props.linkDoc.anchor2, Doc, null)?.annotationOn === this.props.sourceDoc
- ? Cast(this.props.linkDoc.anchor12, Doc, null)
+ Cast(this.props.linkDoc.link_anchor_1, Doc, null)?.annotationOn === this.props.sourceDoc
+ ? Cast(this.props.linkDoc.link_anchor_1, Doc, null)
+ : Cast(this.props.linkDoc.link_anchor_2, Doc, null)?.annotationOn === this.props.sourceDoc
+ ? Cast(this.props.linkDoc.link_anchor_12, Doc, null)
: undefined;
- if (focusDoc) this.props.docView.ComponentView?.scrollFocus?.(focusDoc, true);
- LinkFollower.FollowLink(this.props.linkDoc, this.props.sourceDoc, this.props.docView.props, false);
+ if (focusDoc) this.props.docView.props.focus(focusDoc, { instant: true });
+ LinkFollower.FollowLink(this.props.linkDoc, this.props.sourceDoc, false);
}
}
);
};
+ deleteLink = (e: React.PointerEvent): void => setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => LinkManager.Instance.deleteLink(this.props.linkDoc))));
+
render() {
const destinationIcon = Doc.toIcon(this.props.destinationDoc) as any as IconProp;
const title = StrCast(this.props.destinationDoc.title).length > 18 ? StrCast(this.props.destinationDoc.title).substr(0, 14) + '...' : this.props.destinationDoc.title;
- // ...
- // from anika to bob: here's where the text that is specifically linked would show up (linkDoc.storedText)
- // ...
const source =
this.props.sourceDoc.type === DocumentType.RTF
? this.props.linkDoc.storedText
@@ -141,24 +146,34 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
: undefined;
return (
- <div className="linkMenu-item">
- <div className={'linkMenu-item-content expand-two'}>
+ <div className="linkMenu-item" style={{ background: LinkManager.currentLink === this.props.linkDoc ? 'lightBlue' : undefined }}>
+ <div className="linkMenu-item-content expand-two">
<div
ref={this._drag}
className="linkMenu-name" //title="drag to view target. click to customize."
- onPointerLeave={LinkDocPreview.Clear}
- onPointerEnter={e =>
- this.props.linkDoc &&
- LinkDocPreview.SetLinkInfo({
- docProps: this.props.docView.props,
- linkSrc: this.props.sourceDoc,
- linkDoc: this.props.linkDoc,
- showHeader: false,
- location: [this._drag.current?.getBoundingClientRect().right ?? 100, this._drag.current?.getBoundingClientRect().top ?? e.clientY],
- })
- }
onPointerDown={this.onLinkButtonDown}>
- <div className="linkMenu-text">
+ <div className="linkMenu-item-buttons">
+ <Tooltip title={<div className="dash-tooltip">Edit Link</div>}>
+ <div className="button" style={{ background: LinkManager.currentLink === this.props.linkDoc ? 'black' : 'gray' }} ref={this._editRef} onPointerDown={this.onEdit} onClick={e => e.stopPropagation()}>
+ <FontAwesomeIcon className="fa-icon" icon="edit" size="sm" />
+ </div>
+ </Tooltip>
+ </div>
+ <div
+ className="linkMenu-text"
+ onPointerLeave={LinkDocPreview.Clear}
+ onPointerEnter={e =>
+ this.props.linkDoc &&
+ this.props.clearLinkEditor &&
+ LinkDocPreview.SetLinkInfo({
+ docProps: this.props.docView.props,
+ linkSrc: this.props.sourceDoc,
+ linkDoc: this.props.linkDoc,
+ showHeader: false,
+ location: [(this._drag.current?.getBoundingClientRect().left ?? 100) + 40, (this._drag.current?.getBoundingClientRect().top ?? e.clientY) + 25],
+ noPreview: false,
+ })
+ }>
{source ? (
<p className="linkMenu-source-title">
{' '}
@@ -173,13 +188,13 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
{this.props.linkDoc.linksToAnnotation && Cast(this.props.destinationDoc.data, WebField)?.url.href === this.props.linkDoc.annotationUri ? 'Annotation in' : ''} {StrCast(title)}
</p>
</div>
- {!this.props.linkDoc.description ? null : <p className="linkMenu-description">{StrCast(this.props.linkDoc.description)}</p>}
+ {!this.props.linkDoc.link_description ? null : <p className="linkMenu-description">{StrCast(this.props.linkDoc.link_description)}</p>}
</div>
- <div className="linkMenu-item-buttons" ref={this._buttonRef}>
- <Tooltip title={<div className="dash-tooltip">Edit Link</div>}>
- <div className="button" ref={this._editRef} onPointerDown={this.onEdit} onClick={e => e.stopPropagation()}>
- <FontAwesomeIcon className="fa-icon" icon="edit" size="sm" />
+ <div className="linkMenu-item-buttons">
+ <Tooltip title={<div className="dash-tooltip">Delete Link</div>}>
+ <div className="button" style={{ background: 'red' }} onPointerDown={this.deleteLink} onClick={e => e.stopPropagation()}>
+ <FontAwesomeIcon className="fa-icon" icon="trash" size="sm" />
</div>
</Tooltip>
</div>
diff --git a/src/client/views/linking/LinkPopup.scss b/src/client/views/linking/LinkPopup.scss
index 60c9ebfcd..b20ad9476 100644
--- a/src/client/views/linking/LinkPopup.scss
+++ b/src/client/views/linking/LinkPopup.scss
@@ -1,7 +1,7 @@
.linkPopup-container {
background: white;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23);
- top: 35px;
+ top: 0;
height: 200px;
width: 200px;
position: absolute;
@@ -38,8 +38,7 @@
}
}
-
.searchBox-container {
background: pink;
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/linking/LinkPopup.tsx b/src/client/views/linking/LinkPopup.tsx
index 0bcb68f82..745efea08 100644
--- a/src/client/views/linking/LinkPopup.tsx
+++ b/src/client/views/linking/LinkPopup.tsx
@@ -11,10 +11,13 @@ import { SearchBox } from '../search/SearchBox';
import { DefaultStyleProvider } from '../StyleProvider';
import './LinkPopup.scss';
import React = require('react');
+import { OpenWhere } from '../nodes/DocumentView';
interface LinkPopupProps {
showPopup: boolean;
linkFrom?: () => Doc | undefined;
+ linkCreateAnchor?: () => Doc | undefined;
+ linkCreated?: (link: Doc) => void;
// groupType: string;
// linkDoc: Doc;
// docView: DocumentView;
@@ -32,14 +35,10 @@ export class LinkPopup extends React.Component<LinkPopupProps> {
// TODO: should check for valid URL
@undoBatch
- makeLinkToURL = (target: string, lcoation: string) => {
- ((this.view as any)?.TextView as FormattedTextBox).makeLinkAnchor(undefined, 'onRadd:rightight', target, target);
- };
+ makeLinkToURL = (target: string, lcoation: string) => ((this.view as any)?.TextView as FormattedTextBox).makeLinkAnchor(undefined, OpenWhere.addRight, target, target);
@action
- onLinkChange = (e: React.ChangeEvent<HTMLInputElement>) => {
- this.linkURL = e.target.value;
- };
+ onLinkChange = (e: React.ChangeEvent<HTMLInputElement>) => (this.linkURL = e.target.value);
getPWidth = () => 500;
getPHeight = () => 500;
@@ -67,9 +66,10 @@ export class LinkPopup extends React.Component<LinkPopupProps> {
Document={Doc.MySearcher}
DataDoc={Doc.MySearcher}
linkFrom={linkDoc}
+ linkCreateAnchor={this.props.linkCreateAnchor}
linkSearch={true}
+ linkCreated={this.props.linkCreated}
fieldKey="data"
- dropAction="move"
isSelected={returnTrue}
isContentActive={returnTrue}
select={returnTrue}
@@ -84,15 +84,13 @@ export class LinkPopup extends React.Component<LinkPopupProps> {
PanelWidth={this.getPWidth}
PanelHeight={this.getPHeight}
renderDepth={0}
- focus={DocUtils.DefaultFocus}
+ focus={emptyFunction}
docViewPath={returnEmptyDoclist}
whenChildContentsActiveChanged={emptyFunction}
bringToFront={emptyFunction}
- docFilters={returnEmptyFilter}
- docRangeFilters={returnEmptyFilter}
+ childFilters={returnEmptyFilter}
+ childFiltersByRanges={returnEmptyFilter}
searchFilterDocs={returnEmptyDoclist}
- ContainingCollectionView={undefined}
- ContainingCollectionDoc={undefined}
/>
</div>
</div>
diff --git a/src/client/views/linking/LinkRelationshipSearch.tsx b/src/client/views/linking/LinkRelationshipSearch.tsx
index 53da880e4..9662b2fea 100644
--- a/src/client/views/linking/LinkRelationshipSearch.tsx
+++ b/src/client/views/linking/LinkRelationshipSearch.tsx
@@ -1,8 +1,8 @@
-import { observer } from "mobx-react";
+import { observer } from 'mobx-react';
import './LinkEditor.scss';
-import React = require("react");
+import React = require('react');
-interface LinkRelationshipSearchProps {
+interface link_relationshipSearchProps {
results: string[] | undefined;
display: string;
//callback fn to set rel + hide dropdown upon setting
@@ -10,48 +10,48 @@ interface LinkRelationshipSearchProps {
toggleSearch: () => void;
}
@observer
-export class LinkRelationshipSearch extends React.Component<LinkRelationshipSearchProps> {
-
+export class link_relationshipSearch extends React.Component<link_relationshipSearchProps> {
handleResultClick = (e: React.MouseEvent) => {
const relationship = (e.target as HTMLParagraphElement).textContent;
if (relationship) {
this.props.handleRelationshipSearchChange(relationship);
}
- }
+ };
handleMouseEnter = () => {
this.props.toggleSearch();
- }
+ };
handleMouseLeave = () => {
this.props.toggleSearch();
- }
+ };
/**
- * Render an empty div to increase the height of LinkEditor to accommodate 2+ results
- */
+ * Render an empty div to increase the height of LinkEditor to accommodate 2+ results
+ */
emptyDiv = () => {
- if (this.props.results && this.props.results.length > 2 && this.props.display === "block") {
- return <div style={{ height: "50px" }} />;
+ if (this.props.results && this.props.results.length > 2 && this.props.display === 'block') {
+ return <div style={{ height: '50px' }} />;
}
- }
+ };
render() {
return (
<div className="linkEditor-relationship-dropdown-container">
- <div className="linkEditor-relationship-dropdown"
- style={{ display: this.props.display }}
- onMouseEnter={this.handleMouseEnter}
- onMouseLeave={this.handleMouseLeave}
- >
- { // return a dropdown of relationship results if there exist results
- this.props.results
- ? this.props.results.map(result => {
- return <p key={result} onClick={this.handleResultClick}>
- {result}
- </p>;
+ <div className="linkEditor-relationship-dropdown" style={{ display: this.props.display }} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
+ {
+ // return a dropdown of relationship results if there exist results
+ this.props.results ? (
+ this.props.results.map(result => {
+ return (
+ <p key={result} onClick={this.handleResultClick}>
+ {result}
+ </p>
+ );
})
- : <p>No matching relationships</p>
+ ) : (
+ <p>No matching relationships</p>
+ )
}
</div>
@@ -60,4 +60,4 @@ export class LinkRelationshipSearch extends React.Component<LinkRelationshipSear
</div>
);
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 497b4993c..6558d215a 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -7,10 +7,11 @@ import { Doc, DocListCast } from '../../../fields/Doc';
import { ComputedField } from '../../../fields/ScriptField';
import { Cast, DateCast, NumCast } from '../../../fields/Types';
import { AudioField, nullAudio } from '../../../fields/URLField';
-import { emptyFunction, formatTime, OmitKeys, returnFalse, setupMoveUpEvents } from '../../../Utils';
+import { emptyFunction, formatTime, returnFalse, setupMoveUpEvents } from '../../../Utils';
import { DocUtils } from '../../documents/Documents';
import { Networking } from '../../Network';
import { DragManager } from '../../util/DragManager';
+import { LinkManager } from '../../util/LinkManager';
import { undoBatch } from '../../util/UndoManager';
import { CollectionStackedTimeline, TrimScope } from '../collections/CollectionStackedTimeline';
import { ContextMenu } from '../ContextMenu';
@@ -18,6 +19,7 @@ import { ContextMenuProps } from '../ContextMenuItem';
import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent';
import './AudioBox.scss';
import { FieldView, FieldViewProps } from './FieldView';
+import { PinProps, PresBox } from './trails';
/**
* AudioBox
@@ -37,7 +39,7 @@ declare class MediaRecorder {
constructor(e: any); // whatever MediaRecorder has
}
-enum media_state {
+export enum media_state {
PendingRecording = 'pendingRecording',
Recording = 'recording',
Paused = 'paused',
@@ -74,7 +76,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
return DateCast(this.dataDoc[this.fieldKey + '-recordingStart'])?.date.getTime();
}
@computed get rawDuration() {
- return NumCast(this.dataDoc[`${this.fieldKey}-duration`]);
+ return NumCast(this.dataDoc[`${this.fieldKey}_duration`]);
} // bcz: shouldn't be needed since it's computed from audio element
// mehek: not 100% sure but i think due to the order in which things are loaded this is necessary ^^
// if you get rid of it and set the value to 0 the timeline and waveform will set their bounds incorrectly
@@ -83,7 +85,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
return this.props.PanelHeight() < 50;
} // used to collapse timeline when node is shrunk
@computed get links() {
- return DocListCast(this.dataDoc.links);
+ return LinkManager.Links(this.dataDoc);
}
@computed get mediaState() {
return this.dataDoc.mediaState as media_state;
@@ -111,8 +113,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
@action
componentDidMount() {
- this.props.setContentView?.(this); // this tells the DocumentView that this AudioBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link.
-
+ this.props.setContentView?.(this);
if (this.path) {
this.mediaState = media_state.Paused;
this.setPlayheadTime(NumCast(this.layoutDoc.clipStart));
@@ -122,27 +123,29 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
}
getLinkData(l: Doc) {
- let la1 = l.anchor1 as Doc;
- let la2 = l.anchor2 as Doc;
+ let la1 = l.link_anchor_1 as Doc;
+ let la2 = l.link_anchor_2 as Doc;
const linkTime = this.timeline?.anchorStart(la2) || this.timeline?.anchorStart(la1) || 0;
if (Doc.AreProtosEqual(la1, this.dataDoc)) {
- la1 = l.anchor2 as Doc;
- la2 = l.anchor1 as Doc;
+ la1 = l.link_anchor_2 as Doc;
+ la2 = l.link_anchor_1 as Doc;
}
return { la1, la2, linkTime };
}
- getAnchor = () => {
- return (
+ getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
+ const anchor =
CollectionStackedTimeline.createAnchor(
this.rootDoc,
this.dataDoc,
this.annotationKey,
- '_timecodeToShow' /* audioStart */,
- '_timecodeToHide' /* audioEnd */,
- this._ele?.currentTime || Cast(this.props.Document._currentTimecode, 'number', null) || (this.mediaState === media_state.Recording ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined)
- ) || this.rootDoc
- );
+ this._ele?.currentTime || Cast(this.props.Document._layout_currentTimecode, 'number', null) || (this.mediaState === media_state.Recording ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined),
+ undefined,
+ undefined,
+ addAsAnnotation
+ ) || this.rootDoc;
+ PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), temporal: true } }, this.rootDoc);
+ return anchor;
};
// updates timecode and shows it in timeline, follows links at time
@@ -152,12 +155,12 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
this.links
.map(l => this.getLinkData(l))
.forEach(({ la1, la2, linkTime }) => {
- if (linkTime > NumCast(this.layoutDoc._currentTimecode) && linkTime < this._ele!.currentTime) {
+ if (linkTime > NumCast(this.layoutDoc._layout_currentTimecode) && linkTime < this._ele!.currentTime) {
Doc.linkFollowHighlight(la1);
}
});
- this.layoutDoc._currentTimecode = this._ele.currentTime;
- this.timeline?.scrollToTime(NumCast(this.layoutDoc._currentTimecode));
+ this.layoutDoc._layout_currentTimecode = this._ele.currentTime;
+ this.timeline?.scrollToTime(NumCast(this.layoutDoc._layout_currentTimecode));
}
};
@@ -194,8 +197,9 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
// removes from currently playing display
@action
removeCurrentlyPlaying = () => {
- if (CollectionStackedTimeline.CurrentlyPlaying) {
- const index = CollectionStackedTimeline.CurrentlyPlaying.indexOf(this.layoutDoc);
+ const docView = this.props.DocumentView?.();
+ if (CollectionStackedTimeline.CurrentlyPlaying && docView) {
+ const index = CollectionStackedTimeline.CurrentlyPlaying.indexOf(docView);
index !== -1 && CollectionStackedTimeline.CurrentlyPlaying.splice(index, 1);
}
};
@@ -203,11 +207,12 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
// adds doc to currently playing display
@action
addCurrentlyPlaying = () => {
+ const docView = this.props.DocumentView?.();
if (!CollectionStackedTimeline.CurrentlyPlaying) {
CollectionStackedTimeline.CurrentlyPlaying = [];
}
- if (CollectionStackedTimeline.CurrentlyPlaying.indexOf(this.layoutDoc) === -1) {
- CollectionStackedTimeline.CurrentlyPlaying.push(this.layoutDoc);
+ if (docView && CollectionStackedTimeline.CurrentlyPlaying.indexOf(docView) === -1) {
+ CollectionStackedTimeline.CurrentlyPlaying.push(docView);
}
};
@@ -216,7 +221,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
if (this.mediaState === media_state.Recording) {
setTimeout(this.updateRecordTime, 30);
if (!this._paused) {
- this.layoutDoc._currentTimecode = (new Date().getTime() - this._recordStart - this._pausedTime) / 1000;
+ this.layoutDoc._layout_currentTimecode = (new Date().getTime() - this._recordStart - this._pausedTime) / 1000;
}
}
};
@@ -228,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);
}
@@ -248,7 +253,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
this._recorder = undefined;
const now = new Date().getTime();
this._paused && (this._pausedTime += now - this._pauseStart);
- this.dataDoc[this.fieldKey + '-duration'] = (now - this._recordStart - this._pausedTime) / 1000;
+ this.dataDoc[this.fieldKey + '_duration'] = (now - this._recordStart - this._pausedTime) / 1000;
this.mediaState = media_state.Paused;
this._stream?.getAudioTracks()[0].stop();
const ind = DocUtils.ActiveRecordings.indexOf(this);
@@ -322,18 +327,28 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
}
};
- // pause play back
+ IsPlaying = () => this.mediaState === media_state.Playing;
+ TogglePause = () => {
+ if (this.mediaState === media_state.Paused) this.Play();
+ else this.pause();
+ };
+ // pause playback without removing from the playback list to allow user to play it again.
@action
- Pause = () => {
+ pause = () => {
if (this._ele) {
this._ele.pause();
this.mediaState = media_state.Paused;
// if paused in the middle of playback, prevents restart on next play
if (!this._finished) clearTimeout(this._play);
- this.removeCurrentlyPlaying();
}
};
+ // pause playback and remove from playback list
+ @action
+ Pause = () => {
+ this.pause();
+ this.removeCurrentlyPlaying();
+ };
// for dictation button, creates a text document for dictation
onFile = (e: any) => {
@@ -347,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);
}
@@ -407,11 +422,11 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
if (!this.layoutDoc.dontAutoPlayFollowedLinks) {
this.playFrom(this.timeline?.anchorStart(link) || 0, this.timeline?.anchorEnd(link));
} else {
- this._ele!.currentTime = this.layoutDoc._currentTimecode = this.timeline?.anchorStart(link) || 0;
+ this._ele!.currentTime = this.layoutDoc._layout_currentTimecode = this.timeline?.anchorStart(link) || 0;
}
} else {
this.links
- .filter(l => l.anchor1 === link || l.anchor2 === link)
+ .filter(l => l.link_anchor_1 === link || l.link_anchor_2 === link)
.forEach(l => {
const { la1, la2 } = this.getLinkData(l);
const startTime = this.timeline?.anchorStart(la1) || this.timeline?.anchorStart(la2);
@@ -420,7 +435,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
if (!this.layoutDoc.dontAutoPlayFollowedLinks) {
this.playFrom(startTime, endTime);
} else {
- this._ele!.currentTime = this.layoutDoc._currentTimecode = startTime;
+ this._ele!.currentTime = this.layoutDoc._layout_currentTimecode = startTime;
}
}
});
@@ -430,9 +445,9 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
@action
timelineWhenChildContentsActiveChanged = (isActive: boolean) => this.props.whenChildContentsActiveChanged((this._isAnyChildContentActive = isActive));
- timelineScreenToLocal = () => this.props.ScreenToLocalTransform().translate(0, -AudioBox.bottomControlsHeight);
+ timelineScreenToLocal = () => this.props.ScreenToLocalTransform().translate(0, -AudioBox.topControlsHeight);
- setPlayheadTime = (time: number) => (this._ele!.currentTime = this.layoutDoc._currentTimecode = time);
+ setPlayheadTime = (time: number) => (this._ele!.currentTime /*= this.layoutDoc._layout_currentTimecode*/ = time);
playing = () => this.mediaState === media_state.Playing;
@@ -532,7 +547,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
<div className="record-button" onPointerDown={this._paused ? this.recordPlay : this.recordPause}>
<FontAwesomeIcon size="2x" icon={this._paused ? 'play' : 'pause'} />
</div>
- <div className="record-timecode">{formatTime(Math.round(NumCast(this.layoutDoc._currentTimecode)))}</div>
+ <div className="record-timecode">{formatTime(Math.round(NumCast(this.layoutDoc._layout_currentTimecode)))}</div>
</div>
) : (
<div className="audiobox-start-record" onPointerDown={this.Record}>
@@ -608,7 +623,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
{this.audio}
<div className="audiobox-timecodes">
- <div className="timecode-current">{this.timeline && formatTime(Math.round(NumCast(this.layoutDoc._currentTimecode) - NumCast(this.timeline.clipStart)))}</div>
+ <div className="timecode-current">{this.timeline && formatTime(Math.round(NumCast(this.layoutDoc._layout_currentTimecode) - NumCast(this.timeline.clipStart)))}</div>
{this.miniPlayer ? (
<div>/</div>
) : (
@@ -619,15 +634,11 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
step="0.1"
min="1"
max="5"
- value={this.timeline?._zoomFactor}
+ value={this.timeline?._zoomFactor ?? 1}
className="toolbar-slider"
id="zoom-slider"
- onPointerDown={(e: React.PointerEvent) => {
- e.stopPropagation();
- }}
- onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
- this.zoom(Number(e.target.value));
- }}
+ onPointerDown={(e: React.PointerEvent) => e.stopPropagation()}
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.zoom(Number(e.target.value))}
/>
</div>
)}
@@ -643,7 +654,9 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
return (
<CollectionStackedTimeline
ref={action((r: any) => (this._stackedTimeline = r))}
- {...OmitKeys(this.props, ['CollectionFreeFormDocumentView']).omit}
+ {...this.props}
+ CollectionFreeFormDocumentView={undefined}
+ dataFieldKey={this.fieldKey}
fieldKey={this.annotationKey}
dictationKey={this.fieldKey + '-dictation'}
mediaPath={this.path}
@@ -651,7 +664,6 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
startTag={'_timecodeToShow' /* audioStart */}
endTag={'_timecodeToHide' /* audioEnd */}
bringToFront={emptyFunction}
- CollectionView={undefined}
playFrom={this.playFrom}
setTime={this.setPlayheadTime}
playing={this.playing}
@@ -678,7 +690,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
<audio
ref={this.setRef}
className={`audiobox-control${this.props.isContentActive() ? '-interactive' : ''}`}
- onLoadedData={action(e => this._ele?.duration && this._ele?.duration !== Infinity && (this.dataDoc[this.fieldKey + '-duration'] = this._ele.duration))}>
+ onLoadedData={action(e => this._ele?.duration && this._ele?.duration !== Infinity && (this.dataDoc[this.fieldKey + '_duration'] = this._ele.duration))}>
<source src={this.path} type="audio/mpeg" />
Not supported.
</audio>
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.scss b/src/client/views/nodes/CollectionFreeFormDocumentView.scss
index 724394025..f99011b8f 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.scss
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.scss
@@ -5,5 +5,5 @@
touch-action: manipulation;
top: 0;
left: 0;
- pointer-events: none;
-} \ No newline at end of file
+ //pointer-events: none;
+}
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 260c98816..6710cee63 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -1,31 +1,28 @@
import { action, computed, observable } from 'mobx';
import { observer } from 'mobx-react';
import { Doc, Opt } from '../../../fields/Doc';
-import { InkField } from '../../../fields/InkField';
import { List } from '../../../fields/List';
import { listSpec } from '../../../fields/Schema';
import { ComputedField } from '../../../fields/ScriptField';
import { Cast, NumCast, StrCast } from '../../../fields/Types';
import { TraceMobx } from '../../../fields/util';
import { numberRange } from '../../../Utils';
-import { DocumentType } from '../../documents/DocumentTypes';
import { DocumentManager } from '../../util/DocumentManager';
import { SelectionManager } from '../../util/SelectionManager';
import { Transform } from '../../util/Transform';
import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
import { DocComponent } from '../DocComponent';
+import { InkingStroke } from '../InkingStroke';
import { StyleProp } from '../StyleProvider';
import './CollectionFreeFormDocumentView.scss';
-import { DocumentView, DocumentViewProps } from './DocumentView';
+import { DocumentView, DocumentViewProps, OpenWhere } from './DocumentView';
import React = require('react');
export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
- dataProvider?: (doc: Doc, replica: string) => { x: number; y: number; zIndex?: number; color?: string; backgroundColor?: string; opacity?: number; highlight?: boolean; z: number; transition?: string } | undefined;
+ dataProvider?: (doc: Doc, replica: string) => { x: number; y: number; zIndex?: number; rotation?: number; color?: string; backgroundColor?: string; opacity?: number; highlight?: boolean; z: number; transition?: string } | undefined;
sizeProvider?: (doc: Doc, replica: string) => { width: number; height: number } | undefined;
renderCutoffProvider: (doc: Doc) => boolean;
zIndex?: number;
- highlight?: boolean;
- rotation: number;
dataTransition?: string;
replica: string;
CollectionFreeFormView: CollectionFreeFormView;
@@ -39,13 +36,15 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
{ key: 'x' },
{ key: 'y' },
{ key: '_rotation', val: 0 },
- { key: '_scrollTop' },
+ { key: '_layout_scrollTop' },
{ key: 'opacity', val: 1 },
- { key: 'viewScale', val: 1 },
- { key: 'panX' },
- { key: 'panY' },
+ { key: '_currentFrame' },
+ { key: 'freeform_scale', val: 1 },
+ { key: 'freeform_scale', val: 1 },
+ { key: 'freeform_panX' },
+ { key: 'freeform_panY' },
]; // fields that are configured to be animatable using animation frames
- public static animStringFields = ['backgroundColor', 'color']; // fields that are configured to be animatable using animation frames
+ public static animStringFields = ['backgroundColor', 'color', 'fillColor']; // fields that are configured to be animatable using animation frames
public static animDataFields = (doc: Doc) => (Doc.LayoutFieldKey(doc) ? [Doc.LayoutFieldKey(doc)] : []); // fields that are configured to be animatable using animation frames
@observable _animPos: number[] | undefined = undefined;
@observable _contentView: DocumentView | undefined | null;
@@ -55,7 +54,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
}
get transform() {
- return `translate(${this.X}px, ${this.Y}px) rotate(${NumCast(this.Document.rotation, this.props.rotation)}deg)`;
+ return `translate(${this.X}px, ${this.Y}px) rotate(${NumCast(this.Rot, this.Rot)}deg)`;
}
get X() {
return this.dataProvider?.x ?? NumCast(this.Document.x);
@@ -66,6 +65,9 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
get ZInd() {
return this.dataProvider?.zIndex ?? NumCast(this.Document.zIndex);
}
+ get Rot() {
+ return this.dataProvider?.rotation ?? NumCast(this.Document._rotation);
+ }
get Opacity() {
return this.dataProvider?.opacity;
}
@@ -75,9 +77,6 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
get Color() {
return this.dataProvider?.color ?? Cast(this.Document._color, 'string', null);
}
- get Highlight() {
- return this.dataProvider?.highlight;
- }
@computed get dataProvider() {
return this.props.dataProvider?.(this.props.Document, this.props.replica);
}
@@ -86,13 +85,14 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
}
styleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string) => {
- if (doc === this.layoutDoc)
+ if (doc === this.layoutDoc) {
// prettier-ignore
switch (property) {
case StyleProp.Opacity: return this.Opacity; // only change the opacity for this specific document, not its children
case StyleProp.BackgroundColor: return this.BackgroundColor;
case StyleProp.Color: return this.Color;
}
+ }
return this.props.styleProvider?.(doc, props, property);
};
@@ -110,6 +110,15 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
}, {} as { [val: string]: Opt<string> });
}
+ public static setStringValues(time: number, d: Doc, vals: { [val: string]: Opt<string> }) {
+ const timecode = Math.round(time);
+ Object.keys(vals).forEach(val => {
+ const findexed = Cast(d[`${val}-indexed`], listSpec('string'), []).slice();
+ findexed[timecode] = vals[val] as any as string;
+ d[`${val}-indexed`] = new List<string>(findexed);
+ });
+ }
+
public static setValues(time: number, d: Doc, vals: { [val: string]: Opt<number> }) {
const timecode = Math.round(time);
Object.keys(vals).forEach(val => {
@@ -119,47 +128,6 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
});
}
- public static updateKeyframe(docs: Doc[], time: number, targetDoc?: Doc) {
- const timecode = Math.round(time);
- docs.forEach(
- action(doc => {
- doc._viewTransition = doc.dataTransition = 'all 1s';
- CollectionFreeFormDocumentView.animFields.forEach(val => {
- const findexed = Cast(doc[`${val.key}-indexed`], listSpec('number'), null);
- findexed?.length <= timecode + 1 && findexed.push(undefined as any as number);
- });
- CollectionFreeFormDocumentView.animStringFields.forEach(val => {
- const findexed = Cast(doc[`${val}-indexed`], listSpec('string'), null);
- findexed?.length <= timecode + 1 && findexed.push(undefined as any as string);
- });
- CollectionFreeFormDocumentView.animDataFields(doc).forEach(val => {
- const findexed = Cast(doc[`${val}-indexed`], listSpec(InkField), null);
- findexed?.length <= timecode + 1 && findexed.push(undefined as any);
- });
- })
- );
- setTimeout(
- () =>
- docs.forEach(doc => {
- doc._viewTransition = undefined;
- doc.dataTransition = 'inherit';
- }),
- 1010
- );
- }
-
- public static gotoKeyframe(docs: Doc[], duration = 1000) {
- docs.forEach(doc => (doc._viewTransition = doc.dataTransition = `all ${duration}ms`));
- setTimeout(
- () =>
- docs.forEach(doc => {
- doc._viewTransition = undefined;
- doc.dataTransition = 'inherit';
- }),
- 1010
- );
- }
-
public static setupZoom(doc: Doc, targDoc: Doc) {
const width = new List<number>();
const height = new List<number>();
@@ -184,17 +152,18 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
}
CollectionFreeFormDocumentView.animFields.forEach(val => (doc[val.key] = ComputedField.MakeInterpolatedNumber(val.key, 'activeFrame', doc, currTimecode, val.val)));
CollectionFreeFormDocumentView.animStringFields.forEach(val => (doc[val] = ComputedField.MakeInterpolatedString(val, 'activeFrame', doc, currTimecode)));
- CollectionFreeFormDocumentView.animDataFields(doc).forEach(val => (Doc.GetProto(doc)[val] = ComputedField.MakeInterpolatedDataField(val, 'activeFrame', Doc.GetProto(doc), currTimecode)));
- const targetDoc = doc.type === DocumentType.RTF ? Doc.GetProto(doc) : doc; // data fields, like rtf 'text' exist on the data doc, so
- doc !== targetDoc && (targetDoc.context = doc.context); // the computed fields don't see the layout doc -- need to copy the context to the data doc (HACK!!!) and set the activeFrame on the data doc (HACK!!!)
- targetDoc.activeFrame = ComputedField.MakeFunction('self.context?._currentFrame||0');
+ CollectionFreeFormDocumentView.animDataFields(doc).forEach(val => (doc[val] = ComputedField.MakeInterpolatedDataField(val, 'activeFrame', doc, currTimecode)));
+ const targetDoc = doc; // data fields, like rtf 'text' exist on the data doc, so
+ //doc !== targetDoc && (targetDoc.embedContainer = doc.embedContainer); // the computed fields don't see the layout doc -- need to copy the embedContainer to the data doc (HACK!!!) and set the activeFrame on the data doc (HACK!!!)
+ targetDoc.activeFrame = ComputedField.MakeFunction('self.embedContainer?._currentFrame||0');
targetDoc.dataTransition = 'inherit';
});
}
@action public float = () => {
- const { Document: topDoc, ContainingCollectionView: container } = this.props;
- const screenXf = container?.screenToLocalTransform();
+ const topDoc = this.rootDoc;
+ const containerDocView = this.props.docViewPath().lastElement();
+ const screenXf = containerDocView?.screenToLocalTransform();
if (screenXf) {
SelectionManager.DeselectAll();
if (topDoc.z) {
@@ -203,7 +172,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
topDoc.x = spt[0];
topDoc.y = spt[1];
this.props.removeDocument?.(topDoc);
- this.props.addDocTab(topDoc, 'inParent');
+ this.props.addDocTab(topDoc, OpenWhere.inParentFromScreen);
} else {
const spt = this.screenToLocalTransform().inverse().transformPoint(0, 0);
const fpt = screenXf.transformPoint(spt[0], spt[1]);
@@ -211,7 +180,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
topDoc.x = fpt[0];
topDoc.y = fpt[1];
}
- setTimeout(() => SelectionManager.SelectView(DocumentManager.Instance.getDocumentView(topDoc, container)!, false), 0);
+ setTimeout(() => SelectionManager.SelectView(DocumentManager.Instance.getDocumentView(topDoc, containerDocView), false), 0);
}
};
@@ -222,7 +191,6 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
panelWidth = () => this.sizeProvider?.width || this.props.PanelWidth?.();
panelHeight = () => this.sizeProvider?.height || this.props.PanelHeight?.();
screenToLocalTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-this.X, -this.Y);
- focusDoc = (doc: Doc) => this.props.focus(doc, {});
returnThis = () => this;
render() {
TraceMobx();
@@ -234,18 +202,19 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
PanelWidth: this.panelWidth,
PanelHeight: this.panelHeight,
};
+ const isInk = StrCast(this.layoutDoc.layout).includes(InkingStroke.name) && !this.props.LayoutTemplateString && !this.layoutDoc._stroke_isInkMask;
return (
<div
- className={'collectionFreeFormDocumentView-container'}
+ className="collectionFreeFormDocumentView-container"
style={{
- outline: this.Highlight ? 'orange solid 2px' : '',
width: this.panelWidth(),
height: this.panelHeight(),
transform: this.transform,
transformOrigin: '50% 50%',
transition: this.dataProvider?.transition ?? (this.props.dataTransition ? this.props.dataTransition : this.dataProvider ? this.dataProvider.transition : StrCast(this.layoutDoc.dataTransition)),
zIndex: this.ZInd,
- display: this.ZInd === -99 ? 'none' : undefined,
+ display: this.sizeProvider?.width ? undefined : 'none',
+ pointerEvents: isInk ? 'none' : undefined,
}}>
{this.props.renderCutoffProvider(this.props.Document) ? (
<div style={{ position: 'absolute', width: this.panelWidth(), height: this.panelHeight(), background: 'lightGreen' }} />
diff --git a/src/client/views/nodes/ColorBox.tsx b/src/client/views/nodes/ColorBox.tsx
index c229a966a..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,10 +49,10 @@ export class ColorBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
render() {
- const scaling = Math.min(this.layoutDoc.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.isContentActive() ? '-interactive' : ''}`}
+ className={`colorBox-container${this.props.isContentActive() ? '-interactive' : ''}`}
onPointerDown={e => e.button === 0 && !e.ctrlKey && e.stopPropagation()}
onClick={e => e.stopPropagation()}
style={{ transform: `scale(${scaling})`, width: `${100 * scaling}%`, height: `${100 * scaling}%` }}>
@@ -72,7 +73,7 @@ export class ColorBox extends ViewBoxBaseComponent<FieldViewProps>() {
SetActiveInkWidth(e.target.value);
SelectionManager.Views()
.filter(i => StrCast(i.rootDoc.type) === DocumentType.INK)
- .map(i => (i.rootDoc.strokeWidth = Number(e.target.value)));
+ .map(i => (i.rootDoc.stroke_width = Number(e.target.value)));
}}
/>
</div>
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 d74da9748..2290e0711 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -1,17 +1,18 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, observable } from 'mobx';
+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 { emptyFunction, OmitKeys, returnFalse, returnNone, setupMoveUpEvents } from '../../../Utils';
+import { DocCast, NumCast, StrCast } from '../../../fields/Types';
+import { emptyFunction, returnFalse, returnNone, returnZero, setupMoveUpEvents } from '../../../Utils';
+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';
import './ComparisonBox.scss';
import { DocumentView, DocumentViewProps } from './DocumentView';
import { FieldView, FieldViewProps } from './FieldView';
+import { PinProps, PresBox } from './trails';
import React = require('react');
@observer
@@ -24,126 +25,178 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
@observable _animating = '';
+ @computed get clipWidth() {
+ return NumCast(this.layoutDoc[this.clipWidthKey], 50);
+ }
+ 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));
+ droppedDocs.lastElement().embedContainer = 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';
- this.layoutDoc._clipWidth = (targetWidth * 100) / this.props.PanelWidth();
+ // on click, animate slider movement to the targetWidth
+ this.layoutDoc[this.clipWidthKey] = (targetWidth * 100) / this.props.PanelWidth();
setTimeout(
action(() => (this._animating = '')),
200
);
- }),
- false
+ })
);
+ }
};
@action
private onPointerMove = ({ movementX }: PointerEvent) => {
- const width = movementX * this.props.ScreenToLocalTransform().Scale + (NumCast(this.layoutDoc._clipWidth) / 100) * this.props.PanelWidth();
+ const width = movementX * this.props.ScreenToLocalTransform().Scale + (this.clipWidth / 100) * this.props.PanelWidth();
if (width && width > 5 && width < this.props.PanelWidth()) {
- this.layoutDoc._clipWidth = (width * 100) / this.props.PanelWidth();
+ this.layoutDoc[this.clipWidthKey] = (width * 100) / this.props.PanelWidth();
}
return false;
};
+ getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
+ 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);
+ PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), clippable: true } }, this.rootDoc);
+ return anchor;
+ }
+ return this.rootDoc;
+ };
+
@undoBatch
clearDoc = (e: React.MouseEvent, fieldKey: string) => {
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 clipWidth = NumCast(this.layoutDoc._clipWidth) + '%';
const clearButton = (which: string) => {
return (
<div
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, {});
- }}
- {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight']).omit}
- isContentActive={returnFalse}
- isDocumentActive={returnFalse}
- styleProvider={this.docStyleProvider}
+ {...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={emptyFunction}
+ isDocumentActive={returnFalse}
+ 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>
);
};
const displayBox = (which: string, index: number, cover: number) => {
return (
- <div className={`${which}Box-cont`} key={which} style={{ width: this.props.PanelWidth() }} onPointerDown={e => this.registerSliding(e, cover)} ref={ele => this.createDropTarget(ele, which, index)}>
+ <div className={`${index === 0 ? 'before' : 'after'}Box-cont`} key={which} style={{ width: this.props.PanelWidth() }} onPointerDown={e => this.registerSliding(e, cover)} ref={ele => this.createDropTarget(ele, which, index)}>
{displayDoc(which)}
</div>
);
};
return (
- <div className={`comparisonBox${this.props.isContentActive() || SnappingManager.GetIsDragging() ? '-interactive' : ''}` /* change className to easily disable/enable pointer events in CSS */}>
- {displayBox(this.fieldKey === 'data' ? 'compareBox-after' : `${this.fieldKey}2`, 1, this.props.PanelWidth() - 3)}
- <div className="clip-div" style={{ width: clipWidth, transition: this._animating, background: StrCast(this.layoutDoc._backgroundColor, 'gray') }}>
- {displayBox(this.fieldKey === 'data' ? 'compareBox-before' : `${this.fieldKey}1`, 0, 0)}
+ <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)}
</div>
<div
className="slide-bar"
style={{
- left: `calc(${clipWidth} - 0.5px)`,
- cursor: NumCast(this.layoutDoc._clipWidth) < 5 ? 'e-resize' : NumCast(this.layoutDoc._clipWidth) / 100 > (this.props.PanelWidth() - 5) / this.props.PanelWidth() ? 'w-resize' : undefined,
+ 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/DataViz.tsx b/src/client/views/nodes/DataViz.tsx
deleted file mode 100644
index df4c8f937..000000000
--- a/src/client/views/nodes/DataViz.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import { observer } from 'mobx-react';
-import * as React from 'react';
-import { ViewBoxBaseComponent } from '../DocComponent';
-import './DataViz.scss';
-import { FieldView, FieldViewProps } from './FieldView';
-
-@observer
-export class DataVizBox extends ViewBoxBaseComponent<FieldViewProps>() {
- public static LayoutString(fieldKey: string) {
- return FieldView.LayoutString(DataVizBox, fieldKey);
- }
-
- render() {
- return (
- <div>
- <div>Hi</div>
- </div>
- );
- }
-}
diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.scss b/src/client/views/nodes/DataVizBox/DataVizBox.scss
index e69de29bb..cd500e9ae 100644
--- a/src/client/views/nodes/DataVizBox/DataVizBox.scss
+++ b/src/client/views/nodes/DataVizBox/DataVizBox.scss
@@ -0,0 +1,4 @@
+.dataviz {
+ overflow: auto;
+ height: 100%;
+}
diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
index 592723ee9..0fe24fe8d 100644
--- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx
+++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
@@ -1,90 +1,150 @@
-import { action, computed, observable } from "mobx";
-import { observer } from "mobx-react";
-import * as React from "react";
-import { StrCast } from "../../../../fields/Types";
-import { ViewBoxBaseComponent } from "../../DocComponent";
-import { FieldViewProps, FieldView } from "../FieldView";
-import "./DataVizBox.scss";
-import { HistogramBox } from "./HistogramBox";
-import { TableBox } from "./TableBox";
-
-enum DataVizView {
- TABLE = "table",
- HISTOGRAM= "histogram"
-}
+import { action, computed, observable, ObservableMap, ObservableSet } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { Doc, StrListCast } from '../../../../fields/Doc';
+import { List } from '../../../../fields/List';
+import { Cast, CsvCast, NumCast, StrCast } from '../../../../fields/Types';
+import { CsvField } from '../../../../fields/URLField';
+import { Docs } from '../../../documents/Documents';
+import { ViewBoxAnnotatableComponent } from '../../DocComponent';
+import { FieldView, FieldViewProps } from '../FieldView';
+import { PinProps } from '../trails';
+import { LineChart } from './components/LineChart';
+import { TableBox } from './components/TableBox';
+import './DataVizBox.scss';
+export enum DataVizView {
+ TABLE = 'table',
+ LINECHART = 'lineChart',
+}
@observer
-export class DataVizBox extends ViewBoxBaseComponent<FieldViewProps>() {
- @observable private pairs: {x: number, y:number}[] = [{x: 1, y:2}];
-
- // TODO: nda - make this use enum values instead
- // @observable private currView: DataVizView = DataVizView.TABLE;
- @computed get currView() {
- if (this.rootDoc._dataVizView) {
- return StrCast(this.rootDoc._dataVizView);
- } else {
- return "table";
- }
+export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
+ public static LayoutString(fieldKey: string) {
+ return FieldView.LayoutString(DataVizBox, fieldKey);
}
-
- constructor(props: any) {
- super(props);
- if (!this.rootDoc._dataVizView) {
- // TODO: nda - this might not always want to default to "table"
- this.rootDoc._dataVizView = "table";
- }
+ // says we have an object and any string
+ // 2 ways of doing it
+ // @observable private pairs: { [key: string]: number | string | undefined }[] = [];
+ // @observable private pairs: { [key: string]: FieldResult }[] = [];
+ static pairSet = new ObservableMap<string, { [key: string]: string }[]>();
+ @computed.struct get pairs() {
+ return DataVizBox.pairSet.get(CsvCast(this.rootDoc[this.fieldKey]).url.href);
}
+ private _chartRenderer: LineChart | undefined;
+ // // another way would be store a schema that defines the type of data we are expecting from an imported doc
+
+ // method1() {
+ // this.pairs[0].x = 3;
+ // }
+
+ // method() {
+ // // this.pairs[0].x = 3;
+ // // go through the pairs
+ // const x = this.pairs[0].x;
+ // if (typeof x == 'number') {
+ // let x1 = Number(x);
+ // // let x1 = NumCast(x);
+ // }
+ // }
+
+ // could use field result
+ // [key: string]: FieldResult;
+ // instead of numeric x,y in there,
+
+ // TODO: nda - use onmousedown and onmouseup when dragging and changing height and width to update the height and width props only when dragging stops
- public static LayoutString(fieldKey: string) { return FieldView.LayoutString(DataVizBox, fieldKey); }
+ @computed get dataVizView(): DataVizView {
+ return StrCast(this.layoutDoc._dataVizView, 'table') as DataVizView;
+ }
@action
- private createPairs() {
- const xVals: number[] = [0, 1, 2, 3, 4, 5];
- // const yVals: number[] = [10, 20, 30, 40, 50, 60];
- const yVals: number[] = [1, 2, 3, 4, 5, 6];
- let pairs: {
- x: number,
- y:number
- }[] = [];
- if (xVals.length != yVals.length) return pairs;
- for (let i = 0; i < xVals.length; i++) {
- pairs.push({x: xVals[i], y: yVals[i]});
+ restoreView = (data: Doc) => {
+ const changedView = this.dataVizView !== data.presDataVizView && (this.layoutDoc._dataVizView = data.presDataVizView);
+ const changedAxes = this.axes.join('') !== StrListCast(data.presDataVizAxes).join('') && (this.layoutDoc._data_vizAxes = new List<string>(StrListCast(data.presDataVizAxes)));
+ const func = () => this._chartRenderer?.restoreView(data);
+ if (changedView || changedAxes) {
+ setTimeout(func, 100);
+ return true;
}
- this.pairs = pairs;
- return pairs;
+ return func() ?? false;
+ };
+
+ getAnchor = (addAsAnnotation?: boolean, pinProps?: PinProps) => {
+ const anchor =
+ 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;
+
+ this.addDocument(anchor);
+ return anchor;
+ };
+
+ @computed.struct get axes() {
+ return StrListCast(this.layoutDoc.data_vizAxes);
}
+ selectAxes = (axes: string[]) => (this.layoutDoc.data_vizAxes = new List<string>(axes));
@computed get selectView() {
- switch(this.currView) {
- case "table":
- return (<TableBox pairs={this.pairs} />)
- case "histogram":
- return (<HistogramBox rootDoc={this.rootDoc} pairs={this.pairs}/>)
+ const width = this.props.PanelWidth() * 0.9;
+ const height = (this.props.PanelHeight() - 32) /* height of 'change view' button */ * 0.9;
+ const margin = { top: 10, right: 25, bottom: 50, left: 25 };
+ if (!this.pairs) return 'no data';
+ // prettier-ignore
+ switch (this.dataVizView) {
+ case DataVizView.TABLE: return <TableBox pairs={this.pairs} axes={this.axes} docView={this.props.DocumentView} selectAxes={this.selectAxes}/>;
+ case DataVizView.LINECHART: return <LineChart ref={r => (this._chartRenderer = r ?? undefined)} height={height} width={width} fieldKey={this.fieldKey} margin={margin} rootDoc={this.rootDoc} axes={this.axes} pairs={this.pairs} dataDoc={this.dataDoc} />;
}
}
-
- @computed get pairVals() {
- return this.createPairs();
+ @computed get dataUrl() {
+ return Cast(this.dataDoc[this.fieldKey], CsvField);
}
componentDidMount() {
- this.createPairs();
+ this.props.setContentView?.(this);
+ this.fetchData();
+ }
+
+ fetchData() {
+ if (DataVizBox.pairSet.has(CsvCast(this.rootDoc[this.fieldKey]).url.href)) return;
+ DataVizBox.pairSet.set(CsvCast(this.rootDoc[this.fieldKey]).url.href, []);
+ fetch('/csvData?uri=' + this.dataUrl?.url.href) //
+ .then(res => res.json().then(action(res => !res.errno && DataVizBox.pairSet.set(CsvCast(this.rootDoc[this.fieldKey]).url.href, res))));
}
// handle changing the view using a button
@action changeViewHandler(e: React.MouseEvent<HTMLButtonElement>) {
e.preventDefault();
e.stopPropagation();
- this.rootDoc._dataVizView = this.currView == "table" ? "histogram" : "table";
+ this.layoutDoc._dataVizView = this.dataVizView === DataVizView.TABLE ? DataVizView.LINECHART : DataVizView.TABLE;
}
render() {
- return (
- <div className="dataViz">
- <button onClick={(e) => this.changeViewHandler(e)}>Change View</button>
+ return !this.pairs?.length ? (
+ <div>Loading...</div>
+ ) : (
+ <div
+ className="dataViz"
+ onWheel={e => e.stopPropagation()}
+ 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 }
+ )
+ }>
+ <button onClick={e => this.changeViewHandler(e)}>{this.dataVizView === DataVizView.TABLE ? DataVizView.LINECHART : DataVizView.TABLE}</button>
{this.selectView}
</div>
);
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/nodes/DataVizBox/DrawHelper.ts b/src/client/views/nodes/DataVizBox/DrawHelper.ts
deleted file mode 100644
index 595cecebf..000000000
--- a/src/client/views/nodes/DataVizBox/DrawHelper.ts
+++ /dev/null
@@ -1,247 +0,0 @@
-export class PIXIPoint {
- public get x() { return this.coords[0]; }
- public get y() { return this.coords[1]; }
- public set x(value: number) { this.coords[0] = value; }
- public set y(value: number) { this.coords[1] = value; }
- public coords: number[] = [0, 0];
- constructor(x: number, y: number) {
- this.coords[0] = x;
- this.coords[1] = y;
- }
-}
-
-export class PIXIRectangle {
- public x: number;
- public y: number;
- public width: number;
- public height: number;
- public get left() { return this.x; }
- public get right() { return this.x + this.width; }
- public get top() { return this.y; }
- public get bottom() { return this.top + this.height; }
- public static get EMPTY() { return new PIXIRectangle(0, 0, -1, -1); }
- constructor(x: number, y: number, width: number, height: number) {
- this.x = x;
- this.y = y;
- this.width = width;
- this.height = height;
- }
-}
-
-export class MathUtil {
-
- public static EPSILON: number = 0.001;
-
- public static Sign(value: number): number {
- return value >= 0 ? 1 : -1;
- }
-
- public static AddPoint(p1: PIXIPoint, p2: PIXIPoint, inline: boolean = false): PIXIPoint {
- if (inline) {
- p1.x += p2.x;
- p1.y += p2.y;
- return p1;
- }
- else {
- return new PIXIPoint(p1.x + p2.x, p1.y + p2.y);
- }
- }
-
- public static Perp(p1: PIXIPoint): PIXIPoint {
- return new PIXIPoint(-p1.y, p1.x);
- }
-
- public static DividePoint(p1: PIXIPoint, by: number, inline: boolean = false): PIXIPoint {
- if (inline) {
- p1.x /= by;
- p1.y /= by;
- return p1;
- }
- else {
- return new PIXIPoint(p1.x / by, p1.y / by);
- }
- }
-
- public static MultiplyConstant(p1: PIXIPoint, by: number, inline: boolean = false) {
- if (inline) {
- p1.x *= by;
- p1.y *= by;
- return p1;
- }
- else {
- return new PIXIPoint(p1.x * by, p1.y * by);
- }
- }
-
- public static SubtractPoint(p1: PIXIPoint, p2: PIXIPoint, inline: boolean = false): PIXIPoint {
- if (inline) {
- p1.x -= p2.x;
- p1.y -= p2.y;
- return p1;
- }
- else {
- return new PIXIPoint(p1.x - p2.x, p1.y - p2.y);
- }
- }
-
- public static Area(rect: PIXIRectangle): number {
- return rect.width * rect.height;
- }
-
- public static DistToLineSegment(v: PIXIPoint, w: PIXIPoint, p: PIXIPoint) {
- // Return minimum distance between line segment vw and point p
- var l2 = MathUtil.DistSquared(v, w); // i.e. |w-v|^2 - avoid a sqrt
- if (l2 === 0.0) return MathUtil.Dist(p, v); // v === w case
- // Consider the line extending the segment, parameterized as v + t (w - v).
- // We find projection of point p onto the line.
- // It falls where t = [(p-v) . (w-v)] / |w-v|^2
- // We clamp t from [0,1] to handle points outside the segment vw.
- var dot = MathUtil.Dot(
- MathUtil.SubtractPoint(p, v),
- MathUtil.SubtractPoint(w, v)) / l2;
- var t = Math.max(0, Math.min(1, dot));
- // Projection falls on the segment
- var projection = MathUtil.AddPoint(v,
- MathUtil.MultiplyConstant(
- MathUtil.SubtractPoint(w, v), t));
- return MathUtil.Dist(p, projection);
- }
-
- public static LineSegmentIntersection(ps1: PIXIPoint, pe1: PIXIPoint, ps2: PIXIPoint, pe2: PIXIPoint): PIXIPoint | undefined {
- var a1 = pe1.y - ps1.y;
- var b1 = ps1.x - pe1.x;
-
- var a2 = pe2.y - ps2.y;
- var b2 = ps2.x - pe2.x;
-
- var delta = a1 * b2 - a2 * b1;
- if (delta === 0) {
- return undefined;
- }
- var c2 = a2 * ps2.x + b2 * ps2.y;
- var c1 = a1 * ps1.x + b1 * ps1.y;
- var invdelta = 1 / delta;
- return new PIXIPoint((b2 * c1 - b1 * c2) * invdelta, (a1 * c2 - a2 * c1) * invdelta);
- }
-
- public static PointInPIXIRectangle(p: PIXIPoint, rect: PIXIRectangle): boolean {
- if (p.x < rect.left - this.EPSILON) {
- return false;
- }
- if (p.x > rect.right + this.EPSILON) {
- return false;
- }
- if (p.y < rect.top - this.EPSILON) {
- return false;
- }
- if (p.y > rect.bottom + this.EPSILON) {
- return false;
- }
-
- return true;
- }
-
- public static LinePIXIRectangleIntersection(lineFrom: PIXIPoint, lineTo: PIXIPoint, rect: PIXIRectangle): Array<PIXIPoint> {
- var r1 = new PIXIPoint(rect.left, rect.top);
- var r2 = new PIXIPoint(rect.right, rect.top);
- var r3 = new PIXIPoint(rect.right, rect.bottom);
- var r4 = new PIXIPoint(rect.left, rect.bottom);
- var ret = new Array<PIXIPoint>();
- var dist = this.Dist(lineFrom, lineTo);
- var inter = this.LineSegmentIntersection(lineFrom, lineTo, r1, r2);
- if (inter && this.PointInPIXIRectangle(inter, rect) &&
- this.Dist(inter, lineFrom) < dist && this.Dist(inter, lineTo) < dist) {
- ret.push(inter);
- }
- inter = this.LineSegmentIntersection(lineFrom, lineTo, r2, r3);
- if (inter && this.PointInPIXIRectangle(inter, rect) &&
- this.Dist(inter, lineFrom) < dist && this.Dist(inter, lineTo) < dist) {
- ret.push(inter);
- }
- inter = this.LineSegmentIntersection(lineFrom, lineTo, r3, r4);
- if (inter && this.PointInPIXIRectangle(inter, rect) &&
- this.Dist(inter, lineFrom) < dist && this.Dist(inter, lineTo) < dist) {
- ret.push(inter);
- }
- inter = this.LineSegmentIntersection(lineFrom, lineTo, r4, r1);
- if (inter && this.PointInPIXIRectangle(inter, rect) &&
- this.Dist(inter, lineFrom) < dist && this.Dist(inter, lineTo) < dist) {
- ret.push(inter);
- }
- return ret;
- }
-
- public static Intersection(rect1: PIXIRectangle, rect2: PIXIRectangle): PIXIRectangle {
- const left = Math.max(rect1.x, rect2.x);
- const right = Math.min(rect1.x + rect1.width, rect2.x + rect2.width);
- const top = Math.max(rect1.y, rect2.y);
- const bottom = Math.min(rect1.y + rect1.height, rect2.y + rect2.height);
- return new PIXIRectangle(left, top, right - left, bottom - top);
- }
-
- public static Dist(p1: PIXIPoint, p2: PIXIPoint): number {
- return Math.sqrt(MathUtil.DistSquared(p1, p2));
- }
-
- public static Dot(p1: PIXIPoint, p2: PIXIPoint): number {
- return p1.x * p2.x + p1.y * p2.y;
- }
-
- public static Normalize(p1: PIXIPoint) {
- var d = this.Length(p1);
- return new PIXIPoint(p1.x / d, p1.y / d);
- }
-
- public static Length(p1: PIXIPoint): number {
- return Math.sqrt(p1.x * p1.x + p1.y * p1.y);
- }
-
- public static DistSquared(p1: PIXIPoint, p2: PIXIPoint): number {
- const a = p1.x - p2.x;
- const b = p1.y - p2.y;
- return (a * a + b * b);
- }
-
- public static RectIntersectsRect(r1: PIXIRectangle, r2: PIXIRectangle): boolean {
- return !(r2.x > r1.x + r1.width ||
- r2.x + r2.width < r1.x ||
- r2.y > r1.y + r1.height ||
- r2.y + r2.height < r1.y);
- }
-
- public static ArgMin(temp: number[]): number {
- let index = 0;
- let value = temp[0];
- for (let i = 1; i < temp.length; i++) {
- if (temp[i] < value) {
- value = temp[i];
- index = i;
- }
- }
- return index;
- }
-
- public static ArgMax(temp: number[]): number {
- let index = 0;
- let value = temp[0];
- for (let i = 1; i < temp.length; i++) {
- if (temp[i] > value) {
- value = temp[i];
- index = i;
- }
- }
- return index;
- }
-
- public static Combinations<T>(chars: T[]) {
- let result = new Array<T>();
- let f = (prefix: any, chars: any) => {
- for (let i = 0; i < chars.length; i++) {
- result.push(prefix.concat(chars[i]));
- f(prefix.concat(chars[i]), chars.slice(i + 1));
- }
- };
- f([], chars);
- return result;
- }
-} \ No newline at end of file
diff --git a/src/client/views/nodes/DataVizBox/HistogramBox.scss b/src/client/views/nodes/DataVizBox/HistogramBox.scss
deleted file mode 100644
index 5aac9dc77..000000000
--- a/src/client/views/nodes/DataVizBox/HistogramBox.scss
+++ /dev/null
@@ -1,18 +0,0 @@
-// change the stroke color of line-svg class
-.svgLine {
- position: absolute;
- background: darkGray;
- stroke: #000;
- stroke-width: 1px;
- width:100%;
- height:100%;
- opacity: 0.4;
-}
-
-.svgContainer {
- position: absolute;
- top:0;
- left:0;
- width:100%;
- height: 100%;
-} \ No newline at end of file
diff --git a/src/client/views/nodes/DataVizBox/HistogramBox.tsx b/src/client/views/nodes/DataVizBox/HistogramBox.tsx
deleted file mode 100644
index 00dc2ef46..000000000
--- a/src/client/views/nodes/DataVizBox/HistogramBox.tsx
+++ /dev/null
@@ -1,159 +0,0 @@
-import { action, computed, observable } from "mobx";
-import { observer } from "mobx-react";
-import * as React from "react";
-import { Doc } from "../../../../fields/Doc";
-import { NumCast } from "../../../../fields/Types";
-import "./HistogramBox.scss";
-
-interface HistogramBoxProps {
- rootDoc: Doc;
- pairs: {
- x: number,
- y: number
- }[]
-}
-
-
-export class HistogramBox extends React.Component<HistogramBoxProps> {
-
- private origin = {x: 0.1 * this.width, y: 0.9 * this.height};
-
- @computed get width() {
- return NumCast(this.props.rootDoc.width);
- }
-
- @computed get height() {
- return NumCast(this.props.rootDoc.height);
- }
-
- @computed get x() {
- return NumCast(this.props.rootDoc.x);
- }
-
- @computed get y() {
- return NumCast(this.props.rootDoc.y);
- }
-
- @computed get generatePoints() {
- // evenly distribute points along the x axis
- const xVals: number[] = this.props.pairs.map(p => p.x);
- const yVals: number[] = this.props.pairs.map(p => p.y);
-
- const xMin = Math.min(...xVals);
- const xMax = Math.max(...xVals);
- const yMin = Math.min(...yVals);
- const yMax = Math.max(...yVals);
-
- const xRange = xMax - xMin;
- const yRange = yMax - yMin;
-
- const xScale = this.width / xRange;
- const yScale = this.height / yRange;
-
- const xOffset = (this.x + (0.1 * this.width)) - xMin * xScale;
- const yOffset = (this.y + (0.25 * this.height)) - yMin * yScale;
-
- const points: {
- x: number,
- y: number
- }[] = this.props.pairs.map(p => {
- return {
- x: (p.x * xScale + xOffset) + this.origin.x,
- y: (p.y * yScale + yOffset)
- }
- });
-
- return points;
- }
-
- @computed get generateGraphLine() {
- const points = this.generatePoints;
- // loop through points and create a line from each point to the next
- let lines: {
- x1: number,
- y1: number,
- x2: number,
- y2: number
- }[] = [];
- for (let i = 0; i < points.length - 1; i++) {
- lines.push({
- x1: points[i].x,
- y1: points[i].y,
- x2: points[i + 1].x,
- y2: points[i + 1].y
- });
- }
- // generate array of svg with lines
- let svgLines: JSX.Element[] = [];
- for (let i = 0; i < lines.length; i++) {
- svgLines.push(
- <line
- className="svgLine"
- key={i}
- x1={lines[i].x1}
- y1={lines[i].y1}
- x2={lines[i].x2}
- y2={lines[i].y2}
- stroke="black"
- strokeWidth={2}
- />
- );
- }
-
- let res = [];
- for (let i = 0; i < svgLines.length; i++) {
- res.push(<svg className="svgContainer">{svgLines[i]}</svg>)
- }
- return res;
- }
-
- @computed get generateAxes() {
-
- const xAxis = {
- x1: 0.1 * this.width,
- x2: 0.9 * this.width,
- y1: 0.9 * this.height,
- y2: 0.9 * this.height,
- };
-
- const yAxis = {
- x1: 0.1 * this.width,
- x2: 0.1 * this.width,
- y1: 0.25 * this.height,
- y2: 0.9 * this.height,
- };
-
-
- return (
- [
- (<svg className="svgContainer">
- {/* <line className="svgLine" x1={yAxis} y1={xAxis} x2={this.width - (0.1 * this.width)} y2={xAxis} /> */}
- <line className="svgLine" x1={xAxis.x1} y1={xAxis.y1} x2={xAxis.x2} y2={xAxis.y2}/>
-
- {/* <line className="svgLine" x1={yAxis} y1={xAxis} x2={yAxis} y2={this.y + 50} /> */}
- </svg>),
- (
- <svg className="svgContainer">
- <line className="svgLine" x1={yAxis.x1} y1={yAxis.y1} x2={yAxis.x2} y2={yAxis.y2} />
- {/* <line className="svgLine" x1={yAxis} y1={xAxis} x2={yAxis} y2={this.y + 50} /> */}
- </svg>)
- ]
- )
- }
-
-
- render() {
- return (
- <div>histogram box
- {/* <svg className="svgContainer">
- {this.generateSVGLine}
- </svg> */}
- {this.generateAxes[0]}
- {this.generateAxes[1]}
- {this.generateGraphLine.map(line => line)}
- </div>
- )
-
- }
-
-} \ No newline at end of file
diff --git a/src/client/views/nodes/DataVizBox/TableBox.tsx b/src/client/views/nodes/DataVizBox/TableBox.tsx
deleted file mode 100644
index dfa8262d8..000000000
--- a/src/client/views/nodes/DataVizBox/TableBox.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import { action, computed, observable } from "mobx";
-import { observer } from "mobx-react";
-import * as React from "react";
-
-interface TableBoxProps {
- pairs: {x: number, y:number}[]
-}
-
-
-export class TableBox extends React.Component<TableBoxProps> {
-
-
-
- render() {
- return (
- <div className="table-container">
- <table className="table">
- <thead>
- <tr className="table-row">
- <th>x</th>
- <th>y</th>
- </tr>
- </thead>
- <tbody>
- {this.props.pairs.map(p => {
- return (<tr className="table-row">
- <td>{p.x}</td>
- <td>{p.y}</td>
- </tr>)
- })}
- </tbody>
- </table>
- </div>
- )
- }
-
-} \ No newline at end of file
diff --git a/src/client/views/nodes/DataVizBox/components/Chart.scss b/src/client/views/nodes/DataVizBox/components/Chart.scss
new file mode 100644
index 000000000..d4f7bfb32
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/components/Chart.scss
@@ -0,0 +1,41 @@
+.chart-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ cursor: default;
+
+ .tooltip {
+ // make the height width bigger
+ width: fit-content;
+ height: fit-content;
+ }
+
+ .hoverHighlight-selected,
+ .selected {
+ // change the color of the circle element to be red
+ fill: transparent;
+ outline: red solid 2px;
+ border-radius: 100%;
+ position: absolute;
+ transform-box: fill-box;
+ transform-origin: center;
+ }
+ .hoverHighlight {
+ fill: transparent;
+ outline: black solid 1px;
+ border-radius: 100%;
+ }
+ .hoverHighlight-selected {
+ fill: transparent;
+ scale: 1;
+ outline: black solid 1px;
+ border-radius: 100%;
+ }
+ .datapoint {
+ fill: black;
+ }
+ .brushed {
+ // change the color of the circle element to be red
+ fill: red;
+ }
+}
diff --git a/src/client/views/nodes/DataVizBox/components/LineChart.tsx b/src/client/views/nodes/DataVizBox/components/LineChart.tsx
new file mode 100644
index 000000000..6b564b0c9
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx
@@ -0,0 +1,322 @@
+import { action, computed, IReactionDisposer, observable, reaction } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+// import d3
+import * as d3 from 'd3';
+import { Doc, DocListCast } from '../../../../../fields/Doc';
+import { Id } from '../../../../../fields/FieldSymbols';
+import { List } from '../../../../../fields/List';
+import { listSpec } from '../../../../../fields/Schema';
+import { Cast, DocCast } from '../../../../../fields/Types';
+import { Docs } from '../../../../documents/Documents';
+import { DocumentManager } from '../../../../util/DocumentManager';
+import { LinkManager } from '../../../../util/LinkManager';
+import { PinProps, PresBox } from '../../trails';
+import { DataVizBox } from '../DataVizBox';
+import { createLineGenerator, drawLine, minMaxRange, scaleCreatorNumerical, xAxisCreator, xGrid, yAxisCreator, yGrid } from '../utils/D3Utils';
+import './Chart.scss';
+
+export interface DataPoint {
+ x: number;
+ y: number;
+}
+interface SelectedDataPoint extends DataPoint {
+ elem?: d3.Selection<d3.BaseType, unknown, SVGGElement, unknown>;
+}
+export interface LineChartProps {
+ rootDoc: Doc;
+ axes: string[];
+ pairs: { [key: string]: any }[];
+ width: number;
+ height: number;
+ dataDoc: Doc;
+ fieldKey: string;
+ margin: {
+ top: number;
+ right: number;
+ bottom: number;
+ left: number;
+ };
+}
+
+@observer
+export class LineChart extends React.Component<LineChartProps> {
+ private _disposers: { [key: string]: IReactionDisposer } = {};
+ private _lineChartRef: React.RefObject<HTMLDivElement> = React.createRef();
+ private _lineChartSvg: d3.Selection<SVGGElement, unknown, null, undefined> | undefined;
+ @observable _currSelected: SelectedDataPoint | undefined = undefined;
+ // TODO: nda - some sort of mapping that keeps track of the annotated points so we can easily remove when annotations list updates
+
+ @computed get _lineChartData() {
+ if (this.props.axes.length <= 1) return [];
+ return this.props.pairs
+ ?.filter(pair => (!this.incomingLinks.length ? true : Array.from(Object.keys(pair)).some(key => pair[key] && key.startsWith('select'))))
+ .map(pair => ({ x: Number(pair[this.props.axes[0]]), y: Number(pair[this.props.axes[1]]) }))
+ .sort((a, b) => (a.x < b.x ? -1 : 1));
+ }
+ @computed get incomingLinks() {
+ return LinkManager.Instance.getAllRelatedLinks(this.props.rootDoc) // out of all links
+ .filter(link => link.link_anchor_1 !== this.props.rootDoc) // get links where this chart doc is the target of the link
+ .map(link => DocCast(link.link_anchor_1)); // then return the source of the link
+ }
+ @computed get incomingSelected() {
+ return this.incomingLinks // all links that are pointing to this node
+ .map(anchor => DocumentManager.Instance.getFirstDocumentView(anchor)?.ComponentView as DataVizBox) // get their data viz boxes
+ .filter(dvb => dvb)
+ .map(dvb => dvb.pairs?.filter(pair => pair['select' + dvb.rootDoc[Id]])) // get all the datapoints they have selected field set by incoming anchor
+ .lastElement();
+ }
+ @computed get rangeVals(): { xMin?: number; xMax?: number; yMin?: number; yMax?: number } {
+ return minMaxRange([this._lineChartData]);
+ }
+ componentWillUnmount() {
+ Array.from(Object.keys(this._disposers)).forEach(key => this._disposers[key]());
+ }
+ componentDidMount = () => {
+ this._disposers.chartData = reaction(
+ () => ({ dataSet: this._lineChartData, w: this.width, h: this.height }),
+ ({ dataSet, w, h }) => {
+ if (dataSet) {
+ this.drawChart([dataSet], this.rangeVals, w, h);
+ // redraw annotations when the chart data has changed, or the local or inherited selection has changed
+ this.clearAnnotations();
+ this._currSelected && this.drawAnnotations(Number(this._currSelected.x), Number(this._currSelected.y), true);
+ this.incomingSelected?.forEach((pair: any) => this.drawAnnotations(Number(pair[this.props.axes[0]]), Number(pair[this.props.axes[1]])));
+ }
+ },
+ { fireImmediately: true }
+ );
+ this._disposers.annos = reaction(
+ () => DocListCast(this.props.dataDoc[this.props.fieldKey + '_annotations']),
+ annotations => {
+ // modify how d3 renders so that anything in this annotations list would be potentially highlighted in some way
+ // could be blue colored to make it look like anchor
+ // this.drawAnnotations()
+ // loop through annotations and draw them
+ annotations.forEach(a => this.drawAnnotations(Number(a.x), Number(a.y)));
+ // this.drawAnnotations(annotations.x, annotations.y);
+ },
+ { fireImmediately: true }
+ );
+ this._disposers.highlights = reaction(
+ () => ({
+ selected: this._currSelected,
+ incomingSelected: this.incomingSelected,
+ }),
+ ({ selected, incomingSelected }) => {
+ // redraw annotations when the chart data has changed, or the local or inherited selection has changed
+ this.clearAnnotations();
+ selected && this.drawAnnotations(Number(selected.x), Number(selected.y), true);
+ incomingSelected?.forEach((pair: any) => this.drawAnnotations(Number(pair[this.props.axes[0]]), Number(pair[this.props.axes[1]])));
+ },
+ { fireImmediately: true }
+ );
+ };
+
+ // anything that doesn't need to be recalculated should just be stored as drawCharts (i.e. computed values) and drawChart is gonna iterate over these observables and generate svgs based on that
+
+ clearAnnotations = () => {
+ const elements = document.querySelectorAll('.datapoint');
+ for (let i = 0; i < elements.length; i++) {
+ const element = elements[i];
+ element.classList.remove('brushed');
+ element.classList.remove('selected');
+ }
+ };
+ // gets called whenever the "data_annotations" fields gets updated
+ drawAnnotations = (dataX: number, dataY: number, selected?: boolean) => {
+ // TODO: nda - can optimize this by having some sort of mapping of the x and y values to the individual circle elements
+ // loop through all html elements with class .circle-d1 and find the one that has "data-x" and "data-y" attributes that match the dataX and dataY
+ // if it exists, then highlight it
+ // if it doesn't exist, then remove the highlight
+ const elements = document.querySelectorAll('.datapoint');
+ for (let i = 0; i < elements.length; i++) {
+ const element = elements[i];
+ const x = element.getAttribute('data-x');
+ const y = element.getAttribute('data-y');
+ if (x === dataX.toString() && y === dataY.toString()) {
+ element.classList.add(selected ? 'selected' : 'brushed');
+ }
+ // TODO: nda - this remove highlight code should go where we remove the links
+ // } else {
+ // }
+ }
+ };
+
+ removeAnnotations(dataX: number, dataY: number) {
+ // loop through and remove any annotations that no longer exist
+ }
+
+ @action
+ restoreView = (data: Doc) => {
+ const coords = Cast(data.presDataVizSelection, listSpec('number'), null);
+ if (coords?.length > 1 && (this._currSelected?.x !== coords[0] || this._currSelected?.y !== coords[1])) {
+ this.setCurrSelected(coords[0], coords[1]);
+ return true;
+ }
+ if (this._currSelected) {
+ this.setCurrSelected();
+ return true;
+ }
+ return false;
+ };
+
+ // create a document anchor that stores whatever is needed to reconstruct the viewing state (selection,zoom,etc)
+ getAnchor = (pinProps?: PinProps) => {
+ const anchor = Docs.Create.ConfigDocument({
+ //
+ title: '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;
+ };
+
+ @computed get height() {
+ return this.props.height - this.props.margin.top - this.props.margin.bottom;
+ }
+
+ @computed get width() {
+ return this.props.width - this.props.margin.left - this.props.margin.right;
+ }
+
+ setupTooltip() {
+ return d3
+ .select(this._lineChartRef.current)
+ .append('div')
+ .attr('class', 'tooltip')
+ .style('opacity', 0)
+ .style('background', '#fff')
+ .style('border', '1px solid #ccc')
+ .style('padding', '5px')
+ .style('position', 'absolute')
+ .style('font-size', '12px');
+ }
+
+ // TODO: nda - use this everyewhere we update currSelected?
+ @action
+ setCurrSelected(x?: number, y?: number) {
+ // TODO: nda - get rid of svg element in the list?
+ this._currSelected = x !== undefined && y !== undefined ? { x, y } : undefined;
+ this.props.pairs.forEach(pair => pair[this.props.axes[0]] === x && pair[this.props.axes[1]] === y && (pair.selected = true));
+ this.props.pairs.forEach(pair => (pair.selected = pair[this.props.axes[0]] === x && pair[this.props.axes[1]] === y ? true : undefined));
+ }
+
+ drawDataPoints(data: DataPoint[], idx: number, xScale: d3.ScaleLinear<number, number, never>, yScale: d3.ScaleLinear<number, number, never>) {
+ if (this._lineChartSvg) {
+ const circleClass = '.circle-' + idx;
+ this._lineChartSvg
+ .selectAll(circleClass)
+ .data(data)
+ .join('circle') // enter append
+ .attr('class', `${circleClass} datapoint`)
+ .attr('r', '3') // radius
+ .attr('cx', d => xScale(d.x))
+ .attr('cy', d => yScale(d.y))
+ .attr('data-x', d => d.x)
+ .attr('data-y', d => d.y);
+ }
+ }
+
+ // TODO: nda - can use d3.create() to create html element instead of appending
+ drawChart = (dataSet: DataPoint[][], rangeVals: { xMin?: number; xMax?: number; yMin?: number; yMax?: number }, width: number, height: number) => {
+ // clearing tooltip and the current chart
+ d3.select(this._lineChartRef.current).select('svg').remove();
+ d3.select(this._lineChartRef.current).select('.tooltip').remove();
+
+ const { xMin, xMax, yMin, yMax } = rangeVals;
+ if (xMin === undefined || xMax === undefined || yMin === undefined || yMax === undefined) {
+ return;
+ }
+
+ // creating the x and y scales
+ const xScale = scaleCreatorNumerical(xMin, xMax, 0, width);
+ const yScale = scaleCreatorNumerical(0, yMax, height, 0);
+
+ // adding svg
+ const margin = this.props.margin;
+ const svg = (this._lineChartSvg = d3
+ .select(this._lineChartRef.current)
+ .append('svg')
+ .attr('width', `${width + margin.left + margin.right}`)
+ .attr('height', `${height + margin.top + margin.bottom}`)
+ .append('g')
+ .attr('transform', `translate(${margin.left}, ${margin.top})`));
+
+ // create x and y grids
+ xGrid(svg.append('g'), height, xScale);
+ yGrid(svg.append('g'), width, yScale);
+ xAxisCreator(svg.append('g'), height, xScale);
+ yAxisCreator(svg.append('g'), width, yScale);
+
+ // draw the plot line
+ const data = dataSet[0];
+ const lineGen = createLineGenerator(xScale, yScale);
+ drawLine(svg.append('path'), data, lineGen);
+
+ // draw the datapoint circle
+ this.drawDataPoints(data, 0, xScale, yScale);
+
+ const higlightFocusPt = svg.append('g').style('display', 'none');
+ higlightFocusPt.append('circle').attr('r', 5).attr('class', 'circle');
+ const tooltip = this.setupTooltip();
+ // add all the tooltipContent to the tooltip
+ const mousemove = action((e: any) => {
+ const bisect = d3.bisector((d: DataPoint) => d.x).left;
+ const xPos = d3.pointer(e)[0];
+ const x0 = Math.min(data.length - 1, bisect(data, xScale.invert(xPos - 5))); // shift x by -5 so that you can reach points on the left-side axis
+ const d0 = data[x0];
+ if (!d0) return;
+
+ this.updateTooltip(higlightFocusPt, xScale, d0, yScale, tooltip);
+ });
+
+ const onPointClick = action((e: any) => {
+ const bisect = d3.bisector((d: DataPoint) => d.x).left;
+ const xPos = d3.pointer(e)[0];
+ const x0 = bisect(data, xScale.invert(xPos - 5)); // shift x by -5 so that you can reach points on the left-side axis
+ const d0 = data[x0];
+ // find .circle-d1 with data-x = d0.x and data-y = d0.y
+ const selected = svg.selectAll('.datapoint').filter((d: any) => d['data-x'] === d0.x && d['data-y'] === d0.y);
+ this.setCurrSelected(d0.x, d0.y);
+ this.updateTooltip(higlightFocusPt, xScale, d0, yScale, tooltip);
+ });
+
+ svg.append('rect')
+ .attr('class', 'overlay')
+ .attr('width', width)
+ .attr('height', this.height + margin.top + margin.bottom)
+ .attr('fill', 'none')
+ .attr('translate', `translate(${margin.left}, ${-(margin.top + margin.bottom)})`)
+ .style('opacity', 0)
+ .on('mouseover', () => higlightFocusPt.style('display', null))
+ .on('mouseout', () => tooltip.transition().duration(300).style('opacity', 0))
+ .on('mousemove', mousemove)
+ .on('click', onPointClick);
+ };
+
+ private updateTooltip(
+ higlightFocusPt: d3.Selection<SVGGElement, unknown, null, undefined>,
+ xScale: d3.ScaleLinear<number, number, never>,
+ d0: DataPoint,
+ yScale: d3.ScaleLinear<number, number, never>,
+ tooltip: d3.Selection<HTMLDivElement, unknown, null, undefined>
+ ) {
+ higlightFocusPt.attr('transform', `translate(${xScale(d0.x)},${yScale(d0.y)})`).attr('class', this._currSelected?.x === d0.x && this._currSelected?.y === d0.y ? 'hoverHighlight-selected' : 'hoverHighlight');
+ tooltip.transition().duration(300).style('opacity', 0.9);
+ // TODO: nda - updating the inner html could be deadly cause injection attacks!
+ tooltip
+ .html(() => `<b>(${d0.x},${d0.y})</b>`) // text content for tooltip
+ .style('pointer-events', 'none')
+ .style('transform', `translate(${xScale(d0.x) - this.width / 2}px,${yScale(d0.y) - 30}px)`);
+ }
+
+ render() {
+ const selectedPt = this._currSelected ? `x: ${this._currSelected.x} y: ${this._currSelected.y}` : 'none';
+ return (
+ <div ref={this._lineChartRef} className="chart-container">
+ <span> {this.props.axes.length < 2 ? 'first use table view to select two axes to plot' : `Selected: ${selectedPt}`}</span>
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/nodes/DataVizBox/TableBox.scss b/src/client/views/nodes/DataVizBox/components/TableBox.scss
index 1264d6a46..1264d6a46 100644
--- a/src/client/views/nodes/DataVizBox/TableBox.scss
+++ b/src/client/views/nodes/DataVizBox/components/TableBox.scss
diff --git a/src/client/views/nodes/DataVizBox/components/TableBox.tsx b/src/client/views/nodes/DataVizBox/components/TableBox.tsx
new file mode 100644
index 000000000..d84e34d52
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/components/TableBox.tsx
@@ -0,0 +1,105 @@
+import { action, computed } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { Doc } from '../../../../../fields/Doc';
+import { Id } from '../../../../../fields/FieldSymbols';
+import { List } from '../../../../../fields/List';
+import { emptyFunction, returnFalse, setupMoveUpEvents, Utils } from '../../../../../Utils';
+import { DragManager } from '../../../../util/DragManager';
+import { DocumentView } from '../../DocumentView';
+import { DataVizView } from '../DataVizBox';
+
+interface TableBoxProps {
+ pairs: { [key: string]: any }[];
+ selectAxes: (axes: string[]) => void;
+ axes: string[];
+ docView?: () => DocumentView | undefined;
+}
+
+@observer
+export class TableBox extends React.Component<TableBoxProps> {
+ @computed get columns() {
+ return this.props.pairs.length ? Array.from(Object.keys(this.props.pairs[0])) : [];
+ }
+ render() {
+ return (
+ <div className="table-container">
+ <table className="table">
+ <thead>
+ <tr className="table-row">
+ {this.columns
+ .filter(col => !col.startsWith('select'))
+ .map(col => {
+ const header = React.createRef<HTMLElement>();
+ return (
+ <th
+ ref={header as any}
+ style={{
+ color: this.props.axes.slice().reverse().lastElement() === col ? 'green' : this.props.axes.lastElement() === col ? 'red' : undefined,
+ fontWeight: this.props.axes.includes(col) ? 'bolder' : 'normal',
+ }}
+ onPointerDown={e => {
+ const downX = e.clientX;
+ const downY = e.clientY;
+ setupMoveUpEvents(
+ {},
+ e,
+ e => {
+ const sourceAnchorCreator = () => this.props.docView?.()!.rootDoc!;
+ const targetCreator = (annotationOn: Doc | undefined) => {
+ const embedding = Doc.MakeEmbedding(this.props.docView?.()!.rootDoc!);
+ embedding._dataVizView = DataVizView.LINECHART;
+ embedding._data_vizAxes = new List<string>([col, col]);
+ embedding.annotationOn = annotationOn; //this.props.docView?.()!.rootDoc!;
+ return embedding;
+ };
+ if (this.props.docView?.() && !Utils.isClick(e.clientX, e.clientY, downX, downY, Date.now())) {
+ DragManager.StartAnchorAnnoDrag([header.current!], new DragManager.AnchorAnnoDragData(this.props.docView()!, sourceAnchorCreator, targetCreator), downX, downY, {
+ dragComplete: e => {
+ if (!e.aborted && e.annoDragData && e.annoDragData.linkSourceDoc && e.annoDragData.dropDocument && e.linkDocument) {
+ e.linkDocument.link_displayLine = true;
+ // e.annoDragData.linkSourceDoc.followLinkToggle = e.annoDragData.dropDocument.annotationOn === this.props.rootDoc;
+ // e.annoDragData.linkSourceDoc.followLinkZoom = false;
+ }
+ },
+ });
+ return true;
+ }
+ return false;
+ },
+ emptyFunction,
+ action(e => {
+ const newAxes = this.props.axes;
+ if (newAxes.includes(col)) {
+ newAxes.splice(newAxes.indexOf(col), 1);
+ } else if (newAxes.length >= 1) {
+ newAxes[1] = col;
+ } else {
+ newAxes[0] = col;
+ }
+ this.props.selectAxes(newAxes);
+ })
+ );
+ }}>
+ {col}
+ </th>
+ );
+ })}
+ </tr>
+ </thead>
+ <tbody>
+ {this.props.pairs?.map((p, i) => {
+ return (
+ <tr className="table-row" onClick={action(e => (p['select' + this.props.docView?.()?.rootDoc![Id]] = !p['select' + this.props.docView?.()?.rootDoc![Id]]))}>
+ {this.columns.map(col => (
+ <td style={{ fontWeight: p['select' + this.props.docView?.()?.rootDoc![Id]] ? 'bold' : '' }}>{p[col]}</td>
+ ))}
+ </tr>
+ );
+ })}
+ </tbody>
+ </table>
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/nodes/DataVizBox/utils/D3Utils.ts b/src/client/views/nodes/DataVizBox/utils/D3Utils.ts
new file mode 100644
index 000000000..e1ff6f8eb
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/utils/D3Utils.ts
@@ -0,0 +1,67 @@
+import * as d3 from 'd3';
+import { DataPoint } from '../components/LineChart';
+
+// TODO: nda - implement function that can handle range for strings
+
+export const minMaxRange = (dataPts: DataPoint[][]) => {
+ // find the max and min of all the data points
+ const yMin = d3.min(dataPts, d => d3.min(d, d => Number(d.y)));
+ const yMax = d3.max(dataPts, d => d3.max(d, d => Number(d.y)));
+
+ const xMin = d3.min(dataPts, d => d3.min(d, d => Number(d.x)));
+ const xMax = d3.max(dataPts, d => d3.max(d, d => Number(d.x)));
+
+ return { xMin, xMax, yMin, yMax };
+};
+
+export const scaleCreatorCategorical = (labels: string[], range: number[]) => {
+ const scale = d3.scaleBand().domain(labels).range(range);
+
+ return scale;
+};
+
+export const scaleCreatorNumerical = (domA: number, domB: number, rangeA: number, rangeB: number) => {
+ return d3.scaleLinear().domain([domA, domB]).range([rangeA, rangeB]);
+};
+
+export const createLineGenerator = (xScale: d3.ScaleLinear<number, number, never>, yScale: d3.ScaleLinear<number, number, never>) => {
+ // TODO: nda - look into the different types of curves
+ return d3
+ .line<DataPoint>()
+ .x(d => xScale(d.x))
+ .y(d => yScale(d.y))
+ .curve(d3.curveMonotoneX);
+};
+
+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));
+};
+
+export const yAxisCreator = (g: d3.Selection<SVGGElement, unknown, null, undefined>, marginLeft: number, yScale: d3.ScaleLinear<number, number, never>) => {
+ g.attr('class', 'y-axis').call(d3.axisLeft(yScale));
+};
+
+export const xGrid = (g: d3.Selection<SVGGElement, unknown, null, undefined>, height: number, scale: d3.ScaleLinear<number, number, never>) => {
+ g.attr('class', 'xGrid')
+ .attr('transform', `translate(0,${height})`)
+ .call(
+ d3
+ .axisBottom(scale)
+ .tickSize(-height)
+ .tickFormat((a, b) => '')
+ );
+};
+
+export const yGrid = (g: d3.Selection<SVGGElement, unknown, null, undefined>, width: number, scale: d3.ScaleLinear<number, number, never>) => {
+ g.attr('class', 'yGrid').call(
+ d3
+ .axisLeft(scale)
+ .tickSize(-width)
+ .tickFormat((a, b) => '')
+ );
+};
+
+export const drawLine = (p: d3.Selection<SVGPathElement, unknown, null, undefined>, dataPts: DataPoint[], lineGen: d3.Line<DataPoint>) => {
+ p.datum(dataPts).attr('fill', 'none').attr('stroke', 'rgba(53, 162, 235, 0.5)').attr('stroke-width', 2).attr('class', 'line').attr('d', lineGen);
+};
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index 6d6609317..e954d0484 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';
@@ -9,6 +10,7 @@ import { DirectoryImportBox } from '../../util/Import & Export/DirectoryImportBo
import { CollectionDockingView } from '../collections/CollectionDockingView';
import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
import { CollectionSchemaView } from '../collections/collectionSchema/CollectionSchemaView';
+import { SchemaRowBox } from '../collections/collectionSchema/SchemaRowBox';
import { CollectionView } from '../collections/CollectionView';
import { InkingStroke } from '../InkingStroke';
import { PresElementBox } from '../nodes/trails/PresElementBox';
@@ -24,7 +26,6 @@ import { DocumentViewProps } from './DocumentView';
import './DocumentView.scss';
import { EquationBox } from './EquationBox';
import { FieldView, FieldViewProps } from './FieldView';
-import { FilterBox } from './FilterBox';
import { FormattedTextBox } from './formattedText/FormattedTextBox';
import { FunctionPlotBox } from './FunctionPlotBox';
import { ImageBox } from './ImageBox';
@@ -32,8 +33,10 @@ import { KeyValueBox } from './KeyValueBox';
import { LabelBox } from './LabelBox';
import { LinkAnchorBox } from './LinkAnchorBox';
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';
@@ -43,7 +46,6 @@ import { VideoBox } from './VideoBox';
import { WebBox } from './WebBox';
import React = require('react');
import XRegExp = require('xregexp');
-import { LoadingBox } from './LoadingBox';
const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
@@ -59,7 +61,7 @@ class ObserverJsxParser1 extends JsxParser {
}
}
-const ObserverJsxParser: typeof JsxParser = ObserverJsxParser1 as any;
+export const ObserverJsxParser: typeof JsxParser = ObserverJsxParser1 as any;
interface HTMLtagProps {
Document: Doc;
@@ -120,16 +122,16 @@ export class DocumentContentsView extends React.Component<
select: (ctrl: boolean) => void;
NativeDimScaling?: () => number;
setHeight?: (height: number) => void;
- layoutKey: string;
+ layout_fieldKey: string;
}
> {
@computed get layout(): string {
TraceMobx();
if (this.props.LayoutTemplateString) return this.props.LayoutTemplateString;
if (!this.layoutDoc) return '<p>awaiting layout</p>';
- if (this.props.layoutKey === 'layout_keyValue') return StrCast(this.props.Document.layout_keyValue, KeyValueBox.LayoutString('data'));
- const layout = Cast(this.layoutDoc[this.layoutDoc === this.props.Document && this.props.layoutKey ? this.props.layoutKey : StrCast(this.layoutDoc.layoutKey, 'layout')], 'string');
- if (layout === undefined) return this.props.Document.data ? "<FieldView {...props} fieldKey='data' />" : KeyValueBox.LayoutString(this.layoutDoc.proto ? 'proto' : '');
+ if (this.props.layout_fieldKey === 'layout_keyValue') return StrCast(this.props.Document.layout_keyValue, KeyValueBox.LayoutString());
+ const layout = Cast(this.layoutDoc[this.layoutDoc === this.props.Document && this.props.layout_fieldKey ? this.props.layout_fieldKey : StrCast(this.layoutDoc.layout_fieldKey, 'layout')], 'string');
+ if (layout === undefined) return this.props.Document.data ? "<FieldView {...props} fieldKey='data' />" : KeyValueBox.LayoutString();
if (typeof layout === 'string') return layout;
return '<p>Loading layout</p>';
}
@@ -139,15 +141,14 @@ export class DocumentContentsView extends React.Component<
return proto instanceof Promise ? undefined : proto;
}
get layoutDoc() {
- const params = StrCast(this.props.Document.PARAMS);
// bcz: replaced this with below : is it correct? change was made to accommodate passing fieldKey's from a layout script
- // const template: Doc = this.props.LayoutTemplate?.() || Doc.Layout(this.props.Document, this.props.layoutKey ? Cast(this.props.Document[this.props.layoutKey], Doc, null) : undefined);
+ // const template: Doc = this.props.LayoutTemplate?.() || Doc.Layout(this.props.Document, this.props.layout_fieldKey ? Cast(this.props.Document[this.props.layout_fieldKey], Doc, null) : undefined);
const template: Doc =
this.props.LayoutTemplate?.() ||
(this.props.LayoutTemplateString && this.props.Document) ||
- (this.props.layoutKey && StrCast(this.props.Document[this.props.layoutKey]) && this.props.Document) ||
- Doc.Layout(this.props.Document, this.props.layoutKey ? Cast(this.props.Document[this.props.layoutKey], Doc, null) : undefined);
- return Doc.expandTemplateLayout(template, this.props.Document, params ? '(' + params + ')' : this.props.layoutKey);
+ (this.props.layout_fieldKey && StrCast(this.props.Document[this.props.layout_fieldKey]) && this.props.Document) ||
+ Doc.Layout(this.props.Document, this.props.layout_fieldKey ? Cast(this.props.Document[this.props.layout_fieldKey], Doc, null) : undefined);
+ return Doc.expandTemplateLayout(template, this.props.Document);
}
CreateBindings(onClick: Opt<ScriptField>, onInput: Opt<ScriptField>): JsxBindings {
@@ -155,7 +156,6 @@ export class DocumentContentsView extends React.Component<
// these are the properties in DocumentViewProps that need to be removed to pass on only DocumentSharedViewProps to the FieldViews
'hideResizeHandles',
'hideTitle',
- 'treeViewDoc',
'contentPointerEvents',
'radialMenu',
'LayoutTemplateString',
@@ -254,7 +254,6 @@ export class DocumentContentsView extends React.Component<
YoutubeBox,
PresElementBox,
SearchBox,
- FilterBox,
FunctionPlotBox,
ColorBox,
DashWebRTCVideo,
@@ -268,12 +267,14 @@ export class DocumentContentsView extends React.Component<
HTMLtag,
ComparisonBox,
LoadingBox,
+ PhysicsSimulationBox,
+ SchemaRowBox,
}}
bindings={bindings}
jsx={layoutFrame}
showWarnings={true}
onError={(test: any) => {
- console.log('DocumentContentsView:' + test);
+ console.log('DocumentContentsView:' + test, bindings, layoutFrame);
}}
/>
);
diff --git a/src/client/views/nodes/DocumentIcon.tsx b/src/client/views/nodes/DocumentIcon.tsx
index 56de2d1fc..6e2ed72b8 100644
--- a/src/client/views/nodes/DocumentIcon.tsx
+++ b/src/client/views/nodes/DocumentIcon.tsx
@@ -1,23 +1,39 @@
-
-import { observer } from "mobx-react";
-import * as React from "react";
-import { DocumentView } from "./DocumentView";
-import { DocumentManager } from "../../util/DocumentManager";
-import { Transformer, ts } from "../../util/Scripting";
-import { Field } from "../../../fields/Doc";
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { DocumentView } from './DocumentView';
+import { DocumentManager } from '../../util/DocumentManager';
+import { Transformer, ts } from '../../util/Scripting';
+import { Field } from '../../../fields/Doc';
+import { Tooltip } from '@material-ui/core';
+import { action, observable } from 'mobx';
+import { Id } from '../../../fields/FieldSymbols';
+import { factory } from 'typescript';
+import { LightboxView } from '../LightboxView';
@observer
-export class DocumentIcon extends React.Component<{ view: DocumentView, index: number }> {
+export class DocumentIcon extends React.Component<{ view: DocumentView; index: number }> {
+ @observable _hovered = false;
+ static get DocViews() {
+ return LightboxView.LightboxDoc ? DocumentManager.Instance.DocumentViews.filter(v => LightboxView.IsLightboxDocView(v.props.docViewPath())) : DocumentManager.Instance.DocumentViews;
+ }
render() {
const view = this.props.view;
const { left, top, right, bottom } = view.getBounds() || { left: 0, top: 0, right: 0, bottom: 0 };
return (
- <div className="documentIcon-outerDiv" style={{
- position: "absolute",
- transform: `translate(${(left + right) / 2}px, ${top}px)`,
- }}>
- <p>d{this.props.index}</p>
+ <div
+ className="documentIcon-outerDiv"
+ onPointerEnter={action(e => (this._hovered = true))}
+ onPointerLeave={action(e => (this._hovered = false))}
+ style={{
+ pointerEvents: 'all',
+ opacity: this._hovered ? 0.3 : 1,
+ position: 'absolute',
+ transform: `translate(${(left + right) / 2}px, ${top}px)`,
+ }}>
+ <Tooltip title={<>{this.props.view.rootDoc.title}</>}>
+ <p>d{this.props.index}</p>
+ </Tooltip>
</div>
);
}
@@ -41,7 +57,9 @@ export class DocumentIconContainer extends React.Component {
const match = node.text.match(/d([0-9]+)/);
if (match) {
const m = parseInt(match[1]);
+ const doc = DocumentIcon.DocViews[m].rootDoc;
usedDocuments.add(m);
+ return factory.createIdentifier(`idToDoc("${doc[Id]}")`);
}
}
}
@@ -52,14 +70,14 @@ export class DocumentIconContainer extends React.Component {
};
},
getVars() {
- const docs = Array.from(DocumentManager.Instance.DocumentViews);
+ const docs = DocumentIcon.DocViews;
const capturedVariables: { [name: string]: Field } = {};
- usedDocuments.forEach(index => capturedVariables[`d${index}`] = docs[index].props.Document);
- return { capturedVariables };
- }
+ usedDocuments.forEach(index => (capturedVariables[`d${index}`] = docs.length > index ? docs[index].props.Document : `d${index}`));
+ return capturedVariables;
+ },
};
}
render() {
- return Array.from(DocumentManager.Instance.DocumentViews).map((dv, i) => <DocumentIcon key={i} index={i} view={dv} />);
+ return DocumentIcon.DocViews.map((dv, i) => <DocumentIcon key={i} index={i} view={dv} />);
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx
index 627487a9e..7723a088d 100644
--- a/src/client/views/nodes/DocumentLinksButton.tsx
+++ b/src/client/views/nodes/DocumentLinksButton.tsx
@@ -9,13 +9,14 @@ 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';
import { TaskCompletionBox } from './TaskCompletedBox';
import React = require('react');
import _ = require('lodash');
+import { PinProps } from './trails';
const higflyout = require('@hig/flyout');
export const { anchorPoints } = higflyout;
@@ -51,7 +52,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
dragComplete: dropEv => {
if (this.props.View && dropEv.linkDocument) {
// dropEv.linkDocument equivalent to !dropEve.aborted since linkDocument is only assigned on a completed drop
- !dropEv.linkDocument.linkRelationship && (Doc.GetProto(dropEv.linkDocument).linkRelationship = 'hyperlink');
+ !dropEv.linkDocument.link_relationship && (Doc.GetProto(dropEv.linkDocument).link_relationship = 'hyperlink');
}
linkDrag?.end();
},
@@ -71,9 +72,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
this.onLinkButtonMoved,
emptyFunction,
action((e, doubleTap) => {
- if (doubleTap) {
- DocumentView.showBackLinks(this.props.View.rootDoc);
- }
+ doubleTap && DocumentView.showBackLinks(this.props.View.rootDoc);
}),
undefined,
undefined,
@@ -81,7 +80,6 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
);
};
- @undoBatch
onLinkButtonDown = (e: React.PointerEvent): void => {
setupMoveUpEvents(
this,
@@ -126,66 +124,23 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
e,
returnFalse,
emptyFunction,
- undoBatch(
- action((e, doubleTap) => {
- if (doubleTap && !this.props.StartLink) {
- if (DocumentLinksButton.StartLink === this.props.View.props.Document) {
- DocumentLinksButton.StartLink = undefined;
- DocumentLinksButton.StartLinkView = undefined;
- DocumentLinksButton.AnnotationId = undefined;
- } else if (DocumentLinksButton.StartLink && DocumentLinksButton.StartLink !== this.props.View.props.Document) {
- const sourceDoc = DocumentLinksButton.StartLink;
- const targetDoc = this.props.View.ComponentView?.getAnchor?.() || this.props.View.Document;
- const linkDoc = DocUtils.MakeLink({ doc: sourceDoc }, { doc: targetDoc }, 'links'); //why is long drag here when this is used for completing links by clicking?
-
- LinkManager.currentLink = linkDoc;
-
- runInAction(() => {
- if (linkDoc) {
- TaskCompletionBox.textDisplayed = 'Link Created';
- TaskCompletionBox.popupX = e.screenX;
- TaskCompletionBox.popupY = e.screenY - 133;
- TaskCompletionBox.taskCompleted = true;
-
- LinkDescriptionPopup.popupX = e.screenX;
- LinkDescriptionPopup.popupY = e.screenY - 100;
- LinkDescriptionPopup.descriptionPopup = true;
-
- const rect = document.body.getBoundingClientRect();
- if (LinkDescriptionPopup.popupX + 200 > rect.width) {
- LinkDescriptionPopup.popupX -= 190;
- TaskCompletionBox.popupX -= 40;
- }
- if (LinkDescriptionPopup.popupY + 100 > rect.height) {
- LinkDescriptionPopup.popupY -= 40;
- TaskCompletionBox.popupY -= 40;
- }
-
- setTimeout(
- action(() => (TaskCompletionBox.taskCompleted = false)),
- 2500
- );
- }
- });
- }
- }
- })
- )
+ 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) => {
- 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;
DocumentLinksButton.AnnotationUri = undefined;
//!this.props.StartLink
} else if (startLink !== endLink) {
- endLink = endLinkView?.docView?._componentView?.getAnchor?.() || endLink;
- startLink = DocumentLinksButton.StartLinkView?.docView?._componentView?.getAnchor?.() || startLink;
- const linkDoc = DocUtils.MakeLink({ doc: startLink }, { doc: endLink }, DocumentLinksButton.AnnotationId ? 'hypothes.is annotation' : undefined, undefined, undefined, true);
+ endLink = endLinkView?.docView?._componentView?.getAnchor?.(true, pinProps) || endLink;
+ startLink = DocumentLinksButton.StartLinkView?.docView?._componentView?.getAnchor?.(true) || startLink;
+ const linkDoc = DocUtils.MakeLink(startLink, endLink, { link_relationship: DocumentLinksButton.AnnotationId ? 'hypothes.is annotation' : undefined });
LinkManager.currentLink = linkDoc;
@@ -228,8 +183,8 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
);
}
}
- })
- );
+ });
+ }
@action clearLinks() {
DocumentLinksButton.StartLink = undefined;
@@ -238,9 +193,9 @@ 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.anchor2 as Doc], filters, []).length || DocUtils.FilterDocs([link.anchor1 as Doc], filters, []).length) {
+ 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);
}
});
@@ -279,11 +234,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
</div>
) : null}
{!this.props.StartLink && DocumentLinksButton.StartLink !== this.props.View.props.Document ? ( //if the origin node is not this node
- <div
- className={'documentLinksButton-endLink'}
- ref={this._linkButton}
- onPointerDown={DocumentLinksButton.StartLink && this.completeLink}
- onClick={e => DocumentLinksButton.StartLink && DocumentLinksButton.finishLinkClick(e.clientX, e.clientY, DocumentLinksButton.StartLink, this.props.View.props.Document, true, this.props.View)}>
+ <div className={'documentLinksButton-endLink'} ref={this._linkButton} onPointerDown={DocumentLinksButton.StartLink && this.completeLink}>
<FontAwesomeIcon className="documentdecorations-icon" icon="link" />
</div>
) : null}
@@ -305,7 +256,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
top: 0,
pointerEvents: 'none',
}}>
- <Tooltip title={!DocumentLinksButton.LinkEditorDocView ? <div className="dash-tooltip">{title}</div> : <></>}>{this.linkButtonInner}</Tooltip>
+ {DocumentLinksButton.LinkEditorDocView ? this.linkButtonInner : <Tooltip title={<div className="dash-tooltip">{title}</div>}>{this.linkButtonInner}</Tooltip>}
</div>
);
}
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index 6cadeec41..f1627e1e1 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -17,18 +17,17 @@
top: 0;
}
-.documentView-node,
-.documentView-node-topmost {
+.documentView-node {
position: inherit;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: inherit;
- transition: outline 0.3s linear;
+ // transition: outline 0.3s linear;
// background: $white; //overflow: hidden;
- transform-origin: left top;
+ transform-origin: center;
&.minimized {
width: 30px;
@@ -51,6 +50,22 @@
height: calc(100% - 20px);
}
+ .documentView-htmlOverlay {
+ position: absolute;
+ display: flex;
+ top: 0;
+ height: 100%;
+ width: 100%;
+ .documentView-htmlOverlayInner {
+ box-shadow: black 0.2vw 0.2vw 0.8vw;
+ background: rgb(255, 255, 255);
+ overflow: auto;
+ position: relative;
+ margin: auto;
+ padding: 20px;
+ }
+ }
+
.documentView-linkAnchorBoxAnchor {
display: flex;
overflow: hidden;
@@ -146,6 +161,7 @@
width: 100%;
height: 100%;
border-radius: inherit;
+ white-space: normal;
.documentView-styleContentWrapper {
width: 100%;
@@ -192,15 +208,14 @@
}
}
-.documentView-node:hover,
-.documentView-node-topmost:hover {
+.documentView-node:hover {
> .documentView-styleWrapper {
> .documentView-titleWrapper-hover {
display: inline-block;
}
- }
-
- > .documentView-styleWrapper {
+ // > .documentView-contentsView {
+ // opacity: 0.5;
+ // }
> .documentView-captionWrapper {
opacity: 1;
}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index a35400e72..b6f1626f8 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1,21 +1,20 @@
import { IconProp } from '@fortawesome/fontawesome-svg-core';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Tooltip } from '@material-ui/core';
import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
-import { AclAdmin, AclEdit, AclPrivate, DataSym, Doc, DocListCast, Field, Opt, StrListCast, WidthSym } from '../../../fields/Doc';
-import { Document } from '../../../fields/documentSchemas';
+import { computedFn } from 'mobx-utils';
+import { Bounce, Fade, Flip, LightSpeed, Roll, Rotate, Zoom } from 'react-reveal';
+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 { ObjectField } from '../../../fields/ObjectField';
+import { RefField } from '../../../fields/RefField';
import { listSpec } from '../../../fields/Schema';
import { ScriptField } from '../../../fields/ScriptField';
-import { BoolCast, Cast, ImageCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
+import { BoolCast, Cast, DocCast, ImageCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
import { AudioField } from '../../../fields/URLField';
-import { GetEffectiveAcl, SharingPermissions, TraceMobx } from '../../../fields/util';
-import { MobileInterface } from '../../../mobile/MobileInterface';
-import { emptyFunction, isTargetChildOf as isParentOf, lightOrDark, OmitKeys, returnEmptyString, returnFalse, returnTrue, returnVal, simulateMouseClick, Utils } from '../../../Utils';
+import { GetEffectiveAcl, TraceMobx } from '../../../fields/util';
+import { emptyFunction, isTargetChildOf as isParentOf, lightOrDark, returnEmptyString, returnFalse, returnTrue, returnVal, simulateMouseClick, Utils } from '../../../Utils';
import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
import { DocServer } from '../../DocServer';
import { Docs, DocUtils } from '../../documents/Documents';
@@ -25,33 +24,33 @@ import { DictationManager } from '../../util/DictationManager';
import { DocumentManager } from '../../util/DocumentManager';
import { DragManager, dropActionType } from '../../util/DragManager';
import { InteractionUtils } from '../../util/InteractionUtils';
-import { LinkFollower } from '../../util/LinkFollower';
+import { FollowLinkScript } from '../../util/LinkFollower';
import { LinkManager } from '../../util/LinkManager';
import { ScriptingGlobals } from '../../util/ScriptingGlobals';
import { SelectionManager } from '../../util/SelectionManager';
-import { SettingsManager } from '../../util/SettingsManager';
import { SharingManager } from '../../util/SharingManager';
import { SnappingManager } from '../../util/SnappingManager';
import { Transform } from '../../util/Transform';
import { undoBatch, UndoManager } from '../../util/UndoManager';
-import { CollectionView } from '../collections/CollectionView';
import { ContextMenu } from '../ContextMenu';
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 } from './DocumentContentsView';
+import { DocumentContentsView, ObserverJsxParser } from './DocumentContentsView';
import { DocumentLinksButton } from './DocumentLinksButton';
import './DocumentView.scss';
import { FieldViewProps } from './FieldView';
import { FormattedTextBox } from './formattedText/FormattedTextBox';
+import { KeyValueBox } from './KeyValueBox';
import { LinkAnchorBox } from './LinkAnchorBox';
-import { LinkDocPreview } from './LinkDocPreview';
-import { RadialMenu } from './RadialMenu';
-import { ScriptingBox } from './ScriptingBox';
-import { PresBox } from './trails/PresBox';
+import { PresEffect, PresEffectDirection } from './trails';
+import { PinProps, PresBox } from './trails/PresBox';
import React = require('react');
const { Howl } = require('howler');
@@ -64,44 +63,82 @@ declare class MediaRecorder {
constructor(e: any);
}
-export enum ViewAdjustment {
- resetView = 1,
- doNothing = 0,
+export enum OpenWhere {
+ lightbox = 'lightbox',
+ add = 'add',
+ addLeft = 'add:left',
+ addRight = 'add:right',
+ addBottom = 'add:bottom',
+ close = 'close',
+ fullScreen = 'fullScreen',
+ toggle = 'toggle',
+ toggleRight = 'toggle:right',
+ replace = 'replace',
+ replaceRight = 'replace:right',
+ replaceLeft = 'replace:left',
+ inParent = 'inParent',
+ inParentFromScreen = 'inParentFromScreen',
+ overlay = 'overlay',
+}
+export enum OpenWhereMod {
+ none = '',
+ left = 'left',
+ right = 'right',
+ top = 'top',
+ bottom = 'bottom',
+ rightKeyValue = 'rightKeyValue',
}
-
-export const ViewSpecPrefix = 'viewSpec'; // field prefix for anchor fields that are immediately copied over to the target document when link is followed. Other anchor properties will be copied over in the specific setViewSpec() method on their view (which allows for seting preview values instead of writing to the document)
export interface DocFocusOptions {
- originalTarget?: Doc; // set in JumpToDocument, used by TabDocView to determine whether to fit contents to tab
- willZoom?: boolean; // determines whether to zoom in on target document
- scale?: number; // percent of containing frame to zoom into document
- afterFocus?: DocAfterFocusFunc; // function to call after focusing on a document
+ willPan?: boolean; // determines whether to pan to target document
+ willZoomCentered?: boolean; // determines whether to zoom in on target document. if zoomScale is 0, this just centers the document
+ zoomScale?: number; // percent of containing frame to zoom into document
+ zoomTime?: number;
+ didMove?: boolean; // whether a document was changed during the showDocument process
docTransform?: Transform; // when a document can't be panned and zoomed within its own container (say a group), then we need to continue to move up the render hierarchy to find something that can pan and zoom. when this happens the docTransform must accumulate all the transforms of each level of the hierarchy
instant?: boolean; // whether focus should happen instantly (as opposed to smooth zoom)
+ preview?: boolean; // whether changes should be previewed by the componentView or written to the document
+ effect?: Doc; // animation effect for focus
+ noSelect?: boolean; // whether target should be selected after focusing
+ playAudio?: boolean; // whether to play audio annotation on focus
+ openLocation?: OpenWhere; // where to open a missing document
+ zoomTextSelections?: boolean; // whether to display a zoomed overlay of anchor text selections
+ toggleTarget?: boolean; // whether to toggle target on and off
+ anchorDoc?: Doc; // doc containing anchor info to apply at end of focus to target doc
+ easeFunc?: 'linear' | 'ease'; // transition method for scrolling
}
-export type DocAfterFocusFunc = (notFocused: boolean) => Promise<ViewAdjustment>;
-export type DocFocusFunc = (doc: Doc, options: DocFocusOptions) => void;
+export type DocFocusFunc = (doc: Doc, options: DocFocusOptions) => Opt<number>;
export type StyleProviderFunc = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string) => any;
export interface DocComponentView {
updateIcon?: () => void; // updates the icon representation of the document
- getAnchor?: () => Doc; // returns an Anchor Doc that represents the current state of the doc's componentview (e.g., the current playhead location of a an audio/video box)
- scrollFocus?: (doc: Doc, smooth: boolean) => Opt<number>; // returns the duration of the focus
+ getAnchor?: (addAsAnnotation: boolean, pinData?: PinProps) => Doc; // returns an Anchor Doc that represents the current state of the doc's componentview (e.g., the current playhead location of a an audio/video box)
+ restoreView?: (viewSpec: Doc) => boolean;
+ scrollPreview?: (docView: DocumentView, doc: Doc, focusSpeed: number, options: DocFocusOptions) => Opt<number>; // returns the duration of the focus
brushView?: (view: { width: number; height: number; panX: number; panY: number }) => void;
- setViewSpec?: (anchor: Doc, preview: boolean) => void; // sets viewing information for a componentview, typically when following a link. 'preview' tells the view to use the values without writing to the document
+ 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;
menuControls?: () => JSX.Element; // controls to display in the top menu bar when the document is selected.
isAnyChildContentActive?: () => boolean; // is any child content of the document active
+ onClickScriptDisable?: () => 'never' | 'always'; // disable click scripts : never, always, or undefined = only when selected
getKeyFrameEditing?: () => boolean; // whether the document is in keyframe editing mode (if it is, then all hidden documents that are not active at the keyframe time will still be shown)
setKeyFrameEditing?: (set: boolean) => void; // whether the document is in keyframe editing mode (if it is, then all hidden documents that are not active at the keyframe time will still be shown)
playFrom?: (time: number, endTime?: number) => void;
- Pause?: () => void;
- setFocus?: () => void;
+ Pause?: () => void; // pause a media document (eg, audio/video)
+ IsPlaying?: () => boolean; // is a media document playing
+ TogglePause?: (keep?: boolean) => void; // toggle media document playing state
+ setFocus?: () => void; // sets input focus to the componentView
+ setData?: (data: Field | Promise<RefField | undefined>) => boolean;
componentUI?: (boundsLeft: number, boundsTop: number) => JSX.Element | null;
+ incrementalRendering?: () => void;
+ layout_fitWidth?: () => boolean; // whether the component always fits width (eg, KeyValueBox)
+ overridePointerEvents?: () => 'all' | 'none' | undefined; // if the conmponent overrides the pointer events for the document
fieldKey?: string;
annotationKey?: string;
getTitle?: () => string;
- getScrollHeight?: () => number;
getCenter?: (xf: Transform) => { X: number; Y: number };
ptToScreen?: (pt: { X: number; Y: number }) => { X: number; Y: number };
ptFromScreen?: (pt: { X: number; Y: number }) => { X: number; Y: number };
@@ -116,37 +153,39 @@ export interface DocumentViewSharedProps {
Document: Doc;
DataDoc?: Doc;
contentBounds?: () => undefined | { x: number; y: number; r: number; b: number };
- fitContentsToBox?: () => boolean; // used by freeformview to fit its contents to its panel. corresponds to _fitContentsToBox property on a Document
- ContainingCollectionView: Opt<CollectionView>;
- ContainingCollectionDoc: Opt<Doc>;
+ fitContentsToBox?: () => boolean; // used by freeformview to fit its contents to its panel. corresponds to _freeform_fitContentsToBox property on a Document
suppressSetHeight?: boolean;
thumbShown?: () => boolean;
- isHovering?: () => boolean;
setContentView?: (view: DocComponentView) => any;
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;
- fitWidth?: (doc: Doc) => boolean;
- docFilters: () => string[];
- docRangeFilters: () => string[];
+ layout_fitWidth?: (doc: Doc) => boolean | undefined;
+ childFilters: () => string[];
+ childFiltersByRanges: () => string[];
searchFilterDocs: () => Doc[];
- showTitle?: () => string;
+ layout_showTitle?: () => string;
whenChildContentsActiveChanged: (isActive: boolean) => void;
rootSelected: (outsideReaction?: boolean) => boolean; // whether the root of a template has been selected
- addDocTab: (doc: Doc, where: string) => boolean;
+ addDocTab: (doc: Doc, where: OpenWhere) => boolean;
filterAddDocument?: (doc: Doc[]) => boolean; // allows a document that renders a Collection view to filter or modify any documents added to the collection (see PresBox for an example)
- addDocument?: (doc: Doc | Doc[]) => boolean;
- removeDocument?: (doc: Doc | Doc[]) => boolean;
- moveDocument?: (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => boolean;
- pinToPres: (document: Doc) => void;
+ addDocument?: (doc: Doc | Doc[], annotationKey?: string) => boolean;
+ removeDocument?: (doc: Doc | Doc[], annotationKey?: string) => boolean;
+ moveDocument?: (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[], annotationKey?: string) => boolean) => boolean;
+ pinToPres: (document: Doc, pinProps: PinProps) => void;
ScreenToLocalTransform: () => Transform;
bringToFront: (doc: Doc, sendToBack?: boolean) => void;
+ dragAction?: dropActionType;
+ treeViewDoc?: Doc;
xPadding?: number;
yPadding?: number;
dropAction?: dropActionType;
@@ -155,7 +194,10 @@ 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
+ waitForDoubleClickToClick?: () => 'never' | 'always' | undefined;
+ defaultDoubleClick?: () => 'default' | 'ignore' | undefined;
pointerEvents?: () => Opt<string>;
scriptContext?: any; // can be assigned anything and will be passed as 'scriptContext' to any OnClick script that executes on this document
createNewFilterDoc?: () => void;
@@ -166,20 +208,20 @@ export interface DocumentViewSharedProps {
// these props are specific to DocuentViews
export interface DocumentViewProps extends DocumentViewSharedProps {
// properties specific to DocumentViews but not to FieldView
- hideResizeHandles?: boolean; // whether to suppress DocumentDecorations when this document is selected
+ hideDecorations?: boolean; // whether to suppress all DocumentDecorations when doc is selected
+ hideResizeHandles?: boolean; // whether to suppress resized handles on doc decorations when this document is selected
hideTitle?: boolean; // forces suppression of title. e.g, treeView document labels suppress titles in case they are globally active via settings
hideDecorationTitle?: boolean; // forces suppression of title. e.g, treeView document labels suppress titles in case they are globally active via settings
hideDocumentButtonBar?: boolean;
hideOpenButton?: boolean;
hideDeleteButton?: boolean;
- treeViewDoc?: Doc;
+ hideLinkAnchors?: boolean;
isDocumentActive?: () => boolean | undefined; // whether a document should handle pointer events
isContentActive: () => boolean | undefined; // whether document contents should handle pointer events
contentPointerEvents?: string; // pointer events allowed for content of a document view. eg. set to "none" in menuSidebar for sharedDocs so that you can select a document, but not interact with its contents
radialMenu?: String[];
LayoutTemplateString?: string;
dontCenter?: 'x' | 'y' | 'xy';
- dontScaleFilter?: (doc: Doc) => boolean; // decides whether a document can be scaled to fit its container vs native size with scrolling
NativeWidth?: () => number;
NativeHeight?: () => number;
NativeDimScaling?: () => number; // scaling the DocumentView does to transform its contents into its panel & needed by ScreenToLocal NOTE: Must also be added to FieldViewProps
@@ -198,8 +240,7 @@ export interface DocumentViewInternalProps extends DocumentViewProps {
NativeWidth: () => number;
NativeHeight: () => number;
isSelected: (outsideReaction?: boolean) => boolean;
- isHovering: () => boolean;
- select: (ctrlPressed: boolean) => void;
+ select: (ctrlPressed: boolean, shiftPress?: boolean) => void;
DocumentView: () => DocumentView;
viewPath: () => DocumentView[];
}
@@ -207,26 +248,27 @@ export interface DocumentViewInternalProps extends DocumentViewProps {
@observer
export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps>() {
public static SelectAfterContextMenu = true; // whether a document should be selected after it's contextmenu is triggered.
- _animateScaleTime = 300; // milliseconds;
- @observable _animateScalingTo = 0;
- @observable _pendingDoubleClick = false;
private _disposers: { [name: string]: IReactionDisposer } = {};
+ private _doubleClickTimeout: NodeJS.Timeout | undefined;
+ private _singleClickFunc: undefined | (() => any);
+ private _longPressSelector: NodeJS.Timeout | undefined;
private _downX: number = 0;
private _downY: number = 0;
- private _firstX: number = -1;
- private _firstY: number = -1;
+ private _downTime: number = 0;
private _lastTap: number = 0;
private _doubleTap = false;
private _mainCont = React.createRef<HTMLDivElement>();
private _titleRef = React.createRef<EditableView>();
- private _timeout: NodeJS.Timeout | undefined;
private _dropDisposer?: DragManager.DragDropDisposer;
private _holdDisposer?: InteractionUtils.MultiTouchEventDisposer;
protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
+
@observable _componentView: Opt<DocComponentView>; // needs to be accessed from DocumentView wrapper class
+ @observable _animateScaleTime: Opt<number>; // milliseconds for animating between views. defaults to 300 if not uset
+ @observable _animateScalingTo = 0;
- private get topMost() {
- return this.props.renderDepth === 0 && !LightboxView.LightboxDoc;
+ public get animateScaleTime() {
+ return this._animateScaleTime ?? 100;
}
public get displayName() {
return 'DocumentView(' + this.props.Document.title + ')';
@@ -237,7 +279,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
public get LayoutFieldKey() {
return Doc.LayoutFieldKey(this.layoutDoc);
}
- @computed get ShowTitle() {
+ @computed get layout_showTitle() {
return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.ShowTitle) as Opt<string>;
}
@computed get NativeDimScaling() {
@@ -246,9 +288,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
@computed get thumb() {
return ImageCast(this.layoutDoc['thumb-frozen'], ImageCast(this.layoutDoc.thumb))?.url?.href.replace('.png', '_m.png');
}
- @computed get hidden() {
- return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Hidden);
- }
@computed get opacity() {
return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Opacity);
}
@@ -261,8 +300,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
@computed get widgetDecorations() {
return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Decorations + (this.props.isSelected() ? ':selected' : ''));
}
- @computed get backgroundColor() {
- return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor);
+ @computed get backgroundBoxColor() {
+ return this.thumbShown() ? undefined : this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor + ':box');
}
@computed get docContents() {
return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.DocContents);
@@ -270,14 +309,17 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
@computed get headerMargin() {
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;
+ }
@computed get titleHeight() {
return this.props?.styleProvider?.(this.layoutDoc, this.props, StyleProp.TitleHeight) || 0;
}
- @computed get pointerEvents() {
+ get pointerEvents(): 'none' | 'all' | 'visiblePainted' | undefined {
return this.props.styleProvider?.(this.Document, this.props, StyleProp.PointerEvents + (this.props.isSelected() ? ':selected' : ''));
}
@computed get finalLayoutKey() {
- return StrCast(this.Document.layoutKey, 'layout');
+ return StrCast(this.Document.layout_fieldKey, 'layout');
}
@computed get nativeWidth() {
return this.props.NativeWidth();
@@ -285,6 +327,16 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
@computed get nativeHeight() {
return this.props.NativeHeight();
}
+ @computed get disableClickScriptFunc() {
+ const onScriptDisable = this.props.onClickScriptDisable ?? this._componentView?.onClickScriptDisable?.() ?? this.layoutDoc.onClickScriptDisable;
+ // prettier-ignore
+ return (
+ DocumentView.LongPress ||
+ onScriptDisable === 'always' ||
+ (onScriptDisable !== 'never' && (this.rootSelected() || this.props.isSelected())) ||
+ this._componentView?.isAnyChildContentActive?.()
+ );
+ }
@computed get onClickHandler() {
return this.props.onClick?.() ?? this.props.onBrowseClick?.() ?? Cast(this.Document.onClick, ScriptField, Cast(this.layoutDoc.onClick, ScriptField, null));
}
@@ -304,11 +356,17 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
componentDidMount() {
this.setupHandlers();
}
- //componentDidUpdate() { 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));
}
@@ -322,377 +380,188 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
Object.values(this._disposers).forEach(disposer => disposer?.());
}
- handle1PointerHoldStart = (e: Event, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): any => {
- this.removeMoveListeners();
- this.removeEndListeners();
- document.removeEventListener('pointermove', this.onPointerMove);
- document.removeEventListener('pointerup', this.onPointerUp);
- if (RadialMenu.Instance._display === false) {
- this.addHoldMoveListeners();
- this.addHoldEndListeners();
- this.onRadialMenu(e, me);
- const pt = me.touchEvent.touches[me.touchEvent.touches.length - 1];
- this._firstX = pt.pageX;
- this._firstY = pt.pageY;
- }
- };
-
- handle1PointerHoldMove = (e: Event, me: InteractionUtils.MultiTouchEvent<TouchEvent>): void => {
- const pt = me.touchEvent.touches[me.touchEvent.touches.length - 1];
-
- if (this._firstX === -1 || this._firstY === -1) {
- return;
- }
- if (Math.abs(pt.pageX - this._firstX) > 150 || Math.abs(pt.pageY - this._firstY) > 150) {
- this.handle1PointerHoldEnd(e, me);
- }
- };
-
- handle1PointerHoldEnd = (e: Event, me: InteractionUtils.MultiTouchEvent<TouchEvent>): void => {
- this.removeHoldMoveListeners();
- this.removeHoldEndListeners();
- RadialMenu.Instance.closeMenu();
- this._firstX = -1;
- this._firstY = -1;
- SelectionManager.DeselectAll();
- me.touchEvent.stopPropagation();
- me.touchEvent.preventDefault();
- e.stopPropagation();
- if (RadialMenu.Instance.used) {
- this.onContextMenu(undefined, me.touches[0].pageX, me.touches[0].pageY);
- }
- };
-
- handle2PointersDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>) => {
- if (!this.props.isSelected()) {
- e.stopPropagation();
- e.preventDefault();
-
- this.removeMoveListeners();
- this.addMoveListeners();
- this.removeEndListeners();
- this.addEndListeners();
- }
- };
-
- handle1PointerDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>) => {
- SelectionManager.DeselectAll();
- if (this.Document.onPointerDown) return;
- const touch = me.touchEvent.changedTouches.item(0);
- if (touch) {
- this._downX = touch.clientX;
- this._downY = touch.clientY;
- if ((this.props.isDocumentActive?.() || this.layoutDoc.onDragStart || this.onClickHandler) && !e.ctrlKey && !this.layoutDoc._lockedPosition && !DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc)) {
- e.stopPropagation();
- }
- this.removeMoveListeners();
- this.addMoveListeners();
- this.removeEndListeners();
- this.addEndListeners();
- e.stopPropagation();
- }
- };
-
- handle1PointerMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => {
- if (e.cancelBubble && this.props.isDocumentActive?.()) {
- this.removeMoveListeners();
- } else if (!e.cancelBubble && (this.props.isDocumentActive?.() || this.layoutDoc.onDragStart || this.onClickHandler) && !this.layoutDoc._lockedPosition && !DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc)) {
- const touch = me.touchEvent.changedTouches.item(0);
- if (touch && (Math.abs(this._downX - touch.clientX) > 3 || Math.abs(this._downY - touch.clientY) > 3)) {
- if (!e.altKey && (!this.topMost || this.layoutDoc.onDragStart || this.onClickHandler)) {
- this.cleanUpInteractions();
- this.startDragging(this._downX, this._downY, this.Document.dropAction ? (this.Document.dropAction as any) : e.ctrlKey || e.altKey ? 'alias' : undefined);
- }
- }
- e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers
- e.preventDefault();
- }
- };
-
- @action
- handle2PointersMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => {
- const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true);
- const pt1 = myTouches[0];
- const pt2 = myTouches[1];
- const oldPoint1 = this.prevPoints.get(pt1.identifier);
- const oldPoint2 = this.prevPoints.get(pt2.identifier);
- const pinching = InteractionUtils.Pinning(pt1, pt2, oldPoint1!, oldPoint2!);
- if (pinching !== 0 && oldPoint1 && oldPoint2) {
- const dW = Math.abs(pt1.clientX - pt2.clientX) - Math.abs(oldPoint1.clientX - oldPoint2.clientX);
- const dH = Math.abs(pt1.clientY - pt2.clientY) - Math.abs(oldPoint1.clientY - oldPoint2.clientY);
- const dX = -1 * Math.sign(dW);
- const dY = -1 * Math.sign(dH);
-
- if (dX !== 0 || dY !== 0 || dW !== 0 || dH !== 0) {
- const doc = Document(this.props.Document);
- const layoutDoc = Document(Doc.Layout(this.props.Document));
- let nwidth = Doc.NativeWidth(layoutDoc);
- let nheight = Doc.NativeHeight(layoutDoc);
- const width = layoutDoc._width || 0;
- const height = layoutDoc._height || (nheight / nwidth) * width;
- const scale = this.props.ScreenToLocalTransform().Scale * this.NativeDimScaling;
- const actualdW = Math.max(width + dW * scale, 20);
- const actualdH = Math.max(height + dH * scale, 20);
- doc.x = (doc.x || 0) + dX * (actualdW - width);
- doc.y = (doc.y || 0) + dY * (actualdH - height);
- const fixedAspect = e.ctrlKey || (nwidth && nheight);
- if (fixedAspect && (!nwidth || !nheight)) {
- Doc.SetNativeWidth(layoutDoc, (nwidth = layoutDoc._width || 0));
- Doc.SetNativeHeight(layoutDoc, (nheight = layoutDoc._height || 0));
- }
- if (nwidth > 0 && nheight > 0) {
- if (Math.abs(dW) > Math.abs(dH)) {
- if (!fixedAspect) {
- Doc.SetNativeWidth(layoutDoc, (actualdW / (layoutDoc._width || 1)) * Doc.NativeWidth(layoutDoc));
- }
- layoutDoc._width = actualdW;
- if (fixedAspect && !this.props.DocumentView().fitWidth) layoutDoc._height = (nheight / nwidth) * layoutDoc._width;
- else layoutDoc._height = actualdH;
- } else {
- if (!fixedAspect) {
- Doc.SetNativeHeight(layoutDoc, (actualdH / (layoutDoc._height || 1)) * Doc.NativeHeight(doc));
- }
- layoutDoc._height = actualdH;
- if (fixedAspect && !this.props.DocumentView().fitWidth) layoutDoc._width = (nwidth / nheight) * layoutDoc._height;
- else layoutDoc._width = actualdW;
- }
- } else {
- dW && (layoutDoc._width = actualdW);
- dH && (layoutDoc._height = actualdH);
- dH && layoutDoc._autoHeight && (layoutDoc._autoHeight = false);
- }
- }
- e.stopPropagation();
- e.preventDefault();
- }
- };
-
- @action
- onRadialMenu = (e: Event, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): void => {
- const pt = me.touchEvent.touches[me.touchEvent.touches.length - 1];
- RadialMenu.Instance.openMenu(pt.pageX - 15, pt.pageY - 15);
-
- // RadialMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "add:right"), icon: "map-pin", selected: -1 });
- const effectiveAcl = GetEffectiveAcl(this.props.Document[DataSym]);
- (effectiveAcl === AclEdit || effectiveAcl === AclAdmin) &&
- RadialMenu.Instance.addItem({
- description: 'Delete',
- event: () => {
- this.props.ContainingCollectionView?.removeDocument(this.props.Document), RadialMenu.Instance.closeMenu();
- },
- icon: 'external-link-square-alt',
- selected: -1,
- });
- // RadialMenu.Instance.addItem({ description: "Open in a new tab", event: () => this.props.addDocTab(this.props.Document, "add:right"), icon: "trash", selected: -1 });
- RadialMenu.Instance.addItem({ description: 'Pin', event: () => this.props.pinToPres(this.props.Document), icon: 'map-pin', selected: -1 });
- RadialMenu.Instance.addItem({ description: 'Open', event: () => MobileInterface.Instance.handleClick(this.props.Document), icon: 'trash', selected: -1 });
-
- SelectionManager.DeselectAll();
- };
-
startDragging(x: number, y: number, dropAction: dropActionType, hideSource = false) {
if (this._mainCont.current) {
- const dragData = new DragManager.DocumentDragData([this.props.Document]);
+ const views = SelectionManager.Views().filter(dv => dv.docView?._mainCont.current);
+ const selected = views.some(dv => dv.rootDoc === this.Document) ? views : [this.props.DocumentView()];
+ const dragData = new DragManager.DocumentDragData(selected.map(dv => dv.rootDoc));
const [left, top] = this.props.ScreenToLocalTransform().scale(this.NativeDimScaling).inverse().transformPoint(0, 0);
dragData.offset = this.props
.ScreenToLocalTransform()
.scale(this.NativeDimScaling)
.transformDirection(x - left, y - top);
- // dragData.offset[0] = Math.min(this.rootDoc[WidthSym](), dragData.offset[0]); // bcz: this was breaking dragging rotated objects since the offset may be out of bounds with regard to the unrotated document
- // dragData.offset[1] = Math.min(this.rootDoc[HeightSym](), dragData.offset[1]);
dragData.dropAction = dropAction;
dragData.treeViewDoc = this.props.treeViewDoc;
dragData.removeDocument = this.props.removeDocument;
dragData.moveDocument = this.props.moveDocument;
- //dragData.dimSource :
- // dragEffects field, set dim
- // add kv pairs to a doc, swap properties with the node while dragging, and then swap when dropping
- // add a dragEffects prop to DocumentView as a function that sets up. Each view has its own prop, when you start dragging:
- // in Draganager, figure out which doc(s) you're dragging and change what opacity function returns
+ 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([this._mainCont.current], dragData, x, y, { hideSource: hideSource || (!dropAction && !this.layoutDoc.onDragStart && !this.props.dontHideOnDrag) }, () =>
- setTimeout(action(() => ffview && (ffview.ChildDrag = undefined)))
+ DragManager.StartDocumentDrag(
+ selected.map(dv => dv.docView!._mainCont.current!),
+ dragData,
+ x,
+ y,
+ { hideSource: hideSource || (!dropAction && !this.layoutDoc.onDragStart && !this.props.dontHideOnDrag) },
+ () => setTimeout(action(() => ffview && (ffview.ChildDrag = undefined)))
); // this needs to happen after the drop event is processed.
ffview?.setupDragLines(false);
}
}
- onKeyDown = (e: React.KeyboardEvent) => {
- if (e.altKey) {
- e.stopPropagation();
- e.preventDefault();
- if (e.key === '†' || e.key === 't') {
- if (!StrCast(this.layoutDoc._showTitle)) this.layoutDoc._showTitle = 'title';
- if (!this._titleRef.current) setTimeout(() => this._titleRef.current?.setIsFocused(true), 0);
- else if (!this._titleRef.current.setIsFocused(true)) {
- // if focus didn't change, focus on interior text...
- this._titleRef.current?.setIsFocused(false);
- this._componentView?.setFocus?.();
- }
- }
- }
+ 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)?.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;
};
- focus = (anchor: Doc, options: DocFocusOptions) => {
- LightboxView.SetCookie(StrCast(anchor['cookies-set']));
- // copying over VIEW fields immediately allows the view type to switch to create the right _componentView
- Array.from(Object.keys(Doc.GetProto(anchor)))
- .filter(key => key.startsWith(ViewSpecPrefix))
- .forEach(spec => {
- this.layoutDoc[spec.replace(ViewSpecPrefix, '')] = (field => (field instanceof ObjectField ? ObjectField.MakeCopy(field) : field))(anchor[spec]);
- });
- // after a timeout, the right _componentView should have been created, so call it to update its view spec values
- setTimeout(() => this._componentView?.setViewSpec?.(anchor, LinkDocPreview.LinkInfo ? true : false));
- const focusSpeed = this._componentView?.scrollFocus?.(anchor, options?.instant === false || !LinkDocPreview.LinkInfo); // bcz: smooth parameter should really be passed into focus() instead of inferred here
- const endFocus = focusSpeed === undefined ? options?.afterFocus : async (moved: boolean) => options?.afterFocus?.(true) ?? ViewAdjustment.doNothing;
- this.props.focus(options?.docTransform ? anchor : this.rootDoc, {
- ...options,
- afterFocus: (didFocus: boolean) => new Promise<ViewAdjustment>(res => setTimeout(async () => res(endFocus ? await endFocus(didFocus || focusSpeed !== undefined) : ViewAdjustment.doNothing), focusSpeed ?? 0)),
- });
+ // switches text input focus to the title bar of the document (and displays the title bar if it hadn't been)
+ setTitleFocus = () => {
+ if (!StrCast(this.layoutDoc._layout_showTitle)) this.layoutDoc._layout_showTitle = 'title';
+ setTimeout(() => this._titleRef.current?.setIsFocused(true)); // use timeout in case title wasn't shown to allow re-render so that titleref will be defined
};
+
+ public static addDocTabFunc: (doc: Doc, location: OpenWhere) => boolean = returnFalse;
+
onClick = action((e: React.MouseEvent | React.PointerEvent) => {
- if (!this.Document.ignoreClick && this.props.renderDepth >= 0 && Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) {
+ if (!this.Document.ignoreClick && this.pointerEvents !== 'none' && this.props.renderDepth >= 0 && Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, this._downTime)) {
let stopPropagate = true;
let preventDefault = true;
- const isScriptBox = () => StrCast(Doc.LayoutField(this.layoutDoc))?.includes(ScriptingBox.name);
(this.rootDoc._raiseWhenDragged === undefined ? DragManager.GetRaiseWhenDragged() : this.rootDoc._raiseWhenDragged) && this.props.bringToFront(this.rootDoc);
- if (this._doubleTap && (this.props.Document.type !== DocumentType.FONTICON || this.onDoubleClickHandler)) {
- // && !this.onClickHandler?.script) { // disable double-click to show full screen for things that have an on click behavior since clicking them twice can be misinterpreted as a double click
- if (this._timeout) {
- clearTimeout(this._timeout);
- this._pendingDoubleClick = false;
- this._timeout = undefined;
+ if (this._doubleTap) {
+ const defaultDblclick = this.props.defaultDoubleClick?.() || this.Document.defaultDoubleClick;
+ if (this.onDoubleClickHandler?.script) {
+ const { clientX, clientY, shiftKey, altKey, ctrlKey } = e; // or we could call e.persist() to capture variables
+ // prettier-ignore
+ const func = () => this.onDoubleClickHandler.script.run( {
+ this: this.layoutDoc,
+ self: this.rootDoc,
+ scriptContext: this.props.scriptContext,
+ documentView: this.props.DocumentView(),
+ clientX, clientY, altKey, shiftKey, ctrlKey,
+ value: undefined,
+ }, console.log );
+ UndoManager.RunInBatch(() => (func().result?.select === true ? this.props.select(false) : ''), 'on double click');
+ } else if (!Doc.IsSystem(this.rootDoc) && (defaultDblclick === undefined || defaultDblclick === 'default')) {
+ UndoManager.RunInBatch(() => LightboxView.AddDocTab(this.rootDoc, OpenWhere.lightbox), 'double tap');
+ SelectionManager.DeselectAll();
+ Doc.UnBrushDoc(this.props.Document);
+ } else {
+ this._singleClickFunc?.();
}
- if (this.onDoubleClickHandler?.script && !StrCast(Doc.LayoutField(this.layoutDoc))?.includes(ScriptingBox.name)) {
- // bcz: hack? don't execute script if you're clicking on a scripting box itself
- const { clientX, clientY, shiftKey, altKey, ctrlKey } = e;
- const func = () =>
- this.onDoubleClickHandler.script.run(
+ this._doubleClickTimeout && clearTimeout(this._doubleClickTimeout);
+ this._doubleClickTimeout = undefined;
+ this._singleClickFunc = undefined;
+ } else {
+ let clickFunc: undefined | (() => any);
+ if (!this.disableClickScriptFunc && this.onClickHandler?.script) {
+ const { clientX, clientY, shiftKey, altKey, metaKey } = e;
+ const func = () => {
+ // replace default add doc func with this view's add doc func.
+ // to allow override behaviors for how to display links to undisplayed documents.
+ // e.g., if this document is part of a labeled 'lightbox' container, then documents will be shown in place
+ // instead of in the global lightbox
+ const oldFunc = DocumentViewInternal.addDocTabFunc;
+ DocumentViewInternal.addDocTabFunc = this.props.addDocTab;
+ this.onClickHandler?.script.run(
{
this: this.layoutDoc,
self: this.rootDoc,
+ _readOnly_: false,
scriptContext: this.props.scriptContext,
- thisContainer: this.props.ContainingCollectionDoc,
documentView: this.props.DocumentView(),
clientX,
clientY,
- altKey,
shiftKey,
- ctrlKey,
+ altKey,
+ metaKey,
},
console.log
- );
- UndoManager.RunInBatch(() => (func().result?.select === true ? this.props.select(false) : ''), 'on double click');
- } else if (!Doc.IsSystem(this.rootDoc) && !this.rootDoc.isLinkButton) {
- UndoManager.RunInBatch(() => LightboxView.AddDocTab(this.rootDoc, 'lightbox', this.props.LayoutTemplate?.(), this.props.addDocTab), 'double tap');
- SelectionManager.DeselectAll();
- Doc.UnBrushDoc(this.props.Document);
- }
- } else if (this.onClickHandler?.script && !isScriptBox()) {
- // bcz: hack? don't execute script if you're clicking on a scripting box itself
- const { clientX, clientY, shiftKey, altKey } = e;
- const func = () =>
- this.onClickHandler.script.run(
- {
- this: this.layoutDoc,
- self: this.rootDoc,
- _readOnly_: false,
- scriptContext: this.props.scriptContext,
- thisContainer: this.props.ContainingCollectionDoc,
- documentView: this.props.DocumentView(),
- clientX,
- clientY,
- shiftKey,
- altKey,
- },
- console.log
- ).result?.select === true
- ? this.props.select(false)
- : '';
- const clickFunc = () => (this.props.Document.dontUndo ? func() : UndoManager.RunInBatch(func, 'on click'));
- if (this.onDoubleClickHandler) {
- runInAction(() => (this._pendingDoubleClick = true));
- this._timeout = setTimeout(() => {
- this._timeout = undefined;
- clickFunc();
- }, 350);
- } else clickFunc();
- } else if (this.allLinks && this.Document.type !== DocumentType.LINK && !isScriptBox() && this.Document.isLinkButton && !e.shiftKey && !e.ctrlKey) {
- SelectionManager.DeselectAll();
- this.allLinks.length && LinkFollower.FollowLink(undefined, this.props.Document, this.props, e.altKey);
- } else {
- if ((this.layoutDoc.onDragStart || this.props.Document.rootDocument) && !(e.ctrlKey || e.button > 0)) {
- // onDragStart implies a button doc that we don't want to select when clicking. RootDocument & isTemplaetForField implies we're clicking on part of a template instance and we want to select the whole template, not the part
- stopPropagate = false; // don't stop propagation for field templates -- want the selection to propagate up to the root document of the template
+ ).result?.select === true
+ ? this.props.select(false)
+ : '';
+ DocumentViewInternal.addDocTabFunc = oldFunc;
+ };
+ clickFunc = () => UndoManager.RunInBatch(func, 'click ' + this.rootDoc.title);
} else {
- runInAction(() => (this._pendingDoubleClick = true));
- this._timeout = setTimeout(
- action(() => {
- this._pendingDoubleClick = false;
- this._timeout = undefined;
- }),
- 350
- );
- this.props.select(e.ctrlKey || e.shiftKey);
+ // 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)) {
+ stopPropagate = false; // don't stop propagation for field templates -- want the selection to propagate up to the root document of the template
+ }
+ preventDefault = false;
+ }
+ 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 if (!DocumentView.LongPress) {
+ this._singleClickFunc();
+ this._singleClickFunc = undefined;
}
- preventDefault = false;
}
stopPropagate && e.stopPropagation();
preventDefault && e.preventDefault();
}
});
+ @action
onPointerDown = (e: React.PointerEvent): void => {
- if (this.rootDoc.type === DocumentType.INK && Doc.ActiveTool === InkTool.Eraser) return;
- // continue if the event hasn't been canceled AND we are using a mouse or this has an onClick or onDragStart function (meaning it is a button document)
- if (!(InteractionUtils.IsType(e, InteractionUtils.MOUSETYPE) || [InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool))) {
- if (!InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) {
- e.stopPropagation();
- if (SelectionManager.IsSelected(this.props.DocumentView(), true) && this.props.Document._viewType !== CollectionViewType.Docking) e.preventDefault(); // goldenlayout needs to be able to move its tabs, so can't preventDefault for it
- // TODO: check here for panning/inking
+ this._longPressSelector = setTimeout(() => {
+ if (DocumentView.LongPress) {
+ if (this.rootDoc.undoIgnoreFields) {
+ runInAction(() => (UndoStack.HideInline = !UndoStack.HideInline));
+ } else {
+ this.props.select(false);
+ }
}
- return;
- }
+ }, 1000);
+ if (!GestureOverlay.DownDocView) GestureOverlay.DownDocView = this.props.DocumentView();
+
this._downX = e.clientX;
this._downY = e.clientY;
+ this._downTime = Date.now();
if ((Doc.ActiveTool === InkTool.None || this.props.addDocTab === returnFalse) && !(this.props.Document.rootDocument && !(e.ctrlKey || e.button > 0))) {
- // if this is part of a template, let the event go up to the tempalte root unless right/ctrl clicking
+ // click events stop here if the document is active and no modes are overriding it
+ // if this is part of a template, let the event go up to the template root unless right/ctrl clicking
if (
- (this.props.isDocumentActive?.() || this.layoutDoc.onDragStart) &&
+ // prettier-ignore
+ this.props.isDocumentActive?.() &&
!this.props.onBrowseClick?.() &&
!this.Document.ignoreClick &&
- !e.ctrlKey &&
- (e.button === 0 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) &&
- !DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc)
+ e.button === 0 &&
+ this.pointerEvents !== 'none' &&
+ !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.isDocumentActive?.()) {
- document.removeEventListener('pointermove', this.onPointerMove);
- document.addEventListener('pointermove', this.onPointerMove);
+ //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() || BoolCast(this.rootDoc._dragWhenActive))) {
+ document.addEventListener('pointermove', this.onPointerMove);
+ }
}
- document.removeEventListener('pointerup', this.onPointerUp);
document.addEventListener('pointerup', this.onPointerUp);
}
};
+ @action
onPointerMove = (e: PointerEvent): void => {
- if (e.cancelBubble) return;
- if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || [InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) return;
-
- if ((this.props.isDocumentActive?.() || this.layoutDoc.onDragStart) && !this.layoutDoc._lockedPosition && !DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc)) {
- if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) {
- if (!e.altKey && (!this.topMost || this.layoutDoc.onDragStart || this.onClickHandler) && (e.buttons === 1 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE))) {
- document.removeEventListener('pointermove', this.onPointerMove);
- document.removeEventListener('pointerup', this.onPointerUp);
- this.startDragging(this._downX, this._downY, ((e.ctrlKey || e.altKey) && 'alias') || ((this.props.dropAction || this.Document.dropAction || undefined) as dropActionType));
- }
- }
- e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers
- e.preventDefault();
+ if (e.buttons !== 1 || [InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) return;
+
+ 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.dragAction || this.props.dragAction || undefined) as dropActionType));
}
};
@@ -702,70 +571,47 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
document.removeEventListener('pointerup', this.onPointerUp);
};
+ @action
onPointerUp = (e: PointerEvent): void => {
this.cleanupPointerEvents();
+ this._longPressSelector && clearTimeout(this._longPressSelector);
- if (this.onPointerUpHandler?.script && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) {
+ if (this.onPointerUpHandler?.script) {
this.onPointerUpHandler.script.run({ self: this.rootDoc, this: this.layoutDoc }, console.log);
- } else {
- this._doubleTap = Date.now() - this._lastTap < 300 && e.button === 0 && Math.abs(e.clientX - this._downX) < 2 && Math.abs(e.clientY - this._downY) < 2;
- // bcz: this is a placeholder. documents, when selected, should stopPropagation on doubleClicks if they want to keep the DocumentView from getting them
- if (!this.props.isSelected(true) || ![DocumentType.PDF, DocumentType.RTF].includes(StrCast(this.rootDoc.type) as any)) this._lastTap = Date.now(); // don't want to process the start of a double tap if the doucment is selected
+ } else if (e.button === 0 && Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, this._downTime)) {
+ 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();
};
@undoBatch
@action
- toggleFollowLink = (location: Opt<string>, zoom?: boolean, setPushpin?: boolean): void => {
- this.Document.ignoreClick = false;
- if (setPushpin) {
- this.Document.isPushpin = !this.Document.isPushpin;
- this.Document._isLinkButton = this.Document.isPushpin || this.Document._isLinkButton;
- } else {
- this.Document._isLinkButton = !this.Document._isLinkButton;
- }
- if (this.Document._isLinkButton && !this.onClickHandler) {
- zoom !== undefined && (this.Document.followLinkZoom = zoom);
- this.Document.followLinkLocation = location;
- } else if (this.Document._isLinkButton && this.onClickHandler) {
- this.Document._isLinkButton = false;
- this.dataDoc.onClick = this.Document.onClick = this.layoutDoc.onClick = undefined;
- }
- };
- @undoBatch
- @action
- toggleTargetOnClick = (): void => {
- this.Document.ignoreClick = false;
- this.Document._isLinkButton = true;
- this.Document.isPushpin = true;
- };
- @undoBatch
- @action
- followLinkOnClick = (location: Opt<string>, zoom: boolean): void => {
- this.Document.ignoreClick = false;
- this.Document._isLinkButton = true;
- this.Document.isPushpin = false;
- this.Document.followLinkZoom = zoom;
- this.Document.followLinkLocation = location;
+ toggleFollowLink = (zoom?: boolean, setTargetToggle?: boolean): void => {
+ const hadOnClick = this.rootDoc.onClick;
+ this.noOnClick();
+ this.Document.onClick = hadOnClick ? undefined : FollowLinkScript();
+ this.Document.waitForDoubleClickToClick = hadOnClick ? undefined : 'never';
};
@undoBatch
@action
- selectOnClick = (): void => {
+ followLinkOnClick = (): void => {
this.Document.ignoreClick = false;
- this.Document._isLinkButton = false;
- this.Document.isPushpin = false;
- this.Document.onClick = this.layoutDoc.onClick = undefined;
+ this.Document.onClick = FollowLinkScript();
+ this.Document.followLinkToggle = false;
+ this.Document.followLinkZoom = false;
+ this.Document.followLinkLocation = undefined;
};
@undoBatch
noOnClick = (): void => {
this.Document.ignoreClick = false;
- this.Document._isLinkButton = false;
+ this.Document.onClick = Doc.GetProto(this.Document).onClick = undefined;
};
@undoBatch deleteClicked = () => this.props.removeDocument?.(this.props.Document);
@undoBatch setToggleDetail = () =>
(this.Document.onClick = ScriptField.MakeScript(
- `toggleDetail(documentView, "${StrCast(this.Document.layoutKey)
+ `toggleDetail(documentView, "${StrCast(this.Document.layout_fieldKey)
.replace('layout_', '')
.replace(/^layout$/, 'detail')}")`,
{ documentView: 'any' }
@@ -773,42 +619,68 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
@undoBatch
@action
- drop = async (e: Event, de: DragManager.DropEvent) => {
- if (this.props.dontRegisterView || this.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) return;
+ drop = (e: Event, de: DragManager.DropEvent) => {
+ 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);
- }
- if (de.complete.annoDragData || this.rootDoc !== linkdrag.linkSourceDoc.context) {
- const dropDoc = de.complete.annoDragData?.dropDocument ?? this._componentView?.getAnchor?.() ?? this.props.Document;
- de.complete.linkDocument = DocUtils.MakeLink({ doc: linkdrag.linkSourceDoc }, { doc: dropDoc }, undefined, undefined, undefined, undefined, [de.x, de.y - 50]);
+ if (linkdrag) {
+ linkdrag.linkSourceDoc = linkdrag.linkSourceGetAnchor();
+ if (linkdrag.linkSourceDoc) {
+ if (de.complete.annoDragData && !de.complete.annoDragData.dropDocument) {
+ de.complete.annoDragData.dropDocument = de.complete.annoDragData.dropDocCreator(undefined);
+ }
+ if (de.complete.annoDragData || this.rootDoc !== linkdrag.linkSourceDoc.embedContainer) {
+ 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
@action
- makeIntoPortal = async () => {
- const portalLink = this.allLinks.find(d => d.anchor1 === this.props.Document);
+ makeIntoPortal = () => {
+ const portalLink = this.allLinks.find(d => d.link_anchor_1 === this.props.Document && d.link_relationship === 'portal to:portal from');
if (!portalLink) {
- const portal = Docs.Create.FreeformDocument([], { _width: NumCast(this.layoutDoc._width) + 10, _height: NumCast(this.layoutDoc._height), _fitWidth: true, title: StrCast(this.props.Document.title) + ' [Portal]' });
- DocUtils.MakeLink({ doc: this.props.Document }, { doc: portal }, 'portal to:portal from');
+ DocUtils.MakeLink(
+ this.props.Document,
+ Docs.Create.FreeformDocument([], { _width: NumCast(this.layoutDoc._width) + 10, _height: NumCast(this.layoutDoc._height), _isLightbox: true, _layout_fitWidth: true, title: StrCast(this.props.Document.title) + ' [Portal]' }),
+ { link_relationship: 'portal to:portal from' }
+ );
}
- this.Document.followLinkLocation = 'inPlace';
- this.Document.followLinkZoom = true;
- this.Document._isLinkButton = true;
+ this.Document.followLinkLocation = OpenWhere.lightbox;
+ this.Document.onClick = FollowLinkScript();
+ };
+
+ importDocument = () => {
+ const input = document.createElement('input');
+ input.type = 'file';
+ input.accept = '.zip';
+ input.onchange = _e => {
+ if (input.files) {
+ const batch = UndoManager.StartBatch('importing');
+ Doc.importDocument(input.files[0]).then(doc => {
+ if (doc instanceof Doc) {
+ this.props.addDocTab(doc, OpenWhere.addRight);
+ batch.end();
+ }
+ });
+ }
+ };
+ input.click();
};
@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);
@@ -832,14 +704,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
if (!cm || (e as any)?.nativeEvent?.SchemaHandled) return;
if (e && !(e.nativeEvent as any).dash) {
- const onDisplay = () =>
- setTimeout(() => {
- DocumentViewInternal.SelectAfterContextMenu && !this.props.isSelected(true) && SelectionManager.SelectView(this.props.DocumentView(), false); // on a mac, the context menu is triggered on mouse down, but a YouTube video becaomes interactive when selected which means that the context menu won't show up. by delaying the selection until hopefully after the pointer up, the context menu will appear.
- setTimeout(() => {
- const ele = document.elementFromPoint(e.clientX, e.clientY);
- simulateMouseClick(ele, e.clientX, e.clientY, e.screenX, e.screenY);
- });
- });
+ const onDisplay = () => {
+ DocumentViewInternal.SelectAfterContextMenu && !this.props.isSelected(true) && SelectionManager.SelectView(this.props.DocumentView(), false); // on a mac, the context menu is triggered on mouse down, but a YouTube video becaomes interactive when selected which means that the context menu won't show up. by delaying the selection until hopefully after the pointer up, the context menu will appear.
+ setTimeout(() => simulateMouseClick(document.elementFromPoint(e.clientX, e.clientY), e.clientX, e.clientY, e.screenX, e.screenY));
+ };
if (navigator.userAgent.includes('Macintosh')) {
cm.displayMenu((e?.pageX || pageX || 0) - 15, (e?.pageY || pageY || 0) - 15, undefined, undefined, onDisplay);
} else {
@@ -857,79 +725,52 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
.forEach(item => item.label && cm.addItem({ description: item.label, event: () => item.script.script.run({ this: this.layoutDoc, scriptContext: this.props.scriptContext, self: this.rootDoc }), icon: item.icon as IconProp }));
if (!this.props.Document.isFolder) {
- const templateDoc = Cast(this.props.Document[StrCast(this.props.Document.layoutKey)], Doc, null);
+ const 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 : [];
- !Doc.noviceMode && templateDoc && appearanceItems.push({ description: 'Open Template ', event: () => this.props.addDocTab(templateDoc, 'add:right'), icon: 'eye' });
- !Doc.noviceMode &&
- appearanceItems.push({
- description: 'Add a Field',
- event: () => {
- const alias = Doc.MakeAlias(this.rootDoc);
- alias.layout = FormattedTextBox.LayoutString('newfield');
- alias.title = 'newfield';
- alias._height = 35;
- alias._width = 100;
- alias.syncLayoutFieldWithTitle = true;
- alias.x = NumCast(this.rootDoc.x) + NumCast(this.rootDoc.width);
- alias.y = NumCast(this.rootDoc.y);
- this.props.addDocument?.(alias);
- },
- icon: 'eye',
- });
- DocListCast(this.Document.links).length &&
- appearanceItems.splice(0, 0, { description: `${this.layoutDoc.hideLinkButton ? 'Show' : 'Hide'} Link Button`, event: action(() => (this.layoutDoc.hideLinkButton = !this.layoutDoc.hideLinkButton)), icon: 'eye' });
- !appearance && cm.addItem({ description: 'UI Controls...', subitems: appearanceItems, icon: 'compass' });
+ !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._viewType !== CollectionViewType.Docking && this.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Tree) {
+ 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 : [];
if (this.props.bringToFront !== emptyFunction) {
const zorders = cm.findByDescription('ZOrder...');
const zorderItems: ContextMenuProps[] = zorders && 'subitems' in zorders ? zorders.subitems : [];
- zorderItems.push({ description: 'Bring to Front', event: () => SelectionManager.Views().forEach(dv => dv.props.bringToFront(dv.rootDoc, false)), icon: 'expand-arrows-alt' });
- zorderItems.push({ description: 'Send to Back', event: () => SelectionManager.Views().forEach(dv => dv.props.bringToFront(dv.rootDoc, true)), icon: 'expand-arrows-alt' });
+ zorderItems.push({ description: 'Bring to Front', event: () => SelectionManager.Views().forEach(dv => dv.props.bringToFront(dv.rootDoc, false)), icon: 'arrow-up' });
+ zorderItems.push({ description: 'Send to Back', event: () => SelectionManager.Views().forEach(dv => dv.props.bringToFront(dv.rootDoc, true)), icon: 'arrow-down' });
zorderItems.push({
description: this.rootDoc._raiseWhenDragged !== false ? 'Keep ZIndex when dragged' : 'Allow ZIndex to change when dragged',
event: undoBatch(action(() => (this.rootDoc._raiseWhenDragged = this.rootDoc._raiseWhenDragged === undefined ? false : undefined))),
- icon: 'expand-arrows-alt',
+ icon: 'hand-point-up',
});
- !zorders && cm.addItem({ description: 'ZOrder...', noexpand: true, subitems: zorderItems, icon: 'compass' });
+ !zorders && cm.addItem({ description: 'Z Order...', addDivider: true, noexpand: true, subitems: zorderItems, icon: 'compass' });
}
- !Doc.noviceMode && onClicks.push({ description: 'Enter Portal', event: this.makeIntoPortal, icon: 'window-restore' });
+ onClicks.push({ description: 'Enter Portal', event: this.makeIntoPortal, icon: 'window-restore' });
!Doc.noviceMode && onClicks.push({ description: 'Toggle Detail', event: this.setToggleDetail, icon: 'concierge-bell' });
- this.props.CollectionFreeFormDocumentView &&
- onClicks.push({
- description: (this.Document.followLinkZoom ? "Don't" : '') + ' zoom following link',
- event: () => (this.Document.followLinkZoom = !this.Document.followLinkZoom),
- icon: this.Document.ignoreClick ? 'unlock' : 'lock',
- });
- if (!this.Document.annotationOn) {
- const options = cm.findByDescription('Options...');
- const optionItems: ContextMenuProps[] = options && 'subitems' in options ? options.subitems : [];
- !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'compass' });
-
- onClicks.push({ description: this.Document.ignoreClick ? 'Select' : 'Do Nothing', event: () => (this.Document.ignoreClick = !this.Document.ignoreClick), icon: this.Document.ignoreClick ? 'unlock' : 'lock' });
- onClicks.push({ description: this.Document.isLinkButton ? 'Remove Follow Behavior' : 'Follow Link in Place', event: () => this.toggleFollowLink('inPlace', true, false), icon: 'link' });
- !this.Document.isLinkButton && onClicks.push({ description: 'Follow Link on Right', event: () => this.toggleFollowLink('add:right', false, false), icon: 'link' });
- onClicks.push({ description: this.Document.isLinkButton || this.onClickHandler ? 'Remove Click Behavior' : 'Follow Link', event: () => this.toggleFollowLink(undefined, false, false), icon: 'link' });
- onClicks.push({ description: (this.Document.isPushpin ? 'Remove' : 'Make') + ' Pushpin', event: () => this.toggleFollowLink(undefined, false, true), icon: 'map-pin' });
- onClicks.push({ description: 'Edit onClick Script', event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.props.Document, undefined, 'onClick'), 'edit onClick'), icon: 'terminal' });
- !existingOnClick && cm.addItem({ description: 'OnClick...', addDivider: true, noexpand: true, subitems: onClicks, icon: 'mouse-pointer' });
- } else if (DocListCast(this.Document.links).length) {
- onClicks.push({ description: 'Select on Click', event: () => this.selectOnClick(), icon: 'link' });
- onClicks.push({ description: 'Follow Link on Click', event: () => this.followLinkOnClick(undefined, false), icon: 'link' });
- onClicks.push({ description: 'Toggle Link Target on Click', event: () => this.toggleTargetOnClick(), icon: 'map-pin' });
- !existingOnClick && cm.addItem({ description: 'OnClick...', addDivider: true, subitems: onClicks, icon: 'mouse-pointer' });
+ 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' });
+ }
}
}
const funcs: ContextMenuProps[] = [];
if (!Doc.noviceMode && this.layoutDoc.onDragStart) {
- funcs.push({ description: 'Drag an Alias', icon: 'edit', event: () => this.Document.dragFactory && (this.layoutDoc.onDragStart = ScriptField.MakeFunction('getAlias(this.dragFactory)')) });
+ funcs.push({ description: 'Drag an Embedding', icon: 'edit', event: () => this.Document.dragFactory && (this.layoutDoc.onDragStart = ScriptField.MakeFunction('getEmbedding(this.dragFactory)')) });
funcs.push({ description: 'Drag a Copy', icon: 'edit', event: () => this.Document.dragFactory && (this.layoutDoc.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)')) });
funcs.push({ description: 'Drag Document', icon: 'edit', event: () => (this.layoutDoc.onDragStart = undefined) });
cm.addItem({ description: 'OnDrag...', noexpand: true, subitems: funcs, icon: 'asterisk' });
@@ -938,7 +779,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
const more = cm.findByDescription('More...');
const moreItems = more && 'subitems' in more ? more.subitems : [];
if (!Doc.IsSystem(this.rootDoc)) {
- (this.rootDoc._viewType !== CollectionViewType.Docking || !Doc.noviceMode) && moreItems.push({ description: 'Share', event: () => SharingManager.Instance.open(this.props.DocumentView()), icon: 'users' });
if (!Doc.noviceMode) {
moreItems.push({ description: 'Make View of Metadata Field', event: () => Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.DataDoc), icon: 'concierge-bell' });
moreItems.push({ description: `${this.Document._chromeHidden ? 'Show' : 'Hide'} Chrome`, event: () => (this.Document._chromeHidden = !this.Document._chromeHidden), icon: 'project-diagram' });
@@ -952,18 +792,26 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
}
}
- if (this.props.removeDocument && !Doc.IsSystem(this.rootDoc) && Doc.ActiveDashboard !== this.props.Document) {
+ !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._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)
- moreItems.push({ description: 'Close', event: this.deleteClicked, icon: 'times' });
+ constantItems.push({ description: 'Close', event: this.deleteClicked, icon: 'times' });
}
- !more && moreItems.length && cm.addItem({ description: 'More...', subitems: moreItems, icon: 'compass' });
}
+ cm.addItem({ description: 'General...', noexpand: !Doc.IsSystem(this.rootDoc), subitems: constantItems, icon: 'question' });
+
const help = cm.findByDescription('Help...');
const helpItems: ContextMenuProps[] = help && 'subitems' in help ? help.subitems : [];
- helpItems.push({ description: 'Show Metadata', event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), 'add:right'), icon: 'layer-group' });
- !Doc.noviceMode && helpItems.push({ description: 'Text Shortcuts Ctrl+/', event: () => this.props.addDocTab(Docs.Create.PdfDocument('/assets/cheat-sheet.pdf', { _width: 300, _height: 300 }), 'add:right'), icon: 'keyboard' });
+ !Doc.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;
@@ -998,44 +846,36 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
break;
}
// Add link to help documentation
- if (documentationDescription && documentationLink) {
- console.log('add documentation item');
+ if (!this.props.treeViewDoc && documentationDescription && documentationLink) {
helpItems.push({
description: documentationDescription,
- event: () => {
- window.open(documentationLink, '_blank');
- },
+ event: () => window.open(documentationLink, '_blank'),
icon: 'book',
});
}
- cm.addItem({ description: 'Help...', noexpand: true, subitems: helpItems, icon: 'question' });
+ if (!help) cm.addItem({ description: 'Help...', noexpand: !Doc.noviceMode, subitems: helpItems, icon: 'question' });
+ else cm.moveAfter(help);
- if (!this.topMost) e?.stopPropagation(); // DocumentViews should stop propagation of this event
+ e?.stopPropagation(); // DocumentViews should stop propagation of this event
cm.displayMenu((e?.pageX || pageX || 0) - 15, (e?.pageY || pageY || 0) - 15);
};
- collectionFilters = () => StrListCast(this.props.Document._docFilters);
- collectionRangeDocFilters = () => StrListCast(this.props.Document._docRangeFilters);
- @computed get showFilterIcon() {
- return this.collectionFilters().length || this.collectionRangeDocFilters().length
- ? 'hasFilter'
- : this.props.docFilters?.().filter(f => Utils.IsRecursiveFilter(f) && f !== Utils.noDragsDocFilter).length || this.props.docRangeFilters().length
- ? 'inheritsFilter'
- : undefined;
- }
rootSelected = (outsideReaction?: boolean) => this.props.isSelected(outsideReaction) || (this.props.Document.rootDocument && this.props.rootSelected?.(outsideReaction)) || false;
panelHeight = () => this.props.PanelHeight() - this.headerMargin;
screenToLocal = () => this.props.ScreenToLocalTransform().translate(0, -this.headerMargin);
- onClickFunc = () => this.onClickHandler;
+ onClickFunc: any = () => (this.disableClickScriptFunc ? undefined : this.onClickHandler);
setHeight = (height: number) => (this.layoutDoc._height = height);
- setContentView = action((view: { getAnchor?: () => Doc; forward?: () => boolean; back?: () => boolean }) => (this._componentView = view));
- isContentActive = (outsideReaction?: boolean) => {
- return this.props.isContentActive() === false
+ setContentView = action((view: { getAnchor?: (addAsAnnotation: boolean) => Doc; forward?: () => boolean; back?: () => boolean }) => (this._componentView = view));
+ isContentActive = (outsideReaction?: boolean): boolean | undefined => {
+ // true - if the document has been activated directly or indirectly (by having its children selected)
+ // false - if its pointer events are explicitly turned off or if it's container tells it that it's inactive
+ // undefined - it is not active, but it should be responsive to actions that might active it or its contents (eg clicking)
+ return this.props.isContentActive() === false || this.props.pointerEvents?.() === 'none' || (this.rootDoc.pointerEvents === 'none' && !StrCast(this.props.LayoutTemplateString).includes(KeyValueBox.name))
? false
: Doc.ActiveTool !== InkTool.None ||
SnappingManager.GetIsDragging() ||
this.rootSelected() ||
- this.props.Document.forceActive ||
+ this.rootDoc.forceActive ||
this.props.isSelected(outsideReaction) ||
this._componentView?.isAnyChildContentActive?.() ||
this.props.isContentActive()
@@ -1044,48 +884,32 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
};
@observable _retryThumb = 1;
thumbShown = () => {
- return !this.props.isSelected() &&
+ const childHighlighted = () =>
+ 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.isContentActive() &&
LightboxView.LightboxDoc !== this.rootDoc &&
this.thumb &&
!Doc.AreProtosEqual(DocumentLinksButton.StartLink, this.rootDoc) &&
- (!Doc.isBrushedHighlightedDegree(this.props.Document) || this.rootDoc._viewType === CollectionViewType.Docking) &&
- !this._componentView?.isAnyChildContentActive?.()
+ ((!childHighlighted() && !childOverlayed() && !Doc.isBrushedHighlightedDegree(this.rootDoc)) || this.rootDoc._type_collection === CollectionViewType.Docking)
? true
: false;
};
- get audioAnnoState() {
- return this.dataDoc.audioAnnoState ?? 'stopped';
- }
- @computed get audioAnnoView() {
- const audioAnnosCount = Cast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations'], listSpec(AudioField), null)?.length;
- const audioTextAnnos = Cast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations-text'], listSpec('string'), null);
- const audioIconColors = new Map<string, string>([
- ['recording', 'red'],
- ['playing', 'green'],
- ['stopped', audioAnnosCount ? 'blue' : 'gray'],
- ]);
- return this.props.renderDepth === -1 || SnappingManager.GetIsDragging() || (!this.props.isSelected() && !this.props.isHovering() && this.audioAnnoState !== 'recording') || (!audioAnnosCount && this.audioAnnoState === 'stopped') ? null : (
- <Tooltip title={<div>{audioTextAnnos?.lastElement()}</div>}>
- <div className="documentView-audioBackground" onPointerDown={this.playAnnotation}>
- <FontAwesomeIcon className="documentView-audioFont" style={{ color: audioIconColors.get(StrCast(this.audioAnnoState)) }} icon={!audioAnnosCount ? 'microphone' : 'file-audio'} size="sm" />
- </div>
- </Tooltip>
- );
- }
+ childFilters = () => [...this.props.childFilters(), ...StrListCast(this.layoutDoc.childFilters)];
+
+ /// disable pointer events on content when there's an enabled onClick script, or if contents are marked inactive
+ contentPointerEvents = () => ((!this.disableClickScriptFunc && this.onClickHandler) || this.isContentActive() === false ? 'none' : this.pointerEvents);
@computed get contents() {
TraceMobx();
+ const isInk = StrCast(this.layoutDoc.layout).includes(InkingStroke.name) && !this.props.LayoutTemplateString;
return (
<div
className="documentView-contentsView"
style={{
- pointerEvents:
- (this.props.pointerEvents?.() as any) ?? this.rootDoc.layoutKey === 'layout_icon'
- ? 'none'
- : (this.props.contentPointerEvents as any)
- ? (this.props.contentPointerEvents as any)
- : this.rootDoc.type !== DocumentType.INK && this.isContentActive()
- ? 'all'
- : 'none',
+ pointerEvents: (isInk ? 'none' : this.contentPointerEvents()) ?? 'all',
height: this.headerMargin ? `calc(100% - ${this.headerMargin}px)` : undefined,
}}>
{!this._retryThumb || !this.thumbShown() ? null : (
@@ -1096,20 +920,19 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
height={this.props.PanelHeight()}
onError={(e: any) => {
setTimeout(action(() => (this._retryThumb = 0)));
- setTimeout(
- action(() => (this._retryThumb = 1)),
- 150
- );
+ // prettier-ignore
+ setTimeout(action(() => (this._retryThumb = 1)), 150 );
}}
/>
)}
<DocumentContentsView
key={1}
{...this.props}
+ pointerEvents={this.contentPointerEvents}
docViewPath={this.props.viewPath}
thumbShown={this.thumbShown}
- isHovering={this.props.isHovering}
setContentView={this.setContentView}
+ childFilters={this.childFilters}
NativeDimScaling={this.props.NativeDimScaling}
PanelHeight={this.panelHeight}
setHeight={!this.props.suppressSetHeight ? this.setHeight : undefined}
@@ -1117,36 +940,28 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
ScreenToLocalTransform={this.screenToLocal}
rootSelected={this.rootSelected}
onClick={this.onClickFunc}
- focus={this.focus}
- layoutKey={this.finalLayoutKey}
+ focus={this.props.focus}
+ setTitleFocus={this.setTitleFocus}
+ layout_fieldKey={this.finalLayoutKey}
/>
- {this.layoutDoc.hideAllLinks ? null : this.allLinkEndpoints}
- {this.audioAnnoView}
+ {this.layoutDoc.layout_hideAllLinks ? null : this.allLinkEndpoints}
</div>
);
}
- get indicatorIcon() {
- if (this.props.Document['acl-Public'] !== SharingPermissions.None) return 'globe-americas';
- else if (this.props.Document.numGroupsShared || NumCast(this.props.Document.numUsersShared, 0) > 1) return 'users';
- else return 'user';
- }
-
- @undoBatch
- hideLinkAnchor = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && (doc.hidden = true), true);
anchorPanelWidth = () => this.props.PanelWidth() || 1;
anchorPanelHeight = () => this.props.PanelHeight() || 1;
anchorStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string): any => {
// prettier-ignore
- switch (property) {
+ switch (property.split(':')[0]) {
case StyleProp.ShowTitle: return '';
case StyleProp.PointerEvents: return 'none';
- case StyleProp.LinkSource: return this.props.Document; // pass the LinkSource to the LinkAnchorBox
+ 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.
@@ -1155,64 +970,46 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
TraceMobx();
return LinkManager.Instance.getAllRelatedLinks(this.rootDoc).filter(
link =>
- Doc.AreProtosEqual(link.anchor1 as Doc, this.rootDoc) ||
- Doc.AreProtosEqual(link.anchor2 as Doc, this.rootDoc) ||
- ((link.anchor1 as Doc).unrendered && Doc.AreProtosEqual((link.anchor1 as Doc).annotationOn as Doc, this.rootDoc)) ||
- ((link.anchor2 as Doc).unrendered && Doc.AreProtosEqual((link.anchor2 as Doc).annotationOn as Doc, this.rootDoc))
+ 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)?.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.link_displayLine = false));
@computed get allLinkEndpoints() {
// the small blue dots that mark the endpoints of links
TraceMobx();
- if (this.layoutDoc.unrendered || this.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) return null;
- if (this.rootDoc.type === DocumentType.PRES || this.rootDoc.type === DocumentType.LINK || this.props.dontRegisterView) return null;
- const filtered = DocUtils.FilterDocs(this.directLinks, this.props.docFilters?.() ?? [], []).filter(d => !d.hidden);
- return filtered.map((link, i) => (
+ 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
{...this.props}
isContentActive={returnFalse}
Document={link}
+ docViewPath={this.props.viewPath}
PanelWidth={this.anchorPanelWidth}
PanelHeight={this.anchorPanelHeight}
dontRegisterView={false}
- showTitle={returnEmptyString}
+ layout_showTitle={returnEmptyString}
hideCaptions={true}
- fitWidth={returnTrue}
+ hideLinkAnchors={true}
+ layout_fitWidth={returnTrue}
+ removeDocument={this.hideLink(link)}
styleProvider={this.anchorStyleProvider}
- removeDocument={this.hideLinkAnchor}
LayoutTemplate={undefined}
- LayoutTemplateString={LinkAnchorBox.LayoutString(`anchor${Doc.LinkEndpoint(link, this.rootDoc)}`)}
+ LayoutTemplateString={LinkAnchorBox.LayoutString(`link_anchor_${Doc.LinkEndpoint(link, this.rootDoc)}`)}
/>
</div>
));
}
- @action
- playAnnotation = () => {
- const self = this;
- const audioAnnos = Cast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations'], listSpec(AudioField), null);
- const anno = audioAnnos.lastElement();
- if (anno instanceof AudioField && this.audioAnnoState === 'stopped') {
- new Howl({
- src: [anno.url.href],
- format: ['mp3'],
- autoplay: true,
- loop: false,
- volume: 0.5,
- onend: function () {
- runInAction(() => (self.dataDoc.audioAnnoState = 'stopped'));
- },
- });
- this.dataDoc.audioAnnoState = 'playing';
- }
- };
-
- static recordAudioAnnotation(dataDoc: Doc, field: string, onEnd?: () => void) {
+ static recordAudioAnnotation(dataDoc: Doc, field: string, onRecording?: (stop: () => void) => void, onEnd?: () => void) {
let gumStream: any;
let recorder: any;
navigator.mediaDevices
@@ -1236,7 +1033,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);
@@ -1249,65 +1046,84 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
};
runInAction(() => (dataDoc.audioAnnoState = 'recording'));
recorder.start();
- setTimeout(() => {
+ const stopFunc = () => {
recorder.stop();
DictationManager.Controls.stop(false);
runInAction(() => (dataDoc.audioAnnoState = 'stopped'));
gumStream.getAudioTracks()[0].stop();
- }, 5000);
+ };
+ if (onRecording) onRecording(stopFunc);
+ else setTimeout(stopFunc, 5000);
});
}
+ playAnnotation = () => {
+ const self = this;
+ const audioAnnoState = this.dataDoc.audioAnnoState ?? 'stopped';
+ const audioAnnos = Cast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations'], listSpec(AudioField), null);
+ const anno = audioAnnos?.lastElement();
+ if (anno instanceof AudioField && audioAnnoState === 'stopped') {
+ new Howl({
+ src: [anno.url.href],
+ format: ['mp3'],
+ autoplay: true,
+ loop: false,
+ volume: 0.5,
+ onend: action(() => (self.dataDoc.audioAnnoState = 'stopped')),
+ });
+ this.dataDoc.audioAnnoState = 'playing';
+ }
+ };
- captionStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewInternalProps>, property: string) => this.props?.styleProvider?.(doc, props, property + ':caption');
+ captionStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string) => this.props?.styleProvider?.(doc, props, property + ':caption');
@computed get innards() {
TraceMobx();
const ffscale = () => this.props.DocumentView().props.CollectionFreeFormDocumentView?.().props.ScreenToLocalTransform().Scale || 1;
- const showTitle = this.ShowTitle?.split(':')[0];
- const showTitleHover = this.ShowTitle?.includes(':hover');
- const showCaption = !this.props.hideCaptions && this.Document._viewType !== CollectionViewType.Carousel ? StrCast(this.layoutDoc._showCaption) : undefined;
- const captionView = !showCaption ? null : (
+ const layout_showTitle = this.layout_showTitle?.split(':')[0];
+ const layout_showTitleHover = this.layout_showTitle?.includes(':hover');
+ const captionView = !this.layout_showCaption ? null : (
<div
className="documentView-captionWrapper"
style={{
- pointerEvents: this.onClickHandler || this.Document.ignoreClick ? 'none' : this.isContentActive() || this.props.isDocumentActive?.() ? 'all' : undefined,
+ pointerEvents: this.Document.ignoreClick ? 'none' : this.isContentActive() || this.props.isDocumentActive?.() ? 'all' : undefined,
minWidth: 50 * ffscale(),
maxHeight: `max(100%, ${20 * ffscale()}px)`,
}}>
<FormattedTextBox
- {...OmitKeys(this.props, ['children']).omit}
+ {...this.props}
yPadding={10}
xPadding={10}
- fieldKey={showCaption}
+ fieldKey={this.layout_showCaption}
fontSize={12 * Math.max(1, (2 * ffscale()) / 3)}
styleProvider={this.captionStyleProvider}
dontRegisterView={true}
noSidebar={true}
dontScale={true}
+ renderDepth={this.props.renderDepth}
isContentActive={this.isContentActive}
- onClick={this.onClickFunc}
/>
</div>
);
- const targetDoc = showTitle?.startsWith('_') ? this.layoutDoc : this.rootDoc;
+ const targetDoc = layout_showTitle?.startsWith('_') ? this.layoutDoc : this.rootDoc;
const background = StrCast(
- SharingManager.Instance.users.find(users => users.user.email === this.dataDoc.author)?.sharingDoc.userColor,
- Doc.UserDoc().showTitle && [DocumentType.RTF, DocumentType.COL].includes(this.rootDoc.type as any) ? StrCast(Doc.SharingDoc().userColor) : 'rgba(0,0,0,0.4)'
+ SharingManager.Instance.users.find(u => u.user.email === this.dataDoc.author)?.sharingDoc.userColor,
+ Doc.UserDoc().layout_showTitle && [DocumentType.RTF, DocumentType.COL].includes(this.rootDoc.type as any) ? StrCast(Doc.SharingDoc().userColor) : 'rgba(0,0,0,0.4)'
);
- const titleView = !showTitle ? null : (
+ const layout_sidebarWidthPercent = +StrCast(this.layoutDoc.layout_sidebarWidthPercent).replace('%', '');
+ const titleView = !layout_showTitle ? null : (
<div
- className={`documentView-titleWrapper${showTitleHover ? '-hover' : ''}`}
+ className={`documentView-titleWrapper${layout_showTitleHover ? '-hover' : ''}`}
key="title"
style={{
position: this.headerMargin ? 'relative' : 'absolute',
height: this.titleHeight,
- width: !this.headerMargin ? `calc(100% - 18px)` : '100%', // leave room for annotation button
+ width: !this.headerMargin ? `calc(${layout_sidebarWidthPercent || 100}% - 18px)` : (layout_sidebarWidthPercent || 100) + '%', // leave room for annotation button
color: lightOrDark(background),
background,
- pointerEvents: this.onClickHandler || this.Document.ignoreClick ? 'none' : this.isContentActive() || this.props.isDocumentActive?.() ? 'all' : undefined,
+ pointerEvents: (!this.disableClickScriptFunc && this.onClickHandler) || this.Document.ignoreClick ? 'none' : this.isContentActive() || this.props.isDocumentActive?.() ? 'all' : undefined,
}}>
<EditableView
ref={this._titleRef}
- contents={showTitle
+ contents={layout_showTitle
.split(';')
.map(field => field.trim())
.map(field => targetDoc[field]?.toString())
@@ -1316,27 +1132,27 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
fontSize={10}
GetValue={() => {
this.props.select(false);
- return showTitle.split(';').length === 1 ? showTitle + '=' + Field.toString(targetDoc[showTitle.split(';')[0]] as any as Field) : '#' + showTitle;
+ return layout_showTitle.split(';').length === 1 ? layout_showTitle + '=' + Field.toString(targetDoc[layout_showTitle.split(';')[0]] as any as Field) : '#' + layout_showTitle;
}}
SetValue={undoBatch((input: string) => {
if (input?.startsWith('#')) {
- if (this.props.showTitle) {
- this.rootDoc._showTitle = input?.substring(1) ? input.substring(1) : undefined;
+ if (this.props.layout_showTitle) {
+ this.rootDoc._layout_showTitle = input?.substring(1) ? input.substring(1) : undefined;
} else {
- Doc.UserDoc().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(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);
+ 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);
}
return true;
})}
/>
</div>
);
- return this.props.hideTitle || (!showTitle && !showCaption) ? (
+ return this.props.hideTitle || (!layout_showTitle && !this.layout_showCaption) ? (
this.contents
) : (
<div className="documentView-styleWrapper">
@@ -1348,94 +1164,97 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
</div>
);
}
- @observable _: string = '';
+
renderDoc = (style: object) => {
TraceMobx();
- const thumb = ImageCast(this.layoutDoc['thumb-frozen'], ImageCast(this.layoutDoc.thumb))?.url?.href.replace('.png', '_m.png');
- const isButton = this.props.Document.type === DocumentType.FONTICON;
- if (!(this.props.Document instanceof Doc) || GetEffectiveAcl(this.props.Document[DataSym]) === AclPrivate || this.hidden) return null;
- return (
- this.docContents ?? (
- <div
- className={`documentView-node${this.topMost ? '-topmost' : ''}`}
- id={this.props.Document[Id]}
- style={{
- ...style,
- background: isButton || thumb ? undefined : this.backgroundColor,
- 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),
- transform: this._animateScalingTo ? `scale(${this._animateScalingTo})` : undefined,
- transition: !this._animateScalingTo ? StrCast(this.Document.dataTransition) : `transform ${this._animateScaleTime / 1000}s ease-${this._animateScalingTo < 1 ? 'in' : 'out'}`,
- }}>
- {this.innards}
- {this.onClickHandler && this.props.ContainingCollectionView?.props.Document._viewType === CollectionViewType.Time ? <div className="documentView-contentBlocker" /> : null}
- {this.widgetDecorations ?? null}
- </div>
- )
- );
+ return !DocCast(this.Document) || GetEffectiveAcl(this.Document[DocData]) === AclPrivate
+ ? null
+ : this.docContents ?? (
+ <div
+ className="documentView-node"
+ id={this.Document[Id]}
+ style={{
+ ...style,
+ background: this.backgroundBoxColor,
+ opacity: this.opacity,
+ cursor: Doc.ActiveTool === InkTool.None ? 'grab' : 'crosshair',
+ color: StrCast(this.layoutDoc.color, 'inherit'),
+ 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'}`,
+ }}>
+ {this.innards}
+ {this.widgetDecorations ?? null}
+ </div>
+ );
};
+
+ /**
+ * returns an entrance animation effect function to wrap a JSX element
+ * @param presEffectDoc presentation effects document that specifies the animation effect parameters
+ * @returns a function that will wrap a JSX animation element wrapping any JSX element
+ */
+ public static AnimationEffect(renderDoc: JSX.Element, presEffectDoc: Opt<Doc>, root: Doc) {
+ const dir = presEffectDoc?.presEffectDirection ?? presEffectDoc?.followLinkAnimDirection;
+ const effectProps = {
+ left: dir === PresEffectDirection.Left,
+ right: dir === PresEffectDirection.Right,
+ top: dir === PresEffectDirection.Top,
+ bottom: dir === PresEffectDirection.Bottom,
+ opposite: true,
+ delay: 0,
+ duration: Cast(presEffectDoc?.presTransition, 'number', Cast(presEffectDoc?.followLinkTransitionTime, 'number', null)),
+ };
+ //prettier-ignore
+ switch (StrCast(presEffectDoc?.presEffect, StrCast(presEffectDoc?.followLinkAnimEffect))) {
+ default:
+ case PresEffect.None: return renderDoc;
+ case PresEffect.Zoom: return <Zoom {...effectProps}>{renderDoc}</Zoom>;
+ case PresEffect.Fade: return <Fade {...effectProps}>{renderDoc}</Fade>;
+ case PresEffect.Flip: return <Flip {...effectProps}>{renderDoc}</Flip>;
+ case PresEffect.Rotate: return <Rotate {...effectProps}>{renderDoc}</Rotate>;
+ case PresEffect.Bounce: return <Bounce {...effectProps}>{renderDoc}</Bounce>;
+ case PresEffect.Roll: return <Roll {...effectProps}>{renderDoc}</Roll>;
+ case PresEffect.Lightspeed: return <LightSpeed {...effectProps}>{renderDoc}</LightSpeed>;
+ }
+ }
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) || { path: undefined };
+ const borderPath = this.props.styleProvider?.(this.props.Document, this.props, StyleProp.BorderPath);
const boxShadow =
this.props.treeViewDoc || !highlighting
- ? null
+ ? 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);
const renderDoc = this.renderDoc({
borderRadius: this.borderRounding,
- outline: highlighting && !this.borderRounding && highlighting ? `${highlighting.highlightColor} ${highlighting.highlightStyle} ${highlighting.highlightIndex}px` : 'solid 0px',
- border: highlighting && this.borderRounding && highlighting && highlighting.highlightStyle === 'dashed' ? `${highlighting.highlightStyle} ${highlighting.highlightColor} ${highlighting.highlightIndex}px` : undefined,
+ outline: highlighting && !this.borderRounding && !highlighting.highlightStroke ? `${highlighting.highlightColor} ${highlighting.highlightStyle} ${highlighting.highlightIndex}px` : 'solid 0px',
+ border: highlighting && this.borderRounding && highlighting.highlightStyle === 'dashed' ? `${highlighting.highlightStyle} ${highlighting.highlightColor} ${highlighting.highlightIndex}px` : undefined,
boxShadow,
- clipPath: borderPath.path ? `path('${borderPath.path}')` : undefined,
+ clipPath: borderPath?.clipPath,
});
- const animRenderDoc = PresBox.Instance?.isActiveItemTarget(this.layoutDoc) ? PresBox.AnimationEffect(renderDoc, PresBox.Instance.activeItem) : renderDoc;
return (
<div
className={`${DocumentView.ROOT_DIV} docView-hack`}
ref={this._mainCont}
onContextMenu={this.onContextMenu}
- onKeyDown={this.onKeyDown}
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)}
style={{
- display: this.hidden ? 'inline' : undefined,
borderRadius: this.borderRounding,
- pointerEvents: this.pointerEvents,
+ pointerEvents: this.pointerEvents === 'visiblePainted' ? 'none' : this.pointerEvents,
}}>
- {!borderPath.path ? (
- animRenderDoc
- ) : (
- <>
- {animRenderDoc}
- <div key="border2" className="documentView-customBorder" style={{ pointerEvents: 'none' }}>
- <svg style={{ overflow: 'visible', height: '100%' }} viewBox={`0 0 ${this.props.PanelWidth()} ${this.props.PanelHeight()}`}>
- <path d={borderPath.path} style={{ stroke: 'black', fill: 'transparent', strokeWidth: borderPath.width }} />
- </svg>
- </div>
- </>
- )}
- {this.showFilterIcon ? (
- <FontAwesomeIcon
- icon={'filter'}
- size="lg"
- style={{ position: 'absolute', top: '1%', right: '1%', cursor: 'pointer', padding: 1, color: this.showFilterIcon === 'hasFilter' ? '#18c718bd' : 'orange', zIndex: 1 }}
- onPointerDown={action(e => {
- this.props.select(false);
- SettingsManager.propertiesWidth = 250;
- e.stopPropagation();
- })}
- />
- ) : null}
+ <>
+ {DocumentViewInternal.AnimationEffect(renderDoc, this.rootDoc[Animation], this.rootDoc)}
+ {borderPath?.jsx}
+ </>
</div>
);
}
@@ -1444,40 +1263,70 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
@observer
export class DocumentView extends React.Component<DocumentViewProps> {
public static ROOT_DIV = 'documentView-effectsWrapper';
+ @observable public static LongPress = false;
+ @observable public docView: DocumentViewInternal | undefined | null;
+ @observable public textHtmlOverlay: Opt<string>;
+ @observable private _isHovering = false;
+
+ public htmlOverlayEffect = '';
public get displayName() {
return 'DocumentView(' + this.props.Document?.title + ')';
} // this makes mobx trace() statements more descriptive
public ContentRef = React.createRef<HTMLDivElement>();
+ public ViewTimer: NodeJS.Timeout | undefined; // timer for res
+ public AnimEffectTimer: NodeJS.Timeout | undefined; // timer for res
private _disposers: { [name: string]: IReactionDisposer } = {};
+ public clearViewTransition = () => {
+ this.ViewTimer && clearTimeout(this.ViewTimer);
+ this.rootDoc._viewTransition = undefined;
+ };
+ public startDragging = (x: number, y: number, dropAction: dropActionType, hideSource = false) => this.docView?.startDragging(x, y, dropAction, hideSource);
- public static showBackLinks(linkSource: Doc) {
- const docid = Doc.CurrentUserEmail + Doc.GetProto(linkSource)[Id] + '-pivotish';
- DocServer.GetRefField(docid).then(docx => {
- const rootAlias = () => {
- const rootAlias = Doc.MakeAlias(linkSource);
- rootAlias.x = rootAlias.y = 0;
- return rootAlias;
- };
- const linkCollection =
- (docx instanceof Doc && docx) ||
- Docs.Create.StackingDocument(
- [
- /*rootAlias()*/
- ],
- { title: linkSource.title + '-pivot', _width: 500, _height: 500 },
- docid
- );
- linkCollection.linkSource = linkSource;
- if (!linkCollection.reactionScript) linkCollection.reactionScript = ScriptField.MakeScript('updateLinkCollection(self)');
- LightboxView.SetLightboxDoc(linkCollection);
+ public showContextMenu = (pageX: number, pageY: number) => this.docView?.onContextMenu(undefined, pageX, pageY);
+
+ public setAnimEffect = (presEffect: Doc, timeInMs: number, afterTrans?: () => void) => {
+ this.AnimEffectTimer && clearTimeout(this.AnimEffectTimer);
+ 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`;
+ if (dataTrans) this.rootDoc._dataTransition = `${transProp} ${timeInMs}ms`;
+ this.ViewTimer && clearTimeout(this.ViewTimer);
+ return (this.ViewTimer = setTimeout(() => {
+ this.rootDoc._viewTransition = undefined;
+ this.rootDoc._dataTransition = 'inherit';
+ afterTrans?.();
+ }, timeInMs + 10));
+ };
+ public static SetViewTransition(docs: Doc[], transProp: string, timeInMs: number, afterTrans?: () => void, dataTrans = false) {
+ docs.forEach(doc => {
+ doc._viewTransition = `${transProp} ${timeInMs}ms`;
+ dataTrans && (doc.dataTransition = `${transProp} ${timeInMs}ms`);
});
+ return setTimeout(
+ () =>
+ docs.forEach(doc => {
+ doc._viewTransition = undefined;
+ dataTrans && (doc.dataTransition = 'inherit');
+ afterTrans?.();
+ }),
+ timeInMs + 10
+ );
}
- @observable public docView: DocumentViewInternal | undefined | null;
-
- showContextMenu(pageX: number, pageY: number) {
- return this.docView?.onContextMenu(undefined, pageX, pageY);
+ // shows a stacking view collection (by default, but the user can change) of all documents linked to the source
+ public static showBackLinks(linkAnchor: Doc) {
+ const docId = Doc.CurrentUserEmail + Doc.GetProto(linkAnchor)[Id] + '-pivotish';
+ // prettier-ignore
+ DocServer.GetRefField(docId).then(docx =>
+ LightboxView.SetLightboxDoc(
+ (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)
+ )
+ );
}
+
get Document() {
return this.props.Document;
}
@@ -1485,13 +1334,10 @@ export class DocumentView extends React.Component<DocumentViewProps> {
return this.props.renderDepth === 0;
}
get rootDoc() {
- return this.docView?.rootDoc || this.Document;
+ return this.docView?.rootDoc ?? this.Document;
}
get dataDoc() {
- return this.docView?.dataDoc || this.Document;
- }
- get finalLayoutKey() {
- return this.docView?.finalLayoutKey || 'layout';
+ return this.docView?.dataDoc ?? this.Document;
}
get ContentDiv() {
return this.docView?.ContentDiv;
@@ -1505,22 +1351,19 @@ export class DocumentView extends React.Component<DocumentViewProps> {
get LayoutFieldKey() {
return this.docView?.LayoutFieldKey || 'layout';
}
- get fitWidth() {
- return this.props.fitWidth?.(this.rootDoc) || this.layoutDoc.fitWidth;
+ @computed get layout_fitWidth() {
+ return this.docView?._componentView?.layout_fitWidth?.() ?? this.props.layout_fitWidth?.(this.rootDoc) ?? this.layoutDoc?.layout_fitWidth;
+ }
+ @computed get anchorViewDoc() {
+ 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' : ''));
}
- linkButtonInverseScaling = () => (this.props.NativeDimScaling?.() || 1) * this.screenToLocalTransform().Scale;
-
@computed get linkCountView() {
- return (this.props.renderDepth === -1 || SnappingManager.GetIsDragging() || (this.isSelected() && this.props.renderDepth) || !this._isHovering || this.hideLinkButton) &&
- DocumentLinksButton.LinkEditorDocView?.rootDoc !== this.rootDoc ? null : (
- <DocumentLinksButton View={this} scaling={this.linkButtonInverseScaling} OnHover={true} Bottom={this.topMost} ShowCount={true} />
- );
+ const hideCount = this.props.renderDepth === -1 || SnappingManager.GetIsDragging() || (this.isSelected() && this.props.renderDepth) || !this._isHovering || this.hideLinkButton;
+ return hideCount ? null : <DocumentLinksButton View={this} scaling={this.scaleToScreenSpace} OnHover={true} Bottom={this.topMost} ShowCount={true} />;
}
-
@computed get docViewPath(): DocumentView[] {
return this.props.docViewPath ? [...this.props.docViewPath(), this] : [this];
}
@@ -1528,13 +1371,13 @@ export class DocumentView extends React.Component<DocumentViewProps> {
return Doc.Layout(this.Document, this.props.LayoutTemplate?.());
}
@computed get nativeWidth() {
- return this.docView?._componentView?.reverseNativeScaling?.() ? 0 : returnVal(this.props.NativeWidth?.(), Doc.NativeWidth(this.layoutDoc, this.props.DataDoc, !this.fitWidth));
+ return this.docView?._componentView?.reverseNativeScaling?.() ? 0 : returnVal(this.props.NativeWidth?.(), Doc.NativeWidth(this.layoutDoc, this.props.DataDoc, !this.layout_fitWidth));
}
@computed get nativeHeight() {
- return this.docView?._componentView?.reverseNativeScaling?.() ? 0 : returnVal(this.props.NativeHeight?.(), Doc.NativeHeight(this.layoutDoc, this.props.DataDoc, !this.fitWidth));
+ 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.fitWidth && !this.nativeWidth) || this.props.dontScaleFilter?.(this.Document) || [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);
@@ -1545,27 +1388,28 @@ export class DocumentView extends React.Component<DocumentViewProps> {
@computed get nativeScaling() {
if (this.shouldNotScale) return 1;
const minTextScale = this.Document.type === DocumentType.RTF ? 0.1 : 0;
- if (this.fitWidth || this.props.PanelHeight() / (this.effectiveNativeHeight || 1) > this.props.PanelWidth() / (this.effectiveNativeWidth || 1)) {
- return Math.max(minTextScale, this.props.PanelWidth() / (this.effectiveNativeWidth || 1)); // width-limited or fitWidth
+ if (this.layout_fitWidth || this.props.PanelHeight() / (this.effectiveNativeHeight || 1) > this.props.PanelWidth() / (this.effectiveNativeWidth || 1)) {
+ return Math.max(minTextScale, this.props.PanelWidth() / (this.effectiveNativeWidth || 1)); // width-limited or layout_fitWidth
}
return Math.max(minTextScale, this.props.PanelHeight() / (this.effectiveNativeHeight || 1)); // height-limited or unscaled
}
-
@computed get panelWidth() {
return this.effectiveNativeWidth ? this.effectiveNativeWidth * this.nativeScaling : this.props.PanelWidth();
}
@computed get panelHeight() {
- if (this.effectiveNativeHeight && !this.layoutDoc.nativeHeightUnfrozen) {
- const scrollHeight = this.fitWidth ? Math.max(this.ComponentView?.getScrollHeight?.() ?? NumCast(this.layoutDoc.scrollHeight)) : 0;
- return Math.min(this.props.PanelHeight(), Math.max(scrollHeight, this.effectiveNativeHeight) * this.nativeScaling);
+ if (this.effectiveNativeHeight && (!this.layout_fitWidth || !this.layoutDoc.nativeHeightUnfrozen)) {
+ return Math.min(this.props.PanelHeight(), this.effectiveNativeHeight * this.nativeScaling);
}
return this.props.PanelHeight();
}
@computed get Xshift() {
- return this.effectiveNativeWidth ? (this.props.PanelWidth() - this.effectiveNativeWidth * this.nativeScaling) / 2 : 0;
+ return this.effectiveNativeWidth ? Math.max(0, (this.props.PanelWidth() - this.effectiveNativeWidth * this.nativeScaling) / 2) : 0;
}
@computed get Yshift() {
- return this.effectiveNativeWidth && this.effectiveNativeHeight && Math.abs(this.Xshift) < 0.001 && (!this.layoutDoc.nativeHeightUnfrozen || (!this.fitWidth && this.effectiveNativeHeight * this.nativeScaling <= this.props.PanelHeight()))
+ return this.effectiveNativeWidth &&
+ this.effectiveNativeHeight &&
+ Math.abs(this.Xshift) < 0.001 &&
+ (!this.layoutDoc.nativeHeightUnfrozen || (!this.layout_fitWidth && this.effectiveNativeHeight * this.nativeScaling <= this.props.PanelHeight()))
? Math.max(0, (this.props.PanelHeight() - this.effectiveNativeHeight * this.nativeScaling) / 2)
: 0;
}
@@ -1576,13 +1420,15 @@ export class DocumentView extends React.Component<DocumentViewProps> {
return this.props.dontCenter?.includes('y') ? 0 : this.Yshift;
}
- toggleNativeDimensions = () => this.docView && Doc.toggleNativeDimensions(this.layoutDoc, this.docView.NativeDimScaling, this.props.PanelWidth(), this.props.PanelHeight());
- focus = (doc: Doc, options: DocFocusOptions) => this.docView?.focus(doc, options);
- getBounds = () => {
- if (!this.docView || !this.docView.ContentDiv || this.props.Document.presBox || this.props.treeViewDoc || Doc.AreProtosEqual(this.props.Document, Doc.UserDoc())) {
+ public toggleNativeDimensions = () => this.docView && Doc.toggleNativeDimensions(this.layoutDoc, this.docView.NativeDimScaling, this.props.PanelWidth(), this.props.PanelHeight());
+ public getBounds = () => {
+ if (!this.docView?.ContentDiv || this.props.treeViewDoc || Doc.AreProtosEqual(this.props.Document, Doc.UserDoc())) {
return undefined;
}
- const xf = this.docView?.props.ScreenToLocalTransform().scale(this.nativeScaling).inverse();
+ const xf = this.docView.props
+ .ScreenToLocalTransform()
+ .scale(this.trueNativeWidth() ? this.nativeScaling : 1)
+ .inverse();
const [[left, top], [right, bottom]] = [xf.transformPoint(0, 0), xf.transformPoint(this.panelWidth, this.panelHeight)];
if (this.docView.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) {
const docuBox = this.docView.ContentDiv.getElementsByClassName('linkAnchorBox-cont');
@@ -1591,15 +1437,21 @@ export class DocumentView extends React.Component<DocumentViewProps> {
return { left, top, right, bottom, center: this.ComponentView?.getCenter?.(xf) };
};
- public iconify(finished?: () => void) {
+ public iconify(finished?: () => void, animateTime?: number) {
this.ComponentView?.updateIcon?.();
- const layoutKey = Cast(this.Document.layoutKey, 'string', null);
- if (layoutKey !== 'layout_icon') {
- this.switchViews(true, 'icon', finished);
- if (layoutKey && layoutKey !== 'layout' && layoutKey !== 'layout_icon') this.Document.deiconifyLayout = layoutKey.replace('layout_', '');
+ const animTime = this.docView?._animateScaleTime;
+ runInAction(() => this.docView && animateTime !== undefined && (this.docView._animateScaleTime = animateTime));
+ const finalFinished = action(() => {
+ finished?.();
+ this.docView && (this.docView._animateScaleTime = animTime);
+ });
+ const layout_fieldKey = Cast(this.Document.layout_fieldKey, 'string', null);
+ if (layout_fieldKey !== 'layout_icon') {
+ this.switchViews(true, 'icon', finalFinished);
+ if (layout_fieldKey && layout_fieldKey !== 'layout' && layout_fieldKey !== 'layout_icon') this.Document.deiconifyLayout = layout_fieldKey.replace('layout_', '');
} else {
const deiconifyLayout = Cast(this.Document.deiconifyLayout, 'string', null);
- this.switchViews(deiconifyLayout ? true : false, deiconifyLayout, finished);
+ this.switchViews(deiconifyLayout ? true : false, deiconifyLayout, finalFinished);
this.Document.deiconifyLayout = undefined;
this.props.bringToFront(this.rootDoc);
}
@@ -1610,12 +1462,13 @@ export class DocumentView extends React.Component<DocumentViewProps> {
Doc.setNativeView(this.props.Document);
custom && DocUtils.makeCustomViewClicked(this.props.Document, Docs.Create.StackingDocument, layout, undefined);
};
- switchViews = action((custom: boolean, view: string, finished?: () => void, useExistingLayout = false) => {
+ @action
+ switchViews = (custom: boolean, view: string, finished?: () => void, useExistingLayout = false) => {
this.docView && (this.docView._animateScalingTo = 0.1); // shrink doc
setTimeout(
action(() => {
if (useExistingLayout && custom && this.rootDoc['layout_' + view]) {
- this.rootDoc.layoutKey = 'layout_' + view;
+ this.rootDoc.layout_fieldKey = 'layout_' + view;
} else {
this.setCustomView(custom, view);
}
@@ -1625,39 +1478,40 @@ export class DocumentView extends React.Component<DocumentViewProps> {
this.docView && (this.docView._animateScalingTo = 0);
finished?.();
}),
- this.docView!._animateScaleTime - 10
+ this.docView ? Math.max(0, this.docView.animateScaleTime - 10) : 0
);
}),
- this.docView!._animateScaleTime - 10
+ this.docView ? Math.max(0, this.docView?.animateScaleTime - 10) : 0
);
- });
-
- startDragging = (x: number, y: number, dropAction: dropActionType, hideSource = false) => this.docView?.startDragging(x, y, dropAction, hideSource);
+ };
+ scaleToScreenSpace = () => (1 / (this.props.NativeDimScaling?.() || 1)) * this.screenToLocalTransform().Scale;
docViewPathFunc = () => this.docViewPath;
isSelected = (outsideReaction?: boolean) => SelectionManager.IsSelected(this, outsideReaction);
- select = (extendSelection: boolean) => SelectionManager.SelectView(this, !SelectionManager.Views().some(v => v.props.Document === this.props.ContainingCollectionDoc) && extendSelection);
+ select = (extendSelection: boolean) => SelectionManager.SelectView(this, extendSelection);
NativeWidth = () => this.effectiveNativeWidth;
NativeHeight = () => this.effectiveNativeHeight;
PanelWidth = () => this.panelWidth;
PanelHeight = () => this.panelHeight;
NativeDimScaling = () => this.nativeScaling;
selfView = () => this;
+ trueNativeWidth = () => returnVal(this.props.NativeWidth?.(), Doc.NativeWidth(this.layoutDoc, this.props.DataDoc, false));
screenToLocalTransform = () =>
this.props
.ScreenToLocalTransform()
.translate(-this.centeringX, -this.centeringY)
- .scale(1 / this.nativeScaling);
+ .scale(this.trueNativeWidth() ? 1 / this.nativeScaling : 1);
componentDidMount() {
- this._disposers.reactionScript = reaction(
- () => ScriptCast(this.rootDoc.reactionScript)?.script?.run({ this: this.props.Document, self: Cast(this.rootDoc, Doc, null) || this.props.Document }).result,
+ this._disposers.updateContentsScript = reaction(
+ () => ScriptCast(this.rootDoc.updateContentsScript)?.script?.run({ this: this.props.Document, self: Cast(this.rootDoc, Doc, null) || this.props.Document }).result,
output => output
);
this._disposers.height = reaction(
+ // 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);
@@ -1666,41 +1520,42 @@ export class DocumentView extends React.Component<DocumentViewProps> {
Object.values(this._disposers).forEach(disposer => disposer?.());
!BoolCast(this.props.Document.dontRegisterView, this.props.dontRegisterView) && DocumentManager.Instance.RemoveView(this);
}
- _hoverTimeout: any = undefined;
- isHovering = () => this._isHovering;
- @observable _isHovering = false;
+ @computed get htmlOverlay() {
+ return !this.textHtmlOverlay ? null : (
+ <div className="documentView-htmlOverlay">
+ <div className="documentView-htmlOverlayInner">
+ <Fade delay={0} duration={500}>
+ {DocumentViewInternal.AnimationEffect(
+ <div className="webBox-textHighlight">
+ <ObserverJsxParser autoCloseVoidElements={true} key={42} onError={(e: any) => console.log('PARSE error', e)} renderInWrapper={false} jsx={StrCast(this.textHtmlOverlay)} />
+ </div>,
+ { presEffect: this.htmlOverlayEffect ?? 'Zoom' } as any as Doc,
+ this.rootDoc
+ )}{' '}
+ </Fade>
+ </div>
+ </div>
+ );
+ }
render() {
TraceMobx();
const xshift = Math.abs(this.Xshift) <= 0.001 ? this.props.PanelWidth() : undefined;
const yshift = Math.abs(this.Yshift) <= 0.001 ? this.props.PanelHeight() : undefined;
- const isButton = this.props.Document.type === DocumentType.FONTICON || this.props.Document._viewType === CollectionViewType.Linear;
+
return (
- <div
- className="contentFittingDocumentView"
- onPointerEnter={action(() => {
- clearTimeout(this._hoverTimeout);
- this._isHovering = true;
- })}
- onPointerLeave={action(() => {
- clearTimeout(this._hoverTimeout);
- this._hoverTimeout = setTimeout(
- action(() => (this._isHovering = false)),
- 500
- );
- })}>
+ <div className="contentFittingDocumentView" onPointerEnter={action(() => (this._isHovering = true))} onPointerLeave={action(() => (this._isHovering = false))}>
{!this.props.Document || !this.props.PanelWidth() ? null : (
<div
className="contentFittingDocumentView-previewDoc"
ref={this.ContentRef}
style={{
transition: this.props.dataTransition,
- transform: isButton ? undefined : `translate(${this.centeringX}px, ${this.centeringY}px)`,
- width: isButton ? '100%' : xshift ?? `${(100 * (this.props.PanelWidth() - this.Xshift * 2)) / this.props.PanelWidth()}%`,
- height:
- isButton || this.props.forceAutoHeight
- ? undefined
- : yshift ?? (this.fitWidth ? `${this.panelHeight}px` : `${(((100 * this.effectiveNativeHeight) / this.effectiveNativeWidth) * this.props.PanelWidth()) / this.props.PanelHeight()}%`),
+ transform: `translate(${this.centeringX}px, ${this.centeringY}px)`,
+ width: xshift ?? `${(100 * (this.props.PanelWidth() - this.Xshift * 2)) / this.props.PanelWidth()}%`,
+ height: this.props.forceAutoHeight
+ ? undefined
+ : yshift ?? (this.layout_fitWidth ? `${this.panelHeight}px` : `${(((100 * this.effectiveNativeHeight) / this.effectiveNativeWidth) * this.props.PanelWidth()) / this.props.PanelHeight()}%`),
}}>
<DocumentViewInternal
{...this.props}
@@ -1713,11 +1568,11 @@ export class DocumentView extends React.Component<DocumentViewProps> {
NativeDimScaling={this.NativeDimScaling}
isSelected={this.isSelected}
select={this.select}
- isHovering={this.isHovering}
ScreenToLocalTransform={this.screenToLocalTransform}
focus={this.props.focus || emptyFunction}
ref={action((r: DocumentViewInternal | null) => r && (this.docView = r))}
/>
+ {this.htmlOverlay}
</div>
)}
@@ -1727,36 +1582,38 @@ export class DocumentView extends React.Component<DocumentViewProps> {
}
}
-export function deiconifyViewFunc(documentView: DocumentView) {
- documentView.iconify();
- //StrCast(doc.layoutKey).split("_")[1] === "icon" && setNativeView(doc);
-}
ScriptingGlobals.add(function deiconifyView(documentView: DocumentView) {
documentView.iconify();
documentView.select(false);
});
+ScriptingGlobals.add(function deiconifyViewToLightbox(documentView: DocumentView) {
+ //documentView.iconify(() =>
+ LightboxView.AddDocTab(documentView.rootDoc, OpenWhere.lightbox, 'layout'); //, 0);
+});
+
ScriptingGlobals.add(function toggleDetail(dv: DocumentView, detailLayoutKeySuffix: string) {
- if (dv.Document.layoutKey === 'layout_' + detailLayoutKeySuffix) dv.switchViews(false, 'layout');
+ if (dv.Document.layout_fieldKey === 'layout_' + detailLayoutKeySuffix) dv.switchViews(false, 'layout');
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]();
- const links = DocListCast(linkSource.links);
+ 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 alias = Doc.MakeAlias(otherdoc);
- alias.x = wid;
- alias.y = 0;
- alias._lockedPosition = false;
- wid += otherdoc[WidthSym]();
- Doc.AddDocToList(Doc.GetProto(linkCollection), 'data', alias);
+ embedding = Doc.MakeEmbedding(otherdoc);
+ embedding.x = wid;
+ embedding.y = 0;
+ embedding._lockedPosition = false;
+ 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.tsx b/src/client/views/nodes/EquationBox.tsx
index c279341cc..8d45c5724 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';
@@ -21,6 +21,7 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() {
public static SelectOnLoad: string = '';
_ref: React.RefObject<EquationEditor> = React.createRef();
componentDidMount() {
+ this.props.setContentView?.(this);
if (EquationBox.SelectOnLoad === this.rootDoc[Id] && (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath()))) {
this.props.select(false);
@@ -66,7 +67,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,
@@ -78,31 +79,21 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() {
if (e.key === 'Backspace' && !this.dataDoc.text) this.props.removeDocument?.(this.rootDoc);
};
@undoBatch
- onChange = (str: string) => {
- this.dataDoc.text = str;
+ onChange = (str: string) => (this.dataDoc.text = str);
+
+ updateSize = () => {
const style = this._ref.current && getComputedStyle(this._ref.current.element.current);
- if (style) {
- const _height = Number(style.height.replace('px', ''));
- const _width = Number(style.width.replace('px', ''));
- this.layoutDoc._width = Math.max(35, _width);
- this.layoutDoc._height = Math.max(25, _height);
+ if (style?.width.endsWith('px') && style?.height.endsWith('px')) {
+ this.layoutDoc._width = Math.max(35, Number(style.width.replace('px', '')));
+ this.layoutDoc._height = Math.max(25, Number(style.height.replace('px', '')));
}
};
render() {
TraceMobx();
- const scale = (this.props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._viewScale, 1);
+ const scale = (this.props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._freeform_scale, 1);
return (
<div
- ref={r => {
- r instanceof HTMLDivElement &&
- new ResizeObserver(
- action((entries: any) => {
- if (entries[0].contentBoxSize[0].inlineSize) {
- this.rootDoc._width = entries[0].contentBoxSize[0].inlineSize;
- }
- })
- ).observe(r);
- }}
+ ref={r => this.updateSize()}
className="equationBox-cont"
onPointerDown={e => !e.ctrlKey && e.stopPropagation()}
style={{
@@ -110,9 +101,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._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 e53422ab7..4ebf22ddf 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -20,7 +20,7 @@ export interface FieldViewProps extends DocumentViewSharedProps {
select: (isCtrlPressed: boolean) => void;
isContentActive: (outsideReaction?: boolean) => boolean | undefined;
- isDocumentActive?: () => boolean;
+ isDocumentActive?: () => boolean | undefined;
isSelected: (outsideReaction?: boolean) => boolean;
setHeight?: (height: number) => void;
NativeDimScaling?: () => number; // scaling the DocumentView does to transform its contents into its panel & needed by ScreenToLocal NOTE: Must also be added to DocumentViewInternalsProps
@@ -32,6 +32,7 @@ export interface FieldViewProps extends DocumentViewSharedProps {
// See currentUserUtils headerTemplate for examples of creating text boxes from html which set some of these fields
// Also, see InkingStroke for examples of creating text boxes from render() methods which set some of these fields
backgroundColor?: string;
+ treeViewDoc?: Doc;
color?: string;
fontSize?: number;
height?: number;
@@ -44,55 +45,23 @@ 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
get field(): FieldResult {
- const { Document, fieldKey } = this.props;
+ const { Document, fieldKey: fieldKey } = this.props;
return Document[fieldKey];
}
render() {
const field = this.field;
- if (field === undefined) {
- return <p>{'<null>'}</p>;
- }
- // if (typeof field === "string") {
- // return <p>{field}</p>;
- // }
- // else if (field instanceof RichTextField) {
- // return <FormattedTextBox {...this.props} />;
- // }
- // else if (field instanceof ImageField) {
- // return <ImageBox {...this.props} />;
- // }
- // else if (field instaceof PresBox) {
- // return <PresBox {...this.props} />;
- // }
- // else if (field instanceof VideoField) {
- // return <VideoBox {...this.props} />;
- // }
- // else if (field instanceof AudioField) {
- // return <AudioBox {...this.props} />;
- //}
- else if (field instanceof DateField) {
- return <p>{field.date.toLocaleString()}</p>;
- } else if (field instanceof Doc) {
- return (
- <p>
- <b>{field.title?.toString()}</b>
- </p>
- );
- } else if (field instanceof List) {
- return <div> {field.length ? field.map(f => Field.toString(f)).join(', ') : ''} </div>;
- }
- // bcz: this belongs here, but it doesn't render well so taking it out for now
- else if (field instanceof WebField) {
- return <p>{Field.toString(field.url.href)}</p>;
- } else if (!(field instanceof Promise)) {
- return <p>{Field.toString(field)}</p>;
- } else {
- return <p> {'Waiting for server...'} </p>;
- }
+ // prettier-ignore
+ if (field instanceof Doc) return <p> <b>{field.title?.toString()}</b></p>;
+ if (field === undefined) return <p>{'<null>'}</p>;
+ if (field instanceof DateField) return <p>{field.date.toLocaleString()}</p>;
+ if (field instanceof List) return <div> {field.map(f => Field.toString(f)).join(', ')} </div>;
+ if (field instanceof WebField) return <p>{Field.toString(field.url.href)}</p>;
+ if (!(field instanceof Promise)) return <p>{Field.toString(field)}</p>;
+ return <p> {'Waiting for server...'} </p>;
}
}
diff --git a/src/client/views/nodes/FilterBox.tsx b/src/client/views/nodes/FilterBox.tsx
deleted file mode 100644
index 04d252abe..000000000
--- a/src/client/views/nodes/FilterBox.tsx
+++ /dev/null
@@ -1,588 +0,0 @@
-import React = require('react');
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, observable, reaction, runInAction } from 'mobx';
-import { observer } from 'mobx-react';
-import Select from 'react-select';
-import { Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt } from '../../../fields/Doc';
-import { List } from '../../../fields/List';
-import { RichTextField } from '../../../fields/RichTextField';
-import { listSpec } from '../../../fields/Schema';
-import { ComputedField, ScriptField } from '../../../fields/ScriptField';
-import { Cast, DocCast, NumCast, StrCast } from '../../../fields/Types';
-import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from '../../../Utils';
-import { Docs, DocumentOptions } from '../../documents/Documents';
-import { DocumentType } from '../../documents/DocumentTypes';
-import { UserOptions } from '../../util/GroupManager';
-import { ScriptingGlobals } from '../../util/ScriptingGlobals';
-import { SelectionManager } from '../../util/SelectionManager';
-import { CollectionTreeView } from '../collections/CollectionTreeView';
-import { CollectionView } from '../collections/CollectionView';
-import { ViewBoxBaseComponent } from '../DocComponent';
-import { EditableView } from '../EditableView';
-import { SearchBox } from '../search/SearchBox';
-import { DashboardToggleButton, DefaultStyleProvider, StyleProp } from '../StyleProvider';
-import { DocumentViewProps } from './DocumentView';
-import { FieldView, FieldViewProps } from './FieldView';
-import './FilterBox.scss';
-const higflyout = require('@hig/flyout');
-export const { anchorPoints } = higflyout;
-export const Flyout = higflyout.default;
-
-@observer
-export class FilterBox extends ViewBoxBaseComponent<FieldViewProps>() {
- constructor(props: Readonly<FieldViewProps>) {
- super(props);
- const targetDoc = FilterBox.targetDoc;
- if (targetDoc && !targetDoc.currentFilter) targetDoc.currentFilter = FilterBox.createFilterDoc();
- }
-
- /// creates a new, empty filter doc
- static createFilterDoc() {
- const clearAll = `getProto(self).data = new List([])`;
- const reqdOpts: DocumentOptions = {
- _lockedPosition: true,
- _autoHeight: true,
- _fitWidth: true,
- _height: 150,
- _xPadding: 5,
- _yPadding: 5,
- _gridGap: 5,
- _forceActive: true,
- title: 'Unnamed Filter',
- filterBoolean: 'AND',
- boxShadow: '0 0',
- childDontRegisterViews: true,
- targetDropAction: 'same',
- ignoreClick: true,
- system: true,
- childDropAction: 'none',
- treeViewHideTitle: true,
- treeViewTruncateTitleWidth: 150,
- childContextMenuLabels: new List<string>(['Clear All']),
- childContextMenuScripts: new List<ScriptField>([ScriptField.MakeFunction(clearAll)!]),
- };
- return Docs.Create.FilterDocument(reqdOpts);
- }
- public static LayoutString(fieldKey: string) {
- return FieldView.LayoutString(FilterBox, fieldKey);
- }
-
- public _filterSelected = false;
- public _filterMatch = 'matched';
-
- @computed static get currentContainingCollectionDoc() {
- let docView: any = SelectionManager.Views()[0];
- let doc = docView.Document;
-
- while (docView && doc && doc.type !== 'collection') {
- doc = docView.props.ContainingCollectionDoc;
- docView = docView.props.ContainingCollectionView;
- }
-
- return doc;
- }
-
- // /**
- // * @returns the relevant doc according to the value of FilterBox._filterScope i.e. either the Current Dashboard or the Current Collection
- // */
-
- // trying to get this to work rather than the version below this
-
- // @computed static get targetDoc() {
- // console.log(FilterBox.currentContainingCollectionDoc.type);
- // if (FilterBox._filterScope === "Current Collection") {
- // return FilterBox.currentContainingCollectionDoc;
- // }
- // else return CollectionDockingView.Instance.props.Document;
- // // return FilterBox._filterScope === "Current Collection" ? SelectionManager.Views()[0].Document || CollectionDockingView.Instance.props.Document : CollectionDockingView.Instance.props.Document;
- // }
-
- /**
- * @returns the relevant doc according to the value of FilterBox._filterScope i.e. either the Current Dashboard or the Current Collection
- */
- @computed static get targetDoc() {
- return SelectionManager.Docs().length ? SelectionManager.Docs()[0] : Doc.ActiveDashboard;
- }
- @computed static get targetDocChildKey() {
- if (SelectionManager.Views().length) {
- return SelectionManager.Views()[0].ComponentView?.annotationKey || SelectionManager.Views()[0].ComponentView?.fieldKey || 'data';
- }
- return 'data';
- }
- @computed static get targetDocChildren() {
- return DocListCast(FilterBox.targetDoc?.[FilterBox.targetDocChildKey] || Doc.ActiveDashboard?.data);
- }
-
- @observable _loaded = false;
- componentDidMount() {
- reaction(
- () => DocListCastAsync(this.layoutDoc[this.fieldKey]),
- async activeTabsAsync => {
- const activeTabs = await activeTabsAsync;
- activeTabs && (await SearchBox.foreachRecursiveDocAsync(activeTabs, emptyFunction));
- runInAction(() => (this._loaded = true));
- },
- { fireImmediately: true }
- );
- }
-
- @computed get allDocs() {
- // trace();
- const allDocs = new Set<Doc>();
- const targetDoc = FilterBox.targetDoc;
- if (this._loaded && targetDoc) {
- SearchBox.foreachRecursiveDoc(FilterBox.targetDocChildren, (depth, doc) => allDocs.add(doc));
- }
- return Array.from(allDocs);
- }
-
- @computed get _allFacets() {
- // trace();
- const noviceReqFields = ['author', 'tags', 'text', 'type'];
- const noviceLayoutFields: string[] = []; //["_curPage"];
- const noviceFields = [...noviceReqFields, ...noviceLayoutFields];
-
- const keys = new Set<string>(noviceFields);
- this.allDocs.forEach(doc => SearchBox.documentKeys(doc).filter(key => keys.add(key)));
- return Array.from(keys.keys())
- .filter(key => key[0])
- .filter(key => key[0] === '#' || key.indexOf('lastModified') !== -1 || (key[0] === key[0].toUpperCase() && !key.startsWith('_')) || noviceFields.includes(key) || !Doc.noviceMode)
- .sort();
- }
-
- /**
- * The current attributes selected to filter based on
- */
- @computed get activeAttributes() {
- return DocListCast(this.dataDoc[this.props.fieldKey]);
- }
-
- /**
- * @returns a string array of the current attributes
- */
- @computed get currentFacets() {
- return this.activeAttributes.map(attribute => StrCast(attribute.title));
- }
-
- gatherFieldValues(childDocs: Doc[], facetKey: string) {
- const valueSet = new Set<string>();
- let rtFields = 0;
- childDocs.forEach(d => {
- const facetVal = d[facetKey];
- if (facetVal instanceof RichTextField) rtFields++;
- valueSet.add(Field.toString(facetVal as Field));
- const fieldKey = Doc.LayoutFieldKey(d);
- const annos = !Field.toString(Doc.LayoutField(d) as Field).includes('CollectionView');
- const data = d[annos ? fieldKey + '-annotations' : fieldKey];
- if (data !== undefined) {
- let subDocs = DocListCast(data);
- if (subDocs.length > 0) {
- let newarray: Doc[] = [];
- while (subDocs.length > 0) {
- newarray = [];
- subDocs.forEach(t => {
- const facetVal = t[facetKey];
- if (facetVal instanceof RichTextField) rtFields++;
- facetVal !== undefined && valueSet.add(Field.toString(facetVal as Field));
- 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));
- });
- subDocs = newarray;
- }
- }
- }
- });
- return { strings: Array.from(valueSet.keys()), rtFields };
- }
- removeFilterDoc = (doc: Doc | Doc[]) => ((doc instanceof Doc ? [doc] : doc).map(doc => this.removeFilter(StrCast(doc.title))).length ? true : false);
- public removeFilter = (filterName: string) => {
- const targetDoc = FilterBox.targetDoc;
- if (targetDoc) {
- const filterDoc = targetDoc.currentFilter as Doc;
- const attributes = DocListCast(filterDoc.data);
- const found = attributes.findIndex(doc => doc.title === filterName);
- if (found !== -1) {
- (filterDoc.data as List<Doc>).splice(found, 1);
- const docFilter = Cast(targetDoc._docFilters, listSpec('string'));
- if (docFilter) {
- let index: number;
- while ((index = docFilter.findIndex(item => item.split(':')[0] === filterName)) !== -1) {
- docFilter.splice(index, 1);
- }
- }
- const docRangeFilters = Cast(targetDoc._docRangeFilters, listSpec('string'));
- if (docRangeFilters) {
- let index: number;
- while ((index = docRangeFilters.findIndex(item => item.split(':')[0] === filterName)) !== -1) {
- docRangeFilters.splice(index, 3);
- }
- }
- }
- }
- return true;
- };
- /**
- * Responds to clicking the check box in the flyout menu
- */
- facetClick = (facetHeader: string) => {
- const { targetDoc, targetDocChildren } = FilterBox;
- if (!targetDoc) return;
- const found = this.activeAttributes.findIndex(doc => doc.title === facetHeader);
- if (found !== -1) {
- this.removeFilter(facetHeader);
- } else {
- const allCollectionDocs = targetDocChildren;
- const facetValues = this.gatherFieldValues(targetDocChildren, facetHeader);
-
- let nonNumbers = 0;
- let minVal = Number.MAX_VALUE,
- maxVal = -Number.MAX_VALUE;
- facetValues.strings.map(val => {
- const num = val ? Number(val) : Number.NaN;
- if (Number.isNaN(num)) {
- val && nonNumbers++;
- } else {
- minVal = Math.min(num, minVal);
- maxVal = Math.max(num, maxVal);
- }
- });
- let newFacet: Opt<Doc>;
- if (facetHeader === 'text' || facetValues.rtFields / allCollectionDocs.length > 0.1) {
- newFacet = Docs.Create.TextDocument('', {
- title: facetHeader,
- system: true,
- target: targetDoc,
- _width: 100,
- _height: 25,
- _stayInCollection: true,
- treeViewExpandedView: 'layout',
- _treeViewOpen: true,
- _forceActive: true,
- ignoreClick: true,
- });
- Doc.GetProto(newFacet).forceActive = true; // required for FormattedTextBox to be able to gain focus since it will never be 'selected'
- Doc.GetProto(newFacet).type = DocumentType.COL; // forces item to show an open/close button instead ofa checkbox
- newFacet._textBoxPaddingX = newFacet._textBoxPaddingY = 4;
- const scriptText = `setDocFilter(this?.target, "${facetHeader}", text, "match")`;
- newFacet.onTextChanged = ScriptField.MakeScript(scriptText, { this: Doc.name, text: 'string' });
- } else if (facetHeader !== 'tags' && nonNumbers / facetValues.strings.length < 0.1) {
- newFacet = Docs.Create.SliderDocument({
- title: facetHeader,
- system: true,
- target: targetDoc,
- _fitWidth: true,
- _height: 40,
- _stayInCollection: true,
- treeViewExpandedView: 'layout',
- _treeViewOpen: true,
- _forceActive: true,
- _overflow: 'visible',
- });
- const newFacetField = Doc.LayoutFieldKey(newFacet);
- const ranged = Doc.readDocRangeFilter(targetDoc, facetHeader);
- Doc.GetProto(newFacet).type = DocumentType.COL; // forces item to show an open/close button instead ofa checkbox
- const extendedMinVal = minVal - Math.min(1, Math.floor(Math.abs(maxVal - minVal) * 0.1));
- const extendedMaxVal = Math.max(minVal + 1, maxVal + Math.min(1, Math.ceil(Math.abs(maxVal - minVal) * 0.05)));
- newFacet[newFacetField + '-min'] = ranged === undefined ? extendedMinVal : ranged[0];
- newFacet[newFacetField + '-max'] = ranged === undefined ? extendedMaxVal : ranged[1];
- Doc.GetProto(newFacet)[newFacetField + '-minThumb'] = extendedMinVal;
- Doc.GetProto(newFacet)[newFacetField + '-maxThumb'] = extendedMaxVal;
- const scriptText = `setDocRangeFilter(this?.target, "${facetHeader}", range)`;
- newFacet.onThumbChanged = ScriptField.MakeScript(scriptText, { this: Doc.name, range: 'number' });
- newFacet.data = ComputedField.MakeFunction(`readNumFacetData(self.target, self, "${FilterBox.targetDocChildKey}", "${facetHeader}")`);
- } else {
- newFacet = new Doc();
- newFacet.system = true;
- newFacet.title = facetHeader;
- newFacet.treeViewOpen = true;
- newFacet.layout = CollectionView.LayoutString('data');
- newFacet.layoutKey = 'layout';
- newFacet.type = DocumentType.COL;
- newFacet.target = targetDoc;
- newFacet.data = ComputedField.MakeFunction(`readFacetData(self.target, "${FilterBox.targetDocChildKey}", "${facetHeader}")`);
- }
- newFacet.hideContextMenu = true;
- Doc.AddDocToList(this.dataDoc, this.props.fieldKey, newFacet);
- }
- };
-
- @computed get scriptField() {
- const scriptText = 'setDocFilter(this?.target, heading, this.title, checked)';
- const script = ScriptField.MakeScript(scriptText, { this: Doc.name, heading: 'string', checked: 'string', containingTreeView: Doc.name });
- return script ? () => script : undefined;
- }
-
- /**
- * Sets whether filters are ANDed or ORed together
- */
- @action
- changeBool = (e: any) => {
- FilterBox.targetDoc && (DocCast(FilterBox.targetDoc.currentFilter).filterBoolean = e.currentTarget.value);
- };
-
- /**
- * Changes whether to select matched or unmatched documents
- */
- @action
- changeMatch = (e: any) => {
- this._filterMatch = e.currentTarget.value;
- };
-
- @action
- changeSelected = () => {
- if (this._filterSelected) {
- this._filterSelected = false;
- SelectionManager.DeselectAll();
- } else {
- this._filterSelected = true;
- // helper method to select specified docs
- }
- };
-
- FilteringStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps | DocumentViewProps>, property: string) {
- switch (property.split(':')[0]) {
- case StyleProp.Decorations:
- if (doc && !doc.treeViewHideHeaderFields) {
- return (
- <>
- <div style={{ marginRight: '5px', fontSize: '10px' }}>
- <select className="filterBox-selection">
- <option value="Is" key="Is">
- Is
- </option>
- <option value="Is Not" key="Is Not">
- Is Not
- </option>
- </select>
- </div>
- <div className="filterBox-treeView-close" onClick={e => this.removeFilter(StrCast(doc.title))}>
- <FontAwesomeIcon icon={'times'} size="sm" />
- </div>
- </>
- );
- }
- }
- return DefaultStyleProvider(doc, props, property);
- }
-
- suppressChildClick = () => ScriptField.MakeScript('')!;
-
- /**
- * Adds a filterDoc to the list of saved filters
- */
- saveFilter = () => {
- Doc.AddDocToList(Doc.UserDoc(), 'savedFilters', this.props.Document);
- };
-
- /**
- * Changes the title of the filterDoc
- */
- onTitleValueChange = (val: string) => {
- Doc.GetProto(this.props.Document).title = val || `FilterDoc for ${FilterBox.targetDoc?.title}`;
- return true;
- };
-
- /**
- * The flyout from which you can select a saved filter to apply
- */
- @computed get flyoutPanel() {
- return DocListCast(Doc.UserDoc().savedFilters).map(doc => {
- return (
- <div className="filterBox-tempFlyout" onWheel={e => e.stopPropagation()} style={{ height: 20, border: '2px' }} onPointerDown={() => this.props.updateFilterDoc?.(doc)}>
- {StrCast(doc.title)}
- </div>
- );
- });
- }
- setTreeHeight = (hgt: number) => {
- this.layoutDoc._height = NumCast(this.layoutDoc._autoHeightMargins) + 150; // need to add all the border sizes together.
- };
-
- /**
- * add lock and hide button decorations for the "Dashboards" flyout TreeView
- */
- FilterStyleProvider = (doc: Opt<Doc>, props: Opt<FieldViewProps | DocumentViewProps>, property: string) => {
- if (property.split(':')[0] === StyleProp.Decorations) {
- return !doc || doc.treeViewHideHeaderFields ? null : DashboardToggleButton(doc, 'hidden', 'trash', 'trash', () => this.removeFilter(StrCast(doc.title)));
- }
- return this.props.styleProvider?.(doc, props, property);
- };
-
- layoutHeight = () => this.layoutDoc[HeightSym]();
- render() {
- const facetCollection = this.props.Document;
-
- const options = this._allFacets.filter(facet => this.currentFacets.indexOf(facet) === -1).map(facet => ({ value: facet, label: facet }));
- return this.props.dontRegisterView ? null : (
- <div className="filterBox-treeView" style={{ width: '100%' }}>
- <div className="filterBox-title">
- <EditableView key="editableView" contents={this.props.Document.title} height={24} fontSize={15} GetValue={() => StrCast(this.props.Document.title)} SetValue={this.onTitleValueChange} />
- </div>
-
- <div className="filterBox-select-bool">
- <select className="filterBox-selection" onChange={this.changeBool} defaultValue={StrCast((FilterBox.targetDoc?.currentFilter as Doc)?.filterBoolean)}>
- <option value="AND" key="AND">
- AND
- </option>
- <option value="OR" key="OR">
- OR
- </option>
- </select>
- <div className="filterBox-select-text">filters together</div>
- </div>
-
- <div className="filterBox-select">
- <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-tree" key="tree">
- <CollectionTreeView
- Document={facetCollection}
- DataDoc={Doc.GetProto(facetCollection)}
- fieldKey={this.props.fieldKey}
- CollectionView={undefined}
- disableDocBrushing={true}
- setHeight={this.setTreeHeight} // if the tree view can trigger the height of the filter box to change, then this needs to be filled in.
- docFilters={returnEmptyFilter}
- docRangeFilters={returnEmptyFilter}
- searchFilterDocs={returnEmptyDoclist}
- childDocumentsActive={returnTrue}
- ContainingCollectionDoc={this.props.ContainingCollectionDoc}
- ContainingCollectionView={this.props.ContainingCollectionView}
- PanelWidth={this.props.PanelWidth}
- PanelHeight={this.layoutHeight}
- rootSelected={this.props.rootSelected}
- renderDepth={1}
- dropAction={this.props.dropAction}
- ScreenToLocalTransform={this.props.ScreenToLocalTransform}
- isAnyChildContentActive={returnFalse}
- addDocTab={returnFalse}
- pinToPres={returnFalse}
- isSelected={returnFalse}
- select={returnFalse}
- bringToFront={emptyFunction}
- isContentActive={returnTrue}
- whenChildContentsActiveChanged={returnFalse}
- treeViewHideTitle={true}
- focus={returnFalse}
- onCheckedClick={this.scriptField}
- treeViewHideHeaderFields={false}
- dontRegisterView={true}
- styleProvider={this.FilterStyleProvider}
- docViewPath={this.props.docViewPath}
- scriptContext={this.props.scriptContext}
- moveDocument={returnFalse}
- removeDocument={this.removeFilterDoc}
- addDocument={returnFalse}
- />
- </div>
- <div className="filterBox-bottom">
- {/* <div className="filterBox-select-matched">
- <input className="filterBox-select-box" type="checkbox"
- onChange={this.changeSelected} />
- <div className="filterBox-select-text">select</div>
- <select className="filterBox-selection" onChange={e => this.changeMatch(e)}>
- <option value="matched" key="matched">matched</option>
- <option value="unmatched" key="unmatched">unmatched</option>
- </select>
- <div className="filterBox-select-text">documents</div>
- </div> */}
-
- <div style={{ display: 'flex' }}>
- <div className="filterBox-saveWrapper">
- <div className="filterBox-saveBookmark" onPointerDown={this.saveFilter}>
- <div>SAVE</div>
- </div>
- </div>
- <div className="filterBox-saveWrapper">
- <div className="filterBox-saveBookmark">
- <Flyout className="myFilters-flyout" anchorPoint={anchorPoints.TOP} content={this.flyoutPanel}>
- <div>FILTERS</div>
- </Flyout>
- </div>
- </div>
- <div className="filterBox-saveWrapper">
- <div className="filterBox-saveBookmark" onPointerDown={this.props.createNewFilterDoc}>
- <div>NEW</div>
- </div>
- </div>
- </div>
- </div>
- </div>
- );
- }
-}
-
-ScriptingGlobals.add(function determineCheckedState(layoutDoc: Doc, facetHeader: string, facetValue: string) {
- const docFilters = Cast(layoutDoc._docFilters, listSpec('string'), []);
- for (const filter of docFilters) {
- const fields = filter.split(':'); // split into key:value:modifiers
- if (fields[0] === facetHeader && fields[1] === facetValue) {
- return fields[2];
- }
- }
- return undefined;
-});
-ScriptingGlobals.add(function readNumFacetData(layoutDoc: Doc, facetDoc: Doc, childKey: string, facetHeader: string) {
- const allCollectionDocs = new Set<Doc>();
- const activeTabs = DocListCast(layoutDoc[childKey]);
- SearchBox.foreachRecursiveDoc(activeTabs, (depth: number, doc: Doc) => allCollectionDocs.add(doc));
- const set = new Set<string>();
- if (facetHeader === 'tags')
- allCollectionDocs.forEach(child =>
- Field.toString(child[facetHeader] as Field)
- .split(':')
- .forEach(key => set.add(key))
- );
- else allCollectionDocs.forEach(child => set.add(Field.toString(child[facetHeader] as Field)));
- const facetValues = Array.from(set).filter(v => v);
-
- let minVal = Number.MAX_VALUE,
- maxVal = -Number.MAX_VALUE;
- facetValues.map(val => {
- const num = val ? Number(val) : Number.NaN;
- if (!Number.isNaN(num)) {
- minVal = Math.min(num, minVal);
- maxVal = Math.max(num, maxVal);
- }
- });
- const newFacetField = Doc.LayoutFieldKey(facetDoc);
- const ranged = Doc.readDocRangeFilter(layoutDoc, facetHeader);
- const extendedMinVal = minVal - Math.min(1, Math.floor(Math.abs(maxVal - minVal) * 0.1));
- const extendedMaxVal = Math.max(minVal + 1, maxVal + Math.min(1, Math.ceil(Math.abs(maxVal - minVal) * 0.05)));
- facetDoc[newFacetField + '-min'] = ranged === undefined ? extendedMinVal : ranged[0];
- facetDoc[newFacetField + '-max'] = ranged === undefined ? extendedMaxVal : ranged[1];
- Doc.GetProto(facetDoc)[newFacetField + '-minThumb'] = extendedMinVal;
- Doc.GetProto(facetDoc)[newFacetField + '-maxThumb'] = extendedMaxVal;
-});
-ScriptingGlobals.add(function readFacetData(layoutDoc: Doc, childKey: string, facetHeader: string) {
- const allCollectionDocs = new Set<Doc>();
- const activeTabs = DocListCast(layoutDoc[childKey]);
- SearchBox.foreachRecursiveDoc(activeTabs, (depth: number, doc: Doc) => allCollectionDocs.add(doc));
- const set = new Set<string>();
- if (facetHeader === 'tags')
- allCollectionDocs.forEach(child =>
- Field.toString(child[facetHeader] as Field)
- .split(':')
- .forEach(key => set.add(key))
- );
- else allCollectionDocs.forEach(child => set.add(Field.toString(child[facetHeader] as Field)));
- const facetValues = Array.from(set).filter(v => v);
-
- let nonNumbers = 0;
-
- facetValues.map(val => Number.isNaN(Number(val)) && nonNumbers++);
- const facetValueDocSet = (nonNumbers / facetValues.length > 0.1 ? facetValues.sort() : facetValues.sort((n1: string, n2: string) => Number(n1) - Number(n2))).map(facetValue => {
- const doc = new Doc();
- doc.system = true;
- doc.title = facetValue.toString();
- doc.target = layoutDoc;
- doc.facetHeader = facetHeader;
- doc.facetValue = facetValue;
- doc.treeViewHideHeaderFields = true;
- doc.treeViewChecked = ComputedField.MakeFunction('determineCheckedState(self.target, self.facetHeader, self.facetValue)');
- return doc;
- });
- return new List<Doc>(facetValueDocSet);
-});
diff --git a/src/client/views/nodes/FunctionPlotBox.tsx b/src/client/views/nodes/FunctionPlotBox.tsx
index e09155ac2..61711417f 100644
--- a/src/client/views/nodes/FunctionPlotBox.tsx
+++ b/src/client/views/nodes/FunctionPlotBox.tsx
@@ -12,8 +12,10 @@ import { TraceMobx } from '../../../fields/util';
import { Docs } from '../../documents/Documents';
import { DragManager } from '../../util/DragManager';
import { undoBatch } from '../../util/UndoManager';
-import { ViewBoxBaseComponent } from '../DocComponent';
+import { ViewBoxAnnotatableComponent } from '../DocComponent';
+import { DocFocusOptions, DocumentView } from './DocumentView';
import { FieldView, FieldViewProps } from './FieldView';
+import { PinProps, PresBox } from './trails';
const EquationSchema = createSchema({});
@@ -21,7 +23,7 @@ type EquationDocument = makeInterface<[typeof EquationSchema, typeof documentSch
const EquationDocument = makeInterface(EquationSchema, documentSchema);
@observer
-export class FunctionPlotBox extends ViewBoxBaseComponent<FieldViewProps>() {
+export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
public static LayoutString(fieldKey: string) {
return FieldView.LayoutString(FunctionPlotBox, fieldKey);
}
@@ -36,22 +38,21 @@ export class FunctionPlotBox extends ViewBoxBaseComponent<FieldViewProps>() {
componentDidMount() {
this.props.setContentView?.(this);
reaction(
- () => [DocListCast(this.dataDoc[this.fieldKey]).map(doc => doc?.text), this.layoutDoc.width, this.layoutDoc.height, this.dataDoc.xRange, this.dataDoc.yRange],
+ () => [DocListCast(this.dataDoc[this.fieldKey]).map(doc => doc?.text), this.layoutDoc.width, this.layoutDoc.height, this.rootDoc.xRange, this.rootDoc.yRange],
() => this.createGraph()
);
}
- getAnchor = () => {
- const anchor = Docs.Create.TextanchorDocument({ annotationOn: this.rootDoc });
- anchor.xRange = new List<number>(Array.from(this._plot.options.xAxis.domain));
- anchor.yRange = new List<number>(Array.from(this._plot.options.yAxis.domain));
+ getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
+ 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));
+ if (addAsAnnotation) this.addDocument(anchor);
return anchor;
};
- @action
- scrollFocus = (doc: Doc, smooth: boolean) => {
- this.dataDoc.xRange = new List<number>(Array.from(Cast(doc.xRange, listSpec('number'), Cast(this.dataDoc.xRange, listSpec('number'), [-10, 10]))));
- this.dataDoc.yRange = new List<number>(Array.from(Cast(doc.yRange, listSpec('number'), Cast(this.dataDoc.xRange, listSpec('number'), [-1, 9]))));
- return 0;
- };
createGraph = (ele?: HTMLDivElement) => {
this._plotEle = ele || this._plotEle;
const width = this.props.PanelWidth();
@@ -63,8 +64,8 @@ export class FunctionPlotBox extends ViewBoxBaseComponent<FieldViewProps>() {
target: '#' + this._plotEle.id,
width,
height,
- xAxis: { domain: Cast(this.dataDoc.xRange, listSpec('number'), [-10, 10]) },
- yAxis: { domain: Cast(this.dataDoc.xRange, listSpec('number'), [-1, 9]) },
+ xAxis: { domain: Cast(this.rootDoc.xRange, listSpec('number'), [-10, 10]) },
+ yAxis: { domain: Cast(this.rootDoc.yRange, listSpec('number'), [-1, 9]) },
grid: true,
data: fns.map(fn => ({
fn,
@@ -79,9 +80,10 @@ export class FunctionPlotBox extends ViewBoxBaseComponent<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;
};
@@ -92,7 +94,7 @@ export class FunctionPlotBox extends ViewBoxBaseComponent<FieldViewProps>() {
if (ele) {
this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.layoutDoc);
}
- // if (this.autoHeight) this.tryUpdateScrollHeight();
+ // if (this.layout_autoHeight) this.tryUpdateScrollHeight();
};
@computed get theGraph() {
return <div id={`${this._plotId}`} ref={r => r && this.createGraph(r)} style={{ position: 'absolute', width: '100%', height: '100%' }} onPointerDown={e => e.stopPropagation()} />;
@@ -103,7 +105,7 @@ export class FunctionPlotBox extends ViewBoxBaseComponent<FieldViewProps>() {
<div
ref={this.createDropTarget}
style={{
- pointerEvents: !this.isContentActive() ? 'all' : undefined,
+ pointerEvents: !this.props.isContentActive() ? 'all' : undefined,
width: this.props.PanelWidth(),
height: this.props.PanelHeight(),
}}>
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index cd2b23f02..29943e156 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -2,10 +2,11 @@
border-radius: inherit;
width: 100%;
height: 100%;
- position: relative;
+ position: absolute;
+ top: 0;
+ left: 0;
transform-origin: top left;
-
.imageBox-annotationLayer {
position: absolute;
transform-origin: left top;
@@ -95,7 +96,6 @@
height: 100%;
}
-
.imageBox-fader {
position: relative;
width: 100%;
@@ -103,7 +103,8 @@
display: flex;
height: 100%;
- .imageBox-fadeBlocker, .imageBox-fadeBlocker-hover{
+ .imageBox-fadeBlocker,
+ .imageBox-fadeBlocker-hover {
width: 100%;
height: 100%;
position: absolute;
@@ -120,23 +121,20 @@
}
}
}
+.imageBox-alternateDropTarget {
+ position: absolute;
+ color: white;
+ background: black;
+ right: 0;
+ bottom: 0;
+ z-index: 2;
+}
.imageBox-fader img {
position: absolute;
left: 0;
}
-.imageBox-fadeBlocker {
- -webkit-transition: opacity 1s ease-in-out;
- -moz-transition: opacity 1s ease-in-out;
- -o-transition: opacity 1s ease-in-out;
- transition: opacity 1s ease-in-out;
-}
-
.imageBox-fadeBlocker-hover {
- -webkit-transition: opacity 1s ease-in-out;
- -moz-transition: opacity 1s ease-in-out;
- -o-transition: opacity 1s ease-in-out;
- transition: opacity 1s ease-in-out;
opacity: 0;
-} \ No newline at end of file
+}
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 461d6984d..f67930a3f 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -1,7 +1,10 @@
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+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';
@@ -11,13 +14,15 @@ import { ComputedField } from '../../../fields/ScriptField';
import { Cast, NumCast, StrCast } from '../../../fields/Types';
import { ImageField } from '../../../fields/URLField';
import { TraceMobx } from '../../../fields/util';
-import { emptyFunction, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../Utils';
+import { DashColor, emptyFunction, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils } from '../../../Utils';
import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
import { CognitiveServices, Confidence, Service, Tag } from '../../cognitive_services/CognitiveServices';
-import { DocUtils } from '../../documents/Documents';
+import { Docs, DocUtils } from '../../documents/Documents';
import { DocumentType } from '../../documents/DocumentTypes';
import { Networking } from '../../Network';
+import { DocumentManager } from '../../util/DocumentManager';
import { DragManager } from '../../util/DragManager';
+import { SnappingManager } from '../../util/SnappingManager';
import { undoBatch } from '../../util/UndoManager';
import { ContextMenu } from '../../views/ContextMenu';
import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
@@ -26,10 +31,13 @@ import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComp
import { MarqueeAnnotator } from '../MarqueeAnnotator';
import { AnchorMenu } from '../pdf/AnchorMenu';
import { StyleProp } from '../StyleProvider';
+import { DocFocusOptions, OpenWhere } from './DocumentView';
import { FaceRectangles } from './FaceRectangles';
import { FieldView, FieldViewProps } from './FieldView';
import './ImageBox.scss';
+import { PinProps, PresBox } from './trails';
import React = require('react');
+import Color = require('color');
export const pageSchema = createSchema({
googlePhotosUrl: 'string',
@@ -48,30 +56,50 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
public static LayoutString(fieldKey: string) {
return FieldView.LayoutString(ImageBox, fieldKey);
}
+ private _ignoreScroll = false;
+ private _forcedScroll = false;
private _dropDisposer?: DragManager.DragDropDisposer;
private _disposers: { [name: string]: IReactionDisposer } = {};
- private _getAnchor: (savedAnnotations?: ObservableMap<number, HTMLDivElement[]>) => Opt<Doc> = () => undefined;
+ private _getAnchor: (savedAnnotations: Opt<ObservableMap<number, HTMLDivElement[]>>, addAsAnnotation: boolean) => Opt<Doc> = () => undefined;
+ private _overlayIconRef = React.createRef<HTMLDivElement>();
@observable _curSuffix = '';
@observable _uploadIcon = uploadIcons.idle;
+ constructor(props: any) {
+ super(props);
+ this.props.setContentView?.(this);
+ }
+
protected createDropTarget = (ele: HTMLDivElement) => {
this._dropDisposer?.();
ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.props.Document));
};
- setViewSpec = (anchor: Doc, preview: boolean) => {}; // sets viewing information for a componentview, typically when following a link. 'preview' tells the view to use the values without writing to the document
- getAnchor = () => {
- const anchor = this._getAnchor?.(this._savedAnnotations);
- anchor && this.addDocument(anchor);
- return anchor ?? this.rootDoc;
+ getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
+ const anchor =
+ this._getAnchor?.(this._savedAnnotations, false) ?? // use marquee anchor, otherwise, save zoom/pan as anchor
+ 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,
+ annotationOn: this.rootDoc,
+ });
+ if (anchor) {
+ if (!addAsAnnotation) anchor.backgroundColor = 'transparent';
+ /* addAsAnnotation &&*/ this.addDocument(anchor);
+ PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), pannable: true } }, this.rootDoc);
+ return anchor;
+ }
+ return this.rootDoc;
};
componentDidMount() {
- this.props.setContentView?.(this); // bcz: do not remove this. without it, stepping into an image in the lightbox causes an infinite loop....
this._disposers.sizer = reaction(
() => ({
forceFull: this.props.renderDepth < 1 || this.layoutDoc._showFullRes,
- scrSize: this.props.ScreenToLocalTransform().inverse().transformDirection(this.nativeSize.nativeWidth, this.nativeSize.nativeHeight)[0] / this.nativeSize.nativeWidth,
+ scrSize: (this.props.ScreenToLocalTransform().inverse().transformDirection(this.nativeSize.nativeWidth, this.nativeSize.nativeHeight)[0] / this.nativeSize.nativeWidth) * NumCast(this.rootDoc._freeform_scale, 1),
selected: this.props.isSelected(),
}),
({ forceFull, scrSize, selected }) => (this._curSuffix = selected ? '_o' : this.fieldKey === 'icon' ? '_m' : forceFull ? '_o' : scrSize < 0.25 ? '_s' : scrSize < 0.5 ? '_m' : scrSize < 0.8 ? '_l' : '_o'),
@@ -79,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;
@@ -87,6 +115,16 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
},
{ fireImmediately: true }
);
+ this._disposers.scroll = reaction(
+ () => this.layoutDoc.layout_scrollTop,
+ s_top => {
+ this._forcedScroll = true;
+ !this._ignoreScroll && this._mainCont.current && (this._mainCont.current.scrollTop = NumCast(s_top));
+ this._mainCont.current?.scrollTo({ top: NumCast(s_top) });
+ this._forcedScroll = false;
+ },
+ { fireImmediately: true }
+ );
}
componentWillUnmount() {
@@ -97,39 +135,61 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
@action
drop = (e: Event, de: DragManager.DropEvent) => {
if (de.complete.docDragData) {
- if (de.metaKey) {
- de.complete.docDragData.droppedDocuments.forEach(
- action((drop: Doc) => {
- Doc.AddDocToList(this.dataDoc, this.fieldKey + '-alternates', drop);
- e.stopPropagation();
- })
- );
+ let added = true;
+ 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)) {
+ 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 && e.preventDefault();
+ e.stopPropagation();
+ return added;
}
+ return false;
};
@undoBatch
resolution = () => (this.layoutDoc._showFullRes = !this.layoutDoc._showFullRes);
@undoBatch
+ 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._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;
+ });
+ @undoBatch
rotate = action(() => {
- const nw = NumCast(this.dataDoc[this.fieldKey + '-nativeWidth']);
- const nh = NumCast(this.dataDoc[this.fieldKey + '-nativeHeight']);
+ const nw = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']);
+ const nh = NumCast(this.dataDoc[this.fieldKey + '_nativeHeight']);
const w = this.layoutDoc._width;
const h = this.layoutDoc._height;
this.dataDoc[this.fieldKey + '-rotation'] = (NumCast(this.dataDoc[this.fieldKey + '-rotation']) + 90) % 360;
- this.dataDoc[this.fieldKey + '-nativeWidth'] = nh;
- this.dataDoc[this.fieldKey + '-nativeHeight'] = nw;
+ this.dataDoc[this.fieldKey + '_nativeWidth'] = nh;
+ this.dataDoc[this.fieldKey + '_nativeHeight'] = nw;
this.layoutDoc._width = h;
this.layoutDoc._height = w;
});
@@ -137,42 +197,46 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
crop = (region: Doc | undefined, addCrop?: boolean) => {
if (!region) return;
const cropping = Doc.MakeCopy(region, true);
- Doc.GetProto(region).backgroundColor = 'transparent';
Doc.GetProto(region).lockedPosition = true;
Doc.GetProto(region).title = 'region:' + this.rootDoc.title;
- Doc.GetProto(region).isPushpin = true;
+ Doc.GetProto(region).followLinkToggle = true;
this.addDocument(region);
const anchx = NumCast(cropping.x);
const anchy = NumCast(cropping.y);
const anchw = NumCast(cropping._width);
const anchh = NumCast(cropping._height);
- const viewScale = NumCast(this.rootDoc[this.fieldKey + '-nativeWidth']) / anchw;
+ const viewScale = NumCast(this.rootDoc[this.fieldKey + '_nativeWidth']) / anchw;
cropping.title = 'crop: ' + this.rootDoc.title;
cropping.x = NumCast(this.rootDoc.x) + NumCast(this.rootDoc._width);
cropping.y = NumCast(this.rootDoc.y);
cropping._width = anchw * (this.props.NativeDimScaling?.() || 1);
cropping._height = anchh * (this.props.NativeDimScaling?.() || 1);
- cropping.isLinkButton = undefined;
+ cropping.onClick = undefined;
const croppingProto = Doc.GetProto(cropping);
croppingProto.annotationOn = undefined;
- croppingProto.isPrototype = true;
+ croppingProto.isDataDoc = true;
+ croppingProto.backgroundColor = undefined;
croppingProto.proto = Cast(this.rootDoc.proto, Doc, null)?.proto; // set proto of cropping's data doc to be IMAGE_PROTO
croppingProto.type = DocumentType.IMG;
croppingProto.layout = ImageBox.LayoutString('data');
croppingProto.data = ObjectField.MakeCopy(this.rootDoc[this.fieldKey] as ObjectField);
- croppingProto['data-nativeWidth'] = anchw;
- croppingProto['data-nativeHeight'] = anchh;
- croppingProto.viewScale = viewScale;
- croppingProto.viewScaleMin = viewScale;
- croppingProto.panX = anchx / viewScale;
- croppingProto.panY = anchy / viewScale;
- croppingProto.panXMin = anchx / viewScale;
- croppingProto.panXMax = anchw / viewScale;
- croppingProto.panYMin = anchy / viewScale;
- croppingProto.panYMax = anchh / viewScale;
+ croppingProto['data_nativeWidth'] = anchw;
+ croppingProto['data_nativeHeight'] = anchh;
+ croppingProto.freeform_scale = viewScale;
+ croppingProto.freeform_scaleMin = viewScale;
+ croppingProto.freeform_panX = 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;
if (addCrop) {
- DocUtils.MakeLink({ doc: region }, { doc: cropping }, 'cropped image', '');
+ DocUtils.MakeLink(region, cropping, { link_relationship: 'cropped image' });
+ cropping.x = NumCast(this.rootDoc.x) + this.rootDoc[Width]();
+ cropping.y = NumCast(this.rootDoc.y);
+ this.props.addDocTab(cropping, OpenWhere.inParent);
}
+ DocumentManager.Instance.AddViewRenderedCb(cropping, dv => setTimeout(() => (dv.ComponentView as ImageBox).setNativeSize(), 200));
this.props.bringToFront(cropping);
return cropping;
};
@@ -181,9 +245,10 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
const field = Cast(this.dataDoc[this.fieldKey], ImageField);
if (field) {
const funcs: ContextMenuProps[] = [];
- funcs.push({ description: 'Rotate Clockwise 90', event: this.rotate, icon: 'expand-arrows-alt' });
- funcs.push({ description: `Show ${this.layoutDoc._showFullRes ? 'Dynamic Res' : 'Full Res'}`, event: this.resolution, icon: 'expand-arrows-alt' });
- funcs.push({ description: 'Copy path', event: () => Utils.CopyText(this.choosePath(field.url)), icon: 'expand-arrows-alt' });
+ funcs.push({ description: 'Rotate Clockwise 90', event: this.rotate, icon: 'redo-alt' });
+ funcs.push({ description: `Show ${this.layoutDoc._showFullRes ? 'Dynamic Res' : 'Full Res'}`, event: this.resolution, icon: 'expand' });
+ funcs.push({ description: 'Set Native Pixel Size', event: this.setNativeSize, icon: 'expand-arrows-alt' });
+ funcs.push({ description: 'Copy path', event: () => Utils.CopyText(this.choosePath(field.url)), icon: 'copy' });
if (!Doc.noviceMode) {
funcs.push({ description: 'Export to Google Photos', event: () => GooglePhotos.Transactions.UploadImages([this.props.Document]), icon: 'caret-square-right' });
@@ -239,7 +304,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)} />;
};
@@ -248,6 +313,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
return !tags ? null : <img id={'google-tags'} src={'/assets/google_tags.png'} />;
};
+ getScrollHeight = () => (this.props.layout_fitWidth?.(this.rootDoc) !== false && NumCast(this.rootDoc._freeform_scale, 1) === NumCast(this.rootDoc._freeform_scaleMin, 1) ? this.nativeSize.nativeHeight : undefined);
+
@computed
private get considerDownloadIcon() {
const data = this.dataDoc[this.fieldKey];
@@ -281,9 +348,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
setTimeout(
action(() => {
this._uploadIcon = idle;
- if (data) {
- dataDoc[this.fieldKey] = data;
- }
+ data && (dataDoc[this.fieldKey] = data);
}),
2000
);
@@ -294,11 +359,46 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
@computed get nativeSize() {
TraceMobx();
- const nativeWidth = NumCast(this.dataDoc[this.fieldKey + '-nativeWidth'], NumCast(this.layoutDoc[this.fieldKey + '-nativeWidth'], 500));
- const nativeHeight = NumCast(this.dataDoc[this.fieldKey + '-nativeHeight'], NumCast(this.layoutDoc[this.fieldKey + '-nativeHeight'], 1));
+ const nativeWidth = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth'], NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth'], 500));
+ const nativeHeight = NumCast(this.dataDoc[this.fieldKey + '_nativeHeight'], NumCast(this.layoutDoc[this.fieldKey + '_nativeHeight'], 500));
const nativeOrientation = NumCast(this.dataDoc[this.fieldKey + '-nativeOrientation'], 1);
return { nativeWidth, nativeHeight, nativeOrientation };
}
+ @computed get overlayImageIcon() {
+ const usePath = this.rootDoc[`_${this.fieldKey}_usePath`];
+ return (
+ <Tooltip
+ title={
+ <div className="dash-tooltip">
+ toggle between
+ <span style={{ color: usePath === undefined ? 'black' : undefined }}>
+ <em> primary, </em>
+ </span>
+ <span style={{ color: usePath === 'alternate' ? 'black' : undefined }}>
+ <em>alternate, </em>
+ </span>
+ and show
+ <span style={{ color: usePath === 'alternate:hover' ? 'black' : undefined }}>
+ <em> alternate on hover</em>
+ </span>
+ </div>
+ }>
+ <div
+ className="imageBox-alternateDropTarget"
+ 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: (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',
+ color: usePath === undefined ? 'black' : 'white',
+ }}>
+ <FontAwesomeIcon icon="turn-up" size="lg" />
+ </div>
+ </Tooltip>
+ );
+ }
@computed get paths() {
const field = Cast(this.dataDoc[this.fieldKey], ImageField, null); // retrieve the primary image URL that is being rendered from the data doc
@@ -308,14 +408,16 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
.filter(url => url)
.map(url => this.choosePath(url)); // access the primary layout data of the alternate documents
const paths = field ? [this.choosePath(field.url), ...altpaths] : altpaths;
- return paths.length ? paths : [Utils.CorsProxy('http://www.cs.brown.edu/~bcz/noImage.png')];
+ return paths.length ? paths : [Utils.CorsProxy('https://cs.brown.edu/~bcz/noImage.png')];
}
+ @observable _isHovering = false; // flag to switch between primary and alternate images on hover
@computed get content() {
TraceMobx();
- const srcpath = this.paths[0];
- const fadepath = this.paths[Math.min(1, this.paths.length - 1)];
+ const backAlpha = DashColor(this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor)).alpha();
+ const srcpath = this.layoutDoc.hideImage ? '' : this.paths[0];
+ const fadepath = this.layoutDoc.hideImage ? '' : this.paths.lastElement();
const { nativeWidth, nativeHeight, nativeOrientation } = this.nativeSize;
const rotation = NumCast(this.dataDoc[this.fieldKey + '-rotation']);
const aspect = rotation % 180 ? nativeHeight / nativeWidth : 1;
@@ -330,27 +432,26 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
transformOrigin = 'right top';
transform = `translate(-100%, 0%) rotate(${rotation}deg) scale(${aspect})`;
}
+ const usePath = this.rootDoc[`_${this.fieldKey}_usePath`];
return (
- <div className="imageBox-cont" key={this.layoutDoc[Id]} ref={this.createDropTarget} onPointerDown={this.marqueeDown}>
- <div className="imageBox-fader" style={{ overflow: Array.from(this.props.docViewPath?.()).slice(-1)[0].fitWidth ? 'auto' : undefined }}>
+ <div className="imageBox-cont" onPointerEnter={action(() => (this._isHovering = true))} onPointerLeave={action(() => (this._isHovering = false))} key={this.layoutDoc[Id]} ref={this.createDropTarget} onPointerDown={this.marqueeDown}>
+ <div className="imageBox-fader" style={{ opacity: backAlpha }}>
<img key="paths" src={srcpath} style={{ transform, transformOrigin }} draggable={false} width={nativeWidth} />
{fadepath === srcpath ? null : (
- <div className={`imageBox-fadeBlocker${this.props.isHovering?.() ? '-hover' : ''}`}>
- <img className="imageBox-fadeaway" key={'fadeaway'} src={fadepath} style={{ transform, transformOrigin }} draggable={false} width={nativeWidth} />
+ <div className={`imageBox-fadeBlocker${(this._isHovering && usePath === 'alternate:hover') || usePath === 'alternate' ? '-hover' : ''}`} style={{ transition: StrCast(this.layoutDoc.viewTransition, 'opacity 1000ms') }}>
+ <img className="imageBox-fadeaway" key="fadeaway" src={fadepath} style={{ transform, transformOrigin }} draggable={false} width={nativeWidth} />
</div>
)}
</div>
{!Doc.noviceMode && this.considerDownloadIcon}
{this.considerGooglePhotosLink()}
<FaceRectangles document={this.dataDoc} color={'#0000FF'} backgroundColor={'#0000FF'} />
+ {this.overlayImageIcon}
</div>
);
}
- screenToLocalTransform = this.props.ScreenToLocalTransform;
- contentFunc = () => [this.content];
-
private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
@observable _marqueeing: number[] | undefined;
@@ -359,8 +460,9 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
TraceMobx();
return <div className="imageBox-annotationLayer" style={{ height: this.props.PanelHeight() }} ref={this._annotationLayer} />;
}
+ screenToLocalTransform = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._layout_scrollTop) * this.props.ScreenToLocalTransform().Scale);
marqueeDown = (e: React.PointerEvent) => {
- if (!e.altKey && e.button === 0 && NumCast(this.rootDoc._viewScale, 1) <= NumCast(this.rootDoc.viewScaleMin, 1) && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) {
+ if (!e.altKey && e.button === 0 && NumCast(this.rootDoc._freeform_scale, 1) <= NumCast(this.rootDoc.freeform_scaleMin, 1) && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) {
setupMoveUpEvents(
this,
e,
@@ -381,8 +483,10 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
this._marqueeing = undefined;
this.props.select(false);
};
- savedAnnotations = () => this._savedAnnotations;
+ focus = (anchor: Doc, options: DocFocusOptions) => this._ffref.current?.focus(anchor, options);
+ _ffref = React.createRef<CollectionFreeFormView>();
+ savedAnnotations = () => this._savedAnnotations;
render() {
TraceMobx();
const borderRad = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding);
@@ -392,29 +496,47 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
className="imageBox"
onContextMenu={this.specificContextMenu}
ref={this._mainCont}
+ onScroll={action(e => {
+ if (!this._forcedScroll) {
+ if (this.layoutDoc._layout_scrollTop || this._mainCont.current?.scrollTop) {
+ this._ignoreScroll = true;
+ this.layoutDoc._layout_scrollTop = this._mainCont.current?.scrollTop;
+ this._ignoreScroll = false;
+ }
+ }
+ })}
style={{
+ 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,
borderRadius,
+ overflow: this.layoutDoc.layout_fitWidth || this.props.layout_fitWidth?.(this.rootDoc) ? 'auto' : undefined,
}}>
<CollectionFreeFormView
- {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
+ ref={this._ffref}
+ {...this.props}
+ setContentView={emptyFunction}
+ NativeWidth={returnZero}
+ NativeHeight={returnZero}
renderDepth={this.props.renderDepth + 1}
fieldKey={this.annotationKey}
- CollectionView={undefined}
+ styleProvider={this.props.styleProvider}
isAnnotationOverlay={true}
annotationLayerHostsContent={true}
PanelWidth={this.props.PanelWidth}
PanelHeight={this.props.PanelHeight}
ScreenToLocalTransform={this.screenToLocalTransform}
select={emptyFunction}
+ focus={this.focus}
+ getScrollHeight={this.getScrollHeight}
NativeDimScaling={returnOne}
+ isAnyChildContentActive={returnFalse}
whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
removeDocument={this.removeDocument}
moveDocument={this.moveDocument}
addDocument={this.addDocument}>
- {this.contentFunc}
+ {this.content}
</CollectionFreeFormView>
{this.annotationLayer}
{!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? null : (
@@ -427,8 +549,10 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
addDocument={this.addDocument}
finishMarquee={this.finishMarquee}
savedAnnotations={this.savedAnnotations}
+ selectionText={returnEmptyString}
annotationLayer={this._annotationLayer.current}
mainCont={this._mainCont.current}
+ highlightDragSrcColor={''}
anchorMenuCrop={this.crop}
/>
)}
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index 7d04c4b64..a6712a3db 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -3,34 +3,37 @@ import { observer } from 'mobx-react';
import { Doc, Field, FieldResult } from '../../../fields/Doc';
import { List } from '../../../fields/List';
import { RichTextField } from '../../../fields/RichTextField';
-import { listSpec } from '../../../fields/Schema';
import { ComputedField, ScriptField } from '../../../fields/ScriptField';
-import { Cast, DocCast, FieldValue, NumCast } from '../../../fields/Types';
+import { DocCast, NumCast } from '../../../fields/Types';
import { ImageField } from '../../../fields/URLField';
+import { returnAll, returnAlways, returnTrue } from '../../../Utils';
import { Docs } from '../../documents/Documents';
import { SetupDrag } from '../../util/DragManager';
import { CompiledScript, CompileScript, ScriptOptions } from '../../util/Scripting';
import { undoBatch } from '../../util/UndoManager';
-import { FieldView, FieldViewProps } from './FieldView';
-import './KeyValueBox.scss';
-import { KeyValuePair } from './KeyValuePair';
-import React = require('react');
import { ContextMenu } from '../ContextMenu';
import { ContextMenuProps } from '../ContextMenuItem';
-import e = require('express');
+import { OpenWhere } from './DocumentView';
+import { FieldView, FieldViewProps } from './FieldView';
import { FormattedTextBox } from './formattedText/FormattedTextBox';
import { ImageBox } from './ImageBox';
+import './KeyValueBox.scss';
+import { KeyValuePair } from './KeyValuePair';
+import React = require('react');
+import { DocumentManager } from '../../util/DocumentManager';
+import { ScriptingGlobals } from '../../util/ScriptingGlobals';
+import { ScriptingRepl } from '../ScriptingRepl';
+import { DocumentIconContainer } from './DocumentIcon';
export type KVPScript = {
script: CompiledScript;
type: 'computed' | 'script' | false;
onDelegate: boolean;
};
-
@observer
export class KeyValueBox extends React.Component<FieldViewProps> {
- public static LayoutString(fieldStr: string) {
- return FieldView.LayoutString(KeyValueBox, fieldStr);
+ public static LayoutString() {
+ return FieldView.LayoutString(KeyValueBox, 'data');
}
private _mainCont = React.createRef<HTMLDivElement>();
@@ -38,13 +41,20 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
private _keyInput = React.createRef<HTMLInputElement>();
private _valInput = React.createRef<HTMLInputElement>();
+ componentDidMount() {
+ this.props.setContentView?.(this);
+ }
+ reverseNativeScaling = returnTrue;
+ able = returnAlways;
+ layout_fitWidth = returnTrue;
+ overridePointerEvents = returnAll;
+ onClickScriptDisable = returnAlways;
+
@observable private rows: KeyValuePair[] = [];
+ @observable _splitPercentage = 50;
- @computed get splitPercentage() {
- return NumCast(this.props.Document.schemaSplitPercentage, 50);
- }
get fieldDocToLayout() {
- return this.props.fieldKey ? Cast(this.props.Document[this.props.fieldKey], Doc, null) : this.props.Document;
+ return this.props.fieldKey ? DocCast(this.props.Document[this.props.fieldKey], DocCast(this.props.Document)) : this.props.Document;
}
@action
@@ -62,30 +72,33 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
};
public static CompileKVPScript(value: string): KVPScript | undefined {
const eq = value.startsWith('=');
- value = eq ? value.substr(1) : value;
- const dubEq = value.startsWith(':=') ? 'computed' : value.startsWith(';=') ? 'script' : false;
- value = dubEq ? value.substr(2) : value;
- const options: ScriptOptions = { addReturn: true, params: { this: Doc.name, self: Doc.name, _last_: 'any', _readOnly_: 'boolean' }, editable: false };
+ value = eq ? value.substring(1) : value;
+ const dubEq = value.startsWith(':=') ? 'computed' : value.startsWith('$=') ? 'script' : false;
+ value = dubEq ? value.substring(2) : value;
+ const options: ScriptOptions = { addReturn: true, typecheck: false, params: { this: Doc.name, self: Doc.name, _last_: 'any', _readOnly_: 'boolean' }, editable: true };
if (dubEq) options.typecheck = false;
- const script = CompileScript(value, options);
+ const script = CompileScript(value, { ...options, transformer: DocumentIconContainer.getTransformer() });
return !script.compiled ? undefined : { script, type: dubEq, onDelegate: eq };
}
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);
} else if (type === 'script') {
field = new ScriptField(script);
} else {
- const res = script.run({ this: target }, console.log);
- if (!res.success) return false;
+ const res = script.run({ this: Doc.Layout(doc), self: doc }, console.log);
+ if (!res.success) {
+ target[key] = script.originalScript;
+ return true;
+ }
field = res.result;
}
- if (Field.IsField(field, true)) {
+ if (Field.IsField(field, true) && (key !== 'proto' || field !== target)) {
target[key] = field;
return true;
}
@@ -132,7 +145,9 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
const rows: JSX.Element[] = [];
let i = 0;
const self = this;
- for (const key of Object.keys(ids).slice().sort()) {
+ const keys = Object.keys(ids).slice();
+ //for (const key of [...keys.filter(id => id !== 'layout' && !id.includes('_')).sort(), ...keys.filter(id => id === 'layout' || id.includes('_')).sort()]) {
+ for (const key of keys.sort()) {
rows.push(
<KeyValuePair
doc={realDoc}
@@ -147,7 +162,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
if (el) self.rows.push(el);
};
})()}
- keyWidth={100 - this.splitPercentage}
+ keyWidth={100 - this._splitPercentage}
rowStyle={'keyValueBox-' + (i++ % 2 ? 'oddRow' : 'evenRow')}
key={key}
keyName={key}
@@ -165,7 +180,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
this._keyInput.current!.select();
e.stopPropagation();
}}
- style={{ width: `${100 - this.splitPercentage}%` }}>
+ style={{ width: `${100 - this._splitPercentage}%` }}>
<input style={{ width: '100%' }} ref={this._keyInput} type="text" placeholder="Key" />
</td>
<td
@@ -174,7 +189,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
this._valInput.current!.select();
e.stopPropagation();
}}
- style={{ width: `${this.splitPercentage}%` }}>
+ style={{ width: `${this._splitPercentage}%` }}>
<input style={{ width: '100%' }} ref={this._valInput} type="text" placeholder="Value" onKeyDown={this.onEnterKey} />
</td>
</tr>
@@ -184,7 +199,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
@action
onDividerMove = (e: PointerEvent): void => {
const nativeWidth = this._mainCont.current!.getBoundingClientRect();
- this.props.Document.schemaSplitPercentage = Math.max(0, 100 - Math.round(((e.clientX - nativeWidth.left) / nativeWidth.width) * 100));
+ this._splitPercentage = Math.max(0, 100 - Math.round(((e.clientX - nativeWidth.left) / nativeWidth.width) * 100));
};
@action
onDividerUp = (e: PointerEvent): void => {
@@ -198,25 +213,25 @@ 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([], { _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) => {
const metaKey = row.props.keyName;
- const fieldTemplate = Doc.IsDelegateField(templateDoc, metaKey) ? Doc.MakeDelegate(templateDoc) : Doc.MakeAlias(templateDoc);
+ const fieldTemplate = Doc.IsDelegateField(templateDoc, metaKey) ? Doc.MakeDelegate(templateDoc) : Doc.MakeEmbedding(templateDoc);
fieldTemplate.title = metaKey;
- fieldTemplate.fitWidth = true;
+ fieldTemplate.layout_fitWidth = true;
fieldTemplate._xMargin = 10;
fieldTemplate._yMargin = 10;
fieldTemplate._width = 100;
@@ -259,8 +274,8 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
openItems.push({
description: 'Default Perspective',
event: () => {
- this.props.addDocTab(this.props.Document, 'close');
- this.props.addDocTab(this.fieldDocToLayout, 'add:right');
+ this.props.addDocTab(this.props.Document, OpenWhere.close);
+ this.props.addDocTab(this.fieldDocToLayout, OpenWhere.addRight);
},
icon: 'image',
});
@@ -269,8 +284,8 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
render() {
const dividerDragger =
- this.splitPercentage === 0 ? null : (
- <div className="keyValueBox-dividerDragger" style={{ transform: `translate(calc(${100 - this.splitPercentage}% - 5px), 0px)` }}>
+ this._splitPercentage === 0 ? null : (
+ <div className="keyValueBox-dividerDragger" style={{ transform: `translate(calc(${100 - this._splitPercentage}% - 5px), 0px)` }}>
<div className="keyValueBox-dividerDraggerThumb" onPointerDown={this.onDividerDown} />
</div>
);
@@ -280,10 +295,10 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
<table className="keyValueBox-table">
<tbody className="keyValueBox-tbody">
<tr className="keyValueBox-header">
- <th className="keyValueBox-key" style={{ width: `${100 - this.splitPercentage}%` }} ref={this._keyHeader} onPointerDown={SetupDrag(this._keyHeader, this.getFieldView)}>
+ <th className="keyValueBox-key" style={{ width: `${100 - this._splitPercentage}%` }} ref={this._keyHeader} onPointerDown={SetupDrag(this._keyHeader, this.getFieldView)}>
Key
</th>
- <th className="keyValueBox-fields" style={{ width: `${this.splitPercentage}%` }}>
+ <th className="keyValueBox-fields" style={{ width: `${this._splitPercentage}%` }}>
Fields
</th>
</tr>
diff --git a/src/client/views/nodes/KeyValuePair.scss b/src/client/views/nodes/KeyValuePair.scss
index 5b660e582..57d36932e 100644
--- a/src/client/views/nodes/KeyValuePair.scss
+++ b/src/client/views/nodes/KeyValuePair.scss
@@ -1,60 +1,52 @@
-@import "../global/globalCssVariables";
-
+@import '../global/globalCssVariables';
.keyValuePair-td-key {
- display:inline-block;
+ display: inline-block;
- .keyValuePair-td-key-container{
- width:100%;
- height:100%;
- display: flex;
- flex-direction: row;
- flex-wrap: nowrap;
- justify-content: space-between;
- align-items: center;
- .keyValuePair-td-key-delete{
+ .keyValuePair-td-key-container {
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ justify-content: space-between;
+ .keyValuePair-td-key-delete {
position: relative;
background-color: transparent;
- color:red;
+ color: red;
}
.keyValuePair-td-key-check {
position: relative;
margin: 0;
}
.keyValuePair-keyField {
- width:100%;
+ width: 100%;
margin-left: 20px;
- margin-top: -1px;
font-family: monospace;
- // text-align: center;
- align-self: center;
position: relative;
overflow: auto;
+ display: inline;
}
}
}
.keyValuePair-td-value {
- display:inline-block;
+ display: inline-block;
overflow: scroll;
font-family: monospace;
height: 30px;
- .keyValuePair-td-value-container {
- display: flex;
- align-items: center;
- align-content: center;
- flex-direction: row;
- justify-content: space-between;
- flex-wrap: nowrap;
- width: 100%;
- height: 100%;
+ .keyValuePair-td-value-container {
+ display: inline;
+ justify-content: space-between;
+ width: 100%;
+ height: 100%;
- img {
- max-height: 36px;
- width: auto;
- }
- .videoBox-cont{
- width: auto;
- max-height: 36px;
- }
+ img {
+ max-height: 36px;
+ width: auto;
+ }
+ .videoBox-cont {
+ width: auto;
+ max-height: 36px;
+ }
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx
index 80def3025..01acdccb7 100644
--- a/src/client/views/nodes/KeyValuePair.tsx
+++ b/src/client/views/nodes/KeyValuePair.tsx
@@ -1,18 +1,19 @@
import { action, observable } from 'mobx';
-import { observer } from "mobx-react";
-import { Doc, Field, Opt } from '../../../fields/Doc';
-import { emptyFunction, returnFalse, returnOne, returnZero, returnEmptyFilter, returnEmptyDoclist, emptyPath } from '../../../Utils';
-import { Docs } from '../../documents/Documents';
+import { observer } from 'mobx-react';
+import { Doc, Field } from '../../../fields/Doc';
+import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnZero } from '../../../Utils';
import { Transform } from '../../util/Transform';
import { undoBatch } from '../../util/UndoManager';
import { ContextMenu } from '../ContextMenu';
-import { EditableView } from "../EditableView";
+import { EditableView } from '../EditableView';
+import { DefaultStyleProvider } from '../StyleProvider';
+import { OpenWhere } from './DocumentView';
import { FieldView, FieldViewProps } from './FieldView';
import { KeyValueBox } from './KeyValueBox';
-import "./KeyValueBox.scss";
-import "./KeyValuePair.scss";
-import React = require("react");
-import { DefaultStyleProvider } from '../StyleProvider';
+import './KeyValueBox.scss';
+import './KeyValuePair.scss';
+import React = require('react');
+import { DocCast } from '../../../fields/Types';
// Represents one row in a key value plane
@@ -23,7 +24,7 @@ export interface KeyValuePairProps {
keyWidth: number;
PanelHeight: () => number;
PanelWidth: () => number;
- addDocTab: (doc: Doc, where: string) => boolean;
+ addDocTab: (doc: Doc, where: OpenWhere) => boolean;
}
@observer
export class KeyValuePair extends React.Component<KeyValuePairProps> {
@@ -34,41 +35,38 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
@action
handleCheck = (e: React.ChangeEvent<HTMLInputElement>) => {
this.isChecked = e.currentTarget.checked;
- }
+ };
@action
uncheck = () => {
this.checkbox.current!.checked = false;
this.isChecked = false;
- }
+ };
onContextMenu = (e: React.MouseEvent) => {
const value = this.props.doc[this.props.keyName];
if (value instanceof Doc) {
e.stopPropagation();
e.preventDefault();
- ContextMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(value, { _width: 300, _height: 300 }), "add:right"), icon: "layer-group" });
+ ContextMenu.Instance.addItem({ description: 'Open Fields', event: () => this.props.addDocTab(value, ((OpenWhere.addRight as string) + 'KeyValue') as OpenWhere), icon: 'layer-group' });
ContextMenu.Instance.displayMenu(e.clientX, e.clientY);
}
- }
+ };
render() {
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,
- ContainingCollectionView: undefined,
- ContainingCollectionDoc: undefined,
fieldKey: this.props.keyName,
rootSelected: returnFalse,
isSelected: returnFalse,
setHeight: returnFalse,
select: emptyFunction,
- dropAction: "alias",
bringToFront: emptyFunction,
renderDepth: 1,
isContentActive: returnFalse,
@@ -89,47 +87,41 @@ 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";
+ const keyStyle = protoCount === 0 ? 'black' : 'blue';
- const hover = { transition: "0.3s ease opacity", opacity: this.isPointerOver || this.isChecked ? 1 : 0 };
+ const hover = { transition: '0.3s ease opacity', opacity: this.isPointerOver || this.isChecked ? 1 : 0 };
return (
- <tr className={this.props.rowStyle} onPointerEnter={action(() => this.isPointerOver = true)} onPointerLeave={action(() => this.isPointerOver = false)}>
+ <tr className={this.props.rowStyle} onPointerEnter={action(() => (this.isPointerOver = true))} onPointerLeave={action(() => (this.isPointerOver = false))}>
<td className="keyValuePair-td-key" style={{ width: `${this.props.keyWidth}%` }}>
<div className="keyValuePair-td-key-container">
- <button style={hover} className="keyValuePair-td-key-delete" onClick={undoBatch(() => {
- if (Object.keys(props.Document).indexOf(props.fieldKey) !== -1) {
- delete props.Document[props.fieldKey];
- }
- else delete props.Document.proto![props.fieldKey];
- })}>
+ <button
+ style={hover}
+ className="keyValuePair-td-key-delete"
+ onClick={undoBatch(() => {
+ if (Object.keys(props.Document).indexOf(props.fieldKey) !== -1) {
+ delete props.Document[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" style={{ color: keyStyle }}>{"(".repeat(parenCount)}{props.fieldKey}{")".repeat(parenCount)}</div>
+ <input className="keyValuePair-td-key-check" type="checkbox" style={hover} onChange={this.handleCheck} ref={this.checkbox} />
+ <div className="keyValuePair-keyField" style={{ marginLeft: 20 * (props.fieldKey.match(/_/g)?.length || 0), color: keyStyle }}>
+ {'('.repeat(parenCount)}
+ {props.fieldKey}
+ {')'.repeat(parenCount)}
+ </div>
</div>
</td>
<td className="keyValuePair-td-value" style={{ width: `${100 - this.props.keyWidth}%` }} onContextMenu={this.onContextMenu}>
<div className="keyValuePair-td-value-container">
- <EditableView
- contents={contents}
- maxHeight={36}
- height={"auto"}
- GetValue={() => Field.toKeyValueString(props.Document, props.fieldKey)}
- SetValue={(value: string) =>
- KeyValueBox.SetField(props.Document, props.fieldKey, value)} />
+ <EditableView contents={contents} GetValue={() => Field.toKeyValueString(props.Document, props.fieldKey)} SetValue={(value: string) => KeyValueBox.SetField(props.Document, props.fieldKey, value)} />
</div>
</td>
</tr>
);
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx
index b58a9affb..4439be0cd 100644
--- a/src/client/views/nodes/LabelBox.tsx
+++ b/src/client/views/nodes/LabelBox.tsx
@@ -15,16 +15,17 @@ import { FieldView, FieldViewProps } from './FieldView';
import BigText from './LabelBigText';
import './LabelBox.scss';
-
export interface LabelBoxProps {
label?: string;
}
@observer
-export class LabelBox extends ViewBoxBaseComponent<(FieldViewProps & LabelBoxProps)>() {
- public static LayoutString(fieldKey: string) { return FieldView.LayoutString(LabelBox, fieldKey); }
+export class LabelBox extends ViewBoxBaseComponent<FieldViewProps & LabelBoxProps>() {
+ public static LayoutString(fieldKey: string) {
+ return FieldView.LayoutString(LabelBox, fieldKey);
+ }
public static LayoutStringWithTitle(fieldStr: string, label?: string) {
- return !label ? LabelBox.LayoutString(fieldStr) : `<LabelBox fieldKey={'${fieldStr}'} label={'${label}'} {...props} />`; //e.g., "<ImageBox {...props} fieldKey={"data} />"
+ return !label ? LabelBox.LayoutString(fieldStr) : `<LabelBox fieldKey={'${fieldStr}'} label={'${label}'} {...props} />`; //e.g., "<ImageBox {...props} fieldKey={"data} />"
}
private dropDisposer?: DragManager.DragDropDisposer;
private _timeout: any;
@@ -36,10 +37,7 @@ export class LabelBox extends ViewBoxBaseComponent<(FieldViewProps & LabelBoxPro
}
getTitle() {
- return this.rootDoc["title-custom"] ? StrCast(this.rootDoc.title) :
- this.props.label ? this.props.label :
- typeof this.rootDoc[this.fieldKey] === "string" ? StrCast(this.rootDoc[this.fieldKey]) :
- StrCast(this.rootDoc.title);
+ return this.rootDoc.title_custom ? StrCast(this.rootDoc.title) : this.props.label ? this.props.label : typeof this.rootDoc[this.fieldKey] === 'string' ? StrCast(this.rootDoc[this.fieldKey]) : StrCast(this.rootDoc.title);
}
protected createDropTarget = (ele: HTMLDivElement) => {
@@ -47,101 +45,120 @@ export class LabelBox extends ViewBoxBaseComponent<(FieldViewProps & LabelBoxPro
if (ele) {
this.dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.props.Document);
}
- }
+ };
- get paramsDoc() { return Doc.AreProtosEqual(this.layoutDoc, this.dataDoc) ? this.dataDoc : this.layoutDoc; }
+ get paramsDoc() {
+ return Doc.AreProtosEqual(this.layoutDoc, this.dataDoc) ? this.dataDoc : this.layoutDoc;
+ }
specificContextMenu = (e: React.MouseEvent): void => {
const funcs: ContextMenuProps[] = [];
- !Doc.noviceMode && funcs.push({
- description: "Clear Script Params", event: () => {
- const params = Cast(this.paramsDoc["onClick-paramFieldKeys"], listSpec("string"), []);
- params?.map(p => this.paramsDoc[p] = undefined);
- }, icon: "trash"
- });
+ !Doc.noviceMode &&
+ funcs.push({
+ description: 'Clear Script Params',
+ event: () => {
+ const params = Cast(this.paramsDoc['onClick-paramFieldKeys'], listSpec('string'), []);
+ params?.map(p => (this.paramsDoc[p] = undefined));
+ },
+ icon: 'trash',
+ });
- funcs.length && ContextMenu.Instance.addItem({ description: "OnClick...", noexpand: true, subitems: funcs, icon: "mouse-pointer" });
- }
+ funcs.length && ContextMenu.Instance.addItem({ description: 'OnClick...', noexpand: true, subitems: funcs, icon: 'mouse-pointer' });
+ };
@undoBatch
@action
drop = (e: Event, de: DragManager.DropEvent) => {
const docDragData = de.complete.docDragData;
- const params = Cast(this.paramsDoc["onClick-paramFieldKeys"], listSpec("string"), []);
+ const params = Cast(this.paramsDoc['onClick-paramFieldKeys'], listSpec('string'), []);
const missingParams = params?.filter(p => !this.paramsDoc[p]);
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));
+ 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;
- @computed get hoverColor() { return this._mouseOver ? StrCast(this.layoutDoc._hoverBackgroundColor) : "unset"; }
+ @computed get hoverColor() {
+ return this._mouseOver ? StrCast(this.layoutDoc._hoverBackgroundColor) : this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor);
+ }
- fitTextToBox = (r: any): any => {
+ fitTextToBox = (r: any) => {
const singleLine = BoolCast(this.rootDoc._singleLine, true);
const params = {
rotateText: null,
fontSizeFactor: 1,
- minimumFontSize: NumCast(this.rootDoc._minFontSize, 8),
- maximumFontSize: NumCast(this.rootDoc._maxFontSize, 1000),
- limitingDimension: "both",
- horizontalAlign: "center",
- verticalAlign: "center",
- textAlign: "center",
+ minimumFontSize: NumCast(this.rootDoc._label_minFontSize, 8),
+ maximumFontSize: NumCast(this.rootDoc._label_maxFontSize, 1000),
+ limitingDimension: 'both',
+ horizontalAlign: 'center',
+ verticalAlign: 'center',
+ textAlign: 'center',
singleLine,
- whiteSpace: singleLine ? "nowrap" : "pre-wrap"
+ whiteSpace: singleLine ? 'nowrap' : 'pre-wrap',
};
this._timeout = undefined;
if (!r) return params;
- if (!r.offsetHeight || !r.offsetWidth) return this._timeout = setTimeout(() => this.fitTextToBox(r));
+ if (!r.offsetHeight || !r.offsetWidth) return (this._timeout = setTimeout(() => this.fitTextToBox(r)));
const parent = r.parentNode;
const parentStyle = parent.style;
- parentStyle.display = "";
- parentStyle.alignItems = "";
- r.setAttribute("style", "");
- r.style.width = singleLine ? "" : "100%";
+ parentStyle.display = '';
+ parentStyle.alignItems = '';
+ r.setAttribute('style', '');
+ r.style.width = singleLine ? '' : '100%';
- r.style.textOverflow = "ellipsis";
- r.style.overflow = "hidden";
+ r.style.textOverflow = 'ellipsis';
+ r.style.overflow = 'hidden';
BigText(r, params);
return params;
- }
+ };
// (!missingParams || !missingParams.length ? "" : "(" + missingParams.map(m => m + ":").join(" ") + ")")
render() {
- const boxParams = this.fitTextToBox(null);// this causes mobx to trigger re-render when data changes
- const params = Cast(this.paramsDoc["onClick-paramFieldKeys"], listSpec("string"), []);
+ const boxParams = this.fitTextToBox(null); // this causes mobx to trigger re-render when data changes
+ const params = Cast(this.paramsDoc['onClick-paramFieldKeys'], listSpec('string'), []);
const missingParams = params?.filter(p => !this.paramsDoc[p]);
params?.map(p => DocListCast(this.paramsDoc[p])); // bcz: really hacky form of prefetching ...
const label = this.getTitle();
return (
- <div className="labelBox-outerDiv"
- onMouseLeave={action(() => this._mouseOver = false)}
- onMouseOver={action(() => this._mouseOver = true)}
- ref={this.createDropTarget} onContextMenu={this.specificContextMenu}
+ <div
+ className="labelBox-outerDiv"
+ onMouseLeave={action(() => (this._mouseOver = false))}
+ onMouseOver={action(() => (this._mouseOver = true))}
+ ref={this.createDropTarget}
+ onContextMenu={this.specificContextMenu}
style={{ boxShadow: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BoxShadow) }}>
- <div className="labelBox-mainButton" style={{
- backgroundColor: this.hoverColor,
- fontSize: StrCast(this.layoutDoc._fontSize),
- fontFamily: StrCast(this.layoutDoc._fontFamily) || "inherit",
- letterSpacing: StrCast(this.layoutDoc.letterSpacing),
- textTransform: StrCast(this.layoutDoc.textTransform) as any,
- paddingLeft: NumCast(this.rootDoc._xPadding),
- paddingRight: NumCast(this.rootDoc._xPadding),
- paddingTop: NumCast(this.rootDoc._yPadding),
- paddingBottom: NumCast(this.rootDoc._yPadding),
- width: this.props.PanelWidth(),
- height: this.props.PanelHeight(),
- whiteSpace: boxParams.singleLine ? "pre" : "pre-wrap"
- }} >
- <span style={{ width: boxParams.singleLine ? "" : "100%" }} ref={action((r: any) => this.fitTextToBox(r))}>
- {label.startsWith("#") ? (null) : label.replace(/([^a-zA-Z])/g, "$1\u200b")}
+ <div
+ className="labelBox-mainButton"
+ style={{
+ backgroundColor: this.hoverColor,
+ fontSize: StrCast(this.layoutDoc._text_fontSize),
+ color: StrCast(this.layoutDoc._color),
+ fontFamily: StrCast(this.layoutDoc._text_fontFamily) || 'inherit',
+ letterSpacing: StrCast(this.layoutDoc.letterSpacing),
+ textTransform: StrCast(this.layoutDoc.textTransform) as any,
+ paddingLeft: NumCast(this.rootDoc._xPadding),
+ paddingRight: NumCast(this.rootDoc._xPadding),
+ paddingTop: NumCast(this.rootDoc._yPadding),
+ paddingBottom: NumCast(this.rootDoc._yPadding),
+ width: this.props.PanelWidth(),
+ height: this.props.PanelHeight(),
+ whiteSpace: typeof boxParams !== 'number' && boxParams.singleLine ? 'pre' : 'pre-wrap',
+ }}>
+ <span style={{ width: typeof boxParams !== 'number' && boxParams.singleLine ? '' : '100%' }} ref={action((r: any) => this.fitTextToBox(r))}>
+ {label.startsWith('#') ? null : label.replace(/([^a-zA-Z])/g, '$1\u200b')}
</span>
</div>
- <div className="labelBox-fieldKeyParams" >
- {!missingParams?.length ? (null) : missingParams.map(m => <div key={m} className="labelBox-missingParam">{m}</div>)}
+ <div className="labelBox-fieldKeyParams">
+ {!missingParams?.length
+ ? null
+ : missingParams.map(m => (
+ <div key={m} className="labelBox-missingParam">
+ {m}
+ </div>
+ ))}
</div>
</div>
);
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx
index 5102eae51..9bcd04cf5 100644
--- a/src/client/views/nodes/LinkAnchorBox.tsx
+++ b/src/client/views/nodes/LinkAnchorBox.tsx
@@ -1,25 +1,19 @@
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, observable } from 'mobx';
+import { action, computed, observable } from 'mobx';
import { observer } from 'mobx-react';
import { Doc } from '../../../fields/Doc';
-import { Cast, NumCast, StrCast } from '../../../fields/Types';
+import { NumCast, StrCast } from '../../../fields/Types';
import { TraceMobx } from '../../../fields/util';
import { emptyFunction, setupMoveUpEvents, Utils } from '../../../Utils';
import { DragManager } from '../../util/DragManager';
import { LinkFollower } from '../../util/LinkFollower';
import { SelectionManager } from '../../util/SelectionManager';
-import { ContextMenu } from '../ContextMenu';
-import { ContextMenuProps } from '../ContextMenuItem';
import { ViewBoxBaseComponent } from '../DocComponent';
-import { LinkEditor } from '../linking/LinkEditor';
import { StyleProp } from '../StyleProvider';
import { FieldView, FieldViewProps } from './FieldView';
import './LinkAnchorBox.scss';
import { LinkDocPreview } from './LinkDocPreview';
import React = require('react');
-const higflyout = require('@hig/flyout');
-export const { anchorPoints } = higflyout;
-export const Flyout = higflyout.default;
+import globalCssVariables = require('../global/globalCssVariables.scss');
@observer
export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() {
@@ -33,138 +27,80 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() {
_timeout: NodeJS.Timeout | undefined;
@observable _x = 0;
@observable _y = 0;
- @observable _selected = false;
- @observable _editing = false;
- @observable _forceOpen = false;
+
+ @computed get linkSource() {
+ return this.props.docViewPath()[this.props.docViewPath().length - 2].rootDoc; // this.props.styleProvider?.(this.dataDoc, this.props, StyleProp.LinkSource);
+ }
onPointerDown = (e: React.PointerEvent) => {
- setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, emptyFunction, false);
+ setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, (e, doubleTap) => {
+ if (doubleTap) LinkFollower.FollowLink(this.rootDoc, this.linkSource, false);
+ else this.props.select(false);
+ });
};
onPointerMove = action((e: PointerEvent, down: number[], delta: number[]) => {
- const cdiv = this._ref && this._ref.current && this._ref.current.parentElement;
+ const cdiv = this._ref?.current?.parentElement;
if (!this._isOpen && cdiv) {
const bounds = cdiv.getBoundingClientRect();
const pt = Utils.getNearestPointInPerimeter(bounds.left, bounds.top, bounds.width, bounds.height, e.clientX, e.clientY);
const separation = Math.sqrt((pt[0] - e.clientX) * (pt[0] - e.clientX) + (pt[1] - e.clientY) * (pt[1] - e.clientY));
if (separation > 100) {
const dragData = new DragManager.DocumentDragData([this.rootDoc]);
- dragData.dropAction = 'alias';
- dragData.removeDropProperties = ['anchor1_x', 'anchor1_y', 'anchor2_x', 'anchor2_y', 'isLinkButton'];
+ dragData.dropAction = 'embed';
+ 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.link_autoMoveAnchors = false;
}
}
return false;
});
- @action
- onClick = (e: React.MouseEvent) => {
- if (e.button === 2 || e.ctrlKey || !this.layoutDoc.isLinkButton) {
- this.props.select(false);
- }
- if (!this._doubleTap && !e.ctrlKey && e.button < 2) {
- const anchorContainerDoc = this.props.styleProvider?.(this.dataDoc, this.props, StyleProp.LinkSource);
- this._editing = true;
- anchorContainerDoc && this.props.bringToFront(anchorContainerDoc, false);
- if (anchorContainerDoc && !this.layoutDoc.onClick && !this._isOpen) {
- this._timeout = setTimeout(
- action(() => {
- LinkFollower.FollowLink(this.rootDoc, anchorContainerDoc, this.props, false);
- this._editing = false;
- }),
- 300 - (Date.now() - this._lastTap)
- );
- e.stopPropagation();
- }
- } else {
- this._timeout && clearTimeout(this._timeout);
- this._timeout = undefined;
- this._doubleTap = false;
- this.openLinkEditor(e);
- e.stopPropagation();
- }
- };
-
- openLinkDocOnRight = (e: React.MouseEvent) => {
- this.props.addDocTab(this.rootDoc, 'add:right');
- };
- openLinkTargetOnRight = (e: React.MouseEvent) => {
- const alias = Doc.MakeAlias(Cast(this.layoutDoc[this.fieldKey], Doc, null));
- alias._isLinkButton = undefined;
- alias.layoutKey = 'layout';
- this.props.addDocTab(alias, 'add:right');
- };
- @action
- openLinkEditor = action((e: React.MouseEvent) => {
- SelectionManager.DeselectAll();
- this._editing = this._forceOpen = true;
- });
-
- specificContextMenu = (e: React.MouseEvent): void => {
- const funcs: ContextMenuProps[] = [];
- funcs.push({ description: 'Open Link Target on Right', event: () => this.openLinkTargetOnRight(e), icon: 'eye' });
- funcs.push({ description: 'Open Link on Right', event: () => this.openLinkDocOnRight(e), icon: 'eye' });
- funcs.push({ description: 'Open Link Editor', event: () => this.openLinkEditor(e), icon: 'eye' });
- funcs.push({ description: 'Toggle Always Show Link', event: () => (this.props.Document.linkDisplay = !this.props.Document.linkDisplay), icon: 'eye' });
- ContextMenu.Instance.addItem({ description: 'Options...', subitems: funcs, icon: 'asterisk' });
- };
+ specificContextMenu = (e: React.MouseEvent): void => {};
render() {
TraceMobx();
const small = this.props.PanelWidth() <= 1; // this happens when rendered in a treeView
const x = NumCast(this.rootDoc[this.fieldKey + '_x'], 100);
const y = NumCast(this.rootDoc[this.fieldKey + '_y'], 100);
- const linkSource = this.props.styleProvider?.(this.dataDoc, this.props, StyleProp.LinkSource);
const background = this.props.styleProvider?.(this.dataDoc, this.props, StyleProp.BackgroundColor + ':anchor');
- const anchor = this.fieldKey === 'anchor1' ? 'anchor2' : 'anchor1';
- const anchorScale = !this.dataDoc[this.fieldKey + '-useLinkSmallAnchor'] && (x === 0 || x === 100 || y === 0 || y === 100) ? 1 : 0.25;
-
+ const anchor = this.fieldKey === 'link_anchor_1' ? 'link_anchor_2' : 'link_anchor_1';
+ 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 flyout = (
- <div className="linkAnchorBoxBox-flyout" title=" " onPointerOver={() => Doc.UnBrushDoc(this.rootDoc)}>
- <LinkEditor sourceDoc={Cast(this.dataDoc[this.fieldKey], Doc, null)} hideback={true} linkDoc={this.rootDoc} showLinks={action(() => {})} />
- {!this._forceOpen ? null : (
- <div className="linkAnchorBox-linkCloser" onPointerDown={action(() => (this._isOpen = this._editing = this._forceOpen = false))}>
- <FontAwesomeIcon color="dimgray" icon={'times'} size={'sm'} />
- </div>
- )}
- </div>
- );
+ const selView = SelectionManager.Views().lastElement()?.props.LayoutTemplateString?.includes('link_anchor_1')
+ ? 'link_anchor_1'
+ : SelectionManager.Views().lastElement()?.props.LayoutTemplateString?.includes('link_anchor_2')
+ ? 'link_anchor_2'
+ : '';
return (
<div
+ ref={this._ref}
+ title={targetTitle}
className={`linkAnchorBox-cont${small ? '-small' : ''}`}
- //onPointerLeave={} //LinkDocPreview.Clear}
onPointerEnter={e =>
LinkDocPreview.SetLinkInfo({
docProps: this.props,
- linkSrc: linkSource,
+ linkSrc: this.linkSource,
linkDoc: this.rootDoc,
showHeader: true,
location: [e.clientX, e.clientY + 20],
+ noPreview: false,
})
}
onPointerDown={this.onPointerDown}
- onClick={this.onClick}
- title={targetTitle}
onContextMenu={this.specificContextMenu}
- ref={this._ref}
style={{
+ border: selView && this.rootDoc[selView] === this.rootDoc[this.fieldKey] ? `solid ${globalCssVariables.MEDIUM_GRAY} 2px` : undefined,
background,
left: `calc(${x}% - ${small ? 2.5 : 7.5}px)`,
top: `calc(${y}% - ${small ? 2.5 : 7.5}px)`,
transform: `scale(${anchorScale})`,
- }}>
- {!this._editing && !this._forceOpen ? null : (
- <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={flyout} open={this._forceOpen ? true : undefined} onOpen={() => (this._isOpen = true)} onClose={action(() => (this._isOpen = this._forceOpen = this._editing = false))}>
- <span className="linkAnchorBox-button">
- <FontAwesomeIcon icon={'eye'} size={'lg'} />
- </span>
- </Flyout>
- )}
- </div>
+ cursor: 'grab',
+ }}
+ />
);
}
}
diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx
index 43f4b43fb..710d41471 100644
--- a/src/client/views/nodes/LinkBox.tsx
+++ b/src/client/views/nodes/LinkBox.tsx
@@ -1,29 +1,38 @@
-import React = require("react");
-import { observer } from "mobx-react";
-import { emptyFunction, returnFalse } from "../../../Utils";
-import { ViewBoxBaseComponent } from "../DocComponent";
-import { StyleProp } from "../StyleProvider";
-import { ComparisonBox } from "./ComparisonBox";
+import React = require('react');
+import { observer } from 'mobx-react';
+import { emptyFunction, returnAlways, returnFalse, returnTrue } from '../../../Utils';
+import { ViewBoxBaseComponent } from '../DocComponent';
+import { StyleProp } from '../StyleProvider';
+import { ComparisonBox } from './ComparisonBox';
import { FieldView, FieldViewProps } from './FieldView';
-import "./LinkBox.scss";
+import './LinkBox.scss';
@observer
export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
- public static LayoutString(fieldKey: string) { return FieldView.LayoutString(LinkBox, fieldKey); }
- isContentActiveFunc = () => this.isContentActive();
+ public static LayoutString(fieldKey: string = 'link') {
+ return FieldView.LayoutString(LinkBox, fieldKey);
+ }
+
+ onClickScriptDisable = returnAlways;
+ componentDidMount() {
+ this.props.setContentView?.(this);
+ }
render() {
- if (this.dataDoc.treeViewOpen === undefined) setTimeout(() => this.dataDoc.treeViewOpen = true);
- return <div className={`linkBox-container${this.isContentActive() ? "-interactive" : ""}`}
- style={{ background: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor) }} >
- <ComparisonBox {...this.props}
- fieldKey="anchor"
- setHeight={emptyFunction}
- dontRegisterView={true}
- renderDepth={this.props.renderDepth + 1}
- isContentActive={this.isContentActiveFunc}
- addDocument={returnFalse}
- removeDocument={returnFalse}
- moveDocument={returnFalse} />
- </div>;
+ if (this.dataDoc.treeViewOpen === undefined) setTimeout(() => (this.dataDoc.treeViewOpen = true));
+ return (
+ <div className={`linkBox-container${this.props.isContentActive() ? '-interactive' : ''}`} style={{ background: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor) }}>
+ <ComparisonBox
+ {...this.props}
+ fieldKey="link_anchor"
+ setHeight={emptyFunction}
+ dontRegisterView={true}
+ renderDepth={this.props.renderDepth + 1}
+ isContentActive={this.props.isContentActive}
+ addDocument={returnFalse}
+ removeDocument={returnFalse}
+ moveDocument={returnFalse}
+ />
+ </div>
+ );
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/nodes/LinkDescriptionPopup.tsx b/src/client/views/nodes/LinkDescriptionPopup.tsx
index 91bd505c5..c45045a8a 100644
--- a/src/client/views/nodes/LinkDescriptionPopup.tsx
+++ b/src/client/views/nodes/LinkDescriptionPopup.tsx
@@ -24,7 +24,7 @@ export class LinkDescriptionPopup extends React.Component<{}> {
onDismiss = (add: boolean) => {
LinkDescriptionPopup.descriptionPopup = false;
if (add) {
- LinkManager.currentLink && (Doc.GetProto(LinkManager.currentLink).description = this.description);
+ LinkManager.currentLink && (Doc.GetProto(LinkManager.currentLink).link_description = this.description);
}
};
diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx
index 27e79a83b..86191de63 100644
--- a/src/client/views/nodes/LinkDocPreview.tsx
+++ b/src/client/views/nodes/LinkDocPreview.tsx
@@ -3,21 +3,22 @@ import { Tooltip } from '@material-ui/core';
import { action, computed, observable } from 'mobx';
import { observer } from 'mobx-react';
import wiki from 'wikijs';
-import { Doc, DocCastAsync, DocListCast, HeightSym, Opt, WidthSym } from '../../../fields/Doc';
-import { Cast, NumCast, StrCast } from '../../../fields/Types';
+import { Doc, DocCastAsync, Opt } from '../../../fields/Doc';
+import { Height, Width } from '../../../fields/DocSymbols';
+import { Cast, DocCast, NumCast, PromiseValue, StrCast } from '../../../fields/Types';
import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnNone, setupMoveUpEvents } from '../../../Utils';
import { DocServer } from '../../DocServer';
-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';
import { LinkManager } from '../../util/LinkManager';
+import { SettingsManager } from '../../util/SettingsManager';
import { Transform } from '../../util/Transform';
-import { undoBatch } from '../../util/UndoManager';
-import { DocumentView, DocumentViewSharedProps } from './DocumentView';
+import { SearchBox } from '../search/SearchBox';
+import { DocumentView, DocumentViewSharedProps, OpenWhere } from './DocumentView';
import './LinkDocPreview.scss';
import React = require('react');
-import { LinkEditor } from '../linking/LinkEditor';
interface LinkDocPreviewProps {
linkDoc?: Doc;
@@ -26,6 +27,7 @@ interface LinkDocPreviewProps {
location: number[];
hrefs?: string[];
showHeader?: boolean;
+ noPreview?: boolean;
}
@observer
export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
@@ -36,31 +38,40 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
LinkDocPreview.LinkInfo !== info && (LinkDocPreview.LinkInfo = info);
}
+ static _instance: Opt<LinkDocPreview>;
+
_infoRef = React.createRef<HTMLDivElement>();
_linkDocRef = React.createRef<HTMLDivElement>();
@observable public static LinkInfo: Opt<LinkDocPreviewProps>;
@observable _targetDoc: Opt<Doc>;
+ @observable _markerTargetDoc: Opt<Doc>;
@observable _linkDoc: Opt<Doc>;
@observable _linkSrc: Opt<Doc>;
@observable _toolTipText = '';
@observable _hrefInd = 0;
+ constructor(props: any) {
+ super(props);
+ LinkDocPreview._instance = this;
+ }
+
@action init() {
var linkTarget = this.props.linkDoc;
this._linkSrc = this.props.linkSrc;
this._linkDoc = this.props.linkDoc;
- const anchor1 = this._linkDoc?.anchor1 as Doc;
- const anchor2 = this._linkDoc?.anchor2 as Doc;
- if (anchor1 && anchor2) {
- linkTarget = Doc.AreProtosEqual(anchor1, this._linkSrc) || Doc.AreProtosEqual(anchor1?.annotationOn as Doc, this._linkSrc) ? anchor2 : anchor1;
+ const link_anchor_1 = this._linkDoc?.link_anchor_1 as Doc;
+ const link_anchor_2 = this._linkDoc?.link_anchor_2 as Doc;
+ if (link_anchor_1 && link_anchor_2) {
+ linkTarget = Doc.AreProtosEqual(link_anchor_1, this._linkSrc) || Doc.AreProtosEqual(link_anchor_1?.annotationOn as Doc, this._linkSrc) ? link_anchor_2 : link_anchor_1;
}
if (linkTarget?.annotationOn && linkTarget?.type !== DocumentType.RTF) {
- // want to show annotation context document if annotation is not text
- linkTarget && DocCastAsync(linkTarget.annotationOn).then(action(anno => (this._targetDoc = anno)));
+ // want to show annotation embedContainer document if annotation is not text
+ linkTarget && DocCastAsync(linkTarget.annotationOn).then(action(anno => (this._markerTargetDoc = this._targetDoc = anno)));
} else {
- this._targetDoc = linkTarget;
+ this._markerTargetDoc = this._targetDoc = linkTarget;
}
this._toolTipText = '';
+ this.updateHref();
}
componentDidUpdate(props: any) {
if (props.linkSrc !== this.props.linkSrc || props.linkDoc !== this.props.linkDoc || props.hrefs !== this.props.hrefs) this.init();
@@ -70,6 +81,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
document.addEventListener('pointerdown', this.onPointerDown, true);
}
+ @action
componentWillUnmount() {
LinkDocPreview.SetLinkInfo(undefined);
document.removeEventListener('pointerdown', this.onPointerDown, true);
@@ -79,7 +91,8 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
!this._linkDocRef.current?.contains(e.target as any) && LinkDocPreview.Clear(); // close preview when not clicking anywhere other than the info bar of the preview
};
- @computed get href() {
+ @action
+ updateHref() {
if (this.props.hrefs?.length) {
const href = this.props.hrefs[this._hrefInd];
if (href.indexOf(Doc.localServerPath()) !== 0) {
@@ -89,45 +102,53 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
.page(href.replace('https://en.wikipedia.org/wiki/', ''))
.then(page => page.summary().then(action(summary => (this._toolTipText = summary.substring(0, 500)))));
} else {
- setTimeout(action(() => (this._toolTipText = 'url => ' + href)));
+ this._toolTipText = 'url => ' + href;
}
} else {
// hyperlink to a document .. decode doc id and retrieve from the server. this will trigger vals() being invalidated
- const anchorDoc = href.replace(Doc.localServerPath(), '').split('?')[0];
- anchorDoc &&
- DocServer.GetRefField(anchorDoc).then(
- action(anchor => {
- if (anchor instanceof Doc && DocListCast(anchor.links).length) {
- this._linkDoc = this._linkDoc ?? DocListCast(anchor.links)[0];
- const automaticLink = this._linkDoc.linkRelationship === LinkManager.AutoKeywords;
- if (automaticLink) {
- // automatic links specify the target in the link info, not the source
- const linkTarget = anchor;
- this._linkSrc = LinkManager.getOppositeAnchor(this._linkDoc, linkTarget);
- this._targetDoc = linkTarget;
- } else {
- this._linkSrc = anchor;
- const linkTarget = LinkManager.getOppositeAnchor(this._linkDoc, this._linkSrc);
- this._targetDoc = /*linkTarget?.type === DocumentType.MARKER &&*/ linkTarget?.annotationOn ? Cast(linkTarget.annotationOn, Doc, null) ?? linkTarget : linkTarget;
- }
- this._toolTipText = '';
+ const anchorDocId = href.replace(Doc.localServerPath(), '').split('?')[0];
+ const anchorDoc = anchorDocId ? PromiseValue(DocCast(DocServer.GetCachedRefField(anchorDocId) ?? DocServer.GetRefField(anchorDocId))) : undefined;
+ anchorDoc?.then?.(
+ action(anchor => {
+ if (anchor instanceof Doc && LinkManager.Links(anchor).length) {
+ this._linkDoc = this._linkDoc ?? LinkManager.Links(anchor)[0];
+ const automaticLink = this._linkDoc.link_relationship === LinkManager.AutoKeywords;
+ if (automaticLink) {
+ // automatic links specify the target in the link info, not the source
+ const linkTarget = anchor;
+ this._linkSrc = LinkManager.getOppositeAnchor(this._linkDoc, linkTarget);
+ this._markerTargetDoc = this._targetDoc = linkTarget;
+ } else {
+ this._linkSrc = anchor;
+ const linkTarget = LinkManager.getOppositeAnchor(this._linkDoc, this._linkSrc);
+ this._markerTargetDoc = linkTarget;
+ this._targetDoc = /*linkTarget?.type === DocumentType.MARKER &&*/ linkTarget?.annotationOn ? Cast(linkTarget.annotationOn, Doc, null) ?? linkTarget : linkTarget;
}
- })
- );
+ if (LinkDocPreview.LinkInfo?.noPreview || this._linkSrc?.followLinkToggle || this._markerTargetDoc?.type === DocumentType.PRES) this.followLink();
+ }
+ })
+ );
}
return href;
}
return undefined;
}
- @observable _showEditor = false;
+
+ @action
editLink = (e: React.PointerEvent): void => {
- LinkManager.currentLink = this.props.linkDoc;
setupMoveUpEvents(
this,
e,
returnFalse,
emptyFunction,
- action(() => (this._showEditor = !this._showEditor))
+ action(() => {
+ LinkManager.currentLink = this._linkDoc;
+ LinkManager.currentLinkAnchor = this._linkSrc;
+ this.props.docProps.DocumentView?.().select(false);
+ if ((SettingsManager.propertiesWidth ?? 0) < 100) {
+ SettingsManager.propertiesWidth = 250;
+ }
+ })
);
};
nextHref = (e: React.PointerEvent) => {
@@ -148,11 +169,14 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
};
followLink = () => {
+ LinkDocPreview.Clear();
if (this._linkDoc && this._linkSrc) {
- LinkDocPreview.Clear();
- LinkFollower.FollowLink(this._linkDoc, this._linkSrc, this.props.docProps, false);
+ LinkFollower.FollowLink(this._linkDoc, this._linkSrc, false);
} else if (this.props.hrefs?.length) {
- this.props.docProps?.addDocTab(Docs.Create.WebDocument(this.props.hrefs[0], { title: this.props.hrefs[0], _nativeWidth: 850, _width: 200, _height: 400, useCors: true }), 'add:right');
+ const webDoc =
+ Array.from(SearchBox.staticSearchCollection(Doc.MyFilesystem, this.props.hrefs[0]).keys()).lastElement() ??
+ Docs.Create.WebDocument(this.props.hrefs[0], { title: this.props.hrefs[0], _nativeWidth: 850, _width: 200, _height: 400, data_useCors: true });
+ this.props.docProps?.addDocTab(webDoc, OpenWhere.lightbox);
}
};
@@ -160,44 +184,44 @@ 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._targetDoc || !this._linkSrc ? null : (
+ return !this._linkDoc || !this._markerTargetDoc || !this._targetDoc || !this._linkSrc ? null : (
<div className="linkDocPreview-info">
+ <div className="linkDocPreview-buttonBar" style={{ float: 'left' }}>
+ <Tooltip title={<div className="dash-tooltip">Edit Link</div>} placement="top">
+ <div className="linkDocPreview-button" onPointerDown={this.editLink}>
+ <FontAwesomeIcon className="linkDocPreview-fa-icon" icon="edit" color="white" size="sm" />
+ </div>
+ </Tooltip>
+ </div>
<div className="linkDocPreview-title" style={{ pointerEvents: 'all' }}>
- {StrCast(this._targetDoc.title).length > 16 ? StrCast(this._targetDoc.title).substr(0, 16) + '...' : StrCast(this._targetDoc.title)}
- <p className="linkDocPreview-description"> {StrCast(this._linkDoc.description)}</p>
+ {StrCast(this._markerTargetDoc.title).length > 16 ? StrCast(this._markerTargetDoc.title).substr(0, 16) + '...' : StrCast(this._markerTargetDoc.title)}
+ <p className="linkDocPreview-description"> {StrCast(this._linkDoc.link_description)}</p>
</div>
- <div className="linkDocPreview-buttonBar">
+ <div className="linkDocPreview-buttonBar" style={{ float: 'right' }}>
<Tooltip title={<div className="dash-tooltip">Next Link</div>} placement="top">
<div className="linkDocPreview-button" style={{ background: (this.props.hrefs?.length || 0) <= 1 ? 'gray' : 'green' }} onPointerDown={this.nextHref}>
<FontAwesomeIcon className="linkDocPreview-fa-icon" icon="chevron-right" color="white" size="sm" />
</div>
</Tooltip>
-
- <Tooltip title={<div className="dash-tooltip">Edit Link</div>} placement="top">
- <div className="linkDocPreview-button" onPointerDown={this.editLink}>
- <FontAwesomeIcon className="linkDocPreview-fa-icon" icon="edit" color="white" size="sm" />
- </div>
- </Tooltip>
</div>
</div>
);
}
@computed get docPreview() {
- const href = this.href; // needs to be here to trigger lookup of web pages and docs on server
return (!this._linkDoc || !this._targetDoc || !this._linkSrc) && !this._toolTipText ? null : (
<div className="linkDocPreview-inner">
{!this.props.showHeader ? null : this.previewHeader}
@@ -228,7 +252,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
<DocumentView
ref={r => {
const targetanchor = this._linkDoc && this._linkSrc && LinkManager.getOppositeAnchor(this._linkDoc, this._linkSrc);
- targetanchor && this._targetDoc !== targetanchor && r?.focus(targetanchor, {});
+ targetanchor && this._targetDoc !== targetanchor && r?.props.focus?.(targetanchor, {});
}}
Document={this._targetDoc!}
moveDocument={returnFalse}
@@ -239,24 +263,22 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
isDocumentActive={returnFalse}
isContentActive={returnFalse}
addDocument={returnFalse}
- showTitle={returnEmptyString}
+ layout_showTitle={returnEmptyString}
removeDocument={returnFalse}
addDocTab={returnFalse}
pinToPres={returnFalse}
dontRegisterView={true}
- docFilters={returnEmptyFilter}
- docRangeFilters={returnEmptyFilter}
+ childFilters={returnEmptyFilter}
+ childFiltersByRanges={returnEmptyFilter}
searchFilterDocs={returnEmptyDoclist}
- ContainingCollectionDoc={undefined}
- ContainingCollectionView={undefined}
renderDepth={0}
suppressSetHeight={true}
PanelWidth={this.width}
PanelHeight={this.height}
pointerEvents={returnNone}
- focus={DocUtils.DefaultFocus}
+ focus={emptyFunction}
whenChildContentsActiveChanged={returnFalse}
- ignoreAutoHeight={true} // need to ignore autoHeight otherwise autoHeight text boxes will expand beyond the preview panel size.
+ ignoreAutoHeight={true} // need to ignore layout_autoHeight otherwise layout_autoHeight text boxes will expand beyond the preview panel size.
bringToFront={returnFalse}
NativeWidth={Doc.NativeWidth(this._targetDoc) ? () => Doc.NativeWidth(this._targetDoc) : undefined}
NativeHeight={Doc.NativeHeight(this._targetDoc) ? () => Doc.NativeHeight(this._targetDoc) : undefined}
@@ -274,9 +296,8 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
className="linkDocPreview"
ref={this._linkDocRef}
onPointerDown={this.followLinkPointerDown}
- style={{ left: this.props.location[0], top: this.props.location[1], width: this._showEditor ? 'auto' : this.width() + borders, height: this._showEditor ? 'max-content' : this.height() + borders + (this.props.showHeader ? 37 : 0) }}>
- {this._showEditor ? null : this.docPreview}
- {!this._showEditor || !this._linkSrc || !this._linkDoc ? null : <LinkEditor sourceDoc={this._linkSrc} linkDoc={this._linkDoc} showLinks={action(() => (this._showEditor = !this._showEditor))} />}
+ 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 d63ed2575..4c3b8dabe 100644
--- a/src/client/views/nodes/LoadingBox.scss
+++ b/src/client/views/nodes/LoadingBox.scss
@@ -6,6 +6,13 @@
background-color: #fdfdfd;
height: 100%;
align-items: center;
+ .textContainer,
+ .text {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 80%;
+ text-align: center;
+ }
}
.textContainer {
@@ -17,14 +24,6 @@
align-content: center;
}
-.textContainer,
-.text {
- overflow: hidden;
- text-overflow: ellipsis;
- max-width: 80%;
- text-align: center;
-}
-
.headerText {
text-align: center;
font-weight: bold;
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 6479e933e..de0b57fd7 100644
--- a/src/client/views/nodes/MapBox/MapBox.tsx
+++ b/src/client/views/nodes/MapBox/MapBox.tsx
@@ -1,13 +1,15 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Autocomplete, GoogleMap, GoogleMapProps, Marker } from '@react-google-maps/api';
+import BingMapsReact from 'bingmaps-react';
import { action, computed, IReactionDisposer, observable, ObservableMap, runInAction } from 'mobx';
import { 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';
-import { emptyFunction, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../../Utils';
+import { emptyFunction, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../../Utils';
import { Docs } from '../../../documents/Documents';
import { DragManager } from '../../../util/DragManager';
import { SnappingManager } from '../../../util/SnappingManager';
@@ -20,6 +22,7 @@ import { AnchorMenu } from '../../pdf/AnchorMenu';
import { Annotation } from '../../pdf/Annotation';
import { SidebarAnnos } from '../../SidebarAnnos';
import { FieldView, FieldViewProps } from '../FieldView';
+import { PinProps } from '../trails';
import './MapBox.scss';
import { MapBoxInfoWindow } from './MapBoxInfoWindow';
@@ -43,20 +46,22 @@ const mapContainerStyle = {
};
const defaultCenter = {
- lat: 38.685,
- lng: -115.234,
+ lat: 42.360081,
+ lng: -71.058884,
};
const mapOptions = {
fullscreenControl: false,
};
+const bingApiKey = process.env.BING_MAPS; // if you're running local, get a Bing Maps api key here: https://www.bingmapsportal.com/ and then add it to the .env file in the Dash-Web root directory as: _CLIENT_BING_MAPS=<your apikey>
const apiKey = process.env.GOOGLE_MAPS;
const script = document.createElement('script');
script.defer = true;
script.async = true;
script.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places,drawing`;
+console.log(script.src);
document.head.appendChild(script);
/**
@@ -84,6 +89,7 @@ const options = {
@observer
export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps & Partial<GoogleMapProps>>() {
+ static UseBing = true;
private _dropDisposer?: DragManager.DragDropDisposer;
private _disposers: { [name: string]: IReactionDisposer } = {};
private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
@@ -93,7 +99,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
return FieldView.LayoutString(MapBox, fieldKey);
}
public get SidebarKey() {
- return this.fieldKey + '-sidebar';
+ return this.fieldKey + '_sidebar';
}
private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean) => void);
@computed get inlineTextAnnotations() {
@@ -121,7 +127,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
@observable _showSidebar = false;
@computed get SidebarShown() {
- return this._showSidebar || this.layoutDoc._showSidebar ? true : false;
+ return this._showSidebar || this.layoutDoc._layout_showSidebar ? true : false;
}
static _canAnnotate = true;
@@ -129,8 +135,8 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
private _sidebarRef = React.createRef<SidebarAnnos>();
private _ref: React.RefObject<HTMLDivElement> = React.createRef();
- constructor(props: any) {
- super(props);
+ componentDidMount() {
+ this.props.setContentView?.(this);
}
@action
@@ -262,7 +268,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
setTimeout(() => {
if (this._loadPending && this._map.getBounds()) {
this._loadPending = false;
- this.layoutDoc.fitContentsToBox && this.fitBounds(this._map);
+ this.layoutDoc.freeform_fitContentsToBox && this.fitBounds(this._map);
}
}, 250);
// listener to addmarker event
@@ -277,7 +283,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
centered = () => {
if (this._loadPending && this._map.getBounds()) {
this._loadPending = false;
- this.layoutDoc.fitContentsToBox && this.fitBounds(this._map);
+ this.layoutDoc.freeform_fitContentsToBox && this.fitBounds(this._map);
}
this.dataDoc.mapLat = this._map.getCenter()?.lat();
this.dataDoc.mapLng = this._map.getCenter()?.lng();
@@ -287,7 +293,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
zoomChanged = () => {
if (this._loadPending && this._map.getBounds()) {
this._loadPending = false;
- this.layoutDoc.fitContentsToBox && this.fitBounds(this._map);
+ this.layoutDoc.freeform_fitContentsToBox && this.fitBounds(this._map);
}
this.dataDoc.mapZoom = this._map.getZoom();
};
@@ -322,13 +328,11 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
*/
sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => {
console.log('print all sidebar Docs');
- console.log(this.allSidebarDocs);
- if (!this.layoutDoc._showSidebar) this.toggleSidebar();
+ if (!this.layoutDoc._layout_showSidebar) this.toggleSidebar();
const docs = doc instanceof Doc ? [doc] : doc;
docs.forEach(doc => {
if (doc.lat !== undefined && doc.lng !== undefined) {
const existingMarker = this.allMapMarkers.find(marker => marker.lat === doc.lat && marker.lng === doc.lng);
- doc.onClickBehavior = 'enterPortal';
if (existingMarker) {
Doc.AddDocToList(existingMarker, 'data', doc);
} else {
@@ -337,8 +341,6 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
}
}
}); //add to annotation list
- console.log('sidebaraddDocument');
- console.log(doc);
return this.addDocument(doc, sidebarKey); // add to sidebar list
};
@@ -350,12 +352,8 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
* @returns
*/
sidebarRemoveDocument = (doc: Doc | Doc[], sidebarKey?: string) => {
- if (this.layoutDoc._showSidebar) this.toggleSidebar();
+ if (this.layoutDoc._layout_showSidebar) this.toggleSidebar();
const docs = doc instanceof Doc ? [doc] : doc;
- docs.forEach(doc => {
- console.log(this.allMapMarkers);
- console.log(this.allSidebarDocs);
- });
return this.removeDocument(doc, sidebarKey);
};
@@ -373,16 +371,16 @@ export class MapBox 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;
this.layoutDoc._width = fullWidth + localDelta[0];
- this.layoutDoc._sidebarWidthPercent = ((100 * (this.sidebarWidth() + localDelta[0])) / (fullWidth + localDelta[0])).toString() + '%';
+ this.layoutDoc._layout_sidebarWidthPercent = ((100 * (this.sidebarWidth() + localDelta[0])) / (fullWidth + localDelta[0])).toString() + '%';
} else {
this._showSidebar = false;
this.layoutDoc._width = mapWidth;
- this.layoutDoc._sidebarWidthPercent = '0%';
+ this.layoutDoc._layout_sidebarWidthPercent = '0%';
}
return false;
}),
@@ -391,12 +389,12 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
);
};
- sidebarWidth = () => (Number(this.sidebarWidthPercent.substring(0, this.sidebarWidthPercent.length - 1)) / 100) * this.props.PanelWidth();
- @computed get sidebarWidthPercent() {
- return StrCast(this.layoutDoc._sidebarWidthPercent, '0%');
+ sidebarWidth = () => (Number(this.layout_sidebarWidthPercent.substring(0, this.layout_sidebarWidthPercent.length - 1)) / 100) * this.props.PanelWidth();
+ @computed get layout_sidebarWidthPercent() {
+ return StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%');
}
@computed get sidebarColor() {
- return StrCast(this.layoutDoc.sidebarColor, StrCast(this.layoutDoc[this.props.fieldKey + '-backgroundColor'], '#e4e4e4'));
+ return StrCast(this.layoutDoc.sidebar_color, StrCast(this.layoutDoc[this.props.fieldKey + '_backgroundColor'], '#e4e4e4'));
}
/**
@@ -405,7 +403,6 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
*/
@action
private handlePlaceChanged = () => {
- console.log(this.searchBox);
const place = this.searchBox.getPlace();
if (!place.geometry || !place.geometry.location) {
@@ -416,7 +413,6 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
// zoom in on the location of the search result
if (place.geometry.viewport) {
- console.log(this._map);
this._map.fitBounds(place.geometry.viewport);
} else {
this._map.setCenter(place.geometry.location);
@@ -458,7 +454,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
title="Toggle Sidebar"
style={{
display: !this.props.isContentActive() ? 'none' : undefined,
- top: StrCast(this.rootDoc._showTitle) === 'title' ? 20 : 5,
+ top: StrCast(this.rootDoc._layout_showTitle) === 'title' ? 20 : 5,
backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK,
}}
onPointerDown={this.sidebarBtnDown}>
@@ -472,8 +468,8 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
toggleSidebar = () => {
//1.2 * w * ? = .2 * w .2/1.2
const prevWidth = this.sidebarWidth();
- this.layoutDoc._showSidebar = (this.layoutDoc._sidebarWidthPercent = StrCast(this.layoutDoc._sidebarWidthPercent, '0%') === '0%' ? `${(100 * 0.2) / 1.2}%` : '0%') !== '0%';
- this.layoutDoc._width = this.layoutDoc._showSidebar ? NumCast(this.layoutDoc._width) * 1.2 : Math.max(20, NumCast(this.layoutDoc._width) - prevWidth);
+ this.layoutDoc._layout_showSidebar = (this.layoutDoc._layout_sidebarWidthPercent = StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%') === '0%' ? `${(100 * 0.2) / 1.2}%` : '0%') !== '0%';
+ this.layoutDoc._width = this.layoutDoc._layout_showSidebar ? NumCast(this.layoutDoc._width) * 1.2 : Math.max(20, NumCast(this.layoutDoc._width) - prevWidth);
};
sidebarDown = (e: React.PointerEvent) => {
@@ -481,8 +477,8 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
};
sidebarMove = (e: PointerEvent, down: number[], delta: number[]) => {
const bounds = this._ref.current!.getBoundingClientRect();
- this.layoutDoc._sidebarWidthPercent = '' + 100 * Math.max(0, 1 - (e.clientX - bounds.left) / bounds.width) + '%';
- this.layoutDoc._showSidebar = this.layoutDoc._sidebarWidthPercent !== '0%';
+ this.layoutDoc._layout_sidebarWidthPercent = '' + 100 * Math.max(0, 1 - (e.clientX - bounds.left) / bounds.width) + '%';
+ this.layoutDoc._layout_showSidebar = this.layoutDoc._layout_sidebarWidthPercent !== '0%';
e.preventDefault();
return false;
};
@@ -516,9 +512,8 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
return this.addDocument(doc, annotationKey);
};
- pointerEvents = () => {
- return this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' && !MarqueeOptionsMenu.Instance.isShown() ? 'all' : SnappingManager.GetIsDragging() ? undefined : 'none';
- };
+ pointerEvents = () => (this.props.isContentActive() && !MarqueeOptionsMenu.Instance.isShown() ? 'all' : 'none');
+
@computed get annotationLayer() {
return (
<div className="mapBox-annotationLayer" style={{ height: Doc.NativeHeight(this.Document) || undefined }} ref={this._annotationLayer}>
@@ -531,10 +526,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
);
}
- getAnchor = () => {
- const anchor = AnchorMenu.Instance?.GetAnchor(this._savedAnnotations) ?? this.rootDoc;
- return anchor;
- };
+ getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => AnchorMenu.Instance?.GetAnchor(this._savedAnnotations, addAsAnnotation) ?? this.rootDoc;
/**
* render contents in allMapMarkers (e.g. images with exifData) into google maps as map marker
@@ -554,78 +546,109 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
// }
};
- panelWidth = () => this.props.PanelWidth() / (this.props.NativeDimScaling?.() || 1) - this.sidebarWidth(); // (this.Document.scrollHeight || Doc.NativeHeight(this.Document) || 0);
- panelHeight = () => this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1); // () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : Doc.NativeWidth(this.Document);
- scrollXf = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._scrollTop));
- transparentFilter = () => [...this.props.docFilters(), Utils.IsTransparentFilter()];
- opaqueFilter = () => [...this.props.docFilters(), Utils.IsOpaqueFilter()];
+ 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.childFilters(), Utils.IsTransparentFilter()];
+ opaqueFilter = () => [...this.props.childFilters(), Utils.IsOpaqueFilter()];
infoWidth = () => this.props.PanelWidth() / 5;
infoHeight = () => this.props.PanelHeight() / 5;
anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick;
savedAnnotations = () => this._savedAnnotations;
+
+ _bingSearchManager: any;
+ _bingMap: any;
+ get MicrosoftMaps() {
+ return (window as any).Microsoft.Maps;
+ }
+ // uses Bing Search to retrieve lat/lng for a location. eg.,
+ // const results = this.geocodeQuery(map.map, 'Philadelphia, PA');
+ // to move the map to that location:
+ // const location = await this.geocodeQuery(this._bingMap, 'Philadelphia, PA');
+ // this._bingMap.current.setView({
+ // mapTypeId: this.MicrosoftMaps.MapTypeId.aerial,
+ // center: new this.MicrosoftMaps.Location(loc.latitude, loc.longitude),
+ // });
+ //
+ bingGeocode = (map: any, query: string) => {
+ return new Promise<{ latitude: number; longitude: number }>((res, reject) => {
+ //If search manager is not defined, load the search module.
+ if (!this._bingSearchManager) {
+ //Create an instance of the search manager and call the geocodeQuery function again.
+ this.MicrosoftMaps.loadModule('Microsoft.Maps.Search', () => {
+ this._bingSearchManager = new this.MicrosoftMaps.Search.SearchManager(map.current);
+ res(this.bingGeocode(map, query));
+ });
+ } else {
+ this._bingSearchManager.geocode({
+ where: query,
+ callback: action((r: any) => {
+ res(r.results[0].location);
+ }),
+ errorCallback: (e: any) => reject(),
+ });
+ }
+ });
+ };
+
+ bingViewOptions = {
+ center: { latitude: defaultCenter.lat, longitude: defaultCenter.lng },
+ mapTypeId: 'grayscale',
+ };
+ bingMapOptions = {
+ navigationBarMode: 'square',
+ };
+ bingMapReady = (map: any) => (this._bingMap = map.map);
render() {
- const renderAnnotations = (docFilters?: () => string[]) => null;
- // bcz: commmented this out. Otherwise, any documents that are rendered with an InfoWindow of a marker
- // will also be rendered as freeform annotations on the map. However, it doesn't seem that rendering
- // freeform documents on the map does anything anyway, so getting rid of it for now. Also, since documents
- // are rendered twice, adding a new note to the InfoWindow loses focus immediately on creation since it gets
- // shifted to the non-visible view of the document in this freeform view.
- // <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit}
- // renderDepth={this.props.renderDepth + 1}
- // isAnnotationOverlay={true}
- // fieldKey={this.annotationKey}
- // CollectionView={undefined}
- // setPreviewCursor={this.setPreviewCursor}
- // PanelWidth={this.panelWidth}
- // PanelHeight={this.panelHeight}
- // ScreenToLocalTransform={this.scrollXf}
- // scaling={returnOne}
- // dropAction={"alias"}
- // docFilters={docFilters || this.props.docFilters}
- // dontRenderDocuments={docFilters ? false : true}
- // select={emptyFunction}
- // bringToFront={emptyFunction}
- // whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
- // removeDocument={this.removeDocument}
- // moveDocument={this.moveDocument}
- // addDocument={this.sidebarAddDocument}
- // childPointerEvents={"all"}
- // pointerEvents={Doc.ActiveTool !== InkTool.None || this._isAnnotating || SnappingManager.GetIsDragging() ? "all" : "none"} />;
+ const renderAnnotations = (childFilters?: () => string[]) => null;
return (
<div className="mapBox" ref={this._ref}>
- {/*console.log(apiKey)*/}
- {/* <LoadScript
- googleMapsApiKey={apiKey!}
- libraries={['places', 'drawing']}
- > */}
- <div className="mapBox-wrapper" onWheel={e => e.stopPropagation()} onPointerDown={e => e.button === 0 && !e.ctrlKey && e.stopPropagation()} style={{ width: `calc(100% - ${this.sidebarWidthPercent})` }}>
+ <div
+ className="mapBox-wrapper"
+ onWheel={e => e.stopPropagation()}
+ onPointerDown={async e => {
+ e.button === 0 && !e.ctrlKey && e.stopPropagation();
+ // just a simple test of bing maps geocode api
+ // const loc = await this.bingGeocode(this._bingMap, 'Philadelphia, PA');
+ // this._bingMap.current.setView({
+ // mapTypeId: this.MicrosoftMaps.MapTypeId.aerial,
+ // center: new this.MicrosoftMaps.Location(loc.latitude, loc.longitude),
+ // zoom: 15,
+ // });
+ }}
+ style={{ width: `calc(100% - ${this.layout_sidebarWidthPercent})`, pointerEvents: this.pointerEvents() }}>
<div style={{ mixBlendMode: 'multiply' }}>{renderAnnotations(this.transparentFilter)}</div>
{renderAnnotations(this.opaqueFilter)}
{SnappingManager.GetIsDragging() ? null : renderAnnotations()}
{this.annotationLayer}
- <GoogleMap mapContainerStyle={mapContainerStyle} onZoomChanged={this.zoomChanged} onCenterChanged={this.centered} onLoad={this.loadHandler} options={mapOptions}>
- <Autocomplete onLoad={this.setSearchBox} onPlaceChanged={this.handlePlaceChanged}>
- <input className="mapBox-input" ref={this.inputRef} type="text" onKeyDown={e => e.stopPropagation()} placeholder="Enter location" />
- </Autocomplete>
-
- {this.renderMarkers()}
- {this.allMapMarkers
- .filter(marker => marker.infoWindowOpen)
- .map(marker => (
- <MapBoxInfoWindow
- key={marker[Id]}
- {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
- place={marker}
- markerMap={this.markerMap}
- PanelWidth={this.infoWidth}
- PanelHeight={this.infoHeight}
- moveDocument={this.moveDocument}
- isAnyChildContentActive={this.isAnyChildContentActive}
- whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
- />
- ))}
- {/* {this.handleMapCenter(this._map)} */}
- </GoogleMap>
+
+ {!MapBox.UseBing ? null : <BingMapsReact onMapReady={this.bingMapReady} bingMapsKey={bingApiKey} height="100%" mapOptions={this.bingMapOptions} width="100%" viewOptions={this.bingViewOptions} />}
+ <div style={{ display: MapBox.UseBing ? 'none' : undefined }}>
+ <GoogleMap mapContainerStyle={mapContainerStyle} onZoomChanged={this.zoomChanged} onCenterChanged={this.centered} onLoad={this.loadHandler} options={mapOptions}>
+ <Autocomplete onLoad={this.setSearchBox} onPlaceChanged={this.handlePlaceChanged}>
+ <input className="mapBox-input" ref={this.inputRef} type="text" onKeyDown={e => e.stopPropagation()} placeholder="Enter location" />
+ </Autocomplete>
+
+ {this.renderMarkers()}
+ {this.allMapMarkers
+ .filter(marker => marker.infoWindowOpen)
+ .map(marker => (
+ <MapBoxInfoWindow
+ key={marker[Id]}
+ {...this.props}
+ setContentView={emptyFunction}
+ place={marker}
+ markerMap={this.markerMap}
+ PanelWidth={this.infoWidth}
+ PanelHeight={this.infoHeight}
+ moveDocument={this.moveDocument}
+ isAnyChildContentActive={this.isAnyChildContentActive}
+ whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
+ />
+ ))}
+ {/* {this.handleMapCenter(this._map)} */}
+ </GoogleMap>
+ </div>
{!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? null : (
<MarqueeAnnotator
rootDoc={this.rootDoc}
@@ -638,12 +661,13 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
finishMarquee={this.finishMarquee}
savedAnnotations={this.savedAnnotations}
annotationLayer={this._annotationLayer.current}
+ selectionText={returnEmptyString}
mainCont={this._mainCont.current}
/>
)}
</div>
{/* </LoadScript > */}
- <div className="mapBox-sidebar" style={{ position: 'absolute', right: 0, height: '100%', width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}>
+ <div className="mapBox-sidebar" style={{ position: 'absolute', right: 0, height: '100%', width: `${this.layout_sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}>
<SidebarAnnos
ref={this._sidebarRef}
{...this.props}
@@ -651,6 +675,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
rootDoc={this.rootDoc}
layoutDoc={this.layoutDoc}
dataDoc={this.dataDoc}
+ usePanelWidth={true}
showSidebar={this.SidebarShown}
nativeWidth={NumCast(this.layoutDoc._nativeWidth)}
whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
diff --git a/src/client/views/nodes/MapBox/MapBox2.tsx b/src/client/views/nodes/MapBox/MapBox2.tsx
new file mode 100644
index 000000000..a9154c5bb
--- /dev/null
+++ b/src/client/views/nodes/MapBox/MapBox2.tsx
@@ -0,0 +1,642 @@
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Autocomplete, GoogleMap, GoogleMapProps, Marker } from '@react-google-maps/api';
+import { action, computed, IReactionDisposer, observable, ObservableMap, runInAction } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { Doc, DocListCast, Opt } from '../../../../fields/Doc';
+import { Width } from '../../../../fields/DocSymbols';
+import { Id } from '../../../../fields/FieldSymbols';
+import { InkTool } from '../../../../fields/InkField';
+import { NumCast, StrCast } from '../../../../fields/Types';
+import { emptyFunction, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../../Utils';
+import { Docs } from '../../../documents/Documents';
+import { DragManager } from '../../../util/DragManager';
+import { SnappingManager } from '../../../util/SnappingManager';
+import { UndoManager } from '../../../util/UndoManager';
+import { MarqueeOptionsMenu } from '../../collections/collectionFreeForm';
+import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../../DocComponent';
+import { Colors } from '../../global/globalEnums';
+import { MarqueeAnnotator } from '../../MarqueeAnnotator';
+import { AnchorMenu } from '../../pdf/AnchorMenu';
+import { Annotation } from '../../pdf/Annotation';
+import { SidebarAnnos } from '../../SidebarAnnos';
+import { FieldView, FieldViewProps } from '../FieldView';
+import { PinProps } from '../trails';
+import './MapBox2.scss';
+import { MapBoxInfoWindow } from './MapBoxInfoWindow';
+
+/**
+ * MapBox2 architecture:
+ * Main component: MapBox2.tsx
+ * Supporting Components: SidebarAnnos, CollectionStackingView
+ *
+ * MapBox2 is a node that extends the ViewBoxAnnotatableComponent. Similar to PDFBox and WebBox, it supports interaction between sidebar content and document content.
+ * The main body of MapBox2 uses Google Maps API to allow location retrieval, adding map markers, pan and zoom, and open street view.
+ * Dash Document architecture is integrated with Maps API: When drag and dropping documents with ExifData (gps Latitude and Longitude information) available,
+ * sidebarAddDocument function checks if the document contains lat & lng information, if it does, then the document is added to both the sidebar and the infowindow (a pop up corresponding to a map marker--pin on map).
+ * The lat and lng field of the document is filled when importing (spec see ConvertDMSToDD method and processFileUpload method in Documents.ts).
+ * A map marker is considered a document that contains a collection with stacking view of documents, it has a lat, lng location, which is passed to Maps API's custom marker (red pin) to be rendered on the google maps
+ */
+
+// const _global = (window /* browser */ || global /* node */) as any;
+
+const mapContainerStyle = {
+ height: '100%',
+};
+
+const defaultCenter = {
+ lat: 42.360081,
+ lng: -71.058884,
+};
+
+const mapOptions = {
+ fullscreenControl: false,
+};
+
+const apiKey = process.env.GOOGLE_MAPS;
+
+const script = document.createElement('script');
+script.defer = true;
+script.async = true;
+script.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places,drawing`;
+console.log(script.src);
+document.head.appendChild(script);
+
+/**
+ * Consider integrating later: allows for drawing, circling, making shapes on map
+ */
+// const drawingManager = new window.google.maps.drawing.DrawingManager({
+// drawingControl: true,
+// drawingControlOptions: {
+// position: google.maps.ControlPosition.TOP_RIGHT,
+// drawingModes: [
+// google.maps.drawing.OverlayType.MARKER,
+// // currently we are not supporting the following drawing mode on map, a thought for future development
+// google.maps.drawing.OverlayType.CIRCLE,
+// google.maps.drawing.OverlayType.POLYLINE,
+// ],
+// },
+// });
+
+// options for searchbox in Google Maps Places Autocomplete API
+const options = {
+ fields: ['formatted_address', 'geometry', 'name'], // note: level of details is charged by item per retrieval, not recommended to return all fields
+ strictBounds: false,
+ types: ['establishment'], // type pf places, subject of change according to user need
+} as google.maps.places.AutocompleteOptions;
+
+@observer
+export class MapBox2 extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps & Partial<GoogleMapProps>>() {
+ private _dropDisposer?: DragManager.DragDropDisposer;
+ private _disposers: { [name: string]: IReactionDisposer } = {};
+ private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
+ @observable private _overlayAnnoInfo: Opt<Doc>;
+ showInfo = action((anno: Opt<Doc>) => (this._overlayAnnoInfo = anno));
+ public static LayoutString(fieldKey: string) {
+ return FieldView.LayoutString(MapBox2, fieldKey);
+ }
+ public get SidebarKey() {
+ return this.fieldKey + '_sidebar';
+ }
+ private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean) => void);
+ @computed get inlineTextAnnotations() {
+ return this.allMapMarkers.filter(a => a.textInlineAnnotations);
+ }
+
+ @observable private _map: google.maps.Map = null as unknown as google.maps.Map;
+ @observable private selectedPlace: Doc | undefined;
+ @observable private markerMap: { [id: string]: google.maps.Marker } = {};
+ @observable private center = navigator.geolocation ? navigator.geolocation.getCurrentPosition : defaultCenter;
+ @observable private _marqueeing: number[] | undefined;
+ @observable private _isAnnotating = false;
+ @observable private inputRef = React.createRef<HTMLInputElement>();
+ @observable private searchMarkers: google.maps.Marker[] = [];
+ @observable private searchBox = new window.google.maps.places.Autocomplete(this.inputRef.current!, options);
+ @observable private _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>();
+ @computed get allSidebarDocs() {
+ return DocListCast(this.dataDoc[this.SidebarKey]);
+ }
+ @computed get allMapMarkers() {
+ return DocListCast(this.dataDoc[this.annotationKey]);
+ }
+ @observable private toggleAddMarker = false;
+ private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
+
+ @observable _showSidebar = false;
+ @computed get SidebarShown() {
+ return this._showSidebar || this.layoutDoc._layout_showSidebar ? true : false;
+ }
+
+ static _canAnnotate = true;
+ static _hadSelection: boolean = false;
+ private _sidebarRef = React.createRef<SidebarAnnos>();
+ private _ref: React.RefObject<HTMLDivElement> = React.createRef();
+
+ componentDidMount() {
+ this.props.setContentView?.(this);
+ }
+
+ @action
+ private setSearchBox = (searchBox: any) => {
+ this.searchBox = searchBox;
+ };
+
+ // iterate allMarkers to size, center, and zoom map to contain all markers
+ private fitBounds = (map: google.maps.Map) => {
+ const curBounds = map.getBounds() ?? new window.google.maps.LatLngBounds();
+ const isFitting = this.allMapMarkers.reduce((fits, place) => fits && curBounds?.contains({ lat: NumCast(place.lat), lng: NumCast(place.lng) }), true as boolean);
+ !isFitting && map.fitBounds(this.allMapMarkers.reduce((bounds, place) => bounds.extend({ lat: NumCast(place.lat), lng: NumCast(place.lng) }), new window.google.maps.LatLngBounds()));
+ };
+
+ /**
+ * Custom control for add marker button
+ * @param controlDiv
+ * @param map
+ */
+ private CenterControl = () => {
+ const controlDiv = document.createElement('div');
+ controlDiv.className = 'MapBox2-addMarker';
+ // Set CSS for the control border.
+ const controlUI = document.createElement('div');
+ controlUI.style.backgroundColor = '#fff';
+ controlUI.style.borderRadius = '3px';
+ controlUI.style.cursor = 'pointer';
+ controlUI.style.marginTop = '10px';
+ controlUI.style.borderRadius = '4px';
+ controlUI.style.marginBottom = '22px';
+ controlUI.style.textAlign = 'center';
+ controlUI.style.position = 'absolute';
+ controlUI.style.width = '32px';
+ controlUI.style.height = '32px';
+ controlUI.title = 'Click to toggle marker mode. In marker mode, click on map to place a marker.';
+
+ const plIcon = document.createElement('img');
+ plIcon.src = 'https://cdn4.iconfinder.com/data/icons/wirecons-free-vector-icons/32/add-256.png';
+ plIcon.style.color = 'rgb(25,25,25)';
+ plIcon.style.fontFamily = 'Roboto,Arial,sans-serif';
+ plIcon.style.fontSize = '16px';
+ plIcon.style.lineHeight = '32px';
+ plIcon.style.left = '18';
+ plIcon.style.top = '15';
+ plIcon.style.position = 'absolute';
+ plIcon.width = 14;
+ plIcon.height = 14;
+ plIcon.innerHTML = 'Add';
+ controlUI.appendChild(plIcon);
+
+ // Set CSS for the control interior.
+ const markerIcon = document.createElement('img');
+ markerIcon.src = 'https://cdn0.iconfinder.com/data/icons/small-n-flat/24/678111-map-marker-1024.png';
+ markerIcon.style.color = 'rgb(25,25,25)';
+ markerIcon.style.fontFamily = 'Roboto,Arial,sans-serif';
+ markerIcon.style.fontSize = '16px';
+ markerIcon.style.lineHeight = '32px';
+ markerIcon.style.left = '-2';
+ markerIcon.style.top = '1';
+ markerIcon.width = 30;
+ markerIcon.height = 30;
+ markerIcon.style.position = 'absolute';
+ markerIcon.innerHTML = 'Add';
+ controlUI.appendChild(markerIcon);
+
+ // Setup the click event listeners
+ controlUI.addEventListener('click', () => {
+ if (this.toggleAddMarker === true) {
+ this.toggleAddMarker = false;
+ console.log('add marker button status:' + this.toggleAddMarker);
+ controlUI.style.backgroundColor = '#fff';
+ markerIcon.style.color = 'rgb(25,25,25)';
+ } else {
+ this.toggleAddMarker = true;
+ console.log('add marker button status:' + this.toggleAddMarker);
+ controlUI.style.backgroundColor = '#4476f7';
+ markerIcon.style.color = 'rgb(255,255,255)';
+ }
+ });
+ controlDiv.appendChild(controlUI);
+ return controlDiv;
+ };
+
+ /**
+ * Place the marker on google maps & store the empty marker as a MapMarker Document in allMarkers list
+ * @param position - the LatLng position where the marker is placed
+ * @param map
+ */
+ @action
+ private placeMarker = (position: google.maps.LatLng, map: google.maps.Map) => {
+ const marker = new google.maps.Marker({
+ position: position,
+ map: map,
+ });
+ map.panTo(position);
+ const mapMarker = Docs.Create.MapMarkerDocument(NumCast(position.lat()), NumCast(position.lng()), false, [], {});
+ this.addDocument(mapMarker, this.annotationKey);
+ };
+
+ _loadPending = true;
+ /**
+ * store a reference to google map instance
+ * setup the drawing manager on the top right corner of map
+ * fit map bounds to contain all markers
+ * @param map
+ */
+ @action
+ private loadHandler = (map: google.maps.Map) => {
+ this._map = map;
+ this._loadPending = true;
+ const centerControlDiv = this.CenterControl();
+ map.controls[google.maps.ControlPosition.TOP_RIGHT].push(centerControlDiv);
+ //drawingManager.setMap(map);
+ // if (navigator.geolocation) {
+ // navigator.geolocation.getCurrentPosition(
+ // (position: Position) => {
+ // const pos = {
+ // lat: position.coords.latitude,
+ // lng: position.coords.longitude,
+ // };
+ // this._map.setCenter(pos);
+ // }
+ // );
+ // } else {
+ // alert("Your geolocation is not supported by browser.")
+ // };
+ map.setZoom(NumCast(this.dataDoc.mapZoom, 2.5));
+ map.setCenter(new google.maps.LatLng(NumCast(this.dataDoc.mapLat), NumCast(this.dataDoc.mapLng)));
+ setTimeout(() => {
+ if (this._loadPending && this._map.getBounds()) {
+ this._loadPending = false;
+ this.layoutDoc.freeform_fitContentsToBox && this.fitBounds(this._map);
+ }
+ }, 250);
+ // listener to addmarker event
+ this._map.addListener('click', (e: MouseEvent) => {
+ if (this.toggleAddMarker === true) {
+ this.placeMarker((e as any).latLng, map);
+ }
+ });
+ };
+
+ @action
+ centered = () => {
+ if (this._loadPending && this._map.getBounds()) {
+ this._loadPending = false;
+ this.layoutDoc.freeform_fitContentsToBox && this.fitBounds(this._map);
+ }
+ this.dataDoc.mapLat = this._map.getCenter()?.lat();
+ this.dataDoc.mapLng = this._map.getCenter()?.lng();
+ };
+
+ @action
+ zoomChanged = () => {
+ if (this._loadPending && this._map.getBounds()) {
+ this._loadPending = false;
+ this.layoutDoc.freeform_fitContentsToBox && this.fitBounds(this._map);
+ }
+ this.dataDoc.mapZoom = this._map.getZoom();
+ };
+
+ /**
+ * Load and render all map markers
+ * @param marker
+ * @param place
+ */
+ @action
+ private markerLoadHandler = (marker: google.maps.Marker, place: Doc) => {
+ place[Id] ? (this.markerMap[place[Id]] = marker) : null;
+ };
+
+ /**
+ * on clicking the map marker, set the selected place to the marker document & set infowindowopen to be true
+ * @param e
+ * @param place
+ */
+ @action
+ private markerClickHandler = (e: google.maps.MapMouseEvent, place: Doc) => {
+ // set which place was clicked
+ this.selectedPlace = place;
+ place.infoWindowOpen = true;
+ };
+
+ /**
+ * Called when dragging documents into map sidebar or directly into infowindow; to create a map marker, ref to MapMarkerDocument in Documents.ts
+ * @param doc
+ * @param sidebarKey
+ * @returns
+ */
+ sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => {
+ console.log('print all sidebar Docs');
+ if (!this.layoutDoc._layout_showSidebar) this.toggleSidebar();
+ const docs = doc instanceof Doc ? [doc] : doc;
+ docs.forEach(doc => {
+ if (doc.lat !== undefined && doc.lng !== undefined) {
+ const existingMarker = this.allMapMarkers.find(marker => marker.lat === doc.lat && marker.lng === doc.lng);
+ if (existingMarker) {
+ Doc.AddDocToList(existingMarker, 'data', doc);
+ } else {
+ const marker = Docs.Create.MapMarkerDocument(NumCast(doc.lat), NumCast(doc.lng), false, [doc], {});
+ this.addDocument(marker, this.annotationKey);
+ }
+ }
+ }); //add to annotation list
+
+ return this.addDocument(doc, sidebarKey); // add to sidebar list
+ };
+
+ /**
+ * Removing documents from the sidebar
+ * @param doc
+ * @param sidebarKey
+ * @returns
+ */
+ sidebarRemoveDocument = (doc: Doc | Doc[], sidebarKey?: string) => {
+ if (this.layoutDoc._layout_showSidebar) this.toggleSidebar();
+ const docs = doc instanceof Doc ? [doc] : doc;
+ return this.removeDocument(doc, sidebarKey);
+ };
+
+ /**
+ * Toggle sidebar onclick the tiny comment button on the top right corner
+ * @param e
+ */
+ sidebarBtnDown = (e: React.PointerEvent) => {
+ setupMoveUpEvents(
+ this,
+ e,
+ (e, down, delta) =>
+ runInAction(() => {
+ const localDelta = this.props
+ .ScreenToLocalTransform()
+ .scale(this.props.NativeDimScaling?.() || 1)
+ .transformDirection(delta[0], delta[1]);
+ const fullWidth = this.layoutDoc[Width]();
+ const mapWidth = fullWidth - this.sidebarWidth();
+ if (this.sidebarWidth() + localDelta[0] > 0) {
+ this._showSidebar = true;
+ this.layoutDoc._width = fullWidth + localDelta[0];
+ this.layoutDoc._layout_sidebarWidthPercent = ((100 * (this.sidebarWidth() + localDelta[0])) / (fullWidth + localDelta[0])).toString() + '%';
+ } else {
+ this._showSidebar = false;
+ this.layoutDoc._width = mapWidth;
+ this.layoutDoc._layout_sidebarWidthPercent = '0%';
+ }
+ return false;
+ }),
+ emptyFunction,
+ () => UndoManager.RunInBatch(this.toggleSidebar, 'toggle sidebar map')
+ );
+ };
+
+ sidebarWidth = () => (Number(this.layout_sidebarWidthPercent.substring(0, this.layout_sidebarWidthPercent.length - 1)) / 100) * this.props.PanelWidth();
+ @computed get layout_sidebarWidthPercent() {
+ return StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%');
+ }
+ @computed get sidebarColor() {
+ return StrCast(this.layoutDoc.sidebar_color, StrCast(this.layoutDoc[this.props.fieldKey + '_backgroundColor'], '#e4e4e4'));
+ }
+
+ /**
+ * function that reads the place inputed from searchbox, then zoom in on the location that's been autocompleted;
+ * add a customized temporary marker on the map
+ */
+ @action
+ private handlePlaceChanged = () => {
+ const place = this.searchBox.getPlace();
+
+ if (!place.geometry || !place.geometry.location) {
+ // user entered the name of a place that wasn't suggested & pressed the enter key, or place details request failed
+ window.alert("No details available for input: '" + place.name + "'");
+ return;
+ }
+
+ // zoom in on the location of the search result
+ if (place.geometry.viewport) {
+ this._map.fitBounds(place.geometry.viewport);
+ } else {
+ this._map.setCenter(place.geometry.location);
+ this._map.setZoom(17);
+ }
+
+ // customize icon => customized icon for the nature of the location selected
+ const icon = {
+ url: place.icon as string,
+ size: new google.maps.Size(71, 71),
+ origin: new google.maps.Point(0, 0),
+ anchor: new google.maps.Point(17, 34),
+ scaledSize: new google.maps.Size(25, 25),
+ };
+
+ // put temporary cutomized marker on searched location
+ this.searchMarkers.forEach(marker => {
+ marker.setMap(null);
+ });
+ this.searchMarkers = [];
+ this.searchMarkers.push(
+ new window.google.maps.Marker({
+ map: this._map,
+ icon,
+ title: place.name,
+ position: place.geometry.location,
+ })
+ );
+ };
+
+ /**
+ * Handles toggle of sidebar on click the little comment button
+ */
+ @computed get sidebarHandle() {
+ return (
+ <div
+ className="MapBox2-overlayButton-sidebar"
+ key="sidebar"
+ title="Toggle Sidebar"
+ style={{
+ display: !this.props.isContentActive() ? 'none' : undefined,
+ top: StrCast(this.rootDoc._layout_showTitle) === 'title' ? 20 : 5,
+ backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK,
+ }}
+ onPointerDown={this.sidebarBtnDown}>
+ <FontAwesomeIcon style={{ color: Colors.WHITE }} icon={'comment-alt'} size="sm" />
+ </div>
+ );
+ }
+
+ // TODO: Adding highlight box layer to Maps
+ @action
+ toggleSidebar = () => {
+ //1.2 * w * ? = .2 * w .2/1.2
+ const prevWidth = this.sidebarWidth();
+ this.layoutDoc._layout_showSidebar = (this.layoutDoc._layout_sidebarWidthPercent = StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%') === '0%' ? `${(100 * 0.2) / 1.2}%` : '0%') !== '0%';
+ this.layoutDoc._width = this.layoutDoc._layout_showSidebar ? NumCast(this.layoutDoc._width) * 1.2 : Math.max(20, NumCast(this.layoutDoc._width) - prevWidth);
+ };
+
+ sidebarDown = (e: React.PointerEvent) => {
+ setupMoveUpEvents(this, e, this.sidebarMove, emptyFunction, () => setTimeout(this.toggleSidebar), true);
+ };
+ sidebarMove = (e: PointerEvent, down: number[], delta: number[]) => {
+ const bounds = this._ref.current!.getBoundingClientRect();
+ this.layoutDoc._layout_sidebarWidthPercent = '' + 100 * Math.max(0, 1 - (e.clientX - bounds.left) / bounds.width) + '%';
+ this.layoutDoc._layout_showSidebar = this.layoutDoc._layout_sidebarWidthPercent !== '0%';
+ e.preventDefault();
+ return false;
+ };
+
+ setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean) => void) => (this._setPreviewCursor = func);
+
+ @action
+ onMarqueeDown = (e: React.PointerEvent) => {
+ if (!e.altKey && e.button === 0 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) {
+ setupMoveUpEvents(
+ this,
+ e,
+ action(e => {
+ MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
+ this._marqueeing = [e.clientX, e.clientY];
+ return true;
+ }),
+ returnFalse,
+ () => MarqueeAnnotator.clearAnnotations(this._savedAnnotations),
+ false
+ );
+ }
+ };
+ @action finishMarquee = (x?: number, y?: number) => {
+ this._marqueeing = undefined;
+ this._isAnnotating = false;
+ x !== undefined && y !== undefined && this._setPreviewCursor?.(x, y, false, false);
+ };
+
+ addDocumentWrapper = (doc: Doc | Doc[], annotationKey?: string) => {
+ return this.addDocument(doc, annotationKey);
+ };
+
+ pointerEvents = () => {
+ 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 (
+ <div className="MapBox2-annotationLayer" style={{ height: Doc.NativeHeight(this.Document) || undefined }} ref={this._annotationLayer}>
+ {this.inlineTextAnnotations
+ .sort((a, b) => NumCast(a.y) - NumCast(b.y))
+ .map(anno => (
+ <Annotation key={`${anno[Id]}-annotation`} {...this.props} fieldKey={this.annotationKey} pointerEvents={this.pointerEvents} showInfo={this.showInfo} dataDoc={this.dataDoc} anno={anno} />
+ ))}
+ </div>
+ );
+ }
+
+ getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => AnchorMenu.Instance?.GetAnchor(this._savedAnnotations, addAsAnnotation) ?? this.rootDoc;
+
+ /**
+ * render contents in allMapMarkers (e.g. images with exifData) into google maps as map marker
+ * @returns
+ */
+ private renderMarkers = () => {
+ return this.allMapMarkers.map(place => (
+ <Marker key={place[Id]} position={{ lat: NumCast(place.lat), lng: NumCast(place.lng) }} onLoad={marker => this.markerLoadHandler(marker, place)} onClick={(e: google.maps.MapMouseEvent) => this.markerClickHandler(e, place)} />
+ ));
+ };
+
+ // TODO: auto center on select a document in the sidebar
+ private handleMapCenter = (map: google.maps.Map) => {
+ // console.log("print the selected views in selectionManager:")
+ // if (SelectionManager.Views().lastElement()) {
+ // console.log(SelectionManager.Views().lastElement());
+ // }
+ };
+
+ panelWidth = () => this.props.PanelWidth() / (this.props.NativeDimScaling?.() || 1) - this.sidebarWidth();
+ panelHeight = () => this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1);
+ scrollXf = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._layout_scrollTop));
+ 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;
+ savedAnnotations = () => this._savedAnnotations;
+
+ get MicrosoftMaps() {
+ return (window as any).Microsoft.Maps;
+ }
+ render() {
+ const renderAnnotations = (childFilters?: () => string[]) => null;
+ return (
+ <div className="MapBox2" ref={this._ref}>
+ <div
+ className="MapBox2-wrapper"
+ onWheel={e => e.stopPropagation()}
+ onPointerDown={async e => {
+ e.button === 0 && !e.ctrlKey && e.stopPropagation();
+ }}
+ style={{ width: `calc(100% - ${this.layout_sidebarWidthPercent})`, pointerEvents: this.pointerEvents() }}>
+ <div style={{ mixBlendMode: 'multiply' }}>{renderAnnotations(this.transparentFilter)}</div>
+ {renderAnnotations(this.opaqueFilter)}
+ {SnappingManager.GetIsDragging() ? null : renderAnnotations()}
+ {this.annotationLayer}
+
+ <div>
+ <GoogleMap mapContainerStyle={mapContainerStyle} onZoomChanged={this.zoomChanged} onCenterChanged={this.centered} onLoad={this.loadHandler} options={mapOptions}>
+ <Autocomplete onLoad={this.setSearchBox} onPlaceChanged={this.handlePlaceChanged}>
+ <input className="MapBox2-input" ref={this.inputRef} type="text" onKeyDown={e => e.stopPropagation()} placeholder="Enter location" />
+ </Autocomplete>
+
+ {this.renderMarkers()}
+ {this.allMapMarkers
+ .filter(marker => marker.infoWindowOpen)
+ .map(marker => (
+ <MapBoxInfoWindow
+ key={marker[Id]}
+ {...this.props}
+ setContentView={emptyFunction}
+ place={marker}
+ markerMap={this.markerMap}
+ PanelWidth={this.infoWidth}
+ PanelHeight={this.infoHeight}
+ moveDocument={this.moveDocument}
+ isAnyChildContentActive={this.isAnyChildContentActive}
+ whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
+ />
+ ))}
+ {/* {this.handleMapCenter(this._map)} */}
+ </GoogleMap>
+ </div>
+ {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? null : (
+ <MarqueeAnnotator
+ rootDoc={this.rootDoc}
+ anchorMenuClick={this.anchorMenuClick}
+ scrollTop={0}
+ down={this._marqueeing}
+ scaling={returnOne}
+ addDocument={this.addDocumentWrapper}
+ docView={this.props.docViewPath().lastElement()}
+ finishMarquee={this.finishMarquee}
+ savedAnnotations={this.savedAnnotations}
+ annotationLayer={this._annotationLayer.current}
+ selectionText={returnEmptyString}
+ mainCont={this._mainCont.current}
+ />
+ )}
+ </div>
+ {/* </LoadScript > */}
+ <div className="MapBox2-sidebar" style={{ width: `${this.layout_sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}>
+ <SidebarAnnos
+ ref={this._sidebarRef}
+ {...this.props}
+ fieldKey={this.fieldKey}
+ rootDoc={this.rootDoc}
+ layoutDoc={this.layoutDoc}
+ dataDoc={this.dataDoc}
+ usePanelWidth={true}
+ showSidebar={this.SidebarShown}
+ nativeWidth={NumCast(this.layoutDoc._nativeWidth)}
+ whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
+ PanelWidth={this.sidebarWidth}
+ sidebarAddDocument={this.sidebarAddDocument}
+ moveDocument={this.moveDocument}
+ removeDocument={this.sidebarRemoveDocument}
+ />
+ </div>
+ {this.sidebarHandle}
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx
index 00bedafbe..577101445 100644
--- a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx
+++ b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx
@@ -4,7 +4,7 @@ import { observer } from 'mobx-react';
import * as React from 'react';
import { Doc } from '../../../../fields/Doc';
import { Id } from '../../../../fields/FieldSymbols';
-import { emptyFunction, OmitKeys, returnAll, returnEmptyFilter, returnFalse, returnOne, returnTrue, returnZero, setupMoveUpEvents } from '../../../../Utils';
+import { emptyFunction, returnAll, returnEmptyFilter, returnFalse, returnOne, returnTrue, returnZero, setupMoveUpEvents } from '../../../../Utils';
import { Docs } from '../../../documents/Documents';
import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
import { CollectionNoteTakingView } from '../../collections/CollectionNoteTakingView';
@@ -32,7 +32,7 @@ export class MapBoxInfoWindow extends React.Component<MapBoxInfoWindowProps & Vi
addNoteClick = (e: React.PointerEvent) => {
setupMoveUpEvents(this, e, returnFalse, emptyFunction, e => {
- const newBox = Docs.Create.TextDocument('Note', { _autoHeight: true });
+ const newBox = Docs.Create.TextDocument('Note', { _layout_autoHeight: true });
FormattedTextBox.SelectOnLoad = newBox[Id]; // track the new text box so we can give it a prop that tells it to focus itself when it's displayed
Doc.AddDocToList(this.props.place, 'data', newBox);
this._stack?.scrollToBottom();
@@ -42,7 +42,7 @@ export class MapBoxInfoWindow extends React.Component<MapBoxInfoWindowProps & Vi
};
_stack: CollectionStackingView | CollectionNoteTakingView | null | undefined;
- childFitWidth = (doc: Doc) => doc.type === DocumentType.RTF;
+ childLayoutFitWidth = (doc: Doc) => doc.type === DocumentType.RTF;
addDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((p, d) => p && Doc.AddDocToList(this.props.place, 'data', d), true as boolean);
removeDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((p, d) => p && Doc.RemoveDocFromList(this.props.place, 'data', d), true as boolean);
render() {
@@ -52,14 +52,14 @@ export class MapBoxInfoWindow extends React.Component<MapBoxInfoWindowProps & Vi
<div style={{ width: this.props.PanelWidth(), height: this.props.PanelHeight() }}>
<CollectionStackingView
ref={r => (this._stack = r)}
- {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
+ {...this.props}
+ setContentView={emptyFunction}
Document={this.props.place}
DataDoc={undefined}
fieldKey="data"
- CollectionView={undefined}
NativeWidth={returnZero}
NativeHeight={returnZero}
- docFilters={returnEmptyFilter}
+ childFilters={returnEmptyFilter}
setHeight={emptyFunction}
isAnnotationOverlay={false}
select={emptyFunction}
@@ -69,12 +69,12 @@ export class MapBoxInfoWindow extends React.Component<MapBoxInfoWindowProps & Vi
rootSelected={returnFalse}
childHideResizeHandles={returnTrue}
childHideDecorationTitle={returnTrue}
- childFitWidth={this.childFitWidth}
+ childLayoutFitWidth={this.childLayoutFitWidth}
// childDocumentsActive={returnFalse}
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 fcb3ccb07..fd4c6366b 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -3,17 +3,23 @@ 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 { Cast, ImageCast, NumCast, StrCast } from '../../../fields/Types';
+import { InkTool } from '../../../fields/InkField';
+import { ComputedField } from '../../../fields/ScriptField';
+import { Cast, FieldValue, ImageCast, NumCast, StrCast } from '../../../fields/Types';
import { ImageField, PdfField } from '../../../fields/URLField';
import { TraceMobx } from '../../../fields/util';
-import { emptyFunction, setupMoveUpEvents, Utils } from '../../../Utils';
+import { emptyFunction, returnFalse, setupMoveUpEvents, Utils } from '../../../Utils';
import { Docs, DocUtils } from '../../documents/Documents';
-import { DocumentType } from '../../documents/DocumentTypes';
+import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
+import { DocumentManager } from '../../util/DocumentManager';
import { KeyCodes } from '../../util/KeyCodes';
+import { SelectionManager } from '../../util/SelectionManager';
import { undoBatch, UndoManager } from '../../util/UndoManager';
import { CollectionFreeFormView } from '../collections/collectionFreeForm';
+import { CollectionStackingView } from '../collections/CollectionStackingView';
import { ContextMenu } from '../ContextMenu';
import { ContextMenuProps } from '../ContextMenuItem';
import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent';
@@ -22,10 +28,11 @@ import { LightboxView } from '../LightboxView';
import { CreateImage } from '../nodes/WebBoxRenderer';
import { PDFViewer } from '../pdf/PDFViewer';
import { SidebarAnnos } from '../SidebarAnnos';
+import { DocFocusOptions, DocumentView, OpenWhere } from './DocumentView';
import { FieldView, FieldViewProps } from './FieldView';
import { ImageBox } from './ImageBox';
import './PDFBox.scss';
-import { VideoBox } from './VideoBox';
+import { PinProps, PresBox } from './trails';
import React = require('react');
@observer
@@ -39,7 +46,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
private _initialScrollTarget: Opt<Doc>;
private _pdfViewer: PDFViewer | undefined;
private _searchRef = React.createRef<HTMLInputElement>();
- private _selectReactionDisposer: IReactionDisposer | undefined;
+ private _disposers: { [name: string]: IReactionDisposer } = {};
private _sidebarRef = React.createRef<SidebarAnnos>();
@observable private _searching: boolean = false;
@@ -57,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._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)));
@@ -93,38 +100,38 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
const cropping = Doc.MakeCopy(region, true);
Doc.GetProto(region).lockedPosition = true;
Doc.GetProto(region).title = 'region:' + this.rootDoc.title;
- Doc.GetProto(region).isPushpin = true;
+ Doc.GetProto(region).followLinkToggle = true;
this.addDocument(region);
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);
cropping.y = NumCast(this.rootDoc.y);
cropping._width = anchw;
cropping._height = anchh;
- cropping.isLinkButton = undefined;
+ cropping.onClick = undefined;
const croppingProto = Doc.GetProto(cropping);
croppingProto.annotationOn = undefined;
- croppingProto.isPrototype = true;
+ croppingProto.isDataDoc = true;
croppingProto.proto = Cast(this.rootDoc.proto, Doc, null)?.proto; // set proto of cropping's data doc to be IMAGE_PROTO
croppingProto.type = DocumentType.IMG;
croppingProto.layout = ImageBox.LayoutString('data');
croppingProto.data = new ImageField(Utils.CorsProxy('http://www.cs.brown.edu/~bcz/noImage.png'));
- croppingProto['data-nativeWidth'] = anchw;
- croppingProto['data-nativeHeight'] = anchh;
+ croppingProto['data_nativeWidth'] = anchw;
+ croppingProto['data_nativeHeight'] = anchh;
if (addCrop) {
- DocUtils.MakeLink({ doc: region }, { doc: cropping }, 'cropped image', '');
+ DocUtils.MakeLink(region, cropping, { link_relationship: 'cropped image' });
}
this.props.bringToFront(cropping);
@@ -134,12 +141,12 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
htmlString,
anchw,
anchh,
- (NumCast(region.y) * this.props.PanelWidth()) / NumCast(this.rootDoc[this.fieldKey + '-nativeWidth']),
- (NumCast(region.x) * this.props.PanelWidth()) / NumCast(this.rootDoc[this.fieldKey + '-nativeWidth']),
+ (NumCast(region.y) * this.props.PanelWidth()) / NumCast(this.rootDoc[this.fieldKey + '_nativeWidth']),
+ (NumCast(region.x) * this.props.PanelWidth()) / NumCast(this.rootDoc[this.fieldKey + '_nativeWidth']),
4
)
.then((data_url: any) => {
- VideoBox.convertDataUri(data_url, region[Id]).then(returnedfilename =>
+ Utils.convertDataUri(data_url, region[Id]).then(returnedfilename =>
setTimeout(
action(() => {
croppingProto.data = new ImageField(returnedfilename);
@@ -163,30 +170,30 @@ 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._scrollTop),
- NumCast(this.rootDoc[this.fieldKey + '-nativeHeight'], 1),
+ NumCast(this.layoutDoc._layout_scrollTop),
+ NumCast(this.rootDoc[this.fieldKey + '_nativeHeight'], 1),
true,
this.layoutDoc[Id] + '-icon',
(iconFile: string, nativeWidth: number, nativeHeight: number) => {
setTimeout(() => {
this.dataDoc.icon = new ImageField(iconFile);
- this.dataDoc['icon-nativeWidth'] = nativeWidth;
- this.dataDoc['icon-nativeHeight'] = nativeHeight;
+ this.dataDoc['icon_nativeWidth'] = nativeWidth;
+ this.dataDoc['icon_nativeHeight'] = nativeHeight;
}, 500);
}
);
};
componentWillUnmount() {
- this._selectReactionDisposer?.();
+ Object.values(this._disposers).forEach(disposer => disposer?.());
}
componentDidMount() {
this.props.setContentView?.(this);
- this._selectReactionDisposer = reaction(
+ this._disposers.select = reaction(
() => this.props.isSelected(),
() => {
document.removeEventListener('keydown', this.onKeyDown);
@@ -194,40 +201,66 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
},
{ fireImmediately: true }
);
+ this._disposers.scroll = reaction(
+ () => this.rootDoc.layout_scrollTop,
+ () => {
+ if (!(ComputedField.WithoutComputed(() => FieldValue(this.props.Document[this.SidebarKey + '_panY'])) instanceof ComputedField)) {
+ this.props.Document[this.SidebarKey + '_panY'] = ComputedField.MakeFunction('this.layout_scrollTop');
+ }
+ this.props.Document[this.SidebarKey + '_freeform_scale'] = 1;
+ this.props.Document[this.SidebarKey + '_freeform_panX'] = 0;
+ }
+ );
}
- brushView = (view: { width: number; height: number; panX: number; panY: number }) => {
- this._pdfViewer?.brushView(view);
- };
- scrollFocus = (doc: Doc, smooth: boolean) => {
- let didToggle = false;
- if (DocListCast(this.props.Document[this.fieldKey + '-sidebar']).includes(doc) && !this.SidebarShown) {
- this.toggleSidebar(!smooth);
- didToggle = true;
+ brushView = (view: { width: number; height: number; panX: number; panY: number }) => this._pdfViewer?.brushView(view);
+
+ sidebarAddDocTab = (doc: Doc, where: OpenWhere) => {
+ if (DocListCast(this.props.Document[this.props.fieldKey + '_sidebar']).includes(doc) && !this.SidebarShown) {
+ this.toggleSidebar(false);
+ return true;
}
- if (this._sidebarRef?.current?.makeDocUnfiltered(doc)) return 1;
- this._initialScrollTarget = doc;
- return this._pdfViewer?.scrollFocus(doc, smooth) ?? (didToggle ? 1 : undefined);
+ return this.props.addDocTab(doc, where);
+ };
+ focus = (anchor: Doc, options: DocFocusOptions) => {
+ this._initialScrollTarget = anchor;
+ return this._pdfViewer?.scrollFocus(anchor, NumCast(anchor.y, NumCast(anchor.presViewScroll)), options);
};
- getAnchor = () => {
- const anchor =
- this._pdfViewer?._getAnchor(this._pdfViewer.savedAnnotations()) ??
- Docs.Create.TextanchorDocument({
- title: StrCast(this.rootDoc.title + '@' + NumCast(this.layoutDoc._scrollTop)?.toFixed(0)),
- y: NumCast(this.layoutDoc._scrollTop),
- unrendered: true,
+
+ getView = async (doc: Doc) => {
+ if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) this.toggleSidebar(false);
+ return new Promise<Opt<DocumentView>>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv)));
+ };
+
+ getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
+ let ele: Opt<HTMLDivElement> = undefined;
+ if (this._pdfViewer?.selectionContent()) {
+ ele = document.createElement('div');
+ ele.append(this._pdfViewer.selectionContent()!);
+ }
+ const docAnchor = () =>
+ Docs.Create.ConfigDocument({
+ title: StrCast(this.rootDoc.title + '@' + NumCast(this.layoutDoc._layout_scrollTop)?.toFixed(0)),
+ annotationOn: this.rootDoc,
});
- this.addDocument(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);
+ anchor.text = ele?.textContent ?? '';
+ anchor.textHtml = ele?.innerHTML;
+ if (addAsAnnotation || annoAnchor) {
+ this.addDocument(anchor);
+ }
return anchor;
};
@action
loaded = (nw: number, nh: number, np: number) => {
- this.dataDoc[this.props.fieldKey + '-numPages'] = np;
+ 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._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) => {
@@ -244,14 +277,14 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
public prevAnnotation = () => this._pdfViewer?.prevAnnotation();
public nextAnnotation = () => this._pdfViewer?.nextAnnotation();
public backPage = () => {
- this.Document._curPage = Math.max(1, (NumCast(this.Document._curPage) || 1) - 1);
+ this.Document._layout_curPage = Math.max(1, (NumCast(this.Document._layout_curPage) || 1) - 1);
return true;
};
public forwardPage = () => {
- this.Document._curPage = Math.min(NumCast(this.dataDoc[this.props.fieldKey + '-numPages']), (NumCast(this.Document._curPage) || 1) + 1);
+ this.Document._layout_curPage = Math.min(NumCast(this.dataDoc[this.props.fieldKey + '_numPages']), (NumCast(this.Document._layout_curPage) || 1) + 1);
return true;
};
- public gotoPage = (p: number) => (this.Document._curPage = p);
+ public gotoPage = (p: number) => (this.Document._layout_curPage = p);
@undoBatch
onKeyDown = action((e: KeyboardEvent) => {
@@ -272,17 +305,18 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
setPdfViewer = (pdfViewer: PDFViewer) => {
this._pdfViewer = pdfViewer;
- if (this._initialScrollTarget) {
- this.scrollFocus(this._initialScrollTarget, false);
+ const docView = this.props.DocumentView?.();
+ if (this._initialScrollTarget && docView) {
+ this.focus(this._initialScrollTarget, { instant: true });
this._initialScrollTarget = undefined;
}
};
searchStringChanged = (e: React.ChangeEvent<HTMLInputElement>) => (this._searchString = e.currentTarget.value);
// adding external documents; to sidebar key
- // if (doc.Geolocation) this.addDocument(doc, this.fieldkey+"-annotation")
- sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => {
- if (!this.layoutDoc._showSidebar) this.toggleSidebar();
+ // if (doc.Geolocation) this.addDocument(doc, this.fieldkey+"_annotation")
+ sidebarAddDocument = (doc: Doc | Doc[], sidebarKey: string = this.SidebarKey) => {
+ if (!this.layoutDoc._show_sidebar) this.toggleSidebar();
return this.addDocument(doc, sidebarKey);
};
sidebarBtnDown = (e: React.PointerEvent, onButton: boolean) => {
@@ -296,19 +330,19 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
.ScreenToLocalTransform()
.scale(this.props.NativeDimScaling?.() || 1)
.transformDirection(delta[0], delta[1]);
- const nativeWidth = NumCast(this.layoutDoc[this.fieldKey + '-nativeWidth']);
+ const nativeWidth = NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth']);
const curNativeWidth = NumCast(this.layoutDoc.nativeWidth, nativeWidth);
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]);
- this.layoutDoc._showSidebar = nativeWidth !== this.layoutDoc._nativeWidth;
+ onButton && (this.layoutDoc._width = this.layoutDoc[Width]() + localDelta[0]);
+ this.layoutDoc._show_sidebar = nativeWidth !== this.layoutDoc._nativeWidth;
}
return false;
},
(e, movement, isClick) => !isClick && batch.end(),
() => {
- this.toggleSidebar();
+ onButton && this.toggleSidebar();
batch.end();
}
);
@@ -316,18 +350,18 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
@observable _previewNativeWidth: Opt<number> = undefined;
@observable _previewWidth: Opt<number> = undefined;
toggleSidebar = action((preview: boolean = false) => {
- const nativeWidth = NumCast(this.layoutDoc[this.fieldKey + '-nativeWidth']);
+ const nativeWidth = NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth']);
const sideratio = ((!this.layoutDoc.nativeWidth || this.layoutDoc.nativeWidth === nativeWidth ? PDFBox.openSidebarWidth : 0) + nativeWidth) / nativeWidth;
const pdfratio = ((!this.layoutDoc.nativeWidth || this.layoutDoc.nativeWidth === nativeWidth ? PDFBox.openSidebarWidth + PDFBox.sidebarResizerWidth : 0) + nativeWidth) / nativeWidth;
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._showSidebar = nativeWidth !== this.layoutDoc._nativeWidth;
+ this.layoutDoc._width = (this.layoutDoc[Width]() * nativeWidth * pdfratio) / curNativeWidth;
+ this.layoutDoc._show_sidebar = nativeWidth !== this.layoutDoc._nativeWidth;
}
});
settingsPanel() {
@@ -342,7 +376,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
</>
);
const searchTitle = `${!this._searching ? 'Open' : 'Close'} Search Bar`;
- const curPage = NumCast(this.Document._curPage) || 1;
+ const curPage = NumCast(this.Document._layout_curPage) || 1;
return !this.props.isContentActive() || this._pdfViewer?.isAnnotating ? null : (
<div
className="pdfBox-ui"
@@ -388,7 +422,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
<input
value={curPage}
style={{ width: `${curPage > 99 ? 4 : 3}ch`, pointerEvents: 'all' }}
- onChange={e => (this.Document._curPage = Number(e.currentTarget.value))}
+ onChange={e => (this.Document._layout_curPage = Number(e.currentTarget.value))}
onKeyDown={e => e.stopPropagation()}
onClick={action(() => (this._pageControls = !this._pageControls))}
/>
@@ -404,12 +438,20 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
const nativeDiff = NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc);
return PDFBox.sidebarResizerWidth + nativeDiff * (this.props.NativeDimScaling?.() || 1);
};
+ @undoBatch
+ toggleSidebarType = () => (this.rootDoc.sidebar_collectionType = this.rootDoc.sidebar_collectionType === CollectionViewType.Freeform ? CollectionViewType.Stacking : CollectionViewType.Freeform);
specificContextMenu = (e: React.MouseEvent): void => {
- const funcs: ContextMenuProps[] = [];
- funcs.push({ description: 'Copy path', event: () => this.pdfUrl && Utils.CopyText(Utils.prepend('') + this.pdfUrl.url.pathname), icon: 'expand-arrows-alt' });
- funcs.push({ description: 'update icon', event: () => this.pdfUrl && this.updateIcon(), icon: 'expand-arrows-alt' });
- //funcs.push({ description: "Toggle Sidebar ", event: () => this.toggleSidebar(), icon: "expand-arrows-alt" });
- ContextMenu.Instance.addItem({ description: 'Options...', subitems: funcs, icon: 'asterisk' });
+ const cm = ContextMenu.Instance;
+ const options = cm.findByDescription('Options...');
+ const optionItems: ContextMenuProps[] = options && 'subitems' in options ? options.subitems : [];
+ optionItems.push({ description: 'Toggle Sidebar Type', event: this.toggleSidebarType, icon: 'expand-arrows-alt' });
+ !Doc.noviceMode && optionItems.push({ description: 'update icon', event: () => this.pdfUrl && this.updateIcon(), icon: 'expand-arrows-alt' });
+ //optionItems.push({ description: "Toggle Sidebar ", event: () => this.toggleSidebar(), icon: "expand-arrows-alt" });
+ !options && ContextMenu.Instance.addItem({ description: 'Options...', subitems: optionItems, icon: 'asterisk' });
+ const help = cm.findByDescription('Help...');
+ const helpItems: ContextMenuProps[] = help && 'subitems' in help ? help.subitems : [];
+ helpItems.push({ description: 'Copy path', event: () => this.pdfUrl && Utils.CopyText(Utils.prepend('') + this.pdfUrl.url.pathname), icon: 'expand-arrows-alt' });
+ !help && ContextMenu.Instance.addItem({ description: 'Help...', noexpand: true, subitems: helpItems, icon: 'asterisk' });
};
@computed get renderTitleBox() {
@@ -426,7 +468,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick;
@observable _showSidebar = false;
@computed get SidebarShown() {
- return this._showSidebar || this.layoutDoc._showSidebar ? true : false;
+ return this._showSidebar || this.layoutDoc._show_sidebar ? true : false;
}
@computed get sidebarHandle() {
return (
@@ -436,7 +478,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
title="Toggle Sidebar"
style={{
display: !this.props.isContentActive() ? 'none' : undefined,
- top: StrCast(this.rootDoc._showTitle) === 'title' ? 20 : 5,
+ top: StrCast(this.rootDoc._layout_showTitle) === 'title' ? 20 : 5,
backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK,
}}
onPointerDown={e => this.sidebarBtnDown(e, true)}>
@@ -445,18 +487,89 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
);
}
- isPdfContentActive = () => this.isAnyChildContentActive() || this.props.isSelected() || (this.props.renderDepth === 0 && LightboxView.IsLightboxDocView(this.props.docViewPath()));
+ public get SidebarKey() {
+ return this.fieldKey + '_sidebar';
+ }
+ @computed get pdfScale() {
+ const pdfNativeWidth = NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth']);
+ const nativeWidth = NumCast(this.layoutDoc.nativeWidth, pdfNativeWidth);
+ const pdfRatio = pdfNativeWidth / nativeWidth;
+ return (pdfRatio * this.props.PanelWidth()) / pdfNativeWidth;
+ }
+ @computed get sidebarNativeWidth() {
+ return this.sidebarWidth() / this.pdfScale;
+ }
+ @computed get sidebarNativeHeight() {
+ return this.props.PanelHeight() / this.pdfScale;
+ }
+ sidebarNativeWidthFunc = () => this.sidebarNativeWidth;
+ sidebarNativeHeightFunc = () => this.sidebarNativeHeight;
+ sidebarMoveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => this.moveDocument(doc, targetCollection, addDocument, this.SidebarKey);
+ sidebarRemDocument = (doc: Doc | Doc[]) => this.removeDocument(doc, this.SidebarKey);
+ sidebarScreenToLocal = () => this.props.ScreenToLocalTransform().translate((this.sidebarWidth() - this.props.PanelWidth()) / this.pdfScale, 0);
+ @computed get sidebarCollection() {
+ const renderComponent = (tag: string) => {
+ const ComponentTag = tag === CollectionViewType.Freeform ? CollectionFreeFormView : CollectionStackingView;
+ return ComponentTag === CollectionStackingView ? (
+ <SidebarAnnos
+ ref={this._sidebarRef}
+ {...this.props}
+ rootDoc={this.rootDoc}
+ layoutDoc={this.layoutDoc}
+ dataDoc={this.dataDoc}
+ setHeight={emptyFunction}
+ nativeWidth={this._previewNativeWidth ?? NumCast(this.layoutDoc._nativeWidth)}
+ showSidebar={this.SidebarShown}
+ whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
+ sidebarAddDocument={this.sidebarAddDocument}
+ moveDocument={this.moveDocument}
+ removeDocument={this.removeDocument}
+ />
+ ) : (
+ <div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => SelectionManager.SelectView(this.props.DocumentView?.()!, false), true)}>
+ <ComponentTag
+ {...this.props}
+ setContentView={emptyFunction} // override setContentView to do nothing
+ NativeWidth={this.sidebarNativeWidthFunc}
+ NativeHeight={this.sidebarNativeHeightFunc}
+ PanelHeight={this.props.PanelHeight}
+ PanelWidth={this.sidebarWidth}
+ xPadding={0}
+ yPadding={0}
+ viewField={this.SidebarKey}
+ isAnnotationOverlay={false}
+ originTopLeft={true}
+ isAnyChildContentActive={this.isAnyChildContentActive}
+ select={emptyFunction}
+ whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
+ removeDocument={this.sidebarRemDocument}
+ moveDocument={this.sidebarMoveDocument}
+ addDocument={this.sidebarAddDocument}
+ ScreenToLocalTransform={this.sidebarScreenToLocal}
+ renderDepth={this.props.renderDepth + 1}
+ noSidebar={true}
+ fieldKey={this.SidebarKey}
+ />
+ </div>
+ );
+ };
+ return (
+ <div className={'formattedTextBox-sidebar' + (Doc.ActiveTool !== InkTool.None ? '-inking' : '')} style={{ width: '100%', right: 0, backgroundColor: `white` }}>
+ {renderComponent(StrCast(this.layoutDoc.sidebar_collectionType))}
+ </div>
+ );
+ }
@computed get renderPdfView() {
TraceMobx();
const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1;
const scale = previewScale * (this.props.NativeDimScaling?.() || 1);
- return (
+ return !this._pdf ? null : (
<div
- className={'pdfBox'}
+ className="pdfBox"
onContextMenu={this.specificContextMenu}
style={{
display: this.props.thumbShown?.() ? 'none' : undefined,
- height: this.props.Document._scrollTop && !this.Document._fitWidth && window.screen.width > 600 ? (NumCast(this.Document._height) * this.props.PanelWidth()) / NumCast(this.Document._width) : undefined,
+ height: this.props.Document._layout_scrollTop && !this.Document._layout_fitWidth && window.screen.width > 600 ? (NumCast(this.Document._height) * this.props.PanelWidth()) / NumCast(this.Document._width) : undefined,
}}>
<div className="pdfBox-background" onPointerDown={e => this.sidebarBtnDown(e, false)} />
<div
@@ -470,12 +583,14 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
}}>
<PDFViewer
{...this.props}
+ sidebarAddDoc={this.sidebarAddDocument}
rootDoc={this.rootDoc}
+ addDocTab={this.sidebarAddDocTab}
layoutDoc={this.layoutDoc}
dataDoc={this.dataDoc}
- pdf={this._pdf!}
+ 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}
@@ -486,22 +601,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
crop={this.crop}
/>
</div>
- <div style={{ position: 'absolute', height: '100%', right: 0, top: 0, width: `calc(100 * ${this.sidebarWidth() / this.props.PanelWidth()}%` }}>
- <SidebarAnnos
- ref={this._sidebarRef}
- {...this.props}
- rootDoc={this.rootDoc}
- layoutDoc={this.layoutDoc}
- dataDoc={this.dataDoc}
- setHeight={emptyFunction}
- nativeWidth={this._previewNativeWidth ?? NumCast(this.layoutDoc._nativeWidth)}
- showSidebar={this.SidebarShown}
- whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
- sidebarAddDocument={this.sidebarAddDocument}
- moveDocument={this.moveDocument}
- removeDocument={this.removeDocument}
- />
- </div>
+ <div style={{ position: 'absolute', height: '100%', right: 0, top: 0, width: `calc(100 * ${this.sidebarWidth() / this.props.PanelWidth()}%` }}>{this.sidebarCollection}</div>
{this.settingsPanel()}
</div>
);
@@ -511,21 +611,17 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
static pdfpromise = new Map<string, Promise<Pdfjs.PDFDocumentProxy>>();
render() {
TraceMobx();
- if (this._pdf) {
- if (!this.props.thumbShown?.()) {
- return this.renderPdfView;
- }
- return null;
- }
+ if (this.props.thumbShown?.()) return null;
+ const pdfView = this.renderPdfView;
const href = this.pdfUrl?.url.href;
- if (href) {
+ if (!pdfView && href) {
if (PDFBox.pdfcache.get(href)) setTimeout(action(() => (this._pdf = PDFBox.pdfcache.get(href))));
else {
if (!PDFBox.pdfpromise.get(href)) PDFBox.pdfpromise.set(href, Pdfjs.getDocument(href).promise);
PDFBox.pdfpromise.get(href)?.then(action((pdf: any) => PDFBox.pdfcache.set(href, (this._pdf = pdf))));
}
}
- return this.renderTitleBox;
+ return pdfView ?? this.renderTitleBox;
}
}
diff --git a/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.scss b/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.scss
new file mode 100644
index 000000000..ac2c611c7
--- /dev/null
+++ b/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.scss
@@ -0,0 +1,90 @@
+.physicsSimApp {
+ * {
+ box-sizing: border-box;
+ font-size: 14px;
+ }
+
+ .mechanicsSimulationContainer {
+ background-color: white;
+ height: 100%;
+ width: 100%;
+ display: flex;
+
+ .mechanicsSimulationEquationContainer {
+ position: fixed;
+ left: 60%;
+ padding: 1em;
+ height: 100%;
+ transform-origin: top left;
+
+ .mechanicsSimulationControls {
+ display: flex;
+ justify-content: space-between;
+ }
+ }
+ .rod,
+ .spring,
+ .wheel,
+ .showvecs,
+ .wedge {
+ pointer-events: none;
+ position: absolute;
+ left: 0;
+ top: 0;
+ }
+ }
+
+ .coordinateSystem {
+ z-index: -100;
+ }
+
+ th,
+ td {
+ border-collapse: collapse;
+ padding: 1em;
+ }
+
+ table {
+ min-width: 300px;
+ }
+
+ tr:nth-child(even) {
+ background-color: #d6eeee;
+ }
+
+ button {
+ z-index: 50;
+ }
+
+ .angleLabel {
+ font-weight: bold;
+ font-size: 20px;
+ user-select: none;
+ pointer-events: none;
+ }
+
+ .mechanicsSimulationSettingsMenu {
+ width: 100%;
+ height: 100%;
+ font-size: 12px;
+ background-color: rgb(224, 224, 224);
+ border-radius: 2px;
+ border-color: black;
+ border-style: solid;
+ padding: 10px;
+ position: fixed;
+ z-index: 1000;
+ }
+
+ .mechanicsSimulationSettingsMenuRow {
+ display: flex;
+ }
+
+ .mechanicsSimulationSettingsMenuRowDescription {
+ width: 50%;
+ }
+
+ .dropdownMenu {
+ z-index: 50;
+ }
+}
diff --git a/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.tsx b/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.tsx
new file mode 100644
index 000000000..cd1ff17dd
--- /dev/null
+++ b/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.tsx
@@ -0,0 +1,1987 @@
+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';
+import { List } from '../../../../fields/List';
+import { BoolCast, NumCast, StrCast } from '../../../../fields/Types';
+import { ViewBoxAnnotatableComponent } from '../../DocComponent';
+import { FieldView, FieldViewProps } from './../FieldView';
+import './PhysicsSimulationBox.scss';
+import InputField from './PhysicsSimulationInputField';
+import * as questions from './PhysicsSimulationQuestions.json';
+import * as tutorials from './PhysicsSimulationTutorial.json';
+import Wall from './PhysicsSimulationWall';
+import Weight from './PhysicsSimulationWeight';
+import React = require('react');
+
+interface IWallProps {
+ length: number;
+ xPos: number;
+ yPos: number;
+ angleInDegrees: number;
+}
+interface IForce {
+ description: string;
+ magnitude: number;
+ directionInDegrees: number;
+}
+interface VectorTemplate {
+ top: number;
+ left: number;
+ width: number;
+ height: number;
+ x1: number;
+ y1: number;
+ x2: number;
+ y2: number;
+ weightX: number;
+ weightY: number;
+}
+interface QuestionTemplate {
+ questionSetup: string[];
+ variablesForQuestionSetup: string[];
+ question: string;
+ answerParts: string[];
+ answerSolutionDescriptions: string[];
+ goal: string;
+ hints: { description: string; content: string }[];
+}
+
+interface TutorialTemplate {
+ question: string;
+ steps: {
+ description: string;
+ content: string;
+ forces: {
+ description: string;
+ magnitude: number;
+ directionInDegrees: number;
+ component: boolean;
+ }[];
+ showMagnitude: boolean;
+ }[];
+}
+
+@observer
+export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
+ public static LayoutString(fieldKey: string) {
+ return FieldView.LayoutString(PhysicsSimulationBox, fieldKey);
+ }
+
+ _widthDisposer: IReactionDisposer | undefined;
+ @observable _simReset = 0;
+
+ // semi-Constants
+ xMin = 0;
+ yMin = 0;
+ xMax = this.props.PanelWidth() * 0.6;
+ yMax = this.props.PanelHeight();
+ color = `rgba(0,0,0,0.5)`;
+ radius = 50;
+ wallPositions: IWallProps[] = [];
+
+ @computed get circularMotionRadius() {
+ return (NumCast(this.dataDoc.circularMotionRadius, 150) * this.props.PanelWidth()) / 1000;
+ }
+ @computed get gravity() {
+ return NumCast(this.dataDoc.simulation_gravity, -9.81);
+ }
+ @computed get simulationType() {
+ return StrCast(this.dataDoc.simulation_type, 'Inclined Plane');
+ }
+ @computed get simulationMode() {
+ return StrCast(this.dataDoc.simulation_mode, 'Freeform');
+ }
+ // Used for spring simulation
+ @computed get springConstant() {
+ return NumCast(this.dataDoc.spring_constant, 0.5);
+ }
+ @computed get springLengthRest() {
+ return NumCast(this.dataDoc.spring_lengthRest, 200);
+ }
+ @computed get springLengthStart() {
+ return NumCast(this.dataDoc.spring_lengthStart, 200);
+ }
+
+ @computed get pendulumAngle() {
+ return NumCast(this.dataDoc.pendulum_angle);
+ }
+ @computed get pendulumAngleStart() {
+ return NumCast(this.dataDoc.pendulum_angleStart);
+ }
+ @computed get pendulumLength() {
+ return NumCast(this.dataDoc.pendulum_length);
+ }
+ @computed get pendulumLengthStart() {
+ return NumCast(this.dataDoc.pendulum_lengthStart);
+ }
+
+ // Used for wedge simulation
+ @computed get wedgeAngle() {
+ return NumCast(this.dataDoc.wedge_angle, 26);
+ }
+ @computed get wedgeHeight() {
+ return NumCast(this.dataDoc.wedge_height, Math.tan((26 * Math.PI) / 180) * this.xMax * 0.5);
+ }
+ @computed get wedgeWidth() {
+ return NumCast(this.dataDoc.wedge_width, this.xMax * 0.5);
+ }
+ @computed get mass1() {
+ return NumCast(this.dataDoc.mass1, 1);
+ }
+ @computed get mass2() {
+ return NumCast(this.dataDoc.mass2, 1);
+ }
+
+ @computed get mass1Radius() {
+ return NumCast(this.dataDoc.mass1_radius, 30);
+ }
+ @computed get mass1PosXStart() {
+ return NumCast(this.dataDoc.mass1_positionXstart);
+ }
+ @computed get mass1PosYStart() {
+ return NumCast(this.dataDoc.mass1_positionYstart);
+ }
+ @computed get mass1VelXStart() {
+ return NumCast(this.dataDoc.mass1_velocityXstart);
+ }
+ @computed get mass1VelYStart() {
+ return NumCast(this.dataDoc.mass1_velocityYstart);
+ }
+
+ @computed get mass2PosXStart() {
+ return NumCast(this.dataDoc.mass2_positionXstart);
+ }
+ @computed get mass2PosYStart() {
+ return NumCast(this.dataDoc.mass2_positionYstart);
+ }
+ @computed get mass2VelXStart() {
+ return NumCast(this.dataDoc.mass2_velocityXstart);
+ }
+ @computed get mass2VelYStart() {
+ return NumCast(this.dataDoc.mass2_velocityYstart);
+ }
+
+ @computed get selectedQuestion() {
+ return this.dataDoc.selectedQuestion ? (JSON.parse(StrCast(this.dataDoc.selectedQuestion)) as QuestionTemplate) : questions.inclinePlane[0];
+ }
+ @computed get tutorial() {
+ return this.dataDoc.tutorial ? (JSON.parse(StrCast(this.dataDoc.tutorial)) as TutorialTemplate) : tutorials.inclinePlane;
+ }
+ @computed get selectedSolutions() {
+ return NumListCast(this.dataDoc.selectedSolutions);
+ }
+ @computed get questionPartOne() {
+ return StrCast(this.dataDoc.questionPartOne);
+ }
+ @computed get questionPartTwo() {
+ return StrCast(this.dataDoc.questionPartTwo);
+ }
+
+ componentWillUnmount() {
+ this._widthDisposer?.();
+ }
+
+ componentDidMount() {
+ // Setup and update simulation
+ this._widthDisposer = reaction(() => [this.props.PanelWidth(), this.props.PanelHeight()], this.setupSimulation, { fireImmediately: true });
+
+ // Create walls
+ this.wallPositions = [
+ { length: 100, xPos: 0, yPos: 0, angleInDegrees: 0 },
+ { length: 100, xPos: 0, yPos: 100, angleInDegrees: 0 },
+ { length: 100, xPos: 0, yPos: 0, angleInDegrees: 90 },
+ { length: 100, xPos: (this.xMax / this.props.PanelWidth()) * 100, yPos: 0, angleInDegrees: 90 },
+ ];
+ }
+
+ componentDidUpdate() {
+ if (this.xMax !== this.props.PanelWidth() * 0.6 || this.yMax != this.props.PanelHeight()) {
+ this.xMax = this.props.PanelWidth() * 0.6;
+ this.yMax = this.props.PanelHeight();
+ this.setupSimulation();
+ }
+ }
+
+ gravityForce = (mass: number): IForce => ({
+ description: 'Gravity',
+ magnitude: mass * Math.abs(this.gravity),
+ directionInDegrees: 270,
+ });
+
+ @action
+ setupSimulation = () => {
+ const simulationType = this.simulationType;
+ const mode = this.simulationMode;
+ this.dataDoc.simulation_paused = true;
+ if (simulationType != 'Circular Motion') {
+ this.dataDoc.mass1_velocityXstart = 0;
+ this.dataDoc.mass1_velocityYstart = 0;
+ this.dataDoc.mass1_velocityX = 0;
+ this.dataDoc.mass1_velocityY = 0;
+ }
+ if (mode == 'Freeform') {
+ this.dataDoc.simulation_showForceMagnitudes = true;
+ // prettier-ignore
+ switch (simulationType) {
+ case 'One Weight':
+ this.dataDoc.simulation_showComponentForces = false;
+ this.dataDoc.mass1_positionYstart = this.yMin + this.mass1Radius;
+ this.dataDoc.mass1_positionXstart = (this.xMax + this.xMin) / 2 - this.mass1Radius;
+ this.dataDoc.mass1_positionY = this.getDisplayYPos(this.yMin + this.mass1Radius);
+ this.dataDoc.mass1_positionX = (this.xMax + this.xMin) / 2 - this.mass1Radius;
+ this.dataDoc.mass1_forcesUpdated = JSON.stringify([this.gravityForce(this.mass1)]);
+ this.dataDoc.mass1_forcesStart = JSON.stringify([this.gravityForce(this.mass1)]);
+ break;
+ case 'Inclined Plane': this.setupInclinedPlane(); break;
+ case 'Pendulum': this.setupPendulum(); break;
+ case 'Spring': this.setupSpring(); break;
+ case 'Circular Motion': this.setupCircular(20); break;
+ case 'Pulley': this.setupPulley(); break;
+ case 'Suspension': this.setupSuspension();break;
+ }
+ this._simReset++;
+ } else if (mode == 'Review') {
+ this.dataDoc.simulation_showComponentForces = false;
+ this.dataDoc.simulation_showForceMagnitudes = true;
+ this.dataDoc.simulation_showAcceleration = false;
+ this.dataDoc.simulation_showVelocity = false;
+ this.dataDoc.simulation_showForces = true;
+ this.generateNewQuestion();
+ // prettier-ignore
+ switch (simulationType) {
+ case 'One Weight' : break;// TODO - one weight review problems
+ case 'Spring': this.setupSpring(); break; // TODO - spring review problems
+ case 'Inclined Plane': this.dataDoc.mass1_forcesUpdated = this.dataDoc.mass1_forcesStart = ''; break;
+ case 'Pendulum': this.setupPendulum(); break; // TODO - pendulum review problems
+ case 'Circular Motion': this.setupCircular(0); break; // TODO - circular motion review problems
+ case 'Pulley': this.setupPulley(); break; // TODO - pulley tutorial review problems
+ case 'Suspension': this.setupSuspension(); break; // TODO - suspension tutorial review problems
+ }
+ } else if (mode == 'Tutorial') {
+ this.dataDoc.simulation_showComponentForces = false;
+ this.dataDoc.tutorial_stepNumber = 0;
+ this.dataDoc.simulation_showAcceleration = false;
+ if (this.simulationType != 'Circular Motion') {
+ this.dataDoc.mass1_velocityX = 0;
+ this.dataDoc.mass1_velocityY = 0;
+ this.dataDoc.simulation_showVelocity = false;
+ } else {
+ this.dataDoc.mass1_velocityX = 20;
+ this.dataDoc.mass1_velocityY = 0;
+ this.dataDoc.simulation_showVelocity = true;
+ }
+
+ switch (this.simulationType) {
+ case 'One Weight':
+ this.dataDoc.simulation_showForces = true;
+ this.dataDoc.mass1_positionYstart = this.yMax - 100;
+ this.dataDoc.mass1_positionXstart = (this.xMax + this.xMin) / 2 - this.mass1Radius;
+ this.dataDoc.tutorial = JSON.stringify(tutorials.freeWeight);
+ this.dataDoc.mass1_forcesStart = JSON.stringify(tutorials.freeWeight.steps[0].forces);
+ this.dataDoc.simulation_showForceMagnitudes = tutorials.freeWeight.steps[0].showMagnitude;
+ break;
+ case 'Spring':
+ this.dataDoc.simulation_showForces = true;
+ this.setupSpring();
+ this.dataDoc.mass1_positionYstart = this.yMin + 200 + 19.62;
+ this.dataDoc.mass1_positionXstart = (this.xMax + this.xMin) / 2 - this.mass1Radius;
+ this.dataDoc.tutorial = JSON.stringify(tutorials.spring);
+ this.dataDoc.mass1_forcesStart = JSON.stringify(tutorials.spring.steps[0].forces);
+ this.dataDoc.simulation_showForceMagnitudes = tutorials.spring.steps[0].showMagnitude;
+ break;
+ case 'Pendulum':
+ this.setupPendulum();
+ this.dataDoc.tutorial = JSON.stringify(tutorials.pendulum);
+ this.dataDoc.mass1_forcesStart = JSON.stringify(tutorials.pendulum.steps[0].forces);
+ this.dataDoc.simulation_showForceMagnitudes = tutorials.pendulum.steps[0].showMagnitude;
+ break;
+ case 'Inclined Plane':
+ this.dataDoc.wedge_angle = 26;
+ this.setupInclinedPlane();
+ this.dataDoc.simulation_showForces = true;
+ this.dataDoc.tutorial = JSON.stringify(tutorials.inclinePlane);
+ this.dataDoc.mass1_forcesStart = JSON.stringify(tutorials.inclinePlane.steps[0].forces);
+ this.dataDoc.simulation_showForceMagnitudes = tutorials.inclinePlane.steps[0].showMagnitude;
+ break;
+ case 'Circular Motion':
+ this.dataDoc.simulation_showForces = true;
+ this.setupCircular(40);
+ this.dataDoc.tutorial = JSON.stringify(tutorials.circular);
+ this.dataDoc.mass1_forcesStart = JSON.stringify(tutorials.circular.steps[0].forces);
+ this.dataDoc.simulation_showForceMagnitudes = tutorials.circular.steps[0].showMagnitude;
+ break;
+ case 'Pulley':
+ this.dataDoc.simulation_showForces = true;
+ this.setupPulley();
+ this.dataDoc.tutorial = JSON.stringify(tutorials.pulley);
+ this.dataDoc.mass1_forcesStart = JSON.stringify(tutorials.pulley.steps[0].forces);
+ this.dataDoc.simulation_showForceMagnitudes = tutorials.pulley.steps[0].showMagnitude;
+ break;
+ case 'Suspension':
+ this.dataDoc.simulation_showForces = true;
+ this.setupSuspension();
+ this.dataDoc.tutorial = JSON.stringify(tutorials.suspension);
+ this.dataDoc.mass1_forcesStart = JSON.stringify(tutorials.suspension.steps[0].forces);
+ this.dataDoc.simulation_showForceMagnitudes = tutorials.suspension.steps[0].showMagnitude;
+ break;
+ }
+ this._simReset++;
+ }
+ };
+
+ // Helper function to go between display and real values
+ getDisplayYPos = (yPos: number) => this.yMax - yPos - 2 * this.mass1Radius + 5;
+ getYPosFromDisplay = (yDisplay: number) => this.yMax - yDisplay - 2 * this.mass1Radius + 5;
+
+ // Update forces when coefficient of static friction changes in freeform mode
+ updateForcesWithFriction = (coefficient: number, width = this.wedgeWidth, height = this.wedgeHeight) => {
+ const normalForce: IForce = {
+ description: 'Normal Force',
+ magnitude: Math.abs(this.gravity) * Math.cos(Math.atan(height / width)) * this.mass1,
+ directionInDegrees: 180 - 90 - (Math.atan(height / width) * 180) / Math.PI,
+ };
+ let frictionForce: IForce = {
+ description: 'Static Friction Force',
+ magnitude: coefficient * Math.abs(this.gravity) * Math.cos(Math.atan(height / width)) * this.mass1,
+ directionInDegrees: 180 - (Math.atan(height / width) * 180) / Math.PI,
+ };
+ // reduce magnitude or friction force if necessary such that block cannot slide up plane
+ let yForce = -Math.abs(this.gravity) * this.mass1;
+ yForce += normalForce.magnitude * Math.sin((normalForce.directionInDegrees * Math.PI) / 180);
+ yForce += frictionForce.magnitude * Math.sin((frictionForce.directionInDegrees * Math.PI) / 180);
+ if (yForce > 0) {
+ frictionForce.magnitude = (-normalForce.magnitude * Math.sin((normalForce.directionInDegrees * Math.PI) / 180) + Math.abs(this.gravity) * this.mass1) / Math.sin((frictionForce.directionInDegrees * Math.PI) / 180);
+ }
+
+ const normalForceComponent: IForce = {
+ description: 'Normal Force',
+ magnitude: this.mass1 * Math.abs(this.gravity) * Math.cos(Math.atan(height / width)),
+ directionInDegrees: 180 - 90 - (Math.atan(height / width) * 180) / Math.PI,
+ };
+ const gravityParallel: IForce = {
+ description: 'Gravity Parallel Component',
+ magnitude: this.mass1 * Math.abs(this.gravity) * Math.sin(Math.PI / 2 - Math.atan(height / width)),
+ directionInDegrees: 180 - 90 - (Math.atan(height / width) * 180) / Math.PI + 180,
+ };
+ const gravityPerpendicular: IForce = {
+ description: 'Gravity Perpendicular Component',
+ magnitude: this.mass1 * Math.abs(this.gravity) * Math.cos(Math.PI / 2 - Math.atan(height / width)),
+ directionInDegrees: 360 - (Math.atan(height / width) * 180) / Math.PI,
+ };
+ const gravityForce = this.gravityForce(this.mass1);
+ if (coefficient != 0) {
+ this.dataDoc.mass1_forcesStart = JSON.stringify([gravityForce, normalForce, frictionForce]);
+ this.dataDoc.mass1_forcesUpdated = JSON.stringify([gravityForce, normalForce, frictionForce]);
+ this.dataDoc.mass1_componentForces = JSON.stringify([frictionForce, normalForceComponent, gravityParallel, gravityPerpendicular]);
+ } else {
+ this.dataDoc.mass1_forcesStart = JSON.stringify([gravityForce, normalForce]);
+ this.dataDoc.mass1_forcesUpdated = JSON.stringify([gravityForce, normalForce]);
+ this.dataDoc.mass1_componentForces = JSON.stringify([normalForceComponent, gravityParallel, gravityPerpendicular]);
+ }
+ };
+
+ // Change wedge height and width and weight position to match new wedge angle
+ changeWedgeBasedOnNewAngle = (angle: number) => {
+ const radAng = (angle * Math.PI) / 180;
+ this.dataDoc.wedge_width = this.xMax * 0.5;
+ this.dataDoc.wedge_height = Math.tan(radAng) * this.dataDoc.wedge_width;
+
+ // update weight position based on updated wedge width/height
+ let yPos = this.yMax - this.dataDoc.wedge_height - this.mass1Radius * Math.cos(radAng) - this.mass1Radius;
+ let xPos = this.xMax * 0.25 + this.mass1Radius * Math.sin(radAng) - this.mass1Radius;
+
+ this.dataDoc.mass1_positionXstart = xPos;
+ this.dataDoc.mass1_positionYstart = yPos;
+ if (this.simulationMode == 'Freeform') {
+ this.updateForcesWithFriction(NumCast(this.dataDoc.coefficientOfStaticFriction), this.dataDoc.wedge_width, Math.tan(radAng) * this.dataDoc.wedge_width);
+ }
+ };
+
+ // In review mode, update forces when coefficient of static friction changed
+ updateReviewForcesBasedOnCoefficient = (coefficient: number) => {
+ let theta = this.wedgeAngle;
+ let index = this.selectedQuestion.variablesForQuestionSetup.indexOf('theta - max 45');
+ if (index >= 0) {
+ theta = NumListCast(this.dataDoc.questionVariables)[index];
+ }
+ if (isNaN(theta)) {
+ return;
+ }
+ this.dataDoc.review_GravityMagnitude = Math.abs(this.gravity);
+ this.dataDoc.review_GravityAngle = 270;
+ this.dataDoc.review_NormalMagnitude = Math.abs(this.gravity) * Math.cos((theta * Math.PI) / 180);
+ this.dataDoc.review_NormalAngle = 90 - theta;
+ let yForce = -Math.abs(this.gravity);
+ yForce += Math.abs(this.gravity) * Math.cos((theta * Math.PI) / 180) * Math.sin(((90 - theta) * Math.PI) / 180);
+ yForce += coefficient * Math.abs(this.gravity) * Math.cos((theta * Math.PI) / 180) * Math.sin(((180 - theta) * Math.PI) / 180);
+ let friction = coefficient * Math.abs(this.gravity) * Math.cos((theta * Math.PI) / 180);
+ if (yForce > 0) {
+ friction = (-(Math.abs(this.gravity) * Math.cos((theta * Math.PI) / 180)) * Math.sin(((90 - theta) * Math.PI) / 180) + Math.abs(this.gravity)) / Math.sin(((180 - theta) * Math.PI) / 180);
+ }
+ this.dataDoc.review_StaticMagnitude = friction;
+ this.dataDoc.review_StaticAngle = 180 - theta;
+ };
+
+ // In review mode, update forces when wedge angle changed
+ updateReviewForcesBasedOnAngle = (angle: number) => {
+ this.dataDoc.review_GravityMagnitude = Math.abs(this.gravity);
+ this.dataDoc.review_GravityAngle = 270;
+ this.dataDoc.review_NormalMagnitude = Math.abs(this.gravity) * Math.cos((angle * Math.PI) / 180);
+ this.dataDoc.review_NormalAngle = 90 - angle;
+ let yForce = -Math.abs(this.gravity);
+ yForce += Math.abs(this.gravity) * Math.cos((angle * Math.PI) / 180) * Math.sin(((90 - angle) * Math.PI) / 180);
+ yForce += NumCast(this.dataDoc.review_Coefficient) * Math.abs(this.gravity) * Math.cos((angle * Math.PI) / 180) * Math.sin(((180 - angle) * Math.PI) / 180);
+ let friction = NumCast(this.dataDoc.review_Coefficient) * Math.abs(this.gravity) * Math.cos((angle * Math.PI) / 180);
+ if (yForce > 0) {
+ friction = (-(Math.abs(this.gravity) * Math.cos((angle * Math.PI) / 180)) * Math.sin(((90 - angle) * Math.PI) / 180) + Math.abs(this.gravity)) / Math.sin(((180 - angle) * Math.PI) / 180);
+ }
+ this.dataDoc.review_StaticMagnitude = friction;
+ this.dataDoc.review_StaticAngle = 180 - angle;
+ };
+
+ // Solve for the correct answers to the generated problem
+ getAnswersToQuestion = (question: QuestionTemplate, questionVars: number[]) => {
+ const solutions: number[] = [];
+
+ let theta = this.wedgeAngle;
+ let index = question.variablesForQuestionSetup.indexOf('theta - max 45');
+ if (index >= 0) {
+ theta = questionVars[index];
+ }
+ let muS: number = NumCast(this.dataDoc.coefficientOfStaticFriction);
+ index = question.variablesForQuestionSetup.indexOf('coefficient of static friction');
+ if (index >= 0) {
+ muS = questionVars[index];
+ }
+
+ for (let i = 0; i < question.answerSolutionDescriptions.length; i++) {
+ const description = question.answerSolutionDescriptions[i];
+ if (!isNaN(NumCast(description))) {
+ solutions.push(NumCast(description));
+ } else if (description == 'solve normal force angle from wedge angle') {
+ solutions.push(90 - theta);
+ } else if (description == 'solve normal force magnitude from wedge angle') {
+ solutions.push(Math.abs(this.gravity) * Math.cos((theta / 180) * Math.PI));
+ } else if (description == 'solve static force magnitude from wedge angle given equilibrium') {
+ let normalForceMagnitude = Math.abs(this.gravity) * Math.cos((theta / 180) * Math.PI);
+ let normalForceAngle = 90 - theta;
+ let frictionForceAngle = 180 - theta;
+ let frictionForceMagnitude = (-normalForceMagnitude * Math.sin((normalForceAngle * Math.PI) / 180) + Math.abs(this.gravity)) / Math.sin((frictionForceAngle * Math.PI) / 180);
+ solutions.push(frictionForceMagnitude);
+ } else if (description == 'solve static force angle from wedge angle given equilibrium') {
+ solutions.push(180 - theta);
+ } else if (description == 'solve minimum static coefficient from wedge angle given equilibrium') {
+ let normalForceMagnitude = Math.abs(this.gravity) * Math.cos((theta / 180) * Math.PI);
+ let normalForceAngle = 90 - theta;
+ let frictionForceAngle = 180 - theta;
+ let frictionForceMagnitude = (-normalForceMagnitude * Math.sin((normalForceAngle * Math.PI) / 180) + Math.abs(this.gravity)) / Math.sin((frictionForceAngle * Math.PI) / 180);
+ let frictionCoefficient = frictionForceMagnitude / normalForceMagnitude;
+ solutions.push(frictionCoefficient);
+ } else if (description == 'solve maximum wedge angle from coefficient of static friction given equilibrium') {
+ solutions.push((Math.atan(muS) * 180) / Math.PI);
+ }
+ }
+ this.dataDoc.selectedSolutions = new List<number>(solutions);
+ return solutions;
+ };
+
+ // In review mode, check if input answers match correct answers and optionally generate alert
+ checkAnswers = (showAlert: boolean = true) => {
+ let error: boolean = false;
+ let epsilon: number = 0.01;
+ if (this.selectedQuestion) {
+ for (let i = 0; i < this.selectedQuestion.answerParts.length; i++) {
+ if (this.selectedQuestion.answerParts[i] == 'force of gravity') {
+ if (Math.abs(NumCast(this.dataDoc.review_GravityMagnitude) - this.selectedSolutions[i]) > epsilon) {
+ error = true;
+ }
+ } else if (this.selectedQuestion.answerParts[i] == 'angle of gravity') {
+ if (Math.abs(NumCast(this.dataDoc.review_GravityAngle) - this.selectedSolutions[i]) > epsilon) {
+ error = true;
+ }
+ } else if (this.selectedQuestion.answerParts[i] == 'normal force') {
+ if (Math.abs(NumCast(this.dataDoc.review_NormalMagnitude) - this.selectedSolutions[i]) > epsilon) {
+ error = true;
+ }
+ } else if (this.selectedQuestion.answerParts[i] == 'angle of normal force') {
+ if (Math.abs(NumCast(this.dataDoc.review_NormalAngle) - this.selectedSolutions[i]) > epsilon) {
+ error = true;
+ }
+ } else if (this.selectedQuestion.answerParts[i] == 'force of static friction') {
+ if (Math.abs(NumCast(this.dataDoc.review_StaticMagnitude) - this.selectedSolutions[i]) > epsilon) {
+ error = true;
+ }
+ } else if (this.selectedQuestion.answerParts[i] == 'angle of static friction') {
+ if (Math.abs(NumCast(this.dataDoc.review_StaticAngle) - this.selectedSolutions[i]) > epsilon) {
+ error = true;
+ }
+ } else if (this.selectedQuestion.answerParts[i] == 'coefficient of static friction') {
+ if (Math.abs(NumCast(this.dataDoc.coefficientOfStaticFriction) - this.selectedSolutions[i]) > epsilon) {
+ error = true;
+ }
+ } else if (this.selectedQuestion.answerParts[i] == 'wedge angle') {
+ if (Math.abs(this.wedgeAngle - this.selectedSolutions[i]) > epsilon) {
+ error = true;
+ }
+ }
+ }
+ }
+ if (showAlert) {
+ this.dataDoc.simulation_paused = false;
+ setTimeout(() => (this.dataDoc.simulation_paused = true), 3000);
+ }
+ if (this.selectedQuestion.goal == 'noMovement') {
+ this.dataDoc.noMovement = !error;
+ }
+ };
+
+ // Reset all review values to default
+ resetReviewValuesToDefault = () => {
+ this.dataDoc.review_GravityMagnitude = 0;
+ this.dataDoc.review_GravityAngle = 0;
+ this.dataDoc.review_NormalMagnitude = 0;
+ this.dataDoc.review_NormalAngle = 0;
+ this.dataDoc.review_StaticMagnitude = 0;
+ this.dataDoc.review_StaticAngle = 0;
+ this.dataDoc.coefficientOfKineticFriction = 0;
+ this.dataDoc.simulation_paused = true;
+ };
+
+ // In review mode, reset problem variables and generate a new question
+ generateNewQuestion = () => {
+ this.resetReviewValuesToDefault();
+
+ const vars: number[] = [];
+ let question: QuestionTemplate = questions.inclinePlane[0];
+
+ if (this.simulationType === 'Inclined Plane') {
+ this.dataDoc.questionNumber = (NumCast(this.dataDoc.questionNumber) + 1) % questions.inclinePlane.length;
+ question = questions.inclinePlane[NumCast(this.dataDoc.questionNumber)];
+
+ let coefficient = 0;
+ let wedge_angle = 0;
+
+ for (let i = 0; i < question.variablesForQuestionSetup.length; i++) {
+ if (question.variablesForQuestionSetup[i] == 'theta - max 45') {
+ let randValue = Math.floor(Math.random() * 44 + 1);
+ vars.push(randValue);
+ wedge_angle = randValue;
+ } else if (question.variablesForQuestionSetup[i] == 'coefficient of static friction') {
+ let randValue = Math.round(Math.random() * 1000) / 1000;
+ vars.push(randValue);
+ coefficient = randValue;
+ }
+ }
+ this.dataDoc.wedge_angle = wedge_angle;
+ this.changeWedgeBasedOnNewAngle(wedge_angle);
+ this.dataDoc.coefficientOfStaticFriction = coefficient;
+ this.dataDoc.review_Coefficient = coefficient;
+ }
+ let q = '';
+ for (let i = 0; i < question.questionSetup.length; i++) {
+ q += question.questionSetup[i];
+ if (i != question.questionSetup.length - 1) {
+ q += vars[i];
+ if (question.variablesForQuestionSetup[i].includes('theta')) {
+ q += ' degree (≈' + Math.round((1000 * (vars[i] * Math.PI)) / 180) / 1000 + ' rad)';
+ }
+ }
+ }
+ this.dataDoc.questionVariables = new List<number>(vars);
+ this.dataDoc.selectedQuestion = JSON.stringify(question);
+ this.dataDoc.questionPartOne = q;
+ this.dataDoc.questionPartTwo = question.question;
+ this.dataDoc.answers = new List<number>(this.getAnswersToQuestion(question, vars));
+ //this.dataDoc.simulation_reset = (!this.dataDoc.simulation_reset);
+ };
+
+ // Default setup for uniform circular motion simulation
+ @action
+ setupCircular = (value: number) => {
+ this.dataDoc.simulation_showComponentForces = false;
+ this.dataDoc.mass1_velocityYstart = 0;
+ this.dataDoc.mass1_velocityXstart = value;
+ let xPos = (this.xMax + this.xMin) / 2 - this.mass1Radius;
+ let yPos = (this.yMax + this.yMin) / 2 + this.circularMotionRadius - this.mass1Radius;
+ this.dataDoc.mass1_positionYstart = yPos;
+ this.dataDoc.mass1_positionXstart = xPos;
+ const tensionForce: IForce = {
+ description: 'Centripetal Force',
+ magnitude: (this.dataDoc.mass1_velocityXstart ** 2 * this.mass1) / this.circularMotionRadius,
+ directionInDegrees: 90,
+ };
+ this.dataDoc.mass1_forcesUpdated = JSON.stringify([tensionForce]);
+ this.dataDoc.mass1_forcesStart = JSON.stringify([tensionForce]);
+ this._simReset++;
+ };
+
+ setupInclinedPlane = () => {
+ this.changeWedgeBasedOnNewAngle(this.wedgeAngle);
+ this.dataDoc.mass1_forcesStart = JSON.stringify([this.gravityForce(this.mass1)]);
+ this.updateForcesWithFriction(NumCast(this.dataDoc.coefficientOfStaticFriction));
+ };
+
+ // Default setup for pendulum simulation
+ setupPendulum = () => {
+ const length = (300 * this.props.PanelWidth()) / 1000;
+ const angle = 30;
+ const x = length * Math.cos(((90 - angle) * Math.PI) / 180);
+ const y = length * Math.sin(((90 - angle) * Math.PI) / 180);
+ const xPos = this.xMax / 2 - x - this.mass1Radius;
+ const yPos = y - this.mass1Radius - 5;
+ this.dataDoc.mass1_positionXstart = xPos;
+ this.dataDoc.mass1_positionYstart = yPos;
+ const forceOfTension: IForce = {
+ description: 'Tension',
+ magnitude: this.mass1 * Math.abs(this.gravity) * Math.sin((60 * Math.PI) / 180),
+ directionInDegrees: 90 - angle,
+ };
+ const gravityParallel: IForce = {
+ description: 'Gravity Parallel Component',
+ magnitude: this.mass1 * Math.abs(this.gravity) * Math.sin(((90 - angle) * Math.PI) / 180),
+ directionInDegrees: -angle - 90,
+ };
+ const gravityPerpendicular: IForce = {
+ description: 'Gravity Perpendicular Component',
+ magnitude: this.mass1 * Math.abs(this.gravity) * Math.cos(((90 - angle) * Math.PI) / 180),
+ directionInDegrees: -angle,
+ };
+
+ this.dataDoc.mass1_componentForces = JSON.stringify([forceOfTension, gravityParallel, gravityPerpendicular]);
+ this.dataDoc.mass1_forcesUpdated = JSON.stringify([this.gravityForce(this.mass1)]);
+ this.dataDoc.mass1_forcesStart = JSON.stringify([this.gravityForce(this.mass1), forceOfTension]);
+ this.dataDoc.pendulum_angle = this.dataDoc.pendulum_angleStart = 30;
+ this.dataDoc.pendulum_length = this.dataDoc.pendulum_lengthStart = 300;
+ };
+
+ // Default setup for spring simulation
+ @action
+ setupSpring = () => {
+ this.dataDoc.simulation_showComponentForces = false;
+ this.dataDoc.mass1_forcesUpdated = JSON.stringify([this.gravityForce(this.mass1)]);
+ this.dataDoc.mass1_forcesStart = JSON.stringify([this.gravityForce(this.mass1)]);
+ this.dataDoc.mass1_positionXstart = this.xMax / 2 - this.mass1Radius;
+ this.dataDoc.mass1_positionYstart = 200;
+ this.dataDoc.spring_constant = 0.5;
+ this.dataDoc.spring_lengthRest = 200;
+ this.dataDoc.spring_lengthStart = 200;
+ this._simReset++;
+ };
+
+ // Default setup for suspension simulation
+ @action
+ setupSuspension = () => {
+ let xPos = (this.xMax + this.xMin) / 2 - this.mass1Radius;
+ let yPos = this.yMin + 200;
+ this.dataDoc.mass1_positionYstart = yPos;
+ this.dataDoc.mass1_positionXstart = xPos;
+ this.dataDoc.mass1_positionY = this.getDisplayYPos(yPos);
+ this.dataDoc.mass1_positionX = xPos;
+ let tensionMag = (this.mass1 * Math.abs(this.gravity)) / (2 * Math.sin(Math.PI / 4));
+ const tensionForce1: IForce = {
+ description: 'Tension',
+ magnitude: tensionMag,
+ directionInDegrees: 45,
+ };
+ const tensionForce2: IForce = {
+ description: 'Tension',
+ magnitude: tensionMag,
+ directionInDegrees: 135,
+ };
+ const gravity = this.gravityForce(this.mass1);
+ this.dataDoc.mass1_forcesUpdated = JSON.stringify([tensionForce1, tensionForce2, gravity]);
+ this.dataDoc.mass1_forcesStart = JSON.stringify([tensionForce1, tensionForce2, gravity]);
+ this._simReset++;
+ };
+
+ // Default setup for pulley simulation
+ @action
+ setupPulley = () => {
+ this.dataDoc.simulation_showComponentForces = false;
+ this.dataDoc.mass1_positionYstart = (this.yMax + this.yMin) / 2;
+ this.dataDoc.mass1_positionXstart = (this.xMin + this.xMax) / 2 - 2 * this.mass1Radius - 5;
+ this.dataDoc.mass1_positionY = this.getDisplayYPos((this.yMax + this.yMin) / 2);
+ this.dataDoc.mass1_positionX = (this.xMin + this.xMax) / 2 - 2 * this.mass1Radius - 5;
+ const a = (-1 * ((this.mass1 - this.mass2) * Math.abs(this.gravity))) / (this.mass1 + this.mass2);
+ const gravityForce1 = this.gravityForce(this.mass1);
+ const tensionForce1: IForce = {
+ description: 'Tension',
+ magnitude: this.mass1 * a + this.mass1 * Math.abs(this.gravity),
+ directionInDegrees: 90,
+ };
+ this.dataDoc.mass1_forcesUpdated = JSON.stringify([gravityForce1, tensionForce1]);
+ this.dataDoc.mass1_forcesStart = JSON.stringify([gravityForce1, tensionForce1]);
+
+ const gravityForce2 = this.gravityForce(this.mass2);
+ const tensionForce2: IForce = {
+ description: 'Tension',
+ magnitude: -this.mass2 * a + this.mass2 * Math.abs(this.gravity),
+ directionInDegrees: 90,
+ };
+ this.dataDoc.mass2_positionYstart = (this.yMax + this.yMin) / 2;
+ this.dataDoc.mass2_positionXstart = (this.xMin + this.xMax) / 2 + 5;
+ this.dataDoc.mass2_positionY = this.getDisplayYPos((this.yMax + this.yMin) / 2);
+ this.dataDoc.mass2_positionX = (this.xMin + this.xMax) / 2 + 5;
+ this.dataDoc.mass2_forcesUpdated = JSON.stringify([gravityForce2, tensionForce2]);
+ this.dataDoc.mass2_forcesStart = JSON.stringify([gravityForce2, tensionForce2]);
+ this._simReset++;
+ };
+
+ public static parseJSON(json: string) {
+ return !json ? [] : (JSON.parse(json) as IForce[]);
+ }
+
+ // Handle force change in review mode
+ updateReviewModeValues = () => {
+ const forceOfGravityReview: IForce = {
+ description: 'Gravity',
+ magnitude: NumCast(this.dataDoc.review_GravityMagnitude),
+ directionInDegrees: NumCast(this.dataDoc.review_GravityAngle),
+ };
+ const normalForceReview: IForce = {
+ description: 'Normal Force',
+ magnitude: NumCast(this.dataDoc.review_NormalMagnitude),
+ directionInDegrees: NumCast(this.dataDoc.review_NormalAngle),
+ };
+ const staticFrictionForceReview: IForce = {
+ description: 'Static Friction Force',
+ magnitude: NumCast(this.dataDoc.review_StaticMagnitude),
+ directionInDegrees: NumCast(this.dataDoc.review_StaticAngle),
+ };
+ this.dataDoc.mass1_forcesStart = JSON.stringify([forceOfGravityReview, normalForceReview, staticFrictionForceReview]);
+ this.dataDoc.mass1_forcesUpdated = JSON.stringify([forceOfGravityReview, normalForceReview, staticFrictionForceReview]);
+ };
+
+ pause = () => (this.dataDoc.simulation_paused = true);
+ componentForces1 = () => PhysicsSimulationBox.parseJSON(StrCast(this.dataDoc.mass1_componentForces));
+ setComponentForces1 = (forces: IForce[]) => (this.dataDoc.mass1_componentForces = JSON.stringify(forces));
+ componentForces2 = () => PhysicsSimulationBox.parseJSON(StrCast(this.dataDoc.mass2_componentForces));
+ setComponentForces2 = (forces: IForce[]) => (this.dataDoc.mass2_componentForces = JSON.stringify(forces));
+ startForces1 = () => PhysicsSimulationBox.parseJSON(StrCast(this.dataDoc.mass1_forcesStart));
+ startForces2 = () => PhysicsSimulationBox.parseJSON(StrCast(this.dataDoc.mass2_forcesStart));
+ forcesUpdated1 = () => PhysicsSimulationBox.parseJSON(StrCast(this.dataDoc.mass1_forcesUpdated));
+ setForcesUpdated1 = (forces: IForce[]) => (this.dataDoc.mass1_forcesUpdated = JSON.stringify(forces));
+ forcesUpdated2 = () => PhysicsSimulationBox.parseJSON(StrCast(this.dataDoc.mass2_forcesUpdated));
+ setForcesUpdated2 = (forces: IForce[]) => (this.dataDoc.mass2_forcesUpdated = JSON.stringify(forces));
+ setPosition1 = (xPos: number | undefined, yPos: number | undefined) => {
+ yPos !== undefined && (this.dataDoc.mass1_positionY = Math.round(yPos * 100) / 100);
+ xPos !== undefined && (this.dataDoc.mass1_positionX = Math.round(xPos * 100) / 100);
+ };
+ setPosition2 = (xPos: number | undefined, yPos: number | undefined) => {
+ yPos !== undefined && (this.dataDoc.mass2_positionY = Math.round(yPos * 100) / 100);
+ xPos !== undefined && (this.dataDoc.mass2_positionX = Math.round(xPos * 100) / 100);
+ };
+ setVelocity1 = (xVel: number | undefined, yVel: number | undefined) => {
+ yVel !== undefined && (this.dataDoc.mass1_velocityY = (-1 * Math.round(yVel * 100)) / 100);
+ xVel !== undefined && (this.dataDoc.mass1_velocityX = Math.round(xVel * 100) / 100);
+ };
+ setVelocity2 = (xVel: number | undefined, yVel: number | undefined) => {
+ yVel !== undefined && (this.dataDoc.mass2_velocityY = (-1 * Math.round(yVel * 100)) / 100);
+ xVel !== undefined && (this.dataDoc.mass2_velocityX = Math.round(xVel * 100) / 100);
+ };
+ setAcceleration1 = (xAccel: number, yAccel: number) => {
+ this.dataDoc.mass1_accelerationY = yAccel;
+ this.dataDoc.mass1_accelerationX = xAccel;
+ };
+ setAcceleration2 = (xAccel: number, yAccel: number) => {
+ this.dataDoc.mass2_accelerationY = yAccel;
+ this.dataDoc.mass2_accelerationX = xAccel;
+ };
+ setPendulumAngle = (angle: number | undefined, length: number | undefined) => {
+ angle !== undefined && (this.dataDoc.pendulum_angle = angle);
+ length !== undefined && (this.dataDoc.pendulum_length = length);
+ };
+ setSpringLength = (length: number) => {
+ this.dataDoc.spring_lengthStart = length;
+ };
+ resetRequest = () => this._simReset;
+ render() {
+ const commonWeightProps = {
+ pause: this.pause,
+ paused: BoolCast(this.dataDoc.simulation_paused),
+ panelWidth: this.props.PanelWidth,
+ panelHeight: this.props.PanelHeight,
+ resetRequest: this.resetRequest,
+ xMax: this.xMax,
+ xMin: this.xMin,
+ yMax: this.yMax,
+ yMin: this.yMin,
+ wallPositions: this.wallPositions,
+ gravity: Math.abs(this.gravity),
+ timestepSize: 0.05,
+ showComponentForces: BoolCast(this.dataDoc.simulation_showComponentForces),
+ coefficientOfKineticFriction: NumCast(this.dataDoc.coefficientOfKineticFriction),
+ elasticCollisions: BoolCast(this.dataDoc.elasticCollisions),
+ simulationMode: this.simulationMode,
+ noMovement: BoolCast(this.dataDoc.noMovement),
+ circularMotionRadius: this.circularMotionRadius,
+ wedgeHeight: this.wedgeHeight,
+ wedgeWidth: this.wedgeWidth,
+ springConstant: this.springConstant,
+ springStartLength: this.springLengthStart,
+ springRestLength: this.springLengthRest,
+ setSpringLength: this.setSpringLength,
+ setPendulumAngle: this.setPendulumAngle,
+ pendulumAngle: this.pendulumAngle,
+ pendulumLength: this.pendulumLength,
+ startPendulumAngle: this.pendulumAngleStart,
+ startPendulumLength: this.pendulumLengthStart,
+ radius: this.mass1Radius,
+ simulationSpeed: NumCast(this.dataDoc.simulation_speed, 2),
+ showAcceleration: BoolCast(this.dataDoc.simulation_showAcceleration),
+ showForceMagnitudes: BoolCast(this.dataDoc.simulation_showForceMagnitudes),
+ showForces: BoolCast(this.dataDoc.simulation_showForces),
+ showVelocity: BoolCast(this.dataDoc.simulation_showVelocity),
+ simulationType: this.simulationType,
+ };
+ return (
+ <div className="physicsSimApp">
+ <div className="mechanicsSimulationContainer">
+ <div className="mechanicsSimulationContentContainer">
+ <div className="mechanicsSimulationButtonsAndElements">
+ <div className="mechanicsSimulationButtons">
+ {!this.dataDoc.simulation_paused && (
+ <div
+ style={{
+ position: 'fixed',
+ left: 0.1 * this.props.PanelWidth() + 'px',
+ top: 0.95 * this.props.PanelHeight() + 'px',
+ width: 0.5 * this.props.PanelWidth() + 'px',
+ }}>
+ <LinearProgress />
+ </div>
+ )}
+ </div>
+ <div
+ className="mechanicsSimulationElements"
+ style={{
+ //
+ width: '60%',
+ height: '100%',
+ position: 'absolute',
+ background: 'yellow',
+ }}>
+ <Weight
+ {...commonWeightProps}
+ color="red"
+ componentForces={this.componentForces1}
+ setComponentForces={this.setComponentForces1}
+ displayXVelocity={NumCast(this.dataDoc.mass1_velocityX)}
+ displayYVelocity={NumCast(this.dataDoc.mass1_velocityY)}
+ mass={this.mass1}
+ startForces={this.startForces1}
+ startPosX={this.mass1PosXStart}
+ startPosY={this.mass1PosYStart}
+ startVelX={this.mass1VelXStart}
+ startVelY={this.mass1VelYStart}
+ updateMassPosX={NumCast(this.dataDoc.mass1_xChange)}
+ updateMassPosY={NumCast(this.dataDoc.mass1_yChange)}
+ forcesUpdated={this.forcesUpdated1}
+ setForcesUpdated={this.setForcesUpdated1}
+ setPosition={this.setPosition1}
+ setVelocity={this.setVelocity1}
+ setAcceleration={this.setAcceleration1}
+ />
+ {this.simulationType == 'Pulley' && (
+ <Weight
+ {...commonWeightProps}
+ color="green"
+ componentForces={this.componentForces2}
+ setComponentForces={this.setComponentForces2}
+ displayXVelocity={NumCast(this.dataDoc.mass2_velocityX)}
+ displayYVelocity={NumCast(this.dataDoc.mass2_velocityY)}
+ mass={this.mass2}
+ startForces={this.startForces2}
+ startPosX={this.mass2PosXStart}
+ startPosY={this.mass2PosYStart}
+ startVelX={this.mass2VelXStart}
+ startVelY={this.mass2VelYStart}
+ updateMassPosX={NumCast(this.dataDoc.mass2_xChange)}
+ updateMassPosY={NumCast(this.dataDoc.mass2_yChange)}
+ forcesUpdated={this.forcesUpdated2}
+ setForcesUpdated={this.setForcesUpdated2}
+ setPosition={this.setPosition2}
+ setVelocity={this.setVelocity2}
+ setAcceleration={this.setAcceleration2}
+ />
+ )}
+ </div>
+ <div style={{ position: 'absolute', transformOrigin: 'top left', top: 0, left: 0, width: '100%', height: '100%' }}>
+ {(this.simulationType == 'One Weight' || this.simulationType == 'Inclined Plane') &&
+ this.wallPositions?.map((element, index) => <Wall key={index} length={element.length} xPos={element.xPos} yPos={element.yPos} angleInDegrees={element.angleInDegrees} />)}
+ </div>
+ </div>
+ </div>
+ <div
+ className="mechanicsSimulationEquationContainer"
+ onWheel={e => this.props.isContentActive() && e.stopPropagation()}
+ style={{ overflow: 'auto', height: `${Math.max(1, 800 / this.props.PanelWidth()) * 100}%`, transform: `scale(${Math.min(1, this.props.PanelWidth() / 850)})` }}>
+ <div className="mechanicsSimulationControls">
+ <Stack direction="row" spacing={1}>
+ {this.dataDoc.simulation_paused && this.simulationMode != 'Tutorial' && (
+ <IconButton onClick={() => (this.dataDoc.simulation_paused = false)}>
+ <PlayArrowIcon />
+ </IconButton>
+ )}
+ {!this.dataDoc.simulation_paused && this.simulationMode != 'Tutorial' && (
+ <IconButton onClick={() => (this.dataDoc.simulation_paused = true)}>
+ <PauseIcon />
+ </IconButton>
+ )}
+ {this.dataDoc.simulation_paused && this.simulationMode != 'Tutorial' && (
+ <IconButton onClick={action(() => this._simReset++)}>
+ <ReplayIcon />
+ </IconButton>
+ )}
+ </Stack>
+ <div className="dropdownMenu">
+ <select
+ value={StrCast(this.simulationType)}
+ onChange={event => {
+ this.dataDoc.simulation_type = event.target.value;
+ this.setupSimulation();
+ }}
+ style={{ height: '2em', width: '100%', fontSize: '16px' }}>
+ <option value="One Weight">Projectile</option>
+ <option value="Inclined Plane">Inclined Plane</option>
+ <option value="Pendulum">Pendulum</option>
+ <option value="Spring">Spring</option>
+ <option value="Circular Motion">Circular Motion</option>
+ <option value="Pulley">Pulley</option>
+ <option value="Suspension">Suspension</option>
+ </select>
+ </div>
+ <div className="dropdownMenu">
+ <select
+ value={this.simulationMode}
+ onChange={event => {
+ this.dataDoc.simulation_mode = event.target.value;
+ this.setupSimulation();
+ }}
+ style={{ height: '2em', width: '100%', fontSize: '16px' }}>
+ <option value="Tutorial">Tutorial Mode</option>
+ <option value="Freeform">Freeform Mode</option>
+ <option value="Review">Review Mode</option>
+ </select>
+ </div>
+ </div>
+ {this.simulationMode == 'Review' && this.simulationType != 'Inclined Plane' && (
+ <div className="wordProblemBox">
+ <p>
+ <>{this.simulationType} review problems in progress!</>
+ </p>
+ <hr />
+ </div>
+ )}
+ {this.simulationMode == 'Review' && this.simulationType == 'Inclined Plane' && (
+ <div>
+ {!this.dataDoc.hintDialogueOpen && (
+ <IconButton
+ onClick={() => (this.dataDoc.hintDialogueOpen = true)}
+ sx={{
+ position: 'fixed',
+ left: this.xMax - 50 + 'px',
+ top: this.yMin + 14 + 'px',
+ }}>
+ <QuestionMarkIcon />
+ </IconButton>
+ )}
+ <Dialog maxWidth={'sm'} fullWidth={true} open={BoolCast(this.dataDoc.hintDialogueOpen)} onClose={() => (this.dataDoc.hintDialogueOpen = false)}>
+ <DialogTitle>Hints</DialogTitle>
+ <DialogContent>
+ {this.selectedQuestion.hints?.map((hint: any, index: number) => (
+ <div key={index}>
+ <DialogContentText>
+ <details>
+ <summary>
+ <b>
+ Hint {index + 1}: {hint.description}
+ </b>
+ </summary>
+ {hint.content}
+ </details>
+ </DialogContentText>
+ </div>
+ ))}
+ </DialogContent>
+ <DialogActions>
+ <Button onClick={() => (this.dataDoc.hintDialogueOpen = false)}>Close</Button>
+ </DialogActions>
+ </Dialog>
+ <div className="wordProblemBox">
+ <div className="question">
+ <p>{this.questionPartOne}</p>
+ <p>{this.questionPartTwo}</p>
+ </div>
+ <div className="answers">
+ {this.selectedQuestion.answerParts.includes('force of gravity') && (
+ <InputField
+ label={<p>Gravity magnitude</p>}
+ lowerBound={0}
+ dataDoc={this.dataDoc}
+ prop="review_GravityMagnitude"
+ step={0.1}
+ unit={'N'}
+ upperBound={50}
+ value={NumCast(this.dataDoc.review_GravityMagnitude)}
+ showIcon={BoolCast(this.dataDoc.simulation_showIcon)}
+ correctValue={NumListCast(this.dataDoc.answers)[this.selectedQuestion.answerParts.indexOf('force of gravity')]}
+ labelWidth={'7em'}
+ />
+ )}
+ {this.selectedQuestion.answerParts.includes('angle of gravity') && (
+ <InputField
+ label={<p>Gravity angle</p>}
+ lowerBound={0}
+ dataDoc={this.dataDoc}
+ prop="review_GravityAngle"
+ step={1}
+ unit={'°'}
+ upperBound={360}
+ value={NumCast(this.dataDoc.review_GravityAngle)}
+ radianEquivalent={true}
+ showIcon={BoolCast(this.dataDoc.simulation_showIcon)}
+ correctValue={NumListCast(this.dataDoc.answers)[this.selectedQuestion.answerParts.indexOf('angle of gravity')]}
+ labelWidth={'7em'}
+ />
+ )}
+ {this.selectedQuestion.answerParts.includes('normal force') && (
+ <InputField
+ label={<p>Normal force magnitude</p>}
+ lowerBound={0}
+ dataDoc={this.dataDoc}
+ prop="review_NormalMagnitude"
+ step={0.1}
+ unit={'N'}
+ upperBound={50}
+ value={NumCast(this.dataDoc.review_NormalMagnitude)}
+ showIcon={BoolCast(this.dataDoc.simulation_showIcon)}
+ correctValue={NumListCast(this.dataDoc.answers)[this.selectedQuestion.answerParts.indexOf('normal force')]}
+ labelWidth={'7em'}
+ />
+ )}
+ {this.selectedQuestion.answerParts.includes('angle of normal force') && (
+ <InputField
+ label={<p>Normal force angle</p>}
+ lowerBound={0}
+ dataDoc={this.dataDoc}
+ prop="review_NormalAngle"
+ step={1}
+ unit={'°'}
+ upperBound={360}
+ value={NumCast(this.dataDoc.review_NormalAngle)}
+ radianEquivalent={true}
+ showIcon={BoolCast(this.dataDoc.simulation_showIcon)}
+ correctValue={NumListCast(this.dataDoc.answers)[this.selectedQuestion.answerParts.indexOf('angle of normal force')]}
+ labelWidth={'7em'}
+ />
+ )}
+ {this.selectedQuestion.answerParts.includes('force of static friction') && (
+ <InputField
+ label={<p>Static friction magnitude</p>}
+ lowerBound={0}
+ dataDoc={this.dataDoc}
+ prop="review_StaticMagnitude"
+ step={0.1}
+ unit={'N'}
+ upperBound={50}
+ value={NumCast(this.dataDoc.review_StaticMagnitude)}
+ showIcon={BoolCast(this.dataDoc.simulation_showIcon)}
+ correctValue={NumListCast(this.dataDoc.answers)[this.selectedQuestion.answerParts.indexOf('force of static friction')]}
+ labelWidth={'7em'}
+ />
+ )}
+ {this.selectedQuestion.answerParts.includes('angle of static friction') && (
+ <InputField
+ label={<p>Static friction angle</p>}
+ lowerBound={0}
+ dataDoc={this.dataDoc}
+ prop="review_StaticAngle"
+ step={1}
+ unit={'°'}
+ upperBound={360}
+ value={NumCast(this.dataDoc.review_StaticAngle)}
+ radianEquivalent={true}
+ showIcon={BoolCast(this.dataDoc.simulation_showIcon)}
+ correctValue={NumListCast(this.dataDoc.answers)[this.selectedQuestion.answerParts.indexOf('angle of static friction')]}
+ labelWidth={'7em'}
+ />
+ )}
+ {this.selectedQuestion.answerParts.includes('coefficient of static friction') && (
+ <InputField
+ label={
+ <Box>
+ &mu;<sub>s</sub>
+ </Box>
+ }
+ lowerBound={0}
+ dataDoc={this.dataDoc}
+ prop="coefficientOfStaticFriction"
+ step={0.1}
+ unit={''}
+ upperBound={1}
+ value={NumCast(this.dataDoc.coefficientOfStaticFriction)}
+ effect={this.updateReviewForcesBasedOnCoefficient}
+ showIcon={BoolCast(this.dataDoc.simulation_showIcon)}
+ correctValue={NumListCast(this.dataDoc.answers)[this.selectedQuestion.answerParts.indexOf('coefficient of static friction')]}
+ />
+ )}
+ {this.selectedQuestion.answerParts.includes('wedge angle') && (
+ <InputField
+ label={<Box>&theta;</Box>}
+ lowerBound={0}
+ dataDoc={this.dataDoc}
+ prop="wedge_angle"
+ step={1}
+ unit={'°'}
+ upperBound={49}
+ value={this.wedgeAngle}
+ effect={(val: number) => {
+ this.changeWedgeBasedOnNewAngle(val);
+ this.updateReviewForcesBasedOnAngle(val);
+ }}
+ radianEquivalent={true}
+ showIcon={BoolCast(this.dataDoc.simulation_showIcon)}
+ correctValue={NumListCast(this.dataDoc.answers)[this.selectedQuestion.answerParts.indexOf('wedge angle')]}
+ />
+ )}
+ </div>
+ </div>
+ </div>
+ )}
+ {this.simulationMode == 'Tutorial' && (
+ <div className="wordProblemBox">
+ <div className="question">
+ <h2>Problem</h2>
+ <p>{this.tutorial.question}</p>
+ </div>
+ <div
+ style={{
+ display: 'flex',
+ justifyContent: 'spaceBetween',
+ width: '100%',
+ }}>
+ <IconButton
+ onClick={() => {
+ let step = NumCast(this.dataDoc.tutorial_stepNumber) - 1;
+ step = Math.max(step, 0);
+ step = Math.min(step, this.tutorial.steps.length - 1);
+ this.dataDoc.tutorial_stepNumber = step;
+ this.dataDoc.mass1_forcesStart = JSON.stringify(this.tutorial.steps[step].forces);
+ this.dataDoc.mass1_forcesUpdated = JSON.stringify(this.tutorial.steps[step].forces);
+ this.dataDoc.simulation_showForceMagnitudes = this.tutorial.steps[step].showMagnitude;
+ }}
+ disabled={this.dataDoc.tutorial_stepNumber == 0}>
+ <ArrowLeftIcon />
+ </IconButton>
+ <div>
+ <h3>
+ Step {NumCast(this.dataDoc.tutorial_stepNumber) + 1}: {this.tutorial.steps[NumCast(this.dataDoc.tutorial_stepNumber)].description}
+ </h3>
+ <p>{this.tutorial.steps[NumCast(this.dataDoc.tutorial_stepNumber)].content}</p>
+ </div>
+ <IconButton
+ onClick={() => {
+ let step = NumCast(this.dataDoc.tutorial_stepNumber) + 1;
+ step = Math.max(step, 0);
+ step = Math.min(step, this.tutorial.steps.length - 1);
+ this.dataDoc.tutorial_stepNumber = step;
+ this.dataDoc.mass1_forcesStart = JSON.stringify(this.tutorial.steps[step].forces);
+ this.dataDoc.mass1_forcesUpdated = JSON.stringify(this.tutorial.steps[step].forces);
+ this.dataDoc.simulation_showForceMagnitudes = this.tutorial.steps[step].showMagnitude;
+ }}
+ disabled={this.dataDoc.tutorial_stepNumber === this.tutorial.steps.length - 1}>
+ <ArrowRightIcon />
+ </IconButton>
+ </div>
+ <div>
+ {(this.simulationType == 'One Weight' || this.simulationType == 'Inclined Plane' || this.simulationType == 'Pendulum') && <p>Resources</p>}
+ {this.simulationType == 'One Weight' && (
+ <ul>
+ <li>
+ <a
+ href="https://www.khanacademy.org/science/physics/one-dimensional-motion"
+ target="_blank"
+ rel="noreferrer"
+ style={{
+ color: 'blue',
+ textDecoration: 'underline',
+ }}>
+ Khan Academy - One Dimensional Motion
+ </a>
+ </li>
+ <li>
+ <a
+ href="https://www.khanacademy.org/science/physics/two-dimensional-motion"
+ target="_blank"
+ rel="noreferrer"
+ style={{
+ color: 'blue',
+ textDecoration: 'underline',
+ }}>
+ Khan Academy - Two Dimensional Motion
+ </a>
+ </li>
+ </ul>
+ )}
+ {this.simulationType == 'Inclined Plane' && (
+ <ul>
+ <li>
+ <a
+ href="https://www.khanacademy.org/science/physics/forces-newtons-laws#normal-contact-force"
+ target="_blank"
+ rel="noreferrer"
+ style={{
+ color: 'blue',
+ textDecoration: 'underline',
+ }}>
+ Khan Academy - Normal Force
+ </a>
+ </li>
+ <li>
+ <a
+ href="https://www.khanacademy.org/science/physics/forces-newtons-laws#inclined-planes-friction"
+ target="_blank"
+ rel="noreferrer"
+ style={{
+ color: 'blue',
+ textDecoration: 'underline',
+ }}>
+ Khan Academy - Inclined Planes
+ </a>
+ </li>
+ </ul>
+ )}
+ {this.simulationType == 'Pendulum' && (
+ <ul>
+ <li>
+ <a
+ href="https://www.khanacademy.org/science/physics/forces-newtons-laws#tension-tutorial"
+ target="_blank"
+ rel="noreferrer"
+ style={{
+ color: 'blue',
+ textDecoration: 'underline',
+ }}>
+ Khan Academy - Tension
+ </a>
+ </li>
+ </ul>
+ )}
+ </div>
+ </div>
+ )}
+ {this.simulationMode == 'Review' && this.simulationType == 'Inclined Plane' && (
+ <div
+ style={{
+ display: 'flex',
+ justifyContent: 'space-between',
+ marginTop: '10px',
+ }}>
+ <p
+ style={{
+ color: 'blue',
+ textDecoration: 'underline',
+ cursor: 'pointer',
+ }}
+ onClick={() => (this.dataDoc.simulation_mode = 'Tutorial')}>
+ {' '}
+ Go to walkthrough{' '}
+ </p>
+ <div style={{ display: 'flex', flexDirection: 'column' }}>
+ <Button
+ onClick={action(() => {
+ this._simReset++;
+ this.checkAnswers();
+ this.dataDoc.simulation_showIcon = true;
+ })}
+ variant="outlined">
+ <p>Submit</p>
+ </Button>
+ <Button
+ onClick={() => {
+ this.generateNewQuestion();
+ this.dataDoc.simulation_showIcon = false;
+ }}
+ variant="outlined">
+ <p>New question</p>
+ </Button>
+ </div>
+ </div>
+ )}
+ {this.simulationMode == 'Freeform' && (
+ <div className="vars">
+ <FormControl component="fieldset">
+ <FormGroup>
+ {this.simulationType == 'One Weight' && (
+ <FormControlLabel
+ control={<Checkbox checked={BoolCast(this.dataDoc.elasticCollisions)} onChange={() => (this.dataDoc.elasticCollisions = !this.dataDoc.elasticCollisions)} />}
+ label="Make collisions elastic"
+ labelPlacement="start"
+ />
+ )}
+ <FormControlLabel
+ control={<Checkbox checked={BoolCast(this.dataDoc.simulation_showForces)} onChange={() => (this.dataDoc.simulation_showForces = !this.dataDoc.simulation_showForces)} />}
+ label="Show force vectors"
+ labelPlacement="start"
+ />
+ {(this.simulationType == 'Inclined Plane' || this.simulationType == 'Pendulum') && (
+ <FormControlLabel
+ control={<Checkbox checked={BoolCast(this.dataDoc.simulation_showComponentForces)} onChange={() => (this.dataDoc.simulation_showComponentForces = !this.dataDoc.simulation_showComponentForces)} />}
+ label="Show component force vectors"
+ labelPlacement="start"
+ />
+ )}
+ <FormControlLabel
+ control={<Checkbox checked={BoolCast(this.dataDoc.simulation_showAcceleration)} onChange={() => (this.dataDoc.simulation_showAcceleration = !this.dataDoc.simulation_showAcceleration)} />}
+ label="Show acceleration vector"
+ labelPlacement="start"
+ />
+ <FormControlLabel
+ control={<Checkbox checked={BoolCast(this.dataDoc.simulation_showVelocity)} onChange={() => (this.dataDoc.simulation_showVelocity = !this.dataDoc.simulation_showVelocity)} />}
+ label="Show velocity vector"
+ labelPlacement="start"
+ />
+ <InputField label={<Box>Speed</Box>} lowerBound={1} dataDoc={this.dataDoc} prop="simulation_speed" step={1} unit={'x'} upperBound={10} value={NumCast(this.dataDoc.simulation_speed, 2)} labelWidth={'5em'} />
+ {this.dataDoc.simulation_paused && this.simulationType != 'Circular Motion' && (
+ <InputField
+ label={<Box>Gravity</Box>}
+ lowerBound={-30}
+ dataDoc={this.dataDoc}
+ prop="gravity"
+ step={0.01}
+ unit={'m/s2'}
+ upperBound={0}
+ value={NumCast(this.dataDoc.simulation_gravity, -9.81)}
+ effect={(val: number) => this.setupSimulation()}
+ labelWidth={'5em'}
+ />
+ )}
+ {this.dataDoc.simulation_paused && this.simulationType != 'Pulley' && (
+ <InputField
+ label={<Box>Mass</Box>}
+ lowerBound={1}
+ dataDoc={this.dataDoc}
+ prop="mass1"
+ step={0.1}
+ unit={'kg'}
+ upperBound={5}
+ value={this.mass1 ?? 1}
+ effect={(val: number) => this.setupSimulation()}
+ labelWidth={'5em'}
+ />
+ )}
+ {this.dataDoc.simulation_paused && this.simulationType == 'Pulley' && (
+ <InputField
+ label={<Box>Red mass</Box>}
+ lowerBound={1}
+ dataDoc={this.dataDoc}
+ prop="mass1"
+ step={0.1}
+ unit={'kg'}
+ upperBound={5}
+ value={this.mass1 ?? 1}
+ effect={(val: number) => this.setupSimulation()}
+ labelWidth={'5em'}
+ />
+ )}
+ {this.dataDoc.simulation_paused && this.simulationType == 'Pulley' && (
+ <InputField
+ label={<Box>Blue mass</Box>}
+ lowerBound={1}
+ dataDoc={this.dataDoc}
+ prop="mass2"
+ step={0.1}
+ unit={'kg'}
+ upperBound={5}
+ value={this.mass2 ?? 1}
+ effect={(val: number) => this.setupSimulation()}
+ labelWidth={'5em'}
+ />
+ )}
+ {this.dataDoc.simulation_paused && this.simulationType == 'Circular Motion' && (
+ <InputField
+ label={<Box>Rod length</Box>}
+ lowerBound={100}
+ dataDoc={this.dataDoc}
+ prop="circularMotionRadius"
+ step={5}
+ unit={'m'}
+ upperBound={250}
+ value={this.circularMotionRadius}
+ effect={(val: number) => this.setupSimulation()}
+ labelWidth={'5em'}
+ />
+ )}
+ </FormGroup>
+ </FormControl>
+ {this.simulationType == 'Spring' && this.dataDoc.simulation_paused && (
+ <div>
+ <InputField
+ label={<Typography color="inherit">Spring stiffness</Typography>}
+ lowerBound={0.1}
+ dataDoc={this.dataDoc}
+ prop="spring_constant"
+ step={1}
+ unit={'N/m'}
+ upperBound={500}
+ value={this.springConstant}
+ effect={action(() => this._simReset++)}
+ radianEquivalent={false}
+ mode={'Freeform'}
+ labelWidth={'7em'}
+ />
+ <InputField
+ label={<Typography color="inherit">Rest length</Typography>}
+ lowerBound={10}
+ dataDoc={this.dataDoc}
+ prop="spring_lengthRest"
+ step={100}
+ unit=""
+ upperBound={500}
+ value={this.springLengthRest}
+ effect={action(() => this._simReset++)}
+ radianEquivalent={false}
+ mode="Freeform"
+ labelWidth={'7em'}
+ />
+ <InputField
+ label={<Typography color="inherit">Starting displacement</Typography>}
+ lowerBound={-(this.springLengthRest - 10)}
+ dataDoc={this.dataDoc}
+ prop=""
+ step={10}
+ unit=""
+ upperBound={this.springLengthRest}
+ value={this.springLengthStart - this.springLengthRest}
+ effect={action((val: number) => {
+ this.dataDoc.mass1_positionYstart = this.springLengthRest + val;
+ this.dataDoc.spring_lengthStart = this.springLengthRest + val;
+ this._simReset++;
+ })}
+ radianEquivalent={false}
+ mode="Freeform"
+ labelWidth={'7em'}
+ />
+ </div>
+ )}
+ {this.simulationType == 'Inclined Plane' && this.dataDoc.simulation_paused && (
+ <div>
+ <InputField
+ label={<Box>&theta;</Box>}
+ lowerBound={0}
+ dataDoc={this.dataDoc}
+ prop="wedge_angle"
+ step={1}
+ unit={'°'}
+ upperBound={49}
+ value={this.wedgeAngle}
+ effect={action((val: number) => {
+ this.changeWedgeBasedOnNewAngle(val);
+ this._simReset++;
+ })}
+ radianEquivalent={true}
+ mode={'Freeform'}
+ labelWidth={'2em'}
+ />
+ <InputField
+ label={
+ <Box>
+ &mu;<sub>s</sub>
+ </Box>
+ }
+ lowerBound={0}
+ dataDoc={this.dataDoc}
+ prop="coefficientOfStaticFriction"
+ step={0.1}
+ unit={''}
+ upperBound={1}
+ value={NumCast(this.dataDoc.coefficientOfStaticFriction) ?? 0}
+ effect={action((val: number) => {
+ this.updateForcesWithFriction(val);
+ if (val < NumCast(this.dataDoc.coefficientOfKineticFriction)) {
+ this.dataDoc.soefficientOfKineticFriction = val;
+ }
+ this._simReset++;
+ })}
+ mode={'Freeform'}
+ labelWidth={'2em'}
+ />
+ <InputField
+ label={
+ <Box>
+ &mu;<sub>k</sub>
+ </Box>
+ }
+ lowerBound={0}
+ dataDoc={this.dataDoc}
+ prop="coefficientOfKineticFriction"
+ step={0.1}
+ unit={''}
+ upperBound={NumCast(this.dataDoc.coefficientOfStaticFriction)}
+ value={NumCast(this.dataDoc.coefficientOfKineticFriction) ?? 0}
+ effect={action(() => this._simReset++)}
+ mode={'Freeform'}
+ labelWidth={'2em'}
+ />
+ </div>
+ )}
+ {this.simulationType == 'Inclined Plane' && !this.dataDoc.simulation_paused && (
+ <Typography>
+ <>
+ &theta;: {Math.round(this.wedgeAngle * 100) / 100}° ≈ {Math.round(((this.wedgeAngle * Math.PI) / 180) * 100) / 100} rad
+ <br />
+ &mu; <sub>s</sub>: {this.dataDoc.coefficientOfStaticFriction}
+ <br />
+ &mu; <sub>k</sub>: {this.dataDoc.coefficientOfKineticFriction}
+ </>
+ </Typography>
+ )}
+ {this.simulationType == 'Pendulum' && !this.dataDoc.simulation_paused && (
+ <Typography>
+ &theta;: {Math.round(this.pendulumAngle * 100) / 100}° ≈ {Math.round(((this.pendulumAngle * Math.PI) / 180) * 100) / 100} rad
+ </Typography>
+ )}
+ {this.simulationType == 'Pendulum' && this.dataDoc.simulation_paused && (
+ <div>
+ <InputField
+ label={<Box>Angle</Box>}
+ lowerBound={0}
+ dataDoc={this.dataDoc}
+ prop="pendulum_angle"
+ step={1}
+ unit={'°'}
+ upperBound={59}
+ value={NumCast(this.dataDoc.pendulum_angle, 30)}
+ effect={action(value => {
+ this.dataDoc.pendulum_angleStart = value;
+ this.dataDoc.pendulum_lengthStart = this.dataDoc.pendulum_length;
+ if (this.simulationType == 'Pendulum') {
+ const mag = this.mass1 * Math.abs(this.gravity) * Math.cos((value * Math.PI) / 180);
+
+ const forceOfTension: IForce = {
+ description: 'Tension',
+ magnitude: mag,
+ directionInDegrees: 90 - value,
+ };
+ const gravityParallel: IForce = {
+ description: 'Gravity Parallel Component',
+ magnitude: Math.abs(this.gravity) * Math.cos((value * Math.PI) / 180),
+ directionInDegrees: 270 - value,
+ };
+ const gravityPerpendicular: IForce = {
+ description: 'Gravity Perpendicular Component',
+ magnitude: Math.abs(this.gravity) * Math.sin((value * Math.PI) / 180),
+ directionInDegrees: -value,
+ };
+
+ const length = this.pendulumLength;
+ const x = length * Math.cos(((90 - value) * Math.PI) / 180);
+ const y = length * Math.sin(((90 - value) * Math.PI) / 180);
+ const xPos = this.xMax / 2 - x - NumCast(this.dataDoc.radius);
+ const yPos = y - NumCast(this.dataDoc.radius) - 5;
+ this.dataDoc.mass1_positionXstart = xPos;
+ this.dataDoc.mass1_positionYstart = yPos;
+
+ this.dataDoc.mass1_forcesStart = JSON.stringify([this.gravityForce(this.mass1), forceOfTension]);
+ this.dataDoc.mass1_forcesUpdated = JSON.stringify([this.gravityForce(this.mass1), forceOfTension]);
+ this.dataDoc.mass1_componentForces = JSON.stringify([forceOfTension, gravityParallel, gravityPerpendicular]);
+ this._simReset++;
+ }
+ })}
+ radianEquivalent={true}
+ mode="Freeform"
+ labelWidth="5em"
+ />
+ <InputField
+ label={<Box>Rod length</Box>}
+ lowerBound={0}
+ dataDoc={this.dataDoc}
+ prop="pendulum_length"
+ step={1}
+ unit="m"
+ upperBound={400}
+ value={Math.round(this.pendulumLength)}
+ effect={action(value => {
+ if (this.simulationType == 'Pendulum') {
+ this.dataDoc.pendulum_angleStart = this.pendulumAngle;
+ this.dataDoc.pendulum_lengthStart = value;
+ this._simReset++;
+ }
+ })}
+ radianEquivalent={false}
+ mode="Freeform"
+ labelWidth="5em"
+ />
+ </div>
+ )}
+ </div>
+ )}
+ <div className="mechanicsSimulationEquation">
+ {this.simulationMode == 'Freeform' && (
+ <table>
+ <tbody>
+ <tr>
+ <td>{this.simulationType == 'Pulley' ? 'Red Weight' : ''}</td>
+ <td>X</td>
+ <td>Y</td>
+ </tr>
+ <tr>
+ <td
+ style={{ cursor: 'help' }}
+ // onClick={() => {
+ // window.open(
+ // "https://www.khanacademy.org/science/physics/two-dimensional-motion"
+ // );
+ // }}
+ >
+ <Box>Position</Box>
+ </td>
+ {(!this.dataDoc.simulation_paused || this.simulationType == 'Inclined Plane' || this.simulationType == 'Circular Motion' || this.simulationType == 'Pulley') && (
+ <td style={{ cursor: 'default' }}>
+ <>{this.dataDoc.mass1_positionX} m</>
+ </td>
+ )}{' '}
+ {this.dataDoc.simulation_paused && this.simulationType != 'Inclined Plane' && this.simulationType != 'Circular Motion' && this.simulationType != 'Pulley' && (
+ <td
+ style={{
+ cursor: 'default',
+ }}>
+ <InputField
+ lowerBound={this.simulationType == 'Projectile' ? 1 : (this.xMax + this.xMin) / 4 - this.radius - 15}
+ dataDoc={this.dataDoc}
+ prop="mass1_positionX"
+ step={1}
+ unit={'m'}
+ upperBound={this.simulationType == 'Projectile' ? this.xMax - 110 : (3 * (this.xMax + this.xMin)) / 4 - this.radius / 2 - 15}
+ value={NumCast(this.dataDoc.mass1_positionX)}
+ effect={value => {
+ this.dataDoc.mass1_xChange = value;
+ if (this.simulationType == 'Suspension') {
+ let x1rod = (this.xMax + this.xMin) / 2 - this.radius - this.yMin - 200;
+ let x2rod = (this.xMax + this.xMin) / 2 + this.yMin + 200 + this.radius;
+ let deltaX1 = value + this.radius - x1rod;
+ let deltaX2 = x2rod - (value + this.radius);
+ let deltaY = this.getYPosFromDisplay(NumCast(this.dataDoc.mass1_positionY)) + this.radius;
+ let dir1T = Math.PI - Math.atan(deltaY / deltaX1);
+ let dir2T = Math.atan(deltaY / deltaX2);
+ let tensionMag2 = (this.mass1 * Math.abs(this.gravity)) / ((-Math.cos(dir2T) / Math.cos(dir1T)) * Math.sin(dir1T) + Math.sin(dir2T));
+ let tensionMag1 = (-tensionMag2 * Math.cos(dir2T)) / Math.cos(dir1T);
+ dir1T = (dir1T * 180) / Math.PI;
+ dir2T = (dir2T * 180) / Math.PI;
+ const tensionForce1: IForce = {
+ description: 'Tension',
+ magnitude: tensionMag1,
+ directionInDegrees: dir1T,
+ };
+ const tensionForce2: IForce = {
+ description: 'Tension',
+ magnitude: tensionMag2,
+ directionInDegrees: dir2T,
+ };
+ const gravity = this.gravityForce(this.mass1);
+ this.dataDoc.mass1_forcesUpdated = JSON.stringify([tensionForce1, tensionForce2, gravity]);
+ }
+ }}
+ small={true}
+ mode="Freeform"
+ />
+ </td>
+ )}{' '}
+ {(!this.dataDoc.simulation_paused || this.simulationType == 'Inclined Plane' || this.simulationType == 'Circular Motion' || this.simulationType == 'Pulley') && (
+ <td style={{ cursor: 'default' }}>{`${NumCast(this.dataDoc.mass1_positionY)} m`}</td>
+ )}{' '}
+ {this.dataDoc.simulation_paused && this.simulationType != 'Inclined Plane' && this.simulationType != 'Circular Motion' && this.simulationType != 'Pulley' && (
+ <td
+ style={{
+ cursor: 'default',
+ }}>
+ <InputField
+ lowerBound={1}
+ dataDoc={this.dataDoc}
+ prop="mass1_positionY"
+ step={1}
+ unit="m"
+ upperBound={this.yMax - 110}
+ value={NumCast(this.dataDoc.mass1_positionY)}
+ effect={value => {
+ this.dataDoc.mass1_yChange = value;
+ if (this.simulationType == 'Suspension') {
+ let x1rod = (this.xMax + this.xMin) / 2 - this.radius - this.yMin - 200;
+ let x2rod = (this.xMax + this.xMin) / 2 + this.yMin + 200 + this.radius;
+ let deltaX1 = NumCast(this.dataDoc.mass1_positionX) + this.radius - x1rod;
+ let deltaX2 = x2rod - (NumCast(this.dataDoc.mass1_positionX) + this.radius);
+ let deltaY = this.getYPosFromDisplay(value) + this.radius;
+ let dir1T = Math.PI - Math.atan(deltaY / deltaX1);
+ let dir2T = Math.atan(deltaY / deltaX2);
+ let tensionMag2 = (this.mass1 * Math.abs(this.gravity)) / ((-Math.cos(dir2T) / Math.cos(dir1T)) * Math.sin(dir1T) + Math.sin(dir2T));
+ let tensionMag1 = (-tensionMag2 * Math.cos(dir2T)) / Math.cos(dir1T);
+ dir1T = (dir1T * 180) / Math.PI;
+ dir2T = (dir2T * 180) / Math.PI;
+ const tensionForce1: IForce = {
+ description: 'Tension',
+ magnitude: tensionMag1,
+ directionInDegrees: dir1T,
+ };
+ const tensionForce2: IForce = {
+ description: 'Tension',
+ magnitude: tensionMag2,
+ directionInDegrees: dir2T,
+ };
+ const gravity = this.gravityForce(this.mass1);
+ this.dataDoc.mass1_forcesUpdated = JSON.stringify([tensionForce1, tensionForce2, gravity]);
+ }
+ }}
+ small={true}
+ mode="Freeform"
+ />
+ </td>
+ )}{' '}
+ </tr>
+ <tr>
+ <td
+ style={{ cursor: 'help' }}
+ // onClick={() => {
+ // window.open(
+ // "https://www.khanacademy.org/science/physics/two-dimensional-motion"
+ // );
+ // }}
+ >
+ <Box>Velocity</Box>
+ </td>
+ {(!this.dataDoc.simulation_paused || (this.simulationType != 'One Weight' && this.simulationType != 'Circular Motion')) && (
+ <td style={{ cursor: 'default' }}>{`${NumCast(this.dataDoc.mass1_velocityX)} m/s`}</td>
+ )}{' '}
+ {this.dataDoc.simulation_paused && (this.simulationType == 'One Weight' || this.simulationType == 'Circular Motion') && (
+ <td
+ style={{
+ cursor: 'default',
+ }}>
+ <InputField
+ lowerBound={-50}
+ dataDoc={this.dataDoc}
+ prop="mass1_velocityX"
+ step={1}
+ unit={'m/s'}
+ upperBound={50}
+ value={NumCast(this.dataDoc.mass1_velocityX)}
+ effect={action(value => {
+ this.dataDoc.mass1_velocityXstart = value;
+ this._simReset++;
+ })}
+ small={true}
+ mode="Freeform"
+ />
+ </td>
+ )}{' '}
+ {(!this.dataDoc.simulation_paused || this.simulationType != 'One Weight') && (
+ <td style={{ cursor: 'default' }}>
+ <>{this.dataDoc.mass1_velocityY} m/s</>
+ </td>
+ )}{' '}
+ {this.dataDoc.simulation_paused && this.simulationType == 'One Weight' && (
+ <td
+ style={{
+ cursor: 'default',
+ }}>
+ <InputField
+ lowerBound={-50}
+ dataDoc={this.dataDoc}
+ prop="mass1_velocityY"
+ step={1}
+ unit="m/s"
+ upperBound={50}
+ value={NumCast(this.dataDoc.mass1_velocityY)}
+ effect={value => {
+ this.dataDoc.mass1_velocityYstart = -value;
+ }}
+ small={true}
+ mode="Freeform"
+ />
+ </td>
+ )}{' '}
+ </tr>
+ <tr>
+ <td
+ style={{ cursor: 'help' }}
+ // onClick={() => {
+ // window.open(
+ // "https://www.khanacademy.org/science/physics/two-dimensional-motion"
+ // );
+ // }}
+ >
+ <Box>Acceleration</Box>
+ </td>
+ <td style={{ cursor: 'default' }}>
+ <>
+ {this.dataDoc.mass1_accelerationX} m/s<sup>2</sup>
+ </>
+ </td>
+ <td style={{ cursor: 'default' }}>
+ <>
+ {this.dataDoc.mass1_accelerationY} m/s<sup>2</sup>
+ </>
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <Box>Momentum</Box>
+ </td>
+ <td>{Math.round(NumCast(this.dataDoc.mass1_velocityX) * this.mass1 * 10) / 10} kg*m/s</td>
+ <td>{Math.round(NumCast(this.dataDoc.mass1_velocityY) * this.mass1 * 10) / 10} kg*m/s</td>
+ </tr>
+ </tbody>
+ </table>
+ )}
+ {this.simulationMode == 'Freeform' && this.simulationType == 'Pulley' && (
+ <table>
+ <tbody>
+ <tr>
+ <td>Blue Weight</td>
+ <td>X</td>
+ <td>Y</td>
+ </tr>
+ <tr>
+ <td>
+ <Box>Position</Box>
+ </td>
+ <td style={{ cursor: 'default' }}>{`${this.dataDoc.mass2_positionX} m`}</td>
+ <td style={{ cursor: 'default' }}>{`${this.dataDoc.mass2_positionY} m`}</td>
+ </tr>
+ <tr>
+ <td>
+ <Box>Velocity</Box>
+ </td>
+ <td style={{ cursor: 'default' }}>{`${this.dataDoc.mass2_positionX} m/s`}</td>
+ <td style={{ cursor: 'default' }}>{`${this.dataDoc.mass2_positionY} m/s`}</td>
+ </tr>
+ <tr>
+ <td>
+ <Box>Acceleration</Box>
+ </td>
+ <td style={{ cursor: 'default' }}>
+ <>
+ {this.dataDoc.mass2_accelerationX} m/s<sup>2</sup>
+ </>
+ </td>
+ <td style={{ cursor: 'default' }}>
+ <>
+ {this.dataDoc.mass2_accelerationY} m/s<sup>2</sup>
+ </>
+ </td>
+ </tr>
+ <tr>
+ <td>
+ <Box>Momentum</Box>
+ </td>
+ <td>{Math.round(NumCast(this.dataDoc.mass2_velocityX) * this.mass1 * 10) / 10} kg*m/s</td>
+ <td>{Math.round(NumCast(this.dataDoc.mass2_velocityY) * this.mass1 * 10) / 10} kg*m/s</td>
+ </tr>
+ </tbody>
+ </table>
+ )}
+ </div>
+ {this.simulationType != 'Pendulum' && this.simulationType != 'Spring' && (
+ <div>
+ <p>Kinematic Equations</p>
+ <ul>
+ <li>
+ Position: x<sub>1</sub>=x<sub>0</sub>+v<sub>0</sub>t+
+ <sup>1</sup>&frasl;
+ <sub>2</sub>at
+ <sup>2</sup>
+ </li>
+ <li>
+ Velocity: v<sub>1</sub>=v<sub>0</sub>+at
+ </li>
+ <li>Acceleration: a = F/m</li>
+ </ul>
+ </div>
+ )}
+ {this.simulationType == 'Spring' && (
+ <div>
+ <p>Harmonic Motion Equations: Spring</p>
+ <ul>
+ <li>
+ Spring force: F<sub>s</sub>=kd
+ </li>
+ <li>
+ Spring period: T<sub>s</sub>=2&pi;&#8730;<sup>m</sup>&frasl;
+ <sub>k</sub>
+ </li>
+ <li>Equilibrium displacement for vertical spring: d = mg/k</li>
+ <li>
+ Elastic potential energy: U<sub>s</sub>=<sup>1</sup>&frasl;
+ <sub>2</sub>kd<sup>2</sup>
+ </li>
+ <ul>
+ <li>Maximum when system is at maximum displacement, 0 when system is at 0 displacement</li>
+ </ul>
+ <li>
+ Translational kinetic energy: K=<sup>1</sup>&frasl;
+ <sub>2</sub>mv<sup>2</sup>
+ </li>
+ <ul>
+ <li>Maximum when system is at maximum/minimum velocity (at 0 displacement), 0 when velocity is 0 (at maximum displacement)</li>
+ </ul>
+ </ul>
+ </div>
+ )}
+ {this.simulationType == 'Pendulum' && (
+ <div>
+ <p>Harmonic Motion Equations: Pendulum</p>
+ <ul>
+ <li>
+ Pendulum period: T<sub>p</sub>=2&pi;&#8730;<sup>l</sup>&frasl;
+ <sub>g</sub>
+ </li>
+ </ul>
+ </div>
+ )}
+ </div>
+ </div>
+ <div
+ style={{
+ position: 'fixed',
+ top: this.yMax - 120 + 20 + 'px',
+ left: this.xMin + 90 - 80 + 'px',
+ zIndex: -10000,
+ }}>
+ <svg width={100 + 'px'} height={100 + 'px'}>
+ <defs>
+ <marker id="miniArrow" markerWidth="20" markerHeight="20" refX="0" refY="3" orient="auto" markerUnits="strokeWidth">
+ <path d="M0,0 L0,6 L9,3 z" fill={'#000000'} />
+ </marker>
+ </defs>
+ <line x1={20} y1={70} x2={70} y2={70} stroke={'#000000'} strokeWidth="2" markerEnd="url(#miniArrow)" />
+ <line x1={20} y1={70} x2={20} y2={20} stroke={'#000000'} strokeWidth="2" markerEnd="url(#miniArrow)" />
+ </svg>
+ <p
+ style={{
+ position: 'fixed',
+ top: this.yMax - 120 + 40 + 'px',
+ left: this.xMin + 90 - 80 + 'px',
+ }}>
+ {this.simulationType == 'Circular Motion' ? 'Z' : 'Y'}
+ </p>
+ <p
+ style={{
+ position: 'fixed',
+ top: this.yMax - 120 + 80 + 'px',
+ left: this.xMin + 90 - 40 + 'px',
+ }}>
+ X
+ </p>
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/nodes/PhysicsBox/PhysicsSimulationInputField.tsx b/src/client/views/nodes/PhysicsBox/PhysicsSimulationInputField.tsx
new file mode 100644
index 000000000..d595a499e
--- /dev/null
+++ b/src/client/views/nodes/PhysicsBox/PhysicsSimulationInputField.tsx
@@ -0,0 +1,179 @@
+import { TextField, InputAdornment } from '@mui/material';
+import { Doc } from '../../../../fields/Doc';
+import React = require('react');
+import TaskAltIcon from '@mui/icons-material/TaskAlt';
+import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline';
+import { isNumber } from 'lodash';
+export interface IInputProps {
+ label?: JSX.Element;
+ lowerBound: number;
+ dataDoc: Doc;
+ prop: string;
+ step: number;
+ unit: string;
+ upperBound: number;
+ value: number | string | Array<number | string>;
+ correctValue?: number;
+ showIcon?: boolean;
+ effect?: (val: number) => any;
+ radianEquivalent?: boolean;
+ small?: boolean;
+ mode?: string;
+ update?: boolean;
+ labelWidth?: string;
+}
+
+interface IState {
+ tempValue: string | number | (string | number)[];
+ tempRadianValue: number;
+ width: string;
+ margin: string;
+}
+
+export default class InputField extends React.Component<IInputProps, IState> {
+ constructor(props: any) {
+ super(props);
+ this.state = {
+ tempValue: this.props.mode != 'Freeform' && !this.props.showIcon ? 0 : this.props.value,
+ tempRadianValue: this.props.mode != 'Freeform' && !this.props.showIcon ? 0 : (Number(this.props.value) * Math.PI) / 180,
+ width: this.props.small ? '6em' : '7em',
+ margin: this.props.small ? '0px' : '10px',
+ };
+ }
+
+ epsilon: number = 0.01;
+
+ componentDidMount(): void {
+ this.setState({ tempValue: Number(this.props.value) });
+ }
+
+ componentDidUpdate(prevProps: Readonly<IInputProps>, prevState: Readonly<IState>, snapshot?: any): void {
+ if (prevProps.value != this.props.value && isNumber(this.props.value) && !isNaN(this.props.value)) {
+ if (this.props.mode == 'Freeform') {
+ if (isNumber(this.state.tempValue) && Math.abs(this.state.tempValue - Number(this.props.value)) > 1) {
+ this.setState({ tempValue: Number(this.props.value) });
+ }
+ }
+ }
+ if (prevProps.update != this.props.update) {
+ this.externalUpdate();
+ }
+ }
+
+ externalUpdate = () => {
+ this.setState({ tempValue: Number(this.props.value) });
+ this.setState({ tempRadianValue: (Number(this.props.value) * Math.PI) / 180 });
+ };
+
+ onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+ let value = event.target.value == '' ? 0 : Number(event.target.value);
+ if (value > this.props.upperBound) {
+ value = this.props.upperBound;
+ } else if (value < this.props.lowerBound) {
+ value = this.props.lowerBound;
+ }
+ if (this.props.prop != '') {
+ this.props.dataDoc[this.props.prop] = value;
+ }
+ this.setState({ tempValue: event.target.value == '' ? event.target.value : value });
+ this.setState({ tempRadianValue: (value * Math.PI) / 180 });
+ if (this.props.effect) {
+ this.props.effect(value);
+ }
+ };
+
+ onChangeRadianValue = (event: React.ChangeEvent<HTMLInputElement>) => {
+ let value = event.target.value === '' ? 0 : Number(event.target.value);
+ if (value > 2 * Math.PI) {
+ value = 2 * Math.PI;
+ } else if (value < 0) {
+ value = 0;
+ }
+ if (this.props.prop != '') {
+ this.props.dataDoc[this.props.prop] = (value * 180) / Math.PI;
+ }
+ this.setState({ tempValue: (value * 180) / Math.PI });
+ this.setState({ tempRadianValue: value });
+ if (this.props.effect) {
+ this.props.effect((value * 180) / Math.PI);
+ }
+ };
+
+ render() {
+ return (
+ <div
+ style={{
+ display: 'flex',
+ lineHeight: '1.5',
+ textAlign: 'right',
+ alignItems: 'center',
+ }}>
+ {this.props.label && (
+ <div
+ style={{
+ marginTop: '0.3em',
+ marginBottom: '-0.5em',
+ width: this.props.labelWidth ?? '2em',
+ }}>
+ {this.props.label}
+ </div>
+ )}
+ <TextField
+ type="number"
+ variant="standard"
+ value={this.state.tempValue}
+ onChange={this.onChange}
+ sx={{
+ height: '1em',
+ width: this.state.width,
+ marginLeft: this.state.margin,
+ zIndex: 'modal',
+ }}
+ inputProps={{
+ step: this.props.step,
+ min: this.props.lowerBound,
+ max: this.props.upperBound,
+ type: 'number',
+ }}
+ InputProps={{
+ startAdornment: (
+ <InputAdornment position="start">
+ {Math.abs(Number(this.props.value) - (this.props.correctValue ?? 0)) < this.epsilon && this.props.showIcon && <TaskAltIcon color={'success'} />}
+ {Math.abs(Number(this.props.value) - (this.props.correctValue ?? 0)) >= this.epsilon && this.props.showIcon && <ErrorOutlineIcon color={'error'} />}
+ </InputAdornment>
+ ),
+ endAdornment: <InputAdornment position="end">{this.props.unit}</InputAdornment>,
+ }}
+ />
+ {this.props.radianEquivalent && (
+ <div style={{ marginTop: '0.3em', marginBottom: '-0.5em', width: '1em' }}>
+ <p>≈</p>
+ </div>
+ )}
+ {this.props.radianEquivalent && (
+ <TextField
+ type="number"
+ variant="standard"
+ value={this.state.tempRadianValue}
+ onChange={this.onChangeRadianValue}
+ sx={{
+ height: '1em',
+ width: this.state.width,
+ marginLeft: this.state.margin,
+ zIndex: 'modal',
+ }}
+ inputProps={{
+ step: Math.PI / 8,
+ min: 0,
+ max: 2 * Math.PI,
+ type: 'number',
+ }}
+ InputProps={{
+ endAdornment: <InputAdornment position="end">rad</InputAdornment>,
+ }}
+ />
+ )}
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/nodes/PhysicsBox/PhysicsSimulationQuestions.json b/src/client/views/nodes/PhysicsBox/PhysicsSimulationQuestions.json
new file mode 100644
index 000000000..cc79f7aad
--- /dev/null
+++ b/src/client/views/nodes/PhysicsBox/PhysicsSimulationQuestions.json
@@ -0,0 +1,161 @@
+{
+ "inclinePlane": [
+ {
+ "questionSetup": [
+ "There is a 1kg weight on an inclined plane. The plane is at a ",
+ " angle from the ground. The system is in equilibrium (the net force on the weight is 0)."
+ ],
+ "variablesForQuestionSetup": ["theta - max 45"],
+ "question": "What are the magnitudes and directions of the forces acting on the weight?",
+ "answerParts": [
+ "force of gravity",
+ "angle of gravity",
+ "normal force",
+ "angle of normal force",
+ "force of static friction",
+ "angle of static friction"
+ ],
+ "answerSolutionDescriptions": [
+ "9.81",
+ "270",
+ "solve normal force magnitude from wedge angle",
+ "solve normal force angle from wedge angle",
+ "solve static force magnitude from wedge angle given equilibrium",
+ "solve static force angle from wedge angle given equilibrium"
+ ],
+ "goal": "noMovement",
+ "hints": [
+ {
+ "description": "Direction of Force of Gravity",
+ "content": "The force of gravity acts in the negative y direction: 3Ï€/2 rad."
+ },
+ {
+ "description": "Direction of Normal Force",
+ "content": "The normal force acts in the direction perpendicular to the incline plane: π/2-θ rad, where θ is the angle of the incline plane."
+ },
+ {
+ "description": "Direction of Force of Friction",
+ "content": "The force of friction acts in the direction along the incline plane: π-θ rad, where θ is the angle of the incline plane."
+ },
+ {
+ "description": "Magnitude of Force of Gravity",
+ "content": "The magnitude of the force of gravity is approximately 9.81."
+ },
+ {
+ "description": "Magnitude of Normal Force",
+ "content": "The magnitude of the normal force is equal to m*g*cos(θ), where θ is the angle of the incline plane."
+ },
+ {
+ "description": "Net Force in Equilibrium",
+ "content": "For the system to be in equilibrium, the sum of the x components of all forces must equal 0, and the sum of the y components of all forces must equal 0."
+ },
+ {
+ "description": "X Component of Normal Force",
+ "content": "The x component of the normal force is equal to m*g*cos(θ)*cos(π/2-θ), where θ is the angle of the incline plane."
+ },
+ {
+ "description": "X Component of Force of Friction",
+ "content": "Since the net force in the x direction must be 0, we know the magnitude of the x component of the friction force is m*g*cos(θ)*cos(π/2-θ)."
+ },
+ {
+ "description": "Y Component of Normal Force",
+ "content": "The y component of the normal force is equal to m*g*cos(θ)*sin(π/2-θ), where θ is the angle of the incline plane. The y component of gravity is equal to m*g"
+ },
+ {
+ "description": "Y Component of Force of Friction",
+ "content": "Since the net force in the x direction must be 0, we know the magnitude of the y component of the friction force is m*g-m*g*cos(θ)*sin(π/2-θ)."
+ },
+ {
+ "description": "Magnitude of Force of Friction",
+ "content": "Combining the x and y components of the friction force, we get the magnitude of the friction force is equal to sqrt((m*g*cos(θ)*cos(π/2-θ))^2 + (m*g-m*g*cos(θ)*sin(π/2-θ))^2)."
+ }
+ ]
+ },
+ {
+ "questionSetup": [
+ "There is a 1kg weight on an inclined plane. The plane is at a ",
+ " angle from the ground. The system is in equilibrium (the net force on the weight is 0)."
+ ],
+ "variablesForQuestionSetup": ["theta - max 45"],
+ "question": "What is the minimum coefficient of static friction?",
+ "answerParts": ["coefficient of static friction"],
+ "answerSolutionDescriptions": [
+ "solve minimum static coefficient from wedge angle given equilibrium"
+ ],
+ "goal": "noMovement",
+ "hints": [
+ {
+ "description": "Net Force in Equilibrium",
+ "content": "If the system is in equilibrium, the sum of the x components of all forces must equal 0. In this system, the normal force and force of static friction have non-zero x components."
+ },
+ {
+ "description": "X Component of Normal Force",
+ "content": "The x component of the normal force is equal to m*g*cos(θ)*cos(π/2-θ), where θ is the angle of the incline plane."
+ },
+ {
+ "description": "X Component of Force of Friction",
+ "content": "The x component of the force of static friction is equal to μ*m*g*cos(θ)*cos(π-θ), where θ is the angle of the incline plane."
+ },
+ {
+ "description": "Equation to Solve for Minimum Coefficient of Static Friction",
+ "content": "Since the net force in the x direction must be 0, we can solve the equation 0=m*g*cos(θ)*cos(π/2-θ)+μ*m*g*cos(θ)*cos(π-θ) for μ to find the minimum coefficient of static friction such that the system stays in equilibrium."
+ }
+ ]
+ },
+ {
+ "questionSetup": [
+ "There is a 1kg weight on an inclined plane. The coefficient of static friction is ",
+ ". The system is in equilibrium (the net force on the weight is 0)."
+ ],
+ "variablesForQuestionSetup": ["coefficient of static friction"],
+ "question": "What is the maximum angle of the plane from the ground?",
+ "answerParts": ["wedge angle"],
+ "answerSolutionDescriptions": [
+ "solve maximum wedge angle from coefficient of static friction given equilibrium"
+ ],
+ "goal": "noMovement",
+ "hints": [
+ {
+ "description": "Net Force in Equilibrium",
+ "content": "If the system is in equilibrium, the sum of the x components of all forces must equal 0. In this system, the normal force and force of static friction have non-zero x components."
+ },
+ {
+ "description": "X Component of Normal Force",
+ "content": "The x component of the normal force is equal to m*g*cos(θ)*cos(π/2-θ), where θ is the angle of the incline plane."
+ },
+ {
+ "description": "X Component of Force of Friction",
+ "content": "The x component of the force of static friction is equal to μ*m*g*cos(θ)*cos(π-θ), where θ is the angle of the incline plane."
+ },
+ {
+ "description": "Equation to Solve for Maximum Wedge Angle",
+ "content": "Since the net force in the x direction must be 0, we can solve the equation 0=m*g*cos(θ)*cos(π/2-θ)+μ*m*g*cos(θ)*cos(π-θ) for θ to find the maximum wedge angle such that the system stays in equilibrium."
+ },
+ {
+ "description": "Simplifying Equation to Solve for Maximum Wedge Angle",
+ "content": "Simplifying 0=m*g*cos(θ)*cos(π/2-θ)+μ*m*g*cos(θ)*cos(π-θ), we get cos(π/2-θ)=-μ*cos(π-θ)."
+ },
+ {
+ "description": "Simplifying Equation to Solve for Maximum Wedge Angle",
+ "content": "The cosine subtraction formula states that cos(A-B)=cos(A)*cos(B)+sin(A)sin(B)."
+ },
+ {
+ "description": "Simplifying Equation to Solve for Maximum Wedge Angle",
+ "content": "Applying the cosine subtraction formula to cos(π/2-θ)=-μ*cos(π-θ), we get cos(π/2)*cos(θ)+sin(π/2)*sin(θ)=-μ*(cos(π)cos(θ)+sin(π)sin(θ))."
+ },
+ {
+ "description": "Simplifying Equation to Solve for Maximum Wedge Angle",
+ "content": "Simplifying cos(π/2)*cos(θ)-sin(π/2)*sin(θ)=-μ*(cos(π)cos(θ)-sin(π)sin(θ)), we get -sin(θ)=-μ*(-cos(θ))."
+ },
+ {
+ "description": "Simplifying Equation to Solve for Maximum Wedge Angle",
+ "content": "Simplifying -sin(θ)=-μ*(-cos(θ)), we get tan(θ)=-μ."
+ },
+ {
+ "description": "Simplifying Equation to Solve for Maximum Wedge Angle",
+ "content": "Solving for θ, we get θ = atan(μ)."
+ }
+ ]
+ }
+ ]
+}
diff --git a/src/client/views/nodes/PhysicsBox/PhysicsSimulationTutorial.json b/src/client/views/nodes/PhysicsBox/PhysicsSimulationTutorial.json
new file mode 100644
index 000000000..3015deaa4
--- /dev/null
+++ b/src/client/views/nodes/PhysicsBox/PhysicsSimulationTutorial.json
@@ -0,0 +1,600 @@
+{
+ "freeWeight": {
+ "question": "A 1kg weight is at rest on the ground. What are the magnitude and directions of the forces acting on the weight?",
+ "steps": [
+ {
+ "description": "Forces",
+ "content": "There are two forces acting on the weight: the force of gravity and the normal force.",
+ "forces": [
+ {
+ "description": "Gravity",
+ "magnitude": 9.81,
+ "directionInDegrees": 270,
+ "component": false
+ },
+ {
+ "description": "Normal Force",
+ "magnitude": 9.81,
+ "directionInDegrees": 90,
+ "component": false
+ }
+ ],
+ "showMagnitude": false
+ },
+ {
+ "description": "Force of Gravity",
+ "content": "The force of gravity acts in the negative y direction: 3Ï€/2 rad. It has magnitude equal to m*g. We can approximate g as 9.81.",
+ "forces": [
+ {
+ "description": "Gravity",
+ "magnitude": 9.81,
+ "directionInDegrees": 270,
+ "component": false
+ }
+ ],
+ "showMagnitude": true
+ },
+ {
+ "description": "Normal Force",
+ "content": "The normal force acts in the positive y direction: π/2 rad. It has magnitude equal to m*g. We can approximate g as 9.81.",
+ "forces": [
+ {
+ "description": "Normal Force",
+ "magnitude": 9.81,
+ "directionInDegrees": 90,
+ "component": false
+ }
+ ],
+ "showMagnitude": true
+ },
+ {
+ "description": "All Forces",
+ "content": "Combining all of the forces, we get the following free body diagram.",
+ "forces": [
+ {
+ "description": "Gravity",
+ "magnitude": 9.81,
+ "directionInDegrees": 270,
+ "component": false
+ },
+ {
+ "description": "Normal Force",
+ "magnitude": 9.81,
+ "directionInDegrees": 90,
+ "component": false
+ }
+ ],
+ "showMagnitude": true
+ }
+ ]
+ },
+ "pendulum": {
+ "question": "A 1kg weight on a 300m rod of negligible mass is released from an angle 30 degrees offset from equilibrium. What are the magnitude and directions of the forces acting on the weight immediately after release? (Ignore air resistance)",
+ "steps": [
+ {
+ "description": "Forces",
+ "content": "There are two force acting on the weight: the force of gravity and the force of tension.",
+ "forces": [
+ {
+ "description": "Gravity",
+ "magnitude": 9.81,
+ "directionInDegrees": 270,
+ "component": false
+ },
+ {
+ "description": "Tension",
+ "magnitude": 8.5,
+ "directionInDegrees": 60,
+ "component": false
+ }
+ ],
+ "showMagnitude": false
+ },
+ {
+ "description": "Force of Gravity",
+ "content": "The force of gravity acts in the negative y direction: 3Ï€/2 rad. It has magnitude equal to m*g. We can approximate g as 9.81.",
+ "forces": [
+ {
+ "description": "Gravity",
+ "magnitude": 9.81,
+ "directionInDegrees": 270,
+ "component": false
+ }
+ ],
+ "showMagnitude": true
+ },
+ {
+ "description": "Tension",
+ "content": "The force of tension acts along the direction of the rod. The rod is 30 degrees offset from equilibrium, so the direction along the rod is 90-30=60 degrees. The tension force has two components—the component creating the centripetal force and the component canceling out the parallel component of gravity. The weight has just been released, so it has velocity 0, meaning the centripetal force is 0. Thus, the tension force only acts to cancel out the parallel component of gravity. Thus, the magnitude of tension is m*g*sin(60°)",
+ "forces": [
+ {
+ "description": "Tension",
+ "magnitude": 8.5,
+ "directionInDegrees": 60,
+ "component": false
+ },
+ {
+ "description": "Gravity - Parallel Component",
+ "magnitude": 8.5,
+ "directionInDegrees": 240,
+ "component": true
+ }
+ ],
+ "showMagnitude": true
+ },
+ {
+ "description": "All Forces",
+ "content": "Combining all of the forces, we get the following free body diagram.",
+ "forces": [
+ {
+ "description": "Gravity",
+ "magnitude": 9.81,
+ "directionInDegrees": 270,
+ "component": false
+ },
+ {
+ "description": "Tension",
+ "magnitude": 8.5,
+ "directionInDegrees": 60,
+ "component": false
+ }
+ ],
+ "showMagnitude": true
+ }
+ ]
+ },
+ "inclinePlane": {
+ "question": "There is a 1kg weight on an inclined plane. The plane is at an angle θ from the ground, and has a coefficient of static friction μ. The system is in equilibrium (the net force on the weight is 0). What are the magnitudes and directions of the forces acting on the weight?",
+ "steps": [
+ {
+ "description": "Forces",
+ "content": "There are three forces acting on the weight: the force of gravity, the normal force, and the force of static friction.",
+ "forces": [
+ {
+ "description": "Gravity",
+ "magnitude": 9.81,
+ "directionInDegrees": 270,
+ "component": false
+ },
+ {
+ "description": "Normal Force",
+ "magnitude": 8.817,
+ "directionInDegrees": 64,
+ "component": false
+ },
+ {
+ "description": "Friction Force",
+ "magnitude": 4.3,
+ "directionInDegrees": 154,
+ "component": false
+ }
+ ],
+ "showMagnitude": false
+ },
+ {
+ "description": "Force of Gravity",
+ "content": "The force of gravity acts in the negative y direction: 3Ï€/2 rad. It has magnitude equal to m*g. We can approximate g as 9.81.",
+ "forces": [
+ {
+ "description": "Gravity",
+ "magnitude": 9.81,
+ "directionInDegrees": 270,
+ "component": false
+ }
+ ],
+ "showMagnitude": true
+ },
+ {
+ "description": "Normal Force",
+ "content": "The normal force acts in the direction perpendicular to the incline plane: π/2-θ rad, where θ is the angle of the incline plane. The magnitude of the normal force is equal to m*g*cos(θ).",
+ "forces": [
+ {
+ "description": "Normal Force",
+ "magnitude": 8.817,
+ "directionInDegrees": 64,
+ "component": false
+ }
+ ],
+ "showMagnitude": true
+ },
+ {
+ "description": "Force of Static Friction",
+ "content": "The force of static friction acts in the direction along the incline plane: π-θ rad, where θ is the angle of the incline plane. We can use the knowledge that the system is in equilibrium to solve for the magnitude of the force of static friction.",
+ "forces": [
+ {
+ "description": "Friction Force",
+ "magnitude": 4.3,
+ "directionInDegrees": 154,
+ "component": false
+ }
+ ],
+ "showMagnitude": false
+ },
+ {
+ "description": "Net X Force in Equilibrium",
+ "content": "For the system to be in equilibrium, the sum of the x components of all forces must equal 0. The x component of the normal force is equal to m*g*cos(θ)*cos(π/2-θ), where θ is the angle of the incline plane. The x component of gravity is equal to 0. Since the net force in the x direction must be 0, we know the magnitude of the x component of the friction force is m*g*cos(θ)*cos(π/2-θ).",
+ "forces": [
+ {
+ "description": "Normal Force - X Component",
+ "magnitude": 3.87,
+ "directionInDegrees": 0,
+ "component": true
+ },
+ {
+ "description": "Friction Force - X Component",
+ "magnitude": 3.87,
+ "directionInDegrees": 180,
+ "component": true
+ }
+ ],
+ "showMagnitude": true
+ },
+ {
+ "description": "Net Y Force Normal Force",
+ "content": "For the system to be in equilibrium, the sum of the y components of all forces must equal 0. The y component of the normal force is equal to m*g*cos(θ)*sin(π/2-θ), where θ is the angle of the incline plane. The y component of gravity is equal to m*g. Since the net force in the x direction must be 0, we know the magnitude of the y component of the friction force is m*g-m*g*cos(θ)*sin(π/2-θ).",
+ "forces": [
+ {
+ "description": "Normal Force - Y Component ",
+ "magnitude": 7.92,
+ "directionInDegrees": 90,
+ "component": true
+ },
+ {
+ "description": "Gravity - Y Component ",
+ "magnitude": 9.81,
+ "directionInDegrees": 270,
+ "component": true
+ },
+ {
+ "description": "Friction Force - Y Component ",
+ "magnitude": 1.89,
+ "directionInDegrees": 90,
+ "component": true
+ }
+ ],
+ "showMagnitude": true
+ },
+ {
+ "description": "Magnitude of Force of Friction",
+ "content": "Combining the x and y components of the friction force, we get the magnitude of the friction force is equal to sqrt((m*g*cos(θ)*cos(π/2-θ))^2 + (m*g*cos(θ)*sin(π/2-θ)-m*g)^2).",
+ "forces": [
+ {
+ "description": "Friction Force",
+ "magnitude": 4.3,
+ "directionInDegrees": 154,
+ "component": false
+ }
+ ],
+ "showMagnitude": true
+ },
+ {
+ "description": "All Forces",
+ "content": "Combining all of the forces, we get the following free body diagram.",
+ "forces": [
+ {
+ "description": "Gravity",
+ "magnitude": 9.81,
+ "directionInDegrees": 270,
+ "component": false
+ },
+ {
+ "description": "Normal Force",
+ "magnitude": 8.817,
+ "directionInDegrees": 64,
+ "component": false
+ },
+ {
+ "description": "Friction Force",
+ "magnitude": 4.3,
+ "directionInDegrees": 154,
+ "component": false
+ }
+ ],
+ "showMagnitude": true
+ }
+ ]
+ },
+ "spring": {
+ "question": "A 1kg weight is on a spring of negligible mass with rest length 200m and spring constant 0.5. What is the equilibrium spring length?",
+ "steps": [
+ {
+ "description": "Forces",
+ "content": "We can start by solving for the forces acting on the weight at any given point in time. There are two forces potentially acting on the weight: the force of gravity and the spring force. In equilibrium, these forces will be perfectly balanced.",
+ "forces": [
+ {
+ "description": "Gravity",
+ "magnitude": 9.81,
+ "directionInDegrees": 270,
+ "component": false
+ },
+ {
+ "description": "Spring Force",
+ "magnitude": 9.81,
+ "directionInDegrees": 90,
+ "component": false
+ }
+ ],
+ "showMagnitude": false
+ },
+ {
+ "description": "Force of Gravity",
+ "content": "The force of gravity acts in the negative y direction: 3Ï€/2 rad. It has magnitude equal to m*g. We can approximate g as 9.81.",
+ "forces": [
+ {
+ "description": "Gravity",
+ "magnitude": 9.81,
+ "directionInDegrees": 270,
+ "component": false
+ }
+ ],
+ "showMagnitude": true
+ },
+ {
+ "description": "Spring Force",
+ "content": "The spring force acts in the negative y direction (3Ï€/2 rad) if the spring is compressed. The spring force acts in the positive y direction (Ï€/2 rad) if the spring is extended. Because the forces are perfectly balanced and gravity acts in the negative y direction, the spring force must act in the positive y direction and have the same magnitude as the force og gravity, m*g. We can approximate g as 9.81.",
+ "forces": [
+ {
+ "description": "Spring Force",
+ "magnitude": 9.81,
+ "directionInDegrees": 90,
+ "component": false
+ }
+ ],
+ "showMagnitude": true
+ },
+ {
+ "description": "Spring Force",
+ "content": "We can use the spring force equation, Fs=kd to solve for the displacement such that Fs=mg. Setting them equal, we get mg=kd. Plugging in for the known values of m,g, and k, we get 1*9.81=0.5d. Solving for d, we get d=19.62 as the equilibrium starting displacement",
+ "forces": [
+ {
+ "description": "Spring Force",
+ "magnitude": 9.81,
+ "directionInDegrees": 90,
+ "component": false
+ }
+ ],
+ "showMagnitude": true
+ }
+ ]
+ },
+ "circular": {
+ "question": "A 1kg weight is attached to a 100m rod of negligible mass. The weight is undergoing uniform circular motion with tangential velocity 40 m/s. What are the magnitude and directions of the forces acting on the weight? (Ignore air resistance)",
+ "steps": [
+ {
+ "description": "Forces",
+ "content": "There is one force acting on the weight: the centripetal force.",
+ "forces": [
+ {
+ "description": "Centripetal Force",
+ "magnitude": 16,
+ "directionInDegrees": 90,
+ "component": false
+ }
+ ],
+ "showMagnitude": false
+ },
+ {
+ "description": "Centripetal Force",
+ "content": "The centripetal force is always directed toward the center of the circle. The formula for solving for the magnitude of centripetal force for an object undergoing uniform circular motion is Fc=mv^2 / r. Plugging in for known values, we get Fc=1*(40^2)/100. Solving for this, we get Fc=16",
+ "forces": [
+ {
+ "description": "Centripetal Force",
+ "magnitude": 16,
+ "directionInDegrees": 90,
+ "component": false
+ }
+ ],
+ "showMagnitude": true
+ }
+ ]
+ },
+ "pulley": {
+ "question": "A 1kg red weight is attached to a simple pulley with a rope of negligible mass. A 1.5kg blue weight is attached to the other end of the simple pulley. What are the forces acting on the red weight?",
+ "steps": [
+ {
+ "description": "Forces",
+ "content": "There are two force acting on the red weight: the force of gravity and the force of tension.",
+ "forces": [
+ {
+ "description": "Gravity",
+ "magnitude": 9.81,
+ "directionInDegrees": 270,
+ "component": false
+ },
+ {
+ "description": "Tension",
+ "magnitude": 11.77,
+ "directionInDegrees": 90,
+ "component": false
+ }
+ ],
+ "showMagnitude": false
+ },
+ {
+ "description": "Gravity",
+ "content": "The force of gravity acts in the negative y direction: 3Ï€/2 rad. It has magnitude equal to m*g. We can approximate g as 9.81.",
+ "forces": [
+ {
+ "description": "Gravity",
+ "magnitude": 9.81,
+ "directionInDegrees": 270,
+ "component": false
+ }
+ ],
+ "showMagnitude": true
+ },
+ {
+ "description": "Tension",
+ "content": "The force of tension acts in the positive y direction: π/2 rad. We know that the acceleration in a simple pulley system is (mass 2 - mass 1) * acceleration due to gravity / (mass 1 + mass 2) = (1.5-1) * 9.81 / (1.5+1) = 1.962 m/s^2. Because the acceleration is caused by the force of gravity and force of tension, we can solve for the force of tension acting on the weight as mass 1 * (a + acceleration due to gravity) = 1 * (1.962+9.81) = 11.77.",
+ "forces": [
+ {
+ "description": "Tension",
+ "magnitude": 11.77,
+ "directionInDegrees": 90,
+ "component": false
+ }
+ ],
+ "showMagnitude": true
+ },
+ {
+ "description": "All Forces",
+ "content": "Combining all of the forces, we get the following free body diagram.",
+ "forces": [
+ {
+ "description": "Gravity",
+ "magnitude": 9.81,
+ "directionInDegrees": 270,
+ "component": false
+ },
+ {
+ "description": "Tension",
+ "magnitude": 11.77,
+ "directionInDegrees": 90,
+ "component": false
+ }
+ ],
+ "showMagnitude": true
+ }
+ ]
+ },
+ "suspension": {
+ "question": "A 1kg weight is attached to two rods hanging from 45° angles from the ceiling. The system is in equilibrium, i.e. the weight does not move. What are the magnitudes and directions of the forces acting on the weight?",
+ "steps": [
+ {
+ "description": "Forces",
+ "content": "There are three force acting on the red weight: the force of gravity, the force of tension from the left rod, and the force of tension from the right rod.",
+ "forces": [
+ {
+ "description": "Gravity",
+ "magnitude": 9.81,
+ "directionInDegrees": 270,
+ "component": false
+ },
+ {
+ "description": "Left Tension",
+ "magnitude": 6.94,
+ "directionInDegrees": 135,
+ "component": false
+ },
+ {
+ "description": "Right Tension",
+ "magnitude": 6.94,
+ "directionInDegrees": 45,
+ "component": false
+ }
+ ],
+ "showMagnitude": false
+ },
+ {
+ "description": "Force X Components",
+ "content": "There are two forces with x components to consider: the tension from the left rod and the tension from the right rod. These must cancel each other out so that the net x force is 0.",
+ "forces": [
+ {
+ "description": "Left Tension X Component",
+ "magnitude": 4.907,
+ "directionInDegrees": 180,
+ "component": true
+ },
+ {
+ "description": "Right Tension X Component",
+ "magnitude": 4.907,
+ "directionInDegrees": 0,
+ "component": true
+ }
+ ],
+ "showMagnitude": false
+ }, {
+ "description": "Force Y Components",
+ "content": "There are three forces with y components to consider: the tension from the left rod, the tension from the right rod, and the force of gravity.",
+ "forces": [
+ {
+ "description": "Left Tension Y Component",
+ "magnitude": 4.907,
+ "directionInDegrees": 90,
+ "component": true
+ },
+ {
+ "description": "Right Tension Y Component",
+ "magnitude": 4.907,
+ "directionInDegrees": 90,
+ "component": true
+ },
+ {
+ "description": "Gravity Y Component",
+ "magnitude": 9.81,
+ "directionInDegrees": 270,
+ "component": true
+ }
+ ],
+ "showMagnitude": false
+ }, {
+ "description": "Force Y Components",
+ "content": "The y components of forces must cancel each other out so that the net y force is 0. Thus, gravity = left tension y component + right tension y component. Because the x components of tension are the same and the angles of each rod are the same, the y components must be the same. Thus, the y component for each force of tension must be 9.81/2.",
+ "forces": [
+ {
+ "description": "Left Tension Y Component",
+ "magnitude": 4.907,
+ "directionInDegrees": 90,
+ "component": true
+ },
+ {
+ "description": "Right Tension Y Component",
+ "magnitude": 4.907,
+ "directionInDegrees": 90,
+ "component": true
+ },
+ {
+ "description": "Gravity Y Component",
+ "magnitude": 9.81,
+ "directionInDegrees": 270,
+ "component": true
+ }
+ ],
+ "showMagnitude": true
+ }, {
+ "description": "Tension",
+ "content": "Now that we know the y component of tension for each rod is 4.907, we can solve for the full force of tension as 4.907 = T * sin(45°) -> T = 6.94.",
+ "forces": [
+ {
+ "description": "Left Tension",
+ "magnitude": 6.94,
+ "directionInDegrees": 135,
+ "component": false
+ },
+ {
+ "description": "Right Tension",
+ "magnitude": 6.94,
+ "directionInDegrees": 45,
+ "component": false
+ }
+ ],
+ "showMagnitude": true
+ },
+ {
+ "description": "All Forces",
+ "content": "Combining all of the forces, we get the following free body diagram.",
+ "forces": [
+ {
+ "description": "Gravity",
+ "magnitude": 9.81,
+ "directionInDegrees": 270,
+ "component": false
+ },
+ {
+ "description": "Left Tension",
+ "magnitude": 6.94,
+ "directionInDegrees": 135,
+ "component": false
+ },
+ {
+ "description": "Right Tension",
+ "magnitude": 6.94,
+ "directionInDegrees": 45,
+ "component": false
+ }
+ ],
+ "showMagnitude": true
+ }
+ ]
+ }
+}
diff --git a/src/client/views/nodes/PhysicsBox/PhysicsSimulationWall.tsx b/src/client/views/nodes/PhysicsBox/PhysicsSimulationWall.tsx
new file mode 100644
index 000000000..8cc1d0fbf
--- /dev/null
+++ b/src/client/views/nodes/PhysicsBox/PhysicsSimulationWall.tsx
@@ -0,0 +1,34 @@
+import React = require('react');
+
+export interface Force {
+ magnitude: number;
+ directionInDegrees: number;
+}
+export interface IWallProps {
+ length: number;
+ xPos: number;
+ yPos: number;
+ angleInDegrees: number;
+}
+
+export default class Wall extends React.Component<IWallProps> {
+
+ constructor(props: any) {
+ super(props)
+ }
+
+ wallStyle = {
+ width: this.props.angleInDegrees == 0 ? this.props.length + "%" : "5px",
+ height: this.props.angleInDegrees == 0 ? "5px" : this.props.length + "%",
+ position: "absolute" as "absolute",
+ left: this.props.xPos + "%",
+ top: this.props.yPos + "%",
+ backgroundColor: "#6c7b8b",
+ margin: 0,
+ padding: 0,
+ };
+
+ render () {
+ return (<div style={this.wallStyle}></div>);
+ }
+};
diff --git a/src/client/views/nodes/PhysicsBox/PhysicsSimulationWeight.tsx b/src/client/views/nodes/PhysicsBox/PhysicsSimulationWeight.tsx
new file mode 100644
index 000000000..2165c8ba9
--- /dev/null
+++ b/src/client/views/nodes/PhysicsBox/PhysicsSimulationWeight.tsx
@@ -0,0 +1,990 @@
+import { computed, IReactionDisposer, reaction } from 'mobx';
+import { observer } from 'mobx-react';
+import './PhysicsSimulationBox.scss';
+import React = require('react');
+
+interface IWallProps {
+ length: number;
+ xPos: number;
+ yPos: number;
+ angleInDegrees: number;
+}
+interface IForce {
+ description: string;
+ magnitude: number;
+ directionInDegrees: number;
+}
+export interface IWeightProps {
+ pause: () => void;
+ panelWidth: () => number;
+ panelHeight: () => number;
+ resetRequest: () => number;
+ circularMotionRadius: number;
+ coefficientOfKineticFriction: number;
+ color: string;
+ componentForces: () => IForce[];
+ setComponentForces: (x: IForce[]) => {};
+ displayXVelocity: number;
+ displayYVelocity: number;
+ elasticCollisions: boolean;
+ gravity: number;
+ mass: number;
+ simulationMode: string;
+ noMovement: boolean;
+ paused: boolean;
+ pendulumAngle: number;
+ pendulumLength: number;
+ radius: number;
+ showAcceleration: boolean;
+ showComponentForces: boolean;
+ showForceMagnitudes: boolean;
+ showForces: boolean;
+ showVelocity: boolean;
+ simulationSpeed: number;
+ simulationType: string;
+ springConstant: number;
+ springRestLength: number;
+ springStartLength: number;
+ startForces: () => IForce[];
+ startPendulumAngle: number;
+ startPendulumLength: number;
+ startPosX: number;
+ startPosY: number;
+ startVelX: number;
+ startVelY: number;
+ timestepSize: number;
+ updateMassPosX: number;
+ updateMassPosY: number;
+ forcesUpdated: () => IForce[];
+ setForcesUpdated: (x: IForce[]) => {};
+ setPosition: (x: number | undefined, y: number | undefined) => void;
+ setVelocity: (x: number | undefined, y: number | undefined) => void;
+ setAcceleration: (x: number, y: number) => void;
+ setPendulumAngle: (ang: number | undefined, length: number | undefined) => void;
+ setSpringLength: (length: number) => void;
+ wallPositions: IWallProps[];
+ wedgeHeight: number;
+ wedgeWidth: number;
+ xMax: number;
+ xMin: number;
+ yMax: number;
+ yMin: number;
+}
+
+interface IState {
+ angleLabel: number;
+ clickPositionX: number;
+ clickPositionY: number;
+ coordinates: string;
+ dragging: boolean;
+ kineticFriction: boolean;
+ maxPosYConservation: number;
+ timer: number;
+ updatedStartPosX: any;
+ updatedStartPosY: any;
+ xPosition: number;
+ xVelocity: number;
+ yPosition: number;
+ yVelocity: number;
+ xAccel: number;
+ yAccel: number;
+}
+@observer
+export default class Weight extends React.Component<IWeightProps, IState> {
+ constructor(props: any) {
+ super(props);
+ this.state = {
+ angleLabel: 0,
+ clickPositionX: 0,
+ clickPositionY: 0,
+ coordinates: '',
+ dragging: false,
+ kineticFriction: false,
+ maxPosYConservation: 0,
+ timer: 0,
+ updatedStartPosX: this.props.startPosX,
+ updatedStartPosY: this.props.startPosY,
+ xPosition: this.props.startPosX,
+ xVelocity: this.props.startVelX,
+ yPosition: this.props.startPosY,
+ yVelocity: this.props.startVelY,
+ xAccel: 0,
+ yAccel: 0,
+ };
+ }
+
+ _timer: NodeJS.Timeout | undefined;
+ _resetDisposer: IReactionDisposer | undefined;
+
+ componentWillUnmount() {
+ this._timer && clearTimeout(this._timer);
+ this._resetDisposer?.();
+ }
+ componentWillUpdate(nextProps: Readonly<IWeightProps>, nextState: Readonly<IState>, nextContext: any): void {
+ if (nextProps.paused) {
+ this._timer && clearTimeout(this._timer);
+ this._timer = undefined;
+ } else if (this.props.paused) {
+ this._timer && clearTimeout(this._timer);
+ this._timer = setInterval(() => this.setState({ timer: this.state.timer + 1 }), 50);
+ }
+ }
+
+ // Constants
+ @computed get draggable() {
+ return !['Inclined Plane', 'Pendulum'].includes(this.props.simulationType) && this.props.simulationMode === 'Freeform';
+ }
+ @computed get panelHeight() {
+ return Math.max(800, this.props.panelHeight()) + 'px';
+ }
+ @computed get panelWidth() {
+ return Math.max(1000, this.props.panelWidth()) + 'px';
+ }
+
+ @computed get walls() {
+ return ['One Weight', 'Inclined Plane'].includes(this.props.simulationType) ? this.props.wallPositions : [];
+ }
+ epsilon = 0.0001;
+ labelBackgroundColor = `rgba(255,255,255,0.5)`;
+
+ // Variables
+ weightStyle = {
+ alignItems: 'center',
+ backgroundColor: this.props.color,
+ borderColor: 'black',
+ borderRadius: 50 + '%',
+ borderStyle: 'solid',
+ display: 'flex',
+ height: 2 * this.props.radius + 'px',
+ justifyContent: 'center',
+ left: this.props.startPosX + 'px',
+ position: 'absolute' as 'absolute',
+ top: this.props.startPosY + 'px',
+ touchAction: 'none',
+ width: 2 * this.props.radius + 'px',
+ zIndex: 5,
+ };
+
+ // Helper function to go between display and real values
+ getDisplayYPos = (yPos: number) => this.props.yMax - yPos - 2 * this.props.radius + 5;
+ gravityForce = (): IForce => ({
+ description: 'Gravity',
+ magnitude: this.props.mass * this.props.gravity,
+ directionInDegrees: 270,
+ });
+ // Update display values when simulation updates
+ setDisplayValues = (xPos: number = this.state.xPosition, yPos: number = this.state.yPosition, xVel: number = this.state.xVelocity, yVel: number = this.state.yVelocity) => {
+ this.props.setPosition(xPos, this.getDisplayYPos(yPos));
+ this.props.setVelocity(xVel, yVel);
+ const xAccel = Math.round(this.getNewAccelerationX(this.props.forcesUpdated()) * 100) / 100;
+ const yAccel = (-1 * Math.round(this.getNewAccelerationY(this.props.forcesUpdated()) * 100)) / 100;
+ this.props.setAcceleration(xAccel, yAccel);
+ this.setState({ xAccel, yAccel });
+ };
+ componentDidMount() {
+ this._resetDisposer = reaction(() => this.props.resetRequest(), this.resetEverything);
+ }
+ componentDidUpdate(prevProps: Readonly<IWeightProps>, prevState: Readonly<IState>, snapshot?: any): void {
+ if (prevProps.simulationType != this.props.simulationType) {
+ this.setState({ xVelocity: this.props.startVelX, yVelocity: this.props.startVelY });
+ this.setDisplayValues();
+ }
+
+ // Change pendulum angle from input field
+ if (prevProps.startPendulumAngle != this.props.startPendulumAngle || prevProps.startPendulumLength !== this.props.startPendulumLength) {
+ const length = this.props.startPendulumLength;
+ const x = length * Math.cos(((90 - this.props.startPendulumAngle) * Math.PI) / 180);
+ const y = length * Math.sin(((90 - this.props.startPendulumAngle) * Math.PI) / 180);
+ const xPosition = this.props.xMax / 2 - x - this.props.radius;
+ const yPosition = y - this.props.radius - 5;
+ this.setState({ xPosition, yPosition, updatedStartPosX: xPosition, updatedStartPosY: yPosition });
+ this.props.setPendulumAngle(this.props.startPendulumAngle, this.props.startPendulumLength);
+ }
+
+ // When display values updated by user, update real value
+ if (prevProps.updateMassPosX !== this.props.updateMassPosX) {
+ const x = Math.min(Math.max(0, this.props.updateMassPosX), this.props.xMax - 2 * this.props.radius);
+ this.setState({ updatedStartPosX: x, xPosition: x });
+ this.props.setPosition(x, undefined);
+ }
+ if (prevProps.updateMassPosY != this.props.updateMassPosY) {
+ const y = Math.min(Math.max(0, this.props.updateMassPosY), this.props.yMax - 2 * this.props.radius);
+ const coordinatePosition = this.getDisplayYPos(y);
+ this.setState({ yPosition: coordinatePosition, updatedStartPosY: coordinatePosition });
+ this.props.setPosition(undefined, this.getDisplayYPos(y));
+
+ if (this.props.displayXVelocity != this.state.xVelocity) {
+ this.setState({ xVelocity: this.props.displayXVelocity });
+ this.props.setVelocity(this.props.displayXVelocity, undefined);
+ }
+
+ if (this.props.displayYVelocity != -this.state.yVelocity) {
+ this.setState({ yVelocity: -this.props.displayYVelocity });
+ this.props.setVelocity(undefined, this.props.displayYVelocity);
+ }
+ }
+
+ // Make sure weight doesn't go above max height
+ if ((prevState.updatedStartPosY != this.state.updatedStartPosY || prevProps.startVelY != this.props.startVelY) && !isNaN(this.state.updatedStartPosY) && !isNaN(this.props.startVelY)) {
+ if (this.props.simulationType == 'One Weight') {
+ let maxYPos = this.state.updatedStartPosY;
+ if (this.props.startVelY != 0) {
+ maxYPos -= (this.props.startVelY * this.props.startVelY) / (2 * this.props.gravity);
+ }
+ if (maxYPos < 0) maxYPos = 0;
+
+ this.setState({ maxPosYConservation: maxYPos });
+ }
+ }
+
+ // Check for collisions and update
+ if (!this.props.paused && !this.props.noMovement && prevState.timer != this.state.timer) {
+ let collisions = false;
+ if (this.props.simulationType == 'One Weight' || this.props.simulationType == 'Inclined Plane') {
+ const collisionsWithGround = this.checkForCollisionsWithGround();
+ const collisionsWithWalls = this.checkForCollisionsWithWall();
+ collisions = collisionsWithGround || collisionsWithWalls;
+ }
+ if (this.props.simulationType == 'Pulley') {
+ if (this.state.yPosition <= this.props.yMin + 100 || this.state.yPosition >= this.props.yMax - 100) {
+ collisions = true;
+ }
+ }
+ if (!collisions) this.update();
+
+ this.setDisplayValues();
+ }
+
+ // Convert from static to kinetic friction if/when weight slips on inclined plane
+ if (prevState.xVelocity != this.state.xVelocity) {
+ if (this.props.simulationType == 'Inclined Plane' && Math.abs(this.state.xVelocity) > 0.1 && this.props.simulationMode != 'Review' && !this.state.kineticFriction) {
+ this.setState({ kineticFriction: true });
+ const normalForce: IForce = {
+ description: 'Normal Force',
+ magnitude: this.props.mass * this.props.gravity * Math.cos(Math.atan(this.props.wedgeHeight / this.props.wedgeWidth)),
+ directionInDegrees: 180 - 90 - (Math.atan(this.props.wedgeHeight / this.props.wedgeWidth) * 180) / Math.PI,
+ };
+ const frictionForce: IForce = {
+ description: 'Kinetic Friction Force',
+ magnitude: this.props.mass * this.props.coefficientOfKineticFriction * this.props.gravity * Math.cos(Math.atan(this.props.wedgeHeight / this.props.wedgeWidth)),
+ directionInDegrees: 180 - (Math.atan(this.props.wedgeHeight / this.props.wedgeWidth) * 180) / Math.PI,
+ };
+ // reduce magnitude of friction force if necessary such that block cannot slide up plane
+ // prettier-ignore
+ const yForce = - this.props.gravity +
+ normalForce.magnitude * Math.sin((normalForce.directionInDegrees * Math.PI) / 180) +
+ frictionForce.magnitude * Math.sin((frictionForce.directionInDegrees * Math.PI) / 180);
+ if (yForce > 0) {
+ frictionForce.magnitude = (-normalForce.magnitude * Math.sin((normalForce.directionInDegrees * Math.PI) / 180) + this.props.gravity) / Math.sin((frictionForce.directionInDegrees * Math.PI) / 180);
+ }
+
+ const normalForceComponent: IForce = {
+ description: 'Normal Force',
+ magnitude: this.props.mass * this.props.gravity * Math.cos(Math.atan(this.props.wedgeHeight / this.props.wedgeWidth)),
+ directionInDegrees: 180 - 90 - (Math.atan(this.props.wedgeHeight / this.props.wedgeWidth) * 180) / Math.PI,
+ };
+ const gravityParallel: IForce = {
+ description: 'Gravity Parallel Component',
+ magnitude: this.props.mass * this.props.gravity * Math.sin(Math.PI / 2 - Math.atan(this.props.wedgeHeight / this.props.wedgeWidth)),
+ directionInDegrees: 180 - 90 - (Math.atan(this.props.wedgeHeight / this.props.wedgeWidth) * 180) / Math.PI + 180,
+ };
+ const gravityPerpendicular: IForce = {
+ description: 'Gravity Perpendicular Component',
+ magnitude: this.props.mass * this.props.gravity * Math.cos(Math.PI / 2 - Math.atan(this.props.wedgeHeight / this.props.wedgeWidth)),
+ directionInDegrees: 360 - (Math.atan(this.props.wedgeHeight / this.props.wedgeWidth) * 180) / Math.PI,
+ };
+ const kineticFriction = this.props.coefficientOfKineticFriction != 0 ? [frictionForce] : [];
+ this.props.setForcesUpdated([this.gravityForce(), normalForce, ...kineticFriction]);
+ this.props.setComponentForces([normalForceComponent, gravityParallel, gravityPerpendicular, ...kineticFriction]);
+ }
+ }
+
+ // Update x position when start pos x changes
+ if (prevProps.startPosX != this.props.startPosX) {
+ if (this.props.paused && !isNaN(this.props.startPosX)) {
+ this.setState({ xPosition: this.props.startPosX, updatedStartPosX: this.props.startPosX });
+ this.props.setPosition(this.props.startPosX, undefined);
+ }
+ }
+
+ // Update y position when start pos y changes TODO debug
+ if (prevProps.startPosY != this.props.startPosY) {
+ if (this.props.paused && !isNaN(this.props.startPosY)) {
+ this.setState({ yPosition: this.props.startPosY, updatedStartPosY: this.props.startPosY ?? 0 });
+ this.props.setPosition(undefined, this.getDisplayYPos(this.props.startPosY));
+ }
+ }
+
+ // Update wedge coordinates
+ if (!this.state.coordinates || this.props.yMax !== prevProps.yMax || prevProps.wedgeWidth != this.props.wedgeWidth || prevProps.wedgeHeight != this.props.wedgeHeight) {
+ const left = this.props.xMax * 0.25;
+ const coordinatePair1 = Math.round(left) + ',' + this.props.yMax + ' ';
+ const coordinatePair2 = Math.round(left + this.props.wedgeWidth) + ',' + this.props.yMax + ' ';
+ const coordinatePair3 = Math.round(left) + ',' + (this.props.yMax - this.props.wedgeHeight);
+ this.setState({ coordinates: coordinatePair1 + coordinatePair2 + coordinatePair3 });
+ }
+
+ if (this.state.xPosition != prevState.xPosition || this.state.yPosition != prevState.yPosition) {
+ this.weightStyle = {
+ alignItems: 'center',
+ backgroundColor: this.props.color,
+ borderColor: 'black',
+ borderRadius: 50 + '%',
+ borderStyle: 'solid',
+ display: 'flex',
+ height: 2 * this.props.radius + 'px',
+ justifyContent: 'center',
+ left: this.state.xPosition + 'px',
+ position: 'absolute' as 'absolute',
+ top: this.state.yPosition + 'px',
+ touchAction: 'none',
+ width: 2 * this.props.radius + 'px',
+ zIndex: 5,
+ };
+ }
+ }
+
+ // Reset simulation on reset button click
+ resetEverything = () => {
+ this.setState({
+ kineticFriction: false,
+ xPosition: this.state.updatedStartPosX,
+ yPosition: this.state.updatedStartPosY,
+ xVelocity: this.props.startVelX,
+ yVelocity: this.props.startVelY,
+ angleLabel: Math.round(this.props.pendulumAngle * 100) / 100,
+ });
+ this.props.setPendulumAngle(this.props.startPendulumAngle, undefined);
+ this.props.setForcesUpdated(this.props.startForces());
+ this.props.setPosition(this.state.updatedStartPosX, this.state.updatedStartPosY);
+ this.props.setVelocity(this.props.startVelX, this.props.startVelY);
+ this.props.setAcceleration(0, 0);
+ setTimeout(() => this.setState({ timer: this.state.timer + 1 }));
+ };
+
+ // Compute x acceleration from forces, F=ma
+ getNewAccelerationX = (forceList: IForce[]) => {
+ // prettier-ignore
+ return forceList.reduce((newXacc, force) =>
+ newXacc + (force.magnitude * Math.cos((force.directionInDegrees * Math.PI) / 180)) / this.props.mass, 0);
+ };
+
+ // Compute y acceleration from forces, F=ma
+ getNewAccelerationY = (forceList: IForce[]) => {
+ // prettier-ignore
+ return forceList.reduce((newYacc, force) =>
+ newYacc + (-1 * (force.magnitude * Math.sin((force.directionInDegrees * Math.PI) / 180))) / this.props.mass, 0);
+ };
+
+ // Compute uniform circular motion forces given x, y positions
+ getNewCircularMotionForces = (xPos: number, yPos: number): IForce[] => {
+ const deltaX = (this.props.xMin + this.props.xMax) / 2 - (xPos + this.props.radius);
+ const deltaY = yPos + this.props.radius - (this.props.yMin + this.props.yMax) / 2;
+ return [
+ {
+ description: 'Centripetal Force',
+ magnitude: (this.props.startVelX ** 2 * this.props.mass) / this.props.circularMotionRadius,
+ directionInDegrees: (Math.atan2(deltaY, deltaX) * 180) / Math.PI,
+ },
+ ];
+ };
+
+ // Compute spring forces given y position
+ getNewSpringForces = (yPos: number): IForce[] => {
+ const yPosPlus = yPos - this.props.springRestLength > 0;
+ const yPosMinus = yPos - this.props.springRestLength < 0;
+ return [
+ this.gravityForce(),
+ {
+ description: 'Spring Force',
+ magnitude: this.props.springConstant * (yPos - this.props.springRestLength) * (yPosPlus ? 1 : yPosMinus ? -1 : 0),
+ directionInDegrees: yPosPlus ? 90 : 270,
+ },
+ ];
+ };
+
+ // Compute pendulum forces given position, velocity
+ getNewPendulumForces = (xPos: number, yPos: number, xVel: number, yVel: number): IForce[] => {
+ const x = this.props.xMax / 2 - xPos - this.props.radius;
+ const y = yPos + this.props.radius + 5;
+ const angle = (ang => (ang < 0 ? ang + 180 : ang))((Math.atan(y / x) * 180) / Math.PI);
+
+ let oppositeAngle = 90 - angle;
+ if (oppositeAngle < 0) {
+ oppositeAngle = 90 - (180 - angle);
+ }
+
+ const pendulumLength = Math.sqrt(x * x + y * y);
+ this.props.setPendulumAngle(oppositeAngle, undefined);
+
+ const mag = this.props.mass * this.props.gravity * Math.cos((oppositeAngle * Math.PI) / 180) + (this.props.mass * (xVel * xVel + yVel * yVel)) / pendulumLength;
+
+ return [
+ this.gravityForce(),
+ {
+ description: 'Tension',
+ magnitude: mag,
+ directionInDegrees: angle,
+ },
+ ];
+ };
+
+ // Check for collisions in x direction
+ checkForCollisionsWithWall = () => {
+ let collision = false;
+ if (this.state.xVelocity !== 0) {
+ this.walls
+ .filter(wall => wall.angleInDegrees === 90)
+ .forEach(wall => {
+ const wallX = (wall.xPos / 100) * this.props.panelWidth();
+ const minX = this.state.xPosition < wallX && wall.xPos < 0.35;
+ const maxX = this.state.xPosition + 2 * this.props.radius >= wallX && wall.xPos > 0.35;
+ if (minX || maxX) {
+ this.setState({
+ xPosition: minX ? wallX + 0.01 : wallX - 2 * this.props.radius - 0.01,
+ xVelocity: this.props.elasticCollisions ? -this.state.xVelocity : 0,
+ });
+ collision = true;
+ }
+ });
+ }
+ return collision;
+ };
+
+ // Check for collisions in y direction
+ checkForCollisionsWithGround = () => {
+ let collision = false;
+ const minY = this.state.yPosition;
+ const maxY = this.state.yPosition + 2 * this.props.radius;
+ if (this.state.yVelocity > 0) {
+ this.walls.forEach(wall => {
+ if (wall.angleInDegrees == 0 && wall.yPos > 0.4) {
+ const groundY = (wall.yPos / 100) * this.props.panelHeight();
+ const gravity = this.gravityForce();
+ if (maxY > groundY) {
+ this.setState({ yPosition: groundY - 2 * this.props.radius - 0.01 });
+ if (this.props.elasticCollisions) {
+ this.setState({ yVelocity: -this.state.yVelocity });
+ } else {
+ this.setState({ yVelocity: 0 });
+ const normalForce: IForce = {
+ description: 'Normal force',
+ magnitude: gravity.magnitude,
+ directionInDegrees: -gravity.directionInDegrees,
+ };
+ this.props.setForcesUpdated([gravity, normalForce]);
+ if (this.props.simulationType === 'Inclined Plane') {
+ this.props.setComponentForces([gravity, normalForce]);
+ }
+ }
+ collision = true;
+ }
+ }
+ });
+ }
+ if (this.state.yVelocity < 0) {
+ this.walls.forEach(wall => {
+ if (wall.angleInDegrees == 0 && wall.yPos < 0.4) {
+ const groundY = (wall.yPos / 100) * this.props.panelHeight();
+ if (minY < groundY) {
+ this.setState({
+ yPosition: groundY + 0.01,
+ yVelocity: this.props.elasticCollisions ? -this.state.yVelocity : 0,
+ });
+ collision = true;
+ }
+ }
+ });
+ }
+ return collision;
+ };
+
+ // Called at each RK4 step
+ evaluate = (currentXPos: number, currentYPos: number, currentXVel: number, currentYVel: number, currdeltaXPos: number, currdeltaYPos: number, currdeltaXVel: number, currdeltaYVel: number, dt: number) => {
+ const xPos = currentXPos + currdeltaXPos * dt;
+ const yPos = currentYPos + currdeltaYPos * dt;
+ const xVel = currentXVel + currdeltaXVel * dt;
+ const yVel = currentYVel + currdeltaYVel * dt;
+ const forces = this.getForces(xPos, yPos, xVel, yVel);
+ return {
+ xPos,
+ yPos,
+ xVel,
+ yVel,
+ deltaXPos: xVel,
+ deltaYPos: yVel,
+ deltaXVel: this.getNewAccelerationX(forces),
+ deltaYVel: this.getNewAccelerationY(forces),
+ };
+ };
+
+ getForces = (xPos: number, yPos: number, xVel: number, yVel: number) => {
+ // prettier-ignore
+ switch (this.props.simulationType) {
+ case 'Pendulum': return this.getNewPendulumForces(xPos, yPos, xVel, yVel);
+ case 'Spring' : return this.getNewSpringForces(yPos);
+ case 'Circular Motion': return this.getNewCircularMotionForces(xPos, yPos);
+ default: return this.props.forcesUpdated();
+ }
+ };
+
+ // Update position, velocity using RK4 method
+ update = () => {
+ const startXVel = this.state.xVelocity;
+ const startYVel = this.state.yVelocity;
+ let xPos = this.state.xPosition;
+ let yPos = this.state.yPosition;
+ let xVel = this.state.xVelocity;
+ let yVel = this.state.yVelocity;
+ const forces = this.getForces(xPos, yPos, xVel, yVel);
+ const xAcc = this.getNewAccelerationX(forces);
+ const yAcc = this.getNewAccelerationY(forces);
+ const coeff = (this.props.timestepSize * 1.0) / 6.0;
+ for (let i = 0; i < this.props.simulationSpeed; i++) {
+ const k1 = this.evaluate(xPos, yPos, xVel, yVel, xVel, yVel, xAcc, yAcc, 0);
+ const k2 = this.evaluate(xPos, yPos, xVel, yVel, k1.deltaXPos, k1.deltaYPos, k1.deltaXVel, k1.deltaYVel, this.props.timestepSize * 0.5);
+ const k3 = this.evaluate(xPos, yPos, xVel, yVel, k2.deltaXPos, k2.deltaYPos, k2.deltaXVel, k2.deltaYVel, this.props.timestepSize * 0.5);
+ const k4 = this.evaluate(xPos, yPos, xVel, yVel, k3.deltaXPos, k3.deltaYPos, k3.deltaXVel, k3.deltaYVel, this.props.timestepSize);
+
+ xVel += coeff * (k1.deltaXVel + 2 * (k2.deltaXVel + k3.deltaXVel) + k4.deltaXVel);
+ yVel += coeff * (k1.deltaYVel + 2 * (k2.deltaYVel + k3.deltaYVel) + k4.deltaYVel);
+ xPos += coeff * (k1.deltaXPos + 2 * (k2.deltaXPos + k3.deltaXPos) + k4.deltaXPos);
+ yPos += coeff * (k1.deltaYPos + 2 * (k2.deltaYPos + k3.deltaYPos) + k4.deltaYPos);
+ }
+ // make sure harmonic motion maintained and errors don't propagate
+ switch (this.props.simulationType) {
+ case 'Spring':
+ const equilibriumPos = this.props.springRestLength + (this.props.mass * this.props.gravity) / this.props.springConstant;
+ const amplitude = Math.abs(equilibriumPos - this.props.springStartLength);
+ if (startYVel < 0 && yVel > 0 && yPos < this.props.springRestLength) {
+ yPos = equilibriumPos - amplitude;
+ } else if (startYVel > 0 && yVel < 0 && yPos > this.props.springRestLength) {
+ yPos = equilibriumPos + amplitude;
+ }
+ break;
+ case 'Pendulum':
+ const startX = this.state.updatedStartPosX;
+ if (startXVel <= 0 && xVel > 0) {
+ xPos = this.state.updatedStartPosX;
+ if (this.state.updatedStartPosX > this.props.xMax / 2) {
+ xPos = this.props.xMax / 2 + (this.props.xMax / 2 - startX) - 2 * this.props.radius;
+ }
+ yPos = this.props.startPosY;
+ } else if (startXVel >= 0 && xVel < 0) {
+ xPos = this.state.updatedStartPosX;
+ if (this.state.updatedStartPosX < this.props.xMax / 2) {
+ xPos = this.props.xMax / 2 + (this.props.xMax / 2 - startX) - 2 * this.props.radius;
+ }
+ yPos = this.props.startPosY;
+ }
+ break;
+ case 'One Weight':
+ if (yPos < this.state.maxPosYConservation) {
+ yPos = this.state.maxPosYConservation;
+ }
+ }
+ this.setState({ xVelocity: xVel, yVelocity: yVel, xPosition: xPos, yPosition: yPos });
+
+ const forcesn = this.getForces(xPos, yPos, xVel, yVel);
+ this.props.setForcesUpdated(forcesn);
+
+ // set component forces if they change
+ if (this.props.simulationType == 'Pendulum') {
+ const x = this.props.xMax / 2 - xPos - this.props.radius;
+ const y = yPos + this.props.radius + 5;
+ let angle = (Math.atan(y / x) * 180) / Math.PI;
+ if (angle < 0) {
+ angle += 180;
+ }
+ let oppositeAngle = 90 - angle;
+ if (oppositeAngle < 0) {
+ oppositeAngle = 90 - (180 - angle);
+ }
+
+ const pendulumLength = Math.sqrt(x * x + y * y);
+
+ const tensionComponent: IForce = {
+ description: 'Tension',
+ magnitude: this.props.mass * this.props.gravity * Math.cos((oppositeAngle * Math.PI) / 180) + (this.props.mass * (xVel * xVel + yVel * yVel)) / pendulumLength,
+ directionInDegrees: angle,
+ };
+ const gravityParallel: IForce = {
+ description: 'Gravity Parallel Component',
+ magnitude: this.props.gravity * Math.cos(((90 - angle) * Math.PI) / 180),
+ directionInDegrees: 270 - (90 - angle),
+ };
+ const gravityPerpendicular: IForce = {
+ description: 'Gravity Perpendicular Component',
+ magnitude: this.props.gravity * Math.sin(((90 - angle) * Math.PI) / 180),
+ directionInDegrees: -(90 - angle),
+ };
+ if (this.props.gravity * Math.sin(((90 - angle) * Math.PI) / 180) < 0) {
+ gravityPerpendicular.magnitude = Math.abs(this.props.gravity * Math.sin(((90 - angle) * Math.PI) / 180));
+ gravityPerpendicular.directionInDegrees = 180 - (90 - angle);
+ }
+ this.props.setComponentForces([tensionComponent, gravityParallel, gravityPerpendicular]);
+ }
+ };
+
+ renderForce = (force: IForce, index: number, asComponent: boolean, color = '#0d0d0d') => {
+ if (force.magnitude < this.epsilon) return;
+
+ const angle = (force.directionInDegrees * Math.PI) / 180;
+ const arrowStartY = this.state.yPosition + this.props.radius - this.props.radius * Math.sin(angle);
+ const arrowStartX = this.state.xPosition + this.props.radius + this.props.radius * Math.cos(angle);
+ const arrowEndY = arrowStartY - Math.abs(force.magnitude) * Math.sin(angle) - this.props.radius * Math.sin(angle);
+ const arrowEndX = arrowStartX + Math.abs(force.magnitude) * Math.cos(angle) + this.props.radius * Math.cos(angle);
+
+ let labelTop = arrowEndY + (force.directionInDegrees >= 0 && force.directionInDegrees < 180 ? 40 : -40);
+ let labelLeft = arrowEndX + (force.directionInDegrees > 90 && force.directionInDegrees < 270 ? -120 : 30);
+
+ labelTop = Math.max(Math.min(labelTop, this.props.yMax + 50), this.props.yMin);
+ labelLeft = Math.max(Math.min(labelLeft, this.props.xMax - 60), this.props.xMin);
+
+ return (
+ <div key={index} style={{ zIndex: 6, position: 'absolute' }}>
+ <div
+ style={{
+ pointerEvents: 'none',
+ position: 'absolute',
+ left: this.props.xMin,
+ top: this.props.yMin,
+ }}>
+ <svg width={this.props.xMax - this.props.xMin + 'px'} height={this.panelHeight}>
+ <defs>
+ <marker id="forceArrow" markerWidth="4" markerHeight="4" refX="0" refY="2" orient="auto" markerUnits="strokeWidth">
+ <path d="M0,0 L0,4 L4,2 z" fill={color} />
+ </marker>
+ </defs>
+ <line strokeDasharray={asComponent ? '10,10' : undefined} x1={arrowStartX} y1={arrowStartY} x2={arrowEndX} y2={arrowEndY} stroke={color} strokeWidth="5" markerEnd="url(#forceArrow)" />
+ </svg>
+ </div>
+ <div
+ style={{
+ pointerEvents: 'none',
+ position: 'absolute',
+ left: labelLeft + 'px',
+ top: labelTop + 'px',
+ lineHeight: 1,
+ backgroundColor: this.labelBackgroundColor,
+ }}>
+ <p>{force.description || 'Force'}</p>
+ {this.props.showForceMagnitudes && <p>{Math.round(100 * force.magnitude) / 100} N</p>}
+ </div>
+ </div>
+ );
+ };
+
+ renderVector = (id: string, magX: number, magY: number, color: string, label: string) => {
+ const mag = Math.sqrt(magX * magX + magY * magY);
+ return (
+ <div className="showvecs" style={{ zIndex: 6 }}>
+ <svg width={this.panelWidth} height={this.panelHeight}>
+ <defs>
+ <marker id={id} markerWidth="10" markerHeight="10" refX="0" refY="3" orient="auto" markerUnits="strokeWidth">
+ <path d="M0,0 L0,6 L9,3 z" fill={color} />
+ </marker>
+ </defs>
+ <line
+ x1={this.state.xPosition + this.props.radius + (magX / mag) * this.props.radius}
+ y1={this.state.yPosition + this.props.radius + (magY / mag) * this.props.radius}
+ x2={this.state.xPosition + this.props.radius + (magX / mag) * this.props.radius + magX}
+ y2={this.state.yPosition + this.props.radius + (magY / mag) * this.props.radius + magY}
+ stroke={color}
+ strokeWidth="5"
+ markerEnd={`url(#${id})`}
+ />
+ </svg>
+ <div
+ style={{
+ pointerEvents: 'none',
+ position: 'absolute',
+ left: this.state.xPosition + this.props.radius + 2 * (magX / mag) * this.props.radius + magX + 'px',
+ top: this.state.yPosition + this.props.radius + 2 * (magY / mag) * this.props.radius + magY + 'px',
+ lineHeight: 1,
+ }}>
+ <p style={{ background: 'white' }}>{label}</p>
+ </div>
+ </div>
+ );
+ };
+
+ // Render weight, spring, rod(s), vectors
+ render() {
+ return (
+ <div>
+ <div
+ className="weightContainer"
+ onPointerDown={e => {
+ if (this.draggable) {
+ this.props.pause();
+ this.setState({
+ dragging: true,
+ clickPositionX: e.clientX,
+ clickPositionY: e.clientY,
+ });
+ }
+ }}
+ onPointerMove={e => {
+ if (this.state.dragging) {
+ let newY = this.state.yPosition + e.clientY - this.state.clickPositionY;
+ if (newY > this.props.yMax - 2 * this.props.radius - 10) {
+ newY = this.props.yMax - 2 * this.props.radius - 10;
+ } else if (newY < 10) {
+ newY = 10;
+ }
+
+ let newX = this.state.xPosition + e.clientX - this.state.clickPositionX;
+ if (newX > this.props.xMax - 2 * this.props.radius - 10) {
+ newX = this.props.xMax - 2 * this.props.radius - 10;
+ } else if (newX < 10) {
+ newX = 10;
+ }
+ if (this.props.simulationType == 'Suspension') {
+ if (newX < (this.props.xMax + this.props.xMin) / 4 - this.props.radius - 15) {
+ newX = (this.props.xMax + this.props.xMin) / 4 - this.props.radius - 15;
+ } else if (newX > (3 * (this.props.xMax + this.props.xMin)) / 4 - this.props.radius / 2 - 15) {
+ newX = (3 * (this.props.xMax + this.props.xMin)) / 4 - this.props.radius / 2 - 15;
+ }
+ }
+
+ this.setState({ yPosition: newY });
+ this.props.setPosition(undefined, Math.round((this.props.yMax - 2 * this.props.radius - newY + 5) * 100) / 100);
+ if (this.props.simulationType != 'Pulley') {
+ this.setState({ xPosition: newX });
+ this.props.setPosition(newX, undefined);
+ }
+ if (this.props.simulationType != 'Suspension') {
+ if (this.props.simulationType != 'Pulley') {
+ this.setState({ updatedStartPosX: newX });
+ }
+ this.setState({ updatedStartPosY: newY });
+ }
+ this.setState({
+ clickPositionX: e.clientX,
+ clickPositionY: e.clientY,
+ });
+ this.setDisplayValues();
+ }
+ }}
+ onPointerUp={e => {
+ if (this.state.dragging) {
+ if (this.props.simulationType != 'Pendulum' && this.props.simulationType != 'Suspension') {
+ this.resetEverything();
+ }
+ this.setState({ dragging: false });
+ let newY = this.state.yPosition + e.clientY - this.state.clickPositionY;
+ if (newY > this.props.yMax - 2 * this.props.radius - 10) {
+ newY = this.props.yMax - 2 * this.props.radius - 10;
+ } else if (newY < 10) {
+ newY = 10;
+ }
+
+ let newX = this.state.xPosition + e.clientX - this.state.clickPositionX;
+ if (newX > this.props.xMax - 2 * this.props.radius - 10) {
+ newX = this.props.xMax - 2 * this.props.radius - 10;
+ } else if (newX < 10) {
+ newX = 10;
+ }
+ if (this.props.simulationType == 'Spring') {
+ this.props.setSpringLength(newY);
+ }
+ if (this.props.simulationType == 'Suspension') {
+ const x1rod = (this.props.xMax + this.props.xMin) / 2 - this.props.radius - this.props.yMin - 200;
+ const x2rod = (this.props.xMax + this.props.xMin) / 2 + this.props.yMin + 200 + this.props.radius;
+ const deltaX1 = this.state.xPosition + this.props.radius - x1rod;
+ const deltaX2 = x2rod - (this.state.xPosition + this.props.radius);
+ const deltaY = this.state.yPosition + this.props.radius;
+ const dir1T = Math.PI - Math.atan(deltaY / deltaX1);
+ const dir2T = Math.atan(deltaY / deltaX2);
+ const tensionMag2 = (this.props.mass * this.props.gravity) / ((-Math.cos(dir2T) / Math.cos(dir1T)) * Math.sin(dir1T) + Math.sin(dir2T));
+ const tensionMag1 = (-tensionMag2 * Math.cos(dir2T)) / Math.cos(dir1T);
+ const tensionForce1: IForce = {
+ description: 'Tension',
+ magnitude: tensionMag1,
+ directionInDegrees: (dir1T * 180) / Math.PI,
+ };
+ const tensionForce2: IForce = {
+ description: 'Tension',
+ magnitude: tensionMag2,
+ directionInDegrees: (dir2T * 180) / Math.PI,
+ };
+ this.props.setForcesUpdated([tensionForce1, tensionForce2, this.gravityForce()]);
+ }
+ }
+ }}>
+ <div className="weight" style={this.weightStyle}>
+ <p className="weightLabel">{this.props.mass} kg</p>
+ </div>
+ </div>
+ {this.props.simulationType == 'Spring' && (
+ <div className="spring">
+ <svg width={this.panelWidth} height={this.panelHeight}>
+ {[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(val => {
+ const count = 10;
+ const xPos1 = this.state.xPosition + this.props.radius + (val % 2 === 0 ? -20 : 20);
+ const xPos2 = this.state.xPosition + this.props.radius + (val === 10 ? 0 : val % 2 === 0 ? 20 : -20);
+ const yPos1 = (val * this.state.yPosition) / count;
+ const yPos2 = val === 10 ? this.state.yPosition + this.props.radius : ((val + 1) * this.state.yPosition) / count;
+ return <line key={val} x1={xPos1} strokeLinecap="round" y1={yPos1} x2={xPos2} y2={yPos2} stroke={'#808080'} strokeWidth="10" />;
+ })}
+ </svg>
+ </div>
+ )}
+
+ {this.props.simulationType == 'Pulley' && (
+ <div className="rod">
+ <svg width={this.panelWidth} height={this.panelHeight}>
+ <line //
+ x1={this.state.xPosition + this.props.radius}
+ y1={this.state.yPosition + this.props.radius}
+ x2={this.state.xPosition + this.props.radius}
+ y2={this.props.yMin}
+ stroke={'#deb887'}
+ strokeWidth="10"
+ />
+ </svg>
+ </div>
+ )}
+ {this.props.simulationType == 'Pulley' && (
+ <div className="wheel">
+ <svg width={this.panelWidth} height={this.panelHeight}>
+ <circle cx={(this.props.xMax + this.props.xMin) / 2} cy={this.props.radius} r={this.props.radius * 1.5} fill={'#808080'} />
+ </svg>
+ </div>
+ )}
+ {this.props.simulationType == 'Suspension' && (
+ <div className="rod">
+ <svg width={this.panelWidth} height={this.panelHeight}>
+ <line
+ x1={this.state.xPosition + this.props.radius}
+ y1={this.state.yPosition + this.props.radius}
+ x2={(this.props.xMax + this.props.xMin) / 2 - this.props.radius - this.props.yMin - 200}
+ y2={this.props.yMin}
+ stroke={'#deb887'}
+ strokeWidth="10"
+ />
+ </svg>
+ <p
+ style={{
+ position: 'absolute',
+ left: (this.props.xMax + this.props.xMin) / 2 - this.props.radius - this.props.yMin - 200 + 80 + 'px',
+ top: 10 + 'px',
+ backgroundColor: this.labelBackgroundColor,
+ }}>
+ {Math.round(
+ ((Math.atan((this.state.yPosition + this.props.radius) / (this.state.xPosition + this.props.radius - ((this.props.xMax + this.props.xMin) / 2 - this.props.radius - this.props.yMin - 200))) * 180) / Math.PI) * 100
+ ) / 100}
+ °
+ </p>
+ <div className="rod">
+ <svg width={this.props.panelWidth() + 'px'} height={this.panelHeight}>
+ <line
+ x1={this.state.xPosition + this.props.radius}
+ y1={this.state.yPosition + this.props.radius}
+ x2={(this.props.xMax + this.props.xMin) / 2 + this.props.yMin + 200 + this.props.radius}
+ y2={this.props.yMin}
+ stroke={'#deb887'}
+ strokeWidth="10"
+ />
+ </svg>
+ </div>
+ <p
+ style={{
+ position: 'absolute',
+ left: (this.props.xMax + this.props.xMin) / 2 + this.props.yMin + 200 + this.props.radius - 80 + 'px',
+ top: 10 + 'px',
+ backgroundColor: this.labelBackgroundColor,
+ }}>
+ {Math.round(
+ ((Math.atan((this.state.yPosition + this.props.radius) / ((this.props.xMax + this.props.xMin) / 2 + this.props.yMin + 200 + this.props.radius - (this.state.xPosition + this.props.radius))) * 180) / Math.PI) * 100
+ ) / 100}
+ °
+ </p>
+ </div>
+ )}
+ {this.props.simulationType == 'Circular Motion' && (
+ <div className="rod">
+ <svg width={this.panelWidth} height={this.panelHeight}>
+ <line
+ x1={this.state.xPosition + this.props.radius}
+ y1={this.state.yPosition + this.props.radius}
+ x2={(this.props.xMin + this.props.xMax) / 2}
+ y2={(this.props.yMin + this.props.yMax) / 2}
+ stroke={'#deb887'}
+ strokeWidth="10"
+ />
+ </svg>
+ </div>
+ )}
+ {this.props.simulationType == 'Pendulum' && (
+ <div className="rod">
+ <svg width={this.panelWidth} height={this.panelHeight}>
+ <line x1={this.state.xPosition + this.props.radius} y1={this.state.yPosition + this.props.radius} x2={this.props.xMax / 2} y2={-5} stroke={'#deb887'} strokeWidth="10" />
+ </svg>
+ {!this.state.dragging && (
+ <div>
+ <p
+ style={{
+ position: 'absolute',
+ zIndex: 5,
+ left: this.state.xPosition + 'px',
+ top: this.state.yPosition - 70 + 'px',
+ backgroundColor: this.labelBackgroundColor,
+ }}>
+ {Math.round(this.props.pendulumLength)} m
+ </p>
+ <p
+ style={{
+ position: 'absolute',
+ left: this.props.xMax / 2 + 'px',
+ top: 30 + 'px',
+ backgroundColor: this.labelBackgroundColor,
+ }}>
+ {Math.round(this.props.pendulumAngle * 100) / 100}°
+ </p>
+ </div>
+ )}
+ </div>
+ )}
+ {this.props.simulationType == 'Inclined Plane' && (
+ <div>
+ <div className="wedge">
+ <svg width={this.panelWidth} height={this.props.yMax + 'px'}>
+ <polygon points={this.state.coordinates} style={{ fill: 'burlywood' }} />
+ </svg>
+ </div>
+ <p
+ style={{
+ position: 'absolute',
+ left: Math.round(this.props.xMax * 0.25 + this.props.wedgeWidth / 3) + 'px',
+ top: Math.round(this.props.yMax - 40) + 'px',
+ }}>
+ {Math.round(((Math.atan(this.props.wedgeHeight / this.props.wedgeWidth) * 180) / Math.PI) * 100) / 100}°
+ </p>
+ </div>
+ )}
+ {!this.state.dragging &&
+ this.props.showAcceleration &&
+ this.renderVector(
+ 'accArrow',
+ this.getNewAccelerationX(this.props.forcesUpdated()),
+ this.getNewAccelerationY(this.props.forcesUpdated()),
+ 'green',
+ `${Math.round(100 * Math.sqrt(this.state.xAccel * this.state.xAccel + this.state.yAccel * this.state.yAccel)) / 100} m/s^2`
+ )}
+ {!this.state.dragging &&
+ this.props.showVelocity &&
+ this.renderVector(
+ 'velArrow',
+ this.state.xVelocity,
+ this.state.yVelocity,
+ 'blue',
+ `${Math.round(100 * Math.sqrt(this.props.displayXVelocity * this.props.displayXVelocity + this.props.displayYVelocity * this.props.displayYVelocity)) / 100} m/s`
+ )}
+ {!this.state.dragging && this.props.showComponentForces && this.props.componentForces().map((force, index) => this.renderForce(force, index, true))}
+ {!this.state.dragging && this.props.showForces && this.props.forcesUpdated().map((force, index) => this.renderForce(force, index, false))}
+ </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 0ff7c4292..04f11a5df 100644
--- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
@@ -1,58 +1,65 @@
-import { action, observable } from "mobx";
-import { observer } from "mobx-react";
-import * as React from "react";
-import { VideoField } from "../../../../fields/URLField";
-import { Upload } from "../../../../server/SharedMediaTypes";
-import { ViewBoxBaseComponent } from "../../DocComponent";
-import { FieldView } from "../FieldView";
-import { VideoBox } from "../VideoBox";
+import { action, observable } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { VideoField } from '../../../../fields/URLField';
+import { Upload } from '../../../../server/SharedMediaTypes';
+import { ViewBoxBaseComponent } from '../../DocComponent';
+import { FieldView } from '../FieldView';
+import { VideoBox } from '../VideoBox';
import { RecordingView } from './RecordingView';
-import { DocumentType } from "../../../documents/DocumentTypes";
-import { Presentation } from "../../../util/TrackMovements";
-import { Doc } from "../../../../fields/Doc";
-import { Id } from "../../../../fields/FieldSymbols";
-
+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() {
+ public static LayoutString(fieldKey: string) {
+ return FieldView.LayoutString(RecordingBox, fieldKey);
+ }
- public static LayoutString(fieldKey: string) { return FieldView.LayoutString(RecordingBox, fieldKey); }
-
- private _ref: React.RefObject<HTMLDivElement> = React.createRef();
+ private _ref: React.RefObject<HTMLDivElement> = React.createRef();
- constructor(props: any) {
+ constructor(props: any) {
super(props);
- }
-
- componentDidMount() {
- Doc.SetNativeWidth(this.dataDoc, 1280);
- Doc.SetNativeHeight(this.dataDoc, 720);
- }
-
- @observable result: Upload.AccessPathInfo | undefined = undefined
- @observable videoDuration: number | undefined = undefined
-
- @action
- setVideoDuration = (duration: number) => {
- this.videoDuration = duration
- }
-
- @action
- setResult = (info: Upload.AccessPathInfo, presentation?: Presentation) => {
- this.result = info
- this.dataDoc.type = DocumentType.VID;
- this.dataDoc[this.fieldKey + "-duration"] = this.videoDuration;
-
- this.dataDoc.layout = VideoBox.LayoutString(this.fieldKey);
- this.dataDoc[this.props.fieldKey] = new VideoField(this.result.accessPaths.client);
- this.dataDoc[this.fieldKey + "-recorded"] = true;
- // stringify the presentation and store it
- presentation?.movements && (this.dataDoc[this.fieldKey + "-presentation"] = JSON.stringify(presentation));
- }
-
- render() {
- return <div className="recordingBox" ref={this._ref}>
- {!this.result && <RecordingView setResult={this.setResult} setDuration={this.setVideoDuration} id={this.rootDoc.proto?.[Id] || ''} />}
- </div>;
- }
+ }
+
+ componentDidMount() {
+ Doc.SetNativeWidth(this.dataDoc, 1280);
+ Doc.SetNativeHeight(this.dataDoc, 720);
+ }
+
+ @observable result: Upload.AccessPathInfo | undefined = undefined;
+ @observable videoDuration: number | undefined = undefined;
+
+ @action
+ setVideoDuration = (duration: number) => {
+ this.videoDuration = duration;
+ };
+
+ @action
+ setResult = (info: Upload.AccessPathInfo, presentation?: Presentation) => {
+ this.result = info;
+ this.dataDoc.type = DocumentType.VID;
+ this.dataDoc[this.fieldKey + '_duration'] = this.videoDuration;
+
+ this.dataDoc.layout = VideoBox.LayoutString(this.fieldKey);
+ this.dataDoc[this.props.fieldKey] = new VideoField(this.result.accessPaths.client);
+ this.dataDoc[this.fieldKey + '-recorded'] = true;
+ // stringify the presentation and store it
+ if (presentation?.movements) {
+ const presCopy = { ...presentation };
+ presCopy.movements = presentation.movements.map(movement => ({ ...movement, doc: movement.doc[Id] })) as any;
+ this.dataDoc[this.fieldKey + '-presentation'] = JSON.stringify(presCopy);
+ }
+ };
+
+ render() {
+ return (
+ <div className="recordingBox" ref={this._ref}>
+ {!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 ec5917b9e..51eb774e2 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx
@@ -1,32 +1,31 @@
import * as React from 'react';
-import "./RecordingView.scss";
-import { useEffect, useRef, useState } from "react";
-import { ProgressBar } from "./ProgressBar"
-import { MdBackspace } from 'react-icons/md';
+import { useEffect, useRef, useState } from 'react';
+import { IconContext } from 'react-icons';
import { FaCheckCircle } from 'react-icons/fa';
-import { IconContext } from "react-icons";
-import { Networking } from '../../../Network';
+import { MdBackspace } from 'react-icons/md';
import { Upload } from '../../../../server/SharedMediaTypes';
import { returnFalse, returnTrue, setupMoveUpEvents } from '../../../../Utils';
+import { Networking } from '../../../Network';
import { Presentation, TrackMovements } from '../../../util/TrackMovements';
+import { ProgressBar } from './ProgressBar';
+import './RecordingView.scss';
export interface MediaSegment {
- videoChunks: any[],
- endTime: number,
- startTime: number,
- presentation?: Presentation,
+ videoChunks: any[];
+ endTime: number;
+ startTime: number;
+ presentation?: Presentation;
}
interface IRecordingViewProps {
- setResult: (info: Upload.AccessPathInfo, presentation?: Presentation) => void
- setDuration: (seconds: number) => void
- id: string
+ setResult: (info: Upload.AccessPathInfo, presentation?: Presentation) => void;
+ setDuration: (seconds: number) => void;
+ id: string;
}
const MAXTIME = 100000;
export function RecordingView(props: IRecordingViewProps) {
-
const [recording, setRecording] = useState(false);
const recordingTimerRef = useRef<number>(0);
const [recordingTimer, setRecordingTimer] = useState(0); // unit is 0.01 second
@@ -46,19 +45,16 @@ export function RecordingView(props: IRecordingViewProps) {
const [finished, setFinished] = useState<boolean>(false);
const [trackScreen, setTrackScreen] = useState<boolean>(false);
-
-
const DEFAULT_MEDIA_CONSTRAINTS = {
video: {
width: 1280,
height: 720,
-
},
audio: {
echoCancellation: true,
noiseSuppression: true,
- sampleRate: 44100
- }
+ sampleRate: 44100,
+ },
};
useEffect(() => {
@@ -71,12 +67,11 @@ 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);
- !(result instanceof Error) ? props.setResult(result, concatPres || undefined) : console.error("video conversion failed");
+ !(result instanceof Error) ? props.setResult(result, concatPres || undefined) : console.error('video conversion failed');
})();
}
}, [videos]);
@@ -87,7 +82,9 @@ export function RecordingView(props: IRecordingViewProps) {
}, [finished]);
// check if the browser supports media devices on first load
- useEffect(() => { if (!navigator.mediaDevices) alert('This browser does not support getUserMedia.'); }, []);
+ useEffect(() => {
+ if (!navigator.mediaDevices) alert('This browser does not support getUserMedia.');
+ }, []);
useEffect(() => {
let interval: any = null;
@@ -102,24 +99,24 @@ export function RecordingView(props: IRecordingViewProps) {
}, [recording]);
useEffect(() => {
- setVideoProgressHelper(recordingTimer)
+ setVideoProgressHelper(recordingTimer);
recordingTimerRef.current = recordingTimer;
}, [recordingTimer]);
const setVideoProgressHelper = (progress: number) => {
const newProgress = (progress / MAXTIME) * 100;
setProgress(newProgress);
- }
+ };
const startShowingStream = async (mediaConstraints = DEFAULT_MEDIA_CONSTRAINTS) => {
const stream = await navigator.mediaDevices.getUserMedia(mediaConstraints);
- videoElementRef.current!.src = "";
+ videoElementRef.current!.src = '';
videoElementRef.current!.srcObject = stream;
videoElementRef.current!.muted = true;
return stream;
- }
+ };
const record = async () => {
// don't need to start a new stream every time we start recording a new segment
@@ -145,29 +142,28 @@ export function RecordingView(props: IRecordingViewProps) {
const nextVideo = {
videoChunks,
endTime: recordingTimerRef.current,
- startTime: videos?.lastElement()?.endTime || 0
+ startTime: videos?.lastElement()?.endTime || 0,
};
// depending on if a presenation exists, add it to the video
const presentation = TrackMovements.Instance.yieldPresentation();
- setVideos(videos => [...videos, (presentation != null && trackScreen) ? { ...nextVideo, presentation } : nextVideo]);
+ setVideos(videos => [...videos, presentation != null && trackScreen ? { ...nextVideo, presentation } : nextVideo]);
}
// reset the temporary chunks
videoChunks = [];
setRecording(false);
- }
+ };
videoRecorder.current.start(200);
- }
-
+ };
// if this is called, then we're done recording all the segments
const finish = (e: React.PointerEvent) => {
e.stopPropagation();
// call stop on the video recorder if active
- videoRecorder.current?.state !== "inactive" && videoRecorder.current?.stop();
+ videoRecorder.current?.state !== 'inactive' && videoRecorder.current?.stop();
// end the streams (audio/video) to remove recording icon
const stream = videoElementRef.current!.srcObject;
@@ -178,94 +174,91 @@ export function RecordingView(props: IRecordingViewProps) {
// this will call upon progessbar to update videos to be in the correct order
setFinished(true);
- }
+ };
const pause = (e: React.PointerEvent) => {
e.stopPropagation();
// if recording, then this is just a new segment
- videoRecorder.current?.state === "recording" && videoRecorder.current.stop();
- }
+ videoRecorder.current?.state === 'recording' && videoRecorder.current.stop();
+ };
const start = (e: React.PointerEvent) => {
- setupMoveUpEvents({}, e, returnTrue, returnFalse, e => {
- // start recording if not already recording
- if (!videoRecorder.current || videoRecorder.current.state === "inactive") record();
-
- return true; // cancels propagation to documentView to avoid selecting it.
- }, false, false);
- }
+ setupMoveUpEvents(
+ {},
+ e,
+ returnTrue,
+ returnFalse,
+ e => {
+ // start recording if not already recording
+ if (!videoRecorder.current || videoRecorder.current.state === 'inactive') record();
+
+ return true; // cancels propagation to documentView to avoid selecting it.
+ },
+ false,
+ false
+ );
+ };
const undoPrevious = (e: React.PointerEvent) => {
e.stopPropagation();
setDoUndo(prev => !prev);
- }
+ };
- const handleOnTimeUpdate = () => { playing && setVideoProgressHelper(videoElementRef.current!.currentTime); };
+ const handleOnTimeUpdate = () => {
+ playing && setVideoProgressHelper(videoElementRef.current!.currentTime);
+ };
const millisecondToMinuteSecond = (milliseconds: number) => {
const toTwoDigit = (digit: number) => {
- return String(digit).length == 1 ? "0" + digit : digit
- }
+ return String(digit).length == 1 ? '0' + digit : digit;
+ };
const minutes = Math.floor((milliseconds % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((milliseconds % (1000 * 60)) / 1000);
- return toTwoDigit(minutes) + " : " + toTwoDigit(seconds);
- }
+ return toTwoDigit(minutes) + ' : ' + toTwoDigit(seconds);
+ };
return (
<div className="recording-container">
<div className="video-wrapper">
- <video id={`video-${props.id}`}
- autoPlay
- muted
- onTimeUpdate={() => handleOnTimeUpdate()}
- ref={videoElementRef}
- />
+ <video id={`video-${props.id}`} autoPlay muted onTimeUpdate={() => handleOnTimeUpdate()} ref={videoElementRef} />
<div className="recording-sign">
<span className="dot" />
<p className="timer">{millisecondToMinuteSecond(recordingTimer * 10)}</p>
</div>
<div className="controls">
-
<div className="controls-inner-container">
- <div className="record-button-wrapper">
- {recording ?
- <button className="stop-button" onPointerDown={pause} /> :
- <button className="record-button" onPointerDown={start} />
- }
- </div>
-
- {!recording && (videos.length > 0 ?
-
- <div className="options-wrapper video-edit-wrapper">
- <IconContext.Provider value={{ color: "grey", className: "video-edit-buttons", style: { display: canUndo ? 'inherit' : 'none' } }}>
- <MdBackspace onPointerDown={undoPrevious} />
- </IconContext.Provider>
- <IconContext.Provider value={{ color: "#cc1c08", className: "video-edit-buttons" }}>
- <FaCheckCircle onPointerDown={finish} />
- </IconContext.Provider>
- </div>
-
- : <div className="options-wrapper track-screen-wrapper">
- <label className="track-screen">
- <input type="checkbox" checked={trackScreen} onChange={(e) => { setTrackScreen(e.target.checked) }} />
- <span className="checkmark"></span>
- Track Screen
- </label>
- </div>)}
-
+ <div className="record-button-wrapper">{recording ? <button className="stop-button" onPointerDown={pause} /> : <button className="record-button" onPointerDown={start} />}</div>
+
+ {!recording &&
+ (videos.length > 0 ? (
+ <div className="options-wrapper video-edit-wrapper">
+ <IconContext.Provider value={{ color: 'grey', className: 'video-edit-buttons', style: { display: canUndo ? 'inherit' : 'none' } }}>
+ <MdBackspace onPointerDown={undoPrevious} />
+ </IconContext.Provider>
+ <IconContext.Provider value={{ color: '#cc1c08', className: 'video-edit-buttons' }}>
+ <FaCheckCircle onPointerDown={finish} />
+ </IconContext.Provider>
+ </div>
+ ) : (
+ <div className="options-wrapper track-screen-wrapper">
+ <label className="track-screen">
+ <input
+ type="checkbox"
+ checked={trackScreen}
+ onChange={e => {
+ setTrackScreen(e.target.checked);
+ }}
+ />
+ <span className="checkmark"></span>
+ Track Screen
+ </label>
+ </div>
+ ))}
</div>
-
</div>
- <ProgressBar
- videos={videos}
- setVideos={setVideos}
- orderVideos={orderVideos}
- progress={progress}
- recording={recording}
- doUndo={doUndo}
- setCanUndo={setCanUndo}
- />
+ <ProgressBar videos={videos} setVideos={setVideos} orderVideos={orderVideos} progress={progress} recording={recording} doUndo={doUndo} setCanUndo={setCanUndo} />
</div>
- </div>)
-} \ No newline at end of file
+ </div>
+ );
+}
diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx
index 76a24d831..312b3c619 100644
--- a/src/client/views/nodes/ScreenshotBox.tsx
+++ b/src/client/views/nodes/ScreenshotBox.tsx
@@ -5,13 +5,14 @@ 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, OmitKeys, returnFalse, returnOne } from '../../../Utils';
+import { emptyFunction, returnFalse, returnOne, returnZero } from '../../../Utils';
import { DocUtils } from '../../documents/Documents';
import { DocumentType } from '../../documents/DocumentTypes';
import { Networking } from '../../Network';
@@ -24,6 +25,7 @@ import { FieldView, FieldViewProps } from './FieldView';
import { FormattedTextBox } from './formattedText/FormattedTextBox';
import './ScreenshotBox.scss';
import { VideoBox } from './VideoBox';
+
declare class MediaRecorder {
constructor(e: any, options?: any); // whatever MediaRecorder has
}
@@ -128,9 +130,9 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
this.setupDictation();
}
}
- getAnchor = () => {
- const startTime = Cast(this.layoutDoc._currentTimecode, 'number', null) || (this._videoRec ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined);
- return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, '_timecodeToShow', '_timecodeToHide', startTime, startTime === undefined ? undefined : startTime + 3) || this.rootDoc;
+ getAnchor = (addAsAnnotation: boolean) => {
+ const startTime = Cast(this.layoutDoc._layout_currentTimecode, 'number', null) || (this._videoRec ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined);
+ return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, startTime, startTime === undefined ? undefined : startTime + 3, undefined, addAsAnnotation) || this.rootDoc;
};
videoLoad = () => {
@@ -140,7 +142,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 +224,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);
}
@@ -235,14 +237,14 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
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);
- this.dataDoc[this.fieldKey + '-duration'] = (new Date().getTime() - this.recordingStart!) / 1000;
+ 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
this.dataDoc.type = DocumentType.VID;
this.layoutDoc.layout = VideoBox.LayoutString(this.fieldKey);
this.dataDoc.nativeWidth = this.dataDoc.nativeHeight = undefined;
- this.layoutDoc._fitWidth = undefined;
+ this.layoutDoc._layout_fitWidth = undefined;
this.dataDoc[this.props.fieldKey] = new VideoField(result.accessPaths.agnostic.client);
} else alert('video conversion failed');
};
@@ -270,15 +272,14 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
setupDictation = () => {
if (this.dataDoc[this.fieldKey + '-dictation']) return;
const dictationText = DocUtils.GetNewTextDoc('dictation', NumCast(this.rootDoc.x), NumCast(this.rootDoc.y) + NumCast(this.layoutDoc._height) + 10, NumCast(this.layoutDoc._width), 2 * NumCast(this.layoutDoc._height));
- dictationText._autoHeight = false;
+ dictationText._layout_autoHeight = false;
const dictationTextProto = Doc.GetProto(dictationText);
dictationTextProto.recordingSource = this.dataDoc;
dictationTextProto.recordingStart = ComputedField.MakeFunction(`self.recordingSource["${this.props.fieldKey}-recordingStart"]`);
dictationTextProto.mediaState = ComputedField.MakeFunction('self.recordingSource.mediaState');
this.dataDoc[this.fieldKey + '-dictation'] = dictationText;
};
- contentFunc = () => [this.threed, this.content];
- 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();
@@ -287,7 +288,10 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
<div className="videoBox-viewer">
<div style={{ position: 'relative', height: this.videoPanelHeight() }}>
<CollectionFreeFormView
- {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight']).omit}
+ {...this.props}
+ setContentView={emptyFunction}
+ NativeWidth={returnZero}
+ NativeHeight={returnZero}
PanelHeight={this.videoPanelHeight}
PanelWidth={this.props.PanelWidth}
focus={this.props.focus}
@@ -296,25 +300,26 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
select={emptyFunction}
isContentActive={returnFalse}
NativeDimScaling={returnOne}
+ isAnyChildContentActive={returnFalse}
whenChildContentsActiveChanged={emptyFunction}
removeDocument={returnFalse}
moveDocument={returnFalse}
addDocument={returnFalse}
- CollectionView={undefined}
ScreenToLocalTransform={this.props.ScreenToLocalTransform}
- renderDepth={this.props.renderDepth + 1}
- ContainingCollectionDoc={this.props.ContainingCollectionDoc}>
- {this.contentFunc}
+ renderDepth={this.props.renderDepth + 1}>
+ <>
+ {this.threed}
+ {this.content}
+ </>
</CollectionFreeFormView>
</div>
- <div style={{ position: 'relative', height: this.formattedPanelHeight() }}>
+ <div style={{ background: 'white', position: 'relative', height: this.formattedPanelHeight() }}>
{!(this.dataDoc[this.fieldKey + '-dictation'] instanceof Doc) ? null : (
<FormattedTextBox
- {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight']).omit}
- Document={this.dataDoc[this.fieldKey + '-dictation']}
+ {...this.props}
+ Document={DocCast(this.dataDoc[this.fieldKey + '-dictation'])}
fieldKey={'text'}
PanelHeight={this.formattedPanelHeight}
- isAnnotationOverlay={true}
select={emptyFunction}
isContentActive={emptyFunction}
NativeDimScaling={returnOne}
@@ -324,9 +329,8 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
removeDocument={returnFalse}
moveDocument={returnFalse}
addDocument={returnFalse}
- CollectionView={undefined}
renderDepth={this.props.renderDepth + 1}
- ContainingCollectionDoc={this.props.ContainingCollectionDoc}></FormattedTextBox>
+ />
)}
</div>
</div>
diff --git a/src/client/views/nodes/ScriptingBox.tsx b/src/client/views/nodes/ScriptingBox.tsx
index 4c8a836f1..3ad3c911d 100644
--- a/src/client/views/nodes/ScriptingBox.tsx
+++ b/src/client/views/nodes/ScriptingBox.tsx
@@ -8,7 +8,7 @@ import { listSpec } from '../../../fields/Schema';
import { ScriptField } from '../../../fields/ScriptField';
import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
import { TraceMobx } from '../../../fields/util';
-import { returnEmptyString } from '../../../Utils';
+import { returnAlways, returnEmptyString, returnTrue } from '../../../Utils';
import { DragManager } from '../../util/DragManager';
import { InteractionUtils } from '../../util/InteractionUtils';
import { CompileScript, ScriptParam } from '../../util/Scripting';
@@ -20,6 +20,7 @@ import { EditableView } from '../EditableView';
import { FieldView, FieldViewProps } from '../nodes/FieldView';
import { OverlayView } from '../OverlayView';
import { DocumentIconContainer } from './DocumentIcon';
+import { DocFocusOptions, DocumentView } from './DocumentView';
import './ScriptingBox.scss';
const _global = (window /* browser */ || global) /* node */ as any;
@@ -113,6 +114,8 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable
}
}
+ onClickScriptDisable = returnAlways;
+
@action
componentDidMount() {
this.props.setContentView?.(this);
@@ -164,7 +167,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable
// only included in buttons, transforms scripting UI to a button
@action
onFinish = () => {
- this.rootDoc.layoutKey = 'layout';
+ this.rootDoc.layout_fieldKey = 'layout';
};
// displays error message
@@ -179,13 +182,15 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable
const params: ScriptParam = {};
this.compileParams.forEach(p => (params[p.split(':')[0].trim()] = p.split(':')[1].trim()));
- const result = CompileScript(this.rawText, {
- editable: true,
- transformer: DocumentIconContainer.getTransformer(),
- params,
- typecheck: false,
- });
- Doc.SetInPlace(this.rootDoc, this.fieldKey, result.compiled ? new ScriptField(result, undefined, this.rawText) : new ScriptField(undefined, undefined, this.rawText), true);
+ const result = !this.rawText.trim()
+ ? ({ compiled: false, errors: undefined } as any)
+ : CompileScript(this.rawText, {
+ editable: true,
+ transformer: DocumentIconContainer.getTransformer(),
+ params,
+ typecheck: false,
+ });
+ Doc.SetInPlace(this.rootDoc, this.fieldKey, result.compiled ? new ScriptField(result, undefined, this.rawText) : undefined, true);
this.onError(result.compiled ? undefined : result.errors);
return result.compiled;
};
@@ -197,7 +202,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable
const bindings: { [name: string]: any } = {};
this.paramsNames.forEach(key => (bindings[key] = this.rootDoc[key]));
// binds vars so user doesnt have to refer to everything as self.<var>
- ScriptCast(this.rootDoc[this.fieldKey], null)?.script.run({ self: this.rootDoc, this: this.layoutDoc, ...bindings }, this.onError);
+ ScriptCast(this.rootDoc[this.fieldKey], null)?.script.run({ ...bindings, self: this.rootDoc, this: this.layoutDoc }, this.onError);
}
};
@@ -265,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
@@ -481,10 +490,6 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable
return value;
}
- scrollFocus = () => {
- return undefined;
- };
-
getSuggestedParams(pos: number) {
const firstScript = this.rawText.slice(0, pos);
const indexP = firstScript.lastIndexOf('.');
@@ -701,7 +706,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable
// toolbar (with compile and apply buttons) for scripting UI
renderScriptingTools() {
- const buttonStyle = 'scriptingBox-button' + (StrCast(this.rootDoc.layoutKey).startsWith('layout_on') ? '-finish' : '');
+ const buttonStyle = 'scriptingBox-button' + (StrCast(this.rootDoc.layout_fieldKey).startsWith('layout_on') ? '-finish' : '');
return (
<div className="scriptingBox-toolbar">
<button
@@ -729,7 +734,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable
Save
</button>
- {!StrCast(this.rootDoc.layoutKey).startsWith('layout_on') ? null : ( // onClick, onChecked, etc need a Finish button to return to their default layout
+ {!StrCast(this.rootDoc.layout_fieldKey).startsWith('layout_on') ? null : ( // onClick, onChecked, etc need a Finish button to return to their default layout
<button
className={buttonStyle}
onPointerDown={e => {
@@ -775,7 +780,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable
// toolbar (with edit and run buttons and error message) for params UI
renderTools(toolSet: string, func: () => void) {
- const buttonStyle = 'scriptingBox-button' + (StrCast(this.rootDoc.layoutKey).startsWith('layout_on') ? '-finish' : '');
+ const buttonStyle = 'scriptingBox-button' + (StrCast(this.rootDoc.layout_fieldKey).startsWith('layout_on') ? '-finish' : '');
return (
<div className="scriptingBox-toolbar">
<button
@@ -794,7 +799,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable
}}>
{toolSet}
</button>
- {!StrCast(this.rootDoc.layoutKey).startsWith('layout_on') ? null : (
+ {!StrCast(this.rootDoc.layout_fieldKey).startsWith('layout_on') ? null : (
<button
className={buttonStyle}
onPointerDown={e => {
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 70ac84fa4..1f52c2d92 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -3,17 +3,20 @@ 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 * as rp from 'request-promise';
-import { Doc, DocListCast, HeightSym, 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';
import { Cast, NumCast, StrCast } from '../../../fields/Types';
import { AudioField, ImageField, VideoField } from '../../../fields/URLField';
-import { emptyFunction, formatTime, OmitKeys, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../Utils';
+import { emptyFunction, formatTime, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils } from '../../../Utils';
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';
import { SnappingManager } from '../../util/SnappingManager';
@@ -27,10 +30,11 @@ import { DocumentDecorations } from '../DocumentDecorations';
import { MarqueeAnnotator } from '../MarqueeAnnotator';
import { AnchorMenu } from '../pdf/AnchorMenu';
import { StyleProp } from '../StyleProvider';
+import { OpenWhere } from './DocumentView';
import { FieldView, FieldViewProps } from './FieldView';
import { RecordingBox } from './RecordingBox';
+import { PinProps, PresBox } from './trails';
import './VideoBox.scss';
-import { ObjectField } from '../../../fields/ObjectField';
const path = require('path');
/**
@@ -50,30 +54,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
public static LayoutString(fieldKey: string) {
return FieldView.LayoutString(VideoBox, fieldKey);
}
- /**
- * Uploads an image buffer to the server and stores with specified filename. by default the image
- * is stored at multiple resolutions each retrieved by using the filename appended with _o, _s, _m, _l (indicating original, small, medium, or large)
- * @param imageUri the bytes of the image
- * @param returnedFilename the base filename to store the image on the server
- * @param nosuffix optionally suppress creating multiple resolution images
- */
- public static async convertDataUri(imageUri: string, returnedFilename: string, nosuffix = false, replaceRootFilename?: string) {
- try {
- const posting = Utils.prepend('/uploadURI');
- const returnedUri = await rp.post(posting, {
- body: {
- uri: imageUri,
- name: returnedFilename,
- nosuffix,
- replaceRootFilename,
- },
- json: true,
- });
- return returnedUri;
- } catch (e) {
- console.log('VideoBox :' + e);
- }
- }
static _youtubeIframeCounter: number = 0;
static heightPercent = 80; // height of video relative to videoBox when timeline is open
@@ -108,12 +88,12 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
@observable _scrubbing: boolean = false;
@computed get links() {
- return DocListCast(this.dataDoc.links);
+ return LinkManager.Links(this.dataDoc);
}
@computed get heightPercent() {
- return NumCast(this.layoutDoc._timelineHeightPercent, 100);
+ return NumCast(this.layoutDoc._layout_timelineHeightPercent, 100);
} // current percent of video relative to VideoBox height
- // @computed get rawDuration() { return NumCast(this.dataDoc[this.fieldKey + "-duration"]); }
+ // @computed get rawDuration() { return NumCast(this.dataDoc[this.fieldKey + "_duration"]); }
@observable rawDuration: number = 0;
@computed get youtubeVideoId() {
@@ -131,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() {
@@ -241,10 +221,20 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
}
}
+ _keepCurrentlyPlaying = false; // flag to prevent document when paused from being removed from global 'currentlyPlaying' list
+ IsPlaying = () => this._playing;
+ TogglePause = () => {
+ if (!this._playing) this.Play();
+ else {
+ this._keepCurrentlyPlaying = true;
+ this.pause();
+ setTimeout(() => (this._keepCurrentlyPlaying = false));
+ }
+ };
+
// pauses video
- @action public Pause = (update: boolean = true) => {
+ @action public pause = (update: boolean = true) => {
this._playing = false;
- this.removeCurrentlyPlaying();
try {
update && this.player?.pause();
update && this._audioPlayer?.pause();
@@ -262,6 +252,10 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
}
this._playRegionTimer = undefined;
};
+ @action Pause = (update: boolean = true) => {
+ this.pause(update);
+ !this._keepCurrentlyPlaying && this.removeCurrentlyPlaying();
+ };
// toggles video full screen
@action public FullScreen = () => {
@@ -273,7 +267,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
this.player && this._contentRef && this._contentRef.requestFullscreen();
}
try {
- this._youtubePlayer && this.props.addDocTab(this.rootDoc, 'add');
+ this._youtubePlayer && this.props.addDocTab(this.rootDoc, OpenWhere.add);
} catch (e) {
console.log('Video FullScreen Exception:', e);
}
@@ -331,19 +325,19 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
y: NumCast(this.layoutDoc.y, 1),
_width: 150,
_height: 50,
- title: (this.layoutDoc._currentTimecode || 0).toString(),
- _isLinkButton: true,
+ title: (this.layoutDoc._layout_currentTimecode || 0).toString(),
+ onClick: FollowLinkScript(),
});
this.props.addDocument?.(b);
- DocUtils.MakeLink({ doc: b }, { doc: this.rootDoc }, 'video snapshot');
+ DocUtils.MakeLink(b, this.rootDoc, { link_relationship: 'video snapshot' });
Networking.PostToServer('/youtubeScreenshot', {
id: this.youtubeVideoId,
- timecode: this.layoutDoc._currentTimecode,
+ timecode: this.layoutDoc._layout_currentTimecode,
}).then(response => {
const resolved = response?.accessPaths?.agnostic?.client;
if (resolved) {
this.props.removeDocument?.(b);
- this.createRealSummaryLink(resolved);
+ this.createSnapshotLink(resolved);
}
});
} else {
@@ -351,48 +345,51 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
const dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png'
// if you want to preview the captured image,
const retitled = StrCast(this.rootDoc.title).replace(/[ -\.:]/g, '');
- const encodedFilename = encodeURIComponent('snapshot' + retitled + '_' + (this.layoutDoc._currentTimecode || 0).toString().replace(/\./, '_'));
+ const encodedFilename = encodeURIComponent('snapshot' + retitled + '_' + (this.layoutDoc._layout_currentTimecode || 0).toString().replace(/\./, '_'));
const filename = basename(encodedFilename);
- VideoBox.convertDataUri(dataUrl, filename).then((returnedFilename: string) => returnedFilename && (cb ?? this.createRealSummaryLink)(returnedFilename, downX, downY));
+ Utils.convertDataUri(dataUrl, filename).then((returnedFilename: string) => returnedFilename && (cb ?? this.createSnapshotLink)(returnedFilename, downX, downY));
}
};
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);
};
// creates link for snapshot
- createRealSummaryLink = (imagePath: string, downX?: number, downY?: number) => {
+ createSnapshotLink = (imagePath: string, downX?: number, downY?: number) => {
const url = !imagePath.startsWith('/') ? Utils.CorsProxy(imagePath) : imagePath;
const width = NumCast(this.layoutDoc._width) || 1;
const height = NumCast(this.layoutDoc._height);
- const imageSummary = Docs.Create.ImageDocument(url, {
+ const imageSnapshot = Docs.Create.ImageDocument(url, {
_nativeWidth: Doc.NativeWidth(this.layoutDoc),
_nativeHeight: Doc.NativeHeight(this.layoutDoc),
x: NumCast(this.layoutDoc.x) + width,
y: NumCast(this.layoutDoc.y),
- _isLinkButton: true,
+ onClick: FollowLinkScript(),
_width: 150,
_height: (height / width) * 150,
- title: '--snapshot' + NumCast(this.layoutDoc._currentTimecode) + ' image-',
+ title: '--snapshot' + NumCast(this.layoutDoc._layout_currentTimecode) + ' image-',
});
- Doc.SetNativeWidth(Doc.GetProto(imageSummary), Doc.NativeWidth(this.layoutDoc));
- Doc.SetNativeHeight(Doc.GetProto(imageSummary), Doc.NativeHeight(this.layoutDoc));
- this.props.addDocument?.(imageSummary);
- const link = DocUtils.MakeLink({ doc: imageSummary }, { doc: this.getAnchor() }, 'video snapshot');
- link && (Doc.GetProto(link.anchor2 as Doc).timecodeToHide = NumCast((link.anchor2 as Doc).timecodeToShow) + 3);
- setTimeout(() => downX !== undefined && downY !== undefined && DocumentManager.Instance.getFirstDocumentView(imageSummary)?.startDragging(downX, downY, 'move', true));
+ Doc.SetNativeWidth(Doc.GetProto(imageSnapshot), Doc.NativeWidth(this.layoutDoc));
+ Doc.SetNativeHeight(Doc.GetProto(imageSnapshot), Doc.NativeHeight(this.layoutDoc));
+ this.props.addDocument?.(imageSnapshot);
+ const link = DocUtils.MakeLink(imageSnapshot, this.getAnchor(true), { link_relationship: 'video snapshot' });
+ link && (Doc.GetProto(link.link_anchor_2 as Doc).timecodeToHide = NumCast((link.link_anchor_2 as Doc).timecodeToShow) + 3);
+ setTimeout(() => downX !== undefined && downY !== undefined && DocumentManager.Instance.getFirstDocumentView(imageSnapshot)?.startDragging(downX, downY, 'move', true));
};
- getAnchor = () => {
- const timecode = Cast(this.layoutDoc._currentTimecode, 'number', null);
- const marquee = AnchorMenu.Instance.GetAnchor?.();
- return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, '_timecodeToShow' /* videoStart */, '_timecodeToHide' /* videoEnd */, timecode ? timecode : undefined, undefined, marquee) || this.rootDoc;
+ getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
+ const timecode = Cast(this.layoutDoc._layout_currentTimecode, 'number', null);
+ const marquee = AnchorMenu.Instance.GetAnchor?.(undefined, addAsAnnotation);
+ if (!addAsAnnotation && marquee) marquee.backgroundColor = 'transparent';
+ const anchor = CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, timecode ? timecode : undefined, undefined, marquee, addAsAnnotation) || this.rootDoc;
+ PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), temporal: true } }, this.rootDoc);
+ return anchor;
};
// sets video info on load
@@ -405,25 +402,29 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
}
if (Number.isFinite(this.player!.duration)) {
this.rawDuration = this.player!.duration;
- this.dataDoc[this.fieldKey + '-duration'] = this.rawDuration;
- } else this.rawDuration = NumCast(this.dataDoc[this.fieldKey + '-duration']);
+ this.dataDoc[this.fieldKey + '_duration'] = this.rawDuration;
+ } else this.rawDuration = NumCast(this.dataDoc[this.fieldKey + '_duration']);
});
// updates video time
@action
updateTimecode = () => {
- this.player && (this.layoutDoc._currentTimecode = this.player.currentTime);
+ this.player && (this.layoutDoc._layout_currentTimecode = this.player.currentTime);
try {
- this._youtubePlayer && (this.layoutDoc._currentTimecode = this._youtubePlayer.getCurrentTime?.());
+ this._youtubePlayer && (this.layoutDoc._layout_currentTimecode = this._youtubePlayer.getCurrentTime?.());
} catch (e) {
console.log('Video Timecode Exception:', e);
}
};
+ // getView = async (doc: Doc) => {
+ // return new Promise<Opt<DocumentView>>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv)));
+ // };
+
// extracts video thumbnails and saves them as field of doc
getVideoThumbnails = () => {
- if (this.dataDoc.thumbnails !== undefined) return;
- this.dataDoc.thumbnails = new List<string>();
+ if (this.dataDoc[this.fieldKey + '_thumbnails'] !== undefined) return;
+ this.dataDoc[this.fieldKey + '_thumbnails'] = new List<string>();
const thumbnailPromises: Promise<any>[] = [];
const video = document.createElement('video');
@@ -436,12 +437,12 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
canvas.getContext('2d')?.drawImage(video, 0, 0, video.videoWidth, video.videoHeight, 0, 0, 100, 100);
const retitled = StrCast(this.rootDoc.title).replace(/[ -\.:]/g, '');
const encodedFilename = encodeURIComponent('thumbnail' + retitled + '_' + video.currentTime.toString().replace(/\./, '_'));
- thumbnailPromises?.push(VideoBox.convertDataUri(canvas.toDataURL(), basename(encodedFilename), true));
+ thumbnailPromises?.push(Utils.convertDataUri(canvas.toDataURL(), basename(encodedFilename), true));
const newTime = video.currentTime + video.duration / (VideoBox.numThumbnails - 1);
if (newTime < video.duration) {
video.currentTime = newTime;
} else {
- Promise.all(thumbnailPromises).then(thumbnails => (this.dataDoc.thumbnails = new List<string>(thumbnails)));
+ Promise.all(thumbnailPromises).then(thumbnails => (this.dataDoc[this.fieldKey + '_thumbnails'] = new List<string>(thumbnails)));
}
};
@@ -459,12 +460,12 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
// vref.onfullscreenchange = action((e) => this._fullScreen = vref.webkitDisplayingFullscreen);
this._disposers.reactionDisposer?.();
this._disposers.reactionDisposer = reaction(
- () => NumCast(this.layoutDoc._currentTimecode),
+ () => NumCast(this.layoutDoc._layout_currentTimecode),
time => !this._playing && (vref.currentTime = time),
{ fireImmediately: true }
);
- (!this.dataDoc.thumbnails || this.dataDoc.thumbnails.length != VideoBox.numThumbnails) && this.getVideoThumbnails();
+ (!this.dataDoc[this.fieldKey + '_thumbnails'] || StrListCast(this.dataDoc[this.fieldKey + '_thumbnails']).length != VideoBox.numThumbnails) && this.getVideoThumbnails();
}
};
@@ -530,7 +531,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
this.dataDoc.layout = RecordingBox.LayoutString(this.fieldKey);
// delete assoicated video data
this.dataDoc[this.props.fieldKey] = '';
- this.dataDoc[this.fieldKey + '-duration'] = '';
+ this.dataDoc[this.fieldKey + '_duration'] = '';
// delete assoicated presentation data
this.dataDoc[this.fieldKey + '-presentation'] = '';
},
@@ -572,7 +573,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
key="video"
autoPlay={this._screenCapture}
ref={this.setVideoRef}
- style={this._fullScreen ? this.fullScreenSize() : this.isCropped ? { width: 'max-content', height: 'max-content', transform: `scale(${1 / NumCast(this.rootDoc._viewScale)})`, transformOrigin: 'top left' } : {}}
+ style={this._fullScreen ? this.fullScreenSize() : this.isCropped ? { width: 'max-content', height: 'max-content', transform: `scale(${1 / NumCast(this.rootDoc._freeform_scale)})`, transformOrigin: 'top left' } : {}}
onCanPlay={this.videoLoad}
controls={VideoBox._nativeControls}
onPlay={() => this.Play()}
@@ -619,8 +620,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
this._disposers.reactionDisposer?.();
this._disposers.youtubeReactionDisposer?.();
this._disposers.reactionDisposer = reaction(
- () => this.layoutDoc._currentTimecode,
- () => !this._playing && this.Seek(NumCast(this.layoutDoc._currentTimecode))
+ () => this.layoutDoc._layout_currentTimecode,
+ () => !this._playing && this.Seek(NumCast(this.layoutDoc._layout_currentTimecode))
);
this._disposers.youtubeReactionDisposer = reaction(
() => Doc.ActiveTool === InkTool.None && this.props.isSelected(true) && !SnappingManager.GetIsDragging() && !DocumentDecorations.Instance.Interacting,
@@ -676,15 +677,15 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
this._clicking = false;
if (this.props.isContentActive()) {
// const local = this.props.ScreenToLocalTransform().scale(this.props.scaling?.() || 1).transformPoint(e.clientX, e.clientY);
- // this.layoutDoc._timelineHeightPercent = Math.max(0, Math.min(100, local[1] / this.props.PanelHeight() * 100));
+ // this.layoutDoc._layout_timelineHeightPercent = Math.max(0, Math.min(100, local[1] / this.props.PanelHeight() * 100));
- this.layoutDoc._timelineHeightPercent = 80;
+ this.layoutDoc._layout_timelineHeightPercent = 80;
}
return false;
}),
emptyFunction,
() => {
- this.layoutDoc._timelineHeightPercent = this.heightPercent !== 100 ? 100 : VideoBox.heightPercent;
+ this.layoutDoc._layout_timelineHeightPercent = this.heightPercent !== 100 ? 100 : VideoBox.heightPercent;
setTimeout(
action(() => (this._clicking = false)),
500
@@ -695,23 +696,24 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
);
};
- // removes video from currently playing display
+ // removes from currently playing display
@action
removeCurrentlyPlaying = () => {
- if (CollectionStackedTimeline.CurrentlyPlaying) {
- const index = CollectionStackedTimeline.CurrentlyPlaying.indexOf(this.layoutDoc);
+ const docView = this.props.DocumentView?.();
+ if (CollectionStackedTimeline.CurrentlyPlaying && docView) {
+ const index = CollectionStackedTimeline.CurrentlyPlaying.indexOf(docView);
index !== -1 && CollectionStackedTimeline.CurrentlyPlaying.splice(index, 1);
}
};
-
- // adds video to currently playing display
+ // adds doc to currently playing display
@action
addCurrentlyPlaying = () => {
+ const docView = this.props.DocumentView?.();
if (!CollectionStackedTimeline.CurrentlyPlaying) {
CollectionStackedTimeline.CurrentlyPlaying = [];
}
- if (CollectionStackedTimeline.CurrentlyPlaying.indexOf(this.layoutDoc) === -1) {
- CollectionStackedTimeline.CurrentlyPlaying.push(this.layoutDoc);
+ if (docView && CollectionStackedTimeline.CurrentlyPlaying.indexOf(docView) === -1) {
+ CollectionStackedTimeline.CurrentlyPlaying.push(docView);
}
};
@@ -719,7 +721,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
this._youtubeIframeId = VideoBox._youtubeIframeCounter++;
this._youtubeContentCreated = this._forceCreateYouTubeIFrame ? true : true;
const classname = 'videoBox-content-YouTube' + (this._fullScreen ? '-fullScreen' : '');
- const start = untracked(() => Math.round(NumCast(this.layoutDoc._currentTimecode)));
+ const start = untracked(() => Math.round(NumCast(this.layoutDoc._layout_currentTimecode)));
return (
<iframe
key={this._youtubeIframeId}
@@ -738,7 +740,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
@action.bound
addDocWithTimecode(doc: Doc | Doc[]): boolean {
const docs = doc instanceof Doc ? [doc] : doc;
- const curTime = NumCast(this.layoutDoc._currentTimecode);
+ const curTime = NumCast(this.layoutDoc._layout_currentTimecode);
docs.forEach(doc => (doc._timecodeToHide = (doc._timecodeToShow = curTime) + 1));
return this.addDocument(doc);
}
@@ -857,7 +859,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
// starts marquee selection
marqueeDown = (e: React.PointerEvent) => {
- if (!e.altKey && e.button === 0 && NumCast(this.layoutDoc._viewScale, 1) === 1 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen].includes(Doc.ActiveTool)) {
+ if (!e.altKey && e.button === 0 && NumCast(this.layoutDoc._freeform_scale, 1) === 1 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen].includes(Doc.ActiveTool)) {
setupMoveUpEvents(
this,
e,
@@ -868,6 +870,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
}),
returnFalse,
() => MarqueeAnnotator.clearAnnotations(this._savedAnnotations),
+ false,
false
);
}
@@ -888,18 +891,16 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
.scale(this.scaling())
.translate(0, (-this.heightPercent / 100) * this.props.PanelHeight());
- setPlayheadTime = (time: number) => (this.player!.currentTime = this.layoutDoc._currentTimecode = time);
+ setPlayheadTime = (time: number) => (this.player!.currentTime = this.layoutDoc._layout_currentTimecode = time);
timelineHeight = () => (this.props.PanelHeight() * (100 - this.heightPercent)) / 100;
playing = () => this._playing;
- contentFunc = () => [this.youtubeVideoId ? this.youtubeContent : this.content];
-
scaling = () => this.props.NativeDimScaling?.() || 1;
panelWidth = () => (this.props.PanelWidth() * this.heightPercent) / 100;
- panelHeight = () => (this.layoutDoc._fitWidth ? this.panelWidth() / (Doc.NativeAspect(this.rootDoc) || 1) : (this.props.PanelHeight() * this.heightPercent) / 100);
+ panelHeight = () => (this.layoutDoc._layout_fitWidth ? this.panelWidth() / (Doc.NativeAspect(this.rootDoc) || 1) : (this.props.PanelHeight() * this.heightPercent) / 100);
screenToLocalTransform = () => {
const offset = (this.props.PanelWidth() - this.panelWidth()) / 2 / this.scaling();
@@ -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) => {
@@ -949,29 +950,22 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
);
};
- scrollFocus = (doc: Doc, smooth: boolean) => {
- if (doc !== this.rootDoc) {
- const showTime = Cast(doc._timecodeToShow, 'number', null);
- showTime !== undefined && setTimeout(() => this.Seek(showTime), 100);
- return 0.1;
- }
- };
-
// renders CollectionStackedTimeline
@computed get renderTimeline() {
return (
- <div className="videoBox-stackPanel" style={{ transition: this.transition, height: `${100 - this.heightPercent}%` }}>
+ <div className="videoBox-stackPanel" style={{ transition: this.transition, height: `${100 - this.heightPercent}%`, display: this.heightPercent === 100 ? 'none' : '' }}>
<CollectionStackedTimeline
ref={action((r: any) => (this._stackedTimeline = r))}
{...this.props}
+ dataFieldKey={this.fieldKey}
fieldKey={this.annotationKey}
dictationKey={this.fieldKey + '-dictation'}
mediaPath={this.audiopath}
+ thumbnails={() => StrListCast(this.dataDoc[this.fieldKey + '_thumbnails'])}
renderDepth={this.props.renderDepth + 1}
startTag={'_timecodeToShow' /* videoStart */}
endTag={'_timecodeToHide' /* videoEnd */}
bringToFront={emptyFunction}
- CollectionView={undefined}
playFrom={this.playFrom}
setTime={this.setPlayheadTime}
playing={this.playing}
@@ -1005,14 +999,14 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
Doc.GetProto(region).backgroundColor = 'transparent';
Doc.GetProto(region).lockedPosition = true;
Doc.GetProto(region).title = 'region:' + this.rootDoc.title;
- Doc.GetProto(region).isPushpin = true;
+ Doc.GetProto(region).followLinkToggle = true;
region._timecodeToHide = NumCast(region._timecodeToShow) + 0.0001;
this.addDocument(region);
const anchx = NumCast(cropping.x);
const anchy = NumCast(cropping.y);
const anchw = NumCast(cropping._width);
const anchh = NumCast(cropping._height);
- const viewScale = NumCast(this.rootDoc[this.fieldKey + '-nativeWidth']) / anchw;
+ const viewScale = NumCast(this.rootDoc[this.fieldKey + '_nativeWidth']) / anchw;
cropping.title = 'crop: ' + this.rootDoc.title;
cropping.x = NumCast(this.rootDoc.x) + NumCast(this.rootDoc._width);
cropping.y = NumCast(this.rootDoc.y);
@@ -1020,28 +1014,28 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
cropping._height = anchh * (this.props.NativeDimScaling?.() || 1);
cropping.timecodeToHide = undefined;
cropping.timecodeToShow = undefined;
- cropping.isLinkButton = undefined;
+ cropping.onClick = undefined;
const croppingProto = Doc.GetProto(cropping);
croppingProto.annotationOn = undefined;
- croppingProto.isPrototype = true;
+ croppingProto.isDataDoc = true;
croppingProto.proto = Cast(this.rootDoc.proto, Doc, null)?.proto; // set proto of cropping's data doc to be IMAGE_PROTO
croppingProto.type = DocumentType.VID;
croppingProto.layout = VideoBox.LayoutString('data');
croppingProto.data = ObjectField.MakeCopy(this.rootDoc[this.fieldKey] as ObjectField);
- croppingProto['data-nativeWidth'] = anchw;
- croppingProto['data-nativeHeight'] = anchh;
+ croppingProto['data_nativeWidth'] = anchw;
+ croppingProto['data_nativeHeight'] = anchh;
croppingProto.videoCrop = true;
- croppingProto.currentTimecode = this.layoutDoc._currentTimecode;
- croppingProto.viewScale = viewScale;
- croppingProto.viewScaleMin = viewScale;
- croppingProto.panX = anchx / viewScale;
- croppingProto.panY = anchy / viewScale;
- croppingProto.panXMin = anchx / viewScale;
- croppingProto.panXMax = anchw / viewScale;
- croppingProto.panYMin = anchy / viewScale;
- croppingProto.panYMax = anchh / viewScale;
+ croppingProto.layout_currentTimecode = this.layoutDoc._layout_currentTimecode;
+ croppingProto.freeform_scale = viewScale;
+ croppingProto.freeform_scale_min = viewScale;
+ croppingProto.freeform_ = anchx / viewScale;
+ croppingProto.freeform_panY = anchy / 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({ doc: region }, { doc: cropping }, 'cropped image', '');
+ DocUtils.MakeLink(region, cropping, { link_relationship: 'cropped image' });
}
this.props.bringToFront(cropping);
return cropping;
@@ -1059,7 +1053,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
style={{
pointerEvents: this.layoutDoc._lockedPosition ? 'none' : undefined,
borderRadius,
- overflow: this.props.docViewPath?.().slice(-1)[0].fitWidth ? 'auto' : undefined,
+ overflow: this.props.docViewPath?.().slice(-1)[0].layout_fitWidth ? 'auto' : undefined,
}}
onWheel={e => {
e.stopPropagation();
@@ -1076,23 +1070,27 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
left: (this.props.PanelWidth() - this.panelWidth()) / 2,
}}>
<CollectionFreeFormView
- {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
+ {...this.props}
+ setContentView={emptyFunction}
+ NativeWidth={returnZero}
+ NativeHeight={returnZero}
renderDepth={this.props.renderDepth + 1}
fieldKey={this.annotationKey}
- CollectionView={undefined}
isAnnotationOverlay={true}
annotationLayerHostsContent={true}
PanelWidth={this.panelWidth}
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}
moveDocument={this.moveDocument}
addDocument={this.addDocWithTimecode}>
- {this.contentFunc}
+ {this.youtubeVideoId ? this.youtubeContent : this.content}
</CollectionFreeFormView>
</div>
{this.annotationLayer}
@@ -1122,7 +1120,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
@computed get UIButtons() {
const bounds = this.props.docViewPath().lastElement().getBounds();
const width = (bounds?.right || 0) - (bounds?.left || 0);
- const curTime = NumCast(this.layoutDoc._currentTimecode) - (this.timeline?.clipStart || 0);
+ const curTime = NumCast(this.layoutDoc._layout_currentTimecode) - (this.timeline?.clipStart || 0);
return (
<>
<div className="videobox-button" title={this._playing ? 'play' : 'pause'} onPointerDown={this.onPlayDown}>
diff --git a/src/client/views/nodes/WebBox.scss b/src/client/views/nodes/WebBox.scss
index a41f66ef0..75847c100 100644
--- a/src/client/views/nodes/WebBox.scss
+++ b/src/client/views/nodes/WebBox.scss
@@ -7,6 +7,7 @@
left: 0;
position: relative;
display: flex;
+ overflow: hidden;
.webBox-sideResizer {
position: absolute;
@@ -182,6 +183,7 @@
height: 100%;
position: absolute;
top: 0;
+ left: 0;
body {
::selection {
color: white;
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 5ce6a0eb1..34a1229ba 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -1,18 +1,22 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction, trace } from 'mobx';
+import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as WebRequest from 'web-request';
-import { Doc, DocListCast, 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 } from '../../../fields/Types';
+import { Cast, ImageCast, NumCast, StrCast, WebCast } from '../../../fields/Types';
import { ImageField, WebField } from '../../../fields/URLField';
import { TraceMobx } from '../../../fields/util';
-import { emptyFunction, getWordAtPoint, OmitKeys, returnEmptyString, returnFalse, returnOne, 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';
import { ScriptingGlobals } from '../../util/ScriptingGlobals';
import { SnappingManager } from '../../util/SnappingManager';
import { undoBatch, UndoManager } from '../../util/UndoManager';
@@ -21,21 +25,20 @@ import { CollectionFreeFormView } from '../collections/collectionFreeForm/Collec
import { ContextMenu } from '../ContextMenu';
import { ContextMenuProps } from '../ContextMenuItem';
import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent';
-import { DocumentDecorations } from '../DocumentDecorations';
import { Colors } from '../global/globalEnums';
import { LightboxView } from '../LightboxView';
import { MarqueeAnnotator } from '../MarqueeAnnotator';
import { AnchorMenu } from '../pdf/AnchorMenu';
import { Annotation } from '../pdf/Annotation';
+import { GPTPopup } from '../pdf/GPTPopup/GPTPopup';
import { SidebarAnnos } from '../SidebarAnnos';
import { StyleProp } from '../StyleProvider';
-import { DocumentViewProps } from './DocumentView';
+import { DocComponentView, DocFocusOptions, DocumentView, DocumentViewProps, OpenWhere } from './DocumentView';
import { FieldView, FieldViewProps } from './FieldView';
import { LinkDocPreview } from './LinkDocPreview';
-import { VideoBox } from './VideoBox';
+import { PinProps, PresBox } from './trails';
import './WebBox.scss';
import React = require('react');
-import { DragManager } from '../../util/DragManager';
const { CreateImage } = require('./WebBoxRenderer');
const _global = (window /* browser */ || global) /* node */ as any;
const htmlToText = require('html-to-text');
@@ -46,6 +49,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
}
public static openSidebarWidth = 250;
public static sidebarResizerWidth = 5;
+ static webStyleSheet = addStyleSheet();
private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean) => void);
private _setBrushViewer: undefined | ((view: { width: number; height: number; panX: number; panY: number }) => void);
private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
@@ -53,16 +57,19 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
private _disposers: { [name: string]: IReactionDisposer } = {};
private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
private _keyInput = React.createRef<HTMLInputElement>();
- private _initialScroll: Opt<number> = NumCast(this.layoutDoc.thumbScrollTop, NumCast(this.layoutDoc.scrollTop));
- private _getAnchor: (savedAnnotations?: ObservableMap<number, HTMLDivElement[]>) => Opt<Doc> = () => undefined;
+ private _initialScroll: Opt<number> = NumCast(this.layoutDoc.thumbScrollTop, NumCast(this.layoutDoc.layout_scrollTop));
private _sidebarRef = React.createRef<SidebarAnnos>();
private _searchRef = React.createRef<HTMLInputElement>();
private _searchString = '';
- @observable private _webUrl = ''; // url of the src parameter of the embedded iframe but not necessarily the rendered page - eg, when following a link, the rendered page changes but we don't wan the src parameter to also change as that would cause an unnecessary re-render.
+ private _scrollTimer: any;
+
+ private get _getAnchor() {
+ return AnchorMenu.Instance?.GetAnchor;
+ }
+ @observable private _webUrl = ''; // url of the src parameter of the embedded iframe but not necessarily the rendered page - eg, when following a link, the rendered page changes but we don't want the src parameter to also change as that would cause an unnecessary re-render.
@observable private _hackHide = false; // apparently changing the value of the 'sandbox' prop doesn't necessarily apply it to the active iframe. so thisforces the ifrmae to be rebuilt when allowScripts is toggled
@observable private _searching: boolean = false;
@observable private _showSidebar = false;
- @observable private _scrollTimer: any;
@observable private _webPageHasBeenRendered = false;
@observable private _overlayAnnoInfo: Opt<Doc>;
@observable private _marqueeing: number[] | undefined;
@@ -78,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]);
@@ -87,7 +94,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
return this.allAnnotations.filter(a => a.textInlineAnnotations);
}
@computed get webField() {
- return Cast(this.dataDoc[this.props.fieldKey], WebField)?.url;
+ return Cast(this.rootDoc[this.props.fieldKey], WebField)?.url;
}
@computed get webThumb() {
return (
@@ -95,7 +102,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
ImageCast(
this.layoutDoc['thumb-frozen'],
ImageCast(
- this.layoutDoc.thumbScrollTop === this.layoutDoc._scrollTop && this.layoutDoc.thumbNativeWidth === NumCast(this.layoutDoc.nativeWidth) && this.layoutDoc.thumbNativeHeight === NumCast(this.layoutDoc.nativeHeight)
+ this.layoutDoc.thumbScrollTop === this.layoutDoc._layout_scrollTop && this.layoutDoc.thumbNativeWidth === NumCast(this.layoutDoc.nativeWidth) && this.layoutDoc.thumbNativeHeight === NumCast(this.layoutDoc.nativeHeight)
? this.layoutDoc.thumb
: undefined
)
@@ -137,81 +144,65 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
};
updateThumb = async () => {
- const imageBitmap = ImageCast(this.layoutDoc['thumb-frozen'])?.url.href;
- const scrollTop = NumCast(this.layoutDoc._scrollTop);
+ if (!this._iframe) return;
+ const scrollTop = NumCast(this.layoutDoc._layout_scrollTop);
const nativeWidth = NumCast(this.layoutDoc.nativeWidth);
const nativeHeight = (nativeWidth * this.props.PanelHeight()) / this.props.PanelWidth();
- if (
- !this.rootDoc.thumbLockout &&
- !this.props.dontRegisterView &&
- this._iframe &&
- !imageBitmap &&
- (scrollTop !== this.layoutDoc.thumbScrollTop || nativeWidth !== this.layoutDoc.thumbNativeWidth || nativeHeight !== this.layoutDoc.thumbNativeHeight)
- ) {
- var htmlString = this._iframe.contentDocument && new XMLSerializer().serializeToString(this._iframe.contentDocument);
- if (!htmlString) {
- htmlString = await (await fetch(Utils.CorsProxy(this.webField!.href))).text();
- }
- this.layoutDoc.thumb = undefined;
- this.rootDoc.thumbLockout = true; // lock to prevent multiple thumb updates.
- CreateImage(this._webUrl.endsWith('/') ? this._webUrl.substring(0, this._webUrl.length - 1) : this._webUrl, this._iframe.contentDocument?.styleSheets ?? [], htmlString, nativeWidth, nativeHeight, scrollTop)
- .then((data_url: any) => {
- if (data_url.includes('<!DOCTYPE')) {
- console.log('BAD DATA IN THUMB CREATION');
- return;
- }
- VideoBox.convertDataUri(data_url, this.layoutDoc[Id] + '-icon' + new Date().getTime(), true, this.layoutDoc[Id] + '-icon').then(returnedfilename =>
- setTimeout(
- action(() => {
- this.rootDoc.thumbLockout = false;
- this.layoutDoc.thumb = new ImageField(returnedfilename);
- this.layoutDoc.thumbScrollTop = scrollTop;
- this.layoutDoc.thumbNativeWidth = nativeWidth;
- this.layoutDoc.thumbNativeHeight = nativeHeight;
- }),
- 500
- )
- );
- })
- .catch(function (error: any) {
- console.error('oops, something went wrong!', error);
- });
+ var htmlString = this._iframe.contentDocument && new XMLSerializer().serializeToString(this._iframe.contentDocument);
+ if (!htmlString) {
+ htmlString = await (await fetch(Utils.CorsProxy(this.webField!.href))).text();
}
+ this.layoutDoc.thumb = undefined;
+ this.rootDoc.thumbLockout = true; // lock to prevent multiple thumb updates.
+ CreateImage(this._webUrl.endsWith('/') ? this._webUrl.substring(0, this._webUrl.length - 1) : this._webUrl, this._iframe.contentDocument?.styleSheets ?? [], htmlString, nativeWidth, nativeHeight, scrollTop)
+ .then((data_url: any) => {
+ if (data_url.includes('<!DOCTYPE')) {
+ console.log('BAD DATA IN THUMB CREATION');
+ return;
+ }
+ Utils.convertDataUri(data_url, this.layoutDoc[Id] + '-icon' + new Date().getTime(), true, this.layoutDoc[Id] + '-icon').then(returnedfilename =>
+ setTimeout(
+ action(() => {
+ this.rootDoc.thumbLockout = false;
+ this.layoutDoc.thumb = new ImageField(returnedfilename);
+ this.layoutDoc.thumbScrollTop = scrollTop;
+ this.layoutDoc.thumbNativeWidth = nativeWidth;
+ this.layoutDoc.thumbNativeHeight = nativeHeight;
+ }),
+ 500
+ )
+ );
+ })
+ .catch(function (error: any) {
+ console.error('oops, something went wrong!', error);
+ });
};
+
async componentDidMount() {
this.props.setContentView?.(this); // this tells the DocumentView that this WebBox is the "content" of the document. this allows the DocumentView to call WebBox relevant methods to configure the UI (eg, show back/forward buttons)
runInAction(() => {
- this._annotationKeySuffix = () => this._urlHash + '-annotations';
+ this._annotationKeySuffix = () => this._urlHash + '_annotations';
const reqdFuncs: { [key: string]: string } = {};
- // bcz: need to make sure that doc.data-annotations points to the currently active web page's annotations (this could/should be when the doc is created)
- reqdFuncs[this.fieldKey + '-annotations'] = `copyField(this["${this.fieldKey}-"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"-annotations"`;
- reqdFuncs[this.fieldKey + '-sidebar'] = `copyField(this["${this.fieldKey}-"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"-sidebar"`;
+ // bcz: need to make sure that doc.data_annotations points to the currently active web page's annotations (this could/should be when the doc is created)
+ reqdFuncs[this.fieldKey + '_annotations'] = `copyField(this["${this.fieldKey}_"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"_annotations"])`;
+ reqdFuncs[this.fieldKey + '_annotations-setter'] = `this["${this.fieldKey}_"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"_annotations"] = value`;
+ reqdFuncs[this.fieldKey + '_sidebar'] = `copyField(this["${this.fieldKey}_"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"_sidebar"])`;
DocUtils.AssignScripts(this.dataDoc, {}, reqdFuncs);
});
- reaction(
- () => this.props.isSelected(true) || this.isAnyChildContentActive() || Doc.isBrushedHighlightedDegree(this.props.Document),
- async selected => {
- if (selected) {
- this._webPageHasBeenRendered = true;
- } else if (
- (!this.props.isContentActive(true) || SnappingManager.GetIsDragging()) && // update thumnail when unselected AND (no child annotation is active OR we've started dragging the document in which case no additional deselect will occur so this is the only chance to update the thumbnail)
- !this.props.docViewPath().lastElement()?.docView?._pendingDoubleClick && // don't create a thumbnail when double-clicking to enter lightbox because thumbnail will be empty
- LightboxView.LightboxDoc !== this.rootDoc
- ) {
- // don't create a thumbnail if entering Lightbox from maximize either, since thumb will be empty.
- this.updateThumb();
- }
- },
- { fireImmediately: this.props.isSelected(true) || this.isAnyChildContentActive() || (Doc.isBrushedHighlightedDegreeUnmemoized(this.props.Document) ? true : false) }
+ this._disposers.urlchange = reaction(
+ () => WebCast(this.rootDoc.data),
+ url => {
+ this.submitURL(url.url.href, false, false);
+ }
);
- this._disposers.autoHeight = reaction(
- () => this.layoutDoc._autoHeight,
- autoHeight => {
- if (autoHeight) {
- this.layoutDoc._nativeHeight = NumCast(this.props.Document[this.props.fieldKey + '-nativeHeight']);
- this.props.setHeight?.(NumCast(this.props.Document[this.props.fieldKey + '-nativeHeight']) * (this.props.NativeDimScaling?.() || 1));
+ this._disposers.layout_autoHeight = reaction(
+ () => this.layoutDoc._layout_autoHeight,
+ layout_autoHeight => {
+ if (layout_autoHeight) {
+ this.layoutDoc._nativeHeight = NumCast(this.props.Document[this.props.fieldKey + '_nativeHeight']);
+ this.props.setHeight?.(NumCast(this.props.Document[this.props.fieldKey + '_nativeHeight']) * (this.props.NativeDimScaling?.() || 1));
}
}
);
@@ -224,7 +215,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) {
@@ -235,13 +226,13 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
}
this._disposers.scrollReaction = reaction(
- () => NumCast(this.layoutDoc._scrollTop),
+ () => NumCast(this.layoutDoc._layout_scrollTop),
scrollTop => {
const viewTrans = StrCast(this.Document._viewTransition);
const durationMiliStr = viewTrans.match(/([0-9]*)ms/);
const durationSecStr = viewTrans.match(/([0-9.]*)s/);
const duration = durationMiliStr ? Number(durationMiliStr[1]) : durationSecStr ? Number(durationSecStr[1]) * 1000 : 0;
- this.goTo(scrollTop, duration);
+ this.goTo(scrollTop, duration, 'ease');
},
{ fireImmediately: true }
);
@@ -254,6 +245,10 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
// this._iframe?.contentDocument?.removeEventListener("pointerup", this.iframeUp);
}
+ private _selectionText: string = '';
+ private _selectionContent: DocumentFragment | undefined;
+ selectionText = () => this._selectionText;
+ selectionContent = () => this._selectionContent;
@action
createTextAnnotation = (sel: Selection, selRange: Range | undefined) => {
if (this._mainCont.current && selRange) {
@@ -273,6 +268,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
}
}
//this._selectionText = selRange.cloneContents().textContent || "";
+ this._selectionContent = selRange?.cloneContents();
+ this._selectionText = this._selectionContent?.textContent || '';
// clear selection
if (sel.empty) sel.empty(); // Chrome
@@ -280,38 +277,61 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
return this._savedAnnotations;
};
- menuControls = () => this.urlEditor; // controls to be added to the top bar when a document of this type is selected
-
setBrushViewer = (func?: (view: { width: number; height: number; panX: number; panY: number }) => void) => (this._setBrushViewer = func);
brushView = (view: { width: number; height: number; panX: number; panY: number }) => this._setBrushViewer?.(view);
- scrollFocus = (doc: Doc, smooth: boolean) => {
- if (StrCast(doc.webUrl) !== this._url) this.submitURL(StrCast(doc.webUrl), !smooth);
- if (DocListCast(this.props.Document[this.fieldKey + '-sidebar']).includes(doc) && !this.SidebarShown) {
- this.toggleSidebar(!smooth);
- }
- if (this._sidebarRef?.current?.makeDocUnfiltered(doc)) return 1;
- if (doc !== this.rootDoc && this._outerRef.current) {
+ 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(doc.y), doc[HeightSym](), NumCast(this.layoutDoc._scrollTop), windowHeight, windowHeight * 0.1, Math.max(NumCast(doc.y) + doc[HeightSym](), this.getScrollHeight()));
- if (scrollTo !== undefined && this._initialScroll === undefined) {
- const focusSpeed = smooth ? NumCast(doc.focusSpeed, 500) : 0;
- this.goTo(scrollTo, focusSpeed);
- return focusSpeed;
- } else if (!this._webPageHasBeenRendered || !this.getScrollHeight() || this._initialScroll !== undefined) {
- this._initialScroll = scrollTo;
+ 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;
+ this.goTo(scrollTo, focusTime, options.easeFunc);
+ return focusTime;
+ } else {
+ this._initialScroll = scrollTo;
+ }
}
}
- return undefined;
};
- getAnchor = () => {
+ @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();
+ 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)));
+ };
+
+ sidebarAddDocTab = (doc: Doc, where: OpenWhere) => {
+ if (DocListCast(this.props.Document[this.props.fieldKey + '_sidebar']).includes(doc) && !this.SidebarShown) {
+ this.toggleSidebar(false);
+ return true;
+ }
+ return this.props.addDocTab(doc, where);
+ };
+ getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
+ let ele: Opt<HTMLDivElement> = undefined;
+ try {
+ const contents = this._iframe?.contentWindow?.getSelection()?.getRangeAt(0).cloneContents();
+ if (contents) {
+ ele = document.createElement('div');
+ ele.append(contents);
+ }
+ } catch (e) {}
const anchor =
- this._getAnchor(this._savedAnnotations) ??
- Docs.Create.WebanchorDocument(this._url, {
- title: StrCast(this.rootDoc.title + ' ' + this.layoutDoc._scrollTop),
- y: NumCast(this.layoutDoc._scrollTop),
- unrendered: true,
+ this._getAnchor(this._savedAnnotations, false) ??
+ Docs.Create.ConfigDocument({
+ title: StrCast(this.rootDoc.title + ' ' + this.layoutDoc._layout_scrollTop),
+ y: NumCast(this.layoutDoc._layout_scrollTop),
+ annotationOn: this.rootDoc,
});
+ PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), scrollable: pinProps?.pinData ? true : false, pannable: true } }, this.rootDoc);
+ anchor.text = ele?.textContent ?? '';
+ anchor.textHtml = ele?.innerHTML;
+ //addAsAnnotation &&
this.addDocumentWrapper(anchor);
return anchor;
};
@@ -328,8 +348,13 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
const scale = (this.props.NativeDimScaling?.() || 1) * mainContBounds.scale;
const sel = this._iframe.contentWindow.getSelection();
if (sel) {
+ this._selectionText = sel.toString();
+ AnchorMenu.Instance.setSelectedText(sel.toString());
this._textAnnotationCreator = () => this.createTextAnnotation(sel, !sel.isCollapsed ? sel.getRangeAt(0) : undefined);
- AnchorMenu.Instance.jumpTo(e.clientX * scale + mainContBounds.translateX, e.clientY * scale + mainContBounds.translateY - NumCast(this.layoutDoc._scrollTop) * scale);
+ AnchorMenu.Instance.jumpTo(e.clientX * scale + mainContBounds.translateX, e.clientY * scale + mainContBounds.translateY - NumCast(this.layoutDoc._layout_scrollTop) * scale);
+ // Changing which document to add the annotation to (the currently selected WebBox)
+ GPTPopup.Instance.setSidebarId(`${this.props.fieldKey}_${this._urlHash}_sidebar`);
+ GPTPopup.Instance.addDoc = this.sidebarAddDocument;
}
}
};
@@ -341,7 +366,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
const word = getWordAtPoint(e.target, e.clientX, e.clientY);
this._setPreviewCursor?.(e.clientX, e.clientY, false, true);
MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
- e.button !== 2 && (this._marqueeing = [e.clientX * scale + mainContBounds.translateX, e.clientY * scale + mainContBounds.translateY - NumCast(this.layoutDoc._scrollTop) * scale]);
+ e.button !== 2 && (this._marqueeing = [e.clientX * scale + mainContBounds.translateX, e.clientY * scale + mainContBounds.translateY - NumCast(this.layoutDoc._layout_scrollTop) * scale]);
if (word || ((e.target as any) || '').className.includes('rangeslider') || (e.target as any)?.onclick || (e.target as any)?.parentNode?.onclick) {
setTimeout(
action(() => (this._marqueeing = undefined)),
@@ -363,16 +388,13 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
ContextMenu.Instance.setIgnoreEvents(true);
}
};
-
- getScrollHeight = () => this._scrollHeight;
-
isFirefox = () => {
return 'InstallTrigger' in window; // navigator.userAgent.indexOf("Chrome") !== -1;
};
iframeClick = () => this._iframeClick;
iframeScaling = () => 1 / this.props.ScreenToLocalTransform().Scale;
- addStyleSheet(document: any, styleType: string = 'text/css') {
+ addWebStyleSheet(document: any, styleType: string = 'text/css') {
if (document) {
const style = document.createElement('style');
style.type = styleType;
@@ -380,7 +402,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
return (sheets as any).sheet;
}
}
- addStyleSheetRule(sheet: any, selector: any, css: any, selectorPrefix = '.') {
+ addWebStyleSheetRule(sheet: any, selector: any, css: any, selectorPrefix = '.') {
const propText =
typeof css === 'string'
? css
@@ -397,8 +419,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
if (this._initialScroll !== undefined) {
this.setScrollPos(this._initialScroll);
}
-
- this.addStyleSheetRule(this.addStyleSheet(this._iframe?.contentDocument), '::selection', { color: 'white', background: 'orange' }, '');
+ this._scrollHeight = this._iframe?.contentDocument?.body?.scrollHeight ?? 0;
+ this.addWebStyleSheetRule(this.addWebStyleSheet(this._iframe?.contentDocument), '::selection', { color: 'white', background: 'orange' }, '');
let href: Opt<string>;
try {
@@ -422,17 +444,24 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
.replace('search&', 'search?')
.replace('?gbv=1', '');
}
- this.submitURL(requrlraw, undefined, true);
+ this.setData(requrlraw);
}
const iframeContent = iframe?.contentDocument;
if (iframeContent) {
iframeContent.addEventListener('pointerup', this.iframeUp);
iframeContent.addEventListener('pointerdown', this.iframeDown);
+ // iframeContent.addEventListener(
+ // 'wheel',
+ // e => {
+ // e.ctrlKey && e.preventDefault();
+ // },
+ // { passive: false }
+ // );
const initHeights = () => {
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();
@@ -441,7 +470,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
action(() => initHeights),
5000
);
- iframe.setAttribute('enable-annotation', 'true');
iframeContent.addEventListener(
'click',
undoBatch(
@@ -452,56 +480,70 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
}
const origin = this.webField?.origin;
if (href && origin) {
+ const batch = UndoManager.StartBatch('webclick');
e.stopPropagation();
- setTimeout(() => this.submitURL(href.replace(Utils.prepend(''), origin)));
+ setTimeout(() => {
+ this.setData(href.replace(Utils.prepend(''), origin));
+ batch.end();
+ });
if (this._outerRef.current) {
- this._outerRef.current.scrollTop = NumCast(this.layoutDoc._scrollTop);
+ this._outerRef.current.scrollTop = NumCast(this.layoutDoc._layout_scrollTop);
this._outerRef.current.scrollLeft = 0;
}
}
})
)
);
- iframe.contentDocument.addEventListener('wheel', this.iframeWheel, false);
- //iframe.contentDocument.addEventListener('scroll', () => !this.active() && this._iframe && (this._iframe.scrollTop = NumCast(this.layoutDoc._scrollTop), false));
+ iframe.contentDocument.addEventListener('wheel', this.iframeWheel, { passive: false });
}
};
@action
- iframeWheel = (e: any) => {
+ iframeWheel = (e: WheelEvent) => {
if (!this._scrollTimer) {
- this._scrollTimer = setTimeout(
- action(() => (this._scrollTimer = undefined)),
- 250
- ); // this turns events off on the iframe which allows scrolling to change direction smoothly
+ addStyleSheetRule(WebBox.webStyleSheet, 'webBox-iframe', { 'pointer-events': 'none' });
+ this._scrollTimer = setTimeout(() => {
+ this._scrollTimer = undefined;
+ clearStyleSheetRules(WebBox.webStyleSheet);
+ }, 250); // this turns events off on the iframe which allows scrolling to change direction smoothly
+ }
+ if (e.ctrlKey) {
+ if (this._innerCollectionView) {
+ this._innerCollectionView.zoom(e.screenX, e.screenY, e.deltaY);
+ const offset = e.clientY - NumCast(this.layoutDoc._layout_scrollTop);
+ 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();
}
};
@action
setDashScrollTop = (scrollTop: number, timeout: number = 250) => {
const iframeHeight = Math.max(scrollTop, this._scrollHeight - this.panelHeight());
- this._scrollTimer && clearTimeout(this._scrollTimer);
- this._scrollTimer = setTimeout(
- action(() => {
- this._scrollTimer = undefined;
- const newScrollTop = scrollTop > iframeHeight ? iframeHeight : scrollTop;
- if (!LinkDocPreview.LinkInfo && this._outerRef.current && newScrollTop !== this.layoutDoc.thumbScrollTop && (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath()))) {
- this.layoutDoc.thumb = undefined;
- this.layoutDoc.thumbScrollTop = undefined;
- this.layoutDoc.thumbNativeWidth = undefined;
- this.layoutDoc.thumbNativeHeight = undefined;
- this.layoutDoc.scrollTop = this._outerRef.current.scrollTop = newScrollTop;
- } else if (this._outerRef.current) this._outerRef.current.scrollTop = newScrollTop;
- }),
- timeout
- );
+ if (this._scrollTimer) {
+ clearTimeout(this._scrollTimer);
+ clearStyleSheetRules(WebBox.webStyleSheet);
+ }
+ addStyleSheetRule(WebBox.webStyleSheet, 'webBox-iframe', { 'pointer-events': 'none' });
+ this._scrollTimer = setTimeout(() => {
+ clearStyleSheetRules(WebBox.webStyleSheet);
+ this._scrollTimer = undefined;
+ const newScrollTop = scrollTop > iframeHeight ? iframeHeight : scrollTop;
+ if (!LinkDocPreview.LinkInfo && this._outerRef.current && newScrollTop !== this.layoutDoc.thumbScrollTop && (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath()))) {
+ this.layoutDoc.thumb = undefined;
+ this.layoutDoc.thumbScrollTop = undefined;
+ this.layoutDoc.thumbNativeWidth = undefined;
+ this.layoutDoc.thumbNativeHeight = undefined;
+ this.layoutDoc.layout_scrollTop = this._outerRef.current.scrollTop = newScrollTop;
+ } else if (this._outerRef.current) this._outerRef.current.scrollTop = newScrollTop;
+ }, timeout);
};
- goTo = (scrollTop: number, duration: number) => {
+ goTo = (scrollTop: number, duration: number, easeFunc: 'linear' | 'ease' | undefined) => {
if (this._outerRef.current) {
const iframeHeight = Math.max(scrollTop, this._scrollHeight - this.panelHeight());
if (duration) {
- smoothScroll(duration, [this._outerRef.current], scrollTop);
+ smoothScroll(duration, [this._outerRef.current], scrollTop, easeFunc);
this.setDashScrollTop(scrollTop, duration);
} else {
this.setDashScrollTop(scrollTop);
@@ -510,14 +552,15 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
};
forward = (checkAvailable?: boolean) => {
- const future = Cast(this.dataDoc[this.fieldKey + '-future'], listSpec('string'), []);
- const history = Cast(this.dataDoc[this.fieldKey + '-history'], listSpec('string'), []);
+ const future = Cast(this.rootDoc[this.fieldKey + '_future'], listSpec('string'), []);
+ const history = Cast(this.rootDoc[this.fieldKey + '_history'], listSpec('string'), []);
if (checkAvailable) return future.length;
runInAction(() => {
if (future.length) {
const curUrl = this._url;
- this.dataDoc[this.fieldKey + '-history'] = new List<string>([...history, this._url]);
+ this.dataDoc[this.fieldKey + '_history'] = new List<string>([...history, this._url]);
this.dataDoc[this.fieldKey] = new WebField(new URL(future.pop()!));
+ this._scrollHeight = 0;
if (this._webUrl === this._url) {
this._webUrl = curUrl;
setTimeout(action(() => (this._webUrl = this._url)));
@@ -531,15 +574,16 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
};
back = (checkAvailable?: boolean) => {
- const future = Cast(this.dataDoc[this.fieldKey + '-future'], listSpec('string'));
- const history = Cast(this.dataDoc[this.fieldKey + '-history'], listSpec('string'), []);
+ const future = Cast(this.rootDoc[this.fieldKey + '_future'], listSpec('string'));
+ const history = Cast(this.rootDoc[this.fieldKey + '_history'], listSpec('string'), []);
if (checkAvailable) return history.length;
runInAction(() => {
if (history.length) {
const curUrl = this._url;
- if (future === undefined) this.dataDoc[this.fieldKey + '-future'] = new List<string>([this._url]);
- else this.dataDoc[this.fieldKey + '-future'] = new List<string>([...future, this._url]);
+ if (future === undefined) this.dataDoc[this.fieldKey + '_future'] = new List<string>([this._url]);
+ else this.dataDoc[this.fieldKey + '_future'] = new List<string>([...future, this._url]);
this.dataDoc[this.fieldKey] = new WebField(new URL(history.pop()!));
+ this._scrollHeight = 0;
if (this._webUrl === this._url) {
this._webUrl = curUrl;
setTimeout(action(() => (this._webUrl = this._url)));
@@ -553,8 +597,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
};
static urlHash = (s: string) => {
+ const split = s.split('');
return Math.abs(
- s.split('').reduce((a: any, b: any) => {
+ split.reduce((a: any, b: any) => {
a = (a << 5) - a + b.charCodeAt(0);
return a & a;
}, 0)
@@ -565,23 +610,18 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
if (!newUrl) return;
if (!newUrl.startsWith('http')) newUrl = 'http://' + newUrl;
try {
- const future = Cast(this.dataDoc[this.fieldKey + '-future'], listSpec('string'));
- const history = Cast(this.dataDoc[this.fieldKey + '-history'], listSpec('string'));
- const url = this.webField?.toString();
- if (url && !preview) {
- this.dataDoc[this.fieldKey + '-history'] = new List<string>([...(history || []), url]);
- this.layoutDoc._scrollTop = 0;
+ if (!preview) {
if (this._webPageHasBeenRendered) {
this.layoutDoc.thumb = undefined;
this.layoutDoc.thumbScrollTop = undefined;
this.layoutDoc.thumbNativeWidth = undefined;
this.layoutDoc.thumbNativeHeight = undefined;
}
- future && (future.length = 0);
}
if (!preview) {
- this.dataDoc[this.fieldKey] = new WebField(new URL(newUrl));
- !dontUpdateIframe && (this._webUrl = this._url);
+ if (!dontUpdateIframe) {
+ this._webUrl = this._url;
+ }
}
} catch (e) {
console.log('WebBox URL error:' + this._url);
@@ -595,53 +635,34 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
const uri = dataTransfer.getData('text/uri-list');
const url = uri || html || this._url || '';
const newurl = url.startsWith(window.location.origin) ? url.replace(window.location.origin, this._url?.match(/http[s]?:\/\/[^\/]*/)?.[0] || '') : url;
- this.submitURL(newurl);
+ this.setData(newurl);
e.stopPropagation();
};
+
+ @action
+ setData = (data: Field | Promise<RefField | undefined>) => {
+ if (!(typeof data === 'string') && !(data instanceof WebField)) return false;
+ if (Field.toString(data) === this._url) return false;
+ this._scrollHeight = 0;
+ const oldUrl = this._url;
+ const history = Cast(this.rootDoc[this.fieldKey + '_history'], listSpec('string'), []);
+ const weburl = new WebField(Field.toString(data));
+ this.dataDoc[this.fieldKey + '_future'] = new List<string>([]);
+ this.dataDoc[this.fieldKey + '_history'] = new List<string>([...(history || []), oldUrl]);
+ this.dataDoc[this.fieldKey] = weburl;
+ return true;
+ };
onWebUrlValueKeyDown = (e: React.KeyboardEvent) => {
- e.key === 'Enter' && this.submitURL(this._keyInput.current!.value);
+ if (e.key === 'Enter') this.setData(this._keyInput.current!.value);
e.stopPropagation();
};
- @computed get urlEditor() {
- return (
- <div className="collectionMenu-webUrlButtons" onDrop={this.onWebUrlDrop} onDragOver={e => e.preventDefault()}>
- <input
- className="collectionMenu-urlInput"
- key={this._url}
- placeholder="ENTER URL"
- defaultValue={this._url}
- onDrop={this.onWebUrlDrop}
- onDragOver={e => e.preventDefault()}
- onKeyDown={this.onWebUrlValueKeyDown}
- onClick={e => {
- this._keyInput.current!.select();
- e.stopPropagation();
- }}
- ref={this._keyInput}
- />
- <div style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between', maxWidth: '250px' }}>
- <button className="submitUrl" onClick={() => this.submitURL(this._keyInput.current!.value)} onDragOver={e => e.stopPropagation()} onDrop={this.onWebUrlDrop}>
- GO
- </button>
- <button className="submitUrl" onClick={() => this.back}>
- {' '}
- <FontAwesomeIcon icon="caret-left" size="lg" />{' '}
- </button>
- <button className="submitUrl" onClick={() => this.forward}>
- {' '}
- <FontAwesomeIcon icon="caret-right" size="lg" />{' '}
- </button>
- </div>
- </div>
- );
- }
-
specificContextMenu = (e: React.MouseEvent | PointerEvent): void => {
const cm = ContextMenu.Instance;
const funcs: ContextMenuProps[] = [];
if (!cm.findByDescription('Options...')) {
- !Doc.noviceMode && funcs.push({ description: (this.layoutDoc.useCors ? "Don't Use" : 'Use') + ' Cors', event: () => (this.layoutDoc.useCors = !this.layoutDoc.useCors), icon: 'snowflake' });
+ !Doc.noviceMode &&
+ funcs.push({ description: (this.layoutDoc[this.fieldKey + '_useCors'] ? "Don't Use" : 'Use') + ' Cors', event: () => (this.layoutDoc[this.fieldKey + '_useCors'] = !this.layoutDoc[this.fieldKey + '_useCors']), icon: 'snowflake' });
funcs.push({
description: (this.layoutDoc.allowScripts ? 'Prevent' : 'Allow') + ' Scripts',
event: () => {
@@ -654,16 +675,17 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
icon: 'snowflake',
});
funcs.push({
- description: (!this.layoutDoc.forceReflow ? 'Force' : 'Prevent') + ' Reflow',
+ description: (!this.layoutDoc.layout_forceReflow ? 'Force' : 'Prevent') + ' Reflow',
event: () => {
- const nw = !this.layoutDoc.forceReflow ? undefined : Doc.NativeWidth(this.layoutDoc) - this.sidebarWidth() / (this.props.NativeDimScaling?.() || 1);
- this.layoutDoc.forceReflow = !nw;
+ const nw = !this.layoutDoc.layout_forceReflow ? undefined : Doc.NativeWidth(this.layoutDoc) - this.sidebarWidth() / (this.props.NativeDimScaling?.() || 1);
+ this.layoutDoc.layout_forceReflow = !nw;
if (nw) {
- Doc.SetInPlace(this.layoutDoc, this.fieldKey + '-nativeWidth', nw, true);
+ Doc.SetInPlace(this.layoutDoc, this.fieldKey + '_nativeWidth', nw, true);
}
},
icon: 'snowflake',
});
+ funcs.push({ description: 'Create Thumbnail', event: () => this.updateThumb(), icon: 'portrait' });
cm.addItem({ description: 'Options...', subitems: funcs, icon: 'asterisk' });
}
};
@@ -686,7 +708,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
}
};
@action finishMarquee = (x?: number, y?: number, e?: PointerEvent) => {
- this._getAnchor = AnchorMenu.Instance?.GetAnchor;
this._marqueeing = undefined;
this._isAnnotating = false;
this._iframeClick = undefined;
@@ -705,57 +726,43 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
};
@computed get urlContent() {
- if (this._hackHide || (this.webThumb && !this._webPageHasBeenRendered && LightboxView.LightboxDoc !== this.rootDoc)) return null;
- this.props.thumbShown?.();
- const field = this.dataDoc[this.props.fieldKey];
- let view;
+ setTimeout(
+ action(() => {
+ if (this._initialScroll === undefined && !this._webPageHasBeenRendered) {
+ this.setScrollPos(NumCast(this.layoutDoc.thumbScrollTop, NumCast(this.layoutDoc.layout_scrollTop)));
+ }
+ this._webPageHasBeenRendered = true;
+ })
+ );
+ const field = this.rootDoc[this.props.fieldKey];
if (field instanceof HtmlField) {
- view = <span className="webBox-htmlSpan" contentEditable onPointerDown={e => e.stopPropagation()} dangerouslySetInnerHTML={{ __html: field.html }} />;
- } else if (field instanceof WebField) {
- const url = this.layoutDoc.useCors ? Utils.CorsProxy(this._webUrl) : this._webUrl;
- view = (
+ return <span className="webBox-htmlSpan" contentEditable onPointerDown={e => e.stopPropagation()} dangerouslySetInnerHTML={{ __html: field.html }} />;
+ }
+ if (field instanceof WebField) {
+ const url = this.layoutDoc[this.fieldKey + '_useCors'] ? Utils.CorsProxy(this._webUrl) : this._webUrl;
+ return (
<iframe
className="webBox-iframe"
- enable-annotation={'true'}
- style={{ pointerEvents: this._scrollTimer ? 'none' : undefined }}
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`}
/>
);
- } else {
- view = (
- <iframe
- className="webBox-iframe"
- enable-annotation={'true'}
- style={{ pointerEvents: this._scrollTimer ? 'none' : undefined }} // if we allow pointer events when scrolling is on, then reversing direction does not work smoothly
- ref={action((r: HTMLIFrameElement | null) => (this._iframe = r))}
- src={'https://crossorigin.me/https://cs.brown.edu'}
- />
- );
}
- setTimeout(
- action(() => {
- this._scrollHeight = Math.max(this._scrollHeight, this._iframe && this._iframe.contentDocument && this._iframe.contentDocument.body ? this._iframe.contentDocument.body.scrollHeight : 0);
- if (this._initialScroll === undefined && !this._webPageHasBeenRendered) {
- this.setScrollPos(NumCast(this.layoutDoc.thumbScrollTop, NumCast(this.layoutDoc.scrollTop)));
- }
- this._webPageHasBeenRendered = true;
- })
- );
- return view;
+ return <iframe className="webBox-iframe" ref={action((r: HTMLIFrameElement | null) => (this._iframe = r))} src={'https://crossorigin.me/https://cs.brown.edu'} />;
}
addDocumentWrapper = (doc: Doc | Doc[], annotationKey?: string) => {
- (doc instanceof Doc ? [doc] : doc).forEach(doc => (doc.webUrl = this._url));
+ (doc instanceof Doc ? [doc] : doc).forEach(doc => (doc.presData = new WebField(this._url)));
return this.addDocument(doc, annotationKey);
};
sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => {
- if (!this.layoutDoc._showSidebar) this.toggleSidebar();
+ if (!this.layoutDoc._layout_showSidebar) this.toggleSidebar();
return this.addDocumentWrapper(doc, sidebarKey);
};
@observable _draggingSidebar = false;
@@ -771,15 +778,15 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
.ScreenToLocalTransform()
.scale(this.props.NativeDimScaling?.() || 1)
.transformDirection(delta[0], delta[1]);
- const nativeWidth = NumCast(this.layoutDoc[this.fieldKey + '-nativeWidth']);
- const nativeHeight = NumCast(this.layoutDoc[this.fieldKey + '-nativeHeight']);
+ const nativeWidth = NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth']);
+ const nativeHeight = NumCast(this.layoutDoc[this.fieldKey + '_nativeHeight']);
const curNativeWidth = NumCast(this.layoutDoc.nativeWidth, nativeWidth);
const ratio = (curNativeWidth + ((onButton ? 1 : -1) * localDelta[0]) / (this.props.NativeDimScaling?.() || 1)) / nativeWidth;
if (ratio >= 1) {
this.layoutDoc.nativeWidth = nativeWidth * ratio;
this.layoutDoc.nativeHeight = nativeHeight * (1 + ratio);
- onButton && (this.layoutDoc._width = this.layoutDoc[WidthSym]() + localDelta[0]);
- this.layoutDoc._showSidebar = nativeWidth !== this.layoutDoc._nativeWidth;
+ onButton && (this.layoutDoc._width = this.layoutDoc[Width]() + localDelta[0]);
+ this.layoutDoc._layout_showSidebar = nativeWidth !== this.layoutDoc._nativeWidth;
}
return false;
}),
@@ -801,7 +808,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
title="Toggle Sidebar"
style={{
display: !this.props.isContentActive() ? 'none' : undefined,
- top: StrCast(this.rootDoc._showTitle) === 'title' ? 20 : 5,
+ top: StrCast(this.layoutDoc._layout_showTitle) === 'title' ? 20 : 5,
backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK,
}}
onPointerDown={e => this.sidebarBtnDown(e, true)}>
@@ -812,42 +819,57 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
@observable _previewNativeWidth: Opt<number> = undefined;
@observable _previewWidth: Opt<number> = undefined;
toggleSidebar = action((preview: boolean = false) => {
- var nativeWidth = NumCast(this.layoutDoc[this.fieldKey + '-nativeWidth']);
+ var nativeWidth = NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth']);
if (!nativeWidth) {
- const defaultNativeWidth = this.dataDoc[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);
- nativeWidth = NumCast(this.layoutDoc[this.fieldKey + '-nativeWidth']);
+ 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;
const pdfratio = ((!this.layoutDoc.nativeWidth || this.layoutDoc.nativeWidth === nativeWidth ? WebBox.openSidebarWidth + WebBox.sidebarResizerWidth : 0) + nativeWidth) / nativeWidth;
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._showSidebar = !this.layoutDoc._showSidebar;
- this.layoutDoc._width = (this.layoutDoc[WidthSym]() * nativeWidth * pdfratio) / curNativeWidth;
- if (!this.layoutDoc._showSidebar && !(this.dataDoc[this.fieldKey] instanceof WebField)) {
- this.layoutDoc.nativeWidth = this.dataDoc[this.fieldKey + '-nativeWidth'] = undefined;
+ this.layoutDoc._layout_showSidebar = !this.layoutDoc._layout_showSidebar;
+ 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 {
this.layoutDoc.nativeWidth = nativeWidth * pdfratio;
}
}
});
+ @action
+ onZoomWheel = (e: React.WheelEvent) => {
+ if (this.props.isContentActive(true)) {
+ e.stopPropagation();
+ }
+ };
sidebarWidth = () => {
if (!this.SidebarShown) return 0;
if (this._previewWidth) return WebBox.sidebarResizerWidth + WebBox.openSidebarWidth; // return default sidebar if previewing (as in viewing a link target)
const nativeDiff = NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc);
return WebBox.sidebarResizerWidth + nativeDiff * (this.props.NativeDimScaling?.() || 1);
};
+ _innerCollectionView: CollectionFreeFormView | undefined;
+ zoomScaling = () => this._innerCollectionView?.zoomScaling() ?? 1;
+ setInnerContent = (component: DocComponentView) => (this._innerCollectionView = component as CollectionFreeFormView);
+
@computed get content() {
- const interactive =
- !this.props.docViewPath().lastElement()?.docView?._pendingDoubleClick && this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' && Doc.ActiveTool === InkTool.None && !DocumentDecorations.Instance?.Interacting;
+ const interactive = this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' && Doc.ActiveTool === InkTool.None;
return (
- <div className={'webBox-cont' + (interactive ? '-interactive' : '')} onKeyDown={e => e.stopPropagation()} style={{ width: !this.layoutDoc.forceReflow ? NumCast(this.layoutDoc[this.fieldKey + '-nativeWidth']) || `100%` : '100%' }}>
- {this.urlContent}
+ <div
+ className={'webBox-cont' + (interactive ? '-interactive' : '')}
+ onKeyDown={e => e.stopPropagation()}
+ style={{
+ width: !this.layoutDoc.layout_forceReflow ? NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth']) || `100%` : '100%',
+ transform: `scale(${this.zoomScaling()}) translate(${-NumCast(this.layoutDoc.freeform_panX)}px, ${-NumCast(this.layoutDoc.freeform_panY)}px)`,
+ }}>
+ {this._hackHide || (this.webThumb && !this._webPageHasBeenRendered && LightboxView.LightboxDoc !== this.rootDoc) ? null : this.urlContent}
</div>
);
}
@@ -855,7 +877,13 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
@computed get annotationLayer() {
TraceMobx();
return (
- <div className="webBox-annotationLayer" style={{ height: Doc.NativeHeight(this.Document) || undefined }} ref={this._annotationLayer}>
+ <div
+ className="webBox-annotationLayer"
+ style={{
+ transform: `scale(${this.zoomScaling()}) translate(${-NumCast(this.layoutDoc.freeform_panX)}px, ${-NumCast(this.layoutDoc.freeform_panY)}px)`,
+ height: Doc.NativeHeight(this.Document) || undefined,
+ }}
+ ref={this._annotationLayer}>
{this.inlineTextAnnotations
.sort((a, b) => NumCast(a.y) - NumCast(b.y))
.map(anno => (
@@ -865,51 +893,57 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
);
}
@computed get SidebarShown() {
- return this._showSidebar || this.layoutDoc._showSidebar ? true : false;
+ return this._showSidebar || this.layoutDoc._layout_showSidebar ? true : false;
}
@computed get webpage() {
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
- {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
+ {...this.props}
+ setContentView={this.setInnerContent}
+ NativeWidth={returnZero}
+ NativeHeight={returnZero}
+ originTopLeft={false}
+ isAnnotationOverlayScrollable={true}
renderDepth={this.props.renderDepth + 1}
isAnnotationOverlay={true}
fieldKey={this.annotationKey}
- CollectionView={undefined}
setPreviewCursor={this.setPreviewCursor}
setBrushViewer={this.setBrushViewer}
PanelWidth={this.panelWidth}
PanelHeight={this.panelHeight}
ScreenToLocalTransform={this.scrollXf}
NativeDimScaling={returnOne}
- dropAction={'alias'}
- docFilters={docFilters}
+ focus={this.focus}
+ childFilters={childFilters}
select={emptyFunction}
+ isAnyChildContentActive={returnFalse}
bringToFront={emptyFunction}
styleProvider={this.childStyleProvider}
whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
removeDocument={this.removeDocument}
moveDocument={this.moveDocument}
- addDocument={this.addDocument}
+ addDocument={this.addDocumentWrapper}
childPointerEvents={this.props.isContentActive() ? 'all' : undefined}
pointerEvents={this.annotationPointerEvents}
/>
);
return (
<div
- className={'webBox-outerContent'}
+ className="webBox-outerContent"
ref={this._outerRef}
style={{
height: `${100 / scale}%`,
pointerEvents,
}}
- onWheel={StopEvent} // block wheel events from propagating since they're handled by the iframe
+ // when active, block wheel events from propagating since they're handled by the iframe
+ onWheel={this.onZoomWheel}
onScroll={e => this.setDashScrollTop(this._outerRef.current?.scrollTop || 0)}
onPointerDown={this.onMarqueeDown}>
- <div className={'webBox-innerContent'} style={{ height: this._webPageHasBeenRendered && this._scrollHeight ? this.scrollHeight : '100%', pointerEvents }}>
+ <div className="webBox-innerContent" style={{ height: (this._webPageHasBeenRendered && this._scrollHeight) || '100%', pointerEvents }}>
{this.content}
{<div style={{ display: DragManager.docsBeingDragged.length ? 'none' : undefined, mixBlendMode: 'multiply' }}>{renderAnnotations(this.transparentFilter)}</div>}
{renderAnnotations(this.opaqueFilter)}
@@ -956,20 +990,23 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
searchStringChanged = (e: React.ChangeEvent<HTMLInputElement>) => (this._searchString = e.currentTarget.value);
showInfo = action((anno: Opt<Doc>) => (this._overlayAnnoInfo = anno));
setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean) => void) => (this._setPreviewCursor = func);
- panelWidth = () => this.props.PanelWidth() / (this.props.NativeDimScaling?.() || 1) - this.sidebarWidth() + WebBox.sidebarResizerWidth; // (this.Document.scrollHeight || Doc.NativeHeight(this.Document) || 0);
- panelHeight = () => this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1); // () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : Doc.NativeWidth(this.Document);
- scrollXf = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._scrollTop));
+ panelWidth = () => this.props.PanelWidth() / (this.props.NativeDimScaling?.() || 1) - this.sidebarWidth() + WebBox.sidebarResizerWidth;
+ 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);
@@ -978,7 +1015,11 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
<div
className="webBox"
ref={this._mainCont}
- style={{ pointerEvents: this.pointerEvents(), position: SnappingManager.GetIsDragging() ? 'absolute' : undefined, display: !SnappingManager.GetIsDragging() && this.props.thumbShown?.() ? 'none' : undefined }}>
+ style={{
+ pointerEvents: this.pointerEvents(), //
+ position: SnappingManager.GetIsDragging() ? 'absolute' : undefined,
+ display: !this.props.isContentActive() && this.props.thumbShown?.() ? 'none' : undefined,
+ }}>
<div className="webBox-background" style={{ backgroundColor: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor) }} />
<div
className="webBox-container"
@@ -1003,10 +1044,10 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
docView={this.props.docViewPath().lastElement()}
finishMarquee={this.finishMarquee}
savedAnnotations={this.savedAnnotationsCreator}
- selectionText={returnEmptyString}
+ selectionText={this.selectionText}
annotationLayer={this._annotationLayer.current}
mainCont={this._mainCont.current}
- />{' '}
+ />
</div>
)}
</div>
@@ -1024,7 +1065,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
ref={this._sidebarRef}
{...this.props}
whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
- fieldKey={this.fieldKey + '-' + this._urlHash}
+ fieldKey={this.fieldKey + '_' + this._urlHash}
rootDoc={this.rootDoc}
layoutDoc={this.layoutDoc}
dataDoc={this.dataDoc}
@@ -1043,5 +1084,5 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
}
}
ScriptingGlobals.add(function urlHash(url: string) {
- return WebBox.urlHash(url);
+ return url ? WebBox.urlHash(url) : 0;
});
diff --git a/src/client/views/nodes/WebBoxRenderer.js b/src/client/views/nodes/WebBoxRenderer.js
index cebb94d86..eb8064780 100644
--- a/src/client/views/nodes/WebBoxRenderer.js
+++ b/src/client/views/nodes/WebBoxRenderer.js
@@ -42,6 +42,8 @@ var ForeignHtmlRenderer = function (styleSheets) {
url = CorsProxy(new URL(webUrl).origin + inurl);
} else if (!inurl.startsWith('http') && !inurl.startsWith('//')) {
url = CorsProxy(webUrl + '/' + inurl);
+ } else if (inurl.startsWith('https')) {
+ url = CorsProxy(inurl);
}
xhr.open('GET', url);
xhr.responseType = 'blob';
@@ -124,7 +126,7 @@ var ForeignHtmlRenderer = function (styleSheets) {
if (url === null) {
break;
}
- searchStartIndex = url.foundAtIndex + url.value.length;
+ searchStartIndex = url.foundAtIndex + url.value.length + 1;
if (mustEndWithQuote && url.value[url.value.length - 1] !== '"') continue;
const unquoted = removeQuotes(url.value);
if (!unquoted /* || (!unquoted.startsWith('http')&& !unquoted.startsWith("/") )*/ || unquoted === 'http://' || unquoted === 'https://') {
@@ -216,7 +218,9 @@ var ForeignHtmlRenderer = function (styleSheets) {
}
const styleElem = document.createElement('style');
- styleElem.innerHTML = cssStyles.replace('&gt;', '>').replace('&lt;', '<');
+ styleElem.innerHTML =
+ '#mw-sidebar-checkbox ~ .vector-main-menu-container { display: none !important; } ' + // hack to prevent wikipedia menu from appearing
+ cssStyles.replace('&gt;', '>').replace('&lt;', '<');
const styleElemString = new XMLSerializer().serializeToString(styleElem).replace(/&gt;/g, '>').replace(/&lt;/g, '<');
diff --git a/src/client/views/nodes/button/ButtonScripts.ts b/src/client/views/nodes/button/ButtonScripts.ts
deleted file mode 100644
index b4a382faf..000000000
--- a/src/client/views/nodes/button/ButtonScripts.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { ScriptingGlobals } from "../../../util/ScriptingGlobals";
-import { SelectionManager } from "../../../util/SelectionManager";
-import { Colors } from "../../global/globalEnums";
-
-// toggle: Set overlay status of selected document
-ScriptingGlobals.add(function changeView(view: string) {
- const selected = SelectionManager.Views().length ? SelectionManager.Views()[0] : undefined;
- selected ? selected.Document._viewType = view : console.log("[FontIconBox.tsx] changeView failed");
-});
-
-// toggle: Set overlay status of selected document
-ScriptingGlobals.add(function toggleOverlay(readOnly?: boolean) {
- const selected = SelectionManager.Views().length ? SelectionManager.Views()[0] : undefined;
- if (readOnly) return selected?.Document.z ? Colors.MEDIUM_BLUE : "transparent";
- selected ? selected.props.CollectionFreeFormDocumentView?.().float() : console.log("failed");
-}); \ No newline at end of file
diff --git a/src/client/views/nodes/button/FontIconBadge.tsx b/src/client/views/nodes/button/FontIconBadge.tsx
index 3b5aac221..b50588ce2 100644
--- a/src/client/views/nodes/button/FontIconBadge.tsx
+++ b/src/client/views/nodes/button/FontIconBadge.tsx
@@ -1,10 +1,6 @@
-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";
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import './FontIconBadge.scss';
interface FontIconBadgeProps {
value: string | undefined;
@@ -25,13 +21,17 @@ export class FontIconBadge extends React.Component<FontIconBadgeProps> {
// }
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}
+ 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>
- </div>;
+ );
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/nodes/button/FontIconBox.scss b/src/client/views/nodes/button/FontIconBox.scss
index a1ca777b3..f3b43501b 100644
--- a/src/client/views/nodes/button/FontIconBox.scss
+++ b/src/client/views/nodes/button/FontIconBox.scss
@@ -43,10 +43,6 @@
cursor: pointer;
flex-direction: column;
- &:hover {
- background-color: rgba(0, 0, 0, 0.3) !important;
- }
-
svg {
width: 50% !important;
height: 50%;
@@ -68,10 +64,6 @@
justify-content: center;
align-items: center;
justify-items: center;
-
- &:hover {
- filter: brightness(0.85) !important;
- }
}
&.tglBtn,
@@ -166,7 +158,7 @@
width: 100%;
border-radius: 100%;
flex-direction: column;
- margin-top: -4px;
+ // margin-top: -4px;
svg {
width: 60% !important;
@@ -220,10 +212,6 @@
box-shadow: 0px 3px 4px rgba(0, 0, 0, 0.3);
border-radius: 3px;
}
-
- &:hover {
- background-color: rgba(0, 0, 0, 0.3) !important;
- }
}
&.colorBtnLabel {
@@ -248,10 +236,6 @@
align-content: center;
align-items: center;
- &:hover {
- background-color: rgba(0, 0, 0, 0.3) !important;
- }
-
.menuButton-dropdownList {
position: absolute;
width: 150px;
@@ -283,10 +267,6 @@
cursor: pointer;
background: transparent;
- &:hover {
- background-color: rgba(0, 0, 0, 0.3) !important;
- }
-
&.slider {
color: $white;
cursor: pointer;
@@ -447,11 +427,11 @@
}
.dropbox-background {
- width: 100vw;
- height: 100vh;
- top: 0;
+ width: 200vw;
+ height: 200vh;
+ top: -100vh;
z-index: 20;
- left: 0;
+ left: -100vw;
background: transparent;
position: fixed;
}
diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx
index 42d20ba99..5bba51ec8 100644
--- a/src/client/views/nodes/button/FontIconBox.tsx
+++ b/src/client/views/nodes/button/FontIconBox.tsx
@@ -5,17 +5,19 @@ 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 { Doc, StrListCast } from '../../../../fields/Doc';
+import { Height, Width } from '../../../../fields/DocSymbols';
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, StopEvent, Utils } from '../../../../Utils';
+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 { undoable, undoBatch, UndoManager } from '../../../util/UndoManager';
import { CollectionFreeFormView } from '../../collections/collectionFreeForm';
import { ContextMenu } from '../../ContextMenu';
import { DocComponent } from '../../DocComponent';
@@ -26,6 +28,8 @@ import { ActiveFillColor, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, SetAc
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';
@@ -41,16 +45,12 @@ export enum ButtonType {
ToggleButton = 'tglBtn',
ColorButton = 'colorBtn',
ToolButton = 'toolBtn',
- NumberButton = 'numBtn',
+ NumberSliderButton = 'numSliderBtn',
+ NumberDropdownButton = 'numDropdownBtn',
+ NumberInlineButton = 'numInlineBtn',
EditableText = 'editableText',
}
-export enum NumButtonType {
- Slider = 'slider',
- DropdownOptions = 'list',
- Inline = 'inline',
-}
-
export interface ButtonProps extends FieldViewProps {
type?: ButtonType;
}
@@ -59,9 +59,10 @@ 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, 'add:right');
+ dragFactory && this.props.addDocTab(dragFactory, OpenWhere.addRight);
};
dragAsTemplate = (): void => {
this.layoutDoc.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)');
@@ -88,16 +89,16 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
static GetRecognizeGestures() {
return BoolCast(Doc.UserDoc()._recognizeGestures);
}
- static SetRecognizeGesturs(show: boolean) {
+ static SetRecognizeGestures(show: boolean) {
Doc.UserDoc()._recognizeGestures = show;
}
// Determining UI Specs
@computed get label() {
- return StrCast(this.rootDoc.label, StrCast(this.rootDoc.title));
+ return StrCast(this.rootDoc.icon_label, StrCast(this.rootDoc.title));
}
Icon = (color: string) => {
- const icon = StrCast(this.dataDoc.icon, 'user') as any;
+ 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} />;
};
@@ -128,100 +129,116 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
/**
* Number button
*/
- @computed get numberButton() {
- const numBtnType: string = StrCast(this.rootDoc.numBtnType);
- const numScript = ScriptCast(this.rootDoc.script);
- const setValue = (value: number) => numScript?.script.run({ value, _readOnly_: false });
-
+ @computed get numberSliderButton() {
+ const numScript = (value?: number) => ScriptCast(this.rootDoc.script).script.run({ self: this.rootDoc, value, _readOnly_: value === undefined });
// Script for checking the outcome of the toggle
- const checkResult: number = numScript?.script.run({ value: 0, _readOnly_: true }).result || 0;
-
+ const checkResult = Number(numScript().result ?? 0).toPrecision(NumCast(this.dataDoc.numPrecision, 3));
const label = !FontIconBox.GetShowLabels() ? null : <div className="fontIconBox-label">{this.label}</div>;
- if (numBtnType === NumButtonType.Slider) {
- 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'}
- id="slider"
- onPointerDown={() => (this._batch = UndoManager.StartBatch('presDuration'))}
- onPointerUp={() => this._batch?.end()}
- onChange={e => {
- e.stopPropagation();
- setValue(Number(e.target.value));
- }}
- />
- </div>
- );
+ const dropdown = (
+ <div className="menuButton-dropdownBox" onPointerDown={e => e.stopPropagation()}>
+ <input
+ className="menu-slider"
+ type="range"
+ step="1"
+ min={NumCast(this.rootDoc.numBtnMin, 0)}
+ max={NumCast(this.rootDoc.numBtnMax, 100)}
+ //readOnly={true}
+ value={checkResult}
+ onPointerDown={() => (this._batch = UndoManager.StartBatch('num slider changing'))}
+ onPointerUp={() => this._batch?.end()}
+ onChange={undoable(e => {
+ e.stopPropagation();
+ numScript(Number(e.target.value));
+ }, 'set num 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 = (value?: number) => ScriptCast(this.rootDoc.script)?.script.run({ self: this.rootDoc, value, _readOnly_: value === undefined });
+
+ const checkResult = Number(numScript().result ?? 0).toPrecision(NumCast(this.dataDoc.numPrecision, 3));
+
+ const items: number[] = [];
+ for (let i = 0; i < 100; i += 2) items.push(i);
+
+ const list = items.map(value => {
return (
- <div className={`menuButton ${this.type} ${numBtnType}`} onClick={action(() => (this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen))}>
- {checkResult}
- {label}
- {this.rootDoc.dropDownOpen ? dropdown : null}
+ <div
+ className="list-item"
+ key={`${value}`}
+ style={{
+ backgroundColor: value.toString() === checkResult ? Colors.LIGHT_BLUE : undefined,
+ }}
+ onClick={undoable(value => numScript(value), `${this.rootDoc.title} button set from list`)}>
+ {value}
</div>
);
- } else if (numBtnType === NumButtonType.DropdownOptions) {
- 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 === checkResult ? Colors.LIGHT_BLUE : undefined,
- }}
- onClick={() => setValue(value)}>
- {value}
- </div>
- );
- });
- return (
- <div className={`menuButton ${this.type} ${numBtnType}`}>
- <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))}>
- <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>
+ });
+ return (
+ <div className="menuButton numBtn list">
+ <div className="button" onClick={undoable(e => numScript(Number(checkResult) - 1), `${this.rootDoc.title} decrement value`)}>
+ <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} readOnly={true} onChange={undoable(e => numScript(Number(e.target.value)), `${this.rootDoc.title} button set value`)} />
+ </div>
+ <div className={`button`} onClick={undoable(e => numScript(Number(checkResult) + 1), `${this.rootDoc.title} increment value`)}>
+ <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={e => {
- e.stopPropagation();
- this.rootDoc.dropDownOpen = false;
- }}
- />
+ {this.rootDoc.dropDownOpen ? (
+ <div>
+ <div className="menuButton-dropdownList" style={{ left: '25%' }}>
+ {list}
</div>
- ) : null}
- </div>
- );
- } else {
- return <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 />;
}
/**
@@ -235,7 +252,11 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
<div
className={`menuButton ${this.type} ${active}`}
style={{ color: color, backgroundColor: backgroundColor, borderBottomLeftRadius: this.dropdown ? 0 : undefined }}
- onClick={action(() => (this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen))}>
+ 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 }}>
@@ -270,7 +291,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
const selected = SelectionManager.Docs().lastElement();
if (selected) {
if (StrCast(selected.type) === DocumentType.COL) {
- text = StrCast(selected._viewType);
+ text = StrCast(selected._type_collection);
} else {
dropdown = false;
text = selected.type === DocumentType.RTF ? 'Text' : StrCast(selected.type);
@@ -282,27 +303,23 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
text = 'User Default';
}
noviceList = [CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Stacking, CollectionViewType.NoteTaking];
- } else if (script?.script.originalScript.startsWith('setFont')) {
- const editorView = RichTextMenu.Instance?.TextView?.EditorView;
- text = StrCast((editorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily);
- noviceList = ['Roboto', 'Times New Roman', 'Arial', 'Georgia', 'Comic Sans MS', 'Tahoma', 'Impact', 'Crimson Text'];
- }
+ } 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.includes(value))
+ .filter(value => !Doc.noviceMode || !noviceList.length || noviceList.includes(value))
.map(value => (
<div
className="list-item"
- key={`${value}`}
+ key={value}
style={{
- fontFamily: script.script.originalScript.startsWith('setFont') ? value : undefined,
+ fontFamily: script.script.originalScript.startsWith('{ return setFont') ? value : undefined,
backgroundColor: value === text ? Colors.LIGHT_BLUE : undefined,
}}
- onClick={undoBatch(() => script.script.run({ value }))}>
+ onClick={undoable(() => script.script.run({ self: this.rootDoc, value }), value)}>
{value[0].toUpperCase() + value.slice(1)}
</div>
));
@@ -318,7 +335,15 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
<div
className={`menuButton ${this.type} ${active}`}
style={{ backgroundColor: this.rootDoc.dropDownOpen ? Colors.MEDIUM_BLUE : backgroundColor, color: color, display: dropdown ? undefined : 'flex' }}
- onClick={dropdown ? () => (this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen) : undefined}>
+ 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}
@@ -334,10 +359,12 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
</div>
<div
className="dropbox-background"
- onClick={e => {
+ onClick={action(e => {
e.stopPropagation();
this.rootDoc.dropDownOpen = false;
- }}
+ this.noTooltip = false;
+ Doc.UnBrushAllDocs();
+ })}
/>
</div>
) : null}
@@ -355,7 +382,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
ev.preventDefault();
ev.stopPropagation();
const s = this.colorScript;
- s && undoBatch(() => s.script.run({ value: Utils.colorString(value), _readOnly_: false }).result)();
+ 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} />;
@@ -366,7 +393,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
@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({ value: undefined, _readOnly_: true }).result ?? 'transparent';
+ const curColor = this.colorScript?.script.run({ self: this.rootDoc, value: undefined, _readOnly_: true }).result ?? 'transparent';
const label =
!this.label || !FontIconBox.GetShowLabels() ? null : (
@@ -375,19 +402,14 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
</div>
);
- // dropdown caret seems superfluous since clicking the color button does the same thing
- // const dropdownCaret = <div
- // className="menuButton-dropDown"
- // style={{ borderBottomRightRadius: this.dropdown ? 0 : undefined }}>
- // <FontAwesomeIcon icon={'caret-down'} color={color} size="sm" />
- // </div>;
- //setTimeout(() => this.colorPicker(curColor)); // cause an update to the color picker rendered in MainView
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()}>
@@ -406,6 +428,8 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
e.preventDefault();
e.stopPropagation();
this.colorPickerClosed = true;
+ this.noTooltip = false;
+ Doc.UnBrushAllDocs();
})}
/>
</div>
@@ -482,7 +506,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
<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} contents={checkResult} />
+ <EditableView GetValue={() => script?.script.run({ value: '', _readOnly_: true }).result} SetValue={setValue} oneLine={true} contents={checkResult} />
</div>
</div>
);
@@ -502,14 +526,20 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
// prettier-ignore
switch (this.type) {
- case ButtonType.DropdownList: return this.dropdownListButton;
- case ButtonType.ColorButton: return this.colorButton;
- case ButtonType.NumberButton: return this.numberButton;
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: button = (
- <div className={`menuButton ${this.type}`} style={{ color, backgroundColor, opacity: 1, gridAutoColumns: `${NumCast(this.rootDoc._height)} auto` }}>
+ 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()}
@@ -534,23 +564,49 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
break;
}
- return !this.layoutDoc.toolTip ? button : <Tooltip title={<div className="dash-tooltip">{StrCast(this.layoutDoc.toolTip)}</div>}>{button}</Tooltip>;
+ 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');
+ 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 selected = SelectionManager.Views().lastElement();
- if (checkResult) {
- return selected?.props.Document._backgroundColor ?? 'transparent';
+ 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));
}
- if (selected) selected.props.Document._backgroundColor = color;
});
// toggle: Set overlay status of selected document
@@ -560,7 +616,7 @@ ScriptingGlobals.add(function setHeaderColor(color?: string, checkResult?: boole
}
Doc.SharingDoc().userColor = undefined;
Doc.GetProto(Doc.SharingDoc()).userColor = color;
- Doc.UserDoc().showTitle = color === 'transparent' ? undefined : StrCast(Doc.UserDoc().showTitle, 'creationDate');
+ Doc.UserDoc().layout_showTitle = color === 'transparent' ? undefined : StrCast(Doc.UserDoc().layout_showTitle, 'author_date');
});
// toggle: Set overlay status of selected document
@@ -573,127 +629,107 @@ ScriptingGlobals.add(function toggleOverlay(checkResult?: boolean) {
selected ? selected.props.CollectionFreeFormDocumentView?.().float() : console.log('[FontIconBox.tsx] toggleOverlay failed');
});
-/** TEXT
- * setFont
- * setFontSize
- * toggleBold
- * toggleUnderline
- * toggleItalic
- * setAlignment
- * toggleBold
- * toggleItalic
- * toggleUnderline
- **/
-
-// toggle: Set overlay status of selected document
-ScriptingGlobals.add(function setFont(font: string, checkResult?: boolean) {
- if (checkResult) return RichTextMenu.Instance?.fontFamily;
- font && RichTextMenu.Instance.setFontFamily(font);
-});
-
-ScriptingGlobals.add(function getActiveTextInfo(info: 'family' | 'size' | 'color' | 'highlight') {
- const editorView = RichTextMenu.Instance.TextView?.EditorView;
- const style = editorView?.state && RichTextMenu.Instance.getActiveFontStylesOnSelection();
+ScriptingGlobals.add(function showFreeform(attr: 'flashcards' | 'grid' | 'snaplines' | 'clusters' | 'arrange' | 'viewAll', checkResult?: boolean) {
+ const selected = SelectionManager.Docs().lastElement();
// prettier-ignore
- switch (info) {
- case 'family': return style?.activeFamilies[0];
- case 'size': return style?.activeSizes[0];
- case 'color': return style?.activeColors[0];
- case 'highlight': return style?.activeHighlights[0];
- }
-});
+ 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) => doc._freeform_backgroundGrid,
+ setDoc: (doc:Doc) => doc._freeform_backgroundGrid = !doc._freeform_backgroundGrid,
+ }],
+ ['snaplines', {
+ checkResult: (doc:Doc) => doc._freeform_snapLines,
+ setDoc: (doc:Doc) => doc._freeform_snapLines = !doc._freeform_snapLines,
+ }],
+ ['viewAll', {
+ checkResult: (doc:Doc) => doc._freeform_fitContentsToBox,
+ 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) => doc._freeform_useClusters,
+ 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) => doc._autoArrange,
+ setDoc: (doc:Doc) => doc._autoArrange = !doc._autoArrange,
+ }],
+ ['flashcards', {
+ checkResult: (doc:Doc) => Doc.UserDoc().defaultToFlashcards,
+ setDoc: (doc:Doc) => Doc.UserDoc().defaultToFlashcards = !Doc.UserDoc().defaultToFlashcards,
+ }],
+ ]);
-ScriptingGlobals.add(function setAlignment(align: 'left' | 'right' | 'center', checkResult?: boolean) {
- const editorView = RichTextMenu.Instance?.TextView?.EditorView;
if (checkResult) {
- return (editorView ? RichTextMenu.Instance.textAlign : Doc.UserDoc().textAlign) === align ? Colors.MEDIUM_BLUE : 'transparent';
+ return map.get(attr)?.checkResult(selected) ? Colors.MEDIUM_BLUE : 'transparent';
}
- if (editorView?.state) RichTextMenu.Instance.align(editorView, editorView.dispatch, align);
- else Doc.UserDoc().textAlign = align;
+ 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 setBulletList(mapStyle: 'bullet' | 'decimal', checkResult?: boolean) {
+ScriptingGlobals.add(function setFontAttr(attr: 'font' | 'fontColor' | 'highlight' | 'fontSize', value: any, checkResult?: boolean) {
const editorView = RichTextMenu.Instance?.TextView?.EditorView;
- if (checkResult) {
- const active = editorView?.state && RichTextMenu.Instance.getActiveListStyle();
- if (active === mapStyle) return Colors.MEDIUM_BLUE;
- return 'transparent';
- }
- editorView?.state && RichTextMenu.Instance.changeListType(mapStyle);
-});
-
-// toggle: Set overlay status of selected document
-ScriptingGlobals.add(function setFontColor(color?: string, checkResult?: boolean) {
- if (checkResult) return RichTextMenu.Instance.fontColor;
- color && RichTextMenu.Instance.setColor(color);
-});
-
-// toggle: Set overlay status of selected document
-ScriptingGlobals.add(function setFontHighlight(color?: string, checkResult?: boolean) {
const selected = SelectionManager.Docs().lastElement();
- const editorView = RichTextMenu.Instance.TextView?.EditorView;
+ // 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 (selected ?? Doc.UserDoc())._fontHighlight;
- }
- if (selected) {
- selected._fontColor = color;
- if (color) {
- editorView?.state && RichTextMenu.Instance.setHighlight(color, editorView, editorView?.dispatch);
- }
+ return map.get(attr)?.checkResult();
}
- Doc.UserDoc()._fontHighlight = color;
+ map.get(attr)?.setDoc?.();
});
-// toggle: Set overlay status of selected document
-ScriptingGlobals.add(function setFontSize(size: string | number, checkResult?: boolean) {
- if (checkResult) {
- return RichTextMenu.Instance?.fontSize.replace('px', '');
- }
- if (typeof size === 'number') size = size.toString();
- if (size && Number(size).toString() === size) size += 'px';
- RichTextMenu.Instance.setFontSize(size);
-});
-ScriptingGlobals.add(function toggleNoAutoLinkAnchor(checkResult?: boolean) {
- const editorView = RichTextMenu.Instance?.TextView?.EditorView;
- if (checkResult) {
- return (editorView ? RichTextMenu.Instance.noAutoLink : false) ? Colors.MEDIUM_BLUE : 'transparent';
- }
- if (editorView) RichTextMenu.Instance?.toggleNoAutoLinkAnchor();
-});
-ScriptingGlobals.add(function toggleDictation(checkResult?: boolean) {
+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;
- if (checkResult) {
- return textView?._recording ? Colors.MEDIUM_BLUE : 'transparent';
- }
- if (textView) runInAction(() => (textView._recording = !textView._recording));
-});
-
-ScriptingGlobals.add(function toggleBold(checkResult?: boolean) {
- const editorView = RichTextMenu.Instance?.TextView?.EditorView;
- if (checkResult) {
- return (editorView ? RichTextMenu.Instance.bold : Doc.UserDoc().fontWeight === 'bold') ? Colors.MEDIUM_BLUE : 'transparent';
- }
- if (editorView) RichTextMenu.Instance?.toggleBold();
- else Doc.UserDoc().fontWeight = Doc.UserDoc().fontWeight === 'bold' ? undefined : 'bold';
-});
-
-ScriptingGlobals.add(function toggleUnderline(checkResult?: boolean) {
- const editorView = RichTextMenu.Instance?.TextView?.EditorView;
- if (checkResult) {
- return (editorView ? RichTextMenu.Instance.underline : Doc.UserDoc().textDecoration === 'underline') ? Colors.MEDIUM_BLUE : 'transparent';
- }
- if (editorView) RichTextMenu.Instance?.toggleUnderline();
- else Doc.UserDoc().textDecoration = Doc.UserDoc().textDecoration === 'underline' ? undefined : 'underline';
-});
-
-ScriptingGlobals.add(function toggleItalic(checkResult?: boolean) {
- const editorView = RichTextMenu.Instance?.TextView?.EditorView;
- if (checkResult) {
- return (editorView ? RichTextMenu.Instance.italics : Doc.UserDoc().fontStyle === 'italics') ? Colors.MEDIUM_BLUE : 'transparent';
- }
- if (editorView) RichTextMenu.Instance?.toggleItalics();
- else Doc.UserDoc().fontStyle = Doc.UserDoc().fontStyle === 'italics' ? undefined : 'italics';
+ 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() {
@@ -722,8 +758,8 @@ export function createInkGroup(inksToGroup?: Doc[], isSubGroup?: boolean) {
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 });
})
);
@@ -760,8 +796,8 @@ export function createInkGroup(inksToGroup?: Doc[], isSubGroup?: boolean) {
// 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]();
+ 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
@@ -789,7 +825,7 @@ function setActiveTool(tool: InkTool | GestureUtils.Gestures, keepPrim: boolean,
GestureOverlay.Instance.KeepPrimitiveMode = keepPrim;
}
if (Object.values(GestureUtils.Gestures).includes(tool as any)) {
- if (GestureOverlay.Instance.InkShape === tool) {
+ if (GestureOverlay.Instance.InkShape === tool && !keepPrim) {
Doc.ActiveTool = InkTool.None;
GestureOverlay.Instance.InkShape = undefined;
} else {
@@ -798,7 +834,7 @@ function setActiveTool(tool: InkTool | GestureUtils.Gestures, keepPrim: boolean,
}
} else if (tool) {
// pen or eraser
- if (Doc.ActiveTool === tool && !GestureOverlay.Instance.InkShape) {
+ if (Doc.ActiveTool === tool && !GestureOverlay.Instance.InkShape && !keepPrim) {
Doc.ActiveTool = InkTool.None;
} else {
Doc.ActiveTool = tool as any;
@@ -813,62 +849,39 @@ function setActiveTool(tool: InkTool | GestureUtils.Gestures, keepPrim: boolean,
ScriptingGlobals.add(setActiveTool, 'sets the active ink tool mode');
// toggle: Set overlay status of selected document
-ScriptingGlobals.add(function setIsInkMask(checkResult?: boolean) {
- const selected = SelectionManager.Docs().lastElement();
- if (checkResult) {
- if (selected?.type === DocumentType.INK) {
- return BoolCast(selected.isInkMask) ? Colors.MEDIUM_BLUE : 'transparent';
- }
- return ActiveIsInkMask() ? Colors.MEDIUM_BLUE : 'transparent';
- }
- SetActiveIsInkMask(!ActiveIsInkMask());
- SelectionManager.Docs()
- .filter(doc => doc.type === DocumentType.INK)
- .map(doc => (doc.isInkMask = !doc.isInkMask));
-});
-
-// toggle: Set overlay status of selected document
-ScriptingGlobals.add(function setFillColor(color?: string, checkResult?: boolean) {
+ScriptingGlobals.add(function setInkProperty(option: 'inkMask' | 'fillColor' | 'strokeWidth' | 'strokeColor', value: any, checkResult?: boolean) {
const selected = SelectionManager.Docs().lastElement();
- if (checkResult) {
- if (selected?.type === DocumentType.INK) {
- return StrCast(selected.fillColor);
- }
- return ActiveFillColor();
- }
- SetActiveFillColor(StrCast(color));
- SelectionManager.Docs()
- .filter(doc => doc.type === DocumentType.INK)
- .map(doc => (doc.fillColor = color));
-});
-
-ScriptingGlobals.add(function setStrokeWidth(width: number, checkResult?: boolean) {
- if (checkResult) {
- const selected = SelectionManager.Docs().lastElement();
- if (selected?.type === DocumentType.INK) {
- return NumCast(selected.strokeWidth);
- }
- return ActiveInkWidth();
- }
- SetActiveInkWidth(width.toString());
- SelectionManager.Docs()
- .filter(doc => doc.type === DocumentType.INK)
- .map(doc => (doc.strokeWidth = Number(width)));
-});
+ // 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)),
+ }],
+ ]);
-// toggle: Set overlay status of selected document
-ScriptingGlobals.add(function setStrokeColor(color?: string, checkResult?: boolean) {
if (checkResult) {
- const selected = SelectionManager.Docs().lastElement();
- if (selected?.type === DocumentType.INK) {
- return StrCast(selected.color);
- }
- return ActiveInkColor();
+ return map.get(option)?.checkResult();
}
- SetActiveInkColor(StrCast(color));
+ map.get(option)?.setMode();
SelectionManager.Docs()
.filter(doc => doc.type === DocumentType.INK)
- .map(doc => (doc.color = String(color)));
+ .map(doc => map.get(option)?.setInk(doc));
});
/** WEB
@@ -880,7 +893,7 @@ ScriptingGlobals.add(function webSetURL(url: string, checkResult?: boolean) {
if (checkResult) {
return StrCast(selected.rootDoc.data, Cast(selected.rootDoc.data, WebField, null)?.url?.href);
}
- (selected.ComponentView as WebBox).submitURL(url);
+ selected.ComponentView?.setData?.(url);
//selected.rootDoc.data = new WebField(url);
}
});
@@ -905,23 +918,32 @@ ScriptingGlobals.add(function webBack(checkResult?: boolean) {
ScriptingGlobals.add(function toggleSchemaPreview(checkResult?: boolean) {
const selected = SelectionManager.Docs().lastElement();
if (checkResult && selected) {
- const result: boolean = NumCast(selected.schemaPreviewWidth) > 0;
+ const result: boolean = NumCast(selected.schema_previewWidth) > 0;
if (result) return Colors.MEDIUM_BLUE;
else return 'transparent';
} else if (selected) {
- if (NumCast(selected.schemaPreviewWidth) > 0) {
- selected.schemaPreviewWidth = 0;
+ if (NumCast(selected.schema_previewWidth) > 0) {
+ selected.schema_previewWidth = 0;
} else {
- selected.schemaPreviewWidth = 200;
+ 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));
+ SelectionManager.Docs().map(doc => (doc._text_fontFamily = key));
const editorView = RichTextMenu.Instance.TextView?.EditorView;
if (checkResult) {
return StrCast((editorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily);
diff --git a/src/client/views/nodes/button/colorDropdown/ColorDropdown.tsx b/src/client/views/nodes/button/colorDropdown/ColorDropdown.tsx
index 7f414ddbb..74c3c563c 100644
--- a/src/client/views/nodes/button/colorDropdown/ColorDropdown.tsx
+++ b/src/client/views/nodes/button/colorDropdown/ColorDropdown.tsx
@@ -12,7 +12,7 @@ export class ColorDropdown extends Component<IButtonProps> {
const active: string = StrCast(this.props.rootDoc.dropDownOpen);
const script: string = StrCast(this.props.rootDoc.script);
- const scriptCheck: string = script + "(undefined, true)";
+ const scriptCheck: string = script + '(undefined, true)';
const boolResult = ScriptField.MakeScript(scriptCheck)?.script.run().result;
const stroke: boolean = false;
@@ -24,24 +24,21 @@ export class ColorDropdown extends Component<IButtonProps> {
// 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 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 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 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;
@@ -51,26 +48,30 @@ export class ColorDropdown extends Component<IButtonProps> {
}
};
return (
- <div className={`menuButton ${this.props.type} ${active}`}
+ <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}
+ 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" }} />
+ <div className="colorButton-color" style={{ backgroundColor: boolResult ? boolResult : '#FFFFFF' }} />
{label}
{/* {dropdownCaret} */}
- {this.props.rootDoc.dropDownOpen ?
+ {this.props.rootDoc.dropDownOpen ? (
<div>
- <div className="menuButton-dropdownBox"
- onPointerDown={e => e.stopPropagation()}
- onClick={e => e.stopPropagation()}>
+ <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
+ className="dropbox-background"
+ onClick={e => {
+ e.stopPropagation();
+ this.props.rootDoc.dropDownOpen = false;
+ }}
+ />
</div>
- : null}
+ ) : null}
</div>
);
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/nodes/formattedText/DashDocCommentView.tsx b/src/client/views/nodes/formattedText/DashDocCommentView.tsx
index 85523c176..aa269d8d6 100644
--- a/src/client/views/nodes/formattedText/DashDocCommentView.tsx
+++ b/src/client/views/nodes/formattedText/DashDocCommentView.tsx
@@ -32,19 +32,23 @@ export class DashDocCommentView {
};
this.root = ReactDOM.createRoot(this.dom);
- this.root.render(<DashDocCommentViewInternal view={view} getPos={getPos} docid={node.attrs.docid} />);
+ this.root.render(<DashDocCommentViewInternal view={view} getPos={getPos} docId={node.attrs.docId} />);
(this as any).dom = this.dom;
}
destroy() {
- // ReactDOM.unmountComponentAtNode(this.dom);
+ this.root.unmount();
+ }
+ deselectNode() {
+ this.dom.classList.remove('ProseMirror-selectednode');
+ }
+ selectNode() {
+ this.dom.classList.add('ProseMirror-selectednode');
}
-
- selectNode() {}
}
interface IDashDocCommentViewInternal {
- docid: string;
+ docId: string;
view: any;
getPos: any;
}
@@ -59,13 +63,13 @@ export class DashDocCommentViewInternal extends React.Component<IDashDocCommentV
}
onPointerLeaveCollapsed(e: any) {
- DocServer.GetRefField(this.props.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowUnhighlight());
+ DocServer.GetRefField(this.props.docId).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowUnhighlight());
e.preventDefault();
e.stopPropagation();
}
onPointerEnterCollapsed(e: any) {
- DocServer.GetRefField(this.props.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc, false));
+ DocServer.GetRefField(this.props.docId).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc, false));
e.preventDefault();
e.stopPropagation();
}
@@ -78,7 +82,7 @@ export class DashDocCommentViewInternal extends React.Component<IDashDocCommentV
const tr = this.props.view.state.tr.setNodeMarkup(target.pos, undefined, { ...target.node.attrs, hidden: target.node.attrs.hidden ? false : true });
this.props.view.dispatch(tr.setSelection(TextSelection.create(tr.doc, this.props.getPos() + (expand ? 2 : 1)))); // update the attrs
setTimeout(() => {
- expand && DocServer.GetRefField(this.props.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc));
+ expand && DocServer.GetRefField(this.props.docId).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc));
try {
this.props.view.dispatch(this.props.view.state.tr.setSelection(TextSelection.create(this.props.view.state.tr.doc, this.props.getPos() + (expand ? 2 : 1))));
} catch (e) {}
@@ -96,12 +100,12 @@ export class DashDocCommentViewInternal extends React.Component<IDashDocCommentV
const state = this.props.view.state;
for (let i = this.props.getPos() + 1; i < state.doc.content.size; i++) {
const m = state.doc.nodeAt(i);
- if (m && m.type === state.schema.nodes.dashDoc && m.attrs.docid === this.props.docid) {
+ if (m && m.type === state.schema.nodes.dashDoc && m.attrs.docId === this.props.docId) {
return { node: m, pos: i, hidden: m.attrs.hidden } as { node: any; pos: number; hidden: boolean };
}
}
- const dashDoc = state.schema.nodes.dashDoc.create({ width: 75, height: 35, title: 'dashDoc', docid: this.props.docid, float: 'right' });
+ const dashDoc = state.schema.nodes.dashDoc.create({ width: 75, height: 35, title: 'dashDoc', docId: this.props.docId, float: 'right' });
this.props.view.dispatch(state.tr.insert(this.props.getPos() + 1, dashDoc));
setTimeout(() => {
try {
@@ -115,7 +119,7 @@ export class DashDocCommentViewInternal extends React.Component<IDashDocCommentV
return (
<span
className="formattedTextBox-inlineComment"
- id={'DashDocCommentView-' + this.props.docid}
+ id={'DashDocCommentView-' + this.props.docId}
onPointerLeave={this.onPointerLeaveCollapsed}
onPointerEnter={this.onPointerEnterCollapsed}
onPointerUp={this.onPointerUpCollapsed}
diff --git a/src/client/views/nodes/formattedText/DashDocView.tsx b/src/client/views/nodes/formattedText/DashDocView.tsx
index 63ee7d1f3..48f4c2afd 100644
--- a/src/client/views/nodes/formattedText/DashDocView.tsx
+++ b/src/client/views/nodes/formattedText/DashDocView.tsx
@@ -2,14 +2,15 @@ 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';
import { ColorScheme } from '../../../util/SettingsManager';
import { Transform } from '../../../util/Transform';
-import { DocumentView } from '../DocumentView';
+import { DocFocusOptions, DocumentView } from '../DocumentView';
import { FormattedTextBox } from './FormattedTextBox';
import React = require('react');
@@ -41,18 +42,33 @@ export class DashDocView {
this.root = ReactDOM.createRoot(this.dom);
this.root.render(
- <DashDocViewInternal docid={node.attrs.docid} alias={node.attrs.alias} width={node.attrs.width} height={node.attrs.height} hidden={node.attrs.hidden} fieldKey={node.attrs.fieldKey} tbox={tbox} view={view} node={node} getPos={getPos} />
+ <DashDocViewInternal
+ docId={node.attrs.docId}
+ embedding={node.attrs.embedding}
+ width={node.attrs.width}
+ height={node.attrs.height}
+ hidden={node.attrs.hidden}
+ fieldKey={node.attrs.fieldKey}
+ tbox={tbox}
+ view={view}
+ node={node}
+ getPos={getPos}
+ />
);
}
destroy() {
- // ReactDOM.unmountComponentAtNode(this.dom);
+ setTimeout(() => {
+ try {
+ this.root.unmount();
+ } catch {}
+ });
}
selectNode() {}
}
interface IDashDocViewInternal {
- docid: string;
- alias: string;
+ docId: string;
+ embedding: string;
tbox: FormattedTextBox;
width: string;
height: string;
@@ -69,25 +85,18 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> {
_textBox: FormattedTextBox;
@observable _dashDoc: Doc | undefined;
@observable _finalLayout: any;
- @observable _resolvedDataDoc: any;
@observable _width: number = 0;
@observable _height: number = 0;
updateDoc = action((dashDoc: Doc) => {
this._dashDoc = dashDoc;
- this._finalLayout = this.props.docid ? dashDoc : Doc.expandTemplateLayout(Doc.Layout(dashDoc), dashDoc, this.props.fieldKey);
+ this._finalLayout = dashDoc;
- if (this._finalLayout) {
- if (!Doc.AreProtosEqual(this._finalLayout, dashDoc)) {
- this._finalLayout.rootDocument = dashDoc.aliasOf;
- }
- this._resolvedDataDoc = Cast(this._finalLayout.resolvedDataDoc, Doc, null);
- }
if (this.props.width !== (this._dashDoc?._width ?? '') + 'px' || this.props.height !== (this._dashDoc?._height ?? '') + 'px') {
try {
this._width = NumCast(this._dashDoc?._width);
this._height = NumCast(this._dashDoc?._height);
- // bcz: an exception will be thrown if two aliases are open at the same time when a doc view comment is made
+ // bcz: an exception will be thrown if two embeddings are open at the same time when a doc view comment is made
this.props.view.dispatch(
this.props.view.state.tr.setNodeMarkup(this.props.getPos(), null, {
...this.props.node.attrs,
@@ -105,15 +114,15 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> {
super(props);
this._textBox = this.props.tbox;
- DocServer.GetRefField(this.props.docid + this.props.alias).then(async dashDoc => {
+ DocServer.GetRefField(this.props.docId + this.props.embedding).then(async dashDoc => {
if (!(dashDoc instanceof Doc)) {
- this.props.alias &&
- DocServer.GetRefField(this.props.docid).then(async dashDocBase => {
+ this.props.embedding &&
+ DocServer.GetRefField(this.props.docId).then(async dashDocBase => {
if (dashDocBase instanceof Doc) {
- const aliasedDoc = Doc.MakeAlias(dashDocBase, this.props.docid + this.props.alias);
- aliasedDoc.layoutKey = 'layout';
- this.props.fieldKey && DocUtils.makeCustomViewClicked(aliasedDoc, Docs.Create.StackingDocument, this.props.fieldKey, undefined);
- this.updateDoc(aliasedDoc);
+ const embedding = Doc.MakeEmbedding(dashDocBase, this.props.docId + this.props.embedding);
+ embedding.layout_fieldKey = 'layout';
+ this.props.fieldKey && DocUtils.makeCustomViewClicked(embedding, Docs.Create.StackingDocument, this.props.fieldKey, undefined);
+ this.updateDoc(embedding);
}
});
} else {
@@ -149,7 +158,7 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> {
const { scale, translateX, translateY } = Utils.GetScreenTransform(this._spanRef.current);
return new Transform(-translateX, -translateY, 1).scale(1 / scale);
};
- outerFocus = (target: Doc) => this._textBox.props.focus(this._textBox.props.Document, {}); // ideally, this would scroll to show the focus target
+ outerFocus = (target: Doc, options: DocFocusOptions) => this._textBox.focus(target, options); // ideally, this would scroll to show the focus target
onKeyDown = (e: any) => {
e.stopPropagation();
@@ -159,12 +168,12 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> {
};
onPointerLeave = () => {
- const ele = document.getElementById('DashDocCommentView-' + this.props.docid) as HTMLDivElement;
+ const ele = document.getElementById('DashDocCommentView-' + this.props.docId) as HTMLDivElement;
ele && (ele.style.backgroundColor = '');
};
onPointerEnter = () => {
- const ele = document.getElementById('DashDocCommentView-' + this.props.docid) as HTMLDivElement;
+ const ele = document.getElementById('DashDocCommentView-' + this.props.docId) as HTMLDivElement;
ele && (ele.style.backgroundColor = 'orange');
};
@@ -180,6 +189,8 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> {
height: this._height,
position: 'absolute',
display: 'inline-block',
+ left: 0,
+ top: 0,
}}
onPointerLeave={this.onPointerLeave}
onPointerEnter={this.onPointerEnter}
@@ -189,29 +200,26 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> {
onWheel={e => e.preventDefault()}>
<DocumentView
Document={this._finalLayout}
- DataDoc={this._resolvedDataDoc}
addDocument={returnFalse}
- rootSelected={this._textBox.props.isSelected}
+ rootSelected={returnFalse} //{this._textBox.props.isSelected}
removeDocument={this.removeDoc}
isDocumentActive={returnFalse}
- isContentActive={this._textBox.props.isContentActive}
+ isContentActive={emptyFunction}
styleProvider={this._textBox.props.styleProvider}
docViewPath={this._textBox.props.docViewPath}
ScreenToLocalTransform={this.getDocTransform}
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}
- ContainingCollectionView={this._textBox.props.ContainingCollectionView}
- ContainingCollectionDoc={this._textBox.props.ContainingCollectionDoc}
/>
</div>
);
diff --git a/src/client/views/nodes/formattedText/DashFieldView.scss b/src/client/views/nodes/formattedText/DashFieldView.scss
index c36e6804b..ad315acc8 100644
--- a/src/client/views/nodes/formattedText/DashFieldView.scss
+++ b/src/client/views/nodes/formattedText/DashFieldView.scss
@@ -1,3 +1,5 @@
+@import '../../global/globalCssVariables';
+
.dashFieldView {
position: relative;
display: inline-flex;
@@ -22,7 +24,7 @@
position: relative;
display: inline-block;
font-weight: normal;
- background: rgba(0,0,0,0.1);
+ background: rgba(0, 0, 0, 0.1);
}
.dashFieldView-fieldSpan {
min-width: 8px;
@@ -31,11 +33,13 @@
padding-left: 2px;
display: inline-block;
background-color: rgba(155, 155, 155, 0.24);
- font-weight: bold;
span {
+ user-select: all;
min-width: 100%;
display: inline-block;
}
}
}
- \ No newline at end of file
+.ProseMirror-selectedNode {
+ outline: solid 1px $light-blue !important;
+}
diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx
index f088b39d1..b4fb7a44e 100644
--- a/src/client/views/nodes/formattedText/DashFieldView.tsx
+++ b/src/client/views/nodes/formattedText/DashFieldView.tsx
@@ -1,36 +1,38 @@
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+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, DocListCast, Field } from '../../../../fields/Doc';
+import { Doc } from '../../../../fields/Doc';
import { List } from '../../../../fields/List';
import { listSpec } from '../../../../fields/Schema';
import { SchemaHeaderField } from '../../../../fields/SchemaHeaderField';
-import { ComputedField } from '../../../../fields/ScriptField';
import { Cast, StrCast } from '../../../../fields/Types';
+import { emptyFunction, returnFalse, returnZero, setupMoveUpEvents } from '../../../../Utils';
import { DocServer } from '../../../DocServer';
+import { CollectionViewType } from '../../../documents/DocumentTypes';
+import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu';
+import { SchemaTableCell } from '../../collections/collectionSchema/SchemaTableCell';
+import { OpenWhere } from '../DocumentView';
import './DashFieldView.scss';
import { FormattedTextBox } from './FormattedTextBox';
import React = require('react');
-import { emptyFunction, returnFalse, setupMoveUpEvents } from '../../../../Utils';
-import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu';
-import { Tooltip } from '@material-ui/core';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { CollectionViewType } from '../../../documents/DocumentTypes';
export class DashFieldView {
dom: HTMLDivElement; // container for label and value
root: any;
+ node: any;
+ tbox: FormattedTextBox;
+ unclickable = () => !this.tbox.props.isSelected() && this.node.marks.some((m: any) => m.type === this.tbox.EditorView?.state.schema.marks.linkAnchor && m.attrs.noPreview);
constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) {
- const { boolVal, strVal } = DashFieldViewInternal.fieldContent(tbox.props.Document, tbox.rootDoc, node.attrs.fieldKey);
-
+ this.node = node;
+ this.tbox = tbox;
this.dom = document.createElement('div');
this.dom.style.width = node.attrs.width;
this.dom.style.height = node.attrs.height;
- this.dom.style.fontWeight = 'bold';
this.dom.style.position = 'relative';
this.dom.style.display = 'inline-block';
- this.dom.textContent = node.attrs.fieldKey.startsWith('#') ? node.attrs.fieldKey : node.attrs.fieldKey + ' ' + strVal;
this.dom.onkeypress = function (e: any) {
e.stopPropagation();
};
@@ -45,22 +47,47 @@ export class DashFieldView {
};
this.root = ReactDOM.createRoot(this.dom);
- this.root.render(<DashFieldViewInternal fieldKey={node.attrs.fieldKey} docid={node.attrs.docid} width={node.attrs.width} height={node.attrs.height} hideKey={node.attrs.hideKey} editable={node.attrs.editable} tbox={tbox} />);
+ this.root.render(
+ <DashFieldViewInternal
+ node={node}
+ unclickable={this.unclickable}
+ getPos={getPos}
+ fieldKey={node.attrs.fieldKey}
+ docId={node.attrs.docId}
+ width={node.attrs.width}
+ height={node.attrs.height}
+ hideKey={node.attrs.hideKey}
+ editable={node.attrs.editable}
+ tbox={tbox}
+ />
+ );
}
destroy() {
- //this.root.unmount();
+ setTimeout(() => {
+ try {
+ this.root.unmount();
+ } catch {}
+ });
+ }
+ @action deselectNode() {
+ this.dom.classList.remove('ProseMirror-selectednode');
+ }
+ @action selectNode() {
+ this.dom.classList.add('ProseMirror-selectednode');
}
- selectNode() {}
}
interface IDashFieldViewInternal {
fieldKey: string;
- docid: string;
+ docId: string;
hideKey: boolean;
tbox: FormattedTextBox;
width: number;
height: number;
editable: boolean;
+ node: any;
+ getPos: any;
+ unclickable: () => boolean;
}
@observer
@@ -70,14 +97,15 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna
_fieldKey: string;
_fieldStringRef = React.createRef<HTMLSpanElement>();
@observable _dashDoc: Doc | undefined;
+ @observable _expanded = false;
constructor(props: IDashFieldViewInternal) {
super(props);
this._fieldKey = this.props.fieldKey;
this._textBoxDoc = this.props.tbox.props.Document;
- if (this.props.docid) {
- DocServer.GetRefField(this.props.docid).then(action(async dashDoc => dashDoc instanceof Doc && (this._dashDoc = dashDoc)));
+ if (this.props.docId) {
+ DocServer.GetRefField(this.props.docId).then(action(dashDoc => dashDoc instanceof Doc && (this._dashDoc = dashDoc)));
} else {
this._dashDoc = this.props.tbox.rootDoc;
}
@@ -86,165 +114,46 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna
this._reactionDisposer?.();
}
- public static multiValueDelimeter = ';';
- public static fieldContent(textBoxDoc: Doc, dashDoc: Doc, fieldKey: string) {
- const dashVal = dashDoc[fieldKey] ?? dashDoc[DataSym][fieldKey] ?? (fieldKey === 'PARAMS' ? textBoxDoc[fieldKey] : '');
- const fval = dashVal instanceof List ? dashVal.join(DashFieldViewInternal.multiValueDelimeter) : StrCast(dashVal).startsWith(':=') || dashVal === '' ? Doc.Layout(textBoxDoc)[fieldKey] : dashVal;
- return { boolVal: Cast(fval, 'boolean', null), strVal: Field.toString(fval as Field) || '' };
- }
-
// set the display of the field's value (checkbox for booleans, span of text for strings)
@computed get fieldValueContent() {
- if (this._dashDoc) {
- const { boolVal, strVal } = DashFieldViewInternal.fieldContent(this._textBoxDoc, this._dashDoc, this._fieldKey);
- // field value is a boolean, so use a checkbox or similar widget to display it
- if (boolVal === true || boolVal === false) {
- return (
- <input
- className="dashFieldView-fieldCheck"
- type="checkbox"
- checked={boolVal}
- onChange={e => {
- if (this._fieldKey.startsWith('_')) Doc.Layout(this._textBoxDoc)[this._fieldKey] = e.target.checked;
- Doc.SetInPlace(this._dashDoc!, this._fieldKey, e.target.checked, true);
- }}
- />
- );
- } // field value is a string, so display it as an editable span
- else {
- // bcz: this is unfortunate, but since this React component is nested within a non-React text box (prosemirror), we can't
- // use React events. Essentially, React events occur after native events have been processed, so corresponding React events
- // will never fire because Prosemirror has handled the native events. So we add listeners for native events here.
- return (
- <span
- className="dashFieldView-fieldSpan"
- contentEditable={this.props.editable}
- style={{ display: strVal.length < 2 ? 'inline-block' : undefined }}
- suppressContentEditableWarning={true}
- defaultValue={strVal}
- ref={r => {
- r?.addEventListener('keydown', e => this.fieldSpanKeyDown(e, r));
- r?.addEventListener('blur', e => r && this.updateText(r.textContent!, false));
- r?.addEventListener(
- 'pointerdown',
- action(e => e.stopPropagation())
- );
- }}>
- {strVal}
- </span>
- );
- }
- }
+ return !this._dashDoc ? null : (
+ <div onClick={action(e => (this._expanded = !this.props.editable ? !this._expanded : true))} style={{ fontSize: 'smaller', width: this.props.hideKey ? this.props.tbox.props.PanelWidth() - 20 : undefined }}>
+ <SchemaTableCell
+ Document={this._dashDoc}
+ col={0}
+ deselectCell={emptyFunction}
+ selectCell={emptyFunction}
+ maxWidth={this.props.hideKey ? undefined : () => 100}
+ columnWidth={this.props.hideKey ? () => this.props.tbox.props.PanelWidth() - 20 : returnZero}
+ selectedCell={() => [this._dashDoc!, 0]}
+ fieldKey={this._fieldKey}
+ rowHeight={returnZero}
+ isRowActive={() => this._expanded && this.props.editable}
+ padding={0}
+ getFinfo={emptyFunction}
+ setColumnValues={returnFalse}
+ allowCRs={true}
+ oneLine={!this._expanded}
+ finishEdit={action(() => (this._expanded = false))}
+ />
+ </div>
+ );
}
- // we need to handle all key events on the input span or else they will propagate to prosemirror.
- @action
- fieldSpanKeyDown = (e: KeyboardEvent, span: HTMLSpanElement) => {
- if (e.key === 'Enter') {
- // handle the enter key by "submitting" the current text to Dash's database.
- this.updateText(span.textContent!, true);
- e.preventDefault(); // prevent default to avoid a newline from being generated and wiping out this field view
- }
- if (e.key === 'a' && (e.ctrlKey || e.metaKey)) {
- // handle ctrl-A to select all the text within the span
- if (window.getSelection) {
- const range = document.createRange();
- range.selectNodeContents(span);
- window.getSelection()!.removeAllRanges();
- window.getSelection()!.addRange(range);
- }
- e.preventDefault(); //prevent default so that all the text in the prosemirror text box isn't selected
- }
- e.stopPropagation(); // we need to handle all events or else they will propagate to prosemirror.
- };
-
- @action
- updateText = (nodeText: string, forceMatch: boolean) => {
- if (nodeText) {
- const newText = nodeText.startsWith(':=') || nodeText.startsWith('=:=') ? ':=-computed-' : nodeText;
-
- // const json = {
- // doc: {
- // type: 'doc',
- // content: [
- // {
- // type: 'paragraph',
- // attrs: { align: null, color: null, id: null, indent: null, inset: null, lineSpacing: null, paddingBottom: null, paddingTop: null },
- // content: [{ type: 'dashField', attrs: { fieldKey: 'text', docid: '7e02a420-8add-49b0-ad20-54680567575f', hideKey: true, editable: false }, marks: [{ type: 'strong' }] }],
- // },
- // {
- // type: 'paragraph',
- // attrs: { align: null, color: null, id: null, indent: null, inset: null, lineSpacing: null, paddingBottom: null, paddingTop: null },
- // content: [{ type: 'text', marks: [{ type: 'user_mark', attrs: { userid: 'mart@bar.com', modified: 1667334077 } }] }],
- // },
- // ],
- // },
- // };
-
- // const json = {
- // doc: {
- // type: 'doc',
- // content: [
- // {
- // type: 'paragraph',
- // attrs: { align: null, color: null, id: null, indent: null, inset: null, lineSpacing: null, paddingBottom: null, paddingTop: null },
- // content: [{ type: 'dashField', attrs: { fieldKey: 'hello', docid: '', hideKey: true, editable: true }, marks: [{ type: 'strong' }, { type: 'user_mark', attrs: { userid: 'mart@bar.com', modified: 1667334006 } }] }],
- // },
- // {
- // type: 'paragraph',
- // attrs: { align: null, color: null, id: null, indent: null, inset: null, lineSpacing: null, paddingBottom: null, paddingTop: null },
- // content: [{ type: 'text', marks: [{ type: 'user_mark', attrs: { userid: 'mart@bar.com', modified: 1667334088 } }] }],
- // },
- // ],
- // },
- // };
- // look for a document whose id === the fieldKey being displayed. If there's a match, then that document
- // holds the different enumerated values for the field in the titles of its collected documents.
- // if there's a partial match from the start of the input text, complete the text --- TODO: make this an auto suggest box and select from a drop down.
- DocServer.GetRefField(this._fieldKey).then(options => {
- let modText = '';
- options instanceof Doc && DocListCast(options.data).forEach(opt => (forceMatch ? StrCast(opt.title).startsWith(newText) : StrCast(opt.title) === newText) && (modText = StrCast(opt.title)));
- if (modText) {
- // elementfieldSpan.innerHTML = this._dashDoc![this._fieldKey as string] = modText;
- Doc.SetInPlace(this._dashDoc!, this._fieldKey, modText, true);
- } // if the text starts with a ':=' then treat it as an expression by making a computed field from its value storing it in the key
- else if (nodeText.startsWith(':=')) {
- this._dashDoc![DataSym][this._fieldKey] = ComputedField.MakeFunction(nodeText.substring(2));
- } else if (nodeText.startsWith('=:=')) {
- Doc.Layout(this._textBoxDoc)[this._fieldKey] = ComputedField.MakeFunction(nodeText.substring(3));
- } else {
- if (Number(newText).toString() === newText) {
- if (this._fieldKey.startsWith('_')) Doc.Layout(this._textBoxDoc)[this._fieldKey] = Number(newText);
- Doc.SetInPlace(this._dashDoc!, this._fieldKey, Number(newText), true);
- } else {
- const splits = newText.split(DashFieldViewInternal.multiValueDelimeter);
- if (this._fieldKey !== 'PARAMS' || !this._textBoxDoc[this._fieldKey] || this._dashDoc?.PARAMS) {
- const strVal = splits.length > 1 ? new List<string>(splits) : newText;
- if (this._fieldKey.startsWith('_')) Doc.Layout(this._textBoxDoc)[this._fieldKey] = strVal;
- Doc.SetInPlace(this._dashDoc!, this._fieldKey, strVal, true);
- }
- }
- }
- });
- }
- };
-
createPivotForField = (e: React.MouseEvent) => {
- let container = this.props.tbox.props.ContainingCollectionView;
- while (container?.props.Document.isTemplateForField || container?.props.Document.isTemplateDoc) {
- container = container.props.ContainingCollectionView;
- }
+ let container = this.props.tbox.props.DocumentView?.().props.docViewPath().lastElement();
if (container) {
- const alias = Doc.MakeAlias(container.props.Document);
- alias._viewType = CollectionViewType.Time;
- let list = Cast(alias._columnHeaders, listSpec(SchemaHeaderField));
+ const embedding = Doc.MakeEmbedding(container.rootDoc);
+ embedding._type_collection = CollectionViewType.Time;
+ const colHdrKey = '_' + container.LayoutFieldKey + '_columnHeaders';
+ let list = Cast(embedding[colHdrKey], listSpec(SchemaHeaderField));
if (!list) {
- alias._columnHeaders = list = new List<SchemaHeaderField>();
+ embedding[colHdrKey] = list = new List<SchemaHeaderField>();
}
list.map(c => c.heading).indexOf(this._fieldKey) === -1 && list.push(new SchemaHeaderField(this._fieldKey, '#f1efeb'));
list.map(c => c.heading).indexOf('text') === -1 && list.push(new SchemaHeaderField('text', '#f1efeb'));
- alias._pivotField = this._fieldKey.startsWith('#') ? '#' : this._fieldKey;
- this.props.tbox.props.addDocTab(alias, 'add:right');
+ embedding._pivotField = this._fieldKey.startsWith('#') ? 'tags' : this._fieldKey;
+ this.props.tbox.props.addDocTab(embedding, OpenWhere.addRight);
}
};
@@ -303,12 +212,12 @@ export class DashFieldViewMenu extends AntimodeMenu<AntimodeMenuProps> {
document.addEventListener('pointerdown', hideMenu, true);
};
render() {
- return this.getElement([
+ return this.getElement(
<Tooltip key="trash" title={<div className="dash-tooltip">{`Show Pivot Viewer for '${this._fieldKey}'`}</div>}>
<button className="antimodeMenu-button" onPointerDown={this.showFields}>
<FontAwesomeIcon icon="eye" size="lg" />
</button>
- </Tooltip>,
- ]);
+ </Tooltip>
+ );
}
}
diff --git a/src/client/views/nodes/formattedText/EquationView.tsx b/src/client/views/nodes/formattedText/EquationView.tsx
index 0fd2a7808..5e62d94c2 100644
--- a/src/client/views/nodes/formattedText/EquationView.tsx
+++ b/src/client/views/nodes/formattedText/EquationView.tsx
@@ -1,5 +1,5 @@
import EquationEditor from 'equation-editor-react';
-import { IReactionDisposer } from 'mobx';
+import { IReactionDisposer, trace } from 'mobx';
import { observer } from 'mobx-react';
import { TextSelection } from 'prosemirror-state';
import * as ReactDOM from 'react-dom/client';
@@ -12,7 +12,11 @@ import React = require('react');
export class EquationView {
dom: HTMLDivElement; // container for label and value
root: any;
+ tbox: FormattedTextBox;
+ view: any;
constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) {
+ this.tbox = tbox;
+ this.view = view;
this.dom = document.createElement('div');
this.dom.style.width = node.attrs.width;
this.dom.style.height = node.attrs.height;
@@ -28,13 +32,17 @@ export class EquationView {
_editor: EquationEditor | undefined;
setEditor = (editor?: EquationEditor) => (this._editor = editor);
destroy() {
- // ReactDOM.unmountComponentAtNode(this.dom);
+ this.root.unmount();
}
setSelection() {
this._editor?.mathField.focus();
}
selectNode() {
- this._editor?.mathField.focus();
+ this.tbox._applyingChange = this.tbox.fieldKey; // setting focus will make prosemirror lose focus, which will cause it to change its selection to a text selection, which causes this view to get rebuilt but it's no longer node selected, so the equationview won't have focus
+ setTimeout(() => {
+ this._editor?.mathField.focus();
+ setTimeout(() => (this.tbox._applyingChange = ''));
+ });
}
deselectNode() {}
}
diff --git a/src/client/views/nodes/formattedText/FootnoteView.tsx b/src/client/views/nodes/formattedText/FootnoteView.tsx
index 1683cc972..cf48e1250 100644
--- a/src/client/views/nodes/formattedText/FootnoteView.tsx
+++ b/src/client/views/nodes/formattedText/FootnoteView.tsx
@@ -1,10 +1,10 @@
-import { EditorView } from "prosemirror-view";
-import { EditorState } from "prosemirror-state";
-import { keymap } from "prosemirror-keymap";
-import { baseKeymap, toggleMark } from "prosemirror-commands";
-import { schema } from "./schema_rts";
-import { redo, undo } from "prosemirror-history";
-import { StepMap } from "prosemirror-transform";
+import { EditorView } from 'prosemirror-view';
+import { EditorState } from 'prosemirror-state';
+import { keymap } from 'prosemirror-keymap';
+import { baseKeymap, toggleMark } from 'prosemirror-commands';
+import { schema } from './schema_rts';
+import { redo, undo } from 'prosemirror-history';
+import { StepMap } from 'prosemirror-transform';
export class FootnoteView {
innerView: any;
@@ -20,38 +20,39 @@ export class FootnoteView {
this.getPos = getPos;
// The node's representation in the editor (empty, for now)
- this.dom = document.createElement("footnote");
+ this.dom = document.createElement('footnote');
- this.dom.addEventListener("pointerup", this.toggle, true);
+ this.dom.addEventListener('pointerup', this.toggle, true);
// These are used when the footnote is selected
this.innerView = null;
}
selectNode() {
- this.dom.classList.add("ProseMirror-selectednode");
+ this.dom.classList.add('ProseMirror-selectednode');
if (!this.innerView) this.open();
}
deselectNode() {
- this.dom.classList.remove("ProseMirror-selectednode");
+ this.dom.classList.remove('ProseMirror-selectednode');
if (this.innerView) this.close();
}
open() {
// Append a tooltip to the outer node
- const tooltip = this.dom.appendChild(document.createElement("div"));
- tooltip.className = "footnote-tooltip";
+ const tooltip = this.dom.appendChild(document.createElement('div'));
+ tooltip.className = 'footnote-tooltip';
// And put a sub-ProseMirror into that
this.innerView = new EditorView(tooltip, {
// You can use any node as an editor document
state: EditorState.create({
doc: this.node,
- plugins: [keymap(baseKeymap),
- keymap({
- "Mod-z": () => undo(this.outerView.state, this.outerView.dispatch),
- "Mod-y": () => redo(this.outerView.state, this.outerView.dispatch),
- "Mod-b": toggleMark(schema.marks.strong)
- }),
+ plugins: [
+ keymap(baseKeymap),
+ keymap({
+ 'Mod-z': () => undo(this.outerView.state, this.outerView.dispatch),
+ 'Mod-y': () => redo(this.outerView.state, this.outerView.dispatch),
+ 'Mod-b': toggleMark(schema.marks.strong),
+ }),
// new Plugin({
// view(newView) {
// // TODO -- make this work with RichTextMenu
@@ -59,7 +60,6 @@ export class FootnoteView {
// }
// })
],
-
}),
// This is the magic part
dispatchTransaction: this.dispatchInner.bind(this),
@@ -69,36 +69,37 @@ export class FootnoteView {
// footnote is node-selected (and thus DOM-selected) when
// the parent editor is focused.
e.stopPropagation();
- document.addEventListener("pointerup", this.ignore, true);
+ document.addEventListener('pointerup', this.ignore, true);
if (this.outerView.hasFocus()) this.innerView.focus();
- }) as any
- }
+ }) as any,
+ },
});
setTimeout(() => this.innerView?.docView.setSelection(0, 0, this.innerView.root, true), 0);
}
ignore = (e: PointerEvent) => {
e.stopPropagation();
- document.removeEventListener("pointerup", this.ignore, true);
- }
+ document.removeEventListener('pointerup', this.ignore, true);
+ };
toggle = () => {
if (this.innerView) this.close();
else this.open();
- }
+ };
close() {
this.innerView?.destroy();
this.innerView = null;
- this.dom.textContent = "";
+ this.dom.textContent = '';
}
dispatchInner(tr: any) {
const { state, transactions } = this.innerView.state.applyTransaction(tr);
this.innerView.updateState(state);
- if (!tr.getMeta("fromOutside")) {
- const outerTr = this.outerView.state.tr, offsetMap = StepMap.offset(this.getPos() + 1);
+ if (!tr.getMeta('fromOutside')) {
+ const outerTr = this.outerView.state.tr,
+ offsetMap = StepMap.offset(this.getPos() + 1);
for (const transaction of transactions) {
for (const step of transaction.steps) {
outerTr.step(step.map(offsetMap));
@@ -117,11 +118,11 @@ export class FootnoteView {
if (start !== null) {
let { a: endA, b: endB } = node.content.findDiffEnd(state.doc.content);
const overlap = start - Math.min(endA, endB);
- if (overlap > 0) { endA += overlap; endB += overlap; }
- this.innerView.dispatch(
- state.tr
- .replace(start, endB, node.slice(start, endA))
- .setMeta("fromOutside", true));
+ if (overlap > 0) {
+ endA += overlap;
+ endB += overlap;
+ }
+ this.innerView.dispatch(state.tr.replace(start, endB, node.slice(start, endA)).setMeta('fromOutside', true));
}
}
return true;
@@ -139,4 +140,3 @@ export class FootnoteView {
return true;
}
}
-
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss
index d3d8c47c0..109b62e6f 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.scss
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss
@@ -15,7 +15,7 @@ audiotag {
position: absolute;
cursor: pointer;
border-radius: 10px;
- width: 10px;
+ width: 12px;
margin-top: -2px;
font-size: 4px;
background: lightblue;
@@ -24,6 +24,27 @@ audiotag:hover {
transform: scale(2);
transform-origin: bottom center;
}
+.formattedTextBox {
+ touch-action: none;
+ background: inherit;
+ padding: 0;
+ border-width: 0px;
+ border-radius: inherit;
+ border-color: $medium-gray;
+ box-sizing: border-box;
+ background-color: inherit;
+ border-style: solid;
+ overflow-y: auto;
+ overflow-x: hidden;
+ color: inherit;
+ display: flex;
+ flex-direction: row;
+ transition: opacity 1s;
+ width: 100%;
+ position: relative;
+ top: 0;
+ left: 0;
+}
.formattedTextBox-cont {
touch-action: none;
@@ -51,6 +72,17 @@ audiotag:hover {
position: absolute;
}
}
+.formattedTextBox-alternateButton {
+ align-items: center;
+ flex-direction: column;
+ position: absolute;
+ color: white;
+ background: black;
+ right: 0;
+ bottom: 0;
+ width: 11;
+ height: 11;
+}
.formattedTextBox-outer-selected,
.formattedTextBox-outer {
@@ -149,6 +181,10 @@ audiotag:hover {
}
}
+.gpt-typing-wrapper {
+ padding: 10px;
+}
+
// .menuicon {
// display: inline-block;
// border-right: 1px solid rgba(0, 0, 0, 0.2);
@@ -189,16 +225,15 @@ audiotag:hover {
}
footnote {
- display: inline-block;
+ display: inline-flex;
+ top: -0.5em;
position: relative;
cursor: pointer;
-
- div {
- padding: 0 !important;
- }
+ height: 1em;
+ width: 0.5em;
}
-footnote::after {
+footnote::before {
content: counter(prosemirror-footnote);
vertical-align: super;
font-size: 75%;
@@ -212,19 +247,20 @@ footnote::after {
.footnote-tooltip {
cursor: auto;
font-size: 75%;
- position: absolute;
- left: -30px;
- top: calc(100% + 10px);
+ position: relative;
background: silver;
- padding: 3px;
border-radius: 2px;
- max-width: 100px;
- min-width: 50px;
- width: max-content;
+ min-width: 100px;
+ top: 2em;
+ height: max-content;
+ left: -1em;
+ padding: 3px;
}
.prosemirror-attribution {
font-size: 8px;
+ float: right;
+ display: inline;
}
.footnote-tooltip::before {
@@ -233,8 +269,7 @@ footnote::after {
border-left-color: transparent;
border-right-color: transparent;
position: absolute;
- top: -5px;
- left: 27px;
+ top: -0.5em;
content: ' ';
height: 0;
width: 0;
@@ -728,8 +763,8 @@ footnote::after {
cursor: auto;
font-size: 75%;
position: absolute;
- left: -30px;
- top: calc(100% + 10px);
+ // left: -30px;
+ // top: calc(100% + 10px);
background: silver;
padding: 3px;
border-radius: 2px;
@@ -740,6 +775,8 @@ footnote::after {
.prosemirror-attribution {
font-size: 8px;
+ float: right;
+ display: inline;
}
.footnote-tooltip::before {
@@ -748,8 +785,7 @@ footnote::after {
border-left-color: transparent;
border-right-color: transparent;
position: absolute;
- top: -5px;
- left: 27px;
+ top: -0.5em;
content: ' ';
height: 0;
width: 0;
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index e9f59f17d..da0fc9ffb 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1,7 +1,8 @@
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Tooltip } from '@material-ui/core';
import { isEqual } from 'lodash';
-import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
+import { action, computed, IReactionDisposer, observable, ObservableSet, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import { baseKeymap, selectAll } from 'prosemirror-commands';
import { history } from 'prosemirror-history';
@@ -11,25 +12,31 @@ 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, DataSym, Doc, DocListCast, DocListCastAsync, Field, ForceServerWrite, HeightSym, Opt, UpdatingFromServer, WidthSym } from '../../../../fields/Doc';
+import { Doc, DocListCast, Field, Opt } from '../../../../fields/Doc';
+import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DocCss, ForceServerWrite, Height, UpdatingFromServer, Width } from '../../../../fields/DocSymbols';
import { Id } from '../../../../fields/FieldSymbols';
import { InkTool } from '../../../../fields/InkField';
+import { List } from '../../../../fields/List';
import { PrefetchProxy } from '../../../../fields/Proxy';
import { RichTextField } from '../../../../fields/RichTextField';
import { RichTextUtils } from '../../../../fields/RichTextUtils';
import { ComputedField } from '../../../../fields/ScriptField';
import { BoolCast, Cast, DocCast, FieldValue, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
import { GetEffectiveAcl, TraceMobx } from '../../../../fields/util';
-import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, OmitKeys, returnFalse, returnZero, setupMoveUpEvents, smoothScroll, unimplementedFunction, Utils } from '../../../../Utils';
+import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, returnFalse, returnZero, setupMoveUpEvents, smoothScroll, unimplementedFunction, Utils } from '../../../../Utils';
import { GoogleApiClientUtils, Pulls, Pushes } from '../../../apis/google_docs/GoogleApiClientUtils';
+import { gptAPICall, GPTCallType, gptImageCall } from '../../../apis/gpt/GPT';
import { DocServer } from '../../../DocServer';
import { Docs, DocUtils } from '../../../documents/Documents';
import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
+import { Networking } from '../../../Network';
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';
@@ -45,9 +52,10 @@ import { LightboxView } from '../../LightboxView';
import { AnchorMenu } from '../../pdf/AnchorMenu';
import { SidebarAnnos } from '../../SidebarAnnos';
import { StyleProp } from '../../StyleProvider';
-import { DocumentViewInternal } from '../DocumentView';
+import { DocFocusOptions, DocumentView, DocumentViewInternal, OpenWhere } from '../DocumentView';
import { FieldView, FieldViewProps } from '../FieldView';
import { LinkDocPreview } from '../LinkDocPreview';
+import { PinProps, PresBox } from '../trails';
import { DashDocCommentView } from './DashDocCommentView';
import { DashDocView } from './DashDocView';
import { DashFieldView } from './DashFieldView';
@@ -64,30 +72,31 @@ import { SummaryView } from './SummaryView';
import applyDevTools = require('prosemirror-dev-tools');
import React = require('react');
const translateGoogleApi = require('translate-google-api');
-
export const GoogleRef = 'googleDocId';
-
type PullHandler = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => void;
+export interface FormattedTextBoxProps {
+ allowScroll?: boolean;
+}
@observer
-export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
+export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps & FormattedTextBoxProps>() {
public static LayoutString(fieldStr: string) {
return FieldView.LayoutString(FormattedTextBox, fieldStr);
}
public static blankState = () => EditorState.create(FormattedTextBox.Instance.config);
public static Instance: FormattedTextBox;
public static LiveTextUndo: UndoManager.Batch | undefined;
- static _globalHighlights: string[] = ['Audio Tags', 'Text from Others', 'Todo Items', 'Important Items', 'Disagree Items', 'Ignore Items'];
+ static _globalHighlightsCache: string = '';
+ static _globalHighlights = new ObservableSet<string>(['Audio Tags', 'Text from Others', 'Todo Items', 'Important Items', 'Disagree Items', 'Ignore Items']);
static _highlightStyleSheet: any = addStyleSheet();
static _bulletStyleSheet: any = addStyleSheet();
static _userStyleSheet: any = addStyleSheet();
- static _canAnnotate = true;
static _hadSelection: boolean = false;
private _sidebarRef = React.createRef<SidebarAnnos>();
private _ref: React.RefObject<HTMLDivElement> = React.createRef();
private _scrollRef: React.RefObject<HTMLDivElement> = React.createRef();
private _editorView: Opt<EditorView>;
- private _applyingChange: string = '';
+ public _applyingChange: string = '';
private _searchIndex = 0;
private _lastTimedMark: Mark | undefined = undefined;
private _cachedLinks: Doc[] = [];
@@ -102,7 +111,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
private _rules: RichTextRules | undefined;
private _forceUncollapse = true; // if the cursor doesn't move between clicks, then the selection will disappear for some reason. This flags the 2nd click as happening on a selection which allows bullet points to toggle
private _forceDownNode: Node | undefined;
- private _downEvent: any;
private _downX = 0;
private _downY = 0;
private _break = true;
@@ -111,38 +119,38 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
return this._editorView;
}
public get SidebarKey() {
- return this.fieldKey + '-sidebar';
+ return this.fieldKey + '_sidebar';
}
@computed get allSidebarDocs() {
return DocListCast(this.dataDoc[this.SidebarKey]);
}
@computed get noSidebar() {
- return this.props.docViewPath?.()[this.props.docViewPath().length - 2]?.rootDoc.type === DocumentType.RTF || this.props.noSidebar || this.Document._noSidebar;
+ return this.props.docViewPath().lastElement()?.props.hideDecorationTitle || this.props.noSidebar || this.Document._layout_noSidebar;
}
- @computed get sidebarWidthPercent() {
- return this._showSidebar ? '20%' : StrCast(this.layoutDoc._sidebarWidthPercent, '0%');
+ @computed get layout_sidebarWidthPercent() {
+ 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 autoHeight() {
- return (this.props.forceAutoHeight || this.layoutDoc._autoHeight) && !this.props.ignoreAutoHeight;
+ @computed get layout_autoHeight() {
+ return (this.props.forceAutoHeight || this.layoutDoc._layout_autoHeight) && !this.props.ignoreAutoHeight;
}
@computed get textHeight() {
- return NumCast(this.rootDoc[this.fieldKey + '-height']);
+ return NumCast(this.rootDoc[this.fieldKey + '_height']);
}
@computed get scrollHeight() {
- return NumCast(this.rootDoc[this.fieldKey + '-scrollHeight']);
+ return NumCast(this.rootDoc[this.fieldKey + '_scrollHeight']);
}
@computed get sidebarHeight() {
- return !this.sidebarWidth() ? 0 : NumCast(this.rootDoc[this.SidebarKey + '-height']);
+ return !this.sidebarWidth() ? 0 : NumCast(this.rootDoc[this.SidebarKey + '_height']);
}
@computed get titleHeight() {
return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.HeaderMargin) || 0;
}
- @computed get autoHeightMargins() {
- return this.titleHeight + NumCast(this.layoutDoc._autoHeightMargins);
+ @computed get layout_autoHeightMargins() {
+ return this.titleHeight + NumCast(this.layoutDoc._layout_autoHeightMargins);
}
@computed get _recording() {
return this.dataDoc?.mediaState === 'recording';
@@ -152,7 +160,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
}
@computed get config() {
this._keymap = buildKeymap(schema, this.props);
- this._rules = new RichTextRules(this.props.Document, this);
+ this._rules = new RichTextRules(this.rootDoc, this);
return {
schema,
plugins: [
@@ -171,6 +179,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
};
}
+ // State for GPT
+ @observable
+ private gptRes: string = '';
+
public static PasteOnLoad: ClipboardEvent | undefined;
public static SelectOnLoad = '';
public static DontSelectInitialText = false; // whether initial text should be selected or not
@@ -187,13 +199,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
return '';
}
public static GetDocFromUrl(url: string) {
- return url.startsWith(document.location.origin) ? new URL(url).pathname.split('doc/').lastElement() : ''; // docid
+ return url.startsWith(document.location.origin) ? new URL(url).pathname.split('doc/').lastElement() : ''; // docId
}
constructor(props: any) {
super(props);
FormattedTextBox.Instance = this;
- this.updateHighlights();
this._recordingStart = Date.now();
}
@@ -203,8 +214,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
public RemoveLinkFromDoc(linkDoc?: Doc) {
this.unhighlightSearchTerms();
const state = this._editorView?.state;
- const a1 = linkDoc?.anchor1 as Doc;
- const a2 = linkDoc?.anchor2 as Doc;
+ const a1 = linkDoc?.link_anchor_1 as Doc;
+ const a2 = linkDoc?.link_anchor_2 as Doc;
if (state && a1 && a2 && this._editorView) {
this.removeDocument(a1);
this.removeDocument(a2);
@@ -231,31 +242,40 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
}
}
- getAnchor = () => this.makeLinkAnchor(undefined, 'add:right', undefined, 'Anchored Selection');
+ getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
+ if (!pinProps && this._editorView?.state.selection.empty) return this.rootDoc;
+ const anchor = Docs.Create.ConfigDocument({ title: StrCast(this.rootDoc.title), annotationOn: this.rootDoc });
+ this.addDocument(anchor);
+ this.makeLinkAnchor(anchor, OpenWhere.addRight, undefined, 'Anchored Selection', false, addAsAnnotation);
+ PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), scrollable: true } }, this.rootDoc);
+ return anchor;
+ };
@action
setupAnchorMenu = () => {
AnchorMenu.Instance.Status = 'marquee';
AnchorMenu.Instance.OnClick = (e: PointerEvent) => {
- !this.layoutDoc.showSidebar && this.toggleSidebar();
- this._sidebarRef.current?.anchorMenuClick(this.getAnchor());
+ !this.layoutDoc.layout_showSidebar && this.toggleSidebar();
+ setTimeout(() => this._sidebarRef.current?.anchorMenuClick(this.makeLinkAnchor(undefined, OpenWhere.addRight, undefined, 'Anchored Selection', true))); // give time for sidebarRef to be created
};
AnchorMenu.Instance.OnAudio = (e: PointerEvent) => {
- !this.layoutDoc.showSidebar && this.toggleSidebar();
- const anchor = this.getAnchor();
- const target = this._sidebarRef.current?.anchorMenuClick(anchor);
- if (target) {
- anchor.followLinkAudio = true;
- DocumentViewInternal.recordAudioAnnotation(Doc.GetProto(target), Doc.LayoutFieldKey(target));
- target.title = ComputedField.MakeFunction(`self["text-audioAnnotations-text"].lastElement()`);
- }
+ !this.layoutDoc.layout_showSidebar && this.toggleSidebar();
+ const anchor = this.makeLinkAnchor(undefined, OpenWhere.addRight, undefined, 'Anchored Selection', true, true);
+ setTimeout(() => {
+ const target = this._sidebarRef.current?.anchorMenuClick(anchor);
+ if (target) {
+ anchor.followLinkAudio = true;
+ DocumentViewInternal.recordAudioAnnotation(Doc.GetProto(target), Doc.LayoutFieldKey(target));
+ target.title = ComputedField.MakeFunction(`self["text_audioAnnotations_text"].lastElement()`);
+ }
+ });
};
AnchorMenu.Instance.Highlight = action((color: string, isLinkButton: boolean) => {
- this._editorView?.state && RichTextMenu.Instance.setHighlight(color, this._editorView, this._editorView?.dispatch);
+ this._editorView?.state && RichTextMenu.Instance.setHighlight(color);
return undefined;
});
- AnchorMenu.Instance.onMakeAnchor = this.getAnchor;
+ AnchorMenu.Instance.onMakeAnchor = () => this.getAnchor(true);
AnchorMenu.Instance.StartCropDrag = unimplementedFunction;
/**
* This function is used by the PDFmenu to create an anchor highlight and a new linked text annotation.
@@ -270,23 +290,23 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
return target;
};
- DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.props.docViewPath().lastElement(), this.getAnchor, targetCreator), e.pageX, e.pageY);
+ DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.props.docViewPath().lastElement(), () => this.getAnchor(true), targetCreator), e.pageX, e.pageY);
});
const coordsB = this._editorView!.coordsAtPos(this._editorView!.state.selection.to);
this.props.isSelected(true) && AnchorMenu.Instance.jumpTo(coordsB.left, coordsB.bottom);
};
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 prevLayoutData = this.rootDoc !== this.layoutDoc ? Cast(this.layoutDoc[this.fieldKey], RichTextField, null) : 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"'));
@@ -298,35 +318,28 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
accumTags.push(node.attrs.fieldKey);
}
});
- const curTags = Object.keys(dataDoc).filter(key => key.startsWith('#'));
- const added = accumTags.filter(tag => !curTags.includes(tag));
- const removed = curTags.filter(tag => !accumTags.includes(tag));
- removed.forEach(r => (dataDoc[r] = undefined));
- added.forEach(a => (dataDoc[a] = a));
+ 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;
- curText !== Cast(dataDoc[this.fieldKey], RichTextField)?.Text && (dataDoc[this.fieldKey + '-lastModified'] = new DateField(new Date(Date.now())));
- if ((!curTemp && !curProto) || curText || json.includes('dash')) {
+ const textChange = newText !== prevData?.Text;
+ textChange && (dataDoc[this.fieldKey + '_modificationDate'] = new DateField(new Date(Date.now())));
+ if ((!prevData && !protoData) || newText || (!newText && !protoData)) {
// if no template, or there's text that didn't come from the layout template, write it to the document. (if this is driven by a template, then this overwrites the template text which is intended)
- if (removeSelection(json) !== removeSelection(curLayout?.Data)) {
+ if (removeSelection(newJson) !== removeSelection(prevLayoutData?.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
- 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; //(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: 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)));
- 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 });
+ 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: newText });
unchanged = false;
}
this._applyingChange = '';
@@ -354,8 +367,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
let linkTime;
let linkAnchor;
let link;
- DocListCast(this.dataDoc.links).forEach((l, i) => {
- const anchor = (l.anchor1 as Doc).annotationOn ? (l.anchor1 as Doc) : (l.anchor2 as Doc).annotationOn ? (l.anchor2 as Doc) : undefined;
+ LinkManager.Links(this.dataDoc).forEach((l, i) => {
+ const anchor = (l.link_anchor_1 as Doc).annotationOn ? (l.link_anchor_1 as Doc) : (l.link_anchor_2 as Doc).annotationOn ? (l.link_anchor_2 as Doc) : undefined;
if (anchor && (anchor.annotationOn as Doc).mediaState === 'recording') {
linkTime = NumCast(anchor._timecodeToShow /* audioStart */);
linkAnchor = anchor;
@@ -391,18 +404,19 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
autoLink = () => {
const newAutoLinks = new Set<Doc>();
- const oldAutoLinks = DocListCast(this.props.Document.links).filter(link => link.linkRelationship === LinkManager.AutoKeywords);
+ const oldAutoLinks = LinkManager.Links(this.props.Document).filter(link => link.link_relationship === LinkManager.AutoKeywords);
if (this._editorView?.state.doc.textContent) {
+ const isNodeSel = this._editorView.state.selection instanceof NodeSelection;
const f = this._editorView.state.selection.from;
const t = this._editorView.state.selection.to;
var tr = this._editorView.state.tr as any;
const autoAnch = this._editorView.state.schema.marks.autoLinkAnchor;
tr = tr.removeMark(0, tr.doc.content.size, autoAnch);
DocListCast(Doc.MyPublishedDocs.data).forEach(term => (tr = this.hyperlinkTerm(tr, term, newAutoLinks)));
- tr = tr.setSelection(new TextSelection(tr.doc.resolve(f), tr.doc.resolve(t)));
+ 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);
}
- oldAutoLinks.filter(oldLink => !newAutoLinks.has(oldLink) && oldLink.anchor2 !== this.rootDoc).forEach(LinkManager.Instance.deleteLink);
+ oldAutoLinks.filter(oldLink => !newAutoLinks.has(oldLink) && oldLink.link_anchor_2 !== this.rootDoc).forEach(LinkManager.Instance.deleteLink);
};
updateTitle = () => {
@@ -411,7 +425,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
!this.props.dontRegisterView && // (this.props.Document.isTemplateForField === "text" || !this.props.Document.isTemplateForField) && // only update the title if the data document's data field is changing
(title.startsWith('-') || title.startsWith('@')) &&
this._editorView &&
- !this.dataDoc['title-custom'] &&
+ !this.dataDoc.title_custom &&
(Doc.LayoutFieldKey(this.rootDoc) === this.fieldKey || this.fieldKey === 'text')
) {
let node = this._editorView.state.doc;
@@ -443,8 +457,8 @@ 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 ??
- (DocListCast(this.Document.links).find(link => Doc.AreProtosEqual(Cast(link.anchor1, Doc, null), this.rootDoc) && Doc.AreProtosEqual(Cast(link.anchor2, Doc, null), target)) ||
- DocUtils.MakeLink({ doc: this.props.Document }, { doc: target }, LinkManager.AutoKeywords)!);
+ (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 })!);
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 ?? []));
@@ -498,12 +512,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
const end = this._editorView.state.doc.nodeSize - 2;
this._editorView.dispatch(this._editorView.state.tr.removeMark(0, end, mark).removeMark(0, end, activeMark));
}
- if (FormattedTextBox.PasteOnLoad) {
- const pdfDocId = FormattedTextBox.PasteOnLoad.clipboardData?.getData('dash/pdfOrigin');
- const pdfRegionId = FormattedTextBox.PasteOnLoad.clipboardData?.getData('dash/pdfRegion');
- FormattedTextBox.PasteOnLoad = undefined;
- setTimeout(() => pdfDocId && pdfRegionId && this.addPdfReference(pdfDocId, pdfRegionId, undefined), 10);
- }
};
adoptAnnotation = (start: number, end: number, mark: Mark) => {
const view = this._editorView!;
@@ -517,41 +525,56 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
this.setupEditor(this.config, this.fieldKey);
this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.layoutDoc);
}
- // if (this.autoHeight) this.tryUpdateScrollHeight();
+ // if (this.layout_autoHeight) this.tryUpdateScrollHeight();
};
@undoBatch
@action
drop = (e: Event, de: DragManager.DropEvent) => {
- if (de.complete.annoDragData) de.complete.annoDragData.dropDocCreator = this.getAnchor;
+ 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];
- target._fitContentsToBox = true;
- const node = schema.nodes.dashDoc.create({
- width: target[WidthSym](),
- height: target[HeightSym](),
- title: 'dashDoc',
- docid: target[Id],
- float: 'unset',
- });
- if (!['alias', 'copy'].includes((dragData.dropAction ?? '') as any)) {
- dragData.removeDocument?.(dragData.draggedDocuments[0]);
+ const dataDoc = Doc.IsDelegateField(DocCast(this.layoutDoc.proto), this.fieldKey) ? DocCast(this.layoutDoc.proto) : this.dataDoc;
+ const effectiveAcl = GetEffectiveAcl(dataDoc);
+ let added = [AclEdit, AclAdmin, AclSelfEdit].includes(effectiveAcl);
+ const draggedDoc = dragData.draggedDocuments.lastElement();
+ if (added) {
+ // replace text contents when dragging with Alt
+ if (de.altKey) {
+ const fieldKey = Doc.LayoutFieldKey(draggedDoc);
+ if (draggedDoc[fieldKey] instanceof RichTextField && !Doc.AreProtosEqual(draggedDoc, this.props.Document)) {
+ Doc.GetProto(this.dataDoc)[this.fieldKey] = Field.Copy(draggedDoc[fieldKey]);
+ }
+
+ // embed document when drag marked as embed
+ } else if (de.embedKey) {
+ const node = schema.nodes.dashDoc.create({
+ width: draggedDoc[Width](),
+ height: draggedDoc[Height](),
+ title: 'dashDoc',
+ docId: draggedDoc[Id],
+ float: 'unset',
+ });
+ if (!['embed', 'copy'].includes((dragData.dropAction ?? '') as any)) {
+ added = dragData.removeDocument?.(draggedDoc) ? true : false;
+ }
+ if (added) {
+ draggedDoc._freeform_fitContentsToBox = true;
+ draggedDoc.embedContainer = this.rootDoc;
+ const view = this._editorView!;
+ view.dispatch(view.state.tr.insert(view.posAtCoords({ left: de.x, top: de.y })!.pos, node));
+ }
}
- 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 {
@@ -599,62 +622,63 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
return ret;
}
- updateHighlights = () => {
- const highlights = FormattedTextBox._globalHighlights;
+ updateHighlights = (highlights: string[]) => {
+ if (Array.from(highlights).join('') === FormattedTextBox._globalHighlightsCache) return;
+ setTimeout(() => (FormattedTextBox._globalHighlightsCache = Array.from(highlights).join('')));
clearStyleSheetRules(FormattedTextBox._userStyleSheet);
- if (highlights.indexOf('Audio Tags') === -1) {
+ if (!highlights.includes('Audio Tags')) {
addStyleSheetRule(FormattedTextBox._userStyleSheet, 'audiotag', { display: 'none' }, '');
}
- if (highlights.indexOf('Text from Others') !== -1) {
+ if (highlights.includes('Text from Others')) {
addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-remote', { background: 'yellow' });
}
- if (highlights.indexOf('My Text') !== -1) {
+ if (highlights.includes('My Text')) {
addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-' + Doc.CurrentUserEmail.replace('.', '').replace('@', ''), { background: 'moccasin' });
}
- if (highlights.indexOf('Todo Items') !== -1) {
+ if (highlights.includes('Todo Items')) {
addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UT-todo', { outline: 'black solid 1px' });
}
- if (highlights.indexOf('Important Items') !== -1) {
+ if (highlights.includes('Important Items')) {
addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UT-important', { 'font-size': 'larger' });
}
- if (highlights.indexOf('Bold Text') !== -1) {
- addStyleSheetRule(FormattedTextBox._userStyleSheet, '.formattedTextBox-inner-selected .ProseMirror strong > span', { 'font-size': 'large' }, '');
- addStyleSheetRule(FormattedTextBox._userStyleSheet, '.formattedTextBox-inner-selected .ProseMirror :not(strong > span)', { 'font-size': '0px' }, '');
+ if (highlights.includes('Bold Text')) {
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, '.formattedTextBox-inner .ProseMirror strong > span', { 'font-size': 'large' }, '');
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, '.formattedTextBox-inner .ProseMirror :not(strong > span)', { 'font-size': '0px' }, '');
}
- if (highlights.indexOf('Disagree Items') !== -1) {
+ if (highlights.includes('Disagree Items')) {
addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UT-disagree', { 'text-decoration': 'line-through' });
}
- if (highlights.indexOf('Ignore Items') !== -1) {
+ if (highlights.includes('Ignore Items')) {
addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UT-ignore', { 'font-size': '1' });
}
- if (highlights.indexOf('By Recent Minute') !== -1) {
+ if (highlights.includes('By Recent Minute')) {
addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-' + Doc.CurrentUserEmail.replace('.', '').replace('@', ''), { opacity: '0.1' });
const min = Math.round(Date.now() / 1000 / 60);
numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-min-' + (min - i), { opacity: ((10 - i - 1) / 10).toString() }));
- setTimeout(this.updateHighlights);
}
- if (highlights.indexOf('By Recent Hour') !== -1) {
+ if (highlights.includes('By Recent Hour')) {
addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-' + Doc.CurrentUserEmail.replace('.', '').replace('@', ''), { opacity: '0.1' });
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[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;
@computed get SidebarShown() {
- return this._showSidebar || this.layoutDoc._showSidebar ? true : false;
+ return this._showSidebar || this.layoutDoc._layout_showSidebar ? true : false;
}
@action
toggleSidebar = (preview: boolean = false) => {
const prevWidth = 1 - this.sidebarWidth() / Number(getComputedStyle(this._ref.current!).width.replace('px', ''));
if (preview) this._showSidebar = true;
- else this.layoutDoc._showSidebar = (this.layoutDoc._sidebarWidthPercent = StrCast(this.layoutDoc._sidebarWidthPercent, '0%') === '0%' ? '50%' : '0%') !== '0%';
+ else this.layoutDoc._layout_showSidebar = (this.layoutDoc._layout_sidebarWidthPercent = StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%') === '0%' ? '50%' : '0%') !== '0%';
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,
@@ -672,31 +696,41 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
.ScreenToLocalTransform()
.scale(this.props.NativeDimScaling?.() || 1)
.transformDirection(delta[0], delta[1]);
- const sidebarWidth = (NumCast(this.layoutDoc._width) * Number(this.sidebarWidthPercent.replace('%', ''))) / 100;
- const width = this.layoutDoc[WidthSym]() + localDelta[0];
- this.layoutDoc._sidebarWidthPercent = Math.max(0, (sidebarWidth + localDelta[0]) / width) * 100 + '%';
+ const sidebarWidth = (NumCast(this.layoutDoc._width) * Number(this.layout_sidebarWidthPercent.replace('%', ''))) / 100;
+ 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._showSidebar = this.layoutDoc._sidebarWidthPercent !== '0%';
+ this.layoutDoc._layout_showSidebar = this.layoutDoc._layout_sidebarWidthPercent !== '0%';
e.preventDefault();
return false;
};
- @undoBatch
deleteAnnotation = (anchor: Doc) => {
- LinkManager.Instance.deleteLink(DocListCast(anchor.links)[0]);
+ 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
- pinToPres = (anchor: Doc) => this.props.pinToPres(anchor);
+ pinToPres = (anchor: Doc) => this.props.pinToPres(anchor, {});
@undoBatch
- makePushpin = (anchor: Doc) => (anchor.isPushpin = !anchor.isPushpin);
+ makeTargetToggle = (anchor: Doc) => (anchor.followLinkToggle = !anchor.followLinkToggle);
+
+ @undoBatch
+ showTargetTrail = (anchor: Doc) => {
+ const trail = DocCast(anchor.presTrail);
+ if (trail) {
+ Doc.ActivePresentation = trail;
+ this.props.addDocTab(trail, OpenWhere.replaceRight);
+ }
+ };
- isPushpin = (anchor: Doc) => BoolCast(anchor.isPushpin);
+ isTargetToggler = (anchor: Doc) => BoolCast(anchor.followLinkToggle);
specificContextMenu = (e: React.MouseEvent): void => {
const cm = ContextMenu.Instance;
@@ -711,16 +745,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.MakePushpin = () => this.makePushpin(anchor as Doc);
- AnchorMenu.Instance.IsPushpin = () => this.isPushpin(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);
})
);
@@ -734,7 +774,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
description: 'plain',
event: undoBatch(() => {
Doc.setNativeView(this.rootDoc);
- this.layoutDoc.autoHeightMargins = undefined;
+ this.layoutDoc.layout_autoHeightMargins = undefined;
}),
icon: 'eye',
});
@@ -742,18 +782,18 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
description: 'metadata',
event: undoBatch(() => {
this.dataDoc.layout_meta = Cast(Doc.UserDoc().emptyHeader, Doc, null)?.layout;
- this.rootDoc.layoutKey = 'layout_meta';
- setTimeout(() => (this.rootDoc._headerHeight = this.rootDoc._autoHeightMargins = 50), 50);
+ this.rootDoc.layout_fieldKey = 'layout_meta';
+ setTimeout(() => (this.rootDoc._headerHeight = this.rootDoc._layout_autoHeightMargins = 50), 50);
}),
icon: 'eye',
});
- const noteTypesDoc = Cast(Doc.UserDoc()['template-notes'], Doc, null);
+ const noteTypesDoc = Cast(Doc.UserDoc().template_notes, Doc, null);
DocListCast(noteTypesDoc?.data).forEach(note => {
const icon: IconProp = StrCast(note.icon) as IconProp;
changeItems.push({
description: StrCast(note.title),
event: undoBatch(() => {
- this.layoutDoc.autoHeightMargins = undefined;
+ this.layoutDoc.layout_autoHeightMargins = undefined;
Doc.setNativeView(this.rootDoc);
DocUtils.makeCustomViewClicked(this.rootDoc, Docs.Create.TreeDocument, StrCast(note.title), note);
}),
@@ -765,24 +805,30 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
const expertHighlighting = [...noviceHighlighting, 'Important Items', 'Ignore Items', 'Disagree Items', 'By Recent Minute', 'By Recent Hour'];
(Doc.noviceMode ? noviceHighlighting : expertHighlighting).forEach(option =>
highlighting.push({
- description: (FormattedTextBox._globalHighlights.indexOf(option) === -1 ? 'Highlight ' : 'Unhighlight ') + option,
- event: () => {
+ description: (!FormattedTextBox._globalHighlights.has(option) ? 'Highlight ' : 'Unhighlight ') + option,
+ event: action(() => {
e.stopPropagation();
- if (FormattedTextBox._globalHighlights.indexOf(option) === -1) {
- FormattedTextBox._globalHighlights.push(option);
+ if (!FormattedTextBox._globalHighlights.has(option)) {
+ FormattedTextBox._globalHighlights.add(option);
} else {
- FormattedTextBox._globalHighlights.splice(FormattedTextBox._globalHighlights.indexOf(option), 1);
+ FormattedTextBox._globalHighlights.delete(option);
}
- runInAction(() => (this.layoutDoc._highlights = FormattedTextBox._globalHighlights.join('')));
- this.updateHighlights();
- },
- icon: 'expand-arrows-alt',
+ }),
+ icon: !FormattedTextBox._globalHighlights.has(option) ? 'highlighter' : 'remove-format',
})
);
const uicontrols: ContextMenuProps[] = [];
- !Doc.noviceMode && uicontrols.push({ description: `${FormattedTextBox._canAnnotate ? "Don't" : ''} Show Menu on Selections`, event: () => (FormattedTextBox._canAnnotate = !FormattedTextBox._canAnnotate), icon: 'expand-arrows-alt' });
- uicontrols.push({ description: !this.Document._noSidebar ? 'Hide Sidebar Handle' : 'Show Sidebar Handle', event: () => (this.layoutDoc._noSidebar = !this.layoutDoc._noSidebar), icon: 'expand-arrows-alt' });
+ uicontrols.push({
+ description: !this.Document._layout_noSidebar ? 'Hide Sidebar Handle' : 'Show Sidebar Handle',
+ event: () => (this.layoutDoc._layout_noSidebar = !this.layoutDoc._layout_noSidebar),
+ icon: !this.Document._layout_noSidebar ? 'eye-slash' : 'eye',
+ });
+ uicontrols.push({
+ 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 &&
uicontrols.push({
@@ -812,10 +858,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
this.rootDoc.title = this.layoutDoc.isTemplateForField as string;
this.rootDoc.isTemplateDoc = false;
this.rootDoc.isTemplateForField = '';
- this.rootDoc.layoutKey = 'layout';
+ this.rootDoc.layout_fieldKey = 'layout';
MakeTemplate(this.rootDoc, true, title);
setTimeout(() => {
- this.rootDoc._autoHeight = this.layoutDoc._autoHeight; // autoHeight, width and height
+ this.rootDoc._layout_autoHeight = this.layoutDoc._layout_autoHeight; // layout_autoHeight, width and height
this.rootDoc._width = this.layoutDoc._width || 300; // are stored on the template, since we're getting rid of the old template
this.rootDoc._height = this.layoutDoc._height || 200; // we need to copy them over to the root. This should probably apply to all '_' fields
this.rootDoc._backgroundColor = Cast(this.layoutDoc._backgroundColor, 'string', null);
@@ -823,7 +869,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
}, 10);
}
Doc.UserDoc().defaultTextLayout = new PrefetchProxy(this.rootDoc);
- Doc.AddDocToList(Cast(Doc.UserDoc()['template-notes'], Doc, null), 'data', this.rootDoc);
+ Doc.AddDocToList(Cast(Doc.UserDoc().template_notes, Doc, null), 'data', this.rootDoc);
},
icon: 'eye',
});
@@ -831,12 +877,77 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
const options = cm.findByDescription('Options...');
const optionItems = options && 'subitems' in options ? options.subitems : [];
- optionItems.push({ description: !this.Document._singleLine ? 'Make Single Line' : 'Make Multi Line', event: () => (this.layoutDoc._singleLine = !this.layoutDoc._singleLine), icon: 'expand-arrows-alt' });
- optionItems.push({ description: `${this.Document._autoHeight ? 'Lock' : 'Auto'} Height`, event: () => (this.layoutDoc._autoHeight = !this.layoutDoc._autoHeight), icon: 'plus' });
+ 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',
+ });
+ !Doc.noviceMode &&
+ optionItems.push({
+ description: `${this.Document._layout_autoHeight ? 'Lock' : 'Auto'} Height`,
+ event: () => (this.layoutDoc._layout_autoHeight = !this.layoutDoc._layout_autoHeight),
+ icon: this.Document._layout_autoHeight ? 'lock' : 'unlock',
+ });
+ optionItems.push({ description: `show markdown options`, event: RTFMarkup.Instance.open, icon: 'text' });
!options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'eye' });
this._downX = this._downY = Number.NaN;
};
+ animateRes = (resIndex: number) => {
+ if (resIndex < this.gptRes.length) {
+ this.dataDoc.text = (this.dataDoc.text as RichTextField)?.Text + this.gptRes[resIndex];
+ setTimeout(() => {
+ this.animateRes(resIndex + 1);
+ }, 20);
+ }
+ };
+
+ askGPT = action(async () => {
+ try {
+ let res = await gptAPICall((this.dataDoc.text as RichTextField)?.Text, GPTCallType.COMPLETION);
+ if (res) {
+ this.gptRes = res;
+ this.animateRes(0);
+ }
+ } catch (err) {
+ console.log(err);
+ this.dataDoc.text = (this.dataDoc.text as RichTextField)?.Text + 'Something went wrong';
+ }
+ });
+
+ generateImage = async () => {
+ console.log('Generate image from text: ', (this.dataDoc.text as RichTextField)?.Text);
+ try {
+ 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 newDoc = Docs.Create.ImageDocument(source, {
+ x: NumCast(this.rootDoc.x) + NumCast(this.layoutDoc._width) + 10,
+ y: NumCast(this.rootDoc.y),
+ _height: 200,
+ _width: 200,
+ data_nativeWidth: result.nativeWidth,
+ data_nativeHeight: result.nativeHeight,
+ });
+ if (Doc.IsInMyOverlay(this.rootDoc)) {
+ newDoc.overlayX = this.rootDoc.x;
+ newDoc.overlayY = NumCast(this.rootDoc.y) + NumCast(this.rootDoc._height);
+ Doc.AddToMyOverlay(newDoc);
+ } else {
+ this.props.addDocument?.(newDoc);
+ }
+ // Create link between prompt and image
+ DocUtils.MakeLink(this.rootDoc, newDoc, { link_relationship: 'Image Prompt' });
+ }
+ } catch (err) {
+ console.log(err);
+ return '';
+ }
+ };
+
breakupDictation = () => {
if (this._editorView && this._recording) {
this.stopDictation(true);
@@ -866,14 +977,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
if (this._editorView && this._recordingStart) {
if (this._break) {
const textanchorFunc = () => {
- const tanch = Docs.Create.TextanchorDocument({ title: 'dictation anchor' });
+ const tanch = Docs.Create.ConfigDocument({ title: 'dictation anchor' });
return this.addDocument(tanch) ? tanch : undefined;
};
const link = DocUtils.MakeLinkToActiveAudio(textanchorFunc, false).lastElement();
if (link) {
Doc.GetProto(link).isDictation = true;
- const audioanchor = Cast(link.anchor2, Doc, null);
- const textanchor = Cast(link.anchor1, Doc, null);
+ const audioanchor = Cast(link.link_anchor_2, Doc, null);
+ const textanchor = Cast(link.link_anchor_1, Doc, null);
if (audioanchor) {
audioanchor.backgroundColor = 'tan';
const audiotag = this._editorView.state.schema.nodes.audiotag.create({
@@ -896,27 +1007,37 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
};
// TODO: nda -- Look at how link anchors are added
- makeLinkAnchor(anchorDoc?: Doc, location?: string, targetHref?: string, title?: string) {
+ makeLinkAnchor(anchorDoc?: Doc, location?: string, targetHref?: string, title?: string, noPreview?: boolean, addAsAnnotation?: boolean) {
const state = this._editorView?.state;
if (state) {
+ let selectedText = '';
const sel = state.selection;
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) this.addDocument(anchor);
+ if (anchor !== anchorDoc && addAsAnnotation) this.addDocument(anchor);
tr.doc.nodesBetween(sel.from, sel.to, (node: any, pos: number, parent: any) => {
if (node.firstChild === null && node.marks.find((m: Mark) => m.type.name === schema.marks.splitter.name)) {
const allAnchors = [{ href, title, anchorId: anchor[Id] }];
allAnchors.push(...(node.marks.find((m: Mark) => m.type.name === schema.marks.linkAnchor.name)?.attrs.allAnchors ?? []));
- const link = state.schema.marks.linkAnchor.create({ allAnchors, title, location });
+ const link = state.schema.marks.linkAnchor.create({ allAnchors, title, location, noPreview });
tr = tr.addMark(pos, pos + node.nodeSize, link);
+ selectedText += (node as Node).textContent;
}
});
this.dataDoc[ForceServerWrite] = this.dataDoc[UpdatingFromServer] = true; // need to allow permissions for adding links to readonly/augment only documents
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;
@@ -924,19 +1045,22 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
return anchorDoc ?? this.rootDoc;
}
- scrollFocus = (textAnchor: Doc, smooth: boolean) => {
- let didToggle = false;
- if (DocListCast(this.Document[this.fieldKey + '-sidebar']).includes(textAnchor) && !this.SidebarShown) {
- this.toggleSidebar(!smooth);
- didToggle = true;
+ getView = async (doc: Doc) => {
+ 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));
}
+ return new Promise<Opt<DocumentView>>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv)));
+ };
+ focus = (textAnchor: Doc, options: DocFocusOptions) => {
+ const focusSpeed = options.zoomTime ?? 500;
const textAnchorId = textAnchor[Id];
const findAnchorFrag = (frag: Fragment, editor: EditorView) => {
const nodes: Node[] = [];
let hadStart = start !== 0;
frag.forEach((node, index) => {
const examinedNode = findAnchorNode(node, editor);
- if (examinedNode?.node && (examinedNode.node.textContent || examinedNode.node.type === this._editorView?.state.schema.nodes.audiotag)) {
+ if (examinedNode?.node && (examinedNode.node.textContent || examinedNode.node.type === this._editorView?.state.schema.nodes.dashDoc || examinedNode.node.type === this._editorView?.state.schema.nodes.audiotag)) {
nodes.push(examinedNode.node);
!hadStart && (start = index + examinedNode.start);
hadStart = true;
@@ -951,9 +1075,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
}
return undefined;
}
+ if (node.type === this._editorView?.state.schema.nodes.dashDoc) {
+ if (node.attrs.docId === textAnchorId) {
+ return { node, start: 0 };
+ }
+ return undefined;
+ }
if (!node.isText) {
const content = findAnchorFrag(node.content, editor);
- return { node: node.copy(content.frag), start: content.start };
+ if (content.frag.childCount) return { node: content.frag.childCount ? content.frag.child(0) : node, start: content.start };
}
const marks = [...node.marks];
const linkIndex = marks.findIndex(mark => mark.type === editor.state.schema.marks.linkAnchor);
@@ -967,8 +1097,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
const ret = findAnchorFrag(editor.state.doc.content, editor);
const content = (ret.frag as any)?.content;
- if ((ret.frag.size > 2 || (content?.length && content[0].type === this._editorView.state.schema.nodes.audiotag)) && ret.start >= 0) {
- smooth && (this._focusSpeed = 500);
+ if ((ret.frag.size || (content?.length && content[0].type === this._editorView.state.schema.nodes.dashDoc) || (content?.length && content[0].type === this._editorView.state.schema.nodes.audiotag)) && ret.start >= 0) {
+ !options.instant && (this._focusSpeed = focusSpeed);
let selection = TextSelection.near(editor.state.doc.resolve(ret.start)); // default to near the start
if (ret.frag.firstChild) {
selection = TextSelection.between(editor.state.doc.resolve(ret.start), editor.state.doc.resolve(ret.start + ret.frag.firstChild.nodeSize)); // bcz: looks better to not have the target selected
@@ -978,18 +1108,18 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
addStyleSheetRule(FormattedTextBox._highlightStyleSheet, `${escAnchorId}`, { background: 'yellow', transform: 'scale(3)', 'transform-origin': 'left bottom' });
setTimeout(() => (this._focusSpeed = undefined), this._focusSpeed);
setTimeout(() => clearStyleSheetRules(FormattedTextBox._highlightStyleSheet), Math.max(this._focusSpeed || 0, 3000));
+ return focusSpeed;
+ } else {
+ return this.props.focus(this.rootDoc, options);
}
}
-
- return this._didScroll ? this._focusSpeed : didToggle ? 1 : undefined; // if we actually scrolled, then return some focusSpeed
};
- getScrollHeight = () => this.scrollHeight;
- // if the scroll height has changed and we're in autoHeight mode, then we need to update the textHeight component of the doc.
+ // if the scroll height has changed and we're in layout_autoHeight mode, then we need to update the textHeight component of the doc.
// Since we also monitor all component height changes, this will update the document's height.
resetNativeHeight = (scrollHeight: number) => {
const nh = this.layoutDoc.isTemplateForField ? 0 : NumCast(this.layoutDoc._nativeHeight);
- this.rootDoc[this.fieldKey + '-height'] = scrollHeight;
+ this.rootDoc[this.fieldKey + '_height'] = scrollHeight;
if (nh) this.layoutDoc._nativeHeight = scrollHeight;
};
@@ -998,34 +1128,45 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
}
componentDidMount() {
!this.props.dontSelectOnLoad && this.props.setContentView?.(this); // this tells the DocumentView that this AudioBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link.
- this._cachedLinks = DocListCast(this.Document.links);
+ this._cachedLinks = LinkManager.Links(this.Document);
this._disposers.breakupDictation = reaction(() => DocumentManager.Instance.RecordingEvent, this.breakupDictation);
- this._disposers.autoHeight = reaction(
- () => this.autoHeight,
- autoHeight => autoHeight && this.tryUpdateScrollHeight()
+ this._disposers.layout_autoHeight = reaction(
+ () => this.layout_autoHeight,
+ layout_autoHeight => layout_autoHeight && this.tryUpdateScrollHeight()
+ );
+ this._disposers.highlights = reaction(
+ () => Array.from(FormattedTextBox._globalHighlights).slice(),
+ highlights => this.updateHighlights(highlights),
+ { fireImmediately: true }
);
this._disposers.width = reaction(
() => this.props.PanelWidth(),
width => this.tryUpdateScrollHeight()
);
this._disposers.scrollHeight = reaction(
- () => ({ scrollHeight: this.scrollHeight, autoHeight: this.autoHeight, width: NumCast(this.layoutDoc._width) }),
- ({ width, scrollHeight, autoHeight }) => width && autoHeight && this.resetNativeHeight(scrollHeight),
+ () => ({ scrollHeight: this.scrollHeight, layout_autoHeight: this.layout_autoHeight, width: NumCast(this.layoutDoc._width) }),
+ ({ width, scrollHeight, layout_autoHeight }) => width && layout_autoHeight && this.resetNativeHeight(scrollHeight),
{ fireImmediately: true }
);
this._disposers.componentHeights = reaction(
- // set the document height when one of the component heights changes and autoHeight is on
- () => ({ sidebarHeight: this.sidebarHeight, textHeight: this.textHeight, autoHeight: this.autoHeight, marginsHeight: this.autoHeightMargins }),
- ({ sidebarHeight, textHeight, autoHeight, marginsHeight }) => {
+ // set the document height when one of the component heights changes and layout_autoHeight is on
+ () => ({ 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 (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(
- () => DocListCast(this.dataDoc.links), // if a link is deleted, then remove all hyperlinks that reference it from the text's marks
+ () => LinkManager.Links(this.dataDoc), // if a link is deleted, then remove all hyperlinks that reference it from the text's marks
newLinks => {
this._cachedLinks.forEach(l => !newLinks.includes(l) && this.RemoveLinkFromDoc(l));
this._cachedLinks = newLinks;
@@ -1043,8 +1184,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
this._disposers.editorState = reaction(
() => {
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]) };
+ 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(DocCast(whichDoc[this.fieldKey])) };
},
incomingValue => {
if (this._editorView && this._applyingChange !== this.fieldKey) {
@@ -1088,13 +1229,20 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
this._disposers.selected = reaction(
() => this.props.isSelected(),
action(selected => {
- this.layoutDoc._highlights = selected ? FormattedTextBox._globalHighlights.join('') : '';
+ if (FormattedTextBox._globalHighlights.has('Bold Text')) {
+ 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) {
+ AnchorMenu.Instance?.setEditorView(this._editorView);
+ AnchorMenu.Instance?.setTextDoc(this.dataDoc);
}
}),
{ fireImmediately: true }
@@ -1114,7 +1262,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
}
var quickScroll: string | undefined = '';
this._disposers.scroll = reaction(
- () => NumCast(this.layoutDoc._scrollTop),
+ () => NumCast(this.layoutDoc._layout_scrollTop),
pos => {
if (!this._ignoreScroll && this._scrollRef.current && !this.props.dontSelectOnLoad) {
const viewTrans = quickScroll ?? StrCast(this.Document._viewTransition);
@@ -1122,7 +1270,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
const durationSecStr = viewTrans.match(/([0-9.]*)s/);
const duration = durationMiliStr ? Number(durationMiliStr[1]) : durationSecStr ? Number(durationSecStr[1]) * 1000 : 0;
if (duration) {
- smoothScroll(duration, this._scrollRef.current, Math.abs(pos || 0));
+ this._scrollStopper = smoothScroll(duration, this._scrollRef.current, Math.abs(pos || 0), 'ease', this._scrollStopper);
} else {
this._scrollRef.current.scrollTo({ top: pos });
}
@@ -1191,7 +1339,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
}
}, 0);
dataDoc.title = exportState.title;
- this.dataDoc['title-custom'] = true;
+ this.dataDoc.title_custom = true;
dataDoc.googleDocUnchanged = true;
} else {
delete dataDoc[GoogleRef];
@@ -1234,61 +1382,38 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
};
handlePaste = (view: EditorView, event: Event, slice: Slice): boolean => {
- const cbe = event as ClipboardEvent;
- const pdfDocId = cbe.clipboardData?.getData('dash/pdfOrigin');
- const pdfRegionId = cbe.clipboardData?.getData('dash/pdfRegion');
- return pdfDocId && pdfRegionId && this.addPdfReference(pdfDocId, pdfRegionId, slice) ? true : false;
+ const pdfAnchorId = (event as ClipboardEvent).clipboardData?.getData('dash/pdfAnchor');
+ return pdfAnchorId && this.addPdfReference(pdfAnchorId) ? true : false;
};
- addPdfReference = (pdfDocId: string, pdfRegionId: string, slice?: Slice) => {
+ addPdfReference = (pdfAnchorId: string) => {
const view = this._editorView!;
- if (pdfDocId && pdfRegionId) {
- DocServer.GetRefField(pdfDocId).then(pdfDoc => {
- DocServer.GetRefField(pdfRegionId).then(pdfRegion => {
- if (pdfDoc instanceof Doc && pdfRegion instanceof Doc) {
- setTimeout(async () => {
- const targetField = Doc.LayoutFieldKey(pdfDoc);
- const targetAnnotations = await DocListCastAsync(pdfDoc[DataSym][targetField + '-annotations']); // bcz: better to have the PDF's view handle updating its own annotations
- if (targetAnnotations) targetAnnotations.push(pdfRegion);
- else Doc.AddDocToList(pdfDoc[DataSym], targetField + '-annotations', pdfRegion);
- });
-
- const link = DocUtils.MakeLink({ doc: this.rootDoc }, { doc: pdfRegion }, 'PDF pasted');
- if (link) {
- const linkId = link[Id];
- const quote = view.state.schema.nodes.blockquote.create({ content: addMarkToFrag(slice?.content || view.state.doc.content, (node: Node) => addLinkMark(node, StrCast(pdfDoc.title), linkId)) });
- const newSlice = new Slice(Fragment.from(quote), slice?.openStart || 0, slice?.openEnd || 0);
- if (slice) {
- view.dispatch(view.state.tr.replaceSelection(newSlice).scrollIntoView().setMeta('paste', true).setMeta('uiEvent', 'paste'));
- } else {
- selectAll(view.state, (tx: Transaction) => view.dispatch(tx.replaceSelection(newSlice).scrollIntoView()));
- }
- }
+ if (pdfAnchorId) {
+ DocServer.GetRefField(pdfAnchorId).then(pdfAnchor => {
+ if (pdfAnchor instanceof Doc) {
+ const dashField = view.state.schema.nodes.paragraph.create({}, [
+ view.state.schema.nodes.dashField.create({ fieldKey: 'text', docId: pdfAnchor[Id], hideKey: true, editable: false }, undefined, [
+ view.state.schema.marks.linkAnchor.create({
+ allAnchors: [{ href: `/doc/${this.rootDoc[Id]}`, title: this.rootDoc.title, anchorId: `${this.rootDoc[Id]}` }],
+ location: 'add:right',
+ title: `from: ${DocCast(pdfAnchor.embedContainer).title}`,
+ noPreview: true,
+ docref: false,
+ }),
+ view.state.schema.marks.pFontSize.create({ fontSize: '8px' }),
+ view.state.schema.marks.em.create({}),
+ ]),
+ ]);
+
+ const link = DocUtils.MakeLink(pdfAnchor, this.rootDoc, { link_relationship: 'PDF pasted' });
+ if (link) {
+ view.dispatch(view.state.tr.replaceSelectionWith(dashField, false).scrollIntoView().setMeta('paste', true).setMeta('uiEvent', 'paste'));
}
- });
+ }
});
return true;
}
return false;
-
- function addMarkToFrag(frag: Fragment, marker: (node: Node) => Node) {
- const nodes: Node[] = [];
- frag.forEach(node => nodes.push(marker(node)));
- return Fragment.fromArray(nodes);
- }
-
- function addLinkMark(node: Node, title: string, linkId: string) {
- if (!node.isText) {
- const content = addMarkToFrag(node.content, (node: Node) => addLinkMark(node, title, linkId));
- return node.copy(content);
- }
- const marks = [...node.marks];
- const linkIndex = marks.findIndex(mark => mark.type.name === 'link');
- const allLinks = [{ href: Doc.globalServerPath(linkId), title, linkId }];
- const link = view.state.schema.mark(view.state.schema.marks.linkAnchor, { allLinks, location: 'add:right', title, docref: true });
- marks.splice(linkIndex === -1 ? 0 : linkIndex, 1, link);
- return node.mark(marks);
- }
};
isActiveTab(el: Element | null | undefined) {
@@ -1309,6 +1434,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
});
}
_didScroll = false;
+ _scrollStopper: undefined | (() => void);
setupEditor(config: any, fieldKey: string) {
const curText = Cast(this.dataDoc[this.fieldKey], RichTextField, null) || StrCast(this.dataDoc[this.fieldKey]);
const rtfField = Cast((!curText && this.layoutDoc[this.fieldKey]) || this.dataDoc[fieldKey], RichTextField);
@@ -1327,7 +1453,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
const shift = Math.min(topOff ?? Number.MAX_VALUE, botOff ?? Number.MAX_VALUE);
const scrollPos = scrollRef.scrollTop + shift * self.props.ScreenToLocalTransform().Scale;
if (this._focusSpeed !== undefined) {
- scrollPos && smoothScroll(this._focusSpeed, scrollRef, scrollPos);
+ scrollPos && (this._scrollStopper = smoothScroll(this._focusSpeed, scrollRef, scrollPos, 'ease', this._scrollStopper));
} else {
scrollRef.scrollTo({ top: scrollPos });
}
@@ -1367,7 +1493,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
if (startupText) {
dispatch(state.tr.insertText(startupText));
}
- const textAlign = StrCast(this.dataDoc['text-align'], StrCast(Doc.UserDoc().textAlign, 'left'));
+ const textAlign = StrCast(this.dataDoc.text_align, StrCast(Doc.UserDoc().textAlign, 'left'));
if (textAlign !== 'left') {
selectAll(this._editorView.state, tr => {
this._editorView!.dispatch(tr.replaceSelectionWith(state.schema.nodes.paragraph.create({ align: textAlign })));
@@ -1378,11 +1504,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) ?? [];
@@ -1392,10 +1518,9 @@ 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) {
+ } else {
this._editorView.dispatch(this._editorView.state.tr.addStoredMark(schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) })));
}
}
@@ -1404,24 +1529,30 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
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)] : []),
- ];
+ this._editorView.dispatch(
+ this._editorView.state.tr.setStoredMarks([
+ ...(this._editorView.state.storedMarks ?? []),
+ ...(!this._editorView.state.storedMarks?.some(mark => mark.type === schema.marks.user_mark) ? [schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) })] : []),
+ ...(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;
+ pdfAnchorId && this.addPdfReference(pdfAnchorId);
+ }
}
FormattedTextBox.DontSelectInitialText = false;
}
componentWillUnmount() {
- FormattedTextBox.Focused === this && (FormattedTextBox.Focused = undefined);
Object.values(this._disposers).forEach(disposer => disposer?.());
this.endUndoTypingBatch();
this.unhighlightSearchTerms();
@@ -1436,7 +1567,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
} else (e.nativeEvent as any).handledByInnerReactInstance = true;
if (this.Document.forceActive) e.stopPropagation();
- this.tryUpdateScrollHeight(); // if a doc a fitwidth doc is being viewed in different context (eg freeform & lightbox), then it will have conflicting heights. so when the doc is clicked on, we want to make sure it has the appropriate height for the selected view.
+ this.tryUpdateScrollHeight(); // if a doc a fitWidth doc is being viewed in different embedContainer (eg freeform & lightbox), then it will have conflicting heights. so when the doc is clicked on, we want to make sure it has the appropriate height for the selected view.
if ((e.target as any).tagName === 'AUDIOTAG') {
e.preventDefault();
e.stopPropagation();
@@ -1448,7 +1579,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
const func = () => {
const docView = DocumentManager.Instance.getDocumentView(audiodoc);
if (!docView) {
- this.props.addDocTab(audiodoc, 'add:bottom');
+ this.props.addDocTab(audiodoc, OpenWhere.addBottom);
setTimeout(func);
} else docView.ComponentView?.playFrom?.(timecode, Cast(anchor.timecodeToHide, 'number', null)); // bcz: would be nice to find the next audio tag in the doc and play until that
};
@@ -1463,7 +1594,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
}
this._downX = e.clientX;
this._downY = e.clientY;
- this._downEvent = true;
FormattedTextBoxComment.textBox = this;
if (e.button === 0 && (this.props.rootSelected(true) || this.props.isSelected(true)) && !e.altKey && !e.ctrlKey && !e.metaKey) {
if (e.clientX < this.ProseRef!.getBoundingClientRect().right) {
@@ -1472,33 +1602,30 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
// but that's changed, so this shouldn't be needed.
//e.stopPropagation(); // if the text box is selected, then it consumes all down events
document.addEventListener('pointerup', this.onSelectEnd);
- document.addEventListener('pointermove', this.onSelectMove);
}
}
if (e.button === 2 || (e.button === 0 && e.ctrlKey)) {
e.preventDefault();
}
};
- onSelectMove = (e: PointerEvent) => e.stopPropagation();
onSelectEnd = (e: PointerEvent) => {
document.removeEventListener('pointerup', this.onSelectEnd);
- document.removeEventListener('pointermove', this.onSelectMove);
};
onPointerUp = (e: React.PointerEvent): void => {
- if (!this._editorView?.state.selection.empty && FormattedTextBox._canAnnotate && !(e.nativeEvent as any).dash) this.setupAnchorMenu();
- if (!this._downEvent) return;
- this._downEvent = false;
- if (this.props.isContentActive(true) && !(e.nativeEvent as any).dash) {
- const editor = this._editorView!;
+ const editor = this._editorView!;
+ const state = editor?.state;
+ if (!state || !editor || !this.ProseRef?.children[0].className.includes('-focused')) return;
+ if (!state.selection.empty && !(state.selection instanceof NodeSelection)) this.setupAnchorMenu();
+ else if (this.props.isContentActive(true)) {
const pcords = editor.posAtCoords({ left: e.clientX, top: e.clientY });
- !this.props.isSelected(true) && editor.dispatch(editor.state.tr.setSelection(new TextSelection(editor.state.doc.resolve(pcords?.pos || 0))));
+ // !this.props.isSelected(true) &&
+ editor.dispatch(state.tr.setSelection(new TextSelection(state.doc.resolve(pcords?.pos || 0))));
let target = e.target as any; // hrefs are stored on the dataset of the <a> node that wraps the hyerlink <span>
while (target && !target.dataset?.targethrefs) target = target.parentElement;
- FormattedTextBoxComment.update(this, editor, undefined, target?.dataset?.targethrefs, target?.dataset.linkdoc);
- }
-
- if (e.button === 0 && this.props.isSelected(true) && !e.altKey) {
- e.stopPropagation();
+ FormattedTextBoxComment.update(this, editor, undefined, target?.dataset?.targethrefs, target?.dataset.linkdoc, target?.dataset.nopreview === 'true');
+ if (pcords && pcords.inside > 0 && state.doc.nodeAt(pcords.inside)?.type === state.schema.nodes.dashDoc) {
+ return;
+ }
}
};
@action
@@ -1527,12 +1654,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
@action
onFocused = (e: React.FocusEvent): void => {
//applyDevTools.applyDevTools(this._editorView);
- FormattedTextBox.Focused = this;
this.ProseRef?.children[0] === e.nativeEvent.target && this._editorView && RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this.props);
+ e.stopPropagation();
};
- @observable public static Focused: FormattedTextBox | undefined;
onClick = (e: React.MouseEvent): void => {
+ if (!this.props.isContentActive()) return;
if ((e.nativeEvent as any).handledByInnerReactInstance) {
e.stopPropagation();
return;
@@ -1564,8 +1691,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
if (this.props.isSelected(true)) {
// if text box is selected, then it consumes all click events
(e.nativeEvent as any).handledByInnerReactInstance = true;
- if (this.ProseRef?.children[0] !== e.nativeEvent.target) e.stopPropagation(); // if you double click on text, then it will be selected instead of sending a double click to DocumentView & opening a lightbox. Also,if a text box has isLinkButton, this will prevent link following if you've selected the document to edit it.
- // e.stopPropagation(); // bcz: not sure why this was here. We need to allow the DocumentView to get clicks to process doubleClicks (see above comment)
this.hitBulletTargets(e.clientX, e.clientY, !this._editorView?.state.selection.empty || this._forceUncollapse, false, this._forceDownNode, e.shiftKey);
}
this._forceUncollapse = !(this._editorView!.root as any).getSelection().isCollapsed;
@@ -1605,20 +1730,27 @@ 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
onBlur = (e: any) => {
if (this.ProseRef?.children[0] !== e.nativeEvent.target) return;
- this.autoLink();
- FormattedTextBox.Focused === this && (FormattedTextBox.Focused = undefined);
+ if (!(this.EditorView?.state.selection instanceof NodeSelection) || this.EditorView.state.selection.node.type !== this.EditorView.state.schema.nodes.footnote) {
+ const stordMarks = this._editorView?.state.storedMarks?.slice();
+ this.autoLink();
+ if (this._editorView?.state.tr) {
+ const tr = stordMarks?.reduce((tr, m) => {
+ tr.addStoredMark(m);
+ return tr;
+ }, this._editorView.state.tr);
+ tr && this._editorView.dispatch(tr);
+ }
+ }
if (RichTextMenu.Instance?.view === this._editorView && !this.props.isSelected(true)) {
RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined);
}
@@ -1630,13 +1762,13 @@ 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(
() =>
translateGoogleApi(result1[0], { from: 'es', to: 'en' }).then((result: any) => {
- this.dataDoc[this.fieldKey + '-translation'] = result1 + '\r\n\r\n' + result[0];
+ this.dataDoc[this.fieldKey + '_translation'] = result1 + '\r\n\r\n' + result[0];
}),
1000
);
@@ -1646,10 +1778,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
}
this._lastText = curText;
}
- if (StrCast(this.rootDoc.title).startsWith('@') && !this.dataDoc['title-custom']) {
+ if (StrCast(this.rootDoc.title).startsWith('@') && !this.dataDoc.title_custom) {
UndoManager.RunInBatch(() => {
- this.dataDoc['title-custom'] = true;
- this.dataDoc.showTitle = 'title';
+ this.dataDoc.title_custom = true;
+ this.dataDoc.layout_showTitle = 'title';
const tr = this._editorView!.state.tr;
this._editorView?.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(0), tr.doc.resolve(StrCast(this.rootDoc.title).length + 2))).deleteSelection());
}, 'titler');
@@ -1657,8 +1789,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
};
onKeyDown = (e: React.KeyboardEvent) => {
- if (e.altKey) {
+ if ((e.altKey || e.ctrlKey) && e.key === 't') {
e.preventDefault();
+ e.stopPropagation();
+ this.props.setTitleFocus?.();
return;
}
const state = this._editorView!.state;
@@ -1707,22 +1841,28 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
if (!LinkDocPreview.LinkInfo && this._scrollRef.current) {
if (!this.props.dontSelectOnLoad) {
this._ignoreScroll = true;
- this.layoutDoc._scrollTop = this._scrollRef.current.scrollTop;
+ this.layoutDoc._layout_scrollTop = this._scrollRef.current.scrollTop;
this._ignoreScroll = false;
+ e.stopPropagation();
+ e.preventDefault();
}
}
};
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) {
- const proseHeight = !this.ProseRef
- ? 0
- : children.reduce((p, child) => p + Number(getComputedStyle(child).height.replace('px', '')) + Number(getComputedStyle(child).marginTop.replace('px', '')) + Number(getComputedStyle(child).marginBottom.replace('px', '')), margins);
- const scrollHeight = this.ProseRef && Math.min(NumCast(this.layoutDoc.docMaxAutoHeight, proseHeight), proseHeight);
+ 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.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);
+ const setScrollHeight = () => (this.rootDoc[this.fieldKey + '_scrollHeight'] = scrollHeight);
+
if (this.rootDoc === this.layoutDoc || this.layoutDoc.resolvedDataDoc) {
setScrollHeight();
} else {
@@ -1731,23 +1871,21 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
}
}
};
- fitContentsToBox = () => this.props.Document._fitContentsToBox;
- sidebarContentScaling = () => (this.props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._viewScale, 1);
+ fitContentsToBox = () => BoolCast(this.props.Document._freeform_fitContentsToBox);
+ sidebarContentScaling = () => (this.props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._freeform_scale, 1);
sidebarAddDocument = (doc: Doc | Doc[], sidebarKey: string = this.SidebarKey) => {
- if (!this.layoutDoc._showSidebar) this.toggleSidebar();
- // console.log("printting allSideBarDocs");
- // console.log(this.allSidebarDocs);
+ if (!this.layoutDoc._layout_showSidebar) this.toggleSidebar();
return this.addDocument(doc, sidebarKey);
};
sidebarMoveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => this.moveDocument(doc, targetCollection, addDocument, this.SidebarKey);
sidebarRemDocument = (doc: Doc | Doc[]) => this.removeDocument(doc, this.SidebarKey);
- setSidebarHeight = (height: number) => (this.rootDoc[this.SidebarKey + '-height'] = height);
- sidebarWidth = () => (Number(this.sidebarWidthPercent.substring(0, this.sidebarWidthPercent.length - 1)) / 100) * this.props.PanelWidth();
+ setSidebarHeight = (height: number) => (this.rootDoc[this.SidebarKey + '_height'] = height);
+ sidebarWidth = () => (Number(this.layout_sidebarWidthPercent.substring(0, this.layout_sidebarWidthPercent.length - 1)) / 100) * this.props.PanelWidth();
sidebarScreenToLocal = () =>
this.props
.ScreenToLocalTransform()
.translate(-(this.props.PanelWidth() - this.sidebarWidth()) / (this.props.NativeDimScaling?.() || 1), 0)
- .scale(1 / NumCast(this.layoutDoc._viewScale, 1) / (this.props.NativeDimScaling?.() || 1));
+ .scale(1 / NumCast(this.layoutDoc._freeform_scale, 1) / (this.props.NativeDimScaling?.() || 1));
@computed get audioHandle() {
return !this._recording ? null : (
@@ -1777,11 +1915,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
className="formattedTextBox-sidebar-handle"
onPointerDown={this.sidebarDown}
style={{
- backgroundColor: backgroundColor,
- color: color,
+ backgroundColor,
+ color,
opacity: annotated ? 1 : undefined,
}}>
- <FontAwesomeIcon icon={'comment-alt'} />
+ <FontAwesomeIcon icon="comment-alt" />
</div>
);
}
@@ -1792,74 +1930,131 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
<SidebarAnnos
ref={this._sidebarRef}
{...this.props}
- fieldKey={this.fieldKey}
rootDoc={this.rootDoc}
layoutDoc={this.layoutDoc}
dataDoc={this.dataDoc}
- ScreenToLocalTransform={this.sidebarScreenToLocal}
+ usePanelWidth={true}
nativeWidth={NumCast(this.layoutDoc._nativeWidth)}
- whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
showSidebar={this.SidebarShown}
- PanelWidth={this.sidebarWidth}
- setHeight={this.setSidebarHeight}
+ whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
sidebarAddDocument={this.sidebarAddDocument}
moveDocument={this.moveDocument}
removeDocument={this.removeDocument}
+ ScreenToLocalTransform={this.sidebarScreenToLocal}
+ fieldKey={this.fieldKey}
+ PanelWidth={this.sidebarWidth}
+ setHeight={this.setSidebarHeight}
/>
) : (
<div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => SelectionManager.SelectView(this.props.DocumentView?.()!, false), true)}>
<ComponentTag
- {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
+ {...this.props}
+ setContentView={emptyFunction}
NativeWidth={returnZero}
NativeHeight={returnZero}
PanelHeight={this.props.PanelHeight}
PanelWidth={this.sidebarWidth}
xPadding={0}
yPadding={0}
- scaleField={this.SidebarKey + '-scale'}
+ viewField={this.SidebarKey}
isAnnotationOverlay={false}
select={emptyFunction}
+ isAnyChildContentActive={returnFalse}
NativeDimScaling={this.sidebarContentScaling}
whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
removeDocument={this.sidebarRemDocument}
moveDocument={this.sidebarMoveDocument}
addDocument={this.sidebarAddDocument}
- CollectionView={undefined}
ScreenToLocalTransform={this.sidebarScreenToLocal}
renderDepth={this.props.renderDepth + 1}
setHeight={this.setSidebarHeight}
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.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}>
- {renderComponent(StrCast(this.layoutDoc.sidebarViewType))}
+ <div className={'formattedTextBox-sidebar' + (Doc.ActiveTool !== InkTool.None ? '-inking' : '')} style={{ width: `${this.layout_sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}>
+ {renderComponent(StrCast(this.layoutDoc.sidebar_collectionType))}
</div>
);
}
+ cycleAlternateText = () => {
+ 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`];
+ return (
+ <Tooltip
+ title={
+ <div className="dash-tooltip">
+ toggle (%/) between
+ <span style={{ color: usePath === undefined ? 'black' : undefined }}>
+ <em> primary, </em>
+ </span>
+ <span style={{ color: usePath === 'alternate' ? 'black' : undefined }}>
+ <em>alternate, </em>
+ </span>
+ and show
+ <span style={{ color: usePath === 'alternate:hover' ? 'black' : undefined }}>
+ <em> alternate on hover</em>
+ </span>
+ </div>
+ }>
+ <div
+ className="formattedTextBox-alternateButton"
+ onPointerDown={e => setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, e => this.cycleAlternateText())}
+ style={{
+ display: this.props.isContentActive() && !SnappingManager.GetIsDragging() ? 'flex' : 'none',
+ background: usePath === undefined ? 'white' : usePath === 'alternate' ? 'black' : 'gray',
+ color: usePath === undefined ? 'black' : 'white',
+ }}>
+ <FontAwesomeIcon icon="turn-up" size="sm" />
+ </div>
+ </Tooltip>
+ );
+ }
+ @computed get fieldKey() {
+ const usePath = StrCast(this.rootDoc[`${this.props.fieldKey}_usePath`]);
+ 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;
render() {
TraceMobx();
- const active = this.props.isContentActive() || this.props.isSelected();
- const selected = active;
- const scale = (this.props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._viewScale, 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 active = this.props.isContentActive();
+ const scale = (this.props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._freeform_scale, 1);
+ const rounded = StrCast(this.layoutDoc.layout_borderRounding) === '100%' ? '-rounded' : '';
+ if (!active && FormattedTextBoxComment.textBox === this) setTimeout(FormattedTextBoxComment.Hide);
const minimal = this.props.ignoreAutoHeight;
const paddingX = NumCast(this.layoutDoc._xMargin, this.props.xPadding || 0);
const paddingY = NumCast(this.layoutDoc._yMargin, this.props.yPadding || 0);
- const selPad = (selected && !this.layoutDoc._singleLine) || minimal ? Math.min(paddingY, Math.min(paddingX, 10)) : 0;
- const selPaddingClass = selected && !this.layoutDoc._singleLine && paddingY >= 10 ? '-selected' : '';
+ const selPad = (active && !this.layoutDoc._createDocOnCR) || minimal ? Math.min(paddingY, Math.min(paddingX, 10)) : 0;
+ const selPaddingClass = active && !this.layoutDoc._createDocOnCR && paddingY >= 10 ? '-selected' : '';
const styleFromLayoutString = Doc.styleFromLayoutString(this.rootDoc, this.layoutDoc, this.props, scale); // this converts any expressions in the format string to style props. e.g., <FormattedTextBox height='{this._headerHeight}px' >
return styleFromLayoutString?.height === '0px' ? null : (
<div
- className="formattedTextBox-cont"
- onWheel={e => this.props.isContentActive() && e.stopPropagation()}
+ className="formattedTextBox"
+ onPointerEnter={action(() => (this._isHovering = true))}
+ onPointerLeave={action(() => (this._isHovering = false))}
+ ref={r => {
+ this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel);
+ this._oldWheel = r;
+ r?.addEventListener('wheel', this.onPassiveWheel, { passive: false });
+ }}
style={{
...(this.props.dontScale
? {}
@@ -1869,8 +2064,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
width: `${100 / scale}%`,
height: `${100 / scale}%`,
}),
+ display: !this.props.isContentActive() && this.props.thumbShown?.() ? 'none' : undefined,
transition: 'inherit',
- // overflowY: this.layoutDoc._autoHeight ? "hidden" : undefined,
+ // 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),
@@ -1878,12 +2074,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
...styleFromLayoutString,
}}>
<div
- className={`formattedTextBox-cont`}
+ className="formattedTextBox-cont"
ref={this._ref}
style={{
- overflow: this.autoHeight && this.props.CollectionFreeFormDocumentView?.() ? 'hidden' : undefined, //x this breaks viewing an autoHeight doc in its own tab, or in the lightbox
- height: this.props.height || (this.autoHeight && this.props.renderDepth && !this.props.suppressSetHeight ? 'max-content' : undefined),
- pointerEvents: interactive ? undefined : 'none',
+ 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: Doc.ActiveTool === InkTool.None ? undefined : 'none',
}}
onContextMenu={this.specificContextMenu}
onKeyDown={this.onKeyDown}
@@ -1895,12 +2091,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
onPointerDown={this.onPointerDown}
onDoubleClick={this.onDoubleClick}>
<div
- className={`formattedTextBox-outer${selected ? '-selected' : ''}`}
+ className={`formattedTextBox-outer${active ? '-selected' : ''}`}
ref={this._scrollRef}
style={{
- width: this.props.dontSelectOnLoad ? '100%' : `calc(100% - ${this.sidebarWidthPercent})`,
- pointerEvents: !active && !SnappingManager.GetIsDragging() ? 'none' : undefined,
- overflow: this.layoutDoc._singleLine ? 'hidden' : this.layoutDoc._autoHeight ? 'visible' : undefined,
+ width: this.props.dontSelectOnLoad ? '100%' : `calc(100% - ${this.layout_sidebarWidthPercent})`,
+ overflow: this.layoutDoc._createDocOnCR ? 'hidden' : this.layoutDoc._layout_autoHeight ? 'visible' : undefined,
}}
onScroll={this.onScroll}
onDrop={this.ondrop}>
@@ -1913,13 +2108,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
paddingRight: StrCast(this.layoutDoc._textBoxPaddingX, `${paddingX - selPad}px`),
paddingTop: StrCast(this.layoutDoc._textBoxPaddingY, `${paddingY - selPad}px`),
paddingBottom: StrCast(this.layoutDoc._textBoxPaddingY, `${paddingY - selPad}px`),
- pointerEvents: !active && !SnappingManager.GetIsDragging() ? (this.layoutDoc.isLinkButton ? 'none' : undefined) : undefined,
+ // pointerEvents: !active && IsFollowLinkScript(this.layoutDoc.onClick) ? 'none' : undefined,
}}
/>
</div>
- {this.noSidebar || this.props.dontSelectOnLoad || !this.SidebarShown || this.sidebarWidthPercent === '0%' ? null : this.sidebarCollection}
- {this.noSidebar || this.Document._noSidebar || this.props.dontSelectOnLoad || this.Document._singleLine ? null : this.sidebarHandle}
+ {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_enableAltContentUI ? this.overlayAlternateIcon : null}
</div>
</div>
);
diff --git a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx
index bdf59863b..e7ca26d5c 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx
@@ -1,5 +1,5 @@
import { Mark, ResolvedPos } from 'prosemirror-model';
-import { EditorState } from 'prosemirror-state';
+import { EditorState, NodeSelection } from 'prosemirror-state';
import { EditorView } from 'prosemirror-view';
import { Doc } from '../../../../fields/Doc';
import { DocServer } from '../../../DocServer';
@@ -92,7 +92,7 @@ export class FormattedTextBoxComment {
FormattedTextBoxComment.tooltip.style.display = '';
}
- static update(textBox: FormattedTextBox, view: EditorView, lastState?: EditorState, hrefs: string = '', linkDoc: string = '') {
+ static update(textBox: FormattedTextBox, view: EditorView, lastState?: EditorState, hrefs: string = '', linkDoc: string = '', noPreview: boolean = false) {
FormattedTextBoxComment.textBox = textBox;
if (hrefs || !lastState?.doc.eq(view.state.doc) || !lastState?.selection.eq(view.state.selection)) {
FormattedTextBoxComment.setupPreview(
@@ -102,12 +102,13 @@ export class FormattedTextBoxComment {
?.trim()
.split(' ')
.filter(h => h),
- linkDoc
+ linkDoc,
+ noPreview
);
}
}
- static setupPreview(view: EditorView, textBox: FormattedTextBox, hrefs?: string[], linkDoc?: string) {
+ static setupPreview(view: EditorView, textBox: FormattedTextBox, hrefs?: string[], linkDoc?: string, noPreview?: boolean) {
const state = view.state;
// this section checks to see if the insertion point is over text entered by a different user. If so, it sets ths comment text to indicate the user and the modification date
if (state.selection.$from) {
@@ -130,8 +131,8 @@ export class FormattedTextBoxComment {
if (state.selection.$from && hrefs?.length) {
const nbef = findStartOfMark(state.selection.$from, view, findLinkMark);
const naft = findEndOfMark(state.selection.$from, view, findLinkMark) || nbef;
- nbef &&
- naft &&
+ //nbef &&
+ naft &&
LinkDocPreview.SetLinkInfo({
docProps: textBox.props,
linkSrc: textBox.rootDoc,
@@ -139,6 +140,7 @@ export class FormattedTextBoxComment {
location: (pos => [pos.left, pos.top + 25])(view.coordsAtPos(state.selection.from - Math.max(0, nbef - 1))),
hrefs,
showHeader: true,
+ noPreview,
});
}
}
diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
index 3d9bd6add..8d57cc081 100644
--- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
+++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
@@ -4,11 +4,14 @@ 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 { Doc } from '../../../../fields/Doc';
+import { AclAugment, AclSelfEdit } from '../../../../fields/DocSymbols';
import { GetEffectiveAcl } from '../../../../fields/util';
import { Utils } from '../../../../Utils';
import { Docs } from '../../../documents/Documents';
+import { RTFMarkup } from '../../../util/RTFMarkup';
import { SelectionManager } from '../../../util/SelectionManager';
+import { OpenWhere } from '../DocumentView';
import { liftListItem, sinkListItem } from './prosemirrorPatches.js';
const mac = typeof navigator !== 'undefined' ? /Mac/.test(navigator.platform) : false;
@@ -135,7 +138,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
//Command to create a new Tab with a PDF of all the command shortcuts
bind('Mod-/', (state: EditorState, dispatch: (tx: Transaction) => void) => {
const newDoc = Docs.Create.PdfDocument(Utils.prepend('/assets/cheat-sheet.pdf'), { _width: 300, _height: 300 });
- props.addDocTab(newDoc, 'add:right');
+ props.addDocTab(newDoc, OpenWhere.addRight);
});
//Commands to modify BlockType
@@ -177,6 +180,83 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
dispatch(state.tr.setSelection(new TextSelection(state.doc.resolve(1), state.doc.resolve(state.doc.content.size - 1))));
return true;
});
+ bind('Cmd-?', (state: EditorState, dispatch: (tx: Transaction) => void) => {
+ RTFMarkup.Instance.open();
+ return true;
+ });
+ bind('Cmd-e', (state: EditorState, dispatch: (tx: Transaction) => void) => {
+ if (!state.selection.empty) {
+ const mark = state.schema.marks.summarizeInclusive.create();
+ const tr = state.tr.addMark(state.selection.$from.pos, state.selection.$to.pos, mark);
+ const content = tr.selection.content();
+ tr.selection.replaceWith(tr, schema.nodes.summary.create({ visibility: false, text: content, textslice: content.toJSON() }));
+ dispatch(tr);
+ }
+ return true;
+ });
+ bind('Cmd-]', (state: EditorState, dispatch: (tx: Transaction) => void) => {
+ const resolved = state.doc.resolve(state.selection.from) as any;
+ const tr = state.tr;
+ if (resolved?.parent.type.name === 'paragraph') {
+ tr.setNodeMarkup(resolved.path[resolved.path.length - 4], schema.nodes.paragraph, { ...resolved.parent.attrs, align: 'right' }, resolved.parent.marks);
+ } else {
+ const node = resolved.nodeAfter;
+ const sm = state.storedMarks || undefined;
+ if (node) {
+ tr.replaceRangeWith(state.selection.from, state.selection.from, schema.nodes.paragraph.create({ align: 'right' })).setStoredMarks([...node.marks, ...(sm ? sm : [])]);
+ }
+ }
+ dispatch(tr);
+ return true;
+ });
+ bind('Cmd-\\', (state: EditorState, dispatch: (tx: Transaction) => void) => {
+ const resolved = state.doc.resolve(state.selection.from) as any;
+ const tr = state.tr;
+ if (resolved?.parent.type.name === 'paragraph') {
+ tr.setNodeMarkup(resolved.path[resolved.path.length - 4], schema.nodes.paragraph, { ...resolved.parent.attrs, align: 'center' }, resolved.parent.marks);
+ } else {
+ const node = resolved.nodeAfter;
+ const sm = state.storedMarks || undefined;
+ if (node) {
+ tr.replaceRangeWith(state.selection.from, state.selection.from, schema.nodes.paragraph.create({ align: 'center' })).setStoredMarks([...node.marks, ...(sm ? sm : [])]);
+ }
+ }
+ dispatch(tr);
+ return true;
+ });
+ bind('Cmd-[', (state: EditorState, dispatch: (tx: Transaction) => void) => {
+ const resolved = state.doc.resolve(state.selection.from) as any;
+ const tr = state.tr;
+ if (resolved?.parent.type.name === 'paragraph') {
+ tr.setNodeMarkup(resolved.path[resolved.path.length - 4], schema.nodes.paragraph, { ...resolved.parent.attrs, align: 'left' }, resolved.parent.marks);
+ } else {
+ const node = resolved.nodeAfter;
+ const sm = state.storedMarks || undefined;
+ if (node) {
+ tr.replaceRangeWith(state.selection.from, state.selection.from, schema.nodes.paragraph.create({ align: 'left' })).setStoredMarks([...node.marks, ...(sm ? sm : [])]);
+ }
+ }
+ dispatch(tr);
+ return true;
+ });
+
+ bind('Cmd-f', (state: EditorState, dispatch: (tx: Transaction) => void) => {
+ const content = state.tr.selection.empty ? undefined : state.tr.selection.content().content.textBetween(0, state.tr.selection.content().size + 1);
+ const newNode = schema.nodes.footnote.create({}, content ? state.schema.text(content) : undefined);
+ const tr = state.tr;
+ tr.replaceSelectionWith(newNode); // replace insertion with a footnote.
+ dispatch(
+ tr.setSelection(
+ new NodeSelection( // select the footnote node to open its display
+ tr.doc.resolve(
+ // get the location of the footnote node by subtracting the nodesize of the footnote from the current insertion point anchor (which will be immediately after the footnote node)
+ tr.selection.anchor - (tr.selection.$anchor.nodeBefore?.nodeSize || 0)
+ )
+ )
+ )
+ );
+ return true;
+ });
bind('Ctrl-a', (state: EditorState, dispatch: (tx: Transaction) => void) => {
dispatch(state.tr.setSelection(new TextSelection(state.doc.resolve(1), state.doc.resolve(state.doc.content.size - 1))));
diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx
index 6c6d26af5..7c3e4baad 100644
--- a/src/client/views/nodes/formattedText/RichTextMenu.tsx
+++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx
@@ -20,6 +20,7 @@ import { FormattedTextBox } from './FormattedTextBox';
import { updateBullets } from './ProsemirrorExampleTransfer';
import './RichTextMenu.scss';
import { schema } from './schema_rts';
+import { EquationBox } from '../EquationBox';
const { toggleMark } = require('prosemirror-commands');
@observer
@@ -53,7 +54,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
@observable private _activeFontColor: string = 'black';
@observable private showColorDropdown: boolean = false;
- @observable private activeHighlightColor: string = 'transparent';
+ @observable private _activeHighlightColor: string = 'transparent';
@observable private showHighlightDropdown: boolean = false;
@observable private currentLink: string | undefined = '';
@@ -88,6 +89,9 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
@computed get fontColor() {
return this._activeFontColor;
}
+ @computed get fontHighlight() {
+ return this._activeHighlightColor;
+ }
@computed get fontFamily() {
return this._activeFontFamily;
}
@@ -97,6 +101,16 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
@computed get textAlign() {
return this._activeAlignment;
}
+ _disposer: IReactionDisposer | undefined;
+ componentDidMount() {
+ this._disposer = reaction(
+ () => SelectionManager.Views(),
+ views => this.updateMenu(undefined, undefined, undefined)
+ );
+ }
+ componentWillUnmount() {
+ this._disposer?.();
+ }
@action
public updateMenu(view: EditorView | undefined, lastState: EditorState | undefined, props: any) {
@@ -124,10 +138,10 @@ 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]) : '...';
+ this._activeHighlightColor = !activeHighlights.length ? '' : activeHighlights.length > 0 ? String(activeHighlights[0]) : '...';
// update link in current selection
this.getTextLinkTargetTitle().then(targetTitle => this.setCurrentLink(targetTitle));
@@ -206,6 +220,8 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
m.type === state.schema.marks.pFontSize && activeSizes.add(m.attrs.fontSize);
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._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) };
}
@@ -328,7 +344,9 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
this.setMark(fmark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(fmark)), true);
this.view.focus();
}
- } else Doc.UserDoc()._fontSize = fontSize;
+ } else if (SelectionManager.Views().some(dv => dv.ComponentView instanceof EquationBox)) {
+ SelectionManager.Views().forEach(dv => (dv.rootDoc._text_fontSize = fontSize));
+ } else Doc.UserDoc().fontSize = fontSize;
this.updateMenu(this.view, undefined, this.props);
};
@@ -337,15 +355,17 @@ 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);
};
- setHighlight(color: String, view: EditorView, dispatch: any) {
- const highlightMark = view.state.schema.mark(view.state.schema.marks.marker, { highlight: color });
- if (view.state.selection.empty) return false;
- view.focus();
- this.setMark(highlightMark, view.state, dispatch, false);
+ setHighlight(color: string) {
+ if (this.view) {
+ const highlightMark = this.view.state.schema.mark(this.view.state.schema.marks.marker, { highlight: color });
+ this.setMark(highlightMark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(highlightMark)), true);
+ this.view.focus();
+ } else Doc.UserDoc()._fontHighlight = color;
+ this.updateMenu(this.view, undefined, this.props);
}
setColor(color: string) {
@@ -548,7 +568,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
}
@action setActiveHighlight(color: string) {
- this.activeHighlightColor = color;
+ this._activeHighlightColor = color;
}
@action setCurrentLink(link: string) {
@@ -603,15 +623,15 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
if (linkclicked) {
const linkDoc = await DocServer.GetRefField(linkclicked);
if (linkDoc instanceof Doc) {
- const anchor1 = await Cast(linkDoc.anchor1, Doc);
- const anchor2 = await Cast(linkDoc.anchor2, Doc);
+ const link_anchor_1 = await Cast(linkDoc.link_anchor_1, Doc);
+ const link_anchor_2 = await Cast(linkDoc.link_anchor_2, Doc);
const currentDoc = SelectionManager.Docs().lastElement();
- if (currentDoc && anchor1 && anchor2) {
- if (Doc.AreProtosEqual(currentDoc, anchor1)) {
- return StrCast(anchor2.title);
+ if (currentDoc && link_anchor_1 && link_anchor_2) {
+ if (Doc.AreProtosEqual(currentDoc, link_anchor_1)) {
+ return StrCast(link_anchor_2.title);
}
- if (Doc.AreProtosEqual(currentDoc, anchor2)) {
- return StrCast(anchor1.title);
+ if (Doc.AreProtosEqual(currentDoc, link_anchor_2)) {
+ return StrCast(link_anchor_1.title);
}
}
}
@@ -738,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 dc5d8ada8..ac1e7ce5d 100644
--- a/src/client/views/nodes/formattedText/RichTextRules.ts
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -1,11 +1,12 @@
import { ellipsis, emDash, InputRule, smartQuotes, textblockTypeInputRule } from 'prosemirror-inputrules';
import { NodeSelection, TextSelection } from 'prosemirror-state';
-import { DataSym, Doc } 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 { returnFalse, Utils } from '../../../../Utils';
+import { NumCast } from '../../../../fields/Types';
+import { Utils } from '../../../../Utils';
import { DocServer } from '../../../DocServer';
import { Docs, DocUtils } from '../../../documents/Documents';
import { FormattedTextBox } from './FormattedTextBox';
@@ -28,7 +29,7 @@ export class RichTextRules {
emDash,
// > blockquote
- wrappingInputRule(/^\s*>\s$/, schema.nodes.blockquote),
+ wrappingInputRule(/%>$/, schema.nodes.blockquote),
// 1. create numerical ordered list
wrappingInputRule(
@@ -75,22 +76,31 @@ 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('', { _layoutKey: inlineLayoutKey, _width: 75, _height: 35, annotationOn: textDoc, _fitWidth: true, _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.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
textDocInline.proto = textDoc; // make the annotation inherit from the outer text doc so that it can resolve any nested field references, e.g., [[field]]
textDocInline._textContext = ComputedField.MakeFunction(`copyField(self.${inlineFieldKey})`);
textDoc[inlineLayoutKey] = FormattedTextBox.LayoutString(inlineFieldKey); // create a layout string for the layout key that will render the annotation text
textDoc[inlineFieldKey] = ''; // set a default value for the annotation
const node = (state.doc.resolve(start) as any).nodeAfter;
- const newNode = schema.nodes.dashComment.create({ docid: textDocInline[Id] });
- const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 35, title: 'dashDoc', docid: textDocInline[Id], float: 'right' });
+ const newNode = schema.nodes.dashComment.create({ docId: textDocInline[Id] });
+ const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 35, title: 'dashDoc', docId: textDocInline[Id], float: 'right' });
const sm = state.storedMarks || undefined;
const replaced = node
? state.tr
@@ -190,21 +200,6 @@ export class RichTextRules {
}
}),
- // %f create footnote
- new InputRule(new RegExp(/%f$/), (state, match, start, end) => {
- const newNode = schema.nodes.footnote.create({});
- const tr = state.tr;
- tr.deleteRange(start, end).replaceSelectionWith(newNode); // replace insertion with a footnote.
- return tr.setSelection(
- new NodeSelection( // select the footnote node to open its display
- tr.doc.resolve(
- // get the location of the footnote node by subtracting the nodesize of the footnote from the current insertion point anchor (which will be immediately after the footnote node)
- tr.selection.anchor - (tr.selection.$anchor.nodeBefore?.nodeSize || 0)
- )
- )
- );
- }),
-
// activate a style by name using prefix '%<color name>'
new InputRule(new RegExp(/%[a-z]+$/), (state, match, start, end) => {
const color = match[0].substring(1, match[0].length);
@@ -229,6 +224,12 @@ export class RichTextRules {
}),
// stop using active style
+ new InputRule(new RegExp(/%\//), (state, match, start, end) => {
+ setTimeout(this.TextBox.cycleAlternateText);
+ return state.tr.deleteRange(start, end);
+ }),
+
+ // stop using active style
new InputRule(new RegExp(/%%$/), (state, match, start, end) => {
const tr = state.tr.deleteRange(start, end);
const marks = state.tr.selection.$anchor.nodeBefore?.marks;
@@ -248,33 +249,37 @@ export class RichTextRules {
// [[fieldKey:Doc]] => 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 docId = match[3]?.replace(':', '');
const value = match[2]?.substring(1);
+ const linkToDoc = (target: Doc) => {
+ const rstate = this.TextBox.EditorView?.state;
+ const selection = rstate?.selection.$from.pos;
+ if (rstate) {
+ this.TextBox.EditorView?.dispatch(rstate.tr.setSelection(new TextSelection(rstate.doc.resolve(start), rstate.doc.resolve(end - 3))));
+ }
+
+ DocUtils.MakeLink(this.TextBox.getAnchor(true), target, { link_relationship: 'portal to:portal from' });
+
+ const fstate = this.TextBox.EditorView?.state;
+ if (fstate && selection) {
+ this.TextBox.EditorView?.dispatch(fstate.tr.setSelection(new TextSelection(fstate.doc.resolve(selection))));
+ }
+ };
if (!fieldKey) {
- if (docid) {
- DocServer.GetRefField(docid).then(docx => {
- const rstate = this.TextBox.EditorView?.state;
- const selection = rstate?.selection.$from.pos;
- if (rstate) {
- this.TextBox.EditorView?.dispatch(rstate.tr.setSelection(new TextSelection(rstate.doc.resolve(start), rstate.doc.resolve(end - 3))));
- }
- const target = (docx instanceof Doc && docx) || Docs.Create.FreeformDocument([], { title: docid, _width: 500, _height: 500 }, docid);
- DocUtils.MakeLink({ doc: this.TextBox.getAnchor() }, { doc: target }, 'portal to:portal from', undefined);
-
- const fstate = this.TextBox.EditorView?.state;
- if (fstate && selection) {
- this.TextBox.EditorView?.dispatch(fstate.tr.setSelection(new TextSelection(fstate.doc.resolve(selection))));
- }
- });
+ 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);
}
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 fieldView = state.schema.nodes.dashField.create({ fieldKey, docid, hideKey: 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);
}),
@@ -296,43 +301,22 @@ export class RichTextRules {
// create an inline equation node
// eq:<equation>>
- new InputRule(new RegExp(/:eq([a-zA-Z-0-9\(\)]*)$/), (state, match, start, end) => {
+ new InputRule(new RegExp(/%eq([a-zA-Z-0-9\(\)]*)$/), (state, match, start, end) => {
const fieldKey = 'math' + Utils.GenerateGuid();
this.TextBox.dataDoc[fieldKey] = match[1];
const tr = state.tr.setSelection(new TextSelection(state.tr.doc.resolve(end - 3), state.tr.doc.resolve(end))).replaceSelectionWith(schema.nodes.equation.create({ fieldKey }));
return tr.setSelection(new NodeSelection(tr.doc.resolve(tr.selection.$from.pos - 1)));
}),
- // create an inline view of a document {{ <layoutKey> : <Doc> }}
- // {{:Doc}} => show default view of document
- // {{<layout>}} => show layout for this doc
- // {{<layout> : Doc}} => show layout for another doc
- new InputRule(new RegExp(/\{\{([a-zA-Z_ \-0-9]*)(\([a-zA-Z0-9…._/\-]*\))?(:[a-zA-Z_@\.\? \-0-9]+)?\}\}$/), (state, match, start, end) => {
- const fieldKey = match[1] || '';
- const fieldParam = match[2]?.replace('…', '...') || '';
- const rawdocid = match[3]?.substring(1);
- const docid = rawdocid ? (!rawdocid.includes('@') ? normalizeEmail(Doc.CurrentUserEmail) + '@' + rawdocid : rawdocid) : undefined;
- if (!fieldKey && !docid) return state.tr;
- docid &&
- DocServer.GetRefField(docid).then(docx => {
- if (!(docx instanceof Doc && docx)) {
- Docs.Create.FreeformDocument([], { title: rawdocid, _width: 500, _height: 500 }, docid);
- }
- });
- const node = (state.doc.resolve(start) as any).nodeAfter;
- const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 75, title: 'dashDoc', docid, fieldKey: fieldKey + fieldParam, float: 'unset', alias: Utils.GenerateGuid() });
- const sm = state.storedMarks || undefined;
- return node ? state.tr.replaceRangeWith(start, end, dashDoc).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr;
- }),
-
// create an inline view of a tag stored under the '#' field
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 = StrCast(this.Document[DataSym].tags, ':');
- if (!tags.includes(`#${tag}:`)) {
- this.Document[DataSym].tags = `${tags + '#' + tag + ':'}`;
+ //this.Document[DocData]['#' + tag] = '#' + tag;
+ const tags = StrListCast(this.Document[DocData].tags);
+ if (!tags.includes(tag)) {
+ tags.push(tag);
+ this.Document[DocData].tags = new List<string>(tags);
}
const fieldView = state.schema.nodes.dashField.create({ fieldKey: '#' + tag });
return state.tr
@@ -374,7 +358,7 @@ export class RichTextRules {
const content = selected.selection.content();
const replaced = node ? selected.replaceRangeWith(start, end, schema.nodes.summary.create({ visibility: true, text: content, textslice: content.toJSON() })) : state.tr;
- return replaced.setSelection(new TextSelection(replaced.doc.resolve(end + 1))).setStoredMarks([...node.marks, ...sm]);
+ return replaced.setSelection(new TextSelection(replaced.doc.resolve(end))).setStoredMarks([...node.marks, ...sm]);
}),
new InputRule(new RegExp(/%\)/), (state, match, start, end) => {
diff --git a/src/client/views/nodes/formattedText/SummaryView.tsx b/src/client/views/nodes/formattedText/SummaryView.tsx
index 1fe6d822b..3355e4529 100644
--- a/src/client/views/nodes/formattedText/SummaryView.tsx
+++ b/src/client/views/nodes/formattedText/SummaryView.tsx
@@ -42,7 +42,7 @@ export class SummaryView {
className = (visible: boolean) => 'formattedTextBox-summarizer' + (visible ? '' : '-collapsed');
destroy() {
- // ReactDOM.unmountComponentAtNode(this.dom);
+ this.root.unmount();
}
selectNode() {}
diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts
index 00c41e187..7e17008bb 100644
--- a/src/client/views/nodes/formattedText/marks_rts.ts
+++ b/src/client/views/nodes/formattedText/marks_rts.ts
@@ -46,7 +46,7 @@ export const marks: { [index: string]: MarkSpec } = {
toDOM(node: any) {
const targethrefs = node.attrs.allAnchors.reduce((p: string, item: { href: string; title: string; anchorId: string }) => (p ? p + ' ' + item.href : item.href), '');
const anchorids = node.attrs.allAnchors.reduce((p: string, item: { href: string; title: string; anchorId: string }) => (p ? p + ' ' + item.anchorId : item.anchorId), '');
- return ['a', { class: anchorids, 'data-targethrefs': targethrefs, 'data-linkdoc': node.attrs.linkDoc, title: node.attrs.title, location: node.attrs.location, style: `background: lightBlue` }, 0];
+ return ['a', { class: anchorids, 'data-targethrefs': targethrefs, 'data-noPreview': 'true', 'data-linkdoc': node.attrs.linkDoc, title: node.attrs.title, location: node.attrs.location, style: `background: lightBlue` }, 0];
},
},
noAutoLinkAnchor: {
@@ -75,6 +75,7 @@ export const marks: { [index: string]: MarkSpec } = {
allAnchors: { default: [] as { href: string; title: string; anchorId: string }[] },
location: { default: null },
title: { default: null },
+ noPreview: { default: false },
docref: { default: false }, // flags whether the linked text comes from a document within Dash. If so, an attribution label is appended after the text
},
inclusive: false,
@@ -85,6 +86,7 @@ export const marks: { [index: string]: MarkSpec } = {
return {
location: dom.getAttribute('location'),
title: dom.getAttribute('title'),
+ noPreview: dom.getAttribute('noPreview'),
};
},
},
@@ -95,12 +97,9 @@ export const marks: { [index: string]: MarkSpec } = {
return node.attrs.docref && node.attrs.title
? [
'div',
- ['span', `"`],
['span', 0],
- ['span', `"`],
- ['br'],
[
- 'a',
+ 'span',
{
...node.attrs,
class: 'prosemirror-attribution',
@@ -108,19 +107,8 @@ export const marks: { [index: string]: MarkSpec } = {
},
node.attrs.title,
],
- ['br'],
]
- : //node.attrs.allLinks.length === 1 ?
- ['a', { class: anchorids, 'data-targethrefs': targethrefs, title: node.attrs.title, location: node.attrs.location, style: `text-decoration: underline` }, 0];
- // ["div", { class: "prosemirror-anchor" },
- // ["span", { class: "prosemirror-linkBtn" },
- // ["a", { ...node.attrs, class: linkids, "data-targetids": targetids, title: `${node.attrs.title}` }, 0],
- // ["input", { class: "prosemirror-hrefoptions" }],
- // ],
- // ["div", { class: "prosemirror-links" }, ...node.attrs.allLinks.map((item: { href: string, title: string }) =>
- // ["a", { class: "prosemirror-dropdownlink", href: item.href }, item.title]
- // )]
- // ];
+ : ['a', { class: anchorids, 'data-targethrefs': targethrefs, title: node.attrs.title, 'data-noPreview': node.attrs.noPreview, location: node.attrs.location, style: `text-decoration: underline; cursor: default` }, 0];
},
},
@@ -361,7 +349,7 @@ export const marks: { [index: string]: MarkSpec } = {
group: 'inline',
toDOM(node: any) {
const uid = node.attrs.userid.replace('.', '').replace('@', '');
- const min = Math.round(node.attrs.modified / 12);
+ const min = Math.round(node.attrs.modified / 60);
const hr = Math.round(min / 60);
const day = Math.round(hr / 60 / 24);
const remote = node.attrs.userid !== Doc.CurrentUserEmail ? ' UM-remote' : '';
diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts
index aa2475dca..f27fb18e2 100644
--- a/src/client/views/nodes/formattedText/nodes_rts.ts
+++ b/src/client/views/nodes/formattedText/nodes_rts.ts
@@ -176,7 +176,7 @@ export const nodes: { [index: string]: NodeSpec } = {
dashComment: {
attrs: {
- docid: { default: '' },
+ docId: { default: '' },
},
inline: true,
group: 'inline',
@@ -213,7 +213,7 @@ export const nodes: { [index: string]: NodeSpec } = {
title: { default: null },
float: { default: 'left' },
location: { default: 'add:right' },
- docid: { default: '' },
+ docId: { default: '' },
},
group: 'inline',
draggable: true,
@@ -246,8 +246,8 @@ export const nodes: { [index: string]: NodeSpec } = {
float: { default: 'right' },
hidden: { default: false }, // whether dashComment node has toggle the dashDoc's display off
fieldKey: { default: '' },
- docid: { default: '' },
- alias: { default: '' },
+ docId: { default: '' },
+ embedding: { default: '' },
},
group: 'inline',
draggable: false,
@@ -261,7 +261,7 @@ export const nodes: { [index: string]: NodeSpec } = {
inline: true,
attrs: {
fieldKey: { default: '' },
- docid: { default: '' },
+ docId: { default: '' },
hideKey: { default: false },
editable: { default: true },
},
diff --git a/src/client/views/nodes/trails/PresBox.scss b/src/client/views/nodes/trails/PresBox.scss
index 4d60a02f1..bf56b4d9e 100644
--- a/src/client/views/nodes/trails/PresBox.scss
+++ b/src/client/views/nodes/trails/PresBox.scss
@@ -12,7 +12,7 @@
height: 100%;
min-height: 35px;
letter-spacing: 2px;
- overflow: hidden;
+ //overflow: hidden;
transition: 0.7s opacity ease;
.presBox-listCont {
@@ -938,14 +938,20 @@
min-width: 15px;
max-width: 100px;
left: 8px;
+ margin: auto;
+ margin-left: unset;
+ height: 100%;
}
.presBox-presentPanel {
display: flex;
justify-self: end;
width: 100%;
- max-width: 300px;
- min-width: 150px;
+ margin: auto;
+ margin-right: unset;
+ height: 100%;
+ position: relative;
+ user-select: none;
}
select {
@@ -955,7 +961,7 @@
.presBox-button {
cursor: pointer;
- height: 25px;
+ //height: 100%;
border-radius: 5px;
display: none;
justify-content: center;
@@ -986,6 +992,9 @@
width: max-content;
position: absolute;
right: 10px;
+ margin: auto;
+ margin-right: unset;
+ height: 100%;
.present-icon {
margin-right: 7px;
@@ -999,9 +1008,16 @@
position: relative;
display: inline-block;
left: 8px;
+ margin: auto;
+ margin-left: unset;
}
}
+.presBox-buttons.inOverlay {
+ padding-top: unset;
+ padding-bottom: unset;
+}
+
.presBox-backward,
.presBox-forward {
width: 25px;
@@ -1052,6 +1068,8 @@
font-size: 30px;
position: absolute;
min-width: 50px;
+ margin: auto;
+ margin-left: unset;
}
}
@@ -1087,13 +1105,12 @@
color: $white;
border-radius: 5px;
grid-template-rows: 100%;
- height: 25;
+ height: 100%;
width: max-content;
min-width: max-content;
justify-content: space-evenly;
align-items: center;
display: flex;
- position: absolute;
transition: all 0.2s;
.presPanel-button-text {
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 4e8aed8a6..ef1c3911c 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -3,44 +3,65 @@ 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 { ColorState, SketchPicker } from 'react-color';
-import { Bounce, Fade, Flip, LightSpeed, Roll, Rotate, Zoom } from 'react-reveal';
-import { Doc, DocListCast, FieldResult, 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 { InkTool } from '../../../../fields/InkField';
+import { InkField } from '../../../../fields/InkField';
import { List } from '../../../../fields/List';
import { ObjectField } from '../../../../fields/ObjectField';
import { listSpec } from '../../../../fields/Schema';
+import { ComputedField, ScriptField } from '../../../../fields/ScriptField';
import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types';
-import { emptyFunction, returnFalse, returnOne, returnTrue, setupMoveUpEvents, StopEvent } from '../../../../Utils';
+import { AudioField } from '../../../../fields/URLField';
+import { emptyFunction, emptyPath, returnFalse, returnOne, setupMoveUpEvents, StopEvent } from '../../../../Utils';
import { DocServer } from '../../../DocServer';
import { Docs } from '../../../documents/Documents';
import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
import { DocumentManager } from '../../../util/DocumentManager';
import { ScriptingGlobals } from '../../../util/ScriptingGlobals';
import { SelectionManager } from '../../../util/SelectionManager';
+import { SerializationHelper } from '../../../util/SerializationHelper';
import { SettingsManager } from '../../../util/SettingsManager';
import { undoBatch, UndoManager } from '../../../util/UndoManager';
import { CollectionDockingView } from '../../collections/CollectionDockingView';
-import { CollectionFreeFormView, MarqueeViewBounds } from '../../collections/collectionFreeForm';
+import { CollectionFreeFormView, computeTimelineLayout, MarqueeViewBounds } from '../../collections/collectionFreeForm';
+import { CollectionStackedTimeline } from '../../collections/CollectionStackedTimeline';
import { CollectionView } from '../../collections/CollectionView';
import { TabDocView } from '../../collections/TabDocView';
import { ViewBoxBaseComponent } from '../../DocComponent';
import { Colors } from '../../global/globalEnums';
import { LightboxView } from '../../LightboxView';
-import { CollectionFreeFormDocumentView } from '../CollectionFreeFormDocumentView';
+import { DocFocusOptions, DocumentView, OpenWhere, OpenWhereMod } from '../DocumentView';
import { FieldView, FieldViewProps } from '../FieldView';
import { ScriptingBox } from '../ScriptingBox';
import './PresBox.scss';
-import { PresEffect, PresMovement, PresStatus } from './PresEnums';
-
+import { PresEffect, PresEffectDirection, PresMovement, PresStatus } from './PresEnums';
+const { Howl } = require('howler');
+
+export interface pinDataTypes {
+ scrollable?: boolean;
+ dataviz?: number[];
+ pannable?: boolean;
+ type_collection?: boolean;
+ inkable?: boolean;
+ filters?: boolean;
+ pivot?: boolean;
+ temporal?: boolean;
+ clippable?: boolean;
+ datarange?: boolean;
+ dataview?: boolean;
+ poslayoutview?: boolean;
+ dataannos?: boolean;
+}
export interface PinProps {
audioRange?: boolean;
activeFrame?: number;
+ currentFrame?: number;
hidePresBox?: boolean;
pinViewport?: MarqueeViewBounds; // pin a specific viewport on a freeform view (use MarqueeView.CurViewBounds to compute if no region has been selected)
pinDocLayout?: boolean; // pin layout info (width/height/x/y)
- pinDocContent?: boolean; // pin data info (scroll/pan/zoom/text)
+ pinAudioPlay?: boolean; // pin audio annotation
+ pinData?: pinDataTypes;
}
@observer
@@ -48,40 +69,22 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
public static LayoutString(fieldKey: string) {
return FieldView.LayoutString(PresBox, fieldKey);
}
+ static navigateToDocScript: ScriptField;
- /**
- * returns an entrance animation effect function to wrap a JSX element
- * @param presEffectDoc presentation effects document that specifies the animation effect parameters
- * @returns a function that will wrap a JSX animation element wrapping any JSX element
- */
- public static AnimationEffect(renderDoc: JSX.Element, presEffectDoc: Doc) {
- const effectProps = {
- left: presEffectDoc.presEffectDirection === PresEffect.Left,
- right: presEffectDoc.presEffectDirection === PresEffect.Right,
- top: presEffectDoc.presEffectDirection === PresEffect.Top,
- bottom: presEffectDoc.presEffectDirection === PresEffect.Bottom,
- opposite: true,
- delay: NumCast(presEffectDoc.presTransition),
- };
- //prettier-ignore
- switch (StrCast(presEffectDoc.presEffect)) {
- default:
- case PresEffect.None: return renderDoc;
- case PresEffect.Zoom: return <Zoom {...effectProps}>{renderDoc}</Zoom>;
- case PresEffect.Fade: return <Fade {...effectProps}>{renderDoc}</Fade>;
- case PresEffect.Flip: return <Flip {...effectProps}>{renderDoc}</Flip>;
- case PresEffect.Rotate: return <Rotate {...effectProps}>{renderDoc}</Rotate>;
- case PresEffect.Bounce: return <Bounce {...effectProps}>{renderDoc}</Bounce>;
- case PresEffect.Roll: return <Roll {...effectProps}>{renderDoc}</Roll>;
- case PresEffect.Lightspeed: return <LightSpeed {...effectProps}>{renderDoc}</LightSpeed>;
+ constructor(props: any) {
+ super(props);
+ if (!PresBox.navigateToDocScript) {
+ PresBox.navigateToDocScript = ScriptField.MakeFunction('navigateToDoc(self.presentationTargetDoc, self)')!;
}
}
private _disposers: { [name: string]: IReactionDisposer } = {};
public selectedArray = new ObservableSet<Doc>();
+ _batch: UndoManager.Batch | undefined = undefined; // undo batch for dragging sliders which generate multiple scene edit events as the cursor moves
+ _keyTimer: NodeJS.Timeout | undefined; // timer for turning off transition flag when key frame change has completed. Need to clear this if you do a second navigation before first finishes, or else first timer can go off during second naviation.
+ _unmounting = false; // flag that view is unmounting used to block RemFromMap from deleting things
@observable public static Instance: PresBox;
- @observable static startMarquee: boolean = false; // onclick "+ new slide" in presentation mode, set as true, then when marquee selection finish, onPointerUp automatically triggers PinWithView
@observable _isChildActive = false;
@observable _moveOnFromAudio: boolean = true;
@@ -93,18 +96,18 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@observable _expandBoolean: boolean = false;
@observable _transitionTools: boolean = false;
@observable _newDocumentTools: boolean = false;
- @observable _progressivizeTools: boolean = false;
@observable _openMovementDropdown: boolean = false;
@observable _openEffectDropdown: boolean = false;
+ @observable _openBulletEffectDropdown: boolean = false;
@observable _presentTools: boolean = false;
@observable _treeViewMap: Map<Doc, number> = new Map();
@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');
@@ -119,17 +122,21 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
return NumCast(this.rootDoc._itemIndex);
}
@computed get activeItem() {
- return Cast(this.childDocs[NumCast(this.rootDoc._itemIndex)], Doc, null);
+ return DocCast(this.childDocs[NumCast(this.rootDoc._itemIndex)]);
}
@computed get targetDoc() {
return Cast(this.activeItem?.presentationTargetDoc, Doc, null);
}
+ public static targetRenderedDoc = (doc: Doc) => {
+ const targetDoc = Cast(doc?.presentationTargetDoc, Doc, null);
+ return targetDoc?.layout_unrendered ? DocCast(targetDoc.annotationOn) : targetDoc;
+ };
@computed get scrollable() {
- if (this.targetDoc.type === DocumentType.PDF || this.targetDoc.type === DocumentType.WEB || this.targetDoc.type === DocumentType.RTF || 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() {
@@ -144,21 +151,19 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
isActiveItemTarget = (layoutDoc: Doc) => this.activeItem?.presentationTargetDoc === layoutDoc;
clearSelectedArray = () => this.selectedArray.clear();
- addToSelectedArray = (doc: Doc) => this.selectedArray.add(doc);
- removeFromSelectedArray = (doc: Doc) => this.selectedArray.delete(doc);
+ addToSelectedArray = action((doc: Doc) => this.selectedArray.add(doc));
+ removeFromSelectedArray = action((doc: Doc) => this.selectedArray.delete(doc));
- _unmounting = false;
@action
componentWillUnmount() {
this._unmounting = true;
+ if (this._presTimer) clearTimeout(this._presTimer);
document.removeEventListener('keydown', PresBox.keyEventsWrapper, true);
this.resetPresentation();
- // Turn of progressivize editors
this.turnOffEdit(true);
Object.values(this._disposers).forEach(disposer => disposer?.());
}
- @action
componentDidMount() {
this._disposers.keyboard = reaction(
() => this.selectedDoc,
@@ -182,14 +187,31 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
);
this.props.setContentView?.(this);
this._unmounting = false;
- this.rootDoc._forceRenderEngine = 'timeline';
- this.layoutDoc.presStatus = PresStatus.Edit;
- this.layoutDoc._gridGap = 0;
- this.layoutDoc._yMargin = 0;
+ if (this.props.renderDepth > 0) {
+ runInAction(() => {
+ this.rootDoc._forceRenderEngine = computeTimelineLayout.name;
+ this.layoutDoc._gridGap = 0;
+ this.layoutDoc._yMargin = 0;
+ });
+ }
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,
+ editing => {
+ if (editing) {
+ this.childDocs.forEach(doc => {
+ if (doc.presIndexed !== undefined) {
+ this.progressivizedItems(doc)?.forEach(indexedDoc => (indexedDoc.opacity = undefined));
+ doc.presIndexed = Math.min(this.progressivizedItems(doc)?.length ?? 0, 1);
+ }
+ });
+ }
+ }
);
}
@@ -199,17 +221,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
PresBox.Instance = this;
};
- // There are still other internal frames and should go through all frames before going to next slide
- nextInternalFrame = (targetDoc: Doc, activeItem: Doc) => {
- const currentFrame = Cast(targetDoc?._currentFrame, 'number', null);
- const childDocs = DocListCast(targetDoc[Doc.LayoutFieldKey(targetDoc)]);
- targetDoc._viewTransition = 'all 1s';
- setTimeout(() => (targetDoc._viewTransition = undefined), 1010);
- this.nextKeyframe(targetDoc, activeItem);
- if (activeItem.presProgressivize) CollectionFreeFormDocumentView.updateKeyframe(childDocs, currentFrame || 0, targetDoc);
- else targetDoc.keyFrameEditing = true;
- };
-
_mediaTimer!: [NodeJS.Timeout, Doc];
// 'Play on next' for audio or video therefore first navigate to the audio/video before it should be played
startTempMedia = (targetDoc: Doc, activeItem: Doc) => {
@@ -221,7 +232,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
};
stopTempMedia = (targetDocField: FieldResult) => {
- const targetDoc = Cast(targetDocField, Doc, null);
+ const targetDoc = DocCast(DocCast(targetDocField).annotationOn) ?? DocCast(targetDocField);
if ([DocumentType.VID, DocumentType.AUDIO].includes(targetDoc.type as any)) {
const targMedia = DocumentManager.Instance.getDocumentView(targetDoc);
targMedia?.ComponentView?.Pause?.();
@@ -232,38 +243,91 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
// TODO: to handle child slides (entering into subtrail and exiting), also the next() and back() functions
// No more frames in current doc and next slide is defined, therefore move to next slide
nextSlide = (slideNum?: number) => {
- let nextSelected = slideNum ?? this.itemIndex + 1;
- this.gotoDocument(nextSelected, this.activeItem);
- for (nextSelected = nextSelected + 1; nextSelected < this.childDocs.length; nextSelected++) {
- if (this.childDocs[nextSelected].groupWithUp) {
- this.gotoDocument(nextSelected, this.activeItem, true);
- } else {
- break;
- }
- }
+ const nextSlideInd = slideNum ?? this.itemIndex + 1;
+ let curSlideInd = nextSlideInd;
+ //CollectionStackedTimeline.CurrentlyPlaying?.map(clipView => clipView?.ComponentView?.Pause?.());
+ this.clearSelectedArray();
+ const doGroupWithUp =
+ (nextSelected: number, force = false) =>
+ () => {
+ if (nextSelected < this.childDocs.length) {
+ if (force || this.childDocs[nextSelected].groupWithUp) {
+ this.addToSelectedArray(this.childDocs[nextSelected]);
+ const serial = nextSelected + 1 < this.childDocs.length && NumCast(this.childDocs[nextSelected + 1].groupWithUp) > 1;
+ if (serial) {
+ this.gotoDocument(nextSelected, this.activeItem, true, async () => {
+ const waitTime = NumCast(this.activeItem.presDuration) - NumCast(this.activeItem.presTransition);
+ await new Promise<void>(res => setTimeout(() => res(), Math.max(0, waitTime)));
+ doGroupWithUp(nextSelected + 1)();
+ });
+ } else {
+ this.gotoDocument(nextSelected, this.activeItem, true);
+ curSlideInd = this.itemIndex;
+ doGroupWithUp(nextSelected + 1)();
+ }
+ }
+ }
+ };
+ doGroupWithUp(curSlideInd, true)();
};
+ // docs within a slide target that will be progressively revealed
+ progressivizedItems = (doc: Doc) => {
+ 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.layout_unrendered);
+ }
+ };
// Called when the user activates 'next' - to move to the next part of the pres. trail
@action
next = () => {
- const activeNext = Cast(this.childDocs[this.itemIndex + 1], Doc, null);
- const activeItem: Doc = this.activeItem;
- const targetDoc: Doc = this.targetDoc;
- const lastFrame = Cast(targetDoc?.lastFrame, 'number', null);
- const curFrame = NumCast(targetDoc?._currentFrame);
- let internalFrames: boolean = false;
- if (activeItem.presProgressivize || activeItem.zoomProgressivize || targetDoc.scrollProgressivize) internalFrames = true;
- if (internalFrames && lastFrame !== undefined && curFrame < lastFrame) {
- // Case 1: There are still other frames and should go through all frames before going to next slide
- this.nextInternalFrame(targetDoc, activeItem);
- } else if (this.childDocs[this.itemIndex + 1] !== undefined) {
- // Case 2: No more frames in current doc and next slide is defined, therefore move to next slide
+ const progressiveReveal = (first: boolean) => {
+ const presIndexed = Cast(this.activeItem?.presIndexed, 'number', null);
+ if (presIndexed !== undefined) {
+ const targetRenderedDoc = PresBox.targetRenderedDoc(this.activeItem);
+ targetRenderedDoc._dataTransition = 'all 1s';
+ targetRenderedDoc.opacity = 1;
+ setTimeout(() => (targetRenderedDoc._dataTransition = 'inherit'), 1000);
+ const listItems = this.progressivizedItems(this.activeItem);
+ if (listItems && presIndexed < listItems.length) {
+ if (!first) {
+ const listItemDoc = listItems[presIndexed];
+ const targetView = listItems && DocumentManager.Instance.getFirstDocumentView(listItemDoc);
+ Doc.linkFollowUnhighlight();
+ Doc.HighlightDoc(listItemDoc);
+ listItemDoc.presEffect = this.activeItem.presBulletEffect;
+ listItemDoc.presTransition = 500;
+ targetView?.setAnimEffect(listItemDoc, 500);
+ if (targetView?.docView && this.activeItem.presBulletExpand) {
+ 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;
+ }
+ return true;
+ }
+ }
+ };
+ if (progressiveReveal(false)) return true;
+ if (this.childDocs[this.itemIndex + 1] !== undefined) {
+ // Case 1: No more frames in current doc and next slide is defined, therefore move to next slide
const slides = DocListCast(this.rootDoc[StrCast(this.presFieldKey, 'data')]);
const curLast = this.selectedArray.size ? Math.max(...Array.from(this.selectedArray).map(d => slides.indexOf(DocCast(d)))) : this.itemIndex;
- this.nextSlide(curLast + 1);
- } else if (this.childDocs[this.itemIndex + 1] === undefined && (this.layoutDoc.presLoop || this.layoutDoc.presStatus === PresStatus.Edit)) {
- // Case 3: Last slide and presLoop is toggled ON or it is in Edit mode
- this.nextSlide(0);
+ this.nextSlide(curLast + 1 === this.childDocs.length ? (this.layoutDoc.presLoop ? 0 : curLast) : curLast + 1);
+ progressiveReveal(true); // shows first progressive document, but without a transition effect
+ } else {
+ if (this.childDocs[this.itemIndex + 1] === undefined && (this.layoutDoc.presLoop || this.layoutDoc.presStatus === PresStatus.Edit)) {
+ // Case 2: Last slide and presLoop is toggled ON or it is in Edit mode
+ this.nextSlide(0);
+ progressiveReveal(true); // shows first progressive document, but without a transition effect
+ }
+ return 0;
}
return this.itemIndex;
};
@@ -272,29 +336,20 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@action
back = () => {
const activeItem: Doc = this.activeItem;
- const targetDoc: Doc = this.targetDoc;
- const prevItem = Cast(this.childDocs[Math.max(0, this.itemIndex - 1)], Doc, null);
- const prevTargetDoc = Cast(prevItem.presentationTargetDoc, Doc, null);
- const lastFrame = Cast(targetDoc.lastFrame, 'number', null);
- const curFrame = NumCast(targetDoc._currentFrame);
let prevSelected = this.itemIndex;
// Functionality for group with up
let didZoom = activeItem.presMovement;
for (; prevSelected > 0 && this.childDocs[Math.max(0, prevSelected - 1)].groupWithUp; prevSelected--) {
didZoom = didZoom === 'none' ? this.childDocs[prevSelected].presMovement : didZoom;
}
- if (lastFrame !== undefined && curFrame >= 1) {
- // Case 1: There are still other frames and should go through all frames before going to previous slide
- this.prevKeyframe(targetDoc, activeItem);
- } else if (activeItem && this.childDocs[this.itemIndex - 1] !== undefined) {
+ if (activeItem && this.childDocs[this.itemIndex - 1] !== undefined) {
// Case 2: There are no other frames so it should go to the previous slide
prevSelected = Math.max(0, prevSelected - 1);
this.nextSlide(prevSelected);
this.rootDoc._itemIndex = prevSelected;
- if (NumCast(prevTargetDoc.lastFrame) > 0) prevTargetDoc._currentFrame = NumCast(prevTargetDoc.lastFrame);
} else if (this.childDocs[this.itemIndex - 1] === undefined && this.layoutDoc.presLoop) {
// Case 3: Pres loop is on so it should go to the last slide
- this.gotoDocument(this.childDocs.length - 1, activeItem);
+ this.nextSlide(this.childDocs.length - 1);
}
return this.itemIndex;
};
@@ -302,24 +357,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
//The function that is called when a document is clicked or reached through next or back.
//it'll also execute the necessary actions if presentation is playing.
@undoBatch
- public gotoDocument = action((index: number, from?: Doc, group?: boolean) => {
+ public gotoDocument = action((index: number, from?: Doc, group?: boolean, finished?: () => void) => {
Doc.UnBrushAllDocs();
-
if (index >= 0 && index < this.childDocs.length) {
this.rootDoc._itemIndex = index;
- const activeItem: Doc = this.activeItem;
- const targetDoc: Doc = this.targetDoc;
- if (activeItem.presActiveFrame !== undefined) {
- const transTime = NumCast(activeItem.presTransition, 500);
- const context = DocCast(DocCast(activeItem.presentationTargetDoc).context);
- if (context) {
- const contextView = DocumentManager.Instance.getFirstDocumentView(context);
- if (contextView?.ComponentView) {
- CollectionFreeFormDocumentView.gotoKeyframe((contextView.ComponentView as CollectionFreeFormView).childDocs.slice(), transTime);
- context._currentFrame = NumCast(activeItem.presActiveFrame);
- }
- }
- }
if (from?.mediaStopTriggerList && this.layoutDoc.presStatus !== PresStatus.Edit) {
DocListCast(from.mediaStopTriggerList).forEach(this.stopTempMedia);
}
@@ -327,142 +368,316 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
this.stopTempMedia(from.presentationTargetDoc);
}
// If next slide is audio / video 'Play automatically' then the next slide should be played
- if (this.layoutDoc.presStatus !== PresStatus.Edit && (targetDoc.type === DocumentType.AUDIO || targetDoc.type === DocumentType.VID) && activeItem.mediaStart === 'auto') {
- this.startTempMedia(targetDoc, activeItem);
- }
- if (targetDoc) {
- Doc.linkFollowHighlight(targetDoc.annotationOn instanceof Doc ? [targetDoc, targetDoc.annotationOn] : targetDoc);
- targetDoc && runInAction(() => (targetDoc.focusSpeed = activeItem.presMovement === PresMovement.Jump ? 0 : NumCast(activeItem.presTransition, 500)));
- setTimeout(() => (targetDoc.focusSpeed = undefined), NumCast(targetDoc.focusSpeed) + 10);
- }
- if (targetDoc?.lastFrame !== undefined) {
- targetDoc._currentFrame = 0;
+ if (this.layoutDoc.presStatus !== PresStatus.Edit && (this.targetDoc.type === DocumentType.AUDIO || this.targetDoc.type === DocumentType.VID) && this.activeItem.mediaStart === 'auto') {
+ this.startTempMedia(this.targetDoc, this.activeItem);
}
if (!group) this.clearSelectedArray();
this.childDocs[index] && this.addToSelectedArray(this.childDocs[index]); //Update selected array
this.turnOffEdit();
- this.navigateToActiveItem(); //Handles movement to element only when presTrail is list
- this.onHideDocument(); //Handles hide after/before
+ this.navigateToActiveItem(finished); //Handles movement to element only when presTrail is list
+ this.doHideBeforeAfter(); //Handles hide after/before
}
});
- static pinDataTypes(target: Doc) {
- const scrollable = [DocumentType.PDF, DocumentType.RTF, DocumentType.WEB].includes(target.type as any) || target._viewType === CollectionViewType.Stacking;
- const pannable = [DocumentType.IMG].includes(target.type as any) || (target.type === DocumentType.COL && target._viewType === CollectionViewType.Freeform);
- const temporal = [DocumentType.AUDIO, DocumentType.VID].includes(target.type as any);
- const clippable = [DocumentType.COMPARISON].includes(target.type as any);
- const dataview = [DocumentType.INK, DocumentType.COL].includes(target.type as any) && target.activeFrame === undefined;
- const poslayoutview = [DocumentType.COL].includes(target.type as any) && target.activeFrame === undefined;
- const textview = [DocumentType.RTF].includes(target.type as any) && target.activeFrame === undefined;
- return { scrollable, pannable, temporal, clippable, dataview, textview, poslayoutview };
+ 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?._type_collection === CollectionViewType.Stacking;
+ const pannable = [DocumentType.IMG, DocumentType.PDF].includes(targetType) || (targetType === DocumentType.COL && target?._type_collection === CollectionViewType.Freeform);
+ 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 type_collection = targetType === DocumentType.COL;
+ const filters = true;
+ const pivot = true;
+ const dataannos = false;
+ return { scrollable, pannable, inkable, type_collection, pivot, filters, temporal, clippable, dataview, datarange, poslayoutview, dataannos };
}
@action
- static restoreTargetDocView(bestTarget: Doc, activeItem: Doc) {
- const transTime = NumCast(activeItem.presTransition, 500);
- const presTransitionTime = `all ${transTime}ms`;
- const { scrollable, pannable, temporal, clippable, dataview, textview, poslayoutview } = this.pinDataTypes(bestTarget);
- bestTarget._viewTransition = presTransitionTime;
- if (clippable) bestTarget._clipWidth = activeItem.presPinClipWidth;
- if (temporal) bestTarget._currentTimecode = activeItem.presStartTime;
- if (scrollable) {
- bestTarget._scrollTop = activeItem.presPinViewScroll;
- const contentBounds = Cast(activeItem.presPinViewBounds, listSpec('number'));
- if (contentBounds) {
- const dv = DocumentManager.Instance.getDocumentView(bestTarget)?.ComponentView;
- dv?.brushView?.({ panX: (contentBounds[0] + contentBounds[2]) / 2, panY: (contentBounds[1] + contentBounds[3]) / 2, width: contentBounds[2] - contentBounds[0], height: contentBounds[3] - contentBounds[1] });
+ 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?.layout_unrendered ? DocCast(targetDoc?.annotationOn) : targetDoc);
+ if (!bestTarget || activeItem === bestTarget) return;
+ let changed = false;
+ if (pinDocLayout) {
+ if (
+ bestTarget.x !== NumCast(activeItem.presX, NumCast(bestTarget.x)) ||
+ bestTarget.y !== NumCast(activeItem.presY, NumCast(bestTarget.y)) ||
+ bestTarget.rotation !== NumCast(activeItem.presRotation, NumCast(bestTarget.rotation)) ||
+ bestTarget.width !== NumCast(activeItem.presWidth, NumCast(bestTarget.width)) ||
+ bestTarget.height !== NumCast(activeItem.presHeight, NumCast(bestTarget.height))
+ ) {
+ bestTarget._dataTransition = `all ${transTime}ms`;
+ bestTarget.x = NumCast(activeItem.presX, NumCast(bestTarget.x));
+ bestTarget.y = NumCast(activeItem.presY, NumCast(bestTarget.y));
+ bestTarget.rotation = NumCast(activeItem.presRotation, NumCast(bestTarget.rotation));
+ bestTarget.width = NumCast(activeItem.presWidth, NumCast(bestTarget.width));
+ bestTarget.height = NumCast(activeItem.presHeight, NumCast(bestTarget.height));
+ setTimeout(() => (bestTarget._dataTransition = undefined), transTime + 10);
+ changed = true;
}
}
- if (dataview) Doc.GetProto(bestTarget)[Doc.LayoutFieldKey(bestTarget)] = activeItem.presData instanceof ObjectField ? activeItem.presData[Copy]() : activeItem.presData;
- if (textview) Doc.GetProto(bestTarget)[Doc.LayoutFieldKey(bestTarget)] = activeItem.presData instanceof ObjectField ? activeItem.presData[Copy]() : activeItem.presData;
- if (poslayoutview) {
+
+ const activeFrame = activeItem.presActiveFrame ?? activeItem.presCurrentFrame;
+ if (activeFrame !== undefined) {
+ const transTime = NumCast(activeItem.presTransition, 500);
+ const acontext = activeItem.presActiveFrame !== undefined ? DocCast(DocCast(activeItem.presentationTargetDoc).embedContainer) : DocCast(activeItem.presentationTargetDoc);
+ const context = DocCast(acontext)?.annotationOn ? DocCast(DocCast(acontext).annotationOn) : acontext;
+ if (context) {
+ const ffview = DocumentManager.Instance.getFirstDocumentView(context)?.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView;
+ if (ffview?.childDocs) {
+ PresBox.Instance._keyTimer = CollectionFreeFormView.gotoKeyframe(PresBox.Instance._keyTimer, ffview.childDocs, transTime);
+ ffview.rootDoc._currentFrame = NumCast(activeFrame);
+ }
+ }
+ }
+ if ((pinDataTypes?.dataview && activeItem.presData !== undefined) || (!pinDataTypes && activeItem.presData !== undefined)) {
+ bestTarget._dataTransition = `all ${transTime}ms`;
+ const fkey = Doc.LayoutFieldKey(bestTarget);
+ const setData = bestTargetView?.ComponentView?.setData;
+ if (setData) setData(activeItem.presData);
+ else Doc.GetProto(bestTarget)[fkey] = activeItem.presData instanceof ObjectField ? activeItem.presData[Copy]() : activeItem.presData;
+ bestTarget[fkey + '_usePath'] = activeItem.presUsePath;
+ setTimeout(() => (bestTarget._dataTransition = undefined), transTime + 10);
+ }
+ if (pinDataTypes?.datarange || (!pinDataTypes && activeItem.presXRange !== undefined)) {
+ if (bestTarget.xRange !== activeItem.presXRange) {
+ bestTarget.xRange = (activeItem.presXRange as ObjectField)?.[Copy]();
+ changed = true;
+ }
+ if (bestTarget.yRange !== activeItem.presYRange) {
+ bestTarget.yRange = (activeItem.presYRange as ObjectField)?.[Copy]();
+ changed = true;
+ }
+ }
+ if (pinDataTypes?.clippable || (!pinDataTypes && activeItem.presClipWidth !== undefined)) {
+ const fkey = '_' + Doc.LayoutFieldKey(bestTarget);
+ if (bestTarget[fkey + '_clipWidth'] !== activeItem.presClipWidth) {
+ bestTarget[fkey + '_clipWidth'] = activeItem.presClipWidth;
+ changed = true;
+ }
+ }
+ if (pinDataTypes?.temporal || (!pinDataTypes && activeItem.presStartTime !== undefined)) {
+ if (bestTarget._layout_currentTimecode !== activeItem.presStartTime) {
+ bestTarget._layout_currentTimecode = activeItem.presStartTime;
+ changed = true;
+ }
+ }
+ if (pinDataTypes?.inkable || (!pinDataTypes && (activeItem.presFillColor !== undefined || activeItem.color !== undefined))) {
+ if (bestTarget.fillColor !== activeItem.presFillColor) {
+ Doc.GetProto(bestTarget).fillColor = activeItem.presFillColor;
+ changed = true;
+ }
+ if (bestTarget.color !== activeItem.presColor) {
+ Doc.GetProto(bestTarget).color = activeItem.presColor;
+ changed = true;
+ }
+ if (bestTarget.width !== activeItem.width) {
+ bestTarget._width = NumCast(activeItem.presWidth, NumCast(bestTarget.width));
+ changed = true;
+ }
+ if (bestTarget.height !== activeItem.height) {
+ bestTarget._height = NumCast(activeItem.presHeight, NumCast(bestTarget.height));
+ changed = true;
+ }
+ }
+ 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.childFilters !== activeItem.presDocFilters) {
+ bestTarget.childFilters = ObjectField.MakeCopy(activeItem.presDocFilters as ObjectField) || new List<string>([]);
+ changed = true;
+ }
+ }
+
+ if ((pinDataTypes?.pivot && activeItem.presPivotField !== undefined) || (!pinDataTypes && activeItem.presPivotField !== undefined)) {
+ if (bestTarget.pivotField !== activeItem.presPivotField) {
+ bestTarget.pivotField = activeItem.presPivotField;
+ bestTarget._prevFilterIndex = 1; // need to revisit this...see CollectionTimeView
+ changed = true;
+ }
+ }
+ if (bestTargetView?.ComponentView?.restoreView?.(activeItem)) {
+ changed = true;
+ }
+
+ if (pinDataTypes?.scrollable || (!pinDataTypes && activeItem.presViewScroll !== undefined)) {
+ if (bestTarget._layout_scrollTop !== activeItem.presViewScroll) {
+ bestTarget._layout_scrollTop = activeItem.presViewScroll;
+ changed = true;
+ const contentBounds = Cast(activeItem.presPinViewBounds, listSpec('number'));
+ if (contentBounds) {
+ const dv = DocumentManager.Instance.getDocumentView(bestTarget)?.ComponentView;
+ dv?.brushView?.({ panX: (contentBounds[0] + contentBounds[2]) / 2, panY: (contentBounds[1] + contentBounds[3]) / 2, width: contentBounds[2] - contentBounds[0], height: contentBounds[3] - contentBounds[1] });
+ }
+ }
+ }
+ if (pinDataTypes?.dataannos || (!pinDataTypes && activeItem.presAnnotations !== undefined)) {
+ const fkey = Doc.LayoutFieldKey(bestTarget);
+ 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.layout_unrendered && !newItems.includes(doc))
+ .map(doc => {
+ doc.hidden = true;
+ return doc;
+ });
+ const newList = new List<Doc>([...oldItems, ...hiddenItems, ...newItems]);
+ Doc.GetProto(bestTarget)[fkey + '_annotations'] = newList;
+ }
+ if (pinDataTypes?.poslayoutview || (!pinDataTypes && activeItem.presPinLayoutData !== undefined)) {
+ changed = true;
+ const layoutField = Doc.LayoutFieldKey(bestTarget);
+ const transitioned = new Set<Doc>();
StrListCast(activeItem.presPinLayoutData)
- .map(str => JSON.parse(str) as { id: string; x: number; y: number; w: number; h: number })
- .forEach(data => {
- const doc = DocServer.GetCachedRefField(data.id) as Doc;
- doc._dataTransition = presTransitionTime;
- doc.x = data.x;
- doc.y = data.y;
- doc._width = data.w;
- doc._height = data.h;
+ .map(str => JSON.parse(str) as { id: string; x: number; y: number; back: string; fill: string; w: number; h: number; data: string; text: string })
+ .forEach(async data => {
+ const doc = DocCast(DocServer.GetCachedRefField(data.id));
+ if (doc) {
+ transitioned.add(doc);
+ const field = !data.data ? undefined : await SerializationHelper.Deserialize(data.data);
+ const tfield = !data.text ? undefined : await SerializationHelper.Deserialize(data.text);
+ doc._dataTransition = `all ${transTime}ms`;
+ doc.x = data.x;
+ doc.y = data.y;
+ data.back && (doc._backgroundColor = data.back);
+ data.fill && (doc._fillColor = data.fill);
+ doc._width = data.w;
+ doc._height = data.h;
+ data.data && (Doc.GetProto(doc).data = field);
+ data.text && (Doc.GetProto(doc).text = tfield);
+ Doc.AddDocToList(Doc.GetProto(bestTarget), layoutField, doc);
+ }
});
- setTimeout(
- () =>
- StrListCast(activeItem.presPinLayoutData)
- .map(str => JSON.parse(str) as { id: string; x: number; y: number; w: number; h: number })
- .forEach(
- action(data => {
- const doc = DocServer.GetCachedRefField(data.id) as Doc;
- doc._dataTransition = undefined;
- })
- ),
- transTime + 10
- );
+ setTimeout(() => Array.from(transitioned).forEach(action(doc => (doc._dataTransition = undefined))), transTime + 10);
}
- if (pannable) {
+ if ((pinDataTypes?.pannable || (!pinDataTypes && (activeItem.presPinViewBounds !== undefined || activeItem.presPanX !== undefined || activeItem.presViewScale !== undefined))) && !bestTarget._isGroup) {
const contentBounds = Cast(activeItem.presPinViewBounds, listSpec('number'));
if (contentBounds) {
const viewport = { panX: (contentBounds[0] + contentBounds[2]) / 2, panY: (contentBounds[1] + contentBounds[3]) / 2, width: contentBounds[2] - contentBounds[0], height: contentBounds[3] - contentBounds[1] };
- bestTarget._panX = viewport.panX;
- bestTarget._panY = viewport.panY;
+ bestTarget._freeform_panX = viewport.panX;
+ bestTarget._freeform_panY = viewport.panY;
const dv = DocumentManager.Instance.getDocumentView(bestTarget);
if (dv) {
+ changed = true;
const computedScale = NumCast(activeItem.presZoom, 1) * Math.min(dv.props.PanelWidth() / viewport.width, dv.props.PanelHeight() / viewport.height);
- activeItem.presMovement === 'zoom' && (bestTarget._viewScale = computedScale);
+ activeItem.presMovement === PresMovement.Zoom && (bestTarget._freeform_scale = computedScale);
dv.ComponentView?.brushView?.(viewport);
}
} else {
- bestTarget._panX = activeItem.presPinViewX;
- bestTarget._panY = activeItem.presPinViewY;
- activeItem.presMovement === 'zoom' && (bestTarget._viewScale = activeItem.presPinViewScale);
+ if (bestTarget._freeform_panX !== activeItem.presPanX || bestTarget._freeform_panY !== activeItem.presPanY || bestTarget._freeform_scale !== activeItem.presViewScale) {
+ bestTarget._freeform_panX = activeItem.presPanX ?? bestTarget._freeform_panX;
+ bestTarget._freeform_panY = activeItem.presPanY ?? bestTarget._freeform_panY;
+ bestTarget._freeform_scale = activeItem.presViewScale ?? bestTarget._freeform_scale;
+ changed = true;
+ }
}
}
- return setTimeout(() => (bestTarget._viewTransition = undefined), transTime + 10);
+ if (changed) {
+ return bestTargetView?.setViewTransition('all', transTime);
+ }
}
/// copies values from the targetDoc (which is the prototype of the pinDoc) to
/// reserved fields on the pinDoc so that those values can be restored to the
/// target doc when navigating to it.
@action
- static pinDocView(pinDoc: Doc, pinProps: PinProps | undefined, targetDoc: Doc) {
- const { scrollable, pannable, temporal, clippable, dataview, textview, poslayoutview } = this.pinDataTypes(pinDoc);
- if (pinProps?.pinDocLayout) {
+ static pinDocView(pinDoc: Doc, pinProps: PinProps, targetDoc: Doc) {
+ if (pinProps.pinDocLayout) {
pinDoc.presPinLayout = true;
pinDoc.presX = NumCast(targetDoc.x);
pinDoc.presY = NumCast(targetDoc.y);
- pinDoc.presRot = NumCast(targetDoc.rotation);
+ pinDoc.presRotation = NumCast(targetDoc.rotation);
pinDoc.presWidth = NumCast(targetDoc.width);
pinDoc.presHeight = NumCast(targetDoc.height);
}
- if (pinProps?.pinDocContent) {
- pinDoc.presPinData = scrollable || temporal || pannable || clippable || dataview || textview || poslayoutview || pinProps.activeFrame !== undefined;
- if (dataview) pinDoc.presData = targetDoc[Doc.LayoutFieldKey(targetDoc)] instanceof ObjectField ? (targetDoc[Doc.LayoutFieldKey(targetDoc)] as ObjectField)[Copy]() : targetDoc.data;
- if (textview) pinDoc.presData = targetDoc[Doc.LayoutFieldKey(targetDoc)] instanceof ObjectField ? (targetDoc[Doc.LayoutFieldKey(targetDoc)] as ObjectField)[Copy]() : targetDoc.text;
- if (scrollable) pinDoc.presPinViewScroll = pinDoc._scrollTop;
- if (clippable) pinDoc.presPinClipWidth = pinDoc._clipWidth;
- if (poslayoutview) pinDoc.presPinLayoutData = new List<string>(DocListCast(pinDoc.presData).map(d => JSON.stringify({ id: d[Id], x: NumCast(d.x), y: NumCast(d.y), w: NumCast(d._width), h: NumCast(d._height) })));
- if (pannable) {
- pinDoc.presPinViewX = NumCast(pinDoc._panX);
- pinDoc.presPinViewY = NumCast(pinDoc._panY);
- pinDoc.presPinViewScale = NumCast(pinDoc._viewScale, 1);
+ if (pinProps.pinAudioPlay) pinDoc.presPlayAudio = true;
+ if (pinProps.pinData) {
+ pinDoc.presPinData =
+ pinProps.pinData.scrollable ||
+ pinProps.pinData.temporal ||
+ pinProps.pinData.pannable ||
+ pinProps.pinData.type_collection ||
+ pinProps.pinData.clippable ||
+ pinProps.pinData.datarange ||
+ pinProps.pinData.dataview ||
+ pinProps.pinData.poslayoutview ||
+ pinProps?.activeFrame !== undefined;
+ const fkey = Doc.LayoutFieldKey(targetDoc);
+ if (pinProps.pinData.dataview) {
+ pinDoc.presUsePath = targetDoc[fkey + '_usePath'];
+ pinDoc.presData = targetDoc[fkey] instanceof ObjectField ? (targetDoc[fkey] as ObjectField)[Copy]() : targetDoc.data;
}
- if (temporal) {
- pinDoc.presStartTime = pinDoc._currentTimecode;
- const duration = NumCast(pinDoc[`${Doc.LayoutFieldKey(pinDoc)}-duration`], NumCast(pinDoc.presStartTime) + 0.1);
- pinDoc.presEndTime = NumCast(pinDoc.clipEnd, duration);
+ if (pinProps.pinData.dataannos) {
+ const fkey = Doc.LayoutFieldKey(targetDoc);
+ pinDoc.presAnnotations = new List<Doc>(DocListCast(Doc.GetProto(targetDoc)[fkey + '_annotations']).filter(doc => !doc.layout_unrendered));
+ }
+ if (pinProps.pinData.inkable) {
+ pinDoc.presFillColor = targetDoc.fillColor;
+ pinDoc.presColor = targetDoc.color;
+ pinDoc.presWidth = targetDoc._width;
+ pinDoc.presHeight = targetDoc._height;
+ }
+ if (pinProps.pinData.scrollable) pinDoc.presViewScroll = targetDoc._layout_scrollTop;
+ if (pinProps.pinData.clippable) {
+ const fkey = Doc.LayoutFieldKey(targetDoc);
+ pinDoc.presClipWidth = targetDoc[fkey + '_clipWidth'];
+ }
+ if (pinProps.pinData.datarange) {
+ pinDoc.presXRange = undefined; //targetDoc?.xrange;
+ pinDoc.presYRange = undefined; //targetDoc?.yrange;
+ }
+ if (pinProps.pinData.poslayoutview)
+ pinDoc.presPinLayoutData = new List<string>(
+ DocListCast(targetDoc[fkey] as ObjectField).map(d =>
+ JSON.stringify({
+ id: d[Id],
+ x: NumCast(d.x),
+ y: NumCast(d.y),
+ w: NumCast(d._width),
+ h: NumCast(d._height),
+ fill: StrCast(d._fillColor),
+ back: StrCast(d._backgroundColor),
+ data: SerializationHelper.Serialize(d.data instanceof ObjectField ? d.data[Copy]() : ''),
+ text: SerializationHelper.Serialize(d.text instanceof ObjectField ? d.text[Copy]() : ''),
+ })
+ )
+ );
+ 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);
+ pinDoc.presPanY = NumCast(targetDoc._freeform_panY);
+ pinDoc.presViewScale = NumCast(targetDoc._freeform_scale, 1);
+ }
+ if (pinProps.pinData.temporal) {
+ pinDoc.presStartTime = targetDoc._layout_currentTimecode;
+ const duration = NumCast(pinDoc[`${Doc.LayoutFieldKey(pinDoc)}_duration`], NumCast(targetDoc.presStartTime) + 0.1);
+ pinDoc.presEndTime = NumCast(targetDoc.clipEnd, duration);
}
}
if (pinProps?.pinViewport) {
// If pinWithView option set then update scale and x / y props of slide
const bounds = pinProps.pinViewport;
pinDoc.presPinView = true;
- pinDoc.presPinViewScale = NumCast(pinDoc._viewScale, 1);
- pinDoc.presPinViewX = bounds.left + bounds.width / 2;
- pinDoc.presPinViewY = bounds.top + bounds.height / 2;
+ pinDoc.presViewScale = NumCast(targetDoc._freeform_scale, 1);
+ pinDoc.presPanX = bounds.left + bounds.width / 2;
+ pinDoc.presPanY = bounds.top + bounds.height / 2;
pinDoc.presPinViewBounds = new List<number>([bounds.left, bounds.top, bounds.left + bounds.width, bounds.top + bounds.height]);
}
}
-
- static _navTimer: NodeJS.Timeout;
/**
* This method makes sure that cursor navigates to the element that
* has the option open and last in the group.
@@ -471,183 +686,161 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
* a new tab. If presCollection is undefined it will open the document
* on the right.
*/
- navigateToActiveItem = () => {
+ navigateToActiveItem = (afterNav?: () => void) => {
const activeItem: Doc = this.activeItem;
const targetDoc: Doc = this.targetDoc;
- const srcContext = Cast(targetDoc.context, Doc, null) ?? Cast(Cast(targetDoc.annotationOn, Doc, null)?.context, Doc, null);
- const presCollection = Cast(this.layoutDoc.presCollection, Doc, null);
- const collectionDocView = presCollection ? DocumentManager.Instance.getDocumentView(presCollection) : undefined;
- const includesDoc: boolean = DocListCast(presCollection?.data).includes(targetDoc);
- const tab = CollectionDockingView.Instance && Array.from(CollectionDockingView.Instance.tabMap).find(tab => tab.DashDoc === srcContext);
- // Handles the setting of presCollection
- if (includesDoc) {
- //Case 1: Pres collection should not change as it is already the same
- } else if (tab !== undefined) {
- // Case 2: Pres collection should update
- this.layoutDoc.presCollection = srcContext;
- }
- const presStatus = this.rootDoc.presStatus;
+ const finished = () => {
+ afterNav?.();
+ console.log('Finish Slide Nav: ' + targetDoc.title);
+ targetDoc[Animation] = undefined;
+ };
const selViewCache = Array.from(this.selectedArray);
const dragViewCache = Array.from(this._dragArray);
const eleViewCache = Array.from(this._eleArray);
- const self = this;
const resetSelection = action(() => {
- const presDocView = DocumentManager.Instance.getDocumentView(self.rootDoc);
- if (presDocView) SelectionManager.SelectView(presDocView, false);
- self.rootDoc.presStatus = presStatus;
- self.clearSelectedArray();
- selViewCache.forEach(doc => self.addToSelectedArray(doc));
- self._dragArray.splice(0, self._dragArray.length, ...dragViewCache);
- self._eleArray.splice(0, self._eleArray.length, ...eleViewCache);
- });
- const openInTab = (doc: Doc, finished?: () => void) => {
- (collectionDocView ?? this).props.addDocTab(doc, '');
- this.layoutDoc.presCollection = targetDoc;
- // this still needs some fixing
- setTimeout(resetSelection, 500);
- if (doc !== targetDoc) {
- setTimeout(finished ?? emptyFunction, 100); /// give it some time to create the targetDoc if we're opening up its context
- } else {
- finished?.();
+ if (!this.props.isSelected()) {
+ const presDocView = DocumentManager.Instance.getDocumentView(this.rootDoc);
+ if (presDocView) SelectionManager.SelectView(presDocView, false);
+ this.clearSelectedArray();
+ selViewCache.forEach(doc => this.addToSelectedArray(doc));
+ this._dragArray.splice(0, this._dragArray.length, ...dragViewCache);
+ this._eleArray.splice(0, this._eleArray.length, ...eleViewCache);
}
- };
- PresBox.NavigateToTarget(targetDoc, activeItem, openInTab, srcContext, includesDoc || tab ? undefined : resetSelection);
+ finished();
+ });
+ PresBox.NavigateToTarget(targetDoc, activeItem, resetSelection);
};
- static NavigateToTarget(targetDoc: Doc, activeItem: Doc, openInTab: any, srcContext: Doc, finished?: () => void) {
- if ((activeItem.presPinLayout || activeItem.presPinView) && DocCast(targetDoc.context)?._currentFrame === undefined) {
- const transTime = NumCast(activeItem.presTransition, 500);
- const presTransitionTime = `all ${transTime}ms`;
- targetDoc._dataTransition = presTransitionTime;
- targetDoc.x = NumCast(activeItem.presX, NumCast(targetDoc.x));
- targetDoc.y = NumCast(activeItem.presY, NumCast(targetDoc.y));
- targetDoc.rotation = NumCast(activeItem.presRot, NumCast(targetDoc.rotation));
- targetDoc.width = NumCast(activeItem.presWidth, NumCast(targetDoc.width));
- targetDoc.height = NumCast(activeItem.presHeight, NumCast(targetDoc.height));
- setTimeout(() => (targetDoc._dataTransition = undefined), transTime + 10);
- }
- // If openDocument is selected then it should open the document for the user
- if (activeItem.openDocument) {
- LightboxView.SetLightboxDoc(targetDoc); // openInTab(targetDoc);
- } else if (targetDoc && activeItem.presMovement !== PresMovement.None) {
- LightboxView.SetLightboxDoc(undefined);
- const zooming = activeItem.presMovement !== PresMovement.Pan;
- DocumentManager.Instance.jumpToDocument(targetDoc, zooming, openInTab, srcContext ? [srcContext] : [], undefined, undefined, undefined, finished, undefined, true, NumCast(activeItem.presZoom, 1));
- } else if (activeItem.presMovement === PresMovement.None && targetDoc.type === DocumentType.SCRIPTING) {
+ static NavigateToTarget(targetDoc: Doc, activeItem: Doc, finished?: () => void) {
+ if (activeItem.presMovement === PresMovement.None && targetDoc.type === DocumentType.SCRIPTING) {
(DocumentManager.Instance.getFirstDocumentView(targetDoc)?.ComponentView as ScriptingBox)?.onRun?.();
+ return;
}
- // After navigating to the document, if it is added as a presPinView then it will
- // adjust the pan and scale to that of the pinView when it was added.
- if (activeItem.presPinData || activeItem.presPinView) {
- clearTimeout(PresBox._navTimer);
- // targetDoc may or may not be displayed. this gets the first available document (or alias) view that matches targetDoc
- const bestTarget = DocumentManager.Instance.getFirstDocumentView(targetDoc)?.props.Document;
- if (bestTarget) PresBox._navTimer = PresBox.restoreTargetDocView(bestTarget, activeItem);
- }
- }
-
- /**
- * Uses the viewfinder to progressivize through the different views of a single collection.
- * @param activeItem: document for which internal zoom is used
- */
- zoomProgressivizeNext = (activeItem: Doc) => {
- const targetDoc: Doc = this.targetDoc;
- const srcContext = Cast(targetDoc?.context, Doc, null);
- const docView = DocumentManager.Instance.getDocumentView(targetDoc);
- const vfLeft = this.checkList(targetDoc, activeItem['viewfinder-left-indexed']);
- const vfWidth = this.checkList(targetDoc, activeItem['viewfinder-width-indexed']);
- const vfTop = this.checkList(targetDoc, activeItem['viewfinder-top-indexed']);
- const vfHeight = this.checkList(targetDoc, activeItem['viewfinder-height-indexed']);
- // Case 1: document that is not a Golden Layout tab
- if (srcContext) {
- const srcDocView = DocumentManager.Instance.getDocumentView(srcContext);
- if (srcDocView) {
- const layoutdoc = Doc.Layout(targetDoc);
- const panelWidth: number = srcDocView.props.PanelWidth();
- const panelHeight: number = srcDocView.props.PanelHeight();
- const newPanX = NumCast(targetDoc.x) + NumCast(layoutdoc._width) / 2;
- const newPanY = NumCast(targetDoc.y) + NumCast(layoutdoc._height) / 2;
- const newScale = 0.9 * Math.min(Number(panelWidth) / vfWidth, Number(panelHeight) / vfHeight);
- srcContext._panX = newPanX + (vfLeft + vfWidth / 2);
- srcContext._panY = newPanY + (vfTop + vfHeight / 2);
- srcContext._viewScale = newScale;
+ const effect = activeItem.presEffect && activeItem.presEffect !== PresEffect.None ? activeItem.presEffect : undefined;
+ const presTime = NumCast(activeItem.presTransition, effect ? 750 : 500);
+ const options: DocFocusOptions = {
+ willPan: activeItem.presMovement !== PresMovement.None,
+ willZoomCentered: activeItem.presMovement === PresMovement.Zoom || activeItem.presMovement === PresMovement.Jump || activeItem.presMovement === PresMovement.Center,
+ zoomScale: activeItem.presMovement === PresMovement.Center ? 0 : NumCast(activeItem.presZoom, 1),
+ zoomTime: activeItem.presMovement === PresMovement.Jump ? 0 : Math.min(Math.max(effect ? 750 : 500, (effect ? 0.2 : 1) * presTime), presTime),
+ effect: activeItem,
+ noSelect: true,
+ openLocation: OpenWhere.addLeft,
+ anchorDoc: activeItem,
+ easeFunc: StrCast(activeItem.presEaseFunc, 'ease') as any,
+ zoomTextSelections: BoolCast(activeItem.presZoomText),
+ playAudio: BoolCast(activeItem.presPlayAudio),
+ };
+ if (activeItem.presOpenInLightbox) {
+ const context = DocCast(targetDoc.annotationOn) ?? targetDoc;
+ if (!DocumentManager.Instance.getLightboxDocumentView(context)) {
+ LightboxView.SetLightboxDoc(context);
}
}
- // Case 2: document is the containing collection
- if (docView && !srcContext) {
- const panelWidth: number = docView.props.PanelWidth();
- const panelHeight: number = docView.props.PanelHeight();
- const newScale = 0.9 * Math.min(Number(panelWidth) / vfWidth, Number(panelHeight) / vfHeight);
- targetDoc._panX = vfLeft + vfWidth / 2;
- targetDoc._panY = vfTop + vfWidth / 2;
- targetDoc._viewScale = newScale;
- }
- const resize = document.getElementById('resizable');
- if (resize) {
- resize.style.width = vfWidth + 'px';
- resize.style.height = vfHeight + 'px';
- resize.style.top = vfTop + 'px';
- resize.style.left = vfLeft + 'px';
- }
- };
+ if (targetDoc) {
+ 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
+ if (!DocumentManager.Instance.getLightboxDocumentView(DocCast(targetDoc.annotationOn) ?? targetDoc)) {
+ LightboxView.SetLightboxDoc(undefined);
+ }
+ DocumentManager.Instance.showDocument(targetDoc, options, finished);
+ });
+ } else finished?.();
+ }
/**
* For 'Hide Before' and 'Hide After' buttons making sure that
* they are hidden each time the presentation is updated.
*/
@action
- onHideDocument = () => {
+ doHideBeforeAfter = () => {
this.childDocs.forEach((doc, index) => {
const curDoc = Cast(doc, Doc, null);
- const tagDoc = Cast(curDoc.presentationTargetDoc, Doc, null);
- //if (tagDoc) tagDoc.opacity = 1;
+ const tagDoc = PresBox.targetRenderedDoc(curDoc);
const itemIndexes: number[] = this.getAllIndexes(this.tagDocs, tagDoc);
- const curInd: number = itemIndexes.indexOf(index);
- if (tagDoc === this.layoutDoc.presCollection) {
- tagDoc.opacity = 1;
- } else {
- if (curDoc.presHideBefore) {
- if (itemIndexes.length > 1 && curInd !== 0) {
- tagDoc.opacity = 1;
- } else {
- if (index > this.itemIndex) {
- tagDoc.opacity = 0;
- } else if (index === this.itemIndex || !curDoc.presHideAfter) {
- tagDoc.opacity = 1;
- }
- }
+ let opacity: Opt<number> = index === this.itemIndex ? 1 : undefined;
+ if (curDoc.presHide) {
+ if (index !== this.itemIndex) {
+ opacity = 1;
}
- if (curDoc.presHideAfter) {
- if (itemIndexes.length > 1 && curInd !== itemIndexes.length - 1) {
- tagDoc.opacity = 1;
- } else {
- if (index < this.itemIndex) {
- tagDoc.opacity = 0;
- } else if (index === this.itemIndex || !curDoc.presHideBefore) {
- tagDoc.opacity = 1;
- }
- }
+ }
+ const hidingIndBef = itemIndexes.find(item => item >= this.itemIndex) ?? itemIndexes.slice().reverse().lastElement();
+ if (curDoc.presHideBefore && index === hidingIndBef) {
+ if (index > this.itemIndex) {
+ opacity = 0;
+ } else if (index === this.itemIndex || !curDoc.presHideAfter) {
+ opacity = 1;
+ setTimeout(() => (tagDoc._dataTransition = undefined), 1000);
}
}
+ const hidingIndAft =
+ itemIndexes
+ .slice()
+ .reverse()
+ .find(item => item <= this.itemIndex) ?? itemIndexes.lastElement();
+ if (curDoc.presHideAfter && index === hidingIndAft) {
+ if (index < this.itemIndex) {
+ opacity = 0;
+ } else if (index === this.itemIndex || !curDoc.presHideBefore) {
+ opacity = 1;
+ }
+ }
+ const hidingInd = itemIndexes.find(item => item === this.itemIndex);
+ if (curDoc.presHide && index === hidingInd) {
+ if (index === this.itemIndex) {
+ opacity = 0;
+ }
+ }
+ opacity !== undefined && (tagDoc.opacity = opacity);
});
};
- //The function that starts or resets presentaton functionally, depending on presStatus of the layoutDoc
- @action
- startAutoPres = (startSlide: number) => {
- this.layoutDoc.presStatus = PresStatus.Autoplay;
- this.startPresentation(startSlide);
- clearTimeout(this._presTimer);
- const func = (itemIndex: number) => {
- if (itemIndex === this.next()) this.layoutDoc.presStatus = PresStatus.Manual;
- else
- this._presTimer = setTimeout(
- () => this.layoutDoc.presStatus !== PresStatus.Manual && func(this.itemIndex),
- NumCast(this.activeItem.presDuration, this.activeItem.type === DocumentType.SCRIPTING ? 0 : 2500) + NumCast(this.activeItem.presTransition)
- );
+ _exitTrail: Opt<() => void>;
+ PlayTrail = (docs: Doc[]) => {
+ const savedStates = docs.map(doc => {
+ switch (doc.type) {
+ case DocumentType.COL:
+ 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) {
+ return { type: doc.type, doc, data: doc.data?.[Copy](), fillColor: doc.fillColor, color: doc.color, x: NumCast(doc.x), y: NumCast(doc.y) };
+ }
+ }
+ return undefined;
+ });
+ this.startPresentation(0);
+ this._exitTrail = () => {
+ savedStates
+ .filter(savedState => savedState)
+ .map(savedState => {
+ switch (savedState?.type) {
+ case CollectionViewType.Freeform:
+ {
+ const { x, y, s, doc } = savedState!;
+ doc._freeform_panX = x;
+ doc._freeform_panY = y;
+ doc._freeform_scale = s;
+ }
+ break;
+ case DocumentType.INK:
+ {
+ const { data, fillColor, color, x, y, doc } = savedState!;
+ doc.x = x;
+ doc.y = y;
+ doc.data = data;
+ doc.fillColor = fillColor;
+ doc.color = color;
+ }
+ break;
+ }
+ });
+ LightboxView.SetLightboxDoc(undefined);
+ Doc.RemFromMyOverlay(this.rootDoc);
+ return PresStatus.Edit;
};
-
- func(this.itemIndex);
};
// The function pauses the auto presentation
@@ -664,7 +857,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
//stops the presentaton.
resetPresentation = () => {
this.childDocs
- .map(doc => Cast(doc.presentationTargetDoc, Doc, null))
+ .map(doc => PresBox.targetRenderedDoc(doc))
.filter(doc => doc instanceof Doc)
.forEach(doc => {
try {
@@ -676,14 +869,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
};
// The function allows for viewing the pres path on toggle
- @action togglePath = (srcContext: Doc, off?: boolean) => {
- if (off) {
- this._pathBoolean = false;
- srcContext.presPathView = false;
- } else {
- runInAction(() => (this._pathBoolean = !this._pathBoolean));
- srcContext.presPathView = this._pathBoolean;
- }
+ @action togglePath = (off?: boolean) => {
+ this._pathBoolean = off ? false : !this._pathBoolean;
+ CollectionFreeFormView.ShowPresPaths = this._pathBoolean;
};
// The function allows for expanding the view of pres on toggle
@@ -695,46 +883,75 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
});
};
+ initializePresState = (startIndex: number) => {
+ this.childDocs.forEach((doc, index) => {
+ const tagDoc = PresBox.targetRenderedDoc(doc);
+ if (doc.presHideBefore && index > startIndex) tagDoc.opacity = 0;
+ if (doc.presHideAfter && index < startIndex) tagDoc.opacity = 0;
+ if (doc.presIndexed !== undefined && index >= startIndex) {
+ const startInd = NumCast(doc.presIndexedStart);
+ this.progressivizedItems(doc)
+ ?.slice(startInd)
+ .forEach(indexedDoc => (indexedDoc.opacity = 0));
+ doc.presIndexed = Math.min(this.progressivizedItems(doc)?.length ?? 0, startInd);
+ }
+ // if (doc.presHide && this.childDocs.indexOf(doc) === startIndex) tagDoc.opacity = 0;
+ });
+ };
+
/**
* The function that starts the presentation at the given index, also checking if actions should be applied
* directly at start.
* @param startIndex: index that the presentation will start at
*/
+ @action
startPresentation = (startIndex: number) => {
- this.childDocs.forEach(doc => {
- const tagDoc = doc.presentationTargetDoc as Doc;
- if (doc.presHideBefore && this.childDocs.indexOf(doc) > startIndex) {
- tagDoc.opacity = 0;
- }
- if (doc.presHideAfter && this.childDocs.indexOf(doc) < startIndex) {
- tagDoc.opacity = 0;
- }
- });
- this.gotoDocument(startIndex, this.activeItem);
+ PresBox.Instance = this;
+ clearTimeout(this._presTimer);
+ if (this.childDocs.length) {
+ this.layoutDoc.presStatus = PresStatus.Autoplay;
+ this.initializePresState(startIndex);
+ const func = () => {
+ const delay = NumCast(this.activeItem.presDuration, this.activeItem.type === DocumentType.SCRIPTING ? 0 : 2500) + NumCast(this.activeItem.presTransition);
+ this._presTimer = setTimeout(() => {
+ if (!this.next()) this.layoutDoc.presStatus = this._exitTrail?.() ?? PresStatus.Manual;
+ this.layoutDoc.presStatus === PresStatus.Autoplay && func();
+ }, delay);
+ };
+ this.gotoDocument(startIndex, this.activeItem, undefined, func);
+ }
};
/**
* The method called to open the presentation as a minimized view
*/
@action
- updateMinimize = async () => {
- if (DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc)) {
- this.layoutDoc.presStatus = PresStatus.Edit;
- Doc.RemoveDocFromList(Doc.MyOverlayDocs, undefined, this.rootDoc);
- CollectionDockingView.AddSplit(this.rootDoc, 'right');
- } else {
- this.layoutDoc.presStatus = PresStatus.Edit;
- clearTimeout(this._presTimer);
- const pt = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0);
- this.rootDoc.overlayX = pt[0] + (this.props.PanelWidth() - 250);
- this.rootDoc.overlayY = pt[1] + 10;
- this.rootDoc._height = 30;
- this.rootDoc._width = 248;
- Doc.AddDocToList(Doc.MyOverlayDocs, undefined, this.rootDoc);
- this.props.removeDocument?.(this.layoutDoc);
+ enterMinimize = () => {
+ this.updateCurrentPresentation(this.rootDoc);
+ clearTimeout(this._presTimer);
+ const pt = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0);
+ this.props.removeDocument?.(this.layoutDoc);
+ return PresBox.OpenPresMinimized(this.rootDoc, [pt[0] + (this.props.PanelWidth() - 250), pt[1] + 10]);
+ };
+ exitMinimize = () => {
+ if (Doc.IsInMyOverlay(this.layoutDoc)) {
+ Doc.RemFromMyOverlay(this.rootDoc);
+ CollectionDockingView.AddSplit(this.rootDoc, OpenWhereMod.right);
}
+ return PresStatus.Edit;
};
+ public static minimizedWidth = 198;
+ public static OpenPresMinimized(doc: Doc, pt: number[]) {
+ doc.overlayX = pt[0];
+ doc.overlayY = pt[1];
+ doc._height = 30;
+ doc._width = PresBox.minimizedWidth;
+ Doc.AddToMyOverlay(doc);
+ PresBox.Instance?.initializePresState(PresBox.Instance.itemIndex);
+ return (doc.presStatus = PresStatus.Manual);
+ }
+
/**
* Called when the user changes the view type
* Either 'List' (stacking) or 'Slides' (carousel)
@@ -742,11 +959,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;
}
@@ -766,20 +983,18 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
if (this.childDocs[stopDocIndex - 1].mediaStopTriggerList) {
const list = DocListCast(this.childDocs[stopDocIndex - 1].mediaStopTriggerList);
list.push(activeItem);
- // this.childDocs[stopDocIndex - 1].mediaStopTriggerList = list;
- console.log(list);
+ // this.childDocs[stopDocIndex - 1].mediaStopTriggerList = list;\
} else {
this.childDocs[stopDocIndex - 1].mediaStopTriggerList = new List<Doc>();
const list = DocListCast(this.childDocs[stopDocIndex - 1].mediaStopTriggerList);
list.push(activeItem);
// this.childDocs[stopDocIndex - 1].mediaStopTriggerList = list;
- console.log(list);
}
});
movementName = action((activeItem: Doc) => {
- if (![PresMovement.Zoom, PresMovement.Pan, PresMovement.Jump, PresMovement.None].includes(StrCast(activeItem.presMovement) as any)) {
- activeItem.presMovement = 'zoom';
+ if (![PresMovement.Zoom, PresMovement.Pan, PresMovement.Center, PresMovement.Jump, PresMovement.None].includes(StrCast(activeItem.presMovement) as any)) {
+ return PresMovement.Zoom;
}
return StrCast(activeItem.presMovement);
});
@@ -802,17 +1017,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
return false;
}
} else {
- if (!doc.aliasOf) {
- const original = Doc.MakeAlias(doc);
- TabDocView.PinDoc(original);
- setTimeout(() => this.removeDocument(doc), 0);
- return false;
- } else {
- if (!doc.presentationTargetDoc) doc.title = doc.title + ' - Slide';
- doc.aliasOf instanceof Doc && (doc.presentationTargetDoc = doc.aliasOf);
- doc.presMovement = PresMovement.Zoom;
- if (this._expandBoolean) doc.presExpandInlineButton = true;
- }
+ if (!doc.presentationTargetDoc) doc.title = doc.title + ' - Slide';
+ doc.presentationTargetDoc = doc.createdFrom; // dropped document will be a new embedding of an embedded document somewhere else.
+ doc.presMovement = PresMovement.Zoom;
+ if (this._expandBoolean) doc.presExpandInlineButton = true;
}
});
return true;
@@ -821,9 +1029,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
removeDocument = (doc: Doc) => Doc.RemoveDocFromList(this.rootDoc, this.fieldKey, doc);
getTransform = () => this.props.ScreenToLocalTransform().translate(-5, -65); // listBox padding-left and pres-box-cont minHeight
panelHeight = () => this.props.PanelHeight() - 40;
- isContentActive = (outsideReaction?: boolean) =>
- Doc.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.
*/
@@ -865,12 +1070,22 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
presDocView && SelectionManager.SelectView(presDocView, false);
};
+ focusElement = (doc: Doc, options: DocFocusOptions) => {
+ this.selectElement(doc);
+ return undefined;
+ };
+
//Regular click
@action
- selectElement = async (doc: Doc) => {
- this.gotoDocument(this.childDocs.indexOf(doc), this.activeItem);
- if (doc.presPinView || doc.presentationTargetDoc === this.layoutDoc.presCollection) setTimeout(() => this.updateCurrentPresentation(DocCast(doc.context)), 0);
- else this.updateCurrentPresentation(DocCast(doc.context));
+ selectElement = (doc: Doc, noNav = false) => {
+ CollectionStackedTimeline.CurrentlyPlaying?.map((clip, i) => clip?.ComponentView?.Pause?.());
+ if (noNav) {
+ const index = this.childDocs.indexOf(doc);
+ if (index >= 0 && index < this.childDocs.length) {
+ this.rootDoc._itemIndex = index;
+ }
+ } else this.gotoDocument(this.childDocs.indexOf(doc), this.activeItem);
+ this.updateCurrentPresentation(DocCast(doc.embedContainer));
};
//Command click
@@ -905,22 +1120,22 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
//regular click
@action
- regularSelect = (doc: Doc, ref: HTMLElement, drag: HTMLElement, focus: boolean, selectPres = true) => {
+ regularSelect = (doc: Doc, ref: HTMLElement, drag: HTMLElement, noNav: boolean, selectPres = true) => {
this.clearSelectedArray();
this.addToSelectedArray(doc);
this._eleArray.splice(0, this._eleArray.length, ref);
this._dragArray.splice(0, this._dragArray.length, drag);
- focus && this.selectElement(doc);
+ this.selectElement(doc, noNav);
selectPres && this.selectPres();
};
- modifierSelect = (doc: Doc, ref: HTMLElement, drag: HTMLElement, focus: boolean, cmdClick: boolean, shiftClick: boolean) => {
+ modifierSelect = (doc: Doc, ref: HTMLElement, drag: HTMLElement, noNav: boolean, cmdClick: boolean, shiftClick: boolean) => {
if (cmdClick) this.multiSelect(doc, ref, drag);
else if (shiftClick) this.shiftSelect(doc, ref, drag);
- else this.regularSelect(doc, ref, drag, focus);
+ else this.regularSelect(doc, ref, drag, noNav);
};
- static keyEventsWrapper = (e: KeyboardEvent) => PresBox.Instance.keyEvents(e);
+ static keyEventsWrapper = (e: KeyboardEvent) => PresBox.Instance?.keyEvents(e);
// Key for when the presentaiton is active
@action
@@ -946,12 +1161,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
break;
case 'Escape':
- if (DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc)) {
- this.updateMinimize();
- } else if (this.layoutDoc.presStatus === 'edit') {
+ if (Doc.IsInMyOverlay(this.layoutDoc)) {
+ this.exitClicked();
+ } else if (this.layoutDoc.presStatus === PresStatus.Edit) {
this.clearSelectedArray();
this._eleArray.length = this._dragArray.length = 0;
- } else this.layoutDoc.presStatus = 'edit';
+ } else {
+ this.layoutDoc.presStatus = PresStatus.Edit;
+ }
if (this._presTimer) clearTimeout(this._presTimer);
handled = true;
break;
@@ -1010,30 +1227,19 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
};
- /**
- *
- */
- @action
- viewPaths = () => {
- const srcContext = Cast(this.rootDoc.presCollection, Doc, null);
- if (srcContext) {
- this.togglePath(srcContext);
- }
- };
-
getAllIndexes = (arr: Doc[], val: Doc) => arr.map((doc, i) => (doc === val ? i : -1)).filter(i => i !== -1);
// Adds the index in the pres path graphically
@computed get order() {
const order: JSX.Element[] = [];
const docs: Doc[] = [];
- const presCollection = Cast(this.rootDoc.presCollection, Doc, null);
+ const presCollection = DocumentManager.GetContextPath(this.activeItem).reverse().lastElement();
const dv = DocumentManager.Instance.getDocumentView(presCollection);
this.childDocs
.filter(doc => Cast(doc.presentationTargetDoc, Doc, null))
.forEach((doc, index) => {
const tagDoc = Cast(doc.presentationTargetDoc, Doc, null);
- const srcContext = Cast(tagDoc.context, Doc, null);
+ const srcContext = Cast(tagDoc.embedContainer, Doc, null);
const width = NumCast(tagDoc._width) / 10;
const height = Math.max(NumCast(tagDoc._height) / 10, 15);
const edge = Math.max(width, height);
@@ -1067,15 +1273,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
} else if (doc.presPinView && presCollection === tagDoc && dv) {
// Case B: Document is presPinView and is presCollection
- const scale: number = 1 / NumCast(doc.presPinViewScale);
+ const scale: number = 1 / NumCast(doc.presViewScale);
const height: number = dv.props.PanelHeight() * scale;
const width: number = dv.props.PanelWidth() * scale;
const indWidth = width / 10;
const indHeight = Math.max(height / 10, 15);
const indEdge = Math.max(indWidth, indHeight);
const indFontSize = indEdge * 0.8;
- const xLoc: number = NumCast(doc.presPinViewX) - width / 2;
- const yLoc: number = NumCast(doc.presPinViewY) - height / 2;
+ const xLoc: number = NumCast(doc.presPanX) - width / 2;
+ const yLoc: number = NumCast(doc.presPanY) - height / 2;
docs.push(tagDoc);
order.push(
<>
@@ -1093,25 +1299,23 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
/**
* Method called for viewing paths which adds a single line with
* points at the center of each document added.
- * Design choice: When this is called it sets _fitContentsToBox as true so the
+ * Design choice: When this is called it sets _freeform_fitContentsToBox as true so the
* user can have an overview of all of the documents in the collection.
* (Design needed for when documents in presentation trail are in another
* collection)
*/
@computed get paths() {
let pathPoints = '';
- const presCollection = Cast(this.rootDoc.presCollection, Doc, null);
this.childDocs.forEach((doc, index) => {
const tagDoc = Cast(doc.presentationTargetDoc, Doc, null);
- const srcContext = Cast(tagDoc?.context, Doc, null);
- if (tagDoc && presCollection === srcContext) {
+ if (tagDoc) {
const n1x = NumCast(tagDoc.x) + NumCast(tagDoc._width) / 2;
const n1y = NumCast(tagDoc.y) + NumCast(tagDoc._height) / 2;
if ((index = 0)) pathPoints = n1x + ',' + n1y;
else pathPoints = pathPoints + ' ' + n1x + ',' + n1y;
- } else if (doc.presPinView && presCollection === tagDoc) {
- const n1x = NumCast(doc.presPinViewX);
- const n1y = NumCast(doc.presPinViewY);
+ } else if (doc.presPinView) {
+ const n1x = NumCast(doc.presPanX);
+ const n1y = NumCast(doc.presPanY);
if ((index = 0)) pathPoints = n1x + ',' + n1y;
else pathPoints = pathPoints + ' ' + n1x + ',' + n1y;
}
@@ -1133,18 +1337,25 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
/>
);
}
+ getPaths = (collection: Doc) => this.paths; // needs to be smarter and figure out the paths to draw for this specific collection. or better yet, draw everything in an overlay layer instad of within a collection
// Converts seconds to ms and updates presTransition
- setTransitionTime = (number: String, change?: number) => {
+ public static SetTransitionTime = (number: String, setter: (timeInMS: number) => void, change?: number) => {
let timeInMS = Number(number) * 1000;
if (change) timeInMS += change;
if (timeInMS < 100) timeInMS = 100;
- if (timeInMS > 10000) timeInMS = 10000;
- this.selectedArray.forEach(doc => (doc.presTransition = timeInMS));
+ if (timeInMS > 100000) timeInMS = 100000;
+ setter(timeInMS);
+ };
+
+ @undoBatch
+ updateTransitionTime = (number: String, change?: number) => {
+ PresBox.SetTransitionTime(number, (timeInMS: number) => this.selectedArray.forEach(doc => (doc.presTransition = timeInMS)), change);
};
// Converts seconds to ms and updates presTransition
- setZoom = (number: String, change?: number) => {
+ @undoBatch
+ updateZoom = (number: String, change?: number) => {
let scale = Number(number) / 100;
if (change) scale += change;
if (scale < 0.01) scale = 0.01;
@@ -1152,8 +1363,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
this.selectedArray.forEach(doc => (doc.presZoom = scale));
};
- // Converts seconds to ms and updates presDuration
- setDurationTime = (number: String, change?: number) => {
+ /*
+ * Converts seconds to ms and updates presDuration
+ */
+ @undoBatch
+ updateDurationTime = (number: String, change?: number) => {
let timeInMS = Number(number) * 1000;
if (change) timeInMS += change;
if (timeInMS < 100) timeInMS = 100;
@@ -1161,9 +1375,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
this.selectedArray.forEach(doc => (doc.presDuration = timeInMS));
};
- /**
- * When the movement dropdown is changes
- */
@undoBatch
updateMovement = action((movement: PresMovement, all?: boolean) => (all ? this.childDocs : this.selectedArray).forEach(doc => (doc.presMovement = movement)));
@@ -1176,6 +1387,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@undoBatch
@action
+ updateHide = (activeItem: Doc) => {
+ activeItem.presHide = !activeItem.presHide;
+ this.selectedArray.forEach(doc => (doc.presHide = activeItem.presHide));
+ };
+
+ @undoBatch
+ @action
updateHideAfter = (activeItem: Doc) => {
activeItem.presHideAfter = !activeItem.presHideAfter;
this.selectedArray.forEach(doc => (doc.presHideAfter = activeItem.presHideAfter));
@@ -1184,27 +1402,204 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@undoBatch
@action
updateOpenDoc = (activeItem: Doc) => {
- activeItem.openDocument = !activeItem.openDocument;
- this.selectedArray.forEach(doc => (doc.openDocument = activeItem.openDocument));
+ activeItem.presOpenInLightbox = !activeItem.presOpenInLightbox;
+ this.selectedArray.forEach(doc => (doc.presOpenInLightbox = activeItem.presOpenInLightbox));
};
@undoBatch
@action
- updateEffectDirection = (effect: PresEffect, all?: boolean) => (all ? this.childDocs : this.selectedArray).forEach(doc => (doc.presEffectDirection = effect));
+ updateEaseFunc = (activeItem: Doc) => {
+ activeItem.presEaseFunc = activeItem.presEaseFunc === 'linear' ? 'ease' : 'linear';
+ this.selectedArray.forEach(doc => (doc.presEaseFunc = activeItem.presEaseFunc));
+ };
@undoBatch
@action
- updateEffect = (effect: PresEffect, all?: boolean) => (all ? this.childDocs : this.selectedArray).forEach(doc => (doc.presEffect = effect));
+ updateEffectDirection = (effect: PresEffectDirection, all?: boolean) => (all ? this.childDocs : this.selectedArray).forEach(doc => (doc.presEffectDirection = effect));
+
+ @undoBatch
+ @action
+ updateEffect = (effect: PresEffect, bullet: boolean, all?: boolean) => (all ? this.childDocs : this.selectedArray).forEach(doc => (bullet ? (doc.presBulletEffect = effect) : (doc.presEffect = effect)));
+
+ static _sliderBatch: any;
+ public static inputter = (min: string, step: string, max: string, value: number, active: boolean, change: (val: string) => void, hmargin?: number) => {
+ return (
+ <input
+ type="range"
+ step={step}
+ 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 => {
+ PresBox._sliderBatch = UndoManager.StartBatch('pres slider');
+ e.stopPropagation();
+ }}
+ onPointerUp={() => PresBox._sliderBatch.end()}
+ onChange={e => {
+ e.stopPropagation();
+ change(e.target.value);
+ }}
+ />
+ );
+ };
+
+ @undoBatch
+ @action
+ applyTo = (array: Doc[]) => {
+ this.updateMovement(this.activeItem.presMovement as PresMovement, true);
+ this.updateEffect(this.activeItem.presEffect as PresEffect, false, true);
+ this.updateEffect(this.activeItem.presBulletEffect as PresEffect, true, true);
+ this.updateEffectDirection(this.activeItem.presEffectDirection as PresEffectDirection, true);
+ const { presTransition, presDuration, presHideBefore, presHideAfter } = this.activeItem;
+ array.forEach(curDoc => {
+ curDoc.presTransition = presTransition;
+ curDoc.presDuration = presDuration;
+ curDoc.presHideBefore = presHideBefore;
+ curDoc.presHideAfter = presHideAfter;
+ });
+ };
+
+ @computed get visibiltyDurationDropdown() {
+ const activeItem = this.activeItem;
+ if (activeItem && this.targetDoc) {
+ const targetType = this.targetDoc.type;
+ let duration = activeItem.presDuration ? NumCast(activeItem.presDuration) / 1000 : 0;
+ if (activeItem.type === DocumentType.AUDIO) duration = NumCast(activeItem.duration);
+ return (
+ <div className="presBox-ribbon">
+ <div className="ribbon-doubleButton">
+ <Tooltip title={<div className="dash-tooltip">{'Hide before presented'}</div>}>
+ <div className={`ribbon-toggle ${activeItem.presHideBefore ? 'active' : ''}`} onClick={() => this.updateHideBefore(activeItem)}>
+ Hide before
+ </div>
+ </Tooltip>
+ <Tooltip title={<div className="dash-tooltip">{'Hide while presented'}</div>}>
+ <div className={`ribbon-toggle ${activeItem.presHide ? 'active' : ''}`} onClick={() => this.updateHide(activeItem)}>
+ Hide
+ </div>
+ </Tooltip>
- _batch: UndoManager.Batch | undefined = undefined;
+ <Tooltip title={<div className="dash-tooltip">{'Hide after presented'}</div>}>
+ <div className={`ribbon-toggle ${activeItem.presHideAfter ? 'active' : ''}`} onClick={() => this.updateHideAfter(activeItem)}>
+ Hide after
+ </div>
+ </Tooltip>
+ <Tooltip title={<div className="dash-tooltip">{'Open in lightbox view'}</div>}>
+ <div className="ribbon-toggle" style={{ backgroundColor: activeItem.presOpenInLightbox ? Colors.LIGHT_BLUE : '' }} onClick={() => this.updateOpenDoc(activeItem)}>
+ Lightbox
+ </div>
+ </Tooltip>
+ <Tooltip title={<div className="dash-tooltip">{'Transition movement style'}</div>}>
+ <div className="ribbon-toggle" onClick={() => this.updateEaseFunc(activeItem)}>
+ {`${StrCast(activeItem.presEaseFunc, 'ease')}`}
+ </div>
+ </Tooltip>
+ </div>
+ {[DocumentType.AUDIO, DocumentType.VID].includes(targetType as any as DocumentType) ? null : (
+ <>
+ <div className="ribbon-doubleButton">
+ <div className="presBox-subheading">Slide Duration</div>
+ <div className="ribbon-property">
+ <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)}>
+ <FontAwesomeIcon icon={'caret-up'} />
+ </div>
+ <div className="ribbon-propertyUpDownItem" onClick={() => this.updateDurationTime(String(duration), -1000)}>
+ <FontAwesomeIcon icon={'caret-down'} />
+ </div>
+ </div>
+ </div>
+ {PresBox.inputter('0.1', '0.1', '20', duration, targetType !== DocumentType.AUDIO, this.updateDurationTime)}
+ <div className={'slider-headers'} style={{ display: targetType === DocumentType.AUDIO ? 'none' : 'grid' }}>
+ <div className="slider-text">Short</div>
+ <div className="slider-text">Medium</div>
+ <div className="slider-text">Long</div>
+ </div>
+ </>
+ )}
+ </div>
+ );
+ }
+ }
+ @computed get progressivizeDropdown() {
+ const activeItem = this.activeItem;
+ if (activeItem && this.targetDoc) {
+ const effect = activeItem.presBulletEffect ? activeItem.presBulletEffect : PresMovement.None;
+ const bulletEffect = (effect: PresEffect) => (
+ <div className={`presBox-dropdownOption ${activeItem.presEffect === effect || (effect === PresEffect.None && !activeItem.presEffect) ? 'active' : ''}`} onPointerDown={StopEvent} onClick={() => this.updateEffect(effect, true)}>
+ {effect}
+ </div>
+ );
+ return (
+ <div className="presBox-ribbon">
+ <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}>
+ <div className="presBox-subheading">Progressivize Collection</div>
+ <input
+ className="presBox-checkbox"
+ style={{ margin: 10 }}
+ type="checkbox"
+ onChange={() => {
+ activeItem.presIndexed = activeItem.presIndexed === undefined ? 0 : undefined;
+ activeItem.presHideBefore = activeItem.presIndexed !== undefined;
+ const tagDoc = PresBox.targetRenderedDoc(this.activeItem);
+ const type = DocCast(tagDoc?.annotationOn)?.type ?? tagDoc.type;
+ activeItem.presIndexedStart = type === DocumentType.COL ? 1 : 0;
+ // a progressivized slide doesn't have sub-slides, but rather iterates over the data list of the target being progressivized.
+ // to avoid creating a new slide to correspond to each of the target's data list, we create a computedField to refernce the target's data list.
+ let dataField = Doc.LayoutFieldKey(tagDoc);
+ if (Cast(tagDoc[dataField], listSpec(Doc), null)?.filter(d => d instanceof Doc) === undefined) dataField = dataField + '_annotations';
+
+ if (DocCast(activeItem.presentationTargetDoc).annotationOn) activeItem.data = ComputedField.MakeFunction(`self.presentationTargetDoc.annotationOn["${dataField}"]`);
+ else activeItem.data = ComputedField.MakeFunction(`self.presentationTargetDoc["${dataField}"]`);
+ }}
+ checked={Cast(activeItem.presIndexed, 'number', null) !== undefined ? true : false}
+ />
+ </div>
+ <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}>
+ <div className="presBox-subheading">Progressivize First Bullet</div>
+ <input className="presBox-checkbox" style={{ margin: 10 }} type="checkbox" onChange={() => (activeItem.presIndexedStart = activeItem.presIndexedStart ? 0 : 1)} checked={!NumCast(activeItem.presIndexedStart)} />
+ </div>
+ <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}>
+ <div className="presBox-subheading">Expand Current Bullet</div>
+ <input className="presBox-checkbox" style={{ margin: 10 }} type="checkbox" onChange={() => (activeItem.presBulletExpand = !activeItem.presBulletExpand)} checked={BoolCast(activeItem.presBulletExpand)} />
+ </div>
+
+ <div className="ribbon-box">
+ Bullet Effect
+ <div
+ className="presBox-dropdown"
+ onClick={action(e => {
+ e.stopPropagation();
+ this._openBulletEffectDropdown = !this._openBulletEffectDropdown;
+ })}
+ style={{ borderBottomLeftRadius: this._openBulletEffectDropdown ? 0 : 5, border: this._openBulletEffectDropdown ? `solid 2px ${Colors.MEDIUM_BLUE}` : 'solid 1px black' }}>
+ {effect?.toString()}
+ <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this._openBulletEffectDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon={'angle-down'} />
+ <div className={'presBox-dropdownOptions'} style={{ display: this._openBulletEffectDropdown ? 'grid' : 'none' }} onPointerDown={e => e.stopPropagation()}>
+ {bulletEffect(PresEffect.None)}
+ {bulletEffect(PresEffect.Fade)}
+ {bulletEffect(PresEffect.Flip)}
+ {bulletEffect(PresEffect.Rotate)}
+ {bulletEffect(PresEffect.Bounce)}
+ {bulletEffect(PresEffect.Roll)}
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+ }
+ return null;
+ }
@computed get transitionDropdown() {
- const activeItem: Doc = this.activeItem;
- const targetDoc: Doc = this.targetDoc;
- const isPresCollection: boolean = targetDoc === this.layoutDoc.presCollection;
- const isPinWithView: boolean = BoolCast(activeItem.presPinView);
+ const activeItem = this.activeItem;
const presEffect = (effect: PresEffect) => (
- <div className={`presBox-dropdownOption ${this.activeItem.presEffect === effect || (effect === PresEffect.None && !this.activeItem.presEffect) ? 'active' : ''}`} onPointerDown={StopEvent} onClick={() => this.updateEffect(effect)}>
+ <div className={`presBox-dropdownOption ${activeItem.presEffect === effect || (effect === PresEffect.None && !activeItem.presEffect) ? 'active' : ''}`} onPointerDown={StopEvent} onClick={() => this.updateEffect(effect, false)}>
{effect}
</div>
);
@@ -1213,47 +1608,22 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
{movement}
</div>
);
- const presDirection = (diretion: PresEffect, icon: string, gridColumn: number, gridRow: number, opts: object) => {
- const color = this.activeItem.presEffectDirection === diretion || (diretion === PresEffect.Center && !this.activeItem.presEffectDirection) ? Colors.LIGHT_BLUE : 'black';
+ const presDirection = (direction: PresEffectDirection, icon: string, gridColumn: number, gridRow: number, opts: object) => {
+ const color = activeItem.presEffectDirection === direction || (direction === PresEffectDirection.Center && !activeItem.presEffectDirection) ? Colors.LIGHT_BLUE : 'black';
return (
- <Tooltip title={<div className="dash-tooltip">{diretion}</div>}>
+ <Tooltip title={<div className="dash-tooltip">{direction}</div>}>
<div
- style={{ ...opts, border: diretion === PresEffect.Center ? `solid 2px ${color}` : undefined, borderRadius: '100%', cursor: 'pointer', gridColumn, gridRow, justifySelf: 'center', color }}
- onClick={() => this.updateEffectDirection(diretion)}>
+ style={{ ...opts, border: direction === PresEffectDirection.Center ? `solid 2px ${color}` : undefined, borderRadius: '100%', cursor: 'pointer', gridColumn, gridRow, justifySelf: 'center', color }}
+ onClick={() => this.updateEffectDirection(direction)}>
{icon ? <FontAwesomeIcon icon={icon as any} /> : null}
</div>
</Tooltip>
);
};
- const inputter = (min: string, step: string, max: string, value: number, active: boolean, change: (val: string) => void) => {
- return (
- <input
- type="range"
- step={step}
- min={min}
- max={max}
- value={value}
- className={`toolbar-slider ${active ? '' : 'none'}`}
- onPointerDown={e => {
- this._batch = UndoManager.StartBatch('pres slider');
- e.stopPropagation();
- }}
- onPointerUp={() => this._batch?.end()}
- onChange={e => {
- e.stopPropagation();
- change(e.target.value);
- }}
- />
- );
- };
- if (activeItem && targetDoc) {
- const type = targetDoc.type;
+ if (activeItem && this.targetDoc) {
const transitionSpeed = activeItem.presTransition ? NumCast(activeItem.presTransition) / 1000 : 0.5;
const zoom = NumCast(activeItem.presZoom, 1) * 100;
- let duration = activeItem.presDuration ? NumCast(activeItem.presDuration) / 1000 : 2;
- if (activeItem.type === DocumentType.AUDIO) duration = NumCast(activeItem.duration);
- const effect = this.activeItem.presEffect ? this.activeItem.presEffect : 'None';
- activeItem.presMovement = activeItem.presMovement ? activeItem.presMovement : 'Zoom';
+ const effect = activeItem.presEffect ? activeItem.presEffect : PresMovement.None;
return (
<div
className={`presBox-ribbon ${this._transitionTools && this.layoutDoc.presStatus === PresStatus.Edit ? 'active' : ''}`}
@@ -1263,6 +1633,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
e.stopPropagation();
this._openMovementDropdown = false;
this._openEffectDropdown = false;
+ this._openBulletEffectDropdown = false;
})}>
<div className="ribbon-box">
Movement
@@ -1276,130 +1647,89 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
{this.movementName(activeItem)}
<FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this._openMovementDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon={'angle-down'} />
<div className={'presBox-dropdownOptions'} id={'presBoxMovementDropdown'} onPointerDown={StopEvent} style={{ display: this._openMovementDropdown ? 'grid' : 'none' }}>
- {isPresCollection || (isPresCollection && isPinWithView) ? null : presMovement(PresMovement.None)}
+ {presMovement(PresMovement.None)}
+ {presMovement(PresMovement.Center)}
{presMovement(PresMovement.Zoom)}
{presMovement(PresMovement.Pan)}
- {isPresCollection || (isPresCollection && isPinWithView) ? null : presMovement(PresMovement.Jump)}
+ {presMovement(PresMovement.Jump)}
</div>
</div>
<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={action(e => this.setZoom(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={undoBatch(() => this.setZoom(String(zoom), 0.1))}>
+ <div className="ribbon-propertyUpDownItem" onClick={() => this.updateZoom(String(zoom), 0.1)}>
<FontAwesomeIcon icon={'caret-up'} />
</div>
- <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setZoom(String(zoom), -0.1))}>
+ <div className="ribbon-propertyUpDownItem" onClick={() => this.updateZoom(String(zoom), -0.1)}>
<FontAwesomeIcon icon={'caret-down'} />
</div>
</div>
</div>
- {inputter('0', '1', '100', zoom, activeItem.presMovement === PresMovement.Zoom, this.setZoom)}
- <div className="ribbon-doubleButton" style={{ display: activeItem.presMovement === PresMovement.Pan || activeItem.presMovement === PresMovement.Zoom ? 'inline-flex' : 'none' }}>
- <div className="presBox-subheading">Movement Speed</div>
+ {PresBox.inputter('0', '1', '100', zoom, activeItem.presMovement === PresMovement.Zoom, this.updateZoom)}
+ <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.setTransitionTime(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={undoBatch(() => this.setTransitionTime(String(transitionSpeed), 1000))}>
+ <div className="ribbon-propertyUpDownItem" onClick={() => this.updateTransitionTime(String(transitionSpeed), 1000)}>
<FontAwesomeIcon icon={'caret-up'} />
</div>
- <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setTransitionTime(String(transitionSpeed), -1000))}>
+ <div className="ribbon-propertyUpDownItem" onClick={() => this.updateTransitionTime(String(transitionSpeed), -1000)}>
<FontAwesomeIcon icon={'caret-down'} />
</div>
</div>
</div>
- {inputter('0.1', '0.1', '10', transitionSpeed, [PresMovement.Pan, PresMovement.Zoom].includes(activeItem.presMovement as any), this.setTransitionTime)}
- <div className={`slider-headers ${activeItem.presMovement === PresMovement.Pan || activeItem.presMovement === PresMovement.Zoom ? '' : 'none'}`}>
+ {PresBox.inputter('0.1', '0.1', '100', transitionSpeed, true, this.updateTransitionTime)}
+ <div className={'slider-headers'}>
<div className="slider-text">Fast</div>
<div className="slider-text">Medium</div>
<div className="slider-text">Slow</div>
</div>
</div>
<div className="ribbon-box">
- Visibility {'&'} Duration
- <div className="ribbon-doubleButton">
- {isPresCollection ? null : (
- <Tooltip title={<div className="dash-tooltip">{'Hide before presented'}</div>}>
- <div className={`ribbon-toggle ${activeItem.presHideBefore ? 'active' : ''}`} onClick={() => this.updateHideBefore(activeItem)}>
- Hide before
- </div>
- </Tooltip>
- )}
- {isPresCollection ? null : (
- <Tooltip title={<div className="dash-tooltip">{'Hide after presented'}</div>}>
- <div className={`ribbon-toggle ${activeItem.presHideAfter ? 'active' : ''}`} onClick={() => this.updateHideAfter(activeItem)}>
- Hide after
- </div>
- </Tooltip>
- )}
- <Tooltip title={<div className="dash-tooltip">{'Open in lightbox view'}</div>}>
- <div className="ribbon-toggle" style={{ backgroundColor: activeItem.openDocument ? Colors.LIGHT_BLUE : '' }} onClick={() => this.updateOpenDoc(activeItem)}>
- Lightbox
- </div>
- </Tooltip>
+ Effects
+ <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}>
+ <div className="presBox-subheading">Play Audio Annotation</div>
+ <input className="presBox-checkbox" style={{ margin: 10 }} type="checkbox" onChange={() => (activeItem.presPlayAudio = !BoolCast(activeItem.presPlayAudio))} checked={BoolCast(activeItem.presPlayAudio)} />
</div>
- {type === DocumentType.AUDIO || type === DocumentType.VID ? null : (
- <>
- <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={action(e => this.setDurationTime(e.target.value))} /> s
- </div>
- <div className="ribbon-propertyUpDown">
- <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setDurationTime(String(duration), 1000))}>
- <FontAwesomeIcon icon={'caret-up'} />
- </div>
- <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setDurationTime(String(duration), -1000))}>
- <FontAwesomeIcon icon={'caret-down'} />
- </div>
- </div>
- </div>
- {inputter('0.1', '0.1', '20', duration, targetDoc.type !== DocumentType.AUDIO, this.setDurationTime)}
- <div className={'slider-headers'} style={{ display: targetDoc.type === DocumentType.AUDIO ? 'none' : 'grid' }}>
- <div className="slider-text">Short</div>
- <div className="slider-text">Medium</div>
- <div className="slider-text">Long</div>
- </div>
- </>
- )}
- </div>
- {isPresCollection ? null : (
- <div className="ribbon-box">
- Effects
- <div
- className="presBox-dropdown"
- onClick={action(e => {
- e.stopPropagation();
- this._openEffectDropdown = !this._openEffectDropdown;
- })}
- style={{ borderBottomLeftRadius: this._openEffectDropdown ? 0 : 5, border: this._openEffectDropdown ? `solid 2px ${Colors.MEDIUM_BLUE}` : 'solid 1px black' }}>
- {effect.toString()}
- <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this._openEffectDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon={'angle-down'} />
- <div className={'presBox-dropdownOptions'} id={'presBoxMovementDropdown'} style={{ display: this._openEffectDropdown ? 'grid' : 'none' }} onPointerDown={e => e.stopPropagation()}>
- {presEffect(PresEffect.None)}
- {presEffect(PresEffect.Fade)}
- {presEffect(PresEffect.Flip)}
- {presEffect(PresEffect.Rotate)}
- {presEffect(PresEffect.Bounce)}
- {presEffect(PresEffect.Roll)}
- </div>
- </div>
- <div className="ribbon-doubleButton" style={{ display: effect === 'None' ? 'none' : 'inline-flex' }}>
- <div className="presBox-subheading">Effect direction</div>
- <div className="ribbon-property">{StrCast(this.activeItem.presEffectDirection)}</div>
- </div>
- <div className="effectDirection" style={{ display: effect === 'None' ? 'none' : 'grid', width: 40 }}>
- {presDirection(PresEffect.Left, 'angle-right', 1, 2, {})}
- {presDirection(PresEffect.Right, 'angle-left', 3, 2, {})}
- {presDirection(PresEffect.Top, 'angle-down', 2, 1, {})}
- {presDirection(PresEffect.Bottom, 'angle-up', 2, 3, {})}
- {presDirection(PresEffect.Center, '', 2, 2, { width: 10, height: 10, alignSelf: 'center' })}
+ <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}>
+ <div className="presBox-subheading">Zoom Text Selections</div>
+ <input className="presBox-checkbox" style={{ margin: 10 }} type="checkbox" onChange={() => (activeItem.presZoomText = !BoolCast(activeItem.presZoomText))} checked={BoolCast(activeItem.presZoomText)} />
+ </div>
+ <div
+ className="presBox-dropdown"
+ onClick={action(e => {
+ e.stopPropagation();
+ this._openEffectDropdown = !this._openEffectDropdown;
+ })}
+ style={{ borderBottomLeftRadius: this._openEffectDropdown ? 0 : 5, border: this._openEffectDropdown ? `solid 2px ${Colors.MEDIUM_BLUE}` : 'solid 1px black' }}>
+ {effect?.toString()}
+ <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this._openEffectDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon={'angle-down'} />
+ <div className={'presBox-dropdownOptions'} id={'presBoxMovementDropdown'} style={{ display: this._openEffectDropdown ? 'grid' : 'none' }} onPointerDown={e => e.stopPropagation()}>
+ {presEffect(PresEffect.None)}
+ {presEffect(PresEffect.Fade)}
+ {presEffect(PresEffect.Flip)}
+ {presEffect(PresEffect.Rotate)}
+ {presEffect(PresEffect.Bounce)}
+ {presEffect(PresEffect.Roll)}
</div>
</div>
- )}
+ <div className="ribbon-doubleButton" style={{ display: effect === PresEffectDirection.None ? 'none' : 'inline-flex' }}>
+ <div className="presBox-subheading">Effect direction</div>
+ <div className="ribbon-property">{StrCast(this.activeItem.presEffectDirection)}</div>
+ </div>
+ <div className="effectDirection" style={{ display: effect === PresEffectDirection.None ? 'none' : 'grid', width: 40 }}>
+ {presDirection(PresEffectDirection.Left, 'angle-right', 1, 2, {})}
+ {presDirection(PresEffectDirection.Right, 'angle-left', 3, 2, {})}
+ {presDirection(PresEffectDirection.Top, 'angle-down', 2, 1, {})}
+ {presDirection(PresEffectDirection.Bottom, 'angle-up', 2, 3, {})}
+ {presDirection(PresEffectDirection.Center, '', 2, 2, { width: 10, height: 10, alignSelf: 'center' })}
+ </div>
+ </div>
<div className="ribbon-final-box">
<div className="ribbon-final-button-hidden" onClick={() => this.applyTo(this.childDocs)}>
Apply to all
@@ -1409,173 +1739,156 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
);
}
}
-
- @undoBatch
- @action
- applyTo = (array: Doc[]) => {
- this.updateMovement(this.activeItem.presMovement as PresMovement, true);
- this.updateEffect(this.activeItem.presEffect as PresEffect, true);
- this.updateEffectDirection(this.activeItem.presEffectDirection as PresEffect, true);
- const { presTransition, presDuration, presHideBefore, presHideAfter } = this.activeItem;
- array.forEach(curDoc => {
- curDoc.presTransition = presTransition;
- curDoc.presDuration = presDuration;
- curDoc.presHideBefore = presHideBefore;
- curDoc.presHideAfter = presHideAfter;
- });
- };
-
@computed get mediaOptionsDropdown() {
- const activeItem: Doc = this.activeItem;
- const targetDoc: Doc = this.targetDoc;
- const clipStart: number = NumCast(activeItem.clipStart);
- const clipEnd: number = NumCast(activeItem.clipEnd, NumCast(activeItem[Doc.LayoutFieldKey(activeItem) + '-duration']));
- const mediaStopDocInd: number = NumCast(activeItem.mediaStopDoc);
- if (activeItem && targetDoc) {
+ const activeItem = this.activeItem;
+ if (activeItem && this.targetDoc) {
+ const clipStart = NumCast(activeItem.clipStart);
+ const clipEnd = NumCast(activeItem.clipEnd, NumCast(activeItem[Doc.LayoutFieldKey(activeItem) + '_duration']));
return (
- <div>
- <div className={'presBox-ribbon'} onClick={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}>
- <div>
- <div className="ribbon-box">
- Start {'&'} End Time
- <div className={'slider-headers'}>
- <div className="slider-block">
- <div className="slider-text" style={{ fontWeight: 500 }}>
- Start time (s)
- </div>
- <div id={'startTime'} className="slider-number" style={{ backgroundColor: Colors.LIGHT_GRAY }}>
- <input
- className="presBox-input"
- style={{ textAlign: 'center', width: '100%', height: 15, fontSize: 10 }}
- type="number"
- value={NumCast(activeItem.presStartTime).toFixed(2)}
- onKeyDown={e => e.stopPropagation()}
- onChange={action((e: React.ChangeEvent<HTMLInputElement>) => {
- activeItem.presStartTime = Number(e.target.value);
- })}
- />
- </div>
- </div>
- <div className="slider-block">
- <div className="slider-text" style={{ fontWeight: 500 }}>
- Duration (s)
- </div>
- <div className="slider-number" style={{ backgroundColor: Colors.LIGHT_BLUE }}>
- {Math.round((NumCast(activeItem.presEndTime) - NumCast(activeItem.presStartTime)) * 10) / 10}
- </div>
+ <div className={'presBox-ribbon'} onClick={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}>
+ <div>
+ <div className="ribbon-box">
+ Start {'&'} End Time
+ <div className={'slider-headers'}>
+ <div className="slider-block">
+ <div className="slider-text" style={{ fontWeight: 500 }}>
+ Start time (s)
</div>
- <div className="slider-block">
- <div className="slider-text" style={{ fontWeight: 500 }}>
- End time (s)
- </div>
- <div id={'endTime'} className="slider-number" style={{ backgroundColor: Colors.LIGHT_GRAY }}>
- <input
- className="presBox-input"
- onKeyDown={e => e.stopPropagation()}
- style={{ textAlign: 'center', width: '100%', height: 15, fontSize: 10 }}
- type="number"
- value={NumCast(activeItem.presEndTime).toFixed(2)}
- onChange={action((e: React.ChangeEvent<HTMLInputElement>) => {
- activeItem.presEndTime = Number(e.target.value);
- })}
- />
- </div>
+ <div id={'startTime'} className="slider-number" style={{ backgroundColor: Colors.LIGHT_GRAY }}>
+ <input
+ 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>) => {
+ activeItem.presStartTime = Number(e.target.value);
+ })}
+ />
</div>
</div>
- <div className="multiThumb-slider">
- <input
- type="range"
- step="0.1"
- min={clipStart}
- max={clipEnd}
- value={NumCast(activeItem.presEndTime)}
- style={{ gridColumn: 1, gridRow: 1 }}
- className={`toolbar-slider ${'end'}`}
- id="toolbar-slider"
- onPointerDown={e => {
- this._batch = UndoManager.StartBatch('presEndTime');
- const endBlock = document.getElementById('endTime');
- if (endBlock) {
- endBlock.style.color = Colors.LIGHT_GRAY;
- endBlock.style.backgroundColor = Colors.MEDIUM_BLUE;
- }
- e.stopPropagation();
- }}
- onPointerUp={() => {
- this._batch?.end();
- const endBlock = document.getElementById('endTime');
- if (endBlock) {
- endBlock.style.color = Colors.BLACK;
- endBlock.style.backgroundColor = Colors.LIGHT_GRAY;
- }
- }}
- onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
- e.stopPropagation();
- activeItem.presEndTime = Number(e.target.value);
- }}
- />
- <input
- type="range"
- step="0.1"
- min={clipStart}
- max={clipEnd}
- value={NumCast(activeItem.presStartTime)}
- style={{ gridColumn: 1, gridRow: 1 }}
- className={`toolbar-slider ${'start'}`}
- id="toolbar-slider"
- onPointerDown={e => {
- this._batch = UndoManager.StartBatch('presStartTime');
- const startBlock = document.getElementById('startTime');
- if (startBlock) {
- startBlock.style.color = Colors.LIGHT_GRAY;
- startBlock.style.backgroundColor = Colors.MEDIUM_BLUE;
- }
- e.stopPropagation();
- }}
- onPointerUp={() => {
- this._batch?.end();
- const startBlock = document.getElementById('startTime');
- if (startBlock) {
- startBlock.style.color = Colors.BLACK;
- startBlock.style.backgroundColor = Colors.LIGHT_GRAY;
- }
- }}
- onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
- e.stopPropagation();
- activeItem.presStartTime = Number(e.target.value);
- }}
- />
- </div>
- <div className="slider-headers">
- <div className="slider-text">{clipStart.toFixed(2)} s</div>
- <div className="slider-text"></div>
- <div className="slider-text">{clipEnd.toFixed(2)} s</div>
- </div>
- </div>
- <div className="ribbon-final-box">
- Playback
- <div className="presBox-subheading">Start playing:</div>
- <div className="presBox-radioButtons">
- <div className="checkbox-container">
- <input className="presBox-checkbox" type="checkbox" onChange={() => (activeItem.mediaStart = 'manual')} checked={activeItem.mediaStart === 'manual'} />
- <div>On click</div>
+ <div className="slider-block">
+ <div className="slider-text" style={{ fontWeight: 500 }}>
+ Duration (s)
</div>
- <div className="checkbox-container">
- <input className="presBox-checkbox" type="checkbox" onChange={() => (activeItem.mediaStart = 'auto')} checked={activeItem.mediaStart === 'auto'} />
- <div>Automatically</div>
+ <div className="slider-number" style={{ backgroundColor: Colors.LIGHT_BLUE }}>
+ {Math.round((NumCast(activeItem.presEndTime) - NumCast(activeItem.presStartTime)) * 10) / 10}
</div>
</div>
- <div className="presBox-subheading">Stop playing:</div>
- <div className="presBox-radioButtons">
- <div className="checkbox-container">
- <input className="presBox-checkbox" type="checkbox" onChange={() => (activeItem.mediaStop = 'manual')} checked={activeItem.mediaStop === 'manual'} />
- <div>At audio end time</div>
+ <div className="slider-block">
+ <div className="slider-text" style={{ fontWeight: 500 }}>
+ End time (s)
</div>
- <div className="checkbox-container">
- <input className="presBox-checkbox" type="checkbox" onChange={() => (activeItem.mediaStop = 'auto')} checked={activeItem.mediaStop === 'auto'} />
- <div>On slide change</div>
+ <div id={'endTime'} className="slider-number" style={{ backgroundColor: Colors.LIGHT_GRAY }}>
+ <input
+ className="presBox-input"
+ 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);
+ })}
+ />
</div>
- {/* <div className="checkbox-container">
+ </div>
+ </div>
+ <div className="multiThumb-slider">
+ <input
+ type="range"
+ step="0.1"
+ min={clipStart}
+ max={clipEnd}
+ value={NumCast(activeItem.presEndTime)}
+ style={{ gridColumn: 1, gridRow: 1 }}
+ className={`toolbar-slider ${'end'}`}
+ id="toolbar-slider"
+ onPointerDown={e => {
+ this._batch = UndoManager.StartBatch('presEndTime');
+ const endBlock = document.getElementById('endTime');
+ if (endBlock) {
+ endBlock.style.color = Colors.LIGHT_GRAY;
+ endBlock.style.backgroundColor = Colors.MEDIUM_BLUE;
+ }
+ e.stopPropagation();
+ }}
+ onPointerUp={() => {
+ this._batch?.end();
+ const endBlock = document.getElementById('endTime');
+ if (endBlock) {
+ endBlock.style.color = Colors.BLACK;
+ endBlock.style.backgroundColor = Colors.LIGHT_GRAY;
+ }
+ }}
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
+ e.stopPropagation();
+ activeItem.presEndTime = Number(e.target.value);
+ }}
+ />
+ <input
+ type="range"
+ step="0.1"
+ min={clipStart}
+ max={clipEnd}
+ value={NumCast(activeItem.presStartTime)}
+ style={{ gridColumn: 1, gridRow: 1 }}
+ className={`toolbar-slider ${'start'}`}
+ id="toolbar-slider"
+ onPointerDown={e => {
+ this._batch = UndoManager.StartBatch('presStartTime');
+ const startBlock = document.getElementById('startTime');
+ if (startBlock) {
+ startBlock.style.color = Colors.LIGHT_GRAY;
+ startBlock.style.backgroundColor = Colors.MEDIUM_BLUE;
+ }
+ e.stopPropagation();
+ }}
+ onPointerUp={() => {
+ this._batch?.end();
+ const startBlock = document.getElementById('startTime');
+ if (startBlock) {
+ startBlock.style.color = Colors.BLACK;
+ startBlock.style.backgroundColor = Colors.LIGHT_GRAY;
+ }
+ }}
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
+ e.stopPropagation();
+ activeItem.presStartTime = Number(e.target.value);
+ }}
+ />
+ </div>
+ <div className="slider-headers">
+ <div className="slider-text">{clipStart.toFixed(2)} s</div>
+ <div className="slider-text"></div>
+ <div className="slider-text">{clipEnd.toFixed(2)} s</div>
+ </div>
+ </div>
+ <div className="ribbon-final-box">
+ Playback
+ <div className="presBox-subheading">Start playing:</div>
+ <div className="presBox-radioButtons">
+ <div className="checkbox-container">
+ <input className="presBox-checkbox" type="checkbox" onChange={() => (activeItem.mediaStart = 'manual')} checked={activeItem.mediaStart === 'manual'} />
+ <div>On click</div>
+ </div>
+ <div className="checkbox-container">
+ <input className="presBox-checkbox" type="checkbox" onChange={() => (activeItem.mediaStart = 'auto')} checked={activeItem.mediaStart === 'auto'} />
+ <div>Automatically</div>
+ </div>
+ </div>
+ <div className="presBox-subheading">Stop playing:</div>
+ <div className="presBox-radioButtons">
+ <div className="checkbox-container">
+ <input className="presBox-checkbox" type="checkbox" onChange={() => (activeItem.mediaStop = 'manual')} checked={activeItem.mediaStop === 'manual'} />
+ <div>At audio end time</div>
+ </div>
+ <div className="checkbox-container">
+ <input className="presBox-checkbox" type="checkbox" onChange={() => (activeItem.mediaStop = 'auto')} checked={activeItem.mediaStop === 'auto'} />
+ <div>On slide change</div>
+ </div>
+ {/* <div className="checkbox-container">
<input className="presBox-checkbox"
type="checkbox"
onChange={() => activeItem.mediaStop = "afterSlide"}
@@ -1592,7 +1905,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
</select>
</div>
</div> */}
- </div>
</div>
</div>
</div>
@@ -1600,11 +1912,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
);
}
}
-
@computed get newDocumentToolbarDropdown() {
return (
<div
- className={'presBox-toolbar-dropdown'}
+ className="presBox-toolbar-dropdown"
style={{ display: this._newDocumentTools && this.layoutDoc.presStatus === 'edit' ? 'inline-flex' : 'none' }}
onClick={e => e.stopPropagation()}
onPointerUp={e => e.stopPropagation()}
@@ -1740,15 +2051,17 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
if (freeform && layout) doc = this.createTemplate(layout, title);
if (!freeform && !layout) doc = Docs.Create.TextDocument('', { _nativeWidth: 400, _width: 225, title: title });
if (doc) {
- const presCollection = Cast(this.layoutDoc.presCollection, Doc, null);
+ const tabMap = CollectionDockingView.Instance?.tabMap;
+ const tab = tabMap && Array.from(tabMap).find(tab => tab.DashDoc.type === DocumentType.COL)?.DashDoc;
+ const presCollection = DocumentManager.GetContextPath(this.activeItem).reverse().lastElement().presentationTargetDoc ?? tab;
const data = Cast(presCollection?.data, listSpec(Doc));
const presData = Cast(this.rootDoc.data, listSpec(Doc));
if (data && presData) {
data.push(doc);
- TabDocView.PinDoc(doc);
+ TabDocView.PinDoc(doc, {});
this.gotoDocument(this.childDocs.length, this.activeItem);
} else {
- this.props.addDocTab(doc, 'add:right');
+ this.props.addDocTab(doc, OpenWhere.addRight);
}
}
};
@@ -1756,20 +2069,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, _fitContentsToBox: true, x, y });
- case 'header': return Docs.Create.FreeformDocument([header()], { title: input ? input : 'Section header', _width: 400, _height: 225, _fitContentsToBox: true, x, y });
- case 'content': return Docs.Create.FreeformDocument([contentTitle(), content()], { title: input ? input : 'Title and content', _width: 400, _height: 225, _fitContentsToBox: true, x, y });
- case 'twoColumns': return Docs.Create.FreeformDocument([contentTitle(), content1(), content2()], { title: input ? input : 'Title and two columns', _width: 400, _height: 225, _fitContentsToBox: 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 })
}
};
@@ -1781,7 +2094,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
className="dropdown-play-button"
onClick={undoBatch(
action(() => {
- this.updateMinimize();
+ this.enterMinimize();
this.turnOffEdit(true);
this.gotoDocument(this.itemIndex, this.activeItem);
})
@@ -1793,6 +2106,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
onClick={undoBatch(
action(() => {
this.layoutDoc.presStatus = 'manual';
+ this.initializePresState(this.itemIndex);
this.turnOffEdit(true);
this.gotoDocument(this.itemIndex, this.activeItem);
})
@@ -1803,470 +2117,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
);
}
- scrollFocus = () => {
- this.startOrPause(false);
- return undefined;
- };
-
- // Case in which the document has keyframes to navigate to next key frame
- @action
- nextKeyframe = (tagDoc: Doc, curDoc: Doc): void => {
- const childDocs = DocListCast(tagDoc[Doc.LayoutFieldKey(tagDoc)]);
- const currentFrame = Cast(tagDoc._currentFrame, 'number', null);
- if (currentFrame === undefined) {
- tagDoc._currentFrame = 0;
- // CollectionFreeFormDocumentView.setupScroll(tagDoc, 0);
- // CollectionFreeFormDocumentView.setupKeyframes(childDocs, 0);
- }
- // if (tagDoc.editScrollProgressivize) CollectionFreeFormDocumentView.updateScrollframe(tagDoc, currentFrame);
- CollectionFreeFormDocumentView.updateKeyframe(childDocs, currentFrame || 0, tagDoc);
- tagDoc._currentFrame = Math.max(0, (currentFrame || 0) + 1);
- tagDoc.lastFrame = Math.max(NumCast(tagDoc._currentFrame), NumCast(tagDoc.lastFrame));
- };
-
@action
- prevKeyframe = (tagDoc: Doc, actItem: Doc): void => {
- const childDocs = DocListCast(tagDoc[Doc.LayoutFieldKey(tagDoc)]);
- const currentFrame = Cast(tagDoc._currentFrame, 'number', null);
- if (currentFrame === undefined) {
- tagDoc._currentFrame = 0;
- // CollectionFreeFormDocumentView.setupKeyframes(childDocs, 0);
- }
- CollectionFreeFormDocumentView.gotoKeyframe(childDocs.slice());
- tagDoc._currentFrame = Math.max(0, (currentFrame || 0) - 1);
- };
-
- /**
- * Returns the collection type as a string for headers
- */
- @computed get stringType() {
- if (this.activeItem) {
- // prettier-ignore
- switch (this.targetDoc.type) {
- case DocumentType.PDF: return 'PDF';
- case DocumentType.RTF: return 'Text node';
- case DocumentType.COL: return 'Collection';
- case DocumentType.AUDIO: return 'Audio';
- case DocumentType.VID: return 'Video';
- case DocumentType.IMG: return 'Image';
- case DocumentType.WEB: return 'Web page';
- case DocumentType.MAP: return 'Map';
- default: return 'Other node';
- }
- }
- return '';
- }
-
- @observable private openActiveColorPicker: boolean = false;
- @observable private openViewedColorPicker: boolean = false;
-
- @computed get progressivizeDropdown() {
- const activeItem: Doc = this.activeItem;
- const targetDoc: Doc = this.targetDoc;
- if (activeItem && targetDoc) {
- const activeFontColor = targetDoc['pres-text-color'] ? StrCast(targetDoc['pres-text-color']) : 'Black';
- const viewedFontColor = targetDoc['pres-text-viewed-color'] ? StrCast(targetDoc['pres-text-viewed-color']) : 'Black';
- return (
- <div className={`presBox-ribbon ${this._progressivizeTools && this.layoutDoc.presStatus === 'edit' ? 'active' : ''}`} onClick={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}>
- <div className="ribbon-box">
- {this.stringType} selected
- <div
- className="ribbon-doubleButton"
- style={{
- borderTop: 'solid 1px darkgrey',
- display: (targetDoc.type === DocumentType.COL && targetDoc._viewType === 'freeform') || targetDoc.type === DocumentType.IMG || targetDoc.type === DocumentType.RTF ? 'inline-flex' : 'none',
- }}>
- <div className="ribbon-toggle" style={{ backgroundColor: activeItem.presProgressivize ? Colors.LIGHT_BLUE : '' }} onClick={this.progressivizeChild}>
- Contents
- </div>
- <div className="ribbon-toggle" style={{ opacity: activeItem.presProgressivize ? 1 : 0.4, backgroundColor: targetDoc.editProgressivize ? Colors.LIGHT_BLUE : '' }} onClick={this.editProgressivize}>
- Edit
- </div>
- </div>
- <div className="ribbon-doubleButton" style={{ display: activeItem.presProgressivize ? 'inline-flex' : 'none' }}>
- <div className="presBox-subheading">Active text color</div>
- <div
- className="ribbon-colorBox"
- style={{ backgroundColor: activeFontColor, height: 15, width: 15 }}
- onClick={action(() => {
- this.openActiveColorPicker = !this.openActiveColorPicker;
- })}></div>
- </div>
- {this.activeColorPicker}
- <div className="ribbon-doubleButton" style={{ display: activeItem.presProgressivize ? 'inline-flex' : 'none' }}>
- <div className="presBox-subheading">Viewed font color</div>
- <div className="ribbon-colorBox" style={{ backgroundColor: viewedFontColor, height: 15, width: 15 }} onClick={action(() => (this.openViewedColorPicker = !this.openViewedColorPicker))}></div>
- </div>
- {this.viewedColorPicker}
- <div
- className="ribbon-doubleButton"
- style={{ borderTop: 'solid 1px darkgrey', display: (targetDoc.type === DocumentType.COL && targetDoc._viewType === 'freeform') || targetDoc.type === DocumentType.IMG ? 'inline-flex' : 'none' }}>
- <div className="ribbon-toggle" style={{ backgroundColor: activeItem.zoomProgressivize ? Colors.LIGHT_BLUE : '' }} onClick={this.progressivizeZoom}>
- Zoom
- </div>
- <div className="ribbon-toggle" style={{ opacity: activeItem.zoomProgressivize ? 1 : 0.4, backgroundColor: activeItem.editZoomProgressivize ? Colors.LIGHT_BLUE : '' }} onClick={this.editZoomProgressivize}>
- Edit
- </div>
- </div>
- <div
- className="ribbon-doubleButton"
- style={{
- borderTop: 'solid 1px darkgrey',
- display: targetDoc._viewType === 'stacking' || targetDoc.type === DocumentType.PDF || targetDoc.type === DocumentType.WEB || targetDoc.type === DocumentType.RTF ? 'inline-flex' : 'none',
- }}>
- <div className="ribbon-toggle" style={{ backgroundColor: activeItem.scrollProgressivize ? Colors.LIGHT_BLUE : '' }} onClick={this.progressivizeScroll}>
- Scroll
- </div>
- <div className="ribbon-toggle" style={{ opacity: activeItem.scrollProgressivize ? 1 : 0.4, backgroundColor: targetDoc.editScrollProgressivize ? Colors.LIGHT_BLUE : '' }} onClick={this.editScrollProgressivize}>
- Edit
- </div>
- </div>
- </div>
- <div className="ribbon-final-box">
- Frames
- <div className="ribbon-doubleButton">
- <div className="ribbon-frameSelector">
- <div
- key="back"
- title="back frame"
- className="backKeyframe"
- onClick={e => {
- e.stopPropagation();
- this.prevKeyframe(targetDoc, activeItem);
- }}>
- <FontAwesomeIcon icon={'caret-left'} size={'lg'} />
- </div>
- <div
- key="num"
- title="toggle view all"
- className="numKeyframe"
- style={{ color: targetDoc.keyFrameEditing ? 'white' : 'black', backgroundColor: targetDoc.keyFrameEditing ? Colors.MEDIUM_BLUE : Colors.LIGHT_BLUE }}
- onClick={action(() => (targetDoc.keyFrameEditing = !targetDoc.keyFrameEditing))}>
- {NumCast(targetDoc._currentFrame)}
- </div>
- <div
- key="fwd"
- title="forward frame"
- className="fwdKeyframe"
- onClick={e => {
- e.stopPropagation();
- this.nextKeyframe(targetDoc, activeItem);
- }}>
- <FontAwesomeIcon icon={'caret-right'} size={'lg'} />
- </div>
- </div>
- <Tooltip
- title={
- <>
- <div className="dash-tooltip">{'Last frame'}</div>
- </>
- }>
- <div className="ribbon-property">{NumCast(targetDoc.lastFrame)}</div>
- </Tooltip>
- </div>
- <div className="ribbon-frameList">
- {this.frameListHeader}
- {this.frameList}
- </div>
- <div className="ribbon-toggle" style={{ height: 20, backgroundColor: Colors.LIGHT_BLUE }} onClick={() => console.log(' TODO: play frames')}>
- Play
- </div>
- </div>
- </div>
- );
- }
- }
-
- @undoBatch
- @action
- switchActive = (color: ColorState) => {
- this.targetDoc['pres-text-color'] = String(color.hex);
- return true;
- };
- @undoBatch
- @action
- switchPresented = (color: ColorState) => {
- this.targetDoc['pres-text-viewed-color'] = String(color.hex);
- return true;
- };
-
- @computed get activeColorPicker() {
- return !this.openActiveColorPicker ? null : (
- <SketchPicker
- onChange={this.switchActive}
- presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb', 'transparent']}
- color={StrCast(this.targetDoc['pres-text-color'])}
- />
- );
- }
-
- @computed get viewedColorPicker() {
- return !this.openViewedColorPicker ? null : (
- <SketchPicker
- onChange={this.switchPresented}
- presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb', 'transparent']}
- color={StrCast(this.targetDoc['pres-text-viewed-color'])}
- />
- );
- }
-
- @action
- turnOffEdit = (paths?: boolean) => {
- // Turn off paths
- if (paths) {
- const srcContext = Cast(this.rootDoc.presCollection, Doc, null);
- if (srcContext) this.togglePath(srcContext, true);
- }
- // Turn off the progressivize editors for each document
- this.childDocs.forEach(doc => {
- doc.editSnapZoomProgressivize = false;
- doc.editZoomProgressivize = false;
- const targetDoc = Cast(doc.presentationTargetDoc, Doc, null);
- if (targetDoc) {
- targetDoc.editZoomProgressivize = false;
- // targetDoc.editScrollProgressivize = false;
- }
- });
- };
-
- //Toggle whether the user edits or not
- @action
- editZoomProgressivize = (e: React.MouseEvent) => {
- const activeItem: Doc = this.activeItem;
- const targetDoc: Doc = this.targetDoc;
- if (!targetDoc.editZoomProgressivize) {
- if (!activeItem.zoomProgressivize) activeItem.zoomProgressivize = true;
- targetDoc.zoomProgressivize = true;
- targetDoc.editZoomProgressivize = true;
- activeItem.editZoomProgressivize = true;
- } else {
- targetDoc.editZoomProgressivize = false;
- activeItem.editZoomProgressivize = false;
- }
- };
-
- //Toggle whether the user edits or not
- @action
- editScrollProgressivize = (e: React.MouseEvent) => {
- const targetDoc: Doc = this.targetDoc;
- if (!targetDoc.editScrollProgressivize) {
- if (!targetDoc.scrollProgressivize) {
- targetDoc.scrollProgressivize = true;
- this.activeItem.scrollProgressivize = true;
- }
- targetDoc.editScrollProgressivize = true;
- } else {
- targetDoc.editScrollProgressivize = false;
- }
- };
-
- //Progressivize Zoom
- @action
- progressivizeScroll = (e: React.MouseEvent) => {
- e.stopPropagation();
- this.activeItem.scrollProgressivize = !this.activeItem.scrollProgressivize;
- const targetDoc: Doc = this.targetDoc;
- targetDoc.scrollProgressivize = !targetDoc.scrollProgressivize;
- // CollectionFreeFormDocumentView.setupScroll(targetDoc, NumCast(targetDoc._currentFrame));
- if (targetDoc.editScrollProgressivize) {
- targetDoc.editScrollProgressivize = false;
- targetDoc._currentFrame = 0;
- targetDoc.lastFrame = 0;
- }
- };
-
- //Progressivize Zoom
- @action
- progressivizeZoom = (e: React.MouseEvent) => {
- e.stopPropagation();
- const activeItem: Doc = this.activeItem;
- activeItem.zoomProgressivize = !activeItem.zoomProgressivize;
- const targetDoc: Doc = this.targetDoc;
- targetDoc.zoomProgressivize = !targetDoc.zoomProgressivize;
- CollectionFreeFormDocumentView.setupZoom(activeItem, targetDoc);
- if (activeItem.editZoomProgressivize) {
- activeItem.editZoomProgressivize = false;
- targetDoc._currentFrame = 0;
- targetDoc.lastFrame = 0;
- }
- };
-
- //Progressivize Child Docs
- @action
- editProgressivize = (e: React.MouseEvent) => {
- const activeItem: Doc = this.activeItem;
- const targetDoc: Doc = this.targetDoc;
- targetDoc._currentFrame = targetDoc.lastFrame;
- if (!targetDoc.editProgressivize) {
- if (!activeItem.presProgressivize) {
- activeItem.presProgressivize = true;
- targetDoc.presProgressivize = true;
- }
- targetDoc.editProgressivize = true;
- } else {
- targetDoc.editProgressivize = false;
- }
- };
-
- @action
- progressivizeChild = (e: React.MouseEvent) => {
- e.stopPropagation();
- const activeItem: Doc = this.activeItem;
- const targetDoc: Doc = this.targetDoc;
- const docs = DocListCast(targetDoc[Doc.LayoutFieldKey(targetDoc)]);
- if (!activeItem.presProgressivize) {
- targetDoc.keyFrameEditing = false;
- activeItem.presProgressivize = true;
- targetDoc.presProgressivize = true;
- targetDoc._currentFrame = 0;
- docs.forEach((doc, i) => CollectionFreeFormDocumentView.setupKeyframes([doc], i, true));
- targetDoc.lastFrame = targetDoc.lastFrame ? NumCast(targetDoc.lastFrame) : docs.length - 1;
- } else {
- // targetDoc.editProgressivize = false;
- activeItem.presProgressivize = false;
- targetDoc.presProgressivize = false;
- targetDoc._currentFrame = 0;
- targetDoc.keyFrameEditing = true;
- }
- };
-
- @action
- checkMovementLists = (doc: Doc, xlist: any, ylist: any) => {
- const x: List<number> = xlist;
- const y: List<number> = ylist;
- const tags: JSX.Element[] = [];
- let pathPoints = ''; //List of all of the pathpoints that need to be added
- for (let i = 0; i < x.length - 1; i++) {
- if (y[i] || x[i]) {
- if (i === 0) pathPoints = x[i] - 11 + ',' + (y[i] + 33);
- else pathPoints = pathPoints + ' ' + (x[i] - 11) + ',' + (y[i] + 33);
- tags.push(
- <div className="progressivizeMove-frame" style={{ position: 'absolute', top: y[i], left: x[i] }}>
- {i}
- </div>
- );
- }
- }
- tags.push(
- <svg style={{ overflow: 'visible', position: 'absolute' }}>
- <polyline
- points={pathPoints}
- style={{
- position: 'absolute',
- opacity: 1,
- stroke: '#000000',
- strokeWidth: 2,
- strokeDasharray: '10 5',
- }}
- fill="none"
- />
- </svg>
- );
- return tags;
- };
-
- @observable
- toggleDisplayMovement = (doc: Doc) => (doc.displayMovement = !doc.displayMovement);
-
- @action
- checkList = (doc: Doc, list: any): number => {
- const x: List<number> = list;
- if (x?.length >= NumCast(doc._currentFrame) + 1) {
- return x[NumCast(doc._currentFrame)];
- } else if (x) {
- x.length = NumCast(doc._currentFrame) + 1;
- x[NumCast(doc._currentFrame)] = x[NumCast(doc._currentFrame) - 1];
- return x[NumCast(doc._currentFrame)];
- }
- return 100;
- };
-
- @computed get progressivizeChildDocs() {
- const targetDoc: Doc = this.targetDoc;
- const docs = DocListCast(targetDoc[Doc.LayoutFieldKey(targetDoc)]);
- const tags: JSX.Element[] = [];
- docs.forEach((doc, index) => {
- if (doc['x-indexed'] && doc['y-indexed']) {
- tags.push(<div style={{ position: 'absolute', display: doc.displayMovement ? 'block' : 'none' }}>{this.checkMovementLists(doc, doc['x-indexed'], doc['y-indexed'])}</div>);
- }
- tags.push(
- <div
- className="progressivizeButton"
- key={index}
- onPointerLeave={() => {
- if (NumCast(targetDoc._currentFrame) < NumCast(doc.appearFrame)) doc.opacity = 0;
- }}
- onPointerOver={() => {
- if (NumCast(targetDoc._currentFrame) < NumCast(doc.appearFrame)) doc.opacity = 0.5;
- }}
- onClick={e => {
- this.toggleDisplayMovement(doc);
- e.stopPropagation();
- }}
- style={{ backgroundColor: doc.displayMovement ? Colors.LIGHT_BLUE : '#c8c8c8', top: NumCast(doc.y), left: NumCast(doc.x) }}>
- <div className="progressivizeButton-prev">
- <FontAwesomeIcon
- icon={'caret-left'}
- size={'lg'}
- onClick={e => {
- e.stopPropagation();
- this.prevAppearFrame(doc, index);
- }}
- />
- </div>
- <div className="progressivizeButton-frame">{NumCast(doc.appearFrame)}</div>
- <div className="progressivizeButton-next">
- <FontAwesomeIcon
- icon={'caret-right'}
- size={'lg'}
- onClick={e => {
- e.stopPropagation();
- this.nextAppearFrame(doc, index);
- }}
- />
- </div>
- </div>
- );
- });
- return tags;
- }
-
- @action
- nextAppearFrame = (doc: Doc, i: number) => {
- doc.appearFrame = (Cast(doc.appearFrame, 'number', null) ?? 0) + 1;
- this.updateOpacityList(doc['opacity-indexed'], NumCast(doc.appearFrame));
- };
-
- @action
- prevAppearFrame = (doc: Doc, i: number) => {
- doc.appearFrame = Math.max(0, (Cast(doc.appearFrame, 'number', null) ?? 0) - 1);
- this.updateOpacityList(doc['opacity-indexed'], NumCast(doc.appearFrame));
- };
-
- @action
- updateOpacityList = (list: any, frame: number) => {
- const x: List<number> = list;
- if (x && x.length >= frame) {
- for (let i = 0; i < x.length; i++) {
- if (i < frame) {
- x[i] = 0;
- } else if (i >= frame) {
- x[i] = 1;
- }
- }
- list = x;
- } else {
- x.length = frame + 1;
- for (let i = 0; i < x.length; i++) {
- if (i < frame) {
- x[i] = 0;
- } else if (i >= frame) {
- x[i] = 1;
- }
- }
- list = x;
- }
- };
+ turnOffEdit = (paths?: boolean) => paths && this.togglePath(true); // Turn off paths
@computed
get toolbarWidth(): number {
@@ -2279,35 +2131,27 @@ 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 activeColor = Colors.LIGHT_BLUE;
const inactiveColor = Colors.WHITE;
- return mode === CollectionViewType.Carousel3D ? 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"} />
<FontAwesomeIcon className={`dropdown ${this.newDocumentTools ? "active" : ""}`} icon={"angle-down"} />
</div></Tooltip> */}
- <Tooltip title={<div className="dash-tooltip">{'View paths'}</div>}>
+ <Tooltip title={<div className="dash-tooltip">View paths</div>}>
<div
- style={{ opacity: this.childDocs.length > 1 && this.layoutDoc.presCollection ? 1 : 0.3, color: this._pathBoolean ? Colors.MEDIUM_BLUE : 'white', width: isMini ? '100%' : undefined }}
+ style={{ opacity: this.childDocs.length > 1 ? 1 : 0.3, color: this._pathBoolean ? Colors.MEDIUM_BLUE : 'white', width: isMini ? '100%' : undefined }}
className={'toolbar-button'}
- onClick={this.childDocs.length > 1 && this.layoutDoc.presCollection ? this.viewPaths : undefined}>
+ onClick={this.childDocs.length > 1 ? () => this.togglePath() : undefined}>
<FontAwesomeIcon icon={'exchange-alt'} />
</div>
</Tooltip>
{isMini ? null : (
<>
<div className="toolbar-divider" />
- {/* <Tooltip title={<><div className="dash-tooltip">{this._expandBoolean ? "Minimize all" : "Expand all"}</div></>}>
- <div className={"toolbar-button"}
- style={{ color: this._expandBoolean ? Colors.MEDIUM_BLUE : 'white' }}
- onClick={this.toggleExpandMode}>
- <FontAwesomeIcon icon={"eye"} />
- </div>
- </Tooltip>
- <div className="toolbar-divider" /> */}
<Tooltip title={<div className="dash-tooltip">{this._presKeyEvents ? 'Keys are active' : 'Keys are not active - click anywhere on the presentation trail to activate keys'}</div>}>
<div className="toolbar-button" style={{ cursor: this._presKeyEvents ? 'default' : 'pointer', position: 'absolute', right: 30, fontSize: 16 }}>
<FontAwesomeIcon className={'toolbar-thumbtack'} icon={'keyboard'} style={{ color: this._presKeyEvents ? activeColor : inactiveColor }} />
@@ -2330,10 +2174,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;
return (
- <div className="presBox-buttons" 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}>
@@ -2350,12 +2196,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
</select>
)}
<div className="presBox-presentPanel" style={{ opacity: this.childDocs.length ? 1 : 0.3 }}>
- <span className={`presBox-button ${this.layoutDoc.presStatus === 'edit' ? 'present' : ''}`}>
+ <span className={`presBox-button ${this.layoutDoc.presStatus === PresStatus.Edit ? 'present' : ''}`}>
<div
className="presBox-button-left"
onClick={undoBatch(() => {
if (this.childDocs.length) {
this.layoutDoc.presStatus = 'manual';
+ this.initializePresState(this.itemIndex);
this.gotoDocument(this.itemIndex, this.activeItem);
}
})}>
@@ -2379,89 +2226,18 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
);
}
- @action
- getList = (list: any): List<number> => list;
-
- @action
- updateList = (list: any): List<number> => {
- const targetDoc: Doc = this.targetDoc;
- const x: List<number> = list;
- x[x.length - 1] = NumCast(targetDoc._scrollY);
- return x;
- };
-
- @action
- newFrame = () => {
- const activeItem: Doc = this.activeItem;
- const type: string = StrCast(this.targetDoc.type);
- if (!activeItem.frameList) activeItem.frameList = new List<number>();
- switch (type) {
- case DocumentType.PDF || DocumentType.RTF || DocumentType.WEB:
- this.updateList(activeItem.frameList);
- break;
- }
- };
-
- @computed get frameListHeader() {
- return (
- <div className="frameList-header">
- &nbsp; Frames {this.panable ? <i>Panable</i> : this.scrollable ? <i>Scrollable</i> : null}
- <div className={'frameList-headerButtons'}>
- <Tooltip title={<div className="dash-tooltip">{'Add frame by example'}</div>}>
- <div
- className={'headerButton'}
- onClick={e => {
- e.stopPropagation();
- this.newFrame();
- }}>
- <FontAwesomeIcon icon={'plus'} onPointerDown={e => e.stopPropagation()} />
- </div>
- </Tooltip>
- <Tooltip title={<div className="dash-tooltip">{'Edit in collection'}</div>}>
- <div
- className={'headerButton'}
- onClick={e => {
- e.stopPropagation();
- console.log('New frame');
- }}>
- <FontAwesomeIcon icon={'edit'} onPointerDown={e => e.stopPropagation()} />
- </div>
- </Tooltip>
- </div>
- </div>
- );
- }
-
- @computed get frameList() {
- const frameList: List<number> = this.getList(this.activeItem.frameList);
- return !frameList ? null : (
- <div className="frameList-container">
- {frameList.map(value => (
- <div className="framList-item" />
- ))}
- </div>
- );
- }
-
- @computed get playButtonFrames() {
- const targetDoc = this.targetDoc;
- return !this.targetDoc ? null : (
- <div className="presPanel-button-frame" style={{ display: targetDoc.lastFrame !== undefined && targetDoc.lastFrame >= 0 ? 'inline-flex' : 'none' }}>
- <div>{NumCast(targetDoc._currentFrame)}</div>
- <div className="presPanel-divider" style={{ border: 'solid 0.5px white', height: '60%' }}></div>
- <div>{NumCast(targetDoc.lastFrame)}</div>
- </div>
- );
- }
-
@computed get playButtons() {
- const presEnd: boolean = !this.layoutDoc.presLoop && this.itemIndex === this.childDocs.length - 1;
+ 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 = 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' }}>
<Tooltip title={<div className="dash-tooltip">{'Loop'}</div>}>
- <div className="presPanel-button" style={{ color: this.layoutDoc.presLoop ? Colors.MEDIUM_BLUE : 'white' }} onClick={() => (this.layoutDoc.presLoop = !this.layoutDoc.presLoop)}>
+ <div
+ className="presPanel-button"
+ style={{ color: this.layoutDoc.presLoop ? Colors.MEDIUM_BLUE : 'white' }}
+ onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => (this.layoutDoc.presLoop = !this.layoutDoc.presLoop), false, false)}>
<FontAwesomeIcon icon={'redo-alt'} />
</div>
</Tooltip>
@@ -2469,43 +2245,78 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
<div
className="presPanel-button"
style={{ opacity: presStart ? 0.4 : 1 }}
- onClick={e => {
- this.back();
- if (this._presTimer) {
- clearTimeout(this._presTimer);
- this.layoutDoc.presStatus = PresStatus.Manual;
- }
- e.stopPropagation();
- }}>
+ onPointerDown={e =>
+ setupMoveUpEvents(
+ this,
+ e,
+ returnFalse,
+ emptyFunction,
+ () => {
+ this.back();
+ if (this._presTimer) {
+ clearTimeout(this._presTimer);
+ this.layoutDoc.presStatus = PresStatus.Manual;
+ }
+ e.stopPropagation();
+ },
+ false,
+ false
+ )
+ }>
<FontAwesomeIcon icon={'arrow-left'} />
</div>
<Tooltip title={<div className="dash-tooltip">{this.layoutDoc.presStatus === PresStatus.Autoplay ? 'Pause' : 'Autoplay'}</div>}>
- <div className="presPanel-button" onClick={e => this.startOrPause(true)}>
- <FontAwesomeIcon icon={this.layoutDoc.presStatus === PresStatus.Autoplay ? 'pause' : 'play'} />
+ <div className="presPanel-button" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => this.startOrPause(true), false, false)}>
+ <FontAwesomeIcon color={this.layoutDoc.presStatus === PresStatus.Autoplay ? 'red' : undefined} icon={this.layoutDoc.presStatus === PresStatus.Autoplay ? 'pause' : 'play'} />
</div>
</Tooltip>
<div
className="presPanel-button"
style={{ opacity: presEnd ? 0.4 : 1 }}
- onClick={e => {
- this.next();
- if (this._presTimer) {
- clearTimeout(this._presTimer);
- this.layoutDoc.presStatus = PresStatus.Manual;
- }
- e.stopPropagation();
- }}>
+ onPointerDown={e =>
+ setupMoveUpEvents(
+ this,
+ e,
+ returnFalse,
+ emptyFunction,
+ () => {
+ this.next();
+ if (this._presTimer) {
+ clearTimeout(this._presTimer);
+ this.layoutDoc.presStatus = PresStatus.Manual;
+ }
+ e.stopPropagation();
+ },
+ false,
+ false
+ )
+ }>
<FontAwesomeIcon icon={'arrow-right'} />
</div>
<div className="presPanel-divider"></div>
<Tooltip title={<div className="dash-tooltip">{'Click to return to 1st slide'}</div>}>
- <div className="presPanel-button" style={{ border: 'solid 1px white' }} onClick={() => this.nextSlide(0)}>
+ <div
+ className="presPanel-button"
+ style={{ border: 'solid 1px white' }}
+ onPointerDown={e =>
+ setupMoveUpEvents(
+ this,
+ e,
+ returnFalse,
+ emptyFunction,
+ () => {
+ this.nextSlide(0);
+ },
+ false,
+ false
+ )
+ }>
<b>1</b>
</div>
</Tooltip>
- <div className="presPanel-button-text" onClick={() => this.gotoDocument(0, this.activeItem)} style={{ display: this.props.PanelWidth() > 250 ? 'inline-flex' : 'none' }}>
- Slide {this.itemIndex + 1} / {this.childDocs.length}
- {this.playButtonFrames}
+ <div className="presPanel-button-text" onClick={() => this.gotoDocument(0, this.activeItem)} style={{ display: inOverlay || this.props.PanelWidth() > 250 ? 'inline-flex' : 'none' }}>
+ {inOverlay ? '' : 'Slide'} {this.itemIndex + 1}
+ {this.activeItem?.presIndexed !== undefined ? `(${this.activeItem.presIndexed}/${this.progressivizedItems(this.activeItem)?.length})` : ''} / {this.childDocs.length}
</div>
<div className="presPanel-divider"></div>
{this.props.PanelWidth() > 250 ? (
@@ -2513,14 +2324,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
className="presPanel-button-text"
onClick={undoBatch(
action(() => {
- this.layoutDoc.presStatus = 'edit';
+ this.layoutDoc.presStatus = PresStatus.Edit;
clearTimeout(this._presTimer);
})
)}>
EXIT
</div>
) : (
- <div className="presPanel-button" onClick={undoBatch(action(() => (this.layoutDoc.presStatus = 'edit')))}>
+ <div className="presPanel-button" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, this.exitClicked, false, false)}>
<FontAwesomeIcon icon={'times'} />
</div>
)}
@@ -2531,7 +2342,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@action
startOrPause = (makeActive = true) => {
makeActive && this.updateCurrentPresentation();
- if (this.layoutDoc.presStatus === PresStatus.Manual || this.layoutDoc.presStatus === PresStatus.Edit) this.startAutoPres(this.itemIndex);
+ if (!this.layoutDoc.presStatus || this.layoutDoc.presStatus === PresStatus.Manual || this.layoutDoc.presStatus === PresStatus.Edit) this.startPresentation(this.itemIndex);
else this.pauseAutoPres();
};
@@ -2552,18 +2363,16 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
this.layoutDoc.presStatus = PresStatus.Manual;
}
};
+
@undoBatch
@action
- exitClicked = (e: PointerEvent) => {
- this.updateMinimize();
- this.layoutDoc.presStatus = PresStatus.Edit;
+ exitClicked = () => {
+ this.layoutDoc.presStatus = this._exitTrail?.() ?? this.exitMinimize();
clearTimeout(this._presTimer);
};
- @action
- startMarqueeCreateSlide = () => (PresBox.startMarquee = true);
-
- AddToMap = (treeViewDoc: Doc, index: number[]): Doc[] => {
+ AddToMap = (treeViewDoc: Doc, index: number[]) => {
+ if (!treeViewDoc.presentationTargetDoc) return this.childDocs; // if treeViewDoc is not a pres elements, then it's a sub-bullet of a progressivized slide which isn't added to the linearized list of pres elements since it's not really a pres element.
var indexNum = 0;
for (let i = 0; i < index.length; i++) {
indexNum += index[i] * 10 ** -i;
@@ -2576,26 +2385,24 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
this.dataDoc[this.presFieldKey] = new List<Doc>(sorted); // this is a flat array of Docs
}
}
- return this.childDocs;
};
- RemFromMap = (treeViewDoc: Doc, index: number[]): Doc[] => {
+ RemFromMap = (treeViewDoc: Doc, index: number[]) => {
+ if (!treeViewDoc.presentationTargetDoc) return this.childDocs; // if treeViewDoc is not a pres elements, then it's a sub-bullet of a progressivized slide which isn't added to the linearized list of pres elements since it's not really a pres element.
if (!this._unmounting && this.isTree) {
this._treeViewMap.delete(treeViewDoc);
this.dataDoc[this.presFieldKey] = new List<Doc>(this.sort(this._treeViewMap));
}
- return this.childDocs;
};
- // TODO: [AL] implement sort function for an array of numbers (e.g. arr[1,2,4] v arr[1,2,1])
sort = (treeViewMap: Map<Doc, number>) => [...treeViewMap.entries()].sort((a: [Doc, number], b: [Doc, number]) => (a[1] > b[1] ? 1 : a[1] < b[1] ? -1 : 0)).map(kv => kv[0]);
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 presEnd: boolean = !this.layoutDoc.presLoop && this.itemIndex === this.childDocs.length - 1;
- const presStart: boolean = !this.layoutDoc.presLoop && this.itemIndex === 0;
+ 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;
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
@@ -2628,8 +2435,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
</div>
</Tooltip>
<div className="presPanel-button-text">
- Slide {this.itemIndex + 1} / {this.childDocs.length}
- {this.playButtonFrames}
+ Slide {this.itemIndex + 1}
+ {this.activeItem?.presIndexed !== undefined ? `(${this.activeItem.presIndexed}/${this.progressivizedItems(this.activeItem)?.length})` : ''} / {this.childDocs.length}
</div>
<div className="presPanel-divider" />
<div className="presPanel-button-text" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, returnFalse, this.exitClicked, false, false)}>
@@ -2638,7 +2445,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
</div>
</div>
) : (
- <div className="presBox-cont" style={{ minWidth: DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc) ? 240 : undefined }}>
+ <div className="presBox-cont" style={{ minWidth: Doc.IsInMyOverlay(this.rootDoc) ? PresBox.minimizedWidth : undefined }}>
{this.topPanel}
{this.toolbar}
{this.newDocumentToolbarDropdown}
@@ -2647,24 +2454,26 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
{mode !== CollectionViewType.Invalid ? (
<CollectionView
{...this.props}
- ContainingCollectionDoc={this.props.Document}
PanelWidth={this.props.PanelWidth}
PanelHeight={this.panelHeight}
childIgnoreNativeSize={true}
moveDocument={returnFalse}
- //childFitWidth={returnTrue}
+ ignoreUnrendered={true}
+ childDragAction="move"
+ //childLayoutFitWidth={returnTrue}
childOpacity={returnOne}
+ childClickScript={PresBox.navigateToDocScript}
childLayoutTemplate={this.childLayoutTemplate}
childXPadding={Doc.IsComicStyle(this.rootDoc) ? 20 : undefined}
filterAddDocument={this.addDocumentFilter}
removeDocument={returnFalse}
dontRegisterView={true}
- focus={this.selectElement}
+ focus={this.focusElement}
scriptContext={this}
ScreenToLocalTransform={this.getTransform}
AddToMap={this.AddToMap}
RemFromMap={this.RemFromMap}
- hierarchyIndex={[]}
+ hierarchyIndex={emptyPath}
/>
) : null}
</div>
@@ -2681,16 +2490,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
</div>
);
}
- static NavigateToDoc(bestTarget: Doc, activeItem: Doc) {
- const srcContext = Cast(bestTarget.context, Doc, null) ?? Cast(Cast(bestTarget.annotationOn, Doc, null)?.context, Doc, null);
- const openInTab = (doc: Doc, finished?: () => void) => {
- CollectionDockingView.AddSplit(doc, 'right');
- finished?.();
- };
- PresBox.NavigateToTarget(bestTarget, activeItem, openInTab, srcContext);
- }
}
ScriptingGlobals.add(function navigateToDoc(bestTarget: Doc, activeItem: Doc) {
- PresBox.NavigateToDoc(bestTarget, activeItem);
+ PresBox.NavigateToTarget(bestTarget, activeItem);
});
diff --git a/src/client/views/nodes/trails/PresElementBox.scss b/src/client/views/nodes/trails/PresElementBox.scss
index 415253af1..4f95f0c1f 100644
--- a/src/client/views/nodes/trails/PresElementBox.scss
+++ b/src/client/views/nodes/trails/PresElementBox.scss
@@ -44,6 +44,12 @@ $slide-active: #5b9fdd;
display: flex;
align-items: center;
+ .presItem-number {
+ cursor: pointer;
+ &:hover {
+ background-color: $light-blue;
+ }
+ }
.presItem-name {
display: flex;
min-width: 20px;
diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx
index 5d14a0e9a..f31cf6147 100644
--- a/src/client/views/nodes/trails/PresElementBox.tsx
+++ b/src/client/views/nodes/trails/PresElementBox.tsx
@@ -1,22 +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 { Copy, Id } from '../../../../fields/FieldSymbols';
-import { InkField } from '../../../../fields/InkField';
+import { Doc, DocListCast, Opt } from '../../../../fields/Doc';
+import { Height, Width } from '../../../../fields/DocSymbols';
+import { Id } from '../../../../fields/FieldSymbols';
import { List } from '../../../../fields/List';
-import { RichTextField } from '../../../../fields/RichTextField';
import { Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types';
import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents } from '../../../../Utils';
-import { Docs, DocUtils } from '../../../documents/Documents';
-import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
+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 { MarqueeView } from '../../collections/collectionFreeForm';
+import { undoable, undoBatch } from '../../../util/UndoManager';
import { ViewBoxBaseComponent } from '../../DocComponent';
import { EditableView } from '../../EditableView';
import { Colors } from '../../global/globalEnums';
@@ -42,7 +40,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
// Idea: this boolean will determine whether to automatically show the video when this preselement is selected.
// @observable static showVideo: boolean = false;
@computed get indexInPres() {
- return DocListCast(this.presBox[StrCast(this.presBox.presFieldKey, 'data')]).indexOf(this.rootDoc);
+ return DocListCast(this.presBox?.[StrCast(this.presBox.presFieldKey, 'data')]).indexOf(this.rootDoc);
} // the index field is where this document is in the presBox display list (since this value is different for each presentation element, the value can't be stored on the layout template which is used by all display elements)
@computed get expandViewHeight() {
return 100;
@@ -50,25 +48,21 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
@computed get collapsedHeight() {
return 35;
} // the collapsed height changes depending on the state of the presBox. We could store this on the presentation element template if it's used by only one presentation - but if it's shared by multiple, then this value must be looked up
- @computed get presStatus() {
- return this.presBox.presStatus;
- }
@computed get selectedArray() {
return this.presBoxView?.selectedArray;
}
@computed get presBoxView() {
- const vpath = this.props.docViewPath();
- return vpath.length > 1 ? (vpath[vpath.length - 2].ComponentView as PresBox) : undefined;
+ return this.props.DocumentView?.()?.props.docViewPath().lastElement()?.ComponentView as PresBox;
}
@computed get presBox() {
- return this.props.ContainingCollectionDoc!;
+ return this.props.DocumentView?.().props.docViewPath().lastElement()?.rootDoc;
}
@computed get targetDoc() {
return Cast(this.rootDoc.presentationTargetDoc, Doc, null) || this.rootDoc;
}
componentDidMount() {
- this.layoutDoc.hideLinkButton = true;
+ this.layoutDoc.layout_hideLinkButton = true;
this._heightDisposer = reaction(
() => ({ expand: this.rootDoc.presExpandInlineButton, height: this.collapsedHeight }),
({ expand, height }) => (this.layoutDoc._height = height + (expand ? this.expandViewHeight : 0)),
@@ -103,7 +97,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
return !this.rootDoc.presExpandInlineButton || !this.targetDoc ? null : (
<div className="presItem-embedded" style={{ height: this.embedHeight(), width: '50%' }}>
<DocumentView
- Document={this.rootDoc}
+ Document={PresBox.targetRenderedDoc(this.rootDoc)}
DataDoc={undefined} //this.targetDoc[DataSym] !== this.targetDoc && this.targetDoc[DataSym]}
PanelWidth={this.embedWidth}
PanelHeight={this.embedHeight}
@@ -113,17 +107,15 @@ 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}
- ContainingCollectionView={undefined}
- ContainingCollectionDoc={undefined}
rootSelected={returnTrue}
addDocument={returnFalse}
removeDocument={returnFalse}
fitContentsToBox={returnTrue}
moveDocument={this.props.moveDocument!}
- focus={DocUtils.DefaultFocus}
+ focus={emptyFunction}
whenChildContentsActiveChanged={returnFalse}
addDocTab={returnFalse}
pinToPres={returnFalse}
@@ -142,7 +134,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
onClick={e => {
e.stopPropagation();
e.preventDefault();
- this.presBoxView?.modifierSelect(doc, this._itemRef.current!, this._dragRef.current!, !e.shiftKey && !e.ctrlKey && !e.metaKey, e.ctrlKey || e.metaKey, e.shiftKey);
+ this.presBoxView?.modifierSelect(doc, this._itemRef.current!, this._dragRef.current!, e.shiftKey || e.ctrlKey || e.metaKey, e.ctrlKey || e.metaKey, e.shiftKey);
this.presExpandDocumentClick();
}}>
<div className="presItem-groupNum">{`${ind + 1}.`}</div>
@@ -180,22 +172,14 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
const element = e.target as any;
e.stopPropagation();
e.preventDefault();
- if (element && !(e.ctrlKey || e.metaKey)) {
- if (this.selectedArray?.has(this.rootDoc)) {
- this.selectedArray.size === 1 && this.presBoxView?.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, false, false);
- setupMoveUpEvents(this, e, this.startDrag, emptyFunction, emptyFunction);
- } else {
- setupMoveUpEvents(
- this,
- e,
- (e: PointerEvent) => {
- this.presBoxView?.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, false, false);
- return this.startDrag(e);
- },
- emptyFunction,
- emptyFunction
- );
- }
+ if (element && !(e.ctrlKey || e.metaKey || e.button === 2)) {
+ this.presBoxView?.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, true, false);
+ setupMoveUpEvents(this, e, this.startDrag, emptyFunction, e => {
+ e.stopPropagation();
+ e.preventDefault();
+ this.presBoxView?.modifierSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, e.shiftKey || e.ctrlKey || e.metaKey, e.ctrlKey || e.metaKey, e.shiftKey);
+ this.presBoxView?.activeItem && this.showRecording(this.presBoxView?.activeItem);
+ });
}
};
@@ -208,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.props.ContainingCollectionDoc : 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);
}
@@ -228,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;
@@ -280,16 +268,15 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
};
- @undoBatch
- removeItem = action((e: React.MouseEvent) => {
+ removePresentationItem = undoable((e: React.MouseEvent) => {
e.stopPropagation();
- if (this.indexInPres < (this.presBoxView?.itemIndex || 0)) {
- this.presBox.itemIndex = (this.presBoxView?.itemIndex || 0) - 1;
+ if (this.presBox && this.indexInPres < (this.presBoxView?.itemIndex || 0)) {
+ 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
@@ -307,95 +294,52 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
*/
@undoBatch
@action
- updateCapturedContainerLayout = (targetDoc: Doc, activeItem: Doc) => {
+ updateCapturedContainerLayout = (presTargetDoc: Doc, activeItem: Doc) => {
+ const targetDoc = DocCast(presTargetDoc.annotationOn) ?? presTargetDoc;
activeItem.presX = NumCast(targetDoc.x);
activeItem.presY = NumCast(targetDoc.y);
- activeItem.presRot = NumCast(targetDoc.rotation);
+ activeItem.presRotation = NumCast(targetDoc.rotation);
activeItem.presWidth = NumCast(targetDoc.width);
activeItem.presHeight = NumCast(targetDoc.height);
+ activeItem.presPinLayout = true;
};
/**
* Method called for updating the view of the currently selected document
*
- * @param targetDoc
+ * @param presTargetDoc
* @param activeItem
*/
@undoBatch
@action
- updateCapturedViewContents = (targetDoc: Doc, activeItem: Doc) => {
- switch (targetDoc.type) {
- case DocumentType.PDF:
- case DocumentType.WEB:
- case DocumentType.RTF:
- const scroll = targetDoc._scrollTop;
- activeItem.presPinViewScroll = scroll;
- if (targetDoc.type === DocumentType.RTF) {
- activeItem.presData = targetDoc[Doc.LayoutFieldKey(targetDoc)] instanceof RichTextField ? (targetDoc[Doc.LayoutFieldKey(targetDoc)] as RichTextField)[Copy]() : targetDoc.text;
- }
- break;
- case DocumentType.INK:
- activeItem.presData = targetDoc[Doc.LayoutFieldKey(targetDoc)] instanceof InkField ? (targetDoc[Doc.LayoutFieldKey(targetDoc)] as InkField)[Copy]() : targetDoc.data;
- break;
- case DocumentType.VID:
- case DocumentType.AUDIO:
- activeItem.presStartTime = targetDoc._currentTimecode;
- break;
- case DocumentType.COMPARISON:
- const clipWidth = targetDoc._clipWidth;
- activeItem.presPinClipWidth = clipWidth;
- break;
- case DocumentType.COL:
- activeItem.presPinLayoutData = new List<string>(DocListCast(targetDoc[Doc.LayoutFieldKey(targetDoc)]).map(d => JSON.stringify({ id: d[Id], x: NumCast(d.x), y: NumCast(d.y), w: NumCast(d._width), h: NumCast(d._height) })));
- default:
- const bestView = DocumentManager.Instance.getFirstDocumentView(targetDoc);
- if (activeItem.presPinViewBounds && bestView) {
- const bounds = MarqueeView.CurViewBounds(targetDoc, bestView.props.PanelWidth(), bestView.props.PanelHeight());
- activeItem.presPinView = true;
- activeItem.presPinViewScale = NumCast(targetDoc._viewScale, 1);
- activeItem.presPinViewX = bounds.left + bounds.width / 2;
- activeItem.presPinViewY = bounds.top + bounds.height / 2;
- activeItem.presPinViewBounds = new List<number>([bounds.left, bounds.top, bounds.left + bounds.width, bounds.top + bounds.height]);
- } else {
- activeItem.presPinViewX = targetDoc._panX;
- activeItem.presPinViewY = targetDoc._panY;
- activeItem.presPinViewScale = targetDoc._viewScale;
- }
- }
+ updateCapturedViewContents = (presTargetDoc: Doc, activeItem: Doc) => {
+ const target = DocCast(presTargetDoc.annotationOn) ?? presTargetDoc;
+ PresBox.pinDocView(activeItem, { pinData: PresBox.pinDataTypes(target) }, target);
};
@computed get recordingIsInOverlay() {
- let isInOverlay = false;
- DocListCast(Doc.MyOverlayDocs.data).forEach(doc => {
- if (doc.slides === this.rootDoc) {
- isInOverlay = true;
- // return
- }
- });
- return isInOverlay;
+ return DocListCast(Doc.MyOverlayDocs.data).some(doc => doc.slides === this.rootDoc);
}
// a previously recorded video will have timecode defined
- static videoIsRecorded = (activeItem: Doc) => {
- const casted = Cast(activeItem.recording, Doc, null);
- return casted && 'currentTimecode' in casted;
+ static videoIsRecorded = (activeItem: Opt<Doc>) => {
+ const casted = Cast(activeItem?.recording, Doc, null);
+ return casted && 'layout_currentTimecode' in casted;
};
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 = () => {
// Remove every recording that already exists in overlay view
DocListCast(Doc.MyOverlayDocs.data).forEach(doc => {
- // if it's a recording video, don't remove from overlay (user can lose data)
- if (!PresElementBox.videoIsRecorded(DocCast(doc.slides))) return;
-
if (doc.slides !== null) {
- Doc.RemoveDocFromList(Doc.MyOverlayDocs, undefined, doc);
+ // if it's a recording video, don't remove from overlay (user can lose data)
+ if (PresElementBox.videoIsRecorded(DocCast(doc.slides))) {
+ Doc.RemFromMyOverlay(doc);
+ }
}
});
};
@@ -411,14 +355,11 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
@undoBatch
@action
showRecording = (activeItem: Doc, iconClick: boolean = false) => {
- // if (iconClick) PresElementBox.showVideo = true;
- // if (!PresElementBox.showVideo) return;
-
// remove the overlays on switch *IF* not opened from the specific icon
if (!iconClick) PresElementBox.removeEveryExistingRecordingInOverlay();
if (activeItem.recording) {
- Doc.AddDocToList(Doc.MyOverlayDocs, undefined, Cast(activeItem.recording, Doc, null));
+ Doc.AddToMyOverlay(DocCast(activeItem.recording));
}
};
@@ -430,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
@@ -440,11 +381,11 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
const recording = Docs.Create.WebCamDocument('', {
_width: 384,
_height: 216,
- hideDocumentButtonBar: true,
- hideDecorationTitle: true,
- hideOpenButton: true,
+ layout_hideDocumentButtonBar: true,
+ layout_hideDecorationTitle: true,
+ layout_hideOpenButton: true,
// hideDeleteButton: true,
- cloneFieldFilter: new List<string>(['system']),
+ cloneFieldFilter: new List<string>(['isSystem']),
});
// attach the recording to the slide, and attach the slide to the recording
@@ -452,66 +393,67 @@ 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);
}
};
@computed
get toolbarWidth(): number {
const presBoxDocView = DocumentManager.Instance.getDocumentView(this.presBox);
- let width: number = NumCast(this.presBox._width);
+ let width: number = NumCast(this.presBox?._width);
if (presBoxDocView) width = presBoxDocView.props.PanelWidth();
if (width === 0) width = 300;
return width;
}
@computed get presButtons() {
- const presBox: Doc = this.presBox; //presBox
- const presBoxColor: string = StrCast(presBox._backgroundColor);
+ const presBox = this.presBox; //presBox
+ const presBoxColor: string = StrCast(presBox?._backgroundColor);
const presColorBool: boolean = presBoxColor ? presBoxColor !== Colors.WHITE && presBoxColor !== 'transparent' : false;
const targetDoc: Doc = this.targetDoc;
const activeItem: Doc = this.rootDoc;
const items: JSX.Element[] = [];
- if (activeItem.presPinLayout) {
- items.push(
- <Tooltip key="slide" title={<div className="dash-tooltip">Update captured doc layout</div>}>
- <div className="slideButton" onClick={() => this.updateCapturedContainerLayout(targetDoc, activeItem)} style={{ fontWeight: 700, display: 'flex' }}>
- L
- </div>
- </Tooltip>
- );
- }
- if (activeItem.presPinData || activeItem.presPinView) {
- items.push(
- <Tooltip key="flex" title={<div className="dash-tooltip">Update captured doc content</div>}>
- <div className="slideButton" onClick={() => this.updateCapturedViewContents(targetDoc, activeItem)} style={{ fontWeight: 700, display: 'flex' }}>
- C
- </div>
- </Tooltip>
- );
- }
- if (!Doc.noviceMode) {
- items.push(
- <Tooltip key="slash" title={<div className="dash-tooltip">{this.recordingIsInOverlay ? 'Hide Recording' : `${PresElementBox.videoIsRecorded(activeItem) ? 'Show' : 'Start'} recording`}</div>}>
- <div className="slideButton" onClick={e => (this.recordingIsInOverlay ? this.hideRecording(e, true) : this.startRecording(e, activeItem))} style={{ fontWeight: 700 }}>
- <FontAwesomeIcon icon={`video${this.recordingIsInOverlay ? '-slash' : ''}`} onPointerDown={e => e.stopPropagation()} />
- </div>
- </Tooltip>
- );
- }
- if (this.indexInPres === 0) {
+ items.push(
+ <Tooltip key="slide" title={<div className="dash-tooltip">Update captured doc layout</div>}>
+ <div
+ className="slideButton"
+ onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => this.updateCapturedContainerLayout(targetDoc, activeItem), true)}
+ style={{ opacity: activeItem.presPinLayout ? 1 : 0.5, fontWeight: 700, display: 'flex' }}>
+ L
+ </div>
+ </Tooltip>
+ );
+ items.push(
+ <Tooltip key="flex" title={<div className="dash-tooltip">Update captured doc content</div>}>
+ <div
+ className="slideButton"
+ onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => this.updateCapturedViewContents(targetDoc, activeItem))}
+ style={{ opacity: activeItem.presPinData || activeItem.presPinView ? 1 : 0.5, fontWeight: 700, display: 'flex' }}>
+ C
+ </div>
+ </Tooltip>
+ );
+ items.push(
+ <Tooltip key="slash" title={<div className="dash-tooltip">{this.recordingIsInOverlay ? 'Hide Recording' : `${PresElementBox.videoIsRecorded(activeItem) ? 'Show' : 'Start'} recording`}</div>}>
+ <div className="slideButton" onClick={e => (this.recordingIsInOverlay ? this.hideRecording(e, true) : this.startRecording(e, activeItem))} style={{ fontWeight: 700 }}>
+ <FontAwesomeIcon icon={`video${this.recordingIsInOverlay ? '-slash' : ''}`} onPointerDown={e => e.stopPropagation()} />
+ </div>
+ </Tooltip>
+ );
+ if (this.indexInPres !== 0) {
items.push(
<Tooltip key="arrow" title={<div className="dash-tooltip">{activeItem.groupWithUp ? 'Ungroup' : 'Group with up'}</div>}>
<div
className="slideButton"
- onClick={() => (activeItem.groupWithUp = !activeItem.groupWithUp)}
+ onClick={() => (activeItem.groupWithUp = (NumCast(activeItem.groupWithUp) + 1) % 3)}
style={{
zIndex: 1000 - this.indexInPres,
fontWeight: 700,
backgroundColor: activeItem.groupWithUp ? (presColorBool ? presBoxColor : Colors.MEDIUM_BLUE) : undefined,
+ outline: NumCast(activeItem.groupWithUp) > 1 ? 'solid black 1px' : undefined,
height: activeItem.groupWithUp ? 53 : 18,
transform: activeItem.groupWithUp ? 'translate(0, -17px)' : undefined,
}}>
@@ -536,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>
@@ -546,10 +488,10 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
@computed get mainItem() {
const isSelected: boolean = this.selectedArray?.has(this.rootDoc) ? true : false;
- const isCurrent: boolean = this.presBox._itemIndex === this.indexInPres;
+ const isCurrent: boolean = this.presBox?._itemIndex === this.indexInPres;
const miniView: boolean = this.toolbarWidth <= 110;
- const presBox: Doc = this.presBox; //presBox
- const presBoxColor: string = StrCast(presBox._backgroundColor);
+ const presBox = this.presBox; //presBox
+ const presBoxColor: string = StrCast(presBox?._backgroundColor);
const presColorBool: boolean = presBoxColor ? presBoxColor !== Colors.WHITE && presBoxColor !== 'transparent' : false;
const activeItem: Doc = this.rootDoc;
@@ -566,15 +508,9 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
paddingTop: NumCast(this.layoutDoc._yPadding, this.props.yPadding),
paddingBottom: NumCast(this.layoutDoc._yPadding, this.props.yPadding),
}}
- onClick={e => {
- e.stopPropagation();
- e.preventDefault();
- this.presBoxView?.modifierSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, !e.shiftKey && !e.ctrlKey && !e.metaKey, e.ctrlKey || e.metaKey, e.shiftKey);
- this.showRecording(activeItem);
- }}
onDoubleClick={action(e => {
this.toggleProperties();
- this.presBoxView?.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, true);
+ this.presBoxView?.regularSelect(this.rootDoc, this._itemRef.current!, this._dragRef.current!, false);
})}
onPointerOver={this.onPointerOver}
onPointerLeave={this.onPointerLeave}
@@ -590,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
@@ -601,8 +537,17 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
width: `calc(100% ${this.rootDoc.presExpandInlineButton ? '- 50%' : ''} - ${this.presButtons.length * 22}px`,
cursor: isSelected ? 'text' : 'grab',
}}>
- <div>{`${this.indexInPres + 1}. `}</div>
- <EditableView ref={this._titleRef} editing={!isSelected ? false : undefined} contents={activeItem.title} overflow={'ellipsis'} GetValue={() => StrCast(activeItem.title)} SetValue={this.onSetValue} />
+ <div
+ className="presItem-number"
+ title="select without navigation"
+ onPointerDown={e => {
+ e.stopPropagation();
+ if (this._itemRef.current && this._dragRef.current) {
+ this.presBoxView?.modifierSelect(activeItem, this._itemRef.current, this._dragRef.current, true, false, false);
+ }
+ }}
+ onClick={e => e.stopPropagation()}>{`${this.indexInPres + 1}. `}</div>
+ <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/nodes/trails/PresEnums.ts b/src/client/views/nodes/trails/PresEnums.ts
index c6a222c3a..564829d54 100644
--- a/src/client/views/nodes/trails/PresEnums.ts
+++ b/src/client/views/nodes/trails/PresEnums.ts
@@ -1,6 +1,7 @@
export enum PresMovement {
Zoom = 'zoom',
Pan = 'pan',
+ Center = 'center',
Jump = 'jump',
None = 'none',
}
@@ -14,11 +15,15 @@ export enum PresEffect {
Bounce = 'Bounce',
Roll = 'Roll',
None = 'None',
+}
+
+export enum PresEffectDirection {
Left = 'Enter from left',
Right = 'Enter from right',
Center = 'Enter from center',
Top = 'Enter from Top',
Bottom = 'Enter from bottom',
+ None = 'None',
}
export enum PresStatus {
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx
index ee2ae10a7..5480600b0 100644
--- a/src/client/views/pdf/AnchorMenu.tsx
+++ b/src/client/views/pdf/AnchorMenu.tsx
@@ -10,6 +10,10 @@ import { SelectionManager } from '../../util/SelectionManager';
import { AntimodeMenu, AntimodeMenuProps } from '../AntimodeMenu';
import { LinkPopup } from '../linking/LinkPopup';
import { ButtonDropdown } from '../nodes/formattedText/RichTextMenu';
+import { gptAPICall, GPTCallType } from '../../apis/gpt/GPT';
+import { GPTPopup, GPTPopupMode } from './GPTPopup/GPTPopup';
+import { LightboxView } from '../LightboxView';
+import { EditorView } from 'prosemirror-view';
import './AnchorMenu.scss';
@observer
@@ -17,6 +21,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
static Instance: AnchorMenu;
private _disposer: IReactionDisposer | undefined;
+ private _disposer2: IReactionDisposer | undefined;
private _commentCont = React.createRef<HTMLButtonElement>();
private _palette = [
'rgba(208, 2, 27, 0.8)',
@@ -36,15 +41,61 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
'rgba(0, 0, 0, 0.8)',
];
- @observable private _keyValue: string = '';
- @observable private _valueValue: string = '';
- @observable private _added: boolean = false;
@observable private highlightColor: string = 'rgba(245, 230, 95, 0.616)';
@observable private _showLinkPopup: boolean = false;
- @observable public Highlighting: boolean = false;
@observable public Status: 'marquee' | 'annotation' | '' = '';
+ // GPT additions
+ @observable private GPTpopupText: string = '';
+ @observable private loadingGPT: boolean = false;
+ @observable private showGPTPopup: boolean = false;
+ @observable private GPTMode: GPTPopupMode = GPTPopupMode.SUMMARY;
+ @observable private selectedText: string = '';
+ @observable private editorView?: EditorView;
+ @observable private textDoc?: Doc;
+ @observable private highlightRange: number[] | undefined;
+ private selectionRange: number[] | undefined;
+
+ @action
+ setGPTPopupVis = (vis: boolean) => {
+ this.showGPTPopup = vis;
+ };
+ @action
+ setGPTMode = (mode: GPTPopupMode) => {
+ this.GPTMode = mode;
+ };
+
+ @action
+ setGPTPopupText = (txt: string) => {
+ this.GPTpopupText = txt;
+ };
+
+ @action
+ setLoading = (loading: boolean) => {
+ this.loadingGPT = loading;
+ };
+
+ @action
+ setHighlightRange(r: number[] | undefined) {
+ this.highlightRange = r;
+ }
+
+ @action
+ public setSelectedText = (txt: string) => {
+ this.selectedText = txt;
+ };
+
+ @action
+ public setEditorView = (editor: EditorView) => {
+ this.editorView = editor;
+ };
+
+ @action
+ public setTextDoc = (textDoc: Doc) => {
+ this.textDoc = textDoc;
+ };
+
public onMakeAnchor: () => Opt<Doc> = () => undefined; // Method to get anchor from text search
public OnCrop: (e: PointerEvent) => void = unimplementedFunction;
@@ -52,13 +103,13 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
public OnAudio: (e: PointerEvent) => void = unimplementedFunction;
public StartDrag: (e: PointerEvent, ele: HTMLElement) => void = unimplementedFunction;
public StartCropDrag: (e: PointerEvent, ele: HTMLElement) => void = unimplementedFunction;
- public Highlight: (color: string, isPushpin: boolean) => Opt<Doc> = (color: string, isPushpin: boolean) => undefined;
- public GetAnchor: (savedAnnotations?: ObservableMap<number, HTMLDivElement[]>) => Opt<Doc> = () => undefined;
+ public Highlight: (color: string, isTargetToggler: boolean, savedAnnotations?: ObservableMap<number, HTMLDivElement[]>, addAsAnnotation?: boolean) => Opt<Doc> = (color: string, isTargetToggler: boolean) => undefined;
+ public GetAnchor: (savedAnnotations: Opt<ObservableMap<number, HTMLDivElement[]>>, addAsAnnotation: boolean) => Opt<Doc> = (savedAnnotations: Opt<ObservableMap<number, HTMLDivElement[]>>, addAsAnnotation: boolean) => undefined;
public Delete: () => void = unimplementedFunction;
- public AddTag: (key: string, value: string) => boolean = returnFalse;
public PinToPres: () => void = unimplementedFunction;
- public MakePushpin: () => void = unimplementedFunction;
- public IsPushpin: () => boolean = returnFalse;
+ public MakeTargetToggle: () => void = unimplementedFunction;
+ public ShowTargetTrail: () => void = unimplementedFunction;
+ public IsTargetToggler: () => boolean = returnFalse;
public get Active() {
return this._left > 0;
}
@@ -70,16 +121,102 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
AnchorMenu.Instance._canFade = false;
}
+ componentWillUnmount() {
+ this._disposer?.();
+ this._disposer2?.();
+ }
+
componentDidMount() {
+ this._disposer2 = reaction(
+ () => this._opacity,
+ opacity => {
+ if (!opacity) {
+ this._showLinkPopup = false;
+ this.setGPTPopupVis(false);
+ this.setGPTPopupText('');
+ }
+ },
+ { fireImmediately: true }
+ );
this._disposer = reaction(
- () => SelectionManager.Views(),
+ () => SelectionManager.Views().slice(),
selected => {
this._showLinkPopup = false;
+ this.setGPTPopupVis(false);
+ this.setGPTPopupText('');
AnchorMenu.Instance.fadeOut(true);
}
);
}
+ /**
+ * Invokes the API with the selected text and stores it in the summarized text.
+ * @param e pointer down event
+ */
+ gptSummarize = async (e: React.PointerEvent) => {
+ this.setHighlightRange(undefined);
+ this.setGPTPopupVis(true);
+ this.setGPTMode(GPTPopupMode.SUMMARY);
+ this.setLoading(true);
+
+ try {
+ const res = await gptAPICall(this.selectedText, GPTCallType.SUMMARY);
+ if (res) {
+ this.setGPTPopupText(res);
+ } else {
+ this.setGPTPopupText('Something went wrong.');
+ }
+ } catch (err) {
+ console.error(err);
+ }
+
+ this.setLoading(false);
+ };
+
+ /**
+ * Makes a GPT call to edit selected text.
+ * @returns nothing
+ */
+ gptEdit = async () => {
+ if (!this.editorView) return;
+ this.setHighlightRange(undefined);
+ const state = this.editorView.state;
+ const sel = state.selection;
+ const fullText = state.doc.textBetween(0, this.editorView.state.doc.content.size, ' \n');
+ const selectedText = state.doc.textBetween(sel.from, sel.to);
+
+ this.setGPTPopupVis(true);
+ this.setGPTMode(GPTPopupMode.EDIT);
+ this.setLoading(true);
+
+ try {
+ let res = await gptAPICall(selectedText, GPTCallType.EDIT);
+ // let res = await this.mockGPTCall();
+ if (!res) return;
+ res = res.trim();
+ const resultText = fullText.slice(0, sel.from - 1) + res + fullText.slice(sel.to - 1);
+
+ if (res) {
+ this.setGPTPopupText(resultText);
+ this.setHighlightRange([sel.from - 1, sel.from - 1 + res.length]);
+ } else {
+ this.setGPTPopupText('Something went wrong.');
+ }
+ } catch (err) {
+ console.error(err);
+ }
+
+ this.setLoading(false);
+ };
+
+ /**
+ * Replaces text suggestions from GPT.
+ */
+ replaceText = (replacement: string) => {
+ if (!this.editorView || !this.textDoc) return;
+ this.textDoc.text = replacement;
+ };
+
pointerDown = (e: React.PointerEvent) => {
setupMoveUpEvents(
this,
@@ -112,9 +249,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
@action
highlightClicked = (e: React.MouseEvent) => {
- if (!this.Highlight(this.highlightColor, false) && this.Pinned) {
- this.Highlighting = !this.Highlighting;
- }
+ this.Highlight(this.highlightColor, false, undefined, true);
AnchorMenu.Instance.fadeOut(true);
};
@@ -129,7 +264,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
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: this.Highlighting ? '' : 'rotate(-45deg)' }} />
+ <FontAwesomeIcon icon="highlighter" size="lg" style={{ transition: 'transform 0.1s', transform: 'rotate(-45deg)' }} />
<div className="color-preview" style={{ backgroundColor: this.highlightColor }}></div>
</div>
</button>
@@ -174,83 +309,116 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
this.highlightColor = Utils.colorString(col);
};
- @action keyChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
- this._keyValue = e.currentTarget.value;
- };
- @action valueChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
- this._valueValue = e.currentTarget.value;
+ /**
+ * Returns whether the selected text can be summarized. The goal is to have
+ * all selected text available to summarize but its only supported for pdf and web ATM.
+ * @returns Whether the GPT icon for summarization should appear
+ */
+ canSummarize = (): boolean => {
+ const docs = SelectionManager.Docs();
+ if (docs.length > 0) {
+ return docs.some(doc => doc.type === 'pdf' || doc.type === 'web');
+ }
+ return false;
};
- @action addTag = (e: React.PointerEvent) => {
- if (this._keyValue.length > 0 && this._valueValue.length > 0) {
- this._added = this.AddTag(this._keyValue, this._valueValue);
- setTimeout(
- action(() => (this._added = false)),
- 1000
- );
+
+ /**
+ * Returns whether the selected text can be edited.
+ * @returns Whether the GPT icon for summarization should appear
+ */
+ canEdit = (): boolean => {
+ const docs = SelectionManager.Docs();
+ if (docs.length > 0) {
+ return docs.some(doc => doc.type === 'rtf');
}
+ return false;
};
render() {
const buttons =
- 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>,
- AnchorMenu.Instance.OnAudio === unimplementedFunction ? (
- <></>
- ) : (
- <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>
- ),
- //NOTE: link popup is currently in progress
- <Tooltip key="link" title={<div className="dash-tooltip">{'Find document to link to selected text'}</div>}>
- <button className="antimodeMenu-button link" onPointerDown={this.toggleLinkPopup} style={{}}>
- <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} linkFrom={this.onMakeAnchor} />,
- AnchorMenu.Instance.StartCropDrag === unimplementedFunction ? (
- <></>
- ) : (
- <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>
- ),
- ]
- : [
- <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="pushpin" title={<div className="dash-tooltip">{'toggle pushpin behavior'}</div>}>
- <button className="antimodeMenu-button" style={{ color: this.IsPushpin() ? 'black' : 'white', backgroundColor: this.IsPushpin() ? 'white' : 'black' }} onPointerDown={this.MakePushpin}>
- <FontAwesomeIcon icon="thumbtack" size="lg" />
- </button>
- </Tooltip>,
- // <div key="7" className="anchorMenu-addTag" >
- // <input onChange={this.keyChanged} placeholder="Key" style={{ gridColumn: 1 }} />
- // <input onChange={this.valueChanged} placeholder="Value" style={{ gridColumn: 3 }} />
- // </div>,
- // <button key="8" className="antimodeMenu-button" title={`Add tag: ${this._keyValue} with value: ${this._valueValue}`} onPointerDown={this.addTag}>
- // <FontAwesomeIcon style={{ transition: "all .2s" }} color={this._added ? "#42f560" : "white"} icon="check" size="lg" /></button>,
- ];
+ 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>
+ {/* 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>
+ )}
+ <GPTPopup
+ key="gptpopup"
+ visible={this.showGPTPopup}
+ text={this.GPTpopupText}
+ highlightRange={this.highlightRange}
+ loading={this.loadingGPT}
+ callSummaryApi={this.gptSummarize}
+ callEditApi={this.gptEdit}
+ replaceText={this.replaceText}
+ 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>
+ )}
+ {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>
+ )}
+ <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} />,
+ {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>
+ )}
+ </>
+ ) : (
+ <>
+ <Tooltip key="trash" title={<div className="dash-tooltip">Remove Link Anchor</div>}>
+ <button className="antimodeMenu-button" style={{ display: this.Delete === returnFalse ? 'none' : undefined }} 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" style={{ display: this.PinToPres === returnFalse ? 'none' : undefined }} 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" style={{ display: this.ShowTargetTrail === returnFalse ? 'none' : undefined }} 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={{ display: this.IsTargetToggler === returnFalse ? 'none' : undefined, color: this.IsTargetToggler() ? 'black' : 'white', backgroundColor: this.IsTargetToggler() ? 'white' : 'black' }}
+ onPointerDown={this.MakeTargetToggle}>
+ <FontAwesomeIcon icon="thumbtack" size="lg" />
+ </button>
+ </Tooltip>
+ </>
+ );
return this.getElement(buttons);
}
diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx
index ee418a02f..db6b1f011 100644
--- a/src/client/views/pdf/Annotation.tsx
+++ b/src/client/views/pdf/Annotation.tsx
@@ -1,12 +1,13 @@
import React = require('react');
-import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
+import { action, computed } from 'mobx';
import { observer } from 'mobx-react';
import { Doc, DocListCast, Opt } from '../../../fields/Doc';
import { Id } from '../../../fields/FieldSymbols';
import { List } from '../../../fields/List';
-import { BoolCast, Cast, NumCast, StrCast } from '../../../fields/Types';
+import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../fields/Types';
import { LinkFollower } from '../../util/LinkFollower';
import { undoBatch } from '../../util/UndoManager';
+import { OpenWhere } from '../nodes/DocumentView';
import { FieldViewProps } from '../nodes/FieldView';
import { AnchorMenu } from './AnchorMenu';
import './Annotation.scss';
@@ -22,7 +23,7 @@ interface IAnnotationProps extends FieldViewProps {
export class Annotation extends React.Component<IAnnotationProps> {
render() {
return (
- <div>
+ <div style={{ display: this.props.anno.textCopied && !Doc.isBrushedHighlightedDegree(this.props.anno) ? 'none' : undefined }}>
{DocListCast(this.props.anno.textInlineAnnotations).map(a => (
<RegionAnnotation pointerEvents={this.props.pointerEvents} {...this.props} document={a} key={a[Id]} />
))}
@@ -52,12 +53,20 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
};
@undoBatch
- pinToPres = () => this.props.pinToPres(this.annoTextRegion);
+ pinToPres = () => this.props.pinToPres(this.annoTextRegion, {});
@undoBatch
- makePushpin = () => (this.annoTextRegion.isPushpin = !this.annoTextRegion.isPushpin);
+ makeTargretToggle = () => (this.annoTextRegion.followLinkToggle = !this.annoTextRegion.followLinkToggle);
- isPushpin = () => BoolCast(this.annoTextRegion.isPushpin);
+ isTargetToggler = () => BoolCast(this.annoTextRegion.followLinkToggle);
+ @undoBatch
+ showTargetTrail = (anchor: Doc) => {
+ const trail = DocCast(anchor.presTrail);
+ if (trail) {
+ Doc.ActivePresentation = trail;
+ this.props.addDocTab(trail, OpenWhere.replaceRight);
+ }
+ };
@action
onPointerDown = (e: React.PointerEvent) => {
@@ -65,24 +74,18 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
AnchorMenu.Instance.Status = 'annotation';
AnchorMenu.Instance.Delete = this.deleteAnnotation.bind(this);
AnchorMenu.Instance.Pinned = false;
- AnchorMenu.Instance.AddTag = this.addTag.bind(this);
AnchorMenu.Instance.PinToPres = this.pinToPres;
- AnchorMenu.Instance.MakePushpin = this.makePushpin;
- AnchorMenu.Instance.IsPushpin = this.isPushpin;
+ AnchorMenu.Instance.MakeTargetToggle = this.makeTargretToggle;
+ AnchorMenu.Instance.IsTargetToggler = this.isTargetToggler;
+ AnchorMenu.Instance.ShowTargetTrail = () => this.showTargetTrail(this.annoTextRegion);
AnchorMenu.Instance.jumpTo(e.clientX, e.clientY, true);
e.stopPropagation();
} else if (e.button === 0) {
e.stopPropagation();
- LinkFollower.FollowLink(undefined, this.annoTextRegion, this.props, false);
+ LinkFollower.FollowLink(undefined, this.annoTextRegion, false);
}
};
- addTag = (key: string, value: string): boolean => {
- const valNum = parseInt(value);
- this.annoTextRegion[key] = isNaN(valNum) ? value : valNum;
- return true;
- };
-
render() {
const brushed = this.annoTextRegion && Doc.isBrushedHighlightedDegree(this.annoTextRegion);
return (
diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.scss b/src/client/views/pdf/GPTPopup/GPTPopup.scss
new file mode 100644
index 000000000..44413ede7
--- /dev/null
+++ b/src/client/views/pdf/GPTPopup/GPTPopup.scss
@@ -0,0 +1,132 @@
+$textgrey: #707070;
+$lighttextgrey: #a3a3a3;
+$greyborder: #d3d3d3;
+$lightgrey: #ececec;
+$button: #5b97ff;
+$highlightedText: #82e0ff;
+
+.summary-box {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ background-color: #ffffff;
+ box-shadow: 0 2px 5px #7474748d;
+ color: $textgrey;
+ position: fixed;
+ bottom: 10px;
+ right: 10px;
+ width: 250px;
+ min-height: 200px;
+ border-radius: 15px;
+ padding: 15px;
+ padding-bottom: 0;
+ z-index: 999;
+
+ .summary-heading {
+ display: flex;
+ align-items: center;
+ border-bottom: 1px solid $greyborder;
+ padding-bottom: 5px;
+
+ .summary-text {
+ font-size: 12px;
+ font-weight: 500;
+ }
+ }
+
+ label {
+ color: $textgrey;
+ font-size: 12px;
+ font-weight: 400;
+ letter-spacing: 1px;
+ margin: 0;
+ padding-right: 5px;
+ }
+
+ a {
+ cursor: pointer;
+ }
+
+ .content-wrapper {
+ padding-top: 10px;
+ min-height: 50px;
+ max-height: 150px;
+ overflow-y: auto;
+ }
+
+ .btns-wrapper {
+ height: 50px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ .summarizing {
+ display: flex;
+ align-items: center;
+ }
+ }
+
+ button {
+ font-size: 9px;
+ padding: 10px;
+ color: #ffffff;
+ background-color: $button;
+ border-radius: 5px;
+ }
+
+ .text-btn {
+ &:hover {
+ background-color: $button;
+ }
+ }
+
+ .btn-secondary {
+ font-size: 8px;
+ padding: 10px 5px;
+ background-color: $lightgrey;
+ color: $textgrey;
+ &:hover {
+ background-color: $lightgrey;
+ }
+ }
+
+ .icon-btn {
+ background-color: #ffffff;
+ padding: 10px;
+ border-radius: 50%;
+ color: $button;
+ border: 1px solid $button;
+ }
+
+ .ai-warning {
+ padding: 10px 0;
+ font-size: 10px;
+ color: $lighttextgrey;
+ border-top: 1px solid $greyborder;
+ }
+
+ .highlighted-text {
+ background-color: $highlightedText;
+ }
+}
+
+// Typist CSS
+.Typist .Cursor {
+ display: inline-block;
+}
+.Typist .Cursor--blinking {
+ opacity: 1;
+ animation: blink 1s linear infinite;
+}
+
+@keyframes blink {
+ 0% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0;
+ }
+ 100% {
+ opacity: 1;
+ }
+}
diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
new file mode 100644
index 000000000..8bd060d4f
--- /dev/null
+++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
@@ -0,0 +1,188 @@
+import React = require('react');
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { action, observable } from 'mobx';
+import { observer } from 'mobx-react';
+import ReactLoading from 'react-loading';
+import Typist from 'react-typist';
+import { Doc } from '../../../../fields/Doc';
+import { Docs } from '../../../documents/Documents';
+import './GPTPopup.scss';
+
+export enum GPTPopupMode {
+ SUMMARY,
+ EDIT,
+}
+
+interface GPTPopupProps {
+ visible: boolean;
+ text: string;
+ loading: boolean;
+ mode: GPTPopupMode;
+ callSummaryApi: (e: React.PointerEvent) => Promise<void>;
+ callEditApi: (e: React.PointerEvent) => Promise<void>;
+ replaceText: (replacement: string) => void;
+ highlightRange?: number[];
+}
+
+@observer
+export class GPTPopup extends React.Component<GPTPopupProps> {
+ static Instance: GPTPopup;
+
+ @observable
+ private done: boolean = false;
+ @observable
+ private sidebarId: string = '';
+
+ @action
+ public setDone = (done: boolean) => {
+ this.done = done;
+ };
+ @action
+ public setSidebarId = (id: string) => {
+ this.sidebarId = id;
+ };
+
+ public addDoc: (doc: Doc | Doc[], sidebarKey?: string | undefined) => boolean = () => false;
+
+ /**
+ * Transfers the summarization text to a sidebar annotation text document.
+ */
+ private transferToText = () => {
+ const newDoc = Docs.Create.TextDocument(this.props.text.trim(), {
+ _width: 200,
+ _height: 50,
+ _layout_fitWidth: true,
+ _layout_autoHeight: true,
+ });
+ this.addDoc(newDoc, this.sidebarId);
+ };
+
+ constructor(props: GPTPopupProps) {
+ super(props);
+ GPTPopup.Instance = this;
+ }
+
+ componentDidUpdate = () => {
+ if (this.props.loading) {
+ this.setDone(false);
+ }
+ };
+
+ summaryBox = () => (
+ <>
+ <div>
+ {this.heading('SUMMARY')}
+ <div className="content-wrapper">
+ {!this.props.loading &&
+ (!this.done ? (
+ <Typist
+ key={this.props.text}
+ avgTypingDelay={15}
+ cursor={{ hideWhenDone: true }}
+ onTypingDone={() => {
+ setTimeout(() => {
+ this.setDone(true);
+ }, 500);
+ }}>
+ {this.props.text}
+ </Typist>
+ ) : (
+ this.props.text
+ ))}
+ </div>
+ </div>
+ {!this.props.loading && (
+ <div className="btns-wrapper">
+ {this.done ? (
+ <>
+ <button className="icon-btn" onPointerDown={e => this.props.callSummaryApi(e)}>
+ <FontAwesomeIcon icon="redo-alt" size="lg" />
+ </button>
+ <button
+ className="text-btn"
+ onClick={e => {
+ this.transferToText();
+ }}>
+ Transfer to Text
+ </button>
+ </>
+ ) : (
+ <div className="summarizing">
+ <span>Summarizing</span>
+ <ReactLoading type="bubbles" color="#bcbcbc" width={20} height={20} />
+ <button
+ className="btn-secondary"
+ onClick={e => {
+ this.setDone(true);
+ }}>
+ Stop Animation
+ </button>
+ </div>
+ )}
+ </div>
+ )}
+ </>
+ );
+
+ editBox = () => {
+ const hr = this.props.highlightRange;
+ return (
+ <>
+ <div>
+ {this.heading('TEXT EDIT SUGGESTIONS')}
+ <div className="content-wrapper">
+ {hr && (
+ <div>
+ {this.props.text.slice(0, hr[0])} <span className="highlighted-text">{this.props.text.slice(hr[0], hr[1])}</span> {this.props.text.slice(hr[1])}
+ </div>
+ )}
+ </div>
+ </div>
+ {hr && !this.props.loading && (
+ <>
+ <div className="btns-wrapper">
+ <>
+ <button className="icon-btn" onPointerDown={e => this.props.callEditApi(e)}>
+ <FontAwesomeIcon icon="redo-alt" size="lg" />
+ </button>
+ <button
+ className="text-btn"
+ onClick={e => {
+ this.props.replaceText(this.props.text);
+ }}>
+ Replace Text
+ </button>
+ </>
+ </div>
+ {this.aiWarning()}
+ </>
+ )}
+ </>
+ );
+ };
+
+ aiWarning = () =>
+ this.done ? (
+ <div className="ai-warning">
+ <FontAwesomeIcon icon="exclamation-circle" size="sm" style={{ paddingRight: '5px' }} />
+ AI generated responses can contain inaccurate or misleading content.
+ </div>
+ ) : (
+ <></>
+ );
+
+ heading = (headingText: string) => (
+ <div className="summary-heading">
+ <label className="summary-text">{headingText}</label>
+ {this.props.loading && <ReactLoading type="spin" color="#bcbcbc" width={14} height={14} />}
+ </div>
+ );
+
+ render() {
+ return (
+ <div className="summary-box" style={{ display: this.props.visible ? 'flex' : 'none' }}>
+ {this.props.mode === GPTPopupMode.SUMMARY ? this.summaryBox() : this.editBox()}
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index abc7336bd..8f6b8cd41 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -2,12 +2,13 @@ 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';
import { TraceMobx } from '../../../fields/util';
-import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, OmitKeys, smoothScroll, Utils } from '../../../Utils';
+import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, returnAll, returnFalse, returnNone, returnTrue, returnZero, smoothScroll, Utils } from '../../../Utils';
import { DocUtils } from '../../documents/Documents';
import { DragManager } from '../../util/DragManager';
import { SelectionManager } from '../../util/SelectionManager';
@@ -16,7 +17,7 @@ import { SnappingManager } from '../../util/SnappingManager';
import { MarqueeOptionsMenu } from '../collections/collectionFreeForm';
import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
import { MarqueeAnnotator } from '../MarqueeAnnotator';
-import { DocumentViewProps } from '../nodes/DocumentView';
+import { DocFocusOptions, DocumentViewProps } from '../nodes/DocumentView';
import { FieldViewProps } from '../nodes/FieldView';
import { LinkDocPreview } from '../nodes/LinkDocPreview';
import { StyleProp } from '../StyleProvider';
@@ -24,13 +25,14 @@ import { AnchorMenu } from './AnchorMenu';
import { Annotation } from './Annotation';
import './PDFViewer.scss';
import React = require('react');
+import { GPTPopup } from './GPTPopup/GPTPopup';
const PDFJSViewer = require('pdfjs-dist/web/pdf_viewer');
const pdfjsLib = require('pdfjs-dist');
const _global = (window /* browser */ || global) /* node */ as any;
//pdfjsLib.GlobalWorkerOptions.workerSrc = `/assets/pdf.worker.js`;
// The workerSrc property shall be specified.
-pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://unpkg.com/pdfjs-dist@2.14.305/build/pdf.worker.js';
+pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://unpkg.com/pdfjs-dist@2.16.105/build/pdf.worker.js';
interface IViewerProps extends FieldViewProps {
Document: Doc;
@@ -40,9 +42,10 @@ interface IViewerProps extends FieldViewProps {
fieldKey: string;
pdf: Pdfjs.PDFDocumentProxy;
url: string;
+ sidebarAddDoc: (doc: Doc | Doc[], sidebarKey?: string | undefined) => boolean;
loaded?: (nw: number, nh: number, np: number) => void;
setPdfViewer: (view: PDFViewer) => void;
- anchorMenuClick?: () => undefined | ((anchor: Doc) => void);
+ anchorMenuClick?: () => undefined | ((anchor: Doc, summarize?: boolean) => void);
crop: (region: Doc | undefined, addCrop?: boolean) => Doc | undefined;
}
@@ -68,23 +71,27 @@ export class PDFViewer extends React.Component<IViewerProps> {
private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
private _disposers: { [name: string]: IReactionDisposer } = {};
private _viewer: React.RefObject<HTMLDivElement> = React.createRef();
- public _getAnchor: (savedAnnotations?: ObservableMap<number, HTMLDivElement[]>) => Opt<Doc> = () => undefined;
_mainCont: React.RefObject<HTMLDivElement> = React.createRef();
private _selectionText: string = '';
+ private _selectionContent: DocumentFragment | undefined;
private _downX: number = 0;
private _downY: number = 0;
private _lastSearch = false;
private _viewerIsSetup = false;
private _ignoreScroll = false;
- private _initialScroll: Opt<number>;
+ private _initialScroll: { loc: Opt<number>; easeFunc: 'linear' | 'ease' | undefined } | undefined;
private _forcedScroll = true;
+ get _getAnchor() {
+ return AnchorMenu.Instance?.GetAnchor;
+ }
selectionText = () => this._selectionText;
+ selectionContent = () => this._selectionContent;
@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(), undefined);
+ 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);
@@ -95,12 +102,12 @@ export class PDFViewer extends React.Component<IViewerProps> {
this.setupPdfJsViewer();
this._mainCont.current?.addEventListener('scroll', e => ((e.target as any).scrollLeft = 0));
- this._disposers.autoHeight = reaction(
- () => this.props.layoutDoc._autoHeight,
- autoHeight => {
- if (autoHeight) {
- this.props.layoutDoc._nativeHeight = NumCast(this.props.Document[this.props.fieldKey + '-nativeHeight']);
- this.props.setHeight?.(NumCast(this.props.Document[this.props.fieldKey + '-nativeHeight']) * (this.props.NativeDimScaling?.() || 1));
+ this._disposers.layout_autoHeight = reaction(
+ () => this.props.layoutDoc._layout_autoHeight,
+ layout_autoHeight => {
+ if (layout_autoHeight) {
+ this.props.layoutDoc._nativeHeight = NumCast(this.props.Document[this.props.fieldKey + '_nativeHeight']);
+ this.props.setHeight?.(NumCast(this.props.Document[this.props.fieldKey + '_nativeHeight']) * (this.props.NativeDimScaling?.() || 1));
}
}
);
@@ -117,7 +124,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
{ fireImmediately: true }
);
this._disposers.curPage = reaction(
- () => Cast(this.props.Document._curPage, 'number', null),
+ () => Cast(this.props.Document._layout_curPage, 'number', null),
page => page !== undefined && page !== this._pdfViewer?.currentPageNumber && this.gotoPage(page),
{ fireImmediately: true }
);
@@ -131,10 +138,17 @@ export class PDFViewer extends React.Component<IViewerProps> {
copy = (e: ClipboardEvent) => {
if (this.props.isContentActive() && e.clipboardData) {
e.clipboardData.setData('text/plain', this._selectionText);
+ const anchor = this._getAnchor(undefined, false);
+ if (anchor) {
+ anchor.textCopied = true;
+ e.clipboardData.setData('dash/pdfAnchor', anchor[Id]);
+ }
e.preventDefault();
}
};
+ @observable _scrollHeight = 0;
+
@action
initialLoad = async () => {
if (this._pageSizes.length === 0) {
@@ -155,25 +169,27 @@ export class PDFViewer extends React.Component<IViewerProps> {
)
)
);
- this.props.Document.scrollHeight = (this._pageSizes.reduce((size, page) => size + page.height, 0) * 96) / 72;
}
+ runInAction(() => (this._scrollHeight = (this._pageSizes.reduce((size, page) => size + page.height, 0) * 96) / 72));
};
+ _scrollStopper: undefined | (() => void);
+
// scrolls to focus on a nested annotation document. if this is part a link preview then it will jump to the scroll location,
// otherwise it will scroll smoothly.
- scrollFocus = (doc: Doc, smooth: boolean) => {
+ scrollFocus = (doc: Doc, scrollTop: number, options: DocFocusOptions) => {
const mainCont = this._mainCont.current;
let focusSpeed: Opt<number>;
if (doc !== this.props.rootDoc && mainCont) {
const windowHeight = this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1);
- const scrollTo = doc.unrendered ? NumCast(doc.y) : Utils.scrollIntoView(NumCast(doc.y), doc[HeightSym](), NumCast(this.props.layoutDoc._scrollTop), windowHeight, 0.1 * windowHeight, NumCast(this.props.Document.scrollHeight));
- if (scrollTo !== undefined && scrollTo !== this.props.layoutDoc._scrollTop) {
- if (!this._pdfViewer) this._initialScroll = scrollTo;
- else if (smooth) smoothScroll((focusSpeed = NumCast(doc.focusSpeed, 500)), mainCont, scrollTo);
+ 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);
else this._mainCont.current?.scrollTo({ top: Math.abs(scrollTo || 0) });
}
} else {
- this._initialScroll = NumCast(this.props.layoutDoc._scrollTop);
+ this._initialScroll = { loc: NumCast(this.props.layoutDoc._layout_scrollTop), easeFunc: options.easeFunc };
}
return focusSpeed;
};
@@ -193,17 +209,22 @@ export class PDFViewer extends React.Component<IViewerProps> {
pagesinit = () => {
if (this._pdfViewer._setDocumentViewerElement?.offsetParent) {
- runInAction(() => (this._pdfViewer.currentScaleValue = this.props.layoutDoc._viewScale = 1));
- this.gotoPage(NumCast(this.props.Document._curPage, 1));
+ runInAction(() => (this._pdfViewer.currentScaleValue = this.props.layoutDoc._freeform_scale = 1));
+ this.gotoPage(NumCast(this.props.Document._layout_curPage, 1));
}
document.removeEventListener('pagesinit', this.pagesinit);
- var quickScroll: string | undefined = this._initialScroll ? this._initialScroll.toString() : '';
+ var quickScroll: { loc?: string; easeFunc?: 'ease' | 'linear' } | undefined = { loc: this._initialScroll ? this._initialScroll.loc?.toString() : '', easeFunc: this._initialScroll ? this._initialScroll.easeFunc : undefined };
+ this._disposers.scale = reaction(
+ () => NumCast(this.props.layoutDoc._freeform_scale, 1),
+ scale => (this._pdfViewer.currentScaleValue = scale),
+ { fireImmediately: true }
+ );
this._disposers.scroll = reaction(
- () => Math.abs(NumCast(this.props.Document._scrollTop)),
+ () => Math.abs(NumCast(this.props.Document._layout_scrollTop)),
pos => {
if (!this._ignoreScroll) {
this._showWaiting && this.setupPdfJsViewer();
- const viewTrans = quickScroll ?? StrCast(this.props.Document._viewTransition);
+ const viewTrans = quickScroll?.loc ?? StrCast(this.props.Document._viewTransition);
const durationMiliStr = viewTrans.match(/([0-9]*)ms/);
const durationSecStr = viewTrans.match(/([0-9.]*)s/);
const duration = durationMiliStr ? Number(durationMiliStr[1]) : durationSecStr ? Number(durationSecStr[1]) * 1000 : 0;
@@ -211,7 +232,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
if (duration) {
setTimeout(
() => {
- this._mainCont.current && smoothScroll(duration, this._mainCont.current, pos);
+ this._mainCont.current && (this._scrollStopper = smoothScroll(duration, this._mainCont.current, pos, this._initialScroll?.easeFunc ?? 'ease', this._scrollStopper));
setTimeout(() => (this._forcedScroll = false), duration);
},
this._mainCont.current ? 0 : 250
@@ -226,7 +247,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
);
quickScroll = undefined;
if (this._initialScroll !== undefined && this._mainCont.current) {
- this._mainCont.current?.scrollTo({ top: Math.abs(this._initialScroll || 0) });
+ this._mainCont.current?.scrollTo({ top: Math.abs(this._initialScroll?.loc || 0) });
this._initialScroll = undefined;
}
};
@@ -285,7 +306,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
@action
scrollToAnnotation = (scrollToAnnotation: Doc) => {
if (scrollToAnnotation) {
- this.scrollFocus(scrollToAnnotation, true);
+ this.scrollFocus(scrollToAnnotation, NumCast(scrollToAnnotation.y), { zoomTime: 500 });
Doc.linkFollowHighlight(scrollToAnnotation);
}
};
@@ -296,12 +317,12 @@ export class PDFViewer extends React.Component<IViewerProps> {
if (this._mainCont.current && !this._forcedScroll) {
this._ignoreScroll = true; // the pdf scrolled, so we need to tell the Doc to scroll but we don't want the doc to then try to set the PDF scroll pos (which would interfere with the smooth scroll animation)
if (!LinkDocPreview.LinkInfo) {
- this.props.layoutDoc._scrollTop = this._mainCont.current.scrollTop;
+ this.props.layoutDoc._layout_scrollTop = this._mainCont.current.scrollTop;
}
this._ignoreScroll = false;
if (this._scrollTimer) clearTimeout(this._scrollTimer); // wait until a scrolling pause, then create an anchor to audio
this._scrollTimer = setTimeout(() => {
- DocUtils.MakeLinkToActiveAudio(() => this.props.DocumentView?.().ComponentView?.getAnchor!()!, false);
+ DocUtils.MakeLinkToActiveAudio(() => this.props.DocumentView?.().ComponentView?.getAnchor!(true)!, false);
this._scrollTimer = undefined;
}, 200);
}
@@ -351,7 +372,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
// if alt+left click, drag and annotate
this._downX = e.clientX;
this._downY = e.clientY;
- if ((this.props.Document._viewScale || 1) !== 1) return;
+ if ((this.props.Document._freeform_scale || 1) !== 1) return;
if ((e.button !== 0 || e.altKey) && this.props.isContentActive(true)) {
this._setPreviewCursor?.(e.clientX, e.clientY, true, false);
}
@@ -363,7 +384,6 @@ export class PDFViewer extends React.Component<IViewerProps> {
const target = e.target as any;
if (e.target && (target.className.includes('endOfContent') || (target.parentElement.className !== 'textLayer' && target.parentElement.parentElement?.className !== 'textLayer'))) {
this._textSelecting = false;
- document.addEventListener('pointermove', this.onSelectMove); // need this to prevent document from being dragged if stopPropagation doesn't get called
} else {
// if textLayer is hit, then we select text instead of using a marquee so clear out the marquee.
setTimeout(
@@ -373,35 +393,37 @@ export class PDFViewer extends React.Component<IViewerProps> {
this._styleRule = addStyleSheetRule(PDFViewer._annotationStyle, 'htmlAnnotation', { 'pointer-events': 'none' });
document.addEventListener('pointerup', this.onSelectEnd);
- document.addEventListener('pointermove', this.onSelectMove);
}
}
};
@action
finishMarquee = (x?: number, y?: number) => {
- this._getAnchor = AnchorMenu.Instance?.GetAnchor;
this.isAnnotating = false;
this._marqueeing = undefined;
this._textSelecting = true;
- document.removeEventListener('pointermove', this.onSelectMove);
};
- onSelectMove = (e: PointerEvent) => e.stopPropagation();
-
@action
onSelectEnd = (e: PointerEvent): void => {
this.isAnnotating = false;
clearStyleSheetRules(PDFViewer._annotationStyle);
this.props.select(false);
- document.removeEventListener('pointermove', this.onSelectMove);
document.removeEventListener('pointerup', this.onSelectEnd);
const sel = window.getSelection();
+ if (sel) {
+ AnchorMenu.Instance.setSelectedText(sel.toString());
+ }
+
if (sel?.type === 'Range') {
this.createTextAnnotation(sel, sel.getRangeAt(0));
AnchorMenu.Instance.jumpTo(e.clientX, e.clientY);
}
+
+ // Changing which document to add the annotation to (the currently selected PDF)
+ GPTPopup.Instance.setSidebarId('data_sidebar');
+ GPTPopup.Instance.addDoc = this.props.sidebarAddDoc;
};
@action
@@ -411,9 +433,9 @@ export class PDFViewer extends React.Component<IViewerProps> {
const clientRects = selRange.getClientRects();
for (let i = 0; i < clientRects.length; i++) {
const rect = clientRects.item(i);
- if (rect && rect.width !== this._mainCont.current.clientWidth && rect.width) {
+ if (rect?.width && rect.width < this._mainCont.current.clientWidth / this.props.ScreenToLocalTransform().Scale) {
const scaleX = this._mainCont.current.offsetWidth / boundingRect.width;
- const pdfScale = NumCast(this.props.layoutDoc._viewScale, 1);
+ const pdfScale = NumCast(this.props.layoutDoc._freeform_scale, 1);
const annoBox = document.createElement('div');
annoBox.className = 'marqueeAnnotator-annotationBox';
// transforms the positions from screen onto the pdf div
@@ -425,7 +447,8 @@ export class PDFViewer extends React.Component<IViewerProps> {
}
}
}
- this._selectionText = selRange.cloneContents().textContent || '';
+ this._selectionContent = selRange.cloneContents();
+ this._selectionText = this._selectionContent?.textContent || '';
// clear selection
if (sel.empty) {
@@ -437,11 +460,8 @@ export class PDFViewer extends React.Component<IViewerProps> {
}
};
- scrollXf = () => {
- return this._mainCont.current ? this.props.ScreenToLocalTransform().translate(0, NumCast(this.props.layoutDoc._scrollTop)) : this.props.ScreenToLocalTransform();
- };
-
onClick = (e: React.MouseEvent) => {
+ this._scrollStopper?.();
if (this._setPreviewCursor && e.button === 0 && Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) {
this._setPreviewCursor(e.clientX, e.clientY, false, false);
}
@@ -458,19 +478,23 @@ export class PDFViewer extends React.Component<IViewerProps> {
if (e.ctrlKey) {
const curScale = Number(this._pdfViewer.currentScaleValue);
this._pdfViewer.currentScaleValue = Math.max(1, Math.min(10, curScale - (curScale * e.deltaY) / 1000));
- this.props.layoutDoc._viewScale = Number(this._pdfViewer.currentScaleValue);
+ this.props.layoutDoc._freeform_scale = Number(this._pdfViewer.currentScaleValue);
}
}
};
- 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() {
return (
- <div className="pdfViewerDash-annotationLayer" style={{ height: Doc.NativeHeight(this.props.Document), transform: `scale(${NumCast(this.props.layoutDoc._viewScale, 1)})` }} ref={this._annotationLayer}>
+ <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`} />
+ <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>
);
@@ -480,48 +504,55 @@ 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>
);
}
+ getScrollHeight = () => this._scrollHeight;
showInfo = action((anno: Opt<Doc>) => (this._overlayAnnoInfo = anno));
- overlayTransform = () => this.scrollXf().scale(1 / NumCast(this.props.layoutDoc._viewScale, 1));
- panelWidth = () => this.props.PanelWidth() / (this.props.NativeDimScaling?.() || 1); // (this.Document.scrollHeight || Doc.NativeHeight(this.Document) || 0);
- panelHeight = () => this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1); // () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : Doc.NativeWidth(this.Document);
- transparentFilter = () => [...this.props.docFilters(), Utils.IsTransparentFilter()];
- opaqueFilter = () => [...this.props.docFilters(), Utils.noDragsDocFilter, ...(DragManager.docsBeingDragged.length ? [] : [Utils.IsOpaqueFilter()])];
+ scrollXf = () => (this._mainCont.current ? this.props.ScreenToLocalTransform().translate(0, NumCast(this.props.layoutDoc._layout_scrollTop)) : this.props.ScreenToLocalTransform());
+ 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.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';
+ if (this.inlineTextAnnotations.includes(doc) || this.props.isContentActive() === false) return 'none';
return '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={{
mixBlendMode: mixBlendMode,
display: display,
- transform: `scale(${NumCast(this.props.layoutDoc._viewScale, 1)})`,
pointerEvents: Doc.ActiveTool !== InkTool.None ? 'all' : undefined,
}}>
<CollectionFreeFormView
- {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
+ {...this.props}
+ NativeWidth={returnZero}
+ NativeHeight={returnZero}
+ setContentView={emptyFunction} // override setContentView to do nothing
+ pointerEvents={SnappingManager.GetIsDragging() && this.props.isContentActive() ? returnAll : returnNone} // freeform view doesn't get events unless something is being dragged onto it.
+ childPointerEvents={this.props.isContentActive() !== false ? 'all' : 'none'} // but freeform children need to get events to allow text editing, etc
renderDepth={this.props.renderDepth + 1}
isAnnotationOverlay={true}
- fieldKey={this.props.fieldKey + '-annotations'}
- CollectionView={undefined}
+ fieldKey={this.props.fieldKey + '_annotations'}
+ getScrollHeight={this.getScrollHeight}
setPreviewCursor={this.setPreviewCursor}
setBrushViewer={this.setBrushViewer}
PanelHeight={this.panelHeight}
PanelWidth={this.panelWidth}
ScreenToLocalTransform={this.overlayTransform}
- dropAction={'alias'}
- docFilters={docFilters}
+ isAnyChildContentActive={returnFalse}
+ isAnnotationOverlayScrollable={true}
+ childFilters={childFilters}
select={emptyFunction}
bringToFront={emptyFunction}
styleProvider={this.childStyleProvider}
@@ -529,14 +560,14 @@ export class PDFViewer extends React.Component<IViewerProps> {
</div>
);
@computed get overlayTransparentAnnotations() {
- return this.renderAnnotations(this.transparentFilter, 'multiply', DragManager.docsBeingDragged.length ? 'none' : undefined);
+ return 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>
@@ -558,8 +589,8 @@ export class PDFViewer extends React.Component<IViewerProps> {
onPointerDown={this.onPointerDown}
onClick={this.onClick}
style={{
- overflowX: NumCast(this.props.layoutDoc._viewScale, 1) !== 1 ? 'scroll' : undefined,
- height: !this.props.Document._fitWidth && window.screen.width > 600 ? Doc.NativeHeight(this.props.Document) : `100%`,
+ overflowX: NumCast(this.props.layoutDoc._freeform_scale, 1) !== 1 ? 'scroll' : undefined,
+ height: !this.props.Document._layout_fitWidth && window.screen.width > 600 ? Doc.NativeHeight(this.props.Document) : `100%`,
}}>
{this.pdfViewerDiv}
{this.annotationLayer}
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index c5177de90..d13c09443 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -2,12 +2,15 @@ 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 } from '../../../fields/Doc';
+import { Doc, DocListCast, DocListCastAsync, Field, Opt } from '../../../fields/Doc';
+import { DirectLinks } from '../../../fields/DocSymbols';
import { Id } from '../../../fields/FieldSymbols';
-import { StrCast } from '../../../fields/Types';
+import { DocCast, StrCast } from '../../../fields/Types';
import { DocUtils } from '../../documents/Documents';
import { DocumentType } from '../../documents/DocumentTypes';
import { DocumentManager } from '../../util/DocumentManager';
+import { LinkManager } from '../../util/LinkManager';
+import { undoBatch } from '../../util/UndoManager';
import { CollectionDockingView } from '../collections/CollectionDockingView';
import { ViewBoxBaseComponent } from '../DocComponent';
import { FieldView, FieldViewProps } from '../nodes/FieldView';
@@ -20,6 +23,8 @@ const ERROR = 0.03;
export interface SearchBoxProps extends FieldViewProps {
linkSearch: boolean;
linkFrom?: (() => Doc | undefined) | undefined;
+ linkCreateAnchor?: () => Doc | undefined;
+ linkCreated?: (link: Doc) => void;
}
/**
@@ -43,7 +48,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
@observable _linkedDocsIn: Map<Doc, Set<Doc>> = new Map<Doc, Set<Doc>>();
@observable _selectedResult: Doc | undefined = undefined;
@observable _deletedDocsStatus: boolean = false;
- @observable _onlyAliases: boolean = true;
+ @observable _onlyEmbeddings: boolean = true;
/**
* This is the constructor for the SearchBox class.
@@ -109,13 +114,12 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
this.selectElement(doc, () => DocumentManager.Instance.getFirstDocumentView(doc)?.ComponentView?.search?.(this._searchString, undefined, false));
});
- // TODO: nda -- Change this method to change what happens when you click on the item.
+ @undoBatch
makeLink = action((linkTo: Doc) => {
- if (this.props.linkFrom) {
- const linkFrom = this.props.linkFrom();
- if (linkFrom) {
- DocUtils.MakeLink({ doc: linkFrom }, { doc: linkTo });
- }
+ const linkFrom = this.props.linkCreateAnchor?.();
+ if (linkFrom) {
+ const link = DocUtils.MakeLink(linkFrom, linkTo, {});
+ link && this.props.linkCreated?.(link);
}
});
@@ -136,8 +140,10 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
visited.push(d);
const fieldKey = Doc.LayoutFieldKey(d);
const annos = !Field.toString(Doc.LayoutField(d) as Field).includes('CollectionView');
- const data = d[annos ? fieldKey + '-annotations' : fieldKey];
+ 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;
@@ -163,7 +169,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
.map(async d => {
const fieldKey = Doc.LayoutFieldKey(d);
const annos = !Field.toString(Doc.LayoutField(d) as Field).includes('CollectionView');
- const data = d[annos ? fieldKey + '-annotations' : fieldKey];
+ const data = d[annos ? fieldKey + '_annotations' : fieldKey];
const docs = await DocListCastAsync(data);
docs && newarray.push(...docs);
func(depth, d);
@@ -201,53 +207,57 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
*/
@action
searchCollection(query: string) {
- const blockedTypes = [DocumentType.PRESELEMENT, DocumentType.KVP, DocumentType.FILTER, DocumentType.SEARCH, DocumentType.SEARCHITEM, DocumentType.FONTICON, DocumentType.BUTTON, DocumentType.SCRIPTING];
+ this._selectedResult = undefined;
+ this._results = SearchBox.staticSearchCollection(CollectionDockingView.Instance?.rootDoc, query);
+
+ this.computePageRanks();
+ }
+ @action
+ static staticSearchCollection(rootDoc: Opt<Doc>, query: string) {
+ const blockedTypes = [DocumentType.PRESELEMENT, DocumentType.CONFIG, DocumentType.KVP, DocumentType.FILTER, DocumentType.SEARCH, DocumentType.SEARCHITEM, DocumentType.FONTICON, DocumentType.BUTTON, DocumentType.SCRIPTING];
const blockedKeys = [
'x',
'y',
'proto',
'width',
- 'autoHeight',
+ 'layout_autoHeight',
'acl-Override',
'acl-Public',
- 'context',
+ 'embedContainer',
'zIndex',
'height',
- 'text-scrollHeight',
- 'text-height',
+ 'text_scrollHeight',
+ 'text_height',
'cloneFieldFilter',
- 'isPrototype',
- 'text-annotations',
- 'dragFactory-count',
- 'text-noTemplate',
- 'aliases',
- 'system',
- 'layoutKey',
- 'baseProto',
+ 'isDataDoc',
+ 'text_annotations',
+ 'dragFactory_count',
+ 'text_noTemplate',
+ 'proto_embeddings',
+ 'isSystem',
+ 'layout_fieldKey',
+ 'isBaseProto',
'xMargin',
'yMargin',
'links',
'layout',
'layout_keyValue',
- 'fitWidth',
- 'viewType',
- 'title-custom',
- 'panX',
- 'panY',
- 'viewScale',
+ 'layout_fitWidth',
+ 'type_collection',
+ 'title_custom',
+ 'freeform_panX',
+ 'freeform_panY',
+ 'freeform_scale',
];
- const collection = CollectionDockingView.Instance;
query = query.toLowerCase();
- this._results.clear();
- this._selectedResult = undefined;
-
- if (collection !== undefined) {
- const docs = DocListCast(collection.rootDoc[Doc.LayoutFieldKey(collection.rootDoc)]);
+ const results = new Map<Doc, string[]>();
+ if (rootDoc) {
+ const docs = DocListCast(rootDoc[Doc.LayoutFieldKey(rootDoc)]);
const docIDs: String[] = [];
SearchBox.foreachRecursiveDoc(docs, (depth: number, doc: Doc) => {
- const dtype = StrCast(doc.type, 'string') as DocumentType;
- if (dtype && !blockedTypes.includes(dtype) && !docIDs.includes(doc[Id]) && depth > 0) {
+ const dtype = StrCast(doc.type) as DocumentType;
+ if (dtype && !blockedTypes.includes(dtype) && !docIDs.includes(doc[Id]) && depth >= 0) {
const hlights = new Set<string>();
SearchBox.documentKeys(doc).forEach(
key =>
@@ -258,14 +268,13 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
blockedKeys.forEach(key => hlights.delete(key));
if (Array.from(hlights.keys()).length > 0) {
- this._results.set(doc, Array.from(hlights.keys()));
+ results.set(doc, Array.from(hlights.keys()));
}
}
docIDs.push(doc[Id]);
});
}
-
- this.computePageRanks();
+ return results;
}
/**
@@ -284,7 +293,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) => {
@@ -293,9 +302,9 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
} else {
const linkedDocSet = new Set<Doc>();
- Doc.GetProto(doc)[DirectLinksSym].forEach(link => {
- const d1 = link?.anchor1 as Doc;
- const d2 = link?.anchor2 as Doc;
+ 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)) {
linkedDocSet.add(d2);
this._linkedDocsIn.get(d2)?.add(doc);
@@ -380,7 +389,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
const query = StrCast(this._searchString);
Doc.SetSearchQuery(query);
- Array.from(this._results.keys()).forEach(doc => DocumentManager.Instance.getFirstDocumentView(doc)?.ComponentView?.search?.(this._searchString, undefined, true));
+ if (!this.props.linkSearch) Array.from(this._results.keys()).forEach(doc => DocumentManager.Instance.getFirstDocumentView(doc)?.ComponentView?.search?.(this._searchString, undefined, true));
this._results.clear();
if (query) {
@@ -407,7 +416,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
* or opening it in a new tab.
*/
selectElement = async (doc: Doc, finishFunc: () => void) => {
- await DocumentManager.Instance.jumpToDocument(doc, true, undefined, [], undefined, undefined, undefined, finishFunc);
+ await DocumentManager.Instance.showDocument(doc, { willZoomCentered: true }, finishFunc);
};
/**
@@ -437,6 +446,8 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
const resultsJSX = Array();
+ const fromDoc = this.props.linkFrom?.();
+
sortedResults.forEach(result => {
var className = 'searchBox-results-scroll-view-result';
@@ -460,6 +471,13 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
e.stopPropagation();
}
}
+ style={{
+ fontWeight: LinkManager.Links(fromDoc).find(
+ link => Doc.AreProtosEqual(LinkManager.getOppositeAnchor(link, fromDoc!), result[0] as Doc) || Doc.AreProtosEqual(DocCast(LinkManager.getOppositeAnchor(link, fromDoc!)?.annotationOn), result[0] as Doc)
+ )
+ ? 'bold'
+ : '',
+ }}
className={className}>
<div className="searchBox-result-title">{title as string}</div>
<div className="searchBox-result-type">{formattedType}</div>
@@ -482,7 +500,10 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
defaultValue={''}
autoComplete="off"
onChange={this.onInputChange}
- onKeyPress={e => (e.key === 'Enter' ? this.submitSearch() : null)}
+ onKeyPress={e => {
+ e.key === 'Enter' ? this.submitSearch() : null;
+ e.stopPropagation();
+ }}
type="text"
placeholder="Search..."
id="search-input"
diff --git a/src/client/views/topbar/TopBar.tsx b/src/client/views/topbar/TopBar.tsx
index 50a93ee92..68d652cc4 100644
--- a/src/client/views/topbar/TopBar.tsx
+++ b/src/client/views/topbar/TopBar.tsx
@@ -3,20 +3,22 @@ import { Button, FontSize, IconButton, Size } from 'browndash-components';
import { action, computed, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { FaBug, FaCamera } from 'react-icons/fa';
-import { AclAdmin, Doc, DocListCast } from '../../../fields/Doc';
+import { FaBug, FaCamera, FaStamp } from 'react-icons/fa';
+import { Doc } from '../../../fields/Doc';
+import { AclAdmin } from '../../../fields/DocSymbols';
import { StrCast } from '../../../fields/Types';
import { GetEffectiveAcl } from '../../../fields/util';
import { DocumentManager } from '../../util/DocumentManager';
import { PingManager } from '../../util/PingManager';
import { ReportManager } from '../../util/ReportManager';
+import { ServerStats } from '../../util/ServerStats';
import { SettingsManager } from '../../util/SettingsManager';
import { SharingManager } from '../../util/SharingManager';
import { UndoManager } from '../../util/UndoManager';
import { CollectionDockingView } from '../collections/CollectionDockingView';
import { ContextMenu } from '../ContextMenu';
import { DashboardView } from '../DashboardView';
-import { Borders, Colors } from '../global/globalEnums';
+import { Colors } from '../global/globalEnums';
import { MainView } from '../MainView';
import './TopBar.scss';
@@ -37,10 +39,11 @@ export class TopBar extends React.Component {
@observable backgroundColor: string = Colors.DARK_GRAY;
@observable happyHeart: boolean = PingManager.Instance.isBeating;
- setHappyHeart = action((status: boolean) => this.happyHeart = status);
- dispose = reaction(() => PingManager.Instance.isBeating, isBeating => this.setHappyHeart(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.
@@ -53,7 +56,7 @@ export class TopBar extends React.Component {
return (
<div className="topbar-left">
{Doc.ActiveDashboard ? (
- <IconButton onClick={this.navigateToHome} icon={<FontAwesomeIcon icon="home" />} isCircle={true} hoverStyle="gray" color={this.textColor} />
+ <IconButton onClick={this.navigateToHome} icon={<FontAwesomeIcon icon="home" />} color={this.textColor} />
) : (
<div className="logo-container">
<img className="logo" src="/assets/medium-blue-light-blue-circle.png" alt="dash logo"></img>
@@ -61,19 +64,8 @@ export class TopBar extends React.Component {
<span style={{ color: Colors.LIGHT_BLUE, fontWeight: 500 }}>dash</span>
</div>
)}
- {Doc.ActiveDashboard && !Doc.noviceMode && (
- <Button
- text="Explore"
- tooltip="Browsing mode for directly navigating to documents"
- fontSize={FontSize.SECONDARY}
- isActive={MainView.Instance._exploreMode}
- size={Size.SMALL}
- color={this.textColor}
- borderRadius={5}
- hoverStyle="gray"
- iconPosition="right"
- onClick={action(() => (MainView.Instance._exploreMode = !MainView.Instance._exploreMode))}
- />
+ {Doc.ActiveDashboard && (
+ <Button text="Explore" tooltip="Browsing mode for directly navigating to documents" size={Size.SMALL} color={this.textColor} onClick={action(() => (MainView.Instance._exploreMode = !MainView.Instance._exploreMode))} />
)}
</div>
);
@@ -101,13 +93,8 @@ export class TopBar extends React.Component {
<Button
text={StrCast(Doc.ActiveDashboard.title)}
tooltip="Browsing mode for directly navigating to documents"
- fontSize={FontSize.SECONDARY}
size={Size.SMALL}
color={'white'}
- type="outline"
- backgroundColor={Colors.MEDIUM_BLUE}
- borderRadius={5}
- hoverStyle="none"
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' });
@@ -128,21 +115,13 @@ export class TopBar extends React.Component {
onClick={() => {
SharingManager.Instance.open(undefined, Doc.ActiveDashboard);
}}
- type="outline"
- fontSize={FontSize.SECONDARY}
size={Size.SMALL}
- borderRadius={5}
- hoverStyle="gray"
/>
{!Doc.noviceMode && (
<IconButton
- fontSize={FontSize.SECONDARY}
- isCircle={true}
tooltip="Work on a copy of the dashboard layout"
size={Size.SMALL}
color={this.textColor}
- borderRadius={Borders.STANDARD}
- hoverStyle="gray"
onClick={async () => {
const batch = UndoManager.StartBatch('snapshot');
await DashboardView.snapshotDashboard();
@@ -163,36 +142,13 @@ export class TopBar extends React.Component {
@computed get topbarRight() {
return (
<div className="topbar-right">
- <IconButton size={Size.SMALL} isCircle={true} color={Colors.LIGHT_GRAY} backgroundColor={Colors.DARK_GRAY} hoverStyle="gray" onClick={() => ReportManager.Instance.open()} icon={<FaBug />} />
- <IconButton
- size={Size.SMALL}
- isCircle={true}
- color={Colors.LIGHT_GRAY}
- backgroundColor={Colors.DARK_GRAY}
- hoverStyle="gray"
- onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/', '_blank')}
- icon={<FontAwesomeIcon icon="question-circle" />}
- />
- <IconButton
- size={Size.SMALL}
- isCircle={true}
- color={Colors.LIGHT_GRAY}
- backgroundColor={Colors.DARK_GRAY}
- hoverStyle="gray"
- isActive={SettingsManager.Instance.isOpen}
- onClick={() => SettingsManager.Instance.open()}
- icon={<FontAwesomeIcon icon="cog" />}
- />
- <IconButton
- size={Size.SMALL}
- isCircle={true}
- color={this.happyHeart ? Colors.LIGHT_BLUE : Colors.ERROR_RED}
- backgroundColor={Colors.DARK_GRAY}
- hoverStyle="gray"
- isActive={SettingsManager.Instance.isOpen}
- onClick={() => SettingsManager.Instance.open()}
- icon={<FontAwesomeIcon icon={this.happyHeart ? "heart" : "heart-broken"} />}
- />
+ <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" />} />
+ <div className={'topbarHeart' + (this.happyHeart ? '' : '-red')}>
+ <IconButton size={Size.SMALL} color={this.happyHeart ? Colors.LIGHT_BLUE : Colors.ERROR_RED} icon={<FontAwesomeIcon icon={this.happyHeart ? 'heart' : 'heart-broken'} />} />
+ </div>
{/* <Button text={'Logout'} borderRadius={5} hoverStyle={'gray'} backgroundColor={Colors.DARK_GRAY} color={this.textColor} fontSize={FontSize.SECONDARY} onClick={() => window.location.assign(Utils.prepend('/logout'))} /> */}
</div>
);
diff --git a/src/decycler/decycler.d.ts b/src/decycler/decycler.d.ts
new file mode 100644
index 000000000..84620f79c
--- /dev/null
+++ b/src/decycler/decycler.d.ts
@@ -0,0 +1,2 @@
+export declare const decycle: Function;
+export declare const retrocycle: Function;
diff --git a/src/decycler/decycler.js b/src/decycler/decycler.js
new file mode 100644
index 000000000..7fb8a45c7
--- /dev/null
+++ b/src/decycler/decycler.js
@@ -0,0 +1,51 @@
+///
+/// this is a modified copy of the npm project: https://www.npmjs.com/package/json-decycle
+/// the original code is used as replacer when stringifying JSON objects that have cycles.
+/// However, we want an additional replacer that stringifies Dash Fields and Docs in a custom way.
+/// So this modified code allows for a custom replacer to be added to this object that will run before this replacer
+///
+
+const g = e => typeof e === 'object' && e != null && !(e instanceof Boolean) && !(e instanceof Date) && !(e instanceof Number) && !(e instanceof RegExp) && !(e instanceof String);
+const b = e => String('#') + e.map(t => String(t).replace(/~/g, '~0').replace(/\//g, '~1')).join('/');
+// eslint-disable-next-line node/no-unsupported-features/es-syntax
+export function decycle(replacer) {
+ const e = new WeakMap();
+ return function (n, rr) {
+ const r = replacer(n, rr);
+ if (n !== '$ref' && g(r)) {
+ if (e.has(r)) return { $ref: b(e.get(r)) };
+ e.set(r, [...(e.get(this) === undefined ? [] : e.get(this)), n]);
+ }
+ return r;
+ };
+}
+// eslint-disable-next-line node/no-unsupported-features/es-syntax
+export function retrocycle() {
+ const e = new WeakMap();
+ const t = new WeakMap();
+ const n = new Set();
+ function r(o) {
+ const c = o.$ref.slice(1).split('/');
+ let s;
+ let a = this;
+ // eslint-disable-next-line no-plusplus
+ for (let p = 0; p < c.length; p++) {
+ s = c[p].replace(/~1/g, '/').replace(/~0/g, '~');
+ a = a[s];
+ }
+ const f = e.get(o);
+ f[t.get(o)] = a;
+ }
+ return function (c, s) {
+ if (c === '$ref') n.add(this);
+ else if (g(s)) {
+ const f = c === '' && Object.keys(this).length === 1;
+ if (f) n.forEach(r, this);
+ else {
+ e.set(s, this);
+ t.set(s, c);
+ }
+ }
+ return s;
+ };
+}
diff --git a/src/fields/DateField.ts b/src/fields/DateField.ts
index 26f51b2d3..2ea619bd9 100644
--- a/src/fields/DateField.ts
+++ b/src/fields/DateField.ts
@@ -1,11 +1,11 @@
-import { Deserializable } from "../client/util/SerializationHelper";
-import { serializable, date } from "serializr";
-import { ObjectField } from "./ObjectField";
-import { Copy, ToScriptString, ToString } from "./FieldSymbols";
-import { scriptingGlobal, ScriptingGlobals } from "../client/util/ScriptingGlobals";
+import { Deserializable } from '../client/util/SerializationHelper';
+import { serializable, date } from 'serializr';
+import { ObjectField } from './ObjectField';
+import { Copy, ToScriptString, ToString } from './FieldSymbols';
+import { scriptingGlobal, ScriptingGlobals } from '../client/util/ScriptingGlobals';
@scriptingGlobal
-@Deserializable("date")
+@Deserializable('date')
export class DateField extends ObjectField {
@serializable(date())
readonly date: Date;
@@ -24,7 +24,7 @@ export class DateField extends ObjectField {
}
[ToScriptString]() {
- return `new DateField(new Date(${this.date.toISOString()}))`;
+ return `new DateField(new Date("${this.date.toISOString()}"))`;
}
[ToString]() {
return this.date.toLocaleString();
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 70cb10970..c9f7e4114 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -1,6 +1,6 @@
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { saveAs } from 'file-saver';
-import { action, computed, observable, ObservableMap, runInAction } from 'mobx';
+import { action, computed, observable, ObservableMap, ObservableSet, runInAction } from 'mobx';
import { computedFn } from 'mobx-utils';
import { alias, map, serializable } from 'serializr';
import { DocServer } from '../client/DocServer';
@@ -9,12 +9,42 @@ 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 { InkTool } from './InkField';
-import { List } from './List';
+import {
+ AclAdmin,
+ AclAugment,
+ AclEdit,
+ AclPrivate,
+ AclReadonly,
+ AclSelfEdit,
+ AclUnset,
+ Animation,
+ CachedUpdates,
+ DirectLinks,
+ DocAcl,
+ DocCss,
+ DocData,
+ DocFields,
+ DocLayout,
+ FieldKeys,
+ FieldTuples,
+ ForceServerWrite,
+ Height,
+ Highlight,
+ Initializing,
+ Self,
+ SelfProxy,
+ Update,
+ UpdatingFromServer,
+ Width,
+} from './DocSymbols';
+import { Copy, HandleUpdate, Id, OnUpdate, Parent, ToScriptString, ToString } from './FieldSymbols';
+import { InkField, InkTool } from './InkField';
+import { List, ListFieldName } from './List';
import { ObjectField } from './ObjectField';
import { PrefetchProxy, ProxyField } from './Proxy';
import { FieldId, RefField } from './RefField';
@@ -22,31 +52,33 @@ import { RichTextField } from './RichTextField';
import { listSpec } from './Schema';
import { ComputedField, ScriptField } from './ScriptField';
import { Cast, DocCast, FieldValue, NumCast, StrCast, ToConstructor } from './Types';
-import { AudioField, ImageField, MapField, PdfField, VideoField, WebField } from './URLField';
+import { AudioField, CsvField, ImageField, PdfField, VideoField, WebField } from './URLField';
import { deleteProperty, GetEffectiveAcl, getField, getter, makeEditable, makeReadOnly, normalizeEmail, setter, SharingPermissions, updateFunction } from './util';
import JSZip = require('jszip');
-
export namespace Field {
export function toKeyValueString(doc: Doc, key: string): string {
- const onDelegate = Object.keys(doc).includes(key);
+ const onDelegate = Object.keys(doc).includes(key.replace(/^_/, ''));
const field = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
- return !Field.IsField(field) ? '' : (onDelegate ? '=' : '') + (field instanceof ComputedField ? `:=${field.script.originalScript}` : Field.toScriptString(field));
+ return !Field.IsField(field)
+ ? key.startsWith('_')
+ ? '='
+ : ''
+ : (onDelegate ? '=' : '') + (field instanceof ComputedField ? `:=${field.script.originalScript}` : field instanceof ScriptField ? `$=${field.script.originalScript}` : Field.toScriptString(field));
}
export function toScriptString(field: Field): string {
- if (typeof field === 'string') {
- if (field.startsWith('{"')) return `'${field}'`; // bcz: hack ... want to quote the string the right way. if there are nested "'s, then use ' instead of ". In this case, test for the start of a JSON string of the format {"property": ... } and use outer 's instead of "s
- return `"${field}"`;
+ switch (typeof field) {
+ case 'string':
+ if (field.startsWith('{"')) return `'${field}'`; // bcz: hack ... want to quote the string the right way. if there are nested "'s, then use ' instead of ". In this case, test for the start of a JSON string of the format {"property": ... } and use outer 's instead of "s
+ return !field.includes('`') ? `\`${field}\`` : `"${field}"`;
+ case 'number':
+ case 'boolean':
+ return String(field);
}
- if (typeof field === 'number' || typeof field === 'boolean') return String(field);
- if (field === undefined || field === null) return 'null';
- return field[ToScriptString]();
+ return field?.[ToScriptString]?.() ?? 'null';
}
export function toString(field: Field): string {
- if (typeof field === 'string') return field;
- if (typeof field === 'number' || typeof field === 'boolean') return String(field);
- if (field instanceof ObjectField) return field[ToString]();
- if (field instanceof RefField) return field[ToString]();
- return '';
+ if (typeof field === 'string' || typeof field === 'number' || typeof field === 'boolean') return String(field);
+ return field?.[ToString]?.() || '';
}
export function IsField(field: any): field is Field;
export function IsField(field: any, includeUndefined: true): field is Field | undefined;
@@ -79,80 +111,58 @@ export async function DocCastAsync(field: FieldResult): Promise<Opt<Doc>> {
return Cast(field, Doc);
}
-export function NumListCast(field: FieldResult) {
- return Cast(field, listSpec('number'), []);
-}
-export function StrListCast(field: FieldResult) {
- return Cast(field, listSpec('string'), []);
+export function NumListCast(field: FieldResult, defaultVal: number[] = []) {
+ return Cast(field, listSpec('number'), defaultVal);
}
-export function DocListCast(field: FieldResult) {
- return Cast(field, listSpec(Doc), []).filter(d => d instanceof Doc) as Doc[];
+export function StrListCast(field: FieldResult, defaultVal: string[] = []) {
+ return Cast(field, listSpec('string'), defaultVal);
}
-export function DocListCastOrNull(field: FieldResult) {
- return Cast(field, listSpec(Doc), null)?.filter(d => d instanceof Doc) as Doc[] | undefined;
+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 DataSym = Symbol('Data');
-export const LayoutSym = Symbol('Layout');
-export const FieldsSym = Symbol('Fields');
-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');
-
-const AclMap = new Map<string, symbol>([
- ['None', AclUnset],
- [SharingPermissions.None, AclPrivate],
- [SharingPermissions.View, AclReadonly],
- [SharingPermissions.Augment, AclAugment],
- [SharingPermissions.SelfEdit, AclSelfEdit],
- [SharingPermissions.Edit, AclEdit],
- [SharingPermissions.Admin, AclAdmin],
+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 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] }]));
// 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 permissions: { [key: string]: symbol } = {};
- doc[UpdatingFromServer] = true;
- Object.keys(doc).filter(key => key.startsWith('acl') && (permissions[key] = AclMap.get(StrCast(doc[key]))!));
- doc[UpdatingFromServer] = false;
-
- if (Object.keys(permissions).length) {
- doc[AclSym] = permissions;
+ 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[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;
}
}
@scriptingGlobal
-@Deserializable('Doc', updateCachedAcls).withFields(['id'])
+@Deserializable('Doc', updateCachedAcls, ['id'])
export class Doc extends RefField {
- //TODO tfs: these should be temporary...
- private static mainDocId: string | undefined;
- public static get MainDocId() {
- return this.mainDocId;
- }
- public static set MainDocId(id: string | undefined) {
- this.mainDocId = id;
- }
-
@observable public static CurrentlyLoading: Doc[];
// removes from currently loading display
@action
@@ -207,6 +217,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);
}
@@ -228,6 +247,19 @@ export class Doc extends RefField {
public static get MyFileOrphans() {
return DocCast(Doc.UserDoc().myFileOrphans);
}
+ public static AddFileOrphan(doc: Doc) {
+ if (
+ doc &&
+ Doc.MyFileOrphans instanceof Doc &&
+ Doc.IsDataProto(doc) &&
+ !Doc.IsSystem(doc) &&
+ ![DocumentType.CONFIG, 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);
}
@@ -265,70 +297,67 @@ export class Doc extends RefField {
}
constructor(id?: FieldId, forceSave?: boolean) {
super(id);
- const doc = new Proxy<this>(this, {
+ const docProxy = new Proxy<this>(this, {
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: () => {
throw new Error("Currently properties can't be defined on documents using Object.defineProperty");
},
});
- this[SelfProxy] = doc;
+ this[SelfProxy] = docProxy;
if (!id || forceSave) {
- DocServer.CreateField(doc);
+ DocServer.CreateField(docProxy);
}
- return doc;
+ 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);
+ field !== undefined && (this[FieldKeys][key] = true);
if (!(field instanceof ObjectField)) continue;
field[Parent] = this[Self];
field[OnUpdate] = updateFunction(this[Self], key, field, this[SelfProxy]);
}
}
- private get __fieldKeys() {
- return this.___fieldKeys;
- }
- private set __fieldKeys(value) {
- this.___fieldKeys = value;
- }
- @observable private ___fields: any = {};
- @observable private ___fieldKeys: any = {};
- @observable public [AclSym]: { [key: string]: symbol } = {};
- @observable public [DirectLinksSym]: Set<Doc> = new Set();
+ @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 [DocAcl]: { [key: string]: symbol } = {};
+ @observable public [DocCss]: number = 0; // incrementer denoting a change to CSS layout
+ @observable public [DirectLinks]: Set<Doc> = new Set();
+ @observable public [Animation]: Opt<Doc>;
+ @observable public [Highlight]: boolean = false;
+ static __Anim(Doc: Doc) {
+ // for debugging to print AnimationSym field easily.
+ return Doc[Animation];
+ }
private [UpdatingFromServer]: boolean = false;
private [ForceServerWrite]: boolean = false;
@@ -340,15 +369,15 @@ export class Doc extends RefField {
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 [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);
}
@@ -356,7 +385,7 @@ export class Doc extends RefField {
const templateLayoutDoc = Cast(Doc.LayoutField(this[SelfProxy]), Doc, null);
if (templateLayoutDoc) {
let renderFieldKey: any;
- const layoutField = templateLayoutDoc[StrCast(templateLayoutDoc.layoutKey, 'layout')];
+ const layoutField = templateLayoutDoc[StrCast(templateLayoutDoc.layout_fieldKey, 'layout')];
if (typeof layoutField === 'string') {
renderFieldKey = layoutField.split("fieldKey={'")[1].split("'")[0]; //layoutField.split("'")[1];
} else {
@@ -485,40 +514,33 @@ 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 IsPrototype(doc: Doc) {
- return GetT(doc, 'isPrototype', 'boolean', true);
+ export function IsDataProto(doc: Doc) {
+ return GetT(doc, 'isDataDoc', 'boolean', true);
}
export function IsBaseProto(doc: Doc) {
- return GetT(doc, 'baseProto', 'boolean', true);
+ return GetT(doc, 'isBaseProto', 'boolean', true);
}
export function IsSystem(doc: Doc) {
- return GetT(doc, 'system', 'boolean', true);
+ return GetT(doc, 'isSystem', 'boolean', true);
}
export function IsDelegateField(doc: Doc, fieldKey: string) {
return doc && Get(doc, fieldKey, true) !== undefined;
}
export async function SetInPlace(doc: Doc, key: string, value: Field | undefined, defaultProto: boolean) {
if (key.startsWith('_')) key = key.substring(1);
- const hasProto = doc.proto instanceof Doc;
+ 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;
- }
- export async function SetOnPrototype(doc: Doc, key: string, value: Field) {
- const proto = Object.getOwnPropertyNames(doc).indexOf('isPrototype') === -1 ? doc.proto : doc;
-
- if (proto) {
- 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;
}
@@ -551,22 +573,14 @@ export namespace Doc {
// compare whether documents or their protos match
export function AreProtosEqual(doc?: Doc, other?: Doc) {
- if (!doc || !other) return false;
- const r = doc === other;
- const r2 = Doc.GetProto(doc) === other;
- const r3 = Doc.GetProto(other) === doc;
- const r4 = Doc.GetProto(doc) === Doc.GetProto(other) && Doc.GetProto(other) !== undefined;
- return r || r2 || r3 || r4;
+ return 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 base prototype ('isPrototype').
+ // 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 {
- if (doc instanceof Promise) {
- // console.log("GetProto: warning: got Promise insead of Doc");
- }
- const proto = doc && (Doc.GetT(doc, 'isPrototype', '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 {
@@ -580,7 +594,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);
@@ -657,7 +671,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),
@@ -670,100 +684,135 @@ export namespace Doc {
return bounds;
}
- export function MakeAlias(doc: Doc, id?: string) {
- const alias = !GetT(doc, 'isPrototype', 'boolean', true) && doc.proto ? Doc.MakeCopy(doc, undefined, id) : Doc.MakeDelegate(doc, id);
- const layout = Doc.LayoutField(alias);
- if (layout instanceof Doc && layout !== alias && layout === Doc.Layout(alias)) {
- Doc.SetLayout(alias, Doc.MakeAlias(layout));
+ export function MakeEmbedding(doc: Doc, id?: string) {
+ const embedding = !GetT(doc, 'isDataDoc', 'boolean', true) && doc.proto ? Doc.MakeCopy(doc, undefined, id) : Doc.MakeDelegate(doc, id);
+ const layout = Doc.LayoutField(embedding);
+ if (layout instanceof Doc && layout !== embedding && layout === Doc.Layout(embedding)) {
+ Doc.SetLayout(embedding, Doc.MakeEmbedding(layout));
}
- alias.aliasOf = doc;
- alias.aliasNumber = Doc.GetProto(doc).aliasNumber = NumCast(Doc.GetProto(doc).aliasNumber) + 1;
- alias.title = ComputedField.MakeFunction(`renameAlias(this)`);
- alias.author = Doc.CurrentUserEmail;
+ embedding.createdFrom = doc;
+ embedding.proto_embeddingId = Doc.GetProto(doc).proto_embeddingId = NumCast(Doc.GetProto(doc).proto_embeddingId) + 1;
+ embedding.title = ComputedField.MakeFunction(`renameEmbedding(this)`);
+ embedding.author = Doc.CurrentUserEmail;
- Doc.AddDocToList(Doc.GetProto(doc)[DataSym], 'aliases', alias);
+ Doc.AddDocToList(Doc.GetProto(doc)[DocData], 'proto_embeddings', embedding);
- return alias;
+ return embedding;
}
- export function BestAlias(doc: Doc) {
- const bestAlias = Doc.GetProto(doc) ? DocListCast(doc.aliases).find(doc => !doc.context && doc.author === Doc.CurrentUserEmail) : doc;
- return bestAlias ?? Doc.MakeAlias(doc);
+ export function BestEmbedding(doc: Doc) {
+ const bestEmbedding = Doc.GetProto(doc) ? DocListCast(doc.proto_embeddings).find(doc => !doc.embedContainer && doc.author === Doc.CurrentUserEmail) : doc;
+ return bestEmbedding ?? Doc.MakeEmbedding(doc);
}
- export async function makeClone(doc: Doc, cloneMap: Map<string, Doc>, linkMap: Map<Doc, Doc>, rtfs: { copy: Doc; key: string; field: RichTextField }[], exclusions: string[], dontCreate: boolean, asBranch: boolean): Promise<Doc> {
+ // this lists out all the tag ids that can be in a RichTextField that might contain document ids.
+ // if a document is cloned, we need to make sure to clone all of these referenced documents as well;
+ export const DocsInTextFieldIds = ['audioId', 'textId', 'anchorId', 'docId'];
+ export async function makeClone(doc: Doc, cloneMap: Map<string, Doc>, linkMap: Map<string, Doc>, rtfs: { copy: Doc; key: string; field: RichTextField }[], exclusions: string[], pruneDocs: Doc[], cloneLinks: boolean): Promise<Doc> {
if (Doc.IsBaseProto(doc)) return doc;
if (cloneMap.get(doc[Id])) return cloneMap.get(doc[Id])!;
- const copy = dontCreate ? (asBranch ? Cast(doc.branchMaster, Doc, null) || doc : doc) : new Doc(undefined, true);
+ const copy = new Doc(undefined, true);
cloneMap.set(doc[Id], copy);
- const fieldExclusions = doc.type === DocumentType.MARKER ? exclusions.filter(ex => ex !== 'annotationOn') : exclusions;
- const filter = [...fieldExclusions, ...Cast(doc.cloneFieldFilter, listSpec('string'), [])];
+ const filter = [...exclusions, ...StrListCast(doc.cloneFieldFilter)];
await Promise.all(
Object.keys(doc).map(async key => {
if (filter.includes(key)) return;
- const assignKey = (val: any) => !dontCreate && (copy[key] = val);
+ const assignKey = (val: any) => (copy[key] = val);
const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
const field = ProxyField.WithoutProxy(() => doc[key]);
const copyObjectField = async (field: ObjectField) => {
const list = await Cast(doc[key], listSpec(Doc));
const docs = list && (await DocListCastAsync(list))?.filter(d => d instanceof Doc);
if (docs !== undefined && docs.length) {
- const clones = await Promise.all(docs.map(async d => Doc.makeClone(d, cloneMap, linkMap, rtfs, exclusions, dontCreate, asBranch)));
- !dontCreate && assignKey(new List<Doc>(clones));
- } else if (doc[key] instanceof Doc) {
- assignKey(key.includes('layout[') ? undefined : key.startsWith('layout') ? (doc[key] as Doc) : await Doc.makeClone(doc[key] as Doc, cloneMap, linkMap, rtfs, exclusions, dontCreate, asBranch)); // reference documents except copy documents that are expanded template fields
+ const clones = await Promise.all(docs.map(async d => Doc.makeClone(d, cloneMap, linkMap, rtfs, exclusions, pruneDocs, cloneLinks)));
+ assignKey(new List<Doc>(clones));
} else {
- !dontCreate && assignKey(ObjectField.MakeCopy(field));
+ assignKey(ObjectField.MakeCopy(field));
if (field instanceof RichTextField) {
- if (field.Data.includes('"audioId":') || field.Data.includes('"textId":') || field.Data.includes('"anchorId":')) {
+ if (DocsInTextFieldIds.some(id => field.Data.includes(`"${id}":`))) {
+ const docidsearch = new RegExp('(' + DocsInTextFieldIds.map(exp => '(' + exp + ')').join('|') + ')":"([a-z-A-Z0-9_]*)"', 'g');
+ const rawdocids = field.Data.match(docidsearch);
+ const docids = rawdocids?.map((str: string) =>
+ DocsInTextFieldIds.reduce((output, exp) => output.replace(new RegExp(`${exp}":`, 'g'), ''), str)
+ .replace(/"/g, '')
+ .trim()
+ );
+ const results = docids && (await DocServer.GetRefFields(docids));
+ const docs = results && Array.from(Object.keys(results)).map(key => DocCast(results[key]));
+ docs?.map(doc => doc && Doc.makeClone(doc, cloneMap, linkMap, rtfs, exclusions, pruneDocs, cloneLinks));
rtfs.push({ copy, key, field });
}
}
}
};
- if (key === 'proto') {
- if (doc[key] instanceof Doc) {
- assignKey(await Doc.makeClone(doc[key] as Doc, cloneMap, linkMap, rtfs, exclusions, dontCreate, asBranch));
- }
- } else if (key === 'anchor1' || key === 'anchor2') {
- if (doc[key] instanceof Doc) {
- assignKey(await Doc.makeClone(doc[key] as Doc, cloneMap, linkMap, rtfs, exclusions, true, asBranch));
- }
- } else {
- if (field instanceof RefField) {
- assignKey(field);
- } else if (cfield instanceof ComputedField) {
- !dontCreate && assignKey(ComputedField.MakeFunction(cfield.script.originalScript));
- } else if (field instanceof ObjectField) {
- await copyObjectField(field);
- } else if (field instanceof Promise) {
- debugger; //This shouldn't happen...
+ const docAtKey = doc[key];
+ 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))) {
+ assignKey(await Doc.makeClone(docAtKey, cloneMap, linkMap, rtfs, exclusions, pruneDocs, cloneLinks));
} else {
- assignKey(field);
+ assignKey(docAtKey);
}
+ } else if (field instanceof RefField) {
+ assignKey(field);
+ } else if (cfield instanceof ComputedField) {
+ assignKey(cfield[Copy]()); // ComputedField.MakeFunction(cfield.script.originalScript));
+ } else if (field instanceof ObjectField) {
+ await copyObjectField(field);
+ } else if (field instanceof Promise) {
+ debugger; //This shouldn't happen...
+ } else {
+ assignKey(field);
}
})
);
- for (const link of Array.from(doc[DirectLinksSym])) {
- const linkClone = await Doc.makeClone(link, cloneMap, linkMap, rtfs, exclusions, dontCreate, asBranch);
- linkMap.set(link, linkClone);
- }
- if (!dontCreate) {
- Doc.SetInPlace(copy, 'title', (asBranch ? 'BRANCH: ' : 'CLONE: ') + doc.title, true);
- asBranch ? (copy.branchOf = doc) : (copy.cloneOf = doc);
- if (!Doc.IsPrototype(copy)) {
- Doc.AddDocToList(doc, 'branches', Doc.GetProto(copy));
+ 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])) &&
+ (cloneMap.has(DocCast(link.link_anchor_2)?.[Id]) || cloneMap.has(DocCast(DocCast(link.link_anchor_2)?.annotationOn)?.[Id])))
+ ) {
+ linkMap.set(link[Id], await Doc.makeClone(link, cloneMap, linkMap, rtfs, exclusions, pruneDocs, cloneLinks));
}
- cloneMap.set(doc[Id], copy);
- }
- Doc.IsPrototype(copy) && Doc.AddDocToList(Doc.MyFileOrphans, undefined, copy);
+ });
+ Doc.SetInPlace(copy, 'title', '>:' + doc.title, true);
+ copy.cloneOf = doc;
+ cloneMap.set(doc[Id], copy);
+
+ Doc.AddFileOrphan(copy);
return copy;
}
- export async function MakeClone(doc: Doc, dontCreate: boolean = false, asBranch = false, cloneMap: Map<string, Doc> = new Map()) {
- const linkMap = new Map<Doc, Doc>();
+ export function repairClone(clone: Doc, cloneMap: Map<string, Doc>, visited: Set<Doc>) {
+ if (visited.has(clone)) return;
+ visited.add(clone);
+ Object.keys(clone)
+ .filter(key => key !== 'cloneOf')
+ .map(key => {
+ const docAtKey = DocCast(clone[key]);
+ if (docAtKey && !Doc.IsSystem(docAtKey)) {
+ if (!Array.from(cloneMap.values()).includes(docAtKey)) {
+ if (cloneMap.has(docAtKey[Id])) {
+ clone[key] = cloneMap.get(docAtKey[Id]);
+ } else clone[key] = undefined;
+ } else {
+ repairClone(docAtKey, cloneMap, visited);
+ }
+ }
+ });
+ }
+ export function MakeClones(docs: Doc[], cloneLinks: boolean) {
+ const cloneMap = new Map<string, Doc>();
+ return docs.map(doc => Doc.MakeClone(doc, cloneLinks, cloneMap));
+ }
+
+ export async function MakeClone(doc: Doc, cloneLinks = true, cloneMap: Map<string, Doc> = new Map()) {
+ const linkMap = new Map<string, Doc>();
const rtfMap: { copy: Doc; key: string; field: RichTextField }[] = [];
- const copy = await Doc.makeClone(doc, cloneMap, linkMap, rtfMap, ['cloneOf', 'branches', 'branchOf', 'context'], dontCreate, asBranch);
- Array.from(linkMap.entries()).map((links: Doc[]) => LinkManager.Instance.addLink(links[1], true));
+ const copy = await Doc.makeClone(doc, cloneMap, linkMap, rtfMap, ['cloneOf'], doc.embedContainer ? [DocCast(doc.embedContainer)] : [], cloneLinks);
+ const repaired = new Set<Doc>();
+ const linkedDocs = Array.from(linkMap.values());
+ linkedDocs.map((link: Doc) => LinkManager.Instance.addLink(link, true));
rtfMap.map(({ copy, key, field }) => {
const replacer = (match: any, attr: string, id: string, offset: any, string: any) => {
const mapped = cloneMap.get(id);
@@ -773,70 +822,80 @@ export namespace Doc {
const mapped = cloneMap.get(id);
return href + (mapped ? mapped[Id] : id);
};
- const regex = `(${Doc.localServerPath()})([^"]*)`;
- const re = new RegExp(regex, 'g');
- copy[key] = new RichTextField(field.Data.replace(/("textId":|"audioId":|"anchorId":)"([^"]+)"/g, replacer).replace(re, replacer2), field.Text);
+ const re = new RegExp(`(${Doc.localServerPath()})([^"]*)`, 'g');
+ const docidsearch = new RegExp('(' + DocsInTextFieldIds.map(exp => `"${exp}":`).join('|') + ')"([^"]+)"', 'g');
+ copy[key] = new RichTextField(field.Data.replace(docidsearch, replacer).replace(re, replacer2), field.Text);
});
- return { clone: copy, map: cloneMap };
+ const clonedDocs = [...Array.from(cloneMap.values()), ...linkedDocs];
+ clonedDocs.map(clone => Doc.repairClone(clone, cloneMap, repaired));
+ return { clone: copy, map: cloneMap, linkMap };
}
- export async function Zip(doc: Doc) {
- // const a = document.createElement("a");
- // const url = Utils.prepend(`/downloadId/${this.props.Document[Id]}`);
- // a.href = url;
- // a.download = `DocExport-${this.props.Document[Id]}.zip`;
- // a.click();
- const { clone, map } = await Doc.MakeClone(doc, true);
+ export async function Zip(doc: Doc, zipFilename = 'dashExport.zip') {
+ const { clone, map, linkMap } = await Doc.MakeClone(doc);
+ const proms = new Set<string>();
function replacer(key: any, value: any) {
- if (['branchOf', 'cloneOf', 'context', 'cursors'].includes(key)) return undefined;
- else if (value instanceof Doc) {
- if (key !== 'field' && Number.isNaN(Number(key))) {
- const __fields = value[FieldsSym]();
- return { id: value[Id], __type: 'Doc', fields: __fields };
- } else {
- return { fieldId: value[Id], __type: 'proxy' };
- }
- } else if (value instanceof ScriptField) return { script: value.script, __type: 'script' };
- else if (value instanceof RichTextField) return { Data: value.Data, Text: value.Text, __type: 'RichTextField' };
- else if (value instanceof ImageField) return { url: value.url.href, __type: 'image' };
- else if (value instanceof PdfField) return { url: value.url.href, __type: 'pdf' };
- else if (value instanceof AudioField) return { url: value.url.href, __type: 'audio' };
- else if (value instanceof VideoField) return { url: value.url.href, __type: 'video' };
- else if (value instanceof WebField) return { url: value.url.href, __type: 'web' };
- else if (value instanceof MapField) return { url: value.url.href, __type: 'map' };
- else if (value instanceof DateField) return { date: value.toString(), __type: 'date' };
- else if (value instanceof ProxyField) return { fieldId: value.fieldId, __type: 'proxy' };
- else if (value instanceof Array && key !== 'fields') return { fields: value, __type: 'list' };
- else if (value instanceof ComputedField) return { script: value.script, __type: 'computed' };
- else return value;
+ if (key && ['branchOf', 'cloneOf', 'cursors'].includes(key)) return undefined;
+ if (value instanceof ImageField) {
+ const extension = value.url.href.replace(/.*\./, '');
+ proms.add(value.url.href.replace('.' + extension, '_o.' + extension));
+ return SerializationHelper.Serialize(value);
+ }
+ if (value instanceof PdfField || value instanceof AudioField || value instanceof VideoField) {
+ proms.add(value.url.href);
+ return SerializationHelper.Serialize(value);
+ }
+ if (
+ value instanceof Doc ||
+ value instanceof ScriptField ||
+ value instanceof RichTextField ||
+ value instanceof InkField ||
+ value instanceof CsvField ||
+ value instanceof WebField ||
+ value instanceof DateField ||
+ value instanceof ProxyField ||
+ value instanceof ComputedField
+ ) {
+ return SerializationHelper.Serialize(value);
+ }
+ if (value instanceof Array && key !== ListFieldName && key !== InkField.InkDataFieldName) return { fields: value, __type: 'list' };
+ return value;
}
const docs: { [id: string]: any } = {};
+ const links: { [id: string]: any } = {};
Array.from(map.entries()).forEach(f => (docs[f[0]] = f[1]));
- const docString = JSON.stringify({ id: doc[Id], docs }, replacer);
+ Array.from(linkMap.entries()).forEach(l => (links[l[0]] = l[1]));
+ const jsonDocs = JSON.stringify({ id: clone[Id], docs, links }, decycle(replacer));
const zip = new JSZip();
-
- zip.file('doc.json', docString);
-
- // // Generate a directory within the Zip file structure
- // var img = zip.folder("images");
-
- // // Add a file to the directory, in this case an image with data URI as contents
- // img.file("smile.gif", imgData, {base64: true});
-
- // Generate the zip file asynchronously
- zip.generateAsync({ type: 'blob' }).then((content: any) => {
- // Force down of the Zip file
- saveAs(content, doc.title + '.zip'); // glr: Possibly change the name of the document to match the title?
- });
- }
- //
- // Determines whether the layout needs to be expanded (as a template).
- // template expansion is rquired when the layout is a template doc/field and there's a datadoc which isn't equal to the layout template
- //
- export function WillExpandTemplateLayout(layoutDoc: Doc, dataDoc?: Doc) {
- return (layoutDoc.isTemplateForField || layoutDoc.isTemplateDoc) && dataDoc && layoutDoc !== dataDoc;
+ var count = 0;
+ const promArr = Array.from(proms).filter(url => url.startsWith(window.location.origin));
+ if (!promArr.length) {
+ zip.file('docs.json', jsonDocs);
+ zip.generateAsync({ type: 'blob' }).then(content => saveAs(content, zipFilename));
+ } else
+ promArr.forEach((url, i) => {
+ // loading a file and add it in a zip file
+ JSZipUtils.getBinaryContent(url, (err: any, data: any) => {
+ if (err) throw err; // or handle the error
+ // // Generate a directory within the Zip file structure
+ // const assets = zip.folder("assets");
+ // assets.file(filename, data, {binary: true});
+ const assetPathOnServer = promArr[i].replace(window.location.origin + '/', '').replace(/\//g, '%%%');
+ zip.file(assetPathOnServer, data, { binary: true });
+ console.log(' => ' + url);
+ if (++count === promArr.length) {
+ zip.file('docs.json', jsonDocs);
+ zip.generateAsync({ type: 'blob' }).then(content => saveAs(content, zipFilename));
+ // const a = document.createElement("a");
+ // const url = Utils.prepend(`/downloadId/${this.props.Document[Id]}`);
+ // a.href = url;
+ // a.download = `DocExport-${this.props.Document[Id]}.zip`;
+ // a.click();
+ }
+ });
+ });
}
const _pendingMap: Map<string, boolean> = new Map();
@@ -844,44 +903,34 @@ export namespace Doc {
// Returns an expanded template layout for a target data document if there is a template relationship
// between the two. If so, the layoutDoc is expanded into a new document that inherits the properties
// of the original layout while allowing for individual layout properties to be overridden in the expanded layout.
- // templateArgs should be equivalent to the layout key that generates the template since that's where the template parameters are stored in ()'s at the end of the key.
- // NOTE: the template will have references to "@params" -- the template arguments will be assigned to the '@params' field
- // so that when the @params key is accessed, it will be rewritten as the key that is stored in the 'params' field and
- // the derefence will then occur on the rootDocument (the original document).
- // in the future, field references could be written as @<someparam> and then arguments would be passed in the layout key as:
- // layout_mytemplate(somparam=somearg).
- // then any references to @someparam would be rewritten as accesses to 'somearg' on the rootDocument
- export function expandTemplateLayout(templateLayoutDoc: Doc, targetDoc?: Doc, templateArgs?: string) {
- const args = templateArgs?.match(/\(([a-zA-Z0-9._\-]*)\)/)?.[1].replace('()', '') || StrCast(templateLayoutDoc.PARAMS);
- if ((!args && !WillExpandTemplateLayout(templateLayoutDoc, targetDoc)) || !targetDoc) return templateLayoutDoc;
-
- const templateField = StrCast(templateLayoutDoc.isTemplateForField); // the field that the template renders
+ export function expandTemplateLayout(templateLayoutDoc: Doc, targetDoc?: Doc) {
+ // nothing to do if the layout isn't a template or we don't have a target that's different than the template
+ if (!targetDoc || templateLayoutDoc === targetDoc || (!templateLayoutDoc.isTemplateForField && !templateLayoutDoc.isTemplateDoc)) {
+ return templateLayoutDoc;
+ }
+
+ const templateField = StrCast(templateLayoutDoc.isTemplateForField, Doc.LayoutFieldKey(templateLayoutDoc)); // the field that the template renders
// First it checks if an expanded layout already exists -- if so it will be stored on the dataDoc
// using the template layout doc's id as the field key.
// 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 params = args.split('=').length > 1 ? args.split('=')[0] : 'PARAMS';
- const layoutFielddKey = Doc.LayoutFieldKey(templateLayoutDoc);
- const expandedLayoutFieldKey = (templateField || layoutFielddKey) + '-layout[' + templateLayoutDoc[Id] + (args ? `(${args})` : '') + ']';
+ const expandedLayoutFieldKey = templateField + '-layout[' + templateLayoutDoc[Id] + ']';
let expandedTemplateLayout = targetDoc?.[expandedLayoutFieldKey];
if (templateLayoutDoc.resolvedDataDoc instanceof Promise) {
expandedTemplateLayout = undefined;
_pendingMap.set(targetDoc[Id] + expandedLayoutFieldKey, true);
- } else if (expandedTemplateLayout === undefined && !_pendingMap.get(targetDoc[Id] + expandedLayoutFieldKey + args)) {
- if (templateLayoutDoc.resolvedDataDoc === (targetDoc.rootDocument || Doc.GetProto(targetDoc)) && templateLayoutDoc.PARAMS === StrCast(targetDoc.PARAMS)) {
+ } else if (expandedTemplateLayout === undefined && !_pendingMap.get(targetDoc[Id] + expandedLayoutFieldKey)) {
+ 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 + args, true);
+ _pendingMap.set(targetDoc[Id] + expandedLayoutFieldKey, true);
setTimeout(
action(() => {
const newLayoutDoc = Doc.MakeDelegate(templateLayoutDoc, undefined, '[' + templateLayoutDoc.title + ']');
- // the template's arguments are stored in params which is derefenced to find
- // the actual field key where the parameterized template data is stored.
- newLayoutDoc[params] = args !== '...' ? args : ''; // ... signifies the layout has sub template(s) -- so we have to expand the layout for them so that they can get the correct 'rootDocument' field, but we don't need to reassign their params. it would be better if the 'rootDocument' field could be passed dynamically to avoid have to create instances
newLayoutDoc.rootDocument = targetDoc;
const dataDoc = Doc.GetProto(targetDoc);
newLayoutDoc.resolvedDataDoc = dataDoc;
@@ -890,7 +939,7 @@ export namespace Doc {
}
targetDoc[expandedLayoutFieldKey] = newLayoutDoc;
- _pendingMap.delete(targetDoc[Id] + expandedLayoutFieldKey + args);
+ _pendingMap.delete(targetDoc[Id] + expandedLayoutFieldKey);
})
);
}
@@ -903,11 +952,11 @@ export namespace Doc {
// otherwise, it just returns the childDoc
export function GetLayoutDataDocPair(containerDoc: Doc, containerDataDoc: Opt<Doc>, childDoc: Doc) {
if (!childDoc || childDoc instanceof Promise || !Doc.GetProto(childDoc)) {
- console.log('No, no, no!');
+ console.log('Warning: GetLayoutDataDocPair childDoc not defined');
return { layout: childDoc, data: childDoc };
}
- const resolvedDataDoc = Doc.AreProtosEqual(containerDataDoc, containerDoc) || (!childDoc.isTemplateDoc && !childDoc.isTemplateForField && !childDoc.PARAMS) ? undefined : containerDataDoc;
- return { layout: Doc.expandTemplateLayout(childDoc, resolvedDataDoc, '(' + StrCast(containerDoc.PARAMS) + ')'), data: resolvedDataDoc };
+ const resolvedDataDoc = Doc.AreProtosEqual(containerDataDoc, containerDoc) || (!childDoc.isTemplateDoc && !childDoc.isTemplateForField) ? undefined : containerDataDoc;
+ return { layout: Doc.expandTemplateLayout(childDoc, resolvedDataDoc), data: resolvedDataDoc };
}
export function Overwrite(doc: Doc, overwrite: Doc, copyProto: boolean = false): Doc {
@@ -915,7 +964,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) {
@@ -935,47 +984,46 @@ export namespace Doc {
export function MakeCopy(doc: Doc, copyProto: boolean = false, copyProtoId?: string, retitle = false): Doc {
const copy = new Doc(copyProtoId, true);
- const exclude = Cast(doc.cloneFieldFilter, listSpec('string'), []);
- Object.keys(doc).forEach(key => {
- if (exclude.includes(key)) return;
- const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
- const field = ProxyField.WithoutProxy(() => doc[key]);
- if (key === 'proto' && copyProto) {
- if (doc[key] instanceof Doc) {
- copy[key] = Doc.MakeCopy(doc[key]!, false);
- }
- } else {
- 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
- : doc[key] // reference documents except remove documents that are expanded teplate fields
- : ObjectField.MakeCopy(field);
- } else if (field instanceof Promise) {
- debugger; //This shouldn't happend...
+ 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 {
- copy[key] = field;
+ 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;
+ }
}
- }
- });
- copy.author = Doc.CurrentUserEmail;
+ });
if (copyProto) {
- Doc.GetProto(copy).context = undefined;
- Doc.GetProto(copy).aliases = new List<Doc>([copy]);
+ Doc.GetProto(copy).embedContainer = undefined;
+ Doc.GetProto(copy).proto_embeddings = new List<Doc>([copy]);
} else {
- Doc.AddDocToList(Doc.GetProto(copy)[DataSym], 'aliases', copy);
+ Doc.AddDocToList(Doc.GetProto(copy)[DocData], 'proto_embeddings', copy);
}
- copy.context = undefined;
+ copy.embedContainer = undefined;
Doc.defaultAclPrivate && (copy['acl-Public'] = 'Not Shared');
if (retitle) {
copy.title = incrementTitleCopy(StrCast(copy.title));
}
- Doc.IsPrototype(copy) && Doc.AddDocToList(Doc.MyFileOrphans, undefined, copy);
+ Doc.AddFileOrphan(copy);
return copy;
}
@@ -985,14 +1033,16 @@ export namespace Doc {
if (doc) {
const delegate = new Doc(id, true);
delegate[Initializing] = true;
+ updateCachedAcls(delegate);
delegate.proto = doc;
delegate.author = Doc.CurrentUserEmail;
Object.keys(doc)
.filter(key => key.startsWith('acl'))
.forEach(key => (delegate[key] = doc[key]));
- if (!Doc.IsSystem(doc)) Doc.AddDocToList(doc[DataSym], 'aliases', 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;
@@ -1007,13 +1057,13 @@ export namespace Doc {
delegateProto[Initializing] = true;
delegateProto.proto = doc;
delegateProto.author = Doc.CurrentUserEmail;
- delegateProto.isPrototype = true;
+ delegateProto.isDataDoc = true;
title && (delegateProto.title = title);
const delegate = new Doc(id, true);
delegate[Initializing] = true;
delegate.proto = delegateProto;
delegate.author = Doc.CurrentUserEmail;
- Doc.AddDocToList(delegateProto[DataSym], 'aliases', delegate);
+ Doc.AddDocToList(delegateProto[DocData], 'proto_embeddings', delegate);
delegate[Initializing] = false;
delegateProto[Initializing] = false;
return delegate;
@@ -1025,9 +1075,9 @@ export namespace Doc {
const proto = new Doc();
proto.author = Doc.CurrentUserEmail;
const target = Doc.MakeDelegate(proto);
- const targetKey = StrCast(templateDoc.layoutKey, 'layout');
+ const targetKey = StrCast(templateDoc.layout_fieldKey, 'layout');
const applied = ApplyTemplateTo(templateDoc, target, targetKey, templateDoc.title + '(...' + _applyCount++ + ')');
- target.layoutKey = targetKey;
+ target.layout_fieldKey = targetKey;
applied && (Doc.GetProto(applied).type = templateDoc.type);
Doc.defaultAclPrivate && (applied['acl-Public'] = 'Not Shared');
return applied;
@@ -1115,9 +1165,9 @@ export namespace Doc {
BrushedDoc: ObservableMap<Doc, boolean> = new ObservableMap();
SearchMatchDoc: ObservableMap<Doc, { searchMatch: number }> = new ObservableMap();
}
- const brushManager = new DocBrush();
+ export const brushManager = new DocBrush();
- export class DocData {
+ export class UserDocData {
@observable _user_doc: Doc = undefined!;
@observable _sharing_doc: Doc = undefined!;
@observable _searchQuery: string = '';
@@ -1127,36 +1177,37 @@ export namespace Doc {
// 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;
+ return overrideLayout || doc[DocLayout] || doc;
}
export function SetLayout(doc: Doc, layout: Doc | string) {
- doc[StrCast(doc.layoutKey, 'layout')] = layout;
+ doc[StrCast(doc.layout_fieldKey, 'layout')] = layout;
}
export function LayoutField(doc: Doc) {
- return doc[StrCast(doc.layoutKey, 'layout')];
+ return doc[StrCast(doc.layout_fieldKey, 'layout')];
}
export function LayoutFieldKey(doc: Doc): string {
- return StrCast(Doc.Layout(doc).layout).split("'")[1];
+ return StrCast(Doc.Layout(doc).layout).split("'")[1]; // bcz: TODO check on this . used to always reference 'layout', now it uses the layout speicfied by the current layout_fieldKey
}
export function NativeAspect(doc: Doc, dataDoc?: Doc, useDim?: boolean) {
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) {
- const dheight = doc ? NumCast((dataDoc || doc)[Doc.LayoutFieldKey(doc) + '-nativeHeight'], useHeight ? doc[HeightSym]() : 0) : 0;
- const nheight = doc ? (Doc.NativeWidth(doc, dataDoc, useHeight) * doc[HeightSym]()) / doc[WidthSym]() : 0;
- return !doc ? 0 : NumCast(doc._nativeHeight, nheight || dheight);
+ if (!doc) return 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) {
- doc[(fieldKey ?? Doc.LayoutFieldKey(doc)) + '-nativeWidth'] = width;
+ doc[(fieldKey ?? Doc.LayoutFieldKey(doc)) + '_nativeWidth'] = width;
}
export function SetNativeHeight(doc: Doc, height: number | undefined, fieldKey?: string) {
- doc[(fieldKey ?? Doc.LayoutFieldKey(doc)) + '-nativeHeight'] = height;
+ doc[(fieldKey ?? Doc.LayoutFieldKey(doc)) + '_nativeHeight'] = height;
}
- const manager = new DocData();
+ const manager = new UserDocData();
export function SearchQuery(): string {
return manager._searchQuery;
}
@@ -1224,8 +1275,8 @@ export namespace Doc {
const lastBrushed = Array.from(brushManager.BrushedDoc.keys()).lastElement();
if (lastBrushed) {
for (const link of LinkManager.Instance.getAllDirectLinks(lastBrushed)) {
- const a1 = Cast(link.anchor1, Doc, null);
- const a2 = Cast(link.anchor2, Doc, null);
+ const a1 = Cast(link.link_anchor_1, Doc, null);
+ const a2 = Cast(link.link_anchor_2, Doc, null);
if (Doc.AreProtosEqual(a1, doc) || Doc.AreProtosEqual(a2, doc) || Doc.AreProtosEqual(Cast(a1.annotationOn, Doc, null), doc) || Doc.AreProtosEqual(Cast(a2.annotationOn, Doc, null), doc)) {
return DocBrushStatus.linkHighlighted;
}
@@ -1257,54 +1308,66 @@ export namespace Doc {
}
export function LinkEndpoint(linkDoc: Doc, anchorDoc: Doc) {
- return Doc.AreProtosEqual(anchorDoc, (linkDoc.anchor1 as Doc).annotationOn as Doc) || Doc.AreProtosEqual(anchorDoc, linkDoc.anchor1 as Doc) ? '1' : '2';
+ if (linkDoc.link_anchor_2 === anchorDoc || (linkDoc.link_anchor_2 as Doc).annotationOn) 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';
}
export function linkFollowUnhighlight() {
- Doc.UnhighlightAll();
+ clearTimeout(UnhighlightTimer);
+ UnhighlightWatchers.forEach(watcher => watcher());
+ UnhighlightWatchers.length = 0;
+ highlightedDocs.forEach(doc => Doc.UnHighlightDoc(doc));
document.removeEventListener('pointerdown', linkFollowUnhighlight);
}
- let _lastDate = 0;
- export function linkFollowHighlight(destDoc: Doc | Doc[], dataAndDisplayDocs = true) {
+ let UnhighlightWatchers: (() => void)[] = [];
+ export let UnhighlightTimer: any;
+ export function AddUnHighlightWatcher(watcher: () => void) {
+ if (UnhighlightTimer) {
+ UnhighlightWatchers.push(watcher);
+ } else watcher();
+ }
+ export function linkFollowHighlight(destDoc: Doc | Doc[], dataAndDisplayDocs = true, presEffect?: Doc) {
linkFollowUnhighlight();
- (destDoc instanceof Doc ? [destDoc] : destDoc).forEach(doc => Doc.HighlightDoc(doc, dataAndDisplayDocs));
+ (destDoc instanceof Doc ? [destDoc] : destDoc).forEach(doc => Doc.HighlightDoc(doc, dataAndDisplayDocs, presEffect));
document.removeEventListener('pointerdown', linkFollowUnhighlight);
document.addEventListener('pointerdown', linkFollowUnhighlight);
- const lastDate = (_lastDate = Date.now());
- window.setTimeout(() => _lastDate === lastDate && linkFollowUnhighlight(), 5000);
+ if (UnhighlightTimer) clearTimeout(UnhighlightTimer);
+ UnhighlightTimer = window.setTimeout(() => {
+ linkFollowUnhighlight();
+ UnhighlightTimer = 0;
+ }, 5000);
}
- export class HighlightBrush {
- @observable HighlightedDoc: Map<Doc, boolean> = new Map();
- }
- const highlightManager = new HighlightBrush();
+ 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 highlightManager.HighlightedDoc.get(doc) || highlightManager.HighlightedDoc.get(Doc.GetProto(doc));
+ return doc[Highlight] || Doc.GetProto(doc)[Highlight];
}
- export function HighlightDoc(doc: Doc, dataAndDisplayDocs = true) {
+ export function HighlightDoc(doc: Doc, dataAndDisplayDocs = true, presEffect?: Doc) {
runInAction(() => {
- highlightManager.HighlightedDoc.set(doc, true);
- dataAndDisplayDocs && highlightManager.HighlightedDoc.set(Doc.GetProto(doc), true);
+ highlightedDocs.add(doc);
+ doc[Highlight] = true;
+ doc[Animation] = presEffect;
+ if (dataAndDisplayDocs) {
+ highlightedDocs.add(Doc.GetProto(doc));
+ Doc.GetProto(doc)[Highlight] = true;
+ }
});
}
- export function UnHighlightDoc(doc: Doc) {
+ /// if doc is defined, then it is unhighlighted, otherwise all highlighted docs are unhighlighted
+ export function UnHighlightDoc(doc?: Doc) {
runInAction(() => {
- highlightManager.HighlightedDoc.set(doc, false);
- highlightManager.HighlightedDoc.set(Doc.GetProto(doc), false);
+ (doc ? [doc] : Array.from(highlightedDocs)).forEach(doc => {
+ highlightedDocs.delete(doc);
+ highlightedDocs.delete(Doc.GetProto(doc));
+ doc[Highlight] = Doc.GetProto(doc)[Highlight] = false;
+ doc[Animation] = undefined;
+ });
});
}
- export function UnhighlightAll() {
- const mapEntries = highlightManager.HighlightedDoc.keys();
- let docEntry: IteratorResult<Doc>;
- while (!(docEntry = mapEntries.next()).done) {
- const targetDoc = docEntry.value;
- targetDoc && Doc.UnHighlightDoc(targetDoc);
- }
- }
export function UnBrushAllDocs() {
- brushManager.BrushedDoc.clear();
+ runInAction(() => brushManager.BrushedDoc.clear());
}
export function getDocTemplate(doc?: Doc) {
@@ -1329,7 +1392,7 @@ export namespace Doc {
if (typeof value === 'string') {
value = value.replace(`,${Utils.noRecursionHack}`, '');
}
- const fieldVal = key === '#' ? (StrCast(doc.tags).includes(':#' + value + ':') ? StrCast(doc.tags) : undefined) : doc[key];
+ const fieldVal = doc[key];
if (Cast(fieldVal, listSpec('string'), []).length) {
const vals = Cast(fieldVal, listSpec('string'), []);
const docs = vals.some(v => (v as any) instanceof Doc);
@@ -1337,70 +1400,70 @@ export namespace Doc {
return vals.some(v => v.includes(value)); // bcz: arghh: Todo: comparison should be parameterized as exact, or substring
}
const fieldStr = Field.toString(fieldVal as Field);
- return fieldStr.includes(value); // bcz: arghh: Todo: comparison should be parameterized as exact, or substring
+ return fieldStr.includes(value) || (value === String.fromCharCode(127) + '--undefined--' && fieldVal === undefined); // bcz: arghh: Todo: comparison should be parameterized as exact, or substring
}
export function deiconifyView(doc: Doc) {
- StrCast(doc.layoutKey).split('_')[1] === 'icon' && setNativeView(doc);
+ StrCast(doc.layout_fieldKey).split('_')[1] === 'icon' && setNativeView(doc);
}
export function setNativeView(doc: any) {
- const prevLayout = StrCast(doc.layoutKey).split('_')[1];
+ const prevLayout = StrCast(doc.layout_fieldKey).split('_')[1];
const deiconify = prevLayout === 'icon' && StrCast(doc.deiconifyLayout) ? 'layout_' + StrCast(doc.deiconifyLayout) : '';
prevLayout === 'icon' && (doc.deiconifyLayout = undefined);
- doc.layoutKey = deiconify || 'layout';
+ doc.layout_fieldKey = deiconify || 'layout';
}
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);
}
}
// 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, fieldSuffix?: string, append: boolean = true) {
+ 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 = '_' + (fieldSuffix ? fieldSuffix + '-' : '') + '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
- if (fields[0] === key && (fields[1] === value || modifiers === 'match')) {
+ for (let i = 0; i < childFilters.length; i++) {
+ const fields = childFilters[i].split(':'); // 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 + ':' + value + ':' + 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])];
}
}
}
@@ -1412,18 +1475,18 @@ export namespace Doc {
export function toggleNativeDimensions(layoutDoc: Doc, contentScale: number, panelWidth: number, panelHeight: number) {
runInAction(() => {
if (Doc.NativeWidth(layoutDoc) || Doc.NativeHeight(layoutDoc)) {
- layoutDoc._viewScale = NumCast(layoutDoc._viewScale, 1) * contentScale;
+ layoutDoc._freeform_scale = NumCast(layoutDoc._freeform_scale, 1) * contentScale;
layoutDoc._nativeWidth = undefined;
layoutDoc._nativeHeight = undefined;
- layoutDoc._forceReflow = undefined;
+ layoutDoc._layout_forceReflow = undefined;
layoutDoc._nativeHeightUnfrozen = undefined;
layoutDoc._nativeDimModifiable = undefined;
} else {
- layoutDoc._autoHeight = false;
+ layoutDoc._layout_autoHeight = false;
if (!Doc.NativeWidth(layoutDoc)) {
layoutDoc._nativeWidth = NumCast(layoutDoc._width, panelWidth);
layoutDoc._nativeHeight = NumCast(layoutDoc._height, panelHeight);
- layoutDoc._forceReflow = true;
+ layoutDoc._layout_forceReflow = true;
layoutDoc._nativeHeightUnfrozen = true;
layoutDoc._nativeDimModifiable = true;
}
@@ -1451,15 +1514,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';
@@ -1478,16 +1559,26 @@ export namespace Doc {
}
}
- export async function importDocument(file: File) {
+ ///
+ // imports a previously exported zip file which contains a set of documents and their assets (eg, images, videos)
+ // the 'remap' parameter determines whether the ids of the documents loaded should be kept as they were, or remapped to new ids
+ // If they are not remapped, loading the file will overwrite any existing documents with those ids
+ //
+ export async function importDocument(file: File, remap = false) {
const upload = Utils.prepend('/uploadDoc');
const formData = new FormData();
if (file) {
formData.append('file', file);
- formData.append('remap', 'true');
+ formData.append('remap', remap.toString());
const response = await fetch(upload, { method: 'POST', body: formData });
const json = await response.json();
if (json !== 'error') {
- const doc = await DocServer.GetRefField(json);
+ const docs = await DocServer.GetRefFields(json.docids as string[]);
+ const doc = DocCast(await DocServer.GetRefField(json.id));
+ const links = await DocServer.GetRefFields(json.linkids as string[]);
+ Array.from(Object.keys(links))
+ .map(key => links[key])
+ .forEach(link => link instanceof Doc && LinkManager.Instance.addLink(link));
return doc;
}
}
@@ -1635,11 +1726,14 @@ export namespace Doc {
}
}
+export function IdToDoc(id: string) {
+ return DocCast(DocServer.GetCachedRefField(id));
+}
ScriptingGlobals.add(function idToDoc(id: string): any {
- return DocServer.GetCachedRefField(id);
+ return IdToDoc(id);
});
-ScriptingGlobals.add(function renameAlias(doc: any) {
- return StrCast(Doc.GetProto(doc).title).replace(/\([0-9]*\)/, '') + `(${doc.aliasNumber})`;
+ScriptingGlobals.add(function renameEmbedding(doc: any) {
+ return StrCast(Doc.GetProto(doc).title).replace(/\([0-9]*\)/, '') + `(${doc.proto_embeddingId})`;
});
ScriptingGlobals.add(function getProto(doc: any) {
return Doc.GetProto(doc);
@@ -1647,8 +1741,8 @@ ScriptingGlobals.add(function getProto(doc: any) {
ScriptingGlobals.add(function getDocTemplate(doc?: any) {
return Doc.getDocTemplate(doc);
});
-ScriptingGlobals.add(function getAlias(doc: any) {
- return Doc.MakeAlias(doc);
+ScriptingGlobals.add(function getEmbedding(doc: any) {
+ return Doc.MakeEmbedding(doc);
});
ScriptingGlobals.add(function getCopy(doc: any, copyProto: any) {
return doc.isTemplateDoc ? Doc.ApplyTemplate(doc) : Doc.MakeCopy(doc, copyProto);
@@ -1672,6 +1766,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
new file mode 100644
index 000000000..65decc147
--- /dev/null
+++ b/src/fields/DocSymbols.ts
@@ -0,0 +1,26 @@
+export const Update = Symbol('DocUpdate');
+export const Self = Symbol('DocSelf');
+export const SelfProxy = Symbol('DocSelfProxy');
+export const FieldKeys = Symbol('DocFieldKeys');
+export const FieldTuples = Symbol('DocFieldTuples');
+export const Width = Symbol('DocWidth');
+export const Height = Symbol('DocHeight');
+export const Animation = Symbol('DocAnimation');
+export const Highlight = Symbol('DocHighlight');
+export const DocData = Symbol('DocData');
+export const DocLayout = Symbol('DocLayout');
+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');
+export const AclSelfEdit = Symbol('DocAclSelfEdit');
+export const AclEdit = Symbol('DocAclEdit');
+export const AclAdmin = Symbol('DocAclAdmin');
+export const UpdatingFromServer = Symbol('DocUpdatingFromServer');
+export const Initializing = Symbol('DocInitializing');
+export const ForceServerWrite = Symbol('DocForceServerWrite');
+export const CachedUpdates = Symbol('DocCachedUpdates');
diff --git a/src/fields/FieldLoader.scss b/src/fields/FieldLoader.scss
new file mode 100644
index 000000000..9a23c3e4f
--- /dev/null
+++ b/src/fields/FieldLoader.scss
@@ -0,0 +1,15 @@
+.fieldLoader {
+ z-index: 10000;
+ width: 200px;
+ height: 30;
+ background: lightblue;
+ position: absolute;
+ left: calc(50% - 99px);
+ top: calc(50% + 110px);
+ display: flex;
+ align-items: center;
+ padding: 20px;
+ margin: auto;
+ display: 'block';
+ box-shadow: darkslategrey 0.2vw 0.2vw 0.8vw;
+}
diff --git a/src/fields/FieldLoader.tsx b/src/fields/FieldLoader.tsx
new file mode 100644
index 000000000..2a7b936f7
--- /dev/null
+++ b/src/fields/FieldLoader.tsx
@@ -0,0 +1,15 @@
+import { observable } from 'mobx';
+import { observer } from 'mobx-react';
+
+import * as React from 'react';
+import './FieldLoader.scss';
+
+@observer
+export class FieldLoader extends React.Component {
+ @observable public static ServerLoadStatus = { requested: 0, retrieved: 0 };
+ public static active = false;
+
+ render() {
+ return <div className="fieldLoader">{`Requested: ${FieldLoader.ServerLoadStatus.requested} ... ${FieldLoader.ServerLoadStatus.retrieved} `}</div>;
+ }
+}
diff --git a/src/fields/FieldSymbols.ts b/src/fields/FieldSymbols.ts
index 8d040f493..c381f14f5 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 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 OnUpdate = Symbol('FieldOnUpdate');
+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/InkField.ts b/src/fields/InkField.ts
index a074098c1..22bea3927 100644
--- a/src/fields/InkField.ts
+++ b/src/fields/InkField.ts
@@ -1,5 +1,5 @@
import { Bezier } from 'bezier-js';
-import { createSimpleSchema, list, object, serializable } from 'serializr';
+import { alias, createSimpleSchema, list, object, serializable } from 'serializr';
import { ScriptingGlobals } from '../client/util/ScriptingGlobals';
import { Deserializable } from '../client/util/SerializationHelper';
import { Copy, ToScriptString, ToString } from './FieldSymbols';
@@ -64,7 +64,8 @@ const strokeDataSchema = createSimpleSchema({
@Deserializable('ink')
export class InkField extends ObjectField {
- @serializable(list(object(strokeDataSchema)))
+ public static InkDataFieldName = '__inkData';
+ @serializable(alias(InkField.InkDataFieldName, list(object(strokeDataSchema))))
readonly inkData: InkData;
constructor(data: InkData) {
diff --git a/src/fields/List.ts b/src/fields/List.ts
index edaa16003..033fa569b 100644
--- a/src/fields/List.ts
+++ b/src/fields/List.ts
@@ -3,8 +3,9 @@ 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 { FieldTuples, Self, SelfProxy, Update } from './DocSymbols';
import { Field } from './Doc';
-import { Copy, OnUpdate, Parent, Self, SelfProxy, ToScriptString, ToString, Update } from './FieldSymbols';
+import { Copy, OnUpdate, Parent, ToScriptString, ToString } from './FieldSymbols';
import { ObjectField } from './ObjectField';
import { ProxyField } from './Proxy';
import { RefField } from './RefField';
@@ -21,12 +22,12 @@ const listHandlers: any = {
if (value instanceof RefField) {
throw new Error('fill with RefFields not supported yet');
}
- const res = this[Self].__fields.fill(value, start, end);
+ const res = this[Self].__fieldTuples.fill(value, start, end);
this[Update]();
return res;
},
pop(): any {
- const field = toRealField(this[Self].__fields.pop());
+ const field = toRealField(this[Self].__fieldTuples.pop());
this[Update]();
return field;
},
@@ -34,7 +35,7 @@ const listHandlers: any = {
items = items.map(toObjectField);
const list = this[Self];
- const length = list.__fields.length;
+ 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
@@ -43,23 +44,23 @@ const listHandlers: any = {
item[OnUpdate] = updateFunction(list, i + length, item, this);
}
}
- const res = list.__fields.push(...items);
+ const res = list.__fieldTuples.push(...items);
this[Update]({ op: '$addToSet', items, length: length + items.length });
return res;
}),
reverse() {
- const res = this[Self].__fields.reverse();
+ const res = this[Self].__fieldTuples.reverse();
this[Update]();
return res;
},
shift() {
- const res = toRealField(this[Self].__fields.shift());
+ const res = toRealField(this[Self].__fieldTuples.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);
+ const res = this[Self].__fieldTuples.sort(cmpFunc ? (first: any, second: any) => cmpFunc(toRealField(first), toRealField(second)) : undefined);
this[Update]();
return res;
},
@@ -67,7 +68,7 @@ const listHandlers: 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);
+ 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
@@ -77,12 +78,18 @@ const listHandlers: any = {
item[OnUpdate] = updateFunction(list, i + start, item, this);
}
}
- const res = list.__fields.splice(start, deleteCount, ...items);
+ 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[Update](
items.length === 0 && deleteCount
- ? { op: '$remFromSet', items: removed, length: list.__fields.length }
- : items.length && !deleteCount && start === list.__fields.length
- ? { op: '$addToSet', items, length: list.__fields.length }
+ ? { op: '$remFromSet', items: removed, hint: { start: start, deleteCount: deleteCount }, length: list.__fieldTuples.length }
+ : items.length && !deleteCount && start === list.__fieldTuples.length
+ ? { op: '$addToSet', items, length: list.__fieldTuples.length }
: undefined
);
return res.map(toRealField);
@@ -90,7 +97,7 @@ const listHandlers: any = {
unshift(...items: any[]) {
items = items.map(toObjectField);
const list = this[Self];
- const length = list.__fields.length;
+ 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
@@ -100,43 +107,46 @@ const listHandlers: any = {
item[OnUpdate] = updateFunction(list, i, item, this);
}
}
- const res = this[Self].__fields.unshift(...items);
+ const res = this[Self].__fieldTuples.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);
+ 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].__fields.includes(valueToFind, fromIndex);
+ return this[Self].__fieldTuples.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);
+ return this[Self].__fieldTuples.indexOf(valueToFind, fromIndex);
}
},
join(separator: any) {
this[Self].__realFields();
- return this[Self].__fields.map(toRealField).join(separator);
+ 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].__fields.lastIndexOf(valueToFind, fromIndex);
+ return this[Self].__fieldTuples.lastIndexOf(valueToFind, fromIndex);
}
},
slice(begin: number, end: number) {
this[Self].__realFields();
- return this[Self].__fields.slice(begin, end).map(toRealField);
+ return this[Self].__fieldTuples.slice(begin, end).map(toRealField);
},
/// Iteration methods
@@ -147,55 +157,55 @@ const listHandlers: 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);
+ // 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].__fields.filter((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
+ // 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].__fields.find((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
+ // 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].__fields.findIndex((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
+ // 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].__fields.forEach((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
+ // 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].__fields.map((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
+ // 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].__fields.reduce((acc:any, element:any, index:number, array:any) => callback(acc, toRealField(element), index, array), initialValue);
+ // 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].__fields.reduceRight((acc:any, element:any, index:number, array:any) => callback(acc, toRealField(element), index, array), initialValue);
+ // 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].__fields.some((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
+ // return this[Self].__fieldTuples.some((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
},
values() {
return this[Self].__realFields().values();
@@ -210,10 +220,10 @@ function toObjectField(field: Field) {
}
function toRealField(field: Field) {
- return field instanceof ProxyField ? field.value() : field;
+ return field instanceof ProxyField ? field.value : field;
}
-function listGetter(target: any, prop: string | number | symbol, receiver: any): any {
+function listGetter(target: any, prop: string | symbol, receiver: any): any {
if (listHandlers.hasOwnProperty(prop)) {
return listHandlers[prop];
}
@@ -237,6 +247,7 @@ type ListUpdate<T> = ListSpliceUpdate<T> | ListIndexUpdate<T>;
type StoredType<T extends Field> = T extends RefField ? ProxyField<T> : T;
+export const ListFieldName = 'fields';
@Deserializable('list')
class ListImpl<T extends Field> extends ObjectField {
constructor(fields?: T[]) {
@@ -244,9 +255,9 @@ class ListImpl<T extends Field> extends ObjectField {
const list = new Proxy<this>(this, {
set: setter,
get: listGetter,
- ownKeys: target => Object.keys(target.__fields),
+ 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,
@@ -271,36 +282,28 @@ 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 promised = this.__fields.filter(f => f instanceof ProxyField && f.promisedValue()).map(f => ({ field: f as any, promisedFieldId: f instanceof ProxyField ? f.promisedValue() : '' }));
+ 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 (promised.length) {
- const batchPromise = DocServer.GetRefFields(promised.map(p => p.promisedFieldId));
+ if (unrequested.length) {
+ const batchPromise = DocServer.GetRefFields(unrequested.map(p => p.fieldId));
// as soon as we get the fields from the server, set all the list values in one
// action to generate one React dom update.
- batchPromise.then(
- action(pfields => {
- for (let i = 0; i < promised.length; i++) {
- promised[i].field.setValue(pfields[promised[i].promisedFieldId]);
- }
- })
- );
+ const allSetPromise = batchPromise.then(action(pfields => unrequested.map(toReq => toReq.setValue(pfields[toReq.fieldId]))));
// we also have to mark all lists items with this promise so that any calls to them
// will await the batch request and return the requested field value.
- // This assumes the handler for 'promise' in the call above being invoked before the
- // handler for 'promise' in the lines below.
- promised.forEach(p => p.field.setPromise(batchPromise.then(pfields => pfields[p.promisedFieldId])));
+ unrequested.forEach(p => p.setExternalValuePromise(allSetPromise));
}
- return this.__fields.map(toRealField);
+ return this[FieldTuples].map(toRealField);
}
- @serializable(alias('fields', list(autoObject(), { afterDeserialize: afterDocDeserialize })))
- private get __fields() {
- return this.___fields;
+ @serializable(alias(ListFieldName, list(autoObject(), { afterDeserialize: afterDocDeserialize })))
+ 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) {
@@ -311,14 +314,14 @@ class ListImpl<T extends Field> extends ObjectField {
}
[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 [FieldTuples]: StoredType<T>[] = [];
private [Update] = (diff: any) => {
// console.log(diff);
diff --git a/src/fields/ListSpec.ts b/src/fields/ListSpec.ts
deleted file mode 100644
index e69de29bb..000000000
--- a/src/fields/ListSpec.ts
+++ /dev/null
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 e924ef7a3..c076f5fe1 100644
--- a/src/fields/Proxy.ts
+++ b/src/fields/Proxy.ts
@@ -1,17 +1,16 @@
import { Deserializable } from '../client/util/SerializationHelper';
-import { FieldWaiting } from './Doc';
+import { Field, FieldWaiting, Opt } from './Doc';
import { primitive, serializable } from 'serializr';
-import { observable, action, runInAction } from 'mobx';
+import { observable, action, runInAction, computed } from 'mobx';
import { DocServer } from '../client/DocServer';
import { RefField } from './RefField';
import { ObjectField } from './ObjectField';
-import { Id, Copy, ToScriptString, ToString } from './FieldSymbols';
+import { Id, Copy, ToScriptString, ToString, ToValue } from './FieldSymbols';
import { scriptingGlobal } from '../client/util/ScriptingGlobals';
-import { Plugins } from './util';
function deserializeProxy(field: any) {
- if (!field.cache) {
- field.cache = DocServer.GetCachedRefField(field.fieldId) as any;
+ if (!field.cache.field) {
+ field.cache = { field: DocServer.GetCachedRefField(field.fieldId) as any, p: undefined };
}
}
@Deserializable('proxy', deserializeProxy)
@@ -25,18 +24,22 @@ export class ProxyField<T extends RefField> extends ObjectField {
//this.cache = DocServer.GetCachedRefField(value) as any;
this.fieldId = value;
} else if (value) {
- this.cache = value;
+ this.cache = { field: value, p: undefined };
this.fieldId = value[Id];
}
}
+ [ToValue](doc: any) {
+ return ProxyField.toValue(this);
+ }
+
[Copy]() {
- if (this.cache) return new ProxyField<T>(this.cache);
+ if (this.cache.field) return new ProxyField<T>(this.cache.field);
return new ProxyField<T>(this.fieldId);
}
[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';
@@ -48,48 +51,40 @@ export class ProxyField<T extends RefField> extends ObjectField {
// This getter/setter and nested object thing is
// because mobx doesn't play well with observable proxies
@observable.ref
- private _cache: { readonly field: T | undefined } = { field: undefined };
- private get cache(): T | undefined {
- return this._cache.field;
+ private _cache: { readonly field: T | undefined; p: FieldWaiting<T> | undefined } = { field: undefined, p: undefined };
+ private get cache(): { field: T | undefined; p: FieldWaiting<T> | undefined } {
+ return this._cache;
}
- private set cache(field: T | undefined) {
- this._cache = { field };
+ private set cache(val: { field: T | undefined; p: FieldWaiting<T> | undefined }) {
+ runInAction(() => (this._cache = { ...val }));
}
private failed = false;
- private promise?: Promise<any>;
- @action
- value(): T | undefined | FieldWaiting<T> {
- if (this.cache) return this.cache;
+ @computed get value(): T | undefined | FieldWaiting<T> {
+ if (this.cache.field) return this.cache.field;
if (this.failed) return undefined;
- const cached = DocServer.GetCachedRefField(this.fieldId) as T;
- if (cached !== undefined) {
- this.cache = cached;
- } else if (!this.promise) {
- this.promise = DocServer.GetRefField(this.fieldId).then(
- action((field: any) => {
- this.promise = undefined;
- this.cache = field;
- this.failed = field === undefined;
- return field;
- })
- ) as FieldWaiting<T>;
+ this.cache.field = DocServer.GetCachedRefField(this.fieldId) as T;
+ if (!this.cache.field && !this.cache.p) {
+ this.cache = {
+ field: undefined,
+ p: DocServer.GetRefField(this.fieldId).then(val => this.setValue(val as T)) as FieldWaiting<T>,
+ };
}
- return cached ?? this.promise;
+ return this.cache.field ?? this.cache.p;
}
- promisedValue(): string {
- return !this.cache && !this.failed && !this.promise && !DocServer.GetCachedRefField(this.fieldId) ? this.fieldId : '';
+ @computed get needsRequesting(): boolean {
+ return !this.cache.field && !this.failed && !this._cache.p && !DocServer.GetCachedRefField(this.fieldId) ? true : false;
}
- setPromise(promise: any) {
- this.promise = promise;
+
+ setExternalValuePromise(externalValuePromise: Promise<any>) {
+ this.cache.p = externalValuePromise.then(() => this.value) as FieldWaiting<T>;
}
@action
- setValue(field: any) {
- this.promise = undefined;
- this.cache = field;
- if (field === undefined) this.failed = true;
+ setValue(field: Opt<T>) {
+ this.cache = { field, p: undefined };
+ this.failed = field === undefined;
return field;
}
}
@@ -113,17 +108,15 @@ export namespace ProxyField {
}
}
- export function initPlugin() {
- Plugins.addGetterPlugin((doc, _, value) => {
- if (useProxy && value instanceof ProxyField) {
- return { value: value.value() };
- }
- });
+ export function toValue(value: any) {
+ if (useProxy) {
+ return { value: value.value };
+ }
}
}
function prefetchValue(proxy: PrefetchProxy<RefField>) {
- return proxy.value() as any;
+ return proxy.value as any;
}
@scriptingGlobal
diff --git a/src/fields/RichTextField.ts b/src/fields/RichTextField.ts
index d7edd4266..3e75a071f 100644
--- a/src/fields/RichTextField.ts
+++ b/src/fields/RichTextField.ts
@@ -1,11 +1,11 @@
-import { serializable } from "serializr";
-import { scriptingGlobal } from "../client/util/ScriptingGlobals";
-import { Deserializable } from "../client/util/SerializationHelper";
-import { Copy, ToScriptString, ToString } from "./FieldSymbols";
-import { ObjectField } from "./ObjectField";
+import { serializable } from 'serializr';
+import { scriptingGlobal } from '../client/util/ScriptingGlobals';
+import { Deserializable } from '../client/util/SerializationHelper';
+import { Copy, ToScriptString, ToString } from './FieldSymbols';
+import { ObjectField } from './ObjectField';
@scriptingGlobal
-@Deserializable("RichTextField")
+@Deserializable('RichTextField')
export class RichTextField extends ObjectField {
@serializable(true)
readonly Data: string;
@@ -13,14 +13,14 @@ export class RichTextField extends ObjectField {
@serializable(true)
readonly Text: string;
- constructor(data: string, text: string = "") {
+ constructor(data: string, text: string = '') {
super();
this.Data = data;
this.Text = text;
}
Empty() {
- return !(this.Text || this.Data.toString().includes("dashField") || this.Data.toString().includes("align"));
+ return !(this.Text || this.Data.toString().includes('dashField') || this.Data.toString().includes('align'));
}
[Copy]() {
@@ -28,14 +28,16 @@ export class RichTextField extends ObjectField {
}
[ToScriptString]() {
- return `new RichTextField("${this.Data.replace(/"/g, "\\\"")}", "${this.Text}")`;
+ return `new RichTextField("${this.Data.replace(/"/g, '\\"')}", "${this.Text}")`;
}
[ToString]() {
return this.Text;
}
public static DashField(fieldKey: string) {
- return new RichTextField(`{"doc":{"type":"doc","content":[{"type":"paragraph","attrs":{"align":null,"color":null,"id":null,"indent":null,"inset":null,"lineSpacing":null,"paddingBottom":null,"paddingTop":null},"content":[{"type":"dashField","attrs":{"fieldKey":"${fieldKey}","docid":""}}]}]},"selection":{"type":"text","anchor":2,"head":2},"storedMarks":[]}`, "");
+ return new RichTextField(
+ `{"doc":{"type":"doc","content":[{"type":"paragraph","attrs":{"align":null,"color":null,"id":null,"indent":null,"inset":null,"lineSpacing":null,"paddingBottom":null,"paddingTop":null},"content":[{"type":"dashField","attrs":{"fieldKey":"${fieldKey}","docId":""}}]}]},"selection":{"type":"text","anchor":2,"head":2},"storedMarks":[]}`,
+ ''
+ );
}
-
-} \ No newline at end of file
+}
diff --git a/src/fields/RichTextUtils.ts b/src/fields/RichTextUtils.ts
index bf055cd8b..24cd078f2 100644
--- a/src/fields/RichTextUtils.ts
+++ b/src/fields/RichTextUtils.ts
@@ -264,18 +264,18 @@ export namespace RichTextUtils {
const imageNode = (schema: any, image: ImageTemplate, textNote: Doc) => {
const { url: src, width, agnostic } = image;
- let docid: string;
+ let docId: string;
const guid = Utils.GenerateDeterministicGuid(agnostic);
const backingDocId = StrCast(textNote[guid]);
if (!backingDocId) {
const backingDoc = Docs.Create.ImageDocument(agnostic, { _width: 300, _height: 300 });
DocUtils.makeCustomViewClicked(backingDoc, Docs.Create.FreeformDocument);
- docid = backingDoc[Id];
- textNote[guid] = docid;
+ docId = backingDoc[Id];
+ textNote[guid] = docId;
} else {
- docid = backingDocId;
+ docId = backingDocId;
}
- return schema.node('image', { src, agnostic, width, docid, float: null, location: 'add:right' });
+ return schema.node('image', { src, agnostic, width, docId, float: null, location: 'add:right' });
};
const textNode = (schema: any, run: docs_v1.Schema$TextRun) => {
@@ -395,11 +395,11 @@ export namespace RichTextUtils {
if (new RegExp(window.location.origin + delimiter).test(url) && !url.endsWith(alreadyShared)) {
const linkDoc = await DocServer.GetRefField(url.split(delimiter)[1]);
if (linkDoc instanceof Doc) {
- let exported = (await Cast(linkDoc.anchor2, Doc))!;
+ let exported = (await Cast(linkDoc.link_anchor_2, Doc))!;
if (!exported.customLayout) {
- exported = Doc.MakeAlias(exported);
+ exported = Doc.MakeEmbedding(exported);
DocUtils.makeCustomViewClicked(exported, Docs.Create.FreeformDocument);
- linkDoc.anchor2 = exported;
+ linkDoc.link_anchor_2 = exported;
}
url = Utils.shareUrl(exported[Id]);
}
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 0b51db70b..4b1855cb0 100644
--- a/src/fields/SchemaHeaderField.ts
+++ b/src/fields/SchemaHeaderField.ts
@@ -2,7 +2,7 @@ import { Deserializable } from '../client/util/SerializationHelper';
import { serializable, primitive } from 'serializr';
import { ObjectField } from './ObjectField';
import { Copy, ToScriptString, ToString, OnUpdate } from './FieldSymbols';
-import { scriptingGlobal } from '../client/util/ScriptingGlobals';
+import { scriptingGlobal, ScriptingGlobals } from '../client/util/ScriptingGlobals';
import { ColumnType } from '../client/views/collections/collectionSchema/CollectionSchemaView';
export const PastelSchemaPalette = new Map<string, string>([
@@ -115,9 +115,12 @@ export class SchemaHeaderField extends ObjectField {
}
[ToScriptString]() {
- return `header(${this.heading},${this.type},${this.width}})`;
+ return `schemaHeaderField("${this.heading}","${this.color}",${this.type},${this.width},${this.desc},${this.collapsed})`;
}
[ToString]() {
return `SchemaHeaderField`;
}
}
+ScriptingGlobals.add(function schemaHeaderField(heading: string, color: string, type: number, width: number, desc?: boolean, collapsed?: boolean) {
+ return new SchemaHeaderField(heading, color, type, width, desc, collapsed);
+});
diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts
index 4896c027d..2b8750714 100644
--- a/src/fields/ScriptField.ts
+++ b/src/fields/ScriptField.ts
@@ -6,11 +6,10 @@ import { scriptingGlobal, ScriptingGlobals } from '../client/util/ScriptingGloba
import { autoObject, Deserializable } from '../client/util/SerializationHelper';
import { numberRange } from '../Utils';
import { Doc, Field, Opt } from './Doc';
-import { Copy, Id, ToScriptString, ToString } from './FieldSymbols';
+import { Copy, Id, ToScriptString, ToString, ToValue } from './FieldSymbols';
import { List } from './List';
import { ObjectField } from './ObjectField';
import { Cast, StrCast } from './Types';
-import { Plugins } from './util';
function optional(propSchema: PropSchema) {
return custom(
@@ -104,30 +103,9 @@ export class ScriptField extends ObjectField {
}
this.rawscript = rawscript;
this.setterscript = setterscript;
- this.script = script ?? (CompileScript('false', { addReturn: true }) as CompiledScript);
+ this.script = script ?? ScriptField.GetScriptFieldCache('false:') ?? (CompileScript('false', { addReturn: true }) as CompiledScript);
}
- // init(callback: (res: Field) => any) {
- // const options = this.options!;
- // const keys = Object.keys(options.options.capturedIds);
- // Server.GetFields(keys).then(fields => {
- // let captured: { [name: string]: Field } = {};
- // keys.forEach(key => captured[options.options.capturedIds[key]] = fields[key]);
- // const opts: ScriptOptions = {
- // addReturn: options.options.addReturn,
- // params: options.options.params,
- // requiredType: options.options.requiredType,
- // capturedVariables: captured
- // };
- // const script = CompileScript(options.script, opts);
- // if (!script.compiled) {
- // throw new Error("Can't compile script");
- // }
- // this._script = script;
- // callback(this);
- // });
- // }
-
[Copy](): ObjectField {
return new ScriptField(this.script, this.setterscript, this.rawscript);
}
@@ -136,7 +114,7 @@ export class ScriptField extends ObjectField {
}
[ToScriptString]() {
- return 'script field';
+ return this.script.originalScript;
}
[ToString]() {
return this.script.originalScript;
@@ -173,19 +151,20 @@ export class ComputedField extends ScriptField {
_lastComputedResult: any;
//TODO maybe add an observable cache based on what is passed in for doc, considering there shouldn't really be that many possible values for doc
value = computedFn((doc: Doc) => this._valueOutsideReaction(doc));
- _valueOutsideReaction = (doc: Doc) => (this._lastComputedResult = this.script.run({ this: doc, self: Cast(doc.rootDocument, Doc, null) || doc, _last_: this._lastComputedResult, _readOnly_: true }, console.log).result);
+ _valueOutsideReaction = (doc: Doc) => (this._lastComputedResult = this.script.run({ this: doc, self: Cast(doc.rootDocument, Doc, null) ?? doc, value: '', _last_: this._lastComputedResult, _readOnly_: true }, console.log).result);
+ [ToValue](doc: Doc) {
+ return ComputedField.toValue(doc, this);
+ }
[Copy](): ObjectField {
return new ComputedField(this.script, this.setterscript, this.rawscript);
}
- public static MakeScript(script: string, params: object = {}) {
- const compiled = ScriptField.CompileScript(script, params, false);
- return compiled.compiled ? new ComputedField(compiled) : undefined;
- }
- public static MakeFunction(script: string, params: object = {}, capturedVariables?: { [name: string]: Doc | string | number | boolean }) {
- const compiled = ScriptField.CompileScript(script, params, true, capturedVariables);
- return compiled.compiled ? new ComputedField(compiled) : undefined;
+ public static MakeFunction(script: string, params: object = {}, capturedVariables?: { [name: string]: Doc | string | number | boolean }, setterscript?: string) {
+ const compiled = ScriptField.CompileScript(script, params, true, { value: '', ...capturedVariables });
+ const compiledsetter = setterscript ? ScriptField.CompileScript(setterscript, { ...params, value: 'any' }, false, capturedVariables) : undefined;
+ const compiledsetscript = compiledsetter?.compiled ? compiledsetter : undefined;
+ return compiled.compiled ? new ComputedField(compiled, compiledsetscript) : undefined;
}
public static MakeInterpolatedNumber(fieldKey: string, interpolatorKey: string, doc: Doc, curTimecode: number, defaultVal: Opt<number>) {
if (!doc[`${fieldKey}-indexed`]) {
@@ -208,14 +187,20 @@ export class ComputedField extends ScriptField {
return getField.compiled ? new ComputedField(getField, setField?.compiled ? setField : undefined) : undefined;
}
public static MakeInterpolatedDataField(fieldKey: string, interpolatorKey: string, doc: Doc, curTimecode: number) {
+ if (doc[`${fieldKey}`] instanceof List) return;
if (!doc[`${fieldKey}-indexed`]) {
const flist = new List<Field>(numberRange(curTimecode + 1).map(i => undefined) as any as Field[]);
flist[curTimecode] = Field.Copy(doc[fieldKey]);
doc[`${fieldKey}-indexed`] = flist;
}
const getField = ScriptField.CompileScript(`getIndexVal(self['${fieldKey}-indexed'], self.${interpolatorKey})`, {}, true, {});
- const setField = ScriptField.CompileScript(`setIndexVal(self['${fieldKey}-indexed'], self.${interpolatorKey}, value)`, { value: 'any' }, true, {});
- return getField.compiled ? new ComputedField(getField, setField?.compiled ? setField : undefined) : undefined;
+ const setField = ScriptField.CompileScript(
+ `{setIndexVal (self['${fieldKey}-indexed'], self.${interpolatorKey}, value); console.log(self["${fieldKey}-indexed"][self.${interpolatorKey}],self.data,self["${fieldKey}-indexed"]))}`,
+ { value: 'any' },
+ false,
+ {}
+ );
+ return (doc[`${fieldKey}`] = getField.compiled ? new ComputedField(getField, setField?.compiled ? setField : undefined) : undefined);
}
}
export namespace ComputedField {
@@ -239,12 +224,10 @@ export namespace ComputedField {
}
}
- export function initPlugin() {
- Plugins.addGetterPlugin((doc, _, value) => {
- if (useComputed && value instanceof ComputedField) {
- return { value: value._valueOutsideReaction(doc), shouldReturn: true };
- }
- });
+ export function toValue(doc: any, value: any) {
+ if (useComputed) {
+ return { value: value._valueOutsideReaction(doc) };
+ }
}
}
diff --git a/src/fields/Types.ts b/src/fields/Types.ts
index bf40a0d7b..251b1149d 100644
--- a/src/fields/Types.ts
+++ b/src/fields/Types.ts
@@ -1,48 +1,50 @@
-import { Field, Opt, FieldResult, Doc } from "./Doc";
-import { List } from "./List";
-import { RefField } from "./RefField";
-import { DateField } from "./DateField";
-import { ScriptField } from "./ScriptField";
-import { URLField, WebField, ImageField } from "./URLField";
-import { TextField } from "@material-ui/core";
-import { RichTextField } from "./RichTextField";
-
-export type ToType<T extends InterfaceValue> =
- T extends "string" ? string :
- T extends "number" ? number :
- T extends "boolean" ? boolean :
- T extends ListSpec<infer U> ? List<U> :
- // T extends { new(...args: any[]): infer R } ? (R | Promise<R>) : never;
- T extends DefaultFieldConstructor<infer _U> ? never :
- T extends { new(...args: any[]): List<Field> } ? never :
- T extends { new(...args: any[]): infer R } ? R :
- T extends (doc?: Doc) => infer R ? R : never;
-
-export type ToConstructor<T extends Field> =
- T extends string ? "string" :
- T extends number ? "number" :
- T extends boolean ? "boolean" :
- T extends List<infer U> ? ListSpec<U> :
- new (...args: any[]) => T;
+import { Field, Opt, FieldResult, Doc } 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';
+
+export type ToType<T extends InterfaceValue> = T extends 'string'
+ ? string
+ : T extends 'number'
+ ? number
+ : T extends 'boolean'
+ ? boolean
+ : T extends ListSpec<infer U>
+ ? List<U>
+ : // T extends { new(...args: any[]): infer R } ? (R | Promise<R>) : never;
+ T extends DefaultFieldConstructor<infer _U>
+ ? never
+ : T extends { new (...args: any[]): List<Field> }
+ ? never
+ : T extends { new (...args: any[]): infer R }
+ ? R
+ : T extends (doc?: Doc) => infer R
+ ? R
+ : never;
+
+export type ToConstructor<T extends Field> = T extends string ? 'string' : T extends number ? 'number' : T extends boolean ? 'boolean' : T extends List<infer U> ? ListSpec<U> : new (...args: any[]) => T;
export type ToInterface<T extends Interface> = {
- [P in Exclude<keyof T, "proto">]: T[P] extends DefaultFieldConstructor<infer F> ? Exclude<FieldResult<F>, undefined> : FieldResult<ToType<T[P]>>;
+ [P in Exclude<keyof T, 'proto'>]: T[P] extends DefaultFieldConstructor<infer F> ? Exclude<FieldResult<F>, undefined> : FieldResult<ToType<T[P]>>;
};
// type ListSpec<T extends Field[]> = { List: ToContructor<Head<T>> | ListSpec<Tail<T>> };
export type ListSpec<T extends Field> = { List: ToConstructor<T> };
export type DefaultFieldConstructor<T extends Field> = {
- type: ToConstructor<T>,
- defaultVal: T
+ type: ToConstructor<T>;
+ defaultVal: T;
};
// type ListType<U extends Field[]> = { 0: List<ListType<Tail<U>>>, 1: ToType<Head<U>> }[HasTail<U> extends true ? 0 : 1];
export type Head<T extends any[]> = T extends [any, ...any[]] ? T[0] : never;
-export type Tail<T extends any[]> =
- ((...t: T) => any) extends ((_: any, ...tail: infer TT) => any) ? TT : [];
-export type HasTail<T extends any[]> = T extends ([] | [any]) ? false : true;
+export type Tail<T extends any[]> = ((...t: T) => any) extends (_: any, ...tail: infer TT) => any ? TT : [];
+export type HasTail<T extends any[]> = T extends [] | [any] ? false : true;
export type InterfaceValue = ToConstructor<Field> | ListSpec<Field> | DefaultFieldConstructor<Field> | ((doc?: Doc) => any);
//TODO Allow you to optionally specify default values for schemas, which should then make that field not be partial
@@ -58,14 +60,14 @@ export function Cast<T extends CastCtor>(field: FieldResult, ctor: T): FieldResu
export function Cast<T extends CastCtor>(field: FieldResult, ctor: T, defaultVal: WithoutList<WithoutRefField<ToType<T>>> | null): WithoutList<ToType<T>>;
export function Cast<T extends CastCtor>(field: FieldResult, ctor: T, defaultVal?: ToType<T> | null): FieldResult<ToType<T>> | undefined {
if (field instanceof Promise) {
- return defaultVal === undefined ? field.then(f => Cast(f, ctor) as any) as any : defaultVal === null ? undefined : defaultVal;
+ return defaultVal === undefined ? (field.then(f => Cast(f, ctor) as any) as any) : defaultVal === null ? undefined : defaultVal;
}
if (field !== undefined && !(field instanceof Promise)) {
- if (typeof ctor === "string") {
+ if (typeof ctor === 'string') {
if (typeof field === ctor) {
return field as ToType<T>;
}
- } else if (typeof ctor === "object") {
+ } else if (typeof ctor === 'object') {
if (field instanceof List) {
return field as any;
}
@@ -77,19 +79,20 @@ export function Cast<T extends CastCtor>(field: FieldResult, ctor: T, defaultVal
}
export function DocCast(field: FieldResult, defaultVal?: Doc) {
- return Cast(field, Doc, null) ?? defaultVal;
+ const doc = Cast(field, Doc, null);
+ return doc && !(doc instanceof Promise) ? doc : (defaultVal as Doc);
}
export function NumCast(field: FieldResult, defaultVal: number | null = 0) {
- return Cast(field, "number", defaultVal);
+ return Cast(field, 'number', defaultVal);
}
-export function StrCast(field: FieldResult, defaultVal: string | null = "") {
- return Cast(field, "string", defaultVal);
+export function StrCast(field: FieldResult, defaultVal: string | null = '') {
+ return Cast(field, 'string', defaultVal);
}
export function BoolCast(field: FieldResult, defaultVal: boolean | null = false) {
- return Cast(field, "boolean", defaultVal);
+ return Cast(field, 'boolean', defaultVal);
}
export function DateCast(field: FieldResult) {
return Cast(field, DateField, null);
@@ -101,6 +104,9 @@ export function RTFCast(field: FieldResult) {
export function ScriptCast(field: FieldResult, defaultVal: ScriptField | null = null) {
return Cast(field, ScriptField, defaultVal);
}
+export function CsvCast(field: FieldResult, defaultVal: CsvField | null = null) {
+ return Cast(field, CsvField, defaultVal);
+}
export function WebCast(field: FieldResult, defaultVal: WebField | null = null) {
return Cast(field, WebField, defaultVal);
}
@@ -113,7 +119,7 @@ type WithoutList<T extends Field> = T extends List<infer R> ? (R extends RefFiel
export function FieldValue<T extends Field, U extends WithoutList<T>>(field: FieldResult<T>, defaultValue: U): WithoutList<T>;
export function FieldValue<T extends Field>(field: FieldResult<T>): Opt<T>;
export function FieldValue<T extends Field>(field: FieldResult<T>, defaultValue?: T): Opt<T> {
- return (field instanceof Promise || field === undefined) ? defaultValue : field;
+ return field instanceof Promise || field === undefined ? defaultValue : field;
}
export interface PromiseLike<T> {
@@ -121,5 +127,9 @@ export interface PromiseLike<T> {
}
export function PromiseValue<T extends Field>(field: FieldResult<T>): PromiseLike<Opt<T>> {
if (field instanceof Promise) return field as Promise<Opt<T>>;
- return { then(cb: ((field: Opt<T>) => void)) { return cb(field); } };
-} \ No newline at end of file
+ return {
+ then(cb: (field: Opt<T>) => void) {
+ return cb(field);
+ },
+ };
+}
diff --git a/src/fields/URLField.ts b/src/fields/URLField.ts
index 00c78e231..8ac20b1e5 100644
--- a/src/fields/URLField.ts
+++ b/src/fields/URLField.ts
@@ -54,9 +54,6 @@ export const nullAudio = 'https://actions.google.com/sounds/v1/alarms/beep_short
@Deserializable('audio')
export class AudioField extends URLField {}
@scriptingGlobal
-@Deserializable('recording')
-export class RecordingField extends URLField {}
-@scriptingGlobal
@Deserializable('image')
export class ImageField extends URLField {}
@scriptingGlobal
@@ -69,14 +66,8 @@ export class PdfField extends URLField {}
@Deserializable('web')
export class WebField extends URLField {}
@scriptingGlobal
-@Deserializable('map')
-export class MapField extends URLField {}
-@scriptingGlobal
@Deserializable('csv')
export class CsvField extends URLField {}
@scriptingGlobal
@Deserializable('youtube')
export class YoutubeField extends URLField {}
-@scriptingGlobal
-@Deserializable('webcam')
-export class WebCamField extends URLField {}
diff --git a/src/fields/documentSchemas.ts b/src/fields/documentSchemas.ts
index 24b5a359d..76b287be7 100644
--- a/src/fields/documentSchemas.ts
+++ b/src/fields/documentSchemas.ts
@@ -9,15 +9,15 @@ 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
- _curPage: 'number', // current page of a page based document
+ _layout_curPage: 'number', // current page of a page based document
_currentFrame: 'number', // current frame of a frame based collection (e.g., a progressive slide)
lastFrame: 'number', // last frame of a frame based collection (e.g., a progressive slide)
activeFrame: 'number', // the active frame of a frame based animated document
- _currentTimecode: 'number', // current play back time of a temporal document (video / audio)
+ _layout_currentTimecode: 'number', // current play back time of a temporal document (video / audio)
_timecodeToShow: 'number', // the time that a document should be displayed (e.g., time an annotation should be displayed on a video)
_timecodeToHIde: 'number', // the time that a document should be hidden
markers: listSpec(Doc), // list of markers for audio / video
@@ -25,52 +25,52 @@ export const documentSchema = createSchema({
y: 'number', // y coordinate when in a freeform view
z: 'number', // z "coordinate" - non-zero specifies the overlay layer of a freeformview
zIndex: 'number', // zIndex of a document in a freeform view
- _scrollTop: 'number', // scroll position of a scrollable document (pdf, text, web)
+ _layout_scrollTop: 'number', // scroll position of a scrollable document (pdf, text, web)
lat: 'number',
lng: 'number',
// appearance properties on the layout
'_backgroundGrid-spacing': 'number', // the size of the grid for collection views
- _autoHeight: 'boolean', // whether the height of the document should be computed automatically based on its contents
+ _layout_autoHeight: 'boolean', // whether the height of the document should be computed automatically based on its contents
_nativeWidth: 'number', // native width of document which determines how much document contents are scaled when the document's width is set
_nativeHeight: 'number', // "
_width: 'number', // width of document in its container's coordinate system
_height: 'number', // "
- _xPadding: 'number', // pixels of padding on left/right of collectionfreeformview contents when fitContentsToBox is set
- _yPadding: 'number', // pixels of padding on top/bottom of collectionfreeformview contents when fitContentsToBox is set
+ _xPadding: 'number', // pixels of padding on left/right of collectionfreeformview contents when freeform_fitContentsToBox is set
+ _yPadding: 'number', // pixels of padding on top/bottom of collectionfreeformview contents when freeform_fitContentsToBox is set
_xMargin: 'number', // margin added on left/right of most documents to add separation from their container
_yMargin: 'number', // margin added on top/bottom of most documents to add separation from their container
_overflow: 'string', // sets overflow behvavior for CollectionFreeForm views
- _showCaption: 'string', // whether editable caption text is overlayed at the bottom of the document
- _showTitle: 'string', // the fieldkey(s) whose contents should be displayed at the top of the document. separate multiple keys with ";". Use :hover suffix to indicate title should be shown on hover
+ _layout_showCaption: 'string', // whether editable caption text is overlayed at the bottom of the document
+ _layout_showTitle: 'string', // the fieldkey(s) whose contents should be displayed at the top of the document. separate multiple keys with ";". Use :hover suffix to indicate title should be shown on hover
_pivotField: 'string', // specifies which field key should be used as the timeline/pivot axis
_columnsFill: 'boolean', // whether documents in a stacking view column should be sized to fill the column
_columnsSort: 'string', // how a document should be sorted "ascending", "descending", undefined (none)
_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',
- _sidebarWidthPercent: 'string', // percent of text window width taken up by sidebar
+ // _columnHeaders: listSpec(SchemaHeaderField), // header descriptions for stacking/masonry
+ // _schemaHeaders: listSpec(SchemaHeaderField), // header descriptions for schema views
+ _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
- fitContentsToBox: 'boolean', // whether freeform view contents should be zoomed/panned to fill the area of the document view box
+ freeform_fitContentsToBox: 'boolean', // whether freeform view contents should be zoomed/panned to fill the area of the document view box
fontSize: 'string',
hidden: 'boolean', // whether a document should not be displayed
- isInkMask: 'boolean', // is the document a mask (ie, sits on top of other documents, has an unbounded width/height that is dark, and content uses 'hard-light' mix-blend-mode to let other documents pop through)
- layout: 'string', // this is the native layout string for the document. templates can be added using other fields and setting layoutKey below
- layoutKey: 'string', // holds the field key for the field that actually holds the current lyoat
+ stroke_isInkMask: 'boolean', // is the document a mask (ie, sits on top of other documents, has an unbounded width/height that is dark, and content uses 'hard-light' mix-blend-mode to let other documents pop through)
+ layout: 'string', // this is the native layout string for the document. templates can be added using other fields and setting layout_fieldKey below
+ layout_fieldKey: 'string', // holds the field key for the field that actually holds the current lyoat
letterSpacing: 'string',
opacity: 'number', // opacity of document
- strokeWidth: 'number',
- strokeBezier: 'number',
- strokeStartMarker: 'string',
- strokeEndMarker: 'string',
- strokeDash: 'string',
+ stroke_width: 'number',
+ stroke_bezier: 'number',
+ stroke_startMarker: 'string',
+ stroke_endMarker: 'string',
+ stroke_dash: 'string',
textTransform: 'string',
treeViewOpen: 'boolean', // flag denoting whether the documents sub-tree (contents) is visible or hidden
treeViewExpandedView: 'string', // name of field whose contents are being displayed as the document's subtree
@@ -84,24 +84,23 @@ export const documentSchema = createSchema({
onPointerDown: ScriptField, // script to run when document is clicked (can be overriden by an onClick prop)
onPointerUp: ScriptField, // script to run when document is clicked (can be overriden by an onClick prop)
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, inPlace, default, )
+ 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
- linkDisplay: 'boolean', // whether a link connection should be shown between link anchor endpoints.
- isInPlaceContainer: 'boolean', // whether the marked object will display addDocTab() calls that target "inPlace" destinations
- isLinkButton: 'boolean', // whether document functions as a link follow button to follow the first link on the document when clicked
+ 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
- displayArrow: '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 "alias", "copy", "move")
- targetDropAction: 'string', // allows the target of a drop event to specify the dropAction ("alias", "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 "alias" or "copy")
- removeDropProperties: listSpec('string'), // properties that should be removed from the alias/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 4a62a6a1f..e439768ee 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -1,42 +1,22 @@
-import { action, observable, runInAction, trace } from 'mobx';
+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 { CollectionDockingView } from '../client/views/collections/CollectionDockingView';
import { returnZero } from '../Utils';
import CursorField from './CursorField';
-import {
- AclAdmin,
- AclAugment,
- AclEdit,
- AclPrivate,
- AclReadonly,
- AclSelfEdit,
- AclSym,
- AclUnset,
- DataSym,
- Doc,
- DocListCast,
- DocListCastAsync,
- FieldResult,
- ForceServerWrite,
- HeightSym,
- Initializing,
- LayoutSym,
- updateCachedAcls,
- UpdatingFromServer,
- WidthSym,
-} from './Doc';
-import { Id, OnUpdate, Parent, Self, SelfProxy, Update } from './FieldSymbols';
+import { aclLevel, Doc, DocListCast, DocListCastAsync, HierarchyMapping, ReverseHierarchyMap, StrListCast, updateCachedAcls } from './Doc';
+import { AclAdmin, AclEdit, AclPrivate, AclSelfEdit, DocAcl, DocData, DocLayout, FieldKeys, ForceServerWrite, Height, Initializing, SelfProxy, Update, UpdatingFromServer, Width } from './DocSymbols';
+import { Id, OnUpdate, Parent, ToValue } from './FieldSymbols';
import { List } from './List';
import { ObjectField } from './ObjectField';
import { PrefetchProxy, ProxyField } from './Proxy';
import { RefField } from './RefField';
import { RichTextField } from './RichTextField';
import { SchemaHeaderField } from './SchemaHeaderField';
-import { ComputedField } from './ScriptField';
+import { ComputedField, ScriptField } from './ScriptField';
import { ScriptCast, StrCast } from './Types';
function _readOnlySetter(): never {
@@ -48,33 +28,16 @@ export function TraceMobx() {
tracing && trace();
}
-export interface GetterResult {
- value: FieldResult;
- shouldReturn?: boolean;
-}
-export type GetterPlugin = (receiver: any, prop: string | number, currentValue: any) => GetterResult | undefined;
-const getterPlugins: GetterPlugin[] = [];
-
-export namespace Plugins {
- export function addGetterPlugin(plugin: GetterPlugin) {
- getterPlugins.push(plugin);
- }
-}
-
const _setterImpl = action(function (target: any, prop: string | symbol | number, value: any, receiver: any): boolean {
- if (SerializationHelper.IsSerializing()) {
+ if (SerializationHelper.IsSerializing() || typeof prop === 'symbol') {
target[prop] = value;
return true;
}
- if (typeof prop === 'symbol') {
- target[prop] = value;
- return true;
- }
if (value !== undefined) {
value = value[SelfProxy] || value;
}
- const curValue = target.__fields[prop];
+ 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
@@ -83,6 +46,7 @@ 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");
@@ -105,30 +69,41 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number
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);
- // if (target.__fields[prop] !== value) {
- // console.log("ASSIGN " + prop + " " + value);
- // }
- 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 (typeof value === "object" && !(value instanceof ObjectField)) debugger;
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 } });
+ 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: () => (receiver[prop] = curValue),
- prop: prop?.toString(),
- });
+ 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(),
+ },
+ value
+ );
return true;
}
return false;
@@ -151,24 +126,17 @@ export function denormalizeEmail(email: string) {
return email.replace(/__/g, '.');
}
-// playground mode allows the user to add/delete documents or make layout changes without them saving to the server
-// let playgroundMode = false;
-
-// export function togglePlaygroundMode() {
-// playgroundMode = !playgroundMode;
-// }
-
/**
* 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);
- }
+ // 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);
+ // }
}
/**
@@ -183,8 +151,11 @@ export function inheritParentAcls(parent: Doc, child: Doc) {
* View: a user with view access to a document can only view it - they cannot add/remove/edit anything.
*
* None: the document is not shared with that user.
+ *
+ * Unset: Remove a sharing permission (eg., used )
*/
export enum SharingPermissions {
+ Unset = 'None',
Admin = 'Admin',
Edit = 'Edit',
SelfEdit = 'Self Edit',
@@ -204,22 +175,16 @@ 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;
- // authored documents are private until an ACL is set.
- if (!target[AclSym] && target.author && target.author !== Doc.CurrentUserEmail) return AclPrivate;
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) {
- if (prop === UpdatingFromServer || prop === Initializing || target[UpdatingFromServer] || prop === AclSym) return AclAdmin; // requesting the UpdatingFromServer prop or AclSym must always go through to keep the local DB consistent
+ 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);
}
-let HierarchyMapping: Map<symbol, number> | undefined;
-
let cachedGroups = observable([] as string[]);
-/// bcz; argh!! TODO; These do not belong here, but there were include order problems with leaving them in util.ts
-// need to investigate further what caused the mobx update problems and move to a better location.
const getCachedGroupByNameCache = computedFn(function (name: string) {
return cachedGroups.includes(name);
}, true);
@@ -230,43 +195,33 @@ export function SetCachedGroups(groups: string[]) {
runInAction(() => cachedGroups.push(...groups));
}
function getEffectiveAcl(target: any, user?: string): symbol {
- const targetAcls = target[AclSym];
- 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
- const targetAuthor = target.__fields?.author || target.author; // target may be a Doc of Proxy, so check __fields.author and .author
- if (userChecked === targetAuthor || !targetAuthor) return AclAdmin;
- if (GetCachedGroupByName('Admin')) return AclAdmin;
+ 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
if (targetAcls && Object.keys(targetAcls).length) {
- HierarchyMapping =
- HierarchyMapping ||
- new Map<symbol, number>([
- [AclPrivate, 0],
- [AclReadonly, 1],
- [AclAugment, 2],
- [AclSelfEdit, 2.5],
- [AclEdit, 3],
- [AclAdmin, 4],
- ]);
-
let effectiveAcl = AclPrivate;
for (const [key, value] of Object.entries(targetAcls)) {
// 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)! > HierarchyMapping.get(effectiveAcl)!) {
- if (GetCachedGroupByName(entity) || userChecked === entity) {
+ if (HierarchyMapping.get(value as symbol)!.level > HierarchyMapping.get(effectiveAcl)!.level) {
+ if (GetCachedGroupByName(entity) || userChecked === entity || entity === 'Me') {
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;
+ //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)! < 3 ? AclEdit : 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.__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;
}
/**
@@ -279,9 +234,9 @@ function getEffectiveAcl(target: any, user?: string): symbol {
*/
export function distributeAcls(key: string, acl: SharingPermissions, target: Doc, inheritingFromCollection?: boolean, visited?: Doc[], isDashboard?: boolean) {
if (!visited) visited = [] as Doc[];
- if (visited.includes(target)) return;
+ if (!target || visited.includes(target)) return;
- if ((target._viewType === CollectionViewType.Docking && visited.length > 1) || Doc.GetProto(visited[0]) !== Doc.GetProto(target)) {
+ if ((target._type_collection === 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...???)
@@ -291,21 +246,9 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc
}
visited.push(target);
- const HierarchyMapping = new Map<string, number>([
- ['Not Shared', 0],
- ['Can View', 1],
- ['Can Augment', 2],
- ['Self Edit', 2.5],
- ['Can Edit', 3],
- ['Admin', 4],
- ]);
-
let layoutDocChanged = false; // determines whether fetchProto should be called or not (i.e. is there a change that should be reflected in target[AclSym])
- let dataDocChanged = false;
- const dataDoc = target[DataSym];
-
// 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] || HierarchyMapping.get(StrCast(target[key]))! > HierarchyMapping.get(acl)!)) {
+ if (GetEffectiveAcl(target) === AclAdmin && (!inheritingFromCollection || !target[key] || ReverseHierarchyMap.get(StrCast(target[key]))!.level > ReverseHierarchyMap.get(acl)!.level)) {
target[key] = acl;
layoutDocChanged = true;
@@ -316,34 +259,27 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc
}
}
- if (dataDoc && (!inheritingFromCollection || !dataDoc[key] || HierarchyMapping.get(StrCast(dataDoc[key]))! > HierarchyMapping.get(acl)!)) {
+ let dataDocChanged = false;
+ const dataDoc = target[DocData];
+ if (dataDoc && (!inheritingFromCollection || !dataDoc[key] || ReverseHierarchyMap.get(StrCast(dataDoc[key]))! > ReverseHierarchyMap.get(acl)!)) {
if (GetEffectiveAcl(dataDoc) === AclAdmin) {
dataDoc[key] = acl;
dataDocChanged = true;
}
// maps over the links of the document
- const links = DocListCast(dataDoc.links);
- links.forEach(link => distributeAcls(key, acl, link, inheritingFromCollection, visited));
+ LinkManager.Links(dataDoc).forEach(link => distributeAcls(key, acl, link, inheritingFromCollection, visited));
// maps over the children of the document
- DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc) + (isDashboard ? '-all' : '')]).map(d => {
+ DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc)]).forEach(d => {
distributeAcls(key, acl, d, inheritingFromCollection, visited);
- // }
- const data = d[DataSym];
- if (data) {
- distributeAcls(key, acl, data, inheritingFromCollection, visited);
- }
+ distributeAcls(key, acl, d[DocData], inheritingFromCollection, visited);
});
// maps over the annotations of the document
- DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc) + '-annotations']).map(d => {
+ DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc) + '_annotations']).forEach(d => {
distributeAcls(key, acl, d, inheritingFromCollection, visited);
- // }
- const data = d[DataSym];
- if (data) {
- distributeAcls(key, acl, data, inheritingFromCollection, visited);
- }
+ distributeAcls(key, acl, d[DocData], inheritingFromCollection, visited);
});
}
@@ -353,80 +289,71 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc
export function setter(target: any, in_prop: string | symbol | number, value: any, receiver: any): boolean {
let prop = in_prop;
- const effectiveAcl = getPropAcl(target, 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;
// 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, 'None'].includes(value))) return true;
- // if (typeof prop === "string" && prop.startsWith("acl") && !["Can Edit", "Can Augment", "Can View", "Not Shared", undefined].includes(value)) return true;
+ 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 && 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);
}
-export function getter(target: any, in_prop: string | symbol | number, receiver: any): any {
- let prop = in_prop;
-
- if (in_prop === AclSym) return target[AclSym];
- if (in_prop === 'toString' || (in_prop !== HeightSym && in_prop !== WidthSym && in_prop !== LayoutSym && typeof prop === 'symbol')) return target.__fields[prop] || target[prop];
- if (GetEffectiveAcl(target) === AclPrivate) return prop === HeightSym || prop === WidthSym ? returnZero : undefined;
- if (prop === LayoutSym) return target.__LAYOUT__;
- if (typeof prop === 'string' && prop !== '__id' && prop !== '__fields' && prop.startsWith('_')) {
- if (!prop.startsWith('__')) prop = prop.substring(1);
- if (target.__LAYOUT__) return target.__LAYOUT__[prop];
+export function getter(target: any, prop: string | symbol, proxy: any): any {
+ // prettier-ignore
+ switch (prop) {
+ case 'then' : return undefined;
+ case 'constructor': case 'toString': case 'valueOf':
+ case 'serializeInfo': case 'factory':
+ return target[prop];
+ 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];
+ if (prop.startsWith('__')) return target[prop];
+ if (GetEffectiveAcl(target) === AclPrivate && prop !== 'author') return undefined;
}
- if (prop === 'then') {
- //If we're being awaited
- return undefined;
- }
- if (typeof prop === 'symbol') {
- return target.__fields[prop] || target[prop];
- }
- if (SerializationHelper.IsSerializing()) {
- return target[prop];
- }
- return getFieldImpl(target, prop, receiver);
+
+ const layout_prop = prop.startsWith('_') ? prop.substring(1) : undefined;
+ if (layout_prop && target.__LAYOUT__) return target.__LAYOUT__[layout_prop];
+ return getFieldImpl(target, layout_prop ?? prop, proxy);
}
-function getFieldImpl(target: any, prop: string | number, receiver: any, ignoreProto: boolean = false): any {
- receiver = receiver || target[SelfProxy];
- let field = target.__fields[prop];
- for (const plugin of getterPlugins) {
- const res = plugin(receiver, prop, field);
- if (res === undefined) continue;
- if (res.shouldReturn) {
- return res.value;
- } else {
- field = res.value;
- }
- }
+function getFieldImpl(target: any, prop: string | number, proxy: any, ignoreProto: boolean = false): any {
+ 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') {
- const proto = getFieldImpl(target, 'proto', receiver, true); //TODO tfs: instead of receiver we could use target[SelfProxy]... I don't which semantics we want or if it really matters
+ const proto = getFieldImpl(target, 'proto', proxy, true); //TODO tfs: instead of proxy we could use target[SelfProxy]... I don't which semantics we want or if it really matters
if (proto instanceof Doc && GetEffectiveAcl(proto) !== AclPrivate) {
- return getFieldImpl(proto[Self], prop, receiver, ignoreProto);
+ return getFieldImpl(proto, prop, proxy, ignoreProto);
}
- return undefined;
}
return field;
}
export function getField(target: any, prop: string | number, ignoreProto: boolean = false): any {
- return getFieldImpl(target, prop, undefined, ignoreProto);
+ return getFieldImpl(target, prop, target[SelfProxy], ignoreProto);
}
export function deleteProperty(target: any, prop: string | number | symbol) {
if (typeof prop === 'symbol') {
delete target[prop];
- return true;
+ } else {
+ target[SelfProxy][prop] = undefined;
}
- target[SelfProxy][prop] = undefined;
return true;
}
@@ -437,7 +364,7 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any
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)) } }
+ ? { $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>);
@@ -450,7 +377,8 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any
diff?.op === '$addToSet'
? {
redo: () => {
- receiver[prop].push(...diff.items.map((item: any) => (item.value ? item.value() : item)));
+ console.log('redo $add: ' + prop, diff.items); // bcz: uncomment to log undo
+ receiver[prop].push(...diff.items.map((item: any) => item.value ?? item));
lastValue = ObjectField.MakeCopy(receiver[prop]);
},
undo: action(() => {
@@ -460,19 +388,20 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any
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.value() : item);
+ const ind = receiver[prop].indexOf(item.value ?? item);
ind !== -1 && receiver[prop].splice(ind, 1);
}
});
lastValue = ObjectField.MakeCopy(receiver[prop]);
}),
- prop: '',
+ prop: 'add ' + diff.items.length + ' items to list',
}
: diff?.op === '$remFromSet'
? {
redo: action(() => {
+ console.log('redo $rem: ' + prop, diff.items); // bcz: uncomment to log undo
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.value() : item);
+ 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]);
@@ -484,16 +413,17 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any
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.value() : item);
- ind !== -1 && receiver[prop].indexOf(item.value ? item.value() : item) === -1 && receiver[prop].splice(ind, 0, item);
+ 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: '',
+ prop: 'remove ' + diff.items.length + ' items from list',
}
: {
redo: () => {
+ console.log('redo list: ' + prop, receiver[prop]); // bcz: uncomment to log undo
receiver[prop] = ObjectField.MakeCopy(newValue as List<any>);
lastValue = ObjectField.MakeCopy(receiver[prop]);
},
@@ -502,8 +432,9 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any
receiver[prop] = ObjectField.MakeCopy(prevValue as List<any>);
lastValue = ObjectField.MakeCopy(receiver[prop]);
},
- prop: '',
- }
+ prop: 'assign list',
+ },
+ diff?.items
);
}
target[Update](op);
diff --git a/src/mobile/AudioUpload.tsx b/src/mobile/AudioUpload.tsx
index 64baa351c..7a1dde9fb 100644
--- a/src/mobile/AudioUpload.tsx
+++ b/src/mobile/AudioUpload.tsx
@@ -13,13 +13,24 @@ import { listSpec } from '../fields/Schema';
import { Cast, FieldValue } from '../fields/Types';
import { nullAudio } from '../fields/URLField';
import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, emptyPath } from '../Utils';
-import "./ImageUpload.scss";
+import './ImageUpload.scss';
import { MobileInterface } from './MobileInterface';
import React = require('react');
@observer
export class AudioUpload extends React.Component {
- @observable public _audioCol: Doc = FieldValue(Cast(Docs.Create.FreeformDocument([Cast(Docs.Create.AudioDocument(nullAudio, { title: "mobile audio", _width: 500, _height: 100 }), Doc) as Doc], { title: "mobile audio", _width: 300, _height: 300, _fitContentsToBox: true, boxShadow: "0 0" }), Doc)) as Doc;
+ @observable public _audioCol: Doc = FieldValue(
+ Cast(
+ Docs.Create.FreeformDocument([Cast(Docs.Create.AudioDocument(nullAudio, { title: 'mobile audio', _width: 500, _height: 100 }), Doc) as Doc], {
+ title: 'mobile audio',
+ _width: 300,
+ _height: 300,
+ _layout_fitContentsToBox: true,
+ layout_boxShadow: '0 0',
+ }),
+ Doc
+ )
+ ) as Doc;
/**
* Handles the onclick functionality for the 'Restart' button
@@ -28,25 +39,36 @@ export class AudioUpload extends React.Component {
@action
clearUpload = () => {
for (let i = 1; i < 8; i++) {
- this.setOpacity(i, "0.2");
+ this.setOpacity(i, '0.2');
}
- this._audioCol = FieldValue(Cast(
- Docs.Create.FreeformDocument(
- [Cast(Docs.Create.AudioDocument(nullAudio, {
- title: "mobile audio",
- _width: 500,
- _height: 100
- }), Doc) as Doc], { title: "mobile audio", _width: 300, _height: 300, _fitContentsToBox: true, boxShadow: "0 0" }), Doc)) as Doc;
- }
+ this._audioCol = FieldValue(
+ Cast(
+ Docs.Create.FreeformDocument(
+ [
+ Cast(
+ Docs.Create.AudioDocument(nullAudio, {
+ title: 'mobile audio',
+ _width: 500,
+ _height: 100,
+ }),
+ Doc
+ ) as Doc,
+ ],
+ { title: 'mobile audio', _width: 300, _height: 300, _layout_fitContentsToBox: true, layout_boxShadow: '0 0' }
+ ),
+ Doc
+ )
+ ) as Doc;
+ };
- /**
+ /**
* Handles the onClick of the 'Close' button
* Reset upload interface and toggle audio
*/
closeUpload = () => {
this.clearUpload();
MobileInterface.Instance.toggleAudio();
- }
+ };
/**
* Handles the on click of the 'Upload' button.
@@ -57,14 +79,14 @@ export class AudioUpload extends React.Component {
const audioDoc = this._audioCol;
const data = Cast(audioRightSidebar.data, listSpec(Doc));
for (let i = 1; i < 8; i++) {
- setTimeout(() => this.setOpacity(i, "1"), i * 200);
+ setTimeout(() => this.setOpacity(i, '1'), i * 200);
}
if (data) {
data.push(audioDoc);
}
// Resets uploader after 3 seconds
setTimeout(this.clearUpload, 3000);
- }
+ };
// Returns the upload audio menu
private get uploadInterface() {
@@ -72,11 +94,13 @@ export class AudioUpload extends React.Component {
<>
<ContextMenu />
<DictationOverlay />
- <div style={{ display: "none" }}><RichTextMenu key="rich" /></div>
+ <div style={{ display: 'none' }}>
+ <RichTextMenu key="rich" />
+ </div>
<div className="closeUpload" onClick={() => this.closeUpload()}>
- <FontAwesomeIcon icon="window-close" size={"lg"} />
+ <FontAwesomeIcon icon="window-close" size={'lg'} />
</div>
- <FontAwesomeIcon icon="microphone" size="lg" style={{ fontSize: "130" }} />
+ <FontAwesomeIcon icon="microphone" size="lg" style={{ fontSize: '130' }} />
<div className="audioUpload_cont">
<DocumentView
Document={this._audioCol}
@@ -86,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}
@@ -96,12 +120,10 @@ export class AudioUpload extends React.Component {
isDocumentActive={returnTrue}
isContentActive={emptyFunction}
focus={emptyFunction}
- styleProvider={() => "rgba(0,0,0,0)"}
+ styleProvider={() => 'rgba(0,0,0,0)'}
docViewPath={returnEmptyDoclist}
whenChildContentsActiveChanged={emptyFunction}
bringToFront={emptyFunction}
- ContainingCollectionView={undefined}
- ContainingCollectionDoc={undefined}
/>
</div>
<div className="restart_label" onClick={this.clearUpload}>
@@ -125,28 +147,16 @@ export class AudioUpload extends React.Component {
// Handles the setting of the loading bar
setOpacity = (index: number, opacity: string) => {
- const slab = document.getElementById("slab0" + index);
+ const slab = document.getElementById('slab0' + index);
if (slab) {
slab.style.opacity = opacity;
}
- }
+ };
@observable private dialogueBoxOpacity = 1;
@observable private overlayOpacity = 0.4;
render() {
- return (
- <MainViewModal
- contents={this.uploadInterface}
- isDisplayed={true}
- interactive={true}
- dialogueBoxDisplayedOpacity={this.dialogueBoxOpacity}
- overlayDisplayedOpacity={this.overlayOpacity}
- closeOnExternalClick={this.closeUpload}
- />
- );
+ return <MainViewModal contents={this.uploadInterface} isDisplayed={true} interactive={true} dialogueBoxDisplayedOpacity={this.dialogueBoxOpacity} overlayDisplayedOpacity={this.overlayOpacity} closeOnExternalClick={this.closeUpload} />;
}
-
}
-
-
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 8265de445..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;
@@ -397,16 +397,14 @@ export class MobileInterface extends React.Component {
renderDepth={0}
isDocumentActive={returnTrue}
isContentActive={emptyFunction}
- focus={DocUtils.DefaultFocus}
+ focus={emptyFunction}
styleProvider={this.whitebackground}
docViewPath={returnEmptyDoclist}
whenChildContentsActiveChanged={emptyFunction}
bringToFront={emptyFunction}
- docFilters={returnEmptyFilter}
- docRangeFilters={returnEmptyFilter}
+ childFilters={returnEmptyFilter}
+ childFiltersByRanges={returnEmptyFilter}
searchFilterDocs={returnEmptyDoclist}
- ContainingCollectionView={undefined}
- ContainingCollectionDoc={undefined}
/>
</div>
);
@@ -616,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
@@ -659,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>
@@ -668,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" />
@@ -700,7 +698,7 @@ export class MobileInterface extends React.Component {
className="docButton"
title={Doc.isDocPinned(this._activeDoc) ? 'Unpin from presentation' : 'Pin to presentation'}
style={{ backgroundColor: isPinned ? 'black' : 'white', color: isPinned ? 'white' : 'black' }}
- onClick={e => TabDocView.PinDoc(this._activeDoc)}>
+ onClick={e => TabDocView.PinDoc(this._activeDoc, {})}>
<FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="map-pin" />
</div>
);
@@ -724,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);
@@ -744,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 => {
@@ -802,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/server/ApiManagers/DataVizManager.ts b/src/server/ApiManagers/DataVizManager.ts
new file mode 100644
index 000000000..0d43130d1
--- /dev/null
+++ b/src/server/ApiManagers/DataVizManager.ts
@@ -0,0 +1,26 @@
+import { csvParser, csvToString } from "../DataVizUtils";
+import { Method, _success } from "../RouteManager";
+import ApiManager, { Registration } from "./ApiManager";
+import { Directory, serverPathToFile } from "./UploadManager";
+import * as path from 'path';
+
+export default class DataVizManager extends ApiManager {
+ protected initialize(register: Registration): void {
+ register({
+ method: Method.GET,
+ subscription: "/csvData",
+ secureHandler: async ({ req, res }) => {
+ const uri = req.query.uri as string;
+
+ return new Promise<void>(resolve => {
+ const name = path.basename(uri);
+ const sPath = serverPathToFile(Directory.csv, name);
+ const parsedCsv = csvParser(csvToString(sPath));
+ _success(res, parsedCsv);
+ resolve();
+ });
+ }
+ });
+ }
+
+} \ No newline at end of file
diff --git a/src/server/ApiManagers/SearchManager.ts b/src/server/ApiManagers/SearchManager.ts
index a74e13a62..186f0bcd3 100644
--- a/src/server/ApiManagers/SearchManager.ts
+++ b/src/server/ApiManagers/SearchManager.ts
@@ -1,94 +1,89 @@
-import { exec } from "child_process";
-import { cyan, green, red, yellow } from "colors";
+import { exec } from 'child_process';
+import { cyan, green, red, yellow } from 'colors';
import * as path from 'path';
-import { log_execution } from "../ActionUtilities";
-import { Database } from "../database";
-import { Method } from "../RouteManager";
-import RouteSubscriber from "../RouteSubscriber";
-import { Search } from "../Search";
-import ApiManager, { Registration } from "./ApiManager";
-import { Directory, pathToDirectory } from "./UploadManager";
+import { log_execution } from '../ActionUtilities';
+import { Database } from '../database';
+import { Method } from '../RouteManager';
+import RouteSubscriber from '../RouteSubscriber';
+import { Search } from '../Search';
+import ApiManager, { Registration } from './ApiManager';
+import { Directory, pathToDirectory } from './UploadManager';
const findInFiles = require('find-in-files');
export class SearchManager extends ApiManager {
-
protected initialize(register: Registration): void {
-
register({
method: Method.GET,
- subscription: new RouteSubscriber("solr").add("action"),
+ subscription: new RouteSubscriber('solr').add('action'),
secureHandler: async ({ req, res }) => {
const { action } = req.params;
switch (action) {
- case "start":
- case "stop":
- const status = req.params.action === "start";
+ case 'start':
+ case 'stop':
+ const status = req.params.action === 'start';
SolrManager.SetRunning(status);
break;
- case "update":
+ case 'update':
await SolrManager.update();
break;
default:
console.log(yellow(`${action} is an unknown solr operation.`));
}
- res.redirect("/home");
- }
+ res.redirect('/home');
+ },
});
register({
method: Method.GET,
- subscription: "/textsearch",
+ subscription: '/textsearch',
secureHandler: async ({ req, res }) => {
const q = req.query.q;
if (q === undefined) {
res.send([]);
return;
}
- const resObj: { ids: string[], numFound: number, lines: string[] } = { ids: [], numFound: 0, lines: [] };
+ const resObj: { ids: string[]; numFound: number; lines: string[] } = { ids: [], numFound: 0, lines: [] };
let results: any;
const dir = pathToDirectory(Directory.text);
try {
const regex = new RegExp(q.toString());
- results = await findInFiles.find({ 'term': q, 'flags': 'ig' }, dir, ".txt$");
+ results = await findInFiles.find({ term: q, flags: 'ig' }, dir, '.txt$');
for (const result in results) {
- resObj.ids.push(path.basename(result, ".txt").replace(/upload_/, ""));
+ resObj.ids.push(path.basename(result, '.txt').replace(/upload_/, ''));
resObj.lines.push(results[result].line);
resObj.numFound++;
}
res.send(resObj);
} catch (e) {
- console.log(red("textsearch:bad RegExp" + q.toString()));
+ console.log(red('textsearch:bad RegExp' + q.toString()));
res.send([]);
return;
}
- }
+ },
});
register({
method: Method.GET,
- subscription: "/dashsearch",
+ subscription: '/dashsearch',
secureHandler: async ({ req, res }) => {
const solrQuery: any = {};
- ["q", "fq", "start", "rows", "sort", "hl.maxAnalyzedChars", "hl", "hl.fl"].forEach(key => solrQuery[key] = req.query[key]);
+ ['q', 'fq', 'start', 'rows', 'sort', 'hl.maxAnalyzedChars', 'hl', 'hl.fl'].forEach(key => (solrQuery[key] = req.query[key]));
if (solrQuery.q === undefined) {
res.send([]);
return;
}
const results = await Search.search(solrQuery);
res.send(results);
- }
+ },
});
-
}
-
}
export namespace SolrManager {
-
export function SetRunning(status: boolean) {
- const args = status ? "start" : "stop -p 8983";
+ const args = status ? 'start' : 'stop -p 8983';
console.log(`solr management: trying to ${args}`);
- exec(`solr ${args}`, { cwd: "./solr-8.3.1/bin" }, (error, stdout, stderr) => {
+ exec(`solr ${args}`, { cwd: './solr-8.3.1/bin' }, (error, stdout, stderr) => {
if (error) {
console.log(red(`solr management error: unable to ${args} server`));
console.log(red(error.message));
@@ -97,39 +92,39 @@ export namespace SolrManager {
console.log(yellow(stderr));
});
if (status) {
- console.log(cyan("Start script is executing: please allow 15 seconds for solr to start on port 8983."));
+ console.log(cyan('Start script is executing: please allow 15 seconds for solr to start on port 8983.'));
}
}
export async function update() {
- console.log(green("Beginning update..."));
+ console.log(green('Beginning update...'));
await log_execution<void>({
- startMessage: "Clearing existing Solr information...",
- endMessage: "Solr information successfully cleared",
+ startMessage: 'Clearing existing Solr information...',
+ endMessage: 'Solr information successfully cleared',
action: Search.clear,
- color: cyan
+ color: cyan,
});
const cursor = await log_execution({
- startMessage: "Connecting to and querying for all documents from database...",
+ startMessage: 'Connecting to and querying for all documents from database...',
endMessage: ({ result, error }) => {
const success = error === null && result !== undefined;
if (!success) {
- console.log(red("Unable to connect to the database."));
+ console.log(red('Unable to connect to the database.'));
process.exit(0);
}
- return "Connection successful and query complete";
+ return 'Connection successful and query complete';
},
action: () => Database.Instance.query({}),
- color: yellow
+ color: yellow,
});
const updates: any[] = [];
let numDocs = 0;
function updateDoc(doc: any) {
numDocs++;
- if ((numDocs % 50) === 0) {
+ if (numDocs % 50 === 0) {
console.log(`Batch of 50 complete, total of ${numDocs}`);
}
- if (doc.__type !== "Doc") {
+ if (doc.__type !== 'Doc') {
return;
}
const fields = doc.fields;
@@ -143,8 +138,8 @@ export namespace SolrManager {
const term = ToSearchTerm(value);
if (term !== undefined) {
const { suffix, value } = term;
- if (key.endsWith('lastModified')) {
- update["lastModified" + suffix] = value;
+ if (key.endsWith('modificationDate')) {
+ update['modificationDate' + suffix] = value;
}
update[key + suffix] = value;
dynfield = true;
@@ -157,51 +152,54 @@ export namespace SolrManager {
await cursor?.forEach(updateDoc);
const result = await log_execution({
startMessage: `Dispatching updates for ${updates.length} documents`,
- endMessage: "Dispatched updates complete",
+ endMessage: 'Dispatched updates complete',
action: () => Search.updateDocuments(updates),
- color: cyan
+ color: cyan,
});
try {
if (result) {
const { status } = JSON.parse(result).responseHeader;
- console.log(status ? red(`Failed with status code (${status})`) : green("Success!"));
+ console.log(status ? red(`Failed with status code (${status})`) : green('Success!'));
} else {
- console.log(red("Solr is likely not running!"));
+ console.log(red('Solr is likely not running!'));
}
} catch (e) {
- console.log(red("Error:"));
+ console.log(red('Error:'));
console.log(e);
- console.log("\n");
+ console.log('\n');
}
await cursor?.close();
}
- const suffixMap: { [type: string]: (string | [string, string | ((json: any) => any)]) } = {
- "number": "_n",
- "string": "_t",
- "boolean": "_b",
- "image": ["_t", "url"],
- "video": ["_t", "url"],
- "pdf": ["_t", "url"],
- "audio": ["_t", "url"],
- "web": ["_t", "url"],
- "map": ["_t", "url"],
- "date": ["_d", value => new Date(value.date).toISOString()],
- "proxy": ["_i", "fieldId"],
- "prefetch_proxy": ["_i", "fieldId"],
- "list": ["_l", list => {
- const results = [];
- for (const value of list.fields) {
- const term = ToSearchTerm(value);
- if (term) {
- results.push(term.value);
+ const suffixMap: { [type: string]: string | [string, string | ((json: any) => any)] } = {
+ number: '_n',
+ string: '_t',
+ boolean: '_b',
+ image: ['_t', 'url'],
+ video: ['_t', 'url'],
+ pdf: ['_t', 'url'],
+ audio: ['_t', 'url'],
+ web: ['_t', 'url'],
+ map: ['_t', 'url'],
+ date: ['_d', value => new Date(value.date).toISOString()],
+ proxy: ['_i', 'fieldId'],
+ prefetch_proxy: ['_i', 'fieldId'],
+ list: [
+ '_l',
+ list => {
+ const results = [];
+ for (const value of list.fields) {
+ const term = ToSearchTerm(value);
+ if (term) {
+ results.push(term.value);
+ }
}
- }
- return results.length ? results : null;
- }]
+ return results.length ? results : null;
+ },
+ ],
};
- function ToSearchTerm(val: any): { suffix: string, value: any } | undefined {
+ function ToSearchTerm(val: any): { suffix: string; value: any } | undefined {
if (val === null || val === undefined) {
return;
}
@@ -213,7 +211,7 @@ export namespace SolrManager {
if (Array.isArray(suffix)) {
const accessor = suffix[1];
- if (typeof accessor === "function") {
+ if (typeof accessor === 'function') {
val = accessor(val);
} else {
val = val[accessor];
@@ -223,5 +221,4 @@ export namespace SolrManager {
return { suffix, value: val };
}
-
-} \ No newline at end of file
+}
diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts
index 0a16bd8ec..de1661ed6 100644
--- a/src/server/ApiManagers/UploadManager.ts
+++ b/src/server/ApiManagers/UploadManager.ts
@@ -1,19 +1,19 @@
-import ApiManager, { Registration } from './ApiManager';
-import { Method, _success } from '../RouteManager';
import * as formidable from 'formidable';
-import v4 = require('uuid/v4');
-const AdmZip = require('adm-zip');
-import { extname, basename, dirname } from 'path';
import { createReadStream, createWriteStream, unlink, writeFile } from 'fs';
-import { publicDirectory, filesDirectory } from '..';
-import { Database } from '../database';
-import { DashUploadUtils, InjectSize, SizeSuffix } from '../DashUploadUtils';
+import { basename, dirname, extname, normalize } from 'path';
import * as sharp from 'sharp';
-import { AcceptableMedia, Upload } from '../SharedMediaTypes';
-import { normalize } from 'path';
+import { filesDirectory, publicDirectory } from '..';
+import { retrocycle } from '../../decycler/decycler';
+import { DashUploadUtils, InjectSize, SizeSuffix } from '../DashUploadUtils';
+import { Database } from '../database';
+import { Method, _success } from '../RouteManager';
import RouteSubscriber from '../RouteSubscriber';
-const imageDataUri = require('image-data-uri');
+import { AcceptableMedia, Upload } from '../SharedMediaTypes';
+import ApiManager, { Registration } from './ApiManager';
import { SolrManager } from './SearchManager';
+import v4 = require('uuid/v4');
+const AdmZip = require('adm-zip');
+const imageDataUri = require('image-data-uri');
const fs = require('fs');
export enum Directory {
@@ -85,7 +85,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);
}
}
@@ -190,13 +190,9 @@ export default class UploadManager extends ApiManager {
const ids: { [id: string]: string } = {};
let remap = true;
const getId = (id: string): string => {
- if (!remap) return id;
- if (id.endsWith('Proto')) return id;
- if (id in ids) {
- return ids[id];
- } else {
- return (ids[id] = v4());
- }
+ if (!remap || id.endsWith('Proto')) return id;
+ if (id in ids) return ids[id];
+ return (ids[id] = v4());
};
const mapFn = (doc: any) => {
if (doc.id) {
@@ -238,48 +234,50 @@ export default class UploadManager extends ApiManager {
form.parse(req, async (_err, fields, files) => {
remap = fields.remap !== 'false';
let id: string = '';
+ let docids: string[] = [];
+ let linkids: string[] = [];
try {
for (const name in files) {
const f = files[name];
const path_2 = Array.isArray(f) ? '' : f.path;
const zip = new AdmZip(path_2);
zip.getEntries().forEach((entry: any) => {
- if (!entry.entryName.startsWith('files/')) return;
- let directory = dirname(entry.entryName) + '/';
- const extension = extname(entry.entryName);
- const base = basename(entry.entryName).split('.')[0];
+ let entryName = entry.entryName.replace(/%%%/g, '/');
+ if (!entryName.startsWith('files/')) {
+ return;
+ }
+ const extension = extname(entryName);
+ const pathname = publicDirectory + '/' + entry.entryName;
+ const targetname = publicDirectory + '/' + entryName;
try {
zip.extractEntryTo(entry.entryName, publicDirectory, true, false);
- directory = '/' + directory;
-
- createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + '_o' + extension));
- createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + '_s' + extension));
- createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + '_m' + extension));
- createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + '_l' + extension));
+ createReadStream(pathname).pipe(createWriteStream(targetname));
+ if (extension !== '.pdf') {
+ createReadStream(pathname).pipe(createWriteStream(targetname.replace('_o' + extension, '_s' + extension)));
+ createReadStream(pathname).pipe(createWriteStream(targetname.replace('_o' + extension, '_m' + extension)));
+ createReadStream(pathname).pipe(createWriteStream(targetname.replace('_o' + extension, '_l' + extension)));
+ }
+ unlink(pathname, () => {});
} catch (e) {
console.log(e);
}
});
- const json = zip.getEntry('doc.json');
+ const json = zip.getEntry('docs.json');
try {
- const data = JSON.parse(json.getData().toString('utf8'));
- const datadocs = data.docs;
+ const data = JSON.parse(json.getData().toString('utf8'), retrocycle());
+ const { docs, links } = data;
id = getId(data.id);
- const docs = Object.keys(datadocs).map(key => datadocs[key]);
- docs.forEach(mapFn);
+ const rdocs = Object.keys(docs).map(key => docs[key]);
+ const ldocs = Object.keys(links).map(key => links[key]);
+ [...rdocs, ...ldocs].forEach(mapFn);
+ docids = rdocs.map(doc => doc.id);
+ linkids = ldocs.map(link => link.id);
await Promise.all(
- docs.map(
- (doc: any) =>
+ [...rdocs, ...ldocs].map(
+ doc =>
new Promise<void>(res => {
- Database.Instance.replace(
- doc.id,
- doc,
- (err, r) => {
- err && console.log(err);
- res();
- },
- true
- );
+ // overwrite mongo doc with json doc contents
+ Database.Instance.replace(doc.id, doc, (err, r) => res(err && console.log(err)), true);
})
)
);
@@ -289,7 +287,7 @@ export default class UploadManager extends ApiManager {
unlink(path_2, () => {});
}
SolrManager.update();
- res.send(JSON.stringify(id || 'error'));
+ res.send(JSON.stringify({ id, docids, linkids } || 'error'));
} catch (e) {
console.log(e);
}
diff --git a/src/server/ApiManagers/UserManager.ts b/src/server/ApiManagers/UserManager.ts
index 53e55c1c3..c3dadd821 100644
--- a/src/server/ApiManagers/UserManager.ts
+++ b/src/server/ApiManagers/UserManager.ts
@@ -5,6 +5,7 @@ import { msToTime } from '../ActionUtilities';
import * as bcrypt from 'bcrypt-nodejs';
import { Opt } from '../../fields/Doc';
import { WebSocket } from '../websocket';
+import { DashStats } from '../DashStats';
export const timeMap: { [id: string]: number } = {};
interface ActivityUnit {
diff --git a/src/server/DashStats.ts b/src/server/DashStats.ts
new file mode 100644
index 000000000..8d341db63
--- /dev/null
+++ b/src/server/DashStats.ts
@@ -0,0 +1,293 @@
+import { cyan, magenta } from 'colors';
+import { Response } from 'express';
+import SocketIO from 'socket.io';
+import { timeMap } from './ApiManagers/UserManager';
+import { WebSocket } from './websocket';
+const fs = require('fs');
+
+/**
+ * DashStats focuses on tracking user data for each session.
+ *
+ * This includes time connected, number of operations, and
+ * the rate of their operations
+ */
+export namespace DashStats {
+ export const SAMPLING_INTERVAL = 1000; // in milliseconds (ms) - Time interval to update the frontend.
+ export const RATE_INTERVAL = 10; // in seconds (s) - Used to calculate rate
+
+ const statsCSVFilename = './src/server/stats/userLoginStats.csv';
+ const columns = ['USERNAME', 'ACTION', 'TIME'];
+
+ /**
+ * UserStats holds the stats associated with a particular user.
+ */
+ interface UserStats {
+ socketId: string;
+ username: string;
+ time: string;
+ operations: number;
+ rate: number;
+ }
+
+ /**
+ * UserLastOperations is the queue object for each user
+ * storing their past operations.
+ */
+ interface UserLastOperations {
+ sampleOperations: number; // stores how many operations total are in this rate section (10 sec, for example)
+ lastSampleOperations: number; // stores how many total operations were recorded at the last sample
+ previousOperationsQueue: number[]; // stores the operations to calculate rate.
+ }
+
+ /**
+ * StatsDataBundle represents an object that will be sent to the frontend view
+ * on each websocket update.
+ */
+ interface StatsDataBundle {
+ connectedUsers: UserStats[];
+ }
+
+ /**
+ * CSVStore represents how objects will be stored in the CSV
+ */
+ interface CSVStore {
+ USERNAME: string;
+ ACTION: string;
+ TIME: string;
+ }
+
+ /**
+ * ServerTraffic describes the current traffic going to the backend.
+ */
+ enum ServerTraffic {
+ NOT_BUSY,
+ BUSY,
+ VERY_BUSY
+ }
+
+ // These values can be changed after further testing how many
+ // users correspond to each traffic level in Dash.
+ const BUSY_SERVER_BOUND = 2;
+ const VERY_BUSY_SERVER_BOUND = 3;
+
+ const serverTrafficMessages = [
+ "Not Busy",
+ "Busy",
+ "Very Busy"
+ ]
+
+ // lastUserOperations maps each username to a UserLastOperations
+ // structure
+ export const lastUserOperations = new Map<string, UserLastOperations>();
+
+ /**
+ * handleStats is called when the /stats route is called, providing a JSON
+ * object with relevant stats. In this case, we return the number of
+ * current connections and
+ * @param res Response object from Express
+ */
+ export function handleStats(res: Response) {
+ let current = getCurrentStats();
+ const results: CSVStore[] = [];
+ res.json({
+ currentConnections: current.length,
+ socketMap: current,
+ });
+ }
+
+ /**
+ * getUpdatedStatesBundle() sends an updated copy of the current stats to the
+ * frontend /statsview route via websockets.
+ *
+ * @returns a StatsDataBundle that is sent to the frontend view on each websocket update
+ */
+ export function getUpdatedStatsBundle(): StatsDataBundle {
+ let current = getCurrentStats();
+
+ return {
+ connectedUsers: current,
+ }
+ }
+
+ /**
+ * handleStatsView() is called when the /statsview route is called. This
+ * will use pug to render a frontend view of the current stats
+ *
+ * @param res
+ */
+ export function handleStatsView(res: Response) {
+ let current = getCurrentStats();
+
+ let connectedUsers = current.map((socketPair) => {
+ return socketPair.time + " - " + socketPair.username + " Operations: " + socketPair.operations;
+ })
+
+ let serverTraffic = ServerTraffic.NOT_BUSY;
+ if(current.length < BUSY_SERVER_BOUND) {
+ serverTraffic = ServerTraffic.NOT_BUSY;
+ } else if(current.length >= BUSY_SERVER_BOUND && current.length < VERY_BUSY_SERVER_BOUND) {
+ serverTraffic = ServerTraffic.BUSY;
+ } else {
+ serverTraffic = ServerTraffic.VERY_BUSY;
+ }
+
+ res.render("stats.pug", {
+ title: "Dash Stats",
+ numConnections: connectedUsers.length,
+ serverTraffic: serverTraffic,
+ serverTrafficMessage : serverTrafficMessages[serverTraffic],
+ connectedUsers: connectedUsers
+ });
+ }
+
+ /**
+ * logUserLogin() writes a login event to the CSV file.
+ *
+ * @param username the username in the format of "username@domain.com logged in"
+ * @param socket the websocket associated with the current connection
+ */
+ export function logUserLogin(username: string | undefined, socket: SocketIO.Socket) {
+ if (!(username === undefined)) {
+ let currentDate = new Date();
+ console.log(magenta(`User ${username.split(' ')[0]} logged in at: ${currentDate.toISOString()}`));
+
+ let toWrite: CSVStore = {
+ USERNAME : username,
+ ACTION : "loggedIn",
+ TIME : currentDate.toISOString()
+ }
+
+ let statsFile = fs.createWriteStream(statsCSVFilename, { flags: "a"});
+ statsFile.write(convertToCSV(toWrite));
+ statsFile.end();
+ console.log(cyan(convertToCSV(toWrite)));
+ }
+ }
+
+ /**
+ * logUserLogout() writes a logout event to the CSV file.
+ *
+ * @param username the username in the format of "username@domain.com logged in"
+ * @param socket the websocket associated with the current connection.
+ */
+ export function logUserLogout(username: string | undefined, socket: SocketIO.Socket) {
+ if (!(username === undefined)) {
+ let currentDate = new Date();
+
+ let statsFile = fs.createWriteStream(statsCSVFilename, { flags: "a"});
+ let toWrite: CSVStore = {
+ USERNAME : username,
+ ACTION : "loggedOut",
+ TIME : currentDate.toISOString()
+ }
+ statsFile.write(convertToCSV(toWrite));
+ statsFile.end();
+ }
+ }
+
+ /**
+ * getLastOperationsOrDefault() is a helper method that will attempt
+ * to query the lastUserOperations map for a specified username. If the
+ * username is not in the map, an empty UserLastOperations object is returned.
+ * @param username
+ * @returns the user's UserLastOperations structure or an empty
+ * UserLastOperations object (All values set to 0) if the username is not found.
+ */
+ function getLastOperationsOrDefault(username: string): UserLastOperations {
+ if(lastUserOperations.get(username) === undefined) {
+ let initializeOperationsQueue = [];
+ for(let i = 0; i < RATE_INTERVAL; i++) {
+ initializeOperationsQueue.push(0);
+ }
+ return {
+ sampleOperations: 0,
+ lastSampleOperations: 0,
+ previousOperationsQueue: initializeOperationsQueue
+ }
+ }
+ return lastUserOperations.get(username)!;
+ }
+
+ /**
+ * updateLastOperations updates a specific user's UserLastOperations information
+ * for the current sampling cycle. The method removes old/outdated counts for
+ * operations from the queue and adds new data for the current sampling
+ * cycle to the queue, updating the total count as it goes.
+ * @param lastOperationData the old UserLastOperations data that must be updated
+ * @param currentOperations the total number of operations measured for this sampling cycle.
+ * @returns the udpated UserLastOperations structure.
+ */
+ function updateLastOperations(lastOperationData: UserLastOperations, currentOperations: number): UserLastOperations {
+ // create a copy of the UserLastOperations to modify
+ let newLastOperationData: UserLastOperations = {
+ sampleOperations: lastOperationData.sampleOperations,
+ lastSampleOperations: lastOperationData.lastSampleOperations,
+ previousOperationsQueue: lastOperationData.previousOperationsQueue.slice()
+ }
+
+ let newSampleOperations = newLastOperationData.sampleOperations;
+ newSampleOperations -= newLastOperationData.previousOperationsQueue.shift()!; // removes and returns the first element of the queue
+ let operationsThisCycle = currentOperations - lastOperationData.lastSampleOperations;
+ newSampleOperations += operationsThisCycle; // add the operations this cycle to find out what our count for the interval should be (e.g operations in the last 10 seconds)
+
+ // update values for the copy object
+ newLastOperationData.sampleOperations = newSampleOperations;
+
+ newLastOperationData.previousOperationsQueue.push(operationsThisCycle);
+ newLastOperationData.lastSampleOperations = currentOperations;
+
+ return newLastOperationData;
+ }
+
+ /**
+ * getUserOperationsOrDefault() is a helper method to get the user's total
+ * operations for the CURRENT sampling interval. The method will return 0
+ * if the username is not in the userOperations map.
+ * @param username the username to search the map for
+ * @returns the total number of operations recorded up to this sampling cycle.
+ */
+ function getUserOperationsOrDefault(username: string): number {
+ return WebSocket.userOperations.get(username) === undefined ? 0 : WebSocket.userOperations.get(username)!
+ }
+
+ /**
+ * getCurrentStats() calculates the total stats for this cycle. In this case,
+ * getCurrentStats() returns an Array of UserStats[] objects describing
+ * the stats for each user
+ * @returns an array of UserStats storing data for each user at the current moment.
+ */
+ function getCurrentStats(): UserStats[] {
+ let socketPairs: UserStats[] = [];
+ for (let [key, value] of WebSocket.socketMap) {
+ let username = value.split(' ')[0];
+ let connectionTime = new Date(timeMap[username]);
+
+ let connectionTimeString = connectionTime.toLocaleDateString() + " " + connectionTime.toLocaleTimeString();
+
+ if (!key.disconnected) {
+ let lastRecordedOperations = getLastOperationsOrDefault(username);
+ let currentUserOperationCount = getUserOperationsOrDefault(username);
+
+ socketPairs.push({
+ socketId: key.id,
+ username: username,
+ time: connectionTimeString.includes("Invalid Date") ? "" : connectionTimeString,
+ operations : WebSocket.userOperations.get(username) ? WebSocket.userOperations.get(username)! : 0,
+ rate: lastRecordedOperations.sampleOperations
+ });
+ lastUserOperations.set(username, updateLastOperations(lastRecordedOperations,currentUserOperationCount));
+ }
+ }
+ return socketPairs;
+ }
+
+ /**
+ * convertToCSV() is a helper method that stringifies a CSVStore object
+ * that can be written to the CSV file later.
+ * @param dataObject the object to stringify
+ * @returns the object as a string.
+ */
+ function convertToCSV(dataObject: CSVStore): string {
+ return `${dataObject.USERNAME},${dataObject.ACTION},${dataObject.TIME}\n`;
+ }
+}
diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts
index 4870d218b..eaaac4e6d 100644
--- a/src/server/DashUploadUtils.ts
+++ b/src/server/DashUploadUtils.ts
@@ -70,7 +70,14 @@ export namespace DashUploadUtils {
// make a list of paths to create the ordered text file for ffmpeg
const filePathsText = filePaths.map(filePath => `file '${filePath}'`).join('\n');
// write the text file to the file system
- writeFile(textFilePath, filePathsText, err => console.log(err));
+ await new Promise<void>((res, reject) =>
+ writeFile(textFilePath, filePathsText, err => {
+ if (err) {
+ reject();
+ console.log(err);
+ } else res();
+ })
+ );
// make output file name based on timestamp
const outputFileName = `output-${Utils.GenerateGuid()}.mp4`;
@@ -86,7 +93,10 @@ export namespace DashUploadUtils {
.outputOptions('-c copy')
//.videoCodec("copy")
.save(outputFilePath)
- .on('error', reject)
+ .on('error', (err: any) => {
+ console.log(err);
+ reject();
+ })
.on('end', resolve);
});
@@ -171,9 +181,10 @@ export namespace DashUploadUtils {
});
}
- export async function upload(file: File): Promise<Upload.FileResponse> {
+ export async function upload(file: File, overwriteGuid?: string): Promise<Upload.FileResponse> {
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]}`;
@@ -195,6 +206,7 @@ export namespace DashUploadUtils {
.videoCodec('copy') // this will copy the data instead of reencode it
.save(file.path.replace('.mkv', '.mp4'))
.on('end', res)
+ .on('error', (e: any) => console.log(e))
);
file.path = file.path.replace('.mkv', '.mp4');
format = '.mp4';
@@ -224,7 +236,7 @@ export namespace DashUploadUtils {
return { source: file, result: { name: 'Unsupported video format', message: `Could not upload unsupported file (${name}). Please convert to an .mp4` } };
}
}
- if (videoFormats.includes(format)) {
+ if (videoFormats.includes(format) || format.includes('.webm')) {
return MoveParsedFile(file, Directory.videos);
}
fs.unlink(path, () => {});
@@ -398,7 +410,9 @@ export namespace DashUploadUtils {
// Use the request library to parse out file level image information in the headers
const { headers } = await new Promise<any>((resolve, reject) => {
return request.head(resolvedUrl, (error, res) => (error ? reject(error) : resolve(res)));
- }).catch(e => console.log(e));
+ }).catch(e => {
+ console.log('Error processing headers: ', e);
+ });
try {
// Compute the native width and height ofthe image with an npm module
@@ -499,7 +513,7 @@ export namespace DashUploadUtils {
const parseExifData = async (source: string) => {
const image = await request.get(source, { encoding: null });
- const { data, error } = await new Promise(resolve => {
+ const { data, error } = await new Promise<{ data: any; error: any }>(resolve => {
new ExifImage({ image }, (error, data) => {
let reason: Opt<string> = undefined;
if (error) {
diff --git a/src/server/DataVizUtils.ts b/src/server/DataVizUtils.ts
index 4fd0ca6ff..15f03b319 100644
--- a/src/server/DataVizUtils.ts
+++ b/src/server/DataVizUtils.ts
@@ -1,13 +1,17 @@
+import { readFileSync } from 'fs';
+
export function csvParser(csv: string) {
- const lines = csv.split("\n");
- const headers = lines[0].split(",");
- const data = lines.slice(1).map(line => {
- const values = line.split(",");
- const obj: any = {};
- for (let i = 0; i < headers.length; i++) {
- obj[headers[i]] = values[i];
- }
- return obj;
- });
+ const lines = csv.split('\n');
+ const headers = lines[0].split(',').map(header => header.trim());
+ const data = lines.slice(1).map(line =>
+ line.split(',').reduce((last, value, i) => {
+ last[headers[i]] = value.trim();
+ return last;
+ }, {} as any)
+ );
return data;
-} \ No newline at end of file
+}
+
+export function csvToString(path: string) {
+ return readFileSync(path, 'utf8');
+}
diff --git a/src/server/Message.ts b/src/server/Message.ts
index d87ae5027..8f0af08bc 100644
--- a/src/server/Message.ts
+++ b/src/server/Message.ts
@@ -94,4 +94,6 @@ export namespace MessageStore {
export const YoutubeApiQuery = new Message<YoutubeQueryInput>("Youtube Api Query");
export const DeleteField = new Message<string>("Delete field");
export const DeleteFields = new Message<string[]>("Delete fields");
+
+ export const UpdateStats = new Message<string>("updatestats");
}
diff --git a/src/server/index.ts b/src/server/index.ts
index 6e6bde3cb..8b2e18847 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -4,6 +4,7 @@ import * as mobileDetect from 'mobile-detect';
import * as path from 'path';
import * as qs from 'query-string';
import { log_execution } from './ActionUtilities';
+import DataVizManager from './ApiManagers/DataVizManager';
import DeleteManager from './ApiManagers/DeleteManager';
import DownloadManager from './ApiManagers/DownloadManager';
import GeneralGoogleManager from './ApiManagers/GeneralGoogleManager';
@@ -18,6 +19,7 @@ import { GoogleCredentialsLoader, SSL } from './apis/google/CredentialsLoader';
import { GoogleApiServerUtils } from './apis/google/GoogleApiServerUtils';
import { DashSessionAgent } from './DashSession/DashSessionAgent';
import { AppliedSessionAgent } from './DashSession/Session/agents/applied_session_agent';
+import { DashStats } from './DashStats';
import { DashUploadUtils } from './DashUploadUtils';
import { Database } from './database';
import { Logger } from './ProcessFactory';
@@ -62,7 +64,19 @@ async function preliminaryFunctions() {
* with the server
*/
function routeSetter({ isRelease, addSupervisedRoute, logRegistrationOutcome }: RouteManager) {
- const managers = [new SessionManager(), new UserManager(), new UploadManager(), new DownloadManager(), new SearchManager(), new PDFManager(), new DeleteManager(), new UtilManager(), new GeneralGoogleManager(), new GooglePhotosManager()];
+ const managers = [
+ new SessionManager(),
+ new UserManager(),
+ new UploadManager(),
+ new DownloadManager(),
+ new SearchManager(),
+ new PDFManager(),
+ new DeleteManager(),
+ new UtilManager(),
+ new GeneralGoogleManager(),
+ new GooglePhotosManager(),
+ new DataVizManager(),
+ ];
// initialize API Managers
console.log(yellow('\nregistering server routes...'));
@@ -85,6 +99,18 @@ function routeSetter({ isRelease, addSupervisedRoute, logRegistrationOutcome }:
addSupervisedRoute({
method: Method.GET,
+ subscription: '/stats',
+ secureHandler: ({ res }) => DashStats.handleStats(res),
+ });
+
+ addSupervisedRoute({
+ method: Method.GET,
+ subscription: '/statsview',
+ secureHandler: ({ res }) => DashStats.handleStatsView(res),
+ });
+
+ addSupervisedRoute({
+ method: Method.GET,
subscription: '/resolvedPorts',
secureHandler: ({ res }) => res.send(resolvedPorts),
publicHandler: ({ res }) => res.send(resolvedPorts),
diff --git a/src/server/server_Initialization.ts b/src/server/server_Initialization.ts
index b0db71f9c..805da1d43 100644
--- a/src/server/server_Initialization.ts
+++ b/src/server/server_Initialization.ts
@@ -22,6 +22,7 @@ import { Database } from './database';
import RouteManager from './RouteManager';
import RouteSubscriber from './RouteSubscriber';
import { WebSocket } from './websocket';
+import brotli = require('brotli');
import expressFlash = require('express-flash');
import flash = require('connect-flash');
const MongoStore = require('connect-mongo')(session);
@@ -171,31 +172,29 @@ function registerCorsProxy(server: express.Express) {
function proxyServe(req: any, requrl: string, response: any) {
const htmlBodyMemoryStream = new (require('memorystream'))();
var retrieveHTTPBody: any;
+ var wasinBrFormat = false;
const sendModifiedBody = () => {
const header = response.headers['content-encoding'];
+ const httpsToCors = (match: any, href: string, offset: any, string: any) => `href="${resolvedServerUrl + '/corsProxy/http' + href}"`;
if (header?.includes('gzip')) {
try {
- const replacer = (match: any, href: string, offset: any, string: any) => {
- return `href="${resolvedServerUrl + '/corsProxy/http' + href}"`;
- };
- const zipToStringDecoder = new (require('string_decoder').StringDecoder)('utf8');
const bodyStream = htmlBodyMemoryStream.read();
if (bodyStream) {
- const htmlText = zipToStringDecoder.write(
- zlib
- .gunzipSync(bodyStream)
- .toString('utf8')
- .replace('<head>', '<head> <style>[id ^= "google"] { display: none; } </style>')
- .replace(/href="https?([^"]*)"/g, replacer)
- .replace(/target="_blank"/g, '')
- );
+ const htmlInputText = wasinBrFormat ? Buffer.from(brotli.decompress(bodyStream)) : zlib.gunzipSync(bodyStream);
+ const htmlText = htmlInputText
+ .toString('utf8')
+ .replace('<head>', '<head> <style>[id ^= "google"] { display: none; } </style>')
+ .replace(/href="https?([^"]*)"/g, httpsToCors)
+ .replace(/data-srcset="[^"]*"/g, '')
+ .replace(/srcset="[^"]*"/g, '')
+ .replace(/target="_blank"/g, '');
response.send(zlib.gzipSync(htmlText));
} else {
req.pipe(request(requrl)).pipe(response);
console.log('EMPTY body:' + req.url);
}
} catch (e) {
- console.log('EROR?: ', e);
+ console.log('ERROR?: ', e);
}
} else {
req.pipe(htmlBodyMemoryStream).pipe(response);
@@ -216,6 +215,10 @@ function proxyServe(req: any, requrl: string, response: any) {
} else if (headerCharRegex.test(header || '')) {
delete res.headers[headerName];
} else res.headers[headerName] = header;
+ if (headerName === 'content-encoding') {
+ wasinBrFormat = res.headers[headerName] === 'br';
+ res.headers[headerName] = 'gzip';
+ }
});
res.headers['x-permitted-cross-domain-policies'] = 'all';
res.headers['x-frame-options'] = '';
diff --git a/src/server/updateProtos.ts b/src/server/updateProtos.ts
index c5552f6bf..2f3772a77 100644
--- a/src/server/updateProtos.ts
+++ b/src/server/updateProtos.ts
@@ -1,14 +1,22 @@
-import { Database } from "./database";
+import { Database } from './database';
-const protos =
- ["text", "image", "web", "collection", "kvp", "video", "audio", "pdf", "icon", "import", "linkdoc", "map"];
+const protos = ['text', 'image', 'web', 'collection', 'kvp', 'video', 'audio', 'pdf', 'icon', 'import', 'linkdoc', 'map'];
(async function () {
await Promise.all(
- protos.map(protoId => new Promise(res => Database.Instance.update(protoId, {
- $set: { "fields.baseProto": true }
- }, res)))
+ protos.map(
+ protoId =>
+ new Promise(res =>
+ Database.Instance.update(
+ protoId,
+ {
+ $set: { 'fields.isBaseProto': true },
+ },
+ res
+ )
+ )
+ )
);
- console.log("done");
-})(); \ No newline at end of file
+ console.log('done');
+})();
diff --git a/src/server/websocket.ts b/src/server/websocket.ts
index 9b91a35a6..be5cdb202 100644
--- a/src/server/websocket.ts
+++ b/src/server/websocket.ts
@@ -12,16 +12,19 @@ import { GoogleCredentialsLoader, SSL } from './apis/google/CredentialsLoader';
import YoutubeApi from './apis/youtube/youtubeApiSample';
import { initializeGuest } from './authentication/DashUserModel';
import { Client } from './Client';
+import { DashStats } from './DashStats';
import { Database } from './database';
import { DocumentsCollection } from './IDatabase';
import { Diff, GestureContent, MessageStore, MobileDocumentUploadContent, MobileInkOverlayContent, Transferable, Types, UpdateMobileInkOverlayPositionContent, YoutubeQueryInput, YoutubeQueryTypes } from './Message';
import { Search } from './Search';
import { resolvedPorts } from './server_Initialization';
+var _ = require('lodash');
export namespace WebSocket {
export let _socket: Socket;
- const clients: { [key: string]: Client } = {};
+ export const clients: { [key: string]: Client } = {};
export const socketMap = new Map<SocketIO.Socket, string>();
+ export const userOperations = new Map<string, number>();
export let disconnect: Function;
export async function initialize(isRelease: boolean, app: express.Express) {
@@ -49,6 +52,8 @@ export namespace WebSocket {
next();
});
+ socket.emit(MessageStore.UpdateStats.Message, DashStats.getUpdatedStatsBundle());
+
// convenience function to log server messages on the client
function log(message?: any, ...optionalParams: any[]) {
socket.emit('log', ['Message from server:', message, ...optionalParams]);
@@ -97,6 +102,15 @@ export namespace WebSocket {
console.log('received bye');
});
+ socket.on('disconnect', function () {
+ let currentUser = socketMap.get(socket);
+ if (!(currentUser === undefined)) {
+ let currentUsername = currentUser.split(' ')[0];
+ DashStats.logUserLogout(currentUsername, socket);
+ delete timeMap[currentUsername];
+ }
+ });
+
Utils.Emit(socket, MessageStore.Foo, 'handshooken');
Utils.AddServerHandler(socket, MessageStore.Bar, guid => barReceived(socket, guid));
@@ -130,6 +144,12 @@ export namespace WebSocket {
socket.disconnect(true);
};
});
+
+ setInterval(function () {
+ // Utils.Emit(socket, MessageStore.UpdateStats, DashStats.getUpdatedStatsBundle());
+
+ io.emit(MessageStore.UpdateStats.Message, DashStats.getUpdatedStatsBundle());
+ }, DashStats.SAMPLING_INTERVAL);
}
function processGesturePoints(socket: Socket, content: GestureContent) {
@@ -176,7 +196,12 @@ export namespace WebSocket {
const currentdate = new Date();
const datetime = currentdate.getDate() + '/' + (currentdate.getMonth() + 1) + '/' + currentdate.getFullYear() + ' @ ' + currentdate.getHours() + ':' + currentdate.getMinutes() + ':' + currentdate.getSeconds();
console.log(blue(`user ${userEmail} has connected to the web socket at: ${datetime}`));
+ printActiveUsers();
+
+ timeMap[userEmail] = Date.now();
socketMap.set(socket, userEmail + ' at ' + datetime);
+ userOperations.set(userEmail, 0);
+ DashStats.logUserLogin(userEmail, socket);
}
function getField([id, callback]: [string, (result?: Transferable) => void]) {
@@ -199,7 +224,7 @@ export namespace WebSocket {
return Database.Instance.getDocument(id, callback);
}
function GetRefField([id, callback]: [string, (result?: Transferable) => void]) {
- process.stdout.write(`.`);
+ process.stdout.write(`+`);
GetRefFieldLocal([id, callback]);
}
@@ -293,15 +318,79 @@ export namespace WebSocket {
);
}
+ /**
+ * findClosestIndex() is a helper function that will try to find
+ * the closest index of a list that has the same value as
+ * a specified argument/index pair.
+ * @param list the list to search through
+ * @param indexesToDelete a list of indexes that are already marked for deletion
+ * so they will be ignored
+ * @param value the value of the item to remove
+ * @param hintIndex the index that the element was at on the client's copy of
+ * the data
+ * @returns the closest index with the same value or -1 if the element was not found.
+ */
+ function findClosestIndex(list: any, indexesToDelete: number[], value: any, hintIndex: number) {
+ let closestIndex = -1;
+ for (let i = 0; i < list.length; i++) {
+ if (list[i] === value && !indexesToDelete.includes(i)) {
+ if (Math.abs(i - hintIndex) < Math.abs(closestIndex - hintIndex)) {
+ closestIndex = i;
+ }
+ }
+ }
+ return closestIndex;
+ }
+
+ /**
+ * remFromListField() receives the items to remove and a hint
+ * from the client, and attempts to make the modification to the
+ * server's copy of the data. If server's copy does not match
+ * the client's after removal, the server will SEND BACk
+ * its version to the client.
+ * @param socket the socket that the client is connected on
+ * @param diff an object containing the items to remove and a hint
+ * (the hint contains start index and deleteCount, the number of
+ * items to delete)
+ * @param curListItems the server's current copy of the data
+ */
function remFromListField(socket: Socket, diff: Diff, curListItems?: Transferable): void {
diff.diff.$set = diff.diff.$remFromSet;
delete diff.diff.$remFromSet;
const updatefield = Array.from(Object.keys(diff.diff.$set))[0];
const remListItems = diff.diff.$set[updatefield].fields;
const curList = (curListItems as any)?.fields?.[updatefield.replace('fields.', '')]?.fields.filter((f: any) => f !== null) || [];
- diff.diff.$set[updatefield].fields = curList?.filter(
- (curItem: any) => !remListItems.some((remItem: any) => (remItem.fieldId ? remItem.fieldId === curItem.fieldId : remItem.heading ? remItem.heading === curItem.heading : remItem === curItem))
- );
+ const hint = diff.diff.$set.hint;
+
+ if (hint) {
+ // indexesToRemove stores the indexes that we mark for deletion, which is later used to filter the list (delete the elements)
+ let indexesToRemove: number[] = [];
+ for (let i = 0; i < hint.deleteCount; i++) {
+ if (curList.length > i + hint.start && _.isEqual(curList[i + hint.start], remListItems[i])) {
+ indexesToRemove.push(i + hint.start);
+ continue;
+ }
+
+ let closestIndex = findClosestIndex(curList, indexesToRemove, remListItems[i], i + hint.start);
+ if (closestIndex !== -1) {
+ indexesToRemove.push(closestIndex);
+ } else {
+ console.log('Item to delete was not found - index = -1');
+ }
+ }
+
+ diff.diff.$set[updatefield].fields = curList?.filter((curItem: any, index: number) => !indexesToRemove.includes(index));
+ } else {
+ // go back to the original way to delete if we didn't receive
+ // a hint from the client
+ diff.diff.$set[updatefield].fields = curList?.filter(
+ (curItem: any) => !remListItems.some((remItem: any) => (remItem.fieldId ? remItem.fieldId === curItem.fieldId : remItem.heading ? remItem.heading === curItem.heading : remItem === curItem))
+ );
+ }
+
+ // if the client and server have different versions of the data after
+ // deletion, they will have different lengths and the server will
+ // send its version of the data to the client
const sendBack = diff.diff.length !== diff.diff.$set[updatefield].fields.length;
delete diff.diff.length;
Database.Instance.update(
@@ -309,6 +398,7 @@ export namespace WebSocket {
diff.diff,
() => {
if (sendBack) {
+ // the two copies are different, so the server sends its copy.
console.log('SEND BACK');
const id = socket.id;
socket.id = '';
@@ -346,6 +436,11 @@ export namespace WebSocket {
var CurUser: string | undefined = undefined;
function UpdateField(socket: Socket, diff: Diff) {
+ const curUser = socketMap.get(socket);
+ if (!curUser) return;
+ let currentUsername = curUser.split(' ')[0];
+ userOperations.set(currentUsername, userOperations.get(currentUsername) !== undefined ? userOperations.get(currentUsername)! + 1 : 0);
+
if (CurUser !== socketMap.get(socket)) {
CurUser = socketMap.get(socket);
console.log('Switch User: ' + CurUser);
@@ -381,8 +476,8 @@ export namespace WebSocket {
if (term !== undefined) {
const { suffix, value } = term;
update[key + suffix] = { set: value };
- if (key.endsWith('lastModified')) {
- update['lastModified' + suffix] = value;
+ if (key.endsWith('modificationDate')) {
+ update['modificationDate' + suffix] = value;
}
}
}