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/Utils.ts9
-rw-r--r--src/client/DocServer.ts85
-rw-r--r--src/client/Network.ts11
-rw-r--r--src/client/apis/gpt/GPT.ts29
-rw-r--r--src/client/apis/youtube/YoutubeBox.tsx6
-rw-r--r--src/client/documents/DocumentTypes.ts5
-rw-r--r--src/client/documents/Documents.ts364
-rw-r--r--src/client/util/BranchingTrailManager.tsx137
-rw-r--r--src/client/util/CurrentUserUtils.ts300
-rw-r--r--src/client/util/DictationManager.ts2
-rw-r--r--src/client/util/DocumentManager.ts45
-rw-r--r--src/client/util/DragManager.ts35
-rw-r--r--src/client/util/DropConverter.ts14
-rw-r--r--src/client/util/GroupManager.tsx41
-rw-r--r--src/client/util/GroupMemberView.tsx105
-rw-r--r--src/client/util/InteractionUtils.tsx5
-rw-r--r--src/client/util/LinkFollower.ts1
-rw-r--r--src/client/util/LinkManager.ts15
-rw-r--r--src/client/util/PingManager.ts6
-rw-r--r--src/client/util/RTFMarkup.tsx15
-rw-r--r--src/client/util/ReplayMovements.ts1
-rw-r--r--src/client/util/SearchUtil.ts104
-rw-r--r--src/client/util/SelectionManager.ts36
-rw-r--r--src/client/util/ServerStats.tsx26
-rw-r--r--src/client/util/SettingsManager.tsx148
-rw-r--r--src/client/util/SharingManager.tsx44
-rw-r--r--src/client/util/reportManager/ReportManager.tsx10
-rw-r--r--src/client/views/AntimodeMenu.tsx7
-rw-r--r--src/client/views/AudioWaveform.tsx6
-rw-r--r--src/client/views/ContextMenu.tsx7
-rw-r--r--src/client/views/ContextMenuItem.tsx25
-rw-r--r--src/client/views/DashboardView.tsx293
-rw-r--r--src/client/views/DocComponent.tsx9
-rw-r--r--src/client/views/DocumentButtonBar.tsx9
-rw-r--r--src/client/views/DocumentDecorations.tsx48
-rw-r--r--src/client/views/EditableView.scss4
-rw-r--r--src/client/views/EditableView.tsx42
-rw-r--r--src/client/views/FilterPanel.scss66
-rw-r--r--src/client/views/FilterPanel.tsx382
-rw-r--r--src/client/views/InkStroke.scss1
-rw-r--r--src/client/views/InkTangentHandles.tsx1
-rw-r--r--src/client/views/InkTranscription.tsx5
-rw-r--r--src/client/views/InkingStroke.tsx46
-rw-r--r--src/client/views/LightboxView.tsx17
-rw-r--r--src/client/views/Main.tsx19
-rw-r--r--src/client/views/MainView.scss10
-rw-r--r--src/client/views/MainView.tsx135
-rw-r--r--src/client/views/MainViewModal.tsx10
-rw-r--r--src/client/views/MarqueeAnnotator.tsx8
-rw-r--r--src/client/views/OverlayView.scss1
-rw-r--r--src/client/views/OverlayView.tsx19
-rw-r--r--src/client/views/Palette.tsx69
-rw-r--r--src/client/views/PreviewCursor.scss5
-rw-r--r--src/client/views/PreviewCursor.tsx12
-rw-r--r--src/client/views/PropertiesButtons.tsx16
-rw-r--r--src/client/views/PropertiesDocBacklinksSelector.tsx12
-rw-r--r--src/client/views/PropertiesDocContextSelector.tsx1
-rw-r--r--src/client/views/PropertiesSection.tsx88
-rw-r--r--src/client/views/PropertiesView.scss114
-rw-r--r--src/client/views/PropertiesView.tsx996
-rw-r--r--src/client/views/ScriptingRepl.tsx23
-rw-r--r--src/client/views/SidebarAnnos.tsx34
-rw-r--r--src/client/views/StyleProvider.scss9
-rw-r--r--src/client/views/StyleProvider.tsx106
-rw-r--r--src/client/views/TemplateMenu.tsx2
-rw-r--r--src/client/views/UndoStack.tsx83
-rw-r--r--src/client/views/animationtimeline/Keyframe.tsx10
-rw-r--r--src/client/views/animationtimeline/Timeline.tsx2
-rw-r--r--src/client/views/animationtimeline/Track.tsx42
-rw-r--r--src/client/views/collections/CollectionCarouselView.tsx3
-rw-r--r--src/client/views/collections/CollectionDockingView.scss5
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx16
-rw-r--r--src/client/views/collections/CollectionMasonryViewFieldRow.tsx1
-rw-r--r--src/client/views/collections/CollectionMenu.tsx11
-rw-r--r--src/client/views/collections/CollectionNoteTakingView.tsx35
-rw-r--r--src/client/views/collections/CollectionNoteTakingViewColumn.tsx10
-rw-r--r--src/client/views/collections/CollectionNoteTakingViewDivider.tsx11
-rw-r--r--src/client/views/collections/CollectionPileView.tsx14
-rw-r--r--src/client/views/collections/CollectionStackedTimeline.tsx16
-rw-r--r--src/client/views/collections/CollectionStackingView.scss9
-rw-r--r--src/client/views/collections/CollectionStackingView.tsx16
-rw-r--r--src/client/views/collections/CollectionStackingViewFieldColumn.tsx23
-rw-r--r--src/client/views/collections/CollectionSubView.tsx10
-rw-r--r--src/client/views/collections/CollectionTimeView.tsx6
-rw-r--r--src/client/views/collections/CollectionTreeView.tsx24
-rw-r--r--src/client/views/collections/CollectionView.tsx6
-rw-r--r--src/client/views/collections/TabDocView.tsx240
-rw-r--r--src/client/views/collections/TreeSort.ts6
-rw-r--r--src/client/views/collections/TreeView.scss21
-rw-r--r--src/client/views/collections/TreeView.tsx190
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx31
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss7
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx158
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx38
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.scss1
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx88
-rw-r--r--src/client/views/collections/collectionLinear/CollectionLinearView.scss2
-rw-r--r--src/client/views/collections/collectionLinear/CollectionLinearView.tsx47
-rw-r--r--src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss6
-rw-r--r--src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx17
-rw-r--r--src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx74
-rw-r--r--src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx74
-rw-r--r--src/client/views/collections/collectionSchema/CollectionSchemaView.scss28
-rw-r--r--src/client/views/collections/collectionSchema/CollectionSchemaView.tsx32
-rw-r--r--src/client/views/collections/collectionSchema/SchemaRowBox.tsx54
-rw-r--r--src/client/views/collections/collectionSchema/SchemaTableCell.tsx26
-rw-r--r--src/client/views/global/globalScripts.ts70
-rw-r--r--src/client/views/linking/LinkMenu.scss1
-rw-r--r--src/client/views/linking/LinkMenu.tsx3
-rw-r--r--src/client/views/linking/LinkMenuGroup.tsx4
-rw-r--r--src/client/views/linking/LinkMenuItem.scss24
-rw-r--r--src/client/views/linking/LinkMenuItem.tsx29
-rw-r--r--src/client/views/linking/LinkPopup.tsx3
-rw-r--r--src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx92
-rw-r--r--src/client/views/newlightbox/ExploreView/ExploreView.tsx50
-rw-r--r--src/client/views/newlightbox/NewLightboxView.tsx139
-rw-r--r--src/client/views/newlightbox/components/Recommendation/Recommendation.tsx170
-rw-r--r--src/client/views/nodes/AudioBox.tsx97
-rw-r--r--src/client/views/nodes/CollectionFreeFormDocumentView.tsx1
-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.tsx33
-rw-r--r--src/client/views/nodes/DataVizBox/DataVizBox.scss15
-rw-r--r--src/client/views/nodes/DataVizBox/DataVizBox.tsx210
-rw-r--r--src/client/views/nodes/DataVizBox/components/Chart.scss68
-rw-r--r--src/client/views/nodes/DataVizBox/components/Histogram.tsx518
-rw-r--r--src/client/views/nodes/DataVizBox/components/LineChart.tsx162
-rw-r--r--src/client/views/nodes/DataVizBox/components/PieChart.tsx401
-rw-r--r--src/client/views/nodes/DataVizBox/components/TableBox.tsx219
-rw-r--r--src/client/views/nodes/DataVizBox/utils/D3Utils.ts1
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx2
-rw-r--r--src/client/views/nodes/DocumentIcon.tsx2
-rw-r--r--src/client/views/nodes/DocumentLinksButton.tsx4
-rw-r--r--src/client/views/nodes/DocumentView.tsx165
-rw-r--r--src/client/views/nodes/EquationBox.scss9
-rw-r--r--src/client/views/nodes/FontIconBox/FontIconBox.tsx223
-rw-r--r--src/client/views/nodes/FontIconBox/TrailsIcon.tsx81
-rw-r--r--src/client/views/nodes/FunctionPlotBox.tsx4
-rw-r--r--src/client/views/nodes/ImageBox.tsx59
-rw-r--r--src/client/views/nodes/KeyValueBox.tsx17
-rw-r--r--src/client/views/nodes/KeyValuePair.tsx14
-rw-r--r--src/client/views/nodes/LinkBox.tsx2
-rw-r--r--src/client/views/nodes/LinkDocPreview.scss2
-rw-r--r--src/client/views/nodes/LinkDocPreview.tsx16
-rw-r--r--src/client/views/nodes/LoadingBox.scss40
-rw-r--r--src/client/views/nodes/LoadingBox.tsx20
-rw-r--r--src/client/views/nodes/MapBox/MapAnchorMenu.scss54
-rw-r--r--src/client/views/nodes/MapBox/MapAnchorMenu.tsx143
-rw-r--r--src/client/views/nodes/MapBox/MapBox.scss161
-rw-r--r--src/client/views/nodes/MapBox/MapBox.tsx966
-rw-r--r--src/client/views/nodes/MapBox/MapBox2.tsx14
-rw-r--r--src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx4
-rw-r--r--src/client/views/nodes/MapBox/MapPushpinBox.tsx34
-rw-r--r--src/client/views/nodes/PDFBox.tsx19
-rw-r--r--src/client/views/nodes/RecordingBox/RecordingBox.tsx228
-rw-r--r--src/client/views/nodes/RecordingBox/RecordingView.tsx45
-rw-r--r--src/client/views/nodes/ScreenshotBox.scss36
-rw-r--r--src/client/views/nodes/ScreenshotBox.tsx50
-rw-r--r--src/client/views/nodes/ScriptingBox.tsx2
-rw-r--r--src/client/views/nodes/VideoBox.scss2
-rw-r--r--src/client/views/nodes/VideoBox.tsx42
-rw-r--r--src/client/views/nodes/WebBox.tsx78
-rw-r--r--src/client/views/nodes/WebBoxRenderer.js6
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.scss20
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx308
-rw-r--r--src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts2
-rw-r--r--src/client/views/nodes/formattedText/RichTextMenu.tsx31
-rw-r--r--src/client/views/nodes/formattedText/RichTextRules.ts21
-rw-r--r--src/client/views/nodes/formattedText/marks_rts.ts2
-rw-r--r--src/client/views/nodes/generativeFill/GenerativeFill.scss97
-rw-r--r--src/client/views/nodes/generativeFill/GenerativeFill.tsx590
-rw-r--r--src/client/views/nodes/generativeFill/GenerativeFillButtons.scss4
-rw-r--r--src/client/views/nodes/generativeFill/GenerativeFillButtons.tsx44
-rw-r--r--src/client/views/nodes/generativeFill/generativeFillUtils/BrushHandler.ts25
-rw-r--r--src/client/views/nodes/generativeFill/generativeFillUtils/GenerativeFillMathHelpers.ts10
-rw-r--r--src/client/views/nodes/generativeFill/generativeFillUtils/ImageHandler.ts314
-rw-r--r--src/client/views/nodes/generativeFill/generativeFillUtils/PointerHandler.ts15
-rw-r--r--src/client/views/nodes/generativeFill/generativeFillUtils/generativeFillConstants.ts9
-rw-r--r--src/client/views/nodes/generativeFill/generativeFillUtils/generativeFillInterfaces.ts20
-rw-r--r--src/client/views/nodes/trails/PresBox.scss15
-rw-r--r--src/client/views/nodes/trails/PresBox.tsx841
-rw-r--r--src/client/views/nodes/trails/PresElementBox.scss4
-rw-r--r--src/client/views/nodes/trails/PresElementBox.tsx118
-rw-r--r--src/client/views/pdf/AnchorMenu.tsx249
-rw-r--r--src/client/views/pdf/Annotation.tsx4
-rw-r--r--src/client/views/pdf/GPTPopup/GPTPopup.scss66
-rw-r--r--src/client/views/pdf/GPTPopup/GPTPopup.tsx4
-rw-r--r--src/client/views/pdf/PDFViewer.tsx18
-rw-r--r--src/client/views/search/IconBar.tsx28
-rw-r--r--src/client/views/search/SearchBox.tsx183
-rw-r--r--src/client/views/selectedDoc/SelectedDocView.tsx61
-rw-r--r--src/client/views/topbar/TopBar.tsx122
-rw-r--r--src/client/views/webcam/DashWebRTCVideo.tsx6
-rw-r--r--src/fields/Doc.ts226
-rw-r--r--src/fields/DocSymbols.ts3
-rw-r--r--src/fields/FieldLoader.tsx5
-rw-r--r--src/fields/IconField.ts26
-rw-r--r--src/fields/List.ts46
-rw-r--r--src/fields/PresField.ts6
-rw-r--r--src/fields/RichTextUtils.ts10
-rw-r--r--src/fields/Types.ts9
-rw-r--r--src/fields/documentSchemas.ts21
-rw-r--r--src/mobile/MobileInterface.tsx1
-rw-r--r--src/mobile/MobileMain.tsx2
-rw-r--r--src/server/ApiManagers/AzureManager.ts19
-rw-r--r--src/server/ApiManagers/UploadManager.ts21
-rw-r--r--src/server/ApiManagers/UserManager.ts16
-rw-r--r--src/server/DashUploadUtils.ts95
-rw-r--r--src/server/server_Initialization.ts73
210 files changed, 9509 insertions, 4915 deletions
diff --git a/src/.DS_Store b/src/.DS_Store
index 06389d6ae..c363efb13 100644
--- a/src/.DS_Store
+++ b/src/.DS_Store
Binary files differ
diff --git a/src/Utils.ts b/src/Utils.ts
index 8b9fe2aab..9b969081f 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -3,10 +3,10 @@ import v5 = require('uuid/v5');
import { ColorState } from 'react-color';
import * as rp from 'request-promise';
import { Socket } from 'socket.io';
+import { DocumentType } from './client/documents/DocumentTypes';
import { Colors } from './client/views/global/globalEnums';
import { Message } from './server/Message';
import Color = require('color');
-import { DocumentType } from './client/documents/DocumentTypes';
export namespace Utils {
export let CLICK_TIME = 300;
@@ -76,7 +76,7 @@ export namespace Utils {
});
return returnedUri;
} catch (e) {
- console.log('VideoBox :' + e);
+ console.log('ConvertDataURI :' + e);
}
}
@@ -758,8 +758,8 @@ export function DashColor(color: string) {
}
export function lightOrDark(color: any) {
- if (color === 'transparent') return 'gray';
- if (color.startsWith?.('linear')) return 'black';
+ if (color === 'transparent') return Colors.DARK_GRAY;
+ if (color.startsWith?.('linear')) return Colors.BLACK;
const nonAlphaColor = color.startsWith('#') ? (color as string).substring(0, 7) : color.startsWith('rgba') ? color.replace(/,.[^,]*\)/, ')').replace('rgba', 'rgb') : color;
const col = DashColor(nonAlphaColor).rgb();
const colsum = col.red() + col.green() + col.blue();
@@ -769,6 +769,7 @@ export function lightOrDark(color: any) {
export function getWordAtPoint(elem: any, x: number, y: number): string | undefined {
if (elem.tagName === 'INPUT') return 'input';
+ if (elem.tagName === 'TEXTAREA') return 'textarea';
if (elem.nodeType === elem.TEXT_NODE) {
const range = elem.ownerDocument.createRange();
range.selectNodeContents(elem);
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
index ba59a9f50..353e11775 100644
--- a/src/client/DocServer.ts
+++ b/src/client/DocServer.ts
@@ -1,18 +1,19 @@
import { runInAction } from 'mobx';
import * as rp from 'request-promise';
-import * as io from 'socket.io-client';
-import { Doc, Opt } from '../fields/Doc';
+import { Doc, DocListCast, Opt } from '../fields/Doc';
import { UpdatingFromServer } from '../fields/DocSymbols';
import { FieldLoader } from '../fields/FieldLoader';
import { HandleUpdate, Id, Parent } from '../fields/FieldSymbols';
import { ObjectField } from '../fields/ObjectField';
import { RefField } from '../fields/RefField';
-import { StrCast } from '../fields/Types';
-import MobileInkOverlay from '../mobile/MobileInkOverlay';
+import { DocCast, StrCast } from '../fields/Types';
+//import MobileInkOverlay from '../mobile/MobileInkOverlay';
import { emptyFunction, Utils } from '../Utils';
import { GestureContent, MessageStore, MobileDocumentUploadContent, MobileInkOverlayContent, UpdateMobileInkOverlayPositionContent, YoutubeQueryTypes } from './../server/Message';
+import { DocumentType } from './documents/DocumentTypes';
+import { LinkManager } from './util/LinkManager';
import { SerializationHelper } from './util/SerializationHelper';
-import { GestureOverlay } from './views/GestureOverlay';
+//import { GestureOverlay } from './views/GestureOverlay';
/**
* This class encapsulates the transfer and cross-client synchronization of
@@ -30,34 +31,47 @@ 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);
+ export function FindDocByTitle(title: string) {
+ const foundDocId =
+ title &&
+ 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) {
- if (print) {
- const strings: string[] = [];
- Array.from(Object.keys(_cache)).forEach(key => {
- const doc = _cache[key];
- if (doc instanceof Doc) strings.push(StrCast(doc.author) + ' ' + StrCast(doc.title) + ' ' + StrCast(Doc.GetT(doc, 'title', 'string', true)));
- });
- strings.sort().forEach((str, i) => console.log(i.toString() + ' ' + str));
- }
- const filtered = Array.from(Object.keys(_cache)).filter(key => {
- const doc = _cache[key] as Doc;
- if (!(StrCast(doc.author).includes('.edu') || StrCast(doc.author).includes('.com')) || doc.author == Doc.CurrentUserEmail) return true;
- return false;
+ let cacheDocumentIds = ''; // ; separate string of all documents ids in the user's working set (cached on the server)
+ export let CacheNeedsUpdate = false;
+ export function UPDATE_SERVER_CACHE() {
+ const prototypes = Object.values(DocumentType)
+ .filter(type => type !== DocumentType.NONE)
+ .map(type => _cache[type + 'Proto'])
+ .filter(doc => doc instanceof Doc)
+ .map(doc => doc as Doc);
+ const references = new Set<Doc>(prototypes);
+ Doc.FindReferences(Doc.UserDoc(), references, undefined);
+ DocListCast(DocCast(Doc.UserDoc().myLinkDatabase).data).forEach(link => {
+ if (!references.has(DocCast(link.link_anchor_1)) && !references.has(DocCast(link.link_anchor_2))) {
+ Doc.RemoveDocFromList(DocCast(Doc.UserDoc().myLinkDatabase), 'data', link);
+ Doc.AddDocToList(Doc.MyRecentlyClosed, undefined, link);
+ }
});
- if (filtered.length === lastCacheUpdate) return;
- lastCacheUpdate = filtered.length;
+ LinkManager.userLinkDBs.forEach(linkDb => Doc.FindReferences(linkDb, references, undefined));
+ const filtered = Array.from(references);
+
+ const newCacheUpdate = filtered.map(doc => doc[Id]).join(';');
+ if (newCacheUpdate === cacheDocumentIds) return;
+ cacheDocumentIds = newCacheUpdate;
+
+ // print out cached docs
+ console.log('Set cached docs = ');
+ const is_filtered = filtered.filter(doc => !Doc.IsSystem(doc));
+ const strings = is_filtered.map(doc => StrCast(doc.title) + ' ' + (Doc.IsDataProto(doc) ? '(data)' : '(embedding)'));
+ strings.sort().forEach((str, i) => console.log(i.toString() + ' ' + str));
rp.post(Utils.prepend('/setCacheDocumentIds'), {
body: {
- cacheDocumentIds: filtered.join(';'),
+ cacheDocumentIds,
},
json: true,
});
@@ -156,7 +170,7 @@ export namespace DocServer {
_cache = {};
USER_ID = identifier;
protocol = protocol.startsWith('https') ? 'wss' : 'ws';
- _socket = io.connect(`${protocol}://${hostname}:${port}`);
+ _socket = require('socket.io-client')(`${protocol}://${hostname}:${port}`, { transports: ['websocket'], rejectUnauthorized: false });
// io.connect(`https://7f079dda.ngrok.io`);// if using ngrok, create a special address for the websocket
_GetCachedRefField = _GetCachedRefFieldImpl;
@@ -177,17 +191,17 @@ export namespace DocServer {
// mobile ink overlay socket events to communicate between mobile view and desktop view
_socket.addEventListener('receiveGesturePoints', (content: GestureContent) => {
- MobileInkOverlay.Instance.drawStroke(content);
+ // MobileInkOverlay.Instance.drawStroke(content);
});
_socket.addEventListener('receiveOverlayTrigger', (content: MobileInkOverlayContent) => {
- GestureOverlay.Instance.enableMobileInkOverlay(content);
- MobileInkOverlay.Instance.initMobileInkOverlay(content);
+ //GestureOverlay.Instance.enableMobileInkOverlay(content);
+ // MobileInkOverlay.Instance.initMobileInkOverlay(content);
});
_socket.addEventListener('receiveUpdateOverlayPosition', (content: UpdateMobileInkOverlayPositionContent) => {
- MobileInkOverlay.Instance.updatePosition(content);
+ // MobileInkOverlay.Instance.updatePosition(content);
});
_socket.addEventListener('receiveMobileDocumentUpload', (content: MobileDocumentUploadContent) => {
- MobileInkOverlay.Instance.uploadDocument(content);
+ // MobileInkOverlay.Instance.uploadDocument(content);
});
}
@@ -352,7 +366,7 @@ export namespace DocServer {
// i) which documents need to be fetched
// ii) which are already in the process of being fetched
// iii) which already exist in the cache
- for (const id of ids) {
+ for (const id of ids.filter(id => id)) {
const cached = _cache[id];
if (cached === undefined) {
defaultPromises.push({
@@ -381,7 +395,7 @@ export namespace DocServer {
// fields for the given ids. This returns a promise, which, when resolved, indicates that all the JSON serialized versions of
// the fields have been returned from the server
console.log('Requesting ' + requestedIds.length);
- FieldLoader.active && runInAction(() => (FieldLoader.ServerLoadStatus.requested = requestedIds.length));
+ setTimeout(() => runInAction(() => (FieldLoader.ServerLoadStatus.requested = requestedIds.length)));
const serializedFields = await Utils.EmitCallback(_socket, MessageStore.GetRefFields, requestedIds);
// 3) when the serialized RefFields have been received, go head and begin deserializing them into objects.
@@ -391,7 +405,7 @@ export namespace DocServer {
console.log('deserializing ' + serializedFields.length + ' fields');
for (const field of serializedFields) {
processed++;
- if (FieldLoader.active && processed % 150 === 0) {
+ if (processed % 150 === 0) {
runInAction(() => (FieldLoader.ServerLoadStatus.retrieved = processed));
await new Promise(res => setTimeout(res)); // force loading to yield to splash screen rendering to update progress
}
@@ -477,6 +491,7 @@ export namespace DocServer {
* @param field the [RefField] to be serialized and sent to the server to be stored in the database
*/
export function CreateField(field: RefField) {
+ CacheNeedsUpdate = true;
_CreateField(field);
}
diff --git a/src/client/Network.ts b/src/client/Network.ts
index 39bf69e32..89b31fdca 100644
--- a/src/client/Network.ts
+++ b/src/client/Network.ts
@@ -58,11 +58,16 @@ export namespace Networking {
])
);
}
+ formData.set('fileguids', fileguidpairs.map(pair => pair.guid).join(';'));
+ formData.set('filesize', fileguidpairs.reduce((sum, pair) => sum + pair.file.size, 0).toString());
// 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 {
// Handle the case where fileguidpairs is a single file.
- formData.append(fileguidpairs.guid ?? Utils.GenerateGuid(), fileguidpairs.file);
+ const guids = fileguidpairs.guid ?? Utils.GenerateGuid();
+ formData.set('fileguids', guids);
+ formData.set('filesize', fileguidpairs.file.size.toString());
+ formData.append(guids, fileguidpairs.file);
}
const parameters = {
method: 'POST',
@@ -74,10 +79,10 @@ export namespace Networking {
return response.json();
}
- export async function UploadYoutubeToServer<T extends Upload.FileInformation = Upload.FileInformation>(videoId: string): Promise<Upload.FileResponse<T>[]> {
+ export async function UploadYoutubeToServer<T extends Upload.FileInformation = Upload.FileInformation>(videoId: string, overwriteId?: string): Promise<Upload.FileResponse<T>[]> {
const parameters = {
method: 'POST',
- body: JSON.stringify({ videoId }),
+ body: JSON.stringify({ videoId, overwriteId }),
json: true,
};
const response = await fetch('/uploadYoutubeVideo', parameters);
diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index 4b3960902..6bde7989b 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -14,7 +14,7 @@ type GPTCallOpts = {
};
const callTypeMap: { [type: string]: GPTCallOpts } = {
- summary: { model: 'text-davinci-003', maxTokens: 256, temp: 0.5, prompt: 'Summarize this text briefly: ' },
+ summary: { model: 'text-davinci-003', maxTokens: 256, temp: 0.5, prompt: 'Summarize this text in simpler terms: ' },
edit: { model: 'text-davinci-003', maxTokens: 256, temp: 0.5, prompt: 'Reword this: ' },
completion: { model: 'text-davinci-003', maxTokens: 256, temp: 0.5, prompt: '' },
};
@@ -39,7 +39,6 @@ const gptAPICall = async (inputText: string, callType: GPTCallType) => {
temperature: opts.temp,
prompt: `${opts.prompt}${inputText}`,
});
- console.log(response.data.choices[0]);
return response.data.choices[0].text;
} catch (err) {
console.log(err);
@@ -47,7 +46,7 @@ const gptAPICall = async (inputText: string, callType: GPTCallType) => {
}
};
-const gptImageCall = async (prompt: string) => {
+const gptImageCall = async (prompt: string, n?: number) => {
try {
const configuration = new Configuration({
apiKey: process.env.OPENAI_KEY,
@@ -55,33 +54,15 @@ const gptImageCall = async (prompt: string) => {
const openai = new OpenAIApi(configuration);
const response = await openai.createImage({
prompt: prompt,
- n: 1,
+ n: n ?? 1,
size: '1024x1024',
});
- return response.data.data[0].url;
+ return response.data.data.map(data => data.url);
+ // 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/apis/youtube/YoutubeBox.tsx b/src/client/apis/youtube/YoutubeBox.tsx
index 05879a247..2da9927c0 100644
--- a/src/client/apis/youtube/YoutubeBox.tsx
+++ b/src/client/apis/youtube/YoutubeBox.tsx
@@ -6,7 +6,7 @@ import { Cast, NumCast, StrCast } from '../../../fields/Types';
import { Utils } from '../../../Utils';
import { DocServer } from '../../DocServer';
import { Docs } from '../../documents/Documents';
-import { DocumentDecorations } from '../../views/DocumentDecorations';
+import { DocumentView } from '../../views/nodes/DocumentView';
import { FieldView, FieldViewProps } from '../../views/nodes/FieldView';
import '../../views/nodes/WebBox.scss';
import './YoutubeBox.scss';
@@ -355,9 +355,9 @@ export class YoutubeBox extends React.Component<FieldViewProps> {
</div>
);
- const frozen = !this.props.isSelected() || DocumentDecorations.Instance.Interacting;
+ const frozen = !this.props.isSelected() || DocumentView.Interacting;
- const classname = 'webBox-cont' + (this.props.isSelected() && Doc.ActiveTool === InkTool.None && !DocumentDecorations.Instance.Interacting ? '-interactive' : '');
+ const classname = 'webBox-cont' + (this.props.isSelected() && Doc.ActiveTool === InkTool.None && !DocumentView.Interacting ? '-interactive' : '');
return (
<>
<div className={classname}>{content}</div>
diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts
index b629d9b8c..f8d129e79 100644
--- a/src/client/documents/DocumentTypes.ts
+++ b/src/client/documents/DocumentTypes.ts
@@ -14,13 +14,11 @@ export enum DocumentType {
INK = 'ink',
SCREENSHOT = 'screenshot',
FONTICON = 'fonticonbox',
- FILTER = 'filter',
SEARCH = 'search', // search query
LABEL = 'label', // simple text label
BUTTON = 'button', // onClick button
WEBCAM = 'webcam', // webcam
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
FUNCPLOT = 'funcplot', // function plotter
@@ -31,16 +29,15 @@ export enum DocumentType {
// special purpose wrappers that either take no data or are compositions of lower level types
LINK = 'link',
- LINKANCHOR = 'linkanchor',
IMPORT = 'import',
SLIDER = 'slider',
PRES = 'presentation',
PRESELEMENT = 'preselement',
COLOR = 'color',
YOUTUBE = 'youtube',
- SEARCHITEM = 'searchitem',
COMPARISON = 'comparison',
GROUP = 'group',
+ PUSHPIN = "pushpin",
SCRIPTDB = 'scriptdb', // database of scripts
GROUPDB = 'groupdb', // database of groups
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index f22fa9f17..254be67b9 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1,5 +1,5 @@
import { IconProp } from '@fortawesome/fontawesome-svg-core';
-import { action, runInAction } from 'mobx';
+import { action, reaction, runInAction } from 'mobx';
import { basename } from 'path';
import { DateField } from '../../fields/DateField';
import { Doc, DocListCast, Field, Opt, updateCachedAcls } from '../../fields/Doc';
@@ -15,11 +15,10 @@ import { BoolCast, Cast, DocCast, FieldValue, NumCast, ScriptCast, StrCast } fro
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';
+import { OmitKeys, Utils } from '../../Utils';
import { YoutubeBox } from '../apis/youtube/YoutubeBox';
import { DocServer } from '../DocServer';
import { Networking } from '../Network';
-import { DocumentManager } from '../util/DocumentManager';
import { DragManager, dropActionType } from '../util/DragManager';
import { DirectoryImportBox } from '../util/Import & Export/DirectoryImportBox';
import { FollowLinkScript } from '../util/LinkFollower';
@@ -33,13 +32,13 @@ import { ContextMenu } from '../views/ContextMenu';
import { ContextMenuProps } from '../views/ContextMenuItem';
import { DFLT_IMAGE_NATIVE_DIM } from '../views/global/globalCssVariables.scss';
import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, InkingStroke } from '../views/InkingStroke';
-import { AudioBox } from '../views/nodes/AudioBox';
-import { FontIconBox } from '../views/nodes/FontIconBox/FontIconBox';
+import { AudioBox, media_state } from '../views/nodes/AudioBox';
import { ColorBox } from '../views/nodes/ColorBox';
import { ComparisonBox } from '../views/nodes/ComparisonBox';
import { DataVizBox } from '../views/nodes/DataVizBox/DataVizBox';
import { EquationBox } from '../views/nodes/EquationBox';
import { FieldViewProps } from '../views/nodes/FieldView';
+import { FontIconBox } from '../views/nodes/FontIconBox/FontIconBox';
import { FormattedTextBox } from '../views/nodes/formattedText/FormattedTextBox';
import { FunctionPlotBox } from '../views/nodes/FunctionPlotBox';
import { ImageBox } from '../views/nodes/ImageBox';
@@ -49,6 +48,7 @@ import { LinkBox } from '../views/nodes/LinkBox';
import { LinkDescriptionPopup } from '../views/nodes/LinkDescriptionPopup';
import { LoadingBox } from '../views/nodes/LoadingBox';
import { MapBox } from '../views/nodes/MapBox/MapBox';
+import { MapPushpinBox } from '../views/nodes/MapBox/MapPushpinBox';
import { PDFBox } from '../views/nodes/PDFBox';
import { PhysicsSimulationBox } from '../views/nodes/PhysicsBox/PhysicsSimulationBox';
import { RecordingBox } from '../views/nodes/RecordingBox/RecordingBox';
@@ -58,7 +58,6 @@ import { SliderBox } from '../views/nodes/SliderBox';
import { TaskCompletionBox } from '../views/nodes/TaskCompletedBox';
import { PresBox } from '../views/nodes/trails/PresBox';
import { PresElementBox } from '../views/nodes/trails/PresElementBox';
-import { ImportElementBox } from '../views/nodes/importBox/ImportElementBox';
import { VideoBox } from '../views/nodes/VideoBox';
import { WebBox } from '../views/nodes/WebBox';
import { SearchBox } from '../views/search/SearchBox';
@@ -75,6 +74,8 @@ export class FInfo {
readOnly: boolean = false;
fieldType?: string = '';
values?: Field[];
+
+ filterable?: boolean = true;
// format?: string; // format to display values (e.g, decimal places, $, etc)
// parse?: ScriptField; // parse a value from a string
constructor(d: string, readOnly?: boolean) {
@@ -85,63 +86,79 @@ export class FInfo {
class BoolInfo extends FInfo {
fieldType? = 'boolean';
values?: boolean[] = [true, false];
+ constructor(d: string, filterable?: boolean) {
+ super(d);
+ this.filterable = filterable;
+ }
}
class NumInfo extends FInfo {
fieldType? = 'number';
values?: number[] = [];
- constructor(d: string, readOnly?: boolean, values?: number[]) {
+ constructor(d: string, filterable?: boolean, readOnly?: boolean, values?: number[]) {
super(d, readOnly);
this.values = values;
+ this.filterable = filterable;
}
}
class StrInfo extends FInfo {
fieldType? = 'string';
values?: string[] = [];
- constructor(d: string, readOnly?: boolean, values?: string[]) {
+ constructor(d: string, filterable?: boolean, readOnly?: boolean, values?: string[]) {
super(d, readOnly);
this.values = values;
+ this.filterable = filterable;
}
}
class DocInfo extends FInfo {
fieldType? = 'Doc';
values?: Doc[] = [];
- constructor(d: string, values?: Doc[]) {
+ constructor(d: string, filterable?: boolean, values?: Doc[]) {
super(d, true);
this.values = values;
+ this.filterable = filterable;
}
}
class DimInfo extends FInfo {
fieldType? = 'enumeration';
values? = [DimUnit.Pixel, DimUnit.Ratio];
readOnly = false;
+ filterable = false;
}
class PEInfo extends FInfo {
fieldType? = 'enumeration';
values? = ['all', 'none'];
readOnly = false;
+ filterable = false;
}
class DAInfo extends FInfo {
fieldType? = 'enumeration';
values? = ['embed', 'copy', 'move', 'same', 'proto', 'none'];
readOnly = false;
+ filterable = false;
}
class CTypeInfo extends FInfo {
fieldType? = 'enumeration';
values? = Array.from(Object.keys(CollectionViewType));
readOnly = false;
+ filterable = false;
}
class DTypeInfo extends FInfo {
fieldType? = 'enumeration';
values? = Array.from(Object.keys(DocumentType));
- readOnly = true;
}
class DateInfo extends FInfo {
fieldType? = 'date';
values?: DateField[] = [];
+ filterable = true;
+}
+class ListInfo extends FInfo {
+ fieldType? = 'list';
+ values?: List<any>[] = [];
}
type BOOLt = BoolInfo | boolean;
type NUMt = NumInfo | number;
type STRt = StrInfo | string;
+type LISTt = ListInfo | List<any>;
type DOCt = DocInfo | Doc;
type DIMt = DimInfo | typeof DimUnit.Pixel | typeof DimUnit.Ratio;
type PEVt = PEInfo | 'none' | 'all';
@@ -151,53 +168,57 @@ type DATEt = DateInfo | number;
type DTYPEt = DTypeInfo | string;
export class DocumentOptions {
// coordinate and dimensions depending on view
- x?: NUMt = new NumInfo('x coordinate of document in a freeform view');
- y?: NUMt = new NumInfo('y coordinage of document in a freeform view');
- z?: NUMt = new NumInfo('whether document is in overlay (1) or not (0)', false, [1, 0]);
- _dimMagnitude?: NUMt = new NumInfo("magnitude of collectionMulti{row,col} element's width or height");
+ x?: NUMt = new NumInfo('x coordinate of document in a freeform view', false);
+ y?: NUMt = new NumInfo('y coordinage of document in a freeform view', false);
+ z?: NUMt = new NumInfo('whether document is in overlay (1) or not (0)', false, false, [1, 0]);
+ _dimMagnitude?: NUMt = new NumInfo("magnitude of collectionMulti{row,col} element's width or height", false);
_dimUnit?: DIMt = new DimInfo("units of collectionMulti{row,col} element's width or height - 'px' or '*' for pixels or relative units");
- lat?: NUMt = new NumInfo('latitude coordinate for map views');
- lng?: NUMt = new NumInfo('longitude coordinate for map views');
- _timecodeToShow?: NUMt = new NumInfo('the time that a document should be displayed (e.g., when an annotation shows up as a video plays)');
- _timecodeToHide?: NUMt = new NumInfo('the time that a document should be hidden');
+ latitude?: NUMt = new NumInfo('latitude coordinate for map views', false);
+ longitude?: NUMt = new NumInfo('longitude coordinate for map views', false);
+ map?: STRt = new StrInfo('text location of map');
+ map_type?: STRt = new StrInfo('type of map view', false);
+ map_zoom?: NUMt = new NumInfo('zoom of a map view', false);
+ _timecodeToShow?: NUMt = new NumInfo('the time that a document should be displayed (e.g., when an annotation shows up as a video plays)', false);
+ _timecodeToHide?: NUMt = new NumInfo('the time that a document should be hidden', false);
_width?: NUMt = new NumInfo('displayed width of a document');
_height?: NUMt = new NumInfo('displayed height of document');
- data_nativeWidth?: NUMt = new NumInfo('native width of data field contents (e.g., the pixel width of an image)');
- data_nativeHeight?: NUMt = new NumInfo('native height of data field contents (e.g., the pixel height of an image)');
- linearBtnWidth?: NUMt = new NumInfo('unexpanded width of a linear menu button (button "width" changes when it expands)');
- _nativeWidth?: NUMt = new NumInfo('native width of document contents (e.g., the pixel width of an image)');
- _nativeHeight?: NUMt = new NumInfo('native height of document contents (e.g., the pixel height of an image)');
- _nativeDimModifiable?: BOOLt = new BoolInfo('native dimensions can be modified using document decoration reizers');
+ data_nativeWidth?: NUMt = new NumInfo('native width of data field contents (e.g., the pixel width of an image)', false);
+ data_nativeHeight?: NUMt = new NumInfo('native height of data field contents (e.g., the pixel height of an image)', false);
+ linearBtnWidth?: NUMt = new NumInfo('unexpanded width of a linear menu button (button "width" changes when it expands)', false);
+ _nativeWidth?: NUMt = new NumInfo('native width of document contents (e.g., the pixel width of an image)', false);
+ _nativeHeight?: NUMt = new NumInfo('native height of document contents (e.g., the pixel height of an image)', false);
+ _nativeDimModifiable?: BOOLt = new BoolInfo('native dimensions can be modified using document decoration reizers', false);
_nativeHeightUnfrozen?: BOOLt = new BoolInfo('native height can be changed independent of width by dragging decoration resizers');
- 'acl-Guest'?: string; // public permissions
+ 'acl-Guest'?: STRt = new StrInfo("permissions granted to users logged in as 'guest' (either view, or private)"); // public permissions
'_acl-Guest'?: string; // public permissions
type?: DTYPEt = new DTypeInfo('type of document', true);
type_collection?: COLLt = new CTypeInfo('how collection is rendered'); // sub type of a collection
_type_collection?: COLLt = new CTypeInfo('how collection is rendered'); // sub type of a collection
- title?: STRt = new StrInfo('title of document');
+ title?: STRt = new StrInfo('title of document', true);
caption?: RichTextField;
author?: string; // STRt = new StrInfo('creator of document'); // bcz: don't change this. Otherwise, the userDoc's field Infos will have a FieldInfo assigned to its author field which will render it unreadable
author_date?: DATEt = new DateInfo('date the document was created', true);
- annotationOn?: DOCt = new DocInfo('document annotated by this document');
- color?: STRt = new StrInfo('foreground color data doc');
- hidden?: BOOLt = new BoolInfo('whether the document is not rendered by its collection');
- backgroundColor?: STRt = new StrInfo('background color for data doc');
- opacity?: NUMt = new NumInfo('document opacity');
- viewTransitionTime?: NUMt = new NumInfo('transition duration for view parameters');
- dontRegisterView?: BOOLt = new BoolInfo('are views of this document registered so that they can be found when following links, etc');
+ annotationOn?: DOCt = new DocInfo('document annotated by this document', false);
+ embedContainer?: DOCt = new DocInfo('document that displays (contains) this discument', false);
+ color?: STRt = new StrInfo('foreground color data doc', false);
+ hidden?: BOOLt = new BoolInfo('whether the document is not rendered by its collection', false);
+ backgroundColor?: STRt = new StrInfo('background color for data doc', false);
+ opacity?: NUMt = new NumInfo('document opacity', false);
+ viewTransitionTime?: NUMt = new NumInfo('transition duration for view parameters', false);
+ dontRegisterView?: BOOLt = new BoolInfo('are views of this document registered so that they can be found when following links, etc', false);
_undoIgnoreFields?: List<string>; //'fields that should not be added to the undo stack (opacity for Undo/Redo/and sidebar) AND whether modifications to document are undoable (true for linearview menu buttons to prevent open/close from entering undo stack)'
undoIgnoreFields?: List<string>; //'fields that should not be added to the undo stack (opacity for Undo/Redo/and sidebar) AND whether modifications to document are undoable (true for linearview menu buttons to prevent open/close from entering undo stack)'
- _headerHeight?: NUMt = new NumInfo('height of document header used for displaying title');
- _headerFontSize?: NUMt = new NumInfo('font size of header of custom notes');
+ _headerHeight?: NUMt = new NumInfo('height of document header used for displaying title', false);
+ _headerFontSize?: NUMt = new NumInfo('font size of header of custom notes', false);
_headerPointerEvents?: PEVt = new PEInfo('types of events the header of a custom text document can consume');
_lockedPosition?: BOOLt = new BoolInfo("lock the x,y coordinates of the document so that it can't be dragged");
_lockedTransform?: BOOLt = new BoolInfo('lock the freeform_panx,freeform_pany and scale parameters of the document so that it be panned/zoomed');
layout?: string | Doc; // default layout string or template document
- layout_keyValue?: STRt = new StrInfo('layout definition for showing keyValue view of document');
- layout_explainer?: STRt = new StrInfo('explanation displayed at top of a collection to describe its purpose');
- layout_headerButton?: DOCt = new DocInfo('the (button) Doc to display at the top of a collection.');
+ layout_keyValue?: STRt = new StrInfo('layout definition for showing keyValue view of document', false);
+ layout_explainer?: STRt = new StrInfo('explanation displayed at top of a collection to describe its purpose', false);
+ layout_headerButton?: DOCt = new DocInfo('the (button) Doc to display at the top of a collection.', false);
layout_disableBrushing?: BOOLt = new BoolInfo('whether to suppress border highlighting');
layout_unrendered?: BOOLt = new BoolInfo('denotes an annotation that is not rendered with a DocumentView (e.g, rtf/pdf text selections and links to scroll locations in web/pdf)');
layout_hideOpenButton?: BOOLt = new BoolInfo('whether to hide the open full screen button when selected');
@@ -209,33 +230,33 @@ export class DocumentOptions {
layout_hideDecorationTitle?: BOOLt = new BoolInfo('whether to suppress the document decortations title when selected');
layout_borderRounding?: string;
layout_boxShadow?: string; // box-shadow css string OR "standard" to use dash standard box shadow
- layout_maxAutoHeight?: NUMt = new NumInfo('maximum height for newly created (eg, from pasting) text documents');
+ layout_maxAutoHeight?: NUMt = new NumInfo('maximum height for newly created (eg, from pasting) text documents', false);
_layout_autoHeight?: BOOLt = new BoolInfo('whether document automatically resizes vertically to display contents');
- _layout_curPage?: NUMt = new NumInfo('current page of a PDF or other? paginated document');
- _layout_currentTimecode?: NUMt = new NumInfo('the current timecode of a time-based document (e.g., current time of a video) value is in seconds');
+ _layout_curPage?: NUMt = new NumInfo('current page of a PDF or other? paginated document', false);
+ _layout_currentTimecode?: NUMt = new NumInfo('the current timecode of a time-based document (e.g., current time of a video) value is in seconds', false);
_layout_hideContextMenu?: BOOLt = new BoolInfo('whether the context menu can be shown');
_layout_fitWidth?: BOOLt = new BoolInfo('whether document should scale its contents to fit its rendered width or not (e.g., for PDFviews)');
_layout_fitContentsToBox?: BOOLt = new BoolInfo('whether a freeformview should zoom/scale to create a shrinkwrapped view of its content');
- _layout_fieldKey?: STRt = new StrInfo('the field key containing the current layout definition');
+ _layout_fieldKey?: STRt = new StrInfo('the field key containing the current layout definition', false);
_layout_enableAltContentUI?: BOOLt = new BoolInfo('whether to show alternate content button');
_layout_showTitle?: string; // field name to display in header (:hover is an optional suffix)
_layout_showSidebar?: BOOLt = new BoolInfo('whether an annotationsidebar should be displayed for text docuemnts');
_layout_showCaption?: string; // which field to display in the caption area. leave empty to have no caption
_chromeHidden?: BOOLt = new BoolInfo('whether the editing chrome for a document is hidden');
- _gridGap?: NUMt = new NumInfo('gap between items in masonry view');
- _xMargin?: NUMt = new NumInfo('gap between left edge of document and start of masonry/stacking layouts');
- _yMargin?: NUMt = new NumInfo('gap between top edge of dcoument and start of masonry/stacking layouts');
- _xPadding?: NUMt = new NumInfo('x padding');
- _yPadding?: NUMt = new NumInfo('y padding');
+ _gridGap?: NUMt = new NumInfo('gap between items in masonry view', false);
+ _xMargin?: NUMt = new NumInfo('gap between left edge of document and start of masonry/stacking layouts', false);
+ _yMargin?: NUMt = new NumInfo('gap between top edge of dcoument and start of masonry/stacking layouts', false);
+ _xPadding?: NUMt = new NumInfo('x padding', false);
+ _yPadding?: NUMt = new NumInfo('y padding', false);
_singleLine?: boolean; // whether label box is restricted to one line of text
_createDocOnCR?: boolean; // whether carriage returns and tabs create new text documents
- _columnWidth?: NUMt = new NumInfo('width of table column');
+ _columnWidth?: NUMt = new NumInfo('width of table column', false);
_columnsHideIfEmpty?: BOOLt = new BoolInfo('whether stacking view column headings should be hidden');
- _caption_xMargin?: NUMt = new NumInfo('x margin of caption inside of a carousel collection', true);
- _caption_yMargin?: NUMt = new NumInfo('y margin of caption inside of a carousel collection', true);
- icon_nativeWidth?: NUMt = new NumInfo('native width of icon view', true);
- icon_nativeHeight?: NUMt = new NumInfo('native height of icon view', true);
+ _caption_xMargin?: NUMt = new NumInfo('x margin of caption inside of a carousel collection', false, true);
+ _caption_yMargin?: NUMt = new NumInfo('y margin of caption inside of a carousel collection', false, true);
+ icon_nativeWidth?: NUMt = new NumInfo('native width of icon view', false, true);
+ icon_nativeHeight?: NUMt = new NumInfo('native height of icon view', false, true);
_text_fontSize?: string;
_text_fontFamily?: string;
_text_fontWeight?: string;
@@ -243,14 +264,16 @@ export class DocumentOptions {
infoWindowOpen?: BOOLt = new BoolInfo('whether info window corresponding to pin is open (on MapDocuments)');
_carousel_index?: NUMt = new NumInfo('which item index the carousel viewer is showing');
- _label_minFontSize?: NUMt = new NumInfo('minimum font size for labelBoxes');
- _label_maxFontSize?: NUMt = new NumInfo('maximum font size for labelBoxes');
- stroke_width?: NUMt = new NumInfo('width of an ink stroke');
- icon_label?: STRt = new StrInfo('label to use for a fontIcon doc (otherwise, the title is used)');
- mediaState?: STRt = new StrInfo('status of audio/video media document: "pendingRecording", "recording", "paused", "playing"');
+ _label_minFontSize?: NUMt = new NumInfo('minimum font size for labelBoxes', false);
+ _label_maxFontSize?: NUMt = new NumInfo('maximum font size for labelBoxes', false);
+ stroke_width?: NUMt = new NumInfo('width of an ink stroke', false);
+ icon_label?: STRt = new StrInfo('label to use for a fontIcon doc (otherwise, the title is used)', false);
+ mediaState?: STRt = new StrInfo(`status of audio/video media document: ${media_state.PendingRecording}, ${media_state.Recording}, ${media_state.Paused}, ${media_state.Playing}`, false);
recording?: BOOLt = new BoolInfo('whether WebCam is recording or not');
autoPlayAnchors?: BOOLt = new BoolInfo('whether to play audio/video when an anchor is clicked in a stackedTimeline.');
dontPlayLinkOnSelect?: BOOLt = new BoolInfo('whether an audio/video should start playing when a link is followed to it.');
+ openFactoryLocation?: string; // an OpenWhere value to place the factory created document
+ openFactoryAsDelegate?: boolean; //
updateContentsScript?: ScriptField; // reactive script invoked when viewing a document that can update contents of a collection (or do anything)
toolTip?: string; // tooltip to display on hover
toolType?: string; // type of pen tool
@@ -263,7 +286,7 @@ export class DocumentOptions {
contextMenuIcons?: List<string>;
childFilters_boolean?: string;
childFilters?: List<string>;
- childLimitHeight?: NUMt = new NumInfo('whether to limit the height of collection children. 0 - means height can be no bigger than width');
+ childLimitHeight?: NUMt = new NumInfo('whether to limit the height of collection children. 0 - means height can be no bigger than width', false);
childLayoutTemplate?: Doc; // template for collection to use to render its children (see PresBox layout in tree view)
childLayoutString?: string; // template string for collection to use to render its children
childDocumentsActive?: BOOLt = new BoolInfo('whether child documents are active when parent is document active');
@@ -275,12 +298,12 @@ export class DocumentOptions {
childContextMenuIcons?: List<string>;
targetScriptKey?: string; // where to write a template script (used by collections with click templates which need to target onClick, onDoubleClick, etc)
- lastFrame?: NUMt = new NumInfo('the last frame of a frame-based collection (e.g., progressive slide)');
- activeFrame?: NUMt = new NumInfo('the active frame of a document in a frame base collection');
- appearFrame?: NUMt = new NumInfo('the frame in which the document appears');
- _currentFrame?: NUMt = new NumInfo('the current frame of a frame-based collection (e.g., progressive slide)');
+ lastFrame?: NUMt = new NumInfo('the last frame of a frame-based collection (e.g., progressive slide)', false);
+ activeFrame?: NUMt = new NumInfo('the active frame of a document in a frame base collection', false);
+ appearFrame?: NUMt = new NumInfo('the frame in which the document appears', false);
+ _currentFrame?: NUMt = new NumInfo('the current frame of a frame-based collection (e.g., progressive slide)', false);
- isSystem?: BOOLt = new BoolInfo('is this a system created/owned doc', true);
+ isSystem?: BOOLt = new BoolInfo('is this a system created/owned doc', false);
isBaseProto?: BOOLt = new BoolInfo('is doc a base level prototype for data documents as opposed to data documents which are prototypes for layout documents. base protos are not cloned during a deep');
isTemplateForField?: string; // the field key for which the containing document is a rendering template
isTemplateDoc?: BOOLt = new BoolInfo('is the document a template for creating other documents');
@@ -289,28 +312,38 @@ export class DocumentOptions {
_isTimelineLabel?: BOOLt = new BoolInfo('is document a timeline label');
_isLightbox?: BOOLt = new BoolInfo('whether a collection acts as a lightbox by opening lightbox links by hiding all other documents in collection besides link target');
- 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');
+ mapPin?: DOCt = new DocInfo('pin associated with a config anchor', false);
+ config_latitude?: NUMt = new NumInfo('latitude of a map', false);
+ config_longitude?: NUMt = new NumInfo('longitude of map', false);
+ config_map_zoom?: NUMt = new NumInfo('zoom of map', false);
+ config_map_type?: STRt = new StrInfo('map view type (e.g, aerial)', false);
+ config_map?: STRt = new StrInfo('text location of map', false);
+ config_panX?: NUMt = new NumInfo('panX saved as a view spec', false);
+ config_panY?: NUMt = new NumInfo('panY saved as a view spec', false);
+ config_viewScale?: NUMt = new NumInfo('viewScale saved as a view Spec', false);
+ presentation_transition?: NUMt = new NumInfo('the time taken for the transition TO a document', false);
+ presentation_duration?: NUMt = new NumInfo('the duration of the slide in presentation view', false);
+ presentation_zoomText?: BOOLt = new BoolInfo('whether text anchors should shown in a larger box when following links to make them stand out', false);
data?: any;
data_useCors?: BOOLt = new BoolInfo('whether CORS protocol should be used for web page');
columnHeaders?: List<SchemaHeaderField>; // headers for stacking views
schemaHeaders?: List<SchemaHeaderField>; // headers for schema view
- dockingConfig?: STRt = new StrInfo('configuration of golden layout windows (applies only if doc is rendered as a CollectionDockingView)');
+ dockingConfig?: STRt = new StrInfo('configuration of golden layout windows (applies only if doc is rendered as a CollectionDockingView)', false);
icon?: string; // icon used by fonticonbox to render button
noteType?: string;
+ // STOPPING HERE
+
// freeform properties
- _freeform_backgroundGrid?: boolean;
+ _freeform_backgroundGrid?: BOOLt = new BoolInfo('whether background grid is shown on freeform collections');
+ _freeform_scale_min?: NUMt = new NumInfo('how far out a view can zoom (used by image/videoBoxes that are clipped');
+ _freeform_scale_max?: NUMt = new NumInfo('how far in a view can zoom (used by sidebar freeform views');
_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');
+ _freeform_noZoom?: BOOLt = new BoolInfo('disables zooming (used by Pile docs)');
//BUTTONS
buttonText?: string;
@@ -336,8 +369,8 @@ export class DocumentOptions {
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_anchor_1?: DOCt = new DocInfo('start anchor of a link');
+ link_anchor_2?: DOCt = new DocInfo('end anchor of a link');
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)');
@@ -360,11 +393,9 @@ export class DocumentOptions {
waitForDoubleClickToClick?: 'always' | 'never' | 'default'; // whether a click function wait for double click to expire. 'default' undefined = wait only if there's a click handler, "never" = never wait, "always" = alway wait
onPointerDown?: ScriptField;
onPointerUp?: ScriptField;
- openFactoryLocation?: string; // an OpenWhere value to place the factory created document
- openFactoryAsDelegate?: BOOLt = new BoolInfo('create a delegate of the factory');
_forceActive?: BOOLt = new BoolInfo('flag to handle pointer events when not selected (or otherwise active)');
_dragOnlyWithinContainer?: BOOLt = new BoolInfo('whether the document should remain in its collection when someone tries to drag and drop it elsewhere');
- _raiseWhenDragged?: BOOLt = new BoolInfo('whether a document is brought to front when dragged.');
+ _keepZWhenDragged?: BOOLt = new BoolInfo('whether a document should keep its z-order when dragged.');
childDragAction?: DROPt = new DAInfo('what should happen to the child documents when they are dragged from the collection');
dropConverter?: ScriptField; // script to run when documents are dropped on this Document.
dropAction?: DROPt = new DAInfo("what should happen to this document when it's dropped somewhere else");
@@ -373,30 +404,30 @@ export class DocumentOptions {
cloneFieldFilter?: List<string>; // fields not to copy when the document is clonedclipboard?: Doc;
dragWhenActive?: BOOLt = new BoolInfo('should document drag when it is active - e.g., pileView, group');
dragAction?: DROPt = new DAInfo('how to drag document when it is active (e.g., tree, groups)');
- dragFactory_count?: NUMt = new NumInfo('number of items created from a drag button (used for setting title with incrementing index)', true);
- dragFactory?: 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
+ dragFactory_count?: NUMt = new NumInfo('number of items created from a drag button (used for setting title with incrementing index)', false, true);
+ dragFactory?: DOCt = new DocInfo('document to create when dragging with a suitable onDragStart script', false);
+ clickFactory?: DOCt = new DocInfo('document to create when clicking on a button with a suitable onClick script', false);
onDragStart?: ScriptField; //script to execute at start of drag operation -- e.g., when a "creator" button is dragged this script generates a different document to drop
target?: Doc; // available for use in scripts. used to provide a document parameter to the script (Note, this is a convenience entry since any field could be used for parameterizing a script)
- 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; //
- 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?: BOOLt = new BoolInfo('whether the expanded view can be changed');
- treeViewChecked?: ScriptField; // script to call when a tree view checkbox is checked
- 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
- treeViewFreezeChildren?: STRt = new StrInfo('set (add, remove, add|remove) to disable adding, removing or both from collection');
+ treeView_HideTitle?: BOOLt = new BoolInfo('whether to hide the top document title of a tree view');
+ treeView_HideUnrendered?: BOOLt = new BoolInfo("tells tree view not to display documents that have an 'layout_unrendered' tag unless they also have a treeView_FieldKey tag (presBox)");
+ treeView_HideHeaderIfTemplate?: BOOLt = new BoolInfo('whether to hide the header for a document in a tree view only if a childLayoutTemplate is provided (presBox)');
+ treeView_HideHeader?: BOOLt = new BoolInfo('whether to hide the header for a document in a tree view');
+ treeView_HideHeaderFields?: BOOLt = new BoolInfo('whether to hide the drop down options for tree view items.');
+ treeView_ChildDoubleClick?: ScriptField; //
+ treeView_OpenIsTransient?: BOOLt = new BoolInfo("ignores the treeView_Open Doc flag, allowing a treeView_Item's expand/collapse state to be independent of other views of the same document in the same or any other tree view");
+ treeView_Open?: BOOLt = new BoolInfo('whether this document is expanded in a tree view');
+ treeView_ExpandedView?: string; // which field/thing is displayed when this item is opened in tree view
+ treeView_ExpandedViewLock?: BOOLt = new BoolInfo('whether the expanded view can be changed');
+ treeView_Checked?: ScriptField; // script to call when a tree view checkbox is checked
+ treeView_TruncateTitleWidth?: NUMt = new NumInfo('maximum width of a treew view title before truncation');
+ treeView_HasOverlay?: BOOLt = new BoolInfo('whether the treeview has an overlay for freeform annotations');
+ treeView_Type?: string; // whether treeview is a Slide, file system, or (default) collection hierarchy
+ treeView_FreezeChildren?: 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
+ sidebar_type_collection?: string; // collection type of text sidebar
data_dashboards?: List<any>; // list of dashboards used in shareddocs;
text?: string;
@@ -521,7 +552,7 @@ export namespace Docs {
DocumentType.MAP,
{
layout: { view: MapBox, dataField: defaultDataKey },
- options: { _height: 600, _width: 800, nativeDimModifiable: true, systemIcon: 'BsFillPinMapFill' },
+ options: { map: '', _height: 600, _width: 800, nativeDimModifiable: true, systemIcon: 'BsFillPinMapFill' },
},
],
[
@@ -636,7 +667,7 @@ export namespace Docs {
DocumentType.CONFIG,
{
layout: { view: CollectionView, dataField: defaultDataKey },
- options: { layout_hideLinkButton: true, pointerEvents: 'none', layout_unrendered: true },
+ options: { config: '', layout_hideLinkButton: true, layout_unrendered: true },
},
],
[
@@ -680,7 +711,7 @@ export namespace Docs {
DocumentType.DATAVIZ,
{
layout: { view: DataVizBox, dataField: defaultDataKey },
- options: { _layout_fitWidth: true, nativeDimModifiable: true },
+ options: { dataViz_title: '', dataViz_line: '', dataViz_pie: '', dataViz_histogram: '', dataViz: 'table', _layout_fitWidth: true, nativeDimModifiable: true },
},
],
[
@@ -713,6 +744,13 @@ export namespace Docs {
},
},
],
+ [
+ DocumentType.PUSHPIN,
+ {
+ layout: { view: MapPushpinBox, dataField: defaultDataKey },
+ options: {},
+ },
+ ],
]);
const suffix = 'Proto';
@@ -745,6 +783,10 @@ export namespace Docs {
// an entry dedicated to the given DocumentType)
target && PrototypeMap.set(type, target);
});
+ reaction(
+ () => (proto => StrCast(proto?.BROADCAST_MESSAGE))(DocServer.GetCachedRefField('rtfProto') as Doc),
+ msg => msg && alert(msg)
+ );
}
/**
@@ -842,7 +884,7 @@ export namespace Docs {
* only when creating a DockDocument from the current user's already existing
* main document.
*/
- function InstanceFromProto(proto: Doc, data: Field | undefined, options: DocumentOptions, delegId?: string, fieldKey: string = 'data', protoId?: string, placeholderDoc?: Doc) {
+ function InstanceFromProto(proto: Doc, data: Field | undefined, options: DocumentOptions, delegId?: string, fieldKey: string = 'data', protoId?: string, placeholderDoc?: Doc, noView?: boolean) {
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, '^_');
@@ -854,7 +896,7 @@ export namespace Docs {
dataProps.author_date = new DateField();
if (fieldKey) {
dataProps[`${fieldKey}_modificationDate`] = new DateField();
- dataProps[fieldKey] = data;
+ dataProps[fieldKey] = options.data ?? data;
// so that the list of annotations is already initialised, prevents issues in addonly.
// without this, if a doc has no annotations but the user has AddOnly privileges, they won't be able to add an annotation because they would have needed to create the field's list which they don't have permissions to do.
@@ -869,31 +911,34 @@ export namespace Docs {
dataDoc.proto = proto;
}
- const viewFirstProps: { [id: string]: any } = { author: Doc.CurrentUserEmail };
- viewFirstProps['acl-Guest'] = options['_acl-Guest'] ?? (Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.View);
- let viewDoc: Doc;
- // determines whether viewDoc should be created using placeholder Doc or default
- if (placeholderDoc) {
- placeholderDoc._height = options._height !== undefined ? Number(options._height) : undefined;
- placeholderDoc._width = options._width !== undefined ? Number(options._width) : undefined;
- viewDoc = Doc.assign(placeholderDoc, viewFirstProps, true, true);
- Array.from(Object.keys(placeholderDoc))
- .filter(key => key.startsWith('acl'))
- .forEach(key => (dataDoc[key] = viewDoc[key] = placeholderDoc[key]));
- } else {
- viewDoc = Doc.assign(Doc.MakeDelegate(dataDoc, delegId), viewFirstProps, true, true);
- }
- Doc.assign(viewDoc, viewProps, true, true);
- if (![DocumentType.LINK, DocumentType.CONFIG, DocumentType.LABEL].includes(viewDoc.type as any)) {
- DocUtils.MakeLinkToActiveAudio(() => viewDoc);
- }
+ if (!noView) {
+ const viewFirstProps: { [id: string]: any } = { author: Doc.CurrentUserEmail };
+ viewFirstProps['acl-Guest'] = options['_acl-Guest'] ?? (Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.View);
+ let viewDoc: Doc;
+ // determines whether viewDoc should be created using placeholder Doc or default
+ if (placeholderDoc) {
+ placeholderDoc._height = options._height !== undefined ? Number(options._height) : undefined;
+ placeholderDoc._width = options._width !== undefined ? Number(options._width) : undefined;
+ viewDoc = Doc.assign(placeholderDoc, viewFirstProps, true, true);
+ Array.from(Object.keys(placeholderDoc))
+ .filter(key => key.startsWith('acl'))
+ .forEach(key => (dataDoc[key] = viewDoc[key] = placeholderDoc[key]));
+ } else {
+ viewDoc = Doc.assign(Doc.MakeDelegate(dataDoc, delegId), viewFirstProps, true, true);
+ }
+ Doc.assign(viewDoc, viewProps, true, true);
+ if (![DocumentType.LINK, DocumentType.CONFIG, DocumentType.LABEL].includes(viewDoc.type as any)) {
+ DocUtils.MakeLinkToActiveAudio(() => viewDoc);
+ }
+ updateCachedAcls(dataDoc);
+ updateCachedAcls(viewDoc);
- Doc.AddFileOrphan(dataDoc);
+ return viewDoc;
+ }
updateCachedAcls(dataDoc);
- updateCachedAcls(viewDoc);
- return viewDoc;
+ return dataDoc;
}
export function ImageDocument(url: string | ImageField, options: DocumentOptions = {}, overwriteDoc?: Doc) {
@@ -1041,7 +1086,7 @@ export namespace Docs {
const nwid = options._nativeWidth || undefined;
const nhght = options._nativeHeight || undefined;
if (!nhght && width && height && nwid) options._nativeHeight = (Number(nwid) * Number(height)) / Number(width);
- return InstanceFromProto(Prototypes.get(DocumentType.WEB), new WebField(url ? url : 'http://www.bing.com/'), options);
+ return InstanceFromProto(Prototypes.get(DocumentType.WEB), new WebField(url ? url : 'https://www.wikipedia.org/'), options);
}
export function HtmlDocument(html: string, options: DocumentOptions = {}) {
@@ -1052,6 +1097,10 @@ export namespace Docs {
return InstanceFromProto(Prototypes.get(DocumentType.MAP), new List(documents), options);
}
+ export function PushpinDocument(latitude: number, longitude: number, infoWindowOpen: boolean, documents: Array<Doc>, options: DocumentOptions, id?: string) {
+ return InstanceFromProto(Prototypes.get(DocumentType.PUSHPIN), new List(documents), { latitude, longitude, infoWindowOpen, ...options }, id);
+ }
+
// shouldn't ever need to create a KVP document-- instead set the LayoutTemplateString to be a KeyValueBox for the DocumentView (see addDocTab in TabDocView)
// export function KVPDocument(document: Doc, options: DocumentOptions = {}) {
// return InstanceFromProto(Prototypes.get(DocumentType.KVP), document, { title: document.title + '.kvp', ...options });
@@ -1064,18 +1113,20 @@ export namespace Docs {
}
export function ConfigDocument(options: DocumentOptions, id?: string) {
- return InstanceFromProto(Prototypes.get(DocumentType.CONFIG), options?.data, options, id);
+ return InstanceFromProto(Prototypes.get(DocumentType.CONFIG), options?.data, options, id, '', undefined, undefined, true);
}
export function HTMLMarkerDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Freeform }, id);
}
- export function MapMarkerDocument(lat: number, lng: number, infoWindowOpen: boolean, documents: Array<Doc>, options: DocumentOptions, id?: string) {
- return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { lat, lng, infoWindowOpen, ...options, _type_collection: CollectionViewType.Freeform }, id);
- }
export function PileDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
- return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { dropAction: 'move', _forceActive: true, _freeform_noZoom: true, _freeform_noAutoPan: true, ...options, _type_collection: CollectionViewType.Pile }, id);
+ return InstanceFromProto(
+ Prototypes.get(DocumentType.COL),
+ new List(documents),
+ { backgroundColor: 'transparent', dropAction: 'move', _forceActive: true, _freeform_noZoom: true, _freeform_noAutoPan: true, ...options, _type_collection: CollectionViewType.Pile },
+ id
+ );
}
export function LinearDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
@@ -1099,7 +1150,9 @@ export namespace Docs {
}
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, _type_collection: CollectionViewType.Tree }, id, undefined, protoId);
+ const doc = InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _xMargin: 5, _yMargin: 5, ...options, _type_collection: CollectionViewType.Tree }, id, undefined, protoId);
+ Doc.GetProto(doc).treeView = ''; /// not really needed, but makes keyvalue pane look better
+ return doc;
}
export function StackingDocument(documents: Array<Doc>, options: DocumentOptions, id?: string, protoId?: string) {
@@ -1151,20 +1204,17 @@ export namespace Docs {
export function FontIconDocument(options?: DocumentOptions) {
return InstanceFromProto(Prototypes.get(DocumentType.FONTICON), undefined, { ...(options || {}) });
}
- export function FilterDocument(options?: DocumentOptions) {
- return InstanceFromProto(Prototypes.get(DocumentType.FILTER), undefined, { ...(options || {}) });
- }
export function PresElementBoxDocument() {
return Prototypes.get(DocumentType.PRESELEMENT);
}
export function DataVizDocument(url: string, options?: DocumentOptions, overwriteDoc?: Doc) {
- return InstanceFromProto(Prototypes.get(DocumentType.DATAVIZ), new CsvField(url), { title: 'Data Viz', ...options }, undefined, undefined, undefined, overwriteDoc);
+ return InstanceFromProto(Prototypes.get(DocumentType.DATAVIZ), new CsvField(url), { title: 'Data Viz', type: 'dataviz', ...options }, undefined, undefined, undefined, overwriteDoc);
}
export function DockDocument(documents: Array<Doc>, config: string, options: DocumentOptions, id?: string) {
- const ret = InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { treeViewFreezeChildren: 'remove|add', ...options, type_collection: CollectionViewType.Docking, dockingConfig: config }, id);
+ const ret = InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { treeView_FreezeChildren: 'remove|add', ...options, type_collection: CollectionViewType.Docking, dockingConfig: config }, id);
documents.map(c => Doc.SetContainer(c, ret));
return ret;
}
@@ -1243,7 +1293,6 @@ export namespace DocUtils {
if (d.cookies && (!filterFacets.cookies || !Object.keys(filterFacets.cookies).some(key => d.cookies === key))) {
return false;
}
-
for (const facetKey of Object.keys(filterFacets).filter(fkey => fkey !== 'cookies' && fkey !== Utils.noDragsDocFilter.split(Doc.FilterSep)[0])) {
const facet = filterFacets[facetKey];
@@ -1256,16 +1305,16 @@ export namespace DocUtils {
// metadata facets that exist
const exists = Object.keys(facet).filter(value => facet[value] === 'exists');
- // metadata facets that exist
+ // facets that unset metadata (a hack for making cookies work)
const unsets = Object.keys(facet).filter(value => facet[value] === 'unset');
- // facets that have an x next to them
+ // facets that specify that a field must not match a specific value
const xs = Object.keys(facet).filter(value => facet[value] === 'x');
if (!unsets.length && !exists.length && !xs.length && !checks.length && !matches.length) return true;
const failsNotEqualFacets = !xs.length ? false : xs.some(value => Doc.matchFieldValue(d, facetKey, value));
const satisfiesCheckFacets = !checks.length ? true : checks.some(value => Doc.matchFieldValue(d, facetKey, value));
- const satisfiesExistsFacets = !exists.length ? true : exists.some(value => d[facetKey] !== undefined);
+ const satisfiesExistsFacets = !exists.length ? true : exists.some(value => (facetKey !== '-linkedTo' ? d[facetKey] !== undefined : LinkManager.Instance.getAllRelatedLinks(d).length));
const satisfiesUnsetsFacets = !unsets.length ? true : unsets.some(value => d[facetKey] === undefined);
const satisfiesMatchFacets = !matches.length
? true
@@ -1309,7 +1358,7 @@ export namespace DocUtils {
export let ActiveRecordings: { props: FieldViewProps; getAnchor: (addAsAnnotation: boolean) => Doc }[] = [];
export function MakeLinkToActiveAudio(getSourceDoc: () => Doc | undefined, broadcastEvent = true) {
- broadcastEvent && runInAction(() => (DocumentManager.Instance.RecordingEvent = DocumentManager.Instance.RecordingEvent + 1));
+ broadcastEvent && runInAction(() => (Doc.RecordingEvent = Doc.RecordingEvent + 1));
return DocUtils.ActiveRecordings.map(audio => {
const sourceDoc = getSourceDoc();
return sourceDoc && DocUtils.MakeLink(sourceDoc, audio.getAnchor(true) || audio.props.Document, { link_displayLine: false, link_relationship: 'recording annotation:linked recording', link_description: 'recording timeline' });
@@ -1318,7 +1367,6 @@ export namespace DocUtils {
export function MakeLink(source: Doc, target: Doc, linkSettings: { link_relationship?: string; link_description?: string; link_displayLine?: boolean }, id?: string, showPopup?: number[]) {
if (!linkSettings.link_relationship) linkSettings.link_relationship = target.type === DocumentType.RTF ? 'Commentary:Comments On' : 'link';
- const sv = DocumentManager.Instance.getDocumentView(source);
if (target.doc === Doc.UserDoc()) return undefined;
const makeLink = action((linkDoc: Doc, showPopup?: number[]) => {
@@ -1556,7 +1604,7 @@ export namespace DocUtils {
const documentList: ContextMenuProps[] = DocListCast(DocListCast(Doc.MyTools?.data)[0]?.data)
.filter(btnDoc => !btnDoc.hidden)
.map(btnDoc => Cast(btnDoc?.dragFactory, Doc, null))
- .filter(doc => doc && doc !== Doc.UserDoc().emptyTrail && doc !== Doc.UserDoc().emptyDataViz)
+ .filter(doc => doc && doc !== Doc.UserDoc().emptyTrail && doc !== Doc.UserDoc().emptyNote && doc.title)
.map((dragDoc, i) => ({
description: ':' + StrCast(dragDoc.title).replace('Untitled ', ''),
event: undoable((args: { x: number; y: number }) => {
@@ -1652,36 +1700,19 @@ export namespace DocUtils {
}
export function pileup(docList: Doc[], x?: number, y?: number, size: number = 55, create: boolean = true) {
- let w = 0,
- h = 0;
runInAction(() => {
- docList.forEach(d => {
- DocUtils.iconify(d);
- w = Math.max(NumCast(d._width), w);
- h = Math.max(NumCast(d._height), h);
- });
docList.forEach((d, i) => {
- d.x = Math.cos((Math.PI * 2 * i) / docList.length) * size;
- d.y = Math.sin((Math.PI * 2 * i) / docList.length) * size;
- d._timecodeToShow = undefined; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection
- });
- const aggBounds = aggregateBounds(
- docList.map(d => ({ x: NumCast(d.x), y: NumCast(d.y), width: NumCast(d._width), height: NumCast(d._height) })),
- 0,
- 0
- );
- docList.forEach((d, i) => {
- d.x = NumCast(d.x) - (aggBounds.r + aggBounds.x) / 2;
- d.y = NumCast(d.y) - (aggBounds.b + aggBounds.y) / 2;
+ DocUtils.iconify(d);
+ d.x = Math.cos((Math.PI * 2 * i) / docList.length) * size - size;
+ d.y = Math.sin((Math.PI * 2 * i) / docList.length) * size - size;
d._timecodeToShow = undefined; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection
});
});
if (create) {
- const newCollection = Docs.Create.PileDocument(docList, { title: 'pileup', x: (x || 0) - size, y: (y || 0) - size, _width: size * 2, _height: size * 2, dragWhenActive: true });
+ const newCollection = Docs.Create.PileDocument(docList, { title: 'pileup', _freeform_noZoom: true, x: (x || 0) - size, y: (y || 0) - size, _width: size * 2, _height: size * 2, dragWhenActive: true, _layout_fitWidth: false });
newCollection.x = NumCast(newCollection.x) + NumCast(newCollection._width) / 2 - size;
newCollection.y = NumCast(newCollection.y) + NumCast(newCollection._height) / 2 - size;
newCollection._width = newCollection._height = size * 2;
- newCollection._jitterRotation = 10;
return newCollection;
}
}
@@ -1770,8 +1801,8 @@ export namespace DocUtils {
const longitude = result.exifData?.data?.GPSLongitude;
const longitudeDirection = result.exifData?.data?.GPSLongitudeRef;
if (latitude !== undefined && longitude !== undefined && latitudeDirection !== undefined && longitudeDirection !== undefined) {
- proto.lat = ConvertDMSToDD(latitude[0], latitude[1], latitude[2], latitudeDirection);
- proto.lng = ConvertDMSToDD(longitude[0], longitude[1], longitude[2], longitudeDirection);
+ proto.latitude = ConvertDMSToDD(latitude[0], latitude[1], latitude[2], latitudeDirection);
+ proto.longitude = ConvertDMSToDD(longitude[0], longitude[1], longitude[2], longitudeDirection);
}
}
if (Upload.isVideoInformation(result)) {
@@ -1779,8 +1810,6 @@ export namespace DocUtils {
}
if (overwriteDoc) {
Doc.removeCurrentlyLoading(overwriteDoc);
- // loading doc icons are just labels. so any icon views of loading docs need to be replaced with the proper icon view.
- DocumentManager.Instance.getAllDocumentViews(overwriteDoc).forEach(dv => StrCast(dv.rootDoc.layout_fieldKey) === 'layout_icon' && dv.iconify(() => dv.iconify()));
}
generatedDocuments.push(doc);
}
@@ -1813,7 +1842,7 @@ export namespace DocUtils {
export function uploadYoutubeVideoLoading(videoId: string, options: DocumentOptions, overwriteDoc?: Doc) {
const generatedDocuments: Doc[] = [];
- Networking.UploadYoutubeToServer(videoId).then(upfiles => {
+ Networking.UploadYoutubeToServer(videoId, overwriteDoc?.[Id]).then(upfiles => {
const {
source: { name, type },
result,
@@ -1873,7 +1902,6 @@ export namespace DocUtils {
export function copyDragFactory(dragFactory: Doc) {
if (!dragFactory) return undefined;
const ndoc = dragFactory.isTemplateDoc ? Doc.ApplyTemplate(dragFactory) : Doc.MakeCopy(dragFactory, true);
- ndoc && Doc.AddFileOrphan(Doc.GetProto(ndoc));
if (ndoc && dragFactory['dragFactory_count'] !== undefined) {
dragFactory['dragFactory_count'] = NumCast(dragFactory['dragFactory_count']) + 1;
Doc.SetInPlace(ndoc, 'title', ndoc.title + ' ' + NumCast(dragFactory['dragFactory_count']).toString(), true);
diff --git a/src/client/util/BranchingTrailManager.tsx b/src/client/util/BranchingTrailManager.tsx
new file mode 100644
index 000000000..a224b84f4
--- /dev/null
+++ b/src/client/util/BranchingTrailManager.tsx
@@ -0,0 +1,137 @@
+import { action, computed, observable } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { Doc } from '../../fields/Doc';
+import { Id } from '../../fields/FieldSymbols';
+import { PresBox } from '../views/nodes/trails';
+import { OverlayView } from '../views/OverlayView';
+import { DocumentManager } from './DocumentManager';
+import { Docs } from '../documents/Documents';
+import { nullAudio } from '../../fields/URLField';
+
+@observer
+export class BranchingTrailManager extends React.Component {
+ public static Instance: BranchingTrailManager;
+
+ constructor(props: any) {
+ super(props);
+ if (!BranchingTrailManager.Instance) {
+ BranchingTrailManager.Instance = this;
+ }
+ }
+
+ setupUi = () => {
+ OverlayView.Instance.addWindow(<BranchingTrailManager></BranchingTrailManager>, { x: 100, y: 150, width: 1000, title: 'Branching Trail'});
+ // OverlayView.Instance.forceUpdate();
+ console.log(OverlayView.Instance);
+ // let hi = Docs.Create.TextDocument("beee", {
+ // x: 100,
+ // y: 100,
+ // })
+ // hi.overlayX = 100;
+ // hi.overlayY = 100;
+
+ // Doc.AddToMyOverlay(hi);
+ console.log(DocumentManager._overlayViews);
+ };
+
+
+ // stack of the history
+ @observable private slideHistoryStack: String[] = [];
+ @action setSlideHistoryStack = action((newArr: String[]) => {
+ this.slideHistoryStack = newArr;
+ });
+
+ @observable private containsSet: Set<String> = new Set<String>();
+
+ // prev pres to copmare with
+ @observable private prevPresId: String | null = null;
+ @action setPrevPres = action((newId: String | null) => {
+ this.prevPresId = newId;
+ });
+
+ // docId to Doc map
+ @observable private docIdToDocMap: Map<String, Doc> = new Map<String, Doc>();
+
+ observeDocumentChange = (targetDoc: Doc, pres: PresBox) => {
+ const presId = pres.props.Document[Id];
+ if (this.prevPresId === presId) {
+ return;
+ }
+
+ const targetDocId = targetDoc[Id];
+ this.docIdToDocMap.set(targetDocId, targetDoc);
+
+ if (this.prevPresId === null) {
+ this.setupUi();
+ }
+
+ if (this.prevPresId === null || this.prevPresId !== presId) {
+ Doc.UserDoc().isBranchingMode = true;
+ this.setPrevPres(presId);
+
+ // REVERT THE SET
+ const stringified = [presId, targetDocId].toString();
+ if (this.containsSet.has([presId, targetDocId].toString())) {
+ // remove all the elements after the targetDocId
+ const newStack = this.slideHistoryStack.slice(0, this.slideHistoryStack.indexOf(stringified));
+ const removed = this.slideHistoryStack.slice(this.slideHistoryStack.indexOf(stringified));
+ this.setSlideHistoryStack(newStack);
+
+ removed.forEach(info => this.containsSet.delete(info.toString()));
+ } else {
+ this.setSlideHistoryStack([...this.slideHistoryStack, stringified]);
+ this.containsSet.add(stringified);
+ }
+ }
+ console.log(this.slideHistoryStack.length);
+ if (this.slideHistoryStack.length === 0) {
+ Doc.UserDoc().isBranchingMode = false;
+ }
+ };
+
+ clickHandler = (e: React.PointerEvent<HTMLButtonElement>, targetDocId: string, removeIndex: number) => {
+ const targetDoc = this.docIdToDocMap.get(targetDocId);
+ if (!targetDoc) {
+ return;
+ }
+
+ const newStack = this.slideHistoryStack.slice(0, removeIndex);
+ const removed = this.slideHistoryStack.slice(removeIndex);
+
+ this.setSlideHistoryStack(newStack);
+
+ removed.forEach(info => this.containsSet.delete(info.toString()));
+ DocumentManager.Instance.showDocument(targetDoc, { willZoomCentered: true });
+ if (this.slideHistoryStack.length === 0) {
+ Doc.UserDoc().isBranchingMode = false;
+ }
+ //PresBox.NavigateToTarget(targetDoc, targetDoc);
+ };
+
+ @computed get trailBreadcrumbs() {
+ return (
+ <div style={{ border: '.5rem solid green', padding: '5px', backgroundColor: 'white', minHeight: '50px', minWidth: '1000px' }}>
+ {this.slideHistoryStack.map((info, index) => {
+ const [presId, targetDocId] = info.split(',');
+ const doc = this.docIdToDocMap.get(targetDocId);
+ if (!doc) {
+ return <></>;
+ }
+ return (
+ <span key={targetDocId}>
+ <button key={index} onPointerDown={e => this.clickHandler(e, targetDocId, index)}>
+ {presId.slice(0, 3) + ':' + doc.title}
+ </button>
+ -{'>'}
+ </span>
+ );
+ })}
+ </div>
+ );
+ }
+
+ render() {
+ return <div>{BranchingTrailManager.Instance.trailBreadcrumbs}</div>;
+ }
+}
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index a86011042..452bb74cd 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -1,7 +1,6 @@
-import { reaction } from "mobx";
+import { observable, reaction, runInAction } from "mobx";
import * as rp from 'request-promise';
import { Doc, DocListCast, DocListCastAsync, Opt } from "../../fields/Doc";
-import { FieldLoader } from "../../fields/FieldLoader";
import { InkTool } from "../../fields/InkField";
import { List } from "../../fields/List";
import { PrefetchProxy } from "../../fields/Proxy";
@@ -9,19 +8,20 @@ import { RichTextField } from "../../fields/RichTextField";
import { listSpec } from "../../fields/Schema";
import { ScriptField } from "../../fields/ScriptField";
import { Cast, DateCast, DocCast, StrCast } from "../../fields/Types";
-import { nullAudio } from "../../fields/URLField";
+import { nullAudio, URLField, WebField } from "../../fields/URLField";
import { SetCachedGroups, SharingPermissions } from "../../fields/util";
import { GestureUtils } from "../../pen-gestures/GestureUtils";
-import { OmitKeys, Utils, addStyleSheetRule } from "../../Utils";
+import { addStyleSheetRule, OmitKeys, Utils } from "../../Utils";
import { DocServer } from "../DocServer";
import { Docs, DocumentOptions, DocUtils, FInfo } from "../documents/Documents";
import { CollectionViewType, DocumentType } from "../documents/DocumentTypes";
import { TreeViewType } from "../views/collections/CollectionTreeView";
import { DashboardView } from "../views/DashboardView";
import { Colors } from "../views/global/globalEnums";
-import { MainView } from "../views/MainView";
-import { ButtonType } from "../views/nodes/FontIconBox/FontIconBox";
+import { media_state } from "../views/nodes/AudioBox";
import { OpenWhere } from "../views/nodes/DocumentView";
+import { ButtonType } from "../views/nodes/FontIconBox/FontIconBox";
+import { ImportElementBox } from "../views/nodes/importBox/ImportElementBox";
import { OverlayView } from "../views/OverlayView";
import { DragManager, dropActionType } from "./DragManager";
import { MakeTemplate } from "./DropConverter";
@@ -30,7 +30,6 @@ import { LinkManager } from "./LinkManager";
import { ScriptingGlobals } from "./ScriptingGlobals";
import { ColorScheme, SettingsManager } from "./SettingsManager";
import { UndoManager } from "./UndoManager";
-import { ImportElementBox } from "../views/nodes/importBox/ImportElementBox";
interface Button {
// DocumentOptions fields a button can set
@@ -53,7 +52,7 @@ interface Button {
// fields that do not correspond to DocumentOption fields
scripts?: { script?: string; onClick?: string; onDoubleClick?: string }
- funcs?: { [key:string]: string };
+ funcs?: { [key:string]: any};
subMenu?: Button[];
}
@@ -152,7 +151,7 @@ export class CurrentUserUtils {
{ noteType: "Idea", backgroundColor: "pink", icon: "lightbulb" },
{ noteType: "Topic", backgroundColor: "lightblue", icon: "book-open" }];
const reqdNoteList = reqdTempOpts.map(opts => {
- const reqdOpts = {...opts, title: "text", width:200, layout_autoHeight: true, layout_fitWidth: true};
+ const reqdOpts = {...opts, isSystem:true, title: "text", width:200, layout_autoHeight: true, layout_fitWidth: true};
const noteType = tempNotes ? DocListCast(tempNotes.data).find(doc => doc.noteType === opts.noteType): undefined;
return DocUtils.AssignOpts(noteType, reqdOpts) ?? MakeTemplate(Docs.Create.TextDocument("",reqdOpts), true, opts.noteType??"Note");
});
@@ -163,7 +162,6 @@ export class CurrentUserUtils {
/// Initializes collection of templates for notes and click functions
static setupDocTemplates(doc: Doc, field="myTemplates") {
- DocUtils.AssignDocField(doc, "presElement", opts => Docs.Create.PresElementBoxDocument(), { });
const templates = [
CurrentUserUtils.setupNoteTemplates(doc),
CurrentUserUtils.setupClickEditorTemplates(doc)
@@ -273,18 +271,18 @@ export class CurrentUserUtils {
{key: "Webpage", creator: opts => Docs.Create.WebDocument("",opts), opts: { _width: 400, _height: 512, _nativeWidth: 850, data_useCors: true, }},
{key: "Comparison", creator: Docs.Create.ComparisonDocument, opts: { _width: 300, _height: 300 }},
{key: "Audio", creator: opts => Docs.Create.AudioDocument(nullAudio, opts),opts: { _width: 200, _height: 100, }},
- {key: "Map", creator: opts => Docs.Create.MapDocument([], opts), opts: { _width: 800, _height: 600, _layout_fitWidth: true, _layout_showSidebar: true, }},
+ {key: "Map", creator: opts => Docs.Create.MapDocument([], opts), opts: { _width: 800, _height: 600, _layout_fitWidth: true, }},
{key: "Screengrab", creator: Docs.Create.ScreenshotDocument, opts: { _width: 400, _height: 200 }},
{key: "WebCam", creator: opts => Docs.Create.WebCamDocument("", opts), opts: { _width: 400, _height: 200, recording:true, isSystem: true, cloneFieldFilter: new List<string>(["isSystem"]) }},
{key: "Button", creator: Docs.Create.ButtonDocument, opts: { _width: 150, _height: 50, _xPadding: 10, _yPadding: 10}, scripts: {onClick: FollowLinkScript()?.script.originalScript ?? ""}},
{key: "Script", creator: opts => Docs.Create.ScriptingDocument(null, opts), opts: { _width: 200, _height: 250, }},
{key: "DataViz", creator: opts => Docs.Create.DataVizDocument("/users/rz/Downloads/addresses.csv", opts), opts: { _width: 300, _height: 300 }},
- {key: "Header", creator: headerTemplate, opts: { _width: 300, _height: 70, _headerPointerEvents: "all", _headerHeight: 12, _headerFontSize: 9, _layout_autoHeight: true, treeViewHideUnrendered: true}},
- {key: "Trail", creator: Docs.Create.PresDocument, opts: { _width: 400, _height: 30, _type_collection: CollectionViewType.Stacking, dropAction: "embed" as dropActionType, treeViewHideTitle: true, _layout_fitWidth:true, _chromeHidden: true, layout_boxShadow: "0 0" }},
+ {key: "Header", creator: headerTemplate, opts: { _width: 300, _height: 70, _headerPointerEvents: "all", _headerHeight: 12, _headerFontSize: 9, _layout_autoHeight: true, treeView_HideUnrendered: true}},
+ {key: "Trail", creator: Docs.Create.PresDocument, opts: { _width: 400, _height: 30, _type_collection: CollectionViewType.Stacking, dropAction: "embed" as dropActionType, treeView_HideTitle: 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,
+ treeView_HasOverlay: true, _text_fontSize: "20px", _layout_autoHeight: true,
+ dropAction:'move', treeView_Type: TreeViewType.outline,
backgroundColor: "white", _xMargin: 0, _yMargin: 0, _createDocOnCR: true
}, funcs: {title: 'self.text?.Text'}},
];
@@ -295,26 +293,27 @@ export class CurrentUserUtils {
{ 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 physics simulation",title: "Simulation", icon: "rocket",dragFactory: doc.emptySimulation as Doc, clickFactory: DocCast(doc.emptySimulation), funcs: { hidden: "IsNoviceMode()"}},
{ toolTip: "Tap or drag to create a note board", title: "Notes", icon: "folder", dragFactory: doc.emptyNoteboard as Doc, clickFactory: DocCast(doc.emptyNoteboard)},
{ toolTip: "Tap or drag to create a collection", title: "Col", icon: "folder", dragFactory: doc.emptyCollection as Doc, clickFactory: DocCast(doc.emptyTab)},
{ toolTip: "Tap or drag to create a webpage", title: "Web", icon: "globe-asia", dragFactory: doc.emptyWebpage as Doc, clickFactory: DocCast(doc.emptyWebpage)},
{ toolTip: "Tap or drag to create a comparison box", title: "Compare", icon: "columns", dragFactory: doc.emptyComparison as Doc, clickFactory: DocCast(doc.emptyComparison)},
{ toolTip: "Tap or drag to create an audio recorder", title: "Audio", icon: "microphone", dragFactory: doc.emptyAudio as Doc, clickFactory: DocCast(doc.emptyAudio), openFactoryLocation: OpenWhere.overlay},
{ toolTip: "Tap or drag to create a map", title: "Map", icon: "map-marker-alt", dragFactory: doc.emptyMap as Doc, clickFactory: DocCast(doc.emptyMap)},
- { toolTip: "Tap or drag to create a screen grabber", title: "Grab", icon: "photo-video", dragFactory: doc.emptyScreengrab as Doc, clickFactory: DocCast(doc.emptyScreengrab), openFactoryLocation: OpenWhere.overlay},
- { toolTip: "Tap or drag to create a WebCam recorder", title: "WebCam", icon: "photo-video", dragFactory: doc.emptyWebCam as Doc, clickFactory: DocCast(doc.emptyWebCam), openFactoryLocation: OpenWhere.overlay},
+ { 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, funcs: { hidden: "IsNoviceMode()"}},
+ { toolTip: "Tap or drag to create a WebCam recorder", title: "WebCam", icon: "photo-video", dragFactory: doc.emptyWebCam as Doc, clickFactory: DocCast(doc.emptyWebCam), openFactoryLocation: OpenWhere.overlay, funcs: { hidden: "IsNoviceMode()"}},
{ toolTip: "Tap or drag to create a button", title: "Button", icon: "bolt", dragFactory: doc.emptyButton as Doc, clickFactory: DocCast(doc.emptyButton)},
- { toolTip: "Tap or drag to create a scripting box", title: "Script", icon: "terminal", dragFactory: doc.emptyScript as Doc, clickFactory: DocCast(doc.emptyScript)},
- { 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: "Tap or drag to create a scripting box", title: "Script", icon: "terminal", dragFactory: doc.emptyScript as Doc, clickFactory: DocCast(doc.emptyScript), funcs: { hidden: "IsNoviceMode()"}},
+ { toolTip: "Tap or drag to create a data viz node", title: "DataViz", icon: "chart-bar", dragFactory: doc.emptyDataViz as Doc, clickFactory: DocCast(doc.emptyDataViz)},
+ { toolTip: "Tap or drag to create a bullet slide", title: "PPT Slide", icon: "file", dragFactory: doc.emptySlide as Doc, clickFactory: DocCast(doc.emptySlide), openFactoryLocation: OpenWhere.overlay, funcs: { hidden: "IsNoviceMode()"}},
+ { toolTip: "Tap or drag to create a data note", title: "DataNote", icon: "window-maximize",dragFactory: doc.emptyHeader as Doc,clickFactory: DocCast(doc.emptyHeader), openFactoryAsDelegate: true, funcs: { hidden: "IsNoviceMode()"} },
{ toolTip: "Toggle a Calculator REPL", title: "replviewer", icon: "calculator", clickFactory: '<ScriptingRepl />' as any, openFactoryLocation: OpenWhere.overlay}, // 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},
+ // { 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); }'},
+ funcs: tuple.funcs,
...tuple, }))
}
@@ -323,7 +322,7 @@ export class CurrentUserUtils {
const creatorBtns = CurrentUserUtils.creatorBtnDescriptors(doc).map((reqdOpts) => {
const btn = dragCreatorDoc ? DocListCast(dragCreatorDoc.data).find(doc => doc.title === reqdOpts.title): undefined;
const opts:DocumentOptions = {...OmitKeys(reqdOpts, ["funcs", "scripts", "backgroundColor"]).omit,
- _width: 35, _height: 35, _layout_hideContextMenu: true, _dragOnlyWithinContainer: true,
+ _width: 60, _height: 60, _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);
@@ -339,16 +338,16 @@ export class CurrentUserUtils {
}
/// returns descriptions needed to buttons for the left sidebar to open up panes displaying different collections of documents
- static leftSidebarMenuBtnDescriptions(doc: Doc):{title:string, target:Doc, icon:string, toolTip: string, scripts:{[key:string]:any}, funcs?:{[key:string]:any}}[] {
+ static leftSidebarMenuBtnDescriptions(doc: Doc):{title:string, target:Doc, icon:string, toolTip: string, scripts:{[key:string]:any}, funcs?:{[key:string]:any}, hidden?: boolean}[] {
const badgeValue = "((len) => len && len !== '0' ? len: undefined)(docList(self.target.data).filter(doc => !docList(self.target.viewed).includes(doc)).length.toString())";
const getActiveDashTrails = "Doc.ActiveDashboard?.myTrails";
return [
{ title: "Dashboards", toolTip: "Dashboards", target: this.setupDashboards(doc, "myDashboards"), ignoreClick: true, icon: "desktop", funcs: {hidden: "IsNoviceMode()"} },
{ title: "Search", toolTip: "Search ⌘F", target: this.setupSearcher(doc, "mySearcher"), ignoreClick: true, icon: "search", },
{ title: "Files", toolTip: "Files", target: this.setupFilesystem(doc, "myFilesystem"), ignoreClick: true, icon: "folder-open", },
- { title: "Tools", toolTip: "Tools", target: this.setupToolsBtnPanel(doc, "myTools"), ignoreClick: true, icon: "wrench", funcs: {hidden: "IsNoviceMode()"} },
+ { title: "Tools", toolTip: "Tools", target: this.setupToolsBtnPanel(doc, "myTools"), ignoreClick: true, icon: "wrench", },
{ title: "Imports", toolTip: "Imports ⌘I", target: this.setupImportSidebar(doc, "myImports"), ignoreClick:false, icon: "upload", },
- { title: "Closed", toolTip: "Recently Closed", target: this.setupRecentlyClosed(doc, "myRecentlyClosed"), ignoreClick: true, icon: "archive", },
+ { title: "Closed", toolTip: "Recently Closed", target: this.setupRecentlyClosed(doc, "myRecentlyClosed"), ignoreClick: true, icon: "archive", hidden: true }, // this doc is hidden from the Sidebar, but it's still being used in MyFilesystem which ignores the hidden field
{ title: "Shared", toolTip: "Shared Docs", target: Doc.MySharedDocs, ignoreClick: true, icon: "users", funcs: {badgeValue: badgeValue}},
{ title: "Trails", toolTip: "Trails ⌘R", target: Doc.UserDoc(), ignoreClick: true, icon: "pres-trail", funcs: {target: getActiveDashTrails}},
{ title: "User Doc", toolTip: "User Doc", target: this.setupUserDocView(doc, "myUserDocView"), ignoreClick: true, icon: "address-card",funcs: {hidden: "IsNoviceMode()"} },
@@ -357,17 +356,17 @@ 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.assign(new Doc(), opts as any), {isSystem:true, undoIgnoreFields: new List<string>(['proto'])});
+ DocUtils.AssignDocField(doc, field, (opts) => Doc.assign(new Doc(), opts as any), {title:"leftSidebarPanel", isSystem:true, undoIgnoreFields: new List<string>(['proto'])});
}
/// Initializes the left sidebar menu buttons and the panels they open up
static setupLeftSidebarMenu(doc: Doc, field="myLeftSidebarMenu") {
this.setupLeftSidebarPanel(doc);
const myLeftSidebarMenu = DocCast(doc[field]);
- const menuBtns = CurrentUserUtils.leftSidebarMenuBtnDescriptions(doc).map(({ title, target, icon, toolTip, scripts, funcs }) => {
+ const menuBtns = CurrentUserUtils.leftSidebarMenuBtnDescriptions(doc).map(({ title, target, icon, toolTip, hidden, scripts, funcs }) => {
const btnDoc = myLeftSidebarMenu ? DocListCast(myLeftSidebarMenu.data).find(doc => doc.title === title) : undefined;
const reqdBtnOpts:DocumentOptions = {
- title, icon, target, toolTip, btnType: ButtonType.MenuButton, isSystem: true, undoIgnoreFields: new List<string>(['height', 'data_columnHeaders']), dontRegisterView: true,
+ title, icon, target, toolTip, hidden, btnType: ButtonType.MenuButton, isSystem: true, undoIgnoreFields: new List<string>(['height', 'data_columnHeaders']), dontRegisterView: true,
_width: 60, _height: 60, _dragOnlyWithinContainer: true, _layout_hideContextMenu: true,
};
return DocUtils.AssignScripts(DocUtils.AssignOpts(btnDoc, reqdBtnOpts) ?? Docs.Create.FontIconDocument(reqdBtnOpts), scripts, funcs);
@@ -375,7 +374,7 @@ export class CurrentUserUtils {
const reqdStackOpts:DocumentOptions ={
title: "menuItemPanel", childDragAction: "same", layout_boxShadow: "rgba(0,0,0,0)", dontRegisterView: true, ignoreClick: true,
- _chromeHidden: true, _gridGap: 0, _yMargin: 0, _yPadding: 0, _xMargin: 0, _layout_autoHeight: false, _width: 60, _columnWidth: 60, _lockedPosition: true, isSystem: true,
+ _chromeHidden: true, _gridGap: 0, _yMargin: 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)" });
}
@@ -489,13 +488,13 @@ export class CurrentUserUtils {
const contextMenuLabels = [/*"Create New Dashboard"*/] as string[];
const contextMenuIcons = [/*"plus"*/] as string[];
const childContextMenuScripts = [toggleDarkTheme, `toggleComicMode()`, `snapshotDashboard()`, `shareDashboard(self)`, 'removeDashboard(self)', 'resetDashboard(self)']; // entries must be kept in synch with childContextMenuLabels, childContextMenuIcons, and childContextMenuFilters
- const childContextMenuFilters = ['!IsNoviceMode()', '!IsNoviceMode()', '!IsNoviceMode()', undefined as any, undefined as any, undefined as any];// entries must be kept in synch with childContextMenuLabels, childContextMenuIcons, and childContextMenuScripts
+ const childContextMenuFilters = ['!IsNoviceMode()', '!IsNoviceMode()', '!IsNoviceMode()', undefined as any, undefined as any, '!IsNoviceMode()'];// entries must be kept in synch with childContextMenuLabels, childContextMenuIcons, and childContextMenuScripts
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, 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",
+ title: "My Dashboards", childHideLinkButton: true, treeView_FreezeChildren: "remove|add", treeView_HideTitle: true, layout_boxShadow: "0 0", childDontRegisterViews: true,
+ dropAction: "same", treeView_Type: TreeViewType.fileSystem, isFolder: true, isSystem: true, treeView_TruncateTitleWidth: 350, ignoreClick: true,
+ layout_headerButton: newDashboardButton, childDragAction: "none",
_layout_showTitle: "title", _height: 400, _gridGap: 5, _forceActive: true, _lockedPosition: true,
contextMenuLabels:new List<string>(contextMenuLabels),
contextMenuIcons:new List<string>(contextMenuIcons),
@@ -519,25 +518,25 @@ export class CurrentUserUtils {
/// initializes the left sidebar File system pane
static setupFilesystem(doc: Doc, field:string) {
var myFilesystem = DocCast(doc[field]);
- const myFileOrphans = DocUtils.AssignDocField(doc, "myFileOrphans", (opts) => Docs.Create.TreeDocument([], opts), { title: "Unfiled", undoIgnoreFields:new List<string>(['treeViewSortCriterion']), _dragOnlyWithinContainer: true, isSystem: true, isFolder: true });
const newFolder = `TreeView_addNewFolder()`;
const newFolderOpts: DocumentOptions = {
- _forceActive: true, _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, _width: 30, _height: 30, undoIgnoreFields:new List<string>(['treeViewSortCriterion']),
+ _forceActive: true, _dragOnlyWithinContainer: true, _layout_hideContextMenu: true, _width: 30, _height: 30, undoIgnoreFields:new List<string>(['treeView_SortCriterion']),
title: "New folder", color: Colors.BLACK, btnType: ButtonType.ClickButton, toolTip: "Create new folder", buttonText: "New folder", icon: "folder-plus", isSystem: true
};
const newFolderScript = { onClick: newFolder};
const newFolderButton = DocUtils.AssignScripts(DocUtils.AssignOpts(DocCast(myFilesystem?.layout_headerButton), newFolderOpts) ?? Docs.Create.FontIconDocument(newFolderOpts), newFolderScript);
- const reqdOpts:DocumentOptions = { _layout_showTitle: "title", _height: 100, _gridGap: 5, _forceActive: true, _lockedPosition: true,
- title: "My Documents", layout_headerButton: newFolderButton, treeViewHideTitle: true, dropAction: "proto", isSystem: true,
- isFolder: true, treeViewType: TreeViewType.fileSystem, childHideLinkButton: true, layout_boxShadow: "0 0", childDontRegisterViews: true,
- treeViewTruncateTitleWidth: 350, ignoreClick: true, childDragAction: "embed",
+ const reqdOpts:DocumentOptions = { _layout_showTitle: "title", _height: 100, _forceActive: true,
+ title: "My Documents", layout_headerButton: newFolderButton, treeView_HideTitle: true, dropAction: 'add', isSystem: true,
+ isFolder: true, treeView_Type: TreeViewType.fileSystem, childHideLinkButton: true, layout_boxShadow: "0 0", childDontRegisterViews: true,
+ treeView_TruncateTitleWidth: 350, ignoreClick: true, childDragAction: "embed",
childContextMenuLabels: new List<string>(["Create new folder"]),
childContextMenuIcons: new List<string>(["plus"]),
layout_explainer: "This is your file manager where you can create folders to keep track of documents independently of your dashboard."
};
- myFilesystem = DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.TreeDocument(items??[], opts), reqdOpts, [myFileOrphans]);
+ const fileFolders = new Set(DocListCast(DocCast(doc[field])?.data));
+ myFilesystem = DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.TreeDocument(items??[], opts), reqdOpts, Array.from(fileFolders));
const childContextMenuScripts = [newFolder];
if (Cast(myFilesystem.childContextMenuScripts, listSpec(ScriptField), null)?.length !== childContextMenuScripts.length) {
myFilesystem.childContextMenuScripts = new List<ScriptField>(childContextMenuScripts.map(script => ScriptField.MakeFunction(script)!));
@@ -547,9 +546,9 @@ export class CurrentUserUtils {
/// initializes the panel displaying docs that have been recently closed
static setupRecentlyClosed(doc: Doc, field:string) {
- const reqdOpts:DocumentOptions = { _layout_showTitle: "title", _lockedPosition: true, _gridGap: 5, _forceActive: true,
- title: "My Recently Closed", childHideLinkButton: true, treeViewHideTitle: true, childDragAction: "embed", isSystem: true,
- treeViewTruncateTitleWidth: 350, ignoreClick: true, layout_boxShadow: "0 0", childDontRegisterViews: true, dropAction: "same",
+ const reqdOpts:DocumentOptions = { _layout_showTitle: "title", _lockedPosition: true, _gridGap: 5, _forceActive: true, isFolder: true,
+ title: "My Recently Closed", childHideLinkButton: true, treeView_HideTitle: true, childDragAction: "move", isSystem: true,
+ treeView_TruncateTitleWidth: 350, ignoreClick: true, layout_boxShadow: "0 0", childDontRegisterViews: true, dropAction: "same",
contextMenuLabels: new List<string>(["Empty recently closed"]),
contextMenuIcons:new List<string>(["trash"]),
layout_explainer: "Recently closed documents appear in this menu. They will only be deleted if you explicity empty this list."
@@ -562,8 +561,6 @@ export class CurrentUserUtils {
toolTip: "Empty recently closed",};
DocUtils.AssignDocField(recentlyClosed, "layout_headerButton", (opts) => Docs.Create.FontIconDocument(opts), clearBtnsOpts, undefined, {onClick: clearAll("self.target")});
- //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"))!])
}
@@ -575,9 +572,9 @@ export class CurrentUserUtils {
const reqdOpts:DocumentOptions = {
_lockedPosition: true, _gridGap: 5, _forceActive: true, title: Doc.CurrentUserEmail +"-view",
layout_boxShadow: "0 0", childDontRegisterViews: true, dropAction: "same", ignoreClick: true, isSystem: true,
- treeViewHideTitle: true, treeViewTruncateTitleWidth: 350
+ treeView_HideTitle: true, treeView_TruncateTitleWidth: 350
};
- if (!doc[field]) DocUtils.AssignOpts(doc, {treeViewOpen: true, treeViewExpandedView: "fields" });
+ if (!doc[field]) DocUtils.AssignOpts(doc, {treeView_Open: true, treeView_ExpandedView: "fields" });
return DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.TreeDocument(items??[], opts), reqdOpts, [doc]);
}
@@ -586,11 +583,16 @@ export class CurrentUserUtils {
dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }),
_lockedPosition: true, isSystem: true, flexDirection: "row"
})
+ static multiToggleList = (opts: DocumentOptions, docs: Doc[]) => Docs.Create.FontIconDocument({
+ ...opts, data:docs, _gridGap: 0, _xMargin: 5, _yMargin: 5, layout_boxShadow: "0 0", _forceActive: true,
+ dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }),
+ _lockedPosition: true, isSystem: true, flexDirection: "row"
+ })
static createToolButton = (opts: DocumentOptions) => Docs.Create.FontIconDocument({
btnType: ButtonType.ToolButton, _forceActive: true, _layout_hideContextMenu: true,
_dropPropertiesToRemove: new List<string>([ "_layout_hideContextMenu"]),
- _nativeWidth: 40, _nativeHeight: 40, _width: 40, _height: 40, isSystem: true, ...opts,
+ /*_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
@@ -606,6 +608,7 @@ export class CurrentUserUtils {
{ 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"}},
+ { scripts: { }, opts: { title: "Branching", layout: "<Branching>", toolTip: "Branch, baby!"}}
];
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 = {
@@ -619,19 +622,26 @@ export class CurrentUserUtils {
static freeTools(): Button[] {
return [
- { title: "Bottom", icon: "arrows-down-to-line",toolTip: "Make doc topmost", btnType: ButtonType.ClickButton, expertMode: false, funcs: {}, scripts: { onClick: 'sendToBack()'}}, // Only when floating document is selected in freeform
- { title: "Top", icon: "arrows-up-to-line", toolTip: "Make doc bottommost", btnType: ButtonType.ClickButton, expertMode: false, funcs: {}, scripts: { onClick: 'bringToFront()'}}, // Only when floating document is selected in freeform
- { title: "Z order", icon: "z", toolTip: "Bring Forward on Drag (double click to set for all)",waitForDoubleClickToClick:true, btnType: ButtonType.ToggleButton, expertMode: false, funcs: {}, scripts: { onClick: 'toggleRaiseOnDrag(false, _readOnly_)', onDoubleClick:`{ return toggleRaiseOnDrag(true, _readOnly_)`}}, // Only when floating document is selected in freeform
+ { title: "Bottom", icon: "arrows-down-to-line",toolTip: "Make doc topmost", btnType: ButtonType.ClickButton, expertMode: false, funcs: {}, scripts: { onClick: 'sendToBack()'}}, // Only when floating document is selected in freeform
+ { title: "Top", icon: "arrows-up-to-line", toolTip: "Make doc bottommost", btnType: ButtonType.ClickButton, expertMode: false, funcs: {}, scripts: { onClick: 'bringToFront()'}}, // Only when floating document is selected in freeform
+ { title: "Z order", icon: "z", toolTip: "Keep Z order on Drag", btnType: ButtonType.ToggleButton, expertMode: false, funcs: {}, scripts: { onClick: '{ return toggleRaiseOnDrag(_readOnly_);}'}}, // Only when floating document is selected in freeform
+ ]
+ }
+ static stackTools(): Button[] {
+ return [
+ { title: "Center", icon: "align-center", toolTip: "Center Align Stack", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"center", funcs: {}, scripts: { onClick: '{ return showFreeform(self.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
]
}
static viewTools(): Button[] {
return [
- { title: "Snap", icon: "th", toolTip: "Show Snap Lines", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"snaplines", funcs: {}, scripts: { onClick: '{ return showFreeform(self.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
- { title: "Grid", icon: "border-all", toolTip: "Show Grid", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"grid", funcs: {}, scripts: { onClick: '{ return showFreeform(self.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
- { title: "View All", icon: "object-group", toolTip: "Fit all Docs to View", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"viewAll", funcs: {}, scripts: { onClick: '{ return showFreeform(self.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
- { title: "Clusters", icon: "braille", toolTip: "Show Doc Clusters", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"clusters", funcs: {}, scripts: { onClick: '{ return showFreeform(self.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
- { title: "Cards", icon: "brain", toolTip: "Flashcards", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"flashcards", funcs: {}, scripts: { onClick: '{ return showFreeform(self.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
- { title: "Arrange",icon:"arrow-down-short-wide",toolTip:"Toggle Auto Arrange", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"arrange", funcs: {hidden: 'IsNoviceMode()'}, scripts: { onClick: '{ return showFreeform(self.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
+ { title: "Snap", icon: "th", toolTip: "Show Snap Lines", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"snaplines", funcs: {}, scripts: { onClick: '{ return showFreeform(self.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
+ { title: "Grid", icon: "border-all", toolTip: "Show Grid", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"grid", funcs: {}, scripts: { onClick: '{ return showFreeform(self.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
+ { title: "View All", icon: "object-group", toolTip: "Fit all Docs to View",btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"viewAll", funcs: {}, scripts: { onClick: '{ return showFreeform(self.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
+ // want the same style as toggle button, but don't want it to act as an actual toggle, so set disableToggle to true,
+ { title: "Fit All", icon: "arrows-left-right", toolTip: "Fit all Docs to View (persistent)", btnType: ButtonType.ClickButton, ignoreClick: false, expertMode: false, toolType:"viewAllPersist", funcs: {}, scripts: { onClick: '{ return showFreeform(self.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
+ { title: "Clusters", icon: "braille", toolTip: "Show Doc Clusters", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"clusters", funcs: {}, scripts: { onClick: '{ return showFreeform(self.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
+ { title: "Cards", icon: "brain", toolTip: "Flashcards", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"flashcards", funcs: {}, scripts: { onClick: '{ return showFreeform(self.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
+ { title: "Arrange", icon:"arrow-down-short-wide",toolTip:"Toggle Auto Arrange", btnType: ButtonType.ToggleButton, ignoreClick: true, expertMode: false, toolType:"arrange", funcs: {hidden: 'IsNoviceMode()'}, scripts: { onClick: '{ return showFreeform(self.toolType, _readOnly_);}'}}, // Only when floating document is selected in freeform
]
}
static textTools():Button[] {
@@ -640,17 +650,17 @@ export class CurrentUserUtils {
btnList: new List<string>(["Roboto", "Roboto Mono", "Nunito", "Times New Roman", "Arial", "Georgia", "Comic Sans MS", "Tahoma", "Impact", "Crimson Text"]) },
{ title: "Font Size",toolTip: "Font size (%size)", btnType: ButtonType.NumberDropdownButton, toolType:"fontSize", ignoreClick: true, scripts: {script: '{ return setFontAttr(self.toolType, value, _readOnly_);}'}, numBtnMax: 200, numBtnMin: 6 },
{ title: "Color", toolTip: "Font color (%color)", btnType: ButtonType.ColorButton, icon: "font", toolType:"fontColor",ignoreClick: true, scripts: {script: '{ return setFontAttr(self.toolType, value, _readOnly_);}'}},
- { title: "Highlight",toolTip:"Font highlight", btnType: ButtonType.ColorButton, icon: "highlighter", toolType:"highlight",ignoreClick: true, scripts: {script: '{ return setFontAttr(self.toolType, value, _readOnly_);}'},funcs: {hidden: "IsNoviceMode()"} },
+ { title: "Highlight",toolTip: "Font highlight", btnType: ButtonType.ColorButton, icon: "highlighter", toolType:"highlight",ignoreClick: true, scripts: {script: '{ return setFontAttr(self.toolType, value, _readOnly_);}'},funcs: {hidden: "IsNoviceMode()"} },
{ title: "Bold", toolTip: "Bold (Ctrl+B)", btnType: ButtonType.ToggleButton, icon: "bold", toolType:"bold", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
{ title: "Italic", toolTip: "Italic (Ctrl+I)", btnType: ButtonType.ToggleButton, icon: "italic", toolType:"italics", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
{ title: "Under", toolTip: "Underline (Ctrl+U)", btnType: ButtonType.ToggleButton, icon: "underline", toolType:"underline",ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
{ title: "Bullets", toolTip: "Bullet List", btnType: ButtonType.ToggleButton, icon: "list", toolType:"bullet", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
{ title: "#", toolTip: "Number List", btnType: ButtonType.ToggleButton, icon: "list-ol", toolType:"decimal", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
- { title: "Alignment",toolTip: "Alignment", btnType: ButtonType.MultiToggleButton, toolType:"alignment", ignoreClick: true,
+ { title: "Align", toolTip: "Alignment", btnType: ButtonType.MultiToggleButton, toolType:"alignment", ignoreClick: true,
subMenu: [
- { title: "Left", toolTip: "Left align (Cmd-[)", btnType: ButtonType.ToggleButton, icon: "align-left", toolType:"left", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}' }},
- { title: "Center", toolTip: "Center align (Cmd-\\)",btnType: ButtonType.ToggleButton, icon: "align-center",toolType:"center",ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
- { title: "Right", toolTip: "Right align (Cmd-])", btnType: ButtonType.ToggleButton, icon: "align-right", toolType:"right", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
+ { title: "Left", toolTip: "Left align (Cmd-[)", btnType: ButtonType.ToggleButton, icon: "align-left", toolType:"left", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}' }},
+ { title: "Center", toolTip: "Center align (Cmd-\\)",btnType: ButtonType.ToggleButton, icon: "align-center",toolType:"center",ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
+ { title: "Right", toolTip: "Right align (Cmd-])", btnType: ButtonType.ToggleButton, icon: "align-right", toolType:"right", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} },
]
},
{ title: "Dictate", toolTip: "Dictate", btnType: ButtonType.ToggleButton, icon: "microphone", toolType:"dictation", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'}},
@@ -665,7 +675,7 @@ export class CurrentUserUtils {
return [
{ 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: "Eraser", toolTip: "Eraser (Ctrl+E)", btnType: ButtonType.ToggleButton, icon: "eraser", toolType: "eraser", scripts: {onClick:'{ return setActiveTool(self.toolType, false, _readOnly_);}' }, funcs: {hidden:"IsNoviceMode()" }},
{ 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_);}`} },
@@ -677,8 +687,8 @@ export class CurrentUserUtils {
static schemaTools():Button[] {
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_); }'} },
+ {title: "Preview", toolTip: "Show selection preview", btnType: ButtonType.ToggleButton, icon: "portrait", scripts:{ onClick: '{ return toggleSchemaPreview(_readOnly_); }'} },
+ {title: "1 Line",toolTip: "Single Line Rows", btnType: ButtonType.ToggleButton, icon: "eye", scripts:{ onClick: '{ return toggleSingleLineSchema(_readOnly_); }'} },
];
}
@@ -689,7 +699,6 @@ export class CurrentUserUtils {
{ title: "URL", toolTip: "URL", width: 250, btnType: ButtonType.EditableText, icon: "lock", ignoreClick: true, scripts: { script: '{ return webSetURL(value, _readOnly_); }'} },
];
}
-
static contextMenuTools():Button[] {
return [
{ btnList: new List<string>([CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Tree,
@@ -697,11 +706,11 @@ 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, expertMode: false, width: 30, scripts: { onClick: 'pinWithView(altKey)'}, funcs: {hidden: "IsNoneSelected()"}},
+ 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: 30, scripts: { onClick: 'pinWithView(altKey)'}, funcs: {hidden: "IsNoneSelected()"}},
{ title: "Fill", icon: "fill-drip", toolTip: "Background Fill Color",btnType: ButtonType.ColorButton, expertMode: false, ignoreClick: true, width: 30, scripts: { script: 'return setBackgroundColor(value, _readOnly_)'}, funcs: {hidden: "IsNoneSelected()"}}, // Only when a document is selected
- { title: "Header", icon: "heading", toolTip: "Header Color", btnType: ButtonType.ColorButton, expertMode: true, ignoreClick: true, scripts: { script: 'return setHeaderColor(value, _readOnly_)'}, funcs: {hidden: "IsNoneSelected()"}},
- { title: "Overlay", icon: "layer-group", toolTip: "Overlay", btnType: ButtonType.ToggleButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode, true)'}, scripts: { onClick: 'return { toggleOverlay(_readOnly_); }'}}, // Only when floating document is selected in freeform
+ { title: "Header", icon: "heading", toolTip: "Header Color", btnType: ButtonType.ColorButton, expertMode: true, ignoreClick: true, scripts: { script: 'return setHeaderColor(value, _readOnly_)'}, funcs: {hidden: "IsNoneSelected()"}},
+ { title: "Overlay", icon: "layer-group", toolTip: "Overlay", btnType: ButtonType.ToggleButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode, true)'}, scripts: { onClick: '{ return toggleOverlay(_readOnly_); }'}}, // Only when floating document is selected in freeform
{ title: "Back", icon: "chevron-left", toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode)'}, width: 30, scripts: { onClick: 'prevKeyFrame(_readOnly_)'}},
{ title: "Num", icon:"", toolTip: "Frame Number (click to toggle edit mode)", btnType: ButtonType.TextButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode)', buttonText: 'selectedDocs()?.lastElement()?.currentFrame?.toString()'}, width: 20, scripts: { onClick: '{ return curKeyFrame(_readOnly_);}'}},
{ title: "Fwd", icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode)'}, width: 30, scripts: { onClick: 'nextKeyFrame(_readOnly_)'}},
@@ -709,9 +718,10 @@ export class CurrentUserUtils {
{ title: "Ink", icon: "Ink", toolTip: "Ink functions", subMenu: CurrentUserUtils.inkTools(), expertMode: false, toolType:DocumentType.INK, funcs: { linearView_IsOpen: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`}, scripts: { onClick: 'setInkToolDefaults()'} }, // Always available
{ title: "Doc", icon: "Doc", toolTip: "Freeform Doc tools", subMenu: CurrentUserUtils.freeTools(), expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode, true)`, linearView_IsOpen: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Always available
{ title: "View", icon: "View", toolTip: "View tools", subMenu: CurrentUserUtils.viewTools(), expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode)`, linearView_IsOpen: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Always available
+ { title: "Stack", icon: "View", toolTip: "Stacking tools", subMenu: CurrentUserUtils.stackTools(), expertMode: false, toolType:CollectionViewType.Stacking, funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode)`, linearView_IsOpen: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Always available
{ title: "Web", icon: "Web", toolTip: "Web functions", subMenu: CurrentUserUtils.webTools(), expertMode: false, toolType:DocumentType.WEB, funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode)`, linearView_IsOpen: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Only when Web is selected
- { title: "Schema", icon: "Schema",linearBtnWidth:58,toolTip: "Schema functions",subMenu: CurrentUserUtils.schemaTools(), expertMode: false, toolType:CollectionViewType.Schema, funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode)`, linearView_IsOpen: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} } // Only when Schema is selected
- ];
+ { title: "Schema", icon: "Schema",linearBtnWidth:58,toolTip: "Schema functions",subMenu: CurrentUserUtils.schemaTools(),expertMode: false,toolType:CollectionViewType.Schema,funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode)`, linearView_IsOpen: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Only when Schema is selected
+ ];
}
/// initializes a context menu button for the top bar context menu
@@ -730,34 +740,59 @@ export class CurrentUserUtils {
return DocUtils.AssignScripts(DocUtils.AssignOpts(btnDoc, reqdOpts) ?? Docs.Create.FontIconDocument(reqdOpts), params.scripts, reqdFuncs);
}
+
+ static setupContextMenuBtn(params:Button, menuDoc:Doc):Doc {
+ const menuBtnDoc = DocListCast(menuDoc?.data).find(doc => doc.title === params.title);
+ const subMenu = params.subMenu;
+ if (!subMenu) { // button does not have a sub menu
+ return this.setupContextMenuButton(params, menuBtnDoc);
+ }
+ // linear view
+ const reqdSubMenuOpts = { ...OmitKeys(params, ["scripts", "funcs", "subMenu"]).omit, undoIgnoreFields: new List<string>(['width', "linearView_IsOpen"]),
+ childDontRegisterViews: true, flexGap: 0, _height: 30, ignoreClick: params.scripts?.onClick ? false : true,
+ linearView_SubMenu: true, linearView_Expandable: true};
+
+ const items = (menuBtnDoc?:Doc) => !menuBtnDoc ? [] : subMenu.map(sub => this.setupContextMenuBtn(sub, menuBtnDoc) );
+ const creator = params.btnType === ButtonType.MultiToggleButton ? this.multiToggleList : this.linearButtonList;
+ const btnDoc = DocUtils.AssignScripts( DocUtils.AssignDocField(menuDoc, StrCast(params.title),
+ (opts) => creator(opts, items(menuBtnDoc)), reqdSubMenuOpts, items(menuBtnDoc)), params.scripts, params.funcs);
+ if (!menuBtnDoc) Doc.GetProto(btnDoc).data = new List<Doc>(items(btnDoc));
+ return btnDoc;
+ }
+
/// Initializes all the default buttons for the top bar context menu
static setupContextMenuButtons(doc: Doc, field="myContextMenuBtns") {
const reqdCtxtOpts:DocumentOptions = { title: "context menu buttons", undoIgnoreFields:new List<string>(['width', "linearView_IsOpen"]), flexGap: 0, childDragAction: 'embed', childDontRegisterViews: true, linearView_IsOpen: true, ignoreClick: true, linearView_Expandable: false, _height: 35 };
const ctxtMenuBtnsDoc = DocUtils.AssignDocField(doc, field, (opts, items) => this.linearButtonList(opts, items??[]), reqdCtxtOpts, undefined);
- const ctxtMenuBtns = CurrentUserUtils.contextMenuTools().map(params => {
- const menuBtnDoc = DocListCast(ctxtMenuBtnsDoc?.data).find(doc => doc.title === params.title);
- if (!params.subMenu) { // button does not have a sub menu
- return this.setupContextMenuButton(params, menuBtnDoc);
- } else { // linear view
- let reqdSubMenuOpts;
- if (params.btnType === ButtonType.MultiToggleButton) {
- reqdSubMenuOpts = { ...OmitKeys(params, ["scripts", "funcs", "subMenu"]).omit, undoIgnoreFields: new List<string>(['width', "linearView_IsOpen"]),
- childDontRegisterViews: true, flexGap: 0, _height: 30, ignoreClick: params.scripts?.onClick ? false : true,
- linearView_SubMenu: true, linearView_Dropdown: true, };
- } else {
- reqdSubMenuOpts = { ...OmitKeys(params, ["scripts", "funcs", "subMenu"]).omit, undoIgnoreFields: new List<string>(['width', "linearView_IsOpen"]),
- childDontRegisterViews: true, flexGap: 0, _height: 30, ignoreClick: params.scripts?.onClick ? false : true,
- linearView_SubMenu: true, linearView_Expandable: true, };
- }
- const items = params.subMenu?.map(sub =>
- this.setupContextMenuButton(sub, DocListCast(menuBtnDoc?.data).find(doc => doc.title === sub.title))
- );
- return DocUtils.AssignScripts(
- DocUtils.AssignDocField(ctxtMenuBtnsDoc, StrCast(params.title), (opts) => this.linearButtonList(opts, items??[]), reqdSubMenuOpts, items), params.scripts, params.funcs);
- }
- });
+ const ctxtMenuBtns = CurrentUserUtils.contextMenuTools().map(params => this.setupContextMenuBtn(params, ctxtMenuBtnsDoc) );
return DocUtils.AssignOpts(ctxtMenuBtnsDoc, reqdCtxtOpts, ctxtMenuBtns);
}
+ /// Initializes all the default buttons for the top bar context menu
+ static setupTopbarButtons(doc: Doc, field="myTopBarBtns") {
+ Doc.UserDoc().currentRecording = undefined;
+ Doc.UserDoc().workspaceRecordingState = undefined;
+ Doc.UserDoc().workspaceReplayingState = undefined;
+ const dockedBtns = DocCast(doc[field]);
+ const dockBtn = (opts: DocumentOptions, scripts: {[key:string]:string|undefined}, funcs?: {[key:string]:any}) =>
+ DocUtils.AssignScripts(DocUtils.AssignOpts(DocListCast(dockedBtns?.data)?.find(doc => doc.title === opts.title), opts) ??
+ 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
+ { opts: { title: "Replicate",icon:"camera",toolTip: "Copy dashboard layout",btnType: ButtonType.ClickButton, expertMode: true}, scripts: { onClick: `snapshotDashboard()`}},
+ { opts: { title: "Recordings", toolTip: "Workspace Recordings", btnType: ButtonType.DropdownList,expertMode: false, ignoreClick: true, width: 100}, funcs: {hidden: `false`, btnList:`getWorkspaceRecordings()`}, scripts: { script: `{ return replayWorkspace(value, _readOnly_); }`, onDragScript: `{ return startRecordingDrag(value); }`}},
+ { opts: { title: "Stop Rec",icon: "stop", toolTip: "Stop recording", btnType: ButtonType.ClickButton, expertMode: false}, funcs: {hidden: `!isWorkspaceRecording()`}, scripts: { onClick: `stopWorkspaceRecording()`}},
+ { opts: { title: "Play", icon: "play", toolTip: "Play recording", btnType: ButtonType.ClickButton, expertMode: false}, funcs: {hidden: `isWorkspaceReplaying() !== "${media_state.Paused}"`}, scripts: { onClick: `resumeWorkspaceReplaying(getCurrentRecording())`}},
+ { opts: { title: "Pause", icon: "pause",toolTip: "Pause playback", btnType: ButtonType.ClickButton, expertMode: false}, funcs: {hidden: `isWorkspaceReplaying() !== "${media_state.Playing}"`}, scripts: { onClick: `pauseWorkspaceReplaying(getCurrentRecording())`}},
+ { opts: { title: "Stop", icon: "stop", toolTip: "Stop playback", btnType: ButtonType.ClickButton, expertMode: false}, funcs: {hidden: `isWorkspaceReplaying() !== "${media_state.Paused}"`}, scripts: { onClick: `stopWorkspaceReplaying(getCurrentRecording())`}},
+ { opts: { title: "Delete", icon: "trash",toolTip: "delete selected rec", btnType: ButtonType.ClickButton, expertMode: false}, funcs: {hidden: `isWorkspaceReplaying() !== "${media_state.Paused}"`}, scripts: { onClick: `removeWorkspaceReplaying(getCurrentRecording())`}}
+ ];
+ const btns = btnDescs.map(desc => dockBtn({_width: desc.opts.width??30, _height: 30, defaultDoubleClick: 'ignore', undoIgnoreFields: new List<string>(['opacity']), _dragOnlyWithinContainer: true, ...desc.opts}, desc.scripts, desc.funcs));
+ const dockBtnsReqdOpts:DocumentOptions = {
+ title: "docked buttons", _height: 40, flexGap: 0, layout_boxShadow: "standard", childDragAction: 'move',
+ childDontRegisterViews: true, linearView_IsOpen: true, linearView_Expandable: false, ignoreClick: true
+ };
+ return DocUtils.AssignDocField(doc, field, (opts, items) => this.linearButtonList(opts, items??[]), dockBtnsReqdOpts, btns);
+ }
/// collection of documents rendered in the overlay layer above all tabs and other UI
static setupOverlays(doc: Doc, field = "myOverlayDocs") {
@@ -774,6 +809,7 @@ export class CurrentUserUtils {
const linkDocs = new Doc(linkDatabaseId, true);
linkDocs.title = "LINK DATABASE: " + Doc.CurrentUserEmail;
linkDocs.author = Doc.CurrentUserEmail;
+ linkDocs.isSystem = true;
linkDocs.data = new List<Doc>([]);
linkDocs["acl-Guest"] = SharingPermissions.Augment;
doc.myLinkDatabase = new PrefetchProxy(linkDocs);
@@ -787,19 +823,19 @@ export class CurrentUserUtils {
static setupSharedDocs(doc: Doc, sharingDocumentId: string) {
const dblClkScript = "{scriptContext.openLevel(documentView); addDocToList(scriptContext.props.treeView.props.Document, 'viewed', documentView.rootDoc);}";
- const sharedScripts = { treeViewChildDoubleClick: dblClkScript, }
+ const sharedScripts = { treeView_ChildDoubleClick: dblClkScript, }
const sharedDocOpts:DocumentOptions = {
title: "My Shared Docs",
userColor: "rgb(202, 202, 202)",
- isFolder:true, undoIgnoreFields:new List<string>(['treeViewSortCriterion']),
+ isFolder:true, undoIgnoreFields:new List<string>(['treeView_SortCriterion']),
// childContextMenuFilters: new List<ScriptField>([dashboardFilter!,]),
// childContextMenuScripts: new List<ScriptField>([addToDashboards!,]),
// childContextMenuLabels: new List<string>(["Add to Dashboards",]),
// childContextMenuIcons: new List<string>(["user-plus",]),
"acl-Guest": SharingPermissions.Augment, "_acl-Guest": SharingPermissions.Augment,
childDragAction: "embed", isSystem: true, contentPointerEvents: "all", childLimitHeight: 0, _yMargin: 0, _gridGap: 15, childDontRegisterViews:true,
- // NOTE: treeViewHideTitle & _layout_showTitle is for a TreeView's editable title, _layout_showTitle is for DocumentViews title bar
- _layout_showTitle: "title", treeViewHideTitle: true, ignoreClick: true, _lockedPosition: true, layout_boxShadow: "0 0", _chromeHidden: true, dontRegisterView: true,
+ // NOTE: treeView_HideTitle & _layout_showTitle is for a TreeView's editable title, _layout_showTitle is for DocumentViews title bar
+ _layout_showTitle: "title", treeView_HideTitle: 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'"
};
@@ -837,7 +873,6 @@ export class CurrentUserUtils {
doc.isSystem ?? (doc.isSystem = true);
doc.title ?? (doc.title = Doc.CurrentUserEmail);
Doc.noviceMode ?? (Doc.noviceMode = true);
- doc._raiseWhenDragged ?? (doc._raiseWhenDragged = true);
doc._showLabel ?? (doc._showLabel = true);
doc.textAlign ?? (doc.textAlign = "left");
doc.activeTool = InkTool.None;
@@ -862,7 +897,7 @@ export class CurrentUserUtils {
doc.userColor ?? (doc.userColor = Colors.LIGHT_GRAY);
doc.userTheme ?? (doc.userTheme = ColorScheme.Dark);
doc.filterDocCount = 0;
- doc.treeViewFreezeChildren = "remove|add";
+ doc.treeView_FreezeChildren = "remove|add";
doc.activePage = doc.activeDashboard === undefined ? 'home': doc.activePage;
this.setupLinkDocs(doc, linkDatabaseId);
this.setupSharedDocs(doc, sharingDocumentId); // sets up the right sidebar collection for mobile upload documents and sharing
@@ -871,14 +906,19 @@ export class CurrentUserUtils {
this.setupOverlays(doc); // sets up the overlay panel where documents and other widgets can be added to float over the rest of the dashboard
this.setupPublished(doc); // sets up the list doc of all docs that have been published (meaning that they can be auto-linked by typing their title into another text box)
this.setupContextMenuButtons(doc); // set up the row of buttons at the top of the dashboard that change depending on what is selected
+ this.setupTopbarButtons(doc);
this.setupDockedButtons(doc); // the bottom bar of font icons
this.setupLeftSidebarMenu(doc); // the left-side column of buttons that open their contents in a flyout panel on the left
this.setupDocTemplates(doc); // sets up the template menu of templates
- this.setupFieldInfos(doc); // sets up the collection of field info descriptions for each possible DocumentOption
+ //this.setupFieldInfos(doc); // sets up the collection of field info descriptions for each possible DocumentOption
DocUtils.AssignDocField(doc, "globalScriptDatabase", (opts) => Docs.Prototypes.MainScriptDocument(), {});
- DocUtils.AssignDocField(doc, "myHeaderBar", (opts) => Docs.Create.MulticolumnDocument([], opts), { title: "header bar", isSystem: true, childDocumentsActive:false, dropAction: 'move'}); // drop down panel at top of dashboard for stashing documents
+ DocUtils.AssignDocField(doc, "myHeaderBar", (opts) => Docs.Create.MulticolumnDocument([], opts), { title: "My Header Bar", isSystem: true, childDocumentsActive:false, dropAction: 'move'}); // drop down panel at top of dashboard for stashing documents
+ Doc.AddDocToList(Doc.MyFilesystem, undefined, Doc.MyDashboards)
Doc.AddDocToList(Doc.MyFilesystem, undefined, Doc.MySharedDocs)
+ Doc.AddDocToList(Doc.MyFilesystem, undefined, Doc.MyRecentlyClosed)
+
+ Doc.GetProto(DocCast(Doc.UserDoc().emptyWebpage)).data = new WebField("https://www.wikipedia.org")
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)
@@ -886,7 +926,7 @@ export class CurrentUserUtils {
}
new LinkManager();
- setTimeout(DocServer.UPDATE_SERVER_CACHE, 2500);
+ DocServer.CacheNeedsUpdate && setTimeout(DocServer.UPDATE_SERVER_CACHE, 2500);
setInterval(DocServer.UPDATE_SERVER_CACHE, 120000);
return doc;
}
@@ -909,22 +949,22 @@ export class CurrentUserUtils {
});
}
+ @observable public static ServerVersion: string = ';'
public static async loadCurrentUser() {
return rp.get(Utils.prepend("/getCurrentUser")).then(async response => {
if (response) {
- const result: { id: string, email: string, cacheDocumentIds: string } = JSON.parse(response);
+ const result: { version: string, userDocumentId: string, sharingDocumentId: string, linkDatabaseId: string, email: string, cacheDocumentIds: string, resolvedPorts: string } = JSON.parse(response);
+ runInAction(() => CurrentUserUtils.ServerVersion = result.version);
Doc.CurrentUserEmail = result.email;
- resolvedPorts = JSON.parse(await (await fetch("/resolvedPorts")).text());
+ resolvedPorts = result.resolvedPorts as any;
DocServer.init(window.location.protocol, window.location.hostname, resolvedPorts.socket, result.email);
if (result.cacheDocumentIds)
{
const ids = result.cacheDocumentIds.split(";");
const batch = 30000;
- FieldLoader.active = true;
for (let i = 0; i < ids.length; i = Math.min(ids.length, i+batch)) {
await DocServer.GetRefFields(ids.slice(i, i+batch));
}
- FieldLoader.active = false;
}
return result;
} else {
@@ -933,27 +973,24 @@ export class CurrentUserUtils {
});
}
- public static async loadUserDocument(id: string) {
- await rp.get(Utils.prepend("/getUserDocumentIds")).then(ids => {
- const { userDocumentId, sharingDocumentId, linkDatabaseId } = JSON.parse(ids);
- if (userDocumentId) {
- return DocServer.GetRefField(userDocumentId).then(async field => {
- Docs.newAccount = !(field instanceof Doc);
- await Docs.Prototypes.initialize();
- const userDoc = Docs.newAccount ? new Doc(userDocumentId, true) : field as Doc;
- this.updateUserDocument(Doc.SetUserDoc(userDoc), sharingDocumentId, linkDatabaseId);
- if (Docs.newAccount) {
- if (Doc.CurrentUserEmail === "guest") {
- DashboardView.createNewDashboard(undefined, "guest dashboard");
- } else {
- userDoc.activePage = "home";
- }
- }
- return userDoc;
- });
- } else {
- throw new Error("There should be a user id! Why does Dash think there isn't one?");
+ public static async loadUserDocument(info:{
+ userDocumentId: string;
+ sharingDocumentId: string;
+ linkDatabaseId: string;
+ }) {
+ return DocServer.GetRefField(info.userDocumentId).then(async field => {
+ Docs.newAccount = !(field instanceof Doc);
+ await Docs.Prototypes.initialize();
+ const userDoc = Docs.newAccount ? new Doc(info.userDocumentId, true) : field as Doc;
+ this.updateUserDocument(Doc.SetUserDoc(userDoc), info.sharingDocumentId, info.linkDatabaseId);
+ if (Docs.newAccount) {
+ if (Doc.CurrentUserEmail === "guest") {
+ DashboardView.createNewDashboard(undefined, "guest dashboard");
+ } else {
+ userDoc.activePage = "home";
+ }
}
+ return userDoc;
});
}
@@ -993,8 +1030,5 @@ export class CurrentUserUtils {
ScriptingGlobals.add(function MySharedDocs() { return Doc.MySharedDocs; }, "document containing all shared Docs");
ScriptingGlobals.add(function IsNoviceMode() { return Doc.noviceMode; }, "is Dash in novice mode");
ScriptingGlobals.add(function toggleComicMode() { Doc.UserDoc().renderStyle = Doc.UserDoc().renderStyle === "comic" ? undefined : "comic"; }, "switches between comic and normal document rendering");
-ScriptingGlobals.add(function createNewPresentation() { return MainView.Instance.createNewPresentation(); }, "creates a new presentation when called");
-ScriptingGlobals.add(function openPresentation(pres:Doc) { return MainView.Instance.openPresentation(pres); }, "creates a new presentation when called");
-ScriptingGlobals.add(function createNewFolder() { return MainView.Instance.createNewFolder(); }, "creates a new folder in myFiles when called");
ScriptingGlobals.add(function importDocument() { return CurrentUserUtils.importDocument(); }, "imports files from device directly into the import sidebar");
-ScriptingGlobals.add(function setInkToolDefaults() { Doc.ActiveTool = InkTool.None; }); \ No newline at end of file
+ScriptingGlobals.add(function setInkToolDefaults() { Doc.ActiveTool = InkTool.None; });
diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts
index 717473aa1..0fd7e840c 100644
--- a/src/client/util/DictationManager.ts
+++ b/src/client/util/DictationManager.ts
@@ -11,7 +11,7 @@ import { Utils } from '../../Utils';
import { Docs } from '../documents/Documents';
import { DocumentType } from '../documents/DocumentTypes';
import { DictationOverlay } from '../views/DictationOverlay';
-import { DocumentView, OpenWhere, OpenWhereMod } from '../views/nodes/DocumentView';
+import { DocumentView, OpenWhere } from '../views/nodes/DocumentView';
import { SelectionManager } from './SelectionManager';
import { UndoManager } from './UndoManager';
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 8e4e0d8f3..b9f6059f4 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -1,9 +1,9 @@
-import { action, computed, observable, ObservableSet } from 'mobx';
+import { action, computed, observable, ObservableSet, observe } from 'mobx';
import { Doc, DocListCast, Opt } from '../../fields/Doc';
import { AclAdmin, AclEdit, Animation } from '../../fields/DocSymbols';
import { Id } from '../../fields/FieldSymbols';
import { listSpec } from '../../fields/Schema';
-import { Cast, DocCast, StrCast } from '../../fields/Types';
+import { Cast, DocCast, NumCast, StrCast } from '../../fields/Types';
import { AudioField } from '../../fields/URLField';
import { GetEffectiveAcl } from '../../fields/util';
import { CollectionViewType } from '../documents/DocumentTypes';
@@ -11,7 +11,6 @@ import { CollectionDockingView } from '../views/collections/CollectionDockingVie
import { TabDocView } from '../views/collections/TabDocView';
import { LightboxView } from '../views/LightboxView';
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 { PresBox } from '../views/nodes/trails';
@@ -23,7 +22,6 @@ export class DocumentManager {
//global holds all of the nodes (regardless of which collection they're in)
@observable _documentViews = new Set<DocumentView>();
@observable public LinkAnchorBoxViews: DocumentView[] = [];
- @observable public RecordingEvent = 0;
@observable public LinkedDocumentViews: { a: DocumentView; b: DocumentView; l: Doc }[] = [];
@computed public get DocumentViews() {
return Array.from(this._documentViews).filter(view => !(view.ComponentView instanceof KeyValueBox) && (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(view.docViewPath)));
@@ -41,7 +39,22 @@ export class DocumentManager {
}
//private constructor so no other class can create a nodemanager
- private constructor() {}
+ private constructor() {
+ if (!Doc.CurrentlyLoading) Doc.CurrentlyLoading = [];
+ observe(Doc.CurrentlyLoading, change => {
+ // watch CurrentlyLoading-- when something is loaded, it's removed from the list and we have to update its icon if it were iconified since LoadingBox icons are different than the media they become
+ switch (change.type as any) {
+ case 'update':
+ break;
+ case 'remove':
+ // DocumentManager.Instance.getAllDocumentViews(change as any).forEach(dv => StrCast(dv.rootDoc.layout_fieldKey) === 'layout_icon' && dv.iconify(() => dv.iconify()));
+ break;
+ case 'splice':
+ (change as any).removed.forEach((doc: Doc) => DocumentManager.Instance.getAllDocumentViews(doc).forEach(dv => StrCast(dv.rootDoc.layout_fieldKey) === 'layout_icon' && dv.iconify(() => dv.iconify())));
+ break;
+ }
+ });
+ }
private _viewRenderedCbs: { doc: Doc; func: (dv: DocumentView) => any }[] = [];
public AddViewRenderedCb = (doc: Opt<Doc>, func: (dv: DocumentView) => any) => {
@@ -75,7 +88,7 @@ export class DocumentManager {
@action
public AddView = (view: DocumentView) => {
- //console.log("MOUNT " + view.props.Document.title + "/" + view.props.LayoutTemplateString);
+ if (view.props.LayoutTemplateString?.includes(KeyValueBox.name)) return;
if (view.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) {
const viewAnchorIndex = view.props.LayoutTemplateString.includes('link_anchor_2') ? 'link_anchor_2' : 'link_anchor_1';
const link = view.rootDoc;
@@ -87,8 +100,6 @@ export class DocumentManager {
})
);
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.AddDocumentView(view);
}
@@ -205,7 +216,7 @@ export class DocumentManager {
}
static playAudioAnno(doc: Doc) {
- const anno = Cast(doc[Doc.LayoutFieldKey(doc) + '-audioAnnotations'], listSpec(AudioField), null)?.lastElement();
+ const anno = Cast(doc[Doc.LayoutFieldKey(doc) + '_audioAnnotations'], listSpec(AudioField), null)?.lastElement();
if (anno) {
if (anno instanceof AudioField) {
new Howl({
@@ -251,6 +262,7 @@ export class DocumentManager {
options: DocFocusOptions, // options for how to navigate to target
finished?: (changed: boolean) => void // func called after focusing on target with flag indicating whether anything needed to be done.
) => {
+ Doc.RemoveDocFromList(Doc.MyRecentlyClosed, undefined, targetDoc);
const docContextPath = DocumentManager.GetContextPath(targetDoc, true);
if (docContextPath.some(doc => doc.hidden)) options.toggleTarget = false;
let rootContextView =
@@ -307,17 +319,20 @@ export class DocumentManager {
@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);
+ //if (docView.ComponentView instanceof FormattedTextBox)
+ //viewSpec !== docView.rootDoc &&
+ docView.ComponentView?.focus?.(viewSpec, options);
PresBox.restoreTargetDocView(docView, viewSpec, options.zoomTime ?? 500);
- Doc.linkFollowHighlight(docView.rootDoc, undefined, options.effect);
+ Doc.linkFollowHighlight(viewSpec ? [docView.rootDoc, viewSpec] : docView.rootDoc, undefined, options.effect);
+ if (options.playMedia) docView.ComponentView?.playFrom?.(NumCast(docView.rootDoc._layout_currentTimecode));
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 (options.zoomTextSelections && Doc.UnhighlightTimer && contextView && viewSpec.text_html) {
// 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);
+ contextView.htmlOverlayEffect = StrCast(options?.effect?.presentation_effect, StrCast(options?.effect?.followLinkAnimEffect));
+ contextView.textHtmlOverlay = StrCast(targetDoc.text_html);
DocumentManager._overlayViews.add(contextView);
}
Doc.AddUnHighlightWatcher(() => {
@@ -335,7 +350,7 @@ export function DocFocusOrOpen(doc: Doc, options: DocFocusOptions = { willZoomCe
if (dv && (!containingDoc || dv.props.docViewPath().lastElement()?.Document === containingDoc)) {
DocumentManager.Instance.showDocumentView(dv, options).then(() => dv && Doc.linkFollowHighlight(dv.rootDoc));
} else {
- const container = DocCast(containingDoc ?? doc.embedContainer ?? doc);
+ const container = DocCast(containingDoc ?? doc.embedContainer ?? Doc.BestEmbedding(doc));
const showDoc = !Doc.IsSystem(container) ? container : doc;
options.toggleTarget = undefined;
DocumentManager.Instance.showDocument(showDoc, options, () => DocumentManager.Instance.showDocument(doc, { ...options, openLocation: undefined })).then(() => {
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 85101fcab..6d6eaebec 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -4,18 +4,17 @@ import { Doc, Field, Opt, StrListCast } from '../../fields/Doc';
import { List } from '../../fields/List';
import { PrefetchProxy } from '../../fields/Proxy';
import { ScriptField } from '../../fields/ScriptField';
-import { BoolCast, ScriptCast, StrCast } from '../../fields/Types';
+import { 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 = 'embed' | 'copy' | 'move' | 'same' | 'proto' | 'none' | undefined; // undefined = move, "same" = move but don't call dropPropertiesToRemove
+export type dropActionType = 'embed' | 'copy' | 'move' | 'add' | 'same' | 'proto' | 'none' | undefined; // undefined = move, "same" = move but don't call dropPropertiesToRemove
/**
* Initialize drag
@@ -61,12 +60,6 @@ export namespace DragManager {
export let StartWindowDrag: Opt<(e: { pageX: number; pageY: number }, dragDocs: Doc[], finishDrag?: (aborted: boolean) => void) => void>;
export let CompleteWindowDrag: Opt<(aborted: boolean) => void>;
- export function GetRaiseWhenDragged() {
- return BoolCast(Doc.UserDoc()._raiseWhenDragged);
- }
- export function SetRaiseWhenDragged(val: boolean) {
- Doc.UserDoc()._raiseWhenDragged = val;
- }
export function Root() {
const root = document.getElementById('root');
if (!root) {
@@ -195,7 +188,7 @@ export namespace DragManager {
}
// drag a document and drop it (or make an embed/copy on drop)
- export function StartDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions, dropEvent?: () => any) {
+ export function StartDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions, onDropCompleted?: (e?: DragCompleteEvent) => any) {
const addAudioTag = (dropDoc: any) => {
dropDoc && !dropDoc.author_date && (dropDoc.author_date = new DateField());
dropDoc instanceof Doc && DocUtils.MakeLinkToActiveAudio(() => dropDoc);
@@ -203,7 +196,7 @@ export namespace DragManager {
};
const finishDrag = async (e: DragCompleteEvent) => {
const docDragData = e.docDragData;
- dropEvent?.(); // glr: optional additional function to be called - in this case with presentation trails
+ onDropCompleted?.(e); // 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 = (
@@ -213,6 +206,8 @@ export namespace DragManager {
? addAudioTag(ScriptCast(d.onDragStart).script.run({ this: d }).result)
: docDragData.dropAction === 'embed'
? Doc.BestEmbedding(d)
+ : docDragData.dropAction === 'add'
+ ? d
: docDragData.dropAction === 'proto'
? Doc.GetProto(d)
: docDragData.dropAction === 'copy'
@@ -343,6 +338,7 @@ export namespace DragManager {
dragLabel.style.zIndex = '100001';
dragLabel.style.fontSize = '10px';
dragLabel.style.position = 'absolute';
+ dragLabel.style.background = '#ffffff90';
dragLabel.innerText = 'drag titlebar to embed on drop'; // bcz: need to move this to a status bar
dragDiv.appendChild(dragLabel);
DragManager.Root().appendChild(dragDiv);
@@ -458,7 +454,7 @@ export namespace DragManager {
runInAction(() => docsBeingDragged.push(...docsToDrag));
const hideDragShowOriginalElements = (hide: boolean) => {
- dragLabel.style.display = hide ? '' : 'none';
+ dragLabel.style.display = hide && !CanEmbed ? '' : 'none';
!hide && dragElements.map(dragElement => dragElement.parentNode === dragDiv && dragDiv.removeChild(dragElement));
setTimeout(() => eles.forEach(ele => (ele.hidden = hide)));
};
@@ -603,18 +599,9 @@ export namespace DragManager {
}
}
-ScriptingGlobals.add(function toggleRaiseOnDrag(forAllDocs: boolean, readOnly?: boolean) {
+ScriptingGlobals.add(function toggleRaiseOnDrag(readOnly?: boolean) {
if (readOnly) {
- if (SelectionManager.Views().length)
- return SelectionManager.Views().some(dv => dv.rootDoc.raiseWhenDragged)
- ? Colors.MEDIUM_BLUE
- : SelectionManager.Views().some(dv => dv.rootDoc.raiseWhenDragged === false)
- ? 'transparent'
- : DragManager.GetRaiseWhenDragged()
- ? Colors.MEDIUM_BLUE_ALT
- : Colors.LIGHT_BLUE;
- return DragManager.GetRaiseWhenDragged() ? Colors.MEDIUM_BLUE_ALT : 'transparent';
+ return SelectionManager.Views().some(dv => dv.rootDoc.keepZWhenDragged);
}
- if (!forAllDocs) SelectionManager.Views().map(dv => (dv.rootDoc.raiseWhenDragged ? (dv.rootDoc.raiseWhenDragged = undefined) : dv.rootDoc.raiseWhenDragged === false ? (dv.rootDoc.raiseWhenDragged = true) : (dv.rootDoc.raiseWhenDragged = false)));
- else DragManager.SetRaiseWhenDragged(!DragManager.GetRaiseWhenDragged());
+ SelectionManager.Views().map(dv => (dv.rootDoc.keepZWhenDragged = !dv.rootDoc.keepZWhenDragged));
});
diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts
index f235be192..dbdf580cd 100644
--- a/src/client/util/DropConverter.ts
+++ b/src/client/util/DropConverter.ts
@@ -1,15 +1,15 @@
-import { DragManager } from './DragManager';
import { Doc, DocListCast, Opt } from '../../fields/Doc';
-import { DocumentType } from '../documents/DocumentTypes';
import { ObjectField } from '../../fields/ObjectField';
-import { StrCast, Cast } from '../../fields/Types';
-import { Docs } from '../documents/Documents';
-import { ScriptField, ComputedField } from '../../fields/ScriptField';
import { RichTextField } from '../../fields/RichTextField';
-import { ImageField } from '../../fields/URLField';
-import { ScriptingGlobals } from './ScriptingGlobals';
import { listSpec } from '../../fields/Schema';
+import { ScriptField } from '../../fields/ScriptField';
+import { Cast, StrCast } from '../../fields/Types';
+import { ImageField } from '../../fields/URLField';
+import { Docs } from '../documents/Documents';
+import { DocumentType } from '../documents/DocumentTypes';
import { ButtonType } from '../views/nodes/FontIconBox/FontIconBox';
+import { DragManager } from './DragManager';
+import { ScriptingGlobals } from './ScriptingGlobals';
export function MakeTemplate(doc: Doc, first: boolean = true, rename: Opt<string> = undefined, templateField: string = '') {
if (templateField) Doc.GetProto(doc).title = templateField; /// the title determines which field is being templated
diff --git a/src/client/util/GroupManager.tsx b/src/client/util/GroupManager.tsx
index 5802d5ee0..8973306bf 100644
--- a/src/client/util/GroupManager.tsx
+++ b/src/client/util/GroupManager.tsx
@@ -1,21 +1,22 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Button, IconButton, Size, Type } from 'browndash-components';
import { action, computed, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import Select from 'react-select';
import * as RequestPromise from 'request-promise';
-import { Doc, DocListCast, DocListCastAsync, Opt } from '../../fields/Doc';
-import { StrCast, Cast } from '../../fields/Types';
+import { DateField } from '../../fields/DateField';
+import { Doc, DocListCast, Opt } from '../../fields/Doc';
+import { Id } from '../../fields/FieldSymbols';
+import { listSpec } from '../../fields/Schema';
+import { Cast, StrCast } from '../../fields/Types';
import { Utils } from '../../Utils';
import { MainViewModal } from '../views/MainViewModal';
import { TaskCompletionBox } from '../views/nodes/TaskCompletedBox';
import './GroupManager.scss';
import { GroupMemberView } from './GroupMemberView';
+import { SettingsManager } from './SettingsManager';
import { SharingManager, User } from './SharingManager';
-import { listSpec } from '../../fields/Schema';
-import { DateField } from '../../fields/DateField';
-import { Id } from '../../fields/FieldSymbols';
-import { Button, IconButton, Size } from 'browndash-components';
/**
* Interface for options for the react-select component
@@ -281,7 +282,7 @@ export class GroupManager extends React.Component<{}> {
*/
private get groupCreationModal() {
const contents = (
- <div className="group-create" style={{background: StrCast(Doc.UserDoc().userBackgroundColor), color: StrCast(Doc.UserDoc().userColor)}}>
+ <div className="group-create" style={{ background: SettingsManager.userBackgroundColor, color: SettingsManager.userColor }}>
<div className="group-heading" style={{ marginBottom: 0 }}>
<p>
<b>New Group</b>
@@ -297,10 +298,10 @@ export class GroupManager extends React.Component<{}> {
/>
</div>
</div>
- <div className="group-input" style={{border: StrCast(Doc.UserDoc().userColor)}} >
+ <div className="group-input" style={{ border: StrCast(Doc.UserDoc().userColor) }}>
<input ref={this.inputRef} onKeyDown={this.handleKeyDown} autoFocus type="text" placeholder="Group name" onChange={action(() => (this.buttonColour = this.inputRef.current?.value ? 'black' : '#979797'))} />
</div>
- <div style={{border: StrCast(Doc.UserDoc().userColor)}}>
+ <div style={{ border: StrCast(Doc.UserDoc().userColor) }}>
<Select
className="select-users"
isMulti
@@ -331,8 +332,8 @@ export class GroupManager extends React.Component<{}> {
}}
/>
</div>
- <div className={"create-button"}>
- <Button text={"Create"} type={"tertiary"} color={StrCast(Doc.UserDoc().userColor)} onClick={this.createGroup} />
+ <div className={'create-button'}>
+ <Button text={'Create'} type={Type.TERT} color={StrCast(Doc.UserDoc().userColor)} onClick={this.createGroup} />
</div>
</div>
);
@@ -366,29 +367,25 @@ export class GroupManager extends React.Component<{}> {
const groups = this.groupSort === 'ascending' ? this.allGroups.sort(sortGroups) : this.groupSort === 'descending' ? this.allGroups.sort(sortGroups).reverse() : this.allGroups;
return (
- <div className="group-interface" style={{background: StrCast(Doc.UserDoc().userBackgroundColor), color: StrCast(Doc.UserDoc().userColor)}}>
+ <div className="group-interface" style={{ background: SettingsManager.userBackgroundColor, color: SettingsManager.userColor }}>
{this.groupCreationModal}
{this.currentGroup ? <GroupMemberView group={this.currentGroup} onCloseButtonClick={action(() => (this.currentGroup = undefined))} /> : null}
<div className="group-heading">
<p>
<b>Manage Groups</b>
</p>
- <Button icon={<FontAwesomeIcon icon={'plus'}/>} iconPlacement={'left'} text={"Create Group"} type={"tertiary"} color={StrCast(Doc.UserDoc().userColor)} onClick={action(() => (this.createGroupModalOpen = true))} />
- <div className={'close-button'} >
- <Button
- icon={<FontAwesomeIcon icon={'times'} size={'lg'} />}
- onClick={this.close}
- color={StrCast(Doc.UserDoc().userColor)}
- />
+ <Button icon={<FontAwesomeIcon icon={'plus'} />} iconPlacement={'left'} text={'Create Group'} type={Type.TERT} color={StrCast(Doc.UserDoc().userColor)} onClick={action(() => (this.createGroupModalOpen = true))} />
+ <div className={'close-button'}>
+ <Button icon={<FontAwesomeIcon icon={'times'} size={'lg'} />} onClick={this.close} color={StrCast(Doc.UserDoc().userColor)} />
</div>
</div>
<div className="main-container">
<div className="sort-groups" onClick={action(() => (this.groupSort = this.groupSort === 'ascending' ? 'descending' : this.groupSort === 'descending' ? 'none' : 'ascending'))}>
Name
- <IconButton icon={<FontAwesomeIcon icon={this.groupSort === 'ascending' ? 'caret-up' : this.groupSort === 'descending' ? 'caret-down' : 'caret-right'}/>} size={Size.XSMALL} color={StrCast(Doc.UserDoc().userColor)} />
+ <IconButton icon={<FontAwesomeIcon icon={this.groupSort === 'ascending' ? 'caret-up' : this.groupSort === 'descending' ? 'caret-down' : 'caret-right'} />} size={Size.XSMALL} color={StrCast(Doc.UserDoc().userColor)} />
</div>
- <div className={'style-divider'} style={{background: StrCast(Doc.UserDoc().userColor)}}/>
- <div className="group-body" style={{background: StrCast(Doc.UserDoc().userBackgroundColor), color: StrCast(Doc.UserDoc().userColor)}}>
+ <div className={'style-divider'} style={{ background: StrCast(Doc.UserDoc().userColor) }} />
+ <div className="group-body" style={{ background: StrCast(Doc.UserDoc().userBackgroundColor), color: StrCast(Doc.UserDoc().userColor) }}>
{groups.map(group => (
<div className="group-row" key={StrCast(group.title || group.groupName)}>
<div className="group-name">{StrCast(group.title || group.groupName)}</div>
diff --git a/src/client/util/GroupMemberView.tsx b/src/client/util/GroupMemberView.tsx
index f2050dc61..7de0f336f 100644
--- a/src/client/util/GroupMemberView.tsx
+++ b/src/client/util/GroupMemberView.tsx
@@ -1,14 +1,15 @@
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, observable } from "mobx";
-import { observer } from "mobx-react";
-import * as React from "react";
-import Select from "react-select";
-import { Doc } from "../../fields/Doc";
-import { StrCast } from "../../fields/Types";
-import { MainViewModal } from "../views/MainViewModal";
-import { GroupManager, UserOptions } from "./GroupManager";
-import "./GroupMemberView.scss";
-import { Button, IconButton, Size } from "browndash-components";
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { action, observable } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import Select from 'react-select';
+import { Doc } from '../../fields/Doc';
+import { StrCast } from '../../fields/Types';
+import { MainViewModal } from '../views/MainViewModal';
+import { GroupManager, UserOptions } from './GroupManager';
+import './GroupMemberView.scss';
+import { Button, IconButton, Size, Type } from 'browndash-components';
+import { SettingsManager } from './SettingsManager';
interface GroupMemberViewProps {
group: Doc;
@@ -17,44 +18,37 @@ interface GroupMemberViewProps {
@observer
export class GroupMemberView extends React.Component<GroupMemberViewProps> {
-
- @observable private memberSort: "ascending" | "descending" | "none" = "none";
+ @observable private memberSort: 'ascending' | 'descending' | 'none' = 'none';
private get editingInterface() {
let members: string[] = this.props.group ? JSON.parse(StrCast(this.props.group.members)) : [];
- members = this.memberSort === "ascending" ? members.sort() : this.memberSort === "descending" ? members.sort().reverse() : members;
+ members = this.memberSort === 'ascending' ? members.sort() : this.memberSort === 'descending' ? members.sort().reverse() : members;
const options: UserOptions[] = this.props.group ? GroupManager.Instance.options.filter(option => !(JSON.parse(StrCast(this.props.group.members)) as string[]).includes(option.value)) : [];
const hasEditAccess = GroupManager.Instance.hasEditAccess(this.props.group);
- return (!this.props.group ? null :
- <div className="editing-interface" style={{background: StrCast(Doc.UserDoc().userBackgroundColor), color: StrCast(Doc.UserDoc().userColor)}}>
+ return !this.props.group ? null : (
+ <div className="editing-interface" style={{ background: SettingsManager.userBackgroundColor, color: SettingsManager.userColor }}>
<div className="editing-header">
<input
className="group-title"
- style={{ marginLeft: !hasEditAccess ? "-14%" : 0 }}
+ style={{ marginLeft: !hasEditAccess ? '-14%' : 0 }}
value={StrCast(this.props.group.title || this.props.group.groupName)}
- onChange={e => this.props.group.title = e.currentTarget.value}
- disabled={!hasEditAccess}
- >
- </input>
- <div className={"memberView-closeButton"} >
- <Button
- icon={<FontAwesomeIcon icon={'times'} size={'lg'} />}
- onClick={action(this.props.onCloseButtonClick)}
- color={StrCast(Doc.UserDoc().userColor)}
- />
+ onChange={e => (this.props.group.title = e.currentTarget.value)}
+ disabled={!hasEditAccess}></input>
+ <div className={'memberView-closeButton'}>
+ <Button icon={<FontAwesomeIcon icon={'times'} size={'lg'} />} onClick={action(this.props.onCloseButtonClick)} color={StrCast(Doc.UserDoc().userColor)} />
</div>
- {GroupManager.Instance.hasEditAccess(this.props.group) ?
+ {GroupManager.Instance.hasEditAccess(this.props.group) ? (
<div className="group-buttons">
- <div style={{border: StrCast(Doc.UserDoc().userColor)}} >
- <Select
+ <div style={{ border: StrCast(Doc.UserDoc().userColor) }}>
+ <Select
className="add-member-dropdown"
isSearchable={true}
options={options}
onChange={selectedOption => GroupManager.Instance.addMemberToGroup(this.props.group, (selectedOption as UserOptions).value)}
- placeholder={"Add members"}
+ placeholder={'Add members'}
value={null}
styles={{
control: () => ({
@@ -78,52 +72,33 @@ export class GroupMemberView extends React.Component<GroupMemberViewProps> {
}}
/>
</div>
- <div className={"delete-button"}>
- <Button text={"Delete Group"} type={"tertiary"} color={StrCast(Doc.UserDoc().userColor)} onClick={() => GroupManager.Instance.deleteGroup(this.props.group)} />
+ <div className={'delete-button'}>
+ <Button text={'Delete Group'} type={Type.TERT} color={StrCast(Doc.UserDoc().userColor)} onClick={() => GroupManager.Instance.deleteGroup(this.props.group)} />
</div>
- </div> :
- null}
- <div
- className="sort-emails"
- style={{ paddingTop: hasEditAccess ? 0 : 35 }}
- onClick={action(() => this.memberSort = this.memberSort === "ascending" ? "descending" : this.memberSort === "descending" ? "none" : "ascending")}>
- Emails {this.memberSort === "ascending" ? "↑" : this.memberSort === "descending" ? "↓" : ""} {/* → */}
+ </div>
+ ) : null}
+ <div className="sort-emails" style={{ paddingTop: hasEditAccess ? 0 : 35 }} onClick={action(() => (this.memberSort = this.memberSort === 'ascending' ? 'descending' : this.memberSort === 'descending' ? 'none' : 'ascending'))}>
+ Emails {this.memberSort === 'ascending' ? '↑' : this.memberSort === 'descending' ? '↓' : ''} {/* → */}
</div>
</div>
- <div className={'style-divider'} style={{background: StrCast(Doc.UserDoc().userColor)}}/>
- <div className="editing-contents"
- style={{ height: hasEditAccess ? "62%" : "85%" }}
- >
+ <div className={'style-divider'} style={{ background: StrCast(Doc.UserDoc().userColor) }} />
+ <div className="editing-contents" style={{ height: hasEditAccess ? '62%' : '85%' }}>
{members.map(member => (
- <div
- className="editing-row"
- key={member}
- >
- <div className="user-email">
- {member}
- </div>
- {hasEditAccess ?
- <div className={"remove-button"} onClick={() => GroupManager.Instance.removeMemberFromGroup(this.props.group, member)}>
+ <div className="editing-row" key={member}>
+ <div className="user-email">{member}</div>
+ {hasEditAccess ? (
+ <div className={'remove-button'} onClick={() => GroupManager.Instance.removeMemberFromGroup(this.props.group, member)}>
<IconButton icon={<FontAwesomeIcon icon={'trash-alt'} />} size={Size.XSMALL} color={StrCast(Doc.UserDoc().userColor)} onClick={() => GroupManager.Instance.removeMemberFromGroup(this.props.group, member)} />
</div>
- : null}
+ ) : null}
</div>
))}
</div>
</div>
);
-
}
render() {
- return <MainViewModal
- isDisplayed={true}
- interactive={true}
- contents={this.editingInterface}
- dialogueBoxStyle={{ width: 400, height: 250 }}
- closeOnExternalClick={this.props.onCloseButtonClick}
- />;
+ return <MainViewModal isDisplayed={true} interactive={true} contents={this.editingInterface} dialogueBoxStyle={{ width: 400, height: 250 }} closeOnExternalClick={this.props.onCloseButtonClick} />;
}
-
-
-} \ No newline at end of file
+}
diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx
index d0f459291..4e32ed67f 100644
--- a/src/client/util/InteractionUtils.tsx
+++ b/src/client/util/InteractionUtils.tsx
@@ -108,7 +108,8 @@ export namespace InteractionUtils {
opacity: number,
nodefs: boolean,
downHdlr?: (e: React.PointerEvent) => void,
- mask?: boolean
+ mask?: boolean,
+ dropshadow?: string
) {
const pts = shape ? makePolygon(shape, points) : points;
@@ -161,7 +162,6 @@ 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`}
/>
@@ -172,6 +172,7 @@ export namespace InteractionUtils {
<Tag
d={bezier ? strpts : undefined}
points={bezier ? undefined : strpts}
+ filter={!dropshadow ? undefined : `drop-shadow(-1px -1px 0px ${dropshadow}) drop-shadow(2px -1px 0px ${dropshadow}) drop-shadow(2px 2px 0px ${dropshadow}) drop-shadow(-1px 2px 0px ${dropshadow})`}
style={{
// filter: drawHalo ? "url(#inkSelectionHalo)" : undefined,
fill: fill && fill !== 'transparent' ? fill : 'none',
diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts
index b8fea340f..2fc811b09 100644
--- a/src/client/util/LinkFollower.ts
+++ b/src/client/util/LinkFollower.ts
@@ -73,6 +73,7 @@ export class LinkFollower {
const toggleTarget = canToggle && BoolCast(sourceDoc.followLinkToggle);
const options: DocFocusOptions = {
playAudio: BoolCast(sourceDoc.followLinkAudio),
+ playMedia: BoolCast(sourceDoc.followLinkVideo),
toggleTarget,
noSelect: true,
willPan: true,
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index c7f092565..a533fdd1f 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -1,7 +1,8 @@
-import { action, observable, observe } from 'mobx';
+import { action, observable, observe, runInAction } from 'mobx';
import { computedFn } from 'mobx-utils';
import { Doc, DocListCast, DocListCastAsync, Field, Opt } from '../../fields/Doc';
import { DirectLinks } from '../../fields/DocSymbols';
+import { FieldLoader } from '../../fields/FieldLoader';
import { List } from '../../fields/List';
import { ProxyField } from '../../fields/Proxy';
import { Cast, DocCast, PromiseValue, StrCast } from '../../fields/Types';
@@ -55,10 +56,12 @@ export class LinkManager {
Promise.all([lproto?.link_anchor_1 as Doc, lproto?.link_anchor_2 as Doc].map(PromiseValue)).then((lAnchs: Opt<Doc>[]) =>
Promise.all(lAnchs.map(lAnch => PromiseValue(lAnch?.proto as Doc))).then((lAnchProtos: Opt<Doc>[]) =>
Promise.all(lAnchProtos.map(lAnchProto => PromiseValue(lAnchProto?.proto as Doc))).then(
- action(lAnchProtoProtos => {
- link && lAnchs[0] && Doc.GetProto(lAnchs[0])[DirectLinks].add(link);
- link && lAnchs[1] && Doc.GetProto(lAnchs[1])[DirectLinks].add(link);
- })
+ link &&
+ action(lAnchProtoProtos => {
+ Doc.AddDocToList(Doc.UserDoc(), 'links', link);
+ lAnchs[0] && Doc.GetProto(lAnchs[0])[DirectLinks].add(link);
+ lAnchs[1] && Doc.GetProto(lAnchs[1])[DirectLinks].add(link);
+ })
)
)
)
@@ -132,6 +135,7 @@ export class LinkManager {
},
true
);
+ runInAction(() => (FieldLoader.ServerLoadStatus.message = 'links'));
LinkManager.addLinkDB(Doc.LinkDBDoc());
}
@@ -143,6 +147,7 @@ export class LinkManager {
};
public addLink(linkDoc: Doc, checkExists = false) {
+ Doc.AddDocToList(Doc.UserDoc(), 'links', linkDoc);
if (!checkExists || !DocListCast(Doc.LinkDBDoc().data).includes(linkDoc)) {
Doc.AddDocToList(Doc.LinkDBDoc(), 'data', linkDoc);
setTimeout(DocServer.UPDATE_SERVER_CACHE, 100);
diff --git a/src/client/util/PingManager.ts b/src/client/util/PingManager.ts
index 0c41a1ea7..4dd2fcd35 100644
--- a/src/client/util/PingManager.ts
+++ b/src/client/util/PingManager.ts
@@ -1,5 +1,6 @@
-import { action, observable } from 'mobx';
+import { action, observable, runInAction } from 'mobx';
import { Networking } from '../Network';
+import { CurrentUserUtils } from './CurrentUserUtils';
export class PingManager {
// create static instance and getter for global use
@observable static _instance: PingManager;
@@ -18,7 +19,8 @@ export class PingManager {
};
sendPing = async (): Promise<void> => {
try {
- await Networking.PostToServer('/ping', { date: new Date() });
+ const res = await Networking.PostToServer('/ping', { date: new Date() });
+ runInAction(() => (CurrentUserUtils.ServerVersion = res.message));
!this.IsBeating && this.setIsBeating(true);
} catch {
if (this.IsBeating) {
diff --git a/src/client/util/RTFMarkup.tsx b/src/client/util/RTFMarkup.tsx
index b93d4f293..c8940194c 100644
--- a/src/client/util/RTFMarkup.tsx
+++ b/src/client/util/RTFMarkup.tsx
@@ -2,6 +2,9 @@ import { action, computed, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { MainViewModal } from '../views/MainViewModal';
+import { SettingsManager } from './SettingsManager';
+import { Doc } from '../../fields/Doc';
+import { StrCast } from '../../fields/Types';
@observer
export class RTFMarkup extends React.Component<{}> {
@@ -30,7 +33,7 @@ export class RTFMarkup extends React.Component<{}> {
*/
@computed get cheatSheet() {
return (
- <div style={{ textAlign: 'initial', height: '100%' }}>
+ <div style={{ background: SettingsManager.userBackgroundColor, color: SettingsManager.userColor, textAlign: 'initial', height: '100%' }}>
<p>
<b style={{ fontSize: 'larger' }}>{`wiki:phrase`}</b>
{` display wikipedia page for entered text (terminate with carriage return)`}
@@ -132,6 +135,14 @@ export class RTFMarkup extends React.Component<{}> {
}
render() {
- return <MainViewModal contents={this.cheatSheet} isDisplayed={this.isOpen} interactive={true} closeOnExternalClick={this.close} />;
+ return (
+ <MainViewModal
+ dialogueBoxStyle={{ backgroundColor: SettingsManager.userBackgroundColor, color: SettingsManager.userColor, padding: '16px' }}
+ 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 cbc465d6a..d99630f82 100644
--- a/src/client/util/ReplayMovements.ts
+++ b/src/client/util/ReplayMovements.ts
@@ -187,7 +187,6 @@ export class ReplayMovements {
} else {
// tab wasn't open - open it and play the movement
const openedColFFView = this.openTab(movement.doc);
- console.log('openedColFFView', openedColFFView);
openedColFFView && this.zoomAndPan(movement, openedColFFView);
}
diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts
index d154c48a4..560d6b30f 100644
--- a/src/client/util/SearchUtil.ts
+++ b/src/client/util/SearchUtil.ts
@@ -1,13 +1,115 @@
import * as rp from 'request-promise';
import { DocServer } from '../DocServer';
-import { Doc } from '../../fields/Doc';
+import { Doc, DocListCast, Field, Opt } from '../../fields/Doc';
import { Id } from '../../fields/FieldSymbols';
import { Utils } from '../../Utils';
import { DocumentType } from '../documents/DocumentTypes';
+import { StrCast } from '../../fields/Types';
export namespace SearchUtil {
export type HighlightingResult = { [id: string]: { [key: string]: string[] } };
+ export function SearchCollection(rootDoc: Opt<Doc>, query: string) {
+ const blockedTypes = [DocumentType.PRESELEMENT, DocumentType.CONFIG, DocumentType.KVP, DocumentType.FONTICON, DocumentType.BUTTON, DocumentType.SCRIPTING];
+ const blockedKeys = [
+ 'x',
+ 'y',
+ 'proto',
+ 'width',
+ 'layout_autoHeight',
+ 'acl-Override',
+ 'acl-Guest',
+ 'embedContainer',
+ 'zIndex',
+ 'height',
+ 'text_scrollHeight',
+ 'text_height',
+ 'cloneFieldFilter',
+ 'isDataDoc',
+ 'text_annotations',
+ 'dragFactory_count',
+ 'text_noTemplate',
+ 'proto_embeddings',
+ 'isSystem',
+ 'layout_fieldKey',
+ 'isBaseProto',
+ 'xMargin',
+ 'yMargin',
+ 'links',
+ 'layout',
+ 'layout_keyValue',
+ 'layout_fitWidth',
+ 'type_collection',
+ 'title_custom',
+ 'freeform_panX',
+ 'freeform_panY',
+ 'freeform_scale',
+ ];
+ query = query.toLowerCase();
+
+ const results = new Map<Doc, string[]>();
+ if (rootDoc) {
+ const docs = DocListCast(rootDoc[Doc.LayoutFieldKey(rootDoc)]);
+ const docIDs: String[] = [];
+ SearchUtil.foreachRecursiveDoc(docs, (depth: number, doc: Doc) => {
+ const dtype = StrCast(doc.type) as DocumentType;
+ if (dtype && !blockedTypes.includes(dtype) && !docIDs.includes(doc[Id]) && depth >= 0) {
+ const hlights = new Set<string>();
+ SearchUtil.documentKeys(doc).forEach(
+ key =>
+ (Field.toString(doc[key] as Field) + Field.toScriptString(doc[key] as Field))
+ .toLowerCase() //
+ .includes(query) && hlights.add(key)
+ );
+ blockedKeys.forEach(key => hlights.delete(key));
+
+ if (Array.from(hlights.keys()).length > 0) {
+ results.set(doc, Array.from(hlights.keys()));
+ }
+ }
+ docIDs.push(doc[Id]);
+ });
+ }
+ return results;
+ }
+ /**
+ * @param {Doc} doc - doc for which keys are returned
+ *
+ * This method returns a list of a document doc's keys.
+ */
+ export function documentKeys(doc: Doc) {
+ const keys: { [key: string]: boolean } = {};
+ Doc.GetAllPrototypes(doc).map(proto => Object.keys(proto).forEach(key => (keys[key] = false)));
+ return Array.from(Object.keys(keys));
+ }
+
+ /**
+ * @param {Doc[]} docs - docs to be searched through recursively
+ * @param {number, Doc => void} func - function to be called on each doc
+ *
+ * This method iterates through an array of docs and all docs within those docs, calling
+ * the function func on each doc.
+ */
+ export function foreachRecursiveDoc(docs: Doc[], func: (depth: number, doc: Doc) => void) {
+ let newarray: Doc[] = [];
+ var depth = 0;
+ const visited: Doc[] = [];
+ while (docs.length > 0) {
+ newarray = [];
+ docs.filter(d => d && !visited.includes(d)).forEach(d => {
+ visited.push(d);
+ const fieldKey = Doc.LayoutFieldKey(d);
+ const annos = !Field.toString(Doc.LayoutField(d) as Field).includes('CollectionView');
+ const data = d[annos ? fieldKey + '_annotations' : fieldKey];
+ data && newarray.push(...DocListCast(data));
+ const sidebar = d[fieldKey + '_sidebar'];
+ sidebar && newarray.push(...DocListCast(sidebar));
+ func(depth, d);
+ });
+ docs = newarray;
+ depth++;
+ }
+ }
export interface IdSearchResult {
ids: string[];
lines: string[][];
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index 4be9448b3..d0f66d124 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -1,11 +1,14 @@
import { action, observable, ObservableMap } from 'mobx';
import { computedFn } from 'mobx-utils';
import { Doc, Opt } from '../../fields/Doc';
-import { DocCast } from '../../fields/Types';
-import { CollectionViewType } from '../documents/DocumentTypes';
+import { List } from '../../fields/List';
+import { listSpec } from '../../fields/Schema';
+import { Cast, DocCast } from '../../fields/Types';
+import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
import { DocumentView } from '../views/nodes/DocumentView';
import { LinkManager } from './LinkManager';
import { ScriptingGlobals } from './ScriptingGlobals';
+import { UndoManager } from './UndoManager';
export namespace SelectionManager {
class Manager {
@@ -23,9 +26,6 @@ export namespace SelectionManager {
// if doc is not in SelectedDocuments, add it
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();
}
@@ -51,6 +51,8 @@ export namespace SelectionManager {
}
@action
DeselectAll(): void {
+ LinkManager.currentLink = undefined;
+ LinkManager.currentLinkAnchor = undefined;
manager.SelectedSchemaDocument = undefined;
Array.from(manager.SelectedViewsMap.keys()).forEach(dv => dv.props.whenChildContentsActiveChanged(false));
manager.SelectedViewsMap.clear();
@@ -124,3 +126,27 @@ ScriptingGlobals.add(function SelectionManager_selectedDocType(type: string, exp
ScriptingGlobals.add(function deselectAll() {
SelectionManager.DeselectAll();
});
+ScriptingGlobals.add(function undo() {
+ SelectionManager.DeselectAll();
+ return UndoManager.Undo();
+});
+
+export function ShowUndoStack() {
+ SelectionManager.DeselectAll();
+ var buffer = '';
+ UndoManager.undoStack.forEach((batch, i) => {
+ buffer += 'Batch => ' + UndoManager.undoStackNames[i] + '\n';
+ ///batch.forEach(undo => (buffer += ' ' + undo.prop + '\n'));
+ });
+ alert(buffer);
+}
+ScriptingGlobals.add(function redo() {
+ SelectionManager.DeselectAll();
+ return UndoManager.Redo();
+});
+ScriptingGlobals.add(function selectedDocs(container: Doc, excludeCollections: boolean, prevValue: any) {
+ const docs = SelectionManager.Views()
+ .map(dv => dv.props.Document)
+ .filter(d => !Doc.AreProtosEqual(d, container) && !d.annotationOn && d.type !== DocumentType.KVP && (!excludeCollections || d.type !== DocumentType.COL || !Cast(d.data, listSpec(Doc), null)));
+ return docs.length ? new List(docs) : prevValue;
+});
diff --git a/src/client/util/ServerStats.tsx b/src/client/util/ServerStats.tsx
index 6a6ec158e..ac9fecd5c 100644
--- a/src/client/util/ServerStats.tsx
+++ b/src/client/util/ServerStats.tsx
@@ -6,6 +6,7 @@ import './SharingManager.scss';
import { PingManager } from './PingManager';
import { StrCast } from '../../fields/Types';
import { Doc } from '../../fields/Doc';
+import { SettingsManager } from './SettingsManager';
@observer
export class ServerStats extends React.Component<{}> {
@@ -42,21 +43,22 @@ export class ServerStats extends React.Component<{}> {
*/
@computed get sharingInterface() {
return (
- <div style={{
- display: "flex",
- height: 300,
- width: 400,
- background: StrCast(Doc.UserDoc().userBackgroundColor),
- opacity: 0.6}}>
- <div style={{width: 300,height: 100,margin: "auto",display: "flex",flexDirection: "column"}}>
- {PingManager.Instance.IsBeating ? 'The server connection is active' :
- 'The server connection has been interrupted.NOTE: Any changes made will appear to persist but will be lost after a browser refreshes.'}
-
- <br/>
+ <div
+ style={{
+ display: 'flex',
+ height: 300,
+ width: 400,
+ background: SettingsManager.userBackgroundColor,
+ opacity: 0.6,
+ }}>
+ <div style={{ width: 300, height: 100, margin: 'auto', display: 'flex', flexDirection: 'column' }}>
+ {PingManager.Instance.IsBeating ? 'The server connection is active' : 'The server connection has been interrupted.NOTE: Any changes made will appear to persist but will be lost after a browser refreshes.'}
+
+ <br />
<span>Active users:{this._stats?.socketMap.length}</span>
{this._stats?.socketMap.map((user: any) => (
<p>{user.username}</p>
- ))}
+ ))}
</div>
</div>
);
diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx
index b8e327968..085ff4bb1 100644
--- a/src/client/util/SettingsManager.tsx
+++ b/src/client/util/SettingsManager.tsx
@@ -1,27 +1,21 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Button, ColorPicker, Dropdown, DropdownType, EditableText, Group, NumberDropdown, Size, Toggle, ToggleType, Type } from 'browndash-components';
import { action, computed, observable, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { ColorState, SketchPicker } from 'react-color';
+import { BsGoogle } from 'react-icons/bs';
+import { FaFillDrip, FaPalette } from 'react-icons/fa';
import { Doc } from '../../fields/Doc';
-import { Id } from '../../fields/FieldSymbols';
+import { DashVersion } from '../../fields/DocSymbols';
import { BoolCast, Cast, StrCast } from '../../fields/Types';
import { addStyleSheet, addStyleSheetRule, Utils } from '../../Utils';
import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager';
import { DocServer } from '../DocServer';
import { Networking } from '../Network';
import { MainViewModal } from '../views/MainViewModal';
-import { FontIconBox } from '../views/nodes/FontIconBox/FontIconBox';
-import { DragManager } from './DragManager';
import { GroupManager } from './GroupManager';
import './SettingsManager.scss';
import { undoBatch } from './UndoManager';
-import { Button, ColorPicker, Dropdown, DropdownType, EditableText, Group, NumberDropdown, Size, Toggle, ToggleType, Type } from 'browndash-components';
-import { BsGoogle } from 'react-icons/bs';
-import { FaFillDrip, FaPalette } from 'react-icons/fa';
-const higflyout = require('@hig/flyout');
-export const { anchorPoints } = higflyout;
-export const Flyout = higflyout.default;
export enum ColorScheme {
Dark = 'Dark',
@@ -53,13 +47,6 @@ export class SettingsManager extends React.Component<{}> {
@observable public static propertiesWidth: number = 0;
@observable public static headerBarHeight: number = 0;
- @computed get backgroundColor() {
- return Doc.UserDoc().activeCollectionBackground;
- }
- @computed get userTheme() {
- return Doc.UserDoc().userTheme;
- }
-
constructor(props: {}) {
super(props);
SettingsManager.Instance = this;
@@ -79,15 +66,15 @@ export class SettingsManager extends React.Component<{}> {
}
};
- @computed get userColor() {
+ @computed public static get userColor() {
return StrCast(Doc.UserDoc().userColor);
}
- @computed get userVariantColor() {
+ @computed public static get userVariantColor() {
return StrCast(Doc.UserDoc().userVariantColor);
}
- @computed get userBackgroundColor() {
+ @computed public static get userBackgroundColor() {
return StrCast(Doc.UserDoc().userBackgroundColor);
}
@@ -99,12 +86,8 @@ export class SettingsManager extends React.Component<{}> {
Doc.UserDoc().userBackgroundColor = color;
addStyleSheetRule(SettingsManager._settingsStyle, 'lm_header', { background: `${color} !important` });
});
- @undoBatch switchUserColor = action((color: string) => {
- Doc.UserDoc().userColor = color;
- });
- @undoBatch switchUserVariantColor = action((color: string) => {
- Doc.UserDoc().userVariantColor = color;
- });
+ @undoBatch switchUserColor = action((color: string) => (Doc.UserDoc().userColor = color));
+ @undoBatch switchUserVariantColor = action((color: string) => (Doc.UserDoc().userVariantColor = color));
@undoBatch playgroundModeToggle = action(() => {
this.playgroundMode = !this.playgroundMode;
if (this.playgroundMode) {
@@ -159,6 +142,7 @@ export class SettingsManager extends React.Component<{}> {
formLabel="Theme"
size={Size.SMALL}
type={Type.TERT}
+ closeOnSelect={false}
selectedVal={userTheme}
setSelectedVal={scheme => this.changeColorScheme(scheme as string)}
items={colorSchemes.map((scheme, i) => ({
@@ -166,14 +150,38 @@ export class SettingsManager extends React.Component<{}> {
val: scheme,
}))}
dropdownType={DropdownType.SELECT}
- color={this.userColor}
+ color={SettingsManager.userColor}
fillWidth
/>
{userTheme === ColorScheme.Custom && (
<Group formLabel="Custom Theme">
- <ColorPicker tooltip={'User Color'} color={this.userColor} type={Type.SEC} icon={<FaFillDrip />} selectedColor={this.userColor} setSelectedColor={this.switchUserColor} />
- <ColorPicker tooltip={'User Background Color'} color={this.userColor} type={Type.SEC} icon={<FaPalette />} selectedColor={this.userBackgroundColor} setSelectedColor={this.switchUserBackgroundColor} />
- <ColorPicker tooltip={'User Variant Color'} color={this.userColor} type={Type.SEC} icon={<FaPalette />} selectedColor={this.userVariantColor} setSelectedColor={this.switchUserVariantColor} />
+ <ColorPicker
+ tooltip={'User Color'} //
+ color={SettingsManager.userColor}
+ type={Type.SEC}
+ icon={<FaFillDrip />}
+ selectedColor={SettingsManager.userColor}
+ setSelectedColor={this.switchUserColor}
+ setFinalColor={this.switchUserColor}
+ />
+ <ColorPicker
+ tooltip={'User Background Color'}
+ color={SettingsManager.userColor}
+ type={Type.SEC}
+ icon={<FaPalette />}
+ selectedColor={SettingsManager.userBackgroundColor}
+ setSelectedColor={this.switchUserBackgroundColor}
+ setFinalColor={this.switchUserBackgroundColor}
+ />
+ <ColorPicker
+ tooltip={'User Variant Color'}
+ color={SettingsManager.userColor}
+ type={Type.SEC}
+ icon={<FaPalette />}
+ selectedColor={SettingsManager.userVariantColor}
+ setSelectedColor={this.switchUserVariantColor}
+ setFinalColor={this.switchUserVariantColor}
+ />
</Group>
)}
</div>
@@ -190,7 +198,7 @@ export class SettingsManager extends React.Component<{}> {
onClick={e => (Doc.UserDoc().layout_showTitle = Doc.UserDoc().layout_showTitle ? undefined : 'author_date')}
toggleStatus={Doc.UserDoc().layout_showTitle !== undefined}
size={Size.XSMALL}
- color={this.userColor}
+ color={SettingsManager.userColor}
/>
<Toggle
formLabel={'Show Full Toolbar'}
@@ -199,25 +207,25 @@ export class SettingsManager extends React.Component<{}> {
onClick={e => (Doc.UserDoc()['documentLinksButton-fullMenu'] = !Doc.UserDoc()['documentLinksButton-fullMenu'])}
toggleStatus={BoolCast(Doc.UserDoc()['documentLinksButton-fullMenu'])}
size={Size.XSMALL}
- color={this.userColor}
+ color={SettingsManager.userColor}
/>
<Toggle
formLabel={'Show Button Labels'}
formLabelPlacement={'right'}
toggleType={ToggleType.SWITCH}
- onClick={e => FontIconBox.SetShowLabels(!FontIconBox.GetShowLabels())}
- toggleStatus={FontIconBox.GetShowLabels()}
+ onClick={e => Doc.SetShowIconLabels(!Doc.GetShowIconLabels())}
+ toggleStatus={Doc.GetShowIconLabels()}
size={Size.XSMALL}
- color={this.userColor}
+ color={SettingsManager.userColor}
/>
<Toggle
formLabel={'Recognize Ink Gestures'}
formLabelPlacement={'right'}
toggleType={ToggleType.SWITCH}
- onClick={e => FontIconBox.SetRecognizeGestures(!FontIconBox.GetRecognizeGestures())}
- toggleStatus={FontIconBox.GetRecognizeGestures()}
+ onClick={e => Doc.SetRecognizeGestures(!Doc.GetRecognizeGestures())}
+ toggleStatus={Doc.GetRecognizeGestures()}
size={Size.XSMALL}
- color={this.userColor}
+ color={SettingsManager.userColor}
/>
<Toggle
formLabel={'Hide Labels In Ink Shapes'}
@@ -226,7 +234,7 @@ export class SettingsManager extends React.Component<{}> {
onClick={e => (Doc.UserDoc().activeInkHideTextLabels = !Doc.UserDoc().activeInkHideTextLabels)}
toggleStatus={BoolCast(Doc.UserDoc().activeInkHideTextLabels)}
size={Size.XSMALL}
- color={this.userColor}
+ color={SettingsManager.userColor}
/>
<Toggle
formLabel={'Open Ink Docs in Lightbox'}
@@ -235,7 +243,16 @@ export class SettingsManager extends React.Component<{}> {
onClick={e => (Doc.UserDoc().openInkInLightbox = !Doc.UserDoc().openInkInLightbox)}
toggleStatus={BoolCast(Doc.UserDoc().openInkInLightbox)}
size={Size.XSMALL}
- color={this.userColor}
+ color={SettingsManager.userColor}
+ />
+ <Toggle
+ formLabel={'Show Link Lines'}
+ formLabelPlacement={'right'}
+ toggleType={ToggleType.SWITCH}
+ onClick={e => (Doc.UserDoc().showLinkLines = !Doc.UserDoc().showLinkLines)}
+ toggleStatus={BoolCast(Doc.UserDoc().showLinkLines)}
+ size={Size.XSMALL}
+ color={SettingsManager.userColor}
/>
</div>
);
@@ -267,7 +284,7 @@ export class SettingsManager extends React.Component<{}> {
<div className="tab-column-content">
{/* <NumberInput/> */}
<Group formLabel={'Default Font'}>
- <NumberDropdown color={this.userColor} numberDropdownType={'input'} min={0} max={50} step={2} type={Type.TERT} number={0} unit={'px'} setNumber={() => {}} />
+ <NumberDropdown color={SettingsManager.userColor} numberDropdownType={'input'} min={0} max={50} step={2} type={Type.TERT} number={0} unit={'px'} setNumber={() => {}} />
<Dropdown
items={fontFamilies.map(val => {
return {
@@ -278,13 +295,14 @@ export class SettingsManager extends React.Component<{}> {
},
};
})}
+ closeOnSelect={true}
dropdownType={DropdownType.SELECT}
type={Type.TERT}
selectedVal={StrCast(Doc.UserDoc().fontFamily)}
setSelectedVal={val => {
this.changeFontFamily(val as string);
}}
- color={this.userColor}
+ color={SettingsManager.userColor}
fillWidth
/>
</Group>
@@ -312,12 +330,12 @@ export class SettingsManager extends React.Component<{}> {
@computed get passwordContent() {
return (
<div className="password-content">
- <EditableText placeholder="Current password" type={Type.SEC} color={this.userColor} val={''} setVal={val => this.changeVal(val as string, 'curr')} fillWidth password />
- <EditableText placeholder="New password" type={Type.SEC} color={this.userColor} val={''} setVal={val => this.changeVal(val as string, 'new')} fillWidth password />
- <EditableText placeholder="Confirm new password" type={Type.SEC} color={this.userColor} val={''} setVal={val => this.changeVal(val as string, 'conf')} fillWidth password />
+ <EditableText placeholder="Current password" type={Type.SEC} color={SettingsManager.userColor} val={''} setVal={val => this.changeVal(val as string, 'curr')} fillWidth password />
+ <EditableText placeholder="New password" type={Type.SEC} color={SettingsManager.userColor} val={''} setVal={val => this.changeVal(val as string, 'new')} fillWidth password />
+ <EditableText placeholder="Confirm new password" type={Type.SEC} color={SettingsManager.userColor} val={''} setVal={val => this.changeVal(val as string, 'conf')} fillWidth password />
{!this.passwordResultText ? null : <div className={`${this.passwordResultText.startsWith('Error') ? 'error' : 'success'}-text`}>{this.passwordResultText}</div>}
- <Button type={Type.SEC} text={'Forgot Password'} color={this.userColor} />
- <Button type={Type.TERT} text={'Submit'} onClick={this.changePassword} color={this.userColor} />
+ <Button type={Type.SEC} text={'Forgot Password'} color={SettingsManager.userColor} />
+ <Button type={Type.TERT} text={'Submit'} onClick={this.changePassword} color={SettingsManager.userColor} />
</div>
);
}
@@ -357,6 +375,7 @@ export class SettingsManager extends React.Component<{}> {
<div className="tab-column-content">
<Dropdown
formLabel={'Mode'}
+ closeOnSelect={true}
items={[
{
text: 'Novice',
@@ -377,17 +396,18 @@ export class SettingsManager extends React.Component<{}> {
dropdownType={DropdownType.SELECT}
type={Type.TERT}
placement="bottom-start"
- color={this.userColor}
+ color={SettingsManager.userColor}
fillWidth
/>
- <Toggle formLabel={'Playground Mode'} toggleType={ToggleType.SWITCH} toggleStatus={this.playgroundMode} onClick={this.playgroundModeToggle} color={this.userColor} />
+ <Toggle formLabel={'Playground Mode'} toggleType={ToggleType.SWITCH} toggleStatus={this.playgroundMode} onClick={this.playgroundModeToggle} color={SettingsManager.userColor} />
</div>
<div className="tab-column-title" style={{ marginTop: 20, marginBottom: 10 }}>
Freeform Navigation
</div>
<div className="tab-column-content">
<Dropdown
- formLabel={'Scroll Mode'}
+ formLabel="Scroll Mode"
+ closeOnSelect={true}
items={[
{
text: 'Scroll to Pan',
@@ -405,15 +425,22 @@ export class SettingsManager extends React.Component<{}> {
dropdownType={DropdownType.SELECT}
type={Type.TERT}
placement="bottom-start"
- color={this.userColor}
+ color={SettingsManager.userColor}
/>
</div>
</div>
<div className="tab-column">
<div className="tab-column-title">Permissions</div>
<div className="tab-column-content">
- <Button text={'Manage Groups'} type={Type.TERT} onClick={() => GroupManager.Instance?.open()} color={this.userColor} />
- <Toggle toggleType={ToggleType.SWITCH} formLabel={'Default access private'} color={this.userColor} toggleStatus={BoolCast(Doc.defaultAclPrivate)} onClick={action(() => (Doc.defaultAclPrivate = !Doc.defaultAclPrivate))} />
+ <Button text={'Manage Groups'} type={Type.TERT} onClick={() => GroupManager.Instance?.open()} color={SettingsManager.userColor} />
+ <Toggle
+ toggleType={ToggleType.SWITCH}
+ formLabel={'Default access private'}
+ color={SettingsManager.userColor}
+ toggleStatus={BoolCast(Doc.defaultAclPrivate)}
+ onClick={action(() => (Doc.defaultAclPrivate = !Doc.defaultAclPrivate))}
+ />
+ <Toggle toggleType={ToggleType.SWITCH} formLabel={'Enable Sharing UI'} color={SettingsManager.userColor} toggleStatus={BoolCast(Doc.IsSharingEnabled)} onClick={action(() => (Doc.IsSharingEnabled = !Doc.IsSharingEnabled))} />
</div>
</div>
</div>
@@ -432,7 +459,7 @@ export class SettingsManager extends React.Component<{}> {
];
return (
<div className="settings-interface">
- <div className="settings-panel" style={{ background: this.userColor }}>
+ <div className="settings-panel" style={{ background: SettingsManager.userColor }}>
<div className="settings-tabs">
{tabs.map(tab => {
const isActive = this.activeTab === tab.title;
@@ -440,8 +467,8 @@ export class SettingsManager extends React.Component<{}> {
<div
key={tab.title}
style={{
- background: isActive ? this.userBackgroundColor : this.userColor,
- color: isActive ? this.userColor : this.userBackgroundColor,
+ background: isActive ? SettingsManager.userBackgroundColor : SettingsManager.userColor,
+ color: isActive ? SettingsManager.userColor : SettingsManager.userBackgroundColor,
}}
className={'tab-control ' + (isActive ? 'active' : 'inactive')}
onClick={action(() => (this.activeTab = tab.title))}>
@@ -452,18 +479,19 @@ export class SettingsManager extends React.Component<{}> {
</div>
<div className="settings-user">
- <div className="settings-username" style={{ color: this.userBackgroundColor }}>
+ <div style={{ color: SettingsManager.userBackgroundColor }}>{DashVersion}</div>
+ <div className="settings-username" style={{ color: SettingsManager.userBackgroundColor }}>
{Doc.CurrentUserEmail}
</div>
- <Button text={Doc.GuestDashboard ? 'Exit' : 'Log Out'} type={Type.TERT} color={this.userVariantColor} onClick={() => window.location.assign(Utils.prepend('/logout'))} />
+ <Button text={Doc.GuestDashboard ? 'Exit' : 'Log Out'} type={Type.TERT} color={SettingsManager.userVariantColor} onClick={() => window.location.assign(Utils.prepend('/logout'))} />
</div>
</div>
<div className="close-button">
- <Button icon={<FontAwesomeIcon icon={'times'} size={'lg'} />} onClick={this.close} color={this.userColor} />
+ <Button icon={<FontAwesomeIcon icon={'times'} size={'lg'} />} onClick={this.close} color={SettingsManager.userColor} />
</div>
- <div className="settings-content" style={{ color: this.userColor, background: this.userBackgroundColor }}>
+ <div className="settings-content" style={{ color: SettingsManager.userColor, background: SettingsManager.userBackgroundColor }}>
{tabs.map(tab => (
<div key={tab.title} className={'tab-section ' + (this.activeTab === tab.title ? 'active' : 'inactive')}>
{tab.ele}
diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx
index cadcb1f8a..8d59426ec 100644
--- a/src/client/util/SharingManager.tsx
+++ b/src/client/util/SharingManager.tsx
@@ -8,6 +8,7 @@ import Select from 'react-select';
import * as RequestPromise from 'request-promise';
import { Doc, DocListCast, DocListCastAsync, HierarchyMapping, ReverseHierarchyMap } from '../../fields/Doc';
import { AclAdmin, AclPrivate, DocAcl, DocData } from '../../fields/DocSymbols';
+import { FieldLoader } from '../../fields/FieldLoader';
import { Id } from '../../fields/FieldSymbols';
import { StrCast } from '../../fields/Types';
import { distributeAcls, GetEffectiveAcl, normalizeEmail, SharingPermissions, TraceMobx } from '../../fields/util';
@@ -17,12 +18,12 @@ import { DictationOverlay } from '../views/DictationOverlay';
import { MainViewModal } from '../views/MainViewModal';
import { DocumentView } from '../views/nodes/DocumentView';
import { TaskCompletionBox } from '../views/nodes/TaskCompletedBox';
-import { SearchBox } from '../views/search/SearchBox';
import { DocumentManager } from './DocumentManager';
import { GroupManager, UserOptions } from './GroupManager';
import { GroupMemberView } from './GroupMemberView';
-import { LinkManager } from './LinkManager';
+import { SearchUtil } from './SearchUtil';
import { SelectionManager } from './SelectionManager';
+import { SettingsManager } from './SettingsManager';
import './SharingManager.scss';
import { undoable } from './UndoManager';
@@ -136,6 +137,7 @@ export class SharingManager extends React.Component<{}> {
this.populating = true;
const userList = await RequestPromise.get(Utils.prepend('/getUsers'));
const raw = (JSON.parse(userList) as User[]).filter(user => user.email !== 'guest' && user.email !== Doc.CurrentUserEmail);
+ runInAction(() => (FieldLoader.ServerLoadStatus.message = 'users'));
const docs = await DocServer.GetRefFields(raw.reduce((list, user) => [...list, user.sharingDocumentId, user.linkDatabaseId], [] as string[]));
raw.map(
action((newUser: User) => {
@@ -144,7 +146,7 @@ export class SharingManager extends React.Component<{}> {
if (sharingDoc instanceof Doc && linkDatabase instanceof Doc) {
if (!this.users.find(user => user.user.email === newUser.email)) {
this.users.push({ user: newUser, sharingDoc, linkDatabase, userColor: StrCast(sharingDoc.userColor) });
- LinkManager.addLinkDB(linkDatabase);
+ //LinkManager.addLinkDB(linkDatabase);
}
}
})
@@ -376,15 +378,17 @@ export class SharingManager extends React.Component<{}> {
}
});
- const { left, width, top, height } = this.shareDocumentButtonRef.current!.getBoundingClientRect();
- TaskCompletionBox.popupX = left - 1.5 * width;
- TaskCompletionBox.popupY = top - 1.5 * height;
- TaskCompletionBox.textDisplayed = 'Document shared!';
- TaskCompletionBox.taskCompleted = true;
- setTimeout(
- action(() => (TaskCompletionBox.taskCompleted = false)),
- 2000
- );
+ if (this.shareDocumentButtonRef.current) {
+ const { left, width, top, height } = this.shareDocumentButtonRef.current.getBoundingClientRect();
+ TaskCompletionBox.popupX = left - 1.5 * width;
+ TaskCompletionBox.popupY = top - 1.5 * height;
+ TaskCompletionBox.textDisplayed = 'Document shared!';
+ TaskCompletionBox.taskCompleted = true;
+ setTimeout(
+ action(() => (TaskCompletionBox.taskCompleted = false)),
+ 2000
+ );
+ }
this.layoutDocAcls = false;
this.selectedUsers = null;
@@ -444,7 +448,7 @@ export class SharingManager extends React.Component<{}> {
if (this.myDocAcls) {
const newDocs: Doc[] = [];
- SearchBox.foreachRecursiveDoc(docs, (depth, doc) => newDocs.push(doc));
+ SearchUtil.foreachRecursiveDoc(docs, (depth, doc) => newDocs.push(doc));
docs = newDocs.filter(doc => GetEffectiveAcl(doc) === AclAdmin);
}
@@ -525,10 +529,10 @@ export class SharingManager extends React.Component<{}> {
const permissions = uniform ? StrCast(targetDoc?.[groupKey]) : '-multiple-';
return !permissions ? null : (
- <div key={groupKey} className={'container'} style={{ background: StrCast(Doc.UserDoc().userBackgroundColor), color: StrCast(Doc.UserDoc().userColor) }}>
+ <div key={groupKey} className={'container'} style={{ background: SettingsManager.userBackgroundColor, color: SettingsManager.userColor }}>
<div className={'padding'}>{StrCast(group.title)}</div>
&nbsp;
- {group instanceof Doc ? <IconButton icon={<FontAwesomeIcon icon={'info-circle'} />} size={Size.XSMALL} color={StrCast(Doc.UserDoc().userColor)} onClick={action(() => (GroupManager.Instance.currentGroup = group))} /> : null}
+ {group instanceof Doc ? <IconButton icon={<FontAwesomeIcon icon={'info-circle'} />} size={Size.XSMALL} color={SettingsManager.userColor} onClick={action(() => (GroupManager.Instance.currentGroup = group))} /> : null}
<div className={'edit-actions'}>
{admin || this.myDocAcls ? (
<select className={`permissions-dropdown-${permissions}`} value={permissions} onChange={e => this.setInternalGroupSharing(group, e.currentTarget.value)}>
@@ -550,10 +554,10 @@ export class SharingManager extends React.Component<{}> {
<div
className="sharing-contents"
style={{
- background: StrCast(Doc.UserDoc().userBackgroundColor),
+ background: SettingsManager.userBackgroundColor,
color: StrCast(Doc.UserDoc().userColor),
}}>
- <p className="share-title" style={{ color: StrCast(Doc.UserDoc().userColor) }}>
+ <p className="share-title" style={{ color: SettingsManager.userColor }}>
<div className="share-info" onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/features/collaboration/', '_blank')}>
<FontAwesomeIcon icon={'question-circle'} size={'sm'} onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/features/collaboration/', '_blank')} />
</div>
@@ -561,10 +565,10 @@ export class SharingManager extends React.Component<{}> {
{this.focusOn(docs.length < 2 ? StrCast(targetDoc?.title, 'this document') : '-multiple-')}
</p>
<div className="share-copy-link">
- <Button type={Type.TERT} color={StrCast(Doc.UserDoc().userColor)} icon={<FontAwesomeIcon icon={'copy'} size="sm" />} iconPlacement={'left'} text={'Copy Guest URL'} onClick={this.copyURL} />
+ <Button type={Type.TERT} color={SettingsManager.userColor} icon={<FontAwesomeIcon icon={'copy'} size="sm" />} iconPlacement={'left'} text={'Copy Guest URL'} onClick={this.copyURL} />
</div>
<div className="close-button">
- <Button icon={<FontAwesomeIcon icon={'times'} size={'lg'} />} onClick={this.close} color={StrCast(Doc.UserDoc().userColor)} />
+ <Button icon={<FontAwesomeIcon icon={'times'} size={'lg'} />} onClick={this.close} color={SettingsManager.userColor} />
</div>
{admin ? (
<div className="share-container">
@@ -606,7 +610,7 @@ export class SharingManager extends React.Component<{}> {
</select>
</div>
<div className="share-button">
- <Button text={'SHARE'} type={Type.TERT} color={StrCast(Doc.UserDoc().userColor)} onClick={this.share} />
+ <Button text={'SHARE'} type={Type.TERT} color={SettingsManager.userColor} onClick={this.share} />
</div>
</div>
<div className="sort-checkboxes">
diff --git a/src/client/util/reportManager/ReportManager.tsx b/src/client/util/reportManager/ReportManager.tsx
index e6de410a7..802a2f09f 100644
--- a/src/client/util/reportManager/ReportManager.tsx
+++ b/src/client/util/reportManager/ReportManager.tsx
@@ -19,9 +19,7 @@ import { BugType, FileData, Priority, ReportForm, ViewState, bugDropdownItems, d
import { Filter, FormInput, FormTextArea, IssueCard, IssueView, Tag } from './ReportManagerComponents';
import { StrCast } from '../../../fields/Types';
import { MdRefresh } from 'react-icons/md';
-const higflyout = require('@hig/flyout');
-export const { anchorPoints } = higflyout;
-export const Flyout = higflyout.default;
+import { SettingsManager } from '../SettingsManager';
/**
* Class for reporting and viewing Github issues within the app.
@@ -214,11 +212,11 @@ export class ReportManager extends React.Component<{}> {
* @returns the component that dispays all issues
*/
private viewIssuesComponent = () => {
- const darkMode = isDarkMode(StrCast(Doc.UserDoc().userBackgroundColor));
+ const darkMode = isDarkMode(SettingsManager.userBackgroundColor);
const colors = darkMode ? darkColors : lightColors;
return (
- <div className="view-issues" style={{ backgroundColor: StrCast(Doc.UserDoc().userBackgroundColor), color: colors.text }}>
+ <div className="view-issues" style={{ backgroundColor: SettingsManager.userBackgroundColor, color: colors.text }}>
<div className="left" style={{ display: this.rightExpanded ? 'none' : 'flex' }}>
<div className="report-header">
<h2 style={{ color: colors.text }}>Open Issues</h2>
@@ -311,6 +309,7 @@ export class ReportManager extends React.Component<{}> {
<Dropdown
color={StrCast(Doc.UserDoc().userColor)}
formLabel={'Type'}
+ closeOnSelect={true}
items={bugDropdownItems}
selectedVal={this.formData.type}
setSelectedVal={val => {
@@ -323,6 +322,7 @@ export class ReportManager extends React.Component<{}> {
<Dropdown
color={StrCast(Doc.UserDoc().userColor)}
formLabel={'Priority'}
+ closeOnSelect={true}
items={priorityDropdownItems}
selectedVal={this.formData.priority}
setSelectedVal={val => {
diff --git a/src/client/views/AntimodeMenu.tsx b/src/client/views/AntimodeMenu.tsx
index c41ea7053..16e76694d 100644
--- a/src/client/views/AntimodeMenu.tsx
+++ b/src/client/views/AntimodeMenu.tsx
@@ -3,6 +3,7 @@ import { observable, action, runInAction } from 'mobx';
import './AntimodeMenu.scss';
import { StrCast } from '../../fields/Types';
import { Doc } from '../../fields/Doc';
+import { SettingsManager } from '../util/SettingsManager';
export interface AntimodeMenuProps {}
/**
@@ -150,7 +151,7 @@ export abstract class AntimodeMenu<T extends AntimodeMenuProps> extends React.Co
left: this._left,
top: this._top,
opacity: this._opacity,
- background: StrCast(Doc.UserDoc().userBackgroundColor),
+ background: SettingsManager.userBackgroundColor,
transitionProperty: this._transitionProperty,
transitionDuration: this._transitionDuration,
transitionDelay: this._transitionDelay,
@@ -176,7 +177,7 @@ export abstract class AntimodeMenu<T extends AntimodeMenuProps> extends React.Co
height: 'inherit',
width: 200,
opacity: this._opacity,
- background: StrCast(Doc.UserDoc().userBackgroundColor),
+ background: SettingsManager.userBackgroundColor,
transitionProperty: this._transitionProperty,
transitionDuration: this._transitionDuration,
transitionDelay: this._transitionDelay,
@@ -199,7 +200,7 @@ export abstract class AntimodeMenu<T extends AntimodeMenuProps> extends React.Co
left: this._left,
top: this._top,
opacity: this._opacity,
- background: StrCast(Doc.UserDoc().userBackgroundColor),
+ background: SettingsManager.userBackgroundColor,
transitionProperty: this._transitionProperty,
transitionDuration: this._transitionDuration,
transitionDelay: this._transitionDelay,
diff --git a/src/client/views/AudioWaveform.tsx b/src/client/views/AudioWaveform.tsx
index 14c922526..c779ce8c4 100644
--- a/src/client/views/AudioWaveform.tsx
+++ b/src/client/views/AudioWaveform.tsx
@@ -3,7 +3,7 @@ import axios from 'axios';
import { action, computed, IReactionDisposer, reaction } from 'mobx';
import { observer } from 'mobx-react';
import Waveform from 'react-audio-waveform';
-import { Doc } from '../../fields/Doc';
+import { Doc, NumListCast } from '../../fields/Doc';
import { List } from '../../fields/List';
import { listSpec } from '../../fields/Schema';
import { Cast } from '../../fields/Types';
@@ -54,7 +54,7 @@ export class AudioWaveform extends React.Component<AudioWaveformProps> {
}
@computed get audioBuckets() {
- return Cast(this.props.layoutDoc[this.audioBucketField(this.clipStart, this.clipEnd, this.zoomFactor)], listSpec('number'), []);
+ return NumListCast(this.props.layoutDoc[this.audioBucketField(this.clipStart, this.clipEnd, this.zoomFactor)]);
}
audioBucketField = (start: number, end: number, zoomFactor: number) => this.props.fieldKey + '_audioBuckets/' + '/' + start.toFixed(2).replace('.', '_') + '/' + end.toFixed(2).replace('.', '_') + '/' + zoomFactor * 10;
@@ -102,7 +102,7 @@ export class AudioWaveform extends React.Component<AudioWaveformProps> {
render() {
return (
<div className="audioWaveform">
- <Waveform color={Colors.MEDIUM_BLUE_ALT} height={this.waveHeight} barWidth={200 / this.audioBuckets.length} pos={this.props.duration} duration={this.props.duration} peaks={this.audioBuckets} progressColor={Colors.MEDIUM_BLUE_ALT} />
+ <Waveform color={Colors.MEDIUM_BLUE_ALT} height={this.waveHeight} barWidth={200 / this.audioBuckets.length} pos={this.props.duration} duration={this.props.duration} peaks={Array.from(this.audioBuckets)} progressColor={Colors.MEDIUM_BLUE_ALT} />
</div>
);
}
diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx
index 8412a9aae..adefc7e9c 100644
--- a/src/client/views/ContextMenu.tsx
+++ b/src/client/views/ContextMenu.tsx
@@ -7,6 +7,7 @@ import { ContextMenuItem, ContextMenuProps, OriginalMenuProps } from './ContextM
import { Utils } from '../../Utils';
import { StrCast } from '../../fields/Types';
import { Doc } from '../../fields/Doc';
+import { SettingsManager } from '../util/SettingsManager';
@observer
export class ContextMenu extends React.Component {
@@ -195,7 +196,7 @@ export class ContextMenu extends React.Component {
<div
className="contextMenu-group"
style={{
- background: StrCast(Doc.UserDoc().userVariantColor),
+ background: StrCast(SettingsManager.userVariantColor),
}}>
<div className="contextMenu-description">{value.join(' -> ')}</div>
</div>
@@ -222,8 +223,8 @@ export class ContextMenu extends React.Component {
style={{
left: this.pageX,
...(this._yRelativeToTop ? { top: this.pageY } : { bottom: this.pageY }),
- background: StrCast(Doc.UserDoc().userBackgroundColor),
- color: StrCast(Doc.UserDoc().userColor),
+ background: SettingsManager.userBackgroundColor,
+ color: SettingsManager.userColor,
}}>
{!this.itemsNeedSearch ? null : (
<span className={'search-icon'}>
diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx
index daa2c152a..c2cbca3e1 100644
--- a/src/client/views/ContextMenuItem.tsx
+++ b/src/client/views/ContextMenuItem.tsx
@@ -6,12 +6,13 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { UndoManager } from '../util/UndoManager';
import { Doc } from '../../fields/Doc';
import { StrCast } from '../../fields/Types';
+import { SettingsManager } from '../util/SettingsManager';
export interface OriginalMenuProps {
description: string;
event: (stuff?: any) => void;
undoable?: boolean;
- icon: IconProp; //maybe should be optional (icon?)
+ icon: IconProp | JSX.Element; //maybe should be optional (icon?)
closeMenu?: () => void;
}
@@ -82,19 +83,20 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select
);
};
+ isJSXElement(val: any): val is JSX.Element {
+ return React.isValidElement(val);
+ }
+
render() {
if ('event' in this.props) {
return (
<div className={'contextMenu-item' + (this.props.selected ? ' contextMenu-itemSelected' : '')} onPointerDown={this.handleEvent}>
- {this.props.icon ? (
- <span className="contextMenu-item-icon-background">
- <FontAwesomeIcon icon={this.props.icon} size="sm" />
- </span>
- ) : null}
+ {this.props.icon ? <span className="contextMenu-item-icon-background">{this.isJSXElement(this.props.icon) ? this.props.icon : <FontAwesomeIcon icon={this.props.icon} size="sm" />}</span> : null}
<div className="contextMenu-description">{this.props.description.replace(':', '')}</div>
- <div className={`contextMenu-item-background`}
+ <div
+ className={`contextMenu-item-background`}
style={{
- background: StrCast(Doc.UserDoc().userColor)
+ background: SettingsManager.userColor,
}}
/>
</div>
@@ -110,7 +112,7 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select
style={{
marginLeft: window.innerHeight - this._overPosX - 50 > 0 ? '90%' : '20%',
marginTop,
- background: StrCast(Doc.UserDoc().userBackgroundColor)
+ background: SettingsManager.userBackgroundColor,
}}>
{this._items.map(prop => (
<ContextMenuItem {...prop} key={prop.description} closeMenu={this.props.closeMenu} />
@@ -141,9 +143,10 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select
{this.props.description}
<FontAwesomeIcon icon={'angle-right'} size="lg" style={{ position: 'absolute', right: '10px' }} />
</div>
- <div className={`contextMenu-item-background`}
+ <div
+ className={`contextMenu-item-background`}
style={{
- background: StrCast(Doc.UserDoc().userColor)
+ background: SettingsManager.userColor,
}}
/>
{submenu}
diff --git a/src/client/views/DashboardView.tsx b/src/client/views/DashboardView.tsx
index ae55c8ebf..014a6358f 100644
--- a/src/client/views/DashboardView.tsx
+++ b/src/client/views/DashboardView.tsx
@@ -1,10 +1,10 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Button, ColorPicker, EditableText, FontSize, IconButton, Size, Type } from 'browndash-components';
+import { Button, ColorPicker, EditableText, Size, Type } from 'browndash-components';
import { action, computed, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { FaPlus } from 'react-icons/fa';
-import { Doc, DocListCast, DocListCastAsync } from '../../fields/Doc';
+import { Doc, DocListCast } from '../../fields/Doc';
import { AclPrivate, DocAcl } from '../../fields/DocSymbols';
import { Id } from '../../fields/FieldSymbols';
import { List } from '../../fields/List';
@@ -14,11 +14,11 @@ import { ScriptField } from '../../fields/ScriptField';
import { Cast, ImageCast, StrCast } from '../../fields/Types';
import { DocServer } from '../DocServer';
import { Docs, DocumentOptions, DocUtils } from '../documents/Documents';
-import { CollectionViewType } from '../documents/DocumentTypes';
import { HistoryUtil } from '../util/History';
import { ScriptingGlobals } from '../util/ScriptingGlobals';
+import { SettingsManager } from '../util/SettingsManager';
import { SharingManager } from '../util/SharingManager';
-import { undoBatch } from '../util/UndoManager';
+import { undoable, undoBatch, UndoManager } from '../util/UndoManager';
import { CollectionDockingView } from './collections/CollectionDockingView';
import { CollectionView } from './collections/CollectionView';
import { ContextMenu } from './ContextMenu';
@@ -36,28 +36,16 @@ enum DashboardGroup {
@observer
export class DashboardView extends React.Component {
- //TODO: delete dashboard, share dashboard, etc.
-
public static _urlState: HistoryUtil.DocUrl;
+ @observable private openModal = false;
@observable private selectedDashboardGroup = DashboardGroup.MyDashboards;
-
- @observable private newDashboardName: string | undefined = undefined;
- @observable private newDashboardColor: string | undefined = "#AFAFAF";
- @action abortCreateNewDashboard = () => {
- this.newDashboardName = undefined;
- };
- @action setNewDashboardName(name: string) {
- this.newDashboardName = name;
- }
- @action setNewDashboardColor(color: string) {
- this.newDashboardColor = color;
- }
-
- @action
- selectDashboardGroup = (group: DashboardGroup) => {
- this.selectedDashboardGroup = group;
- };
+ @observable private newDashboardName = '';
+ @observable private newDashboardColor = '#AFAFAF';
+ @action abortCreateNewDashboard = () => (this.openModal = false);
+ @action setNewDashboardName = (name: string) => (this.newDashboardName = name);
+ @action setNewDashboardColor = (color: string) => (this.newDashboardColor = color);
+ @action selectDashboardGroup = (group: DashboardGroup) => (this.selectedDashboardGroup = group);
clickDashboard = (e: React.MouseEvent, dashboard: Doc) => {
if (this.selectedDashboardGroup === DashboardGroup.SharedDashboards) {
@@ -69,194 +57,152 @@ export class DashboardView extends React.Component {
};
getDashboards = (whichGroup: DashboardGroup) => {
- const allDashboards = DocListCast(Doc.MyDashboards.data);
if (whichGroup === DashboardGroup.MyDashboards) {
- return allDashboards.filter(dashboard => Doc.GetProto(dashboard).author === Doc.CurrentUserEmail);
+ return DocListCast(Doc.MyDashboards.data).filter(dashboard => Doc.GetProto(dashboard).author === Doc.CurrentUserEmail);
}
- const sharedDashboards = DocListCast(Doc.MySharedDocs.data_dashboards).filter(doc => doc.dockingConfig);
- return sharedDashboards;
- };
-
- isUnviewedSharedDashboard = (dashboard: Doc): boolean => {
- // const sharedDashboards = DocListCast(Doc.MySharedDocs.data_dashboards).filter(doc => doc._type_collection === CollectionViewType.Docking);
- return !DocListCast(Doc.MySharedDocs.viewed).includes(dashboard);
+ return DocListCast(Doc.MySharedDocs.data_dashboards).filter(doc => doc.dockingConfig);
};
- getSharedDashboards = () => {
- const sharedDashs = DocListCast(Doc.MySharedDocs.data_dashboards).filter(doc => doc._type_collection === CollectionViewType.Docking);
- return sharedDashs.filter(dashboard => !DocListCast(Doc.MySharedDocs.viewed).includes(dashboard));
- };
+ isUnviewedSharedDashboard = (dashboard: Doc) => !DocListCast(Doc.MySharedDocs.viewed).includes(dashboard);
@undoBatch
- createNewDashboard = async (name: string, background?: string) => {
- setTimeout(() => {
- this.abortCreateNewDashboard();
- }, 100);
+ createNewDashboard = (name: string, background?: string) => {
DashboardView.createNewDashboard(undefined, name, background);
+ this.abortCreateNewDashboard();
};
@computed
get namingInterface() {
- const dashboardCount = DocListCast(Doc.MyDashboards.data).length + 1;
- const placeholder = `Dashboard ${dashboardCount}`;
return (
- <div className="new-dashboard"
- style={{
- background: StrCast(Doc.UserDoc().userBackgroundColor),
- color: StrCast(Doc.UserDoc().userColor)
- }}
- >
+ <div
+ className="new-dashboard"
+ style={{
+ background: SettingsManager.userBackgroundColor,
+ color: SettingsManager.userColor,
+ }}>
<div className="header">Create New Dashboard</div>
- <EditableText
- formLabel='Title'
- placeholder={placeholder}
- type={Type.SEC}
- color={StrCast(Doc.UserDoc().userColor)}
- setVal={val => this.setNewDashboardName(val as string)}
- fillWidth
- />
+ <EditableText formLabel="Title" placeholder={this.newDashboardName} type={Type.SEC} color={SettingsManager.userColor} setVal={val => this.setNewDashboardName(val as string)} fillWidth />
<ColorPicker
- formLabel='Background'
- colorPickerType='github'
+ formLabel="Background" //
+ colorPickerType="github"
type={Type.TERT}
selectedColor={this.newDashboardColor}
- setSelectedColor={color => {
- this.setNewDashboardColor(color);
- }}
+ setFinalColor={this.setNewDashboardColor}
+ setSelectedColor={this.setNewDashboardColor}
/>
<div className="button-bar">
- <Button text="Cancel" color={StrCast(Doc.UserDoc().userColor)} onClick={this.abortCreateNewDashboard} />
- <Button type={Type.TERT} text="Create" color={StrCast(Doc.UserDoc().userVariantColor)} onClick={() => this.createNewDashboard(this.newDashboardName!, this.newDashboardColor)} />
+ <Button text="Cancel" color={SettingsManager.userColor} onClick={this.abortCreateNewDashboard} />
+ <Button text="Create" color={SettingsManager.userVariantColor} type={Type.TERT} onClick={() => this.createNewDashboard(this.newDashboardName, this.newDashboardColor)} />
</div>
</div>
);
}
+ @action
+ openNewDashboardModal = () => {
+ this.openModal = true;
+ this.setNewDashboardName(`Dashboard ${DocListCast(Doc.MyDashboards.data).length + 1}`);
+ };
_downX: number = 0;
_downY: number = 0;
- @action
- onContextMenu = (dashboard: Doc, e?: React.MouseEvent, pageX?: number, pageY?: number) => {
+ onContextMenu = (dashboard: Doc, e: React.MouseEvent) => {
// the touch onContextMenu is button 0, the pointer onContextMenu is button 2
- if (e) {
+ if (navigator.userAgent.includes('Mozilla') || (Math.abs(this._downX - e.clientX) < 3 && Math.abs(this._downY - e.clientY) < 3)) {
e.preventDefault();
e.stopPropagation();
- e.persist();
- if (!navigator.userAgent.includes('Mozilla') && (Math.abs(this._downX - e?.clientX) > 3 || Math.abs(this._downY - e?.clientY) > 3)) {
- return;
- }
- const cm = ContextMenu.Instance;
- cm.addItem({
- description: 'Share Dashboard',
- event: async () => {
- SharingManager.Instance.open(undefined, dashboard);
- },
+ ContextMenu.Instance.addItem({
+ description: `Share Dashboard`,
+ event: () => SharingManager.Instance.open(undefined, dashboard),
icon: 'edit',
});
- cm.addItem({
- description: 'Delete Dashboard',
- event: async () => {
- DashboardView.removeDashboard(dashboard);
- },
+ ContextMenu.Instance.addItem({
+ description: `Delete Dashboard ${Doc.noviceMode ? '(disabled)' : ''}`,
+ event: () => !Doc.noviceMode && DashboardView.removeDashboard(dashboard),
icon: 'trash',
});
- cm.displayMenu((e?.pageX || pageX || 0) - 15, (e?.pageY || pageY || 0) - 15);
+ ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15);
}
};
render() {
- const color = StrCast(Doc.UserDoc().userColor)
- const variant = StrCast(Doc.UserDoc().userVariantColor)
+ const color = SettingsManager.userColor;
+ const variant = SettingsManager.userVariantColor;
return (
<>
<div className="dashboard-view">
<div className="left-menu">
- <Button
- text={'My Dashboards'}
- active={this.selectedDashboardGroup === DashboardGroup.MyDashboards}
- color={color}
- align={'flex-start'}
- onClick={() => this.selectDashboardGroup(DashboardGroup.MyDashboards)}
- fillWidth
- />
- <Button
+ <Button text="My Dashboards" active={this.selectedDashboardGroup === DashboardGroup.MyDashboards} color={color} align={'flex-start'} onClick={() => this.selectDashboardGroup(DashboardGroup.MyDashboards)} fillWidth />
+ <Button
text={'Shared Dashboards' + ' (' + this.getDashboards(DashboardGroup.SharedDashboards).length + ')'}
active={this.selectedDashboardGroup === DashboardGroup.SharedDashboards}
color={this.getDashboards(DashboardGroup.SharedDashboards).some(dash => !DocListCast(Doc.MySharedDocs.viewed).includes(dash)) ? 'green' : color}
- align={'flex-start'}
+ align="flex-start"
onClick={() => this.selectDashboardGroup(DashboardGroup.SharedDashboards)}
fillWidth
/>
- <Button icon={<FaPlus />} color={variant} iconPlacement="left" text="New Dashboard" type={Type.TERT} onClick={() => this.setNewDashboardName('')} />
+ <Button icon={<FaPlus />} color={variant} iconPlacement="left" text="New Dashboard" type={Type.TERT} onClick={this.openNewDashboardModal} />
</div>
<div className="all-dashboards">
- {this.getDashboards(this.selectedDashboardGroup).map(dashboard => {
- const href = ImageCast(dashboard.thumb)?.url.href;
- const shared = Object.keys(dashboard[DocAcl])
- .filter(key => key !== `acl-${Doc.CurrentUserEmailNormalized}` && !['acl-Me', 'acl-Guest'].includes(key))
- .some(key => dashboard[DocAcl][key] !== AclPrivate);
- return (
- <div
- className="dashboard-container"
- key={dashboard[Id]}
- style={{ background: this.isUnviewedSharedDashboard(dashboard) && this.selectedDashboardGroup === DashboardGroup.SharedDashboards ? '#6CB982' : shared ? variant : '' }}
- onContextMenu={e => this.onContextMenu(dashboard, e)}
- onClick={e => this.clickDashboard(e, dashboard)}>
- <img
- src={
- href ?? 'https://media.istockphoto.com/photos/hot-air-balloons-flying-over-the-botan-canyon-in-turkey-picture-id1297349747?b=1&k=20&m=1297349747&s=170667a&w=0&h=oH31fJty_4xWl_JQ4OIQWZKP8C6ji9Mz7L4XmEnbqRU='
- }
- />
- <div className="info">
- <EditableText
- type={Type.PRIM}
- color={color}
- val={StrCast(dashboard.title)}
- setVal={val => (Doc.GetProto(dashboard).title = val)}
- />
- {this.selectedDashboardGroup === DashboardGroup.SharedDashboards && this.isUnviewedSharedDashboard(dashboard) ? <div>unviewed</div> : <div></div>}
- <div
- className="more"
- onPointerDown={e => {
- this._downX = e.clientX;
- this._downY = e.clientY;
- }}
- onClick={e => {
- e.preventDefault();
- e.stopPropagation();
- this.onContextMenu(dashboard, e);
- }}>
- <Button size={Size.SMALL} color={color} icon={<FontAwesomeIcon color={color} icon="bars" />} />
- </div>
- </div>
- <div className={`background`} style={{
- background: StrCast(Doc.UserDoc().userColor),
- filter: 'opacity(0.2)'
- }}/>
- <div className={'dashboard-status' + (shared ? '-shared' : '')}>{shared ? 'shared' : ''}</div>
- </div>
- );
- })}
- <div
- className="dashboard-container-new"
- onClick={() => {
- this.setNewDashboardName('');
- }}>
- +
- <div className={`background`} style={{
- background: StrCast(Doc.UserDoc().userColor),
- filter: 'opacity(0.2)'
- }}/>
- </div>
+ {this.selectedDashboardGroup === DashboardGroup.SharedDashboards && !this.getDashboards(this.selectedDashboardGroup).length
+ ? 'No one has shared a dashboard with you'
+ : this.getDashboards(this.selectedDashboardGroup).map(dashboard => {
+ const href = ImageCast(dashboard.thumb)?.url?.href;
+ const shared = Object.keys(dashboard[DocAcl])
+ .filter(key => key !== `acl-${Doc.CurrentUserEmailNormalized}` && !['acl-Me', 'acl-Guest'].includes(key))
+ .some(key => dashboard[DocAcl][key] !== AclPrivate);
+ return (
+ <div
+ className="dashboard-container"
+ key={dashboard[Id]}
+ style={{ background: this.isUnviewedSharedDashboard(dashboard) && this.selectedDashboardGroup === DashboardGroup.SharedDashboards ? '#6CB982' : shared ? variant : '' }}
+ onContextMenu={e => this.onContextMenu(dashboard, e)}
+ onClick={e => this.clickDashboard(e, dashboard)}>
+ <img
+ src={
+ href ??
+ 'https://media.istockphoto.com/photos/hot-air-balloons-flying-over-the-botan-canyon-in-turkey-picture-id1297349747?b=1&k=20&m=1297349747&s=170667a&w=0&h=oH31fJty_4xWl_JQ4OIQWZKP8C6ji9Mz7L4XmEnbqRU='
+ }
+ />
+ <div className="info">
+ <EditableText type={Type.PRIM} color={color} val={StrCast(dashboard.title)} setVal={val => (Doc.GetProto(dashboard).title = val)} />
+ {this.selectedDashboardGroup === DashboardGroup.SharedDashboards && this.isUnviewedSharedDashboard(dashboard) ? <div>unviewed</div> : <div></div>}
+ <div
+ className="more"
+ onPointerDown={e => {
+ this._downX = e.clientX;
+ this._downY = e.clientY;
+ }}
+ onClick={e => this.onContextMenu(dashboard, e)}>
+ <Button size={Size.SMALL} color={color} icon={<FontAwesomeIcon color={color} icon="bars" />} />
+ </div>
+ </div>
+ <div
+ className="background"
+ style={{
+ background: SettingsManager.userColor,
+ filter: 'opacity(0.2)',
+ }}
+ />
+ <div className={'dashboard-status' + (shared ? '-shared' : '')}>{shared ? 'shared' : ''}</div>
+ </div>
+ );
+ })}
+ {this.selectedDashboardGroup === DashboardGroup.SharedDashboards ? null : (
+ <div className="dashboard-container-new" onClick={this.openNewDashboardModal}>
+ +
+ <div
+ className="background"
+ style={{
+ background: SettingsManager.userColor,
+ filter: 'opacity(0.2)',
+ }}
+ />
+ </div>
+ )}
</div>
</div>
- <MainViewModal
- contents={this.namingInterface}
- isDisplayed={this.newDashboardName !== undefined}
- interactive={true}
- closeOnExternalClick={this.abortCreateNewDashboard}
- dialogueBoxStyle={{ width: '400px', height: '180px', color: Colors.LIGHT_GRAY }}
- />
+ <MainViewModal contents={this.namingInterface} isDisplayed={this.openModal} interactive={true} closeOnExternalClick={this.abortCreateNewDashboard} dialogueBoxStyle={{ width: '400px', height: '180px', color: Colors.LIGHT_GRAY }} />
</>
);
}
@@ -278,6 +224,7 @@ export class DashboardView extends React.Component {
public static openDashboard = (doc: Doc | undefined, fromHistory = false) => {
if (!doc) return false;
Doc.AddDocToList(Doc.MyDashboards, 'data', doc);
+ Doc.RemoveDocFromList(Doc.MyRecentlyClosed, 'data', doc);
// this has the side-effect of setting the main container since we're assigning the active/guest dashboard
Doc.UserDoc() ? (Doc.ActiveDashboard = doc) : (Doc.GuestDashboard = doc);
@@ -312,13 +259,14 @@ export class DashboardView extends React.Component {
return true;
};
- public static removeDashboard = async (dashboard: Doc) => {
- const dashboards = await DocListCastAsync(Doc.MyDashboards.data);
- if (dashboards?.length) {
- if (dashboard === Doc.ActiveDashboard) DashboardView.openDashboard(dashboards.find(doc => doc !== dashboard));
+ public static removeDashboard = (dashboard: Doc) => {
+ const dashboards = DocListCast(Doc.MyDashboards.data).filter(dash => dash !== dashboard);
+ undoable(() => {
+ if (dashboard === Doc.ActiveDashboard) DashboardView.openDashboard(dashboards.lastElement());
Doc.RemoveDocFromList(Doc.MyDashboards, 'data', dashboard);
+ Doc.AddDocToList(Doc.MyRecentlyClosed, 'data', dashboard, undefined, true, true);
if (!dashboards.length) Doc.ActivePage = 'home';
- }
+ }, 'remove dashboard')();
};
public static resetDashboard = (dashboard: Doc) => {
@@ -416,11 +364,14 @@ export class DashboardView extends React.Component {
backgroundColor: background,
title: `Untitled Tab 1`,
};
+
const title = name ? name : `Dashboard ${dashboardCount}`;
const freeformDoc = Doc.GuestTarget || Docs.Create.FreeformDocument([], freeformOptions);
const dashboardDoc = Docs.Create.StandardCollectionDockingDocument([{ doc: freeformDoc, initialWidth: 600 }], { title: title }, id, 'row');
+ Doc.AddDocToList(Doc.MyHeaderBar, 'data', freeformDoc);
dashboardDoc['pane-count'] = 1;
+ freeformDoc.embedContainer = dashboardDoc;
Doc.AddDocToList(Doc.MyDashboards, 'data', dashboardDoc);
@@ -458,12 +409,12 @@ export class DashboardView extends React.Component {
title: 'My Trails',
_layout_showTitle: 'title',
_height: 100,
- treeViewHideTitle: true,
+ treeView_HideTitle: true,
_layout_fitWidth: true,
_gridGap: 5,
_forceActive: true,
childDragAction: 'embed',
- treeViewTruncateTitleWidth: 150,
+ treeView_TruncateTitleWidth: 150,
ignoreClick: true,
layout_headerButton: myTrailsBtn,
contextMenuIcons: new List<string>(['plus']),
@@ -475,7 +426,7 @@ export class DashboardView extends React.Component {
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)' });
+ const myTrails = DocUtils.AssignScripts(Docs.Create.TreeDocument([], reqdOpts), { treeView_ChildDoubleClick: 'openPresentation(documentView.rootDoc)' });
dashboardDoc.myTrails = new PrefetchProxy(myTrails);
const contextMenuScripts = [reqdBtnScript.onClick];
@@ -500,6 +451,8 @@ ScriptingGlobals.add(function resetDashboard(dashboard: Doc) {
ScriptingGlobals.add(function addToDashboards(dashboard: Doc) {
DashboardView.openDashboard(Doc.MakeEmbedding(dashboard));
}, 'adds Dashboard to set of Dashboards');
-ScriptingGlobals.add(function snapshotDashboard() {
- DashboardView.snapshotDashboard();
+ScriptingGlobals.add(async function snapshotDashboard() {
+ const batch = UndoManager.StartBatch('snapshot');
+ await DashboardView.snapshotDashboard();
+ batch.end();
}, 'creates a snapshot copy of a dashboard');
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index 8fe5c2e01..483b92957 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -3,7 +3,7 @@ import { DateField } from '../../fields/DateField';
import { Doc, DocListCast, HierarchyMapping, Opt, ReverseHierarchyMap } from '../../fields/Doc';
import { AclAdmin, AclAugment, AclEdit, AclPrivate, AclReadonly, DocAcl, DocData } from '../../fields/DocSymbols';
import { List } from '../../fields/List';
-import { Cast, StrCast } from '../../fields/Types';
+import { Cast, DocCast, StrCast } from '../../fields/Types';
import { distributeAcls, GetEffectiveAcl, inheritParentAcls, SharingPermissions } from '../../fields/util';
import { returnFalse } from '../../Utils';
import { DocUtils } from '../documents/Documents';
@@ -139,7 +139,7 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>()
const indocs = doc instanceof Doc ? [doc] : doc;
const docs = indocs.filter(doc => [AclEdit, AclAdmin].includes(effectiveAcl) || GetEffectiveAcl(doc) === AclAdmin);
- docs.forEach(doc => doc.annotationOn === this.props.Document && Doc.SetInPlace(doc, 'annotationOn', undefined, 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));
@@ -149,9 +149,9 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>()
toRemove.forEach(doc => {
leavePushpin && DocUtils.LeavePushpin(doc, annotationKey ?? this.annotationKey);
Doc.RemoveDocFromList(targetDataDoc, annotationKey ?? this.annotationKey, doc);
+ Doc.RemoveDocFromList(Doc.GetProto(doc), 'proto_embeddings', doc);
doc.embedContainer = undefined;
if (recent) {
- Doc.RemoveDocFromList(recent, 'data', doc);
doc.type !== DocumentType.LOADING && Doc.AddDocToList(recent, 'data', doc, undefined, true, true);
}
});
@@ -194,6 +194,7 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>()
added.forEach(doc => {
doc._dragOnlyWithinContainer = undefined;
if (annotationKey ?? this._annotationKeySuffix()) Doc.GetProto(doc).annotationOn = this.rootDoc;
+ else Doc.GetProto(doc).annotationOn = undefined;
Doc.SetContainer(doc, this.rootDoc);
inheritParentAcls(targetDataDoc, doc, true);
});
@@ -201,7 +202,7 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>()
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()));
+ targetDataDoc[(annotationKey ?? this.annotationKey) + '_modificationDate'] = new DateField();
}
}
return true;
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 16f5ad168..c1ec5b4a4 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -562,11 +562,11 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
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) {
+ const anchor = rootView.ComponentView?.getAnchor?.(true) ?? rootDoc;
+ const trail = DocCast(anchor.presentationTrail) ?? Doc.MakeCopy(DocCast(Doc.UserDoc().emptyTrail), true);
+ if (trail !== anchor.presentationTrail) {
DocUtils.MakeLink(anchor, trail, { link_relationship: 'link trail' });
- anchor.presTrail = trail;
+ anchor.presentationTrail = trail;
}
Doc.ActivePresentation = trail;
this.props.views().lastElement()?.props.addDocTab(trail, OpenWhere.replaceRight);
@@ -589,7 +589,6 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
<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}
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 956dac555..7e55b0ebc 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -57,7 +57,6 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
@observable private _editingTitle = false;
@observable private _hidden = false;
@observable public AddToSelection = false; // if Shift is pressed, then this should be set so that clicking on the selection background is ignored so overlapped documents can be added to the selection set.
- @observable public Interacting = false;
@observable public pushIcon: IconProp = 'arrow-alt-circle-up';
@observable public pullIcon: IconProp = 'arrow-alt-circle-down';
@observable public pullColor: string = 'white';
@@ -80,8 +79,10 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
// show decorations whenever pointer moves outside of selection bounds.
'pointermove',
action(e => {
- if (this.Bounds.x !== Number.MAX_VALUE && (this.Bounds.x > e.clientX || this.Bounds.r < e.clientX || this.Bounds.y > e.clientY || this.Bounds.b < e.clientY)) {
- this._showNothing = false;
+ if (this.Bounds.x || this.Bounds.y || this.Bounds.r || this.Bounds.b) {
+ if (this.Bounds.x !== Number.MAX_VALUE && (this.Bounds.x > e.clientX || this.Bounds.r < e.clientX || this.Bounds.y > e.clientY || this.Bounds.b < e.clientY)) {
+ this._showNothing = false;
+ }
}
})
);
@@ -328,7 +329,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
*/
@action
onRadiusDown = (e: React.PointerEvent): void => {
- this._isRounding = DocumentDecorations.Instance.Interacting = true;
+ this._isRounding = DocumentView.Interacting = true;
this._resizeUndo = UndoManager.StartBatch('DocDecs set radius');
// Call util move event function
setupMoveUpEvents(
@@ -351,7 +352,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
return false;
}, // moveEvent
action(e => {
- DocumentDecorations.Instance.Interacting = this._isRounding = false;
+ DocumentView.Interacting = this._isRounding = false;
this._resizeUndo?.end();
}), // upEvent
e => {}, // clickEvent,
@@ -476,7 +477,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
@action
onPointerDown = (e: React.PointerEvent): void => {
setupMoveUpEvents(this, e, this.onPointerMove, this.onPointerUp, emptyFunction);
- this.Interacting = true; // turns off pointer events on things like youtube videos and web pages so that dragging doesn't get "stuck" when cursor moves over them
+ DocumentView.Interacting = true; // turns off pointer events on things like youtube videos and web pages so that dragging doesn't get "stuck" when cursor moves over them
this._resizeHdlId = e.currentTarget.className;
const bounds = e.currentTarget.getBoundingClientRect();
this._offX = this._resizeHdlId.toLowerCase().includes('left') ? bounds.right - e.clientX : bounds.left - e.clientX;
@@ -617,8 +618,8 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
}
let actualdW = Math.max(docwidth + dW * scale, 20);
let actualdH = Math.max(docheight + dH * scale, 20);
- let dX = !dWin ? 0 : scale * refCent[0] * (1 - (1 + dWin / refWidth));
- let dY = !dHin ? 0 : scale * refCent[1] * (1 - (1 + dHin / refHeight));
+ let dX = !dWin ? 0 : (scale * refCent[0] * -dWin) / refWidth;
+ let dY = !dHin ? 0 : (scale * refCent[1] * -dHin) / refHeight;
const preserveNativeDim = !doc._nativeHeightUnfrozen && !doc._nativeDimModifiable;
const fixedAspect = nwidth && nheight && (!doc._layout_fitWidth || preserveNativeDim || e.ctrlKey || doc.nativeHeightUnfrozen || doc.nativeDimModifiable);
if (fixedAspect) {
@@ -630,6 +631,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
} else {
if (!doc._layout_fitWidth || preserveNativeDim) {
actualdH = (nheight / nwidth) * actualdW;
+ dYin && (dY = -dW * scale * (nheight / nwidth));
doc._height = actualdH;
} else if (!modifyNativeDim || dragBotRight) {
doc._height = actualdH;
@@ -646,6 +648,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
} else {
if (!doc._layout_fitWidth || preserveNativeDim) {
actualdW = (nwidth / nheight) * actualdH;
+ dXin && (dX = -dH * scale * (nwidth / nheight));
doc._width = actualdW;
} else if (!modifyNativeDim || dragBotRight) {
doc._width = actualdW;
@@ -684,7 +687,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
@action
onPointerUp = (e: PointerEvent): void => {
this._resizeHdlId = '';
- this.Interacting = false;
+ DocumentView.Interacting = false;
this._resizeUndo?.end();
SnappingManager.clearSnapLines();
@@ -825,13 +828,14 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
const radiusHandle = (borderRadius / docMax) * maxDist;
const radiusHandleLocation = Math.min(radiusHandle, maxDist);
- const sharingMenu = docShareMode ? (
- <div className="documentDecorations-share">
- <div className={`documentDecorations-share${shareMode}`}>
- &nbsp;
- {shareSymbolIcon + ' ' + shareMode}
- &nbsp;
- {/* {!Doc.noviceMode ? (
+ const sharingMenu =
+ Doc.IsSharingEnabled && docShareMode ? (
+ <div className="documentDecorations-share">
+ <div className={`documentDecorations-share${shareMode}`}>
+ &nbsp;
+ {shareSymbolIcon + ' ' + shareMode}
+ &nbsp;
+ {/* {!Doc.noviceMode ? (
<div className="checkbox">
<div className="checkbox-box">
<input type="checkbox" checked={this.showLayoutAcl} onChange={action(() => (this.showLayoutAcl = !this.showLayoutAcl))} />
@@ -840,11 +844,11 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
</div>
) : null}
&nbsp; */}
+ </div>
</div>
- </div>
- ) : (
- <div />
- );
+ ) : (
+ <div />
+ );
const titleArea = this._editingTitle ? (
<input
@@ -883,7 +887,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
);
return (
- <div className={`documentDecorations${colorScheme}`} style={{ opacity: this._showNothing ? 0.1 : undefined }}>
+ <div className={`documentDecorations${colorScheme}`} style={{ display: this._showNothing ? 'none' : undefined }}>
<div
className="documentDecorations-background"
style={{
@@ -891,7 +895,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
height: bounds.b - bounds.y + this._resizeBorderWidth + 'px',
left: bounds.x - this._resizeBorderWidth / 2,
top: bounds.y - this._resizeBorderWidth / 2,
- pointerEvents: DocumentDecorations.Instance.AddToSelection || this.Interacting ? 'none' : 'all',
+ pointerEvents: DocumentDecorations.Instance.AddToSelection || DocumentView.Interacting ? 'none' : 'all',
display: SelectionManager.Views().length <= 1 || hideDecorations ? 'none' : undefined,
}}
onPointerDown={this.onBackgroundDown}
diff --git a/src/client/views/EditableView.scss b/src/client/views/EditableView.scss
index 0955ba8ff..f7c03caf9 100644
--- a/src/client/views/EditableView.scss
+++ b/src/client/views/EditableView.scss
@@ -3,7 +3,8 @@
overflow-wrap: break-word;
word-wrap: break-word;
hyphens: auto;
- overflow: hidden;
+ overflow: auto;
+ height: 100%;
min-width: 20;
text-overflow: ellipsis;
}
@@ -11,6 +12,7 @@
.editableView-container-editing-oneLine {
width: 100%;
height: max-content;
+ overflow: hidden;
span {
p {
diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx
index 4a986cb54..ca4ffaf3a 100644
--- a/src/client/views/EditableView.tsx
+++ b/src/client/views/EditableView.tsx
@@ -6,7 +6,6 @@ import { ObjectField } from '../../fields/ObjectField';
import './EditableView.scss';
import { DocumentIconContainer } from './nodes/DocumentIcon';
import { OverlayView } from './OverlayView';
-import { EditableText } from 'browndash-components';
export interface EditableProps {
/**
@@ -232,7 +231,7 @@ export class EditableView extends React.Component<EditableProps> {
onChange: this.props.autosuggestProps.onChange,
}}
/>
- ) :
+ ) : this.props.oneLine !== false && this.props.GetValue()?.toString().indexOf('\n') === -1 ? (
<input
className="editableView-input"
ref={r => (this._inputref = r)}
@@ -248,30 +247,31 @@ export class EditableView extends React.Component<EditableProps> {
onClick={this.stopPropagation}
onPointerUp={this.stopPropagation}
/>
- // ) : (
- // <textarea
- // className="editableView-input"
- // ref={r => (this._inputref = r)}
- // style={{ display: this.props.display, overflow: 'auto', fontSize: this.props.fontSize, minHeight: `min(100%, ${(this.props.GetValue()?.split('\n').length || 1) * 15})`, minWidth: 20, background: this.props.background }}
- // placeholder={this.props.placeholder}
- // onBlur={e => this.finalizeEdit(e.currentTarget.value, false, true, false)}
- // defaultValue={this.props.GetValue()}
- // autoFocus={true}
- // onChange={this.onChange}
- // onKeyDown={this.onKeyDown}
- // onKeyPress={this.stopPropagation}
- // onPointerDown={this.stopPropagation}
- // onClick={this.stopPropagation}
- // onPointerUp={this.stopPropagation}
- // />
- // );
+ ) : (
+ <textarea
+ className="editableView-input"
+ ref={r => (this._inputref = r)}
+ style={{ display: this.props.display, overflow: 'auto', fontSize: this.props.fontSize, minHeight: `min(100%, ${(this.props.GetValue()?.split('\n').length || 1) * 15})`, minWidth: 20, background: this.props.background }}
+ placeholder={this.props.placeholder}
+ onBlur={e => this.finalizeEdit(e.currentTarget.value, false, true, false)}
+ defaultValue={this.props.GetValue()}
+ autoFocus={true}
+ onChange={this.onChange}
+ onKeyDown={this.onKeyDown}
+ onKeyPress={this.stopPropagation}
+ onPointerDown={this.stopPropagation}
+ onClick={this.stopPropagation}
+ onPointerUp={this.stopPropagation}
+ />
+ );
}
render() {
- if (this._editing && this.props.GetValue() !== undefined) {
+ const gval = this.props.GetValue()?.replace(/\n/g, '\\r\\n');
+ if (this._editing && gval !== undefined) {
return this.props.sizeToContent ? (
<div style={{ display: 'grid', minWidth: 100 }}>
- <div style={{ display: 'inline-block', position: 'relative', height: 0, width: '100%', overflow: 'hidden' }}>{this.props.GetValue()}</div>
+ <div style={{ display: 'inline-block', position: 'relative', height: 0, width: '100%', overflow: 'hidden' }}>{gval}</div>
{this.renderEditor()}
</div>
) : (
diff --git a/src/client/views/FilterPanel.scss b/src/client/views/FilterPanel.scss
index 4f0460659..d6d2956aa 100644
--- a/src/client/views/FilterPanel.scss
+++ b/src/client/views/FilterPanel.scss
@@ -135,7 +135,7 @@
.filterBox-addFilter {
width: 120px;
- background-color: #e9e9e9;
+ backgroundcolor: #e9e9e9;
border-radius: 12px;
padding: 5px;
margin: 5px;
@@ -156,7 +156,7 @@
right: 0;
top: 0;
z-index: 1;
- // background-color: #9f9f9f;
+ // background-color: #9f9f9f;
.filterBox-tree {
z-index: 0;
@@ -191,22 +191,60 @@
}
}
+.filterBox-facetHeader {
+ display: flex;
+ align-items: center;
+ // float:right;
+ .filterBox-facetHeader-collapse {
+ // float: right;
+ // justify-items: right;
+ // align-items: flex-end;
+ margin-left: auto;
+ // margin-right: 9px;
-.filterBox-facetHeader{
- display: flex;
- align-items: center;
- // float:right;
-
- .filterBox-facetHeader-collapse{
float: right;
- justify-items: right;
- align-items: flex-end;
- margin-left: auto;
- margin-right: 9px;
+ font-size: 16;
}
+ .filterBox-facetHeader-remove {
+ // margin-left: auto;
+ float: right;
+ font-size: 16;
+ font-weight: bold;
+ }
}
-
-
+.filterbox-collpasedAndActive {
+ // left:100px;
+ text-indent: 18px;
+ // background-color: pink;
+ font-size: 12px;
+ font-weight: bold;
+}
+
+.sliderBox-outerDiv {
+ display: inline-block;
+ vertical-align: middle;
+}
+
+// .sliderBox-outerDiv {
+// width: 30%;// width: calc(100% - 14px); // 14px accounts for handles that are at the max value of the slider that would extend outside the box
+// height: 40; // height: 100%;
+// border-radius: inherit;
+// display: flex;
+// flex-direction: column;
+// position: relative;
+// // background-color: yellow;
+// .slider-tracks {
+// top: 7px;
+// position: relative;
+// }
+// .slider-ticks {
+// position: relative;
+// }
+// .slider-handles {
+// top: 7px;
+// position: relative;
+// }
+// }
diff --git a/src/client/views/FilterPanel.tsx b/src/client/views/FilterPanel.tsx
index 68d29942b..69ceb0f65 100644
--- a/src/client/views/FilterPanel.tsx
+++ b/src/client/views/FilterPanel.tsx
@@ -1,24 +1,32 @@
import React = require('react');
import { action, computed, observable, ObservableMap } from 'mobx';
import { observer } from 'mobx-react';
+import { Handles, Rail, Slider, Ticks, Tracks } from 'react-compound-slider';
+import { AiOutlineMinusSquare, AiOutlinePlusSquare } from 'react-icons/ai';
+import { CiCircleRemove } from 'react-icons/ci';
import Select from 'react-select';
import { Doc, DocListCast, Field, StrListCast } from '../../fields/Doc';
import { RichTextField } from '../../fields/RichTextField';
-import { StrCast } from '../../fields/Types';
+import { DocumentOptions, FInfo } from '../documents/Documents';
import { DocumentManager } from '../util/DocumentManager';
import { UserOptions } from '../util/GroupManager';
+import { SearchUtil } from '../util/SearchUtil';
+import { undoable } from '../util/UndoManager';
import './FilterPanel.scss';
import { FieldView } from './nodes/FieldView';
-import { SearchBox } from './search/SearchBox';
-import { undoable } from '../util/UndoManager';
-import { AiOutlineMinusSquare } from 'react-icons/ai';
-import { CiCircleRemove } from 'react-icons/ci';
+import { Handle, Tick, TooltipRail, Track } from './nodes/SliderBox-components';
+import { SettingsManager } from '../util/SettingsManager';
+import { Id } from '../../fields/FieldSymbols';
+import { List } from '../../fields/List';
interface filterProps {
rootDoc: Doc;
}
+
@observer
export class FilterPanel extends React.Component<filterProps> {
+ private _documentOptions: DocumentOptions = new DocumentOptions();
+
public static LayoutString(fieldKey: string) {
return FieldView.LayoutString(FilterPanel, fieldKey);
}
@@ -41,43 +49,77 @@ export class FilterPanel extends React.Component<filterProps> {
const allDocs = new Set<Doc>();
const targetDoc = this.targetDoc;
if (targetDoc) {
- SearchBox.foreachRecursiveDoc([this.targetDoc], (depth, doc) => allDocs.add(doc));
+ SearchUtil.foreachRecursiveDoc([this.targetDoc], (depth, doc) => allDocs.add(doc));
}
+ console.log('this is all Docs' + Array.from(allDocs));
return Array.from(allDocs);
}
@computed get _allFacets() {
// trace();
- const noviceReqFields = ['author', 'tags', 'text', 'type'];
+ const noviceReqFields = ['author', 'tags', 'text', 'type', '-linkedTo'];
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)));
+ this.allDocs.forEach(doc => SearchUtil.documentKeys(doc).filter(key => keys.add(key)));
const sortedKeys = Array.from(keys.keys())
.filter(key => key[0])
.filter(key => key.indexOf('modificationDate') !== -1 || (key[0] === key[0].toUpperCase() && !key.startsWith('_')) || noviceFields.includes(key) || !Doc.noviceMode)
.sort();
+
+ // console.log('THIS IS HERE ' + Doc.UserDoc().color + 'space ' + Doc.UserDoc().color);
noviceFields.forEach(key => sortedKeys.splice(sortedKeys.indexOf(key), 1));
+
+ console.log('this is novice fields ' + noviceFields + 'and this is sorted Keys ' + sortedKeys);
+
return [...noviceFields, ...sortedKeys];
}
+ @computed get rangeFilters() {
+ return StrListCast(this.targetDoc?._childFiltersByRanges).filter((filter, i) => !(i % 3));
+ }
+
/**
- * The current attributes selected to filter based on
+ * activeFilters( ) -- all filters that currently have a filter set on them in this document (ranges, and others)
+ * ["#tags::bob::check", "tags::joe::check", "width", "height"]
*/
@computed get activeFilters() {
- return StrListCast(this.targetDoc?._childFilters);
+ return StrListCast(this.targetDoc?._childFilters).concat(this.rangeFilters);
}
+ @computed get mapActiveFiltersToFacets() {
+ const filters = new Map<string, string>();
+ //this.targetDoc.docFilters
+ this.activeFilters.map(filter => filters.set(filter.split(Doc.FilterSep)[1], filter.split(Doc.FilterSep)[0]));
+ return filters;
+ }
+
+ //
+ // activeFacetHeaders() - just the facet names, not the rest of the filter
+ //
+ // this wants to return all the filter facets that have an existing filter set on them in order to show them in the rendered panel
+ // this set may overlap the selectedFilters
+ // if the components reloads, these will still exist and be shown
+
+ // ["#tags", "width", "height"]
+ //
+
+ @computed get activeFacetHeaders() {
+ const activeHeaders = new Array();
+ this.activeFilters.map(filter => activeHeaders.push(filter.split(Doc.FilterSep)[0]));
+
+ return activeHeaders;
+ }
/**
* @returns a string array of the current attributes
*/
- @computed get currentFacets() {
- return this.activeFilters.map(filter => filter.split(Doc.FilterSep)[0]);
- }
+ // @computed get currentFacets() {
+ // return this.activeFilters.map(filter => filter.split(Doc.FilterSep)[0]);
+ // }
gatherFieldValues(childDocs: Doc[], facetKey: string) {
- const valueSet = new Set<string>();
+ const valueSet = new Set<string>(StrListCast(this.props.rootDoc.childFilters).map(filter => filter.split(Doc.FilterSep)[1]));
let rtFields = 0;
let subDocs = childDocs;
if (subDocs.length > 0) {
@@ -99,6 +141,7 @@ export class FilterPanel extends React.Component<filterProps> {
}
// }
// });
+
return { strings: Array.from(valueSet.keys()), rtFields };
}
@@ -107,46 +150,93 @@ export class FilterPanel extends React.Component<filterProps> {
Doc.setDocRangeFilter(this.targetDoc, filterName, undefined);
};
- @observable _chosenFacets = new ObservableMap<string, 'text' | 'checkbox' | 'slider' | 'range'>();
- @computed get activeFacets() {
- const facets = new Map<string, 'text' | 'checkbox' | 'slider' | 'range'>(this._chosenFacets);
- StrListCast(this.targetDoc?._childFilters).map(filter => facets.set(filter.split(Doc.FilterSep)[0], filter.split(Doc.FilterSep)[2] === 'match' ? 'text' : 'checkbox'));
- setTimeout(() => StrListCast(this.targetDoc?._childFilters).map(action(filter => this._chosenFacets.set(filter.split(Doc.FilterSep)[0], filter.split(Doc.FilterSep)[2] === 'match' ? 'text' : 'checkbox'))));
- return facets;
+ // @observable _chosenFacets = new ObservableMap<string, 'text' | 'checkbox' | 'slider' | 'range'>();
+ @observable _chosenFacetsCollapse = new ObservableMap<string, boolean>();
+ @observable _collapseReturnKeys = new Array();
+
+ // this computed function gets the active filters and maps them to their headers
+
+ //
+ // activeRenderedFacetInfos()
+ // returns renderInfo for all user selected filters and for all existing filters set on the document
+ // Map("tags" => {"checkbox"},
+ // "width" => {"rangs", domain:[1978,1992]})
+ //
+
+ @computed get activeRenderedFacetInfos() {
+ return new Set(
+ Array.from(new Set(Array.from(this._selectedFacetHeaders).concat(this.activeFacetHeaders))).map(facetHeader => {
+ const facetValues = this.gatherFieldValues(this.targetDocChildren, facetHeader);
+
+ let nonNumbers = 0;
+ let minVal = Number.MAX_VALUE,
+ maxVal = -Number.MAX_VALUE;
+ facetValues.strings.map(val => {
+ const num = val ? Number(val) : Number.NaN;
+ if (Number.isNaN(num)) {
+ val && nonNumbers++;
+ } else {
+ minVal = Math.min(num, minVal);
+ maxVal = Math.max(num, maxVal);
+ }
+ });
+
+ if (facetHeader === 'text') {
+ return { facetHeader, renderType: 'text' };
+ } else if (facetHeader !== 'tags' && nonNumbers / facetValues.strings.length < 0.1) {
+ const extendedMinVal = minVal - Math.min(1, Math.floor(Math.abs(maxVal - minVal) * 0.1));
+ const extendedMaxVal = Math.max(minVal + 1, maxVal + Math.min(1, Math.ceil(Math.abs(maxVal - minVal) * 0.05)));
+ const ranged = Doc.readDocRangeFilter(this.targetDoc, facetHeader); // not the filter range, but the zooomed in range on the filter
+ return { facetHeader, renderType: 'range', domain: [extendedMinVal, extendedMaxVal], range: ranged ? ranged : [extendedMinVal, extendedMaxVal] };
+ } else {
+ return { facetHeader, renderType: 'checkbox' };
+ }
+ })
+ );
}
+
+ @observable _selectedFacetHeaders = new Set<string>();
+
/**
- * Responds to clicking the check box in the flyout menu
+ * user clicks on a filter facet because they want to see it.
+ * this adds this chosen filter to a set of user selected filters called: selectedFilters
+ * if this component reloads, then these filters will go away since they haven't been written to any Doc anywhere
+ *
+ * // this._selectedFacets.add(facetHeader); .. add to Set() not array
*/
+
@action
facetClick = (facetHeader: string) => {
- if (!this.targetDoc) return;
- const allCollectionDocs = this.targetDocChildren;
- const facetValues = this.gatherFieldValues(this.targetDocChildren, facetHeader);
+ // just when someone chooses a facet
- let nonNumbers = 0;
- let minVal = Number.MAX_VALUE,
- maxVal = -Number.MAX_VALUE;
- facetValues.strings.map(val => {
- const num = val ? Number(val) : Number.NaN;
- if (Number.isNaN(num)) {
- val && nonNumbers++;
- } else {
- minVal = Math.min(num, minVal);
- maxVal = Math.max(num, maxVal);
+ this._selectedFacetHeaders.add(facetHeader);
+
+ return;
+ };
+
+ @action
+ sortingCurrentFacetValues = (facetHeader: string) => {
+ this._collapseReturnKeys.splice(0);
+
+ Array.from(this.activeRenderedFacetInfos.keys()).map(renderInfo => {
+ if (renderInfo.renderType === 'range' && renderInfo.facetHeader === facetHeader && renderInfo.range) {
+ this._collapseReturnKeys.push(renderInfo.range.map(number => number.toFixed(2)));
}
});
- if (facetHeader === 'text' || (facetValues.rtFields / allCollectionDocs.length > 0.1 && facetValues.strings.length > 20)) {
- this._chosenFacets.set(facetHeader, 'text');
- } else if (facetHeader !== 'tags' && nonNumbers / facetValues.strings.length < 0.1) {
- } else {
- this._chosenFacets.set(facetHeader, 'checkbox');
+
+ for (var key of this.facetValues(facetHeader)) {
+ if (this.mapActiveFiltersToFacets.get(key)) {
+ this._collapseReturnKeys.push(key);
+ }
}
+
+ return <div className=" filterbox-collpasedAndActive">{this._collapseReturnKeys.join(', ')}</div>;
};
facetValues = (facetHeader: string) => {
const allCollectionDocs = new Set<Doc>();
- SearchBox.foreachRecursiveDoc(this.targetDocChildren, (depth: number, doc: Doc) => allCollectionDocs.add(doc));
- const set = new Set<string>([String.fromCharCode(127) + '--undefined--']);
+ SearchUtil.foreachRecursiveDoc(this.targetDocChildren, (depth: number, doc: Doc) => allCollectionDocs.add(doc));
+ const set = new Set<string>([...StrListCast(this.props.rootDoc.childFilters).map(filter => filter.split(Doc.FilterSep)[1]), Doc.FilterNone, Doc.FilterAny]);
if (facetHeader === 'tags')
allCollectionDocs.forEach(child =>
StrListCast(child[facetHeader])
@@ -156,8 +246,11 @@ export class FilterPanel extends React.Component<filterProps> {
else
allCollectionDocs.forEach(child => {
const fieldVal = child[facetHeader] as Field;
- set.add(Field.toString(fieldVal));
- (fieldVal === true || fieldVal === false) && set.add((!fieldVal).toString());
+ if (!(fieldVal instanceof List)) {
+ // currently we have no good way of filtering based on a field that is a list
+ set.add(Field.toString(fieldVal));
+ (fieldVal === true || fieldVal === false) && set.add((!fieldVal).toString());
+ }
});
const facetValues = Array.from(set).filter(v => v);
@@ -168,14 +261,55 @@ export class FilterPanel extends React.Component<filterProps> {
};
render() {
- const options = this._allFacets.filter(facet => this.currentFacets.indexOf(facet) === -1).map(facet => ({ value: facet, label: facet }));
- console.log("this is option " + options)
- console.log("this is alll facets " + this._allFacets)
+ let filteredOptions: string[] = ['author', 'tags', 'text', 'acl-Guest', ...this._allFacets.filter(facet => facet[0] === facet.charAt(0).toUpperCase())];
+
+ Object.entries(this._documentOptions).forEach((pair: [string, FInfo]) => {
+ if (pair[1].filterable) {
+ filteredOptions.push(pair[0]);
+ }
+ });
+
+ let options = filteredOptions.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} />
+ <Select
+ styles={{
+ control: (baseStyles, state) => ({
+ ...baseStyles,
+ color: SettingsManager.userColor,
+ background: SettingsManager.userBackgroundColor,
+ }),
+ placeholder: (baseStyles, state) => ({
+ ...baseStyles,
+ color: SettingsManager.userColor,
+ background: SettingsManager.userBackgroundColor,
+ }),
+ input: (baseStyles, state) => ({
+ ...baseStyles,
+ color: SettingsManager.userColor,
+ background: SettingsManager.userBackgroundColor,
+ }),
+ option: (baseStyles, state) => ({
+ ...baseStyles,
+ color: SettingsManager.userColor,
+ background: !state.isFocused ? SettingsManager.userBackgroundColor : SettingsManager.userVariantColor,
+ }),
+ menuList: (baseStyles, state) => ({
+ ...baseStyles,
+ backgroundColor: SettingsManager.userBackgroundColor,
+ }),
+ }}
+ placeholder="Add a filter..."
+ options={options}
+ isMulti={false}
+ onChange={val => this.facetClick((val as UserOptions).value)}
+ onKeyDown={e => e.stopPropagation()}
+ value={null}
+ closeMenuOnSelect={true}
+ />
</div>
{/* THE FOLLOWING CODE SHOULD BE DEVELOPER FOR BOOLEAN EXPRESSION (AND / OR) */}
{/* <div className="filterBox-select-bool">
@@ -190,38 +324,72 @@ export class FilterPanel extends React.Component<filterProps> {
</div>
<div className="filterBox-tree" key="tree">
- {Array.from(this.activeFacets.keys()).map(facetHeader => (
- <div>
- <div className = "filterBox-facetHeader">
- <div className = "filterBox-facetHeader-Header"> </div>
- {facetHeader.charAt(0).toUpperCase() + facetHeader.slice(1)}
-
- <div className = "filterBox-facetHeader-collapse">
- <AiOutlineMinusSquare/>
- {/* <CiCircleRemove/> */}
- </div>
-
- </div>
-
+ {Array.from(this.activeRenderedFacetInfos.keys()).map(
+ (
+ renderInfo // iterato over activeFacetRenderInfos ==> renderInfo which you can renderInfo.facetHeader
+ ) => (
+ <div>
+ <div className="filterBox-facetHeader">
+ <div className="filterBox-facetHeader-Header"> </div>
+ {renderInfo.facetHeader.charAt(0).toUpperCase() + renderInfo.facetHeader.slice(1)}
- {this.displayFacetValueFilterUIs(this.activeFacets.get(facetHeader), facetHeader)}
- </div>
- ))}
+ <div
+ className="filterBox-facetHeader-collapse"
+ onClick={action(e => {
+ const collapseBoolValue = this._chosenFacetsCollapse.get(renderInfo.facetHeader);
+ this._chosenFacetsCollapse.set(renderInfo.facetHeader, !collapseBoolValue);
+ })}>
+ {this._chosenFacetsCollapse.get(renderInfo.facetHeader) ? <AiOutlinePlusSquare /> : <AiOutlineMinusSquare />}
+ </div>
+
+ <div
+ className="filterBox-facetHeader-remove"
+ onClick={action(e => {
+ if (renderInfo.facetHeader === 'text') {
+ Doc.setDocFilter(this.targetDoc, renderInfo.facetHeader, 'match', 'remove');
+ } else {
+ for (var key of this.facetValues(renderInfo.facetHeader)) {
+ if (this.mapActiveFiltersToFacets.get(key)) {
+ Doc.setDocFilter(this.targetDoc, renderInfo.facetHeader, key, 'remove');
+ }
+ }
+ }
+ this._selectedFacetHeaders.delete(renderInfo.facetHeader);
+ this._chosenFacetsCollapse.delete(renderInfo.facetHeader);
+
+ if (renderInfo.domain) {
+ Doc.setDocRangeFilter(this.targetDoc, renderInfo.facetHeader, renderInfo.domain, 'remove');
+ }
+ })}>
+ <CiCircleRemove />{' '}
+ </div>
+ </div>
+
+ {this._chosenFacetsCollapse.get(renderInfo.facetHeader)
+ ? this.sortingCurrentFacetValues(renderInfo.facetHeader)
+ : this.displayFacetValueFilterUIs(renderInfo.renderType, renderInfo.facetHeader, renderInfo.domain, renderInfo.range)}
+ {/* */}
+ </div>
+ )
+ )}
</div>
</div>
);
}
- private displayFacetValueFilterUIs(type: string | undefined, facetHeader: string): React.ReactNode {
+ private displayFacetValueFilterUIs(type: string | undefined, facetHeader: string, renderInfoDomain?: number[] | undefined, renderInfoRange?: number[]): React.ReactNode {
switch (type) {
case 'text':
return (
<input
- placeholder={
+ key={this.targetDoc[Id]}
+ placeholder={'enter text to match'}
+ defaultValue={
StrListCast(this.targetDoc._childFilters)
.find(filter => filter.split(Doc.FilterSep)[0] === facetHeader)
- ?.split(Doc.FilterSep)[1] ?? '-empty-'
+ ?.split(Doc.FilterSep)[1]
}
+ style={{ color: SettingsManager.userColor, background: SettingsManager.userBackgroundColor }}
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)}
/>
@@ -233,11 +401,11 @@ export class FilterPanel extends React.Component<filterProps> {
<div>
<input
style={{ width: 20, marginLeft: 20 }}
- checked={
+ checked={['check', 'exists'].includes(
StrListCast(this.targetDoc._childFilters)
.find(filter => filter.split(Doc.FilterSep)[0] === facetHeader && filter.split(Doc.FilterSep)[1] == facetValue)
- ?.split(Doc.FilterSep)[2] === 'check'
- }
+ ?.split(Doc.FilterSep)[2] ?? ''
+ )}
type={type}
onChange={undoable(e => Doc.setDocFilter(this.targetDoc, facetHeader, fval, e.target.checked ? 'check' : 'remove'), 'set filter')}
/>
@@ -245,6 +413,82 @@ export class FilterPanel extends React.Component<filterProps> {
</div>
);
});
+
+ case 'range':
+ const domain = renderInfoDomain;
+ const range = renderInfoRange;
+
+ if (range) {
+ console.log('this is info range ' + range[0] + ' , ' + range[1]);
+ }
+
+ if (domain) {
+ console.log('this is info domain ' + domain[0] + ', ' + domain[1]);
+
+ return (
+ <>
+ {/* <div className="sliderBox-outerDiv-checkBox" style={{ float: 'left' }}>
+ <Checkbox color="primary" onChange={action(() => console.log('on change'))} />
+ </div> */}
+
+ <div className="sliderBox-outerDiv" style={{ width: '95%', height: 45, float: 'right' }}>
+ <Slider
+ mode={2}
+ step={Math.min(1, 0.1 * (domain[1] - domain[0]))}
+ domain={[domain[0], domain[1]]} // -1000, 1000
+ rootStyle={{ position: 'relative', width: '100%' }}
+ onChange={values => Doc.setDocRangeFilter(this.targetDoc, facetHeader, values)}
+ values={renderInfoRange!}>
+ <Rail>{railProps => <TooltipRail {...railProps} />}</Rail>
+ <Handles>
+ {({ handles, activeHandleID, getHandleProps }) => (
+ <div className="slider-handles">
+ {handles.map((handle, i) => {
+ // const value = i === 0 ? defaultValues[0] : defaultValues[1];
+ return (
+ <div>
+ <Handle key={handle.id} handle={handle} domain={domain} isActive={handle.id === activeHandleID} getHandleProps={getHandleProps} />
+ </div>
+ );
+ })}
+ </div>
+ )}
+ </Handles>
+ <Tracks left={false} right={false}>
+ {({ tracks, getTrackProps }) => (
+ <div className="slider-tracks">
+ {tracks.map(({ id, source, target }) => (
+ <Track key={id} source={source} target={target} disabled={false} getTrackProps={getTrackProps} />
+ ))}
+ </div>
+ )}
+ </Tracks>
+ <Ticks count={5}>
+ {({ ticks }) => (
+ <div className="slider-ticks">
+ {ticks.map(tick => (
+ <Tick key={tick.id} tick={tick} count={ticks.length} format={(val: number) => val.toString()} />
+ ))}
+ </div>
+ )}
+ </Ticks>
+ </Slider>
+ </div>
+ </>
+ );
+ }
+
+ // case 'range'
+ // return <Slider ...
+ // return <slider domain={renderInfo.domain}> domain is number[] for min and max
+ // onChange = { ... Doc.setDocRangeFilter(this.targetDoc, facetHeader, [extendedMinVal, extendedMaxVal] ) }
+ //
+ // OR
+
+ // return <div>
+ // <slider domain={renderInfo.domain}> // domain is number[] for min and max
+ // <dimain changing handles >
+ // <?div
}
}
}
diff --git a/src/client/views/InkStroke.scss b/src/client/views/InkStroke.scss
index f504890a5..c672824bf 100644
--- a/src/client/views/InkStroke.scss
+++ b/src/client/views/InkStroke.scss
@@ -17,6 +17,7 @@
display: flex;
align-items: center;
height: 100%;
+ width: 100%;
transition: inherit;
.inkStroke {
mix-blend-mode: multiply;
diff --git a/src/client/views/InkTangentHandles.tsx b/src/client/views/InkTangentHandles.tsx
index 71e0ff63c..65f6a6dfa 100644
--- a/src/client/views/InkTangentHandles.tsx
+++ b/src/client/views/InkTangentHandles.tsx
@@ -12,7 +12,6 @@ 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: InkingStroke;
diff --git a/src/client/views/InkTranscription.tsx b/src/client/views/InkTranscription.tsx
index 6c213f40f..258bfad66 100644
--- a/src/client/views/InkTranscription.tsx
+++ b/src/client/views/InkTranscription.tsx
@@ -260,7 +260,10 @@ export class InkTranscription extends React.Component {
CollectionFreeFormView.collectionsWithUnprocessedInk.forEach(ffView => {
// TODO: nda - will probably want to go through ffView unprocessed docs and then see if any of the inksToGroup docs are in it and only use those
const selected = ffView.unprocessedDocs;
- const newCollection = this.groupInkDocs(selected, ffView);
+ const newCollection = this.groupInkDocs(
+ selected.filter(doc => doc.embedContainer),
+ ffView
+ );
ffView.unprocessedDocs = [];
InkTranscription.Instance.transcribeInk(newCollection, selected, false);
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index d0210d63b..f5ffff639 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -91,8 +91,8 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
const anchor = Docs.Create.ConfigDocument({
title: 'Ink anchor:' + this.rootDoc.title,
// set presentation timing for restoring shape
- presDuration: 1100,
- presTransition: 1000,
+ presentation_duration: 1100,
+ presentation_transition: 1000,
annotationOn: this.rootDoc,
});
if (anchor) {
@@ -362,6 +362,15 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
_subContentView: DocComponentView | undefined;
setSubContentView = (doc: DocComponentView) => (this._subContentView = doc);
+ @computed get fillColor() {
+ const isInkMask = BoolCast(this.layoutDoc.stroke_isInkMask);
+ return isInkMask ? DashColor(StrCast(this.layoutDoc.fillColor, 'transparent')).blacken(0).rgb().toString() : this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.FillColor) ?? 'transparent';
+ }
+ @computed get strokeColor() {
+ const { inkData } = this.inkScaledData();
+ const fillColor = this.fillColor;
+ return !InkingStroke.IsClosed(inkData) && fillColor && fillColor !== 'transparent' ? fillColor : this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color) ?? StrCast(this.layoutDoc.color);
+ }
render() {
TraceMobx();
const { inkData, inkStrokeWidth, inkLeft, inkTop, inkScaleX, inkScaleY, inkWidth, inkHeight } = this.inkScaledData();
@@ -371,8 +380,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
const markerScale = NumCast(this.layoutDoc.stroke_markerScale, 1);
const closed = InkingStroke.IsClosed(inkData);
const isInkMask = BoolCast(this.layoutDoc.stroke_isInkMask);
- const fillColor = isInkMask ? DashColor(StrCast(this.layoutDoc.fillColor, 'transparent')).blacken(0).rgb().toString() : this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.FillColor) ?? 'transparent';
- const strokeColor = !closed && fillColor && fillColor !== 'transparent' ? fillColor : this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color) ?? StrCast(this.layoutDoc.color);
+ const fillColor = this.fillColor;
// 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[Width]() !== Math.round(this.layoutDoc[Width]()) || this.layoutDoc[Height]() !== Math.round(this.layoutDoc[Height]()))) {
@@ -381,13 +389,17 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
this.layoutDoc._height = Math.round(NumCast(this.layoutDoc[Height]()));
});
}
+ 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 : undefined;
+ const color = StrCast(this.layoutDoc.stroke_outlineColor, !closed && fillColor && fillColor !== 'transparent' ? StrCast(this.layoutDoc.color, 'transparent') : 'transparent');
// Visually renders the polygonal line made by the user.
const inkLine = InteractionUtils.CreatePolyline(
inkData,
inkLeft,
inkTop,
- strokeColor,
+ this.strokeColor,
inkStrokeWidth,
inkStrokeWidth,
StrCast(this.layoutDoc.stroke_lineJoin),
@@ -403,31 +415,28 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
'',
'none',
1.0,
- false
+ false,
+ undefined,
+ undefined,
+ color === 'transparent' ? highlightColor : undefined
);
- 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, mask: boolean = false) =>
InteractionUtils.CreatePolyline(
inkData,
inkLeft,
inkTop,
- mask && highlightColor === 'transparent' ? strokeColor : highlightColor,
+ mask && color === 'transparent' ? this.strokeColor : color,
inkStrokeWidth,
- inkStrokeWidth + (fillColor ? (closed ? 2 : (highlightIndex ?? 0) + 2) : 2),
+ inkStrokeWidth + NumCast(this.layoutDoc.stroke_borderWidth) + (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,
- '',
- '',
+ startMarker,
+ endMarker,
markerScale,
- undefined,
+ StrCast(this.layoutDoc.stroke_dash),
inkScaleX,
inkScaleY,
'',
@@ -435,7 +444,8 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
0.0,
false,
downHdlr,
- mask
+ mask,
+ highlightColor
);
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.
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index 286d39943..8f081b321 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -16,7 +16,6 @@ import { CollectionStackedTimeline } from './collections/CollectionStackedTimeli
import { TabDocView } from './collections/TabDocView';
import { GestureOverlay } from './GestureOverlay';
import './LightboxView.scss';
-import { MainView } from './MainView';
import { DocumentView, OpenWhere, OpenWhereMod } from './nodes/DocumentView';
import { DefaultStyleProvider, wavyBorderPath } from './StyleProvider';
@@ -55,13 +54,13 @@ export class LightboxView extends React.Component<LightboxViewProps> {
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;
+ this.LightboxDoc.layout_fieldKey = this._savedState.layout_fieldKey ? this._savedState.layout_fieldKey : undefined;
}
if (!doc) {
this._childFilters && (this._childFilters.length = 0);
this._future = this._history = [];
Doc.ActiveTool = InkTool.None;
- MainView.Instance._exploreMode = false;
+ DocumentView.ExploreMode = false;
} else {
const l = DocUtils.MakeLinkToActiveAudio(() => doc).lastElement();
l && (Cast(l.link_anchor_2, Doc, null).backgroundColor = 'lightgreen');
@@ -270,7 +269,7 @@ export class LightboxView extends React.Component<LightboxViewProps> {
addDocTab={this.addDocTab}
pinToPres={TabDocView.PinDoc}
bringToFront={emptyFunction}
- onBrowseClick={MainView.Instance.exploreMode}
+ onBrowseClick={DocumentView.exploreMode}
focus={emptyFunction}
/>
</GestureOverlay>
@@ -313,8 +312,10 @@ export class LightboxView extends React.Component<LightboxViewProps> {
className="lightboxView-tabBtn"
title="open in tab"
onClick={e => {
+ const lightdoc = LightboxView._docTarget || LightboxView._doc!;
e.stopPropagation();
- CollectionDockingView.AddSplit(LightboxView._docTarget || LightboxView._doc!, OpenWhereMod.none);
+ Doc.RemoveDocFromList(Doc.MyRecentlyClosed, 'data', lightdoc);
+ CollectionDockingView.AddSplit(lightdoc, OpenWhereMod.none);
SelectionManager.DeselectAll();
LightboxView.SetLightboxDoc(undefined);
}}>
@@ -333,12 +334,12 @@ export class LightboxView extends React.Component<LightboxViewProps> {
<div
className="lightboxView-exploreBtn"
title="toggle explore mode to navigate among documents only"
- style={{ background: MainView.Instance._exploreMode ? 'white' : undefined }}
+ style={{ background: DocumentView.ExploreMode ? 'white' : undefined }}
onClick={action(e => {
e.stopPropagation();
- MainView.Instance._exploreMode = !MainView.Instance._exploreMode;
+ DocumentView.ExploreMode = !DocumentView.ExploreMode;
})}>
- <FontAwesomeIcon color={MainView.Instance._exploreMode ? 'black' : 'white'} icon={'globe-americas'} size="2x" />
+ <FontAwesomeIcon color={DocumentView.ExploreMode ? 'black' : 'white'} icon={'globe-americas'} size="2x" />
</div>
</div>
);
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index 2fa42d091..96bd52d39 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -2,25 +2,23 @@
// (module as any).hot.accept();
// }
+import * as dotenv from 'dotenv'; // see https://github.com/motdotla/dotenv#how-do-i-use-dotenv-with-import
import * as React from 'react';
import * as ReactDOM from 'react-dom/client';
import { AssignAllExtensions } from '../../extensions/General/Extensions';
import { FieldLoader } from '../../fields/FieldLoader';
-import { DocServer } from '../DocServer';
-import { Docs } from '../documents/Documents';
import { CurrentUserUtils } from '../util/CurrentUserUtils';
-import { LinkManager } from '../util/LinkManager'; // this must come before importing Docs and CurrentUserUtils
+import { PingManager } from '../util/PingManager';
import { ReplayMovements } from '../util/ReplayMovements';
import { TrackMovements } from '../util/TrackMovements';
import { CollectionView } from './collections/CollectionView';
-import { MainView } from './MainView';
-import * as dotenv from 'dotenv'; // see https://github.com/motdotla/dotenv#how-do-i-use-dotenv-with-import
-import { PingManager } from '../util/PingManager';
import './global/globalScripts';
+import { MainView } from './MainView';
+import { BranchingTrailManager } from '../util/BranchingTrailManager';
dotenv.config();
AssignAllExtensions();
-FieldLoader.ServerLoadStatus = { requested: 0, retrieved: 0 }; // bcz: not sure why this is needed to get the code loaded properly...
+FieldLoader.ServerLoadStatus = { requested: 0, retrieved: 0, message: 'cache' }; // bcz: not sure why this is needed to get the code loaded properly...
(async () => {
MainView.Live = window.location.search.includes('live');
@@ -29,7 +27,11 @@ FieldLoader.ServerLoadStatus = { requested: 0, retrieved: 0 }; // bcz: not sure
window.location.search.includes('safe') && CollectionView.SetSafeMode(true);
const info = await CurrentUserUtils.loadCurrentUser();
// if (info.email === 'guest') DocServer.Control.makeReadOnly();
- await CurrentUserUtils.loadUserDocument(info.id);
+ if (!info.userDocumentId) {
+ alert('Fatal Error: user not found in database');
+ return;
+ }
+ await CurrentUserUtils.loadUserDocument(info);
setTimeout(() => {
document.getElementById('root')!.addEventListener(
'wheel',
@@ -49,6 +51,7 @@ FieldLoader.ServerLoadStatus = { requested: 0, retrieved: 0 }; // bcz: not sure
document.cookie = `loadtime=${loading};${expires};path=/`;
new TrackMovements();
new ReplayMovements();
+ new BranchingTrailManager({});
new PingManager();
root.render(<MainView />);
}, 0);
diff --git a/src/client/views/MainView.scss b/src/client/views/MainView.scss
index b3faff442..c880dde9a 100644
--- a/src/client/views/MainView.scss
+++ b/src/client/views/MainView.scss
@@ -78,7 +78,7 @@ h1,
.mainView-container-Dark {
.lm_header .lm_tab {
padding: 0px;
- opacity: 0.7;
+ //opacity: 0.7;
box-shadow: none;
height: 25px;
border-bottom: black solid;
@@ -194,11 +194,11 @@ h1,
left: 0;
position: absolute;
z-index: 2;
- background-color: linen; //$light-gray;
+ // background-color: linen; //$light-gray;
- .editable-title {
- background-color: linen; //$light-gray;
- }
+ // .editable-title {
+ // background-color: linen; //$light-gray;
+ // }
}
}
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index cbaa763f5..707cf3a34 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -4,20 +4,20 @@ import * as far from '@fortawesome/free-regular-svg-icons';
import * as fa from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import 'browndash-components/dist/styles/global.min.css';
-import { action, computed, configure, observable, reaction, runInAction } from 'mobx';
+import { action, computed, configure, observable, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import 'normalize.css';
import * as React from 'react';
import { Doc, DocListCast, Opt } from '../../fields/Doc';
-import { ScriptField } from '../../fields/ScriptField';
import { DocCast, StrCast } from '../../fields/Types';
-import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, returnZero, setupMoveUpEvents, Utils } from '../../Utils';
+import { emptyFunction, lightOrDark, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, returnZero, setupMoveUpEvents, Utils } from '../../Utils';
import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager';
import { DocServer } from '../DocServer';
import { Docs } from '../documents/Documents';
import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
import { CaptureManager } from '../util/CaptureManager';
import { DocumentManager } from '../util/DocumentManager';
+import { DragManager } from '../util/DragManager';
import { GroupManager } from '../util/GroupManager';
import { HistoryUtil } from '../util/History';
import { Hypothesis } from '../util/HypothesisUtils';
@@ -43,7 +43,6 @@ import { DictationOverlay } from './DictationOverlay';
import { DocumentDecorations } from './DocumentDecorations';
import { GestureOverlay } from './GestureOverlay';
import { LEFT_MENU_WIDTH, TOPBAR_HEIGHT } from './global/globalCssVariables.scss';
-import { Colors } from './global/globalEnums';
import { KeyManager } from './GlobalKeyHandler';
import { InkTranscription } from './InkTranscription';
import { LightboxView } from './LightboxView';
@@ -55,12 +54,17 @@ import { DocumentView, DocumentViewInternal, OpenWhere, OpenWhereMod } from './n
import { DashFieldViewMenu } from './nodes/formattedText/DashFieldView';
import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
import { RichTextMenu } from './nodes/formattedText/RichTextMenu';
+import GenerativeFill from './nodes/generativeFill/GenerativeFill';
+import { ImageBox } from './nodes/ImageBox';
import { LinkDescriptionPopup } from './nodes/LinkDescriptionPopup';
import { LinkDocPreview } from './nodes/LinkDocPreview';
+import { MapAnchorMenu } from './nodes/MapBox/MapAnchorMenu';
+import { MapBox } from './nodes/MapBox/MapBox';
import { RadialMenu } from './nodes/RadialMenu';
import { TaskCompletionBox } from './nodes/TaskCompletedBox';
import { OverlayView } from './OverlayView';
import { AnchorMenu } from './pdf/AnchorMenu';
+import { GPTPopup } from './pdf/GPTPopup/GPTPopup';
import { PreviewCursor } from './PreviewCursor';
import { PropertiesView } from './PropertiesView';
import { DashboardStyleProvider, DefaultStyleProvider } from './StyleProvider';
@@ -72,7 +76,7 @@ export class MainView extends React.Component {
public static Instance: MainView;
public static Live: boolean = false;
private _docBtnRef = React.createRef<HTMLDivElement>();
- @observable public LastButton: Opt<Doc>;
+
@observable private _windowWidth: number = 0;
@observable private _windowHeight: number = 0;
@observable private _dashUIWidth: number = 0; // width of entire main dashboard region including left menu buttons and properties panel (but not including the dashboard selector button row)
@@ -140,6 +144,12 @@ export class MainView extends React.Component {
mainDocViewHeight = () => this._dashUIHeight - this.headerBarDocHeight();
componentDidMount() {
+ const scriptTag = document.createElement('script');
+ scriptTag.setAttribute('type', 'text/javascript');
+ scriptTag.setAttribute('src', 'https://www.bing.com/api/maps/mapcontrol?callback=makeMap');
+ scriptTag.async = true;
+ scriptTag.defer = true;
+ document.body.appendChild(scriptTag);
document.getElementById('root')?.addEventListener('scroll', e => (ele => (ele.scrollLeft = ele.scrollTop = 0))(document.getElementById('root')!));
const ele = document.getElementById('loader');
const prog = document.getElementById('dash-progress');
@@ -156,8 +166,8 @@ export class MainView extends React.Component {
DocServer.setLivePlaygroundFields([
'dataTransition',
'viewTransition',
- 'treeViewOpen',
- 'treeViewExpandedView',
+ 'treeView_Open',
+ 'treeView_ExpandedView',
'carousel_index',
'itemIndex', // for changing slides in presentations
'layout_sidebarWidthPercent',
@@ -182,14 +192,6 @@ export class MainView extends React.Component {
'currentFrame',
]); // can play with these fields on someone else's
}
- DocServer.GetRefField('rtfProto').then(
- proto =>
- proto instanceof Doc &&
- reaction(
- () => StrCast(proto.BROADCAST_MESSAGE),
- msg => msg && alert(msg)
- )
- );
const tag = document.createElement('script');
tag.src = 'https://www.youtube.com/iframe_api';
@@ -347,6 +349,7 @@ export class MainView extends React.Component {
fa.faMousePointer,
fa.faMusic,
fa.faObjectGroup,
+ fa.faArrowsLeftRight,
fa.faPause,
fa.faPen,
fa.faPenNib,
@@ -450,6 +453,7 @@ export class MainView extends React.Component {
fa.faSortUp,
fa.faSortDown,
fa.faTable,
+ fa.faTableColumns,
fa.faTh,
fa.faThList,
fa.faProjectDiagram,
@@ -527,10 +531,9 @@ export class MainView extends React.Component {
});
initEventListeners = () => {
- window.addEventListener('beforeunload', () => DocServer.UPDATE_SERVER_CACHE());
+ window.addEventListener('beforeunload', DocServer.UPDATE_SERVER_CACHE);
window.addEventListener('drop', e => e.preventDefault(), false); // prevent default behavior of navigating to a new web page
window.addEventListener('dragover', e => e.preventDefault(), false);
- // document.addEventListener("pointermove", action(e => SearchBox.Instance._undoBackground = UndoManager.batchCounter ? "#000000a8" : undefined));
document.addEventListener('pointerdown', this.globalPointerDown, true);
document.addEventListener('pointermove', this.globalPointerMove, true);
document.addEventListener('pointerup', this.globalPointerClick, true);
@@ -573,11 +576,7 @@ export class MainView extends React.Component {
Doc.AddDocToList(Doc.MyFilesystem, 'data', folder);
};
- @observable _exploreMode = false;
- @computed get exploreMode() {
- return () => (this._exploreMode ? ScriptField.MakeScript('CollectionBrowseClick(documentView, clientX, clientY)', { documentView: 'any', clientX: 'number', clientY: 'number' })! : undefined);
- }
- waitForDoubleClick = () => (this._exploreMode ? 'never' : undefined);
+ waitForDoubleClick = () => (DocumentView.ExploreMode ? 'never' : undefined);
headerBarScreenXf = () => new Transform(-this.leftScreenOffsetOfMainDocView - this.leftMenuFlyoutWidth(), -this.headerBarDocHeight(), 1);
mainScreenToLocalXf = () => new Transform(-this.leftScreenOffsetOfMainDocView - this.leftMenuFlyoutWidth(), -this.topOfMainDocContent, 1);
addHeaderDoc = (doc: Doc | Doc[], annotationKey?: string) => (doc instanceof Doc ? [doc] : doc).reduce((done, doc) => Doc.AddDocToList(this.headerBarDoc, 'data', doc), true);
@@ -704,7 +703,7 @@ export class MainView extends React.Component {
switch (whereFields[0]) {
case OpenWhere.lightbox: return LightboxView.AddDocTab(doc, location);
case OpenWhere.close: return CollectionDockingView.CloseSplit(doc, whereMods);
- case OpenWhere.toggle: return CollectionDockingView.ToggleSplit(doc, whereMods);
+ case OpenWhere.toggle: return CollectionDockingView.ToggleSplit(doc, whereMods, undefined, "dontSelectOnActivate"); // bcz: hack! mark the toggle so that it won't be selected on activation- this is needed so that the backlinks menu can toggle views of targets on and off without selecting them
case OpenWhere.add:default:return CollectionDockingView.AddSplit(doc, whereMods, undefined, undefined, keyValue);
}
};
@@ -748,7 +747,7 @@ export class MainView extends React.Component {
@computed get leftMenuPanel() {
return (
- <div key="menu" className="mainView-leftMenuPanel" style={{ background: StrCast(Doc.UserDoc().userBackgroundColor), display: LightboxView.LightboxDoc ? 'none' : undefined }}>
+ <div key="menu" className="mainView-leftMenuPanel" style={{ background: SettingsManager.userBackgroundColor, display: LightboxView.LightboxDoc ? 'none' : undefined }}>
<DocumentView
Document={Doc.MyLeftSidebarMenu}
DataDoc={undefined}
@@ -805,20 +804,24 @@ export class MainView extends React.Component {
{this.flyout}
<div
className="mainView-libraryHandle"
- style={{ background: StrCast(Doc.UserDoc().userBackgroundColor), left: leftMenuFlyoutWidth - 10 /* ~half width of handle */, display: !this._leftMenuFlyoutWidth ? 'none' : undefined }}
+ style={{ background: SettingsManager.userBackgroundColor, left: leftMenuFlyoutWidth - 10 /* ~half width of handle */, display: !this._leftMenuFlyoutWidth ? 'none' : undefined }}
onPointerDown={this.onFlyoutPointerDown}>
- <FontAwesomeIcon icon="chevron-left" color={StrCast(Doc.UserDoc().userColor)} style={{ opacity: '50%' }} size="sm" />
+ <FontAwesomeIcon icon="chevron-left" color={SettingsManager.userColor} style={{ opacity: '50%' }} size="sm" />
</div>
<div className="mainView-innerContainer" style={{ width: `calc(100% - ${width}px)` }}>
{this.dockingContent}
{this._hideUI ? null : (
- <div className="mainView-propertiesDragger" key="props" onPointerDown={this.onPropertiesPointerDown} style={{ right: this.propertiesWidth() - 1, background: 'linen' }}>
- <FontAwesomeIcon icon={this.propertiesWidth() < 10 ? 'chevron-left' : 'chevron-right'} color={this.colorScheme === ColorScheme.Dark ? Colors.WHITE : Colors.BLACK} size="sm" />
+ <div className="mainView-propertiesDragger" key="props" onPointerDown={this.onPropertiesPointerDown} style={{ background: SettingsManager.userVariantColor, right: this.propertiesWidth() - 1 }}>
+ <FontAwesomeIcon icon={this.propertiesWidth() < 10 ? 'chevron-left' : 'chevron-right'} color={SettingsManager.userColor} size="sm" />
</div>
)}
- <div className="properties-container" style={{ width: this.propertiesWidth() }}>
- {this.propertiesWidth() < 10 ? null : <PropertiesView styleProvider={DefaultStyleProvider} addDocTab={DocumentViewInternal.addDocTabFunc} width={this.propertiesWidth()} height={this.propertiesHeight()} />}
+ <div className="properties-container" style={{ width: this.propertiesWidth(), color: SettingsManager.userColor }}>
+ {
+ <div style={{ display: this.propertiesWidth() < 10 ? 'none' : undefined }}>
+ <PropertiesView styleProvider={DefaultStyleProvider} addDocTab={DocumentViewInternal.addDocTabFunc} width={this.propertiesWidth()} height={this.propertiesHeight()} />
+ </div>
+ }
</div>
</div>
</div>
@@ -858,11 +861,11 @@ export class MainView extends React.Component {
//setTimeout(action(() => (this._leftMenuFlyoutWidth += 0.5)));
this._sidebarContent.proto = DocCast(button.target);
- this.LastButton = button;
+ DocumentView.LastPressedSidebarBtn = button;
});
closeFlyout = action(() => {
- this.LastButton = undefined;
+ DocumentView.LastPressedSidebarBtn = undefined;
this._panelContent = 'none';
this._sidebarContent.proto = undefined;
this._leftMenuFlyoutWidth = 0;
@@ -880,7 +883,7 @@ export class MainView extends React.Component {
@computed get docButtons() {
return !Doc.MyDockedBtns ? null : (
- <div className="mainView-docButtons" style={{ background: StrCast(Doc.UserDoc().userBackgroundColor), color: StrCast(Doc.UserDoc().userColor) }} ref={this._docBtnRef}>
+ <div className="mainView-docButtons" style={{ background: SettingsManager.userBackgroundColor, color: SettingsManager.userColor }} ref={this._docBtnRef}>
<CollectionLinearView
Document={Doc.MyDockedBtns}
DataDoc={undefined}
@@ -910,19 +913,22 @@ export class MainView extends React.Component {
childFiltersByRanges={returnEmptyFilter}
searchFilterDocs={returnEmptyDoclist}
/>
- {['watching', 'recording'].includes(String(this.userDoc?.presentationMode) ?? '') ? <div style={{ border: '.5rem solid green', padding: '5px' }}>{StrCast(this.userDoc?.presentationMode)}</div> : <></>}
+ {['watching', 'recording'].includes(StrCast(this.userDoc?.presentationMode)) ? <div style={{ border: '.5rem solid green', padding: '5px' }}>{StrCast(this.userDoc?.presentationMode)}</div> : <></>}
</div>
);
}
@computed get snapLines() {
- return !SelectionManager.Views().some(dv => dv.rootDoc.freeform_snapLines) ? null : (
+ SnappingManager.GetIsDragging();
+ const dragged = DragManager.docsBeingDragged.lastElement();
+ const dragPar = dragged ? DocumentManager.Instance.getDocumentView(dragged)?.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView : undefined;
+ return !dragPar?.rootDoc.freeform_snapLines ? null : (
<div className="mainView-snapLines">
<svg style={{ width: '100%', height: '100%' }}>
{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'} />
+ <line key={i} x1="0" y1={l} x2="2000" y2={l} stroke={lightOrDark(dragPar.rootDoc.backgroundColor ?? 'gray')} opacity={0.3} strokeWidth={1} strokeDasharray={'2 2'} />
))}
{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'} />
+ <line key={i} y1={this.topOfMainDocContent.toString()} x1={l} y2="2000" x2={l} stroke={lightOrDark(dragPar.rootDoc.backgroundColor ?? 'gray')} opacity={0.3} strokeWidth={1} strokeDasharray={'2 2'} />
))}
</svg>
</div>
@@ -957,14 +963,50 @@ export class MainView extends React.Component {
@computed get linkDocPreview() {
return LinkDocPreview.LinkInfo ? <LinkDocPreview {...LinkDocPreview.LinkInfo} /> : null;
}
+ @observable mapBoxHackBool = false;
+ @computed get mapBoxHack() {
+ return this.mapBoxHackBool ? null : (
+ <MapBox
+ ref={action((r: any) => r && (this.mapBoxHackBool = true))}
+ fieldKey="data"
+ select={returnFalse}
+ isSelected={returnFalse}
+ Document={this.headerBarDoc}
+ DataDoc={undefined}
+ addDocTab={returnFalse}
+ pinToPres={emptyFunction}
+ docViewPath={returnEmptyDoclist}
+ styleProvider={DefaultStyleProvider}
+ rootSelected={returnTrue}
+ addDocument={returnFalse}
+ removeDocument={returnFalse}
+ fitContentsToBox={returnTrue}
+ isDocumentActive={returnTrue} // headerBar is always documentActive (ie, the docView gets pointer events)
+ isContentActive={returnTrue} // headerBar is awlays contentActive which means its items are always documentActive
+ ScreenToLocalTransform={Transform.Identity}
+ childHideResizeHandles={returnTrue}
+ childDragAction="move"
+ dontRegisterView={true}
+ PanelWidth={this.headerBarDocWidth}
+ PanelHeight={this.headerBarDocHeight}
+ renderDepth={0}
+ focus={emptyFunction}
+ whenChildContentsActiveChanged={emptyFunction}
+ bringToFront={emptyFunction}
+ childFilters={returnEmptyFilter}
+ childFiltersByRanges={returnEmptyFilter}
+ searchFilterDocs={returnEmptyDoclist}
+ />
+ );
+ }
render() {
return (
<div
className={`mainView-container ${this.colorScheme}`}
style={{
- color: StrCast(Doc.UserDoc().userColor),
- background: StrCast(Doc.UserDoc().userBackgroundColor),
+ color: SettingsManager.userColor,
+ background: SettingsManager.userBackgroundColor,
}}
onScroll={() => (ele => (ele.scrollTop = ele.scrollLeft = 0))(document.getElementById('root')!)}
ref={r => {
@@ -1012,14 +1054,18 @@ export class MainView extends React.Component {
<ContextMenu />
<RadialMenu />
<AnchorMenu />
+ <MapAnchorMenu />
<DashFieldViewMenu />
<MarqueeOptionsMenu />
- <OverlayView />
<TimelineMenu />
<RichTextMenu />
<InkTranscription />
{this.snapLines}
<LightboxView key="lightbox" PanelWidth={this._windowWidth} PanelHeight={this._windowHeight} maxBorder={[200, 50]} />
+ <OverlayView />
+ {this.mapBoxHack}
+ <GPTPopup key="gptpopup" />
+ <GenerativeFill imageEditorOpen={ImageBox.imageEditorOpen} imageEditorSource={ImageBox.imageEditorSource} imageRootDoc={ImageBox.imageRootDoc} addDoc={ImageBox.addDoc} />
{/* <NewLightboxView key="newLightbox" PanelWidth={this._windowWidth} PanelHeight={this._windowHeight} maxBorder={[200, 50]} /> */}
</div>
);
@@ -1029,3 +1075,12 @@ export class MainView extends React.Component {
ScriptingGlobals.add(function selectMainMenu(doc: Doc, title: string) {
MainView.Instance.selectMenu(doc);
});
+ScriptingGlobals.add(function createNewPresentation() {
+ return MainView.Instance.createNewPresentation();
+}, 'creates a new presentation when called');
+ScriptingGlobals.add(function openPresentation(pres: Doc) {
+ return MainView.Instance.openPresentation(pres);
+}, 'creates a new presentation when called');
+ScriptingGlobals.add(function createNewFolder() {
+ return MainView.Instance.createNewFolder();
+}, 'creates a new folder in myFiles when called');
diff --git a/src/client/views/MainViewModal.tsx b/src/client/views/MainViewModal.tsx
index 42df99864..67b722e7a 100644
--- a/src/client/views/MainViewModal.tsx
+++ b/src/client/views/MainViewModal.tsx
@@ -1,10 +1,10 @@
-import * as React from 'react';
-import './MainViewModal.scss';
+import { isDark } from 'browndash-components';
import { observer } from 'mobx-react';
+import * as React from 'react';
import { Doc } from '../../fields/Doc';
import { StrCast } from '../../fields/Types';
-import { isDark } from 'browndash-components';
-import { Colors } from './global/globalEnums';
+import { SettingsManager } from '../util/SettingsManager';
+import './MainViewModal.scss';
export interface MainViewOverlayProps {
isDisplayed: boolean;
@@ -45,7 +45,7 @@ export class MainViewModal extends React.Component<MainViewOverlayProps> {
className="overlay"
onClick={this.props?.closeOnExternalClick}
style={{
- backgroundColor: isDark(StrCast(Doc.UserDoc().userColor)) ? "#DFDFDF30" : "#32323230",
+ backgroundColor: isDark(SettingsManager.userColor) ? '#DFDFDF30' : '#32323230',
...(p.overlayStyle || {}),
}}
/>
diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx
index a4a2c1df9..3d8d569fa 100644
--- a/src/client/views/MarqueeAnnotator.tsx
+++ b/src/client/views/MarqueeAnnotator.tsx
@@ -167,9 +167,9 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
annotationOn: this.props.rootDoc,
text: this.props.selectionText(),
backgroundColor: 'transparent',
- presDuration: 2100,
- presTransition: 500,
- presZoomText: true,
+ presentation_duration: 2100,
+ presentation_transition: 500,
+ presentation_zoomText: true,
title: 'Selection on ' + this.props.rootDoc.title,
});
let minX = Number.MAX_VALUE;
@@ -201,7 +201,7 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
textRegionAnnoProto.height = Math.max(maxY, 0) - Math.max(minY, 0);
textRegionAnnoProto.width = Math.max(maxX, 0) - Math.max(minX, 0);
// mainAnnoDocProto.text = this._selectionText;
- textRegionAnnoProto.textInlineAnnotations = new List<Doc>(annoDocs);
+ textRegionAnnoProto.text_inlineAnnotations = new List<Doc>(annoDocs);
savedAnnoMap.clear();
return textRegionAnno;
};
diff --git a/src/client/views/OverlayView.scss b/src/client/views/OverlayView.scss
index 5362bf9f0..9b10d1cf7 100644
--- a/src/client/views/OverlayView.scss
+++ b/src/client/views/OverlayView.scss
@@ -4,6 +4,7 @@
top: 0;
width: 100vw;
height: 100vh;
+ z-index: 1001; // shouold be greater than LightboxView's z-index so that link lines and the presentation mini player appear
/* background-color: pink; */
}
diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx
index 339507f65..c174befc0 100644
--- a/src/client/views/OverlayView.tsx
+++ b/src/client/views/OverlayView.tsx
@@ -7,17 +7,16 @@ 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 { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnTrue, setupMoveUpEvents, Utils } from '../../Utils';
import { DocumentType } from '../documents/DocumentTypes';
-import { DocumentManager } from '../util/DocumentManager';
import { DragManager } from '../util/DragManager';
import { Transform } from '../util/Transform';
import { CollectionFreeFormLinksView } from './collections/collectionFreeForm/CollectionFreeFormLinksView';
import { LightboxView } from './LightboxView';
-import { MainView } from './MainView';
import { DocumentView, DocumentViewInternal } from './nodes/DocumentView';
import './OverlayView.scss';
import { DefaultStyleProvider } from './StyleProvider';
+const _global = (window /* browser */ || global) /* node */ as any;
export type OverlayDisposer = () => void;
@@ -117,6 +116,20 @@ export class OverlayView extends React.Component {
super(props);
if (!OverlayView.Instance) {
OverlayView.Instance = this;
+ new _global.ResizeObserver(
+ action((entries: any) => {
+ for (const entry of entries) {
+ DocListCast(Doc.MyOverlayDocs?.data).forEach(doc => {
+ if (NumCast(doc.overlayX) > entry.contentRect.width - 10) {
+ doc.overlayX = entry.contentRect.width - 10;
+ }
+ if (NumCast(doc.overlayY) > entry.contentRect.height - 10) {
+ doc.overlayY = entry.contentRect.height - 10;
+ }
+ });
+ }
+ })
+ ).observe(window.document.body);
}
}
diff --git a/src/client/views/Palette.tsx b/src/client/views/Palette.tsx
new file mode 100644
index 000000000..749eb08a2
--- /dev/null
+++ b/src/client/views/Palette.tsx
@@ -0,0 +1,69 @@
+import { IReactionDisposer, observable, reaction } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { Doc } from '../../fields/Doc';
+import { NumCast } from '../../fields/Types';
+import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnTrue, emptyPath } from '../../Utils';
+import { Transform } from '../util/Transform';
+import { DocumentView } from './nodes/DocumentView';
+import './Palette.scss';
+
+export interface PaletteProps {
+ x: number;
+ y: number;
+ thumb: number[];
+ thumbDoc: Doc;
+}
+
+@observer
+export default class Palette extends React.Component<PaletteProps> {
+ private _selectedDisposer?: IReactionDisposer;
+ @observable private _selectedIndex: number = 0;
+
+ componentDidMount = () => {
+ this._selectedDisposer = reaction(
+ () => NumCast(this.props.thumbDoc.selectedIndex),
+ i => (this._selectedIndex = i),
+ { fireImmediately: true }
+ );
+ };
+
+ componentWillUnmount = () => {
+ this._selectedDisposer?.();
+ };
+
+ render() {
+ return (
+ <div className="palette-container" style={{ transform: `translate(${this.props.x}px, ${this.props.y}px)` }}>
+ <div className="palette-thumb" style={{ transform: `translate(${this.props.thumb[0] - this.props.x}px, ${this.props.thumb[1] - this.props.y}px)` }}>
+ <div className="palette-thumbContent" style={{ transform: `translate(-${this._selectedIndex * 50 + 10}px, 0px)` }}>
+ <DocumentView
+ Document={this.props.thumbDoc}
+ DataDoc={undefined}
+ addDocument={undefined}
+ addDocTab={returnFalse}
+ rootSelected={returnTrue}
+ pinToPres={emptyFunction}
+ removeDocument={undefined}
+ ScreenToLocalTransform={Transform.Identity}
+ PanelWidth={() => window.screen.width}
+ PanelHeight={() => window.screen.height}
+ renderDepth={0}
+ isDocumentActive={returnTrue}
+ isContentActive={emptyFunction}
+ focus={emptyFunction}
+ docViewPath={returnEmptyDoclist}
+ styleProvider={returnEmptyString}
+ whenChildContentsActiveChanged={emptyFunction}
+ bringToFront={emptyFunction}
+ childFilters={returnEmptyFilter}
+ childFiltersByRanges={returnEmptyFilter}
+ searchFilterDocs={returnEmptyDoclist}
+ />
+ <div className="palette-cover" style={{ transform: `translate(${Math.max(0, this._selectedIndex) * 50.75 + 23}px, 0px)` }}></div>
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/PreviewCursor.scss b/src/client/views/PreviewCursor.scss
index 7be765ea9..82488c750 100644
--- a/src/client/views/PreviewCursor.scss
+++ b/src/client/views/PreviewCursor.scss
@@ -1,4 +1,3 @@
-.previewCursor-Dark,
.previewCursor {
color: black;
position: absolute;
@@ -9,7 +8,3 @@
opacity: 1;
z-index: 1001;
}
-
-.previewCursor-Dark {
- color: white;
-}
diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx
index 82d2bff56..1d88e9ad6 100644
--- a/src/client/views/PreviewCursor.tsx
+++ b/src/client/views/PreviewCursor.tsx
@@ -1,9 +1,9 @@
import { action, observable, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { Doc } from '../../fields/Doc';
+import { Doc, Opt } from '../../fields/Doc';
import { StrCast } from '../../fields/Types';
-import { returnFalse } from '../../Utils';
+import { lightOrDark, returnFalse } from '../../Utils';
import { Docs, DocumentOptions, DocUtils } from '../documents/Documents';
import { ImageUtils } from '../util/Import & Export/ImageUtils';
import { Transform } from '../util/Transform';
@@ -21,6 +21,7 @@ export class PreviewCursor extends React.Component<{}> {
static _slowLoadDocuments?: (files: File[] | string, options: DocumentOptions, generatedDocuments: Doc[], text: string, completed: ((doc: Doc[]) => void) | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => Promise<void>;
@observable static _clickPoint = [0, 0];
@observable public static Visible = false;
+ public static Doc: Opt<Doc>;
constructor(props: any) {
super(props);
document.addEventListener('keydown', this.onKeyPress);
@@ -178,7 +179,12 @@ export class PreviewCursor extends React.Component<{}> {
}
render() {
return !PreviewCursor._clickPoint || !PreviewCursor.Visible ? null : (
- <div className={`previewCursor${StrCast(Doc.ActiveDashboard?.colorScheme)}`} onBlur={this.onBlur} tabIndex={0} ref={e => e?.focus()} style={{ transform: `translate(${PreviewCursor._clickPoint[0]}px, ${PreviewCursor._clickPoint[1]}px)` }}>
+ <div
+ className="previewCursor"
+ onBlur={this.onBlur}
+ tabIndex={0}
+ ref={e => e?.focus()}
+ style={{ color: lightOrDark(PreviewCursor.Doc?.backgroundColor ?? 'white'), transform: `translate(${PreviewCursor._clickPoint[0]}px, ${PreviewCursor._clickPoint[1]}px)` }}>
I
</div>
);
diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx
index 2e3668268..40d42a4de 100644
--- a/src/client/views/PropertiesButtons.tsx
+++ b/src/client/views/PropertiesButtons.tsx
@@ -32,9 +32,7 @@ import { TfiBarChart } from 'react-icons/tfi';
import { CiGrid31 } from 'react-icons/ci';
import { RxWidth } from 'react-icons/rx';
import { Dropdown, DropdownType, IListItemProps, Toggle, ToggleType, Type } from 'browndash-components';
-const higflyout = require('@hig/flyout');
-export const { anchorPoints } = higflyout;
-export const Flyout = higflyout.default;
+import { SettingsManager } from '../util/SettingsManager';
enum UtilityButtonState {
Default,
@@ -60,7 +58,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
<Toggle
toggleStatus={BoolCast(targetDoc[property])}
text={label(targetDoc?.[property])}
- color={StrCast(Doc.UserDoc().userColor)}
+ color={SettingsManager.userColor}
icon={icon(targetDoc?.[property] as any)}
iconPlacement={'left'}
align={'flex-start'}
@@ -244,10 +242,10 @@ export class PropertiesButtons extends React.Component<{}, {}> {
@computed get layout_autoHeightButton() {
// store previous dimensions to store old values
return this.propertyToggleBtn(
- on => 'Auto\xA0Size',
+ on => (on ? 'AUTO\xA0SIZE' : 'FIXED SIZE'),
'_layout_autoHeight',
on => `Automatical vertical sizing to show all content`,
- on => 'arrows-alt-v'
+ on => <FontAwesomeIcon icon="arrows-alt-v" size="lg" />
);
}
@@ -375,15 +373,15 @@ export class PropertiesButtons extends React.Component<{}, {}> {
val: value[1],
};
});
- console.log('click val: ', this.onClickVal);
return !this.selectedDoc ? null : (
<Dropdown
tooltip={'Choose onClick behavior'}
items={items}
+ closeOnSelect={true}
selectedVal={this.onClickVal}
setSelectedVal={val => this.handleOptionChange(val as string)}
title={'Choose onClick behaviour'}
- color={StrCast(Doc.UserDoc().userColor)}
+ color={SettingsManager.userColor}
dropdownType={DropdownType.SELECT}
type={Type.SEC}
fillWidth
@@ -515,7 +513,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
{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.chromeButton, { display: isNovice ? 'none' : '' })}
{toggle(this.gridButton, { display: !isCollection ? 'none' : '' })}
{/* {toggle(this.groupButton, { display: isTabView || !isCollection ? 'none' : '' })} */}
{toggle(this.snapButton, { display: !isCollection ? 'none' : '' })}
diff --git a/src/client/views/PropertiesDocBacklinksSelector.tsx b/src/client/views/PropertiesDocBacklinksSelector.tsx
index da4438481..b1e99c3f5 100644
--- a/src/client/views/PropertiesDocBacklinksSelector.tsx
+++ b/src/client/views/PropertiesDocBacklinksSelector.tsx
@@ -1,3 +1,4 @@
+import { action } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { Doc } from '../../fields/Doc';
@@ -5,6 +6,8 @@ import { Cast } from '../../fields/Types';
import { DocumentType } from '../documents/DocumentTypes';
import { LinkManager } from '../util/LinkManager';
import { SelectionManager } from '../util/SelectionManager';
+import { SettingsManager } from '../util/SettingsManager';
+import { CollectionDockingView } from './collections/CollectionDockingView';
import { LinkMenu } from './linking/LinkMenu';
import { OpenWhere, OpenWhereMod } from './nodes/DocumentView';
import './PropertiesDocBacklinksSelector.scss';
@@ -18,20 +21,21 @@ type PropertiesDocBacklinksSelectorProps = {
@observer
export class PropertiesDocBacklinksSelector extends React.Component<PropertiesDocBacklinksSelectorProps> {
- getOnClick = (link: Doc) => {
+ getOnClick = action((link: Doc) => {
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;
-
+ LinkManager.currentLink = link;
if (otherdoc) {
otherdoc.hidden = false;
this.props.addDocTab(Doc.IsDataProto(otherdoc) ? Doc.MakeDelegate(otherdoc) : otherdoc, OpenWhere.toggleRight);
+ CollectionDockingView.Instance?.endUndoBatch();
}
- };
+ });
render() {
return !SelectionManager.Views().length ? null : (
- <div className="propertiesDocBacklinksSelector">
+ <div className="propertiesDocBacklinksSelector" style={{ color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }}>
{this.props.hideTitle ? null : <p key="contexts">Contexts:</p>}
<LinkMenu docView={SelectionManager.Views().lastElement()} clearLinkEditor={undefined} itemHandler={this.getOnClick} style={{ left: 0, top: 0 }} />
</div>
diff --git a/src/client/views/PropertiesDocContextSelector.tsx b/src/client/views/PropertiesDocContextSelector.tsx
index 395aa2b61..d157e7b1c 100644
--- a/src/client/views/PropertiesDocContextSelector.tsx
+++ b/src/client/views/PropertiesDocContextSelector.tsx
@@ -38,7 +38,6 @@ export class PropertiesDocContextSelector extends React.Component<PropertiesDocC
}, new Set<Doc>())
.keys()
);
- console.log('embeddings ' + embeddings.length);
return doclayouts
.filter(doc => !Doc.AreProtosEqual(doc, CollectionDockingView.Instance?.props.Document))
diff --git a/src/client/views/PropertiesSection.tsx b/src/client/views/PropertiesSection.tsx
index ec8043ffe..bd586b2f9 100644
--- a/src/client/views/PropertiesSection.tsx
+++ b/src/client/views/PropertiesSection.tsx
@@ -1,66 +1,66 @@
import React = require('react');
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
-import { action, computed, observable } from "mobx"
-import { observer } from "mobx-react"
-import './PropertiesSection.scss'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { action, computed, observable } from 'mobx';
+import { observer } from 'mobx-react';
+import './PropertiesSection.scss';
import { Doc } from '../../fields/Doc';
import { StrCast } from '../../fields/Types';
+import { SettingsManager } from '../util/SettingsManager';
export interface PropertiesSectionProps {
- title: string,
- content?: JSX.Element | string | null,
- isOpen: boolean,
- setIsOpen: (bool: boolean) => any
- inSection?: boolean,
- setInSection?: (bool: boolean) => any
- onDoubleClick?: () => void
+ title: string;
+ children?: JSX.Element | string | null;
+ isOpen: boolean;
+ setIsOpen: (bool: boolean) => any;
+ inSection?: boolean;
+ setInSection?: (bool: boolean) => any;
+ onDoubleClick?: () => void;
}
@observer
export class PropertiesSection extends React.Component<PropertiesSectionProps> {
@computed get color() {
- return StrCast(Doc.UserDoc().userColor);
+ return SettingsManager.userColor;
}
@computed get backgroundColor() {
- return StrCast(Doc.UserDoc().userBackgroundColor);
+ return SettingsManager.userBackgroundColor;
}
@computed get variantColor() {
- return StrCast(Doc.UserDoc().userVariantColor);
+ return SettingsManager.userVariantColor;
}
@observable isDouble: boolean = false;
render() {
- console.log(this.props.title, this.props.content)
- if (this.props.content === undefined || this.props.content === null) return null
- else return <div className="propertiesView-section" onPointerEnter={action(() => (this.props.setInSection && this.props.setInSection(true)))} onPointerLeave={action(() => (this.props.setInSection && this.props.setInSection(false)))}>
- <div className="propertiesView-sectionTitle"
- onDoubleClick={action((e) => {
- this.isDouble = true;
- this.props.onDoubleClick && this.props.onDoubleClick()
- console.log("open options")
- this.props.setIsOpen(true)
- setTimeout(() => this.isDouble = false, 300)
- })}
- onClick={action((e) => {
- this.props.setIsOpen(!this.props.isOpen)
- })}
- style={{
- background: this.props.isOpen ? this.variantColor : this.backgroundColor,
- color: this.color
- }}>
- {this.props.title}
- <div className="propertiesView-sectionTitle-icon">
- <FontAwesomeIcon icon={this.props.isOpen ? 'caret-down' : 'caret-right'} size="lg" />
- </div>
- </div>
- {!this.props.isOpen ? null :
- <div className="propertiesView-content">
- {this.props.content}
- </div>
- }
- </div>
+ if (this.props.children === undefined || this.props.children === null) return null;
+ else
+ return (
+ <div className="propertiesView-section" onPointerEnter={action(() => this.props.setInSection && this.props.setInSection(true))} onPointerLeave={action(() => this.props.setInSection && this.props.setInSection(false))}>
+ <div
+ className="propertiesView-sectionTitle"
+ onDoubleClick={action(e => {
+ this.isDouble = true;
+ this.props.onDoubleClick && this.props.onDoubleClick();
+ this.props.setIsOpen(true);
+ setTimeout(() => (this.isDouble = false), 300);
+ })}
+ onClick={action(e => {
+ this.props.setIsOpen(!this.props.isOpen);
+ })}
+ style={{
+ background: this.variantColor,
+ // this.props.isOpen ? this.variantColor : this.backgroundColor,
+ color: this.color,
+ }}>
+ {this.props.title}
+ <div className="propertiesView-sectionTitle-icon">
+ <FontAwesomeIcon icon={this.props.isOpen ? 'caret-down' : 'caret-right'} size="lg" />
+ </div>
+ </div>
+ {!this.props.isOpen ? null : <div className="propertiesView-content">{this.props.children}</div>}
+ </div>
+ );
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/PropertiesView.scss b/src/client/views/PropertiesView.scss
index b79486167..2da2ec568 100644
--- a/src/client/views/PropertiesView.scss
+++ b/src/client/views/PropertiesView.scss
@@ -1,5 +1,12 @@
@import './global/globalCssVariables.scss';
+.propertiesView-presentationTrails-title {
+ display: flex;
+}
+.propertiesView-presentationTrails-title-icon {
+ position: absolute;
+ right: 4;
+}
.propertiesView {
height: 100%;
width: 250;
@@ -16,6 +23,14 @@
padding: 10px;
font-size: 24px;
font-weight: bold;
+ display: flex;
+ flex-direction: row;
+ }
+ .propertiesView-titleExtender {
+ text-overflow: ellipsis;
+ max-width: 100%;
+ white-space: pre;
+ overflow: hidden;
}
overflow-x: hidden;
@@ -26,36 +41,47 @@
padding: 5px 10px;
}
+ .propertiesView-propAndInfoGrouping {
+ display: flex;
+ }
+
+ .propertiesView-info {
+ margin-top: -5;
+ float: right;
+ font-size: 20;
+ path {
+ fill: white !important;
+ }
+ }
.propertiesView-sharing {
//border-bottom: 1px solid black;
//padding: 8.5px;
+ .propertiesView-buttonContainer {
+ float: right;
+ display: flex;
- .propertiesView-buttonContainer {
- float: right;
- display: flex;
-
- button {
- width: 15;
- height: 15;
- padding: 0;
- margin-top: -5;
- }
+ button {
+ width: 15;
+ height: 15;
+ padding: 0;
+ margin-top: -5;
}
+ }
- .change-buttons {
- display: flex;
+ .change-buttons {
+ display: flex;
- button {
- width: 5;
- height: 5;
- }
+ button {
+ width: 5;
+ height: 5;
+ }
- input {
- width: 100%;
- }
+ input {
+ width: 100%;
}
+ }
}
.propertiesView-acls-checkbox {
@@ -63,11 +89,11 @@
margin-left: 50px;
}
- .propertiesView-shareDropDown{
+ .propertiesView-shareDropDown {
margin-right: 10px;
min-width: 65px;
-
- & .propertiesView-shareDropDownNone{
+
+ & .propertiesView-shareDropDownNone {
padding: 0px;
padding-left: 3px;
padding-right: 3px;
@@ -77,7 +103,7 @@
border: 1px solid rgb(71, 71, 71);
}
& .propertiesView-shareDropDownEdit,
- .propertiesView-shareDropDownAdmin{
+ .propertiesView-shareDropDownAdmin {
padding: 0px;
padding-left: 3px;
padding-right: 3px;
@@ -86,17 +112,16 @@
border-radius: 6px;
border: 1px solid rgb(75, 75, 5);
}
- & .propertiesView-shareDropDownAugment{
+ & .propertiesView-shareDropDownAugment {
padding: 0px;
padding-left: 3px;
padding-right: 3px;
background: rgb(208, 255, 208);
- color:rgb(19, 80, 19);
+ color: rgb(19, 80, 19);
border-radius: 6px;
border: 1px solid rgb(19, 80, 19);
-
}
- & .propertiesView-shareDropDownView{
+ & .propertiesView-shareDropDownView {
padding: 0px;
padding-left: 3px;
padding-right: 3px;
@@ -105,7 +130,7 @@
border-radius: 6px;
border: 1px solid rgb(25, 25, 101);
}
- & .propertiesView-shareDropDownNot-Shared{
+ & .propertiesView-shareDropDownNot-Shared {
padding: 0px;
padding-left: 3px;
padding-right: 3px;
@@ -245,15 +270,12 @@
cursor: auto;
}
}
+}
- }
-
- .propertiesView-presTrails {
- //border-bottom: 1px solid black;
- //padding: 8.5px;
-
-
- }
+//.propertiesView-presentationTrails {
+//border-bottom: 1px solid black;
+//padding: 8.5px;
+//}
.inking-button {
display: flex;
@@ -515,9 +537,9 @@
}
}
-.propertiesView-wordTitle{
- color:darkslategray;
- font-weight:200;
+.propertiesView-wordTitle {
+ color: darkslategray;
+ font-weight: 200;
}
.editable-title {
@@ -532,15 +554,15 @@
}
}
-.propertiesView-wordType{
- color:darkslategray;
- font-weight:200;
+.propertiesView-wordType {
+ color: darkslategray;
+ font-weight: 200;
}
-.currentType{
+.currentType {
text-decoration: underline;
display: flex;
- align-items:center;
+ align-items: center;
// border: solid 1px #323232;
// padding-left: 5px;
// padding-top: 4px;
@@ -548,8 +570,8 @@
// height: fit-content;
}
-.currentType-icon{
- margin-right:5px;
+.currentType-icon {
+ margin-right: 5px;
}
.properties-flyout {
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 27b9c3c7a..04a948a27 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -3,14 +3,12 @@ import { IconLookup } from '@fortawesome/fontawesome-svg-core';
import { faAnchor, faArrowRight, faWindowMaximize } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Checkbox, Tooltip } from '@material-ui/core';
-import { Button, Colors, EditableText, IconButton, NumberInput, Size, Slider, Type } from 'browndash-components';
+import { Colors, EditableText, IconButton, NumberInput, Size, Slider, Type } from 'browndash-components';
import { concat } from 'lodash';
-import { Lambda, action, computed, observable } from 'mobx';
+import { action, computed, IReactionDisposer, observable, reaction, trace } from 'mobx';
import { observer } from 'mobx-react';
import { ColorState, SketchPicker } from 'react-color';
-import * as Icons from "react-icons/bs"; //{BsCollectionFill, BsFillFileEarmarkImageFill} from "react-icons/bs"
-import { GrCircleInformation } from 'react-icons/gr';
-import { Utils, emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, setupMoveUpEvents } from '../../Utils';
+import * as Icons from 'react-icons/bs'; //{BsCollectionFill, BsFillFileEarmarkImageFill} from "react-icons/bs"
import { Doc, DocListCast, Field, FieldResult, HierarchyMapping, NumListCast, Opt, ReverseHierarchyMap, StrListCast } from '../../fields/Doc';
import { AclAdmin, DocAcl, DocData, Height, Width } from '../../fields/DocSymbols';
import { Id } from '../../fields/FieldSymbols';
@@ -18,30 +16,29 @@ import { InkField } from '../../fields/InkField';
import { List } from '../../fields/List';
import { ComputedField } from '../../fields/ScriptField';
import { Cast, DocCast, NumCast, StrCast } from '../../fields/Types';
-import { GetEffectiveAcl, SharingPermissions, normalizeEmail } from '../../fields/util';
-import { DocumentType } from '../documents/DocumentTypes';
+import { GetEffectiveAcl, normalizeEmail, SharingPermissions } from '../../fields/util';
+import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, setupMoveUpEvents, Utils } from '../../Utils';
+import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
import { DocumentManager } from '../util/DocumentManager';
import { GroupManager } from '../util/GroupManager';
import { LinkManager } from '../util/LinkManager';
import { SelectionManager } from '../util/SelectionManager';
+import { SettingsManager } from '../util/SettingsManager';
import { SharingManager } from '../util/SharingManager';
import { Transform } from '../util/Transform';
-import { UndoManager, undoBatch, undoable } from '../util/UndoManager';
+import { undoable, undoBatch, UndoManager } from '../util/UndoManager';
import { EditableView } from './EditableView';
import { FilterPanel } from './FilterPanel';
import { InkStrokeProperties } from './InkStrokeProperties';
+import { DocumentView, OpenWhere, StyleProviderFunc } from './nodes/DocumentView';
+import { KeyValueBox } from './nodes/KeyValueBox';
+import { PresBox, PresEffect, PresEffectDirection } from './nodes/trails';
import { PropertiesButtons } from './PropertiesButtons';
import { PropertiesDocBacklinksSelector } from './PropertiesDocBacklinksSelector';
import { PropertiesDocContextSelector } from './PropertiesDocContextSelector';
import { PropertiesSection } from './PropertiesSection';
import './PropertiesView.scss';
import { DefaultStyleProvider } from './StyleProvider';
-import { DocumentView, OpenWhere, StyleProviderFunc } from './nodes/DocumentView';
-import { KeyValueBox } from './nodes/KeyValueBox';
-import { PresBox, PresEffect, PresEffectDirection } from './nodes/trails';
-const higflyout = require('@hig/flyout');
-export const { anchorPoints } = higflyout;
-export const Flyout = higflyout.default;
const _global = (window /* browser */ || global) /* node */ as any;
interface PropertiesViewProps {
@@ -51,11 +48,16 @@ interface PropertiesViewProps {
addDocTab: (doc: Doc, where: OpenWhere) => boolean;
}
-
@observer
export class PropertiesView extends React.Component<PropertiesViewProps> {
private _widthUndo?: UndoManager.Batch;
+ public static Instance: PropertiesView | undefined;
+ constructor(props: any) {
+ super(props);
+ PropertiesView.Instance = this;
+ }
+
@computed get MAX_EMBED_HEIGHT() {
return 200;
}
@@ -79,6 +81,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
}
@observable layoutFields: boolean = false;
+ @observable layoutDocAcls: boolean = false;
@observable openOptions: boolean = true;
@observable openSharing: boolean = true;
@@ -90,15 +93,6 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
@observable openTransform: boolean = true;
@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
- */
- private selectedDocListenerDisposer: Opt<Lambda>;
-
- // @observable selectedUser: string = "";
- // @observable addButtonPressed: boolean = false;
- @observable layoutDocAcls: boolean = false;
-
//Pres Trails booleans:
@observable openPresTransitions: boolean = true;
@observable openPresProgressivize: boolean = false;
@@ -108,19 +102,29 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
@observable inOptions: boolean = false;
@observable _controlButton: boolean = false;
+ private _disposers: { [name: string]: IReactionDisposer } = {};
componentDidMount() {
- this.selectedDocListenerDisposer?.();
- // this.selectedDocListenerDisposer = autorun(() => this.openFilters && this.selectedDoc && this.checkFilterDoc());
+ this._disposers.link = reaction(
+ () => LinkManager.currentLink,
+ link => {
+ link && this.CloseAll();
+ link && (this.openLinks = true);
+ },
+ { fireImmediately: true }
+ );
}
componentWillUnmount() {
- this.selectedDocListenerDisposer?.();
+ Object.values(this._disposers).forEach(disposer => disposer?.());
}
@computed get isInk() {
return this.selectedDoc?.type === DocumentType.INK;
}
+ @computed get isStack() {
+ return [CollectionViewType.Stacking, CollectionViewType.NoteTaking].includes(this.selectedDoc?.type_collection as any);
+ }
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);
@@ -191,7 +195,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
});
rows.push(
- <div className="propertiesView-field" key="newKeyValue" style={{ marginTop: '3px', backgroundColor: "white", textAlign: "center" }}>
+ <div className="propertiesView-field" key="newKeyValue" style={{ marginTop: '3px', backgroundColor: SettingsManager.userBackgroundColor, textAlign: 'center' }}>
<EditableView key="editableView" oneLine contents={'add key:value or #tags'} height={13} fontSize={10} GetValue={() => ''} SetValue={this.setKeyValue} />
</div>
);
@@ -249,14 +253,12 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
return !this.selectedDoc ? null : <PropertiesDocContextSelector DocView={this.selectedDocumentView} hideTitle={true} addDocTab={this.props.addDocTab} />;
}
- @computed get contextCount(){
- console.log("in context count");
- if (this.selectedDocumentView){
- const target = (this.selectedDocumentView.props.Document)
- const embeddings = DocListCast(target.proto_embeddings)
- console.log(embeddings.length -1 );
- return (embeddings.length - 1)
- } else{
+ @computed get contextCount() {
+ if (this.selectedDocumentView) {
+ const target = this.selectedDocumentView.props.Document;
+ const embeddings = DocListCast(target.proto_embeddings);
+ return embeddings.length - 1;
+ } else {
return 0;
}
}
@@ -266,13 +268,11 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
return !selAnchor ? null : <PropertiesDocBacklinksSelector Document={selAnchor} hideTitle={true} addDocTab={this.props.addDocTab} />;
}
- @computed get linkCount(){
- const selAnchor = this.selectedDocumentView?.anchorViewDoc ?? LinkManager.currentLinkAnchor ?? this.selectedDoc;
+ @computed get linkCount() {
+ const selAnchor = this.selectedDocumentView?.anchorViewDoc ?? LinkManager.currentLinkAnchor ?? this.selectedDoc;
var counter = 0;
- LinkManager.Links(selAnchor).forEach((l, i) =>
- counter ++
- );
+ LinkManager.Links(selAnchor).forEach((l, i) => counter++);
return counter;
}
@@ -356,7 +356,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
return (
<Tooltip title={<div className="dash-tooltip">Notify with message</div>}>
<div className="notify-button">
- <FontAwesomeIcon className="notify-button-icon" icon="bell" size="sm" />
+ <FontAwesomeIcon className="notify-button-icon" icon="bell" size="sm" />
</div>
</Tooltip>
);
@@ -367,16 +367,16 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
*/
@computed get expansionIcon() {
return (
- <div className="expansion-button" >
- <IconButton
- icon={<FontAwesomeIcon icon={'ellipsis-h'} />}
- size={Size.XSMALL}
- color={StrCast(Doc.UserDoc().userColor)}
+ <div className="expansion-button">
+ <IconButton
+ icon={<FontAwesomeIcon icon={'ellipsis-h'} />}
+ size={Size.XSMALL}
+ color={SettingsManager.userColor}
onClick={action(() => {
if (this.selectedDocumentView || this.selectedDoc) {
SharingManager.Instance.open(this.selectedDocumentView?.props.Document === this.selectedDoc ? this.selectedDocumentView : undefined, this.selectedDoc);
}
- })}
+ })}
/>
</div>
);
@@ -418,7 +418,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
<div>
<div className={'propertiesView-shareDropDown'}>
<div className={`propertiesView-shareDropDown${permission}`}>
- <div >{admin && permission !== 'Owner' ? this.getPermissionsSelect(name, permission, showGuestOptions) : concat(shareImage, ' ', permission)}</div>
+ <div>{admin && permission !== 'Owner' ? this.getPermissionsSelect(name, permission, showGuestOptions) : concat(shareImage, ' ', permission)}</div>
</div>
</div>
</div>
@@ -450,7 +450,6 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
const target = docs[0];
const showAdmin = GetEffectiveAcl(target) == AclAdmin;
- console.log(GetEffectiveAcl(target), Doc.GetProto(target)[`acl-${normalizeEmail(Doc.CurrentUserEmail)}`])
const individualTableEntries = [];
const usersAdded: string[] = []; // all shared users being added - organized by denormalized email
@@ -522,7 +521,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
<div>
<br></br> Individuals with Access to this Document
</div>
- <div className="propertiesView-sharingTable" style={{background: StrCast(Doc.UserDoc().userBackgroundColor), color: StrCast(Doc.UserDoc().userColor)}}>
+ <div className="propertiesView-sharingTable" style={{ background: SettingsManager.userBackgroundColor, color: SettingsManager.userColor }}>
{<div> {individualTableEntries}</div>}
</div>
{groupTableEntries.length > 0 ? (
@@ -530,7 +529,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
<div>
<br></br> Groups with Access to this Document
</div>
- <div className="propertiesView-sharingTable" style={{background: StrCast(Doc.UserDoc().userBackgroundColor), color: StrCast(Doc.UserDoc().userColor)}}>
+ <div className="propertiesView-sharingTable" style={{ background: SettingsManager.userBackgroundColor, color: SettingsManager.userColor }}>
{<div> {groupTableEntries}</div>}
</div>
</div>
@@ -550,80 +549,75 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
toggleCheckbox = () => (this.layoutFields = !this.layoutFields);
@computed get color() {
- return StrCast(Doc.UserDoc().userColor);
+ return SettingsManager.userColor;
}
@computed get backgroundColor() {
- return StrCast(Doc.UserDoc().userBackgroundColor);
+ return SettingsManager.userBackgroundColor;
}
@computed get variantColor() {
- return StrCast(Doc.UserDoc().userVariantColor);
+ return SettingsManager.userVariantColor;
}
@computed get editableTitle() {
-
const titles = new Set<string>();
- const title = Array.from(titles.keys()).length > 1 ? '--multiple selected--' : StrCast(this.selectedDoc?.title);
SelectionManager.Views().forEach(dv => titles.add(StrCast(dv.rootDoc.title)));
+ const title = Array.from(titles.keys()).length > 1 ? '--multiple selected--' : StrCast(this.selectedDoc?.title);
return (
- <EditableText
- val={title}
- setVal={this.setTitle}
- color={this.color}
- type={Type.SEC}
- formLabel={"Title"}
- fillWidth
- />
+ <div>
+ <EditableText val={title} setVal={this.setTitle} color={this.color} type={Type.SEC} formLabel={'Title'} fillWidth />
+ {LinkManager.currentLinkAnchor ? (
+ <p className="propertiesView-titleExtender">
+ <>
+ <b>Anchor:</b>
+ {LinkManager.currentLinkAnchor.title}
+ </>
+ </p>
+ ) : null}
+ {LinkManager.currentLink?.title ? (
+ <p className="propertiesView-titleExtender">
+ <>
+ <b>Link:</b>
+ {LinkManager.currentLink.title}
+ </>
+ </p>
+ ) : null}
+ </div>
);
}
@computed get currentType() {
- // console.log("current type " + this.selectedDoc?.type)
-
- const documentType = StrCast(this.selectedDoc?.type)
- var currentType: string = documentType;
- var capitalizedDocType = Utils.cleanDocumentType(currentType as DocumentType);
-
- return (
- <div>
- <div className = "propertiesView-wordType">Type</div>
- <div className= "currentType">
- <div className='currentType-icon'>
- {this.currentComponent}
- </div>
-
- {capitalizedDocType}
-
- </div>
-
- </div>
-
- )
- }
+ const documentType = StrCast(this.selectedDoc?.type);
+ var currentType: string = documentType;
+ var capitalizedDocType = Utils.cleanDocumentType(currentType as DocumentType);
- @computed get currentComponent() {
+ return (
+ <div>
+ Type
+ {/* <div className = "propertiesView-wordType">Type</div> */}
+ <div className="currentType">
+ <div className="currentType-icon">{this.currentComponent}</div>
- var iconName = StrCast(this.selectedDoc?.systemIcon)
+ {capitalizedDocType}
+ </div>
+ </div>
+ );
+ }
- // if (this.selectedDoc?.type === DocumentType.COL){
- // console.log("i did it!")
- // }
-
+ @computed get currentComponent() {
+ var iconName = StrCast(this.selectedDoc?.systemIcon);
- if (iconName){
+ if (iconName) {
const Icon = Icons[iconName as keyof typeof Icons];
return <Icon />;
- } else{
- return <Icons.BsFillCollectionFill/>
-
}
+ return <Icons.BsFillCollectionFill />;
}
@undoBatch
@action
setTitle = (value: string | number) => {
- console.log(value)
if (SelectionManager.Views().length > 1) {
SelectionManager.Views().map(dv => Doc.SetInPlace(dv.rootDoc, 'title', value, true));
} else if (this.dataDoc) {
@@ -687,7 +681,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
className="inking-button-points"
style={{ backgroundColor: InkStrokeProperties.Instance._controlButton ? 'black' : '' }}
onPointerDown={action(() => (InkStrokeProperties.Instance._controlButton = !InkStrokeProperties.Instance._controlButton))}>
- <FontAwesomeIcon icon="bezier-curve" size="lg" />
+ <FontAwesomeIcon icon="bezier-curve" size="lg" />
</div>
</Tooltip>
</div>
@@ -703,13 +697,20 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
marginLeft: title === '∠:' ? '39px' : '',
}}>
<div className="inputBox-title"> {title} </div>
- <input className="inputBox-input" type="text" value={value} onChange={e => setter(e.target.value)} onKeyPress={e => e.stopPropagation()} />
+ <input
+ className="inputBox-input"
+ type="text"
+ value={value}
+ style={{ color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }}
+ onChange={e => setter(e.target.value)}
+ onKeyPress={e => e.stopPropagation()}
+ />
<div className="inputBox-button">
<div className="inputBox-button-up" key="up2" onPointerDown={undoBatch(action(() => this.upDownButtons('up', key)))}>
- <FontAwesomeIcon icon="caret-up" size="sm" />
+ <FontAwesomeIcon icon="caret-up" size="sm" />
</div>
<div className="inputbox-Button-down" key="down2" onPointerDown={undoBatch(action(() => this.upDownButtons('down', key)))}>
- <FontAwesomeIcon icon="caret-down" size="sm" />
+ <FontAwesomeIcon icon="caret-down" size="sm" />
</div>
</div>
</div>
@@ -782,39 +783,39 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
}
@computed get shapeXps() {
- return this.getField('x');
+ return NumCast(this.selectedDoc?.x);
}
@computed get shapeYps() {
- return this.getField('y');
+ return NumCast(this.selectedDoc?.y);
}
@computed get shapeHgt() {
- return this.getField('_height');
+ return NumCast(this.selectedDoc?._height);
}
@computed get shapeWid() {
- return this.getField('_width');
+ return NumCast(this.selectedDoc?._width);
}
set shapeXps(value) {
- this.selectedDoc && (this.selectedDoc.x = Number(value));
+ this.selectedDoc && (this.selectedDoc.x = Math.round(value * 100) / 100);
}
set shapeYps(value) {
- this.selectedDoc && (this.selectedDoc.y = Number(value));
+ this.selectedDoc && (this.selectedDoc.y = Math.round(value * 100) / 100);
}
set shapeWid(value) {
- this.selectedDoc && (this.selectedDoc._width = Number(value));
+ this.selectedDoc && (this.selectedDoc._width = Math.round(value * 100) / 100);
}
set shapeHgt(value) {
- this.selectedDoc && (this.selectedDoc._height = Number(value));
+ this.selectedDoc && (this.selectedDoc._height = Math.round(value * 100) / 100);
}
@computed get hgtInput() {
return this.inputBoxDuo(
'hgt',
this.shapeHgt,
- undoable((val: string) => !isNaN(Number(val)) && (this.shapeHgt = val), 'set height'),
+ undoable((val: string) => !isNaN(Number(val)) && (this.shapeHgt = +val), 'set height'),
'H:',
'wid',
this.shapeWid,
- undoable((val: string) => !isNaN(Number(val)) && (this.shapeWid = val), 'set width'),
+ undoable((val: string) => !isNaN(Number(val)) && (this.shapeWid = +val), 'set width'),
'W:'
);
}
@@ -822,11 +823,11 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
return this.inputBoxDuo(
'Xps',
this.shapeXps,
- undoable((val: string) => val !== '0' && !isNaN(Number(val)) && (this.shapeXps = val), 'set x coord'),
+ undoable((val: string) => val !== '0' && !isNaN(Number(val)) && (this.shapeXps = +val), 'set x coord'),
'X:',
'Yps',
this.shapeYps,
- undoable((val: string) => val !== '0' && !isNaN(Number(val)) && (this.shapeYps = val), 'set y coord'),
+ undoable((val: string) => val !== '0' && !isNaN(Number(val)) && (this.shapeYps = +val), 'set y coord'),
'Y:'
);
}
@@ -960,13 +961,13 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
regInput = (key: string, value: any, setter: (val: string) => {}) => {
return (
<div className="inputBox">
- <input className="inputBox-input" type="text" value={value} onChange={e => setter(e.target.value)} />
+ <input className="inputBox-input" type="text" value={value} style={{ color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }} onChange={e => setter(e.target.value)} />
<div className="inputBox-button">
<div className="inputBox-button-up" key="up2" onPointerDown={undoBatch(action(() => this.upDownButtons('up', key)))}>
- <FontAwesomeIcon icon="caret-up" size="sm" />
+ <FontAwesomeIcon icon="caret-up" size="sm" />
</div>
<div className="inputbox-Button-down" key="down2" onPointerDown={undoBatch(action(() => this.upDownButtons('down', key)))}>
- <FontAwesomeIcon icon="caret-down" size="sm" />
+ <FontAwesomeIcon icon="caret-down" size="sm" />
</div>
</div>
</div>
@@ -974,7 +975,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
};
@action
- onDoubleClick = () => {
+ CloseAll = () => {
this.openContexts = false;
this.openLinks = false;
this.openOptions = false;
@@ -983,7 +984,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
this.openSharing = false;
this.openLayout = false;
this.openFilters = false;
- }
+ };
@computed get widthAndDash() {
return (
@@ -996,6 +997,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
<input
className="width-range"
type="range"
+ style={{ color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }}
defaultValue={Number(this.widthStk)}
min={1}
max={100}
@@ -1020,6 +1022,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
className="width-range"
type="range"
defaultValue={this.markScal}
+ style={{ color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }}
min={0}
max={10}
onChange={action(e => (this.markScal = +e.target.value))}
@@ -1064,107 +1067,76 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
);
}
- getNumber = (label: string, unit: string, min: number, max: number, number: number, setNumber: any) => {
- return <div>
- <NumberInput
- formLabel={label}
- formLabelPlacement={'left'}
- type={Type.SEC}
- unit={unit}
- fillWidth
- color={this.color}
- number={number}
- setNumber={setNumber}
- min={min}
- max={max}
- />
- <Slider
- multithumb={false}
- color={this.color}
- size={Size.XSMALL}
- min={min}
- max={max}
- unit={unit}
- number={number}
- setNumber={setNumber}
- fillWidth
- />
- </div>
- }
+ _sliderBatch: UndoManager.Batch | undefined;
+ setFinalNumber = () => {
+ this._sliderBatch?.end();
+ };
+ getNumber = (label: string, unit: string, min: number, max: number, number: number, setNumber: any, autorange?: number, autorangeMinVal?: number) => {
+ return (
+ <div key={label + this.selectedDoc?.title}>
+ <NumberInput formLabel={label} formLabelPlacement={'left'} type={Type.SEC} unit={unit} fillWidth color={this.color} number={number} setNumber={setNumber} min={min} max={max} />
+ <Slider
+ key={label}
+ onPointerDown={e => (this._sliderBatch = UndoManager.StartBatch('slider ' + label))}
+ multithumb={false}
+ color={this.color}
+ size={Size.XSMALL}
+ min={min}
+ max={max}
+ autorangeMinVal={autorangeMinVal}
+ autorange={autorange}
+ number={number}
+ unit={unit}
+ decimals={1}
+ setFinalNumber={this.setFinalNumber}
+ setNumber={setNumber}
+ fillWidth
+ />
+ </div>
+ );
+ };
@computed get transformEditor() {
return (
<div className="transform-editor">
+ {!this.isStack ? null : this.getNumber('Gap', ' px', 0, 200, NumCast(this.selectedDoc!.gridGap), (val: number) => !isNaN(val) && (this.selectedDoc!.gridGap = val))}
+ {!this.isStack ? null : this.getNumber('xMargin', ' px', 0, 500, NumCast(this.selectedDoc!.xMargin), (val: number) => !isNaN(val) && (this.selectedDoc!.xMargin = val))}
{this.isInk ? this.controlPointsButton : null}
- {this.getNumber(
- "Height",
- " px",
- 0,
- 1000,
- Number(this.shapeHgt),
- undoable((val: string) => !isNaN(Number(val)) && (this.shapeHgt = val), 'set height')
- )}
- {this.getNumber(
- "Width",
- " px",
- 0,
- 1000,
- Number(this.shapeWid),
- undoable((val: string) => !isNaN(Number(val)) && (this.shapeWid = val), 'set width')
- )}
- {this.getNumber(
- "X Coordinate",
- " px",
- -2000,
- 2000,
- Number(this.shapeXps),
- undoable((val: string) => !isNaN(Number(val)) && (this.shapeXps = val), 'set x coord')
- )}
- {this.getNumber(
- "Y Coordinate",
- " px",
- -2000,
- 2000,
- Number(this.shapeYps),
- undoable((val: string) => !isNaN(Number(val)) && (this.shapeYps = val), 'set y coord')
- )}
+ {this.getNumber('Width', ' px', 0, Math.max(1000, this.shapeWid), this.shapeWid, (val: number) => !isNaN(val) && (this.shapeWid = val), 1000, 1)}
+ {this.getNumber('Height', ' px', 0, Math.max(1000, this.shapeHgt), this.shapeHgt, (val: number) => !isNaN(val) && (this.shapeHgt = val), 1000, 1)}
+ {this.getNumber('X', ' px', this.shapeXps - 500, this.shapeXps + 500, this.shapeXps, (val: number) => !isNaN(val) && (this.shapeXps = val), 1000)}
+ {this.getNumber('Y', ' px', this.shapeYps - 500, this.shapeYps + 500, this.shapeYps, (val: number) => !isNaN(val) && (this.shapeYps = val), 1000)}
</div>
);
}
@computed get optionsSubMenu() {
- return <PropertiesSection
- title="Options"
- content={<PropertiesButtons />}
- inSection={this.inOptions}
- isOpen={this.openOptions}
- setInSection={(bool) => this.inOptions = bool}
- setIsOpen={(bool) => this.openOptions = bool}
- onDoubleClick={() => this.onDoubleClick()}
- />
+ return (
+ <PropertiesSection title="Options" inSection={this.inOptions} isOpen={this.openOptions} setInSection={bool => (this.inOptions = bool)} setIsOpen={bool => (this.openOptions = bool)} onDoubleClick={this.CloseAll}>
+ <PropertiesButtons />
+ </PropertiesSection>
+ );
}
@computed get sharingSubMenu() {
- return <PropertiesSection
- title="Sharing & Permissions"
- content={<>
- {/* <div className="propertiesView-buttonContainer"> */}
+ return (
+ <PropertiesSection title="Sharing and Permissions" isOpen={this.openSharing} setIsOpen={bool => (this.openSharing = bool)} onDoubleClick={() => this.CloseAll()}>
+ <>
+ {/* <div className="propertiesView-buttonContainer"> */}
<div className="propertiesView-acls-checkbox">
Layout Permissions
<Checkbox color="primary" onChange={action(() => (this.layoutDocAcls = !this.layoutDocAcls))} checked={this.layoutDocAcls} />
</div>
{/* <Tooltip title={<><div className="dash-tooltip">{"Re-distribute sharing settings"}</div></>}>
- <button onPointerDown={() => SharingManager.Instance.distributeOverCollection(this.selectedDoc!)}>
- <FontAwesomeIcon icon="redo-alt" size="1x" />
- </button>
+ <button onPointerDown={() => SharingManager.Instance.distributeOverCollection(this.selectedDoc!)}>
+ <FontAwesomeIcon icon="redo-alt" size="1x" />
+ </button>
</Tooltip> */}
- {/* </div> */}
- {this.sharingTable}
- </>}
- isOpen={this.openSharing}
- setIsOpen={(bool) => this.openSharing = bool}
- onDoubleClick={() => this.onDoubleClick()}
- />
+ {/* </div> */}
+ {this.sharingTable}
+ </>
+ </PropertiesSection>
+ );
}
/**
@@ -1192,15 +1164,13 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
};
@computed get filtersSubMenu() {
- return <PropertiesSection
- title="Filters"
- content={<div className="propertiesView-content filters" style={{ position: 'relative', height: 'auto' }}>
- <FilterPanel rootDoc={this.selectedDoc ?? Doc.ActiveDashboard!} />
- </div>}
- isOpen={this.openFilters}
- setIsOpen={(bool) => this.openFilters = bool}
- onDoubleClick={() => this.onDoubleClick()}
- />
+ return (
+ <PropertiesSection title="Filters" isOpen={this.openFilters} setIsOpen={bool => (this.openFilters = bool)} onDoubleClick={() => this.CloseAll()}>
+ <div className="propertiesView-content filters" style={{ position: 'relative', height: 'auto' }}>
+ <FilterPanel rootDoc={this.selectedDoc ?? Doc.ActiveDashboard!} />
+ </div>
+ </PropertiesSection>
+ );
}
@computed get inkSubMenu() {
@@ -1208,68 +1178,46 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
return (
<>
- <PropertiesSection
- title="Appearance"
- content={this.isInk ? this.appearanceEditor : null}
- isOpen={this.openAppearance}
- setIsOpen={(bool) => this.openAppearance = bool}
- onDoubleClick={() => this.onDoubleClick()}
- />
- <PropertiesSection
- title="Transform"
- content={this.transformEditor}
- isOpen={this.openTransform}
- setIsOpen={(bool) => this.openTransform = bool}
- onDoubleClick={() => this.onDoubleClick()}
- />
+ <PropertiesSection title="Appearance" isOpen={this.openAppearance} setIsOpen={bool => (this.openAppearance = bool)} onDoubleClick={() => this.CloseAll()}>
+ {this.isInk ? this.appearanceEditor : null}
+ </PropertiesSection>
+ <PropertiesSection title="Transform" isOpen={this.openTransform} setIsOpen={bool => (this.openTransform = bool)} onDoubleClick={() => this.CloseAll()}>
+ {this.transformEditor}
+ </PropertiesSection>
</>
);
}
@computed get fieldsSubMenu() {
- return <PropertiesSection
- title="Fields & Tags"
- content={<div className="propertiesView-content fields">{
- Doc.noviceMode ? this.noviceFields : this.expandedField}
- </div>}
- isOpen={this.openFields}
- setIsOpen={(bool) => this.openFields = bool}
- onDoubleClick={() => this.onDoubleClick()}
- />
+ return (
+ <PropertiesSection title="Fields & Tags" isOpen={this.openFields} setIsOpen={bool => (this.openFields = bool)} onDoubleClick={() => this.CloseAll()}>
+ <div className="propertiesView-content fields">{Doc.noviceMode ? this.noviceFields : this.expandedField}</div>
+ </PropertiesSection>
+ );
}
@computed get contextsSubMenu() {
- return <PropertiesSection
- title="Other Contexts"
- content={this.contextCount > 0 ? this.contexts : "There are no other contexts."}
- isOpen={this.openContexts}
- setIsOpen={(bool) => this.openContexts = bool}
- onDoubleClick={() => this.onDoubleClick()}
- />
+ return (
+ <PropertiesSection title="Other Contexts" isOpen={this.openContexts} setIsOpen={bool => (this.openContexts = bool)} onDoubleClick={() => this.CloseAll()}>
+ {this.contextCount > 0 ? this.contexts : 'There are no other contexts.'}
+ </PropertiesSection>
+ );
}
-
-
-
-
@computed get linksSubMenu() {
- return <PropertiesSection
- title="Linked To"
- content={this.linkCount > 0 ? this.links : "There are no current links." }
- isOpen={this.openLinks}
- setIsOpen={(bool) => this.openLinks = bool}
- onDoubleClick={() => this.onDoubleClick()}
- />
+ return (
+ <PropertiesSection title="Linked To" isOpen={this.openLinks} setIsOpen={bool => (this.openLinks = bool)} onDoubleClick={this.CloseAll}>
+ {this.linkCount > 0 ? this.links : 'There are no current links.'}
+ </PropertiesSection>
+ );
}
@computed get layoutSubMenu() {
- return <PropertiesSection
- title="Layout"
- content={this.layoutPreview}
- isOpen={this.openLayout}
- setIsOpen={(bool) => this.openLayout = bool}
- onDoubleClick={() => this.onDoubleClick()}
- />
+ return (
+ <PropertiesSection title="Layout" isOpen={this.openLayout} setIsOpen={bool => (this.openLayout = bool)} onDoubleClick={this.CloseAll}>
+ {this.layoutPreview}
+ </PropertiesSection>
+ );
}
@computed get description() {
@@ -1379,10 +1327,10 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
};
onDescriptionKey = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
- if (e.key === 'Enter') {
- this.setDescripValue(this.description);
- document.getElementById('link_description_input')?.blur();
- }
+ // if (e.key === 'Enter') {
+ // this.setDescripValue(this.description);
+ // document.getElementById('link_description_input')?.blur();
+ // }
};
onSelectOutRelationship = () => {
@@ -1434,6 +1382,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
get editRelationship() {
return (
<input
+ style={{ color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }}
autoComplete={'off'}
id="link_relationship_input"
value={StrCast(LinkManager.currentLink?.link_relationship)}
@@ -1451,7 +1400,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
return (
<textarea
autoComplete="off"
- style={{ textAlign: 'left' }}
+ style={{ textAlign: 'left', color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }}
id="link_description_input"
value={StrCast(LinkManager.currentLink?.link_description)}
onKeyDown={this.onDescriptionKey}
@@ -1470,224 +1419,242 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
if (scale > 1) scale = 1;
this.sourceAnchor && (this.sourceAnchor.followLinkZoomScale = scale);
};
-
+
@computed get linkProperties() {
const zoom = Number((NumCast(this.sourceAnchor?.followLinkZoomScale, 1) * 100).toPrecision(3));
const targZoom = this.sourceAnchor?.followLinkZoom;
const indent = 30;
const hasSelectedAnchor = LinkManager.Links(this.sourceAnchor).includes(LinkManager.currentLink!);
- return <>
- <div className="propertiesView-section" style={{ background: 'darkgray' }}>
- <div className="propertiesView-input first" style={{ display: 'grid', gridTemplateColumns: '84px auto' }}>
- <p>Relationship</p>
- {this.editRelationship}
- </div>
- <div className="propertiesView-input" style={{ display: 'grid', gridTemplateColumns: '84px auto' }}>
- <p>Description</p>
- {this.editDescription}
- </div>
- <div className="propertiesView-input inline">
- <p>Show link</p>
- <button
- style={{ background: !LinkManager.currentLink?.link_displayLine ? '' : '#4476f7', borderRadius: 3 }}
- onPointerDown={e => this.toggleLinkProp(e, 'link_displayLine')}
- onClick={e => e.stopPropagation()}
- className="propertiesButton">
- <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" />
- </button>
- </div>
- <div className="propertiesView-input inline" style={{ marginLeft: 10 }}>
- <p>Auto-move anchors</p>
- <button
- style={{ background: !LinkManager.currentLink?.link_autoMoveAnchors ? '' : '#4476f7', borderRadius: 3 }}
- onPointerDown={e => this.toggleLinkProp(e, 'link_autoMoveAnchors')}
- onClick={e => e.stopPropagation()}
- className="propertiesButton">
- <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" />
- </button>
- </div>
- <div className="propertiesView-input inline" style={{ marginLeft: 10 }}>
- <p>Display arrow</p>
- <button
- style={{ background: !LinkManager.currentLink?.link_displayArrow ? '' : '#4476f7', borderRadius: 3 }}
- onPointerDown={e => this.toggleLinkProp(e, 'link_displayArrow')}
- onClick={e => e.stopPropagation()}
- className="propertiesButton">
- <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" />
- </button>
- </div>
- </div>
- {!hasSelectedAnchor ? null : (
- <div className="propertiesView-section">
- <div className="propertiesView-input inline first" style={{ display: 'grid', gridTemplateColumns: '84px calc(100% - 84px)' }}>
- <p>Follow by</p>
- <select onChange={e => this.changeFollowBehavior(e.currentTarget.value === 'Default' ? undefined : e.currentTarget.value)} value={Cast(this.sourceAnchor?.followLinkLocation, 'string', null)}>
- <option value={undefined}>Default</option>
- <option value={OpenWhere.addLeft}>Opening in new left pane</option>
- <option value={OpenWhere.addRight}>Opening in new right pane</option>
- <option value={OpenWhere.replaceLeft}>Replacing left tab</option>
- <option value={OpenWhere.replaceRight}>Replacing right tab</option>
- <option value={OpenWhere.lightbox}>Opening in lightbox</option>
- <option value={OpenWhere.add}>Opening in new tab</option>
- <option value={OpenWhere.replace}>Replacing current tab</option>
- <option value={OpenWhere.inParent}>Opening in same collection</option>
- {LinkManager.currentLink?.linksToAnnotation ? <option value="openExternal">Open in external page</option> : null}
- </select>
- </div>
- <div className="propertiesView-input inline first" style={{ display: 'grid', gridTemplateColumns: '84px calc(100% - 134px) 50px' }}>
- <p>Animation</p>
- <select style={{ width: '100%', gridColumn: 2 }} onChange={e => this.changeAnimationBehavior(e.currentTarget.value)} value={StrCast(this.sourceAnchor?.followLinkAnimEffect, 'default')}>
- <option value="default">Default</option>
- {[PresEffect.None, PresEffect.Zoom, PresEffect.Lightspeed, PresEffect.Fade, PresEffect.Flip, PresEffect.Rotate, PresEffect.Bounce, PresEffect.Roll].map(effect => (
- <option key={effect.toString()} value={effect.toString()}>
- {effect.toString()}
- </option>
- ))}
- </select>
- <div className="effectDirection" style={{ marginLeft: '10px', display: 'grid', width: 40, height: 36, gridColumn: 3, gridTemplateRows: '12px 12px 12px' }}>
- {this.animationDirection(PresEffectDirection.Left, 'angle-right', 1, 2, {})}
- {this.animationDirection(PresEffectDirection.Right, 'angle-left', 3, 2, {})}
- {this.animationDirection(PresEffectDirection.Top, 'angle-down', 2, 1, {})}
- {this.animationDirection(PresEffectDirection.Bottom, 'angle-up', 2, 3, {})}
- {this.animationDirection(PresEffectDirection.Center, '', 2, 2, { width: 10, height: 10, alignSelf: 'center' })}
+ return (
+ <>
+ <div className="propertiesView-section">
+ <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>
- {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'} />
+ {!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
+ style={{ color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }}
+ onChange={e => this.changeFollowBehavior(e.currentTarget.value === 'Default' ? undefined : e.currentTarget.value)}
+ value={Cast(this.sourceAnchor?.followLinkLocation, 'string', null)}>
+ <option value={undefined}>Default</option>
+ <option value={OpenWhere.addLeft}>Opening in new left pane</option>
+ <option value={OpenWhere.addRight}>Opening in new right pane</option>
+ <option value={OpenWhere.replaceLeft}>Replacing left tab</option>
+ <option value={OpenWhere.replaceRight}>Replacing right tab</option>
+ <option value={OpenWhere.lightbox}>Opening in lightbox</option>
+ <option value={OpenWhere.add}>Opening in new tab</option>
+ <option value={OpenWhere.replace}>Replacing current tab</option>
+ <option value={OpenWhere.inParent}>Opening in same collection</option>
+ {LinkManager.currentLink?.linksToAnnotation ? <option value="openExternal">Open in external page</option> : null}
+ </select>
+ </div>
+ <div className="propertiesView-input inline first" style={{ display: 'grid', gridTemplateColumns: '84px calc(100% - 134px) 50px' }}>
+ <p>Animation</p>
+ <select
+ style={{ width: '100%', gridColumn: 2, color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }}
+ onChange={e => this.changeAnimationBehavior(e.currentTarget.value)}
+ value={StrCast(this.sourceAnchor?.followLinkAnimEffect, 'default')}>
+ <option value="default">Default</option>
+ {[PresEffect.None, PresEffect.Zoom, PresEffect.Lightspeed, PresEffect.Fade, PresEffect.Flip, PresEffect.Rotate, PresEffect.Bounce, PresEffect.Roll].map(effect => (
+ <option key={effect.toString()} value={effect.toString()}>
+ {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 className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setZoom(String(zoom), -0.1))}>
- <FontAwesomeIcon icon={'caret-down'} />
+ </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>Play Target Video</p>
+ <button
+ style={{ background: !this.sourceAnchor?.followLinkVideo ? '' : '#4476f7', borderRadius: 3 }}
+ onPointerDown={e => this.toggleAnchorProp(e, 'followLinkVideo', 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%', color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }} readOnly={true} type="number" value={zoom} />
+ <div className="ribbon-propertyUpDown" style={{ display: 'flex', flexDirection: 'column' }}>
+ <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setZoom(String(zoom), 0.1))}>
+ <FontAwesomeIcon icon={'caret-up'} />
+ </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>
- <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>
- )}
- </>
+ )}
+ </>
+ );
}
/**
@@ -1718,33 +1685,30 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
<div
className="propertiesView"
style={{
- background: StrCast(Doc.UserDoc().userBackgroundColor),
- color: StrCast(Doc.UserDoc().userColor),
+ background: SettingsManager.userBackgroundColor,
+ color: SettingsManager.userColor,
width: this.props.width,
minWidth: this.props.width,
}}>
- <div className = "propertiesView-propAndInfoGrouping">
+ <div className="propertiesView-propAndInfoGrouping">
<div className="propertiesView-title" style={{ width: this.props.width }}>
Properties
+ <div className="propertiesView-info" onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/properties')}>
+ <IconButton icon={<FontAwesomeIcon icon="info-circle" />} color={SettingsManager.userColor} />
+ </div>
</div>
- <div className = "propertiesView-info" onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/')}>
- <GrCircleInformation/> </div>
-
</div>
-
<div className="propertiesView-name">{this.editableTitle}</div>
- <div className = "propertiesView-type"> {this.currentType} </div>
- {this.contextsSubMenu}
+ <div className="propertiesView-type"> {this.currentType} </div>
+ {this.optionsSubMenu}
{this.linksSubMenu}
- {!this.selectedDoc || !LinkManager.currentLink || (!hasSelectedAnchor && this.selectedDoc !== LinkManager.currentLink) ? null : (
- this.linkProperties
- )}
+ {!LinkManager.currentLink || !this.openLinks ? null : this.linkProperties}
{this.inkSubMenu}
- {this.optionsSubMenu}
+ {this.contextsSubMenu}
{this.fieldsSubMenu}
{isNovice ? null : this.sharingSubMenu}
- {isNovice ? null : this.filtersSubMenu}
+ {this.filtersSubMenu}
{isNovice ? null : this.layoutSubMenu}
</div>
);
@@ -1760,7 +1724,6 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
Presentation
</div>
<div className="propertiesView-name" style={{ borderBottom: 0 }}>
-
{this.editableTitle}
<div className="propertiesView-presSelected">
<div className="propertiesView-selectedCount">{PresBox.Instance.selectedArray.size} selected</div>
@@ -1768,50 +1731,71 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
</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-presentationTrails">
+ <div
+ className="propertiesView-presentationTrails-title"
+ onPointerDown={action(() => (this.openPresTransitions = !this.openPresTransitions))}
+ style={{
+ color: SettingsManager.userColor,
+ backgroundColor: this.openPresTransitions ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor,
+ }}>
&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" />
+ <div className="propertiesView-presentationTrails-title-icon">
+ <FontAwesomeIcon icon={this.openPresTransitions ? 'caret-down' : 'caret-right'} size="lg" />
</div>
</div>
- {this.openPresTransitions ? <div className="propertiesView-presTrails-content">{PresBox.Instance.transitionDropdown}</div> : null}
+ {this.openPresTransitions ? <div className="propertiesView-presentationTrails-content">{PresBox.Instance.transitionDropdown}</div> : null}
</div>
)}
{!selectedItem ? null : (
- <div className="propertiesView-presTrails">
+ <div className="propertiesView-presentationTrails">
<div
- className="propertiesView-presTrails-title"
+ className="propertiesView-presentationTrails-title"
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" />
+ style={{
+ color: SettingsManager.userColor,
+ backgroundColor: this.openPresVisibilityAndDuration ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor,
+ }}>
+ &nbsp; <FontAwesomeIcon style={{ alignSelf: 'center' }} icon={'rocket'} /> &nbsp; Visibility
+ <div className="propertiesView-presentationTrails-title-icon">
+ <FontAwesomeIcon icon={this.openPresVisibilityAndDuration ? 'caret-down' : 'caret-right'} size="lg" />
</div>
</div>
- {this.openPresVisibilityAndDuration ? <div className="propertiesView-presTrails-content">{PresBox.Instance.visibiltyDurationDropdown}</div> : null}
+ {this.openPresVisibilityAndDuration ? <div className="propertiesView-presentationTrails-content">{PresBox.Instance.visibilityDurationDropdown}</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' : '' }}>
+ <div className="propertiesView-presentationTrails">
+ <div
+ className="propertiesView-presentationTrails-title"
+ onPointerDown={action(() => (this.openPresProgressivize = !this.openPresProgressivize))}
+ style={{
+ color: SettingsManager.userColor,
+ backgroundColor: this.openPresProgressivize ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor,
+ }}>
&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" />
+ <div className="propertiesView-presentationTrails-title-icon">
+ <FontAwesomeIcon icon={this.openPresProgressivize ? 'caret-down' : 'caret-right'} size="lg" />
</div>
</div>
- {this.openPresProgressivize ? <div className="propertiesView-presTrails-content">{PresBox.Instance.progressivizeDropdown}</div> : null}
+ {this.openPresProgressivize ? <div className="propertiesView-presentationTrails-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' : '' }}>
+ <div className="propertiesView-presentationTrails">
+ <div
+ className="propertiesView-presentationTrails-title"
+ onPointerDown={action(() => (this.openSlideOptions = !this.openSlideOptions))}
+ style={{
+ color: SettingsManager.userColor,
+ backgroundColor: this.openSlideOptions ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor,
+ }}>
&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" />
+ <div className="propertiesView-presentationTrails-title-icon">
+ <FontAwesomeIcon icon={this.openSlideOptions ? 'caret-down' : 'caret-right'} size="lg" />
</div>
</div>
- {this.openSlideOptions ? <div className="propertiesView-presTrails-content">{PresBox.Instance.mediaOptionsDropdown}</div> : null}
+ {this.openSlideOptions ? <div className="propertiesView-presentationTrails-content">{PresBox.Instance.mediaOptionsDropdown}</div> : null}
</div>
)}
</div>
diff --git a/src/client/views/ScriptingRepl.tsx b/src/client/views/ScriptingRepl.tsx
index 2bc2d5e6b..9966dbced 100644
--- a/src/client/views/ScriptingRepl.tsx
+++ b/src/client/views/ScriptingRepl.tsx
@@ -5,6 +5,7 @@ import * as React from 'react';
import { DocumentManager } from '../util/DocumentManager';
import { CompileScript, Transformer, ts } from '../util/Scripting';
import { ScriptingGlobals } from '../util/ScriptingGlobals';
+import { SettingsManager } from '../util/SettingsManager';
import { undoable } from '../util/UndoManager';
import { DocumentIconContainer } from './nodes/DocumentIcon';
import { OverlayView } from './OverlayView';
@@ -245,17 +246,29 @@ export class ScriptingRepl extends React.Component {
render() {
return (
<div className="scriptingRepl-outerContainer">
- <div className="scriptingRepl-commandsContainer" ref={this.commandsRef}>
+ <div className="scriptingRepl-commandsContainer" style={{ background: SettingsManager.userBackgroundColor }} ref={this.commandsRef}>
{this.commands.map(({ command, result }, i) => {
return (
- <div className="scriptingRepl-resultContainer" key={i}>
- <div className="scriptingRepl-commandString">{command || <br />}</div>
- <div className="scriptingRepl-commandResult">{<ScriptingValueDisplay scrollToBottom={this.maybeScrollToBottom} value={result} />}</div>
+ <div className="scriptingRepl-resultContainer" style={{ background: SettingsManager.userBackgroundColor }} key={i}>
+ <div className="scriptingRepl-commandString" style={{ background: SettingsManager.userBackgroundColor }}>
+ {command || <br />}
+ </div>
+ <div className="scriptingRepl-commandResult" style={{ background: SettingsManager.userBackgroundColor }}>
+ {<ScriptingValueDisplay scrollToBottom={this.maybeScrollToBottom} value={result} />}
+ </div>
</div>
);
})}
</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"
+ style={{ background: SettingsManager.userBackgroundColor }} //
+ onFocus={this.onFocus}
+ onBlur={this.onBlur}
+ value={this.commandString}
+ onChange={this.onChange}
+ onKeyDown={this.onKeyDown}
+ />
</div>
);
}
diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx
index db273cc88..1e1b8e0e6 100644
--- a/src/client/views/SidebarAnnos.tsx
+++ b/src/client/views/SidebarAnnos.tsx
@@ -9,11 +9,11 @@ import { emptyFunction, returnAll, returnFalse, returnOne, returnTrue, returnZer
import { Docs, DocUtils } from '../documents/Documents';
import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
import { LinkManager } from '../util/LinkManager';
+import { SearchUtil } from '../util/SearchUtil';
import { Transform } from '../util/Transform';
import { CollectionStackingView } from './collections/CollectionStackingView';
import { FieldViewProps } from './nodes/FieldView';
import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
-import { SearchBox } from './search/SearchBox';
import './SidebarAnnos.scss';
import { StyleProp } from './StyleProvider';
import React = require('react');
@@ -43,7 +43,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
@computed get allMetadata() {
const keys = new Map<string, FieldResult<Field>>();
DocListCast(this.props.rootDoc[this.sidebarKey]).forEach(doc =>
- SearchBox.documentKeys(doc)
+ SearchUtil.documentKeys(doc)
.filter(key => key[0] && key[0] !== '_' && key[0] === key[0].toUpperCase())
.map(key => keys.set(key, doc[key]))
);
@@ -62,12 +62,9 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
DocListCast(this.props.rootDoc[this.sidebarKey]).forEach(doc => keys.add(StrCast(doc.author)));
return Array.from(keys.keys()).sort();
}
- get filtersKey() {
- return '_' + this.sidebarKey + '_childFilters';
- }
- anchorMenuClick = (anchor: Doc) => {
- const startup = StrListCast(this.props.rootDoc.childFilters)
+ anchorMenuClick = (anchor: Doc, filterExlusions?: string[]) => {
+ const startup = this.childFilters()
.map(filter => filter.split(':')[0])
.join(' ');
const target = Docs.Create.TextDocument(startup, {
@@ -87,6 +84,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
const taggedContent = this.childFilters()
.filter(data => data.split(':')[0])
+ .filter(data => !filterExlusions?.includes(data.split(':')[0]))
.map(data => {
const key = data.split(':')[0];
const val = Field.Copy(this.allMetadata.get(key));
@@ -150,8 +148,9 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
};
makeDocUnfiltered = (doc: 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>();
+ if (this.childFilters()) {
+ // if any child filters exist, get rid of them
+ this.props.layoutDoc._childFilters = new List<string>();
}
return true;
}
@@ -177,7 +176,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
addDocument = (doc: Doc | Doc[]) => this.props.sidebarAddDocument(doc, this.sidebarKey);
moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => this.props.moveDocument(doc, targetCollection, addDocument, this.sidebarKey);
removeDocument = (doc: Doc | Doc[]) => this.props.removeDocument(doc, this.sidebarKey);
- childFilters = () => [...StrListCast(this.props.layoutDoc._childFilters), ...StrListCast(this.props.layoutDoc[this.filtersKey])];
+ childFilters = () => StrListCast(this.props.layoutDoc._childFilters);
layout_showTitle = () => 'title';
setHeightCallback = (height: number) => this.props.setHeight?.(height + this.filtersHeight());
sortByLinkAnchorY = (a: Doc, b: Doc) => {
@@ -187,25 +186,25 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
};
render() {
const renderTag = (tag: string) => {
- const active = StrListCast(this.props.rootDoc[this.filtersKey]).includes(`tags:${tag}:check`);
+ const active = this.childFilters().includes(`tags${Doc.FilterSep}${tag}${Doc.FilterSep}check`);
return (
- <div key={tag} className={`sidebarAnnos-filterTag${active ? '-active' : ''}`} onClick={e => Doc.setDocFilter(this.props.rootDoc, 'tags', tag, 'check', true, this.sidebarKey, e.shiftKey)}>
+ <div key={tag} className={`sidebarAnnos-filterTag${active ? '-active' : ''}`} onClick={e => Doc.setDocFilter(this.props.rootDoc, 'tags', tag, 'check', true, undefined, e.shiftKey)}>
{tag}
</div>
);
};
const renderMeta = (tag: string, dflt: FieldResult<Field>) => {
- const active = StrListCast(this.props.rootDoc[this.filtersKey]).includes(`${tag}:${dflt}:exists`);
+ const active = this.childFilters().includes(`${tag}${Doc.FilterSep}${Doc.FilterAny}${Doc.FilterSep}exists`);
return (
- <div key={tag} className={`sidebarAnnos-filterTag${active ? '-active' : ''}`} onClick={e => Doc.setDocFilter(this.props.rootDoc, tag, dflt, 'exists', true, this.sidebarKey, e.shiftKey)}>
+ <div key={tag} className={`sidebarAnnos-filterTag${active ? '-active' : ''}`} onClick={e => Doc.setDocFilter(this.props.rootDoc, tag, Doc.FilterAny, 'exists', true, undefined, e.shiftKey)}>
{tag}
</div>
);
};
const renderUsers = (user: string) => {
- const active = StrListCast(this.props.rootDoc[this.filtersKey]).includes(`author:${user}:check`);
+ const active = this.childFilters().includes(`author:${user}:check`);
return (
- <div key={user} className={`sidebarAnnos-filterUser${active ? '-active' : ''}`} onClick={e => Doc.setDocFilter(this.props.rootDoc, 'author', user, 'check', true, this.sidebarKey, e.shiftKey)}>
+ <div key={user} className={`sidebarAnnos-filterUser${active ? '-active' : ''}`} onClick={e => Doc.setDocFilter(this.props.rootDoc, 'author', user, 'check', true, undefined, e.shiftKey)}>
{user}
</div>
);
@@ -223,12 +222,13 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
height: '100%',
}}>
<div className="sidebarAnnos-tagList" style={{ height: this.filtersHeight() }} onWheel={e => e.stopPropagation()}>
- {this.allUsers.map(renderUsers)}
+ {this.allUsers.length > 1 ? this.allUsers.map(renderUsers) : null}
{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
{...this.props}
diff --git a/src/client/views/StyleProvider.scss b/src/client/views/StyleProvider.scss
index c06bb287e..f069e7e1b 100644
--- a/src/client/views/StyleProvider.scss
+++ b/src/client/views/StyleProvider.scss
@@ -18,10 +18,15 @@
cursor: default;
}
.styleProvider-filter {
- right: 0;
+ right: 15;
+ .styleProvider-filterShift {
+ left: 0;
+ top: 0;
+ position: absolute;
+ }
}
.styleProvider-audio {
- right: 15;
+ right: 30;
}
.styleProvider-lock:hover,
.styleProvider-audio:hover,
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index bbbad3690..1a4b8d7d2 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -1,9 +1,10 @@
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tooltip } from '@material-ui/core';
-import { IconButton, Shadows, Size } from 'browndash-components';
+import { Dropdown, DropdownType, IconButton, IListItemProps, ListBox, ListItem, Popup, Shadows, Size, Type } from 'browndash-components';
import { action, runInAction } from 'mobx';
import { extname } from 'path';
+import { BsArrowDown, BsArrowDownUp, BsArrowUp } from 'react-icons/bs';
import { Doc, Opt, StrListCast } from '../../fields/Doc';
import { BoolCast, Cast, DocCast, ImageCast, NumCast, StrCast } from '../../fields/Types';
import { DashColor, lightOrDark, Utils } from '../../Utils';
@@ -14,21 +15,21 @@ import { LinkManager } from '../util/LinkManager';
import { SelectionManager } from '../util/SelectionManager';
import { ColorScheme, SettingsManager } from '../util/SettingsManager';
import { undoBatch, UndoManager } from '../util/UndoManager';
-import { TreeSort } from './collections/TreeView';
+import { TreeSort } from './collections/TreeSort';
import { Colors } from './global/globalEnums';
import { InkingStroke } from './InkingStroke';
-import { MainView } from './MainView';
-import { DocumentViewProps } from './nodes/DocumentView';
+import { DocumentView, DocumentViewProps } from './nodes/DocumentView';
import { FieldViewProps } from './nodes/FieldView';
import { KeyValueBox } from './nodes/KeyValueBox';
import { SliderBox } from './nodes/SliderBox';
-import { BsArrowDown, BsArrowUp, BsArrowDownUp } from 'react-icons/bs'
import './StyleProvider.scss';
import React = require('react');
+import { PropertiesView } from './PropertiesView';
+import { FaFilter } from 'react-icons/fa';
export enum StyleProp {
- TreeViewIcon = 'treeViewIcon',
- TreeViewSortings = 'treeViewSortings', // options for how to sort tree view items
+ TreeViewIcon = 'treeView_Icon',
+ TreeViewSortings = 'treeView_Sortings', // 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
BoxShadow = 'boxShadow', // box shadow - used for making collections standout and for showing clusters in free form views
@@ -44,7 +45,6 @@ export enum StyleProp {
ShowCaption = 'layout_showCaption',
TitleHeight = 'titleHeight', // Height of Title area
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
FontFamily = 'fontFamily', // font family of text
@@ -90,15 +90,16 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
const isCaption = property.includes(':caption');
const isAnchor = property.includes(':anchor');
const isAnnotated = property.includes(':annotated');
+ const isInk = () => doc && StrCast(Doc.Layout(doc).layout).includes(InkingStroke.name) && !props?.LayoutTemplateString;
const isOpen = property.includes(':open');
const isEmpty = property.includes(':empty');
const boxBackground = property.includes(':box');
const fieldKey = props?.fieldKey ? props.fieldKey + '_' : isCaption ? 'caption_' : '';
const lockedPosition = () => doc && BoolCast(doc._lockedPosition);
const backgroundCol = () => props?.styleProvider?.(doc, props, StyleProp.BackgroundColor);
+ const color = () => props?.styleProvider?.(doc, props, StyleProp.Color);
const opacity = () => props?.styleProvider?.(doc, props, StyleProp.Opacity);
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:
@@ -111,17 +112,17 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
return Doc.toIcon(doc, isEmpty ? undefined : isOpen);
case StyleProp.TreeViewSortings:
const allSorts: { [key: string]: { color: string; icon: JSX.Element | string } | undefined } = {};
- allSorts[TreeSort.Down] = { color: Colors.MEDIUM_BLUE, icon: <BsArrowDown/> };
- allSorts[TreeSort.Up] = { color: 'crimson', icon: <BsArrowUp/> };
+ allSorts[TreeSort.AlphaDown] = { color: Colors.MEDIUM_BLUE, icon: <BsArrowDown/> };
+ allSorts[TreeSort.AlphaUp] = { color: 'crimson', icon: <BsArrowUp/> };
if (doc?._type_collection === CollectionViewType.Freeform) allSorts[TreeSort.Zindex] = { color: 'green', icon: 'Z' };
- allSorts[TreeSort.None] = { color: 'darkgray', icon: <BsArrowDownUp/> };
+ allSorts[TreeSort.WhenAdded] = { color: 'darkgray', icon: <BsArrowDownUp/> };
return allSorts;
case StyleProp.Highlighting:
if (doc && (Doc.IsSystem(doc) || doc.type === DocumentType.FONTICON)) return undefined;
if (doc && !doc.layout_disableBrushing && !props?.disableBrushing) {
const selected = SelectionManager.Views().some(dv => dv.rootDoc === doc);
const highlightIndex = Doc.isBrushedHighlightedDegree(doc) || (selected ? Doc.DocBrushStatus.selfBrushed : 0);
- const highlightColor = ['transparent', 'rgb(68, 118, 247)', selected ? 'black' : 'rgb(68, 118, 247)', 'orange', 'lightBlue'][highlightIndex];
+ const highlightColor = ['transparent', 'rgb(68, 118, 247)', selected ? "black" : 'rgb(68, 118, 247)', 'orange', 'lightBlue'][highlightIndex];
const highlightStyle = ['solid', 'dashed', 'solid', 'solid', 'solid'][highlightIndex];
if (highlightIndex) {
return {
@@ -135,7 +136,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
return undefined;
case StyleProp.DocContents:return undefined;
case StyleProp.WidgetColor:return isAnnotated ? Colors.LIGHT_BLUE : darkScheme() ? 'lightgrey' : 'dimgrey';
- case StyleProp.Opacity: return props?.LayoutTemplateString?.includes(KeyValueBox.name) ? 1 : doc?.textInlineAnnotations ? 0 : Cast(doc?._opacity, "number", Cast(doc?.opacity, 'number', null));
+ case StyleProp.Opacity: return props?.LayoutTemplateString?.includes(KeyValueBox.name) ? 1 : doc?.text_inlineAnnotations ? 0 : Cast(doc?._opacity, "number", Cast(doc?.opacity, 'number', null));
case StyleProp.HideLinkBtn:return props?.hideLinkButton || (!selected && doc?.layout_hideLinkButton);
case StyleProp.FontSize: return StrCast(doc?.[fieldKey + 'fontSize'], StrCast(doc?._text_fontSize, StrCast(Doc.UserDoc().fontSize)));
case StyleProp.FontFamily: return StrCast(doc?.[fieldKey + 'fontFamily'], StrCast(doc?._text_fontFamily, StrCast(Doc.UserDoc().fontFamily)));
@@ -146,7 +147,8 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
case StyleProp.ShowTitle:
return (
(doc &&
- !doc.presentationTargetDoc &&
+ !props?.LayoutTemplateString &&
+ !doc.presentation_targetDoc &&
!props?.LayoutTemplateString?.includes(KeyValueBox.name) &&
props?.layout_showTitle?.() !== '' &&
StrCast(
@@ -161,15 +163,14 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
''
);
case StyleProp.Color:
- if (MainView.Instance.LastButton === doc) return Doc.UserDoc().userBackgroundColor;
- if (Doc.IsSystem(doc!)) return StrCast(Doc.UserDoc().userColor)
- if (doc?.type === DocumentType.FONTICON) return Doc.UserDoc().userColor;
+ if (DocumentView.LastPressedSidebarBtn === doc) return SettingsManager.userBackgroundColor;
+ if (Doc.IsSystem(doc!)) return SettingsManager.userColor;
+ if (doc?.type === DocumentType.FONTICON) return SettingsManager.userColor;
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, StyleProp.BackgroundColor);
- if (!backColor) return undefined;
- return lightOrDark(backColor);
+ return backColor ? lightOrDark(backColor) : undefined;
case StyleProp.BorderRounding:
return StrCast(doc?.[fieldKey + 'borderRounding'], StrCast(doc?.layout_borderRounding, doc?._type_collection === CollectionViewType.Pile ? '50%' : ''));
case StyleProp.BorderPath:
@@ -188,8 +189,6 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
</div>
),
};
- case StyleProp.JitterRotation:
- return Doc.IsComicStyle(doc) ? random(-1, 1, NumCast(doc?.x), NumCast(doc?.y)) * ((props?.PanelWidth() || 0) > (props?.PanelHeight() || 0) ? 5 : 10) : 0;
case StyleProp.HeaderMargin:
return ([CollectionViewType.Stacking, CollectionViewType.NoteTaking, CollectionViewType.Masonry, CollectionViewType.Tree].includes(doc?._type_collection as any) ||
(doc?.type === DocumentType.RTF && !layout_showTitle()?.includes('noMargin')) ||
@@ -199,7 +198,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
? 15
: 0;
case StyleProp.BackgroundColor: {
- if (MainView.Instance.LastButton === doc) return StrCast(Doc.UserDoc().userColor); // hack to indicate active menu panel item
+ if (DocumentView.LastPressedSidebarBtn === doc) return SettingsManager.userColor; // hack to indicate active menu panel item
let docColor: Opt<string> = StrCast(doc?.[fieldKey + '_backgroundColor'], StrCast(doc?._backgroundColor, isCaption ? 'rgba(0,0,0,0.4)' : ''));
// prettier-ignore
switch (doc?.type) {
@@ -208,12 +207,10 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
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:
@@ -224,7 +221,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
case DocumentType.COL:
if (StrCast(Doc.LayoutField(doc)).includes(SliderBox.name)) break;
docColor = docColor || (Doc.IsSystem(doc)
- ? StrCast(Doc.UserDoc().userBackgroundColor)
+ ? SettingsManager.userBackgroundColor
: doc.annotationOn
? '#00000010' // faint interior for collections on PDFs, images, etc
: doc?._isGroup
@@ -245,12 +242,12 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
switch (doc?.type) {
case DocumentType.COL:
return StrCast(
- doc?.layout_borderRounding,
+ doc?.layout_boxShadow,
doc?._type_collection === CollectionViewType.Pile
? '4px 4px 10px 2px'
: lockedPosition() || doc?._isGroup || docProps?.LayoutTemplateString
? undefined // groups have no drop shadow -- they're supposed to be "invisible". LayoutString's imply collection is being rendered as something else (e.g., title of a Slide)
- : `${darkScheme() ? Colors.DARK_GRAY : Colors.MEDIUM_GRAY} ${StrCast(doc.layout_borderRounding, '0.2vw 0.2vw 0.8vw')}`
+ : `${darkScheme() ? Colors.DARK_GRAY : Colors.MEDIUM_GRAY} ${StrCast(doc.layout_boxShadow, '0.2vw 0.2vw 0.8vw')}`
);
case DocumentType.LABEL:
@@ -268,14 +265,13 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
}
}
case StyleProp.PointerEvents:
- const isInk = doc && StrCast(Doc.Layout(doc).layout).includes(InkingStroke.name) && !props?.LayoutTemplateString;
if (StrCast(doc?.pointerEvents) && !props?.LayoutTemplateString?.includes(KeyValueBox.name)) return StrCast(doc!.pointerEvents); // honor pointerEvents field (set by lock button usually) if it's not a keyValue view of the Doc
if (docProps?.DocumentView?.().ComponentView?.overridePointerEvents?.() !== undefined) return docProps?.DocumentView?.().ComponentView?.overridePointerEvents?.();
- if (MainView.Instance._exploreMode || doc?.layout_unrendered) return isInk ? 'visiblePainted' : 'all';
+ if (DocumentView.ExploreMode || doc?.layout_unrendered) return isInk() ? 'visiblePainted' : 'all';
if (props?.contentPointerEvents) return StrCast(props.contentPointerEvents);
if (props?.pointerEvents?.() === 'none') return 'none';
if (opacity() === 0) return 'none';
- if (props?.isDocumentActive?.()) return isInk ? 'visiblePainted' : 'all';
+ 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:
const lock = () => {
@@ -288,25 +284,59 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
}
};
const filter = () => {
+ const dashView = DocumentManager.Instance.getDocumentView(Doc.ActiveDashboard);
const showFilterIcon =
StrListCast(doc?._childFilters).length || StrListCast(doc?._childFiltersByRanges).length
- ? '#18c718bd' //'hasFilter'
+ ? 'green' // #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 className="styleProvider-filter">
+ <Dropdown
+ type={Type.TERT}
+ dropdownType={DropdownType.CLICK}
+ fillWidth
+ iconProvider={(active:boolean) => <div className='styleProvider-filterShift'><FaFilter/></div>}
+ closeOnSelect={true}
+ setSelectedVal={
+ action((dv) => {
+ (dv as any).select(false);
+ (SettingsManager.propertiesWidth = 250);
+ setTimeout(action(() => {
+ if (PropertiesView.Instance) {
+ PropertiesView.Instance.CloseAll();
+ PropertiesView.Instance.openFilters = true;
+ }
+ }));
+ })
+ }
+ size={Size.XSMALL}
+ width={15}
+ height={15}
+ title={showFilterIcon === 'green' ?
+ "This view is filtered. Click to view/change filters":
+ "this view inherits filters from one of its parents"}
+ color={SettingsManager.userColor}
+ background={showFilterIcon}
+ items={[ ...(dashView ? [dashView]: []), ...(props?.docViewPath?.()??[]), ...(props?.DocumentView?[props?.DocumentView?.()]:[])]
+ .filter(dv => StrListCast(dv.rootDoc.childFilters).length || StrListCast(dv.rootDoc.childRangeFilters).length)
+ .map(dv => ({
+ text: StrCast(dv.rootDoc.title),
+ val: dv as any,
+ style: {color:SettingsManager.userColor, background:SettingsManager.userBackgroundColor},
+ } as IListItemProps)) }
+ />
</div>
);
};
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 audioAnnosCount = (doc: Doc) => StrListCast(doc[Doc.LayoutFieldKey(doc) + '_audioAnnotations']).length;
+ if (!doc || props?.renderDepth === -1 || !audioAnnosCount(doc)) 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>}>
+ <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>
@@ -324,7 +354,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
}
export function DashboardToggleButton(doc: Doc, field: string, onIcon: IconProp, offIcon: IconProp, clickFunc?: () => void) {
- const color = StrCast(Doc.UserDoc().userColor);
+ const color = SettingsManager.userColor;
return (
<IconButton
size={Size.XSMALL}
diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx
index 5c2ab3f70..a3884c9eb 100644
--- a/src/client/views/TemplateMenu.tsx
+++ b/src/client/views/TemplateMenu.tsx
@@ -102,7 +102,7 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
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} />);
- addedTypes.concat(noteTypes).map(template => (template.treeViewChecked = this.templateIsUsed(firstDoc, template)));
+ addedTypes.concat(noteTypes).map(template => (template.treeView_Checked = this.templateIsUsed(firstDoc, template)));
this._addedKeys &&
Array.from(this._addedKeys)
.filter(key => !noteTypes.some(nt => nt.title === key))
diff --git a/src/client/views/UndoStack.tsx b/src/client/views/UndoStack.tsx
index caf04cc1b..f07e38af1 100644
--- a/src/client/views/UndoStack.tsx
+++ b/src/client/views/UndoStack.tsx
@@ -1,12 +1,12 @@
-import { action, observable } from 'mobx';
+import { Tooltip } from '@mui/material';
+import { Popup, Type } from 'browndash-components';
+import { observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
+import { StrCast } from '../../fields/Types';
+import { SettingsManager } from '../util/SettingsManager';
import { UndoManager } from '../util/UndoManager';
import './UndoStack.scss';
-import { StrCast } from '../../fields/Types';
-import { Doc } from '../../fields/Doc';
-import { Popup, Type, isDark } from 'browndash-components';
-import { Colors } from './global/globalEnums';
interface UndoStackProps {
width?: number;
@@ -18,38 +18,51 @@ export class UndoStack extends React.Component<UndoStackProps> {
@observable static HideInline: boolean;
@observable static Expand: boolean;
render() {
- const background = UndoManager.batchCounter.get() ? 'yellow' : StrCast(Doc.UserDoc().userBackgroundColor)
+ const background = UndoManager.batchCounter.get() ? 'yellow' : SettingsManager.userVariantColor;
+ const color = UndoManager.batchCounter.get() ? 'black' : SettingsManager.userColor;
return this.props.inline && UndoStack.HideInline ? null : (
- <div className="undoStack-outerContainer">
- <Popup
- text={'Undo/Redo Stack'}
- color={UndoManager.batchCounter.get() ? 'yellow' : StrCast(Doc.UserDoc().userVariantColor)}
- placement={`top-start`}
- type={Type.TERT}
- popup={
- <div className="undoStack-commandsContainer" ref={r => r?.scroll({ behavior: 'auto', top: r?.scrollHeight + 20 })}
- style={{
- background: background,
- color: isDark(background) ? Colors.LIGHT_GRAY : Colors.DARK_GRAY
- }}>
- {UndoManager.undoStackNames.map((name, i) => (
- <div className="undoStack-resultContainer" key={i}>
- <div className="undoStack-commandString">{name.replace(/[^\.]*\./, '')}</div>
- </div>
- ))}
- {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>
+ <Tooltip title={'undo stack (if it stays yellow, undo is broken - you should reload Dash)'}>
+ <div>
+ <div className="undoStack-outerContainer">
+ <Popup
+ text="stack"
+ color={color}
+ background={background}
+ placement={`top-start`}
+ type={Type.TERT}
+ popup={
+ <div
+ className="undoStack-commandsContainer"
+ ref={r => r?.scroll({ behavior: 'auto', top: r?.scrollHeight + 20 })}
+ style={{
+ background,
+ color,
+ }}>
+ {Array.from(UndoManager.undoStackNames).map((name, i) => (
+ <div className="undoStack-resultContainer" key={i}
+ onClick={e => {
+ const size = UndoManager.undoStackNames.length;
+ for (let n = 0; n < size-i; n++ ) UndoManager.Undo(); } }
+ >
+ <div className="undoStack-commandString">{StrCast(name).replace(/[^\.]*\./, '')}</div>
+ </div>
+ ))}
+ {Array.from(UndoManager.redoStackNames)
+ .reverse()
+ .map((name, i) => (
+ <div className="undoStack-resultContainer" key={i} onClick={e =>
+ { for (let n = 0; n <= i; n++ ) UndoManager.Redo() }}>
+ <div className="undoStack-commandString" style={{ fontWeight: 'bold', background: SettingsManager.userBackgroundColor, color: SettingsManager.userColor }}>
+ {StrCast(name).replace(/[^\.]*\./, '')}
+ </div>
+ </div>
+ ))}
</div>
- ))}
- </div>
- }
- />
- </div>
+ }
+ />
+ </div>
+ </div>
+ </Tooltip>
);
}
}
diff --git a/src/client/views/animationtimeline/Keyframe.tsx b/src/client/views/animationtimeline/Keyframe.tsx
index 3465a5283..addc00c85 100644
--- a/src/client/views/animationtimeline/Keyframe.tsx
+++ b/src/client/views/animationtimeline/Keyframe.tsx
@@ -123,7 +123,7 @@ export type RegionData = makeInterface<[typeof RegionDataSchema]>;
export const RegionData = makeInterface(RegionDataSchema);
interface IProps {
- node: Doc;
+ animatedDoc: Doc;
RegionData: Doc;
collection: Doc;
tickSpacing: number;
@@ -167,7 +167,7 @@ export class Keyframe extends React.Component<IProps> {
return RegionData(this.props.RegionData);
}
@computed private get regions() {
- return DocListCast(this.props.node.regions);
+ return DocListCast(this.props.animatedDoc.regions);
}
@computed private get keyframes() {
return DocListCast(this.regiondata.keyframes);
@@ -375,7 +375,7 @@ export class Keyframe extends React.Component<IProps> {
*/
@action
makeRegionMenu = (kf: Doc, e: MouseEvent) => {
- TimelineMenu.Instance.addItem('button', 'Remove Region', () => Cast(this.props.node.regions, listSpec(Doc))?.splice(this.regions.indexOf(this.props.RegionData), 1)),
+ TimelineMenu.Instance.addItem('button', 'Remove Region', () => Cast(this.props.animatedDoc.regions, listSpec(Doc))?.splice(this.regions.indexOf(this.props.RegionData), 1)),
TimelineMenu.Instance.addItem('input', `fadeIn: ${this.regiondata.fadeIn}ms`, val => {
runInAction(() => {
let cannotMove: boolean = false;
@@ -461,7 +461,7 @@ export class Keyframe extends React.Component<IProps> {
e.stopPropagation();
const div = ref.current!;
div.style.opacity = '1';
- Doc.BrushDoc(this.props.node);
+ Doc.BrushDoc(this.props.animatedDoc);
};
/**
@@ -473,7 +473,7 @@ export class Keyframe extends React.Component<IProps> {
e.stopPropagation();
const div = ref.current!;
div.style.opacity = '0';
- Doc.UnBrushDoc(this.props.node);
+ Doc.UnBrushDoc(this.props.animatedDoc);
};
///////////////////////UI STUFF /////////////////////////
diff --git a/src/client/views/animationtimeline/Timeline.tsx b/src/client/views/animationtimeline/Timeline.tsx
index adc97bbb4..7ca13756a 100644
--- a/src/client/views/animationtimeline/Timeline.tsx
+++ b/src/client/views/animationtimeline/Timeline.tsx
@@ -535,7 +535,7 @@ export class Timeline extends React.Component<FieldViewProps> {
{this.children.map(doc => (
<Track
ref={ref => this.mapOfTracks.push(ref)}
- node={doc}
+ animatedDoc={doc}
currentBarX={this._currentBarX}
changeCurrentBarX={this.changeCurrentBarX}
transform={this.props.ScreenToLocalTransform()}
diff --git a/src/client/views/animationtimeline/Track.tsx b/src/client/views/animationtimeline/Track.tsx
index 2349ba786..1010332f5 100644
--- a/src/client/views/animationtimeline/Track.tsx
+++ b/src/client/views/animationtimeline/Track.tsx
@@ -12,7 +12,7 @@ import { Keyframe, KeyframeFunc, RegionData } from './Keyframe';
import './Track.scss';
interface IProps {
- node: Doc;
+ animatedDoc: Doc;
currentBarX: number;
transform: Transform;
collection: Doc;
@@ -36,23 +36,23 @@ export class Track extends React.Component<IProps> {
private objectWhitelist = ['data'];
@computed private get regions() {
- return DocListCast(this.props.node.regions);
+ return DocListCast(this.props.animatedDoc.regions);
}
@computed private get time() {
return NumCast(KeyframeFunc.convertPixelTime(this.props.currentBarX, 'mili', 'time', this.props.tickSpacing, this.props.tickIncrement));
}
async componentDidMount() {
- const regions = await DocListCastAsync(this.props.node.regions);
- if (!regions) this.props.node.regions = new List<Doc>(); //if there is no region, then create new doc to store stuff
+ const regions = await DocListCastAsync(this.props.animatedDoc.regions);
+ if (!regions) this.props.animatedDoc.regions = new List<Doc>(); //if there is no region, then create new doc to store stuff
//these two lines are exactly same from timeline.tsx
const relativeHeight = window.innerHeight / 20;
runInAction(() => (this._trackHeight = relativeHeight < this.MAX_TITLE_HEIGHT ? relativeHeight : this.MAX_TITLE_HEIGHT)); //for responsiveness
this._timelineVisibleReaction = this.timelineVisibleReaction();
this._currentBarXReaction = this.currentBarXReaction();
- if (DocListCast(this.props.node.regions).length === 0) this.createRegion(this.time);
- this.props.node.hidden = false;
- this.props.node.opacity = 1;
+ if (DocListCast(this.props.animatedDoc.regions).length === 0) this.createRegion(this.time);
+ this.props.animatedDoc.hidden = false;
+ this.props.animatedDoc.opacity = 1;
// this.autoCreateKeyframe();
}
@@ -127,13 +127,13 @@ export class Track extends React.Component<IProps> {
*/
@action
autoCreateKeyframe = () => {
- const objects = this.objectWhitelist.map(key => this.props.node[key]);
- intercept(this.props.node, change => {
+ const objects = this.objectWhitelist.map(key => this.props.animatedDoc[key]);
+ intercept(this.props.animatedDoc, change => {
return change;
});
return reaction(
() => {
- return [...this.primitiveWhitelist.map(key => this.props.node[key]), ...objects];
+ return [...this.primitiveWhitelist.map(key => this.props.animatedDoc[key]), ...objects];
},
(changed, reaction) => {
//check for region
@@ -171,14 +171,14 @@ export class Track extends React.Component<IProps> {
() => {
const regiondata = this.findRegion(this.time);
if (regiondata) {
- this.props.node.hidden = false;
+ this.props.animatedDoc.hidden = false;
// if (!this._autoKfReaction) {
// // this._autoKfReaction = this.autoCreateKeyframe();
// }
this.timeChange();
} else {
- this.props.node.hidden = true;
- this.props.node.opacity = 0;
+ this.props.animatedDoc.hidden = true;
+ this.props.animatedDoc.opacity = 0;
//if (this._autoKfReaction) this._autoKfReaction();
}
}
@@ -250,10 +250,10 @@ export class Track extends React.Component<IProps> {
private applyKeys = async (kf: Doc) => {
this.primitiveWhitelist.forEach(key => {
if (!kf[key]) {
- this.props.node[key] = undefined;
+ this.props.animatedDoc[key] = undefined;
} else {
const stored = kf[key];
- this.props.node[key] = stored instanceof ObjectField ? stored[Copy]() : stored;
+ this.props.animatedDoc[key] = stored instanceof ObjectField ? stored[Copy]() : stored;
}
});
};
@@ -282,11 +282,11 @@ export class Track extends React.Component<IProps> {
const dif = NumCast(right[key]) - NumCast(left[key]);
const deltaLeft = this.time - NumCast(left.time);
const ratio = deltaLeft / (NumCast(right.time) - NumCast(left.time));
- this.props.node[key] = NumCast(left[key]) + dif * ratio;
+ this.props.animatedDoc[key] = NumCast(left[key]) + dif * ratio;
} else {
// case data
const stored = left[key];
- this.props.node[key] = stored instanceof ObjectField ? stored[Copy]() : stored;
+ this.props.animatedDoc[key] = stored instanceof ObjectField ? stored[Copy]() : stored;
}
});
};
@@ -326,7 +326,7 @@ export class Track extends React.Component<IProps> {
regiondata.duration = rightRegion.position - regiondata.position;
}
if (this.regions.length === 0 || !rightRegion || (rightRegion && rightRegion.position - regiondata.position >= NumCast(regiondata.fadeIn) + NumCast(regiondata.fadeOut))) {
- Cast(this.props.node.regions, listSpec(Doc))?.push(regiondata);
+ Cast(this.props.animatedDoc.regions, listSpec(Doc))?.push(regiondata);
this._newKeyframe = true;
this.saveStateRegion = regiondata;
return regiondata;
@@ -360,7 +360,7 @@ export class Track extends React.Component<IProps> {
@action
copyDocDataToKeyFrame = (doc: Doc) => {
this.primitiveWhitelist.map(key => {
- const originalVal = this.props.node[key];
+ const originalVal = this.props.animatedDoc[key];
doc[key] = originalVal instanceof ObjectField ? originalVal[Copy]() : originalVal;
});
};
@@ -377,8 +377,8 @@ export class Track extends React.Component<IProps> {
ref={this._inner}
style={{ height: `${this._trackHeight}px` }}
onDoubleClick={this.onInnerDoubleClick}
- onPointerOver={() => Doc.BrushDoc(this.props.node)}
- onPointerOut={() => Doc.UnBrushDoc(this.props.node)}>
+ onPointerOver={() => Doc.BrushDoc(this.props.animatedDoc)}
+ onPointerOut={() => Doc.UnBrushDoc(this.props.animatedDoc)}>
{this.regions?.map((region, i) => {
return <Keyframe key={`${i}`} {...this.props} RegionData={region} makeKeyData={this.makeKeyData} />;
})}
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index ea02bcd4c..7fa36d228 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -47,7 +47,7 @@ export class CollectionCarouselView extends CollectionSubView() {
@computed get content() {
const index = NumCast(this.layoutDoc._carousel_index);
const curDoc = this.childLayoutPairs?.[index];
- const captionProps = { ...this.props, fieldKey: 'caption', setHeight: undefined };
+ const captionProps = { ...this.props, fieldKey: 'caption', setHeight: undefined, setContentView: undefined };
const marginX = NumCast(this.layoutDoc['caption_xMargin']);
const marginY = NumCast(this.layoutDoc['caption_yMargin']);
const show_captions = StrCast(this.layoutDoc._layout_showCaption);
@@ -58,6 +58,7 @@ export class CollectionCarouselView extends CollectionSubView() {
{...this.props}
NativeWidth={returnZero}
NativeHeight={returnZero}
+ setContentView={undefined}
onDoubleClick={this.onContentDoubleClick}
onClick={this.onContentClick}
isDocumentActive={this.props.childDocumentsActive?.() ? this.props.isDocumentActive : this.props.isContentActive}
diff --git a/src/client/views/collections/CollectionDockingView.scss b/src/client/views/collections/CollectionDockingView.scss
index a4c5229aa..b13753025 100644
--- a/src/client/views/collections/CollectionDockingView.scss
+++ b/src/client/views/collections/CollectionDockingView.scss
@@ -37,6 +37,10 @@
display: flex;
}
+.lm_active {
+ height: 27px !important;
+}
+
.lm_active .lm_title {
-webkit-appearance: none;
// font-weight: 700;
@@ -176,6 +180,7 @@
display: flex;
align-content: center;
justify-content: center;
+ background: $dark-gray;
}
.lm_controls > li {
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 16982595d..8472c59db 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -3,6 +3,7 @@ import { observer } from 'mobx-react';
import * as ReactDOM from 'react-dom/client';
import * as GoldenLayout from '../../../client/goldenLayout';
import { Doc, DocListCast, Opt } from '../../../fields/Doc';
+import { AclAdmin, AclEdit } from '../../../fields/DocSymbols';
import { Id } from '../../../fields/FieldSymbols';
import { InkTool } from '../../../fields/InkField';
import { List } from '../../../fields/List';
@@ -13,6 +14,7 @@ import { emptyFunction, incrementTitleCopy } from '../../../Utils';
import { DocServer } from '../../DocServer';
import { Docs } from '../../documents/Documents';
import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
+import { DocumentManager } from '../../util/DocumentManager';
import { DragManager } from '../../util/DragManager';
import { InteractionUtils } from '../../util/InteractionUtils';
import { ScriptingGlobals } from '../../util/ScriptingGlobals';
@@ -28,8 +30,6 @@ import './CollectionDockingView.scss';
import { CollectionFreeFormView } from './collectionFreeForm';
import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
import { TabDocView } from './TabDocView';
-import { DocumentManager } from '../../util/DocumentManager';
-import { AclAdmin, AclEdit } from '../../../fields/DocSymbols';
import React = require('react');
const _global = (window /* browser */ || global) /* node */ as any;
@@ -118,6 +118,7 @@ export class CollectionDockingView extends CollectionSubView() {
const j = tab.header.parent.contentItems.indexOf(tab.contentItem);
if (j !== -1) {
tab.header.parent.contentItems[j].remove();
+ CollectionDockingView.Instance.endUndoBatch();
return CollectionDockingView.Instance.layoutChanged();
}
}
@@ -164,7 +165,7 @@ export class CollectionDockingView extends CollectionSubView() {
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 && !keyValue);
+ const tab = Array.from(CollectionDockingView.Instance.tabMap).find(tab => tab.DashDoc === document && !tab.contentItem.config.props.keyValue && !keyValue);
if (tab) {
tab.header.parent.setActiveContentItem(tab.contentItem);
return true;
@@ -466,7 +467,10 @@ export class CollectionDockingView extends CollectionSubView() {
this._flush = this._flush ?? UndoManager.StartBatch('tab movement');
if (tab.DashDoc && ![DocumentType.PRES].includes(tab.DashDoc?.type) && !tab.contentItem.config.props.keyValue) {
Doc.AddDocToList(Doc.MyHeaderBar, 'data', tab.DashDoc);
- Doc.AddDocToList(Doc.MyRecentlyClosed, 'data', tab.DashDoc, undefined, true, true);
+ // if you close a tab that is not embedded somewhere else (an embedded Doc can be opened simultaneously in a tab), then add the tab to recently closed
+ if (tab.DashDoc.embedContainer === this.rootDoc) tab.DashDoc.embedContainer = undefined;
+ if (!tab.DashDoc.embedContainer) Doc.AddDocToList(Doc.MyRecentlyClosed, 'data', tab.DashDoc, undefined, true, true);
+ Doc.RemoveDocFromList(Doc.GetProto(tab.DashDoc), 'proto_embeddings', tab.DashDoc);
}
if (CollectionDockingView.Instance) {
const dview = CollectionDockingView.Instance.props.Document;
@@ -495,6 +499,7 @@ export class CollectionDockingView extends CollectionSubView() {
_layout_fitWidth: true,
title: `Untitled Tab ${NumCast(dashboard['pane-count'])}`,
});
+ Doc.AddDocToList(Doc.MyHeaderBar, 'data', docToAdd);
inheritParentAcls(this.rootDoc, docToAdd, false);
CollectionDockingView.AddSplit(docToAdd, OpenWhereMod.none, stack);
}
@@ -538,6 +543,7 @@ export class CollectionDockingView extends CollectionSubView() {
_freeform_backgroundGrid: true,
title: `Untitled Tab ${NumCast(dashboard['pane-count'])}`,
});
+ Doc.AddDocToList(Doc.MyHeaderBar, 'data', docToAdd);
inheritParentAcls(this.dataDoc, docToAdd, false);
CollectionDockingView.AddSplit(docToAdd, OpenWhereMod.none, stack);
}
@@ -546,7 +552,7 @@ export class CollectionDockingView extends CollectionSubView() {
};
render() {
- const href = ImageCast(this.rootDoc.thumb)?.url.href;
+ const href = ImageCast(this.rootDoc.thumb)?.url?.href;
return this.props.renderDepth > -1 ? (
<div>
{href ? (
diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
index 22beb19de..06522b85e 100644
--- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
+++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
@@ -7,7 +7,6 @@ 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, returnFalse, setupMoveUpEvents } from '../../../Utils';
import { Docs } from '../../documents/Documents';
import { DragManager } from '../../util/DragManager';
diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx
index 5135cfb57..ec9d86c1a 100644
--- a/src/client/views/collections/CollectionMenu.tsx
+++ b/src/client/views/collections/CollectionMenu.tsx
@@ -41,6 +41,7 @@ import { COLLECTION_BORDER_WIDTH } from './CollectionView';
import { TabDocView } from './TabDocView';
import { CollectionFreeFormView } from './collectionFreeForm';
import { CollectionLinearView } from './collectionLinear';
+import { media_state } from '../nodes/AudioBox';
interface CollectionMenuProps {
panelHeight: () => number;
@@ -157,7 +158,7 @@ export class CollectionMenu extends AntimodeMenu<CollectionMenuProps> {
<Toggle
toggleType={ToggleType.BUTTON}
type={Type.PRIM}
- color={StrCast(Doc.UserDoc().userColor)}
+ color={SettingsManager.userColor}
onClick={this.toggleTopBar}
toggleStatus={SettingsManager.headerBarHeight > 0}
icon={<FontAwesomeIcon icon={headerIcon} size="lg" />}
@@ -166,7 +167,7 @@ export class CollectionMenu extends AntimodeMenu<CollectionMenuProps> {
<Toggle
toggleType={ToggleType.BUTTON}
type={Type.PRIM}
- color={StrCast(Doc.UserDoc().userColor)}
+ color={SettingsManager.userColor}
onClick={this.toggleProperties}
toggleStatus={SettingsManager.propertiesWidth > 0}
icon={<FontAwesomeIcon icon={propIcon} size="lg" />}
@@ -181,8 +182,8 @@ export class CollectionMenu extends AntimodeMenu<CollectionMenuProps> {
<div
className="collectionMenu-container"
style={{
- background: StrCast(Doc.UserDoc().userBackgroundColor),
- // borderColor: StrCast(Doc.UserDoc().userColor)
+ background: SettingsManager.userBackgroundColor,
+ // borderColor: SettingsManager.userColor
}}>
{this.contMenuButtons}
{hardCodedButtons}
@@ -579,7 +580,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu
@undoBatch
@action
startRecording = () => {
- const doc = Docs.Create.ScreenshotDocument({ title: 'screen recording', _layout_fitWidth: true, _width: 400, _height: 200, mediaState: 'pendingRecording' });
+ const doc = Docs.Create.ScreenshotDocument({ title: 'screen recording', _layout_fitWidth: true, _width: 400, _height: 200, mediaState: media_state.PendingRecording });
CollectionDockingView.AddSplit(doc, OpenWhereMod.right);
};
diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx
index 53a42d2a6..a69049b59 100644
--- a/src/client/views/collections/CollectionNoteTakingView.tsx
+++ b/src/client/views/collections/CollectionNoteTakingView.tsx
@@ -1,10 +1,10 @@
import React = require('react');
import { CursorProperty } from 'csstype';
-import { action, computed, IReactionDisposer, observable, reaction } from 'mobx';
+import { action, computed, IReactionDisposer, observable, reaction, trace } from 'mobx';
import { observer } from 'mobx-react';
import { Doc, Field, Opt } from '../../../fields/Doc';
import { DocData, Height, Width } from '../../../fields/DocSymbols';
-import { Id } from '../../../fields/FieldSymbols';
+import { Copy, Id } from '../../../fields/FieldSymbols';
import { List } from '../../../fields/List';
import { listSpec } from '../../../fields/Schema';
import { SchemaHeaderField } from '../../../fields/SchemaHeaderField';
@@ -93,7 +93,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
// we use availableWidth to convert from a percentage to a pixel count.
@computed get availableWidth() {
const numDividers = this.numGroupColumns - 1;
- return this.maxColWidth - numDividers * this.DividerWidth;
+ return this.maxColWidth - numDividers * this.DividerWidth - 2 * NumCast(this.layoutDoc.xMargin);
}
// children is passed as a prop to the NoteTakingField, which uses this function
@@ -216,7 +216,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
return this.props.styleProvider?.(doc, props, property);
};
- isContentActive = () => this.props.isSelected() || this.props.isContentActive();
+ isContentActive = () => this.props.isContentActive();
blockPointerEventsWhenDragging = () => (this.docsDraggedRowCol.length ? 'none' : undefined);
// getDisplayDoc returns the rules for displaying a document in this view (ie. DocumentView)
@@ -521,6 +521,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
this.observer.observe(ref);
}
}}
+ PanelWidth={this.props.PanelWidth}
select={this.props.select}
addDocument={this.addDocument}
chromeHidden={this.chromeHidden}
@@ -589,6 +590,8 @@ export class CollectionNoteTakingView extends CollectionSubView() {
const rightHeader = this.colHeaderData[colIndex + 1];
leftHeader.setWidth(leftHeader.width + movementX / this.availableWidth);
rightHeader.setWidth(rightHeader.width - movementX / this.availableWidth);
+ const headers = Cast(this.dataDoc[this.fieldKey + '_columnHeaders'], listSpec(SchemaHeaderField), null);
+ headers.splice(headers.indexOf(leftHeader), 1, leftHeader[Copy]());
};
// renderedSections returns a list of all of the JSX elements used (columns and dividers). If the view
@@ -596,17 +599,15 @@ export class CollectionNoteTakingView extends CollectionSubView() {
// allows the user to adjust the column widths.
@computed get renderedSections() {
TraceMobx();
- const entries = Array.from(this.Sections.entries());
- const sections = entries;
- const eles: JSX.Element[] = [];
- for (let i = 0; i < sections.length; i++) {
- const col = this.sectionNoteTaking(sections[i][0], sections[i][1]);
- eles.push(col);
- if (i < sections.length - 1) {
- eles.push(<CollectionNoteTakingViewDivider key={`divider${i}`} index={i} setColumnStartXCoords={this.setColumnStartXCoords} xMargin={this.xMargin} />);
- }
- }
- return eles;
+ const sections = Array.from(this.Sections.entries());
+ return sections.map((sec, i) => (
+ <>
+ {this.sectionNoteTaking(sec[0], sec[1])}
+ {i === sections.length - 1 ? null : ( //
+ <CollectionNoteTakingViewDivider key={`divider${i}`} isContentActive={this.isContentActive} index={i} setColumnStartXCoords={this.setColumnStartXCoords} xMargin={this.xMargin} />
+ )}
+ </>
+ ));
}
@computed get nativeWidth() {
@@ -621,7 +622,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
}
@computed get backgroundEvents() {
- return this.props.isContentActive() === false ? 'none' : undefined;
+ return this.isContentActive() === false ? 'none' : undefined;
}
observer: any;
@@ -636,7 +637,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
style={{
overflowY: this.props.isContentActive() ? 'auto' : 'hidden',
background: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor),
- pointerEvents: this.backgroundEvents ? 'all' : undefined,
+ pointerEvents: this.backgroundEvents,
}}
onScroll={action(e => (this._scroll = e.currentTarget.scrollTop))}
onPointerLeave={action(e => (this.docsDraggedRowCol.length = 0))}
diff --git a/src/client/views/collections/CollectionNoteTakingViewColumn.tsx b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx
index 3286d60bd..52cc21903 100644
--- a/src/client/views/collections/CollectionNoteTakingViewColumn.tsx
+++ b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx
@@ -1,13 +1,13 @@
import React = require('react');
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, observable } from 'mobx';
+import { action, computed, observable, trace } from 'mobx';
import { observer } from 'mobx-react';
import { Doc, DocListCast, Opt } from '../../../fields/Doc';
import { Copy, Id } from '../../../fields/FieldSymbols';
import { RichTextField } from '../../../fields/RichTextField';
import { listSpec } from '../../../fields/Schema';
import { SchemaHeaderField } from '../../../fields/SchemaHeaderField';
-import { Cast } from '../../../fields/Types';
+import { Cast, NumCast } from '../../../fields/Types';
import { ImageField } from '../../../fields/URLField';
import { TraceMobx } from '../../../fields/util';
import { returnEmptyString } from '../../../Utils';
@@ -50,6 +50,7 @@ interface CSVFieldColumnProps {
maxColWidth: number;
dividerWidth: number;
availableWidth: number;
+ PanelWidth: () => number;
}
/**
@@ -62,9 +63,9 @@ export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColu
// columnWidth returns the width of a column in absolute pixels
@computed get columnWidth() {
- if (!this.props.colHeaderData || !this.props.headingObject || this.props.colHeaderData.length === 1) return '100%';
+ if (!this.props.colHeaderData || !this.props.headingObject || this.props.colHeaderData.length === 1) return `${(this.props.availableWidth / this.props.PanelWidth()) * 100}%`;
const i = this.props.colHeaderData.indexOf(this.props.headingObject);
- return this.props.colHeaderData[i].width * 100 + '%';
+ return ((this.props.colHeaderData[i].width * this.props.availableWidth) / this.props.PanelWidth()) * 100 + '%';
}
private dropDisposer?: DragManager.DragDropDisposer;
@@ -297,6 +298,7 @@ export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColu
style={{
width: this.columnWidth,
background: this._background,
+ marginLeft: this.props.headings().findIndex((h: any) => h[0] === this.props.headingObject) === 0 ? NumCast(this.props.Document.xMargin) : 0,
}}
ref={this.createColumnDropRef}
onPointerEnter={this.pointerEntered}
diff --git a/src/client/views/collections/CollectionNoteTakingViewDivider.tsx b/src/client/views/collections/CollectionNoteTakingViewDivider.tsx
index a1309b11f..af822d917 100644
--- a/src/client/views/collections/CollectionNoteTakingViewDivider.tsx
+++ b/src/client/views/collections/CollectionNoteTakingViewDivider.tsx
@@ -1,4 +1,5 @@
-import { action, observable } from 'mobx';
+import { action, observable, trace } from 'mobx';
+import { observer } from 'mobx-react';
import * as React from 'react';
import { emptyFunction, setupMoveUpEvents } from '../../../Utils';
import { UndoManager } from '../../util/UndoManager';
@@ -7,6 +8,7 @@ interface DividerProps {
index: number;
xMargin: number;
setColumnStartXCoords: (movementX: number, colIndex: number) => void;
+ isContentActive: () => boolean | undefined;
}
/**
@@ -14,24 +16,26 @@ interface DividerProps {
* which only appear when there is more than 1 column in CollectionNoteTakingView. Dividers
* are two simple vertical lines that allow the user to alter the widths of CollectionNoteTakingViewColumns.
*/
+@observer
export class CollectionNoteTakingViewDivider extends React.Component<DividerProps> {
@observable private isHoverActive = false;
@observable private isResizingActive = false;
@action
private registerResizing = (e: React.PointerEvent<HTMLDivElement>) => {
- const batch = UndoManager.StartBatch('resizing');
+ let batch: UndoManager.Batch | undefined;
setupMoveUpEvents(
this,
e,
(e, down, delta) => {
+ if (!batch) batch = UndoManager.StartBatch('resizing');
this.props.setColumnStartXCoords(delta[0], this.props.index);
return false;
},
action(() => {
this.isResizingActive = false;
this.isHoverActive = false;
- batch.end();
+ batch?.end();
}),
emptyFunction
);
@@ -46,6 +50,7 @@ export class CollectionNoteTakingViewDivider extends React.Component<DividerProp
display: 'flex',
alignItems: 'center',
cursor: 'col-resize',
+ pointerEvents: this.props.isContentActive() ? 'all' : 'none',
}}
onPointerEnter={action(() => (this.isHoverActive = true))}
onPointerLeave={action(() => !this.isResizingActive && (this.isHoverActive = false))}>
diff --git a/src/client/views/collections/CollectionPileView.tsx b/src/client/views/collections/CollectionPileView.tsx
index 91be31289..91701b213 100644
--- a/src/client/views/collections/CollectionPileView.tsx
+++ b/src/client/views/collections/CollectionPileView.tsx
@@ -1,20 +1,19 @@
-import { action, computed, IReactionDisposer, reaction } from 'mobx';
+import { action, computed, IReactionDisposer } from 'mobx';
import { observer } from 'mobx-react';
import { Doc, DocListCast } from '../../../fields/Doc';
import { Height, Width } from '../../../fields/DocSymbols';
+import { ScriptField } from '../../../fields/ScriptField';
import { NumCast, StrCast } from '../../../fields/Types';
-import { emptyFunction, returnFalse, returnTrue, setupMoveUpEvents } from '../../../Utils';
+import { emptyFunction, returnFalse, 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 { OpenWhere } from '../nodes/DocumentView';
+import { computePassLayout, computeStarburstLayout } from './collectionFreeForm';
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() {
@@ -93,13 +92,14 @@ export class CollectionPileView extends CollectionSubView() {
this.layoutDoc._freeform_panY = -10;
this.props.Document._freeform_pileEngine = computePassLayout.name;
} else {
- const defaultSize = NumCast(this.rootDoc._starburstDiameter, 500);
+ const defaultSize = NumCast(this.rootDoc._starburstDiameter, 400);
this.rootDoc.x = NumCast(this.rootDoc.x) + this.layoutDoc[Width]() / 2 - defaultSize / 2;
this.rootDoc.y = NumCast(this.rootDoc.y) + this.layoutDoc[Height]() / 2 - defaultSize / 2;
this.layoutDoc._freeform_pileWidth = this.layoutDoc[Width]();
this.layoutDoc._freeform_pileHeight = this.layoutDoc[Height]();
this.layoutDoc._freeform_panX = this.layoutDoc._freeform_panY = 0;
this.layoutDoc._width = this.layoutDoc._height = defaultSize;
+ this.layoutDoc.background;
this.props.Document._freeform_pileEngine = computeStarburstLayout.name;
}
});
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index ad84d859d..ad3160a08 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -2,12 +2,12 @@ import React = require('react');
import { action, computed, IReactionDisposer, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import { computedFn } from 'mobx-utils';
-import { Doc, Opt, StrListCast } from '../../../fields/Doc';
+import { Doc, Opt } from '../../../fields/Doc';
import { Id } from '../../../fields/FieldSymbols';
import { List } from '../../../fields/List';
import { listSpec } from '../../../fields/Schema';
import { ComputedField, ScriptField } from '../../../fields/ScriptField';
-import { Cast, NumCast, ScriptCast } from '../../../fields/Types';
+import { Cast, NumCast } from '../../../fields/Types';
import { ImageField } from '../../../fields/URLField';
import { emptyFunction, formatTime, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnNone, returnTrue, returnZero, setupMoveUpEvents, smoothScrollHorizontal, StopEvent } from '../../../Utils';
import { Docs } from '../../documents/Documents';
@@ -32,7 +32,7 @@ import './CollectionStackedTimeline.scss';
export type CollectionStackedTimelineProps = {
Play: () => void;
Pause: () => void;
- playLink: (linkDoc: Doc) => void;
+ playLink: (linkDoc: Doc, options: DocFocusOptions) => void;
playFrom: (seekTimeInSeconds: number, endTime?: number) => void;
playing: () => boolean;
setTime: (time: number) => void;
@@ -161,6 +161,10 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
this.layoutDoc.clipEnd = this.trimEnd;
this._trimming = TrimScope.None;
}
+ @action
+ public CancelTrimming() {
+ this._trimming = TrimScope.None;
+ }
@action
public setZoom(zoom: number) {
@@ -677,7 +681,7 @@ interface StackedTimelineAnchorProps {
height: number;
toTimeline: (screen_delta: number, width: number) => number;
styleProvider?: (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string) => any;
- playLink: (linkDoc: Doc) => void;
+ playLink: (linkDoc: Doc, options: DocFocusOptions) => void;
setTime: (time: number) => void;
startTag: string;
endTag: string;
@@ -716,7 +720,7 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps>
this._disposer = reaction(
() => this.props.currentTimecode(),
time => {
- const dictationDoc = Cast(this.props.layoutDoc['data-dictation'], Doc, null);
+ const dictationDoc = Cast(this.props.layoutDoc.data_dictation, Doc, null);
const isDictation = dictationDoc && LinkManager.Links(this.props.mark).some(link => Cast(link.link_anchor_1, Doc, null)?.annotationOn === dictationDoc);
if (
!LightboxView.LightboxDoc &&
@@ -793,7 +797,7 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps>
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): number | undefined => {
- this.props.playLink(mark);
+ this.props.playLink(mark, options);
return undefined;
};
return {
diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss
index 255bc3889..dddb3ec71 100644
--- a/src/client/views/collections/CollectionStackingView.scss
+++ b/src/client/views/collections/CollectionStackingView.scss
@@ -395,16 +395,23 @@
}
.collectionStackingView-addDocumentButton {
- font-size: 75%;
letter-spacing: 2px;
cursor: pointer;
+ .editableView-container-editing {
+ text-align: right;
+ }
.editableView-input {
outline-color: black;
letter-spacing: 2px;
color: grey;
border: 0px;
+ background: yellow;
+ text-align: right;
padding-top: 10px; // : 12px 10px 11px 10px;
+ input {
+ text-align: right;
+ }
}
}
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index a5c276125..36f8bc101 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -31,6 +31,7 @@ import { CollectionMasonryViewFieldRow } from './CollectionMasonryViewFieldRow';
import './CollectionStackingView.scss';
import { CollectionStackingViewFieldColumn } from './CollectionStackingViewFieldColumn';
import { CollectionSubView } from './CollectionSubView';
+import { SettingsManager } from '../../util/SettingsManager';
const _global = (window /* browser */ || global) /* node */ as any;
export type collectionStackingViewProps = {
@@ -67,7 +68,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
}
// it looks like this gets the column headers that Mehek was showing just now
@computed get colHeaderData() {
- return Cast(this.layoutDoc['_' + this.fieldKey + '_columnHeaders'], listSpec(SchemaHeaderField), null);
+ return Cast(this.dataDoc['_' + 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() {
@@ -120,7 +121,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
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['_' + this.fieldKey + '_columnHeaders'] = new List<SchemaHeaderField>();
+ this.dataDoc['_' + this.fieldKey + '_columnHeaders'] = new List<SchemaHeaderField>();
}
}
@@ -137,7 +138,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
// assuming we need to get rowSpan because we might be dealing with many columns. Grid gap makes sense if multiple columns
const rowSpan = Math.ceil((height() + this.gridGap) / this.gridGap);
// just getting the style
- const style = this.isStackingView ? { width: width(), marginTop: i ? this.gridGap : 0, height: height() } : { gridRowEnd: `span ${rowSpan}` };
+ const style = this.isStackingView ? { margin: this.rootDoc._stacking_alignCenter ? 'auto' : undefined, width: width(), marginTop: i ? this.gridGap : 0, height: height() } : { gridRowEnd: `span ${rowSpan}` };
// So we're choosing whether we're going to render a column or a masonry doc
return (
<div className={`collectionStackingView-${this.isStackingView ? 'columnDoc' : 'masonryDoc'}`} key={d[Id]} style={style}>
@@ -158,7 +159,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
if (!this.pivotField || this.colHeaderData instanceof Promise) return new Map<SchemaHeaderField, Doc[]>();
if (this.colHeaderData === undefined) {
- setTimeout(() => (this.layoutDoc['_' + this.fieldKey + '_columnHeaders'] = new List<SchemaHeaderField>()), 0);
+ setTimeout(() => (this.dataDoc['_' + this.fieldKey + '_columnHeaders'] = new List<SchemaHeaderField>()), 0);
return new Map<SchemaHeaderField, Doc[]>();
}
const colHeaderData = Array.from(this.colHeaderData);
@@ -207,7 +208,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
// reset section headers when a new filter is inputted
this._pivotFieldDisposer = reaction(
() => this.pivotField,
- () => (this.layoutDoc['_' + this.fieldKey + '_columnHeaders'] = new List())
+ () => (this.dataDoc['_' + this.fieldKey + '_columnHeaders'] = new List())
);
this._layout_autoHeightDisposer = reaction(
() => this.layoutDoc._layout_autoHeight,
@@ -426,7 +427,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
className="collectionStackingView-columnDragger"
onPointerDown={this.columnDividerDown}
ref={this._draggerRef}
- style={{ cursor: this._cursor, color: StrCast(Doc.UserDoc().userColor), left: `${this.columnWidth + this.xMargin}px`, top: `${Math.max(0, this.yMargin - 9)}px` }}>
+ style={{ cursor: this._cursor, color: SettingsManager.userColor, left: `${this.columnWidth + this.xMargin}px`, top: `${Math.max(0, this.yMargin - 9)}px` }}>
<FontAwesomeIcon icon={'arrows-alt-h'} />
</div>
);
@@ -598,7 +599,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
action((entries: any) => {
if (this.layoutDoc._layout_autoHeight && ref && this.refList.length && !SnappingManager.GetIsDragging()) {
const height = this.refList.reduce((p, r) => p + Number(getComputedStyle(r).height.replace('px', '')), 0);
- this.props.setHeight?.(this.headerMargin + height);
+ this.props.setHeight?.(2 * this.headerMargin + height); // bcz: added 2x for header to fix problem with scrollbars appearing in Tools panel
}
})
);
@@ -669,7 +670,6 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
if (menuDoc) {
const width: number = NumCast(menuDoc._width, 30);
const height: number = NumCast(menuDoc._height, 30);
- console.log(menuDoc.title, width, height);
return (
<div className="buttonMenu-docBtn" style={{ width: width, height: height }}>
<DocumentView
diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
index ebb4ba5a1..3598d548a 100644
--- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
+++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
@@ -22,9 +22,6 @@ import { EditableView } from '../EditableView';
import './CollectionStackingView.scss';
import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
import { Id } from '../../../fields/FieldSymbols';
-const higflyout = require('@hig/flyout');
-export const { anchorPoints } = higflyout;
-export const Flyout = higflyout.default;
// So this is how we are storing a column
interface CSVFieldColumnProps {
@@ -215,15 +212,16 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
const layoutItems: ContextMenuProps[] = [];
const docItems: ContextMenuProps[] = [];
const dataDoc = this.props.DataDoc || this.props.Document;
-
+ const width = this._ele ? Number(getComputedStyle(this._ele).width.replace('px', '')) : 0;
+ const height = this._ele ? Number(getComputedStyle(this._ele).height.replace('px', '')) : 0;
DocUtils.addDocumentCreatorMenuItems(
doc => {
FormattedTextBox.SelectOnLoad = doc[Id];
return this.props.addDocument?.(doc);
},
this.props.addDocument,
- x,
- y,
+ 0,
+ 0,
true
);
@@ -275,8 +273,11 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
this.props.addDocument?.(created);
}
});
- const pt = this.props.screenToLocalTransform().inverse().transformPoint(x, y);
- ContextMenu.Instance.displayMenu(x, y, undefined, true);
+ const pt = this.props
+ .screenToLocalTransform()
+ .inverse()
+ .transformPoint(width - 30, height);
+ ContextMenu.Instance.displayMenu(pt[0], pt[1], undefined, true);
};
@computed get innards() {
@@ -360,7 +361,11 @@ 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: 'calc(100% - 25px)', maxWidth: this.props.columnWidth / this.props.numGroupColumns - 25, marginBottom: 10 }}>
+ <div
+ key={`${heading}-add-document`}
+ onKeyDown={e => e.stopPropagation()}
+ className="collectionStackingView-addDocumentButton"
+ style={{ width: 'calc(100% - 25px)', maxWidth: this.props.columnWidth / this.props.numGroupColumns - 25, marginBottom: 10 }}>
<EditableView
GetValue={returnEmptyString}
SetValue={this.addNewTextDoc}
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index c189ef126..54a60271a 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -298,7 +298,11 @@ export function CollectionSubView<X>(moreProps?: X) {
let source = split;
if (split.startsWith('data:image') && split.includes('base64')) {
const [{ accessPaths }] = await Networking.PostToServer('/uploadRemoteImage', { sources: [split] });
- source = Utils.prepend(accessPaths.agnostic.client);
+ if (accessPaths.agnostic.client.indexOf('dashblobstore') === -1) {
+ source = Utils.prepend(accessPaths.agnostic.client);
+ } else {
+ source = accessPaths.agnostic.client;
+ }
}
if (source.startsWith('http')) {
const doc = Docs.Create.ImageDocument(source, { ...options, _width: 300 });
@@ -343,10 +347,10 @@ export function CollectionSubView<X>(moreProps?: X) {
}
if (uriList || text) {
- if ((uriList || text).includes('www.youtube.com/watch') || text.includes('www.youtube.com/embed')) {
+ if ((uriList || text).includes('www.youtube.com/watch') || text.includes('www.youtube.com/embed') || text.includes('www.youtube.com/shorts')) {
const batch = UndoManager.StartBatch('youtube upload');
const generatedDocuments: Doc[] = [];
- this.slowLoadDocuments((uriList || text).split('v=')[1].split('&')[0], options, generatedDocuments, text, completed, addDocument).then(batch.end);
+ this.slowLoadDocuments((uriList || text).split('v=').lastElement().split('&')[0].split('shorts/').lastElement(), options, generatedDocuments, text, completed, addDocument).then(batch.end);
return;
}
diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx
index 192d48c64..a8f5345b7 100644
--- a/src/client/views/collections/CollectionTimeView.tsx
+++ b/src/client/views/collections/CollectionTimeView.tsx
@@ -65,9 +65,9 @@ export class CollectionTimeView extends CollectionSubView() {
@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);
+ this._focusFilters = StrListCast(anchor.config_docFilters);
+ this._focusRangeFilters = StrListCast(anchor.config_docRangeFilters);
+ this._focusPivotField = StrCast(anchor.config_pivotField);
return undefined;
};
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 00137736d..eed04b3ee 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -66,17 +66,17 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
return this.props.DataDoc || this.doc;
}
@computed get treeViewtruncateTitleWidth() {
- return NumCast(this.doc.treeViewTruncateTitleWidth, this.panelWidth());
+ return NumCast(this.doc.treeView_TruncateTitleWidth, this.panelWidth());
}
@computed get treeChildren() {
TraceMobx();
return this.props.childDocuments || this.childDocs;
}
@computed get outlineMode() {
- return this.doc.treeViewType === TreeViewType.outline;
+ return this.doc.treeView_Type === TreeViewType.outline;
}
@computed get fileSysMode() {
- return this.doc.treeViewType === TreeViewType.fileSystem;
+ return this.doc.treeView_Type === TreeViewType.fileSystem;
}
@computed get dashboardMode() {
return this.doc === Doc.MyDashboards;
@@ -150,6 +150,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
console.log('WHAAAT');
}
dragData.dropAction = dropAction && !isAlreadyInTree() ? dropAction : sameTree ? 'same' : dragData.dropAction;
+ e.stopPropagation();
}
};
@@ -190,9 +191,9 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
// need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout
if (!Doc.noviceMode) {
const layoutItems: ContextMenuProps[] = [];
- layoutItems.push({ description: 'Make tree state ' + (this.doc.treeViewOpenIsTransient ? 'persistent' : 'transient'), event: () => (this.doc.treeViewOpenIsTransient = !this.doc.treeViewOpenIsTransient), icon: 'paint-brush' });
- layoutItems.push({ description: (this.doc.treeViewHideHeaderFields ? 'Show' : 'Hide') + ' Header Fields', event: () => (this.doc.treeViewHideHeaderFields = !this.doc.treeViewHideHeaderFields), icon: 'paint-brush' });
- layoutItems.push({ description: (this.doc.treeViewHideTitle ? 'Show' : 'Hide') + ' Title', event: () => (this.doc.treeViewHideTitle = !this.doc.treeViewHideTitle), icon: 'paint-brush' });
+ layoutItems.push({ description: 'Make tree state ' + (this.doc.treeView_OpenIsTransient ? 'persistent' : 'transient'), event: () => (this.doc.treeView_OpenIsTransient = !this.doc.treeView_OpenIsTransient), icon: 'paint-brush' });
+ layoutItems.push({ description: (this.doc.treeView_HideHeaderFields ? 'Show' : 'Hide') + ' Header Fields', event: () => (this.doc.treeView_HideHeaderFields = !this.doc.treeView_HideHeaderFields), icon: 'paint-brush' });
+ layoutItems.push({ description: (this.doc.treeView_HideTitle ? 'Show' : 'Hide') + ' Title', event: () => (this.doc.treeView_HideTitle = !this.doc.treeView_HideTitle), icon: 'paint-brush' });
ContextMenu.Instance.addItem({ description: 'Options...', subitems: layoutItems, icon: 'eye' });
const existingOnClick = ContextMenu.Instance.findByDescription('OnClick...');
const onClicks: ContextMenuProps[] = existingOnClick && 'subitems' in existingOnClick ? existingOnClick.subitems : [];
@@ -214,7 +215,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
height={'auto'}
GetValue={() => StrCast(this.dataDoc.title)}
SetValue={undoBatch((value: string, shift: boolean, enter: boolean) => {
- if (enter && this.props.Document.treeViewType === TreeViewType.outline) this.makeTextCollection(this.treeChildren);
+ if (enter && this.props.Document.treeView_Type === TreeViewType.outline) this.makeTextCollection(this.treeChildren);
this.dataDoc.title = value;
return true;
})}
@@ -260,7 +261,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
const icons = StrListCast(this.doc.childContextMenuIcons);
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);
+ headerFields = () => this.props.treeViewHideHeaderFields || BoolCast(this.doc.treeView_HideHeaderFields);
@observable _renderCount = 1;
@computed get treeViewElements() {
TraceMobx();
@@ -388,7 +389,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
const background = () => this.props.styleProvider?.(this.doc, this.props, StyleProp.BackgroundColor);
const color = () => this.props.styleProvider?.(this.doc, this.props, StyleProp.Color);
const pointerEvents = () => (this.props.isContentActive() === false ? 'none' : undefined);
- const titleBar = this.props.treeViewHideTitle || this.doc.treeViewHideTitle ? null : this.titleBar;
+ const titleBar = this.props.treeViewHideTitle || this.doc.treeView_HideTitle ? null : this.titleBar;
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%', pointerEvents: 'all' }}>
{!this.buttonMenu && !this.noviceExplainer ? null : (
@@ -400,7 +401,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
<div
className="collectionTreeView-contents"
key="tree"
- ref={r => !this.doc.treeViewHasOverlay && r && this.createTreeDropTarget(r)}
+ ref={r => !this.doc.treeView_HasOverlay && r && this.createTreeDropTarget(r)}
style={{
...(!titleBar ? { marginLeft: this.marginX(), paddingTop: this.marginTop() } : {}),
color: color(),
@@ -425,6 +426,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
minHeight: '100%',
}}
onWheel={e => e.stopPropagation()}
+ onClick={e => (!this.layoutDoc.forceActive ? this.props.select(false) : SelectionManager.DeselectAll())}
onDrop={this.onTreeDrop}>
<ul className={`no-indent${this.outlineMode ? '-outline' : ''}`}>{this.treeViewElements}</ul>
</div>
@@ -439,7 +441,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
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 ? (
+ {!(this.doc instanceof Doc) || !this.treeChildren ? null : this.doc.treeView_HasOverlay ? (
<CollectionFreeFormView
{...this.props}
setContentView={emptyFunction}
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index c33519afb..ce19b3f9b 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -1,7 +1,7 @@
import { computed, observable, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { Doc, DocListCast } from '../../../fields/Doc';
+import { Doc, DocListCast, Opt } from '../../../fields/Doc';
import { ObjectField } from '../../../fields/ObjectField';
import { ScriptField } from '../../../fields/ScriptField';
import { BoolCast, Cast, ScriptCast, StrCast } from '../../../fields/Types';
@@ -40,8 +40,8 @@ interface CollectionViewProps_ extends FieldViewProps {
isAnnotationOverlay?: boolean; // is the collection an annotation overlay (eg an overlay on an image/video/etc)
isAnnotationOverlayScrollable?: boolean; // whether the annotation overlay can be vertically scrolled (just for tree views, currently)
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;
+ setPreviewCursor?: (func: (x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void) => void;
+ setBrushViewer?: (func?: (view: { width: number; height: number; panX: number; panY: number }, transTime: number) => void) => void;
ignoreUnrendered?: boolean;
// property overrides for child documents
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index 67b7b39dd..dab53b671 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -1,6 +1,6 @@
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Tooltip } from '@material-ui/core';
+import { Popup, Type } from 'browndash-components';
import { clamp } from 'lodash';
import { action, computed, IReactionDisposer, observable, ObservableSet, reaction } from 'mobx';
import { observer } from 'mobx-react';
@@ -10,20 +10,20 @@ import { DocData, Height, Width } from '../../../fields/DocSymbols';
import { Id } from '../../../fields/FieldSymbols';
import { List } from '../../../fields/List';
import { FieldId } from '../../../fields/RefField';
-import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../fields/Types';
+import { Cast, DocCast, NumCast, StrCast } from '../../../fields/Types';
import { emptyFunction, lightOrDark, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents, simulateMouseClick, Utils } from '../../../Utils';
import { DocServer } from '../../DocServer';
import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
import { DocumentManager } from '../../util/DocumentManager';
import { DragManager, dropActionType } from '../../util/DragManager';
import { SelectionManager } from '../../util/SelectionManager';
+import { SettingsManager } from '../../util/SettingsManager';
import { SnappingManager } from '../../util/SnappingManager';
import { Transform } from '../../util/Transform';
import { undoable, UndoManager } from '../../util/UndoManager';
import { DashboardView } from '../DashboardView';
-import { Colors, Shadows } from '../global/globalEnums';
+import { Colors } from '../global/globalEnums';
import { LightboxView } from '../LightboxView';
-import { MainView } from '../MainView';
import { DocFocusOptions, DocumentView, DocumentViewProps, OpenWhere, OpenWhereMod } from '../nodes/DocumentView';
import { DashFieldView } from '../nodes/formattedText/DashFieldView';
import { KeyValueBox } from '../nodes/KeyValueBox';
@@ -34,7 +34,6 @@ import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormV
import { CollectionView } from './CollectionView';
import './TabDocView.scss';
import React = require('react');
-import { Popup, Toggle, Type } from 'browndash-components';
const _global = (window /* browser */ || global) /* node */ as any;
interface TabDocViewProps {
@@ -137,7 +136,11 @@ export class TabDocView extends React.Component<TabDocViewProps> {
setupMoveUpEvents(
this,
e,
- e => !e.defaultPrevented && DragManager.StartDocumentDrag([iconWrap], new DragManager.DocumentDragData([doc], doc.dropAction as dropActionType), e.clientX, e.clientY),
+ e =>
+ !e.defaultPrevented &&
+ DragManager.StartDocumentDrag([iconWrap], new DragManager.DocumentDragData([doc], doc.dropAction as dropActionType), e.clientX, e.clientY, undefined, () => {
+ CollectionDockingView.CloseSplit(doc);
+ }),
returnFalse,
action(e => {
if (this.view) {
@@ -213,15 +216,15 @@ export class TabDocView extends React.Component<TabDocViewProps> {
);
// highlight the tab when the tab document is brushed in any part of the UI
- tab._disposers.reactionDisposer = reaction(
- () => ({ title: doc.title, degree: Doc.IsBrushedDegree(doc) }),
- ({ title, degree }) => {
- //titleEle.value = title;
- // titleEle.style.padding = degree ? 0 : 2;
- // titleEle.style.border = `${["gray", "gray", "gray"][degree]} ${["none", "dashed", "solid"][degree]} 2px`;
- },
- { fireImmediately: true }
- );
+ // tab._disposers.reactionDisposer = reaction(
+ // () => ({ title: doc.title, degree: Doc.IsBrushedDegree(doc) }),
+ // ({ title, degree }) => {
+ // titleEle.value = title;
+ // titleEle.style.padding = degree ? 0 : 2;
+ // titleEle.style.border = `${['gray', 'gray', 'gray'][degree]} ${['none', 'dashed', 'solid'][degree]} 2px`;
+ // },
+ // { fireImmediately: true }
+ // );
// clean up the tab when it is closed
tab.closeElement
@@ -256,46 +259,45 @@ export class TabDocView extends React.Component<TabDocViewProps> {
return;
}
const anchorDoc = DocumentManager.Instance.getDocumentView(doc)?.ComponentView?.getAnchor?.(false, pinProps);
- const pinDoc = Doc.MakeDelegate(anchorDoc && anchorDoc !== doc ? anchorDoc : doc);
- pinDoc.presentationTargetDoc = anchorDoc ?? doc;
+ const pinDoc = anchorDoc?.type === DocumentType.CONFIG ? anchorDoc : Doc.MakeDelegate(anchorDoc && anchorDoc !== doc ? anchorDoc : doc);
+ pinDoc.presentation_targetDoc = anchorDoc ?? doc;
pinDoc.title = doc.title + ' - Slide';
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.presentation_movement = doc.type === DocumentType.SCRIPTING || pinProps?.pinDocLayout ? PresMovement.None : PresMovement.Zoom;
+ pinDoc.presentation_duration = pinDoc.presentation_duration ?? 1000;
+ pinDoc.presentation_groupWithUp = false;
Doc.SetContainer(pinDoc, curPres);
// these should potentially all be props passed down by the CollectionTreeView to the TreeView elements. That way the PresBox could configure all of its children at render time
- pinDoc.treeViewRenderAsBulletHeader = true; // forces a tree view to render the document next to the bullet in the header area
- pinDoc.treeViewHeaderWidth = '100%'; // forces the header to grow to be the same size as its largest sibling.
- pinDoc.treeViewChildrenOnRoot = true; // tree view will look for hierarchical children on the root doc, not the data doc.
- pinDoc.treeViewFieldKey = 'data'; // tree view will treat the 'data' field as the field where the hierarchical children are located instead of using the document's layout string field
- pinDoc.treeViewExpandedView = 'data'; // in case the data doc has an expandedView set, this will mask that field and use the 'data' field when expanding the tree view
- pinDoc.treeViewHideHeaderIfTemplate = true; // this will force the document to render itself as the tree view header
+ pinDoc.treeView = ''; // not really needed, but makes key value pane look better
+ pinDoc.treeView_RenderAsBulletHeader = true; // forces a tree view to render the document next to the bullet in the header area
+ pinDoc.treeView_HeaderWidth = '100%'; // forces the header to grow to be the same size as its largest sibling.
+ pinDoc.treeView_ChildrenOnRoot = true; // tree view will look for hierarchical children on the root doc, not the data doc.
+ pinDoc.treeView_FieldKey = '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.treeView_ExpandedView = '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.treeView_HideHeaderIfTemplate = true; // this will force the document to render itself as the tree view header
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);
+ pinDoc.presentation_mediaStart = 'manual';
+ pinDoc.presentation_mediaStop = 'manual';
}
if (pinProps?.activeFrame !== undefined) {
- pinDoc.presActiveFrame = pinProps?.activeFrame;
+ pinDoc.config_activeFrame = pinProps?.activeFrame;
pinDoc.title = doc.title + ' (move)';
- pinDoc.presMovement = PresMovement.Pan;
+ pinDoc.presentation_movement = PresMovement.Pan;
}
if (pinProps?.currentFrame !== undefined) {
- pinDoc.presCurrentFrame = pinProps?.currentFrame;
+ pinDoc.config_currentFrame = pinProps?.currentFrame;
pinDoc.title = doc.title + ' (move)';
- pinDoc.presMovement = PresMovement.Pan;
+ pinDoc.presentation_movement = PresMovement.Pan;
}
if (pinDoc.stroke_isInkMask) {
- pinDoc.presHideAfter = true;
- pinDoc.presHideBefore = true;
- pinDoc.presMovement = PresMovement.None;
+ pinDoc.presentation_hideAfter = true;
+ pinDoc.presentation_hideBefore = true;
+ pinDoc.presentation_movement = PresMovement.None;
}
- if (curPres.expandBoolean) pinDoc.presExpandInlineButton = true;
+ if (curPres.expandBoolean) pinDoc.presentation_expandInlineButton = true;
Doc.AddDocToList(curPres, 'data', pinDoc, PresBox.Instance?.sortArray()?.lastElement());
PresBox.Instance?.clearSelectedArray();
pinDoc && PresBox.Instance?.addToSelectedArray(pinDoc); //Update selected array
@@ -347,7 +349,7 @@ export class TabDocView extends React.Component<TabDocViewProps> {
private onActiveContentItemChanged(contentItem: any) {
if (!contentItem || (this.stack === contentItem.parent && ((contentItem?.tab === this.tab && !this._isActive) || (contentItem?.tab !== this.tab && this._isActive)))) {
this._activated = this._isActive = !contentItem || contentItem?.tab === this.tab;
- if (!this._view) setTimeout(() => SelectionManager.SelectView(this._view, false));
+ if (!this._view && this.tab?.contentItem?.config?.props?.panelName !== 'dontSelectOnActivate') setTimeout(() => SelectionManager.SelectView(this._view, false));
!this._isActive && this._document && Doc.UnBrushDoc(this._document); // bcz: bad -- trying to simulate a pointer leave event when a new tab is opened up on top of an existing one.
}
}
@@ -379,7 +381,7 @@ export class TabDocView extends React.Component<TabDocViewProps> {
return LightboxView.AddDocTab(doc, location);
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.toggle: return CollectionDockingView.ToggleSplit(doc, whereMods, this.stack, "dontSelectOnActivate", keyValue);
case OpenWhere.add:default:return CollectionDockingView.AddSplit(doc, whereMods, this.stack, undefined, keyValue);
}
};
@@ -393,10 +395,10 @@ export class TabDocView extends React.Component<TabDocViewProps> {
};
getCurrentFrame = () => {
- return NumCast(Cast(PresBox.Instance.activeItem.presentationTargetDoc, Doc, null)._currentFrame);
+ return NumCast(Cast(PresBox.Instance.activeItem.presentation_targetDoc, Doc, null)._currentFrame);
};
static Activate = (tabDoc: Doc) => {
- const tab = Array.from(CollectionDockingView.Instance?.tabMap!).find(tab => tab.DashDoc === tabDoc);
+ const tab = Array.from(CollectionDockingView.Instance?.tabMap!).find(tab => tab.DashDoc === tabDoc && !tab.contentItem.config.props.keyValue);
tab?.header.parent.setActiveContentItem(tab.contentItem); // glr: Panning does not work when this is set - (this line is for trying to make a tab that is not topmost become topmost)
return tab !== undefined;
};
@@ -418,9 +420,10 @@ export class TabDocView extends React.Component<TabDocViewProps> {
PanelHeight = () => this._panelHeight;
miniMapColor = () => Colors.MEDIUM_GRAY;
tabView = () => this._view;
- disableMinimap = () => !this._document || this._document.layout !== CollectionView.LayoutString(Doc.LayoutFieldKey(this._document)) || this._document?._type_collection !== CollectionViewType.Freeform;
+ disableMinimap = () => !this._document;
whenChildContentActiveChanges = (isActive: boolean) => (this._isAnyChildContentActive = isActive);
isContentActive = () => this._isContentActive;
+ waitForDoubleClick = () => (DocumentView.ExploreMode ? 'never' : undefined);
@computed get docView() {
return !this._activated || !this._document ? null : (
<>
@@ -436,8 +439,8 @@ export class TabDocView extends React.Component<TabDocViewProps> {
hideTitle={this.props.keyValue}
Document={this._document}
DataDoc={!Doc.AreProtosEqual(this._document[DocData], this._document) ? this._document[DocData] : undefined}
- onBrowseClick={MainView.Instance.exploreMode}
- waitForDoubleClickToClick={MainView.Instance.waitForDoubleClick}
+ onBrowseClick={DocumentView.exploreMode}
+ waitForDoubleClickToClick={this.waitForDoubleClick}
isContentActive={this.isContentActive}
isDocumentActive={returnFalse}
PanelWidth={this.PanelWidth}
@@ -458,9 +461,7 @@ export class TabDocView extends React.Component<TabDocViewProps> {
bringToFront={emptyFunction}
pinToPres={TabDocView.PinDoc}
/>
- {this.disableMinimap() || this._document._type_collection !== CollectionViewType.Freeform ? null : (
- <TabMinimapView key="minimap" addDocTab={this.addDocTab} PanelHeight={this.PanelHeight} PanelWidth={this.PanelWidth} background={this.miniMapColor} document={this._document} tabView={this.tabView} />
- )}
+ {this.disableMinimap() ? null : <TabMinimapView key="minimap" addDocTab={this.addDocTab} PanelHeight={this.PanelHeight} PanelWidth={this.PanelWidth} background={this.miniMapColor} document={this._document} tabView={this.tabView} />}
</>
);
}
@@ -472,7 +473,7 @@ export class TabDocView extends React.Component<TabDocViewProps> {
style={{
fontFamily: Doc.UserDoc().renderStyle === 'comic' ? 'Comic Sans MS' : undefined,
}}
- onPointerEnter={action(() => (this._hovering = true))}
+ onPointerOver={action(() => (this._hovering = true))}
onPointerLeave={action(() => (this._hovering = false))}
onDragOver={action(() => (this._hovering = true))}
onDragLeave={action(() => (this._hovering = false))}
@@ -501,6 +502,18 @@ interface TabMinimapViewProps {
PanelHeight: () => number;
background: () => string;
}
+interface TabMiniThumbProps {
+ miniWidth: () => number;
+ miniHeight: () => number;
+ miniTop: () => number;
+ miniLeft: () => number;
+}
+@observer
+class TabMiniThumb extends React.Component<TabMiniThumbProps> {
+ render() {
+ return <div className="miniThumb" style={{ width: `${this.props.miniWidth()}% `, height: `${this.props.miniHeight()}% `, left: `${this.props.miniLeft()}% `, top: `${this.props.miniTop()}% ` }} />;
+ }
+}
@observer
export class TabMinimapView extends React.Component<TabMinimapViewProps> {
static miniStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string): any => {
@@ -512,25 +525,17 @@ export class TabMinimapView extends React.Component<TabMinimapViewProps> {
return 'none';
case StyleProp.DocContents:
const background = ((type: DocumentType) => {
+ // prettier-ignore
switch (type) {
- case DocumentType.PDF:
- return 'pink';
- case DocumentType.AUDIO:
- return 'lightgreen';
- case DocumentType.WEB:
- return 'brown';
- case DocumentType.IMG:
- return 'blue';
- case DocumentType.MAP:
- return 'orange';
- case DocumentType.VID:
- return 'purple';
- case DocumentType.RTF:
- return 'yellow';
- case DocumentType.COL:
- return undefined;
- default:
- return 'gray';
+ case DocumentType.PDF: return 'pink';
+ case DocumentType.AUDIO: return 'lightgreen';
+ case DocumentType.WEB: return 'brown';
+ case DocumentType.IMG: return 'blue';
+ case DocumentType.MAP: return 'orange';
+ case DocumentType.VID: return 'purple';
+ case DocumentType.RTF: return 'yellow';
+ case DocumentType.COL: return undefined;
+ default: return 'gray';
}
})(doc.type as DocumentType);
return !background ? undefined : <div style={{ width: doc[Width](), height: doc[Height](), position: 'absolute', display: 'block', background }} />;
@@ -550,13 +555,13 @@ export class TabMinimapView extends React.Component<TabMinimapViewProps> {
returnMiniSize = () => NumCast(this.props.document._miniMapSize, 150);
miniDown = (e: React.PointerEvent) => {
const doc = this.props.document;
- const renderBounds = this.renderBounds ?? { l: 0, r: 0, t: 0, b: 0, dim: 1 };
const miniSize = this.returnMiniSize();
doc &&
setupMoveUpEvents(
this,
e,
action((e: PointerEvent, down: number[], delta: number[]) => {
+ const renderBounds = this.renderBounds ?? { l: 0, r: 0, t: 0, b: 0, dim: 1 };
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;
@@ -565,60 +570,57 @@ export class TabMinimapView extends React.Component<TabMinimapViewProps> {
emptyFunction
);
};
- render() {
- if (!this.renderBounds) return null;
- const miniWidth = (this.props.PanelWidth() / NumCast(this.props.document._freeform_scale, 1) / this.renderBounds.dim) * 100;
- const miniHeight = (this.props.PanelHeight() / NumCast(this.props.document._freeform_scale, 1) / this.renderBounds.dim) * 100;
- const miniLeft = 50 + ((NumCast(this.props.document._freeform_panX) - this.renderBounds.cx) / this.renderBounds.dim) * 100 - miniWidth / 2;
- const miniTop = 50 + ((NumCast(this.props.document._freeform_panY) - this.renderBounds.cy) / this.renderBounds.dim) * 100 - miniHeight / 2;
+ popup = () => {
+ if (!this.renderBounds) return <></>;
+ const renderBounds = this.renderBounds;
+ const miniWidth = () => (this.props.PanelWidth() / NumCast(this.props.document._freeform_scale, 1) / renderBounds.dim) * 100;
+ const miniHeight = () => (this.props.PanelHeight() / NumCast(this.props.document._freeform_scale, 1) / renderBounds.dim) * 100;
+ const miniLeft = () => 50 + ((NumCast(this.props.document._freeform_panX) - renderBounds.cx) / renderBounds.dim) * 100 - miniWidth() / 2;
+ const miniTop = () => 50 + ((NumCast(this.props.document._freeform_panY) - renderBounds.cy) / renderBounds.dim) * 100 - miniHeight() / 2;
const miniSize = this.returnMiniSize();
return (
- <div className="miniMap-hidden">
- <Popup
- icon={<FontAwesomeIcon icon="globe-asia" size="lg" />}
- color={StrCast(Doc.UserDoc().userVariantColor, Colors.MEDIUM_BLUE)}
- type={Type.TERT}
- onPointerDown={e => e.stopPropagation()}
- placement={'top-end'}
- popup={
- <div className="miniMap" style={{ width: miniSize, height: miniSize, background: this.props.background() }}>
- <CollectionFreeFormView
- Document={this.props.document}
- docViewPath={returnEmptyDoclist}
- childLayoutTemplate={this.childLayoutTemplate} // bcz: Ugh .. should probably be rendering a CollectionView or the minimap should be part of the collectionFreeFormView to avoid having to set stuff like this.
- noOverlay={true} // don't render overlay Docs since they won't scale
- setHeight={returnFalse}
- isContentActive={emptyFunction}
- isAnyChildContentActive={returnFalse}
- select={emptyFunction}
- isSelected={returnFalse}
- dontRegisterView={true}
- fieldKey={Doc.LayoutFieldKey(this.props.document)}
- bringToFront={emptyFunction}
- rootSelected={returnTrue}
- addDocument={returnFalse}
- moveDocument={returnFalse}
- removeDocument={returnFalse}
- PanelWidth={this.returnMiniSize}
- PanelHeight={this.returnMiniSize}
- ScreenToLocalTransform={Transform.Identity}
- renderDepth={0}
- whenChildContentsActiveChanged={emptyFunction}
- focus={emptyFunction}
- styleProvider={TabMinimapView.miniStyleProvider}
- addDocTab={this.props.addDocTab}
- pinToPres={TabDocView.PinDoc}
- childFilters={CollectionDockingView.Instance?.childDocFilters ?? returnEmptyDoclist}
- childFiltersByRanges={CollectionDockingView.Instance?.childDocRangeFilters ?? returnEmptyDoclist}
- searchFilterDocs={CollectionDockingView.Instance?.searchFilterDocs ?? returnEmptyDoclist}
- fitContentsToBox={returnTrue}
- />
- <div className="miniOverlay" onPointerDown={this.miniDown}>
- <div className="miniThumb" style={{ width: `${miniWidth}% `, height: `${miniHeight}% `, left: `${miniLeft}% `, top: `${miniTop}% ` }} />
- </div>
- </div>
- }
+ <div className="miniMap" style={{ width: miniSize, height: miniSize, background: this.props.background() }}>
+ <CollectionFreeFormView
+ Document={this.props.document}
+ docViewPath={returnEmptyDoclist}
+ childLayoutTemplate={this.childLayoutTemplate} // bcz: Ugh .. should probably be rendering a CollectionView or the minimap should be part of the collectionFreeFormView to avoid having to set stuff like this.
+ noOverlay={true} // don't render overlay Docs since they won't scale
+ setHeight={returnFalse}
+ isContentActive={emptyFunction}
+ isAnyChildContentActive={returnFalse}
+ select={emptyFunction}
+ isSelected={returnFalse}
+ dontRegisterView={true}
+ fieldKey={Doc.LayoutFieldKey(this.props.document)}
+ bringToFront={emptyFunction}
+ rootSelected={returnTrue}
+ addDocument={returnFalse}
+ moveDocument={returnFalse}
+ removeDocument={returnFalse}
+ PanelWidth={this.returnMiniSize}
+ PanelHeight={this.returnMiniSize}
+ ScreenToLocalTransform={Transform.Identity}
+ renderDepth={0}
+ whenChildContentsActiveChanged={emptyFunction}
+ focus={emptyFunction}
+ styleProvider={TabMinimapView.miniStyleProvider}
+ addDocTab={this.props.addDocTab}
+ pinToPres={TabDocView.PinDoc}
+ childFilters={CollectionDockingView.Instance?.childDocFilters ?? returnEmptyDoclist}
+ childFiltersByRanges={CollectionDockingView.Instance?.childDocRangeFilters ?? returnEmptyDoclist}
+ searchFilterDocs={CollectionDockingView.Instance?.searchFilterDocs ?? returnEmptyDoclist}
+ fitContentsToBox={returnTrue}
/>
+ <div className="miniOverlay" onPointerDown={this.miniDown}>
+ <TabMiniThumb miniLeft={miniLeft} miniTop={miniTop} miniWidth={miniWidth} miniHeight={miniHeight} />
+ </div>
+ </div>
+ );
+ };
+ render() {
+ return this.props.document.layout !== CollectionView.LayoutString(Doc.LayoutFieldKey(this.props.document)) || this.props.document?._type_collection !== CollectionViewType.Freeform ? null : (
+ <div className="miniMap-hidden">
+ <Popup icon={<FontAwesomeIcon icon="globe-asia" size="lg" />} color={SettingsManager.userVariantColor} type={Type.TERT} onPointerDown={e => e.stopPropagation()} placement={'top-end'} popup={this.popup} />
</div>
);
}
diff --git a/src/client/views/collections/TreeSort.ts b/src/client/views/collections/TreeSort.ts
new file mode 100644
index 000000000..977acd030
--- /dev/null
+++ b/src/client/views/collections/TreeSort.ts
@@ -0,0 +1,6 @@
+export enum TreeSort {
+ AlphaUp = 'alphabetical from z',
+ AlphaDown = 'alphabetical from A',
+ Zindex = 'by Z index',
+ WhenAdded = 'when added',
+}
diff --git a/src/client/views/collections/TreeView.scss b/src/client/views/collections/TreeView.scss
index d22e85880..0cc11bf1c 100644
--- a/src/client/views/collections/TreeView.scss
+++ b/src/client/views/collections/TreeView.scss
@@ -23,6 +23,26 @@
.treeView-bulletIcons {
width: 100%;
height: 100%;
+ // changes start here.
+
+ .treeView-expandIcon {
+ display: none;
+ left: -8px;
+ position: absolute;
+ }
+
+ .treeView-checkIcon {
+ left: 3.5px;
+ top: 2px;
+ position: absolute;
+ }
+
+ &:hover {
+ .treeView-expandIcon {
+ display: unset;
+ }
+ }
+ // end changes
position: relative;
display: flex;
flex-direction: row;
@@ -121,7 +141,6 @@
filter: opacity(0.2) !important;
}
}
-
//align-items: center;
::-webkit-scrollbar {
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index fb23fc7f1..832e102bc 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -2,7 +2,7 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed, IReactionDisposer, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
-import { Doc, DocListCast, Field, Opt, StrListCast } from '../../../fields/Doc';
+import { Doc, DocListCast, Field, FieldResult, Opt, StrListCast } from '../../../fields/Doc';
import { DocData, Height, Width } from '../../../fields/DocSymbols';
import { Id } from '../../../fields/FieldSymbols';
import { List } from '../../../fields/List';
@@ -35,6 +35,8 @@ import { CollectionView } from './CollectionView';
import './TreeView.scss';
import React = require('react');
import { IconButton, Size } from 'browndash-components';
+import { TreeSort } from './TreeSort';
+import { SettingsManager } from '../../util/SettingsManager';
export interface TreeViewProps {
treeView: CollectionTreeView;
@@ -77,18 +79,12 @@ const treeBulletWidth = function () {
return Number(TREE_BULLET_WIDTH.replace('px', ''));
};
-export enum TreeSort {
- Up = 'up',
- Down = 'down',
- Zindex = 'z',
- None = 'none',
-}
/**
* Renders a treeView of a collection of documents
*
* special fields:
- * treeViewOpen : flag denoting whether the documents sub-tree (contents) is visible or hidden
- * treeViewExpandedView : name of field whose contents are being displayed as the document's subtree
+ * treeView_Open : flag denoting whether the documents sub-tree (contents) is visible or hidden
+ * treeView_ExpandedView : name of field whose contents are being displayed as the document's subtree
*/
@observer
export class TreeView extends React.Component<TreeViewProps> {
@@ -104,16 +100,16 @@ export class TreeView extends React.Component<TreeViewProps> {
private _treedropDisposer?: DragManager.DragDropDisposer;
get treeViewOpenIsTransient() {
- return this.props.treeView.doc.treeViewOpenIsTransient || Doc.IsDataProto(this.doc);
+ return this.props.treeView.doc.treeView_OpenIsTransient || Doc.IsDataProto(this.doc);
}
set treeViewOpen(c: boolean) {
if (this.treeViewOpenIsTransient) this._transientOpenState = c;
else {
- this.doc.treeViewOpen = c;
+ this.doc.treeView_Open = c;
this._transientOpenState = false;
}
}
- @observable _transientOpenState = false; // override of the treeViewOpen field allowing the display state to be independent of the document's state
+ @observable _transientOpenState = false; // override of the treeView_Open field allowing the display state to be independent of the document's state
@observable _editTitle: boolean = false;
@observable _dref: DocumentView | undefined | null;
get displayName() {
@@ -127,34 +123,34 @@ export class TreeView extends React.Component<TreeViewProps> {
: this.props.treeView.fileSysMode
? this.doc.isFolder
? this.fieldKey
- : 'embeddings' // for displaying
+ : 'data' // file system folders display their contents (data). used to be they displayed their embeddings but now its a tree structure and not a flat list
: this.props.treeView.outlineMode || this.childDocs
? this.fieldKey
: Doc.noviceMode
? 'layout'
- : StrCast(this.props.treeView.doc.treeViewExpandedView, 'fields');
+ : StrCast(this.props.treeView.doc.treeView_ExpandedView, 'fields');
}
@computed get doc() {
return this.props.document;
}
@computed get treeViewOpen() {
- return (!this.treeViewOpenIsTransient && Doc.GetT(this.doc, 'treeViewOpen', 'boolean', true)) || this._transientOpenState;
+ return (!this.treeViewOpenIsTransient && Doc.GetT(this.doc, 'treeView_Open', 'boolean', true)) || this._transientOpenState;
}
@computed get treeViewExpandedView() {
- return this.validExpandViewTypes.includes(StrCast(this.doc.treeViewExpandedView)) ? StrCast(this.doc.treeViewExpandedView) : this.defaultExpandedView;
+ return this.validExpandViewTypes.includes(StrCast(this.doc.treeView_ExpandedView)) ? StrCast(this.doc.treeView_ExpandedView) : this.defaultExpandedView;
}
@computed get MAX_EMBED_HEIGHT() {
return NumCast(this.props.treeViewParent.maxEmbedHeight, 200);
}
@computed get dataDoc() {
- return this.props.document.treeViewChildrenOnRoot ? this.doc : this.doc[DocData];
+ return this.props.document.treeView_ChildrenOnRoot ? this.doc : this.doc[DocData];
}
@computed get layoutDoc() {
return Doc.Layout(this.doc);
}
@computed get fieldKey() {
- return StrCast(this.doc._treeViewFieldKey, Doc.LayoutFieldKey(this.doc));
+ return StrCast(this.doc._treeView_FieldKey, Doc.LayoutFieldKey(this.doc));
}
@computed get childDocs() {
return this.childDocList(this.fieldKey);
@@ -227,6 +223,38 @@ export class TreeView extends React.Component<TreeViewProps> {
}
};
+ @undoBatch
+ @action
+ recurToggle = (childList: Doc[]) => {
+ if (childList.length > 0) {
+ childList.forEach(child => {
+ child.runProcess = !!!child.runProcess;
+ TreeView.ToggleChildrenRun.get(child)?.();
+ });
+ }
+ };
+
+ @undoBatch
+ @action
+ getRunningChildren = (childList: Doc[]) => {
+ if (childList.length === 0) {
+ return [];
+ }
+
+ const runningChildren: FieldResult[] = [];
+ childList.forEach(child => {
+ if (child.runProcess && TreeView.GetRunningChildren.get(child)) {
+ if (child.runProcess) {
+ runningChildren.push(child);
+ }
+ runningChildren.push(...(TreeView.GetRunningChildren.get(child)?.() ?? []));
+ }
+ });
+ return runningChildren;
+ };
+
+ static GetRunningChildren = new Map<Doc, any>();
+ static ToggleChildrenRun = new Map<Doc, () => void>();
constructor(props: any) {
super(props);
if (!TreeView._openLevelScript) {
@@ -235,6 +263,17 @@ export class TreeView extends React.Component<TreeViewProps> {
}
this._openScript = Doc.IsSystem(this.props.document) ? undefined : () => TreeView._openLevelScript!;
this._editTitleScript = Doc.IsSystem(this.props.document) ? () => TreeView._openLevelScript! : () => TreeView._openTitleScript!;
+
+ // set for child processing highligting
+ this.dataDoc.hasChildren = this.childDocs.length > 0;
+ // this.dataDoc.children = this.childDocs;
+ TreeView.ToggleChildrenRun.set(this.doc, () => {
+ this.recurToggle(this.childDocs);
+ });
+
+ TreeView.GetRunningChildren.set(this.doc, () => {
+ return this.getRunningChildren(this.childDocs);
+ });
}
_treeEle: any;
@@ -294,7 +333,7 @@ export class TreeView extends React.Component<TreeViewProps> {
const pt = [e.clientX, e.clientY];
const rect = this._header.current!.getBoundingClientRect();
const before = pt[1] < rect.top + rect.height / 2;
- const inside = pt[0] > Math.min(rect.left + 75, rect.left + rect.width * 0.75) || (!before && this.treeViewOpen && this.childDocs?.length);
+ const inside = pt[0] > rect.left + rect.width * 0.33 || (!before && this.treeViewOpen && this.childDocs?.length);
this._header.current!.className = 'treeView-header';
if (!this.props.treeView.outlineMode || DragManager.DocDragData?.treeViewDoc === this.props.treeView.rootDoc) {
if (inside) this._header.current!.className += ' treeView-header-inside';
@@ -308,13 +347,13 @@ export class TreeView extends React.Component<TreeViewProps> {
const bullet = Docs.Create.TextDocument('', {
layout: CollectionView.LayoutString('data'),
title: '-title-',
- treeViewExpandedViewLock: true,
- treeViewExpandedView: 'data',
+ treeView_ExpandedViewLock: true,
+ treeView_ExpandedView: 'data',
_type_collection: CollectionViewType.Tree,
layout_hideLinkButton: true,
_layout_showSidebar: true,
_layout_fitWidth: true,
- treeViewType: TreeViewType.outline,
+ treeView_Type: TreeViewType.outline,
x: 0,
y: 0,
_xMargin: 0,
@@ -354,7 +393,7 @@ export class TreeView extends React.Component<TreeViewProps> {
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);
+ const inside = this.props.treeView.fileSysMode && !this.doc.isFolder ? false : pt[0] > rect.left + rect.width * 0.33 || (!before && this.treeViewOpen && this.childDocs?.length ? true : false);
if (de.complete.linkDragData) {
const sourceDoc = de.complete.linkDragData.linkSourceGetAnchor();
const destDoc = this.doc;
@@ -395,7 +434,7 @@ export class TreeView extends React.Component<TreeViewProps> {
};
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;
+ const canAdd = (!this.props.treeView.outlineMode && !StrCast((inside ? this.props.document : this.props.treeViewParent)?.treeView_FreezeChildren).includes('add')) || forceAdd;
if (canAdd) {
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);
@@ -438,7 +477,7 @@ export class TreeView extends React.Component<TreeViewProps> {
doc && Object.keys(doc).forEach(key => !(key in ids) && doc[key] !== ComputedField.undefined && (ids[key] = key));
for (const key of Object.keys(ids).slice().sort()) {
- if (this.props.skipFields?.includes(key) || key === 'title' || key === 'treeViewOpen') continue;
+ if (this.props.skipFields?.includes(key) || key === 'title' || key === 'treeView_Open') continue;
const contents = doc[key];
let contentElement: (JSX.Element | null)[] | JSX.Element = [];
@@ -538,7 +577,7 @@ export class TreeView extends React.Component<TreeViewProps> {
const expandKey = this.treeViewExpandedView;
const sortings = (this.props.styleProvider?.(this.doc, this.props.treeView.props, StyleProp.TreeViewSortings) as { [key: string]: { color: string; icon: JSX.Element | string } }) ?? {};
if (['links', 'annotations', 'embeddings', this.fieldKey].includes(expandKey)) {
- const sorting = StrCast(this.doc.treeViewSortCriterion, TreeSort.None);
+ const sorting = StrCast(this.doc.treeView_SortCriterion, TreeSort.WhenAdded);
const sortKeys = Object.keys(sortings);
const curSortIndex = Math.max(
0,
@@ -550,7 +589,7 @@ export class TreeView extends React.Component<TreeViewProps> {
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
- const ordering = StrCast(this.doc.treeViewSortCriterion);
+ const ordering = StrCast(this.doc.treeView_SortCriterion);
if (ordering === TreeSort.Zindex) {
const docs = TreeView.sortDocs(this.childDocs || ([] as Doc[]), ordering);
doc.zIndex = addBefore ? NumCast(addBefore.zIndex) + (before ? -0.5 : 0.5) : 1000;
@@ -578,10 +617,11 @@ export class TreeView extends React.Component<TreeViewProps> {
return (
<div>
{!docs?.length || this.props.AddToMap /* hack to identify pres box trees */ ? null : (
- <div className={'treeView-sorting'}>
+ <div className="treeView-sorting">
<IconButton
color={sortings[sorting]?.color}
size={Size.XSMALL}
+ tooltip={`Sorted by : ${this.doc.treeView_SortCriterion}. click to cycle`}
icon={sortings[sorting]?.icon}
onPointerDown={e => {
downX = e.clientX;
@@ -590,7 +630,7 @@ export class TreeView extends React.Component<TreeViewProps> {
}}
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]);
+ !this.props.treeView.outlineMode && (this.doc.treeView_SortCriterion = sortKeys[(curSortIndex + 1) % sortKeys.length]);
e.stopPropagation();
}
}, 'sort order')}
@@ -600,8 +640,8 @@ export class TreeView extends React.Component<TreeViewProps> {
<ul
style={{ cursor: 'inherit' }}
key={expandKey + 'more'}
- title="click to change sort order"
- className={''} //this.doc.treeViewHideTitle ? 'no-indent' : ''}
+ title={`Sorted by : ${this.doc.treeView_SortCriterion}. click to cycle`}
+ className="" //this.doc.treeView_HideTitle ? 'no-indent' : ''}
onPointerDown={e => {
downX = e.clientX;
downY = e.clientY;
@@ -609,7 +649,7 @@ export class TreeView extends React.Component<TreeViewProps> {
}}
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]);
+ !this.props.treeView.outlineMode && (this.doc.treeView_SortCriterion = sortKeys[(curSortIndex + 1) % sortKeys.length]);
e.stopPropagation();
}
}, 'sort order')}>
@@ -683,7 +723,7 @@ export class TreeView extends React.Component<TreeViewProps> {
{
this: this.doc.isTemplateForField && this.props.dataDoc ? this.props.dataDoc : this.doc,
heading: this.props.treeViewParent.title,
- checked: this.doc.treeViewChecked === 'check' ? 'x' : this.doc.treeViewChecked === 'x' ? 'remove' : 'check',
+ checked: this.doc.treeView_Checked === 'check' ? 'x' : this.doc.treeView_Checked === 'x' ? 'remove' : 'check',
containingTreeView: this.props.treeView.props.Document,
},
console.log
@@ -697,8 +737,8 @@ export class TreeView extends React.Component<TreeViewProps> {
@computed get renderBullet() {
TraceMobx();
const iconType = this.props.treeView.props.styleProvider?.(this.doc, this.props.treeView.props, StyleProp.TreeViewIcon + (this.treeViewOpen ? ':open' : !this.childDocs.length ? ':empty' : '')) || 'question';
- const color = StrCast(Doc.UserDoc().userColor);
- const checked = this.onCheckedClick ? this.doc.treeViewChecked ?? 'unchecked' : undefined;
+ const color = SettingsManager.userColor;
+ const checked = this.onCheckedClick ? this.doc.treeView_Checked ?? 'unchecked' : undefined;
return (
<div
className={`bullet${this.props.treeView.outlineMode ? '-outline' : ''}`}
@@ -722,15 +762,14 @@ export class TreeView extends React.Component<TreeViewProps> {
)
) : (
<div className="treeView-bulletIcons" style={{ color: Doc.IsSystem(DocCast(this.doc.proto)) ? 'red' : undefined }}>
- {this.onCheckedClick ? (
- <IconButton
- color={color}
- icon={<FontAwesomeIcon size="sm" icon={checked === 'check' ? 'check' : checked === 'x' ? 'times' : checked === 'unchecked' ? 'square' : !this.treeViewOpen ? 'caret-right' : 'caret-down'} />}
- size={Size.XSMALL}
+ <div className={`treeView-${this.onCheckedClick ? 'checkIcon' : 'expandIcon'}`}>
+ <FontAwesomeIcon
+ size="sm"
+ style={{ display: this.childDocs?.length >= 1 ? 'block' : 'none' }}
+ icon={checked === 'check' ? 'check' : checked === 'x' ? 'times' : checked === 'unchecked' ? 'square' : !this.treeViewOpen ? 'caret-right' : 'caret-down'}
/>
- ) : (
- <IconButton color={color} icon={<FontAwesomeIcon icon={iconType as IconProp} />} size={Size.XSMALL} />
- )}
+ </div>
+ {this.onCheckedClick ? null : typeof iconType === 'string' ? <FontAwesomeIcon icon={iconType as IconProp} /> : iconType}
</div>
)}
</div>
@@ -748,9 +787,9 @@ export class TreeView extends React.Component<TreeViewProps> {
}
@action
expandNextviewType = () => {
- if (this.treeViewOpen && !this.doc.isFolder && !this.props.treeView.outlineMode && !this.doc.treeViewExpandedViewLock) {
+ if (this.treeViewOpen && !this.doc.isFolder && !this.props.treeView.outlineMode && !this.doc.treeView_ExpandedViewLock) {
const next = (modes: any[]) => modes[(modes.indexOf(StrCast(this.treeViewExpandedView)) + 1) % modes.length];
- this.doc.treeViewExpandedView = next(this.validExpandViewTypes);
+ this.doc.treeView_ExpandedView = next(this.validExpandViewTypes);
}
this.treeViewOpen = true;
};
@@ -758,8 +797,8 @@ export class TreeView extends React.Component<TreeViewProps> {
@observable headerEleWidth = 0;
@computed get titleButtons() {
const customHeaderButtons = this.props.styleProvider?.(this.doc, this.props.treeView.props, StyleProp.Decorations);
- const color = StrCast(Doc.UserDoc().userColor);
- return this.props.treeViewHideHeaderFields() || this.doc.treeViewHideHeaderFields ? null : (
+ const color = SettingsManager.userColor;
+ return this.props.treeViewHideHeaderFields() || this.doc.treeView_HideHeaderFields ? null : (
<>
{customHeaderButtons} {/* e.g.,. hide button is set by dashboardStyleProvider */}
<IconButton
@@ -771,7 +810,7 @@ export class TreeView extends React.Component<TreeViewProps> {
e.stopPropagation();
}}
/>
- {Doc.noviceMode ? null : this.doc.treeViewExpandedViewLock || Doc.IsSystem(this.doc) ? null : (
+ {Doc.noviceMode ? null : this.doc.treeView_ExpandedViewLock || Doc.IsSystem(this.doc) ? null : (
<span className="collectionTreeView-keyHeader" title="type of expanded data" key={this.treeViewExpandedView} onPointerDown={this.expandNextviewType}>
{this.treeViewExpandedView}
</span>
@@ -815,7 +854,7 @@ export class TreeView extends React.Component<TreeViewProps> {
onChildClick = () => this.props.onChildClick?.() ?? (this._editTitleScript?.() || ScriptField.MakeFunction(`DocFocusOrOpen(self)`)!);
- onChildDoubleClick = () => ScriptCast(this.props.treeView.Document.treeViewChildDoubleClick, !this.props.treeView.outlineMode ? this._openScript?.() : null);
+ onChildDoubleClick = () => ScriptCast(this.props.treeView.Document.treeView_ChildDoubleClick, !this.props.treeView.outlineMode ? this._openScript?.() : null);
refocus = () => this.props.treeView.props.focus(this.props.treeView.props.Document, {});
ignoreEvent = (e: any) => {
@@ -861,7 +900,7 @@ export class TreeView extends React.Component<TreeViewProps> {
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) => {
- if (this.doc.treeViewHideHeader || (this.doc.treeViewHideHeaderIfTemplate && this.props.treeView.props.childLayoutTemplate?.()) || this.props.treeView.outlineMode) {
+ if (this.doc.treeView_HideHeader || (this.doc.treeView_HideHeaderIfTemplate && this.props.treeView.props.childLayoutTemplate?.()) || this.props.treeView.outlineMode) {
switch (e.key) {
case 'Tab':
e.stopPropagation?.();
@@ -903,6 +942,7 @@ export class TreeView extends React.Component<TreeViewProps> {
height={12}
sizeToContent={true}
fontSize={12}
+ isEditingCallback={action(e => (this._editTitle = e))}
GetValue={() => StrCast(this.doc.title)}
OnTab={undoBatch((shift?: boolean) => {
if (!shift) this.props.indentDocument?.(true);
@@ -927,8 +967,8 @@ export class TreeView extends React.Component<TreeViewProps> {
}
})}
Document={this.doc}
+ DataDoc={undefined} // or this.dataDoc?
layout_fitWidth={returnTrue}
- DataDoc={undefined}
scriptContext={this}
hideDecorationTitle={this.props.treeView.outlineMode}
hideResizeHandles={this.props.treeView.outlineMode}
@@ -975,8 +1015,8 @@ export class TreeView extends React.Component<TreeViewProps> {
ref={this._tref}
title="click to edit title. Double Click or Drag to Open"
style={{
- backgroundColor: Doc.IsSystem(this.props.document) || this.props.document.isFolder ? StrCast(Doc.UserDoc().userVariantColor) : undefined,
- color: Doc.IsSystem(this.props.document) || this.props.document.isFolder ? lightOrDark(StrCast(Doc.UserDoc().userVariantColor)) : undefined,
+ backgroundColor: Doc.IsSystem(this.props.document) || this.props.document.isFolder ? SettingsManager.userVariantColor : undefined,
+ color: Doc.IsSystem(this.props.document) || this.props.document.isFolder ? lightOrDark(SettingsManager.userVariantColor) : undefined,
fontWeight: Doc.IsSearchMatch(this.doc) !== undefined ? 'bold' : undefined,
textDecoration: Doc.GetT(this.doc, 'title', 'string', true) ? 'underline' : undefined,
outline: this.doc === Doc.ActiveDashboard ? 'dashed 1px #06123232' : undefined,
@@ -1005,7 +1045,7 @@ export class TreeView extends React.Component<TreeViewProps> {
<div
className="treeView-background"
style={{
- background: StrCast(Doc.UserDoc().userColor),
+ background: SettingsManager.userColor,
}}
/>
{contents}
@@ -1067,7 +1107,7 @@ export class TreeView extends React.Component<TreeViewProps> {
// renders the text version of a document as the header. This is used in the file system mode and in other vanilla tree views.
@computed get renderTitleAsHeader() {
- return this.props.treeView.Document.treeViewHideUnrendered && this.doc.layout_unrendered && !this.doc.treeViewFieldKey ? (
+ return this.props.treeView.Document.treeView_HideUnrendered && this.doc.layout_unrendered && !this.doc.treeView_FieldKey ? (
<div></div>
) : (
<>
@@ -1088,7 +1128,7 @@ export class TreeView extends React.Component<TreeViewProps> {
};
@computed get renderBorder() {
- const sorting = StrCast(this.doc.treeViewSortCriterion, TreeSort.None);
+ const sorting = StrCast(this.doc.treeView_SortCriterion, TreeSort.WhenAdded);
const sortings = (this.props.styleProvider?.(this.doc, this.props.treeView.props, StyleProp.TreeViewSortings) ?? {}) as { [key: string]: { color: string; label: string } };
return (
<div className={`treeView-border${this.props.treeView.outlineMode ? TreeViewType.outline : ''}`} style={{ borderColor: sortings[sorting]?.color }}>
@@ -1101,14 +1141,14 @@ export class TreeView extends React.Component<TreeViewProps> {
const pt = [de.clientX, de.clientY];
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);
+ const inside = this.props.treeView.fileSysMode && !this.doc.isFolder ? false : pt[0] > rect.left + rect.width * 0.33 || (!before && this.treeViewOpen && this.childDocs?.length ? true : false);
const docs = this.props.treeView.onTreeDrop(de, (docs: Doc[]) => this.dropDocuments(docs, before, inside, 'copy', undefined, undefined, false));
};
render() {
TraceMobx();
- const hideTitle = this.doc.treeViewHideHeader || (this.doc.treeViewHideHeaderIfTemplate && this.props.treeView.props.childLayoutTemplate?.()) || this.props.treeView.outlineMode;
+ const hideTitle = this.doc.treeView_HideHeader || (this.doc.treeView_HideHeaderIfTemplate && this.props.treeView.props.childLayoutTemplate?.()) || this.props.treeView.outlineMode;
return this.props.renderedIds?.indexOf(this.doc[Id]) !== -1 ? (
'<' + this.doc.title + '>' // just print the title of documents we've previously rendered in this hierarchical path to avoid cycles
) : (
@@ -1120,9 +1160,9 @@ export class TreeView extends React.Component<TreeViewProps> {
// onKeyDown={this.onKeyDown}
>
<li className="collection-child">
- {hideTitle && this.doc.type !== DocumentType.RTF && !this.doc.treeViewRenderAsBulletHeader // should test for prop 'treeViewRenderDocWithBulletAsHeader"
+ {hideTitle && this.doc.type !== DocumentType.RTF && !this.doc.treeView_RenderAsBulletHeader // should test for prop 'treeView_RenderDocWithBulletAsHeader"
? this.renderEmbeddedDocument(false, returnFalse)
- : this.renderBulletHeader(hideTitle ? this.renderDocumentAsHeader(!this.doc.treeViewRenderAsBulletHeader) : this.renderTitleAsHeader, this._editTitle)}
+ : this.renderBulletHeader(hideTitle ? this.renderDocumentAsHeader(!this.doc.treeView_RenderAsBulletHeader) : this.renderTitleAsHeader, this._editTitle)}
</li>
</div>
);
@@ -1130,7 +1170,7 @@ export class TreeView extends React.Component<TreeViewProps> {
public static sortDocs(childDocs: Doc[], criterion: string | undefined) {
const docs = childDocs.slice();
- if (criterion !== TreeSort.None) {
+ if (criterion !== TreeSort.WhenAdded) {
const sortAlphaNum = (a: string, b: string): 0 | 1 | -1 => {
const reN = /[0-9]*$/;
const aA = a.replace(reN, '') ? a.replace(reN, '') : +a; // get rid of trailing numbers
@@ -1145,8 +1185,8 @@ export class TreeView extends React.Component<TreeViewProps> {
}
};
docs.sort(function (d1, d2): 0 | 1 | -1 {
- const a = criterion === TreeSort.Up ? d2 : d1;
- const b = criterion === TreeSort.Up ? d1 : d2;
+ const a = criterion === TreeSort.AlphaUp ? d2 : d1;
+ const b = criterion === TreeSort.AlphaUp ? d1 : d2;
const first = a[criterion === TreeSort.Zindex ? 'zIndex' : 'title'];
const second = b[criterion === TreeSort.Zindex ? 'zIndex' : 'title'];
if (typeof first === 'number' && typeof second === 'number') return first - second > 0 ? 1 : -1;
@@ -1161,7 +1201,7 @@ export class TreeView extends React.Component<TreeViewProps> {
childDocs: Doc[],
treeView: CollectionTreeView,
parentTreeView: CollectionTreeView | TreeView | undefined,
- treeViewParent: Doc,
+ treeView_Parent: Doc,
dataDoc: Doc | undefined,
parentCollectionDoc: Doc | undefined,
containerPrevSibling: Doc | undefined,
@@ -1175,7 +1215,7 @@ export class TreeView extends React.Component<TreeViewProps> {
isContentActive: (outsideReaction?: boolean) => boolean,
panelWidth: () => number,
renderDepth: number,
- treeViewHideHeaderFields: () => boolean,
+ treeView_HideHeaderFields: () => boolean,
renderedIds: string[],
onCheckedClick: undefined | (() => ScriptField),
onChildClick: undefined | (() => ScriptField),
@@ -1192,19 +1232,19 @@ export class TreeView extends React.Component<TreeViewProps> {
hierarchyIndex?: number[],
renderCount?: number
) {
- const viewSpecScript = Cast(treeViewParent.viewSpecScript, ScriptField);
+ const viewSpecScript = Cast(treeView_Parent.viewSpecScript, ScriptField);
if (viewSpecScript) {
childDocs = childDocs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result);
}
- const docs = TreeView.sortDocs(childDocs, StrCast(treeViewParent.treeViewSortCriterion, TreeSort.None));
+ const docs = TreeView.sortDocs(childDocs, StrCast(treeView_Parent.treeView_SortCriterion, TreeSort.WhenAdded));
const rowWidth = () => panelWidth() - treeBulletWidth() * (treeView.props.NativeDimScaling?.() || 1);
- const treeViewRefs = new Map<Doc, TreeView | undefined>();
+ const treeView_Refs = new Map<Doc, TreeView | undefined>();
return docs
.filter(child => child instanceof Doc)
.map((child, i) => {
if (renderCount && i > renderCount) return null;
- const pair = Doc.GetLayoutDataDocPair(treeViewParent, dataDoc, child);
+ const pair = Doc.GetLayoutDataDocPair(treeView_Parent, dataDoc, child);
if (!pair.layout || pair.data instanceof Promise) {
return null;
}
@@ -1217,11 +1257,11 @@ export class TreeView extends React.Component<TreeViewProps> {
FormattedTextBox.SelectOnLoad = child[Id];
TreeView._editTitleOnLoad = editTitle ? { id: child[Id], parent } : undefined;
Doc.AddDocToList(newParent, fieldKey, child, addAfter, false);
- newParent.treeViewOpen = true;
+ newParent.treeView_Open = true;
Doc.SetContainer(child, treeView.Document);
}
};
- const indent = i === 0 ? undefined : (editTitle: boolean) => dentDoc(editTitle, docs[i - 1], undefined, treeViewRefs.get(docs[i - 1]));
+ const indent = i === 0 ? undefined : (editTitle: boolean) => dentDoc(editTitle, docs[i - 1], undefined, treeView_Refs.get(docs[i - 1]));
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);
@@ -1232,10 +1272,10 @@ export class TreeView extends React.Component<TreeViewProps> {
return (
<TreeView
key={child[Id]}
- ref={r => treeViewRefs.set(child, r ? r : undefined)}
+ ref={r => treeView_Refs.set(child, r ? r : undefined)}
document={pair.layout}
dataDoc={pair.data}
- treeViewParent={treeViewParent}
+ treeViewParent={treeView_Parent}
prevSibling={docs[i]}
// TODO: [AL] add these
hierarchyIndex={hierarchyIndex ? [...hierarchyIndex, i + 1] : undefined}
@@ -1247,7 +1287,7 @@ export class TreeView extends React.Component<TreeViewProps> {
onCheckedClick={onCheckedClick}
onChildClick={onChildClick}
renderDepth={renderDepth}
- removeDoc={StrCast(treeViewParent.treeViewFreezeChildren).includes('remove') ? undefined : remove}
+ removeDoc={StrCast(treeView_Parent.treeView_FreezeChildren).includes('remove') ? undefined : remove}
addDocument={addDocument}
styleProvider={styleProvider}
panelWidth={rowWidth}
@@ -1258,7 +1298,7 @@ export class TreeView extends React.Component<TreeViewProps> {
addDocTab={addDocTab}
ScreenToLocalTransform={screenToLocalXf}
isContentActive={isContentActive}
- treeViewHideHeaderFields={treeViewHideHeaderFields}
+ treeViewHideHeaderFields={treeView_HideHeaderFields}
renderedIds={renderedIds}
skipFields={skipFields}
firstLevel={firstLevel}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index fb8ec93b2..24a758d8c 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -21,6 +21,8 @@ export interface CollectionFreeFormLinkViewProps {
LinkDocs: Doc[];
}
+// props.screentolocatransform
+
@observer
export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFormLinkViewProps> {
@observable _opacity: number = 0;
@@ -59,7 +61,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.link_displayLine) && (this._opacity = 0.05)),
+ action(() => (!LinkDocs.length || !(linkDoc.link_displayLine || Doc.UserDoc().showLinkLines)) && (this._opacity = 0.05)),
750
); // this will unhighlight the link line.
const a = A.ContentDiv.getBoundingClientRect();
@@ -226,6 +228,7 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
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);
+ const link = this.props.LinkDocs[0];
return {
a,
b,
@@ -235,11 +238,12 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
bActive,
textX,
textY,
- // pt1,
- // pt2,
+ // fully connected
+ // pt1,
+ // pt2,
// this code adds space between links
- pt1: [pt1[0] + pt1normalized[0] * 13, pt1[1] + pt1normalized[1] * 13],
- pt2: [pt2[0] + pt2normalized[0] * 13, pt2[1] + pt2normalized[1] * 13],
+ pt1: link.link_displayArrow ? [pt1[0] + pt1normalized[0] * 3*NumCast(link.link_displayArrow_scale, 4), pt1[1] + pt1normalized[1] * 3*NumCast(link.link_displayArrow_scale, 3)] : pt1,
+ pt2: link.link_displayArrow ? [pt2[0] + pt2normalized[0] * 3*NumCast(link.link_displayArrow_scale, 4), pt2[1] + pt2normalized[1] * 3*NumCast(link.link_displayArrow_scale, 3)] : pt2,
};
}
@@ -253,9 +257,9 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
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 linkDescription = Field.toString(link.link_description as any as Field).split('\n')[0];
- const linkSize = currRelationshipIndex === -1 || currRelationshipIndex >= linkRelationshipSizes.length ? -1 : linkRelationshipSizes[currRelationshipIndex];
+ const linkSize = Doc.noviceMode || 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 ? StrCast(link._backgroundColor, 'black') : linkColorList[currRelationshipIndex];
@@ -265,15 +269,12 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
//thickness varies linearly from 3px to 12px for increasing link count
const strokeWidth = linkSize === -1 ? '3px' : Math.floor(2 + 10 * (linkSize / Math.max(...linkRelationshipSizes))) + 'px';
- if (link.link_displayArrow === undefined) {
- link.link_displayArrow = false;
- }
-
- return link.opacity === 0 || !a.width || !b.width || (!link.link_displayLine && !aActive && !bActive) ? null : (
+ const arrowScale = NumCast(link.link_displayArrow_scale, 3)
+ return link.opacity === 0 || !a.width || !b.width || (!(Doc.UserDoc().showLinkLines || link.link_displayLine) && !aActive && !bActive) ? null : (
<>
<defs>
- <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 id={`${link[Id] + 'arrowhead'}`} markerWidth={`${4*arrowScale}`} markerHeight={`${3*arrowScale}`} refX="0" refY={`${1.5*arrowScale}`} orient="auto">
+ <polygon points={`0 0, ${3*arrowScale} ${1.5*arrowScale}, 0 ${3*arrowScale}`} fill={stroke} />
</marker>
<filter id="outline">
<feMorphology in="SourceAlpha" result="expanded" operator="dilate" radius="1" />
@@ -303,7 +304,7 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
{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">{linkDescription.substring(0, 50) + (linkDescription.length > 50 ? '...' : '')}</tspan>
<tspan dy="2">&nbsp;</tspan>
</text>
)}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
index c6419885b..e4ae251c8 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
@@ -153,7 +153,12 @@
overflow-y: auto;
overflow-x: hidden;
}
-
+.collectionFreeFormView-brushView {
+ pointer-events: none;
+ position: absolute;
+ transition: opacity 0.5s;
+ z-index: 1000;
+}
.collectionfreeformview-container {
// touch action none means that the browser will handle none of the touch actions. this allows us to implement our own actions.
touch-action: none;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index ba31916a7..8a812c671 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -16,7 +16,7 @@ import { BoolCast, Cast, DocCast, FieldValue, NumCast, ScriptCast, StrCast } fro
import { ImageField } from '../../../../fields/URLField';
import { TraceMobx } from '../../../../fields/util';
import { GestureUtils } from '../../../../pen-gestures/GestureUtils';
-import { aggregateBounds, emptyFunction, intersectRect, returnFalse, returnNone, returnTrue, returnZero, setupMoveUpEvents, Utils } from '../../../../Utils';
+import { aggregateBounds, emptyFunction, intersectRect, lightOrDark, 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';
@@ -52,6 +52,7 @@ import { CollectionFreeFormRemoteCursors } from './CollectionFreeFormRemoteCurso
import './CollectionFreeFormView.scss';
import { MarqueeView } from './MarqueeView';
import React = require('react');
+import { FollowLinkScript } from '../../../util/LinkFollower';
export type collectionFreeformViewProps = {
NativeWidth?: () => number;
@@ -97,7 +98,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
private _brushtimer: any;
private _brushtimer1: any;
- private get isAnnotationOverlay() {
+ public get isAnnotationOverlay() {
return this.props.isAnnotationOverlay;
}
public get scaleFieldKey() {
@@ -155,7 +156,7 @@ 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.width && !e.bounds.z).map(e => e.bounds!),
+ this._layoutElements.filter(e => e.bounds?.width && !e.bounds.z).map(e => e.bounds!),
NumCast(this.layoutDoc._xPadding, 10),
NumCast(this.layoutDoc._yPadding, 10)
);
@@ -237,7 +238,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
onChildClickHandler = () => this.props.childClickScript || ScriptCast(this.Document.onChildClick);
onChildDoubleClickHandler = () => this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick);
elementFunc = () => this._layoutElements;
- shrinkWrap = () => {
+ fitContentOnce = () => {
if (this.props.DocumentView?.().nativeWidth) return;
const vals = this.fitToContentVals;
this.layoutDoc._freeform_panX = vals.bounds.cx;
@@ -311,6 +312,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
focus = (anchor: Doc, options: DocFocusOptions) => {
if (this._lightboxDoc) return;
+ if (anchor.type !== DocumentType.CONFIG && !DocListCast(this.Document[this.fieldKey ?? Doc.LayoutFieldKey(this.Document)]).includes(anchor)) 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);
@@ -319,7 +321,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
// 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...
+ // glr: freeform transform speed can be set by adjusting presentation_transition 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);
@@ -371,7 +373,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
const nd = [Doc.NativeWidth(layoutDoc), Doc.NativeHeight(layoutDoc)];
layoutDoc._width = NumCast(layoutDoc._width, 300);
layoutDoc._height = NumCast(layoutDoc._height, nd[0] && nd[1] ? (nd[1] / nd[0]) * NumCast(layoutDoc._width) : 300);
- (d._raiseWhenDragged === undefined ? DragManager.GetRaiseWhenDragged() : d._raiseWhenDragged) && (d.zIndex = zsorted.length + 1 + i); // bringToFront
+ !d._keepZWhenDragged && (d.zIndex = zsorted.length + 1 + i); // bringToFront
}
if (this.layoutDoc._autoArrange || de.metaKey) {
@@ -420,11 +422,27 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
if (linkDragData.linkDragView.props.docViewPath().includes(this.props.docViewPath().lastElement())) {
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' });
- 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
- }
+ const source =
+ !linkDragData.dragDocument.embedContainer || linkDragData.dragDocument.embedContainer !== this.rootDoc
+ ? Docs.Create.TextDocument('', { _width: 200, _height: 75, x: xp, y: yp, title: 'dropped annotation' })
+ : Docs.Create.FontIconDocument({
+ title: 'anchor',
+ icon_label: '',
+ followLinkToggle: true,
+ icon: 'map-pin',
+ x: xp,
+ y: yp,
+ backgroundColor: '#ACCEF7',
+ layout_hideAllLinks: true,
+ layout_hideLinkButton: true,
+ _width: 15,
+ _height: 15,
+ _xPadding: 0,
+ onClick: FollowLinkScript(),
+ });
+ 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();
!added && e.preventDefault();
return added;
@@ -681,6 +699,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
case GestureUtils.Gestures.Stroke:
const points = ge.points;
const B = this.getTransform().transformBounds(ge.bounds.left, ge.bounds.top, ge.bounds.width, ge.bounds.height);
+ console.log(ge.bounds.left, ge.bounds.top, ge.bounds.width, ge.bounds.height);
const inkDoc = Docs.Create.InkDocument(
ActiveInkColor(),
ActiveInkWidth() * this.props.ScreenToLocalTransform().Scale,
@@ -1022,7 +1041,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
@action
zoom = (pointX: number, pointY: number, deltaY: number): void => {
- if (this.Document._isGroup || this.Document._freeform_noZoom) return;
+ if (this.Document._isGroup || this.Document[(this.props.viewField ?? '_') + '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);
@@ -1030,12 +1049,15 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
if (deltaScale * invTransform.Scale > 20) {
deltaScale = 20 / invTransform.Scale;
}
- if (deltaScale < 1 && invTransform.Scale <= NumCast(this.rootDoc._freeform_scale_min, 1) && this.isAnnotationOverlay) {
+ if (deltaScale < 1 && invTransform.Scale <= NumCast(this.rootDoc[this.scaleFieldKey + '_min'])) {
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;
+ if (deltaScale * invTransform.Scale > NumCast(this.rootDoc[this.scaleFieldKey + '_max'], Number.MAX_VALUE)) {
+ deltaScale = NumCast(this.rootDoc[this.scaleFieldKey + '_max'], 1) / invTransform.Scale;
+ }
+ if (deltaScale * invTransform.Scale < NumCast(this.rootDoc[this.scaleFieldKey + '_min'], this.isAnnotationOverlay ? 1 : 0)) {
+ deltaScale = NumCast(this.rootDoc[this.scaleFieldKey + '_min'], 1) / invTransform.Scale;
}
const localTransform = invTransform.scaleAbout(deltaScale, x, y);
@@ -1050,7 +1072,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
onPointerWheel = (e: React.WheelEvent): void => {
if (this.Document._isGroup || !this.isContentActive()) return; // group style collections neither pan nor zoom
PresBox.Instance?.pauseAutoPres();
- if (this.layoutDoc._Transform || this.props.Document.treeViewOutlineMode === TreeViewType.outline) return;
+ if (this.layoutDoc._Transform || this.props.Document.treeView_OutlineMode === TreeViewType.outline) return;
e.stopPropagation();
const docHeight = NumCast(this.rootDoc[Doc.LayoutFieldKey(this.rootDoc) + '_nativeHeight'], this.nativeHeight);
const scrollable = NumCast(this.layoutDoc[this.scaleFieldKey], 1) === 1 && docHeight > this.props.PanelHeight() / this.nativeDimScaling + 1e-4;
@@ -1264,13 +1286,15 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
return this.addDocument?.(newDoc);
}
};
- pointerEvents = () => {
+ @computed get _pointerEvents() {
const engine = this.props.layoutEngine?.() || StrCast(this.props.Document._layoutEngine);
- const pointerEvents = DocumentDecorations.Instance.Interacting
+ const pointerEvents = DocumentView.Interacting
? 'none'
: this.props.childPointerEvents ?? (this.props.viewDefDivClick || (engine === computePassLayout.name && !this.props.isSelected(true)) || this.isContentActive() === false ? 'none' : this.props.pointerEvents?.());
return pointerEvents;
- };
+ }
+ pointerEvents = () => this._pointerEvents;
+ childContentsActive = () => (this.props.childContentsActive ?? this.isContentActive() === false ? returnFalse : emptyFunction)();
getChildDocView(entry: PoolData) {
const childLayout = entry.pair.layout;
const childData = entry.pair.data;
@@ -1299,7 +1323,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
childFiltersByRanges={this.childDocRangeFilters}
searchFilterDocs={this.searchFilterDocs}
isDocumentActive={this.props.childDocumentsActive?.() || this.rootDoc._isGroup ? this.props.isDocumentActive : this.isContentActive}
- isContentActive={this.props.childContentsActive ?? this.isContentActive() === false ? returnFalse : emptyFunction}
+ isContentActive={this.childContentsActive}
focus={this.Document._isGroup ? this.groupFocus : this.isAnnotationOverlay ? this.props.focus : this.focus}
addDocTab={this.addDocTab}
addDocument={this.props.addDocument}
@@ -1316,8 +1340,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
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.treeViewFreezeChildDimensions)} // bcz: check this
+ //fitContentsToBox={this.props.fitContentsToBox || BoolCast(this.props.treeView_FreezeChildDimensions)} // bcz: check this
/>
);
}
@@ -1356,6 +1379,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
@observable _lightboxDoc: Opt<Doc>;
getCalculatedPositions(params: { pair: { layout: Doc; data?: Doc }; index: number; collection: Doc }): PoolData {
+ const random = (min: number, max: number, x: number, y: number) => /* min should not be equal to max */ min + (((Math.abs(x * y) * 9301 + 49297) % 233280) / 233280) * (max - min);
const childDoc = params.pair.layout;
const childDocLayout = Doc.Layout(childDoc);
const layoutFrameNumber = Cast(this.Document._currentFrame, 'number'); // frame number that container is at which determines layout frame values
@@ -1366,11 +1390,15 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
layoutFrameNumber === undefined
? { _width: Cast(childDocLayout._width, 'number'), _height: Cast(childDocLayout._height, 'number'), _rotation: Cast(childDocLayout._rotation, 'number'), x: childDoc.x, y: childDoc.y, opacity: this.props.childOpacity?.() }
: CollectionFreeFormDocumentView.getValues(childDoc, layoutFrameNumber);
+ // prettier-ignore
+ const rotation = Cast(_rotation,'number',
+ !this.layoutDoc._rotation_jitter ? null
+ : NumCast(this.layoutDoc._rotation_jitter) * random(-1, 1, NumCast(x), NumCast(y)) );
return {
x: Number.isNaN(NumCast(x)) ? 0 : NumCast(x),
y: Number.isNaN(NumCast(y)) ? 0 : NumCast(y),
z: Cast(z, 'number'),
- rotation: Cast(_rotation, 'number'),
+ rotation: rotation,
color: Cast(color, 'string') ? StrCast(color) : this.props.styleProvider?.(childDoc, this.props, StyleProp.Color),
backgroundColor: Cast(backgroundColor, 'string') ? StrCast(backgroundColor) : this.getClusterColor(childDoc, this.props, StyleProp.BackgroundColor),
opacity: !_width ? 0 : this._keyframeEditing ? 1 : Cast(opacity, 'number') ?? this.props.styleProvider?.(childDoc, this.props, StyleProp.Opacity),
@@ -1435,21 +1463,17 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
}.bind(this)
);
- childPositionProviderUnmemoized = (doc: Doc, replica: string) => {
- return this._layoutPoolData.get(doc[Id] + (replica || ''));
- };
+ childPositionProviderUnmemoized = (doc: Doc, replica: string) => this._layoutPoolData.get(doc[Id] + (replica || ''));
childDataProvider = computedFn(
function childDataProvider(this: any, doc: Doc, replica: string) {
- return this._layoutPoolData.get(doc[Id] + (replica || ''));
+ return this.childPositionProviderUnmemoized(doc, replica);
}.bind(this)
);
- childSizeProviderUnmemoized = (doc: Doc, replica: string) => {
- return this._layoutSizeData.get(doc[Id] + (replica || ''));
- };
+ childSizeProviderUnmemoized = (doc: Doc, replica: string) => this._layoutSizeData.get(doc[Id] + (replica || ''));
childSizeProvider = computedFn(
function childSizeProvider(this: any, doc: Doc, replica: string) {
- return this._layoutSizeData.get(doc[Id] + (replica || ''));
+ return this.childSizeProviderUnmemoized(doc, replica);
}.bind(this)
);
@@ -1523,19 +1547,13 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
});
});
- 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.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._freeform_useClusters && !this._clusterSets.length && this.childDocs.length && this.updateClusters(true);
return elements;
}
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 });
+ const anchor = Docs.Create.ConfigDocument({ title: 'ViewSpec - ' + StrCast(this.layoutDoc._type_collection), layout_unrendered: true, presentation_transition: 500, annotationOn: this.rootDoc });
PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), pannable: !this.Document._isGroup, type_collection: true, filters: true } }, this.rootDoc);
if (addAsAnnotation) {
@@ -1592,6 +1610,15 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
elements => (this._layoutElements = elements || []),
{ fireImmediately: true, name: 'doLayout' }
);
+
+ this._disposers.fitContent = reaction(
+ () => this.rootDoc.fitContentOnce,
+ fitContentOnce => {
+ if (fitContentOnce) this.fitContentOnce();
+ this.rootDoc.fitContentOnce = undefined;
+ },
+ { fireImmediately: true, name: 'fitContent' }
+ );
})
);
}
@@ -1782,6 +1809,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
!Doc.noviceMode &&
optionItems.push({ description: (this._showAnimTimeline ? 'Close' : 'Open') + ' Animation Timeline', event: action(() => (this._showAnimTimeline = !this._showAnimTimeline)), icon: 'eye' });
this.props.renderDepth && optionItems.push({ description: 'Use Background Color as Default', event: () => (Cast(Doc.UserDoc().emptyCollection, Doc, null)._backgroundColor = StrCast(this.layoutDoc._backgroundColor)), icon: 'palette' });
+ this.props.renderDepth &&
+ optionItems.push({
+ description: 'Fit Content Once',
+ event: () => {
+ this.fitContentOnce();
+ },
+ icon: 'object-group',
+ });
if (!Doc.noviceMode) {
optionItems.push({ description: (!Doc.NativeWidth(this.layoutDoc) || !Doc.NativeHeight(this.layoutDoc) ? 'Freeze' : 'Unfreeze') + ' Aspect', event: this.toggleNativeDimensions, icon: 'snowflake' });
}
@@ -1864,6 +1899,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
showPresPaths = () => (CollectionFreeFormView.ShowPresPaths ? PresBox.Instance.getPaths(this.rootDoc) : null);
+ brushedView = () => this._brushedView;
+ gridColor = () => {
+ const backColor = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor + ':box');
+ return lightOrDark(backColor);
+ };
@computed get marqueeView() {
TraceMobx();
return (
@@ -1896,6 +1936,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
PanelHeight={this.props.PanelHeight}
panX={this.panX}
panY={this.panY}
+ color={this.gridColor}
nativeDimScaling={this.nativeDim}
zoomScaling={this.zoomScaling}
layoutDoc={this.layoutDoc}
@@ -1906,13 +1947,13 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
</div>
) : null}
<CollectionFreeFormViewPannableContents
- brushView={this._brushedView}
+ brushedView={this.brushedView}
isAnnotationOverlay={this.isAnnotationOverlay}
isAnnotationOverlayScrollable={this.props.isAnnotationOverlayScrollable}
transform={this.contentTransform}
zoomScaling={this.zoomScaling}
presPaths={this.showPresPaths}
- presPinView={BoolCast(this.Document.presPinView)}
+ presPinView={BoolCast(this.Document.config_pinView)}
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}
@@ -1943,19 +1984,19 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
};
@action
- brushView = (viewport: { width: number; height: number; panX: number; panY: number }) => {
- this._brushedView = { ...viewport, panX: viewport.panX - viewport.width / 2, panY: viewport.panY - viewport.height / 2, opacity: 1 };
+ brushView = (viewport: { width: number; height: number; panX: number; panY: number }, transTime: number) => {
this._brushtimer1 && clearTimeout(this._brushtimer1);
this._brushtimer && clearTimeout(this._brushtimer);
+ this._brushedView = { width: 0, height: 0, panX: 0, panY: 0, opacity: 0 };
this._brushtimer1 = setTimeout(
action(() => {
- this._brushedView.opacity = 0;
+ this._brushedView = { ...viewport, panX: viewport.panX - viewport.width / 2, panY: viewport.panY - viewport.height / 2, opacity: 1 };
this._brushtimer = setTimeout(
- action(() => (this._brushedView = { width: 0, height: 0, panX: 0, panY: 0, opacity: 0 })),
- 500
+ action(() => (this._brushedView.opacity = 0)),
+ 2500
);
}),
- 1000
+ transTime + 1
);
};
lightboxPanelWidth = () => Math.max(0, this.props.PanelWidth() - 30);
@@ -2082,7 +2123,7 @@ interface CollectionFreeFormViewPannableContentsProps {
presPinView?: boolean;
isAnnotationOverlay: boolean | undefined;
isAnnotationOverlayScrollable: boolean | undefined;
- brushView: { panX: number; panY: number; width: number; height: number; opacity: number };
+ brushedView: () => { panX: number; panY: number; width: number; height: number; opacity: number };
}
@observer
@@ -2154,6 +2195,7 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF
}
render() {
+ const brushedView = this.props.brushedView();
return (
<div
className={'collectionfreeformview' + (this.props.viewDefDivClick ? '-viewDef' : '-none')}
@@ -2171,21 +2213,18 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF
//willChange: "transform"
}}>
{this.props.children}
- {!this.props.brushView.width ? null : (
+ {
<div
className="collectionFreeFormView-brushView"
style={{
- zIndex: 1000,
- opacity: this.props.brushView.opacity,
- border: 'orange solid 2px',
- position: 'absolute',
- transform: `translate(${this.props.brushView.panX}px, ${this.props.brushView.panY}px)`,
- width: this.props.brushView.width,
- height: this.props.brushView.height,
- transition: 'opacity 2s',
+ opacity: brushedView.opacity,
+ transform: `translate(${brushedView.panX}px, ${brushedView.panY}px)`,
+ width: brushedView.width,
+ height: brushedView.height,
+ border: `orange solid ${brushedView.width * 0.005}px`,
}}
/>
- )}
+ }
{this.presPaths}
</div>
);
@@ -2197,6 +2236,7 @@ interface CollectionFreeFormViewBackgroundGridProps {
panY: () => number;
PanelWidth: () => number;
PanelHeight: () => number;
+ color: () => string;
isAnnotationOverlay?: boolean;
nativeDimScaling: () => number;
zoomScaling: () => number;
@@ -2218,7 +2258,7 @@ class CollectionFreeFormBackgroundGrid extends React.Component<CollectionFreeFor
const renderGridSpace = gridSpace * this.props.zoomScaling();
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)';
+ const strokeStyle = this.props.color();
return !this.props.nativeDimScaling() ? null : (
<canvas
className="collectionFreeFormView-grid"
@@ -2266,7 +2306,7 @@ export function CollectionBrowseClick(dv: DocumentView, clientX: number, clientY
}
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);
+ ffview?.zoomSmoothlyAboutPt(ffview.getTransform().transformPoint(clientX, clientY), ffview?.isAnnotationOverlay ? 1 : 0.5, browseTransitionTime);
Doc.linkFollowHighlight(dv?.props.Document, false);
}
});
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
index 0f51fe6ff..607f9fb95 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
@@ -8,6 +8,7 @@ import { IconButton } from 'browndash-components';
import { StrCast } from '../../../../fields/Types';
import { Doc } from '../../../../fields/Doc';
import { computed } from 'mobx';
+import { SettingsManager } from '../../../util/SettingsManager';
@observer
export class MarqueeOptionsMenu extends AntimodeMenu<AntimodeMenuProps> {
@@ -27,43 +28,18 @@ export class MarqueeOptionsMenu extends AntimodeMenu<AntimodeMenuProps> {
}
@computed get userColor() {
- return StrCast(Doc.UserDoc().userColor)
+ return SettingsManager.userColor;
}
render() {
const presPinWithViewIcon = <img src="/assets/pinWithView.png" style={{ width: 19 }} />;
const buttons = (
<>
- <IconButton
- tooltip={"Create a Collection"}
- onPointerDown={this.createCollection}
- icon={<FontAwesomeIcon icon="object-group"/>}
- color={this.userColor}
- />
- <IconButton
- tooltip={"Create a Grouping"}
- onPointerDown={e => this.createCollection(e, true)}
- icon={<FontAwesomeIcon icon="layer-group"/>}
- color={this.userColor}
- />
- <IconButton
- tooltip={"Summarize Documents"}
- onPointerDown={this.summarize}
- icon={<FontAwesomeIcon icon="compress-arrows-alt"/>}
- color={this.userColor}
- />
- <IconButton
- tooltip={"Delete Documents"}
- onPointerDown={this.delete}
- icon={<FontAwesomeIcon icon="trash-alt"/>}
- color={this.userColor}
- />
- <IconButton
- tooltip={"Pin selected region"}
- onPointerDown={this.pinWithView}
- icon={presPinWithViewIcon}
- color={this.userColor}
- />
+ <IconButton tooltip={'Create a Collection'} onPointerDown={this.createCollection} icon={<FontAwesomeIcon icon="object-group" />} color={this.userColor} />
+ <IconButton tooltip={'Create a Grouping'} onPointerDown={e => this.createCollection(e, true)} icon={<FontAwesomeIcon icon="layer-group" />} color={this.userColor} />
+ <IconButton tooltip={'Summarize Documents'} onPointerDown={this.summarize} icon={<FontAwesomeIcon icon="compress-arrows-alt" />} color={this.userColor} />
+ <IconButton tooltip={'Delete Documents'} onPointerDown={this.delete} icon={<FontAwesomeIcon icon="trash-alt" />} color={this.userColor} />
+ <IconButton tooltip={'Pin selected region'} onPointerDown={this.pinWithView} icon={<FontAwesomeIcon icon="map-pin" />} color={this.userColor} />
</>
);
return this.getElement(buttons);
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.scss b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
index e0f5cbe5b..7c9d0f6e1 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.scss
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
@@ -17,7 +17,6 @@
box-sizing: border-box;
position: absolute;
border-width: 1px;
- border-color: black;
pointer-events: none;
.marquee-legend {
bottom: -18px;
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 090cf356c..cd7bd28e9 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -10,7 +10,7 @@ import { SchemaHeaderField } from '../../../../fields/SchemaHeaderField';
import { Cast, DocCast, FieldValue, NumCast, StrCast } from '../../../../fields/Types';
import { ImageField } from '../../../../fields/URLField';
import { distributeAcls, GetEffectiveAcl, SharingPermissions } from '../../../../fields/util';
-import { intersectRect, returnFalse, Utils } from '../../../../Utils';
+import { intersectRect, lightOrDark, returnFalse, Utils } from '../../../../Utils';
import { CognitiveServices } from '../../../cognitive_services/CognitiveServices';
import { Docs, DocumentOptions, DocUtils } from '../../../documents/Documents';
import { DocumentType } from '../../../documents/DocumentTypes';
@@ -39,7 +39,7 @@ interface MarqueeViewProps {
trySelectCluster: (addToSel: boolean) => boolean;
nudge?: (x: number, y: number, nudgeTime?: number) => boolean;
ungroup?: () => void;
- setPreviewCursor?: (func: (x: number, y: number, drag: boolean, hide: boolean) => void) => void;
+ setPreviewCursor?: (func: (x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void) => void;
slowLoadDocuments: (files: File[] | string, options: DocumentOptions, generatedDocuments: Doc[], text: string, completed: ((doc: Doc[]) => void) | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => Promise<void>;
}
@@ -170,7 +170,15 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
TreeView._editTitleOnLoad = { id: slide[Id], parent: undefined };
this.props.addDocument?.(slide);
e.stopPropagation();
- }*/ else if (!e.ctrlKey && !e.metaKey && SelectionManager.Views().length < 2) {
+ }*/ else if (e.key === 'p' && e.ctrlKey) {
+ e.preventDefault();
+ (async () => {
+ const text: string = await navigator.clipboard.readText();
+ const ns = text.split('\n').filter(t => t.trim() !== '\r' && t.trim() !== '');
+ this.pasteTable(ns, x, y);
+ })();
+ 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('type new note');
this.props.addLiveTextDocument(DocUtils.GetNewTextDoc('-typed text-', x, y, 200, 100, this.props.xPadding === 0));
@@ -185,44 +193,26 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
// any row that has only one column is a section header-- this header is then added as a column to subsequent rows until the next header
// assumes each cell is a string or a number
pasteTable(ns: string[], x: number, y: number) {
- while (ns.length > 0 && ns[0].split('\t').length < 2) {
- ns.splice(0, 1);
- }
- if (ns.length > 0) {
- const columns = ns[0].split('\t');
- const docList: Doc[] = [];
- let groupAttr: string | number = '';
- const rowProto = new Doc();
- rowProto.title = rowProto.Id;
- rowProto._width = 200;
- 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) {
- groupAttr = values[0];
- continue;
- }
- const docDataProto = Doc.MakeDelegate(rowProto);
- 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;
- }
- docDataProto.title = i.toString();
- const doc = Doc.MakeDelegate(docDataProto);
- doc._width = 200;
- docList.push(doc);
+ let csvRows = [];
+ const headers = ns[0].split('\t');
+ csvRows.push(headers.join(','));
+ ns[0] = '';
+ const eachCell = ns.join('\t').split('\t');
+ let eachRow = [];
+ for (let i = 1; i < eachCell.length; i++) {
+ eachRow.push(eachCell[i].replace(/\,/g, ''));
+ if (i % headers.length == 0) {
+ csvRows.push(eachRow);
+ eachRow = [];
}
- const newCol = Docs.Create.SchemaDocument([...(groupAttr ? [new SchemaHeaderField('_group', '#f1efeb')] : []), ...columns.filter(c => c).map(c => new SchemaHeaderField(c, '#f1efeb'))], docList, {
- x: x,
- y: y,
- title: 'droppedTable',
- _width: 300,
- _height: 100,
- });
-
- this.props.addDocument?.(newCol);
}
+
+ const blob = new Blob([csvRows.join('\n')], { type: 'text/csv' });
+ const options = { x: x, y: y, title: 'droppedTable', _width: 300, _height: 100, type: 'text/csv' };
+ const file = new File([blob], 'droppedTable', options);
+ const loading = Docs.Create.LoadingDocument(file, options);
+ DocUtils.uploadFileToDoc(file, {}, loading);
+ this.props.addDocument?.(loading);
}
@action
@@ -235,7 +225,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
// allow marquee if right click OR alt+left click OR in adding presentation slide & left key drag mode
if (e.button === 2 || (e.button === 0 && e.altKey)) {
// if (e.altKey || (MarqueeView.DragMarquee && this.props.active(true))) {
- this.setPreviewCursor(e.clientX, e.clientY, true, false);
+ this.setPreviewCursor(e.clientX, e.clientY, true, false, this.props.Document);
// (!e.altKey) && e.stopPropagation(); // bcz: removed so that you can alt-click on button in a collection to switch link following behaviors.
e.preventDefault();
// }
@@ -308,17 +298,19 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
}
}
- setPreviewCursor = action((x: number, y: number, drag: boolean, hide: boolean) => {
+ setPreviewCursor = action((x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => {
if (hide) {
this._downX = this._lastX = x;
this._downY = this._lastY = y;
this._commandExecuted = false;
PreviewCursor.Visible = false;
+ PreviewCursor.Doc = undefined;
} else if (drag) {
this._downX = this._lastX = x;
this._downY = this._lastY = y;
this._commandExecuted = false;
PreviewCursor.Visible = false;
+ PreviewCursor.Doc = undefined;
this.cleanupInteractions(true);
document.addEventListener('pointermove', this.onPointerMove, true);
document.addEventListener('pointerup', this.onPointerUp, true);
@@ -328,6 +320,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
this._downY = y;
const effectiveAcl = GetEffectiveAcl(this.props.Document[DocData]);
if ([AclAdmin, AclEdit, AclAugment].includes(effectiveAcl)) {
+ PreviewCursor.Doc = doc;
PreviewCursor.Show(x, y, this.onKeyPress, this.props.addLiveTextDocument, this.props.getTransform, this.props.addDocument, this.props.nudge, this.props.slowLoadDocuments);
}
this.clearSelection();
@@ -342,7 +335,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
if (!(e.nativeEvent as any).marqueeHit) {
(e.nativeEvent as any).marqueeHit = true;
if (!this.props.trySelectCluster(e.shiftKey)) {
- this.setPreviewCursor(e.clientX, e.clientY, false, false);
+ this.setPreviewCursor(e.clientX, e.clientY, false, false, this.props.Document);
} else e.stopPropagation();
}
}
@@ -377,7 +370,6 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
: ((doc: Doc) => {
Doc.GetProto(doc).data = new List<Doc>(selected);
Doc.GetProto(doc).title = makeGroup ? 'grouping' : 'nested freeform';
- !this.props.isAnnotationOverlay && Doc.AddFileOrphan(Doc.GetProto(doc));
doc._freeform_panX = doc._freeform_panY = 0;
return doc;
})(Doc.MakeCopy(Doc.UserDoc().emptyCollection as Doc, true));
@@ -640,12 +632,20 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
transform: `translate(${p[0]}px, ${p[1]}px)`,
width: Math.abs(v[0]),
height: Math.abs(v[1]),
+ color: lightOrDark(this.props.Document?.backgroundColor ?? 'white'),
+ borderColor: lightOrDark(this.props.Document?.backgroundColor ?? 'white'),
zIndex: 2000,
}}>
{' '}
{this._lassoFreehand ? (
<svg height={2000} width={2000}>
- <polyline points={this._lassoPts.reduce((s, pt) => s + pt[0] + ',' + pt[1] + ' ', '')} fill="none" stroke="black" strokeWidth="1" strokeDasharray="3" />
+ <polyline //
+ points={this._lassoPts.reduce((s, pt) => s + pt[0] + ',' + pt[1] + ' ', '')}
+ fill="none"
+ stroke={lightOrDark(this.props.Document?.backgroundColor ?? 'white')}
+ strokeWidth="1"
+ strokeDasharray="3"
+ />
</svg>
) : (
<span className="marquee-legend" />
diff --git a/src/client/views/collections/collectionLinear/CollectionLinearView.scss b/src/client/views/collections/collectionLinear/CollectionLinearView.scss
index 6b3318bf3..d0c14a21d 100644
--- a/src/client/views/collections/collectionLinear/CollectionLinearView.scss
+++ b/src/client/views/collections/collectionLinear/CollectionLinearView.scss
@@ -17,7 +17,7 @@
.collectionLinearView-menuOpener {
user-select: none;
}
-
+
> input:not(:checked) ~ &.true {
background-color: transparent;
}
diff --git a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
index 56b8366d0..faf7501c4 100644
--- a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
+++ b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
@@ -1,5 +1,6 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tooltip } from '@material-ui/core';
+import { Toggle, ToggleType, Type } from 'browndash-components';
import { action, IReactionDisposer, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
@@ -7,21 +8,20 @@ import { Doc, Opt } from '../../../../fields/Doc';
import { Height, Width } from '../../../../fields/DocSymbols';
import { Id } from '../../../../fields/FieldSymbols';
import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
-import { emptyFunction, returnEmptyDoclist, returnTrue, StopEvent, Utils } from '../../../../Utils';
+import { emptyFunction, returnEmptyDoclist, returnTrue, Utils } from '../../../../Utils';
import { CollectionViewType } from '../../../documents/DocumentTypes';
+import { BranchingTrailManager } from '../../../util/BranchingTrailManager';
import { DocumentManager } from '../../../util/DocumentManager';
import { DragManager, dropActionType } from '../../../util/DragManager';
+import { SettingsManager } from '../../../util/SettingsManager';
import { Transform } from '../../../util/Transform';
import { DocumentLinksButton } from '../../nodes/DocumentLinksButton';
import { DocumentView } from '../../nodes/DocumentView';
import { LinkDescriptionPopup } from '../../nodes/LinkDescriptionPopup';
-import { StyleProp } from '../../StyleProvider';
import { UndoStack } from '../../UndoStack';
import { CollectionStackedTimeline } from '../CollectionStackedTimeline';
import { CollectionSubView } from '../CollectionSubView';
import './CollectionLinearView.scss';
-import { Button, Toggle, ToggleType, Type } from 'browndash-components';
-import { Colors } from '../../global/globalEnums';
/**
* CollectionLinearView is the class for rendering the horizontal collection
@@ -145,6 +145,7 @@ export class CollectionLinearView extends CollectionSubView() {
case '<LinkingUI>': return this.getLinkUI();
case '<CurrentlyPlayingUI>': return this.getCurrentlyPlayingUI();
case '<UndoStack>': return <UndoStack key={doc[Id]} width={200} height={40} inline={true} />;
+ case '<Branching>': return Doc.UserDoc().isBranchingMode ? <BranchingTrailManager /> : null;
}
const nested = doc._type_collection === CollectionViewType.Linear;
@@ -204,15 +205,16 @@ export class CollectionLinearView extends CollectionSubView() {
const menuOpener = (
<Toggle
- text={Cast(this.props.Document.icon, 'string', null)}
- icon={Cast(this.props.Document.icon, 'string', null) ? undefined : <FontAwesomeIcon icon={isExpanded ? 'minus' : 'plus'} />}
+ text={Cast(this.props.Document.icon, 'string', null)}
+ icon={Cast(this.props.Document.icon, 'string', null) ? undefined : <FontAwesomeIcon color={SettingsManager.userColor} icon={isExpanded ? 'minus' : 'plus'} />}
+ color={SettingsManager.userColor}
+ background={SettingsManager.userVariantColor}
type={Type.TERT}
- color={StrCast(Doc.UserDoc().userVariantColor, Colors.MEDIUM_BLUE)}
onPointerDown={e => e.stopPropagation()}
toggleType={ToggleType.BUTTON}
toggleStatus={BoolCast(this.layoutDoc.linearView_IsOpen)}
onClick={() => {
- this.layoutDoc.linearView_IsOpen = !isExpanded;
+ this.layoutDoc.linearView_IsOpen = !isExpanded;
}}
tooltip={isExpanded ? 'Close' : 'Open'}
fillWidth={true}
@@ -223,25 +225,22 @@ export class CollectionLinearView extends CollectionSubView() {
return (
<div className={`collectionLinearView-outer ${this.layoutDoc.linearView_SubMenu}`} style={{ backgroundColor: this.layoutDoc.linearView_IsOpen ? undefined : 'transparent' }}>
<div className="collectionLinearView" ref={this.createDashEventsTarget} onContextMenu={this.myContextMenu} style={{ minHeight: this.dimension(), pointerEvents: 'all' }}>
- {this.props.Document.linearView_Dropdown ?
- <div>Hello World!</div>
- :
+ {
<>
- {!this.props.Document.linearView_Expandable ? null : menuOpener}
- {!this.layoutDoc.linearView_IsOpen ? null : (
- <div
- className="collectionLinearView-content"
- style={{
- height: this.dimension(),
- flexDirection: flexDir as any,
- gap: flexGap,
- }}>
- {this.childLayoutPairs.map(pair => this.getDisplayDoc(pair.layout))}
- </div>
- )}
+ {!this.layoutDoc.linearView_Expandable ? null : menuOpener}
+ {!this.layoutDoc.linearView_IsOpen ? null : (
+ <div
+ className="collectionLinearView-content"
+ style={{
+ height: this.dimension(),
+ flexDirection: flexDir as any,
+ gap: flexGap,
+ }}>
+ {this.childLayoutPairs.map(pair => this.getDisplayDoc(pair.layout))}
+ </div>
+ )}
</>
}
-
</div>
</div>
);
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss
index f87a06033..cb0d5e03f 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss
@@ -9,6 +9,12 @@
flex-direction: column;
width: 100%;
align-items: center;
+ position: relative;
+ > .iconButton-container {
+ top: 0;
+ left: 0;
+ position: absolute;
+ }
.contentFittingDocumentView {
width: unset;
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
index 10532b9d9..81453c0b8 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
@@ -1,3 +1,6 @@
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Tooltip } from '@mui/material';
+import { Button } from 'browndash-components';
import { action, computed } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
@@ -5,8 +8,9 @@ import { Doc, DocListCast } from '../../../../fields/Doc';
import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
import { returnFalse } from '../../../../Utils';
import { DragManager, dropActionType } from '../../../util/DragManager';
+import { SettingsManager } from '../../../util/SettingsManager';
import { Transform } from '../../../util/Transform';
-import { undoBatch } from '../../../util/UndoManager';
+import { undoable, undoBatch } from '../../../util/UndoManager';
import { DocumentView } from '../../nodes/DocumentView';
import { CollectionSubView } from '../CollectionSubView';
import './CollectionMulticolumnView.scss';
@@ -299,10 +303,13 @@ export class CollectionMulticolumnView extends CollectionSubView() {
.scale(this.props.NativeDimScaling?.() || 1);
const shouldNotScale = () => this.props.fitContentsToBox?.() || BoolCast(layout.freeform_fitContentsToBox);
collector.push(
- <div className="document-wrapper" key={'wrapper' + i} style={{ width: width() }}>
- {this.getDisplayDoc(layout, dxf, docwidth, docheight, shouldNotScale)}
- <WidthLabel layout={layout} collectionDoc={Document} />
- </div>,
+ <Tooltip title={'Tab: ' + StrCast(layout.title)}>
+ <div className="document-wrapper" key={'wrapper' + i} style={{ width: width() }}>
+ {this.getDisplayDoc(layout, dxf, docwidth, docheight, shouldNotScale)}
+ <Button tooltip="Remove document from header bar" icon={<FontAwesomeIcon icon="times" size="lg" />} onClick={undoable(e => this.props.removeDocument?.(layout), 'close doc')} color={SettingsManager.userColor} />
+ <WidthLabel layout={layout} collectionDoc={Document} />
+ </div>
+ </Tooltip>,
<ResizeBar
width={resizerWidth}
key={'resizer' + i}
diff --git a/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx b/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx
index 273e609ca..868b1140d 100644
--- a/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx
+++ b/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx
@@ -1,12 +1,12 @@
-import * as React from "react";
-import { observer } from "mobx-react";
-import { observable, action } from "mobx";
-import { Doc } from "../../../../fields/Doc";
-import { NumCast, StrCast } from "../../../../fields/Types";
-import { DimUnit } from "./CollectionMulticolumnView";
-import { UndoManager } from "../../../util/UndoManager";
-import { StyleProviderFunc } from "../../nodes/DocumentView";
-import { StyleProp } from "../../StyleProvider";
+import * as React from 'react';
+import { observer } from 'mobx-react';
+import { observable, action } from 'mobx';
+import { Doc } from '../../../../fields/Doc';
+import { NumCast, StrCast } from '../../../../fields/Types';
+import { DimUnit } from './CollectionMulticolumnView';
+import { UndoManager } from '../../../util/UndoManager';
+import { StyleProviderFunc } from '../../nodes/DocumentView';
+import { StyleProp } from '../../StyleProvider';
interface ResizerProps {
width: number;
@@ -31,13 +31,13 @@ export default class ResizeBar extends React.Component<ResizerProps> {
this.props.select(false);
e.stopPropagation();
e.preventDefault();
- window.removeEventListener("pointermove", this.onPointerMove);
- window.removeEventListener("pointerup", this.onPointerUp);
- window.addEventListener("pointermove", this.onPointerMove);
- window.addEventListener("pointerup", this.onPointerUp);
+ window.removeEventListener('pointermove', this.onPointerMove);
+ window.removeEventListener('pointerup', this.onPointerUp);
+ window.addEventListener('pointermove', this.onPointerMove);
+ window.addEventListener('pointerup', this.onPointerUp);
this.isResizingActive = true;
- this._resizeUndo = UndoManager.StartBatch("multcol resizing");
- }
+ this._resizeUndo = UndoManager.StartBatch('multcol resizing');
+ };
private onPointerMove = ({ movementX }: PointerEvent) => {
const { toLeft, toRight, columnUnitLength } = this.props;
@@ -47,30 +47,30 @@ export default class ResizeBar extends React.Component<ResizerProps> {
const unitLength = columnUnitLength();
if (unitLength) {
if (toNarrow) {
- const scale = StrCast(toNarrow._dimUnit, "*") === DimUnit.Ratio ? unitLength : 1;
+ const scale = StrCast(toNarrow._dimUnit, '*') === DimUnit.Ratio ? unitLength : 1;
toNarrow._dimMagnitude = Math.max(0.05, NumCast(toNarrow._dimMagnitude, 1) - Math.abs(movementX) / scale);
}
if (toWiden) {
- const scale = StrCast(toWiden._dimUnit, "*") === DimUnit.Ratio ? unitLength : 1;
+ const scale = StrCast(toWiden._dimUnit, '*') === DimUnit.Ratio ? unitLength : 1;
toWiden._dimMagnitude = Math.max(0.05, NumCast(toWiden._dimMagnitude, 1) + Math.abs(movementX) / scale);
}
}
- }
+ };
private get isActivated() {
const { toLeft, toRight } = this.props;
if (toLeft && toRight) {
- if (StrCast(toLeft._dimUnit, "*") === DimUnit.Pixel && StrCast(toRight._dimUnit, "*") === DimUnit.Pixel) {
+ if (StrCast(toLeft._dimUnit, '*') === DimUnit.Pixel && StrCast(toRight._dimUnit, '*') === DimUnit.Pixel) {
return false;
}
return true;
} else if (toLeft) {
- if (StrCast(toLeft._dimUnit, "*") === DimUnit.Pixel) {
+ if (StrCast(toLeft._dimUnit, '*') === DimUnit.Pixel) {
return false;
}
return true;
} else if (toRight) {
- if (StrCast(toRight._dimUnit, "*") === DimUnit.Pixel) {
+ if (StrCast(toRight._dimUnit, '*') === DimUnit.Pixel) {
return false;
}
return true;
@@ -82,23 +82,25 @@ export default class ResizeBar extends React.Component<ResizerProps> {
private onPointerUp = () => {
this.isResizingActive = false;
this.isHoverActive = false;
- window.removeEventListener("pointermove", this.onPointerMove);
- window.removeEventListener("pointerup", this.onPointerUp);
+ window.removeEventListener('pointermove', this.onPointerMove);
+ window.removeEventListener('pointerup', this.onPointerUp);
this._resizeUndo?.end();
this._resizeUndo = undefined;
- }
+ };
render() {
- return <div className="multiColumnResizer"
- style={{
- width: this.props.width,
- backgroundColor: !this.props.isContentActive?.() ? "" : this.props.styleProvider?.(undefined, undefined, StyleProp.WidgetColor)
- }}
- onPointerEnter={action(() => this.isHoverActive = true)}
- onPointerLeave={action(() => !this.isResizingActive && (this.isHoverActive = false))}
- >
- <div className={"multiColumnResizer-hdl"} onPointerDown={e => this.registerResizing(e)} />
- </div>;
+ return (
+ <div
+ className="multiColumnResizer"
+ style={{
+ pointerEvents: this.props.isContentActive?.() ? 'all' : 'none',
+ width: this.props.width,
+ backgroundColor: !this.props.isContentActive?.() ? '' : this.props.styleProvider?.(undefined, undefined, StyleProp.WidgetColor),
+ }}
+ onPointerEnter={action(() => (this.isHoverActive = true))}
+ onPointerLeave={action(() => !this.isResizingActive && (this.isHoverActive = false))}>
+ <div className={'multiColumnResizer-hdl'} onPointerDown={e => this.registerResizing(e)} />
+ </div>
+ );
}
-
-} \ No newline at end of file
+}
diff --git a/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx b/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx
index 006ef4df6..5a9d6a82c 100644
--- a/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx
+++ b/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx
@@ -1,12 +1,12 @@
-import * as React from "react";
-import { observer } from "mobx-react";
-import { observable, action } from "mobx";
-import { Doc } from "../../../../fields/Doc";
-import { NumCast, StrCast } from "../../../../fields/Types";
-import { DimUnit } from "./CollectionMultirowView";
-import { UndoManager } from "../../../util/UndoManager";
-import { StyleProp } from "../../StyleProvider";
-import { StyleProviderFunc } from "../../nodes/DocumentView";
+import * as React from 'react';
+import { observer } from 'mobx-react';
+import { observable, action } from 'mobx';
+import { Doc } from '../../../../fields/Doc';
+import { NumCast, StrCast } from '../../../../fields/Types';
+import { DimUnit } from './CollectionMultirowView';
+import { UndoManager } from '../../../util/UndoManager';
+import { StyleProp } from '../../StyleProvider';
+import { StyleProviderFunc } from '../../nodes/DocumentView';
interface ResizerProps {
height: number;
@@ -29,13 +29,13 @@ export default class ResizeBar extends React.Component<ResizerProps> {
private registerResizing = (e: React.PointerEvent<HTMLDivElement>) => {
e.stopPropagation();
e.preventDefault();
- window.removeEventListener("pointermove", this.onPointerMove);
- window.removeEventListener("pointerup", this.onPointerUp);
- window.addEventListener("pointermove", this.onPointerMove);
- window.addEventListener("pointerup", this.onPointerUp);
+ window.removeEventListener('pointermove', this.onPointerMove);
+ window.removeEventListener('pointerup', this.onPointerUp);
+ window.addEventListener('pointermove', this.onPointerMove);
+ window.addEventListener('pointerup', this.onPointerUp);
this.isResizingActive = true;
- this._resizeUndo = UndoManager.StartBatch("multcol resizing");
- }
+ this._resizeUndo = UndoManager.StartBatch('multcol resizing');
+ };
private onPointerMove = ({ movementY }: PointerEvent) => {
const { toTop: toTop, toBottom: toBottom, columnUnitLength } = this.props;
@@ -45,30 +45,30 @@ export default class ResizeBar extends React.Component<ResizerProps> {
const unitLength = columnUnitLength();
if (unitLength) {
if (toNarrow) {
- const scale = StrCast(toNarrow._dimUnit, "*") === DimUnit.Ratio ? unitLength : 1;
+ const scale = StrCast(toNarrow._dimUnit, '*') === DimUnit.Ratio ? unitLength : 1;
toNarrow._dimMagnitude = Math.max(0.05, NumCast(toNarrow._dimMagnitude, 1) - Math.abs(movementY) / scale);
}
if (toWiden) {
- const scale = StrCast(toWiden._dimUnit, "*") === DimUnit.Ratio ? unitLength : 1;
+ const scale = StrCast(toWiden._dimUnit, '*') === DimUnit.Ratio ? unitLength : 1;
toWiden._dimMagnitude = Math.max(0.05, NumCast(toWiden._dimMagnitude, 1) + Math.abs(movementY) / scale);
}
}
- }
+ };
private get isActivated() {
const { toTop, toBottom } = this.props;
if (toTop && toBottom) {
- if (StrCast(toTop._dimUnit, "*") === DimUnit.Pixel && StrCast(toBottom._dimUnit, "*") === DimUnit.Pixel) {
+ if (StrCast(toTop._dimUnit, '*') === DimUnit.Pixel && StrCast(toBottom._dimUnit, '*') === DimUnit.Pixel) {
return false;
}
return true;
} else if (toTop) {
- if (StrCast(toTop._dimUnit, "*") === DimUnit.Pixel) {
+ if (StrCast(toTop._dimUnit, '*') === DimUnit.Pixel) {
return false;
}
return true;
} else if (toBottom) {
- if (StrCast(toBottom._dimUnit, "*") === DimUnit.Pixel) {
+ if (StrCast(toBottom._dimUnit, '*') === DimUnit.Pixel) {
return false;
}
return true;
@@ -80,23 +80,25 @@ export default class ResizeBar extends React.Component<ResizerProps> {
private onPointerUp = () => {
this.isResizingActive = false;
this.isHoverActive = false;
- window.removeEventListener("pointermove", this.onPointerMove);
- window.removeEventListener("pointerup", this.onPointerUp);
+ window.removeEventListener('pointermove', this.onPointerMove);
+ window.removeEventListener('pointerup', this.onPointerUp);
this._resizeUndo?.end();
this._resizeUndo = undefined;
- }
+ };
render() {
- return <div className="multiRowResizer"
- style={{
- height: this.props.height,
- backgroundColor: !this.props.isContentActive?.() ? "" : this.props.styleProvider?.(undefined, undefined, StyleProp.WidgetColor)
- }}
- onPointerEnter={action(() => this.isHoverActive = true)}
- onPointerLeave={action(() => !this.isResizingActive && (this.isHoverActive = false))}
- >
- <div className={"multiRowResizer-hdl"} onPointerDown={e => this.registerResizing(e)} />
- </div>;
+ return (
+ <div
+ className="multiRowResizer"
+ style={{
+ pointerEvents: this.props.isContentActive?.() ? 'all' : 'none',
+ height: this.props.height,
+ backgroundColor: !this.props.isContentActive?.() ? '' : this.props.styleProvider?.(undefined, undefined, StyleProp.WidgetColor),
+ }}
+ onPointerEnter={action(() => (this.isHoverActive = true))}
+ onPointerLeave={action(() => !this.isResizingActive && (this.isHoverActive = false))}>
+ <div className={'multiRowResizer-hdl'} onPointerDown={e => this.registerResizing(e)} />
+ </div>
+ );
}
-
-} \ No newline at end of file
+}
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.scss b/src/client/views/collections/collectionSchema/CollectionSchemaView.scss
index 52ebb7763..76bd392a5 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaView.scss
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.scss
@@ -9,12 +9,32 @@
.schema-table {
background-color: $white;
cursor: grab;
+ width: 100%;
.schema-table-content {
overflow: overlay;
scroll-behavior: smooth;
}
+ .schema-add {
+ position: relative;
+ height: 30;
+ display: flex;
+ align-items: center;
+ width: 100%;
+ text-align: right;
+ background: lightgray;
+ .editableView-container-editing {
+ width: 100%;
+ }
+ .editableView-input {
+ width: 100%;
+ float: right;
+ text-align: right;
+ background: yellow;
+ }
+ }
+
.schema-column-menu,
.schema-filter-menu {
background: $light-gray;
@@ -173,6 +193,12 @@
flex-direction: row;
height: 100%;
overflow: auto;
+
+ .schemaSelectionCell {
+ align-self: center;
+ width: 100%;
+ display: flex;
+ }
}
.schema-header-row > .schema-column-header:nth-child(2) > .left {
@@ -185,7 +211,7 @@
overflow-x: hidden;
overflow-y: auto;
padding: 5px;
- display: inline-block;
+ display: inline-flex;
}
.schema-row {
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
index ee5bf82ed..d2b61167e 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
@@ -63,7 +63,7 @@ export class CollectionSchemaView extends CollectionSubView() {
public static _minColWidth: number = 25;
public static _rowMenuWidth: number = 60;
public static _previewDividerWidth: number = 4;
- public static _newNodeInputHeight: number = 30;
+ public static _newNodeInputHeight: number = 20;
public fieldInfos = new ObservableMap<string, FInfo>();
@observable _menuKeys: string[] = [];
@@ -609,10 +609,10 @@ export class CollectionSchemaView extends CollectionSubView() {
this._menuKeys = this.documentKeys.filter(value => value.toLowerCase().includes(this._menuValue.toLowerCase()));
};
- getFieldFilters = (field: string) => StrListCast(this.Document._childFilters).filter(filter => filter.split(':')[0] == field);
+ getFieldFilters = (field: string) => StrListCast(this.Document._childFilters).filter(filter => filter.split(Doc.FilterSep)[0] == field);
removeFieldFilters = (field: string) => {
- this.getFieldFilters(field).forEach(filter => Doc.setDocFilter(this.Document, field, filter.split(':')[1], 'remove'));
+ this.getFieldFilters(field).forEach(filter => Doc.setDocFilter(this.Document, field, filter.split(Doc.FilterSep)[1], 'remove'));
};
onFilterKeyDown = (e: React.KeyboardEvent) => {
@@ -766,8 +766,8 @@ export class CollectionSchemaView extends CollectionSubView() {
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(':');
+ const ind = filters.findIndex(filter => filter.split(Doc.FilterSep)[1] === key);
+ const fields = ind === -1 ? undefined : filters[ind].split(Doc.FilterSep);
bool = fields ? fields[2] === 'check' : false;
}
return (
@@ -810,7 +810,6 @@ export class CollectionSchemaView extends CollectionSubView() {
}
@computed get sortedDocs() {
- trace();
const field = StrCast(this.layoutDoc.sortField);
const desc = BoolCast(this.layoutDoc.sortDesc);
const docs = !field
@@ -837,6 +836,7 @@ export class CollectionSchemaView extends CollectionSubView() {
<div ref={this._menuTarget} style={{ background: 'red', top: 0, left: 0, position: 'absolute', zIndex: 10000 }}></div>
<div
className="schema-table"
+ style={{ width: `calc(100% - ${this.previewWidth + 4}px)` }}
onWheel={e => this.props.isContentActive() && e.stopPropagation()}
ref={r => {
// prevent wheel events from passively propagating up through containers
@@ -870,14 +870,18 @@ export class CollectionSchemaView extends CollectionSubView() {
{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}
- />
+ {this.layoutDoc.chromeHidden ? null : (
+ <div className="schema-add">
+ <EditableView
+ GetValue={returnEmptyString}
+ SetValue={undoable(value => (value ? this.addRow(Docs.Create.TextDocument(value, { title: value, _layout_autoHeight: true })) : false), 'add text doc')}
+ placeholder={"Type text to create note or ':' to create specific type"}
+ contents={'+ New Node'}
+ menuCallback={this.menuCallback}
+ height={CollectionSchemaView._newNodeInputHeight}
+ />
+ </div>
+ )}
</div>
{this.previewWidth > 0 && <div className="schema-preview-divider" style={{ width: CollectionSchemaView._previewDividerWidth }} onPointerDown={this.onDividerDown}></div>}
{this.previewWidth > 0 && (
diff --git a/src/client/views/collections/collectionSchema/SchemaRowBox.tsx b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx
index e8e606030..4e418728f 100644
--- a/src/client/views/collections/collectionSchema/SchemaRowBox.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx
@@ -16,6 +16,10 @@ import { CollectionSchemaView } from './CollectionSchemaView';
import './CollectionSchemaView.scss';
import { SchemaTableCell } from './SchemaTableCell';
import { Transform } from '../../../util/Transform';
+import { IconButton, Size } from 'browndash-components';
+import { CgClose } from 'react-icons/cg';
+import { FaExternalLinkAlt } from 'react-icons/fa';
+import { emptyFunction, returnFalse, setupMoveUpEvents } from '../../../../Utils';
@observer
export class SchemaRowBox extends ViewBoxBaseComponent<FieldViewProps>() {
@@ -110,22 +114,40 @@ export class SchemaRowBox extends ViewBoxBaseComponent<FieldViewProps>() {
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>
+ <IconButton
+ tooltip="close"
+ icon={<CgClose size={'16px'} />}
+ size={Size.XSMALL}
+ onPointerDown={e =>
+ setupMoveUpEvents(
+ this,
+ e,
+ returnFalse,
+ emptyFunction,
+ undoable(e => {
+ e.stopPropagation();
+ this.props.removeDocument?.(this.rootDoc);
+ }, 'Delete Row')
+ )
+ }
+ />
+ <IconButton
+ tooltip="open preview"
+ icon={<FaExternalLinkAlt />}
+ size={Size.XSMALL}
+ onPointerDown={e =>
+ setupMoveUpEvents(
+ this,
+ e,
+ returnFalse,
+ emptyFunction,
+ undoable(e => {
+ e.stopPropagation();
+ this.props.addDocTab(this.rootDoc, OpenWhere.addRight);
+ }, 'Open schema Doc preview')
+ )
+ }
+ />
</div>
<div className="row-cells">
{this.schemaView?.columnKeys?.map((key, index) => (
diff --git a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
index 1c9c0de53..e18f27fb0 100644
--- a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx
@@ -325,10 +325,34 @@ export class SchemaEnumerationCell extends React.Component<SchemaTableCellProps>
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 className="schemaSelectionCell" style={{ color, textDecoration, cursor, pointerEvents }}>
<div style={{ width: '100%' }}>
<Select
styles={{
+ container: base => ({
+ ...base,
+ height: 20,
+ }),
+ control: base => ({
+ ...base,
+ height: 20,
+ minHeight: 20,
+ }),
+ placeholder: base => ({
+ ...base,
+ top: '40%',
+ }),
+ input: base => ({
+ ...base,
+ height: 20,
+ minHeight: 20,
+ margin: 0,
+ }),
+ indicatorsContainer: base => ({
+ ...base,
+ height: 20,
+ transform: 'scale(0.75)',
+ }),
menuPortal: base => ({
...base,
left: 0,
diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts
index b906065a0..d8ed93bd7 100644
--- a/src/client/views/global/globalScripts.ts
+++ b/src/client/views/global/globalScripts.ts
@@ -1,26 +1,28 @@
-import { Colors } from "browndash-components";
-import { runInAction, action } from "mobx";
-import { aggregateBounds } from "../../../Utils";
-import { Doc } from "../../../fields/Doc";
-import { Width, Height } from "../../../fields/DocSymbols";
-import { InkTool } from "../../../fields/InkField";
-import { Cast, StrCast, NumCast, BoolCast } from "../../../fields/Types";
-import { WebField } from "../../../fields/URLField";
-import { GestureUtils } from "../../../pen-gestures/GestureUtils";
-import { LinkManager } from "../../util/LinkManager";
-import { ScriptingGlobals } from "../../util/ScriptingGlobals";
-import { SelectionManager } from "../../util/SelectionManager";
-import { UndoManager } from "../../util/UndoManager";
-import { GestureOverlay } from "../GestureOverlay";
-import { InkTranscription } from "../InkTranscription";
-import { ActiveFillColor, SetActiveFillColor, ActiveIsInkMask, SetActiveIsInkMask, ActiveInkWidth, SetActiveInkWidth, ActiveInkColor, SetActiveInkColor } from "../InkingStroke";
-import { CollectionFreeFormView } from "../collections/collectionFreeForm";
-import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView";
-import { WebBox } from "../nodes/WebBox";
-import { RichTextMenu } from "../nodes/formattedText/RichTextMenu";
-import { DocumentType } from "../../documents/DocumentTypes";
+import { Colors } from 'browndash-components';
+import { runInAction, action } from 'mobx';
+import { aggregateBounds } from '../../../Utils';
+import { Doc } from '../../../fields/Doc';
+import { Width, Height } from '../../../fields/DocSymbols';
+import { InkTool } from '../../../fields/InkField';
+import { Cast, StrCast, NumCast, BoolCast } from '../../../fields/Types';
+import { WebField } from '../../../fields/URLField';
+import { GestureUtils } from '../../../pen-gestures/GestureUtils';
+import { LinkManager } from '../../util/LinkManager';
+import { ScriptingGlobals } from '../../util/ScriptingGlobals';
+import { SelectionManager } from '../../util/SelectionManager';
+import { undoable, UndoManager } from '../../util/UndoManager';
+import { GestureOverlay } from '../GestureOverlay';
+import { InkTranscription } from '../InkTranscription';
+import { ActiveFillColor, SetActiveFillColor, ActiveIsInkMask, SetActiveIsInkMask, ActiveInkWidth, SetActiveInkWidth, ActiveInkColor, SetActiveInkColor } from '../InkingStroke';
+import { CollectionFreeFormView } from '../collections/collectionFreeForm';
+import { CollectionFreeFormDocumentView } from '../nodes/CollectionFreeFormDocumentView';
+import { WebBox } from '../nodes/WebBox';
+import { RichTextMenu } from '../nodes/formattedText/RichTextMenu';
+import { DocumentType } from '../../documents/DocumentTypes';
-ScriptingGlobals.add(function IsNoneSelected() { return SelectionManager.Views().length <= 0; }, "are no document selected");
+ScriptingGlobals.add(function IsNoneSelected() {
+ return SelectionManager.Views().length <= 0;
+}, 'are no document selected');
// toggle: Set overlay status of selected document
ScriptingGlobals.add(function setView(view: string) {
@@ -31,7 +33,6 @@ ScriptingGlobals.add(function setView(view: string) {
// toggle: Set overlay status of selected document
ScriptingGlobals.add(function setBackgroundColor(color?: string, checkResult?: boolean) {
const selectedViews = SelectionManager.Views();
- console.log(color, checkResult);
if (Doc.ActiveTool !== InkTool.None) {
if (checkResult) {
return ActiveFillColor();
@@ -52,7 +53,6 @@ ScriptingGlobals.add(function setBackgroundColor(color?: string, checkResult?: b
if (contentFrameNumber !== undefined) {
CollectionFreeFormDocumentView.setStringValues(contentFrameNumber, dv.rootDoc, { fieldKey: color });
} else {
- console.log('setting color to: ', color)
dv.rootDoc['_' + fieldKey] = color;
}
});
@@ -77,7 +77,6 @@ ScriptingGlobals.add(function setHeaderColor(color?: string, checkResult?: boole
// toggle: Set overlay status of selected document
ScriptingGlobals.add(function toggleOverlay(checkResult?: boolean) {
- console.log(checkResult);
const selected = SelectionManager.Views().length ? SelectionManager.Views()[0] : undefined;
if (checkResult) {
if (NumCast(selected?.Document.z) >= 1) return true;
@@ -86,10 +85,10 @@ ScriptingGlobals.add(function toggleOverlay(checkResult?: boolean) {
selected ? selected.props.CollectionFreeFormDocumentView?.().float() : console.log('[FontIconBox.tsx] toggleOverlay failed');
});
-ScriptingGlobals.add(function showFreeform(attr: 'flashcards' | 'grid' | 'snaplines' | 'clusters' | 'arrange' | 'viewAll', checkResult?: boolean) {
+ScriptingGlobals.add(function showFreeform(attr: 'flashcards' | 'center' | 'grid' | 'snaplines' | 'clusters' | 'arrange' | 'viewAll' | 'viewAllPersist', checkResult?: boolean) {
const selected = SelectionManager.Docs().lastElement();
// prettier-ignore
- const map: Map<'flashcards' | 'grid' | 'snaplines' | 'clusters' | 'arrange'| 'viewAll', { waitForRender?: boolean, checkResult: (doc:Doc) => any; setDoc: (doc:Doc) => void;}> = new Map([
+ const map: Map<'flashcards' | 'center' |'grid' | 'snaplines' | 'clusters' | 'arrange'| 'viewAll' | 'viewAllPersist', { waitForRender?: boolean, checkResult: (doc:Doc) => any; setDoc: (doc:Doc) => void;}> = new Map([
['grid', {
checkResult: (doc:Doc) => BoolCast(doc._freeform_backgroundGrid, false),
setDoc: (doc:Doc) => doc._freeform_backgroundGrid = !doc._freeform_backgroundGrid,
@@ -102,6 +101,14 @@ ScriptingGlobals.add(function showFreeform(attr: 'flashcards' | 'grid' | 'snapli
checkResult: (doc:Doc) => BoolCast(doc._freeform_fitContentsToBox, false),
setDoc: (doc:Doc) => doc._freeform_fitContentsToBox = !doc._freeform_fitContentsToBox,
}],
+ ['center', {
+ checkResult: (doc:Doc) => BoolCast(doc._stacking_alignCenter, false),
+ setDoc: (doc:Doc) => doc._stacking_alignCenter = !doc._stacking_alignCenter,
+ }],
+ ['viewAllPersist', {
+ checkResult: (doc:Doc) => false,
+ setDoc: (doc:Doc) => doc.fitContentOnce = true
+ }],
['clusters', {
waitForRender: true, // flags that undo batch should terminate after a re-render giving the script the chance to fire
checkResult: (doc:Doc) => BoolCast(doc._freeform_useClusters, false),
@@ -119,7 +126,6 @@ ScriptingGlobals.add(function showFreeform(attr: 'flashcards' | 'grid' | 'snapli
]);
if (checkResult) {
- console.log(attr, map.get(attr)?.checkResult(selected))
return map.get(attr)?.checkResult(selected);
}
const batch = map.get(attr)?.waitForRender ? UndoManager.StartBatch('set freeform attribute') : { end: () => {} };
@@ -180,8 +186,8 @@ ScriptingGlobals.add(function toggleCharStyle(charStyle: attrname, checkResult?:
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)) }],
+ ['dictation', { checkResult: () => textView?._recordingDictation ? true:false,
+ toggle: () => textView && runInAction(() => (textView._recordingDictation = !textView._recordingDictation)) }],
['noAutoLink',{ checkResult: () => (editorView ? RichTextMenu.Instance.noAutoLink : false),
toggle: () => editorView && RichTextMenu.Instance?.toggleNoAutoLinkAnchor()}],
['bold', { checkResult: () => (editorView ? RichTextMenu.Instance.bold : (Doc.UserDoc().fontWeight === 'bold') ? true:false),
@@ -193,14 +199,12 @@ ScriptingGlobals.add(function toggleCharStyle(charStyle: attrname, checkResult?:
const map = new Map(attrs.concat(alignments).concat(listings));
if (checkResult) {
- console.log(charStyle, checkResult, map.get(charStyle)?.checkResult());
return map.get(charStyle)?.checkResult();
}
- map.get(charStyle)?.toggle();
+ undoable(() => map.get(charStyle)?.toggle(), 'toggle ' + charStyle)();
});
export function checkInksToGroup() {
- // console.log("getting here to inks group");
if (Doc.ActiveTool === InkTool.Write) {
CollectionFreeFormView.collectionsWithUnprocessedInk.forEach(ffView => {
// TODO: nda - will probably want to go through ffView unprocessed docs and then see if any of the inksToGroup docs are in it and only use those
diff --git a/src/client/views/linking/LinkMenu.scss b/src/client/views/linking/LinkMenu.scss
index 80cf93ed8..0b9f32eee 100644
--- a/src/client/views/linking/LinkMenu.scss
+++ b/src/client/views/linking/LinkMenu.scss
@@ -12,7 +12,6 @@
position: relative;
border: 1px solid #e4e4e4;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23);
- background: white;
max-height: 230px;
overflow-y: scroll;
z-index: 10;
diff --git a/src/client/views/linking/LinkMenu.tsx b/src/client/views/linking/LinkMenu.tsx
index 65d13a6c3..9dc133e28 100644
--- a/src/client/views/linking/LinkMenu.tsx
+++ b/src/client/views/linking/LinkMenu.tsx
@@ -7,6 +7,7 @@ import { LinkDocPreview } from '../nodes/LinkDocPreview';
import './LinkMenu.scss';
import { LinkMenuGroup } from './LinkMenuGroup';
import React = require('react');
+import { SettingsManager } from '../../util/SettingsManager';
interface Props {
docView: DocumentView;
@@ -58,7 +59,7 @@ export class LinkMenu extends React.Component<Props> {
const style = this.props.style ?? (dv => ({ left: dv?.left || 0, top: this.props.docView.topMost ? undefined : (dv?.bottom || 0) + 15, bottom: this.props.docView.topMost ? 20 : undefined, maxWidth: 200 }))(this.props.docView.getBounds());
return (
- <div className="linkMenu" ref={this._linkMenuRef} style={{ ...style }}>
+ <div className="linkMenu" ref={this._linkMenuRef} style={{ ...style, background: SettingsManager.userBackgroundColor, color: SettingsManager.userColor }}>
<div className="linkMenu-list">{this.renderAllGroups(LinkManager.Instance.getRelatedGroupedLinks(sourceAnchor))}</div>
</div>
);
diff --git a/src/client/views/linking/LinkMenuGroup.tsx b/src/client/views/linking/LinkMenuGroup.tsx
index 8324a97d9..c1a5a634c 100644
--- a/src/client/views/linking/LinkMenuGroup.tsx
+++ b/src/client/views/linking/LinkMenuGroup.tsx
@@ -23,10 +23,10 @@ interface LinkMenuGroupProps {
export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> {
private _menuRef = React.createRef<HTMLDivElement>();
- getBackgroundColor = (): string => {
+ getBackgroundColor = (): string | undefined => {
const link_relationshipList = StrListCast(Doc.UserDoc().link_relationshipList);
const linkColorList = StrListCast(Doc.UserDoc().link_ColorList);
- let color = 'white';
+ let color: string | undefined;
// if this link's relationship property is not default "link", set its color
if (link_relationshipList) {
const relationshipIndex = link_relationshipList.indexOf(this.props.groupType);
diff --git a/src/client/views/linking/LinkMenuItem.scss b/src/client/views/linking/LinkMenuItem.scss
index 806a2c381..e83f631a1 100644
--- a/src/client/views/linking/LinkMenuItem.scss
+++ b/src/client/views/linking/LinkMenuItem.scss
@@ -12,8 +12,6 @@
padding-top: 1px;
padding-bottom: 1px;
- background-color: white;
-
.linkMenu-name {
position: relative;
width: auto;
@@ -25,7 +23,7 @@
.linkMenu-source-title {
text-decoration: none;
- color: rgb(43, 43, 43);
+ //color: rgb(43, 43, 43);
font-size: 7px;
padding-bottom: 0.75px;
@@ -48,13 +46,12 @@
height: 12px;
padding: 1px;
margin-right: 3px;
- color: rgb(161, 161, 161);
+ // color: rgb(161, 161, 161);
}
}
.linkMenu-destination-title {
text-decoration: none;
- color: #4476f7;
font-size: 13px;
line-height: 0.9;
padding-bottom: 2px;
@@ -64,7 +61,6 @@
&:hover {
text-decoration: underline;
- color: rgb(60, 90, 156);
//display: inline;
text-overflow: break;
cursor: pointer;
@@ -75,7 +71,6 @@
.linkMenu-description {
text-decoration: none;
font-style: italic;
- color: rgb(95, 97, 102);
font-size: 9px;
line-height: 0.9;
margin-left: 20px;
@@ -97,17 +92,7 @@
width: 100%;
}
- .link-metadata {
- padding: 0 10px 0 16px;
- margin-bottom: 4px;
- color: $medium-gray;
- font-style: italic;
- font-size: 10.5px;
- }
-
&:hover {
- background-color: rgb(201, 239, 252);
-
.linkMenu-item-buttons {
opacity: 1;
}
@@ -115,7 +100,6 @@
.linkMenu-item-content {
.linkMenu-destination-title {
text-decoration: underline;
- color: rgb(60, 90, 156);
//display: inline;
text-overflow: break;
}
@@ -129,7 +113,7 @@
display: flex;
opacity: 0;
- .button {
+ .linkMenu-deleteButton {
width: 20px;
height: 20px;
margin: 0;
@@ -137,7 +121,7 @@
padding-right: 6px;
border-radius: 50%;
pointer-events: auto;
- background-color: $dark-gray;
+ background-color: red;
color: $white;
font-size: 65%;
transition: transform 0.2s;
diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index 737d675aa..bf2a4e1a9 100644
--- a/src/client/views/linking/LinkMenuItem.tsx
+++ b/src/client/views/linking/LinkMenuItem.tsx
@@ -69,10 +69,8 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
}
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,
@@ -84,14 +82,17 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
},
emptyFunction,
action(() => {
- const trail = DocCast(this.props.docView.rootDoc.presTrail);
+ const trail = DocCast(this.props.docView.rootDoc.presentationTrail);
if (trail) {
Doc.ActivePresentation = trail;
DocumentViewInternal.addDocTabFunc(trail, OpenWhere.replaceRight);
} else {
SelectionManager.SelectView(this.props.docView, false);
+ LinkManager.currentLink = this.props.linkDoc === LinkManager.currentLink ? undefined : this.props.linkDoc;
+ LinkManager.currentLinkAnchor = LinkManager.currentLink ? this.sourceAnchor : undefined;
+
if ((SettingsManager.propertiesWidth ?? 0) < 100) {
- SettingsManager.propertiesWidth = 250;
+ setTimeout(action(() => (SettingsManager.propertiesWidth = 250)));
}
}
})
@@ -130,7 +131,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
};
deleteLink = (e: React.PointerEvent): void => setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => LinkManager.Instance.deleteLink(this.props.linkDoc))));
-
+ @observable _hover = false;
render() {
const destinationIcon = Doc.toIcon(this.props.destinationDoc) as any as IconProp;
@@ -146,7 +147,15 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
: undefined;
return (
- <div className="linkMenu-item" style={{ background: LinkManager.currentLink === this.props.linkDoc ? 'lightBlue' : undefined }}>
+ <div
+ className="linkMenu-item"
+ onPointerEnter={action(e => (this._hover = true))}
+ onPointerLeave={action(e => (this._hover = false))}
+ style={{
+ fontSize: this._hover ? 'larger' : undefined,
+ fontWeight: this._hover ? 'bold' : undefined,
+ background: LinkManager.currentLink === this.props.linkDoc ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor,
+ }}>
<div className="linkMenu-item-content expand-two">
<div
ref={this._drag}
@@ -154,7 +163,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
onPointerDown={this.onLinkButtonDown}>
<div className="linkMenu-item-buttons">
<Tooltip title={<div className="dash-tooltip">Edit Link</div>}>
- <div className="button" style={{ background: LinkManager.currentLink === this.props.linkDoc ? 'black' : 'gray' }} ref={this._editRef} onPointerDown={this.onEdit} onClick={e => e.stopPropagation()}>
+ <div className="button" ref={this._editRef} onPointerDown={this.onEdit} onClick={e => e.stopPropagation()}>
<FontAwesomeIcon className="fa-icon" icon="edit" size="sm" />
</div>
</Tooltip>
@@ -188,12 +197,12 @@ 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.link_description ? null : <p className="linkMenu-description">{StrCast(this.props.linkDoc.link_description)}</p>}
+ {!this.props.linkDoc.link_description ? null : <p className="linkMenu-description">{StrCast(this.props.linkDoc.link_description).split('\n')[0].substring(0, 50)}</p>}
</div>
<div className="linkMenu-item-buttons">
<Tooltip title={<div className="dash-tooltip">Delete Link</div>}>
- <div className="button" style={{ background: 'red' }} onPointerDown={this.deleteLink} onClick={e => e.stopPropagation()}>
+ <div className="linkMenu-deleteButton" onPointerDown={this.deleteLink} onClick={e => e.stopPropagation()}>
<FontAwesomeIcon className="fa-icon" icon="trash" size="sm" />
</div>
</Tooltip>
diff --git a/src/client/views/linking/LinkPopup.tsx b/src/client/views/linking/LinkPopup.tsx
index 6895c0746..9a9a89732 100644
--- a/src/client/views/linking/LinkPopup.tsx
+++ b/src/client/views/linking/LinkPopup.tsx
@@ -3,15 +3,14 @@ import { observer } from 'mobx-react';
import { EditorView } from 'prosemirror-view';
import { Doc } from '../../../fields/Doc';
import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from '../../../Utils';
-import { DocUtils } from '../../documents/Documents';
import { Transform } from '../../util/Transform';
import { undoBatch } from '../../util/UndoManager';
+import { OpenWhere } from '../nodes/DocumentView';
import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
import { SearchBox } from '../search/SearchBox';
import { DefaultStyleProvider } from '../StyleProvider';
import './LinkPopup.scss';
import React = require('react');
-import { OpenWhere } from '../nodes/DocumentView';
interface LinkPopupProps {
linkFrom?: () => Doc | undefined;
diff --git a/src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx b/src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx
index 0ede75407..3a95e5f74 100644
--- a/src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx
+++ b/src/client/views/newlightbox/ButtonMenu/ButtonMenu.tsx
@@ -1,53 +1,49 @@
-import './ButtonMenu.scss';
+import { action } from 'mobx';
import * as React from 'react';
-import { IButtonMenu } from "./utils";
-import { NewLightboxView } from '../NewLightboxView';
-import { SelectionManager } from '../../../util/SelectionManager';
-import { CollectionDockingView } from '../../collections/CollectionDockingView';
-import { OpenWhereMod } from '../../nodes/DocumentView';
import { Doc } from '../../../../fields/Doc';
import { InkTool } from '../../../../fields/InkField';
-import { MainView } from '../../MainView';
-import { action } from 'mobx';
+import { SelectionManager } from '../../../util/SelectionManager';
+import { CollectionDockingView } from '../../collections/CollectionDockingView';
+import { DocumentView, OpenWhereMod } from '../../nodes/DocumentView';
+import { NewLightboxView } from '../NewLightboxView';
+import './ButtonMenu.scss';
+import { IButtonMenu } from './utils';
export const ButtonMenu = (props: IButtonMenu) => {
-
- return <div className={`newLightboxButtonMenu-container`}>
- <div
- className="newLightboxView-navBtn"
- title="toggle fit width"
- onClick={e => {
- e.stopPropagation();
- NewLightboxView.NewLightboxDoc!._fitWidth = !NewLightboxView.NewLightboxDoc!._fitWidth;
- }}>
- </div>
- <div
- className="newLightboxView-tabBtn"
- title="open in tab"
- onClick={e => {
- e.stopPropagation();
- CollectionDockingView.AddSplit(NewLightboxView.NewLightboxDoc || NewLightboxView.NewLightboxDoc!, OpenWhereMod.none);
- SelectionManager.DeselectAll();
- NewLightboxView.SetNewLightboxDoc(undefined);
- }}>
- </div>
- <div
- className="newLightboxView-penBtn"
- title="toggle pen annotation"
- style={{ background: Doc.ActiveTool === InkTool.Pen ? 'white' : undefined }}
- onClick={e => {
- e.stopPropagation();
- Doc.ActiveTool = Doc.ActiveTool === InkTool.Pen ? InkTool.None : InkTool.Pen;
- }}>
- </div>
- <div
- className="newLightboxView-exploreBtn"
- title="toggle explore mode to navigate among documents only"
- style={{ background: MainView.Instance._exploreMode ? 'white' : undefined }}
- onClick={action(e => {
- e.stopPropagation();
- MainView.Instance._exploreMode = !MainView.Instance._exploreMode;
- })}>
- </div>
- </div>
-} \ No newline at end of file
+ return (
+ <div className={`newLightboxButtonMenu-container`}>
+ <div
+ className="newLightboxView-navBtn"
+ title="toggle fit width"
+ onClick={e => {
+ e.stopPropagation();
+ NewLightboxView.LightboxDoc!._fitWidth = !NewLightboxView.LightboxDoc!._fitWidth;
+ }}></div>
+ <div
+ className="newLightboxView-tabBtn"
+ title="open in tab"
+ onClick={e => {
+ e.stopPropagation();
+ CollectionDockingView.AddSplit(NewLightboxView.LightboxDoc || NewLightboxView.LightboxDoc!, OpenWhereMod.none);
+ SelectionManager.DeselectAll();
+ NewLightboxView.SetNewLightboxDoc(undefined);
+ }}></div>
+ <div
+ className="newLightboxView-penBtn"
+ title="toggle pen annotation"
+ style={{ background: Doc.ActiveTool === InkTool.Pen ? 'white' : undefined }}
+ onClick={e => {
+ e.stopPropagation();
+ Doc.ActiveTool = Doc.ActiveTool === InkTool.Pen ? InkTool.None : InkTool.Pen;
+ }}></div>
+ <div
+ className="newLightboxView-exploreBtn"
+ title="toggle explore mode to navigate among documents only"
+ style={{ background: DocumentView.ExploreMode ? 'white' : undefined }}
+ onClick={action(e => {
+ e.stopPropagation();
+ DocumentView.ExploreMode = !DocumentView.ExploreMode;
+ })}></div>
+ </div>
+ );
+};
diff --git a/src/client/views/newlightbox/ExploreView/ExploreView.tsx b/src/client/views/newlightbox/ExploreView/ExploreView.tsx
index 855bfd9e2..a1d6375c4 100644
--- a/src/client/views/newlightbox/ExploreView/ExploreView.tsx
+++ b/src/client/views/newlightbox/ExploreView/ExploreView.tsx
@@ -1,30 +1,32 @@
import './ExploreView.scss';
-import { IBounds, IExploreView, emptyBounds } from "./utils";
-import { IRecommendation } from "../components";
+import { IBounds, IExploreView, emptyBounds } from './utils';
+import { IRecommendation } from '../components';
import * as React from 'react';
import { NewLightboxView } from '../NewLightboxView';
import { StrCast } from '../../../../fields/Types';
-
-
export const ExploreView = (props: IExploreView) => {
- const { recs, bounds=emptyBounds } = props
-
- return <div className={`exploreView-container`}>
- {recs && recs.map((rec) => {
- console.log(rec.embedding, bounds)
- const x_bound: number = Math.max(Math.abs(bounds.max_x), Math.abs(bounds.min_x))
- const y_bound: number = Math.max(Math.abs(bounds.max_y), Math.abs(bounds.min_y))
- console.log(x_bound, y_bound)
- if (rec.embedding) {
- const x = (rec.embedding.x / x_bound) * 50;
- const y = (rec.embedding.y / y_bound) * 50;
- console.log(x, y)
- return <div className={`exploreView-doc`} onClick={() => {}} style={{top: `calc(50% + ${y}%)`, left: `calc(50% + ${x}%)`}}>
- {rec.title}
- </div>
- } else return (null)
- })}
- <div className={`exploreView-doc`} style={{top: `calc(50% + ${0}%)`, left: `calc(50% + ${0}%)`, background: '#073763', color: 'white'}}>{StrCast(NewLightboxView.NewLightboxDoc?.title)}</div>
- </div>
-} \ No newline at end of file
+ const { recs, bounds = emptyBounds } = props;
+
+ return (
+ <div className={`exploreView-container`}>
+ {recs &&
+ recs.map(rec => {
+ const x_bound: number = Math.max(Math.abs(bounds.max_x), Math.abs(bounds.min_x));
+ const y_bound: number = Math.max(Math.abs(bounds.max_y), Math.abs(bounds.min_y));
+ if (rec.embedding) {
+ const x = (rec.embedding.x / x_bound) * 50;
+ const y = (rec.embedding.y / y_bound) * 50;
+ return (
+ <div className={`exploreView-doc`} onClick={() => {}} style={{ top: `calc(50% + ${y}%)`, left: `calc(50% + ${x}%)` }}>
+ {rec.title}
+ </div>
+ );
+ } else return null;
+ })}
+ <div className={`exploreView-doc`} style={{ top: `calc(50% + ${0}%)`, left: `calc(50% + ${0}%)`, background: '#073763', color: 'white' }}>
+ {StrCast(NewLightboxView.LightboxDoc?.title)}
+ </div>
+ </div>
+ );
+};
diff --git a/src/client/views/newlightbox/NewLightboxView.tsx b/src/client/views/newlightbox/NewLightboxView.tsx
index 3acbd1a32..ca90f6a0f 100644
--- a/src/client/views/newlightbox/NewLightboxView.tsx
+++ b/src/client/views/newlightbox/NewLightboxView.tsx
@@ -2,35 +2,32 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnTrue } from '../../../Utils';
-import { Doc, DocListCast, Opt, StrListCast } from '../../../fields/Doc';
+import { Doc, DocListCast, Opt } from '../../../fields/Doc';
import { InkTool } from '../../../fields/InkField';
import { Cast, NumCast, StrCast } from '../../../fields/Types';
+import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnTrue } from '../../../Utils';
import { DocUtils } from '../../documents/Documents';
import { DocumentManager } from '../../util/DocumentManager';
import { LinkManager } from '../../util/LinkManager';
import { SelectionManager } from '../../util/SelectionManager';
import { Transform } from '../../util/Transform';
-import { GestureOverlay } from '../GestureOverlay';
-import { MainView } from '../MainView';
-import { DefaultStyleProvider } from '../StyleProvider';
import { CollectionStackedTimeline } from '../collections/CollectionStackedTimeline';
import { TabDocView } from '../collections/TabDocView';
+import { GestureOverlay } from '../GestureOverlay';
+import { LightboxView } from '../LightboxView';
import { DocumentView, OpenWhere } from '../nodes/DocumentView';
+import { DefaultStyleProvider } from '../StyleProvider';
+import { IRecommendation } from './components';
import { ExploreView } from './ExploreView';
-import { IBounds, emptyBounds } from './ExploreView/utils';
+import { emptyBounds, IBounds } from './ExploreView/utils';
import { NewLightboxHeader } from './Header';
import './NewLightboxView.scss';
import { RecommendationList } from './RecommendationList';
-import { IRecommendation } from './components';
-import { fetchKeywords, fetchRecommendations } from './utils';
-import { List } from '../../../fields/List';
-import { LightboxView } from '../LightboxView';
enum LightboxStatus {
- RECOMMENDATIONS = "recommendations",
- ANNOTATIONS = "annotations",
- NONE = "none"
+ RECOMMENDATIONS = 'recommendations',
+ ANNOTATIONS = 'annotations',
+ NONE = 'none',
}
interface LightboxViewProps {
@@ -63,30 +60,30 @@ export class NewLightboxView extends React.Component<LightboxViewProps> {
@observable private static _docView: Opt<DocumentView>;
// keywords
- @observable private static _keywords: string[] = []
+ @observable private static _keywords: string[] = [];
@action public static SetKeywords(kw: string[]) {
- this._keywords = kw
+ this._keywords = kw;
}
@computed public static get Keywords() {
- return this._keywords
+ return this._keywords;
}
// query
- @observable private static _query: string = ''
+ @observable private static _query: string = '';
@action public static SetQuery(query: string) {
- this._query = query
+ this._query = query;
}
@computed public static get Query() {
- return this._query
+ return this._query;
}
// keywords
- @observable private static _recs: IRecommendation[] = []
+ @observable private static _recs: IRecommendation[] = [];
@action public static SetRecs(recs: IRecommendation[]) {
- this._recs = recs
+ this._recs = recs;
}
@computed public static get Recs() {
- return this._recs
+ return this._recs;
}
// bounds
@@ -108,7 +105,7 @@ export class NewLightboxView extends React.Component<LightboxViewProps> {
}
// newLightbox sidebar status
- @observable private static _sidebarStatus: Opt<string> = "";
+ @observable private static _sidebarStatus: Opt<string> = '';
@action public static SetSidebarStatus(sidebarStatus: Opt<string>) {
this._sidebarStatus = sidebarStatus;
}
@@ -129,7 +126,7 @@ export class NewLightboxView extends React.Component<LightboxViewProps> {
this._docFilters && (this._docFilters.length = 0);
this._future = this._history = [];
Doc.ActiveTool = InkTool.None;
- MainView.Instance._exploreMode = false;
+ DocumentView.ExploreMode = false;
} else {
const l = DocUtils.MakeLinkToActiveAudio(() => doc).lastElement();
l && (Cast(l.link_anchor_2, Doc, null).backgroundColor = 'lightgreen');
@@ -289,35 +286,38 @@ export class NewLightboxView extends React.Component<LightboxViewProps> {
@computed
get documentView() {
- if (!LightboxView.LightboxDoc) return null
- else return (<GestureOverlay isActive={true}>
- <DocumentView
- ref={action((r: DocumentView | null) => (NewLightboxView._docView = r !== null ? r : undefined))}
- Document={LightboxView.LightboxDoc}
- DataDoc={undefined}
- PanelWidth={this.newLightboxWidth}
- PanelHeight={this.newLightboxHeight}
- LayoutTemplate={NewLightboxView.LightboxDocTemplate}
- isDocumentActive={returnTrue} // without this being true, sidebar annotations need to be activated before text can be selected.
- isContentActive={returnTrue}
- styleProvider={DefaultStyleProvider}
- ScreenToLocalTransform={this.newLightboxScreenToLocal}
- renderDepth={0}
- rootSelected={returnTrue}
- docViewPath={returnEmptyDoclist}
- childFilters={this.docFilters}
- childFiltersByRanges={returnEmptyFilter}
- searchFilterDocs={returnEmptyDoclist}
- addDocument={undefined}
- removeDocument={undefined}
- whenChildContentsActiveChanged={emptyFunction}
- addDocTab={this.addDocTab}
- pinToPres={TabDocView.PinDoc}
- bringToFront={emptyFunction}
- onBrowseClick={MainView.Instance.exploreMode}
- focus={emptyFunction}
- />
- </GestureOverlay>)
+ if (!LightboxView.LightboxDoc) return null;
+ else
+ return (
+ <GestureOverlay isActive={true}>
+ <DocumentView
+ ref={action((r: DocumentView | null) => (NewLightboxView._docView = r !== null ? r : undefined))}
+ Document={LightboxView.LightboxDoc}
+ DataDoc={undefined}
+ PanelWidth={this.newLightboxWidth}
+ PanelHeight={this.newLightboxHeight}
+ LayoutTemplate={NewLightboxView.LightboxDocTemplate}
+ isDocumentActive={returnTrue} // without this being true, sidebar annotations need to be activated before text can be selected.
+ isContentActive={returnTrue}
+ styleProvider={DefaultStyleProvider}
+ ScreenToLocalTransform={this.newLightboxScreenToLocal}
+ renderDepth={0}
+ rootSelected={returnTrue}
+ docViewPath={returnEmptyDoclist}
+ childFilters={this.docFilters}
+ childFiltersByRanges={returnEmptyFilter}
+ searchFilterDocs={returnEmptyDoclist}
+ addDocument={undefined}
+ removeDocument={undefined}
+ whenChildContentsActiveChanged={emptyFunction}
+ addDocTab={this.addDocTab}
+ pinToPres={TabDocView.PinDoc}
+ bringToFront={emptyFunction}
+ onBrowseClick={DocumentView.exploreMode}
+ focus={emptyFunction}
+ />
+ </GestureOverlay>
+ );
}
future = () => NewLightboxView._future;
@@ -337,29 +337,28 @@ export class NewLightboxView extends React.Component<LightboxViewProps> {
NewLightboxView.SetNewLightboxDoc(undefined);
}
}}>
- <div className={`app-document`} style={{gridTemplateColumns: `calc(100% - 400px) 400px`}}>
- <div
- className="newLightboxView-contents"
- style={{
- top: 20,
- left: 20,
- width: this.newLightboxWidth(),
- height: this.newLightboxHeight() - 40,
- }}>
+ <div className={`app-document`} style={{ gridTemplateColumns: `calc(100% - 400px) 400px` }}>
+ <div
+ className="newLightboxView-contents"
+ style={{
+ top: 20,
+ left: 20,
+ width: this.newLightboxWidth(),
+ height: this.newLightboxHeight() - 40,
+ }}>
<NewLightboxHeader height={newLightboxHeaderHeight} width={this.newLightboxWidth()} />
- {!NewLightboxView._explore ?
- <div className="newLightboxView-doc" style={{height: this.newLightboxHeight()}}>
+ {!NewLightboxView._explore ? (
+ <div className="newLightboxView-doc" style={{ height: this.newLightboxHeight() }}>
{this.documentView}
</div>
- :
+ ) : (
<div className={`explore`}>
- <ExploreView recs={NewLightboxView.Recs} bounds={NewLightboxView.Bounds}/>
+ <ExploreView recs={NewLightboxView.Recs} bounds={NewLightboxView.Bounds} />
</div>
- }
- </div>
- <RecommendationList keywords={NewLightboxView.Keywords}/>
+ )}
+ </div>
+ <RecommendationList keywords={NewLightboxView.Keywords} />
</div>
-
</div>
);
}
diff --git a/src/client/views/newlightbox/components/Recommendation/Recommendation.tsx b/src/client/views/newlightbox/components/Recommendation/Recommendation.tsx
index c0d357ad5..96846673b 100644
--- a/src/client/views/newlightbox/components/Recommendation/Recommendation.tsx
+++ b/src/client/views/newlightbox/components/Recommendation/Recommendation.tsx
@@ -1,5 +1,5 @@
import * as React from 'react';
-import { IRecommendation } from "./utils";
+import { IRecommendation } from './utils';
import './Recommendation.scss';
import { getType } from '../../utils';
import { FaEyeSlash } from 'react-icons/fa';
@@ -9,82 +9,94 @@ import { Doc } from '../../../../../fields/Doc';
import { Docs } from '../../../../documents/Documents';
export const Recommendation = (props: IRecommendation) => {
- const {title, data, type, text, transcript, loading, source, previewUrl, related_concepts, distance, docId} = props
-
- return <div className={`recommendation-container ${loading && 'loading'} ${previewUrl && 'previewUrl'}`} onClick={() => {
- let doc: Doc | null = null;
- if (source == "Dash" && docId) {
- const docView = DocumentManager.Instance.getDocumentViewById(docId)
- if (docView) {
- doc = docView.rootDoc;
- }
- } else if (data) {
- console.log(data, type)
- switch(type) {
- case "YouTube":
- console.log('create ', type, 'document')
- doc = Docs.Create.VideoDocument(data, { title: title, _width: 400, _height: 315, transcript: transcript })
- break;
- case "Video":
- console.log('create ', type, 'document')
- doc = Docs.Create.VideoDocument(data, { title: title, _width: 400, _height: 315, transcript: transcript })
- break;
- case "Webpage":
- console.log('create ', type, 'document')
- doc = Docs.Create.WebDocument(data, { title: title, text: text })
- break;
- case "HTML":
- console.log('create ', type, 'document')
- doc = Docs.Create.WebDocument(data, { title: title, text: text })
- break;
- case "Text":
- console.log('create ', type, 'document')
- doc = Docs.Create.TextDocument(data, { title: title, text: text })
- break;
- case "PDF":
- console.log('create ', type, 'document')
- doc = Docs.Create.PdfDocument(data, { title: title, text: text })
- break;
- }
- }
- if (doc !== null) NewLightboxView.SetNewLightboxDoc(doc)
- }}>
- {loading ?
- <div className={`image-container`}>
- </div>
- :
- previewUrl ? <div className={`image-container`}>
- {<img className={`image`} src={previewUrl}></img>}
- </div>
- : null
- }
- <div className={`title`}>{title}</div>
- <div className={`info`}>
- {!loading && <div className={`type-container`}>
- <div className={`lb-label`}>Type</div><div className={`lb-type`}>{getType(type!)}</div>
- </div>}
- {!loading && <div className={`distance-container`}>
- <div className={`lb-label`}>Distance</div><div className={`lb-distance`}>{distance}</div>
- </div>}
- </div>
- <div className={`source`}>
- {!loading && <div className={`source-container`}>
- <div className={`lb-label`}>Source</div><div className={`lb-source`}>{source}</div>
- </div>}
- </div>
- <div className={`explainer`}>
- {!loading &&
- <div>
- You are seeing this recommendation because this document also explores
- <div className={`concepts-container`}>
- {related_concepts?.map((val) => {
- return <div className={'concept'}>{val}</div>
- })}
- </div>
- </div>}
- </div>
- <div className={`hide-rec`}>
- {!loading && <><div>Hide Recommendation</div><div style={{fontSize: 15, paddingRight: 5}}><FaEyeSlash/></div></>}
+ const { title, data, type, text, transcript, loading, source, previewUrl, related_concepts, distance, docId } = props;
+
+ return (
+ <div
+ className={`recommendation-container ${loading && 'loading'} ${previewUrl && 'previewUrl'}`}
+ onClick={() => {
+ let doc: Doc | null = null;
+ if (source == 'Dash' && docId) {
+ const docView = DocumentManager.Instance.getDocumentViewsById(docId).lastElement();
+ if (docView) {
+ doc = docView.rootDoc;
+ }
+ } else if (data) {
+ switch (type) {
+ case 'YouTube':
+ console.log('create ', type, 'document');
+ doc = Docs.Create.VideoDocument(data, { title: title, _width: 400, _height: 315, transcript });
+ break;
+ case 'Video':
+ console.log('create ', type, 'document');
+ doc = Docs.Create.VideoDocument(data, { title: title, _width: 400, _height: 315, transcript });
+ break;
+ case 'Webpage':
+ console.log('create ', type, 'document');
+ doc = Docs.Create.WebDocument(data, { title: title, text: text });
+ break;
+ case 'HTML':
+ console.log('create ', type, 'document');
+ doc = Docs.Create.WebDocument(data, { title: title, text: text });
+ break;
+ case 'Text':
+ console.log('create ', type, 'document');
+ doc = Docs.Create.TextDocument(data, { title: title, text: text });
+ break;
+ case 'PDF':
+ console.log('create ', type, 'document');
+ doc = Docs.Create.PdfDocument(data, { title: title, text: text });
+ break;
+ }
+ }
+ if (doc !== null) NewLightboxView.SetNewLightboxDoc(doc);
+ }}>
+ {loading ? <div className={`image-container`}></div> : previewUrl ? <div className={`image-container`}>{<img className={`image`} src={previewUrl}></img>}</div> : null}
+ <div className={`title`}>{title}</div>
+ <div className={`info`}>
+ {!loading && (
+ <div className={`type-container`}>
+ <div className={`lb-label`}>Type</div>
+ <div className={`lb-type`}>{getType(type!)}</div>
+ </div>
+ )}
+ {!loading && (
+ <div className={`distance-container`}>
+ <div className={`lb-label`}>Distance</div>
+ <div className={`lb-distance`}>{distance}</div>
+ </div>
+ )}
+ </div>
+ <div className={`source`}>
+ {!loading && (
+ <div className={`source-container`}>
+ <div className={`lb-label`}>Source</div>
+ <div className={`lb-source`}>{source}</div>
+ </div>
+ )}
+ </div>
+ <div className={`explainer`}>
+ {!loading && (
+ <div>
+ You are seeing this recommendation because this document also explores
+ <div className={`concepts-container`}>
+ {related_concepts?.map(val => {
+ return <div className={'concept'}>{val}</div>;
+ })}
+ </div>
+ </div>
+ )}
+ </div>
+ <div className={`hide-rec`}>
+ {!loading && (
+ <>
+ <div>Hide Recommendation</div>
+ <div style={{ fontSize: 15, paddingRight: 5 }}>
+ <FaEyeSlash />
+ </div>
+ </>
+ )}
+ </div>
</div>
- </div>
-} \ No newline at end of file
+ );
+};
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 6558d215a..8d80f1364 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -1,14 +1,15 @@
import React = require('react');
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Tooltip } from '@material-ui/core';
import { action, computed, IReactionDisposer, observable, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import { DateField } from '../../../fields/DateField';
-import { Doc, DocListCast } from '../../../fields/Doc';
+import { Doc } from '../../../fields/Doc';
import { ComputedField } from '../../../fields/ScriptField';
import { Cast, DateCast, NumCast } from '../../../fields/Types';
import { AudioField, nullAudio } from '../../../fields/URLField';
import { emptyFunction, formatTime, returnFalse, setupMoveUpEvents } from '../../../Utils';
-import { DocUtils } from '../../documents/Documents';
+import { Docs, DocUtils } from '../../documents/Documents';
import { Networking } from '../../Network';
import { DragManager } from '../../util/DragManager';
import { LinkManager } from '../../util/LinkManager';
@@ -18,6 +19,7 @@ import { ContextMenu } from '../ContextMenu';
import { ContextMenuProps } from '../ContextMenuItem';
import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent';
import './AudioBox.scss';
+import { DocFocusOptions } from './DocumentView';
import { FieldView, FieldViewProps } from './FieldView';
import { PinProps, PresBox } from './trails';
@@ -66,14 +68,14 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
_stream: MediaStream | undefined; // passed to MediaRecorder, records device input audio
_play: any = null; // timeout for playback
- @observable _stackedTimeline: any; // CollectionStackedTimeline ref
+ @observable _stackedTimeline: CollectionStackedTimeline | null | undefined; // CollectionStackedTimeline ref
@observable _finished: boolean = false; // has playback reached end of clip
@observable _volume: number = 1;
@observable _muted: boolean = false;
@observable _paused: boolean = false; // is recording paused
// @observable rawDuration: number = 0; // computed from the length of the audio element when loaded
@computed get recordingStart() {
- return DateCast(this.dataDoc[this.fieldKey + '-recordingStart'])?.date.getTime();
+ return DateCast(this.dataDoc[this.fieldKey + '_recordingStart'])?.date.getTime();
}
@computed get rawDuration() {
return NumCast(this.dataDoc[`${this.fieldKey}_duration`]);
@@ -134,16 +136,19 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
}
getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
- const anchor =
- CollectionStackedTimeline.createAnchor(
- this.rootDoc,
- this.dataDoc,
- this.annotationKey,
- 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;
+ const timecode = Cast(this.layoutDoc._layout_currentTimecode, 'number', null);
+ const anchor = addAsAnnotation
+ ? CollectionStackedTimeline.createAnchor(
+ this.rootDoc,
+ this.dataDoc,
+ this.annotationKey,
+ 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
+ : Docs.Create.ConfigDocument({ title: '#' + timecode, _timecodeToShow: timecode, annotationOn: this.rootDoc });
+
PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), temporal: true } }, this.rootDoc);
return anchor;
};
@@ -230,10 +235,10 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
recordAudioAnnotation = async () => {
this._stream = await navigator.mediaDevices.getUserMedia({ audio: true });
this._recorder = new MediaRecorder(this._stream);
- this.dataDoc[this.fieldKey + '-recordingStart'] = new DateField();
+ this.dataDoc[this.fieldKey + '_recordingStart'] = new DateField();
DocUtils.ActiveRecordings.push(this);
this._recorder.ondataavailable = async (e: any) => {
- const [{ result }] = await Networking.UploadFilesToServer({file: 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);
}
@@ -359,9 +364,10 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
returnFalse,
action(() => {
const newDoc = DocUtils.GetNewTextDoc('', NumCast(this.rootDoc.x), NumCast(this.rootDoc.y) + NumCast(this.layoutDoc._height) + 10, NumCast(this.layoutDoc._width), 2 * NumCast(this.layoutDoc._height));
- 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');
+ const textField = Doc.LayoutFieldKey(newDoc);
+ Doc.GetProto(newDoc)[`${textField}_recordingSource`] = this.dataDoc;
+ Doc.GetProto(newDoc)[`${textField}_recordingStart`] = ComputedField.MakeFunction(`self.${textField}_recordingSource.${this.fieldKey}_recordingStart`);
+ Doc.GetProto(newDoc).mediaState = ComputedField.MakeFunction(`self.${textField}_recordingSource.mediaState`);
if (Doc.IsInMyOverlay(this.rootDoc)) {
newDoc.overlayX = this.rootDoc.x;
newDoc.overlayY = NumCast(this.rootDoc.y) + NumCast(this.rootDoc._height);
@@ -417,7 +423,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
};
// plays link
- playLink = (link: Doc) => {
+ playLink = (link: Doc, options: DocFocusOptions) => {
if (link.annotationOn === this.rootDoc) {
if (!this.layoutDoc.dontAutoPlayFollowedLinks) {
this.playFrom(this.timeline?.anchorStart(link) || 0, this.timeline?.anchorEnd(link));
@@ -472,8 +478,34 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
};
// for trim button, double click displays full clip, single displays curr trim bounds
+ onResetPointerDown = (e: React.PointerEvent) => {
+ e.stopPropagation();
+ this.timeline &&
+ setupMoveUpEvents(
+ this,
+ e,
+ returnFalse,
+ returnFalse,
+ action(e => {
+ if (this.timeline?.IsTrimming !== TrimScope.None) {
+ this.timeline?.CancelTrimming();
+ } else {
+ this.beginEndtime = this.timeline?.trimEnd;
+ this.beginStarttime = this.timeline?.trimStart;
+ this.startTrim(TrimScope.All);
+ }
+ })
+ );
+ };
+
+ beginEndtime: number | undefined;
+ beginStarttime: number | undefined;
+
+ // for trim button, double click displays full clip, single displays curr trim bounds
onClipPointerDown = (e: React.PointerEvent) => {
e.stopPropagation();
+ this.beginEndtime = this.timeline?.trimEnd;
+ this.beginStarttime = this.timeline?.trimStart;
this.timeline &&
setupMoveUpEvents(
this,
@@ -524,7 +556,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
r,
(e, de) => {
const [xp, yp] = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y);
- de.complete.docDragData && this.timeline.internalDocDrop(e, de, de.complete.docDragData, xp);
+ de.complete.docDragData && this.timeline?.internalDocDrop(e, de, de.complete.docDragData, xp);
},
this.layoutDoc,
undefined
@@ -586,9 +618,22 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
</div>
{!this.miniPlayer && (
- <div className="audiobox-button" title={this.timeline?.IsTrimming !== TrimScope.None ? 'finish' : 'trim'} onPointerDown={this.onClipPointerDown}>
- <FontAwesomeIcon icon={this.timeline?.IsTrimming !== TrimScope.None ? 'check' : 'cut'} size={'1x'} />
- </div>
+ <>
+ <Tooltip title={<>trim audio</>}>
+ <div className="audiobox-button" onPointerDown={this.onClipPointerDown}>
+ <FontAwesomeIcon icon={this.timeline?.IsTrimming !== TrimScope.None ? 'check' : 'cut'} size={'1x'} />
+ </div>
+ </Tooltip>
+ {this.timeline?.IsTrimming == TrimScope.None && !NumCast(this.layoutDoc.clipStart) && NumCast(this.layoutDoc.clipEnd) === this.rawDuration ? (
+ <></>
+ ) : (
+ <Tooltip title={<>{this.timeline?.IsTrimming !== TrimScope.None ? 'Cancel trimming' : 'Edit original timeline'}</>}>
+ <div className="audiobox-button" onPointerDown={this.onResetPointerDown}>
+ <FontAwesomeIcon icon={this.timeline?.IsTrimming !== TrimScope.None ? 'cancel' : 'arrows-left-right'} size={'1x'} />
+ </div>
+ </Tooltip>
+ )}
+ </>
)}
</div>
<div className="controls-right">
@@ -653,12 +698,12 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
@computed get renderTimeline() {
return (
<CollectionStackedTimeline
- ref={action((r: any) => (this._stackedTimeline = r))}
+ ref={action((r: CollectionStackedTimeline | null) => (this._stackedTimeline = r))}
{...this.props}
CollectionFreeFormDocumentView={undefined}
dataFieldKey={this.fieldKey}
fieldKey={this.annotationKey}
- dictationKey={this.fieldKey + '-dictation'}
+ dictationKey={this.fieldKey + '_dictation'}
mediaPath={this.path}
renderDepth={this.props.renderDepth + 1}
startTag={'_timecodeToShow' /* audioStart */}
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 6710cee63..4a438f826 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -40,7 +40,6 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
{ key: 'opacity', val: 1 },
{ 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
diff --git a/src/client/views/nodes/ColorBox.tsx b/src/client/views/nodes/ColorBox.tsx
index aae759702..1b6fe5748 100644
--- a/src/client/views/nodes/ColorBox.tsx
+++ b/src/client/views/nodes/ColorBox.tsx
@@ -14,6 +14,8 @@ import { ActiveInkColor, ActiveInkWidth, SetActiveInkColor, SetActiveInkWidth }
import './ColorBox.scss';
import { FieldView, FieldViewProps } from './FieldView';
import { RichTextMenu } from './formattedText/RichTextMenu';
+import { ScriptingGlobals } from '../../util/ScriptingGlobals';
+import { DashColor } from '../../../Utils';
@observer
export class ColorBox extends ViewBoxBaseComponent<FieldViewProps>() {
@@ -81,3 +83,10 @@ export class ColorBox extends ViewBoxBaseComponent<FieldViewProps>() {
);
}
}
+
+
+ScriptingGlobals.add(
+ function interpColors(c1:string, c2:string, weight=0.5) {
+ return DashColor(c1).mix(DashColor(c2),weight)
+ }
+) \ No newline at end of file
diff --git a/src/client/views/nodes/ComparisonBox.scss b/src/client/views/nodes/ComparisonBox.scss
index a12f1c12b..39c864b2b 100644
--- a/src/client/views/nodes/ComparisonBox.scss
+++ b/src/client/views/nodes/ComparisonBox.scss
@@ -60,6 +60,8 @@
opacity: 0.8;
pointer-events: all;
cursor: pointer;
+ width: 15px;
+ height: 15px;
}
.clear-button.before {
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index ca5ec9389..b09fcd882 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -96,7 +96,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
const anchor = Docs.Create.ConfigDocument({
title: 'CompareAnchor:' + this.rootDoc.title,
// set presentation timing properties for restoring view
- presTransition: 1000,
+ presentation_transition: 1000,
annotationOn: this.rootDoc,
});
if (anchor) {
@@ -109,12 +109,11 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
};
@undoBatch
- clearDoc = (e: React.MouseEvent, fieldKey: string) => {
- e.stopPropagation; // prevent click event action (slider movement) in registerSliding
- delete this.dataDoc[fieldKey];
- };
+ clearDoc = (fieldKey: string) => 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) => {
+ if (this.dataDoc[which]) return false;
this.dataDoc[which] = doc;
return true;
};
@@ -128,6 +127,24 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
whenChildContentsActiveChanged = action((isActive: boolean) => (this._isAnyChildContentActive = isActive));
+ closeDown = (e: React.PointerEvent, which: string) => {
+ setupMoveUpEvents(
+ this,
+ e,
+ e => {
+ const de = new DragManager.DocumentDragData([DocCast(this.dataDoc[which])], 'move');
+ de.moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean): boolean => {
+ this.clearDoc(which);
+ return addDocument(doc);
+ };
+ de.canEmbed = true;
+ DragManager.StartDocumentDrag([this._closeRef.current!], de, e.clientX, e.clientY);
+ return true;
+ },
+ emptyFunction,
+ e => this.clearDoc(which)
+ );
+ };
docStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string): any => {
if (property === StyleProp.PointerEvents) return 'none';
return this.props.styleProvider?.(doc, props, property);
@@ -136,13 +153,15 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
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);
+ _closeRef = React.createRef<HTMLDivElement>();
render() {
const clearButton = (which: string) => {
return (
<div
+ ref={this._closeRef}
className={`clear-button ${which}`}
- onPointerDown={e => e.stopPropagation()} // prevent triggering slider movement in registerSliding
- onClick={e => this.clearDoc(e, which)}>
+ onPointerDown={e => this.closeDown(e, which)} // prevent triggering slider movement in registerSliding
+ >
<FontAwesomeIcon className={`clear-button ${which}`} icon="times" size="sm" />
</div>
);
diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.scss b/src/client/views/nodes/DataVizBox/DataVizBox.scss
index cd500e9ae..385ef5a1b 100644
--- a/src/client/views/nodes/DataVizBox/DataVizBox.scss
+++ b/src/client/views/nodes/DataVizBox/DataVizBox.scss
@@ -1,4 +1,17 @@
.dataviz {
- overflow: auto;
+ overflow: scroll;
height: 100%;
+ width: 100%;
+
+ .datatype-button{
+ display: flex;
+ flex-direction: row;
+ }
+ .button-container {
+ pointer-events: unset;
+ }
+}
+.start-message {
+ margin: 10px;
+ align-self: baseline;
}
diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
index d548ab9f1..8f32e2ba4 100644
--- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx
+++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx
@@ -1,137 +1,182 @@
-import { action, computed, observable, ObservableMap, ObservableSet } from 'mobx';
+import { Toggle, ToggleType, Type } from 'browndash-components';
+import { action, computed, ObservableMap } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { Doc, StrListCast } from '../../../../fields/Doc';
+import { Doc, Field, StrListCast } from '../../../../fields/Doc';
import { List } from '../../../../fields/List';
-import { Cast, CsvCast, NumCast, StrCast } from '../../../../fields/Types';
+import { Cast, CsvCast, 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 { Histogram } from './components/Histogram';
import { LineChart } from './components/LineChart';
+import { PieChart } from './components/PieChart';
import { TableBox } from './components/TableBox';
import './DataVizBox.scss';
export enum DataVizView {
TABLE = 'table',
LINECHART = 'lineChart',
+ HISTOGRAM = 'histogram',
+ PIECHART = 'pieChart',
}
@observer
export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
- public static LayoutString(fieldKey: string) {
- return FieldView.LayoutString(DataVizBox, fieldKey);
+ public static LayoutString(fieldStr: string) {
+ return FieldView.LayoutString(DataVizBox, fieldStr);
}
- // 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);
- // }
- // }
+ // all datasets that have been retrieved from the server stored as a map from the dataset url to an array of records
+ static dataset = new ObservableMap<string, { [key: string]: string }[]>();
+ private _vizRenderer: LineChart | Histogram | PieChart | undefined;
- // 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
+ // all CSV records in the dataset (that aren't an empty row)
+ @computed.struct get records() {
+ var records = DataVizBox.dataset.get(CsvCast(this.rootDoc[this.fieldKey]).url.href);
+ return records?.filter(record => Object.keys(record).some(key => record[key]));
+ }
+ // currently chosen visualization type: line, pie, histogram, table
@computed get dataVizView(): DataVizView {
- return StrCast(this.layoutDoc._dataVizView, 'table') as DataVizView;
+ return StrCast(this.layoutDoc._dataViz, 'table') as DataVizView;
+ }
+
+ @computed get dataUrl() {
+ return Cast(this.dataDoc[this.fieldKey], CsvField);
+ }
+ @computed.struct get axes() {
+ return StrListCast(this.layoutDoc.dataViz_axes);
}
- @action
+ selectAxes = (axes: string[]) => (this.layoutDoc.dataViz_axes = new List<string>(axes));
+
+ @action // pinned / linked anchor doc includes selected rows, graph titles, and graph colors
restoreView = (data: Doc) => {
- const changedView = this.dataVizView !== data.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);
+ const changedView = this.dataVizView !== data.config_dataViz && (this.layoutDoc._dataViz = data.config_dataViz);
+ const changedAxes = this.axes.join('') !== StrListCast(data.config_dataVizAxes).join('') && (this.layoutDoc._dataViz_axes = new List<string>(StrListCast(data.config_dataVizAxes)));
+ this.layoutDoc.dataViz_selectedRows = Field.Copy(data.dataViz_selectedRows);
+ this.layoutDoc.dataViz_histogram_barColors = Field.Copy(data.dataViz_histogram_barColors);
+ this.layoutDoc.dataViz_histogram_defaultColor = data.dataViz_histogram_defaultColor;
+ this.layoutDoc.dataViz_pie_sliceColors = Field.Copy(data.dataViz_pie_sliceColors);
+ Object.keys(this.layoutDoc).map(key => {
+ if (key.startsWith('dataViz_histogram_title') || key.startsWith('dataViz_lineChart_title') || key.startsWith('dataViz_pieChart_title')) {
+ this.layoutDoc['_' + key] = data[key];
+ }
+ });
+ const func = () => this._vizRenderer?.restoreView(data);
if (changedView || changedAxes) {
setTimeout(func, 100);
return true;
}
return func() ?? false;
};
-
getAnchor = (addAsAnnotation?: boolean, pinProps?: PinProps) => {
const anchor = !pinProps
? this.rootDoc
- : this._chartRenderer?.getAnchor(pinProps) ??
+ : this._vizRenderer?.getAnchor(pinProps) ??
Docs.Create.ConfigDocument({
// when we clear selection -> we should have it so chartBox getAnchor returns undefined
// this is for when we want the whole doc (so when the chartBox getAnchor returns without a marker)
/*put in some options*/
});
-
- anchor.presDataVizView = this.dataVizView;
- anchor.presDataVizAxes = this.axes.length ? new List<string>(this.axes) : undefined;
-
+ anchor.config_dataViz = this.dataVizView;
+ anchor.config_dataVizAxes = this.axes.length ? new List<string>(this.axes) : undefined;
+ anchor.dataViz_selectedRows = Field.Copy(this.layoutDoc.dataViz_selectedRows);
+ anchor.dataViz_histogram_barColors = Field.Copy(this.layoutDoc.dataViz_histogram_barColors);
+ anchor.dataViz_histogram_defaultColor = this.layoutDoc.dataViz_histogram_defaultColor;
+ anchor.dataViz_pie_sliceColors = Field.Copy(this.layoutDoc.dataViz_pie_sliceColors);
+ Object.keys(this.layoutDoc).map(key => {
+ if (key.startsWith('dataViz_histogram_title') || key.startsWith('dataViz_lineChart_title') || key.startsWith('dataViz_pieChart_title')) {
+ anchor[key] = this.layoutDoc[key];
+ }
+ });
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() {
- 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 dataUrl() {
- return Cast(this.dataDoc[this.fieldKey], CsvField);
- }
-
componentDidMount() {
this.props.setContentView?.(this);
- this.fetchData();
+ if (!DataVizBox.dataset.has(CsvCast(this.rootDoc[this.fieldKey]).url.href)) 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, []);
+ fetchData = () => {
+ DataVizBox.dataset.set(CsvCast(this.rootDoc[this.fieldKey]).url.href, []); // assign temporary dataset as a lock to prevent duplicate server requests
fetch('/csvData?uri=' + this.dataUrl?.url.href) //
- .then(res => res.json().then(action(res => !res.errno && DataVizBox.pairSet.set(CsvCast(this.rootDoc[this.fieldKey]).url.href, res))));
- }
+ .then(res => res.json().then(action(res => !res.errno && DataVizBox.dataset.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.layoutDoc._dataVizView = this.dataVizView === DataVizView.TABLE ? DataVizView.LINECHART : DataVizView.TABLE;
- }
+ // toggles for user to decide which chart type to view the data in
+ renderVizView = () => {
+ const width = this.props.PanelWidth() * 0.9;
+ const height = (this.props.PanelHeight() - 32) /* height of 'change view' button */ * 0.9;
+ const margin = { top: 10, right: 25, bottom: 75, left: 45 };
+ if (this.records) {
+ switch (this.dataVizView) {
+ case DataVizView.TABLE:
+ return <TableBox layoutDoc={this.layoutDoc} records={this.records} axes={this.axes} height={height} width={width} margin={margin} rootDoc={this.rootDoc} docView={this.props.DocumentView} selectAxes={this.selectAxes} />;
+ case DataVizView.LINECHART:
+ return (
+ <LineChart
+ layoutDoc={this.layoutDoc}
+ ref={r => (this._vizRenderer = r ?? undefined)}
+ height={height}
+ width={width}
+ fieldKey={this.fieldKey}
+ margin={margin}
+ rootDoc={this.rootDoc}
+ axes={this.axes}
+ records={this.records}
+ dataDoc={this.dataDoc}
+ />
+ );
+ case DataVizView.HISTOGRAM:
+ return (
+ <Histogram
+ layoutDoc={this.layoutDoc}
+ ref={r => (this._vizRenderer = r ?? undefined)}
+ height={height}
+ width={width}
+ fieldKey={this.fieldKey}
+ margin={margin}
+ rootDoc={this.rootDoc}
+ axes={this.axes}
+ records={this.records}
+ dataDoc={this.dataDoc}
+ />
+ );
+ case DataVizView.PIECHART:
+ return (
+ <PieChart
+ layoutDoc={this.layoutDoc}
+ ref={r => (this._vizRenderer = r ?? undefined)}
+ height={height}
+ width={width}
+ fieldKey={this.fieldKey}
+ margin={margin}
+ rootDoc={this.rootDoc}
+ axes={this.axes}
+ records={this.records}
+ dataDoc={this.dataDoc}
+ />
+ );
+ }
+ }
+ return 'no data/visualization';
+ };
render() {
- return !this.pairs?.length ? (
- <div>Loading...</div>
+ return !this.records?.length ? (
+ // displays how to get data into the DataVizBox if its empty
+ <div className="start-message">To create a DataViz box, either import / drag a CSV file into your canvas or copy a data table and use the command 'ctrl + p' to bring the data table to your canvas.</div>
) : (
<div
className="dataViz"
+ style={{
+ pointerEvents: this.props.isContentActive() === true ? "all" : "none"
+ }}
onWheel={e => e.stopPropagation()}
ref={r =>
r?.addEventListener(
@@ -143,8 +188,13 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
{ passive: false }
)
}>
- <button onClick={e => this.changeViewHandler(e)}>{this.dataVizView === DataVizView.TABLE ? DataVizView.LINECHART : DataVizView.TABLE}</button>
- {this.selectView}
+ <div className={'datatype-button'}>
+ <Toggle text={'TABLE'} toggleType={ToggleType.BUTTON} type={Type.SEC} color={'black'} onClick={e => (this.layoutDoc._dataViz = DataVizView.TABLE)} toggleStatus={this.layoutDoc._dataViz == DataVizView.TABLE} />
+ <Toggle text={'LINECHART'} toggleType={ToggleType.BUTTON} type={Type.SEC} color={'black'} onClick={e => (this.layoutDoc._dataViz = DataVizView.LINECHART)} toggleStatus={this.layoutDoc._dataViz == DataVizView.LINECHART} />
+ <Toggle text={'HISTOGRAM'} toggleType={ToggleType.BUTTON} type={Type.SEC} color={'black'} onClick={e => (this.layoutDoc._dataViz = DataVizView.HISTOGRAM)} toggleStatus={this.layoutDoc._dataViz == DataVizView.HISTOGRAM} />
+ <Toggle text={'PIE CHART'} toggleType={ToggleType.BUTTON} type={Type.SEC} color={'black'} onClick={e => (this.layoutDoc._dataViz = DataVizView.PIECHART)} toggleStatus={this.layoutDoc._dataViz == DataVizView.PIECHART} />
+ </div>
+ {this.renderVizView()}
</div>
);
}
diff --git a/src/client/views/nodes/DataVizBox/components/Chart.scss b/src/client/views/nodes/DataVizBox/components/Chart.scss
index d4f7bfb32..50dfe7f05 100644
--- a/src/client/views/nodes/DataVizBox/components/Chart.scss
+++ b/src/client/views/nodes/DataVizBox/components/Chart.scss
@@ -3,6 +3,55 @@
flex-direction: column;
align-items: center;
cursor: default;
+ margin-top: 10px;
+ overflow-y: visible;
+
+ .graph{
+ overflow: visible;
+ }
+ .graph-title{
+ align-items: center;
+ font-size: larger;
+ display: flex;
+ flex-direction: row;
+ margin-top: -10px;
+ margin-bottom: -10px;
+ }
+ .asHistogram-checkBox {
+ // display: flex;
+ // flex-direction: row;
+ align-items: left;
+ align-self: left;
+ align-content: left;
+ justify-content: flex-end;
+ float: left;
+ left: 0;
+ position: relative;
+ margin-bottom: -35px;
+ }
+ .selected-data{
+ align-items: center;
+ text-align: center;
+ display: flex;
+ flex-direction: row;
+ margin: 10px;
+ margin-top: -25px;
+ margin-bottom: 5px;
+ }
+ .slice {
+ &.hover {
+ stroke: black;
+ stroke-width: 2px;
+ }
+ }
+
+ .histogram-bar{
+ outline: thin solid black;
+ &.hover{
+ outline: 3px solid black;
+ outline-offset: -3px;
+ }
+ }
.tooltip {
// make the height width bigger
@@ -39,3 +88,22 @@
fill: red;
}
}
+.tableBox {
+ display: flex;
+ flex-direction: column;
+}
+.table-container{
+ overflow: scroll;
+ margin: 5px;
+ margin-left: 25px;
+ margin-right: 10px;
+ margin-bottom: 0;
+}
+.selectAll-buttons {
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-end;
+ margin-top: 5px;
+ margin-right: 10px;
+ float: right;
+} \ No newline at end of file
diff --git a/src/client/views/nodes/DataVizBox/components/Histogram.tsx b/src/client/views/nodes/DataVizBox/components/Histogram.tsx
new file mode 100644
index 000000000..e67e2bf31
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/components/Histogram.tsx
@@ -0,0 +1,518 @@
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { ColorPicker, EditableText, IconButton, Size, Type } from 'browndash-components';
+import * as d3 from 'd3';
+import { action, computed, IReactionDisposer, observable, reaction } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { FaFillDrip } from 'react-icons/fa';
+import { Doc, NumListCast, StrListCast } from '../../../../../fields/Doc';
+import { List } from '../../../../../fields/List';
+import { listSpec } from '../../../../../fields/Schema';
+import { Cast, DocCast, StrCast } from '../../../../../fields/Types';
+import { Docs } from '../../../../documents/Documents';
+import { LinkManager } from '../../../../util/LinkManager';
+import { undoable } from '../../../../util/UndoManager';
+import { PinProps, PresBox } from '../../trails';
+import { scaleCreatorNumerical, yAxisCreator } from '../utils/D3Utils';
+import './Chart.scss';
+
+export interface HistogramProps {
+ rootDoc: Doc;
+ layoutDoc: Doc;
+ axes: string[];
+ records: { [key: string]: any }[];
+ width: number;
+ height: number;
+ dataDoc: Doc;
+ fieldKey: string;
+ margin: {
+ top: number;
+ right: number;
+ bottom: number;
+ left: number;
+ };
+}
+
+@observer
+export class Histogram extends React.Component<HistogramProps> {
+ private _disposers: { [key: string]: IReactionDisposer } = {};
+ private _histogramRef: React.RefObject<HTMLDivElement> = React.createRef();
+ private _histogramSvg: d3.Selection<SVGGElement, unknown, null, undefined> | undefined;
+ private numericalXData: boolean = false; // whether the data is organized by numbers rather than categoreis
+ private numericalYData: boolean = false; // whether the y axis is controlled by provided data rather than frequency
+ private maxBins = 15; // maximum number of bins that is readable on a normal sized doc
+ @observable _currSelected: any | undefined = undefined; // Object of selected bar
+ private curBarSelected: any = undefined; // histogram bin of selected bar
+ private selectedData: any = undefined; // Selection of selected bar
+ private hoverOverData: any = undefined; // Selection of bar being hovered over
+
+ @computed get _tableDataIds() {
+ return !this.parentViz ? this.props.records.map((rec, i) => i) : NumListCast(this.parentViz.dataViz_selectedRows);
+ }
+ // returns all the data records that will be rendered by only returning those records that have been selected by the parent visualization (or all records if there is no parent)
+ @computed get _tableData() {
+ return !this.parentViz ? this.props.records : this._tableDataIds.map(rowId => this.props.records[rowId]);
+ }
+ // filters all data to just display selected data if brushed (created from an incoming link)
+ @computed get _histogramData() {
+ if (this.props.axes.length < 1) return [];
+ if (this.props.axes.length < 2) {
+ var ax0 = this.props.axes[0];
+ if (/\d/.test(this.props.records[0][ax0])) {
+ this.numericalXData = true;
+ }
+ return this._tableData.map(record => ({ [ax0]: record[this.props.axes[0]] }));
+ }
+ var ax0 = this.props.axes[0];
+ var ax1 = this.props.axes[1];
+ if (/\d/.test(this.props.records[0][ax0])) {
+ this.numericalXData = true;
+ }
+ if (/\d/.test(this.props.records[0][ax1])) {
+ this.numericalYData = true;
+ }
+ return this._tableData.map(record => ({ [ax0]: record[this.props.axes[0]], [ax1]: record[this.props.axes[1]] }));
+ }
+
+ @computed get defaultGraphTitle() {
+ var ax0 = this.props.axes[0];
+ var ax1 = this.props.axes.length > 1 ? this.props.axes[1] : undefined;
+ if (this.props.axes.length < 2 || !ax1 || !/\d/.test(this.props.records[0][ax1]) || !this.numericalYData) {
+ return ax0 + ' Histogram';
+ } else return ax0 + ' by ' + ax1 + ' Histogram';
+ }
+
+ @computed get parentViz() {
+ return DocCast(this.props.rootDoc.dataViz_parentViz);
+ // return LinkManager.Instance.getAllRelatedLinks(this.props.rootDoc) // out of all links
+ // .filter(link => link.link_anchor_1 == this.props.rootDoc.dataViz_parentViz) // get links where this chart doc is the target of the link
+ // .map(link => DocCast(link.link_anchor_1)); // then return the source of the link
+ }
+
+ @computed get rangeVals(): { xMin?: number; xMax?: number; yMin?: number; yMax?: number } {
+ if (this.numericalXData) {
+ const data = this.data(this._histogramData);
+ return { xMin: Math.min.apply(null, data), xMax: Math.max.apply(null, data), yMin: 0, yMax: 0 };
+ }
+ return { xMin: 0, xMax: 0, yMin: 0, yMax: 0 };
+ }
+
+ componentWillUnmount() {
+ Array.from(Object.keys(this._disposers)).forEach(key => this._disposers[key]());
+ }
+ componentDidMount = () => {
+ this._disposers.chartData = reaction(
+ () => ({ dataSet: this._histogramData, w: this.width, h: this.height }),
+ ({ dataSet, w, h }) => dataSet!.length > 0 && this.drawChart(dataSet, w, h),
+ { fireImmediately: true }
+ );
+ };
+
+ @action
+ restoreView = (data: Doc) => {};
+ // create a document anchor that stores whatever is needed to reconstruct the viewing state (selection,zoom,etc)
+ getAnchor = (pinProps?: PinProps) => {
+ const anchor = Docs.Create.ConfigDocument({
+ title: 'histogram doc selection' + this._currSelected,
+ });
+ PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this.props.rootDoc);
+ 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;
+ }
+
+ // cleans data by converting numerical data to numbers and taking out empty cells
+ data = (dataSet: any) => {
+ var validData = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || Number.isNaN(d[key])));
+ const field = dataSet[0] ? Object.keys(dataSet[0])[0] : undefined;
+ return !field
+ ? []
+ : validData.map((d: { [x: string]: any }) =>
+ !this.numericalXData //
+ ? d[field]
+ : +d[field!].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '')
+ );
+ };
+
+ // outlines the bar selected / hovered over
+ highlightSelectedBar = (changeSelectedVariables: boolean, svg: any, eachRectWidth: any, pointerX: any, xAxisTitle: any, yAxisTitle: any, histDataSet: any) => {
+ var sameAsCurrent: boolean;
+ var barCounter = -1;
+ const selected = svg.selectAll('.histogram-bar').filter((d: any) => {
+ barCounter++; // uses the order of bars and width of each bar to find which one the pointer is over
+ if (barCounter * eachRectWidth <= pointerX && pointerX <= (barCounter + 1) * eachRectWidth) {
+ var showSelected = this.numericalYData
+ ? this._histogramData.filter((data: { [x: string]: any }) => StrCast(data[xAxisTitle]).replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '') == d[0])[0]
+ : histDataSet.filter((data: { [x: string]: any }) => data[xAxisTitle].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '') == d[0])[0];
+ if (this.numericalXData) {
+ // calculating frequency
+ if (d[0] && d[1] && d[0] != d[1]) {
+ showSelected = { [xAxisTitle]: d3.min(d) + ' to ' + d3.max(d), frequency: d.length };
+ } else if (!this.numericalYData) showSelected = { [xAxisTitle]: showSelected[xAxisTitle], frequency: d.length };
+ }
+ if (changeSelectedVariables) {
+ // for when a bar is selected - not just hovered over
+ sameAsCurrent = this._currSelected ? showSelected[xAxisTitle] == this._currSelected![xAxisTitle] && showSelected[yAxisTitle] == this._currSelected![yAxisTitle] : false;
+ this._currSelected = sameAsCurrent ? undefined : showSelected;
+ this.selectedData = sameAsCurrent ? undefined : d;
+ } else this.hoverOverData = d;
+ return true;
+ }
+ return false;
+ });
+ if (changeSelectedVariables) {
+ if (sameAsCurrent!) this.curBarSelected = undefined;
+ else this.curBarSelected = selected;
+ }
+ };
+
+ // draws the histogram
+ drawChart = (dataSet: any, width: number, height: number) => {
+ d3.select(this._histogramRef.current).select('svg').remove();
+ d3.select(this._histogramRef.current).select('.tooltip').remove();
+
+ const data = this.data(dataSet);
+ const xAxisTitle = Object.keys(dataSet[0])[0];
+ const yAxisTitle = this.numericalYData ? Object.keys(dataSet[0])[1] : 'frequency';
+ const uniqueArr: unknown[] = [...new Set(data)];
+ var numBins = this.numericalXData && Number.isInteger(data[0]) ? this.rangeVals.xMax! - this.rangeVals.xMin! : uniqueArr.length;
+ var translateXAxis = !this.numericalXData || numBins < this.maxBins ? width / (numBins + 1) / 2 : 0;
+ if (numBins > this.maxBins) numBins = this.maxBins;
+ const startingPoint = this.numericalXData ? this.rangeVals.xMin! : 0;
+ const endingPoint = this.numericalXData ? this.rangeVals.xMax! : numBins;
+
+ // converts data into Objects
+ var histDataSet = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || Number.isNaN(d[key])));
+ if (!this.numericalXData) {
+ var histStringDataSet: { [x: string]: unknown }[] = [];
+ if (this.numericalYData) {
+ for (let i = 0; i < dataSet.length; i++) {
+ histStringDataSet.push({ [yAxisTitle]: dataSet[i][yAxisTitle], [xAxisTitle]: dataSet[i][xAxisTitle] });
+ }
+ } else {
+ for (let i = 0; i < uniqueArr.length; i++) {
+ histStringDataSet.push({ [yAxisTitle]: 0, [xAxisTitle]: uniqueArr[i] });
+ }
+ for (let i = 0; i < data.length; i++) {
+ let barData = histStringDataSet.filter(each => each[xAxisTitle] == data[i]);
+ histStringDataSet.filter(each => each[xAxisTitle] == data[i])[0][yAxisTitle] = Number(barData[0][yAxisTitle]) + 1;
+ }
+ }
+ histDataSet = histStringDataSet;
+ }
+
+ // initial graph and binning data for histogram
+ var svg = (this._histogramSvg = d3
+ .select(this._histogramRef.current)
+ .append('svg')
+ .attr('class', 'graph')
+ .attr('width', width + this.props.margin.right + this.props.margin.left)
+ .attr('height', height + this.props.margin.top + this.props.margin.bottom)
+ .append('g')
+ .attr('transform', 'translate(' + this.props.margin.left + ',' + this.props.margin.top + ')'));
+ var x = d3
+ .scaleLinear()
+ .domain(this.numericalXData ? [startingPoint!, endingPoint!] : [0, numBins])
+ .range([0, width]);
+ var histogram = d3
+ .histogram()
+ .value(function (d) {
+ return d;
+ })
+ .domain([startingPoint!, endingPoint!])
+ .thresholds(x.ticks(numBins));
+ var bins = histogram(data);
+ var eachRectWidth = width / bins.length;
+ var graphStartingPoint = bins[0].x1 && bins[1] ? bins[0].x1! - (bins[1].x1! - bins[1].x0!) : 0;
+ bins[0].x0 = graphStartingPoint;
+ x = x.domain([graphStartingPoint, endingPoint]).range([0, Number.isInteger(this.rangeVals.xMin!) ? width - eachRectWidth : width]);
+ var xAxis;
+
+ // more calculations based on bins
+ // x-axis
+ if (!this.numericalXData) {
+ // reorganize to match data if the data is strings rather than numbers
+ // uniqueArr.sort()
+ histDataSet.sort();
+ for (let i = 0; i < data.length; i++) {
+ var index = 0;
+ for (let j = 0; j < uniqueArr.length; j++) {
+ if (uniqueArr[j] == data[i]) {
+ index = j;
+ }
+ }
+ if (bins[index]) bins[index].push(data[i]);
+ }
+ bins.pop();
+ eachRectWidth = width / bins.length;
+ bins.forEach(d => (d.x0 = d.x0!));
+ xAxis = d3
+ .axisBottom(x)
+ .ticks(bins.length > 1 ? bins.length - 1 : 1)
+ .tickFormat(i => uniqueArr[i.valueOf()] as string)
+ .tickPadding(10);
+ x.range([0, width - eachRectWidth]);
+ x.domain([0, bins.length - 1]);
+ translateXAxis = eachRectWidth / 2;
+ } else {
+ var allSame = true;
+ for (var i = 0; i < bins.length; i++) {
+ if (bins[i] && bins[i][0]) {
+ var compare = bins[i][0];
+ for (let j = 1; j < bins[i].length; j++) {
+ if (bins[i][j] != compare) allSame = false;
+ }
+ }
+ }
+ if (allSame) {
+ translateXAxis = eachRectWidth / 2;
+ eachRectWidth = width / bins.length;
+ } else {
+ eachRectWidth = width / (bins.length + 1);
+ var tickDiff = bins.length >= 2 ? bins[bins.length - 2].x1! - bins[bins.length - 2].x0! : 0;
+ var curDomain = x.domain();
+ x.domain([curDomain[0], curDomain[0] + tickDiff * bins.length]);
+ }
+
+ xAxis = d3.axisBottom(x).ticks(bins.length - 1);
+ x.range([0, width - eachRectWidth]);
+ }
+ // y-axis
+ const maxFrequency = this.numericalYData
+ ? d3.max(histDataSet, function (d: any) {
+ return d[yAxisTitle] ? Number(d[yAxisTitle]!.replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '')) : 0;
+ })
+ : d3.max(bins, function (d) {
+ return d.length;
+ });
+ var y = d3.scaleLinear().range([height, 0]);
+ y.domain([0, +maxFrequency!]);
+ var yAxis = d3.axisLeft(y).ticks(maxFrequency!);
+ if (this.numericalYData) {
+ const yScale = scaleCreatorNumerical(0, Number(maxFrequency), height, 0);
+ yAxisCreator(svg.append('g'), width, yScale);
+ } else {
+ svg.append('g').call(yAxis);
+ }
+ svg.append('g')
+ .attr('transform', 'translate(' + translateXAxis + ', ' + height + ')')
+ .call(xAxis);
+
+ // click/hover
+ const onPointClick = action((e: any) => this.highlightSelectedBar(true, svg, eachRectWidth, d3.pointer(e)[0], xAxisTitle, yAxisTitle, histDataSet));
+ const onHover = action((e: any) => {
+ this.highlightSelectedBar(false, svg, eachRectWidth, d3.pointer(e)[0], xAxisTitle, yAxisTitle, histDataSet);
+ updateHighlights();
+ });
+ const mouseOut = action((e: any) => {
+ this.hoverOverData = undefined;
+ updateHighlights();
+ });
+ const updateHighlights = () => {
+ const hoverOverBar = this.hoverOverData;
+ const selectedData = this.selectedData;
+ svg.selectAll('rect').attr('class', function (d: any) {
+ return (hoverOverBar && hoverOverBar[0] == d[0]) || (selectedData && selectedData[0] == d[0]) ? 'histogram-bar hover' : 'histogram-bar';
+ });
+ };
+ svg.on('click', onPointClick).on('mouseover', onHover).on('mouseout', mouseOut);
+
+ // axis titles
+ svg.append('text')
+ .attr('transform', 'translate(' + width / 2 + ' ,' + (height + 40) + ')')
+ .style('text-anchor', 'middle')
+ .text(xAxisTitle);
+ svg.append('text')
+ .attr('transform', 'rotate(-90)' + ' ' + 'translate( 0, ' + -10 + ')')
+ .attr('x', -(height / 2))
+ .attr('y', -20)
+ .style('text-anchor', 'middle')
+ .text(yAxisTitle);
+ d3.format('.0f');
+
+ // draw bars
+ var selected = this.selectedData;
+ svg.selectAll('rect')
+ .data(bins)
+ .enter()
+ .append('rect')
+ .attr(
+ 'transform',
+ this.numericalYData
+ ? function (d) {
+ const eachData = histDataSet.filter((data: { [x: string]: number }) => {
+ return data[xAxisTitle] == d[0];
+ });
+ const length = eachData.length ? eachData[0][yAxisTitle].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '') : 0;
+ return 'translate(' + x(d.x0!) + ',' + y(length) + ')';
+ }
+ : function (d) {
+ return 'translate(' + x(d.x0!) + ',' + y(d.length) + ')';
+ }
+ )
+ .attr(
+ 'height',
+ this.numericalYData
+ ? function (d) {
+ const eachData = histDataSet.filter((data: { [x: string]: number }) => {
+ return data[xAxisTitle] == d[0];
+ });
+ const length = eachData.length ? eachData[0][yAxisTitle].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '') : 0;
+ return height - y(length);
+ }
+ : function (d) {
+ return height - y(d.length);
+ }
+ )
+ .attr('width', eachRectWidth)
+ .attr(
+ 'class',
+ selected
+ ? function (d) {
+ return selected && selected[0] === d[0] ? 'histogram-bar hover' : 'histogram-bar';
+ }
+ : function (d) {
+ return 'histogram-bar';
+ }
+ )
+ .attr('fill', d => {
+ var barColor;
+ const barColors = StrListCast(this.props.layoutDoc.dataViz_histogram_barColors).map(each => each.split('::'));
+ barColors.forEach(each => {
+ if (d[0] && d[0].toString() && each[0] == d[0].toString()) barColor = each[1];
+ else {
+ const range = StrCast(each[0]).split(' to ');
+ if (Number(range[0]) <= d[0] && d[0] <= Number(range[1])) barColor = each[1];
+ }
+ });
+ return barColor ? StrCast(barColor) : StrCast(this.props.layoutDoc.dataViz_histogram_defaultColor);
+ });
+ };
+
+ @action changeSelectedColor = (color: string) => {
+ this.curBarSelected.attr('fill', color);
+ const barName = StrCast(this._currSelected[this.props.axes[0]].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, ''));
+
+ const barColors = Cast(this.props.layoutDoc.dataViz_histogram_barColors, listSpec('string'), null);
+ barColors.forEach(each => each.split('::')[0] === barName && barColors.splice(barColors.indexOf(each), 1));
+ barColors.push(StrCast(barName + '::' + color));
+ };
+
+ @action eraseSelectedColor = () => {
+ this.curBarSelected.attr('fill', this.props.layoutDoc.dataViz_histogram_defaultColor);
+ const barName = StrCast(this._currSelected[this.props.axes[0]].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, ''));
+
+ const barColors = Cast(this.props.layoutDoc.dataViz_histogram_barColors, listSpec('string'), null);
+ barColors.forEach(each => each.split('::')[0] === barName && barColors.splice(barColors.indexOf(each), 1));
+ };
+
+ updateBarColors = () => {
+ var svg = this._histogramSvg;
+ if (svg)
+ svg.selectAll('rect').attr('fill', (d: any) => {
+ var barColor;
+ const barColors = StrListCast(this.props.layoutDoc.dataViz_histogram_barColors).map(each => each.split('::'));
+ barColors.forEach(each => {
+ if (d[0] && d[0].toString() && each[0] == d[0].toString()) barColor = each[1];
+ else {
+ const range = StrCast(each[0]).split(' to ');
+ if (Number(range[0]) <= d[0] && d[0] <= Number(range[1])) barColor = each[1];
+ }
+ });
+ return barColor ? StrCast(barColor) : StrCast(this.props.layoutDoc.dataViz_histogram_defaultColor);
+ });
+ };
+
+ render() {
+ this.updateBarColors();
+ this._histogramData;
+ var curSelectedBarName = '';
+ var titleAccessor: any = '';
+ if (this.props.axes.length == 2) titleAccessor = 'dataViz_histogram_title' + this.props.axes[0] + '-' + this.props.axes[1];
+ else if (this.props.axes.length > 0) titleAccessor = 'dataViz_histogram_title' + this.props.axes[0];
+ if (!this.props.layoutDoc[titleAccessor]) this.props.layoutDoc[titleAccessor] = this.defaultGraphTitle;
+ if (!this.props.layoutDoc.dataViz_histogram_defaultColor) this.props.layoutDoc.dataViz_histogram_defaultColor = '#69b3a2';
+ if (!this.props.layoutDoc.dataViz_histogram_barColors) this.props.layoutDoc.dataViz_histogram_barColors = new List<string>();
+ var selected = 'none';
+ if (this._currSelected) {
+ curSelectedBarName = StrCast(this._currSelected![this.props.axes[0]].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, ''));
+ selected = '{ ';
+ Object.keys(this._currSelected).forEach(key =>
+ key //
+ ? (selected += key + ': ' + this._currSelected[key] + ', ')
+ : ''
+ );
+ selected = selected.substring(0, selected.length - 2) + ' }';
+ }
+ var selectedBarColor;
+ var barColors = StrListCast(this.props.layoutDoc.histogramBarColors).map(each => each.split('::'));
+ barColors.forEach(each => each[0] === curSelectedBarName && (selectedBarColor = each[1]));
+
+ if (this._histogramData.length > 0 || !this.parentViz) {
+ return this.props.axes.length >= 1 ? (
+ <div className="chart-container">
+ <div className="graph-title">
+ <EditableText
+ val={StrCast(this.props.layoutDoc[titleAccessor])}
+ setVal={undoable(
+ action(val => (this.props.layoutDoc[titleAccessor] = val as string)),
+ 'Change Graph Title'
+ )}
+ color={'black'}
+ size={Size.LARGE}
+ fillWidth
+ />
+ &nbsp; &nbsp;
+ <ColorPicker
+ tooltip={'Change Default Bar Color'}
+ type={Type.SEC}
+ icon={<FaFillDrip />}
+ selectedColor={StrCast(this.props.layoutDoc.dataViz_histogram_defaultColor)}
+ setFinalColor={undoable(color => (this.props.layoutDoc.dataViz_histogram_defaultColor = color), 'Change Default Bar Color')}
+ setSelectedColor={undoable(color => (this.props.layoutDoc.dataViz_histogram_defaultColor = color), 'Change Default Bar Color')}
+ size={Size.XSMALL}
+ />
+ </div>
+ <div ref={this._histogramRef} />
+ {selected != 'none' ? (
+ <div className={'selected-data'}>
+ Selected: {selected}
+ &nbsp; &nbsp;
+ <ColorPicker
+ tooltip={'Change Bar Color'}
+ type={Type.SEC}
+ icon={<FaFillDrip />}
+ selectedColor={selectedBarColor ? selectedBarColor : this.curBarSelected.attr('fill')}
+ setFinalColor={undoable(color => this.changeSelectedColor(color), 'Change Selected Bar Color')}
+ setSelectedColor={undoable(color => this.changeSelectedColor(color), 'Change Selected Bar Color')}
+ size={Size.XSMALL}
+ />
+ &nbsp;
+ <IconButton
+ icon={<FontAwesomeIcon icon={'eraser'} />}
+ size={Size.XSMALL}
+ color={'black'}
+ type={Type.SEC}
+ tooltip={'Revert to the default bar color'}
+ onClick={undoable(
+ action(() => this.eraseSelectedColor()),
+ 'Change Selected Bar Color'
+ )}
+ />
+ </div>
+ ) : null}
+ </div>
+ ) : (
+ <span className="chart-container"> {'first use table view to select a column to graph'}</span>
+ );
+ }
+ // when it is a brushed table and the incoming table doesn't have any rows selected
+ return <div className="chart-container">Selected rows of data from the incoming DataVizBox to display.</div>;
+ }
+}
diff --git a/src/client/views/nodes/DataVizBox/components/LineChart.tsx b/src/client/views/nodes/DataVizBox/components/LineChart.tsx
index 6b564b0c9..3de7a0c4a 100644
--- a/src/client/views/nodes/DataVizBox/components/LineChart.tsx
+++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx
@@ -1,16 +1,15 @@
+import { EditableText, Size } from 'browndash-components';
+import * as d3 from 'd3';
import { action, computed, IReactionDisposer, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-// import d3
-import * as d3 from 'd3';
-import { Doc, DocListCast } from '../../../../../fields/Doc';
-import { Id } from '../../../../../fields/FieldSymbols';
+import { Doc, DocListCast, NumListCast, StrListCast } from '../../../../../fields/Doc';
import { List } from '../../../../../fields/List';
import { listSpec } from '../../../../../fields/Schema';
-import { Cast, DocCast } from '../../../../../fields/Types';
+import { Cast, DocCast, StrCast } from '../../../../../fields/Types';
import { Docs } from '../../../../documents/Documents';
import { DocumentManager } from '../../../../util/DocumentManager';
-import { LinkManager } from '../../../../util/LinkManager';
+import { undoable } from '../../../../util/UndoManager';
import { PinProps, PresBox } from '../../trails';
import { DataVizBox } from '../DataVizBox';
import { createLineGenerator, drawLine, minMaxRange, scaleCreatorNumerical, xAxisCreator, xGrid, yAxisCreator, yGrid } from '../utils/D3Utils';
@@ -20,13 +19,14 @@ export interface DataPoint {
x: number;
y: number;
}
-interface SelectedDataPoint extends DataPoint {
+export interface SelectedDataPoint extends DataPoint {
elem?: d3.Selection<d3.BaseType, unknown, SVGGElement, unknown>;
}
export interface LineChartProps {
rootDoc: Doc;
+ layoutDoc: Doc;
axes: string[];
- pairs: { [key: string]: any }[];
+ records: { [key: string]: any }[];
width: number;
height: number;
dataDoc: Doc;
@@ -47,24 +47,35 @@ export class LineChart extends React.Component<LineChartProps> {
@observable _currSelected: SelectedDataPoint | undefined = undefined;
// TODO: nda - some sort of mapping that keeps track of the annotated points so we can easily remove when annotations list updates
+ @computed get _tableDataIds() {
+ return !this.parentViz ? this.props.records.map((rec, i) => i) : NumListCast(this.parentViz.dataViz_selectedRows);
+ }
+ // returns all the data records that will be rendered by only returning those records that have been selected by the parent visualization (or all records if there is no parent)
+ @computed get _tableData() {
+ return !this.parentViz ? this.props.records : this._tableDataIds.map(rowId => this.props.records[rowId]);
+ }
@computed get _lineChartData() {
+ var guids = StrListCast(this.props.layoutDoc.dataViz_rowIds);
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));
+ return this._tableData.map(record => ({ x: Number(record[this.props.axes[0]]), y: Number(record[this.props.axes[1]]) })).sort((a, b) => (a.x < b.x ? -1 : 1));
}
- @computed get 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 graphTitle() {
+ return this.props.axes[1] + ' vs. ' + this.props.axes[0] + ' Line Chart';
}
- @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 parentViz() {
+ return DocCast(this.props.rootDoc.dataViz_parentViz);
+ // return LinkManager.Instance.getAllRelatedLinks(this.props.rootDoc) // out of all links
+ // .filter(link => {
+ // return link.link_anchor_1 == this.props.rootDoc.dataViz_parentViz;
+ // }) // get links where this chart doc is the target of the link
+ // .map(link => DocCast(link.link_anchor_1)); // then return the source of the link
+ }
+ @computed get incomingHighlited() {
+ // return selected x and y axes
+ // otherwise, use the selection of whatever is linked to us
+ const incomingVizBox = DocumentManager.Instance.getFirstDocumentView(this.parentViz)?.ComponentView as DataVizBox;
+ const highlitedRowIds = NumListCast(incomingVizBox?.rootDoc?.dataViz_highlitedRows);
+ return this._tableData.filter((record, i) => highlitedRowIds.includes(this._tableDataIds[i])); // get all the datapoints they have selected field set by incoming anchor
}
@computed get rangeVals(): { xMin?: number; xMax?: number; yMin?: number; yMax?: number } {
return minMaxRange([this._lineChartData]);
@@ -78,10 +89,6 @@ export class LineChart extends React.Component<LineChartProps> {
({ dataSet, w, h }) => {
if (dataSet) {
this.drawChart([dataSet], this.rangeVals, w, h);
- // redraw annotations when the chart data has changed, or the local or inherited selection has changed
- this.clearAnnotations();
- this._currSelected && this.drawAnnotations(Number(this._currSelected.x), Number(this._currSelected.y), true);
- this.incomingSelected?.forEach((pair: any) => this.drawAnnotations(Number(pair[this.props.axes[0]]), Number(pair[this.props.axes[1]])));
}
},
{ fireImmediately: true }
@@ -93,7 +100,7 @@ export class LineChart extends React.Component<LineChartProps> {
// could be blue colored to make it look like anchor
// this.drawAnnotations()
// loop through annotations and draw them
- annotations.forEach(a => this.drawAnnotations(Number(a.x), Number(a.y)));
+ // annotations.forEach(a => this.drawAnnotations(Number(a.x), Number(a.y)));
// this.drawAnnotations(annotations.x, annotations.y);
},
{ fireImmediately: true }
@@ -101,13 +108,13 @@ export class LineChart extends React.Component<LineChartProps> {
this._disposers.highlights = reaction(
() => ({
selected: this._currSelected,
- incomingSelected: this.incomingSelected,
+ incomingHighlited: this.incomingHighlited,
}),
- ({ selected, incomingSelected }) => {
+ ({ selected, incomingHighlited }) => {
// redraw annotations when the chart data has changed, or the local or inherited selection has changed
this.clearAnnotations();
selected && this.drawAnnotations(Number(selected.x), Number(selected.y), true);
- incomingSelected?.forEach((pair: any) => this.drawAnnotations(Number(pair[this.props.axes[0]]), Number(pair[this.props.axes[1]])));
+ incomingHighlited?.forEach((record: any) => this.drawAnnotations(Number(record[this.props.axes[0]]), Number(record[this.props.axes[1]])));
},
{ fireImmediately: true }
);
@@ -143,13 +150,9 @@ export class LineChart extends React.Component<LineChartProps> {
}
};
- removeAnnotations(dataX: number, dataY: number) {
- // loop through and remove any annotations that no longer exist
- }
-
@action
restoreView = (data: Doc) => {
- const coords = Cast(data.presDataVizSelection, listSpec('number'), null);
+ const coords = Cast(data.config_dataVizSelection, 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;
@@ -167,8 +170,8 @@ export class LineChart extends React.Component<LineChartProps> {
//
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;
+ PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this.props.rootDoc);
+ anchor.config_dataVizSelection = this._currSelected ? new List<number>([this._currSelected.x, this._currSelected.y]) : undefined;
return anchor;
};
@@ -180,6 +183,14 @@ export class LineChart extends React.Component<LineChartProps> {
return this.props.width - this.props.margin.left - this.props.margin.right;
}
+ @computed get defaultGraphTitle() {
+ var ax0 = this.props.axes[0];
+ var ax1 = this.props.axes.length > 1 ? this.props.axes[1] : undefined;
+ if (this.props.axes.length < 2 || !/\d/.test(this.props.records[0][ax0]) || !ax1) {
+ return ax0 + ' Line Chart';
+ } else return ax1 + ' by ' + ax0 + ' Line Chart';
+ }
+
setupTooltip() {
return d3
.select(this._lineChartRef.current)
@@ -197,9 +208,9 @@ export class LineChart extends React.Component<LineChartProps> {
@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));
+ if (this._currSelected && this._currSelected.x == x && this._currSelected.y == y) this._currSelected = undefined;
+ else this._currSelected = x !== undefined && y !== undefined ? { x, y } : undefined;
+ this.props.records.forEach(record => record[this.props.axes[0]] === x && record[this.props.axes[1]] === y && (record.selected = true));
}
drawDataPoints(data: DataPoint[], idx: number, xScale: d3.ScaleLinear<number, number, never>, yScale: d3.ScaleLinear<number, number, never>) {
@@ -219,7 +230,7 @@ export class LineChart extends React.Component<LineChartProps> {
}
// 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) => {
+ drawChart = (dataSet: any[][], 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();
@@ -238,6 +249,7 @@ export class LineChart extends React.Component<LineChartProps> {
const svg = (this._lineChartSvg = d3
.select(this._lineChartRef.current)
.append('svg')
+ .attr('class', 'graph')
.attr('width', `${width + margin.left + margin.right}`)
.attr('height', `${height + margin.top + margin.bottom}`)
.append('g')
@@ -249,13 +261,20 @@ export class LineChart extends React.Component<LineChartProps> {
xAxisCreator(svg.append('g'), height, xScale);
yAxisCreator(svg.append('g'), width, yScale);
- // draw the plot line
+ // get valid data points
const data = dataSet[0];
const lineGen = createLineGenerator(xScale, yScale);
- drawLine(svg.append('path'), data, lineGen);
-
+ var validData = data.filter(d => {
+ var valid = true;
+ Object.keys(data[0]).map(key => {
+ if (!d[key] || Number.isNaN(d[key])) valid = false;
+ });
+ return valid;
+ });
+ // draw the plot line
+ drawLine(svg.append('path'), validData, lineGen);
// draw the datapoint circle
- this.drawDataPoints(data, 0, xScale, yScale);
+ this.drawDataPoints(validData, 0, xScale, yScale);
const higlightFocusPt = svg.append('g').style('display', 'none');
higlightFocusPt.append('circle').attr('r', 5).attr('class', 'circle');
@@ -293,6 +312,20 @@ export class LineChart extends React.Component<LineChartProps> {
.on('mouseout', () => tooltip.transition().duration(300).style('opacity', 0))
.on('mousemove', mousemove)
.on('click', onPointClick);
+
+ // axis titles
+ svg.append('text')
+ .attr('transform', 'translate(' + width / 2 + ' ,' + (height + 40) + ')')
+ .style('text-anchor', 'middle')
+ .text(this.props.axes[0]);
+ svg.append('text')
+ .attr('transform', 'rotate(-90)' + ' ' + 'translate( 0, ' + -10 + ')')
+ .attr('x', -(height / 2))
+ .attr('y', -20)
+ .attr('height', 20)
+ .attr('width', 20)
+ .style('text-anchor', 'middle')
+ .text(this.props.axes[1]);
};
private updateTooltip(
@@ -308,15 +341,40 @@ export class LineChart extends React.Component<LineChartProps> {
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)`);
+ .style('transform', `translate(${xScale(d0.x) - this.width}px,${yScale(d0.y)}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>
- );
+ var titleAccessor: any = '';
+ if (this.props.axes.length == 2) titleAccessor = 'dataViz_lineChart_title' + this.props.axes[0] + '-' + this.props.axes[1];
+ else if (this.props.axes.length > 0) titleAccessor = 'dataViz_lineChart_title' + this.props.axes[0];
+ if (!this.props.layoutDoc[titleAccessor]) this.props.layoutDoc[titleAccessor] = this.defaultGraphTitle;
+ const selectedPt = this._currSelected ? `{ ${this.props.axes[0]}: ${this._currSelected.x} ${this.props.axes[1]}: ${this._currSelected.y} }` : 'none';
+ if (this._lineChartData.length > 0 || !this.parentViz || this.parentViz.length == 0) {
+ return this.props.axes.length >= 2 && /\d/.test(this.props.records[0][this.props.axes[0]]) && /\d/.test(this.props.records[0][this.props.axes[1]]) ? (
+ <div className="chart-container">
+ <div className="graph-title">
+ <EditableText
+ val={StrCast(this.props.layoutDoc[titleAccessor])}
+ setVal={undoable(
+ action(val => (this.props.layoutDoc[titleAccessor] = val as string)),
+ 'Change Graph Title'
+ )}
+ color={'black'}
+ size={Size.LARGE}
+ fillWidth
+ />
+ </div>
+ <div ref={this._lineChartRef} />
+ {selectedPt != 'none' ? <div className={'selected-data'}> {`Selected: ${selectedPt}`}</div> : null}
+ </div>
+ ) : (
+ <span className="chart-container"> {'first use table view to select two numerical axes to plot'}</span>
+ );
+ } else
+ return (
+ // when it is a brushed table and the incoming table doesn't have any rows selected
+ <div className="chart-container">Selected rows of data from the incoming DataVizBox to display.</div>
+ );
}
}
diff --git a/src/client/views/nodes/DataVizBox/components/PieChart.tsx b/src/client/views/nodes/DataVizBox/components/PieChart.tsx
new file mode 100644
index 000000000..561f39141
--- /dev/null
+++ b/src/client/views/nodes/DataVizBox/components/PieChart.tsx
@@ -0,0 +1,401 @@
+import { ColorPicker, EditableText, Size, Type } from 'browndash-components';
+import * as d3 from 'd3';
+import { action, computed, IReactionDisposer, observable, reaction } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { FaFillDrip } from 'react-icons/fa';
+import { Doc, NumListCast, StrListCast } from '../../../../../fields/Doc';
+import { List } from '../../../../../fields/List';
+import { listSpec } from '../../../../../fields/Schema';
+import { Cast, DocCast, StrCast } from '../../../../../fields/Types';
+import { Docs } from '../../../../documents/Documents';
+import { undoable } from '../../../../util/UndoManager';
+import { PinProps, PresBox } from '../../trails';
+import './Chart.scss';
+import { Checkbox } from '@material-ui/core';
+
+export interface PieChartProps {
+ rootDoc: Doc;
+ layoutDoc: Doc;
+ axes: string[];
+ records: { [key: string]: any }[];
+ width: number;
+ height: number;
+ dataDoc: Doc;
+ fieldKey: string;
+ margin: {
+ top: number;
+ right: number;
+ bottom: number;
+ left: number;
+ };
+}
+
+@observer
+export class PieChart extends React.Component<PieChartProps> {
+ private _disposers: { [key: string]: IReactionDisposer } = {};
+ private _piechartRef: React.RefObject<HTMLDivElement> = React.createRef();
+ private _piechartSvg: d3.Selection<SVGGElement, unknown, null, undefined> | undefined;
+ private curSliceSelected: any = undefined; // d3 data of selected slice
+ private selectedData: any = undefined; // Selection of selected slice
+ private hoverOverData: any = undefined; // Selection of slice being hovered over
+ @observable _currSelected: any | undefined = undefined; // Object of selected slice
+
+ @computed get _tableDataIds() {
+ return !this.parentViz ? this.props.records.map((rec, i) => i) : NumListCast(this.parentViz.dataViz_selectedRows);
+ }
+ // returns all the data records that will be rendered by only returning those records that have been selected by the parent visualization (or all records if there is no parent)
+ @computed get _tableData() {
+ return !this.parentViz ? this.props.records : this._tableDataIds.map(rowId => this.props.records[rowId]);
+ }
+
+ // organized by specified number percentages/ratios if one column is selected and it contains numbers
+ // otherwise, assume data is organized by categories
+ @computed get byCategory() {
+ return !/\d/.test(this.props.records[0][this.props.axes[0]]) || this.props.layoutDoc.dataViz_pie_asHistogram;
+ }
+ // filters all data to just display selected data if brushed (created from an incoming link)
+ @computed get _pieChartData() {
+ if (this.props.axes.length < 1) return [];
+
+ const ax0 = this.props.axes[0];
+ if (this.props.axes.length < 2) {
+ return this._tableData.map(record => ({ [ax0]: record[this.props.axes[0]] }));
+ }
+ const ax1 = this.props.axes[1];
+ return this._tableData.map(record => ({ [ax0]: record[this.props.axes[0]], [ax1]: record[this.props.axes[1]] }));
+ }
+
+ @computed get defaultGraphTitle() {
+ var ax0 = this.props.axes[0];
+ var ax1 = this.props.axes.length > 1 ? this.props.axes[1] : undefined;
+ if (this.props.axes.length < 2 || !/\d/.test(this.props.records[0][ax0]) || !ax1) {
+ return ax0 + ' Pie Chart';
+ }
+ return ax1 + ' by ' + ax0 + ' Pie Chart';
+ }
+
+ @computed get parentViz() {
+ return DocCast(this.props.rootDoc.dataViz_parentViz);
+ // return LinkManager.Instance.getAllRelatedLinks(this.props.rootDoc) // out of all links
+ // .filter(link => link.link_anchor_1 == this.props.rootDoc.dataViz_parentViz) // get links where this chart doc is the target of the link
+ // .map(link => DocCast(link.link_anchor_1)); // then return the source of the link
+ }
+
+ componentWillUnmount() {
+ Array.from(Object.keys(this._disposers)).forEach(key => this._disposers[key]());
+ }
+ componentDidMount = () => {
+ this._disposers.chartData = reaction(
+ () => ({ dataSet: this._pieChartData, w: this.width, h: this.height }),
+ ({ dataSet, w, h }) => {
+ if (dataSet!.length > 0) {
+ this.drawChart(dataSet, w, h);
+ }
+ },
+ { fireImmediately: true }
+ );
+ };
+
+ @action
+ restoreView = (data: Doc) => {};
+ // 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: 'piechart doc selection' + this._currSelected,
+ });
+ PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this.props.rootDoc);
+ 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;
+ }
+
+ // cleans data by converting numerical data to numbers and taking out empty cells
+ data = (dataSet: any) => {
+ const validData = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || Number.isNaN(d[key])));
+ const field = dataSet[0] ? Object.keys(dataSet[0])[0] : undefined;
+ return !field
+ ? undefined
+ : validData.map((d: { [x: string]: any }) =>
+ this.byCategory
+ ? d[field] //
+ : +d[field].replace(/\$/g, '').replace(/\%/g, '').replace(/\#/g, '').replace(/\</g, '')
+ );
+ };
+
+ // outlines the slice selected / hovered over
+ highlightSelectedSlice = (changeSelectedVariables: boolean, svg: any, arc: any, radius: any, pointer: any, pieDataSet: any) => {
+ var index = -1;
+ var sameAsCurrent: boolean;
+ const selected = svg.selectAll('.slice').filter((d: any) => {
+ index++;
+ var p1 = [0, 0]; // center of pie
+ var p3 = [arc.centroid(d)[0] * 2, arc.centroid(d)[1] * 2]; // outward peak of arc
+ var p2 = [radius * Math.sin(d.startAngle), -radius * Math.cos(d.startAngle)]; // start of arc
+ var p4 = [radius * Math.sin(d.endAngle), -radius * Math.cos(d.endAngle)]; // end of arc
+
+ // draw an imaginary horizontal line from the pointer to see how many times it crosses a slice edge
+ var lineCrossCount = 0;
+ // if for all 4 lines
+ if (Math.min(p1[1], p2[1]) <= pointer[1] && pointer[1] <= Math.max(p1[1], p2[1])) {
+ // within y bounds
+ if (pointer[0] <= ((pointer[1] - p1[1]) * (p2[0] - p1[0])) / (p2[1] - p1[1]) + p1[0]) lineCrossCount++;
+ } // intercepts x
+ if (Math.min(p2[1], p3[1]) <= pointer[1] && pointer[1] <= Math.max(p2[1], p3[1])) {
+ if (pointer[0] <= ((pointer[1] - p2[1]) * (p3[0] - p2[0])) / (p3[1] - p2[1]) + p2[0]) lineCrossCount++;
+ }
+ if (Math.min(p3[1], p4[1]) <= pointer[1] && pointer[1] <= Math.max(p3[1], p4[1])) {
+ if (pointer[0] <= ((pointer[1] - p3[1]) * (p4[0] - p3[0])) / (p4[1] - p3[1]) + p3[0]) lineCrossCount++;
+ }
+ if (Math.min(p4[1], p1[1]) <= pointer[1] && pointer[1] <= Math.max(p4[1], p1[1])) {
+ if (pointer[0] <= ((pointer[1] - p4[1]) * (p1[0] - p4[0])) / (p1[1] - p4[1]) + p4[0]) lineCrossCount++;
+ }
+ if (lineCrossCount % 2 != 0) {
+ // inside the slice of it crosses an odd number of edges
+ var showSelected = this.byCategory ? pieDataSet[index] : this._pieChartData[index];
+ if (changeSelectedVariables) {
+ // for when a bar is selected - not just hovered over
+ sameAsCurrent = this._currSelected
+ ? showSelected[Object.keys(showSelected)[0]] == this._currSelected![Object.keys(showSelected)[0]] && showSelected[Object.keys(showSelected)[1]] == this._currSelected![Object.keys(showSelected)[1]]
+ : this._currSelected === showSelected;
+ this._currSelected = sameAsCurrent ? undefined : showSelected;
+ this.selectedData = sameAsCurrent ? undefined : d;
+ } else this.hoverOverData = d;
+ return true;
+ }
+ return false;
+ });
+ if (changeSelectedVariables) {
+ if (sameAsCurrent!) this.curSliceSelected = undefined;
+ else this.curSliceSelected = selected;
+ }
+ };
+
+ // draws the pie chart
+ drawChart = (dataSet: any, width: number, height: number) => {
+ d3.select(this._piechartRef.current).select('svg').remove();
+ d3.select(this._piechartRef.current).select('.tooltip').remove();
+
+ var percentField = Object.keys(dataSet[0])[0];
+ var descriptionField = Object.keys(dataSet[0])[1]!;
+ var radius = Math.min(width, height - this.props.margin.top - this.props.margin.bottom) / 2;
+
+ // converts data into Objects
+ var data = this.data(dataSet);
+ var pieDataSet = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || Number.isNaN(d[key])));
+ if (this.byCategory) {
+ let uniqueCategories = [...new Set(data)];
+ var pieStringDataSet: { frequency: number }[] = [];
+ for (let i = 0; i < uniqueCategories.length; i++) {
+ pieStringDataSet.push({ frequency: 0, [percentField]: uniqueCategories[i] });
+ }
+ for (let i = 0; i < data.length; i++) {
+ let sliceData = pieStringDataSet.filter((each: any) => each[percentField] == data[i]);
+ sliceData[0].frequency = sliceData[0].frequency + 1;
+ }
+ pieDataSet = pieStringDataSet;
+ percentField = Object.keys(pieDataSet[0])[0];
+ descriptionField = Object.keys(pieDataSet[0])[1]!;
+ data = this.data(pieStringDataSet);
+ }
+ var trackDuplicates: { [key: string]: any } = {};
+ data.forEach((eachData: any) => (!trackDuplicates[eachData] ? (trackDuplicates[eachData] = 0) : null));
+
+ // initial chart
+ var svg = (this._piechartSvg = d3
+ .select(this._piechartRef.current)
+ .append('svg')
+ .attr('class', 'graph')
+ .attr('width', width + this.props.margin.right + this.props.margin.left)
+ .attr('height', height + this.props.margin.top + this.props.margin.bottom)
+ .append('g'));
+ let g = svg.append('g').attr('transform', 'translate(' + (width / 2 + this.props.margin.left) + ',' + height / 2 + ')');
+ var pie = d3.pie();
+ var arc = d3.arc().innerRadius(0).outerRadius(radius);
+
+ // click/hover
+ const onPointClick = action((e: any) => this.highlightSelectedSlice(true, svg, arc, radius, d3.pointer(e), pieDataSet));
+ const onHover = action((e: any) => {
+ this.highlightSelectedSlice(false, svg, arc, radius, d3.pointer(e), pieDataSet);
+ updateHighlights();
+ });
+ const mouseOut = action((e: any) => {
+ this.hoverOverData = undefined;
+ updateHighlights();
+ });
+ const updateHighlights = () => {
+ const hoverOverSlice = this.hoverOverData;
+ const selectedData = this.selectedData;
+ svg.selectAll('path').attr('class', function (d: any) {
+ return (selectedData && d.startAngle == selectedData.startAngle && d.endAngle == selectedData.endAngle) || (hoverOverSlice && d.startAngle == hoverOverSlice.startAngle && d.endAngle == hoverOverSlice.endAngle)
+ ? 'slice hover'
+ : 'slice';
+ });
+ };
+
+ // drawing the slices
+ var selected = this.selectedData;
+ var arcs = g.selectAll('arc').data(pie(data)).enter().append('g');
+ const possibleDataPointVals: { [x: string]: any }[] = [];
+ pieDataSet.forEach((each: { [x: string]: any | { valueOf(): number } }) => {
+ var dataPointVal: { [x: string]: any } = {};
+ dataPointVal[percentField] = each[percentField];
+ if (descriptionField) dataPointVal[descriptionField] = each[descriptionField];
+ try {
+ dataPointVal[percentField] = Number(dataPointVal[percentField].replace(/\$/g, '').replace(/\%/g, '').replace(/\#/g, '').replace(/\</g, ''));
+ } catch (error) {}
+ possibleDataPointVals.push(dataPointVal);
+ });
+ const sliceColors = StrListCast(this.props.layoutDoc.dataViz_pie_sliceColors).map(each => each.split('::'));
+ arcs.append('path')
+ .attr('fill', (d, i) => {
+ var dataPoint;
+ const possibleDataPoints = possibleDataPointVals.filter((pval: any) => pval[percentField] === Number(d.data));
+ if (possibleDataPoints.length == 1) dataPoint = possibleDataPoints[0];
+ else {
+ dataPoint = possibleDataPoints[trackDuplicates[d.data.toString()]];
+ trackDuplicates[d.data.toString()] = trackDuplicates[d.data.toString()] + 1;
+ }
+ var sliceColor;
+ if (dataPoint) {
+ const sliceTitle = dataPoint[this.props.axes[0]];
+ const accessByName = StrCast(sliceTitle) ? StrCast(sliceTitle).replace(/\$/g, '').replace(/\%/g, '').replace(/\#/g, '').replace(/\</g, '') : sliceTitle;
+ sliceColors.forEach(each => each[0] == accessByName && (sliceColor = each[1]));
+ }
+ return sliceColor ? StrCast(sliceColor) : d3.schemeSet3[i] ? d3.schemeSet3[i] : d3.schemeSet3[i % d3.schemeSet3.length];
+ })
+ .attr(
+ 'class',
+ selected
+ ? function (d) {
+ return selected && d.startAngle == selected.startAngle && d.endAngle == selected.endAngle ? 'slice hover' : 'slice';
+ }
+ : function (d) {
+ return 'slice';
+ }
+ )
+ .attr('d', arc)
+ .on('click', onPointClick)
+ .on('mouseover', onHover)
+ .on('mouseout', mouseOut);
+
+ // adding labels
+ trackDuplicates = {};
+ data.forEach((eachData: any) => (!trackDuplicates[eachData] ? (trackDuplicates[eachData] = 0) : null));
+ arcs.size() < 100 &&
+ arcs
+ .append('text')
+ .attr('transform', function (d) {
+ var centroid = arc.centroid(d as unknown as d3.DefaultArcObject);
+ var heightOffset = (centroid[1] / radius) * Math.abs(centroid[1]);
+ return 'translate(' + (centroid[0] + centroid[0] / (radius * 0.02)) + ',' + (centroid[1] + heightOffset) + ')';
+ })
+ .attr('text-anchor', 'middle')
+ .text(function (d) {
+ var dataPoint;
+ const possibleDataPoints = possibleDataPointVals.filter((pval: any) => pval[percentField] === Number(d.data));
+ if (possibleDataPoints.length == 1) dataPoint = pieDataSet[possibleDataPointVals.indexOf(possibleDataPoints[0])];
+ else {
+ dataPoint = pieDataSet[possibleDataPointVals.indexOf(possibleDataPoints[trackDuplicates[d.data.toString()]])];
+ trackDuplicates[d.data.toString()] = trackDuplicates[d.data.toString()] + 1;
+ }
+ return dataPoint ? dataPoint[percentField]! + (!descriptionField ? '' : ' - ' + dataPoint[descriptionField])! : '';
+ });
+ };
+
+ @action changeSelectedColor = (color: string) => {
+ this.curSliceSelected.attr('fill', color);
+ const sliceTitle = this._currSelected[this.props.axes[0]];
+ const sliceName = StrCast(sliceTitle) ? StrCast(sliceTitle).replace(/\$/g, '').replace(/\%/g, '').replace(/\#/g, '').replace(/\</g, '') : sliceTitle;
+
+ const sliceColors = Cast(this.props.layoutDoc.dataViz_pie_sliceColors, listSpec('string'), null);
+ sliceColors.map(each => {
+ if (each.split('::')[0] == sliceName) sliceColors.splice(sliceColors.indexOf(each), 1);
+ });
+ sliceColors.push(StrCast(sliceName + '::' + color));
+ };
+
+ @action changeHistogramCheckBox = () => {
+ this.props.layoutDoc.dataViz_pie_asHistogram = !this.props.layoutDoc.dataViz_pie_asHistogram;
+ this.drawChart(this._pieChartData, this.width, this.height);
+ };
+
+ render() {
+ var titleAccessor: any = '';
+ if (this.props.axes.length == 2) titleAccessor = 'dataViz_pie_title' + this.props.axes[0] + '-' + this.props.axes[1];
+ else if (this.props.axes.length > 0) titleAccessor = 'dataViz_pie_title' + this.props.axes[0];
+ if (!this.props.layoutDoc[titleAccessor]) this.props.layoutDoc[titleAccessor] = this.defaultGraphTitle;
+ if (!this.props.layoutDoc.dataViz_pie_sliceColors) this.props.layoutDoc.dataViz_pie_sliceColors = new List<string>();
+ var selected: string;
+ var curSelectedSliceName = '';
+ if (this._currSelected) {
+ const sliceTitle = this._currSelected[this.props.axes[0]];
+ curSelectedSliceName = StrCast(sliceTitle) ? StrCast(sliceTitle).replace(/\$/g, '').replace(/\%/g, '').replace(/\#/g, '').replace(/\</g, '') : sliceTitle;
+ selected = '{ ';
+ Object.keys(this._currSelected).map(key => {
+ key != '' ? (selected += key + ': ' + this._currSelected[key] + ', ') : '';
+ });
+ selected = selected.substring(0, selected.length - 2);
+ selected += ' }';
+ } else selected = 'none';
+ var selectedSliceColor;
+ var sliceColors = StrListCast(this.props.layoutDoc.dataViz_pie_sliceColors).map(each => each.split('::'));
+ sliceColors.forEach(each => {
+ if (each[0] == curSelectedSliceName!) selectedSliceColor = each[1];
+ });
+
+ if (this._pieChartData.length > 0 || !this.parentViz) {
+ return this.props.axes.length >= 1 ? (
+ <div className="chart-container">
+ <div className="graph-title">
+ <EditableText
+ val={StrCast(this.props.layoutDoc[titleAccessor])}
+ setVal={undoable(
+ action(val => (this.props.layoutDoc[titleAccessor] = val as string)),
+ 'Change Graph Title'
+ )}
+ color={'black'}
+ size={Size.LARGE}
+ fillWidth
+ />
+ </div>
+ {this.props.axes.length === 1 && /\d/.test(this.props.records[0][this.props.axes[0]]) ? (
+ <div className={'asHistogram-checkBox'} style={{ width: this.props.width }}>
+ <Checkbox color="primary" onChange={this.changeHistogramCheckBox} checked={this.props.layoutDoc.dataViz_pie_asHistogram as boolean} />
+ Organize data as histogram
+ </div>
+ ) : null}
+ <div ref={this._piechartRef} />
+ {selected != 'none' ? (
+ <div className={'selected-data'}>
+ Selected: {selected}
+ &nbsp; &nbsp;
+ <ColorPicker
+ tooltip={'Change Slice Color'}
+ type={Type.SEC}
+ icon={<FaFillDrip />}
+ selectedColor={selectedSliceColor ? selectedSliceColor : this.curSliceSelected.attr('fill')}
+ setFinalColor={undoable(color => this.changeSelectedColor(color), 'Change Selected Slice Color')}
+ setSelectedColor={undoable(color => this.changeSelectedColor(color), 'Change Selected Slice Color')}
+ size={Size.XSMALL}
+ />
+ </div>
+ ) : null}
+ </div>
+ ) : (
+ <span className="chart-container"> {'first use table view to select a column to graph'}</span>
+ );
+ } else
+ return (
+ // when it is a brushed table and the incoming table doesn't have any rows selected
+ <div className="chart-container">Selected rows of data from the incoming DataVizBox to display.</div>
+ );
+ }
+}
diff --git a/src/client/views/nodes/DataVizBox/components/TableBox.tsx b/src/client/views/nodes/DataVizBox/components/TableBox.tsx
index d84e34d52..4d6027ca4 100644
--- a/src/client/views/nodes/DataVizBox/components/TableBox.tsx
+++ b/src/client/views/nodes/DataVizBox/components/TableBox.tsx
@@ -1,42 +1,112 @@
-import { action, computed } from 'mobx';
+import { Button, Type } from 'browndash-components';
+import { action, computed, IReactionDisposer, reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { Doc } from '../../../../../fields/Doc';
-import { Id } from '../../../../../fields/FieldSymbols';
+import { Doc, Field, NumListCast } from '../../../../../fields/Doc';
import { List } from '../../../../../fields/List';
-import { emptyFunction, returnFalse, setupMoveUpEvents, Utils } from '../../../../../Utils';
+import { listSpec } from '../../../../../fields/Schema';
+import { Cast, DocCast } from '../../../../../fields/Types';
+import { emptyFunction, setupMoveUpEvents, Utils } from '../../../../../Utils';
import { DragManager } from '../../../../util/DragManager';
import { DocumentView } from '../../DocumentView';
import { DataVizView } from '../DataVizBox';
+import './Chart.scss';
interface TableBoxProps {
- pairs: { [key: string]: any }[];
+ rootDoc: Doc;
+ layoutDoc: Doc;
+ records: { [key: string]: any }[];
selectAxes: (axes: string[]) => void;
axes: string[];
+ width: number;
+ height: number;
+ margin: {
+ top: number;
+ right: number;
+ bottom: number;
+ left: number;
+ };
docView?: () => DocumentView | undefined;
}
@observer
export class TableBox extends React.Component<TableBoxProps> {
+ _inputChangedDisposer?: IReactionDisposer;
+ componentDidMount() {
+ // if the tableData changes (ie., when records are selected by the parent (input) visulization),
+ // then we need to remove any selected rows that are no longer part of the visualized dataset.
+ this._inputChangedDisposer = reaction(() => this._tableData.slice(), this.filterSelectedRowsDown, { fireImmediately: true });
+ }
+ componentWillUnmount() {
+ this._inputChangedDisposer?.();
+ }
+ @computed get _tableDataIds() {
+ return !this.parentViz ? this.props.records.map((rec, i) => i) : NumListCast(this.parentViz.dataViz_selectedRows);
+ }
+ // returns all the data records that will be rendered by only returning those records that have been selected by the parent visualization (or all records if there is no parent)
+ @computed get _tableData() {
+ return !this.parentViz ? this.props.records : this._tableDataIds.map(rowId => this.props.records[rowId]);
+ }
+
+ @computed get parentViz() {
+ return DocCast(this.props.rootDoc.dataViz_parentViz);
+ // return LinkManager.Instance.getAllRelatedLinks(this.props.rootDoc) // out of all links
+ // .filter(link => link.link_anchor_1 == this.props.rootDoc.dataViz_parentViz) // get links where this chart doc is the target of the link
+ // .map(link => DocCast(link.link_anchor_1)); // then return the source of the link
+ }
+
@computed get columns() {
- return this.props.pairs.length ? Array.from(Object.keys(this.props.pairs[0])) : [];
+ return this._tableData.length ? Array.from(Object.keys(this._tableData[0])).filter(header => header != '' && header != undefined) : [];
}
+
+ // updates the 'dataViz_selectedRows' and 'dataViz_highlightedRows' fields to no longer include rows that aren't in the table
+ filterSelectedRowsDown = () => {
+ const selected = NumListCast(this.props.layoutDoc.dataViz_selectedRows);
+ this.props.layoutDoc.dataViz_selectedRows = new List<number>(selected.filter(rowId => this._tableDataIds.includes(rowId))); // filters through selected to remove guids that were removed in the incoming data
+ const highlighted = NumListCast(this.props.layoutDoc.dataViz_highlitedRows);
+ this.props.layoutDoc.dataViz_highlitedRows = new List<number>(highlighted.filter(rowId => this._tableDataIds.includes(rowId))); // filters through highlighted to remove guids that were removed in the incoming data
+ };
+
render() {
- 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 (
+ if (this._tableData.length > 0) {
+ return (
+ <div
+ className="tableBox"
+ tabIndex={0}
+ onKeyDown={e => {
+ if (this.props.layoutDoc && e.key === 'a' && (e.ctrlKey || e.metaKey)) {
+ e.stopPropagation();
+ this.props.layoutDoc.dataViz_selectedRows = new List<number>(this._tableDataIds);
+ }
+ }}>
+ <div className="selectAll-buttons">
+ <Button onClick={action(() => (this.props.layoutDoc.dataViz_selectedRows = new List<number>(this._tableDataIds)))} text="Select All" type={Type.SEC} color={'black'} />
+ <Button onClick={action(() => (this.props.layoutDoc.dataViz_selectedRows = new List<number>()))} text="Deselect All" type={Type.SEC} color={'black'} />
+ </div>
+ <div
+ className="table-container"
+ style={{ height: this.props.height }}
+ ref={r =>
+ r?.addEventListener(
+ 'wheel', // if scrollTop is 0, then don't let wheel trigger scroll on any container (which it would since onScroll won't be triggered on this)
+ (e: WheelEvent) => {
+ if (!r.scrollTop && e.deltaY <= 0) e.preventDefault();
+ e.stopPropagation();
+ },
+ { passive: false }
+ )
+ }>
+ <table className="table">
+ <thead>
+ <tr className="table-row">
+ {this.columns.map(col => (
<th
- ref={header as any}
+ key={this.columns.indexOf(col)}
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',
+ color: this.props.axes.slice().reverse().lastElement() === col ? 'darkgreen' : this.props.axes.lastElement() === col ? 'darkred' : undefined,
+ background: this.props.axes.slice().reverse().lastElement() === col ? '#E3fbdb' : this.props.axes.lastElement() === col ? '#Fbdbdb' : undefined,
+ fontWeight: 'bolder',
+ border: '3px solid black',
}}
onPointerDown={e => {
const downX = e.clientX;
@@ -45,24 +115,37 @@ export class TableBox extends React.Component<TableBoxProps> {
{},
e,
e => {
+ // dragging off a column to create a brushed DataVizBox
const sourceAnchorCreator = () => this.props.docView?.()!.rootDoc!;
const targetCreator = (annotationOn: Doc | undefined) => {
const embedding = Doc.MakeEmbedding(this.props.docView?.()!.rootDoc!);
- embedding._dataVizView = DataVizView.LINECHART;
- embedding._data_vizAxes = new List<string>([col, col]);
+ embedding._dataViz = DataVizView.TABLE;
+ embedding._dataViz_axes = new List<string>([col, col]);
+ embedding._dataViz_parentViz = this.props.rootDoc;
embedding.annotationOn = annotationOn; //this.props.docView?.()!.rootDoc!;
+ embedding.histogramBarColors = Field.Copy(this.props.layoutDoc.histogramBarColors);
+ embedding.defaultHistogramColor = this.props.layoutDoc.defaultHistogramColor;
+ embedding.pieSliceColors = Field.Copy(this.props.layoutDoc.pieSliceColors);
return embedding;
};
if (this.props.docView?.() && !Utils.isClick(e.clientX, e.clientY, downX, downY, Date.now())) {
- DragManager.StartAnchorAnnoDrag([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;
- }
- },
- });
+ DragManager.StartAnchorAnnoDrag(
+ e.target instanceof HTMLElement ? [e.target] : [],
+ new DragManager.AnchorAnnoDragData(this.props.docView()!, sourceAnchorCreator, targetCreator),
+ downX,
+ downY,
+ {
+ dragComplete: e => {
+ if (!e.aborted && e.annoDragData && e.annoDragData.linkSourceDoc && e.annoDragData.dropDocument && e.linkDocument) {
+ e.linkDocument.link_displayLine = true;
+ e.linkDocument.link_matchEmbeddings = true;
+ e.linkDocument.link_displayArrow = true;
+ // e.annoDragData.linkSourceDoc.followLinkToggle = e.annoDragData.dropDocument.annotationOn === this.props.rootDoc;
+ // e.annoDragData.linkSourceDoc.followLinkZoom = false;
+ }
+ },
+ }
+ );
return true;
}
return false;
@@ -70,36 +153,66 @@ export class TableBox extends React.Component<TableBoxProps> {
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;
- }
+ if (newAxes.includes(col)) newAxes.splice(newAxes.indexOf(col), 1);
+ else if (newAxes.length > 1) newAxes[1] = col;
+ else newAxes.push(col);
this.props.selectAxes(newAxes);
})
);
}}>
{col}
</th>
- );
- })}
- </tr>
- </thead>
- <tbody>
- {this.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>
- );
+ </thead>
+ <tbody>
+ {this._tableDataIds
+ ?.map(rowId => ({ record: this.props.records[rowId], rowId }))
+ .map(({ record, rowId }) => (
+ <tr
+ key={rowId}
+ className="table-row"
+ onClick={action(e => {
+ const highlited = Cast(this.props.layoutDoc.dataViz_highlitedRows, listSpec('number'), null);
+ const selected = Cast(this.props.layoutDoc.dataViz_selectedRows, listSpec('number'), null);
+ if (e.metaKey) {
+ // highlighting a row
+ if (highlited?.includes(rowId)) highlited.splice(highlited.indexOf(rowId), 1);
+ else highlited?.push(rowId);
+ if (!selected?.includes(rowId)) selected?.push(rowId);
+ } else {
+ // selecting a row
+ if (selected?.includes(rowId)) {
+ if (highlited?.includes(rowId)) highlited.splice(highlited.indexOf(rowId), 1);
+ selected.splice(selected.indexOf(rowId), 1);
+ } else selected?.push(rowId);
+ }
+ e.stopPropagation();
+ })}
+ style={{
+ background: NumListCast(this.props.layoutDoc.dataViz_highlitedRows).includes(rowId) ? 'lightYellow' : NumListCast(this.props.layoutDoc.dataViz_selectedRows).includes(rowId) ? 'lightgrey' : '',
+ width: '110%',
+ }}>
+ {this.columns.map(col => {
+ // each cell
+ const colSelected = this.props.axes.length > 1 ? this.props.axes[0] == col || this.props.axes[1] == col : this.props.axes.length > 0 ? this.props.axes[0] == col : false;
+ return (
+ <td key={this.columns.indexOf(col)} style={{ border: colSelected ? '3px solid black' : '1px solid black', fontWeight: colSelected ? 'bolder' : 'normal' }}>
+ {record[col]}
+ </td>
+ );
+ })}
+ </tr>
+ ))}
+ </tbody>
+ </table>
+ </div>
+ </div>
+ );
+ } else
+ return (
+ // when it is a brushed table and the incoming table doesn't have any rows selected
+ <div className="chart-container">Selected rows of data from the incoming DataVizBox to display.</div>
+ );
}
}
diff --git a/src/client/views/nodes/DataVizBox/utils/D3Utils.ts b/src/client/views/nodes/DataVizBox/utils/D3Utils.ts
index e1ff6f8eb..10bfb0c64 100644
--- a/src/client/views/nodes/DataVizBox/utils/D3Utils.ts
+++ b/src/client/views/nodes/DataVizBox/utils/D3Utils.ts
@@ -34,7 +34,6 @@ export const createLineGenerator = (xScale: d3.ScaleLinear<number, number, never
};
export const xAxisCreator = (g: d3.Selection<SVGGElement, unknown, null, undefined>, height: number, xScale: d3.ScaleLinear<number, number, never>) => {
- console.log('x axis creator being called');
g.attr('class', 'x-axis').attr('transform', `translate(0,${height})`).call(d3.axisBottom(xScale).tickSize(15));
};
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index 2d8663c9c..8ac9d6804 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -47,6 +47,7 @@ import { VideoBox } from './VideoBox';
import { WebBox } from './WebBox';
import React = require('react');
import XRegExp = require('xregexp');
+import { MapPushpinBox } from './MapBox/MapPushpinBox';
const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
@@ -271,6 +272,7 @@ export class DocumentContentsView extends React.Component<
PhysicsSimulationBox,
SchemaRowBox,
ImportElementBox,
+ MapPushpinBox,
}}
bindings={bindings}
jsx={layoutFrame}
diff --git a/src/client/views/nodes/DocumentIcon.tsx b/src/client/views/nodes/DocumentIcon.tsx
index 6e2ed72b8..bccbd66e8 100644
--- a/src/client/views/nodes/DocumentIcon.tsx
+++ b/src/client/views/nodes/DocumentIcon.tsx
@@ -9,6 +9,7 @@ import { action, observable } from 'mobx';
import { Id } from '../../../fields/FieldSymbols';
import { factory } from 'typescript';
import { LightboxView } from '../LightboxView';
+import { SettingsManager } from '../../util/SettingsManager';
@observer
export class DocumentIcon extends React.Component<{ view: DocumentView; index: number }> {
@@ -29,6 +30,7 @@ export class DocumentIcon extends React.Component<{ view: DocumentView; index: n
pointerEvents: 'all',
opacity: this._hovered ? 0.3 : 1,
position: 'absolute',
+ background: SettingsManager.userBackgroundColor,
transform: `translate(${(left + right) / 2}px, ${top}px)`,
}}>
<Tooltip title={<>{this.props.view.rootDoc.title}</>}>
diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx
index 7723a088d..4db0bf5fa 100644
--- a/src/client/views/nodes/DocumentLinksButton.tsx
+++ b/src/client/views/nodes/DocumentLinksButton.tsx
@@ -18,10 +18,6 @@ import React = require('react');
import _ = require('lodash');
import { PinProps } from './trails';
-const higflyout = require('@hig/flyout');
-export const { anchorPoints } = higflyout;
-export const Flyout = higflyout.default;
-
interface DocumentLinksButtonProps {
View: DocumentView;
Bottom?: boolean;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 2990e2159..72c1a9806 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -4,7 +4,7 @@ import { observer } from 'mobx-react';
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 { AclPrivate, Animation, AudioPlay, DocData, Width } from '../../../fields/DocSymbols';
import { Id } from '../../../fields/FieldSymbols';
import { InkTool } from '../../../fields/InkField';
import { List } from '../../../fields/List';
@@ -28,6 +28,7 @@ import { FollowLinkScript } from '../../util/LinkFollower';
import { LinkManager } from '../../util/LinkManager';
import { ScriptingGlobals } from '../../util/ScriptingGlobals';
import { SelectionManager } from '../../util/SelectionManager';
+import { SettingsManager } from '../../util/SettingsManager';
import { SharingManager } from '../../util/SharingManager';
import { SnappingManager } from '../../util/SnappingManager';
import { Transform } from '../../util/Transform';
@@ -47,7 +48,6 @@ import { DocumentLinksButton } from './DocumentLinksButton';
import './DocumentView.scss';
import { FieldViewProps } from './FieldView';
import { FormattedTextBox } from './formattedText/FormattedTextBox';
-import { KeyValueBox } from './KeyValueBox';
import { LinkAnchorBox } from './LinkAnchorBox';
import { PresEffect, PresEffectDirection } from './trails';
import { PinProps, PresBox } from './trails/PresBox';
@@ -100,6 +100,7 @@ export interface DocFocusOptions {
effect?: Doc; // animation effect for focus
noSelect?: boolean; // whether target should be selected after focusing
playAudio?: boolean; // whether to play audio annotation on focus
+ playMedia?: boolean; // whether to play start target videos
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
@@ -113,13 +114,14 @@ export interface DocComponentView {
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;
+ brushView?: (view: { width: number; height: number; panX: number; panY: number }, transTime: number) => void;
getView?: (doc: Doc) => Promise<Opt<DocumentView>>; // returns a nested DocumentView for the specified doc or undefined
addDocTab?: (doc: Doc, where: OpenWhere) => boolean; // determines how to add a document - used in following links to open the target ina local lightbox
addDocument?: (doc: Doc | Doc[], annotationKey?: string) => boolean; // add a document (used only by collections)
reverseNativeScaling?: () => boolean; // DocumentView's setup screenToLocal based on the doc having a nativeWidth/Height. However, some content views (e.g., FreeFormView w/ fitContentsToBox set) may ignore the native dimensions so this flags the DocumentView to not do Nativre scaling.
shrinkWrap?: () => void; // requests a document to display all of its contents with no white space. currently only implemented (needed?) for freeform views
select?: (ctrlKey: boolean, shiftKey: boolean) => void;
+ focus?: (textAnchor: Doc, options: DocFocusOptions) => Opt<number>;
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
@@ -314,7 +316,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
@computed get titleHeight() {
return this.props?.styleProvider?.(this.layoutDoc, this.props, StyleProp.TitleHeight) || 0;
}
- get pointerEvents(): 'none' | 'all' | 'visiblePainted' | undefined {
+ @computed get pointerEvents(): 'none' | 'all' | 'visiblePainted' | undefined {
return this.props.styleProvider?.(this.Document, this.props, StyleProp.PointerEvents + (this.props.isSelected() ? ':selected' : ''));
}
@computed get finalLayoutKey() {
@@ -428,7 +430,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
if (!this.Document.ignoreClick && this.pointerEvents !== 'none' && this.props.renderDepth >= 0 && Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, this._downTime)) {
let stopPropagate = true;
let preventDefault = true;
- (this.rootDoc._raiseWhenDragged === undefined ? DragManager.GetRaiseWhenDragged() : this.rootDoc._raiseWhenDragged) && this.props.bringToFront(this.rootDoc);
+ !this.rootDoc._keepZWhenDragged && this.props.bringToFront(this.rootDoc);
if (this._doubleTap) {
const defaultDblclick = this.props.defaultDoubleClick?.() || this.Document.defaultDoubleClick;
if (this.onDoubleClickHandler?.script) {
@@ -629,7 +631,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
const linkdrag = de.complete.annoDragData ?? de.complete.linkDragData;
if (linkdrag) {
linkdrag.linkSourceDoc = linkdrag.linkSourceGetAnchor();
- if (linkdrag.linkSourceDoc) {
+ if (linkdrag.linkSourceDoc && linkdrag.linkSourceDoc !== this.rootDoc) {
if (de.complete.annoDragData && !de.complete.annoDragData.dropDocument) {
de.complete.annoDragData.dropDocument = de.complete.annoDragData.dropDocCreator(undefined);
}
@@ -704,7 +706,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
if (e && !(e.nativeEvent as any).dash) {
const onDisplay = () => {
- DocumentViewInternal.SelectAfterContextMenu && !this.props.isSelected(true) && SelectionManager.SelectView(this.props.DocumentView(), false); // on a mac, the context menu is triggered on mouse down, but a YouTube video becaomes interactive when selected which means that the context menu won't show up. by delaying the selection until hopefully after the pointer up, the context menu will appear.
+ if (this.rootDoc.type !== DocumentType.MAP) DocumentViewInternal.SelectAfterContextMenu && !this.props.isSelected(true) && SelectionManager.SelectView(this.props.DocumentView(), false); // on a mac, the context menu is triggered on mouse down, but a YouTube video becaomes interactive when selected which means that the context menu won't show up. by delaying the selection until hopefully after the pointer up, the context menu will appear.
setTimeout(() => simulateMouseClick(document.elementFromPoint(e.clientX, e.clientY), e.clientX, e.clientY, e.screenX, e.screenY));
};
if (navigator.userAgent.includes('Macintosh')) {
@@ -744,11 +746,11 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
zorderItems.push({ description: 'Bring to Front', event: () => SelectionManager.Views().forEach(dv => dv.props.bringToFront(dv.rootDoc, false)), icon: 'arrow-up' });
zorderItems.push({ description: 'Send to Back', event: () => SelectionManager.Views().forEach(dv => dv.props.bringToFront(dv.rootDoc, true)), icon: 'arrow-down' });
zorderItems.push({
- description: this.rootDoc._raiseWhenDragged !== false ? 'Keep ZIndex when dragged' : 'Allow ZIndex to change when dragged',
- event: undoBatch(action(() => (this.rootDoc._raiseWhenDragged = this.rootDoc._raiseWhenDragged === undefined ? false : undefined))),
+ description: !this.rootDoc._keepZDragged ? 'Keep ZIndex when dragged' : 'Allow ZIndex to change when dragged',
+ event: undoBatch(action(() => (this.rootDoc._keepZWhenDragged = !this.rootDoc._keepZWhenDragged))),
icon: 'hand-point-up',
});
- !zorders && cm.addItem({ description: 'Z Order...', addDivider: true, noexpand: true, subitems: zorderItems, icon: 'compass' });
+ !zorders && cm.addItem({ description: 'Z Order...', addDivider: true, noexpand: true, subitems: zorderItems, icon: 'layer-group' });
}
onClicks.push({ description: 'Enter Portal', event: this.makeIntoPortal, icon: 'window-restore' });
@@ -761,10 +763,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
!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' });
+ !Doc.noviceMode && 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: 'Restore On Click default', 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' });
}
@@ -798,7 +800,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
!more && moreItems.length && cm.addItem({ description: 'More...', subitems: moreItems, icon: 'compass' });
}
const constantItems: ContextMenuProps[] = [];
- if (!Doc.IsSystem(this.rootDoc)) {
+ if (!Doc.IsSystem(this.rootDoc) && this.rootDoc._type_collection !== CollectionViewType.Docking) {
constantItems.push({ description: 'Zip Export', icon: 'download', event: async () => Doc.Zip(this.props.Document) });
(this.rootDoc._type_collection !== CollectionViewType.Docking || !Doc.noviceMode) && constantItems.push({ description: 'Share', event: () => SharingManager.Instance.open(this.props.DocumentView()), icon: 'users' });
if (this.props.removeDocument && Doc.ActiveDashboard !== this.props.Document) {
@@ -806,7 +808,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
constantItems.push({ description: 'Close', event: this.deleteClicked, icon: 'times' });
}
}
- constantItems.push({ description: 'Show Metadata', event: () => this.props.addDocTab(this.props.Document, (OpenWhere.addRight.toString() + 'KeyValue') as OpenWhere), icon: 'layer-group' });
+ constantItems.push({ description: 'Show Metadata', event: () => this.props.addDocTab(this.props.Document, (OpenWhere.addRight.toString() + 'KeyValue') as OpenWhere), icon: 'table-columns' });
cm.addItem({ description: 'General...', noexpand: false, subitems: constantItems, icon: 'question' });
const help = cm.findByDescription('Help...');
@@ -846,6 +848,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
documentationDescription = 'See text node documentation';
documentationLink = 'https://brown-dash.github.io/Dash-Documentation/documents/text/';
break;
+ case DocumentType.DATAVIZ:
+ documentationDescription = 'See DataViz node documentation';
+ documentationLink = 'https://brown-dash.github.io/Dash-Documentation/documents/dataViz/';
+ break;
}
// Add link to help documentation
if (!this.props.treeViewDoc && documentationDescription && documentationLink) {
@@ -862,28 +868,32 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
cm.displayMenu((e?.pageX || pageX || 0) - 15, (e?.pageY || pageY || 0) - 15);
};
- rootSelected = (outsideReaction?: boolean) => this.props.isSelected(outsideReaction) || (this.props.Document.rootDocument && this.props.rootSelected?.(outsideReaction)) || false;
+ @computed get _rootSelected() {
+ return this.props.isSelected(false) || (this.props.Document.rootDocument && this.props.rootSelected?.(false)) || false;
+ }
+ rootSelected = (outsideReaction?: boolean) => this._rootSelected;
panelHeight = () => this.props.PanelHeight() - this.headerMargin;
screenToLocal = () => this.props.ScreenToLocalTransform().translate(0, -this.headerMargin);
onClickFunc: any = () => (this.disableClickScriptFunc ? undefined : this.onClickHandler);
setHeight = (height: number) => (this.layoutDoc._height = height);
setContentView = action((view: { getAnchor?: (addAsAnnotation: boolean) => Doc; forward?: () => boolean; back?: () => boolean }) => (this._componentView = view));
- isContentActive = (outsideReaction?: boolean): boolean | undefined => {
+ @computed get _isContentActive() {
// true - if the document has been activated directly or indirectly (by having its children selected)
// false - if its pointer events are explicitly turned off or if it's container tells it that it's inactive
// undefined - it is not active, but it should be responsive to actions that might active it or its contents (eg clicking)
- return this.props.isContentActive() === false || this.props.pointerEvents?.() === 'none' || (this.rootDoc.pointerEvents === 'none' && !StrCast(this.props.LayoutTemplateString).includes(KeyValueBox.name))
+ return this.props.isContentActive() === false || this.props.pointerEvents?.() === 'none'
? false
: Doc.ActiveTool !== InkTool.None || SnappingManager.GetIsDragging() || this.rootSelected() || this.rootDoc.forceActive || this._componentView?.isAnyChildContentActive?.() || this.props.isContentActive()
? true
: undefined;
- };
+ }
+ isContentActive = (): boolean | undefined => this._isContentActive;
@observable _retryThumb = 1;
- thumbShown = () => {
- const childHighlighted = () =>
- Array.from(Doc.highlightedDocs.keys())
- .concat(Array.from(Doc.brushManager.BrushedDoc.keys()))
- .some(doc => Doc.AreProtosEqual(DocCast(doc.annotationOn), this.rootDoc));
+ @computed get _thumbShown() {
+ const childHighlighted = () => false;
+ // Array.from(Doc.highlightedDocs.keys())
+ // .concat(Array.from(Doc.brushManager.BrushedDoc.keys()))
+ // .some(doc => Doc.AreProtosEqual(DocCast(doc.annotationOn), this.rootDoc));
const childOverlayed = () => Array.from(DocumentManager._overlayViews).some(view => Doc.AreProtosEqual(view.rootDoc, this.rootDoc));
return !this.props.LayoutTemplateString &&
!this.isContentActive() &&
@@ -893,12 +903,15 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
((!childHighlighted() && !childOverlayed() && !Doc.isBrushedHighlightedDegree(this.rootDoc)) || this.rootDoc._type_collection === CollectionViewType.Docking)
? true
: false;
- };
+ }
+ thumbShown = () => this._thumbShown;
childFilters = () => [...this.props.childFilters(), ...StrListCast(this.layoutDoc.childFilters)];
/// disable pointer events on content when there's an enabled onClick script (but not the browse script) and the contents aren't forced active, or if contents are marked inactive
- contentPointerEvents = () => ((!this.disableClickScriptFunc && this.onClickHandler && !this.props.onBrowseClick?.() && this.isContentActive() !== true) || this.isContentActive() === false ? 'none' : this.pointerEvents);
-
+ @computed get _contentPointerEvents() {
+ return (!this.disableClickScriptFunc && this.onClickHandler && !this.props.onBrowseClick?.() && this.isContentActive() !== true) || this.isContentActive() === false ? 'none' : this.pointerEvents;
+ }
+ contentPointerEvents = () => this._contentPointerEvents;
@computed get contents() {
TraceMobx();
const isInk = StrCast(this.layoutDoc.layout).includes(InkingStroke.name) && !this.props.LayoutTemplateString;
@@ -954,6 +967,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
case StyleProp.ShowTitle: return '';
case StyleProp.PointerEvents: return 'none';
case StyleProp.Highlighting: return undefined;
+ case StyleProp.Opacity: {
+ const filtered = DocUtils.FilterDocs(this.directLinks, this.props.childFilters?.() ?? [], []).filter(d => d.link_displayLine || Doc.UserDoc().showLinkLines);
+ return filtered.some(link => link._link_displayArrow) ? 0 : undefined;
+ }
}
return this.props.styleProvider?.(doc, props, property);
};
@@ -967,8 +984,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
TraceMobx();
return LinkManager.Instance.getAllRelatedLinks(this.rootDoc).filter(
link =>
- Doc.AreProtosEqual(link.link_anchor_1 as Doc, this.rootDoc) ||
- Doc.AreProtosEqual(link.link_anchor_2 as Doc, this.rootDoc) ||
+ (link.link_matchEmbeddings ? link.link_anchor_1 === this.rootDoc : Doc.AreProtosEqual(link.link_anchor_1 as Doc, this.rootDoc)) ||
+ (link.link_matchEmbeddings ? link.link_anchor_2 === 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))
);
@@ -982,7 +999,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
// the small blue dots that mark the endpoints of links
TraceMobx();
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);
+ const filtered = DocUtils.FilterDocs(this.directLinks, this.props.childFilters?.() ?? [], []).filter(d => d.link_displayLine || Doc.UserDoc().showLinkLines);
return filtered.map(link => (
<div className="documentView-anchorCont" key={link[Id]}>
<DocumentView
@@ -1014,9 +1031,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
audio: true,
})
.then(function (stream) {
- let audioTextAnnos = Cast(dataDoc[field + '-audioAnnotations-text'], listSpec('string'), null);
+ let audioTextAnnos = Cast(dataDoc[field + '_audioAnnotations_text'], listSpec('string'), null);
if (audioTextAnnos) audioTextAnnos.push('');
- else audioTextAnnos = dataDoc[field + '-audioAnnotations-text'] = new List<string>(['']);
+ else audioTextAnnos = dataDoc[field + '_audioAnnotations_text'] = new List<string>(['']);
DictationManager.Controls.listen({
interimHandler: value => (audioTextAnnos[audioTextAnnos.length - 1] = value),
continuous: { indefinite: false },
@@ -1033,15 +1050,15 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
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);
+ const audioAnnos = Cast(dataDoc[field + '_audioAnnotations'], listSpec(AudioField), null);
if (audioAnnos === undefined) {
- dataDoc[field + '-audioAnnotations'] = new List([audioField]);
+ dataDoc[field + '_audioAnnotations'] = new List([audioField]);
} else {
audioAnnos.push(audioField);
}
}
};
- runInAction(() => (dataDoc.audioAnnoState = 'recording'));
+ //runInAction(() => (dataDoc.audioAnnoState = 'recording'));
recorder.start();
const stopFunc = () => {
recorder.stop();
@@ -1056,18 +1073,26 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
playAnnotation = () => {
const self = this;
const audioAnnoState = this.dataDoc.audioAnnoState ?? 'stopped';
- const audioAnnos = Cast(this.dataDoc[this.LayoutFieldKey + '-audioAnnotations'], listSpec(AudioField), null);
+ 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';
+ if (anno instanceof AudioField) {
+ switch (audioAnnoState) {
+ case 'stopped':
+ this.dataDoc[AudioPlay] = 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';
+ break;
+ case 'playing':
+ this.dataDoc[AudioPlay]?.stop();
+ this.dataDoc.audioAnnoState = 'stopped';
+ break;
+ }
}
};
@@ -1081,9 +1106,11 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
<div
className="documentView-captionWrapper"
style={{
- pointerEvents: this.Document.ignoreClick ? 'none' : this.isContentActive() || this.props.isDocumentActive?.() ? 'all' : undefined,
+ pointerEvents: this.rootDoc.ignoreClick ? 'none' : this.isContentActive() || this.props.isDocumentActive?.() ? 'all' : undefined,
minWidth: 50 * ffscale(),
maxHeight: `max(100%, ${20 * ffscale()}px)`,
+ background: StrCast(this.layoutDoc._backgroundColor, 'rgba(0,0,0,0.2)'),
+ color: lightOrDark(StrCast(this.layoutDoc._backgroundColor, 'black')),
}}>
<FormattedTextBox
{...this.props}
@@ -1101,10 +1128,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
</div>
);
const targetDoc = showTitle?.startsWith('_') ? this.layoutDoc : this.rootDoc;
- const background = StrCast(
- SharingManager.Instance.users.find(u => u.user.email === this.dataDoc.author)?.sharingDoc.headingColor,
- Doc.UserDoc().layout_showTitle && [DocumentType.RTF, DocumentType.COL].includes(this.rootDoc.type as any) ? StrCast(Doc.SharingDoc().headingColor) : 'rgba(0,0,0,0.4)'
- );
+ const background = StrCast(SharingManager.Instance.users.find(u => u.user.email === this.dataDoc.author)?.sharingDoc.headingColor, StrCast(Doc.SharingDoc().headingColor, SettingsManager.userBackgroundColor));
const sidebarWidthPercent = +StrCast(this.layoutDoc.layout_sidebarWidthPercent).replace('%', '');
const titleView = !showTitle ? null : (
<div
@@ -1114,7 +1138,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
position: this.headerMargin ? 'relative' : 'absolute',
height: this.titleHeight,
width: !this.headerMargin ? `calc(${sidebarWidthPercent || 100}% - 18px)` : (sidebarWidthPercent || 100) + '%', // leave room for annotation button
- color: lightOrDark(background),
+ color: background === 'transparent' ? SettingsManager.userColor : lightOrDark(background),
background,
pointerEvents: (!this.disableClickScriptFunc && this.onClickHandler) || this.Document.ignoreClick ? 'none' : this.isContentActive() || this.props.isDocumentActive?.() ? 'all' : undefined,
}}>
@@ -1128,14 +1152,13 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
display={'block'}
fontSize={10}
GetValue={() => {
- this.props.select(false);
return showTitle.split(';').length === 1 ? showTitle + '=' + Field.toString(targetDoc[showTitle.split(';')[0]] as any as Field) : '#' + showTitle;
}}
SetValue={undoBatch((input: string) => {
if (input?.startsWith('#')) {
- if (this.props.layout_showTitle) {
+ if (this.rootDoc.layout_showTitle) {
this.rootDoc._layout_showTitle = input?.substring(1) ? input.substring(1) : undefined;
- } else {
+ } else if (!this.props.layout_showTitle) {
Doc.UserDoc().layout_showTitle = input?.substring(1) ? input.substring(1) : 'author_date';
}
} else {
@@ -1193,7 +1216,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
* @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 dir = presEffectDoc?.presentation_effectDirection ?? presEffectDoc?.followLinkAnimDirection;
const effectProps = {
left: dir === PresEffectDirection.Left,
right: dir === PresEffectDirection.Right,
@@ -1201,10 +1224,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
bottom: dir === PresEffectDirection.Bottom,
opposite: true,
delay: 0,
- duration: Cast(presEffectDoc?.presTransition, 'number', Cast(presEffectDoc?.followLinkTransitionTime, 'number', null)),
+ duration: Cast(presEffectDoc?.presentation_transition, 'number', Cast(presEffectDoc?.followLinkTransitionTime, 'number', null)),
};
//prettier-ignore
- switch (StrCast(presEffectDoc?.presEffect, StrCast(presEffectDoc?.followLinkAnimEffect))) {
+ switch (StrCast(presEffectDoc?.presentation_effect, StrCast(presEffectDoc?.followLinkAnimEffect))) {
default:
case PresEffect.None: return renderDoc;
case PresEffect.Zoom: return <Zoom {...effectProps}>{renderDoc}</Zoom>;
@@ -1216,16 +1239,22 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
case PresEffect.Lightspeed: return <LightSpeed {...effectProps}>{renderDoc}</LightSpeed>;
}
}
+ @computed get highlighting() {
+ return this.props.styleProvider?.(this.props.Document, this.props, StyleProp.Highlighting);
+ }
+ @computed get borderPath() {
+ return this.props.styleProvider?.(this.props.Document, this.props, StyleProp.BorderPath);
+ }
render() {
TraceMobx();
- const highlighting = this.props.styleProvider?.(this.props.Document, this.props, StyleProp.Highlighting);
- const borderPath = this.props.styleProvider?.(this.props.Document, this.props, StyleProp.BorderPath);
+ const highlighting = this.highlighting;
+ const borderPath = this.borderPath;
const boxShadow =
this.props.treeViewDoc || !highlighting
? this.boxShadow
: highlighting && this.borderRounding && highlighting.highlightStyle !== 'dashed'
? `0 0 0 ${highlighting.highlightIndex}px ${highlighting.highlightColor}`
- : this.boxShadow || (this.props.Document.isTemplateForField ? 'black 0.2vw 0.2vw 0.8vw' : undefined);
+ : this.boxShadow || (this.rootDoc.isTemplateForField ? 'black 0.2vw 0.2vw 0.8vw' : undefined);
const renderDoc = this.renderDoc({
borderRadius: this.borderRounding,
outline: highlighting && !this.borderRounding && !highlighting.highlightStroke ? `${highlighting.highlightColor} ${highlighting.highlightStyle} ${highlighting.highlightIndex}px` : 'solid 0px',
@@ -1241,9 +1270,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
onContextMenu={this.onContextMenu}
onPointerDown={this.onPointerDown}
onClick={this.onClick}
- onPointerEnter={e => (!SnappingManager.GetIsDragging() || DragManager.CanEmbed) && Doc.BrushDoc(this.props.Document)}
- onPointerOver={e => (!SnappingManager.GetIsDragging() || DragManager.CanEmbed) && Doc.BrushDoc(this.props.Document)}
- onPointerLeave={e => !isParentOf(this.ContentDiv, document.elementFromPoint(e.nativeEvent.x, e.nativeEvent.y)) && Doc.UnBrushDoc(this.props.Document)}
+ onPointerEnter={e => (!SnappingManager.GetIsDragging() || DragManager.CanEmbed) && Doc.BrushDoc(this.rootDoc)}
+ onPointerOver={e => (!SnappingManager.GetIsDragging() || DragManager.CanEmbed) && Doc.BrushDoc(this.rootDoc)}
+ onPointerLeave={e => !isParentOf(this.ContentDiv, document.elementFromPoint(e.nativeEvent.x, e.nativeEvent.y)) && Doc.UnBrushDoc(this.rootDoc)}
style={{
borderRadius: this.borderRounding,
pointerEvents: this.pointerEvents === 'visiblePainted' ? 'none' : this.pointerEvents,
@@ -1260,7 +1289,13 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
@observer
export class DocumentView extends React.Component<DocumentViewProps> {
public static ROOT_DIV = 'documentView-effectsWrapper';
+ @observable public static Interacting = false;
@observable public static LongPress = false;
+ @observable public static ExploreMode = false;
+ @observable public static LastPressedSidebarBtn: Opt<Doc>; // bcz: this is a hack to handle highlighting buttons in the leftpanel menu .. need to find a cleaner approach
+ @computed public static get exploreMode() {
+ return () => (DocumentView.ExploreMode ? ScriptField.MakeScript('CollectionBrowseClick(documentView, clientX, clientY)', { documentView: 'any', clientX: 'number', clientY: 'number' })! : undefined);
+ }
@observable public docView: DocumentViewInternal | undefined | null;
@observable public textHtmlOverlay: Opt<string>;
@observable private _isHovering = false;
@@ -1343,7 +1378,7 @@ export class DocumentView extends React.Component<DocumentViewProps> {
return this.docView?._componentView;
}
get allLinks() {
- return this.docView?.allLinks || [];
+ return (this.docView?.allLinks || []).filter(link => !link.link_matchEmbeddings || link.link_anchor_1 === this.rootDoc || link.link_anchor_2 === this.rootDoc);
}
get LayoutFieldKey() {
return this.docView?.LayoutFieldKey || 'layout';
@@ -1526,7 +1561,7 @@ export class DocumentView extends React.Component<DocumentViewProps> {
<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,
+ { presentation_effect: this.htmlOverlayEffect ?? 'Zoom' } as any as Doc,
this.rootDoc
)}{' '}
</Fade>
diff --git a/src/client/views/nodes/EquationBox.scss b/src/client/views/nodes/EquationBox.scss
index 9714e1bd0..f5871db22 100644
--- a/src/client/views/nodes/EquationBox.scss
+++ b/src/client/views/nodes/EquationBox.scss
@@ -1,8 +1,9 @@
-@import "../global/globalCssVariables.scss";
+@import '../global/globalCssVariables.scss';
.equationBox-cont {
- transform-origin: top left;
+ transform-origin: center;
+ background-color: #e7e7e7;
> span {
- width: 100%;
+ width: 100%;
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
index da1b89200..394108be4 100644
--- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx
+++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
@@ -1,26 +1,26 @@
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Button, ColorPicker, Dropdown, DropdownType, EditableText, IconButton, IListItemProps, NumberDropdown, NumberDropdownType, Popup, Size, Toggle, ToggleType, Type } from 'browndash-components';
+import { Button, ColorPicker, Dropdown, DropdownType, EditableText, IconButton, IListItemProps, MultiToggle, NumberDropdown, NumberDropdownType, Popup, Size, Toggle, ToggleType, Type } from 'browndash-components';
import { action, computed, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { Doc, DocListCast, StrListCast } from '../../../../fields/Doc';
import { ScriptField } from '../../../../fields/ScriptField';
-import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
+import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
+import { emptyFunction, setupMoveUpEvents, Utils } from '../../../../Utils';
import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
import { SelectionManager } from '../../../util/SelectionManager';
+import { SettingsManager } from '../../../util/SettingsManager';
import { undoable, UndoManager } from '../../../util/UndoManager';
import { ContextMenu } from '../../ContextMenu';
import { DocComponent } from '../../DocComponent';
import { EditableView } from '../../EditableView';
-import { Colors } from '../../global/globalEnums';
+import { SelectedDocView } from '../../selectedDoc';
import { StyleProp } from '../../StyleProvider';
-import { FieldView, FieldViewProps } from '../FieldView';
import { OpenWhere } from '../DocumentView';
-import { RichTextMenu } from '../formattedText/RichTextMenu';
+import { FieldView, FieldViewProps } from '../FieldView';
import './FontIconBox.scss';
-import { SelectedDocView } from '../../selectedDoc';
-import { Utils } from '../../../../Utils';
+import TrailsIcon from './TrailsIcon';
export enum ButtonType {
TextButton = 'textBtn',
@@ -67,19 +67,6 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
}
};
- static GetShowLabels() {
- return BoolCast(Doc.UserDoc()._showLabel);
- }
- static SetShowLabels(show: boolean) {
- Doc.UserDoc()._showLabel = show;
- }
- static GetRecognizeGestures() {
- return BoolCast(Doc.UserDoc()._recognizeGestures);
- }
- static SetRecognizeGestures(show: boolean) {
- Doc.UserDoc()._recognizeGestures = show;
- }
-
// Determining UI Specs
@computed get label() {
return StrCast(this.rootDoc.icon_label, StrCast(this.rootDoc.title));
@@ -92,8 +79,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
else return null;
}
icon = StrCast(this.dataDoc[this.fieldKey ?? 'icon'] ?? this.dataDoc.icon, 'user') as any;
- const trailsIcon = () => <img src={`/assets/${'presTrails.png'}`} style={{ width: 30, height: 30, filter: `invert(${color === Colors.DARK_GRAY ? '0%' : '100%'})` }} />;
- return !icon ? null : icon === 'pres-trail' ? trailsIcon() : <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={icon} color={color} />;
+ return !icon ? null : icon === 'pres-trail' ? TrailsIcon(color) : <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={icon} color={color} />;
};
@computed get dropdown() {
return BoolCast(this.rootDoc.dropDownOpen);
@@ -140,11 +126,11 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color);
// Script for checking the outcome of the toggle
const checkResult = Number(Number(numScript().result ?? 0).toPrecision(NumCast(this.dataDoc.numPrecision, 3)));
- const label = !FontIconBox.GetShowLabels() ? null : <div className="fontIconBox-label">{this.label}</div>;
return (
<NumberDropdown
color={color}
+ background={SettingsManager.userBackgroundColor}
numberDropdownType={type}
showPlusMinus={false}
tooltip={this.label}
@@ -168,14 +154,14 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
return (
<div
className={`menuButton ${this.type} ${active}`}
- style={{ color: color, backgroundColor: backgroundColor, borderBottomLeftRadius: this.dropdown ? 0 : undefined }}
+ style={{ color, backgroundColor, borderBottomLeftRadius: this.dropdown ? 0 : undefined }}
onClick={action(() => {
this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen;
this.noTooltip = this.rootDoc.dropDownOpen;
Doc.UnBrushAllDocs();
})}>
{this.Icon(color)}
- {!this.label || !FontIconBox.GetShowLabels() ? null : (
+ {!this.label || !Doc.GetShowIconLabels() ? null : (
<div className="fontIconBox-label" style={{ color: color, backgroundColor: backgroundColor }}>
{' '}
{this.label}{' '}
@@ -189,64 +175,69 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
);
}
+ dropdownItemDown = (e: React.PointerEvent, value: string | number) => {
+ setupMoveUpEvents(
+ this,
+ e,
+ (e: PointerEvent) => {
+ return ScriptCast(this.rootDoc.onDragScript)?.script.run({ this: this.layoutDoc, self: this.rootDoc, value: { doc: value, e } }).result;
+ },
+ emptyFunction,
+ emptyFunction
+ );
+ return false;
+ };
+
/**
* Dropdown list
*/
@computed get dropdownListButton() {
- const active: string = StrCast(this.rootDoc.dropDownOpen);
- const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color);
- const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor);
-
const script = ScriptCast(this.rootDoc.script);
let noviceList: string[] = [];
let text: string | undefined;
- let dropdown = true;
let getStyle: (val: string) => any = () => {};
let icon: IconProp = 'caret-down';
- let isViewDropdown: boolean = script?.script.originalScript.startsWith('setView');
- try {
- if (isViewDropdown) {
- const selectedDocs: Doc[] = SelectionManager.Docs();
- const selected = SelectionManager.Docs().lastElement();
- console.log('selected');
- if (selected) {
- if (StrCast(selected.type) === DocumentType.COL) {
- text = StrCast(selected._type_collection);
- console.log('collection selected', text);
+ const isViewDropdown = script?.script.originalScript.startsWith('setView');
+ if (isViewDropdown) {
+ const selected = SelectionManager.Docs();
+ if (selected.lastElement()) {
+ if (StrCast(selected.lastElement().type) === DocumentType.COL) {
+ text = StrCast(selected.lastElement()._type_collection);
+ } else {
+ if (selected.length > 1) {
+ text = selected.length + ' selected';
} else {
- console.log('doc selected', selected.title);
- dropdown = false;
- if (selectedDocs.length > 1) {
- text = selectedDocs.length + ' selected';
- } else {
- text = Utils.cleanDocumentType(StrCast(selected.type) as DocumentType);
- icon = Doc.toIcon(selected);
- }
- return <Popup icon={<FontAwesomeIcon size={'1x'} icon={icon} />} text={text} type={Type.TERT} color={color} popup={<SelectedDocView selectedDocs={selectedDocs} />} fillWidth />;
+ text = Utils.cleanDocumentType(StrCast(selected.lastElement().type) as DocumentType);
+ icon = Doc.toIcon(selected.lastElement());
}
- } else {
- dropdown = false;
- return <Button text={`None Selected`} type={Type.TERT} color={color} fillWidth inactive />;
+ return (
+ <Popup
+ icon={<FontAwesomeIcon size={'1x'} icon={icon} />}
+ text={text}
+ type={Type.TERT}
+ color={SettingsManager.userColor}
+ background={SettingsManager.userVariantColor}
+ popup={<SelectedDocView selectedDocs={selected} />}
+ fillWidth
+ />
+ );
}
- noviceList = [CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Stacking, CollectionViewType.NoteTaking];
} else {
- text = StrCast((RichTextMenu.Instance?.TextView?.EditorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily);
- getStyle = (val: string) => {
- return { fontFamily: val };
- };
+ return <Button text="None Selected" type={Type.TERT} color={SettingsManager.userColor} background={SettingsManager.userVariantColor} fillWidth inactive />;
}
- } catch (e) {
- console.log(e);
+ noviceList = [CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Stacking, CollectionViewType.NoteTaking];
+ } else {
+ text = script?.script.run({ this: this.layoutDoc, self: this.rootDoc, value: '', _readOnly_: true }).result;
+ // text = StrCast((RichTextMenu.Instance?.TextView?.EditorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily);
+ getStyle = (val: string) => ({ fontFamily: val });
}
- console.log('current item: ', text);
-
// Get items to place into the list
const list: IListItemProps[] = this.buttonList
.filter(value => !Doc.noviceMode || !noviceList.length || noviceList.includes(value))
.map(value => ({
- text: value.charAt(0).toUpperCase() + value.slice(1),
+ text: typeof value === 'string' ? value.charAt(0).toUpperCase() + value.slice(1) : StrCast(DocCast(value)?.title),
val: value,
style: getStyle(value),
onClick: undoable(() => script.script.run({ this: this.layoutDoc, self: this.rootDoc, value }), value),
@@ -256,10 +247,13 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
return (
<Dropdown
selectedVal={text}
- setSelectedVal={undoable(val => script.script.run({ this: this.layoutDoc, self: this.rootDoc, val }), `dropdown select ${this.label}`)}
- color={color}
- type={isViewDropdown ? Type.TERT : Type.PRIM}
+ setSelectedVal={undoable(value => script.script.run({ this: this.layoutDoc, self: this.rootDoc, value }), `dropdown select ${this.label}`)}
+ color={SettingsManager.userColor}
+ background={SettingsManager.userVariantColor}
+ type={Type.TERT}
+ closeOnSelect={false}
dropdownType={DropdownType.SELECT}
+ onItemDown={this.dropdownItemDown}
items={list}
tooltip={this.label}
fillWidth
@@ -271,30 +265,66 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
return ScriptCast(this.rootDoc.script);
}
+ colorBatch: UndoManager.Batch | undefined;
/**
* Color button
*/
@computed get colorButton() {
const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color);
- const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor);
const curColor = this.colorScript?.script.run({ this: this.layoutDoc, self: this.rootDoc, value: undefined, _readOnly_: true }).result ?? 'transparent';
const tooltip: string = StrCast(this.rootDoc.toolTip);
return (
<ColorPicker
setSelectedColor={value => {
- const s = this.colorScript;
- s && undoable(() => s.script.run({ this: this.layoutDoc, self: this.rootDoc, value: value, _readOnly_: false }).result, `Set ${tooltip} to ${value}`)();
+ if (!this.colorBatch) this.colorBatch = UndoManager.StartBatch(`Set ${tooltip} color`);
+ this.colorScript?.script.run({ this: this.layoutDoc, self: this.rootDoc, value: value, _readOnly_: false });
+ }}
+ setFinalColor={value => {
+ this.colorScript?.script.run({ this: this.layoutDoc, self: this.rootDoc, value: value, _readOnly_: false });
+ this.colorBatch?.end();
+ this.colorBatch = undefined;
}}
+ defaultPickerType="Classic"
selectedColor={curColor}
type={Type.PRIM}
color={color}
+ background={SettingsManager.userBackgroundColor}
icon={this.Icon(color)!}
tooltip={tooltip}
label={this.label}
/>
);
}
+ @computed get multiToggleButton() {
+ // Determine the type of toggle button
+ const tooltip: string = StrCast(this.rootDoc.toolTip);
+
+ const script = ScriptCast(this.rootDoc.onClick);
+ const toggleStatus = script ? script.script.run({ this: this.layoutDoc, self: this.rootDoc, value: undefined, _readOnly_: true }).result : false;
+ // Colors
+ const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color);
+ const items = DocListCast(this.rootDoc.data);
+ return (
+ <MultiToggle
+ tooltip={`Toggle ${tooltip}`}
+ type={Type.PRIM}
+ color={color}
+ background={SettingsManager.userBackgroundColor}
+ label={this.label}
+ items={DocListCast(this.rootDoc.data).map(item => ({
+ icon: <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={StrCast(item.icon) as any} color={color} />,
+ tooltip: StrCast(item.toolTip),
+ val: StrCast(item.toolType),
+ }))}
+ selectedVal={StrCast(items.find(itemDoc => ScriptCast(itemDoc.onClick).script.run({ this: itemDoc, self: itemDoc, value: undefined, _readOnly_: true }).result)?.toolType)}
+ setSelectedVal={(val: string | number) => {
+ const itemDoc = items.find(item => item.toolType === val);
+ itemDoc && ScriptCast(itemDoc.onClick).script.run({ this: itemDoc, self: itemDoc, value: val, _readOnly_: false });
+ }}
+ />
+ );
+ }
@computed get toggleButton() {
// Determine the type of toggle button
@@ -315,6 +345,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
toggleStatus={toggleStatus}
text={buttonText}
color={color}
+ //background={SettingsManager.userBackgroundColor}
icon={this.Icon(color)!}
label={this.label}
onPointerDown={() => script.script.run({ this: this.layoutDoc, self: this.rootDoc, value: !toggleStatus, _readOnly_: false })}
@@ -354,52 +385,28 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
}
render() {
- // determine dash button metadata
const color = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Color);
- const backgroundColor = this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor);
- const tooltip: string = StrCast(this.rootDoc.toolTip);
- const onClickScript = ScriptCast(this.rootDoc.onClick);
- const script = ScriptCast(this.rootDoc.script);
- // TODO:glr Add label of button type
- let button: JSX.Element = this.defaultButton;
+ const tooltip = StrCast(this.rootDoc.toolTip);
+ const scriptFunc = () => ScriptCast(this.rootDoc.onClick)?.script.run({ this: this.layoutDoc, self: this.rootDoc, _readOnly_: false });
+ const btnProps = { tooltip, icon: this.Icon(color)!, label: this.label };
// prettier-ignore
switch (this.type) {
- case ButtonType.EditableText:
- button = this.editableText;
- break;
- case ButtonType.DropdownList:
- button = this.dropdownListButton;
- break;
- case ButtonType.ColorButton:
- button = this.colorButton;
- break;
case ButtonType.NumberDropdownButton:
case ButtonType.NumberInlineButton:
- case ButtonType.NumberSliderButton:
- button = this.numberDropdown;
- break;
- case ButtonType.DropdownButton:
- button = this.dropdownButton;
- break;
- case ButtonType.ToggleButton: button = this.toggleButton; break;
+ case ButtonType.NumberSliderButton: return this.numberDropdown;
+ case ButtonType.EditableText: return this.editableText;
+ case ButtonType.DropdownList: return this.dropdownListButton;
+ case ButtonType.ColorButton: return this.colorButton;
+ case ButtonType.DropdownButton: return this.dropdownButton;
+ case ButtonType.MultiToggleButton: return this.multiToggleButton;
+ case ButtonType.ToggleButton: return this.toggleButton;
case ButtonType.ClickButton:
- case ButtonType.ToolButton:
- button = (
- <IconButton tooltip={tooltip}
- color={color} icon={this.Icon(color)!} label={this.label}/>
- );
- break;
- case ButtonType.TextButton:
- button = (
- <Button tooltip={tooltip} icon={this.Icon(color)!} text={StrCast(this.rootDoc.buttonText)} label={this.label}/>
- );
- break;
- case ButtonType.MenuButton: button = (
- <IconButton tooltip={tooltip} onPointerDown={() => onClickScript?.script.run({ this: this.layoutDoc, self: this.rootDoc, _readOnly_: false })} tooltipPlacement='right' size={Size.LARGE} color={color} icon={this.Icon(color)!} label={this.label}/>
- );
- break;
+ case ButtonType.ToolButton: return <IconButton {...btnProps} size={Size.LARGE} color={color} />;
+ case ButtonType.TextButton: return <Button {...btnProps} color={color}
+ background={SettingsManager.userBackgroundColor} text={StrCast(this.rootDoc.buttonText)}/>;
+ case ButtonType.MenuButton: return <IconButton {...btnProps} color={color}
+ background={SettingsManager.userBackgroundColor} size={Size.LARGE} tooltipPlacement='right' onPointerDown={scriptFunc} />;
}
-
- return button;
+ return this.defaultButton;
}
}
diff --git a/src/client/views/nodes/FontIconBox/TrailsIcon.tsx b/src/client/views/nodes/FontIconBox/TrailsIcon.tsx
new file mode 100644
index 000000000..09fd6e3ae
--- /dev/null
+++ b/src/client/views/nodes/FontIconBox/TrailsIcon.tsx
@@ -0,0 +1,81 @@
+import * as React from 'react';
+
+const TrailsIcon = (fill: string) => (
+ <svg version="1.0" xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 1080.000000 1080.000000" preserveAspectRatio="xMidYMid meet">
+ <g transform="translate(0.000000,1080.000000) scale(0.100000,-0.100000)" fill={fill} stroke="none">
+ <path
+ d="M665 9253 c-74 -10 -157 -38 -240 -81 -74 -37 -107 -63 -186 -141
+-104 -104 -156 -191 -201 -334 l-23 -72 0 -3215 c0 -3072 1 -3218 18 -3280 10
+-36 39 -108 64 -160 40 -82 59 -107 142 -190 81 -81 111 -103 191 -143 52 -26
+122 -55 155 -65 57 -16 322 -17 4775 -20 3250 -2 4736 1 4784 8 256 39 486
+220 588 462 63 148 59 -96 56 3413 -3 3049 -4 3203 -21 3260 -78 260 -285 467
+-542 542 -57 17 -308 18 -4795 19 -2604 1 -4748 -1 -4765 -3z m9187 -787 c65
+-19 114 -60 143 -120 l25 -51 0 -2898 c0 -2582 -2 -2901 -15 -2934 -24 -57
+-62 -101 -108 -126 l-42 -22 -4435 -3 c-3954 -2 -4440 0 -4481 13 -26 9 -63
+33 -87 56 -79 79 -72 -205 -72 3012 0 2156 3 2889 12 2918 20 70 91 136 168
+160 14 4 2010 8 4436 8 3710 1 4418 -1 4456 -13z"
+ />
+ <path
+ d="M7692 7839 c-46 -14 -109 -80 -122 -128 -7 -27 -9 -472 -8 -1443 l3
+-1403 24 -38 c13 -21 42 -50 64 -65 l41 -27 816 0 816 0 41 27 c22 15 51 44
+64 65 l24 38 0 1425 0 1425 -24 38 c-13 21 -42 50 -64 65 l-41 27 -800 2
+c-488 1 -814 -2 -834 -8z"
+ />
+ <path
+ d="M1982 7699 c-46 -14 -109 -80 -122 -128 -7 -27 -10 -308 -8 -893 l3
+-853 24 -38 c13 -21 42 -50 64 -65 l41 -27 1386 0 1386 0 41 27 c22 15 51 44
+64 65 l24 38 0 876 0 875 -27 41 c-15 22 -44 51 -65 64 l-38 24 -1370 2 c-847
+1 -1383 -2 -1403 -8z"
+ />
+ <path
+ d="M6413 7093 c-13 -2 -23 -9 -23 -15 0 -24 21 -307 26 -343 l5 -40 182
+-1 c200 -1 307 -15 484 -65 57 -16 107 -29 112 -29 5 0 36 75 69 168 33 92 63
+175 67 184 6 14 -10 22 -92 48 -126 39 -308 76 -447 89 -106 11 -337 13 -383
+4z"
+ />
+ <path
+ d="M5840 7033 c-63 -8 -238 -29 -388 -47 -150 -18 -274 -35 -276 -37 -2
+-2 8 -89 23 -194 22 -163 29 -190 44 -193 10 -2 91 6 180 17 89 12 258 32 376
+46 118 14 216 27 218 28 7 8 -43 391 -52 392 -5 1 -62 -4 -125 -12z"
+ />
+ <path
+ d="M4762 4789 c-46 -14 -109 -80 -122 -128 -7 -27 -10 -323 -8 -943 l3
+-903 24 -38 c13 -21 42 -50 64 -65 l41 -27 926 0 926 0 41 27 c22 15 51 44 64
+65 l24 38 0 926 0 925 -27 41 c-15 22 -44 51 -65 64 l-38 24 -910 2 c-557 1
+-923 -2 -943 -8z"
+ />
+ <path
+ d="M8487 4297 c-26 -215 -161 -474 -307 -585 -27 -20 -49 -40 -49 -44
+-1 -3 49 -79 110 -167 l110 -161 44 31 c176 126 333 350 418 594 30 86 77 282
+77 320 0 8 -57 19 -167 34 -93 13 -182 25 -199 28 -31 5 -31 5 -37 -50z"
+ />
+ <path
+ d="M3965 4233 c-106 -9 -348 -36 -415 -47 -55 -8 -75 -15 -74 -26 1 -20
+56 -374 59 -377 1 -2 46 4 101 12 159 24 409 45 526 45 l108 0 0 200 0 200
+-132 -2 c-73 -1 -151 -3 -173 -5z"
+ />
+ <path
+ d="M3020 4079 c-85 -23 -292 -94 -368 -125 -97 -40 -298 -140 -305 -151
+-5 -7 172 -315 192 -336 4 -4 41 10 82 32 103 55 272 123 414 165 66 20 125
+38 132 41 11 4 -4 70 -78 348 -10 39 -14 41 -69 26z"
+ />
+ <path
+ d="M6955 3538 c-21 -91 -74 -362 -72 -364 7 -7 260 -44 367 -54 146 -13
+359 -13 475 0 49 6 90 12 91 13 2 1 -12 90 -29 197 -26 155 -36 194 -47 192
+-8 -2 -85 -6 -170 -9 -160 -6 -357 7 -505 33 -103 18 -104 18 -110 -8z"
+ />
+ <path
+ d="M1993 3513 c-52 -67 -71 -106 -98 -198 -35 -122 -44 -284 -21 -415 9
+-51 18 -96 21 -98 4 -5 360 79 375 88 7 4 7 24 0 60 -21 109 -7 244 31 307
+l20 31 -146 131 c-80 72 -147 131 -149 131 -2 0 -17 -17 -33 -37z"
+ />
+ <path
+ d="M2210 2519 c-91 -50 -166 -92 -168 -94 -2 -1 11 -26 28 -54 l32 -51
+244 0 c134 0 244 2 244 5 0 3 -23 33 -51 67 -28 35 -72 98 -97 140 -26 43 -51
+77 -57 77 -5 0 -84 -41 -175 -90z"
+ />
+ </g>
+ </svg>
+);
+
+export default TrailsIcon;
diff --git a/src/client/views/nodes/FunctionPlotBox.tsx b/src/client/views/nodes/FunctionPlotBox.tsx
index 61711417f..40f48dafe 100644
--- a/src/client/views/nodes/FunctionPlotBox.tsx
+++ b/src/client/views/nodes/FunctionPlotBox.tsx
@@ -48,8 +48,8 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>
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));
+ anchor.config_xRange = new List<number>(Array.from(this._plot.options.xAxis.domain));
+ anchor.config_yRange = new List<number>(Array.from(this._plot.options.yAxis.domain));
if (addAsAnnotation) this.addDocument(anchor);
return anchor;
};
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index d763753a5..1ab120af5 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -22,7 +22,6 @@ 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';
@@ -37,7 +36,6 @@ 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',
@@ -56,6 +54,17 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
public static LayoutString(fieldKey: string) {
return FieldView.LayoutString(ImageBox, fieldKey);
}
+
+ @observable public static imageRootDoc: Doc | undefined;
+ @observable public static imageEditorOpen: boolean = false;
+ @observable public static imageEditorSource: string = '';
+ @observable public static addDoc: ((doc: Doc | Doc[], annotationKey?: string) => boolean) | undefined;
+ @action public static setImageEditorOpen(open: boolean) {
+ ImageBox.imageEditorOpen = open;
+ }
+ @action public static setImageEditorSource(source: string) {
+ ImageBox.imageEditorSource = source;
+ }
private _ignoreScroll = false;
private _forcedScroll = false;
private _dropDisposer?: DragManager.DragDropDisposer;
@@ -76,20 +85,20 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
};
getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
+ const visibleAnchor = this._getAnchor?.(this._savedAnnotations, false); // use marquee anchor, otherwise, save zoom/pan as anchor
const anchor =
- this._getAnchor?.(this._savedAnnotations, false) ?? // use marquee anchor, otherwise, save zoom/pan as anchor
+ visibleAnchor ??
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,
+ config_panX: NumCast(this.layoutDoc._freeform_panX),
+ config_panY: NumCast(this.layoutDoc._freeform_panY),
+ config_viewScale: Cast(this.layoutDoc._freeform_scale, 'number', null),
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);
+ addAsAnnotation && this.addDocument(anchor);
+ PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), pannable: visibleAnchor ? false : true } }, this.rootDoc);
return anchor;
}
return this.rootDoc;
@@ -222,13 +231,13 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
croppingProto['data_nativeWidth'] = anchw;
croppingProto['data_nativeHeight'] = anchh;
croppingProto.freeform_scale = viewScale;
- croppingProto.freeform_scaleMin = viewScale;
+ croppingProto.freeform_scale_min = 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;
+ croppingProto.freeform_panX_min = anchx / viewScale;
+ croppingProto.freeform_panX_max = anchw / viewScale;
+ croppingProto.freeform_panY_min = anchy / viewScale;
+ croppingProto.freeform_panY_max = anchh / viewScale;
if (addCrop) {
DocUtils.MakeLink(region, cropping, { link_relationship: 'cropped image' });
cropping.x = NumCast(this.rootDoc.x) + this.rootDoc[Width]();
@@ -248,6 +257,16 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
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' });
+ funcs.push({
+ description: 'Open Image Editor',
+ event: action(() => {
+ ImageBox.setImageEditorOpen(true);
+ ImageBox.setImageEditorSource(this.choosePath(field.url));
+ ImageBox.addDoc = this.props.addDocument;
+ ImageBox.imageRootDoc = this.rootDoc;
+ }),
+ icon: 'pencil-alt',
+ });
if (!Doc.noviceMode) {
funcs.push({ description: 'Export to Google Photos', event: () => GooglePhotos.Transactions.UploadImages([this.props.Document]), icon: 'caret-square-right' });
@@ -289,13 +308,14 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
@computed private get url() {
const data = Cast(this.dataDoc[this.fieldKey], ImageField);
- return data ? data.url.href : undefined;
+ return data ? data.url?.href : undefined;
}
choosePath(url: URL) {
+ if (!url?.href) return '';
const lower = url.href.toLowerCase();
if (url.protocol === 'data') return url.href;
- if (url.href.indexOf(window.location.origin) === -1 && url.href.indexOf("dashblobstore") === -1) return Utils.CorsProxy(url.href);
+ if (url.href.indexOf(window.location.origin) === -1 && url.href.indexOf('dashblobstore') === -1) return Utils.CorsProxy(url.href);
if (!/\.(png|jpg|jpeg|gif|webp)$/.test(lower)) return `/assets/unknown-file-icon-hi.png`;
const ext = extname(url.href);
@@ -320,7 +340,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
if (!(data instanceof ImageField)) {
return null;
}
- const primary = data.url.href;
+ const primary = data.url?.href;
if (primary.includes(window.location.origin)) {
return null;
}
@@ -414,7 +434,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
@computed get content() {
TraceMobx();
- const backAlpha = DashColor(this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor)).alpha();
+ const backColor = DashColor(this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor));
+ const backAlpha = backColor.red() === 0 && backColor.green() === 0 && backColor.blue() === 0 ? backColor.alpha() : 1;
const srcpath = this.layoutDoc.hideImage ? '' : this.paths[0];
const fadepath = this.layoutDoc.hideImage ? '' : this.paths.lastElement();
const { nativeWidth, nativeHeight, nativeOrientation } = this.nativeSize;
@@ -482,7 +503,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
this._marqueeing = undefined;
this.props.select(false);
};
- focus = (anchor: Doc, options: DocFocusOptions) => this._ffref.current?.focus(anchor, options);
+ focus = (anchor: Doc, options: DocFocusOptions) => (anchor.type === DocumentType.CONFIG ? undefined : this._ffref.current?.focus(anchor, options));
_ffref = React.createRef<CollectionFreeFormView>();
savedAnnotations = () => this._savedAnnotations;
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index a6712a3db..673f711be 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -4,7 +4,7 @@ import { Doc, Field, FieldResult } from '../../../fields/Doc';
import { List } from '../../../fields/List';
import { RichTextField } from '../../../fields/RichTextField';
import { ComputedField, ScriptField } from '../../../fields/ScriptField';
-import { DocCast, NumCast } from '../../../fields/Types';
+import { DocCast } from '../../../fields/Types';
import { ImageField } from '../../../fields/URLField';
import { returnAll, returnAlways, returnTrue } from '../../../Utils';
import { Docs } from '../../documents/Documents';
@@ -13,6 +13,7 @@ import { CompiledScript, CompileScript, ScriptOptions } from '../../util/Scripti
import { undoBatch } from '../../util/UndoManager';
import { ContextMenu } from '../ContextMenu';
import { ContextMenuProps } from '../ContextMenuItem';
+import { DocumentIconContainer } from './DocumentIcon';
import { OpenWhere } from './DocumentView';
import { FieldView, FieldViewProps } from './FieldView';
import { FormattedTextBox } from './formattedText/FormattedTextBox';
@@ -20,10 +21,6 @@ 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;
@@ -147,7 +144,15 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
const self = this;
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()) {
+ for (const key of keys.sort((a: string, b: string) => {
+ const a_ = a.split('_')[0];
+ const b_ = b.split('_')[0];
+ if (a_ < b_) return -1;
+ if (a_ > b_) return 1;
+ if (a === a_) return -1;
+ if (b === b_) return 1;
+ return a === b ? 0 : a < b ? -1 : 1;
+ })) {
rows.push(
<KeyValuePair
doc={realDoc}
diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx
index 01acdccb7..f22cb195f 100644
--- a/src/client/views/nodes/KeyValuePair.tsx
+++ b/src/client/views/nodes/KeyValuePair.tsx
@@ -14,6 +14,8 @@ import './KeyValueBox.scss';
import './KeyValuePair.scss';
import React = require('react');
import { DocCast } from '../../../fields/Types';
+import { Tooltip } from '@material-ui/core';
+import { DocumentOptions, FInfo } from '../../documents/Documents';
// Represents one row in a key value plane
@@ -109,11 +111,13 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
X
</button>
<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>
+ <Tooltip title={Object.entries(new DocumentOptions()).find((pair: [string, FInfo]) => pair[0].replace(/^_/, '') === props.fieldKey)?.[1].description ?? ''}>
+ <div className="keyValuePair-keyField" style={{ marginLeft: 20 * (props.fieldKey.match(/_/g)?.length || 0), color: keyStyle }}>
+ {'('.repeat(parenCount)}
+ {props.fieldKey}
+ {')'.repeat(parenCount)}
+ </div>
+ </Tooltip>
</div>
</td>
<td className="keyValuePair-td-value" style={{ width: `${100 - this.props.keyWidth}%` }} onContextMenu={this.onContextMenu}>
diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx
index 710d41471..efb949a47 100644
--- a/src/client/views/nodes/LinkBox.tsx
+++ b/src/client/views/nodes/LinkBox.tsx
@@ -18,7 +18,7 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() {
this.props.setContentView?.(this);
}
render() {
- if (this.dataDoc.treeViewOpen === undefined) setTimeout(() => (this.dataDoc.treeViewOpen = true));
+ if (this.dataDoc.treeView_Open === undefined) setTimeout(() => (this.dataDoc.treeView_Open = true));
return (
<div className={`linkBox-container${this.props.isContentActive() ? '-interactive' : ''}`} style={{ background: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor) }}>
<ComparisonBox
diff --git a/src/client/views/nodes/LinkDocPreview.scss b/src/client/views/nodes/LinkDocPreview.scss
index c68e55f73..28216394d 100644
--- a/src/client/views/nodes/LinkDocPreview.scss
+++ b/src/client/views/nodes/LinkDocPreview.scss
@@ -1,7 +1,6 @@
.linkDocPreview {
position: absolute;
pointer-events: all;
- background-color: lightblue;
border: 8px solid white;
border-radius: 7px;
box-shadow: 3px 3px 1.5px grey;
@@ -11,7 +10,6 @@
cursor: pointer;
.linkDocPreview-inner {
- background-color: white;
width: 100%;
height: 100%;
pointer-events: none;
diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx
index 86191de63..198cbe851 100644
--- a/src/client/views/nodes/LinkDocPreview.tsx
+++ b/src/client/views/nodes/LinkDocPreview.tsx
@@ -13,12 +13,13 @@ import { DocumentType } from '../../documents/DocumentTypes';
import { DragManager } from '../../util/DragManager';
import { LinkFollower } from '../../util/LinkFollower';
import { LinkManager } from '../../util/LinkManager';
+import { SearchUtil } from '../../util/SearchUtil';
import { SettingsManager } from '../../util/SettingsManager';
import { Transform } from '../../util/Transform';
-import { SearchBox } from '../search/SearchBox';
import { DocumentView, DocumentViewSharedProps, OpenWhere } from './DocumentView';
import './LinkDocPreview.scss';
import React = require('react');
+import { DocumentManager } from '../../util/DocumentManager';
interface LinkDocPreviewProps {
linkDoc?: Doc;
@@ -174,9 +175,14 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
LinkFollower.FollowLink(this._linkDoc, this._linkSrc, false);
} else if (this.props.hrefs?.length) {
const webDoc =
- Array.from(SearchBox.staticSearchCollection(Doc.MyFilesystem, this.props.hrefs[0]).keys()).lastElement() ??
+ Array.from(SearchUtil.SearchCollection(Doc.MyFilesystem, this.props.hrefs[0]).keys()).lastElement() ??
Docs.Create.WebDocument(this.props.hrefs[0], { title: this.props.hrefs[0], _nativeWidth: 850, _width: 200, _height: 400, data_useCors: true });
- this.props.docProps?.addDocTab(webDoc, OpenWhere.lightbox);
+ DocumentManager.Instance.showDocument(webDoc, {
+ openLocation: OpenWhere.lightbox,
+ willPan: true,
+ zoomTime: 500,
+ });
+ //this.props.docProps?.addDocTab(webDoc, OpenWhere.lightbox);
}
};
@@ -198,7 +204,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
};
@computed get previewHeader() {
return !this._linkDoc || !this._markerTargetDoc || !this._targetDoc || !this._linkSrc ? null : (
- <div className="linkDocPreview-info">
+ <div className="linkDocPreview-info" style={{ background: SettingsManager.userBackgroundColor }}>
<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}>
@@ -296,7 +302,7 @@ 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.width() + borders, height: this.height() + borders + (this.props.showHeader ? 37 : 0) }}>
+ style={{ borderColor: SettingsManager.userColor, 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 d4a7e18f2..cabd4de05 100644
--- a/src/client/views/nodes/LoadingBox.scss
+++ b/src/client/views/nodes/LoadingBox.scss
@@ -5,34 +5,28 @@
justify-content: center;
background-color: #fdfdfd;
height: 100%;
+ width: 100%;
align-items: center;
- .textContainer,
- .text {
- overflow: hidden;
+ .loadingBox-textContainer,
+ .loadingBox-title {
text-overflow: ellipsis;
max-width: 80%;
text-align: center;
- display: flex;
- flex-direction: column;
+ display: block;
gap: 8px;
align-items: center;
+ white-space: normal;
+ word-break: all;
+ .loadingBox-headerText {
+ text-align: center;
+ font-weight: bold;
+ height: auto;
+ width: 100%;
+ }
+ }
+ .loadingBox-spinner {
+ position: absolute;
+ top: 0;
+ left: 0;
}
-}
-
-.textContainer {
- margin: 5px;
-}
-
-.textContainer {
- justify-content: center;
- align-content: center;
-}
-
-.headerText {
- text-align: center;
- font-weight: bold;
-}
-
-.spinner {
- text-align: center;
}
diff --git a/src/client/views/nodes/LoadingBox.tsx b/src/client/views/nodes/LoadingBox.tsx
index fcbd0128d..bdc074e0c 100644
--- a/src/client/views/nodes/LoadingBox.tsx
+++ b/src/client/views/nodes/LoadingBox.tsx
@@ -1,14 +1,14 @@
-import { action, observable, runInAction } from 'mobx';
+import { observable, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import ReactLoading from 'react-loading';
import { Doc } from '../../../fields/Doc';
+import { Id } from '../../../fields/FieldSymbols';
import { StrCast } from '../../../fields/Types';
import { Networking } from '../../Network';
import { ViewBoxAnnotatableComponent } from '../DocComponent';
import { FieldView, FieldViewProps } from './FieldView';
import './LoadingBox.scss';
-import { Id } from '../../../fields/FieldSymbols';
/**
* LoadingBox Class represents a placeholder doc for documents that are currently
@@ -44,9 +44,9 @@ export class LoadingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
this.rootDoc.loadingError = 'Upload interrupted, please try again';
} else {
const updateFunc = async () => {
- const result = await Networking.QueryYoutubeProgress(StrCast(this.rootDoc[Id])); // We use the guid of the overwriteDoc to track file uploads.
+ const result = await Networking.QueryYoutubeProgress(StrCast(this.rootDoc[Id])); // We use the guid of the overwriteDoc to track file uploads.
runInAction(() => (this.progress = result.progress));
- this._timer = setTimeout(updateFunc, 1000);
+ !this.rootDoc.loadingError && (this._timer = setTimeout(updateFunc, 1000));
};
this._timer = setTimeout(updateFunc, 1000);
}
@@ -58,10 +58,14 @@ export class LoadingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
render() {
return (
<div className="loadingBoxContainer" style={{ background: !this.rootDoc.loadingError ? '' : 'red' }}>
- <div className="textContainer">
- <p className="headerText">{StrCast(this.rootDoc.loadingError, 'Loading ' + (this.progress.replace('[download]', '') || '(can take several minutes)'))}</p>
- <span className="text">{StrCast(this.rootDoc.title)}</span>
- {this.rootDoc.loadingError ? null : <ReactLoading type={'spinningBubbles'} color={'blue'} height={100} width={100} />}
+ <div className="loadingBox-textContainer">
+ <span className="loadingBox-title">{StrCast(this.rootDoc.title)}</span>
+ <p className="loadingBox-headerText">{StrCast(this.rootDoc.loadingError, 'Loading ' + (this.progress.replace('[download]', '') || '(can take several minutes)'))}</p>
+ {this.rootDoc.loadingError ? null : (
+ <div className="loadingBox-spinner">
+ <ReactLoading type="spinningBubbles" color="blue" height={100} width={100} />
+ </div>
+ )}
</div>
</div>
);
diff --git a/src/client/views/nodes/MapBox/MapAnchorMenu.scss b/src/client/views/nodes/MapBox/MapAnchorMenu.scss
new file mode 100644
index 000000000..6990bdcf1
--- /dev/null
+++ b/src/client/views/nodes/MapBox/MapAnchorMenu.scss
@@ -0,0 +1,54 @@
+.anchorMenu-addTag {
+ display: grid;
+ width: 200px;
+ padding: 5px;
+ grid-template-columns: 90px 20px 90px;
+}
+.anchorMenu-highlighter {
+ padding-right: 5px;
+ .antimodeMenu-button {
+ padding: 0;
+ padding: 0;
+ padding-right: 0px;
+ padding-left: 0px;
+ width: 5px;
+ }
+}
+.anchor-color-preview-button {
+ width: 25px !important;
+ .anchor-color-preview {
+ display: flex;
+ flex-direction: column;
+ padding-right: 3px;
+ width: unset !important;
+ .color-preview {
+ width: 60%;
+ top: 80%;
+ height: 4px;
+ position: relative;
+ top: unset;
+ width: 15px;
+ margin-top: 5px;
+ display: block;
+ }
+ }
+}
+
+.color-wrapper {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+
+ button.color-button {
+ width: 20px;
+ height: 20px;
+ border-radius: 15px !important;
+ margin: 3px;
+ border: 2px solid transparent !important;
+ padding: 3px;
+
+ &.active {
+ border: 2px solid white;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/MapBox/MapAnchorMenu.tsx b/src/client/views/nodes/MapBox/MapAnchorMenu.tsx
new file mode 100644
index 000000000..f0827936b
--- /dev/null
+++ b/src/client/views/nodes/MapBox/MapAnchorMenu.tsx
@@ -0,0 +1,143 @@
+import React = require('react');
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { action, computed, IReactionDisposer, observable, ObservableMap, reaction } from 'mobx';
+import { observer } from 'mobx-react';
+import { ColorState } from 'react-color';
+import { Doc, Opt } from '../../../../fields/Doc';
+import { returnFalse, setupMoveUpEvents, unimplementedFunction, Utils } from '../../../../Utils';
+import { SelectionManager } from '../../../util/SelectionManager';
+import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu';
+import { LinkPopup } from '../../linking/LinkPopup';
+import { gptAPICall, GPTCallType } from '../../../apis/gpt/GPT';
+// import { GPTPopup, GPTPopupMode } from './../../GPTPopup/GPTPopup';
+import { EditorView } from 'prosemirror-view';
+import './MapAnchorMenu.scss';
+import { ColorPicker, Group, IconButton, Popup, Size, Toggle, ToggleType, Type } from 'browndash-components';
+import { StrCast } from '../../../../fields/Types';
+import { DocumentType } from '../../../documents/DocumentTypes';
+import { SettingsManager } from '../../../util/SettingsManager';
+
+@observer
+export class MapAnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
+ static Instance: MapAnchorMenu;
+
+ private _disposer: IReactionDisposer | undefined;
+ private _disposer2: IReactionDisposer | undefined;
+ private _commentCont = React.createRef<HTMLButtonElement>();
+
+ public onMakeAnchor: () => Opt<Doc> = () => undefined; // Method to get anchor from text search
+
+ public Center: () => void = unimplementedFunction;
+ // public OnClick: (e: PointerEvent) => void = unimplementedFunction;
+ // public OnAudio: (e: PointerEvent) => void = unimplementedFunction;
+ // public StartDrag: (e: PointerEvent, ele: HTMLElement) => void = unimplementedFunction;
+ // public StartCropDrag: (e: PointerEvent, ele: HTMLElement) => void = unimplementedFunction;
+ public Highlight: (color: string, isTargetToggler: boolean, savedAnnotations?: ObservableMap<number, HTMLDivElement[]>, addAsAnnotation?: boolean) => Opt<Doc> = (color: string, isTargetToggler: boolean) => undefined;
+ public GetAnchor: (savedAnnotations: Opt<ObservableMap<number, HTMLDivElement[]>>, addAsAnnotation: boolean) => Opt<Doc> = (savedAnnotations: Opt<ObservableMap<number, HTMLDivElement[]>>, addAsAnnotation: boolean) => undefined;
+ public Delete: () => void = unimplementedFunction;
+ public LinkNote: () => void = unimplementedFunction;
+ // public MakeTargetToggle: () => void = unimplementedFunction;
+ // public ShowTargetTrail: () => void = unimplementedFunction;
+ public IsTargetToggler: () => boolean = returnFalse;
+ public get Active() {
+ return this._left > 0;
+ }
+
+ constructor(props: Readonly<{}>) {
+ super(props);
+
+ MapAnchorMenu.Instance = this;
+ MapAnchorMenu.Instance._canFade = false;
+ }
+
+ componentWillUnmount() {
+ this._disposer?.();
+ this._disposer2?.();
+ }
+
+ componentDidMount() {
+ this._disposer2 = reaction(
+ () => this._opacity,
+ opacity => {
+ if (!opacity) {
+ }
+ },
+ { fireImmediately: true }
+ );
+ this._disposer = reaction(
+ () => SelectionManager.Views().slice(),
+ selected => {
+ MapAnchorMenu.Instance.fadeOut(true);
+ }
+ );
+ }
+ // audioDown = (e: React.PointerEvent) => {
+ // setupMoveUpEvents(this, e, returnFalse, returnFalse, e => this.OnAudio?.(e));
+ // };
+
+ // cropDown = (e: React.PointerEvent) => {
+ // setupMoveUpEvents(
+ // this,
+ // e,
+ // (e: PointerEvent) => {
+ // this.StartCropDrag(e, this._commentCont.current!);
+ // return true;
+ // },
+ // returnFalse,
+ // e => this.OnCrop?.(e)
+ // );
+ // };
+
+ static top = React.createRef<HTMLDivElement>();
+ // public get Top(){
+ // return this.top
+ // }
+
+ render() {
+ const buttons = (
+ <>
+ {
+ <IconButton
+ tooltip="Delete Pin" //
+ onPointerDown={this.Delete}
+ icon={<FontAwesomeIcon icon="trash-alt" />}
+ color={SettingsManager.userColor}
+ />
+ }
+ {
+ <IconButton
+ tooltip="Link Note to Pin" //
+ onPointerDown={this.LinkNote}
+ icon={<FontAwesomeIcon icon="sticky-note" />}
+ color={SettingsManager.userColor}
+ />
+ }
+ {
+ <IconButton
+ tooltip="Center on pin" //
+ onPointerDown={this.Center}
+ icon={<FontAwesomeIcon icon="compress-arrows-alt" />}
+ color={SettingsManager.userColor}
+ />
+ }
+ {/* {this.IsTargetToggler !== returnFalse && (
+ <Toggle
+ tooltip={'Make target visibility toggle on click'}
+ type={Type.PRIM}
+ toggleType={ToggleType.BUTTON}
+ toggleStatus={this.IsTargetToggler()}
+ onClick={this.MakeTargetToggle}
+ icon={<FontAwesomeIcon icon="thumbtack" />}
+ color={SettingsManager.userColor}
+ />
+ )} */}
+ </>
+ );
+
+ return this.getElement(
+ <div ref={MapAnchorMenu.top} style={{ width: '100%', display: 'flex' }}>
+ {buttons}
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/nodes/MapBox/MapBox.scss b/src/client/views/nodes/MapBox/MapBox.scss
index fb15520f6..242677231 100644
--- a/src/client/views/nodes/MapBox/MapBox.scss
+++ b/src/client/views/nodes/MapBox/MapBox.scss
@@ -1,87 +1,108 @@
-@import "../../global/globalCssVariables.scss";
+@import '../../global/globalCssVariables.scss';
.mapBox {
- width: 100%;
- height: 100%;
- overflow: hidden;
- display: flex;
-
- .mapBox-infoWindow {
- background-color: white;
- opacity: 0.75;
- padding: 12;
- font-size: 17;
- }
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+ display: flex;
- .mapBox-overlayButton-sidebar {
- background: #121721;
- height: 25px;
- width: 25px;
- right: 5px;
- display: flex;
- position: absolute;
- align-items: center;
- justify-content: center;
- border-radius: 3px;
- pointer-events: all;
- z-index: 1; // so it appears on top of the document's title, if shown
-
- box-shadow: $standard-box-shadow;
- transition: 0.2s;
-
- &:hover{
- filter: brightness(0.85);
- }
+ .mapBox-infoWindow {
+ background-color: white;
+ opacity: 0.75;
+ padding: 12;
+ font-size: 17;
+ }
+ .mapBox-searchbar {
+ display: flex;
+ flex-direction: row;
+ width: calc(100% - 40px);
+ .editableText-container {
+ width: 100%;
+ font-size: 16px !important;
}
-
- .mapBox-wrapper {
+ input {
width: 100%;
- .mapBox-input {
- box-sizing: border-box;
- border: 1px solid transparent;
- width: 240px;
- height: 32px;
- padding: 0 12px;
- border-radius: 3px;
- box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
- font-size: 14px;
- outline: none;
- text-overflow: ellipses;
- position: absolute;
- left: 50%;
- margin-left: -120px;
- }
}
+ }
+ .mapBox-topbar {
+ display: flex;
+ flex-direction: row;
+ }
- .mapBox-sidebar-handle {
- top: 0;
- //top: calc(50% - 17.5px); // use this to center vertically -- make sure it looks okay for slide views
- width: 10px;
- height: 100%;
- max-height: 35px;
- background: lightgray;
- border-radius: 20px;
- cursor:grabbing;
+ .mapBox-overlayButton-sidebar {
+ background: #121721;
+ height: 25px;
+ width: 25px;
+ right: 5px;
+ display: flex;
+ position: absolute;
+ align-items: center;
+ justify-content: center;
+ border-radius: 3px;
+ pointer-events: all;
+ z-index: 1; // so it appears on top of the document's title, if shown
+
+ box-shadow: $standard-box-shadow;
+ transition: 0.2s;
+
+ &:hover {
+ filter: brightness(0.85);
}
- .mapBox-addMarker {
+ }
+
+ .mapBox-wrapper {
+ width: 100%;
+ .mapBox-input {
+ box-sizing: border-box;
+ border: 1px solid transparent;
+ width: 240px;
+ height: 32px;
+ padding: 0 12px;
+ border-radius: 3px;
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
+ font-size: 14px;
+ outline: none;
+ text-overflow: ellipses;
+ position: absolute;
left: 50%;
- margin-left: 120px;
- right: unset !important;
- margin-top: -10;
- height: max-content;
- }
- .searchbox {
- display:none;
- }
- .mapBox-addMarker {
- display:none;
+ margin-left: -120px;
}
+ }
+ .mapBox-sidebar {
+ position: absolute;
+ right: 0;
+ height: 100%;
+ }
+
+ .mapBox-sidebar-handle {
+ top: 0;
+ //top: calc(50% - 17.5px); // use this to center vertically -- make sure it looks okay for slide views
+ width: 10px;
+ height: 100%;
+ max-height: 35px;
+ background: lightgray;
+ border-radius: 20px;
+ cursor: grabbing;
+ }
+ .mapBox-addMarker {
+ left: 50%;
+ margin-left: 120px;
+ right: unset !important;
+ margin-top: -10;
+ height: max-content;
+ }
+ .searchbox {
+ display: none;
+ }
+ .mapBox-addMarker {
+ display: none;
+ }
}
.mapBox:hover {
.mapBox-addMarker {
- display:block;
+ display: block;
}
.searchbox {
- display :block;
+ display: block;
}
}
diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx
index de0b57fd7..d7469e530 100644
--- a/src/client/views/nodes/MapBox/MapBox.tsx
+++ b/src/client/views/nodes/MapBox/MapBox.tsx
@@ -1,31 +1,33 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Autocomplete, GoogleMap, GoogleMapProps, Marker } from '@react-google-maps/api';
import BingMapsReact from 'bingmaps-react';
-import { action, computed, IReactionDisposer, observable, ObservableMap, runInAction } from 'mobx';
+import { Button, EditableText, IconButton, Type } from 'browndash-components';
+import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { Doc, DocListCast, Opt } from '../../../../fields/Doc';
-import { Width } from '../../../../fields/DocSymbols';
-import { Id } from '../../../../fields/FieldSymbols';
+import { Doc, DocListCast, Field, Opt } from '../../../../fields/Doc';
+import { DocCss, Highlight, Width } from '../../../../fields/DocSymbols';
import { InkTool } from '../../../../fields/InkField';
-import { NumCast, StrCast } from '../../../../fields/Types';
-import { emptyFunction, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../../Utils';
-import { Docs } from '../../../documents/Documents';
+import { DocCast, NumCast, StrCast } from '../../../../fields/Types';
+import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../../Utils';
+import { Docs, DocUtils } from '../../../documents/Documents';
+import { DocumentType } from '../../../documents/DocumentTypes';
+import { DocumentManager } from '../../../util/DocumentManager';
import { DragManager } from '../../../util/DragManager';
+import { LinkManager } from '../../../util/LinkManager';
import { SnappingManager } from '../../../util/SnappingManager';
-import { UndoManager } from '../../../util/UndoManager';
+import { Transform } from '../../../util/Transform';
+import { undoable, UndoManager } from '../../../util/UndoManager';
import { MarqueeOptionsMenu } from '../../collections/collectionFreeForm';
import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../../DocComponent';
import { Colors } from '../../global/globalEnums';
import { MarqueeAnnotator } from '../../MarqueeAnnotator';
-import { AnchorMenu } from '../../pdf/AnchorMenu';
-import { Annotation } from '../../pdf/Annotation';
import { SidebarAnnos } from '../../SidebarAnnos';
+import { DocumentView } from '../DocumentView';
import { FieldView, FieldViewProps } from '../FieldView';
-import { PinProps } from '../trails';
+import { PinProps, PresBox } from '../trails';
+import { MapAnchorMenu } from './MapAnchorMenu';
import './MapBox.scss';
-import { MapBoxInfoWindow } from './MapBoxInfoWindow';
-
+// amongus
/**
* MapBox architecture:
* Main component: MapBox.tsx
@@ -39,30 +41,7 @@ import { MapBoxInfoWindow } from './MapBoxInfoWindow';
* A map marker is considered a document that contains a collection with stacking view of documents, it has a lat, lng location, which is passed to Maps API's custom marker (red pin) to be rendered on the google maps
*/
-// const _global = (window /* browser */ || global /* node */) as any;
-
-const mapContainerStyle = {
- height: '100%',
-};
-
-const defaultCenter = {
- lat: 42.360081,
- lng: -71.058884,
-};
-
-const mapOptions = {
- fullscreenControl: false,
-};
-
const bingApiKey = process.env.BING_MAPS; // if you're running local, get a Bing Maps api key here: https://www.bingmapsportal.com/ and then add it to the .env file in the Dash-Web root directory as: _CLIENT_BING_MAPS=<your apikey>
-const apiKey = process.env.GOOGLE_MAPS;
-
-const script = document.createElement('script');
-script.defer = true;
-script.async = true;
-script.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places,drawing`;
-console.log(script.src);
-document.head.appendChild(script);
/**
* Consider integrating later: allows for drawing, circling, making shapes on map
@@ -80,245 +59,56 @@ document.head.appendChild(script);
// },
// });
-// options for searchbox in Google Maps Places Autocomplete API
-const options = {
- fields: ['formatted_address', 'geometry', 'name'], // note: level of details is charged by item per retrieval, not recommended to return all fields
- strictBounds: false,
- types: ['establishment'], // type pf places, subject of change according to user need
-} as google.maps.places.AutocompleteOptions;
-
@observer
-export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps & Partial<GoogleMapProps>>() {
- static UseBing = true;
- private _dropDisposer?: DragManager.DragDropDisposer;
- private _disposers: { [name: string]: IReactionDisposer } = {};
- private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
- @observable private _overlayAnnoInfo: Opt<Doc>;
- showInfo = action((anno: Opt<Doc>) => (this._overlayAnnoInfo = anno));
+export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps>() {
public static LayoutString(fieldKey: string) {
return FieldView.LayoutString(MapBox, fieldKey);
}
- public get SidebarKey() {
- return this.fieldKey + '_sidebar';
- }
- private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean) => void);
- @computed get inlineTextAnnotations() {
- return this.allMapMarkers.filter(a => a.textInlineAnnotations);
- }
+ private _dragRef = React.createRef<HTMLDivElement>();
+ private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
+ private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
+ private _sidebarRef = React.createRef<SidebarAnnos>();
+ private _ref: React.RefObject<HTMLDivElement> = React.createRef();
+ private _disposers: { [key: string]: IReactionDisposer } = {};
+ private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void);
- @observable private _map: google.maps.Map = null as unknown as google.maps.Map;
- @observable private selectedPlace: Doc | undefined;
- @observable private markerMap: { [id: string]: google.maps.Marker } = {};
- @observable private center = navigator.geolocation ? navigator.geolocation.getCurrentPosition : defaultCenter;
@observable private _marqueeing: number[] | undefined;
- @observable private _isAnnotating = false;
- @observable private inputRef = React.createRef<HTMLInputElement>();
- @observable private searchMarkers: google.maps.Marker[] = [];
- @observable private searchBox = new window.google.maps.places.Autocomplete(this.inputRef.current!, options);
@observable private _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>();
@computed get allSidebarDocs() {
return DocListCast(this.dataDoc[this.SidebarKey]);
}
- @computed get allMapMarkers() {
+ // this list contains pushpins and configs
+ @computed get allAnnotations() {
return DocListCast(this.dataDoc[this.annotationKey]);
}
- @observable private toggleAddMarker = false;
- private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
-
- @observable _showSidebar = false;
+ @computed get allPushpins() {
+ return this.allAnnotations.filter(anno => anno.type === DocumentType.PUSHPIN);
+ }
@computed get SidebarShown() {
- return this._showSidebar || this.layoutDoc._layout_showSidebar ? true : false;
+ return this.layoutDoc._layout_showSidebar ? true : false;
+ }
+ @computed get sidebarWidthPercent() {
+ return StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%');
+ }
+ @computed get sidebarColor() {
+ return StrCast(this.layoutDoc.sidebar_color, StrCast(this.layoutDoc[this.props.fieldKey + '_backgroundColor'], '#e4e4e4'));
+ }
+ @computed get SidebarKey() {
+ return this.fieldKey + '_sidebar';
}
-
- static _canAnnotate = true;
- static _hadSelection: boolean = false;
- private _sidebarRef = React.createRef<SidebarAnnos>();
- private _ref: React.RefObject<HTMLDivElement> = React.createRef();
componentDidMount() {
+ this._unmounting = false;
this.props.setContentView?.(this);
}
- @action
- private setSearchBox = (searchBox: any) => {
- this.searchBox = searchBox;
- };
-
- // iterate allMarkers to size, center, and zoom map to contain all markers
- private fitBounds = (map: google.maps.Map) => {
- const curBounds = map.getBounds() ?? new window.google.maps.LatLngBounds();
- const isFitting = this.allMapMarkers.reduce((fits, place) => fits && curBounds?.contains({ lat: NumCast(place.lat), lng: NumCast(place.lng) }), true as boolean);
- !isFitting && map.fitBounds(this.allMapMarkers.reduce((bounds, place) => bounds.extend({ lat: NumCast(place.lat), lng: NumCast(place.lng) }), new window.google.maps.LatLngBounds()));
- };
-
- /**
- * Custom control for add marker button
- * @param controlDiv
- * @param map
- */
- private CenterControl = () => {
- const controlDiv = document.createElement('div');
- controlDiv.className = 'mapBox-addMarker';
- // Set CSS for the control border.
- const controlUI = document.createElement('div');
- controlUI.style.backgroundColor = '#fff';
- controlUI.style.borderRadius = '3px';
- controlUI.style.cursor = 'pointer';
- controlUI.style.marginTop = '10px';
- controlUI.style.borderRadius = '4px';
- controlUI.style.marginBottom = '22px';
- controlUI.style.textAlign = 'center';
- controlUI.style.position = 'absolute';
- controlUI.style.width = '32px';
- controlUI.style.height = '32px';
- controlUI.title = 'Click to toggle marker mode. In marker mode, click on map to place a marker.';
-
- const plIcon = document.createElement('img');
- plIcon.src = 'https://cdn4.iconfinder.com/data/icons/wirecons-free-vector-icons/32/add-256.png';
- plIcon.style.color = 'rgb(25,25,25)';
- plIcon.style.fontFamily = 'Roboto,Arial,sans-serif';
- plIcon.style.fontSize = '16px';
- plIcon.style.lineHeight = '32px';
- plIcon.style.left = '18';
- plIcon.style.top = '15';
- plIcon.style.position = 'absolute';
- plIcon.width = 14;
- plIcon.height = 14;
- plIcon.innerHTML = 'Add';
- controlUI.appendChild(plIcon);
-
- // Set CSS for the control interior.
- const markerIcon = document.createElement('img');
- markerIcon.src = 'https://cdn0.iconfinder.com/data/icons/small-n-flat/24/678111-map-marker-1024.png';
- markerIcon.style.color = 'rgb(25,25,25)';
- markerIcon.style.fontFamily = 'Roboto,Arial,sans-serif';
- markerIcon.style.fontSize = '16px';
- markerIcon.style.lineHeight = '32px';
- markerIcon.style.left = '-2';
- markerIcon.style.top = '1';
- markerIcon.width = 30;
- markerIcon.height = 30;
- markerIcon.style.position = 'absolute';
- markerIcon.innerHTML = 'Add';
- controlUI.appendChild(markerIcon);
-
- // Setup the click event listeners
- controlUI.addEventListener('click', () => {
- if (this.toggleAddMarker === true) {
- this.toggleAddMarker = false;
- console.log('add marker button status:' + this.toggleAddMarker);
- controlUI.style.backgroundColor = '#fff';
- markerIcon.style.color = 'rgb(25,25,25)';
- } else {
- this.toggleAddMarker = true;
- console.log('add marker button status:' + this.toggleAddMarker);
- controlUI.style.backgroundColor = '#4476f7';
- markerIcon.style.color = 'rgb(255,255,255)';
- }
- });
- controlDiv.appendChild(controlUI);
- return controlDiv;
- };
-
- /**
- * Place the marker on google maps & store the empty marker as a MapMarker Document in allMarkers list
- * @param position - the LatLng position where the marker is placed
- * @param map
- */
- @action
- private placeMarker = (position: google.maps.LatLng, map: google.maps.Map) => {
- const marker = new google.maps.Marker({
- position: position,
- map: map,
- });
- map.panTo(position);
- const mapMarker = Docs.Create.MapMarkerDocument(NumCast(position.lat()), NumCast(position.lng()), false, [], {});
- this.addDocument(mapMarker, this.annotationKey);
- };
-
- _loadPending = true;
- /**
- * store a reference to google map instance
- * setup the drawing manager on the top right corner of map
- * fit map bounds to contain all markers
- * @param map
- */
- @action
- private loadHandler = (map: google.maps.Map) => {
- this._map = map;
- this._loadPending = true;
- const centerControlDiv = this.CenterControl();
- map.controls[google.maps.ControlPosition.TOP_RIGHT].push(centerControlDiv);
- //drawingManager.setMap(map);
- // if (navigator.geolocation) {
- // navigator.geolocation.getCurrentPosition(
- // (position: Position) => {
- // const pos = {
- // lat: position.coords.latitude,
- // lng: position.coords.longitude,
- // };
- // this._map.setCenter(pos);
- // }
- // );
- // } else {
- // alert("Your geolocation is not supported by browser.")
- // };
- map.setZoom(NumCast(this.dataDoc.mapZoom, 2.5));
- map.setCenter(new google.maps.LatLng(NumCast(this.dataDoc.mapLat), NumCast(this.dataDoc.mapLng)));
- setTimeout(() => {
- if (this._loadPending && this._map.getBounds()) {
- this._loadPending = false;
- this.layoutDoc.freeform_fitContentsToBox && this.fitBounds(this._map);
- }
- }, 250);
- // listener to addmarker event
- this._map.addListener('click', (e: MouseEvent) => {
- if (this.toggleAddMarker === true) {
- this.placeMarker((e as any).latLng, map);
- }
- });
- };
-
- @action
- centered = () => {
- if (this._loadPending && this._map.getBounds()) {
- this._loadPending = false;
- this.layoutDoc.freeform_fitContentsToBox && this.fitBounds(this._map);
- }
- this.dataDoc.mapLat = this._map.getCenter()?.lat();
- this.dataDoc.mapLng = this._map.getCenter()?.lng();
- };
-
- @action
- zoomChanged = () => {
- if (this._loadPending && this._map.getBounds()) {
- this._loadPending = false;
- this.layoutDoc.freeform_fitContentsToBox && this.fitBounds(this._map);
- }
- this.dataDoc.mapZoom = this._map.getZoom();
- };
-
- /**
- * Load and render all map markers
- * @param marker
- * @param place
- */
- @action
- private markerLoadHandler = (marker: google.maps.Marker, place: Doc) => {
- place[Id] ? (this.markerMap[place[Id]] = marker) : null;
- };
-
- /**
- * on clicking the map marker, set the selected place to the marker document & set infowindowopen to be true
- * @param e
- * @param place
- */
- @action
- private markerClickHandler = (e: google.maps.MapMouseEvent, place: Doc) => {
- // set which place was clicked
- this.selectedPlace = place;
- place.infoWindowOpen = true;
- };
+ _unmounting = false;
+ componentWillUnmount(): void {
+ this._unmounting = true;
+ this.deselectPin();
+ this._rerenderTimeout && clearTimeout(this._rerenderTimeout);
+ Object.keys(this._disposers).forEach(key => this._disposers[key]?.());
+ }
/**
* Called when dragging documents into map sidebar or directly into infowindow; to create a map marker, ref to MapMarkerDocument in Documents.ts
@@ -327,35 +117,42 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
* @returns
*/
sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => {
- console.log('print all sidebar Docs');
if (!this.layoutDoc._layout_showSidebar) this.toggleSidebar();
const docs = doc instanceof Doc ? [doc] : doc;
docs.forEach(doc => {
- if (doc.lat !== undefined && doc.lng !== undefined) {
- const existingMarker = this.allMapMarkers.find(marker => marker.lat === doc.lat && marker.lng === doc.lng);
- if (existingMarker) {
- Doc.AddDocToList(existingMarker, 'data', doc);
- } else {
- const marker = Docs.Create.MapMarkerDocument(NumCast(doc.lat), NumCast(doc.lng), false, [doc], {});
- this.addDocument(marker, this.annotationKey);
- }
+ let existingPin = this.allPushpins.find(pin => pin.latitude === doc.latitude && pin.longitude === doc.longitude) ?? this.selectedPin;
+ if (doc.latitude !== undefined && doc.longitude !== undefined && !existingPin) {
+ existingPin = this.createPushpin(NumCast(doc.latitude), NumCast(doc.longitude), StrCast(doc.map));
+ }
+ if (existingPin) {
+ setTimeout(() => {
+ // we use a timeout in case this is called from the sidebar which may have just added a link that hasn't made its way into th elink manager yet
+ if (!LinkManager.Instance.getAllRelatedLinks(doc).some(link => DocCast(link.link_anchor_1)?.mapPin === existingPin || DocCast(link.link_anchor_2)?.mapPin === existingPin)) {
+ const anchor = this.getAnchor(true, undefined, existingPin);
+ anchor && DocUtils.MakeLink(anchor, doc, { link_relationship: 'link to map location' });
+ doc.latitude = existingPin?.latitude;
+ doc.longitude = existingPin?.longitude;
+ }
+ });
}
}); //add to annotation list
return this.addDocument(doc, sidebarKey); // add to sidebar list
};
+ removeMapDocument = (doc: Doc | Doc[], annotationKey?: string) => {
+ const docs = doc instanceof Doc ? [doc] : doc;
+ this.allAnnotations.filter(anno => docs.includes(DocCast(anno.mapPin))).forEach(anno => (anno.mapPin = undefined));
+ return this.removeDocument(doc, annotationKey, undefined);
+ };
+
/**
* 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);
- };
+ sidebarRemoveDocument = (doc: Doc | Doc[], sidebarKey?: string) => this.removeMapDocument(doc, sidebarKey);
/**
* Toggle sidebar onclick the tiny comment button on the top right corner
@@ -374,11 +171,11 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
const fullWidth = this.layoutDoc[Width]();
const mapWidth = fullWidth - this.sidebarWidth();
if (this.sidebarWidth() + localDelta[0] > 0) {
- this._showSidebar = true;
+ this.layoutDoc._layout_showSidebar = true;
this.layoutDoc._width = fullWidth + localDelta[0];
this.layoutDoc._layout_sidebarWidthPercent = ((100 * (this.sidebarWidth() + localDelta[0])) / (fullWidth + localDelta[0])).toString() + '%';
} else {
- this._showSidebar = false;
+ this.layoutDoc._layout_showSidebar = false;
this.layoutDoc._width = mapWidth;
this.layoutDoc._layout_sidebarWidthPercent = '0%';
}
@@ -388,60 +185,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
() => UndoManager.RunInBatch(this.toggleSidebar, 'toggle sidebar map')
);
};
-
- sidebarWidth = () => (Number(this.layout_sidebarWidthPercent.substring(0, this.layout_sidebarWidthPercent.length - 1)) / 100) * this.props.PanelWidth();
- @computed get layout_sidebarWidthPercent() {
- return StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%');
- }
- @computed get sidebarColor() {
- return StrCast(this.layoutDoc.sidebar_color, StrCast(this.layoutDoc[this.props.fieldKey + '_backgroundColor'], '#e4e4e4'));
- }
-
- /**
- * function that reads the place inputed from searchbox, then zoom in on the location that's been autocompleted;
- * add a customized temporary marker on the map
- */
- @action
- private handlePlaceChanged = () => {
- const place = this.searchBox.getPlace();
-
- if (!place.geometry || !place.geometry.location) {
- // user entered the name of a place that wasn't suggested & pressed the enter key, or place details request failed
- window.alert("No details available for input: '" + place.name + "'");
- return;
- }
-
- // zoom in on the location of the search result
- if (place.geometry.viewport) {
- this._map.fitBounds(place.geometry.viewport);
- } else {
- this._map.setCenter(place.geometry.location);
- this._map.setZoom(17);
- }
-
- // customize icon => customized icon for the nature of the location selected
- const icon = {
- url: place.icon as string,
- size: new google.maps.Size(71, 71),
- origin: new google.maps.Point(0, 0),
- anchor: new google.maps.Point(17, 34),
- scaledSize: new google.maps.Size(25, 25),
- };
-
- // put temporary cutomized marker on searched location
- this.searchMarkers.forEach(marker => {
- marker.setMap(null);
- });
- this.searchMarkers = [];
- this.searchMarkers.push(
- new window.google.maps.Marker({
- map: this._map,
- icon,
- title: place.name,
- position: place.geometry.location,
- })
- );
- };
+ sidebarWidth = () => (Number(this.sidebarWidthPercent.substring(0, this.sidebarWidthPercent.length - 1)) / 100) * this.props.PanelWidth();
/**
* Handles toggle of sidebar on click the little comment button
@@ -466,12 +210,28 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
// TODO: Adding highlight box layer to Maps
@action
toggleSidebar = () => {
- //1.2 * w * ? = .2 * w .2/1.2
const prevWidth = this.sidebarWidth();
this.layoutDoc._layout_showSidebar = (this.layoutDoc._layout_sidebarWidthPercent = StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%') === '0%' ? `${(100 * 0.2) / 1.2}%` : '0%') !== '0%';
this.layoutDoc._width = this.layoutDoc._layout_showSidebar ? NumCast(this.layoutDoc._width) * 1.2 : Math.max(20, NumCast(this.layoutDoc._width) - prevWidth);
};
+ createNoteAnnotation = () => {
+ const createFunc = undoable(
+ action(() => {
+ const note = this._sidebarRef.current?.anchorMenuClick(this.getAnchor(true), ['latitude', 'longitude', '-linkedTo']);
+ if (note && this.selectedPin) {
+ note.latitude = this.selectedPin.latitude;
+ note.longitude = this.selectedPin.longitude;
+ note.map = this.selectedPin.map;
+ }
+ }),
+ 'create note annotation'
+ );
+ if (!this.layoutDoc.layout_showSidebar) {
+ this.toggleSidebar();
+ setTimeout(createFunc);
+ } else createFunc();
+ };
sidebarDown = (e: React.PointerEvent) => {
setupMoveUpEvents(this, e, this.sidebarMove, emptyFunction, () => setTimeout(this.toggleSidebar), true);
};
@@ -483,7 +243,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
return false;
};
- setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean) => void) => (this._setPreviewCursor = func);
+ setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void) => (this._setPreviewCursor = func);
@action
onMarqueeDown = (e: React.PointerEvent) => {
@@ -504,48 +264,13 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
};
@action finishMarquee = (x?: number, y?: number) => {
this._marqueeing = undefined;
- this._isAnnotating = false;
- x !== undefined && y !== undefined && this._setPreviewCursor?.(x, y, false, false);
+ x !== undefined && y !== undefined && this._setPreviewCursor?.(x, y, false, false, this.rootDoc);
};
- addDocumentWrapper = (doc: Doc | Doc[], annotationKey?: string) => {
- return this.addDocument(doc, annotationKey);
- };
+ addDocumentWrapper = (doc: Doc | Doc[], annotationKey?: string) => this.addDocument(doc, annotationKey);
pointerEvents = () => (this.props.isContentActive() && !MarqueeOptionsMenu.Instance.isShown() ? 'all' : 'none');
- @computed get annotationLayer() {
- return (
- <div className="mapBox-annotationLayer" style={{ height: Doc.NativeHeight(this.Document) || undefined }} ref={this._annotationLayer}>
- {this.inlineTextAnnotations
- .sort((a, b) => NumCast(a.y) - NumCast(b.y))
- .map(anno => (
- <Annotation key={`${anno[Id]}-annotation`} {...this.props} fieldKey={this.annotationKey} pointerEvents={this.pointerEvents} showInfo={this.showInfo} dataDoc={this.dataDoc} anno={anno} />
- ))}
- </div>
- );
- }
-
- getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => AnchorMenu.Instance?.GetAnchor(this._savedAnnotations, addAsAnnotation) ?? this.rootDoc;
-
- /**
- * render contents in allMapMarkers (e.g. images with exifData) into google maps as map marker
- * @returns
- */
- private renderMarkers = () => {
- return this.allMapMarkers.map(place => (
- <Marker key={place[Id]} position={{ lat: NumCast(place.lat), lng: NumCast(place.lng) }} onLoad={marker => this.markerLoadHandler(marker, place)} onClick={(e: google.maps.MapMouseEvent) => this.markerClickHandler(e, place)} />
- ));
- };
-
- // TODO: auto center on select a document in the sidebar
- private handleMapCenter = (map: google.maps.Map) => {
- // console.log("print the selected views in selectionManager:")
- // if (SelectionManager.Views().lastElement()) {
- // console.log(SelectionManager.Views().lastElement());
- // }
- };
-
panelWidth = () => this.props.PanelWidth() / (this.props.NativeDimScaling?.() || 1) - this.sidebarWidth();
panelHeight = () => this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1);
scrollXf = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._layout_scrollTop));
@@ -582,24 +307,409 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
} else {
this._bingSearchManager.geocode({
where: query,
- callback: action((r: any) => {
- res(r.results[0].location);
- }),
+ callback: action((r: any) => res(r.results[0].location)),
errorCallback: (e: any) => reject(),
});
}
});
};
+ @observable
+ bingSearchBarContents: any = this.rootDoc.map; // For Bing Maps: The contents of the Bing search bar (string)
+
+ geoDataRequestOptions = {
+ entityType: 'PopulatedPlace',
+ };
+
+ // incrementer: number = 0;
+ /*
+ * Creates Pushpin doc and adds it to the list of annotations
+ */
+ @action
+ createPushpin = undoable((latitude: number, longitude: number, map?: string) => {
+ // Stores the pushpin as a MapMarkerDocument
+ const pushpin = Docs.Create.PushpinDocument(
+ NumCast(latitude),
+ NumCast(longitude),
+ false,
+ [],
+ { title: map ?? `lat=${latitude},lng=${longitude}`, map: map }
+ // ,'pushpinIDamongus'+ this.incrementer++
+ );
+ this.addDocument(pushpin, this.annotationKey);
+ return pushpin;
+ // mapMarker.infoWindowOpen = true;
+ }, 'createpin');
+
+ // The pin that is selected
+ @observable selectedPin: Doc | undefined;
+
+ @action
+ deselectPin = () => {
+ if (this.selectedPin) {
+ // Removes filter
+ Doc.setDocFilter(this.rootDoc, 'latitude', this.selectedPin.latitude, 'remove');
+ Doc.setDocFilter(this.rootDoc, 'longitude', this.selectedPin.longitude, 'remove');
+ Doc.setDocFilter(this.rootDoc, '-linkedTo', `mapPin=${Field.toScriptString(DocCast(this.selectedPin))}`, 'remove');
+
+ const temp = this.selectedPin;
+ if (!this._unmounting) {
+ this._bingMap.current.entities.remove(this.map_docToPinMap.get(temp));
+ }
+ const newpin = new this.MicrosoftMaps.Pushpin(new this.MicrosoftMaps.Location(temp.latitude, temp.longitude));
+ this.MicrosoftMaps.Events.addHandler(newpin, 'click', (e: any) => this.pushpinClicked(temp as Doc));
+ if (!this._unmounting) {
+ this._bingMap.current.entities.push(newpin);
+ }
+ this.map_docToPinMap.set(temp, newpin);
+ this.selectedPin = undefined;
+ this.bingSearchBarContents = this.rootDoc.map;
+ }
+ };
+
+ getView = async (doc: Doc) => {
+ if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) this.toggleSidebar();
+ return new Promise<Opt<DocumentView>>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv)));
+ };
+ /*
+ * Pushpin onclick
+ */
+ @action
+ pushpinClicked = (pinDoc: Doc) => {
+ this.deselectPin();
+ this.selectedPin = pinDoc;
+ this.bingSearchBarContents = pinDoc.map;
+
+ // Doc.setDocFilter(this.rootDoc, 'latitude', this.selectedPin.latitude, 'match');
+ // Doc.setDocFilter(this.rootDoc, 'longitude', this.selectedPin.longitude, 'match');
+ Doc.setDocFilter(this.rootDoc, '-linkedTo', `mapPin=${Field.toScriptString(this.selectedPin)}`, 'check');
+
+ this.recolorPin(this.selectedPin, 'green');
+
+ MapAnchorMenu.Instance.Delete = this.deleteSelectedPin;
+ MapAnchorMenu.Instance.Center = this.centerOnSelectedPin;
+ MapAnchorMenu.Instance.LinkNote = this.createNoteAnnotation;
+
+ const point = this._bingMap.current.tryLocationToPixel(new this.MicrosoftMaps.Location(this.selectedPin.latitude, this.selectedPin.longitude));
+ const x = point.x + (this.props.PanelWidth() - this.sidebarWidth()) / 2;
+ const y = point.y + this.props.PanelHeight() / 2 + 32;
+ const cpt = this.props.ScreenToLocalTransform().inverse().transformPoint(x, y);
+ MapAnchorMenu.Instance.jumpTo(cpt[0], cpt[1], true);
+
+ document.addEventListener('pointerdown', this.tryHideMapAnchorMenu, true);
+ };
+
+ /**
+ * Map OnClick
+ */
+ @action
+ mapOnClick = (e: { location: { latitude: any; longitude: any } }) => {
+ this.props.select(false);
+ this.deselectPin();
+ };
+ /*
+ * Updates values of layout doc to match the current map
+ */
+ @action
+ mapRecentered = () => {
+ if (
+ Math.abs(NumCast(this.dataDoc.latitude) - this._bingMap.current.getCenter().latitude) > 1e-7 || //
+ Math.abs(NumCast(this.dataDoc.longitude) - this._bingMap.current.getCenter().longitude) > 1e-7
+ ) {
+ this.dataDoc.latitude = this._bingMap.current.getCenter().latitude;
+ this.dataDoc.longitude = this._bingMap.current.getCenter().longitude;
+ this.dataDoc.map = '';
+ this.bingSearchBarContents = '';
+ }
+ this.dataDoc.map_zoom = this._bingMap.current.getZoom();
+ };
+ /*
+ * Updates maptype
+ */
+ @action
+ updateMapType = () => (this.dataDoc.map_type = this._bingMap.current.getMapTypeId());
+
+ /*
+ * For Bing Maps
+ * Called by search button's onClick
+ * Finds the geocode of the searched contents and sets location to that location
+ **/
+ @action
+ bingSearch = () => {
+ return this.bingGeocode(this._bingMap, this.bingSearchBarContents).then(location => {
+ this.dataDoc.latitude = location.latitude;
+ this.dataDoc.longitude = location.longitude;
+ this.dataDoc.map_zoom = this._bingMap.current.getZoom();
+ this.dataDoc.map = this.bingSearchBarContents;
+ });
+ };
+
+ /*
+ * Returns doc w/ relevant info
+ */
+ getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps, existingPin?: Doc) => {
+ /// this should use SELECTED pushpin for lat/long if there is a selection, otherwise CENTER
+ const anchor = Docs.Create.ConfigDocument({
+ title: 'MapAnchor:' + this.rootDoc.title,
+ text: StrCast(this.selectedPin?.map) || StrCast(this.rootDoc.map) || 'map location',
+ config_latitude: NumCast((existingPin ?? this.selectedPin)?.latitude ?? this.dataDoc.latitude),
+ config_longitude: NumCast((existingPin ?? this.selectedPin)?.longitude ?? this.dataDoc.longitude),
+ config_map_zoom: NumCast(this.dataDoc.map_zoom),
+ config_map_type: StrCast(this.dataDoc.map_type),
+ config_map: StrCast((existingPin ?? this.selectedPin)?.map) || StrCast(this.dataDoc.map),
+ layout_unrendered: true,
+ mapPin: existingPin ?? this.selectedPin,
+ annotationOn: this.rootDoc,
+ });
+ if (anchor) {
+ if (!addAsAnnotation) anchor.backgroundColor = 'transparent';
+ addAsAnnotation && this.addDocument(anchor);
+ PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), map: true } }, this.rootDoc);
+ return anchor;
+ }
+ return this.rootDoc;
+ };
+
+ map_docToPinMap = new Map<Doc, any>();
+ map_pinHighlighted = new Map<Doc, boolean>();
+ /*
+ * Input: pin doc
+ * Adds MicrosoftMaps Pushpin to the map (render)
+ */
+ @action
+ addPushpin = (pin: Doc) => {
+ const pushPin = pin.infoWindowOpen
+ ? new this.MicrosoftMaps.Pushpin(new this.MicrosoftMaps.Location(pin.latitude, pin.longitude), {})
+ : new this.MicrosoftMaps.Pushpin(
+ new this.MicrosoftMaps.Location(pin.latitude, pin.longitude)
+ // {icon: 'http://icons.iconarchive.com/icons/icons-land/vista-map-markers/24/Map-Marker-Marker-Outside-Chartreuse-icon.png'}
+ );
+
+ this._bingMap.current.entities.push(pushPin);
+
+ this.MicrosoftMaps.Events.addHandler(pushPin, 'click', (e: any) => this.pushpinClicked(pin));
+ // this.MicrosoftMaps.Events.addHandler(pushPin, 'dblclick', (e: any) => this.pushpinDblClicked(pushPin, pin));
+ this.map_docToPinMap.set(pin, pushPin);
+ };
+
+ /*
+ * Input: pin doc
+ * Removes pin from annotations
+ */
+ @action
+ removePushpin = (pinDoc: Doc) => this.removeMapDocument(pinDoc, this.annotationKey);
+
+ /*
+ * Removes pushpin from map render
+ */
+ deletePushpin = (pinDoc: Doc) => {
+ if (!this._unmounting) {
+ this._bingMap.current.entities.remove(this.map_docToPinMap.get(pinDoc));
+ }
+ this.map_docToPinMap.delete(pinDoc);
+ this.selectedPin = undefined;
+ };
+
+ @action
+ deleteSelectedPin = undoable(() => {
+ if (this.selectedPin) {
+ // Removes filter
+ Doc.setDocFilter(this.rootDoc, 'latitude', this.selectedPin.latitude, 'remove');
+ Doc.setDocFilter(this.rootDoc, 'longitude', this.selectedPin.longitude, 'remove');
+ Doc.setDocFilter(this.rootDoc, '-linkedTo', `mapPin=${Field.toString(DocCast(this.selectedPin.mapPin))}`, 'remove');
+
+ this.removePushpin(this.selectedPin);
+ }
+ MapAnchorMenu.Instance.fadeOut(true);
+ document.removeEventListener('pointerdown', this.tryHideMapAnchorMenu, true);
+ }, 'delete pin');
+
+ tryHideMapAnchorMenu = (e: PointerEvent) => {
+ let target = document.elementFromPoint(e.x, e.y);
+ while (target) {
+ if (target === MapAnchorMenu.top.current) return;
+ target = target.parentElement;
+ }
+ e.stopPropagation();
+ e.preventDefault();
+ MapAnchorMenu.Instance.fadeOut(true);
+ document.removeEventListener('pointerdown', this.tryHideMapAnchorMenu, true);
+ };
+
+ @action
+ centerOnSelectedPin = () => {
+ if (this.selectedPin) {
+ this.dataDoc.latitude = this.selectedPin.latitude;
+ this.dataDoc.longitude = this.selectedPin.longitude;
+ this.dataDoc.map = this.selectedPin.map ?? '';
+ this.bingSearchBarContents = this.selectedPin.map;
+ }
+ MapAnchorMenu.Instance.fadeOut(true);
+ document.removeEventListener('pointerdown', this.tryHideMapAnchorMenu);
+ };
+
+ /**
+ * View options for bing maps
+ */
bingViewOptions = {
- center: { latitude: defaultCenter.lat, longitude: defaultCenter.lng },
+ // center: { latitude: this.dataDoc.latitude ?? defaultCenter.lat, longitude: this.dataDoc.longitude ?? defaultCenter.lng },
+ zoom: this.dataDoc.latitude ?? 10,
mapTypeId: 'grayscale',
};
+
+ /**
+ * Map options
+ */
bingMapOptions = {
navigationBarMode: 'square',
+ backgroundColor: '#f1f3f4',
+ enableInertia: true,
+ supportedMapTypes: ['grayscale', 'canvasLight'],
+ disableMapTypeSelectorMouseOver: true,
+ // showScalebar:true
+ // disableRoadView:true,
+ // disableBirdseye:true
+ streetsideOptions: {
+ showProblemReporting: false,
+ showCurrentAddress: false,
+ },
+ };
+
+ @action
+ searchbarOnEdit = (newText: string) => (this.bingSearchBarContents = newText);
+
+ recolorPin = (pin: Doc, color?: string) => {
+ this._bingMap.current.entities.remove(this.map_docToPinMap.get(pin));
+ this.map_docToPinMap.delete(pin);
+ const newpin = new this.MicrosoftMaps.Pushpin(new this.MicrosoftMaps.Location(pin.latitude, pin.longitude), color ? { color } : {});
+ this.MicrosoftMaps.Events.addHandler(newpin, 'click', (e: any) => this.pushpinClicked(pin));
+ this._bingMap.current.entities.push(newpin);
+ this.map_docToPinMap.set(pin, newpin);
+ };
+
+ /*
+ * Called when BingMap is first rendered
+ * Initializes starting values
+ */
+ @observable _mapReady = false;
+ @action
+ bingMapReady = (map: any) => {
+ this._mapReady = true;
+ this._bingMap = map.map;
+ if (!this._bingMap.current) {
+ alert('NO Map!?');
+ }
+ this.MicrosoftMaps.Events.addHandler(this._bingMap.current, 'click', this.mapOnClick);
+ this.MicrosoftMaps.Events.addHandler(this._bingMap.current, 'viewchangeend', undoable(this.mapRecentered, 'Map Layout Change'));
+ this.MicrosoftMaps.Events.addHandler(this._bingMap.current, 'maptypechanged', undoable(this.updateMapType, 'Map ViewType Change'));
+
+ this._disposers.mapLocation = reaction(
+ () => this.rootDoc.map,
+ mapLoc => (this.bingSearchBarContents = mapLoc),
+ { fireImmediately: true }
+ );
+ this._disposers.highlight = reaction(
+ () => this.allAnnotations.map(doc => doc[Highlight]),
+ () => {
+ const allConfigPins = this.allAnnotations.map(doc => ({ doc, pushpin: DocCast(doc.mapPin) })).filter(pair => pair.pushpin);
+ allConfigPins.forEach(({ doc, pushpin }) => {
+ if (!pushpin[Highlight] && this.map_pinHighlighted.get(pushpin)) {
+ this.recolorPin(pushpin);
+ this.map_pinHighlighted.delete(pushpin);
+ }
+ });
+ allConfigPins.forEach(({ doc, pushpin }) => {
+ if (doc[Highlight] && !this.map_pinHighlighted.get(pushpin)) {
+ this.recolorPin(pushpin, 'orange');
+ this.map_pinHighlighted.set(pushpin, true);
+ }
+ });
+ },
+ { fireImmediately: true }
+ );
+
+ this._disposers.location = reaction(
+ () => ({ lat: this.rootDoc.latitude, lng: this.rootDoc.longitude, zoom: this.rootDoc.map_zoom, mapType: this.rootDoc.map_type }),
+ locationObject => {
+ // if (this._bingMap.current)
+ try {
+ locationObject?.zoom &&
+ this._bingMap.current?.setView({
+ mapTypeId: locationObject.mapType,
+ zoom: locationObject.zoom,
+ center: new this.MicrosoftMaps.Location(locationObject.lat, locationObject.lng),
+ });
+ } catch (e) {
+ console.log(e);
+ }
+ },
+ { fireImmediately: true }
+ );
+ };
+
+ dragToggle = (e: React.PointerEvent) => {
+ let dragClone: HTMLDivElement | undefined;
+
+ setupMoveUpEvents(
+ e,
+ e,
+ e => {
+ if (!dragClone) {
+ dragClone = this._dragRef.current?.cloneNode(true) as HTMLDivElement;
+ dragClone.style.position = 'absolute';
+ dragClone.style.zIndex = '10000';
+ DragManager.Root().appendChild(dragClone);
+ }
+ dragClone.style.transform = `translate(${e.clientX - 15}px, ${e.clientY - 15}px)`;
+ return false;
+ },
+ e => {
+ if (!dragClone) return;
+ DragManager.Root().removeChild(dragClone);
+ let target = document.elementFromPoint(e.x, e.y);
+ while (target) {
+ if (target === this._ref.current) {
+ const cpt = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY);
+ const x = cpt[0] - (this.props.PanelWidth() - this.sidebarWidth()) / 2;
+ const y = cpt[1] - 32 /* height of search bar */ - this.props.PanelHeight() / 2;
+ const location = this._bingMap.current.tryPixelToLocation(new this.MicrosoftMaps.Point(x, y));
+ this.createPushpin(location.latitude, location.longitude);
+ break;
+ }
+ target = target.parentElement;
+ }
+ },
+ e => {
+ const createPin = () => this.createPushpin(this.rootDoc.latitude, this.rootDoc.longitude, this.rootDoc.map);
+ if (this.bingSearchBarContents) {
+ this.bingSearch().then(createPin);
+ } else createPin();
+ }
+ );
};
- bingMapReady = (map: any) => (this._bingMap = map.map);
+
+ searchbarKeyDown = (e: any) => e.key === 'Enter' && this.bingSearch();
+
+ static _firstRender = true;
+ static _rerenderDelay = 500;
+ _rerenderTimeout: any;
render() {
+ // bcz: no idea what's going on here, but bings maps have some kind of bug
+ // such that we need to delay rendering a second map on startup until the first map is rendered.
+ this.rootDoc[DocCss];
+ if (MapBox._rerenderDelay) {
+ // prettier-ignore
+ this._rerenderTimeout = this._rerenderTimeout ??
+ setTimeout(action(() => {
+ if ((window as any).Microsoft?.Maps?.Internal._WorkDispatcher) {
+ MapBox._rerenderDelay = 0;
+ }
+ this._rerenderTimeout = undefined;
+ this.rootDoc[DocCss] = this.rootDoc[DocCss] + 1;
+ }), MapBox._rerenderDelay);
+ return null;
+ }
+
const renderAnnotations = (childFilters?: () => string[]) => null;
return (
<div className="mapBox" ref={this._ref}>
@@ -608,47 +718,87 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
onWheel={e => e.stopPropagation()}
onPointerDown={async e => {
e.button === 0 && !e.ctrlKey && e.stopPropagation();
- // just a simple test of bing maps geocode api
- // const loc = await this.bingGeocode(this._bingMap, 'Philadelphia, PA');
- // this._bingMap.current.setView({
- // mapTypeId: this.MicrosoftMaps.MapTypeId.aerial,
- // center: new this.MicrosoftMaps.Location(loc.latitude, loc.longitude),
- // zoom: 15,
- // });
}}
- style={{ width: `calc(100% - ${this.layout_sidebarWidthPercent})`, pointerEvents: this.pointerEvents() }}>
+ style={{ width: `calc(100% - ${this.sidebarWidthPercent})`, pointerEvents: this.pointerEvents() }}>
<div style={{ mixBlendMode: 'multiply' }}>{renderAnnotations(this.transparentFilter)}</div>
{renderAnnotations(this.opaqueFilter)}
{SnappingManager.GetIsDragging() ? null : renderAnnotations()}
- {this.annotationLayer}
-
- {!MapBox.UseBing ? null : <BingMapsReact onMapReady={this.bingMapReady} bingMapsKey={bingApiKey} height="100%" mapOptions={this.bingMapOptions} width="100%" viewOptions={this.bingViewOptions} />}
- <div style={{ display: MapBox.UseBing ? 'none' : undefined }}>
- <GoogleMap mapContainerStyle={mapContainerStyle} onZoomChanged={this.zoomChanged} onCenterChanged={this.centered} onLoad={this.loadHandler} options={mapOptions}>
- <Autocomplete onLoad={this.setSearchBox} onPlaceChanged={this.handlePlaceChanged}>
- <input className="mapBox-input" ref={this.inputRef} type="text" onKeyDown={e => e.stopPropagation()} placeholder="Enter location" />
- </Autocomplete>
-
- {this.renderMarkers()}
- {this.allMapMarkers
- .filter(marker => marker.infoWindowOpen)
- .map(marker => (
- <MapBoxInfoWindow
- key={marker[Id]}
- {...this.props}
- setContentView={emptyFunction}
- place={marker}
- markerMap={this.markerMap}
- PanelWidth={this.infoWidth}
- PanelHeight={this.infoHeight}
- moveDocument={this.moveDocument}
- isAnyChildContentActive={this.isAnyChildContentActive}
- whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
- />
- ))}
- {/* {this.handleMapCenter(this._map)} */}
- </GoogleMap>
+
+ <div className="mapBox-searchbar">
+ <EditableText
+ // editing
+ setVal={(newText: string | number) => typeof newText === 'string' && this.searchbarOnEdit(newText)}
+ onEnter={e => this.bingSearch()}
+ placeholder={this.bingSearchBarContents || 'enter city/zip/...'}
+ textAlign="center"
+ />
+ <IconButton
+ icon={
+ <svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="magnifying-glass" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" color="#DFDFDF">
+ <path
+ fill="currentColor"
+ d="M416 208c0 45.9-14.9 88.3-40 122.7L502.6 457.4c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L330.7 376c-34.4 25.2-76.8 40-122.7 40C93.1 416 0 322.9 0 208S93.1 0 208 0S416 93.1 416 208zM208 352a144 144 0 1 0 0-288 144 144 0 1 0 0 288z"></path>
+ </svg>
+ }
+ onClick={this.bingSearch}
+ type={Type.TERT}
+ />
+ <div style={{ width: 30, height: 30 }} ref={this._dragRef} onPointerDown={this.dragToggle}>
+ <Button tooltip="drag to place a pushpin" icon={<FontAwesomeIcon size={'lg'} icon={'bullseye'} />} />
+ </div>
</div>
+
+ <BingMapsReact
+ onMapReady={this.bingMapReady} //
+ bingMapsKey={bingApiKey}
+ height="100%"
+ mapOptions={this.bingMapOptions}
+ width="100%"
+ viewOptions={this.bingViewOptions}
+ />
+ <div>
+ {!this._mapReady
+ ? null
+ : this.allAnnotations
+ .filter(anno => !anno.layout_unrendered)
+ .map((pushpin, i) => (
+ <DocumentView
+ key={i}
+ {...this.props}
+ renderDepth={this.props.renderDepth + 1}
+ Document={pushpin}
+ DataDoc={undefined}
+ PanelWidth={returnOne}
+ PanelHeight={returnOne}
+ NativeWidth={returnOne}
+ NativeHeight={returnOne}
+ onKey={undefined}
+ onDoubleClick={undefined}
+ onBrowseClick={undefined}
+ childFilters={returnEmptyFilter}
+ childFiltersByRanges={returnEmptyFilter}
+ searchFilterDocs={returnEmptyDoclist}
+ isDocumentActive={returnFalse}
+ isContentActive={returnFalse}
+ addDocTab={returnFalse}
+ ScreenToLocalTransform={Transform.Identity}
+ fitContentsToBox={undefined}
+ focus={returnOne}
+ />
+ ))}
+ </div>
+ {/* <MapBoxInfoWindow
+ key={Docs.Create.MapMarkerDocument(NumCast(40), NumCast(40), false, [], {})[Id]}
+ {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
+ place={Docs.Create.MapMarkerDocument(NumCast(40), NumCast(40), false, [], {})}
+ markerMap={this.markerMap}
+ PanelWidth={this.infoWidth}
+ PanelHeight={this.infoHeight}
+ moveDocument={this.moveDocument}
+ isAnyChildContentActive={this.isAnyChildContentActive}
+ whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
+ /> */}
+
{!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? null : (
<MarqueeAnnotator
rootDoc={this.rootDoc}
@@ -667,7 +817,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
)}
</div>
{/* </LoadScript > */}
- <div className="mapBox-sidebar" style={{ position: 'absolute', right: 0, height: '100%', width: `${this.layout_sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}>
+ <div className="mapBox-sidebar" style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}>
<SidebarAnnos
ref={this._sidebarRef}
{...this.props}
diff --git a/src/client/views/nodes/MapBox/MapBox2.tsx b/src/client/views/nodes/MapBox/MapBox2.tsx
index a9154c5bb..407a91dd0 100644
--- a/src/client/views/nodes/MapBox/MapBox2.tsx
+++ b/src/client/views/nodes/MapBox/MapBox2.tsx
@@ -98,9 +98,9 @@ export class MapBox2 extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
public get SidebarKey() {
return this.fieldKey + '_sidebar';
}
- private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean) => void);
+ private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void);
@computed get inlineTextAnnotations() {
- return this.allMapMarkers.filter(a => a.textInlineAnnotations);
+ return this.allMapMarkers.filter(a => a.text_inlineAnnotations);
}
@observable private _map: google.maps.Map = null as unknown as google.maps.Map;
@@ -229,7 +229,7 @@ export class MapBox2 extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
map: map,
});
map.panTo(position);
- const mapMarker = Docs.Create.MapMarkerDocument(NumCast(position.lat()), NumCast(position.lng()), false, [], {});
+ const mapMarker = Docs.Create.PushpinDocument(NumCast(position.lat()), NumCast(position.lng()), false, [], {});
this.addDocument(mapMarker, this.annotationKey);
};
@@ -260,7 +260,7 @@ export class MapBox2 extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
// } else {
// alert("Your geolocation is not supported by browser.")
// };
- map.setZoom(NumCast(this.dataDoc.mapZoom, 2.5));
+ map.setZoom(NumCast(this.dataDoc.map_zoom, 2.5));
map.setCenter(new google.maps.LatLng(NumCast(this.dataDoc.mapLat), NumCast(this.dataDoc.mapLng)));
setTimeout(() => {
if (this._loadPending && this._map.getBounds()) {
@@ -292,7 +292,7 @@ export class MapBox2 extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
this._loadPending = false;
this.layoutDoc.freeform_fitContentsToBox && this.fitBounds(this._map);
}
- this.dataDoc.mapZoom = this._map.getZoom();
+ this.dataDoc.map_zoom = this._map.getZoom();
};
/**
@@ -333,7 +333,7 @@ export class MapBox2 extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
if (existingMarker) {
Doc.AddDocToList(existingMarker, 'data', doc);
} else {
- const marker = Docs.Create.MapMarkerDocument(NumCast(doc.lat), NumCast(doc.lng), false, [doc], {});
+ const marker = Docs.Create.PushpinDocument(NumCast(doc.lat), NumCast(doc.lng), false, [doc], {});
this.addDocument(marker, this.annotationKey);
}
}
@@ -502,7 +502,7 @@ export class MapBox2 extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
@action finishMarquee = (x?: number, y?: number) => {
this._marqueeing = undefined;
this._isAnnotating = false;
- x !== undefined && y !== undefined && this._setPreviewCursor?.(x, y, false, false);
+ x !== undefined && y !== undefined && this._setPreviewCursor?.(x, y, false, false, this.props.Document);
};
addDocumentWrapper = (doc: Doc | Doc[], annotationKey?: string) => {
diff --git a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx
index 577101445..66c47d131 100644
--- a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx
+++ b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx
@@ -47,7 +47,9 @@ export class MapBoxInfoWindow extends React.Component<MapBoxInfoWindowProps & Vi
removeDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((p, d) => p && Doc.RemoveDocFromList(this.props.place, 'data', d), true as boolean);
render() {
return (
- <InfoWindow anchor={this.props.markerMap[this.props.place[Id]]} onCloseClick={this.handleInfoWindowClose}>
+ <InfoWindow
+ // anchor={this.props.markerMap[this.props.place[Id]]}
+ onCloseClick={this.handleInfoWindowClose}>
<div className="mapbox-infowindow">
<div style={{ width: this.props.PanelWidth(), height: this.props.PanelHeight() }}>
<CollectionStackingView
diff --git a/src/client/views/nodes/MapBox/MapPushpinBox.tsx b/src/client/views/nodes/MapBox/MapPushpinBox.tsx
new file mode 100644
index 000000000..42bada0ef
--- /dev/null
+++ b/src/client/views/nodes/MapBox/MapPushpinBox.tsx
@@ -0,0 +1,34 @@
+import { observer } from 'mobx-react';
+// import { SettingsManager } from '../../../util/SettingsManager';
+import { ViewBoxBaseComponent } from '../../DocComponent';
+import { FieldView, FieldViewProps } from '../FieldView';
+import React = require('react');
+import { computed } from 'mobx';
+import { MapBox } from './MapBox';
+
+/**
+ * Map Pushpin doc class
+ */
+@observer
+export class MapPushpinBox extends ViewBoxBaseComponent<FieldViewProps>() {
+ public static LayoutString(fieldKey: string) {
+ return FieldView.LayoutString(MapPushpinBox, fieldKey);
+ }
+ componentDidMount() {
+ this.mapBoxView.addPushpin(this.rootDoc);
+ }
+ componentWillUnmount() {
+ this.mapBoxView.deletePushpin(this.rootDoc);
+ }
+
+ @computed get mapBoxView() {
+ return this.props.DocumentView?.()?.props.docViewPath().lastElement()?.ComponentView as MapBox;
+ }
+ @computed get mapBox() {
+ return this.props.DocumentView?.().props.docViewPath().lastElement()?.rootDoc;
+ }
+
+ render() {
+ return <div />;
+ }
+}
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index fd4c6366b..39e27d15b 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -213,7 +213,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
);
}
- brushView = (view: { width: number; height: number; panX: number; panY: number }) => this._pdfViewer?.brushView(view);
+ brushView = (view: { width: number; height: number; panX: number; panY: number }, transTime: number) => this._pdfViewer?.brushView(view, transTime);
sidebarAddDocTab = (doc: Doc, where: OpenWhere) => {
if (DocListCast(this.props.Document[this.props.fieldKey + '_sidebar']).includes(doc) && !this.SidebarShown) {
@@ -224,7 +224,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
};
focus = (anchor: Doc, options: DocFocusOptions) => {
this._initialScrollTarget = anchor;
- return this._pdfViewer?.scrollFocus(anchor, NumCast(anchor.y, NumCast(anchor.presViewScroll)), options);
+ return this._pdfViewer?.scrollFocus(anchor, NumCast(anchor.y, NumCast(anchor.config_scrollTop)), options);
};
getView = async (doc: Doc) => {
@@ -243,14 +243,13 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
title: StrCast(this.rootDoc.title + '@' + NumCast(this.layoutDoc._layout_scrollTop)?.toFixed(0)),
annotationOn: this.rootDoc,
});
- const annoAnchor = this._pdfViewer?._getAnchor(this._pdfViewer.savedAnnotations(), true);
- const anchor = annoAnchor ?? docAnchor();
+ const visibleAnchor = this._pdfViewer?._getAnchor(this._pdfViewer.savedAnnotations(), true);
+ const anchor = visibleAnchor ?? 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);
- }
+ anchor.text_html = ele?.innerHTML;
+ addAsAnnotation && this.addDocument(anchor);
+
return anchor;
};
@@ -439,7 +438,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
return PDFBox.sidebarResizerWidth + nativeDiff * (this.props.NativeDimScaling?.() || 1);
};
@undoBatch
- toggleSidebarType = () => (this.rootDoc.sidebar_collectionType = this.rootDoc.sidebar_collectionType === CollectionViewType.Freeform ? CollectionViewType.Stacking : CollectionViewType.Freeform);
+ toggleSidebarType = () => (this.rootDoc[this.SidebarKey + '_type_collection'] = this.rootDoc[this.SidebarKey + '_type_collection'] === CollectionViewType.Freeform ? CollectionViewType.Stacking : CollectionViewType.Freeform);
specificContextMenu = (e: React.MouseEvent): void => {
const cm = ContextMenu.Instance;
const options = cm.findByDescription('Options...');
@@ -555,7 +554,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
};
return (
<div className={'formattedTextBox-sidebar' + (Doc.ActiveTool !== InkTool.None ? '-inking' : '')} style={{ width: '100%', right: 0, backgroundColor: `white` }}>
- {renderComponent(StrCast(this.layoutDoc.sidebar_collectionType))}
+ {renderComponent(StrCast(this.layoutDoc[this.SidebarKey + '_type_collection']))}
</div>
);
}
diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
index 04f11a5df..481e43feb 100644
--- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx
@@ -1,31 +1,38 @@
import { action, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
+import { DateField } from '../../../../fields/DateField';
+import { Doc, DocListCast } from '../../../../fields/Doc';
+import { Id } from '../../../../fields/FieldSymbols';
+import { List } from '../../../../fields/List';
+import { BoolCast, DocCast } from '../../../../fields/Types';
import { VideoField } from '../../../../fields/URLField';
import { Upload } from '../../../../server/SharedMediaTypes';
+import { Docs } from '../../../documents/Documents';
+import { DocumentType } from '../../../documents/DocumentTypes';
+import { DocumentManager } from '../../../util/DocumentManager';
+import { DragManager } from '../../../util/DragManager';
+import { ScriptingGlobals } from '../../../util/ScriptingGlobals';
+import { SelectionManager } from '../../../util/SelectionManager';
+import { Presentation } from '../../../util/TrackMovements';
+import { undoBatch } from '../../../util/UndoManager';
+import { CollectionFreeFormView } from '../../collections/collectionFreeForm/CollectionFreeFormView';
import { ViewBoxBaseComponent } from '../../DocComponent';
-import { FieldView } from '../FieldView';
+import { media_state } from '../AudioBox';
+import { FieldView, FieldViewProps } 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 { DocCast } from '../../../../fields/Types';
@observer
-export class RecordingBox extends ViewBoxBaseComponent() {
+export class RecordingBox extends ViewBoxBaseComponent<FieldViewProps>() {
public static LayoutString(fieldKey: string) {
return FieldView.LayoutString(RecordingBox, fieldKey);
}
private _ref: React.RefObject<HTMLDivElement> = React.createRef();
- constructor(props: any) {
- super(props);
- }
-
componentDidMount() {
+ this.props.setContentView?.(this);
Doc.SetNativeWidth(this.dataDoc, 1280);
Doc.SetNativeHeight(this.dataDoc, 720);
}
@@ -34,9 +41,7 @@ export class RecordingBox extends ViewBoxBaseComponent() {
@observable videoDuration: number | undefined = undefined;
@action
- setVideoDuration = (duration: number) => {
- this.videoDuration = duration;
- };
+ setVideoDuration = (duration: number) => (this.videoDuration = duration);
@action
setResult = (info: Upload.AccessPathInfo, presentation?: Presentation) => {
@@ -46,20 +51,209 @@ export class RecordingBox extends ViewBoxBaseComponent() {
this.dataDoc.layout = VideoBox.LayoutString(this.fieldKey);
this.dataDoc[this.props.fieldKey] = new VideoField(this.result.accessPaths.client);
- this.dataDoc[this.fieldKey + '-recorded'] = true;
+ 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);
+ this.dataDoc[this.fieldKey + '_presentation'] = JSON.stringify(presCopy);
}
};
+ @undoBatch
+ @action
+ public static WorkspaceStopRecording() {
+ const remDoc = RecordingBox.screengrabber?.rootDoc;
+ if (remDoc) {
+ //if recordingbox is true; when we press the stop button. changed vals temporarily to see if changes happening
+ RecordingBox.screengrabber?.Pause?.();
+ setTimeout(() => {
+ RecordingBox.screengrabber?.Finish?.();
+ remDoc.overlayX = 70; //was 100
+ remDoc.overlayY = 590;
+ RecordingBox.screengrabber = undefined;
+ }, 100);
+ //could break if recording takes too long to turn into videobox. If so, either increase time on setTimeout below or find diff place to do this
+ setTimeout(() => Doc.RemFromMyOverlay(remDoc), 1000);
+ Doc.UserDoc().workspaceRecordingState = media_state.Paused;
+ Doc.AddDocToList(Doc.UserDoc(), 'workspaceRecordings', remDoc);
+ }
+ }
+
+ /**
+ * This method toggles whether or not we are currently using the RecordingBox to record with the topbar button
+ * @param _readOnly_
+ * @returns
+ */
+ @undoBatch
+ @action
+ public static WorkspaceStartRecording(value: string) {
+ const screengrabber =
+ value === 'Record Workspace'
+ ? Docs.Create.ScreenshotDocument({
+ title: `${new DateField()}-${Doc.ActiveDashboard?.title ?? ''}`,
+ _width: 205,
+ _height: 115,
+ })
+ : Docs.Create.WebCamDocument(`${new DateField()}-${Doc.ActiveDashboard?.title ?? ''}`, {
+ title: `${new DateField()}-${Doc.ActiveDashboard?.title ?? ''}`,
+ _width: 205,
+ _height: 115,
+ });
+ screengrabber.overlayX = 70; //was -400
+ screengrabber.overlayY = 590; //was 0
+ Doc.GetProto(screengrabber)[Doc.LayoutFieldKey(screengrabber) + '_trackScreen'] = true;
+ Doc.AddToMyOverlay(screengrabber); //just adds doc to overlay
+ DocumentManager.Instance.AddViewRenderedCb(screengrabber, docView => {
+ RecordingBox.screengrabber = docView.ComponentView as RecordingBox;
+ RecordingBox.screengrabber.Record?.();
+ });
+ Doc.UserDoc().workspaceRecordingState = media_state.Recording;
+ }
+
+ /**
+ * This method changes the menu depending on whether or not we are in playback mode
+ * @param value RecordingBox rootdoc
+ */
+ @undoBatch
+ @action
+ public static replayWorkspace(value: Doc) {
+ Doc.UserDoc().currentRecording = value;
+ value.overlayX = 70;
+ value.overlayY = window.innerHeight - 180;
+ Doc.AddToMyOverlay(value);
+ DocumentManager.Instance.AddViewRenderedCb(value, docView => {
+ Doc.UserDoc().currentRecording = docView.rootDoc;
+ SelectionManager.SelectSchemaViewDoc(value);
+ RecordingBox.resumeWorkspaceReplaying(value);
+ });
+ }
+
+ /**
+ * Adds the recording box to the canvas
+ * @param value current recordingbox
+ */
+ @undoBatch
+ @action
+ public static addRecToWorkspace(value: RecordingBox) {
+ let ffView = Array.from(DocumentManager.Instance.DocumentViews).find(view => view.ComponentView instanceof CollectionFreeFormView);
+ (ffView?.ComponentView as CollectionFreeFormView).props.addDocument?.(value.rootDoc);
+ Doc.RemoveDocFromList(Doc.UserDoc(), 'workspaceRecordings', value.rootDoc);
+ Doc.RemFromMyOverlay(value.rootDoc);
+ Doc.UserDoc().currentRecording = undefined;
+ Doc.UserDoc().workspaceReplayingState = undefined;
+ Doc.UserDoc().workspaceRecordingState = undefined;
+ }
+
+ @action
+ public static resumeWorkspaceReplaying(doc: Doc) {
+ const docView = DocumentManager.Instance.getDocumentView(doc);
+ if (docView?.ComponentView instanceof VideoBox) {
+ docView.ComponentView.Play();
+ }
+ Doc.UserDoc().workspaceReplayingState = media_state.Playing;
+ }
+
+ @action
+ public static pauseWorkspaceReplaying(doc: Doc) {
+ const docView = DocumentManager.Instance.getDocumentView(doc);
+ const videoBox = docView?.ComponentView as VideoBox;
+ if (videoBox) {
+ videoBox.Pause();
+ }
+ Doc.UserDoc().workspaceReplayingState = media_state.Paused;
+ }
+
+ @action
+ public static stopWorkspaceReplaying(value: Doc) {
+ Doc.RemFromMyOverlay(value);
+ Doc.UserDoc().currentRecording = undefined;
+ Doc.UserDoc().workspaceReplayingState = undefined;
+ Doc.UserDoc().workspaceRecordingState = undefined;
+ Doc.RemFromMyOverlay(value);
+ }
+
+ @undoBatch
+ @action
+ public static removeWorkspaceReplaying(value: Doc) {
+ Doc.RemoveDocFromList(Doc.UserDoc(), 'workspaceRecordings', value);
+ Doc.RemFromMyOverlay(value);
+ Doc.UserDoc().currentRecording = undefined;
+ Doc.UserDoc().workspaceReplayingState = undefined;
+ Doc.UserDoc().workspaceRecordingState = undefined;
+ }
+
+ Record: undefined | (() => void);
+ Pause: undefined | (() => void);
+ Finish: undefined | (() => void);
+ getControls = (record: () => void, pause: () => void, finish: () => void) => {
+ this.Record = record;
+ this.Pause = pause;
+ this.Finish = finish;
+ };
render() {
return (
<div className="recordingBox" ref={this._ref}>
- {!this.result && <RecordingView setResult={this.setResult} setDuration={this.setVideoDuration} id={DocCast(this.rootDoc.proto)?.[Id] || ''} />}
+ {!this.result && (
+ <RecordingView
+ forceTrackScreen={BoolCast(this.layoutDoc[this.fieldKey + '_trackScreen'])}
+ getControls={this.getControls}
+ setResult={this.setResult}
+ setDuration={this.setVideoDuration}
+ id={DocCast(this.rootDoc.proto)?.[Id] || ''}
+ />
+ )}
</div>
);
}
+ static screengrabber: RecordingBox | undefined;
}
+
+ScriptingGlobals.add(function stopWorkspaceRecording() {
+ RecordingBox.WorkspaceStopRecording();
+});
+
+ScriptingGlobals.add(function stopWorkspaceReplaying(value: Doc) {
+ RecordingBox.stopWorkspaceReplaying(value);
+});
+ScriptingGlobals.add(function removeWorkspaceReplaying(value: Doc) {
+ RecordingBox.removeWorkspaceReplaying(value);
+});
+
+ScriptingGlobals.add(function getCurrentRecording() {
+ return Doc.UserDoc().currentRecording;
+});
+ScriptingGlobals.add(function getWorkspaceRecordings() {
+ return new List<any>(['Record Workspace', `Record Webcam`, ...DocListCast(Doc.UserDoc().workspaceRecordings)]);
+});
+ScriptingGlobals.add(function isWorkspaceRecording() {
+ return Doc.UserDoc().workspaceRecordingState === media_state.Recording;
+});
+ScriptingGlobals.add(function isWorkspaceReplaying() {
+ return Doc.UserDoc().workspaceReplayingState;
+});
+ScriptingGlobals.add(function replayWorkspace(value: Doc | string, _readOnly_: boolean) {
+ if (_readOnly_) return DocCast(Doc.UserDoc().currentRecording) ?? 'Record Workspace';
+ if (typeof value === 'string') RecordingBox.WorkspaceStartRecording(value);
+ else RecordingBox.replayWorkspace(value);
+});
+ScriptingGlobals.add(function pauseWorkspaceReplaying(value: Doc, _readOnly_: boolean) {
+ RecordingBox.pauseWorkspaceReplaying(value);
+});
+ScriptingGlobals.add(function resumeWorkspaceReplaying(value: Doc, _readOnly_: boolean) {
+ RecordingBox.resumeWorkspaceReplaying(value);
+});
+
+ScriptingGlobals.add(function startRecordingDrag(value: { doc: Doc | string; e: React.PointerEvent }) {
+ if (DocCast(value.doc)) {
+ DragManager.StartDocumentDrag([value.e.target as HTMLElement], new DragManager.DocumentDragData([DocCast(value.doc)], 'embed'), value.e.clientX, value.e.clientY);
+ value.e.preventDefault();
+ return true;
+ }
+});
+ScriptingGlobals.add(function renderDropdown() {
+ if (!Doc.UserDoc().workspaceRecordings || DocListCast(Doc.UserDoc().workspaceRecordings).length === 0) {
+ return true;
+ }
+ return false;
+});
diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx
index 51eb774e2..f7ed82643 100644
--- a/src/client/views/nodes/RecordingBox/RecordingView.tsx
+++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx
@@ -21,6 +21,8 @@ interface IRecordingViewProps {
setResult: (info: Upload.AccessPathInfo, presentation?: Presentation) => void;
setDuration: (seconds: number) => void;
id: string;
+ getControls: (record: () => void, pause: () => void, finish: () => void) => void;
+ forceTrackScreen: boolean;
}
const MAXTIME = 100000;
@@ -60,14 +62,14 @@ export function RecordingView(props: IRecordingViewProps) {
useEffect(() => {
if (finished) {
// make the total presentation that'll match the concatted video
- let concatPres = trackScreen && TrackMovements.Instance.concatPresentations(videos.map(v => v.presentation as Presentation));
+ let concatPres = (trackScreen || props.forceTrackScreen) && TrackMovements.Instance.concatPresentations(videos.map(v => v.presentation as Presentation));
// this async function uses the server to create the concatted video and then sets the result to it's accessPaths
(async () => {
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(file => ({file})))).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);
@@ -132,7 +134,7 @@ export function RecordingView(props: IRecordingViewProps) {
videoRecorder.current.onstart = (event: any) => {
setRecording(true);
// start the recording api when the video recorder starts
- trackScreen && TrackMovements.Instance.start();
+ (trackScreen || props.forceTrackScreen) && TrackMovements.Instance.start();
};
videoRecorder.current.onstop = () => {
@@ -147,7 +149,7 @@ export function RecordingView(props: IRecordingViewProps) {
// 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 || props.forceTrackScreen) ? { ...nextVideo, presentation } : nextVideo]);
}
// reset the temporary chunks
@@ -159,12 +161,9 @@ export function RecordingView(props: IRecordingViewProps) {
};
// if this is called, then we're done recording all the segments
- const finish = (e: React.PointerEvent) => {
- e.stopPropagation();
-
+ const finish = () => {
// call stop on the video recorder if active
videoRecorder.current?.state !== 'inactive' && videoRecorder.current?.stop();
-
// end the streams (audio/video) to remove recording icon
const stream = videoElementRef.current!.srcObject;
stream instanceof MediaStream && stream.getTracks().forEach(track => track.stop());
@@ -176,8 +175,7 @@ export function RecordingView(props: IRecordingViewProps) {
setFinished(true);
};
- const pause = (e: React.PointerEvent) => {
- e.stopPropagation();
+ const pause = () => {
// if recording, then this is just a new segment
videoRecorder.current?.state === 'recording' && videoRecorder.current.stop();
};
@@ -217,6 +215,10 @@ export function RecordingView(props: IRecordingViewProps) {
return toTwoDigit(minutes) + ' : ' + toTwoDigit(seconds);
};
+ useEffect(() => {
+ props.getControls(record, pause, finish);
+ }, []);
+
return (
<div className="recording-container">
<div className="video-wrapper">
@@ -227,7 +229,19 @@ export function RecordingView(props: IRecordingViewProps) {
</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>
+ <div className="record-button-wrapper">
+ {recording ? (
+ <button
+ className="stop-button"
+ onPointerDown={e => {
+ e.stopPropagation();
+ pause();
+ }}
+ />
+ ) : (
+ <button className="record-button" onPointerDown={start} />
+ )}
+ </div>
{!recording &&
(videos.length > 0 ? (
@@ -236,7 +250,12 @@ export function RecordingView(props: IRecordingViewProps) {
<MdBackspace onPointerDown={undoPrevious} />
</IconContext.Provider>
<IconContext.Provider value={{ color: '#cc1c08', className: 'video-edit-buttons' }}>
- <FaCheckCircle onPointerDown={finish} />
+ <FaCheckCircle
+ onPointerDown={e => {
+ e.stopPropagation();
+ finish();
+ }}
+ />
</IconContext.Provider>
</div>
) : (
@@ -244,7 +263,7 @@ export function RecordingView(props: IRecordingViewProps) {
<label className="track-screen">
<input
type="checkbox"
- checked={trackScreen}
+ checked={trackScreen || props.forceTrackScreen}
onChange={e => {
setTrackScreen(e.target.checked);
}}
diff --git a/src/client/views/nodes/ScreenshotBox.scss b/src/client/views/nodes/ScreenshotBox.scss
index 6fb5ea7b3..1e9b64a0b 100644
--- a/src/client/views/nodes/ScreenshotBox.scss
+++ b/src/client/views/nodes/ScreenshotBox.scss
@@ -1,6 +1,5 @@
.screenshotBox {
transform-origin: top left;
- background: white;
color: black;
// .screenshotBox-viewer {
// opacity: 0.99; // hack! overcomes some kind of Chrome weirdness where buttons (e.g., snapshot) disappear at some point as the video is resized larger
@@ -12,46 +11,39 @@
#CANCAN {
canvas {
- width:100% !important;
+ width: 100% !important;
height: 100% !important;
}
}
-.screenshotBox-content, .screenshotBox-content-interactive, .screenshotBox-cont-fullScreen {
+.screenshotBox-content,
+.screenshotBox-content-interactive,
+.screenshotBox-cont-fullScreen {
width: 100%;
z-index: -1; // 0; // logically this should be 0 (or unset) which would give us transparent brush strokes over videos. However, this makes Chrome crawl to a halt
position: absolute;
}
-.screenshotBox-content, .screenshotBox-content-interactive, .screenshotBox-content-fullScreen {
- height: Auto;
+.screenshotBox-content,
+.screenshotBox-content-interactive,
+.screenshotBox-content-fullScreen {
+ height: Auto;
}
.screenshotBox-uiButtons {
- background:dimgray;
- border: orange solid 1px;
position: absolute;
right: 25;
top: 0;
- width:25;
+ width: 22;
height: 25;
- .screenshotBox-snapshot{
- color : white;
- top :0px;
- right : 5px;
- position: absolute;
- background-color:rgba(50, 50, 50, 0.2);
- transform-origin: left top;
- pointer-events:all;
- }
- .screenshotBox-recorder{
- color : white;
- top :0px;
+ .screenshotBox-recorder {
+ color: white;
+ top: 4px;
left: 5px;
position: absolute;
- background-color:rgba(50, 50, 50, 0.2);
+ background-color: rgba(50, 50, 50, 0.2);
transform-origin: left top;
- pointer-events:all;
+ pointer-events: all;
}
}
diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx
index 312b3c619..ebb8a3374 100644
--- a/src/client/views/nodes/ScreenshotBox.tsx
+++ b/src/client/views/nodes/ScreenshotBox.tsx
@@ -17,6 +17,7 @@ import { DocUtils } from '../../documents/Documents';
import { DocumentType } from '../../documents/DocumentTypes';
import { Networking } from '../../Network';
import { CaptureManager } from '../../util/CaptureManager';
+import { SettingsManager } from '../../util/SettingsManager';
import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
import { CollectionStackedTimeline } from '../collections/CollectionStackedTimeline';
import { ContextMenu } from '../ContextMenu';
@@ -25,6 +26,8 @@ import { FieldView, FieldViewProps } from './FieldView';
import { FormattedTextBox } from './formattedText/FormattedTextBox';
import './ScreenshotBox.scss';
import { VideoBox } from './VideoBox';
+import { TrackMovements } from '../../util/TrackMovements';
+import { media_state } from './AudioBox';
declare class MediaRecorder {
constructor(e: any, options?: any); // whatever MediaRecorder has
@@ -116,7 +119,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
@observable private _videoRef: HTMLVideoElement | null = null;
@observable _screenCapture = false;
@computed get recordingStart() {
- return Cast(this.dataDoc[this.props.fieldKey + '-recordingStart'], DateField)?.date.getTime();
+ return Cast(this.dataDoc[this.props.fieldKey + '_recordingStart'], DateField)?.date.getTime();
}
constructor(props: any) {
@@ -179,7 +182,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
ref={r => {
this._videoRef = r;
setTimeout(() => {
- if (this.rootDoc.mediaState === 'pendingRecording' && this._videoRef) {
+ if (this.rootDoc.mediaState === media_state.PendingRecording && this._videoRef) {
this.toggleRecording();
}
}, 100);
@@ -218,26 +221,38 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
// }
return null;
}
+ Record = () => !this._screenCapture && this.toggleRecording();
+ Pause = () => this._screenCapture && this.toggleRecording();
+
toggleRecording = async () => {
if (!this._screenCapture) {
this._audioRec = new MediaRecorder(await navigator.mediaDevices.getUserMedia({ audio: true }));
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.map((file: any) => ({file})));
+ 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);
+ this.dataDoc[this.props.fieldKey + '_audio'] = new AudioField(result.accessPaths.agnostic.client);
}
};
this._videoRef!.srcObject = await (navigator.mediaDevices as any).getDisplayMedia({ video: true });
this._videoRec = new MediaRecorder(this._videoRef!.srcObject);
const vid_chunks: any = [];
- this._videoRec.onstart = () => (this.dataDoc[this.props.fieldKey + '-recordingStart'] = new DateField(new Date()));
+ this._videoRec.onstart = () => {
+ if (this.dataDoc[this.props.fieldKey + '_trackScreen']) TrackMovements.Instance.start();
+ this.dataDoc[this.props.fieldKey + '_recordingStart'] = new DateField(new Date());
+ };
this._videoRec.ondataavailable = (e: any) => vid_chunks.push(e.data);
this._videoRec.onstop = async (e: any) => {
- console.log('screenshotbox: upload');
+ const presentation = TrackMovements.Instance.yieldPresentation();
+ 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);
+ }
+ TrackMovements.Instance.finish();
const file = new File(vid_chunks, `${this.rootDoc[Id]}.mkv`, { type: vid_chunks[0].type, lastModified: Date.now() });
- const [{ result }] = await Networking.UploadFilesToServer({file});
+ const [{ result }] = await Networking.UploadFilesToServer({ file });
this.dataDoc[this.fieldKey + '_duration'] = (new Date().getTime() - this.recordingStart!) / 1000;
if (!(result instanceof Error)) {
// convert this screenshotBox into normal videoBox
@@ -270,14 +285,15 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
};
setupDictation = () => {
- if (this.dataDoc[this.fieldKey + '-dictation']) return;
+ 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));
+ const textField = Doc.LayoutFieldKey(dictationText);
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;
+ dictationTextProto[`${textField}_recordingSource`] = this.dataDoc;
+ dictationTextProto[`${textField}_recordingStart`] = ComputedField.MakeFunction(`self.${textField}_recordingSource.${this.fieldKey}_recordingStart`);
+ dictationTextProto.mediaState = ComputedField.MakeFunction(`self.${textField}_recordingSource.mediaState`);
+ this.dataDoc[this.fieldKey + '_dictation'] = dictationText;
};
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());
@@ -313,11 +329,11 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
</>
</CollectionFreeFormView>
</div>
- <div style={{ background: 'white', position: 'relative', height: this.formattedPanelHeight() }}>
- {!(this.dataDoc[this.fieldKey + '-dictation'] instanceof Doc) ? null : (
+ <div style={{ background: SettingsManager.userColor, position: 'relative', height: this.formattedPanelHeight() }}>
+ {!(this.dataDoc[this.fieldKey + '_dictation'] instanceof Doc) ? null : (
<FormattedTextBox
{...this.props}
- Document={DocCast(this.dataDoc[this.fieldKey + '-dictation'])}
+ Document={DocCast(this.dataDoc[this.fieldKey + '_dictation'])}
fieldKey={'text'}
PanelHeight={this.formattedPanelHeight}
select={emptyFunction}
@@ -335,8 +351,8 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
</div>
</div>
{!this.props.isSelected() ? null : (
- <div className="screenshotBox-uiButtons">
- <div className="screenshotBox-recorder" key="snap" onPointerDown={this.toggleRecording}>
+ <div className="screenshotBox-uiButtons" style={{ background: SettingsManager.userColor }}>
+ <div className="screenshotBox-recorder" style={{ color: SettingsManager.userBackgroundColor, background: SettingsManager.userVariantColor }} key="snap" onPointerDown={this.toggleRecording}>
<FontAwesomeIcon icon="file" size="lg" />
</div>
</div>
diff --git a/src/client/views/nodes/ScriptingBox.tsx b/src/client/views/nodes/ScriptingBox.tsx
index 3ad3c911d..7c8a1849e 100644
--- a/src/client/views/nodes/ScriptingBox.tsx
+++ b/src/client/views/nodes/ScriptingBox.tsx
@@ -610,7 +610,6 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable
' ': {
dataProvider: (token: any) => this.handleToken(token),
component: (blob: any) => {
- console.log('Blob', blob);
return this.renderFuncListElement(blob.entity);
},
output: (item: any, trigger: any) => {
@@ -621,7 +620,6 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable
'.': {
dataProvider: (token: any) => this.handleToken(token),
component: (blob: any) => {
- console.log('Blob', blob);
return this.renderFuncListElement(blob.entity);
},
output: (item: any, trigger: any) => {
diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss
index 5e1359441..f803715ad 100644
--- a/src/client/views/nodes/VideoBox.scss
+++ b/src/client/views/nodes/VideoBox.scss
@@ -84,7 +84,7 @@
width: 0;
height: 0;
position: relative;
- z-index: 100001;
+ z-index: 2000;
}
.videoBox-ui {
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 1f52c2d92..d7f7c9b73 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -26,11 +26,10 @@ import { CollectionStackedTimeline, TrimScope } from '../collections/CollectionS
import { ContextMenu } from '../ContextMenu';
import { ContextMenuProps } from '../ContextMenuItem';
import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent';
-import { DocumentDecorations } from '../DocumentDecorations';
import { MarqueeAnnotator } from '../MarqueeAnnotator';
import { AnchorMenu } from '../pdf/AnchorMenu';
import { StyleProp } from '../StyleProvider';
-import { OpenWhere } from './DocumentView';
+import { DocFocusOptions, DocumentView, OpenWhere } from './DocumentView';
import { FieldView, FieldViewProps } from './FieldView';
import { RecordingBox } from './RecordingBox';
import { PinProps, PresBox } from './trails';
@@ -58,6 +57,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
static _youtubeIframeCounter: number = 0;
static heightPercent = 80; // height of video relative to videoBox when timeline is open
static numThumbnails = 20;
+ private unmounting = false;
private _disposers: { [name: string]: IReactionDisposer } = {};
private _youtubePlayer: YT.Player | undefined = undefined;
private _videoRef: HTMLVideoElement | null = null; // <video> ref
@@ -103,14 +103,14 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
// returns the path of the audio file
@computed get audiopath() {
- const field = Cast(this.props.Document[this.props.fieldKey + '-audio'], AudioField, null);
+ const field = Cast(this.props.Document[this.props.fieldKey + '_audio'], AudioField, null);
const vfield = Cast(this.dataDoc[this.fieldKey], VideoField, null);
return field?.url.href ?? vfield?.url.href ?? '';
}
// returns the presentation data if it exists, null otherwise
@computed get presentation() {
- const data = this.dataDoc[this.fieldKey + '-presentation'];
+ const data = this.dataDoc[this.fieldKey + '_presentation'];
return data ? JSON.parse(StrCast(data)) : null;
}
@@ -125,6 +125,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
}
componentDidMount() {
+ this.unmounting = false;
this.props.setContentView?.(this); // this tells the DocumentView that this VideoBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the VideoBox when making a link.
if (this.youtubeVideoId) {
const youtubeaspect = 400 / 315;
@@ -136,7 +137,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
this.layoutDoc._height = NumCast(this.layoutDoc._width) / youtubeaspect;
}
}
- this.player && this.setPlayheadTime(0);
+ this.player && this.setPlayheadTime(this.timeline.clipStart || 0);
document.addEventListener('keydown', this.keyEvents, true);
if (this.presentation) {
@@ -145,6 +146,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
}
componentWillUnmount() {
+ this.unmounting = true;
this.removeCurrentlyPlaying();
this.Pause();
Object.keys(this._disposers).forEach(d => this._disposers[d]?.());
@@ -192,7 +194,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
this._finished = false;
start = this.timeline.trimStart;
}
-
try {
this._audioPlayer && this.player && (this._audioPlayer.currentTime = this.player?.currentTime);
update && this.player && this.playFrom(start, undefined, true);
@@ -387,7 +388,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
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;
+ const anchor = addAsAnnotation
+ ? CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, timecode ? timecode : undefined, undefined, marquee, addAsAnnotation) || this.rootDoc
+ : Docs.Create.ConfigDocument({ title: '#' + timecode, _timecodeToShow: timecode, annotationOn: this.rootDoc });
PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), temporal: true } }, this.rootDoc);
return anchor;
};
@@ -409,7 +412,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
// updates video time
@action
updateTimecode = () => {
- this.player && (this.layoutDoc._layout_currentTimecode = this.player.currentTime);
+ !this.unmounting && this.player && (this.layoutDoc._layout_currentTimecode = this.player.currentTime);
try {
this._youtubePlayer && (this.layoutDoc._layout_currentTimecode = this._youtubePlayer.getCurrentTime?.());
} catch (e) {
@@ -524,7 +527,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
icon: 'expand-arrows-alt',
});
// if the videobox was turned from a recording box
- if (this.dataDoc[this.fieldKey + '-recorded'] === true) {
+ if (this.dataDoc[this.fieldKey + '_recorded'] === true) {
subitems.push({
description: 'Recreate recording',
event: () => {
@@ -533,7 +536,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
this.dataDoc[this.props.fieldKey] = '';
this.dataDoc[this.fieldKey + '_duration'] = '';
// delete assoicated presentation data
- this.dataDoc[this.fieldKey + '-presentation'] = '';
+ this.dataDoc[this.fieldKey + '_presentation'] = '';
},
icon: 'expand-arrows-alt',
});
@@ -624,7 +627,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
() => !this._playing && this.Seek(NumCast(this.layoutDoc._layout_currentTimecode))
);
this._disposers.youtubeReactionDisposer = reaction(
- () => Doc.ActiveTool === InkTool.None && this.props.isSelected(true) && !SnappingManager.GetIsDragging() && !DocumentDecorations.Instance.Interacting,
+ () => Doc.ActiveTool === InkTool.None && this.props.isSelected(true) && !SnappingManager.GetIsDragging() && !DocumentView.Interacting,
interactive => (iframe.style.pointerEvents = interactive ? 'all' : 'none'),
{ fireImmediately: true }
);
@@ -848,11 +851,11 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
zoom = (zoom: number) => this.timeline?.setZoom(zoom);
// plays link
- playLink = (doc: Doc) => {
- const startTime = Math.max(0, this._stackedTimeline?.anchorStart(doc) || 0);
+ playLink = (doc: Doc, options: DocFocusOptions) => {
+ const startTime = Math.max(0, NumCast(doc.config_clipStart, this._stackedTimeline?.anchorStart(doc) || 0));
const endTime = this.timeline?.anchorEnd(doc);
if (startTime !== undefined) {
- if (!this.layoutDoc.dontAutoPlayFollowedLinks) endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime);
+ if (options.playMedia) endTime ? this.playFrom(startTime, endTime) : this.playFrom(startTime);
else this.Seek(startTime);
}
};
@@ -959,7 +962,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
{...this.props}
dataFieldKey={this.fieldKey}
fieldKey={this.annotationKey}
- dictationKey={this.fieldKey + '-dictation'}
+ dictationKey={this.fieldKey + '_dictation'}
mediaPath={this.audiopath}
thumbnails={() => StrListCast(this.dataDoc[this.fieldKey + '_thumbnails'])}
renderDepth={this.props.renderDepth + 1}
@@ -1040,7 +1043,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
this.props.bringToFront(cropping);
return cropping;
};
-
savedAnnotations = () => this._savedAnnotations;
render() {
const borderRad = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding);
@@ -1078,8 +1080,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
fieldKey={this.annotationKey}
isAnnotationOverlay={true}
annotationLayerHostsContent={true}
- PanelWidth={this.panelWidth}
- PanelHeight={this.panelHeight}
+ PanelWidth={this.props.PanelWidth}
+ PanelHeight={this.props.PanelHeight}
isAnyChildContentActive={returnFalse}
ScreenToLocalTransform={this.screenToLocalTransform}
childFilters={this.timelineDocFilter}
@@ -1120,7 +1122,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._layout_currentTimecode) - (this.timeline?.clipStart || 0);
+ const curTime = NumCast(this.layoutDoc._layout_currentTimecode);
return (
<>
<div className="videobox-button" title={this._playing ? 'play' : 'pause'} onPointerDown={this.onPlayDown}>
@@ -1129,7 +1131,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
{this.timeline && width > 150 && (
<div className="timecode-controls">
- <div className="timecode-current">{formatTime(curTime)}</div>
+ <div className="timecode-current">{formatTime(curTime - (this.timeline?.clipStart || 0))}</div>
{this._fullScreen || (this.heightPercent === 100 && width > 200) ? (
<div className="timeline-slider">
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 34a1229ba..27c19105f 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -50,8 +50,8 @@ 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 _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void);
+ private _setBrushViewer: undefined | ((view: { width: number; height: number; panX: number; panY: number }, transTime: number) => void);
private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
private _outerRef: React.RefObject<HTMLDivElement> = React.createRef();
private _disposers: { [name: string]: IReactionDisposer } = {};
@@ -91,7 +91,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
return DocListCast(this.dataDoc[this.annotationKey]);
}
@computed get inlineTextAnnotations() {
- return this.allAnnotations.filter(a => a.textInlineAnnotations);
+ return this.allAnnotations.filter(a => a.text_inlineAnnotations);
}
@computed get webField() {
return Cast(this.rootDoc[this.props.fieldKey], WebField)?.url;
@@ -125,11 +125,15 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
this._searchRef.current?.setRangeText(searchString);
});
}
- if (clear) {
- this._iframe?.contentWindow?.getSelection()?.empty();
- }
- if (searchString) {
- (this._iframe?.contentWindow as any)?.find(searchString, false, bwd, true);
+ try {
+ if (clear) {
+ this._iframe?.contentWindow?.getSelection()?.empty();
+ }
+ if (searchString) {
+ (this._iframe?.contentWindow as any)?.find(searchString, false, bwd, true);
+ }
+ } catch (e) {
+ console.log('WebBox search error', e);
}
return true;
};
@@ -192,8 +196,13 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
});
this._disposers.urlchange = reaction(
() => WebCast(this.rootDoc.data),
+ url => this.submitURL(false, false)
+ );
+ this._disposers.titling = reaction(
+ () => StrCast(this.rootDoc.title),
url => {
- this.submitURL(url.url.href, false, false);
+ url.startsWith('www') && this.setData('http://' + url);
+ url.startsWith('http') && this.setData(url);
}
);
@@ -277,8 +286,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
return this._savedAnnotations;
};
- 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);
+ setBrushViewer = (func?: (view: { width: number; height: number; panX: number; panY: number }, transTime: number) => void) => (this._setBrushViewer = func);
+ brushView = (view: { width: number; height: number; panX: number; panY: number }, transTime: number) => this._setBrushViewer?.(view, transTime);
focus = (anchor: Doc, options: DocFocusOptions) => {
if (anchor !== this.rootDoc && this._outerRef.current) {
const windowHeight = this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1);
@@ -299,7 +308,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
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;
+ const webUrl = WebCast(doc.config_data)?.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)));
@@ -321,8 +330,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
ele.append(contents);
}
} catch (e) {}
+ const visibleAnchor = this._getAnchor(this._savedAnnotations, false);
const anchor =
- this._getAnchor(this._savedAnnotations, false) ??
+ visibleAnchor ??
Docs.Create.ConfigDocument({
title: StrCast(this.rootDoc.title + ' ' + this.layoutDoc._layout_scrollTop),
y: NumCast(this.layoutDoc._layout_scrollTop),
@@ -330,9 +340,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
});
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);
+ anchor.text_html = ele?.innerHTML;
+ addAsAnnotation && this.addDocumentWrapper(anchor);
return anchor;
};
@@ -360,14 +369,13 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
};
@action
iframeDown = (e: PointerEvent) => {
- const sel = this._iframe?.contentWindow?.getSelection?.();
const mainContBounds = Utils.GetScreenTransform(this._mainCont.current!);
const scale = (this.props.NativeDimScaling?.() || 1) * mainContBounds.scale;
const word = getWordAtPoint(e.target, e.clientX, e.clientY);
- this._setPreviewCursor?.(e.clientX, e.clientY, false, true);
+ this._setPreviewCursor?.(e.clientX, e.clientY, false, true, this.rootDoc);
MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
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) {
+ 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)),
100
@@ -413,6 +421,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
}
_iframetimeout: any = undefined;
+ @observable _warning = 0;
@action
iframeLoaded = (e: any) => {
const iframe = this._iframe;
@@ -426,6 +435,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
try {
href = iframe?.contentWindow?.location.href;
} catch (e) {
+ runInAction(() => this._warning++);
href = undefined;
}
let requrlraw = decodeURIComponent(href?.replace(Utils.prepend('') + '/corsProxy/', '') ?? this._url.toString());
@@ -458,12 +468,19 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
// { passive: false }
// );
const initHeights = () => {
- this._scrollHeight = Math.max(this._scrollHeight, (iframeContent.body.children[0] as any)?.scrollHeight || 0);
+ this._scrollHeight = Math.max(this._scrollHeight, iframeContent.body.scrollHeight || 0);
if (this._scrollHeight) {
this.rootDoc.nativeHeight = Math.min(NumCast(this.rootDoc.nativeHeight), this._scrollHeight);
this.layoutDoc.height = Math.min(this.layoutDoc[Height](), (this.layoutDoc[Width]() * NumCast(this.rootDoc.nativeHeight)) / NumCast(this.rootDoc.nativeWidth));
}
};
+ const swidth = Math.max(NumCast(this.layoutDoc.nativeWidth), iframeContent.body.scrollWidth || 0);
+ if (swidth) {
+ const aspectResize = swidth / NumCast(this.rootDoc.nativeWidth);
+ this.rootDoc.nativeWidth = swidth;
+ this.rootDoc.nativeHeight = NumCast(this.rootDoc.nativeHeight) * aspectResize;
+ this.layoutDoc.height = this.layoutDoc[Height]() * aspectResize;
+ }
initHeights();
this._iframetimeout && clearTimeout(this._iframetimeout);
this._iframetimeout = setTimeout(
@@ -541,7 +558,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
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, easeFunc);
this.setDashScrollTop(scrollTop, duration);
@@ -606,9 +622,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
);
};
@action
- submitURL = (newUrl?: string, preview?: boolean, dontUpdateIframe?: boolean) => {
- if (!newUrl) return;
- if (!newUrl.startsWith('http')) newUrl = 'http://' + newUrl;
+ submitURL = (preview?: boolean, dontUpdateIframe?: boolean) => {
try {
if (!preview) {
if (this._webPageHasBeenRendered) {
@@ -664,9 +678,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
!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',
+ description: (this.dataDoc[this.fieldKey + '_allowScripts'] ? 'Prevent' : 'Allow') + ' Scripts',
event: () => {
- this.layoutDoc.allowScripts = !this.layoutDoc.allowScripts;
+ this.dataDoc[this.fieldKey + '_allowScripts'] = !this.dataDoc[this.fieldKey + '_allowScripts'];
if (this._iframe) {
runInAction(() => (this._hackHide = true));
setTimeout(action(() => (this._hackHide = false)));
@@ -715,7 +729,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
if (sel?.empty) sel.empty(); // Chrome
else if (sel?.removeAllRanges) sel.removeAllRanges(); // Firefox
if (x !== undefined && y !== undefined) {
- this._setPreviewCursor?.(x, y, false, false);
+ this._setPreviewCursor?.(x, y, false, false, this.rootDoc);
ContextMenu.Instance.closeMenu();
ContextMenu.Instance.setIgnoreEvents(false);
if (e?.button === 2 || e?.altKey) {
@@ -740,16 +754,20 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
}
if (field instanceof WebField) {
const url = this.layoutDoc[this.fieldKey + '_useCors'] ? Utils.CorsProxy(this._webUrl) : this._webUrl;
+ const scripts = this.dataDoc[this.fieldKey + '_allowScripts'] || this._webUrl.includes('wikipedia.org') || this._webUrl.includes('google.com') || this._webUrl.startsWith('https://bing');
+ //if (!scripts) console.log('No scripts for: ' + url);
return (
<iframe
+ key={this._warning}
className="webBox-iframe"
ref={action((r: HTMLIFrameElement | null) => (this._iframe = r))}
+ style={{ pointerEvents: this._isAnyChildContentActive || DocumentView.Interacting ? 'none' : undefined }}
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`}
+ sandbox={`${scripts ? 'allow-scripts' : ''} allow-forms allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin`}
/>
);
}
@@ -757,7 +775,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
}
addDocumentWrapper = (doc: Doc | Doc[], annotationKey?: string) => {
- (doc instanceof Doc ? [doc] : doc).forEach(doc => (doc.presData = new WebField(this._url)));
+ (doc instanceof Doc ? [doc] : doc).forEach(doc => (doc.config_data = new WebField(this._url)));
return this.addDocument(doc, annotationKey);
};
@@ -989,7 +1007,7 @@ 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);
+ setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void) => (this._setPreviewCursor = func);
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));
diff --git a/src/client/views/nodes/WebBoxRenderer.js b/src/client/views/nodes/WebBoxRenderer.js
index eb8064780..425ef3e54 100644
--- a/src/client/views/nodes/WebBoxRenderer.js
+++ b/src/client/views/nodes/WebBoxRenderer.js
@@ -42,21 +42,21 @@ 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')) {
+ } else if (inurl.startsWith('https') && !inurl.startsWith(window.location.origin)) {
url = CorsProxy(inurl);
}
xhr.open('GET', url);
xhr.responseType = 'blob';
xhr.onreadystatechange = async function () {
- if (xhr.readyState === 4 && xhr.status === 200) {
+ if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
const resBase64 = await binaryStringToBase64(xhr.response);
resolve({
resourceUrl: inurl,
resourceBase64: resBase64,
});
- } else if (xhr.readyState === 4) {
+ } else if (xhr.readyState === XMLHttpRequest.DONE) {
console.log("COULDN'T FIND: " + (inurl.startsWith('/') ? webUrl + inurl : inurl));
resolve({
resourceUrl: '',
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss
index 109b62e6f..348bdd79e 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.scss
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss
@@ -84,7 +84,6 @@ audiotag:hover {
height: 11;
}
-.formattedTextBox-outer-selected,
.formattedTextBox-outer {
position: relative;
overflow: auto;
@@ -92,9 +91,6 @@ audiotag:hover {
width: 100%;
height: unset;
}
-.formattedTextBox-outer-selected {
- cursor: text;
-}
.formattedTextBox-sidebar-handle {
position: absolute;
@@ -148,10 +144,8 @@ audiotag:hover {
}
.formattedTextBox-inner-rounded,
-.formattedTextBox-inner-rounded-selected,
.formattedTextBox-inner,
-.formattedTextBox-inner-minimal,
-.formattedTextBox-inner-selected {
+.formattedTextBox-inner-minimal {
height: 100%;
white-space: pre-wrap;
.ProseMirror:hover {
@@ -169,17 +163,6 @@ audiotag:hover {
border-width: 1px;
}
}
-.formattedTextBox-inner-rounded-selected,
-.formattedTextBox-inner-selected {
- > .ProseMirror {
- padding: 10px;
- }
-}
-.formattedTextBox-outer-selected {
- > .ProseMirror:hover {
- background: unset;
- }
-}
.gpt-typing-wrapper {
padding: 10px;
@@ -640,7 +623,6 @@ footnote::before {
}
}
- .formattedTextBox-outer-selected,
.formattedTextBox-outer {
position: relative;
overflow: auto;
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index a0eb328a1..360dfca04 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -11,9 +11,10 @@ import { keymap } from 'prosemirror-keymap';
import { Fragment, Mark, Node, Slice } from 'prosemirror-model';
import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from 'prosemirror-state';
import { EditorView } from 'prosemirror-view';
+import { BsMarkdownFill } from 'react-icons/bs';
import { DateField } from '../../../../fields/DateField';
-import { Doc, DocListCast, StrListCast, Field, Opt } from '../../../../fields/Doc';
-import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DocCss, Height, Width, ForceServerWrite, UpdatingFromServer } from '../../../../fields/DocSymbols';
+import { Doc, DocListCast, Field, Opt } from '../../../../fields/Doc';
+import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DocCss, ForceServerWrite, Height, UpdatingFromServer, Width } from '../../../../fields/DocSymbols';
import { Id } from '../../../../fields/FieldSymbols';
import { InkTool } from '../../../../fields/InkField';
import { List } from '../../../../fields/List';
@@ -25,11 +26,10 @@ import { BoolCast, Cast, DocCast, FieldValue, NumCast, ScriptCast, StrCast } fro
import { GetEffectiveAcl, TraceMobx } from '../../../../fields/util';
import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, returnFalse, returnZero, setupMoveUpEvents, smoothScroll, unimplementedFunction, Utils } from '../../../../Utils';
import { GoogleApiClientUtils, Pulls, Pushes } from '../../../apis/google_docs/GoogleApiClientUtils';
-import { gptAPICall, GPTCallType, gptImageCall } from '../../../apis/gpt/GPT';
+import { gptAPICall, GPTCallType } from '../../../apis/gpt/GPT';
import { DocServer } from '../../../DocServer';
import { Docs, DocUtils } from '../../../documents/Documents';
-import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
-import { Networking } from '../../../Network';
+import { CollectionViewType } from '../../../documents/DocumentTypes';
import { DictationManager } from '../../../util/DictationManager';
import { DocumentManager } from '../../../util/DocumentManager';
import { DragManager } from '../../../util/DragManager';
@@ -38,7 +38,7 @@ 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';
+import { undoable, undoBatch, UndoManager } from '../../../util/UndoManager';
import { CollectionFreeFormView } from '../../collections/collectionFreeForm/CollectionFreeFormView';
import { CollectionStackingView } from '../../collections/CollectionStackingView';
import { CollectionTreeView } from '../../collections/CollectionTreeView';
@@ -49,6 +49,7 @@ import { DocumentButtonBar } from '../../DocumentButtonBar';
import { Colors } from '../../global/globalEnums';
import { LightboxView } from '../../LightboxView';
import { AnchorMenu } from '../../pdf/AnchorMenu';
+import { GPTPopup } from '../../pdf/GPTPopup/GPTPopup';
import { SidebarAnnos } from '../../SidebarAnnos';
import { StyleProp } from '../../StyleProvider';
import { DocFocusOptions, DocumentView, DocumentViewInternal, OpenWhere } from '../DocumentView';
@@ -70,7 +71,10 @@ import { schema } from './schema_rts';
import { SummaryView } from './SummaryView';
import applyDevTools = require('prosemirror-dev-tools');
import React = require('react');
-const translateGoogleApi = require('translate-google-api');
+import { media_state } from '../AudioBox';
+import { setCORS } from 'google-translate-api-browser';
+// setting up cors-anywhere server address
+const translate = setCORS('http://cors-anywhere.herokuapp.com/');
export const GoogleRef = 'googleDocId';
type PullHandler = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => void;
@@ -92,10 +96,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
static _userStyleSheet: any = addStyleSheet();
static _hadSelection: boolean = false;
private _sidebarRef = React.createRef<SidebarAnnos>();
+ private _sidebarTagRef = React.createRef<React.Component>();
private _ref: React.RefObject<HTMLDivElement> = React.createRef();
private _scrollRef: React.RefObject<HTMLDivElement> = React.createRef();
private _editorView: Opt<EditorView>;
public _applyingChange: string = '';
+ private _finishingLink = false;
private _searchIndex = 0;
private _lastTimedMark: Mark | undefined = undefined;
private _cachedLinks: Doc[] = [];
@@ -151,11 +157,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
@computed get layout_autoHeightMargins() {
return this.titleHeight + NumCast(this.layoutDoc._layout_autoHeightMargins);
}
- @computed get _recording() {
- return this.dataDoc?.mediaState === 'recording';
+ @computed get _recordingDictation() {
+ return this.dataDoc?.mediaState === media_state.Recording;
}
- set _recording(value) {
- !this.dataDoc.recordingSource && (this.dataDoc.mediaState = value ? 'recording' : undefined);
+ set _recordingDictation(value) {
+ !this.dataDoc[`${this.fieldKey}_recordingSource`] && (this.dataDoc.mediaState = value ? media_state.Recording : undefined);
}
@computed get config() {
this._keymap = buildKeymap(schema, this.props);
@@ -245,7 +251,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
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._finishingLink = true;
this.makeLinkAnchor(anchor, OpenWhere.addRight, undefined, 'Anchored Selection', false, addAsAnnotation);
+ this._finishingLink = false;
PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), scrollable: true } }, this.rootDoc);
return anchor;
};
@@ -261,19 +269,36 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
AnchorMenu.Instance.OnAudio = (e: PointerEvent) => {
!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));
+ let stopFunc: any;
+ Doc.GetProto(target).mediaState = media_state.Recording;
+ Doc.GetProto(target).audioAnnoState = 'recording';
+ DocumentViewInternal.recordAudioAnnotation(Doc.GetProto(target), Doc.LayoutFieldKey(target), stop => (stopFunc = stop));
+ let reactionDisposer = reaction(
+ () => target.mediaState,
+ action(dictation => {
+ if (!dictation) {
+ Doc.GetProto(target).audioAnnoState = 'stopped';
+ stopFunc();
+ reactionDisposer();
+ }
+ })
+ );
target.title = ComputedField.MakeFunction(`self["text_audioAnnotations_text"].lastElement()`);
}
});
};
- AnchorMenu.Instance.Highlight = action((color: string, isLinkButton: boolean) => {
- this._editorView?.state && RichTextMenu.Instance.setHighlight(color);
- return undefined;
- });
+ AnchorMenu.Instance.Highlight = undoable(
+ action((color: string, isLinkButton: boolean) => {
+ this._editorView?.state && RichTextMenu.Instance.setHighlight(color);
+ return undefined;
+ }),
+ 'highlght text'
+ );
AnchorMenu.Instance.onMakeAnchor = () => this.getAnchor(true);
AnchorMenu.Instance.StartCropDrag = unimplementedFunction;
/**
@@ -326,7 +351,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
textChange && (dataDoc[this.fieldKey + '_modificationDate'] = new DateField(new Date(Date.now())));
if ((!prevData && !protoData) || newText || (!newText && !templateData)) {
// if no template, or there's text that didn't come from the layout template, write it to the document. (if this is driven by a template, then this overwrites the template text which is intended)
- if (this.props.isContentActive() && removeSelection(newJson) !== removeSelection(prevData?.Data)) {
+ if ((this._finishingLink || this.props.isContentActive()) && removeSelection(newJson) !== removeSelection(prevData?.Data)) {
const numstring = NumCast(dataDoc[this.fieldKey], null);
dataDoc[this.fieldKey] = numstring !== undefined ? Number(newText) : new RichTextField(newJson, newText);
dataDoc[this.fieldKey + '_noTemplate'] = true; // mark the data field as being split from the template if it has been edited
@@ -368,7 +393,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
let link;
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') {
+ if (anchor && (anchor.annotationOn as Doc).mediaState === media_state.Recording) {
linkTime = NumCast(anchor._timecodeToShow /* audioStart */);
linkAnchor = anchor;
link = l;
@@ -414,7 +439,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
DocListCast(Doc.MyPublishedDocs.data).forEach(term => (tr = this.hyperlinkTerm(tr, term, newAutoLinks)));
tr = tr.setSelection(isNodeSel && false ? new NodeSelection(tr.doc.resolve(f)) : new TextSelection(tr.doc.resolve(f), tr.doc.resolve(t)));
this._editorView?.dispatch(tr);
- this.prepareForTyping();
+ // this.prepareForTyping();
}
oldAutoLinks.filter(oldLink => !newAutoLinks.has(oldLink) && oldLink.link_anchor_2 !== this.rootDoc).forEach(LinkManager.Instance.deleteLink);
};
@@ -435,7 +460,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
const cfield = ComputedField.WithoutComputed(() => FieldValue(this.dataDoc.title));
if (!(cfield instanceof ComputedField)) {
- this.dataDoc.title = prefix + str.substring(0, Math.min(40, str.length)) + (str.length > 40 ? '...' : '');
+ this.dataDoc.title = (prefix + str.substring(0, Math.min(40, str.length)) + (str.length > 40 ? '...' : '')).trim();
if (str.startsWith('@') && str.length > 1) {
Doc.AddDocToList(Doc.MyPublishedDocs, undefined, this.rootDoc);
}
@@ -457,8 +482,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
if (node.firstChild === null && !node.marks.find((m: Mark) => m.type.name === schema.marks.noAutoLinkAnchor.name) && node.marks.find((m: Mark) => m.type.name === schema.marks.splitter.name)) {
alink =
alink ??
- (LinkManager.Links(this.Document).find(link => Doc.AreProtosEqual(Cast(link.link_anchor_1, Doc, null), this.rootDoc) && Doc.AreProtosEqual(Cast(link.link_anchor_2, Doc, null), target)) ||
- DocUtils.MakeLink(this.props.Document, target, { link_relationship: LinkManager.AutoKeywords })!);
+ (LinkManager.Links(this.rootDoc).find(
+ link =>
+ Doc.AreProtosEqual(Cast(link.link_anchor_1, Doc, null), this.rootDoc) && //
+ Doc.AreProtosEqual(Cast(link.link_anchor_2, Doc, null), target)
+ ) ||
+ DocUtils.MakeLink(this.rootDoc, target, { link_relationship: LinkManager.AutoKeywords })!);
newAutoLinks.add(alink);
const allAnchors = [{ href: Doc.localServerPath(target), title: 'a link', anchorId: this.props.Document[Id] }];
allAnchors.push(...(node.marks.find((m: Mark) => m.type.name === schema.marks.autoLinkAnchor.name)?.attrs.allAnchors ?? []));
@@ -566,7 +595,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
draggedDoc._freeform_fitContentsToBox = true;
Doc.SetContainer(draggedDoc, this.rootDoc);
const view = this._editorView!;
- view.dispatch(view.state.tr.insert(view.posAtCoords({ left: de.x, top: de.y })!.pos, node));
+ try {
+ const pos = view.posAtCoords({ left: de.x, top: de.y })?.pos;
+ pos && view.dispatch(view.state.tr.insert(pos, node));
+ added = pos ? true : false; // pos will be null if you don't drop onto an actual text location
+ } catch (e) {
+ console.log('Drop failed', e);
+ added = false;
+ }
}
}
} // otherwise, fall through to outer collection to handle drop
@@ -633,7 +669,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-remote', { background: 'yellow' });
}
if (highlights.includes('My Text')) {
- addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-' + Doc.CurrentUserEmail.replace('.', '').replace('@', ''), { background: 'moccasin' });
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-' + Doc.CurrentUserEmail.replace(/\./g, '').replace(/@/g, ''), { background: 'moccasin' });
}
if (highlights.includes('Todo Items')) {
addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UT-todo', { outline: 'black solid 1px' });
@@ -673,7 +709,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
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._layout_showSidebar = (this.layoutDoc._layout_sidebarWidthPercent = StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%') === '0%' ? '50%' : '0%') !== '0%';
+ else {
+ this.layoutDoc[this.SidebarKey + '_freeform_scale_max'] = 1;
+ 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);
};
@@ -723,7 +762,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
@undoBatch
showTargetTrail = (anchor: Doc) => {
- const trail = DocCast(anchor.presTrail);
+ const trail = DocCast(anchor.presentationTrail);
if (trail) {
Doc.ActivePresentation = trail;
this.props.addDocTab(trail, OpenWhere.replaceRight);
@@ -829,7 +868,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
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({ description: 'Show Highlights...', noexpand: true, subitems: highlighting, icon: 'hand-point-right' });
!Doc.noviceMode &&
uicontrols.push({
description: 'Broadcast Message',
@@ -840,7 +879,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
const appearance = cm.findByDescription('Appearance...');
const appearanceItems = appearance && 'subitems' in appearance ? appearance.subitems : [];
- appearanceItems.push({ description: 'Change Perspective...', noexpand: true, subitems: changeItems, icon: 'external-link-alt' });
+ appearanceItems.push({ description: 'Change Style...', noexpand: true, subitems: changeItems, icon: 'external-link-alt' });
// this.rootDoc.isTemplateDoc && appearanceItems.push({ description: "Make Default Layout", event: async () => Doc.UserDoc().defaultTextLayout = new PrefetchProxy(this.rootDoc), icon: "eye" });
!Doc.noviceMode &&
@@ -891,16 +930,17 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
event: () => (this.layoutDoc._layout_autoHeight = !this.layoutDoc._layout_autoHeight),
icon: this.Document._layout_autoHeight ? 'lock' : 'unlock',
});
- optionItems.push({ description: `show markdown options`, event: RTFMarkup.Instance.open, icon: 'text' });
+ optionItems.push({ description: `show markdown options`, event: RTFMarkup.Instance.open, icon: <BsMarkdownFill /> });
!options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'eye' });
this._downX = this._downY = Number.NaN;
};
- animateRes = (resIndex: number) => {
- if (resIndex < this.gptRes.length) {
- this.dataDoc.text = (this.dataDoc.text as RichTextField)?.Text + this.gptRes[resIndex];
+ animateRes = (resIndex: number, newText: string) => {
+ if (resIndex < newText.length) {
+ const marks = this._editorView?.state.storedMarks ?? [];
+ this._editorView?.dispatch(this._editorView.state.tr.setStoredMarks(marks).insertText(newText[resIndex]).setStoredMarks(marks));
setTimeout(() => {
- this.animateRes(resIndex + 1);
+ this.animateRes(resIndex + 1, newText);
}, 20);
}
};
@@ -908,56 +948,35 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
askGPT = action(async () => {
try {
let res = await gptAPICall((this.dataDoc.text as RichTextField)?.Text, GPTCallType.COMPLETION);
- if (res) {
- this.gptRes = res;
- this.animateRes(0);
+ if (!res) {
+ console.error('GPT call failed');
+ this.animateRes(0, 'Something went wrong.');
+ } else {
+ this.animateRes(0, res);
}
} catch (err) {
- console.log(err);
- this.dataDoc.text = (this.dataDoc.text as RichTextField)?.Text + 'Something went wrong';
+ console.error('GPT call failed');
+ this.animateRes(0, '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 '';
- }
+ GPTPopup.Instance?.setTextAnchor(this.getAnchor(false));
+ GPTPopup.Instance?.setImgTargetDoc(this.rootDoc);
+ GPTPopup.Instance.addToCollection = this.props.addDocument;
+ GPTPopup.Instance.setImgDesc((this.dataDoc.text as RichTextField)?.Text);
+ GPTPopup.Instance.generateImage();
};
breakupDictation = () => {
- if (this._editorView && this._recording) {
+ if (this._editorView && this._recordingDictation) {
this.stopDictation(true);
this._break = true;
const state = this._editorView.state;
const to = state.selection.to;
const updated = TextSelection.create(state.doc, to, to);
this._editorView.dispatch(state.tr.setSelection(updated).insertText('\n', to));
- if (this._recording) {
+ if (this._recordingDictation) {
this.recordDictation();
}
}
@@ -1130,7 +1149,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
componentDidMount() {
!this.props.dontSelectOnLoad && this.props.setContentView?.(this); // this tells the DocumentView that this AudioBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link.
this._cachedLinks = LinkManager.Links(this.Document);
- this._disposers.breakupDictation = reaction(() => DocumentManager.Instance.RecordingEvent, this.breakupDictation);
+ this._disposers.breakupDictation = reaction(() => Doc.RecordingEvent, this.breakupDictation);
this._disposers.layout_autoHeight = reaction(
() => this.layout_autoHeight,
layout_autoHeight => layout_autoHeight && this.tryUpdateScrollHeight()
@@ -1230,7 +1249,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
this._disposers.selected = reaction(
() => this.props.isSelected(),
action(selected => {
- selected && this.prepareForTyping();
+ //selected && setTimeout(() => this.prepareForTyping());
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
}
@@ -1241,26 +1260,20 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this.props);
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 }
);
if (!this.props.dontRegisterView) {
this._disposers.record = reaction(
- () => this._recording,
+ () => this._recordingDictation,
() => {
this.stopDictation(true);
- if (this._recording) {
- this.recordDictation();
- }
- }
+ this._recordingDictation && this.recordDictation();
+ },
+ { fireImmediately: true }
);
- if (this._recording) setTimeout(this.recordDictation);
+ if (this._recordingDictation) setTimeout(this.recordDictation);
}
var quickScroll: string | undefined = '';
this._disposers.scroll = reaction(
@@ -1543,21 +1556,24 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
// add user mark for any first character that was typed since the user mark that gets set in KeyPress won't have been called yet.
prepareForTyping = () => {
- this._editorView?.dispatch(
- this._editorView?.state.tr.setStoredMarks([
- ...(this._editorView.state.storedMarks?.filter(mark => ![schema.marks.em, schema.marks.underline, schema.marks.pFontFamily, schema.marks.pFontSize, schema.marks.strong, schema.marks.pFontColor].includes(mark.type)) ?? []),
- ...[schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) })],
- ...(Doc.UserDoc().fontColor !== 'transparent' && Doc.UserDoc().fontColor ? [schema.mark(schema.marks.pFontColor, { color: StrCast(Doc.UserDoc().fontColor) })] : []),
- ...(Doc.UserDoc().fontStyle === 'italics' ? [schema.mark(schema.marks.em)] : []),
- ...(Doc.UserDoc().textDecoration === 'underline' ? [schema.mark(schema.marks.underline)] : []),
- ...(Doc.UserDoc().fontFamily ? [schema.mark(schema.marks.pFontFamily, { family: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.FontFamily) })] : []),
- ...(Doc.UserDoc().fontSize ? [schema.mark(schema.marks.pFontSize, { fontSize: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.FontSize) })] : []),
- ...(Doc.UserDoc().fontWeight === 'bold' ? [schema.mark(schema.marks.strong)] : []),
- ])
- );
+ if (!this._editorView) return;
+ const docDefaultMarks = [
+ ...(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)] : []),
+ ...[schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) })],
+ ];
+ this._editorView?.dispatch(this._editorView?.state.tr.setStoredMarks(docDefaultMarks));
};
+ @action
componentWillUnmount() {
+ if (this._recordingDictation) {
+ this._recordingDictation = !this._recordingDictation;
+ }
Object.values(this._disposers).forEach(disposer => disposer?.());
this.endUndoTypingBatch();
FormattedTextBox.LiveTextUndo?.end();
@@ -1570,7 +1586,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
onPointerDown = (e: React.PointerEvent): void => {
if ((e.nativeEvent as any).handledByInnerReactInstance) {
- return e.stopPropagation();
+ return; //e.stopPropagation();
} else (e.nativeEvent as any).handledByInnerReactInstance = true;
if (this.Document.forceActive) e.stopPropagation();
@@ -1594,20 +1610,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
}
});
}
- if (this._recording && !e.ctrlKey && e.button === 0) {
+ if (this._recordingDictation && !e.ctrlKey && e.button === 0) {
this.breakupDictation();
- e.preventDefault();
- e.stopPropagation();
}
this._downX = e.clientX;
this._downY = e.clientY;
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) {
- // stop propagation if not in sidebar
- // bcz: Change. drag selecting requires that preventDefault is NOT called. This used to happen in DocumentView,
- // but that's changed, so this shouldn't be needed.
- //e.stopPropagation(); // if the text box is selected, then it consumes all down events
+ // stop propagation if not in sidebar, otherwise nested boxes will lose focus to outer boxes.
+ e.stopPropagation(); // if the text box's content is active, then it consumes all down events
document.addEventListener('pointerup', this.onSelectEnd);
}
}
@@ -1769,13 +1781,14 @@ 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.sidebar_collectionType === 'translation' && !this.fieldKey.includes('translation') && curText.endsWith(' ') && curText !== this._lastText) {
+ if (this.layoutDoc[this.SidebarKey + '_type_collection'] === 'translation' && !this.fieldKey.includes('translation') && curText.endsWith(' ') && curText !== this._lastText) {
try {
- translateGoogleApi(curText, { from: 'en', to: 'es' }).then((result1: any) => {
+ translate(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];
+ translate(result1.text, { from: 'es', to: 'en' }).then((result: any) => {
+ const tb = this._sidebarTagRef.current as FormattedTextBox;
+ tb._editorView?.dispatch(tb._editorView!.state.tr.insertText(result1.text + '\r\n\r\n' + result.text));
}),
1000
);
@@ -1813,7 +1826,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
if (state.selection.empty || !this._rules!.EnteringStyle) {
this._rules!.EnteringStyle = false;
}
- e.stopPropagation();
+ let stopPropagation = true;
for (var i = state.selection.from; i <= state.selection.to; i++) {
const node = state.doc.resolve(i);
if (state.doc.content.size - 1 > i && node?.marks?.().some(mark => mark.type === schema.marks.user_mark && mark.attrs.userid !== Doc.CurrentUserEmail) && [AclAugment, AclSelfEdit].includes(GetEffectiveAcl(this.rootDoc))) {
@@ -1832,6 +1845,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
case 'Tab':
e.preventDefault();
break;
+ case 'c':
+ this._editorView?.state.selection.empty && (stopPropagation = false);
+ break;
default:
if (this._lastTimedMark?.attrs.userid === Doc.CurrentUserEmail) break;
case ' ':
@@ -1841,6 +1857,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
}
break;
}
+ if (stopPropagation) e.stopPropagation();
this.startUndoTypingBatch();
};
ondrop = (e: React.DragEvent) => {
@@ -1898,7 +1915,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
.scale(1 / NumCast(this.layoutDoc._freeform_scale, 1) / (this.props.NativeDimScaling?.() || 1));
@computed get audioHandle() {
- return !this._recording ? null : (
+ return !this._recordingDictation ? null : (
<div
className="formattedTextBox-dictation"
onPointerDown={e =>
@@ -1907,7 +1924,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
e,
returnFalse,
emptyFunction,
- action(e => (this._recording = !this._recording))
+ action(e => (this._recordingDictation = !this._recordingDictation))
)
}>
<FontAwesomeIcon className="formattedTextBox-audioFont" style={{ color: 'red' }} icon={'microphone'} size="sm" />
@@ -1959,6 +1976,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
<div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => SelectionManager.SelectView(this.props.DocumentView?.()!, false), true)}>
<ComponentTag
{...this.props}
+ ref={this._sidebarTagRef as any}
setContentView={emptyFunction}
NativeWidth={returnZero}
NativeHeight={returnZero}
@@ -1981,25 +1999,25 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
fitContentsToBox={this.fitContentsToBox}
noSidebar={true}
treeViewHideTitle={true}
- fieldKey={this.layoutDoc.sidebar_collectionType === 'translation' ? `${this.fieldKey}_translation` : `${this.fieldKey}_sidebar`}
+ fieldKey={this.layoutDoc[this.SidebarKey + '_type_collection'] === 'translation' ? `${this.fieldKey}_translation` : `${this.fieldKey}_sidebar`}
/>
</div>
);
};
return (
<div className={'formattedTextBox-sidebar' + (Doc.ActiveTool !== InkTool.None ? '-inking' : '')} style={{ width: `${this.layout_sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}>
- {renderComponent(StrCast(this.layoutDoc.sidebar_collectionType))}
+ {renderComponent(StrCast(this.layoutDoc[this.SidebarKey + '_type_collection']))}
</div>
);
}
cycleAlternateText = () => {
if (this.layoutDoc._layout_enableAltContentUI) {
- const usePath = this.rootDoc[`${this.props.fieldKey}_usePath`];
+ const usePath = this.rootDoc[`_${this.props.fieldKey}_usePath`];
this.rootDoc[`_${this.props.fieldKey}_usePath`] = usePath === undefined ? 'alternate' : usePath === 'alternate' ? 'alternate:hover' : undefined;
}
};
@computed get overlayAlternateIcon() {
- const usePath = this.rootDoc[`${this.props.fieldKey}_usePath`];
+ const usePath = this.rootDoc[`_${this.props.fieldKey}_usePath`];
return (
<Tooltip
title={
@@ -2036,24 +2054,47 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
}
@observable _isHovering = false;
onPassiveWheel = (e: WheelEvent) => {
+ if (e.clientX > this.ProseRef!.getBoundingClientRect().right) {
+ if (this.rootDoc[this.SidebarKey + '_type_collection'] === CollectionViewType.Freeform) {
+ // if the scrolled freeform is a child of the sidebar component, we need to let the event go through
+ // so react can let the freeform view handle it. We prevent default to stop any containing views from scrolling
+ e.preventDefault();
+ }
+ return;
+ }
+
// 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();
+ // prevent default if selected || child is active but this doc isn't scrollable
+ if (
+ (this._scrollRef.current?.scrollHeight ?? 0) <= Math.ceil(Number(this.layoutDoc._height)) && //
+ (this.props.isSelected() || this.isAnyChildContentActive())
+ ) {
+ e.preventDefault();
+ }
e.stopPropagation();
}
};
_oldWheel: any;
+ @computed get fontColor() {
+ return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color);
+ }
+ @computed get fontSize() {
+ return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontSize);
+ }
+ @computed get fontFamily() {
+ return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontFamily);
+ }
+ @computed get fontWeight() {
+ return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontWeight);
+ }
render() {
TraceMobx();
- const active = this.props.isContentActive();
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;
+ setTimeout(() => !this.props.isContentActive() && FormattedTextBoxComment.textBox === this && FormattedTextBoxComment.Hide);
const paddingX = NumCast(this.layoutDoc._xMargin, this.props.xPadding || 0);
const paddingY = NumCast(this.layoutDoc._yMargin, this.props.yPadding || 0);
- const selPad = (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
@@ -2070,23 +2111,23 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
? {}
: {
transform: `scale(${scale})`,
- transformOrigin: 'top left',
width: `${100 / scale}%`,
height: `${100 / scale}%`,
}),
- display: !this.props.isContentActive() && this.props.thumbShown?.() ? 'none' : undefined,
+ // display: !this.props.isContentActive() && this.props.thumbShown?.() ? 'none' : undefined,
transition: 'inherit',
// overflowY: this.layoutDoc._layout_autoHeight ? "hidden" : undefined,
- color: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color),
- fontSize: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontSize),
- fontFamily: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontFamily),
- fontWeight: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontWeight),
+ color: this.fontColor,
+ fontSize: this.fontSize,
+ fontFamily: this.fontFamily,
+ fontWeight: this.fontWeight,
...styleFromLayoutString,
}}>
<div
className="formattedTextBox-cont"
ref={this._ref}
style={{
+ cursor: this.props.isContentActive() ? 'text' : undefined,
overflow: this.layout_autoHeight && this.props.CollectionFreeFormDocumentView?.() ? 'hidden' : undefined, //x this breaks viewing an layout_autoHeight doc in its own tab, or in the lightbox
height: this.props.height || (this.layout_autoHeight && this.props.renderDepth && !this.props.suppressSetHeight ? 'max-content' : undefined),
pointerEvents: Doc.ActiveTool === InkTool.None && !this.props.onBrowseClick?.() ? undefined : 'none',
@@ -2101,31 +2142,30 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
onPointerDown={this.onPointerDown}
onDoubleClick={this.onDoubleClick}>
<div
- className={`formattedTextBox-outer${active ? '-selected' : ''}`}
+ className="formattedTextBox-outer"
ref={this._scrollRef}
style={{
- width: this.props.dontSelectOnLoad ? '100%' : `calc(100% - ${this.layout_sidebarWidthPercent})`,
+ width: this.props.dontSelectOnLoad || this.noSidebar ? '100%' : `calc(100% - ${this.layout_sidebarWidthPercent})`,
overflow: this.layoutDoc._createDocOnCR ? 'hidden' : this.layoutDoc._layout_autoHeight ? 'visible' : undefined,
}}
onScroll={this.onScroll}
onDrop={this.ondrop}>
<div
- className={minimal ? 'formattedTextBox-minimal' : `formattedTextBox-inner${rounded}${selPaddingClass}`}
+ className={`formattedTextBox-inner${rounded}`}
ref={this.createDropTarget}
style={{
padding: StrCast(this.layoutDoc._textBoxPadding),
- paddingLeft: StrCast(this.layoutDoc._textBoxPaddingX, `${paddingX - selPad}px`),
- paddingRight: StrCast(this.layoutDoc._textBoxPaddingX, `${paddingX - selPad}px`),
- paddingTop: StrCast(this.layoutDoc._textBoxPaddingY, `${paddingY - selPad}px`),
- paddingBottom: StrCast(this.layoutDoc._textBoxPaddingY, `${paddingY - selPad}px`),
- // pointerEvents: !active && IsFollowLinkScript(this.layoutDoc.onClick) ? 'none' : undefined,
+ paddingLeft: StrCast(this.layoutDoc._textBoxPaddingX, `${paddingX}px`),
+ paddingRight: StrCast(this.layoutDoc._textBoxPaddingX, `${paddingX}px`),
+ paddingTop: StrCast(this.layoutDoc._textBoxPaddingY, `${paddingY}px`),
+ paddingBottom: StrCast(this.layoutDoc._textBoxPaddingY, `${paddingY}px`),
}}
/>
</div>
{this.noSidebar || this.props.dontSelectOnLoad || !this.SidebarShown || this.layout_sidebarWidthPercent === '0%' ? null : this.sidebarCollection}
- {this.noSidebar || this.Document._layout_noSidebar || this.props.dontSelectOnLoad || this.Document._createDocOnCR ? null : this.sidebarHandle}
+ {this.noSidebar || this.Document._layout_noSidebar || this.props.dontSelectOnLoad || this.Document._createDocOnCR || this.layoutDoc._chromeHidden ? null : this.sidebarHandle}
{this.audioHandle}
- {this.layoutDoc._layout_enableAltContentUI ? this.overlayAlternateIcon : null}
+ {this.layoutDoc._layout_enableAltContentUI && !this.layoutDoc._chromeHidden ? this.overlayAlternateIcon : null}
</div>
</div>
);
diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
index 112a0d87e..ec11079b4 100644
--- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
+++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
@@ -334,7 +334,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
//Command to create a blank space
bind('Space', (state: EditorState, dispatch: (tx: Transaction) => void) => {
- if (GetEffectiveAcl(props.DataDoc) != AclEdit && GetEffectiveAcl(props.DataDoc) != AclAugment && GetEffectiveAcl(props.DataDoc) != AclAdmin) return true;
+ if (props.DataDoc && GetEffectiveAcl(props.DataDoc) != AclEdit && GetEffectiveAcl(props.DataDoc) != AclAugment && GetEffectiveAcl(props.DataDoc) != AclAdmin) return true;
const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
dispatch(splitMetadata(marks, state.tr));
return false;
diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx
index 7c3e4baad..e3ac4fb9d 100644
--- a/src/client/views/nodes/formattedText/RichTextMenu.tsx
+++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx
@@ -21,6 +21,7 @@ import { updateBullets } from './ProsemirrorExampleTransfer';
import './RichTextMenu.scss';
import { schema } from './schema_rts';
import { EquationBox } from '../EquationBox';
+import { numberRange } from '../../../../Utils';
const { toggleMark } = require('prosemirror-commands');
@observer
@@ -104,7 +105,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
_disposer: IReactionDisposer | undefined;
componentDidMount() {
this._disposer = reaction(
- () => SelectionManager.Views(),
+ () => SelectionManager.Views().slice(),
views => this.updateMenu(undefined, undefined, undefined)
);
}
@@ -149,19 +150,31 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
setMark = (mark: Mark, state: EditorState, dispatch: any, dontToggle: boolean = false) => {
if (mark) {
- const node = (state.selection as NodeSelection).node;
+ const liFirst = numberRange(state.selection.$from.depth + 1).find(i => state.selection.$from.node(i)?.type === state.schema.nodes.list_item);
+ const liTo = numberRange(state.selection.$to.depth + 1).find(i => state.selection.$to.node(i)?.type === state.schema.nodes.list_item);
+ const olFirst = numberRange(state.selection.$from.depth + 1).find(i => state.selection.$from.node(i)?.type === state.schema.nodes.ordered_list);
+ const nodeOl = (liFirst && liTo && state.selection.$from.node(liFirst) !== state.selection.$to.node(liTo) && olFirst) || (!liFirst && !liTo && olFirst);
+ const newPos = nodeOl ? numberRange(state.selection.from).findIndex(i => state.doc.nodeAt(i)?.type === state.schema.nodes.ordered_list) : state.selection.from;
+ const node = (state.selection as NodeSelection).node ?? (newPos >= 0 ? state.doc.nodeAt(newPos) : undefined);
if (node?.type === schema.nodes.ordered_list) {
let attrs = node.attrs;
if (mark.type === schema.marks.pFontFamily) attrs = { ...attrs, fontFamily: mark.attrs.family };
if (mark.type === schema.marks.pFontSize) attrs = { ...attrs, fontSize: mark.attrs.fontSize };
if (mark.type === schema.marks.pFontColor) attrs = { ...attrs, fontColor: mark.attrs.color };
- const tr = updateBullets(state.tr.setNodeMarkup(state.selection.from, node.type, attrs), state.schema);
- dispatch(tr.setSelection(new NodeSelection(tr.doc.resolve(state.selection.from))));
- } else if (dontToggle) {
- const tr = state.tr.addMark(state.selection.from, state.selection.to, mark);
- dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(state.selection.from), tr.doc.resolve(state.selection.to)))); // bcz: need to redo the selection because ctrl-a selections disappear otherwise
- } else {
- toggleMark(mark.type, mark.attrs)(state, dispatch);
+ const tr = updateBullets(state.tr.setNodeMarkup(newPos, node.type, attrs), state.schema);
+ dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(state.selection.from), tr.doc.resolve(state.selection.to))));
+ }
+ {
+ const state = this.view?.state;
+ const tr = this.view?.state.tr;
+ if (tr && state) {
+ if (dontToggle) {
+ tr.addMark(state.selection.from, state.selection.to, mark);
+ dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(state.selection.from), tr.doc.resolve(state.selection.to)))); // bcz: need to redo the selection because ctrl-a selections disappear otherwise
+ } else {
+ toggleMark(mark.type, mark.attrs)(state, dispatch);
+ }
+ }
}
}
};
diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts
index ac1e7ce5d..3e2afd2ce 100644
--- a/src/client/views/nodes/formattedText/RichTextRules.ts
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -243,13 +243,13 @@ export class RichTextRules {
// create a text display of a metadata field on this or another document, or create a hyperlink portal to another document
// [[<fieldKey> : <Doc>]]
- // [[:Doc]] => hyperlink
+ // [[:docTitle]] => hyperlink
// [[fieldKey]] => show field
// [[fieldKey=value]] => show field and also set its value
- // [[fieldKey:Doc]] => show field of doc
+ // [[fieldKey:docTitle]] => show field of doc
new InputRule(new RegExp(/\[\[([a-zA-Z_\? \-0-9]*)(=[a-zA-Z_@\? /\-0-9]*)?(:[a-zA-Z_@:\.\? \-0-9]+)?\]\]$/), (state, match, start, end) => {
const fieldKey = match[1];
- const docId = match[3]?.replace(':', '');
+ const docTitle = match[3]?.replace(':', '');
const value = match[2]?.substring(1);
const linkToDoc = (target: Doc) => {
const rstate = this.TextBox.EditorView?.state;
@@ -266,12 +266,12 @@ export class RichTextRules {
}
};
if (!fieldKey) {
- if (docId) {
- const target = DocServer.QUERY_SERVER_CACHE(docId);
- if (target) setTimeout(() => linkToDoc(target));
- else DocServer.GetRefField(docId).then(docx => linkToDoc((docx instanceof Doc && docx) || Docs.Create.FreeformDocument([], { title: docId + '(auto)', _width: 500, _height: 500 }, docId)));
-
- return state.tr.deleteRange(end - 1, end).deleteRange(start, start + 3);
+ if (docTitle) {
+ const target = DocServer.FindDocByTitle(docTitle);
+ if (target) {
+ setTimeout(() => linkToDoc(target));
+ return state.tr.deleteRange(end - 1, end).deleteRange(start, start + 3);
+ }
}
return state.tr;
}
@@ -279,7 +279,8 @@ export class RichTextRules {
const num = value.match(/^[0-9.]$/);
this.Document[DocData][fieldKey] = value === 'true' ? true : value === 'false' ? false : num ? Number(value) : value;
}
- const fieldView = state.schema.nodes.dashField.create({ fieldKey, docId, hideKey: false });
+ const target = DocServer.FindDocByTitle(docTitle);
+ const fieldView = state.schema.nodes.dashField.create({ fieldKey, docId: target?.[Id], hideKey: false });
return state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))).replaceSelectionWith(fieldView, true);
}),
diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts
index 7e17008bb..6f07588b3 100644
--- a/src/client/views/nodes/formattedText/marks_rts.ts
+++ b/src/client/views/nodes/formattedText/marks_rts.ts
@@ -348,7 +348,7 @@ export const marks: { [index: string]: MarkSpec } = {
excludes: 'user_mark',
group: 'inline',
toDOM(node: any) {
- const uid = node.attrs.userid.replace('.', '').replace('@', '');
+ const uid = node.attrs.userid.replace(/\./g, '').replace(/@/g, '');
const min = Math.round(node.attrs.modified / 60);
const hr = Math.round(min / 60);
const day = Math.round(hr / 60 / 24);
diff --git a/src/client/views/nodes/generativeFill/GenerativeFill.scss b/src/client/views/nodes/generativeFill/GenerativeFill.scss
new file mode 100644
index 000000000..c2669a950
--- /dev/null
+++ b/src/client/views/nodes/generativeFill/GenerativeFill.scss
@@ -0,0 +1,97 @@
+$navHeight: 5rem;
+$canvasSize: 1024px;
+$scale: 0.5;
+
+.generativeFillContainer {
+ position: absolute;
+ top: 0;
+ left: 0;
+ z-index: 9999;
+ height: 100vh;
+ width: 100vw;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+
+ .generativeFillControls {
+ flex-shrink: 0;
+ height: $navHeight;
+ color: #000000;
+ background-color: #ffffff;
+ z-index: 999;
+ width: 100%;
+ display: flex;
+ gap: 3rem;
+ justify-content: space-between;
+ align-items: center;
+ border-bottom: 1px solid #c7cdd0;
+ padding: 0 2rem;
+
+ h1 {
+ font-size: 1.5rem;
+ }
+ }
+
+ .drawingArea {
+ cursor: none;
+ touch-action: none;
+ position: relative;
+ flex-grow: 1;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ background-color: #f0f4f6;
+
+ canvas {
+ display: block;
+ position: absolute;
+ transform-origin: 50% 50%;
+ }
+
+ .pointer {
+ pointer-events: none;
+ position: absolute;
+ border-radius: 50%;
+ width: 50px;
+ height: 50px;
+ border: 1px solid #ffffff;
+ transform: translate(-50%, -50%);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ .innerPointer {
+ width: 100%;
+ height: 100%;
+ border: 1px solid #000000;
+ border-radius: 50%;
+ }
+ }
+
+ .iconContainer {
+ position: absolute;
+ top: 2rem;
+ left: 2rem;
+ display: flex;
+ flex-direction: column;
+ gap: 2rem;
+ }
+
+ .editsBox {
+ position: absolute;
+ top: 2rem;
+ right: 2rem;
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+
+ img {
+ transition: all 0.2s ease-in-out;
+ &:hover {
+ opacity: 0.8;
+ }
+ }
+ }
+ }
+}
diff --git a/src/client/views/nodes/generativeFill/GenerativeFill.tsx b/src/client/views/nodes/generativeFill/GenerativeFill.tsx
new file mode 100644
index 000000000..1ec6d6e3f
--- /dev/null
+++ b/src/client/views/nodes/generativeFill/GenerativeFill.tsx
@@ -0,0 +1,590 @@
+import { Checkbox, FormControlLabel, Slider, TextField } from '@mui/material';
+import { IconButton } from 'browndash-components';
+import { useEffect, useRef, useState } from 'react';
+import { CgClose } from 'react-icons/cg';
+import { IoMdRedo, IoMdUndo } from 'react-icons/io';
+import { Doc, DocListCast } from '../../../../fields/Doc';
+import { List } from '../../../../fields/List';
+import { NumCast } from '../../../../fields/Types';
+import { Utils } from '../../../../Utils';
+import { Docs, DocUtils } from '../../../documents/Documents';
+import { Networking } from '../../../Network';
+import { CollectionDockingView } from '../../collections/CollectionDockingView';
+import { OpenWhereMod } from '../DocumentView';
+import { ImageBox } from '../ImageBox';
+import './GenerativeFill.scss';
+import Buttons from './GenerativeFillButtons';
+import { BrushHandler } from './generativeFillUtils/BrushHandler';
+import { activeColor, canvasSize, eraserColor, freeformRenderSize, newCollectionSize, offsetDistanceY, offsetX } from './generativeFillUtils/generativeFillConstants';
+import { CursorData, ImageDimensions, Point } from './generativeFillUtils/generativeFillInterfaces';
+import { APISuccess, ImageUtility } from './generativeFillUtils/ImageHandler';
+import { PointerHandler } from './generativeFillUtils/PointerHandler';
+import React = require('react');
+
+enum BrushStyle {
+ ADD,
+ SUBTRACT,
+ MARQUEE,
+}
+
+interface GenerativeFillProps {
+ imageEditorOpen: boolean;
+ imageEditorSource: string;
+ imageRootDoc: Doc | undefined;
+ addDoc: ((doc: Doc | Doc[], annotationKey?: string) => boolean) | undefined;
+}
+
+// Added field on image doc: gen_fill_children: List of children Docs
+
+const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addDoc }: GenerativeFillProps) => {
+ const canvasRef = useRef<HTMLCanvasElement>(null);
+ const canvasBackgroundRef = useRef<HTMLCanvasElement>(null);
+ const drawingAreaRef = useRef<HTMLDivElement>(null);
+ const [cursorData, setCursorData] = useState<CursorData>({
+ x: 0,
+ y: 0,
+ width: 150,
+ });
+ const [isBrushing, setIsBrushing] = useState(false);
+ const [canvasScale, setCanvasScale] = useState(0.5);
+ // format: array of [image source, corresponding image Doc]
+ const [edits, setEdits] = useState<(string | Doc)[][]>([]);
+ const [edited, setEdited] = useState(false);
+ const [brushStyle, setBrushStyle] = useState<BrushStyle>(BrushStyle.ADD);
+ const [input, setInput] = useState('');
+ const [loading, setLoading] = useState(false);
+ const [canvasDims, setCanvasDims] = useState<ImageDimensions>({
+ width: canvasSize,
+ height: canvasSize,
+ });
+ // whether to create a new collection or not
+ const [isNewCollection, setIsNewCollection] = useState(true);
+ // the current image in the main canvas
+ const currImg = useRef<HTMLImageElement | null>(null);
+ // the unedited version of each generation (parent)
+ const originalImg = useRef<HTMLImageElement | null>(null);
+ const originalDoc = useRef<Doc | null>(null);
+ // stores history of data urls
+ const undoStack = useRef<string[]>([]);
+ // stores redo stack
+ const redoStack = useRef<string[]>([]);
+
+ // references to keep track of tree structure
+ const newCollectionRef = useRef<Doc | null>(null);
+ const parentDoc = useRef<Doc | null>(null);
+ const childrenDocs = useRef<Doc[]>([]);
+
+ // Undo and Redo
+ const handleUndo = () => {
+ const ctx = ImageUtility.getCanvasContext(canvasRef);
+ if (!ctx || !currImg.current || !canvasRef.current) return;
+
+ const target = undoStack.current[undoStack.current.length - 1];
+ if (!target) {
+ ImageUtility.drawImgToCanvas(currImg.current, canvasRef, canvasDims.width, canvasDims.height);
+ } else {
+ redoStack.current = [...redoStack.current, canvasRef.current.toDataURL()];
+ const img = new Image();
+ img.src = target;
+ ImageUtility.drawImgToCanvas(img, canvasRef, canvasDims.width, canvasDims.height);
+ undoStack.current = undoStack.current.slice(0, -1);
+ }
+ };
+
+ const handleRedo = () => {
+ const ctx = ImageUtility.getCanvasContext(canvasRef);
+ if (!ctx || !currImg.current || !canvasRef.current) return;
+
+ const target = redoStack.current[redoStack.current.length - 1];
+ if (!target) {
+ } else {
+ undoStack.current = [...undoStack.current, canvasRef.current?.toDataURL()];
+ const img = new Image();
+ img.src = target;
+ ImageUtility.drawImgToCanvas(img, canvasRef, canvasDims.width, canvasDims.height);
+ redoStack.current = redoStack.current.slice(0, -1);
+ }
+ };
+
+ // resets any erase strokes
+ const handleReset = () => {
+ if (!canvasRef.current || !currImg.current) return;
+ const ctx = ImageUtility.getCanvasContext(canvasRef);
+ if (!ctx) return;
+ ctx.clearRect(0, 0, canvasSize, canvasSize);
+ undoStack.current = [];
+ redoStack.current = [];
+ ImageUtility.drawImgToCanvas(currImg.current, canvasRef, canvasDims.width, canvasDims.height);
+ };
+
+ // initiate brushing
+ const handlePointerDown = (e: React.PointerEvent) => {
+ const canvas = canvasRef.current;
+ if (!canvas) return;
+ const ctx = ImageUtility.getCanvasContext(canvasRef);
+ if (!ctx) return;
+
+ undoStack.current = [...undoStack.current, canvasRef.current.toDataURL()];
+ redoStack.current = [];
+
+ setIsBrushing(true);
+ const { x, y } = PointerHandler.getPointRelativeToElement(canvas, e, canvasScale);
+ BrushHandler.brushCircleOverlay(x, y, cursorData.width / 2 / canvasScale, ctx, eraserColor, brushStyle === BrushStyle.SUBTRACT);
+ };
+
+ // stop brushing, push to undo stack
+ const handlePointerUp = (e: React.PointerEvent) => {
+ const ctx = ImageUtility.getCanvasContext(canvasBackgroundRef);
+ if (!ctx) return;
+ if (!isBrushing) return;
+ setIsBrushing(false);
+ };
+
+ // handles brushing on pointer movement
+ useEffect(() => {
+ if (!isBrushing) return;
+ const canvas = canvasRef.current;
+ if (!canvas) return;
+ const ctx = ImageUtility.getCanvasContext(canvasRef);
+ if (!ctx) return;
+
+ const handlePointerMove = (e: PointerEvent) => {
+ const currPoint = PointerHandler.getPointRelativeToElement(canvas, e, canvasScale);
+ const lastPoint: Point = {
+ x: currPoint.x - e.movementX / canvasScale,
+ y: currPoint.y - e.movementY / canvasScale,
+ };
+ BrushHandler.createBrushPathOverlay(lastPoint, currPoint, cursorData.width / 2 / canvasScale, ctx, eraserColor, brushStyle === BrushStyle.SUBTRACT);
+ };
+
+ drawingAreaRef.current?.addEventListener('pointermove', handlePointerMove);
+ return () => {
+ drawingAreaRef.current?.removeEventListener('pointermove', handlePointerMove);
+ };
+ }, [isBrushing]);
+
+ // first load
+ useEffect(() => {
+ const loadInitial = async () => {
+ if (!imageEditorSource || imageEditorSource === '') return;
+ const img = new Image();
+ const res = await ImageUtility.urlToBase64(imageEditorSource);
+ if (!res) return;
+ img.src = `data:image/png;base64,${res}`;
+
+ img.onload = () => {
+ currImg.current = img;
+ originalImg.current = img;
+ const imgWidth = img.naturalWidth;
+ const imgHeight = img.naturalHeight;
+ const scale = Math.min(canvasSize / imgWidth, canvasSize / imgHeight);
+ const width = imgWidth * scale;
+ const height = imgHeight * scale;
+ setCanvasDims({ width, height });
+ };
+ };
+
+ loadInitial();
+
+ // cleanup
+ return () => {
+ setInput('');
+ setEdited(false);
+ newCollectionRef.current = null;
+ parentDoc.current = null;
+ childrenDocs.current = [];
+ currImg.current = null;
+ originalImg.current = null;
+ originalDoc.current = null;
+ undoStack.current = [];
+ redoStack.current = [];
+ ImageUtility.clearCanvas(canvasRef);
+ };
+ }, [canvasRef, imageEditorSource]);
+
+ // once the appropriate dimensions are set, draw the image to the canvas
+ useEffect(() => {
+ if (!currImg.current) return;
+ ImageUtility.drawImgToCanvas(currImg.current, canvasRef, canvasDims.width, canvasDims.height);
+ }, [canvasDims]);
+
+ // handles brush sizing
+ useEffect(() => {
+ const handleKeyPress = (e: KeyboardEvent) => {
+ if (e.key === 'ArrowUp') {
+ e.preventDefault();
+ e.stopPropagation();
+ setCursorData(data => ({ ...data, width: data.width + 5 }));
+ } else if (e.key === 'ArrowDown') {
+ e.preventDefault();
+ e.stopPropagation();
+ setCursorData(data => (data.width >= 20 ? { ...data, width: data.width - 5 } : data));
+ }
+ };
+ window.addEventListener('keydown', handleKeyPress);
+ return () => window.removeEventListener('keydown', handleKeyPress);
+ }, []);
+
+ // handle pinch zoom
+ useEffect(() => {
+ const handlePinch = (e: WheelEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ const delta = e.deltaY;
+ const scaleFactor = delta > 0 ? 0.98 : 1.02;
+ setCanvasScale(prevScale => prevScale * scaleFactor);
+ };
+
+ drawingAreaRef.current?.addEventListener('wheel', handlePinch, {
+ passive: false,
+ });
+ return () => drawingAreaRef.current?.removeEventListener('wheel', handlePinch);
+ }, [drawingAreaRef]);
+
+ // updates the current position of the cursor
+ const updateCursorData = (e: React.PointerEvent) => {
+ const drawingArea = drawingAreaRef.current;
+ if (!drawingArea) return;
+ const { x, y } = PointerHandler.getPointRelativeToElement(drawingArea, e, 1);
+ setCursorData(data => ({
+ ...data,
+ x,
+ y,
+ }));
+ };
+
+ // Get AI Edit
+ const getEdit = async () => {
+ const img = currImg.current;
+ if (!img) return;
+ const canvas = canvasRef.current;
+ if (!canvas) return;
+ const ctx = ImageUtility.getCanvasContext(canvasRef);
+ if (!ctx) return;
+ setLoading(true);
+ setEdited(true);
+ try {
+ const canvasOriginalImg = ImageUtility.getCanvasImg(img);
+ if (!canvasOriginalImg) return;
+ const canvasMask = ImageUtility.getCanvasMask(canvas, canvasOriginalImg);
+ if (!canvasMask) return;
+ const maskBlob = await ImageUtility.canvasToBlob(canvasMask);
+ const imgBlob = await ImageUtility.canvasToBlob(canvasOriginalImg);
+ const res = await ImageUtility.getEdit(imgBlob, maskBlob, input !== '' ? input + ' in the same style' : 'Fill in the image in the same style', 2);
+ // const res = await ImageUtility.mockGetEdit(img.src);
+
+ // create first image
+ if (!newCollectionRef.current) {
+ if (!isNewCollection && imageRootDoc) {
+ // if the parent hasn't been set yet
+ if (!parentDoc.current) parentDoc.current = imageRootDoc;
+ } else {
+ if (!(originalImg.current && imageRootDoc)) return;
+ // create new collection and add it to the view
+ newCollectionRef.current = Docs.Create.FreeformDocument([], {
+ x: NumCast(imageRootDoc.x) + NumCast(imageRootDoc._width) + offsetX,
+ y: NumCast(imageRootDoc.y),
+ _width: newCollectionSize,
+ _height: newCollectionSize,
+ title: 'Image edit collection',
+ });
+ DocUtils.MakeLink(imageRootDoc, newCollectionRef.current, { link_relationship: 'Image Edit Version History', link_displayLine: false });
+
+ // opening new tab
+ CollectionDockingView.AddSplit(newCollectionRef.current, OpenWhereMod.right);
+
+ // add the doc to the main freeform
+ await createNewImgDoc(originalImg.current, true);
+ }
+ } else {
+ childrenDocs.current = [];
+ }
+
+ originalImg.current = currImg.current;
+ originalDoc.current = parentDoc.current;
+ const { urls } = res as APISuccess;
+ const imgUrls = await Promise.all(urls.map(url => ImageUtility.convertImgToCanvasUrl(url, canvasDims.width, canvasDims.height)));
+ const imgRes = await Promise.all(
+ imgUrls.map(async url => {
+ const saveRes = await onSave(url);
+ return [url, saveRes as Doc];
+ })
+ );
+ setEdits(imgRes);
+ const image = new Image();
+ image.src = imgUrls[0];
+ ImageUtility.drawImgToCanvas(image, canvasRef, canvasDims.width, canvasDims.height);
+ currImg.current = image;
+ parentDoc.current = imgRes[0][1] as Doc;
+ } catch (err) {
+ console.log(err);
+ }
+ setLoading(false);
+ };
+
+ // adjusts all the img positions to be aligned
+ const adjustImgPositions = () => {
+ if (!parentDoc.current) return;
+ const startY = NumCast(parentDoc.current.y);
+ const children = DocListCast(parentDoc.current.gen_fill_children);
+ const len = children.length;
+ let initialYPositions: number[] = [];
+ for (let i = 0; i < len; i++) {
+ initialYPositions.push(startY + i * offsetDistanceY);
+ }
+ children.forEach((doc, i) => {
+ if (len % 2 === 1) {
+ doc.y = initialYPositions[i] - Math.floor(len / 2) * offsetDistanceY;
+ } else {
+ doc.y = initialYPositions[i] - (len / 2 - 1 / 2) * offsetDistanceY;
+ }
+ });
+ };
+
+ // creates a new image document and returns its reference
+ const createNewImgDoc = async (img: HTMLImageElement, firstDoc: boolean): Promise<Doc | undefined> => {
+ if (!imageRootDoc) return;
+ const src = img.src;
+ const [result] = await Networking.PostToServer('/uploadRemoteImage', { sources: [src] });
+ const source = Utils.prepend(result.accessPaths.agnostic.client);
+
+ if (firstDoc) {
+ const x = 0;
+ const initialY = 0;
+ const newImg = Docs.Create.ImageDocument(source, {
+ x: x,
+ y: initialY,
+ _height: freeformRenderSize,
+ _width: freeformRenderSize,
+ data_nativeWidth: result.nativeWidth,
+ data_nativeHeight: result.nativeHeight,
+ });
+ if (isNewCollection && newCollectionRef.current) {
+ Doc.AddDocToList(newCollectionRef.current, undefined, newImg);
+ } else {
+ addDoc?.(newImg);
+ }
+ parentDoc.current = newImg;
+ return newImg;
+ } else {
+ if (!parentDoc.current) return;
+ const x = NumCast(parentDoc.current.x) + freeformRenderSize + offsetX;
+ const initialY = 0;
+
+ const newImg = Docs.Create.ImageDocument(source, {
+ x: x,
+ y: initialY,
+ _height: freeformRenderSize,
+ _width: freeformRenderSize,
+ data_nativeWidth: result.nativeWidth,
+ data_nativeHeight: result.nativeHeight,
+ });
+
+ const parentList = DocListCast(parentDoc.current.gen_fill_children);
+ if (parentList.length > 0) {
+ parentList.push(newImg);
+ parentDoc.current.gen_fill_children = new List<Doc>(parentList);
+ } else {
+ parentDoc.current.gen_fill_children = new List<Doc>([newImg]);
+ }
+
+ DocUtils.MakeLink(parentDoc.current, newImg, { link_relationship: `Image edit; Prompt: ${input}`, link_displayLine: true });
+ adjustImgPositions();
+
+ if (isNewCollection && newCollectionRef.current) {
+ Doc.AddDocToList(newCollectionRef.current, undefined, newImg);
+ } else {
+ addDoc?.(newImg);
+ }
+ return newImg;
+ }
+ };
+
+ // Saves an image to the collection
+ const onSave = async (src: string) => {
+ const img = new Image();
+ img.src = src;
+ if (!currImg.current || !originalImg.current || !imageRootDoc) return;
+ try {
+ const res = await createNewImgDoc(img, false);
+ return res;
+ } catch (err) {
+ console.log(err);
+ }
+ };
+
+ // Closes the editor view
+ const handleViewClose = () => {
+ ImageBox.setImageEditorOpen(false);
+ ImageBox.setImageEditorSource('');
+ if (newCollectionRef.current) {
+ newCollectionRef.current.fitContentOnce = true;
+ }
+ setEdits([]);
+ };
+
+ return (
+ <div className="generativeFillContainer" style={{ display: imageEditorOpen ? 'flex' : 'none' }}>
+ <div className="generativeFillControls">
+ <h1>Image Editor</h1>
+ <div style={{ display: 'flex', alignItems: 'center', gap: '1.5rem' }}>
+ <FormControlLabel
+ control={
+ <Checkbox
+ // disable once edited has been clicked (doesn't make sense to change after first edit)
+ disabled={edited}
+ checked={isNewCollection}
+ onChange={e => {
+ setIsNewCollection(prev => !prev);
+ }}
+ />
+ }
+ label={'Create New Collection'}
+ labelPlacement="end"
+ sx={{ whiteSpace: 'nowrap' }}
+ />
+ <Buttons getEdit={getEdit} loading={loading} onReset={handleReset} />
+ <IconButton color={activeColor} tooltip="close" icon={<CgClose size={'16px'} />} onClick={handleViewClose} />
+ </div>
+ </div>
+ {/* Main canvas for editing */}
+ <div
+ className="drawingArea" // this only works if pointerevents: none is set on the custom pointer
+ ref={drawingAreaRef}
+ onPointerOver={updateCursorData}
+ onPointerMove={updateCursorData}
+ onPointerDown={handlePointerDown}
+ onPointerUp={handlePointerUp}>
+ <canvas ref={canvasRef} width={canvasDims.width} height={canvasDims.height} style={{ transform: `scale(${canvasScale})` }} />
+ <canvas ref={canvasBackgroundRef} width={canvasDims.width} height={canvasDims.height} style={{ transform: `scale(${canvasScale})` }} />
+ <div
+ className="pointer"
+ style={{
+ left: cursorData.x,
+ top: cursorData.y,
+ width: cursorData.width,
+ height: cursorData.width,
+ }}>
+ <div className="innerPointer"></div>
+ </div>
+ {/* Icons */}
+ <div className="iconContainer">
+ {/* Undo and Redo */}
+ <IconButton
+ style={{ cursor: 'pointer' }}
+ onPointerDown={e => {
+ e.stopPropagation();
+ handleUndo();
+ }}
+ onPointerUp={e => {
+ e.stopPropagation();
+ }}
+ color={activeColor}
+ tooltip="Undo"
+ icon={<IoMdUndo />}
+ />
+ <IconButton
+ style={{ cursor: 'pointer' }}
+ onPointerDown={e => {
+ e.stopPropagation();
+ handleRedo();
+ }}
+ onPointerUp={e => {
+ e.stopPropagation();
+ }}
+ color={activeColor}
+ tooltip="Redo"
+ icon={<IoMdRedo />}
+ />
+ <div onPointerDown={e => e.stopPropagation()} style={{ height: 225, width: '100%', display: 'flex', justifyContent: 'center', cursor: 'pointer' }}>
+ <Slider
+ sx={{
+ '& input[type="range"]': {
+ WebkitAppearance: 'slider-vertical',
+ },
+ }}
+ orientation="vertical"
+ min={25}
+ max={500}
+ defaultValue={150}
+ size="small"
+ valueLabelDisplay="auto"
+ onChange={(e, val) => {
+ setCursorData(prev => ({ ...prev, width: val as number }));
+ }}
+ />
+ </div>
+ </div>
+ {/* Edits thumbnails*/}
+ <div className="editsBox">
+ {edits.map((edit, i) => (
+ <img
+ key={i}
+ width={75}
+ src={edit[0] as string}
+ style={{ cursor: 'pointer' }}
+ onClick={async () => {
+ const img = new Image();
+ img.src = edit[0] as string;
+ ImageUtility.drawImgToCanvas(img, canvasRef, canvasDims.width, canvasDims.height);
+ currImg.current = img;
+ parentDoc.current = edit[1] as Doc;
+ }}
+ />
+ ))}
+ {/* Original img thumbnail */}
+ {edits.length > 0 && (
+ <div style={{ position: 'relative' }}>
+ <label
+ style={{
+ position: 'absolute',
+ bottom: 10,
+ left: 10,
+ color: '#ffffff',
+ fontSize: '0.8rem',
+ letterSpacing: '1px',
+ textTransform: 'uppercase',
+ }}>
+ Original
+ </label>
+ <img
+ width={75}
+ src={originalImg.current?.src}
+ style={{ cursor: 'pointer' }}
+ onClick={() => {
+ if (!originalImg.current) return;
+ const img = new Image();
+ img.src = originalImg.current.src;
+ ImageUtility.drawImgToCanvas(img, canvasRef, canvasDims.width, canvasDims.height);
+ currImg.current = img;
+ parentDoc.current = originalDoc.current;
+ }}
+ />
+ </div>
+ )}
+ </div>
+ </div>
+ <div>
+ <TextField
+ value={input}
+ onChange={e => setInput(e.target.value)}
+ disabled={isBrushing}
+ type="text"
+ label="Prompt"
+ placeholder="Prompt..."
+ InputLabelProps={{ style: { fontSize: '16px' } }}
+ inputProps={{ style: { fontSize: '16px' } }}
+ sx={{
+ backgroundColor: '#ffffff',
+ position: 'absolute',
+ bottom: '16px',
+ transform: 'translateX(calc(50vw - 50%))',
+ width: 'calc(100vw - 64px)',
+ }}
+ />
+ </div>
+ </div>
+ );
+};
+
+export default GenerativeFill;
diff --git a/src/client/views/nodes/generativeFill/GenerativeFillButtons.scss b/src/client/views/nodes/generativeFill/GenerativeFillButtons.scss
new file mode 100644
index 000000000..0180ef904
--- /dev/null
+++ b/src/client/views/nodes/generativeFill/GenerativeFillButtons.scss
@@ -0,0 +1,4 @@
+.generativeFillBtnContainer {
+ display: flex;
+ gap: 1rem;
+}
diff --git a/src/client/views/nodes/generativeFill/GenerativeFillButtons.tsx b/src/client/views/nodes/generativeFill/GenerativeFillButtons.tsx
new file mode 100644
index 000000000..10eca358e
--- /dev/null
+++ b/src/client/views/nodes/generativeFill/GenerativeFillButtons.tsx
@@ -0,0 +1,44 @@
+import './GenerativeFillButtons.scss';
+import React = require('react');
+import ReactLoading from 'react-loading';
+import { activeColor } from './generativeFillUtils/generativeFillConstants';
+import { Button, IconButton, Type } from 'browndash-components';
+import { AiOutlineInfo } from 'react-icons/ai';
+
+interface ButtonContainerProps {
+ getEdit: () => Promise<void>;
+ loading: boolean;
+ onReset: () => void;
+}
+
+const Buttons = ({ loading, getEdit, onReset }: ButtonContainerProps) => {
+ return (
+ <div className="generativeFillBtnContainer">
+ <Button text="RESET" type={Type.PRIM} color={activeColor} onClick={onReset} />
+ {loading ? (
+ <Button
+ text="GET EDITS"
+ type={Type.TERT}
+ color={activeColor}
+ icon={<ReactLoading type="spin" color={'#ffffff'} width={20} height={20} />}
+ iconPlacement="right"
+ onClick={() => {
+ if (!loading) getEdit();
+ }}
+ />
+ ) : (
+ <Button
+ text="GET EDITS"
+ type={Type.TERT}
+ color={activeColor}
+ onClick={() => {
+ if (!loading) getEdit();
+ }}
+ />
+ )}
+ <IconButton type={Type.SEC} color={activeColor} tooltip="Open Documentation" icon={<AiOutlineInfo size={'16px'} />} onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/features/generativeai/#editing', '_blank')} />
+ </div>
+ );
+};
+
+export default Buttons;
diff --git a/src/client/views/nodes/generativeFill/generativeFillUtils/BrushHandler.ts b/src/client/views/nodes/generativeFill/generativeFillUtils/BrushHandler.ts
new file mode 100644
index 000000000..f4ec70fbc
--- /dev/null
+++ b/src/client/views/nodes/generativeFill/generativeFillUtils/BrushHandler.ts
@@ -0,0 +1,25 @@
+import { GenerativeFillMathHelpers } from './GenerativeFillMathHelpers';
+import { eraserColor } from './generativeFillConstants';
+import { Point } from './generativeFillInterfaces';
+
+export class BrushHandler {
+ static brushCircleOverlay = (x: number, y: number, brushRadius: number, ctx: CanvasRenderingContext2D, fillColor: string, erase: boolean) => {
+ ctx.globalCompositeOperation = 'destination-out';
+ ctx.fillStyle = fillColor;
+ ctx.shadowColor = eraserColor;
+ ctx.shadowBlur = 5;
+ ctx.beginPath();
+ ctx.arc(x, y, brushRadius, 0, 2 * Math.PI);
+ ctx.fill();
+ ctx.closePath();
+ };
+
+ static createBrushPathOverlay = (startPoint: Point, endPoint: Point, brushRadius: number, ctx: CanvasRenderingContext2D, fillColor: string, erase: boolean) => {
+ const dist = GenerativeFillMathHelpers.distanceBetween(startPoint, endPoint);
+
+ for (let i = 0; i < dist; i += 5) {
+ const s = i / dist;
+ BrushHandler.brushCircleOverlay(startPoint.x * (1 - s) + endPoint.x * s, startPoint.y * (1 - s) + endPoint.y * s, brushRadius, ctx, fillColor, erase);
+ }
+ };
+}
diff --git a/src/client/views/nodes/generativeFill/generativeFillUtils/GenerativeFillMathHelpers.ts b/src/client/views/nodes/generativeFill/generativeFillUtils/GenerativeFillMathHelpers.ts
new file mode 100644
index 000000000..97e03ff20
--- /dev/null
+++ b/src/client/views/nodes/generativeFill/generativeFillUtils/GenerativeFillMathHelpers.ts
@@ -0,0 +1,10 @@
+import { Point } from './generativeFillInterfaces';
+
+export class GenerativeFillMathHelpers {
+ static distanceBetween = (p1: Point, p2: Point) => {
+ return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
+ };
+ static angleBetween = (p1: Point, p2: Point) => {
+ return Math.atan2(p2.x - p1.x, p2.y - p1.y);
+ };
+}
diff --git a/src/client/views/nodes/generativeFill/generativeFillUtils/ImageHandler.ts b/src/client/views/nodes/generativeFill/generativeFillUtils/ImageHandler.ts
new file mode 100644
index 000000000..47a14135f
--- /dev/null
+++ b/src/client/views/nodes/generativeFill/generativeFillUtils/ImageHandler.ts
@@ -0,0 +1,314 @@
+import { RefObject } from 'react';
+import { bgColor, canvasSize } from './generativeFillConstants';
+
+export interface APISuccess {
+ status: 'success';
+ urls: string[];
+}
+
+export interface APIError {
+ status: 'error';
+ message: string;
+}
+
+export class ImageUtility {
+ /**
+ *
+ * @param canvas Canvas to convert
+ * @returns Blob of canvas
+ */
+ static canvasToBlob = (canvas: HTMLCanvasElement): Promise<Blob> => {
+ return new Promise(resolve => {
+ canvas.toBlob(blob => {
+ if (blob) {
+ resolve(blob);
+ }
+ }, 'image/png');
+ });
+ };
+
+ // given a square api image, get the cropped img
+ static getCroppedImg = (img: HTMLImageElement, width: number, height: number): HTMLCanvasElement | undefined => {
+ // Create a new canvas element
+ const canvas = document.createElement('canvas');
+ canvas.width = width;
+ canvas.height = height;
+ const ctx = canvas.getContext('2d');
+ if (ctx) {
+ // Clear the canvas
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ if (width < height) {
+ // horizontal padding, x offset
+ const xOffset = (canvasSize - width) / 2;
+ ctx.drawImage(img, xOffset, 0, canvas.width, canvas.height, 0, 0, canvas.width, canvas.height);
+ } else {
+ // vertical padding, y offset
+ const yOffset = (canvasSize - height) / 2;
+ ctx.drawImage(img, 0, yOffset, canvas.width, canvas.height, 0, 0, canvas.width, canvas.height);
+ }
+ return canvas;
+ }
+ };
+
+ // converts an image to a canvas data url
+ static convertImgToCanvasUrl = async (imageSrc: string, width: number, height: number): Promise<string> => {
+ return new Promise<string>((resolve, reject) => {
+ const img = new Image();
+ img.onload = () => {
+ const canvas = this.getCroppedImg(img, width, height);
+ if (canvas) {
+ const dataUrl = canvas.toDataURL();
+ resolve(dataUrl);
+ }
+ };
+ img.onerror = error => {
+ reject(error);
+ };
+ img.src = imageSrc;
+ });
+ };
+
+ // calls the openai api to get image edits
+ static getEdit = async (imgBlob: Blob, maskBlob: Blob, prompt: string, n?: number): Promise<APISuccess | APIError> => {
+ const apiUrl = 'https://api.openai.com/v1/images/edits';
+ const fd = new FormData();
+ fd.append('image', imgBlob, 'image.png');
+ fd.append('mask', maskBlob, 'mask.png');
+ fd.append('prompt', prompt);
+ fd.append('size', '1024x1024');
+ fd.append('n', n ? JSON.stringify(n) : '1');
+ fd.append('response_format', 'b64_json');
+
+ try {
+ const res = await fetch(apiUrl, {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${process.env.OPENAI_KEY}`,
+ },
+ body: fd,
+ });
+ const data = await res.json();
+ console.log(data.data);
+ return {
+ status: 'success',
+ urls: (data.data as { b64_json: string }[]).map(data => `data:image/png;base64,${data.b64_json}`),
+ };
+ } catch (err) {
+ console.log(err);
+ return { status: 'error', message: 'API error.' };
+ }
+ };
+
+ // mock api call
+ static mockGetEdit = async (mockSrc: string): Promise<APISuccess | APIError> => {
+ return {
+ status: 'success',
+ urls: [mockSrc, mockSrc, mockSrc],
+ };
+ };
+
+ // Gets the canvas rendering context of a canvas
+ static getCanvasContext = (canvasRef: RefObject<HTMLCanvasElement>): CanvasRenderingContext2D | null => {
+ if (!canvasRef.current) return null;
+ const ctx = canvasRef.current.getContext('2d');
+ if (!ctx) return null;
+ return ctx;
+ };
+
+ // Helper for downloading the canvas (for debugging)
+ static downloadCanvas = (canvas: HTMLCanvasElement) => {
+ const url = canvas.toDataURL();
+ const downloadLink = document.createElement('a');
+ downloadLink.href = url;
+ downloadLink.download = 'canvas';
+
+ downloadLink.click();
+ downloadLink.remove();
+ };
+
+ // Download the canvas (for debugging)
+ static downloadImageCanvas = (imgUrl: string) => {
+ const img = new Image();
+ img.src = imgUrl;
+ img.onload = () => {
+ const canvas = document.createElement('canvas');
+ canvas.width = canvasSize;
+ canvas.height = canvasSize;
+ const ctx = canvas.getContext('2d');
+ ctx?.drawImage(img, 0, 0, canvasSize, canvasSize);
+
+ this.downloadCanvas(canvas);
+ };
+ };
+
+ // Clears the canvas
+ static clearCanvas = (canvasRef: React.RefObject<HTMLCanvasElement>) => {
+ const ctx = this.getCanvasContext(canvasRef);
+ if (!ctx || !canvasRef.current) return;
+ ctx.clearRect(0, 0, canvasRef.current.width, canvasRef.current.height);
+ };
+
+ // Draws the image to the current canvas
+ static drawImgToCanvas = (img: HTMLImageElement, canvasRef: React.RefObject<HTMLCanvasElement>, width: number, height: number) => {
+ const drawImg = (img: HTMLImageElement) => {
+ const ctx = this.getCanvasContext(canvasRef);
+ if (!ctx) return;
+ ctx.globalCompositeOperation = 'source-over';
+ ctx.clearRect(0, 0, width, height);
+ ctx.drawImage(img, 0, 0, width, height);
+ };
+
+ if (img.complete) {
+ drawImg(img);
+ } else {
+ img.onload = () => {
+ drawImg(img);
+ };
+ }
+ };
+
+ // Gets the image mask for the openai endpoint
+ static getCanvasMask = (srcCanvas: HTMLCanvasElement, paddedCanvas: HTMLCanvasElement): HTMLCanvasElement | undefined => {
+ const canvas = document.createElement('canvas');
+ canvas.width = canvasSize;
+ canvas.height = canvasSize;
+ const ctx = canvas.getContext('2d');
+ if (!ctx) return;
+ ctx?.clearRect(0, 0, canvasSize, canvasSize);
+ ctx.drawImage(paddedCanvas, 0, 0);
+
+ // extract and set padding data
+ if (srcCanvas.height > srcCanvas.width) {
+ // horizontal padding, x offset
+ const xOffset = (canvasSize - srcCanvas.width) / 2;
+ ctx?.clearRect(xOffset, 0, srcCanvas.width, srcCanvas.height);
+ ctx.drawImage(srcCanvas, xOffset, 0, srcCanvas.width, srcCanvas.height);
+ } else {
+ // vertical padding, y offset
+ const yOffset = (canvasSize - srcCanvas.height) / 2;
+ ctx?.clearRect(0, yOffset, srcCanvas.width, srcCanvas.height);
+ ctx.drawImage(srcCanvas, 0, yOffset, srcCanvas.width, srcCanvas.height);
+ }
+ return canvas;
+ };
+
+ // Fills in the blank areas of the image with an image reflection (to fill in a square-shaped canvas)
+ static drawHorizontalReflection = (ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement, xOffset: number) => {
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
+ const data = imageData.data;
+ for (let i = 0; i < canvas.height; i++) {
+ for (let j = 0; j < xOffset; j++) {
+ const targetIdx = 4 * (i * canvas.width + j);
+ const sourceI = i;
+ const sourceJ = xOffset + (xOffset - j);
+ const sourceIdx = 4 * (sourceI * canvas.width + sourceJ);
+ data[targetIdx] = data[sourceIdx];
+ data[targetIdx + 1] = data[sourceIdx + 1];
+ data[targetIdx + 2] = data[sourceIdx + 2];
+ }
+ }
+ for (let i = 0; i < canvas.height; i++) {
+ for (let j = canvas.width - 1; j >= canvas.width - 1 - xOffset; j--) {
+ const targetIdx = 4 * (i * canvas.width + j);
+ const sourceI = i;
+ const sourceJ = canvas.width - 1 - xOffset - (xOffset - (canvas.width - j));
+ const sourceIdx = 4 * (sourceI * canvas.width + sourceJ);
+ data[targetIdx] = data[sourceIdx];
+ data[targetIdx + 1] = data[sourceIdx + 1];
+ data[targetIdx + 2] = data[sourceIdx + 2];
+ }
+ }
+ ctx.putImageData(imageData, 0, 0);
+ };
+
+ // Fills in the blank areas of the image with an image reflection (to fill in a square-shaped canvas)
+ static drawVerticalReflection = (ctx: CanvasRenderingContext2D, canvas: HTMLCanvasElement, yOffset: number) => {
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
+ const data = imageData.data;
+ for (let j = 0; j < canvas.width; j++) {
+ for (let i = 0; i < yOffset; i++) {
+ const targetIdx = 4 * (i * canvas.width + j);
+ const sourceJ = j;
+ const sourceI = yOffset + (yOffset - i);
+ const sourceIdx = 4 * (sourceI * canvas.width + sourceJ);
+ data[targetIdx] = data[sourceIdx];
+ data[targetIdx + 1] = data[sourceIdx + 1];
+ data[targetIdx + 2] = data[sourceIdx + 2];
+ }
+ }
+ for (let j = 0; j < canvas.width; j++) {
+ for (let i = canvas.height - 1; i >= canvas.height - 1 - yOffset; i--) {
+ const targetIdx = 4 * (i * canvas.width + j);
+ const sourceJ = j;
+ const sourceI = canvas.height - 1 - yOffset - (yOffset - (canvas.height - i));
+ const sourceIdx = 4 * (sourceI * canvas.width + sourceJ);
+ data[targetIdx] = data[sourceIdx];
+ data[targetIdx + 1] = data[sourceIdx + 1];
+ data[targetIdx + 2] = data[sourceIdx + 2];
+ }
+ }
+ ctx.putImageData(imageData, 0, 0);
+ };
+
+ // Gets the unaltered (besides filling in padding) version of the image for the api call
+ static getCanvasImg = (img: HTMLImageElement): HTMLCanvasElement | undefined => {
+ const canvas = document.createElement('canvas');
+ canvas.width = canvasSize;
+ canvas.height = canvasSize;
+ const ctx = canvas.getContext('2d');
+ if (!ctx) return;
+ // fix scaling
+ const scale = Math.min(canvasSize / img.width, canvasSize / img.height);
+ const width = Math.floor(img.width * scale);
+ const height = Math.floor(img.height * scale);
+ ctx?.clearRect(0, 0, canvasSize, canvasSize);
+ ctx.fillStyle = bgColor;
+ ctx.fillRect(0, 0, canvasSize, canvasSize);
+
+ // extract and set padding data
+ if (img.naturalHeight > img.naturalWidth) {
+ // horizontal padding, x offset
+ const xOffset = Math.floor((canvasSize - width) / 2);
+ ctx.drawImage(img, xOffset, 0, width, height);
+
+ // draw reflected image padding
+ this.drawHorizontalReflection(ctx, canvas, xOffset);
+ } else {
+ // vertical padding, y offset
+ const yOffset = Math.floor((canvasSize - height) / 2);
+ ctx.drawImage(img, 0, yOffset, width, height);
+
+ // draw reflected image padding
+ this.drawVerticalReflection(ctx, canvas, yOffset);
+ }
+ return canvas;
+ };
+
+ /**
+ * Converts a url to base64 (tainted canvas workaround)
+ */
+ static urlToBase64 = async (imageUrl: string): Promise<string | undefined> => {
+ try {
+ const res = await fetch(imageUrl);
+ const blob = await res.blob();
+
+ return new Promise<string>((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onload = () => {
+ const base64Data = reader.result?.toString().split(',')[1];
+ if (base64Data) {
+ resolve(base64Data);
+ } else {
+ reject(new Error('Failed to convert.'));
+ }
+ };
+ reader.onerror = () => {
+ reject(new Error('Error reading image data'));
+ };
+ reader.readAsDataURL(blob);
+ });
+ } catch (err) {
+ console.error(err);
+ }
+ };
+}
diff --git a/src/client/views/nodes/generativeFill/generativeFillUtils/PointerHandler.ts b/src/client/views/nodes/generativeFill/generativeFillUtils/PointerHandler.ts
new file mode 100644
index 000000000..9e620ad11
--- /dev/null
+++ b/src/client/views/nodes/generativeFill/generativeFillUtils/PointerHandler.ts
@@ -0,0 +1,15 @@
+import { Point } from "./generativeFillInterfaces";
+
+export class PointerHandler {
+ static getPointRelativeToElement = (
+ element: HTMLElement,
+ e: React.PointerEvent | PointerEvent,
+ scale: number
+ ): Point => {
+ const boundingBox = element.getBoundingClientRect();
+ return {
+ x: (e.clientX - boundingBox.x) / scale,
+ y: (e.clientY - boundingBox.y) / scale,
+ };
+ };
+}
diff --git a/src/client/views/nodes/generativeFill/generativeFillUtils/generativeFillConstants.ts b/src/client/views/nodes/generativeFill/generativeFillUtils/generativeFillConstants.ts
new file mode 100644
index 000000000..4772304bc
--- /dev/null
+++ b/src/client/views/nodes/generativeFill/generativeFillUtils/generativeFillConstants.ts
@@ -0,0 +1,9 @@
+export const canvasSize = 1024;
+export const freeformRenderSize = 300;
+export const offsetDistanceY = freeformRenderSize + 400;
+export const offsetX = 200;
+export const newCollectionSize = 500;
+
+export const activeColor = '#1976d2';
+export const eraserColor = '#e1e9ec';
+export const bgColor = '#f0f4f6';
diff --git a/src/client/views/nodes/generativeFill/generativeFillUtils/generativeFillInterfaces.ts b/src/client/views/nodes/generativeFill/generativeFillUtils/generativeFillInterfaces.ts
new file mode 100644
index 000000000..1e7801056
--- /dev/null
+++ b/src/client/views/nodes/generativeFill/generativeFillUtils/generativeFillInterfaces.ts
@@ -0,0 +1,20 @@
+export interface CursorData {
+ x: number;
+ y: number;
+ width: number;
+}
+
+export interface Point {
+ x: number;
+ y: number;
+}
+
+export enum BrushMode {
+ ADD,
+ SUBTRACT,
+}
+
+export interface ImageDimensions {
+ width: number;
+ height: number;
+}
diff --git a/src/client/views/nodes/trails/PresBox.scss b/src/client/views/nodes/trails/PresBox.scss
index bf56b4d9e..0b51813a5 100644
--- a/src/client/views/nodes/trails/PresBox.scss
+++ b/src/client/views/nodes/trails/PresBox.scss
@@ -121,7 +121,7 @@
.dropdown.active {
transform: rotate(180deg);
color: $light-blue;
- opacity: 0.8;
+ opacity: 0.7;
}
.presBox-radioButtons {
@@ -187,9 +187,6 @@
font-size: 11;
font-weight: 200;
height: 20;
- background-color: $white;
- color: $black;
- border: solid 1px $black;
display: flex;
margin-left: 5px;
margin-top: 5px;
@@ -210,13 +207,11 @@
.ribbon-propertyUpDownItem {
cursor: pointer;
- color: white;
display: flex;
justify-content: center;
align-items: center;
height: 100%;
width: 100%;
- background: $black;
}
.ribbon-propertyUpDownItem:hover {
@@ -609,7 +604,6 @@
font-weight: 200;
height: 20;
background-color: $white;
- border: solid 1px rgba(0, 0, 0, 0.5);
display: flex;
color: $black;
margin-top: 5px;
@@ -691,16 +685,19 @@
padding-right: 5px;
padding-top: 3;
padding-bottom: 3;
+ opacity: 0.8;
}
.presBox-dropdownOption:hover {
position: relative;
- background-color: lightgrey;
+ opacity: 1;
+ font-weight: bold;
}
.presBox-dropdownOption.active {
position: relative;
- background-color: $light-blue;
+ opacity: 1;
+ font-weight: bold;
}
.presBox-dropdownOptions {
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 56af67802..bcec2d2bd 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -3,7 +3,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tooltip } from '@material-ui/core';
import { action, computed, IReactionDisposer, observable, ObservableSet, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
-import { Doc, DocListCast, Field, FieldResult, Opt, StrListCast } from '../../../../fields/Doc';
+import { Doc, DocListCast, FieldResult, Opt, StrListCast } from '../../../../fields/Doc';
import { Animation } from '../../../../fields/DocSymbols';
import { Copy, Id } from '../../../../fields/FieldSymbols';
import { InkField } from '../../../../fields/InkField';
@@ -13,7 +13,7 @@ import { listSpec } from '../../../../fields/Schema';
import { ComputedField, ScriptField } from '../../../../fields/ScriptField';
import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types';
import { AudioField } from '../../../../fields/URLField';
-import { emptyFunction, emptyPath, returnFalse, returnOne, setupMoveUpEvents, StopEvent } from '../../../../Utils';
+import { emptyFunction, emptyPath, lightOrDark, returnFalse, returnOne, setupMoveUpEvents, StopEvent } from '../../../../Utils';
import { DocServer } from '../../../DocServer';
import { Docs } from '../../../documents/Documents';
import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
@@ -36,6 +36,9 @@ import { FieldView, FieldViewProps } from '../FieldView';
import { ScriptingBox } from '../ScriptingBox';
import './PresBox.scss';
import { PresEffect, PresEffectDirection, PresMovement, PresStatus } from './PresEnums';
+import { BranchingTrailManager } from '../../../util/BranchingTrailManager';
+import { TreeView } from '../../collections/TreeView';
+import { OverlayView } from '../../OverlayView';
const { Howl } = require('howler');
export interface pinDataTypes {
@@ -52,6 +55,7 @@ export interface pinDataTypes {
dataview?: boolean;
poslayoutview?: boolean;
dataannos?: boolean;
+ map?: boolean;
}
export interface PinProps {
audioRange?: boolean;
@@ -74,7 +78,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
constructor(props: any) {
super(props);
if (!PresBox.navigateToDocScript) {
- PresBox.navigateToDocScript = ScriptField.MakeFunction('navigateToDoc(self.presentationTargetDoc, self)')!;
+ PresBox.navigateToDocScript = ScriptField.MakeFunction('navigateToDoc(self.presentation_targetDoc, self)')!;
}
}
@@ -116,7 +120,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
return DocListCast(this.rootDoc[this.presFieldKey]);
}
@computed get tagDocs() {
- return this.childDocs.map(doc => Cast(doc.presentationTargetDoc, Doc, null));
+ return this.childDocs.map(doc => Cast(doc.presentation_targetDoc, Doc, null));
}
@computed get itemIndex() {
return NumCast(this.rootDoc._itemIndex);
@@ -125,10 +129,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
return DocCast(this.childDocs[NumCast(this.rootDoc._itemIndex)]);
}
@computed get targetDoc() {
- return Cast(this.activeItem?.presentationTargetDoc, Doc, null);
+ return Cast(this.activeItem?.presentation_targetDoc, Doc, null);
}
public static targetRenderedDoc = (doc: Doc) => {
- const targetDoc = Cast(doc?.presentationTargetDoc, Doc, null);
+ const targetDoc = Cast(doc?.presentation_targetDoc, Doc, null);
return targetDoc?.layout_unrendered ? DocCast(targetDoc.annotationOn) : targetDoc;
};
@computed get scrollable() {
@@ -149,7 +153,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@computed get selectedDoc() {
return this.selectedDocumentView?.rootDoc;
}
- isActiveItemTarget = (layoutDoc: Doc) => this.activeItem?.presentationTargetDoc === layoutDoc;
+ isActiveItemTarget = (layoutDoc: Doc) => this.activeItem?.presentation_targetDoc === layoutDoc;
clearSelectedArray = () => this.selectedArray.clear();
addToSelectedArray = action((doc: Doc) => this.selectedArray.add(doc));
removeFromSelectedArray = action((doc: Doc) => this.selectedArray.delete(doc));
@@ -196,18 +200,18 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
this.turnOffEdit(true);
this._disposers.selection = reaction(
- () => SelectionManager.Views(),
+ () => SelectionManager.Views().slice(),
views => (!PresBox.Instance || views.some(view => view.props.Document === this.rootDoc)) && this.updateCurrentPresentation(),
{ fireImmediately: true }
);
this._disposers.editing = reaction(
- () => this.layoutDoc.presStatus === PresStatus.Edit,
+ () => this.layoutDoc.presentation_status === PresStatus.Edit,
editing => {
if (editing) {
this.childDocs.forEach(doc => {
- if (doc.presIndexed !== undefined) {
+ if (doc.presentation_indexed !== undefined) {
this.progressivizedItems(doc)?.forEach(indexedDoc => (indexedDoc.opacity = undefined));
- doc.presIndexed = Math.min(this.progressivizedItems(doc)?.length ?? 0, 1);
+ doc.presentation_indexed = Math.min(this.progressivizedItems(doc)?.length ?? 0, 1);
}
});
}
@@ -224,10 +228,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
_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) => {
- const duration: number = NumCast(activeItem.presEndTime) - NumCast(activeItem.presStartTime);
+ const duration: number = NumCast(activeItem.config_clipEnd) - NumCast(activeItem.config_clipStart);
if ([DocumentType.VID, DocumentType.AUDIO].includes(targetDoc.type as any)) {
const targMedia = DocumentManager.Instance.getDocumentView(targetDoc);
- targMedia?.ComponentView?.playFrom?.(NumCast(activeItem.presStartTime), NumCast(activeItem.presStartTime) + duration);
+ targMedia?.ComponentView?.playFrom?.(NumCast(activeItem.config_clipStart), NumCast(activeItem.config_clipStart) + duration);
}
};
@@ -251,12 +255,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
(nextSelected: number, force = false) =>
() => {
if (nextSelected < this.childDocs.length) {
- if (force || this.childDocs[nextSelected].groupWithUp) {
+ if (force || this.childDocs[nextSelected].presentation_groupWithUp) {
this.addToSelectedArray(this.childDocs[nextSelected]);
- const serial = nextSelected + 1 < this.childDocs.length && NumCast(this.childDocs[nextSelected + 1].groupWithUp) > 1;
+ const serial = nextSelected + 1 < this.childDocs.length && NumCast(this.childDocs[nextSelected + 1].presentation_groupWithUp) > 1;
if (serial) {
this.gotoDocument(nextSelected, this.activeItem, true, async () => {
- const waitTime = NumCast(this.activeItem.presDuration) - NumCast(this.activeItem.presTransition);
+ const waitTime = NumCast(this.activeItem.presentation_duration);
await new Promise<void>(res => setTimeout(() => res(), Math.max(0, waitTime)));
doGroupWithUp(nextSelected + 1)();
});
@@ -274,16 +278,33 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
// docs within a slide target that will be progressively revealed
progressivizedItems = (doc: Doc) => {
const targetList = PresBox.targetRenderedDoc(doc);
- if (doc.presIndexed !== undefined && targetList) {
+ if (doc.presentation_indexed !== 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);
}
};
+
+ // go to documents chain
+ runSubroutines = (childrenToRun: Doc[], normallyNextSlide: Doc) => {
+ console.log(childrenToRun, normallyNextSlide, 'runSUBFUNC');
+ if (childrenToRun === undefined) {
+ console.log('children undefined');
+ return;
+ }
+ if (childrenToRun[0] === normallyNextSlide) {
+ return;
+ }
+
+ childrenToRun.forEach(child => {
+ DocumentManager.Instance.showDocument(child, {});
+ });
+ };
+
// Called when the user activates 'next' - to move to the next part of the pres. trail
@action
next = () => {
const progressiveReveal = (first: boolean) => {
- const presIndexed = Cast(this.activeItem?.presIndexed, 'number', null);
+ const presIndexed = Cast(this.activeItem?.presentation_indexed, 'number', null);
if (presIndexed !== undefined) {
const targetRenderedDoc = PresBox.targetRenderedDoc(this.activeItem);
targetRenderedDoc._dataTransition = 'all 1s';
@@ -296,8 +317,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
const targetView = listItems && DocumentManager.Instance.getFirstDocumentView(listItemDoc);
Doc.linkFollowUnhighlight();
Doc.HighlightDoc(listItemDoc);
- listItemDoc.presEffect = this.activeItem.presBulletEffect;
- listItemDoc.presTransition = 500;
+ listItemDoc.presentation_effect = this.activeItem.presBulletEffect;
+ listItemDoc.presentation_transition = 500;
targetView?.setAnimEffect(listItemDoc, 500);
if (targetView?.docView && this.activeItem.presBulletExpand) {
targetView.docView._animateScalingTo = 1.2;
@@ -308,7 +329,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
});
}
listItemDoc.opacity = undefined;
- this.activeItem.presIndexed = presIndexed + 1;
+ this.activeItem.presentation_indexed = presIndexed + 1;
}
return true;
}
@@ -319,10 +340,16 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
// 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;
+
+ // before moving onto next slide, run the subroutines :)
+ const currentDoc = this.childDocs[this.itemIndex];
+ //could i do this.childDocs[this.itemIndex] for first arg?
+ this.runSubroutines(TreeView.GetRunningChildren.get(currentDoc)?.(), this.childDocs[this.itemIndex + 1]);
+
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)) {
+ if (this.childDocs[this.itemIndex + 1] === undefined && (this.layoutDoc.presLoop || this.layoutDoc.presentation_status === 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
@@ -338,9 +365,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
const activeItem: Doc = this.activeItem;
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;
+ let didZoom = activeItem.presentation_movement;
+ for (; prevSelected > 0 && this.childDocs[Math.max(0, prevSelected - 1)].presentation_groupWithUp; prevSelected--) {
+ didZoom = didZoom === 'none' ? this.childDocs[prevSelected].presentation_movement : didZoom;
}
if (activeItem && this.childDocs[this.itemIndex - 1] !== undefined) {
// Case 2: There are no other frames so it should go to the previous slide
@@ -361,20 +388,20 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
Doc.UnBrushAllDocs();
if (index >= 0 && index < this.childDocs.length) {
this.rootDoc._itemIndex = index;
- if (from?.mediaStopTriggerList && this.layoutDoc.presStatus !== PresStatus.Edit) {
+ if (from?.mediaStopTriggerList && this.layoutDoc.presentation_status !== PresStatus.Edit) {
DocListCast(from.mediaStopTriggerList).forEach(this.stopTempMedia);
}
- if (from?.mediaStop === 'auto' && this.layoutDoc.presStatus !== PresStatus.Edit) {
- this.stopTempMedia(from.presentationTargetDoc);
+ if (from?.presentation_mediaStop === 'auto' && this.layoutDoc.presentation_status !== PresStatus.Edit) {
+ this.stopTempMedia(from.presentation_targetDoc);
}
// If next slide is audio / video 'Play automatically' then the next slide should be played
- if (this.layoutDoc.presStatus !== PresStatus.Edit && (this.targetDoc.type === DocumentType.AUDIO || this.targetDoc.type === DocumentType.VID) && this.activeItem.mediaStart === 'auto') {
+ if (this.layoutDoc.presentation_status !== PresStatus.Edit && (this.targetDoc.type === DocumentType.AUDIO || this.targetDoc.type === DocumentType.VID) && this.activeItem.presentation_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(finished); //Handles movement to element only when presTrail is list
+ this.navigateToActiveItem(finished); //Handles movement to element only when presentationTrail is list
this.doHideBeforeAfter(); //Handles hide after/before
}
});
@@ -383,6 +410,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
const inkable = [DocumentType.INK].includes(targetType);
const scrollable = [DocumentType.PDF, DocumentType.RTF, DocumentType.WEB].includes(targetType) || target?._type_collection === CollectionViewType.Stacking;
const pannable = [DocumentType.IMG, DocumentType.PDF].includes(targetType) || (targetType === DocumentType.COL && target?._type_collection === CollectionViewType.Freeform);
+ const map = [DocumentType.MAP].includes(targetType);
const temporal = [DocumentType.AUDIO, DocumentType.VID].includes(targetType);
const clippable = [DocumentType.COMPARISON].includes(targetType);
const datarange = [DocumentType.FUNCPLOT].includes(targetType);
@@ -392,39 +420,39 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
const filters = true;
const pivot = true;
const dataannos = false;
- return { scrollable, pannable, inkable, type_collection, pivot, filters, temporal, clippable, dataview, datarange, poslayoutview, dataannos };
+ return { scrollable, pannable, inkable, type_collection, pivot, map, filters, temporal, clippable, dataview, datarange, poslayoutview, dataannos };
}
@action
playAnnotation = (anno: AudioField) => {};
@action
- static restoreTargetDocView(bestTargetView: Opt<DocumentView>, activeItem: Doc, transTime: number, pinDocLayout: boolean = BoolCast(activeItem.presPinLayout), pinDataTypes?: pinDataTypes, targetDoc?: Doc) {
+ static restoreTargetDocView(bestTargetView: Opt<DocumentView>, activeItem: Doc, transTime: number, pinDocLayout: boolean = BoolCast(activeItem.config_pinLayout), pinDataTypes?: pinDataTypes, targetDoc?: Doc) {
const bestTarget = bestTargetView?.rootDoc ?? (targetDoc?.layout_unrendered ? DocCast(targetDoc?.annotationOn) : targetDoc);
- if (!bestTarget || activeItem === bestTarget) return;
+ if (!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.x !== NumCast(activeItem.config_x, NumCast(bestTarget.x)) ||
+ bestTarget.y !== NumCast(activeItem.config_y, NumCast(bestTarget.y)) ||
+ bestTarget.rotation !== NumCast(activeItem.config_rotation, NumCast(bestTarget.rotation)) ||
+ bestTarget.width !== NumCast(activeItem.config_width, NumCast(bestTarget.width)) ||
+ bestTarget.height !== NumCast(activeItem.config_height, 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));
+ bestTarget.x = NumCast(activeItem.config_x, NumCast(bestTarget.x));
+ bestTarget.y = NumCast(activeItem.config_y, NumCast(bestTarget.y));
+ bestTarget.rotation = NumCast(activeItem.config_rotation, NumCast(bestTarget.rotation));
+ bestTarget.width = NumCast(activeItem.config_width, NumCast(bestTarget.width));
+ bestTarget.height = NumCast(activeItem.config_height, NumCast(bestTarget.height));
setTimeout(() => (bestTarget._dataTransition = undefined), transTime + 10);
changed = true;
}
}
- const activeFrame = activeItem.presActiveFrame ?? activeItem.presCurrentFrame;
+ const activeFrame = activeItem.config_activeFrame ?? activeItem.config_currentFrame;
if (activeFrame !== undefined) {
- const transTime = NumCast(activeItem.presTransition, 500);
- const acontext = activeItem.presActiveFrame !== undefined ? DocCast(DocCast(activeItem.presentationTargetDoc).embedContainer) : DocCast(activeItem.presentationTargetDoc);
+ const transTime = NumCast(activeItem.presentation_transition, 500);
+ const acontext = activeItem.config_activeFrame !== undefined ? DocCast(DocCast(activeItem.presentation_targetDoc).embedContainer) : DocCast(activeItem.presentation_targetDoc);
const context = DocCast(acontext)?.annotationOn ? DocCast(DocCast(acontext).annotationOn) : acontext;
if (context) {
const ffview = DocumentManager.Instance.getFirstDocumentView(context)?.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView;
@@ -434,73 +462,95 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
}
}
- if ((pinDataTypes?.dataview && activeItem.presData !== undefined) || (!pinDataTypes && activeItem.presData !== undefined)) {
+ if ((pinDataTypes?.dataview && activeItem.config_data !== undefined) || (!pinDataTypes && activeItem.config_data !== 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;
+ if (setData) setData(activeItem.config_data);
+ else Doc.GetProto(bestTarget)[fkey] = activeItem.config_data instanceof ObjectField ? activeItem.config_data[Copy]() : activeItem.config_data;
+ bestTarget[fkey + '_usePath'] = activeItem.config_usePath;
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]();
+ if (pinDataTypes?.datarange || (!pinDataTypes && activeItem.config_xRange !== undefined)) {
+ if (bestTarget.xRange !== activeItem.config_xRange) {
+ bestTarget.xRange = (activeItem.config_xRange as ObjectField)?.[Copy]();
changed = true;
}
- if (bestTarget.yRange !== activeItem.presYRange) {
- bestTarget.yRange = (activeItem.presYRange as ObjectField)?.[Copy]();
+ if (bestTarget.yRange !== activeItem.config_yRange) {
+ bestTarget.yRange = (activeItem.config_yRange as ObjectField)?.[Copy]();
changed = true;
}
}
- if (pinDataTypes?.clippable || (!pinDataTypes && activeItem.presClipWidth !== undefined)) {
+ if (pinDataTypes?.clippable || (!pinDataTypes && activeItem.config_clipWidth !== undefined)) {
const fkey = '_' + Doc.LayoutFieldKey(bestTarget);
- if (bestTarget[fkey + '_clipWidth'] !== activeItem.presClipWidth) {
- bestTarget[fkey + '_clipWidth'] = activeItem.presClipWidth;
+ if (bestTarget[fkey + '_clipWidth'] !== activeItem.config_clipWidth) {
+ bestTarget[fkey + '_clipWidth'] = activeItem.config_clipWidth;
changed = true;
}
}
- if (pinDataTypes?.temporal || (!pinDataTypes && activeItem.presStartTime !== undefined)) {
- if (bestTarget._layout_currentTimecode !== activeItem.presStartTime) {
- bestTarget._layout_currentTimecode = activeItem.presStartTime;
+ if (pinDataTypes?.map || (!pinDataTypes && activeItem.config_latitude !== undefined)) {
+ if (bestTarget.latitude !== activeItem.config_latitude) {
+ Doc.SetInPlace(bestTarget, 'latitude', NumCast(activeItem.config_latitude), true);
+ changed = true;
+ }
+ if (bestTarget.longitude !== activeItem.config_longitude) {
+ Doc.SetInPlace(bestTarget, 'longitude', NumCast(activeItem.config_longitude), true);
+ changed = true;
+ }
+ if (bestTarget.zoom !== activeItem.config_map_zoom) {
+ Doc.SetInPlace(bestTarget, 'map_zoom', NumCast(activeItem.config_map_zoom), true);
+ changed = true;
+ }
+ if (bestTarget.map_type !== activeItem.config_map_type) {
+ Doc.SetInPlace(bestTarget, 'map_type', StrCast(activeItem.config_map_type), true);
+ changed = true;
+ }
+ if (bestTarget.map !== activeItem.config_map) {
+ Doc.SetInPlace(bestTarget, 'map', StrCast(activeItem.config_map), true);
+ changed = true;
+ }
+ }
+ if (pinDataTypes?.temporal || (!pinDataTypes && activeItem.config_clipStart !== undefined)) {
+ if (bestTarget._layout_currentTimecode !== activeItem.config_clipStart) {
+ bestTarget._layout_currentTimecode = activeItem.config_clipStart;
changed = true;
}
}
- if (pinDataTypes?.inkable || (!pinDataTypes && (activeItem.presFillColor !== undefined || activeItem.color !== undefined))) {
- if (bestTarget.fillColor !== activeItem.presFillColor) {
- Doc.GetProto(bestTarget).fillColor = activeItem.presFillColor;
+ if (pinDataTypes?.inkable || (!pinDataTypes && (activeItem.config_fillColor !== undefined || activeItem.color !== undefined))) {
+ if (bestTarget.fillColor !== activeItem.config_fillColor) {
+ Doc.GetProto(bestTarget).fillColor = activeItem.config_fillColor;
changed = true;
}
- if (bestTarget.color !== activeItem.presColor) {
- Doc.GetProto(bestTarget).color = activeItem.presColor;
+ if (bestTarget.color !== activeItem.config_color) {
+ Doc.GetProto(bestTarget).color = activeItem.config_color;
changed = true;
}
if (bestTarget.width !== activeItem.width) {
- bestTarget._width = NumCast(activeItem.presWidth, NumCast(bestTarget.width));
+ bestTarget._width = NumCast(activeItem.config_width, NumCast(bestTarget.width));
changed = true;
}
if (bestTarget.height !== activeItem.height) {
- bestTarget._height = NumCast(activeItem.presHeight, NumCast(bestTarget.height));
+ bestTarget._height = NumCast(activeItem.config_height, 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;
+ if ((pinDataTypes?.type_collection && activeItem.config_viewType !== undefined) || (!pinDataTypes && activeItem.config_viewType !== undefined)) {
+ if (bestTarget._type_collection !== activeItem.config_viewType) {
+ bestTarget._type_collection = activeItem.config_viewType;
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>([]);
+ if ((pinDataTypes?.filters && activeItem.config_docFilters !== undefined) || (!pinDataTypes && activeItem.config_docFilters !== undefined)) {
+ if (bestTarget.childFilters !== activeItem.config_docFilters) {
+ bestTarget.childFilters = ObjectField.MakeCopy(activeItem.config_docFilters 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;
+ if ((pinDataTypes?.pivot && activeItem.config_pivotField !== undefined) || (!pinDataTypes && activeItem.config_pivotField !== undefined)) {
+ if (bestTarget.pivotField !== activeItem.config_pivotField) {
+ bestTarget.pivotField = activeItem.config_pivotField;
bestTarget._prevFilterIndex = 1; // need to revisit this...see CollectionTimeView
changed = true;
}
@@ -509,21 +559,21 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
changed = true;
}
- if (pinDataTypes?.scrollable || (!pinDataTypes && activeItem.presViewScroll !== undefined)) {
- if (bestTarget._layout_scrollTop !== activeItem.presViewScroll) {
- bestTarget._layout_scrollTop = activeItem.presViewScroll;
+ if (pinDataTypes?.scrollable || (!pinDataTypes && activeItem.config_scrollTop !== undefined)) {
+ if (bestTarget._layout_scrollTop !== activeItem.config_scrollTop) {
+ bestTarget._layout_scrollTop = activeItem.config_scrollTop;
changed = true;
- const contentBounds = Cast(activeItem.presPinViewBounds, listSpec('number'));
+ const contentBounds = Cast(activeItem.config_viewBounds, 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] });
+ dv?.brushView?.({ panX: (contentBounds[0] + contentBounds[2]) / 2, panY: (contentBounds[1] + contentBounds[3]) / 2, width: contentBounds[2] - contentBounds[0], height: contentBounds[3] - contentBounds[1] }, transTime);
}
}
}
- if (pinDataTypes?.dataannos || (!pinDataTypes && activeItem.presAnnotations !== undefined)) {
+ if (pinDataTypes?.dataannos || (!pinDataTypes && activeItem.config_annotations !== undefined)) {
const fkey = Doc.LayoutFieldKey(bestTarget);
const oldItems = DocListCast(bestTarget[fkey + '_annotations']).filter(doc => doc.layout_unrendered);
- const newItems = DocListCast(activeItem.presAnnotations).map(doc => {
+ const newItems = DocListCast(activeItem.config_annotations).map(doc => {
doc.hidden = false;
return doc;
});
@@ -536,11 +586,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
const newList = new List<Doc>([...oldItems, ...hiddenItems, ...newItems]);
Doc.GetProto(bestTarget)[fkey + '_annotations'] = newList;
}
- if (pinDataTypes?.poslayoutview || (!pinDataTypes && activeItem.presPinLayoutData !== undefined)) {
+ if (pinDataTypes?.poslayoutview || (!pinDataTypes && activeItem.config_pinLayoutData !== undefined)) {
changed = true;
const layoutField = Doc.LayoutFieldKey(bestTarget);
const transitioned = new Set<Doc>();
- StrListCast(activeItem.presPinLayoutData)
+ StrListCast(activeItem.config_pinLayoutData)
.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));
@@ -562,8 +612,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
});
setTimeout(() => Array.from(transitioned).forEach(action(doc => (doc._dataTransition = undefined))), transTime + 10);
}
- if ((pinDataTypes?.pannable || (!pinDataTypes && (activeItem.presPinViewBounds !== undefined || activeItem.presPanX !== undefined || activeItem.presViewScale !== undefined))) && !bestTarget._isGroup) {
- const contentBounds = Cast(activeItem.presPinViewBounds, listSpec('number'));
+ if ((pinDataTypes?.pannable || (!pinDataTypes && (activeItem.config_viewBounds !== undefined || activeItem.config_panX !== undefined || activeItem.config_viewScale !== undefined))) && !bestTarget._isGroup) {
+ const contentBounds = Cast(activeItem.config_viewBounds, 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._freeform_panX = viewport.panX;
@@ -571,15 +621,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
const dv = DocumentManager.Instance.getDocumentView(bestTarget);
if (dv) {
changed = true;
- const computedScale = NumCast(activeItem.presZoom, 1) * Math.min(dv.props.PanelWidth() / viewport.width, dv.props.PanelHeight() / viewport.height);
- activeItem.presMovement === PresMovement.Zoom && (bestTarget._freeform_scale = computedScale);
- dv.ComponentView?.brushView?.(viewport);
+ const computedScale = NumCast(activeItem.config_zoom, 1) * Math.min(dv.props.PanelWidth() / viewport.width, dv.props.PanelHeight() / viewport.height);
+ activeItem.presentation_movement === PresMovement.Zoom && (bestTarget._freeform_scale = computedScale);
+ dv.ComponentView?.brushView?.(viewport, transTime);
}
} else {
- 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;
+ if (bestTarget._freeform_panX !== activeItem.config_panX || bestTarget._freeform_panY !== activeItem.config_panY || bestTarget._freeform_scale !== activeItem.config_viewScale) {
+ bestTarget._freeform_panX = activeItem.config_panX ?? bestTarget._freeform_panX;
+ bestTarget._freeform_panY = activeItem.config_panY ?? bestTarget._freeform_panY;
+ bestTarget._freeform_scale = activeItem.config_viewScale ?? bestTarget._freeform_scale;
changed = true;
}
}
@@ -594,17 +644,19 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
/// target doc when navigating to it.
@action
static pinDocView(pinDoc: Doc, pinProps: PinProps, targetDoc: Doc) {
+ pinDoc.presentation = true;
+ pinDoc.config = '';
if (pinProps.pinDocLayout) {
- pinDoc.presPinLayout = true;
- pinDoc.presX = NumCast(targetDoc.x);
- pinDoc.presY = NumCast(targetDoc.y);
- pinDoc.presRotation = NumCast(targetDoc.rotation);
- pinDoc.presWidth = NumCast(targetDoc.width);
- pinDoc.presHeight = NumCast(targetDoc.height);
+ pinDoc.config_pinLayout = true;
+ pinDoc.config_x = NumCast(targetDoc.x);
+ pinDoc.config_y = NumCast(targetDoc.y);
+ pinDoc.config_rotation = NumCast(targetDoc.rotation);
+ pinDoc.config_width = NumCast(targetDoc.width);
+ pinDoc.config_height = NumCast(targetDoc.height);
}
if (pinProps.pinAudioPlay) pinDoc.presPlayAudio = true;
if (pinProps.pinData) {
- pinDoc.presPinData =
+ pinDoc.config_pinData =
pinProps.pinData.scrollable ||
pinProps.pinData.temporal ||
pinProps.pinData.pannable ||
@@ -616,30 +668,37 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
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;
+ pinDoc.config_usePath = targetDoc[fkey + '_usePath'];
+ pinDoc.config_data = targetDoc[fkey] instanceof ObjectField ? (targetDoc[fkey] as ObjectField)[Copy]() : targetDoc.data;
}
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));
+ pinDoc.config_annotations = 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;
+ pinDoc.config_fillColor = targetDoc.fillColor;
+ pinDoc.config_color = targetDoc.color;
+ pinDoc.config_width = targetDoc._width;
+ pinDoc.config_height = targetDoc._height;
}
- if (pinProps.pinData.scrollable) pinDoc.presViewScroll = targetDoc._layout_scrollTop;
+ if (pinProps.pinData.scrollable) pinDoc.config_scrollTop = targetDoc._layout_scrollTop;
if (pinProps.pinData.clippable) {
const fkey = Doc.LayoutFieldKey(targetDoc);
- pinDoc.presClipWidth = targetDoc[fkey + '_clipWidth'];
+ pinDoc.config_clipWidth = targetDoc[fkey + '_clipWidth'];
}
if (pinProps.pinData.datarange) {
- pinDoc.presXRange = undefined; //targetDoc?.xrange;
- pinDoc.presYRange = undefined; //targetDoc?.yrange;
+ pinDoc.config_xRange = undefined; //targetDoc?.xrange;
+ pinDoc.config_yRange = undefined; //targetDoc?.yrange;
+ }
+ if (pinProps.pinData.map) {
+ // pinDoc.config_latitude = targetDoc?.latitude;
+ // pinDoc.config_longitude = targetDoc?.longitude;
+ pinDoc.config_map_zoom = targetDoc?.map_zoom;
+ pinDoc.config_map_type = targetDoc?.map_type;
+ //...
}
if (pinProps.pinData.poslayoutview)
- pinDoc.presPinLayoutData = new List<string>(
+ pinDoc.config_pinLayoutData = new List<string>(
DocListCast(targetDoc[fkey] as ObjectField).map(d =>
JSON.stringify({
id: d[Id],
@@ -654,30 +713,39 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
})
)
);
- 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.type_collection) pinDoc.config_viewType = targetDoc._type_collection;
+ if (pinProps.pinData.filters) pinDoc.config_docFilters = ObjectField.MakeCopy(targetDoc.childFilters as ObjectField);
+ if (pinProps.pinData.pivot) pinDoc.config_pivotField = 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);
+ pinDoc.config_panX = NumCast(targetDoc._freeform_panX);
+ pinDoc.config_panY = NumCast(targetDoc._freeform_panY);
+ pinDoc.config_viewScale = 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);
+ pinDoc.config_clipStart = targetDoc._layout_currentTimecode;
+ const duration = NumCast(pinDoc[`${Doc.LayoutFieldKey(pinDoc)}_duration`], NumCast(targetDoc.config_clipStart) + 0.1);
+ pinDoc.config_clipEnd = 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.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]);
+ pinDoc.config_pinView = true;
+ pinDoc.config_viewScale = NumCast(targetDoc._freeform_scale, 1);
+ pinDoc.config_panX = bounds.left + bounds.width / 2;
+ pinDoc.config_panY = bounds.top + bounds.height / 2;
+ pinDoc.config_viewBounds = new List<number>([bounds.left, bounds.top, bounds.left + bounds.width, bounds.top + bounds.height]);
}
}
+
+ @action
+ static reversePin(pinDoc: Doc, targetDoc: Doc) {
+ // const fkey = Doc.LayoutFieldKey(targetDoc);
+ pinDoc.config_data = targetDoc.data;
+
+ console.log(pinDoc.presData);
+ }
+
/**
* This method makes sure that cursor navigates to the element that
* has the option open and last in the group.
@@ -712,33 +780,34 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
};
static NavigateToTarget(targetDoc: Doc, activeItem: Doc, finished?: () => void) {
- if (activeItem.presMovement === PresMovement.None && targetDoc.type === DocumentType.SCRIPTING) {
+ if (activeItem.presentation_movement === PresMovement.None && targetDoc.type === DocumentType.SCRIPTING) {
(DocumentManager.Instance.getFirstDocumentView(targetDoc)?.ComponentView as ScriptingBox)?.onRun?.();
return;
}
- const effect = activeItem.presEffect && activeItem.presEffect !== PresEffect.None ? activeItem.presEffect : undefined;
- const presTime = NumCast(activeItem.presTransition, effect ? 750 : 500);
+ const effect = activeItem.presentation_effect && activeItem.presentation_effect !== PresEffect.None ? activeItem.presentation_effect : undefined;
+ const presTime = NumCast(activeItem.presentation_transition, 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),
+ willPan: activeItem.presentation_movement !== PresMovement.None,
+ willZoomCentered: activeItem.presentation_movement === PresMovement.Zoom || activeItem.presentation_movement === PresMovement.Jump || activeItem.presentation_movement === PresMovement.Center,
+ zoomScale: activeItem.presentation_movement === PresMovement.Center ? 0 : NumCast(activeItem.config_zoom, 1),
+ zoomTime: activeItem.presentation_movement === PresMovement.Jump ? 0 : Math.min(Math.max(effect ? 750 : 500, (effect ? 0.2 : 1) * presTime), presTime),
effect: activeItem,
noSelect: true,
openLocation: OpenWhere.addLeft,
anchorDoc: activeItem,
easeFunc: StrCast(activeItem.presEaseFunc, 'ease') as any,
- zoomTextSelections: BoolCast(activeItem.presZoomText),
+ zoomTextSelections: BoolCast(activeItem.presentation_zoomText),
playAudio: BoolCast(activeItem.presPlayAudio),
+ playMedia: activeItem.presentation_mediaStart === 'auto',
};
- if (activeItem.presOpenInLightbox) {
+ if (activeItem.presentation_openInLightbox) {
const context = DocCast(targetDoc.annotationOn) ?? targetDoc;
if (!DocumentManager.Instance.getLightboxDocumentView(context)) {
LightboxView.SetLightboxDoc(context);
}
}
if (targetDoc) {
- if (activeItem.presentationTargetDoc instanceof Doc) activeItem.presentationTargetDoc[Animation] = undefined;
+ if (activeItem.presentation_targetDoc instanceof Doc) activeItem.presentation_targetDoc[Animation] = undefined;
DocumentManager.Instance.AddViewRenderedCb(LightboxView.LightboxDoc, dv => {
// if target or the doc it annotates is not in the lightbox, then close the lightbox
@@ -761,16 +830,16 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
const tagDoc = PresBox.targetRenderedDoc(curDoc);
const itemIndexes: number[] = this.getAllIndexes(this.tagDocs, tagDoc);
let opacity: Opt<number> = index === this.itemIndex ? 1 : undefined;
- if (curDoc.presHide) {
+ if (curDoc.presentation_hide) {
if (index !== this.itemIndex) {
opacity = 1;
}
}
const hidingIndBef = itemIndexes.find(item => item >= this.itemIndex) ?? itemIndexes.slice().reverse().lastElement();
- if (curDoc.presHideBefore && index === hidingIndBef) {
+ if (curDoc.presentation_hideBefore && index === hidingIndBef) {
if (index > this.itemIndex) {
opacity = 0;
- } else if (index === this.itemIndex || !curDoc.presHideAfter) {
+ } else if (index === this.itemIndex || !curDoc.presentation_hideAfter) {
opacity = 1;
setTimeout(() => (tagDoc._dataTransition = undefined), 1000);
}
@@ -780,15 +849,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
.slice()
.reverse()
.find(item => item <= this.itemIndex) ?? itemIndexes.lastElement();
- if (curDoc.presHideAfter && index === hidingIndAft) {
+ if (curDoc.presentation_hideAfter && index === hidingIndAft) {
if (index < this.itemIndex) {
opacity = 0;
- } else if (index === this.itemIndex || !curDoc.presHideBefore) {
+ } else if (index === this.itemIndex || !curDoc.presentation_hideBefore) {
opacity = 1;
}
}
const hidingInd = itemIndexes.find(item => item === this.itemIndex);
- if (curDoc.presHide && index === hidingInd) {
+ if (curDoc.presentation_hide && index === hidingInd) {
if (index === this.itemIndex) {
opacity = 0;
}
@@ -846,9 +915,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
// The function pauses the auto presentation
@action
pauseAutoPres = () => {
- if (this.layoutDoc.presStatus === PresStatus.Autoplay) {
+ if (this.layoutDoc.presentation_status === PresStatus.Autoplay) {
if (this._presTimer) clearTimeout(this._presTimer);
- this.layoutDoc.presStatus = PresStatus.Manual;
+ this.layoutDoc.presentation_status = PresStatus.Manual;
this.childDocs.forEach(this.stopTempMedia);
}
};
@@ -879,23 +948,23 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
runInAction(() => (this._expandBoolean = !this._expandBoolean));
this.rootDoc.expandBoolean = this._expandBoolean;
this.childDocs.forEach(doc => {
- doc.presExpandInlineButton = this._expandBoolean;
+ doc.presentation_expandInlineButton = this._expandBoolean;
});
};
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);
+ if (doc.presentation_hideBefore && index > startIndex) tagDoc.opacity = 0;
+ if (doc.presentation_hideAfter && index < startIndex) tagDoc.opacity = 0;
+ if (doc.presentation_indexed !== undefined && index >= startIndex) {
+ const startInd = NumCast(doc.presentation_indexedStart);
this.progressivizedItems(doc)
?.slice(startInd)
.forEach(indexedDoc => (indexedDoc.opacity = 0));
- doc.presIndexed = Math.min(this.progressivizedItems(doc)?.length ?? 0, startInd);
+ doc.presentation_indexed = Math.min(this.progressivizedItems(doc)?.length ?? 0, startInd);
}
- // if (doc.presHide && this.childDocs.indexOf(doc) === startIndex) tagDoc.opacity = 0;
+ // if (doc.presentation_hide && this.childDocs.indexOf(doc) === startIndex) tagDoc.opacity = 0;
});
};
@@ -909,13 +978,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
PresBox.Instance = this;
clearTimeout(this._presTimer);
if (this.childDocs.length) {
- this.layoutDoc.presStatus = PresStatus.Autoplay;
+ this.layoutDoc.presentation_status = PresStatus.Autoplay;
this.initializePresState(startIndex);
const func = () => {
- const delay = NumCast(this.activeItem.presDuration, this.activeItem.type === DocumentType.SCRIPTING ? 0 : 2500) + NumCast(this.activeItem.presTransition);
+ const delay = NumCast(this.activeItem.presentation_duration, this.activeItem.type === DocumentType.SCRIPTING ? 0 : 2500) + NumCast(this.activeItem.presentation_transition);
this._presTimer = setTimeout(() => {
- if (!this.next()) this.layoutDoc.presStatus = this._exitTrail?.() ?? PresStatus.Manual;
- this.layoutDoc.presStatus === PresStatus.Autoplay && func();
+ if (!this.next()) this.layoutDoc.presentation_status = this._exitTrail?.() ?? PresStatus.Manual;
+ this.layoutDoc.presentation_status === PresStatus.Autoplay && func();
}, delay);
};
this.gotoDocument(startIndex, this.activeItem, undefined, func);
@@ -949,7 +1018,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
doc._width = PresBox.minimizedWidth;
Doc.AddToMyOverlay(doc);
PresBox.Instance?.initializePresState(PresBox.Instance.itemIndex);
- return (doc.presStatus = PresStatus.Manual);
+ return (doc.presentation_status = PresStatus.Manual);
}
/**
@@ -993,39 +1062,37 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
});
movementName = action((activeItem: Doc) => {
- if (![PresMovement.Zoom, PresMovement.Pan, PresMovement.Center, PresMovement.Jump, PresMovement.None].includes(StrCast(activeItem.presMovement) as any)) {
+ if (![PresMovement.Zoom, PresMovement.Pan, PresMovement.Center, PresMovement.Jump, PresMovement.None].includes(StrCast(activeItem.presentation_movement) as any)) {
return PresMovement.Zoom;
}
- return StrCast(activeItem.presMovement);
+ return StrCast(activeItem.presentation_movement);
});
whenChildContentsActiveChanged = action((isActive: boolean) => this.props.whenChildContentsActiveChanged((this._isChildActive = isActive)));
// For dragging documents into the presentation trail
addDocumentFilter = (docs: Doc[]) => {
docs.forEach((doc, i) => {
- if (doc.presentationTargetDoc) return true;
+ if (doc.presentation_targetDoc) return true;
if (doc.type === DocumentType.LABEL) {
const audio = Cast(doc.annotationOn, Doc, null);
if (audio) {
- audio.mediaStart = 'manual';
- audio.mediaStop = 'manual';
- audio.presStartTime = NumCast(doc._timecodeToShow /* audioStart */, NumCast(doc._timecodeToShow /* videoStart */));
- audio.presEndTime = NumCast(doc._timecodeToHide /* audioEnd */, NumCast(doc._timecodeToHide /* videoEnd */));
- audio.presDuration = audio.presStartTime - audio.presEndTime;
+ audio.config_clipStart = NumCast(doc._timecodeToShow /* audioStart */, NumCast(doc._timecodeToShow /* videoStart */));
+ audio.config_clipEnd = NumCast(doc._timecodeToHide /* audioEnd */, NumCast(doc._timecodeToHide /* videoEnd */));
+ audio.presentation_duration = audio.config_clipStart - audio.config_clipEnd;
TabDocView.PinDoc(audio, { audioRange: true });
setTimeout(() => this.removeDocument(doc), 0);
return false;
}
} else {
- 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;
+ if (!doc.presentation_targetDoc) doc.title = doc.title + ' - Slide';
+ doc.presentation_targetDoc = doc.createdFrom; // dropped document will be a new embedding of an embedded document somewhere else.
+ doc.presentation_movement = PresMovement.Zoom;
+ if (this._expandBoolean) doc.presentation_expandInlineButton = true;
}
});
return true;
};
- childLayoutTemplate = () => (!this.isTreeOrStack ? DocCast(Doc.UserDoc().presElement) : DocCast(Doc.UserDoc().presElement));
+ childLayoutTemplate = () => Docs.Create.PresElementBoxDocument();
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;
@@ -1040,7 +1107,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@computed get listOfSelected() {
return Array.from(this.selectedArray).map((doc: Doc, index: any) => {
const curDoc = Cast(doc, Doc, null);
- const tagDoc = Cast(curDoc.presentationTargetDoc, Doc, null);
+ const tagDoc = Cast(curDoc.presentation_targetDoc, Doc, null);
if (curDoc && curDoc === this.activeItem)
return (
<div key={index} className="selectedList-items">
@@ -1084,7 +1151,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
if (index >= 0 && index < this.childDocs.length) {
this.rootDoc._itemIndex = index;
}
- } else this.gotoDocument(this.childDocs.indexOf(doc), this.activeItem);
+ } else {
+ this.gotoDocument(this.childDocs.indexOf(doc), this.activeItem);
+ }
this.updateCurrentPresentation(DocCast(doc.embedContainer));
};
@@ -1146,7 +1215,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
if (anchorNode && anchorNode.className?.includes('lm_title')) return;
switch (e.key) {
case 'Backspace':
- if (this.layoutDoc.presStatus === 'edit') {
+ if (this.layoutDoc.presentation_status === 'edit') {
undoBatch(
action(() => {
for (const doc of this.selectedArray) {
@@ -1163,11 +1232,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
case 'Escape':
if (Doc.IsInMyOverlay(this.layoutDoc)) {
this.exitClicked();
- } else if (this.layoutDoc.presStatus === PresStatus.Edit) {
+ } else if (this.layoutDoc.presentation_status === PresStatus.Edit) {
this.clearSelectedArray();
this._eleArray.length = this._dragArray.length = 0;
} else {
- this.layoutDoc.presStatus = PresStatus.Edit;
+ this.layoutDoc.presentation_status = PresStatus.Edit;
}
if (this._presTimer) clearTimeout(this._presTimer);
handled = true;
@@ -1184,7 +1253,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
this.next();
if (this._presTimer) {
clearTimeout(this._presTimer);
- this.layoutDoc.presStatus = PresStatus.Manual;
+ this.layoutDoc.presentation_status = PresStatus.Manual;
}
}
handled = true;
@@ -1201,19 +1270,19 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
this.back();
if (this._presTimer) {
clearTimeout(this._presTimer);
- this.layoutDoc.presStatus = PresStatus.Manual;
+ this.layoutDoc.presentation_status = PresStatus.Manual;
}
}
handled = true;
break;
case 'Spacebar':
case ' ':
- if (this.layoutDoc.presStatus === PresStatus.Manual) this.startOrPause(true);
- else if (this.layoutDoc.presStatus === PresStatus.Autoplay) if (this._presTimer) clearTimeout(this._presTimer);
+ if (this.layoutDoc.presentation_status === PresStatus.Manual) this.startOrPause(true);
+ else if (this.layoutDoc.presentation_status === PresStatus.Autoplay) if (this._presTimer) clearTimeout(this._presTimer);
handled = true;
break;
case 'a':
- if ((e.metaKey || e.altKey) && this.layoutDoc.presStatus === 'edit') {
+ if ((e.metaKey || e.altKey) && this.layoutDoc.presentation_status === 'edit') {
this.clearSelectedArray();
this.childDocs.forEach(doc => this.addToSelectedArray(doc));
handled = true;
@@ -1236,9 +1305,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
const presCollection = DocumentManager.GetContextPath(this.activeItem).reverse().lastElement();
const dv = DocumentManager.Instance.getDocumentView(presCollection);
this.childDocs
- .filter(doc => Cast(doc.presentationTargetDoc, Doc, null))
+ .filter(doc => Cast(doc.presentation_targetDoc, Doc, null))
.forEach((doc, index) => {
- const tagDoc = Cast(doc.presentationTargetDoc, Doc, null);
+ const tagDoc = Cast(doc.presentation_targetDoc, Doc, null);
const srcContext = Cast(tagDoc.embedContainer, Doc, null);
const width = NumCast(tagDoc._width) / 10;
const height = Math.max(NumCast(tagDoc._height) / 10, 15);
@@ -1271,17 +1340,17 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
</div>
);
}
- } else if (doc.presPinView && presCollection === tagDoc && dv) {
+ } else if (doc.config_pinView && presCollection === tagDoc && dv) {
// Case B: Document is presPinView and is presCollection
- const scale: number = 1 / NumCast(doc.presViewScale);
+ const scale: number = 1 / NumCast(doc.config_viewScale);
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.presPanX) - width / 2;
- const yLoc: number = NumCast(doc.presPanY) - height / 2;
+ const xLoc: number = NumCast(doc.config_panX) - width / 2;
+ const yLoc: number = NumCast(doc.config_panY) - height / 2;
docs.push(tagDoc);
order.push(
<>
@@ -1307,15 +1376,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@computed get paths() {
let pathPoints = '';
this.childDocs.forEach((doc, index) => {
- const tagDoc = Cast(doc.presentationTargetDoc, Doc, null);
+ const tagDoc = Cast(doc.presentation_targetDoc, Doc, null);
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) {
- const n1x = NumCast(doc.presPanX);
- const n1y = NumCast(doc.presPanY);
+ } else if (doc.config_pinView) {
+ const n1x = NumCast(doc.config_panX);
+ const n1y = NumCast(doc.config_panY);
if ((index = 0)) pathPoints = n1x + ',' + n1y;
else pathPoints = pathPoints + ' ' + n1x + ',' + n1y;
}
@@ -1339,7 +1408,7 @@ 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
+ // Converts seconds to ms and updates presentation_transition
public static SetTransitionTime = (number: String, setter: (timeInMS: number) => void, change?: number) => {
let timeInMS = Number(number) * 1000;
if (change) timeInMS += change;
@@ -1350,21 +1419,21 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@undoBatch
updateTransitionTime = (number: String, change?: number) => {
- PresBox.SetTransitionTime(number, (timeInMS: number) => this.selectedArray.forEach(doc => (doc.presTransition = timeInMS)), change);
+ PresBox.SetTransitionTime(number, (timeInMS: number) => this.selectedArray.forEach(doc => (doc.presentation_transition = timeInMS)), change);
};
- // Converts seconds to ms and updates presTransition
+ // Converts seconds to ms and updates presentation_transition
@undoBatch
updateZoom = (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.selectedArray.forEach(doc => (doc.presZoom = scale));
+ this.selectedArray.forEach(doc => (doc.config_zoom = scale));
};
/*
- * Converts seconds to ms and updates presDuration
+ * Converts seconds to ms and updates presentation_duration
*/
@undoBatch
updateDurationTime = (number: String, change?: number) => {
@@ -1372,38 +1441,38 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
if (change) timeInMS += change;
if (timeInMS < 100) timeInMS = 100;
if (timeInMS > 20000) timeInMS = 20000;
- this.selectedArray.forEach(doc => (doc.presDuration = timeInMS));
+ this.selectedArray.forEach(doc => (doc.presentation_duration = timeInMS));
};
@undoBatch
- updateMovement = action((movement: PresMovement, all?: boolean) => (all ? this.childDocs : this.selectedArray).forEach(doc => (doc.presMovement = movement)));
+ updateMovement = action((movement: PresMovement, all?: boolean) => (all ? this.childDocs : this.selectedArray).forEach(doc => (doc.presentation_movement = movement)));
@undoBatch
@action
updateHideBefore = (activeItem: Doc) => {
- activeItem.presHideBefore = !activeItem.presHideBefore;
- this.selectedArray.forEach(doc => (doc.presHideBefore = activeItem.presHideBefore));
+ activeItem.presentation_hideBefore = !activeItem.presentation_hideBefore;
+ this.selectedArray.forEach(doc => (doc.presentation_hideBefore = activeItem.presentation_hideBefore));
};
@undoBatch
@action
updateHide = (activeItem: Doc) => {
- activeItem.presHide = !activeItem.presHide;
- this.selectedArray.forEach(doc => (doc.presHide = activeItem.presHide));
+ activeItem.presentation_hide = !activeItem.presentation_hide;
+ this.selectedArray.forEach(doc => (doc.presentation_hide = activeItem.presentation_hide));
};
@undoBatch
@action
updateHideAfter = (activeItem: Doc) => {
- activeItem.presHideAfter = !activeItem.presHideAfter;
- this.selectedArray.forEach(doc => (doc.presHideAfter = activeItem.presHideAfter));
+ activeItem.presentation_hideAfter = !activeItem.presentation_hideAfter;
+ this.selectedArray.forEach(doc => (doc.presentation_hideAfter = activeItem.presentation_hideAfter));
};
@undoBatch
@action
updateOpenDoc = (activeItem: Doc) => {
- activeItem.presOpenInLightbox = !activeItem.presOpenInLightbox;
- this.selectedArray.forEach(doc => (doc.presOpenInLightbox = activeItem.presOpenInLightbox));
+ activeItem.presentation_openInLightbox = !activeItem.presentation_openInLightbox;
+ this.selectedArray.forEach(doc => (doc.presentation_openInLightbox = activeItem.presentation_openInLightbox));
};
@undoBatch
@@ -1415,11 +1484,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@undoBatch
@action
- updateEffectDirection = (effect: PresEffectDirection, all?: boolean) => (all ? this.childDocs : this.selectedArray).forEach(doc => (doc.presEffectDirection = effect));
+ updateEffectDirection = (effect: PresEffectDirection, all?: boolean) => (all ? this.childDocs : this.selectedArray).forEach(doc => (doc.presentation_effectDirection = effect));
@undoBatch
@action
- updateEffect = (effect: PresEffect, bullet: boolean, all?: boolean) => (all ? this.childDocs : this.selectedArray).forEach(doc => (bullet ? (doc.presBulletEffect = effect) : (doc.presEffect = effect)));
+ updateEffect = (effect: PresEffect, bullet: boolean, all?: boolean) => (all ? this.childDocs : this.selectedArray).forEach(doc => (bullet ? (doc.presBulletEffect = effect) : (doc.presentation_effect = effect)));
static _sliderBatch: any;
public static inputter = (min: string, step: string, max: string, value: number, active: boolean, change: (val: string) => void, hmargin?: number) => {
@@ -1431,7 +1500,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
max={max}
value={value}
readOnly={true}
- style={{ marginLeft: hmargin, marginRight: hmargin, width: `calc(100% - ${2 * (hmargin ?? 0)}px)` }}
+ style={{ marginLeft: hmargin, marginRight: hmargin, width: `calc(100% - ${2 * (hmargin ?? 0)}px)`, background: SettingsManager.userColor, color: SettingsManager.userVariantColor }}
className={`toolbar-slider ${active ? '' : 'none'}`}
onPointerDown={e => {
PresBox._sliderBatch = UndoManager.StartBatch('pres slider');
@@ -1449,52 +1518,71 @@ 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, false, true);
+ this.updateMovement(this.activeItem.presentation_movement as PresMovement, true);
+ this.updateEffect(this.activeItem.presentation_effect 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;
+ this.updateEffectDirection(this.activeItem.presentation_effectDirection as PresEffectDirection, true);
+ const { presentation_transition, presentation_duration, presentation_hideBefore, presentation_hideAfter } = this.activeItem;
array.forEach(curDoc => {
- curDoc.presTransition = presTransition;
- curDoc.presDuration = presDuration;
- curDoc.presHideBefore = presHideBefore;
- curDoc.presHideAfter = presHideAfter;
+ curDoc.presentation_transition = presentation_transition;
+ curDoc.presentation_duration = presentation_duration;
+ curDoc.presentation_hideBefore = presentation_hideBefore;
+ curDoc.presentation_hideAfter = presentation_hideAfter;
});
};
- @computed get visibiltyDurationDropdown() {
+ @computed get visibilityDurationDropdown() {
const activeItem = this.activeItem;
if (activeItem && this.targetDoc) {
const targetType = this.targetDoc.type;
- let duration = activeItem.presDuration ? NumCast(activeItem.presDuration) / 1000 : 0;
+ let duration = activeItem.presentation_duration ? NumCast(activeItem.presentation_duration) / 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)}>
+ <Tooltip title={<div className="dash-tooltip">Hide before presented</div>}>
+ <div
+ className={`ribbon-toggle ${activeItem.presentation_hideBefore ? 'active' : ''}`}
+ style={{ border: `solid 1px ${SettingsManager.userColor}`, color: SettingsManager.userColor, background: activeItem.presentation_hideBefore ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor }}
+ 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)}>
+ <div
+ className={`ribbon-toggle ${activeItem.presentation_hide ? 'active' : ''}`}
+ style={{ border: `solid 1px ${SettingsManager.userColor}`, color: SettingsManager.userColor, background: activeItem.presentation_hide ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor }}
+ onClick={() => this.updateHide(activeItem)}>
Hide
</div>
</Tooltip>
<Tooltip title={<div className="dash-tooltip">{'Hide after presented'}</div>}>
- <div className={`ribbon-toggle ${activeItem.presHideAfter ? 'active' : ''}`} onClick={() => this.updateHideAfter(activeItem)}>
+ <div
+ className={`ribbon-toggle ${activeItem.presentation_hideAfter ? 'active' : ''}`}
+ style={{ border: `solid 1px ${SettingsManager.userColor}`, color: SettingsManager.userColor, background: activeItem.presentation_hideAfter ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor }}
+ 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)}>
+ <div
+ className="ribbon-toggle"
+ style={{
+ border: `solid 1px ${SettingsManager.userColor}`,
+ color: SettingsManager.userColor,
+ background: activeItem.presentation_openInLightbox ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor,
+ }}
+ 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)}>
+ <Tooltip title={<div className="dash-tooltip">Transition movement style</div>}>
+ <div
+ className="ribbon-toggle"
+ style={{ border: `solid 1px ${SettingsManager.userColor}`, color: SettingsManager.userColor, background: activeItem.presEaseFunc === 'ease' ? SettingsManager.userVariantColor : SettingsManager.userBackgroundColor }}
+ onClick={() => this.updateEaseFunc(activeItem)}>
{`${StrCast(activeItem.presEaseFunc, 'ease')}`}
</div>
</Tooltip>
@@ -1503,10 +1591,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
<>
<div className="ribbon-doubleButton">
<div className="presBox-subheading">Slide Duration</div>
- <div className="ribbon-property">
+ <div className="ribbon-property" style={{ border: `solid 1px ${SettingsManager.userColor}` }}>
<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-propertyUpDown" style={{ color: SettingsManager.userBackgroundColor, background: SettingsManager.userColor }}>
<div className="ribbon-propertyUpDownItem" onClick={() => this.updateDurationTime(String(duration), 1000)}>
<FontAwesomeIcon icon={'caret-up'} />
</div>
@@ -1532,7 +1620,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
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)}>
+ <div
+ className={`presBox-dropdownOption ${activeItem.presentation_effect === effect || (effect === PresEffect.None && !activeItem.presentation_effect) ? 'active' : ''}`}
+ onPointerDown={StopEvent}
+ onClick={() => this.updateEffect(effect, true)}>
{effect}
</div>
);
@@ -1542,32 +1633,44 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
<div className="presBox-subheading">Progressivize Collection</div>
<input
className="presBox-checkbox"
- style={{ margin: 10 }}
+ style={{ margin: 10, border: `solid 1px ${SettingsManager.userColor}` }}
type="checkbox"
onChange={() => {
- activeItem.presIndexed = activeItem.presIndexed === undefined ? 0 : undefined;
- activeItem.presHideBefore = activeItem.presIndexed !== undefined;
+ activeItem.presentation_indexed = activeItem.presentation_indexed === undefined ? 0 : undefined;
+ activeItem.presentation_hideBefore = activeItem.presentation_indexed !== undefined;
const tagDoc = PresBox.targetRenderedDoc(this.activeItem);
const type = DocCast(tagDoc?.annotationOn)?.type ?? tagDoc.type;
- activeItem.presIndexedStart = type === DocumentType.COL ? 1 : 0;
+ activeItem.presentation_indexedStart = 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}"]`);
+ if (DocCast(activeItem.presentation_targetDoc).annotationOn) activeItem.data = ComputedField.MakeFunction(`self.presentation_targetDoc.annotationOn["${dataField}"]`);
+ else activeItem.data = ComputedField.MakeFunction(`self.presentation_targetDoc["${dataField}"]`);
}}
- checked={Cast(activeItem.presIndexed, 'number', null) !== undefined ? true : false}
+ checked={Cast(activeItem.presentation_indexed, '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)} />
+ <input
+ className="presBox-checkbox"
+ style={{ margin: 10, border: `solid 1px ${SettingsManager.userColor}` }}
+ type="checkbox"
+ onChange={() => (activeItem.presentation_indexedStart = activeItem.presentation_indexedStart ? 0 : 1)}
+ checked={!NumCast(activeItem.presentation_indexedStart)}
+ />
</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)} />
+ <input
+ className="presBox-checkbox"
+ style={{ margin: 10, border: `solid 1px ${SettingsManager.userColor}` }}
+ type="checkbox"
+ onChange={() => (activeItem.presBulletExpand = !activeItem.presBulletExpand)}
+ checked={BoolCast(activeItem.presBulletExpand)}
+ />
</div>
<div className="ribbon-box">
@@ -1578,10 +1681,18 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
e.stopPropagation();
this._openBulletEffectDropdown = !this._openBulletEffectDropdown;
})}
- style={{ borderBottomLeftRadius: this._openBulletEffectDropdown ? 0 : 5, border: this._openBulletEffectDropdown ? `solid 2px ${Colors.MEDIUM_BLUE}` : 'solid 1px black' }}>
+ style={{
+ color: SettingsManager.userColor,
+ background: SettingsManager.userVariantColor,
+ borderBottomLeftRadius: this._openBulletEffectDropdown ? 0 : 5,
+ border: this._openBulletEffectDropdown ? `solid 2px ${SettingsManager.userVariantColor}` : `solid 1px ${SettingsManager.userColor}`,
+ }}>
{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()}>
+ <div
+ className={'presBox-dropdownOptions'}
+ style={{ display: this._openBulletEffectDropdown ? 'grid' : 'none', color: SettingsManager.userColor, background: SettingsManager.userBackgroundColor }}
+ onPointerDown={e => e.stopPropagation()}>
{bulletEffect(PresEffect.None)}
{bulletEffect(PresEffect.Fade)}
{bulletEffect(PresEffect.Flip)}
@@ -1598,18 +1709,21 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
@computed get transitionDropdown() {
const activeItem = this.activeItem;
- const presEffect = (effect: PresEffect) => (
- <div className={`presBox-dropdownOption ${activeItem.presEffect === effect || (effect === PresEffect.None && !activeItem.presEffect) ? 'active' : ''}`} onPointerDown={StopEvent} onClick={() => this.updateEffect(effect, false)}>
+ const preseEffect = (effect: PresEffect) => (
+ <div
+ className={`presBox-dropdownOption ${activeItem.presentation_effect === effect || (effect === PresEffect.None && !activeItem.presentation_effect) ? 'active' : ''}`}
+ onPointerDown={StopEvent}
+ onClick={() => this.updateEffect(effect, false)}>
{effect}
</div>
);
const presMovement = (movement: PresMovement) => (
- <div className={`presBox-dropdownOption ${activeItem.presMovement === movement ? 'active' : ''}`} onPointerDown={StopEvent} onClick={() => this.updateMovement(movement)}>
+ <div className={`presBox-dropdownOption ${activeItem.presentation_movement === movement ? 'active' : ''}`} onPointerDown={StopEvent} onClick={() => this.updateMovement(movement)}>
{movement}
</div>
);
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';
+ const color = activeItem.presentation_effectDirection === direction || (direction === PresEffectDirection.Center && !activeItem.presentation_effectDirection) ? SettingsManager.userVariantColor : SettingsManager.userColor;
return (
<Tooltip title={<div className="dash-tooltip">{direction}</div>}>
<div
@@ -1621,12 +1735,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
);
};
if (activeItem && this.targetDoc) {
- const transitionSpeed = activeItem.presTransition ? NumCast(activeItem.presTransition) / 1000 : 0.5;
- const zoom = NumCast(activeItem.presZoom, 1) * 100;
- const effect = activeItem.presEffect ? activeItem.presEffect : PresMovement.None;
+ const transitionSpeed = activeItem.presentation_transition ? NumCast(activeItem.presentation_transition) / 1000 : 0.5;
+ const zoom = NumCast(activeItem.config_zoom, 1) * 100;
+ const effect = activeItem.presentation_effect ? activeItem.presentation_effect : PresMovement.None;
return (
<div
- className={`presBox-ribbon ${this._transitionTools && this.layoutDoc.presStatus === PresStatus.Edit ? 'active' : ''}`}
+ className={`presBox-ribbon ${this._transitionTools && this.layoutDoc.presentation_status === PresStatus.Edit ? 'active' : ''}`}
onPointerDown={StopEvent}
onPointerUp={StopEvent}
onClick={action(e => {
@@ -1643,10 +1757,23 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
e.stopPropagation();
this._openMovementDropdown = !this._openMovementDropdown;
})}
- style={{ borderBottomLeftRadius: this._openMovementDropdown ? 0 : 5, border: this._openMovementDropdown ? `solid 2px ${Colors.MEDIUM_BLUE}` : 'solid 1px black' }}>
+ style={{
+ color: SettingsManager.userColor,
+ background: SettingsManager.userVariantColor,
+ borderBottomLeftRadius: this._openMovementDropdown ? 0 : 5,
+ border: this._openMovementDropdown ? `solid 2px ${SettingsManager.userVariantColor}` : `solid 1px ${SettingsManager.userColor}`,
+ }}>
{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' }}>
+ <div
+ className="presBox-dropdownOptions"
+ id={'presBoxMovementDropdown'}
+ onPointerDown={StopEvent}
+ style={{
+ color: SettingsManager.userColor,
+ background: SettingsManager.userBackgroundColor,
+ display: this._openMovementDropdown ? 'grid' : 'none',
+ }}>
{presMovement(PresMovement.None)}
{presMovement(PresMovement.Center)}
{presMovement(PresMovement.Zoom)}
@@ -1654,12 +1781,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
{presMovement(PresMovement.Jump)}
</div>
</div>
- <div className="ribbon-doubleButton" style={{ display: activeItem.presMovement === PresMovement.Zoom ? 'inline-flex' : 'none' }}>
+ <div className="ribbon-doubleButton" style={{ display: activeItem.presentation_movement === PresMovement.Zoom ? 'inline-flex' : 'none' }}>
<div className="presBox-subheading">Zoom (% screen filled)</div>
- <div className="ribbon-property">
+ <div className="ribbon-property" style={{ border: `solid 1px ${SettingsManager.userColor}` }}>
<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-propertyUpDown" style={{ color: SettingsManager.userBackgroundColor, background: SettingsManager.userColor }}>
<div className="ribbon-propertyUpDownItem" onClick={() => this.updateZoom(String(zoom), 0.1)}>
<FontAwesomeIcon icon={'caret-up'} />
</div>
@@ -1668,13 +1795,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
</div>
</div>
</div>
- {PresBox.inputter('0', '1', '100', zoom, activeItem.presMovement === PresMovement.Zoom, this.updateZoom)}
+ {PresBox.inputter('0', '1', '100', zoom, activeItem.presentation_movement === PresMovement.Zoom, this.updateZoom)}
<div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}>
<div className="presBox-subheading">Transition Time</div>
- <div className="ribbon-property">
+ <div className="ribbon-property" style={{ border: `solid 1px ${SettingsManager.userColor}` }}>
<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-propertyUpDown" style={{ color: SettingsManager.userBackgroundColor, background: SettingsManager.userColor }}>
<div className="ribbon-propertyUpDownItem" onClick={() => this.updateTransitionTime(String(transitionSpeed), 1000)}>
<FontAwesomeIcon icon={'caret-up'} />
</div>
@@ -1694,11 +1821,23 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
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)} />
+ <input
+ className="presBox-checkbox"
+ style={{ margin: 10, border: `solid 1px ${SettingsManager.userColor}` }}
+ type="checkbox"
+ onChange={() => (activeItem.presPlayAudio = !BoolCast(activeItem.presPlayAudio))}
+ checked={BoolCast(activeItem.presPlayAudio)}
+ />
</div>
<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)} />
+ <input
+ className="presBox-checkbox"
+ style={{ margin: 10, border: `solid 1px ${SettingsManager.userColor}` }}
+ type="checkbox"
+ onChange={() => (activeItem.presentation_zoomText = !BoolCast(activeItem.presentation_zoomText))}
+ checked={BoolCast(activeItem.presentation_zoomText)}
+ />
</div>
<div
className="presBox-dropdown"
@@ -1706,21 +1845,36 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
e.stopPropagation();
this._openEffectDropdown = !this._openEffectDropdown;
})}
- style={{ borderBottomLeftRadius: this._openEffectDropdown ? 0 : 5, border: this._openEffectDropdown ? `solid 2px ${Colors.MEDIUM_BLUE}` : 'solid 1px black' }}>
+ style={{
+ color: SettingsManager.userColor,
+ background: SettingsManager.userVariantColor,
+ borderBottomLeftRadius: this._openEffectDropdown ? 0 : 5,
+ border: this._openEffectDropdown ? `solid 2px ${SettingsManager.userVariantColor}` : `solid 1px ${SettingsManager.userColor}`,
+ }}>
{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
+ className="presBox-dropdownOptions"
+ id={'presBoxMovementDropdown'}
+ style={{
+ color: SettingsManager.userColor,
+ background: SettingsManager.userBackgroundColor,
+ display: this._openEffectDropdown ? 'grid' : 'none',
+ }}
+ onPointerDown={e => e.stopPropagation()}>
+ {preseEffect(PresEffect.None)}
+ {preseEffect(PresEffect.Fade)}
+ {preseEffect(PresEffect.Flip)}
+ {preseEffect(PresEffect.Rotate)}
+ {preseEffect(PresEffect.Bounce)}
+ {preseEffect(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 className="ribbon-property" style={{ border: `solid 1px ${SettingsManager.userColor}` }}>
+ {StrCast(this.activeItem.presentation_effectDirection)}
+ </div>
</div>
<div className="effectDirection" style={{ display: effect === PresEffectDirection.None ? 'none' : 'grid', width: 40 }}>
{presDirection(PresEffectDirection.Left, 'angle-right', 1, 2, {})}
@@ -1742,8 +1896,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@computed get mediaOptionsDropdown() {
const activeItem = this.activeItem;
if (activeItem && this.targetDoc) {
- const clipStart = NumCast(activeItem.clipStart);
- const clipEnd = NumCast(activeItem.clipEnd, NumCast(activeItem[Doc.LayoutFieldKey(activeItem) + '_duration']));
+ const renderTarget = PresBox.targetRenderedDoc(this.activeItem);
+ const clipStart = NumCast(renderTarget.clipStart);
+ const clipEnd = NumCast(renderTarget.clipEnd, clipStart + NumCast(renderTarget[Doc.LayoutFieldKey(renderTarget) + '_duration']));
+ const config_clipEnd = NumCast(activeItem.config_clipEnd) < NumCast(activeItem.config_clipStart) ? clipEnd - clipStart : NumCast(activeItem.config_clipEnd);
return (
<div className={'presBox-ribbon'} onClick={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}>
<div>
@@ -1754,17 +1910,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
<div className="slider-text" style={{ fontWeight: 500 }}>
Start time (s)
</div>
- <div id={'startTime'} className="slider-number" style={{ backgroundColor: Colors.LIGHT_GRAY }}>
+ <div id="startTime" className="slider-number" style={{ color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }}>
<input
className="presBox-input"
style={{ textAlign: 'center', width: '100%', height: 15, fontSize: 10 }}
type="number"
readOnly={true}
- value={NumCast(activeItem.presStartTime).toFixed(2)}
+ value={NumCast(activeItem.config_clipStart).toFixed(2)}
onKeyDown={e => e.stopPropagation()}
- onChange={action((e: React.ChangeEvent<HTMLInputElement>) => {
- activeItem.presStartTime = Number(e.target.value);
- })}
+ onChange={action(e => (activeItem.config_clipStart = Number(e.target.value)))}
/>
</div>
</div>
@@ -1772,25 +1926,23 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
<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 className="slider-number" style={{ color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }}>
+ {Math.round((config_clipEnd - NumCast(activeItem.config_clipStart)) * 10) / 10}
</div>
</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 }}>
+ <div id="endTime" className="slider-number" style={{ color: SettingsManager.userColor, backgroundColor: SettingsManager.userBackgroundColor }}>
<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);
- })}
+ value={config_clipEnd.toFixed(2)}
+ onChange={action(e => (activeItem.config_clipEnd = Number(e.target.value)))}
/>
</div>
</div>
@@ -1801,16 +1953,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
step="0.1"
min={clipStart}
max={clipEnd}
- value={NumCast(activeItem.presEndTime)}
- style={{ gridColumn: 1, gridRow: 1 }}
+ value={config_clipEnd}
+ style={{ gridColumn: 1, gridRow: 1, background: SettingsManager.userColor, color: SettingsManager.userVariantColor }}
className={`toolbar-slider ${'end'}`}
id="toolbar-slider"
onPointerDown={e => {
- this._batch = UndoManager.StartBatch('presEndTime');
+ this._batch = UndoManager.StartBatch('config_clipEnd');
const endBlock = document.getElementById('endTime');
if (endBlock) {
- endBlock.style.color = Colors.LIGHT_GRAY;
- endBlock.style.backgroundColor = Colors.MEDIUM_BLUE;
+ endBlock.style.backgroundColor = SettingsManager.userVariantColor;
}
e.stopPropagation();
}}
@@ -1818,13 +1969,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
this._batch?.end();
const endBlock = document.getElementById('endTime');
if (endBlock) {
- endBlock.style.color = Colors.BLACK;
- endBlock.style.backgroundColor = Colors.LIGHT_GRAY;
+ endBlock.style.backgroundColor = SettingsManager.userBackgroundColor;
}
}}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
e.stopPropagation();
- activeItem.presEndTime = Number(e.target.value);
+ activeItem.config_clipEnd = Number(e.target.value);
}}
/>
<input
@@ -1832,16 +1982,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
step="0.1"
min={clipStart}
max={clipEnd}
- value={NumCast(activeItem.presStartTime)}
+ value={NumCast(activeItem.config_clipStart)}
style={{ gridColumn: 1, gridRow: 1 }}
className={`toolbar-slider ${'start'}`}
id="toolbar-slider"
onPointerDown={e => {
- this._batch = UndoManager.StartBatch('presStartTime');
+ this._batch = UndoManager.StartBatch('config_clipStart');
const startBlock = document.getElementById('startTime');
if (startBlock) {
- startBlock.style.color = Colors.LIGHT_GRAY;
- startBlock.style.backgroundColor = Colors.MEDIUM_BLUE;
+ startBlock.style.backgroundColor = SettingsManager.userVariantColor;
}
e.stopPropagation();
}}
@@ -1849,13 +1998,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
this._batch?.end();
const startBlock = document.getElementById('startTime');
if (startBlock) {
- startBlock.style.color = Colors.BLACK;
- startBlock.style.backgroundColor = Colors.LIGHT_GRAY;
+ startBlock.style.backgroundColor = SettingsManager.userBackgroundColor;
}
}}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
e.stopPropagation();
- activeItem.presStartTime = Number(e.target.value);
+ activeItem.config_clipStart = Number(e.target.value);
}}
/>
</div>
@@ -1870,22 +2018,46 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
<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'} />
+ <input
+ className="presBox-checkbox"
+ type="checkbox"
+ style={{ border: `solid 1px ${SettingsManager.userColor}` }}
+ onChange={() => (activeItem.presentation_mediaStart = 'manual')}
+ checked={activeItem.presentation_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'} />
+ <input
+ className="presBox-checkbox"
+ style={{ border: `solid 1px ${SettingsManager.userColor}` }}
+ type="checkbox"
+ onChange={() => (activeItem.presentation_mediaStart = 'auto')}
+ checked={activeItem.presentation_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>
+ <input
+ className="presBox-checkbox"
+ type="checkbox"
+ style={{ border: `solid 1px ${SettingsManager.userColor}` }}
+ onChange={() => (activeItem.presentation_mediaStop = 'manual')}
+ checked={activeItem.presentation_mediaStop === 'manual'}
+ />
+ <div>At media end time</div>
</div>
<div className="checkbox-container">
- <input className="presBox-checkbox" type="checkbox" onChange={() => (activeItem.mediaStop = 'auto')} checked={activeItem.mediaStop === 'auto'} />
+ <input
+ className="presBox-checkbox"
+ type="checkbox"
+ style={{ border: `solid 1px ${SettingsManager.userColor}` }}
+ onChange={() => (activeItem.presentation_mediaStop = 'auto')}
+ checked={activeItem.presentation_mediaStop === 'auto'}
+ />
<div>On slide change</div>
</div>
{/* <div className="checkbox-container">
@@ -1916,7 +2088,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
return (
<div
className="presBox-toolbar-dropdown"
- style={{ display: this._newDocumentTools && this.layoutDoc.presStatus === 'edit' ? 'inline-flex' : 'none' }}
+ style={{ display: this._newDocumentTools && this.layoutDoc.presentation_status === 'edit' ? 'inline-flex' : 'none' }}
onClick={e => e.stopPropagation()}
onPointerUp={e => e.stopPropagation()}
onPointerDown={e => e.stopPropagation()}>
@@ -2053,10 +2225,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
if (doc) {
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 presCollection = DocumentManager.GetContextPath(this.activeItem).reverse().lastElement().presentation_targetDoc ?? tab;
const data = Cast(presCollection?.data, listSpec(Doc));
- const presData = Cast(this.rootDoc.data, listSpec(Doc));
- if (data && presData) {
+ const config_data = Cast(this.rootDoc.data, listSpec(Doc));
+ if (data && config_data) {
data.push(doc);
TabDocView.PinDoc(doc, {});
this.gotoDocument(this.childDocs.length, this.activeItem);
@@ -2105,7 +2277,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
className="dropdown-play-button"
onClick={undoBatch(
action(() => {
- this.layoutDoc.presStatus = 'manual';
+ this.layoutDoc.presentation_status = 'manual';
this.initializePresState(this.itemIndex);
this.turnOffEdit(true);
this.gotoDocument(this.itemIndex, this.activeItem);
@@ -2133,8 +2305,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
const propTitle = SettingsManager.propertiesWidth > 0 ? 'Close Presentation Panel' : 'Open Presentation Panel';
const mode = StrCast(this.rootDoc._type_collection) as CollectionViewType;
const isMini: boolean = this.toolbarWidth <= 100;
- const activeColor = Colors.LIGHT_BLUE;
- const inactiveColor = Colors.WHITE;
+ const activeColor = SettingsManager.userVariantColor;
+ const inactiveColor = lightOrDark(SettingsManager.userBackgroundColor) === Colors.WHITE ? Colors.WHITE : SettingsManager.userBackgroundColor;
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)}>
@@ -2144,7 +2316,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
<Tooltip title={<div className="dash-tooltip">View paths</div>}>
<div
style={{ opacity: this.childDocs.length > 1 ? 1 : 0.3, color: this._pathBoolean ? Colors.MEDIUM_BLUE : 'white', width: isMini ? '100%' : undefined }}
- className={'toolbar-button'}
+ className="toolbar-button"
onClick={this.childDocs.length > 1 ? () => this.togglePath() : undefined}>
<FontAwesomeIcon icon={'exchange-alt'} />
</div>
@@ -2181,7 +2353,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
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}>
+ <select className="presBox-viewPicker" style={{ display: this.layoutDoc.presentation_status === 'edit' ? 'block' : 'none' }} onPointerDown={e => e.stopPropagation()} onChange={this.viewChanged} value={mode}>
<option onPointerDown={StopEvent} value={CollectionViewType.Stacking}>
List
</option>
@@ -2196,12 +2368,12 @@ 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 === PresStatus.Edit ? 'present' : ''}`}>
+ <span className={`presBox-button ${this.layoutDoc.presentation_status === PresStatus.Edit ? 'present' : ''}`}>
<div
className="presBox-button-left"
onClick={undoBatch(() => {
if (this.childDocs.length) {
- this.layoutDoc.presStatus = 'manual';
+ this.layoutDoc.presentation_status = 'manual';
this.initializePresState(this.itemIndex);
this.gotoDocument(this.itemIndex, this.activeItem);
}
@@ -2227,12 +2399,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
@computed get playButtons() {
- const presEnd = !this.layoutDoc.presLoop && this.itemIndex === this.childDocs.length - 1 && (this.activeItem.presIndexed === undefined || NumCast(this.activeItem.presIndexed) === (this.progressivizedItems(this.activeItem)?.length ?? 0));
+ const presEnd =
+ !this.layoutDoc.presLoop &&
+ this.itemIndex === this.childDocs.length - 1 &&
+ (this.activeItem.presentation_indexed === undefined || NumCast(this.activeItem.presentation_indexed) === (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' }}>
+ <div className="presPanelOverlay" style={{ display: this.layoutDoc.presentation_status !== 'edit' ? 'inline-flex' : 'none' }}>
<Tooltip title={<div className="dash-tooltip">{'Loop'}</div>}>
<div
className="presPanel-button"
@@ -2255,7 +2430,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
this.back();
if (this._presTimer) {
clearTimeout(this._presTimer);
- this.layoutDoc.presStatus = PresStatus.Manual;
+ this.layoutDoc.presentation_status = PresStatus.Manual;
}
e.stopPropagation();
},
@@ -2265,9 +2440,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
}>
<FontAwesomeIcon icon={'arrow-left'} />
</div>
- <Tooltip title={<div className="dash-tooltip">{this.layoutDoc.presStatus === PresStatus.Autoplay ? 'Pause' : 'Autoplay'}</div>}>
+ <Tooltip title={<div className="dash-tooltip">{this.layoutDoc.presentation_status === PresStatus.Autoplay ? 'Pause' : 'Autoplay'}</div>}>
<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'} />
+ <FontAwesomeIcon color={this.layoutDoc.presentation_status === PresStatus.Autoplay ? 'red' : undefined} icon={this.layoutDoc.presentation_status === PresStatus.Autoplay ? 'pause' : 'play'} />
</div>
</Tooltip>
<div
@@ -2283,7 +2458,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
this.next();
if (this._presTimer) {
clearTimeout(this._presTimer);
- this.layoutDoc.presStatus = PresStatus.Manual;
+ this.layoutDoc.presentation_status = PresStatus.Manual;
}
e.stopPropagation();
},
@@ -2316,7 +2491,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
</Tooltip>
<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}
+ {this.activeItem?.presentation_indexed !== undefined ? `(${this.activeItem.presentation_indexed}/${this.progressivizedItems(this.activeItem)?.length})` : ''} / {this.childDocs.length}
</div>
<div className="presPanel-divider"></div>
{this.props.PanelWidth() > 250 ? (
@@ -2324,7 +2499,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
className="presPanel-button-text"
onClick={undoBatch(
action(() => {
- this.layoutDoc.presStatus = PresStatus.Edit;
+ this.layoutDoc.presentation_status = PresStatus.Edit;
clearTimeout(this._presTimer);
})
)}>
@@ -2342,7 +2517,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
@action
startOrPause = (makeActive = true) => {
makeActive && this.updateCurrentPresentation();
- if (!this.layoutDoc.presStatus || this.layoutDoc.presStatus === PresStatus.Manual || this.layoutDoc.presStatus === PresStatus.Edit) this.startPresentation(this.itemIndex);
+ if (!this.layoutDoc.presentation_status || this.layoutDoc.presentation_status === PresStatus.Manual || this.layoutDoc.presentation_status === PresStatus.Edit) this.startPresentation(this.itemIndex);
else this.pauseAutoPres();
};
@@ -2351,7 +2526,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
this.back();
if (this._presTimer) {
clearTimeout(this._presTimer);
- this.layoutDoc.presStatus = PresStatus.Manual;
+ this.layoutDoc.presentation_status = PresStatus.Manual;
}
};
@@ -2360,19 +2535,19 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
this.next();
if (this._presTimer) {
clearTimeout(this._presTimer);
- this.layoutDoc.presStatus = PresStatus.Manual;
+ this.layoutDoc.presentation_status = PresStatus.Manual;
}
};
@undoBatch
@action
exitClicked = () => {
- this.layoutDoc.presStatus = this._exitTrail?.() ?? this.exitMinimize();
+ this.layoutDoc.presentation_status = this._exitTrail?.() ?? this.exitMinimize();
clearTimeout(this._presTimer);
};
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.
+ if (!treeViewDoc.presentation_targetDoc) 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;
@@ -2388,20 +2563,23 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
};
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 (!treeViewDoc.presentation_targetDoc) 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));
}
};
- 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]);
+ sort = (treeView_Map: Map<Doc, number>) => [...treeView_Map.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._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 presEnd =
+ !this.layoutDoc.presLoop &&
+ this.itemIndex === this.childDocs.length - 1 &&
+ (this.activeItem.presentation_indexed === undefined || NumCast(this.activeItem.presentation_indexed) === (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))}>
@@ -2420,9 +2598,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
<div className="presPanel-button" style={{ opacity: presStart ? 0.4 : 1 }} onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, returnFalse, this.prevClicked, false, false)}>
<FontAwesomeIcon icon={'arrow-left'} />
</div>
- <Tooltip title={<div className="dash-tooltip">{this.layoutDoc.presStatus === PresStatus.Autoplay ? 'Pause' : 'Autoplay'}</div>}>
+ <Tooltip title={<div className="dash-tooltip">{this.layoutDoc.presentation_status === PresStatus.Autoplay ? 'Pause' : 'Autoplay'}</div>}>
<div className="presPanel-button" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, returnFalse, () => this.startOrPause(true), false, false)}>
- <FontAwesomeIcon icon={this.layoutDoc.presStatus === 'auto' ? 'pause' : 'play'} />
+ <FontAwesomeIcon icon={this.layoutDoc.presentation_status === 'auto' ? 'pause' : 'play'} />
</div>
</Tooltip>
<div className="presPanel-button" style={{ opacity: presEnd ? 0.4 : 1 }} onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, returnFalse, this.nextClicked, false, false)}>
@@ -2436,7 +2614,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
</Tooltip>
<div className="presPanel-button-text">
Slide {this.itemIndex + 1}
- {this.activeItem?.presIndexed !== undefined ? `(${this.activeItem.presIndexed}/${this.progressivizedItems(this.activeItem)?.length})` : ''} / {this.childDocs.length}
+ {this.activeItem?.presentation_indexed !== undefined ? `(${this.activeItem.presentation_indexed}/${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)}>
@@ -2460,6 +2638,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
moveDocument={returnFalse}
ignoreUnrendered={true}
childDragAction="move"
+ setContentView={emptyFunction}
//childLayoutFitWidth={returnTrue}
childOpacity={returnOne}
childClickScript={PresBox.navigateToDocScript}
diff --git a/src/client/views/nodes/trails/PresElementBox.scss b/src/client/views/nodes/trails/PresElementBox.scss
index 4f95f0c1f..9ac2b5a94 100644
--- a/src/client/views/nodes/trails/PresElementBox.scss
+++ b/src/client/views/nodes/trails/PresElementBox.scss
@@ -4,6 +4,10 @@ $light-background: #ececec;
$slide-background: #d5dce2;
$slide-active: #5b9fdd;
+.testingv2 {
+ background-color: red;
+}
+
.presItem-container {
cursor: grab;
display: flex;
diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx
index f31cf6147..82ed9e8d5 100644
--- a/src/client/views/nodes/trails/PresElementBox.tsx
+++ b/src/client/views/nodes/trails/PresElementBox.tsx
@@ -6,7 +6,7 @@ import { Doc, DocListCast, Opt } from '../../../../fields/Doc';
import { Height, Width } from '../../../../fields/DocSymbols';
import { Id } from '../../../../fields/FieldSymbols';
import { List } from '../../../../fields/List';
-import { Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types';
+import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types';
import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents } from '../../../../Utils';
import { Docs } from '../../../documents/Documents';
import { CollectionViewType } from '../../../documents/DocumentTypes';
@@ -25,6 +25,9 @@ import { PresBox } from './PresBox';
import './PresElementBox.scss';
import { PresMovement } from './PresEnums';
import React = require('react');
+import { TreeView } from '../../collections/TreeView';
+import { BranchingTrailManager } from '../../../util/BranchingTrailManager';
+import { MultiToggle, Type } from 'browndash-components';
/**
* This class models the view a document added to presentation will have in the presentation.
* It involves some functionality for its buttons and options.
@@ -58,13 +61,13 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
return this.props.DocumentView?.().props.docViewPath().lastElement()?.rootDoc;
}
@computed get targetDoc() {
- return Cast(this.rootDoc.presentationTargetDoc, Doc, null) || this.rootDoc;
+ return Cast(this.rootDoc.presentation_targetDoc, Doc, null) || this.rootDoc;
}
componentDidMount() {
this.layoutDoc.layout_hideLinkButton = true;
this._heightDisposer = reaction(
- () => ({ expand: this.rootDoc.presExpandInlineButton, height: this.collapsedHeight }),
+ () => ({ expand: this.rootDoc.presentation_expandInlineButton, height: this.collapsedHeight }),
({ expand, height }) => (this.layoutDoc._height = height + (expand ? this.expandViewHeight : 0)),
{ fireImmediately: true }
);
@@ -79,7 +82,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
ScreenToLocalListTransform = (xCord: number, yCord: number) => [xCord, yCord];
@action
- presExpandDocumentClick = () => (this.rootDoc.presExpandInlineButton = !this.rootDoc.presExpandInlineButton);
+ presExpandDocumentClick = () => (this.rootDoc.presentation_expandInlineButton = !this.rootDoc.presentation_expandInlineButton);
embedHeight = (): number => this.collapsedHeight + this.expandViewHeight;
// embedWidth = () => this.props.PanelWidth();
@@ -94,7 +97,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
* presentation element.
*/
@computed get renderEmbeddedInline() {
- return !this.rootDoc.presExpandInlineButton || !this.targetDoc ? null : (
+ return !this.rootDoc.presentation_expandInlineButton || !this.targetDoc ? null : (
<div className="presItem-embedded" style={{ height: this.embedHeight(), width: '50%' }}>
<DocumentView
Document={PresBox.targetRenderedDoc(this.rootDoc)}
@@ -158,9 +161,9 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
@computed get transition() {
let transitionInS: number;
- if (this.rootDoc.presTransition) transitionInS = NumCast(this.rootDoc.presTransition) / 1000;
+ if (this.rootDoc.presentation_transition) transitionInS = NumCast(this.rootDoc.presentation_transition) / 1000;
else transitionInS = 0.5;
- return this.rootDoc.presMovement === PresMovement.Jump || this.rootDoc.presMovement === PresMovement.None ? null : 'M: ' + transitionInS + 's';
+ return this.rootDoc.presentation_movement === PresMovement.Jump || this.rootDoc.presentation_movement === PresMovement.None ? null : 'M: ' + transitionInS + 's';
}
private _itemRef: React.RefObject<HTMLDivElement> = React.createRef();
@@ -238,7 +241,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
onPointerMove = (e: PointerEvent) => {
const slide = this._itemRef.current!;
- const dragIsPresItem = DragManager.docsBeingDragged.some(d => d.presentationTargetDoc);
+ const dragIsPresItem = DragManager.docsBeingDragged.some(d => d.presentation_targetDoc);
if (slide && dragIsPresItem) {
const rect = slide.getBoundingClientRect();
const y = e.clientY - rect.top; //y position within the element.
@@ -296,13 +299,24 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
@action
updateCapturedContainerLayout = (presTargetDoc: Doc, activeItem: Doc) => {
const targetDoc = DocCast(presTargetDoc.annotationOn) ?? presTargetDoc;
- activeItem.presX = NumCast(targetDoc.x);
- activeItem.presY = NumCast(targetDoc.y);
- activeItem.presRotation = NumCast(targetDoc.rotation);
- activeItem.presWidth = NumCast(targetDoc.width);
- activeItem.presHeight = NumCast(targetDoc.height);
- activeItem.presPinLayout = true;
+ activeItem.config_x = NumCast(targetDoc.x);
+ activeItem.config_y = NumCast(targetDoc.y);
+ activeItem.config_rotation = NumCast(targetDoc.rotation);
+ activeItem.config_width = NumCast(targetDoc.width);
+ activeItem.config_height = NumCast(targetDoc.height);
+ activeItem.config_pinLayout = !activeItem.config_pinLayout;
+ // activeItem.config_pinLayout = true;
};
+
+ //wait i dont think i have to do anything here since by default it'll revert to the previously saved if I don't save
+ //so basically, don't have an onClick for this, just let it do nada for now
+ @undoBatch
+ @action
+ revertToPreviouslySaved = (presTargetDoc: Doc, activeItem: Doc) => {
+ const target = DocCast(activeItem.annotationOn) ?? activeItem;
+ PresBox.reversePin(activeItem, target);
+ };
+
/**
* Method called for updating the view of the currently selected document
*
@@ -399,6 +413,18 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
};
+ @undoBatch
+ @action
+ lfg = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ // TODO: fix this bug
+ const { toggleChildrenRun } = this.rootDoc;
+ TreeView.ToggleChildrenRun.get(this.rootDoc)?.();
+
+ // call this.rootDoc.recurChildren() to get all the children
+ // if (iconClick) PresElementBox.showVideo = false;
+ };
+
@computed
get toolbarWidth(): number {
const presBoxDocView = DocumentManager.Instance.getDocumentView(this.presBox);
@@ -409,19 +435,21 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
}
@computed get presButtons() {
- 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 presBox = this.presBox;
+ const presBoxColor = StrCast(presBox?._backgroundColor);
+ const presColorBool = presBoxColor ? presBoxColor !== Colors.WHITE && presBoxColor !== 'transparent' : false;
+ const targetDoc = this.targetDoc;
+ const activeItem = this.rootDoc;
+ const hasChildren = BoolCast(this.rootDoc?.hasChildren);
const items: JSX.Element[] = [];
+
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' }}>
+ style={{ opacity: activeItem.config_pinLayout ? 1 : 0.5, fontWeight: 700, display: 'flex' }}>
L
</div>
</Tooltip>
@@ -431,7 +459,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
<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' }}>
+ style={{ opacity: activeItem.config_pinData || activeItem.config_pinView ? 1 : 0.5, fontWeight: 700, display: 'flex' }}>
C
</div>
</Tooltip>
@@ -445,19 +473,29 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
);
if (this.indexInPres !== 0) {
items.push(
- <Tooltip key="arrow" title={<div className="dash-tooltip">{activeItem.groupWithUp ? 'Ungroup' : 'Group with up'}</div>}>
+ <Tooltip
+ key="arrow"
+ title={
+ <div className="dash-tooltip">
+ {!activeItem.presentation_groupWithUp
+ ? 'Not grouped with previous slide (click to group)'
+ : activeItem.presentation_groupWithUp === 1
+ ? 'Run simultaneously with previous slide (click again to run after)'
+ : 'Run after previous slide (click to ungroup from previous)'}
+ </div>
+ }>
<div
className="slideButton"
- onClick={() => (activeItem.groupWithUp = (NumCast(activeItem.groupWithUp) + 1) % 3)}
+ onClick={() => (activeItem.presentation_groupWithUp = (NumCast(activeItem.presentation_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,
+ backgroundColor: activeItem.presentation_groupWithUp ? (presColorBool ? presBoxColor : Colors.MEDIUM_BLUE) : undefined,
+ outline: NumCast(activeItem.presentation_groupWithUp) > 1 ? 'solid black 1px' : undefined,
+ height: activeItem.presentation_groupWithUp ? 53 : 18,
+ transform: activeItem.presentation_groupWithUp ? 'translate(0, -17px)' : undefined,
}}>
- <div style={{ transform: activeItem.groupWithUp ? 'rotate(180deg) translate(0, -17.5px)' : 'rotate(0deg)' }}>
+ <div style={{ transform: activeItem.presentation_groupWithUp ? 'rotate(180deg) translate(0, -17.5px)' : 'rotate(0deg)' }}>
<FontAwesomeIcon icon={'arrow-up'} onPointerDown={e => e.stopPropagation()} />
</div>
</div>
@@ -465,17 +503,33 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
);
}
items.push(
- <Tooltip key="eye" title={<div className="dash-tooltip">{this.rootDoc.presExpandInlineButton ? 'Minimize' : 'Expand'}</div>}>
+ <Tooltip key="eye" title={<div className="dash-tooltip">{this.rootDoc.presentation_expandInlineButton ? 'Minimize' : 'Expand'}</div>}>
<div
className="slideButton"
onClick={e => {
e.stopPropagation();
this.presExpandDocumentClick();
}}>
- <FontAwesomeIcon icon={this.rootDoc.presExpandInlineButton ? 'eye-slash' : 'eye'} onPointerDown={e => e.stopPropagation()} />
+ <FontAwesomeIcon icon={this.rootDoc.presentation_expandInlineButton ? 'eye-slash' : 'eye'} onPointerDown={e => e.stopPropagation()} />
</div>
</Tooltip>
);
+ if (!Doc.noviceMode && hasChildren) {
+ // TODO: replace with if treeveiw, has childrenDocs
+ items.push(
+ <Tooltip key="children" title={<div className="dash-tooltip">Run child processes (tree only)</div>}>
+ <div
+ className="slideButton"
+ onClick={e => {
+ e.stopPropagation();
+ this.lfg(e);
+ }}
+ style={{ fontWeight: 700 }}>
+ <FontAwesomeIcon icon={'circle-play'} onPointerDown={e => e.stopPropagation()} />
+ </div>
+ </Tooltip>
+ );
+ }
items.push(
<Tooltip key="trash" title={<div className="dash-tooltip">Remove from presentation</div>}>
<div className={'slideButton'} onClick={this.removePresentationItem}>
@@ -522,7 +576,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
) : (
<div
ref={this._dragRef}
- className={`presItem-slide ${isCurrent ? 'active' : ''}`}
+ className={`presItem-slide ${isCurrent ? 'active' : ''}${this.rootDoc.runProcess ? ' testingv2' : ''}`}
style={{
display: 'infline-block',
backgroundColor: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor),
@@ -534,7 +588,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
style={{
display: 'inline-flex',
pointerEvents: isSelected ? undefined : 'none',
- width: `calc(100% ${this.rootDoc.presExpandInlineButton ? '- 50%' : ''} - ${this.presButtons.length * 22}px`,
+ width: `calc(100% ${this.rootDoc.presentation_expandInlineButton ? '- 50%' : ''} - ${this.presButtons.length * 22}px`,
cursor: isSelected ? 'text' : 'grab',
}}>
<div
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx
index b877cc36a..85c1cce8c 100644
--- a/src/client/views/pdf/AnchorMenu.tsx
+++ b/src/client/views/pdf/AnchorMenu.tsx
@@ -1,101 +1,37 @@
import React = require('react');
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Tooltip } from '@material-ui/core';
+import { ColorPicker, Group, IconButton, Popup, Size, Toggle, ToggleType, Type } from 'browndash-components';
import { action, computed, IReactionDisposer, observable, ObservableMap, reaction } from 'mobx';
import { observer } from 'mobx-react';
import { ColorState } from 'react-color';
import { Doc, Opt } from '../../../fields/Doc';
+import { StrCast } from '../../../fields/Types';
import { returnFalse, setupMoveUpEvents, unimplementedFunction, Utils } from '../../../Utils';
+import { gptAPICall, GPTCallType } from '../../apis/gpt/GPT';
+import { DocumentType } from '../../documents/DocumentTypes';
import { SelectionManager } from '../../util/SelectionManager';
import { AntimodeMenu, AntimodeMenuProps } from '../AntimodeMenu';
import { LinkPopup } from '../linking/LinkPopup';
-import { ButtonDropdown } from '../nodes/formattedText/RichTextMenu';
-import { gptAPICall, GPTCallType } from '../../apis/gpt/GPT';
-import { GPTPopup, GPTPopupMode } from './GPTPopup/GPTPopup';
-import { LightboxView } from '../LightboxView';
-import { EditorView } from 'prosemirror-view';
import './AnchorMenu.scss';
-import { ColorPicker, Group, IconButton, Popup, Size, Toggle, ToggleType, Type } from 'browndash-components';
-import { StrCast } from '../../../fields/Types';
+import { GPTPopup, GPTPopupMode } from './GPTPopup/GPTPopup';
+import { SettingsManager } from '../../util/SettingsManager';
@observer
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)',
- 'rgba(238, 0, 0, 0.8)',
- 'rgba(245, 166, 35, 0.8)',
- 'rgba(248, 231, 28, 0.8)',
- 'rgba(245, 230, 95, 0.616)',
- 'rgba(139, 87, 42, 0.8)',
- 'rgba(126, 211, 33, 0.8)',
- 'rgba(65, 117, 5, 0.8)',
- 'rgba(144, 19, 254, 0.8)',
- 'rgba(238, 169, 184, 0.8)',
- 'rgba(224, 187, 228, 0.8)',
- 'rgba(225, 223, 211, 0.8)',
- 'rgba(255, 255, 255, 0.8)',
- 'rgba(155, 155, 155, 0.8)',
- 'rgba(0, 0, 0, 0.8)',
- ];
+ private _commentRef = React.createRef<HTMLDivElement>();
+ private _cropRef = React.createRef<HTMLDivElement>();
@observable private highlightColor: string = 'rgba(245, 230, 95, 0.616)';
@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 setSelectedText = (txt: string) => (this.selectedText = txt);
public onMakeAnchor: () => Opt<Doc> = () => undefined; // Method to get anchor from text search
@@ -124,27 +60,12 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
componentWillUnmount() {
this._disposer?.();
- this._disposer2?.();
}
componentDidMount() {
- this._disposer2 = reaction(
- () => this._opacity,
- opacity => {
- if (!opacity) {
- this.setGPTPopupVis(false);
- this.setGPTPopupText('');
- }
- },
- { fireImmediately: true }
- );
this._disposer = reaction(
() => SelectionManager.Views().slice(),
- selected => {
- this.setGPTPopupVis(false);
- this.setGPTPopupText('');
- AnchorMenu.Instance.fadeOut(true);
- }
+ selected => AnchorMenu.Instance.fadeOut(true)
);
}
@@ -153,67 +74,18 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
* @param e pointer down event
*/
gptSummarize = async (e: React.PointerEvent) => {
- this.setHighlightRange(undefined);
- this.setGPTPopupVis(true);
- this.setGPTMode(GPTPopupMode.SUMMARY);
- this.setLoading(true);
+ // move this logic to gptpopup, need to implement generate again
+ GPTPopup.Instance.setVisible(true);
+ GPTPopup.Instance.setMode(GPTPopupMode.SUMMARY);
+ GPTPopup.Instance.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.');
- }
+ GPTPopup.Instance.setText(res || '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;
+ GPTPopup.Instance.setLoading(false);
};
pointerDown = (e: React.PointerEvent) => {
@@ -221,7 +93,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
this,
e,
(e: PointerEvent) => {
- this.StartDrag(e, this._commentCont.current!);
+ this.StartDrag(e, this._commentRef.current!);
return true;
},
returnFalse,
@@ -238,7 +110,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
this,
e,
(e: PointerEvent) => {
- this.StartCropDrag(e, this._commentCont.current!);
+ this.StartCropDrag(e, this._cropRef.current!);
return true;
},
returnFalse,
@@ -260,9 +132,9 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
tooltip={'Click to Highlight'}
onClick={this.highlightClicked}
colorPicker={this.highlightColor}
- color={StrCast(Doc.UserDoc().userColor)}
+ color={SettingsManager.userColor}
/>
- <ColorPicker colorPickerType={'github'} selectedColor={this.highlightColor} setSelectedColor={color => this.changeHighlightColor(color)} size={Size.XSMALL} />
+ <ColorPicker selectedColor={this.highlightColor} setFinalColor={this.changeHighlightColor} setSelectedColor={this.changeHighlightColor} size={Size.XSMALL} />
</Group>
);
}
@@ -284,71 +156,36 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
* 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;
- };
-
- /**
- * 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;
- };
+ canSummarize = () => SelectionManager.Docs().some(doc => [DocumentType.PDF, DocumentType.WEB].includes(doc.type as any));
render() {
const buttons =
this.Status === 'marquee' ? (
<>
{this.highlighter}
- <IconButton
- tooltip="Drag to Place Annotation" //
- onPointerDown={this.pointerDown}
- icon={<FontAwesomeIcon icon="comment-alt" />}
- color={StrCast(Doc.UserDoc().userColor)}
- />
+ <div ref={this._commentRef}>
+ <IconButton
+ tooltip="Drag to Place Annotation" //
+ onPointerDown={this.pointerDown}
+ icon={<FontAwesomeIcon icon="comment-alt" />}
+ color={SettingsManager.userColor}
+ />
+ </div>
{/* GPT Summarize icon only shows up when text is highlighted, not on marquee selection*/}
{AnchorMenu.Instance.StartCropDrag === unimplementedFunction && this.canSummarize() && (
<IconButton
tooltip="Summarize with AI" //
onPointerDown={this.gptSummarize}
icon={<FontAwesomeIcon icon="comment-dots" size="lg" />}
- color={StrCast(Doc.UserDoc().userColor)}
+ color={SettingsManager.userColor}
/>
)}
- <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 : (
<IconButton
tooltip="Click to Record Annotation" //
onPointerDown={this.audioDown}
icon={<FontAwesomeIcon icon="microphone" />}
- color={StrCast(Doc.UserDoc().userColor)}
- />
- )}
- {this.canEdit() && (
- <IconButton
- tooltip="AI edit suggestions" //
- onPointerDown={this.gptEdit}
- icon={<FontAwesomeIcon icon="pencil-alt" />}
- color={StrCast(Doc.UserDoc().userColor)}
+ color={SettingsManager.userColor}
/>
)}
<Popup
@@ -356,15 +193,17 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
type={Type.PRIM}
icon={<FontAwesomeIcon icon={'search'} />}
popup={<LinkPopup key="popup" linkCreateAnchor={this.onMakeAnchor} />}
- color={StrCast(Doc.UserDoc().userColor)}
+ color={SettingsManager.userColor}
/>
{AnchorMenu.Instance.StartCropDrag === unimplementedFunction ? null : (
- <IconButton
- tooltip="Click/Drag to create cropped image" //
- onPointerDown={this.cropDown}
- icon={<FontAwesomeIcon icon="image" />}
- color={StrCast(Doc.UserDoc().userColor)}
- />
+ <div ref={this._cropRef}>
+ <IconButton
+ tooltip="Click/Drag to create cropped image" //
+ onPointerDown={this.cropDown}
+ icon={<FontAwesomeIcon icon="image" />}
+ color={SettingsManager.userColor}
+ />
+ </div>
)}
</>
) : (
@@ -374,7 +213,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
tooltip="Remove Link Anchor" //
onPointerDown={this.Delete}
icon={<FontAwesomeIcon icon="trash-alt" />}
- color={StrCast(Doc.UserDoc().userColor)}
+ color={SettingsManager.userColor}
/>
)}
{this.PinToPres !== returnFalse && (
@@ -382,7 +221,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
tooltip="Pin to Presentation" //
onPointerDown={this.PinToPres}
icon={<FontAwesomeIcon icon="map-pin" />}
- color={StrCast(Doc.UserDoc().userColor)}
+ color={SettingsManager.userColor}
/>
)}
{this.ShowTargetTrail !== returnFalse && (
@@ -390,7 +229,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
tooltip="Show Linked Trail" //
onPointerDown={this.ShowTargetTrail}
icon={<FontAwesomeIcon icon="taxi" />}
- color={StrCast(Doc.UserDoc().userColor)}
+ color={SettingsManager.userColor}
/>
)}
{this.IsTargetToggler !== returnFalse && (
@@ -401,7 +240,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
toggleStatus={this.IsTargetToggler()}
onClick={this.MakeTargetToggle}
icon={<FontAwesomeIcon icon="thumbtack" />}
- color={StrCast(Doc.UserDoc().userColor)}
+ color={SettingsManager.userColor}
/>
)}
</>
diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx
index db6b1f011..caa72c9dc 100644
--- a/src/client/views/pdf/Annotation.tsx
+++ b/src/client/views/pdf/Annotation.tsx
@@ -24,7 +24,7 @@ export class Annotation extends React.Component<IAnnotationProps> {
render() {
return (
<div style={{ display: this.props.anno.textCopied && !Doc.isBrushedHighlightedDegree(this.props.anno) ? 'none' : undefined }}>
- {DocListCast(this.props.anno.textInlineAnnotations).map(a => (
+ {DocListCast(this.props.anno.text_inlineAnnotations).map(a => (
<RegionAnnotation pointerEvents={this.props.pointerEvents} {...this.props} document={a} key={a[Id]} />
))}
</div>
@@ -61,7 +61,7 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
isTargetToggler = () => BoolCast(this.annoTextRegion.followLinkToggle);
@undoBatch
showTargetTrail = (anchor: Doc) => {
- const trail = DocCast(anchor.presTrail);
+ const trail = DocCast(anchor.presentationTrail);
if (trail) {
Doc.ActivePresentation = trail;
this.props.addDocTab(trail, OpenWhere.replaceRight);
diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.scss b/src/client/views/pdf/GPTPopup/GPTPopup.scss
index 44413ede7..5d966395c 100644
--- a/src/client/views/pdf/GPTPopup/GPTPopup.scss
+++ b/src/client/views/pdf/GPTPopup/GPTPopup.scss
@@ -6,12 +6,6 @@ $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;
@@ -21,9 +15,16 @@ $highlightedText: #82e0ff;
padding: 15px;
padding-bottom: 0;
z-index: 999;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ background-color: #ffffff;
+ box-shadow: 0 2px 5px #7474748d;
+ color: $textgrey;
.summary-heading {
display: flex;
+ justify-content: space-between;
align-items: center;
border-bottom: 1px solid $greyborder;
padding-bottom: 5px;
@@ -110,6 +111,59 @@ $highlightedText: #82e0ff;
}
}
+.image-content-wrapper {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 8px;
+ padding-bottom: 16px;
+
+ .img-wrapper {
+ position: relative;
+ cursor: pointer;
+
+ .img-container {
+ pointer-events: none;
+ position: relative;
+
+ img {
+ pointer-events: all;
+ position: relative;
+ }
+ }
+
+ .img-container::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(0, 0, 0, 0.5);
+ opacity: 0;
+ transition: opacity 0.3s ease;
+ }
+
+ .btn-container {
+ position: absolute;
+ right: 8px;
+ bottom: 8px;
+ opacity: 0;
+ transition: opacity 0.3s ease;
+ }
+
+ &:hover {
+ .img-container::after {
+ opacity: 1;
+ }
+
+ .btn-container {
+ opacity: 1;
+ }
+ }
+ }
+}
+
// Typist CSS
.Typist .Cursor {
display: inline-block;
diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
index 9b754588a..038c45582 100644
--- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx
+++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx
@@ -118,15 +118,13 @@ export class GPTPopup extends React.Component<GPTPopupProps> {
try {
let image_urls = await gptImageCall(this.imgDesc);
if (image_urls && image_urls[0]) {
- // need to fix this
const [result] = await Networking.PostToServer('/uploadRemoteImage', { sources: [image_urls[0]] });
- console.log('Result', result);
- console.log('Client', result.accessPaths.agnostic.client);
const source = Utils.prepend(result.accessPaths.agnostic.client);
this.setImgUrls([[image_urls[0], source]]);
}
} catch (err) {
console.log(err);
+ return '';
}
GPTPopup.Instance.setLoading(false);
};
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 1319a236d..2a191477b 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -46,7 +46,7 @@ interface IViewerProps extends FieldViewProps {
sidebarAddDoc: (doc: Doc | Doc[], sidebarKey?: string | undefined) => boolean;
loaded?: (nw: number, nh: number, np: number) => void;
setPdfViewer: (view: PDFViewer) => void;
- anchorMenuClick?: () => undefined | ((anchor: Doc, summarize?: boolean) => void);
+ anchorMenuClick?: () => undefined | ((anchor: Doc) => void);
crop: (region: Doc | undefined, addCrop?: boolean) => Doc | undefined;
}
@@ -67,8 +67,8 @@ export class PDFViewer extends React.Component<IViewerProps> {
private _pdfViewer: any;
private _styleRule: any; // stylesheet rule for making hyperlinks clickable
private _retries = 0; // number of times tried to create the PDF viewer
- 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 _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void);
+ private _setBrushViewer: undefined | ((view: { width: number; height: number; panX: number; panY: number }, transTime: number) => void);
private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
private _disposers: { [name: string]: IReactionDisposer } = {};
private _viewer: React.RefObject<HTMLDivElement> = React.createRef();
@@ -95,7 +95,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
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);
+ return this.allAnnotations.filter(a => a.text_inlineAnnotations);
}
componentDidMount = async () => {
@@ -195,7 +195,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
return focusSpeed;
};
crop = (region: Doc | undefined, addCrop?: boolean) => this.props.crop(region, addCrop);
- brushView = (view: { width: number; height: number; panX: number; panY: number }) => this._setBrushViewer?.(view);
+ brushView = (view: { width: number; height: number; panX: number; panY: number }, transTime: number) => this._setBrushViewer?.(view, transTime);
@action
setupPdfJsViewer = async () => {
@@ -375,7 +375,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
this._downY = e.clientY;
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);
+ this._setPreviewCursor?.(e.clientX, e.clientY, true, false, this.props.Document);
}
if (!e.altKey && e.button === 0 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) {
this.props.select(false);
@@ -464,13 +464,13 @@ export class PDFViewer extends React.Component<IViewerProps> {
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);
+ this._setPreviewCursor(e.clientX, e.clientY, false, false, this.props.Document);
}
// e.stopPropagation(); // bcz: not sure why this was here. We need to allow the DocumentView to get clicks to process doubleClicks
};
- setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean) => void) => (this._setPreviewCursor = func);
- setBrushViewer = (func?: (view: { width: number; height: number; panX: number; panY: number }) => void) => (this._setBrushViewer = func);
+ setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean, doc: Opt<Doc>) => void) => (this._setPreviewCursor = func);
+ setBrushViewer = (func?: (view: { width: number; height: number; panX: number; panY: number }, transTime: number) => void) => (this._setBrushViewer = func);
@action
onZoomWheel = (e: React.WheelEvent) => {
diff --git a/src/client/views/search/IconBar.tsx b/src/client/views/search/IconBar.tsx
index 6103b245c..540c1b5e1 100644
--- a/src/client/views/search/IconBar.tsx
+++ b/src/client/views/search/IconBar.tsx
@@ -1,17 +1,15 @@
import { action, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { DocumentType } from "../../documents/DocumentTypes";
-// import "./SearchBox.scss";
-import "./IconBar.scss";
+import { DocumentType } from '../../documents/DocumentTypes';
+import './IconBar.scss';
import { IconButton } from './IconButton';
-import "./IconButton.scss";
+import './IconButton.scss';
export interface IconBarProps {
setIcons: (icons: string[]) => void;
}
-
@observer
export class IconBar extends React.Component<IconBarProps> {
public _allIcons: string[] = [DocumentType.AUDIO, DocumentType.COL, DocumentType.IMG, DocumentType.LINK, DocumentType.PDF, DocumentType.RTF, DocumentType.VID, DocumentType.WEB, DocumentType.MAP];
@@ -32,7 +30,9 @@ export class IconBar extends React.Component<IconBarProps> {
}
@action.bound
- getIcons(): string[] { return this._icons; }
+ getIcons(): string[] {
+ return this._icons;
+ }
constructor(props: any) {
super(props);
@@ -40,29 +40,33 @@ export class IconBar extends React.Component<IconBarProps> {
}
@action.bound
- getList(): string[] { return this.getIcons(); }
+ getList(): string[] {
+ return this.getIcons();
+ }
@action.bound
- updateList(newList: string[]) { this.updateIcon(newList); }
+ updateList(newList: string[]) {
+ this.updateIcon(newList);
+ }
@action.bound
resetSelf = () => {
this._resetClicked = true;
this.updateList([]);
- }
+ };
@action.bound
selectAll = () => {
this._selectAllClicked = true;
this.updateList(this._allIcons);
- }
+ };
render() {
return (
<div className="icon-bar">
- {this._allIcons.map((type: string) =>
+ {this._allIcons.map((type: string) => (
<IconButton key={type.toString()} type={type} />
- )}
+ ))}
</div>
);
}
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index e911bd283..aa46e57dc 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -18,6 +18,8 @@ import './SearchBox.scss';
import { fetchRecommendations } from '../newlightbox/utils';
import { IRecommendation, Recommendation } from '../newlightbox/components';
import { Colors } from '../global/globalEnums';
+import { SettingsManager } from '../../util/SettingsManager';
+import { SearchUtil } from '../../util/SearchUtil';
const DAMPENING_FACTOR = 0.9;
const MAX_ITERATIONS = 25;
@@ -131,34 +133,6 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
* @param {Doc[]} docs - docs to be searched through recursively
* @param {number, Doc => void} func - function to be called on each doc
*
- * This method iterates through an array of docs and all docs within those docs, calling
- * the function func on each doc.
- */
- static foreachRecursiveDoc(docs: Doc[], func: (depth: number, doc: Doc) => void) {
- let newarray: Doc[] = [];
- var depth = 0;
- const visited: Doc[] = [];
- while (docs.length > 0) {
- newarray = [];
- docs.filter(d => d && !visited.includes(d)).forEach(d => {
- visited.push(d);
- const fieldKey = Doc.LayoutFieldKey(d);
- const annos = !Field.toString(Doc.LayoutField(d) as Field).includes('CollectionView');
- const data = d[annos ? fieldKey + '_annotations' : fieldKey];
- data && newarray.push(...DocListCast(data));
- const sidebar = d[fieldKey + '_sidebar'];
- sidebar && newarray.push(...DocListCast(sidebar));
- func(depth, d);
- });
- docs = newarray;
- depth++;
- }
- }
-
- /**
- * @param {Doc[]} docs - docs to be searched through recursively
- * @param {number, Doc => void} func - function to be called on each doc
- *
* This method iterates asynchronously through an array of docs and all docs within those
* docs, calling the function func on each doc.
*/
@@ -212,74 +186,10 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
@action
searchCollection(query: string) {
this._selectedResult = undefined;
- this._results = SearchBox.staticSearchCollection(CollectionDockingView.Instance?.rootDoc, query);
+ this._results = SearchUtil.SearchCollection(CollectionDockingView.Instance?.rootDoc, query);
this.computePageRanks();
}
- @action
- static staticSearchCollection(rootDoc: Opt<Doc>, query: string) {
- const blockedTypes = [DocumentType.PRESELEMENT, DocumentType.CONFIG, DocumentType.KVP, DocumentType.FILTER, DocumentType.SEARCH, DocumentType.SEARCHITEM, DocumentType.FONTICON, DocumentType.BUTTON, DocumentType.SCRIPTING];
- const blockedKeys = [
- 'x',
- 'y',
- 'proto',
- 'width',
- 'layout_autoHeight',
- 'acl-Override',
- 'acl-Guest',
- 'embedContainer',
- 'zIndex',
- 'height',
- 'text_scrollHeight',
- 'text_height',
- 'cloneFieldFilter',
- 'isDataDoc',
- 'text_annotations',
- 'dragFactory_count',
- 'text_noTemplate',
- 'proto_embeddings',
- 'isSystem',
- 'layout_fieldKey',
- 'isBaseProto',
- 'xMargin',
- 'yMargin',
- 'links',
- 'layout',
- 'layout_keyValue',
- 'layout_fitWidth',
- 'type_collection',
- 'title_custom',
- 'freeform_panX',
- 'freeform_panY',
- 'freeform_scale',
- ];
- query = query.toLowerCase();
-
- const results = new Map<Doc, string[]>();
- if (rootDoc) {
- const docs = DocListCast(rootDoc[Doc.LayoutFieldKey(rootDoc)]);
- const docIDs: String[] = [];
- SearchBox.foreachRecursiveDoc(docs, (depth: number, doc: Doc) => {
- const dtype = StrCast(doc.type) as DocumentType;
- if (dtype && !blockedTypes.includes(dtype) && !docIDs.includes(doc[Id]) && depth >= 0) {
- const hlights = new Set<string>();
- SearchBox.documentKeys(doc).forEach(
- key =>
- Field.toString(doc[key] as Field)
- .toLowerCase()
- .includes(query) && hlights.add(key)
- );
- blockedKeys.forEach(key => hlights.delete(key));
-
- if (Array.from(hlights.keys()).length > 0) {
- results.set(doc, Array.from(hlights.keys()));
- }
- }
- docIDs.push(doc[Id]);
- });
- }
- return results;
- }
/**
* This method initializes the page rank of every document to the reciprocal
@@ -373,17 +283,6 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
}
/**
- * @param {Doc} doc - doc for which keys are returned
- *
- * This method returns a list of a document doc's keys.
- */
- static documentKeys(doc: Doc) {
- const keys: { [key: string]: boolean } = {};
- Doc.GetAllPrototypes(doc).map(proto => Object.keys(proto).forEach(key => (keys[key] = false)));
- return Array.from(Object.keys(keys));
- }
-
- /**
* This method submits a search with the _searchString as its query and updates
* the results array accordingly.
*/
@@ -398,22 +297,21 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
if (query) {
this.searchCollection(query);
- const response = await fetchRecommendations('', query, [], true)
- const recs = response.recommendations
- const recommendations:IRecommendation[] = []
+ const response = await fetchRecommendations('', query, [], true);
+ const recs = response.recommendations;
+ const recommendations: IRecommendation[] = [];
for (const key in recs) {
const title = recs[key].title;
- console.log(title);
- const url = recs[key].url
- const type = recs[key].type
- const text = recs[key].text
- const transcript = recs[key].transcript
- const previewUrl = recs[key].previewUrl
- const embedding = recs[key].embedding
- const distance = recs[key].distance
- const source = recs[key].source
- const related_concepts = recs[key].related_concepts
- const docId = recs[key].doc_id
+ const url = recs[key].url;
+ const type = recs[key].type;
+ const text = recs[key].text;
+ const transcript = recs[key].transcript;
+ const previewUrl = recs[key].previewUrl;
+ const embedding = recs[key].embedding;
+ const distance = recs[key].distance;
+ const source = recs[key].source;
+ const related_concepts = recs[key].related_concepts;
+ const docId = recs[key].doc_id;
recommendations.push({
title: title,
data: url,
@@ -425,11 +323,11 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
distance: Math.round(distance * 100) / 100,
source: source,
related_concepts: related_concepts,
- docId: docId
- })
+ docId: docId,
+ });
}
- const setRecommendations = action(() => this._recommendations = recommendations)
- setRecommendations()
+ const setRecommendations = action(() => (this._recommendations = recommendations));
+ setRecommendations();
}
};
@@ -439,6 +337,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
*/
resetSearch = action(() => {
this._results.forEach((_, doc) => {
+ DocumentManager.Instance.getFirstDocumentView(doc)?.ComponentView?.search?.('', undefined, true);
Doc.UnBrushDoc(doc);
Doc.UnHighlightDoc(doc);
Doc.ClearSearchMatches();
@@ -461,7 +360,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
*/
@computed
public get selectOptions() {
- const selectValues = ['all', 'rtf', 'image', 'pdf', 'web', 'video', 'audio', 'collection'];
+ const selectValues = ['all', DocumentType.RTF, DocumentType.IMG, DocumentType.PDF, DocumentType.WEB, DocumentType.VID, DocumentType.AUDIO, DocumentType.COL];
return selectValues.map(value => (
<option key={value} value={value}>
@@ -517,19 +416,19 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
className={className}>
<div className="searchBox-result-title">{title as string}</div>
<div className="searchBox-result-type">{formattedType}</div>
- <div className="searchBox-result-keys">{result[1].join(', ')}</div>
+ <div className="searchBox-result-keys" style={{ color: SettingsManager.userVariantColor }}>
+ {result[1].join(', ')}
+ </div>
</div>
</Tooltip>
);
}
});
- const recommendationsJSX: JSX.Element[] = this._recommendations.map((props) => (
- <Recommendation {...props}/>
- ))
+ const recommendationsJSX: JSX.Element[] = this._recommendations.map(props => <Recommendation {...props} />);
return (
- <div className="searchBox-container" style={{pointerEvents: 'all', background: StrCast(Doc.UserDoc().userBackgroundColor)}}>
+ <div className="searchBox-container" style={{ pointerEvents: 'all', color: SettingsManager.userColor, background: SettingsManager.userBackgroundColor }}>
<div className="searchBox-bar">
{isLinkSearch ? null : (
<select name="type" id="searchBox-type" className="searchBox-type" onChange={this.onSelectChange}>
@@ -552,20 +451,24 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
ref={this._inputRef}
/>
</div>
- {resultsJSX.length > 0 && <div className="searchBox-results-container">
- <div className="section-header" style={{background: StrCast(Doc.UserDoc().userVariantColor, Colors.MEDIUM_BLUE)}}>
- <div className="section-title">Results</div>
- <div className="section-subtitle">{`${validResults}` + ' result' + (validResults === 1 ? '' : 's')}</div>
+ {resultsJSX.length > 0 && (
+ <div className="searchBox-results-container">
+ <div className="section-header" style={{ background: SettingsManager.userVariantColor }}>
+ <div className="section-title">Results</div>
+ <div className="section-subtitle">{`${validResults}` + ' result' + (validResults === 1 ? '' : 's')}</div>
+ </div>
+ <div className="searchBox-results-view">{resultsJSX}</div>
</div>
- <div className="searchBox-results-view">{resultsJSX}</div>
- </div>}
- {recommendationsJSX.length > 0 && <div className="searchBox-recommendations-container">
- <div className="section-header" style={{background: StrCast(Doc.UserDoc().userVariantColor, Colors.MEDIUM_BLUE)}}>
- <div className="section-title">Recommendations</div>
- <div className="section-subtitle">{`${validResults}` + ' result' + (validResults === 1 ? '' : 's')}</div>
+ )}
+ {recommendationsJSX.length > 0 && (
+ <div className="searchBox-recommendations-container">
+ <div className="section-header" style={{ background: SettingsManager.userVariantColor }}>
+ <div className="section-title">Recommendations</div>
+ <div className="section-subtitle">{`${validResults}` + ' result' + (validResults === 1 ? '' : 's')}</div>
+ </div>
+ <div className="searchBox-recommendations-view">{recommendationsJSX}</div>
</div>
- <div className="searchBox-recommendations-view">{recommendationsJSX}</div>
- </div>}
+ )}
</div>
);
}
diff --git a/src/client/views/selectedDoc/SelectedDocView.tsx b/src/client/views/selectedDoc/SelectedDocView.tsx
index 955a4a174..2139919e0 100644
--- a/src/client/views/selectedDoc/SelectedDocView.tsx
+++ b/src/client/views/selectedDoc/SelectedDocView.tsx
@@ -1,12 +1,14 @@
import React = require('react');
-import { Doc } from "../../../fields/Doc";
-import { observer } from "mobx-react";
-import { computed } from "mobx";
-import { StrCast } from "../../../fields/Types";
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Colors, ListBox } from 'browndash-components';
+import { ListBox } from 'browndash-components';
+import { computed } from 'mobx';
+import { observer } from 'mobx-react';
+import { Doc } from '../../../fields/Doc';
+import { StrCast } from '../../../fields/Types';
import { DocumentManager } from '../../util/DocumentManager';
import { DocFocusOptions } from '../nodes/DocumentView';
+import { emptyFunction } from '../../../Utils';
+import { SettingsManager } from '../../util/SettingsManager';
export interface SelectedDocViewProps {
selectedDocs: Doc[];
@@ -14,34 +16,33 @@ export interface SelectedDocViewProps {
@observer
export class SelectedDocView extends React.Component<SelectedDocViewProps> {
-
@computed get selectedDocs() {
return this.props.selectedDocs;
}
-
render() {
- return <div className={`selectedDocView-container`}>
- <ListBox
- items={this.selectedDocs.map((doc) => {
- const icon = Doc.toIcon(doc);
- const iconEle = <FontAwesomeIcon size={'1x'} icon={icon} />;
- const text = StrCast(doc.title)
- const finished = () => {
-
- };
- const options: DocFocusOptions = {
- playAudio: false,
- };
- return {
- text: text,
- val: StrCast(doc._id),
- icon: iconEle,
- onClick: () => {DocumentManager.Instance.showDocument(doc, options, finished);}
- }
- })}
- color={StrCast(Doc.UserDoc().userColor)}
- />
- </div>
+ return (
+ <div className="selectedDocView-container">
+ <ListBox
+ items={this.selectedDocs.map(doc => {
+ const options: DocFocusOptions = {
+ playAudio: false,
+ playMedia: false,
+ willPan: true,
+ };
+ return {
+ text: StrCast(doc.title),
+ val: StrCast(doc._id),
+ color: SettingsManager.userColor,
+ background: SettingsManager.userBackgroundColor,
+ icon: <FontAwesomeIcon size={'1x'} icon={Doc.toIcon(doc)} />,
+ onClick: () => DocumentManager.Instance.showDocument(doc, options, emptyFunction),
+ };
+ })}
+ color={SettingsManager.userColor}
+ background={SettingsManager.userBackgroundColor}
+ />
+ </div>
+ );
}
-} \ No newline at end of file
+}
diff --git a/src/client/views/topbar/TopBar.tsx b/src/client/views/topbar/TopBar.tsx
index cb8eda9de..55d94406a 100644
--- a/src/client/views/topbar/TopBar.tsx
+++ b/src/client/views/topbar/TopBar.tsx
@@ -1,25 +1,29 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { Button, IconButton, Size, Type, isDark } from 'browndash-components';
+import { Button, IconButton, isDark, Size, Type } from 'browndash-components';
import { action, computed, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { FaBug, FaCamera, FaStamp } from 'react-icons/fa';
+import { FaBug } from 'react-icons/fa';
import { Doc, DocListCast } from '../../../fields/Doc';
-import { AclAdmin } from '../../../fields/DocSymbols';
+import { AclAdmin, DashVersion } from '../../../fields/DocSymbols';
import { StrCast } from '../../../fields/Types';
import { GetEffectiveAcl } from '../../../fields/util';
+import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from '../../../Utils';
+import { CurrentUserUtils } from '../../util/CurrentUserUtils';
import { DocumentManager } from '../../util/DocumentManager';
import { PingManager } from '../../util/PingManager';
import { ReportManager } from '../../util/reportManager/ReportManager';
import { ServerStats } from '../../util/ServerStats';
import { SettingsManager } from '../../util/SettingsManager';
import { SharingManager } from '../../util/SharingManager';
-import { UndoManager } from '../../util/UndoManager';
+import { Transform } from '../../util/Transform';
+import { CollectionDockingView } from '../collections/CollectionDockingView';
+import { CollectionLinearView } from '../collections/collectionLinear';
import { ContextMenu } from '../ContextMenu';
import { DashboardView } from '../DashboardView';
-import { MainView } from '../MainView';
-import { CollectionDockingView } from '../collections/CollectionDockingView';
import { Colors } from '../global/globalEnums';
+import { DocumentView, DocumentViewInternal } from '../nodes/DocumentView';
+import { DefaultStyleProvider } from '../StyleProvider';
import './TopBar.scss';
/**
@@ -35,10 +39,14 @@ export class TopBar extends React.Component {
});
};
- @computed get color() { return StrCast(Doc.UserDoc().userColor, Colors.LIGHT_GRAY); }
- @computed get variantColor() { return StrCast(Doc.UserDoc().userVariantColor, Colors.MEDIUM_BLUE); }
+ @computed get color() {
+ return SettingsManager.userColor;
+ }
+ @computed get variantColor() {
+ return SettingsManager.userVariantColor;
+ }
@computed get backgroundColor() {
- return PingManager.Instance.IsBeating ? StrCast(Doc.UserDoc().userBackgroundColor, Colors.DARK_GRAY) : Colors.MEDIUM_GRAY;
+ return PingManager.Instance.IsBeating ? SettingsManager.userBackgroundColor : Colors.MEDIUM_GRAY;
}
@observable happyHeart: boolean = PingManager.Instance.IsBeating;
@@ -72,12 +80,57 @@ export class TopBar extends React.Component {
</div>
)}
{Doc.ActiveDashboard && (
- <Button text="Explore" tooltip="Browsing mode for directly navigating to documents" size={Size.SMALL} color={this.color} onClick={action(() => (MainView.Instance._exploreMode = !MainView.Instance._exploreMode))} />
+ <Button
+ text="Explore"
+ type={Type.TERT}
+ tooltip="Browsing mode for directly navigating to documents"
+ size={Size.SMALL}
+ color={DocumentView.ExploreMode ? this.variantColor : this.color}
+ background={DocumentView.ExploreMode ? this.color : 'transparent'}
+ onClick={action(() => (DocumentView.ExploreMode = !DocumentView.ExploreMode))}
+ />
)}
</div>
);
}
+ @computed get dashMenuButtons() {
+ const selDoc = Doc.MyTopBarBtns;
+ return !(selDoc instanceof Doc) ? null : (
+ <div className="collectionMenu-contMenuButtons" style={{ height: '100%' }}>
+ <CollectionLinearView
+ Document={selDoc}
+ DataDoc={undefined}
+ fieldKey="data"
+ dropAction="embed"
+ setHeight={returnFalse}
+ styleProvider={DefaultStyleProvider}
+ rootSelected={returnTrue}
+ bringToFront={emptyFunction}
+ select={emptyFunction}
+ isContentActive={returnTrue}
+ isAnyChildContentActive={returnFalse}
+ isSelected={returnFalse}
+ docViewPath={returnEmptyDoclist}
+ moveDocument={returnFalse}
+ addDocument={returnFalse}
+ addDocTab={DocumentViewInternal.addDocTabFunc}
+ pinToPres={emptyFunction}
+ removeDocument={returnFalse}
+ ScreenToLocalTransform={Transform.Identity}
+ PanelWidth={() => 200}
+ PanelHeight={() => 30}
+ renderDepth={0}
+ focus={emptyFunction}
+ whenChildContentsActiveChanged={emptyFunction}
+ childFilters={returnEmptyFilter}
+ childFiltersByRanges={returnEmptyFilter}
+ searchFilterDocs={returnEmptyDoclist}
+ />
+ </div>
+ );
+ }
+
/**
* Returns the center of the topbar
* This part of the topbar contains everything related to the current dashboard including:
@@ -102,26 +155,13 @@ export class TopBar extends React.Component {
tooltip="Open Dashboards"
size={Size.SMALL}
color={this.color}
- style={{fontWeight: 700, fontSize: '1rem'}}
+ style={{ fontWeight: 700, fontSize: '1rem' }}
onClick={(e: React.MouseEvent) => {
const dashView = Doc.ActiveDashboard && DocumentManager.Instance.getDocumentView(Doc.ActiveDashboard);
- ContextMenu.Instance.addItem({ description: 'Open Dashboard View', event: this.navigateToHome, icon: 'edit' });
dashView?.showContextMenu(e.clientX + 20, e.clientY + 30);
}}
/>
- {!Doc.noviceMode && (
- <IconButton
- tooltip="Work on a copy of the dashboard layout"
- size={Size.SMALL}
- color={this.color}
- onClick={async () => {
- const batch = UndoManager.StartBatch('snapshot');
- await DashboardView.snapshotDashboard();
- batch.end();
- }}
- icon={<FaCamera />}
- />
- )}
+ {!Doc.noviceMode && this.dashMenuButtons}
</div>
) : null;
}
@@ -132,27 +172,29 @@ export class TopBar extends React.Component {
* and allows the user to access their account settings etc.
*/
@computed get topbarRight() {
+ const upToDate = DashVersion === CurrentUserUtils.ServerVersion;
return (
<div className="topbar-right">
- {Doc.ActiveDashboard ?
+ {Doc.ActiveDashboard ? (
<Button
text={GetEffectiveAcl(Doc.ActiveDashboard) === AclAdmin ? 'Share' : 'View Original'}
type={Type.TERT}
- color={this.variantColor}
+ color={SettingsManager.userColor}
+ background={this.variantColor}
onClick={() => {
SharingManager.Instance.open(undefined, Doc.ActiveDashboard);
}}
/>
- : null }
- <IconButton tooltip={"Issue Reporter ⌘I"} size={Size.SMALL} color={this.color} onClick={ReportManager.Instance.open} icon={<FaBug />} />
- <IconButton tooltip={"Documentation ⌘D"} size={Size.SMALL} color={this.color} onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/', '_blank')} icon={<FontAwesomeIcon icon="question-circle" />} />
- <IconButton tooltip={"Settings ⌘⇧S"} size={Size.SMALL} color={this.color} onClick={SettingsManager.Instance.open} icon={<FontAwesomeIcon icon="cog" />} />
+ ) : null}
+ <IconButton tooltip={'Issue Reporter ⌘I'} size={Size.SMALL} color={this.color} onClick={ReportManager.Instance.open} icon={<FaBug />} />
+ <IconButton tooltip={'Documentation ⌘D'} size={Size.SMALL} color={this.color} onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/', '_blank')} icon={<FontAwesomeIcon icon="question-circle" />} />
+ <IconButton tooltip={'Settings ⌘⇧S'} size={Size.SMALL} color={this.color} onClick={SettingsManager.Instance.open} icon={<FontAwesomeIcon icon="cog" />} />
<IconButton
size={Size.SMALL}
onClick={ServerStats.Instance.open}
type={Type.TERT}
- tooltip={'Server is ' + (PingManager.Instance.IsBeating ? '' : 'NOT ') + 'running'}
- color={this.happyHeart ? Colors.LIGHT_BLUE : Colors.ERROR_RED}
+ tooltip={'Server is ' + (PingManager.Instance.IsBeating ? '' : 'NOT ') + 'running ' + (upToDate ? DashVersion : 'out of date version:' + DashVersion)}
+ color={this.happyHeart ? (upToDate ? Colors.LIGHT_BLUE : Colors.YELLOW) : Colors.ERROR_RED}
icon={<FontAwesomeIcon icon={this.happyHeart ? 'heart' : 'heart-broken'} />}
/>
{/* <Button text={'Logout'} borderRadius={5} hoverStyle={'gray'} backgroundColor={Colors.DARK_GRAY} color={this.color} fontSize={FontSize.SECONDARY} onClick={() => window.location.assign(Utils.prepend('/logout'))} /> */}
@@ -163,12 +205,14 @@ export class TopBar extends React.Component {
render() {
return (
//TODO:glr Add support for light / dark mode
- <div style={{
- pointerEvents: 'all',
- color: this.color,
- background: this.backgroundColor,
- // borderColor: this.color
- }} className="topbar-container">
+ <div
+ style={{
+ pointerEvents: 'all',
+ color: this.color,
+ background: this.backgroundColor,
+ // borderColor: this.color
+ }}
+ className="topbar-container">
<div className="topbar-inner-container">
{this.topbarLeft}
{this.topbarCenter}
diff --git a/src/client/views/webcam/DashWebRTCVideo.tsx b/src/client/views/webcam/DashWebRTCVideo.tsx
index 02e44a793..524492226 100644
--- a/src/client/views/webcam/DashWebRTCVideo.tsx
+++ b/src/client/views/webcam/DashWebRTCVideo.tsx
@@ -6,8 +6,8 @@ import { observer } from 'mobx-react';
import { Doc } from '../../../fields/Doc';
import { InkTool } from '../../../fields/InkField';
import '../../views/nodes/WebBox.scss';
-import { DocumentDecorations } from '../DocumentDecorations';
import { CollectionFreeFormDocumentViewProps } from '../nodes/CollectionFreeFormDocumentView';
+import { DocumentView } from '../nodes/DocumentView';
import { FieldView, FieldViewProps } from '../nodes/FieldView';
import './DashWebRTCVideo.scss';
import { hangup, initialize, refreshVideos } from './WebCamLogic';
@@ -71,8 +71,8 @@ export class DashWebRTCVideo extends React.Component<CollectionFreeFormDocumentV
</div>
);
- const frozen = !this.props.isSelected() || DocumentDecorations.Instance.Interacting;
- const classname = 'webBox-cont' + (this.props.isSelected() && Doc.ActiveTool === InkTool.None && !DocumentDecorations.Instance.Interacting ? '-interactive' : '');
+ const frozen = !this.props.isSelected() || DocumentView.Interacting;
+ const classname = 'webBox-cont' + (this.props.isSelected() && Doc.ActiveTool === InkTool.None && !DocumentView.Interacting ? '-interactive' : '');
return (
<>
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 5a8a6e4b6..56b97e42f 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -7,9 +7,8 @@ import { DocServer } from '../client/DocServer';
import { DocumentType } from '../client/documents/DocumentTypes';
import { LinkManager } from '../client/util/LinkManager';
import { scriptingGlobal, ScriptingGlobals } from '../client/util/ScriptingGlobals';
-import { SelectionManager } from '../client/util/SelectionManager';
import { afterDocDeserialize, autoObject, Deserializable, SerializationHelper } from '../client/util/SerializationHelper';
-import { undoable, UndoManager } from '../client/util/UndoManager';
+import { undoable } from '../client/util/UndoManager';
import { decycle } from '../decycler/decycler';
import * as JSZipUtils from '../JSZipUtils';
import { DashColor, incrementTitleCopy, intersectRect, Utils } from '../Utils';
@@ -21,6 +20,7 @@ import {
AclPrivate,
AclReadonly,
Animation,
+ AudioPlay,
CachedUpdates,
DirectLinks,
DocAcl,
@@ -48,9 +48,9 @@ import { FieldId, RefField } from './RefField';
import { RichTextField } from './RichTextField';
import { listSpec } from './Schema';
import { ComputedField, ScriptField } from './ScriptField';
-import { Cast, DocCast, FieldValue, NumCast, StrCast, ToConstructor } from './Types';
+import { BoolCast, Cast, DocCast, FieldValue, NumCast, StrCast, ToConstructor } from './Types';
import { AudioField, CsvField, ImageField, PdfField, VideoField, WebField } from './URLField';
-import { deleteProperty, GetEffectiveAcl, getField, getter, makeEditable, makeReadOnly, normalizeEmail, setter, SharingPermissions, containedFieldChangedHandler } from './util';
+import { containedFieldChangedHandler, deleteProperty, GetEffectiveAcl, getField, getter, makeEditable, makeReadOnly, normalizeEmail, setter, SharingPermissions } from './util';
import JSZip = require('jszip');
export namespace Field {
export function toKeyValueString(doc: Doc, key: string): string {
@@ -157,7 +157,26 @@ export function updateCachedAcls(doc: Doc) {
@scriptingGlobal
@Deserializable('Doc', updateCachedAcls, ['id'])
export class Doc extends RefField {
- @observable public static CurrentlyLoading: Doc[];
+ @observable public static RecordingEvent = 0;
+
+ // this isn't really used at the moment, but is intended to indicate whether ink stroke are passed through a gesture recognizer
+ static GetRecognizeGestures() {
+ return BoolCast(Doc.UserDoc()._recognizeGestures);
+ }
+ static SetRecognizeGestures(show: boolean) {
+ Doc.UserDoc()._recognizeGestures = show;
+ }
+
+ //
+ // This controls whether fontIconButtons will display labels under their icons or not
+ //
+ static GetShowIconLabels() {
+ return BoolCast(Doc.UserDoc()._showLabel);
+ }
+ static SetShowIconLabels(show: boolean) {
+ Doc.UserDoc()._showLabel = show;
+ }
+ @observable public static CurrentlyLoading: Doc[] = []; // this assignment doesn't work. the actual assignment happens in DocumentManager's constructor
// removes from currently loading display
@action
public static removeCurrentlyLoading(doc: Doc) {
@@ -170,9 +189,6 @@ export class Doc extends RefField {
// adds doc to currently loading display
@action
public static addCurrentlyLoading(doc: Doc) {
- if (!Doc.CurrentlyLoading) {
- Doc.CurrentlyLoading = [];
- }
if (Doc.CurrentlyLoading.indexOf(doc) === -1) {
Doc.CurrentlyLoading.push(doc);
}
@@ -205,6 +221,9 @@ export class Doc extends RefField {
public static get MyContextMenuBtns() {
return DocCast(Doc.UserDoc().myContextMenuBtns);
}
+ public static get MyTopBarBtns() {
+ return DocCast(Doc.UserDoc().myTopBarBtns);
+ }
public static get MyRecentlyClosed() {
return DocCast(Doc.UserDoc().myRecentlyClosed);
}
@@ -238,22 +257,6 @@ export class Doc extends RefField {
public static get MyFilesystem() {
return DocCast(Doc.UserDoc().myFilesystem);
}
- public static get MyFileOrphans() {
- return DocCast(Doc.UserDoc().myFileOrphans);
- }
- public static AddFileOrphan(doc: Doc) {
- if (
- doc &&
- Doc.MyFileOrphans instanceof Doc &&
- Doc.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);
}
@@ -347,6 +350,7 @@ export class Doc extends RefField {
@observable public [DocAcl]: { [key: string]: symbol } = {};
@observable public [DocCss]: number = 0; // incrementer denoting a change to CSS layout
@observable public [DirectLinks] = new ObservableSet<Doc>();
+ @observable public [AudioPlay]: any; // meant to store sound object from Howl
@observable public [Animation]: Opt<Doc>;
@observable public [Highlight]: boolean = false;
static __Anim(Doc: Doc) {
@@ -388,7 +392,7 @@ export class Doc extends RefField {
} else {
return Cast(layoutField, Doc, null);
}
- return Cast(self[renderFieldKey + '-layout[' + templateLayoutDoc[Id] + ']'], Doc, null) || templateLayoutDoc;
+ return Cast(self[renderFieldKey + '_layout[' + templateLayoutDoc[Id] + ']'], Doc, null) || templateLayoutDoc;
}
return undefined;
}
@@ -400,6 +404,12 @@ export class Doc extends RefField {
public static set noviceMode(val) {
Doc.UserDoc().noviceMode = val;
}
+ public static get IsSharingEnabled() {
+ return Doc.UserDoc().isSharingEnabled as boolean;
+ }
+ public static set IsSharingEnabled(val) {
+ Doc.UserDoc().isSharingEnabled = val;
+ }
public static get defaultAclPrivate() {
return Doc.UserDoc().defaultAclPrivate;
}
@@ -526,6 +536,13 @@ export namespace Doc {
export function IsDelegateField(doc: Doc, fieldKey: string) {
return doc && Get(doc, fieldKey, true) !== undefined;
}
+ //
+ // this will write the value to the key on either the data doc or the embedding doc. The choice
+ // of where to write it is based on:
+ // 1) if the embedding Doc already has this field defined on it, then it will be written to the embedding
+ // 2) if the data doc has the field, then it's written there.
+ // 3) if neither already has the field, then 'defaultProto' determines whether to write it to the data doc (or the embedding)
+ //
export async function SetInPlace(doc: Doc, key: string, value: Field | undefined, defaultProto: boolean) {
if (key.startsWith('_')) key = key.substring(1);
const hasProto = Doc.GetProto(doc) !== doc ? Doc.GetProto(doc) : undefined;
@@ -573,7 +590,7 @@ export namespace Doc {
// compare whether documents or their protos match
export function AreProtosEqual(doc?: Doc, other?: Doc) {
- return doc && other && Doc.GetProto(doc) === Doc.GetProto(other);
+ return doc && other && (doc === other || Doc.GetProto(doc) === Doc.GetProto(other));
}
// Gets the data document for the document. Note: this is mis-named -- it does not specifically
@@ -701,7 +718,8 @@ export namespace Doc {
}
export function BestEmbedding(doc: Doc) {
- const bestEmbedding = Doc.GetProto(doc) ? DocListCast(doc.proto_embeddings).find(doc => !doc.embedContainer && doc.author === Doc.CurrentUserEmail) : doc;
+ const bestEmbedding = Doc.GetProto(doc) ? [doc, ...DocListCast(doc.proto_embeddings)].find(doc => !doc.embedContainer && doc.author === Doc.CurrentUserEmail) : doc;
+ bestEmbedding && Doc.AddDocToList(Doc.GetProto(doc), 'protoEmbeddings', doc);
return bestEmbedding ?? Doc.MakeEmbedding(doc);
}
@@ -782,7 +800,6 @@ export namespace Doc {
copy.cloneOf = doc;
cloneMap.set(doc[Id], copy);
- Doc.AddFileOrphan(copy);
return copy;
}
export function repairClone(clone: Doc, cloneMap: Map<string, Doc>, visited: Set<Doc>) {
@@ -917,7 +934,7 @@ export namespace Doc {
// If it doesn't find the expanded layout, then it makes a delegate of the template layout and
// saves it on the data doc indexed by the template layout's id.
//
- const expandedLayoutFieldKey = templateField + '-layout[' + templateLayoutDoc[Id] + ']';
+ const expandedLayoutFieldKey = templateField + '_layout[' + templateLayoutDoc[Id] + ']';
let expandedTemplateLayout = targetDoc?.[expandedLayoutFieldKey];
if (templateLayoutDoc.resolvedDataDoc instanceof Promise) {
@@ -985,6 +1002,59 @@ export namespace Doc {
return overwrite;
}
+ export function FindReferences(infield: Doc | List<any>, references: Set<Doc>, system: boolean | undefined) {
+ if (infield instanceof List<any>) {
+ infield.forEach(val => (val instanceof Doc || val instanceof List) && FindReferences(val, references, system));
+ return;
+ }
+ const doc = infield as Doc;
+ if (references.has(doc)) {
+ references.add(doc);
+ return;
+ }
+ const excludeLists = ['My Recently Closed', 'My Header Bar', 'My Dashboards'].includes(StrCast(doc.title));
+ if (system !== undefined && ((system && !Doc.IsSystem(doc)) || (!system && Doc.IsSystem(doc)))) return;
+ references.add(doc);
+ Object.keys(doc).forEach(key => {
+ if (key === 'proto') {
+ if (doc.proto instanceof Doc) {
+ Doc.FindReferences(doc.proto, references, system);
+ }
+ } else {
+ const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
+ const field = key === 'author' ? Doc.CurrentUserEmail : ProxyField.WithoutProxy(() => doc[key]);
+ if (field instanceof RefField) {
+ if (field instanceof Doc) {
+ if (key === 'myLinkDatabase') {
+ field instanceof Doc && references.add(field);
+ // skip docs that have been closed and are scheduled for garbage collection
+ } else {
+ Doc.FindReferences(field, references, system);
+ }
+ }
+ } else if (cfield instanceof ComputedField) {
+ } else if (field instanceof ObjectField) {
+ if (field instanceof Doc) {
+ Doc.FindReferences(field, references, system);
+ } else if (field instanceof List) {
+ !excludeLists && Doc.FindReferences(field, references, system);
+ } else if (field instanceof ProxyField) {
+ if (key === 'myLinkDatabase') {
+ field instanceof Doc && references.add(field);
+ // skip docs that have been closed and are scheduled for garbage collection
+ } else {
+ Doc.FindReferences(field.value, references, system);
+ }
+ } else if (field instanceof PrefetchProxy) {
+ Doc.FindReferences(field.value, references, system);
+ }
+ } else if (field instanceof Promise) {
+ debugger; //This shouldn't happend...
+ }
+ }
+ });
+ }
+
export function MakeCopy(doc: Doc, copyProto: boolean = false, copyProtoId?: string, retitle = false): Doc {
const copy = new Doc(copyProtoId, true);
updateCachedAcls(copy);
@@ -1025,7 +1095,6 @@ export namespace Doc {
if (retitle) {
copy.title = incrementTitleCopy(StrCast(copy.title));
}
- Doc.AddFileOrphan(copy);
return copy;
}
@@ -1044,7 +1113,6 @@ export namespace Doc {
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;
@@ -1177,7 +1245,7 @@ export namespace Doc {
// the document containing the view layout information - will be the Document itself unless the Document has
// a layout field or 'layout' is given.
export function Layout(doc: Doc, layout?: Doc): Doc {
- const overrideLayout = layout && Cast(doc[`${StrCast(layout.isTemplateForField, 'data')}-layout[` + layout[Id] + ']'], Doc, null);
+ const overrideLayout = layout && Cast(doc[`${StrCast(layout.isTemplateForField, 'data')}_layout[` + layout[Id] + ']'], Doc, null);
return overrideLayout || doc[DocLayout] || doc;
}
export function SetLayout(doc: Doc, layout: Doc | string) {
@@ -1309,8 +1377,13 @@ export namespace Doc {
}
export function LinkEndpoint(linkDoc: Doc, anchorDoc: Doc) {
- if (linkDoc.link_anchor_2 === anchorDoc || (linkDoc.link_anchor_2 as Doc).annotationOn) return '2';
- return Doc.AreProtosEqual(anchorDoc, (linkDoc.link_anchor_1 as Doc).annotationOn as Doc) || Doc.AreProtosEqual(anchorDoc, linkDoc.link_anchor_1 as Doc) ? '1' : '2';
+ const linkAnchor2 = DocCast(linkDoc.link_anchor_2);
+ const linkAnchor1 = DocCast(linkDoc.link_anchor_1);
+ if (linkDoc.link_matchEmbeddings) {
+ return [linkAnchor2, linkAnchor2.annotationOn].includes(anchorDoc) ? '2' : '1';
+ }
+ if (Doc.AreProtosEqual(linkAnchor2, anchorDoc) || Doc.AreProtosEqual(linkAnchor2.annotationOn as Doc, anchorDoc)) return '2';
+ return Doc.AreProtosEqual(linkAnchor1, anchorDoc) || Doc.AreProtosEqual(linkAnchor1.annotationOn as Doc, anchorDoc) ? '1' : '2';
}
export function linkFollowUnhighlight() {
@@ -1328,9 +1401,9 @@ export namespace Doc {
UnhighlightWatchers.push(watcher);
} else watcher();
}
- export function linkFollowHighlight(destDoc: Doc | Doc[], dataAndDisplayDocs = true, presEffect?: Doc) {
+ export function linkFollowHighlight(destDoc: Doc | Doc[], dataAndDisplayDocs = true, presentation_effect?: Doc) {
linkFollowUnhighlight();
- (destDoc instanceof Doc ? [destDoc] : destDoc).forEach(doc => Doc.HighlightDoc(doc, dataAndDisplayDocs, presEffect));
+ (destDoc instanceof Doc ? [destDoc] : destDoc).forEach(doc => Doc.HighlightDoc(doc, dataAndDisplayDocs, presentation_effect));
document.removeEventListener('pointerdown', linkFollowUnhighlight);
document.addEventListener('pointerdown', linkFollowUnhighlight);
if (UnhighlightTimer) clearTimeout(UnhighlightTimer);
@@ -1345,11 +1418,11 @@ export namespace Doc {
if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(Doc.GetProto(doc)) === AclPrivate || doc.opacity === 0) return false;
return doc[Highlight] || Doc.GetProto(doc)[Highlight];
}
- export function HighlightDoc(doc: Doc, dataAndDisplayDocs = true, presEffect?: Doc) {
+ export function HighlightDoc(doc: Doc, dataAndDisplayDocs = true, presentation_effect?: Doc) {
runInAction(() => {
highlightedDocs.add(doc);
doc[Highlight] = true;
- doc[Animation] = presEffect;
+ doc[Animation] = presentation_effect;
if (dataAndDisplayDocs) {
highlightedDocs.add(Doc.GetProto(doc));
Doc.GetProto(doc)[Highlight] = true;
@@ -1390,18 +1463,36 @@ export namespace Doc {
const isTransparent = (color: string) => color !== '' && DashColor(color).alpha() !== 1;
return isTransparent(StrCast(doc[key]));
}
+ if (key === '-linkedTo') {
+ // links are not a field value, so handled here. value is an expression of form ([field=]idToDoc("..."))
+ const allLinks = LinkManager.Instance.getAllRelatedLinks(doc);
+ const matchLink = (value: string, anchor: Doc) => {
+ const linkedToExp = value?.split('=');
+ if (linkedToExp.length === 1) return Field.toScriptString(anchor) === value;
+ return Field.toScriptString(DocCast(anchor[linkedToExp[0]])) === linkedToExp[1];
+ };
+ // prettier-ignore
+ return (value === Doc.FilterNone && !allLinks.length) ||
+ (value === Doc.FilterAny && !!allLinks.length) ||
+ (allLinks.some(link => matchLink(value,DocCast(link.link_anchor_1)) ||
+ matchLink(value,DocCast(link.link_anchor_2)) ));
+ }
if (typeof value === 'string') {
value = value.replace(`,${Utils.noRecursionHack}`, '');
}
const fieldVal = doc[key];
+ // prettier-ignore
+ if ((value === Doc.FilterAny && fieldVal !== undefined) ||
+ (value === Doc.FilterNone && fieldVal === undefined)) {
+ return true;
+ }
if (Cast(fieldVal, listSpec('string'), []).length) {
- const vals = Cast(fieldVal, listSpec('string'), []);
+ const vals = StrListCast(fieldVal);
const docs = vals.some(v => (v as any) instanceof Doc);
if (docs) return value === Field.toString(fieldVal as Field);
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) || (value === String.fromCharCode(127) + '--undefined--' && fieldVal === undefined); // bcz: arghh: Todo: comparison should be parameterized as exact, or substring
+ return Field.toString(fieldVal as Field).includes(value); // bcz: arghh: Todo: comparison should be parameterized as exact, or substring
}
export function deiconifyView(doc: Doc) {
@@ -1414,24 +1505,40 @@ export namespace Doc {
prevLayout === 'icon' && (doc.deiconifyLayout = undefined);
doc.layout_fieldKey = deiconify || 'layout';
}
- export function setDocRangeFilter(container: Opt<Doc>, key: string, range?: number[]) {
+ export function setDocRangeFilter(container: Opt<Doc>, key: string, range?: readonly number[], modifiers?: 'remove') {
+ //, modifiers: 'remove' | 'set'
if (!container) return;
+
const childFiltersByRanges = Cast(container._childFiltersByRanges, listSpec('string'), []);
+
for (let i = 0; i < childFiltersByRanges.length; i += 3) {
if (childFiltersByRanges[i] === key) {
+ console.log('this is key inside childfilters by range ' + key);
childFiltersByRanges.splice(i, 3);
+ console.log('this is child filters by range ' + childFiltersByRanges);
break;
}
}
if (range !== undefined) {
+ console.log('in doc.ts in set range filter');
childFiltersByRanges.push(key);
childFiltersByRanges.push(range[0].toString());
childFiltersByRanges.push(range[1].toString());
container._childFiltersByRanges = new List<string>(childFiltersByRanges);
+ console.log('this is child filters by range ' + childFiltersByRanges[0] + ',' + childFiltersByRanges[1] + ',' + childFiltersByRanges[2]);
+ console.log('this is new list ' + container._childFiltersByRange);
}
+
+ if (modifiers) {
+ childFiltersByRanges.splice(0, 3);
+ container._childFiltersByRanges = new List<string>(childFiltersByRanges);
+ }
+ console.log('this is child filters by range END' + childFiltersByRanges[0] + ',' + childFiltersByRanges[1] + ',' + childFiltersByRanges[2]);
}
export const FilterSep = '::';
+ export const FilterAny = '--any--';
+ export const FilterNone = '--undefined--';
// filters document in a container collection:
// all documents with the specified value for the specified key are included/excluded
@@ -1443,8 +1550,8 @@ export namespace Doc {
runInAction(() => {
for (let i = 0; i < childFilters.length; i++) {
const fields = childFilters[i].split(FilterSep); // split key:value:modifier
- if (fields[0] === key && (fields[1] === value || modifiers === 'match' || (fields[2] === 'match' && modifiers === 'remove'))) {
- if (fields[2] === modifiers && modifiers && fields[1] === value) {
+ if (fields[0] === key && (fields[1] === value.toString() || modifiers === 'match' || (fields[2] === 'match' && modifiers === 'remove'))) {
+ if (fields[2] === modifiers && modifiers && fields[1] === value.toString()) {
if (toggle) modifiers = 'remove';
else return;
}
@@ -1542,7 +1649,7 @@ export namespace Doc {
case DocumentType.COMPARISON: return 'columns';
case DocumentType.RTF: return 'sticky-note';
case DocumentType.COL:
- const folder: IconProp = isOpen === true ? 'folder-open' : isOpen === false ? 'folder' : 'question';
+ const folder: IconProp = isOpen === true ? 'folder-open' : isOpen === false ? 'folder' : doc?.title==='Untitled Collection'? 'object-group': 'chalkboard';
const chevron: IconProp = isOpen === true ? 'chevron-down' : isOpen === false ? 'chevron-right' : 'question';
return !doc?.isFolder ? folder : chevron;
case DocumentType.WEB: return 'globe-asia';
@@ -1558,6 +1665,10 @@ export namespace Doc {
case DocumentType.PDF: return 'file-pdf';
case DocumentType.LINK: return 'link';
case DocumentType.MAP: return 'map-marker-alt';
+ case DocumentType.DATAVIZ: return 'chart-bar';
+ case DocumentType.EQUATION: return 'calculator';
+ case DocumentType.SIMULATION: return 'rocket';
+ case DocumentType.CONFIG: return 'question-circle';
default: return 'question';
}
}
@@ -1765,24 +1876,6 @@ ScriptingGlobals.add(function setInPlace(doc: any, field: any, value: any) {
ScriptingGlobals.add(function sameDocs(doc1: any, doc2: any) {
return Doc.AreProtosEqual(doc1, doc2);
});
-ScriptingGlobals.add(function undo() {
- SelectionManager.DeselectAll();
- return UndoManager.Undo();
-});
-
-export function ShowUndoStack() {
- SelectionManager.DeselectAll();
- var buffer = '';
- UndoManager.undoStack.forEach((batch, i) => {
- buffer += 'Batch => ' + UndoManager.undoStackNames[i] + '\n';
- ///batch.forEach(undo => (buffer += ' ' + undo.prop + '\n'));
- });
- alert(buffer);
-}
-ScriptingGlobals.add(function redo() {
- SelectionManager.DeselectAll();
- return UndoManager.Redo();
-});
ScriptingGlobals.add(function DOC(id: string) {
console.log("Can't parse a document id in a script");
return 'invalid';
@@ -1797,12 +1890,7 @@ ScriptingGlobals.add(function activePresentationItem() {
const curPres = Doc.ActivePresentation;
return curPres && DocListCast(curPres[Doc.LayoutFieldKey(curPres)])[NumCast(curPres._itemIndex)];
});
-ScriptingGlobals.add(function selectedDocs(container: Doc, excludeCollections: boolean, prevValue: any) {
- const docs = SelectionManager.Views()
- .map(dv => dv.props.Document)
- .filter(d => !Doc.AreProtosEqual(d, container) && !d.annotationOn && d.type !== DocumentType.KVP && (!excludeCollections || d.type !== DocumentType.COL || !Cast(d.data, listSpec(Doc), null)));
- return docs.length ? new List(docs) : prevValue;
-});
+
ScriptingGlobals.add(function setDocFilter(container: Doc, key: string, value: any, modifiers: 'match' | 'check' | 'x' | 'remove') {
Doc.setDocFilter(container, key, value, modifiers);
});
diff --git a/src/fields/DocSymbols.ts b/src/fields/DocSymbols.ts
index 66d1ab094..df74cc9fe 100644
--- a/src/fields/DocSymbols.ts
+++ b/src/fields/DocSymbols.ts
@@ -3,6 +3,7 @@ export const Self = Symbol('DocSelf');
export const SelfProxy = Symbol('DocSelfProxy');
export const FieldKeys = Symbol('DocFieldKeys');
export const FieldTuples = Symbol('DocFieldTuples');
+export const AudioPlay = Symbol('DocAudioPlay');
export const Width = Symbol('DocWidth');
export const Height = Symbol('DocHeight');
export const Animation = Symbol('DocAnimation');
@@ -23,3 +24,5 @@ export const UpdatingFromServer = Symbol('DocUpdatingFromServer');
export const Initializing = Symbol('DocInitializing');
export const ForceServerWrite = Symbol('DocForceServerWrite');
export const CachedUpdates = Symbol('DocCachedUpdates');
+
+export const DashVersion = 'v0.6.00';
diff --git a/src/fields/FieldLoader.tsx b/src/fields/FieldLoader.tsx
index 2a7b936f7..a5a71833c 100644
--- a/src/fields/FieldLoader.tsx
+++ b/src/fields/FieldLoader.tsx
@@ -6,10 +6,9 @@ import './FieldLoader.scss';
@observer
export class FieldLoader extends React.Component {
- @observable public static ServerLoadStatus = { requested: 0, retrieved: 0 };
- public static active = false;
+ @observable public static ServerLoadStatus = { requested: 0, retrieved: 0, message: '' };
render() {
- return <div className="fieldLoader">{`Requested: ${FieldLoader.ServerLoadStatus.requested} ... ${FieldLoader.ServerLoadStatus.retrieved} `}</div>;
+ return <div className="fieldLoader">{`${FieldLoader.ServerLoadStatus.message} request: ${FieldLoader.ServerLoadStatus.requested} ... ${FieldLoader.ServerLoadStatus.retrieved} `}</div>;
}
}
diff --git a/src/fields/IconField.ts b/src/fields/IconField.ts
new file mode 100644
index 000000000..76c4ddf1b
--- /dev/null
+++ b/src/fields/IconField.ts
@@ -0,0 +1,26 @@
+import { Deserializable } from "../client/util/SerializationHelper";
+import { serializable, primitive } from "serializr";
+import { ObjectField } from "./ObjectField";
+import { Copy, ToScriptString, ToString } from "./FieldSymbols";
+
+@Deserializable("icon")
+export class IconField extends ObjectField {
+ @serializable(primitive())
+ readonly icon: string;
+
+ constructor(icon: string) {
+ super();
+ this.icon = icon;
+ }
+
+ [Copy]() {
+ return new IconField(this.icon);
+ }
+
+ [ToScriptString]() {
+ return "invalid";
+ }
+ [ToString]() {
+ return "ICONfield";
+ }
+}
diff --git a/src/fields/List.ts b/src/fields/List.ts
index 183d644d3..f3fcc87f7 100644
--- a/src/fields/List.ts
+++ b/src/fields/List.ts
@@ -1,4 +1,4 @@
-import { action, observable } from 'mobx';
+import { action, computed, observable } from 'mobx';
import { alias, list, serializable } from 'serializr';
import { DocServer } from '../client/DocServer';
import { ScriptingGlobals } from '../client/util/ScriptingGlobals';
@@ -72,13 +72,13 @@ class ListImpl<T extends Field> extends ObjectField {
return res;
},
sort(cmpFunc: any) {
- this[Self].__realFields(); // coerce retrieving entire array
+ this[Self].__realFields; // coerce retrieving entire array
const res = this[Self].__fieldTuples.sort(cmpFunc ? (first: any, second: any) => cmpFunc(toRealField(first), toRealField(second)) : undefined);
this[SelfProxy][FieldChanged]?.();
return res;
},
splice: action(function (this: any, start: number, deleteCount: number, ...items: any[]) {
- this[Self].__realFields(); // coerce retrieving entire array
+ this[Self].__realFields; // coerce retrieving entire array
items = items.map(toObjectField);
const list = this[Self];
const removed = list.__fieldTuples.filter((item: any, i: number) => i >= start && i < start + deleteCount);
@@ -125,104 +125,104 @@ class ListImpl<T extends Field> extends ObjectField {
},
/// Accessor methods
concat: action(function (this: any, ...items: any[]) {
- this[Self].__realFields();
+ this[Self].__realFields;
return this[Self].__fieldTuples.map(toRealField).concat(...items);
}),
includes(valueToFind: any, fromIndex: number) {
if (valueToFind instanceof RefField) {
- return this[Self].__realFields().includes(valueToFind, fromIndex);
+ return this[Self].__realFields.includes(valueToFind, fromIndex);
} else {
return this[Self].__fieldTuples.includes(valueToFind, fromIndex);
}
},
indexOf(valueToFind: any, fromIndex: number) {
if (valueToFind instanceof RefField) {
- return this[Self].__realFields().indexOf(valueToFind, fromIndex);
+ return this[Self].__realFields.indexOf(valueToFind, fromIndex);
}
return this[Self].__fieldTuples.indexOf(valueToFind, fromIndex);
},
join(separator: any) {
- this[Self].__realFields();
+ this[Self].__realFields;
return this[Self].__fieldTuples.map(toRealField).join(separator);
},
lastElement() {
- return this[Self].__realFields().lastElement();
+ return this[Self].__realFields.lastElement();
},
lastIndexOf(valueToFind: any, fromIndex: number) {
if (valueToFind instanceof RefField) {
- return this[Self].__realFields().lastIndexOf(valueToFind, fromIndex);
+ return this[Self].__realFields.lastIndexOf(valueToFind, fromIndex);
} else {
return this[Self].__fieldTuples.lastIndexOf(valueToFind, fromIndex);
}
},
slice(begin: number, end: number) {
- this[Self].__realFields();
+ this[Self].__realFields;
return this[Self].__fieldTuples.slice(begin, end).map(toRealField);
},
/// Iteration methods
entries() {
- return this[Self].__realFields().entries();
+ return this[Self].__realFields.entries();
},
every(callback: any, thisArg: any) {
- return this[Self].__realFields().every(callback, thisArg);
+ return this[Self].__realFields.every(callback, thisArg);
// TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
// If we don't want to support the array parameter, we should use this version instead
// return this[Self].__fieldTuples.every((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
},
filter(callback: any, thisArg: any) {
- return this[Self].__realFields().filter(callback, thisArg);
+ return this[Self].__realFields.filter(callback, thisArg);
// TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
// If we don't want to support the array parameter, we should use this version instead
// return this[Self].__fieldTuples.filter((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
},
find(callback: any, thisArg: any) {
- return this[Self].__realFields().find(callback, thisArg);
+ return this[Self].__realFields.find(callback, thisArg);
// TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
// If we don't want to support the array parameter, we should use this version instead
// return this[Self].__fieldTuples.find((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
},
findIndex(callback: any, thisArg: any) {
- return this[Self].__realFields().findIndex(callback, thisArg);
+ return this[Self].__realFields.findIndex(callback, thisArg);
// TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
// If we don't want to support the array parameter, we should use this version instead
// return this[Self].__fieldTuples.findIndex((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
},
forEach(callback: any, thisArg: any) {
- return this[Self].__realFields().forEach(callback, thisArg);
+ return this[Self].__realFields.forEach(callback, thisArg);
// TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
// If we don't want to support the array parameter, we should use this version instead
// return this[Self].__fieldTuples.forEach((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
},
map(callback: any, thisArg: any) {
- return this[Self].__realFields().map(callback, thisArg);
+ return this[Self].__realFields.map(callback, thisArg);
// TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
// If we don't want to support the array parameter, we should use this version instead
// return this[Self].__fieldTuples.map((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
},
reduce(callback: any, initialValue: any) {
- return this[Self].__realFields().reduce(callback, initialValue);
+ return this[Self].__realFields.reduce(callback, initialValue);
// TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
// If we don't want to support the array parameter, we should use this version instead
// return this[Self].__fieldTuples.reduce((acc:any, element:any, index:number, array:any) => callback(acc, toRealField(element), index, array), initialValue);
},
reduceRight(callback: any, initialValue: any) {
- return this[Self].__realFields().reduceRight(callback, initialValue);
+ return this[Self].__realFields.reduceRight(callback, initialValue);
// TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
// If we don't want to support the array parameter, we should use this version instead
// return this[Self].__fieldTuples.reduceRight((acc:any, element:any, index:number, array:any) => callback(acc, toRealField(element), index, array), initialValue);
},
some(callback: any, thisArg: any) {
- return this[Self].__realFields().some(callback, thisArg);
+ return this[Self].__realFields.some(callback, thisArg);
// TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway.
// If we don't want to support the array parameter, we should use this version instead
// return this[Self].__fieldTuples.some((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg);
},
values() {
- return this[Self].__realFields().values();
+ return this[Self].__realFields.values();
},
[Symbol.iterator]() {
- return this[Self].__realFields().values();
+ return this[Self].__realFields.values();
},
};
static listGetter(target: any, prop: string | symbol, receiver: any): any {
@@ -262,7 +262,7 @@ 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() {
+ @computed private get __realFields() {
const unrequested = this[FieldTuples].filter(f => f instanceof ProxyField && f.needsRequesting).map(f => f as ProxyField<RefField>);
// if we find any ProxyFields that don't have a current value, then
// start the server request for all of them
diff --git a/src/fields/PresField.ts b/src/fields/PresField.ts
new file mode 100644
index 000000000..f236a04fd
--- /dev/null
+++ b/src/fields/PresField.ts
@@ -0,0 +1,6 @@
+//insert code here
+import { ObjectField } from "./ObjectField";
+
+export abstract class PresField extends ObjectField {
+
+} \ No newline at end of file
diff --git a/src/fields/RichTextUtils.ts b/src/fields/RichTextUtils.ts
index 24cd078f2..5ecf25e08 100644
--- a/src/fields/RichTextUtils.ts
+++ b/src/fields/RichTextUtils.ts
@@ -2,20 +2,20 @@ import { AssertionError } from 'assert';
import { docs_v1 } from 'googleapis';
import { Fragment, Mark, Node } from 'prosemirror-model';
import { sinkListItem } from 'prosemirror-schema-list';
-import { Utils, DashColor } from '../Utils';
-import { Docs, DocUtils } from '../client/documents/Documents';
-import { schema } from '../client/views/nodes/formattedText/schema_rts';
+import { EditorState, TextSelection, Transaction } from 'prosemirror-state';
+import { GoogleApiClientUtils } from '../client/apis/google_docs/GoogleApiClientUtils';
import { GooglePhotos } from '../client/apis/google_docs/GooglePhotosClientUtils';
import { DocServer } from '../client/DocServer';
+import { Docs, DocUtils } from '../client/documents/Documents';
import { Networking } from '../client/Network';
import { FormattedTextBox } from '../client/views/nodes/formattedText/FormattedTextBox';
+import { schema } from '../client/views/nodes/formattedText/schema_rts';
+import { DashColor, Utils } from '../Utils';
import { Doc, Opt } from './Doc';
import { Id } from './FieldSymbols';
import { RichTextField } from './RichTextField';
import { Cast, StrCast } from './Types';
import Color = require('color');
-import { EditorState, TextSelection, Transaction } from 'prosemirror-state';
-import { GoogleApiClientUtils } from '../client/apis/google_docs/GoogleApiClientUtils';
export namespace RichTextUtils {
const delimiter = '\n';
diff --git a/src/fields/Types.ts b/src/fields/Types.ts
index 251b1149d..69dbe9756 100644
--- a/src/fields/Types.ts
+++ b/src/fields/Types.ts
@@ -1,11 +1,10 @@
-import { Field, Opt, FieldResult, Doc } from './Doc';
+import { DateField } from './DateField';
+import { Doc, Field, FieldResult, Opt } from './Doc';
import { List } from './List';
import { RefField } from './RefField';
-import { DateField } from './DateField';
-import { ScriptField } from './ScriptField';
-import { URLField, WebField, ImageField, CsvField } from './URLField';
-import { TextField } from '@material-ui/core';
import { RichTextField } from './RichTextField';
+import { ScriptField } from './ScriptField';
+import { CsvField, ImageField, WebField } from './URLField';
export type ToType<T extends InterfaceValue> = T extends 'string'
? string
diff --git a/src/fields/documentSchemas.ts b/src/fields/documentSchemas.ts
index 76b287be7..8eeb52709 100644
--- a/src/fields/documentSchemas.ts
+++ b/src/fields/documentSchemas.ts
@@ -1,8 +1,7 @@
-import { makeInterface, createSchema, listSpec } from './Schema';
-import { ScriptField } from './ScriptField';
-import { Doc } from './Doc';
import { DateField } from './DateField';
-import { SchemaHeaderField } from './SchemaHeaderField';
+import { Doc } from './Doc';
+import { createSchema, listSpec, makeInterface } from './Schema';
+import { ScriptField } from './ScriptField';
export const documentSchema = createSchema({
// content properties
@@ -26,8 +25,8 @@ export const documentSchema = createSchema({
z: 'number', // z "coordinate" - non-zero specifies the overlay layer of a freeformview
zIndex: 'number', // zIndex of a document in a freeform view
_layout_scrollTop: 'number', // scroll position of a scrollable document (pdf, text, web)
- lat: 'number',
- lng: 'number',
+ latitude: 'number',
+ longitude: 'number',
// appearance properties on the layout
'_backgroundGrid-spacing': 'number', // the size of the grid for collection views
@@ -72,11 +71,11 @@ export const documentSchema = createSchema({
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
- treeViewExpandedViewLock: 'boolean', // whether the expanded view can be changed
- treeViewOpenIsTransient: 'boolean', // ignores the treeViewOpen flag (for allowing a view to not be slaved to other views of the document)
- treeViewType: 'string', // whether tree view is an outline, file syste or (default) hierarchy. For outline, clicks edit document titles immediately since double-click opening is turned off
+ treeView_Open: 'boolean', // flag denoting whether the documents sub-tree (contents) is visible or hidden
+ treeView_ExpandedView: 'string', // name of field whose contents are being displayed as the document's subtree
+ treeView_ExpandedViewLock: 'boolean', // whether the expanded view can be changed
+ treeView_OpenIsTransient: 'boolean', // ignores the treeView_Open flag (for allowing a view to not be slaved to other views of the document)
+ treeView_Type: 'string', // whether tree view is an outline, file syste or (default) hierarchy. For outline, clicks edit document titles immediately since double-click opening is turned off
// interaction and linking properties
ignoreClick: 'boolean', // whether documents ignores input clicks (but does not ignore manipulation and other events)
diff --git a/src/mobile/MobileInterface.tsx b/src/mobile/MobileInterface.tsx
index 584a7b432..c16a1c124 100644
--- a/src/mobile/MobileInterface.tsx
+++ b/src/mobile/MobileInterface.tsx
@@ -584,6 +584,7 @@ export class MobileInterface extends React.Component {
y: 400,
title: 'Collection ' + dashboardCount,
};
+
const freeformDoc = Doc.GuestTarget || Docs.Create.FreeformDocument([], freeformOptions);
const dashboardDoc = Docs.Create.StandardCollectionDockingDocument([{ doc: freeformDoc, initialWidth: 600 }], { title: `Dashboard ${dashboardCount}` }, id, 'row');
diff --git a/src/mobile/MobileMain.tsx b/src/mobile/MobileMain.tsx
index 6cbf86f77..dc3a73def 100644
--- a/src/mobile/MobileMain.tsx
+++ b/src/mobile/MobileMain.tsx
@@ -12,7 +12,7 @@ AssignAllExtensions();
const info = await CurrentUserUtils.loadCurrentUser();
DocServer.init(window.location.protocol, window.location.hostname, 4321, info.email + ' (mobile)');
await Docs.Prototypes.initialize();
- await CurrentUserUtils.loadUserDocument(info.id);
+ await CurrentUserUtils.loadUserDocument(info);
document.getElementById('root')!.addEventListener(
'wheel',
event => {
diff --git a/src/server/ApiManagers/AzureManager.ts b/src/server/ApiManagers/AzureManager.ts
index 12bb98ad0..2d0ab3aa6 100644
--- a/src/server/ApiManagers/AzureManager.ts
+++ b/src/server/ApiManagers/AzureManager.ts
@@ -1,8 +1,18 @@
import { ContainerClient, BlobServiceClient } from "@azure/storage-blob";
import * as fs from "fs";
import { Readable, Stream } from "stream";
+import * as path from "path";
const AZURE_STORAGE_CONNECTION_STRING = process.env.AZURE_STORAGE_CONNECTION_STRING;
+const extToType: { [suffix: string]: string } = {
+ ".jpeg" : "image/jpeg",
+ ".jpg" : "image/jpeg",
+ ".png" : "image/png",
+ ".svg" : "image/svg+xml",
+ ".webp" : "image/webp",
+ ".gif" : "image/gif"
+}
+
export class AzureManager {
private _containerClient: ContainerClient;
private _blobServiceClient: BlobServiceClient;
@@ -10,6 +20,7 @@ export class AzureManager {
public static CONTAINER_NAME = "dashmedia";
public static STORAGE_ACCOUNT_NAME = "dashblobstore";
+ public static BASE_STRING = `https://${AzureManager.STORAGE_ACCOUNT_NAME}.blob.core.windows.net/${AzureManager.CONTAINER_NAME}`;
constructor() {
if (!AZURE_STORAGE_CONNECTION_STRING) {
@@ -38,6 +49,14 @@ export class AzureManager {
return blockBlobClient.uploadStream(stream, undefined, undefined, blobOptions);
}
+ public static UploadBase64ImageBlob(filename: string, data: string, filetype?: string) {
+ const confirmedFiletype = filetype ? filetype : extToType[path.extname(filename)];
+ const buffer = Buffer.from(data, "base64");
+ const blockBlobClient = this.Instance.ContainerClient.getBlockBlobClient(filename);
+ const blobOptions = { blobHTTPHeaders: { blobContentType: confirmedFiletype } };
+ return blockBlobClient.upload(buffer, buffer.length, blobOptions);
+ }
+
public static UploadBlobStream(stream: Readable, filename: string, filetype: string) {
const blockBlobClient = this.Instance.ContainerClient.getBlockBlobClient(filename);
const blobOptions = { blobHTTPHeaders: { blobContentType: filetype }};
diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts
index 820e815d8..56a9d9b6b 100644
--- a/src/server/ApiManagers/UploadManager.ts
+++ b/src/server/ApiManagers/UploadManager.ts
@@ -12,6 +12,7 @@ import { AcceptableMedia, Upload } from '../SharedMediaTypes';
import ApiManager, { Registration } from './ApiManager';
import { SolrManager } from './SearchManager';
import v4 = require('uuid/v4');
+import { DashVersion } from '../../fields/DocSymbols';
const AdmZip = require('adm-zip');
const imageDataUri = require('image-data-uri');
const fs = require('fs');
@@ -45,7 +46,7 @@ export default class UploadManager extends ApiManager {
method: Method.POST,
subscription: '/ping',
secureHandler: async ({ req, res }) => {
- _success(res, { message: 'pong', date: new Date() });
+ _success(res, { message: DashVersion, date: new Date() });
},
});
@@ -63,6 +64,17 @@ export default class UploadManager extends ApiManager {
subscription: '/uploadFormData',
secureHandler: async ({ req, res }) => {
const form = new formidable.IncomingForm();
+ let fileguids = '';
+ let filesize = '';
+ form.on('field', (e: string, value: string) => {
+ if (e === 'fileguids') {
+ (fileguids = value).split(';').map(guid => DashUploadUtils.uploadProgress.set(guid, 'reading file'));
+ }
+ if (e === 'filesize') {
+ filesize = value;
+ }
+ });
+ form.on('progress', e => fileguids.split(';').map(guid => DashUploadUtils.uploadProgress.set(guid, `read:(${Math.round((100 * +e) / +filesize)}%) ${e} of ${filesize}`)));
form.keepExtensions = true;
form.uploadDir = pathToDirectory(Directory.parsed_files);
return new Promise<void>(resolve => {
@@ -101,11 +113,10 @@ export default class UploadManager extends ApiManager {
//req.readableBuffer.head.data
return new Promise<void>(async resolve => {
req.addListener('data', async args => {
- console.log(args);
const payload = String.fromCharCode.apply(String, args);
- const videoId = JSON.parse(payload).videoId;
+ const { videoId, overwriteId } = JSON.parse(payload);
const results: Upload.FileResponse[] = [];
- const result = await DashUploadUtils.uploadYoutube(videoId);
+ const result = await DashUploadUtils.uploadYoutube(videoId, overwriteId ?? videoId);
result && results.push(result);
_success(res, results);
resolve();
@@ -122,7 +133,7 @@ export default class UploadManager extends ApiManager {
req.addListener('data', args => {
const payload = String.fromCharCode.apply(String, args);
const videoId = JSON.parse(payload).videoId;
- _success(res, { progress: DashUploadUtils.QueryYoutubeProgress(videoId) });
+ _success(res, { progress: DashUploadUtils.QueryYoutubeProgress(videoId, req.user) });
resolve();
});
});
diff --git a/src/server/ApiManagers/UserManager.ts b/src/server/ApiManagers/UserManager.ts
index c3dadd821..8b7994eac 100644
--- a/src/server/ApiManagers/UserManager.ts
+++ b/src/server/ApiManagers/UserManager.ts
@@ -5,7 +5,8 @@ import { msToTime } from '../ActionUtilities';
import * as bcrypt from 'bcrypt-nodejs';
import { Opt } from '../../fields/Doc';
import { WebSocket } from '../websocket';
-import { DashStats } from '../DashStats';
+import { resolvedPorts } from '../server_Initialization';
+import { DashVersion } from '../../fields/DocSymbols';
export const timeMap: { [id: string]: number } = {};
interface ActivityUnit {
@@ -68,7 +69,18 @@ export default class UserManager extends ApiManager {
register({
method: Method.GET,
subscription: '/getCurrentUser',
- secureHandler: ({ res, user: { _id, email, cacheDocumentIds } }) => res.send(JSON.stringify({ id: _id, email, cacheDocumentIds })),
+ secureHandler: ({ res, user }) =>
+ res.send(
+ JSON.stringify({
+ version: DashVersion,
+ userDocumentId: user.userDocumentId,
+ linkDatabaseId: user.linkDatabaseId,
+ sharingDocumentId: user.sharingDocumentId,
+ email: user.email,
+ cacheDocumentIds: user.cacheDocumentIds,
+ resolvedPorts,
+ })
+ ),
publicHandler: ({ res }) => res.send(JSON.stringify({ id: '__guest__', email: 'guest' })),
});
diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts
index bff60568b..19cb3f240 100644
--- a/src/server/DashUploadUtils.ts
+++ b/src/server/DashUploadUtils.ts
@@ -44,7 +44,7 @@ function isLocal() {
return /Dash-Web[0-9]*[\\\/]src[\\\/]server[\\\/]public[\\\/](.*)/;
}
-function usingAzure(){
+function usingAzure() {
return process.env.USE_AZURE === 'true';
}
@@ -99,7 +99,7 @@ export namespace DashUploadUtils {
merge
.input(textFilePath)
.inputOptions(['-f concat', '-safe 0'])
- .outputOptions('-c copy')
+ // .outputOptions('-c copy')
//.videoCodec("copy")
.save(outputFilePath)
.on('error', (err: any) => {
@@ -135,36 +135,39 @@ export namespace DashUploadUtils {
};
}
- export function QueryYoutubeProgress(videoId: string) {
- return uploadProgress.get(videoId) ?? 'failed';
+ export function QueryYoutubeProgress(videoId: string, user?: Express.User) {
+ // console.log(`PROGRESS:${videoId}`, (user as any)?.email);
+ return uploadProgress.get(videoId) ?? 'pending data upload';
}
- let uploadProgress = new Map<string, string>();
+ export let uploadProgress = new Map<string, string>();
- export function uploadYoutube(videoId: string): Promise<Upload.FileResponse> {
+ export function uploadYoutube(videoId: string, overwriteId: string): Promise<Upload.FileResponse> {
return new Promise<Upload.FileResponse<Upload.FileInformation>>((res, rej) => {
- console.log('Uploading YouTube video: ' + videoId);
const name = videoId;
const path = name.replace(/^-/, '__') + '.mp4';
const finalPath = serverPathToFile(Directory.videos, path);
if (existsSync(finalPath)) {
- uploadProgress.set(videoId, 'computing duration');
+ uploadProgress.set(overwriteId, 'computing duration');
exec(`yt-dlp -o ${finalPath} "https://www.youtube.com/watch?v=${videoId}" --get-duration`, (error: any, stdout: any, stderr: any) => {
const time = Array.from(stdout.trim().split(':')).reverse();
const duration = (time.length > 2 ? Number(time[2]) * 1000 * 60 : 0) + (time.length > 1 ? Number(time[1]) * 60 : 0) + (time.length > 0 ? Number(time[0]) : 0);
res(resolveExistingFile(name, finalPath, Directory.videos, 'video/mp4', duration, undefined));
});
} else {
- uploadProgress.set(videoId, 'starting download');
+ uploadProgress.set(overwriteId, 'starting download');
const ytdlp = spawn(`yt-dlp`, ['-o', path, `https://www.youtube.com/watch?v=${videoId}`, '--max-filesize', '100M', '-f', 'mp4']);
- ytdlp.stdout.on('data', (data: any) => !uploadProgress.get(videoId)?.includes('Aborting.') && uploadProgress.set(videoId, data.toString()));
+ ytdlp.stdout.on('data', (data: any) => uploadProgress.set(overwriteId, data.toString()));
let errors = '';
- ytdlp.stderr.on('data', (data: any) => (errors = data.toString()));
+ ytdlp.stderr.on('data', (data: any) => {
+ uploadProgress.set(overwriteId, 'error:' + data.toString());
+ errors = data.toString();
+ });
ytdlp.on('exit', function (code: any) {
- if (code || uploadProgress.get(videoId)?.includes('Aborting.')) {
+ if (code) {
res({
source: {
size: 0,
@@ -176,7 +179,7 @@ export namespace DashUploadUtils {
result: { name: 'failed youtube query', message: `Could not archive video. ${code ? errors : uploadProgress.get(videoId)}` },
});
} else {
- uploadProgress.set(videoId, 'computing duration');
+ uploadProgress.set(overwriteId, 'computing duration');
exec(`yt-dlp-o ${path} "https://www.youtube.com/watch?v=${videoId}" --get-duration`, (error: any, stdout: any, stderr: any) => {
const time = Array.from(stdout.trim().split(':')).reverse();
const duration = (time.length > 2 ? Number(time[2]) * 1000 * 60 : 0) + (time.length > 1 ? Number(time[1]) * 60 : 0) + (time.length > 0 ? Number(time[0]) : 0);
@@ -194,7 +197,7 @@ export namespace DashUploadUtils {
const isAzureOn = usingAzure();
const { type, path, name } = file;
const types = type?.split('/') ?? [];
- uploadProgress.set(overwriteGuid ?? name, 'uploading'); // If the client sent a guid it uses to track upload progress, use that guid. Otherwise, use the file's name.
+ 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]}`;
@@ -281,6 +284,7 @@ export namespace DashUploadUtils {
const fileKey = (await md5File(file.path)) + '.pdf';
const textFilename = `${fileKey.substring(0, fileKey.length - 4)}.txt`;
if (fExists(fileKey, Directory.pdfs) && fExists(textFilename, Directory.text)) {
+ fs.unlink(file.path, () => {});
return new Promise<Upload.FileResponse>(res => {
const textFilename = `${fileKey.substring(0, fileKey.length - 4)}.txt`;
const readStream = createReadStream(serverPathToFile(Directory.text, textFilename));
@@ -384,13 +388,18 @@ export namespace DashUploadUtils {
if ((rawMatches = /^data:image\/([a-z]+);base64,(.*)/.exec(source)) !== null) {
const [ext, data] = rawMatches.slice(1, 3);
const resolved = (filename = `upload_${Utils.GenerateGuid()}.${ext}`);
- const error = await new Promise<Error | null>(resolve => {
- writeFile(serverPathToFile(Directory.images, resolved), data, 'base64', resolve);
- });
- if (error !== null) {
- return error;
+ if (usingAzure()) {
+ const response = await AzureManager.UploadBase64ImageBlob(resolved, data);
+ source = `${AzureManager.BASE_STRING}/${resolved}`;
+ } else {
+ const error = await new Promise<Error | null>(resolve => {
+ writeFile(serverPathToFile(Directory.images, resolved), data, 'base64', resolve);
+ });
+ if (error !== null) {
+ return error;
+ }
+ source = `${resolvedServerUrl}${clientPathToFile(Directory.images, resolved)}`;
}
- source = `${resolvedServerUrl}${clientPathToFile(Directory.images, resolved)}`;
}
let resolvedUrl: string;
/**
@@ -490,13 +499,13 @@ export namespace DashUploadUtils {
/**
* UploadInspectedImage() takes an image with its metadata. If Azure is being used, this method will call the Azure function
- * to execute the resizing. If Azure is not used, the function will begin to resize the image.
- *
+ * to execute the resizing. If Azure is not used, the function will begin to resize the image.
+ *
* @param metadata metadata object from InspectImage()
* @param filename the name of the file
* @param prefix the prefix to use, which will be set to '' if none is provided.
* @param cleanUp a boolean indicating if the files should be deleted after upload. True by default.
- * @returns the accessPaths for the resized files.
+ * @returns the accessPaths for the resized files.
*/
export const UploadInspectedImage = async (metadata: Upload.InspectionResults, filename?: string, prefix = '', cleanUp = true): Promise<Upload.ImageInformation> => {
const { requestable, source, ...remaining } = metadata;
@@ -504,23 +513,26 @@ export namespace DashUploadUtils {
const { images } = Directory;
const information: Upload.ImageInformation = {
accessPaths: {
- agnostic: usingAzure() ? {
- client: BLOBSTORE_URL + `/${filename}`,
- server: BLOBSTORE_URL + `/${filename}`
- } : getAccessPaths(images, resolved)
+ agnostic: usingAzure()
+ ? {
+ client: BLOBSTORE_URL + `/${resolved}`,
+ server: BLOBSTORE_URL + `/${resolved}`,
+ }
+ : getAccessPaths(images, resolved),
},
...metadata,
};
- let writtenFiles: { [suffix: string] : string};
+ let writtenFiles: { [suffix: string]: string };
if (usingAzure()) {
if (!RESIZE_FUNCTION_URL) {
- throw new Error("Resize function URL not provided.");
+ throw new Error('Resize function URL not provided.');
}
try {
const response = await axios.post(RESIZE_FUNCTION_URL, {
- url: requestable
+ url: requestable,
+ filename: resolved,
});
writtenFiles = response.data.writtenFiles;
} catch (err) {
@@ -576,8 +588,8 @@ export namespace DashUploadUtils {
/**
* outputResizedImages takes in a readable stream and resizes the images according to the sizes defined at the top of this file.
- *
- * The new images will be saved to the server with the corresponding prefixes.
+ *
+ * The new images will be saved to the server with the corresponding prefixes.
* @param streamProvider a Stream of the image to process, taken from the /parsed_files location
* @param outputFileName the basename (No suffix) of the outputted file.
* @param outputDirectory the directory to output to, usually Directory.Images
@@ -589,11 +601,20 @@ export namespace DashUploadUtils {
const outputPath = path.resolve(outputDirectory, (writtenFiles[suffix] = InjectSize(outputFileName, suffix)));
await new Promise<void>(async (resolve, reject) => {
const source = streamProvider();
- let readStream: Stream = source instanceof Promise ? await source : source;
+ let readStream = source instanceof Promise ? await source : source;
+ let error = false;
if (resizer) {
- readStream = readStream.pipe(resizer.withMetadata());
+ readStream = readStream.pipe(resizer.withMetadata()).on('error', async args => {
+ error = true;
+ if (error) {
+ const source2 = streamProvider();
+ let readStream2: Stream | undefined;
+ readStream2 = source2 instanceof Promise ? await source2 : source2;
+ readStream2?.pipe(createWriteStream(outputPath)).on('error', resolve).on('close', resolve);
+ }
+ });
}
- readStream.pipe(createWriteStream(outputPath)).on('close', resolve).on('error', reject);
+ !error && readStream?.pipe(createWriteStream(outputPath)).on('error', resolve).on('close', resolve);
});
}
return writtenFiles;
@@ -608,7 +629,7 @@ export namespace DashUploadUtils {
return [
{ suffix: SizeSuffix.Original },
...Object.values(DashUploadUtils.Sizes).map(({ suffix, width }) => {
- let initial: sharp.Sharp | undefined = sharp().resize(width, undefined, { withoutEnlargement: true });
+ let initial: sharp.Sharp | undefined = sharp({ failOnError: false }).resize(width, undefined, { withoutEnlargement: true });
if (pngs.includes(ext)) {
initial = initial.png(pngOptions);
} else if (jpgs.includes(ext)) {
@@ -621,7 +642,7 @@ export namespace DashUploadUtils {
initial = undefined;
}
return {
- resizer: initial,
+ resizer: suffix === '_o' ? undefined : initial,
suffix,
};
}),
diff --git a/src/server/server_Initialization.ts b/src/server/server_Initialization.ts
index c1934451c..839091194 100644
--- a/src/server/server_Initialization.ts
+++ b/src/server/server_Initialization.ts
@@ -100,7 +100,7 @@ function buildWithMiddleware(server: express.Express) {
passport.session(),
(req: express.Request, res: express.Response, next: express.NextFunction) => {
res.locals.user = req.user;
- if (req.originalUrl.endsWith('.png') /*|| req.originalUrl.endsWith(".js")*/ && req.method === 'GET' && (res as any)._contentLength) {
+ if ((req.originalUrl.endsWith('.png') || req.originalUrl.endsWith('.jpg') || (process.env.RELEASE === 'true' && req.originalUrl.endsWith('.js'))) && req.method === 'GET') {
const period = 30000;
res.set('Cache-control', `public, max-age=${period}`);
} else {
@@ -149,53 +149,78 @@ function registerAuthenticationRoutes(server: express.Express) {
function registerCorsProxy(server: express.Express) {
server.use('/corsProxy', async (req, res) => {
- //const referer = req.headers.referer ? decodeURIComponent(req.headers.referer) : '';
- let requrl = decodeURIComponent(req.url.substring(1));
- const qsplit = requrl.split('?q=');
- const newqsplit = requrl.split('&q=');
+ res.setHeader('Access-Control-Allow-Origin', '*');
+ res.header('Access-Control-Allow-Methods', 'GET, PUT, PATCH, POST, DELETE');
+ res.header('Access-Control-Allow-Headers', req.header('access-control-request-headers'));
+ const referer = req.headers.referer ? decodeURIComponent(req.headers.referer) : '';
+ let requrlraw = decodeURIComponent(req.url.substring(1));
+ const qsplit = requrlraw.split('?q=');
+ const newqsplit = requrlraw.split('&q=');
if (qsplit.length > 1 && newqsplit.length > 1) {
const lastq = newqsplit[newqsplit.length - 1];
- requrl = qsplit[0] + '?q=' + lastq.split('&')[0] + '&' + qsplit[1].split('&')[1];
+ requrlraw = qsplit[0] + '?q=' + lastq.split('&')[0] + '&' + qsplit[1].split('&')[1];
+ }
+ const requrl = requrlraw.startsWith('/') ? referer + requrlraw : requrlraw;
+ // cors weirdness here...
+ // if the referer is a cors page and the cors() route (I think) redirected to /corsProxy/<path> and the requested url path was relative,
+ // then we redirect again to the cors referer and just add the relative path.
+ if (!requrl.startsWith('http') && req.originalUrl.startsWith('/corsProxy') && referer?.includes('corsProxy')) {
+ res.redirect(referer + (referer.endsWith('/') ? '' : '/') + requrl);
+ } else {
+ proxyServe(req, requrl, res);
}
- proxyServe(req, requrl, res);
});
}
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')) {
+ const refToCors = (match: any, tag: string, sym: string, href: string, offset: any, string: any) => `${tag}=${sym + resolvedServerUrl}/corsProxy/${href + sym}`;
+ const relpathToCors = (match: any, href: string, offset: any, string: any) => `="${resolvedServerUrl + '/corsProxy/' + decodeURIComponent(req.originalUrl.split('/corsProxy/')[1].match(/https?:\/\/[^\/]*/)?.[0] ?? '') + '/' + href}"`;
+ if (header) {
try {
const bodyStream = htmlBodyMemoryStream.read();
if (bodyStream) {
- const htmlInputText = wasinBrFormat ? Buffer.from(brotli.decompress(bodyStream)) : zlib.gunzipSync(bodyStream);
+ const htmlInputText = wasinBrFormat ? Buffer.from(brotli.decompress(bodyStream)) : header.includes('gzip') ? zlib.gunzipSync(bodyStream) : bodyStream;
const htmlText = htmlInputText
.toString('utf8')
.replace('<head>', '<head> <style>[id ^= "google"] { display: none; } </style>')
- // .replace(/href="https?([^"]*)"/g, httpsToCors)
+ .replace(/(src|href)=([\'\"])(https?[^\2\n]*)\1/g, refToCors) // replace src or href='http(s)://...' or href="http(s)://.."
+ //.replace(/= *"\/([^"]*)"/g, relpathToCors)
.replace(/data-srcset="[^"]*"/g, '')
.replace(/srcset="[^"]*"/g, '')
.replace(/target="_blank"/g, '');
- response.send(zlib.gzipSync(htmlText));
+ response.send(header?.includes('gzip') ? zlib.gzipSync(htmlText) : htmlText);
} else {
- req.pipe(request(requrl)).pipe(response);
+ req.pipe(request(requrl))
+ .on('error', (e: any) => console.log('requrl ', e))
+ .pipe(response)
+ .on('error', (e: any) => console.log('response pipe error', e));
console.log('EMPTY body:' + req.url);
}
} catch (e) {
console.log('ERROR?: ', e);
}
} else {
- req.pipe(htmlBodyMemoryStream).pipe(response);
+ req.pipe(htmlBodyMemoryStream)
+ .on('error', (e: any) => console.log('html body memorystream error', e))
+ .pipe(response)
+ .on('error', (e: any) => console.log('html body memory stream response error', e));
}
};
- retrieveHTTPBody = () => {
- req.headers.cookie = '';
+ const retrieveHTTPBody = () => {
+ //req.headers.cookie = '';
req.pipe(request(requrl))
- .on('error', (e: any) => console.log(`Malformed CORS url: ${requrl}`, e))
+ .on('error', (e: any) => {
+ console.log(`CORS url error: ${requrl}`, e);
+ response.send(`<html><body bgcolor="red" link="006666" alink="8B4513" vlink="006666">
+ <title>Error</title>
+ <div align="center"><h1>Failed to load: ${requrl} </h1></div>
+ <p>${e}</p>
+ </body></html>`);
+ })
.on('response', (res: any) => {
res.headers;
const headers = Object.keys(res.headers);
@@ -218,16 +243,18 @@ function proxyServe(req: any, requrl: string, response: any) {
response.headers = response._headers = res.headers;
})
.on('end', sendModifiedBody)
- .pipe(htmlBodyMemoryStream);
+ .pipe(htmlBodyMemoryStream)
+ .on('error', (e: any) => console.log('http body pipe error', e));
};
retrieveHTTPBody();
}
function registerEmbeddedBrowseRelativePathHandler(server: express.Express) {
server.use('*', (req, res) => {
+ // res.setHeader('Access-Control-Allow-Origin', '*');
+ // res.header('Access-Control-Allow-Methods', 'GET, PUT, PATCH, POST, DELETE');
+ // res.header('Access-Control-Allow-Headers', req.header('access-control-request-headers'));
const relativeUrl = req.originalUrl;
- // if (req.originalUrl === '/css/main.css' || req.originalUrl === '/favicon.ico') res.end();
- // else
if (!res.headersSent && req.headers.referer?.includes('corsProxy')) {
if (!req.user) res.redirect('/home'); // When no user is logged in, we interpret a relative URL as being a reference to something they don't have access to and redirect to /home
// a request for something by a proxied referrer means it must be a relative reference. So construct a proxied absolute reference here.
@@ -237,8 +264,8 @@ function registerEmbeddedBrowseRelativePathHandler(server: express.Express) {
const actualReferUrl = proxiedRefererUrl.replace(dashServerUrl, ''); // the url of the referer without the proxy (e.g., : https://en.wikipedia.org/wiki/Engelbart)
const absoluteTargetBaseUrl = actualReferUrl.match(/https?:\/\/[^\/]*/)![0]; // the base of the original url (e.g., https://en.wikipedia.org)
const redirectedProxiedUrl = dashServerUrl + encodeURIComponent(absoluteTargetBaseUrl + relativeUrl); // the new proxied full url (e.g., http://localhost:<port>/corsProxy/https://en.wikipedia.org/<somethingelse>)
- if (relativeUrl.startsWith('//')) res.redirect('http:' + relativeUrl);
- else res.redirect(redirectedProxiedUrl);
+ const redirectUrl = relativeUrl.startsWith('//') ? 'http:' + relativeUrl : redirectedProxiedUrl;
+ res.redirect(redirectUrl);
} catch (e) {
console.log('Error embed: ', e);
}