aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorAndy Rickert <andrew_rickert@brown.edu>2020-04-02 17:42:18 -0700
committerAndy Rickert <andrew_rickert@brown.edu>2020-04-02 17:42:18 -0700
commitfb329b1a8abca361d831c7ec1f1a9ea0f3d410cf (patch)
treee09138a0544fe3814b1bd1e95d59bc4c0e96f5ed /src
parent3a1dac48c00dbe81142da90f8b52bfae02ce1921 (diff)
parentb4958eac84339dd7a88c964a9c52e89481048f55 (diff)
merge
Diffstat (limited to 'src')
-rw-r--r--src/Utils.ts82
-rw-r--r--src/client/ClientRecommender.tsx62
-rw-r--r--src/client/DocServer.ts45
-rw-r--r--src/client/Network.ts14
-rw-r--r--src/client/apis/GoogleAuthenticationManager.tsx46
-rw-r--r--src/client/apis/IBM_Recommender.ts66
-rw-r--r--src/client/apis/google_docs/GoogleApiClientUtils.ts2
-rw-r--r--src/client/apis/google_docs/GooglePhotosClientUtils.ts6
-rw-r--r--src/client/cognitive_services/CognitiveServices.ts44
-rw-r--r--src/client/documents/DocumentTypes.ts8
-rw-r--r--src/client/documents/Documents.ts319
-rw-r--r--src/client/goldenLayout.js2
-rw-r--r--src/client/util/DictationManager.ts4
-rw-r--r--src/client/util/DocumentManager.ts102
-rw-r--r--src/client/util/DragManager.ts42
-rw-r--r--src/client/util/DropConverter.ts32
-rw-r--r--src/client/util/Import & Export/DirectoryImportBox.tsx19
-rw-r--r--src/client/util/Import & Export/ImageUtils.ts2
-rw-r--r--src/client/util/InteractionUtils.tsx18
-rw-r--r--src/client/util/LinkManager.ts54
-rw-r--r--src/client/util/ProsemirrorExampleTransfer.ts54
-rw-r--r--src/client/util/RichTextMenu.tsx31
-rw-r--r--src/client/util/RichTextRules.ts562
-rw-r--r--src/client/util/RichTextSchema.tsx320
-rw-r--r--src/client/util/Scripting.ts2
-rw-r--r--src/client/util/SearchUtil.ts8
-rw-r--r--src/client/util/SelectionManager.ts2
-rw-r--r--src/client/util/SettingsManager.scss2
-rw-r--r--src/client/util/type_decls.d1
-rw-r--r--src/client/views/AntimodeMenu.tsx2
-rw-r--r--src/client/views/ContextMenu.tsx4
-rw-r--r--src/client/views/DocComponent.tsx8
-rw-r--r--src/client/views/DocumentButtonBar.tsx69
-rw-r--r--src/client/views/DocumentDecorations.scss43
-rw-r--r--src/client/views/DocumentDecorations.tsx496
-rw-r--r--src/client/views/EditableView.tsx43
-rw-r--r--src/client/views/GestureOverlay.tsx35
-rw-r--r--src/client/views/GlobalKeyHandler.ts15
-rw-r--r--src/client/views/InkingControl.tsx13
-rw-r--r--src/client/views/InkingStroke.tsx4
-rw-r--r--src/client/views/KeyphraseQueryView.tsx4
-rw-r--r--src/client/views/MainView.scss30
-rw-r--r--src/client/views/MainView.tsx77
-rw-r--r--src/client/views/MainViewNotifs.tsx2
-rw-r--r--src/client/views/MetadataEntryMenu.scss6
-rw-r--r--src/client/views/MetadataEntryMenu.tsx25
-rw-r--r--src/client/views/OverlayView.tsx4
-rw-r--r--src/client/views/RecommendationsBox.tsx18
-rw-r--r--src/client/views/ScriptBox.tsx46
-rw-r--r--src/client/views/TemplateMenu.tsx118
-rw-r--r--src/client/views/TouchScrollableMenu.tsx4
-rw-r--r--src/client/views/collections/CollectionCarouselView.scss21
-rw-r--r--src/client/views/collections/CollectionCarouselView.tsx39
-rw-r--r--src/client/views/collections/CollectionDockingView.scss45
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx266
-rw-r--r--src/client/views/collections/CollectionLinearView.tsx34
-rw-r--r--src/client/views/collections/CollectionMasonryViewFieldRow.tsx48
-rw-r--r--src/client/views/collections/CollectionPivotView.scss88
-rw-r--r--src/client/views/collections/CollectionPivotView.tsx148
-rw-r--r--src/client/views/collections/CollectionSchemaCells.tsx14
-rw-r--r--src/client/views/collections/CollectionSchemaHeaders.tsx18
-rw-r--r--src/client/views/collections/CollectionSchemaMovableTableHOC.tsx7
-rw-r--r--src/client/views/collections/CollectionSchemaView.scss6
-rw-r--r--src/client/views/collections/CollectionSchemaView.tsx68
-rw-r--r--src/client/views/collections/CollectionStackingView.scss20
-rw-r--r--src/client/views/collections/CollectionStackingView.tsx146
-rw-r--r--src/client/views/collections/CollectionStackingViewFieldColumn.tsx139
-rw-r--r--src/client/views/collections/CollectionStaffView.tsx12
-rw-r--r--src/client/views/collections/CollectionSubView.tsx375
-rw-r--r--src/client/views/collections/CollectionTimeView.scss93
-rw-r--r--src/client/views/collections/CollectionTimeView.tsx195
-rw-r--r--src/client/views/collections/CollectionTreeView.scss17
-rw-r--r--src/client/views/collections/CollectionTreeView.tsx283
-rw-r--r--src/client/views/collections/CollectionView.scss53
-rw-r--r--src/client/views/collections/CollectionView.tsx278
-rw-r--r--src/client/views/collections/CollectionViewChromes.scss29
-rw-r--r--src/client/views/collections/CollectionViewChromes.tsx156
-rw-r--r--src/client/views/collections/ParentDocumentSelector.scss9
-rw-r--r--src/client/views/collections/ParentDocumentSelector.tsx45
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx332
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx17
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx123
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx85
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss4
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx328
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx188
-rw-r--r--src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss7
-rw-r--r--src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx104
-rw-r--r--src/client/views/collections/collectionMulticolumn/CollectionMultirowView.scss35
-rw-r--r--src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx272
-rw-r--r--src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx51
-rw-r--r--src/client/views/collections/collectionMulticolumn/MulticolumnWidthLabel.tsx12
-rw-r--r--src/client/views/collections/collectionMulticolumn/MultirowHeightLabel.tsx56
-rw-r--r--src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx101
-rw-r--r--src/client/views/document_templates/caption_toggle/DetailedCaptionToggle.tsx72
-rw-r--r--src/client/views/document_templates/image_card/ImageCard.tsx15
-rw-r--r--src/client/views/linking/LinkEditor.scss23
-rw-r--r--src/client/views/linking/LinkEditor.tsx163
-rw-r--r--src/client/views/linking/LinkFollowBox.scss93
-rw-r--r--src/client/views/linking/LinkFollowBox.tsx571
-rw-r--r--src/client/views/linking/LinkMenu.tsx4
-rw-r--r--src/client/views/linking/LinkMenuGroup.tsx7
-rw-r--r--src/client/views/linking/LinkMenuItem.tsx62
-rw-r--r--src/client/views/nodes/AudioBox.scss22
-rw-r--r--src/client/views/nodes/AudioBox.tsx168
-rw-r--r--src/client/views/nodes/ButtonBox.tsx4
-rw-r--r--src/client/views/nodes/CollectionFreeFormDocumentView.tsx42
-rw-r--r--src/client/views/nodes/ColorBox.tsx35
-rw-r--r--src/client/views/nodes/ContentFittingDocumentView.scss2
-rw-r--r--src/client/views/nodes/ContentFittingDocumentView.tsx53
-rw-r--r--src/client/views/nodes/DocuLinkBox.scss27
-rw-r--r--src/client/views/nodes/DocuLinkBox.tsx120
-rw-r--r--src/client/views/nodes/DocumentBox.scss7
-rw-r--r--src/client/views/nodes/DocumentBox.tsx138
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx77
-rw-r--r--src/client/views/nodes/DocumentView.scss30
-rw-r--r--src/client/views/nodes/DocumentView.tsx579
-rw-r--r--src/client/views/nodes/FieldView.tsx29
-rw-r--r--src/client/views/nodes/FontIconBox.tsx2
-rw-r--r--src/client/views/nodes/FormattedTextBox.scss3
-rw-r--r--src/client/views/nodes/FormattedTextBox.tsx346
-rw-r--r--src/client/views/nodes/FormattedTextBoxComment.tsx12
-rw-r--r--src/client/views/nodes/IconBox.scss23
-rw-r--r--src/client/views/nodes/IconBox.tsx93
-rw-r--r--src/client/views/nodes/ImageBox.scss5
-rw-r--r--src/client/views/nodes/ImageBox.tsx183
-rw-r--r--src/client/views/nodes/KeyValueBox.scss4
-rw-r--r--src/client/views/nodes/KeyValuePair.tsx6
-rw-r--r--src/client/views/nodes/LinkBox.scss3
-rw-r--r--src/client/views/nodes/LinkBox.tsx35
-rw-r--r--src/client/views/nodes/PDFBox.scss22
-rw-r--r--src/client/views/nodes/PDFBox.tsx36
-rw-r--r--src/client/views/nodes/PresBox.scss36
-rw-r--r--src/client/views/nodes/PresBox.tsx307
-rw-r--r--src/client/views/nodes/RadialMenu.tsx12
-rw-r--r--src/client/views/nodes/RadialMenuItem.tsx30
-rw-r--r--src/client/views/nodes/ScreenshotBox.scss55
-rw-r--r--src/client/views/nodes/ScreenshotBox.tsx194
-rw-r--r--src/client/views/nodes/SliderBox-components.tsx256
-rw-r--r--src/client/views/nodes/SliderBox-tooltip.css33
-rw-r--r--src/client/views/nodes/SliderBox.scss8
-rw-r--r--src/client/views/nodes/SliderBox.tsx130
-rw-r--r--src/client/views/nodes/VideoBox.tsx25
-rw-r--r--src/client/views/nodes/WebBox.scss22
-rw-r--r--src/client/views/nodes/WebBox.tsx178
-rw-r--r--src/client/views/pdf/Annotation.tsx8
-rw-r--r--src/client/views/pdf/PDFViewer.scss8
-rw-r--r--src/client/views/pdf/PDFViewer.tsx88
-rw-r--r--src/client/views/presentationview/PresElementBox.scss35
-rw-r--r--src/client/views/presentationview/PresElementBox.tsx127
-rw-r--r--src/client/views/search/FilterBox.tsx2
-rw-r--r--src/client/views/search/IconBar.tsx2
-rw-r--r--src/client/views/search/SearchItem.tsx16
-rw-r--r--src/client/views/webcam/DashWebRTCVideo.scss83
-rw-r--r--src/client/views/webcam/DashWebRTCVideo.tsx89
-rw-r--r--src/client/views/webcam/WebCamLogic.js292
-rw-r--r--src/mobile/ImageUpload.tsx9
-rw-r--r--src/mobile/MobileInkOverlay.scss39
-rw-r--r--src/mobile/MobileInkOverlay.tsx191
-rw-r--r--src/mobile/MobileInterface.scss19
-rw-r--r--src/mobile/MobileInterface.tsx307
-rw-r--r--src/new_fields/Doc.ts395
-rw-r--r--src/new_fields/InkField.ts1
-rw-r--r--src/new_fields/ObjectField.ts1
-rw-r--r--src/new_fields/RichTextField.ts16
-rw-r--r--src/new_fields/RichTextUtils.ts43
-rw-r--r--src/new_fields/Schema.ts2
-rw-r--r--src/new_fields/ScriptField.ts13
-rw-r--r--src/new_fields/URLField.ts2
-rw-r--r--src/new_fields/documentSchemas.ts59
-rw-r--r--src/new_fields/util.ts29
-rw-r--r--src/scraping/buxton/.idea/buxton.iml8
-rw-r--r--src/scraping/buxton/.idea/inspectionProfiles/profiles_settings.xml6
-rw-r--r--src/scraping/buxton/.idea/misc.xml4
-rw-r--r--src/scraping/buxton/.idea/modules.xml8
-rw-r--r--src/scraping/buxton/.idea/vcs.xml6
-rw-r--r--src/scraping/buxton/.idea/workspace.xml173
-rw-r--r--src/scraping/buxton/final/BuxtonImporter.ts389
-rw-r--r--src/scraping/buxton/jsonifier.py231
-rw-r--r--src/scraping/buxton/narratives.py38
-rw-r--r--src/scraping/buxton/narratives/Theme - Chord Kbds.docxbin0 -> 5701815 bytes
-rw-r--r--src/scraping/buxton/narratives/chord_keyboards.json39
-rw-r--r--src/scraping/buxton/node_scraper.ts57
-rw-r--r--src/scraping/buxton/scraper.py37
-rw-r--r--src/scraping/buxton/source/Bill_Notes_3_button_optical_mouse.docxbin412208 -> 0 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_Amazon_Kindle_Keyboard.docxbin474022 -> 0 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_Apple_Adj_Keyboard.docxbin1758498 -> 0 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_Apple_Mac_Portable.docxbin748412 -> 0 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_BAT.docxbin1349620 -> 0 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_Bill_Notes_CyKey.docxbin1675500 -> 0 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_Braun_T3.docxbin1510917 -> 0 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_CasioC801.docxbin413861 -> 0 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_Casio_CZ-101.docxbin523939 -> 0 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_Casio_Mini.docxbin467304 -> 0 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_FingerWorks_Prototype.docxbin423384 -> 0 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_Fingerworks_TouchStream.docxbin1558473 -> 0 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_FrogPad.docxbin840173 -> 0 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_Gavilan_SC.docxbin1729610 -> 0 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_Grandjean_Stenotype.docxbin2094142 -> 0 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_Kindle_3G_lighted_cover.docxbin919789 -> 0 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_Matias.docxbin476141 -> 0 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_Microwriter.docxbin1042556 -> 0 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_MousePen.docxbin344083 -> 0 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_NB75D.docxbin27696302 -> 0 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_PARCkbd.docxbin631959 -> 0 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_PARCtab.docbin4046250 -> 0 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_Philco_Mystery_Control.docxbin1880816 -> 0 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_TASA_Kbd.docxbin347612 -> 0 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_The_Tap.docxbin597382 -> 0 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_Twiddler.docxbin526307 -> 0 bytes
-rw-r--r--src/scraping/buxton/source/Bill_Notes_orbiTouch.docbin3945306 -> 0 bytes
-rw-r--r--src/server/ApiManagers/DeleteManager.ts31
-rw-r--r--src/server/ApiManagers/DownloadManager.ts6
-rw-r--r--src/server/ApiManagers/GooglePhotosManager.ts284
-rw-r--r--src/server/ApiManagers/SearchManager.ts154
-rw-r--r--src/server/ApiManagers/SessionManager.ts9
-rw-r--r--src/server/ApiManagers/UploadManager.ts28
-rw-r--r--src/server/ApiManagers/UserManager.ts2
-rw-r--r--src/server/ApiManagers/UtilManager.ts60
-rw-r--r--src/server/DashSession/DashSessionAgent.ts452
-rw-r--r--src/server/DashSession/Session/agents/applied_session_agent.ts58
-rw-r--r--src/server/DashSession/Session/agents/monitor.ts298
-rw-r--r--src/server/DashSession/Session/agents/process_message_router.ts41
-rw-r--r--src/server/DashSession/Session/agents/promisified_ipc_manager.ts173
-rw-r--r--src/server/DashSession/Session/agents/server_worker.ts160
-rw-r--r--src/server/DashSession/Session/utilities/repl.ts128
-rw-r--r--src/server/DashSession/Session/utilities/session_config.ts129
-rw-r--r--src/server/DashSession/Session/utilities/utilities.ts37
-rw-r--r--src/server/DashUploadUtils.ts248
-rw-r--r--src/server/Message.ts41
-rw-r--r--src/server/Recommender.ts274
-rw-r--r--src/server/RouteManager.ts10
-rw-r--r--src/server/SharedMediaTypes.ts44
-rw-r--r--src/server/Websocket/Websocket.ts97
-rw-r--r--src/server/apis/google/GoogleApiServerUtils.ts3
-rw-r--r--src/server/apis/google/GooglePhotosUploadUtils.ts149
-rw-r--r--src/server/authentication/models/current_user_utils.ts204
-rw-r--r--src/server/database.ts8
-rw-r--r--src/server/index.ts7
-rw-r--r--src/server/server_Initialization.ts77
-rw-r--r--src/server/updateSearch.ts121
-rw-r--r--src/typings/index.d.ts3
242 files changed, 11689 insertions, 6305 deletions
diff --git a/src/Utils.ts b/src/Utils.ts
index 4deac9035..e3ec10dcd 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -1,6 +1,6 @@
import v4 = require('uuid/v4');
import v5 = require("uuid/v5");
-import { Socket } from 'socket.io';
+import { Socket, Room } from 'socket.io';
import { Message } from './server/Message';
export namespace Utils {
@@ -26,6 +26,22 @@ export namespace Utils {
return { scale, translateX, translateY };
}
+ export function TraceConsoleLog() {
+ ['log', 'warn'].forEach(function (method) {
+ const old = (console as any)[method];
+ (console as any)[method] = function () {
+ let stack = new Error("").stack?.split(/\n/);
+ // Chrome includes a single "Error" line, FF doesn't.
+ if (stack && stack[0].indexOf('Error') === 0) {
+ stack = stack.slice(1);
+ }
+ const message = (stack?.[1] || "Stack undefined!").trim();
+ const args = ([] as any[]).slice.apply(arguments).concat([message]);
+ return old.apply(console, args);
+ };
+ });
+ }
+
/**
* A convenience method. Prepends the full path (i.e. http://localhost:1050) to the
* requested extension
@@ -294,6 +310,12 @@ export namespace Utils {
handler([arg, loggingCallback('S sending', fn, message.Name)]);
});
}
+ export type RoomHandler = (socket: Socket, room: string) => any;
+ export type UsedSockets = Socket | SocketIOClient.Socket;
+ export type RoomMessage = "create or join" | "created" | "joined";
+ export function AddRoomHandler(socket: Socket, message: RoomMessage, handler: RoomHandler) {
+ socket.on(message, room => handler(socket, room));
+ }
}
export function OmitKeys(obj: any, keys: string[], addKeyFunc?: (dup: any) => void): { omit: any, extract: any } {
@@ -328,16 +350,15 @@ export function timenow() {
return now.toLocaleDateString() + ' ' + h + ':' + m + ' ' + ampm;
}
-export function aggregateBounds(boundsList: { x: number, y: number, width: number, height: number }[], xpad: number, ypad: number) {
- const bounds = boundsList.reduce((bounds, b) => {
- const [sptX, sptY] = [b.x, b.y];
- const [bptX, bptY] = [sptX + b.width, sptY + b.height];
- return {
- x: Math.min(sptX, bounds.x), y: Math.min(sptY, bounds.y),
- r: Math.max(bptX, bounds.r), b: Math.max(bptY, bounds.b)
- };
- }, { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: -Number.MAX_VALUE, b: -Number.MAX_VALUE });
- return { x: bounds.x !== Number.MAX_VALUE ? bounds.x - xpad : bounds.x, y: bounds.y !== Number.MAX_VALUE ? bounds.y - ypad : bounds.y, r: bounds.r !== -Number.MAX_VALUE ? bounds.r + xpad : bounds.r, b: bounds.b !== -Number.MAX_VALUE ? bounds.b + ypad : bounds.b };
+export function aggregateBounds(boundsList: { x: number, y: number, width?: number, height?: number }[], xpad: number, ypad: number) {
+ const bounds = boundsList.map(b => ({ x: b.x, y: b.y, r: b.x + (b.width || 0), b: b.y + (b.height || 0) })).reduce((bounds, b) => ({
+ x: Math.min(b.x, bounds.x), y: Math.min(b.y, bounds.y),
+ r: Math.max(b.r, bounds.r), b: Math.max(b.b, bounds.b)
+ }), { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: -Number.MAX_VALUE, b: -Number.MAX_VALUE });
+ return {
+ x: bounds.x !== Number.MAX_VALUE ? bounds.x - xpad : bounds.x, y: bounds.y !== Number.MAX_VALUE ? bounds.y - ypad : bounds.y,
+ r: bounds.r !== -Number.MAX_VALUE ? bounds.r + xpad : bounds.r, b: bounds.b !== -Number.MAX_VALUE ? bounds.b + ypad : bounds.b
+ };
}
export function intersectRect(r1: { left: number, top: number, width: number, height: number },
r2: { left: number, top: number, width: number, height: number }) {
@@ -348,7 +369,7 @@ export function percent2frac(percent: string) {
return Number(percent.substr(0, percent.length - 1)) / 100;
}
-export function numberRange(num: number) { return Array.from(Array(num)).map((v, i) => i); }
+export function numberRange(num: number) { return num > 0 && num < 1000 ? Array.from(Array(num)).map((v, i) => i) : []; }
export function returnTransparent() { return "transparent"; }
@@ -452,4 +473,41 @@ export function clearStyleSheetRules(sheet: any) {
return true;
}
return false;
+}
+
+export function setupMoveUpEvents(
+ target: object,
+ e: React.PointerEvent,
+ moveEvent: (e: PointerEvent, down: number[], delta: number[]) => boolean,
+ upEvent: (e: PointerEvent) => void,
+ clickEvent: (e: PointerEvent) => void) {
+ (target as any)._downX = (target as any)._lastX = e.clientX;
+ (target as any)._downY = (target as any)._lastY = e.clientY;
+
+ const _moveEvent = (e: PointerEvent): void => {
+ if (Math.abs(e.clientX - (target as any)._downX) > 4 || Math.abs(e.clientY - (target as any)._downY) > 4) {
+ if (moveEvent(e, [(target as any)._downX, (target as any)._downY],
+ [e.clientX - (target as any)._lastX, e.clientY - (target as any)._lastY])) {
+ document.removeEventListener("pointermove", _moveEvent);
+ document.removeEventListener("pointerup", _upEvent);
+ }
+ }
+ (target as any)._lastX = e.clientX;
+ (target as any)._lastY = e.clientY;
+ e.stopPropagation();
+ };
+ const _upEvent = (e: PointerEvent): void => {
+ upEvent(e);
+ if (Math.abs(e.clientX - (target as any)._downX) < 4 && Math.abs(e.clientY - (target as any)._downY) < 4) {
+ clickEvent(e);
+ }
+ document.removeEventListener("pointermove", _moveEvent);
+ document.removeEventListener("pointerup", _upEvent);
+ };
+ e.stopPropagation();
+ e.preventDefault();
+ document.removeEventListener("pointermove", _moveEvent);
+ document.removeEventListener("pointerup", _upEvent);
+ document.addEventListener("pointermove", _moveEvent);
+ document.addEventListener("pointerup", _upEvent);
} \ No newline at end of file
diff --git a/src/client/ClientRecommender.tsx b/src/client/ClientRecommender.tsx
index cb1674943..537e331ab 100644
--- a/src/client/ClientRecommender.tsx
+++ b/src/client/ClientRecommender.tsx
@@ -5,10 +5,10 @@ import { CognitiveServices, Confidence, Tag, Service } from "./cognitive_service
import React = require("react");
import { observer } from "mobx-react";
import { observable, action, computed, reaction } from "mobx";
-var assert = require('assert');
-var sw = require('stopword');
-var FeedParser = require('feedparser');
-var https = require('https');
+// var assert = require('assert');
+// var sw = require('stopword');
+// var FeedParser = require('feedparser');
+// var https = require('https');
import "./ClientRecommender.scss";
import { JSXElement } from "babel-types";
import { RichTextField } from "../new_fields/RichTextField";
@@ -69,7 +69,7 @@ export class ClientRecommender extends React.Component<RecommenderProps> {
*/
private distance(vector1: number[], vector2: number[], metric: string = "cosine") {
- assert(vector1.length === vector2.length, "Vectors are not the same length");
+ // assert(vector1.length === vector2.length, "Vectors are not the same length");
let similarity: number;
switch (metric) {
case "cosine":
@@ -113,7 +113,7 @@ export class ClientRecommender extends React.Component<RecommenderProps> {
}
}
);
- let doclist = Array.from(ClientRecommender.Instance.docVectors);
+ const doclist = Array.from(ClientRecommender.Instance.docVectors);
if (distance_metric === "euclidian") {
doclist.sort((a: RecommenderDocument, b: RecommenderDocument) => a.score - b.score);
}
@@ -169,12 +169,12 @@ export class ClientRecommender extends React.Component<RecommenderProps> {
*/
generateMetadata = async (dataDoc: Doc, extDoc: Doc, threshold: Confidence = Confidence.Excellent) => {
- let converter = (results: any) => {
- let tagDoc = new Doc;
- let tagsList = new List();
+ const converter = (results: any) => {
+ const tagDoc = new Doc;
+ const tagsList = new List();
results.tags.map((tag: Tag) => {
tagsList.push(tag.name);
- let sanitized = tag.name.replace(" ", "_");
+ const sanitized = tag.name.replace(" ", "_");
tagDoc[sanitized] = ComputedField.MakeFunction(`(${tag.confidence} >= this.confidence) ? ${tag.confidence} : "${ComputedField.undefined}"`);
});
extDoc.generatedTags = tagsList;
@@ -193,7 +193,7 @@ export class ClientRecommender extends React.Component<RecommenderProps> {
*/
private url(dataDoc: Doc) {
- let data = Cast(Doc.GetProto(dataDoc)[fieldkey], ImageField);
+ const data = Cast(Doc.GetProto(dataDoc)[fieldkey], ImageField);
return data ? data.url.href : undefined;
}
@@ -215,14 +215,14 @@ export class ClientRecommender extends React.Component<RecommenderProps> {
}
}
else {
- let fielddata = Cast(dataDoc.data, RichTextField);
- fielddata ? data = fielddata[ToPlainText]() : data = "";
+ const fielddata = Cast(dataDoc.data, RichTextField, null);
+ data = fielddata?.Text || "";
}
// STEP 2. Upon receiving response from Text Cognitive Services, do additional processing on keywords.
// Currently we are still using Cognitive Services for internal recommendations, but in the future this might not be necessary.
- let converter = async (results: any, data: string, isImage: boolean = false) => {
+ const converter = async (results: any, data: string, isImage: boolean = false) => {
let keyterms = new List<string>(); // raw keywords
let kp_string: string = ""; // keywords*frequency concatenated into a string. input into TF
let highKP: string[] = [""]; // most frequent keyphrase
@@ -237,7 +237,7 @@ export class ClientRecommender extends React.Component<RecommenderProps> {
}
else { // text processing
results.documents.forEach((doc: any) => {
- let keyPhrases = doc.keyPhrases; // returned by Cognitive Services
+ const keyPhrases = doc.keyPhrases; // returned by Cognitive Services
keyPhrases.map((kp: string) => {
keyterms.push(kp);
const frequency = this.countFrequencies(kp, data); // frequency of keyphrase in paragraph
@@ -308,10 +308,10 @@ export class ClientRecommender extends React.Component<RecommenderProps> {
*/
private countFrequencies(keyphrase: string, paragraph: string) {
- let data = paragraph.split(/ |\n/); // splits by new lines and spaces
- let kp_array = keyphrase.split(" ");
- let num_keywords = kp_array.length;
- let par_length = data.length;
+ const data = paragraph.split(/ |\n/); // splits by new lines and spaces
+ const kp_array = keyphrase.split(" ");
+ const num_keywords = kp_array.length;
+ const par_length = data.length;
let frequency = 0;
// slides keyphrase windows across paragraph and checks if it matches with corresponding paragraph slice
for (let i = 0; i <= par_length - num_keywords; i++) {
@@ -353,8 +353,8 @@ export class ClientRecommender extends React.Component<RecommenderProps> {
bingWebSearch = async (query: string) => {
const converter = async (results: any) => {
- let title_vals: string[] = [];
- let url_vals: string[] = [];
+ const title_vals: string[] = [];
+ const url_vals: string[] = [];
results.webPages.value.forEach((doc: any) => {
title_vals.push(doc.name);
url_vals.push(doc.url);
@@ -369,23 +369,23 @@ export class ClientRecommender extends React.Component<RecommenderProps> {
*/
arxivrequest = async (query: string) => {
- let xhttp = new XMLHttpRequest();
- let serveraddress = "http://export.arxiv.org/api";
+ const xhttp = new XMLHttpRequest();
+ const serveraddress = "http://export.arxiv.org/api";
const maxresults = 5;
- let endpoint = serveraddress + "/query?search_query=all:" + query + "&start=0&max_results=" + maxresults.toString();
- let promisified = (resolve: any, reject: any) => {
+ const endpoint = serveraddress + "/query?search_query=all:" + query + "&start=0&max_results=" + maxresults.toString();
+ const promisified = (resolve: any, reject: any) => {
xhttp.onreadystatechange = function () {
if (this.readyState === 4) {
- let result = xhttp.response;
- let xml = xhttp.responseXML;
+ const result = xhttp.response;
+ const xml = xhttp.responseXML;
console.log("arXiv Result: ", xml);
switch (this.status) {
case 200:
- let title_vals: string[] = [];
- let url_vals: string[] = [];
+ const title_vals: string[] = [];
+ const url_vals: string[] = [];
//console.log(result);
if (xml) {
- let titles = xml.getElementsByTagName("title");
+ const titles = xml.getElementsByTagName("title");
let counter = 1;
if (titles && titles.length > 1) {
while (counter <= maxresults) {
@@ -394,7 +394,7 @@ export class ClientRecommender extends React.Component<RecommenderProps> {
counter++;
}
}
- let ids = xml.getElementsByTagName("id");
+ const ids = xml.getElementsByTagName("id");
counter = 1;
if (ids && ids.length > 1) {
while (counter <= maxresults) {
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
index 5fcd2547e..0c9d5f75c 100644
--- a/src/client/DocServer.ts
+++ b/src/client/DocServer.ts
@@ -1,10 +1,12 @@
import * as OpenSocket from 'socket.io-client';
-import { MessageStore, YoutubeQueryTypes } from "./../server/Message";
+import { MessageStore, YoutubeQueryTypes, GestureContent, MobileInkOverlayContent, UpdateMobileInkOverlayPositionContent, MobileDocumentUploadContent } from "./../server/Message";
import { Opt, Doc } from '../new_fields/Doc';
import { Utils, emptyFunction } from '../Utils';
import { SerializationHelper } from './util/SerializationHelper';
import { RefField } from '../new_fields/RefField';
import { Id, HandleUpdate } from '../new_fields/FieldSymbols';
+import GestureOverlay from './views/GestureOverlay';
+import MobileInkOverlay from '../mobile/MobileInkOverlay';
/**
* This class encapsulates the transfer and cross-client synchronization of
@@ -21,7 +23,7 @@ import { Id, HandleUpdate } from '../new_fields/FieldSymbols';
*/
export namespace DocServer {
let _cache: { [id: string]: RefField | Promise<Opt<RefField>> } = {};
- let _socket: SocketIOClient.Socket;
+ export let _socket: SocketIOClient.Socket;
// this client's distinct GUID created at initialization
let GUID: string;
// indicates whether or not a document is currently being udpated, and, if so, its id
@@ -64,6 +66,26 @@ export namespace DocServer {
}
}
+ export namespace Mobile {
+
+ export function dispatchGesturePoints(content: GestureContent) {
+ Utils.Emit(_socket, MessageStore.GesturePoints, content);
+ }
+
+ export function dispatchOverlayTrigger(content: MobileInkOverlayContent) {
+ // _socket.emit("dispatchBoxTrigger");
+ Utils.Emit(_socket, MessageStore.MobileInkOverlayTrigger, content);
+ }
+
+ export function dispatchOverlayPositionUpdate(content: UpdateMobileInkOverlayPositionContent) {
+ Utils.Emit(_socket, MessageStore.UpdateMobileInkOverlayPosition, content);
+ }
+
+ export function dispatchMobileDocumentUpload(content: MobileDocumentUploadContent) {
+ Utils.Emit(_socket, MessageStore.MobileDocumentUpload, content);
+ }
+ }
+
const instructions = "This page will automatically refresh after this alert is closed. Expect to reconnect after about 30 seconds.";
function alertUser(connectionTerminationReason: string) {
switch (connectionTerminationReason) {
@@ -101,6 +123,21 @@ export namespace DocServer {
Utils.AddServerHandler(_socket, MessageStore.DeleteField, respondToDelete);
Utils.AddServerHandler(_socket, MessageStore.DeleteFields, respondToDelete);
Utils.AddServerHandler(_socket, MessageStore.ConnectionTerminated, alertUser);
+
+ // mobile ink overlay socket events to communicate between mobile view and desktop view
+ _socket.addEventListener("receiveGesturePoints", (content: GestureContent) => {
+ MobileInkOverlay.Instance.drawStroke(content);
+ });
+ _socket.addEventListener("receiveOverlayTrigger", (content: MobileInkOverlayContent) => {
+ GestureOverlay.Instance.enableMobileInkOverlay(content);
+ MobileInkOverlay.Instance.initMobileInkOverlay(content);
+ });
+ _socket.addEventListener("receiveUpdateOverlayPosition", (content: UpdateMobileInkOverlayPositionContent) => {
+ MobileInkOverlay.Instance.updatePosition(content);
+ });
+ _socket.addEventListener("receiveMobileDocumentUpload", (content: MobileDocumentUploadContent) => {
+ MobileInkOverlay.Instance.uploadDocument(content);
+ });
}
function errorFunc(): never {
@@ -218,10 +255,6 @@ export namespace DocServer {
return apiKey;
}
- export async function analyzeImage(image: string, callback: (result: any) => void) {
- Utils.EmitCallback(_socket, MessageStore.AnalyzeInk, image, callback);
- }
-
export function getYoutubeVideos(videoTitle: string, callBack: (videos: any[]) => void) {
Utils.EmitCallback(_socket, MessageStore.YoutubeApiQuery, { type: YoutubeQueryTypes.SearchVideo, userInput: videoTitle }, callBack);
}
diff --git a/src/client/Network.ts b/src/client/Network.ts
index ccf60f199..6982ecf19 100644
--- a/src/client/Network.ts
+++ b/src/client/Network.ts
@@ -1,5 +1,6 @@
import { Utils } from "../Utils";
import requestPromise = require('request-promise');
+import { Upload } from "../server/SharedMediaTypes";
export namespace Networking {
@@ -17,12 +18,21 @@ export namespace Networking {
return requestPromise.post(options);
}
- export async function PostFormDataToServer(relativeRoute: string, formData: FormData) {
+ export async function UploadFilesToServer<T extends Upload.FileInformation = Upload.FileInformation>(files: File | File[]): Promise<Upload.FileResponse<T>[]> {
+ const formData = new FormData();
+ if (Array.isArray(files)) {
+ if (!files.length) {
+ return [];
+ }
+ files.forEach(file => formData.append(Utils.GenerateGuid(), file));
+ } else {
+ formData.append(Utils.GenerateGuid(), files);
+ }
const parameters = {
method: 'POST',
body: formData
};
- const response = await fetch(relativeRoute, parameters);
+ const response = await fetch("/uploadFormData", parameters);
return response.json();
}
diff --git a/src/client/apis/GoogleAuthenticationManager.tsx b/src/client/apis/GoogleAuthenticationManager.tsx
index ce1277667..417dc3c3b 100644
--- a/src/client/apis/GoogleAuthenticationManager.tsx
+++ b/src/client/apis/GoogleAuthenticationManager.tsx
@@ -12,8 +12,8 @@ const prompt = "Paste authorization code here...";
@observer
export default class GoogleAuthenticationManager extends React.Component<{}> {
public static Instance: GoogleAuthenticationManager;
- @observable private openState = false;
private authenticationLink: Opt<string> = undefined;
+ @observable private openState = false;
@observable private authenticationCode: Opt<string> = undefined;
@observable private clickedState = false;
@observable private success: Opt<boolean> = undefined;
@@ -39,24 +39,18 @@ export default class GoogleAuthenticationManager extends React.Component<{}> {
const disposer = reaction(
() => this.authenticationCode,
async authenticationCode => {
- if (!authenticationCode) {
- return;
+ if (authenticationCode) {
+ disposer();
+ const { access_token, avatar, name } = await Networking.PostToServer("/writeGoogleAccessToken", { authenticationCode });
+ runInAction(() => {
+ this.avatar = avatar;
+ this.username = name;
+ this.hasBeenClicked = false;
+ this.success = false;
+ });
+ this.beginFadeout();
+ resolve(access_token);
}
- const { access_token, avatar, name } = await Networking.PostToServer(
- "/writeGoogleAccessToken",
- { authenticationCode }
- );
- runInAction(() => {
- this.avatar = avatar;
- this.username = name;
- });
- this.beginFadeout();
- disposer();
- resolve(access_token);
- action(() => {
- this.hasBeenClicked = false;
- this.success = false;
- });
}
);
});
@@ -86,26 +80,20 @@ export default class GoogleAuthenticationManager extends React.Component<{}> {
GoogleAuthenticationManager.Instance = this;
}
- private handleClick = () => {
- window.open(this.authenticationLink);
- setTimeout(() => this.hasBeenClicked = true, 500);
- }
-
- private handlePaste = action((e: React.ChangeEvent<HTMLInputElement>) => {
- this.authenticationCode = e.currentTarget.value;
- });
-
private get renderPrompt() {
return (
<div className={'authorize-container'}>
{this.displayLauncher ? <button
className={"dispatch"}
- onClick={this.handleClick}
+ onClick={() => {
+ window.open(this.authenticationLink);
+ setTimeout(() => this.hasBeenClicked = true, 500);
+ }}
style={{ marginBottom: this.clickedState ? 15 : 0 }}
>Authorize a Google account...</button> : (null)}
{this.clickedState ? <input
className={'paste-target'}
- onChange={this.handlePaste}
+ onChange={action(e => this.authenticationCode = e.currentTarget.value)}
placeholder={prompt}
/> : (null)}
{this.avatar ? <img
diff --git a/src/client/apis/IBM_Recommender.ts b/src/client/apis/IBM_Recommender.ts
index da6257f28..4e1c541c8 100644
--- a/src/client/apis/IBM_Recommender.ts
+++ b/src/client/apis/IBM_Recommender.ts
@@ -1,40 +1,40 @@
-import { Opt } from "../../new_fields/Doc";
+// import { Opt } from "../../new_fields/Doc";
-const NaturalLanguageUnderstandingV1 = require('ibm-watson/natural-language-understanding/v1');
-const { IamAuthenticator } = require('ibm-watson/auth');
+// const NaturalLanguageUnderstandingV1 = require('ibm-watson/natural-language-understanding/v1');
+// const { IamAuthenticator } = require('ibm-watson/auth');
-export namespace IBM_Recommender {
+// export namespace IBM_Recommender {
- // pass to IBM account is Browngfx1
+// // pass to IBM account is Browngfx1
- const naturalLanguageUnderstanding = new NaturalLanguageUnderstandingV1({
- version: '2019-07-12',
- authenticator: new IamAuthenticator({
- apikey: 'tLiYwbRim3CnBcCO4phubpf-zEiGcub1uh0V-sD9OKhw',
- }),
- url: 'https://gateway-wdc.watsonplatform.net/natural-language-understanding/api'
- });
+// const naturalLanguageUnderstanding = new NaturalLanguageUnderstandingV1({
+// version: '2019-07-12',
+// authenticator: new IamAuthenticator({
+// apikey: 'tLiYwbRim3CnBcCO4phubpf-zEiGcub1uh0V-sD9OKhw',
+// }),
+// url: 'https://gateway-wdc.watsonplatform.net/natural-language-understanding/api'
+// });
- const analyzeParams = {
- 'text': 'this is a test of the keyword extraction feature I am integrating into the program',
- 'features': {
- 'keywords': {
- 'sentiment': true,
- 'emotion': true,
- 'limit': 3
- },
- }
- };
+// const analyzeParams = {
+// 'text': 'this is a test of the keyword extraction feature I am integrating into the program',
+// 'features': {
+// 'keywords': {
+// 'sentiment': true,
+// 'emotion': true,
+// 'limit': 3
+// },
+// }
+// };
- export const analyze = async (_parameters: any): Promise<Opt<string>> => {
- try {
- const response = await naturalLanguageUnderstanding.analyze(_parameters);
- console.log(response);
- return (JSON.stringify(response, null, 2));
- } catch (err) {
- console.log('error: ', err);
- return undefined;
- }
- };
+// export const analyze = async (_parameters: any): Promise<Opt<string>> => {
+// try {
+// const response = await naturalLanguageUnderstanding.analyze(_parameters);
+// console.log(response);
+// return (JSON.stringify(response, null, 2));
+// } catch (err) {
+// console.log('error: ', err);
+// return undefined;
+// }
+// };
-} \ No newline at end of file
+// } \ No newline at end of file
diff --git a/src/client/apis/google_docs/GoogleApiClientUtils.ts b/src/client/apis/google_docs/GoogleApiClientUtils.ts
index d2a79f189..0d44ee8e0 100644
--- a/src/client/apis/google_docs/GoogleApiClientUtils.ts
+++ b/src/client/apis/google_docs/GoogleApiClientUtils.ts
@@ -248,7 +248,7 @@ export namespace GoogleApiClientUtils {
return undefined;
}
requests.push(...options.content.requests);
- const replies: any = await update({ documentId: documentId, requests });
+ const replies: any = await update({ documentId, requests });
if ("errors" in replies) {
console.log("Write operation failed:");
console.log(replies.errors.map((error: any) => error.message));
diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
index 7e5d5fe1b..7c4137f59 100644
--- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts
+++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
@@ -306,7 +306,7 @@ export namespace GooglePhotos {
};
export const WriteMediaItemsToServer = async (body: { mediaItems: any[] }): Promise<UploadInformation[]> => {
- const uploads = await Networking.PostToServer("/googlePhotosMediaDownload", body);
+ const uploads = await Networking.PostToServer("/googlePhotosMediaGet", body);
return uploads;
};
@@ -340,11 +340,11 @@ export namespace GooglePhotos {
const url = data.url.href;
const target = Doc.MakeAlias(source);
const description = parseDescription(target, descriptionKey);
- await DocumentView.makeCustomViewClicked(target, undefined, Docs.Create.FreeformDocument);
+ await DocumentView.makeCustomViewClicked(target, Docs.Create.FreeformDocument);
media.push({ url, description });
}
if (media.length) {
- const results = await Networking.PostToServer("/googlePhotosMediaUpload", { media, album });
+ const results = await Networking.PostToServer("/googlePhotosMediaPost", { media, album });
return results;
}
};
diff --git a/src/client/cognitive_services/CognitiveServices.ts b/src/client/cognitive_services/CognitiveServices.ts
index ce829eb1e..3f3726621 100644
--- a/src/client/cognitive_services/CognitiveServices.ts
+++ b/src/client/cognitive_services/CognitiveServices.ts
@@ -208,7 +208,7 @@ export namespace CognitiveServices {
results.recognitionUnits && (results = results.recognitionUnits);
}
return results;
- }
+ };
}
export interface AzureStrokeData {
@@ -232,13 +232,13 @@ export namespace CognitiveServices {
return data;
},
requester: async (apiKey: string, query: string) => {
- let xhttp = new XMLHttpRequest();
- let serverAddress = "https://api.cognitive.microsoft.com";
- let endpoint = serverAddress + '/bing/v5.0/search?q=' + encodeURIComponent(query);
- let promisified = (resolve: any, reject: any) => {
+ const xhttp = new XMLHttpRequest();
+ const serverAddress = "https://api.cognitive.microsoft.com";
+ const endpoint = serverAddress + '/bing/v5.0/search?q=' + encodeURIComponent(query);
+ const promisified = (resolve: any, reject: any) => {
xhttp.onreadystatechange = function () {
if (this.readyState === 4) {
- let result = xhttp.responseText;
+ const result = xhttp.responseText;
switch (this.status) {
case 200:
return resolve(result);
@@ -266,7 +266,7 @@ export namespace CognitiveServices {
export namespace Appliers {
export const analyzer = async (query: string, converter: BingConverter) => {
- let results = await ExecuteQuery(Service.Bing, Manager, query);
+ const results = await ExecuteQuery(Service.Bing, Manager, query);
console.log("Bing results: ", results);
const { title_vals, url_vals } = await converter(results);
return { title_vals, url_vals };
@@ -281,13 +281,13 @@ export namespace CognitiveServices {
return data;
},
requester: async (apiKey: string, query: string) => {
- let xhttp = new XMLHttpRequest();
- let serverAddress = "https://babel.hathitrust.org/cgi/htd/​";
- let endpoint = serverAddress + '/bing/v5.0/search?q=' + encodeURIComponent(query);
- let promisified = (resolve: any, reject: any) => {
+ const xhttp = new XMLHttpRequest();
+ const serverAddress = "https://babel.hathitrust.org/cgi/htd/​";
+ const endpoint = serverAddress + '/bing/v5.0/search?q=' + encodeURIComponent(query);
+ const promisified = (resolve: any, reject: any) => {
xhttp.onreadystatechange = function () {
if (this.readyState === 4) {
- let result = xhttp.responseText;
+ const result = xhttp.responseText;
switch (this.status) {
case 200:
return resolve(result);
@@ -315,7 +315,7 @@ export namespace CognitiveServices {
export namespace Appliers {
export const analyzer = async (query: string, converter: BingConverter) => {
- let results = await ExecuteQuery(Service.Bing, Manager, query);
+ const results = await ExecuteQuery(Service.Bing, Manager, query);
console.log("Bing results: ", results);
const { title_vals, url_vals } = await converter(results);
return { title_vals, url_vals };
@@ -337,9 +337,9 @@ export namespace CognitiveServices {
});
},
requester: async (apiKey: string, body: string, service: Service) => {
- let serverAddress = "https://eastus.api.cognitive.microsoft.com";
- let endpoint = serverAddress + "/text/analytics/v2.1/keyPhrases";
- let sampleBody = {
+ const serverAddress = "https://eastus.api.cognitive.microsoft.com";
+ const endpoint = serverAddress + "/text/analytics/v2.1/keyPhrases";
+ const sampleBody = {
"documents": [
{
"language": "en",
@@ -348,7 +348,7 @@ export namespace CognitiveServices {
}
]
};
- let actualBody = body;
+ const actualBody = body;
const options = {
uri: endpoint,
body: actualBody,
@@ -368,12 +368,12 @@ export namespace CognitiveServices {
console.log("vectorizing...");
//keyterms = ["father", "king"];
- let args = { method: 'POST', uri: Utils.prepend("/recommender"), body: { keyphrases: keyterms }, json: true };
+ const args = { method: 'POST', uri: Utils.prepend("/recommender"), body: { keyphrases: keyterms }, json: true };
await requestPromise.post(args).then(async (wordvecs) => {
if (wordvecs) {
- let indices = Object.keys(wordvecs);
+ const indices = Object.keys(wordvecs);
console.log("successful vectorization!");
- var vectorValues = new List<number>();
+ const vectorValues = new List<number>();
indices.forEach((ind: any) => {
//console.log(wordvec.word);
vectorValues.push(wordvecs[ind]);
@@ -389,9 +389,9 @@ export namespace CognitiveServices {
}
export const analyzer = async (dataDoc: Doc, target: Doc, keys: string[], data: string, converter: TextConverter, isMainDoc: boolean = false, isInternal: boolean = true) => {
- let results = await ExecuteQuery(Service.Text, Manager, data);
+ const results = await ExecuteQuery(Service.Text, Manager, data);
console.log("Cognitive Services keyphrases: ", results);
- let { keyterms, external_recommendations, kp_string } = await converter(results, data);
+ const { keyterms, external_recommendations, kp_string } = await converter(results, data);
target[keys[0]] = keyterms;
if (isInternal) {
//await vectorize([data], dataDoc, isMainDoc);
diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts
index adc61aa80..5ec1cfdb4 100644
--- a/src/client/documents/DocumentTypes.ts
+++ b/src/client/documents/DocumentTypes.ts
@@ -9,14 +9,14 @@ export enum DocumentType {
VID = "video",
AUDIO = "audio",
PDF = "pdf",
- ICON = "icon",
IMPORT = "import",
LINK = "link",
- LINKDOC = "linkdoc",
+ LINKDB = "linkdb",
BUTTON = "button",
- TEMPLATE = "template",
+ SLIDER = "slider",
EXTENSION = "extension",
YOUTUBE = "youtube",
+ WEBCAM = "webcam",
FONTICON = "fonticonbox",
PRES = "presentation",
RECOMMENDATION = "recommendation",
@@ -28,5 +28,5 @@ export enum DocumentType {
PDFANNO = "pdfanno",
INK = "ink",
DOCUMENT = "document",
- SEARCHBOX = "searchbox",
+ SCREENSHOT = "screenshot",
} \ No newline at end of file
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 948433bd1..da8efe745 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -16,20 +16,16 @@ import { action } from "mobx";
import { ColumnAttributeModel } from "../northstar/core/attribute/AttributeModel";
import { AttributeTransformationModel } from "../northstar/core/attribute/AttributeTransformationModel";
import { AggregateFunction } from "../northstar/model/idea/idea";
-import { MINIMIZED_ICON_SIZE } from "../views/globalCssVariables.scss";
-import { IconBox } from "../views/nodes/IconBox";
-import { OmitKeys, JSONUtils } from "../../Utils";
+import { OmitKeys, JSONUtils, Utils } from "../../Utils";
import { Field, Doc, Opt, DocListCastAsync, FieldResult, DocListCast } from "../../new_fields/Doc";
import { ImageField, VideoField, AudioField, PdfField, WebField, YoutubeField } from "../../new_fields/URLField";
import { HtmlField } from "../../new_fields/HtmlField";
import { List } from "../../new_fields/List";
-import { Cast, NumCast } from "../../new_fields/Types";
-import { IconField } from "../../new_fields/IconField";
+import { Cast, NumCast, StrCast } from "../../new_fields/Types";
import { listSpec } from "../../new_fields/Schema";
import { DocServer } from "../DocServer";
import { dropActionType } from "../util/DragManager";
import { DateField } from "../../new_fields/DateField";
-import { UndoManager, undoBatch } from "../util/UndoManager";
import { YoutubeBox } from "../apis/youtube/YoutubeBox";
import { CollectionDockingView } from "../views/collections/CollectionDockingView";
import { LinkManager } from "../util/LinkManager";
@@ -37,6 +33,7 @@ import { DocumentManager } from "../util/DocumentManager";
import DirectoryImportBox from "../util/Import & Export/DirectoryImportBox";
import { Scripting } from "../util/Scripting";
import { ButtonBox } from "../views/nodes/ButtonBox";
+import { SliderBox } from "../views/nodes/SliderBox";
import { FontIconBox } from "../views/nodes/FontIconBox";
import { SchemaHeaderField } from "../../new_fields/SchemaHeaderField";
import { PresBox } from "../views/nodes/PresBox";
@@ -48,8 +45,8 @@ import { SearchBox } from "../views/search/SearchBox";
//import { PresBox } from "../views/nodes/PresBox";
//import { PresField } from "../../new_fields/PresField";
-import { LinkFollowBox } from "../views/linking/LinkFollowBox";
import { PresElementBox } from "../views/presentationview/PresElementBox";
+import { DashWebRTCVideo } from "../views/webcam/DashWebRTCVideo";
import { QueryBox } from "../views/nodes/QueryBox";
import { ColorBox } from "../views/nodes/ColorBox";
import { DocuLinkBox } from "../views/nodes/DocuLinkBox";
@@ -58,6 +55,12 @@ import { InkingStroke } from "../views/InkingStroke";
import { InkField } from "../../new_fields/InkField";
import { InkingControl } from "../views/InkingControl";
import { RichTextField } from "../../new_fields/RichTextField";
+import { extname } from "path";
+import { MessageStore } from "../../server/Message";
+import { ContextMenuProps } from "../views/ContextMenuItem";
+import { ContextMenu } from "../views/ContextMenu";
+import { LinkBox } from "../views/nodes/LinkBox";
+import { ScreenshotBox } from "../views/nodes/ScreenshotBox";
const requestImageSize = require('../util/request-image-size');
const path = require('path');
@@ -72,74 +75,89 @@ export interface DocumentOptions {
_fitWidth?: boolean;
_fitToBox?: boolean; // whether a freeformview should zoom/scale to create a shrinkwrapped view of its contents
_LODdisable?: boolean;
- _dropAction?: dropActionType;
+ _showTitleHover?: string; //
+ _showTitle?: string; // which field to display in the title area. leave empty to have no title
+ _showCaption?: string; // which field to display in the caption area. leave empty to have no caption
+ _scrollTop?: number; // scroll location for pdfs
_chromeStatus?: string;
_viewType?: number;
_gridGap?: number; // gap between items in masonry view
_xMargin?: number; // gap between left edge of document and start of masonry/stacking layouts
_yMargin?: number; // gap between top edge of dcoument and start of masonry/stacking layouts
- _textTemplate?: RichTextField; // template used by a formattedTextBox to create a text box to render
+ _xPadding?: number;
+ _yPadding?: number;
_itemIndex?: number; // which item index the carousel viewer is showing
- _hideSidebar?: boolean; //whether an annotationsidebar should be displayed for text docuemnts
+ _showSidebar?: boolean; //whether an annotationsidebar should be displayed for text docuemnts
+ _singleLine?: boolean; // whether text document is restricted to a single line (carriage returns make new document)
x?: number;
y?: number;
z?: number;
+ dropAction?: dropActionType;
+ childDropAction?: dropActionType;
layoutKey?: string;
type?: string;
title?: string;
+ style?: string;
page?: number;
scale?: number;
isDisplayPanel?: boolean; // whether the panel functions as GoldenLayout "stack" used to display documents
forceActive?: boolean;
- preventTreeViewOpen?: boolean; // ignores the treeViewOpen Doc flag which allows a treeViewItem's expande/collapse state to be independent of other views of the same document in the tree view
layout?: string | Doc;
hideHeadings?: boolean; // whether stacking view column headings should be hidden
isTemplateForField?: string; // the field key for which the containing document is a rendering template
isTemplateDoc?: boolean;
templates?: List<string>;
- backgroundColor?: string | ScriptField;
+ backgroundColor?: string | ScriptField; // background color for data doc
+ _backgroundColor?: string | ScriptField; // background color for each template layout doc ( overrides backgroundColor )
+ color?: string; // foreground color data doc
+ _color?: string; // foreground color for each template layout doc (overrides color)
+ caption?: RichTextField;
ignoreClick?: boolean;
lockedPosition?: boolean; // lock the x,y coordinates of the document so that it can't be dragged
lockedTransform?: boolean; // lock the panx,pany and scale parameters of the document so that it be panned/zoomed
opacity?: number;
defaultBackgroundColor?: string;
+ dontSelect?: boolean; // whether document decorations should be displayed when the document is selected
+ isBackground?: boolean;
+ isButton?: boolean;
columnWidth?: number;
fontSize?: number;
curPage?: number;
currentTimecode?: number; // the current timecode of a time-based document (e.g., current time of a video) value is in seconds
displayTimecode?: number; // the time that a document should be displayed (e.g., time an annotation should be displayed on a video)
- documentText?: string;
borderRounding?: string;
boxShadow?: string;
- sectionFilter?: string; // field key used to determine headings for sections in stacking and masonry views
+ dontRegisterChildren?: boolean;
+ _pivotField?: string; // field key used to determine headings for sections in stacking, masonry, pivot views
schemaColumns?: List<SchemaHeaderField>;
dockingConfig?: string;
annotationOn?: Doc;
removeDropProperties?: List<string>; // list of properties that should be removed from a document when it is dropped. e.g., a creator button may be forceActive to allow it be dragged, but the forceActive property can be removed from the dropped document
dbDoc?: Doc;
+ linkRelationship?: string; // type of relatinoship a link represents
ischecked?: ScriptField; // returns whether a font icon box is checked
activePen?: Doc; // which pen document is currently active (used as the radio button state for the 'unhecked' pen tool scripts)
onClick?: ScriptField;
onChildClick?: ScriptField; // script given to children of a collection to execute when they are clicked
onPointerDown?: ScriptField;
onPointerUp?: ScriptField;
+ dropConverter?: ScriptField; // script to run when documents are dropped on this Document.
dragFactory?: Doc; // document to create when dragging with a suitable onDragStart script
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
- clipboard?: Doc; //script to execute at start of drag operation -- e.g., when a "creator" button is dragged this script generates a different document to drop
+ clipboard?: Doc;
icon?: string;
sourcePanel?: Doc; // panel to display in 'targetContainer' as the result of a button onClick script
targetContainer?: Doc; // document whose proto will be set to 'panel' as the result of a onClick click script
- dropConverter?: ScriptField; // script to run when documents are dropped on this Document.
strokeWidth?: number;
- color?: string;
+ treeViewPreventOpen?: boolean; // ignores the treeViewOpen Doc flag which allows a treeViewItem's expand/collapse state to be independent of other views of the same document in the tree view
treeViewHideTitle?: boolean; // whether to hide the title of a tree view
+ treeViewHideHeaderFields?: boolean; // whether to hide the drop down options for tree view items.
treeViewOpen?: boolean; // whether this document is expanded in a tree view
treeViewChecked?: ScriptField; // script to call when a tree view checkbox is checked
isFacetFilter?: boolean; // whether document functions as a facet filter in a tree view
limitHeight?: number; // maximum height for newly created (eg, from pasting) text documents
// [key: string]: Opt<Field>;
pointerHack?: boolean; // for buttons, allows onClick handler to fire onPointerDown
- isExpanded?: boolean; // is linear view expanded
textTransform?: string; // is linear view expanded
letterSpacing?: string; // is linear view expanded
flexDirection?: "unset" | "row" | "column" | "row-reverse" | "column-reverse";
@@ -148,6 +166,7 @@ export interface DocumentOptions {
searchText?: string, //for searchbox
sq?: string,
fq?: string,
+ linearViewIsExpanded?: boolean; // is linear view expanded
}
class EmptyBox {
@@ -174,8 +193,8 @@ export namespace Docs {
const TemplateMap: TemplateMap = new Map([
[DocumentType.TEXT, {
- layout: { view: FormattedTextBox, dataField: data },
- options: { _height: 150, backgroundColor: "#f1efeb", defaultBackgroundColor: "#f1efeb" }
+ layout: { view: FormattedTextBox, dataField: "text" },
+ options: { _height: 150, _xMargin: 10, _yMargin: 10 }
}],
[DocumentType.HIST, {
layout: { view: HistogramBox, dataField: data },
@@ -199,7 +218,7 @@ export namespace Docs {
}],
[DocumentType.COL, {
layout: { view: CollectionView, dataField: data },
- options: { _panX: 0, _panY: 0, scale: 1, _width: 500, _height: 500 }
+ options: { _panX: 0, _panY: 0, scale: 1 } // , _width: 500, _height: 500 }
}],
[DocumentType.KVP, {
layout: { view: KeyValueBox, dataField: data },
@@ -221,17 +240,18 @@ export namespace Docs {
layout: { view: PDFBox, dataField: data },
options: { curPage: 1 }
}],
- [DocumentType.ICON, {
- layout: { view: IconBox, dataField: data },
- options: { _width: Number(MINIMIZED_ICON_SIZE), _height: Number(MINIMIZED_ICON_SIZE) },
- }],
[DocumentType.IMPORT, {
layout: { view: DirectoryImportBox, dataField: data },
options: { _height: 150 }
}],
- [DocumentType.LINKDOC, {
+ [DocumentType.LINK, {
+ layout: { view: LinkBox, dataField: data },
+ options: { _height: 150 }
+ }],
+ [DocumentType.LINKDB, {
data: new List<Doc>(),
layout: { view: EmptyBox, dataField: data },
+ options: { childDropAction: "alias", title: "LINK DB" }
}],
[DocumentType.YOUTUBE, {
layout: { view: YoutubeBox, dataField: data }
@@ -239,6 +259,9 @@ export namespace Docs {
[DocumentType.BUTTON, {
layout: { view: ButtonBox, dataField: data },
}],
+ [DocumentType.SLIDER, {
+ layout: { view: SliderBox, dataField: data },
+ }],
[DocumentType.PRES, {
layout: { view: PresBox, dataField: data },
options: {}
@@ -248,11 +271,11 @@ export namespace Docs {
options: { _width: 40, _height: 40, borderRounding: "100%" },
}],
[DocumentType.RECOMMENDATION, {
- layout: { view: RecommendationsBox },
+ layout: { view: RecommendationsBox, dataField: data },
options: { width: 200, height: 200 },
}],
- [DocumentType.LINKFOLLOW, {
- layout: { view: LinkFollowBox, dataField: data }
+ [DocumentType.WEBCAM, {
+ layout: { view: DashWebRTCVideo, dataField: data }
}],
[DocumentType.PRESELEMENT, {
layout: { view: PresElementBox, dataField: data }
@@ -261,14 +284,14 @@ export namespace Docs {
layout: { view: InkingStroke, dataField: data },
options: { backgroundColor: "transparent" }
}],
- [DocumentType.SEARCHBOX, {
- layout: { view: SearchBox, dataField:data},
- options: { width: 200, height: 200 },
+ [DocumentType.SCREENSHOT, {
+ layout: { view: ScreenshotBox, dataField: data },
+ options: {}
}]
]);
// All document prototypes are initialized with at least these values
- const defaultOptions: DocumentOptions = { x: 0, y: 0, _width: 300 };
+ const defaultOptions: DocumentOptions = { x: 0, y: 0, _width: 300 }; // bcz: do we really want to set anything here? could also try to set in render() methods for types that need a default
const suffix = "Proto";
/**
@@ -317,7 +340,7 @@ export namespace Docs {
* A collection of all links in the database. Ideally, this would be a search, but for now all links are cached here.
*/
export function MainLinkDocument() {
- return Prototypes.get(DocumentType.LINKDOC);
+ return Prototypes.get(DocumentType.LINKDB);
}
/**
@@ -347,6 +370,7 @@ export namespace Docs {
const options = { title, type, baseProto: true, ...defaultOptions, ...(template.options || {}) };
options.layout = layout.view.LayoutString(layout.dataField);
const doc = Doc.assign(new Doc(prototypeId, true), { layoutKey: "layout", ...options });
+ doc.layout_keyValue = KeyValueBox.LayoutString("");
return doc;
}
@@ -358,8 +382,62 @@ export namespace Docs {
*/
export namespace Create {
- const delegateKeys = ["x", "y", "layoutKey", "_width", "_height", "_panX", "_panY", "_viewType", "_nativeWidth", "_nativeHeight", "_dropAction", "_annotationOn",
- "_chromeStatus", "_forceActive", "_autoHeight", "_fitWidth", "_LODdisable", "_itemIndex", "_hideSidebar"];
+ export function Buxton() {
+ let responded = false;
+ const loading = new Doc;
+ loading.title = "Please wait for the import script...";
+ const parent = TreeDocument([loading], {
+ title: "The Buxton Collection",
+ _width: 400,
+ _height: 400,
+ _LODdisable: true
+ });
+ const parentProto = Doc.GetProto(parent);
+ const { _socket } = DocServer;
+ _socket.off(MessageStore.BuxtonDocumentResult.Message);
+ _socket.off(MessageStore.BuxtonImportComplete.Message);
+ Utils.AddServerHandler(_socket, MessageStore.BuxtonDocumentResult, ({ device, errors }) => {
+ if (!responded) {
+ responded = true;
+ parentProto.data = new List<Doc>();
+ }
+ if (device) {
+ const { __images } = device;
+ delete device.__images;
+ const { ImageDocument, StackingDocument } = Docs.Create;
+ const constructed = __images.map(({ url, nativeWidth, nativeHeight }) => ({ url: Utils.prepend(url), nativeWidth, nativeHeight }));
+ const deviceImages = constructed.map(({ url, nativeWidth, nativeHeight }, i) => ImageDocument(url, {
+ title: `image${i}.${extname(url)}`,
+ _nativeWidth: nativeWidth,
+ _nativeHeight: nativeHeight
+ }));
+ const doc = StackingDocument(deviceImages, { title: device.title, _LODdisable: true });
+ const deviceProto = Doc.GetProto(doc);
+ deviceProto.hero = new ImageField(constructed[0].url);
+ Docs.Get.DocumentHierarchyFromJson(device, undefined, deviceProto);
+ Doc.AddDocToList(parentProto, "data", doc);
+ } else if (errors) {
+ console.log(errors);
+ } else {
+ alert("A Buxton document import was completely empty (??)");
+ }
+ });
+ Utils.AddServerHandler(_socket, MessageStore.BuxtonImportComplete, ({ deviceCount, errorCount }) => {
+ _socket.off(MessageStore.BuxtonDocumentResult.Message);
+ _socket.off(MessageStore.BuxtonImportComplete.Message);
+ alert(`Successfully imported ${deviceCount} device${deviceCount === 1 ? "" : "s"}, with ${errorCount} error${errorCount === 1 ? "" : "s"}, in ${(Date.now() - startTime) / 1000} seconds.`);
+ });
+ const startTime = Date.now();
+ Utils.Emit(_socket, MessageStore.BeginBuxtonImport, "");
+ return parent;
+ }
+
+ Scripting.addGlobal(Buxton);
+
+ const delegateKeys = ["x", "y", "layoutKey", "_width", "_height", "_panX", "_panY", "_viewType", "_nativeWidth", "_nativeHeight", "dropAction", "childDropAction", "_annotationOn",
+ "_chromeStatus", "_forceActive", "_autoHeight", "_fitWidth", "_LODdisable", "_itemIndex", "_showSidebar", "_showTitle", "_showCaption", "_showTitleHover", "_backgroundColor",
+ "_xMargin", "_yMargin", "_xPadding", "_yPadding", "_singleLine", "_scrollTop",
+ "_color", "isButton", "isBackground", "removeDropProperties", "treeViewOpen"];
/**
* This function receives the relevant document prototype and uses
@@ -379,7 +457,7 @@ export namespace Docs {
* only when creating a DockDocument from the current user's already existing
* main document.
*/
- export function InstanceFromProto(proto: Doc, data: Field | undefined, options: DocumentOptions, delegId?: string) {
+ export function InstanceFromProto(proto: Doc, data: Field | undefined, options: DocumentOptions, delegId?: string, fieldKey: string = "data") {
const { omit: protoProps, extract: delegateProps } = OmitKeys(options, delegateKeys);
if (!("author" in protoProps)) {
@@ -392,10 +470,10 @@ export namespace Docs {
protoProps.isPrototype = true;
- const dataDoc = MakeDataDelegate(proto, protoProps, data);
+ const dataDoc = MakeDataDelegate(proto, protoProps, data, fieldKey);
const viewDoc = Doc.MakeDelegate(dataDoc, delegId);
- AudioBox.ActiveRecordings.map(d => DocUtils.MakeLink({ doc: viewDoc }, { doc: d }, "audio link", "link to audio: " + d.title));
+ viewDoc.type !== DocumentType.LINK && AudioBox.ActiveRecordings.map(d => DocUtils.MakeLink({ doc: viewDoc }, { doc: d }, "audio link", "audio timeline"));
return Doc.assign(viewDoc, delegateProps, true);
}
@@ -410,10 +488,10 @@ export namespace Docs {
* @param options initial values to apply to this new delegate
* @param value the data to store in this new delegate
*/
- function MakeDataDelegate<D extends Field>(proto: Doc, options: DocumentOptions, value?: D) {
+ function MakeDataDelegate<D extends Field>(proto: Doc, options: DocumentOptions, value?: D, fieldKey: string = "data") {
const deleg = Doc.MakeDelegate(proto);
if (value !== undefined) {
- deleg.data = value;
+ deleg[fieldKey] = value;
}
return Doc.assign(deleg, options);
}
@@ -451,8 +529,18 @@ export namespace Docs {
return InstanceFromProto(Prototypes.get(DocumentType.YOUTUBE), new YoutubeField(new URL(url)), options);
}
+ export function WebCamDocument(url: string, options: DocumentOptions = {}) {
+ return InstanceFromProto(Prototypes.get(DocumentType.WEBCAM), "", options);
+ }
+
+ export function ScreenshotDocument(url: string, options: DocumentOptions = {}) {
+ return InstanceFromProto(Prototypes.get(DocumentType.SCREENSHOT), "", options);
+ }
+
export function AudioDocument(url: string, options: DocumentOptions = {}) {
- return InstanceFromProto(Prototypes.get(DocumentType.AUDIO), new AudioField(new URL(url)), options);
+ const instance = InstanceFromProto(Prototypes.get(DocumentType.AUDIO), new AudioField(new URL(url)), options);
+ Doc.GetProto(instance).backgroundColor = ComputedField.MakeFunction("this._audioState === 'playing' ? 'green':'gray'");
+ return instance;
}
export function HistogramDocument(histoOp: HistogramOperation, options: DocumentOptions = {}) {
@@ -469,7 +557,29 @@ export namespace Docs {
}
export function TextDocument(text: string, options: DocumentOptions = {}) {
- return InstanceFromProto(Prototypes.get(DocumentType.TEXT), text, options);
+ return InstanceFromProto(Prototypes.get(DocumentType.TEXT), text, options, undefined, "text");
+ }
+
+ export function LinkDocument(source: { doc: Doc, ctx?: Doc }, target: { doc: Doc, ctx?: Doc }, options: DocumentOptions = {}, id?: string) {
+ const doc = InstanceFromProto(Prototypes.get(DocumentType.LINK), undefined, { isButton: true, treeViewHideTitle: true, treeViewOpen: false, removeDropProperties: new List(["isBackground", "isButton"]), ...options });
+ const linkDocProto = Doc.GetProto(doc);
+ linkDocProto.anchor1 = source.doc;
+ linkDocProto.anchor2 = target.doc;
+ linkDocProto.anchor1_timecode = source.doc.currentTimecode || source.doc.displayTimecode;
+ linkDocProto.anchor2_timecode = target.doc.currentTimecode || source.doc.displayTimecode;
+
+ if (linkDocProto.layout_key1 === undefined) {
+ Cast(linkDocProto.proto, Doc, null).layout_key1 = DocuLinkBox.LayoutString("anchor1");
+ Cast(linkDocProto.proto, Doc, null).layout_key2 = DocuLinkBox.LayoutString("anchor2");
+ Cast(linkDocProto.proto, Doc, null).linkBoxExcludedKeys = new List(["treeViewExpandedView", "treeViewHideTitle", "removeDropProperties", "linkBoxExcludedKeys", "treeViewOpen", "aliasNumber", "isPrototype", "lastOpened", "creationDate", "author"]);
+ Cast(linkDocProto.proto, Doc, null).layoutKey = undefined;
+ }
+
+ LinkManager.Instance.addLink(doc);
+
+ Doc.GetProto(source.doc).links = ComputedField.MakeFunction("links(this)");
+ Doc.GetProto(target.doc).links = ComputedField.MakeFunction("links(this)");
+ return doc;
}
export function InkDocument(color: string, tool: number, strokeWidth: number, points: { X: number, Y: number }[], options: DocumentOptions = {}) {
@@ -480,10 +590,6 @@ export namespace Docs {
return doc;
}
- export function IconDocument(icon: string, options: DocumentOptions = {}) {
- return InstanceFromProto(Prototypes.get(DocumentType.ICON), new IconField(icon), options);
- }
-
export function PdfDocument(url: string, options: DocumentOptions = {}) {
return InstanceFromProto(Prototypes.get(DocumentType.PDF), new PdfField(new URL(url)), options);
}
@@ -551,17 +657,21 @@ export namespace Docs {
return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", schemaColumns: new List(schemaColumns), ...options, _viewType: CollectionViewType.Schema });
}
- export function TreeDocument(documents: Array<Doc>, options: DocumentOptions) {
- return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, _viewType: CollectionViewType.Tree });
+ export function TreeDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
+ return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, _viewType: CollectionViewType.Tree }, id);
}
- export function StackingDocument(documents: Array<Doc>, options: DocumentOptions) {
- return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, _viewType: CollectionViewType.Stacking });
+ export function StackingDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
+ return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, _viewType: CollectionViewType.Stacking }, id);
}
export function MulticolumnDocument(documents: Array<Doc>, options: DocumentOptions) {
return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, _viewType: CollectionViewType.Multicolumn });
}
+ export function MultirowDocument(documents: Array<Doc>, options: DocumentOptions) {
+ return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, _viewType: CollectionViewType.Multirow });
+ }
+
export function MasonryDocument(documents: Array<Doc>, options: DocumentOptions) {
return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, _viewType: CollectionViewType.Masonry });
@@ -571,15 +681,15 @@ export namespace Docs {
return InstanceFromProto(Prototypes.get(DocumentType.BUTTON), undefined, { ...(options || {}) });
}
+ export function SliderDocument(options?: DocumentOptions) {
+ return InstanceFromProto(Prototypes.get(DocumentType.SLIDER), undefined, { ...(options || {}) });
+ }
+
export function FontIconDocument(options?: DocumentOptions) {
return InstanceFromProto(Prototypes.get(DocumentType.FONTICON), undefined, { ...(options || {}) });
}
- export function LinkFollowBoxDocument(options?: DocumentOptions) {
- return InstanceFromProto(Prototypes.get(DocumentType.LINKFOLLOW), undefined, { ...(options || {}) });
- }
-
export function PresElementBoxDocument(options?: DocumentOptions) {
return InstanceFromProto(Prototypes.get(DocumentType.PRESELEMENT), undefined, { ...(options || {}) });
}
@@ -614,7 +724,7 @@ export namespace Docs {
{
type: type,
content: [
- ...configs.map(config => CollectionDockingView.makeDocumentConfig(config.doc, undefined, config.initialWidth, config.path))
+ ...configs.map(config => CollectionDockingView.makeDocumentConfig(config.doc, config.initialWidth, config.path))
]
}
]
@@ -648,19 +758,16 @@ export namespace Docs {
* or the result of any JSON.parse() call.
* @param title an optional title to give to the highest parent document in the hierarchy
*/
- export function DocumentHierarchyFromJson(input: any, title?: string): Opt<Doc> {
+ export function DocumentHierarchyFromJson(input: any, title?: string, appendToTarget?: Doc): Opt<Doc> {
if (input === undefined || input === null || ![...primitives, "object"].includes(typeof input)) {
return undefined;
}
- let parsed = input;
- if (typeof input === "string") {
- parsed = JSONUtils.tryParse(input);
- }
+ input = JSON.parse(typeof input === "string" ? input : JSON.stringify(input));
let converted: Doc;
- if (typeof parsed === "object" && !(parsed instanceof Array)) {
- converted = convertObject(parsed, title);
+ if (typeof input === "object" && !(input instanceof Array)) {
+ converted = convertObject(input, title, appendToTarget);
} else {
- (converted = new Doc).json = toField(parsed);
+ (converted = new Doc).json = toField(input);
}
title && (converted.title = title);
return converted;
@@ -673,12 +780,12 @@ export namespace Docs {
* @returns the object mapped from JSON to field values, where each mapping
* might involve arbitrary recursion (since toField might itself call convertObject)
*/
- const convertObject = (object: any, title?: string): Doc => {
- const target = new Doc();
+ const convertObject = (object: any, title?: string, target?: Doc): Doc => {
+ const resolved = target ?? new Doc;
let result: Opt<Field>;
- Object.keys(object).map(key => (result = toField(object[key], key)) && (target[key] = result));
- title && !target.title && (target.title = title);
- return target;
+ Object.keys(object).map(key => (result = toField(object[key], key)) && (resolved[key] = result));
+ title && !resolved.title && (resolved.title = title);
+ return resolved;
};
/**
@@ -725,9 +832,6 @@ export namespace Docs {
} else if (field instanceof PdfField) {
created = Docs.Create.PdfDocument((field).url.href, resolved);
layout = PDFBox.LayoutString;
- } else if (field instanceof IconField) {
- created = Docs.Create.IconDocument((field).icon, resolved);
- layout = IconBox.LayoutString;
} else if (field instanceof AudioField) {
created = Docs.Create.AudioDocument((field).url.href, resolved);
layout = AudioBox.LayoutString;
@@ -772,7 +876,7 @@ export namespace Docs {
}
if (type.indexOf("excel") !== -1) {
ctor = Docs.Create.DBDocument;
- options._dropAction = "copy";
+ options.dropAction = "copy";
}
if (type.indexOf("html") !== -1) {
if (path.includes(window.location.hostname)) {
@@ -831,41 +935,54 @@ export namespace DocUtils {
});
}
- export function MakeLink(source: { doc: Doc, ctx?: Doc }, target: { doc: Doc, ctx?: Doc }, title: string = "", description: string = "", id?: string) {
+ export function MakeLink(source: { doc: Doc }, target: { doc: Doc }, linkRelationship: string = "", id?: string) {
const sv = DocumentManager.Instance.getDocumentView(source.doc);
if (sv && sv.props.ContainingCollectionDoc === target.doc) return;
if (target.doc === CurrentUserUtils.UserDocument) return undefined;
- const linkDocProto = new Doc(id, true);
- UndoManager.RunInBatch(() => {
- linkDocProto.type = DocumentType.LINK;
+ const linkDoc = Docs.Create.LinkDocument(source, target, { linkRelationship }, id);
+ Doc.GetProto(linkDoc).title = ComputedField.MakeFunction('this.anchor1.title +" (" + (this.linkRelationship||"to") +") " + this.anchor2.title');
- linkDocProto.title = title === "" ? source.doc.title + " to " + target.doc.title : title;
- linkDocProto.linkDescription = description;
- linkDocProto.isPrototype = true;
-
- linkDocProto.anchor1 = source.doc;
- linkDocProto.anchor2 = target.doc;
- linkDocProto.anchor1Context = source.ctx;
- linkDocProto.anchor2Context = target.ctx;
- linkDocProto.anchor1Groups = new List<Doc>([]);
- linkDocProto.anchor2Groups = new List<Doc>([]);
- linkDocProto.anchor1Timecode = source.doc.currentTimecode;
- linkDocProto.anchor2Timecode = target.doc.currentTimecode;
- linkDocProto.layout_key1 = DocuLinkBox.LayoutString("anchor1");
- linkDocProto.layout_key2 = DocuLinkBox.LayoutString("anchor2");
- linkDocProto.width = linkDocProto.height = 0;
- linkDocProto.isBackground = true;
- linkDocProto.isButton = true;
-
- LinkManager.Instance.addLink(linkDocProto);
-
- Doc.GetProto(source.doc).links = ComputedField.MakeFunction("links(this)");
- Doc.GetProto(target.doc).links = ComputedField.MakeFunction("links(this)");
- }, "make link");
- return linkDocProto;
+ Doc.GetProto(source.doc).links = ComputedField.MakeFunction("links(this)");
+ Doc.GetProto(target.doc).links = ComputedField.MakeFunction("links(this)");
+ return linkDoc;
}
+ export function addDocumentCreatorMenuItems(docTextAdder: (d: Doc) => void, docAdder: (d: Doc) => void, x: number, y: number): void {
+ ContextMenu.Instance.addItem({
+ description: "Add Note ...",
+ subitems: DocListCast((Doc.UserDoc().noteTypes as Doc).data).map((note, i) => ({
+ description: ":" + StrCast(note.title),
+ event: (args: { x: number, y: number }) => {
+ const textDoc = Docs.Create.TextDocument("", {
+ _width: 200, x, y, _autoHeight: note._autoHeight !== false,
+ title: StrCast(note.title) + "#" + (note.aliasCount = NumCast(note.aliasCount) + 1)
+ });
+ textDoc.layoutKey = "layout_" + note.title;
+ textDoc[textDoc.layoutKey] = note;
+ docTextAdder(textDoc);
+ },
+ icon: "eye"
+ })) as ContextMenuProps[],
+ icon: "eye"
+ });
+ ContextMenu.Instance.addItem({
+ description: "Add Template Doc ...",
+ subitems: DocListCast(Cast(Doc.UserDoc().expandingButtons, Doc, null)?.data).map(btnDoc => Cast(btnDoc?.dragFactory, Doc, null)).filter(doc => doc).map((dragDoc, i) => ({
+ description: ":" + StrCast(dragDoc.title),
+ event: (args: { x: number, y: number }) => {
+ const newDoc = Doc.ApplyTemplate(dragDoc);
+ if (newDoc) {
+ newDoc.x = x;
+ newDoc.y = y;
+ docAdder(newDoc);
+ }
+ },
+ icon: "eye"
+ })) as ContextMenuProps[],
+ icon: "eye"
+ });
+ }
}
Scripting.addGlobal("Docs", Docs);
diff --git a/src/client/goldenLayout.js b/src/client/goldenLayout.js
index 29b750720..b510385ff 100644
--- a/src/client/goldenLayout.js
+++ b/src/client/goldenLayout.js
@@ -2868,7 +2868,7 @@
* @type {String}
*/
lm.controls.Tab._template = '<li class="lm_tab"><i class="lm_left"></i>' +
- '<span class="lm_title"></span><div class="lm_close_tab"></div>' +
+ '<div class="lm_title_wrap"><input class="lm_title"/></div><div class="lm_close_tab"></div>' +
'<i class="lm_right"></i></li>';
lm.utils.copy(lm.controls.Tab.prototype, {
diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts
index 3394cb93d..569c1ef6d 100644
--- a/src/client/util/DictationManager.ts
+++ b/src/client/util/DictationManager.ts
@@ -326,7 +326,7 @@ export namespace DictationManager {
["open fields", {
action: (target: DocumentView) => {
const kvp = Docs.Create.KVPDocument(target.props.Document, { _width: 300, _height: 300 });
- target.props.addDocTab(kvp, target.props.DataDoc, "onRight");
+ target.props.addDocTab(kvp, "onRight");
}
}],
@@ -340,7 +340,7 @@ export namespace DictationManager {
const proseMirrorState = `{"doc":{"type":"doc","content":[{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"type":"text","text":"${prompt}"}]}]}]}]},"selection":{"type":"text","anchor":${anchor},"head":${head}}}`;
proto.data = new RichTextField(proseMirrorState);
proto.backgroundColor = "#eeffff";
- target.props.addDocTab(newBox, proto, "onRight");
+ target.props.addDocTab(newBox, "onRight");
}
}]
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index fb4c2155a..e0ffaf7e0 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -1,5 +1,5 @@
import { action, computed, observable } from 'mobx';
-import { Doc, DocListCastAsync, DocListCast } from '../../new_fields/Doc';
+import { Doc, DocListCastAsync, DocListCast, Opt } from '../../new_fields/Doc';
import { Id } from '../../new_fields/FieldSymbols';
import { List } from '../../new_fields/List';
import { Cast, NumCast, StrCast } from '../../new_fields/Types';
@@ -28,7 +28,6 @@ export class DocumentManager {
//private constructor so no other class can create a nodemanager
private constructor() {
- // this.DocumentViews = new Array<DocumentView>();
}
//gets all views
@@ -55,7 +54,7 @@ export class DocumentManager {
}
public getDocumentViewById(id: string, preferredCollection?: CollectionView): DocumentView | undefined {
-
+ if (!id) return undefined;
let toReturn: DocumentView | undefined;
const passes = preferredCollection ? [preferredCollection, undefined] : [undefined];
@@ -85,17 +84,24 @@ export class DocumentManager {
return this.getDocumentViewById(toFind[Id], preferredCollection);
}
- public getFirstDocumentView(toFind: Doc): DocumentView | undefined {
+ public getFirstDocumentView(toFind: Doc, originatingDoc: Opt<Doc> = undefined): DocumentView | undefined {
const views = this.getDocumentViews(toFind);
- return views.length ? views[0] : undefined;
+ return views?.find(view => view.props.Document !== originatingDoc);
}
public getDocumentViews(toFind: Doc): DocumentView[] {
const toReturn: DocumentView[] = [];
+ // heurstic to return the "best" documents first:
+ // choose an exact match over an alias match
+ // choose documents that have a PanelWidth() over those that don't (the treeview documents have no panelWidth)
+ DocumentManager.Instance.DocumentViews.map(view =>
+ view.props.Document.presBox === undefined && view.props.PanelWidth() > 1 && view.props.Document === toFind && toReturn.push(view));
DocumentManager.Instance.DocumentViews.map(view =>
- view.props.Document === toFind && toReturn.push(view));
+ view.props.Document.presBox === undefined && view.props.PanelWidth() <= 1 && view.props.Document === toFind && toReturn.push(view));
DocumentManager.Instance.DocumentViews.map(view =>
- view.props.Document !== toFind && Doc.AreProtosEqual(view.props.Document, toFind) && toReturn.push(view));
+ view.props.Document.presBox === undefined && view.props.PanelWidth() > 1 && view.props.Document !== toFind && Doc.AreProtosEqual(view.props.Document, toFind) && toReturn.push(view));
+ DocumentManager.Instance.DocumentViews.map(view =>
+ view.props.Document.presBox === undefined && view.props.PanelWidth() <= 1 && view.props.Document !== toFind && Doc.AreProtosEqual(view.props.Document, toFind) && toReturn.push(view));
return toReturn;
}
@@ -127,20 +133,20 @@ export class DocumentManager {
return pairs;
}
- public jumpToDocument = async (targetDoc: Doc, willZoom: boolean, dockFunc?: (doc: Doc) => void, docContext?: Doc, linkId?: string, closeContextIfNotFound: boolean = false): Promise<void> => {
+ public jumpToDocument = async (targetDoc: Doc, willZoom: boolean, dockFunc?: (doc: Doc) => void, docContext?: Doc, linkId?: string, closeContextIfNotFound: boolean = false, originatingDoc: Opt<Doc> = undefined): Promise<void> => {
const highlight = () => {
const finalDocView = DocumentManager.Instance.getFirstDocumentView(targetDoc);
finalDocView && (finalDocView.Document.scrollToLinkID = linkId);
finalDocView && Doc.linkFollowHighlight(finalDocView.props.Document);
};
- const docView = DocumentManager.Instance.getFirstDocumentView(targetDoc);
- let annotatedDoc = await Cast(docView?.props.Document.annotationOn, Doc);
+ const docView = DocumentManager.Instance.getFirstDocumentView(targetDoc, originatingDoc);
+ let annotatedDoc = await Cast(targetDoc.annotationOn, Doc);
if (annotatedDoc) {
const first = DocumentManager.Instance.getFirstDocumentView(annotatedDoc);
if (first) annotatedDoc = first.props.Document;
}
if (docView) { // we have a docView already and aren't forced to create a new one ... just focus on the document. TODO move into view if necessary otherwise just highlight?
- docView.props.focus(docView.props.Document, false);
+ docView.props.focus(docView.props.Document, willZoom);
highlight();
} else {
const contextDocs = docContext ? await DocListCastAsync(docContext.data) : undefined;
@@ -148,7 +154,7 @@ export class DocumentManager {
const targetDocContext = (annotatedDoc ? annotatedDoc : contextDoc);
if (!targetDocContext) { // we don't have a view and there's no context specified ... create a new view of the target using the dockFunc or default
- (dockFunc || CollectionDockingView.AddRightSplit)(Doc.BrushDoc(Doc.MakeAlias(targetDoc)), undefined);
+ (dockFunc || CollectionDockingView.AddRightSplit)(Doc.BrushDoc(Doc.MakeAlias(targetDoc)));
highlight();
} else {
const targetDocContextView = DocumentManager.Instance.getFirstDocumentView(targetDocContext);
@@ -158,18 +164,22 @@ export class DocumentManager {
targetDocContextView.props.focus(targetDocContextView.props.Document, willZoom);
// now find the target document within the context
- setTimeout(() => {
- const retryDocView = DocumentManager.Instance.getDocumentView(targetDoc);
- if (retryDocView) {
- retryDocView.props.focus(targetDoc, willZoom); // focus on the target if it now exists in the context
- } else {
- if (closeContextIfNotFound && targetDocContextView.props.removeDocument) targetDocContextView.props.removeDocument(targetDocContextView.props.Document);
- targetDoc.layout && (dockFunc || CollectionDockingView.AddRightSplit)(Doc.BrushDoc(Doc.MakeAlias(targetDoc)), undefined); // otherwise create a new view of the target
- }
- highlight();
- }, 0);
+ if (targetDoc.displayTimecode) { // the target should show up once the video scrubs to the display timecode;
+ targetDocContext.currentTimecode = targetDoc.displayTimecode;
+ } else {
+ setTimeout(() => {
+ const retryDocView = DocumentManager.Instance.getDocumentView(targetDoc);
+ if (retryDocView) {
+ retryDocView.props.focus(targetDoc, willZoom); // focus on the target if it now exists in the context
+ } else {
+ if (closeContextIfNotFound && targetDocContextView.props.removeDocument) targetDocContextView.props.removeDocument(targetDocContextView.props.Document);
+ targetDoc.layout && (dockFunc || CollectionDockingView.AddRightSplit)(Doc.BrushDoc(Doc.MakeAlias(targetDoc))); // otherwise create a new view of the target
+ }
+ highlight();
+ }, 0);
+ }
} else { // there's no context view so we need to create one first and try again
- (dockFunc || CollectionDockingView.AddRightSplit)(targetDocContext, undefined);
+ (dockFunc || CollectionDockingView.AddRightSplit)(targetDocContext);
setTimeout(() => {
const finalDocView = DocumentManager.Instance.getFirstDocumentView(targetDoc);
const finalDocContextView = DocumentManager.Instance.getFirstDocumentView(targetDocContext);
@@ -192,14 +202,19 @@ export class DocumentManager {
const second = secondDocWithoutView ? [secondDocWithoutView] : secondDocs;
const linkDoc = first.length ? first[0] : second.length ? second[0] : undefined;
const linkFollowDocs = first.length ? [await first[0].anchor2 as Doc, await first[0].anchor1 as Doc] : second.length ? [await second[0].anchor1 as Doc, await second[0].anchor2 as Doc] : undefined;
- const linkFollowDocContexts = first.length ? [await first[0].anchor2Context as Doc, await first[0].anchor1Context as Doc] : second.length ? [await second[0].anchor1Context as Doc, await second[0].anchor2Context as Doc] : [undefined, undefined];
- const linkFollowTimecodes = first.length ? [NumCast(first[0].anchor2Timecode), NumCast(first[0].anchor1Timecode)] : second.length ? [NumCast(second[0].anchor1Timecode), NumCast(second[0].anchor2Timecode)] : [undefined, undefined];
+ const linkFollowDocContexts = first.length ? [await first[0].context as Doc, await first[0].context as Doc] : second.length ? [await second[0].context as Doc, await second[0].context as Doc] : [undefined, undefined];
+ const linkFollowTimecodes = first.length ? [NumCast(first[0].anchor2_timecode), NumCast(first[0].anchor1_timecode)] : second.length ? [NumCast(second[0].anchor1_timecode), NumCast(second[0].anchor2_timecode)] : [undefined, undefined];
if (linkFollowDocs && linkDoc) {
- const maxLocation = StrCast(linkFollowDocs[0].maximizeLocation, "inTab");
+ const maxLocation = StrCast(linkDoc.maximizeLocation, "inTab");
const targetContext = !Doc.AreProtosEqual(linkFollowDocContexts[reverse ? 1 : 0], currentContext) ? linkFollowDocContexts[reverse ? 1 : 0] : undefined;
const target = linkFollowDocs[reverse ? 1 : 0];
- target.currentTimecode !== undefined && (target.currentTimecode = linkFollowTimecodes[reverse ? 1 : 0]);
- DocumentManager.Instance.jumpToDocument(linkFollowDocs[reverse ? 1 : 0], zoom, (doc: Doc) => focus(doc, maxLocation), targetContext, linkDoc[Id]);
+ const annotatedDoc = await Cast(target.annotationOn, Doc);
+ if (annotatedDoc) {
+ annotatedDoc.currentTimecode !== undefined && (target.currentTimecode = linkFollowTimecodes[reverse ? 1 : 0]);
+ } else {
+ target.currentTimecode !== undefined && (target.currentTimecode = linkFollowTimecodes[reverse ? 1 : 0]);
+ }
+ DocumentManager.Instance.jumpToDocument(linkFollowDocs[reverse ? 1 : 0], zoom, (doc: Doc) => focus(doc, maxLocation), targetContext, linkDoc[Id], undefined, doc);
} else if (link) {
DocumentManager.Instance.jumpToDocument(link, zoom, (doc: Doc) => focus(doc, "onRight"), undefined, undefined);
}
@@ -208,7 +223,7 @@ export class DocumentManager {
@action
zoomIntoScale = (docDelegate: Doc, scale: number) => {
const docView = DocumentManager.Instance.getDocumentView(Doc.GetProto(docDelegate));
- docView && docView.props.zoomToScale(scale);
+ docView?.props.zoomToScale(scale);
}
getScaleOfDocView = (docDelegate: Doc) => {
@@ -221,34 +236,5 @@ export class DocumentManager {
return 1;
}
}
-
- @action
- animateBetweenPoint = (scrpt: number[], expandedDocs: Doc[] | undefined): void => {
- expandedDocs && expandedDocs.map(expDoc => {
- if (expDoc.isMinimized || expDoc.isAnimating === "min") { // MAXIMIZE DOC
- if (expDoc.isMinimized) { // docs are never actaully at the minimized location. so when we unminimize one, we have to set our overrides to make it look like it was at the minimize location
- expDoc.isMinimized = false;
- expDoc.animateToPos = new List<number>([...scrpt, 0]);
- expDoc.animateToDimensions = new List<number>([0, 0]);
- }
- setTimeout(() => {
- expDoc.isAnimating = "max";
- expDoc.animateToPos = new List<number>([0, 0, 1]);
- expDoc.animateToDimensions = new List<number>([NumCast(expDoc.width), NumCast(expDoc.height)]);
- setTimeout(() => expDoc.isAnimating === "max" && (expDoc.isAnimating = expDoc.animateToPos = expDoc.animateToDimensions = undefined), 600);
- }, 0);
- } else { // MINIMIZE DOC
- expDoc.isAnimating = "min";
- expDoc.animateToPos = new List<number>([...scrpt, 0]);
- expDoc.animateToDimensions = new List<number>([0, 0]);
- setTimeout(() => {
- if (expDoc.isAnimating === "min") {
- expDoc.isMinimized = true;
- expDoc.isAnimating = expDoc.animateToPos = expDoc.animateToDimensions = undefined;
- }
- }, 600);
- }
- });
- }
}
Scripting.addGlobal(function focus(doc: any) { DocumentManager.Instance.getDocumentViews(Doc.GetProto(doc)).map(view => view.props.focus(doc, true)); }); \ No newline at end of file
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index deb2da939..07cd2e98f 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -7,15 +7,18 @@ import { DocumentManager } from "./DocumentManager";
import { LinkManager } from "./LinkManager";
import { SelectionManager } from "./SelectionManager";
import { SchemaHeaderField } from "../../new_fields/SchemaHeaderField";
-import { Docs } from "../documents/Documents";
+import { Docs, DocUtils } from "../documents/Documents";
import { ScriptField } from "../../new_fields/ScriptField";
import { List } from "../../new_fields/List";
import { PrefetchProxy } from "../../new_fields/Proxy";
import { listSpec } from "../../new_fields/Schema";
import { Scripting } from "./Scripting";
import { convertDropDataToButtons } from "./DropConverter";
+import { AudioBox } from "../views/nodes/AudioBox";
+import { DateField } from "../../new_fields/DateField";
+import { DocumentView } from "../views/nodes/DocumentView";
-export type dropActionType = "alias" | "copy" | undefined;
+export type dropActionType = "place" | "alias" | "copy" | undefined;
export function SetupDrag(
_reference: React.RefObject<HTMLElement>,
docFunc: () => Doc | Promise<Doc> | undefined,
@@ -130,11 +133,11 @@ export namespace DragManager {
dontHideOnDrop?: boolean;
offset: number[];
dropAction: dropActionType;
+ removeDropProperties?: string[];
userDropAction: dropActionType;
embedDoc?: boolean;
moveDocument?: MoveFunction;
isSelectionMove?: boolean; // indicates that an explicitly selected Document is being dragged. this will suppress onDragStart scripts
- applyAsTemplate?: boolean;
}
export class LinkDragData {
constructor(linkSourceDoc: Doc) {
@@ -144,6 +147,7 @@ export namespace DragManager {
linkSourceDocument: Doc;
dontClearTextBox?: boolean;
linkDocument?: Doc;
+ linkDropCallback?: (data: LinkDragData) => void;
}
export class ColumnDragData {
constructor(colKey: SchemaHeaderField) {
@@ -180,7 +184,7 @@ export namespace DragManager {
);
}
element.dataset.canDrop = "true";
- const handler = (e: Event) => dropFunc(e, (e as CustomEvent<DropEvent>).detail);
+ const handler = (e: Event) => { dropFunc(e, (e as CustomEvent<DropEvent>).detail); };
element.addEventListener("dashOnDrop", handler);
return () => {
element.removeEventListener("dashOnDrop", handler);
@@ -190,14 +194,20 @@ export namespace DragManager {
// drag a document and drop it (or make an alias/copy on drop)
export function StartDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions) {
+ const addAudioTag = (dropDoc: any) => {
+ dropDoc && !dropDoc.creationDate && (dropDoc.creationDate = new DateField);
+ dropDoc instanceof Doc && AudioBox.ActiveRecordings.map(d => DocUtils.MakeLink({ doc: dropDoc }, { doc: d }, "audio link", "audio timeline"));
+ return dropDoc;
+ };
const finishDrag = (e: DragCompleteEvent) => {
e.docDragData && (e.docDragData.droppedDocuments =
- dragData.draggedDocuments.map(d => !dragData.isSelectionMove && !dragData.userDropAction && ScriptCast(d.onDragStart) ? ScriptCast(d.onDragStart).script.run({ this: d }).result :
+ dragData.draggedDocuments.map(d => !dragData.isSelectionMove && !dragData.userDropAction && ScriptCast(d.onDragStart) ? addAudioTag(ScriptCast(d.onDragStart).script.run({ this: d }).result) :
dragData.userDropAction === "alias" || (!dragData.userDropAction && dragData.dropAction === "alias") ? Doc.MakeAlias(d) :
dragData.userDropAction === "copy" || (!dragData.userDropAction && dragData.dropAction === "copy") ? Doc.MakeCopy(d, true) : d)
);
e.docDragData?.droppedDocuments.forEach((drop: Doc, i: number) =>
- Cast(dragData.draggedDocuments[i].removeDropProperties, listSpec("string"), []).map(prop => drop[prop] = undefined));
+ (dragData?.removeDropProperties || []).concat(Cast(dragData.draggedDocuments[i].removeDropProperties, listSpec("string"), [])).map(prop => drop[prop] = undefined)
+ );
};
dragData.draggedDocuments.map(d => d.dragFactory); // does this help? trying to make sure the dragFactory Doc is loaded
StartDrag(eles, dragData, downX, downY, options, finishDrag);
@@ -206,10 +216,9 @@ export namespace DragManager {
// drag a button template and drop a new button
export function StartButtonDrag(eles: HTMLElement[], script: string, title: string, vars: { [name: string]: Field }, params: string[], initialize: (button: Doc) => void, downX: number, downY: number, options?: DragOptions) {
const finishDrag = (e: DragCompleteEvent) => {
- const bd = Docs.Create.ButtonDocument({ _width: 150, _height: 50, title: title });
- bd.onClick = ScriptField.MakeScript(script);
+ const bd = Docs.Create.ButtonDocument({ _width: 150, _height: 50, title, isButton: true, onClick: ScriptField.MakeScript(script) });
params.map(p => Object.keys(vars).indexOf(p) !== -1 && (Doc.GetProto(bd)[p] = new PrefetchProxy(vars[p] as Doc)));
- initialize && initialize(bd);
+ initialize?.(bd);
bd.buttonParams = new List<string>(params);
e.docDragData && (e.docDragData.droppedDocuments = [bd]);
};
@@ -217,7 +226,7 @@ export namespace DragManager {
}
// drag links and drop link targets (aliasing them if needed)
- export async function StartLinkTargetsDrag(dragEle: HTMLElement, downX: number, downY: number, sourceDoc: Doc, specificLinks?: Doc[]) {
+ export async function StartLinkTargetsDrag(dragEle: HTMLElement, docView: DocumentView, downX: number, downY: number, sourceDoc: Doc, specificLinks?: Doc[]) {
const draggedDocs = (specificLinks ? specificLinks : DocListCast(sourceDoc.links)).map(link => LinkManager.Instance.getOppositeAnchor(link, sourceDoc)).filter(l => l) as Doc[];
if (draggedDocs.length) {
@@ -229,12 +238,11 @@ export namespace DragManager {
const dragData = new DragManager.DocumentDragData(moddrag.length ? moddrag : draggedDocs);
dragData.moveDocument = (doc: Doc, targetCollection: Doc | undefined, addDocument: (doc: Doc) => boolean): boolean => {
- const document = SelectionManager.SelectedDocuments()[0];
- document && document.props.removeDocument && document.props.removeDocument(doc);
+ docView.props.removeDocument?.(doc);
addDocument(doc);
return true;
};
- const containingView = SelectionManager.SelectedDocuments()[0] ? SelectionManager.SelectedDocuments()[0].props.ContainingCollectionView : undefined;
+ const containingView = docView.props.ContainingCollectionView;
const finishDrag = (e: DragCompleteEvent) =>
e.docDragData && (e.docDragData.droppedDocuments =
dragData.draggedDocuments.reduce((droppedDocs, d) => {
@@ -266,6 +274,10 @@ export namespace DragManager {
StartDrag([ele], dragData, downX, downY, options);
}
+ export function StartImgDrag(ele: HTMLElement, downX: number, downY: number) {
+ StartDrag([ele], {}, downX, downY);
+ }
+
function StartDrag(eles: HTMLElement[], dragData: { [id: string]: any }, downX: number, downY: number, options?: DragOptions, finishDrag?: (dropData: DragCompleteEvent) => void) {
console.log("drag");
eles = eles.filter(e => e);
@@ -340,7 +352,7 @@ export namespace DragManager {
const moveHandler = (e: PointerEvent) => {
e.preventDefault(); // required or dragging text menu link item ends up dragging the link button as native drag/drop
if (dragData instanceof DocumentDragData) {
- dragData.userDropAction = e.ctrlKey ? "alias" : undefined;
+ dragData.userDropAction = e.ctrlKey && e.altKey ? "copy" : e.ctrlKey ? "alias" : undefined;
}
if (e.shiftKey && CollectionDockingView.Instance && dragData.droppedDocuments.length === 1) {
AbortDrag();
@@ -381,8 +393,8 @@ export namespace DragManager {
hideDragShowOriginalElements();
dispatchDrag(eles, e, dragData, options, finishDrag);
SelectionManager.SetIsDragging(false);
- options?.dragComplete?.(new DragCompleteEvent(false, dragData));
endDrag();
+ options?.dragComplete?.(new DragCompleteEvent(false, dragData));
};
document.addEventListener("pointermove", moveHandler, true);
document.addEventListener("pointerup", upHandler);
diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts
index d0f1d86cb..69dc303cd 100644
--- a/src/client/util/DropConverter.ts
+++ b/src/client/util/DropConverter.ts
@@ -1,5 +1,5 @@
import { DragManager } from "./DragManager";
-import { Doc, DocListCast } from "../../new_fields/Doc";
+import { Doc, DocListCast, Opt } from "../../new_fields/Doc";
import { DocumentType } from "../documents/DocumentTypes";
import { ObjectField } from "../../new_fields/ObjectField";
import { StrCast } from "../../new_fields/Types";
@@ -8,39 +8,57 @@ import { ScriptField, ComputedField } from "../../new_fields/ScriptField";
import { RichTextField } from "../../new_fields/RichTextField";
import { ImageField } from "../../new_fields/URLField";
-export function makeTemplate(doc: Doc): boolean {
+//
+// converts 'doc' into a template that can be used to render other documents.
+// the title of doc is used to determine which field is being templated, so
+// passing a value for 'rename' allows the doc to be given a meangingful name
+// after it has been converted to
+export function makeTemplate(doc: Doc, first: boolean = true, rename: Opt<string> = undefined): boolean {
const layoutDoc = doc.layout instanceof Doc && doc.layout.isTemplateForField ? doc.layout : doc;
+ if (layoutDoc.layout instanceof Doc) { // its already a template
+ return true;
+ }
const layout = StrCast(layoutDoc.layout).match(/fieldKey={'[^']*'}/)![0];
const fieldKey = layout.replace("fieldKey={'", "").replace(/'}$/, "");
const docs = DocListCast(layoutDoc[fieldKey]);
let any = false;
docs.forEach(d => {
if (!StrCast(d.title).startsWith("-")) {
- any = Doc.MakeMetadataFieldTemplate(d, Doc.GetProto(layoutDoc)) || any;
+ const params = StrCast(d.title).match(/\(([a-zA-Z0-9._\-]*)\)/)?.[1].replace("()", "");
+ if (params) {
+ any = makeTemplate(d, false) || any;
+ d.PARAMS = params;
+ } else {
+ any = Doc.MakeMetadataFieldTemplate(d, Doc.GetProto(layoutDoc)) || any;
+ }
} else if (d.type === DocumentType.COL || d.data instanceof RichTextField) {
- any = makeTemplate(d) || any;
+ any = makeTemplate(d, false) || any;
}
});
+ if (!docs.length && first) {
+ any = Doc.MakeMetadataFieldTemplate(doc, Doc.GetProto(layoutDoc)) || any;
+ }
if (layoutDoc[fieldKey] instanceof RichTextField || layoutDoc[fieldKey] instanceof ImageField) {
if (!StrCast(layoutDoc.title).startsWith("-")) {
any = Doc.MakeMetadataFieldTemplate(layoutDoc, Doc.GetProto(layoutDoc));
}
}
+ rename && (doc.title = rename);
return any;
}
export function convertDropDataToButtons(data: DragManager.DocumentDragData) {
data && data.draggedDocuments.map((doc, i) => {
let dbox = doc;
// bcz: isButtonBar is intended to allow a collection of linear buttons to be dropped and nested into another collection of buttons... it's not being used yet, and isn't very elegant
- if (!doc.onDragStart && !doc.onClick && !doc.isButtonBar) {
+ if (!doc.onDragStart && !doc.isButtonBar) {
const layoutDoc = doc.layout instanceof Doc && doc.layout.isTemplateForField ? doc.layout : doc;
if (layoutDoc.type === DocumentType.COL || layoutDoc.type === DocumentType.TEXT || layoutDoc.type === DocumentType.IMG) {
- makeTemplate(layoutDoc);
+ !layoutDoc.isTemplateDoc && makeTemplate(layoutDoc);
} else {
(layoutDoc.layout instanceof Doc) && !data.userDropAction;
}
layoutDoc.isTemplateDoc = true;
- dbox = Docs.Create.FontIconDocument({ _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, backgroundColor: StrCast(doc.backgroundColor), title: "Custom", icon: layoutDoc.isTemplateDoc ? "font" : "bolt" });
+ dbox = Docs.Create.FontIconDocument({ _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, backgroundColor: StrCast(doc.backgroundColor), title: StrCast(layoutDoc.title), icon: layoutDoc.isTemplateDoc ? "font" : "bolt" });
dbox.dragFactory = layoutDoc;
dbox.removeDropProperties = doc.removeDropProperties instanceof ObjectField ? ObjectField.MakeCopy(doc.removeDropProperties) : undefined;
dbox.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)');
diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index 071015193..3d8bcbab7 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx
@@ -22,7 +22,7 @@ import "./DirectoryImportBox.scss";
import { Networking } from "../../Network";
import { BatchedArray } from "array-batcher";
import * as path from 'path';
-import { AcceptibleMedia } from "../../../server/SharedMediaTypes";
+import { AcceptibleMedia, Upload } from "../../../server/SharedMediaTypes";
const unsupported = ["text/html", "text/plain"];
@@ -107,21 +107,22 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
runInAction(() => this.phase = `Internal: uploading ${this.quota - this.completed} files to Dash...`);
const batched = BatchedArray.from(validated, { batchSize: 15 });
- const uploads = await batched.batchedMapAsync<any>(async (batch, collector) => {
- const formData = new FormData();
-
+ const uploads = await batched.batchedMapAsync<Upload.FileResponse<Upload.ImageInformation>>(async (batch, collector) => {
batch.forEach(file => {
sizes.push(file.size);
modifiedDates.push(file.lastModified);
- formData.append(Utils.GenerateGuid(), file);
});
-
- collector.push(...(await Networking.PostFormDataToServer("/uploadFormData", formData)));
+ collector.push(...(await Networking.UploadFilesToServer<Upload.ImageInformation>(batch)));
runInAction(() => this.completed += batch.length);
});
- await Promise.all(uploads.map(async ({ name, type, clientAccessPath, exifData }) => {
- const path = Utils.prepend(clientAccessPath);
+ await Promise.all(uploads.map(async response => {
+ const { source: { type }, result } = response;
+ if (result instanceof Error) {
+ return;
+ }
+ const { accessPaths, exifData } = result;
+ const path = Utils.prepend(accessPaths.agnostic.client);
const document = await Docs.Get.DocumentFromType(type, path, { _width: 300, title: name });
const { data, error } = exifData;
if (document) {
diff --git a/src/client/util/Import & Export/ImageUtils.ts b/src/client/util/Import & Export/ImageUtils.ts
index ff909cc6b..ab8c73d15 100644
--- a/src/client/util/Import & Export/ImageUtils.ts
+++ b/src/client/util/Import & Export/ImageUtils.ts
@@ -24,7 +24,7 @@ export namespace ImageUtils {
const proto = Doc.GetProto(document);
proto["data-nativeWidth"] = nativeWidth;
proto["data-nativeHeight"] = nativeHeight;
- proto.contentSize = contentSize;
+ proto.contentSize = contentSize ? contentSize : undefined;
return data !== undefined;
};
diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx
index 184e37ba5..2eec02a42 100644
--- a/src/client/util/InteractionUtils.tsx
+++ b/src/client/util/InteractionUtils.tsx
@@ -1,3 +1,5 @@
+import React = require("react");
+
export namespace InteractionUtils {
export const MOUSETYPE = "mouse";
export const TOUCHTYPE = "touch";
@@ -90,8 +92,22 @@ export namespace InteractionUtils {
return myTouches;
}
+ // TODO: find a way to reference this function from InkingStroke instead of copy pastign here. copied bc of weird error when on mobile view
+ export function CreatePolyline(points: { X: number, Y: number }[], left: number, top: number, color: string, width: number) {
+ const pts = points.reduce((acc: string, pt: { X: number, Y: number }) => acc + `${pt.X - left},${pt.Y - top} `, "");
+ return (
+ <polyline
+ points={pts}
+ style={{
+ fill: "none",
+ stroke: color,
+ strokeWidth: width
+ }}
+ />
+ );
+ }
+
export function IsType(e: PointerEvent | React.PointerEvent, type: string): boolean {
- console.log(e.button);
switch (type) {
// pen and eraser are both pointer type 'pen', but pen is button 0 and eraser is button 5. -syip2
case PENTYPE:
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index 5f3667acc..4457f41e2 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -40,7 +40,7 @@ export class LinkManager {
public getAllLinks(): Doc[] {
const ldoc = LinkManager.Instance.LinkManagerDoc;
if (ldoc) {
- const docs = DocListCast(ldoc.allLinks);
+ const docs = DocListCast(ldoc.data);
return docs;
}
return [];
@@ -50,7 +50,7 @@ export class LinkManager {
const linkList = LinkManager.Instance.getAllLinks();
linkList.push(linkDoc);
if (LinkManager.Instance.LinkManagerDoc) {
- LinkManager.Instance.LinkManagerDoc.allLinks = new List<Doc>(linkList);
+ LinkManager.Instance.LinkManagerDoc.data = new List<Doc>(linkList);
return true;
}
return false;
@@ -62,7 +62,7 @@ export class LinkManager {
if (index > -1) {
linkList.splice(index, 1);
if (LinkManager.Instance.LinkManagerDoc) {
- LinkManager.Instance.LinkManagerDoc.allLinks = new List<Doc>(linkList);
+ LinkManager.Instance.LinkManagerDoc.data = new List<Doc>(linkList);
return true;
}
}
@@ -135,35 +135,13 @@ export class LinkManager {
return DocListCast(linkDoc.anchor2Groups);
}
}
-
- // sets the groups of the given anchor in the given link
- public setAnchorGroups(linkDoc: Doc, anchor: Doc, groups: Doc[]) {
- if (Doc.AreProtosEqual(anchor, Cast(linkDoc.anchor1, Doc, null))) {
- linkDoc.anchor1Groups = new List<Doc>(groups);
- } else {
- linkDoc.anchor2Groups = new List<Doc>(groups);
- }
- }
-
public addGroupToAnchor(linkDoc: Doc, anchor: Doc, groupDoc: Doc, replace: boolean = false) {
- const groups = LinkManager.Instance.getAnchorGroups(linkDoc, anchor);
- const index = groups.findIndex(gDoc => {
- return StrCast(groupDoc.type).toUpperCase() === StrCast(gDoc.type).toUpperCase();
- });
- if (index > -1 && replace) {
- groups[index] = groupDoc;
- }
- if (index === -1) {
- groups.push(groupDoc);
- }
- LinkManager.Instance.setAnchorGroups(linkDoc, anchor, groups);
+ linkDoc.linkRelationship = groupDoc.linkRelationship;
}
// removes group doc of given group type only from given anchor on given link
public removeGroupFromAnchor(linkDoc: Doc, anchor: Doc, groupType: string) {
- const groups = LinkManager.Instance.getAnchorGroups(linkDoc, anchor);
- const newGroups = groups.filter(groupDoc => StrCast(groupDoc.type).toUpperCase() !== groupType.toUpperCase());
- LinkManager.Instance.setAnchorGroups(linkDoc, anchor, newGroups);
+ linkDoc.linkRelationship = "-ungrouped-";
}
// returns map of group type to anchor's links in that group type
@@ -171,19 +149,10 @@ export class LinkManager {
const related = this.getAllRelatedLinks(anchor);
const anchorGroups = new Map<string, Array<Doc>>();
related.forEach(link => {
- const groups = LinkManager.Instance.getAnchorGroups(link, anchor);
-
- if (groups.length > 0) {
- groups.forEach(groupDoc => {
- const groupType = StrCast(groupDoc.type);
- if (groupType === "") {
- const group = anchorGroups.get("*");
- anchorGroups.set("*", group ? [...group, link] : [link]);
- } else {
- const group = anchorGroups.get(groupType);
- anchorGroups.set(groupType, group ? [...group, link] : [link]);
- }
- });
+ if (!link.linkRelationship || link?.linkRelationship !== "-ungrouped-") {
+ const group = anchorGroups.get(StrCast(link.linkRelationship));
+ anchorGroups.set(StrCast(link.linkRelationship), group ? [...group, link] : [link]);
+
} else {
// if link is in no groups then put it in default group
const group = anchorGroups.get("*");
@@ -215,10 +184,7 @@ export class LinkManager {
const md: Doc[] = [];
const allLinks = LinkManager.Instance.getAllLinks();
allLinks.forEach(linkDoc => {
- const anchor1Groups = LinkManager.Instance.getAnchorGroups(linkDoc, Cast(linkDoc.anchor1, Doc, null));
- const anchor2Groups = LinkManager.Instance.getAnchorGroups(linkDoc, Cast(linkDoc.anchor2, Doc, null));
- anchor1Groups.forEach(groupDoc => { if (StrCast(groupDoc.type).toUpperCase() === groupType.toUpperCase()) { const meta = Cast(groupDoc.metadata, Doc, null); meta && md.push(meta); } });
- anchor2Groups.forEach(groupDoc => { if (StrCast(groupDoc.type).toUpperCase() === groupType.toUpperCase()) { const meta = Cast(groupDoc.metadata, Doc, null); meta && md.push(meta); } });
+ if (StrCast(linkDoc.linkRelationship).toUpperCase() === groupType.toUpperCase()) { md.push(linkDoc); }
});
return md;
}
diff --git a/src/client/util/ProsemirrorExampleTransfer.ts b/src/client/util/ProsemirrorExampleTransfer.ts
index da3815181..42247f177 100644
--- a/src/client/util/ProsemirrorExampleTransfer.ts
+++ b/src/client/util/ProsemirrorExampleTransfer.ts
@@ -6,6 +6,11 @@ import { liftListItem, sinkListItem } from "./prosemirrorPatches.js";
import { splitListItem, wrapInList, } from "prosemirror-schema-list";
import { EditorState, Transaction, TextSelection } from "prosemirror-state";
import { SelectionManager } from "./SelectionManager";
+import { Docs } from "../documents/Documents";
+import { NumCast, BoolCast, Cast } from "../../new_fields/Types";
+import { Doc } from "../../new_fields/Doc";
+import { FormattedTextBox } from "../views/nodes/FormattedTextBox";
+import { Id } from "../../new_fields/FieldSymbols";
const mac = typeof navigator !== "undefined" ? /Mac/.test(navigator.platform) : false;
@@ -25,7 +30,7 @@ export let updateBullets = (tx2: Transaction, schema: Schema, mapStyle?: string)
});
return tx2;
};
-export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?: KeyMap): KeyMap {
+export default function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKeys?: KeyMap): KeyMap {
const keys: { [key: string]: any } = {};
function bind(key: string, cmd: any) {
@@ -144,13 +149,43 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
console.log("bullet demote fail");
}
});
+ bind("Ctrl-Enter", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
+ const layoutDoc = props.Document;
+ const originalDoc = layoutDoc.rootDocument || layoutDoc;
+ if (originalDoc instanceof Doc) {
+ const newDoc = Docs.Create.TextDocument("", {
+ title: "", layout: Cast(originalDoc.layout, Doc, null) || FormattedTextBox.DefaultLayout, _singleLine: BoolCast(originalDoc._singleLine),
+ x: NumCast(originalDoc.x), y: NumCast(originalDoc.y) + NumCast(originalDoc._height) + 10, _width: NumCast(layoutDoc._width), _height: NumCast(layoutDoc._height)
+ });
+ FormattedTextBox.SelectOnLoad = newDoc[Id];
+ props.addDocument(newDoc);
+ }
+ });
const splitMetadata = (marks: any, tx: Transaction) => {
marks && tx.ensureMarks(marks.filter((val: any) => val.type !== schema.marks.metadata && val.type !== schema.marks.metadataKey && val.type !== schema.marks.metadataVal));
marks && tx.setStoredMarks(marks.filter((val: any) => val.type !== schema.marks.metadata && val.type !== schema.marks.metadataKey && val.type !== schema.marks.metadataVal));
return tx;
};
+ const addTextOnRight = (force: boolean) => {
+ const layoutDoc = props.Document;
+ const originalDoc = layoutDoc.rootDocument || layoutDoc;
+ if (force || props.Document._singleLine) {
+ const newDoc = Docs.Create.TextDocument("", {
+ title: "", layout: Cast(originalDoc.layout, Doc, null) || FormattedTextBox.DefaultLayout, _singleLine: BoolCast(originalDoc._singleLine),
+ x: NumCast(originalDoc.x) + NumCast(originalDoc._width) + 10, y: NumCast(originalDoc.y), _width: NumCast(layoutDoc._width), _height: NumCast(layoutDoc._height)
+ });
+ FormattedTextBox.SelectOnLoad = newDoc[Id];
+ props.addDocument(newDoc);
+ return true;
+ }
+ return false;
+ };
+ bind("Alt-Enter", (state: EditorState<S>, dispatch: (tx: Transaction<Schema<any, any>>) => void) => {
+ return addTextOnRight(true);
+ });
bind("Enter", (state: EditorState<S>, dispatch: (tx: Transaction<Schema<any, any>>) => void) => {
+ if (addTextOnRight(false)) return true;
const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
if (!splitListItem(schema.nodes.list_item)(state, dispatch)) {
if (!splitBlockKeepMarks(state, (tx3: Transaction) => {
@@ -175,13 +210,16 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
});
const path = (state.doc.resolve(state.selection.from - 1) as any).path;
const spaceSeparator = path[path.length - 3].childCount > 1 ? 0 : -1;
- const textsel = TextSelection.create(state.doc, range!.end - path[path.length - 3].lastChild.nodeSize + spaceSeparator, range!.end);
- const text = range ? state.doc.textBetween(textsel.from, textsel.to) : "";
- let whitespace = text.length - 1;
- for (; whitespace >= 0 && text[whitespace] !== " "; whitespace--) { }
- if (text.endsWith(":")) {
- dispatch(state.tr.addMark(textsel.from + whitespace + 1, textsel.to, schema.marks.metadata.create() as any).
- addMark(textsel.from + whitespace + 1, textsel.to - 2, schema.marks.metadataKey.create() as any));
+ const anchor = range!.end - path[path.length - 3].lastChild.nodeSize + spaceSeparator;
+ if (anchor >= 0) {
+ const textsel = TextSelection.create(state.doc, anchor, range!.end);
+ const text = range ? state.doc.textBetween(textsel.from, textsel.to) : "";
+ let whitespace = text.length - 1;
+ for (; whitespace >= 0 && text[whitespace] !== " "; whitespace--) { }
+ if (text.endsWith(":")) {
+ dispatch(state.tr.addMark(textsel.from + whitespace + 1, textsel.to, schema.marks.metadata.create() as any).
+ addMark(textsel.from + whitespace + 1, textsel.to - 2, schema.marks.metadataKey.create() as any));
+ }
}
return false;
});
diff --git a/src/client/util/RichTextMenu.tsx b/src/client/util/RichTextMenu.tsx
index e07efe056..3f0ec7aa5 100644
--- a/src/client/util/RichTextMenu.tsx
+++ b/src/client/util/RichTextMenu.tsx
@@ -8,11 +8,10 @@ import { EditorView } from "prosemirror-view";
import { EditorState, NodeSelection, TextSelection } from "prosemirror-state";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { IconProp, library } from '@fortawesome/fontawesome-svg-core';
-import { faBold, faItalic, faUnderline, faStrikethrough, faSubscript, faSuperscript, faIndent, faEyeDropper, faCaretDown, faPalette, faHighlighter, faLink, faPaintRoller, faSleigh } from "@fortawesome/free-solid-svg-icons";
-import { MenuItem, Dropdown } from "prosemirror-menu";
+import { faBold, faItalic, faChevronLeft, faUnderline, faStrikethrough, faSubscript, faSuperscript, faIndent, faEyeDropper, faCaretDown, faPalette, faHighlighter, faLink, faPaintRoller, faSleigh } from "@fortawesome/free-solid-svg-icons";
import { updateBullets } from "./ProsemirrorExampleTransfer";
import { FieldViewProps } from "../views/nodes/FieldView";
-import { NumCast, Cast, StrCast } from "../../new_fields/Types";
+import { Cast, StrCast } from "../../new_fields/Types";
import { FormattedTextBoxProps } from "../views/nodes/FormattedTextBox";
import { unimplementedFunction, Utils } from "../../Utils";
import { wrapInList } from "prosemirror-schema-list";
@@ -24,7 +23,7 @@ import { SelectionManager } from "./SelectionManager";
import { LinkManager } from "./LinkManager";
const { toggleMark, setBlockType } = require("prosemirror-commands");
-library.add(faBold, faItalic, faUnderline, faStrikethrough, faSuperscript, faSubscript, faIndent, faEyeDropper, faCaretDown, faPalette, faHighlighter, faLink, faPaintRoller);
+library.add(faBold, faItalic, faChevronLeft, faUnderline, faStrikethrough, faSuperscript, faSubscript, faIndent, faEyeDropper, faCaretDown, faPalette, faHighlighter, faLink, faPaintRoller);
@observer
export default class RichTextMenu extends AntimodeMenu {
@@ -41,6 +40,7 @@ export default class RichTextMenu extends AntimodeMenu {
private fontColors: (string | undefined)[];
private highlightColors: (string | undefined)[];
+ @observable private collapsed: boolean = false;
@observable private boldActive: boolean = false;
@observable private italicsActive: boolean = false;
@observable private underlineActive: boolean = false;
@@ -146,7 +146,7 @@ export default class RichTextMenu extends AntimodeMenu {
public MakeLinkToSelection = (linkDocId: string, title: string, location: string, targetDocId: string): string => {
if (this.view) {
- const link = this.view.state.schema.marks.link.create({ href: Utils.prepend("/doc/" + linkDocId), title: title, location: location, targetId: targetDocId });
+ const link = this.view.state.schema.marks.link.create({ href: Utils.prepend("/doc/" + linkDocId), title: title, location: location, linkId: linkDocId, targetId: targetDocId });
this.view.dispatch(this.view.state.tr.removeMark(this.view.state.selection.from, this.view.state.selection.to, this.view.state.schema.marks.link).
addMark(this.view.state.selection.from, this.view.state.selection.to, link));
return this.view.state.selection.$from.nodeAfter?.text || "";
@@ -275,6 +275,7 @@ export default class RichTextMenu extends AntimodeMenu {
}
destroy() {
+ this.fadeOut(true);
}
@action
@@ -755,9 +756,18 @@ export default class RichTextMenu extends AntimodeMenu {
}
}
+ @action
+ protected toggleCollapse = (e: React.MouseEvent) => {
+ this.collapsed = !this.collapsed;
+ setTimeout(() => {
+ const x = Math.min(this._left, window.innerWidth - RichTextMenu.Instance.width);
+ RichTextMenu.Instance.jumpTo(x, this._top);
+ }, 0);
+ }
+
render() {
- const row1 = <div className="antimodeMenu-row" key="row1">{[
+ const row1 = <div className="antimodeMenu-row" key="row1" style={{ display: this.collapsed ? "none" : undefined }}>{[
this.createButton("bold", "Bold", this.boldActive, toggleMark(schema.marks.strong)),
this.createButton("italic", "Italic", this.italicsActive, toggleMark(schema.marks.em)),
this.createButton("underline", "Underline", this.underlineActive, toggleMark(schema.marks.underline)),
@@ -772,13 +782,18 @@ export default class RichTextMenu extends AntimodeMenu {
]}</div>;
const row2 = <div className="antimodeMenu-row row-2" key="antimodemenu row2">
- <div key="row">
+ <div key="row" style={{ display: this.collapsed ? "none" : undefined }}>
{[this.createMarksDropdown(this.activeFontSize, this.fontSizeOptions, "font size"),
this.createMarksDropdown(this.activeFontFamily, this.fontFamilyOptions, "font family"),
this.createNodesDropdown(this.activeListType, this.listTypeOptions, "nodes")]}
</div>
<div key="button">
- <button className="antimodeMenu-button" key="pin menu" title="Pin menu" onClick={this.toggleMenuPin} style={this.Pinned ? { backgroundColor: "#121212" } : {}}>
+ <div key="collapser">
+ <button className="antimodeMenu-button" key="collapse menu" title="Collapse menu" onClick={this.toggleCollapse} style={{ backgroundColor: this.collapsed ? "#121212" : "", width: 25 }}>
+ <FontAwesomeIcon icon="chevron-left" size="lg" style={{ transition: "transform 0.3s", transform: this.collapsed ? "rotate(180deg)" : "" }} />
+ </button>
+ </div>
+ <button className="antimodeMenu-button" key="pin menu" title="Pin menu" onClick={this.toggleMenuPin} style={{ backgroundColor: this.Pinned ? "#121212" : "", display: this.collapsed ? "none" : undefined }}>
<FontAwesomeIcon icon="thumbtack" size="lg" style={{ transition: "transform 0.1s", transform: this.Pinned ? "rotate(45deg)" : "" }} />
</button>
{this.getDragger()}
diff --git a/src/client/util/RichTextRules.ts b/src/client/util/RichTextRules.ts
index 8411cc6ee..b0a124cb8 100644
--- a/src/client/util/RichTextRules.ts
+++ b/src/client/util/RichTextRules.ts
@@ -1,289 +1,317 @@
-import { textblockTypeInputRule, smartQuotes, emDash, ellipsis, InputRule } from "prosemirror-inputrules";
-import { schema } from "./RichTextSchema";
-import { wrappingInputRule } from "./prosemirrorPatches";
+import { ellipsis, emDash, InputRule, smartQuotes, textblockTypeInputRule } from "prosemirror-inputrules";
import { NodeSelection, TextSelection } from "prosemirror-state";
-import { StrCast, Cast, NumCast } from "../../new_fields/Types";
-import { Doc, DataSym } from "../../new_fields/Doc";
-import { FormattedTextBox } from "../views/nodes/FormattedTextBox";
-import { Docs, DocUtils } from "../documents/Documents";
+import { DataSym, Doc } from "../../new_fields/Doc";
import { Id } from "../../new_fields/FieldSymbols";
-import { DocServer } from "../DocServer";
+import { ComputedField } from "../../new_fields/ScriptField";
+import { Cast, NumCast } from "../../new_fields/Types";
import { returnFalse, Utils } from "../../Utils";
+import { DocServer } from "../DocServer";
+import { Docs, DocUtils } from "../documents/Documents";
+import { FormattedTextBox } from "../views/nodes/FormattedTextBox";
+import { wrappingInputRule } from "./prosemirrorPatches";
import RichTextMenu from "./RichTextMenu";
-import { RichTextField } from "../../new_fields/RichTextField";
-import { ComputedField } from "../../new_fields/ScriptField";
+import { schema } from "./RichTextSchema";
-export const inpRules = {
- rules: [
- ...smartQuotes,
- ellipsis,
- emDash,
+export class RichTextRules {
+ public Document: Doc;
+ public TextBox: FormattedTextBox;
+ public EnteringStyle: boolean = false;
+ constructor(doc: Doc, textBox: FormattedTextBox) {
+ this.Document = doc;
+ this.TextBox = textBox;
+ }
+ public inpRules = {
+ rules: [
+ ...smartQuotes,
+ ellipsis,
+ emDash,
- // > blockquote
- wrappingInputRule(/^\s*>\s$/, schema.nodes.blockquote),
+ // > blockquote
+ wrappingInputRule(/^\s*>\s$/, schema.nodes.blockquote),
- // 1. ordered list
- wrappingInputRule(
- /^1\.\s$/,
- schema.nodes.ordered_list,
- () => {
- return ({ mapStyle: "decimal", bulletStyle: 1 });
- },
- (match: any, node: any) => {
- return node.childCount + node.attrs.order === +match[1];
- },
- (type: any) => ({ type: type, attrs: { mapStyle: "decimal", bulletStyle: 1 } })
- ),
- // a. alphabbetical list
- wrappingInputRule(
- /^a\.\s$/,
- schema.nodes.ordered_list,
- // match => {
- () => {
- return ({ mapStyle: "alpha", bulletStyle: 1 });
- // return ({ order: +match[1] })
- },
- (match: any, node: any) => {
- return node.childCount + node.attrs.order === +match[1];
- },
- (type: any) => ({ type: type, attrs: { mapStyle: "alpha", bulletStyle: 1 } })
- ),
+ // 1. ordered list
+ wrappingInputRule(
+ /^1\.\s$/,
+ schema.nodes.ordered_list,
+ () => {
+ return ({ mapStyle: "decimal", bulletStyle: 1 });
+ },
+ (match: any, node: any) => {
+ return node.childCount + node.attrs.order === +match[1];
+ },
+ (type: any) => ({ type: type, attrs: { mapStyle: "decimal", bulletStyle: 1 } })
+ ),
+ // a. alphabbetical list
+ wrappingInputRule(
+ /^a\.\s$/,
+ schema.nodes.ordered_list,
+ // match => {
+ () => {
+ return ({ mapStyle: "alpha", bulletStyle: 1 });
+ // return ({ order: +match[1] })
+ },
+ (match: any, node: any) => {
+ return node.childCount + node.attrs.order === +match[1];
+ },
+ (type: any) => ({ type: type, attrs: { mapStyle: "alpha", bulletStyle: 1 } })
+ ),
- // * bullet list
- wrappingInputRule(/^\s*([-+*])\s$/, schema.nodes.bullet_list),
+ // * bullet list
+ wrappingInputRule(/^\s*([-+*])\s$/, schema.nodes.bullet_list),
- // ``` code block
- textblockTypeInputRule(/^```$/, schema.nodes.code_block),
+ // ``` code block
+ textblockTypeInputRule(/^```$/, schema.nodes.code_block),
- // # heading
- textblockTypeInputRule(
- new RegExp(/^(#{1,6})\s$/),
- schema.nodes.heading,
- match => {
- return ({ level: match[1].length });
- }
- ),
+ // # heading
+ textblockTypeInputRule(
+ new RegExp(/^(#{1,6})\s$/),
+ schema.nodes.heading,
+ match => {
+ return ({ level: match[1].length });
+ }
+ ),
- // set the font size using #<font-size>
- new InputRule(
- new RegExp(/%([0-9]+)\s$/),
- (state, match, start, end) => {
- const size = Number(match[1]);
- return state.tr.deleteRange(start, end).addStoredMark(schema.marks.pFontSize.create({ fontSize: size }));
- }),
+ // set the font size using #<font-size>
+ new InputRule(
+ new RegExp(/%([0-9]+)\s$/),
+ (state, match, start, end) => {
+ const size = Number(match[1]);
+ return state.tr.deleteRange(start, end).addStoredMark(schema.marks.pFontSize.create({ fontSize: size }));
+ }),
- // create a text display of a metadata field on this or another document, or create a hyperlink portal to another document
- new InputRule(
- new RegExp(/\[\[([a-zA-Z_ \-0-9]*)(:[a-zA-Z_ \-0-9]+)?\]\]$/),
- (state, match, start, end) => {
- if (!match[2]) {
- const docId = match[1];
- DocServer.GetRefField(docId).then(docx => {
- const target = ((docx instanceof Doc) && docx) || Docs.Create.FreeformDocument([], { title: docId, _width: 500, _height: 500, _LODdisable: true, }, docId);
- DocUtils.Publish(target, docId, returnFalse, returnFalse);
- DocUtils.MakeLink({ doc: (schema as any).Document }, { doc: target }, "portal link", "");
- });
- const link = state.schema.marks.link.create({ href: Utils.prepend("/doc/" + docId), location: "onRight", title: docId, targetId: docId });
- return state.tr.deleteRange(end - 1, end).deleteRange(start, start + 2).addMark(start, end - 3, link);
- }
- const fieldView = state.schema.nodes.dashField.create({ fieldKey: match[2]?.substring(1), docid: match[1] });
- return state.tr.deleteRange(start, end).insert(start, fieldView);
- }),
- // create a text display of a metadata field on this or another document, or create a hyperlink portal to another document
- new InputRule(
- new RegExp(/\{\{([a-zA-Z_ \-0-9]*)(:[a-zA-Z_ \-0-9]+)?\}\}$/),
- (state, match, start, end) => {
- const docId = match[1];
- DocServer.GetRefField(docId).then(docx => {
- if (!(docx instanceof Doc && docx)) {
- const docx = Docs.Create.FreeformDocument([], { title: docId, _width: 500, _height: 500, _LODdisable: true }, docId);
- DocUtils.Publish(docx, docId, returnFalse, returnFalse);
+ // 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 [[fieldKey]] => show field [[fieldKey:Doc]] => show field of doc
+ new InputRule(
+ new RegExp(/\[\[([a-zA-Z_#@\? \-0-9]*)(=[a-zA-Z_#@\? \-0-9]*)?(:[a-zA-Z_#@\? \-0-9]+)?\]\]$/),
+ (state, match, start, end) => {
+ const fieldKey = match[1];
+ const docid = match[3]?.substring(1);
+ const value = match[2]?.substring(1);
+ if (!fieldKey) {
+ if (docid) {
+ DocServer.GetRefField(docid).then(docx => {
+ const target = ((docx instanceof Doc) && docx) || Docs.Create.FreeformDocument([], { title: docid, _width: 500, _height: 500, _LODdisable: true, }, docid);
+ DocUtils.Publish(target, docid, returnFalse, returnFalse);
+ DocUtils.MakeLink({ doc: this.Document }, { doc: target }, "portal to");
+ });
+ const link = state.schema.marks.link.create({ href: Utils.prepend("/doc/" + docid), location: "onRight", title: docid, targetId: docid });
+ return state.tr.deleteRange(end - 1, end).deleteRange(start, start + 2).addMark(start, end - 3, link);
+ }
+ return state.tr;
+ }
+ if (value !== "" && value !== undefined) {
+ this.Document[DataSym][fieldKey] = value === "true" ? true : value === "false" ? false : value;
}
- });
- const node = (state.doc.resolve(start) as any).nodeAfter;
- const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 75, title: "dashDoc", docid: docId, float: "right", fieldKey: match[2]?.substring(1), alias: Utils.GenerateGuid() });
- const sm = state.storedMarks || undefined;
- return node ? state.tr.replaceRangeWith(start, end, dashDoc).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr;
- }),
- new InputRule(
- new RegExp(/##$/),
- (state, match, start, end) => {
- const schemaDoc = Doc.GetDataDoc((schema as any).Document);
- const textDoc = Doc.GetProto(Cast(schemaDoc[DataSym], Doc, null)!);
- const numInlines = NumCast(textDoc.inlineTextCount);
- textDoc.inlineTextCount = numInlines + 1;
- const inlineFieldKey = "inline" + numInlines; // which field on the text document this annotation will write to
- const inlineLayoutKey = "layout_" + inlineFieldKey; // the field holding the layout string that will render the inline annotation
- const textDocInline = Docs.Create.TextDocument("", { layoutKey: inlineLayoutKey, _width: 75, _height: 35, annotationOn: textDoc, _autoHeight: true, fontSize: 9, title: "inline comment" });
- textDocInline.title = inlineFieldKey; // give the annotation its own title
- textDocInline.customTitle = true; // And make sure that it's 'custom' so that editing text doesn't change the title of the containing doc
- textDocInline.isTemplateForField = inlineFieldKey; // this is needed in case the containing text doc is converted to a template at some point
- textDocInline.proto = textDoc; // make the annotation inherit from the outer text doc so that it can resolve any nested field references, e.g., [[field]]
- textDocInline._textContext = ComputedField.MakeFunction(`copyField(this.${inlineFieldKey})`, { this: Doc.name });
- textDoc[inlineLayoutKey] = FormattedTextBox.LayoutString(inlineFieldKey); // create a layout string for the layout key that will render the annotation text
- textDoc[inlineFieldKey] = ""; // set a default value for the annotation
- const node = (state.doc.resolve(start) as any).nodeAfter;
- const newNode = schema.nodes.dashComment.create({ docid: textDocInline[Id] });
- const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 35, title: "dashDoc", docid: textDocInline[Id], float: "right" });
- const sm = state.storedMarks || undefined;
- const replaced = node ? state.tr.insert(start, newNode).replaceRangeWith(start + 1, end + 1, dashDoc).insertText(" ", start + 2).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
- state.tr;
- return replaced;
- }),
- // stop using active style
- new InputRule(
- new RegExp(/%%$/),
- (state, match, start, end) => {
- const tr = state.tr.deleteRange(start, end);
- const marks = state.tr.selection.$anchor.nodeBefore?.marks;
- return marks ? Array.from(marks).filter(m => m !== state.schema.marks.user_mark).reduce((tr, m) => tr.removeStoredMark(m), tr) : tr;
- }),
+ const fieldView = state.schema.nodes.dashField.create({ fieldKey, docid });
+ return state.tr.deleteRange(start, end).insert(start, fieldView);
+ }),
+ // create an inline view of a tag stored under the '#' field
+ new InputRule(
+ new RegExp(/#([a-zA-Z_\-0-9]+)\s$/),
+ (state, match, start, end) => {
+ const tag = match[1];
+ if (!tag) return state.tr;
+ this.Document[DataSym]["#"] = tag;
+ const fieldView = state.schema.nodes.dashField.create({ fieldKey: "#" });
+ return state.tr.deleteRange(start, end).insert(start, fieldView);
+ }),
+ // create an inline view of a document {{ <layoutKey> : <Doc> }} // {{:Doc}} => show default view of document {{<layout>}} => show layout for this doc {{<layout> : Doc}} => show layout for another doc
+ new InputRule(
+ new RegExp(/\{\{([a-zA-Z_ \-0-9]*)(\([a-zA-Z0-9…._\-]*\))?(:[a-zA-Z_ \-0-9]+)?\}\}$/),
+ (state, match, start, end) => {
+ const fieldKey = match[1] || "";
+ const fieldParam = match[2]?.replace("…", "...") || "";
+ const docid = match[3]?.substring(1);
+ if (!fieldKey && !docid) return state.tr;
+ docid && DocServer.GetRefField(docid).then(docx => {
+ if (!(docx instanceof Doc && docx)) {
+ const docx = Docs.Create.FreeformDocument([], { title: docid, _width: 500, _height: 500, _LODdisable: true }, docid);
+ DocUtils.Publish(docx, docid, returnFalse, returnFalse);
+ }
+ });
+ const node = (state.doc.resolve(start) as any).nodeAfter;
+ const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 75, title: "dashDoc", docid, fieldKey: fieldKey + fieldParam, float: "right", alias: Utils.GenerateGuid() });
+ const sm = state.storedMarks || undefined;
+ return node ? state.tr.replaceRangeWith(start, end, dashDoc).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr;
+ }),
+ new InputRule(
+ new RegExp(/##$/),
+ (state, match, start, end) => {
+ const textDoc = this.Document[DataSym];
+ const numInlines = NumCast(textDoc.inlineTextCount);
+ textDoc.inlineTextCount = numInlines + 1;
+ const inlineFieldKey = "inline" + numInlines; // which field on the text document this annotation will write to
+ const inlineLayoutKey = "layout_" + inlineFieldKey; // the field holding the layout string that will render the inline annotation
+ const textDocInline = Docs.Create.TextDocument("", { layoutKey: inlineLayoutKey, _width: 75, _height: 35, annotationOn: textDoc, _autoHeight: true, fontSize: 9, title: "inline comment" });
+ textDocInline.title = inlineFieldKey; // give the annotation its own title
+ textDocInline.customTitle = true; // And make sure that it's 'custom' so that editing text doesn't change the title of the containing doc
+ textDocInline.isTemplateForField = inlineFieldKey; // this is needed in case the containing text doc is converted to a template at some point
+ textDocInline.proto = textDoc; // make the annotation inherit from the outer text doc so that it can resolve any nested field references, e.g., [[field]]
+ textDocInline._textContext = ComputedField.MakeFunction(`copyField(this.${inlineFieldKey})`, { this: Doc.name });
+ textDoc[inlineLayoutKey] = FormattedTextBox.LayoutString(inlineFieldKey); // create a layout string for the layout key that will render the annotation text
+ textDoc[inlineFieldKey] = ""; // set a default value for the annotation
+ const node = (state.doc.resolve(start) as any).nodeAfter;
+ const newNode = schema.nodes.dashComment.create({ docid: textDocInline[Id] });
+ const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 35, title: "dashDoc", docid: textDocInline[Id], float: "right" });
+ const sm = state.storedMarks || undefined;
+ const replaced = node ? state.tr.insert(start, newNode).replaceRangeWith(start + 1, end + 1, dashDoc).insertText(" ", start + 2).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
+ state.tr;
+ return replaced;
+ }),
+ // stop using active style
+ new InputRule(
+ new RegExp(/%%$/),
+ (state, match, start, end) => {
+ const tr = state.tr.deleteRange(start, end);
+ const marks = state.tr.selection.$anchor.nodeBefore?.marks;
+ return marks ? Array.from(marks).filter(m => m !== state.schema.marks.user_mark).reduce((tr, m) => tr.removeStoredMark(m), tr) : tr;
+ }),
- // set the Todo user-tag on the current selection (assumes % was used to initiate an EnteringStyle mode)
- new InputRule(
- new RegExp(/[ti!x]$/),
- (state, match, start, end) => {
- if (state.selection.to === state.selection.from || !(schema as any).EnteringStyle) return null;
- const tag = match[0] === "t" ? "todo" : match[0] === "i" ? "ignore" : match[0] === "x" ? "disagree" : match[0] === "!" ? "important" : "??";
- const node = (state.doc.resolve(start) as any).nodeAfter;
- if (node?.marks.findIndex((m: any) => m.type === schema.marks.user_tag) !== -1) return state.tr.removeMark(start, end, schema.marks.user_tag);
- return node ? state.tr.addMark(start, end, schema.marks.user_tag.create({ userid: Doc.CurrentUserEmail, tag: tag, modified: Math.round(Date.now() / 1000 / 60) })) : state.tr;
- }),
+ // set the Todo user-tag on the current selection (assumes % was used to initiate an EnteringStyle mode)
+ new InputRule(
+ new RegExp(/[ti!x]$/),
+ (state, match, start, end) => {
+ if (state.selection.to === state.selection.from || !this.EnteringStyle) return null;
+ const tag = match[0] === "t" ? "todo" : match[0] === "i" ? "ignore" : match[0] === "x" ? "disagree" : match[0] === "!" ? "important" : "??";
+ const node = (state.doc.resolve(start) as any).nodeAfter;
+ if (node?.marks.findIndex((m: any) => m.type === schema.marks.user_tag) !== -1) return state.tr.removeMark(start, end, schema.marks.user_tag);
+ return node ? state.tr.addMark(start, end, schema.marks.user_tag.create({ userid: Doc.CurrentUserEmail, tag: tag, modified: Math.round(Date.now() / 1000 / 60) })) : state.tr;
+ }),
- // set the First-line indent node type for the selection's paragraph (assumes % was used to initiate an EnteringStyle mode)
- new InputRule(
- new RegExp(/(%d|d)$/),
- (state, match, start, end) => {
- if (!match[0].startsWith("%") && !(schema as any).EnteringStyle) return null;
- const pos = (state.doc.resolve(start) as any);
- for (let depth = pos.path.length / 3 - 1; depth >= 0; depth--) {
- const node = pos.node(depth);
- if (node.type === schema.nodes.paragraph) {
- const replaced = state.tr.setNodeMarkup(pos.pos - pos.parentOffset - 1, node.type, { ...node.attrs, indent: node.attrs.indent === 25 ? undefined : 25 });
- const result = replaced.setSelection(new TextSelection(replaced.doc.resolve(start)));
- return match[0].startsWith("%") ? result.deleteRange(start, end) : result;
+ // set the First-line indent node type for the selection's paragraph (assumes % was used to initiate an EnteringStyle mode)
+ new InputRule(
+ new RegExp(/(%d|d)$/),
+ (state, match, start, end) => {
+ if (!match[0].startsWith("%") && !this.EnteringStyle) return null;
+ const pos = (state.doc.resolve(start) as any);
+ for (let depth = pos.path.length / 3 - 1; depth >= 0; depth--) {
+ const node = pos.node(depth);
+ if (node.type === schema.nodes.paragraph) {
+ const replaced = state.tr.setNodeMarkup(pos.pos - pos.parentOffset - 1, node.type, { ...node.attrs, indent: node.attrs.indent === 25 ? undefined : 25 });
+ const result = replaced.setSelection(new TextSelection(replaced.doc.resolve(start)));
+ return match[0].startsWith("%") ? result.deleteRange(start, end) : result;
+ }
}
- }
- return null;
- }),
+ return null;
+ }),
- // set the Hanging indent node type for the current selection's paragraph (assumes % was used to initiate an EnteringStyle mode)
- new InputRule(
- new RegExp(/(%h|h)$/),
- (state, match, start, end) => {
- if (!match[0].startsWith("%") && !(schema as any).EnteringStyle) return null;
- const pos = (state.doc.resolve(start) as any);
- for (let depth = pos.path.length / 3 - 1; depth >= 0; depth--) {
- const node = pos.node(depth);
- if (node.type === schema.nodes.paragraph) {
- const replaced = state.tr.setNodeMarkup(pos.pos - pos.parentOffset - 1, node.type, { ...node.attrs, indent: node.attrs.indent === -25 ? undefined : -25 });
- const result = replaced.setSelection(new TextSelection(replaced.doc.resolve(start)));
- return match[0].startsWith("%") ? result.deleteRange(start, end) : result;
+ // set the Hanging indent node type for the current selection's paragraph (assumes % was used to initiate an EnteringStyle mode)
+ new InputRule(
+ new RegExp(/(%h|h)$/),
+ (state, match, start, end) => {
+ if (!match[0].startsWith("%") && !this.EnteringStyle) return null;
+ const pos = (state.doc.resolve(start) as any);
+ for (let depth = pos.path.length / 3 - 1; depth >= 0; depth--) {
+ const node = pos.node(depth);
+ if (node.type === schema.nodes.paragraph) {
+ const replaced = state.tr.setNodeMarkup(pos.pos - pos.parentOffset - 1, node.type, { ...node.attrs, indent: node.attrs.indent === -25 ? undefined : -25 });
+ const result = replaced.setSelection(new TextSelection(replaced.doc.resolve(start)));
+ return match[0].startsWith("%") ? result.deleteRange(start, end) : result;
+ }
}
- }
- return null;
- }),
- // set the Quoted indent node type for the current selection's paragraph (assumes % was used to initiate an EnteringStyle mode)
- new InputRule(
- new RegExp(/(%q|q)$/),
- (state, match, start, end) => {
- if (!match[0].startsWith("%") && !(schema as any).EnteringStyle) return null;
- const pos = (state.doc.resolve(start) as any);
- if (state.selection instanceof NodeSelection && state.selection.node.type === schema.nodes.ordered_list) {
- const node = state.selection.node;
- return state.tr.setNodeMarkup(pos.pos, node.type, { ...node.attrs, indent: node.attrs.indent === 30 ? undefined : 30 });
- }
- for (let depth = pos.path.length / 3 - 1; depth >= 0; depth--) {
- const node = pos.node(depth);
- if (node.type === schema.nodes.paragraph) {
- const replaced = state.tr.setNodeMarkup(pos.pos - pos.parentOffset - 1, node.type, { ...node.attrs, inset: node.attrs.inset === 30 ? undefined : 30 });
- const result = replaced.setSelection(new TextSelection(replaced.doc.resolve(start)));
- return match[0].startsWith("%") ? result.deleteRange(start, end) : result;
+ return null;
+ }),
+ // set the Quoted indent node type for the current selection's paragraph (assumes % was used to initiate an EnteringStyle mode)
+ new InputRule(
+ new RegExp(/(%q|q)$/),
+ (state, match, start, end) => {
+ if (!match[0].startsWith("%") && !this.EnteringStyle) return null;
+ const pos = (state.doc.resolve(start) as any);
+ if (state.selection instanceof NodeSelection && state.selection.node.type === schema.nodes.ordered_list) {
+ const node = state.selection.node;
+ return state.tr.setNodeMarkup(pos.pos, node.type, { ...node.attrs, indent: node.attrs.indent === 30 ? undefined : 30 });
}
- }
- return null;
- }),
+ for (let depth = pos.path.length / 3 - 1; depth >= 0; depth--) {
+ const node = pos.node(depth);
+ if (node.type === schema.nodes.paragraph) {
+ const replaced = state.tr.setNodeMarkup(pos.pos - pos.parentOffset - 1, node.type, { ...node.attrs, inset: node.attrs.inset === 30 ? undefined : 30 });
+ const result = replaced.setSelection(new TextSelection(replaced.doc.resolve(start)));
+ return match[0].startsWith("%") ? result.deleteRange(start, end) : result;
+ }
+ }
+ return null;
+ }),
- // center justify text
- new InputRule(
- new RegExp(/%\^$/),
- (state, match, start, end) => {
- const node = (state.doc.resolve(start) as any).nodeAfter;
- const sm = state.storedMarks || undefined;
- const replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "center" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
- state.tr;
- return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2)));
- }),
- // left justify text
- new InputRule(
- new RegExp(/%\[$/),
- (state, match, start, end) => {
- const node = (state.doc.resolve(start) as any).nodeAfter;
- const sm = state.storedMarks || undefined;
- const replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "left" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
- state.tr;
- return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2)));
- }),
- // right justify text
- new InputRule(
- new RegExp(/%\]$/),
- (state, match, start, end) => {
- const node = (state.doc.resolve(start) as any).nodeAfter;
- const sm = state.storedMarks || undefined;
- const replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "right" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
- state.tr;
- return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2)));
- }),
- new InputRule(
- new RegExp(/%\(/),
- (state, match, start, end) => {
- const node = (state.doc.resolve(start) as any).nodeAfter;
- const sm = state.storedMarks || [];
- const mark = state.schema.marks.summarizeInclusive.create();
- sm.push(mark);
- const selected = state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))).addMark(start, end, mark);
- const content = selected.selection.content();
- const replaced = node ? selected.replaceRangeWith(start, end,
- schema.nodes.summary.create({ visibility: true, text: content, textslice: content.toJSON() })) :
- state.tr;
- return replaced.setSelection(new TextSelection(replaced.doc.resolve(end + 1))).setStoredMarks([...node.marks, ...sm]);
- }),
- new InputRule(
- new RegExp(/%\)/),
- (state, match, start, end) => {
- return state.tr.deleteRange(start, end).removeStoredMark(state.schema.marks.summarizeInclusive.create());
- }),
- new InputRule(
- new RegExp(/%f$/),
- (state, match, start, end) => {
- const newNode = schema.nodes.footnote.create({});
- const tr = state.tr;
- tr.deleteRange(start, end).replaceSelectionWith(newNode); // replace insertion with a footnote.
- return tr.setSelection(new NodeSelection( // select the footnote node to open its display
- tr.doc.resolve( // get the location of the footnote node by subtracting the nodesize of the footnote from the current insertion point anchor (which will be immediately after the footnote node)
- tr.selection.anchor - tr.selection.$anchor.nodeBefore!.nodeSize)));
- }),
+ // center justify text
+ new InputRule(
+ new RegExp(/%\^$/),
+ (state, match, start, end) => {
+ const node = (state.doc.resolve(start) as any).nodeAfter;
+ const sm = state.storedMarks || undefined;
+ const replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "center" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
+ state.tr;
+ return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2)));
+ }),
+ // left justify text
+ new InputRule(
+ new RegExp(/%\[$/),
+ (state, match, start, end) => {
+ const node = (state.doc.resolve(start) as any).nodeAfter;
+ const sm = state.storedMarks || undefined;
+ const replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "left" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
+ state.tr;
+ return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2)));
+ }),
+ // right justify text
+ new InputRule(
+ new RegExp(/%\]$/),
+ (state, match, start, end) => {
+ const node = (state.doc.resolve(start) as any).nodeAfter;
+ const sm = state.storedMarks || undefined;
+ const replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "right" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
+ state.tr;
+ return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2)));
+ }),
+ new InputRule(
+ new RegExp(/%\(/),
+ (state, match, start, end) => {
+ const node = (state.doc.resolve(start) as any).nodeAfter;
+ const sm = state.storedMarks || [];
+ const mark = state.schema.marks.summarizeInclusive.create();
+ sm.push(mark);
+ const selected = state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))).addMark(start, end, mark);
+ const content = selected.selection.content();
+ const replaced = node ? selected.replaceRangeWith(start, end,
+ schema.nodes.summary.create({ visibility: true, text: content, textslice: content.toJSON() })) :
+ state.tr;
+ return replaced.setSelection(new TextSelection(replaced.doc.resolve(end + 1))).setStoredMarks([...node.marks, ...sm]);
+ }),
+ new InputRule(
+ new RegExp(/%\)/),
+ (state, match, start, end) => {
+ return state.tr.deleteRange(start, end).removeStoredMark(state.schema.marks.summarizeInclusive.create());
+ }),
+ new InputRule(
+ new RegExp(/%f$/),
+ (state, match, start, end) => {
+ const newNode = schema.nodes.footnote.create({});
+ const tr = state.tr;
+ tr.deleteRange(start, end).replaceSelectionWith(newNode); // replace insertion with a footnote.
+ return tr.setSelection(new NodeSelection( // select the footnote node to open its display
+ tr.doc.resolve( // get the location of the footnote node by subtracting the nodesize of the footnote from the current insertion point anchor (which will be immediately after the footnote node)
+ tr.selection.anchor - tr.selection.$anchor.nodeBefore!.nodeSize)));
+ }),
- // activate a style by name using prefix '%'
- new InputRule(
- new RegExp(/%[a-z]+$/),
- (state, match, start, end) => {
- const color = match[0].substring(1, match[0].length);
- const marks = RichTextMenu.Instance._brushMap.get(color);
- if (marks) {
- const tr = state.tr.deleteRange(start, end);
- return marks ? Array.from(marks).reduce((tr, m) => tr.addStoredMark(m), tr) : tr;
- }
- const isValidColor = (strColor: string) => {
- const s = new Option().style;
- s.color = strColor;
- return s.color === strColor.toLowerCase(); // 'false' if color wasn't assigned
- };
- if (isValidColor(color)) {
- return state.tr.deleteRange(start, end).addStoredMark(schema.marks.pFontColor.create({ color: color }));
- }
- return null;
- }),
- ]
-};
+ // activate a style by name using prefix '%'
+ new InputRule(
+ new RegExp(/%[a-z]+$/),
+ (state, match, start, end) => {
+ const color = match[0].substring(1, match[0].length);
+ const marks = RichTextMenu.Instance._brushMap.get(color);
+ if (marks) {
+ const tr = state.tr.deleteRange(start, end);
+ return marks ? Array.from(marks).reduce((tr, m) => tr.addStoredMark(m), tr) : tr;
+ }
+ const isValidColor = (strColor: string) => {
+ const s = new Option().style;
+ s.color = strColor;
+ return s.color === strColor.toLowerCase(); // 'false' if color wasn't assigned
+ };
+ if (isValidColor(color)) {
+ return state.tr.deleteRange(start, end).addStoredMark(schema.marks.pFontColor.create({ color: color }));
+ }
+ return null;
+ }),
+ ]
+ };
+}
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index 287a1049b..094cd58f3 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -1,27 +1,31 @@
-import { reaction, IReactionDisposer, observable, runInAction } from "mobx";
+import { IReactionDisposer, observable, reaction, runInAction } from "mobx";
import { baseKeymap, toggleMark } from "prosemirror-commands";
import { redo, undo } from "prosemirror-history";
import { keymap } from "prosemirror-keymap";
import { DOMOutputSpecArray, Fragment, MarkSpec, Node, NodeSpec, Schema, Slice } from "prosemirror-model";
import { bulletList, listItem, orderedList } from 'prosemirror-schema-list';
-import { EditorState, NodeSelection, TextSelection, Plugin } from "prosemirror-state";
+import { EditorState, NodeSelection, Plugin, TextSelection } from "prosemirror-state";
import { StepMap } from "prosemirror-transform";
import { EditorView } from "prosemirror-view";
import * as ReactDOM from 'react-dom';
-import { Doc, WidthSym, HeightSym, DataSym, Field } from "../../new_fields/Doc";
+import { Doc, DocListCast, Field, HeightSym, WidthSym } from "../../new_fields/Doc";
+import { Id } from "../../new_fields/FieldSymbols";
+import { List } from "../../new_fields/List";
+import { ObjectField } from "../../new_fields/ObjectField";
+import { listSpec } from "../../new_fields/Schema";
+import { SchemaHeaderField } from "../../new_fields/SchemaHeaderField";
+import { ComputedField } from "../../new_fields/ScriptField";
+import { BoolCast, Cast, NumCast, StrCast } from "../../new_fields/Types";
import { emptyFunction, returnEmptyString, returnFalse, returnOne, Utils } from "../../Utils";
import { DocServer } from "../DocServer";
+import { Docs } from "../documents/Documents";
+import { CollectionViewType } from "../views/collections/CollectionView";
import { DocumentView } from "../views/nodes/DocumentView";
+import { FormattedTextBox } from "../views/nodes/FormattedTextBox";
import { DocumentManager } from "./DocumentManager";
import ParagraphNodeSpec from "./ParagraphNodeSpec";
import { Transform } from "./Transform";
import React = require("react");
-import { BoolCast, NumCast, StrCast } from "../../new_fields/Types";
-import { FormattedTextBox } from "../views/nodes/FormattedTextBox";
-import { ObjectField } from "../../new_fields/ObjectField";
-import { ComputedField } from "../../new_fields/ScriptField";
-import { observer } from "mobx-react";
-import { Id } from "../../new_fields/FieldSymbols";
const blockquoteDOM: DOMOutputSpecArray = ["blockquote", 0], hrDOM: DOMOutputSpecArray = ["hr"],
preDOM: DOMOutputSpecArray = ["pre", ["code", 0]], brDOM: DOMOutputSpecArray = ["br"], ulDOM: DOMOutputSpecArray = ["ul", 0];
@@ -134,6 +138,7 @@ export const nodes: { [index: string]: NodeSpec } = {
inline: true,
attrs: {
src: {},
+ agnostic: { default: null },
width: { default: 100 },
alt: { default: null },
title: { default: null },
@@ -295,6 +300,7 @@ export const marks: { [index: string]: MarkSpec } = {
attrs: {
href: {},
targetId: { default: "" },
+ linkId: { default: "" },
showPreview: { default: true },
location: { default: null },
title: { default: null },
@@ -309,7 +315,7 @@ export const marks: { [index: string]: MarkSpec } = {
toDOM(node: any) {
return node.attrs.docref && node.attrs.title ?
["div", ["span", `"`], ["span", 0], ["span", `"`], ["br"], ["a", { ...node.attrs, class: "prosemirror-attribution", title: `${node.attrs.title}` }, node.attrs.title], ["br"]] :
- ["a", { ...node.attrs, id: node.attrs.targetId, title: `${node.attrs.title}` }, 0];
+ ["a", { ...node.attrs, id: node.attrs.linkId + node.attrs.targetId, title: `${node.attrs.title}` }, 0];
}
},
@@ -504,7 +510,7 @@ export const marks: { [index: string]: MarkSpec } = {
user_mark: {
attrs: {
userid: { default: "" },
- modified: { default: "when?" }, // 5 second intervals since 1970
+ modified: { default: "when?" }, // 1 second intervals since 1970
},
group: "inline",
toDOM(node: any) {
@@ -520,7 +526,7 @@ export const marks: { [index: string]: MarkSpec } = {
user_tag: {
attrs: {
userid: { default: "" },
- modified: { default: "when?" }, // 5 second intervals since 1970
+ modified: { default: "when?" }, // 1 second intervals since 1970
tag: { default: "" }
},
group: "inline",
@@ -613,7 +619,7 @@ export class ImageResizeView {
DocServer.GetRefField(node.attrs.docid).then(async linkDoc =>
(linkDoc instanceof Doc) &&
DocumentManager.Instance.FollowLink(linkDoc, view.state.schema.Document,
- document => addDocTab(document, undefined, node.attrs.location ? node.attrs.location : "inTab"), false));
+ document => addDocTab(document, node.attrs.location ? node.attrs.location : "inTab"), false));
}
};
this._handle.onpointerdown = function (e: any) {
@@ -717,13 +723,14 @@ export class DashDocView {
_outer: HTMLElement;
_dashDoc: Doc | undefined;
_reactionDisposer: IReactionDisposer | undefined;
+ _renderDisposer: IReactionDisposer | undefined;
_textBox: FormattedTextBox;
getDocTransform = () => {
const { scale, translateX, translateY } = Utils.GetScreenTransform(this._outer);
return new Transform(-translateX, -translateY, 1).scale(1 / this.contentScaling() / scale);
}
- contentScaling = () => NumCast(this._dashDoc!._nativeWidth) > 0 && !this._dashDoc!.ignoreAspect ? this._dashDoc![WidthSym]() / NumCast(this._dashDoc!._nativeWidth) : 1;
+ contentScaling = () => NumCast(this._dashDoc!._nativeWidth) > 0 ? this._dashDoc![WidthSym]() / NumCast(this._dashDoc!._nativeWidth) : 1;
outerFocus = (target: Doc) => this._textBox.props.focus(this._textBox.props.Document); // ideally, this would scroll to show the focus target
constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) {
this._textBox = tbox;
@@ -731,6 +738,7 @@ export class DashDocView {
this._outer = document.createElement("span");
this._outer.style.position = "relative";
this._outer.style.textIndent = "0";
+ this._outer.style.border = "1px solid " + StrCast(tbox.Document.color, (Cast(Doc.UserDoc().activeWorkspace, Doc, null).darkScheme ? "dimGray" : "lightGray"));
this._outer.style.width = node.attrs.width;
this._outer.style.height = node.attrs.height;
this._outer.style.display = node.attrs.hidden ? "none" : "inline-block";
@@ -761,13 +769,14 @@ export class DashDocView {
};
const alias = node.attrs.alias;
- const docid = node.attrs.docid || tbox.props.DataDoc?.[Id] || tbox.dataDoc?.[Id];
+ const docid = node.attrs.docid || tbox.props.Document[Id];// tbox.props.DataDoc?.[Id] || tbox.dataDoc?.[Id];
DocServer.GetRefField(docid + alias).then(async dashDoc => {
if (!(dashDoc instanceof Doc)) {
alias && DocServer.GetRefField(docid).then(async dashDocBase => {
if (dashDocBase instanceof Doc) {
- const aliasedDoc = Doc.MakeDelegate(dashDocBase, docid + alias);
- aliasedDoc.layoutKey = "layout_" + node.attrs.fieldKey;
+ const aliasedDoc = Doc.MakeAlias(dashDocBase, docid + alias);
+ aliasedDoc.layoutKey = "layout";
+ node.attrs.fieldKey && DocumentView.makeCustomViewClicked(aliasedDoc, Docs.Create.StackingDocument, node.attrs.fieldKey, undefined);
self.doRender(aliasedDoc, removeDoc, node, view, getPos);
}
});
@@ -790,106 +799,247 @@ export class DashDocView {
}
doRender(dashDoc: Doc, removeDoc: any, node: any, view: any, getPos: any) {
this._dashDoc = dashDoc;
- dashDoc._hideSidebar = true;
- if (node.attrs.width !== dashDoc._width + "px" || node.attrs.height !== dashDoc._height + "px") {
- try { // bcz: an exception will be thrown if two aliases are open at the same time when a doc view comment is made
- view.dispatch(view.state.tr.setNodeMarkup(getPos(), null, { ...node.attrs, width: dashDoc._width + "px", height: dashDoc._height + "px" }));
- } catch (e) {
- console.log(e);
- }
- }
const self = this;
- const finalLayout = Doc.expandTemplateLayout(dashDoc, !Doc.AreProtosEqual(this._textBox.dataDoc, this._textBox.Document) ? this._textBox.dataDoc : undefined);
+ const dashLayoutDoc = Doc.Layout(dashDoc);
+ const finalLayout = node.attrs.docid ? dashDoc : Doc.expandTemplateLayout(dashLayoutDoc, dashDoc, node.attrs.fieldKey);
if (!finalLayout) setTimeout(() => self.doRender(dashDoc, removeDoc, node, view, getPos), 0);
else {
- const layoutKey = StrCast(finalLayout.layoutKey);
- const finalKey = layoutKey && StrCast(finalLayout[layoutKey]).split("'")?.[1];
- if (finalLayout !== dashDoc && finalKey) {
- const finalLayoutField = finalLayout[finalKey];
- if (finalLayoutField instanceof ObjectField) {
- finalLayout._textTemplate = ComputedField.MakeFunction(`copyField(this.${finalKey})`, { this: Doc.name });
- }
- }
- this._reactionDisposer && this._reactionDisposer();
- this._reactionDisposer = reaction(() => [finalLayout[WidthSym](), finalLayout[HeightSym]()], (dim) => {
- this._dashSpan.style.width = this._outer.style.width = dim[0] + "px";
- this._dashSpan.style.height = this._outer.style.height = dim[1] + "px";
+ this._reactionDisposer?.();
+ this._reactionDisposer = reaction(() => ({ dim: [finalLayout[WidthSym](), finalLayout[HeightSym]()], color: finalLayout.color }), ({ dim, color }) => {
+ this._dashSpan.style.width = this._outer.style.width = Math.max(20, dim[0]) + "px";
+ this._dashSpan.style.height = this._outer.style.height = Math.max(20, dim[1]) + "px";
+ this._outer.style.border = "1px solid " + StrCast(finalLayout.color, (Cast(Doc.UserDoc().activeWorkspace, Doc, null).darkScheme ? "dimGray" : "lightGray"));
}, { fireImmediately: true });
- ReactDOM.render(<DocumentView
- Document={finalLayout}
- DataDoc={!node.attrs.docid ? this._textBox.dataDoc : undefined}
- LibraryPath={this._textBox.props.LibraryPath}
- fitToBox={BoolCast(dashDoc._fitToBox)}
- addDocument={returnFalse}
- removeDocument={removeDoc}
- ScreenToLocalTransform={this.getDocTransform}
- addDocTab={this._textBox.props.addDocTab}
- pinToPres={returnFalse}
- renderDepth={1}
- PanelWidth={finalLayout[WidthSym]}
- PanelHeight={finalLayout[HeightSym]}
- focus={this.outerFocus}
- backgroundColor={returnEmptyString}
- parentActive={returnFalse}
- whenActiveChanged={returnFalse}
- bringToFront={emptyFunction}
- zoomToScale={emptyFunction}
- getScale={returnOne}
- dontRegisterView={false}
- ContainingCollectionView={undefined}
- ContainingCollectionDoc={undefined}
- ContentScaling={this.contentScaling}
- />, this._dashSpan);
+ const doReactRender = (finalLayout: Doc, resolvedDataDoc: Doc) => {
+ ReactDOM.unmountComponentAtNode(this._dashSpan);
+ ReactDOM.render(<DocumentView
+ Document={finalLayout}
+ DataDoc={resolvedDataDoc}
+ LibraryPath={this._textBox.props.LibraryPath}
+ fitToBox={BoolCast(dashDoc._fitToBox)}
+ addDocument={returnFalse}
+ removeDocument={removeDoc}
+ ScreenToLocalTransform={this.getDocTransform}
+ addDocTab={this._textBox.props.addDocTab}
+ pinToPres={returnFalse}
+ renderDepth={self._textBox.props.renderDepth + 1}
+ PanelWidth={finalLayout[WidthSym]}
+ PanelHeight={finalLayout[HeightSym]}
+ focus={this.outerFocus}
+ backgroundColor={returnEmptyString}
+ parentActive={returnFalse}
+ whenActiveChanged={returnFalse}
+ bringToFront={emptyFunction}
+ zoomToScale={emptyFunction}
+ getScale={returnOne}
+ dontRegisterView={false}
+ ContainingCollectionView={this._textBox.props.ContainingCollectionView}
+ ContainingCollectionDoc={this._textBox.props.ContainingCollectionDoc}
+ ContentScaling={this.contentScaling}
+ />, this._dashSpan);
+ if (node.attrs.width !== dashDoc._width + "px" || node.attrs.height !== dashDoc._height + "px") {
+ try { // bcz: an exception will be thrown if two aliases are open at the same time when a doc view comment is made
+ view.dispatch(view.state.tr.setNodeMarkup(getPos(), null, { ...node.attrs, width: dashDoc._width + "px", height: dashDoc._height + "px" }));
+ } catch (e) {
+ console.log(e);
+ }
+ }
+ };
+ this._renderDisposer?.();
+ this._renderDisposer = reaction(() => {
+ if (!Doc.AreProtosEqual(finalLayout, dashDoc)) {
+ finalLayout.rootDocument = dashDoc.aliasOf;
+ }
+ const layoutKey = StrCast(finalLayout.layoutKey);
+ const finalKey = layoutKey && StrCast(finalLayout[layoutKey]).split("'")?.[1];
+ if (finalLayout !== dashDoc && finalKey) {
+ const finalLayoutField = finalLayout[finalKey];
+ if (finalLayoutField instanceof ObjectField) {
+ finalLayout[finalKey + "-textTemplate"] = ComputedField.MakeFunction(`copyField(this.${finalKey})`, { this: Doc.name });
+ }
+ }
+ return { finalLayout, resolvedDataDoc: Cast(finalLayout.resolvedDataDoc, Doc, null) };
+ },
+ (res) => doReactRender(res.finalLayout, res.resolvedDataDoc),
+ { fireImmediately: true });
}
}
destroy() {
- this._reactionDisposer && this._reactionDisposer();
+ ReactDOM.unmountComponentAtNode(this._dashSpan);
+ this._reactionDisposer?.();
}
}
export class DashFieldView {
- _fieldWrapper: HTMLDivElement;
- _labelSpan: HTMLSpanElement;
- _fieldSpan: HTMLSpanElement;
+ _fieldWrapper: HTMLDivElement; // container for label and value
+ _labelSpan: HTMLSpanElement; // field label
+ _fieldSpan: HTMLDivElement; // field value
+ _fieldCheck: HTMLInputElement;
+ _enumerables: HTMLDivElement; // field value
_reactionDisposer: IReactionDisposer | undefined;
_textBoxDoc: Doc;
@observable _dashDoc: Doc | undefined;
+ _fieldKey: string;
+ _options: Doc[] = [];
constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) {
+ this._fieldKey = node.attrs.fieldKey;
this._textBoxDoc = tbox.props.Document;
this._fieldWrapper = document.createElement("div");
this._fieldWrapper.style.width = node.attrs.width;
this._fieldWrapper.style.height = node.attrs.height;
this._fieldWrapper.style.position = "relative";
- this._fieldWrapper.style.display = "inline";
+ this._fieldWrapper.style.display = "inline-flex";
+
+ const self = this;
+ this._enumerables = document.createElement("div");
+ this._enumerables.style.width = "10px";
+ this._enumerables.style.height = "10px";
+ this._enumerables.style.position = "relative";
+ this._enumerables.style.display = "none";
+ this._enumerables.style.background = "dimGray";
+
+ this._enumerables.onpointerdown = async (e) => {
+ e.stopPropagation();
+ const collview = await Doc.addFieldEnumerations(self._textBoxDoc, self._fieldKey, [{ title: self._fieldSpan.innerText }]);
+ collview instanceof Doc && tbox.props.addDocTab(collview, "onRight");
+ };
+ const updateText = (forceMatch: boolean) => {
+ self._enumerables.style.display = "none";
+ const newText = self._fieldSpan.innerText.startsWith(":=") || self._fieldSpan.innerText.startsWith("=:=") ? ":=-computed-" : self._fieldSpan.innerText;
+
+ // look for a document whose id === the fieldKey being displayed. If there's a match, then that document
+ // holds the different enumerated values for the field in the titles of its collected documents.
+ // if there's a partial match from the start of the input text, complete the text --- TODO: make this an auto suggest box and select from a drop down.
+ DocServer.GetRefField(self._fieldKey).then(options => {
+ let modText = "";
+ (options instanceof Doc) && DocListCast(options.data).forEach(opt => (forceMatch ? StrCast(opt.title).startsWith(newText) : StrCast(opt.title) === newText) && (modText = StrCast(opt.title)));
+ if (modText) {
+ self._fieldSpan.innerHTML = self._dashDoc![self._fieldKey] = modText;
+ Doc.addFieldEnumerations(self._textBoxDoc, self._fieldKey, []);
+ } // if the text starts with a ':=' then treat it as an expression by making a computed field from its value storing it in the key
+ else if (self._fieldSpan.innerText.startsWith(":=")) {
+ self._dashDoc![self._fieldKey] = ComputedField.MakeFunction(self._fieldSpan.innerText.substring(2));
+ } else if (self._fieldSpan.innerText.startsWith("=:=")) {
+ Doc.Layout(tbox.props.Document)[self._fieldKey] = ComputedField.MakeFunction(self._fieldSpan.innerText.substring(3));
+ } else {
+ self._dashDoc![self._fieldKey] = newText;
+ }
+ });
+ };
+
+
+ this._fieldCheck = document.createElement("input");
+ this._fieldCheck.id = Utils.GenerateGuid();
+ this._fieldCheck.type = "checkbox";
+ this._fieldCheck.style.position = "relative";
+ this._fieldCheck.style.display = "none";
+ this._fieldCheck.style.minWidth = "12px";
+ this._fieldCheck.style.backgroundColor = "rgba(155, 155, 155, 0.24)";
+ this._fieldCheck.onchange = function (e: any) {
+ self._dashDoc![self._fieldKey] = e.target.checked;
+ };
- this._fieldSpan = document.createElement("span");
+ this._fieldSpan = document.createElement("div");
+ this._fieldSpan.id = Utils.GenerateGuid();
+ this._fieldSpan.contentEditable = "true";
this._fieldSpan.style.position = "relative";
- this._fieldSpan.style.display = "inline";
+ this._fieldSpan.style.display = "none";
+ this._fieldSpan.style.minWidth = "12px";
+ this._fieldSpan.style.backgroundColor = "rgba(155, 155, 155, 0.24)";
+ this._fieldSpan.onkeypress = function (e: any) { e.stopPropagation(); };
+ this._fieldSpan.onkeyup = function (e: any) { e.stopPropagation(); };
+ this._fieldSpan.onmousedown = function (e: any) { e.stopPropagation(); self._enumerables.style.display = "inline-block"; };
+ this._fieldSpan.onblur = function (e: any) { updateText(false); };
+
+ const setDashDoc = (doc: Doc) => {
+ self._dashDoc = doc;
+ if (self._options?.length && !self._dashDoc[self._fieldKey]) {
+ self._dashDoc[self._fieldKey] = StrCast(self._options[0].title);
+ }
+ // NOTE: if the field key starts with "@", then the actual field key is stored in the field 'fieldKey' (removing the @).
+ self._fieldKey = self._fieldKey.startsWith("@") ? StrCast(tbox.props.Document[StrCast(self._fieldKey).substring(1)]) : self._fieldKey;
+ this._labelSpan.innerHTML = `${self._fieldKey}: `;
+ const fieldVal = Cast(this._dashDoc?.[self._fieldKey], "boolean", null);
+ this._fieldCheck.style.display = (fieldVal === true || fieldVal === false) ? "inline-block" : "none";
+ this._fieldSpan.style.display = !(fieldVal === true || fieldVal === false) ? "inline-block" : "none";
+ };
+ this._fieldSpan.onkeydown = function (e: any) {
+ e.stopPropagation();
+ if ((e.key === "a" && e.ctrlKey) || (e.key === "a" && e.metaKey)) {
+ if (window.getSelection) {
+ const range = document.createRange();
+ range.selectNodeContents(self._fieldSpan);
+ window.getSelection()!.removeAllRanges();
+ window.getSelection()!.addRange(range);
+ }
+ e.preventDefault();
+ }
+ if (e.key === "Enter") {
+ e.preventDefault();
+ e.ctrlKey && Doc.addFieldEnumerations(self._textBoxDoc, self._fieldKey, [{ title: self._fieldSpan.innerText }]);
+ updateText(true);
+ }
+ };
this._labelSpan = document.createElement("span");
+ this._labelSpan.style.backgroundColor = "rgba(155, 155, 155, 0.44)";
this._labelSpan.style.position = "relative";
- this._labelSpan.style.display = "inline";
- this._labelSpan.style.fontWeight = "bold";
- this._labelSpan.style.fontSize = "larger";
- this._labelSpan.innerHTML = `${node.attrs.fieldKey}: `;
+ this._labelSpan.style.display = "inline-block";
+ this._labelSpan.style.fontSize = "small";
+ this._labelSpan.title = "click to see related tags";
+ this._labelSpan.onpointerdown = function (e: any) {
+ e.stopPropagation();
+ let container = tbox.props.ContainingCollectionView;
+ while (container?.props.Document.isTemplateForField || container?.props.Document.isTemplateDoc) {
+ container = container.props.ContainingCollectionView;
+ }
+ if (container) {
+ const alias = Doc.MakeAlias(container.props.Document);
+ alias.viewType = CollectionViewType.Time;
+ let list = Cast(alias.schemaColumns, listSpec(SchemaHeaderField));
+ if (!list) {
+ alias.schemaColumns = list = new List<SchemaHeaderField>();
+ }
+ list.map(c => c.heading).indexOf(self._fieldKey) === -1 && list.push(new SchemaHeaderField(self._fieldKey, "#f1efeb"));
+ list.map(c => c.heading).indexOf("text") === -1 && list.push(new SchemaHeaderField("text", "#f1efeb"));
+ alias._pivotField = self._fieldKey;
+ tbox.props.addDocTab(alias, "onRight");
+ }
+ };
+ this._labelSpan.innerHTML = `${self._fieldKey}: `;
if (node.attrs.docid) {
- const self = this;
- DocServer.GetRefField(node.attrs.docid).then(async dashDoc => dashDoc instanceof Doc && runInAction(() => self._dashDoc = dashDoc));
+ DocServer.GetRefField(node.attrs.docid).then(async dashDoc => dashDoc instanceof Doc && runInAction(() => setDashDoc(dashDoc)));
} else {
- this._dashDoc = tbox.props.DataDoc || tbox.dataDoc;
+ setDashDoc(tbox.props.DataDoc || tbox.dataDoc);
}
this._reactionDisposer?.();
- this._reactionDisposer = reaction(() => this._dashDoc?.[node.attrs.fieldKey], fval => this._fieldSpan.innerHTML = Field.toString(fval as Field), { fireImmediately: true });
+ this._reactionDisposer = reaction(() => { // this reaction will update the displayed text whenever the document's fieldKey's value changes
+ const dashVal = this._dashDoc?.[self._fieldKey];
+ return StrCast(dashVal).startsWith(":=") || dashVal === "" ? Doc.Layout(tbox.props.Document)[self._fieldKey] : dashVal;
+ }, fval => {
+ const boolVal = Cast(fval, "boolean", null);
+ if (boolVal === true || boolVal === false) {
+ this._fieldCheck.checked = boolVal;
+ } else {
+ this._fieldSpan.innerHTML = Field.toString(fval as Field) || "";
+ }
+ this._fieldCheck.style.display = (boolVal === true || boolVal === false) ? "inline-block" : "none";
+ this._fieldSpan.style.display = !(boolVal === true || boolVal === false) ? "inline-block" : "none";
+ }, { fireImmediately: true });
this._fieldWrapper.appendChild(this._labelSpan);
+ this._fieldWrapper.appendChild(this._fieldCheck);
this._fieldWrapper.appendChild(this._fieldSpan);
+ this._fieldWrapper.appendChild(this._enumerables);
(this as any).dom = this._fieldWrapper;
+ //updateText(false);
}
destroy() {
this._reactionDisposer?.();
}
+ selectNode() { }
}
export class OrderedListView {
@@ -945,12 +1095,12 @@ export class FootnoteView {
"Mod-y": () => redo(this.outerView.state, this.outerView.dispatch),
"Mod-b": toggleMark(schema.marks.strong)
}),
- new Plugin({
- view(newView) {
- // TODO -- make this work with RichTextMenu
- // return FormattedTextBox.getToolTip(newView);
- }
- })
+ // new Plugin({
+ // view(newView) {
+ // // TODO -- make this work with RichTextMenu
+ // // return FormattedTextBox.getToolTip(newView);
+ // }
+ // })
],
}),
@@ -1104,7 +1254,7 @@ const fromJson = schema.nodeFromJSON;
schema.nodeFromJSON = (json: any) => {
const node = fromJson(json);
- if (json.type === schema.marks.summarize.name) {
+ if (json.type === schema.nodes.summary.name) {
node.attrs.text = Slice.fromJSON(schema, node.attrs.textslice);
}
return node;
diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts
index 0fa96963e..ce21b7fa7 100644
--- a/src/client/util/Scripting.ts
+++ b/src/client/util/Scripting.ts
@@ -215,6 +215,8 @@ function forEachNode(node: ts.Node, onEnter: Traverser, onExit?: Traverser, inde
export function CompileScript(script: string, options: ScriptOptions = {}): CompileResult {
const { requiredType = "", addReturn = false, params = {}, capturedVariables = {}, typecheck = true } = options;
+ if (options.params && !options.params.this) options.params.this = Doc.name;
+ if (options.params && !options.params.self) options.params.self = Doc.name;
if (options.globals) {
Scripting.setScriptingGlobals(options.globals);
}
diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts
index 64874b994..b597f1e07 100644
--- a/src/client/util/SearchUtil.ts
+++ b/src/client/util/SearchUtil.ts
@@ -34,7 +34,7 @@ export namespace SearchUtil {
export function Search(query: string, returnDocs: false, options?: SearchParams): Promise<IdSearchResult>;
export async function Search(query: string, returnDocs: boolean, options: SearchParams = {}) {
query = query || "*"; //If we just have a filter query, search for * as the query
- const rpquery = Utils.prepend("/search");
+ const rpquery = Utils.prepend("/dashsearch");
const gotten = await rp.get(rpquery, { qs: { ...options, q: query } });
const result: IdSearchResult = gotten.startsWith("<") ? { ids: [], docs: [], numFound: 0, lines: [] } : JSON.parse(gotten);
if (!returnDocs) {
@@ -52,7 +52,7 @@ export namespace SearchUtil {
const newLines: string[][] = [];
await Promise.all(fileids.map(async (tr: string, i: number) => {
const docQuery = "fileUpload_t:" + tr.substr(0, 7); //If we just have a filter query, search for * as the query
- const docResult = JSON.parse(await rp.get(Utils.prepend("/search"), { qs: { ...options, q: docQuery } }));
+ const docResult = JSON.parse(await rp.get(Utils.prepend("/dashsearch"), { qs: { ...options, q: docQuery } }));
newIds.push(...docResult.ids);
newLines.push(...docResult.ids.map((dr: any) => txtresult.lines[i]));
}));
@@ -121,12 +121,12 @@ export namespace SearchUtil {
export async function GetAllDocs() {
const query = "*";
- let response = await rp.get(Utils.prepend('/search'), {
+ const response = await rp.get(Utils.prepend('/dashsearch'), {
qs:
{ start: 0, rows: 10000, q: query },
});
- let result: IdSearchResult = JSON.parse(response);
+ const result: IdSearchResult = JSON.parse(response);
const { ids, numFound, highlighting } = result;
//console.log(ids.length);
const docMap = await DocServer.GetRefFields(ids);
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index 89b900115..b7d88c54c 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -3,8 +3,6 @@ import { Doc } from "../../new_fields/Doc";
import { DocumentView } from "../views/nodes/DocumentView";
import { computedFn } from "mobx-utils";
import { List } from "../../new_fields/List";
-import { DocumentDecorations } from "../views/DocumentDecorations";
-import RichTextMenu from "./RichTextMenu";
export namespace SelectionManager {
diff --git a/src/client/util/SettingsManager.scss b/src/client/util/SettingsManager.scss
index 7a0fb0741..6513cb223 100644
--- a/src/client/util/SettingsManager.scss
+++ b/src/client/util/SettingsManager.scss
@@ -1,6 +1,6 @@
@import "../views/globalCssVariables";
-.dialogue-box {
+.settings-interface {
background-color: whitesmoke !important;
color: grey;
width: 450px;
diff --git a/src/client/util/type_decls.d b/src/client/util/type_decls.d
index 127f7b798..97f6b79fb 100644
--- a/src/client/util/type_decls.d
+++ b/src/client/util/type_decls.d
@@ -207,4 +207,5 @@ declare const Docs: {
StackingDocument(documents: Doc[], options?: DocumentOptions): Doc;
};
+declare function assignDoc(doc:Doc, field:any, id:any):string;
declare function d(...args:any[]):any;
diff --git a/src/client/views/AntimodeMenu.tsx b/src/client/views/AntimodeMenu.tsx
index 4625eb92f..fba2fb5c6 100644
--- a/src/client/views/AntimodeMenu.tsx
+++ b/src/client/views/AntimodeMenu.tsx
@@ -143,7 +143,7 @@ export default abstract class AntimodeMenu extends React.Component {
protected getElementWithRows(rows: JSX.Element[], numRows: number, hasDragger: boolean = true) {
return (
<div className="antimodeMenu-cont with-rows" onPointerLeave={this.pointerLeave} onPointerEnter={this.pointerEntered} ref={this._mainCont} onContextMenu={this.handleContextMenu}
- style={{ left: this._left, top: this._top, opacity: this._opacity, transition: this._transition, transitionDelay: this._transitionDelay, height: 35 * numRows + "px" }}>
+ style={{ left: this._left, top: this._top, opacity: this._opacity, transition: this._transition, transitionDelay: this._transitionDelay, height: "auto" }}>
{rows}
{hasDragger ? <div className="antimodeMenu-dragger" onPointerDown={this.dragStart} style={{ width: this.Pinned ? "20px" : "0px" }} /> : <></>}
</div>
diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx
index ac803d977..4d04d4e89 100644
--- a/src/client/views/ContextMenu.tsx
+++ b/src/client/views/ContextMenu.tsx
@@ -134,13 +134,13 @@ export class ContextMenu extends React.Component {
}
@action
- displayMenu = (x: number, y: number) => {
+ displayMenu = (x: number, y: number, initSearch = "") => {
//maxX and maxY will change if the UI/font size changes, but will work for any amount
//of items added to the menu
this._pageX = x;
this._pageY = y;
- this._searchString = "";
+ this._searchString = initSearch;
this._shouldDisplay = true;
}
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index ce48e1215..f4e830a48 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -1,4 +1,4 @@
-import { Doc } from '../../new_fields/Doc';
+import { Doc, Opt, DataSym } from '../../new_fields/Doc';
import { Touchable } from './Touchable';
import { computed, action, observable } from 'mobx';
import { Cast } from '../../new_fields/Types';
@@ -11,12 +11,13 @@ import { PositionDocument } from '../../new_fields/documentSchemas';
/// DocComponent returns a generic React base class used by views that don't have any data extensions (e.g.,CollectionFreeFormDocumentView, DocumentView, ButtonBox)
interface DocComponentProps {
Document: Doc;
+ LayoutDoc?: () => Opt<Doc>;
}
export function DocComponent<P extends DocComponentProps, T>(schemaCtor: (doc: Doc) => T) {
class Component extends Touchable<P> {
//TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then
@computed get Document(): T { return schemaCtor(this.props.Document); }
- @computed get layoutDoc() { return PositionDocument(Doc.Layout(this.props.Document)); }
+ @computed get layoutDoc() { return PositionDocument(Doc.Layout(this.props.Document, this.props.LayoutDoc?.())); }
}
return Component;
}
@@ -57,7 +58,8 @@ export function DocAnnotatableComponent<P extends DocAnnotatableProps, T>(schema
//TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then
@computed get Document(): T { return schemaCtor(this.props.Document); }
@computed get layoutDoc() { return Doc.Layout(this.props.Document); }
- @computed get dataDoc() { return (this.props.DataDoc && (this.props.Document.isTemplateForField || this.props.Document.isTemplateDoc) ? this.props.DataDoc : Cast(this.props.Document.resolvedDataDoc, Doc, null) || Doc.GetProto(this.props.Document)) as Doc; }
+ @computed get dataDoc() { return this.props.DataDoc && (this.props.Document.isTemplateForField || this.props.Document.isTemplateDoc) ? this.props.DataDoc : this.props.Document[DataSym]; }
+
_annotationKey: string = "annotations";
public set annotationKey(val: string) { this._annotationKey = val; }
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 65d1ade2a..b95cc6627 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -1,17 +1,15 @@
import { IconProp, library } from '@fortawesome/fontawesome-svg-core';
-import { faArrowAltCircleDown, faArrowAltCircleUp, faCheckCircle, faCloudUploadAlt, faLink, faShare, faStopCircle, faSyncAlt, faTag, faTimes } from '@fortawesome/free-solid-svg-icons';
+import { faArrowAltCircleDown, faPhotoVideo, faArrowAltCircleUp, faCheckCircle, faCloudUploadAlt, faLink, faShare, faStopCircle, faSyncAlt, faTag, faTimes } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { action, computed, observable, runInAction } from "mobx";
import { observer } from "mobx-react";
import { Doc, DocListCast } from "../../new_fields/Doc";
-import { Id } from '../../new_fields/FieldSymbols';
import { RichTextField } from '../../new_fields/RichTextField';
import { NumCast, StrCast } from "../../new_fields/Types";
import { emptyFunction } from "../../Utils";
import { Pulls, Pushes } from '../apis/google_docs/GoogleApiClientUtils';
-import RichTextMenu from '../util/RichTextMenu';
import { UndoManager } from "../util/UndoManager";
-import { CollectionDockingView } from './collections/CollectionDockingView';
+import { CollectionDockingView, DockedFrameRenderer } from './collections/CollectionDockingView';
import { ParentDocSelector } from './collections/ParentDocumentSelector';
import './collections/ParentDocumentSelector.scss';
import './DocumentButtonBar.scss';
@@ -23,6 +21,8 @@ import { Template, Templates } from "./Templates";
import React = require("react");
import { DragManager } from '../util/DragManager';
import { MetadataEntryMenu } from './MetadataEntryMenu';
+import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils';
+import GoogleAuthenticationManager from '../apis/GoogleAuthenticationManager';
const higflyout = require("@hig/flyout");
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
@@ -37,6 +37,7 @@ library.add(faCheckCircle);
library.add(faCloudUploadAlt);
library.add(faSyncAlt);
library.add(faShare);
+library.add(faPhotoVideo);
const cloud: IconProp = "cloud-upload-alt";
const fetch: IconProp = "sync-alt";
@@ -105,7 +106,7 @@ export class DocumentButtonBar extends React.Component<{ views: (DocumentView |
this._pullColorAnimating = false;
});
- get view0() { return this.props.views && this.props.views.length ? this.props.views[0] : undefined; }
+ get view0() { return this.props.views?.[0]; }
@action
onLinkButtonMoved = (e: PointerEvent): void => {
@@ -117,15 +118,10 @@ export class DocumentButtonBar extends React.Component<{ views: (DocumentView |
dragComplete: dropEv => {
const linkDoc = dropEv.linkDragData?.linkDocument as Doc; // equivalent to !dropEve.aborted since linkDocument is only assigned on a completed drop
if (this.view0 && linkDoc) {
- const proto = Doc.GetProto(linkDoc);
- proto.sourceContext = this.view0.props.ContainingCollectionDoc;
-
- const anchor2Title = linkDoc.anchor2 instanceof Doc ? StrCast(linkDoc.anchor2.title) : "-untitled-";
- const anchor2Id = linkDoc.anchor2 instanceof Doc ? linkDoc.anchor2[Id] : "";
- const text = RichTextMenu.Instance.MakeLinkToSelection(linkDoc[Id], anchor2Title, e.ctrlKey ? "onRight" : "inTab", anchor2Id);
- if (linkDoc.anchor2 instanceof Doc) {
- proto.title = text === "" ? proto.title : text + " to " + linkDoc.anchor2.title; // TODO open to more descriptive descriptions of following in text link
- }
+ Doc.GetProto(linkDoc).linkRelationship = "hyperlink";
+ dropEv.linkDragData?.linkDropCallback?.(dropEv.linkDragData);
+ runInAction(() => this.view0!._link = linkDoc);
+ setTimeout(action(() => this.view0!._link = undefined), 0);
}
linkDrag?.end();
},
@@ -161,7 +157,8 @@ export class DocumentButtonBar extends React.Component<{ views: (DocumentView |
title={`${published ? "Push" : "Publish"} to Google Docs`}
className="documentButtonBar-linker"
style={{ animation }}
- onClick={() => {
+ onClick={async () => {
+ await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken();
!published && runInAction(() => this.isAnimatingPulse = true);
DocumentButtonBar.hasPushedHack = false;
targetDoc[Pushes] = NumCast(targetDoc[Pushes]) + 1;
@@ -197,6 +194,27 @@ export class DocumentButtonBar extends React.Component<{ views: (DocumentView |
/>
</div>;
}
+ @computed
+ get pinButton() {
+ const targetDoc = this.view0?.props.Document;
+ const isPinned = targetDoc && CurrentUserUtils.IsDocPinned(targetDoc);
+ return !targetDoc ? (null) : <div className="documentButtonBar-linker"
+ title={CurrentUserUtils.IsDocPinned(targetDoc) ? "Unpin from presentation" : "Pin to presentation"}
+ style={{ backgroundColor: isPinned ? "black" : "white", color: isPinned ? "white" : "black" }}
+
+ onClick={e => {
+ if (isPinned) {
+ DockedFrameRenderer.UnpinDoc(targetDoc);
+ }
+ else {
+ targetDoc.sourceContext = this.view0?.props.ContainingCollectionDoc; // bcz: !! Shouldn't need this ... use search to lookup contexts dynamically
+ DockedFrameRenderer.PinDoc(targetDoc);
+ }
+ }}>
+ <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="map-pin"
+ />
+ </div>;
+ }
@computed
get linkButton() {
@@ -215,10 +233,10 @@ export class DocumentButtonBar extends React.Component<{ views: (DocumentView |
@computed
get metadataButton() {
const view0 = this.view0;
- return !view0 ? (null) : <div title="Show metadata panel" className="documentButtonBar-linkFlyout" ref={this._linkButton}>
+ return !view0 ? (null) : <div title="Show metadata panel" className="documentButtonBar-linkFlyout">
<Flyout anchorPoint={anchorPoints.LEFT_TOP}
content={<MetadataEntryMenu docs={() => this.props.views.filter(dv => dv).map(dv => dv!.props.Document)} suggestWithFunction /> /* tfs: @bcz This might need to be the data document? */}>
- <div className={"documentButtonBar-linkButton-" + "empty"} >
+ <div className={"documentButtonBar-linkButton-" + "empty"} onPointerDown={e => e.stopPropagation()} >
{<FontAwesomeIcon className="documentdecorations-icon" icon="tag" size="sm" />}
</div>
</Flyout>
@@ -227,10 +245,10 @@ export class DocumentButtonBar extends React.Component<{ views: (DocumentView |
@computed
get contextButton() {
- return !this.view0 ? (null) : <ParentDocSelector Views={this.props.views.filter(v => v).map(v => v as DocumentView)} Document={this.view0.props.Document} addDocTab={(doc, data, where) => {
- where === "onRight" ? CollectionDockingView.AddRightSplit(doc, data) :
- this.props.stack ? CollectionDockingView.Instance.AddTab(this.props.stack, doc, data) :
- this.view0?.props.addDocTab(doc, data, "onRight");
+ return !this.view0 ? (null) : <ParentDocSelector Document={this.view0.props.Document} addDocTab={(doc, where) => {
+ where === "onRight" ? CollectionDockingView.AddRightSplit(doc) :
+ this.props.stack ? CollectionDockingView.Instance.AddTab(this.props.stack, doc) :
+ this.view0?.props.addDocTab(doc, "onRight");
return true;
}} />;
}
@@ -277,8 +295,8 @@ export class DocumentButtonBar extends React.Component<{ views: (DocumentView |
const view0 = this.view0;
const templates: Map<Template, boolean> = new Map();
Array.from(Object.values(Templates.TemplateList)).map(template =>
- templates.set(template, this.props.views.reduce((checked, doc) => checked || doc?.getLayoutPropStr("show" + template.Name) ? true : false, false as boolean)));
- return !view0 ? (null) : <div title="Customize layout" className="documentButtonBar-linkFlyout" ref={this._dragRef}>
+ templates.set(template, this.props.views.reduce((checked, doc) => checked || doc?.props.Document["_show" + template.Name] ? true : false, false as boolean)));
+ return !view0 ? (null) : <div title="Tap: Customize layout. Drag: Create alias" className="documentButtonBar-linkFlyout" ref={this._dragRef}>
<Flyout anchorPoint={anchorPoints.LEFT_TOP}
content={<TemplateMenu docViews={this.props.views.filter(v => v).map(v => v as DocumentView)} templates={templates} />}>
<div className={"documentButtonBar-linkButton-" + "empty"} ref={this._dragRef} onPointerDown={this.onAliasButtonDown} >
@@ -291,7 +309,7 @@ export class DocumentButtonBar extends React.Component<{ views: (DocumentView |
render() {
if (!this.view0) return (null);
- const isText = this.view0.props.Document.data instanceof RichTextField; // bcz: Todo - can't assume layout is using the 'data' field. need to add fieldKey to DocumentView
+ const isText = this.view0.props.Document[Doc.LayoutFieldKey(this.view0.props.Document)] instanceof RichTextField;
const considerPull = isText && this.considerGoogleDocsPull;
const considerPush = isText && this.considerGoogleDocsPush;
return <div className="documentButtonBar">
@@ -307,6 +325,9 @@ export class DocumentButtonBar extends React.Component<{ views: (DocumentView |
<div className="documentButtonBar-button">
{this.contextButton}
</div>
+ <div className="documentButtonBar-button">
+ {this.pinButton}
+ </div>
<div className="documentButtonBar-button" style={{ display: !considerPush ? "none" : "" }}>
{this.considerGoogleDocsPush}
</div>
diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index 32346165d..353520026 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -27,6 +27,17 @@ $linkGap : 3px;
opacity: 1;
}
+ .documentDecorations-selector {
+ pointer-events: auto;
+ height: 15px;
+ width: 15px;
+ left: -20px;
+ top: 20px;
+ display: inline-block;
+ position: absolute;
+ opacity: 0.5;
+ }
+
.documentDecorations-radius {
pointer-events: auto;
background: black;
@@ -69,6 +80,7 @@ $linkGap : 3px;
#documentDecorations-topLeftResizer,
#documentDecorations-bottomRightResizer {
cursor: nwse-resize;
+ background: dimGray;
}
#documentDecorations-bottomRightResizer {
@@ -78,6 +90,7 @@ $linkGap : 3px;
#documentDecorations-topRightResizer,
#documentDecorations-bottomLeftResizer {
cursor: nesw-resize;
+ background: dimGray;
}
#documentDecorations-topResizer,
@@ -90,7 +103,15 @@ $linkGap : 3px;
cursor: ew-resize;
}
- .title {
+ .documentDecorations-contextMenu {
+ background: $alt-accent;
+ width: 25px;
+ height: calc(100% + 8px); // 8px for the height of the top resizer bar
+ grid-column-start: 1;
+ grid-column-end : 2;
+ pointer-events: all;
+ }
+ .documentDecorations-title {
background: $alt-accent;
opacity: 1;
grid-column-start: 3;
@@ -98,6 +119,18 @@ $linkGap : 3px;
pointer-events: auto;
overflow: hidden;
text-align: center;
+ display:flex;
+ }
+ .publishBox {
+ width: 20px;
+ height: 22px;
+ grid-column-start: 3;
+ grid-column-end: 4;
+ pointer-events: all;
+ background: darkgray;
+ display: inline-block;
+ position: absolute;
+ right: 0;
}
}
@@ -127,6 +160,7 @@ $linkGap : 3px;
padding-top: 5px;
width: $MINIMIZED_ICON_SIZE;
height: $MINIMIZED_ICON_SIZE;
+ max-height: 20px;
}
.documentDecorations-background {
@@ -153,11 +187,12 @@ $linkGap : 3px;
.link-button-container {
margin-top: $linkGap;
- grid-column: 1/4;
width: max-content;
height: auto;
display: flex;
flex-direction: row;
+ z-index: 998;
+ position: absolute;
}
.linkButtonWrapper {
@@ -246,6 +281,10 @@ $linkGap : 3px;
}
}
+.documentDecorations-darkScheme {
+ background: dimgray;
+}
+
#template-list {
position: absolute;
top: 25px;
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index c5034b901..79600b7c1 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -1,15 +1,14 @@
import { IconProp, library } from '@fortawesome/fontawesome-svg-core';
-import { faArrowAltCircleDown, faArrowAltCircleUp, faCheckCircle, faCloudUploadAlt, faLink, faShare, faStopCircle, faSyncAlt, faTag, faTimes } from '@fortawesome/free-solid-svg-icons';
+import { faCaretUp, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote, faTextHeight, faArrowAltCircleDown, faArrowAltCircleUp, faCheckCircle, faCloudUploadAlt, faLink, faShare, faStopCircle, faSyncAlt, faTag, faTimes } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, computed, observable, reaction } from "mobx";
+import { action, computed, observable, reaction, runInAction } from "mobx";
import { observer } from "mobx-react";
-import { Doc } from "../../new_fields/Doc";
+import { Doc, DataSym } from "../../new_fields/Doc";
import { PositionDocument } from '../../new_fields/documentSchemas';
-import { ObjectField } from '../../new_fields/ObjectField';
import { ScriptField } from '../../new_fields/ScriptField';
-import { Cast, StrCast } from "../../new_fields/Types";
+import { Cast, StrCast, NumCast } from "../../new_fields/Types";
import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils';
-import { Utils } from "../../Utils";
+import { Utils, setupMoveUpEvents, emptyFunction, returnFalse } from "../../Utils";
import { DocUtils } from "../documents/Documents";
import { DocumentType } from '../documents/DocumentTypes';
import { DragManager } from "../util/DragManager";
@@ -18,12 +17,15 @@ import { undoBatch, UndoManager } from "../util/UndoManager";
import { DocumentButtonBar } from './DocumentButtonBar';
import './DocumentDecorations.scss';
import { DocumentView } from "./nodes/DocumentView";
-import { IconBox } from "./nodes/IconBox";
import React = require("react");
-const higflyout = require("@hig/flyout");
-export const { anchorPoints } = higflyout;
-export const Flyout = higflyout.default;
+import { Id } from '../../new_fields/FieldSymbols';
+import e = require('express');
+library.add(faCaretUp);
+library.add(faObjectGroup);
+library.add(faStickyNote);
+library.add(faFilePdf);
+library.add(faFilm, faTextHeight);
library.add(faLink);
library.add(faTag);
library.add(faTimes);
@@ -35,41 +37,71 @@ library.add(faCloudUploadAlt);
library.add(faSyncAlt);
library.add(faShare);
+export type CloseCall = (toBeDeleted: DocumentView[]) => void;
+
@observer
export class DocumentDecorations extends React.Component<{}, { value: string }> {
static Instance: DocumentDecorations;
- private _isPointerDown = false;
- private _resizing = "";
- private _keyinput: React.RefObject<HTMLInputElement>;
+ private _resizeHdlId = "";
+ private _keyinput = React.createRef<HTMLInputElement>();
private _resizeBorderWidth = 16;
private _linkBoxHeight = 20 + 3; // link button height + margin
private _titleHeight = 20;
- private _downX = 0;
- private _downY = 0;
private _resizeUndo?: UndoManager.Batch;
- private _radiusDown = [0, 0];
@observable private _accumulatedTitle = "";
@observable private _titleControlString: string = "#title";
@observable private _edtingTitle = false;
@observable private _hidden = false;
- @observable private _opacity = 1;
- @observable public Interacting = false;
+ @observable private _addedCloseCalls: CloseCall[] = [];
+ @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";
- @observable public openHover = false;
constructor(props: Readonly<{}>) {
super(props);
DocumentDecorations.Instance = this;
- this._keyinput = React.createRef();
reaction(() => SelectionManager.SelectedDocuments().slice(), docs => this.titleBlur(false));
}
- @action titleChanged = (event: any) => this._accumulatedTitle = event.target.value;
+ @computed
+ get Bounds(): { x: number, y: number, b: number, r: number } {
+ return SelectionManager.SelectedDocuments().reduce((bounds, documentView) => {
+ if (documentView.props.renderDepth === 0 ||
+ //documentView.props.Document.dontSelect ||
+ Doc.AreProtosEqual(documentView.props.Document, CurrentUserUtils.UserDocument)) {
+ return bounds;
+ }
+ const transform = (documentView.props.ScreenToLocalTransform().scale(documentView.props.ContentScaling())).inverse();
+ var [sptX, sptY] = transform.transformPoint(0, 0);
+ let [bptX, bptY] = transform.transformPoint(documentView.props.PanelWidth(), documentView.props.PanelHeight());
+ if (documentView.props.Document.type === DocumentType.LINK) {
+ const docuBox = documentView.ContentDiv!.getElementsByClassName("docuLinkBox-cont");
+ if (docuBox.length) {
+ const rect = docuBox[0].getBoundingClientRect();
+ sptX = rect.left;
+ sptY = rect.top;
+ bptX = rect.right;
+ bptY = rect.bottom;
+ }
+ }
+ return {
+ x: Math.min(sptX, bounds.x), y: Math.min(sptY, bounds.y),
+ r: Math.max(bptX, bounds.r), b: Math.max(bptY, bounds.b)
+ };
+ }, { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: Number.MIN_VALUE, b: Number.MIN_VALUE });
+ }
- titleBlur = undoBatch(action((commit: boolean) => {
+ addCloseCall = (handler: CloseCall) => {
+ const currentOffset = this._addedCloseCalls.length - 1;
+ this._addedCloseCalls.push((toBeDeleted: DocumentView[]) => {
+ this._addedCloseCalls.splice(currentOffset, 1);
+ handler(toBeDeleted);
+ });
+ }
+
+ titleBlur = action((commit: boolean) => {
this._edtingTitle = false;
if (commit) {
if (this._accumulatedTitle.startsWith("#") || this._accumulatedTitle.startsWith("=")) {
@@ -77,12 +109,13 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
} else if (this._titleControlString.startsWith("#")) {
const selectionTitleFieldKey = this._titleControlString.substring(1);
selectionTitleFieldKey === "title" && (SelectionManager.SelectedDocuments()[0].props.Document.customTitle = !this._accumulatedTitle.startsWith("-"));
- selectionTitleFieldKey && SelectionManager.SelectedDocuments().forEach(d =>
- Doc.SetInPlace(d.props.Document, selectionTitleFieldKey, typeof d.props.Document[selectionTitleFieldKey] === "number" ? +this._accumulatedTitle : this._accumulatedTitle, true)
- );
+ UndoManager.RunInBatch(() => selectionTitleFieldKey && SelectionManager.SelectedDocuments().forEach(d => {
+ const value = typeof d.props.Document[selectionTitleFieldKey] === "number" ? +this._accumulatedTitle : this._accumulatedTitle;
+ Doc.SetInPlace(d.props.Document, selectionTitleFieldKey, value, true);
+ }), "title blur");
}
}
- }));
+ });
@action titleEntered = (e: any) => {
const key = e.keyCode || e.which;
@@ -90,92 +123,69 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
if (key === 13) {
const text = e.target.value;
if (text.startsWith("::")) {
- const targetID = text.slice(2, text.length);
+ this._accumulatedTitle = text.slice(2, text.length);
const promoteDoc = SelectionManager.SelectedDocuments()[0];
- DocUtils.Publish(promoteDoc.props.Document, targetID, promoteDoc.props.addDocument, promoteDoc.props.removeDocument);
- } else if (text.startsWith(">")) {
- const fieldTemplateView = SelectionManager.SelectedDocuments()[0];
- SelectionManager.DeselectAll();
- const fieldTemplate = fieldTemplateView.props.Document;
- const containerView = fieldTemplateView.props.ContainingCollectionView;
- const docTemplate = fieldTemplateView.props.ContainingCollectionDoc;
- if (containerView && docTemplate) {
- const metaKey = text.startsWith(">>") ? text.slice(2, text.length) : text.slice(1, text.length);
- if (metaKey !== containerView.props.fieldKey && containerView.props.DataDoc) {
- const fd = fieldTemplate.data;
- fd instanceof ObjectField && (Doc.GetProto(containerView.props.DataDoc)[metaKey] = ObjectField.MakeCopy(fd));
- }
- fieldTemplate.title = metaKey;
- Doc.MakeMetadataFieldTemplate(fieldTemplate, Doc.GetProto(docTemplate));
- if (text.startsWith(">>")) {
- Doc.GetProto(docTemplate).layout = StrCast(fieldTemplateView.props.Document.layout).replace(/fieldKey={'[^']*'}/, `fieldKey={"${metaKey}"}`);
- }
- }
+ Doc.SetInPlace(promoteDoc.props.Document, "title", this._accumulatedTitle, true);
+ DocUtils.Publish(promoteDoc.props.Document, this._accumulatedTitle, promoteDoc.props.addDocument, promoteDoc.props.removeDocument);
}
e.target.blur();
}
}
@action onTitleDown = (e: React.PointerEvent): void => {
- this._downX = e.clientX;
- this._downY = e.clientY;
- e.stopPropagation();
- document.removeEventListener("pointermove", this.onTitleMove);
- document.removeEventListener("pointerup", this.onTitleUp);
- document.addEventListener("pointermove", this.onTitleMove);
- document.addEventListener("pointerup", this.onTitleUp);
+ setupMoveUpEvents(this, e, this.onBackgroundMove, (e) => { }, this.onTitleClick);
}
- @action onTitleMove = (e: PointerEvent): void => {
- if (Math.abs(e.clientX - this._downX) > 4 || Math.abs(e.clientY - this._downY) > 4) {
- this.Interacting = true;
- }
- if (this.Interacting) this.onBackgroundMove(e);
- e.stopPropagation();
+ @action onTitleClick = (e: PointerEvent): void => {
+ !this._edtingTitle && (this._accumulatedTitle = this._titleControlString.startsWith("#") ? this.selectionTitle : this._titleControlString);
+ this._edtingTitle = true;
+ setTimeout(() => this._keyinput.current!.focus(), 0);
}
- @action onTitleUp = (e: PointerEvent): void => {
- if (Math.abs(e.clientX - this._downX) < 4 || Math.abs(e.clientY - this._downY) < 4) {
- !this._edtingTitle && (this._accumulatedTitle = this._titleControlString.startsWith("#") ? this.selectionTitle : this._titleControlString);
- this._edtingTitle = true;
- setTimeout(() => this._keyinput.current!.focus(), 0);
- }
- document.removeEventListener("pointermove", this.onTitleMove);
- document.removeEventListener("pointerup", this.onTitleUp);
- this.onBackgroundUp(e);
+
+ @action onSettingsDown = (e: React.PointerEvent): void => {
+ setupMoveUpEvents(this, e, () => false, (e) => { }, this.onSettingsClick);
}
- @computed
- get Bounds(): { x: number, y: number, b: number, r: number } {
- return SelectionManager.SelectedDocuments().reduce((bounds, documentView) => {
- if (documentView.props.renderDepth === 0 ||
- Doc.AreProtosEqual(documentView.props.Document, CurrentUserUtils.UserDocument)) {
- return bounds;
- }
- const transform = (documentView.props.ScreenToLocalTransform().scale(documentView.props.ContentScaling())).inverse();
- var [sptX, sptY] = transform.transformPoint(0, 0);
- let [bptX, bptY] = transform.transformPoint(documentView.props.PanelWidth(), documentView.props.PanelHeight());
- if (documentView.props.Document.type === DocumentType.LINK) {
- const rect = documentView.ContentDiv!.getElementsByClassName("docuLinkBox-cont")[0].getBoundingClientRect();
- sptX = rect.left;
- sptY = rect.top;
- bptX = rect.right;
- bptY = rect.bottom;
- }
- return {
- x: Math.min(sptX, bounds.x), y: Math.min(sptY, bounds.y),
- r: Math.max(bptX, bounds.r), b: Math.max(bptY, bounds.b)
- };
- }, { x: Number.MAX_VALUE, y: Number.MAX_VALUE, r: Number.MIN_VALUE, b: Number.MIN_VALUE });
+ simulateMouseClick(element: Element, x: number, y: number, sx: number, sy: number) {
+ ["pointerdown", "pointerup"].map(event => element.dispatchEvent(
+ new PointerEvent(event, {
+ view: window,
+ bubbles: true,
+ cancelable: true,
+ button: 2,
+ pointerType: "mouse",
+ clientX: x,
+ clientY: y,
+ screenX: sx,
+ screenY: sy,
+ })));
+
+ element.dispatchEvent(
+ new MouseEvent("contextmenu", {
+ view: window,
+ bubbles: true,
+ cancelable: true,
+ button: 2,
+ clientX: x,
+ clientY: y,
+ movementX: 0,
+ movementY: 0,
+ screenX: sx,
+ screenY: sy,
+ }));
+ }
+ @action onSettingsClick = (e: PointerEvent): void => {
+ if (e.button === 0 && !e.altKey && !e.ctrlKey) {
+ let child = SelectionManager.SelectedDocuments()[0].ContentDiv!.children[0];
+ while (child.children.length && child.className !== "jsx-parser") child = child.children[0];
+ this.simulateMouseClick(child.children[0], e.clientX, e.clientY + 30, e.screenX, e.screenY + 30);
+ }
}
onBackgroundDown = (e: React.PointerEvent): void => {
- document.removeEventListener("pointermove", this.onBackgroundMove);
- document.removeEventListener("pointerup", this.onBackgroundUp);
- document.addEventListener("pointermove", this.onBackgroundMove);
- document.addEventListener("pointerup", this.onBackgroundUp);
- e.stopPropagation();
+ setupMoveUpEvents(this, e, this.onBackgroundMove, (e) => { }, (e) => { });
}
@action
- onBackgroundMove = (e: PointerEvent): void => {
+ onBackgroundMove = (e: PointerEvent, down: number[]): boolean => {
const dragDocView = SelectionManager.SelectedDocuments()[0];
const dragData = new DragManager.DocumentDragData(SelectionManager.SelectedDocuments().map(dv => dv.props.Document));
const [left, top] = dragDocView.props.ScreenToLocalTransform().scale(dragDocView.props.ContentScaling()).inverse().transformPoint(0, 0);
@@ -184,200 +194,130 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
dragData.isSelectionMove = true;
this.Interacting = true;
this._hidden = true;
- document.removeEventListener("pointermove", this.onBackgroundMove);
- document.removeEventListener("pointerup", this.onBackgroundUp);
- document.removeEventListener("pointermove", this.onTitleMove);
- document.removeEventListener("pointerup", this.onTitleUp);
DragManager.StartDocumentDrag(SelectionManager.SelectedDocuments().map(documentView => documentView.ContentDiv!), dragData, e.x, e.y, {
dragComplete: action(e => this._hidden = this.Interacting = false),
hideSource: true
});
- e.stopPropagation();
- }
-
- @action
- onBackgroundUp = (e: PointerEvent): void => {
- document.removeEventListener("pointermove", this.onBackgroundMove);
- document.removeEventListener("pointerup", this.onBackgroundUp);
- e.stopPropagation();
- e.preventDefault();
+ return true;
}
onCloseDown = (e: React.PointerEvent): void => {
- e.stopPropagation();
- if (e.button === 0) {
- document.removeEventListener("pointermove", this.onCloseMove);
- document.addEventListener("pointermove", this.onCloseMove);
- document.removeEventListener("pointerup", this.onCloseUp);
- document.addEventListener("pointerup", this.onCloseUp);
- }
- }
- onCloseMove = (e: PointerEvent): void => {
- e.stopPropagation();
- if (e.button === 0) {
- }
+ setupMoveUpEvents(this, e, (e, d) => false, (e) => { }, this.onCloseClick);
}
@undoBatch
@action
- onCloseUp = async (e: PointerEvent) => {
- e.stopPropagation();
+ onCloseClick = async (e: PointerEvent) => {
if (e.button === 0) {
const recent = Cast(CurrentUserUtils.UserDocument.recentlyClosed, Doc) as Doc;
const selected = SelectionManager.SelectedDocuments().slice();
SelectionManager.DeselectAll();
+ this._addedCloseCalls.forEach(handler => handler(selected));
+
selected.map(dv => {
recent && Doc.AddDocToList(recent, "data", dv.props.Document, undefined, true, true);
dv.props.removeDocument && dv.props.removeDocument(dv.props.Document);
});
- document.removeEventListener("pointermove", this.onCloseMove);
- document.removeEventListener("pointerup", this.onCloseUp);
}
}
@action
onMinimizeDown = (e: React.PointerEvent): void => {
- e.stopPropagation();
- if (e.button === 0) {
- document.removeEventListener("pointermove", this.onMinimizeMove);
- document.addEventListener("pointermove", this.onMinimizeMove);
- document.removeEventListener("pointerup", this.onMinimizeUp);
- document.addEventListener("pointerup", this.onMinimizeUp);
- }
- }
-
- @action
- onMinimizeMove = (e: PointerEvent): void => {
- e.stopPropagation();
- if (Math.abs(e.pageX - this._downX) > Utils.DRAG_THRESHOLD ||
- Math.abs(e.pageY - this._downY) > Utils.DRAG_THRESHOLD) {
- document.removeEventListener("pointermove", this.onMinimizeMove);
- document.removeEventListener("pointerup", this.onMinimizeUp);
- }
+ setupMoveUpEvents(this, e, (e, d) => false, (e) => { }, this.onMinimizeClick);
}
@undoBatch
@action
- onMinimizeUp = (e: PointerEvent): void => {
- e.stopPropagation();
+ onMinimizeClick = (e: PointerEvent): void => {
if (e.button === 0) {
- document.removeEventListener("pointermove", this.onMinimizeMove);
- document.removeEventListener("pointerup", this.onMinimizeUp);
const selectedDocs = SelectionManager.SelectedDocuments().map(sd => sd);
selectedDocs.map(dv => {
const layoutKey = Cast(dv.props.Document.layoutKey, "string", null);
const collapse = layoutKey !== "layout_icon";
if (collapse) {
+ dv.switchViews(collapse, "icon");
if (layoutKey && layoutKey !== "layout") dv.props.Document.deiconifyLayout = layoutKey.replace("layout_", "");
- dv.setCustomView(collapse, "icon");
} else {
const deiconifyLayout = Cast(dv.props.Document.deiconifyLayout, "string", null);
- dv.setCustomView(deiconifyLayout ? true : false, deiconifyLayout);
+ dv.switchViews(deiconifyLayout ? true : false, deiconifyLayout);
dv.props.Document.deiconifyLayout = undefined;
}
});
}
+ SelectionManager.DeselectAll();
+ }
+
+ @action
+ onSelectorUp = (e: React.PointerEvent): void => {
+ setupMoveUpEvents(this, e, returnFalse, emptyFunction, action((e) => {
+ const selDoc = SelectionManager.SelectedDocuments()?.[0];
+ if (selDoc) {
+ selDoc.props.ContainingCollectionView?.props.select(false);
+ }
+ }));
}
@action
onRadiusDown = (e: React.PointerEvent): void => {
- e.stopPropagation();
+ setupMoveUpEvents(this, e, this.onRadiusMove, (e) => this._resizeUndo?.end(), (e) => { });
if (e.button === 0) {
- this._radiusDown = [e.clientX, e.clientY];
- this._isPointerDown = true;
this._resizeUndo = UndoManager.StartBatch("DocDecs set radius");
- document.removeEventListener("pointermove", this.onRadiusMove);
- document.removeEventListener("pointerup", this.onRadiusUp);
- document.addEventListener("pointermove", this.onRadiusMove);
- document.addEventListener("pointerup", this.onRadiusUp);
}
}
- onRadiusMove = (e: PointerEvent): void => {
- let dist = Math.sqrt((e.clientX - this._radiusDown[0]) * (e.clientX - this._radiusDown[0]) + (e.clientY - this._radiusDown[1]) * (e.clientY - this._radiusDown[1]));
+ onRadiusMove = (e: PointerEvent, down: number[]): boolean => {
+ let dist = Math.sqrt((e.clientX - down[0]) * (e.clientX - down[0]) + (e.clientY - down[1]) * (e.clientY - down[1]));
dist = dist < 3 ? 0 : dist;
- SelectionManager.SelectedDocuments().map(dv => dv.props.Document.layout instanceof Doc ? dv.props.Document.layout : dv.props.Document.isTemplateForField ? dv.props.Document : Doc.GetProto(dv.props.Document)).
- map(d => d.borderRounding = `${Math.min(100, dist)}%`);
- e.stopPropagation();
- e.preventDefault();
- }
-
- onRadiusUp = (e: PointerEvent): void => {
- e.stopPropagation();
- e.preventDefault();
- this._isPointerDown = false;
- this._resizeUndo && this._resizeUndo.end();
- document.removeEventListener("pointermove", this.onRadiusMove);
- document.removeEventListener("pointerup", this.onRadiusUp);
+ SelectionManager.SelectedDocuments().map(dv => dv.props.Document).map(doc => doc.layout instanceof Doc ? doc.layout : doc.isTemplateForField ? doc : Doc.GetProto(doc)).
+ map(d => d.borderRounding = `${Math.max(0, dist)}px`);
+ return false;
}
- _lastX = 0;
- _lastY = 0;
@action
onPointerDown = (e: React.PointerEvent): void => {
- e.stopPropagation();
+ setupMoveUpEvents(this, e, this.onPointerMove, this.onPointerUp, (e) => { });
if (e.button === 0) {
- this._lastX = e.clientX;
- this._lastY = e.clientY;
- this._isPointerDown = true;
- this._resizing = e.currentTarget.id;
+ this._resizeHdlId = e.currentTarget.id;
this.Interacting = true;
this._resizeUndo = UndoManager.StartBatch("DocDecs resize");
- document.removeEventListener("pointermove", this.onPointerMove);
- document.addEventListener("pointermove", this.onPointerMove);
- document.removeEventListener("pointerup", this.onPointerUp);
- document.addEventListener("pointerup", this.onPointerUp);
}
}
-
- onPointerMove = (e: PointerEvent): void => {
- e.stopPropagation();
- e.preventDefault();
- if (!this._isPointerDown) {
- return;
- }
-
+ onPointerMove = (e: PointerEvent, down: number[], move: number[]): boolean => {
let dX = 0, dY = 0, dW = 0, dH = 0;
- const moveX = e.clientX - this._lastX; // e.movementX;
- const moveY = e.clientY - this._lastY; // e.movementY;
- this._lastX = e.clientX;
- this._lastY = e.clientY;
-
- switch (this._resizing) {
- case "":
- break;
+ switch (this._resizeHdlId) {
+ case "": break;
case "documentDecorations-topLeftResizer":
dX = -1;
dY = -1;
- dW = -moveX;
- dH = -moveY;
+ dW = -move[0];
+ dH = -move[1];
break;
case "documentDecorations-topRightResizer":
- dW = moveX;
+ dW = move[0];
dY = -1;
- dH = -moveY;
+ dH = -move[1];
break;
case "documentDecorations-topResizer":
dY = -1;
- dH = -moveY;
+ dH = -move[1];
break;
case "documentDecorations-bottomLeftResizer":
dX = -1;
- dW = -moveX;
- dH = moveY;
+ dW = -move[0];
+ dH = move[1];
break;
case "documentDecorations-bottomRightResizer":
- dW = moveX;
- dH = moveY;
+ dW = move[0];
+ dH = move[1];
break;
case "documentDecorations-bottomResizer":
- dH = moveY;
+ dH = move[1];
break;
case "documentDecorations-leftResizer":
dX = -1;
- dW = -moveX;
+ dW = -move[0];
break;
case "documentDecorations-rightResizer":
- dW = moveX;
+ dW = move[0];
break;
}
@@ -390,21 +330,31 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
const width = (layoutDoc._width || 0);
const height = (layoutDoc._height || (nheight / nwidth * width));
const scale = element.props.ScreenToLocalTransform().Scale * element.props.ContentScaling();
+ if (nwidth && nheight) {
+ if (Math.abs(dW) > Math.abs(dH)) dH = dW * nheight / nwidth;
+ else dW = dH * nwidth / nheight;
+ }
const actualdW = Math.max(width + (dW * scale), 20);
const actualdH = Math.max(height + (dH * scale), 20);
doc.x = (doc.x || 0) + dX * (actualdW - width);
doc.y = (doc.y || 0) + dY * (actualdH - height);
- const fixedAspect = e.ctrlKey || (!layoutDoc.ignoreAspect && nwidth && nheight);
- if (fixedAspect && e.ctrlKey && layoutDoc.ignoreAspect) {
- layoutDoc.ignoreAspect = false;
- layoutDoc._nativeWidth = nwidth = layoutDoc._width || 0;
- layoutDoc._nativeHeight = nheight = layoutDoc._height || 0;
- }
+ const fixedAspect = (nwidth && nheight);
if (fixedAspect && (!nwidth || !nheight)) {
layoutDoc._nativeWidth = nwidth = layoutDoc._width || 0;
layoutDoc._nativeHeight = nheight = layoutDoc._height || 0;
}
- if (nwidth > 0 && nheight > 0 && !layoutDoc.ignoreAspect) {
+ const anno = Cast(doc.annotationOn, Doc, null);
+ if (e.ctrlKey && anno) {
+ dW !== 0 && runInAction(() => {
+ const dataDoc = anno[DataSym];
+ const fieldKey = Doc.LayoutFieldKey(anno);
+ const nw = NumCast(dataDoc[fieldKey + "-nativeWidth"]);
+ const nh = NumCast(dataDoc[fieldKey + "-nativeHeight"]);
+ dataDoc[fieldKey + "-nativeWidth"] = nw + (dW > 0 ? 10 : -10);
+ dataDoc[fieldKey + "-nativeHeight"] = nh + (dW > 0 ? 10 : -10) * nh / nw;
+ });
+ }
+ else if (nwidth > 0 && nheight > 0) {
if (Math.abs(dW) > Math.abs(dH)) {
if (!fixedAspect) {
layoutDoc._nativeWidth = actualdW / (layoutDoc._width || 1) * (layoutDoc._nativeWidth || 0);
@@ -428,20 +378,15 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
}
}
}));
+ return false;
}
@action
onPointerUp = (e: PointerEvent): void => {
- e.stopPropagation();
- this._resizing = "";
+ this._resizeHdlId = "";
this.Interacting = false;
- if (e.button === 0) {
- e.preventDefault();
- this._isPointerDown = false;
- this._resizeUndo && this._resizeUndo.end();
- document.removeEventListener("pointermove", this.onPointerMove);
- document.removeEventListener("pointerup", this.onPointerUp);
- }
+ (e.button === 0) && this._resizeUndo?.end();
+ this._resizeUndo = undefined;
}
@computed
@@ -467,17 +412,54 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
this.TextBar = ele;
}
}
+ public static DocumentIcon(layout: string) {
+ const button = layout.indexOf("PDFBox") !== -1 ? faFilePdf :
+ layout.indexOf("ImageBox") !== -1 ? faImage :
+ layout.indexOf("Formatted") !== -1 ? faStickyNote :
+ layout.indexOf("Video") !== -1 ? faFilm :
+ layout.indexOf("Collection") !== -1 ? faObjectGroup :
+ faCaretUp;
+ return <FontAwesomeIcon icon={button} className="documentView-minimizedIcon" />;
+ }
render() {
+ const darkScheme = Cast(Doc.UserDoc().activeWorkspace, Doc, null)?.darkScheme ? "dimgray" : undefined;
const bounds = this.Bounds;
const seldoc = SelectionManager.SelectedDocuments().length ? SelectionManager.SelectedDocuments()[0] : undefined;
- if (SelectionManager.GetIsDragging() || bounds.x === Number.MAX_VALUE || !seldoc || this._hidden || isNaN(bounds.r) || isNaN(bounds.b) || isNaN(bounds.x) || isNaN(bounds.y)) {
+ if (SelectionManager.GetIsDragging() || bounds.r - bounds.x < 2 || bounds.x === Number.MAX_VALUE || !seldoc || this._hidden || isNaN(bounds.r) || isNaN(bounds.b) || isNaN(bounds.x) || isNaN(bounds.y)) {
return (null);
}
- const minimizeIcon = (
- <div className="documentDecorations-minimizeButton" onPointerDown={this.onMinimizeDown}>
- {/* Currently, this is set to be enabled if there is no ink selected. It might be interesting to think about minimizing ink if it's useful? -syip2*/}
- {SelectionManager.SelectedDocuments().length === 1 ? IconBox.DocumentIcon(StrCast(SelectionManager.SelectedDocuments()[0].props.Document.layout, "...")) : "..."}
- </div>);
+ const minimal = bounds.r - bounds.x < 100 ? true : false;
+ const minimizeIcon = minimal ? (
+ <div className="documentDecorations-contextMenu" title="Show context menu" onPointerDown={this.onSettingsDown}>
+ <FontAwesomeIcon size="lg" icon="cog" />
+ </div>) : (
+ <div className="documentDecorations-minimizeButton" title="Iconify" onPointerDown={this.onMinimizeDown}>
+ {/* Currently, this is set to be enabled if there is no ink selected. It might be interesting to think about minimizing ink if it's useful? -syip2*/}
+ {SelectionManager.SelectedDocuments().length === 1 ? DocumentDecorations.DocumentIcon(StrCast(seldoc.props.Document.layout, "...")) : "..."}
+ </div>);
+
+ const titleArea = this._edtingTitle ?
+ <>
+ <input ref={this._keyinput} className="documentDecorations-title" type="text" name="dynbox" autoComplete="on" value={this._accumulatedTitle} style={{ width: minimal ? "100%" : "calc(100% - 20px)" }}
+ onBlur={e => this.titleBlur(true)} onChange={action(e => this._accumulatedTitle = e.target.value)} onKeyPress={this.titleEntered} />
+ {minimal ? (null) : <div className="publishBox" title="make document referenceable by its title"
+ onPointerDown={action(e => {
+ if (!seldoc.props.Document.customTitle) {
+ seldoc.props.Document.customTitle = true;
+ StrCast(Doc.GetProto(seldoc.props.Document).title).startsWith("-") && (Doc.GetProto(seldoc.props.Document).title = StrCast(seldoc.props.Document.title).substring(1));
+ this._accumulatedTitle = StrCast(seldoc.props.Document.title);
+ }
+ DocUtils.Publish(seldoc.props.Document, this._accumulatedTitle, seldoc.props.addDocument, seldoc.props.removeDocument);
+ })}>
+ <FontAwesomeIcon size="lg" color={SelectionManager.SelectedDocuments()[0].props.Document.title === SelectionManager.SelectedDocuments()[0].props.Document[Id] ? "green" : undefined} icon="sticky-note"></FontAwesomeIcon>
+ </div>}
+ </> :
+ <div className="documentDecorations-title" onPointerDown={this.onTitleDown} >
+ {minimal ? (null) : <div className="documentDecorations-contextMenu" title="Show context menu" onPointerDown={this.onSettingsDown}>
+ <FontAwesomeIcon size="lg" icon="cog" />
+ </div>}
+ <span style={{ width: "calc(100% - 25px)", display: "inline-block" }}>{`${this.selectionTitle}`}</span>
+ </div>;
bounds.x = Math.max(0, bounds.x - this._resizeBorderWidth / 2) + this._resizeBorderWidth / 2;
bounds.y = Math.max(0, bounds.y - this._resizeBorderWidth / 2 - this._titleHeight) + this._resizeBorderWidth / 2 + this._titleHeight;
@@ -490,7 +472,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
if (bounds.y > bounds.b) {
bounds.y = bounds.b - (this._resizeBorderWidth + this._linkBoxHeight + this._titleHeight);
}
- return (<div className="documentDecorations">
+ return (<div className="documentDecorations" style={{ background: darkScheme }} >
<div className="documentDecorations-background" style={{
width: (bounds.r - bounds.x + this._resizeBorderWidth) + "px",
height: (bounds.b - bounds.y + this._resizeBorderWidth) + "px",
@@ -498,38 +480,48 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
top: bounds.y - this._resizeBorderWidth / 2,
pointerEvents: this.Interacting ? "none" : "all",
zIndex: SelectionManager.SelectedDocuments().length > 1 ? 900 : 0,
- }} onPointerDown={this.onBackgroundDown} onContextMenu={(e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); }} >
+ }} onPointerDown={this.onBackgroundDown} onContextMenu={e => { e.preventDefault(); e.stopPropagation(); }} >
</div>
<div className="documentDecorations-container" ref={this.setTextBar} style={{
width: (bounds.r - bounds.x + this._resizeBorderWidth) + "px",
- height: (bounds.b - bounds.y + this._resizeBorderWidth + this._linkBoxHeight + this._titleHeight + 3) + "px",
+ height: (bounds.b - bounds.y + this._resizeBorderWidth + this._titleHeight) + "px",
left: bounds.x - this._resizeBorderWidth / 2,
top: bounds.y - this._resizeBorderWidth / 2 - this._titleHeight,
- opacity: this._opacity
}}>
{minimizeIcon}
-
- {this._edtingTitle ?
- <input ref={this._keyinput} className="title" type="text" name="dynbox" value={this._accumulatedTitle} onBlur={e => this.titleBlur(true)} onChange={this.titleChanged} onKeyPress={this.titleEntered} /> :
- <div className="title" onPointerDown={this.onTitleDown} ><span>{`${this.selectionTitle}`}</span></div>}
+ {titleArea}
<div className="documentDecorations-closeButton" title="Close Document" onPointerDown={this.onCloseDown}>
<FontAwesomeIcon className="documentdecorations-times" icon={faTimes} size="lg" />
</div>
- <div id="documentDecorations-topLeftResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
- <div id="documentDecorations-topResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
- <div id="documentDecorations-topRightResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
- <div id="documentDecorations-leftResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
+ <div id="documentDecorations-topLeftResizer" className="documentDecorations-resizer"
+ onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
+ <div id="documentDecorations-topResizer" className="documentDecorations-resizer"
+ onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
+ <div id="documentDecorations-topRightResizer" className="documentDecorations-resizer"
+ onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
+ <div id="documentDecorations-leftResizer" className="documentDecorations-resizer"
+ onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
<div id="documentDecorations-centerCont"></div>
- <div id="documentDecorations-rightResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
- <div id="documentDecorations-bottomLeftResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
- <div id="documentDecorations-bottomResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
- <div id="documentDecorations-bottomRightResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
- <div id="documentDecorations-borderRadius" className="documentDecorations-radius" onPointerDown={this.onRadiusDown} onContextMenu={(e) => e.preventDefault()}><span className="borderRadiusTooltip" title="Drag Corner Radius"></span></div>
- <div className="link-button-container">
- <DocumentButtonBar views={SelectionManager.SelectedDocuments()} />
- </div>
+ <div id="documentDecorations-rightResizer" className="documentDecorations-resizer"
+ onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
+ <div id="documentDecorations-bottomLeftResizer" className="documentDecorations-resizer"
+ onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
+ <div id="documentDecorations-bottomResizer" className="documentDecorations-resizer"
+ onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
+ <div id="documentDecorations-bottomRightResizer" className="documentDecorations-resizer"
+ onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
+ {seldoc.props.renderDepth <= 1 || !seldoc.props.ContainingCollectionView ? (null) : <div id="documentDecorations-levelSelector" className="documentDecorations-selector" title="tap to select containing document"
+ onPointerDown={this.onSelectorUp} onContextMenu={(e) => e.preventDefault()}>
+ <FontAwesomeIcon className="documentdecorations-times" icon={faArrowAltCircleUp} size="lg" />
+ </div>}
+ <div id="documentDecorations-borderRadius" className="documentDecorations-radius"
+ onPointerDown={this.onRadiusDown} onContextMenu={(e) => e.preventDefault()}></div>
+
</div >
- </div>
+ <div className="link-button-container" style={{ left: bounds.x - this._resizeBorderWidth / 2, top: bounds.b + this._resizeBorderWidth / 2 }}>
+ <DocumentButtonBar views={SelectionManager.SelectedDocuments()} />
+ </div>
+ </div >
);
}
} \ No newline at end of file
diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx
index 780c5b2f4..2219966e5 100644
--- a/src/client/views/EditableView.tsx
+++ b/src/client/views/EditableView.tsx
@@ -4,8 +4,6 @@ import { observer } from 'mobx-react';
import * as Autosuggest from 'react-autosuggest';
import { ObjectField } from '../../new_fields/ObjectField';
import { SchemaHeaderField } from '../../new_fields/SchemaHeaderField';
-import { ContextMenu } from './ContextMenu';
-import { ContextMenuProps } from './ContextMenuItem';
import "./EditableView.scss";
export interface EditableProps {
@@ -46,6 +44,7 @@ export interface EditableProps {
onClick?: (e: React.MouseEvent) => boolean;
isEditingCallback?: (isEditing: boolean) => void;
menuCallback?: (x: number, y: number) => void;
+ showMenuOnLoad?: boolean;
HeadingObject?: SchemaHeaderField | undefined;
HeadingsHack?: number;
toggle?: () => void;
@@ -59,12 +58,14 @@ export interface EditableProps {
*/
@observer
export class EditableView extends React.Component<EditableProps> {
+ public static loadId = "";
@observable _editing: boolean = false;
@observable _headingsHack: number = 1;
constructor(props: EditableProps) {
super(props);
this._editing = this.props.editing ? true : false;
+ EditableView.loadId = "";
}
@action
@@ -74,19 +75,22 @@ export class EditableView extends React.Component<EditableProps> {
// to false. this will no longer do so -syip
if (nextProps.editing && nextProps.editing !== this._editing) {
this._editing = nextProps.editing;
+ EditableView.loadId = "";
}
}
+ _didShow = false;
+
@action
onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Tab") {
e.stopPropagation();
- this.finalizeEdit(e.currentTarget.value, e.shiftKey);
+ this.finalizeEdit(e.currentTarget.value, e.shiftKey, false);
this.props.OnTab && this.props.OnTab(e.shiftKey);
} else if (e.key === "Enter") {
e.stopPropagation();
if (!e.ctrlKey) {
- this.finalizeEdit(e.currentTarget.value, e.shiftKey);
+ this.finalizeEdit(e.currentTarget.value, e.shiftKey, false);
} else if (this.props.OnFillDown) {
this.props.OnFillDown(e.currentTarget.value);
this._editing = false;
@@ -97,25 +101,36 @@ export class EditableView extends React.Component<EditableProps> {
this._editing = false;
this.props.isEditingCallback?.(false);
} else if (e.key === ":") {
- this.props.menuCallback?.(e.currentTarget.offsetLeft, e.currentTarget.offsetTop);
+ this.props.menuCallback?.(e.currentTarget.getBoundingClientRect().x, e.currentTarget.getBoundingClientRect().y);
}
}
@action
onClick = (e: React.MouseEvent) => {
e.nativeEvent.stopPropagation();
- if (!this.props.onClick || !this.props.onClick(e)) {
- this._editing = true;
- this.props.isEditingCallback?.(true);
+ if (this._ref.current && this.props.showMenuOnLoad) {
+ this.props.menuCallback?.(this._ref.current.getBoundingClientRect().x, this._ref.current.getBoundingClientRect().y);
+ } else {
+ if (!this.props.onClick || !this.props.onClick(e)) {
+ this._editing = true;
+ this.props.isEditingCallback?.(true);
+ }
}
e.stopPropagation();
}
@action
- private finalizeEdit(value: string, shiftDown: boolean) {
- this._editing = false;
+ private finalizeEdit(value: string, shiftDown: boolean, lostFocus: boolean) {
if (this.props.SetValue(value, shiftDown)) {
+ this._editing = false;
+ this.props.isEditingCallback?.(false);
+ } else {
+ this._editing = false;
this.props.isEditingCallback?.(false);
+ !lostFocus && setTimeout(action(() => {
+ this._editing = true;
+ this.props.isEditingCallback?.(true);
+ }), 0);
}
}
@@ -130,6 +145,7 @@ export class EditableView extends React.Component<EditableProps> {
return wasFocused !== this._editing;
}
+ _ref = React.createRef<HTMLDivElement>();
render() {
if (this._editing && this.props.GetValue() !== undefined) {
return this.props.autosuggestProps
@@ -139,7 +155,7 @@ export class EditableView extends React.Component<EditableProps> {
className: "editableView-input",
onKeyDown: this.onKeyDown,
autoFocus: true,
- onBlur: e => this.finalizeEdit(e.currentTarget.value, false),
+ onBlur: e => this.finalizeEdit(e.currentTarget.value, false, true),
onPointerDown: this.stopPropagation,
onClick: this.stopPropagation,
onPointerUp: this.stopPropagation,
@@ -151,14 +167,15 @@ export class EditableView extends React.Component<EditableProps> {
defaultValue={this.props.GetValue()}
onKeyDown={this.onKeyDown}
autoFocus={true}
- onBlur={e => this.finalizeEdit(e.currentTarget.value, false)}
+ onBlur={e => this.finalizeEdit(e.currentTarget.value, false, true)}
onPointerDown={this.stopPropagation} onClick={this.stopPropagation} onPointerUp={this.stopPropagation}
style={{ display: this.props.display, fontSize: this.props.fontSize }}
/>;
} else {
- if (this.props.autosuggestProps) this.props.autosuggestProps.resetValue();
+ this.props.autosuggestProps?.resetValue();
return (this.props.contents instanceof ObjectField ? (null) :
<div className={`editableView-container-editing${this.props.oneLine ? "-oneLine" : ""}`}
+ ref={this._ref}
style={{ display: this.props.display, minHeight: "20px", height: `${this.props.height ? this.props.height : "auto"}`, maxHeight: `${this.props.maxHeight}` }}
onClick={this.onClick}>
<span style={{ fontStyle: this.props.fontStyle, fontSize: this.props.fontSize }}>{this.props.contents}</span>
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index 9a601e07b..ea60907f6 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -27,6 +27,9 @@ import { listSpec } from "../../new_fields/Schema";
import { List } from "../../new_fields/List";
import { CollectionViewType } from "./collections/CollectionView";
import TouchScrollableMenu, { TouchScrollableMenuItem } from "./TouchScrollableMenu";
+import MobileInterface from "../../mobile/MobileInterface";
+import { MobileInkOverlayContent } from "../../server/Message";
+import MobileInkOverlay from "../../mobile/MobileInkOverlay";
import { RadialMenu } from "./nodes/RadialMenu";
import { SelectionManager } from "../util/SelectionManager";
@@ -53,9 +56,11 @@ export default class GestureOverlay extends Touchable {
@observable private _clipboardDoc?: JSX.Element;
@observable private _possibilities: JSX.Element[] = [];
- @computed private get height(): number { return 2 * Math.max(this._pointerY && this._thumbY ? this._thumbY - this._pointerY : 300, 300); }
+ @computed private get height(): number { return 2 * Math.max(this._pointerY && this._thumbY ? this._thumbY - this._pointerY : 100, 100); }
@computed private get showBounds() { return this.Tool !== ToolglassTools.None; }
+ @observable private showMobileInkOverlay: boolean = false;
+
private _d1: Doc | undefined;
private _inkToTextDoc: Doc | undefined;
private _thumbDoc: Doc | undefined;
@@ -141,8 +146,6 @@ export default class GestureOverlay extends Touchable {
const nts = this.getNewTouches(te);
if (nts.nt.length < 5) {
const target = document.elementFromPoint(te.changedTouches.item(0).clientX, te.changedTouches.item(0).clientY);
- te.changedTouches.item(0).identifier;
- console.log(te.touches);
target?.dispatchEvent(
new CustomEvent<InteractionUtils.MultiTouchEvent<React.TouchEvent>>("dashOnTouchStart",
{
@@ -162,7 +165,7 @@ export default class GestureOverlay extends Touchable {
this._holdTimer = setTimeout(() => {
console.log("hold");
const target = document.elementFromPoint(te.changedTouches.item(0).clientX, te.changedTouches.item(0).clientY);
- let pt: any = te.touches[te.touches.length - 1];
+ const pt: any = te.touches[te.touches.length - 1];
if (nts.nt.length === 1 && pt.radiusX > 1 && pt.radiusY > 1) {
target?.dispatchEvent(
new CustomEvent<InteractionUtils.MultiTouchEvent<React.TouchEvent>>("dashOnTouchHoldStart",
@@ -532,7 +535,7 @@ export default class GestureOverlay extends Touchable {
}
else if (this._d1 !== doc && !LinkManager.Instance.doesLinkExist(this._d1, doc)) {
if (this._d1.type !== "ink" && doc.type !== "ink") {
- DocUtils.MakeLink({ doc: this._d1 }, { doc: doc });
+ DocUtils.MakeLink({ doc: this._d1 }, { doc: doc }, "gestural link");
actionPerformed = true;
}
}
@@ -558,6 +561,16 @@ export default class GestureOverlay extends Touchable {
const B = this.svgBounds;
const points = this._points.map(p => ({ X: p.X - B.left, Y: p.Y - B.top }));
+ if (MobileInterface.Instance && MobileInterface.Instance.drawingInk) {
+ const { selectedColor, selectedWidth } = InkingControl.Instance;
+ DocServer.Mobile.dispatchGesturePoints({
+ points: this._points,
+ bounds: B,
+ color: selectedColor,
+ width: selectedWidth
+ });
+ }
+
const initialPoint = this._points[0.];
const xInGlass = initialPoint.X > (this._thumbX ?? Number.MAX_SAFE_INTEGER) && initialPoint.X < (this._thumbX ?? Number.MAX_SAFE_INTEGER) + (this.height);
const yInGlass = initialPoint.Y > (this._thumbY ?? Number.MAX_SAFE_INTEGER) - (this.height) && initialPoint.Y < (this._thumbY ?? Number.MAX_SAFE_INTEGER);
@@ -576,7 +589,7 @@ export default class GestureOverlay extends Touchable {
for (const wR of wordResults) {
console.log(wR);
if (wR?.recognizedText) {
- possibilities.push(wR?.recognizedText)
+ possibilities.push(wR?.recognizedText);
}
possibilities.push(...wR?.alternates?.map((a: any) => a.recognizedString));
}
@@ -717,23 +730,29 @@ export default class GestureOverlay extends Touchable {
this._clipboardDoc = undefined;
}
+ @action
+ enableMobileInkOverlay = (content: MobileInkOverlayContent) => {
+ this.showMobileInkOverlay = content.enableOverlay;
+ }
+
render() {
trace();
return (
<div className="gestureOverlay-cont" onPointerDown={this.onPointerDown} onTouchStart={this.onReactTouchStart}>
+ {this.showMobileInkOverlay ? <MobileInkOverlay /> : <></>}
{this.elements}
<div className="clipboardDoc-cont" style={{
- transform: `translate(${this._thumbX}px, ${(this._thumbY ?? 0) - this.height}px)`,
height: this.height,
width: this.height,
pointerEvents: this._clipboardDoc ? "unset" : "none",
touchAction: this._clipboardDoc ? "unset" : "none",
+ transform: `translate(${this._thumbX}px, ${(this._thumbY || 0) - this.height} px)`,
}}>
{this._clipboardDoc}
</div>
<div className="filter-cont" style={{
- transform: `translate(${this._thumbX}px, ${(this._thumbY ?? 0) - this.height}px)`,
+ transform: `translate(${this._thumbX}px, ${(this._thumbY || 0) - this.height}px)`,
height: this.height,
width: this.height,
pointerEvents: "none",
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index 65d327392..52801b570 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -6,7 +6,6 @@ import { DragManager } from "../util/DragManager";
import { action, runInAction } from "mobx";
import { Doc } from "../../new_fields/Doc";
import { DictationManager } from "../util/DictationManager";
-import { RecommendationsBox } from "./RecommendationsBox";
import SharingManager from "../util/SharingManager";
import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
import { Cast, PromiseValue } from "../../new_fields/Types";
@@ -107,14 +106,14 @@ export default class KeyManager {
});
private shift = async (keyname: string) => {
- let stopPropagation = false;
- let preventDefault = false;
+ const stopPropagation = false;
+ const preventDefault = false;
switch (keyname) {
- case "~":
- DictationManager.Controls.listen({ useOverlay: true, tryExecute: true });
- stopPropagation = true;
- preventDefault = true;
+ // case "~":
+ // DictationManager.Controls.listen({ useOverlay: true, tryExecute: true });
+ // stopPropagation = true;
+ // preventDefault = true;
}
return {
@@ -158,7 +157,7 @@ export default class KeyManager {
return { stopPropagation: false, preventDefault: false };
}
}
- MainView.Instance.mainFreeform && CollectionDockingView.AddRightSplit(MainView.Instance.mainFreeform, undefined);
+ MainView.Instance.mainFreeform && CollectionDockingView.AddRightSplit(MainView.Instance.mainFreeform);
break;
case "arrowleft":
if (document.activeElement) {
diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx
index 6cee702ee..645c7fa54 100644
--- a/src/client/views/InkingControl.tsx
+++ b/src/client/views/InkingControl.tsx
@@ -8,6 +8,7 @@ import { Scripting } from "../util/Scripting";
import { SelectionManager } from "../util/SelectionManager";
import { undoBatch } from "../util/UndoManager";
import GestureOverlay from "./GestureOverlay";
+import { FormattedTextBox } from "./nodes/FormattedTextBox";
export class InkingControl {
@observable static Instance: InkingControl;
@@ -28,8 +29,7 @@ export class InkingControl {
if (number < 0) {
number = 0xFFFFFFFF + number + 1;
}
-
- return number.toString(16).toUpperCase();
+ return (number < 16 ? "0" : "") + number.toString(16).toUpperCase();
}
@undoBatch
@@ -42,7 +42,13 @@ export class InkingControl {
const targetDoc = view.props.Document.dragFactory instanceof Doc ? view.props.Document.dragFactory :
view.props.Document.layout instanceof Doc ? view.props.Document.layout :
view.props.Document.isTemplateForField ? view.props.Document : Doc.GetProto(view.props.Document);
- targetDoc && (Doc.Layout(view.props.Document).backgroundColor = CurrentUserUtils.UserDocument.inkColor);
+ if (targetDoc) {
+ if (StrCast(Doc.Layout(view.props.Document).layout).indexOf("FormattedTextBox") !== -1 && FormattedTextBox.HadSelection) {
+ Doc.Layout(view.props.Document).color = CurrentUserUtils.UserDocument.inkColor;
+ } else {
+ Doc.Layout(view.props.Document)._backgroundColor = CurrentUserUtils.UserDocument.inkColor; // '_backgroundColor' is template specific. 'backgroundColor' would apply to all templates, but has no UI at the moment
+ }
+ }
});
} else {
CurrentUserUtils.ActivePen && (CurrentUserUtils.ActivePen.backgroundColor = this._selectedColor);
@@ -79,7 +85,6 @@ export class InkingControl {
Scripting.addGlobal(function activatePen(pen: any, width: any, color: any) { InkingControl.Instance.switchTool(pen ? InkTool.Pen : InkTool.None); InkingControl.Instance.switchWidth(width); InkingControl.Instance.updateSelectedColor(color); });
Scripting.addGlobal(function activateBrush(pen: any, width: any, color: any) { InkingControl.Instance.switchTool(pen ? InkTool.Highlighter : InkTool.None); InkingControl.Instance.switchWidth(width); InkingControl.Instance.updateSelectedColor(color); });
Scripting.addGlobal(function activateEraser(pen: any) { return InkingControl.Instance.switchTool(pen ? InkTool.Eraser : InkTool.None); });
-Scripting.addGlobal(function activateScrubber(pen: any) { return InkingControl.Instance.switchTool(pen ? InkTool.Scrubber : InkTool.None); });
Scripting.addGlobal(function activateStamp(pen: any) { return InkingControl.Instance.switchTool(pen ? InkTool.Stamp : InkTool.None); });
Scripting.addGlobal(function deactivateInk() { return InkingControl.Instance.switchTool(InkTool.None); });
Scripting.addGlobal(function setInkWidth(width: any) { return InkingControl.Instance.switchWidth(width); });
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index f315ce12a..a791eed40 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -29,13 +29,13 @@ export class InkingStroke extends DocExtendableComponent<FieldViewProps, InkDocu
@computed get PanelHeight() { return this.props.PanelHeight(); }
private analyzeStrokes = () => {
- const data: InkData = Cast(this.Document.data, InkField)?.inkData ?? [];
+ const data: InkData = Cast(this.Document.data, InkField) ?.inkData ?? [];
CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.Document, ["inkAnalysis", "handwriting"], [data]);
}
render() {
TraceMobx();
- const data: InkData = Cast(this.Document.data, InkField)?.inkData ?? [];
+ const data: InkData = Cast(this.Document.data, InkField) ?.inkData ?? [];
const xs = data.map(p => p.X);
const ys = data.map(p => p.Y);
const left = Math.min(...xs);
diff --git a/src/client/views/KeyphraseQueryView.tsx b/src/client/views/KeyphraseQueryView.tsx
index a9dafc4a4..1dc156968 100644
--- a/src/client/views/KeyphraseQueryView.tsx
+++ b/src/client/views/KeyphraseQueryView.tsx
@@ -15,8 +15,8 @@ export class KeyphraseQueryView extends React.Component<KP_Props>{
}
render() {
- let kps = this.props.keyphrases.toString();
- let keyterms = this.props.keyphrases.split(',');
+ const kps = this.props.keyphrases.toString();
+ const keyterms = this.props.keyphrases.split(',');
return (
<div>
<h5>Select queries to send:</h5>
diff --git a/src/client/views/MainView.scss b/src/client/views/MainView.scss
index d39c217ec..e95802e54 100644
--- a/src/client/views/MainView.scss
+++ b/src/client/views/MainView.scss
@@ -21,7 +21,10 @@
z-index: 1;
}
-#mainView-container {
+.mainView-container, .mainView-container-dark {
+ input {
+ color: unset !important;
+ }
width: 100%;
height: 100%;
position: absolute;
@@ -29,6 +32,31 @@
left: 0;
z-index: 1;
touch-action: none;
+ .searchBox-container {
+ background: lightgray;
+ }
+}
+
+.mainView-container-dark {
+ .lm_goldenlayout {
+ background: dimgray;
+ }
+ .marquee {
+ border-color: white;
+ }
+ #search-input {
+ background: lightgray;
+ }
+ .searchBox-container {
+ background: rgb(45,45,45);
+ }
+ .contextMenu-cont, .contextMenu-item {
+ background: dimGray;
+ color: lightgray;
+ }
+ .contextMenu-item:hover {
+ background: gray;
+ }
}
.mainView-mainContent {
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 997266266..8d9be5980 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -1,7 +1,7 @@
import { library } from '@fortawesome/fontawesome-svg-core';
import {
- faArrowDown, faBullseye, faFilter, faArrowUp, faBolt, faCaretUp, faCat, faCheck, faChevronRight, faClone, faCloudUploadAlt, faCommentAlt, faCut, faEllipsisV, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faLongArrowAltRight,
- faMusic, faObjectGroup, faPause, faMousePointer, faPenNib, faFileAudio, faPen, faEraser, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faTv, faUndoAlt, faHighlighter, faMicrophone, faCompressArrowsAlt, faPhone, faStamp, faClipboard
+ faFileAlt, faStickyNote, faArrowDown, faBullseye, faFilter, faArrowUp, faBolt, faCaretUp, faCat, faCheck, faChevronRight, faClone, faCloudUploadAlt, faCommentAlt, faCut, faEllipsisV, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faLongArrowAltRight,
+ faMusic, faObjectGroup, faPause, faMousePointer, faPenNib, faFileAudio, faPen, faEraser, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faTv, faUndoAlt, faHighlighter, faMicrophone, faCompressArrowsAlt, faPhone, faStamp, faClipboard, faVideo,
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed, configure, observable, reaction, runInAction } from 'mobx';
@@ -13,7 +13,7 @@ import { Doc, DocListCast, Field, FieldResult, Opt } from '../../new_fields/Doc'
import { Id } from '../../new_fields/FieldSymbols';
import { List } from '../../new_fields/List';
import { listSpec } from '../../new_fields/Schema';
-import { Cast, FieldValue, StrCast } from '../../new_fields/Types';
+import { Cast, FieldValue, StrCast, BoolCast } from '../../new_fields/Types';
import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils';
import { emptyFunction, returnEmptyString, returnFalse, returnOne, returnTrue, Utils, emptyPath } from '../../Utils';
import GoogleAuthenticationManager from '../apis/GoogleAuthenticationManager';
@@ -48,6 +48,7 @@ import SettingsManager from '../util/SettingsManager';
import { TraceMobx } from '../../new_fields/util';
import { RadialMenu } from './nodes/RadialMenu';
import RichTextMenu from '../util/RichTextMenu';
+import { DocumentType } from '../documents/DocumentTypes';
@observer
export class MainView extends React.Component {
@@ -56,13 +57,15 @@ export class MainView extends React.Component {
private _flyoutSizeOnDown = 0;
private _urlState: HistoryUtil.DocUrl;
private _docBtnRef = React.createRef<HTMLDivElement>();
+ private _mainViewRef = React.createRef<HTMLDivElement>();
@observable private _panelWidth: number = 0;
@observable private _panelHeight: number = 0;
@observable private _flyoutTranslate: boolean = true;
@observable public flyoutWidth: number = 250;
+ private get darkScheme() { return BoolCast(Cast(this.userDoc.activeWorkspace, Doc, null)?.darkScheme); }
- @computed private get userDoc() { return CurrentUserUtils.UserDocument; }
+ @computed private get userDoc() { return Doc.UserDoc(); }
@computed private get mainContainer() { return this.userDoc ? FieldValue(Cast(this.userDoc.activeWorkspace, Doc)) : CurrentUserUtils.GuestWorkspace; }
@computed public get mainFreeform(): Opt<Doc> { return (docs => (docs && docs.length > 1) ? docs[1] : undefined)(DocListCast(this.mainContainer!.data)); }
@computed public get sidebarButtonsDoc() { return Cast(CurrentUserUtils.UserDocument.sidebarButtons, Doc) as Doc; }
@@ -106,6 +109,8 @@ export class MainView extends React.Component {
}
}
+ library.add(faFileAlt);
+ library.add(faStickyNote);
library.add(faFont);
library.add(faExclamation);
library.add(faPortrait);
@@ -142,6 +147,7 @@ export class MainView extends React.Component {
library.add(faArrowUp);
library.add(faCloudUploadAlt);
library.add(faBolt);
+ library.add(faVideo);
library.add(faChevronRight);
library.add(faEllipsisV);
library.add(faMusic);
@@ -185,7 +191,7 @@ export class MainView extends React.Component {
reaction(() => CollectionDockingView.Instance && CollectionDockingView.Instance.initialized,
initialized => initialized && received && DocServer.GetRefField(received).then(docField => {
if (docField instanceof Doc && docField._viewType !== CollectionViewType.Docking) {
- CollectionDockingView.AddRightSplit(docField, undefined);
+ CollectionDockingView.AddRightSplit(docField);
}
}),
);
@@ -209,7 +215,6 @@ export class MainView extends React.Component {
_width: this._panelWidth * .7,
_height: this._panelHeight,
title: "Collection " + workspaceCount,
- backgroundColor: "white"
};
const freeformDoc = CurrentUserUtils.GuestTarget || Docs.Create.FreeformDocument([], freeformOptions);
Doc.AddDocToList(Doc.GetProto(CurrentUserUtils.UserDocument.documents as Doc), "data", freeformDoc);
@@ -275,6 +280,28 @@ export class MainView extends React.Component {
getPHeight = () => this._panelHeight;
getContentsHeight = () => this._panelHeight - this._buttonBarHeight;
+ defaultBackgroundColors = (doc: Doc) => {
+ if (this.darkScheme) {
+ switch (doc.type) {
+ case DocumentType.TEXT || DocumentType.BUTTON: return "#2d2d2d";
+ case DocumentType.LINK:
+ case DocumentType.COL: {
+ if (doc._viewType !== CollectionViewType.Freeform && doc._viewType !== CollectionViewType.Time) return "rgb(62,62,62)";
+ }
+ default: return "black";
+ }
+ } else {
+ switch (doc.type) {
+ case DocumentType.TEXT: return "#f1efeb";
+ case DocumentType.BUTTON: return "lightgray";
+ case DocumentType.LINK:
+ case DocumentType.COL: {
+ if (doc._viewType !== CollectionViewType.Freeform && doc._viewType !== CollectionViewType.Time) return "lightgray";
+ }
+ default: return "white";
+ }
+ }
+ }
@computed get mainDocView() {
return <DocumentView Document={this.mainContainer!}
DataDoc={undefined}
@@ -283,13 +310,13 @@ export class MainView extends React.Component {
addDocTab={this.addDocTabFunc}
pinToPres={emptyFunction}
onClick={undefined}
+ backgroundColor={this.defaultBackgroundColors}
removeDocument={undefined}
ScreenToLocalTransform={Transform.Identity}
ContentScaling={returnOne}
PanelWidth={this.getPWidth}
PanelHeight={this.getPHeight}
renderDepth={0}
- backgroundColor={returnEmptyString}
focus={emptyFunction}
parentActive={returnTrue}
whenActiveChanged={emptyFunction}
@@ -360,21 +387,21 @@ export class MainView extends React.Component {
document.removeEventListener("pointerup", this.onPointerUp);
}
flyoutWidthFunc = () => this.flyoutWidth;
- addDocTabFunc = (doc: Doc, data: Opt<Doc>, where: string, libraryPath?: Doc[]): boolean => {
+ addDocTabFunc = (doc: Doc, where: string, libraryPath?: Doc[]): boolean => {
return where === "close" ? CollectionDockingView.CloseRightSplit(doc) :
doc.dockingConfig ? this.openWorkspace(doc) :
- CollectionDockingView.AddRightSplit(doc, undefined, libraryPath);
+ CollectionDockingView.AddRightSplit(doc, libraryPath);
}
mainContainerXf = () => new Transform(0, -this._buttonBarHeight, 1);
@computed get flyout() {
- const sidebarContent = this.userDoc && this.userDoc.sidebarContainer;
+ const sidebarContent = this.userDoc?.sidebarContainer;
if (!(sidebarContent instanceof Doc)) {
return (null);
}
const sidebarButtonsDoc = Cast(CurrentUserUtils.UserDocument.sidebarButtons, Doc) as Doc;
return <div className="mainView-flyoutContainer" >
- <div className="mainView-tabButtons" style={{ height: `${this._buttonBarHeight}px` }}>
+ <div className="mainView-tabButtons" style={{ height: `${this._buttonBarHeight}px`, backgroundColor: StrCast(sidebarButtonsDoc.backgroundColor) }}>
<DocumentView
Document={sidebarButtonsDoc}
DataDoc={undefined}
@@ -390,7 +417,7 @@ export class MainView extends React.Component {
PanelHeight={this.getPHeight}
renderDepth={0}
focus={emptyFunction}
- backgroundColor={returnEmptyString}
+ backgroundColor={this.defaultBackgroundColors}
parentActive={returnTrue}
whenActiveChanged={emptyFunction}
bringToFront={emptyFunction}
@@ -416,7 +443,7 @@ export class MainView extends React.Component {
PanelHeight={this.getContentsHeight}
renderDepth={0}
focus={emptyFunction}
- backgroundColor={returnEmptyString}
+ backgroundColor={this.defaultBackgroundColors}
parentActive={returnTrue}
whenActiveChanged={emptyFunction}
bringToFront={emptyFunction}
@@ -439,10 +466,10 @@ export class MainView extends React.Component {
@computed get mainContent() {
const sidebar = this.userDoc && this.userDoc.sidebarContainer;
return !this.userDoc || !(sidebar instanceof Doc) ? (null) : (
- <div className="mainView-mainContent" >
+ <div className="mainView-mainContent" style={{ color: this.darkScheme ? "rgb(205,205,205)" : "black" }} >
<div className="mainView-flyoutContainer" onPointerLeave={this.pointerLeaveDragger} style={{ width: this.flyoutWidth }}>
<div className="mainView-libraryHandle" onPointerDown={this.onPointerDown} onPointerOver={this.pointerOverDragger}
- style={{ backgroundColor: `${StrCast(sidebar.backgroundColor, "lightGray")}` }} >
+ style={{ backgroundColor: this.defaultBackgroundColors(sidebar) }}>
<span title="library View Dragger" style={{
width: (this.flyoutWidth !== 0 && this._flyoutTranslate) ? "100%" : "3vw",
//height: (this.flyoutWidth !== 0 && this._flyoutTranslate) ? "100%" : "100vh",
@@ -484,18 +511,20 @@ export class MainView extends React.Component {
return new Transform(-translateX, -translateY, 1 / scale);
}
@computed get docButtons() {
- if (CurrentUserUtils.UserDocument?.expandingButtons instanceof Doc) {
+ const expandingBtns = Doc.UserDoc()?.expandingButtons;
+ if (expandingBtns instanceof Doc) {
return <div className="mainView-docButtons" ref={this._docBtnRef}
- style={{ height: !CurrentUserUtils.UserDocument.expandingButtons.isExpanded ? "42px" : undefined }} >
+ style={{ height: !expandingBtns.linearViewIsExpanded ? "42px" : undefined }} >
<MainViewNotifs />
<CollectionLinearView
- Document={CurrentUserUtils.UserDocument.expandingButtons}
+ Document={expandingBtns}
DataDoc={undefined}
LibraryPath={emptyPath}
fieldKey={"data"}
+ dropAction={"alias"}
annotationsKey={""}
+ bringToFront={emptyFunction}
select={emptyFunction}
- chromeCollapsed={true}
active={returnFalse}
isSelected={returnFalse}
moveDocument={this.moveButtonDoc}
@@ -519,8 +548,16 @@ export class MainView extends React.Component {
return (null);
}
+ get mainViewElement() {
+ return document.getElementById("mainView-container");
+ }
+
+ get mainViewRef() {
+ return this._mainViewRef;
+ }
+
render() {
- return (<div id="mainView-container">
+ return (<div className={"mainView-container" + (this.darkScheme ? "-dark" : "")} ref={this._mainViewRef}>
<DictationOverlay />
<SharingManager />
<SettingsManager />
diff --git a/src/client/views/MainViewNotifs.tsx b/src/client/views/MainViewNotifs.tsx
index 09fa1cb0c..82e07c449 100644
--- a/src/client/views/MainViewNotifs.tsx
+++ b/src/client/views/MainViewNotifs.tsx
@@ -15,7 +15,7 @@ export class MainViewNotifs extends React.Component {
@observable static NotifsCol: Opt<Doc>;
openNotifsCol = () => {
if (MainViewNotifs.NotifsCol) {
- CollectionDockingView.AddRightSplit(MainViewNotifs.NotifsCol, undefined);
+ CollectionDockingView.AddRightSplit(MainViewNotifs.NotifsCol);
}
}
render() {
diff --git a/src/client/views/MetadataEntryMenu.scss b/src/client/views/MetadataEntryMenu.scss
index 5f4a52c0c..5776cf070 100644
--- a/src/client/views/MetadataEntryMenu.scss
+++ b/src/client/views/MetadataEntryMenu.scss
@@ -8,6 +8,12 @@
}
}
+.metadataEntry-autoSuggester {
+ width: 100%;
+ height: 100%;
+ padding-right: 10px;
+}
+
#metadataEntry-outer {
overflow: auto !important;
}
diff --git a/src/client/views/MetadataEntryMenu.tsx b/src/client/views/MetadataEntryMenu.tsx
index 23b21ae0c..8bc80ed06 100644
--- a/src/client/views/MetadataEntryMenu.tsx
+++ b/src/client/views/MetadataEntryMenu.tsx
@@ -195,10 +195,10 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{
_ref = React.createRef<HTMLInputElement>();
render() {
- return (
- <div className="metadataEntry-outerDiv" id="metadataEntry-outer">
- <div className="metadataEntry-inputArea">
- Key:
+ return (<div className="metadataEntry-outerDiv" id="metadataEntry-outer" onPointerDown={e => e.stopPropagation()}>
+ <div className="metadataEntry-inputArea">
+ Key:
+ <div className="metadataEntry-autoSuggester" onClick={e => this.autosuggestRef.current!.input?.focus()} >
<Autosuggest inputProps={{ value: this._currentKey, onChange: this.onKeyChange }}
getSuggestionValue={this.getSuggestionValue}
suggestions={emptyPath}
@@ -207,16 +207,17 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{
onSuggestionsFetchRequested={emptyFunction}
onSuggestionsClearRequested={emptyFunction}
ref={this.autosuggestRef} />
- Value:
- <input className="metadataEntry-input" ref={this._ref} value={this._currentValue} onClick={e => this._ref.current!.focus()} onChange={this.onValueChange} onKeyDown={this.onValueKeyDown} />
- {this.considerChildOptions}
- </div>
- <div className="metadataEntry-keys" >
- <ul>
- {this._allSuggestions.slice().sort().map(s => <li key={s} onClick={action(() => { this._currentKey = s; this.previewValue(); })} >{s}</li>)}
- </ul>
</div>
+ Value:
+ <input className="metadataEntry-input" ref={this._ref} value={this._currentValue} onClick={e => this._ref.current!.focus()} onChange={this.onValueChange} onKeyDown={this.onValueKeyDown} />
+ {this.considerChildOptions}
+ </div>
+ <div className="metadataEntry-keys" >
+ <ul>
+ {this._allSuggestions.slice().sort().map(s => <li key={s} onClick={action(() => { this._currentKey = s; this.previewValue(); })} >{s}</li>)}
+ </ul>
</div>
+ </div>
);
}
} \ No newline at end of file
diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx
index 295cd7c6e..220efd4a8 100644
--- a/src/client/views/OverlayView.tsx
+++ b/src/client/views/OverlayView.tsx
@@ -144,7 +144,7 @@ export class OverlayView extends React.Component {
return (null);
}
return CurrentUserUtils.UserDocument.overlays instanceof Doc && DocListCast(CurrentUserUtils.UserDocument.overlays.data).map(d => {
- d.inOverlay = true;
+ setTimeout(() => d.inOverlay = true, 0);
let offsetx = 0, offsety = 0;
const onPointerMove = action((e: PointerEvent) => {
if (e.buttons === 1) {
@@ -169,7 +169,7 @@ export class OverlayView extends React.Component {
document.addEventListener("pointermove", onPointerMove);
document.addEventListener("pointerup", onPointerUp);
};
- return <div className="overlayView-doc" key={d[Id]} onPointerDown={onPointerDown} style={{ transform: `translate(${d.x}px, ${d.y}px)`, display: d.isMinimized ? "none" : "" }}>
+ return <div className="overlayView-doc" key={d[Id]} onPointerDown={onPointerDown} style={{ transform: `translate(${d.x}px, ${d.y}px)` }}>
<DocumentView
Document={d}
LibraryPath={emptyPath}
diff --git a/src/client/views/RecommendationsBox.tsx b/src/client/views/RecommendationsBox.tsx
index 0e3cfd729..5ebba0abb 100644
--- a/src/client/views/RecommendationsBox.tsx
+++ b/src/client/views/RecommendationsBox.tsx
@@ -6,7 +6,7 @@ import "./RecommendationsBox.scss";
import { Doc, DocListCast, WidthSym, HeightSym } from "../../new_fields/Doc";
import { DocumentIcon } from "./nodes/DocumentIcon";
import { StrCast, NumCast } from "../../new_fields/Types";
-import { returnFalse, emptyFunction, returnEmptyString, returnOne } from "../../Utils";
+import { returnFalse, emptyFunction, returnEmptyString, returnOne, emptyPath } from "../../Utils";
import { Transform } from "../util/Transform";
import { ObjectField } from "../../new_fields/ObjectField";
import { DocumentView } from "./nodes/DocumentView";
@@ -31,7 +31,7 @@ library.add(faBullseye, faLink);
@observer
export class RecommendationsBox extends React.Component<FieldViewProps> {
- public static LayoutString(fieldKey?: string) { return FieldView.LayoutString(RecommendationsBox, fieldKey); }
+ public static LayoutString(fieldKey: string) { return FieldView.LayoutString(RecommendationsBox, fieldKey); }
// @observable private _display: boolean = false;
@observable private _pageX: number = 0;
@@ -48,17 +48,17 @@ export class RecommendationsBox extends React.Component<FieldViewProps> {
@action
private DocumentIcon(doc: Doc) {
- let layoutresult = StrCast(doc.type);
+ const layoutresult = StrCast(doc.type);
let renderDoc = doc;
//let box: number[] = [];
if (layoutresult.indexOf(DocumentType.COL) !== -1) {
renderDoc = Doc.MakeDelegate(renderDoc);
}
- let returnXDimension = () => 150;
- let returnYDimension = () => 150;
- let scale = () => returnXDimension() / NumCast(renderDoc.nativeWidth, returnXDimension());
+ const returnXDimension = () => 150;
+ const returnYDimension = () => 150;
+ const scale = () => returnXDimension() / NumCast(renderDoc.nativeWidth, returnXDimension());
//let scale = () => 1;
- let newRenderDoc = Doc.MakeAlias(renderDoc); /// newRenderDoc -> renderDoc -> render"data"Doc -> TextProt
+ const newRenderDoc = Doc.MakeAlias(renderDoc); /// newRenderDoc -> renderDoc -> render"data"Doc -> TextProt
newRenderDoc.height = NumCast(this.props.Document.documentIconHeight);
newRenderDoc.autoHeight = false;
const docview = <div>
@@ -66,8 +66,8 @@ export class RecommendationsBox extends React.Component<FieldViewProps> {
fitToBox={StrCast(doc.type).indexOf(DocumentType.COL) !== -1}
Document={newRenderDoc}
addDocument={returnFalse}
+ LibraryPath={emptyPath}
removeDocument={returnFalse}
- ruleProvider={undefined}
ScreenToLocalTransform={Transform.Identity}
addDocTab={returnFalse}
pinToPres={returnFalse}
@@ -167,7 +167,7 @@ export class RecommendationsBox extends React.Component<FieldViewProps> {
<div style={{ marginRight: 50 }} onClick={() => DocumentManager.Instance.jumpToDocument(doc, false)}>
<FontAwesomeIcon className="documentdecorations-icon" icon={"bullseye"} size="sm" />
</div>
- <div style={{ marginRight: 50 }} onClick={() => DocUtils.MakeLink({ doc: this.props.Document.sourceDoc as Doc }, { doc: doc }, "User Selected Link", "Generated from Recommender", undefined)}>
+ <div style={{ marginRight: 50 }} onClick={() => DocUtils.MakeLink({ doc: this.props.Document.sourceDoc as Doc }, { doc: doc }, "Recommender", undefined)}>
<FontAwesomeIcon className="documentdecorations-icon" icon={"link"} size="sm" />
</div>
</div>
diff --git a/src/client/views/ScriptBox.tsx b/src/client/views/ScriptBox.tsx
index d24256886..cc5d7640e 100644
--- a/src/client/views/ScriptBox.tsx
+++ b/src/client/views/ScriptBox.tsx
@@ -93,29 +93,33 @@ export class ScriptBox extends React.Component<ScriptBoxProps> {
const params: string[] = [];
const setParams = (p: string[]) => params.splice(0, params.length, ...p);
const scriptingBox = <ScriptBox initialText={originalText} setParams={setParams} onCancel={overlayDisposer} onSave={(text, onError) => {
- const script = CompileScript(text, {
- params: { this: Doc.name, ...contextParams },
- typecheck: false,
- editable: true,
- transformer: DocumentIconContainer.getTransformer()
- });
- if (!script.compiled) {
- onError(script.errors.map(error => error.messageText).join("\n"));
- return;
- }
+ if (!text) {
+ doc[fieldKey] = undefined;
+ } else {
+ const script = CompileScript(text, {
+ params: { this: Doc.name, ...contextParams },
+ typecheck: false,
+ editable: true,
+ transformer: DocumentIconContainer.getTransformer()
+ });
+ if (!script.compiled) {
+ onError(script.errors.map(error => error.messageText).join("\n"));
+ return;
+ }
- const div = document.createElement("div");
- div.style.width = "90";
- div.style.height = "20";
- div.style.background = "gray";
- div.style.position = "absolute";
- div.style.display = "inline-block";
- div.style.transform = `translate(${clientX}px, ${clientY}px)`;
- div.innerHTML = "button";
- params.length && DragManager.StartButtonDrag([div], text, doc.title + "-instance", {}, params, (button: Doc) => { }, clientX, clientY);
+ const div = document.createElement("div");
+ div.style.width = "90";
+ div.style.height = "20";
+ div.style.background = "gray";
+ div.style.position = "absolute";
+ div.style.display = "inline-block";
+ div.style.transform = `translate(${clientX}px, ${clientY}px)`;
+ div.innerHTML = "button";
+ params.length && DragManager.StartButtonDrag([div], text, doc.title + "-instance", {}, params, (button: Doc) => { }, clientX, clientY);
- doc[fieldKey] = new ScriptField(script);
- overlayDisposer();
+ doc[fieldKey] = new ScriptField(script);
+ overlayDisposer();
+ }
}} showDocumentIcons />;
overlayDisposer = OverlayView.Instance.addWindow(scriptingBox, { x: 400, y: 200, width: 500, height: 400, title: title });
}
diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx
index f61eb9cd0..8eb5c5050 100644
--- a/src/client/views/TemplateMenu.tsx
+++ b/src/client/views/TemplateMenu.tsx
@@ -1,17 +1,19 @@
-import { action, observable, runInAction, ObservableSet } from "mobx";
+import { action, observable, runInAction, ObservableSet, trace, computed } from "mobx";
import { observer } from "mobx-react";
import { SelectionManager } from "../util/SelectionManager";
import { undoBatch } from "../util/UndoManager";
import './TemplateMenu.scss';
import { DocumentView } from "./nodes/DocumentView";
-import { Template, Templates } from "./Templates";
+import { Template } from "./Templates";
import React = require("react");
import { Doc, DocListCast } from "../../new_fields/Doc";
+import { Docs, } from "../documents/Documents";
import { StrCast, Cast } from "../../new_fields/Types";
-import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
-const higflyout = require("@hig/flyout");
-export const { anchorPoints } = higflyout;
-export const Flyout = higflyout.default;
+import { CollectionTreeView } from "./collections/CollectionTreeView";
+import { returnTrue, emptyFunction, returnFalse, returnOne, emptyPath } from "../../Utils";
+import { Transform } from "../util/Transform";
+import { ScriptField, ComputedField } from "../../new_fields/ScriptField";
+import { Scripting } from "../util/Scripting";
@observer
class TemplateToggle extends React.Component<{ template: Template, checked: boolean, toggle: (event: React.ChangeEvent<HTMLInputElement>, template: Template) => void }> {
@@ -48,10 +50,15 @@ export interface TemplateMenuProps {
@observer
export class TemplateMenu extends React.Component<TemplateMenuProps> {
+ _addedKeys = new ObservableSet();
+ _customRef = React.createRef<HTMLInputElement>();
@observable private _hidden: boolean = true;
toggleLayout = (e: React.ChangeEvent<HTMLInputElement>, layout: string): void => {
- this.props.docViews.map(dv => dv.setCustomView(e.target.checked, layout));
+ this.props.docViews.map(dv => dv.switchViews(e.target.checked, layout));
+ }
+ toggleDefault = (e: React.ChangeEvent<HTMLInputElement>): void => {
+ this.props.docViews.map(dv => dv.switchViews(false, "layout"));
}
toggleFloat = (e: React.ChangeEvent<HTMLInputElement>): void => {
@@ -62,15 +69,14 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
DocumentView.FloatDoc(topDocView, ex, ey);
}
+ toggleAudio = (e: React.ChangeEvent<HTMLInputElement>): void => {
+ this.props.docViews.map(dv => dv.props.Document._showAudio = e.target.checked);
+ }
@undoBatch
@action
toggleTemplate = (event: React.ChangeEvent<HTMLInputElement>, template: Template): void => {
- if (event.target.checked) {
- this.props.docViews.map(d => d.Document["show" + template.Name] = template.Name.toLowerCase());
- } else {
- this.props.docViews.map(d => d.Document["show" + template.Name] = "");
- }
+ this.props.docViews.forEach(d => Doc.Layout(d.Document)["_show" + template.Name] = event.target.checked ? template.Name.toLowerCase() : "");
}
@action
@@ -81,10 +87,8 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
@undoBatch
@action
toggleChrome = (): void => {
- this.props.docViews.map(dv => {
- const layout = Doc.Layout(dv.Document);
- layout._chromeStatus = (layout._chromeStatus !== "disabled" ? "disabled" : "enabled");
- });
+ this.props.docViews.map(dv => Doc.Layout(dv.Document)).forEach(layout =>
+ layout._chromeStatus = (layout._chromeStatus !== "disabled" ? "disabled" : StrCast(layout._replacedChrome, "enabled")));
}
// todo: add brushes to brushMap to save with a style name
@@ -98,28 +102,82 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
Array.from(Object.keys(Doc.GetProto(this.props.docViews[0].props.Document))).
filter(key => key.startsWith("layout_")).
map(key => runInAction(() => this._addedKeys.add(key.replace("layout_", ""))));
- DocListCast(Cast(CurrentUserUtils.UserDocument.expandingButtons, Doc, null)?.data)?.map(btnDoc => {
- if (StrCast(Cast(btnDoc?.dragFactory, Doc, null)?.title)) {
- runInAction(() => this._addedKeys.add(StrCast(Cast(btnDoc?.dragFactory, Doc, null)?.title)));
- }
- });
}
- _addedKeys = new ObservableSet();
- _customRef = React.createRef<HTMLInputElement>();
+ return100 = () => 100;
+ @computed get scriptField() {
+ return ScriptField.MakeScript("switchView(firstDoc, this)", { this: Doc.name, heading: "string", checked: "string", containingTreeView: Doc.name, firstDoc: Doc.name },
+ { firstDoc: this.props.docViews[0].props.Document });
+ }
render() {
- const layout = Doc.Layout(this.props.docViews[0].Document);
+ const firstDoc = this.props.docViews[0].props.Document;
+ const templateName = StrCast(firstDoc.layoutKey, "layout").replace("layout_", "");
+ const noteTypesDoc = Cast(Doc.UserDoc().noteTypes, Doc, null);
+ const noteTypes = DocListCast(noteTypesDoc?.data);
+ const addedTypes = DocListCast(Cast(Doc.UserDoc().templateButtons, Doc, null)?.data);
+ const layout = Doc.Layout(firstDoc);
const templateMenu: Array<JSX.Element> = [];
this.props.templates.forEach((checked, template) =>
templateMenu.push(<TemplateToggle key={template.Name} template={template} checked={checked} toggle={this.toggleTemplate} />));
- templateMenu.push(<OtherToggle key={"float"} name={"Float"} checked={this.props.docViews[0].Document.z ? true : false} toggle={this.toggleFloat} />);
+ templateMenu.push(<OtherToggle key={"audio"} name={"Audio"} checked={firstDoc._showAudio ? true : false} toggle={this.toggleAudio} />);
+ templateMenu.push(<OtherToggle key={"float"} name={"Float"} checked={firstDoc.z ? true : false} toggle={this.toggleFloat} />);
templateMenu.push(<OtherToggle key={"chrome"} name={"Chrome"} checked={layout._chromeStatus !== "disabled"} toggle={this.toggleChrome} />);
- this._addedKeys && Array.from(this._addedKeys).map(layout =>
- templateMenu.push(<OtherToggle key={layout} name={layout} checked={StrCast(this.props.docViews[0].Document.layoutKey, "layout") === "layout_" + layout} toggle={e => this.toggleLayout(e, layout)} />)
- );
+ templateMenu.push(<OtherToggle key={"default"} name={"Default"} checked={templateName === "layout"} toggle={this.toggleDefault} />);
+ if (noteTypesDoc) {
+ addedTypes.concat(noteTypes).map(template => template.treeViewChecked = ComputedField.MakeFunction("templateIsUsed(this, firstDoc)", { firstDoc: "string" }, { firstDoc: StrCast(firstDoc.title) }));
+ this._addedKeys && Array.from(this._addedKeys).filter(key => !noteTypes.some(nt => nt.title === key)).forEach(template => templateMenu.push(
+ <OtherToggle key={template} name={template} checked={templateName === template} toggle={e => this.toggleLayout(e, template)} />));
+ templateMenu.push(
+ <CollectionTreeView
+ Document={Doc.UserDoc().templateDocs as Doc}
+ CollectionView={undefined}
+ ContainingCollectionDoc={undefined}
+ ContainingCollectionView={undefined}
+ onCheckedClick={this.scriptField!}
+ onChildClick={this.scriptField!}
+ LibraryPath={emptyPath}
+ dropAction={undefined}
+ active={returnTrue}
+ ContentScaling={returnOne}
+ bringToFront={emptyFunction}
+ focus={emptyFunction}
+ whenActiveChanged={emptyFunction}
+ ScreenToLocalTransform={Transform.Identity}
+ isSelected={returnFalse}
+ pinToPres={emptyFunction}
+ select={emptyFunction}
+ renderDepth={1}
+ addDocTab={returnFalse}
+ PanelWidth={this.return100}
+ PanelHeight={this.return100}
+ treeViewHideHeaderFields={true}
+ annotationsKey={""}
+ dontRegisterView={true}
+ fieldKey={"data"}
+ moveDocument={(doc: Doc) => false}
+ removeDocument={(doc: Doc) => false}
+ addDocument={(doc: Doc) => false} />
+ );
+ }
return <ul className="template-list" style={{ display: "block" }}>
- {templateMenu}
<input placeholder="+ layout" ref={this._customRef} onKeyPress={this.onCustomKeypress}></input>
+ {templateMenu}
</ul>;
}
-} \ No newline at end of file
+}
+
+Scripting.addGlobal(function switchView(doc: Doc, template: Doc) {
+ if (template.dragFactory) {
+ template = Cast(template.dragFactory, Doc, null);
+ }
+ const templateTitle = StrCast(template?.title);
+ return templateTitle && DocumentView.makeCustomViewClicked(doc, Docs.Create.FreeformDocument, templateTitle, template);
+});
+
+Scripting.addGlobal(function templateIsUsed(templateDoc: Doc, firstDocTitlte: string) {
+ const firstDoc = SelectionManager.SelectedDocuments().length ? SelectionManager.SelectedDocuments()[0].props.Document : undefined;
+ if (!firstDoc) return false;
+ const template = StrCast(templateDoc.dragFactory ? Cast(templateDoc.dragFactory, Doc, null)?.title : templateDoc.title);
+ return StrCast(firstDoc.layoutKey) === "layout_" + template ? 'check' : 'unchecked';
+ // return SelectionManager.SelectedDocuments().some(view => StrCast(view.props.Document.layoutKey) === "layout_" + template) ? 'check' : 'unchecked'
+}); \ No newline at end of file
diff --git a/src/client/views/TouchScrollableMenu.tsx b/src/client/views/TouchScrollableMenu.tsx
index 4bda0818e..969605be9 100644
--- a/src/client/views/TouchScrollableMenu.tsx
+++ b/src/client/views/TouchScrollableMenu.tsx
@@ -44,7 +44,7 @@ export default class TouchScrollableMenu extends React.Component<TouchScrollable
<div className="shadow" style={{ height: `calc(100% - 25px - ${this.selectedIndex * 25}px)` }}>
</div>
</div>
- )
+ );
}
}
@@ -54,6 +54,6 @@ export class TouchScrollableMenuItem extends React.Component<TouchScrollableMenu
<div className="menuItem-cont" onClick={this.props.onClick}>
{this.props.text}
</div>
- )
+ );
}
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionCarouselView.scss b/src/client/views/collections/CollectionCarouselView.scss
index 4815f1a59..fd1296286 100644
--- a/src/client/views/collections/CollectionCarouselView.scss
+++ b/src/client/views/collections/CollectionCarouselView.scss
@@ -1,6 +1,7 @@
.collectionCarouselView-outer {
background: gray;
+ height : 100%;
.collectionCarouselView-caption {
margin-left: 10%;
margin-right: 10%;
@@ -14,27 +15,23 @@
width: 100%;
}
}
-.carouselView-back {
+.carouselView-back, .carouselView-fwd {
position: absolute;
display: flex;
- left: 0;
top: 50%;
width: 30;
height: 30;
- background: lightgray;
align-items: center;
border-radius: 5px;
justify-content: center;
+ background : rgba(255, 255, 255, 0.46);
}
-.carouselView-fwd {
- position: absolute;
- display: flex;
+.carouselView-fwd {
right: 0;
- top: 50%;
- width: 30;
- height: 30;
+}
+.carouselView-back {
+ left: 0;
+}
+.carouselView-back:hover, .carouselView-fwd:hover {
background: lightgray;
- align-items: center;
- border-radius: 5px;
- justify-content: center;
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index 0933d5924..a0cb1fe19 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -12,28 +12,22 @@ import { CollectionSubView } from './CollectionSubView';
import { faCaretLeft, faCaretRight } from '@fortawesome/free-solid-svg-icons';
import { Doc } from '../../../new_fields/Doc';
import { FormattedTextBox } from '../nodes/FormattedTextBox';
-
-
-
+import { ContextMenu } from '../ContextMenu';
+import { ObjectField } from '../../../new_fields/ObjectField';
type CarouselDocument = makeInterface<[typeof documentSchema,]>;
const CarouselDocument = makeInterface(documentSchema);
@observer
export class CollectionCarouselView extends CollectionSubView(CarouselDocument) {
- @observable public addMenuToggle = React.createRef<HTMLInputElement>();
private _dropDisposer?: DragManager.DragDropDisposer;
- componentWillUnmount() {
- this._dropDisposer && this._dropDisposer();
- }
+ componentWillUnmount() { this._dropDisposer?.(); }
- componentDidMount() {
- }
protected createDashEventsTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view
- this._dropDisposer && this._dropDisposer();
+ this._dropDisposer?.();
if (ele) {
- this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this));
+ this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this));
}
}
@@ -50,18 +44,18 @@ export class CollectionCarouselView extends CollectionSubView(CarouselDocument)
@computed get content() {
const index = NumCast(this.layoutDoc._itemIndex);
return !(this.childLayoutPairs?.[index]?.layout instanceof Doc) ? (null) :
- <div>
- <div className="collectionCarouselView-image">
+ <>
+ <div className="collectionCarouselView-image" key="image">
<ContentFittingDocumentView {...this.props}
Document={this.childLayoutPairs[index].layout}
DataDocument={this.childLayoutPairs[index].data}
PanelHeight={this.panelHeight}
getTransform={this.props.ScreenToLocalTransform} />
</div>
- <div className="collectionCarouselView-caption" style={{ background: `${StrCast(this.props.Document.backgroundColor)}` }}>
+ <div className="collectionCarouselView-caption" key="caption" style={{ background: this.props.backgroundColor?.(this.props.Document) }}>
<FormattedTextBox key={index} {...this.props} Document={this.childLayoutPairs[index].layout} DataDoc={undefined} fieldKey={"caption"}></FormattedTextBox>
</div>
- </div>
+ </>;
}
@computed get buttons() {
return <>
@@ -73,8 +67,21 @@ export class CollectionCarouselView extends CollectionSubView(CarouselDocument)
</div>
</>;
}
+
+
+ onContextMenu = (e: React.MouseEvent): void => {
+ // need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout
+ if (!e.isPropagationStopped()) {
+ ContextMenu.Instance.addItem({
+ description: "Make Hero Image", event: () => {
+ const index = NumCast(this.layoutDoc._itemIndex);
+ (this.dataDoc || Doc.GetProto(this.props.Document)).hero = ObjectField.MakeCopy(this.childLayoutPairs[index].layout.data as ObjectField);
+ }, icon: "plus"
+ });
+ }
+ }
render() {
- return <div className="collectionCarouselView-outer">
+ return <div className="collectionCarouselView-outer" ref={this.createDashEventsTarget} onContextMenu={this.onContextMenu}>
{this.content}
{this.buttons}
</div>;
diff --git a/src/client/views/collections/CollectionDockingView.scss b/src/client/views/collections/CollectionDockingView.scss
index f518ef8fb..2fafcecb2 100644
--- a/src/client/views/collections/CollectionDockingView.scss
+++ b/src/client/views/collections/CollectionDockingView.scss
@@ -1,8 +1,34 @@
@import "../../views/globalCssVariables.scss";
-.lm_active .messageCounter {
- color: white;
- background: #999999;
+.lm_title {
+ margin-top: 3px;
+ background: black;
+ border-radius: 5px;
+ border: solid 1px dimgray;
+ border-width: 2px 2px 0px;
+ height: 20px;
+ transform: translate(0px, -3px);
+}
+.lm_title_wrap {
+ overflow: hidden;
+ height: 19px;
+ margin-top: -3px;
+ display:inline-block;
+}
+.lm_active .lm_title {
+ border: solid 1px lightgray;
+}
+.lm_header .lm_tab .lm_close_tab {
+ position: absolute;
+ text-align: center;
+}
+
+.lm_header .lm_tab {
+ padding-right : 20px;
+}
+
+.lm_popout {
+ display:none;
}
.messageCounter {
@@ -26,9 +52,20 @@
top: 0;
left: 0;
// overflow: hidden; // bcz: menus don't show up when this is on (e.g., the parentSelectorMenu)
-
+ .collectionDockingView-gear {
+ padding-left: 5px;
+ height: 15px;
+ width: 18px;
+ display: inline-block;
+ margin: auto;
+ }
.collectionDockingView-dragAsDocument {
touch-action: none;
+ position: absolute;
+ padding-left: 5px;
+ display: inline-block;
+ width: 100%;
+ height: 100%;
}
.lm_content {
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 7a6d54ac2..4e1e76f39 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -1,26 +1,27 @@
import { library } from '@fortawesome/fontawesome-svg-core';
import { faFile } from '@fortawesome/free-solid-svg-icons';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import 'golden-layout/src/css/goldenlayout-base.css';
import 'golden-layout/src/css/goldenlayout-dark-theme.css';
-import { action, Lambda, observable, reaction, computed, runInAction, trace } from "mobx";
+import { action, computed, Lambda, observable, reaction, runInAction, trace } from "mobx";
import { observer } from "mobx-react";
import * as ReactDOM from 'react-dom';
import Measure from "react-measure";
import * as GoldenLayout from "../../../client/goldenLayout";
import { DateField } from '../../../new_fields/DateField';
-import { Doc, DocListCast, Field, Opt } from "../../../new_fields/Doc";
+import { Doc, DocListCast, Field, Opt, DataSym } from "../../../new_fields/Doc";
import { Id } from '../../../new_fields/FieldSymbols';
import { List } from '../../../new_fields/List';
import { FieldId } from "../../../new_fields/RefField";
-import { listSpec } from "../../../new_fields/Schema";
-import { BoolCast, Cast, NumCast, StrCast } from "../../../new_fields/Types";
+import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
+import { TraceMobx } from '../../../new_fields/util';
import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils';
-import { emptyFunction, returnEmptyString, returnFalse, returnOne, returnTrue, Utils } from "../../../Utils";
+import { emptyFunction, returnOne, returnTrue, Utils } from "../../../Utils";
import { DocServer } from "../../DocServer";
import { Docs } from '../../documents/Documents';
+import { DocumentType } from '../../documents/DocumentTypes';
import { DocumentManager } from '../../util/DocumentManager';
-import { DragManager } from "../../util/DragManager";
+import { DragManager, dropActionType } from "../../util/DragManager";
+import { Scripting } from '../../util/Scripting';
import { SelectionManager } from '../../util/SelectionManager';
import { Transform } from '../../util/Transform';
import { undoBatch } from "../../util/UndoManager";
@@ -28,13 +29,8 @@ import { MainView } from '../MainView';
import { DocumentView } from "../nodes/DocumentView";
import "./CollectionDockingView.scss";
import { SubCollectionViewProps } from "./CollectionSubView";
+import { DockingViewButtonSelector } from './ParentDocumentSelector';
import React = require("react");
-import { ButtonSelector } from './ParentDocumentSelector';
-import { DocumentType } from '../../documents/DocumentTypes';
-import { ComputedField } from '../../../new_fields/ScriptField';
-import { InteractionUtils } from '../../util/InteractionUtils';
-import { TraceMobx } from '../../../new_fields/util';
-import { Scripting } from '../../util/Scripting';
library.add(faFile);
const _global = (window /* browser */ || global /* node */) as any;
@@ -42,7 +38,7 @@ const _global = (window /* browser */ || global /* node */) as any;
export class CollectionDockingView extends React.Component<SubCollectionViewProps> {
@observable public static Instances: CollectionDockingView[] = [];
@computed public static get Instance() { return CollectionDockingView.Instances[0]; }
- public static makeDocumentConfig(document: Doc, dataDoc: Doc | undefined, width?: number, libraryPath?: Doc[]) {
+ public static makeDocumentConfig(document: Doc, width?: number, libraryPath?: Doc[]) {
return {
type: 'react-component',
component: 'DocumentFrameRenderer',
@@ -50,8 +46,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
width: width,
props: {
documentId: document[Id],
- dataDocumentId: dataDoc && dataDoc[Id] !== document[Id] ? dataDoc[Id] : "",
- libraryPath: libraryPath ? libraryPath.map(d => d[Id]) : []
+ libraryPath: libraryPath?.map(d => d[Id])
//collectionDockingView: CollectionDockingView.Instance
}
};
@@ -80,12 +75,12 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
public StartOtherDrag(e: any, dragDocs: Doc[]) {
let config: any;
if (dragDocs.length === 1) {
- config = CollectionDockingView.makeDocumentConfig(dragDocs[0], undefined);
+ config = CollectionDockingView.makeDocumentConfig(dragDocs[0]);
} else {
config = {
type: 'row',
content: dragDocs.map((doc, i) => {
- CollectionDockingView.makeDocumentConfig(doc, undefined);
+ CollectionDockingView.makeDocumentConfig(doc);
})
};
}
@@ -101,10 +96,9 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
@action
public OpenFullScreen(docView: DocumentView, libraryPath?: Doc[]) {
const document = Doc.MakeAlias(docView.props.Document);
- const dataDoc = docView.props.DataDoc;
const newItemStackConfig = {
type: 'stack',
- content: [CollectionDockingView.makeDocumentConfig(document, dataDoc, undefined, libraryPath)]
+ content: [CollectionDockingView.makeDocumentConfig(document, undefined, libraryPath)]
};
const docconfig = this._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, this._goldenLayout);
this._goldenLayout.root.contentItems[0].addChild(docconfig);
@@ -133,36 +127,22 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
@undoBatch
@action
public static CloseRightSplit(document: Opt<Doc>): boolean {
- if (!CollectionDockingView.Instance) return false;
const instance = CollectionDockingView.Instance;
- let retVal = false;
- if (instance._goldenLayout.root.contentItems[0].isRow) {
- retVal = Array.from(instance._goldenLayout.root.contentItems[0].contentItems).some((child: any) => {
- if (child.contentItems.length === 1 && child.contentItems[0].config.component === "DocumentFrameRenderer" &&
- DocumentManager.Instance.getDocumentViewById(child.contentItems[0].config.props.documentId) &&
- ((!document && DocumentManager.Instance.getDocumentViewById(child.contentItems[0].config.props.documentId)!.Document.isDisplayPanel) ||
- (document && Doc.AreProtosEqual(DocumentManager.Instance.getDocumentViewById(child.contentItems[0].config.props.documentId)!.Document, document)))) {
- child.contentItems[0].remove();
+ const tryClose = (childItem: any) => {
+ if (childItem.config?.component === "DocumentFrameRenderer") {
+ const docView = DocumentManager.Instance.getDocumentViewById(childItem.config.props.documentId);
+ if (docView && ((!document && docView.Document.isDisplayPanel) || (document && Doc.AreProtosEqual(docView.props.Document, document)))) {
+ childItem.remove();
instance.layoutChanged(document);
return true;
- } else {
- Array.from(child.contentItems).filter((tab: any) => tab.config.component === "DocumentFrameRenderer").some((tab: any, j: number) => {
- if (DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId) &&
- ((!document && DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId)!.Document.isDisplayPanel) ||
- (document && Doc.AreProtosEqual(DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId)!.Document, document)))) {
- child.contentItems[j].remove();
- child.config.activeItemIndex = Math.max(child.contentItems.length - 1, 0);
- return true;
- }
- return false;
- });
}
- return false;
- });
- }
- if (retVal) {
- instance.stateChanged();
- }
+ }
+ return false;
+ };
+ const retVal = !instance?._goldenLayout.root.contentItems[0].isRow ? false :
+ Array.from(instance._goldenLayout.root.contentItems[0].contentItems).some((child: any) => Array.from(child.contentItems).some(tryClose));
+
+ retVal && instance.stateChanged();
return retVal;
}
@@ -176,35 +156,30 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
}
@undoBatch
@action
- public static ReplaceRightSplit(document: Doc, dataDoc: Doc | undefined, libraryPath?: Doc[]): boolean {
- if (!CollectionDockingView.Instance) return false; const instance = CollectionDockingView.Instance;
- const newItemStackConfig = {
- type: 'stack',
- content: [CollectionDockingView.makeDocumentConfig(document, dataDoc, undefined, libraryPath)]
- };
-
- const newContentItem = instance._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, instance._goldenLayout);
-
+ public static ReplaceRightSplit(document: Doc, libraryPath?: Doc[], addToSplit?: boolean): boolean {
+ if (!CollectionDockingView.Instance) return false;
+ const instance = CollectionDockingView.Instance;
let retVal = false;
if (instance._goldenLayout.root.contentItems[0].isRow) {
retVal = Array.from(instance._goldenLayout.root.contentItems[0].contentItems).some((child: any) => {
if (child.contentItems.length === 1 && child.contentItems[0].config.component === "DocumentFrameRenderer" &&
- DocumentManager.Instance.getDocumentViewById(child.contentItems[0].config.props.documentId)?.Document.isDisplayPanle) {
- child.contentItems[0].remove();
- child.addChild(newContentItem, undefined, true);
+ DocumentManager.Instance.getDocumentViewById(child.contentItems[0].config.props.documentId)?.Document.isDisplayPanel) {
+ const newItemStackConfig = CollectionDockingView.makeDocumentConfig(document, undefined, libraryPath);
+ child.addChild(newItemStackConfig, undefined);
+ !addToSplit && child.contentItems[0].remove();
instance.layoutChanged(document);
return true;
- } else {
- Array.from(child.contentItems).filter((tab: any) => tab.config.component === "DocumentFrameRenderer").some((tab: any, j: number) => {
- if (DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId)?.Document.isDisplayPanel) {
- child.contentItems[j].remove();
- child.addChild(newContentItem, undefined, true);
- return true;
- }
- return false;
- });
}
- return false;
+ return Array.from(child.contentItems).filter((tab: any) => tab.config.component === "DocumentFrameRenderer").some((tab: any, j: number) => {
+ if (DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId)?.Document.isDisplayPanel) {
+ const newItemStackConfig = CollectionDockingView.makeDocumentConfig(document, undefined, libraryPath);
+ child.addChild(newItemStackConfig, undefined);
+ !addToSplit && child.contentItems[j].remove();
+ instance.layoutChanged(document);
+ return true;
+ }
+ return false;
+ });
});
}
if (retVal) {
@@ -219,12 +194,12 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
//
@undoBatch
@action
- public static AddRightSplit(document: Doc, dataDoc: Doc | undefined, libraryPath?: Doc[]) {
+ public static AddRightSplit(document: Doc, libraryPath?: Doc[]) {
if (!CollectionDockingView.Instance) return false;
const instance = CollectionDockingView.Instance;
const newItemStackConfig = {
type: 'stack',
- content: [CollectionDockingView.makeDocumentConfig(document, dataDoc, undefined, libraryPath)]
+ content: [CollectionDockingView.makeDocumentConfig(document, undefined, libraryPath)]
};
const newContentItem = instance._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, instance._goldenLayout);
@@ -251,16 +226,16 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
//
- // Creates a split on the any side of the docking view, based on the passed input pullSide and then adds the Document to the requested side
+ // Creates a split on any side of the docking view based on the passed input pullSide and then adds the Document to the requested side
//
@undoBatch
@action
- public static AddSplit(document: Doc, pullSide: string, dataDoc: Doc | undefined, libraryPath?: Doc[]) {
+ public static AddSplit(document: Doc, pullSide: string, libraryPath?: Doc[]) {
if (!CollectionDockingView.Instance) return false;
const instance = CollectionDockingView.Instance;
const newItemStackConfig = {
type: 'stack',
- content: [CollectionDockingView.makeDocumentConfig(document, dataDoc, undefined, libraryPath)]
+ content: [CollectionDockingView.makeDocumentConfig(document, undefined, libraryPath)]
};
const newContentItem = instance._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, instance._goldenLayout);
@@ -323,18 +298,18 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
//
@undoBatch
@action
- public static UseRightSplit(document: Doc, dataDoc: Doc | undefined, libraryPath?: Doc[]) {
+ public static UseRightSplit(document: Doc, libraryPath?: Doc[], shiftKey?: boolean) {
document.isDisplayPanel = true;
- if (!CollectionDockingView.ReplaceRightSplit(document, dataDoc, libraryPath)) {
- CollectionDockingView.AddRightSplit(document, dataDoc, libraryPath);
+ if (shiftKey || !CollectionDockingView.ReplaceRightSplit(document, libraryPath, shiftKey)) {
+ CollectionDockingView.AddRightSplit(document, libraryPath);
}
}
@undoBatch
@action
- public AddTab = (stack: any, document: Doc, dataDocument: Doc | undefined, libraryPath?: Doc[]) => {
+ public AddTab = (stack: any, document: Doc, libraryPath?: Doc[]) => {
Doc.GetProto(document).lastOpened = new DateField;
- const docContentConfig = CollectionDockingView.makeDocumentConfig(document, dataDocument, undefined, libraryPath);
+ const docContentConfig = CollectionDockingView.makeDocumentConfig(document, undefined, libraryPath);
if (stack === undefined) {
let stack: any = this._goldenLayout.root;
while (!stack.isStack) {
@@ -395,7 +370,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
componentDidMount: () => void = () => {
if (this._containerRef.current) {
this.reactionDisposer = reaction(
- () => StrCast(this.props.Document.dockingConfig),
+ () => this.props.Document.dockingConfig,
() => {
if (!this._goldenLayout || this._ignoreStateChange !== JSON.stringify(this._goldenLayout.toConfig())) {
// Because this is in a set timeout, if this component unmounts right after mounting,
@@ -457,16 +432,6 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
});
window.addEventListener("pointerup", onPointerUp);
const className = (e.target as any).className;
- if (className === "messageCounter") {
- e.stopPropagation();
- e.preventDefault();
- const x = e.clientX;
- const y = e.clientY;
- const docid = (e.target as any).DashDocId;
- const tab = (e.target as any).parentElement as HTMLElement;
- DocServer.GetRefField(docid).then(action(async (sourceDoc: Opt<Field>) =>
- (sourceDoc instanceof Doc) && DragManager.StartLinkTargetsDrag(tab, x, y, sourceDoc)));
- }
if (className === "lm_drag_handle" || className === "lm_close" || className === "lm_maximise" || className === "lm_minimise" || className === "lm_close_tab") {
this._flush = true;
}
@@ -508,24 +473,28 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
}
tabCreated = async (tab: any) => {
+ tab.titleElement[0].Tab = tab;
if (tab.hasOwnProperty("contentItem") && tab.contentItem.config.type !== "stack") {
if (tab.contentItem.config.fixed) {
tab.contentItem.parent.config.fixed = true;
}
const doc = await DocServer.GetRefField(tab.contentItem.config.props.documentId) as Doc;
- const dataDoc = await DocServer.GetRefField(tab.contentItem.config.props.dataDocumentId) as Doc;
if (doc instanceof Doc) {
- const dragSpan = document.createElement("span");
- dragSpan.style.position = "relative";
- dragSpan.style.bottom = "6px";
- dragSpan.style.paddingLeft = "4px";
- dragSpan.style.paddingRight = "2px";
+ //tab.titleElement[0].outerHTML = `<input class='lm_title' style="background:black" value='${doc.title}' />`;
+ tab.titleElement[0].onclick = (e: any) => tab.titleElement[0].focus();
+ tab.titleElement[0].onchange = (e: any) => {
+ tab.titleElement[0].size = e.currentTarget.value.length + 1;
+ Doc.GetProto(doc).title = e.currentTarget.value, true;
+ };
+ tab.titleElement[0].size = StrCast(doc.title).length + 1;
+ tab.titleElement[0].value = doc.title;
+ tab.titleElement[0].style["max-width"] = "100px";
const gearSpan = document.createElement("span");
+ gearSpan.className = "collectionDockingView-gear";
gearSpan.style.position = "relative";
gearSpan.style.paddingLeft = "0px";
gearSpan.style.paddingRight = "12px";
- const upDiv = document.createElement("span");
const stack = tab.contentItem.parent;
// shifts the focus to this tab when another tab is dragged over it
tab.element[0].onmouseenter = (e: any) => {
@@ -536,32 +505,40 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
}
tab.setActive(true);
};
- ReactDOM.render(<span title="Drag as document"
- className="collectionDockingView-dragAsDocument"
- onPointerDown={e => {
+ const onDown = (e: React.PointerEvent) => {
+ if (!(e.nativeEvent as any).defaultPrevented) {
e.preventDefault();
e.stopPropagation();
- DragManager.StartDocumentDrag([dragSpan], new DragManager.DocumentDragData([doc]), e.clientX, e.clientY);
- }}>
- <FontAwesomeIcon icon="file" size="lg" />
- </span>, dragSpan);
- ReactDOM.render(<ButtonSelector Document={doc} Stack={stack} />, gearSpan);
- tab.reactComponents = [dragSpan, gearSpan, upDiv];
- tab.element.append(dragSpan);
+ const dragData = new DragManager.DocumentDragData([doc]);
+ dragData.dropAction = doc.dropAction as dropActionType;
+ DragManager.StartDocumentDrag([gearSpan], dragData, e.clientX, e.clientY);
+ }
+ };
+ let rendered = false;
+ tab.buttonDisposer = reaction(() => ((view: Opt<DocumentView>) => view ? [view] : [])(DocumentManager.Instance.getDocumentView(doc)),
+ (views) => {
+ !rendered && ReactDOM.render(<span title="Drag as document" className="collectionDockingView-dragAsDocument" onPointerDown={onDown} >
+ <DockingViewButtonSelector views={views} Stack={stack} />
+ </span>,
+ gearSpan);
+ rendered = true;
+ });
+
+ tab.reactComponents = [gearSpan];
tab.element.append(gearSpan);
- tab.element.append(upDiv);
- tab.reactionDisposer = reaction(() => [doc.title, Doc.IsBrushedDegree(doc)], () => {
- tab.titleElement[0].textContent = doc.title, { fireImmediately: true };
- tab.titleElement[0].style.outline = `${["transparent", "white", "white"][Doc.IsBrushedDegreeUnmemoized(doc)]} ${["none", "dashed", "solid"][Doc.IsBrushedDegreeUnmemoized(doc)]} 1px`;
+ tab.reactionDisposer = reaction(() => ({ title: doc.title, degree: Doc.IsBrushedDegree(doc) }), ({ title, degree }) => {
+ tab.titleElement[0].textContent = title, { fireImmediately: true };
+ tab.titleElement[0].style.padding = degree ? 0 : 2;
+ tab.titleElement[0].style.border = `${["gray", "gray", "gray"][degree]} ${["none", "dashed", "solid"][degree]} 2px`;
});
//TODO why can't this just be doc instead of the id?
tab.titleElement[0].DashDocId = tab.contentItem.config.props.documentId;
}
}
- tab.titleElement[0].Tab = tab;
tab.closeElement.off('click') //unbind the current click handler
.click(async function () {
- tab.reactionDisposer && tab.reactionDisposer();
+ tab.reactionDisposer?.();
+ tab.buttonDisposer?.();
const doc = await DocServer.GetRefField(tab.contentItem.config.props.documentId);
if (doc instanceof Doc) {
const theDoc = doc;
@@ -594,7 +571,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
stack.header.element[0].style.backgroundColor = DocServer.Control.isReadOnly() ? "#228540" : undefined;
stack.header.element.on('mousedown', (e: any) => {
if (e.target === stack.header.element[0] && e.button === 1) {
- this.AddTab(stack, Docs.Create.FreeformDocument([], { _width: this.props.PanelWidth(), _height: this.props.PanelHeight(), title: "Untitled Collection" }), undefined);
+ this.AddTab(stack, Docs.Create.FreeformDocument([], { _width: this.props.PanelWidth(), _height: this.props.PanelHeight(), title: "Untitled Collection" }));
}
});
@@ -667,9 +644,9 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
interface DockedFrameProps {
documentId: FieldId;
- dataDocumentId: FieldId;
glContainer: any;
libraryPath: (FieldId[]);
+ backgroundColor?: (doc: Doc) => string | undefined;
//collectionDockingView: CollectionDockingView
}
@observer
@@ -679,7 +656,6 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
@observable private _panelWidth = 0;
@observable private _panelHeight = 0;
@observable private _document: Opt<Doc>;
- @observable private _dataDoc: Opt<Doc>;
@observable private _isActive: boolean = false;
get _stack(): any {
@@ -687,12 +663,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
}
constructor(props: any) {
super(props);
- DocServer.GetRefField(this.props.documentId).then(action((f: Opt<Field>) => {
- this._document = f as Doc;
- if (this.props.dataDocumentId && this.props.documentId !== this.props.dataDocumentId) {
- DocServer.GetRefField(this.props.dataDocumentId).then(action((f: Opt<Field>) => this._dataDoc = f as Doc));
- }
- }));
+ DocServer.GetRefField(this.props.documentId).then(action((f: Opt<Field>) => this._document = f as Doc));
this.props.libraryPath && this.setupLibraryPath();
}
@@ -708,24 +679,31 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
**/
@undoBatch
@action
- public PinDoc(doc: Doc) {
+ public static PinDoc(doc: Doc) {
//add this new doc to props.Document
const curPres = Cast(CurrentUserUtils.UserDocument.curPresentation, Doc) as Doc;
if (curPres) {
- const pinDoc = Docs.Create.PresElementBoxDocument({ backgroundColor: "transparent" });
- Doc.GetProto(pinDoc).presentationTargetDoc = doc;
- Doc.GetProto(pinDoc).title = ComputedField.MakeFunction('(this.presentationTargetDoc instanceof Doc) && this.presentationTargetDoc.title?.toString()');
- const data = Cast(curPres.data, listSpec(Doc));
- if (data) {
- data.push(pinDoc);
- } else {
- curPres.data = new List([pinDoc]);
- }
+ const pinDoc = Doc.MakeAlias(doc);
+ pinDoc.presentationTargetDoc = doc;
+ Doc.AddDocToList(curPres, "data", pinDoc);
if (!DocumentManager.Instance.getDocumentView(curPres)) {
- this.addDocTab(curPres, undefined, "onRight");
+ CollectionDockingView.AddRightSplit(curPres);
}
}
}
+ /**
+ * Adds a document to the presentation view
+ **/
+ @undoBatch
+ @action
+ public static UnpinDoc(doc: Doc) {
+ //add this new doc to props.Document
+ const curPres = Cast(CurrentUserUtils.UserDocument.curPresentation, Doc) as Doc;
+ if (curPres) {
+ const ind = DocListCast(curPres.data).findIndex((val) => Doc.AreProtosEqual(val, doc));
+ ind !== -1 && Doc.RemoveDocFromList(curPres, "data", DocListCast(curPres.data)[ind]);
+ }
+ }
componentDidMount() {
const observer = new _global.ResizeObserver(action((entries: any) => {
@@ -757,8 +735,8 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
panelWidth = () => this.layoutDoc && this.layoutDoc.maxWidth ? Math.min(Math.max(NumCast(this.layoutDoc._width), NumCast(this.layoutDoc._nativeWidth)), this._panelWidth) : this._panelWidth;
panelHeight = () => this._panelHeight;
- nativeWidth = () => !this.layoutDoc!.ignoreAspect && !this.layoutDoc!._fitWidth ? NumCast(this.layoutDoc!._nativeWidth) || this._panelWidth : 0;
- nativeHeight = () => !this.layoutDoc!.ignoreAspect && !this.layoutDoc!._fitWidth ? NumCast(this.layoutDoc!._nativeHeight) || this._panelHeight : 0;
+ nativeWidth = () => !this.layoutDoc!._fitWidth ? NumCast(this.layoutDoc!._nativeWidth) || this._panelWidth : 0;
+ nativeHeight = () => !this.layoutDoc!._fitWidth ? NumCast(this.layoutDoc!._nativeHeight) || this._panelHeight : 0;
contentScaling = () => {
if (this.layoutDoc!.type === DocumentType.PDF) {
@@ -784,19 +762,19 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
}
return Transform.Identity();
}
- get previewPanelCenteringOffset() { return this.nativeWidth() && !this.layoutDoc!.ignoreAspect ? (this._panelWidth - this.nativeWidth() * this.contentScaling()) / 2 : 0; }
- get widthpercent() { return this.nativeWidth() && !this.layoutDoc!.ignoreAspect ? `${(this.nativeWidth() * this.contentScaling()) / this.panelWidth() * 100}%` : undefined; }
+ get previewPanelCenteringOffset() { return this.nativeWidth() ? (this._panelWidth - this.nativeWidth() * this.contentScaling()) / 2 : 0; }
+ get widthpercent() { return this.nativeWidth() ? `${(this.nativeWidth() * this.contentScaling()) / this.panelWidth() * 100}%` : undefined; }
- addDocTab = (doc: Doc, dataDoc: Opt<Doc>, location: string, libraryPath?: Doc[]) => {
+ addDocTab = (doc: Doc, location: string, libraryPath?: Doc[]) => {
SelectionManager.DeselectAll();
if (doc.dockingConfig) {
return MainView.Instance.openWorkspace(doc);
} else if (location === "onRight") {
- return CollectionDockingView.AddRightSplit(doc, dataDoc, libraryPath);
+ return CollectionDockingView.AddRightSplit(doc, libraryPath);
} else if (location === "close") {
return CollectionDockingView.CloseRightSplit(doc);
} else {
- return CollectionDockingView.Instance.AddTab(this._stack, doc, dataDoc, libraryPath);
+ return CollectionDockingView.Instance.AddTab(this._stack, doc, libraryPath);
}
}
@@ -804,7 +782,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
TraceMobx();
if (!this._document) return (null);
const document = this._document;
- const resolvedDataDoc = document.layout instanceof Doc ? document : this._dataDoc;
+ const resolvedDataDoc = !Doc.AreProtosEqual(this._document[DataSym], this._document) ? this._document[DataSym] : undefined;// document.layout instanceof Doc ? document : this._dataDoc;
return <DocumentView key={document[Id]}
LibraryPath={this._libraryPath}
Document={document}
@@ -820,9 +798,9 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
parentActive={returnTrue}
whenActiveChanged={emptyFunction}
focus={emptyFunction}
- backgroundColor={returnEmptyString}
+ backgroundColor={CollectionDockingView.Instance.props.backgroundColor}
addDocTab={this.addDocTab}
- pinToPres={this.PinDoc}
+ pinToPres={DockedFrameRenderer.PinDoc}
ContainingCollectionView={undefined}
ContainingCollectionDoc={undefined}
zoomToScale={emptyFunction}
@@ -841,5 +819,5 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
</div >);
}
}
-Scripting.addGlobal(function openOnRight(doc: any) { CollectionDockingView.AddRightSplit(doc, undefined); });
-Scripting.addGlobal(function useRightSplit(doc: any) { CollectionDockingView.UseRightSplit(doc, undefined); });
+Scripting.addGlobal(function openOnRight(doc: any) { CollectionDockingView.AddRightSplit(doc); });
+Scripting.addGlobal(function useRightSplit(doc: any, shiftKey?: boolean) { CollectionDockingView.UseRightSplit(doc, undefined, shiftKey); });
diff --git a/src/client/views/collections/CollectionLinearView.tsx b/src/client/views/collections/CollectionLinearView.tsx
index e613bf411..344605412 100644
--- a/src/client/views/collections/CollectionLinearView.tsx
+++ b/src/client/views/collections/CollectionLinearView.tsx
@@ -31,15 +31,15 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) {
this._dropDisposer && this._dropDisposer();
this._widthDisposer && this._widthDisposer();
this._selectedDisposer && this._selectedDisposer();
- this.childLayoutPairs.filter((pair) => this.isCurrent(pair.layout)).map((pair, ind) => {
+ this.childLayoutPairs.map((pair, ind) => {
Cast(pair.layout.proto?.onPointerUp, ScriptField)?.script.run({ this: pair.layout.proto }, console.log);
});
}
componentDidMount() {
// is there any reason this needs to exist? -syip. yes, it handles autoHeight for stacking views (masonry isn't yet supported).
- this._widthDisposer = reaction(() => this.props.Document[HeightSym]() + this.childDocs.length + (this.props.Document.isExpanded ? 1 : 0),
- () => this.props.Document._width = 5 + (this.props.Document.isExpanded ? this.childDocs.length * (this.props.Document[HeightSym]()) : 10),
+ this._widthDisposer = reaction(() => this.props.Document[HeightSym]() + this.childDocs.length + (this.props.Document.linearViewIsExpanded ? 1 : 0),
+ () => this.props.Document._width = 5 + (this.props.Document.linearViewIsExpanded ? this.childDocs.length * (this.props.Document[HeightSym]()) : 10),
{ fireImmediately: true }
);
@@ -48,7 +48,7 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) {
(i) => runInAction(() => {
this._selectedIndex = i;
let selected: any = undefined;
- this.childLayoutPairs.filter((pair) => this.isCurrent(pair.layout)).map(async (pair, ind) => {
+ this.childLayoutPairs.map(async (pair, ind) => {
const isSelected = this._selectedIndex === ind;
if (isSelected) {
selected = pair;
@@ -67,17 +67,15 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) {
protected createDashEventsTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view
this._dropDisposer && this._dropDisposer();
if (ele) {
- this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this));
+ this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this));
}
}
- public isCurrent(doc: Doc) { return !doc.isMinimized && (Math.abs(NumCast(doc.displayTimecode, -1) - NumCast(this.Document.currentTimecode, -1)) < 1.5 || NumCast(doc.displayTimecode, -1) === -1); }
-
dimension = () => NumCast(this.props.Document._height); // 2 * the padding
getTransform = (ele: React.RefObject<HTMLDivElement>) => () => {
if (!ele.current) return Transform.Identity();
const { scale, translateX, translateY } = Utils.GetScreenTransform(ele.current);
- return new Transform(-translateX, -translateY, 1 / scale);
+ return new Transform(-translateX, -translateY, 1);
}
render() {
@@ -85,20 +83,24 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) {
const flexDir: any = StrCast(this.Document.flexDirection);
return <div className="collectionLinearView-outer">
<div className="collectionLinearView" ref={this.createDashEventsTarget} >
- <input id={`${guid}`} type="checkbox" checked={BoolCast(this.props.Document.isExpanded)} ref={this.addMenuToggle}
- onChange={action((e: any) => this.props.Document.isExpanded = this.addMenuToggle.current!.checked)} />
- <label htmlFor={`${guid}`} style={{ marginTop: "auto", marginBottom: "auto", background: StrCast(this.props.Document.backgroundColor, "black") === StrCast(this.props.Document.color, "white") ? "black" : StrCast(this.props.Document.backgroundColor, "black") }} title="Close Menu"><p>+</p></label>
+ <input id={`${guid}`} type="checkbox" checked={BoolCast(this.props.Document.linearViewIsExpanded)} ref={this.addMenuToggle}
+ onChange={action((e: any) => this.props.Document.linearViewIsExpanded = this.addMenuToggle.current!.checked)} />
+ <label htmlFor={`${guid}`} title="Close Menu" style={{ marginTop: "auto", marginBottom: "auto",
+ background: StrCast(this.props.Document.backgroundColor, "black") === StrCast(this.props.Document.color, "white") ? "black" : StrCast(this.props.Document.backgroundColor, "black") }} >
+ <p>+</p>
+ </label>
- <div className="collectionLinearView-content" style={{ height: this.dimension(), width: NumCast(this.props.Document._width, 25), flexDirection: flexDir }}>
- {this.childLayoutPairs.filter((pair) => this.isCurrent(pair.layout)).map((pair, ind) => {
+ <div className="collectionLinearView-content" style={{ height: this.dimension(), flexDirection: flexDir }}>
+ {this.childLayoutPairs.map((pair, ind) => {
const nested = pair.layout._viewType === CollectionViewType.Linear;
const dref = React.createRef<HTMLDivElement>();
const nativeWidth = NumCast(pair.layout._nativeWidth, this.dimension());
const deltaSize = nativeWidth * .15 / 2;
- return <div className={`collectionLinearView-docBtn` + (pair.layout.onClick || pair.layout.onDragStart ? "-scalable" : "")} key={pair.layout[Id]} ref={dref}
+ const scalable = pair.layout.onClick || pair.layout.onDragStart;
+ return <div className={`collectionLinearView-docBtn` + (scalable ? "-scalable" : "")} key={pair.layout[Id]} ref={dref}
style={{
- width: nested ? pair.layout[WidthSym]() : this.dimension() - deltaSize,
- height: nested && pair.layout.isExpanded ? pair.layout[HeightSym]() : this.dimension() - deltaSize,
+ width: scalable ? (nested ? pair.layout[WidthSym]() : this.dimension() - deltaSize) : undefined,
+ height: nested && pair.layout.linearViewIsExpanded ? pair.layout[HeightSym]() : this.dimension() - deltaSize,
}} >
<DocumentView
Document={pair.layout}
diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
index e25a2f5eb..af3e18a4b 100644
--- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
+++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
@@ -2,13 +2,13 @@ import React = require("react");
import { library } from '@fortawesome/fontawesome-svg-core';
import { faPalette } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, observable, computed } from "mobx";
+import { action, computed, observable } from "mobx";
import { observer } from "mobx-react";
import Measure from "react-measure";
import { Doc } from "../../../new_fields/Doc";
import { PastelSchemaPalette, SchemaHeaderField } from "../../../new_fields/SchemaHeaderField";
import { ScriptField } from "../../../new_fields/ScriptField";
-import { StrCast } from "../../../new_fields/Types";
+import { StrCast, NumCast } from "../../../new_fields/Types";
import { numberRange } from "../../../Utils";
import { Docs } from "../../documents/Documents";
import { DragManager } from "../../util/DragManager";
@@ -16,10 +16,12 @@ import { CompileScript } from "../../util/Scripting";
import { SelectionManager } from "../../util/SelectionManager";
import { Transform } from "../../util/Transform";
import { undoBatch } from "../../util/UndoManager";
-import { anchorPoints, Flyout } from "../DocumentDecorations";
import { EditableView } from "../EditableView";
import { CollectionStackingView } from "./CollectionStackingView";
import "./CollectionStackingView.scss";
+const higflyout = require("@hig/flyout");
+export const { anchorPoints } = higflyout;
+export const Flyout = higflyout.default;
library.add(faPalette);
@@ -73,14 +75,15 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
@undoBatch
rowDrop = action((e: Event, de: DragManager.DropEvent) => {
+ console.log("masronry row drop");
this._createAliasSelected = false;
if (de.complete.docDragData) {
(this.props.parent.Document.dropConverter instanceof ScriptField) &&
this.props.parent.Document.dropConverter.script.run({ dragData: de.complete.docDragData });
- const key = StrCast(this.props.parent.props.Document.sectionFilter);
+ const key = StrCast(this.props.parent.props.Document._pivotField);
const castedValue = this.getValue(this._heading);
de.complete.docDragData.droppedDocuments.forEach(d => d[key] = castedValue);
- this.props.parent.drop(e, de);
+ this.props.parent.onInternalDrop(e, de);
e.stopPropagation();
}
});
@@ -96,7 +99,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
@action
headingChanged = (value: string, shiftDown?: boolean) => {
this._createAliasSelected = false;
- const key = StrCast(this.props.parent.props.Document.sectionFilter);
+ const key = StrCast(this.props.parent.props.Document._pivotField);
const castedValue = this.getValue(value);
if (castedValue) {
if (this.props.parent.sectionHeaders) {
@@ -135,15 +138,16 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
@action
addDocument = (value: string, shiftDown?: boolean) => {
this._createAliasSelected = false;
- const key = StrCast(this.props.parent.props.Document.sectionFilter);
- const newDoc = Docs.Create.TextDocument("", { _height: 18, _width: 200, title: value });
+ const key = StrCast(this.props.parent.props.Document._pivotField);
+ const newDoc = Docs.Create.TextDocument(value, { _autoHeight: true, _width: 200, title: value });
newDoc[key] = this.getValue(this.props.heading);
- return this.props.parent.props.addDocument(newDoc);
+ const docs = this.props.parent.childDocList;
+ return docs ? (docs.splice(0, 0, newDoc) ? true : false) : this.props.parent.props.addDocument(newDoc);
}
deleteRow = undoBatch(action(() => {
this._createAliasSelected = false;
- const key = StrCast(this.props.parent.props.Document.sectionFilter);
+ const key = StrCast(this.props.parent.props.Document._pivotField);
this.props.docList.forEach(d => d[key] = undefined);
if (this.props.parent.sectionHeaders && this.props.headingObject) {
const index = this.props.parent.sectionHeaders.indexOf(this.props.headingObject);
@@ -165,7 +169,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
const [dx, dy] = this.props.screenToLocalTransform().transformDirection(e.clientX - this._startDragPosition.x, e.clientY - this._startDragPosition.y);
if (Math.abs(dx) + Math.abs(dy) > this._sensitivity) {
const alias = Doc.MakeAlias(this.props.parent.props.Document);
- const key = StrCast(this.props.parent.props.Document.sectionFilter);
+ const key = StrCast(this.props.parent.props.Document._pivotField);
let value = this.getValue(this._heading);
value = typeof value === "string" ? `"${value}"` : value;
const script = `return doc.${key} === ${value}`;
@@ -257,7 +261,8 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
@computed get contentLayout() {
const rows = Math.max(1, Math.min(this.props.docList.length, Math.floor((this.props.parent.props.PanelWidth() - 2 * this.props.parent.xMargin) / (this.props.parent.columnWidth + this.props.parent.gridGap))));
- const style = this.props.parent; const collapsed = this._collapsed;
+ const style = this.props.parent;
+ const collapsed = this._collapsed;
const chromeStatus = this.props.parent.props.Document._chromeStatus;
const newEditableViewProps = {
GetValue: () => "",
@@ -270,6 +275,15 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
};
return collapsed ? (null) :
<div style={{ position: "relative" }}>
+ {(chromeStatus !== 'view-mode' && chromeStatus !== 'disabled') ?
+ <div className="collectionStackingView-addDocumentButton"
+ style={{
+ width: style.columnWidth / style.numGroupColumns,
+ padding: NumCast(this.props.parent.layoutDoc._yPadding)
+ }}>
+ <EditableView {...newEditableViewProps} />
+ </div> : null
+ }
<div className={`collectionStackingView-masonryGrid`}
ref={this._contRef}
style={{
@@ -281,18 +295,12 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
{this.props.parent.children(this.props.docList)}
{this.props.parent.columnDragger}
</div>
- {(chromeStatus !== 'view-mode' && chromeStatus !== 'disabled') ?
- <div className="collectionStackingView-addDocumentButton"
- style={{ width: style.columnWidth / style.numGroupColumns }}>
- <EditableView {...newEditableViewProps} />
- </div> : null
- }
</div>;
}
@computed get headingView() {
const heading = this._heading;
- const key = StrCast(this.props.parent.props.Document.sectionFilter);
+ const key = StrCast(this.props.parent.props.Document._pivotField);
const evContents = heading ? heading : this.props.type && this.props.type === "number" ? "0" : `NO ${key.toUpperCase()} VALUE`;
const headerEditableViewProps = {
GetValue: () => evContents,
@@ -313,7 +321,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
<div className="collectionStackingView-sectionHeader-subCont" onPointerDown={this.headerDown}
title={evContents === `NO ${key.toUpperCase()} VALUE` ?
`Documents that don't have a ${key} value will go here. This column cannot be removed.` : ""}
- style={{ background: evContents !== `NO ${key.toUpperCase()} VALUE` ? this._color : "lightgrey", }}>
+ style={{ background: evContents !== `NO ${key.toUpperCase()} VALUE` ? this._color : "lightgrey" }}>
<EditableView {...headerEditableViewProps} />
{evContents === `NO ${key.toUpperCase()} VALUE` ? (null) :
<div className="collectionStackingView-sectionColor">
diff --git a/src/client/views/collections/CollectionPivotView.scss b/src/client/views/collections/CollectionPivotView.scss
deleted file mode 100644
index 505091e98..000000000
--- a/src/client/views/collections/CollectionPivotView.scss
+++ /dev/null
@@ -1,88 +0,0 @@
-.collectionPivotView {
- display: flex;
- flex-direction: row;
- position: absolute;
- height: 100%;
- width: 100%;
-
- .collectionPivotView-flyout {
- width: 400px;
- height: 300px;
- display: inline-block;
-
- .collectionPivotView-flyout-item {
- background-color: lightgray;
- text-align: left;
- display: inline-block;
- position: relative;
- width: 100%;
- }
- }
-
- .pivotKeyEntry {
- position: absolute;
- top: 5px;
- right: 5px;
- z-index: 10;
- pointer-events: all;
- padding: 5px;
- border: 1px solid black;
- }
-
- .collectionPivotView-treeView {
- display: flex;
- flex-direction: column;
- width: 200px;
- height: 100%;
-
- .collectionPivotView-addfacet {
- display: inline-block;
- width: 200px;
- height: 30px;
- background: darkGray;
- text-align: center;
-
- .collectionPivotView-button {
- align-items: center;
- display: flex;
- width: 100%;
- height: 100%;
-
- .collectionPivotView-span {
- margin: auto;
- }
- }
-
- >div,
- >div>div {
- width: 100%;
- height: 100%;
- text-align: center;
- }
- }
-
- .collectionPivotView-tree {
- display: inline-block;
- width: 100%;
- height: calc(100% - 30px);
- }
- }
-
- .collectionPivotView-pivot {
- display: inline-block;
- width: calc(100% - 200px);
- height: 100%;
- }
-
- .collectionPivotView-dragger {
- background-color: lightgray;
- height: 40px;
- width: 20px;
- position: absolute;
- border-radius: 10px;
- top: 55%;
- border: 1px black solid;
- z-index: 2;
- left: -10px;
- }
-} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionPivotView.tsx b/src/client/views/collections/CollectionPivotView.tsx
deleted file mode 100644
index 440b6856b..000000000
--- a/src/client/views/collections/CollectionPivotView.tsx
+++ /dev/null
@@ -1,148 +0,0 @@
-import { faEdit } from "@fortawesome/free-solid-svg-icons";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, computed, IReactionDisposer, observable } from "mobx";
-import { observer } from "mobx-react";
-import { Set } from "typescript-collections";
-import { Doc, DocListCast } from "../../../new_fields/Doc";
-import { List } from "../../../new_fields/List";
-import { listSpec } from "../../../new_fields/Schema";
-import { ComputedField, ScriptField } from "../../../new_fields/ScriptField";
-import { Cast, StrCast } from "../../../new_fields/Types";
-import { Docs } from "../../documents/Documents";
-import { EditableView } from "../EditableView";
-import { anchorPoints, Flyout } from "../TemplateMenu";
-import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView";
-import "./CollectionPivotView.scss";
-import { CollectionSubView } from "./CollectionSubView";
-import { CollectionTreeView } from "./CollectionTreeView";
-import React = require("react");
-
-@observer
-export class CollectionPivotView extends CollectionSubView(doc => doc) {
- componentDidMount() {
- this.props.Document._freeformLayoutEngine = "pivot";
- const childDetailed = this.props.Document.childDetailed; // bcz: needs to be here to make sure the childDetailed layout template has been loaded when the first item is clicked;
- if (!this.props.Document._facetCollection) {
- const facetCollection = Docs.Create.TreeDocument([], { title: "facetFilters", _yMargin: 0, treeViewHideTitle: true });
- facetCollection.target = this.props.Document;
- this.props.Document.excludeFields = new List<string>(["_facetCollection", "_docFilter"]);
-
- const scriptText = "setDocFilter(containingTreeView.target, heading, this.title, checked)";
- const childText = "const alias = getAlias(this); Doc.ApplyTemplateTo(containingCollection.childDetailed, alias, 'layout_detailed'); useRightSplit(alias); ";
- facetCollection.onCheckedClick = ScriptField.MakeScript(scriptText, { this: Doc.name, heading: "boolean", checked: "boolean", containingTreeView: Doc.name });
- this.props.Document.onChildClick = ScriptField.MakeScript(childText, { this: Doc.name, heading: "boolean", containingCollection: Doc.name });
- this.props.Document._facetCollection = facetCollection;
- this.props.Document._fitToBox = true;
- }
- }
- bodyPanelWidth = () => this.props.PanelWidth() - this._facetWidth;
- getTransform = () => this.props.ScreenToLocalTransform().translate(-200, 0);
-
- @computed get _allFacets() {
- const facets = new Set<string>();
- this.childDocs.forEach(child => Object.keys(Doc.GetProto(child)).forEach(key => facets.add(key)));
- return facets.toArray();
- }
-
- /**
- * Responds to clicking the check box in the flyout menu
- */
- facetClick = (facetHeader: string) => {
- const facetCollection = this.props.Document._facetCollection;
- if (facetCollection instanceof Doc) {
- const found = DocListCast(facetCollection.data).findIndex(doc => doc.title === facetHeader);
- if (found !== -1) {
- (facetCollection.data as List<Doc>).splice(found, 1);
- const docFilter = Cast(this.props.Document._docFilter, listSpec("string"));
- if (docFilter) {
- let index: number;
- while ((index = docFilter.findIndex(item => item === facetHeader)) !== -1) {
- docFilter.splice(index, 3);
- }
- }
- } else {
- const newFacet = Docs.Create.TreeDocument([], { title: facetHeader, treeViewOpen: true, isFacetFilter: true });
- const capturedVariables = { layoutDoc: this.props.Document, dataDoc: this.dataDoc };
- const params = { layoutDoc: Doc.name, dataDoc: Doc.name, };
- newFacet.data = ComputedField.MakeFunction(`readFacetData(layoutDoc, dataDoc, "${this.props.fieldKey}", "${facetHeader}")`, params, capturedVariables);
- Doc.AddDocToList(facetCollection, "data", newFacet);
- }
- }
- }
- _canClick = false;
- _facetWidthOnDown = 0;
- @observable _facetWidth = 200;
- onPointerDown = (e: React.PointerEvent) => {
- this._canClick = true;
- this._facetWidthOnDown = e.screenX;
- document.removeEventListener("pointermove", this.onPointerMove);
- document.removeEventListener("pointerup", this.onPointerUp);
- document.addEventListener("pointermove", this.onPointerMove);
- document.addEventListener("pointerup", this.onPointerUp);
- e.stopPropagation();
- e.preventDefault();
- }
-
-
- @action
- onPointerMove = (e: PointerEvent) => {
- this._facetWidth = Math.max(this.props.ScreenToLocalTransform().transformPoint(e.clientX, 0)[0], 0);
- Math.abs(e.movementX) > 6 && (this._canClick = false);
- }
- @action
- onPointerUp = (e: PointerEvent) => {
- if (Math.abs(e.screenX - this._facetWidthOnDown) < 6 && this._canClick) {
- this._facetWidth = this._facetWidth < 15 ? 200 : 0;
- }
- document.removeEventListener("pointermove", this.onPointerMove);
- document.removeEventListener("pointerup", this.onPointerUp);
- }
-
- render() {
- const facetCollection = Cast(this.props.Document?._facetCollection, Doc, null);
- const flyout = (
- <div className="collectionPivotView-flyout" style={{ width: `${this._facetWidth}` }}>
- {this._allFacets.map(facet => <label className="collectionPivotView-flyout-item" key={`${facet}`} onClick={e => this.facetClick(facet)}>
- <input type="checkbox" onChange={e => { }} checked={DocListCast((this.props.Document._facetCollection as Doc)?.data).some(d => d.title === facet)} />
- <span className="checkmark" />
- {facet}
- </label>)}
- </div>
- );
- return !facetCollection ? (null) :
- <div className="collectionPivotView" style={{ height: `calc(100% - ${this.props.Document._chromeStatus === "enabled" ? 51 : 0}px)` }}>
- <div className={"pivotKeyEntry"}>
- <EditableView
- contents={this.props.Document.pivotField}
- GetValue={() => StrCast(this.props.Document.pivotField)}
- SetValue={value => {
- if (value && value.length) {
- this.props.Document.pivotField = value;
- return true;
- }
- return false;
- }}
- />
- </div>
- <div className="collectionPivotView-dragger" key="dragger" onPointerDown={this.onPointerDown} style={{ transform: `translate(${this._facetWidth}px, 0px)` }} >
- <span title="library View Dragger" style={{ width: "5px", position: "absolute", top: "0" }} />
- </div>
- <div className="collectionPivotView-treeView" style={{ width: `${this._facetWidth}px`, overflow: this._facetWidth < 15 ? "hidden" : undefined }}>
- <div className="collectionPivotView-addFacet" style={{ width: `${this._facetWidth}px` }} onPointerDown={e => e.stopPropagation()}>
- <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={flyout}>
- <div className="collectionPivotView-button">
- <span className="collectionPivotView-span">Facet Filters</span>
- <FontAwesomeIcon icon={faEdit} size={"lg"} />
- </div>
- </Flyout>
- </div>
- <div className="collectionPivotView-tree" key="tree">
- <CollectionTreeView {...this.props} Document={facetCollection} />
- </div>
- </div>
- <div className="collectionPivotView-pivot" key="pivot" style={{ width: this.bodyPanelWidth() }}>
- <CollectionFreeFormView {...this.props} ScreenToLocalTransform={this.getTransform} PanelWidth={this.bodyPanelWidth} />
- </div>
- </div>;
- }
-} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx
index 4eba5dc26..f124fe21b 100644
--- a/src/client/views/collections/CollectionSchemaCells.tsx
+++ b/src/client/views/collections/CollectionSchemaCells.tsx
@@ -36,9 +36,10 @@ export interface CellProps {
Document: Doc;
fieldKey: string;
renderDepth: number;
- addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean;
+ addDocTab: (document: Doc, where: string) => boolean;
pinToPres: (document: Doc) => void;
- moveDocument: (document: Doc, targetCollection: Doc | undefined, addDocument: (document: Doc) => boolean) => boolean;
+ moveDocument: (document: Doc, targetCollection: Doc | undefined,
+ addDocument: (document: Doc) => boolean) => boolean;
isFocused: boolean;
changeFocusedCellByIndex: (row: number, col: number) => void;
setIsEditing: (isEditing: boolean) => void;
@@ -76,7 +77,8 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
@action
isEditingCallback = (isEditing: boolean): void => {
- document.addEventListener("keydown", this.onKeyDown);
+ document.removeEventListener("keydown", this.onKeyDown);
+ isEditing && document.addEventListener("keydown", this.onKeyDown);
this._isEditing = isEditing;
this.props.setIsEditing(isEditing);
this.props.changeFocusedCellByIndex(this.props.row, this.props.col);
@@ -155,6 +157,8 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
Document: this.props.rowProps.original,
DataDoc: this.props.rowProps.original,
LibraryPath: [],
+ dropAction: "alias",
+ bringToFront: emptyFunction,
fieldKey: this.props.rowProps.column.id as string,
ContainingCollectionView: this.props.CollectionView,
ContainingCollectionDoc: this.props.CollectionView && this.props.CollectionView.props.Document,
@@ -246,7 +250,9 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } });
if (script.compiled) {
DocListCast(this.props.Document[this.props.fieldKey]).
- forEach((doc, i) => this.applyToDoc(doc, i, this.props.col, script.run));
+ forEach((doc, i) => value.startsWith(":=") ?
+ this.props.setComputed(value.substring(2), doc, this.props.rowProps.column.id!, i, this.props.col) :
+ this.applyToDoc(doc, i, this.props.col, script.run));
}
}}
/>
diff --git a/src/client/views/collections/CollectionSchemaHeaders.tsx b/src/client/views/collections/CollectionSchemaHeaders.tsx
index 92dc8780e..507ee89e4 100644
--- a/src/client/views/collections/CollectionSchemaHeaders.tsx
+++ b/src/client/views/collections/CollectionSchemaHeaders.tsx
@@ -5,11 +5,13 @@ import "./CollectionSchemaView.scss";
import { faPlus, faFont, faHashtag, faAlignJustify, faCheckSquare, faToggleOn, faSortAmountDown, faSortAmountUp, faTimes } from '@fortawesome/free-solid-svg-icons';
import { library, IconProp } from "@fortawesome/fontawesome-svg-core";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { Flyout, anchorPoints } from "../DocumentDecorations";
import { ColumnType } from "./CollectionSchemaView";
import { faFile } from "@fortawesome/free-regular-svg-icons";
import { SchemaHeaderField, PastelSchemaPalette } from "../../../new_fields/SchemaHeaderField";
import { undoBatch } from "../../util/UndoManager";
+const higflyout = require("@hig/flyout");
+export const { anchorPoints } = higflyout;
+export const Flyout = higflyout.default;
library.add(faPlus, faFont, faHashtag, faAlignJustify, faCheckSquare, faToggleOn, faFile as any, faSortAmountDown, faSortAmountUp, faTimes);
@@ -289,13 +291,11 @@ class KeysDropdown extends React.Component<KeysDropdownProps> {
onKeyDown = (e: React.KeyboardEvent): void => {
if (e.key === "Enter") {
const keyOptions = this._searchTerm === "" ? this.props.possibleKeys : this.props.possibleKeys.filter(key => key.toUpperCase().indexOf(this._searchTerm.toUpperCase()) > -1);
- const exactFound = keyOptions.findIndex(key => key.toUpperCase() === this._searchTerm.toUpperCase()) > -1 ||
- this.props.existingKeys.findIndex(key => key.toUpperCase() === this._searchTerm.toUpperCase()) > -1;
-
- if (!exactFound && this._searchTerm !== "" && this.props.canAddNew) {
+ if (keyOptions.length) {
+ this.onSelect(keyOptions[0]);
+ } else if (this._searchTerm !== "" && this.props.canAddNew) {
+ this.setSearchTerm(this._searchTerm || this._key);
this.onSelect(this._searchTerm);
- } else {
- this.setSearchTerm(this._key);
}
}
}
@@ -336,7 +336,7 @@ class KeysDropdown extends React.Component<KeysDropdownProps> {
this.props.existingKeys.findIndex(key => key.toUpperCase() === this._searchTerm.toUpperCase()) > -1;
const options = keyOptions.map(key => {
- return <div key={key} className="key-option" onClick={() => { this.onSelect(key); this.setSearchTerm(""); }}>{key}</div>;
+ return <div key={key} className="key-option" onPointerDown={e => e.stopPropagation()} onClick={() => { this.onSelect(key); this.setSearchTerm(""); }}>{key}</div>;
});
// if search term does not already exist as a group type, give option to create new group type
@@ -354,7 +354,7 @@ class KeysDropdown extends React.Component<KeysDropdownProps> {
<div className="keys-dropdown">
<input className="keys-search" ref={this._inputRef} type="text" value={this._searchTerm} placeholder="Column key" onKeyDown={this.onKeyDown}
onChange={e => this.onChange(e.target.value)} onFocus={this.onFocus} onBlur={this.onBlur}></input>
- <div className="keys-options-wrapper" onPointerEnter={this.onPointerEnter} onPointerOut={this.onPointerOut}>
+ <div className="keys-options-wrapper" onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerOut}>
{this.renderOptions()}
</div>
</div >
diff --git a/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx b/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx
index 153bbd410..670d6dbb2 100644
--- a/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx
+++ b/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx
@@ -3,9 +3,9 @@ import { ReactTableDefaults, TableCellRenderer, RowInfo } from "react-table";
import "./CollectionSchemaView.scss";
import { Transform } from "../../util/Transform";
import { Doc } from "../../../new_fields/Doc";
-import { DragManager, SetupDrag } from "../../util/DragManager";
+import { DragManager, SetupDrag, dropActionType } from "../../util/DragManager";
import { SelectionManager } from "../../util/SelectionManager";
-import { Cast, FieldValue } from "../../../new_fields/Types";
+import { Cast, FieldValue, StrCast } from "../../../new_fields/Types";
import { ContextMenu } from "../ContextMenu";
import { action } from "mobx";
import { library } from '@fortawesome/fontawesome-svg-core';
@@ -135,6 +135,7 @@ export interface MovableRowProps {
rowFocused: boolean;
textWrapRow: (doc: Doc) => void;
rowWrapped: boolean;
+ dropAction: string;
}
export class MovableRow extends React.Component<MovableRowProps> {
@@ -219,7 +220,7 @@ export class MovableRow extends React.Component<MovableRowProps> {
if (!doc) return <></>;
const reference = React.createRef<HTMLDivElement>();
- const onItemDown = SetupDrag(reference, () => doc, this.move);
+ const onItemDown = SetupDrag(reference, () => doc, this.move, StrCast(this.props.dropAction) as dropActionType);
let className = "collectionSchema-row";
if (this.props.rowFocused) className += " row-focused";
diff --git a/src/client/views/collections/CollectionSchemaView.scss b/src/client/views/collections/CollectionSchemaView.scss
index 8b3d332af..a24140b48 100644
--- a/src/client/views/collections/CollectionSchemaView.scss
+++ b/src/client/views/collections/CollectionSchemaView.scss
@@ -39,9 +39,9 @@
cursor: col-resize;
}
- .documentView-node:first-child {
- background: $light-color;
- }
+ // .documentView-node:first-child {
+ // background: $light-color;
+ // }
}
.ReactTable {
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index fa8be5177..981438513 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -14,7 +14,6 @@ import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField";
import { ComputedField } from "../../../new_fields/ScriptField";
import { Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types";
import { Docs, DocumentOptions } from "../../documents/Documents";
-import { DocumentType } from "../../documents/DocumentTypes";
import { Gateway } from "../../northstar/manager/Gateway";
import { CompileScript, Transformer, ts } from "../../util/Scripting";
import { Transform } from "../../util/Transform";
@@ -29,6 +28,7 @@ import "./CollectionSchemaView.scss";
import { CollectionSubView } from "./CollectionSubView";
import { CollectionView } from "./CollectionView";
import { ContentFittingDocumentView } from "../nodes/ContentFittingDocumentView";
+import { setupMoveUpEvents, emptyFunction } from "../../../Utils";
library.add(faCog, faPlus, faSortUp, faSortDown);
library.add(faTable);
@@ -44,20 +44,17 @@ export enum ColumnType {
// this map should be used for keys that should have a const type of value
const columnTypes: Map<string, ColumnType> = new Map([
["title", ColumnType.String],
- ["x", ColumnType.Number], ["y", ColumnType.Number], ["width", ColumnType.Number], ["height", ColumnType.Number],
- ["nativeWidth", ColumnType.Number], ["nativeHeight", ColumnType.Number], ["isPrototype", ColumnType.Boolean],
+ ["x", ColumnType.Number], ["y", ColumnType.Number], ["_width", ColumnType.Number], ["_height", ColumnType.Number],
+ ["_nativeWidth", ColumnType.Number], ["_nativeHeight", ColumnType.Number], ["isPrototype", ColumnType.Boolean],
["page", ColumnType.Number], ["curPage", ColumnType.Number], ["currentTimecode", ColumnType.Number], ["zIndex", ColumnType.Number]
]);
@observer
export class CollectionSchemaView extends CollectionSubView(doc => doc) {
- private _mainCont?: HTMLDivElement;
- private _startPreviewWidth = 0;
+ private _previewCont?: HTMLDivElement;
private DIVIDER_WIDTH = 4;
- @observable previewScript: string = "";
@observable previewDoc: Doc | undefined = undefined;
- @observable private _node: HTMLDivElement | null = null;
@observable private _focusedTable: Doc = this.props.Document;
@computed get previewWidth() { return () => NumCast(this.props.Document.schemaPreviewWidth); }
@@ -66,7 +63,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
@computed get borderWidth() { return Number(COLLECTION_BORDER_WIDTH); }
private createTarget = (ele: HTMLDivElement) => {
- this._mainCont = ele;
+ this._previewCont = ele;
super.CreateDropTarget(ele);
}
@@ -76,9 +73,6 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
@action setPreviewDoc = (doc: Doc) => this.previewDoc = doc;
- @undoBatch
- @action setPreviewScript = (script: string) => this.previewScript = script
-
//toggles preview side-panel of schema
@action
toggleExpander = () => {
@@ -86,28 +80,17 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
}
onDividerDown = (e: React.PointerEvent) => {
- this._startPreviewWidth = this.previewWidth();
- e.stopPropagation();
- e.preventDefault();
- document.addEventListener("pointermove", this.onDividerMove);
- document.addEventListener('pointerup', this.onDividerUp);
+ setupMoveUpEvents(this, e, this.onDividerMove, emptyFunction, action(() => this.toggleExpander()));
}
@action
- onDividerMove = (e: PointerEvent): void => {
- const nativeWidth = this._mainCont!.getBoundingClientRect();
+ onDividerMove = (e: PointerEvent, down: number[], delta: number[]) => {
+ const nativeWidth = this._previewCont!.getBoundingClientRect();
const minWidth = 40;
const maxWidth = 1000;
const movedWidth = this.props.ScreenToLocalTransform().transformDirection(nativeWidth.right - e.clientX, 0)[0];
const width = movedWidth < minWidth ? minWidth : movedWidth > maxWidth ? maxWidth : movedWidth;
this.props.Document.schemaPreviewWidth = width;
- }
- @action
- onDividerUp = (e: PointerEvent): void => {
- document.removeEventListener("pointermove", this.onDividerMove);
- document.removeEventListener('pointerup', this.onDividerUp);
- if (this._startPreviewWidth === this.previewWidth()) {
- this.toggleExpander();
- }
+ return false;
}
onPointerDown = (e: React.PointerEvent): void => {
@@ -120,9 +103,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
}
@computed
- get previewDocument(): Doc | undefined {
- return this.previewDoc ? (this.previewScript && this.previewScript !== "this" ? FieldValue(Cast(this.previewDoc[this.previewScript], Doc)) : this.previewDoc) : undefined;
- }
+ get previewDocument(): Doc | undefined { return this.previewDoc; }
getPreviewTransform = (): Transform => {
return this.props.ScreenToLocalTransform().translate(- this.borderWidth - this.DIVIDER_WIDTH - this.tableWidth, - this.borderWidth);
@@ -136,11 +117,10 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
@computed
get previewPanel() {
- const layoutDoc = this.previewDocument ? Doc.expandTemplateLayout(this.previewDocument, this.props.DataDoc) : undefined;
return <div ref={this.createTarget}>
<ContentFittingDocumentView
- Document={layoutDoc}
- DataDocument={this.previewDocument !== this.props.DataDoc ? this.props.DataDoc : undefined}
+ Document={this.previewDocument}
+ DataDocument={undefined}
LibraryPath={this.props.LibraryPath}
childDocs={this.childDocs}
renderDepth={this.props.renderDepth}
@@ -175,7 +155,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
moveDocument={this.props.moveDocument}
ScreenToLocalTransform={this.props.ScreenToLocalTransform}
active={this.props.active}
- onDrop={this.onDrop}
+ onDrop={this.onExternalDrop}
addDocTab={this.props.addDocTab}
pinToPres={this.props.pinToPres}
isSelected={this.props.isSelected}
@@ -199,7 +179,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
render() {
return <div className="collectionSchemaView-container">
- <div className="collectionSchemaView-tableContainer" onPointerDown={this.onPointerDown} onWheel={e => this.props.active(true) && e.stopPropagation()} onDrop={e => this.onDrop(e, {})} ref={this.createTarget}>
+ <div className="collectionSchemaView-tableContainer" onPointerDown={this.onPointerDown} onWheel={e => this.props.active(true) && e.stopPropagation()} onDrop={e => this.onExternalDrop(e, {})} ref={this.createTarget}>
{this.schemaTable}
</div>
{this.dividerDragger}
@@ -225,7 +205,7 @@ export interface SchemaTableProps {
ScreenToLocalTransform: () => Transform;
active: (outsideReaction: boolean) => boolean;
onDrop: (e: React.DragEvent<Element>, options: DocumentOptions, completed?: (() => void) | undefined) => void;
- addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean;
+ addDocTab: (document: Doc, where: string) => boolean;
pinToPres: (document: Doc) => void;
isSelected: (outsideReaction?: boolean) => boolean;
isFocused: (document: Doc) => boolean;
@@ -409,7 +389,8 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
rowInfo,
rowFocused: !this._headerIsEditing && rowInfo.index === this._focusedCell.row && this.props.isFocused(this.props.Document),
textWrapRow: this.toggleTextWrapRow,
- rowWrapped: this.textWrappedRows.findIndex(id => rowInfo.original[Id] === id) > -1
+ rowWrapped: this.textWrappedRows.findIndex(id => rowInfo.original[Id] === id) > -1,
+ dropAction: StrCast(this.props.Document.childDropAction)
};
}
@@ -477,8 +458,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
@undoBatch
createRow = () => {
- const newDoc = Docs.Create.TextDocument("", { title: "", _width: 100, _height: 30 });
- this.props.addDocument(newDoc);
+ this.props.addDocument(Docs.Create.TextDocument("", { title: "", _width: 100, _height: 30 }));
}
@undoBatch
@@ -559,16 +539,6 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
columns[index] = columnField;
this.columns = columns;
}
-
- // const typesDoc = FieldValue(Cast(this.props.Document.schemaColumnTypes, Doc));
- // if (!typesDoc) {
- // let newTypesDoc = new Doc();
- // newTypesDoc[key] = type;
- // this.props.Document.schemaColumnTypes = newTypesDoc;
- // return;
- // } else {
- // typesDoc[key] = type;
- // }
}
@undoBatch
@@ -692,7 +662,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
onContextMenu = (e: React.MouseEvent): void => {
if (!e.isPropagationStopped() && this.props.Document[Id] !== "mainDoc") { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
// ContextMenu.Instance.addItem({ description: "Make DB", event: this.makeDB, icon: "table" });
- ContextMenu.Instance.addItem({ description: "Toggle text wrapping", event: this.toggleTextwrap, icon: "table" })
+ ContextMenu.Instance.addItem({ description: "Toggle text wrapping", event: this.toggleTextwrap, icon: "table" });
}
}
diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss
index 843c743db..bfa5ea278 100644
--- a/src/client/views/collections/CollectionStackingView.scss
+++ b/src/client/views/collections/CollectionStackingView.scss
@@ -19,6 +19,7 @@
position: absolute;
top: 0;
overflow-y: auto;
+ overflow-x: hidden;
flex-wrap: wrap;
transition: top .5s;
>div {
@@ -159,9 +160,7 @@
}
.collectionStackingView-sectionHeader {
text-align: center;
- margin-left: 2px;
- margin-right: 2px;
- margin-top: 10px;
+ margin: auto;
background: $main-accent;
// overflow: hidden; overflow is visible so the color menu isn't hidden -ftong
@@ -213,6 +212,7 @@
left: 0;
top: 0;
height: 100%;
+ display: none;
[class*="css"] {
max-width: 102px;
@@ -250,6 +250,7 @@
right: 0;
top: 0;
height: 100%;
+ display: none;
[class*="css"] {
max-width: 102px;
@@ -284,6 +285,18 @@
right: 25px;
top: 0;
height: 100%;
+ display: none;
+ }
+ }
+ .collectionStackingView-sectionHeader:hover {
+ .collectionStackingView-sectionColor {
+ display:unset;
+ }
+ .collectionStackingView-sectionOptions {
+ display:unset;
+ }
+ .collectionStackingView-sectionDelete {
+ display:unset;
}
}
@@ -293,7 +306,6 @@
overflow: hidden;
margin: auto;
width: 90%;
- color: lightgrey;
overflow: ellipses;
.editableView-container-editing-oneLine,
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 91c7ca76e..076dd3629 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -9,61 +9,61 @@ import { Id } from "../../../new_fields/FieldSymbols";
import { List } from "../../../new_fields/List";
import { listSpec } from "../../../new_fields/Schema";
import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField";
-import { BoolCast, Cast, NumCast, StrCast, ScriptCast } from "../../../new_fields/Types";
-import { emptyFunction, Utils } from "../../../Utils";
-import { DocumentType } from "../../documents/DocumentTypes";
-import { DragManager } from "../../util/DragManager";
+import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from "../../../new_fields/Types";
+import { TraceMobx } from "../../../new_fields/util";
+import { Utils, setupMoveUpEvents, emptyFunction } from "../../../Utils";
+import { DragManager, dropActionType } from "../../util/DragManager";
import { Transform } from "../../util/Transform";
import { undoBatch } from "../../util/UndoManager";
+import { ContextMenu } from "../ContextMenu";
+import { ContextMenuProps } from "../ContextMenuItem";
import { EditableView } from "../EditableView";
import { ContentFittingDocumentView } from "../nodes/ContentFittingDocumentView";
+import { CollectionMasonryViewFieldRow } from "./CollectionMasonryViewFieldRow";
import "./CollectionStackingView.scss";
import { CollectionStackingViewFieldColumn } from "./CollectionStackingViewFieldColumn";
import { CollectionSubView } from "./CollectionSubView";
-import { ContextMenu } from "../ContextMenu";
-import { ContextMenuProps } from "../ContextMenuItem";
-import { CollectionMasonryViewFieldRow } from "./CollectionMasonryViewFieldRow";
-import { TraceMobx } from "../../../new_fields/util";
import { CollectionViewType } from "./CollectionView";
+import { Docs } from "../../documents/Documents";
@observer
export class CollectionStackingView extends CollectionSubView(doc => doc) {
_masonryGridRef: HTMLDivElement | null = null;
_draggerRef = React.createRef<HTMLDivElement>();
_heightDisposer?: IReactionDisposer;
- _sectionFilterDisposer?: IReactionDisposer;
+ _pivotFieldDisposer?: IReactionDisposer;
_docXfs: any[] = [];
_columnStart: number = 0;
@observable _heightMap = new Map<string, number>();
@observable _cursor: CursorProperty = "grab";
@observable _scroll = 0; // used to force the document decoration to update when scrolling
@computed get sectionHeaders() { return Cast(this.props.Document.sectionHeaders, listSpec(SchemaHeaderField)); }
- @computed get sectionFilter() { return StrCast(this.props.Document.sectionFilter); }
- @computed get filteredChildren() { return this.childLayoutPairs.filter(pair => pair.layout instanceof Doc && !pair.layout.isMinimized).map(pair => pair.layout); }
- @computed get xMargin() { return NumCast(this.props.Document._xMargin, 2 * this.gridGap); }
- @computed get yMargin() { return Math.max(this.props.Document.showTitle && !this.props.Document.showTitleHover ? 30 : 0, NumCast(this.props.Document._yMargin, 2 * this.gridGap)); }
+ @computed get pivotField() { return StrCast(this.props.Document._pivotField); }
+ @computed get filteredChildren() { return this.childLayoutPairs.filter(pair => pair.layout instanceof Doc).map(pair => pair.layout); }
+ @computed get xMargin() { return NumCast(this.props.Document._xMargin, 2 * Math.min(this.gridGap, .05 * this.props.PanelWidth())); }
+ @computed get yMargin() { return Math.max(this.props.Document._showTitle && !this.props.Document._showTitleHover ? 30 : 0, NumCast(this.props.Document._yMargin, 0)); } // 2 * this.gridGap)); }
@computed get gridGap() { return NumCast(this.props.Document._gridGap, 10); }
@computed get isStackingView() { return BoolCast(this.props.Document.singleColumn, true); }
@computed get numGroupColumns() { return this.isStackingView ? Math.max(1, this.Sections.size + (this.showAddAGroup ? 1 : 0)) : 1; }
- @computed get showAddAGroup() { return (this.sectionFilter && (this.props.Document._chromeStatus !== 'view-mode' && this.props.Document._chromeStatus !== 'disabled')); }
+ @computed get showAddAGroup() { return (this.pivotField && (this.props.Document._chromeStatus !== 'view-mode' && this.props.Document._chromeStatus !== 'disabled')); }
@computed get columnWidth() {
return Math.min(this.props.PanelWidth() / (this.props as any).ContentScaling() - 2 * this.xMargin,
this.isStackingView ? Number.MAX_VALUE : NumCast(this.props.Document.columnWidth, 250));
}
@computed get NodeWidth() { return this.props.PanelWidth() - this.gridGap; }
- children(docs: Doc[]) {
+ children(docs: Doc[], columns?: number) {
this._docXfs.length = 0;
return docs.map((d, i) => {
- const width = () => Math.min(d._nativeWidth && !d.ignoreAspect && !this.props.Document.fillColumn ? d[WidthSym]() : Number.MAX_VALUE, this.columnWidth / this.numGroupColumns);
const height = () => this.getDocHeight(d);
+ const width = () => this.getDocWidth(d);
const dref = React.createRef<HTMLDivElement>();
const dxf = () => this.getDocTransform(d, dref.current!);
this._docXfs.push({ dxf: dxf, width: width, height: height });
const rowSpan = Math.ceil((height() + this.gridGap) / this.gridGap);
- const style = this.isStackingView ? { width: width(), marginTop: i === 0 ? 0 : this.gridGap, height: height() } : { gridRowEnd: `span ${rowSpan}` };
+ const style = this.isStackingView ? { width: width(), marginTop: this.gridGap, height: height() } : { gridRowEnd: `span ${rowSpan}` };
return <div className={`collectionStackingView-${this.isStackingView ? "columnDoc" : "masonryDoc"}`} key={d[Id]} ref={dref} style={style} >
- {this.getDisplayDoc(d, Cast(d.resolvedDataDoc, Doc, null) || this.props.DataDoc, dxf, width)}
+ {this.getDisplayDoc(d, (!d.isTemplateDoc && !d.isTemplateForField && !d.PARAMS) ? undefined : this.props.DataDoc, dxf, width)}
</div>;
});
}
@@ -73,31 +73,42 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
}
get Sections() {
- if (!this.sectionFilter || this.sectionHeaders instanceof Promise) return new Map<SchemaHeaderField, Doc[]>();
+ if (!this.pivotField || this.sectionHeaders instanceof Promise) return new Map<SchemaHeaderField, Doc[]>();
if (this.sectionHeaders === undefined) {
setTimeout(() => this.props.Document.sectionHeaders = new List<SchemaHeaderField>(), 0);
return new Map<SchemaHeaderField, Doc[]>();
}
- const sectionHeaders = this.sectionHeaders;
+ const sectionHeaders: SchemaHeaderField[] = Array.from(this.sectionHeaders);
const fields = new Map<SchemaHeaderField, Doc[]>(sectionHeaders.map(sh => [sh, []] as [SchemaHeaderField, []]));
+ let changed = false;
this.filteredChildren.map(d => {
- const sectionValue = (d[this.sectionFilter] ? d[this.sectionFilter] : `NO ${this.sectionFilter.toUpperCase()} VALUE`) as object;
+ const sectionValue = (d[this.pivotField] ? d[this.pivotField] : `NO ${this.pivotField.toUpperCase()} VALUE`) as object;
// the next five lines ensures that floating point rounding errors don't create more than one section -syip
const parsed = parseInt(sectionValue.toString());
const castedSectionValue = !isNaN(parsed) ? parsed : sectionValue;
// look for if header exists already
- const existingHeader = sectionHeaders.find(sh => sh.heading === (castedSectionValue ? castedSectionValue.toString() : `NO ${this.sectionFilter.toUpperCase()} VALUE`));
+ const existingHeader = sectionHeaders.find(sh => sh.heading === (castedSectionValue ? castedSectionValue.toString() : `NO ${this.pivotField.toUpperCase()} VALUE`));
if (existingHeader) {
fields.get(existingHeader)!.push(d);
}
else {
- const newSchemaHeader = new SchemaHeaderField(castedSectionValue ? castedSectionValue.toString() : `NO ${this.sectionFilter.toUpperCase()} VALUE`);
+ const newSchemaHeader = new SchemaHeaderField(castedSectionValue ? castedSectionValue.toString() : `NO ${this.pivotField.toUpperCase()} VALUE`);
fields.set(newSchemaHeader, [d]);
sectionHeaders.push(newSchemaHeader);
+ changed = true;
}
});
+ // remove all empty columns if hideHeadings is set
+ if (this.props.Document.hideHeadings) {
+ Array.from(fields.keys()).filter(key => !fields.get(key)!.length).map(header => {
+ fields.delete(header);
+ sectionHeaders.splice(sectionHeaders.indexOf(header), 1);
+ changed = true;
+ });
+ }
+ changed && setTimeout(action(() => { if (this.sectionHeaders) { this.sectionHeaders.length = 0; this.sectionHeaders.push(...sectionHeaders); } }), 0);
return fields;
}
@@ -131,15 +142,15 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
);
// reset section headers when a new filter is inputted
- this._sectionFilterDisposer = reaction(
- () => this.sectionFilter,
+ this._pivotFieldDisposer = reaction(
+ () => this.pivotField,
() => this.props.Document.sectionHeaders = new List()
);
}
componentWillUnmount() {
super.componentWillUnmount();
- this._heightDisposer && this._heightDisposer();
- this._sectionFilterDisposer && this._sectionFilterDisposer();
+ this._heightDisposer?.();
+ this._pivotFieldDisposer?.();
}
@action
@@ -155,14 +166,17 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
@computed get onClickHandler() { return ScriptCast(this.Document.onChildClick); }
getDisplayDoc(doc: Doc, dataDoc: Doc | undefined, dxf: () => Transform, width: () => number) {
- const layoutDoc = Doc.Layout(doc);
+ const layoutDoc = Doc.Layout(doc, this.props.childLayoutTemplate?.());
const height = () => this.getDocHeight(doc);
return <ContentFittingDocumentView
Document={doc}
- DataDocument={dataDoc}
+ DataDocument={dataDoc || (doc[DataSym] !== doc && doc[DataSym])}
+ backgroundColor={this.props.backgroundColor}
+ LayoutDoc={this.props.childLayoutTemplate}
LibraryPath={this.props.LibraryPath}
renderDepth={this.props.renderDepth + 1}
fitToBox={this.props.fitToBox}
+ dropAction={StrCast(this.props.Document.childDropAction) as dropActionType}
onClick={layoutDoc.isTemplateDoc ? this.onClickHandler : this.onChildClickHandler}
PanelWidth={width}
PanelHeight={height}
@@ -179,42 +193,36 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
pinToPres={this.props.pinToPres}>
</ContentFittingDocumentView>;
}
+
+ getDocWidth(d?: Doc) {
+ if (!d) return 0;
+ const layoutDoc = Doc.Layout(d, this.props.childLayoutTemplate?.());
+ const nw = NumCast(layoutDoc._nativeWidth);
+ return Math.min(nw && !this.props.Document.fillColumn ? d[WidthSym]() : Number.MAX_VALUE, this.columnWidth / this.numGroupColumns);
+ }
getDocHeight(d?: Doc) {
if (!d) return 0;
- const layoutDoc = Doc.Layout(d);
+ const layoutDoc = Doc.Layout(d, this.props.childLayoutTemplate?.());
const nw = NumCast(layoutDoc._nativeWidth);
const nh = NumCast(layoutDoc._nativeHeight);
let wid = this.columnWidth / (this.isStackingView ? this.numGroupColumns : 1);
- if (!layoutDoc.ignoreAspect && !layoutDoc._fitWidth && nw && nh) {
+ if (!layoutDoc._fitWidth && nw && nh) {
const aspect = nw && nh ? nh / nw : 1;
- if (!(d._nativeWidth && !layoutDoc.ignoreAspect && this.props.Document.fillColumn)) wid = Math.min(layoutDoc[WidthSym](), wid);
+ if (!(this.props.Document.fillColumn)) wid = Math.min(layoutDoc[WidthSym](), wid);
return wid * aspect;
}
- return layoutDoc._fitWidth ? !layoutDoc._nativeHeight ? this.props.PanelHeight() - 2 * this.yMargin :
- Math.min(wid * NumCast(layoutDoc.scrollHeight, NumCast(layoutDoc._nativeHeight)) / NumCast(layoutDoc._nativeWidth, 1), this.props.PanelHeight() - 2 * this.yMargin) : layoutDoc[HeightSym]();
+ return layoutDoc._fitWidth ? !nh ? this.props.PanelHeight() - 2 * this.yMargin :
+ Math.min(wid * NumCast(layoutDoc.scrollHeight, nh) / (nw || 1), this.props.PanelHeight() - 2 * this.yMargin) : layoutDoc[HeightSym]();
}
columnDividerDown = (e: React.PointerEvent) => {
- e.stopPropagation();
- e.preventDefault();
runInAction(() => this._cursor = "grabbing");
- document.addEventListener("pointermove", this.onDividerMove);
- document.addEventListener('pointerup', this.onDividerUp);
- this._columnStart = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY)[0];
- }
- @action
- onDividerMove = (e: PointerEvent): void => {
- const dragPos = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY)[0];
- const delta = dragPos - this._columnStart;
- this._columnStart = dragPos;
- this.layoutDoc.columnWidth = Math.max(10, this.columnWidth + delta);
+ setupMoveUpEvents(this, e, this.onDividerMove, action(() => this._cursor = "grab"), emptyFunction);
}
-
@action
- onDividerUp = (e: PointerEvent): void => {
- runInAction(() => this._cursor = "grab");
- document.removeEventListener("pointermove", this.onDividerMove);
- document.removeEventListener('pointerup', this.onDividerUp);
+ onDividerMove = (e: PointerEvent, down: number[], delta: number[]) => {
+ this.layoutDoc.columnWidth = Math.max(10, this.columnWidth + delta[0]);
+ return false;
}
@computed get columnDragger() {
@@ -226,7 +234,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
@undoBatch
@action
- drop = (e: Event, de: DragManager.DropEvent) => {
+ onInternalDrop = (e: Event, de: DragManager.DropEvent) => {
const where = [de.x, de.y];
let targInd = -1;
let plusOne = 0;
@@ -240,7 +248,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
plusOne = where[axis] > (pos[axis] + pos1[axis]) / 2 ? 1 : 0;
}
});
- if (super.drop(e, de)) {
+ if (super.onInternalDrop(e, de)) {
const newDoc = de.complete.docDragData.droppedDocuments[0];
const docs = this.childDocList;
if (docs) {
@@ -256,7 +264,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
}
@undoBatch
@action
- onDrop = async (e: React.DragEvent): Promise<void> => {
+ onExternalDrop = async (e: React.DragEvent): Promise<void> => {
const where = [e.clientX, e.clientY];
let targInd = -1;
this._docXfs.map((cd, i) => {
@@ -266,7 +274,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
targInd = i;
}
});
- super.onDrop(e, {}, () => {
+ super.onExternalDrop(e, {}, () => {
if (targInd !== -1) {
const newDoc = this.childDocs[this.childDocs.length - 1];
const docs = this.childDocList;
@@ -277,9 +285,9 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
}
});
}
- headings = () => Array.from(this.Sections.keys());
+ headings = () => Array.from(this.Sections);
sectionStacking = (heading: SchemaHeaderField | undefined, docList: Doc[]) => {
- const key = this.sectionFilter;
+ const key = this.pivotField;
let type: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" | undefined = undefined;
const types = docList.length ? docList.map(d => typeof d[key]) : this.filteredChildren.map(d => typeof d[key]);
if (types.map((i, idx) => types.indexOf(i) === idx).length === 1) {
@@ -314,7 +322,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
}
sectionMasonry = (heading: SchemaHeaderField | undefined, docList: Doc[]) => {
- const key = this.sectionFilter;
+ const key = this.pivotField;
let type: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" | undefined = undefined;
const types = docList.length ? docList.map(d => typeof d[key]) : this.filteredChildren.map(d => typeof d[key]);
if (types.map((i, idx) => types.indexOf(i) === idx).length === 1) {
@@ -340,7 +348,9 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
@action
addGroup = (value: string) => {
if (value && this.sectionHeaders) {
- this.sectionHeaders.push(new SchemaHeaderField(value));
+ const schemaHdrField = new SchemaHeaderField(value);
+ this.sectionHeaders.push(schemaHdrField);
+ Doc.addFieldEnumerations(undefined, this.pivotField, [{ title: value, _backgroundColor: schemaHdrField.color }]);
return true;
}
return false;
@@ -362,8 +372,6 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
if (!e.isPropagationStopped()) {
const subItems: ContextMenuProps[] = [];
subItems.push({ description: `${this.props.Document.fillColumn ? "Variable Size" : "Autosize"} Column`, event: () => this.props.Document.fillColumn = !this.props.Document.fillColumn, icon: "plus" });
- subItems.push({ description: `${this.props.Document.showTitles ? "Hide Titles" : "Show Titles"}`, event: () => this.props.Document.showTitles = !this.props.Document.showTitles ? "title" : "", icon: "plus" });
- subItems.push({ description: `${this.props.Document.showCaptions ? "Hide Captions" : "Show Captions"}`, event: () => this.props.Document.showCaptions = !this.props.Document.showCaptions ? "caption" : "", icon: "plus" });
ContextMenu.Instance.addItem({ description: "Stacking Options ...", subitems: subItems, icon: "eye" });
}
}
@@ -371,12 +379,15 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
@computed get renderedSections() {
TraceMobx();
let sections = [[undefined, this.filteredChildren] as [SchemaHeaderField | undefined, Doc[]]];
- if (this.sectionFilter) {
+ if (this.pivotField) {
const entries = Array.from(this.Sections.entries());
sections = entries.sort(this.sortFunc);
}
return sections.map(section => this.isStackingView ? this.sectionStacking(section[0], section[1]) : this.sectionMasonry(section[0], section[1]));
}
+
+ @computed get scaling() { return !this.props.Document._nativeWidth ? 1 : this.props.PanelHeight() / NumCast(this.props.Document._nativeHeight); }
+
render() {
TraceMobx();
const editableViewProps = {
@@ -390,12 +401,13 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
ref={this.createRef}
style={{
overflowY: this.props.active() ? "auto" : "hidden",
- transform: `scale(${Math.min(1, this.props.PanelHeight() / this.layoutDoc[HeightSym]())})`,
- height: `${Math.max(100, 100 * 1 / Math.min(this.props.PanelWidth() / this.layoutDoc[WidthSym](), this.props.PanelHeight() / this.layoutDoc[HeightSym]()))}%`,
- transformOrigin: "top"
+ transform: `scale(${this.scaling}`,
+ height: `${1 / this.scaling * 100}%`,
+ width: `${1 / this.scaling * 100}%`,
+ transformOrigin: "top left",
}}
onScroll={action((e: React.UIEvent<HTMLDivElement>) => this._scroll = e.currentTarget.scrollTop)}
- onDrop={this.onDrop.bind(this)}
+ onDrop={this.onExternalDrop.bind(this)}
onContextMenu={this.onContextMenu}
onWheel={e => this.props.active() && e.stopPropagation()} >
{this.renderedSections}
@@ -404,7 +416,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
style={{ width: !this.isStackingView ? "100%" : this.columnWidth / this.numGroupColumns - 10, marginTop: 10 }}>
<EditableView {...editableViewProps} />
</div>}
- {this.props.Document._chromeStatus !== 'disabled' ? <Switch
+ {this.props.Document._chromeStatus !== 'disabled' && this.props.isSelected() ? <Switch
onChange={this.onToggle}
onClick={this.onToggle}
defaultChecked={this.props.Document._chromeStatus !== 'view-mode'}
diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
index 2a9f903bb..0a48c95e4 100644
--- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
+++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
@@ -5,25 +5,27 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { action, observable, runInAction } from "mobx";
import { observer } from "mobx-react";
import { Doc, DocListCast } from "../../../new_fields/Doc";
+import { RichTextField } from "../../../new_fields/RichTextField";
import { PastelSchemaPalette, SchemaHeaderField } from "../../../new_fields/SchemaHeaderField";
import { ScriptField } from "../../../new_fields/ScriptField";
-import { NumCast, StrCast } from "../../../new_fields/Types";
-import { Docs } from "../../documents/Documents";
+import { NumCast, StrCast, Cast } from "../../../new_fields/Types";
+import { ImageField } from "../../../new_fields/URLField";
+import { TraceMobx } from "../../../new_fields/util";
+import { Docs, DocUtils } from "../../documents/Documents";
import { DragManager } from "../../util/DragManager";
import { SelectionManager } from "../../util/SelectionManager";
import { Transform } from "../../util/Transform";
import { undoBatch } from "../../util/UndoManager";
-import { anchorPoints, Flyout } from "../DocumentDecorations";
+import { ContextMenu } from "../ContextMenu";
+import { ContextMenuProps } from "../ContextMenuItem";
import { EditableView } from "../EditableView";
import { CollectionStackingView } from "./CollectionStackingView";
+import { setupMoveUpEvents, emptyFunction } from "../../../Utils";
import "./CollectionStackingView.scss";
-import { TraceMobx } from "../../../new_fields/util";
-import { FormattedTextBox } from "../nodes/FormattedTextBox";
-import { ImageField } from "../../../new_fields/URLField";
-import { ImageBox } from "../nodes/ImageBox";
-import { ContextMenu } from "../ContextMenu";
-import { ContextMenuProps } from "../ContextMenuItem";
-import { RichTextField } from "../../../new_fields/RichTextField";
+import { listSpec } from "../../../new_fields/Schema";
+const higflyout = require("@hig/flyout");
+export const { anchorPoints } = higflyout;
+export const Flyout = higflyout.default;
library.add(faPalette);
@@ -42,20 +44,15 @@ interface CSVFieldColumnProps {
@observer
export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldColumnProps> {
@observable private _background = "inherit";
- @observable private _createAliasSelected: boolean = false;
- private _dropRef: HTMLDivElement | null = null;
private dropDisposer?: DragManager.DragDropDisposer;
private _headerRef: React.RefObject<HTMLDivElement> = React.createRef();
- private _startDragPosition: { x: number, y: number } = { x: 0, y: 0 };
- private _sensitivity: number = 16;
@observable _heading = this.props.headingObject ? this.props.headingObject.heading : this.props.heading;
@observable _color = this.props.headingObject ? this.props.headingObject.color : "#f1efeb";
createColumnDropRef = (ele: HTMLDivElement | null) => {
- this._dropRef = ele;
- this.dropDisposer && this.dropDisposer();
+ this.dropDisposer?.();
if (ele) {
this.dropDisposer = DragManager.MakeDropTarget(ele, this.columnDrop.bind(this));
}
@@ -63,17 +60,11 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
@undoBatch
columnDrop = action((e: Event, de: DragManager.DropEvent) => {
- this._createAliasSelected = false;
if (de.complete.docDragData) {
- const key = StrCast(this.props.parent.props.Document.sectionFilter);
+ const key = StrCast(this.props.parent.props.Document._pivotField);
const castedValue = this.getValue(this._heading);
- if (castedValue) {
- de.complete.docDragData.droppedDocuments.forEach(d => d[key] = castedValue);
- }
- else {
- de.complete.docDragData.droppedDocuments.forEach(d => d[key] = undefined);
- }
- this.props.parent.drop(e, de);
+ de.complete.docDragData.droppedDocuments.forEach(d => Doc.SetInPlace(d, key, castedValue, false));
+ this.props.parent.onInternalDrop(e, de);
e.stopPropagation();
}
});
@@ -93,8 +84,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
@action
headingChanged = (value: string, shiftDown?: boolean) => {
- this._createAliasSelected = false;
- const key = StrCast(this.props.parent.props.Document.sectionFilter);
+ const key = StrCast(this.props.parent.props.Document._pivotField);
const castedValue = this.getValue(value);
if (castedValue) {
if (this.props.parent.sectionHeaders) {
@@ -114,7 +104,6 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
@action
changeColumnColor = (color: string) => {
- this._createAliasSelected = false;
if (this.props.headingObject) {
this.props.headingObject.setColor(color);
this._color = color;
@@ -124,35 +113,31 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
@action
pointerEntered = () => {
if (SelectionManager.GetIsDragging()) {
- this._createAliasSelected = false;
this._background = "#b4b4b4";
}
}
@action
pointerLeave = () => {
- this._createAliasSelected = false;
this._background = "inherit";
- document.removeEventListener("pointermove", this.startDrag);
}
@action
addDocument = (value: string, shiftDown?: boolean) => {
if (!value) return false;
- this._createAliasSelected = false;
- const key = StrCast(this.props.parent.props.Document.sectionFilter);
+ const key = StrCast(this.props.parent.props.Document._pivotField);
const newDoc = Docs.Create.TextDocument(value, { _height: 18, _width: 200, title: value, _autoHeight: true });
newDoc[key] = this.getValue(this.props.heading);
const maxHeading = this.props.docList.reduce((maxHeading, doc) => NumCast(doc.heading) > maxHeading ? NumCast(doc.heading) : maxHeading, 0);
const heading = maxHeading === 0 || this.props.docList.length === 0 ? 1 : maxHeading === 1 ? 2 : 3;
newDoc.heading = heading;
- return this.props.parent.props.addDocument(newDoc);
+ this.props.parent.props.addDocument(newDoc);
+ return false;
}
@action
deleteColumn = () => {
- this._createAliasSelected = false;
- const key = StrCast(this.props.parent.props.Document.sectionFilter);
+ const key = StrCast(this.props.parent.props.Document._pivotField);
this.props.docList.forEach(d => d[key] = undefined);
if (this.props.parent.sectionHeaders && this.props.headingObject) {
const index = this.props.parent.sectionHeaders.indexOf(this.props.headingObject);
@@ -162,7 +147,6 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
@action
collapseSection = () => {
- this._createAliasSelected = false;
if (this.props.headingObject) {
this._headingsHack++;
this.props.headingObject.setCollapsed(!this.props.headingObject.collapsed);
@@ -170,46 +154,23 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
}
}
- startDrag = (e: PointerEvent) => {
- const [dx, dy] = this.props.screenToLocalTransform().transformDirection(e.clientX - this._startDragPosition.x, e.clientY - this._startDragPosition.y);
- if (Math.abs(dx) + Math.abs(dy) > this._sensitivity) {
- const alias = Doc.MakeAlias(this.props.parent.props.Document);
- const key = StrCast(this.props.parent.props.Document.sectionFilter);
- let value = this.getValue(this._heading);
- value = typeof value === "string" ? `"${value}"` : value;
- alias.viewSpecScript = ScriptField.MakeFunction(`doc.${key} === ${value}`, { doc: Doc.name });
- if (alias.viewSpecScript) {
- DragManager.StartDocumentDrag([this._headerRef.current!], new DragManager.DocumentDragData([alias]), e.clientX, e.clientY);
- }
-
- e.stopPropagation();
- document.removeEventListener("pointermove", this.startDrag);
- document.removeEventListener("pointerup", this.pointerUp);
- }
- }
-
- pointerUp = (e: PointerEvent) => {
- e.stopPropagation();
- e.preventDefault();
-
- document.removeEventListener("pointermove", this.startDrag);
- document.removeEventListener("pointerup", this.pointerUp);
- }
-
headerDown = (e: React.PointerEvent<HTMLDivElement>) => {
- e.stopPropagation();
- e.preventDefault();
-
- const [dx, dy] = this.props.screenToLocalTransform().transformDirection(e.clientX, e.clientY);
- this._startDragPosition = { x: dx, y: dy };
+ setupMoveUpEvents(this, e, this.startDrag, emptyFunction, emptyFunction);
+ }
- if (this._createAliasSelected) {
- document.removeEventListener("pointermove", this.startDrag);
- document.addEventListener("pointermove", this.startDrag);
- document.removeEventListener("pointerup", this.pointerUp);
- document.addEventListener("pointerup", this.pointerUp);
+ startDrag = (e: PointerEvent, down: number[], delta: number[]) => {
+ const alias = Doc.MakeAlias(this.props.parent.props.Document);
+ alias._width = this.props.parent.props.PanelWidth() / (Cast(this.props.parent.props.Document.sectionHeaders, listSpec(SchemaHeaderField))?.length || 1);
+ alias._pivotField = undefined;
+ const key = StrCast(this.props.parent.props.Document._pivotField);
+ let value = this.getValue(this._heading);
+ value = typeof value === "string" ? `"${value}"` : value;
+ alias.viewSpecScript = ScriptField.MakeFunction(`doc.${key} === ${value}`, { doc: Doc.name });
+ if (alias.viewSpecScript) {
+ DragManager.StartDocumentDrag([this._headerRef.current!], new DragManager.DocumentDragData([alias]), e.clientX, e.clientY);
+ return true;
}
- runInAction(() => this._createAliasSelected = false);
+ return false;
}
renderColorPicker = () => {
@@ -242,17 +203,11 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
);
}
- @action
- toggleAlias = () => {
- this._createAliasSelected = true;
- }
-
renderMenu = () => {
- const selected = this._createAliasSelected;
return (
<div className="collectionStackingView-optionPicker">
<div className="optionOptions">
- <div className={"optionPicker" + (selected === true ? " active" : "")} onClick={this.toggleAlias}>Create Alias</div>
+ <div className={"optionPicker" + (true ? " active" : "")} onClick={action(() => { })}>Add options here</div>
</div>
</div >
);
@@ -268,8 +223,10 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
ContextMenu.Instance.clearItems();
const layoutItems: ContextMenuProps[] = [];
const docItems: ContextMenuProps[] = [];
-
const dataDoc = this.props.parent.props.DataDoc || this.props.parent.Document;
+
+ DocUtils.addDocumentCreatorMenuItems(this.props.parent.props.addDocument, this.props.parent.props.addDocument, x, y);
+
Array.from(Object.keys(Doc.GetProto(dataDoc))).filter(fieldKey => dataDoc[fieldKey] instanceof RichTextField || dataDoc[fieldKey] instanceof ImageField || typeof (dataDoc[fieldKey]) === "string").map(fieldKey =>
docItems.push({
description: ":" + fieldKey, event: () => {
@@ -287,8 +244,10 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
description: ":" + fieldKey, event: () => {
const created = Docs.Create.CarouselDocument([], { _width: 400, _height: 200, title: fieldKey });
if (created) {
- if (this.props.parent.Document.isTemplateDoc) {
- Doc.MakeMetadataFieldTemplate(created, this.props.parent.props.Document);
+ const container = this.props.parent.Document.resolvedDataDoc ? Doc.GetProto(this.props.parent.Document) : this.props.parent.Document;
+ if (container.isTemplateDoc) {
+ Doc.MakeMetadataFieldTemplate(created, container);
+ return Doc.AddDocToList(container, Doc.LayoutFieldKey(container), created);
}
return this.props.parent.props.addDocument(created);
}
@@ -312,18 +271,19 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
}
});
const pt = this.props.screenToLocalTransform().inverse().transformPoint(x, y);
- ContextMenu.Instance.displayMenu(pt[0], pt[1]);
+ ContextMenu.Instance.displayMenu(x, y);
}
render() {
TraceMobx();
const cols = this.props.cols();
- const key = StrCast(this.props.parent.props.Document.sectionFilter);
+ const key = StrCast(this.props.parent.props.Document._pivotField);
let templatecols = "";
const headings = this.props.headings();
const heading = this._heading;
const style = this.props.parent;
const singleColumn = style.isStackingView;
+ const columnYMargin = this.props.headingObject ? 0 : NumCast(this.props.parent.props.Document._yMargin);
const uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx);
const evContents = heading ? heading : this.props.type && this.props.type === "number" ? "0" : `NO ${key.toUpperCase()} VALUE`;
const headerEditableViewProps = {
@@ -348,6 +308,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
const headingView = this.props.headingObject ?
<div key={heading} className="collectionStackingView-sectionHeader" ref={this._headerRef}
style={{
+ marginTop: NumCast(this.props.parent.props.Document._yMargin),
width: (style.columnWidth) /
((uniqueHeadings.length +
((this.props.parent.props.Document._chromeStatus !== 'view-mode' && this.props.parent.props.Document._chromeStatus !== 'disabled') ? 1 : 0)) || 1)
@@ -360,7 +321,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
`Documents that don't have a ${key} value will go here. This column cannot be removed.` : ""}
style={{
width: "100%",
- background: evContents !== `NO ${key.toUpperCase()} VALUE` ? this._color : "lightgrey",
+ background: evContents !== `NO ${key.toUpperCase()} VALUE` ? this._color : "inherit",
color: "grey"
}}>
<EditableView {...headerEditableViewProps} />
@@ -400,7 +361,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
<div>
<div key={`${heading}-stack`} className={`collectionStackingView-masonry${singleColumn ? "Single" : "Grid"}`}
style={{
- padding: singleColumn ? `${style.yMargin}px ${0}px ${style.yMargin}px ${0}px` : `${style.yMargin}px ${0}px`,
+ padding: singleColumn ? `${columnYMargin}px ${0}px ${style.yMargin}px ${0}px` : `${columnYMargin}px ${0}px`,
margin: "auto",
width: "max-content", //singleColumn ? undefined : `${cols * (style.columnWidth + style.gridGap) + 2 * style.xMargin - style.gridGap}px`,
height: 'max-content',
@@ -409,7 +370,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
gridTemplateColumns: singleColumn ? undefined : templatecols,
gridAutoRows: singleColumn ? undefined : "0px"
}}>
- {this.props.parent.children(this.props.docList)}
+ {this.props.parent.children(this.props.docList, uniqueHeadings.length)}
{singleColumn ? (null) : this.props.parent.columnDragger}
</div>
{(chromeStatus !== 'view-mode' && chromeStatus !== 'disabled') ?
diff --git a/src/client/views/collections/CollectionStaffView.tsx b/src/client/views/collections/CollectionStaffView.tsx
index 8c7e113b2..5b9a69bf7 100644
--- a/src/client/views/collections/CollectionStaffView.tsx
+++ b/src/client/views/collections/CollectionStaffView.tsx
@@ -1,22 +1,20 @@
import { CollectionSubView } from "./CollectionSubView";
-import { Transform } from "../../util/Transform";
import React = require("react");
import { computed, action, IReactionDisposer, reaction, runInAction, observable } from "mobx";
-import { Doc } from "../../../new_fields/Doc";
import { NumCast } from "../../../new_fields/Types";
import "./CollectionStaffView.scss";
import { observer } from "mobx-react";
@observer
export class CollectionStaffView extends CollectionSubView(doc => doc) {
- private getTransform = (): Transform => this.props.ScreenToLocalTransform().translate(0, -this._mainCont.current!.scrollTop);
- private _mainCont = React.createRef<HTMLDivElement>();
private _reactionDisposer: IReactionDisposer | undefined;
@observable private _staves = NumCast(this.props.Document.staves);
+ componentWillUnmount() {
+ this._reactionDisposer?.();
+ }
componentDidMount = () => {
- this._reactionDisposer = reaction(
- () => NumCast(this.props.Document.staves),
+ this._reactionDisposer = reaction(() => NumCast(this.props.Document.staves),
(staves) => runInAction(() => this._staves = staves)
);
@@ -47,7 +45,7 @@ export class CollectionStaffView extends CollectionSubView(doc => doc) {
}
render() {
- return <div className="collectionStaffView" ref={this._mainCont}>
+ return <div className="collectionStaffView">
{this.staves}
{this.addStaffButton}
</div>;
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 8679c8bd1..70927cf22 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -1,12 +1,11 @@
-import { action, computed, IReactionDisposer, reaction, trace } from "mobx";
-import * as rp from 'request-promise';
+import { action, computed, IReactionDisposer, reaction } from "mobx";
import CursorField from "../../../new_fields/CursorField";
-import { Doc, DocListCast, Opt } from "../../../new_fields/Doc";
+import { Doc, DocListCast, Opt, WidthSym, HeightSym } from "../../../new_fields/Doc";
import { Id } from "../../../new_fields/FieldSymbols";
import { List } from "../../../new_fields/List";
import { listSpec } from "../../../new_fields/Schema";
import { ScriptField } from "../../../new_fields/ScriptField";
-import { Cast } from "../../../new_fields/Types";
+import { Cast, StrCast } from "../../../new_fields/Types";
import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
import { Utils } from "../../../Utils";
import { DocServer } from "../../DocServer";
@@ -25,6 +24,7 @@ import { ImageUtils } from "../../util/Import & Export/ImageUtils";
import { Networking } from "../../Network";
import { GestureUtils } from "../../../pen-gestures/GestureUtils";
import { InteractionUtils } from "../../util/InteractionUtils";
+import { Upload } from "../../../server/SharedMediaTypes";
export interface CollectionViewProps extends FieldViewProps {
addDocument: (document: Doc) => boolean;
@@ -33,7 +33,6 @@ export interface CollectionViewProps extends FieldViewProps {
PanelWidth: () => number;
PanelHeight: () => number;
VisibleHeight?: () => number;
- chromeCollapsed: boolean;
setPreviewCursor?: (func: (x: number, y: number, drag: boolean) => void) => void;
fieldKey: string;
}
@@ -41,22 +40,27 @@ export interface CollectionViewProps extends FieldViewProps {
export interface SubCollectionViewProps extends CollectionViewProps {
CollectionView: Opt<CollectionView>;
children?: never | (() => JSX.Element[]) | React.ReactNode;
+ overrideDocuments?: Doc[]; // used to override the documents shown by the sub collection to an explict list (see LinkBox)
+ ignoreFields?: string[]; // used in TreeView to ignore specified fields (see LinkBox)
isAnnotationOverlay?: boolean;
annotationsKey: string;
+ layoutEngine?: () => string;
}
-export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
- class CollectionSubView extends DocComponent<SubCollectionViewProps, T>(schemaCtor) {
+export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?: X) {
+ class CollectionSubView extends DocComponent<X & SubCollectionViewProps, T>(schemaCtor) {
private dropDisposer?: DragManager.DragDropDisposer;
private gestureDisposer?: GestureUtils.GestureEventDisposer;
protected multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
private _childLayoutDisposer?: IReactionDisposer;
+ protected _mainCont?: HTMLDivElement;
protected createDashEventsTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view
this.dropDisposer?.();
this.gestureDisposer?.();
this.multiTouchDisposer?.();
if (ele) {
- this.dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this));
+ this._mainCont = ele;
+ this.dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this));
this.gestureDisposer = GestureUtils.MakeGestureTarget(ele, this.onGesture.bind(this));
this.multiTouchDisposer = InteractionUtils.MakeMultiTouchTarget(ele, this.onTouchStart.bind(this));
}
@@ -66,75 +70,98 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
}
componentDidMount() {
- this._childLayoutDisposer = reaction(() => [this.childDocs, (Cast(this.props.Document.childLayout, Doc) as Doc)?.[Id]],
- (args) => {
- const childLayout = Cast(this.props.Document.childLayout, Doc);
+ this._childLayoutDisposer = reaction(() => ({ childDocs: this.childDocs, childLayout: Cast(this.props.Document.childLayout, Doc) }),
+ ({ childDocs, childLayout }) => {
if (childLayout instanceof Doc) {
- this.childDocs.map(doc => Doc.ApplyTemplateTo(childLayout, doc, "layout_fromParent"));
+ childDocs.map(doc => {
+ doc.layout_fromParent = childLayout;
+ doc.layoutKey = "layout_fromParent";
+ });
}
else if (!(childLayout instanceof Promise)) {
- this.childDocs.filter(d => !d.isTemplateForField).map(doc => doc.layoutKey === "layout_fromParent" && (doc.layoutKey = "layout"));
+ childDocs.filter(d => !d.isTemplateForField).map(doc => doc.layoutKey === "layout_fromParent" && (doc.layoutKey = "layout"));
}
}, { fireImmediately: true });
}
componentWillUnmount() {
- this._childLayoutDisposer && this._childLayoutDisposer();
+ this.gestureDisposer?.();
+ this.multiTouchDisposer?.();
+ this._childLayoutDisposer?.();
}
- @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplateForField ? Doc.GetProto(this.props.DataDoc) : Doc.GetProto(this.props.Document); }
+ @computed get dataDoc() {
+ return (this.props.DataDoc instanceof Doc && this.props.Document.isTemplateForField ? Doc.GetProto(this.props.DataDoc) :
+ this.props.Document.resolvedDataDoc ? this.props.Document : Doc.GetProto(this.props.Document)); // if the layout document has a resolvedDataDoc, then we don't want to get its parent which would be the unexpanded template
+ }
// The data field for rendering this collection will be on the this.props.Document unless we're rendering a template in which case we try to use props.DataDoc.
// When a document has a DataDoc but it's not a template, then it contains its own rendering data, but needs to pass the DataDoc through
// to its children which may be templates.
// If 'annotationField' is specified, then all children exist on that field of the extension document, otherwise, they exist directly on the data document under 'fieldKey'
@computed get dataField() {
- const { annotationsKey, fieldKey } = this.props;
- if (annotationsKey) {
- return this.dataDoc[fieldKey + "-" + annotationsKey];
- }
- return this.dataDoc[fieldKey];
+ return this.dataDoc[this.props.fieldKey + (this.props.annotationsKey ? "-" + this.props.annotationsKey : "")];
}
get childLayoutPairs(): { layout: Doc; data: Doc; }[] {
const { Document, DataDoc } = this.props;
- const validPairs = this.childDocs.map(doc => Doc.GetLayoutDataDocPair(Document, DataDoc, doc)).filter(pair => pair.layout);
- return validPairs.map(({ data, layout }) => ({ data: data!, layout: layout! })); // this mapping is a bit of a hack to coerce types
+ const validPairs = this.childDocs.map(doc => Doc.GetLayoutDataDocPair(Document, !this.props.annotationsKey ? DataDoc : undefined, doc)).filter(pair => pair.layout);
+ return validPairs.map(({ data, layout }) => ({ data: data as Doc, layout: layout! })); // this mapping is a bit of a hack to coerce types
}
get childDocList() {
return Cast(this.dataField, listSpec(Doc));
}
- get childDocs() {
- const docs = DocListCast(this.dataField);
- const viewSpecScript = Cast(this.props.Document.viewSpecScript, ScriptField);
- const viewedDocs = viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs;
- const docFilters = Cast(this.props.Document._docFilter, listSpec("string"), []);
- const clusters: { [key: string]: { [value: string]: string } } = {};
+ @computed get childDocs() {
+ const docFilters = Cast(this.props.Document._docFilters, listSpec("string"), []);
+ const docRangeFilters = Cast(this.props.Document._docRangeFilters, listSpec("string"), []);
+ const filterFacets: { [key: string]: { [value: string]: string } } = {}; // maps each filter key to an object with value=>modifier fields
for (let i = 0; i < docFilters.length; i += 3) {
const [key, value, modifiers] = docFilters.slice(i, i + 3);
- const cluster = clusters[key];
- if (!cluster) {
- const child: { [value: string]: string } = {};
- child[value] = modifiers;
- clusters[key] = child;
- } else {
- cluster[value] = modifiers;
+ if (!filterFacets[key]) {
+ filterFacets[key] = {};
}
+ filterFacets[key][value] = modifiers;
+ }
+
+ let rawdocs: (Doc | Promise<Doc>)[] = [];
+ if (this.dataField instanceof Doc) { // if collection data is just a document, then promote it to a singleton list;
+ rawdocs = [this.dataField];
+ } else if (Cast(this.dataField, listSpec(Doc), null)) { // otherwise, if the collection data is a list, then use it.
+ rawdocs = Cast(this.dataField, listSpec(Doc), null);
+ } else { // Finally, if it's not a doc or a list and the document is a template, we try to render the root doc.
+ // For example, if an image doc is rendered with a slide template, the template will try to render the data field as a collection.
+ // Since the data field is actually an image, we set the list of documents to the singleton of root document's proto which will be an image.
+ const rootDoc = Cast(this.props.Document.rootDocument, Doc, null);
+ rawdocs = rootDoc && !this.props.annotationsKey ? [Doc.GetProto(rootDoc)] : [];
}
- const filteredDocs = docFilters.length ? viewedDocs.filter(d => {
- for (const key of Object.keys(clusters)) {
- const cluster = clusters[key];
- const satisfiesFacet = Object.keys(cluster).some(inner => {
- const modifier = cluster[inner];
- return (modifier === "x") !== Doc.matchFieldValue(d, key, inner);
- });
+ const docs = rawdocs.filter(d => !(d instanceof Promise)).map(d => d as Doc);
+ const viewSpecScript = Cast(this.props.Document.viewSpecScript, ScriptField);
+ const childDocs = viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs;
+
+ const filteredDocs = docFilters.length && !this.props.dontRegisterView ? childDocs.filter(d => {
+ for (const facetKey of Object.keys(filterFacets)) {
+ const facet = filterFacets[facetKey];
+ const satisfiesFacet = Object.keys(facet).some(value =>
+ (facet[value] === "x") !== Doc.matchFieldValue(d, facetKey, value));
if (!satisfiesFacet) {
return false;
}
}
return true;
- }) : viewedDocs;
- return filteredDocs;
+ }) : childDocs;
+ const rangeFilteredDocs = filteredDocs.filter(d => {
+ for (let i = 0; i < docRangeFilters.length; i += 3) {
+ const key = docRangeFilters[i];
+ const min = Number(docRangeFilters[i + 1]);
+ const max = Number(docRangeFilters[i + 2]);
+ const val = Cast(d[key], "number", null);
+ if (val !== undefined && (val < min || val > max)) {
+ return false;
+ }
+ }
+ return true;
+ });
+ return rangeFilteredDocs;
}
@action
@@ -171,23 +198,19 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
@undoBatch
protected onGesture(e: Event, ge: GestureUtils.GestureEvent) {
-
}
@undoBatch
@action
- protected drop(e: Event, de: DragManager.DropEvent): boolean {
+ protected onInternalDrop(e: Event, de: DragManager.DropEvent): boolean {
const docDragData = de.complete.docDragData;
(this.props.Document.dropConverter instanceof ScriptField) &&
this.props.Document.dropConverter.script.run({ dragData: docDragData }); /// bcz: check this
- if (docDragData && !docDragData.applyAsTemplate) {
- if (de.altKey && docDragData.draggedDocuments.length) {
- this.childDocs.map(doc =>
- Doc.ApplyTemplateTo(docDragData.draggedDocuments[0], doc, "layout_fromParent"));
- e.stopPropagation();
- return true;
- }
+ if (docDragData) {
let added = false;
+ if (this.props.Document._freezeOnDrop) {
+ de.complete.docDragData?.droppedDocuments.forEach(drop => Doc.freezeNativeDimensions(drop, drop[WidthSym](), drop[HeightSym]()));
+ }
if (docDragData.dropAction || docDragData.userDropAction) {
added = docDragData.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d) || added, false);
} else if (docDragData.moveDocument) {
@@ -210,150 +233,176 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
@undoBatch
@action
- protected async onDrop(e: React.DragEvent, options: DocumentOptions, completed?: () => void) {
+ protected async onExternalDrop(e: React.DragEvent, options: DocumentOptions, completed?: () => void) {
if (e.ctrlKey) {
e.stopPropagation(); // bcz: this is a hack to stop propagation when dropping an image on a text document with shift+ctrl
return;
}
- const html = e.dataTransfer.getData("text/html");
- const text = e.dataTransfer.getData("text/plain");
+
+ const { dataTransfer } = e;
+ const html = dataTransfer.getData("text/html");
+ const text = dataTransfer.getData("text/plain");
if (text && text.startsWith("<div")) {
return;
}
+
e.stopPropagation();
e.preventDefault();
+ const { addDocument } = this.props;
+ if (!addDocument) {
+ alert("this.props.addDocument does not exist. Aborting drop operation.");
+ return;
+ }
- if (html && FormattedTextBox.IsFragment(html)) {
- const href = FormattedTextBox.GetHref(html);
- if (href) {
- const docid = FormattedTextBox.GetDocFromUrl(href);
- if (docid) { // prosemirror text containing link to dash document
- DocServer.GetRefField(docid).then(f => {
- if (f instanceof Doc) {
- if (options.x || options.y) { f.x = options.x; f.y = options.y; } // should be in CollectionFreeFormView
- (f instanceof Doc) && this.props.addDocument(f);
- }
- });
+ if (html) {
+ if (FormattedTextBox.IsFragment(html)) {
+ const href = FormattedTextBox.GetHref(html);
+ if (href) {
+ const docid = FormattedTextBox.GetDocFromUrl(href);
+ if (docid) { // prosemirror text containing link to dash document
+ DocServer.GetRefField(docid).then(f => {
+ if (f instanceof Doc) {
+ if (options.x || options.y) { f.x = options.x; f.y = options.y; } // should be in CollectionFreeFormView
+ (f instanceof Doc) && addDocument(f);
+ }
+ });
+ } else {
+ addDocument(Docs.Create.WebDocument(href, { ...options, title: href }));
+ }
+ } else if (text) {
+ addDocument(Docs.Create.TextDocument(text, { ...options, _width: 100, _height: 25 }));
+ }
+ return;
+ }
+ if (!html.startsWith("<a")) {
+ const tags = html.split("<");
+ if (tags[0] === "") tags.splice(0, 1);
+ let img = tags[0].startsWith("img") ? tags[0] : tags.length > 1 && tags[1].startsWith("img") ? tags[1] : "";
+ const cors = img.includes("corsProxy") ? img.match(/http.*corsProxy\//)![0] : "";
+ img = cors ? img.replace(cors, "") : img;
+ if (img) {
+ const split = img.split("src=\"")[1].split("\"")[0];
+ 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 (source.startsWith("http")) {
+ const doc = Docs.Create.ImageDocument(source, { ...options, _width: 300 });
+ ImageUtils.ExtractExif(doc);
+ addDocument(doc);
+ }
+ return;
} else {
- this.props.addDocument && this.props.addDocument(Docs.Create.WebDocument(href, { ...options, title: href }));
+ const path = window.location.origin + "/doc/";
+ if (text.startsWith(path)) {
+ const docid = text.replace(Utils.prepend("/doc/"), "").split("?")[0];
+ DocServer.GetRefField(docid).then(f => {
+ if (f instanceof Doc) {
+ if (options.x || options.y) { f.x = options.x; f.y = options.y; } // should be in CollectionFreeFormView
+ (f instanceof Doc) && this.props.addDocument(f);
+ }
+ });
+ } else {
+ const htmlDoc = Docs.Create.HtmlDocument(html, { ...options, title: "-web page-", _width: 300, _height: 300 });
+ Doc.GetProto(htmlDoc)["data-text"] = text;
+ this.props.addDocument(htmlDoc);
+ }
+ return;
}
- } else if (text) {
- this.props.addDocument && this.props.addDocument(Docs.Create.TextDocument(text, { ...options, _width: 100, _height: 25 }));
}
- return;
}
- if (html && !html.startsWith("<a")) {
- const tags = html.split("<");
- if (tags[0] === "") tags.splice(0, 1);
- const img = tags[0].startsWith("img") ? tags[0] : tags.length > 1 && tags[1].startsWith("img") ? tags[1] : "";
- if (img) {
- const split = img.split("src=\"")[1].split("\"")[0];
- const doc = Docs.Create.ImageDocument(split, { ...options, _width: 300 });
- ImageUtils.ExtractExif(doc);
- this.props.addDocument(doc);
+
+ if (text) {
+ if (text.includes("www.youtube.com/watch")) {
+ const url = text.replace("youtube.com/watch?v=", "youtube.com/embed/");
+ addDocument(Docs.Create.VideoDocument(url, {
+ ...options,
+ title: url,
+ _width: 400,
+ _height: 315,
+ _nativeWidth: 600,
+ _nativeHeight: 472.5
+ }));
return;
- } else {
- const path = window.location.origin + "/doc/";
- if (text.startsWith(path)) {
- const docid = text.replace(Utils.prepend("/doc/"), "").split("?")[0];
- DocServer.GetRefField(docid).then(f => {
- if (f instanceof Doc) {
- if (options.x || options.y) { f.x = options.x; f.y = options.y; } // should be in CollectionFreeFormView
- (f instanceof Doc) && this.props.addDocument(f);
- }
- });
- } else {
- const htmlDoc = Docs.Create.HtmlDocument(html, { ...options, title: "-web page-", _width: 300, _height: 300, documentText: text });
- this.props.addDocument(htmlDoc);
- }
+ }
+ let matches: RegExpExecArray | null;
+ if ((matches = /(https:\/\/)?docs\.google\.com\/document\/d\/([^\\]+)\/edit/g.exec(text)) !== null) {
+ const newBox = Docs.Create.TextDocument("", { ...options, _width: 400, _height: 200, title: "Awaiting title from Google Docs..." });
+ const proto = newBox.proto!;
+ const documentId = matches[2];
+ proto[GoogleRef] = documentId;
+ proto.data = "Please select this document and then click on its pull button to load its contents from from Google Docs...";
+ proto.backgroundColor = "#eeeeff";
+ addDocument(newBox);
+ return;
+ }
+ if ((matches = /(https:\/\/)?photos\.google\.com\/(u\/3\/)?album\/([^\\]+)/g.exec(text)) !== null) {
+ const albumId = matches[3];
+ const mediaItems = await GooglePhotos.Query.AlbumSearch(albumId);
+ console.log(mediaItems);
return;
}
}
- if (text && text.indexOf("www.youtube.com/watch") !== -1) {
- const url = text.replace("youtube.com/watch?v=", "youtube.com/embed/");
- this.props.addDocument(Docs.Create.VideoDocument(url, { ...options, title: url, _width: 400, _height: 315, _nativeWidth: 600, _nativeHeight: 472.5 }));
- return;
- }
- let matches: RegExpExecArray | null;
- if ((matches = /(https:\/\/)?docs\.google\.com\/document\/d\/([^\\]+)\/edit/g.exec(text)) !== null) {
- const newBox = Docs.Create.TextDocument("", { ...options, _width: 400, _height: 200, title: "Awaiting title from Google Docs..." });
- const proto = newBox.proto!;
- const documentId = matches[2];
- proto[GoogleRef] = documentId;
- proto.data = "Please select this document and then click on its pull button to load its contents from from Google Docs...";
- proto.backgroundColor = "#eeeeff";
- this.props.addDocument(newBox);
- // const parent = Docs.Create.StackingDocument([newBox], { title: `Google Doc Import (${documentId})` });
- // CollectionDockingView.Instance.AddRightSplit(parent, undefined);
- // proto.height = parent[HeightSym]();
+
+ const { items } = e.dataTransfer;
+ const { length } = items;
+ const files: File[] = [];
+ const generatedDocuments: Doc[] = [];
+ if (!length) {
+ alert("No uploadable content found.");
return;
}
- if ((matches = /(https:\/\/)?photos\.google\.com\/(u\/3\/)?album\/([^\\]+)/g.exec(text)) !== null) {
- const albums = await GooglePhotos.Transactions.ListAlbums();
- const albumId = matches[3];
- const mediaItems = await GooglePhotos.Query.AlbumSearch(albumId);
- console.log(mediaItems);
- }
+
const batch = UndoManager.StartBatch("collection view drop");
- const promises: Promise<void>[] = [];
- // tslint:disable-next-line:prefer-for-of
- for (let i = 0; i < e.dataTransfer.items.length; i++) {
+ for (let i = 0; i < length; i++) {
const item = e.dataTransfer.items[i];
- if (item.kind === "string" && item.type.indexOf("uri") !== -1) {
- let str: string;
- const prom = new Promise<string>(resolve => e.dataTransfer.items[i].getAsString(resolve))
- .then(action((s: string) => rp.head(Utils.CorsProxy(str = s))))
- .then(result => {
- const type = result["content-type"];
- if (type) {
- Docs.Get.DocumentFromType(type, str, options)
- .then(doc => doc && this.props.addDocument(doc));
- }
- });
- promises.push(prom);
+ if (item.kind === "string" && item.type.includes("uri")) {
+ const stringContents = await new Promise<string>(resolve => item.getAsString(resolve));
+ const type = "html";// (await rp.head(Utils.CorsProxy(stringContents)))["content-type"];
+ if (type) {
+ const doc = await Docs.Get.DocumentFromType(type, stringContents, options);
+ doc && generatedDocuments.push(doc);
+ }
}
- const type = item.type;
if (item.kind === "file") {
const file = item.getAsFile();
- const formData = new FormData();
-
- if (!file || !file.type) {
- continue;
- }
-
- formData.append('file', file);
- const dropFileName = file ? file.name : "-empty-";
- promises.push(Networking.PostFormDataToServer("/uploadFormData", formData).then(results => {
- results.map(action((result: any) => {
- const { clientAccessPath, nativeWidth, nativeHeight, contentSize } = result;
- const full = { ...options, _width: 300, title: dropFileName };
- const pathname = Utils.prepend(clientAccessPath);
- Docs.Get.DocumentFromType(type, pathname, full).then(doc => {
- if (doc) {
- const proto = Doc.GetProto(doc);
- proto.fileUpload = basename(pathname).replace("upload_", "").replace(/\.[a-z0-9]*$/, "");
- nativeWidth && (proto["data-nativeWidth"] = nativeWidth);
- nativeHeight && (proto["data-nativeHeight"] = nativeHeight);
- contentSize && (proto.contentSize = contentSize);
- this.props.addDocument(doc);
- }
- });
- }));
- }));
+ file && file.type && files.push(file);
}
}
-
- if (promises.length) {
- Promise.all(promises).finally(() => { completed && completed(); batch.end(); });
+ for (const { source: { name, type }, result } of await Networking.UploadFilesToServer(files)) {
+ if (result instanceof Error) {
+ alert(`Upload failed: ${result.message}`);
+ return;
+ }
+ const full = { ...options, _width: 300, title: name };
+ const pathname = Utils.prepend(result.accessPaths.agnostic.client);
+ const doc = await Docs.Get.DocumentFromType(type, pathname, full);
+ if (!doc) {
+ continue;
+ }
+ const proto = Doc.GetProto(doc);
+ proto.fileUpload = basename(pathname).replace("upload_", "").replace(/\.[a-z0-9]*$/, "");
+ if (Upload.isImageInformation(result)) {
+ proto["data-nativeWidth"] = (result.nativeWidth > result.nativeHeight) ? 400 * result.nativeWidth / result.nativeHeight : 400;
+ proto["data-nativeHeight"] = (result.nativeWidth > result.nativeHeight) ? 400 : 400 / (result.nativeWidth / result.nativeHeight);
+ proto.contentSize = result.contentSize;
+ }
+ generatedDocuments.push(doc);
+ }
+ if (generatedDocuments.length) {
+ generatedDocuments.forEach(addDocument);
+ completed && completed();
} else {
if (text && !text.includes("https://")) {
- this.props.addDocument(Docs.Create.TextDocument(text, { ...options, _width: 400, _height: 315 }));
+ addDocument(Docs.Create.TextDocument(text, { ...options, _width: 400, _height: 315 }));
}
- batch.end();
}
+ batch.end();
}
}
+
return CollectionSubView;
}
diff --git a/src/client/views/collections/CollectionTimeView.scss b/src/client/views/collections/CollectionTimeView.scss
new file mode 100644
index 000000000..fa7c87f4e
--- /dev/null
+++ b/src/client/views/collections/CollectionTimeView.scss
@@ -0,0 +1,93 @@
+.collectionTimeView,
+.collectionTimeView-pivot {
+ display: flex;
+ flex-direction: row;
+ position: absolute;
+ height: 100%;
+ width: 100%;
+ overflow: hidden;
+
+ .collectionTimeView-backBtn {
+ background: green;
+ display: inline;
+ }
+
+ .collectionFreeform-customText {
+ text-align: left;
+ }
+
+ .collectionFreeform-customDiv {
+ position: absolute;
+ }
+
+ .collectionTimeView-thumb {
+ position: absolute;
+ width: 30px;
+ height: 30px;
+ transform: rotate(45deg);
+ display: inline-block;
+ background: gray;
+ bottom: 0;
+ margin-bottom: -17px;
+ border-radius: 9px;
+ opacity: 0.25;
+ }
+
+ .collectionTimeView-thumb-min {
+ margin-left: 25%;
+ }
+
+ .collectionTimeView-thumb-max {
+ margin-left: 75%;
+ }
+
+ .collectionTimeView-thumb-mid {
+ margin-left: 50%;
+ }
+
+ .collectionTimeView-flyout {
+ width: 400px;
+ display: block;
+ text-align: left;
+
+ .collectionTimeView-flyout-item {
+ background-color: lightgray;
+ text-align: left;
+ display: inline-block;
+ position: relative;
+ width: 100%;
+ }
+ }
+
+ .pivotKeyEntry {
+ position: absolute;
+ top: 5px;
+ right: 5px;
+ z-index: 10;
+ pointer-events: all;
+ padding: 5px;
+ border: 1px solid black;
+ display:none;
+ span {
+ margin-left : 10px;
+ }
+ }
+
+ .collectionTimeView-innards {
+ display: inline-block;
+ width: calc(100% - 200px);
+ height: 100%;
+ }
+}
+
+.collectionTimeView-pivot {
+ .collectionFreeform-customText {
+ text-align: center;
+ }
+}
+
+.collectionTimeView:hover, .collectionTimeView-pivot:hover {
+ .pivotKeyEntry {
+ display:unset;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx
new file mode 100644
index 000000000..4f77e8b0e
--- /dev/null
+++ b/src/client/views/collections/CollectionTimeView.tsx
@@ -0,0 +1,195 @@
+import { action, computed, observable, runInAction } from "mobx";
+import { observer } from "mobx-react";
+import { Doc } from "../../../new_fields/Doc";
+import { List } from "../../../new_fields/List";
+import { ObjectField } from "../../../new_fields/ObjectField";
+import { RichTextField } from "../../../new_fields/RichTextField";
+import { ComputedField, ScriptField } from "../../../new_fields/ScriptField";
+import { NumCast, StrCast } from "../../../new_fields/Types";
+import { emptyFunction, returnFalse, setupMoveUpEvents } from "../../../Utils";
+import { Scripting } from "../../util/Scripting";
+import { ContextMenu } from "../ContextMenu";
+import { ContextMenuProps } from "../ContextMenuItem";
+import { EditableView } from "../EditableView";
+import { ViewDefBounds } from "./collectionFreeForm/CollectionFreeFormLayoutEngines";
+import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView";
+import { CollectionSubView } from "./CollectionSubView";
+import "./CollectionTimeView.scss";
+const higflyout = require("@hig/flyout");
+export const { anchorPoints } = higflyout;
+export const Flyout = higflyout.default;
+import React = require("react");
+
+@observer
+export class CollectionTimeView extends CollectionSubView(doc => doc) {
+ _changing = false;
+ @observable _layoutEngine = "pivot";
+ @observable _collapsed: boolean = false;
+ componentWillUnmount() {
+ this.props.Document.onChildClick = undefined;
+ }
+ componentDidMount() {
+ this.props.Document._freezeOnDrop = true;
+ const childDetailed = this.props.Document.childDetailed; // bcz: needs to be here to make sure the childDetailed layout template has been loaded when the first item is clicked;
+ const childText = "const alias = getAlias(this); Doc.ApplyTemplateTo(containingCollection.childDetailed, alias, 'layout_detailView'); alias.layoutKey='layout_detailedView'; alias.dropAction='alias'; alias.removeDropProperties=new List<string>(['dropAction']); useRightSplit(alias, shiftKey); ";
+ this.props.Document.onChildClick = ScriptField.MakeScript(childText, { this: Doc.name, heading: "string", containingCollection: Doc.name, shiftKey: "boolean" });
+ this.props.Document._fitToBox = true;
+ if (!this.props.Document.onViewDefClick) {
+ this.props.Document.onViewDefDivClick = ScriptField.MakeScript("pivotColumnClick(this,payload)", { payload: "any" });
+ }
+ }
+
+ layoutEngine = () => this._layoutEngine;
+ toggleVisibility = action(() => this._collapsed = !this._collapsed);
+
+ onMinDown = (e: React.PointerEvent) => {
+ setupMoveUpEvents(this, e, action((e: PointerEvent, down: number[], delta: number[]) => {
+ const minReq = NumCast(this.props.Document[this.props.fieldKey + "-timelineMinReq"], NumCast(this.props.Document[this.props.fieldKey + "-timelineMin"], 0));
+ const maxReq = NumCast(this.props.Document[this.props.fieldKey + "-timelineMaxReq"], NumCast(this.props.Document[this.props.fieldKey + "-timelineMax"], 10));
+ this.props.Document[this.props.fieldKey + "-timelineMinReq"] = minReq + (maxReq - minReq) * delta[0] / this.props.PanelWidth();
+ this.props.Document[this.props.fieldKey + "-timelineSpan"] = undefined;
+ return false;
+ }), returnFalse, emptyFunction);
+ }
+
+ onMaxDown = (e: React.PointerEvent) => {
+ setupMoveUpEvents(this, e, action((e: PointerEvent, down: number[], delta: number[]) => {
+ const minReq = NumCast(this.props.Document[this.props.fieldKey + "-timelineMinReq"], NumCast(this.props.Document[this.props.fieldKey + "-timelineMin"], 0));
+ const maxReq = NumCast(this.props.Document[this.props.fieldKey + "-timelineMaxReq"], NumCast(this.props.Document[this.props.fieldKey + "-timelineMax"], 10));
+ this.props.Document[this.props.fieldKey + "-timelineMaxReq"] = maxReq + (maxReq - minReq) * delta[0] / this.props.PanelWidth();
+ return false;
+ }), returnFalse, emptyFunction);
+ }
+
+ onMidDown = (e: React.PointerEvent) => {
+ setupMoveUpEvents(this, e, action((e: PointerEvent, down: number[], delta: number[]) => {
+ const minReq = NumCast(this.props.Document[this.props.fieldKey + "-timelineMinReq"], NumCast(this.props.Document[this.props.fieldKey + "-timelineMin"], 0));
+ const maxReq = NumCast(this.props.Document[this.props.fieldKey + "-timelineMaxReq"], NumCast(this.props.Document[this.props.fieldKey + "-timelineMax"], 10));
+ this.props.Document[this.props.fieldKey + "-timelineMinReq"] = minReq - (maxReq - minReq) * delta[0] / this.props.PanelWidth();
+ this.props.Document[this.props.fieldKey + "-timelineMaxReq"] = maxReq - (maxReq - minReq) * delta[0] / this.props.PanelWidth();
+ return false;
+ }), returnFalse, emptyFunction);
+ }
+
+ @computed get contents() {
+ return <div className="collectionTimeView-innards" key="timeline" style={{ width: "100%" }}>
+ <CollectionFreeFormView {...this.props} layoutEngine={this.layoutEngine} />
+ </div>;
+ }
+
+ public static SyncTimelineToPresentation(doc: Doc) {
+ const fieldKey = Doc.LayoutFieldKey(doc);
+ doc[fieldKey + "-timelineCur"] = ComputedField.MakeFunction("(curPresentationItem()[this._pivotField || 'year'] || 0)");
+ }
+ specificMenu = (e: React.MouseEvent) => {
+ const layoutItems: ContextMenuProps[] = [];
+ const doc = this.props.Document;
+
+ layoutItems.push({ description: "Force Timeline", event: () => { doc._forceRenderEngine = "timeline"; }, icon: "compress-arrows-alt" });
+ layoutItems.push({ description: "Force Pivot", event: () => { doc._forceRenderEngine = "pivot"; }, icon: "compress-arrows-alt" });
+ layoutItems.push({ description: "Auto Time/Pivot layout", event: () => { doc._forceRenderEngine = undefined; }, icon: "compress-arrows-alt" });
+ layoutItems.push({ description: "Sync with presentation", event: () => CollectionTimeView.SyncTimelineToPresentation(doc), icon: "compress-arrows-alt" });
+
+ ContextMenu.Instance.addItem({ description: "Pivot/Time Options ...", subitems: layoutItems, icon: "eye" });
+ }
+ @computed get _allFacets() {
+ const facets = new Set<string>();
+ this.childDocs.forEach(child => Object.keys(Doc.GetProto(child)).forEach(key => facets.add(key)));
+ Doc.AreProtosEqual(this.dataDoc, this.props.Document) && this.childDocs.forEach(child => Object.keys(child).forEach(key => facets.add(key)));
+ return Array.from(facets);
+ }
+ menuCallback = (x: number, y: number) => {
+ ContextMenu.Instance.clearItems();
+ const docItems: ContextMenuProps[] = [];
+ const keySet: Set<string> = new Set();
+
+ this.childLayoutPairs.map(pair => this._allFacets.filter(fieldKey =>
+ pair.layout[fieldKey] instanceof RichTextField ||
+ typeof (pair.layout[fieldKey]) === "number" ||
+ typeof (pair.layout[fieldKey]) === "string").map(fieldKey => keySet.add(fieldKey)));
+ Array.from(keySet).map(fieldKey =>
+ docItems.push({ description: ":" + fieldKey, event: () => this.props.Document._pivotField = fieldKey, icon: "compress-arrows-alt" }));
+ docItems.push({ description: ":(null)", event: () => this.props.Document._pivotField = undefined, icon: "compress-arrows-alt" });
+ ContextMenu.Instance.addItem({ description: "Pivot Fields ...", subitems: docItems, icon: "eye" });
+ const pt = this.props.ScreenToLocalTransform().inverse().transformPoint(x, y);
+ ContextMenu.Instance.displayMenu(x, y, ":");
+ }
+
+ @computed get pivotKeyUI() {
+ const newEditableViewProps = {
+ GetValue: () => "",
+ SetValue: (value: any) => {
+ if (value?.length) {
+ this.props.Document._pivotField = value;
+ return true;
+ }
+ return false;
+ },
+ showMenuOnLoad: true,
+ contents: ":" + StrCast(this.props.Document._pivotField),
+ toggle: this.toggleVisibility,
+ color: "#f1efeb" // this.props.headingObject ? this.props.headingObject.color : "#f1efeb";
+ };
+ return <div className={"pivotKeyEntry"}>
+ <button className="collectionTimeView-backBtn"
+ onClick={action(() => {
+ let prevFilterIndex = NumCast(this.props.Document._prevFilterIndex);
+ if (prevFilterIndex > 0) {
+ prevFilterIndex--;
+ this.props.Document._docFilters = ObjectField.MakeCopy(this.props.Document["_prevDocFilter" + prevFilterIndex] as ObjectField);
+ this.props.Document._docRangeFilters = ObjectField.MakeCopy(this.props.Document["_prevDocRangeFilters" + prevFilterIndex] as ObjectField);
+ this.props.Document._prevFilterIndex = prevFilterIndex;
+ } else {
+ this.props.Document._docFilters = new List([]);
+ }
+ })}>
+ back
+ </button>
+ <EditableView {...newEditableViewProps} display={"inline"} menuCallback={this.menuCallback} />
+ </div>;
+ }
+
+ render() {
+ let nonNumbers = 0;
+ this.childDocs.map(doc => {
+ const num = NumCast(doc[StrCast(this.props.Document._pivotField)], Number(StrCast(doc[StrCast(this.props.Document._pivotField)])));
+ if (Number.isNaN(num)) {
+ nonNumbers++;
+ }
+ });
+ const forceLayout = StrCast(this.props.Document._forceRenderEngine);
+ const doTimeline = forceLayout ? (forceLayout === "timeline") : nonNumbers / this.childDocs.length < 0.1 && this.props.PanelWidth() / this.props.PanelHeight() > 6;
+ if (doTimeline !== (this._layoutEngine === "timeline")) {
+ if (!this._changing) {
+ this._changing = true;
+ setTimeout(action(() => {
+ this._layoutEngine = doTimeline ? "timeline" : "pivot";
+ this._changing = false;
+ }), 0);
+ }
+ }
+
+ return <div className={"collectionTimeView" + (doTimeline ? "" : "-pivot")} onContextMenu={this.specificMenu}
+ style={{ width: this.props.PanelWidth(), height: `calc(100% - ${this.props.Document._chromeStatus === "enabled" ? 51 : 0}px)` }}>
+ {this.pivotKeyUI}
+ {this.contents}
+ {!this.props.isSelected() || !doTimeline ? (null) : <>
+ <div className="collectionTimeView-thumb-min collectionTimeView-thumb" key="min" onPointerDown={this.onMinDown} />
+ <div className="collectionTimeView-thumb-max collectionTimeView-thumb" key="mid" onPointerDown={this.onMaxDown} />
+ <div className="collectionTimeView-thumb-mid collectionTimeView-thumb" key="max" onPointerDown={this.onMidDown} />
+ </>}
+ </div>;
+ }
+}
+
+Scripting.addGlobal(function pivotColumnClick(pivotDoc: Doc, bounds: ViewDefBounds) {
+ let prevFilterIndex = NumCast(pivotDoc._prevFilterIndex);
+ pivotDoc["_prevDocFilter" + prevFilterIndex] = ObjectField.MakeCopy(pivotDoc._docFilters as ObjectField);
+ pivotDoc["_prevDocRangeFilters" + prevFilterIndex] = ObjectField.MakeCopy(pivotDoc._docRangeFilters as ObjectField);
+ pivotDoc._prevFilterIndex = ++prevFilterIndex;
+ runInAction(() => {
+ pivotDoc._docFilters = new List();
+ (bounds.payload as string[]).map(filterVal =>
+ Doc.setDocFilter(pivotDoc, StrCast(pivotDoc._pivotField), filterVal, "check"));
+ });
+}); \ No newline at end of file
diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss
index 2fa6813d7..8e95f7fbe 100644
--- a/src/client/views/collections/CollectionTreeView.scss
+++ b/src/client/views/collections/CollectionTreeView.scss
@@ -22,6 +22,7 @@
ul {
list-style: none;
padding-left: 20px;
+ margin-bottom: 1px;// otherwise vertical scrollbars may pop up for no apparent reason....
}
@@ -34,7 +35,9 @@
width: 15px;
color: $intermediate-color;
margin-top: 3px;
- transform: scale(1.3, 1.3);
+ transform: scale(1.3, 1.3);
+ border: #80808030 1px solid;
+ border-radius: 4px;
}
.editableView-container {
@@ -63,7 +66,9 @@
font-size: 8pt;
margin-left: 3px;
display: none;
- background: lightgray;
+}
+.collectionTreeView-keyHeader:hover {
+ background: #797777;
}
.collectionTreeView-subtitle {
@@ -84,9 +89,11 @@
.treeViewItem-openRight {
display: none;
height: 17px;
- background: gray;
width: 15px;
}
+.treeViewItem-openRight:hover {
+ background: #797777;
+}
.treeViewItem-border {
display: inherit;
@@ -101,7 +108,6 @@
.treeViewItem-openRight {
display: inline-block;
height: 17px;
- background: #a8a7a7;
width: 15px;
// display: inline;
@@ -120,6 +126,9 @@
.editableView-container-editing-oneLine {
min-width: 15px;
}
+ .documentView-node-topmost {
+ width: unset;
+ }
}
.treeViewItem-header-above {
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index a7733ab5f..6ee48f11b 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -3,14 +3,14 @@ import { faAngleRight, faArrowsAltH, faBell, faCamera, faCaretDown, faCaretRight
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed, observable, runInAction, untracked } from "mobx";
import { observer } from "mobx-react";
-import { Doc, DocListCast, Field, HeightSym, WidthSym } from '../../../new_fields/Doc';
+import { Doc, DocListCast, Field, HeightSym, WidthSym, DataSym, Opt } from '../../../new_fields/Doc';
import { Id } from '../../../new_fields/FieldSymbols';
import { List } from '../../../new_fields/List';
-import { Document, listSpec } from '../../../new_fields/Schema';
+import { Document, listSpec, createSchema, makeInterface } from '../../../new_fields/Schema';
import { ComputedField, ScriptField } from '../../../new_fields/ScriptField';
import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../new_fields/Types';
import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils';
-import { emptyFunction, emptyPath, returnFalse, Utils } from '../../../Utils';
+import { emptyFunction, emptyPath, returnFalse, Utils, returnOne, returnZero, returnTransparent, returnTrue } from '../../../Utils';
import { Docs, DocUtils } from '../../documents/Documents';
import { DocumentType } from "../../documents/DocumentTypes";
import { DocumentManager } from '../../util/DocumentManager';
@@ -29,12 +29,12 @@ import { ImageBox } from '../nodes/ImageBox';
import { KeyValueBox } from '../nodes/KeyValueBox';
import { ScriptBox } from '../ScriptBox';
import { Templates } from '../Templates';
-import { CollectionSubView } from "./CollectionSubView";
+import { CollectionSubView, SubCollectionViewProps } from "./CollectionSubView";
import "./CollectionTreeView.scss";
import React = require("react");
import { CollectionViewType } from './CollectionView';
import { RichTextField } from '../../../new_fields/RichTextField';
-import { ObjectField } from '../../../new_fields/ObjectField';
+import { DocumentView } from '../nodes/DocumentView';
export interface TreeViewProps {
@@ -46,8 +46,8 @@ export interface TreeViewProps {
renderDepth: number;
deleteDoc: (doc: Doc) => boolean;
moveDocument: DragManager.MoveFunction;
- dropAction: "alias" | "copy" | undefined;
- addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string, libraryPath?: Doc[]) => boolean;
+ dropAction: dropActionType;
+ addDocTab: (doc: Doc, where: string, libraryPath?: Doc[]) => boolean;
pinToPres: (document: Doc) => void;
panelWidth: () => number;
panelHeight: () => number;
@@ -56,14 +56,17 @@ export interface TreeViewProps {
indentDocument?: () => void;
outdentDocument?: () => void;
ScreenToLocalTransform: () => Transform;
+ backgroundColor?: (doc: Doc) => string | undefined;
outerXf: () => { translateX: number, translateY: number };
treeViewId: Doc;
parentKey: string;
active: (outsideReaction?: boolean) => boolean;
- hideHeaderFields: () => boolean;
- preventTreeViewOpen: boolean;
+ treeViewHideHeaderFields: () => boolean;
+ treeViewPreventOpen: boolean;
renderedIds: string[];
onCheckedClick?: ScriptField;
+ onChildClick?: ScriptField;
+ ignoreFields?: string[];
}
library.add(faTrashAlt);
@@ -84,21 +87,21 @@ library.add(faPlus, faMinus);
*
* special fields:
* treeViewOpen : flag denoting whether the documents sub-tree (contents) is visible or hidden
- * preventTreeViewOpen : ignores the treeViewOpen flag (for allowing a view to not be slaved to other views of the document)
+ * treeViewPreventOpen : ignores the treeViewOpen flag (for allowing a view to not be slaved to other views of the document)
* treeViewExpandedView : name of field whose contents are being displayed as the document's subtree
*/
class TreeView extends React.Component<TreeViewProps> {
- static loadId = "";
private _header?: React.RefObject<HTMLDivElement> = React.createRef();
private _treedropDisposer?: DragManager.DragDropDisposer;
private _dref = React.createRef<HTMLDivElement>();
+ private _tref = React.createRef<HTMLDivElement>();
get displayName() { return "TreeView(" + this.props.document.title + ")"; } // this makes mobx trace() statements more descriptive
get defaultExpandedView() { return this.childDocs ? this.fieldKey : StrCast(this.props.document.defaultExpandedView, "fields"); }
@observable _overrideTreeViewOpen = false; // override of the treeViewOpen field allowing the display state to be independent of the document's state
- set treeViewOpen(c: boolean) { if (this.props.preventTreeViewOpen) this._overrideTreeViewOpen = c; else this.props.document.treeViewOpen = this._overrideTreeViewOpen = c; }
- @computed get treeViewOpen() { return (!this.props.preventTreeViewOpen && BoolCast(this.props.document.treeViewOpen)) || this._overrideTreeViewOpen; }
+ set treeViewOpen(c: boolean) { if (this.props.treeViewPreventOpen) this._overrideTreeViewOpen = c; else this.props.document.treeViewOpen = this._overrideTreeViewOpen = c; }
+ @computed get treeViewOpen() { return (!this.props.treeViewPreventOpen && BoolCast(this.props.document.treeViewOpen)) || this._overrideTreeViewOpen; }
@computed get treeViewExpandedView() { return StrCast(this.props.document.treeViewExpandedView, this.defaultExpandedView); }
@computed get MAX_EMBED_HEIGHT() { return NumCast(this.props.document.maxEmbedHeight, 300); }
@computed get dataDoc() { return this.templateDataDoc ? this.templateDataDoc : this.props.document; }
@@ -128,7 +131,7 @@ class TreeView extends React.Component<TreeViewProps> {
}
@undoBatch delete = () => this.props.deleteDoc(this.props.document);
- @undoBatch openRight = () => this.props.addDocTab(this.props.document, this.templateDataDoc, "onRight", this.props.libraryPath);
+ @undoBatch openRight = () => this.props.addDocTab(this.props.dropAction === "alias" ? Doc.MakeAlias(this.props.document) : this.props.document, "onRight", this.props.libraryPath);
@undoBatch indent = () => this.props.addDocument(this.props.document) && this.delete();
@undoBatch move = (doc: Doc, target: Doc | undefined, addDoc: (doc: Doc) => boolean) => {
return this.props.document !== target && this.props.deleteDoc(doc) && addDoc(doc);
@@ -171,39 +174,49 @@ class TreeView extends React.Component<TreeViewProps> {
editableView = (key: string, style?: string) => (<EditableView
oneLine={true}
display={"inline-block"}
- editing={this.dataDoc[Id] === TreeView.loadId}
+ editing={true /*this.dataDoc[Id] === EditableView.loadId*/}
contents={StrCast(this.props.document[key])}
height={12}
fontStyle={style}
fontSize={12}
GetValue={() => StrCast(this.props.document[key])}
- SetValue={undoBatch((value: string) => Doc.SetInPlace(this.props.document, key, value, false) || true)}
+ SetValue={undoBatch((value: string) => {
+ Doc.SetInPlace(this.props.document, key, value, false) || true;
+ this.props.document.editTitle = undefined;
+ })}
OnFillDown={undoBatch((value: string) => {
Doc.SetInPlace(this.props.document, key, value, false);
- const layoutDoc = this.props.document.layout_custom instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.document.layout_custom)) : undefined;
- const doc = layoutDoc || Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, _width: 100, _height: 25, templates: new List<string>([Templates.Title.Layout]) });
- TreeView.loadId = doc[Id];
+ const doc = Docs.Create.FreeformDocument([], { title: "-", x: 0, y: 0, _width: 100, _height: 25, templates: new List<string>([Templates.Title.Layout]) });
+ //EditableView.loadId = doc[Id];
+ this.props.document.editTitle = undefined;
+ doc.editTitle = true;
return this.props.addDocument(doc);
})}
+ onClick={() => {
+ SelectionManager.DeselectAll();
+ Doc.UserDoc().SelectedDocs = new List([this.props.document]);
+ return false;
+ }}
OnTab={undoBatch((shift?: boolean) => {
- TreeView.loadId = this.dataDoc[Id];
+ EditableView.loadId = this.dataDoc[Id];
shift ? this.props.outdentDocument?.() : this.props.indentDocument?.();
setTimeout(() => { // unsetting/setting brushing for this doc will recreate & refocus this editableView after all other treeview changes have been made to the Dom (which may remove focus from this document).
Doc.UnBrushDoc(this.props.document);
Doc.BrushDoc(this.props.document);
- TreeView.loadId = "";
+ EditableView.loadId = "";
}, 0);
})}
/>)
onWorkspaceContextMenu = (e: React.MouseEvent): void => {
if (!e.isPropagationStopped()) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view
+ const sort = this.props.document[`${this.fieldKey}-sortAscending`];
if (this.props.document === CurrentUserUtils.UserDocument.recentlyClosed) {
ContextMenu.Instance.addItem({ description: "Clear All", event: () => Doc.GetProto(CurrentUserUtils.UserDocument.recentlyClosed as Doc).data = new List<Doc>(), icon: "plus" });
} else if (this.props.document !== CurrentUserUtils.UserDocument.workspaces) {
ContextMenu.Instance.addItem({ description: "Pin to Presentation", event: () => this.props.pinToPres(this.props.document), icon: "tv" });
- ContextMenu.Instance.addItem({ description: "Open Tab", event: () => this.props.addDocTab(this.props.document, this.templateDataDoc, "inTab", this.props.libraryPath), icon: "folder" });
- ContextMenu.Instance.addItem({ description: "Open Right", event: () => this.props.addDocTab(this.props.document, this.templateDataDoc, "onRight", this.props.libraryPath), icon: "caret-square-right" });
+ ContextMenu.Instance.addItem({ description: "Open Tab", event: () => this.props.addDocTab(this.props.document, "inTab", this.props.libraryPath), icon: "folder" });
+ ContextMenu.Instance.addItem({ description: "Open Right", event: () => this.props.addDocTab(this.props.document, "onRight", this.props.libraryPath), icon: "caret-square-right" });
if (DocumentManager.Instance.getDocumentViews(this.dataDoc).length) {
ContextMenu.Instance.addItem({ description: "Focus", event: () => (view => view && view.props.focus(this.props.document, true))(DocumentManager.Instance.getFirstDocumentView(this.props.document)), icon: "camera" });
}
@@ -212,7 +225,9 @@ class TreeView extends React.Component<TreeViewProps> {
ContextMenu.Instance.addItem({ description: "Delete Workspace", event: () => this.props.deleteDoc(this.props.document), icon: "trash-alt" });
ContextMenu.Instance.addItem({ description: "Create New Workspace", event: () => MainView.Instance.createNewWorkspace(), icon: "plus" });
}
- ContextMenu.Instance.addItem({ description: "Open Fields", event: () => { const kvp = Docs.Create.KVPDocument(this.props.document, { _width: 300, _height: 300 }); this.props.addDocTab(kvp, this.props.dataDoc ? this.props.dataDoc : kvp, "onRight"); }, icon: "layer-group" });
+ ContextMenu.Instance.addItem({ description: (sort ? "Sort Descending" : (sort === false ? "Unsort" : "Sort Ascending")), event: () => this.props.document[`${this.fieldKey}-sortAscending`] = (sort ? false : (sort === false ? undefined : true)), icon: "minus" });
+ ContextMenu.Instance.addItem({ description: "Toggle Theme Colors", event: () => this.props.document.darkScheme = !this.props.document.darkScheme, icon: "minus" });
+ ContextMenu.Instance.addItem({ description: "Open Fields", event: () => { const kvp = Docs.Create.KVPDocument(this.props.document, { _width: 300, _height: 300 }); this.props.addDocTab(kvp, "onRight"); }, icon: "layer-group" });
ContextMenu.Instance.addItem({ description: "Publish", event: () => DocUtils.Publish(this.props.document, StrCast(this.props.document.title), () => { }, () => { }), icon: "file" });
ContextMenu.Instance.displayMenu(e.pageX > 156 ? e.pageX - 156 : 0, e.pageY - 15);
e.stopPropagation();
@@ -229,7 +244,7 @@ class TreeView extends React.Component<TreeViewProps> {
if (de.complete.linkDragData) {
const sourceDoc = de.complete.linkDragData.linkSourceDocument;
const destDoc = this.props.document;
- DocUtils.MakeLink({ doc: sourceDoc }, { doc: destDoc });
+ DocUtils.MakeLink({ doc: sourceDoc }, { doc: destDoc }, "tree link");
e.stopPropagation();
}
if (de.complete.docDragData) {
@@ -256,6 +271,13 @@ class TreeView extends React.Component<TreeViewProps> {
const finalXf = this.props.ScreenToLocalTransform().translate(offset[0], offset[1] + (this.props.ChromeHeight && this.props.ChromeHeight() < 0 ? this.props.ChromeHeight() : 0));
return finalXf;
}
+ getTransform = () => {
+ const { scale, translateX, translateY } = Utils.GetScreenTransform(this._tref.current!);
+ const outerXf = this.props.outerXf();
+ const offset = this.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY);
+ const finalXf = this.props.ScreenToLocalTransform().translate(offset[0], offset[1]);
+ return finalXf;
+ }
docWidth = () => {
const layoutDoc = Doc.Layout(this.props.document);
const aspect = NumCast(layoutDoc._nativeHeight) / NumCast(layoutDoc._nativeWidth);
@@ -283,6 +305,7 @@ class TreeView extends React.Component<TreeViewProps> {
const rows: JSX.Element[] = [];
for (const key of Object.keys(ids).slice().sort()) {
+ if (this.props.ignoreFields?.includes(key)) continue;
const contents = doc[key];
let contentElement: (JSX.Element | null)[] | JSX.Element = [];
@@ -291,13 +314,13 @@ class TreeView extends React.Component<TreeViewProps> {
const addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => Doc.AddDocToList(this.dataDoc, key, doc, addBefore, before, false, true);
contentElement = TreeView.GetChildElements(contents instanceof Doc ? [contents] :
DocListCast(contents), this.props.treeViewId, doc, undefined, key, this.props.containingCollection, this.props.prevSibling, addDoc, remDoc, this.move,
- this.props.dropAction, this.props.addDocTab, this.props.pinToPres, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active,
- this.props.panelWidth, this.props.ChromeHeight, this.props.renderDepth, this.props.hideHeaderFields, this.props.preventTreeViewOpen,
- [...this.props.renderedIds, doc[Id]], this.props.libraryPath, this.props.onCheckedClick);
+ this.props.dropAction, this.props.addDocTab, this.props.pinToPres, this.props.backgroundColor, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active,
+ this.props.panelWidth, this.props.ChromeHeight, this.props.renderDepth, this.props.treeViewHideHeaderFields, this.props.treeViewPreventOpen,
+ [...this.props.renderedIds, doc[Id]], this.props.libraryPath, this.props.onCheckedClick, this.props.onChildClick, this.props.ignoreFields);
} else {
contentElement = <EditableView
key="editableView"
- contents={contents !== undefined ? contents.toString() : "null"}
+ contents={contents !== undefined ? Field.toString(contents as Field) : "null"}
height={13}
fontSize={12}
GetValue={() => Field.toKeyValueString(doc, key)}
@@ -334,9 +357,9 @@ class TreeView extends React.Component<TreeViewProps> {
{!docs ? (null) :
TreeView.GetChildElements(docs, this.props.treeViewId, Doc.Layout(this.props.document),
this.templateDataDoc, expandKey, this.props.containingCollection, this.props.prevSibling, addDoc, remDoc, this.move,
- this.props.dropAction, this.props.addDocTab, this.props.pinToPres, this.props.ScreenToLocalTransform,
- this.props.outerXf, this.props.active, this.props.panelWidth, this.props.ChromeHeight, this.props.renderDepth, this.props.hideHeaderFields, this.props.preventTreeViewOpen,
- [...this.props.renderedIds, this.props.document[Id]], this.props.libraryPath, this.props.onCheckedClick)}
+ this.props.dropAction, this.props.addDocTab, this.props.pinToPres, this.props.backgroundColor, this.props.ScreenToLocalTransform,
+ this.props.outerXf, this.props.active, this.props.panelWidth, this.props.ChromeHeight, this.props.renderDepth, this.props.treeViewHideHeaderFields, this.props.treeViewPreventOpen,
+ [...this.props.renderedIds, this.props.document[Id]], this.props.libraryPath, this.props.onCheckedClick, this.props.onChildClick, this.props.ignoreFields)}
</ul >;
} else if (this.treeViewExpandedView === "fields") {
return <ul><div ref={this._dref} style={{ display: "inline-block" }} key={this.props.document[Id] + this.props.document.title}>
@@ -350,6 +373,7 @@ class TreeView extends React.Component<TreeViewProps> {
DataDocument={this.templateDataDoc}
LibraryPath={emptyPath}
renderDepth={this.props.renderDepth + 1}
+ backgroundColor={this.props.backgroundColor}
fitToBox={this.boundsOfCollectionDocument !== undefined}
PanelWidth={this.docWidth}
PanelHeight={this.docHeight}
@@ -386,7 +410,7 @@ class TreeView extends React.Component<TreeViewProps> {
@computed
get renderBullet() {
const checked = this.props.document.type === DocumentType.COL ? undefined : this.props.onCheckedClick ? (this.props.document.treeViewChecked ? this.props.document.treeViewChecked : "unchecked") : undefined;
- return <div className="bullet" title="view inline" onClick={this.bulletClick} style={{ color: StrCast(this.props.document.color, checked === "unchecked" ? "white" : "black"), opacity: 0.4 }}>
+ return <div className="bullet" title="view inline" onClick={this.bulletClick} style={{ color: StrCast(this.props.document.color, checked === "unchecked" ? "white" : "inherit"), opacity: checked === "unchecked" ? undefined : 0.4 }}>
{<FontAwesomeIcon icon={checked === "check" ? "check" : (checked === "x" ? "times" : checked === "unchecked" ? "square" : !this.treeViewOpen ? (this.childDocs ? "caret-square-right" : "caret-right") : (this.childDocs ? "caret-square-down" : "caret-down"))} />}
</div>;
}
@@ -395,8 +419,8 @@ class TreeView extends React.Component<TreeViewProps> {
*/
@computed
get renderTitle() {
- const reference = React.createRef<HTMLDivElement>();
- const onItemDown = SetupDrag(reference, () => this.dataDoc, this.move, this.props.dropAction, this.props.treeViewId[Id], true);
+ const onItemDown = SetupDrag(this._tref, () => this.dataDoc, this.move, this.props.dropAction, this.props.treeViewId[Id], true);
+ const editTitle = ScriptField.MakeFunction("this.editTitle=true", { this: Doc.name });
const headerElements = (
<span className="collectionTreeView-keyHeader" key={this.treeViewExpandedView}
@@ -415,17 +439,43 @@ class TreeView extends React.Component<TreeViewProps> {
<FontAwesomeIcon title="open in pane on right" icon="angle-right" size="lg" />
</div>);
return <>
- <div className="docContainer" title="click to edit title" id={`docContainer-${this.props.parentKey}`} ref={reference} onPointerDown={onItemDown}
+ <div className="docContainer" ref={this._tref} title="click to edit title" id={`docContainer-${this.props.parentKey}`} onPointerDown={onItemDown}
style={{
- color: this.props.document.isMinimized ? "red" : "black",
background: Doc.IsHighlighted(this.props.document) ? "orange" : Doc.IsBrushed(this.props.document) ? "#06121212" : "0",
fontWeight: this.props.document.searchMatch ? "bold" : undefined,
outline: BoolCast(this.props.document.workspaceBrush) ? "dashed 1px #06123232" : undefined,
pointerEvents: this.props.active() || SelectionManager.GetIsDragging() ? "all" : "none"
}} >
- {this.editableView("title")}
+ {this.props.document.editTitle ?
+ this.editableView("title") :
+ <DocumentView
+ Document={this.props.document}
+ DataDoc={undefined}
+ LibraryPath={this.props.libraryPath || []}
+ addDocument={undefined}
+ addDocTab={this.props.addDocTab}
+ pinToPres={emptyFunction}
+ onClick={this.props.onChildClick || editTitle}
+ dropAction={this.props.dropAction}
+ moveDocument={this.props.moveDocument}
+ removeDocument={undefined}
+ ScreenToLocalTransform={this.getTransform}
+ ContentScaling={returnOne}
+ PanelWidth={returnZero}
+ PanelHeight={returnZero}
+ renderDepth={1}
+ focus={emptyFunction}
+ parentActive={returnTrue}
+ whenActiveChanged={emptyFunction}
+ bringToFront={emptyFunction}
+ dontRegisterView={BoolCast(this.props.treeViewId.dontRegisterChildren)}
+ ContainingCollectionView={undefined}
+ ContainingCollectionDoc={undefined}
+ zoomToScale={emptyFunction}
+ getScale={returnOne}
+ />}
</div >
- {this.props.hideHeaderFields() ? (null) : headerElements}
+ {this.props.treeViewHideHeaderFields() ? (null) : headerElements}
{openRight}
</>;
}
@@ -456,19 +506,22 @@ class TreeView extends React.Component<TreeViewProps> {
remove: ((doc: Doc) => boolean),
move: DragManager.MoveFunction,
dropAction: dropActionType,
- addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => boolean,
+ addDocTab: (doc: Doc, where: string) => boolean,
pinToPres: (document: Doc) => void,
+ backgroundColor: undefined | ((document: Doc) => string | undefined),
screenToLocalXf: () => Transform,
outerXf: () => { translateX: number, translateY: number },
active: (outsideReaction?: boolean) => boolean,
panelWidth: () => number,
ChromeHeight: undefined | (() => number),
renderDepth: number,
- hideHeaderFields: () => boolean,
- preventTreeViewOpen: boolean,
+ treeViewHideHeaderFields: () => boolean,
+ treeViewPreventOpen: boolean,
renderedIds: string[],
libraryPath: Doc[] | undefined,
- onCheckedClick: ScriptField | undefined
+ onCheckedClick: ScriptField | undefined,
+ onChildClick: ScriptField | undefined,
+ ignoreFields: string[] | undefined
) {
const viewSpecScript = Cast(containingCollection.viewSpecScript, ScriptField);
if (viewSpecScript) {
@@ -476,10 +529,8 @@ class TreeView extends React.Component<TreeViewProps> {
}
const docs = childDocs.slice();
- const dataExtension = containingCollection[key + "_ext"] as Doc;
- const ascending = dataExtension && BoolCast(dataExtension.sortAscending, null);
+ const ascending = containingCollection?.[key + "-sortAscending"];
if (ascending !== undefined) {
-
const sortAlphaNum = (a: string, b: string): 0 | 1 | -1 => {
const reN = /[0-9]*$/;
const aA = a.replace(reN, ""); // get rid of trailing numbers
@@ -560,9 +611,11 @@ class TreeView extends React.Component<TreeViewProps> {
indentDocument={indent}
outdentDocument={outdent}
onCheckedClick={onCheckedClick}
+ onChildClick={onChildClick}
renderDepth={renderDepth}
deleteDoc={remove}
addDocument={addDocument}
+ backgroundColor={backgroundColor}
panelWidth={rowWidth}
panelHeight={rowHeight}
ChromeHeight={ChromeHeight}
@@ -574,15 +627,23 @@ class TreeView extends React.Component<TreeViewProps> {
outerXf={outerXf}
parentKey={key}
active={active}
- hideHeaderFields={hideHeaderFields}
- preventTreeViewOpen={preventTreeViewOpen}
- renderedIds={renderedIds} />;
+ treeViewHideHeaderFields={treeViewHideHeaderFields}
+ treeViewPreventOpen={treeViewPreventOpen}
+ renderedIds={renderedIds}
+ ignoreFields={ignoreFields} />;
});
}
}
+export type collectionTreeViewProps = {
+ treeViewHideTitle?: boolean;
+ treeViewHideHeaderFields?: boolean;
+ onCheckedClick?: ScriptField;
+ onChildClick?: ScriptField;
+};
+
@observer
-export class CollectionTreeView extends CollectionSubView(Document) {
+export class CollectionTreeView extends CollectionSubView(Document, undefined as any as collectionTreeViewProps) {
private treedropDisposer?: DragManager.DragDropDisposer;
private _mainEle?: HTMLDivElement;
@@ -591,7 +652,7 @@ export class CollectionTreeView extends CollectionSubView(Document) {
protected createTreeDropTarget = (ele: HTMLDivElement) => {
this.treedropDisposer && this.treedropDisposer();
if (this._mainEle = ele) {
- this.treedropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this));
+ this.treedropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this));
}
}
@@ -602,13 +663,24 @@ export class CollectionTreeView extends CollectionSubView(Document) {
@action
remove = (document: Document): boolean => {
- const children = Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []);
+ const children = Cast(this.props.Document[DataSym][this.props.fieldKey], listSpec(Doc), []);
if (children.indexOf(document) !== -1) {
children.splice(children.indexOf(document), 1);
return true;
}
return false;
}
+ @action
+ addDoc = (doc: Document, relativeTo: Opt<Doc>, before?: boolean): boolean => {
+ const doAddDoc = () =>
+ Doc.AddDocToList(this.props.Document[DataSym], this.props.fieldKey, doc, relativeTo, before, false, false, false);
+ if (this.props.Document.resolvedDataDoc instanceof Promise) {
+ this.props.Document.resolvedDataDoc.then((resolved: any) => doAddDoc());
+ } else {
+ doAddDoc();
+ }
+ return true;
+ }
onContextMenu = (e: React.MouseEvent): void => {
// need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout
if (!e.isPropagationStopped() && this.props.Document === CurrentUserUtils.UserDocument.workspaces) {
@@ -624,44 +696,59 @@ export class CollectionTreeView extends CollectionSubView(Document) {
ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15);
} else {
const layoutItems: ContextMenuProps[] = [];
- layoutItems.push({ description: (this.props.Document.preventTreeViewOpen ? "Persist" : "Abandon") + "Treeview State", event: () => this.props.Document.preventTreeViewOpen = !this.props.Document.preventTreeViewOpen, icon: "paint-brush" });
- layoutItems.push({ description: (this.props.Document.hideHeaderFields ? "Show" : "Hide") + " Header Fields", event: () => this.props.Document.hideHeaderFields = !this.props.Document.hideHeaderFields, icon: "paint-brush" });
+ layoutItems.push({ description: (this.props.Document.treeViewPreventOpen ? "Persist" : "Abandon") + "Treeview State", event: () => this.props.Document.treeViewPreventOpen = !this.props.Document.treeViewPreventOpen, icon: "paint-brush" });
+ layoutItems.push({ description: (this.props.Document.treeViewHideHeaderFields ? "Show" : "Hide") + " Header Fields", event: () => this.props.Document.treeViewHideHeaderFields = !this.props.Document.treeViewHideHeaderFields, icon: "paint-brush" });
+ layoutItems.push({ description: (this.props.Document.treeViewHideTitle ? "Show" : "Hide") + " Title", event: () => this.props.Document.treeViewHideTitle = !this.props.Document.treeViewHideTitle, icon: "paint-brush" });
ContextMenu.Instance.addItem({ description: "Treeview Options ...", subitems: layoutItems, icon: "eye" });
}
ContextMenu.Instance.addItem({
description: "Buxton Layout", icon: "eye", event: () => {
DocListCast(this.dataDoc[this.props.fieldKey]).map(d => {
DocListCast(d.data).map((img, i) => {
- const caption = (d.captions as any)[i]?.data;
- if (caption instanceof ObjectField) {
- Doc.GetProto(img).caption = ObjectField.MakeCopy(caption as ObjectField);
+ const caption = (d.captions as any)[i];
+ if (caption) {
+ Doc.GetProto(img).caption = caption;
}
- img._hideSidebar = true;
- d.captions = undefined;
});
});
- const { TextDocument, ImageDocument, CarouselDocument } = Docs.Create;
+ const { TextDocument, ImageDocument, CarouselDocument, TreeDocument } = Docs.Create;
const { Document } = this.props;
const fallbackImg = "http://www.cs.brown.edu/~bcz/face.gif";
- const detailedTemplate = `{ "doc": { "type": "doc", "content": [ { "type": "paragraph", "content": [ { "type": "dashField", "attrs": { "fieldKey": "short_description" } } ] }, { "type": "paragraph", "content": [ { "type": "dashField", "attrs": { "fieldKey": "year" } } ] }, { "type": "paragraph", "content": [ { "type": "dashField", "attrs": { "fieldKey": "company" } } ] } ] }, "selection":{"type":"text","anchor":1,"head":1},"storedMarks":[] }`;
+ const detailedTemplate = `{ "doc": { "type": "doc", "content": [ { "type": "paragraph", "content": [ { "type": "dashField", "attrs": { "fieldKey": "year" } } ] }, { "type": "paragraph", "content": [ { "type": "dashField", "attrs": { "fieldKey": "company" } } ] } ] }, "selection":{"type":"text","anchor":1,"head":1},"storedMarks":[] }`;
const textDoc = TextDocument("", { title: "details", _autoHeight: true });
- const detailedLayout = Docs.Create.StackingDocument([
+ const detailView = Docs.Create.StackingDocument([
CarouselDocument([], { title: "data", _height: 350, _itemIndex: 0, backgroundColor: "#9b9b9b3F" }),
textDoc,
- ], { _chromeStatus: "disabled", title: "detailed layout stack" });
- textDoc.data = new RichTextField(detailedTemplate, "short_description year company");
- detailedLayout.isTemplateDoc = makeTemplate(detailedLayout);
-
- const cardLayout = ImageDocument(fallbackImg, { title: "cardLayout", isTemplateDoc: true, isTemplateForField: "hero", }); // this acts like a template doc and a template field ... a little weird, but seems to work?
- cardLayout.proto!.layout = ImageBox.LayoutString("hero");
- cardLayout.showTitle = "title";
- cardLayout.showTitleHover = "titlehover";
-
- Document.childLayout = cardLayout;
- Document.childDetailed = detailedLayout;
- Document._viewType = CollectionViewType.Pivot;
- Document.pivotField = "company";
+ TextDocument("", { title: "shortDescription", _autoHeight: true }),
+ TreeDocument([], { title: "narratives", _height: 75, treeViewHideTitle: true })
+ ], { _chromeStatus: "disabled", _width: 300, _height: 300, _autoHeight: true, title: "detailView" });
+ textDoc.data = new RichTextField(detailedTemplate, "year company");
+ detailView.isTemplateDoc = makeTemplate(detailView);
+
+ const heroView = ImageDocument(fallbackImg, { title: "heroView", isTemplateDoc: true, isTemplateForField: "hero", }); // this acts like a template doc and a template field ... a little weird, but seems to work?
+ heroView.proto!.layout = ImageBox.LayoutString("hero");
+ heroView._showTitle = "title";
+ heroView._showTitleHover = "titlehover";
+
+ Doc.AddDocToList(CurrentUserUtils.UserDocument.expandingButtons as Doc, "data",
+ Docs.Create.FontIconDocument({
+ _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, dropAction: "alias", onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'),
+ dragFactory: heroView, removeDropProperties: new List<string>(["dropAction"]), title: "hero view", icon: "portrait"
+ }));
+
+ Doc.AddDocToList(CurrentUserUtils.UserDocument.expandingButtons as Doc, "data",
+ Docs.Create.FontIconDocument({
+ _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, dropAction: "alias", onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'),
+ dragFactory: detailView, removeDropProperties: new List<string>(["dropAction"]), title: "detail view", icon: "file-alt"
+ }));
+
+ Document.childLayout = heroView;
+ Document.childDetailed = detailView;
+ Document._viewType = CollectionViewType.Time;
+ Document._forceActive = true;
+ Document._pivotField = "company";
+ Document.childDropAction = "alias";
}
});
const existingOnClick = ContextMenu.Instance.findByDescription("OnClick...");
@@ -673,7 +760,7 @@ export class CollectionTreeView extends CollectionSubView(Document) {
!existingOnClick && ContextMenu.Instance.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" });
}
outerXf = () => Utils.GetScreenTransform(this._mainEle!);
- onTreeDrop = (e: React.DragEvent) => this.onDrop(e, {});
+ onTreeDrop = (e: React.DragEvent) => this.onExternalDrop(e, {});
@computed get renderClearButton() {
return <div id="toolbar" key="toolbar">
@@ -685,18 +772,25 @@ export class CollectionTreeView extends CollectionSubView(Document) {
}
render() {
- const dropAction = StrCast(this.props.Document._dropAction) as dropActionType;
- const addDoc = (doc: Doc, relativeTo?: Doc, before?: boolean) => Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, relativeTo, before, false, false, false);
+ const dropAction = StrCast(this.props.Document.childDropAction) as dropActionType;
+ const addDoc = (doc: Doc, relativeTo?: Doc, before?: boolean) => this.addDoc(doc, relativeTo, before);
const moveDoc = (d: Doc, target: Doc | undefined, addDoc: (doc: Doc) => boolean) => this.props.moveDocument(d, target, addDoc);
- return !this.childDocs ? (null) : (
+ const childDocs = this.props.overrideDocuments ? this.props.overrideDocuments : this.childDocs;
+ return !childDocs ? (null) : (
<div className="collectionTreeView-dropTarget" id="body"
- style={{ background: StrCast(this.props.Document.backgroundColor, "lightgray"), paddingTop: `${NumCast(this.props.Document._yMargin, 20)}px` }}
+ style={{
+ background: this.props.backgroundColor?.(this.props.Document),
+ paddingLeft: `${NumCast(this.props.Document._xPadding, 10)}px`,
+ paddingRight: `${NumCast(this.props.Document._xPadding, 10)}px`,
+ paddingTop: `${NumCast(this.props.Document._yPadding, 20)}px`
+ }}
onContextMenu={this.onContextMenu}
onWheel={(e: React.WheelEvent) => this._mainEle && this._mainEle.scrollHeight > this._mainEle.clientHeight && e.stopPropagation()}
onDrop={this.onTreeDrop}
ref={this.createTreeDropTarget}>
- {(this.props.Document.treeViewHideTitle ? (null) : <EditableView
+ {(this.props.treeViewHideTitle || this.props.Document.treeViewHideTitle ? (null) : <EditableView
contents={this.dataDoc.title}
+ editing={false}
display={"block"}
maxHeight={72}
height={"auto"}
@@ -704,18 +798,18 @@ export class CollectionTreeView extends CollectionSubView(Document) {
SetValue={undoBatch((value: string) => Doc.SetInPlace(this.dataDoc, "title", value, false) || true)}
OnFillDown={undoBatch((value: string) => {
Doc.SetInPlace(this.dataDoc, "title", value, false);
- const layoutDoc = this.props.Document.layout_custom instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.Document.layout_custom)) : undefined;
- const doc = layoutDoc || Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, _width: 100, _height: 25, templates: new List<string>([Templates.Title.Layout]) });
- TreeView.loadId = doc[Id];
- Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, this.childDocs.length ? this.childDocs[0] : undefined, true, false, false, false);
+ const doc = Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, _width: 100, _height: 25, templates: new List<string>([Templates.Title.Layout]) });
+ EditableView.loadId = doc[Id];
+ this.addDoc(doc, childDocs.length ? childDocs[0] : undefined, true);
})} />)}
{this.props.Document.allowClear ? this.renderClearButton : (null)}
<ul className="no-indent" style={{ width: "max-content" }} >
{
- TreeView.GetChildElements(this.childDocs, this.props.Document, this.props.Document, this.props.DataDoc, this.props.fieldKey, this.props.ContainingCollectionDoc, undefined, addDoc, this.remove,
- moveDoc, dropAction, this.props.addDocTab, this.props.pinToPres, this.props.ScreenToLocalTransform,
- this.outerXf, this.props.active, this.props.PanelWidth, this.props.ChromeHeight, this.props.renderDepth, () => BoolCast(this.props.Document.hideHeaderFields),
- BoolCast(this.props.Document.preventTreeViewOpen), [], this.props.LibraryPath, ScriptCast(this.props.Document.onCheckedClick))
+ TreeView.GetChildElements(childDocs, this.props.Document, this.props.Document, this.props.DataDoc, this.props.fieldKey, this.props.ContainingCollectionDoc, undefined, addDoc, this.remove,
+ moveDoc, dropAction, this.props.addDocTab, this.props.pinToPres, this.props.backgroundColor, this.props.ScreenToLocalTransform,
+ this.outerXf, this.props.active, this.props.PanelWidth, this.props.ChromeHeight, this.props.renderDepth, () => this.props.treeViewHideHeaderFields || BoolCast(this.props.Document.treeViewHideHeaderFields),
+ BoolCast(this.props.Document.treeViewPreventOpen), [], this.props.LibraryPath, this.props.onCheckedClick || ScriptCast(this.props.Document.onCheckedClick),
+ this.props.onChildClick || ScriptCast(this.props.Document.onChildClick), this.props.ignoreFields)
}
</ul>
</div >
@@ -728,7 +822,14 @@ Scripting.addGlobal(function readFacetData(layoutDoc: Doc, dataDoc: Doc, dataKey
const facetValues = Array.from(allCollectionDocs.reduce((set, child) =>
set.add(Field.toString(child[facetHeader] as Field)), new Set<string>()));
- const facetValueDocSet = facetValues.sort().map(facetValue =>
+ let nonNumbers = 0;
+ facetValues.map(val => {
+ const num = Number(val);
+ if (Number.isNaN(num)) {
+ nonNumbers++;
+ }
+ });
+ const facetValueDocSet = (nonNumbers / facetValues.length > .1 ? facetValues.sort() : facetValues.sort((n1: string, n2: string) => Number(n1) - Number(n2))).map(facetValue =>
Docs.Create.TextDocument("", {
title: facetValue.toString(),
treeViewChecked: ComputedField.MakeFunction("determineCheckedState(layoutDoc, facetHeader, facetValue)",
@@ -739,7 +840,7 @@ Scripting.addGlobal(function readFacetData(layoutDoc: Doc, dataDoc: Doc, dataKey
});
Scripting.addGlobal(function determineCheckedState(layoutDoc: Doc, facetHeader: string, facetValue: string) {
- const docFilters = Cast(layoutDoc._docFilter, listSpec("string"), []);
+ const docFilters = Cast(layoutDoc._docFilters, listSpec("string"), []);
for (let i = 0; i < docFilters.length; i += 3) {
const [header, value, state] = docFilters.slice(i, i + 3);
if (header === facetHeader && value === facetValue) {
diff --git a/src/client/views/collections/CollectionView.scss b/src/client/views/collections/CollectionView.scss
index 1c46081a1..b92c5fdd1 100644
--- a/src/client/views/collections/CollectionView.scss
+++ b/src/client/views/collections/CollectionView.scss
@@ -10,6 +10,59 @@
width: 100%;
height: 100%;
overflow: hidden; // bcz: used to be 'auto' which would create scrollbars when there's a floating doc that's not visible. not sure if that's better, but the scrollbars are annoying...
+
+
+ .collectionTimeView-dragger {
+ background-color: lightgray;
+ height: 40px;
+ width: 20px;
+ position: absolute;
+ border-radius: 10px;
+ top: 55%;
+ border: 1px black solid;
+ z-index: 2;
+ left: -10px;
+ }
+ .collectionTimeView-treeView {
+ display: flex;
+ flex-direction: column;
+ width: 200px;
+ height: 100%;
+ position: absolute;
+ left: 0;
+ top: 0;
+
+ .collectionTimeView-addfacet {
+ display: inline-block;
+ width: 200px;
+ height: 30px;
+ background: darkGray;
+ text-align: left;
+
+ .collectionTimeView-button {
+ align-items: center;
+ display: flex;
+ width: 100%;
+ height: 100%;
+
+ .collectionTimeView-span {
+ margin: auto;
+ }
+ }
+
+ >div,
+ >div>div {
+ width: 100%;
+ height: 100%;
+ }
+ }
+
+ .collectionTimeView-tree {
+ display: inline-block;
+ width: 100%;
+ height: calc(100% - 30px);
+ }
+ }
}
#google-tags {
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index dab0ce08e..df1770ffe 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -1,20 +1,19 @@
import { library } from '@fortawesome/fontawesome-svg-core';
-import { faEye } from '@fortawesome/free-regular-svg-icons';
+import { faEye, faEdit } from '@fortawesome/free-regular-svg-icons';
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faColumns, faCopy, faEllipsisV, faFingerprint, faImage, faProjectDiagram, faSignature, faSquare, faTh, faThList, faTree } from '@fortawesome/free-solid-svg-icons';
-import { action, IReactionDisposer, observable, reaction, runInAction, computed } from 'mobx';
+import { action, observable, computed } from 'mobx';
import { observer } from "mobx-react";
import * as React from 'react';
import Lightbox from 'react-image-lightbox-with-rotate';
import 'react-image-lightbox-with-rotate/style.css'; // This only needs to be imported once in your app
import { DateField } from '../../../new_fields/DateField';
-import { Doc, DocListCast } from '../../../new_fields/Doc';
-import { Id } from '../../../new_fields/FieldSymbols';
-import { listSpec } from '../../../new_fields/Schema';
-import { BoolCast, Cast, StrCast, NumCast } from '../../../new_fields/Types';
+import { DataSym, Doc, DocListCast, Field, Opt } from '../../../new_fields/Doc';
+import { List } from '../../../new_fields/List';
+import { BoolCast, Cast, NumCast, StrCast } from '../../../new_fields/Types';
import { ImageField } from '../../../new_fields/URLField';
import { TraceMobx } from '../../../new_fields/util';
-import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils';
-import { Utils } from '../../../Utils';
+import { Utils, setupMoveUpEvents, returnFalse } from '../../../Utils';
import { DocumentType } from '../../documents/DocumentTypes';
import { DocumentManager } from '../../util/DocumentManager';
import { ImageUtils } from '../../util/Import & Export/ImageUtils';
@@ -23,19 +22,29 @@ import { ContextMenu } from "../ContextMenu";
import { FieldView, FieldViewProps } from '../nodes/FieldView';
import { ScriptBox } from '../ScriptBox';
import { Touchable } from '../Touchable';
+import { CollectionCarouselView } from './CollectionCarouselView';
import { CollectionDockingView } from "./CollectionDockingView";
import { AddCustomFreeFormLayout } from './collectionFreeForm/CollectionFreeFormLayoutEngines';
import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView';
-import { CollectionCarouselView } from './CollectionCarouselView';
import { CollectionLinearView } from './CollectionLinearView';
import { CollectionMulticolumnView } from './collectionMulticolumn/CollectionMulticolumnView';
-import { CollectionPivotView } from './CollectionPivotView';
+import { CollectionMultirowView } from './collectionMulticolumn/CollectionMultirowView';
import { CollectionSchemaView } from "./CollectionSchemaView";
import { CollectionStackingView } from './CollectionStackingView';
import { CollectionStaffView } from './CollectionStaffView';
+import { SubCollectionViewProps } from './CollectionSubView';
+import { CollectionTimeView } from './CollectionTimeView';
import { CollectionTreeView } from "./CollectionTreeView";
import './CollectionView.scss';
import { CollectionViewBaseChrome } from './CollectionViewChromes';
+import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils';
+import { Id } from '../../../new_fields/FieldSymbols';
+import { listSpec } from '../../../new_fields/Schema';
+import { Docs } from '../../documents/Documents';
+import { ScriptField, ComputedField } from '../../../new_fields/ScriptField';
+const higflyout = require("@hig/flyout");
+export const { anchorPoints } = higflyout;
+export const Flyout = higflyout.default;
export const COLLECTION_BORDER_WIDTH = 2;
const path = require('path');
library.add(faTh, faTree, faSquare, faProjectDiagram, faSignature, faThList, faFingerprint, faColumns, faEllipsisV, faImage, faEye as any, faCopy);
@@ -49,11 +58,11 @@ export enum CollectionViewType {
Stacking,
Masonry,
Multicolumn,
- Pivot,
+ Multirow,
+ Time,
Carousel,
Linear,
- Staff,
- Timeline
+ Staff
}
export namespace CollectionViewType {
@@ -66,12 +75,14 @@ export namespace CollectionViewType {
["stacking", CollectionViewType.Stacking],
["masonry", CollectionViewType.Masonry],
["multicolumn", CollectionViewType.Multicolumn],
- ["pivot", CollectionViewType.Pivot],
+ ["multirow", CollectionViewType.Multirow],
+ ["time", CollectionViewType.Time],
["carousel", CollectionViewType.Carousel],
["linear", CollectionViewType.Linear],
]);
export const valueOf = (value: string) => stringMapping.get(value.toLowerCase());
+ export const stringFor = (value: number) => Array.from(stringMapping.entries()).find(entry => entry[1] === value)?.[0];
}
export interface CollectionRenderProps {
@@ -80,17 +91,16 @@ export interface CollectionRenderProps {
moveDocument: (document: Doc, targetCollection: Doc | undefined, addDocument: (document: Doc) => boolean) => boolean;
active: () => boolean;
whenActiveChanged: (isActive: boolean) => void;
+ PanelWidth: () => number;
}
@observer
export class CollectionView extends Touchable<FieldViewProps> {
public static LayoutString(fieldStr: string) { return FieldView.LayoutString(CollectionView, fieldStr); }
- private _reactionDisposer: IReactionDisposer | undefined;
private _isChildActive = false; //TODO should this be observable?
@observable private _isLightboxOpen = false;
@observable private _curLightboxImg = 0;
- @observable private _collapsed = true;
@observable private static _safeMode = false;
public static SetSafeMode(safeMode: boolean) { this._safeMode = safeMode; }
@@ -107,31 +117,17 @@ export class CollectionView extends Touchable<FieldViewProps> {
return viewField;
}
- componentDidMount = () => {
- this._reactionDisposer = reaction(() => StrCast(this.props.Document._chromeStatus),
- () => {
- // chrome status is one of disabled, collapsed, or visible. this determines initial state from document
- // chrome status may also be view-mode, in reference to stacking view's toggle mode. it is essentially disabled mode, but prevents the toggle button from showing up on the left sidebar.
- const chromeStatus = this.props.Document._chromeStatus;
- if (chromeStatus && (chromeStatus === "disabled" || chromeStatus === "collapsed")) {
- runInAction(() => this._collapsed = true);
- }
- });
- }
-
- componentWillUnmount = () => this._reactionDisposer && this._reactionDisposer();
-
- // bcz: Argh? What's the height of the collection chromes??
- chromeHeight = () => (this.props.Document._chromeStatus === "enabled" ? -60 : 0);
-
active = (outsideReaction?: boolean) => this.props.isSelected(outsideReaction) || BoolCast(this.props.Document.forceActive) || this._isChildActive || this.props.renderDepth === 0;
whenActiveChanged = (isActive: boolean) => { this.props.whenActiveChanged(this._isChildActive = isActive); };
@action.bound
addDocument(doc: Doc): boolean {
- const targetDataDoc = Doc.GetProto(this.props.Document);
- Doc.AddDocToList(targetDataDoc, this.props.fieldKey, doc);
+ const targetDataDoc = this.props.Document[DataSym];
+ const docList = DocListCast(targetDataDoc[this.props.fieldKey]);
+ !docList.includes(doc) && (targetDataDoc[this.props.fieldKey] = new List<Doc>([...docList, doc])); // DocAddToList may write to targetdataDoc's parent ... we don't want this. should really change GetProto to GetDataDoc and test for resolvedDataDoc there
+ // Doc.AddDocToList(targetDataDoc, this.props.fieldKey, doc);
+ doc.context = this.props.Document;
targetDataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now()));
Doc.GetProto(doc).lastOpened = new DateField;
return true;
@@ -139,15 +135,18 @@ export class CollectionView extends Touchable<FieldViewProps> {
@action.bound
removeDocument(doc: Doc): boolean {
+ const targetDataDoc = this.props.Document[DataSym];
const docView = DocumentManager.Instance.getDocumentView(doc, this.props.ContainingCollectionView);
docView && SelectionManager.DeselectDoc(docView);
- const value = Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []);
+ const value = DocListCast(targetDataDoc[this.props.fieldKey]);
let index = value.reduce((p, v, i) => (v instanceof Doc && v === doc) ? i : p, -1);
index = index !== -1 ? index : value.reduce((p, v, i) => (v instanceof Doc && Doc.AreProtosEqual(v, doc)) ? i : p, -1);
- ContextMenu.Instance.clearItems();
+ doc.context = undefined;
+ ContextMenu.Instance?.clearItems();
if (index !== -1) {
value.splice(index, 1);
+ targetDataDoc[this.props.fieldKey] = new List<Doc>(value);
return true;
}
return false;
@@ -173,18 +172,19 @@ export class CollectionView extends Touchable<FieldViewProps> {
}
private SubViewHelper = (type: CollectionViewType, renderProps: CollectionRenderProps) => {
- const props = { ...this.props, ...renderProps, chromeCollapsed: this._collapsed, ChromeHeight: this.chromeHeight, CollectionView: this, annotationsKey: "" };
+ const props: SubCollectionViewProps = { ...this.props, ...renderProps, CollectionView: this, annotationsKey: "" };
switch (type) {
case CollectionViewType.Schema: return (<CollectionSchemaView key="collview" {...props} />);
case CollectionViewType.Docking: return (<CollectionDockingView key="collview" {...props} />);
case CollectionViewType.Tree: return (<CollectionTreeView key="collview" {...props} />);
- case CollectionViewType.Staff: return (<CollectionStaffView chromeCollapsed={true} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />);
- case CollectionViewType.Multicolumn: return (<CollectionMulticolumnView chromeCollapsed={true} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />);
+ case CollectionViewType.Staff: return (<CollectionStaffView key="collview" {...props} />);
+ case CollectionViewType.Multicolumn: return (<CollectionMulticolumnView key="collview" {...props} />);
+ case CollectionViewType.Multirow: return (<CollectionMultirowView key="rpwview" {...props} />);
case CollectionViewType.Linear: { return (<CollectionLinearView key="collview" {...props} />); }
case CollectionViewType.Carousel: { return (<CollectionCarouselView key="collview" {...props} />); }
case CollectionViewType.Stacking: { this.props.Document.singleColumn = true; return (<CollectionStackingView key="collview" {...props} />); }
case CollectionViewType.Masonry: { this.props.Document.singleColumn = false; return (<CollectionStackingView key="collview" {...props} />); }
- case CollectionViewType.Pivot: { return (<CollectionPivotView key="collview" {...props} />); }
+ case CollectionViewType.Time: { return (<CollectionTimeView key="collview" {...props} />); }
case CollectionViewType.Freeform:
default: { this.props.Document._freeformLayoutEngine = undefined; return (<CollectionFreeFormView key="collview" {...props} />); }
}
@@ -192,14 +192,13 @@ export class CollectionView extends Touchable<FieldViewProps> {
@action
private collapse = (value: boolean) => {
- this._collapsed = value;
this.props.Document._chromeStatus = value ? "collapsed" : "enabled";
}
private SubView = (type: CollectionViewType, renderProps: CollectionRenderProps) => {
// currently cant think of a reason for collection docking view to have a chrome. mind may change if we ever have nested docking views -syip
- const chrome = this.props.Document._chromeStatus === "disabled" || type === CollectionViewType.Docking ? (null) :
- <CollectionViewBaseChrome CollectionView={this} key="chrome" type={type} collapse={this.collapse} />;
+ const chrome = this.props.Document._chromeStatus === "disabled" || this.props.Document._chromeStatus === "replaced" || type === CollectionViewType.Docking ? (null) :
+ <CollectionViewBaseChrome CollectionView={this} key="chrome" PanelWidth={this.bodyPanelWidth} type={type} collapse={this.collapse} />;
return [chrome, this.SubViewHelper(type, renderProps)];
}
@@ -223,9 +222,10 @@ export class CollectionView extends Touchable<FieldViewProps> {
});
subItems.push({ description: "Staff", event: () => this.props.Document._viewType = CollectionViewType.Staff, icon: "music" });
subItems.push({ description: "Multicolumn", event: () => this.props.Document._viewType = CollectionViewType.Multicolumn, icon: "columns" });
+ subItems.push({ description: "Multirow", event: () => this.props.Document._viewType = CollectionViewType.Multirow, icon: "columns" });
subItems.push({ description: "Masonry", event: () => this.props.Document._viewType = CollectionViewType.Masonry, icon: "columns" });
subItems.push({ description: "Carousel", event: () => this.props.Document._viewType = CollectionViewType.Carousel, icon: "columns" });
- subItems.push({ description: "Pivot", event: () => this.props.Document._viewType = CollectionViewType.Pivot, icon: "columns" });
+ subItems.push({ description: "Pivot/Time", event: () => this.props.Document._viewType = CollectionViewType.Time, icon: "columns" });
switch (this.props.Document._viewType) {
case CollectionViewType.Freeform: {
subItems.push({ description: "Custom", icon: "fingerprint", event: AddCustomFreeFormLayout(this.props.Document, this.props.fieldKey) });
@@ -239,22 +239,26 @@ export class CollectionView extends Touchable<FieldViewProps> {
const layoutItems = existing && "subitems" in existing ? existing.subitems : [];
layoutItems.push({ description: `${this.props.Document.forceActive ? "Select" : "Force"} Contents Active`, event: () => this.props.Document.forceActive = !this.props.Document.forceActive, icon: "project-diagram" });
if (this.props.Document.childLayout instanceof Doc) {
- layoutItems.push({ description: "View Child Layout", event: () => this.props.addDocTab(this.props.Document.childLayout as Doc, undefined, "onRight"), icon: "project-diagram" });
+ layoutItems.push({ description: "View Child Layout", event: () => this.props.addDocTab(this.props.Document.childLayout as Doc, "onRight"), icon: "project-diagram" });
}
if (this.props.Document.childDetailed instanceof Doc) {
- layoutItems.push({ description: "View Child Detailed Layout", event: () => this.props.addDocTab(this.props.Document.childDetailed as Doc, undefined, "onRight"), icon: "project-diagram" });
+ layoutItems.push({ description: "View Child Detailed Layout", event: () => this.props.addDocTab(this.props.Document.childDetailed as Doc, "onRight"), icon: "project-diagram" });
}
!existing && ContextMenu.Instance.addItem({ description: "Layout...", subitems: layoutItems, icon: "hand-point-right" });
- const more = ContextMenu.Instance.findByDescription("More...");
- const moreItems = more && "subitems" in more ? more.subitems : [];
- moreItems.push({ description: "Export Image Hierarchy", icon: "columns", event: () => ImageUtils.ExportHierarchyToFileSystem(this.props.Document) });
- !more && ContextMenu.Instance.addItem({ description: "More...", subitems: moreItems, icon: "hand-point-right" });
+ const open = ContextMenu.Instance.findByDescription("Open...");
+ const openItems = open && "subitems" in open ? open.subitems : [];
+ !open && ContextMenu.Instance.addItem({ description: "Open...", subitems: openItems, icon: "hand-point-right" });
const existingOnClick = ContextMenu.Instance.findByDescription("OnClick...");
const onClicks = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : [];
onClicks.push({ description: "Edit onChildClick script", icon: "edit", event: (obj: any) => ScriptBox.EditButtonScript("On Child Clicked...", this.props.Document, "onChildClick", obj.x, obj.y) });
!existingOnClick && ContextMenu.Instance.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" });
+
+ const more = ContextMenu.Instance.findByDescription("More...");
+ const moreItems = more && "subitems" in more ? more.subitems : [];
+ moreItems.push({ description: "Export Image Hierarchy", icon: "columns", event: () => ImageUtils.ExportHierarchyToFileSystem(this.props.Document) });
+ !more && ContextMenu.Instance.addItem({ description: "More...", subitems: moreItems, icon: "hand-point-right" });
}
}
@@ -274,6 +278,164 @@ export class CollectionView extends Touchable<FieldViewProps> {
onMovePrevRequest={action(() => this._curLightboxImg = (this._curLightboxImg + images.length - 1) % images.length)}
onMoveNextRequest={action(() => this._curLightboxImg = (this._curLightboxImg + 1) % images.length)} />);
}
+ @observable _facetWidth = 0;
+
+ bodyPanelWidth = () => this.props.PanelWidth() - this.facetWidth();
+ getTransform = () => this.props.ScreenToLocalTransform().translate(-this.facetWidth(), 0);
+ facetWidth = () => Math.min(this.props.PanelWidth() - 25, this._facetWidth);
+
+ @computed get dataDoc() {
+ return (this.props.DataDoc && this.props.Document.isTemplateForField ? Doc.GetProto(this.props.DataDoc) :
+ this.props.Document.resolvedDataDoc ? this.props.Document : Doc.GetProto(this.props.Document)); // if the layout document has a resolvedDataDoc, then we don't want to get its parent which would be the unexpanded template
+ }
+ // The data field for rendering this collection will be on the this.props.Document unless we're rendering a template in which case we try to use props.DataDoc.
+ // When a document has a DataDoc but it's not a template, then it contains its own rendering data, but needs to pass the DataDoc through
+ // to its children which may be templates.
+ // If 'annotationField' is specified, then all children exist on that field of the extension document, otherwise, they exist directly on the data document under 'fieldKey'
+ @computed get dataField() {
+ return this.dataDoc[this.props.fieldKey];
+ }
+
+ get childLayoutPairs(): { layout: Doc; data: Doc; }[] {
+ const { Document, DataDoc } = this.props;
+ const validPairs = this.childDocs.map(doc => Doc.GetLayoutDataDocPair(Document, DataDoc, doc)).filter(pair => pair.layout);
+ return validPairs.map(({ data, layout }) => ({ data: data as Doc, layout: layout! })); // this mapping is a bit of a hack to coerce types
+ }
+ get childDocList() {
+ return Cast(this.dataField, listSpec(Doc));
+ }
+ get childDocs() {
+ const dfield = this.dataField;
+ const rawdocs = (dfield instanceof Doc) ? [dfield] : Cast(dfield, listSpec(Doc), Cast(this.props.Document.rootDocument, Doc, null) ? [Cast(this.props.Document.rootDocument, Doc, null)] : []);
+ const docs = rawdocs.filter(d => !(d instanceof Promise)).map(d => d as Doc);
+ const viewSpecScript = Cast(this.props.Document.viewSpecScript, ScriptField);
+ return viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs;
+ }
+ @computed get _allFacets() {
+ const facets = new Set<string>();
+ this.childDocs.filter(child => child).forEach(child => Object.keys(Doc.GetProto(child)).forEach(key => facets.add(key)));
+ Doc.AreProtosEqual(this.dataDoc, this.props.Document) && this.childDocs.filter(child => child).forEach(child => Object.keys(child).forEach(key => facets.add(key)));
+ return Array.from(facets);
+ }
+
+ /**
+ * Responds to clicking the check box in the flyout menu
+ */
+ facetClick = (facetHeader: string) => {
+ const facetCollection = this.props.Document;
+ const found = DocListCast(facetCollection[this.props.fieldKey + "-filter"]).findIndex(doc => doc.title === facetHeader);
+ if (found !== -1) {
+ (facetCollection[this.props.fieldKey + "-filter"] as List<Doc>).splice(found, 1);
+ const docFilter = Cast(this.props.Document._docFilters, listSpec("string"));
+ if (docFilter) {
+ let index: number;
+ while ((index = docFilter.findIndex(item => item === facetHeader)) !== -1) {
+ docFilter.splice(index, 3);
+ }
+ }
+ const docRangeFilters = Cast(this.props.Document._docRangeFilters, listSpec("string"));
+ if (docRangeFilters) {
+ let index: number;
+ while ((index = docRangeFilters.findIndex(item => item === facetHeader)) !== -1) {
+ docRangeFilters.splice(index, 3);
+ }
+ }
+ } else {
+ const allCollectionDocs = DocListCast(this.dataDoc[this.props.fieldKey]);
+ const facetValues = Array.from(allCollectionDocs.reduce((set, child) =>
+ set.add(Field.toString(child[facetHeader] as Field)), new Set<string>()));
+
+ let nonNumbers = 0;
+ let minVal = Number.MAX_VALUE, maxVal = -Number.MAX_VALUE;
+ facetValues.map(val => {
+ const num = Number(val);
+ if (Number.isNaN(num)) {
+ nonNumbers++;
+ } else {
+ minVal = Math.min(num, minVal);
+ maxVal = Math.max(num, maxVal);
+ }
+ });
+ let newFacet: Opt<Doc>;
+ if (nonNumbers / allCollectionDocs.length < .1) {
+ newFacet = Docs.Create.SliderDocument({ title: facetHeader });
+ const ranged = Doc.readDocRangeFilter(this.props.Document, facetHeader);
+ Doc.GetProto(newFacet).type = DocumentType.COL; // forces item to show an open/close button instead ofa checkbox
+ newFacet.treeViewExpandedView = "layout";
+ newFacet.treeViewOpen = true;
+ newFacet._sliderMin = ranged === undefined ? minVal : ranged[0];
+ newFacet._sliderMax = ranged === undefined ? maxVal : ranged[1];
+ newFacet._sliderMinThumb = minVal;
+ newFacet._sliderMaxThumb = maxVal;
+ newFacet.target = this.props.Document;
+ const scriptText = `setDocFilterRange(this.target, "${facetHeader}", range)`;
+ newFacet.onThumbChanged = ScriptField.MakeScript(scriptText, { this: Doc.name, range: "number" });
+
+ Doc.AddDocToList(facetCollection, this.props.fieldKey + "-filter", newFacet);
+ } else {
+ newFacet = Docs.Create.TreeDocument([], { title: facetHeader, treeViewOpen: true, isFacetFilter: true });
+ const capturedVariables = { layoutDoc: this.props.Document, dataDoc: this.dataDoc };
+ const params = { layoutDoc: Doc.name, dataDoc: Doc.name, };
+ newFacet.data = ComputedField.MakeFunction(`readFacetData(layoutDoc, dataDoc, "${this.props.fieldKey}", "${facetHeader}")`, params, capturedVariables);
+ }
+ Doc.AddDocToList(facetCollection, this.props.fieldKey + "-filter", newFacet);
+ }
+ }
+
+
+ onPointerDown = (e: React.PointerEvent) => {
+ setupMoveUpEvents(this, e, action((e: PointerEvent, down: number[], delta: number[]) => {
+ this._facetWidth = Math.max(this.props.ScreenToLocalTransform().transformPoint(e.clientX, 0)[0], 0);
+ return false;
+ }), returnFalse, action(() => this._facetWidth = this.facetWidth() < 15 ? Math.min(this.props.PanelWidth() - 25, 200) : 0));
+ }
+ filterBackground = () => "dimGray";
+ @computed get scriptField() {
+ const scriptText = "setDocFilter(containingTreeView, heading, this.title, checked)";
+ return ScriptField.MakeScript(scriptText, { this: Doc.name, heading: "string", checked: "string", containingTreeView: Doc.name });
+ }
+ @computed get filterView() {
+ const facetCollection = this.props.Document;
+ const flyout = (
+ <div className="collectionTimeView-flyout" style={{ width: `${this.facetWidth()}`, height: this.props.PanelHeight() - 30 }} onWheel={e => e.stopPropagation()}>
+ {this._allFacets.map(facet => <label className="collectionTimeView-flyout-item" key={`${facet}`} onClick={e => this.facetClick(facet)}>
+ <input type="checkbox" onChange={e => { }} checked={DocListCast(this.props.Document[this.props.fieldKey + "-filter"]).some(d => d.title === facet)} />
+ <span className="checkmark" />
+ {facet}
+ </label>)}
+ </div>
+ );
+ return !this._facetWidth || this.props.dontRegisterView ? (null) :
+ <div className="collectionTimeView-treeView" style={{ width: `${this.facetWidth()}px`, overflow: this.facetWidth() < 15 ? "hidden" : undefined }}>
+ <div className="collectionTimeView-addFacet" style={{ width: `${this.facetWidth()}px` }} onPointerDown={e => e.stopPropagation()}>
+ <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={flyout}>
+ <div className="collectionTimeView-button">
+ <span className="collectionTimeView-span">Facet Filters</span>
+ <FontAwesomeIcon icon={faEdit} size={"lg"} />
+ </div>
+ </Flyout>
+ </div>
+ <div className="collectionTimeView-tree" key="tree">
+ <CollectionTreeView {...this.props}
+ CollectionView={this}
+ treeViewHideTitle={true}
+ treeViewHideHeaderFields={true}
+ onCheckedClick={this.scriptField!}
+ ignoreFields={["_facetCollection", "_docFilters"]}
+ annotationsKey={""}
+ dontRegisterView={true}
+ PanelWidth={this.facetWidth}
+ DataDoc={facetCollection}
+ Document={facetCollection}
+ backgroundColor={this.filterBackground}
+ fieldKey={`${this.props.fieldKey}-filter`}
+ moveDocument={returnFalse}
+ removeDocument={returnFalse}
+ addDocument={returnFalse} />
+ </div>
+ </div>;
+ }
+
render() {
TraceMobx();
const props: CollectionRenderProps = {
@@ -282,21 +444,31 @@ export class CollectionView extends Touchable<FieldViewProps> {
moveDocument: this.moveDocument,
active: this.active,
whenActiveChanged: this.whenActiveChanged,
+ PanelWidth: this.bodyPanelWidth
};
return (<div className={"collectionView"}
style={{
pointerEvents: this.props.Document.isBackground ? "none" : "all",
- boxShadow: this.props.Document.isBackground || this.collectionViewType === CollectionViewType.Linear ? undefined : `#9c9396 ${StrCast(this.props.Document.boxShadow, "0.2vw 0.2vw 0.8vw")}`
+ boxShadow: this.props.Document.isBackground || this.collectionViewType === CollectionViewType.Linear ? undefined :
+ `${Cast(Doc.UserDoc().activeWorkspace, Doc, null)?.darkScheme ? "rgb(30, 32, 31)" : "#9c9396"} ${StrCast(this.props.Document.boxShadow, "0.2vw 0.2vw 0.8vw")}`
}}
onContextMenu={this.onContextMenu}>
{this.showIsTagged()}
- {this.collectionViewType !== undefined ? this.SubView(this.collectionViewType, props) : (null)}
+ <div style={{ width: `calc(100% - ${this.facetWidth()}px)`, marginLeft: `${this.facetWidth()}px` }}>
+ {this.collectionViewType !== undefined ? this.SubView(this.collectionViewType, props) : (null)}
+ </div>
{this.lightbox(DocListCast(this.props.Document[this.props.fieldKey]).filter(d => d.type === DocumentType.IMG).map(d =>
Cast(d.data, ImageField) ?
(Cast(d.data, ImageField)!.url.href.indexOf(window.location.origin) === -1) ?
Utils.CorsProxy(Cast(d.data, ImageField)!.url.href) : Cast(d.data, ImageField)!.url.href
:
""))}
+ {!this.props.isSelected() || this.props.PanelHeight() < 100 ? (null) :
+ <div className="collectionTimeView-dragger" key="dragger" onPointerDown={this.onPointerDown} style={{ transform: `translate(${this.facetWidth()}px, 0px)` }} >
+ <span title="library View Dragger" style={{ width: "5px", position: "absolute", top: "0" }} />
+ </div>
+ }
+ {this.filterView}
</div>);
}
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionViewChromes.scss b/src/client/views/collections/CollectionViewChromes.scss
index 414bbfc0b..a691b4805 100644
--- a/src/client/views/collections/CollectionViewChromes.scss
+++ b/src/client/views/collections/CollectionViewChromes.scss
@@ -2,7 +2,8 @@
@import '~js-datepicker/dist/datepicker.min.css';
.collectionViewChrome-cont {
- position: relative;
+ position: absolute;
+ width:100%;
opacity: 0.9;
z-index: 9001;
transition: top .5s;
@@ -26,7 +27,6 @@
outline-color: black;
border: none;
padding: 12px 10px 11px 10px;
- margin-left: 50px;
}
.collectionViewBaseChrome-viewPicker:active {
@@ -58,10 +58,10 @@
position: absolute;
width: 40px;
transform-origin: top left;
+ pointer-events: all;
// margin-top: 10px;
}
.collectionViewBaseChrome-template {
- margin-left: 10px;
display: grid;
background: rgb(238, 238, 238);
color:grey;
@@ -168,23 +168,26 @@
}
- .collectionStackingViewChrome-sectionFilter-cont,
- .collectionTreeViewChrome-sectionFilter-cont {
+ .collectionStackingViewChrome-pivotField-cont,
+ .collectionTreeViewChrome-pivotField-cont {
justify-self: right;
display: flex;
font-size: 75%;
letter-spacing: 2px;
- .collectionStackingViewChrome-sectionFilter-label,
- .collectionTreeViewChrome-sectionFilter-label {
+ .collectionStackingViewChrome-pivotField-label,
+ .collectionTreeViewChrome-pivotField-label {
vertical-align: center;
- padding: 10px;
+ padding-left: 10px;
+ padding-top: 10px;
+ padding-bottom: 10px;
}
- .collectionStackingViewChrome-sectionFilter,
- .collectionTreeViewChrome-sectionFilter {
+ .collectionStackingViewChrome-pivotField,
+ .collectionTreeViewChrome-pivotField {
color: white;
- width: 100px;
+ width:100%;
+ min-width: 100px;
text-align: center;
background: rgb(238, 238, 238);
@@ -209,8 +212,8 @@
}
}
- .collectionStackingViewChrome-sectionFilter:hover,
- .collectionTreeViewChrome-sectionFilter:hover {
+ .collectionStackingViewChrome-pivotField:hover,
+ .collectionTreeViewChrome-pivotField:hover {
cursor: text;
}
}
diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx
index 073a30330..2d565d9db 100644
--- a/src/client/views/collections/CollectionViewChromes.tsx
+++ b/src/client/views/collections/CollectionViewChromes.tsx
@@ -8,7 +8,7 @@ import { List } from "../../../new_fields/List";
import { listSpec } from "../../../new_fields/Schema";
import { ScriptField } from "../../../new_fields/ScriptField";
import { BoolCast, Cast, NumCast, StrCast } from "../../../new_fields/Types";
-import { Utils, emptyFunction } from "../../../Utils";
+import { Utils, emptyFunction, setupMoveUpEvents } from "../../../Utils";
import { DragManager } from "../../util/DragManager";
import { undoBatch } from "../../util/UndoManager";
import { EditableView } from "../EditableView";
@@ -24,6 +24,7 @@ interface CollectionViewChromeProps {
CollectionView: CollectionView;
type: CollectionViewType;
collapse?: (value: boolean) => any;
+ PanelWidth: () => number;
}
interface Filter {
@@ -38,25 +39,30 @@ const stopPropagation = (e: React.SyntheticEvent) => e.stopPropagation();
export class CollectionViewBaseChrome extends React.Component<CollectionViewChromeProps> {
//(!)?\(\(\(doc.(\w+) && \(doc.\w+ as \w+\).includes\(\"(\w+)\"\)
+ get target() { return this.props.CollectionView.props.Document; }
_templateCommand = {
- title: "=> item view", script: "setChildLayout(this.target, this.source?.[0])", params: ["target", "source"],
+ params: ["target", "source"], title: "=> item view",
+ script: "setChildLayout(this.target, this.source?.[0])",
+ immediate: (source: Doc[]) => Doc.setChildLayout(this.target, source?.[0]),
initialize: emptyFunction,
- immediate: (draggedDocs: Doc[]) => Doc.setChildLayout(this.props.CollectionView.props.Document, draggedDocs.length ? draggedDocs[0] : undefined)
};
_narrativeCommand = {
- title: "=> click item view", script: "setChildDetailedLayout(this.target, this.source?.[0])", params: ["target", "source"],
+ params: ["target", "source"], title: "=> click item view",
+ script: "setChildDetailedLayout(this.target, this.source?.[0])",
+ immediate: (source: Doc[]) => Doc.setChildDetailedLayout(this.target, source?.[0]),
initialize: emptyFunction,
- immediate: (draggedDocs: Doc[]) => Doc.setChildDetailedLayout(this.props.CollectionView.props.Document, draggedDocs.length ? draggedDocs[0] : undefined)
};
_contentCommand = {
- title: "=> content", script: "getProto(this.target).data = aliasDocs(this.source);", params: ["target", "source"],
+ params: ["target", "source"], title: "=> content",
+ script: "getProto(this.target).data = aliasDocs(this.source);",
+ immediate: (source: Doc[]) => Doc.GetProto(this.target).data = Doc.aliasDocs(source),
initialize: emptyFunction,
- immediate: (draggedDocs: Doc[]) => Doc.GetProto(this.props.CollectionView.props.Document).data = new List<Doc>(draggedDocs.map((d: any) => Doc.MakeAlias(d)))
};
_viewCommand = {
- title: "=> saved view", script: "this.target._panX = this.restoredPanX; this.target._panY = this.restoredPanY; this.target.scale = this.restoredScale;", params: ["target"],
- initialize: (button: Doc) => { button.restoredPanX = this.props.CollectionView.props.Document._panX; button.restoredPanY = this.props.CollectionView.props.Document._panY; button.restoredScale = this.props.CollectionView.props.Document.scale; },
- immediate: (draggedDocs: Doc[]) => { this.props.CollectionView.props.Document._panX = 0; this.props.CollectionView.props.Document._panY = 0; this.props.CollectionView.props.Document.scale = 1; },
+ params: ["target"], title: "=> saved view",
+ script: "this.target._panX = this.restoredPanX; this.target._panY = this.restoredPanY; this.target.scale = this.restoredScale;",
+ immediate: (source: Doc[]) => { this.target._panX = 0; this.target._panY = 0; this.target.scale = 1; },
+ initialize: (button: Doc) => { button.restoredPanX = this.target._panX; button.restoredPanY = this.target._panY; button.restoredScale = this.target.scale; },
};
_freeform_commands = [this._contentCommand, this._templateCommand, this._narrativeCommand, this._viewCommand];
_stacking_commands = [this._contentCommand, this._templateCommand];
@@ -70,13 +76,14 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
case CollectionViewType.Stacking: return this._stacking_commands;
case CollectionViewType.Masonry: return this._stacking_commands;
case CollectionViewType.Freeform: return this._freeform_commands;
- case CollectionViewType.Pivot: return this._freeform_commands;
+ case CollectionViewType.Time: return this._freeform_commands;
case CollectionViewType.Carousel: return this._freeform_commands;
}
return [];
}
private _picker: any;
private _commandRef = React.createRef<HTMLInputElement>();
+ private _viewRef = React.createRef<HTMLInputElement>();
private _autosuggestRef = React.createRef<Autosuggest>();
@observable private _currentKey: string = "";
@observable private _viewSpecsOpen: boolean = false;
@@ -179,7 +186,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
@action closeViewSpecs = () => {
this._viewSpecsOpen = false;
document.removeEventListener("pointerdown", this.closeViewSpecs);
- };
+ }
@action
openDatePicker = (e: React.PointerEvent) => {
@@ -257,11 +264,13 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
}
subChrome = () => {
+ const collapsed = this.props.CollectionView.props.Document._chromeStatus !== "enabled";
+ if (collapsed) return null;
switch (this.props.type) {
- case CollectionViewType.Stacking: return (<CollectionStackingViewChrome key="collchrome" CollectionView={this.props.CollectionView} type={this.props.type} />);
- case CollectionViewType.Schema: return (<CollectionSchemaViewChrome key="collchrome" CollectionView={this.props.CollectionView} type={this.props.type} />);
- case CollectionViewType.Tree: return (<CollectionTreeViewChrome key="collchrome" CollectionView={this.props.CollectionView} type={this.props.type} />);
- case CollectionViewType.Masonry: return (<CollectionStackingViewChrome key="collchrome" CollectionView={this.props.CollectionView} type={this.props.type} />);
+ case CollectionViewType.Stacking: return (<CollectionStackingViewChrome key="collchrome" PanelWidth={this.props.PanelWidth} CollectionView={this.props.CollectionView} type={this.props.type} />);
+ case CollectionViewType.Schema: return (<CollectionSchemaViewChrome key="collchrome" PanelWidth={this.props.PanelWidth} CollectionView={this.props.CollectionView} type={this.props.type} />);
+ case CollectionViewType.Tree: return (<CollectionTreeViewChrome key="collchrome" PanelWidth={this.props.PanelWidth} CollectionView={this.props.CollectionView} type={this.props.type} />);
+ case CollectionViewType.Masonry: return (<CollectionStackingViewChrome key="collchrome" PanelWidth={this.props.PanelWidth} CollectionView={this.props.CollectionView} type={this.props.type} />);
default: return null;
}
}
@@ -270,32 +279,6 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
return this.props.CollectionView.props.Document;
}
- private get pivotKey() {
- return StrCast(this.document.pivotField);
- }
-
- private set pivotKey(value: string) {
- this.document.pivotField = value;
- }
-
- @observable private pivotKeyDisplay = this.pivotKey;
- getPivotInput = () => {
- if (StrCast(this.document._freeformLayoutEngine) !== "pivot") {
- return (null);
- }
- return (<input className="collectionViewBaseChrome-viewSpecsInput"
- placeholder="PIVOT ON..."
- value={this.pivotKeyDisplay}
- onChange={action((e: React.ChangeEvent<HTMLInputElement>) => this.pivotKeyDisplay = e.currentTarget.value)}
- onKeyPress={action((e: React.KeyboardEvent<HTMLInputElement>) => {
- const value = e.currentTarget.value;
- if (e.which === 13) {
- this.pivotKey = value;
- this.pivotKeyDisplay = "";
- }
- })} />);
- }
-
@action.bound
clearFilter = () => {
this.props.CollectionView.props.Document.viewSpecScript = ScriptField.MakeFunction("true", { doc: Doc.name });
@@ -363,8 +346,21 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
private _startDragPosition: { x: number, y: number } = { x: 0, y: 0 };
private _sensitivity: number = 16;
+ dragViewDown = (e: React.PointerEvent) => {
+ setupMoveUpEvents(this, e, (e, down, delta) => {
+ const vtype = NumCast(this.props.CollectionView.props.Document._viewType) as CollectionViewType;
+ const c = {
+ params: ["target"], title: CollectionViewType.stringFor(vtype),
+ script: `this.target._viewType = ${NumCast(this.props.CollectionView.props.Document._viewType)}`,
+ immediate: (source: Doc[]) => Doc.setChildLayout(this.target, source?.[0]),
+ initialize: emptyFunction,
+ };
+ DragManager.StartButtonDrag([this._viewRef.current!], c.script, StrCast(c.title),
+ { target: this.props.CollectionView.props.Document }, c.params, c.initialize, e.clientX, e.clientY);
+ return true;
+ }, emptyFunction, emptyFunction);
+ }
dragCommandDown = (e: React.PointerEvent) => {
-
this._startDragPosition = { x: e.clientX, y: e.clientY };
document.addEventListener("pointermove", this.dragPointerMove);
document.addEventListener("pointerup", this.dragPointerUp);
@@ -387,40 +383,52 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
dragPointerUp = (e: PointerEvent) => {
document.removeEventListener("pointermove", this.dragPointerMove);
document.removeEventListener("pointerup", this.dragPointerUp);
-
}
render() {
const collapsed = this.props.CollectionView.props.Document._chromeStatus !== "enabled";
return (
- <div className="collectionViewChrome-cont" style={{ top: collapsed ? -70 : 0, height: collapsed ? 0 : undefined }}>
- <div className="collectionViewChrome">
+ <div className="collectionViewChrome-cont" style={{
+ top: collapsed ? -70 : 0, height: collapsed ? 0 : undefined,
+ transform: collapsed ? "" : `scale(${Math.min(1, this.props.CollectionView.props.ScreenToLocalTransform().Scale)})`,
+ transformOrigin: "top left",
+ width: `${this.props.PanelWidth() / Math.min(1, this.props.CollectionView.props.ScreenToLocalTransform().Scale)}px`
+ }}>
+ <div className="collectionViewChrome" style={{ border: "unset", pointerEvents: collapsed ? "none" : undefined }}>
<div className="collectionViewBaseChrome">
<button className="collectionViewBaseChrome-collapse"
style={{
top: collapsed ? 70 : 10,
- transform: `rotate(${collapsed ? 180 : 0}deg) scale(${collapsed ? 0.5 : 1}) translate(${collapsed ? "-100%, -100%" : "0, 0"})`,
- opacity: (collapsed && !this.props.CollectionView.props.isSelected()) ? 0 : 0.9,
+ transform: `rotate(${collapsed ? 180 : 0}deg) scale(0.5) translate(${collapsed ? "-100%, -100%" : "0, 0"})`,
+ opacity: 0.9,
+ display: (collapsed && !this.props.CollectionView.props.isSelected()) ? "none" : undefined,
left: (collapsed ? 0 : "unset"),
}}
title="Collapse collection chrome" onClick={this.toggleCollapse}>
<FontAwesomeIcon icon="caret-up" size="2x" />
</button>
- <select
- className="collectionViewBaseChrome-viewPicker"
- onPointerDown={stopPropagation}
- onChange={this.viewChanged}
- value={NumCast(this.props.CollectionView.props.Document._viewType)}>
- <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="1">Freeform</option>
- <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="2">schema</option>
- <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="4">Tree</option>
- <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="5">Stacking</option>
- <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="6">Masonry</option>
- <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="7">Multicolumn</option>
- <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="8">Pivot</option>
- <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="9">Carousel</option>
- <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="10">Linear</option>
- </select>
+ <div className="collectionViewBaseChrome-template" style={{ marginLeft: 25, display: collapsed ? "none" : undefined }}>
+ <div className="commandEntry-outerDiv" title="drop document to apply or drag to create button" ref={this._viewRef} onPointerDown={this.dragViewDown}>
+ <div className="commandEntry-drop">
+ <FontAwesomeIcon icon="bullseye" size="2x"></FontAwesomeIcon>
+ </div>
+ <select
+ className="collectionViewBaseChrome-viewPicker"
+ onPointerDown={stopPropagation}
+ onChange={this.viewChanged}
+ value={NumCast(this.props.CollectionView.props.Document._viewType)}>
+ <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="1">Freeform</option>
+ <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="2">Schema</option>
+ <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="4">Tree</option>
+ <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="5">Stacking</option>
+ <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="6">Masonry</option>
+ <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="7">MultiCol</option>
+ <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="8">MultiRow</option>
+ <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="9">Pivot/Time</option>
+ <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="10">Carousel</option>
+ </select>
+ </div>
+ </div>
<div className="collectionViewBaseChrome-viewSpecs" title="filter documents to show" style={{ display: collapsed ? "none" : "grid" }}>
<div className="collectionViewBaseChrome-filterIcon" onPointerDown={this.openViewSpecs} >
<FontAwesomeIcon icon="filter" size="2x" />
@@ -464,10 +472,10 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
</div>
</div>
</div>
- <div className="collectionViewBaseChrome-template" ref={this.createDropTarget} >
+ <div className="collectionViewBaseChrome-template" ref={this.createDropTarget} style={{ display: collapsed ? "none" : undefined }}>
<div className="commandEntry-outerDiv" title="drop document to apply or drag to create button" ref={this._commandRef} onPointerDown={this.dragCommandDown}>
<div className="commandEntry-drop">
- <FontAwesomeIcon icon="bullseye" size="2x"></FontAwesomeIcon>
+ <FontAwesomeIcon icon="bullseye" size="2x" />
</div>
<select
className="collectionViewBaseChrome-cmdPicker"
@@ -495,7 +503,7 @@ export class CollectionStackingViewChrome extends React.Component<CollectionView
@observable private suggestions: string[] = [];
@computed private get descending() { return BoolCast(this.props.CollectionView.props.Document.stackingHeadersSortDescending); }
- @computed get sectionFilter() { return StrCast(this.props.CollectionView.props.Document.sectionFilter); }
+ @computed get pivotField() { return StrCast(this.props.CollectionView.props.Document._pivotField); }
getKeySuggestions = async (value: string): Promise<string[]> => {
value = value.toLowerCase();
@@ -533,26 +541,26 @@ export class CollectionStackingViewChrome extends React.Component<CollectionView
}
setValue = (value: string) => {
- this.props.CollectionView.props.Document.sectionFilter = value;
+ this.props.CollectionView.props.Document._pivotField = value;
return true;
}
@action toggleSort = () => { this.props.CollectionView.props.Document.stackingHeadersSortDescending = !this.props.CollectionView.props.Document.stackingHeadersSortDescending; };
- @action resetValue = () => { this._currentKey = this.sectionFilter; };
+ @action resetValue = () => { this._currentKey = this.pivotField; };
render() {
return (
<div className="collectionStackingViewChrome-cont">
- <div className="collectionStackingViewChrome-sectionFilter-cont">
- <div className="collectionStackingViewChrome-sectionFilter-label">
- GROUP ITEMS BY:
+ <div className="collectionStackingViewChrome-pivotField-cont">
+ <div className="collectionStackingViewChrome-pivotField-label">
+ GROUP BY:
</div>
<div className="collectionStackingViewChrome-sortIcon" onClick={this.toggleSort} style={{ transform: `rotate(${this.descending ? "180" : "0"}deg)` }}>
<FontAwesomeIcon icon="caret-up" size="2x" color="white" />
</div>
- <div className="collectionStackingViewChrome-sectionFilter">
+ <div className="collectionStackingViewChrome-pivotField">
<EditableView
- GetValue={() => this.sectionFilter}
+ GetValue={() => this.pivotField}
autosuggestProps={
{
resetValue: this.resetValue,
@@ -574,7 +582,7 @@ export class CollectionStackingViewChrome extends React.Component<CollectionView
}}
oneLine
SetValue={this.setValue}
- contents={this.sectionFilter ? this.sectionFilter : "N/A"}
+ contents={this.pivotField ? this.pivotField : "N/A"}
/>
</div>
</div>
diff --git a/src/client/views/collections/ParentDocumentSelector.scss b/src/client/views/collections/ParentDocumentSelector.scss
index a266861bd..4e704b58f 100644
--- a/src/client/views/collections/ParentDocumentSelector.scss
+++ b/src/client/views/collections/ParentDocumentSelector.scss
@@ -35,6 +35,10 @@
pointer-events: all;
position: relative;
display: inline-block;
+ svg {
+ width:20px !important;
+ height:20px;
+ }
}
.parentDocumentSelector-metadata {
pointer-events: auto;
@@ -46,8 +50,7 @@
div {
overflow: visible !important;
}
- position: absolute;
display: inline-block;
- padding-left: 5px;
- padding-right: 5px;
+ width:100%;
+ height:100%;
} \ No newline at end of file
diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx
index 115f8d633..35e3a8958 100644
--- a/src/client/views/collections/ParentDocumentSelector.tsx
+++ b/src/client/views/collections/ParentDocumentSelector.tsx
@@ -2,7 +2,7 @@ import * as React from "react";
import './ParentDocumentSelector.scss';
import { Doc } from "../../../new_fields/Doc";
import { observer } from "mobx-react";
-import { observable, action, runInAction } from "mobx";
+import { observable, action, runInAction, trace, computed } from "mobx";
import { Id } from "../../../new_fields/FieldSymbols";
import { SearchUtil } from "../../util/SearchUtil";
import { CollectionDockingView } from "./CollectionDockingView";
@@ -11,21 +11,20 @@ import { CollectionViewType } from "./CollectionView";
import { DocumentButtonBar } from "../DocumentButtonBar";
import { DocumentManager } from "../../util/DocumentManager";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { faEdit, faChevronCircleUp } from "@fortawesome/free-solid-svg-icons";
+import { faCog, faChevronCircleUp } from "@fortawesome/free-solid-svg-icons";
import { library } from "@fortawesome/fontawesome-svg-core";
-import { MetadataEntryMenu } from "../MetadataEntryMenu";
import { DocumentView } from "../nodes/DocumentView";
+import { SelectionManager } from "../../util/SelectionManager";
const higflyout = require("@hig/flyout");
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
-library.add(faEdit);
+library.add(faCog);
type SelectorProps = {
Document: Doc,
- Views: DocumentView[],
Stack?: any,
- addDocTab(doc: Doc, dataDoc: Doc | undefined, location: string): void
+ addDocTab(doc: Doc, location: string): void
};
@observer
@@ -61,7 +60,7 @@ export class SelectorContextMenu extends React.Component<SelectorProps> {
col._panX = newPanX;
col._panY = newPanY;
}
- this.props.addDocTab(col, undefined, "inTab"); // bcz: dataDoc?
+ this.props.addDocTab(col, "inTab"); // bcz: dataDoc?
};
}
@@ -79,13 +78,12 @@ export class SelectorContextMenu extends React.Component<SelectorProps> {
export class ParentDocSelector extends React.Component<SelectorProps> {
render() {
const flyout = (
- <div className="parentDocumentSelector-flyout" style={{}} title=" ">
+ <div className="parentDocumentSelector-flyout" title=" ">
<SelectorContextMenu {...this.props} />
</div>
);
- return <div title="Tap to View Contexts/Metadata" onPointerDown={e => e.stopPropagation()} className="parentDocumentSelector-linkFlyout">
- <Flyout anchorPoint={anchorPoints.LEFT_TOP}
- content={flyout}>
+ return <div title="Show Contexts" onPointerDown={e => e.stopPropagation()} className="parentDocumentSelector-linkFlyout">
+ <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={flyout}>
<span className="parentDocumentSelector-button" >
<FontAwesomeIcon icon={faChevronCircleUp} size={"lg"} />
</span>
@@ -95,14 +93,9 @@ export class ParentDocSelector extends React.Component<SelectorProps> {
}
@observer
-export class ButtonSelector extends React.Component<{ Document: Doc, Stack: any }> {
+export class DockingViewButtonSelector extends React.Component<{ views: DocumentView[], Stack: any }> {
@observable hover = false;
- @action
- onPointerDown = (e: React.PointerEvent) => {
- this.hover = !this.hover;
- e.stopPropagation();
- }
customStylesheet(styles: any) {
return {
...styles,
@@ -113,16 +106,20 @@ export class ButtonSelector extends React.Component<{ Document: Doc, Stack: any
};
}
- render() {
- const view = DocumentManager.Instance.getDocumentView(this.props.Document);
- const flyout = (
+ @computed get flyout() {
+ trace();
+ return (
<div className="ParentDocumentSelector-flyout" title=" ">
- <DocumentButtonBar views={[view]} stack={this.props.Stack} />
+ <DocumentButtonBar views={this.props.views} stack={this.props.Stack} />
</div>
);
- return <span title="Tap for menu" onPointerDown={e => e.stopPropagation()} className="buttonSelector">
- <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={flyout} stylesheet={this.customStylesheet}>
- <FontAwesomeIcon icon={faEdit} size={"sm"} />
+ }
+
+ render() {
+ trace();
+ return <span title="Tap for menu, drag tab as document" onPointerDown={e => { this.props.views[0].select(false); e.stopPropagation(); }} className="buttonSelector">
+ <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={this.flyout} stylesheet={this.customStylesheet}>
+ <FontAwesomeIcon icon={"cog"} size={"sm"} />
</Flyout>
</span>;
}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
index be1317b25..bd4db89ec 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
@@ -1,82 +1,171 @@
import { Doc, Field, FieldResult } from "../../../../new_fields/Doc";
-import { NumCast, StrCast, Cast, DateCast } from "../../../../new_fields/Types";
+import { NumCast, StrCast, Cast } from "../../../../new_fields/Types";
import { ScriptBox } from "../../ScriptBox";
import { CompileScript } from "../../../util/Scripting";
import { ScriptField } from "../../../../new_fields/ScriptField";
import { OverlayView, OverlayElementOptions } from "../../OverlayView";
-import { emptyFunction } from "../../../../Utils";
+import { emptyFunction, aggregateBounds } from "../../../../Utils";
import React = require("react");
-import { ObservableMap, runInAction } from "mobx";
import { Id, ToString } from "../../../../new_fields/FieldSymbols";
import { ObjectField } from "../../../../new_fields/ObjectField";
import { RefField } from "../../../../new_fields/RefField";
-interface PivotData {
+export interface ViewDefBounds {
type: string;
- text: string;
+ text?: string;
x: number;
y: number;
- width: number;
- height: number;
- fontSize: number;
+ z?: number;
+ zIndex?: number;
+ width?: number;
+ height?: number;
+ transition?: string;
+ fontSize?: number;
+ highlight?: boolean;
+ color?: string;
+ payload: any;
}
-export interface ViewDefBounds {
- x: number;
- y: number;
+export interface PoolData {
+ x?: number;
+ y?: number;
z?: number;
- width: number;
- height: number;
+ zIndex?: number;
+ width?: number;
+ height?: number;
+ color?: string;
transition?: string;
+ highlight?: boolean;
}
export interface ViewDefResult {
ele: JSX.Element;
bounds?: ViewDefBounds;
}
-
function toLabel(target: FieldResult<Field>) {
+ if (typeof target === "number" || Number(target)) {
+ const truncated = Number(Number(target).toFixed(0));
+ const precise = Number(Number(target).toFixed(2));
+ return truncated === precise ? Number(target).toFixed(0) : Number(target).toFixed(2);
+ }
if (target instanceof ObjectField || target instanceof RefField) {
return target[ToString]();
}
return String(target);
}
+/**
+ * Uses canvas.measureText to compute and return the width of the given text of given font in pixels.
+ *
+ * @param {String} text The text to be rendered.
+ * @param {String} font The css font descriptor that text is to be rendered with (e.g. "bold 14px verdana").
+ *
+ * @see https://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393
+ */
+function getTextWidth(text: string, font: string): number {
+ // re-use canvas object for better performance
+ const canvas = (getTextWidth as any).canvas || ((getTextWidth as any).canvas = document.createElement("canvas"));
+ const context = canvas.getContext("2d");
+ context.font = font;
+ const metrics = context.measureText(text);
+ return metrics.width;
+}
+
+interface PivotColumn {
+ docs: Doc[];
+ filters: string[];
+}
+
-export function computePivotLayout(poolData: ObservableMap<string, any>, pivotDoc: Doc, childDocs: Doc[], childPairs: { layout: Doc, data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: any) => ViewDefResult[]) {
- const pivotAxisWidth = NumCast(pivotDoc.pivotWidth, 200);
- const pivotColumnGroups = new Map<FieldResult<Field>, Doc[]>();
+export function computePivotLayout(
+ poolData: Map<string, PoolData>,
+ pivotDoc: Doc,
+ childDocs: Doc[],
+ filterDocs: Doc[],
+ childPairs: { layout: Doc, data?: Doc }[],
+ panelDim: number[],
+ viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[]
+) {
+ const fieldKey = "data";
+ const pivotColumnGroups = new Map<FieldResult<Field>, PivotColumn>();
- const pivotFieldKey = toLabel(pivotDoc.pivotField);
- for (const doc of childDocs) {
+ const pivotFieldKey = toLabel(pivotDoc._pivotField);
+ for (const doc of filterDocs) {
const val = Field.toString(doc[pivotFieldKey] as Field);
if (val) {
- !pivotColumnGroups.get(val) && pivotColumnGroups.set(val, []);
- pivotColumnGroups.get(val)!.push(doc);
+ !pivotColumnGroups.get(val) && pivotColumnGroups.set(val, { docs: [], filters: [val] });
+ pivotColumnGroups.get(val)!.docs.push(doc);
+ }
+ }
+ let nonNumbers = 0;
+ childDocs.map(doc => {
+ const num = toNumber(doc[pivotFieldKey]);
+ if (num === undefined || Number.isNaN(num)) {
+ nonNumbers++;
+ }
+ });
+ const pivotNumbers = nonNumbers / childDocs.length < .1;
+ if (pivotColumnGroups.size > 10) {
+ const arrayofKeys = Array.from(pivotColumnGroups.keys());
+ const sortedKeys = pivotNumbers ? arrayofKeys.sort((n1: FieldResult, n2: FieldResult) => toNumber(n1)! - toNumber(n2)!) : arrayofKeys.sort();
+ const clusterSize = Math.ceil(pivotColumnGroups.size / 10);
+ const numClusters = Math.ceil(sortedKeys.length / clusterSize);
+ for (let i = 0; i < numClusters; i++) {
+ for (let j = i * clusterSize + 1; j < Math.min(sortedKeys.length, (i + 1) * clusterSize); j++) {
+ const curgrp = pivotColumnGroups.get(sortedKeys[i * clusterSize])!;
+ const newgrp = pivotColumnGroups.get(sortedKeys[j])!;
+ curgrp.docs.push(...newgrp.docs);
+ curgrp.filters.push(...newgrp.filters);
+ pivotColumnGroups.delete(sortedKeys[j]);
+ }
+ }
+ }
+ const fontSize = NumCast(pivotDoc[fieldKey + "-timelineFontSize"], panelDim[1] > 58 ? 20 : Math.max(7, panelDim[1] / 3));
+ const desc = `${fontSize}px ${getComputedStyle(document.body).fontFamily}`;
+ const textlen = Array.from(pivotColumnGroups.keys()).map(c => getTextWidth(toLabel(c), desc)).reduce((p, c) => Math.max(p, c), 0 as number);
+ const max_text = Math.min(Math.ceil(textlen / 120) * 28, panelDim[1] / 2);
+ const maxInColumn = Array.from(pivotColumnGroups.values()).reduce((p, s) => Math.max(p, s.docs.length), 1);
+
+ const colWidth = panelDim[0] / pivotColumnGroups.size;
+ const colHeight = panelDim[1] - max_text;
+ let numCols = 0;
+ let bestArea = 0;
+ let pivotAxisWidth = 0;
+ for (let i = 1; i < 10; i++) {
+ const numInCol = Math.ceil(maxInColumn / i);
+ const hd = colHeight / numInCol;
+ const wd = colWidth / i;
+ const dim = Math.min(hd, wd);
+ if (dim > bestArea) {
+ bestArea = dim;
+ numCols = i;
+ pivotAxisWidth = dim;
}
}
- const minSize = Array.from(pivotColumnGroups.entries()).reduce((min, pair) => Math.min(min, pair[1].length), Infinity);
- let numCols = NumCast(pivotDoc.pivotNumColumns, Math.ceil(Math.sqrt(minSize)));
const docMap = new Map<Doc, ViewDefBounds>();
- const groupNames: PivotData[] = [];
- numCols = Math.min(panelDim[0] / pivotAxisWidth, numCols);
+ const groupNames: ViewDefBounds[] = [];
const expander = 1.05;
const gap = .15;
+ const maxColHeight = pivotAxisWidth * expander * Math.ceil(maxInColumn / numCols);
let x = 0;
- pivotColumnGroups.forEach((val, key) => {
+ const sortedPivotKeys = pivotNumbers ? Array.from(pivotColumnGroups.keys()).sort((n1: FieldResult, n2: FieldResult) => toNumber(n1)! - toNumber(n2)!) : Array.from(pivotColumnGroups.keys()).sort();
+ sortedPivotKeys.forEach(key => {
+ const val = pivotColumnGroups.get(key)!;
let y = 0;
let xCount = 0;
+ const text = toLabel(key);
groupNames.push({
type: "text",
- text: toLabel(key),
+ text,
x,
- y: pivotAxisWidth + 50,
+ y: pivotAxisWidth,
width: pivotAxisWidth * expander * numCols,
- height: NumCast(pivotDoc.pivotFontSize, 10),
- fontSize: NumCast(pivotDoc.pivotFontSize, 10)
+ height: max_text,
+ fontSize,
+ payload: val
});
- for (const doc of val) {
+ for (const doc of val.docs) {
const layoutDoc = Doc.Layout(doc);
let wid = pivotAxisWidth;
let hgt = layoutDoc._nativeWidth ? (NumCast(layoutDoc._nativeHeight) / NumCast(layoutDoc._nativeWidth)) * pivotAxisWidth : pivotAxisWidth;
@@ -85,10 +174,12 @@ export function computePivotLayout(poolData: ObservableMap<string, any>, pivotDo
wid = layoutDoc._nativeHeight ? (NumCast(layoutDoc._nativeWidth) / NumCast(layoutDoc._nativeHeight)) * pivotAxisWidth : pivotAxisWidth;
}
docMap.set(doc, {
- x: x + xCount * pivotAxisWidth * expander + (pivotAxisWidth - wid) / 2 + (val.length < numCols ? (numCols - val.length) * pivotAxisWidth / 2 : 0),
- y: -y,
+ type: "doc",
+ x: x + xCount * pivotAxisWidth * expander + (pivotAxisWidth - wid) / 2 + (val.docs.length < numCols ? (numCols - val.docs.length) * pivotAxisWidth / 2 : 0),
+ y: -y + (pivotAxisWidth - hgt) / 2,
width: wid,
- height: hgt
+ height: hgt,
+ payload: undefined
});
xCount++;
if (xCount >= numCols) {
@@ -99,21 +190,168 @@ export function computePivotLayout(poolData: ObservableMap<string, any>, pivotDo
x += pivotAxisWidth * (numCols * expander + gap);
});
- childPairs.map(pair => {
- const defaultPosition = {
- x: NumCast(pair.layout.x),
- y: NumCast(pair.layout.y),
- z: NumCast(pair.layout.z),
- width: NumCast(pair.layout._width),
- height: NumCast(pair.layout._height)
- };
- const pos = docMap.get(pair.layout) || defaultPosition;
- const data = poolData.get(pair.layout[Id]);
- if (!data || pos.x !== data.x || pos.y !== data.y || pos.z !== data.z || pos.width !== data.width || pos.height !== data.height) {
- runInAction(() => poolData.set(pair.layout[Id], { transition: "transform 1s", ...pos }));
+ const dividers = sortedPivotKeys.map((key, i) =>
+ ({ type: "div", color: "lightGray", x: i * pivotAxisWidth * (numCols * expander + gap) - pivotAxisWidth * (expander - 1) / 2, y: -maxColHeight + pivotAxisWidth, width: pivotAxisWidth * numCols * expander, height: maxColHeight, payload: pivotColumnGroups.get(key)!.filters }));
+ groupNames.push(...dividers);
+ return normalizeResults(panelDim, max_text, childPairs, docMap, poolData, viewDefsToJSX, groupNames, 0, [], childDocs.filter(c => !filterDocs.includes(c)));
+}
+
+function toNumber(val: FieldResult<Field>) {
+ return val === undefined ? undefined : NumCast(val, Number(StrCast(val)));
+}
+
+export function computeTimelineLayout(
+ poolData: Map<string, PoolData>,
+ pivotDoc: Doc,
+ childDocs: Doc[],
+ filterDocs: Doc[],
+ childPairs: { layout: Doc, data?: Doc }[],
+ panelDim: number[],
+ viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[]
+) {
+ const fieldKey = "data";
+ const pivotDateGroups = new Map<number, Doc[]>();
+ const docMap = new Map<Doc, ViewDefBounds>();
+ const groupNames: ViewDefBounds[] = [];
+ const timelineFieldKey = Field.toString(pivotDoc._pivotField as Field);
+ const curTime = toNumber(pivotDoc[fieldKey + "-timelineCur"]);
+ const curTimeSpan = Cast(pivotDoc[fieldKey + "-timelineSpan"], "number", null);
+ const minTimeReq = curTimeSpan === undefined ? Cast(pivotDoc[fieldKey + "-timelineMinReq"], "number", null) : curTime && (curTime - curTimeSpan);
+ const maxTimeReq = curTimeSpan === undefined ? Cast(pivotDoc[fieldKey + "-timelineMaxReq"], "number", null) : curTime && (curTime + curTimeSpan);
+ const fontSize = NumCast(pivotDoc[fieldKey + "-timelineFontSize"], panelDim[1] > 58 ? 20 : Math.max(7, panelDim[1] / 3));
+ const fontHeight = panelDim[1] > 58 ? 30 : panelDim[1] / 2;
+ const findStack = (time: number, stack: number[]) => {
+ const index = stack.findIndex(val => val === undefined || val < x);
+ return index === -1 ? stack.length : index;
+ };
+
+ let minTime = minTimeReq === undefined ? Number.MAX_VALUE : minTimeReq;
+ let maxTime = maxTimeReq === undefined ? -Number.MAX_VALUE : maxTimeReq;
+ filterDocs.map(doc => {
+ const num = NumCast(doc[timelineFieldKey], Number(StrCast(doc[timelineFieldKey])));
+ if (!Number.isNaN(num) && (!minTimeReq || num >= minTimeReq) && (!maxTimeReq || num <= maxTimeReq)) {
+ !pivotDateGroups.get(num) && pivotDateGroups.set(num, []);
+ pivotDateGroups.get(num)!.push(doc);
+ minTime = Math.min(num, minTime);
+ maxTime = Math.max(num, maxTime);
}
});
- return { elements: viewDefsToJSX(groupNames) };
+ if (curTime !== undefined) {
+ if (curTime > maxTime || curTime - minTime > maxTime - curTime) {
+ maxTime = curTime + (curTime - minTime);
+ } else {
+ minTime = curTime - (maxTime - curTime);
+ }
+ }
+ setTimeout(() => {
+ pivotDoc[fieldKey + "-timelineMin"] = minTime = minTimeReq ? Math.min(minTimeReq, minTime) : minTime;
+ pivotDoc[fieldKey + "-timelineMax"] = maxTime = maxTimeReq ? Math.max(maxTimeReq, maxTime) : maxTime;
+ }, 0);
+
+ if (maxTime === minTime) {
+ maxTime = minTime + 1;
+ }
+
+ const arrayofKeys = Array.from(pivotDateGroups.keys());
+ const sortedKeys = arrayofKeys.sort((n1, n2) => n1 - n2);
+ const scaling = panelDim[0] / (maxTime - minTime);
+ let x = 0;
+ let prevKey = Math.floor(minTime);
+
+ if (sortedKeys.length && scaling * (sortedKeys[0] - prevKey) > 25) {
+ groupNames.push({ type: "text", text: toLabel(prevKey), x: x, y: 0, height: fontHeight, fontSize, payload: undefined });
+ }
+ if (!sortedKeys.length && curTime !== undefined) {
+ groupNames.push({ type: "text", text: toLabel(curTime), x: (curTime - minTime) * scaling, zIndex: 1000, color: "orange", y: 0, height: fontHeight, fontSize, payload: undefined });
+ }
+
+ const pivotAxisWidth = NumCast(pivotDoc.pivotTimeWidth, panelDim[1] / 2.5);
+ const stacking: number[] = [];
+ let zind = 0;
+ sortedKeys.forEach(key => {
+ if (curTime !== undefined && curTime > prevKey && curTime <= key) {
+ groupNames.push({ type: "text", text: toLabel(curTime), x: (curTime - minTime) * scaling, y: 0, zIndex: 1000, color: "orange", height: fontHeight, fontSize, payload: key });
+ }
+ const keyDocs = pivotDateGroups.get(key)!;
+ x += scaling * (key - prevKey);
+ const stack = findStack(x, stacking);
+ prevKey = key;
+ if (!stack && (curTime === undefined || Math.abs(x - (curTime - minTime) * scaling) > pivotAxisWidth)) {
+ groupNames.push({ type: "text", text: toLabel(key), x: x, y: stack * 25, height: fontHeight, fontSize, payload: undefined });
+ }
+ layoutDocsAtTime(keyDocs, key);
+ });
+ if (sortedKeys.length && curTime !== undefined && curTime > sortedKeys[sortedKeys.length - 1]) {
+ x = (curTime - minTime) * scaling;
+ groupNames.push({ type: "text", text: toLabel(curTime), x: x, y: 0, zIndex: 1000, color: "orange", height: fontHeight, fontSize, payload: undefined });
+ }
+ if (Math.ceil(maxTime - minTime) * scaling > x + 25) {
+ groupNames.push({ type: "text", text: toLabel(Math.ceil(maxTime)), x: Math.ceil(maxTime - minTime) * scaling, y: 0, height: fontHeight, fontSize, payload: undefined });
+ }
+
+ const divider = { type: "div", color: Cast(Doc.UserDoc().activeWorkspace, Doc, null)?.darkScheme ? "dimGray" : "black", x: 0, y: 0, width: panelDim[0], height: -1, payload: undefined };
+ return normalizeResults(panelDim, fontHeight, childPairs, docMap, poolData, viewDefsToJSX, groupNames, (maxTime - minTime) * scaling, [divider], childDocs.filter(c => !filterDocs.includes(c)));
+
+ function layoutDocsAtTime(keyDocs: Doc[], key: number) {
+ keyDocs.forEach(doc => {
+ const stack = findStack(x, stacking);
+ const layoutDoc = Doc.Layout(doc);
+ let wid = pivotAxisWidth;
+ let hgt = layoutDoc._nativeWidth ? (NumCast(layoutDoc._nativeHeight) / NumCast(layoutDoc._nativeWidth)) * pivotAxisWidth : pivotAxisWidth;
+ if (hgt > pivotAxisWidth) {
+ hgt = pivotAxisWidth;
+ wid = layoutDoc._nativeHeight ? (NumCast(layoutDoc._nativeWidth) / NumCast(layoutDoc._nativeHeight)) * pivotAxisWidth : pivotAxisWidth;
+ }
+ docMap.set(doc, {
+ type: "doc",
+ x: x, y: -Math.sqrt(stack) * pivotAxisWidth / 2 - pivotAxisWidth + (pivotAxisWidth - hgt) / 2,
+ zIndex: (curTime === key ? 1000 : zind++), highlight: curTime === key, width: wid / (Math.max(stack, 1)), height: hgt / (Math.max(stack, 1)), payload: undefined
+ });
+ stacking[stack] = x + pivotAxisWidth;
+ });
+ }
+}
+
+function normalizeResults(panelDim: number[], fontHeight: number, childPairs: { data?: Doc, layout: Doc }[], docMap: Map<Doc, ViewDefBounds>,
+ poolData: Map<string, PoolData>, viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], groupNames: ViewDefBounds[], minWidth: number, extras: ViewDefBounds[],
+ extraDocs: Doc[]): ViewDefResult[] {
+
+ const grpEles = groupNames.map(gn => ({ x: gn.x, y: gn.y, width: gn.width, height: gn.height }) as ViewDefBounds);
+ const docEles = childPairs.filter(d => docMap.get(d.layout)).map(pair => docMap.get(pair.layout) as ViewDefBounds);
+ const aggBounds = aggregateBounds(docEles.concat(grpEles), 0, 0);
+ aggBounds.r = Math.max(minWidth, aggBounds.r - aggBounds.x);
+ const wscale = panelDim[0] / (aggBounds.r - aggBounds.x);
+ let scale = wscale * (aggBounds.b - aggBounds.y) > panelDim[1] ? (panelDim[1]) / (aggBounds.b - aggBounds.y) : wscale;
+ if (Number.isNaN(scale)) scale = 1;
+
+ childPairs.filter(d => docMap.get(d.layout)).map(pair => {
+ const newPosRaw = docMap.get(pair.layout);
+ if (newPosRaw) {
+ const newPos = {
+ x: newPosRaw.x * scale,
+ y: newPosRaw.y * scale,
+ z: newPosRaw.z,
+ highlight: newPosRaw.highlight,
+ zIndex: newPosRaw.zIndex,
+ width: (newPosRaw.width || 0) * scale,
+ height: newPosRaw.height! * scale
+ };
+ poolData.set(pair.layout[Id], { transition: "transform 1s", ...newPos });
+ }
+ });
+ extraDocs.map(ed => poolData.set(ed[Id], { x: 0, y: 0, zIndex: -99 }));
+
+ return viewDefsToJSX(extras.concat(groupNames).map(gname => ({
+ type: gname.type,
+ text: gname.text,
+ x: gname.x * scale,
+ y: gname.y * scale,
+ color: gname.color,
+ width: gname.width === undefined ? undefined : gname.width * scale,
+ height: gname.height === -1 ? 1 : gname.type === "text" ? Math.max(fontHeight * scale, (gname.height || 0) * scale) : (gname.height || 0) * scale,
+ fontSize: gname.fontSize,
+ payload: gname.payload
+ })));
}
export function AddCustomFreeFormLayout(doc: Doc, dataKey: string): () => void {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index b8fbaef5c..a33146388 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -46,8 +46,8 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
// really hacky stuff to make the DocuLinkBox display where we want it to:
// if there's an element in the DOM with the id of the opposite anchor, then that DOM element is a hyperlink source for the current anchor and we want to place our link box at it's top right
// otherwise, we just use the computed nearest point on the document boundary to the target Document
- const targetAhyperlink = window.document.getElementById((this.props.LinkDocs[0][afield] as Doc)[Id]);
- const targetBhyperlink = window.document.getElementById((this.props.LinkDocs[0][bfield] as Doc)[Id]);
+ const targetAhyperlink = window.document.getElementById(this.props.LinkDocs[0][Id] + (this.props.LinkDocs[0][afield] as Doc)[Id]);
+ const targetBhyperlink = window.document.getElementById(this.props.LinkDocs[0][Id] + (this.props.LinkDocs[0][bfield] as Doc)[Id]);
if (!targetBhyperlink) {
this.props.A.props.Document[afield + "_x"] = (apt.point.x - abounds.left) / abounds.width * 100;
this.props.A.props.Document[afield + "_y"] = (apt.point.y - abounds.top) / abounds.height * 100;
@@ -68,8 +68,8 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
(this.props.B.props.Document[(this.props.B.props as any).fieldKey] as Doc);
const m = targetAhyperlink.getBoundingClientRect();
const mp = this.props.B.props.ScreenToLocalTransform().transformPoint(m.right, m.top + 5);
- this.props.B.props.Document[afield + "_x"] = mp[0] / this.props.B.props.PanelWidth() * 100;
- this.props.B.props.Document[afield + "_y"] = mp[1] / this.props.B.props.PanelHeight() * 100;
+ this.props.B.props.Document[bfield + "_x"] = mp[0] / this.props.B.props.PanelWidth() * 100;
+ this.props.B.props.Document[bfield + "_y"] = mp[1] / this.props.B.props.PanelHeight() * 100;
}, 0);
}
})
@@ -95,10 +95,15 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
const pt2 = [bpt.point.x, bpt.point.y];
const aActive = this.props.A.isSelected() || Doc.IsBrushed(this.props.A.props.Document);
const bActive = this.props.A.isSelected() || Doc.IsBrushed(this.props.A.props.Document);
- return !aActive && !bActive ? (null) :
+ const text = StrCast(this.props.A.props.Document.linkRelationship);
+ return !aActive && !bActive ? (null) : (<>
+ <text x={(pt1[0] + pt2[0]) / 2} y={(pt1[1] + pt2[1]) / 2}>
+ {text !== "-ungrouped-" ? text : ""}
+ </text>
<line key="linkLine" className="collectionfreeformlinkview-linkLine"
style={{ opacity: this._opacity, strokeDasharray: "2 2" }}
x1={`${pt1[0]}`} y1={`${pt1[1]}`}
- x2={`${pt2[0]}`} y2={`${pt2[1]}`} />;
+ x2={`${pt2[0]}`} y2={`${pt2[1]}`} />
+ </>);
}
} \ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
index 044d35eca..49ca024a2 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
@@ -1,4 +1,4 @@
-import { computed, IReactionDisposer } from "mobx";
+import { computed } from "mobx";
import { observer } from "mobx-react";
import { Doc } from "../../../../new_fields/Doc";
import { Id } from "../../../../new_fields/FieldSymbols";
@@ -7,69 +7,12 @@ import { DocumentView } from "../../nodes/DocumentView";
import "./CollectionFreeFormLinksView.scss";
import { CollectionFreeFormLinkView } from "./CollectionFreeFormLinkView";
import React = require("react");
-import { Utils } from "../../../../Utils";
+import { Utils, emptyFunction } from "../../../../Utils";
import { SelectionManager } from "../../../util/SelectionManager";
import { DocumentType } from "../../../documents/DocumentTypes";
@observer
export class CollectionFreeFormLinksView extends React.Component {
-
- _brushReactionDisposer?: IReactionDisposer;
- componentDidMount() {
- // this._brushReactionDisposer = reaction(
- // () => {
- // let doclist = DocListCast(this.props.Document[this.props.fieldKey]);
- // return { doclist: doclist ? doclist : [], xs: doclist.map(d => d.x) };
- // },
- // () => {
- // let doclist = DocListCast(this.props.Document[this.props.fieldKey]);
- // let views = doclist ? doclist.filter(doc => StrCast(doc.backgroundLayout).indexOf("istogram") !== -1) : [];
- // views.forEach((dstDoc, i) => {
- // views.forEach((srcDoc, j) => {
- // let dstTarg = dstDoc;
- // let srcTarg = srcDoc;
- // let x1 = NumCast(srcDoc.x);
- // let x2 = NumCast(dstDoc.x);
- // let x1w = NumCast(srcDoc.width, -1);
- // let x2w = NumCast(dstDoc.width, -1);
- // if (x1w < 0 || x2w < 0 || i === j) { }
- // else {
- // let findBrush = (field: (Doc | Promise<Doc>)[]) => field.findIndex(brush => {
- // let bdocs = brush instanceof Doc ? Cast(brush.brushingDocs, listSpec(Doc), []) : undefined;
- // return bdocs && bdocs.length && ((bdocs[0] === dstTarg && bdocs[1] === srcTarg)) ? true : false;
- // });
- // let brushAction = (field: (Doc | Promise<Doc>)[]) => {
- // let found = findBrush(field);
- // if (found !== -1) {
- // field.splice(found, 1);
- // }
- // };
- // if (Math.abs(x1 + x1w - x2) < 20) {
- // let linkDoc: Doc = new Doc();
- // linkDoc.title = "Histogram Brush";
- // linkDoc.linkDescription = "Brush between " + StrCast(srcTarg.title) + " and " + StrCast(dstTarg.Title);
- // linkDoc.brushingDocs = new List([dstTarg, srcTarg]);
-
- // brushAction = (field: (Doc | Promise<Doc>)[]) => {
- // if (findBrush(field) === -1) {
- // field.push(linkDoc);
- // }
- // };
- // }
- // if (dstTarg.brushingDocs === undefined) dstTarg.brushingDocs = new List<Doc>();
- // if (srcTarg.brushingDocs === undefined) srcTarg.brushingDocs = new List<Doc>();
- // let dstBrushDocs = Cast(dstTarg.brushingDocs, listSpec(Doc), []);
- // let srcBrushDocs = Cast(srcTarg.brushingDocs, listSpec(Doc), []);
- // brushAction(dstBrushDocs);
- // brushAction(srcBrushDocs);
- // }
- // });
- // });
- // });
- }
- componentWillUnmount() {
- this._brushReactionDisposer && this._brushReactionDisposer();
- }
@computed
get uniqueConnections() {
const connections = DocumentManager.Instance.LinkedDocumentViews.reduce((drawnPairs, connection) => {
@@ -86,8 +29,10 @@ export class CollectionFreeFormLinksView extends React.Component {
}
return drawnPairs;
}, [] as { a: DocumentView, b: DocumentView, l: Doc[] }[]);
- return connections.filter(c => c.a.props.Document.type === DocumentType.LINK) // get rid of the filter to show links to documents in addition to document anchors
- .map(c => <CollectionFreeFormLinkView key={Utils.GenerateGuid()} A={c.a} B={c.b} LinkDocs={c.l} />);
+ return connections.filter(c =>
+ c.a.props.layoutKey && c.b.props.layoutKey && c.a.props.Document.type === DocumentType.LINK &&
+ c.a.props.bringToFront !== emptyFunction && c.b.props.bringToFront !== emptyFunction // this prevents links to be drawn to anchors in CollectionTree views -- this is a hack that should be fixed
+ ).map(c => <CollectionFreeFormLinkView key={Utils.GenerateGuid()} A={c.a} B={c.b} LinkDocs={c.l} />);
}
render() {
@@ -98,4 +43,60 @@ export class CollectionFreeFormLinksView extends React.Component {
{this.props.children}
</div>;
}
+ // _brushReactionDisposer?: IReactionDisposer;
+ // componentDidMount() {
+ // this._brushReactionDisposer = reaction(
+ // () => {
+ // let doclist = DocListCast(this.props.Document[this.props.fieldKey]);
+ // return { doclist: doclist ? doclist : [], xs: doclist.map(d => d.x) };
+ // },
+ // () => {
+ // let doclist = DocListCast(this.props.Document[this.props.fieldKey]);
+ // let views = doclist ? doclist.filter(doc => StrCast(doc.backgroundLayout).indexOf("istogram") !== -1) : [];
+ // views.forEach((dstDoc, i) => {
+ // views.forEach((srcDoc, j) => {
+ // let dstTarg = dstDoc;
+ // let srcTarg = srcDoc;
+ // let x1 = NumCast(srcDoc.x);
+ // let x2 = NumCast(dstDoc.x);
+ // let x1w = NumCast(srcDoc.width, -1);
+ // let x2w = NumCast(dstDoc.width, -1);
+ // if (x1w < 0 || x2w < 0 || i === j) { }
+ // else {
+ // let findBrush = (field: (Doc | Promise<Doc>)[]) => field.findIndex(brush => {
+ // let bdocs = brush instanceof Doc ? Cast(brush.brushingDocs, listSpec(Doc), []) : undefined;
+ // return bdocs && bdocs.length && ((bdocs[0] === dstTarg && bdocs[1] === srcTarg)) ? true : false;
+ // });
+ // let brushAction = (field: (Doc | Promise<Doc>)[]) => {
+ // let found = findBrush(field);
+ // if (found !== -1) {
+ // field.splice(found, 1);
+ // }
+ // };
+ // if (Math.abs(x1 + x1w - x2) < 20) {
+ // let linkDoc: Doc = new Doc();
+ // linkDoc.title = "Histogram Brush";
+ // linkDoc.linkDescription = "Brush between " + StrCast(srcTarg.title) + " and " + StrCast(dstTarg.Title);
+ // linkDoc.brushingDocs = new List([dstTarg, srcTarg]);
+
+ // brushAction = (field: (Doc | Promise<Doc>)[]) => {
+ // if (findBrush(field) === -1) {
+ // field.push(linkDoc);
+ // }
+ // };
+ // }
+ // if (dstTarg.brushingDocs === undefined) dstTarg.brushingDocs = new List<Doc>();
+ // if (srcTarg.brushingDocs === undefined) srcTarg.brushingDocs = new List<Doc>();
+ // let dstBrushDocs = Cast(dstTarg.brushingDocs, listSpec(Doc), []);
+ // let srcBrushDocs = Cast(srcTarg.brushingDocs, listSpec(Doc), []);
+ // brushAction(dstBrushDocs);
+ // brushAction(srcBrushDocs);
+ // }
+ // });
+ // });
+ // });
+ // }
+ // componentWillUnmount() {
+ // this._brushReactionDisposer?.();
+ // }
} \ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx
index bb9ae4326..92fa2781c 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx
@@ -8,74 +8,65 @@ import { CollectionViewProps } from "../CollectionSubView";
import "./CollectionFreeFormView.scss";
import React = require("react");
import v5 = require("uuid/v5");
+import { computed } from "mobx";
+import { FieldResult } from "../../../../new_fields/Doc";
+import { List } from "../../../../new_fields/List";
@observer
export class CollectionFreeFormRemoteCursors extends React.Component<CollectionViewProps> {
- protected getCursors(): CursorField[] {
+ @computed protected get cursors(): CursorField[] {
const doc = this.props.Document;
- const id = CurrentUserUtils.id;
- if (!id) {
+ let cursors: FieldResult<List<CursorField>>;
+ const { id } = CurrentUserUtils;
+ if (!id || !(cursors = Cast(doc.cursors, listSpec(CursorField)))) {
return [];
}
-
- const cursors = Cast(doc.cursors, listSpec(CursorField));
-
const now = mobxUtils.now();
- // const now = Date.now();
- return (cursors || []).filter(cursor => cursor.data.metadata.id !== id && (now - cursor.data.metadata.timestamp) < 1000);
+ return (cursors || []).filter(({ data: { metadata } }) => metadata.id !== id && (now - metadata.timestamp) < 1000);
}
- private crosshairs?: HTMLCanvasElement;
- drawCrosshairs = (backgroundColor: string) => {
- if (this.crosshairs) {
- const ctx = this.crosshairs.getContext('2d');
- if (ctx) {
- ctx.fillStyle = backgroundColor;
- ctx.fillRect(0, 0, 20, 20);
-
- ctx.fillStyle = "black";
- ctx.lineWidth = 0.5;
-
- ctx.beginPath();
+ @computed get renderedCursors() {
+ return this.cursors.map(({ data: { metadata, position: { x, y } } }) => {
+ return (
+ <div key={metadata.id} className="collectionFreeFormRemoteCursors-cont"
+ style={{ transform: `translate(${x - 10}px, ${y - 10}px)` }}
+ >
+ <canvas className="collectionFreeFormRemoteCursors-canvas"
+ ref={(el) => {
+ if (el) {
+ const ctx = el.getContext('2d');
+ if (ctx) {
+ ctx.fillStyle = "#" + v5(metadata.id, v5.URL).substring(0, 6).toUpperCase() + "22";
+ ctx.fillRect(0, 0, 20, 20);
- ctx.moveTo(10, 0);
- ctx.lineTo(10, 8);
+ ctx.fillStyle = "black";
+ ctx.lineWidth = 0.5;
- ctx.moveTo(10, 20);
- ctx.lineTo(10, 12);
+ ctx.beginPath();
- ctx.moveTo(0, 10);
- ctx.lineTo(8, 10);
+ ctx.moveTo(10, 0);
+ ctx.lineTo(10, 8);
- ctx.moveTo(20, 10);
- ctx.lineTo(12, 10);
+ ctx.moveTo(10, 20);
+ ctx.lineTo(10, 12);
- ctx.stroke();
+ ctx.moveTo(0, 10);
+ ctx.lineTo(8, 10);
- // ctx.font = "10px Arial";
- // ctx.fillText(Doc.CurrentUserEmail[0].toUpperCase(), 10, 10);
- }
- }
- }
+ ctx.moveTo(20, 10);
+ ctx.lineTo(12, 10);
- get sharedCursors() {
- return this.getCursors().map(c => {
- const m = c.data.metadata;
- const l = c.data.position;
- this.drawCrosshairs("#" + v5(m.id, v5.URL).substring(0, 6).toUpperCase() + "22");
- return (
- <div key={m.id} className="collectionFreeFormRemoteCursors-cont"
- style={{ transform: `translate(${l.x - 10}px, ${l.y - 10}px)` }}
- >
- <canvas className="collectionFreeFormRemoteCursors-canvas"
- ref={(el) => { if (el) this.crosshairs = el; }}
+ ctx.stroke();
+ }
+ }
+ }}
width={20}
height={20}
/>
<p className="collectionFreeFormRemoteCursors-symbol">
- {m.identifier[0].toUpperCase()}
+ {metadata.identifier[0].toUpperCase()}
</p>
</div>
);
@@ -83,6 +74,6 @@ export class CollectionFreeFormRemoteCursors extends React.Component<CollectionV
}
render() {
- return this.sharedCursors;
+ return this.renderedCursors;
}
} \ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
index b70697e9a..730392ab5 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
@@ -22,6 +22,8 @@
.collectionFreeform-customText {
position: absolute;
text-align: center;
+ overflow-y: auto;
+ overflow-x: hidden;
}
.collectionfreeformview-container {
@@ -101,7 +103,7 @@
}
.pullpane-indicator {
- z-index: 999;
+ z-index: 99999;
background-color: rgba($color: #000000, $alpha: .4);
position: absolute;
} \ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index cc6a2f4a5..a0dd4f2de 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,22 +1,26 @@
import { library } from "@fortawesome/fontawesome-svg-core";
import { faEye } from "@fortawesome/free-regular-svg-icons";
import { faBraille, faChalkboard, faCompass, faCompressArrowsAlt, faExpandArrowsAlt, faFileUpload, faPaintBrush, faTable, faUpload } from "@fortawesome/free-solid-svg-icons";
-import { action, computed, observable, ObservableMap, reaction, runInAction, IReactionDisposer } from "mobx";
+import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction } from "mobx";
import { observer } from "mobx-react";
-import { Doc, DocListCast, HeightSym, Opt, WidthSym, DocListCastAsync, Field } from "../../../../new_fields/Doc";
+import { computedFn } from "mobx-utils";
+import { Doc, HeightSym, Opt, WidthSym } from "../../../../new_fields/Doc";
import { documentSchema, positionSchema } from "../../../../new_fields/documentSchemas";
import { Id } from "../../../../new_fields/FieldSymbols";
-import { InkTool, InkField, InkData } from "../../../../new_fields/InkField";
-import { createSchema, makeInterface } from "../../../../new_fields/Schema";
+import { InkData, InkField, InkTool } from "../../../../new_fields/InkField";
+import { List } from "../../../../new_fields/List";
+import { RichTextField } from "../../../../new_fields/RichTextField";
+import { createSchema, listSpec, makeInterface } from "../../../../new_fields/Schema";
import { ScriptField } from "../../../../new_fields/ScriptField";
-import { BoolCast, Cast, DateCast, NumCast, StrCast, ScriptCast, FieldValue } from "../../../../new_fields/Types";
-import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils";
-import { aggregateBounds, emptyFunction, intersectRect, returnOne, Utils } from "../../../../Utils";
+import { BoolCast, Cast, FieldValue, NumCast, ScriptCast, StrCast } from "../../../../new_fields/Types";
+import { TraceMobx } from "../../../../new_fields/util";
+import { GestureUtils } from "../../../../pen-gestures/GestureUtils";
+import { aggregateBounds, intersectRect, returnOne, Utils } from "../../../../Utils";
+import { CognitiveServices } from "../../../cognitive_services/CognitiveServices";
import { DocServer } from "../../../DocServer";
-import { Docs, DocUtils } from "../../../documents/Documents";
-import { DocumentType } from "../../../documents/DocumentTypes";
+import { Docs } from "../../../documents/Documents";
import { DocumentManager } from "../../../util/DocumentManager";
-import { DragManager } from "../../../util/DragManager";
+import { DragManager, dropActionType } from "../../../util/DragManager";
import { HistoryUtil } from "../../../util/History";
import { InteractionUtils } from "../../../util/InteractionUtils";
import { SelectionManager } from "../../../util/SelectionManager";
@@ -27,26 +31,18 @@ import { ContextMenu } from "../../ContextMenu";
import { ContextMenuProps } from "../../ContextMenuItem";
import { InkingControl } from "../../InkingControl";
import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView";
-import { DocumentContentsView } from "../../nodes/DocumentContentsView";
+import { DocumentViewProps } from "../../nodes/DocumentView";
import { FormattedTextBox } from "../../nodes/FormattedTextBox";
import { pageSchema } from "../../nodes/ImageBox";
import PDFMenu from "../../pdf/PDFMenu";
+import { CollectionDockingView } from "../CollectionDockingView";
import { CollectionSubView } from "../CollectionSubView";
-import { computePivotLayout, ViewDefResult } from "./CollectionFreeFormLayoutEngines";
+import { computePivotLayout, computeTimelineLayout, PoolData, ViewDefBounds, ViewDefResult } from "./CollectionFreeFormLayoutEngines";
import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCursors";
import "./CollectionFreeFormView.scss";
import MarqueeOptionsMenu from "./MarqueeOptionsMenu";
import { MarqueeView } from "./MarqueeView";
import React = require("react");
-import { computedFn } from "mobx-utils";
-import { TraceMobx } from "../../../../new_fields/util";
-import { GestureUtils } from "../../../../pen-gestures/GestureUtils";
-import { CognitiveServices } from "../../../cognitive_services/CognitiveServices";
-import { RichTextField } from "../../../../new_fields/RichTextField";
-import { List } from "../../../../new_fields/List";
-import { DocumentViewProps } from "../../nodes/DocumentView";
-import { CollectionDockingView } from "../CollectionDockingView";
-import { MainView } from "../../MainView";
library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload);
@@ -58,8 +54,8 @@ export const panZoomSchema = createSchema({
arrangeInit: ScriptField,
useClusters: "boolean",
fitToBox: "boolean",
- xPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set
- yPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set
+ _xPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set
+ _yPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set
panTransformType: "string",
scrollHeight: "number",
fitX: "number",
@@ -82,6 +78,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
private _hitCluster = false;
private _layoutComputeReaction: IReactionDisposer | undefined;
private _layoutPoolData = new ObservableMap<string, any>();
+ private _cachedPool: Map<string, any> = new Map();
@observable private _pullCoords: number[] = [0, 0];
@observable private _pullDirection: string = "";
@@ -122,26 +119,27 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
SelectionManager.DeselectAll();
docs.map(doc => DocumentManager.Instance.getDocumentView(doc)).map(dv => dv && SelectionManager.SelectDoc(dv, true));
}
- public isCurrent(doc: Doc) { return !doc.isMinimized && (Math.abs(NumCast(doc.displayTimecode, -1) - NumCast(this.Document.currentTimecode, -1)) < 1.5 || NumCast(doc.displayTimecode, -1) === -1); }
+ public isCurrent(doc: Doc) { return (Math.abs(NumCast(doc.displayTimecode, -1) - NumCast(this.Document.currentTimecode, -1)) < 1.5 || NumCast(doc.displayTimecode, -1) === -1); }
public getActiveDocuments = () => {
return this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => pair.layout);
}
@action
- onDrop = (e: React.DragEvent): Promise<void> => {
+ onExternalDrop = (e: React.DragEvent): Promise<void> => {
const pt = this.getTransform().transformPoint(e.pageX, e.pageY);
- return super.onDrop(e, { x: pt[0], y: pt[1] });
+ return super.onExternalDrop(e, { x: pt[0], y: pt[1] });
}
@undoBatch
@action
- drop = (e: Event, de: DragManager.DropEvent) => {
+ onInternalDrop = (e: Event, de: DragManager.DropEvent) => {
+ if (this.props.Document.isBackground) return false;
const xf = this.getTransform();
const xfo = this.getTransformOverlay();
const [xp, yp] = xf.transformPoint(de.x, de.y);
const [xpo, ypo] = xfo.transformPoint(de.x, de.y);
- if (super.drop(e, de)) {
+ if (super.onInternalDrop(e, de)) {
if (de.complete.docDragData) {
if (de.complete.docDragData.droppedDocuments.length) {
const firstDoc = de.complete.docDragData.droppedDocuments[0];
@@ -217,6 +215,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
}
@undoBatch
+ @action
updateClusters(useClusters: boolean) {
this.props.Document.useClusters = useClusters;
this._clusterSets.length = 0;
@@ -254,7 +253,6 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
docs.map(doc => this._clusterSets[doc.cluster = NumCast(docFirst.cluster)].push(doc));
}
childLayouts.map(child => !this._clusterSets.some((set, i) => Doc.IndexOf(child, set) !== -1 && child.cluster === i) && this.updateCluster(child));
- childLayouts.map(child => Doc.GetProto(child).clusterStr = child.cluster?.toString());
}
}
@@ -290,16 +288,16 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
}
getClusterColor = (doc: Doc) => {
- let clusterColor = "";
+ let clusterColor = this.props.backgroundColor?.(doc);
const cluster = NumCast(doc.cluster);
if (this.Document.useClusters) {
if (this._clusterSets.length <= cluster) {
setTimeout(() => this.updateCluster(doc), 0);
} else {
// choose a cluster color from a palette
- const colors = ["#da42429e", "#31ea318c", "#8c4000", "#4a7ae2c4", "#d809ff", "#ff7601", "#1dffff", "yellow", "#1b8231f2", "#000000ad"];
+ const colors = ["#da42429e", "#31ea318c", "rgba(197, 87, 20, 0.55)", "#4a7ae2c4", "rgba(216, 9, 255, 0.5)", "#ff7601", "#1dffff", "yellow", "rgba(27, 130, 49, 0.55)", "rgba(0, 0, 0, 0.268)"];
clusterColor = colors[cluster % colors.length];
- const set = this._clusterSets[cluster] && this._clusterSets[cluster].filter(s => s.backgroundColor && (s.backgroundColor !== s.defaultBackgroundColor));
+ const set = this._clusterSets[cluster]?.filter(s => s.backgroundColor);
// override the cluster color with an explicitly set color on a non-background document. then override that with an explicitly set color on a background document
set && set.filter(s => !s.isBackground).map(s => clusterColor = StrCast(s.backgroundColor));
set && set.filter(s => s.isBackground).map(s => clusterColor = StrCast(s.backgroundColor));
@@ -332,31 +330,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
this._lastX = e.pageX;
this._lastY = e.pageY;
}
- // eraser or scrubber plus anything else mode
+ // eraser plus anything else mode
else {
e.stopPropagation();
e.preventDefault();
}
}
- // if (e.button === 0 && !e.shiftKey && !e.altKey && !e.ctrlKey && this.props.active(true)) {
- // document.removeEventListener("pointermove", this.onPointerMove);
- // document.removeEventListener("pointerup", this.onPointerUp);
- // document.addEventListener("pointermove", this.onPointerMove);
- // document.addEventListener("pointerup", this.onPointerUp);
- // if (InkingControl.Instance.selectedTool === InkTool.None) {
- // this._lastX = e.pageX;
- // this._lastY = e.pageY;
- // }
- // else {
- // e.stopPropagation();
- // e.preventDefault();
-
- // if (InkingControl.Instance.selectedTool !== InkTool.Eraser && InkingControl.Instance.selectedTool !== InkTool.Scrubber) {
- // let point = this.getTransform().transformPoint(e.pageX, e.pageY);
- // this._points.push({ x: point[0], y: point[1] });
- // }
- // }
- // }
}
@action
@@ -470,7 +449,6 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
strokes.push(d.inkData.map(pd => ({ X: pd.X + x - left, Y: pd.Y + y - top })));
}
});
- console.log(strokes)
CognitiveServices.Inking.Appliers.InterpretStrokes(strokes).then((results) => {
console.log(results);
@@ -487,7 +465,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
if (this._wordPalette.has(word.recognizedText.toLowerCase())) {
inks[i].color = this._wordPalette.get(word.recognizedText.toLowerCase());
}
- else {
+ else if (word.alternates) {
for (const alt of word.alternates) {
if (this._wordPalette.has(alt.recognizedString.toLowerCase())) {
inks[i].color = this._wordPalette.get(alt.recognizedString.toLowerCase());
@@ -523,30 +501,29 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
@action
pan = (e: PointerEvent | React.Touch | { clientX: number, clientY: number }): void => {
// I think it makes sense for the marquee menu to go away when panned. -syip2
- MarqueeOptionsMenu.Instance.fadeOut(true);
+ MarqueeOptionsMenu.Instance && MarqueeOptionsMenu.Instance.fadeOut(true);
let x = this.Document._panX || 0;
let y = this.Document._panY || 0;
- const docs = this.childLayoutPairs.filter(pair => pair.layout instanceof Doc && !pair.layout.isMinimized).map(pair => pair.layout);
+ const docs = this.childLayoutPairs.filter(pair => pair.layout instanceof Doc).map(pair => pair.layout);
const [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY);
- if (!this.isAnnotationOverlay && docs.length) {
+ if (!this.isAnnotationOverlay && docs.length && this.childDataProvider(docs[0])) {
PDFMenu.Instance.fadeOut(true);
- const minx = this.childDataProvider(docs[0]).x;//docs.length ? NumCast(docs[0].x) : 0;
- const miny = this.childDataProvider(docs[0]).y;//docs.length ? NumCast(docs[0].y) : 0;
- const maxx = this.childDataProvider(docs[0]).width + minx;//docs.length ? NumCast(docs[0].width) + minx : minx;
- const maxy = this.childDataProvider(docs[0]).height + miny;//docs.length ? NumCast(docs[0].height) + miny : miny;
- const ranges = docs.filter(doc => doc).reduce((range, doc) => {
- const x = this.childDataProvider(doc).x;//NumCast(doc.x);
- const y = this.childDataProvider(doc).y;//NumCast(doc.y);
- const xe = this.childDataProvider(doc).width + x;//x + NumCast(layoutDoc.width);
- const ye = this.childDataProvider(doc).height + y; //y + NumCast(layoutDoc.height);
+ const minx = this.childDataProvider(docs[0]).x;
+ const miny = this.childDataProvider(docs[0]).y;
+ const maxx = this.childDataProvider(docs[0]).width + minx;
+ const maxy = this.childDataProvider(docs[0]).height + miny;
+ const ranges = docs.filter(doc => doc && this.childDataProvider(doc)).reduce((range, doc) => {
+ const x = this.childDataProvider(doc).x;
+ const y = this.childDataProvider(doc).y;
+ const xe = this.childDataProvider(doc).width + x;
+ const ye = this.childDataProvider(doc).height + y;
return [[range[0][0] > x ? x : range[0][0], range[0][1] < xe ? xe : range[0][1]],
[range[1][0] > y ? y : range[1][0], range[1][1] < ye ? ye : range[1][1]]];
}, [[minx, maxx], [miny, maxy]]);
const cscale = this.props.ContainingCollectionDoc ? NumCast(this.props.ContainingCollectionDoc.scale) : 1;
- const panelDim = this.props.ScreenToLocalTransform().transformDirection(this.props.PanelWidth() / this.zoomScaling() * cscale,
- this.props.PanelHeight() / this.zoomScaling() * cscale);
+ const panelDim = [this.props.PanelWidth() * cscale / this.zoomScaling(), this.props.PanelHeight() * cscale / this.zoomScaling()];
if (ranges[0][0] - dx > (this.panX() + panelDim[0] / 2)) x = ranges[0][1] + panelDim[0] / 2;
if (ranges[0][1] - dx < (this.panX() - panelDim[0] / 2)) x = ranges[0][0] - panelDim[0] / 2;
if (ranges[1][0] - dy > (this.panY() + panelDim[1] / 2)) y = ranges[1][1] + panelDim[1] / 2;
@@ -641,12 +618,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
// use the centerx and centery as the "new mouse position"
const centerX = Math.min(pt1.clientX, pt2.clientX) + Math.abs(pt2.clientX - pt1.clientX) / 2;
const centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2;
+ // const transformed = this.getTransform().inverse().transformPoint(centerX, centerY);
if (!this._pullDirection) { // if we are not bezel movement
this.pan({ clientX: centerX, clientY: centerY });
} else {
this._pullCoords = [centerX, centerY];
- console.log(MainView.Instance.flyoutWidth);
}
this._lastX = centerX;
@@ -673,20 +650,24 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
const centerY = Math.min(pt1.clientY, pt2.clientY) + Math.abs(pt2.clientY - pt1.clientY) / 2;
this._lastX = centerX;
this._lastY = centerY;
+ const screenBox = this._mainCont?.getBoundingClientRect();
+
// determine if we are using a bezel movement
- if ((this.props.PanelWidth() - this._lastX) < 100) {
- this._pullCoords = [this._lastX, this._lastY];
- this._pullDirection = "right";
- } else if (this._lastX < 100) {
- this._pullCoords = [this._lastX, this._lastY];
- this._pullDirection = "left";
- } else if (this.props.PanelHeight() - this._lastY < 100) {
- this._pullCoords = [this._lastX, this._lastY];
- this._pullDirection = "bottom";
- } else if (this._lastY < 120) { // to account for header
- this._pullCoords = [this._lastX, this._lastY];
- this._pullDirection = "top";
+ if (screenBox) {
+ if ((screenBox.right - centerX) < 100) {
+ this._pullCoords = [centerX, centerY];
+ this._pullDirection = "right";
+ } else if (centerX - screenBox.left < 100) {
+ this._pullCoords = [centerX, centerY];
+ this._pullDirection = "left";
+ } else if (screenBox.bottom - centerY < 100) {
+ this._pullCoords = [centerX, centerY];
+ this._pullDirection = "bottom";
+ } else if (centerY - screenBox.top < 100) {
+ this._pullCoords = [centerX, centerY];
+ this._pullDirection = "top";
+ }
}
@@ -700,27 +681,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
}
cleanUpInteractions = () => {
-
switch (this._pullDirection) {
-
case "left":
- console.log('pulled from left');
- CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([], { title: "New Collection" }), "left", undefined);
- break;
case "right":
- console.log('pulled from right');
- CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([], { title: "New Collection" }), "right", undefined);
- break;
case "top":
- console.log('pulled from top');
- CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([], { title: "New Collection" }), "top", undefined);
- break;
case "bottom":
- console.log('pulled from bottom');
- CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([], { title: "New Collection" }), "bottom", undefined);
- break;
- default:
- break;
+ CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([], { title: "New Collection" }), this._pullDirection);
}
this._pullDirection = "";
@@ -815,6 +781,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
const offset = annotOn && (contextHgt / 2 * 96 / 72);
this.props.Document.scrollY = NumCast(doc.y) - offset;
}
+
+ afterFocus && setTimeout(afterFocus, 1000);
} else {
const layoutdoc = Doc.Layout(doc);
const newPanX = NumCast(doc.x) + NumCast(layoutdoc._width) / 2;
@@ -832,7 +800,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
Doc.linkFollowHighlight(doc);
afterFocus && setTimeout(() => {
- if (afterFocus && afterFocus()) {
+ if (afterFocus?.()) {
this.Document._panX = savedState.px;
this.Document._panY = savedState.py;
this.Document.scale = savedState.s;
@@ -843,7 +811,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
}
- setScaleToZoom = (doc: Doc, scale: number = 0.5) => {
+ setScaleToZoom = (doc: Doc, scale: number = 0.75) => {
this.Document.scale = scale * Math.min(this.props.PanelWidth() / NumCast(doc._width), this.props.PanelHeight() / NumCast(doc._height));
}
@@ -855,6 +823,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
@computed get libraryPath() { return this.props.LibraryPath ? [...this.props.LibraryPath, this.props.Document] : []; }
@computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); }
+ backgroundHalo = () => BoolCast(this.Document.useClusters);
getChildDocumentViewProps(childLayout: Doc, childData?: Doc): DocumentViewProps {
return {
@@ -863,6 +832,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
Document: childLayout,
LibraryPath: this.libraryPath,
layoutKey: undefined,
+ dropAction: StrCast(this.props.Document.childDropAction) as dropActionType,
//onClick: undefined, // this.props.onClick, // bcz: check this out -- I don't think we want to inherit click handlers, or we at least need a way to ignore them
onClick: this.onChildClickHandler,
ScreenToLocalTransform: childLayout.z ? this.getTransformOverlay : this.getTransform,
@@ -874,6 +844,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
ContainingCollectionDoc: this.props.Document,
focus: this.focusDocument,
backgroundColor: this.getClusterColor,
+ backgroundHalo: this.backgroundHalo,
parentActive: this.props.active,
bringToFront: this.bringToFront,
zoomToScale: this.zoomToScale,
@@ -881,93 +852,123 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
};
}
- getCalculatedPositions(params: { doc: Doc, index: number, collection: Doc, docs: Doc[], state: any }): { x?: number, y?: number, z?: number, width?: number, height?: number, transition?: string, state?: any } {
+ getCalculatedPositions(params: { doc: Doc, index: number, collection: Doc, docs: Doc[], state: any }): PoolData {
const result = this.Document.arrangeScript?.script.run(params, console.log);
if (result?.success) {
return { ...result, transition: "transform 1s" };
}
const layoutDoc = Doc.Layout(params.doc);
- return { x: Cast(params.doc.x, "number"), y: Cast(params.doc.y, "number"), z: Cast(params.doc.z, "number"), width: Cast(layoutDoc._width, "number"), height: Cast(layoutDoc._height, "number") };
+ const { x, y, z, color, zIndex } = params.doc;
+ return {
+ x: NumCast(x), y: NumCast(y), z: Cast(z, "number"), color: StrCast(color), zIndex: Cast(zIndex, "number"),
+ width: Cast(layoutDoc._width, "number"), height: Cast(layoutDoc._height, "number")
+ };
}
- viewDefsToJSX = (views: any[]) => {
+ viewDefsToJSX = (views: ViewDefBounds[]) => {
return !Array.isArray(views) ? [] : views.filter(ele => this.viewDefToJSX(ele)).map(ele => this.viewDefToJSX(ele)!);
}
- private viewDefToJSX(viewDef: any): Opt<ViewDefResult> {
+ onViewDefDivClick = (e: React.MouseEvent, payload: any) => {
+ (this.props.Document.onViewDefDivClick as ScriptField)?.script.run({ this: this.props.Document, payload });
+ }
+ private viewDefToJSX(viewDef: ViewDefBounds): Opt<ViewDefResult> {
+ const { x, y, z } = viewDef;
+ const color = StrCast(viewDef.color);
+ const width = Cast(viewDef.width, "number");
+ const height = Cast(viewDef.height, "number");
+ const transform = `translate(${x}px, ${y}px)`;
if (viewDef.type === "text") {
const text = Cast(viewDef.text, "string"); // don't use NumCast, StrCast, etc since we want to test for undefined below
- const x = Cast(viewDef.x, "number");
- const y = Cast(viewDef.y, "number");
- const z = Cast(viewDef.z, "number");
- const width = Cast(viewDef.width, "number");
- const height = Cast(viewDef.height, "number");
const fontSize = Cast(viewDef.fontSize, "number");
- return [text, x, y, width, height].some(val => val === undefined) ? undefined :
+ return [text, x, y].some(val => val === undefined) ? undefined :
{
- ele: <div className="collectionFreeform-customText" key={(text || "") + x + y + z} style={{ width, height, fontSize, transform: `translate(${x}px, ${y}px)` }}>
+ ele: <div className="collectionFreeform-customText" key={(text || "") + x + y + z + color} style={{ width, height, color, fontSize, transform }}>
{text}
</div>,
- bounds: { x: x!, y: y!, z: z, width: width!, height: height! }
+ bounds: viewDef
+ };
+ } else if (viewDef.type === "div") {
+ return [x, y].some(val => val === undefined) ? undefined :
+ {
+ ele: <div className="collectionFreeform-customDiv" title={viewDef.payload?.join(" ")} key={"div" + x + y + z} onClick={e => this.onViewDefDivClick(e, viewDef)}
+ style={{ width, height, backgroundColor: color, transform }} />,
+ bounds: viewDef
};
}
}
childDataProvider = computedFn(function childDataProvider(this: any, doc: Doc) {
- if (!doc) {
- console.log(doc);
- }
return this._layoutPoolData.get(doc[Id]);
}.bind(this));
- doPivotLayout(poolData: ObservableMap<string, any>) {
- return computePivotLayout(poolData, this.props.Document, this.childDocs,
- this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)), [this.props.PanelWidth(), this.props.PanelHeight()], this.viewDefsToJSX);
+ doTimelineLayout(poolData: Map<string, PoolData>) {
+ return computeTimelineLayout(poolData, this.props.Document, this.childDocs, this.childDocs,
+ this.childLayoutPairs, [this.props.PanelWidth(), this.props.PanelHeight()], this.viewDefsToJSX);
+ }
+
+ doPivotLayout(poolData: Map<string, PoolData>) {
+ return computePivotLayout(poolData, this.props.Document, this.childDocs, this.childDocs,
+ this.childLayoutPairs, [this.props.PanelWidth(), this.props.PanelHeight()], this.viewDefsToJSX);
}
- doFreeformLayout(poolData: ObservableMap<string, any>) {
+ doFreeformLayout(poolData: Map<string, PoolData>) {
const layoutDocs = this.childLayoutPairs.map(pair => pair.layout);
const initResult = this.Document.arrangeInit && this.Document.arrangeInit.script.run({ docs: layoutDocs, collection: this.Document }, console.log);
- let state = initResult && initResult.success ? initResult.result.scriptState : undefined;
+ const state = initResult && initResult.success ? initResult.result.scriptState : undefined;
const elements = initResult && initResult.success ? this.viewDefsToJSX(initResult.result.views) : [];
this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map((pair, i) => {
- const data = poolData.get(pair.layout[Id]);
const pos = this.getCalculatedPositions({ doc: pair.layout, index: i, collection: this.Document, docs: layoutDocs, state });
- state = pos.state === undefined ? state : pos.state;
- if (!data || pos.x !== data.x || pos.y !== data.y || pos.z !== data.z || pos.width !== data.width || pos.height !== data.height || pos.transition !== data.transition) {
- runInAction(() => poolData.set(pair.layout[Id], pos));
- }
+ poolData.set(pair.layout[Id], pos);
});
- return { elements: elements };
+ return elements;
}
- get doLayoutComputation() {
- let computedElementData: { elements: ViewDefResult[] };
- switch (this.Document._freeformLayoutEngine) {
- case "pivot": computedElementData = this.doPivotLayout(this._layoutPoolData); break;
- default: computedElementData = this.doFreeformLayout(this._layoutPoolData); break;
+ @computed get doInternalLayoutComputation() {
+ const newPool = new Map<string, any>();
+ switch (this.props.layoutEngine?.()) {
+ case "timeline": return { newPool, computedElementData: this.doTimelineLayout(newPool) };
+ case "pivot": return { newPool, computedElementData: this.doPivotLayout(newPool) };
}
- this.childLayoutPairs.filter((pair, i) => this.isCurrent(pair.layout)).forEach(pair =>
- computedElementData.elements.push({
+ return { newPool, computedElementData: this.doFreeformLayout(newPool) };
+ }
+
+ childLayoutDocFunc = () => this.props.childLayoutTemplate?.() || Cast(this.props.Document.childLayoutTemplate, Doc, null);
+ get doLayoutComputation() {
+ const { newPool, computedElementData } = this.doInternalLayoutComputation;
+ runInAction(() =>
+ Array.from(newPool.keys()).map(key => {
+ const lastPos = this._cachedPool.get(key); // last computed pos
+ const newPos = newPool.get(key);
+ if (!lastPos || newPos.x !== lastPos.x || newPos.y !== lastPos.y || newPos.z !== lastPos.z || newPos.zIndex !== lastPos.zIndex || newPos.width !== lastPos.width || newPos.height !== lastPos.height) {
+ this._layoutPoolData.set(key, newPos);
+ }
+ }));
+ this._cachedPool.clear();
+ Array.from(newPool.keys()).forEach(k => this._cachedPool.set(k, newPool.get(k)));
+ const elements: ViewDefResult[] = computedElementData.slice();
+ this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).forEach(pair =>
+ elements.push({
ele: <CollectionFreeFormDocumentView key={pair.layout[Id]} {...this.getChildDocumentViewProps(pair.layout, pair.data)}
dataProvider={this.childDataProvider}
+ LayoutDoc={this.childLayoutDocFunc}
jitterRotation={NumCast(this.props.Document.jitterRotation)}
- fitToBox={this.props.fitToBox || this.Document._freeformLayoutEngine === "pivot"} />,
+ fitToBox={this.props.fitToBox || this.props.layoutEngine !== undefined} />,
bounds: this.childDataProvider(pair.layout)
}));
- return computedElementData;
+ return elements;
}
componentDidMount() {
super.componentDidMount();
- this._layoutComputeReaction = reaction(() => { TraceMobx(); return this.doLayoutComputation; },
- action((computation: { elements: ViewDefResult[] }) => computation && (this._layoutElements = computation.elements)),
+ this._layoutComputeReaction = reaction(() => this.doLayoutComputation,
+ (elements) => this._layoutElements = elements || [],
{ fireImmediately: true, name: "doLayout" });
}
componentWillUnmount() {
- this._layoutComputeReaction && this._layoutComputeReaction();
+ this._layoutComputeReaction?.();
}
@computed get views() { return this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z).map(ele => ele.ele); }
elementFunc = () => this._layoutElements;
@@ -979,23 +980,23 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
layoutDocsInGrid = () => {
UndoManager.RunInBatch(() => {
- const docs = DocListCast(this.Document[this.props.fieldKey]);
+ const docs = this.childLayoutPairs;
const startX = this.Document._panX || 0;
let x = startX;
let y = this.Document._panY || 0;
let i = 0;
- const width = Math.max(...docs.map(doc => NumCast(doc._width)));
- const height = Math.max(...docs.map(doc => NumCast(doc._height)));
- for (const doc of docs) {
- doc.x = x;
- doc.y = y;
+ const width = Math.max(...docs.map(doc => NumCast(doc.layout._width)));
+ const height = Math.max(...docs.map(doc => NumCast(doc.layout._height)));
+ docs.forEach(pair => {
+ pair.layout.x = x;
+ pair.layout.y = y;
x += width + 20;
if (++i === 6) {
i = 0;
x = startX;
y += height + 20;
}
- }
+ });
}, "arrange contents");
}
@@ -1043,6 +1044,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
// }
onContextMenu = (e: React.MouseEvent) => {
+ if (this.props.children && this.props.annotationsKey) return;
const layoutItems: ContextMenuProps[] = [];
layoutItems.push({ description: "reset view", event: () => { this.props.Document._panX = this.props.Document._panY = 0; this.props.Document.scale = 1; }, icon: "compress-arrows-alt" });
@@ -1080,19 +1082,9 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
}
});
- layoutItems.push({
- description: "Add Note ...",
- subitems: DocListCast((CurrentUserUtils.UserDocument.noteTypes as Doc).data).map((note, i) => ({
- description: (i + 1) + ": " + StrCast(note.title),
- event: (args: { x: number, y: number }) => this.addLiveTextBox(Docs.Create.TextDocument("", { _width: 200, _height: 100, x: this.getTransform().transformPoint(args.x, args.y)[0], y: this.getTransform().transformPoint(args.x, args.y)[1], _autoHeight: true, layout: note, title: StrCast(note.title) })),
- icon: "eye"
- })) as ContextMenuProps[],
- icon: "eye"
- });
ContextMenu.Instance.addItem({ description: "Freeform Options ...", subitems: layoutItems, icon: "eye" });
}
-
private childViews = () => {
const children = typeof this.props.children === "function" ? (this.props.children as any)() as JSX.Element[] : [];
return [
@@ -1101,13 +1093,9 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
];
}
- // @observable private _palette?: JSX.Element;
-
children = () => {
const eles: JSX.Element[] = [];
eles.push(...this.childViews());
- // this._palette && (eles.push(this._palette));
- // this.currentStroke && (eles.push(this.currentStroke));
eles.push(<CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" />);
return eles;
}
@@ -1134,6 +1122,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
}
render() {
TraceMobx();
+ const clientRect = this._mainCont?.getBoundingClientRect();
// update the actual dimensions of the collection so that they can inquired (e.g., by a minimap)
// this.Document.fitX = this.contentBounds && this.contentBounds.x;
// this.Document.fitY = this.contentBounds && this.contentBounds.y;
@@ -1141,11 +1130,10 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
// this.Document.fitH = this.contentBounds && (this.contentBounds.b - this.contentBounds.y);
// if isAnnotationOverlay is set, then children will be stored in the extension document for the fieldKey.
// otherwise, they are stored in fieldKey. All annotations to this document are stored in the extension document
- // let lodarea = this.Document[WidthSym]() * this.Document[HeightSym]() / this.props.ScreenToLocalTransform().Scale / this.props.ScreenToLocalTransform().Scale;
return <div className={"collectionfreeformview-container"}
ref={this.createDashEventsTarget}
onWheel={this.onPointerWheel}//pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined,
- onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onContextMenu={this.onContextMenu}
+ onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onExternalDrop.bind(this)} onContextMenu={this.onContextMenu}
style={{
pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined,
transform: this.contentScaling ? `scale(${this.contentScaling})` : "",
@@ -1153,23 +1141,19 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
width: this.contentScaling ? `${100 / this.contentScaling}%` : "",
height: this.contentScaling ? `${100 / this.contentScaling}%` : this.isAnnotationOverlay ? (this.props.Document.scrollHeight ? this.Document.scrollHeight : "100%") : this.props.PanelHeight()
}}>
- {!this.Document._LODdisable && !this.props.active() && !this.props.isAnnotationOverlay && !this.props.annotationsKey && this.props.renderDepth > 0 ? // && this.props.CollectionView && lodarea < NumCast(this.Document.LODarea, 100000) ?
+ {!this.Document._LODdisable && !this.props.active() && !this.props.isAnnotationOverlay && !this.props.annotationsKey && this.props.renderDepth > 0 ?
this.placeholder : this.marqueeView}
<CollectionFreeFormOverlayView elements={this.elementFunc} />
<div className={"pullpane-indicator"}
style={{
display: this._pullDirection ? "block" : "none",
- // width: this._pullDirection === "left" || this._pullDirection === "right" ? Math.abs(this.props.PanelWidth() - this._pullCoords[0]) : this.props.PanelWidth(),
- // height: this._pullDirection === "top" || this._pullDirection === "bottom" ? Math.abs(this.props.PanelHeight() - this._pullCoords[1]) : this.props.PanelHeight(),
- // top: this._pullDirection === "bottom" ? this._pullCoords[0] : 0,
- // left: this._pullDirection === "right" ? this._pullCoords[1] : 0
- width: this._pullDirection === "left" ? this._pullCoords[0] : this._pullDirection === "right" ? MainView.Instance.getPWidth() - this._pullCoords[0] + MainView.Instance.flyoutWidth : MainView.Instance.getPWidth(),
- height: this._pullDirection === "top" ? this._pullCoords[1] : this._pullDirection === "bottom" ? MainView.Instance.getPHeight() - this._pullCoords[1] : MainView.Instance.getPHeight(),
- left: this._pullDirection === "right" ? undefined : 0,
- right: this._pullDirection === "right" ? 0 : undefined,
- top: this._pullDirection === "bottom" ? undefined : 0,
- bottom: this._pullDirection === "bottom" ? 0 : undefined
+ top: clientRect ? this._pullDirection === "bottom" ? this._pullCoords[1] - clientRect.y : 0 : "auto",
+ // left: clientRect ? this._pullDirection === "right" ? this._pullCoords[0] - clientRect.x - MainView.Instance.flyoutWidth : 0 : "auto",
+ left: clientRect ? this._pullDirection === "right" ? this._pullCoords[0] - clientRect.x : 0 : "auto",
+ width: clientRect ? this._pullDirection === "left" ? this._pullCoords[0] - clientRect.left : this._pullDirection === "right" ? clientRect.right - this._pullCoords[0] : clientRect.width : 0,
+ height: clientRect ? this._pullDirection === "top" ? this._pullCoords[1] - clientRect.top : this._pullDirection === "bottom" ? clientRect.bottom - this._pullCoords[1] : clientRect.height : 0,
+
}}>
</div>
@@ -1184,7 +1168,7 @@ interface CollectionFreeFormOverlayViewProps {
@observer
class CollectionFreeFormOverlayView extends React.Component<CollectionFreeFormOverlayViewProps>{
render() {
- return this.props.elements().filter(ele => ele.bounds && ele.bounds.z).map(ele => ele.ele);
+ return this.props.elements().filter(ele => ele.bounds?.z).map(ele => ele.ele);
}
}
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index e82ca6bf2..276a49570 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -1,27 +1,26 @@
import { action, computed, observable } from "mobx";
import { observer } from "mobx-react";
import { Doc, DocListCast, DataSym, WidthSym, HeightSym } from "../../../../new_fields/Doc";
-import { InkField } from "../../../../new_fields/InkField";
+import { InkField, InkData } from "../../../../new_fields/InkField";
import { List } from "../../../../new_fields/List";
-import { listSpec } from "../../../../new_fields/Schema";
import { SchemaHeaderField } from "../../../../new_fields/SchemaHeaderField";
-import { ComputedField } from "../../../../new_fields/ScriptField";
-import { Cast, NumCast, StrCast, FieldValue } from "../../../../new_fields/Types";
+import { Cast, NumCast, FieldValue, StrCast } from "../../../../new_fields/Types";
import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils";
import { Utils } from "../../../../Utils";
-import { Docs } from "../../../documents/Documents";
+import { Docs, DocUtils } from "../../../documents/Documents";
import { SelectionManager } from "../../../util/SelectionManager";
import { Transform } from "../../../util/Transform";
import { undoBatch } from "../../../util/UndoManager";
+import { ContextMenu } from "../../ContextMenu";
import { PreviewCursor } from "../../PreviewCursor";
-import { CollectionViewType } from "../CollectionView";
+import { SubCollectionViewProps } from "../CollectionSubView";
+import MarqueeOptionsMenu from "./MarqueeOptionsMenu";
import "./MarqueeView.scss";
import React = require("react");
-import MarqueeOptionsMenu from "./MarqueeOptionsMenu";
-import { SubCollectionViewProps } from "../CollectionSubView";
import { CognitiveServices } from "../../../cognitive_services/CognitiveServices";
import { RichTextField } from "../../../../new_fields/RichTextField";
-import { InteractionUtils } from "../../../util/InteractionUtils";
+import { CollectionView } from "../CollectionView";
+import { FormattedTextBox } from "../../nodes/FormattedTextBox";
interface MarqueeViewProps {
getContainerTransform: () => Transform;
@@ -69,7 +68,11 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
//make textbox and add it to this collection
// tslint:disable-next-line:prefer-const
let [x, y] = this.props.getTransform().transformPoint(this._downX, this._downY);
- if (e.key === "q" && e.ctrlKey) {
+ if (e.key === ":") {
+ DocUtils.addDocumentCreatorMenuItems(this.props.addLiveTextDocument, this.props.addDocument, x, y);
+
+ ContextMenu.Instance.displayMenu(this._downX, this._downY);
+ } else if (e.key === "q" && e.ctrlKey) {
e.preventDefault();
(async () => {
const text: string = await navigator.clipboard.readText();
@@ -103,13 +106,15 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
}
});
} else if (!e.ctrlKey) {
- this.props.addLiveTextDocument(
- Docs.Create.TextDocument("", { _width: 200, _height: 100, x: x, y: y, _autoHeight: true, title: "-typed text-" }));
- } else if (e.keyCode > 48 && e.keyCode <= 57) {
- const notes = DocListCast((CurrentUserUtils.UserDocument.noteTypes as Doc).data);
- const text = Docs.Create.TextDocument("", { _width: 200, _height: 100, x: x, y: y, _autoHeight: true, title: "-typed text-" });
- text.layout = notes[(e.keyCode - 49) % notes.length];
- this.props.addLiveTextDocument(text);
+ FormattedTextBox.SelectOnLoadChar = FormattedTextBox.DefaultLayout ? e.key : "";
+ const tbox = Docs.Create.TextDocument("", { _width: 200, _height: 100, x: x, y: y, _autoHeight: true, title: "-typed text-" });
+ const template = FormattedTextBox.DefaultLayout;
+ if (template instanceof Doc) {
+ tbox._width = NumCast(template._width);
+ tbox.layoutKey = "layout_" + StrCast(template.title);
+ tbox[StrCast(tbox.layoutKey)] = template;
+ }
+ this.props.addLiveTextDocument(tbox);
}
e.stopPropagation();
}
@@ -303,27 +308,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
this.hideMarquee();
}
- getCollection = (selected: Doc[], asTemplate: boolean) => {
+ getCollection = (selected: Doc[], asTemplate: boolean, isBackground: boolean = false) => {
const bounds = this.Bounds;
- const defaultPalette = ["rgb(114,229,239)", "rgb(255,246,209)", "rgb(255,188,156)", "rgb(247,220,96)", "rgb(122,176,238)",
- "rgb(209,150,226)", "rgb(127,235,144)", "rgb(252,188,189)", "rgb(247,175,81)",];
- const colorPalette = Cast(this.props.Document.colorPalette, listSpec("string"));
- if (!colorPalette) this.props.Document.colorPalette = new List<string>(defaultPalette);
- const palette = Array.from(Cast(this.props.Document.colorPalette, listSpec("string")) as string[]);
- const usedPaletted = new Map<string, number>();
- [...this.props.activeDocuments(), this.props.Document].map(child => {
- const bg = StrCast(Doc.Layout(child).backgroundColor);
- if (palette.indexOf(bg) !== -1) {
- palette.splice(palette.indexOf(bg), 1);
- if (usedPaletted.get(bg)) usedPaletted.set(bg, usedPaletted.get(bg)! + 1);
- else usedPaletted.set(bg, 1);
- }
- });
- usedPaletted.delete("#f1efeb");
- usedPaletted.delete("white");
- usedPaletted.delete("rgba(255,255,255,1)");
- const usedSequnce = Array.from(usedPaletted.keys()).sort((a, b) => usedPaletted.get(a)! < usedPaletted.get(b)! ? -1 : usedPaletted.get(a)! > usedPaletted.get(b)! ? 1 : 0);
- const chosenColor = (usedPaletted.size === 0) ? "white" : palette.length ? palette[0] : usedSequnce[0];
// const inkData = this.ink ? this.ink.inkData : undefined;
const creator = asTemplate ? Docs.Create.StackingDocument : Docs.Create.FreeformDocument;
const newCollection = creator(selected, {
@@ -331,8 +317,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
y: bounds.top,
_panX: 0,
_panY: 0,
- backgroundColor: this.props.isAnnotationOverlay ? undefined : chosenColor,
- defaultBackgroundColor: this.props.isAnnotationOverlay ? undefined : chosenColor,
+ isBackground,
+ backgroundColor: this.props.isAnnotationOverlay ? "#00000015" : isBackground ? "cyan" : undefined,
_width: bounds.width,
_height: bounds.height,
_LODdisable: true,
@@ -354,11 +340,11 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
this.props.removeDocument(d);
d.x = NumCast(d.x) - bounds.left - bounds.width / 2;
d.y = NumCast(d.y) - bounds.top - bounds.height / 2;
- d.displayTimecode = undefined;
+ d.displayTimecode = undefined; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection
return d;
});
}
- const newCollection = this.getCollection(selected, e.key === "t");
+ const newCollection = this.getCollection(selected, (e as KeyboardEvent)?.key === "t");
this.props.addDocument(newCollection);
this.props.selectDocuments([newCollection], []);
MarqueeOptionsMenu.Instance.fadeOut(true);
@@ -382,28 +368,65 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
wordToColor.set(word, colors[i]);
});
});
- const inkFields = inks.map(i => Cast(i.data, InkField));
- CognitiveServices.Inking.Appliers.InterpretStrokes(inkFields.filter(i => i instanceof InkField).map(i => i!.inkData)).then((results) => {
- const wordResults = results.filter((r: any) => r.category === "inkWord");
- console.log(wordResults);
- console.log(results);
- for (const word of wordResults) {
- const indices: number[] = word.strokeIds;
- indices.forEach(i => {
- if (wordToColor.has(word.recognizedText.toLowerCase())) {
- inks[i].color = wordToColor.get(word.recognizedText.toLowerCase());
- }
- else {
- for (const alt of word.alternates) {
- if (wordToColor.has(alt.recognizedString.toLowerCase())) {
- inks[i].color = wordToColor.get(alt.recognizedString.toLowerCase());
- break;
- }
- }
- }
- })
+ const strokes: InkData[] = [];
+ inks.forEach(i => {
+ const d = Cast(i.data, InkField);
+ const x = NumCast(i.x);
+ const y = NumCast(i.y);
+ const left = Math.min(...d?.inkData.map(pd => pd.X) ?? [0]);
+ const top = Math.min(...d?.inkData.map(pd => pd.Y) ?? [0]);
+ if (d) {
+ strokes.push(d.inkData.map(pd => ({ X: pd.X + x - left, Y: pd.Y + y - top })));
}
});
+ CognitiveServices.Inking.Appliers.InterpretStrokes(strokes).then((results) => {
+ // const wordResults = results.filter((r: any) => r.category === "inkWord");
+ // console.log(wordResults);
+ // console.log(results);
+ // for (const word of wordResults) {
+ // const indices: number[] = word.strokeIds;
+ // indices.forEach(i => {
+ // if (wordToColor.has(word.recognizedText.toLowerCase())) {
+ // inks[i].color = wordToColor.get(word.recognizedText.toLowerCase());
+ // }
+ // else {
+ // for (const alt of word.alternates) {
+ // if (wordToColor.has(alt.recognizedString.toLowerCase())) {
+ // inks[i].color = wordToColor.get(alt.recognizedString.toLowerCase());
+ // break;
+ // }
+ // }
+ // }
+ // })
+ // }
+ // const wordResults = results.filter((r: any) => r.category === "inkWord");
+ // for (const word of wordResults) {
+ // const indices: number[] = word.strokeIds;
+ // indices.forEach(i => {
+ // const otherInks: Doc[] = [];
+ // indices.forEach(i2 => i2 !== i && otherInks.push(inks[i2]));
+ // inks[i].relatedInks = new List<Doc>(otherInks);
+ // const uniqueColors: string[] = [];
+ // Array.from(wordToColor.values()).forEach(c => uniqueColors.indexOf(c) === -1 && uniqueColors.push(c));
+ // inks[i].alternativeColors = new List<string>(uniqueColors);
+ // if (wordToColor.has(word.recognizedText.toLowerCase())) {
+ // inks[i].color = wordToColor.get(word.recognizedText.toLowerCase());
+ // }
+ // else if (word.alternates) {
+ // for (const alt of word.alternates) {
+ // if (wordToColor.has(alt.recognizedString.toLowerCase())) {
+ // inks[i].color = wordToColor.get(alt.recognizedString.toLowerCase());
+ // break;
+ // }
+ // }
+ // }
+ // });
+ // }
+ const lines = results.filter((r: any) => r.category === "line");
+ console.log(lines);
+ const text = lines.map((l: any) => l.recognizedText).join("\r\n");
+ this.props.addDocument(Docs.Create.TextDocument(text, { _width: this.Bounds.width, _height: this.Bounds.height, x: this.Bounds.left + this.Bounds.width, y: this.Bounds.top, title: text }));
+ });
}
}
@@ -411,8 +434,6 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
summary = (e: KeyboardEvent | React.PointerEvent | undefined) => {
const bounds = this.Bounds;
const selected = this.marqueeSelect(false);
- const newCollection = this.getCollection(selected);
-
selected.map(d => {
this.props.removeDocument(d);
d.x = NumCast(d.x) - bounds.left - bounds.width / 2;
@@ -420,25 +441,26 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
d.page = -1;
return d;
});
- newCollection._chromeStatus = "disabled";
- const summary = Docs.Create.TextDocument("", { x: bounds.left, y: bounds.top, _width: 300, _height: 100, _autoHeight: true, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" });
- Doc.GetProto(summary).summarizedDocs = new List<Doc>([newCollection]);
- newCollection.x = bounds.left + bounds.width;
- Doc.GetProto(newCollection).summaryDoc = summary;
- Doc.GetProto(newCollection).title = ComputedField.MakeFunction(`summaryTitle(this);`);
- if (e instanceof KeyboardEvent ? e.key === "s" : true) { // summary is wrapped in an expand/collapse container that also contains the summarized documents in a free form view.
- const container = Docs.Create.FreeformDocument([summary, newCollection], {
- x: bounds.left, y: bounds.top, _width: 300, _height: 200, _autoHeight: true,
- _viewType: CollectionViewType.Stacking, _chromeStatus: "disabled", title: "-summary-"
- });
- Doc.GetProto(summary).maximizeLocation = "inPlace"; // or "onRight"
- this.props.addLiveTextDocument(container);
- } else if (e instanceof KeyboardEvent ? e.key === "S" : false) { // the summary stands alone, but is linked to a collection of the summarized documents - set the OnCLick behavior to link follow to access them
- Doc.GetProto(summary).maximizeLocation = "inTab"; // or "inPlace", or "onRight"
- this.props.addLiveTextDocument(summary);
- }
+ const summary = Docs.Create.TextDocument("", { x: bounds.left + bounds.width / 2, y: bounds.top + bounds.height / 2, _width: 200, _height: 200, _fitToBox: true, _showSidebar: true, title: "overview" });
+ const portal = Doc.MakeAlias(summary);
+ Doc.GetProto(summary)[Doc.LayoutFieldKey(summary) + "-annotations"] = new List<Doc>(selected);
+ Doc.GetProto(summary).layout_portal = CollectionView.LayoutString(Doc.LayoutFieldKey(summary) + "-annotations");
+ summary._backgroundColor = "#e2ad32";
+ portal.layoutKey = "layout_portal";
+ portal.title = "document collection";
+ DocUtils.MakeLink({ doc: summary }, { doc: portal }, "summarizing");
+
+ this.props.addLiveTextDocument(summary);
MarqueeOptionsMenu.Instance.fadeOut(true);
}
+ @action
+ background = (e: KeyboardEvent | React.PointerEvent | undefined) => {
+ const newCollection = this.getCollection([], false, true);
+ this.props.addDocument(newCollection);
+ MarqueeOptionsMenu.Instance.fadeOut(true);
+ this.hideMarquee();
+ setTimeout(() => this.props.selectDocuments([newCollection], []), 0);
+ }
@undoBatch
@action
@@ -453,7 +475,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
this.delete();
e.stopPropagation();
}
- if (e.key === "c" || e.key === "t" || e.key === "s" || e.key === "S") {
+ if (e.key === "c" || e.key === "b" || e.key === "t" || e.key === "s" || e.key === "S") {
this._commandExecuted = true;
e.stopPropagation();
e.preventDefault();
@@ -461,10 +483,12 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
if (e.key === "c" || e.key === "t") {
this.collection(e);
}
-
if (e.key === "s" || e.key === "S") {
this.summary(e);
}
+ if (e.key === "b") {
+ this.background(e);
+ }
this.cleanupInteractions(false);
}
}
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss
index f57ba438a..821c8d804 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss
@@ -1,12 +1,13 @@
.collectionMulticolumnView_contents {
display: flex;
+ overflow: hidden;
width: 100%;
height: 100%;
- overflow: hidden;
.document-wrapper {
display: flex;
flex-direction: column;
+ width: 100%;
.label-wrapper {
display: flex;
@@ -17,13 +18,13 @@
}
- .resizer {
+ .multiColumnResizer {
cursor: ew-resize;
transition: 0.5s opacity ease;
display: flex;
flex-direction: column;
- .internal {
+ .multiColumnResizer-hdl {
width: 100%;
height: 100%;
transition: 0.5s background-color ease;
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
index 041eb69da..aa8e1fb43 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
@@ -1,19 +1,19 @@
+import { action, computed } from 'mobx';
import { observer } from 'mobx-react';
-import { makeInterface } from '../../../../new_fields/Schema';
-import { documentSchema } from '../../../../new_fields/documentSchemas';
-import { CollectionSubView, SubCollectionViewProps } from '../CollectionSubView';
import * as React from "react";
import { Doc } from '../../../../new_fields/Doc';
-import { NumCast, StrCast, BoolCast, ScriptCast } from '../../../../new_fields/Types';
+import { documentSchema } from '../../../../new_fields/documentSchemas';
+import { makeInterface } from '../../../../new_fields/Schema';
+import { BoolCast, NumCast, ScriptCast, StrCast, Cast } from '../../../../new_fields/Types';
+import { DragManager } from '../../../util/DragManager';
+import { Transform } from '../../../util/Transform';
+import { undoBatch } from '../../../util/UndoManager';
import { ContentFittingDocumentView } from '../../nodes/ContentFittingDocumentView';
-import { Utils } from '../../../../Utils';
+import { CollectionSubView } from '../CollectionSubView';
import "./collectionMulticolumnView.scss";
-import { computed, trace, observable, action } from 'mobx';
-import { Transform } from '../../../util/Transform';
-import WidthLabel from './MulticolumnWidthLabel';
import ResizeBar from './MulticolumnResizer';
-import { undoBatch } from '../../../util/UndoManager';
-import { DragManager } from '../../../util/DragManager';
+import WidthLabel from './MulticolumnWidthLabel';
+import { List } from '../../../../new_fields/List';
type MulticolumnDocument = makeInterface<[typeof documentSchema]>;
const MulticolumnDocument = makeInterface(documentSchema);
@@ -28,13 +28,13 @@ interface LayoutData {
starSum: number;
}
-export const WidthUnit = {
+export const DimUnit = {
Pixel: "px",
Ratio: "*"
};
-const resolvedUnits = Object.values(WidthUnit);
-const resizerWidth = 4;
+const resolvedUnits = Object.values(DimUnit);
+const resizerWidth = 8;
@observer
export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocument) {
@@ -45,12 +45,12 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
*/
@computed
private get ratioDefinedDocs() {
- return this.childLayoutPairs.map(({ layout }) => layout).filter(({ widthUnit }) => StrCast(widthUnit) === WidthUnit.Ratio);
+ return this.childLayoutPairs.map(pair => pair.layout).filter(layout => StrCast(layout.dimUnit, "*") === DimUnit.Ratio);
}
/**
- * This loops through all childLayoutPairs and extracts the values for widthUnit
- * and widthMagnitude, ignoring any that are malformed. Additionally, it then
+ * This loops through all childLayoutPairs and extracts the values for dimUnit
+ * and dimMagnitude, ignoring any that are malformed. Additionally, it then
* normalizes the ratio values so that one * value is always 1, with the remaining
* values proportionate to that easily readable metric.
* @returns the list of the resolved width specifiers (unit and magnitude pairs)
@@ -60,11 +60,11 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
private get resolvedLayoutInformation(): LayoutData {
let starSum = 0;
const widthSpecifiers: WidthSpecifier[] = [];
- this.childLayoutPairs.map(({ layout: { widthUnit, widthMagnitude } }) => {
- const unit = StrCast(widthUnit);
- const magnitude = NumCast(widthMagnitude);
+ this.childLayoutPairs.map(pair => {
+ const unit = StrCast(pair.layout.dimUnit, "*");
+ const magnitude = NumCast(pair.layout.dimMagnitude, 1);
if (unit && magnitude && magnitude > 0 && resolvedUnits.includes(unit)) {
- (unit === WidthUnit.Ratio) && (starSum += magnitude);
+ (unit === DimUnit.Ratio) && (starSum += magnitude);
widthSpecifiers.push({ magnitude, unit });
}
/**
@@ -82,9 +82,9 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
setTimeout(() => {
const { ratioDefinedDocs } = this;
if (this.childLayoutPairs.length) {
- const minimum = Math.min(...ratioDefinedDocs.map(({ widthMagnitude }) => NumCast(widthMagnitude)));
+ const minimum = Math.min(...ratioDefinedDocs.map(doc => NumCast(doc.dimMagnitude, 1)));
if (minimum !== 0) {
- ratioDefinedDocs.forEach(layout => layout.widthMagnitude = NumCast(layout.widthMagnitude) / minimum);
+ ratioDefinedDocs.forEach(layout => layout.dimMagnitude = NumCast(layout.dimMagnitude, 1) / minimum, 1);
}
}
});
@@ -103,7 +103,7 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
@computed
private get totalFixedAllocation(): number | undefined {
return this.resolvedLayoutInformation?.widthSpecifiers.reduce(
- (sum, { magnitude, unit }) => sum + (unit === WidthUnit.Pixel ? magnitude : 0), 0);
+ (sum, { magnitude, unit }) => sum + (unit === DimUnit.Pixel ? magnitude : 0), 0);
}
/**
@@ -119,7 +119,7 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
private get totalRatioAllocation(): number | undefined {
const layoutInfoLen = this.resolvedLayoutInformation.widthSpecifiers.length;
if (layoutInfoLen > 0 && this.totalFixedAllocation !== undefined) {
- return this.props.PanelWidth() - (this.totalFixedAllocation + resizerWidth * (layoutInfoLen - 1));
+ return this.props.PanelWidth() - (this.totalFixedAllocation + resizerWidth * (layoutInfoLen - 1)) - 2 * NumCast(this.props.Document._xMargin);
}
}
@@ -160,8 +160,8 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
if (columnUnitLength === undefined) {
return 0; // we're still waiting on promises to resolve
}
- let width = NumCast(layout.widthMagnitude);
- if (StrCast(layout.widthUnit) === WidthUnit.Ratio) {
+ let width = NumCast(layout.dimMagnitude, 1);
+ if (StrCast(layout.dimUnit, "*") === DimUnit.Ratio) {
width *= columnUnitLength;
}
return width;
@@ -190,11 +190,11 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
@undoBatch
@action
- drop = (e: Event, de: DragManager.DropEvent) => {
- if (super.drop(e, de)) {
+ onInternalDrop = (e: Event, de: DragManager.DropEvent) => {
+ if (super.onInternalDrop(e, de)) {
de.complete.docDragData?.droppedDocuments.forEach(action((d: Doc) => {
- d.widthUnit = "*";
- d.widthMagnitude = 1;
+ d.dimUnit = "*";
+ d.dimMagnitude = 1;
}));
}
return false;
@@ -203,6 +203,20 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
@computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); }
+ getDisplayDoc(layout: Doc, dxf: () => Transform, width: () => number, height: () => number) {
+ return <ContentFittingDocumentView
+ {...this.props}
+ Document={layout}
+ DataDocument={layout.resolvedDataDoc as Doc}
+ backgroundColor={this.props.backgroundColor}
+ CollectionDoc={this.props.Document}
+ PanelWidth={width}
+ PanelHeight={height}
+ getTransform={dxf}
+ onClick={this.onChildClickHandler}
+ renderDepth={this.props.renderDepth + 1}
+ />;
+ }
/**
* @returns the resolved list of rendered child documents, displayed
* at their resolved pixel widths, each separated by a resizer.
@@ -214,21 +228,14 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
const collector: JSX.Element[] = [];
for (let i = 0; i < childLayoutPairs.length; i++) {
const { layout } = childLayoutPairs[i];
+ const dxf = () => this.lookupIndividualTransform(layout).translate(-NumCast(Document._xMargin), -NumCast(Document._yMargin));
+ const width = () => this.lookupPixels(layout);
+ const height = () => PanelHeight() - 2 * NumCast(Document._yMargin) - (BoolCast(Document.showWidthLabels) ? 20 : 0);
collector.push(
- <div
- className={"document-wrapper"}
- key={Utils.GenerateGuid()}
- >
- <ContentFittingDocumentView
- {...this.props}
- Document={layout}
- DataDocument={layout.resolvedDataDoc as Doc}
- CollectionDoc={this.props.Document}
- PanelWidth={() => this.lookupPixels(layout)}
- PanelHeight={() => PanelHeight() - (BoolCast(Document.showWidthLabels) ? 20 : 0)}
- getTransform={() => this.lookupIndividualTransform(layout)}
- onClick={this.onChildClickHandler}
- />
+ <div className={"document-wrapper"}
+ key={"wrapper" + i}
+ style={{ width: width() }} >
+ {this.getDisplayDoc(layout, dxf, width, height)}
<WidthLabel
layout={layout}
collectionDoc={Document}
@@ -236,7 +243,8 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
</div>,
<ResizeBar
width={resizerWidth}
- key={Utils.GenerateGuid()}
+ key={"resizer" + i}
+ select={this.props.select}
columnUnitLength={this.getColumnUnitLength}
toLeft={layout}
toRight={childLayoutPairs[i + 1]?.layout}
@@ -249,7 +257,11 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
render(): JSX.Element {
return (
- <div className={"collectionMulticolumnView_contents"} ref={this.createDashEventsTarget}>
+ <div className={"collectionMulticolumnView_contents"}
+ style={{
+ marginLeft: NumCast(this.props.Document._xMargin), marginRight: NumCast(this.props.Document._xMargin),
+ marginTop: NumCast(this.props.Document._yMargin), marginBottom: NumCast(this.props.Document._yMargin)
+ }} ref={this.createDashEventsTarget}>
{this.contents}
</div>
);
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.scss b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.scss
new file mode 100644
index 000000000..79fb195e8
--- /dev/null
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.scss
@@ -0,0 +1,35 @@
+.collectionMultirowView_contents {
+ display: flex;
+ overflow: hidden;
+ width: 100%;
+ height: 100%;
+ flex-direction: column;
+
+ .document-wrapper {
+ display: flex;
+ flex-direction: row;
+ height: 100%;
+
+ .label-wrapper {
+ display: flex;
+ flex-direction: row;
+ justify-content: center;
+ height: 20px;
+ }
+
+ }
+
+ .multiRowResizer {
+ cursor: ns-resize;
+ transition: 0.5s opacity ease;
+ display: flex;
+ flex-direction: row;
+
+ .multiRowResizer-hdl {
+ width: 100%;
+ height: 100%;
+ transition: 0.5s background-color ease;
+ }
+ }
+
+} \ No newline at end of file
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
new file mode 100644
index 000000000..5e59f8237
--- /dev/null
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
@@ -0,0 +1,272 @@
+import { observer } from 'mobx-react';
+import { makeInterface } from '../../../../new_fields/Schema';
+import { documentSchema } from '../../../../new_fields/documentSchemas';
+import { CollectionSubView, SubCollectionViewProps } from '../CollectionSubView';
+import * as React from "react";
+import { Doc } from '../../../../new_fields/Doc';
+import { NumCast, StrCast, BoolCast, ScriptCast } from '../../../../new_fields/Types';
+import { ContentFittingDocumentView } from '../../nodes/ContentFittingDocumentView';
+import { Utils } from '../../../../Utils';
+import "./collectionMultirowView.scss";
+import { computed, trace, observable, action } from 'mobx';
+import { Transform } from '../../../util/Transform';
+import HeightLabel from './MultirowHeightLabel';
+import ResizeBar from './MultirowResizer';
+import { undoBatch } from '../../../util/UndoManager';
+import { DragManager } from '../../../util/DragManager';
+
+type MultirowDocument = makeInterface<[typeof documentSchema]>;
+const MultirowDocument = makeInterface(documentSchema);
+
+interface HeightSpecifier {
+ magnitude: number;
+ unit: string;
+}
+
+interface LayoutData {
+ heightSpecifiers: HeightSpecifier[];
+ starSum: number;
+}
+
+export const DimUnit = {
+ Pixel: "px",
+ Ratio: "*"
+};
+
+const resolvedUnits = Object.values(DimUnit);
+const resizerHeight = 8;
+
+@observer
+export class CollectionMultirowView extends CollectionSubView(MultirowDocument) {
+
+ /**
+ * @returns the list of layout documents whose width unit is
+ * *, denoting that it will be displayed with a ratio, not fixed pixel, value
+ */
+ @computed
+ private get ratioDefinedDocs() {
+ return this.childLayoutPairs.map(pair => pair.layout).filter(layout => StrCast(layout.dimUnit, "*") === DimUnit.Ratio);
+ }
+
+ /**
+ * This loops through all childLayoutPairs and extracts the values for dimUnit
+ * and dimUnit, ignoring any that are malformed. Additionally, it then
+ * normalizes the ratio values so that one * value is always 1, with the remaining
+ * values proportionate to that easily readable metric.
+ * @returns the list of the resolved width specifiers (unit and magnitude pairs)
+ * as well as the sum of the * coefficients, i.e. the ratio magnitudes
+ */
+ @computed
+ private get resolvedLayoutInformation(): LayoutData {
+ let starSum = 0;
+ const heightSpecifiers: HeightSpecifier[] = [];
+ this.childLayoutPairs.map(pair => {
+ const unit = StrCast(pair.layout.dimUnit, "*");
+ const magnitude = NumCast(pair.layout.dimMagnitude, 1);
+ if (unit && magnitude && magnitude > 0 && resolvedUnits.includes(unit)) {
+ (unit === DimUnit.Ratio) && (starSum += magnitude);
+ heightSpecifiers.push({ magnitude, unit });
+ }
+ /**
+ * Otherwise, the child document is ignored and the remaining
+ * space is allocated as if the document were absent from the child list
+ */
+ });
+
+ /**
+ * Here, since these values are all relative, adjustments during resizing or
+ * manual updating can, though their ratios remain the same, cause the values
+ * themselves to drift toward zero. Thus, whenever we change any of the values,
+ * we normalize everything (dividing by the smallest magnitude).
+ */
+ setTimeout(() => {
+ const { ratioDefinedDocs } = this;
+ if (this.childLayoutPairs.length) {
+ const minimum = Math.min(...ratioDefinedDocs.map(layout => NumCast(layout.dimMagnitude, 1)));
+ if (minimum !== 0) {
+ ratioDefinedDocs.forEach(layout => layout.dimMagnitude = NumCast(layout.dimMagnitude, 1) / minimum);
+ }
+ }
+ });
+
+ return { heightSpecifiers, starSum };
+ }
+
+ /**
+ * This returns the total quantity, in pixels, that this
+ * view needs to reserve for child documents that have
+ * (with higher priority) requested a fixed pixel width.
+ *
+ * If the underlying resolvedLayoutInformation returns null
+ * because we're waiting on promises to resolve, this value will be undefined as well.
+ */
+ @computed
+ private get totalFixedAllocation(): number | undefined {
+ return this.resolvedLayoutInformation?.heightSpecifiers.reduce(
+ (sum, { magnitude, unit }) => sum + (unit === DimUnit.Pixel ? magnitude : 0), 0);
+ }
+
+ /**
+ * @returns the total quantity, in pixels, that this
+ * view needs to reserve for child documents that have
+ * (with lower priority) requested a certain relative proportion of the
+ * remaining pixel width not allocated for fixed widths.
+ *
+ * If the underlying totalFixedAllocation returns undefined
+ * because we're waiting indirectly on promises to resolve, this value will be undefined as well.
+ */
+ @computed
+ private get totalRatioAllocation(): number | undefined {
+ const layoutInfoLen = this.resolvedLayoutInformation.heightSpecifiers.length;
+ if (layoutInfoLen > 0 && this.totalFixedAllocation !== undefined) {
+ return this.props.PanelHeight() - (this.totalFixedAllocation + resizerHeight * (layoutInfoLen - 1)) - 2 * NumCast(this.props.Document._yMargin);
+ }
+ }
+
+ /**
+ * @returns the total quantity, in pixels, that
+ * 1* (relative / star unit) is worth. For example,
+ * if the configuration has three documents, with, respectively,
+ * widths of 2*, 2* and 1*, and the panel width returns 1000px,
+ * this accessor returns 1000 / (2 + 2 + 1), or 200px.
+ * Elsewhere, this is then multiplied by each relative-width
+ * document's (potentially decimal) * count to compute its actual width (400px, 400px and 200px).
+ *
+ * If the underlying totalRatioAllocation or this.resolveLayoutInformation return undefined
+ * because we're waiting indirectly on promises to resolve, this value will be undefined as well.
+ */
+ @computed
+ private get rowUnitLength(): number | undefined {
+ if (this.resolvedLayoutInformation && this.totalRatioAllocation !== undefined) {
+ return this.totalRatioAllocation / this.resolvedLayoutInformation.starSum;
+ }
+ }
+
+ /**
+ * This wrapper function exists to prevent mobx from
+ * needlessly rerendering the internal ContentFittingDocumentViews
+ */
+ private getRowUnitLength = () => this.rowUnitLength;
+
+ /**
+ * @param layout the document whose transform we'd like to compute
+ * Given a layout document, this function
+ * returns the resolved width it has requested, in pixels.
+ * @returns the stored row width if already in pixels,
+ * or the ratio width evaluated to a pixel value
+ */
+ private lookupPixels = (layout: Doc): number => {
+ const rowUnitLength = this.rowUnitLength;
+ if (rowUnitLength === undefined) {
+ return 0; // we're still waiting on promises to resolve
+ }
+ let height = NumCast(layout.dimMagnitude, 1);
+ if (StrCast(layout.dimUnit, "*") === DimUnit.Ratio) {
+ height *= rowUnitLength;
+ }
+ return height;
+ }
+
+ /**
+ * @returns the transform that will correctly place
+ * the document decorations box, shifted to the right by
+ * the sum of all the resolved row widths of the
+ * documents before the target.
+ */
+ private lookupIndividualTransform = (layout: Doc) => {
+ const rowUnitLength = this.rowUnitLength;
+ if (rowUnitLength === undefined) {
+ return Transform.Identity(); // we're still waiting on promises to resolve
+ }
+ let offset = 0;
+ for (const { layout: candidate } of this.childLayoutPairs) {
+ if (candidate === layout) {
+ return this.props.ScreenToLocalTransform().translate(0, -offset);
+ }
+ offset += this.lookupPixels(candidate) + resizerHeight;
+ }
+ return Transform.Identity(); // type coersion, this case should never be hit
+ }
+
+ @undoBatch
+ @action
+ onInternalDrop = (e: Event, de: DragManager.DropEvent) => {
+ if (super.onInternalDrop(e, de)) {
+ de.complete.docDragData?.droppedDocuments.forEach(action((d: Doc) => {
+ d.dimUnit = "*";
+ d.dimMagnitude = 1;
+ }));
+ }
+ return false;
+ }
+
+
+ @computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); }
+
+ getDisplayDoc(layout: Doc, dxf: () => Transform, width: () => number, height: () => number) {
+ return <ContentFittingDocumentView
+ {...this.props}
+ Document={layout}
+ DataDocument={layout.resolvedDataDoc as Doc}
+ backgroundColor={this.props.backgroundColor}
+ CollectionDoc={this.props.Document}
+ PanelWidth={width}
+ PanelHeight={height}
+ getTransform={dxf}
+ onClick={this.onChildClickHandler}
+ renderDepth={this.props.renderDepth + 1}
+ />;
+ }
+ /**
+ * @returns the resolved list of rendered child documents, displayed
+ * at their resolved pixel widths, each separated by a resizer.
+ */
+ @computed
+ private get contents(): JSX.Element[] | null {
+ const { childLayoutPairs } = this;
+ const { Document, PanelWidth } = this.props;
+ const collector: JSX.Element[] = [];
+ for (let i = 0; i < childLayoutPairs.length; i++) {
+ const { layout } = childLayoutPairs[i];
+ const dxf = () => this.lookupIndividualTransform(layout).translate(-NumCast(Document._xMargin), -NumCast(Document._yMargin));
+ const height = () => this.lookupPixels(layout);
+ const width = () => PanelWidth() - 2 * NumCast(Document._xMargin) - (BoolCast(Document.showWidthLabels) ? 20 : 0);
+ collector.push(
+ <div
+ className={"document-wrapper"}
+ key={"wrapper" + i}
+ >
+ {this.getDisplayDoc(layout, dxf, width, height)}
+ <HeightLabel
+ layout={layout}
+ collectionDoc={Document}
+ />
+ </div>,
+ <ResizeBar
+ height={resizerHeight}
+ key={"resizer" + i}
+ columnUnitLength={this.getRowUnitLength}
+ toTop={layout}
+ toBottom={childLayoutPairs[i + 1]?.layout}
+ />
+ );
+ }
+ collector.pop(); // removes the final extraneous resize bar
+ return collector;
+ }
+
+ render(): JSX.Element {
+ return (
+ <div className={"collectionMultirowView_contents"}
+ style={{
+ width: `calc(100% - ${2 * NumCast(this.props.Document._xMargin)}px)`,
+ height: `calc(100% - ${2 * NumCast(this.props.Document._yMargin)}px)`,
+ marginLeft: NumCast(this.props.Document._xMargin), marginRight: NumCast(this.props.Document._xMargin),
+ marginTop: NumCast(this.props.Document._yMargin), marginBottom: NumCast(this.props.Document._yMargin)
+ }} ref={this.createDashEventsTarget}>
+ {this.contents}
+ </div>
+ );
+ }
+
+} \ No newline at end of file
diff --git a/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx b/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx
index 11e210958..e1e604686 100644
--- a/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx
+++ b/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx
@@ -3,19 +3,15 @@ import { observer } from "mobx-react";
import { observable, action } from "mobx";
import { Doc } from "../../../../new_fields/Doc";
import { NumCast, StrCast } from "../../../../new_fields/Types";
-import { WidthUnit } from "./CollectionMulticolumnView";
+import { DimUnit } from "./CollectionMulticolumnView";
+import { UndoManager } from "../../../util/UndoManager";
interface ResizerProps {
width: number;
columnUnitLength(): number | undefined;
toLeft?: Doc;
toRight?: Doc;
-}
-
-enum ResizeMode {
- Global = "blue",
- Pinned = "red",
- Undefined = "black"
+ select: (isCtrlPressed: boolean) => void;
}
const resizerOpacity = 1;
@@ -24,18 +20,19 @@ const resizerOpacity = 1;
export default class ResizeBar extends React.Component<ResizerProps> {
@observable private isHoverActive = false;
@observable private isResizingActive = false;
- @observable private resizeMode = ResizeMode.Undefined;
+ private _resizeUndo?: UndoManager.Batch;
@action
- private registerResizing = (e: React.PointerEvent<HTMLDivElement>, mode: ResizeMode) => {
+ private registerResizing = (e: React.PointerEvent<HTMLDivElement>) => {
+ this.props.select(false);
e.stopPropagation();
e.preventDefault();
- this.resizeMode = mode;
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");
}
private onPointerMove = ({ movementX }: PointerEvent) => {
@@ -46,14 +43,12 @@ export default class ResizeBar extends React.Component<ResizerProps> {
const unitLength = columnUnitLength();
if (unitLength) {
if (toNarrow) {
- const { widthUnit, widthMagnitude } = toNarrow;
- const scale = widthUnit === WidthUnit.Ratio ? unitLength : 1;
- toNarrow.widthMagnitude = NumCast(widthMagnitude) - Math.abs(movementX) / scale;
+ const scale = StrCast(toNarrow.dimUnit, "*") === DimUnit.Ratio ? unitLength : 1;
+ toNarrow.dimMagnitude = Math.max(0.05, NumCast(toNarrow.dimMagnitude, 1) - Math.abs(movementX) / scale);
}
- if (this.resizeMode === ResizeMode.Pinned && toWiden) {
- const { widthUnit, widthMagnitude } = toWiden;
- const scale = widthUnit === WidthUnit.Ratio ? unitLength : 1;
- toWiden.widthMagnitude = NumCast(widthMagnitude) + Math.abs(movementX) / scale;
+ if (toWiden) {
+ const scale = StrCast(toWiden.dimUnit, "*") === DimUnit.Ratio ? unitLength : 1;
+ toWiden.dimMagnitude = Math.max(0.05, NumCast(toWiden.dimMagnitude, 1) + Math.abs(movementX) / scale);
}
}
}
@@ -61,17 +56,17 @@ export default class ResizeBar extends React.Component<ResizerProps> {
private get isActivated() {
const { toLeft, toRight } = this.props;
if (toLeft && toRight) {
- if (StrCast(toLeft.widthUnit) === WidthUnit.Pixel && StrCast(toRight.widthUnit) === WidthUnit.Pixel) {
+ if (StrCast(toLeft.dimUnit, "*") === DimUnit.Pixel && StrCast(toRight.dimUnit, "*") === DimUnit.Pixel) {
return false;
}
return true;
} else if (toLeft) {
- if (StrCast(toLeft.widthUnit) === WidthUnit.Pixel) {
+ if (StrCast(toLeft.dimUnit, "*") === DimUnit.Pixel) {
return false;
}
return true;
} else if (toRight) {
- if (StrCast(toRight.widthUnit) === WidthUnit.Pixel) {
+ if (StrCast(toRight.dimUnit, "*") === DimUnit.Pixel) {
return false;
}
return true;
@@ -81,17 +76,18 @@ export default class ResizeBar extends React.Component<ResizerProps> {
@action
private onPointerUp = () => {
- this.resizeMode = ResizeMode.Undefined;
this.isResizingActive = false;
this.isHoverActive = false;
window.removeEventListener("pointermove", this.onPointerMove);
window.removeEventListener("pointerup", this.onPointerUp);
+ this._resizeUndo?.end();
+ this._resizeUndo = undefined;
}
render() {
return (
<div
- className={"resizer"}
+ className={"multiColumnResizer"}
style={{
width: this.props.width,
opacity: this.isActivated && this.isHoverActive ? resizerOpacity : 0
@@ -99,16 +95,7 @@ export default class ResizeBar extends React.Component<ResizerProps> {
onPointerEnter={action(() => this.isHoverActive = true)}
onPointerLeave={action(() => !this.isResizingActive && (this.isHoverActive = false))}
>
- <div
- className={"internal"}
- onPointerDown={e => this.registerResizing(e, ResizeMode.Pinned)}
- style={{ backgroundColor: this.resizeMode }}
- />
- <div
- className={"internal"}
- onPointerDown={e => this.registerResizing(e, ResizeMode.Global)}
- style={{ backgroundColor: this.resizeMode }}
- />
+ <div className={"multiColumnResizer-hdl"} onPointerDown={e => this.registerResizing(e)} />
</div>
);
}
diff --git a/src/client/views/collections/collectionMulticolumn/MulticolumnWidthLabel.tsx b/src/client/views/collections/collectionMulticolumn/MulticolumnWidthLabel.tsx
index b394fed62..5b2054428 100644
--- a/src/client/views/collections/collectionMulticolumn/MulticolumnWidthLabel.tsx
+++ b/src/client/views/collections/collectionMulticolumn/MulticolumnWidthLabel.tsx
@@ -4,7 +4,7 @@ import { computed } from "mobx";
import { Doc } from "../../../../new_fields/Doc";
import { NumCast, StrCast, BoolCast } from "../../../../new_fields/Types";
import { EditableView } from "../../EditableView";
-import { WidthUnit } from "./CollectionMulticolumnView";
+import { DimUnit } from "./CollectionMulticolumnView";
interface WidthLabelProps {
layout: Doc;
@@ -18,8 +18,8 @@ export default class WidthLabel extends React.Component<WidthLabelProps> {
@computed
private get contents() {
const { layout, decimals } = this.props;
- const getUnit = () => StrCast(layout.widthUnit);
- const getMagnitude = () => String(+NumCast(layout.widthMagnitude).toFixed(decimals ?? 3));
+ const getUnit = () => StrCast(layout.dimUnit);
+ const getMagnitude = () => String(+NumCast(layout.dimMagnitude).toFixed(decimals ?? 3));
return (
<div className={"label-wrapper"}>
<EditableView
@@ -27,7 +27,7 @@ export default class WidthLabel extends React.Component<WidthLabelProps> {
SetValue={value => {
const converted = Number(value);
if (!isNaN(converted) && converted > 0) {
- layout.widthMagnitude = converted;
+ layout.dimMagnitude = converted;
return true;
}
return false;
@@ -37,8 +37,8 @@ export default class WidthLabel extends React.Component<WidthLabelProps> {
<EditableView
GetValue={getUnit}
SetValue={value => {
- if (Object.values(WidthUnit).includes(value)) {
- layout.widthUnit = value;
+ if (Object.values(DimUnit).includes(value)) {
+ layout.dimUnit = value;
return true;
}
return false;
diff --git a/src/client/views/collections/collectionMulticolumn/MultirowHeightLabel.tsx b/src/client/views/collections/collectionMulticolumn/MultirowHeightLabel.tsx
new file mode 100644
index 000000000..899577fd5
--- /dev/null
+++ b/src/client/views/collections/collectionMulticolumn/MultirowHeightLabel.tsx
@@ -0,0 +1,56 @@
+import * as React from "react";
+import { observer } from "mobx-react";
+import { computed } from "mobx";
+import { Doc } from "../../../../new_fields/Doc";
+import { NumCast, StrCast, BoolCast } from "../../../../new_fields/Types";
+import { EditableView } from "../../EditableView";
+import { DimUnit } from "./CollectionMultirowView";
+
+interface HeightLabelProps {
+ layout: Doc;
+ collectionDoc: Doc;
+ decimals?: number;
+}
+
+@observer
+export default class HeightLabel extends React.Component<HeightLabelProps> {
+
+ @computed
+ private get contents() {
+ const { layout, decimals } = this.props;
+ const getUnit = () => StrCast(layout.dimUnit);
+ const getMagnitude = () => String(+NumCast(layout.dimMagnitude).toFixed(decimals ?? 3));
+ return (
+ <div className={"label-wrapper"}>
+ <EditableView
+ GetValue={getMagnitude}
+ SetValue={value => {
+ const converted = Number(value);
+ if (!isNaN(converted) && converted > 0) {
+ layout.dimMagnitude = converted;
+ return true;
+ }
+ return false;
+ }}
+ contents={getMagnitude()}
+ />
+ <EditableView
+ GetValue={getUnit}
+ SetValue={value => {
+ if (Object.values(DimUnit).includes(value)) {
+ layout.dimUnit = value;
+ return true;
+ }
+ return false;
+ }}
+ contents={getUnit()}
+ />
+ </div>
+ );
+ }
+
+ render() {
+ return BoolCast(this.props.collectionDoc.showHeightLabels) ? this.contents : (null);
+ }
+
+} \ No newline at end of file
diff --git a/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx b/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx
new file mode 100644
index 000000000..9df8cc3e2
--- /dev/null
+++ b/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx
@@ -0,0 +1,101 @@
+import * as React from "react";
+import { observer } from "mobx-react";
+import { observable, action } from "mobx";
+import { Doc } from "../../../../new_fields/Doc";
+import { NumCast, StrCast } from "../../../../new_fields/Types";
+import { DimUnit } from "./CollectionMultirowView";
+import { UndoManager } from "../../../util/UndoManager";
+
+interface ResizerProps {
+ height: number;
+ columnUnitLength(): number | undefined;
+ toTop?: Doc;
+ toBottom?: Doc;
+}
+
+const resizerOpacity = 1;
+
+@observer
+export default class ResizeBar extends React.Component<ResizerProps> {
+ @observable private isHoverActive = false;
+ @observable private isResizingActive = false;
+ private _resizeUndo?: UndoManager.Batch;
+
+ @action
+ 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);
+ this.isResizingActive = true;
+ this._resizeUndo = UndoManager.StartBatch("multcol resizing");
+ }
+
+ private onPointerMove = ({ movementY }: PointerEvent) => {
+ const { toTop: toTop, toBottom: toBottom, columnUnitLength } = this.props;
+ const movingDown = movementY > 0;
+ const toNarrow = movingDown ? toBottom : toTop;
+ const toWiden = movingDown ? toTop : toBottom;
+ const unitLength = columnUnitLength();
+ if (unitLength) {
+ if (toNarrow) {
+ 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;
+ 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) {
+ return false;
+ }
+ return true;
+ } else if (toTop) {
+ if (StrCast(toTop.dimUnit, "*") === DimUnit.Pixel) {
+ return false;
+ }
+ return true;
+ } else if (toBottom) {
+ if (StrCast(toBottom.dimUnit, "*") === DimUnit.Pixel) {
+ return false;
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @action
+ private onPointerUp = () => {
+ this.isResizingActive = false;
+ this.isHoverActive = false;
+ 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,
+ opacity: this.isActivated && this.isHoverActive ? resizerOpacity : 0
+ }}
+ 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/document_templates/caption_toggle/DetailedCaptionToggle.tsx b/src/client/views/document_templates/caption_toggle/DetailedCaptionToggle.tsx
deleted file mode 100644
index 3aaf4120c..000000000
--- a/src/client/views/document_templates/caption_toggle/DetailedCaptionToggle.tsx
+++ /dev/null
@@ -1,72 +0,0 @@
-import * as React from 'react';
-import { FontStyleProperty, ColorProperty } from 'csstype';
-import { observer } from 'mobx-react';
-import { observable, action, runInAction } from 'mobx';
-import { FormattedTextBox } from '../../nodes/FormattedTextBox';
-import { FieldViewProps } from '../../nodes/FieldView';
-
-interface DetailedCaptionDataProps {
- captionFieldKey?: string;
- detailsFieldKey?: string;
-}
-
-interface DetailedCaptionStylingProps {
- sharedFontColor?: ColorProperty;
- captionFontStyle?: FontStyleProperty;
- detailsFontStyle?: FontStyleProperty;
- toggleSize?: number;
-}
-
-@observer
-export default class DetailedCaptionToggle extends React.Component<DetailedCaptionDataProps & DetailedCaptionStylingProps & FieldViewProps> {
- @observable loaded: boolean = false;
- @observable detailsExpanded: boolean = false;
-
- @action toggleDetails = (e: React.MouseEvent<HTMLDivElement>) => {
- e.preventDefault();
- e.stopPropagation();
- this.detailsExpanded = !this.detailsExpanded;
- }
-
- componentDidMount() {
- runInAction(() => this.loaded = true);
- }
-
- render() {
- const size = this.props.toggleSize || 20;
- return (
- <div style={{
- transition: "0.5s opacity ease",
- opacity: this.loaded ? 1 : 0,
- bottom: 0,
- fontSize: 14,
- width: "100%",
- position: "absolute"
- }}>
- {/* caption */}
- <div style={{ opacity: this.detailsExpanded ? 0 : 1, transition: "opacity 0.3s ease" }}>
- <FormattedTextBox {...this.props} fieldKey={this.props.captionFieldKey || "caption"} />
- </div>
- {/* details */}
- <div style={{ opacity: this.detailsExpanded ? 1 : 0, transition: "opacity 0.3s ease" }}>
- <FormattedTextBox {...this.props} fieldKey={this.props.detailsFieldKey || "captiondetails"} />
- </div>
- {/* toggle */}
- <div
- style={{
- width: size,
- height: size,
- borderRadius: "50%",
- backgroundColor: "red",
- zIndex: 3,
- cursor: "pointer"
- }}
- onClick={this.toggleDetails}
- >
- <span style={{ color: "white" }}></span>
- </div>
- </div>
- );
- }
-
-}
diff --git a/src/client/views/document_templates/image_card/ImageCard.tsx b/src/client/views/document_templates/image_card/ImageCard.tsx
deleted file mode 100644
index 868afc423..000000000
--- a/src/client/views/document_templates/image_card/ImageCard.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import * as React from 'react';
-import { FieldViewProps } from '../../nodes/FieldView';
-import { ImageBox } from '../../nodes/ImageBox';
-
-export default class ImageCard extends React.Component<FieldViewProps> {
-
- render() {
- return (
- <div style={{ padding: 30, borderRadius: 15 }}>
- <ImageBox {...this.props} />
- </div>
- );
- }
-
-} \ No newline at end of file
diff --git a/src/client/views/linking/LinkEditor.scss b/src/client/views/linking/LinkEditor.scss
index fc5f2410c..b47c8976e 100644
--- a/src/client/views/linking/LinkEditor.scss
+++ b/src/client/views/linking/LinkEditor.scss
@@ -4,6 +4,7 @@
width: 100%;
height: auto;
font-size: 12px; // TODO
+ user-select: none;
}
.linkEditor-back {
@@ -22,10 +23,9 @@
}
}
-.linkEditor-button {
- width: 20px;
- height: 20px;
- margin-left: 6px;
+.linkEditor-button, .linkEditor-addbutton {
+ width: 18px;
+ height: 18px;
padding: 0;
// font-size: 12px;
border-radius: 10px;
@@ -34,6 +34,9 @@
background-color: gray;
}
}
+.linkEditor-addbutton{
+ margin-left: 0px;
+}
.linkEditor-groupsLabel {
display: flex;
@@ -49,10 +52,11 @@
.linkEditor-group-row {
display: flex;
margin-bottom: 3px;
+ }
- .linkEditor-group-row-label {
- margin-right: 6px;
- }
+ .linkEditor-group-row-label {
+ margin-right: 6px;
+ display:inline-block;
}
.linkEditor-metadata-row {
@@ -118,7 +122,6 @@
.linkEditor-typeButton {
background-color: transparent;
color: $dark-color;
- width: 100%;
height: 20px;
padding: 0 3px;
padding-bottom: 2px;
@@ -127,6 +130,8 @@
letter-spacing: normal;
font-size: 12px;
font-weight: bold;
+ display: inline-block;
+ width: calc(100% - 40px);
&:hover {
background-color: $light-color;
@@ -140,6 +145,6 @@
margin-top: 5px;
.linkEditor-button {
- margin-left: 6px;
+ margin-left: 3px;
}
} \ No newline at end of file
diff --git a/src/client/views/linking/LinkEditor.tsx b/src/client/views/linking/LinkEditor.tsx
index e3bf6b5f8..b7f3dd995 100644
--- a/src/client/views/linking/LinkEditor.tsx
+++ b/src/client/views/linking/LinkEditor.tsx
@@ -1,17 +1,14 @@
-import { observable, computed, action, trace } from "mobx";
-import React = require("react");
+import { library } from "@fortawesome/fontawesome-svg-core";
+import { faArrowLeft, faCog, faEllipsisV, faExchangeAlt, faPlus, faTable, faTimes, faTrash } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { action, observable } from "mobx";
import { observer } from "mobx-react";
-import './LinkEditor.scss';
-import { StrCast, Cast, FieldValue } from "../../../new_fields/Types";
import { Doc } from "../../../new_fields/Doc";
-import { LinkManager } from "../../util/LinkManager";
-import { Docs } from "../../documents/Documents";
+import { StrCast } from "../../../new_fields/Types";
import { Utils } from "../../../Utils";
-import { faArrowLeft, faEllipsisV, faTable, faTrash, faCog, faExchangeAlt, faTimes, faPlus } from '@fortawesome/free-solid-svg-icons';
-import { library } from "@fortawesome/fontawesome-svg-core";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { SetupDrag } from "../../util/DragManager";
-import { SchemaHeaderField, RandomPastel } from "../../../new_fields/SchemaHeaderField";
+import { LinkManager } from "../../util/LinkManager";
+import './LinkEditor.scss';
+import React = require("react");
library.add(faArrowLeft, faEllipsisV, faTable, faTrash, faCog, faExchangeAlt, faTimes, faPlus);
@@ -108,7 +105,7 @@ class GroupTypesDropdown extends React.Component<GroupTypesDropdownProps> {
if (this._isEditing || this._groupType === "") {
return (
<div className="linkEditor-dropdown">
- <input type="text" value={this._groupType} placeholder="Search for or create a new group"
+ <input type="text" value={this._groupType === "-ungrouped-" ? "" : this._groupType} placeholder="Search for or create a new group"
onChange={e => this.onChange(e.target.value)} onKeyDown={this.onKeyDown} autoFocus></input>
<div className="linkEditor-options-wrapper">
{this.renderOptions()}
@@ -166,7 +163,7 @@ class LinkMetadataEditor extends React.Component<LinkMetadataEditorProps> {
setMetadataValue = (value: string): void => {
if (!this._keyError) {
this._value = value;
- this.props.mdDoc[this._key] = value;
+ Doc.GetProto(this.props.mdDoc)[this._key] = value;
}
}
@@ -187,7 +184,7 @@ class LinkMetadataEditor extends React.Component<LinkMetadataEditorProps> {
<div className="linkEditor-metadata-row">
<input className={this._keyError ? "linkEditor-error" : ""} type="text" value={this._key === "new key" ? "" : this._key} placeholder="key" onChange={e => this.setMetadataKey(e.target.value)}></input>:
<input type="text" value={this._value} placeholder="value" onChange={e => this.setMetadataValue(e.target.value)}></input>
- <button onClick={() => this.removeMetadata()}><FontAwesomeIcon icon="times" size="sm" /></button>
+ <button title="remove metadata from relationship" onClick={() => this.removeMetadata()}><FontAwesomeIcon icon="times" size="sm" /></button>
</div>
);
}
@@ -206,15 +203,13 @@ export class LinkGroupEditor extends React.Component<LinkGroupEditorProps> {
constructor(props: LinkGroupEditorProps) {
super(props);
- const groupMdKeys = LinkManager.Instance.getMetadataKeysInGroup(StrCast(props.groupDoc.type));
- groupMdKeys.forEach(key => {
- this._metadataIds.set(key, Utils.GenerateGuid());
- });
+ const groupMdKeys = LinkManager.Instance.getMetadataKeysInGroup(StrCast(props.groupDoc.linkRelationship));
+ groupMdKeys.forEach(key => this._metadataIds.set(key, Utils.GenerateGuid()));
}
@action
setGroupType = (groupType: string): void => {
- this.props.groupDoc.type = groupType;
+ Doc.GetProto(this.props.groupDoc).linkRelationship = groupType;
}
removeGroupFromLink = (groupType: string): void => {
@@ -225,33 +220,6 @@ export class LinkGroupEditor extends React.Component<LinkGroupEditorProps> {
LinkManager.Instance.deleteGroupType(groupType);
}
- copyGroup = async (groupType: string): Promise<void> => {
- const sourceGroupDoc = this.props.groupDoc;
- const sourceMdDoc = await Cast(sourceGroupDoc.metadata, Doc);
- if (!sourceMdDoc) return;
-
- const destDoc = LinkManager.Instance.getOppositeAnchor(this.props.linkDoc, this.props.sourceDoc);
- // let destGroupList = LinkManager.Instance.getAnchorGroups(this.props.linkDoc, destDoc);
- const keys = LinkManager.Instance.getMetadataKeysInGroup(groupType);
-
- // create new metadata doc with copied kvp
- const destMdDoc = new Doc();
- destMdDoc.anchor1 = StrCast(sourceMdDoc.anchor2);
- destMdDoc.anchor2 = StrCast(sourceMdDoc.anchor1);
- keys.forEach(key => {
- const val = sourceMdDoc[key] === undefined ? "" : StrCast(sourceMdDoc[key]);
- destMdDoc[key] = val;
- });
-
- // create new group doc with new metadata doc
- const destGroupDoc = new Doc();
- destGroupDoc.type = groupType;
- destGroupDoc.metadata = destMdDoc;
-
- if (destDoc) {
- LinkManager.Instance.addGroupToAnchor(this.props.linkDoc, destDoc, destGroupDoc, true);
- }
- }
@action
addMetadata = (groupType: string): void => {
@@ -270,69 +238,34 @@ export class LinkGroupEditor extends React.Component<LinkGroupEditorProps> {
renderMetadata = (): JSX.Element[] => {
const metadata: Array<JSX.Element> = [];
const groupDoc = this.props.groupDoc;
- const mdDoc = FieldValue(Cast(groupDoc.metadata, Doc));
- if (!mdDoc) {
- return [];
- }
- const groupType = StrCast(groupDoc.type);
+ const groupType = StrCast(groupDoc.linkRelationship);
const groupMdKeys = LinkManager.Instance.getMetadataKeysInGroup(groupType);
groupMdKeys.forEach((key) => {
- const val = StrCast(mdDoc[key]);
+ const val = StrCast(groupDoc[key]);
metadata.push(
- <LinkMetadataEditor key={"mded-" + this._metadataIds.get(key)} id={this._metadataIds.get(key)!} groupType={groupType} mdDoc={mdDoc} mdKey={key} mdValue={val} changeMdIdKey={this.changeMdIdKey} />
+ <LinkMetadataEditor key={"mded-" + this._metadataIds.get(key)} id={this._metadataIds.get(key)!} groupType={groupType} mdDoc={groupDoc} mdKey={key} mdValue={val} changeMdIdKey={this.changeMdIdKey} />
);
});
return metadata;
}
- viewGroupAsTable = (groupType: string): JSX.Element => {
- const keys = LinkManager.Instance.getMetadataKeysInGroup(groupType);
- const index = keys.indexOf("");
- if (index > -1) keys.splice(index, 1);
- const cols = ["anchor1", "anchor2", ...[...keys]].map(c => new SchemaHeaderField(c, "#f1efeb"));
- const docs: Doc[] = LinkManager.Instance.getAllMetadataDocsInGroup(groupType);
- const createTable = action(() => Docs.Create.SchemaDocument(cols, docs, { _width: 500, _height: 300, title: groupType + " table" }));
- const ref = React.createRef<HTMLDivElement>();
- return <div ref={ref}><button className="linkEditor-button" onPointerDown={SetupDrag(ref, createTable)} title="Drag to view relationship table"><FontAwesomeIcon icon="table" size="sm" /></button></div>;
- }
-
render() {
- const groupType = StrCast(this.props.groupDoc.type);
+ const groupType = StrCast(this.props.groupDoc.linkRelationship);
// if ((groupType && LinkManager.Instance.getMetadataKeysInGroup(groupType).length > 0) || groupType === "") {
- let buttons;
- if (groupType === "") {
- buttons = (
- <>
- <button className="linkEditor-button" disabled={true} title="Add KVP"><FontAwesomeIcon icon="plus" size="sm" /></button>
- <button className="linkEditor-button" disabled title="Copy group to opposite anchor"><FontAwesomeIcon icon="exchange-alt" size="sm" /></button>
- <button className="linkEditor-button" onClick={() => this.removeGroupFromLink(groupType)} title="Remove group from link"><FontAwesomeIcon icon="times" size="sm" /></button>
- <button className="linkEditor-button" disabled title="Delete group"><FontAwesomeIcon icon="trash" size="sm" /></button>
- <button className="linkEditor-button" disabled title="Drag to view relationship table"><FontAwesomeIcon icon="table" size="sm" /></button>
- </>
- );
- } else {
- buttons = (
- <>
- <button className="linkEditor-button" onClick={() => this.addMetadata(groupType)} title="Add KVP"><FontAwesomeIcon icon="plus" size="sm" /></button>
- <button className="linkEditor-button" onClick={() => this.copyGroup(groupType)} title="Copy group to opposite anchor"><FontAwesomeIcon icon="exchange-alt" size="sm" /></button>
- <button className="linkEditor-button" onClick={() => this.removeGroupFromLink(groupType)} title="Remove group from link"><FontAwesomeIcon icon="times" size="sm" /></button>
- <button className="linkEditor-button" onClick={() => this.deleteGroup(groupType)} title="Delete group"><FontAwesomeIcon icon="trash" size="sm" /></button>
- {this.viewGroupAsTable(groupType)}
- </>
- );
- }
+ const buttons = <button className="linkEditor-button" disabled={groupType === ""} onClick={() => this.deleteGroup(groupType)} title="Delete Relationship from all links"><FontAwesomeIcon icon="trash" size="sm" /></button>;
+ const addButton = <button className="linkEditor-addbutton" onClick={() => this.addMetadata(groupType)} disabled={groupType === ""} title="Add metadata to relationship"><FontAwesomeIcon icon="plus" size="sm" /></button>;
+
return (
<div className="linkEditor-group">
<div className="linkEditor-group-row ">
- <p className="linkEditor-group-row-label">type:</p>
+ {buttons}
<GroupTypesDropdown groupType={groupType} setGroupType={this.setGroupType} />
+ <button className="linkEditor-button" onClick={() => this.removeGroupFromLink(groupType)} title="Remove relationship from link"><FontAwesomeIcon icon="times" size="sm" /></button>
</div>
{this.renderMetadata().length > 0 ? <p className="linkEditor-group-row-label">metadata:</p> : <></>}
+ {addButton}
{this.renderMetadata()}
- <div className="linkEditor-group-buttons">
- {buttons}
- </div>
</div>
);
}
@@ -343,6 +276,7 @@ interface LinkEditorProps {
sourceDoc: Doc;
linkDoc: Doc;
showLinks: () => void;
+ hideback?: boolean;
}
@observer
export class LinkEditor extends React.Component<LinkEditorProps> {
@@ -353,48 +287,23 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
this.props.showLinks();
}
- @action
- addGroup = (): void => {
- // create new metadata document for group
- const mdDoc = new Doc();
- mdDoc.anchor1 = this.props.sourceDoc.title;
- const opp = LinkManager.Instance.getOppositeAnchor(this.props.linkDoc, this.props.sourceDoc);
- if (opp) {
- mdDoc.anchor2 = opp.title;
- }
-
- // create new group document
- const groupDoc = new Doc();
- groupDoc.type = "";
- groupDoc.metadata = mdDoc;
-
- LinkManager.Instance.addGroupToAnchor(this.props.linkDoc, this.props.sourceDoc, groupDoc);
- }
-
render() {
const destination = LinkManager.Instance.getOppositeAnchor(this.props.linkDoc, this.props.sourceDoc);
- const groupList = LinkManager.Instance.getAnchorGroups(this.props.linkDoc, this.props.sourceDoc);
- const groups = groupList.map(groupDoc => {
- return <LinkGroupEditor key={"gred-" + StrCast(groupDoc.type)} linkDoc={this.props.linkDoc} sourceDoc={this.props.sourceDoc} groupDoc={groupDoc} />;
+ const groups = [this.props.linkDoc].map(groupDoc => {
+ return <LinkGroupEditor key={"gred-" + StrCast(groupDoc.linkRelationship)} linkDoc={this.props.linkDoc} sourceDoc={this.props.sourceDoc} groupDoc={groupDoc} />;
});
- if (destination) {
- return (
- <div className="linkEditor">
- <button className="linkEditor-back" onPointerDown={() => this.props.showLinks()}><FontAwesomeIcon icon="arrow-left" size="sm" /></button>
- <div className="linkEditor-info">
- <p className="linkEditor-linkedTo">editing link to: <b>{destination.proto!.title}</b></p>
- <button className="linkEditor-button" onPointerDown={() => this.deleteLink()} title="Delete link"><FontAwesomeIcon icon="trash" size="sm" /></button>
- </div>
- <div className="linkEditor-groupsLabel">
- <b>Relationships:</b>
- <button className="linkEditor-button" onClick={() => this.addGroup()} title=" Add Group"><FontAwesomeIcon icon="plus" size="sm" /></button>
- </div>
- {groups.length > 0 ? groups : <div className="linkEditor-group">There are currently no relationships associated with this link.</div>}
+ return !destination ? (null) : (
+ <div className="linkEditor">
+ {this.props.hideback ? (null) : <button className="linkEditor-back" onPointerDown={() => this.props.showLinks()}><FontAwesomeIcon icon="arrow-left" size="sm" /></button>}
+ <div className="linkEditor-info">
+ <p className="linkEditor-linkedTo">editing link to: <b>{destination.proto!.title}</b></p>
+ <button className="linkEditor-button" onPointerDown={() => this.deleteLink()} title="Delete link"><FontAwesomeIcon icon="trash" size="sm" /></button>
</div>
+ {groups.length > 0 ? groups : <div className="linkEditor-group">There are currently no relationships associated with this link.</div>}
+ </div>
- );
- }
+ );
}
} \ No newline at end of file
diff --git a/src/client/views/linking/LinkFollowBox.scss b/src/client/views/linking/LinkFollowBox.scss
deleted file mode 100644
index 9eeed1cc8..000000000
--- a/src/client/views/linking/LinkFollowBox.scss
+++ /dev/null
@@ -1,93 +0,0 @@
-@import "../globalCssVariables";
-
-.linkFollowBox-main {
- position: absolute;
- background: whitesmoke;
- color: grey;
- border-radius: 15px;
- box-shadow: $intermediate-color 0.2vw 0.2vw 0.4vw;
- border: solid #BBBBBBBB 5px;
- pointer-events: all;
-
- .linkFollowBox-header {
- height: 50px;
- text-align: center;
- text-transform: uppercase;
- letter-spacing: 2px;
- font-size: 16px;
- width: 100%;
- }
-
- .direction-indicator {
- font-size: 12px;
- }
-
- .closeDocument {
- position: relative;
- max-width: 30px;
- top: -20px;
- left: 460px;
- color: $darker-alt-accent
- }
-
- .closeDocument:hover {
- color: $main-accent;
- }
-
- .topHeader {
- width: 100%;
- height: 25px;
- }
-
- .linkFollowBox-footer {
- height: 50px;
- text-align: center;
- display: flex;
- justify-content: center;
- align-items: center;
-
- button {
- background-color: $darker-alt-accent;
- width: 30%;
- }
- }
-
- .linkFollowBox-content {
- display: grid;
- grid-template-columns: 1fr 1fr 1fr;
- grid-column-gap: 5px;
- margin-left: 5px;
- margin-right: 5px;
-
- .linkFollowBox-item {
- background-color: $light-color;
- width: 100%;
- height: 100%;
-
- .linkFollowBox-itemContent {
- padding: 5px;
- font-size: 12px;
- overflow: scroll;
-
- input[type=radio] {
- border: 0px;
- margin-right: 5px;
- }
- }
-
- .title {
- display: flex;
- justify-content: center;
- align-items: center;
- text-transform: uppercase;
- color: $light-color;
- background-color: $lighter-alt-accent;
- width: 100%;
- height: 30px;
- border-bottom: solid $darker-alt-accent 5px;
- font-size: 12px;
- text-align: center;
- }
- }
- }
-} \ No newline at end of file
diff --git a/src/client/views/linking/LinkFollowBox.tsx b/src/client/views/linking/LinkFollowBox.tsx
deleted file mode 100644
index 325c92413..000000000
--- a/src/client/views/linking/LinkFollowBox.tsx
+++ /dev/null
@@ -1,571 +0,0 @@
-import { observable, computed, action, runInAction, reaction, IReactionDisposer } from "mobx";
-import React = require("react");
-import { observer } from "mobx-react";
-import { FieldViewProps, FieldView } from "../nodes/FieldView";
-import { Doc, DocListCastAsync, Opt } from "../../../new_fields/Doc";
-import { undoBatch } from "../../util/UndoManager";
-import { NumCast, FieldValue, Cast, StrCast } from "../../../new_fields/Types";
-import { CollectionViewType } from "../collections/CollectionView";
-import { CollectionDockingView } from "../collections/CollectionDockingView";
-import { SelectionManager } from "../../util/SelectionManager";
-import { DocumentManager } from "../../util/DocumentManager";
-import { DocumentView } from "../nodes/DocumentView";
-import "./LinkFollowBox.scss";
-import { SearchUtil } from "../../util/SearchUtil";
-import { Id } from "../../../new_fields/FieldSymbols";
-import { listSpec } from "../../../new_fields/Schema";
-import { DocServer } from "../../DocServer";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { faTimes } from '@fortawesome/free-solid-svg-icons';
-import { docs_v1 } from "googleapis";
-import { Utils } from "../../../Utils";
-import { Link } from "@react-pdf/renderer";
-
-enum FollowModes {
- OPENTAB = "Open in Tab",
- OPENRIGHT = "Open in Right Split",
- OPENFULL = "Open Full Screen",
- PAN = "Pan to Document",
- INPLACE = "Open In Place"
-}
-
-enum FollowOptions {
- ZOOM = "Zoom",
- NOZOOM = "No Zoom",
-}
-
-@observer
-export class LinkFollowBox extends React.Component<FieldViewProps> {
-
- public static LayoutString(fieldKey: string) { return FieldView.LayoutString(LinkFollowBox, fieldKey); }
- public static Instance: LinkFollowBox | undefined;
- @observable static linkDoc: Doc | undefined = undefined;
- @observable static destinationDoc: Doc | undefined = undefined;
- @observable static sourceDoc: Doc | undefined = undefined;
- @observable selectedMode: string = "";
- @observable selectedContext: Doc | undefined = undefined;
- @observable selectedContextAliases: Doc[] | undefined = undefined;
- @observable selectedOption: string = "";
- @observable selectedContextString: string = "";
- @observable sourceView: DocumentView | undefined = undefined;
- @observable canPan: boolean = false;
- @observable shouldUseOnlyParentContext = false;
- _contextDisposer?: IReactionDisposer;
-
- @observable private _docs: { col: Doc, target: Doc }[] = [];
- @observable private _otherDocs: { col: Doc, target: Doc }[] = [];
-
- constructor(props: FieldViewProps) {
- super(props);
- LinkFollowBox.Instance = this;
- this.resetVars();
- this.props.Document.isBackground = true;
- }
-
- componentDidMount = () => {
- this.resetVars();
-
- this._contextDisposer = reaction(
- () => this.selectedContextString,
- async () => {
- const ref = await DocServer.GetRefField(this.selectedContextString);
- runInAction(() => {
- if (ref instanceof Doc) {
- this.selectedContext = ref;
- }
- });
- if (this.selectedContext instanceof Doc) {
- const aliases = await SearchUtil.GetViewsOfDocument(this.selectedContext);
- runInAction(() => { this.selectedContextAliases = aliases; });
- }
- }
- );
- }
-
- componentWillUnmount = () => {
- this._contextDisposer && this._contextDisposer();
- }
-
- async resetPan() {
- if (LinkFollowBox.destinationDoc && this.sourceView && this.sourceView.props.ContainingCollectionDoc) {
- runInAction(() => this.canPan = false);
- if (this.sourceView.props.ContainingCollectionDoc._viewType === CollectionViewType.Freeform) {
- const docs = Cast(this.sourceView.props.ContainingCollectionDoc.data, listSpec(Doc), []);
- const aliases = await SearchUtil.GetViewsOfDocument(Doc.GetProto(LinkFollowBox.destinationDoc));
-
- aliases.forEach(alias => {
- if (docs.filter(doc => doc === alias).length > 0) {
- runInAction(() => { this.canPan = true; });
- }
- });
- }
- }
- }
-
- @action
- resetVars = () => {
- this.selectedContext = undefined;
- this.selectedContextString = "";
- this.selectedMode = "";
- this.selectedOption = "";
- LinkFollowBox.linkDoc = undefined;
- LinkFollowBox.sourceDoc = undefined;
- LinkFollowBox.destinationDoc = undefined;
- this.sourceView = undefined;
- this.canPan = false;
- this.shouldUseOnlyParentContext = false;
- }
-
- async fetchDocuments() {
- if (LinkFollowBox.destinationDoc) {
- const dest: Doc = LinkFollowBox.destinationDoc;
- const aliases = await SearchUtil.GetViewsOfDocument(Doc.GetProto(dest));
- const { docs } = await SearchUtil.Search("", true, { fq: `data_l:"${dest[Id]}"` });
- const map: Map<Doc, Doc> = new Map;
- const allDocs = await Promise.all(aliases.map(doc => SearchUtil.Search("", true, { fq: `data_l:"${doc[Id]}"` }).then(result => result.docs)));
- allDocs.forEach((docs, index) => docs.forEach(doc => map.set(doc, aliases[index])));
- docs.forEach(doc => map.delete(doc));
- runInAction(async () => {
- this._docs = docs.filter(doc => !Doc.AreProtosEqual(doc, CollectionDockingView.Instance.props.Document)).map(doc => ({ col: doc, target: dest }));
- this._otherDocs = Array.from(map.entries()).filter(entry => !Doc.AreProtosEqual(entry[0], CollectionDockingView.Instance.props.Document)).map(([col, target]) => ({ col, target }));
- const tcontext = LinkFollowBox.linkDoc && (await Cast(LinkFollowBox.linkDoc.anchor2Context, Doc)) as Doc;
- runInAction(() => tcontext && this._docs.splice(0, 0, { col: tcontext, target: dest }));
- });
- }
- }
-
- @action
- setLinkDocs = (linkDoc: Doc, source: Doc, dest: Doc) => {
- this.resetVars();
-
- LinkFollowBox.linkDoc = linkDoc;
- LinkFollowBox.sourceDoc = source;
- LinkFollowBox.destinationDoc = dest;
- this.fetchDocuments();
-
- SelectionManager.SelectedDocuments().forEach(dv => {
- if (dv.props.Document === LinkFollowBox.sourceDoc) {
- this.sourceView = dv;
- }
- });
-
- this.resetPan();
- }
-
- highlightDoc = () => LinkFollowBox.destinationDoc && Doc.linkFollowHighlight(LinkFollowBox.destinationDoc);
-
- @undoBatch
- openFullScreen = () => {
- if (LinkFollowBox.destinationDoc) {
- const view = DocumentManager.Instance.getDocumentView(LinkFollowBox.destinationDoc);
- view && CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(view);
- }
- }
-
- @undoBatch
- openColFullScreen = (options: { context: Doc }) => {
- if (LinkFollowBox.destinationDoc) {
- if (NumCast(options.context._viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) {
- const newPanX = NumCast(LinkFollowBox.destinationDoc.x) + NumCast(LinkFollowBox.destinationDoc._width) / 2;
- const newPanY = NumCast(LinkFollowBox.destinationDoc.y) + NumCast(LinkFollowBox.destinationDoc._height) / 2;
- options.context._panX = newPanX;
- options.context._panY = newPanY;
- }
- const view = DocumentManager.Instance.getDocumentView(options.context);
- view && CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(view);
- this.highlightDoc();
- }
- }
-
- // should container be a doc or documentview or what? This one needs work and is more long term
- @undoBatch
- openInContainer = (options: { container: Doc }) => {
-
- }
-
- static _addDocTab: (undefined | ((doc: Doc, dataDoc: Opt<Doc>, where: string) => boolean));
-
- static setAddDocTab = (addFunc: (doc: Doc, dataDoc: Opt<Doc>, where: string) => boolean) => {
- LinkFollowBox._addDocTab = addFunc;
- }
-
- @undoBatch
- openLinkColRight = (options: { context: Doc, shouldZoom: boolean }) => {
- if (LinkFollowBox.destinationDoc) {
- options.context = Doc.IsPrototype(options.context) ? Doc.MakeDelegate(options.context) : options.context;
- if (NumCast(options.context._viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) {
- const newPanX = NumCast(LinkFollowBox.destinationDoc.x) + NumCast(LinkFollowBox.destinationDoc._width) / 2;
- const newPanY = NumCast(LinkFollowBox.destinationDoc.y) + NumCast(LinkFollowBox.destinationDoc._height) / 2;
- options.context._panX = newPanX;
- options.context._panY = newPanY;
- }
- (LinkFollowBox._addDocTab || this.props.addDocTab)(options.context, undefined, "onRight");
-
- if (options.shouldZoom) this.jumpToLink({ shouldZoom: options.shouldZoom });
-
- this.highlightDoc();
- SelectionManager.DeselectAll();
- }
- }
-
- @undoBatch
- openLinkRight = () => {
- if (LinkFollowBox.destinationDoc) {
- const alias = Doc.MakeAlias(LinkFollowBox.destinationDoc);
- (LinkFollowBox._addDocTab || this.props.addDocTab)(alias, undefined, "onRight");
- this.highlightDoc();
- SelectionManager.DeselectAll();
- }
-
- }
-
- @undoBatch
- jumpToLink = async (options: { shouldZoom: boolean }) => {
- if (LinkFollowBox.sourceDoc && LinkFollowBox.linkDoc) {
- const focus = (document: Doc) => { (LinkFollowBox._addDocTab || this.props.addDocTab)(document, undefined, "inTab"); SelectionManager.DeselectAll(); };
- //let focus = (doc: Doc, maxLocation: string) => this.props.focus(docthis.props.focus(LinkFollowBox.destinationDoc, true, 1, () => this.props.addDocTab(doc, undefined, maxLocation));
-
- DocumentManager.Instance.FollowLink(LinkFollowBox.linkDoc, LinkFollowBox.sourceDoc, focus, options && options.shouldZoom, false, undefined);
- }
- }
-
- @undoBatch
- openLinkTab = () => {
- if (LinkFollowBox.destinationDoc) {
- const fullScreenAlias = Doc.MakeAlias(LinkFollowBox.destinationDoc);
- // this.prosp.addDocTab is empty -- use the link source's addDocTab
- (LinkFollowBox._addDocTab || this.props.addDocTab)(fullScreenAlias, undefined, "inTab");
-
- this.highlightDoc();
- SelectionManager.DeselectAll();
- }
- }
-
- @undoBatch
- openLinkColTab = (options: { context: Doc, shouldZoom: boolean }) => {
- if (LinkFollowBox.destinationDoc) {
- options.context = Doc.IsPrototype(options.context) ? Doc.MakeDelegate(options.context) : options.context;
- if (NumCast(options.context._viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) {
- const newPanX = NumCast(LinkFollowBox.destinationDoc.x) + NumCast(LinkFollowBox.destinationDoc._width) / 2;
- const newPanY = NumCast(LinkFollowBox.destinationDoc.y) + NumCast(LinkFollowBox.destinationDoc._height) / 2;
- options.context._panX = newPanX;
- options.context._panY = newPanY;
- }
- (LinkFollowBox._addDocTab || this.props.addDocTab)(options.context, undefined, "inTab");
- if (options.shouldZoom) this.jumpToLink({ shouldZoom: options.shouldZoom });
-
- this.highlightDoc();
- SelectionManager.DeselectAll();
- }
- }
-
- @undoBatch
- openLinkInPlace = (options: { shouldZoom: boolean }) => {
-
- if (LinkFollowBox.destinationDoc && LinkFollowBox.sourceDoc) {
- if (this.sourceView && this.sourceView.props.addDocument) {
- const destViews = DocumentManager.Instance.getDocumentViews(LinkFollowBox.destinationDoc);
- if (!destViews.find(dv => dv.props.ContainingCollectionView === this.sourceView!.props.ContainingCollectionView)) {
- const alias = Doc.MakeAlias(LinkFollowBox.destinationDoc);
- const y = NumCast(LinkFollowBox.sourceDoc.y);
- const x = NumCast(LinkFollowBox.sourceDoc.x);
-
- const width = NumCast(LinkFollowBox.sourceDoc._width);
- const height = NumCast(LinkFollowBox.sourceDoc._height);
-
- alias.x = x + width + 30;
- alias.y = y;
- alias._width = width;
- alias._height = height;
-
- this.sourceView.props.addDocument(alias);
- }
- }
-
- this.jumpToLink({ shouldZoom: options.shouldZoom });
-
- this.highlightDoc();
- SelectionManager.DeselectAll();
- }
- }
-
- //set this to be the default link behavior, can be any of the above
- public defaultLinkBehavior: (options?: any) => void = this.jumpToLink;
-
- @action
- currentLinkBehavior = () => {
- // this.resetPan();
- if (LinkFollowBox.destinationDoc) {
- if (this.selectedContextString === "") {
- this.selectedContextString = "self";
- this.selectedContext = LinkFollowBox.destinationDoc;
- }
- if (this.selectedOption === "") this.selectedOption = FollowOptions.NOZOOM;
- const shouldZoom: boolean = this.selectedOption === FollowOptions.NOZOOM ? false : true;
- const notOpenInContext: boolean = this.selectedContextString === "self" || this.selectedContextString === LinkFollowBox.destinationDoc[Id];
-
- if (this.selectedMode === FollowModes.INPLACE) {
- if (shouldZoom !== undefined) this.openLinkInPlace({ shouldZoom: shouldZoom });
- }
- else if (this.selectedMode === FollowModes.OPENFULL) {
- if (notOpenInContext) this.openFullScreen();
- else this.selectedContext && this.openColFullScreen({ context: this.selectedContext });
- }
- else if (this.selectedMode === FollowModes.OPENRIGHT) {
- if (notOpenInContext) this.openLinkRight();
- else this.selectedContext && this.openLinkColRight({ context: this.selectedContext, shouldZoom: shouldZoom });
- }
- else if (this.selectedMode === FollowModes.OPENTAB) {
- if (notOpenInContext) this.openLinkTab();
- else this.selectedContext && this.openLinkColTab({ context: this.selectedContext, shouldZoom: shouldZoom });
- }
- else if (this.selectedMode === FollowModes.PAN) {
- this.jumpToLink({ shouldZoom: shouldZoom });
- }
- else return;
- }
- }
-
- @action
- handleModeChange = (e: React.ChangeEvent) => {
- const target = e.target as HTMLInputElement;
- this.selectedMode = target.value;
- this.selectedContext = undefined;
- this.selectedContextString = "";
-
- this.shouldUseOnlyParentContext = (this.selectedMode === FollowModes.INPLACE || this.selectedMode === FollowModes.PAN);
-
- if (this.shouldUseOnlyParentContext) {
- if (this.sourceView && this.sourceView.props.ContainingCollectionDoc) {
- this.selectedContext = this.sourceView.props.ContainingCollectionDoc;
- this.selectedContextString = (StrCast(this.sourceView.props.ContainingCollectionDoc.title));
- }
- }
- }
-
- @action
- handleOptionChange = (e: React.ChangeEvent) => {
- const target = e.target as HTMLInputElement;
- this.selectedOption = target.value;
- }
-
- @action
- handleContextChange = (e: React.ChangeEvent) => {
- const target = e.target as HTMLInputElement;
- this.selectedContextString = target.value;
- // selectedContext is updated in reaction
- this.selectedOption = "";
- }
-
- @computed
- get canOpenInPlace() {
- if (this.sourceView && this.sourceView.props.ContainingCollectionDoc) {
- const colDoc = this.sourceView.props.ContainingCollectionDoc;
- if (colDoc._viewType === CollectionViewType.Freeform) return true;
- }
- return false;
- }
-
- @computed
- get availableModes() {
- return (
- <div>
- <label><input
- type="radio"
- name="mode"
- value={FollowModes.OPENRIGHT}
- checked={this.selectedMode === FollowModes.OPENRIGHT}
- onChange={this.handleModeChange}
- disabled={false} />
- {FollowModes.OPENRIGHT}
- </label><br />
- <label><input
- type="radio"
- name="mode"
- value={FollowModes.OPENTAB}
- checked={this.selectedMode === FollowModes.OPENTAB}
- onChange={this.handleModeChange}
- disabled={false} />
- {FollowModes.OPENTAB}
- </label><br />
- <label><input
- type="radio"
- name="mode"
- value={FollowModes.OPENFULL}
- checked={this.selectedMode === FollowModes.OPENFULL}
- onChange={this.handleModeChange}
- disabled={false} />
- {FollowModes.OPENFULL}
- </label><br />
- <label><input
- type="radio"
- name="mode"
- value={FollowModes.PAN}
- checked={this.selectedMode === FollowModes.PAN}
- onChange={this.handleModeChange}
- disabled={!this.canPan} />
- {FollowModes.PAN}
- </label><br />
- <label><input
- type="radio"
- name="mode"
- value={FollowModes.INPLACE}
- checked={this.selectedMode === FollowModes.INPLACE}
- onChange={this.handleModeChange}
- disabled={!this.canOpenInPlace} />
- {FollowModes.INPLACE}
- </label><br />
- </div>
- );
- }
-
- @computed
- get parentName() {
- if (this.sourceView && this.sourceView.props.ContainingCollectionDoc) {
- return this.sourceView.props.ContainingCollectionDoc.title;
- }
- }
-
- @computed
- get parentID(): string {
- if (this.sourceView && this.sourceView.props.ContainingCollectionDoc) {
- return StrCast(this.sourceView.props.ContainingCollectionDoc[Id]);
- }
- return "col";
- }
-
- @computed
- get availableContexts() {
- return (
- this.shouldUseOnlyParentContext ?
- <label><input
- type="radio" disabled={true}
- name="context"
- value={this.parentID}
- checked={true} />
- {this.parentName} (Parent Collection)
- </label>
- :
- <div>
- <label><input
- type="radio" disabled={LinkFollowBox.linkDoc ? false : true}
- name="context"
- value={LinkFollowBox.destinationDoc ? StrCast(LinkFollowBox.destinationDoc[Id]) : "self"}
- checked={LinkFollowBox.destinationDoc ? this.selectedContextString === StrCast(LinkFollowBox.destinationDoc[Id]) || this.selectedContextString === "self" : true}
- onChange={this.handleContextChange} />
- Open Self
- </label><br />
- {[...this._docs, ...this._otherDocs].map(doc => {
- if (doc && doc.target && doc.col.title !== "Recently Closed") {
- return <div key={doc.col[Id] + doc.target[Id]}><label key={doc.col[Id] + doc.target[Id]}>
- <input
- type="radio" disabled={LinkFollowBox.linkDoc ? false : true}
- name="context"
- value={StrCast(doc.col[Id])}
- checked={this.selectedContextString === StrCast(doc.col[Id])}
- onChange={this.handleContextChange} />
- {doc.col.title}
- </label><br /></div>;
- }
- })}
- </div>
- );
- }
-
- @computed
- get shouldShowZoom(): boolean {
- if (this.selectedMode === FollowModes.OPENFULL) return false;
- if (this.shouldUseOnlyParentContext) return true;
- if (LinkFollowBox.destinationDoc ? this.selectedContextString === LinkFollowBox.destinationDoc[Id] : "self") return false;
-
- let contextMatch: boolean = false;
- if (this.selectedContextAliases) {
- this.selectedContextAliases.forEach(alias => {
- if (alias._viewType === CollectionViewType.Freeform) contextMatch = true;
- });
- }
- if (contextMatch) return true;
-
- return false;
- }
-
- @computed
- get availableOptions() {
- if (LinkFollowBox.destinationDoc) {
- return (
- this.shouldShowZoom ?
- <div>
- <label><input
- type="radio"
- name="option"
- value={FollowOptions.ZOOM}
- checked={this.selectedOption === FollowOptions.ZOOM}
- onChange={this.handleOptionChange}
- disabled={false} />
- {FollowOptions.ZOOM}
- </label><br />
- <label><input
- type="radio"
- name="option"
- value={FollowOptions.NOZOOM}
- checked={this.selectedOption === FollowOptions.NOZOOM}
- onChange={this.handleOptionChange}
- disabled={false} />
- {FollowOptions.NOZOOM}
- </label><br />
- </div>
- :
- <div>No Available Options</div>
- );
- }
- return null;
- }
-
- render() {
- return (
- <div className="linkFollowBox-main" style={{ height: NumCast(this.props.Document._height), width: NumCast(this.props.Document._width) }}>
- <div className="linkFollowBox-header">
- <div className="topHeader">
- {LinkFollowBox.linkDoc ? "Link Title: " + StrCast(LinkFollowBox.linkDoc.title) : "No Link Selected"}
- <div onClick={() => this.props.Document.isMinimized = true} className="closeDocument"><FontAwesomeIcon icon={faTimes} size="lg" /></div>
- </div>
- <div className=" direction-indicator">{LinkFollowBox.linkDoc ?
- LinkFollowBox.sourceDoc && LinkFollowBox.destinationDoc ? "Source: " + StrCast(LinkFollowBox.sourceDoc.title) + ", Destination: " + StrCast(LinkFollowBox.destinationDoc.title)
- : "" : ""}</div>
- </div>
- <div className="linkFollowBox-content" style={{ height: NumCast(this.props.Document._height) - 110 }}>
- <div className="linkFollowBox-item">
- <div className="linkFollowBox-item title">Mode</div>
- <div className="linkFollowBox-itemContent">
- {LinkFollowBox.linkDoc ? this.availableModes : "Please select a link to view modes"}
- </div>
- </div>
- <div className="linkFollowBox-item">
- <div className="linkFollowBox-item title">Context</div>
- <div className="linkFollowBox-itemContent">
- {this.selectedMode !== "" ? this.availableContexts : "Please select a mode to view contexts"}
- </div>
- </div>
- <div className="linkFollowBox-item">
- <div className="linkFollowBox-item title">Options</div>
- <div className="linkFollowBox-itemContent">
- {this.selectedContextString !== "" ? this.availableOptions : "Please select a context to view options"}
- </div>
- </div>
- </div>
- <div className="linkFollowBox-footer">
- <button
- onClick={this.resetVars}>
- Clear Link
- </button>
- <div style={{ width: 20 }}></div>
- <button
- onClick={this.currentLinkBehavior}
- disabled={(LinkFollowBox.linkDoc) ? false : true}>
- Follow Link
- </button>
- </div>
- </div>
- );
- }
-} \ No newline at end of file
diff --git a/src/client/views/linking/LinkMenu.tsx b/src/client/views/linking/LinkMenu.tsx
index 1a40f0c55..b768eacc3 100644
--- a/src/client/views/linking/LinkMenu.tsx
+++ b/src/client/views/linking/LinkMenu.tsx
@@ -16,7 +16,7 @@ library.add(faTrash);
interface Props {
docView: DocumentView;
changeFlyout: () => void;
- addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean;
+ addDocTab: (document: Doc, where: string) => boolean;
}
@observer
@@ -60,7 +60,7 @@ export class LinkMenu extends React.Component<Props> {
if (this._editingLink === undefined) {
return (
<div className="linkMenu">
- <button className="linkEditor-button linkEditor-clearButton" onClick={() => this.clearAllLinks()} title="Clear all links"><FontAwesomeIcon icon="trash" size="sm" /></button>
+ {/* <button className="linkEditor-button linkEditor-clearButton" onClick={() => this.clearAllLinks()} title="Clear all links"><FontAwesomeIcon icon="trash" size="sm" /></button> */}
{/* <input id="linkMenu-searchBar" type="text" placeholder="Search..."></input> */}
<div className="linkMenu-list">
{this.renderAllGroups(groups)}
diff --git a/src/client/views/linking/LinkMenuGroup.tsx b/src/client/views/linking/LinkMenuGroup.tsx
index 0c38ff45c..928413a11 100644
--- a/src/client/views/linking/LinkMenuGroup.tsx
+++ b/src/client/views/linking/LinkMenuGroup.tsx
@@ -17,7 +17,7 @@ interface LinkMenuGroupProps {
group: Doc[];
groupType: string;
showEditor: (linkDoc: Doc) => void;
- addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean;
+ addDocTab: (document: Doc, where: string) => boolean;
docView: DocumentView;
}
@@ -47,7 +47,7 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> {
document.removeEventListener("pointerup", this.onLinkButtonUp);
const targets = this.props.group.map(l => LinkManager.Instance.getOppositeAnchor(l, this.props.sourceDoc)).filter(d => d) as Doc[];
- DragManager.StartLinkTargetsDrag(this._drag.current, e.x, e.y, this.props.sourceDoc, targets);
+ DragManager.StartLinkTargetsDrag(this._drag.current, this.props.docView, e.x, e.y, this.props.sourceDoc, targets);
}
e.stopPropagation();
}
@@ -58,7 +58,7 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> {
if (index > -1) keys.splice(index, 1);
const cols = ["anchor1", "anchor2", ...[...keys]].map(c => new SchemaHeaderField(c, "#f1efeb"));
const docs: Doc[] = LinkManager.Instance.getAllMetadataDocsInGroup(groupType);
- const createTable = action(() => Docs.Create.SchemaDocument(cols, docs, { _width: 500, _height: 300, title: groupType + " table" }));
+ const createTable = action(() => Docs.Create.SchemaDocument(cols, docs, { _width: 500, _height: 300, title: groupType + " table", childDropAction: "alias" }));
const ref = React.createRef<HTMLDivElement>();
return <div ref={ref}><button className="linkEditor-button linkEditor-tableButton" onPointerDown={SetupDrag(ref, createTable)} title="Drag to view relationship table"><FontAwesomeIcon icon="table" size="sm" /></button></div>;
}
@@ -70,6 +70,7 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> {
return <LinkMenuItem key={destination[Id] + this.props.sourceDoc[Id]}
groupType={this.props.groupType}
addDocTab={this.props.addDocTab}
+ docView={this.props.docView}
linkDoc={linkDoc}
sourceDoc={this.props.sourceDoc}
destinationDoc={destination}
diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index b7d27ee30..d091e06ef 100644
--- a/src/client/views/linking/LinkMenuItem.tsx
+++ b/src/client/views/linking/LinkMenuItem.tsx
@@ -8,19 +8,22 @@ import { Cast, StrCast } from '../../../new_fields/Types';
import { DragManager } from '../../util/DragManager';
import { LinkManager } from '../../util/LinkManager';
import { ContextMenu } from '../ContextMenu';
-import { LinkFollowBox } from './LinkFollowBox';
import './LinkMenuItem.scss';
import React = require("react");
+import { DocumentManager } from '../../util/DocumentManager';
+import { setupMoveUpEvents, emptyFunction } from '../../../Utils';
+import { DocumentView } from '../nodes/DocumentView';
library.add(faEye, faEdit, faTimes, faArrowRight, faChevronDown, faChevronUp);
interface LinkMenuItemProps {
groupType: string;
linkDoc: Doc;
+ docView: DocumentView;
sourceDoc: Doc;
destinationDoc: Doc;
showEditor: (linkDoc: Doc) => void;
- addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean;
+ addDocTab: (document: Doc, where: string) => boolean;
}
@observer
@@ -29,29 +32,28 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
private _downX = 0;
private _downY = 0;
private _eleClone: any;
+
+ _editRef = React.createRef<HTMLDivElement>();
@observable private _showMore: boolean = false;
- @action toggleShowMore() { this._showMore = !this._showMore; }
+ @action toggleShowMore(e: React.PointerEvent) { e.stopPropagation(); this._showMore = !this._showMore; }
onEdit = (e: React.PointerEvent): void => {
- e.stopPropagation();
- this.props.showEditor(this.props.linkDoc);
- //SelectionManager.DeselectAll();
+ setupMoveUpEvents(this, e, this.editMoved, emptyFunction, () => this.props.showEditor(this.props.linkDoc));
+ }
+
+ editMoved = (e: PointerEvent) => {
+ DragManager.StartDocumentDrag([this._editRef.current!], new DragManager.DocumentDragData([this.props.linkDoc]), e.x, e.y);
+ return true;
}
renderMetadata = (): JSX.Element => {
- const groups = LinkManager.Instance.getAnchorGroups(this.props.linkDoc, this.props.sourceDoc);
- const index = groups.findIndex(groupDoc => StrCast(groupDoc.type).toUpperCase() === this.props.groupType.toUpperCase());
- const groupDoc = index > -1 ? groups[index] : undefined;
+ const index = StrCast(this.props.linkDoc.title).toUpperCase() === this.props.groupType.toUpperCase() ? 0 : -1;
+ const mdDoc = index > -1 ? this.props.linkDoc : undefined;
let mdRows: Array<JSX.Element> = [];
- if (groupDoc) {
- const mdDoc = Cast(groupDoc.metadata, Doc, null);
- if (mdDoc) {
- const keys = LinkManager.Instance.getMetadataKeysInGroup(this.props.groupType);//groupMetadataKeys.get(this.props.groupType);
- mdRows = keys.map(key => {
- return (<div key={key} className="link-metadata-row"><b>{key}</b>: {StrCast(mdDoc[key])}</div>);
- });
- }
+ if (mdDoc) {
+ const keys = LinkManager.Instance.getMetadataKeysInGroup(this.props.groupType);//groupMetadataKeys.get(this.props.groupType);
+ mdRows = keys.map(key => <div key={key} className="link-metadata-row"><b>{key}</b>: {StrCast(mdDoc[key])}</div>);
}
return (<div className="link-metadata">{mdRows}</div>);
@@ -72,11 +74,6 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
document.removeEventListener("pointermove", this.onLinkButtonMoved);
document.removeEventListener("pointerup", this.onLinkButtonUp);
- if (LinkFollowBox.Instance !== undefined) {
- LinkFollowBox.Instance.props.Document.isMinimized = false;
- LinkFollowBox.Instance.setLinkDocs(this.props.linkDoc, this.props.sourceDoc, this.props.destinationDoc);
- LinkFollowBox.setAddDocTab(this.props.addDocTab);
- }
e.stopPropagation();
}
@@ -86,33 +83,20 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
document.removeEventListener("pointerup", this.onLinkButtonUp);
this._eleClone.style.transform = `translate(${e.x}px, ${e.y}px)`;
- DragManager.StartLinkTargetsDrag(this._eleClone, e.x, e.y, this.props.sourceDoc, [this.props.linkDoc]);
+ DragManager.StartLinkTargetsDrag(this._eleClone, this.props.docView, e.x, e.y, this.props.sourceDoc, [this.props.linkDoc]);
}
e.stopPropagation();
}
onContextMenu = (e: React.MouseEvent) => {
e.preventDefault();
- ContextMenu.Instance.addItem({ description: "Open in Link Follower", event: () => this.openLinkFollower(), icon: "link" });
ContextMenu.Instance.addItem({ description: "Follow Default Link", event: () => this.followDefault(), icon: "arrow-right" });
ContextMenu.Instance.displayMenu(e.clientX, e.clientY);
}
@action.bound
async followDefault() {
- if (LinkFollowBox.Instance !== undefined) {
- LinkFollowBox.setAddDocTab(this.props.addDocTab);
- LinkFollowBox.Instance.setLinkDocs(this.props.linkDoc, this.props.sourceDoc, this.props.destinationDoc);
- LinkFollowBox.Instance.defaultLinkBehavior();
- }
- }
-
- @action.bound
- async openLinkFollower() {
- if (LinkFollowBox.Instance !== undefined) {
- LinkFollowBox.Instance.props.Document.isMinimized = false;
- LinkFollowBox.Instance.setLinkDocs(this.props.linkDoc, this.props.sourceDoc, this.props.destinationDoc);
- }
+ DocumentManager.Instance.FollowLink(this.props.linkDoc, this.props.sourceDoc, doc => this.props.addDocTab(doc, "onRight"), false);
}
render() {
@@ -125,9 +109,9 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
<div ref={this._drag} className="linkMenu-name" title="drag to view target. click to customize." onPointerDown={this.onLinkButtonDown}>
<p >{StrCast(this.props.destinationDoc.title)}</p>
<div className="linkMenu-item-buttons">
- {canExpand ? <div title="Show more" className="button" onPointerDown={() => this.toggleShowMore()}>
+ {canExpand ? <div title="Show more" className="button" onPointerDown={e => this.toggleShowMore(e)}>
<FontAwesomeIcon className="fa-icon" icon={this._showMore ? "chevron-up" : "chevron-down"} size="sm" /></div> : <></>}
- <div title="Edit link" className="button" onPointerDown={this.onEdit}><FontAwesomeIcon className="fa-icon" icon="edit" size="sm" /></div>
+ <div title="Edit link" className="button" ref={this._editRef} onPointerDown={this.onEdit}><FontAwesomeIcon className="fa-icon" icon="edit" size="sm" /></div>
<div title="Follow link" className="button" onClick={this.followDefault} onContextMenu={this.onContextMenu}>
<FontAwesomeIcon className="fa-icon" icon="arrow-right" size="sm" />
</div>
diff --git a/src/client/views/nodes/AudioBox.scss b/src/client/views/nodes/AudioBox.scss
index 3b19a6dba..fb16b8365 100644
--- a/src/client/views/nodes/AudioBox.scss
+++ b/src/client/views/nodes/AudioBox.scss
@@ -5,11 +5,15 @@
display:flex;
pointer-events: all;
cursor:default;
+ .audiobox-buttons {
+ display: flex;
+ width: 100%;
+ align-items: center;
+ }
.audiobox-handle {
width:20px;
height:100%;
display:inline-block;
- background: gray;
}
.audiobox-control, .audiobox-control-interactive {
top:0;
@@ -25,11 +29,14 @@
pointer-events: all;
width:100%;
height:100%;
- position: absolute;
+ position: relative;
pointer-events: none;
}
.audiobox-record-interactive {
pointer-events: all;
+ width:100%;
+ height:100%;
+ position: relative;
}
.audiobox-controls {
width:100%;
@@ -37,7 +44,6 @@
position: relative;
display: flex;
padding-left: 2px;
- border: gray solid 3px;
.audiobox-player {
margin-top:auto;
margin-bottom:auto;
@@ -46,13 +52,18 @@
position: relative;
padding-right: 5px;
display: flex;
- .audiobox-playhead {
+ .audiobox-playhead, .audiobox-dictation {
position: relative;
margin-top: auto;
margin-bottom: auto;
width: 25px;
padding: 2px;
}
+ .audiobox-dictation {
+ align-items: center;
+ display: inherit;
+ background: dimgray;
+ }
.audiobox-timeline {
position:relative;
height:100%;
@@ -74,6 +85,7 @@
margin-left:-2.55px;
background:gray;
border-radius: 100%;
+ opacity:0.9;
background-color: transparent;
box-shadow: black 2px 2px 1px;
.docuLinkBox-cont {
@@ -100,7 +112,7 @@
}
}
.audiobox-linker:hover, .audiobox-linker-mini:hover {
- transform:scale(1.5);
+ opacity:1;
}
.audiobox-marker-container, .audiobox-marker-minicontainer {
position:absolute;
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 62a479b2a..03b2a2297 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -17,6 +17,11 @@ import { ContextMenu } from "../ContextMenu";
import { Id } from "../../../new_fields/FieldSymbols";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { DocumentView } from "./DocumentView";
+import { Docs } from "../../documents/Documents";
+import { ComputedField } from "../../../new_fields/ScriptField";
+import { Networking } from "../../Network";
+
+// testing testing
interface Window {
MediaRecorder: MediaRecorder;
@@ -44,45 +49,53 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
_ele: HTMLAudioElement | null = null;
_recorder: any;
_recordStart = 0;
+ _stream: MediaStream | undefined;
@observable private static _scrubTime = 0;
- @observable private _audioState: "unrecorded" | "recording" | "recorded" = "unrecorded";
- @observable private _playing = false;
- public static SetScrubTime = action((timeInMillisFrom1970: number) => AudioBox._scrubTime = timeInMillisFrom1970);
+ @computed get audioState(): undefined | "recording" | "paused" | "playing" { return this.dataDoc.audioState as (undefined | "recording" | "paused" | "playing"); }
+ set audioState(value) { this.dataDoc.audioState = value; }
+ public static SetScrubTime = (timeInMillisFrom1970: number) => { runInAction(() => AudioBox._scrubTime = 0); runInAction(() => AudioBox._scrubTime = timeInMillisFrom1970); };
public static ActiveRecordings: Doc[] = [];
+ @computed get recordingStart() { return Cast(this.dataDoc[this.props.fieldKey + "-recordingStart"], DateField)?.date.getTime(); }
+ async slideTemplate() { return (await Cast((await Cast(Doc.UserDoc().slidesBtn, Doc) as Doc).dragFactory, Doc) as Doc); }
+
+ componentWillUnmount() {
+ this._reactionDisposer?.();
+ this._linkPlayDisposer?.();
+ this._scrubbingDisposer?.();
+ }
componentDidMount() {
- runInAction(() => this._audioState = this.path ? "recorded" : "unrecorded");
+ runInAction(() => this.audioState = this.path ? "paused" : undefined);
this._linkPlayDisposer = reaction(() => this.layoutDoc.scrollToLinkID,
scrollLinkId => {
- scrollLinkId && DocListCast(this.dataDoc.links).filter(l => l[Id] === scrollLinkId).map(l => {
- const la1 = l.anchor1 as Doc;
- const linkTime = Doc.AreProtosEqual(la1, this.dataDoc) ? NumCast(l.anchor1Timecode) : NumCast(l.anchor2Timecode);
- setTimeout(() => { this.playFrom(linkTime); Doc.linkFollowHighlight(l); }, 250);
- });
- scrollLinkId && Doc.SetInPlace(this.layoutDoc, "scrollToLinkID", undefined, false);
+ if (scrollLinkId) {
+ DocListCast(this.dataDoc.links).filter(l => l[Id] === scrollLinkId).map(l => {
+ const linkTime = Doc.AreProtosEqual(l.anchor1 as Doc, this.dataDoc) ? NumCast(l.anchor1_timecode) : NumCast(l.anchor2_timecode);
+ setTimeout(() => { this.playFromTime(linkTime); Doc.linkFollowHighlight(l); }, 250);
+ });
+ Doc.SetInPlace(this.layoutDoc, "scrollToLinkID", undefined, false);
+ }
}, { fireImmediately: true });
this._reactionDisposer = reaction(() => SelectionManager.SelectedDocuments(),
selected => {
const sel = selected.length ? selected[0].props.Document : undefined;
- this.Document.playOnSelect && sel && !Doc.AreProtosEqual(sel, this.props.Document) && this.playFrom(DateCast(sel.creationTime).date.getTime());
+ this.Document.playOnSelect && this.recordingStart && sel && sel.creationDate && !Doc.AreProtosEqual(sel, this.props.Document) && this.playFromTime(DateCast(sel.creationDate).date.getTime());
+ this.Document.playOnSelect && this.recordingStart && !sel && this.pause();
});
- this._scrubbingDisposer = reaction(() => AudioBox._scrubTime, timeInMillisecondsFrom1970 => {
- const start = DateCast(this.dataDoc[this.props.fieldKey + "-recordingStart"]);
- start && this.playFrom((timeInMillisecondsFrom1970 - start.date.getTime()) / 1000);
- });
+ this._scrubbingDisposer = reaction(() => AudioBox._scrubTime, (time) => this.Document.playOnSelect && this.playFromTime(AudioBox._scrubTime));
}
timecodeChanged = () => {
const htmlEle = this._ele;
- if (this._audioState === "recorded" && htmlEle) {
+ if (this.audioState !== "recording" && htmlEle) {
htmlEle.duration && htmlEle.duration !== Infinity && runInAction(() => this.dataDoc.duration = htmlEle.duration);
DocListCast(this.dataDoc.links).map(l => {
let la1 = l.anchor1 as Doc;
- let linkTime = NumCast(l.anchor2Timecode);
+ let linkTime = NumCast(l.anchor2_timecode);
if (Doc.AreProtosEqual(la1, this.dataDoc)) {
la1 = l.anchor2 as Doc;
- linkTime = NumCast(l.anchor1Timecode);
+ linkTime = NumCast(l.anchor1_timecode);
}
if (linkTime > NumCast(this.Document.currentTimecode) && linkTime < htmlEle.currentTime) {
Doc.linkFollowHighlight(la1);
@@ -94,68 +107,52 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
pause = action(() => {
this._ele!.pause();
- this._playing = false;
+ this.audioState = "paused";
});
+ playFromTime = (absoluteTime: number) => {
+ this.recordingStart && this.playFrom((absoluteTime - this.recordingStart) / 1000);
+ }
playFrom = (seekTimeInSeconds: number) => {
if (this._ele && AudioBox.Enabled) {
if (seekTimeInSeconds < 0) {
- this.pause();
+ if (seekTimeInSeconds > -1) {
+ setTimeout(() => this.playFrom(0), -seekTimeInSeconds * 1000);
+ } else {
+ this.pause();
+ }
} else if (seekTimeInSeconds <= this._ele.duration) {
this._ele.currentTime = seekTimeInSeconds;
this._ele.play();
- runInAction(() => this._playing = true);
+ runInAction(() => this.audioState = "playing");
} else {
this.pause();
}
}
}
- componentWillUnmount() {
- this._reactionDisposer && this._reactionDisposer();
- this._linkPlayDisposer && this._linkPlayDisposer();
- this._scrubbingDisposer && this._scrubbingDisposer();
- }
-
updateRecordTime = () => {
- if (this._audioState === "recording") {
+ if (this.audioState === "recording") {
setTimeout(this.updateRecordTime, 30);
this.Document.currentTimecode = (new Date().getTime() - this._recordStart) / 1000;
}
}
- recordAudioAnnotation = () => {
- let gumStream: any;
- const self = this;
- navigator.mediaDevices.getUserMedia({
- audio: true
- }).then(function (stream) {
- gumStream = stream;
- self._recorder = new MediaRecorder(stream);
- self.dataDoc[self.props.fieldKey + "-recordingStart"] = new DateField(new Date());
- AudioBox.ActiveRecordings.push(self.props.Document);
- self._recorder.ondataavailable = async function (e: any) {
- const formData = new FormData();
- formData.append("file", e.data);
- const res = await fetch(Utils.prepend("/uploadFormData"), {
- method: 'POST',
- body: formData
- });
- const files = await res.json();
- const url = Utils.prepend(files[0].path);
- // upload to server with known URL
- self.props.Document[self.props.fieldKey] = new AudioField(url);
- };
- runInAction(() => self._audioState = "recording");
- self._recordStart = new Date().getTime();
- setTimeout(self.updateRecordTime, 0);
- self._recorder.start();
- setTimeout(() => {
- self.stopRecording();
- gumStream.getAudioTracks()[0].stop();
- }, 60 * 60 * 1000); // stop after an hour?
- });
+ recordAudioAnnotation = async () => {
+ this._stream = await navigator.mediaDevices.getUserMedia({ audio: true });
+ this._recorder = new MediaRecorder(this._stream);
+ this.dataDoc[this.props.fieldKey + "-recordingStart"] = new DateField(new Date());
+ AudioBox.ActiveRecordings.push(this.props.Document);
+ this._recorder.ondataavailable = async (e: any) => {
+ const [{ result }] = await Networking.UploadFilesToServer(e.data);
+ this.props.Document[this.props.fieldKey] = new AudioField(Utils.prepend(result.accessPaths.agnostic.client));
+ };
+ this._recordStart = new Date().getTime();
+ runInAction(() => this.audioState = "recording");
+ setTimeout(this.updateRecordTime, 0);
+ this._recorder.start();
+ setTimeout(() => this._recorder && this.stopRecording(), 60 * 1000); // stop after an hour
}
specificContextMenu = (e: React.MouseEvent): void => {
@@ -167,8 +164,10 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
stopRecording = action(() => {
this._recorder.stop();
+ this._recorder = undefined;
this.dataDoc.duration = (new Date().getTime() - this._recordStart) / 1000;
- this._audioState = "recorded";
+ this.audioState = "paused";
+ this._stream?.getAudioTracks()[0].stop();
const ind = AudioBox.ActiveRecordings.indexOf(this.props.Document);
ind !== -1 && (AudioBox.ActiveRecordings.splice(ind, 1));
});
@@ -185,14 +184,25 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
e.stopPropagation();
}
onStop = (e: any) => {
- this.pause();
- this._ele!.currentTime = 0;
+ this.Document.playOnSelect = !this.Document.playOnSelect;
+ e.stopPropagation();
+ }
+ onFile = (e: any) => {
+ const newDoc = Docs.Create.TextDocument("", {
+ title: "", _chromeStatus: "disabled",
+ x: NumCast(this.props.Document.x), y: NumCast(this.props.Document.y) + NumCast(this.props.Document._height) + 10,
+ _width: NumCast(this.props.Document._width), _height: 3 * NumCast(this.props.Document._height)
+ });
+ Doc.GetProto(newDoc).recordingSource = this.dataDoc;
+ Doc.GetProto(newDoc).recordingStart = ComputedField.MakeFunction(`this.recordingSource["${this.props.fieldKey}-recordingStart"]`);
+ Doc.GetProto(newDoc).audioState = ComputedField.MakeFunction("this.recordingSource.audioState");
+ this.props.addDocument?.(newDoc);
e.stopPropagation();
}
setRef = (e: HTMLAudioElement | null) => {
- e && e.addEventListener("timeupdate", this.timecodeChanged);
- e && e.addEventListener("ended", this.pause);
+ e?.addEventListener("timeupdate", this.timecodeChanged);
+ e?.addEventListener("ended", this.pause);
this._ele = e;
}
@@ -212,45 +222,49 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
render() {
const interactive = this.active() ? "-interactive" : "";
- return <div className={`audiobox-container`} onContextMenu={this.specificContextMenu}
- onClick={!this.path ? this.recordClick : undefined}>
- <div className="audiobox-handle"></div>
+ return <div className={`audiobox-container`} onContextMenu={this.specificContextMenu} onClick={!this.path ? this.recordClick : undefined}>
{!this.path ?
- <button className={`audiobox-record${interactive}`} style={{ backgroundColor: this._audioState === "recording" ? "red" : "black" }}>
- {this._audioState === "recording" ? "STOP" : "RECORD"}
- </button> :
+ <div className="audiobox-buttons">
+ <div className="audiobox-dictation" onClick={this.onFile}>
+ <FontAwesomeIcon style={{ width: "30px", background: this.Document.playOnSelect ? "yellow" : "dimGray" }} icon="file-alt" size={this.props.PanelHeight() < 36 ? "1x" : "2x"} />
+ </div>
+ <button className={`audiobox-record${interactive}`} style={{ backgroundColor: this.audioState === "recording" ? "red" : "black" }}>
+ {this.audioState === "recording" ? "STOP" : "RECORD"}
+ </button>
+ </div> :
<div className="audiobox-controls">
<div className="audiobox-player" onClick={this.onPlay}>
- <div className="audiobox-playhead"> <FontAwesomeIcon style={{ width: "100%" }} icon={this._playing ? "pause" : "play"} size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /></div>
- <div className="audiobox-playhead" onClick={this.onStop}><FontAwesomeIcon style={{ width: "100%" }} icon="stop" size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /></div>
+ <div className="audiobox-playhead"> <FontAwesomeIcon style={{ width: "100%" }} icon={this.audioState === "paused" ? "play" : "pause"} size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /></div>
+ <div className="audiobox-playhead" onClick={this.onStop}><FontAwesomeIcon style={{ width: "100%", background: this.Document.playOnSelect ? "yellow" : "dimGray" }} icon="hand-point-left" size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /></div>
<div className="audiobox-timeline" onClick={e => e.stopPropagation()}
onPointerDown={e => {
if (e.button === 0 && !e.ctrlKey) {
const rect = (e.target as any).getBoundingClientRect();
+ const wasPaused = this.audioState === "paused";
this._ele!.currentTime = this.Document.currentTimecode = (e.clientX - rect.x) / rect.width * NumCast(this.dataDoc.duration);
- this.pause();
+ wasPaused && this.pause();
e.stopPropagation();
}
}} >
{DocListCast(this.dataDoc.links).map((l, i) => {
let la1 = l.anchor1 as Doc;
let la2 = l.anchor2 as Doc;
- let linkTime = NumCast(l.anchor2Timecode);
+ let linkTime = NumCast(l.anchor2_timecode);
if (Doc.AreProtosEqual(la1, this.dataDoc)) {
la1 = l.anchor2 as Doc;
la2 = l.anchor1 as Doc;
- linkTime = NumCast(l.anchor1Timecode);
+ linkTime = NumCast(l.anchor1_timecode);
}
return !linkTime ? (null) :
<div className={this.props.PanelHeight() < 32 ? "audiobox-marker-minicontainer" : "audiobox-marker-container"} key={l[Id]} style={{ left: `${linkTime / NumCast(this.dataDoc.duration, 1) * 100}%` }}>
<div className={this.props.PanelHeight() < 32 ? "audioBox-linker-mini" : "audioBox-linker"} key={"linker" + i}>
<DocumentView {...this.props} Document={l} layoutKey={Doc.LinkEndpoint(l, la2)}
+ ContainingCollectionDoc={this.props.Document}
parentActive={returnTrue} bringToFront={emptyFunction} zoomToScale={emptyFunction} getScale={returnOne}
backgroundColor={returnTransparent} />
</div>
<div key={i} className="audiobox-marker" onPointerEnter={() => Doc.linkFollowHighlight(la1)}
- onPointerDown={e => { if (e.button === 0 && !e.ctrlKey) { this.playFrom(linkTime); e.stopPropagation(); } }}
- onClick={e => { if (e.button === 0 && !e.ctrlKey) { this.pause(); e.stopPropagation(); } }} />
+ onPointerDown={e => { if (e.button === 0 && !e.ctrlKey) { const wasPaused = this.audioState === "paused"; this.playFrom(linkTime); wasPaused && this.pause(); e.stopPropagation(); } }} />
</div>;
})}
<div className="audiobox-current" style={{ left: `${NumCast(this.Document.currentTimecode) / NumCast(this.dataDoc.duration, 1) * 100}%` }} />
diff --git a/src/client/views/nodes/ButtonBox.tsx b/src/client/views/nodes/ButtonBox.tsx
index ee48b47b7..f1bf7cfcf 100644
--- a/src/client/views/nodes/ButtonBox.tsx
+++ b/src/client/views/nodes/ButtonBox.tsx
@@ -81,8 +81,8 @@ export class ButtonBox extends DocComponent<FieldViewProps, ButtonDocument>(Butt
<div className="buttonBox-outerDiv" ref={this.createDropTarget} onContextMenu={this.specificContextMenu}
style={{ boxShadow: this.Document.opacity === 0 ? undefined : StrCast(this.Document.boxShadow, "") }}>
<div className="buttonBox-mainButton" style={{
- background: this.Document.backgroundColor, color: this.Document.color || "black",
- fontSize: this.Document.fontSize, letterSpacing: this.Document.letterSpacing || "", textTransform: this.Document.textTransform || ""
+ background: this.Document.backgroundColor, color: this.Document.color || "inherit",
+ fontSize: this.Document.fontSize, letterSpacing: this.Document.letterSpacing || "", textTransform: (this.Document.textTransform as any) || ""
}} >
<div className="buttonBox-mainButtonCenter">
{(this.Document.text || this.Document.title)}
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 2183129cf..eaab4086c 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -2,7 +2,6 @@ import anime from "animejs";
import { computed, IReactionDisposer, observable, reaction, trace } from "mobx";
import { observer } from "mobx-react";
import { Doc, HeightSym, WidthSym } from "../../../new_fields/Doc";
-import { listSpec } from "../../../new_fields/Schema";
import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
import { Transform } from "../../util/Transform";
import { DocComponent } from "../DocComponent";
@@ -15,9 +14,12 @@ import { returnFalse } from "../../../Utils";
import { ContentFittingDocumentView } from "./ContentFittingDocumentView";
export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
- dataProvider?: (doc: Doc) => { x: number, y: number, width: number, height: number, z: number, transition?: string } | undefined;
+ dataProvider?: (doc: Doc) => { x: number, y: number, zIndex?: number, highlight?: boolean, width: number, height: number, z: number, transition?: string } | undefined;
x?: number;
y?: number;
+ z?: number;
+ zIndex?: number;
+ highlight?: boolean;
width?: number;
height?: number;
jitterRotation: number;
@@ -27,13 +29,13 @@ export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
@observer
export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps, PositionDocument>(PositionDocument) {
- _disposer: IReactionDisposer | undefined = undefined;
-
@observable _animPos: number[] | undefined = undefined;
get displayName() { return "CollectionFreeFormDocumentView(" + this.props.Document.title + ")"; } // this makes mobx trace() statements more descriptive
get transform() { return `scale(${this.props.ContentScaling()}) translate(${this.X}px, ${this.Y}px) rotate(${anime.random(-1, 1) * this.props.jitterRotation}deg)`; }
- get X() { return this._animPos !== undefined ? this._animPos[0] : this.renderScriptDim ? this.renderScriptDim.x : this.props.x !== undefined ? this.props.x : this.dataProvider ? this.dataProvider.x : (this.Document.x || 0); }
- get Y() { return this._animPos !== undefined ? this._animPos[1] : this.renderScriptDim ? this.renderScriptDim.y : this.props.y !== undefined ? this.props.y : this.dataProvider ? this.dataProvider.y : (this.Document.y || 0); }
+ get X() { return this.renderScriptDim ? this.renderScriptDim.x : this.props.x !== undefined ? this.props.x : this.dataProvider ? this.dataProvider.x : (this.Document.x || 0); }
+ get Y() { return this.renderScriptDim ? this.renderScriptDim.y : this.props.y !== undefined ? this.props.y : this.dataProvider ? this.dataProvider.y : (this.Document.y || 0); }
+ get ZInd() { return this.dataProvider ? this.dataProvider.zIndex : (this.Document.zIndex || 0); }
+ get Highlight() { return this.dataProvider?.highlight; }
get width() { return this.renderScriptDim ? this.renderScriptDim.width : this.props.width !== undefined ? this.props.width : this.props.dataProvider && this.dataProvider ? this.dataProvider.width : this.layoutDoc[WidthSym](); }
get height() {
const hgt = this.renderScriptDim ? this.renderScriptDim.height : this.props.height !== undefined ? this.props.height : this.props.dataProvider && this.dataProvider ? this.dataProvider.height : this.layoutDoc[HeightSym]();
@@ -58,25 +60,14 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
return undefined;
}
- componentWillUnmount() { this._disposer?.(); }
- componentDidMount() {
- this._disposer = reaction(() => Array.from(Cast(this.props.Document?.animateToPos, listSpec("number"), null) || []),
- target => this._animPos = !target || !target?.length ? undefined : target[2] ? [NumCast(this.layoutDoc.x), NumCast(this.layoutDoc.y)] :
- this.props.ScreenToLocalTransform().transformPoint(target[0], target[1]),
- { fireImmediately: true });
- }
-
- contentScaling = () => this.nativeWidth > 0 && !this.props.Document.ignoreAspect && !this.props.fitToBox ? this.width / this.nativeWidth : 1;
- clusterColorFunc = (doc: Doc) => this.clusterColor;
+ contentScaling = () => this.nativeWidth > 0 && !this.props.fitToBox ? this.width / this.nativeWidth : 1;
panelWidth = () => (this.dataProvider?.width || this.props.PanelWidth());
panelHeight = () => (this.dataProvider?.height || this.props.PanelHeight());
getTransform = (): Transform => this.props.ScreenToLocalTransform()
.translate(-this.X, -this.Y)
.scale(1 / this.contentScaling())
- @computed
- get clusterColor() { return this.props.backgroundColor(this.props.Document); }
-
+ focusDoc = (doc: Doc) => this.props.focus(doc, false);
render() {
TraceMobx();
return <div className="collectionFreeFormDocumentView-container"
@@ -84,22 +75,25 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
boxShadow:
this.layoutDoc.opacity === 0 ? undefined : // if it's not visible, then no shadow
this.layoutDoc.z ? `#9c9396 ${StrCast(this.layoutDoc.boxShadow, "10px 10px 0.9vw")}` : // if it's a floating doc, give it a big shadow
- this.clusterColor ? (`${this.clusterColor} ${StrCast(this.layoutDoc.boxShadow, `0vw 0vw ${(this.layoutDoc.isBackground ? 100 : 50) / this.props.ContentScaling()}px`)}`) : // if it's just in a cluster, make the shadown roughly match the cluster border extent
+ this.props.backgroundHalo?.() ? (`${this.props.backgroundColor?.(this.props.Document)} ${StrCast(this.layoutDoc.boxShadow, `0vw 0vw ${(this.layoutDoc.isBackground ? 100 : 50) / this.props.ContentScaling()}px`)}`) : // if it's just in a cluster, make the shadown roughly match the cluster border extent
this.layoutDoc.isBackground ? undefined : // if it's a background & has a cluster color, make the shadow spread really big
StrCast(this.layoutDoc.boxShadow, ""),
borderRadius: StrCast(Doc.Layout(this.layoutDoc).borderRounding),
+ outline: this.Highlight ? "orange solid 2px" : "",
transform: this.transform,
- transition: this.Document.isAnimating ? ".5s ease-in" : this.props.transition ? this.props.transition : this.dataProvider ? this.dataProvider.transition : StrCast(this.layoutDoc.transition),
+ transition: this.props.transition ? this.props.transition : this.dataProvider ? this.dataProvider.transition : StrCast(this.layoutDoc.transition),
width: this.width,
height: this.height,
- zIndex: this.Document.zIndex || 0,
+ zIndex: this.ZInd,
+ display: this.ZInd === -99 ? "none" : undefined,
+ pointerEvents: this.props.Document.isBackground ? "none" : undefined
}} >
{!this.props.fitToBox ? <DocumentView {...this.props}
dragDivName={"collectionFreeFormDocumentView-container"}
ContentScaling={this.contentScaling}
ScreenToLocalTransform={this.getTransform}
- backgroundColor={this.clusterColorFunc}
+ backgroundColor={this.props.backgroundColor}
PanelWidth={this.panelWidth}
PanelHeight={this.panelHeight}
/> : <ContentFittingDocumentView {...this.props}
@@ -107,7 +101,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
DataDocument={this.props.DataDoc}
getTransform={this.getTransform}
active={returnFalse}
- focus={(doc: Doc) => this.props.focus(doc, false)}
+ focus={this.focusDoc}
PanelWidth={this.panelWidth}
PanelHeight={this.panelHeight}
/>}
diff --git a/src/client/views/nodes/ColorBox.tsx b/src/client/views/nodes/ColorBox.tsx
index 40674b034..d34d63d01 100644
--- a/src/client/views/nodes/ColorBox.tsx
+++ b/src/client/views/nodes/ColorBox.tsx
@@ -1,16 +1,15 @@
import React = require("react");
import { observer } from "mobx-react";
import { SketchPicker } from 'react-color';
-import { FieldView, FieldViewProps } from './FieldView';
-import "./ColorBox.scss";
-import { InkingControl } from "../InkingControl";
-import { DocExtendableComponent } from "../DocComponent";
+import { documentSchema } from "../../../new_fields/documentSchemas";
import { makeInterface } from "../../../new_fields/Schema";
-import { reaction, observable, action, IReactionDisposer } from "mobx";
-import { SelectionManager } from "../../util/SelectionManager";
import { StrCast } from "../../../new_fields/Types";
import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
-import { documentSchema } from "../../../new_fields/documentSchemas";
+import { SelectionManager } from "../../util/SelectionManager";
+import { DocExtendableComponent } from "../DocComponent";
+import { InkingControl } from "../InkingControl";
+import "./ColorBox.scss";
+import { FieldView, FieldViewProps } from './FieldView';
type ColorDocument = makeInterface<[typeof documentSchema]>;
const ColorDocument = makeInterface(documentSchema);
@@ -19,29 +18,15 @@ const ColorDocument = makeInterface(documentSchema);
export class ColorBox extends DocExtendableComponent<FieldViewProps, ColorDocument>(ColorDocument) {
public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ColorBox, fieldKey); }
- _selectedDisposer: IReactionDisposer | undefined;
- _penDisposer: IReactionDisposer | undefined;
- @observable _startupColor = "black";
-
- componentDidMount() {
- this._selectedDisposer = reaction(() => SelectionManager.SelectedDocuments(),
- action(() => this._startupColor = SelectionManager.SelectedDocuments().length ? StrCast(SelectionManager.SelectedDocuments()[0].Document.backgroundColor, "black") : "black"),
- { fireImmediately: true });
- this._penDisposer = reaction(() => CurrentUserUtils.ActivePen,
- action(() => this._startupColor = CurrentUserUtils.ActivePen ? StrCast(CurrentUserUtils.ActivePen.backgroundColor, "black") : "black"),
- { fireImmediately: true });
- }
- componentWillUnmount() {
- this._penDisposer && this._penDisposer();
- this._selectedDisposer && this._selectedDisposer();
- }
-
render() {
+ const selDoc = SelectionManager.SelectedDocuments()?.[0]?.Document;
return <div className={`colorBox-container${this.active() ? "-interactive" : ""}`}
onPointerDown={e => e.button === 0 && !e.ctrlKey && e.stopPropagation()}
style={{ transformOrigin: "top left", transform: `scale(${this.props.ContentScaling()})`, width: `${100 / this.props.ContentScaling()}%`, height: `${100 / this.props.ContentScaling()}%` }} >
- <SketchPicker color={this._startupColor} onChange={InkingControl.Instance.switchColor} />
+ <SketchPicker onChange={InkingControl.Instance.switchColor}
+ color={StrCast(CurrentUserUtils.ActivePen ? CurrentUserUtils.ActivePen.backgroundColor : undefined,
+ StrCast(selDoc?._backgroundColor, StrCast(selDoc?.backgroundColor, "black")))} />
</div>;
}
} \ No newline at end of file
diff --git a/src/client/views/nodes/ContentFittingDocumentView.scss b/src/client/views/nodes/ContentFittingDocumentView.scss
index 2801af441..eb2d93b9a 100644
--- a/src/client/views/nodes/ContentFittingDocumentView.scss
+++ b/src/client/views/nodes/ContentFittingDocumentView.scss
@@ -19,6 +19,6 @@
.documentView-node:first-child {
position: relative;
- background: $light-color;
+ background: "#B59B66"; //$light-color;
}
} \ No newline at end of file
diff --git a/src/client/views/nodes/ContentFittingDocumentView.tsx b/src/client/views/nodes/ContentFittingDocumentView.tsx
index 51c8e00da..9494a4bc4 100644
--- a/src/client/views/nodes/ContentFittingDocumentView.tsx
+++ b/src/client/views/nodes/ContentFittingDocumentView.tsx
@@ -1,40 +1,43 @@
import React = require("react");
-import { action, computed } from "mobx";
+import { computed } from "mobx";
import { observer } from "mobx-react";
import "react-table/react-table.css";
-import { Doc } from "../../../new_fields/Doc";
-import { ComputedField, ScriptField } from "../../../new_fields/ScriptField";
+import { Doc, Opt } from "../../../new_fields/Doc";
+import { ScriptField } from "../../../new_fields/ScriptField";
import { NumCast, StrCast } from "../../../new_fields/Types";
-import { emptyFunction, returnEmptyString, returnOne } from "../../../Utils";
-import { DragManager } from "../../util/DragManager";
+import { TraceMobx } from "../../../new_fields/util";
+import { emptyFunction, returnOne } from "../../../Utils";
import { Transform } from "../../util/Transform";
-import { undoBatch } from "../../util/UndoManager";
+import { CollectionView } from "../collections/CollectionView";
import '../DocumentDecorations.scss';
import { DocumentView } from "../nodes/DocumentView";
import "./ContentFittingDocumentView.scss";
-import { CollectionView } from "../collections/CollectionView";
-import { TraceMobx } from "../../../new_fields/util";
+import { dropActionType } from "../../util/DragManager";
interface ContentFittingDocumentViewProps {
Document?: Doc;
DataDocument?: Doc;
+ LayoutDoc?: () => Opt<Doc>;
LibraryPath: Doc[];
childDocs?: Doc[];
renderDepth: number;
fitToBox?: boolean;
+ layoutKey?: string;
+ dropAction?: dropActionType;
PanelWidth: () => number;
PanelHeight: () => number;
focus?: (doc: Doc) => void;
CollectionView?: CollectionView;
CollectionDoc?: Doc;
onClick?: ScriptField;
+ backgroundColor?: (doc: Doc) => string | undefined;
getTransform: () => Transform;
addDocument?: (document: Doc) => boolean;
moveDocument?: (document: Doc, target: Doc | undefined, addDoc: ((doc: Doc) => boolean)) => boolean;
removeDocument?: (document: Doc) => boolean;
active: (outsideReaction: boolean) => boolean;
whenActiveChanged: (isActive: boolean) => void;
- addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean;
+ addDocTab: (document: Doc, where: string) => boolean;
pinToPres: (document: Doc) => void;
dontRegisterView?: boolean;
}
@@ -42,33 +45,24 @@ interface ContentFittingDocumentViewProps {
@observer
export class ContentFittingDocumentView extends React.Component<ContentFittingDocumentViewProps>{
public get displayName() { return "DocumentView(" + this.props.Document?.title + ")"; } // this makes mobx trace() statements more descriptive
- private get layoutDoc() { return this.props.Document && Doc.Layout(this.props.Document); }
+ private get layoutDoc() { return this.props.Document && (this.props.LayoutDoc?.() || Doc.Layout(this.props.Document)); }
private get nativeWidth() { return NumCast(this.layoutDoc?._nativeWidth, this.props.PanelWidth()); }
private get nativeHeight() { return NumCast(this.layoutDoc?._nativeHeight, this.props.PanelHeight()); }
- private contentScaling = () => {
+ @computed get scaling() {
const wscale = this.props.PanelWidth() / (this.nativeWidth || this.props.PanelWidth() || 1);
if (wscale * this.nativeHeight > this.props.PanelHeight()) {
return (this.props.PanelHeight() / (this.nativeHeight || this.props.PanelHeight() || 1)) || 1;
}
return wscale || 1;
}
+ private contentScaling = () => this.scaling;
+
+ private PanelWidth = () => this.panelWidth;
+ private PanelHeight = () => this.panelHeight;
+
+ @computed get panelWidth() { return this.nativeWidth && (!this.props.Document || !this.props.Document._fitWidth) ? this.nativeWidth * this.contentScaling() : this.props.PanelWidth(); }
+ @computed get panelHeight() { return this.nativeHeight && (!this.props.Document || !this.props.Document._fitWidth) ? this.nativeHeight * this.contentScaling() : this.props.PanelHeight(); }
- @undoBatch
- @action
- drop = (e: Event, de: DragManager.DropEvent) => {
- const docDragData = de.complete.docDragData;
- if (docDragData) {
- this.props.childDocs && this.props.childDocs.map(otherdoc => {
- const target = Doc.GetProto(otherdoc);
- target.layout = ComputedField.MakeFunction("this.image_data[0]");
- target.layout_custom = Doc.MakeDelegate(docDragData.draggedDocuments[0]);
- });
- e.stopPropagation();
- }
- return true;
- }
- private PanelWidth = () => this.nativeWidth && (!this.props.Document || !this.props.Document._fitWidth) ? this.nativeWidth * this.contentScaling() : this.props.PanelWidth();
- private PanelHeight = () => this.nativeHeight && (!this.props.Document || !this.props.Document._fitWidth) ? this.nativeHeight * this.contentScaling() : this.props.PanelHeight();
private getTransform = () => this.props.getTransform().translate(-this.centeringOffset, -this.centeringYOffset).scale(1 / this.contentScaling());
private get centeringOffset() { return this.nativeWidth && (!this.props.Document || !this.props.Document._fitWidth) ? (this.props.PanelWidth() - this.nativeWidth * this.contentScaling()) / 2 : 0; }
private get centeringYOffset() { return Math.abs(this.centeringOffset) < 0.001 ? (this.props.PanelHeight() - this.nativeHeight * this.contentScaling()) / 2 : 0; }
@@ -92,9 +86,13 @@ export class ContentFittingDocumentView extends React.Component<ContentFittingDo
<DocumentView {...this.props}
Document={this.props.Document}
DataDoc={this.props.DataDocument}
+ LayoutDoc={this.props.LayoutDoc}
LibraryPath={this.props.LibraryPath}
fitToBox={this.props.fitToBox}
+ layoutKey={this.props.layoutKey}
+ dropAction={this.props.dropAction}
onClick={this.props.onClick}
+ backgroundColor={this.props.backgroundColor}
addDocument={this.props.addDocument}
removeDocument={this.props.removeDocument}
moveDocument={this.props.moveDocument}
@@ -110,7 +108,6 @@ export class ContentFittingDocumentView extends React.Component<ContentFittingDo
PanelWidth={this.PanelWidth}
PanelHeight={this.PanelHeight}
focus={this.props.focus || emptyFunction}
- backgroundColor={returnEmptyString}
bringToFront={emptyFunction}
dontRegisterView={this.props.dontRegisterView}
zoomToScale={emptyFunction}
diff --git a/src/client/views/nodes/DocuLinkBox.scss b/src/client/views/nodes/DocuLinkBox.scss
index 57c1a66e0..f2c203548 100644
--- a/src/client/views/nodes/DocuLinkBox.scss
+++ b/src/client/views/nodes/DocuLinkBox.scss
@@ -1,8 +1,29 @@
-.docuLinkBox-cont {
+.docuLinkBox-cont, .docuLinkBox-cont-small {
cursor: default;
position: absolute;
- width: 25px;
- height: 25px;
+ width: 15;
+ height: 15;
border-radius: 20px;
pointer-events: all;
+ user-select: none;
+
+ .docuLinkBox-linkCloser {
+ position: absolute;
+ width: 18;
+ height: 18;
+ background: rgb(219, 21, 21);
+ top: -1px;
+ left: -1px;
+ border-radius: 5px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ padding-left: 2px;
+ padding-top: 1px;
+ }
+}
+
+.docuLinkBox-cont-small {
+ width:5px;
+ height:5px;
} \ No newline at end of file
diff --git a/src/client/views/nodes/DocuLinkBox.tsx b/src/client/views/nodes/DocuLinkBox.tsx
index a4a9a62aa..81cf90f92 100644
--- a/src/client/views/nodes/DocuLinkBox.tsx
+++ b/src/client/views/nodes/DocuLinkBox.tsx
@@ -1,8 +1,9 @@
import { action, observable } from "mobx";
import { observer } from "mobx-react";
-import { Doc, WidthSym, HeightSym } from "../../../new_fields/Doc";
+import { Doc, DocListCast } from "../../../new_fields/Doc";
+import { documentSchema } from "../../../new_fields/documentSchemas";
import { makeInterface } from "../../../new_fields/Schema";
-import { NumCast, StrCast, Cast } from "../../../new_fields/Types";
+import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
import { Utils } from '../../../Utils';
import { DocumentManager } from "../../util/DocumentManager";
import { DragManager } from "../../util/DragManager";
@@ -10,9 +11,14 @@ import { DocComponent } from "../DocComponent";
import "./DocuLinkBox.scss";
import { FieldView, FieldViewProps } from "./FieldView";
import React = require("react");
-import { DocumentType } from "../../documents/DocumentTypes";
-import { documentSchema } from "../../../new_fields/documentSchemas";
-import { Id } from "../../../new_fields/FieldSymbols";
+import { ContextMenuProps } from "../ContextMenuItem";
+import { ContextMenu } from "../ContextMenu";
+import { LinkEditor } from "../linking/LinkEditor";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { SelectionManager } from "../../util/SelectionManager";
+const higflyout = require("@hig/flyout");
+export const { anchorPoints } = higflyout;
+export const Flyout = higflyout.default;
type DocLinkSchema = makeInterface<[typeof documentSchema]>;
const DocLinkDocument = makeInterface(documentSchema);
@@ -20,16 +26,22 @@ const DocLinkDocument = makeInterface(documentSchema);
@observer
export class DocuLinkBox extends DocComponent<FieldViewProps, DocLinkSchema>(DocLinkDocument) {
public static LayoutString(fieldKey: string) { return FieldView.LayoutString(DocuLinkBox, fieldKey); }
- _downx = 0;
- _downy = 0;
+ _doubleTap = false;
+ _lastTap: number = 0;
+ _ref = React.createRef<HTMLDivElement>();
+ _downX = 0;
+ _downY = 0;
+ _isOpen = false;
+ _timeout: NodeJS.Timeout | undefined;
@observable _x = 0;
@observable _y = 0;
@observable _selected = false;
- _ref = React.createRef<HTMLDivElement>();
+ @observable _editing = false;
+ @observable _forceOpen = false;
onPointerDown = (e: React.PointerEvent) => {
- this._downx = e.clientX;
- this._downy = e.clientY;
+ this._downX = e.clientX;
+ this._downY = e.clientY;
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
document.addEventListener("pointermove", this.onPointerMove);
@@ -38,13 +50,17 @@ export class DocuLinkBox extends DocComponent<FieldViewProps, DocLinkSchema>(Doc
}
onPointerMove = action((e: PointerEvent) => {
const cdiv = this._ref && this._ref.current && this._ref.current.parentElement;
- if (cdiv && (Math.abs(e.clientX - this._downx) > 5 || Math.abs(e.clientY - this._downy) > 5)) {
+ if (!this._isOpen && cdiv && (Math.abs(e.clientX - this._downX) > 5 || Math.abs(e.clientY - this._downY) > 5)) {
const bounds = cdiv.getBoundingClientRect();
const pt = Utils.getNearestPointInPerimeter(bounds.left, bounds.top, bounds.width, bounds.height, e.clientX, e.clientY);
const separation = Math.sqrt((pt[0] - e.clientX) * (pt[0] - e.clientX) + (pt[1] - e.clientY) * (pt[1] - e.clientY));
- const dragdist = Math.sqrt((pt[0] - this._downx) * (pt[0] - this._downx) + (pt[1] - this._downy) * (pt[1] - this._downy));
+ const dragdist = Math.sqrt((pt[0] - this._downX) * (pt[0] - this._downX) + (pt[1] - this._downY) * (pt[1] - this._downY));
if (separation > 100) {
- DragManager.StartLinkTargetsDrag(this._ref.current!, pt[0], pt[1], Cast(this.props.Document[this.props.fieldKey], Doc) as Doc, [this.props.Document]); // Containging collection is the document, not a collection... hack.
+ //DragManager.StartLinkTargetsDrag(this._ref.current!, pt[0], pt[1], Cast(this.props.Document[this.props.fieldKey], Doc) as Doc, [this.props.Document]); // Containging collection is the document, not a collection... hack.
+ const dragData = new DragManager.DocumentDragData([this.props.Document]);
+ dragData.dropAction = "alias";
+ dragData.removeDropProperties = ["anchor1_x", "anchor1_y", "anchor2_x", "anchor2_y"];
+ DragManager.StartDocumentDrag([this._ref.current!], dragData, this._downX, this._downY);
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
} else if (dragdist > separation) {
@@ -56,32 +72,90 @@ export class DocuLinkBox extends DocComponent<FieldViewProps, DocLinkSchema>(Doc
onPointerUp = (e: PointerEvent) => {
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
- if (Math.abs(e.clientX - this._downx) < 3 && Math.abs(e.clientY - this._downy) < 3 && (e.button === 2 || e.ctrlKey || !this.props.Document.isButton)) {
+ if (Math.abs(e.clientX - this._downX) < 3 && Math.abs(e.clientY - this._downY) < 3 && (e.button === 2 || e.ctrlKey || !this.props.Document.isButton)) {
this.props.select(false);
}
+ this._doubleTap = (Date.now() - this._lastTap < 300 && e.button === 0 && Math.abs(e.clientX - this._downX) < 2 && Math.abs(e.clientY - this._downY) < 2);
+ this._lastTap = Date.now();
}
+
+ @action
onClick = (e: React.MouseEvent) => {
- if (!this.props.Document.onClick) {
- if (Math.abs(e.clientX - this._downx) < 3 && Math.abs(e.clientY - this._downy) < 3 && (e.button !== 2 && !e.ctrlKey && this.props.Document.isButton)) {
- DocumentManager.Instance.FollowLink(this.props.Document, this.props.Document[this.props.fieldKey] as Doc, document => this.props.addDocTab(document, undefined, "inTab"), false);
+ if (!this._doubleTap) {
+ this._editing = true;
+ this.props.ContainingCollectionDoc && this.props.bringToFront(this.props.ContainingCollectionDoc, false);
+ const {clientX, clientY} = e;
+ if (!this.props.Document.onClick && !this._isOpen) {
+ this._timeout = setTimeout(action(() => {
+ if (Math.abs(clientX - this._downX) < 3 && Math.abs(clientY - this._downY) < 3 && (e.button !== 2 && !e.ctrlKey && this.props.Document.isButton)) {
+ DocumentManager.Instance.FollowLink(this.props.Document, this.props.ContainingCollectionDoc as Doc, document => this.props.addDocTab(document, StrCast(this.props.Document.linkOpenLocation, "inTab")), false);
+ }
+ this._editing = false;
+ }), 300 - (Date.now() - this._lastTap));
}
- e.stopPropagation();
+ } else {
+ this._timeout && clearTimeout(this._timeout);
+ this._timeout = undefined;
}
+ e.stopPropagation();
+ }
+
+ openLinkDocOnRight = (e: React.MouseEvent) => {
+ this.props.addDocTab(this.props.Document, "onRight");
+ }
+ openLinkTargetOnRight = (e: React.MouseEvent) => {
+ const alias = Doc.MakeAlias(Cast(this.props.Document[this.props.fieldKey], Doc, null));
+ alias.isButton = undefined;
+ alias.isBackground = undefined;
+ alias.layoutKey = "layout";
+ this.props.addDocTab(alias, "onRight");
+ }
+ @action
+ openLinkEditor = action((e: React.MouseEvent) => {
+ SelectionManager.DeselectAll();
+ this._editing = this._forceOpen = true;
+ });
+
+ specificContextMenu = (e: React.MouseEvent): void => {
+ const funcs: ContextMenuProps[] = [];
+ funcs.push({ description: "Open Link Target on Right", event: () => this.openLinkTargetOnRight(e), icon: "eye" });
+ funcs.push({ description: "Open Link on Right", event: () => this.openLinkDocOnRight(e), icon: "eye" });
+ funcs.push({ description: "Open Link Editor", event: () => this.openLinkEditor(e), icon: "eye" });
+
+ ContextMenu.Instance.addItem({ description: "Link Funcs...", subitems: funcs, icon: "asterisk" });
}
render() {
- const x = NumCast(this.props.Document[this.props.fieldKey + "_x"], 100);
- const y = NumCast(this.props.Document[this.props.fieldKey + "_y"], 100);
+ const x = this.props.PanelWidth() > 1 ? NumCast(this.props.Document[this.props.fieldKey + "_x"], 100) : 0;
+ const y = this.props.PanelWidth() > 1 ? NumCast(this.props.Document[this.props.fieldKey + "_y"], 100) : 0;
const c = StrCast(this.props.Document.backgroundColor, "lightblue");
const anchor = this.props.fieldKey === "anchor1" ? "anchor2" : "anchor1";
const anchorScale = (x === 0 || x === 100 || y === 0 || y === 100) ? 1 : .15;
const timecode = this.props.Document[anchor + "Timecode"];
const targetTitle = StrCast((this.props.Document[anchor]! as Doc).title) + (timecode !== undefined ? ":" + timecode : "");
- return <div className="docuLinkBox-cont" onPointerDown={this.onPointerDown} onClick={this.onClick} title={targetTitle}
+ const flyout = (
+ <div className="docuLinkBox-flyout" title=" " onPointerOver={() => Doc.UnBrushDoc(this.props.Document)}>
+ <LinkEditor sourceDoc={Cast(this.props.Document[this.props.fieldKey], Doc, null)} hideback={true} linkDoc={this.props.Document} showLinks={action(() => { })} />
+ {!this._forceOpen ? (null) : <div className="docuLinkBox-linkCloser" onPointerDown={action(() => this._isOpen = this._editing = this._forceOpen = false)}>
+ <FontAwesomeIcon color="dimGray" icon={"times"} size={"sm"} />
+ </div>}
+ </div>
+ );
+ const small = this.props.PanelWidth() <= 1;
+ return <div className={`docuLinkBox-cont${small ? "-small" : ""}`} onPointerDown={this.onPointerDown} onClick={this.onClick} title={targetTitle} onContextMenu={this.specificContextMenu}
ref={this._ref} style={{
- background: c, left: `calc(${x}% - 12.5px)`, top: `calc(${y}% - 12.5px)`,
+ background: c,
+ left: !small ? `calc(${x}% - 7.5px)` : undefined,
+ top: !small ? `calc(${y}% - 7.5px)` : undefined,
transform: `scale(${anchorScale / this.props.ContentScaling()})`
- }} />;
+ }} >
+ {!this._editing && !this._forceOpen ? (null) :
+ <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={flyout} open={this._forceOpen ? true : undefined} onOpen={() => this._isOpen = true} onClose={action(() => this._isOpen = this._forceOpen = this._editing = false)}>
+ <span className="parentDocumentSelector-button" >
+ <FontAwesomeIcon icon={"eye"} size={"lg"} />
+ </span>
+ </Flyout>}
+ </div>;
}
}
diff --git a/src/client/views/nodes/DocumentBox.scss b/src/client/views/nodes/DocumentBox.scss
index b7d06b364..ce21391ce 100644
--- a/src/client/views/nodes/DocumentBox.scss
+++ b/src/client/views/nodes/DocumentBox.scss
@@ -3,13 +3,12 @@
height: 100%;
pointer-events: all;
background: gray;
- border: #00000021 solid 15px;
- border-top: #0000005e inset 15px;
- border-bottom: #0000005e outset 15px;
.documentBox-lock {
margin: auto;
color: white;
- margin-top: -15px;
+ position: absolute;
+ }
+ .contentFittingDocumentView {
position: absolute;
}
} \ No newline at end of file
diff --git a/src/client/views/nodes/DocumentBox.tsx b/src/client/views/nodes/DocumentBox.tsx
index 6b7b652c6..debe104d7 100644
--- a/src/client/views/nodes/DocumentBox.tsx
+++ b/src/client/views/nodes/DocumentBox.tsx
@@ -1,111 +1,159 @@
-import { IReactionDisposer, reaction } from "mobx";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { IReactionDisposer, reaction, computed } from "mobx";
import { observer } from "mobx-react";
import { Doc, Field } from "../../../new_fields/Doc";
import { documentSchema } from "../../../new_fields/documentSchemas";
-import { List } from "../../../new_fields/List";
import { makeInterface } from "../../../new_fields/Schema";
import { ComputedField } from "../../../new_fields/ScriptField";
-import { Cast, StrCast, BoolCast } from "../../../new_fields/Types";
-import { emptyFunction, emptyPath } from "../../../Utils";
+import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
+import { emptyPath } from "../../../Utils";
import { ContextMenu } from "../ContextMenu";
import { ContextMenuProps } from "../ContextMenuItem";
-import { DocComponent } from "../DocComponent";
+import { DocAnnotatableComponent } from "../DocComponent";
import { ContentFittingDocumentView } from "./ContentFittingDocumentView";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import "./DocumentBox.scss";
import { FieldView, FieldViewProps } from "./FieldView";
import React = require("react");
+import { TraceMobx } from "../../../new_fields/util";
+import { DocumentView } from "./DocumentView";
+import { Docs } from "../../documents/Documents";
type DocBoxSchema = makeInterface<[typeof documentSchema]>;
const DocBoxDocument = makeInterface(documentSchema);
@observer
-export class DocumentBox extends DocComponent<FieldViewProps, DocBoxSchema>(DocBoxDocument) {
+export class DocumentBox extends DocAnnotatableComponent<FieldViewProps, DocBoxSchema>(DocBoxDocument) {
public static LayoutString(fieldKey: string) { return FieldView.LayoutString(DocumentBox, fieldKey); }
_prevSelectionDisposer: IReactionDisposer | undefined;
_selections: Doc[] = [];
_curSelection = -1;
componentDidMount() {
- this._prevSelectionDisposer = reaction(() => Cast(this.props.Document[this.props.fieldKey], Doc) as Doc, (data) => {
- if (data && !this._selections.includes(data)) {
- this._selections.length = ++this._curSelection;
+ this._prevSelectionDisposer = reaction(() => this.contentDoc[this.props.fieldKey], (data) => {
+ if (data instanceof Doc && !this.isSelectionLocked()) {
+ this._selections.indexOf(data) !== -1 && this._selections.splice(this._selections.indexOf(data), 1);
this._selections.push(data);
+ this._curSelection = this._selections.length - 1;
}
});
}
componentWillUnmount() {
- this._prevSelectionDisposer && this._prevSelectionDisposer();
+ this._prevSelectionDisposer?.();
}
specificContextMenu = (e: React.MouseEvent): void => {
const funcs: ContextMenuProps[] = [];
funcs.push({ description: (this.isSelectionLocked() ? "Show" : "Lock") + " Selection", event: () => this.toggleLockSelection, icon: "expand-arrows-alt" });
+ funcs.push({ description: (this.props.Document.excludeCollections ? "Include" : "Exclude") + " Collections", event: () => Doc.GetProto(this.props.Document).excludeCollections = !this.props.Document.excludeCollections, icon: "expand-arrows-alt" });
funcs.push({ description: `${this.props.Document.forceActive ? "Select" : "Force"} Contents Active`, event: () => this.props.Document.forceActive = !this.props.Document.forceActive, icon: "project-diagram" });
ContextMenu.Instance.addItem({ description: "DocumentBox Funcs...", subitems: funcs, icon: "asterisk" });
}
+ @computed get contentDoc() {
+ return (this.props.Document.isTemplateDoc || this.props.Document.isTemplateForField ? this.props.Document : Doc.GetProto(this.props.Document));
+ }
lockSelection = () => {
- Doc.GetProto(this.props.Document)[this.props.fieldKey] = this.props.Document[this.props.fieldKey];
+ this.contentDoc[this.props.fieldKey] = this.props.Document[this.props.fieldKey];
}
showSelection = () => {
- Doc.GetProto(this.props.Document)[this.props.fieldKey] = ComputedField.MakeFunction("selectedDocs(this,true,[_last_])?.[0]");
+ this.contentDoc[this.props.fieldKey] = ComputedField.MakeFunction(`selectedDocs(this,this.excludeCollections,[_last_])?.[0]`);
}
isSelectionLocked = () => {
- const kvpstring = Field.toKeyValueString(this.props.Document, this.props.fieldKey);
- return !(kvpstring.startsWith("=") || kvpstring.startsWith(":="));
+ const kvpstring = Field.toKeyValueString(this.contentDoc, this.props.fieldKey);
+ return !kvpstring || kvpstring.includes("DOC");
}
toggleLockSelection = () => {
!this.isSelectionLocked() ? this.lockSelection() : this.showSelection();
+ return true;
}
prevSelection = () => {
+ this.lockSelection();
if (this._curSelection > 0) {
- Doc.UserDoc().SelectedDocs = new List([this._selections[--this._curSelection]]);
+ this.contentDoc[this.props.fieldKey] = this._selections[--this._curSelection];
+ return true;
}
}
nextSelection = () => {
if (this._curSelection < this._selections.length - 1 && this._selections.length) {
- Doc.UserDoc().SelectedDocs = new List([this._selections[++this._curSelection]]);
+ this.contentDoc[this.props.fieldKey] = this._selections[++this._curSelection];
+ return true;
}
}
onPointerDown = (e: React.PointerEvent) => {
+ if (this.active() && e.button === 0 && !e.ctrlKey) {
+ e.stopPropagation();
+ }
}
+ onLockClick = (e: React.MouseEvent) => {
+ this.toggleLockSelection();
+ (e.nativeEvent as any).formattedHandled = true;
+ e.stopPropagation();
+ }
+ get xPad() { return NumCast(this.props.Document._xPadding); }
+ get yPad() { return NumCast(this.props.Document._yPadding); }
onClick = (e: React.MouseEvent) => {
- if (this._contRef.current!.getBoundingClientRect().top + 15 > e.clientY) this.toggleLockSelection();
+ let hitWidget: boolean | undefined = false;
+ if (this._contRef.current!.getBoundingClientRect().top + this.yPad > e.clientY) hitWidget = (() => { this.props.select(false); return true; })();
+ else if (this._contRef.current!.getBoundingClientRect().bottom - this.yPad < e.clientY) hitWidget = (() => { this.props.select(false); return true; })();
else {
- if (this._contRef.current!.getBoundingClientRect().left + 15 > e.clientX) this.prevSelection();
- if (this._contRef.current!.getBoundingClientRect().right - 15 < e.clientX) this.nextSelection();
+ if (this._contRef.current!.getBoundingClientRect().left + this.xPad > e.clientX) hitWidget = this.prevSelection();
+ if (this._contRef.current!.getBoundingClientRect().right - this.xPad < e.clientX) hitWidget = this.nextSelection();
+ }
+ if (hitWidget) {
+ (e.nativeEvent as any).formattedHandled = true;
+ e.stopPropagation();
}
}
_contRef = React.createRef<HTMLDivElement>();
- pwidth = () => this.props.PanelWidth() - 30;
- pheight = () => this.props.PanelHeight() - 30;
- getTransform = () => this.props.ScreenToLocalTransform().translate(-15, -15);
+ pwidth = () => this.props.PanelWidth() - 2 * this.xPad;
+ pheight = () => this.props.PanelHeight() - 2 * this.yPad;
+ getTransform = () => this.props.ScreenToLocalTransform().translate(-this.xPad, -this.yPad);
+ get renderContents() {
+ const containedDoc = Cast(this.contentDoc[this.props.fieldKey], Doc, null);
+ const childTemplateName = StrCast(this.props.Document.childTemplateName);
+ if (containedDoc && childTemplateName && !containedDoc["layout_" + childTemplateName]) {
+ setTimeout(() => {
+ DocumentView.createCustomView(containedDoc, Docs.Create.StackingDocument, childTemplateName);
+ Doc.expandTemplateLayout(Cast(containedDoc["layout_" + childTemplateName], Doc, null)!, containedDoc, undefined);
+ }, 0);
+ }
+ const contents = !(containedDoc instanceof Doc) ? (null) : <ContentFittingDocumentView
+ Document={containedDoc}
+ DataDocument={undefined}
+ LibraryPath={emptyPath}
+ CollectionView={this as any} // bcz: hack! need to pass a prop that can be used to select the container (ie, 'this') when the up selector in document decorations is clicked. currently, the up selector allows only a containing collection to be selected
+ fitToBox={this.props.fitToBox}
+ layoutKey={"layout_" + childTemplateName}
+ addDocument={this.props.addDocument}
+ moveDocument={this.props.moveDocument}
+ removeDocument={this.props.removeDocument}
+ addDocTab={this.props.addDocTab}
+ pinToPres={this.props.pinToPres}
+ getTransform={this.getTransform}
+ renderDepth={this.props.renderDepth + 1}
+ PanelWidth={this.pwidth}
+ PanelHeight={this.pheight}
+ focus={this.props.focus}
+ active={this.props.active}
+ dontRegisterView={!this.isSelectionLocked()}
+ whenActiveChanged={this.props.whenActiveChanged}
+ />;
+ return contents;
+ }
render() {
- const containedDoc = this.props.Document[this.props.fieldKey] as Doc;
+ TraceMobx();
return <div className="documentBox-container" ref={this._contRef}
onContextMenu={this.specificContextMenu}
onPointerDown={this.onPointerDown} onClick={this.onClick}
- style={{ background: StrCast(this.props.Document.backgroundColor) }}>
- <div className="documentBox-lock">
+ style={{
+ background: StrCast(this.props.Document.backgroundColor),
+ border: `#00000021 solid ${this.xPad}px`,
+ borderTop: `#0000005e solid ${this.yPad}px`,
+ borderBottom: `#0000005e solid ${this.yPad}px`,
+ }}>
+ {this.renderContents}
+ <div className="documentBox-lock" onClick={this.onLockClick}
+ style={{ marginTop: - this.yPad }}>
<FontAwesomeIcon icon={this.isSelectionLocked() ? "lock" : "unlock"} size="sm" />
</div>
- {!(containedDoc instanceof Doc) ? (null) : <ContentFittingDocumentView
- Document={containedDoc}
- DataDocument={undefined}
- LibraryPath={emptyPath}
- fitToBox={this.props.fitToBox}
- addDocument={this.props.addDocument}
- moveDocument={this.props.moveDocument}
- removeDocument={this.props.removeDocument}
- addDocTab={this.props.addDocTab}
- pinToPres={this.props.pinToPres}
- getTransform={this.getTransform}
- renderDepth={this.props.Document.forceActive ? 0 : this.props.renderDepth + 1} // bcz: really need to have an 'alwaysSelected' prop that's not conflated with renderDepth
- PanelWidth={this.pwidth}
- PanelHeight={this.pheight}
- focus={this.props.focus}
- active={this.props.active}
- whenActiveChanged={this.props.whenActiveChanged}
- />}
- </div>;
+ </div >;
}
}
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index 3ef8126bc..fc61487e5 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -1,7 +1,6 @@
import { computed } from "mobx";
import { observer } from "mobx-react";
import { Doc } from "../../../new_fields/Doc";
-import { ScriptField } from "../../../new_fields/ScriptField";
import { Cast, StrCast } from "../../../new_fields/Types";
import { OmitKeys, Without } from "../../../Utils";
import { HistogramBox } from "../../northstar/dash-nodes/HistogramBox";
@@ -10,25 +9,27 @@ import { CollectionDockingView } from "../collections/CollectionDockingView";
import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
import { CollectionSchemaView } from "../collections/CollectionSchemaView";
import { CollectionView } from "../collections/CollectionView";
-import { LinkFollowBox } from "../linking/LinkFollowBox";
import { YoutubeBox } from "./../../apis/youtube/YoutubeBox";
import { AudioBox } from "./AudioBox";
import { ButtonBox } from "./ButtonBox";
+import { SliderBox } from "./SliderBox";
+import { LinkBox } from "./LinkBox";
import { DocumentBox } from "./DocumentBox";
import { DocumentViewProps } from "./DocumentView";
import "./DocumentView.scss";
import { FontIconBox } from "./FontIconBox";
import { FieldView, FieldViewProps } from "./FieldView";
import { FormattedTextBox } from "./FormattedTextBox";
-import { IconBox } from "./IconBox";
import { ImageBox } from "./ImageBox";
import { KeyValueBox } from "./KeyValueBox";
import { PDFBox } from "./PDFBox";
import { PresBox } from "./PresBox";
import { QueryBox } from "./QueryBox";
import { ColorBox } from "./ColorBox";
+import { DashWebRTCVideo } from "../webcam/DashWebRTCVideo";
import { DocuLinkBox } from "./DocuLinkBox";
import { PresElementBox } from "../presentationview/PresElementBox";
+import { ScreenshotBox } from "./ScreenshotBox";
import { VideoBox } from "./VideoBox";
import { WebBox } from "./WebBox";
import { InkingStroke } from "../InkingStroke";
@@ -58,33 +59,37 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
isSelected: (outsideReaction: boolean) => boolean,
select: (ctrl: boolean) => void,
layoutKey: string,
+ forceLayout?: string,
+ forceFieldKey?: string,
+ hideOnLeave?: boolean,
+ makeLink?: () => Opt<Doc>;
}> {
@computed get layout(): string {
TraceMobx();
if (!this.layoutDoc) return "<p>awaiting layout</p>";
const layout = Cast(this.layoutDoc[StrCast(this.layoutDoc.layoutKey, this.layoutDoc === this.props.Document ? this.props.layoutKey : "layout")], "string");
- if (layout === undefined) {
- return this.props.Document.data ?
- "<FieldView {...props} fieldKey='data' />" :
- KeyValueBox.LayoutString(this.layoutDoc.proto ? "proto" : "");
- } else if (typeof layout === "string") {
- return layout;
- } else {
- return "<p>Loading layout</p>";
- }
+ if (this.props.layoutKey === "layout_keyValue") {
+ return StrCast(this.props.Document.layout_keyValue, KeyValueBox.LayoutString("data"));
+ } else
+ if (layout === undefined) {
+ return this.props.Document.data ?
+ "<FieldView {...props} fieldKey='data' />" :
+ KeyValueBox.LayoutString(this.layoutDoc.proto ? "proto" : "");
+ } else if (typeof layout === "string") {
+ return layout;
+ } else {
+ return "<p>Loading layout</p>";
+ }
}
get dataDoc() {
- if (this.props.DataDoc === undefined && typeof Doc.LayoutField(this.props.Document) !== "string") {
- // if there is no dataDoc (ie, we're not rendering a template layout), but this document has a layout document (not a layout string),
- // then we render the layout document as a template and use this document as the data context for the template layout.
- const proto = Doc.GetProto(this.props.Document);
- return proto instanceof Promise ? undefined : proto;
- }
- return this.props.DataDoc instanceof Promise ? undefined : this.props.DataDoc;
+ const proto = this.props.DataDoc || Doc.GetProto(this.props.Document);
+ return proto instanceof Promise ? undefined : proto;
}
get layoutDoc() {
- return Doc.Layout(this.props.Document);
+ const params = StrCast(this.props.Document.PARAMS);
+ const template: Doc = this.props.LayoutDoc?.() || Doc.Layout(this.props.Document, this.props.layoutKey ? Cast(this.props.Document[this.props.layoutKey], Doc, null) : undefined);
+ return Doc.expandTemplateLayout(template, this.props.Document, params ? "(" + params + ")" : this.props.layoutKey);
}
CreateBindings(): JsxBindings {
@@ -98,20 +103,24 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
render() {
TraceMobx();
- return (this.props.renderDepth > 7 || !this.layout) ? (null) :
- <ObserverJsxParser
- blacklistedAttrs={[]}
- components={{
- FormattedTextBox, ImageBox, IconBox, DirectoryImportBox, FontIconBox: FontIconBox, ButtonBox, FieldView,
- CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, WebBox, KeyValueBox,
- PDFBox, VideoBox, AudioBox, HistogramBox, PresBox, YoutubeBox, LinkFollowBox, PresElementBox, QueryBox,
- ColorBox, DocuLinkBox, InkingStroke, DocumentBox, RecommendationsBox, SearchBox,
- }}
- bindings={this.CreateBindings()}
- jsx={this.layout}
- showWarnings={true}
+ return (this.props.renderDepth > 12 || !this.layout || !this.layoutDoc) ? (null) :
+ this.props.forceLayout === "FormattedTextBox" && this.props.forceFieldKey ?
+ <FormattedTextBox {...this.CreateBindings().props} fieldKey={this.props.forceFieldKey} />
+ :
+ <ObserverJsxParser
+ blacklistedAttrs={[]}
+ components={{
+ FormattedTextBox, ImageBox, DirectoryImportBox, FontIconBox, ButtonBox, SliderBox, FieldView,
+ CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, WebBox, KeyValueBox,
+ PDFBox, VideoBox, AudioBox, HistogramBox, PresBox, YoutubeBox, PresElementBox, QueryBox,
+ ColorBox, DashWebRTCVideo, DocuLinkBox, InkingStroke, DocumentBox, LinkBox,
+ RecommendationsBox, ScreenshotBox
+ }}
+ bindings={this.CreateBindings()}
+ jsx={this.layout}
+ showWarnings={true}
- onError={(test: any) => { console.log(test); }}
- />;
+ onError={(test: any) => { console.log(test); }}
+ />;
}
} \ No newline at end of file
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index 2ce56c73d..d1d96f0a1 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -5,6 +5,8 @@
position: inherit;
top: 0;
left: 0;
+ width: 100%;
+ height: 100%;
border-radius: inherit;
transition: outline .3s linear;
cursor: grab;
@@ -42,6 +44,33 @@
z-index: 1;
}
+ .documentView-lock {
+ width: 20;
+ height: 20;
+ position: absolute;
+ right: -5;
+ top: -5;
+ background: transparent;
+ pointer-events: all;
+ opacity: 0.3;
+ display: flex;
+ color: gold;
+ border-radius: 3px;
+ justify-content: center;
+ cursor: default;
+ }
+ .documentView-lock:hover {
+ opacity:1;
+ }
+
+ .documentView-contentBlocker {
+ pointer-events: all;
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ top: 0;
+ left: 0;
+ }
.documentView-styleWrapper {
position: absolute;
display: inline-block;
@@ -63,7 +92,6 @@
width: 100%;
height: 25;
background: rgba(0, 0, 0, .4);
- padding: 4px;
text-align: center;
text-overflow: ellipsis;
white-space: pre;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index c359be090..75186f018 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -3,56 +3,50 @@ import * as fa from '@fortawesome/free-solid-svg-icons';
import { action, computed, runInAction, trace, observable } from "mobx";
import { observer } from "mobx-react";
import * as rp from "request-promise";
-import { Doc, DocListCast, DocListCastAsync, Opt } from "../../../new_fields/Doc";
+import { Doc, DocListCast, Opt } from "../../../new_fields/Doc";
import { Document, PositionDocument } from '../../../new_fields/documentSchemas';
import { Id } from '../../../new_fields/FieldSymbols';
+import { InkTool } from '../../../new_fields/InkField';
+import { RichTextField } from '../../../new_fields/RichTextField';
import { listSpec } from "../../../new_fields/Schema";
import { ScriptField } from '../../../new_fields/ScriptField';
import { BoolCast, Cast, NumCast, StrCast } from "../../../new_fields/Types";
-import { ImageField, PdfField, VideoField, AudioField } from '../../../new_fields/URLField';
-import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
-import { emptyFunction, returnTransparent, returnTrue, Utils, returnOne } from "../../../Utils";
+import { AudioField, ImageField, PdfField, VideoField } from '../../../new_fields/URLField';
+import { TraceMobx } from '../../../new_fields/util';
+import { GestureUtils } from '../../../pen-gestures/GestureUtils';
+import { emptyFunction, returnOne, returnTransparent, returnTrue, Utils, OmitKeys } from "../../../Utils";
import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
import { DocServer } from "../../DocServer";
-import { Docs, DocUtils, DocumentOptions } from "../../documents/Documents";
+import { Docs, DocumentOptions, DocUtils } from "../../documents/Documents";
import { DocumentType } from '../../documents/DocumentTypes';
import { ClientUtils } from '../../util/ClientUtils';
import { DocumentManager } from "../../util/DocumentManager";
import { DragManager, dropActionType } from "../../util/DragManager";
+import { InteractionUtils } from '../../util/InteractionUtils';
import { Scripting } from '../../util/Scripting';
import { SelectionManager } from "../../util/SelectionManager";
import SharingManager from '../../util/SharingManager';
import { Transform } from "../../util/Transform";
import { undoBatch, UndoManager } from "../../util/UndoManager";
-import { CollectionViewType } from '../collections/CollectionView';
import { CollectionDockingView } from "../collections/CollectionDockingView";
-import { CollectionView } from "../collections/CollectionView";
+import { CollectionView, CollectionViewType } from '../collections/CollectionView';
import { ContextMenu } from "../ContextMenu";
import { ContextMenuProps } from '../ContextMenuItem';
import { DocComponent } from "../DocComponent";
import { EditableView } from '../EditableView';
+import { InkingControl } from '../InkingControl';
import { OverlayView } from '../OverlayView';
import { ScriptBox } from '../ScriptBox';
import { ScriptingRepl } from '../ScriptingRepl';
import { DocumentContentsView } from "./DocumentContentsView";
import "./DocumentView.scss";
-import { FormattedTextBox } from './FormattedTextBox';
import React = require("react");
-import { InteractionUtils } from '../../util/InteractionUtils';
-import { InkingControl } from '../InkingControl';
-import { InkTool } from '../../../new_fields/InkField';
-import { TraceMobx } from '../../../new_fields/util';
-import { List } from '../../../new_fields/List';
-import { FormattedTextBoxComment } from './FormattedTextBoxComment';
-import { GestureUtils } from '../../../pen-gestures/GestureUtils';
-import { RadialMenu } from './RadialMenu';
-import { RadialMenuProps } from './RadialMenuItem';
-
-import { CollectionStackingView } from '../collections/CollectionStackingView';
-import { RichTextField } from '../../../new_fields/RichTextField';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { SchemaHeaderField } from '../../../new_fields/SchemaHeaderField';
import { ClientRecommender } from '../../ClientRecommender';
import { SearchUtil } from '../../util/SearchUtil';
+import { RadialMenu } from './RadialMenu';
+import { KeyphraseQueryView } from '../KeyphraseQueryView';
library.add(fa.faEdit, fa.faTrash, fa.faShare, fa.faDownload, fa.faExpandArrowsAlt, fa.faCompressArrowsAlt, fa.faLayerGroup, fa.faExternalLinkAlt, fa.faAlignCenter, fa.faCaretSquareRight,
fa.faSquare, fa.faConciergeBell, fa.faWindowRestore, fa.faFolder, fa.faMapPin, fa.faLink, fa.faFingerprint, fa.faCrosshairs, fa.faDesktop, fa.faUnlock, fa.faLock, fa.faLaptopCode, fa.faMale,
@@ -63,11 +57,13 @@ export interface DocumentViewProps {
ContainingCollectionDoc: Opt<Doc>;
Document: Doc;
DataDoc?: Doc;
+ LayoutDoc?: () => Opt<Doc>;
LibraryPath: Doc[];
fitToBox?: boolean;
onClick?: ScriptField;
onPointerDown?: ScriptField;
onPointerUp?: ScriptField;
+ dropAction?: dropActionType;
dragDivName?: string;
addDocument?: (doc: Doc) => boolean;
removeDocument?: (doc: Doc) => boolean;
@@ -81,12 +77,12 @@ export interface DocumentViewProps {
parentActive: (outsideReaction: boolean) => boolean;
whenActiveChanged: (isActive: boolean) => void;
bringToFront: (doc: Doc, sendToBack?: boolean) => void;
- addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string, libraryPath?: Doc[]) => boolean;
+ addDocTab: (doc: Doc, where: string, libraryPath?: Doc[]) => boolean;
pinToPres: (document: Doc) => void;
zoomToScale: (scale: number) => void;
- backgroundColor: (doc: Doc) => string | undefined;
+ backgroundHalo?: () => boolean;
+ backgroundColor?: (doc: Doc) => string | undefined;
getScale: () => number;
- animateBetweenIcon?: (maximize: boolean, target: number[]) => void;
ChromeHeight?: () => number;
dontRegisterView?: boolean;
layoutKey?: string;
@@ -99,7 +95,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
private _downY: number = 0;
private _lastTap: number = 0;
private _doubleTap = false;
- private _hitTemplateDrag = false;
private _mainCont = React.createRef<HTMLDivElement>();
private _dropDisposer?: DragManager.DragDropDisposer;
private _showKPQuery: boolean = false;
@@ -116,7 +111,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
@computed get topMost() { return this.props.renderDepth === 0; }
@computed get nativeWidth() { return this.layoutDoc._nativeWidth || 0; }
@computed get nativeHeight() { return this.layoutDoc._nativeHeight || 0; }
- @computed get onClickHandler() { return this.props.onClick ? this.props.onClick : this.Document.onClick; }
+ @computed get onClickHandler() { return this.props.onClick || this.layoutDoc.onClick || this.Document.onClick; }
@computed get onPointerDownHandler() { return this.props.onPointerDown ? this.props.onPointerDown : this.Document.onPointerDown; }
@computed get onPointerUpHandler() { return this.props.onPointerUp ? this.props.onPointerUp : this.Document.onPointerUp; }
@@ -165,22 +160,21 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
me.touchEvent.stopPropagation();
me.touchEvent.preventDefault();
e.stopPropagation();
-
-
+ if (RadialMenu.Instance.used) {
+ this.onContextMenu(me.touches[0]);
+ }
}
@action
onRadialMenu = (e: Event, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>): void => {
- console.log("DISPLAYMENUUUU");
- console.log(me.touchEvent.touches);
// console.log(InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true));
// const pt = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true)[0];
const pt = me.touchEvent.touches[me.touchEvent.touches.length - 1];
RadialMenu.Instance.openMenu(pt.pageX - 15, pt.pageY - 15);
- RadialMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { width: 300, height: 300 }), undefined, "onRight"), icon: "map-pin", selected: -1 });
- RadialMenu.Instance.addItem({ description: "Delete this document", event: () => { this.props.ContainingCollectionView?.removeDocument(this.props.Document), RadialMenu.Instance.closeMenu() }, icon: "layer-group", selected: -1 });
- RadialMenu.Instance.addItem({ description: "Open in a new tab", event: () => this.props.addDocTab(this.props.Document, undefined, "onRight"), icon: "trash", selected: -1 });
+ RadialMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "onRight"), icon: "map-pin", selected: -1 });
+ RadialMenu.Instance.addItem({ description: "Delete this document", event: () => { this.props.ContainingCollectionView?.removeDocument(this.props.Document), RadialMenu.Instance.closeMenu(); }, icon: "layer-group", selected: -1 });
+ RadialMenu.Instance.addItem({ description: "Open in a new tab", event: () => this.props.addDocTab(this.props.Document, "onRight"), icon: "trash", selected: -1 });
RadialMenu.Instance.addItem({ description: "Pin to Presentation", event: () => this.props.pinToPres(this.props.Document), icon: "folder", selected: -1 });
// if (SelectionManager.IsSelected(this, true)) {
@@ -220,17 +214,19 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
this.multiTouchDisposer && this.multiTouchDisposer();
this.holdDisposer && this.holdDisposer();
Doc.UnBrushDoc(this.props.Document);
- !this.props.dontRegisterView && DocumentManager.Instance.DocumentViews.splice(DocumentManager.Instance.DocumentViews.indexOf(this), 1);
+ if (!this.props.dontRegisterView) {
+ const index = DocumentManager.Instance.DocumentViews.indexOf(this);
+ index !== -1 && DocumentManager.Instance.DocumentViews.splice(index, 1);
+ }
}
- startDragging(x: number, y: number, dropAction: dropActionType, applyAsTemplate?: boolean) {
+ startDragging(x: number, y: number, dropAction: dropActionType) {
if (this._mainCont.current) {
const dragData = new DragManager.DocumentDragData([this.props.Document]);
const [left, top] = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(0, 0);
dragData.offset = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).transformDirection(x - left, y - top);
dragData.dropAction = dropAction;
dragData.moveDocument = this.props.moveDocument;// this.Document.onDragStart ? undefined : this.props.moveDocument;
- dragData.applyAsTemplate = applyAsTemplate;
dragData.dragDivName = this.props.dragDivName;
DragManager.StartDocumentDrag([this._mainCont.current], dragData, x, y, { hideSource: !dropAction && !this.Document.onDragStart });
}
@@ -258,7 +254,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
e.stopPropagation();
e.preventDefault();
if (e.key === "†" || e.key === "t") {
- if (!StrCast(this.layoutDoc.showTitle)) this.layoutDoc.showTitle = "title";
+ if (!StrCast(this.layoutDoc._showTitle)) this.layoutDoc._showTitle = "title";
if (!this._titleRef.current) setTimeout(() => this._titleRef.current?.setIsFocused(true), 0);
else if (!this._titleRef.current.setIsFocused(true)) { // if focus didn't change, focus on interior text...
{
@@ -276,43 +272,36 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
}
- onClick = async (e: React.MouseEvent | React.PointerEvent) => {
- console.log(this.props.Document[Id])
- console.log(e.nativeEvent.cancelBubble);
- console.log(CurrentUserUtils.MainDocId !== this.props.Document[Id]);
- console.log(Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD);
- console.log(Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD);
-
- if (!e.nativeEvent.cancelBubble && !this.Document.ignoreClick && CurrentUserUtils.MainDocId !== this.props.Document[Id] &&
+ onClick = (e: React.MouseEvent | React.PointerEvent) => {
+ if (!e.nativeEvent.cancelBubble && !this.Document.ignoreClick &&
(Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD)) {
console.log("click");
e.stopPropagation();
let preventDefault = true;
+ this.props.bringToFront(this.props.Document);
if (this._doubleTap && this.props.renderDepth && !this.onClickHandler?.script) { // disable double-click to show full screen for things that have an on click behavior since clicking them twice can be misinterpreted as a double click
const fullScreenAlias = Doc.MakeAlias(this.props.Document);
- if (StrCast(fullScreenAlias.layoutKey) !== "layout_custom" && fullScreenAlias.layout_custom !== undefined) {
- fullScreenAlias.layoutKey = "layout_custom";
+ if (StrCast(fullScreenAlias.layoutKey) !== "layout_fullScreen" && fullScreenAlias.layout_fullScreen) {
+ fullScreenAlias.layoutKey = "layout_fullScreen";
}
- this.props.addDocTab(fullScreenAlias, undefined, "inTab");
+ UndoManager.RunInBatch(() => this.props.addDocTab(fullScreenAlias, "inTab"), "double tap");
SelectionManager.DeselectAll();
Doc.UnBrushDoc(this.props.Document);
- } else if (this.onClickHandler && this.onClickHandler.script) {
- this.onClickHandler.script.run({ this: this.Document.isTemplateForField && this.props.DataDoc ? this.props.DataDoc : this.props.Document, containingCollection: this.props.ContainingCollectionDoc }, console.log);
+ } else if (this.onClickHandler?.script) {
+ SelectionManager.DeselectAll();
+ UndoManager.RunInBatch(() => this.onClickHandler!.script.run({
+ this: this.props.Document,
+ self: Cast(this.props.Document.rootDocument, Doc, null) || this.props.Document,
+ containingCollection: this.props.ContainingCollectionDoc, shiftKey: e.shiftKey
+ }, console.log) && !this.props.Document.dontSelect && !this.props.Document.isButton && this.select(false), "on click");
} else if (this.Document.type === DocumentType.BUTTON) {
- console.log("button");
-
- ScriptBox.EditButtonScript("On Button Clicked ...", this.props.Document, "onClick", e.clientX, e.clientY);
- } else if (this.props.Document.isButton === "Selector") { // this should be moved to an OnClick script
- FormattedTextBoxComment.Hide();
- console.log("button2");
-
- this.Document.links?.[0] instanceof Doc && (Doc.UserDoc().SelectedDocs = new List([Doc.LinkOtherAnchor(this.Document.links[0], this.props.Document)]));
+ UndoManager.RunInBatch(() => ScriptBox.EditButtonScript("On Button Clicked ...", this.props.Document, "onClick", e.clientX, e.clientY), "on button click");
} else if (this.Document.isButton) {
console.log("button3");
SelectionManager.SelectDoc(this, e.ctrlKey); // don't think this should happen if a button action is actually triggered.
- this.buttonClick(e.altKey, e.ctrlKey);
+ UndoManager.RunInBatch(() => this.buttonClick(e.altKey, e.ctrlKey), "on link button follow");
} else {
SelectionManager.SelectDoc(this, e.ctrlKey);
preventDefault = false;
@@ -322,29 +311,11 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
buttonClick = async (altKey: boolean, ctrlKey: boolean) => {
- const maximizedDocs = await DocListCastAsync(this.Document.maximizedDocs);
- const summarizedDocs = await DocListCastAsync(this.Document.summarizedDocs);
const linkDocs = DocListCast(this.props.Document.links);
- let expandedDocs: Doc[] = [];
- expandedDocs = maximizedDocs ? [...maximizedDocs, ...expandedDocs] : expandedDocs;
- expandedDocs = summarizedDocs ? [...summarizedDocs, ...expandedDocs] : expandedDocs;
- // let expandedDocs = [ ...(maximizedDocs ? maximizedDocs : []), ...(summarizedDocs ? summarizedDocs : []),];
- if (expandedDocs.length) {
- SelectionManager.DeselectAll();
- let maxLocation = StrCast(this.Document.maximizeLocation, "inPlace");
- maxLocation = this.Document.maximizeLocation = (!ctrlKey ? !altKey ? maxLocation : (maxLocation !== "inPlace" ? "inPlace" : "onRight") : (maxLocation !== "inPlace" ? "inPlace" : "inTab"));
- if (maxLocation === "inPlace") {
- expandedDocs.forEach(maxDoc => this.props.addDocument && this.props.addDocument(maxDoc));
- const scrpt = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(NumCast(this.layoutDoc.width) / 2, NumCast(this.layoutDoc.height) / 2);
- DocumentManager.Instance.animateBetweenPoint(scrpt, expandedDocs);
- } else {
- expandedDocs.forEach(maxDoc => (!this.props.addDocTab(maxDoc, undefined, "close") && this.props.addDocTab(maxDoc, undefined, maxLocation)));
- }
- }
- else if (linkDocs.length) {
+ if (linkDocs.length) {
DocumentManager.Instance.FollowLink(undefined, this.props.Document,
// open up target if it's not already in view ... by zooming into the button document first and setting flag to reset zoom afterwards
- (doc: Doc, maxLocation: string) => this.props.focus(this.props.Document, true, 1, () => this.props.addDocTab(doc, undefined, maxLocation)),
+ (doc: Doc, maxLocation: string) => this.props.focus(this.props.Document, true, 1, () => this.props.addDocTab(doc, maxLocation)),
ctrlKey, altKey, this.props.ContainingCollectionDoc);
}
}
@@ -353,18 +324,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
SelectionManager.DeselectAll();
if (this.Document.onPointerDown) return;
const touch = me.touchEvent.changedTouches.item(0);
- console.log("DOWN", SelectionManager.SelectedDocuments());
- console.log("down");
if (touch) {
this._downX = touch.clientX;
this._downY = touch.clientY;
if (!e.nativeEvent.cancelBubble) {
- this._hitTemplateDrag = false;
- for (let element = (e.target as any); element && !this._hitTemplateDrag; element = element.parentElement) {
- if (element.className && element.className.toString() === "collectionViewBaseChrome-collapse") {
- this._hitTemplateDrag = true;
- }
- }
if ((this.active || this.Document.onDragStart || this.Document.onClick) && !e.ctrlKey && !this.Document.lockedPosition && !this.Document.inOverlay) e.stopPropagation();
this.removeMoveListeners();
this.addMoveListeners();
@@ -383,10 +346,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
else if (!e.cancelBubble && (SelectionManager.IsSelected(this, true) || this.props.parentActive(true) || this.Document.onDragStart || this.Document.onClick) && !this.Document.lockedPosition && !this.Document.inOverlay) {
const touch = me.touchEvent.changedTouches.item(0);
- if (Math.abs(this._downX - touch.clientX) > 3 || Math.abs(this._downY - touch.clientY) > 3) {
+ if (touch && (Math.abs(this._downX - touch.clientX) > 3 || Math.abs(this._downY - touch.clientY) > 3)) {
if (!e.altKey && (!this.topMost || this.Document.onDragStart || this.Document.onClick)) {
this.cleanUpInteractions();
- this.startDragging(this._downX, this._downY, this.Document._dropAction ? this.Document._dropAction as any : e.ctrlKey || e.altKey ? "alias" : undefined, this._hitTemplateDrag);
+ this.startDragging(this._downX, this._downY, this.Document.dropAction ? this.Document.dropAction as any : e.ctrlKey || e.altKey ? "alias" : undefined);
}
}
e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers
@@ -439,18 +402,12 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
const actualdH = Math.max(height + (dH * scale), 20);
doc.x = (doc.x || 0) + dX * (actualdW - width);
doc.y = (doc.y || 0) + dY * (actualdH - height);
- const fixedAspect = e.ctrlKey || (!layoutDoc.ignoreAspect && nwidth && nheight);
- if (fixedAspect && e.ctrlKey && layoutDoc.ignoreAspect) {
- layoutDoc.ignoreAspect = false;
-
- layoutDoc._nativeWidth = nwidth = layoutDoc._width || 0;
- layoutDoc._nativeHeight = nheight = layoutDoc._height || 0;
- }
+ const fixedAspect = e.ctrlKey || (nwidth && nheight);
if (fixedAspect && (!nwidth || !nheight)) {
layoutDoc._nativeWidth = nwidth = layoutDoc._width || 0;
layoutDoc._nativeHeight = nheight = layoutDoc._height || 0;
}
- if (nwidth > 0 && nheight > 0 && !layoutDoc.ignoreAspect) {
+ if (nwidth > 0 && nheight > 0) {
if (Math.abs(dW) > Math.abs(dH)) {
if (!fixedAspect) {
layoutDoc._nativeWidth = actualdW / (layoutDoc._width || 1) * (layoutDoc._nativeWidth || 0);
@@ -479,34 +436,26 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
onPointerDown = (e: React.PointerEvent): void => {
- console.log("ting");
- if (this.onPointerDownHandler && this.onPointerDownHandler.script) {
- this.onPointerDownHandler.script.run({ this: this.Document.isTemplateForField && this.props.DataDoc ? this.props.DataDoc : this.props.Document }, console.log);
- document.removeEventListener("pointerup", this.onPointerUp);
- document.addEventListener("pointerup", this.onPointerUp);
- return;
- }
// console.log(e.button)
// console.log(e.nativeEvent)
// continue if the event hasn't been canceled AND we are using a moues or this is has an onClick or onDragStart function (meaning it is a button document)
if (!(InteractionUtils.IsType(e, InteractionUtils.MOUSETYPE) || InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen)) {
if (!InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) {
e.stopPropagation();
+ // TODO: check here for panning/inking
}
return;
}
- if (!e.nativeEvent.cancelBubble || this.Document.onClick || this.Document.onDragStart) {
+ if (!e.nativeEvent.cancelBubble || this.onClickHandler || this.Document.onDragStart) {
this._downX = e.clientX;
this._downY = e.clientY;
- this._hitTemplateDrag = false;
- // this whole section needs to move somewhere else. We're trying to initiate a special "template" drag where
- // this document is the template and we apply it to whatever we drop it on.
- for (let element = (e.target as any); element && !this._hitTemplateDrag; element = element.parentElement) {
- if (element.className && element.className.toString() === "collectionViewBaseChrome-collapse") {
- this._hitTemplateDrag = true;
- }
+ if ((this.active || this.Document.onDragStart || this.onClickHandler) &&
+ !e.ctrlKey &&
+ (e.button === 0 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) &&
+ !this.Document.lockedPosition &&
+ !this.Document.inOverlay) {
+ e.stopPropagation(); // events stop at the lowest document that is active. if right dragging, we let it go through though to allow for context menu clicks. PointerMove callbacks should remove themselves if the move event gets stopPropagated by a lower-level handler (e.g, marquee drag);
}
- if ((this.active || this.Document.onDragStart || this.Document.onClick) && !e.ctrlKey && (e.button === 0 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) && !this.Document.lockedPosition && !this.Document.inOverlay) e.stopPropagation(); // events stop at the lowest document that is active. if right dragging, we let it go through though to allow for context menu clicks. PointerMove callbacks should remove themselves if the move event gets stopPropagated by a lower-level handler (e.g, marquee drag);
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
document.addEventListener("pointermove", this.onPointerMove);
@@ -523,12 +472,12 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
if (e.cancelBubble && this.active) {
document.removeEventListener("pointermove", this.onPointerMove); // stop listening to pointerMove if something else has stopPropagated it (e.g., the MarqueeView)
}
- else if (!e.cancelBubble && (SelectionManager.IsSelected(this, true) || this.props.parentActive(true) || this.Document.onDragStart || this.Document.onClick) && !this.Document.lockedPosition && !this.Document.inOverlay) {
+ else if (!e.cancelBubble && (SelectionManager.IsSelected(this, true) || this.props.parentActive(true) || this.Document.onDragStart || this.onClickHandler) && !this.Document.lockedPosition && !this.Document.inOverlay) {
if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) {
- if (!e.altKey && (!this.topMost || this.Document.onDragStart || this.Document.onClick) && (e.buttons === 1 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE))) {
+ if (!e.altKey && (!this.topMost || this.Document.onDragStart || this.onClickHandler) && (e.buttons === 1 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE))) {
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
- this.startDragging(this._downX, this._downY, this.Document._dropAction ? this.Document._dropAction as any : e.ctrlKey || e.altKey ? "alias" : undefined, this._hitTemplateDrag);
+ this.startDragging(this._downX, this._downY, this.props.dropAction ? this.props.dropAction : this.Document.dropAction ? this.Document.dropAction as any : e.ctrlKey || e.altKey ? "alias" : undefined);
}
}
e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers
@@ -561,73 +510,61 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
@undoBatch
- deleteClicked = (): void => { SelectionManager.DeselectAll(); this.props.removeDocument && this.props.removeDocument(this.props.Document); }
-
- static makeNativeViewClicked = (doc: Doc, prevLayout: string) => {
- undoBatch(() => {
- if (StrCast(doc.title).endsWith("_" + prevLayout)) doc.title = StrCast(doc.title).replace("_" + prevLayout, "");
- doc.layoutKey = "layout";
- })();
- }
+ deleteClicked = (): void => { SelectionManager.DeselectAll(); this.props.removeDocument?.(this.props.Document); }
- static makeCustomViewClicked = (doc: Doc, dataDoc: Opt<Doc>, creator: (documents: Array<Doc>, options: DocumentOptions, id?: string) => Doc, name: string = "custom", docLayoutTemplate?: Doc) => {
- const batch = UndoManager.StartBatch("CustomViewClicked");
- const customName = "layout_" + name;
- if (!StrCast(doc.title).endsWith(name)) doc.title = doc.title + "_" + name;
- if (doc[customName] === undefined) {
- const _width = NumCast(doc._width);
- const _height = NumCast(doc._height);
- const options = { title: "data", _width, x: -_width / 2, y: - _height / 2, };
-
- const field = doc.data;
- let fieldTemplate: Opt<Doc>;
- if (field instanceof RichTextField || typeof (field) === "string") {
- fieldTemplate = Docs.Create.TextDocument("", options);
- } else if (field instanceof PdfField) {
- fieldTemplate = Docs.Create.PdfDocument("http://www.msn.com", options);
- } else if (field instanceof VideoField) {
- fieldTemplate = Docs.Create.VideoDocument("http://www.cs.brown.edu", options);
- } else if (field instanceof AudioField) {
- fieldTemplate = Docs.Create.AudioDocument("http://www.cs.brown.edu", options);
- } else if (field instanceof ImageField) {
- fieldTemplate = Docs.Create.ImageDocument("http://www.cs.brown.edu", options);
- }
-
- if (fieldTemplate) {
- fieldTemplate.backgroundColor = doc.backgroundColor;
- fieldTemplate.heading = 1;
- fieldTemplate._autoHeight = true;
+ // applies a custom template to a document. the template is identified by it's short name (e.g, slideView not layout_slideView)
+ static makeCustomViewClicked = (doc: Doc, creator: (documents: Array<Doc>, options: DocumentOptions, id?: string) => Doc, templateSignature: string = "custom", docLayoutTemplate?: Doc) => {
+ const batch = UndoManager.StartBatch("makeCustomViewClicked");
+ runInAction(() => {
+ doc.layoutKey = "layout_" + templateSignature;
+ if (doc[doc.layoutKey] === undefined) {
+ DocumentView.createCustomView(doc, creator, templateSignature, docLayoutTemplate);
}
-
- const docTemplate = docLayoutTemplate || creator(fieldTemplate ? [fieldTemplate] : [], { title: customName + "(" + doc.title + ")", isTemplateDoc: true, _width: _width + 20, _height: Math.max(100, _height + 45) });
-
- fieldTemplate && Doc.MakeMetadataFieldTemplate(fieldTemplate, Doc.GetProto(docTemplate));
- Doc.ApplyTemplateTo(docTemplate, dataDoc || doc, customName, undefined);
- } else {
- doc.layoutKey = customName;
- }
+ });
batch.end();
}
-
- @undoBatch
- makeBtnClicked = (): void => {
- if (this.Document.isButton || this.Document.onClick || this.Document.ignoreClick) {
- this.Document.isButton = false;
- this.Document.ignoreClick = false;
- this.Document.onClick = undefined;
- } else {
- this.Document.isButton = true;
+ static createCustomView = (doc: Doc, creator: (documents: Array<Doc>, options: DocumentOptions, id?: string) => Doc, templateSignature: string = "custom", docLayoutTemplate?: Doc) => {
+ const iconViews = DocListCast(Cast(Doc.UserDoc().iconViews, Doc, null)?.data);
+ const templBtns = DocListCast(Cast(Doc.UserDoc().templateButtons, Doc, null)?.data);
+ const noteTypes = DocListCast(Cast(Doc.UserDoc().noteTypes, Doc, null)?.data);
+ const allTemplates = iconViews.concat(templBtns).concat(noteTypes).map(btnDoc => (btnDoc.dragFactory as Doc) || btnDoc).filter(doc => doc.isTemplateDoc);
+ const templateName = templateSignature.replace(/\(.*\)/, "");
+ // bcz: this is hacky -- want to have different templates be applied depending on the "type" of a document. but type is not reliable and there could be other types of template searches so this should be generalized
+ // first try to find a template that matches the specific document type (<typeName>_<templateName>). otherwise, fallback to a general match on <templateName>
+ !docLayoutTemplate && allTemplates.forEach(tempDoc => StrCast(tempDoc.title) === doc.type + "_" + templateName && (docLayoutTemplate = tempDoc));
+ !docLayoutTemplate && allTemplates.forEach(tempDoc => StrCast(tempDoc.title) === templateName && (docLayoutTemplate = tempDoc));
+
+ const customName = "layout_" + templateSignature;
+ const _width = NumCast(doc._width);
+ const _height = NumCast(doc._height);
+ const options = { title: "data", backgroundColor: StrCast(doc.backgroundColor), _autoHeight: true, _width, x: -_width / 2, y: - _height / 2, _showSidebar: false };
+
+ let fieldTemplate: Opt<Doc>;
+ if (doc.data instanceof RichTextField || typeof (doc.data) === "string") {
+ fieldTemplate = Docs.Create.TextDocument("", options);
+ } else if (doc.data instanceof PdfField) {
+ fieldTemplate = Docs.Create.PdfDocument("http://www.msn.com", options);
+ } else if (doc.data instanceof VideoField) {
+ fieldTemplate = Docs.Create.VideoDocument("http://www.cs.brown.edu", options);
+ } else if (doc.data instanceof AudioField) {
+ fieldTemplate = Docs.Create.AudioDocument("http://www.cs.brown.edu", options);
+ } else if (doc.data instanceof ImageField) {
+ fieldTemplate = Docs.Create.ImageDocument("http://www.cs.brown.edu", options);
}
+ const docTemplate = docLayoutTemplate || creator(fieldTemplate ? [fieldTemplate] : [], { title: customName + "(" + doc.title + ")", isTemplateDoc: true, _width: _width + 20, _height: Math.max(100, _height + 45) });
+
+ fieldTemplate && Doc.MakeMetadataFieldTemplate(fieldTemplate, Doc.GetProto(docTemplate));
+ Doc.ApplyTemplateTo(docTemplate, doc, customName, undefined);
}
@undoBatch
- makeSelBtnClicked = (): void => {
+ toggleButtonBehavior = (): void => {
if (this.Document.isButton || this.Document.onClick || this.Document.ignoreClick) {
this.Document.isButton = false;
this.Document.ignoreClick = false;
this.Document.onClick = undefined;
} else {
- this.props.Document.isButton = "Selector";
+ this.Document.isButton = true;
}
}
@@ -639,99 +576,53 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
e.stopPropagation();
de.complete.annoDragData.linkedToDoc = true;
- DocUtils.MakeLink({ doc: de.complete.annoDragData.annotationDocument }, { doc: this.props.Document, ctx: this.props.ContainingCollectionDoc },
- `Link from ${StrCast(de.complete.annoDragData.annotationDocument.title)}`);
- }
- if (de.complete.docDragData) {
- if (de.complete.docDragData.applyAsTemplate) {
- Doc.ApplyTemplateTo(de.complete.docDragData.draggedDocuments[0], this.props.Document, "layout_custom");
- e.stopPropagation();
- }
- else if (de.complete.docDragData.draggedDocuments[0].type === "text") {
- const text = Cast(de.complete.docDragData.draggedDocuments[0].data, RichTextField)?.Text;
- if (text && text[0] === "{" && text[text.length - 1] === "}" && text.includes(":")) {
- let loc = text.indexOf(":");
- let key = text.slice(1, loc);
- let value = text.slice(loc + 1, text.length - 1);
- console.log(key);
- console.log(value);
- console.log(this.props.Document);
- this.props.Document[key] = value;
- console.log(de.complete.docDragData.draggedDocuments[0].x);
- console.log(de.complete.docDragData.draggedDocuments[0].x);
- e.preventDefault();
- e.stopPropagation();
- de.complete.aborted = true;
- }
- }
+ DocUtils.MakeLink({ doc: de.complete.annoDragData.annotationDocument }, { doc: this.props.Document }, "link");
}
if (de.complete.linkDragData) {
e.stopPropagation();
// const docs = await SearchUtil.Search(`data_l:"${destDoc[Id]}"`, true);
// const views = docs.map(d => DocumentManager.Instance.getDocumentView(d)).filter(d => d).map(d => d as DocumentView);
de.complete.linkDragData.linkSourceDocument !== this.props.Document &&
- (de.complete.linkDragData.linkDocument = DocUtils.MakeLink({ doc: de.complete.linkDragData.linkSourceDocument }, { doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, "in-text link being created")); // TODODO this is where in text links get passed
+ (de.complete.linkDragData.linkDocument = DocUtils.MakeLink({ doc: de.complete.linkDragData.linkSourceDocument },
+ { doc: this.props.Document }, `link`)); // TODODO this is where in text links get passed
}
}
+ @undoBatch
@action
- onDrop = (e: React.DragEvent) => {
- const text = e.dataTransfer.getData("text/plain");
- if (!e.isDefaultPrevented() && text && text.startsWith("<div")) {
- const oldLayout = this.Document.layout || "";
- const layout = text.replace("{layout}", oldLayout);
- this.Document.layout = layout;
- e.stopPropagation();
- e.preventDefault();
- }
+ public static unfreezeNativeDimensions(layoutDoc: Doc) {
+ layoutDoc._nativeWidth = undefined;
+ layoutDoc._nativeHeight = undefined;
}
- @undoBatch
- @action
- freezeNativeDimensions = (): void => {
- this.layoutDoc._autoHeight = false;
- this.layoutDoc.ignoreAspect = !this.layoutDoc.ignoreAspect;
- if (!this.layoutDoc.ignoreAspect && !this.layoutDoc._nativeWidth) {
- this.layoutDoc._nativeWidth = this.props.PanelWidth();
- this.layoutDoc._nativeHeight = this.props.PanelHeight();
+ toggleNativeDimensions = () => {
+ if (this.Document._nativeWidth || this.Document._nativeHeight) {
+ DocumentView.unfreezeNativeDimensions(this.layoutDoc);
+ }
+ else {
+ Doc.freezeNativeDimensions(this.layoutDoc, this.props.PanelWidth(), this.props.PanelHeight());
}
}
@undoBatch
@action
makeIntoPortal = async () => {
- const anchors = await Promise.all(DocListCast(this.Document.links).map(async (d: Doc) => Cast(d.anchor2, Doc)));
- if (!anchors.find(anchor2 => anchor2 && anchor2.title === this.Document.title + ".portal" ? true : false)) {
- const portalID = (this.Document.title + ".portal").replace(/^-/, "").replace(/\([0-9]*\)$/, "");
- DocServer.GetRefField(portalID).then(existingPortal => {
- const portal = existingPortal instanceof Doc ? existingPortal : Docs.Create.FreeformDocument([], { _width: (this.layoutDoc._width || 0) + 10, _height: this.layoutDoc._height || 0, title: portalID });
- DocUtils.MakeLink({ doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, { doc: portal }, portalID, "portal link");
- this.Document.isButton = true;
- });
+ const portalLink = DocListCast(this.Document.links).find(d => d.anchor1 === this.props.Document);
+ if (!portalLink) {
+ const portal = Docs.Create.FreeformDocument([], { _width: (this.layoutDoc._width || 0) + 10, _height: this.layoutDoc._height || 0, title: StrCast(this.props.Document.title) + ".portal" });
+ DocUtils.MakeLink({ doc: this.props.Document }, { doc: portal }, "portal to");
}
+ this.Document.isButton = true;
}
@undoBatch
@action
- setCustomView =
- (custom: boolean, layout: string): void => {
- if (this.props.ContainingCollectionView?.props.DataDoc || this.props.ContainingCollectionView?.props.Document.isTemplateDoc) {
- Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.ContainingCollectionView.props.Document);
- } else if (custom) {
- DocumentView.makeNativeViewClicked(this.props.Document, StrCast(this.props.Document.layoutKey).split("_")[1]);
-
- let foundLayout: Opt<Doc> = undefined;
- DocListCast(Cast(CurrentUserUtils.UserDocument.expandingButtons, Doc, null)?.data)?.map(btnDoc => {
- if (StrCast(Cast(btnDoc?.dragFactory, Doc, null)?.title) === layout) {
- foundLayout = btnDoc.dragFactory as Doc;
- }
- })
- DocumentView.
- makeCustomViewClicked(this.props.Document, this.props.DataDoc, Docs.Create.StackingDocument, layout, foundLayout);
- } else {
- DocumentView.makeNativeViewClicked(this.props.Document, StrCast(this.props.Document.layoutKey).split("_")[1]);
- }
+ setCustomView = (custom: boolean, layout: string): void => {
+ Doc.setNativeView(this.props.Document);
+ if (custom) {
+ DocumentView.makeCustomViewClicked(this.props.Document, Docs.Create.StackingDocument, layout, undefined);
}
+ }
@undoBatch
@action
@@ -760,7 +651,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
return;
}
e.persist();
- e.stopPropagation();
+ e?.stopPropagation();
if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3 ||
e.isDefaultPrevented()) {
e.preventDefault();
@@ -769,23 +660,41 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
e.preventDefault();
const cm = ContextMenu.Instance;
- const subitems: ContextMenuProps[] = [];
- subitems.push({ description: "Open Full Screen", event: () => CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(this, this.props.LibraryPath), icon: "desktop" });
- subitems.push({ description: "Open Tab ", event: () => this.props.addDocTab(this.props.Document, this.props.DataDoc, "inTab", this.props.LibraryPath), icon: "folder" });
- subitems.push({ description: "Open Right ", event: () => this.props.addDocTab(this.props.Document, this.props.DataDoc, "onRight", this.props.LibraryPath), icon: "caret-square-right" });
- subitems.push({ description: "Open Alias Tab ", event: () => this.props.addDocTab(Doc.MakeAlias(this.props.Document), this.props.DataDoc, "inTab"), icon: "folder" });
- subitems.push({ description: "Open Alias Right", event: () => this.props.addDocTab(Doc.MakeAlias(this.props.Document), this.props.DataDoc, "onRight"), icon: "caret-square-right" });
- subitems.push({ description: "Open Fields ", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), undefined, "onRight"), icon: "layer-group" });
- cm.addItem({ description: "Open...", subitems: subitems, icon: "external-link-alt" });
+ const templateDoc = Cast(this.props.Document[StrCast(this.props.Document.layoutKey)], Doc, null);
+
+ const existing = cm.findByDescription("Layout...");
+ const layoutItems: ContextMenuProps[] = existing && "subitems" in existing ? existing.subitems : [];
+ layoutItems.push({ description: this.Document.isBackground ? "As Foreground" : "As Background", event: this.makeBackground, icon: this.Document.lockedPosition ? "unlock" : "lock" });
+ layoutItems.push({ description: "Make View of Metadata Field", event: () => Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.DataDoc), icon: "concierge-bell" });
+
+ layoutItems.push({ description: `${this.Document._chromeStatus !== "disabled" ? "Hide" : "Show"} Chrome`, event: () => this.Document._chromeStatus = (this.Document._chromeStatus !== "disabled" ? "disabled" : "enabled"), icon: "project-diagram" });
+ layoutItems.push({ description: `${this.Document._autoHeight ? "Variable Height" : "Auto Height"}`, event: () => this.layoutDoc._autoHeight = !this.layoutDoc._autoHeight, icon: "plus" });
+ layoutItems.push({ description: !this.Document._nativeWidth || !this.Document._nativeHeight ? "Freeze" : "Unfreeze", event: this.toggleNativeDimensions, icon: "snowflake" });
+ layoutItems.push({ description: this.Document.lockedPosition ? "Unlock Position" : "Lock Position", event: this.toggleLockPosition, icon: BoolCast(this.Document.lockedPosition) ? "unlock" : "lock" });
+ layoutItems.push({ description: this.Document.lockedTransform ? "Unlock Transform" : "Lock Transform", event: this.toggleLockTransform, icon: BoolCast(this.Document.lockedTransform) ? "unlock" : "lock" });
+ layoutItems.push({ description: "Center View", event: () => this.props.focus(this.props.Document, false), icon: "crosshairs" });
+ layoutItems.push({ description: "Zoom to Document", event: () => this.props.focus(this.props.Document, true), icon: "search" });
+ !existing && cm.addItem({ description: "Layout...", subitems: layoutItems, icon: "compass" });
+ const open = ContextMenu.Instance.findByDescription("Open...");
+ const openItems: ContextMenuProps[] = open && "subitems" in open ? open.subitems : [];
+ openItems.push({ description: "Open Full Screen", event: () => CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(this, this.props.LibraryPath), icon: "desktop" });
+ openItems.push({ description: "Open Tab ", event: () => this.props.addDocTab(this.props.Document, "inTab", this.props.LibraryPath), icon: "folder" });
+ openItems.push({ description: "Open Right ", event: () => this.props.addDocTab(this.props.Document, "onRight", this.props.LibraryPath), icon: "caret-square-right" });
+ openItems.push({ description: "Open Alias Tab ", event: () => this.props.addDocTab(Doc.MakeAlias(this.props.Document), "inTab"), icon: "folder" });
+ openItems.push({ description: "Open Alias Right", event: () => this.props.addDocTab(Doc.MakeAlias(this.props.Document), "onRight"), icon: "caret-square-right" });
+ openItems.push({ description: "Open Fields ", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "onRight"), icon: "layer-group" });
+ templateDoc && openItems.push({ description: "Open Template ", event: () => this.props.addDocTab(templateDoc, "onRight"), icon: "eye" });
+ openItems.push({ description: "Open Repl", icon: "laptop-code", event: () => OverlayView.Instance.addWindow(<ScriptingRepl />, { x: 300, y: 100, width: 200, height: 200, title: "Scripting REPL" }) });
+ !open && cm.addItem({ description: "Open...", subitems: openItems, icon: "external-link-alt" });
- const existingOnClick = ContextMenu.Instance.findByDescription("OnClick...");
+
+ const existingOnClick = cm.findByDescription("OnClick...");
const onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : [];
onClicks.push({ description: "Enter Portal", event: this.makeIntoPortal, icon: "window-restore" });
- onClicks.push({ description: "Toggle Detail", event: () => this.Document.onClick = ScriptField.MakeScript("toggleDetail(this)"), icon: "window-restore" });
+ onClicks.push({ description: "Toggle Detail", event: () => this.Document.onClick = ScriptField.MakeScript(`toggleDetail(this, "${this.props.Document.layoutKey}")`), icon: "window-restore" });
onClicks.push({ description: this.Document.ignoreClick ? "Select" : "Do Nothing", event: () => this.Document.ignoreClick = !this.Document.ignoreClick, icon: this.Document.ignoreClick ? "unlock" : "lock" });
- onClicks.push({ description: this.Document.isButton || this.Document.onClick ? "Remove Click Behavior" : "Follow Link", event: this.makeBtnClicked, icon: "concierge-bell" });
- onClicks.push({ description: this.props.Document.isButton ? "Remove Select Link Behavior" : "Select Link", event: this.makeSelBtnClicked, icon: "concierge-bell" });
+ onClicks.push({ description: this.Document.isButton || this.Document.onClick ? "Remove Click Behavior" : "Follow Link", event: this.toggleButtonBehavior, icon: "concierge-bell" });
onClicks.push({ description: "Edit onClick Script", icon: "edit", event: (obj: any) => ScriptBox.EditButtonScript("On Button Clicked ...", this.props.Document, "onClick", obj.x, obj.y) });
!existingOnClick && cm.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" });
@@ -794,24 +703,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
funcs.push({ description: "Drag an Alias", icon: "edit", event: () => this.Document.dragFactory && (this.Document.onDragStart = ScriptField.MakeFunction('getAlias(this.dragFactory)')) });
funcs.push({ description: "Drag a Copy", icon: "edit", event: () => this.Document.dragFactory && (this.Document.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)')) });
funcs.push({ description: "Drag Document", icon: "edit", event: () => this.Document.onDragStart = undefined });
- ContextMenu.Instance.addItem({ description: "OnDrag...", subitems: funcs, icon: "asterisk" });
+ cm.addItem({ description: "OnDrag...", subitems: funcs, icon: "asterisk" });
}
- const existing = ContextMenu.Instance.findByDescription("Layout...");
- const layoutItems: ContextMenuProps[] = existing && "subitems" in existing ? existing.subitems : [];
- layoutItems.push({ description: this.Document.isBackground ? "As Foreground" : "As Background", event: this.makeBackground, icon: this.Document.lockedPosition ? "unlock" : "lock" });
- layoutItems.push({ description: "Make View of Metadata Field", event: () => Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.DataDoc), icon: "concierge-bell" });
-
- layoutItems.push({ description: `${this.Document._chromeStatus !== "disabled" ? "Hide" : "Show"} Chrome`, event: () => this.Document._chromeStatus = (this.Document._chromeStatus !== "disabled" ? "disabled" : "enabled"), icon: "project-diagram" });
- layoutItems.push({ description: `${this.Document._autoHeight ? "Variable Height" : "Auto Height"}`, event: () => this.layoutDoc._autoHeight = !this.layoutDoc._autoHeight, icon: "plus" });
- layoutItems.push({ description: this.Document.ignoreAspect || !this.Document._nativeWidth || !this.Document._nativeHeight ? "Freeze" : "Unfreeze", event: this.freezeNativeDimensions, icon: "snowflake" });
- layoutItems.push({ description: this.Document.lockedPosition ? "Unlock Position" : "Lock Position", event: this.toggleLockPosition, icon: BoolCast(this.Document.lockedPosition) ? "unlock" : "lock" });
- layoutItems.push({ description: this.Document.lockedTransform ? "Unlock Transform" : "Lock Transform", event: this.toggleLockTransform, icon: BoolCast(this.Document.lockedTransform) ? "unlock" : "lock" });
- layoutItems.push({ description: "Center View", event: () => this.props.focus(this.props.Document, false), icon: "crosshairs" });
- layoutItems.push({ description: "Zoom to Document", event: () => this.props.focus(this.props.Document, true), icon: "search" });
- !existing && cm.addItem({ description: "Layout...", subitems: layoutItems, icon: "compass" });
-
- const more = ContextMenu.Instance.findByDescription("More...");
+ const more = cm.findByDescription("More...");
const moreItems: ContextMenuProps[] = more && "subitems" in more ? more.subitems : [];
if (!ClientUtils.RELEASE) {
@@ -827,8 +722,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
moreItems.push({ description: "Tag Child Images via Google Photos", event: () => GooglePhotos.Query.TagChildImages(this.props.Document), icon: "caret-square-right" });
moreItems.push({ description: "Write Back Link to Album", event: () => GooglePhotos.Transactions.AddTextEnrichment(this.props.Document), icon: "caret-square-right" });
}
- moreItems.push({ description: "Pin to Presentation", event: () => this.props.pinToPres(this.props.Document), icon: "map-pin" }); //I think this should work... and it does! A miracle!
- moreItems.push({ description: "Add Repl", icon: "laptop-code", event: () => OverlayView.Instance.addWindow(<ScriptingRepl />, { x: 300, y: 100, width: 200, height: 200, title: "Scripting REPL" }) });
moreItems.push({
description: "Download document", icon: "download", event: async () =>
console.log(JSON.parse(await rp.get(Utils.CorsProxy("http://localhost:8983/solr/dash/select"), {
@@ -840,7 +733,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
// a.download = `DocExport-${this.props.Document[Id]}.zip`;
// a.click();
});
- let recommender_subitems: ContextMenuProps[] = [];
+ const recommender_subitems: ContextMenuProps[] = [];
recommender_subitems.push({
description: "Internal recommendations",
@@ -848,16 +741,16 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
icon: "brain"
});
- let ext_recommender_subitems: ContextMenuProps[] = [];
+ const ext_recommender_subitems: ContextMenuProps[] = [];
ext_recommender_subitems.push({
description: "arXiv",
- event: () => this.externalRecommendation(e, "arxiv"),
+ event: () => this.externalRecommendation("arxiv"),
icon: "brain"
});
ext_recommender_subitems.push({
description: "Bing",
- event: () => this.externalRecommendation(e, "bing"),
+ event: () => this.externalRecommendation("bing"),
icon: "brain"
});
@@ -933,9 +826,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
//ClientRecommender.Instance.arxivrequest("electrons");
await Promise.all(allDocs.map((doc: Doc) => {
let isMainDoc: boolean = false;
- const dataDoc = Doc.GetDataDoc(doc);
+ const dataDoc = Doc.GetProto(doc);
if (doc.type === DocumentType.TEXT) {
- if (dataDoc === Doc.GetDataDoc(this.props.Document)) {
+ if (dataDoc === Doc.GetProto(this.props.Document)) {
isMainDoc = true;
}
if (!documents.includes(dataDoc)) {
@@ -945,7 +838,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
}
if (doc.type === DocumentType.IMG) {
- if (dataDoc === Doc.GetDataDoc(this.props.Document)) {
+ if (dataDoc === Doc.GetProto(this.props.Document)) {
isMainDoc = true;
}
if (!documents.includes(dataDoc)) {
@@ -956,7 +849,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
}));
const doclist = ClientRecommender.Instance.computeSimilarities("cosine");
- let recDocs: { preview: Doc, score: number }[] = [];
+ const recDocs: { preview: Doc, score: number }[] = [];
// tslint:disable-next-line: prefer-for-of
for (let i = 0; i < doclist.length; i++) {
recDocs.push({ preview: doclist[i].actualDoc, score: doclist[i].score });
@@ -980,7 +873,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
}
@action
- externalRecommendation = async (e: React.MouseEvent, api: string) => {
+ externalRecommendation = async (api: string) => {
if (!ClientRecommender.Instance) new ClientRecommender({ title: "Client Recommender" });
ClientRecommender.Instance.reset_docs();
const doc = Doc.GetDataDoc(this.props.Document);
@@ -1014,12 +907,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
onPointerEnter = (e: React.PointerEvent): void => { Doc.BrushDoc(this.props.Document); };
onPointerLeave = (e: React.PointerEvent): void => { Doc.UnBrushDoc(this.props.Document); };
- // the document containing the view layout information - will be the Document itself unless the Document has
- // a layout field. In that case, all layout information comes from there unless overriden by Document
- get layoutDoc(): Document {
- return Document(Doc.Layout(this.props.Document));
- }
-
// does Document set a layout prop
// does Document set a layout prop
setsLayoutProp = (prop: string) => this.props.Document[prop] !== this.props.Document["default" + prop[0].toUpperCase() + prop.slice(1)] && this.props.Document["default" + prop[0].toUpperCase() + prop.slice(1)];
@@ -1031,15 +918,14 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
select = (ctrlPressed: boolean) => { SelectionManager.SelectDoc(this, ctrlPressed); };
chromeHeight = () => {
- const showTitle = StrCast(this.layoutDoc.showTitle);
- const showTitleHover = StrCast(this.layoutDoc.showTitleHover);
- return (showTitle && !showTitleHover ? 0 : 0) + 1;
+ const showTitle = StrCast(this.layoutDoc._showTitle);
+ const showTextTitle = showTitle && (StrCast(this.layoutDoc.layout).indexOf("PresBox") !== -1 || StrCast(this.layoutDoc.layout).indexOf("FormattedTextBox") !== -1) ? showTitle : undefined;
+ return showTextTitle ? 25 : 1;
}
@computed get finalLayoutKey() {
- const { layoutKey } = this.props;
- if (typeof layoutKey === "string") {
- return layoutKey;
+ if (typeof this.props.layoutKey === "string") {
+ return this.props.layoutKey;
}
const fallback = Cast(this.props.Document.layoutKey, "string");
return typeof fallback === "string" ? fallback : "layout";
@@ -1051,6 +937,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
ContainingCollectionDoc={this.props.ContainingCollectionDoc}
Document={this.props.Document}
DataDoc={this.props.DataDoc}
+ LayoutDoc={this.props.LayoutDoc}
+ makeLink={this.makeLink}
fitToBox={this.props.fitToBox}
LibraryPath={this.props.LibraryPath}
addDocument={this.props.addDocument}
@@ -1058,7 +946,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
moveDocument={this.props.moveDocument}
ScreenToLocalTransform={this.props.ScreenToLocalTransform}
renderDepth={this.props.renderDepth}
- ContentScaling={this.childScaling}
PanelWidth={this.props.PanelWidth}
PanelHeight={this.props.PanelHeight}
focus={this.props.focus}
@@ -1069,8 +956,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
pinToPres={this.props.pinToPres}
zoomToScale={this.props.zoomToScale}
backgroundColor={this.props.backgroundColor}
- animateBetweenIcon={this.props.animateBetweenIcon}
getScale={this.props.getScale}
+ ContentScaling={this.childScaling}
ChromeHeight={this.chromeHeight}
isSelected={this.isSelected}
select={this.select}
@@ -1084,32 +971,53 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
// would be good to generalize this some way.
isNonTemporalLink = (linkDoc: Doc) => {
const anchor = Cast(Doc.AreProtosEqual(this.props.Document, Cast(linkDoc.anchor1, Doc) as Doc) ? linkDoc.anchor1 : linkDoc.anchor2, Doc) as Doc;
- const ept = Doc.AreProtosEqual(this.props.Document, Cast(linkDoc.anchor1, Doc) as Doc) ? linkDoc.anchor1Timecode : linkDoc.anchor2Timecode;
+ const ept = Doc.AreProtosEqual(this.props.Document, Cast(linkDoc.anchor1, Doc) as Doc) ? linkDoc.anchor1_timecode : linkDoc.anchor2_timecode;
return anchor.type === DocumentType.AUDIO && NumCast(ept) ? false : true;
}
+ @observable _link: Opt<Doc>;
+ makeLink = () => {
+ return this._link;
+ }
+
@computed get innards() {
TraceMobx();
- const showTitle = StrCast(this.getLayoutPropStr("showTitle"));
- const showTitleHover = StrCast(this.getLayoutPropStr("showTitleHover"));
- const showCaption = this.getLayoutPropStr("showCaption");
- const showTextTitle = showTitle && StrCast(this.layoutDoc.layout).indexOf("FormattedTextBox") !== -1 ? showTitle : undefined;
+ if (!this.props.PanelWidth()) {
+ return <div style={{ display: "flex", overflow: "hidden" }}>
+ {StrCast(this.props.Document.title)}
+ {this.Document.links && DocListCast(this.Document.links).filter(d => !d.hidden).filter(this.isNonTemporalLink).map((d, i) =>
+ <div className="documentView-docuLinkWrapper" style={{ position: "absolute", top: 0, left: 0 }} key={`${d[Id]}`}>
+ <DocumentView {...this.props} Document={d} ContainingCollectionDoc={this.props.Document}
+ PanelWidth={returnOne} PanelHeight={returnOne} layoutKey={this.linkEndpoint(d)} ContentScaling={returnOne}
+ backgroundColor={returnTransparent} removeDocument={undoBatch(doc => doc.hidden = true)} />
+ </div>)}
+ </div>;
+ }
+ const showTitle = StrCast(this.layoutDoc._showTitle);
+ const showTitleHover = StrCast(this.layoutDoc._showTitleHover);
+ const showCaption = StrCast(this.layoutDoc._showCaption);
+ const showTextTitle = showTitle && (StrCast(this.layoutDoc.layout).indexOf("PresBox") !== -1 || StrCast(this.layoutDoc.layout).indexOf("FormattedTextBox") !== -1) ? showTitle : undefined;
const searchHighlight = (!this.Document.searchFields ? (null) :
<div className="documentView-searchHighlight">
{this.Document.searchFields}
</div>);
const captionView = (!showCaption ? (null) :
<div className="documentView-captionWrapper">
- <FormattedTextBox {...this.props}
- onClick={this.onClickHandler} DataDoc={this.props.DataDoc} active={returnTrue}
- isSelected={this.isSelected} focus={emptyFunction} select={this.select}
- hideOnLeave={true} fieldKey={showCaption}
- />
+ <DocumentContentsView {...OmitKeys(this.props, ['children']).omit}
+ hideOnLeave={true}
+ forceLayout={"FormattedTextBox"}
+ forceFieldKey={showCaption}
+ ContentScaling={this.childScaling}
+ ChromeHeight={this.chromeHeight}
+ isSelected={this.isSelected}
+ select={this.select}
+ onClick={this.onClickHandler}
+ layoutKey={this.finalLayoutKey} />
</div>);
const titleView = (!showTitle ? (null) :
<div className={`documentView-titleWrapper${showTitleHover ? "-hover" : ""}`} style={{
position: showTextTitle ? "relative" : "absolute",
- pointerEvents: SelectionManager.GetIsDragging() ? "none" : "all",
+ pointerEvents: SelectionManager.GetIsDragging() || this.onClickHandler || this.Document.ignoreClick ? "none" : "all",
}}>
<EditableView ref={this._titleRef}
contents={(this.props.DataDoc || this.props.Document)[showTitle]?.toString()}
@@ -1121,7 +1029,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
return <>
{this.Document.links && DocListCast(this.Document.links).filter(d => !d.hidden).filter(this.isNonTemporalLink).map((d, i) =>
<div className="documentView-docuLinkWrapper" key={`${d[Id]}`}>
- <DocumentView {...this.props} ContentScaling={returnOne} Document={d} layoutKey={this.linkEndpoint(d)} backgroundColor={returnTransparent} removeDocument={undoBatch(doc => doc.hidden = true)} />
+ <DocumentView {...this.props} ContentScaling={returnOne} ContainingCollectionDoc={this.props.Document} Document={d} layoutKey={this.linkEndpoint(d)} backgroundColor={returnTransparent} removeDocument={undoBatch(doc => doc.hidden = true)} />
</div>)}
{!showTitle && !showCaption ?
this.Document.searchFields ?
@@ -1133,7 +1041,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
this.contents
:
<div className="documentView-styleWrapper" >
- <div className="documentView-styleContentWrapper" style={{ height: showTextTitle ? "calc(100% - 29px)" : "100%", top: showTextTitle ? "29px" : undefined }}>
+ <div className="documentView-styleContentWrapper" style={{ height: showTextTitle ? `calc(100% - ${this.chromeHeight()}px)` : "100%", top: showTextTitle ? this.chromeHeight() : undefined }}>
{this.contents}
</div>
{titleView}
@@ -1144,48 +1052,65 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
</>;
}
@computed get ignorePointerEvents() {
- return (this.Document.isBackground && !this.isSelected()) || (this.Document.type === DocumentType.INK && InkingControl.Instance.selectedTool !== InkTool.None);
+ return (this.Document.isBackground && !this.isSelected()) || this.props.layoutKey?.includes("layout_key") || (this.Document.type === DocumentType.INK && InkingControl.Instance.selectedTool !== InkTool.None);
}
+ @observable _animate = 0;
+ switchViews = action((custom: boolean, view: string) => {
+ SelectionManager.SetIsDragging(true);
+ this._animate = 0.1;
+ setTimeout(action(() => {
+ this.setCustomView(custom, view);
+ this._animate = 1;
+ setTimeout(action(() => {
+ this._animate = 0;
+ SelectionManager.SetIsDragging(false);
+ }), 400);
+ }), 400);
+ });
+
render() {
if (!(this.props.Document instanceof Doc)) return (null);
- const colorSet = this.setsLayoutProp("backgroundColor");
- const clusterCol = this.props.ContainingCollectionDoc && this.props.ContainingCollectionDoc.clusterOverridesDefaultBackground;
- const backgroundColor = (clusterCol && !colorSet) ?
- this.props.backgroundColor(this.Document) || StrCast(this.layoutDoc.backgroundColor) :
- StrCast(this.layoutDoc.backgroundColor) || this.props.backgroundColor(this.Document);
-
+ const backgroundColor = StrCast(this.layoutDoc._backgroundColor) || StrCast(this.layoutDoc.backgroundColor) || StrCast(this.Document.backgroundColor) || this.props.backgroundColor?.(this.Document);
+ const finalColor = this.layoutDoc.type === DocumentType.FONTICON || this.layoutDoc._viewType === CollectionViewType.Linear ? undefined : backgroundColor;
const fullDegree = Doc.isBrushedHighlightedDegree(this.props.Document);
- const borderRounding = this.getLayoutPropStr("borderRounding");
+ const borderRounding = this.layoutDoc.borderRounding;
const localScale = fullDegree;
- const animDims = this.Document.animateToDimensions ? Array.from(this.Document.animateToDimensions) : undefined;
- const animheight = animDims ? animDims[1] : "100%";
- const animwidth = animDims ? animDims[0] : "100%";
-
- const highlightColors = ["transparent", "maroon", "maroon", "yellow", "magenta", "cyan", "orange"];
+ const highlightColors = Cast(Doc.UserDoc().activeWorkspace, Doc, null)?.darkScheme ?
+ ["transparent", "#65350c", "#65350c", "yellow", "magenta", "cyan", "orange"] :
+ ["transparent", "maroon", "maroon", "yellow", "magenta", "cyan", "orange"];
const highlightStyles = ["solid", "dashed", "solid", "solid", "solid", "solid", "solid"];
let highlighting = fullDegree && this.layoutDoc.type !== DocumentType.FONTICON && this.layoutDoc._viewType !== CollectionViewType.Linear;
highlighting = highlighting && this.props.focus !== emptyFunction; // bcz: hack to turn off highlighting onsidebar panel documents. need to flag a document as not highlightable in a more direct way
return <div className={`documentView-node${this.topMost ? "-topmost" : ""}`} ref={this._mainCont} onKeyDown={this.onKeyDown}
- onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick}
+ onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick}
onPointerEnter={e => Doc.BrushDoc(this.props.Document)} onPointerLeave={e => Doc.UnBrushDoc(this.props.Document)}
style={{
- transition: this.Document.isAnimating ? ".5s linear" : StrCast(this.Document.transition),
+ transformOrigin: this._animate ? "center center" : undefined,
+ transform: this._animate ? `scale(${this._animate})` : undefined,
+ transition: !this._animate ? StrCast(this.Document.transition) : this._animate < 1 ? "transform 0.5s ease-in" : "transform 0.5s ease-out",
pointerEvents: this.ignorePointerEvents ? "none" : "all",
- color: StrCast(this.Document.color),
+ color: StrCast(this.layoutDoc.color, "inherit"),
outline: highlighting && !borderRounding ? `${highlightColors[fullDegree]} ${highlightStyles[fullDegree]} ${localScale}px` : "solid 0px",
border: highlighting && borderRounding ? `${highlightStyles[fullDegree]} ${highlightColors[fullDegree]} ${localScale}px` : undefined,
boxShadow: this.props.Document.isTemplateForField ? "black 0.2vw 0.2vw 0.8vw" : undefined,
- background: this.layoutDoc.type === DocumentType.FONTICON || this.layoutDoc._viewType === CollectionViewType.Linear ? undefined : backgroundColor,
- width: animwidth,
- height: animheight,
+ background: finalColor,
opacity: this.Document.opacity
}}>
- {this.innards}
+ {this.Document.isBackground ? <div className="documentView-lock"> <FontAwesomeIcon icon="unlock" size="lg" /> </div> : (null)}
+ {this.onClickHandler && this.props.ContainingCollectionView?.props.Document._viewType === CollectionViewType.Time ? <>
+ {this.innards}
+ <div className="documentView-contentBlocker" />
+ </> :
+ this.innards}
</div>;
- { this._showKPQuery ? <KeyphraseQueryView keyphrases={this._queries}></KeyphraseQueryView> : undefined }
+ { this._showKPQuery ? <KeyphraseQueryView keyphrases={this._queries}></KeyphraseQueryView> : undefined; }
}
}
-Scripting.addGlobal(function toggleDetail(doc: any) { doc.layoutKey = StrCast(doc.layoutKey, "layout") === "layout" ? "layout_custom" : "layout"; }); \ No newline at end of file
+Scripting.addGlobal(function toggleDetail(doc: any, layoutKey: string, otherKey: string = "layout") {
+ const dv = DocumentManager.Instance.getDocumentView(doc);
+ if (dv?.props.Document.layoutKey === layoutKey) dv?.switchViews(otherKey !== "layout", otherKey.replace("layout_", ""));
+ else dv?.switchViews(true, layoutKey.replace("layout_", ""));
+}); \ No newline at end of file
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index dbbb76f83..0305f43d5 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -2,20 +2,15 @@ import React = require("react");
import { computed } from "mobx";
import { observer } from "mobx-react";
import { DateField } from "../../../new_fields/DateField";
-import { Doc, FieldResult, Opt } from "../../../new_fields/Doc";
-import { IconField } from "../../../new_fields/IconField";
+import { Doc, FieldResult, Opt, Field } from "../../../new_fields/Doc";
import { List } from "../../../new_fields/List";
-import { RichTextField } from "../../../new_fields/RichTextField";
-import { AudioField, ImageField, VideoField } from "../../../new_fields/URLField";
+import { ScriptField } from "../../../new_fields/ScriptField";
+import { AudioField, VideoField } from "../../../new_fields/URLField";
import { Transform } from "../../util/Transform";
import { CollectionView } from "../collections/CollectionView";
import { AudioBox } from "./AudioBox";
-import { FormattedTextBox } from "./FormattedTextBox";
-import { IconBox } from "./IconBox";
-import { ImageBox } from "./ImageBox";
-import { PDFBox } from "./PDFBox";
import { VideoBox } from "./VideoBox";
-import { ScriptField } from "../../../new_fields/ScriptField";
+import { dropActionType } from "../../util/DragManager";
//
// these properties get assigned through the render() method of the DocumentView when it creates this node.
@@ -31,23 +26,28 @@ export interface FieldViewProps {
DataDoc?: Doc;
LibraryPath: Doc[];
onClick?: ScriptField;
+ dropAction: dropActionType;
isSelected: (outsideReaction?: boolean) => boolean;
select: (isCtrlPressed: boolean) => void;
renderDepth: number;
addDocument?: (document: Doc) => boolean;
- addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean;
+ addDocTab: (document: Doc, where: string) => boolean;
pinToPres: (document: Doc) => void;
removeDocument?: (document: Doc) => boolean;
moveDocument?: (document: Doc, targetCollection: Doc | undefined, addDocument: (document: Doc) => boolean) => boolean;
+ backgroundColor?: (document: Doc) => string | undefined;
ScreenToLocalTransform: () => Transform;
+ bringToFront: (doc: Doc, sendToBack?: boolean) => void;
active: (outsideReaction?: boolean) => boolean;
whenActiveChanged: (isActive: boolean) => void;
+ dontRegisterView?: boolean;
focus: (doc: Doc) => void;
PanelWidth: () => number;
PanelHeight: () => number;
setVideoBox?: (player: VideoBox) => void;
ContentScaling: () => number;
ChromeHeight?: () => number;
+ childLayoutTemplate?: () => Opt<Doc>;
}
@observer
@@ -78,9 +78,6 @@ export class FieldView extends React.Component<FieldViewProps> {
// else if (field instaceof PresBox) {
// return <PresBox {...this.props} />;
// }
- else if (field instanceof IconField) {
- return <IconBox {...this.props} />;
- }
else if (field instanceof VideoField) {
return <VideoBox {...this.props} />;
}
@@ -114,16 +111,14 @@ export class FieldView extends React.Component<FieldViewProps> {
// );
}
else if (field instanceof List) {
- return (<div>
- {field.map(f => f instanceof Doc ? f.title : (f && f.toString && f.toString())).join(", ")}
- </div>);
+ return <div> {field.map(f => Field.toString(f)).join(", ")} </div>;
}
// bcz: this belongs here, but it doesn't render well so taking it out for now
// else if (field instanceof HtmlField) {
// return <WebBox {...this.props} />
// }
else if (!(field instanceof Promise)) {
- return <p>{field.toString()}</p>;
+ return <p>{Field.toString(field)}</p>;
}
else {
return <p> {"Waiting for server..."} </p>;
diff --git a/src/client/views/nodes/FontIconBox.tsx b/src/client/views/nodes/FontIconBox.tsx
index a191ac4f4..d4da21239 100644
--- a/src/client/views/nodes/FontIconBox.tsx
+++ b/src/client/views/nodes/FontIconBox.tsx
@@ -36,7 +36,7 @@ export class FontIconBox extends DocComponent<FieldViewProps, FontIconDocument>(
showTemplate = (): void => {
const dragFactory = Cast(this.props.Document.dragFactory, Doc, null);
- dragFactory && this.props.addDocTab(dragFactory, undefined, "onRight");
+ dragFactory && this.props.addDocTab(dragFactory, "onRight");
}
specificContextMenu = (): void => {
diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss
index c203ca0c3..526939438 100644
--- a/src/client/views/nodes/FormattedTextBox.scss
+++ b/src/client/views/nodes/FormattedTextBox.scss
@@ -29,6 +29,7 @@
max-height: 100%;
display: flex;
flex-direction: row;
+ transition: opacity 1s;
.formattedTextBox-dictation {
height: 12px;
@@ -95,8 +96,8 @@
.formattedTextBox-inner-rounded,
.formattedTextBox-inner {
- padding: 10px 10px;
height: 100%;
+ white-space: pre-wrap;
}
// .menuicon {
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 0d97c3029..241345f3e 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -1,24 +1,28 @@
import { library } from '@fortawesome/fontawesome-svg-core';
import { faEdit, faSmile, faTextHeight, faUpload } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { isEqual } from "lodash";
-import { action, computed, IReactionDisposer, Lambda, observable, reaction, runInAction, trace, _allowStateChangesInsideComputed } from "mobx";
+import { action, computed, IReactionDisposer, Lambda, observable, reaction, runInAction } from "mobx";
import { observer } from "mobx-react";
import { baseKeymap } from "prosemirror-commands";
import { history } from "prosemirror-history";
import { inputRules } from 'prosemirror-inputrules';
import { keymap } from "prosemirror-keymap";
-import { Fragment, Mark, Node, Node as ProsNode, Slice } from "prosemirror-model";
+import { Fragment, Mark, Node, Slice } from "prosemirror-model";
import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from "prosemirror-state";
import { ReplaceStep } from 'prosemirror-transform';
import { EditorView } from "prosemirror-view";
import { DateField } from '../../../new_fields/DateField';
-import { Doc, DocListCastAsync, Opt, WidthSym, HeightSym, DataSym, Field } from "../../../new_fields/Doc";
-import { Copy, Id } from '../../../new_fields/FieldSymbols';
+import { DataSym, Doc, DocListCastAsync, Field, HeightSym, Opt, WidthSym } from "../../../new_fields/Doc";
+import { documentSchema } from '../../../new_fields/documentSchemas';
+import { Id } from '../../../new_fields/FieldSymbols';
+import { InkTool } from '../../../new_fields/InkField';
import { RichTextField } from "../../../new_fields/RichTextField";
import { RichTextUtils } from '../../../new_fields/RichTextUtils';
import { createSchema, makeInterface } from "../../../new_fields/Schema";
-import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
-import { numberRange, Utils, addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, returnOne } from '../../../Utils';
+import { Cast, NumCast, StrCast, BoolCast, DateCast } from "../../../new_fields/Types";
+import { TraceMobx } from '../../../new_fields/util';
+import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, returnOne, Utils, returnTrue } from '../../../Utils';
import { GoogleApiClientUtils, Pulls, Pushes } from '../../apis/google_docs/GoogleApiClientUtils';
import { DocServer } from "../../DocServer";
import { Docs, DocUtils } from '../../documents/Documents';
@@ -26,32 +30,31 @@ import { DocumentType } from '../../documents/DocumentTypes';
import { DictationManager } from '../../util/DictationManager';
import { DragManager } from "../../util/DragManager";
import buildKeymap from "../../util/ProsemirrorExampleTransfer";
-import { inpRules } from "../../util/RichTextRules";
-import { DashDocCommentView, FootnoteView, ImageResizeView, DashDocView, OrderedListView, schema, SummaryView, DashFieldView } from "../../util/RichTextSchema";
+import RichTextMenu from '../../util/RichTextMenu';
+import { RichTextRules } from "../../util/RichTextRules";
+import { DashDocCommentView, DashDocView, DashFieldView, FootnoteView, ImageResizeView, OrderedListView, schema, SummaryView } from "../../util/RichTextSchema";
import { SelectionManager } from "../../util/SelectionManager";
import { undoBatch, UndoManager } from "../../util/UndoManager";
-import { DocAnnotatableComponent, DocAnnotatableProps } from "../DocComponent";
+import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
+import { ContextMenu } from '../ContextMenu';
+import { ContextMenuProps } from '../ContextMenuItem';
+import { DocAnnotatableComponent } from "../DocComponent";
import { DocumentButtonBar } from '../DocumentButtonBar';
import { InkingControl } from "../InkingControl";
+import { AudioBox } from './AudioBox';
import { FieldView, FieldViewProps } from "./FieldView";
import "./FormattedTextBox.scss";
import { FormattedTextBoxComment, formattedTextBoxCommentPlugin } from './FormattedTextBoxComment';
import React = require("react");
-import { ContextMenuProps } from '../ContextMenuItem';
-import { ContextMenu } from '../ContextMenu';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { documentSchema } from '../../../new_fields/documentSchemas';
-import { AudioBox } from './AudioBox';
-import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
-import { InkTool } from '../../../new_fields/InkField';
-import { TraceMobx } from '../../../new_fields/util';
-import RichTextMenu from '../../util/RichTextMenu';
+import { PrefetchProxy } from '../../../new_fields/Proxy';
+import { makeTemplate } from '../../util/DropConverter';
library.add(faEdit);
library.add(faSmile, faTextHeight, faUpload);
export interface FormattedTextBoxProps {
hideOnLeave?: boolean;
+ makeLink?: () => Opt<Doc>;
}
const richTextSchema = createSchema({
@@ -81,6 +84,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
private _lastY = 0;
private _undoTyping?: UndoManager.Batch;
private _searchReactionDisposer?: Lambda;
+ private _recordReactionDisposer: Opt<IReactionDisposer>;
private _scrollToRegionReactionDisposer: Opt<IReactionDisposer>;
private _reactionDisposer: Opt<IReactionDisposer>;
private _heightReactionDisposer: Opt<IReactionDisposer>;
@@ -88,12 +92,18 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
private _pullReactionDisposer: Opt<IReactionDisposer>;
private _pushReactionDisposer: Opt<IReactionDisposer>;
private _buttonBarReactionDisposer: Opt<IReactionDisposer>;
+ private _linkMakerDisposer: Opt<IReactionDisposer>;
+ private _scrollDisposer: Opt<IReactionDisposer>;
private dropDisposer?: DragManager.DragDropDisposer;
+ @computed get _recording() { return this.dataDoc.audioState === "recording"; }
+ set _recording(value) { this.dataDoc.audioState = value ? "recording" : undefined; }
+
@observable private _entered = false;
public static FocusedBox: FormattedTextBox | undefined;
public static SelectOnLoad = "";
+ public static SelectOnLoadChar = "";
public static IsFragment(html: string) {
return html.indexOf("data-pm-slice") !== -1;
}
@@ -147,7 +157,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
this.dataDoc[key] = doc || Docs.Create.FreeformDocument([], { title: value, _width: 500, _height: 500 }, value);
DocUtils.Publish(this.dataDoc[key] as Doc, value, this.props.addDocument, this.props.removeDocument);
if (linkDoc) { (linkDoc as Doc).anchor2 = this.dataDoc[key] as Doc; }
- else DocUtils.MakeLink({ doc: this.dataDoc, ctx: this.props.ContainingCollectionDoc }, { doc: this.dataDoc[key] as Doc }, "Ref:" + value, "link to named target", id);
+ else DocUtils.MakeLink({ doc: this.props.Document }, { doc: this.dataDoc[key] as Doc }, "link to named target", id);
});
});
});
@@ -183,21 +193,29 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
(tx.storedMarks && !this._editorView.state.storedMarks) && (this._editorView.state.storedMarks = tx.storedMarks);
const tsel = this._editorView.state.selection.$from;
- tsel.marks().filter(m => m.type === this._editorView!.state.schema.marks.user_mark).map(m => AudioBox.SetScrubTime(Math.max(0, m.attrs.modified * 5000 - 1000)));
- this._applyingChange = true;
- if (!this.props.Document._textTemplate || Doc.GetProto(this.props.Document) === this.dataDoc) {
- this.dataDoc[this.props.fieldKey + "-lastModified"] && (this.dataDoc[this.props.fieldKey + "-backgroundColor"] = "lightGray");
+ tsel.marks().filter(m => m.type === this._editorView!.state.schema.marks.user_mark).map(m => AudioBox.SetScrubTime(Math.max(0, m.attrs.modified * 1000)));
+ const curText = state.doc.textBetween(0, state.doc.content.size, "\n\n");
+ const curTemp = Cast(this.props.Document[this.props.fieldKey + "-textTemplate"], RichTextField);
+ if (!this._applyingChange) {
+ this._applyingChange = true;
this.dataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now()));
- this.dataDoc[this.props.fieldKey] = new RichTextField(JSON.stringify(state.toJSON()), state.doc.textBetween(0, state.doc.content.size, "\n\n"));
+ if (!curTemp || curText) { // if no template, or there's text, write it to the document. (if this is driven by a template, then this overwrites the template text which is intended)
+ this.dataDoc[this.props.fieldKey] = new RichTextField(JSON.stringify(state.toJSON()), curText);
+ this.dataDoc[this.props.fieldKey + "-noTemplate"] = (curTemp?.Text || "") !== curText; // mark the data field as being split from the template if it has been edited
+ } else { // if we've deleted all the text in a note driven by a template, then restore the template data
+ this._editorView.updateState(EditorState.fromJSON(this.config, JSON.parse(curTemp.Data)));
+ this.dataDoc[this.props.fieldKey + "-noTemplate"] = undefined; // mark the data field as not being split from any template it might have
+ }
+ this._applyingChange = false;
}
- this._applyingChange = false;
this.updateTitle();
this.tryUpdateHeight();
}
}
updateTitle = () => {
- if (StrCast(this.dataDoc.title).startsWith("-") && this._editorView && !this.Document.customTitle) {
+ if ((this.props.Document.isTemplateForField === "text" || !this.props.Document.isTemplateForField) && // only update the title if the data document's data field is changing
+ StrCast(this.dataDoc.title).startsWith("-") && this._editorView && !this.Document.customTitle) {
const str = this._editorView.state.doc.textContent;
const titlestr = str.substr(0, Math.min(40, str.length));
this.dataDoc.title = "-" + titlestr + (str.length > 40 ? "..." : "");
@@ -234,7 +252,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
}
protected createDropTarget = (ele: HTMLDivElement) => {
this.ProseRef = ele;
- this.dropDisposer && this.dropDisposer();
+ this.dropDisposer?.();
ele && (this.dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this)));
}
@@ -249,17 +267,6 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
Doc.GetProto(this.dataDoc)[this.props.fieldKey] = new RichTextField(draggedDoc.data.Data, draggedDoc.data.Text);
e.stopPropagation();
}
- // apply as template when dragging with Meta
- } else if (draggedDoc && draggedDoc.type === DocumentType.TEXT && !Doc.AreProtosEqual(draggedDoc, this.props.Document) && de.metaKey) {
- draggedDoc.isTemplateDoc = true;
- let newLayout = Doc.Layout(draggedDoc);
- if (typeof (draggedDoc.layout) === "string") {
- newLayout = Doc.MakeDelegate(draggedDoc);
- newLayout.layout = StrCast(newLayout.layout).replace(/fieldKey={'[^']*'}/, `fieldKey={'${this.props.fieldKey}'}`);
- }
- this.Document.layout_custom = newLayout;
- this.Document.layoutKey = "layout_custom";
- e.stopPropagation();
// embed document when dragging with a userDropAction or an embedDoc flag set
} else if (de.complete.docDragData.userDropAction || de.complete.docDragData.embedDoc) {
const target = de.complete.docDragData.droppedDocuments[0];
@@ -277,8 +284,16 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
e.stopPropagation();
// }
} // otherwise, fall through to outer collection to handle drop
+ } else if (de.complete.linkDragData) {
+ de.complete.linkDragData.linkDropCallback = this.linkDrop;
}
}
+ linkDrop = (data: DragManager.LinkDragData) => {
+ const linkDoc = data.linkDocument!;
+ const anchor1Title = linkDoc.anchor1 instanceof Doc ? StrCast(linkDoc.anchor1.title) : "-untitled-";
+ const anchor1Id = linkDoc.anchor1 instanceof Doc ? linkDoc.anchor1[Id] : "";
+ this.makeLinkToSelection(linkDoc[Id], anchor1Title, "onRight", anchor1Id)
+ }
getNodeEndpoints(context: Node, node: Node): { from: number, to: number } | null {
let offset = 0;
@@ -377,10 +392,23 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
toggleSidebar = () => this._sidebarMovement < 5 && (this.props.Document.sidebarWidthPercent = StrCast(this.props.Document.sidebarWidthPercent, "0%") === "0%" ? "25%" : "0%");
+ public static get DefaultLayout(): Doc | string | undefined {
+ return Cast(Doc.UserDoc().defaultTextLayout, Doc, null) || StrCast(Doc.UserDoc().defaultTextLayout, null);
+ }
specificContextMenu = (e: React.MouseEvent): void => {
const funcs: ContextMenuProps[] = [];
- funcs.push({ description: "Toggle Sidebar", event: () => { e.stopPropagation(); this.toggleSidebar(); }, icon: "expand-arrows-alt" });
- funcs.push({ description: "Record Bullet", event: () => { e.stopPropagation(); this.recordBullet(); }, icon: "expand-arrows-alt" });
+ this.props.Document.isTemplateDoc && funcs.push({ description: "Make Default Layout", event: async () => Doc.UserDoc().defaultTextLayout = new PrefetchProxy(this.props.Document), icon: "eye" });
+ funcs.push({ description: "Reset Default Layout", event: () => Doc.UserDoc().defaultTextLayout = undefined, icon: "eye" });
+ !this.props.Document.rootDocument && funcs.push({
+ description: "Make Template", event: () => {
+ this.props.Document.isTemplateDoc = makeTemplate(this.props.Document, true);
+ Doc.AddDocToList(Cast(Doc.UserDoc().noteTypes, Doc, null), "data", this.props.Document);
+ }, icon: "eye"
+ });
+ funcs.push({ description: "Toggle Single Line", event: () => this.props.Document._singleLine = !this.props.Document._singleLine, icon: "expand-arrows-alt" });
+ funcs.push({ description: "Toggle Sidebar", event: () => this.props.Document._showSidebar = !this.props.Document._showSidebar, icon: "expand-arrows-alt" });
+ funcs.push({ description: "Toggle Audio", event: () => this.props.Document._showAudio = !this.props.Document._showAudio, icon: "expand-arrows-alt" });
+ funcs.push({ description: "Toggle Menubar", event: () => this.toggleMenubar(), icon: "expand-arrows-alt" });
["My Text", "Text from Others", "Todo Items", "Important Items", "Ignore Items", "Disagree Items", "By Recent Minute", "By Recent Hour"].forEach(option =>
funcs.push({
description: (FormattedTextBox._highlights.indexOf(option) === -1 ? "Highlight " : "Unhighlight ") + option, event: () => {
@@ -397,12 +425,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
ContextMenu.Instance.addItem({ description: "Text Funcs...", subitems: funcs, icon: "asterisk" });
}
- @observable _recording = false;
-
recordDictation = () => {
- //this._editorView!.focus();
- if (this._recording) return;
- runInAction(() => this._recording = true);
DictationManager.Controls.listen({
interimHandler: this.setCurrentBulletContent,
continuous: { indefinite: false },
@@ -410,12 +433,14 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
if (results && [DictationManager.Controls.Infringed].includes(results)) {
DictationManager.Controls.stop();
}
- this._editorView!.focus();
+ //this._editorView!.focus();
});
}
- stopDictation = (abort: boolean) => {
- runInAction(() => this._recording = false);
- DictationManager.Controls.stop(!abort);
+ stopDictation = (abort: boolean) => { DictationManager.Controls.stop(!abort); };
+
+ @action
+ toggleMenubar = () => {
+ this.props.Document._chromeStatus = this.props.Document._chromeStatus === "disabled" ? "enabled" : "disabled";
}
recordBullet = async () => {
@@ -435,13 +460,25 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
setCurrentBulletContent = (value: string) => {
if (this._editorView) {
- let state = this._editorView.state;
+ const state = this._editorView.state;
+ const now = Date.now();
+ let mark = schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(now / 1000) });
+ if (!this._break && state.selection.to !== state.selection.from) {
+ for (let i = state.selection.from; i <= state.selection.to; i++) {
+ const pos = state.doc.resolve(i);
+ const um = Array.from(pos.marks()).find(m => m.type === schema.marks.user_mark);
+ if (um) {
+ mark = um;
+ break;
+ }
+ }
+ }
+ const recordingStart = DateCast(this.props.Document.recordingStart).date.getTime();
+ this._break = false;
+ value = "" + (mark.attrs.modified * 1000 - recordingStart) / 1000 + value;
const from = state.selection.from;
- const to = state.selection.to;
- this._editorView.dispatch(state.tr.insertText(value, from, to));
- state = this._editorView.state;
- const updated = TextSelection.create(state.doc, from, from + value.length);
- this._editorView.dispatch(state.tr.setSelection(updated));
+ const inserted = state.tr.insertText(value).addMark(from, from + value.length + 1, mark);
+ this._editorView.dispatch(inserted.setSelection(TextSelection.create(inserted.doc, from, from + value.length + 1)));
}
}
@@ -464,13 +501,14 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
}
_keymap: any = undefined;
+ _rules: RichTextRules | undefined;
@computed get config() {
- this._keymap = buildKeymap(schema);
- (schema as any).Document = this.props.Document;
+ this._keymap = buildKeymap(schema, this.props);
+ this._rules = new RichTextRules(this.props.Document, this);
return {
schema,
plugins: [
- inputRules(inpRules),
+ inputRules(this._rules.inpRules),
this.richTextMenuPlugin(),
history(),
keymap(this._keymap),
@@ -485,6 +523,13 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
};
}
+ makeLinkToSelection(linkDocId: string, title: string, location: string, targetDocId: string) {
+ if (this._editorView) {
+ const link = this._editorView.state.schema.marks.link.create({ href: Utils.prepend("/doc/" + linkDocId), title: title, location: location, linkId: linkDocId, targetId: targetDocId });
+ this._editorView.dispatch(this._editorView.state.tr.removeMark(this._editorView.state.selection.from, this._editorView.state.selection.to, this._editorView.state.schema.marks.link).
+ addMark(this._editorView.state.selection.from, this._editorView.state.selection.to, link));
+ }
+ }
componentDidMount() {
this._buttonBarReactionDisposer = reaction(
() => DocumentButtonBar.Instance,
@@ -495,14 +540,27 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
}
}
);
+ this._linkMakerDisposer = reaction(
+ () => this.props.makeLink?.(),
+ (linkDoc: Opt<Doc>) => {
+ if (linkDoc) {
+ const anchor2Title = linkDoc.anchor2 instanceof Doc ? StrCast(linkDoc.anchor2.title) : "-untitled-";
+ const anchor2Id = linkDoc.anchor2 instanceof Doc ? linkDoc.anchor2[Id] : "";
+ this.makeLinkToSelection(linkDoc[Id], anchor2Title, "onRight", anchor2Id);
+ }
+ },
+ { fireImmediately: true }
+ );
this._reactionDisposer = reaction(
() => {
- const field = Cast(this.props.Document._textTemplate || this.dataDoc[this.props.fieldKey], RichTextField);
- return field ? field.Data : RichTextUtils.Initialize();
+ if (this.dataDoc[this.props.fieldKey + "-noTemplate"] || !this.props.Document[this.props.fieldKey + "-textTemplate"]) {
+ return Cast(this.dataDoc[this.props.fieldKey], RichTextField, null)?.Data;
+ }
+ return Cast(this.props.Document[this.props.fieldKey + "-textTemplate"], RichTextField, null)?.Data;
},
incomingValue => {
- if (this._editorView && !this._applyingChange) {
+ if (incomingValue !== undefined && this._editorView && !this._applyingChange) {
const updatedState = JSON.parse(incomingValue);
this._editorView.updateState(EditorState.fromJSON(this.config, updatedState));
this.tryUpdateHeight();
@@ -542,6 +600,17 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
search => search ? this.highlightSearchTerms([Doc.SearchQuery()]) : this.unhighlightSearchTerms(),
{ fireImmediately: true });
+ this._recordReactionDisposer = reaction(() => this._recording,
+ () => {
+ if (this._recording) {
+ setTimeout(action(() => {
+ this.stopDictation(true);
+ setTimeout(() => this.recordDictation(), 500);
+ }), 500);
+ } else setTimeout(() => this.stopDictation(true), 0);
+ }
+ );
+
this._scrollToRegionReactionDisposer = reaction(
() => StrCast(this.layoutDoc.scrollToLinkID),
async (scrollToLinkID) => {
@@ -574,7 +643,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
if (ret.frag.size > 2 && ret.start >= 0) {
let selection = TextSelection.near(editor.state.doc.resolve(ret.start)); // default to near the start
if (ret.frag.firstChild) {
- selection = TextSelection.between(editor.state.doc.resolve(ret.start + 2), editor.state.doc.resolve(ret.start + ret.frag.firstChild.nodeSize)); // bcz: looks better to not have the target selected
+ selection = TextSelection.between(editor.state.doc.resolve(ret.start), editor.state.doc.resolve(ret.start + ret.frag.firstChild.nodeSize)); // bcz: looks better to not have the target selected
}
editor.dispatch(editor.state.tr.setSelection(new TextSelection(selection.$from, selection.$from)).scrollIntoView());
const mark = editor.state.schema.mark(this._editorView.state.schema.marks.search_highlight);
@@ -588,6 +657,10 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
{ fireImmediately: true }
);
+ this._scrollDisposer = reaction(() => NumCast(this.props.Document.scrollPos),
+ pos => this._scrollRef.current && this._scrollRef.current.scrollTo({ top: pos }), { fireImmediately: true }
+ );
+
setTimeout(() => this.tryUpdateHeight(NumCast(this.layoutDoc.limitHeight, 0)));
}
@@ -703,7 +776,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
targetAnnotations?.push(pdfRegion);
});
- const link = DocUtils.MakeLink({ doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, { doc: pdfRegion, ctx: pdfDoc }, "note on " + pdfDoc.title, "pasted PDF link");
+ const link = DocUtils.MakeLink({ doc: this.props.Document }, { doc: pdfRegion }, "PDF pasted");
if (link) {
cbe.clipboardData!.setData("dash/linkDoc", link[Id]);
const linkId = link[Id];
@@ -739,7 +812,9 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
}
private setupEditor(config: any, fieldKey: string) {
- const rtfField = Cast(this.props.Document._textTemplate || this.dataDoc[fieldKey], RichTextField);
+ const curText = Cast(this.dataDoc[this.props.fieldKey], RichTextField, null);
+ const useTemplate = !curText?.Text && this.props.Document[this.props.fieldKey + "-textTemplate"];
+ const rtfField = Cast((useTemplate && this.props.Document[this.props.fieldKey + "-textTemplate"]) || this.dataDoc[fieldKey], RichTextField);
if (this.ProseRef) {
const self = this;
this._editorView?.destroy();
@@ -769,21 +844,23 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
clipboardTextSerializer: this.clipboardTextSerializer,
handlePaste: this.handlePaste,
});
- this._editorView.state.schema.Document = this.props.Document;
const startupText = !rtfField && this._editorView && Field.toString(this.dataDoc[fieldKey] as Field);
if (startupText) {
this._editorView.dispatch(this._editorView.state.tr.insertText(startupText));
}
}
- const selectOnLoad = this.props.Document[Id] === FormattedTextBox.SelectOnLoad;
- if (selectOnLoad) {
+ const selectOnLoad = (Cast(this.props.Document.rootDocument, Doc, null) || this.props.Document)[Id] === FormattedTextBox.SelectOnLoad;
+ if (selectOnLoad && !this.props.dontRegisterView) {
FormattedTextBox.SelectOnLoad = "";
this.props.select(false);
+ FormattedTextBox.SelectOnLoadChar && this._editorView!.dispatch(this._editorView!.state.tr.insertText(FormattedTextBox.SelectOnLoadChar));
+ FormattedTextBox.SelectOnLoadChar = "";
+
}
(selectOnLoad /* || !rtfField?.Text*/) && this._editorView!.focus();
// add user mark for any first character that was typed since the user mark that gets set in KeyPress won't have been called yet.
- this._editorView!.state.storedMarks = [...(this._editorView!.state.storedMarks ? this._editorView!.state.storedMarks : []), schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.round(Date.now() / 1000 / 5) })];
+ this._editorView!.state.storedMarks = [...(this._editorView!.state.storedMarks ? this._editorView!.state.storedMarks : []), schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) })];
}
getFont(font: string) {
switch (font) {
@@ -799,19 +876,38 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
}
componentWillUnmount() {
- this._scrollToRegionReactionDisposer && this._scrollToRegionReactionDisposer();
- this._reactionDisposer && this._reactionDisposer();
- this._proxyReactionDisposer && this._proxyReactionDisposer();
- this._pushReactionDisposer && this._pushReactionDisposer();
- this._pullReactionDisposer && this._pullReactionDisposer();
- this._heightReactionDisposer && this._heightReactionDisposer();
- this._searchReactionDisposer && this._searchReactionDisposer();
- this._buttonBarReactionDisposer && this._buttonBarReactionDisposer();
- this._editorView && this._editorView.destroy();
+ this._scrollDisposer?.();
+ this._scrollToRegionReactionDisposer?.();
+ this._reactionDisposer?.();
+ this._proxyReactionDisposer?.();
+ this._pushReactionDisposer?.();
+ this._pullReactionDisposer?.();
+ this._heightReactionDisposer?.();
+ this._searchReactionDisposer?.();
+ this._recordReactionDisposer?.();
+ this._buttonBarReactionDisposer?.();
+ this._linkMakerDisposer?.();
+ this._editorView?.destroy();
}
static _downEvent: any;
+ _downX = 0;
+ _downY = 0;
+ _break = false;
onPointerDown = (e: React.PointerEvent): void => {
+ if (this._recording && !e.ctrlKey && e.button === 0) {
+ 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(this._editorView!.state.tr.setSelection(updated).insertText("\n", to));
+ e.preventDefault();
+ e.stopPropagation();
+ if (this._recording) setTimeout(() => this.recordDictation(), 500);
+ }
+ this._downX = e.clientX;
+ this._downY = e.clientY;
this.doLinkOnDeselect();
FormattedTextBox._downEvent = true;
FormattedTextBoxComment.textBox = this;
@@ -840,6 +936,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
if (e.buttons === 1 && this.props.isSelected(true) && !e.altKey) {
e.stopPropagation();
}
+ this._downX = this._downY = Number.NaN;
}
@action
@@ -853,12 +950,16 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
prosediv && (prosediv.keeplocation = undefined);
const pos = this._editorView?.state.selection.$from.pos || 1;
keeplocation && setTimeout(() => this._editorView?.dispatch(this._editorView?.state.tr.setSelection(TextSelection.create(this._editorView.state.doc, pos))));
+ const coords = !Number.isNaN(this._downX) ? { left: this._downX, top: this._downY, bottom: this._downY, right: this._downX } : this._editorView?.coordsAtPos(pos);
// jump rich text menu to this textbox
- const { current } = this._ref;
- if (current) {
- const x = Math.min(Math.max(current.getBoundingClientRect().left, 0), window.innerWidth - RichTextMenu.Instance.width);
- const y = this._ref.current!.getBoundingClientRect().top - RichTextMenu.Instance.height - 50;
+ const bounds = this._ref.current?.getBoundingClientRect();
+ if (bounds && this.props.Document._chromeStatus !== "disabled") {
+ const x = Math.min(Math.max(bounds.left, 0), window.innerWidth - RichTextMenu.Instance.width);
+ let y = Math.min(Math.max(0, bounds.top - RichTextMenu.Instance.height - 50), window.innerHeight - RichTextMenu.Instance.height);
+ if (coords && coords.left > x && coords.left < x + RichTextMenu.Instance.width && coords.top > y && coords.top < y + RichTextMenu.Instance.height + 50) {
+ y = Math.min(bounds.bottom, window.innerHeight - RichTextMenu.Instance.height);
+ }
RichTextMenu.Instance.jumpTo(x, y);
}
}
@@ -910,7 +1011,7 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
// if (linkClicked) {
// DocServer.GetRefField(linkClicked).then(async linkDoc => {
// (linkDoc instanceof Doc) &&
- // DocumentManager.Instance.FollowLink(linkDoc, this.props.Document, document => this.props.addDocTab(document, undefined, location ? location : "inTab"), false);
+ // DocumentManager.Instance.FollowLink(linkDoc, this.props.Document, document => this.props.addDocTab(document, location ? location : "inTab"), false);
// });
// }
// } else {
@@ -922,8 +1023,10 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
// }
// }
- this.hitBulletTargets(e.clientX, e.clientY, e.shiftKey, false);
- if (this._recording) setTimeout(() => { this.stopDictation(true); setTimeout(() => this.recordDictation(), 500); }, 500);
+ if (Math.abs(e.clientX - this._downX) < 4 && Math.abs(e.clientX - this._downX) < 4) {
+ this.props.select(e.ctrlKey);
+ this.hitBulletTargets(e.clientX, e.clientY, e.shiftKey, false);
+ }
}
// this hackiness handles clicking on the list item bullets to do expand/collapse. the bullets are ::before pseudo elements so there's no real way to hit test against them.
@@ -954,8 +1057,9 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
addStyleSheetRule(FormattedTextBox._bulletStyleSheet, list_node.attrs.mapStyle + list_node.attrs.bulletStyle + ":hover:before", { background: "lightgray" });
} else if (Math.abs(pos.pos - pos.inside) < 2) {
if (!highlightOnly) {
- this._editorView!.dispatch(this._editorView!.state.tr.setNodeMarkup(pos.inside, list_node.type, { ...list_node.attrs, visibility: !list_node.attrs.visibility }));
- this._editorView!.dispatch(this._editorView!.state.tr.setSelection(TextSelection.create(this._editorView!.state.doc, pos.inside)));
+ const offset = this._editorView!.state.doc.nodeAt(pos.inside)?.type === schema.nodes.ordered_list ? 1 : 0;
+ this._editorView!.dispatch(this._editorView!.state.tr.setNodeMarkup(pos.inside + offset, list_node.type, { ...list_node.attrs, visibility: !list_node.attrs.visibility }));
+ this._editorView!.dispatch(this._editorView!.state.tr.setSelection(TextSelection.create(this._editorView!.state.doc, pos.inside + offset)));
}
addStyleSheetRule(FormattedTextBox._bulletStyleSheet, list_node.attrs.mapStyle + list_node.attrs.bulletStyle + ":hover:before", { background: "lightgray" });
}
@@ -981,16 +1085,17 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
}
richTextMenuPlugin() {
- const self = FormattedTextBox;
return new Plugin({
view(newView) {
- RichTextMenu.Instance.changeView(newView);
+ RichTextMenu.Instance && RichTextMenu.Instance.changeView(newView);
return RichTextMenu.Instance;
}
});
}
+ public static HadSelection: boolean = false;
onBlur = (e: any) => {
+ FormattedTextBox.HadSelection = window.getSelection()?.toString() !== "";
//DictationManager.Controls.stop(false);
if (this._undoTyping) {
this._undoTyping.end();
@@ -1010,14 +1115,14 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
}
const state = this._editorView!.state;
if (!state.selection.empty && e.key === "%") {
- state.schema.EnteringStyle = true;
+ this._rules!.EnteringStyle = true;
e.preventDefault();
e.stopPropagation();
return;
}
- if (state.selection.empty || !state.schema.EnteringStyle) {
- state.schema.EnteringStyle = false;
+ if (state.selection.empty || !this._rules!.EnteringStyle) {
+ this._rules!.EnteringStyle = false;
}
if (e.key === "Escape") {
this._editorView!.dispatch(state.tr.setSelection(TextSelection.create(state.doc, state.selection.from, state.selection.from)));
@@ -1028,23 +1133,22 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
if (e.key === "Tab" || e.key === "Enter") {
e.preventDefault();
}
- const mark = e.key !== " " && this._lastTimedMark ? this._lastTimedMark : schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.round(Date.now() / 1000 / 5) });
+ const mark = e.key !== " " && this._lastTimedMark ? this._lastTimedMark : schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) });
this._lastTimedMark = mark;
this._editorView!.dispatch(this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark.create({})).addStoredMark(mark));
if (!this._undoTyping) {
this._undoTyping = UndoManager.StartBatch("undoTyping");
}
- if (this._recording) {
- this.stopDictation(true);
- setTimeout(() => this.recordDictation(), 250);
- }
}
+ onscrolled = (ev: React.UIEvent) => {
+ this.props.Document.scrollPos = this._scrollRef.current!.scrollTop;
+ }
@action
tryUpdateHeight(limitHeight?: number) {
let scrollHeight = this._ref.current?.scrollHeight;
- if (!this.layoutDoc.animateToPos && this.layoutDoc._autoHeight && scrollHeight &&
+ if (this.layoutDoc._autoHeight && scrollHeight &&
getComputedStyle(this._ref.current!.parentElement!).top === "0px") { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation
if (limitHeight && scrollHeight > limitHeight) {
scrollHeight = limitHeight;
@@ -1062,23 +1166,23 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
}
@computed get sidebarWidthPercent() { return StrCast(this.props.Document.sidebarWidthPercent, "0%"); }
- @computed get sidebarWidth() { return Number(this.sidebarWidthPercent.substring(0, this.sidebarWidthPercent.length - 1)) / 100 * this.props.PanelWidth(); }
+ sidebarWidth = () => Number(this.sidebarWidthPercent.substring(0, this.sidebarWidthPercent.length - 1)) / 100 * this.props.PanelWidth();
+ sidebarScreenToLocal = () => this.props.ScreenToLocalTransform().translate(-(this.props.PanelWidth() - this.sidebarWidth()), 0);
@computed get sidebarColor() { return StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], "transparent")); }
render() {
TraceMobx();
const rounded = StrCast(this.layoutDoc.borderRounding) === "100%" ? "-rounded" : "";
const interactive = InkingControl.Instance.selectedTool || this.layoutDoc.isBackground;
if (this.props.isSelected()) {
- // TODO: ftong --> update from dash in richtextmenu
- RichTextMenu.Instance.updateFromDash(this._editorView!, undefined, this.props);
+ this._editorView && RichTextMenu.Instance.updateFromDash(this._editorView, undefined, this.props);
} else if (FormattedTextBoxComment.textBox === this) {
FormattedTextBoxComment.Hide();
}
return (
<div className={`formattedTextBox-cont`} ref={this._ref}
style={{
- height: this.layoutDoc._autoHeight ? "max-content" : undefined,
- background: this.props.hideOnLeave ? "rgba(0,0,0 ,0.4)" : undefined,
+ height: this.layoutDoc._autoHeight && this.props.renderDepth ? "max-content" : undefined,
+ background: this.props.hideOnLeave ? "rgba(0,0,0 ,0.4)" : StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"]),
opacity: this.props.hideOnLeave ? (this._entered ? 1 : 0.1) : 1,
color: this.props.hideOnLeave ? "white" : "inherit",
pointerEvents: interactive ? "none" : "all",
@@ -1098,16 +1202,20 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
onPointerEnter={action(() => this._entered = true)}
onPointerLeave={action(() => this._entered = false)}
>
- <div className={`formattedTextBox-outer`} style={{ width: `calc(100% - ${this.sidebarWidthPercent})`, }} ref={this._scrollRef}>
- <div className={`formattedTextBox-inner${rounded}`} style={{ whiteSpace: "pre-wrap", pointerEvents: ((this.Document.isButton || this.props.onClick) && !this.props.isSelected()) ? "none" : undefined }} ref={this.createDropTarget} />
+ <div className={`formattedTextBox-outer`} style={{ width: `calc(100% - ${this.sidebarWidthPercent})`, }} onScroll={this.onscrolled} ref={this._scrollRef}>
+ <div className={`formattedTextBox-inner${rounded}`} ref={this.createDropTarget}
+ style={{
+ padding: `${NumCast(this.Document._xMargin, 0)}px ${NumCast(this.Document._yMargin, 0)}px`,
+ pointerEvents: ((this.Document.isButton || this.props.onClick) && !this.props.isSelected()) ? "none" : undefined
+ }} />
</div>
- {this.props.Document._hideSidebar ? (null) : this.sidebarWidthPercent === "0%" ?
+ {!this.props.Document._showSidebar ? (null) : this.sidebarWidthPercent === "0%" ?
<div className="formattedTextBox-sidebar-handle" onPointerDown={this.sidebarDown} onClick={e => this.toggleSidebar()} /> :
<div className={"formattedTextBox-sidebar" + (InkingControl.Instance.selectedTool !== InkTool.None ? "-inking" : "")}
style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}>
<CollectionFreeFormView {...this.props}
PanelHeight={this.props.PanelHeight}
- PanelWidth={() => this.sidebarWidth}
+ PanelWidth={this.sidebarWidth}
annotationsKey={this.annotationKey}
isAnnotationOverlay={false}
focus={this.props.focus}
@@ -1118,24 +1226,24 @@ export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps &
whenActiveChanged={this.whenActiveChanged}
removeDocument={this.removeDocument}
moveDocument={this.moveDocument}
- addDocument={(doc: Doc) => { doc._hideSidebar = true; return this.addDocument(doc); }}
+ addDocument={this.addDocument}
CollectionView={undefined}
- ScreenToLocalTransform={() => this.props.ScreenToLocalTransform().translate(-(this.props.PanelWidth() - this.sidebarWidth), 0)}
+ ScreenToLocalTransform={this.sidebarScreenToLocal}
renderDepth={this.props.renderDepth + 1}
- ContainingCollectionDoc={this.props.ContainingCollectionDoc}
- chromeCollapsed={true}>
+ ContainingCollectionDoc={this.props.ContainingCollectionDoc}>
</CollectionFreeFormView>
<div className="formattedTextBox-sidebar-handle" onPointerDown={this.sidebarDown} onClick={e => this.toggleSidebar()} />
</div>}
- <div className="formattedTextBox-dictation"
- onClick={e => {
- this._recording ? this.stopDictation(true) : this.recordDictation();
- setTimeout(() => this._editorView!.focus(), 500);
- e.stopPropagation();
- }} >
- <FontAwesomeIcon className="formattedTExtBox-audioFont"
- style={{ color: this._recording ? "red" : "blue", opacity: this._recording ? 1 : 0.5, display: this.props.isSelected() ? "" : "none" }} icon={"microphone"} size="sm" />
- </div>
+ {!this.props.Document._showAudio ? (null) :
+ <div className="formattedTextBox-dictation"
+ onPointerDown={e => {
+ runInAction(() => this._recording = !this._recording);
+ setTimeout(() => this._editorView!.focus(), 500);
+ e.stopPropagation();
+ }} >
+ <FontAwesomeIcon className="formattedTExtBox-audioFont"
+ style={{ color: this._recording ? "red" : "blue", opacity: this._recording ? 1 : 0.5, display: this.props.isSelected() ? "" : "none" }} icon={"microphone"} size="sm" />
+ </div>}
</div>
);
}
diff --git a/src/client/views/nodes/FormattedTextBoxComment.tsx b/src/client/views/nodes/FormattedTextBoxComment.tsx
index fda3e3285..61df188f8 100644
--- a/src/client/views/nodes/FormattedTextBoxComment.tsx
+++ b/src/client/views/nodes/FormattedTextBoxComment.tsx
@@ -84,9 +84,9 @@ export class FormattedTextBoxComment {
const textBox = FormattedTextBoxComment.textBox;
if (FormattedTextBoxComment.linkDoc && !keep && textBox) {
DocumentManager.Instance.FollowLink(FormattedTextBoxComment.linkDoc, textBox.props.Document,
- (doc: Doc, maxLocation: string) => textBox.props.addDocTab(doc, undefined, e.ctrlKey ? "inTab" : "onRight"));
+ (doc: Doc, maxLocation: string) => textBox.props.addDocTab(doc, e.ctrlKey ? "inTab" : "onRight"));
} else if (textBox && (FormattedTextBoxComment.tooltipText as any).href) {
- textBox.props.addDocTab(Docs.Create.WebDocument((FormattedTextBoxComment.tooltipText as any).href, { title: (FormattedTextBoxComment.tooltipText as any).href, _width: 200, _height: 400 }), undefined, "onRight");
+ textBox.props.addDocTab(Docs.Create.WebDocument((FormattedTextBoxComment.tooltipText as any).href, { title: (FormattedTextBoxComment.tooltipText as any).href, _width: 200, _height: 400 }), "onRight");
}
keep && textBox && FormattedTextBoxComment.start !== undefined && textBox.adoptAnnotation(
FormattedTextBoxComment.start, FormattedTextBoxComment.end, FormattedTextBoxComment.mark);
@@ -171,7 +171,7 @@ export class FormattedTextBoxComment {
if (linkDoc instanceof Doc) {
(FormattedTextBoxComment.tooltipText as any).href = mark.attrs.href;
FormattedTextBoxComment.linkDoc = linkDoc;
- const target = FieldValue(Doc.AreProtosEqual(FieldValue(Cast(linkDoc.anchor1, Doc)), textBox.props.Document) ? Cast(linkDoc.anchor2, Doc) : (Cast(linkDoc.anchor1, Doc)) || linkDoc);
+ const target = FieldValue(Doc.AreProtosEqual(FieldValue(Cast(linkDoc.anchor1, Doc)), textBox.dataDoc) ? Cast(linkDoc.anchor2, Doc) : (Cast(linkDoc.anchor1, Doc)) || linkDoc);
try {
ReactDOM.unmountComponentAtNode(FormattedTextBoxComment.tooltipText);
} catch (e) { }
@@ -189,8 +189,8 @@ export class FormattedTextBoxComment {
pinToPres={returnFalse}
dontRegisterView={true}
renderDepth={1}
- PanelWidth={() => Math.min(350, NumCast(target.width, 350))}
- PanelHeight={() => Math.min(250, NumCast(target.height, 250))}
+ PanelWidth={() => Math.min(350, NumCast(target._width, 350))}
+ PanelHeight={() => Math.min(250, NumCast(target._height, 250))}
focus={emptyFunction}
whenActiveChanged={returnFalse}
/>, FormattedTextBoxComment.tooltipText);
@@ -211,7 +211,7 @@ export class FormattedTextBoxComment {
// let start = view.coordsAtPos(state.selection.from), end = view.coordsAtPos(state.selection.to);
const start = view.coordsAtPos(state.selection.from - nbef), end = view.coordsAtPos(state.selection.from - nbef);
// The box in which the tooltip is positioned, to use as base
- const box = (document.getElementById("mainView-container") as any).getBoundingClientRect();
+ const box = (document.getElementsByClassName("mainView-container") as any)[0].getBoundingClientRect();
// Find a center-ish x position from the selection endpoints (when
// crossing lines, end may be more to the left)
const left = Math.max((start.left + end.left) / 2, start.left + 3);
diff --git a/src/client/views/nodes/IconBox.scss b/src/client/views/nodes/IconBox.scss
deleted file mode 100644
index 488681027..000000000
--- a/src/client/views/nodes/IconBox.scss
+++ /dev/null
@@ -1,23 +0,0 @@
-
-@import "../globalCssVariables";
-.iconBox-container {
- position: inherit;
- left:0;
- top:0;
- height: auto;
- width: max-content;
- // overflow: hidden;
- pointer-events: all;
- svg {
- width: $MINIMIZED_ICON_SIZE !important;
- height: $MINIMIZED_ICON_SIZE !important;
- height: auto;
- background: white;
- }
- .iconBox-label {
- position: absolute;
- width:max-content;
- font-size: 14px;
- margin-top: 3px;
- }
-} \ No newline at end of file
diff --git a/src/client/views/nodes/IconBox.tsx b/src/client/views/nodes/IconBox.tsx
deleted file mode 100644
index 172338eb6..000000000
--- a/src/client/views/nodes/IconBox.tsx
+++ /dev/null
@@ -1,93 +0,0 @@
-import React = require("react");
-import { library } from '@fortawesome/fontawesome-svg-core';
-import { faCaretUp, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote, faTag, faTextHeight } from '@fortawesome/free-solid-svg-icons';
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { computed, observable, runInAction } from "mobx";
-import { observer } from "mobx-react";
-import { FieldView, FieldViewProps } from './FieldView';
-import "./IconBox.scss";
-import { Cast, StrCast, BoolCast } from "../../../new_fields/Types";
-import { Doc, DocListCast } from "../../../new_fields/Doc";
-import { IconField } from "../../../new_fields/IconField";
-import { ContextMenu } from "../ContextMenu";
-import Measure from "react-measure";
-import { MINIMIZED_ICON_SIZE } from "../../views/globalCssVariables.scss";
-import { Scripting } from "../../util/Scripting";
-import { ComputedField } from "../../../new_fields/ScriptField";
-
-
-library.add(faCaretUp);
-library.add(faObjectGroup);
-library.add(faStickyNote);
-library.add(faFilePdf);
-library.add(faFilm, faTag, faTextHeight);
-
-@observer
-export class IconBox extends React.Component<FieldViewProps> {
- public static LayoutString(fieldKey: string) { return FieldView.LayoutString(IconBox, fieldKey); }
-
- @observable _panelWidth: number = 0;
- @observable _panelHeight: number = 0;
- @computed get layout(): string { const field = Cast(this.props.Document[this.props.fieldKey], IconField); return field ? field.icon : "<p>Error loading icon data</p>"; }
- @computed get minimizedIcon() { return IconBox.DocumentIcon(this.layout); }
-
- public static summaryTitleScript(inputDoc: Doc) {
- const sumDoc = Cast(inputDoc.summaryDoc, Doc) as Doc;
- if (sumDoc && StrCast(sumDoc.title).startsWith("-")) {
- return sumDoc.title + ".expanded";
- }
- return "???";
- }
- public static titleScript(inputDoc: Doc) {
- const maxDoc = DocListCast(inputDoc.maximizedDocs);
- if (maxDoc.length === 1) {
- return maxDoc[0].title + ".icon";
- }
- return maxDoc.length > 1 ? "-multiple-.icon" : "???";
- }
-
- public static AutomaticTitle(doc: Doc) {
- Doc.GetProto(doc).title = ComputedField.MakeFunction('iconTitle(this);');
- }
-
- public static DocumentIcon(layout: string) {
- const button = layout.indexOf("PDFBox") !== -1 ? faFilePdf :
- layout.indexOf("ImageBox") !== -1 ? faImage :
- layout.indexOf("Formatted") !== -1 ? faStickyNote :
- layout.indexOf("Video") !== -1 ? faFilm :
- layout.indexOf("Collection") !== -1 ? faObjectGroup :
- faCaretUp;
- return <FontAwesomeIcon icon={button} className="documentView-minimizedIcon" />;
- }
-
- setLabelField = (): void => {
- this.props.Document.hideLabel = !this.props.Document.hideLabel;
- }
-
- specificContextMenu = (): void => {
- const cm = ContextMenu.Instance;
- cm.addItem({ description: this.props.Document.hideLabel ? "Show label with icon" : "Remove label from icon", event: this.setLabelField, icon: "tag" });
- if (!this.props.Document.hideLabel) {
- cm.addItem({ description: "Use Target Title", event: () => IconBox.AutomaticTitle(this.props.Document), icon: "text-height" });
- }
- }
- render() {
- const label = this.props.Document.hideLabel ? "" : this.props.Document.title;
- return (
- <div className="iconBox-container" onContextMenu={this.specificContextMenu}>
- {this.minimizedIcon}
- <Measure offset onResize={(r) => runInAction(() => {
- if (r.offset!.width || this.props.Document.hideLabel) {
- this.props.Document.iconWidth = (r.offset!.width + Number(MINIMIZED_ICON_SIZE));
- if (this.props.Document._height === Number(MINIMIZED_ICON_SIZE)) this.props.Document._width = this.props.Document.iconWidth;
- }
- })}>
- {({ measureRef }) =>
- <span ref={measureRef} className="iconBox-label">{label}</span>
- }
- </Measure>
- </div>);
- }
-}
-Scripting.addGlobal(function iconTitle(doc: any) { return IconBox.titleScript(doc); });
-Scripting.addGlobal(function summaryTitle(doc: any) { return IconBox.summaryTitleScript(doc); }); \ No newline at end of file
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index 43f4a0ba9..7bbf4a368 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -8,7 +8,7 @@
transform-origin: top left;
.imageBox-fader {
- pointer-events: all;
+ pointer-events: inherit;
}
}
@@ -34,13 +34,12 @@
height: 100%;
max-width: 100%;
max-height: 100%;
- pointer-events: none;
+ pointer-events: inherit;
background: transparent;
img {
height: auto;
width: 100%;
- pointer-events: all;
}
}
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 207546936..00057055f 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -1,37 +1,34 @@
import { library } from '@fortawesome/fontawesome-svg-core';
import { faEye } from '@fortawesome/free-regular-svg-icons';
-import { faAsterisk, faFileAudio, faImage, faPaintBrush, faBrain } from '@fortawesome/free-solid-svg-icons';
+import { faAsterisk, faBrain, faFileAudio, faImage, faPaintBrush } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, observable, runInAction, trace } from 'mobx';
+import { action, computed, observable, runInAction } from 'mobx';
import { observer } from "mobx-react";
-import { Doc, DocListCast, HeightSym, WidthSym, DataSym } from '../../../new_fields/Doc';
+import { DataSym, Doc, DocListCast, HeightSym, WidthSym } from '../../../new_fields/Doc';
+import { documentSchema } from '../../../new_fields/documentSchemas';
+import { Id } from '../../../new_fields/FieldSymbols';
import { List } from '../../../new_fields/List';
+import { ObjectField } from '../../../new_fields/ObjectField';
import { createSchema, listSpec, makeInterface } from '../../../new_fields/Schema';
import { ComputedField } from '../../../new_fields/ScriptField';
import { Cast, NumCast, StrCast } from '../../../new_fields/Types';
import { AudioField, ImageField } from '../../../new_fields/URLField';
-import { Utils, returnOne, emptyFunction } from '../../../Utils';
+import { TraceMobx } from '../../../new_fields/util';
+import { emptyFunction, returnOne, Utils } from '../../../Utils';
import { CognitiveServices, Confidence, Service, Tag } from '../../cognitive_services/CognitiveServices';
import { Docs } from '../../documents/Documents';
+import { Networking } from '../../Network';
import { DragManager } from '../../util/DragManager';
+import { SelectionManager } from '../../util/SelectionManager';
import { undoBatch } from '../../util/UndoManager';
import { ContextMenu } from "../../views/ContextMenu";
+import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
import { ContextMenuProps } from '../ContextMenuItem';
import { DocAnnotatableComponent } from '../DocComponent';
import FaceRectangles from './FaceRectangles';
import { FieldView, FieldViewProps } from './FieldView';
import "./ImageBox.scss";
import React = require("react");
-import { SearchUtil } from '../../util/SearchUtil';
-import { ClientRecommender } from '../../ClientRecommender';
-import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
-import { documentSchema } from '../../../new_fields/documentSchemas';
-import { Id, Copy } from '../../../new_fields/FieldSymbols';
-import { TraceMobx } from '../../../new_fields/util';
-import { SelectionManager } from '../../util/SelectionManager';
-import { cache } from 'sharp';
-import { ObjectField } from '../../../new_fields/ObjectField';
-import { Networking } from '../../Network';
const requestImageSize = require('../../util/request-image-size');
const path = require('path');
const { Howl } = require('howler');
@@ -69,6 +66,7 @@ const uploadIcons = {
@observer
export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocument>(ImageDocument) {
+ protected multiTouchDisposer?: import("../../util/InteractionUtils").InteractionUtils.MultiTouchEventDisposer | undefined;
public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ImageBox, fieldKey); }
private _imgRef: React.RefObject<HTMLImageElement> = React.createRef();
private _dropDisposer?: DragManager.DragDropDisposer;
@@ -81,22 +79,27 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this)));
}
+ get fieldKey() {
+ return this.props.fieldKey.startsWith("@") ? StrCast(this.props.Document[this.props.fieldKey]) : this.props.fieldKey;
+ }
+
@undoBatch
@action
drop = (e: Event, de: DragManager.DropEvent) => {
if (de.complete.docDragData) {
if (de.metaKey) {
de.complete.docDragData.droppedDocuments.forEach(action((drop: Doc) => {
- Doc.AddDocToList(this.dataDoc, this.props.fieldKey + "-alternates", drop);
+ Doc.AddDocToList(this.dataDoc, this.fieldKey + "-alternates", drop);
e.stopPropagation();
}));
- } else if (de.altKey || !this.dataDoc[this.props.fieldKey]) {
+ } else if (de.altKey || !this.dataDoc[this.fieldKey]) {
const layoutDoc = de.complete.docDragData?.draggedDocuments[0];
const targetField = Doc.LayoutFieldKey(layoutDoc);
- if (layoutDoc?.[DataSym][targetField] instanceof ImageField) {
- this.dataDoc[this.props.fieldKey] = ObjectField.MakeCopy(layoutDoc[DataSym][targetField] as ImageField);
- this.dataDoc[this.props.fieldKey + "-nativeWidth"] = NumCast(layoutDoc[DataSym][targetField + "-nativeWidth"]);
- this.dataDoc[this.props.fieldKey + "-nativeHeight"] = NumCast(layoutDoc[DataSym][targetField + "-nativeHeight"]);
+ const targetDoc = layoutDoc[DataSym];
+ if (targetDoc[targetField] instanceof ImageField) {
+ this.dataDoc[this.fieldKey] = ObjectField.MakeCopy(targetDoc[targetField] as ImageField);
+ this.dataDoc[this.fieldKey + "-nativeWidth"] = NumCast(targetDoc[targetField + "-nativeWidth"]);
+ this.dataDoc[this.fieldKey + "-nativeHeight"] = NumCast(targetDoc[targetField + "-nativeHeight"]);
e.stopPropagation();
}
}
@@ -124,9 +127,9 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
// upload to server with known URL
const audioDoc = Docs.Create.AudioDocument(url, { title: "audio test", _width: 200, _height: 32 });
audioDoc.treeViewExpandedView = "layout";
- const audioAnnos = Cast(this.dataDoc[this.props.fieldKey + "-audioAnnotations"], listSpec(Doc));
+ const audioAnnos = Cast(this.dataDoc[this.fieldKey + "-audioAnnotations"], listSpec(Doc));
if (audioAnnos === undefined) {
- this.dataDoc[this.props.fieldKey + "-audioAnnotations"] = new List([audioDoc]);
+ this.dataDoc[this.fieldKey + "-audioAnnotations"] = new List([audioDoc]);
} else {
audioAnnos.push(audioDoc);
}
@@ -143,23 +146,36 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
@undoBatch
rotate = action(() => {
- const nw = NumCast(this.Document[this.props.fieldKey + "-nativeWidth"]);
- const nh = NumCast(this.Document[this.props.fieldKey + "-nativeHeight"]);
+ const nw = NumCast(this.Document[this.fieldKey + "-nativeWidth"]);
+ const nh = NumCast(this.Document[this.fieldKey + "-nativeHeight"]);
const w = this.Document._width;
const h = this.Document._height;
- this.dataDoc[this.props.fieldKey + "-rotation"] = (NumCast(this.dataDoc[this.props.fieldKey + "-rotation"]) + 90) % 360;
- this.dataDoc[this.props.fieldKey + "-nativeWidth"] = nh;
- this.dataDoc[this.props.fieldKey + "-nativeHeight"] = nw;
+ this.dataDoc[this.fieldKey + "-rotation"] = (NumCast(this.dataDoc[this.fieldKey + "-rotation"]) + 90) % 360;
+ this.dataDoc[this.fieldKey + "-nativeWidth"] = nh;
+ this.dataDoc[this.fieldKey + "-nativeHeight"] = nw;
this.Document._width = h;
this.Document._height = w;
});
specificContextMenu = (e: React.MouseEvent): void => {
- const field = Cast(this.Document[this.props.fieldKey], ImageField);
+ const field = Cast(this.Document[this.fieldKey], ImageField);
if (field) {
const funcs: ContextMenuProps[] = [];
funcs.push({ description: "Copy path", event: () => Utils.CopyText(field.url.href), icon: "expand-arrows-alt" });
funcs.push({ description: "Rotate", event: this.rotate, icon: "expand-arrows-alt" });
+ funcs.push({
+ description: "Reset Native Dimensions", event: action(async () => {
+ const curNW = NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"]);
+ const curNH = NumCast(this.dataDoc[this.fieldKey + "-nativeHeight"]);
+ if (this.props.PanelWidth() / this.props.PanelHeight() > curNW / curNH) {
+ this.dataDoc[this.fieldKey + "-nativeWidth"] = this.props.PanelHeight() * curNW / curNH;
+ this.dataDoc[this.fieldKey + "-nativeHeight"] = this.props.PanelHeight();
+ } else {
+ this.dataDoc[this.fieldKey + "-nativeWidth"] = this.props.PanelWidth();
+ this.dataDoc[this.fieldKey + "-nativeHeight"] = this.props.PanelWidth() * curNH / curNW;
+ }
+ }), icon: "expand-arrows-alt"
+ });
const existingAnalyze = ContextMenu.Instance.findByDescription("Analyzers...");
const modes: ContextMenuProps[] = existingAnalyze && "subitems" in existingAnalyze ? existingAnalyze.subitems : [];
@@ -178,7 +194,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
results.reduce((face: CognitiveServices.Image.Face, faceDocs: List<Doc>) => faceDocs.push(Docs.Get.DocumentHierarchyFromJson(face, `Face: ${face.faceId}`)!), new List<Doc>());
return faceDocs;
};
- this.url && CognitiveServices.Image.Appliers.ProcessImage(this.dataDoc, [this.props.fieldKey + "-faces"], this.url, Service.Face, converter);
+ this.url && CognitiveServices.Image.Appliers.ProcessImage(this.dataDoc, [this.fieldKey + "-faces"], this.url, Service.Face, converter);
}
generateMetadata = (threshold: Confidence = Confidence.Excellent) => {
@@ -190,16 +206,16 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
const sanitized = tag.name.replace(" ", "_");
tagDoc[sanitized] = ComputedField.MakeFunction(`(${tag.confidence} >= this.confidence) ? ${tag.confidence} : "${ComputedField.undefined}"`);
});
- this.dataDoc[this.props.fieldKey + "-generatedTags"] = tagsList;
+ this.dataDoc[this.fieldKey + "-generatedTags"] = tagsList;
tagDoc.title = "Generated Tags Doc";
tagDoc.confidence = threshold;
return tagDoc;
};
- this.url && CognitiveServices.Image.Appliers.ProcessImage(this.dataDoc, [this.props.fieldKey + "-generatedTagsDoc"], this.url, Service.ComputerVision, converter);
+ this.url && CognitiveServices.Image.Appliers.ProcessImage(this.dataDoc, [this.fieldKey + "-generatedTagsDoc"], this.url, Service.ComputerVision, converter);
}
@computed private get url() {
- const data = Cast(this.dataDoc[this.props.fieldKey], ImageField);
+ const data = Cast(this.dataDoc[this.fieldKey], ImageField);
return data ? data.url.href : undefined;
}
@@ -209,7 +225,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
return url.href;
} else if (url.href.indexOf(window.location.origin) === -1) {
return Utils.CorsProxy(url.href);
- } else if (!(lower.endsWith(".png") || lower.endsWith(".jpg") || lower.endsWith(".jpeg"))) {
+ } else if (!/\.(png|jpg|jpeg|gif|webp)$/.test(lower)) {
return url.href;//Why is this here
}
const ext = path.extname(url.href);
@@ -232,37 +248,42 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
}
const original = StrCast(this.dataDoc.originalUrl);
if (error.type === "error" && original) {
- this.dataDoc[this.props.fieldKey] = new ImageField(original);
+ this.dataDoc[this.fieldKey] = new ImageField(original);
}
}
_curSuffix = "_m";
resize = (imgPath: string) => {
const cachedNativeSize = {
- width: NumCast(this.dataDoc[this.props.fieldKey + "-nativeWidth"]),
- height: NumCast(this.dataDoc[this.props.fieldKey + "-nativeHeight"])
+ width: NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"]),
+ height: NumCast(this.dataDoc[this.fieldKey + "-nativeHeight"])
};
- const cachedImgPath = this.dataDoc[this.props.fieldKey + "-imgPath"];
- if (!cachedNativeSize.width || !cachedNativeSize.height || imgPath !== cachedImgPath) {
- (!this.layoutDoc.isTemplateDoc || this.dataDoc !== this.layoutDoc) && requestImageSize(imgPath).then((inquiredSize: any) => {
- const rotation = NumCast(this.dataDoc[this.props.fieldKey + "-rotation"]) % 180;
- const rotatedNativeSize = rotation === 90 || rotation === 270 ? { height: inquiredSize.width, width: inquiredSize.height } : inquiredSize;
- const rotatedAspect = rotatedNativeSize.height / rotatedNativeSize.width;
- const docAspect = this.Document[HeightSym]() / this.Document[WidthSym]();
- setTimeout(action(() => {
- if (this.Document[WidthSym]() && (!cachedNativeSize.width || !cachedNativeSize.height || Math.abs(1 - docAspect / rotatedAspect) > 0.1)) {
- this.Document._height = this.Document[WidthSym]() * rotatedAspect;
- this.dataDoc[this.props.fieldKey + "-nativeWidth"] = this.Document._nativeWidth = rotatedNativeSize.width;
- this.dataDoc[this.props.fieldKey + "-nativeHeight"] = this.Document._nativeHeight = rotatedNativeSize.height;
- }
- this.dataDoc[this.props.fieldKey + "-imgPath"] = imgPath;
- }), 0);
- })
- .catch((err: any) => console.log(err));
- } else if (this.Document._nativeHeight !== cachedNativeSize.width || this.Document._nativeWidth !== cachedNativeSize.height) {
+ const docAspect = this.Document[HeightSym]() / this.Document[WidthSym]();
+ const cachedAspect = cachedNativeSize.height / cachedNativeSize.width;
+ if (!cachedNativeSize.width || !cachedNativeSize.height || Math.abs(NumCast(this.layoutDoc._width) / NumCast(this.layoutDoc._height) - cachedNativeSize.width / cachedNativeSize.height) > 0.05) {
+ if (!this.layoutDoc.isTemplateDoc || this.dataDoc !== this.layoutDoc) {
+ requestImageSize(imgPath).then((inquiredSize: any) => {
+ const rotation = NumCast(this.dataDoc[this.fieldKey + "-rotation"]) % 180;
+ const rotatedNativeSize = rotation === 90 || rotation === 270 ? { height: inquiredSize.width, width: inquiredSize.height } : inquiredSize;
+ const rotatedAspect = rotatedNativeSize.height / rotatedNativeSize.width;
+ setTimeout(action(() => {
+ if (this.Document[WidthSym]() && (!cachedNativeSize.width || !cachedNativeSize.height || Math.abs(1 - docAspect / rotatedAspect) > 0.1)) {
+ this.Document._height = this.Document[WidthSym]() * rotatedAspect;
+ this.dataDoc[this.fieldKey + "-nativeWidth"] = this.Document._nativeWidth = rotatedNativeSize.width;
+ this.dataDoc[this.fieldKey + "-nativeHeight"] = this.Document._nativeHeight = rotatedNativeSize.height;
+ }
+ }), 0);
+ }).catch((err: any) => console.log(err));
+ } else if (Math.abs(1 - docAspect / cachedAspect) > 0.1) {
+ this.Document._width = this.Document[WidthSym]() || cachedNativeSize.width;
+ this.Document._height = this.Document[WidthSym]() * cachedAspect;
+ }
+ } else if (this.Document._nativeWidth !== cachedNativeSize.width || this.Document._nativeHeight !== cachedNativeSize.height) {
!(this.Document[StrCast(this.props.Document.layoutKey)] instanceof Doc) && setTimeout(() => {
- this.Document._nativeWidth = cachedNativeSize.width;
- this.Document._nativeHeight = cachedNativeSize.height;
+ if (!(this.Document[StrCast(this.props.Document.layoutKey)] instanceof Doc)) {
+ this.Document._nativeWidth = cachedNativeSize.width;
+ this.Document._nativeHeight = cachedNativeSize.height;
+ }
}, 0);
}
}
@@ -270,7 +291,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
@action
onPointerEnter = () => {
const self = this;
- const audioAnnos = DocListCast(this.dataDoc[this.props.fieldKey + "-audioAnnotations"]);
+ const audioAnnos = DocListCast(this.dataDoc[this.fieldKey + "-audioAnnotations"]);
if (audioAnnos && audioAnnos.length && this._audioState === 0) {
const anno = audioAnnos[Math.floor(Math.random() * audioAnnos.length)];
anno.data instanceof AudioField && new Howl({
@@ -305,7 +326,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
@computed
private get considerDownloadIcon() {
- const data = this.dataDoc[this.props.fieldKey];
+ const data = this.dataDoc[this.fieldKey];
if (!(data instanceof ImageField)) {
return (null);
}
@@ -321,12 +342,12 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
const { dataDoc } = this;
const { success, failure, idle, loading } = uploadIcons;
runInAction(() => this.uploadIcon = loading);
- const [{ clientAccessPath }] = await Networking.PostToServer("/uploadRemoteImage", { sources: [primary] });
+ const [{ accessPaths }] = await Networking.PostToServer("/uploadRemoteImage", { sources: [primary] });
dataDoc.originalUrl = primary;
let succeeded = true;
let data: ImageField | undefined;
try {
- data = new ImageField(Utils.prepend(clientAccessPath));
+ data = new ImageField(Utils.prepend(accessPaths.agnostic.client));
} catch {
succeeded = false;
}
@@ -334,7 +355,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
setTimeout(action(() => {
this.uploadIcon = idle;
if (data) {
- dataDoc[this.props.fieldKey] = data;
+ dataDoc[this.fieldKey] = data;
}
}), 2000);
}}
@@ -344,8 +365,8 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
@computed get nativeSize() {
const pw = typeof this.props.PanelWidth === "function" ? this.props.PanelWidth() : typeof this.props.PanelWidth === "number" ? (this.props.PanelWidth as any) as number : 50;
- const nativeWidth = NumCast(this.dataDoc[this.props.fieldKey + "-nativeWidth"], pw);
- const nativeHeight = NumCast(this.dataDoc[this.props.fieldKey + "-nativeHeight"], 1);
+ const nativeWidth = NumCast(this.dataDoc[this.fieldKey + "-nativeWidth"], pw);
+ const nativeHeight = NumCast(this.dataDoc[this.fieldKey + "-nativeHeight"], 1);
return { nativeWidth, nativeHeight };
}
@@ -353,9 +374,9 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
let paths = [Utils.CorsProxy("http://www.cs.brown.edu/~bcz/noImage.png")];
// this._curSuffix = "";
// if (w > 20) {
- const alts = DocListCast(this.dataDoc[this.props.fieldKey + "-alternates"]);
+ const alts = DocListCast(this.dataDoc[this.fieldKey + "-alternates"]);
const altpaths = alts.filter(doc => doc.data instanceof ImageField).map(doc => this.choosePath((doc.data as ImageField).url));
- const field = this.dataDoc[this.props.fieldKey];
+ const field = this.dataDoc[this.fieldKey];
// if (w < 100 && this._smallRetryCount < 10) this._curSuffix = "_s";
// else if (w < 600 && this._mediumRetryCount < 10) this._curSuffix = "_m";
// else if (this._largeRetryCount < 10) this._curSuffix = "_l";
@@ -370,13 +391,13 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
const srcpath = this.paths[NumCast(this.props.Document.curPage, 0)];
const fadepath = this.paths[Math.min(1, this.paths.length - 1)];
const { nativeWidth, nativeHeight } = this.nativeSize;
- const rotation = NumCast(this.dataDoc[this.props.fieldKey + "-rotation"]);
+ const rotation = NumCast(this.dataDoc[this.fieldKey + "-rotation"]);
const aspect = (rotation % 180) ? this.Document[HeightSym]() / this.Document[WidthSym]() : 1;
const shift = (rotation % 180) ? (nativeHeight - nativeWidth / aspect) / 2 : 0;
- !this.Document.ignoreAspect && this.resize(srcpath);
+ this.resize(srcpath);
- return <div className="imageBox-cont" key={this.props.Document[Id]} ref={this.createDropTarget} onContextMenu={this.specificContextMenu}>
+ return <div className="imageBox-cont" key={this.props.Document[Id]} ref={this.createDropTarget}>
<div className="imageBox-fader" >
<img key={this._smallRetryCount + (this._mediumRetryCount << 4) + (this._largeRetryCount << 8)} // force cache to update on retrys
src={srcpath}
@@ -393,15 +414,16 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
ref={this._imgRef}
onError={this.onError} /></div>}
</div>
- <div className="imageBox-audioBackground"
- onPointerDown={this.audioDown}
- onPointerEnter={this.onPointerEnter}
- style={{ height: `calc(${.1 * nativeHeight / nativeWidth * 100}%)` }}
- >
- <FontAwesomeIcon className="imageBox-audioFont"
- style={{ color: [DocListCast(this.dataDoc[this.props.fieldKey + "-audioAnnotations"]).length ? "blue" : "gray", "green", "red"][this._audioState] }}
- icon={!DocListCast(this.dataDoc[this.props.fieldKey + "-audioAnnotations"]).length ? "microphone" : faFileAudio} size="sm" />
- </div>
+ {!this.props.Document._showAudio ? (null) :
+ <div className="imageBox-audioBackground"
+ onPointerDown={this.audioDown}
+ onPointerEnter={this.onPointerEnter}
+ style={{ height: `calc(${.1 * nativeHeight / nativeWidth * 100}%)` }}
+ >
+ <FontAwesomeIcon className="imageBox-audioFont"
+ style={{ color: [DocListCast(this.dataDoc[this.fieldKey + "-audioAnnotations"]).length ? "blue" : "gray", "green", "red"][this._audioState] }}
+ icon={!DocListCast(this.dataDoc[this.fieldKey + "-audioAnnotations"]).length ? "microphone" : faFileAudio} size="sm" />
+ </div>}
{this.considerDownloadIcon}
{this.considerGooglePhotosLink()}
<FaceRectangles document={this.dataDoc} color={"#0000FF"} backgroundColor={"#0000FF"} />
@@ -416,7 +438,9 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
style={{
transform: `scale(${this.props.ContentScaling()})`,
width: `${100 / this.props.ContentScaling()}%`,
- height: `${100 / this.props.ContentScaling()}%`
+ height: `${100 / this.props.ContentScaling()}%`,
+ pointerEvents: this.props.Document.isBackground ? "none" : undefined,
+ borderRadius: `${Number(StrCast(this.layoutDoc.borderRounding).replace("px", "")) / this.props.ContentScaling()}px`
}} >
<CollectionFreeFormView {...this.props}
PanelHeight={this.props.PanelHeight}
@@ -435,8 +459,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
CollectionView={undefined}
ScreenToLocalTransform={this.props.ScreenToLocalTransform}
renderDepth={this.props.renderDepth + 1}
- ContainingCollectionDoc={this.props.ContainingCollectionDoc}
- chromeCollapsed={true}>
+ ContainingCollectionDoc={this.props.ContainingCollectionDoc}>
{this.contentFunc}
</CollectionFreeFormView>
</div >);
diff --git a/src/client/views/nodes/KeyValueBox.scss b/src/client/views/nodes/KeyValueBox.scss
index 6e8a36c6a..a26880c9e 100644
--- a/src/client/views/nodes/KeyValueBox.scss
+++ b/src/client/views/nodes/KeyValueBox.scss
@@ -74,7 +74,7 @@ $header-height: 30px;
.keyValueBox-evenRow {
position: relative;
- display: inline-block;
+ display: flex;
width:100%;
height:$header-height;
background: $light-color;
@@ -114,7 +114,7 @@ $header-height: 30px;
.keyValueBox-oddRow {
position: relative;
- display: inline-block;
+ display: flex;
width:100%;
height:30px;
background: $light-color-secondary;
diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx
index e6b512adf..3d59ea61a 100644
--- a/src/client/views/nodes/KeyValuePair.tsx
+++ b/src/client/views/nodes/KeyValuePair.tsx
@@ -22,7 +22,7 @@ export interface KeyValuePairProps {
keyWidth: number;
PanelHeight: () => number;
PanelWidth: () => number;
- addDocTab: (doc: Doc, data: Opt<Doc>, where: string) => boolean;
+ addDocTab: (doc: Doc, where: string) => boolean;
}
@observer
export class KeyValuePair extends React.Component<KeyValuePairProps> {
@@ -46,7 +46,7 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
if (value instanceof Doc) {
e.stopPropagation();
e.preventDefault();
- ContextMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(value, { _width: 300, _height: 300 }), undefined, "onRight"), icon: "layer-group" });
+ ContextMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(value, { _width: 300, _height: 300 }), "onRight"), icon: "layer-group" });
ContextMenu.Instance.displayMenu(e.clientX, e.clientY);
}
}
@@ -61,6 +61,8 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
fieldKey: this.props.keyName,
isSelected: returnFalse,
select: emptyFunction,
+ dropAction:"alias",
+ bringToFront:emptyFunction,
renderDepth: 1,
active: returnFalse,
whenActiveChanged: emptyFunction,
diff --git a/src/client/views/nodes/LinkBox.scss b/src/client/views/nodes/LinkBox.scss
new file mode 100644
index 000000000..b5b8e660f
--- /dev/null
+++ b/src/client/views/nodes/LinkBox.scss
@@ -0,0 +1,3 @@
+.linkBox-container-interactive {
+ pointer-events: all;
+} \ No newline at end of file
diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx
new file mode 100644
index 000000000..0e327e130
--- /dev/null
+++ b/src/client/views/nodes/LinkBox.tsx
@@ -0,0 +1,35 @@
+import React = require("react");
+import { observer } from "mobx-react";
+import { documentSchema } from "../../../new_fields/documentSchemas";
+import { makeInterface, listSpec } from "../../../new_fields/Schema";
+import { returnFalse, returnZero } from "../../../Utils";
+import { CollectionTreeView } from "../collections/CollectionTreeView";
+import { DocExtendableComponent } from "../DocComponent";
+import { FieldView, FieldViewProps } from './FieldView';
+import "./LinkBox.scss";
+import { Cast } from "../../../new_fields/Types";
+
+type LinkDocument = makeInterface<[typeof documentSchema]>;
+const LinkDocument = makeInterface(documentSchema);
+
+@observer
+export class LinkBox extends DocExtendableComponent<FieldViewProps, LinkDocument>(LinkDocument) {
+ public static LayoutString(fieldKey: string) { return FieldView.LayoutString(LinkBox, fieldKey); }
+ render() {
+ return <div className={`linkBox-container${this.active() ? "-interactive" : ""}`}
+ onPointerDown={e => e.button === 0 && !e.ctrlKey && e.stopPropagation()}
+ style={{ background: this.props.backgroundColor?.(this.props.Document) }} >
+
+ <CollectionTreeView {...this.props}
+ ChromeHeight={returnZero}
+ overrideDocuments={[this.dataDoc]}
+ ignoreFields={Cast(this.props.Document.linkBoxExcludedKeys, listSpec("string"), null)}
+ annotationsKey={""}
+ CollectionView={undefined}
+ addDocument={returnFalse}
+ removeDocument={returnFalse}
+ moveDocument={returnFalse}>
+ </CollectionTreeView>
+ </div>;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss
index c7d6f988c..7a3d2e92b 100644
--- a/src/client/views/nodes/PDFBox.scss
+++ b/src/client/views/nodes/PDFBox.scss
@@ -46,7 +46,27 @@
border-radius: 3px;
pointer-events: all;
}
- }
+ }
+ .pdfBox-overlayButton-fwd,
+ .pdfBox-overlayButton-back {
+ background: #121721;
+ height: 25px;
+ width: 25px;
+ display: flex;
+ position: relative;
+ align-items: center;
+ justify-content: center;
+ border-radius: 3px;
+ pointer-events: all;
+ position: absolute;
+ top: 5;
+ }
+ .pdfBox-overlayButton-fwd {
+ left: 45;
+ }
+ .pdfBox-overlayButton-back {
+ left: 25;
+ }
.pdfBox-nextIcon,
.pdfBox-prevIcon {
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index e1c5fd27f..f8c008a2d 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -52,11 +52,11 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>
this._initialScale = this.props.ScreenToLocalTransform().Scale;
const nw = this.Document._nativeWidth = NumCast(this.dataDoc[this.props.fieldKey + "-nativeWidth"], NumCast(this.Document._nativeWidth, 927));
const nh = this.Document._nativeHeight = NumCast(this.dataDoc[this.props.fieldKey + "-nativeHeight"], NumCast(this.Document._nativeHeight, 1200));
- !this.Document._fitWidth && !this.Document.ignoreAspect && (this.Document._height = this.Document[WidthSym]() * (nh / nw));
+ !this.Document._fitWidth && (this.Document._height = this.Document[WidthSym]() * (nh / nw));
const backup = "oldPath";
const { Document } = this.props;
- const { url: { href } } = Cast(Document[this.props.fieldKey], PdfField)!;
+ const { url: { href } } = Cast(this.dataDoc[this.props.fieldKey], PdfField)!;
const pathCorrectionTest = /upload\_[a-z0-9]{32}.(.*)/g;
const matches = pathCorrectionTest.exec(href);
console.log("\nHere's the { url } being fed into the outer regex:");
@@ -78,9 +78,7 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>
}
}
- componentWillUnmount() {
- this._selectReactionDisposer && this._selectReactionDisposer();
- }
+ componentWillUnmount() { this._selectReactionDisposer?.(); }
componentDidMount() {
this._selectReactionDisposer = reaction(() => this.props.isSelected(),
() => {
@@ -93,14 +91,14 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>
this.dataDoc[this.props.fieldKey + "-numPages"] = np;
this.dataDoc[this.props.fieldKey + "-nativeWidth"] = this.Document._nativeWidth = nw * 96 / 72;
this.dataDoc[this.props.fieldKey + "-nativeHeight"] = this.Document._nativeHeight = nh * 96 / 72;
- !this.Document._fitWidth && !this.Document.ignoreAspect && (this.Document._height = this.Document[WidthSym]() * (nh / nw));
+ !this.Document._fitWidth && (this.Document._height = this.Document[WidthSym]() * (nh / nw));
}
- public search(string: string, fwd: boolean) { this._pdfViewer && this._pdfViewer.search(string, fwd); }
- public prevAnnotation() { this._pdfViewer && this._pdfViewer.prevAnnotation(); }
- public nextAnnotation() { this._pdfViewer && this._pdfViewer.nextAnnotation(); }
- public backPage() { this._pdfViewer!.gotoPage((this.Document.curPage || 1) - 1); }
- public forwardPage() { this._pdfViewer!.gotoPage((this.Document.curPage || 1) + 1); }
+ public search = (string: string, fwd: boolean) => { this._pdfViewer?.search(string, fwd); };
+ public prevAnnotation = () => { this._pdfViewer?.prevAnnotation(); };
+ public nextAnnotation = () => { this._pdfViewer?.nextAnnotation(); };
+ public backPage = () => { this._pdfViewer!.gotoPage((this.Document.curPage || 1) - 1); };
+ public forwardPage = () => { this._pdfViewer!.gotoPage((this.Document.curPage || 1) + 1); };
public gotoPage = (p: number) => { this._pdfViewer!.gotoPage(p); };
@undoBatch
@@ -140,12 +138,12 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>
settingsPanel() {
const pageBtns = <>
- <button className="pdfBox-overlayButton-iconCont" key="back" title="Page Back"
- onPointerDown={e => e.stopPropagation()} onClick={e => this.backPage()} style={{ left: 45, top: 5 }}>
+ <button className="pdfBox-overlayButton-back" key="back" title="Page Back"
+ onPointerDown={e => e.stopPropagation()} onClick={e => this.backPage()} >
<FontAwesomeIcon style={{ color: "white" }} icon={"arrow-left"} size="sm" />
</button>
- <button className="pdfBox-overlayButton-iconCont" key="fwd" title="Page Forward"
- onPointerDown={e => e.stopPropagation()} onClick={e => this.forwardPage()} style={{ left: 45, top: 5 }}>
+ <button className="pdfBox-overlayButton-fwd" key="fwd" title="Page Forward"
+ onPointerDown={e => e.stopPropagation()} onClick={e => this.forwardPage()} >
<FontAwesomeIcon style={{ color: "white" }} icon={"arrow-right"} size="sm" />
</button>
</>;
@@ -233,7 +231,7 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>
isChildActive = (outsideReaction?: boolean) => this._isChildActive;
@computed get renderPdfView() {
const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField);
- return <div className={"pdfBox"} onContextMenu={this.specificContextMenu}>
+ return <div className={"pdfBox"} onContextMenu={this.specificContextMenu} style={{ height: this.props.Document._scrollTop && !this.Document._fitWidth ? NumCast(this.Document._height) * this.props.PanelWidth() / NumCast(this.Document._width) : undefined }}>
<PDFViewer {...this.props} pdf={this._pdf!} url={pdfUrl!.url.pathname} active={this.props.active} loaded={this.loaded}
setPdfViewer={this.setPdfViewer} ContainingCollectionView={this.props.ContainingCollectionView}
renderDepth={this.props.renderDepth} PanelHeight={this.props.PanelHeight} PanelWidth={this.props.PanelWidth}
@@ -243,7 +241,7 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>
ScreenToLocalTransform={this.props.ScreenToLocalTransform} select={this.props.select}
isSelected={this.props.isSelected} whenActiveChanged={this.whenActiveChanged}
isChildActive={this.isChildActive}
- fieldKey={this.props.fieldKey} startupLive={this._initialScale < 2.5 ? true : false} />
+ fieldKey={this.props.fieldKey} startupLive={this._initialScale < 2.5 || this.props.Document._scrollTop ? true : false} />
{this.settingsPanel()}
</div>;
}
@@ -252,14 +250,14 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>
render() {
const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField, null);
if (this.props.isSelected() || this.props.Document.scrollY !== undefined) this._everActive = true;
- if (pdfUrl && (this._everActive || (this.dataDoc[this.props.fieldKey + "-nativeWidth"] && this.props.ScreenToLocalTransform().Scale < 2.5))) {
+ if (pdfUrl && (this._everActive || this.props.Document._scrollTop || (this.dataDoc[this.props.fieldKey + "-nativeWidth"] && this.props.ScreenToLocalTransform().Scale < 2.5))) {
if (pdfUrl instanceof PdfField && this._pdf) {
return this.renderPdfView;
}
if (!this._pdfjsRequested) {
this._pdfjsRequested = true;
const promise = Pdfjs.getDocument(pdfUrl.url.href).promise;
- promise.then(pdf => { runInAction(() => { this._pdf = pdf; console.log("promise"); }) });
+ promise.then(action(pdf => { this._pdf = pdf; console.log("promise"); }));
}
}
diff --git a/src/client/views/nodes/PresBox.scss b/src/client/views/nodes/PresBox.scss
index e5a79ab11..ba8389fda 100644
--- a/src/client/views/nodes/PresBox.scss
+++ b/src/client/views/nodes/PresBox.scss
@@ -2,32 +2,48 @@
position: absolute;
z-index: 2;
box-shadow: #AAAAAA .2vw .2vw .4vw;
- right: 0;
- top: 0;
bottom: 0;
width: 100%;
- min-width: 200px;
+ min-width: 120px;
height: 100%;
- min-height: 50px;
+ min-height: 41px;
letter-spacing: 2px;
overflow: hidden;
transition: 0.7s opacity ease;
pointer-events: all;
- .presBox-listCont {
- position: relative;
- padding-left: 10px;
- padding-right: 10px;
- }
-
.presBox-buttons {
padding: 10px;
width: 100%;
+ background: gray;
+ padding-right: 10px;
+ padding-top: 5px;
+ padding-bottom: 5px;
.presBox-button {
margin-right: 2.5%;
margin-left: 2.5%;
width: 20%;
border-radius: 5px;
}
+ .collectionViewBaseChrome-viewPicker {
+ min-width: 50;
+ width: 5%;
+ height: 25;
+ position: relative;
+ display: inline-block;
+ }
+ }
+ .presBox-backward, .presBox-forward {
+ width: 25px;
+ border-radius: 5px;
+ top:50%;
+ position: absolute;
+ display: inline-block;
+ }
+ .presBox-backward {
+ left:5;
+ }
+ .presBox-forward {
+ right:5;
}
} \ No newline at end of file
diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx
index 428e9aa7b..a39c337ca 100644
--- a/src/client/views/nodes/PresBox.tsx
+++ b/src/client/views/nodes/PresBox.tsx
@@ -1,29 +1,26 @@
import React = require("react");
import { library } from '@fortawesome/fontawesome-svg-core';
-import { faArrowLeft, faArrowRight, faEdit, faMinus, faPlay, faPlus, faStop, faTimes } from '@fortawesome/free-solid-svg-icons';
+import { faArrowLeft, faArrowRight, faEdit, faMinus, faPlay, faPlus, faStop, faHandPointLeft, faTimes } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, computed, reaction, IReactionDisposer } from "mobx";
+import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx";
import { observer } from "mobx-react";
-import { Doc, DocListCast, DocListCastAsync } from "../../../new_fields/Doc";
-import { listSpec } from "../../../new_fields/Schema";
-import { Cast, FieldValue, NumCast } from "../../../new_fields/Types";
-import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
+import { Doc, DocListCast } from "../../../new_fields/Doc";
+import { InkTool } from "../../../new_fields/InkField";
+import { BoolCast, Cast, FieldValue, NumCast } from "../../../new_fields/Types";
+import { returnFalse } from "../../../Utils";
import { DocumentManager } from "../../util/DocumentManager";
import { undoBatch } from "../../util/UndoManager";
-import { CollectionViewType } from "../collections/CollectionView";
import { CollectionDockingView } from "../collections/CollectionDockingView";
-import { CollectionView } from "../collections/CollectionView";
-import { ContextMenu } from "../ContextMenu";
+import { CollectionView, CollectionViewType } from "../collections/CollectionView";
+import { InkingControl } from "../InkingControl";
import { FieldView, FieldViewProps } from './FieldView';
import "./PresBox.scss";
-import { DocumentType } from "../../documents/DocumentTypes";
-import { Docs } from "../../documents/Documents";
-import { ComputedField } from "../../../new_fields/ScriptField";
library.add(faArrowLeft);
library.add(faArrowRight);
library.add(faPlay);
library.add(faStop);
+library.add(faHandPointLeft);
library.add(faPlus);
library.add(faTimes);
library.add(faMinus);
@@ -32,95 +29,71 @@ library.add(faEdit);
@observer
export class PresBox extends React.Component<FieldViewProps> {
public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PresBox, fieldKey); }
- _docListChangedReaction: IReactionDisposer | undefined;
+ _childReaction: IReactionDisposer | undefined;
+ @observable _isChildActive = false;
componentDidMount() {
- this._docListChangedReaction = reaction(() => {
- const value = FieldValue(Cast(this.props.Document[this.props.fieldKey], listSpec(Doc)));
- return value ? value.slice() : value;
- }, () => {
- const value = FieldValue(Cast(this.props.Document[this.props.fieldKey], listSpec(Doc)));
- if (value) {
- value.forEach((item, i) => {
- if (item instanceof Doc && item.type !== DocumentType.PRESELEMENT) {
- const pinDoc = Docs.Create.PresElementBoxDocument({ backgroundColor: "transparent" });
- Doc.GetProto(pinDoc).presentationTargetDoc = item;
- Doc.GetProto(pinDoc).title = ComputedField.MakeFunction('this.presentationTargetDoc?.title?.toString()');
- value.splice(i, 1, pinDoc);
- }
- });
- }
- });
+ this.props.Document._forceRenderEngine = "timeline";
+ this.props.Document._replacedChrome = "replaced";
+ this._childReaction = reaction(() => this.childDocs.slice(), (children) => children.forEach((child, i) => child.presentationIndex = i), { fireImmediately: true });
}
-
componentWillUnmount() {
- this._docListChangedReaction && this._docListChangedReaction();
+ this._childReaction?.();
}
@computed get childDocs() { return DocListCast(this.props.Document[this.props.fieldKey]); }
+ @computed get currentIndex() { return NumCast(this.props.Document._itemIndex); }
+
+ updateCurrentPresentation = action(() => Doc.UserDoc().curPresentation = this.props.Document);
- next = async () => {
- const current = NumCast(this.props.Document.selectedDoc);
- //asking to get document at current index
- const docAtCurrentNext = await this.getDocAtIndex(current + 1);
- if (docAtCurrentNext !== undefined) {
- const presDocs = DocListCast(this.props.Document[this.props.fieldKey]);
- let nextSelected = current + 1;
+ next = () => {
+ this.updateCurrentPresentation();
+ if (this.childDocs[this.currentIndex + 1] !== undefined) {
+ let nextSelected = this.currentIndex + 1;
- for (; nextSelected < presDocs.length - 1; nextSelected++) {
- if (!presDocs[nextSelected + 1].groupButton) {
+ for (; nextSelected < this.childDocs.length - 1; nextSelected++) {
+ if (!this.childDocs[nextSelected + 1].groupButton) {
break;
}
}
- this.gotoDocument(nextSelected, current);
+ this.gotoDocument(nextSelected, this.currentIndex);
}
}
- back = async () => {
- const current = NumCast(this.props.Document.selectedDoc);
- //requesting for the doc at current index
- const docAtCurrent = await this.getDocAtIndex(current);
- if (docAtCurrent !== undefined) {
-
- //asking for its presentation id.
- let prevSelected = current;
- let zoomOut: boolean = false;
-
- const presDocs = await DocListCastAsync(this.props.Document[this.props.fieldKey]);
- const currentsArray: Doc[] = [];
- for (; presDocs && prevSelected > 0 && presDocs[prevSelected].groupButton; prevSelected--) {
- currentsArray.push(presDocs[prevSelected]);
+ back = () => {
+ this.updateCurrentPresentation();
+ const docAtCurrent = this.childDocs[this.currentIndex];
+ if (docAtCurrent) {
+ //check if any of the group members had used zooming in including the current document
+ //If so making sure to zoom out, which goes back to state before zooming action
+ let prevSelected = this.currentIndex;
+ let didZoom = docAtCurrent.zoomButton;
+ for (; !didZoom && prevSelected > 0 && this.childDocs[prevSelected].groupButton; prevSelected--) {
+ didZoom = this.childDocs[prevSelected].zoomButton;
}
prevSelected = Math.max(0, prevSelected - 1);
- //checking if any of the group members had used zooming in
- currentsArray.forEach((doc: Doc) => {
- if (doc.showButton) {
- zoomOut = true;
- return;
- }
- });
-
- // if a group set that flag to zero or a single element
- //If so making sure to zoom out, which goes back to state before zooming action
- if (current > 0) {
- if (zoomOut || docAtCurrent.showButton) {
- const prevScale = NumCast(this.childDocs[prevSelected].viewScale, null);
- const curScale = DocumentManager.Instance.getScaleOfDocView(this.childDocs[current]);
- if (prevScale !== undefined && prevScale !== curScale) {
- DocumentManager.Instance.zoomIntoScale(docAtCurrent, prevScale);
- }
+ if (this.currentIndex > 0 && didZoom) {
+ const prevScale = NumCast(this.childDocs[prevSelected].viewScale);
+ const curScale = DocumentManager.Instance.getScaleOfDocView(docAtCurrent);
+ if (prevScale && prevScale !== curScale) {
+ DocumentManager.Instance.zoomIntoScale(docAtCurrent, prevScale);
}
}
- this.gotoDocument(prevSelected, current);
+ this.gotoDocument(prevSelected, this.currentIndex);
}
}
+ whenActiveChanged = action((isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive));
+ active = (outsideReaction?: boolean) => ((InkingControl.Instance.selectedTool === InkTool.None && !this.props.Document.isBackground) &&
+ (this.props.Document.forceActive || this.props.isSelected(outsideReaction) || this._isChildActive || this.props.renderDepth === 0) ? true : false)
+
/**
* This is the method that checks for the actions that need to be performed
* after the document has been presented, which involves 3 button options:
* Hide Until Presented, Hide After Presented, Fade After Presented
*/
showAfterPresented = (index: number) => {
+ this.updateCurrentPresentation();
this.childDocs.forEach((doc, ind) => {
//the order of cases is aligned based on priority
if (doc.hideTillShownButton && ind <= index) {
@@ -141,6 +114,7 @@ export class PresBox extends React.Component<FieldViewProps> {
* Hide Until Presented, Hide After Presented, Fade After Presented
*/
hideIfNotPresented = (index: number) => {
+ this.updateCurrentPresentation();
this.childDocs.forEach((key, ind) => {
//the order of cases is aligned based on priority
@@ -162,6 +136,7 @@ export class PresBox extends React.Component<FieldViewProps> {
* te option open, navigates to that element.
*/
navigateToElement = async (curDoc: Doc, fromDocIndex: number) => {
+ this.updateCurrentPresentation();
const fromDoc = this.childDocs[fromDocIndex].presentationTargetDoc as Doc;
let docToJump = curDoc;
let willZoom = false;
@@ -181,22 +156,24 @@ export class PresBox extends React.Component<FieldViewProps> {
docToJump = doc;
willZoom = false;
}
- if (doc.showButton) {
+ if (doc.zoomButton) {
docToJump = doc;
willZoom = true;
}
});
//docToJump stayed same meaning, it was not in the group or was the last element in the group
+ const aliasOf = await Cast(docToJump.aliasOf, Doc);
+ const srcContext = aliasOf && await Cast(aliasOf.context, Doc);
if (docToJump === curDoc) {
//checking if curDoc has navigation open
- const target = await curDoc.presentationTargetDoc as Doc;
- if (curDoc.navButton) {
- DocumentManager.Instance.jumpToDocument(target, false);
- } else if (curDoc.showButton) {
+ const target = await Cast(curDoc.presentationTargetDoc, Doc);
+ if (curDoc.navButton && target) {
+ DocumentManager.Instance.jumpToDocument(target, false, undefined, srcContext);
+ } else if (curDoc.zoomButton && target) {
const curScale = DocumentManager.Instance.getScaleOfDocView(fromDoc);
//awaiting jump so that new scale can be found, since jumping is async
- await DocumentManager.Instance.jumpToDocument(target, true);
+ await DocumentManager.Instance.jumpToDocument(target, true, undefined, srcContext);
curDoc.viewScale = DocumentManager.Instance.getScaleOfDocView(target);
//saving the scale user was on before zooming in
@@ -210,7 +187,8 @@ export class PresBox extends React.Component<FieldViewProps> {
const curScale = DocumentManager.Instance.getScaleOfDocView(fromDoc);
//awaiting jump so that new scale can be found, since jumping is async
- await DocumentManager.Instance.jumpToDocument(await docToJump.presentationTargetDoc as Doc, willZoom);
+ const presTargetDoc = await docToJump.presentationTargetDoc as Doc;
+ await DocumentManager.Instance.jumpToDocument(presTargetDoc, willZoom, undefined, srcContext);
const newScale = DocumentManager.Instance.getScaleOfDocView(await curDoc.presentationTargetDoc as Doc);
curDoc.viewScale = newScale;
//saving the scale that user was on
@@ -220,86 +198,64 @@ export class PresBox extends React.Component<FieldViewProps> {
}
- /**
- * Async function that supposedly return the doc that is located at given index.
- */
- getDocAtIndex = async (index: number) => {
- const list = FieldValue(Cast(this.props.Document[this.props.fieldKey], listSpec(Doc)));
- if (list && index >= 0 && index < list.length) {
- this.props.Document.selectedDoc = index;
- //awaiting async call to finish to get Doc instance
- return list[index];
- }
- return undefined;
- }
-
@undoBatch
public removeDocument = (doc: Doc) => {
- const value = FieldValue(Cast(this.props.Document[this.props.fieldKey], listSpec(Doc)));
- if (value) {
- const indexOfDoc = value.indexOf(doc);
- if (indexOfDoc !== - 1) {
- value.splice(indexOfDoc, 1)[0];
- return true;
- }
- }
- return false;
+ return Doc.RemoveDocFromList(this.props.Document, this.props.fieldKey, doc);
}
//The function that is called when a document is clicked or reached through next or back.
//it'll also execute the necessary actions if presentation is playing.
- @action
- public gotoDocument = async (index: number, fromDoc: number) => {
+ public gotoDocument = (index: number, fromDoc: number) => {
+ this.updateCurrentPresentation();
Doc.UnBrushAllDocs();
- const list = FieldValue(Cast(this.props.Document[this.props.fieldKey], listSpec(Doc)));
- if (list && index >= 0 && index < list.length) {
- this.props.Document.selectedDoc = index;
+ if (index >= 0 && index < this.childDocs.length) {
+ this.props.Document._itemIndex = index;
if (!this.props.Document.presStatus) {
this.props.Document.presStatus = true;
this.startPresentation(index);
}
- const doc = await list[index];
- if (this.props.Document.presStatus) {
- this.navigateToElement(doc, fromDoc);
- this.hideIfNotPresented(index);
- this.showAfterPresented(index);
- }
+ this.navigateToElement(this.childDocs[index], fromDoc);
+ this.hideIfNotPresented(index);
+ this.showAfterPresented(index);
}
}
//The function that starts or resets presentaton functionally, depending on status flag.
- @action
startOrResetPres = () => {
+ this.updateCurrentPresentation();
if (this.props.Document.presStatus) {
this.resetPresentation();
} else {
this.props.Document.presStatus = true;
this.startPresentation(0);
- this.gotoDocument(0, NumCast(this.props.Document.selectedDoc));
+ this.gotoDocument(0, this.currentIndex);
}
}
+ addDocument = (doc: Doc) => {
+ const newPinDoc = Doc.MakeAlias(doc);
+ newPinDoc.presentationTargetDoc = doc;
+ return Doc.AddDocToList(this.props.Document, this.props.fieldKey, newPinDoc);
+ }
+
+
//The function that resets the presentation by removing every action done by it. It also
//stops the presentaton.
- @action
resetPresentation = () => {
- this.childDocs.forEach((doc: Doc) => {
- doc.opacity = 1;
- doc.viewScale = 1;
- });
- this.props.Document.selectedDoc = 0;
+ this.updateCurrentPresentation();
+ this.childDocs.forEach(doc => doc.opacity = doc.viewScale = 1);
+ this.props.Document._itemIndex = 0;
this.props.Document.presStatus = false;
- if (this.childDocs.length !== 0) {
- DocumentManager.Instance.zoomIntoScale(this.childDocs[0], 1);
- }
+ this.childDocs.length && DocumentManager.Instance.zoomIntoScale(this.childDocs[0], 1);
}
//The function that starts the presentation, also checking if actions should be applied
//directly at start.
startPresentation = (startIndex: number) => {
+ this.updateCurrentPresentation();
this.childDocs.map(doc => {
if (doc.hideTillShownButton && this.childDocs.indexOf(doc) > startIndex) {
doc.opacity = 0;
@@ -313,70 +269,81 @@ export class PresBox extends React.Component<FieldViewProps> {
});
}
- toggleMinimize = undoBatch(action((e: React.PointerEvent) => {
- if (this.props.Document.inOverlay) {
- Doc.RemoveDocFromList((CurrentUserUtils.UserDocument.overlays as Doc), this.props.fieldKey, this.props.Document);
- CollectionDockingView.AddRightSplit(this.props.Document, this.props.DataDoc);
- this.props.Document.inOverlay = false;
- } else {
- this.props.Document.x = e.clientX + 25;
- this.props.Document.y = e.clientY - 25;
- this.props.addDocTab && this.props.addDocTab(this.props.Document, this.props.DataDoc, "close");
- Doc.AddDocToList((CurrentUserUtils.UserDocument.overlays as Doc), this.props.fieldKey, this.props.Document);
+ updateMinimize = undoBatch(action((e: React.ChangeEvent, mode: number) => {
+ if (BoolCast(this.props.Document.inOverlay) !== (mode === CollectionViewType.Invalid)) {
+ if (this.props.Document.inOverlay) {
+ Doc.RemoveDocFromList((Doc.UserDoc().overlays as Doc), undefined, this.props.Document);
+ CollectionDockingView.AddRightSplit(this.props.Document);
+ this.props.Document.inOverlay = false;
+ } else {
+ this.props.Document.x = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0)[0];// 500;//e.clientX + 25;
+ this.props.Document.y = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0)[1];////e.clientY - 25;
+ this.props.addDocTab?.(this.props.Document, "close");
+ Doc.AddDocToList((Doc.UserDoc().overlays as Doc), undefined, this.props.Document);
+ }
}
}));
- specificContextMenu = (e: React.MouseEvent): void => {
- ContextMenu.Instance.addItem({ description: "Make Current Presentation", event: action(() => Doc.UserDoc().curPresentation = this.props.Document), icon: "asterisk" });
- }
-
/**
* Initially every document starts with a viewScale 1, which means
* that they will be displayed in a canvas with scale 1.
*/
- @action
initializeScaleViews = (docList: Doc[], viewtype: number) => {
- this.props.Document._chromeStatus = "disabled";
- const hgt = (viewtype === CollectionViewType.Tree) ? 50 : 72;
- docList.forEach((doc: Doc) => {
- doc.presBox = this.props.Document;
- doc.presBoxKey = this.props.fieldKey;
- doc.collapsedHeight = hgt;
- doc._height = ComputedField.MakeFunction("this.collapsedHeight + Number(this.embedOpen ? 100:0)");
- const curScale = NumCast(doc.viewScale, null);
- if (curScale === undefined) {
- doc.viewScale = 1;
- }
+ const hgt = (viewtype === CollectionViewType.Tree) ? 50 : 46;
+ docList.forEach(doc => {
+ doc.presBox = this.props.Document; // give contained documents a reference to the presentation
+ doc.collapsedHeight = hgt; // set the collpased height for documents based on the type of view (Tree or Stack) they will be displaye din
+ !NumCast(doc.viewScale) && (doc.viewScale = 1);
});
}
-
selectElement = (doc: Doc) => {
- const index = DocListCast(this.props.Document[this.props.fieldKey]).indexOf(doc);
- index !== -1 && this.gotoDocument(index, NumCast(this.props.Document.selectedDoc));
+ this.gotoDocument(this.childDocs.indexOf(doc), NumCast(this.props.Document._itemIndex));
}
getTransform = () => {
- return this.props.ScreenToLocalTransform().translate(-10, -50);// listBox padding-left and pres-box-cont minHeight
+ return this.props.ScreenToLocalTransform().translate(-5, -65);// listBox padding-left and pres-box-cont minHeight
}
+ panelHeight = () => {
+ return this.props.PanelHeight() - 20;
+ }
+
+ @undoBatch
+ viewChanged = action((e: React.ChangeEvent) => {
+ //@ts-ignore
+ this.props.Document._viewType = Number(e.target.selectedOptions[0].value);
+ this.props.Document._viewType === CollectionViewType.Stacking && (this.props.Document._pivotField = undefined); // pivot field may be set by the user in timeline view (or some other way) -- need to reset it here
+ this.updateMinimize(e, Number(this.props.Document._viewType));
+ });
+
+ childLayoutTemplate = () => this.props.Document._viewType === CollectionViewType.Stacking ? Cast(Doc.UserDoc().presentationTemplate, Doc, null) : undefined;
render() {
- this.initializeScaleViews(this.childDocs, NumCast(this.props.Document._viewType));
- return (
- <div className="presBox-cont" onContextMenu={this.specificContextMenu}>
- <div className="presBox-buttons">
- <button className="presBox-button" title="Back" onClick={this.back}><FontAwesomeIcon icon={"arrow-left"} /></button>
- <button className="presBox-button" title={"Reset Presentation" + this.props.Document.presStatus ? "" : " From Start"} onClick={this.startOrResetPres}>
- <FontAwesomeIcon icon={this.props.Document.presStatus ? "stop" : "play"} />
- </button>
- <button className="presBox-button" title="Next" onClick={this.next}><FontAwesomeIcon icon={"arrow-right"} /></button>
- <button className="presBox-button" title={this.props.Document.inOverlay ? "Expand" : "Minimize"} onClick={this.toggleMinimize}><FontAwesomeIcon icon={"eye"} /></button>
- </div>
- {this.props.Document.inOverlay ? (null) :
- <div className="presBox-listCont" >
- <CollectionView {...this.props} focus={this.selectElement} ScreenToLocalTransform={this.getTransform} />
- </div>
+ const mode = NumCast(this.props.Document._viewType, CollectionViewType.Invalid);
+ this.initializeScaleViews(this.childDocs, mode);
+ return <div className="presBox-cont" style={{ minWidth: this.props.Document.inOverlay ? 240 : undefined, pointerEvents: this.active() || this.props.Document.inOverlay ? "all" : "none" }} >
+ <div className="presBox-buttons" style={{ display: this.props.Document._chromeStatus === "disabled" ? "none" : undefined }}>
+ <select className="collectionViewBaseChrome-viewPicker"
+ onPointerDown={e => e.stopPropagation()}
+ onChange={this.viewChanged}
+ value={mode}>
+ <option className="collectionViewBaseChrome-viewOption" onPointerDown={e => e.stopPropagation()} value={CollectionViewType.Invalid}>Min</option>
+ <option className="collectionViewBaseChrome-viewOption" onPointerDown={e => e.stopPropagation()} value={CollectionViewType.Stacking}>List</option>
+ <option className="collectionViewBaseChrome-viewOption" onPointerDown={e => e.stopPropagation()} value={CollectionViewType.Time}>Time</option>
+ <option className="collectionViewBaseChrome-viewOption" onPointerDown={e => e.stopPropagation()} value={CollectionViewType.Carousel}>Slides</option>
+ </select>
+ <button className="presBox-button" title="Back" onClick={this.back}><FontAwesomeIcon icon={"arrow-left"} /></button>
+ <button className="presBox-button" title={"Reset Presentation" + this.props.Document.presStatus ? "" : " From Start"} onClick={this.startOrResetPres}>
+ <FontAwesomeIcon icon={this.props.Document.presStatus ? "stop" : "play"} />
+ </button>
+ <button className="presBox-button" title="Next" onClick={this.next}><FontAwesomeIcon icon={"arrow-right"} /></button>
+ </div>
+ <div className="presBox-listCont" >
+ {mode !== CollectionViewType.Invalid ?
+ <CollectionView {...this.props} PanelHeight={this.panelHeight} moveDocument={returnFalse} childLayoutTemplate={this.childLayoutTemplate}
+ addDocument={this.addDocument} removeDocument={returnFalse} focus={this.selectElement} ScreenToLocalTransform={this.getTransform} />
+ : (null)
}
</div>
- );
+ </div>;
}
} \ No newline at end of file
diff --git a/src/client/views/nodes/RadialMenu.tsx b/src/client/views/nodes/RadialMenu.tsx
index a6fb72a7b..0ffed78de 100644
--- a/src/client/views/nodes/RadialMenu.tsx
+++ b/src/client/views/nodes/RadialMenu.tsx
@@ -5,6 +5,8 @@ import { RadialMenuItem, RadialMenuProps } from "./RadialMenuItem";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import Measure from "react-measure";
import "./RadialMenu.scss";
+import MobileInkOverlay from "../../../mobile/MobileInkOverlay";
+import MobileInterface from "../../../mobile/MobileInterface";
@observer
export class RadialMenu extends React.Component {
@@ -23,6 +25,8 @@ export class RadialMenu extends React.Component {
@observable private _mouseDown: boolean = false;
private _reactionDisposer?: IReactionDisposer;
+ public used: boolean = false;
+
catchTouch = (te: React.TouchEvent) => {
console.log("caught");
@@ -35,6 +39,7 @@ export class RadialMenu extends React.Component {
this._mouseDown = true;
this._mouseX = e.clientX;
this._mouseY = e.clientY;
+ this.used = false;
document.addEventListener("pointermove", this.onPointerMove);
}
@@ -68,6 +73,7 @@ export class RadialMenu extends React.Component {
}
@action
onPointerUp = (e: PointerEvent) => {
+ this.used = true;
this._mouseDown = false;
const curX = e.clientX;
const curY = e.clientY;
@@ -76,8 +82,8 @@ export class RadialMenu extends React.Component {
}
this._shouldDisplay && (this._display = true);
document.removeEventListener("pointermove", this.onPointerMove);
- if (this._closest !== -1) {
- this._items[this._closest]?.event();
+ if (this._closest !== -1 && this._items?.length > this._closest) {
+ this._items[this._closest].event();
}
}
componentWillUnmount() {
@@ -213,7 +219,7 @@ export class RadialMenu extends React.Component {
render() {
- if (!this._display) {
+ if (!this._display || MobileInterface.Instance) {
return null;
}
const style = this._yRelativeToTop ? { left: this._pageX - 130, top: this._pageY - 130 } :
diff --git a/src/client/views/nodes/RadialMenuItem.tsx b/src/client/views/nodes/RadialMenuItem.tsx
index fdc732d3f..bd5b3bff4 100644
--- a/src/client/views/nodes/RadialMenuItem.tsx
+++ b/src/client/views/nodes/RadialMenuItem.tsx
@@ -44,12 +44,12 @@ export class RadialMenuItem extends React.Component<RadialMenuProps> {
setcircle() {
let circlemin = 0;
- let circlemax = 1
+ let circlemax = 1;
this.props.min ? circlemin = this.props.min : null;
this.props.max ? circlemax = this.props.max : null;
if (document.getElementById("myCanvas") !== null) {
- var c: any = document.getElementById("myCanvas");
- let color = "white"
+ const c: any = document.getElementById("myCanvas");
+ let color = "white";
switch (circlemin % 3) {
case 1:
color = "#c2c2c5";
@@ -70,38 +70,38 @@ export class RadialMenuItem extends React.Component<RadialMenuProps> {
}
if (c.getContext) {
- var ctx = c.getContext("2d");
+ const ctx = c.getContext("2d");
ctx.beginPath();
ctx.arc(150, 150, 150, (circlemin / circlemax) * 2 * Math.PI, ((circlemin + 1) / circlemax) * 2 * Math.PI);
ctx.arc(150, 150, 50, ((circlemin + 1) / circlemax) * 2 * Math.PI, (circlemin / circlemax) * 2 * Math.PI, true);
ctx.fillStyle = color;
- ctx.fill()
+ ctx.fill();
}
}
}
calculatorx() {
let circlemin = 0;
- let circlemax = 1
+ let circlemax = 1;
this.props.min ? circlemin = this.props.min : null;
this.props.max ? circlemax = this.props.max : null;
- let avg = ((circlemin / circlemax) + ((circlemin + 1) / circlemax)) / 2;
- let degrees = 360 * avg;
- let x = 100 * Math.cos(degrees * Math.PI / 180);
- let y = -125 * Math.sin(degrees * Math.PI / 180);
+ const avg = ((circlemin / circlemax) + ((circlemin + 1) / circlemax)) / 2;
+ const degrees = 360 * avg;
+ const x = 100 * Math.cos(degrees * Math.PI / 180);
+ const y = -125 * Math.sin(degrees * Math.PI / 180);
return x;
}
calculatory() {
let circlemin = 0;
- let circlemax = 1
+ let circlemax = 1;
this.props.min ? circlemin = this.props.min : null;
this.props.max ? circlemax = this.props.max : null;
- let avg = ((circlemin / circlemax) + ((circlemin + 1) / circlemax)) / 2;
- let degrees = 360 * avg;
- let x = 125 * Math.cos(degrees * Math.PI / 180);
- let y = -100 * Math.sin(degrees * Math.PI / 180);
+ const avg = ((circlemin / circlemax) + ((circlemin + 1) / circlemax)) / 2;
+ const degrees = 360 * avg;
+ const x = 125 * Math.cos(degrees * Math.PI / 180);
+ const y = -100 * Math.sin(degrees * Math.PI / 180);
return y;
}
diff --git a/src/client/views/nodes/ScreenshotBox.scss b/src/client/views/nodes/ScreenshotBox.scss
new file mode 100644
index 000000000..6cc184948
--- /dev/null
+++ b/src/client/views/nodes/ScreenshotBox.scss
@@ -0,0 +1,55 @@
+.screenshotBox {
+ pointer-events: all;
+ 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
+ // }
+ // .inkingCanvas-paths-markers {
+ // opacity : 0.4; // we shouldn't have to do this, but since chrome crawls to a halt with z-index unset in videoBox-content, this is a workaround
+ // }
+}
+
+.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-interactive, .screenshotBox-content-fullScreen {
+ pointer-events: all;
+}
+
+.screenshotBox-uiButtons {
+ background:dimgray;
+ border: orange solid 1px;
+ position: absolute;
+ right: 25;
+ top: 0;
+ width:50;
+ 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;
+ left: 5px;
+ position: absolute;
+ background-color:rgba(50, 50, 50, 0.2);
+ transform-origin: left top;
+ pointer-events:all;
+ }
+}
diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx
new file mode 100644
index 000000000..7c58a5148
--- /dev/null
+++ b/src/client/views/nodes/ScreenshotBox.tsx
@@ -0,0 +1,194 @@
+import React = require("react");
+import { library } from "@fortawesome/fontawesome-svg-core";
+import { faVideo } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { action, computed, IReactionDisposer, observable, runInAction } from "mobx";
+import { observer } from "mobx-react";
+import * as rp from 'request-promise';
+import { documentSchema, positionSchema } from "../../../new_fields/documentSchemas";
+import { makeInterface } from "../../../new_fields/Schema";
+import { ScriptField } from "../../../new_fields/ScriptField";
+import { Cast, StrCast } from "../../../new_fields/Types";
+import { VideoField } from "../../../new_fields/URLField";
+import { emptyFunction, returnFalse, returnOne, Utils } from "../../../Utils";
+import { Docs, DocUtils } from "../../documents/Documents";
+import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
+import { ContextMenu } from "../ContextMenu";
+import { ContextMenuProps } from "../ContextMenuItem";
+import { DocAnnotatableComponent } from "../DocComponent";
+import { InkingControl } from "../InkingControl";
+import { FieldView, FieldViewProps } from './FieldView';
+import "./ScreenshotBox.scss";
+const path = require('path');
+
+type ScreenshotDocument = makeInterface<[typeof documentSchema, typeof positionSchema]>;
+const ScreenshotDocument = makeInterface(documentSchema, positionSchema);
+
+library.add(faVideo);
+
+@observer
+export class ScreenshotBox extends DocAnnotatableComponent<FieldViewProps, ScreenshotDocument>(ScreenshotDocument) {
+ private _reactionDisposer?: IReactionDisposer;
+ private _videoRef: HTMLVideoElement | null = null;
+ public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ScreenshotBox, fieldKey); }
+
+ public get player(): HTMLVideoElement | null {
+ return this._videoRef;
+ }
+
+ videoLoad = () => {
+ const aspect = this.player!.videoWidth / this.player!.videoHeight;
+ const nativeWidth = (this.Document._nativeWidth || 0);
+ const nativeHeight = (this.Document._nativeHeight || 0);
+ if (!nativeWidth || !nativeHeight) {
+ if (!this.Document._nativeWidth) this.Document._nativeWidth = 400;
+ this.Document._nativeHeight = (this.Document._nativeWidth || 0) / aspect;
+ this.Document._height = (this.Document._width || 0) / aspect;
+ }
+ if (!this.Document.duration) this.Document.duration = this.player!.duration;
+ }
+
+ @action public Snapshot() {
+ const width = this.Document._width || 0;
+ const height = this.Document._height || 0;
+ const canvas = document.createElement('canvas');
+ canvas.width = 640;
+ canvas.height = 640 * (this.Document._nativeHeight || 0) / (this.Document._nativeWidth || 1);
+ const ctx = canvas.getContext('2d');//draw image to canvas. scale to target dimensions
+ if (ctx) {
+ ctx.rect(0, 0, canvas.width, canvas.height);
+ ctx.fillStyle = "blue";
+ ctx.fill();
+ this._videoRef && ctx.drawImage(this._videoRef, 0, 0, canvas.width, canvas.height);
+ }
+
+ if (this._videoRef) {
+ //convert to desired file format
+ const dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png'
+ // if you want to preview the captured image,
+ const filename = path.basename(encodeURIComponent("screenshot" + Utils.GenerateGuid().replace(/\..*$/, "").replace(" ", "_")));
+ ScreenshotBox.convertDataUri(dataUrl, filename).then(returnedFilename => {
+ setTimeout(() => {
+ if (returnedFilename) {
+ const imageSummary = Docs.Create.ImageDocument(Utils.prepend(returnedFilename), {
+ x: (this.Document.x || 0) + width, y: (this.Document.y || 0),
+ _width: 150, _height: height / width * 150, title: "--screenshot--"
+ });
+ this.props.addDocument?.(imageSummary);
+ }
+ }, 500);
+ });
+ }
+ }
+
+ componentDidMount() {
+ }
+
+ componentWillUnmount() {
+ this._reactionDisposer && this._reactionDisposer();
+ }
+
+ @action
+ setVideoRef = (vref: HTMLVideoElement | null) => {
+ this._videoRef = vref;
+ }
+
+ public static async convertDataUri(imageUri: string, returnedFilename: string) {
+ try {
+ const posting = Utils.prepend("/uploadURI");
+ const returnedUri = await rp.post(posting, {
+ body: {
+ uri: imageUri,
+ name: returnedFilename
+ },
+ json: true,
+ });
+ return returnedUri;
+
+ } catch (e) {
+ console.log(e);
+ }
+ }
+ @observable _screenCapture = false;
+ specificContextMenu = (e: React.MouseEvent): void => {
+ const field = Cast(this.dataDoc[this.props.fieldKey], VideoField);
+ if (field) {
+ const url = field.url.href;
+ const subitems: ContextMenuProps[] = [];
+ subitems.push({ description: "Take Snapshot", event: () => this.Snapshot(), icon: "expand-arrows-alt" });
+ subitems.push({
+ description: "Screen Capture", event: (async () => {
+ runInAction(() => this._screenCapture = !this._screenCapture);
+ this._videoRef!.srcObject = !this._screenCapture ? undefined : await (navigator.mediaDevices as any).getDisplayMedia({ video: true });
+ }), icon: "expand-arrows-alt"
+ });
+ ContextMenu.Instance.addItem({ description: "Screenshot Funcs...", subitems: subitems, icon: "video" });
+ }
+ }
+
+ @computed get content() {
+ const interactive = InkingControl.Instance.selectedTool || !this.props.isSelected() ? "" : "-interactive";
+ const style = "videoBox-content" + interactive;
+ return <video className={`${style}`} key="video" autoPlay={this._screenCapture} ref={this.setVideoRef}
+ style={{ width: this._screenCapture ? "100%" : undefined, height: this._screenCapture ? "100%" : undefined }}
+ onCanPlay={this.videoLoad}
+ controls={true}
+ onClick={e => e.preventDefault()}>
+ <source type="video/mp4" />
+ Not supported.
+ </video>;
+ }
+
+ toggleRecording = action(async () => {
+ this._screenCapture = !this._screenCapture;
+ this._videoRef!.srcObject = !this._screenCapture ? undefined : await (navigator.mediaDevices as any).getDisplayMedia({ video: true });
+ });
+
+ private get uIButtons() {
+ return (<div className="screenshotBox-uiButtons">
+ <div className="screenshotBox-recorder" key="snap" onPointerDown={this.toggleRecording} >
+ <FontAwesomeIcon icon="file" size="lg" />
+ </div>,
+ <div className="screenshotBox-snapshot" key="snap" onPointerDown={this.onSnapshot} >
+ <FontAwesomeIcon icon="camera" size="lg" />
+ </div>
+ </div>);
+ }
+
+ onSnapshot = (e: React.PointerEvent) => {
+ this.Snapshot();
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+
+ contentFunc = () => [this.content];
+ render() {
+ return (<div className="videoBox" onContextMenu={this.specificContextMenu}
+ style={{ transform: `scale(${this.props.ContentScaling()})`, width: `${100 / this.props.ContentScaling()}%`, height: `${100 / this.props.ContentScaling()}%` }} >
+ <div className="videoBox-viewer" >
+ <CollectionFreeFormView {...this.props}
+ PanelHeight={this.props.PanelHeight}
+ PanelWidth={this.props.PanelWidth}
+ annotationsKey={this.annotationKey}
+ focus={this.props.focus}
+ isSelected={this.props.isSelected}
+ isAnnotationOverlay={true}
+ select={emptyFunction}
+ active={this.annotationsActive}
+ ContentScaling={returnOne}
+ whenActiveChanged={this.whenActiveChanged}
+ removeDocument={this.removeDocument}
+ moveDocument={this.moveDocument}
+ addDocument={returnFalse}
+ CollectionView={undefined}
+ ScreenToLocalTransform={this.props.ScreenToLocalTransform}
+ renderDepth={this.props.renderDepth + 1}
+ ContainingCollectionDoc={this.props.ContainingCollectionDoc}>
+ {this.contentFunc}
+ </CollectionFreeFormView>
+ </div>
+ {this.active() ? this.uIButtons : (null)}
+ </div >);
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/SliderBox-components.tsx b/src/client/views/nodes/SliderBox-components.tsx
new file mode 100644
index 000000000..874a1108f
--- /dev/null
+++ b/src/client/views/nodes/SliderBox-components.tsx
@@ -0,0 +1,256 @@
+import * as React from "react";
+import { SliderItem } from "react-compound-slider";
+import "./SliderBox-tooltip.css";
+
+const { Component, Fragment } = React;
+
+// *******************************************************
+// TOOLTIP RAIL
+// *******************************************************
+const railStyle: React.CSSProperties = {
+ position: "absolute",
+ width: "100%",
+ height: 40,
+ top: -13,
+ borderRadius: 7,
+ cursor: "pointer",
+ opacity: 0.3,
+ zIndex: 300,
+ border: "1px solid grey"
+};
+
+const railCenterStyle: React.CSSProperties = {
+ position: "absolute",
+ width: "100%",
+ height: 14,
+ borderRadius: 7,
+ cursor: "pointer",
+ pointerEvents: "none",
+ backgroundColor: "rgb(155,155,155)"
+};
+
+interface TooltipRailProps {
+ activeHandleID: string;
+ getRailProps: (props: object) => object;
+ getEventData: (e: Event) => object;
+}
+
+export class TooltipRail extends Component<TooltipRailProps> {
+ state = {
+ value: null,
+ percent: null
+ };
+
+ static defaultProps = {
+ disabled: false
+ };
+
+ onMouseEnter = () => {
+ document.addEventListener("mousemove", this.onMouseMove);
+ }
+
+ onMouseLeave = () => {
+ this.setState({ value: null, percent: null });
+ document.removeEventListener("mousemove", this.onMouseMove);
+ }
+
+ onMouseMove = (e: Event) => {
+ const { activeHandleID, getEventData } = this.props;
+
+ if (activeHandleID) {
+ this.setState({ value: null, percent: null });
+ } else {
+ this.setState(getEventData(e));
+ }
+ }
+
+ render() {
+ const { value, percent } = this.state;
+ const { activeHandleID, getRailProps } = this.props;
+
+ return (
+ <Fragment>
+ {!activeHandleID && value ? (
+ <div
+ style={{
+ left: `${percent}%`,
+ position: "absolute",
+ marginLeft: "-11px",
+ marginTop: "-35px"
+ }}
+ >
+ <div className="tooltip">
+ <span className="tooltiptext">Value: {value}</span>
+ </div>
+ </div>
+ ) : null}
+ <div
+ style={railStyle}
+ {...getRailProps({
+ onMouseEnter: this.onMouseEnter,
+ onMouseLeave: this.onMouseLeave
+ })}
+ />
+ <div style={railCenterStyle} />
+ </Fragment>
+ );
+ }
+}
+
+// *******************************************************
+// HANDLE COMPONENT
+// *******************************************************
+interface HandleProps {
+ key: string;
+ handle: SliderItem;
+ isActive: Boolean;
+ disabled?: Boolean;
+ domain: number[];
+ getHandleProps: (id: string, config: object) => object;
+}
+
+export class Handle extends Component<HandleProps> {
+ static defaultProps = {
+ disabled: false
+ };
+
+ state = {
+ mouseOver: false
+ };
+
+ onMouseEnter = () => {
+ this.setState({ mouseOver: true });
+ }
+
+ onMouseLeave = () => {
+ this.setState({ mouseOver: false });
+ }
+
+ render() {
+ const {
+ domain: [min, max],
+ handle: { id, value, percent },
+ isActive,
+ disabled,
+ getHandleProps
+ } = this.props;
+ const { mouseOver } = this.state;
+
+ return (
+ <Fragment>
+ {(mouseOver || isActive) && !disabled ? (
+ <div
+ style={{
+ left: `${percent}%`,
+ position: "absolute",
+ marginLeft: "-11px",
+ marginTop: "-35px"
+ }}
+ >
+ <div className="tooltip">
+ <span className="tooltiptext">Value: {value}</span>
+ </div>
+ </div>
+ ) : null}
+ <div
+ role="slider"
+ aria-valuemin={min}
+ aria-valuemax={max}
+ aria-valuenow={value}
+ style={{
+ left: `${percent}%`,
+ position: "absolute",
+ marginLeft: "-11px",
+ marginTop: "-6px",
+ zIndex: 400,
+ width: 24,
+ height: 24,
+ cursor: "pointer",
+ border: 0,
+ borderRadius: "50%",
+ boxShadow: "1px 1px 1px 1px rgba(0, 0, 0, 0.4)",
+ backgroundColor: disabled ? "#666" : "#3e1db3"
+ }}
+ {...getHandleProps(id, {
+ onMouseEnter: this.onMouseEnter,
+ onMouseLeave: this.onMouseLeave
+ })}
+ />
+ </Fragment>
+ );
+ }
+}
+
+// *******************************************************
+// TRACK COMPONENT
+// *******************************************************
+interface TrackProps {
+ source: SliderItem;
+ target: SliderItem;
+ disabled: Boolean;
+ getTrackProps: () => object;
+}
+
+export function Track({
+ source,
+ target,
+ getTrackProps,
+ disabled = false
+}: TrackProps) {
+ return (
+ <div
+ style={{
+ position: "absolute",
+ height: 14,
+ zIndex: 1,
+ backgroundColor: disabled ? "#999" : "#3e1db3",
+ borderRadius: 7,
+ cursor: "pointer",
+ left: `${source.percent}%`,
+ width: `${target.percent - source.percent}%`
+ }}
+ {...getTrackProps()}
+ />
+ );
+}
+
+// *******************************************************
+// TICK COMPONENT
+// *******************************************************
+interface TickProps {
+ tick: SliderItem;
+ count: number;
+ format: (val: number) => string;
+}
+
+const defaultFormat = (d: number) => `d`;
+
+export function Tick({ tick, count, format = defaultFormat }: TickProps) {
+ return (
+ <div>
+ <div
+ style={{
+ position: "absolute",
+ marginTop: 17,
+ width: 1,
+ height: 5,
+ backgroundColor: "rgb(200,200,200)",
+ left: `${tick.percent}%`
+ }}
+ />
+ <div
+ style={{
+ position: "absolute",
+ marginTop: 25,
+ fontSize: 10,
+ textAlign: "center",
+ marginLeft: `${-(100 / count) / 2}%`,
+ width: `${100 / count}%`,
+ left: `${tick.percent}%`
+ }}
+ >
+ {format(tick.value)}
+ </div>
+ </div>
+ );
+}
diff --git a/src/client/views/nodes/SliderBox-tooltip.css b/src/client/views/nodes/SliderBox-tooltip.css
new file mode 100644
index 000000000..8afde8eb5
--- /dev/null
+++ b/src/client/views/nodes/SliderBox-tooltip.css
@@ -0,0 +1,33 @@
+.tooltip {
+ position: relative;
+ display: inline-block;
+ border-bottom: 1px dotted #222;
+ margin-left: 22px;
+ }
+
+ .tooltip .tooltiptext {
+ width: 100px;
+ background-color: #222;
+ color: #fff;
+ opacity: 0.8;
+ text-align: center;
+ border-radius: 6px;
+ padding: 5px 0;
+ position: absolute;
+ z-index: 1;
+ bottom: 150%;
+ left: 50%;
+ margin-left: -60px;
+ }
+
+ .tooltip .tooltiptext::after {
+ content: "";
+ position: absolute;
+ top: 100%;
+ left: 50%;
+ margin-left: -5px;
+ border-width: 5px;
+ border-style: solid;
+ border-color: #222 transparent transparent transparent;
+ }
+ \ No newline at end of file
diff --git a/src/client/views/nodes/SliderBox.scss b/src/client/views/nodes/SliderBox.scss
new file mode 100644
index 000000000..4ef277d8c
--- /dev/null
+++ b/src/client/views/nodes/SliderBox.scss
@@ -0,0 +1,8 @@
+.sliderBox-outerDiv {
+ width: 100%;
+ height: 100%;
+ pointer-events: all;
+ border-radius: inherit;
+ display: flex;
+ flex-direction: column;
+} \ No newline at end of file
diff --git a/src/client/views/nodes/SliderBox.tsx b/src/client/views/nodes/SliderBox.tsx
new file mode 100644
index 000000000..844d95d11
--- /dev/null
+++ b/src/client/views/nodes/SliderBox.tsx
@@ -0,0 +1,130 @@
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faEdit } from '@fortawesome/free-regular-svg-icons';
+import { computed, runInAction } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { Handles, Rail, Slider, Tracks, Ticks } from 'react-compound-slider';
+import { Doc } from '../../../new_fields/Doc';
+import { documentSchema } from '../../../new_fields/documentSchemas';
+import { createSchema, listSpec, makeInterface } from '../../../new_fields/Schema';
+import { ScriptField } from '../../../new_fields/ScriptField';
+import { BoolCast, FieldValue, StrCast, NumCast, Cast } from '../../../new_fields/Types';
+import { DragManager } from '../../util/DragManager';
+import { ContextMenu } from '../ContextMenu';
+import { ContextMenuProps } from '../ContextMenuItem';
+import { DocComponent } from '../DocComponent';
+import './SliderBox.scss';
+import { Handle, TooltipRail, Track, Tick } from './SliderBox-components';
+import { FieldView, FieldViewProps } from './FieldView';
+import { ScriptBox } from '../ScriptBox';
+
+
+library.add(faEdit as any);
+
+const SliderSchema = createSchema({
+ _sliderMin: "number",
+ _sliderMax: "number",
+ _sliderMinThumb: "number",
+ _sliderMaxThumb: "number",
+});
+
+type SliderDocument = makeInterface<[typeof SliderSchema, typeof documentSchema]>;
+const SliderDocument = makeInterface(SliderSchema, documentSchema);
+
+@observer
+export class SliderBox extends DocComponent<FieldViewProps, SliderDocument>(SliderDocument) {
+ public static LayoutString(fieldKey: string) { return FieldView.LayoutString(SliderBox, fieldKey); }
+ private dropDisposer?: DragManager.DragDropDisposer;
+
+ @computed get dataDoc() {
+ return this.props.DataDoc &&
+ (this.Document.isTemplateForField || BoolCast(this.props.DataDoc.isTemplateForField) ||
+ this.props.DataDoc.layout === this.Document) ? this.props.DataDoc : Doc.GetProto(this.Document);
+ }
+
+ specificContextMenu = (e: React.MouseEvent): void => {
+ const funcs: ContextMenuProps[] = [];
+ funcs.push({ description: "Edit Thumb Change Script", icon: "edit", event: (obj: any) => ScriptBox.EditButtonScript("On Thumb Change ...", this.props.Document, "onThumbChange", obj.x, obj.y) });
+ ContextMenu.Instance.addItem({ description: "Slider Funcs...", subitems: funcs, icon: "asterisk" });
+ }
+ onChange = (values: readonly number[]) => runInAction(() => {
+ this.Document._sliderMinThumb = values[0];
+ this.Document._sliderMaxThumb = values[1];
+ Cast(this.Document.onThumbChanged, ScriptField, null)?.script.run({ range: values, this: this.props.Document });
+ })
+
+ render() {
+ const domain = [NumCast(this.props.Document._sliderMin), NumCast(this.props.Document._sliderMax)];
+ const defaultValues = [NumCast(this.props.Document._sliderMinThumb), NumCast(this.props.Document._sliderMaxThumb)];
+ return (
+ <div className="sliderBox-outerDiv" onContextMenu={this.specificContextMenu} onPointerDown={e => e.stopPropagation()}
+ style={{ boxShadow: this.Document.opacity === 0 ? undefined : StrCast(this.Document.boxShadow, "") }}>
+ <div className="sliderBox-mainButton" onContextMenu={this.specificContextMenu} style={{
+ background: this.Document.backgroundColor, color: this.Document.color || "black",
+ fontSize: this.Document.fontSize, letterSpacing: this.Document.letterSpacing || ""
+ }} >
+ <Slider
+ mode={2}
+ step={1}
+ domain={domain}
+ rootStyle={{ position: "relative", width: "100%" }}
+ onChange={this.onChange}
+ values={defaultValues}
+ >
+
+ <Rail>{railProps => <TooltipRail {...railProps} />}</Rail>
+ <Handles>
+ {({ handles, activeHandleID, getHandleProps }) => (
+ <div className="slider-handles">
+ {handles.map((handle, i) => {
+ const value = i === 0 ? this.Document._sliderMinThumb : this.Document._sliderMaxThumb;
+ return (
+ <div title={String(value)}>
+ <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-tracks">
+ {ticks.map((tick) => (
+ <Tick
+ key={tick.id}
+ tick={tick}
+ count={ticks.length}
+ format={(val: number) => val.toString()}
+ />
+ ))}
+ </div>
+ )}
+ </Ticks>
+ </Slider>
+ </div>
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index d12a8d151..24b66d8f7 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -90,7 +90,7 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
@action public FullScreen() {
this._fullScreen = true;
this.player && this.player.requestFullscreen();
- this._youtubePlayer && this.props.addDocTab(this.props.Document, this.props.DataDoc, "inTab");
+ this._youtubePlayer && this.props.addDocTab(this.props.Document, "inTab");
}
choosePath(url: string) {
@@ -134,7 +134,7 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
});
imageSummary.isButton = true;
this.props.addDocument && this.props.addDocument(imageSummary);
- DocUtils.MakeLink({ doc: imageSummary }, { doc: this.props.Document }, "snapshot from " + this.Document.title, "video frame snapshot");
+ DocUtils.MakeLink({ doc: imageSummary }, { doc: this.props.Document }, "video snapshot");
}
});
}
@@ -195,6 +195,7 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
console.log(e);
}
}
+ @observable _screenCapture = false;
specificContextMenu = (e: React.MouseEvent): void => {
const field = Cast(this.dataDoc[this.props.fieldKey], VideoField);
if (field) {
@@ -203,6 +204,12 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
subitems.push({ description: "Copy path", event: () => { Utils.CopyText(url); }, icon: "expand-arrows-alt" });
subitems.push({ description: "Toggle Show Controls", event: action(() => VideoBox._showControls = !VideoBox._showControls), icon: "expand-arrows-alt" });
subitems.push({ description: "Take Snapshot", event: () => this.Snapshot(), icon: "expand-arrows-alt" });
+ subitems.push({
+ description: "Screen Capture", event: (async () => {
+ runInAction(() => this._screenCapture = !this._screenCapture);
+ this._videoRef!.srcObject = !this._screenCapture ? undefined : await (navigator.mediaDevices as any).getDisplayMedia({ video: true });
+ }), icon: "expand-arrows-alt"
+ });
ContextMenu.Instance.addItem({ description: "Video Funcs...", subitems: subitems, icon: "video" });
}
}
@@ -212,8 +219,14 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
const interactive = InkingControl.Instance.selectedTool || !this.props.isSelected() ? "" : "-interactive";
const style = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive;
return !field ? <div>Loading</div> :
- <video className={`${style}`} key="video" ref={this.setVideoRef} onCanPlay={this.videoLoad} controls={VideoBox._showControls}
- onPlay={() => this.Play()} onSeeked={this.updateTimecode} onPause={() => this.Pause()} onClick={e => e.preventDefault()}>
+ <video className={`${style}`} key="video" autoPlay={this._screenCapture} ref={this.setVideoRef}
+ style={{ width: this._screenCapture ? "100%" : undefined, height: this._screenCapture ? "100%" : undefined }}
+ onCanPlay={this.videoLoad}
+ controls={VideoBox._showControls}
+ onPlay={() => this.Play()}
+ onSeeked={this.updateTimecode}
+ onPause={() => this.Pause()}
+ onClick={e => e.preventDefault()}>
<source src={field.url.href} type="video/mp4" />
Not supported.
</video>;
@@ -261,7 +274,6 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
}
private get uIButtons() {
- const scaling = Math.min(1.8, this.props.ScreenToLocalTransform().Scale);
const curTime = (this.Document.currentTimecode || 0);
return ([<div className="videoBox-time" key="time" onPointerDown={this.onResetDown} >
<span>{"" + Math.round(curTime)}</span>
@@ -354,8 +366,7 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
CollectionView={undefined}
ScreenToLocalTransform={this.props.ScreenToLocalTransform}
renderDepth={this.props.renderDepth + 1}
- ContainingCollectionDoc={this.props.ContainingCollectionDoc}
- chromeCollapsed={true}>
+ ContainingCollectionDoc={this.props.ContainingCollectionDoc}>
{this.contentFunc}
</CollectionFreeFormView>
</div>
diff --git a/src/client/views/nodes/WebBox.scss b/src/client/views/nodes/WebBox.scss
index fbe9bf063..b41687c11 100644
--- a/src/client/views/nodes/WebBox.scss
+++ b/src/client/views/nodes/WebBox.scss
@@ -1,13 +1,18 @@
@import "../globalCssVariables.scss";
+
+.webBox-container, .webBox-container-dragging {
+ transform-origin: top left;
+}
.webBox-cont,
-.webBox-cont-interactive {
+.webBox-cont-dragging {
padding: 0vw;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
+ transform-origin: top left;
overflow: auto;
pointer-events: none;
}
@@ -90,4 +95,19 @@
width: 100%;
margin-right: 10px;
height: 100%;
+}
+
+.touch-iframe-overlay {
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ pointer-events: all;
+
+ .indicator {
+ position: absolute;
+
+ &.active {
+ background-color: rgba(0, 0, 0, 0.1);
+ }
+ }
} \ No newline at end of file
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index a48dc286e..838fbefb1 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -1,26 +1,31 @@
import { library } from "@fortawesome/fontawesome-svg-core";
import { faStickyNote } from '@fortawesome/free-solid-svg-icons';
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { action, computed, observable } from "mobx";
import { observer } from "mobx-react";
import { Doc, FieldResult } from "../../../new_fields/Doc";
+import { documentSchema } from "../../../new_fields/documentSchemas";
import { HtmlField } from "../../../new_fields/HtmlField";
import { InkTool } from "../../../new_fields/InkField";
import { makeInterface } from "../../../new_fields/Schema";
import { Cast, NumCast } from "../../../new_fields/Types";
import { WebField } from "../../../new_fields/URLField";
-import { emptyFunction, returnOne, Utils } from "../../../Utils";
+import { Utils, returnOne, emptyFunction } from "../../../Utils";
import { Docs } from "../../documents/Documents";
+import { DragManager } from "../../util/DragManager";
+import { ImageUtils } from "../../util/Import & Export/ImageUtils";
import { SelectionManager } from "../../util/SelectionManager";
-import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
+import { DocAnnotatableComponent } from "../DocComponent";
import { DocumentDecorations } from "../DocumentDecorations";
import { InkingControl } from "../InkingControl";
import { FieldView, FieldViewProps } from './FieldView';
import { KeyValueBox } from "./KeyValueBox";
import "./WebBox.scss";
import React = require("react");
-import { DocAnnotatableComponent } from "../DocComponent";
-import { documentSchema } from "../../../new_fields/documentSchemas";
+import * as WebRequest from 'web-request';
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
+const htmlToText = require("html-to-text");
+
library.add(faStickyNote);
@@ -32,12 +37,23 @@ export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument>
public static LayoutString(fieldKey: string) { return FieldView.LayoutString(WebBox, fieldKey); }
@observable private collapsed: boolean = true;
- @observable private url: string = "";
+ @observable private url: string = "hello";
- componentDidMount() {
+ private _longPressSecondsHack?: NodeJS.Timeout;
+ private _iframeRef = React.createRef<HTMLIFrameElement>();
+ private _iframeIndicatorRef = React.createRef<HTMLDivElement>();
+ private _iframeDragRef = React.createRef<HTMLDivElement>();
+ @observable private _pressX: number = 0;
+ @observable private _pressY: number = 0;
+ async componentDidMount() {
+
+ this.setURL();
+
+ document.addEventListener("pointerup", this.onLongPressUp);
+ document.addEventListener("pointermove", this.onLongPressMove);
const field = Cast(this.props.Document[this.props.fieldKey], WebField);
- if (field && field.url.href.indexOf("youtube") !== -1) {
+ if (field?.url.href.indexOf("youtube") !== -1) {
const youtubeaspect = 400 / 315;
const nativeWidth = NumCast(this.layoutDoc._nativeWidth);
const nativeHeight = NumCast(this.layoutDoc._nativeHeight);
@@ -46,9 +62,16 @@ export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument>
this.layoutDoc._nativeHeight = NumCast(this.layoutDoc._nativeWidth) / youtubeaspect;
this.layoutDoc._height = NumCast(this.layoutDoc._width) / youtubeaspect;
}
+ } else if (field?.url) {
+ const result = await WebRequest.get(Utils.CorsProxy(field.url.href));
+ this.dataDoc.text = htmlToText.fromString(result.content);
}
- this.setURL();
+ }
+
+ componentWillUnmount() {
+ document.removeEventListener("pointerup", this.onLongPressUp);
+ document.removeEventListener("pointermove", this.onLongPressMove);
}
@action
@@ -58,14 +81,12 @@ export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument>
@action
submitURL = () => {
- const script = KeyValueBox.CompileKVPScript(`new WebField("${this.url}")`);
- if (!script) return;
- KeyValueBox.ApplyKVPScript(this.props.Document, "data", script);
+ this.dataDoc[this.props.fieldKey] = new WebField(new URL(this.url));
}
@action
setURL() {
- const urlField: FieldResult<WebField> = Cast(this.props.Document.data, WebField);
+ const urlField: FieldResult<WebField> = Cast(this.dataDoc[this.props.fieldKey], WebField);
if (urlField) this.url = urlField.url.toString();
else this.url = "";
}
@@ -164,6 +185,107 @@ export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument>
}
}
+ onLongPressDown = (e: React.PointerEvent) => {
+ this._pressX = e.clientX;
+ this._pressY = e.clientY;
+
+ // find the pressed element in the iframe (currently only works if its an img)
+ let pressedElement: HTMLElement | undefined;
+ let pressedBound: ClientRect | undefined;
+ let selectedText: string = "";
+ let pressedImg: boolean = false;
+ if (this._iframeRef.current) {
+ const B = this._iframeRef.current.getBoundingClientRect();
+ const iframeDoc = this._iframeRef.current.contentDocument;
+ if (B && iframeDoc) {
+ // TODO: this only works when scale = 1 as it is currently only inteded for mobile upload
+ const element = iframeDoc.elementFromPoint(this._pressX - B.left, this._pressY - B.top);
+ if (element && element.nodeName === "IMG") {
+ pressedBound = element.getBoundingClientRect();
+ pressedElement = element.cloneNode(true) as HTMLElement;
+ pressedImg = true;
+ } else {
+ // check if there is selected text
+ const text = iframeDoc.getSelection();
+ if (text && text.toString().length > 0) {
+ selectedText = text.toString();
+
+ // get html of the selected text
+ const range = text.getRangeAt(0);
+ const contents = range.cloneContents();
+ const div = document.createElement("div");
+ div.appendChild(contents);
+ pressedElement = div;
+
+ pressedBound = range.getBoundingClientRect();
+ }
+ }
+ }
+ }
+
+ // mark the pressed element
+ if (pressedElement && pressedBound) {
+ if (this._iframeIndicatorRef.current) {
+ this._iframeIndicatorRef.current.style.top = pressedBound.top + "px";
+ this._iframeIndicatorRef.current.style.left = pressedBound.left + "px";
+ this._iframeIndicatorRef.current.style.width = pressedBound.width + "px";
+ this._iframeIndicatorRef.current.style.height = pressedBound.height + "px";
+ this._iframeIndicatorRef.current.classList.add("active");
+ }
+ }
+
+ // start dragging the pressed element if long pressed
+ this._longPressSecondsHack = setTimeout(() => {
+ if (pressedImg && pressedElement && pressedBound) {
+ e.stopPropagation();
+ e.preventDefault();
+ if (pressedElement.nodeName === "IMG") {
+ const src = pressedElement.getAttribute("src"); // TODO: may not always work
+ if (src) {
+ const doc = Docs.Create.ImageDocument(src);
+ ImageUtils.ExtractExif(doc);
+
+ // add clone to div so that dragging ghost is placed properly
+ if (this._iframeDragRef.current) this._iframeDragRef.current.appendChild(pressedElement);
+
+ const dragData = new DragManager.DocumentDragData([doc]);
+ DragManager.StartDocumentDrag([pressedElement], dragData, this._pressX, this._pressY, { hideSource: true });
+ }
+ }
+ } else if (selectedText && pressedBound && pressedElement) {
+ e.stopPropagation();
+ e.preventDefault();
+ // create doc with the selected text's html
+ const doc = Docs.Create.HtmlDocument(pressedElement.innerHTML);
+
+ // create dragging ghost with the selected text
+ if (this._iframeDragRef.current) this._iframeDragRef.current.appendChild(pressedElement);
+
+ // start the drag
+ const dragData = new DragManager.DocumentDragData([doc]);
+ DragManager.StartDocumentDrag([pressedElement], dragData, this._pressX - pressedBound.top, this._pressY - pressedBound.top, { hideSource: true });
+ }
+ }, 1500);
+ }
+
+ onLongPressMove = (e: PointerEvent) => {
+ // this._pressX = e.clientX;
+ // this._pressY = e.clientY;
+ }
+
+ onLongPressUp = (e: PointerEvent) => {
+ if (this._longPressSecondsHack) {
+ clearTimeout(this._longPressSecondsHack);
+ }
+ if (this._iframeIndicatorRef.current) {
+ this._iframeIndicatorRef.current.classList.remove("active");
+ }
+ if (this._iframeDragRef.current) {
+ while (this._iframeDragRef.current.firstChild) this._iframeDragRef.current.removeChild(this._iframeDragRef.current.firstChild);
+ }
+ }
+
+
@computed
get content() {
const field = this.dataDoc[this.props.fieldKey];
@@ -171,9 +293,9 @@ export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument>
if (field instanceof HtmlField) {
view = <span id="webBox-htmlSpan" dangerouslySetInnerHTML={{ __html: field.html }} />;
} else if (field instanceof WebField) {
- view = <iframe src={Utils.CorsProxy(field.url.href)} style={{ position: "absolute", width: "100%", height: "100%", top: 0 }} />;
+ view = <iframe ref={this._iframeRef} src={Utils.CorsProxy(field.url.href)} style={{ position: "absolute", width: "100%", height: "100%", top: 0 }} />;
} else {
- view = <iframe src={"https://crossorigin.me/https://cs.brown.edu"} style={{ position: "absolute", width: "100%", height: "100%", top: 0 }} />;
+ view = <iframe ref={this._iframeRef} src={"https://crossorigin.me/https://cs.brown.edu"} style={{ position: "absolute", width: "100%", height: "100%", top: 0 }} />;
}
const content =
<div style={{ width: "100%", height: "100%", position: "absolute" }} onWheel={this.onPostWheel} onPointerDown={this.onPostPointer} onPointerMove={this.onPostPointer} onPointerUp={this.onPostPointer}>
@@ -181,19 +303,34 @@ export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument>
{view}
</div>;
- const frozen = !this.props.isSelected() || DocumentDecorations.Instance.Interacting;
+ const decInteracting = DocumentDecorations.Instance && DocumentDecorations.Instance.Interacting;
- const classname = "webBox-cont" + (this.props.isSelected() && InkingControl.Instance.selectedTool === InkTool.None && !DocumentDecorations.Instance.Interacting ? "-interactive" : "");
+ const frozen = !this.props.isSelected() || decInteracting;
+
+ const classname = "webBox-cont" + (this.props.isSelected() && InkingControl.Instance.selectedTool === InkTool.None && !decInteracting ? "-interactive" : "");
return (
<>
<div className={classname} >
{content}
</div>
- {!frozen ? (null) : <div className="webBox-overlay" onWheel={this.onPreWheel} onPointerDown={this.onPrePointer} onPointerMove={this.onPrePointer} onPointerUp={this.onPrePointer} />}
+ {!frozen ? (null) :
+ <div className="webBox-overlay" onWheel={this.onPreWheel} onPointerDown={this.onPrePointer} onPointerMove={this.onPrePointer} onPointerUp={this.onPrePointer}>
+ <div className="touch-iframe-overlay" onPointerDown={this.onLongPressDown} >
+ <div className="indicator" ref={this._iframeIndicatorRef}></div>
+ <div className="dragger" ref={this._iframeDragRef}></div>
+ </div>
+ </div>}
</>);
}
render() {
- return (<div className={"webBox-container"} >
+ const dragging = "";//</div>!SelectionManager.GetIsDragging() ? "" : "-dragging";
+ return (<div className={`webBox-container${dragging}`}
+ style={{
+ transform: `scale(${this.props.ContentScaling()})`,
+ width: `${100 / this.props.ContentScaling()}%`,
+ height: `${100 / this.props.ContentScaling()}%`,
+ pointerEvents: this.props.Document.isBackground ? "none" : undefined
+ }} >
<CollectionFreeFormView {...this.props}
PanelHeight={this.props.PanelHeight}
PanelWidth={this.props.PanelWidth}
@@ -211,8 +348,7 @@ export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument>
CollectionView={undefined}
ScreenToLocalTransform={this.props.ScreenToLocalTransform}
renderDepth={this.props.renderDepth + 1}
- ContainingCollectionDoc={this.props.ContainingCollectionDoc}
- chromeCollapsed={true}>
+ ContainingCollectionDoc={this.props.ContainingCollectionDoc}>
{() => [this.content]}
</CollectionFreeFormView>
</div >);
diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx
index d8b340db6..d23c81065 100644
--- a/src/client/views/pdf/Annotation.tsx
+++ b/src/client/views/pdf/Annotation.tsx
@@ -1,7 +1,7 @@
import React = require("react");
import { action, IReactionDisposer, observable, reaction, runInAction } from "mobx";
import { observer } from "mobx-react";
-import { Doc, DocListCast, HeightSym, WidthSym, Opt, DocListCastAsync } from "../../../new_fields/Doc";
+import { Doc, DocListCast, HeightSym, WidthSym } from "../../../new_fields/Doc";
import { Id } from "../../../new_fields/FieldSymbols";
import { List } from "../../../new_fields/List";
import { Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types";
@@ -11,7 +11,7 @@ import "./Annotation.scss";
interface IAnnotationProps {
anno: Doc;
- addDocTab: (document: Doc, dataDoc: Opt<Doc>, where: string) => boolean;
+ addDocTab: (document: Doc, where: string) => boolean;
pinToPres: (document: Doc) => void;
focus: (doc: Doc) => void;
dataDoc: Doc;
@@ -30,7 +30,7 @@ interface IRegionAnnotationProps {
y: number;
width: number;
height: number;
- addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean;
+ addDocTab: (document: Doc, where: string) => boolean;
pinToPres: (document: Doc) => void;
document: Doc;
dataDoc: Doc;
@@ -98,7 +98,7 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
const annoGroup = await Cast(this.props.document.group, Doc);
if (annoGroup) {
DocumentManager.Instance.FollowLink(undefined, annoGroup,
- (doc: Doc, maxLocation: string) => this.props.addDocTab(doc, undefined, e.ctrlKey ? "inTab" : "onRight"),
+ (doc: Doc, maxLocation: string) => this.props.addDocTab(doc, e.ctrlKey ? "inTab" : "onRight"),
false, false, undefined);
e.stopPropagation();
}
diff --git a/src/client/views/pdf/PDFViewer.scss b/src/client/views/pdf/PDFViewer.scss
index 4f81c6f70..5cd2c4fe4 100644
--- a/src/client/views/pdf/PDFViewer.scss
+++ b/src/client/views/pdf/PDFViewer.scss
@@ -1,6 +1,5 @@
-.pdfViewer, .pdfViewer-zoomed {
- pointer-events: all;
+.pdfViewer, .pdfViewer-interactive {
width: 100%;
height: 100%;
position: absolute;
@@ -91,7 +90,8 @@
z-index: 10;
}
}
-.pdfViewer-zoomed {
- overflow-x: scroll;
+
+.pdfViewer-interactive {
+ pointer-events: all;
}
\ No newline at end of file
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index a7c1990e9..71495d95f 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -4,10 +4,10 @@ import * as Pdfjs from "pdfjs-dist";
import "pdfjs-dist/web/pdf_viewer.css";
import { Dictionary } from "typescript-collections";
import { Doc, DocListCast, FieldResult, WidthSym, Opt, HeightSym } from "../../../new_fields/Doc";
-import { Id } from "../../../new_fields/FieldSymbols";
+import { Id, Copy } from "../../../new_fields/FieldSymbols";
import { List } from "../../../new_fields/List";
import { makeInterface, createSchema } from "../../../new_fields/Schema";
-import { ScriptField } from "../../../new_fields/ScriptField";
+import { ScriptField, ComputedField } from "../../../new_fields/ScriptField";
import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
import { smoothScroll, Utils, emptyFunction, returnOne, intersectRect, addStyleSheet, addStyleSheetRule, clearStyleSheetRules } from "../../../Utils";
import { Docs, DocUtils } from "../../documents/Documents";
@@ -31,6 +31,9 @@ import { InkingControl } from "../InkingControl";
import { InkTool } from "../../../new_fields/InkField";
import { TraceMobx } from "../../../new_fields/util";
import { PdfField } from "../../../new_fields/URLField";
+import { PDFBox } from "../nodes/PDFBox";
+import { FormattedTextBox } from "../nodes/FormattedTextBox";
+import { DocumentView } from "../nodes/DocumentView";
const PDFJSViewer = require("pdfjs-dist/web/pdf_viewer");
const pdfjsLib = require("pdfjs-dist");
@@ -65,7 +68,7 @@ interface IViewerProps {
loaded: (nw: number, nh: number, np: number) => void;
active: (outsideReaction?: boolean) => boolean;
isChildActive: (outsideReaction?: boolean) => boolean;
- addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean;
+ addDocTab: (document: Doc, where: string) => boolean;
pinToPres: (document: Doc) => void;
addDocument?: (doc: Doc) => boolean;
setPdfViewer: (view: PDFViewer) => void;
@@ -101,6 +104,7 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
private _reactionDisposer?: IReactionDisposer;
private _selectionReactionDisposer?: IReactionDisposer;
private _annotationReactionDisposer?: IReactionDisposer;
+ private _scrollTopReactionDisposer?: IReactionDisposer;
private _filterReactionDisposer?: IReactionDisposer;
private _searchReactionDisposer?: IReactionDisposer;
private _viewer: React.RefObject<HTMLDivElement> = React.createRef();
@@ -126,10 +130,9 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
!this.props.Document.lockedTransform && (this.props.Document.lockedTransform = true);
// change the address to be the file address of the PNG version of each page
// file address of the pdf
- const { url: { href } } = Cast(this.props.Document[this.props.fieldKey], PdfField)!;
- this._coverPath = href.startsWith(window.location.origin) ?
- JSON.parse(await rp.get(Utils.prepend(`/thumbnail${this.props.url.substring("files/pdfs/".length, this.props.url.length - ".pdf".length)}-${(this.Document.curPage || 1)}.png`))) :
- { width: 100, height: 100, path: "" };
+ const { url: { href } } = Cast(this.dataDoc[this.props.fieldKey], PdfField)!;
+ const addr = Utils.prepend(`/thumbnail${this.props.url.substring("files/pdfs/".length, this.props.url.length - ".pdf".length)}-${(this.Document.curPage || 1)}.png`);
+ this._coverPath = href.startsWith(window.location.origin) ? JSON.parse(await rp.get(addr)) : { width: 100, height: 100, path: "" };
runInAction(() => this._showWaiting = this._showCover = true);
this.props.startupLive && this.setupPdfJsViewer();
this._searchReactionDisposer = reaction(() => this.Document.searchMatch, search => {
@@ -163,10 +166,11 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
componentWillUnmount = () => {
this._reactionDisposer && this._reactionDisposer();
- this._annotationReactionDisposer && this._annotationReactionDisposer();
- this._filterReactionDisposer && this._filterReactionDisposer();
- this._selectionReactionDisposer && this._selectionReactionDisposer();
- this._searchReactionDisposer && this._searchReactionDisposer();
+ this._scrollTopReactionDisposer?.();
+ this._annotationReactionDisposer?.();
+ this._filterReactionDisposer?.();
+ this._selectionReactionDisposer?.();
+ this._searchReactionDisposer?.();
document.removeEventListener("copy", this.copy);
}
@@ -207,6 +211,8 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
this.props.setPdfViewer(this);
await this.initialLoad();
+ this._scrollTopReactionDisposer = reaction(() => Cast(this.layoutDoc._scrollTop, "number", null),
+ (stop) => (stop !== undefined) && this._mainCont.current && smoothScroll(500, this._mainCont.current, stop), { fireImmediately: true });
this._annotationReactionDisposer = reaction(
() => DocListCast(this.dataDoc[this.props.fieldKey + "-annotations"]),
annotations => annotations?.length && (this._annotations = annotations),
@@ -228,6 +234,12 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
this.createPdfViewer();
}
+ pagesinit = action(() => {
+ this._pdfViewer.currentScaleValue = this._zoomed = 1;
+ this.gotoPage(this.Document.curPage || 1);
+ document.removeEventListener("pagesinit", this.pagesinit);
+ });
+
createPdfViewer() {
if (!this._mainCont.current) { // bcz: I don't think this is ever triggered or needed
if (this._retries < 5) {
@@ -238,10 +250,7 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
}
document.removeEventListener("copy", this.copy);
document.addEventListener("copy", this.copy);
- document.addEventListener("pagesinit", action(() => {
- this._pdfViewer.currentScaleValue = this._zoomed = 1;
- this.gotoPage(this.Document.curPage || 1);
- }));
+ document.addEventListener("pagesinit", this.pagesinit);
document.addEventListener("pagerendered", action(() => this._showCover = this._showWaiting = false));
const pdfLinkService = new PDFJSViewer.PDFLinkService();
const pdfFindController = new PDFJSViewer.PDFFindController({ linkService: pdfLinkService });
@@ -268,7 +277,7 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
let minY = Number.MAX_VALUE;
if ((this._savedAnnotations.values()[0][0] as any).marqueeing) {
const anno = this._savedAnnotations.values()[0][0];
- const annoDoc = Docs.Create.FreeformDocument([], { backgroundColor: color, title: "Annotation on " + this.Document.title });
+ const annoDoc = Docs.Create.FreeformDocument([], { backgroundColor: color, _LODdisable: true, title: "Annotation on " + this.Document.title });
if (anno.style.left) annoDoc.x = parseInt(anno.style.left);
if (anno.style.top) annoDoc.y = parseInt(anno.style.top);
if (anno.style.height) annoDoc._height = parseInt(anno.style.height);
@@ -321,7 +330,7 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
@action
gotoPage = (p: number) => {
- this._pdfViewer && this._pdfViewer.scrollPageIntoView({ pageNumber: Math.min(Math.max(1, p), this._pageSizes.length) });
+ this._pdfViewer?.scrollPageIntoView({ pageNumber: Math.min(Math.max(1, p), this._pageSizes.length) });
}
@action
@@ -555,32 +564,35 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
* start a drag event and create or put the necessary info into the drag event.
*/
@action
- startDrag = (e: PointerEvent, ele: HTMLElement): void => {
+ startDrag = async (e: PointerEvent, ele: HTMLElement) => {
e.preventDefault();
e.stopPropagation();
+
+ const clipDoc = Doc.MakeAlias(this.dataDoc);
+ clipDoc._fitWidth = true;
+ clipDoc._width = this.marqueeWidth();
+ clipDoc._height = this.marqueeHeight();
+ clipDoc._scrollTop = this.marqueeY();
const targetDoc = Docs.Create.TextDocument("", { _width: 200, _height: 200, title: "Note linked to " + this.props.Document.title });
+ Doc.GetProto(targetDoc).data = new List<Doc>([clipDoc]);
+ DocumentView.makeCustomViewClicked(targetDoc, Docs.Create.StackingDocument, "slideView", undefined);
+ // const targetDoc = Docs.Create.TextDocument("", { _width: 200, _height: 200, title: "Note linked to " + this.props.Document.title });
+ // Doc.GetProto(targetDoc).snipped = this.dataDoc[this.props.fieldKey][Copy]();
+ // const snipLayout = Docs.Create.PdfDocument("http://www.msn.com", { title: "snippetView", isTemplateDoc: true, isTemplateForField: "snipped", _fitWidth: true, _width: this.marqueeWidth(), _height: this.marqueeHeight(), _scrollTop: this.marqueeY() });
+ // Doc.GetProto(snipLayout).layout = PDFBox.LayoutString("snipped");
const annotationDoc = this.highlight("rgba(146, 245, 95, 0.467)"); // yellowish highlight color when dragging out a text selection
if (annotationDoc) {
DragManager.StartPdfAnnoDrag([ele], new DragManager.PdfAnnoDragData(this.props.Document, annotationDoc, targetDoc), e.pageX, e.pageY, {
- dragComplete: e => !e.aborted && e.annoDragData && !e.annoDragData.linkedToDoc &&
- DocUtils.MakeLink({ doc: annotationDoc }, { doc: e.annoDragData.dropDocument, ctx: e.annoDragData.targetContext }, `Annotation from ${this.Document.title}`, "link from PDF")
+ dragComplete: e => {
+ if (!e.aborted && e.annoDragData && !e.annoDragData.linkedToDoc) {
+ const link = DocUtils.MakeLink({ doc: annotationDoc }, { doc: e.annoDragData.dropDocument }, "Annotation");
+ if (link) link.maximizeLocation = "onRight";
+ }
+ }
});
}
}
- createSnippet = (marquee: { left: number, top: number, width: number, height: number }): void => {
- const view = Doc.MakeAlias(this.props.Document);
- const data = Doc.MakeDelegate(Doc.GetProto(this.props.Document));
- data.title = StrCast(data.title) + "_snippet";
- view.proto = data;
- view._nativeHeight = marquee.height;
- view._height = (this.Document[WidthSym]() / (this.Document._nativeWidth || 1)) * marquee.height;
- view._nativeWidth = this.Document._nativeWidth;
- view.startY = marquee.top;
- view._width = this.Document[WidthSym]();
- DragManager.StartDocumentDrag([], new DragManager.DocumentDragData([view]), 0, 0);
- }
-
scrollXf = () => {
return this._mainCont.current ? this.props.ScreenToLocalTransform().translate(0, this._scrollTop) : this.props.ScreenToLocalTransform();
}
@@ -623,7 +635,7 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
TraceMobx();
return <div className="pdfViewer-annotationLayer" style={{ height: NumCast(this.Document.nativeHeight), transform: `scale(${this._zoomed})` }} ref={this._annotationLayer}>
{this.nonDocAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y)).map((anno, index) =>
- <Annotation {...this.props} focus={this.props.focus} dataDoc={this.dataDoc!} fieldKey={this.props.fieldKey} anno={anno} key={`${anno[Id]}-annotation`} />)}
+ <Annotation {...this.props} focus={this.props.focus} dataDoc={this.dataDoc} fieldKey={this.props.fieldKey} anno={anno} key={`${anno[Id]}-annotation`} />)}
</div>;
}
overlayTransform = () => this.scrollXf().scale(1 / this._zoomed);
@@ -637,6 +649,7 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
setPreviewCursor={this.setPreviewCursor}
PanelHeight={this.panelWidth}
PanelWidth={this.panelHeight}
+ dropAction={"alias"}
VisibleHeight={this.visibleHeight}
focus={this.props.focus}
isSelected={this.props.isSelected}
@@ -644,6 +657,7 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
select={emptyFunction}
active={this.annotationsActive}
ContentScaling={this.contentZoom}
+ bringToFront={emptyFunction}
whenActiveChanged={this.whenActiveChanged}
removeDocument={this.removeDocument}
moveDocument={this.moveDocument}
@@ -651,8 +665,7 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
CollectionView={undefined}
ScreenToLocalTransform={this.overlayTransform}
renderDepth={this.props.renderDepth + 1}
- ContainingCollectionDoc={this.props.ContainingCollectionView?.props.Document}
- chromeCollapsed={true}>
+ ContainingCollectionDoc={this.props.ContainingCollectionView?.props.Document}>
</CollectionFreeFormView>
</div>;
}
@@ -675,9 +688,10 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
contentZoom = () => this._zoomed;
render() {
TraceMobx();
- return <div className={"pdfViewer" + (this._zoomed !== 1 ? "-zoomed" : "")} ref={this._mainCont}
+ return <div className={"pdfViewer" + (this.active() ? "-interactive" : "")} ref={this._mainCont}
onScroll={this.onScroll} onWheel={this.onZoomWheel} onPointerDown={this.onPointerDown} onClick={this.onClick}
style={{
+ overflowX: this._zoomed !== 1 ? "scroll" : undefined,
width: !this.props.Document._fitWidth ? NumCast(this.props.Document._nativeWidth) : `${100 / this.contentScaling}%`,
height: !this.props.Document._fitWidth ? NumCast(this.props.Document._nativeHeight) : `${100 / this.contentScaling}%`,
transform: `scale(${this.props.ContentScaling()})`
diff --git a/src/client/views/presentationview/PresElementBox.scss b/src/client/views/presentationview/PresElementBox.scss
index 34c170be2..8370af490 100644
--- a/src/client/views/presentationview/PresElementBox.scss
+++ b/src/client/views/presentationview/PresElementBox.scss
@@ -4,17 +4,20 @@
background-color: #eeeeee;
pointer-events: all;
width: 100%;
+ height: 100%;
outline-color: maroon;
outline-style: dashed;
- border-radius: 12px;
+ border-radius: 6px;
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
- transition: all .1s;
-
+ transition: all .1s;
+ padding: 0px;
+ padding-left: 5px;
+ padding-bottom: 3px;
.documentView-node {
position: absolute;
z-index: 1;
@@ -32,34 +35,43 @@
.presElementBox-item:hover {
transition: all .1s;
background: #AAAAAA;
- border-radius: 12px;
+ border-radius: 6px;
}
.presElementBox-selected {
background: gray;
color: black;
- border-radius: 12px;
+ border-radius: 6px;
box-shadow: black 2px 2px 5px;
}
.presElementBox-closeIcon {
- float: right;
border-radius: 20px;
transform:scale(0.7);
+ position: absolute;
+ right: 0;
+ top: 0;
+ padding: 8px;
}
.presElementBox-interaction {
color: gray;
float: left;
+ padding: 0px;
+ width: 20px;
+ height: 20px;
}
.presElementBox-interaction-selected {
color: white;
float: left;
+ padding: 0px;
+ width: 22px;
+ height: 22px;
}
.presElementBox-name {
- font-size: 15px;
+ font-size: 12pxππ;
position: absolute;
display: inline-block;
width: calc(100% - 45px);
@@ -70,7 +82,14 @@
.presElementBox-embedded {
position: relative;
- margin-top: 30;
+ margin-top: 22;
+ display: flex;
+ width: auto;
+ justify-content: center;
+ .contentFittingDocumentView {
+ position: absolute;
+ height: 100%;
+ }
}
.presElementBox-embeddedMask {
diff --git a/src/client/views/presentationview/PresElementBox.tsx b/src/client/views/presentationview/PresElementBox.tsx
index dad55e1fd..758795ed5 100644
--- a/src/client/views/presentationview/PresElementBox.tsx
+++ b/src/client/views/presentationview/PresElementBox.tsx
@@ -2,19 +2,18 @@ import { library } from '@fortawesome/fontawesome-svg-core';
import { faFile as fileRegular } from '@fortawesome/free-regular-svg-icons';
import { faArrowDown, faArrowUp, faFile as fileSolid, faFileDownload, faLocationArrow, faSearch } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, computed } from "mobx";
+import { action, computed, IReactionDisposer, reaction } from "mobx";
import { observer } from "mobx-react";
-import { Doc, DocListCast } from "../../../new_fields/Doc";
+import { Doc, DataSym } from "../../../new_fields/Doc";
import { documentSchema } from '../../../new_fields/documentSchemas';
import { Id } from "../../../new_fields/FieldSymbols";
import { createSchema, makeInterface } from '../../../new_fields/Schema';
-import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
-import { emptyFunction, returnFalse, emptyPath } from "../../../Utils";
-import { DocumentType } from "../../documents/DocumentTypes";
+import { Cast, NumCast } from "../../../new_fields/Types";
+import { emptyFunction, emptyPath, returnFalse } from "../../../Utils";
import { Transform } from "../../util/Transform";
import { CollectionViewType } from '../collections/CollectionView';
+import { DocExtendableComponent } from '../DocComponent';
import { ContentFittingDocumentView } from '../nodes/ContentFittingDocumentView';
-import { DocComponent } from '../DocComponent';
import { FieldView, FieldViewProps } from '../nodes/FieldView';
import "./PresElementBox.scss";
import React = require("react");
@@ -29,14 +28,13 @@ library.add(faArrowDown);
export const presSchema = createSchema({
presentationTargetDoc: Doc,
presBox: Doc,
- presBoxKey: "string",
- showButton: "boolean",
+ zoomButton: "boolean",
navButton: "boolean",
hideTillShownButton: "boolean",
fadeButton: "boolean",
hideAfterButton: "boolean",
groupButton: "boolean",
- embedOpen: "boolean"
+ expandInlineButton: "boolean"
});
type PresDocument = makeInterface<[typeof presSchema, typeof documentSchema]>;
@@ -46,13 +44,24 @@ const PresDocument = makeInterface(presSchema, documentSchema);
* It involves some functionality for its buttons and options.
*/
@observer
-export class PresElementBox extends DocComponent<FieldViewProps, PresDocument>(PresDocument) {
+export class PresElementBox extends DocExtendableComponent<FieldViewProps, PresDocument>(PresDocument) {
public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PresElementBox, fieldKey); }
- @computed get indexInPres() { return DocListCast(this.presentationDoc[this.Document.presBoxKey || ""]).indexOf(this.props.Document); }
- @computed get presentationDoc() { return Cast(this.Document.presBox, Doc) as Doc; }
- @computed get targetDoc() { return this.Document.presentationTargetDoc as Doc; }
- @computed get currentIndex() { return NumCast(this.presentationDoc.selectedDoc); }
+ _heightDisposer: IReactionDisposer | undefined;
+ @computed get indexInPres() { return NumCast(this.presElementDoc?.presentationIndex); }
+ @computed get presBoxDoc() { return Cast(this.presElementDoc?.presBox, Doc) as Doc; }
+ @computed get presElementDoc() { return this.props.Document.rootDocument as Doc; }
+ @computed get presLayoutDoc() { return this.props.Document; }
+ @computed get targetDoc() { return this.presElementDoc?.presentationTargetDoc as Doc; }
+ @computed get currentIndex() { return NumCast(this.presBoxDoc?._itemIndex); }
+
+ componentDidMount() {
+ this._heightDisposer = reaction(() => [this.presElementDoc.expandInlineButton, this.presElementDoc.collapsedHeight],
+ params => this.presLayoutDoc._height = NumCast(params[1]) + (Number(params[0]) ? 100 : 0), { fireImmediately: true });
+ }
+ componentWillUnmount() {
+ this._heightDisposer?.();
+ }
/**
* The function that is called on click to turn Hiding document till press option on/off.
@@ -61,13 +70,13 @@ export class PresElementBox extends DocComponent<FieldViewProps, PresDocument>(P
@action
onHideDocumentUntilPressClick = (e: React.MouseEvent) => {
e.stopPropagation();
- this.Document.hideTillShownButton = !this.Document.hideTillShownButton;
- if (!this.Document.hideTillShownButton) {
+ this.presElementDoc.hideTillShownButton = !this.presElementDoc.hideTillShownButton;
+ if (!this.presElementDoc.hideTillShownButton) {
if (this.indexInPres >= this.currentIndex && this.targetDoc) {
this.targetDoc.opacity = 1;
}
} else {
- if (this.presentationDoc.presStatus && this.indexInPres > this.currentIndex && this.targetDoc) {
+ if (this.presBoxDoc.presStatus && this.indexInPres > this.currentIndex && this.targetDoc) {
this.targetDoc.opacity = 0;
}
}
@@ -81,14 +90,14 @@ export class PresElementBox extends DocComponent<FieldViewProps, PresDocument>(P
@action
onHideDocumentAfterPresentedClick = (e: React.MouseEvent) => {
e.stopPropagation();
- this.Document.hideAfterButton = !this.Document.hideAfterButton;
- if (!this.Document.hideAfterButton) {
+ this.presElementDoc.hideAfterButton = !this.presElementDoc.hideAfterButton;
+ if (!this.presElementDoc.hideAfterButton) {
if (this.indexInPres <= this.currentIndex && this.targetDoc) {
this.targetDoc.opacity = 1;
}
} else {
- if (this.Document.fadeButton) this.Document.fadeButton = false;
- if (this.presentationDoc.presStatus && this.indexInPres < this.currentIndex && this.targetDoc) {
+ if (this.presElementDoc.fadeButton) this.presElementDoc.fadeButton = false;
+ if (this.presBoxDoc.presStatus && this.indexInPres < this.currentIndex && this.targetDoc) {
this.targetDoc.opacity = 0;
}
}
@@ -102,14 +111,14 @@ export class PresElementBox extends DocComponent<FieldViewProps, PresDocument>(P
@action
onFadeDocumentAfterPresentedClick = (e: React.MouseEvent) => {
e.stopPropagation();
- this.Document.fadeButton = !this.Document.fadeButton;
- if (!this.Document.fadeButton) {
+ this.presElementDoc.fadeButton = !this.presElementDoc.fadeButton;
+ if (!this.presElementDoc.fadeButton) {
if (this.indexInPres <= this.currentIndex && this.targetDoc) {
this.targetDoc.opacity = 1;
}
} else {
- this.Document.hideAfterButton = false;
- if (this.presentationDoc.presStatus && (this.indexInPres < this.currentIndex) && this.targetDoc) {
+ this.presElementDoc.hideAfterButton = false;
+ if (this.presBoxDoc.presStatus && (this.indexInPres < this.currentIndex) && this.targetDoc) {
this.targetDoc.opacity = 0.5;
}
}
@@ -121,11 +130,11 @@ export class PresElementBox extends DocComponent<FieldViewProps, PresDocument>(P
@action
onNavigateDocumentClick = (e: React.MouseEvent) => {
e.stopPropagation();
- this.Document.navButton = !this.Document.navButton;
- if (this.Document.navButton) {
- this.Document.showButton = false;
+ this.presElementDoc.navButton = !this.presElementDoc.navButton;
+ if (this.presElementDoc.navButton) {
+ this.presElementDoc.zoomButton = false;
if (this.currentIndex === this.indexInPres) {
- this.props.focus(this.props.Document);
+ this.props.focus(this.presElementDoc);
}
}
}
@@ -137,13 +146,13 @@ export class PresElementBox extends DocComponent<FieldViewProps, PresDocument>(P
onZoomDocumentClick = (e: React.MouseEvent) => {
e.stopPropagation();
- this.Document.showButton = !this.Document.showButton;
- if (!this.Document.showButton) {
- this.props.Document.viewScale = 1;
+ this.presElementDoc.zoomButton = !this.presElementDoc.zoomButton;
+ if (!this.presElementDoc.zoomButton) {
+ this.presElementDoc.viewScale = 1;
} else {
- this.Document.navButton = false;
+ this.presElementDoc.navButton = false;
if (this.currentIndex === this.indexInPres) {
- this.props.focus(this.props.Document);
+ this.props.focus(this.presElementDoc);
}
}
}
@@ -152,67 +161,59 @@ export class PresElementBox extends DocComponent<FieldViewProps, PresDocument>(P
*/
ScreenToLocalListTransform = (xCord: number, yCord: number) => [xCord, yCord];
+ embedHeight = () => this.props.PanelHeight() - NumCast(this.presElementDoc.collapsedHeight);
+ embedWidth = () => this.props.PanelWidth() - 20;
/**
* The function that is responsible for rendering the a preview or not for this
* presentation element.
*/
renderEmbeddedInline = () => {
- if (!this.Document.embedOpen || !this.targetDoc) {
- return (null);
- }
-
- const propDocWidth = NumCast(this.layoutDoc._nativeWidth);
- const propDocHeight = NumCast(this.layoutDoc._nativeHeight);
- const scale = () => 175 / NumCast(this.layoutDoc._nativeWidth, 175);
- return (
- <div className="presElementBox-embedded" style={{
- height: propDocHeight === 0 ? NumCast(this.layoutDoc._height) - NumCast(this.layoutDoc.collapsedHeight) : propDocHeight * scale(),
- width: propDocWidth === 0 ? "auto" : propDocWidth * scale(),
- }}>
+ return !this.presElementDoc.expandInlineButton || !this.targetDoc ? (null) :
+ <div className="presElementBox-embedded" style={{ height: this.embedHeight() }}>
<ContentFittingDocumentView
Document={this.targetDoc}
+ DataDocument={this.targetDoc[DataSym] !== this.targetDoc && this.targetDoc[DataSym]}
LibraryPath={emptyPath}
- fitToBox={StrCast(this.targetDoc.type).indexOf(DocumentType.COL) !== -1}
+ fitToBox={true}
addDocument={returnFalse}
removeDocument={returnFalse}
addDocTab={returnFalse}
pinToPres={returnFalse}
- PanelWidth={() => this.props.PanelWidth() - 20}
- PanelHeight={() => 100}
+ PanelWidth={this.embedWidth}
+ PanelHeight={this.embedHeight}
getTransform={Transform.Identity}
active={this.props.active}
moveDocument={this.props.moveDocument!}
- renderDepth={1}
+ renderDepth={this.props.renderDepth + 1}
focus={emptyFunction}
whenActiveChanged={returnFalse}
/>
<div className="presElementBox-embeddedMask" />
- </div>
- );
+ </div>;
}
render() {
const treecontainer = this.props.ContainingCollectionDoc?._viewType === CollectionViewType.Tree;
const className = "presElementBox-item" + (this.currentIndex === this.indexInPres ? " presElementBox-selected" : "");
const pbi = "presElementBox-interaction";
- return (
+ return !this.presElementDoc ? (null) : (
<div className={className} key={this.props.Document[Id] + this.indexInPres}
style={{ outlineWidth: Doc.IsBrushed(this.targetDoc) ? `1px` : "0px", }}
- onClick={e => { this.props.focus(this.props.Document); e.stopPropagation(); }}>
+ onClick={e => { this.props.focus(this.presElementDoc); e.stopPropagation(); }}>
{treecontainer ? (null) : <>
<strong className="presElementBox-name">
- {`${this.indexInPres + 1}. ${this.Document.title}`}
+ {`${this.indexInPres + 1}. ${this.targetDoc?.title}`}
</strong>
- <button className="presElementBox-closeIcon" onPointerDown={e => e.stopPropagation()} onClick={e => this.props.removeDocument && this.props.removeDocument(this.props.Document)}>X</button>
+ <button className="presElementBox-closeIcon" onPointerDown={e => e.stopPropagation()} onClick={e => this.props.removeDocument && this.props.removeDocument(this.presElementDoc)}>X</button>
<br />
</>}
- <button title="Zoom" className={pbi + (this.Document.showButton ? "-selected" : "")} onPointerDown={e => e.stopPropagation()} onClick={this.onZoomDocumentClick}><FontAwesomeIcon icon={"search"} /></button>
- <button title="Navigate" className={pbi + (this.Document.navButton ? "-selected" : "")} onPointerDown={e => e.stopPropagation()} onClick={this.onNavigateDocumentClick}><FontAwesomeIcon icon={"location-arrow"} /></button>
- <button title="Hide Before" className={pbi + (this.Document.hideTillShownButton ? "-selected" : "")} onPointerDown={e => e.stopPropagation()} onClick={this.onHideDocumentUntilPressClick}><FontAwesomeIcon icon={fileSolid} /></button>
- <button title="Fade After" className={pbi + (this.Document.fadeButton ? "-selected" : "")} onPointerDown={e => e.stopPropagation()} onClick={this.onFadeDocumentAfterPresentedClick}><FontAwesomeIcon icon={faFileDownload} /></button>
- <button title="Hide After" className={pbi + (this.Document.hideAfterButton ? "-selected" : "")} onPointerDown={e => e.stopPropagation()} onClick={this.onHideDocumentAfterPresentedClick}><FontAwesomeIcon icon={faFileDownload} /></button>
- <button title="Group With Up" className={pbi + (this.Document.groupButton ? "-selected" : "")} onPointerDown={e => e.stopPropagation()} onClick={e => { e.stopPropagation(); this.Document.groupButton = !this.Document.groupButton; }}><FontAwesomeIcon icon={"arrow-up"} /></button>
- <button title="Expand Inline" className={pbi + (this.Document.embedOpen ? "-selected" : "")} onPointerDown={e => e.stopPropagation()} onClick={e => { e.stopPropagation(); this.Document.embedOpen = !this.Document.embedOpen; }}><FontAwesomeIcon icon={"arrow-down"} /></button>
+ <button title="Zoom" className={pbi + (this.presElementDoc.zoomButton ? "-selected" : "")} onPointerDown={e => e.stopPropagation()} onClick={this.onZoomDocumentClick}><FontAwesomeIcon icon={"search"} /></button>
+ <button title="Navigate" className={pbi + (this.presElementDoc.navButton ? "-selected" : "")} onPointerDown={e => e.stopPropagation()} onClick={this.onNavigateDocumentClick}><FontAwesomeIcon icon={"location-arrow"} /></button>
+ <button title="Hide Before" className={pbi + (this.presElementDoc.hideTillShownButton ? "-selected" : "")} onPointerDown={e => e.stopPropagation()} onClick={this.onHideDocumentUntilPressClick}><FontAwesomeIcon icon={fileSolid} /></button>
+ <button title="Fade After" className={pbi + (this.presElementDoc.fadeButton ? "-selected" : "")} onPointerDown={e => e.stopPropagation()} onClick={this.onFadeDocumentAfterPresentedClick}><FontAwesomeIcon icon={faFileDownload} /></button>
+ <button title="Hide After" className={pbi + (this.presElementDoc.hideAfterButton ? "-selected" : "")} onPointerDown={e => e.stopPropagation()} onClick={this.onHideDocumentAfterPresentedClick}><FontAwesomeIcon icon={faFileDownload} /></button>
+ <button title="Group With Up" className={pbi + (this.presElementDoc.groupButton ? "-selected" : "")} onPointerDown={e => e.stopPropagation()} onClick={e => { e.stopPropagation(); this.presElementDoc.groupButton = !this.presElementDoc.groupButton; }}><FontAwesomeIcon icon={"arrow-up"} /></button>
+ <button title="Expand Inline" className={pbi + (this.presElementDoc.expandInlineButton ? "-selected" : "")} onPointerDown={e => e.stopPropagation()} onClick={e => { e.stopPropagation(); this.presElementDoc.expandInlineButton = !this.presElementDoc.expandInlineButton; }}><FontAwesomeIcon icon={"arrow-down"} /></button>
<br style={{ lineHeight: 0.1 }} />
{this.renderEmbeddedInline()}
diff --git a/src/client/views/search/FilterBox.tsx b/src/client/views/search/FilterBox.tsx
index 432e18b22..1c05ff864 100644
--- a/src/client/views/search/FilterBox.tsx
+++ b/src/client/views/search/FilterBox.tsx
@@ -33,7 +33,7 @@ export enum Keys {
export class FilterBox extends React.Component {
static Instance: FilterBox;
- public _allIcons: string[] = [DocumentType.AUDIO, DocumentType.COL, DocumentType.IMG, DocumentType.LINK, DocumentType.PDF, DocumentType.TEXT, DocumentType.VID, DocumentType.WEB, DocumentType.TEMPLATE];
+ public _allIcons: string[] = [DocumentType.AUDIO, DocumentType.COL, DocumentType.IMG, DocumentType.LINK, DocumentType.PDF, DocumentType.TEXT, DocumentType.VID, DocumentType.WEB];
//if true, any keywords can be used. if false, all keywords are required.
//this also serves as an indicator if the word status filter is applied
diff --git a/src/client/views/search/IconBar.tsx b/src/client/views/search/IconBar.tsx
index fe7dd4223..46c109934 100644
--- a/src/client/views/search/IconBar.tsx
+++ b/src/client/views/search/IconBar.tsx
@@ -26,7 +26,7 @@ library.add(faBan);
@observer
export class IconBar extends React.Component {
- public _allIcons: string[] = [DocumentType.AUDIO, DocumentType.COL, DocumentType.IMG, DocumentType.LINK, DocumentType.PDF, DocumentType.TEXT, DocumentType.VID, DocumentType.WEB, DocumentType.TEMPLATE];
+ public _allIcons: string[] = [DocumentType.AUDIO, DocumentType.COL, DocumentType.IMG, DocumentType.LINK, DocumentType.PDF, DocumentType.TEXT, DocumentType.VID, DocumentType.WEB];
@observable private _icons: string[] = this._allIcons;
diff --git a/src/client/views/search/SearchItem.tsx b/src/client/views/search/SearchItem.tsx
index 8aea737f0..63cef5101 100644
--- a/src/client/views/search/SearchItem.tsx
+++ b/src/client/views/search/SearchItem.tsx
@@ -4,24 +4,24 @@ import { faCaretUp, faChartBar, faFile, faFilePdf, faFilm, faFingerprint, faGlob
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { action, computed, observable, runInAction } from "mobx";
import { observer } from "mobx-react";
-import { Doc, DocListCast } from "../../../new_fields/Doc";
+import { Doc } from "../../../new_fields/Doc";
import { Id } from "../../../new_fields/FieldSymbols";
import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
-import { emptyFunction, returnEmptyString, returnFalse, returnOne, Utils, emptyPath } from "../../../Utils";
+import { emptyFunction, emptyPath, returnFalse, Utils } from "../../../Utils";
import { DocumentType } from "../../documents/DocumentTypes";
import { DocumentManager } from "../../util/DocumentManager";
import { DragManager, SetupDrag } from "../../util/DragManager";
import { SearchUtil } from "../../util/SearchUtil";
import { Transform } from "../../util/Transform";
import { SEARCH_THUMBNAIL_SIZE } from "../../views/globalCssVariables.scss";
-import { CollectionViewType } from "../collections/CollectionView";
import { CollectionDockingView } from "../collections/CollectionDockingView";
+import { CollectionViewType } from "../collections/CollectionView";
+import { ParentDocSelector } from "../collections/ParentDocumentSelector";
import { ContextMenu } from "../ContextMenu";
+import { ContentFittingDocumentView } from "../nodes/ContentFittingDocumentView";
import { SearchBox } from "./SearchBox";
import "./SearchItem.scss";
import "./SelectorContextMenu.scss";
-import { ContentFittingDocumentView } from "../nodes/ContentFittingDocumentView";
-import { ButtonSelector, ParentDocSelector } from "../collections/ParentDocumentSelector";
export interface SearchItemProps {
doc: Doc;
@@ -74,7 +74,7 @@ export class SelectorContextMenu extends React.Component<SearchItemProps> {
col._panX = newPanX;
col._panY = newPanY;
}
- CollectionDockingView.AddRightSplit(col, undefined);
+ CollectionDockingView.AddRightSplit(col);
};
}
render() {
@@ -108,7 +108,7 @@ export class LinkContextMenu extends React.Component<LinkMenuProps> {
unHighlightDoc = (doc: Doc) => () => Doc.UnBrushDoc(doc);
- getOnClick = (col: Doc) => () => CollectionDockingView.AddRightSplit(col, undefined);
+ getOnClick = (col: Doc) => () => CollectionDockingView.AddRightSplit(col);
render() {
return (
@@ -272,7 +272,7 @@ export class SearchItem extends React.Component<SearchItemProps> {
@computed
get contextButton() {
- return <ParentDocSelector Views={DocumentManager.Instance.DocumentViews} Document={this.props.doc} addDocTab={(doc, data, where) => CollectionDockingView.AddRightSplit(doc, data)} />;
+ return <ParentDocSelector Document={this.props.doc} addDocTab={(doc, where) => CollectionDockingView.AddRightSplit(doc)} />;
}
render() {
diff --git a/src/client/views/webcam/DashWebRTCVideo.scss b/src/client/views/webcam/DashWebRTCVideo.scss
new file mode 100644
index 000000000..41307a808
--- /dev/null
+++ b/src/client/views/webcam/DashWebRTCVideo.scss
@@ -0,0 +1,83 @@
+@import "../globalCssVariables";
+
+.webcam-cont {
+ background: whitesmoke;
+ color: grey;
+ border-radius: 15px;
+ box-shadow: #9c9396 0.2vw 0.2vw 0.4vw;
+ border: solid #BBBBBBBB 5px;
+ pointer-events: all;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+
+ .webcam-header {
+ height: 50px;
+ text-align: center;
+ text-transform: uppercase;
+ letter-spacing: 2px;
+ font-size: 16px;
+ width: 100%;
+ margin-top: 20px;
+ }
+
+ .videoContainer {
+ position: relative;
+ width: calc(100% - 20px);
+ height: 100%;
+ /* border: 10px solid red; */
+ margin-left: 10px;
+ }
+
+ .buttonContainer {
+ display: flex;
+ width: calc(100% - 20px);
+ height: 50px;
+ justify-content: center;
+ text-align: center;
+ /* border: 1px solid black; */
+ margin-left: 10px;
+ margin-top: 0;
+ margin-bottom: 15px;
+ }
+
+ #roomName {
+ outline: none;
+ border-radius: inherit;
+ border: 1px solid #BBBBBBBB;
+ margin: 10px;
+ padding: 10px;
+ }
+
+ .side {
+ width: 25%;
+ height: 20%;
+ position: absolute;
+ /* top: 65%; */
+ z-index: 2;
+ right: 0px;
+ bottom: 18px;
+ }
+
+ .main {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ /* top: 20%; */
+ align-self: center;
+ }
+
+ .videoButtons {
+ border-radius: 50%;
+ height: 30px;
+ width: 30px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ justify-self: center;
+ align-self: center;
+ margin: 5px;
+ border: 1px solid black;
+ }
+
+} \ No newline at end of file
diff --git a/src/client/views/webcam/DashWebRTCVideo.tsx b/src/client/views/webcam/DashWebRTCVideo.tsx
new file mode 100644
index 000000000..1d52ba38f
--- /dev/null
+++ b/src/client/views/webcam/DashWebRTCVideo.tsx
@@ -0,0 +1,89 @@
+import { observer } from "mobx-react";
+import React = require("react");
+import { CollectionFreeFormDocumentViewProps } from "../nodes/CollectionFreeFormDocumentView";
+import { FieldViewProps, FieldView } from "../nodes/FieldView";
+import { observable, action } from "mobx";
+import { DocumentDecorations, CloseCall } from "../DocumentDecorations";
+import { InkingControl } from "../InkingControl";
+import "../../views/nodes/WebBox.scss";
+import "./DashWebRTCVideo.scss";
+import adapter from 'webrtc-adapter';
+import { initialize, hangup, refreshVideos } from "./WebCamLogic";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { IconProp, library } from '@fortawesome/fontawesome-svg-core';
+import { faSync, faPhoneSlash } from "@fortawesome/free-solid-svg-icons";
+
+library.add(faSync);
+library.add(faPhoneSlash);
+
+
+/**
+ * This models the component that will be rendered, that can be used as a doc that will reflect the video cams.
+ */
+@observer
+export class DashWebRTCVideo extends React.Component<CollectionFreeFormDocumentViewProps & FieldViewProps> {
+
+ private roomText: HTMLInputElement | undefined;
+ @observable remoteVideoAdded: boolean = false;
+
+ @action
+ changeUILook = () => {
+ this.remoteVideoAdded = true;
+ }
+
+ /**
+ * Function that submits the title entered by user on enter press.
+ */
+ private onEnterKeyDown = (e: React.KeyboardEvent) => {
+ if (e.keyCode === 13) {
+ const submittedTitle = this.roomText!.value;
+ this.roomText!.value = "";
+ this.roomText!.blur();
+ initialize(submittedTitle, this.changeUILook);
+ }
+ }
+
+
+ public static LayoutString(fieldKey: string) { return FieldView.LayoutString(DashWebRTCVideo, fieldKey); }
+
+ @action
+ onClickRefresh = () => {
+ refreshVideos();
+ }
+
+ onClickHangUp = () => {
+ hangup();
+ }
+
+ render() {
+ const content =
+ <div className="webcam-cont" style={{ width: "100%", height: "100%" }}>
+ <div className="webcam-header">DashWebRTC</div>
+ <input id="roomName" type="text" placeholder="Enter room name" ref={(e) => this.roomText = e!} onKeyDown={this.onEnterKeyDown} />
+ <div className="videoContainer">
+ <video id="localVideo" className={"RTCVideo" + (this.remoteVideoAdded ? " side" : " main")} autoPlay playsInline muted ref={(e) => {
+ }}></video>
+ <video id="remoteVideo" className="RTCVideo main" autoPlay playsInline ref={(e) => {
+ }}></video>
+ </div>
+ <div className="buttonContainer">
+ <div className="videoButtons" style={{ background: "red" }} onClick={this.onClickHangUp}><FontAwesomeIcon icon={faPhoneSlash} color="white" /></div>
+ <div className="videoButtons" style={{ background: "green" }} onClick={this.onClickRefresh}><FontAwesomeIcon icon={faSync} color="white" /></div>
+ </div>
+ </div >;
+
+ const frozen = !this.props.isSelected() || DocumentDecorations.Instance.Interacting;
+ const classname = "webBox-cont" + (this.props.isSelected() && !InkingControl.Instance.selectedTool && !DocumentDecorations.Instance.Interacting ? "-interactive" : "");
+
+
+ return (
+ <>
+ <div className={classname} >
+ {content}
+ </div>
+ {!frozen ? (null) : <div className="webBox-overlay" />}
+ </>);
+ }
+
+
+} \ No newline at end of file
diff --git a/src/client/views/webcam/WebCamLogic.js b/src/client/views/webcam/WebCamLogic.js
new file mode 100644
index 000000000..f542fb983
--- /dev/null
+++ b/src/client/views/webcam/WebCamLogic.js
@@ -0,0 +1,292 @@
+'use strict';
+import io from "socket.io-client";
+
+var socket;
+var isChannelReady = false;
+var isInitiator = false;
+var isStarted = false;
+var localStream;
+var pc;
+var remoteStream;
+var turnReady;
+var room;
+
+export function initialize(roomName, handlerUI) {
+
+ var pcConfig = {
+ 'iceServers': [{
+ 'urls': 'stun:stun.l.google.com:19302'
+ }]
+ };
+
+ // Set up audio and video regardless of what devices are present.
+ var sdpConstraints = {
+ offerToReceiveAudio: true,
+ offerToReceiveVideo: true
+ };
+
+ /////////////////////////////////////////////
+
+ room = roomName;
+
+ socket = io.connect(`${window.location.protocol}//${window.location.hostname}:${4321}`);
+
+ if (room !== '') {
+ socket.emit('create or join', room);
+ console.log('Attempted to create or join room', room);
+ }
+
+ socket.on('created', function (room) {
+ console.log('Created room ' + room);
+ isInitiator = true;
+ });
+
+ socket.on('full', function (room) {
+ console.log('Room ' + room + ' is full');
+ });
+
+ socket.on('join', function (room) {
+ console.log('Another peer made a request to join room ' + room);
+ console.log('This peer is the initiator of room ' + room + '!');
+ isChannelReady = true;
+ });
+
+ socket.on('joined', function (room) {
+ console.log('joined: ' + room);
+ isChannelReady = true;
+ });
+
+ socket.on('log', function (array) {
+ console.log.apply(console, array);
+ });
+
+ ////////////////////////////////////////////////
+
+
+ // This client receives a message
+ socket.on('message', function (message) {
+ console.log('Client received message:', message);
+ if (message === 'got user media') {
+ maybeStart();
+ } else if (message.type === 'offer') {
+ if (!isInitiator && !isStarted) {
+ maybeStart();
+ }
+ pc.setRemoteDescription(new RTCSessionDescription(message));
+ doAnswer();
+ } else if (message.type === 'answer' && isStarted) {
+ pc.setRemoteDescription(new RTCSessionDescription(message));
+ } else if (message.type === 'candidate' && isStarted) {
+ var candidate = new RTCIceCandidate({
+ sdpMLineIndex: message.label,
+ candidate: message.candidate
+ });
+ pc.addIceCandidate(candidate);
+ } else if (message === 'bye' && isStarted) {
+ handleRemoteHangup();
+ }
+ });
+
+ ////////////////////////////////////////////////////
+
+ var localVideo = document.querySelector('#localVideo');
+ var remoteVideo = document.querySelector('#remoteVideo');
+
+ const gotStream = (stream) => {
+ console.log('Adding local stream.');
+ localStream = stream;
+ localVideo.srcObject = stream;
+ sendMessage('got user media');
+ if (isInitiator) {
+ maybeStart();
+ }
+ }
+
+
+ navigator.mediaDevices.getUserMedia({
+ audio: true,
+ video: true
+ })
+ .then(gotStream)
+ .catch(function (e) {
+ alert('getUserMedia() error: ' + e.name);
+ });
+
+
+
+ var constraints = {
+ video: true
+ };
+
+ console.log('Getting user media with constraints', constraints);
+
+ const requestTurn = (turnURL) => {
+ var turnExists = false;
+ for (var i in pcConfig.iceServers) {
+ if (pcConfig.iceServers[i].urls.substr(0, 5) === 'turn:') {
+ turnExists = true;
+ turnReady = true;
+ break;
+ }
+ }
+ if (!turnExists) {
+ console.log('Getting TURN server from ', turnURL);
+ // No TURN server. Get one from computeengineondemand.appspot.com:
+ var xhr = new XMLHttpRequest();
+ xhr.onreadystatechange = function () {
+ if (xhr.readyState === 4 && xhr.status === 200) {
+ var turnServer = JSON.parse(xhr.responseText);
+ console.log('Got TURN server: ', turnServer);
+ pcConfig.iceServers.push({
+ 'urls': 'turn:' + turnServer.username + '@' + turnServer.turn,
+ 'credential': turnServer.password
+ });
+ turnReady = true;
+ }
+ };
+ xhr.open('GET', turnURL, true);
+ xhr.send();
+ }
+ }
+
+
+
+
+ if (location.hostname !== 'localhost') {
+ requestTurn(
+ `${window.location.origin}/corsProxy/${encodeURIComponent("https://computeengineondemand.appspot.com/turn?username=41784574&key=4080218913")}`
+ );
+ }
+
+ const maybeStart = () => {
+ console.log('>>>>>>> maybeStart() ', isStarted, localStream, isChannelReady);
+ if (!isStarted && typeof localStream !== 'undefined' && isChannelReady) {
+ console.log('>>>>>> creating peer connection');
+ createPeerConnection();
+ pc.addStream(localStream);
+ isStarted = true;
+ console.log('isInitiator', isInitiator);
+ if (isInitiator) {
+ doCall();
+ }
+ }
+ };
+
+ window.onbeforeunload = function () {
+ sendMessage('bye');
+ };
+
+ /////////////////////////////////////////////////////////
+
+ const createPeerConnection = () => {
+ try {
+ pc = new RTCPeerConnection(null);
+ pc.onicecandidate = handleIceCandidate;
+ pc.onaddstream = handleRemoteStreamAdded;
+ pc.onremovestream = handleRemoteStreamRemoved;
+ console.log('Created RTCPeerConnnection');
+ } catch (e) {
+ console.log('Failed to create PeerConnection, exception: ' + e.message);
+ alert('Cannot create RTCPeerConnection object.');
+ return;
+ }
+ }
+
+ const handleIceCandidate = (event) => {
+ console.log('icecandidate event: ', event);
+ if (event.candidate) {
+ sendMessage({
+ type: 'candidate',
+ label: event.candidate.sdpMLineIndex,
+ id: event.candidate.sdpMid,
+ candidate: event.candidate.candidate
+ });
+ } else {
+ console.log('End of candidates.');
+ }
+ }
+
+ const handleCreateOfferError = (event) => {
+ console.log('createOffer() error: ', event);
+ }
+
+ const doCall = () => {
+ console.log('Sending offer to peer');
+ pc.createOffer(setLocalAndSendMessage, handleCreateOfferError);
+ }
+
+ const doAnswer = () => {
+ console.log('Sending answer to peer.');
+ pc.createAnswer().then(
+ setLocalAndSendMessage,
+ onCreateSessionDescriptionError
+ );
+ }
+
+ const setLocalAndSendMessage = (sessionDescription) => {
+ pc.setLocalDescription(sessionDescription);
+ console.log('setLocalAndSendMessage sending message', sessionDescription);
+ sendMessage(sessionDescription);
+ }
+
+ const onCreateSessionDescriptionError = (error) => {
+ trace('Failed to create session description: ' + error.toString());
+ }
+
+
+
+ const handleRemoteStreamAdded = (event) => {
+ console.log('Remote stream added.');
+ remoteStream = event.stream;
+ remoteVideo.srcObject = remoteStream;
+ handlerUI();
+
+ };
+
+ const handleRemoteStreamRemoved = (event) => {
+ console.log('Remote stream removed. Event: ', event);
+ }
+}
+
+export function hangup() {
+ console.log('Hanging up.');
+ stop();
+ sendMessage('bye');
+ if (localStream) {
+ localStream.getTracks().forEach(track => track.stop());
+ }
+}
+
+function stop() {
+ isStarted = false;
+ if (pc) {
+ pc.close();
+ }
+ pc = null;
+}
+
+function handleRemoteHangup() {
+ console.log('Session terminated.');
+ stop();
+ isInitiator = false;
+ if (localStream) {
+ localStream.getTracks().forEach(track => track.stop());
+ }
+}
+
+function sendMessage(message) {
+ console.log('Client sending message: ', message);
+ socket.emit('message', message, room);
+};
+
+export function refreshVideos() {
+ var localVideo = document.querySelector('#localVideo');
+ var remoteVideo = document.querySelector('#remoteVideo');
+ if (localVideo) {
+ localVideo.srcObject = localStream;
+ }
+ if (remoteVideo) {
+ remoteVideo.srcObject = remoteStream;
+ }
+
+} \ No newline at end of file
diff --git a/src/mobile/ImageUpload.tsx b/src/mobile/ImageUpload.tsx
index 1583e3d5d..5903a2ce9 100644
--- a/src/mobile/ImageUpload.tsx
+++ b/src/mobile/ImageUpload.tsx
@@ -13,6 +13,7 @@ import { observable } from 'mobx';
import { Utils } from '../Utils';
import MobileInterface from './MobileInterface';
import { CurrentUserUtils } from '../server/authentication/models/current_user_utils';
+import { Scripting } from '../client/util/Scripting';
@@ -27,12 +28,11 @@ const inputRef = React.createRef<HTMLInputElement>();
@observer
class Uploader extends React.Component {
- @observable
- error: string = "";
- @observable
- status: string = "";
+ @observable error: string = "";
+ @observable status: string = "";
onClick = async () => {
+ console.log("uploader click");
try {
this.status = "initializing protos";
await Docs.Prototypes.initialize();
@@ -47,6 +47,7 @@ class Uploader extends React.Component {
const upload = window.location.origin + "/uploadFormData";
this.status = "uploading image";
+ console.log("uploading image", formData);
const res = await fetch(upload, {
method: 'POST',
body: formData
diff --git a/src/mobile/MobileInkOverlay.scss b/src/mobile/MobileInkOverlay.scss
new file mode 100644
index 000000000..b9c1fb146
--- /dev/null
+++ b/src/mobile/MobileInkOverlay.scss
@@ -0,0 +1,39 @@
+.mobileInkOverlay {
+ border: 10px dashed red;
+ background-color: rgba(0, 0, 0, .05);
+}
+
+.mobileInkOverlay-border {
+ // background-color: rgba(0, 255, 0, .4);
+ position: absolute;
+ pointer-events: auto;
+ cursor: pointer;
+
+ &.top {
+ width: calc(100% + 20px);
+ height: 10px;
+ top: -10px;
+ left: -10px;
+ }
+
+ &.left {
+ width: 10px;
+ height: calc(100% + 20px);
+ top: -10px;
+ left: -10px;
+ }
+
+ &.right {
+ width: 10px;
+ height: calc(100% + 20px);
+ top: -10px;
+ right: -10px;
+ }
+
+ &.bottom {
+ width: calc(100% + 20px);
+ height: 10px;
+ bottom: -10px;
+ left: -10px;
+ }
+} \ No newline at end of file
diff --git a/src/mobile/MobileInkOverlay.tsx b/src/mobile/MobileInkOverlay.tsx
new file mode 100644
index 000000000..1537ae034
--- /dev/null
+++ b/src/mobile/MobileInkOverlay.tsx
@@ -0,0 +1,191 @@
+import React = require('react');
+import { observer } from "mobx-react";
+import { MobileInkOverlayContent, GestureContent, UpdateMobileInkOverlayPositionContent, MobileDocumentUploadContent } from "../server/Message";
+import { observable, action } from "mobx";
+import { GestureUtils } from "../pen-gestures/GestureUtils";
+import "./MobileInkOverlay.scss";
+import { StrCast, Cast } from '../new_fields/Types';
+import { DragManager } from "../client/util/DragManager";
+import { DocServer } from '../client/DocServer';
+import { Doc, DocListCastAsync } from '../new_fields/Doc';
+import { listSpec } from '../new_fields/Schema';
+
+
+@observer
+export default class MobileInkOverlay extends React.Component {
+ public static Instance: MobileInkOverlay;
+
+ @observable private _scale: number = 1;
+ @observable private _width: number = 0;
+ @observable private _height: number = 0;
+ @observable private _x: number = -300;
+ @observable private _y: number = -300;
+ @observable private _text: string = "";
+
+ @observable private _offsetX: number = 0;
+ @observable private _offsetY: number = 0;
+ @observable private _isDragging: boolean = false;
+ private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
+
+ constructor(props: Readonly<{}>) {
+ super(props);
+ MobileInkOverlay.Instance = this;
+ }
+
+ initialSize(mobileWidth: number, mobileHeight: number) {
+ const maxWidth = window.innerWidth - 30;
+ const maxHeight = window.innerHeight - 30; // -30 for padding
+ if (mobileWidth > maxWidth || mobileHeight > maxHeight) {
+ const scale = Math.min(maxWidth / mobileWidth, maxHeight / mobileHeight);
+ return { width: mobileWidth * scale, height: mobileHeight * scale, scale: scale };
+ }
+ return { width: mobileWidth, height: mobileHeight, scale: 1 };
+ }
+
+ @action
+ initMobileInkOverlay(content: MobileInkOverlayContent) {
+ const { width, height, text } = content;
+ const scaledSize = this.initialSize(width ? width : 0, height ? height : 0);
+ this._width = scaledSize.width;
+ this._height = scaledSize.height;
+ this._scale = scaledSize.scale;
+ this._x = 300; // TODO: center on screen
+ this._y = 25; // TODO: center on screen
+ this._text = text ? text : "";
+ }
+
+ @action
+ updatePosition(content: UpdateMobileInkOverlayPositionContent) {
+ const { dx, dy, dsize } = content;
+ if (dx) this._x += dx;
+ if (dy) this._y += dy;
+ // TODO: scale dsize
+ }
+
+ drawStroke = (content: GestureContent) => {
+ // TODO: figure out why strokes drawn in corner of mobile interface dont get inserted
+
+ const { points, bounds } = content;
+ console.log("received points", points, bounds);
+
+ const B = {
+ right: (bounds.right * this._scale) + this._x,
+ left: (bounds.left * this._scale) + this._x, // TODO: scale
+ bottom: (bounds.bottom * this._scale) + this._y,
+ top: (bounds.top * this._scale) + this._y, // TODO: scale
+ width: bounds.width * this._scale,
+ height: bounds.height * this._scale,
+ };
+
+ const target = document.elementFromPoint(this._x + 10, this._y + 10);
+ target?.dispatchEvent(
+ new CustomEvent<GestureUtils.GestureEvent>("dashOnGesture",
+ {
+ bubbles: true,
+ detail: {
+ points: points,
+ gesture: GestureUtils.Gestures.Stroke,
+ bounds: B
+ }
+ }
+ )
+ );
+ }
+
+ uploadDocument = async (content: MobileDocumentUploadContent) => {
+ const { docId } = content;
+ const doc = await DocServer.GetRefField(docId);
+
+ if (doc && doc instanceof Doc) {
+ const target = document.elementFromPoint(this._x + 10, this._y + 10);
+ const dragData = new DragManager.DocumentDragData([doc]);
+ const complete = new DragManager.DragCompleteEvent(false, dragData);
+
+ if (target) {
+ console.log("dispatching upload doc!!!!", target, doc);
+ target.dispatchEvent(
+ new CustomEvent<DragManager.DropEvent>("dashOnDrop",
+ {
+ bubbles: true,
+ detail: {
+ x: this._x,
+ y: this._y,
+ complete: complete,
+ altKey: false,
+ metaKey: false,
+ ctrlKey: false,
+ shiftKey: false
+ }
+ }
+ )
+ );
+ } else {
+ alert("TARGET IS UNDEFINED");
+ }
+ }
+ }
+
+ @action
+ dragStart = (e: React.PointerEvent) => {
+ document.removeEventListener("pointermove", this.dragging);
+ document.removeEventListener("pointerup", this.dragEnd);
+ document.addEventListener("pointermove", this.dragging);
+ document.addEventListener("pointerup", this.dragEnd);
+
+ this._isDragging = true;
+ this._offsetX = e.pageX - this._mainCont.current!.getBoundingClientRect().left;
+ this._offsetY = e.pageY - this._mainCont.current!.getBoundingClientRect().top;
+
+ e.preventDefault();
+ e.stopPropagation();
+ }
+
+ @action
+ dragging = (e: PointerEvent) => {
+ const x = e.pageX - this._offsetX;
+ const y = e.pageY - this._offsetY;
+
+ // TODO: don't allow drag over library?
+ this._x = Math.min(Math.max(x, 0), window.innerWidth - this._width);
+ this._y = Math.min(Math.max(y, 0), window.innerHeight - this._height);
+
+ e.preventDefault();
+ e.stopPropagation();
+ }
+
+ @action
+ dragEnd = (e: PointerEvent) => {
+ document.removeEventListener("pointermove", this.dragging);
+ document.removeEventListener("pointerup", this.dragEnd);
+
+ this._isDragging = false;
+
+ e.preventDefault();
+ e.stopPropagation();
+ }
+
+ render() {
+
+ return (
+ <div className="mobileInkOverlay"
+ style={{
+ width: this._width,
+ height: this._height,
+ position: "absolute",
+ transform: `translate(${this._x}px, ${this._y}px)`,
+ zIndex: 30000,
+ pointerEvents: "none",
+ borderStyle: this._isDragging ? "solid" : "dashed",
+ }
+ }
+ ref={this._mainCont}
+ >
+ <p>{this._text}</p>
+ <div className="mobileInkOverlay-border top" onPointerDown={this.dragStart}></div>
+ <div className="mobileInkOverlay-border bottom" onPointerDown={this.dragStart}></div>
+ <div className="mobileInkOverlay-border left" onPointerDown={this.dragStart}></div>
+ <div className="mobileInkOverlay-border right" onPointerDown={this.dragStart}></div>
+ </div >
+ );
+ }
+} \ No newline at end of file
diff --git a/src/mobile/MobileInterface.scss b/src/mobile/MobileInterface.scss
new file mode 100644
index 000000000..4d86e208f
--- /dev/null
+++ b/src/mobile/MobileInterface.scss
@@ -0,0 +1,19 @@
+.mobileInterface-inkInterfaceButtons {
+ position: absolute;
+ top: 0px;
+ display: flex;
+ justify-content: space-between;
+ width: 100%;
+ z-index: 9999;
+ height: 50px;
+
+ .mobileInterface-button {
+ height: 100%;
+ }
+}
+
+.mobileInterface-container {
+ height: 100%;
+ position: relative;
+ touch-action: none;
+} \ No newline at end of file
diff --git a/src/mobile/MobileInterface.tsx b/src/mobile/MobileInterface.tsx
index b1eaeaa0a..1d2d57b96 100644
--- a/src/mobile/MobileInterface.tsx
+++ b/src/mobile/MobileInterface.tsx
@@ -1,40 +1,127 @@
import React = require('react');
import { observer } from 'mobx-react';
-import { computed, action } from 'mobx';
+import { computed, action, observable } from 'mobx';
import { CurrentUserUtils } from '../server/authentication/models/current_user_utils';
-import { FieldValue, Cast } from '../new_fields/Types';
-import { Doc } from '../new_fields/Doc';
+import { FieldValue, Cast, StrCast } from '../new_fields/Types';
+import { Doc, DocListCast } from '../new_fields/Doc';
import { Docs } from '../client/documents/Documents';
import { CollectionView } from '../client/views/collections/CollectionView';
import { DocumentView } from '../client/views/nodes/DocumentView';
import { emptyPath, emptyFunction, returnFalse, returnOne, returnEmptyString, returnTrue } from '../Utils';
import { Transform } from '../client/util/Transform';
import { library } from '@fortawesome/fontawesome-svg-core';
-import { faPenNib, faHighlighter, faEraser, faMousePointer } from '@fortawesome/free-solid-svg-icons';
+import { faPenNib, faHighlighter, faEraser, faMousePointer, faBreadSlice, faTrash, faCheck, faLongArrowAltLeft } from '@fortawesome/free-solid-svg-icons';
+import { Scripting } from '../client/util/Scripting';
+import { CollectionFreeFormView } from '../client/views/collections/collectionFreeForm/CollectionFreeFormView';
+import GestureOverlay from '../client/views/GestureOverlay';
+import { InkingControl } from '../client/views/InkingControl';
+import { InkTool } from '../new_fields/InkField';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import "./MobileInterface.scss";
+import { SelectionManager } from '../client/util/SelectionManager';
+import { DateField } from '../new_fields/DateField';
+import { GestureUtils } from '../pen-gestures/GestureUtils';
+import { DocServer } from '../client/DocServer';
+import { DocumentDecorations } from '../client/views/DocumentDecorations';
+import { OverlayView } from '../client/views/OverlayView';
+import { DictationOverlay } from '../client/views/DictationOverlay';
+import SharingManager from '../client/util/SharingManager';
+import { PreviewCursor } from '../client/views/PreviewCursor';
+import { ContextMenu } from '../client/views/ContextMenu';
+import { RadialMenu } from '../client/views/nodes/RadialMenu';
+import PDFMenu from '../client/views/pdf/PDFMenu';
+import MarqueeOptionsMenu from '../client/views/collections/collectionFreeForm/MarqueeOptionsMenu';
+import GoogleAuthenticationManager from '../client/apis/GoogleAuthenticationManager';
+import { listSpec } from '../new_fields/Schema';
+import { Id } from '../new_fields/FieldSymbols';
+import { DocumentManager } from '../client/util/DocumentManager';
+import RichTextMenu from '../client/util/RichTextMenu';
+import { WebField } from "../new_fields/URLField";
+import { FieldResult } from "../new_fields/Doc";
+import { List } from '../new_fields/List';
+
+library.add(faLongArrowAltLeft);
@observer
export default class MobileInterface extends React.Component {
+ @observable static Instance: MobileInterface;
@computed private get userDoc() { return CurrentUserUtils.UserDocument; }
@computed private get mainContainer() { return this.userDoc ? FieldValue(Cast(this.userDoc.activeMobile, Doc)) : CurrentUserUtils.GuestMobile; }
+ // @observable private currentView: "main" | "ink" | "upload" = "main";
+ private mainDoc: any = CurrentUserUtils.setupMobileDoc(this.userDoc);
+ @observable private renderView?: () => JSX.Element;
+
+ // private inkDoc?: Doc;
+ public drawingInk: boolean = false;
+
+ // private uploadDoc?: Doc;
+
+ constructor(props: Readonly<{}>) {
+ super(props);
+ MobileInterface.Instance = this;
+ }
@action
componentDidMount = () => {
library.add(...[faPenNib, faHighlighter, faEraser, faMousePointer]);
if (this.userDoc && !this.mainContainer) {
- const doc = CurrentUserUtils.setupMobileDoc(this.userDoc);
- this.userDoc.activeMobile = doc;
+ this.userDoc.activeMobile = this.mainDoc;
+ }
+ }
+
+ @action
+ switchCurrentView = (doc: (userDoc: Doc) => Doc, renderView?: () => JSX.Element, onSwitch?: () => void) => {
+ if (!this.userDoc) return;
+
+ this.userDoc.activeMobile = doc(this.userDoc);
+ onSwitch && onSwitch();
+
+ this.renderView = renderView;
+ }
+
+ onSwitchInking = () => {
+ InkingControl.Instance.switchTool(InkTool.Pen);
+ MobileInterface.Instance.drawingInk = true;
+
+ DocServer.Mobile.dispatchOverlayTrigger({
+ enableOverlay: true,
+ width: window.innerWidth,
+ height: window.innerHeight
+ });
+ }
+
+ onSwitchUpload = async () => {
+ let width = 300;
+ let height = 300;
+
+ // get width and height of the collection doc
+ if (this.mainContainer) {
+ const data = Cast(this.mainContainer.data, listSpec(Doc));
+ if (data) {
+ const collectionDoc = await data[1]; // this should be the collection doc since the positions should be locked
+ const docView = DocumentManager.Instance.getDocumentView(collectionDoc);
+ if (docView) {
+ width = docView.nativeWidth ? docView.nativeWidth : 300;
+ height = docView.nativeHeight ? docView.nativeHeight : 300;
+ }
+ }
}
+ DocServer.Mobile.dispatchOverlayTrigger({
+ enableOverlay: true,
+ width: width,
+ height: height,
+ text: "Documents uploaded from mobile will show here",
+ });
}
- @computed
- get mainContent() {
+ renderDefaultContent = () => {
if (this.mainContainer) {
return <DocumentView
Document={this.mainContainer}
DataDoc={undefined}
LibraryPath={emptyPath}
- addDocument={undefined}
+ addDocument={returnFalse}
addDocTab={returnFalse}
pinToPres={emptyFunction}
removeDocument={undefined}
@@ -58,11 +145,207 @@ export default class MobileInterface extends React.Component {
return "hello";
}
+ onBack = (e: React.MouseEvent) => {
+ this.switchCurrentView((userDoc: Doc) => this.mainDoc);
+ InkingControl.Instance.switchTool(InkTool.None); // TODO: switch to previous tool
+
+ DocServer.Mobile.dispatchOverlayTrigger({
+ enableOverlay: false,
+ width: window.innerWidth,
+ height: window.innerHeight
+ });
+
+ // this.inkDoc = undefined;
+ this.drawingInk = false;
+ }
+
+ shiftLeft = (e: React.MouseEvent) => {
+ DocServer.Mobile.dispatchOverlayPositionUpdate({
+ dx: -10
+ });
+ e.preventDefault();
+ e.stopPropagation();
+ }
+
+ shiftRight = (e: React.MouseEvent) => {
+ DocServer.Mobile.dispatchOverlayPositionUpdate({
+ dx: 10
+ });
+ e.preventDefault();
+ e.stopPropagation();
+ }
+
+ renderInkingContent = () => {
+ console.log("rendering inking content");
+ // TODO: support panning and zooming
+ // TODO: handle moving of ink strokes
+ if (this.mainContainer) {
+ return (
+ <div className="mobileInterface">
+ <div className="mobileInterface-inkInterfaceButtons">
+ <div className="navButtons">
+ <button className="mobileInterface-button cancel" onClick={this.onBack} title="Cancel drawing">BACK</button>
+ </div>
+ <div className="inkSettingButtons">
+ <button className="mobileInterface-button cancel" onClick={this.onBack} title="Cancel drawing"><FontAwesomeIcon icon="long-arrow-alt-left" /></button>
+ </div>
+ <div className="navButtons">
+ <button className="mobileInterface-button" onClick={this.shiftLeft} title="Shift left">left</button>
+ <button className="mobileInterface-button" onClick={this.shiftRight} title="Shift right">right</button>
+ </div>
+ </div>
+ <CollectionView
+ Document={this.mainContainer}
+ DataDoc={undefined}
+ LibraryPath={emptyPath}
+ fieldKey={""}
+ dropAction={"alias"}
+ bringToFront={emptyFunction }
+ addDocTab={returnFalse}
+ pinToPres={emptyFunction}
+ PanelHeight={() => window.innerHeight}
+ PanelWidth={() => window.innerWidth}
+ focus={emptyFunction}
+ isSelected={returnFalse}
+ select={emptyFunction}
+ active={returnFalse}
+ ContentScaling={returnOne}
+ whenActiveChanged={returnFalse}
+ ScreenToLocalTransform={Transform.Identity}
+ renderDepth={0}
+ ContainingCollectionView={undefined}
+ ContainingCollectionDoc={undefined}>
+ </CollectionView>
+ </div>
+ );
+ }
+ }
+
+ upload = async (e: React.MouseEvent) => {
+ if (this.mainContainer) {
+ const data = Cast(this.mainContainer.data, listSpec(Doc));
+ if (data) {
+ const collectionDoc = await data[1]; // this should be the collection doc since the positions should be locked
+ const children = DocListCast(collectionDoc.data);
+ const uploadDoc = children.length === 1 ? children[0] : Docs.Create.StackingDocument(children, {
+ title: "Mobile Upload Collection", backgroundColor: "white", lockedPosition: true, _width: 300, _height: 300
+ });
+ if (uploadDoc) {
+ DocServer.Mobile.dispatchMobileDocumentUpload({
+ docId: uploadDoc[Id],
+ });
+ }
+ }
+ }
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+ addWebToCollection = async () => {
+ let url = "https://en.wikipedia.org/wiki/Hedgehog";
+ if (this.mainContainer) {
+ const data = Cast(this.mainContainer.data, listSpec(Doc));
+ if (data) {
+ const webDoc = await data[0];
+ const urlField: FieldResult<WebField> = Cast(webDoc.data, WebField);
+ url = urlField ? urlField.url.toString() : "https://en.wikipedia.org/wiki/Hedgehog";
+
+ }
+ }
+ Docs.Create.WebDocument(url, { _width: 300, _height: 300, title: "Mobile Upload Web Doc" });
+ }
+
+ clearUpload = async () => {
+ if (this.mainContainer) {
+ const data = Cast(this.mainContainer.data, listSpec(Doc));
+ if (data) {
+ const collectionDoc = await data[1];
+ const children = DocListCast(collectionDoc.data);
+ children.forEach(doc => {
+ });
+ // collectionDoc[data] = new List<Doc>();
+ }
+ }
+ }
+
+ renderUploadContent() {
+ if (this.mainContainer) {
+ return (
+ <div className="mobileInterface" onDragOver={this.onDragOver}>
+ <div className="mobileInterface-inkInterfaceButtons">
+ <button className="mobileInterface-button cancel" onClick={this.onBack} title="Back">BACK</button>
+ {/* <button className="mobileInterface-button" onClick={this.clearUpload} title="Clear Upload">CLEAR</button> */}
+ {/* <button className="mobileInterface-button" onClick={this.addWeb} title="Add Web Doc to Upload Collection"></button> */}
+ <button className="mobileInterface-button" onClick={this.upload} title="Upload">UPLOAD</button>
+ </div>
+ <DocumentView
+ Document={this.mainContainer}
+ DataDoc={undefined}
+ LibraryPath={emptyPath}
+ addDocument={returnFalse}
+ addDocTab={returnFalse}
+ pinToPres={emptyFunction}
+ removeDocument={undefined}
+ onClick={undefined}
+ ScreenToLocalTransform={Transform.Identity}
+ ContentScaling={returnOne}
+ PanelWidth={() => window.screen.width}
+ PanelHeight={() => window.screen.height}
+ renderDepth={0}
+ focus={emptyFunction}
+ backgroundColor={returnEmptyString}
+ parentActive={returnTrue}
+ whenActiveChanged={emptyFunction}
+ bringToFront={emptyFunction}
+ ContainingCollectionView={undefined}
+ ContainingCollectionDoc={undefined}
+ zoomToScale={emptyFunction}
+ getScale={returnOne}>
+ </DocumentView>
+ </div>
+ );
+ }
+ }
+
+ onDragOver = (e: React.DragEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ }
+
render() {
+ // const content = this.currentView === "main" ? this.mainContent :
+ // this.currentView === "ink" ? this.inkContent :
+ // this.currentView === "upload" ? this.uploadContent : <></>;
return (
- <div className="mobile-container">
- {this.mainContent}
+ <div className="mobileInterface-container" onDragOver={this.onDragOver}>
+ {/* <DocumentDecorations />
+ <GestureOverlay>
+ {this.renderView ? this.renderView() : this.renderDefaultContent()}
+ </GestureOverlay> */}
+
+ {/* <DictationOverlay />
+ <SharingManager />
+ <GoogleAuthenticationManager /> */}
+ <DocumentDecorations />
+ <GestureOverlay>
+ {this.renderView ? this.renderView() : this.renderDefaultContent()}
+ </GestureOverlay>
+ <PreviewCursor />
+ {/* <ContextMenu /> */}
+ <RadialMenu />
+ <RichTextMenu />
+ {/* <PDFMenu />
+ <MarqueeOptionsMenu />
+ <OverlayView /> */}
</div>
);
}
-} \ No newline at end of file
+}
+
+Scripting.addGlobal(function switchMobileView(doc: (userDoc: Doc) => Doc, renderView?: () => JSX.Element, onSwitch?: () => void) { return MobileInterface.Instance.switchCurrentView(doc, renderView, onSwitch); });
+Scripting.addGlobal(function onSwitchMobileInking() { return MobileInterface.Instance.onSwitchInking(); });
+Scripting.addGlobal(function renderMobileInking() { return MobileInterface.Instance.renderInkingContent(); });
+Scripting.addGlobal(function onSwitchMobileUpload() { return MobileInterface.Instance.onSwitchUpload(); });
+Scripting.addGlobal(function renderMobileUpload() { return MobileInterface.Instance.renderUploadContent(); });
+Scripting.addGlobal(function addWebToMobileUpload() { return MobileInterface.Instance.addWebToCollection(); });
+
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index 5f78636a9..85926e393 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -1,22 +1,23 @@
-import { observable, ObservableMap, runInAction } from "mobx";
+import { action, computed, observable, ObservableMap, runInAction } from "mobx";
+import { computedFn } from "mobx-utils";
import { alias, map, serializable } from "serializr";
import { DocServer } from "../client/DocServer";
import { DocumentType } from "../client/documents/DocumentTypes";
import { Scripting, scriptingGlobal } from "../client/util/Scripting";
import { afterDocDeserialize, autoObject, Deserializable, SerializationHelper } from "../client/util/SerializationHelper";
-import { Copy, HandleUpdate, Id, OnUpdate, Parent, Self, SelfProxy, ToScriptString, ToString, Update } from "./FieldSymbols";
+import { UndoManager } from "../client/util/UndoManager";
+import { intersectRect } from "../Utils";
+import { HandleUpdate, Id, OnUpdate, Parent, Self, SelfProxy, ToScriptString, ToString, Update, Copy } from "./FieldSymbols";
import { List } from "./List";
import { ObjectField } from "./ObjectField";
import { PrefetchProxy, ProxyField } from "./Proxy";
import { FieldId, RefField } from "./RefField";
+import { RichTextField } from "./RichTextField";
import { listSpec } from "./Schema";
-import { ComputedField, ScriptField } from "./ScriptField";
-import { BoolCast, Cast, FieldValue, NumCast, StrCast, ToConstructor } from "./Types";
+import { ComputedField } from "./ScriptField";
+import { Cast, FieldValue, NumCast, StrCast, ToConstructor, ScriptCast } from "./Types";
import { deleteProperty, getField, getter, makeEditable, makeReadOnly, setter, updateFunction } from "./util";
-import { intersectRect } from "../Utils";
-import { UndoManager } from "../client/util/UndoManager";
-import { computedFn } from "mobx-utils";
-import { RichTextField } from "./RichTextField";
+import { Docs } from "../client/documents/Documents";
export namespace Field {
export function toKeyValueString(doc: Doc, key: string): string {
@@ -47,7 +48,7 @@ export namespace Field {
} else if (field instanceof RefField) {
return field[ToString]();
}
- return "(null)";
+ return "";
}
export function IsField(field: any): field is Field;
export function IsField(field: any, includeUndefined: true): field is Field | undefined;
@@ -89,6 +90,7 @@ export function DocListCast(field: FieldResult): Doc[] {
export const WidthSym = Symbol("Width");
export const HeightSym = Symbol("Height");
export const DataSym = Symbol("Data");
+export const LayoutSym = Symbol("Layout");
export const UpdatingFromServer = Symbol("UpdatingFromServer");
const CachedUpdates = Symbol("Cached updates");
@@ -110,8 +112,16 @@ export class Doc extends RefField {
get: getter,
// getPrototypeOf: (target) => Cast(target[SelfProxy].proto, Doc) || null, // TODO this might be able to replace the proto logic in getter
has: (target, key) => key in target.__fields,
- ownKeys: target => Object.keys(target.__fields),
+ ownKeys: target => {
+ const obj = {} as any;
+ Object.assign(obj, target.___fields);
+ runInAction(() => obj.__LAYOUT__ = target.__LAYOUT__);
+ return Object.keys(obj);
+ },
getOwnPropertyDescriptor: (target, prop) => {
+ if (prop.toString() === "__LAYOUT__") {
+ return Reflect.getOwnPropertyDescriptor(target, prop);
+ }
if (prop in target.__fields) {
return {
configurable: true,//TODO Should configurable be true?
@@ -135,10 +145,7 @@ export class Doc extends RefField {
[key: string]: FieldResult;
@serializable(alias("fields", map(autoObject(), { afterDeserialize: afterDocDeserialize })))
- private get __fields() {
- return this.___fields;
- }
-
+ private get __fields() { return this.___fields; }
private set __fields(value) {
this.___fields = value;
for (const key in value) {
@@ -156,25 +163,37 @@ export class Doc extends RefField {
private [UpdatingFromServer]: boolean = false;
private [Update] = (diff: any) => {
- if (this[UpdatingFromServer]) {
- return;
- }
- DocServer.UpdateField(this[Id], diff);
+ !this[UpdatingFromServer] && DocServer.UpdateField(this[Id], diff);
}
private [Self] = this;
private [SelfProxy]: any;
public [WidthSym] = () => NumCast(this[SelfProxy]._width);
public [HeightSym] = () => NumCast(this[SelfProxy]._height);
- public get [DataSym]() { return Cast(this[SelfProxy].resolvedDataDoc, Doc, null) || this[SelfProxy]; }
-
- [ToScriptString]() {
- return "invalid";
- }
- [ToString]() {
- return "Doc";
+ public get [LayoutSym]() { return this[SelfProxy].__LAYOUT__; }
+ public get [DataSym]() {
+ const self = this[SelfProxy];
+ return self.resolvedDataDoc && !self.isTemplateForField ? self :
+ Doc.GetProto(Cast(Doc.Layout(self).resolvedDataDoc, Doc, null) || self);
+ }
+ @computed get __LAYOUT__() {
+ const templateLayoutDoc = Cast(Doc.LayoutField(this[SelfProxy]), Doc, null);
+ if (templateLayoutDoc) {
+ let renderFieldKey: any;
+ const layoutField = templateLayoutDoc[StrCast(templateLayoutDoc.layoutKey, "layout")];
+ if (typeof layoutField === "string") {
+ renderFieldKey = layoutField.split("'")[1];
+ } else {
+ return Cast(layoutField, Doc, null);
+ }
+ return Cast(this[SelfProxy][renderFieldKey + "-layout[" + templateLayoutDoc[Id] + "]"], Doc, null) || templateLayoutDoc;
+ }
+ return undefined;
}
+ [ToScriptString]() { return `DOC-"${this[Self][Id]}"-`; }
+ [ToString]() { return `Doc(${this.title})`; }
+
private [CachedUpdates]: { [key: string]: () => void | Promise<any> } = {};
public static CurrentUserEmail: string = "";
public async [HandleUpdate](diff: any) {
@@ -260,8 +279,7 @@ export namespace Doc {
export function Get(doc: Doc, key: string, ignoreProto: boolean = false): FieldResult {
try {
- const self = doc[Self];
- return getField(self, key, ignoreProto);
+ return getField(doc[Self], key, ignoreProto);
} catch {
return doc;
}
@@ -330,9 +348,15 @@ export namespace Doc {
return r || r2 || r3 || r4;
}
- // gets the document's prototype or returns the document if it is a prototype
- export function GetProto(doc: Doc) {
- return doc && (Doc.GetT(doc, "isPrototype", "boolean", true) ? doc : (doc.proto || doc));
+ // Gets the data document for the document. Note: this is mis-named -- it does not specifically
+ // return the doc's proto, but rather recursively searches through the proto inheritance chain
+ // and returns the document who's proto is undefined or whose proto is marked as a base prototype ('isPrototype').
+ export function GetProto(doc: Doc): Doc {
+ if (doc instanceof Promise) {
+ console.log("GetProto: error: got Promise insead of Doc");
+ }
+ const proto = doc && (Doc.GetT(doc, "isPrototype", "boolean", true) ? doc : (doc.proto || doc));
+ return proto === doc ? proto : Doc.GetProto(proto);
}
export function GetDataDoc(doc: Doc): Doc {
const proto = Doc.GetProto(doc);
@@ -356,7 +380,8 @@ export namespace Doc {
index = allowProtos && index !== -1 ? index : list.reduce((p, v, i) => (v instanceof Doc && Doc.AreProtosEqual(v, toFind)) ? i : p, -1);
return index; // list.findIndex(doc => doc === toFind || Doc.AreProtosEqual(doc, toFind));
}
- export function RemoveDocFromList(listDoc: Doc, key: string, doc: Doc) {
+ export function RemoveDocFromList(listDoc: Doc, fieldKey: string | undefined, doc: Doc) {
+ const key = fieldKey ? fieldKey : Doc.LayoutFieldKey(listDoc);
if (listDoc[key] === undefined) {
Doc.GetProto(listDoc)[key] = new List<Doc>();
}
@@ -370,7 +395,8 @@ export namespace Doc {
}
return false;
}
- export function AddDocToList(listDoc: Doc, key: string, doc: Doc, relativeTo?: Doc, before?: boolean, first?: boolean, allowDuplicates?: boolean, reversed?: boolean) {
+ export function AddDocToList(listDoc: Doc, fieldKey: string | undefined, doc: Doc, relativeTo?: Doc, before?: boolean, first?: boolean, allowDuplicates?: boolean, reversed?: boolean) {
+ const key = fieldKey ? fieldKey : Doc.LayoutFieldKey(listDoc);
if (listDoc[key] === undefined) {
Doc.GetProto(listDoc)[key] = new List<Doc>();
}
@@ -416,37 +442,39 @@ export namespace Doc {
return bounds;
}
- export function MakeTitled(title: string) {
- const doc = new Doc();
- doc.title = title;
- return doc;
- }
- export function MakeAlias(doc: Doc) {
- const alias = !GetT(doc, "isPrototype", "boolean", true) ? Doc.MakeCopy(doc) : Doc.MakeDelegate(doc);
- const layout = Doc.Layout(alias);
- if (layout instanceof Doc && layout !== alias) {
+ export function MakeAlias(doc: Doc, id?: string) {
+ const alias = !GetT(doc, "isPrototype", "boolean", true) ? Doc.MakeCopy(doc, undefined, id) : Doc.MakeDelegate(doc, id);
+ const layout = Doc.LayoutField(alias);
+ if (layout instanceof Doc && layout !== alias && layout === Doc.Layout(alias)) {
Doc.SetLayout(alias, Doc.MakeAlias(layout));
}
+ alias.aliasOf = doc;
alias.title = ComputedField.MakeFunction(`renameAlias(this, ${Doc.GetProto(doc).aliasNumber = NumCast(Doc.GetProto(doc).aliasNumber) + 1})`);
return alias;
}
//
- // Determines whether the combination of the layoutDoc and dataDoc represents
- // a template relationship : there is a dataDoc and it doesn't match the layoutDoc an
- // the lyouatDoc's layout is layout string (not a document)
+ // Determines whether the layout needs to be expanded (as a template).
+ // template expansion is rquired when the layout is a template doc/field and there's a datadoc which isn't equal to the layout template
//
export function WillExpandTemplateLayout(layoutDoc: Doc, dataDoc?: Doc) {
- return layoutDoc.isTemplateForField && dataDoc && layoutDoc !== dataDoc && !(Doc.LayoutField(layoutDoc) instanceof Doc);
+ return (layoutDoc.isTemplateForField || layoutDoc.isTemplateDoc) && dataDoc && layoutDoc !== dataDoc;
}
//
// Returns an expanded template layout for a target data document if there is a template relationship
// between the two. If so, the layoutDoc is expanded into a new document that inherits the properties
// of the original layout while allowing for individual layout properties to be overridden in the expanded layout.
- //
- export function expandTemplateLayout(templateLayoutDoc: Doc, dataDoc?: Doc) {
- if (!WillExpandTemplateLayout(templateLayoutDoc, dataDoc) || !dataDoc) return templateLayoutDoc;
+ // templateArgs should be equivalent to the layout key that generates the template since that's where the template parameters are stored in ()'s at the end of the key.
+ // NOTE: the template will have references to "@params" -- the template arguments will be assigned to the '@params' field
+ // so that when the @params key is accessed, it will be rewritten as the key that is stored in the 'params' field and
+ // the derefence will then occur on the rootDocument (the original document).
+ // in the future, field references could be written as @<someparam> and then arguments would be passed in the layout key as:
+ // layout_mytemplate(somparam=somearg).
+ // then any references to @someparam would be rewritten as accesses to 'somearg' on the rootDocument
+ export function expandTemplateLayout(templateLayoutDoc: Doc, targetDoc?: Doc, templateArgs?: string) {
+ const args = templateArgs?.match(/\(([a-zA-Z0-9._\-]*)\)/)?.[1].replace("()", "") || StrCast(templateLayoutDoc.PARAMS);
+ if (!args && !WillExpandTemplateLayout(templateLayoutDoc, targetDoc) || !targetDoc) return templateLayoutDoc;
const templateField = StrCast(templateLayoutDoc.isTemplateForField); // the field that the template renders
// First it checks if an expanded layout already exists -- if so it will be stored on the dataDoc
@@ -454,44 +482,47 @@ 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 expandedTemplateLayout = dataDoc?.[expandedLayoutFieldKey];
- if (expandedTemplateLayout === undefined) {
- setTimeout(() => {
- if (!dataDoc[expandedLayoutFieldKey]) {
- const newLayoutDoc = Doc.MakeDelegate(templateLayoutDoc, undefined, "[" + templateLayoutDoc.title + "]");
- dataDoc[expandedLayoutFieldKey] = newLayoutDoc;
- newLayoutDoc.resolvedDataDoc = dataDoc;
- if (dataDoc[templateField] === undefined && templateLayoutDoc[templateField] instanceof List && Cast(templateLayoutDoc[templateField], listSpec(Doc), []).length) {
- dataDoc[templateField] = ComputedField.MakeFunction(`ObjectField.MakeCopy(templateLayoutDoc["${templateField}"] as List)`, { templateLayoutDoc: Doc.name }, { templateLayoutDoc: templateLayoutDoc });
+ const params = args.split("=").length > 1 ? args.split("=")[0] : "PARAMS";
+ const layoutFielddKey = Doc.LayoutFieldKey(templateLayoutDoc);
+ const expandedLayoutFieldKey = (templateField || layoutFielddKey) + "-layout[" + templateLayoutDoc[Id] + (args ? `(${args})` : "") + "]";
+ let expandedTemplateLayout = targetDoc?.[expandedLayoutFieldKey];
+ if (templateLayoutDoc.resolvedDataDoc instanceof Promise) {
+ expandedTemplateLayout = undefined;
+ }
+ else if (expandedTemplateLayout === undefined) {
+ if (templateLayoutDoc.resolvedDataDoc === Doc.GetProto(targetDoc) && templateLayoutDoc.PARAMS === StrCast(targetDoc.PARAMS)) {
+ expandedTemplateLayout = templateLayoutDoc; // reuse an existing template layout if its for the same document with the same params
+ } else {
+ templateLayoutDoc.resolvedDataDoc && (templateLayoutDoc = Cast(templateLayoutDoc.proto, Doc, null) || templateLayoutDoc); // if the template has already been applied (ie, a nested template), then use the template's prototype
+ setTimeout(action(() => {
+ if (!targetDoc[expandedLayoutFieldKey]) {
+ const newLayoutDoc = Doc.MakeDelegate(templateLayoutDoc, undefined, "[" + templateLayoutDoc.title + "]");
+ // the template's arguments are stored in params which is derefenced to find
+ // the actual field key where the parameterized template data is stored.
+ newLayoutDoc[params] = args !== "..." ? args : ""; // ... signifies the layout has sub template(s) -- so we have to expand the layout for them so that they can get the correct 'rootDocument' field, but we don't need to reassign their params. it would be better if the 'rootDocument' field could be passed dynamically to avoid have to create instances
+ newLayoutDoc.rootDocument = targetDoc;
+ targetDoc[expandedLayoutFieldKey] = newLayoutDoc;
+ const dataDoc = Doc.GetProto(targetDoc);
+ newLayoutDoc.resolvedDataDoc = dataDoc;
+ if (dataDoc[templateField] === undefined && templateLayoutDoc[templateField] instanceof List) {
+ dataDoc[templateField] = ComputedField.MakeFunction(`ObjectField.MakeCopy(templateLayoutDoc["${templateField}"] as List)`, { templateLayoutDoc: Doc.name }, { templateLayoutDoc });
+ }
}
- }
- }, 0);
+ }), 0);
+ }
}
- return expandedTemplateLayout instanceof Doc ? expandedTemplateLayout : undefined; // layout is undefined if the expandedTemplate is pending.
+ return expandedTemplateLayout instanceof Doc ? expandedTemplateLayout : undefined; // layout is undefined if the expandedTemplateLayout is pending.
}
// if the childDoc is a template for a field, then this will return the expanded layout with its data doc.
// otherwise, it just returns the childDoc
export function GetLayoutDataDocPair(containerDoc: Doc, containerDataDoc: Opt<Doc>, childDoc: Doc) {
- const resolvedDataDoc = containerDataDoc === containerDoc || !containerDataDoc ? undefined : Doc.GetDataDoc(containerDataDoc);
- return { layout: Doc.expandTemplateLayout(childDoc, resolvedDataDoc), data: resolvedDataDoc };
- }
- export function CreateDocumentExtensionForField(doc: Doc, fieldKey: string) {
- let proto: Doc | undefined = doc;
- while (proto && !Doc.IsPrototype(proto) && proto.proto) {
- proto = proto.proto;
- }
- let docExtensionForField = ((proto || doc)[fieldKey + "_ext"] as Doc);
- if (!docExtensionForField) {
- docExtensionForField = new Doc(doc[Id] + fieldKey, true);
- docExtensionForField.title = fieldKey + ".ext"; // courtesy field--- shouldn't be needed except maybe for debugging
- docExtensionForField.extendsDoc = doc; // this is used by search to map field matches on the extension doc back to the document it extends.
- docExtensionForField.extendsField = fieldKey; // this can be used by search to map matches on the extension doc back to the field that was extended.
- docExtensionForField.type = DocumentType.EXTENSION;
- (proto || doc)[fieldKey + "_ext"] = new PrefetchProxy(docExtensionForField);
+ if (!childDoc || childDoc instanceof Promise || !Doc.GetProto(childDoc)) {
+ console.log("No, no, no!");
+ return { layout: childDoc, data: childDoc };
}
- return docExtensionForField;
+ const resolvedDataDoc = (Doc.AreProtosEqual(containerDataDoc, containerDoc) || (!childDoc.isTemplateDoc && !childDoc.isTemplateForField && !childDoc.PARAMS) ? undefined : containerDataDoc);
+ return { layout: Doc.expandTemplateLayout(childDoc, resolvedDataDoc, "(" + StrCast(containerDoc.PARAMS) + ")"), data: resolvedDataDoc };
}
export function Overwrite(doc: Doc, overwrite: Doc, copyProto: boolean = false): Doc {
@@ -534,7 +565,8 @@ export namespace Doc {
} else if (cfield instanceof ComputedField) {
copy[key] = ComputedField.MakeFunction(cfield.script.originalScript);
} else if (field instanceof ObjectField) {
- copy[key] = ObjectField.MakeCopy(field);
+ copy[key] = key.includes("layout[") && doc[key] instanceof Doc ? Doc.MakeCopy(doc[key] as Doc, false) :
+ doc[key] instanceof Doc ? doc[key] : ObjectField.MakeCopy(field);
} else if (field instanceof Promise) {
debugger; //This shouldn't happend...
} else {
@@ -561,32 +593,32 @@ export namespace Doc {
let _applyCount: number = 0;
export function ApplyTemplate(templateDoc: Doc) {
if (templateDoc) {
- const applied = ApplyTemplateTo(templateDoc, Doc.MakeDelegate(new Doc()), "layoutCustom", templateDoc.title + "(..." + _applyCount++ + ")");
- applied && (Doc.GetProto(applied).layout = applied.layout);
+ const target = Doc.MakeDelegate(new Doc());
+ const targetKey = StrCast(templateDoc.layoutKey, "layout");
+ const applied = ApplyTemplateTo(templateDoc, target, targetKey, templateDoc.title + "(..." + _applyCount++ + ")");
+ target.layoutKey = targetKey;
+ applied && (Doc.GetProto(applied).type = templateDoc.type);
return applied;
}
return undefined;
}
- export function ApplyTemplateTo(templateDoc: Doc, target: Doc, targetKey: string, titleTarget: string | undefined = undefined) {
+ export function ApplyTemplateTo(templateDoc: Doc, target: Doc, targetKey: string, titleTarget: string | undefined) {
if (!templateDoc) {
target.layout = undefined;
target._nativeWidth = undefined;
target._nativeHeight = undefined;
- target.onClick = undefined;
target.type = undefined;
return;
}
- if ((target[targetKey] as Doc)?.proto !== templateDoc) {
- const layoutCustomLayout = Doc.MakeDelegate(templateDoc);
-
- titleTarget && (Doc.GetProto(target).title = titleTarget);
- Doc.GetProto(target).type = DocumentType.TEMPLATE;
- target.onClick = templateDoc.onClick instanceof ObjectField && templateDoc.onClick[Copy]();
-
- Doc.GetProto(target)[targetKey] = new PrefetchProxy(layoutCustomLayout);
+ if (!Doc.AreProtosEqual(target[targetKey] as Doc, templateDoc)) {
+ if (target.resolvedDataDoc) {
+ target[targetKey] = new PrefetchProxy(templateDoc);
+ } else {
+ titleTarget && (Doc.GetProto(target).title = titleTarget);
+ Doc.GetProto(target)[targetKey] = new PrefetchProxy(templateDoc);
+ }
}
- target.layoutKey = targetKey;
return target;
}
@@ -597,37 +629,38 @@ export namespace Doc {
export function MakeMetadataFieldTemplate(templateField: Doc, templateDoc: Opt<Doc>): boolean {
// find the metadata field key that this template field doc will display (indicated by its title)
- const metadataFieldKey = StrCast(templateField.title).replace(/^-/, "");
+ const metadataFieldKey = StrCast(templateField.isTemplateForField) || StrCast(templateField.title).replace(/^-/, "");
// update the original template to mark it as a template
templateField.isTemplateForField = metadataFieldKey;
templateField.title = metadataFieldKey;
+ const templateFieldValue = templateField[metadataFieldKey] || templateField[Doc.LayoutFieldKey(templateField)];
+ const templateCaptionValue = templateField.caption;
// move any data that the template field had been rendering over to the template doc so that things will still be rendered
// when the template field is adjusted to point to the new metadatafield key.
// note 1: if the template field contained a list of documents, each of those documents will be converted to templates as well.
// note 2: this will not overwrite any field that already exists on the template doc at the field key
- if (!templateDoc?.[metadataFieldKey] && templateField.data instanceof ObjectField) {
- Cast(templateField.data, listSpec(Doc), [])?.map(d => d instanceof Doc && MakeMetadataFieldTemplate(d, templateDoc));
- (Doc.GetProto(templateField)[metadataFieldKey] = ObjectField.MakeCopy(templateField.data));
+ if (!templateDoc?.[metadataFieldKey] && templateFieldValue instanceof ObjectField) {
+ Cast(templateFieldValue, listSpec(Doc), [])?.map(d => d instanceof Doc && MakeMetadataFieldTemplate(d, templateDoc));
+ (Doc.GetProto(templateField)[metadataFieldKey] = ObjectField.MakeCopy(templateFieldValue));
}
- if (templateField.data instanceof RichTextField && templateField.data.Text) {
- templateField._textTemplate = ComputedField.MakeFunction(`copyField(this.${metadataFieldKey})`, { this: Doc.name });
+ if (templateCaptionValue instanceof RichTextField && (templateCaptionValue.Text || templateCaptionValue.Data.toString().includes("dashField"))) {
+ templateField["caption-textTemplate"] = ComputedField.MakeFunction(`copyField(this.caption)`, { this: Doc.name });
+ }
+ if (templateFieldValue instanceof RichTextField && (templateFieldValue.Text || templateFieldValue.Data.toString().includes("dashField"))) {
+ templateField[metadataFieldKey + "-textTemplate"] = ComputedField.MakeFunction(`copyField(this.${metadataFieldKey})`, { this: Doc.name });
}
// get the layout string that the template uses to specify its layout
const templateFieldLayoutString = StrCast(Doc.LayoutField(Doc.Layout(templateField)));
- // change itto render the target metadata field instead of what it was rendering before and assign it to the template field layout document.
+ // change it to render the target metadata field instead of what it was rendering before and assign it to the template field layout document.
Doc.Layout(templateField).layout = templateFieldLayoutString.replace(/fieldKey={'[^']*'}/, `fieldKey={'${metadataFieldKey}'}`);
// assign the template field doc a delegate of any extension document that was previously used to render the template field (since extension doc's carry rendering informatino)
Doc.Layout(templateField)[metadataFieldKey + "_ext"] = Doc.MakeDelegate(templateField[templateFieldLayoutString?.split("'")[1] + "_ext"] as Doc);
- if (templateField.backgroundColor !== templateDoc?.defaultBackgroundColor) {
- templateField.defaultBackgroundColor = templateField.backgroundColor;
- }
-
return true;
}
@@ -665,11 +698,14 @@ export namespace Doc {
}
// the document containing the view layout information - will be the Document itself unless the Document has
- // a layout field. In that case, all layout information comes from there unless overriden by Document
- export function Layout(doc: Doc) { return Doc.LayoutField(doc) instanceof Doc ? Doc.LayoutField(doc) as Doc : doc; }
+ // a layout field or 'layout' is given.
+ export function Layout(doc: Doc, layout?: Doc): Doc {
+ const overrideLayout = layout && Cast(doc[`${StrCast(layout.isTemplateForField, "data")}-layout[` + layout[Id] + "]"], Doc, null);
+ return overrideLayout || doc[LayoutSym] || doc;
+ }
export function SetLayout(doc: Doc, layout: Doc | string) { doc[StrCast(doc.layoutKey, "layout")] = layout; }
export function LayoutField(doc: Doc) { return doc[StrCast(doc.layoutKey, "layout")]; }
- export function LayoutFieldKey(doc: Doc) { return StrCast(Doc.Layout(doc).layout).split("'")[1]; }
+ export function LayoutFieldKey(doc: Doc): string { return StrCast(Doc.Layout(doc).layout).split("'")[1]; }
const manager = new DocData();
export function SearchQuery(): string { return manager._searchQuery; }
export function SetSearchQuery(query: string) { runInAction(() => manager._searchQuery = query); }
@@ -677,26 +713,26 @@ export namespace Doc {
export function SetUserDoc(doc: Doc) { manager._user_doc = doc; }
export function IsBrushed(doc: Doc) {
return computedFn(function IsBrushed(doc: Doc) {
- return brushManager.BrushedDoc.has(doc) || brushManager.BrushedDoc.has(Doc.GetDataDoc(doc));
+ return brushManager.BrushedDoc.has(doc) || brushManager.BrushedDoc.has(Doc.GetProto(doc));
})(doc);
}
// don't bother memoizing (caching) the result if called from a non-reactive context. (plus this avoids a warning message)
export function IsBrushedDegreeUnmemoized(doc: Doc) {
- return brushManager.BrushedDoc.has(doc) ? 2 : brushManager.BrushedDoc.has(Doc.GetDataDoc(doc)) ? 1 : 0;
+ return brushManager.BrushedDoc.has(doc) ? 2 : brushManager.BrushedDoc.has(Doc.GetProto(doc)) ? 1 : 0;
}
export function IsBrushedDegree(doc: Doc) {
return computedFn(function IsBrushDegree(doc: Doc) {
- return brushManager.BrushedDoc.has(doc) ? 2 : brushManager.BrushedDoc.has(Doc.GetDataDoc(doc)) ? 1 : 0;
+ return Doc.IsBrushedDegreeUnmemoized(doc);
})(doc);
}
export function BrushDoc(doc: Doc) {
brushManager.BrushedDoc.set(doc, true);
- brushManager.BrushedDoc.set(Doc.GetDataDoc(doc), true);
+ brushManager.BrushedDoc.set(Doc.GetProto(doc), true);
return doc;
}
export function UnBrushDoc(doc: Doc) {
brushManager.BrushedDoc.delete(doc);
- brushManager.BrushedDoc.delete(Doc.GetDataDoc(doc));
+ brushManager.BrushedDoc.delete(Doc.GetProto(doc));
return doc;
}
@@ -709,14 +745,14 @@ export namespace Doc {
document.removeEventListener("pointerdown", linkFollowUnhighlight);
}
- let dt = 0;
+ let _lastDate = 0;
export function linkFollowHighlight(destDoc: Doc, dataAndDisplayDocs = true) {
linkFollowUnhighlight();
Doc.HighlightDoc(destDoc, dataAndDisplayDocs);
document.removeEventListener("pointerdown", linkFollowUnhighlight);
document.addEventListener("pointerdown", linkFollowUnhighlight);
- const x = dt = Date.now();
- window.setTimeout(() => dt === x && linkFollowUnhighlight(), 5000);
+ const lastDate = _lastDate = Date.now();
+ window.setTimeout(() => _lastDate === lastDate && linkFollowUnhighlight(), 5000);
}
export class HighlightBrush {
@@ -724,18 +760,18 @@ export namespace Doc {
}
const highlightManager = new HighlightBrush();
export function IsHighlighted(doc: Doc) {
- return highlightManager.HighlightedDoc.get(doc) || highlightManager.HighlightedDoc.get(Doc.GetDataDoc(doc));
+ return highlightManager.HighlightedDoc.get(doc) || highlightManager.HighlightedDoc.get(Doc.GetProto(doc));
}
export function HighlightDoc(doc: Doc, dataAndDisplayDocs = true) {
runInAction(() => {
highlightManager.HighlightedDoc.set(doc, true);
- dataAndDisplayDocs && highlightManager.HighlightedDoc.set(Doc.GetDataDoc(doc), true);
+ dataAndDisplayDocs && highlightManager.HighlightedDoc.set(Doc.GetProto(doc), true);
});
}
export function UnHighlightDoc(doc: Doc) {
runInAction(() => {
highlightManager.HighlightedDoc.set(doc, false);
- highlightManager.HighlightedDoc.set(Doc.GetDataDoc(doc), false);
+ highlightManager.HighlightedDoc.set(Doc.GetProto(doc), false);
});
}
export function UnhighlightAll() {
@@ -763,7 +799,7 @@ export namespace Doc {
}
export function matchFieldValue(doc: Doc, key: string, value: any): boolean {
- const fieldVal = doc[key] ? doc[key] : doc[key + "_ext"];
+ const fieldVal = doc[key];
if (Cast(fieldVal, listSpec("string"), []).length) {
const vals = Cast(fieldVal, listSpec("string"), []);
return vals.some(v => v === value);
@@ -771,6 +807,99 @@ export namespace Doc {
const fieldStr = Field.toString(fieldVal as Field);
return fieldStr === value;
}
+
+ export function deiconifyView(doc: any) {
+ StrCast(doc.layoutKey).split("_")[1] === "icon" && setNativeView(doc);
+ }
+
+ export function setNativeView(doc: any) {
+ const prevLayout = StrCast(doc.layoutKey).split("_")[1];
+ const deiconify = prevLayout === "icon" && StrCast(doc.deiconifyLayout) ? "layout_" + StrCast(doc.deiconifyLayout) : "";
+ prevLayout === "icon" && (doc.deiconifyLayout = undefined);
+ doc.layoutKey = deiconify || "layout";
+ }
+ export function setDocFilterRange(target: Doc, key: string, range?: number[]) {
+ const docRangeFilters = Cast(target._docRangeFilters, listSpec("string"), []);
+ for (let i = 0; i < docRangeFilters.length; i += 3) {
+ if (docRangeFilters[i] === key) {
+ docRangeFilters.splice(i, 3);
+ break;
+ }
+ }
+ if (range !== undefined) {
+ docRangeFilters.push(key);
+ docRangeFilters.push(range[0].toString());
+ docRangeFilters.push(range[1].toString());
+ target._docRangeFilters = new List<string>(docRangeFilters);
+ }
+ }
+
+ export function aliasDocs(field: any) {
+ return new List<Doc>(field.map((d: any) => Doc.MakeAlias(d)));
+ }
+
+ // filters document in a container collection:
+ // all documents with the specified value for the specified key are included/excluded
+ // based on the modifiers :"check", "x", undefined
+ export function setDocFilter(container: Doc, key: string, value: any, modifiers?: "check" | "x" | undefined) {
+ const docFilters = Cast(container._docFilters, listSpec("string"), []);
+ for (let i = 0; i < docFilters.length; i += 3) {
+ if (docFilters[i] === key && docFilters[i + 1] === value) {
+ docFilters.splice(i, 3);
+ break;
+ }
+ }
+ if (typeof modifiers === "string") {
+ docFilters.push(key);
+ docFilters.push(value);
+ docFilters.push(modifiers);
+ container._docFilters = new List<string>(docFilters);
+ }
+ }
+ export function readDocRangeFilter(doc: Doc, key: string) {
+ const docRangeFilters = Cast(doc._docRangeFilters, listSpec("string"), []);
+ for (let i = 0; i < docRangeFilters.length; i += 3) {
+ if (docRangeFilters[i] === key) {
+ return [Number(docRangeFilters[i + 1]), Number(docRangeFilters[i + 2])];
+ }
+ }
+ }
+
+ export function freezeNativeDimensions(layoutDoc: Doc, width: number, height: number): void {
+ layoutDoc._autoHeight = false;
+ if (!layoutDoc._nativeWidth) {
+ layoutDoc._nativeWidth = NumCast(layoutDoc._width, width);
+ layoutDoc._nativeHeight = NumCast(layoutDoc._height, height);
+ }
+ }
+ export function assignDocToField(doc: Doc, field: string, id: string) {
+ DocServer.GetRefField(id).then(layout => layout instanceof Doc && (doc[field] = layout));
+ return id;
+ }
+
+ export async function addFieldEnumerations(doc: Opt<Doc>, enumeratedFieldKey: string, enumerations: { title: string, _backgroundColor?: string, color?: string }[]) {
+ let optionsCollection = await DocServer.GetRefField(enumeratedFieldKey);
+ if (!(optionsCollection instanceof Doc)) {
+ optionsCollection = Docs.Create.StackingDocument([], { title: `${enumeratedFieldKey} field set` }, enumeratedFieldKey);
+ Doc.AddDocToList((Doc.UserDoc().fieldTypes as Doc), "data", optionsCollection as Doc);
+ }
+ const options = optionsCollection as Doc;
+ const targetDoc = doc && Doc.GetProto(Cast(doc.rootDocument, Doc, null) || doc);
+ const docFind = `options.data.find(doc => doc.title === (this.rootDocument||this)["${enumeratedFieldKey}"])?`;
+ targetDoc && (targetDoc.backgroundColor = ComputedField.MakeFunction(docFind + `._backgroundColor || "white"`, undefined, { options }));
+ targetDoc && (targetDoc.color = ComputedField.MakeFunction(docFind + `.color || "black"`, undefined, { options }));
+ targetDoc && (targetDoc.borderRounding = ComputedField.MakeFunction(docFind + `.borderRounding`, undefined, { options }));
+ enumerations.map(enumeration => {
+ const found = DocListCast(options.data).find(d => d.title === enumeration.title);
+ if (found) {
+ found._backgroundColor = enumeration._backgroundColor || found._backgroundColor;
+ found._color = enumeration.color || found._color;
+ } else {
+ Doc.AddDocToList(options, "data", Docs.Create.TextDocument(enumeration.title, enumeration));
+ }
+ });
+ return optionsCollection;
+ }
}
Scripting.addGlobal(function renameAlias(doc: any, n: any) { return StrCast(Doc.GetProto(doc).title).replace(/\([0-9]*\)/, "") + `(${n})`; });
@@ -780,28 +909,24 @@ Scripting.addGlobal(function setChildDetailedLayout(target: any, source: any) {
Scripting.addGlobal(function getAlias(doc: any) { return Doc.MakeAlias(doc); });
Scripting.addGlobal(function getCopy(doc: any, copyProto: any) { return doc.isTemplateDoc ? Doc.ApplyTemplate(doc) : Doc.MakeCopy(doc, copyProto); });
Scripting.addGlobal(function copyField(field: any) { return ObjectField.MakeCopy(field); });
-Scripting.addGlobal(function aliasDocs(field: any) { return new List<Doc>(field.map((d: any) => Doc.MakeAlias(d))); });
+Scripting.addGlobal(function aliasDocs(field: any) { return Doc.aliasDocs(field); });
Scripting.addGlobal(function docList(field: any) { return DocListCast(field); });
Scripting.addGlobal(function sameDocs(doc1: any, doc2: any) { return Doc.AreProtosEqual(doc1, doc2); });
+Scripting.addGlobal(function deiconifyView(doc: any) { Doc.deiconifyView(doc); });
Scripting.addGlobal(function undo() { return UndoManager.Undo(); });
Scripting.addGlobal(function redo() { return UndoManager.Redo(); });
+Scripting.addGlobal(function DOC(id: string) { console.log("Can't parse a document id in a script"); return "invalid"; });
+Scripting.addGlobal(function assignDoc(doc: Doc, field: string, id: string) { return Doc.assignDocToField(doc, field, id); });
+Scripting.addGlobal(function curPresentationItem() {
+ const curPres = Doc.UserDoc().curPresentation as Doc;
+ return curPres && DocListCast(curPres[Doc.LayoutFieldKey(curPres)])[NumCast(curPres._itemIndex)];
+});
Scripting.addGlobal(function selectDoc(doc: any) { Doc.UserDoc().SelectedDocs = new List([doc]); });
Scripting.addGlobal(function selectedDocs(container: Doc, excludeCollections: boolean, prevValue: any) {
- const docs = DocListCast(Doc.UserDoc().SelectedDocs).filter(d => !Doc.AreProtosEqual(d, container) && !d.annotationOn && d.type !== DocumentType.DOCUMENT && d.type !== DocumentType.KVP && (!excludeCollections || !Cast(d.data, listSpec(Doc), null)));
+ const docs = DocListCast(Doc.UserDoc().SelectedDocs).
+ filter(d => !Doc.AreProtosEqual(d, container) && !d.annotationOn && d.type !== DocumentType.DOCUMENT && d.type !== DocumentType.KVP &&
+ (!excludeCollections || d.type !== DocumentType.COL || !Cast(d.data, listSpec(Doc), null)));
return docs.length ? new List(docs) : prevValue;
});
-Scripting.addGlobal(function setDocFilter(container: Doc, key: string, value: any, modifiers?: string) {
- const docFilters = Cast(container._docFilter, listSpec("string"), []);
- for (let i = 0; i < docFilters.length; i += 3) {
- if (docFilters[i] === key && docFilters[i + 1] === value) {
- docFilters.splice(i, 3);
- break;
- }
- }
- if (modifiers !== undefined) {
- docFilters.push(key);
- docFilters.push(value);
- docFilters.push(modifiers);
- container._docFilter = new List<string>(docFilters);
- }
-}); \ No newline at end of file
+Scripting.addGlobal(function setDocFilter(container: Doc, key: string, value: any, modifiers?: "check" | "x" | undefined) { Doc.setDocFilter(container, key, value, modifiers); });
+Scripting.addGlobal(function setDocFilterRange(container: Doc, key: string, range: number[]) { Doc.setDocFilterRange(container, key, range); }); \ No newline at end of file
diff --git a/src/new_fields/InkField.ts b/src/new_fields/InkField.ts
index 4a44b4f55..bb93de5ac 100644
--- a/src/new_fields/InkField.ts
+++ b/src/new_fields/InkField.ts
@@ -8,7 +8,6 @@ export enum InkTool {
Pen,
Highlighter,
Eraser,
- Scrubber,
Stamp
}
diff --git a/src/new_fields/ObjectField.ts b/src/new_fields/ObjectField.ts
index 566104b40..9aa1c9b04 100644
--- a/src/new_fields/ObjectField.ts
+++ b/src/new_fields/ObjectField.ts
@@ -1,4 +1,3 @@
-import { Doc } from "./Doc";
import { RefField } from "./RefField";
import { OnUpdate, Parent, Copy, ToScriptString, ToString } from "./FieldSymbols";
import { Scripting } from "../client/util/Scripting";
diff --git a/src/new_fields/RichTextField.ts b/src/new_fields/RichTextField.ts
index 7b6ce9b98..ad4a5a252 100644
--- a/src/new_fields/RichTextField.ts
+++ b/src/new_fields/RichTextField.ts
@@ -33,20 +33,8 @@ export class RichTextField extends ObjectField {
return this.Text;
}
- [ToPlainText]() {
- // Because we're working with plain text, just concatenate all paragraphs
- let content = JSON.parse(this.Data).doc.content;
- let paragraphs = content.filter((item: any) => item.type === "paragraph");
-
- // Functions to flatten ProseMirror paragraph objects (and their components) to plain text
- // While this function already exists in state.doc.textBeteen(), it doesn't account for newlines
- let blockText = (block: any) => block.text;
- let concatenateParagraph = (p: any) => (p.content ? p.content.map(blockText).join(joiner) : "") + delimiter;
-
- // Concatentate paragraphs and string the result together
- let textParagraphs: string[] = paragraphs.map(concatenateParagraph);
- let plainText = textParagraphs.join(joiner);
- return plainText.substring(0, plainText.length - 1);
+ public static DashField(fieldKey: string) {
+ return new RichTextField(`{"doc":{"type":"doc","content":[{"type":"paragraph","attrs":{"align":null,"color":null,"id":null,"indent":null,"inset":null,"lineSpacing":null,"paddingBottom":null,"paddingTop":null},"content":[{"type":"dashField","attrs":{"fieldKey":"${fieldKey}","docid":""}}]}]},"selection":{"type":"text","anchor":2,"head":2},"storedMarks":[]}`, "");
}
} \ No newline at end of file
diff --git a/src/new_fields/RichTextUtils.ts b/src/new_fields/RichTextUtils.ts
index c50f8cc48..c211b3d3c 100644
--- a/src/new_fields/RichTextUtils.ts
+++ b/src/new_fields/RichTextUtils.ts
@@ -1,5 +1,5 @@
import { EditorState, Transaction, TextSelection } from "prosemirror-state";
-import { Node, Fragment, Mark, MarkType } from "prosemirror-model";
+import { Node, Fragment, Mark } from "prosemirror-model";
import { RichTextField } from "./RichTextField";
import { docs_v1 } from "googleapis";
import { GoogleApiClientUtils } from "../client/apis/google_docs/GoogleApiClientUtils";
@@ -17,6 +17,7 @@ import { Id } from "./FieldSymbols";
import { DocumentView } from "../client/views/nodes/DocumentView";
import { AssertionError } from "assert";
import { Networking } from "../client/Network";
+import { extname } from "path";
export namespace RichTextUtils {
@@ -113,6 +114,7 @@ export namespace RichTextUtils {
width: number;
title: string;
url: string;
+ agnostic: string;
}
const parseInlineObjects = async (document: docs_v1.Schema$Document): Promise<Map<string, ImageTemplate>> => {
@@ -123,12 +125,10 @@ export namespace RichTextUtils {
const objects = Object.keys(inlineObjects).map(objectId => inlineObjects[objectId]);
const mediaItems: MediaItem[] = objects.map(object => {
const embeddedObject = object.inlineObjectProperties!.embeddedObject!;
- const baseUrl = embeddedObject.imageProperties!.contentUri!;
- const filename = `upload_${Utils.GenerateGuid()}.png`;
- return { baseUrl, filename };
+ return { baseUrl: embeddedObject.imageProperties!.contentUri! };
});
- const uploads = await Networking.PostToServer("/googlePhotosMediaDownload", { mediaItems });
+ const uploads = await Networking.PostToServer("/googlePhotosMediaGet", { mediaItems });
if (uploads.length !== mediaItems.length) {
throw new AssertionError({ expected: mediaItems.length, actual: uploads.length, message: "Error with internally uploading inlineObjects!" });
@@ -136,16 +136,17 @@ export namespace RichTextUtils {
for (let i = 0; i < objects.length; i++) {
const object = objects[i];
- const { fileNames } = uploads[i];
+ const { accessPaths } = uploads[i];
+ const { agnostic, _m } = accessPaths;
const embeddedObject = object.inlineObjectProperties!.embeddedObject!;
const size = embeddedObject.size!;
const width = size.width!.magnitude!;
- const url = Utils.fileUrl(fileNames.clean);
inlineObjectMap.set(object.objectId!, {
title: embeddedObject.title || `Imported Image from ${document.title}`,
width,
- url
+ url: Utils.prepend(_m.client),
+ agnostic: Utils.prepend(agnostic.client)
});
}
}
@@ -156,7 +157,6 @@ export namespace RichTextUtils {
interface MediaItem {
baseUrl: string;
- filename: string;
}
export const Import = async (documentId: GoogleApiClientUtils.Docs.DocumentId, textNote: Doc): Promise<Opt<GoogleApiClientUtils.Docs.ImportResult>> => {
@@ -268,19 +268,19 @@ export namespace RichTextUtils {
};
const imageNode = (schema: any, image: ImageTemplate, textNote: Doc) => {
- const { url: src, width } = image;
+ const { url: src, width, agnostic } = image;
let docid: string;
- const guid = Utils.GenerateDeterministicGuid(src);
+ const guid = Utils.GenerateDeterministicGuid(agnostic);
const backingDocId = StrCast(textNote[guid]);
if (!backingDocId) {
- const backingDoc = Docs.Create.ImageDocument(src, { _width: 300, _height: 300 });
- DocumentView.makeCustomViewClicked(backingDoc, undefined, Docs.Create.FreeformDocument);
+ const backingDoc = Docs.Create.ImageDocument(agnostic, { _width: 300, _height: 300 });
+ DocumentView.makeCustomViewClicked(backingDoc, Docs.Create.FreeformDocument);
docid = backingDoc[Id];
textNote[guid] = docid;
} else {
docid = backingDocId;
}
- return schema.node("image", { src, width, docid, float: null, location: "onRight" });
+ return schema.node("image", { src, agnostic, width, docid, float: null, location: "onRight" });
};
const textNode = (schema: any, run: docs_v1.Schema$TextRun) => {
@@ -403,7 +403,7 @@ export namespace RichTextUtils {
let exported = (await Cast(linkDoc.anchor2, Doc))!;
if (!exported.customLayout) {
exported = Doc.MakeAlias(exported);
- DocumentView.makeCustomViewClicked(exported, undefined, Docs.Create.FreeformDocument);
+ DocumentView.makeCustomViewClicked(exported, Docs.Create.FreeformDocument);
linkDoc.anchor2 = exported;
}
url = Utils.shareUrl(exported[Id]);
@@ -436,7 +436,7 @@ export namespace RichTextUtils {
const width = attrs.width;
requests.push(await EncodeImage({
startIndex: position + nodeSize - 1,
- uri: attrs.src,
+ uri: attrs.agnostic,
width: Number(typeof width === "string" ? width.replace("px", "") : width)
}));
}
@@ -499,15 +499,18 @@ export namespace RichTextUtils {
};
};
- const EncodeImage = async (information: ImageInformation) => {
- const source = [Docs.Create.ImageDocument(information.uri)];
+ const EncodeImage = async ({ uri, width, startIndex }: ImageInformation) => {
+ if (!uri) {
+ return {};
+ }
+ const source = [Docs.Create.ImageDocument(uri)];
const baseUrls = await GooglePhotos.Transactions.UploadThenFetch(source);
if (baseUrls) {
return {
insertInlineImage: {
uri: baseUrls[0],
- objectSize: { width: { magnitude: information.width, unit: "PT" } },
- location: { index: information.startIndex }
+ objectSize: { width: { magnitude: width, unit: "PT" } },
+ location: { index: startIndex }
}
};
}
diff --git a/src/new_fields/Schema.ts b/src/new_fields/Schema.ts
index 3f0ff4284..72bce283d 100644
--- a/src/new_fields/Schema.ts
+++ b/src/new_fields/Schema.ts
@@ -33,7 +33,7 @@ export function makeInterface<T extends Interface[]>(...schemas: T): InterfaceFu
get(target: any, prop, receiver) {
const field = receiver.doc[prop];
if (prop in schema) {
- const desc = (schema as any)[prop];
+ const desc = prop === "proto" ? Doc : (schema as any)[prop]; // bcz: proto doesn't appear in schemas ... maybe it should?
if (typeof desc === "object" && "defaultVal" in desc && "type" in desc) {//defaultSpec
return Cast(field, desc.type, desc.defaultVal);
} else if (typeof desc === "function" && !ObjectField.isPrototypeOf(desc) && !RefField.isPrototypeOf(desc)) {
diff --git a/src/new_fields/ScriptField.ts b/src/new_fields/ScriptField.ts
index f8a8d1226..148886848 100644
--- a/src/new_fields/ScriptField.ts
+++ b/src/new_fields/ScriptField.ts
@@ -7,6 +7,7 @@ import { Doc, Field } from "../new_fields/Doc";
import { Plugins } from "./util";
import { computedFn } from "mobx-utils";
import { ProxyField } from "./Proxy";
+import { Cast } from "./Types";
function optional(propSchema: PropSchema) {
return custom(value => {
@@ -106,7 +107,7 @@ export class ScriptField extends ObjectField {
}
public static CompileScript(script: string, params: object = {}, addReturn = false, capturedVariables?: { [name: string]: Field }) {
const compiled = CompileScript(script, {
- params: { this: Doc.name, _last_: "any", ...params },
+ params: { this: Doc.name, self: Doc.name, _last_: "any", ...params },
typecheck: false,
editable: true,
addReturn: addReturn,
@@ -114,13 +115,13 @@ export class ScriptField extends ObjectField {
});
return compiled;
}
- public static MakeFunction(script: string, params: object = {}) {
- const compiled = ScriptField.CompileScript(script, params, true);
+ public static MakeFunction(script: string, params: object = {}, capturedVariables?: { [name: string]: Field }) {
+ const compiled = ScriptField.CompileScript(script, params, true, capturedVariables);
return compiled.compiled ? new ScriptField(compiled) : undefined;
}
- public static MakeScript(script: string, params: object = {}) {
- const compiled = ScriptField.CompileScript(script, params, false);
+ public static MakeScript(script: string, params: object = {}, capturedVariables?: { [name: string]: Field }) {
+ const compiled = ScriptField.CompileScript(script, params, false, capturedVariables);
return compiled.compiled ? new ScriptField(compiled) : undefined;
}
}
@@ -130,7 +131,7 @@ export class ScriptField extends ObjectField {
export class ComputedField extends ScriptField {
_lastComputedResult: any;
//TODO maybe add an observable cache based on what is passed in for doc, considering there shouldn't really be that many possible values for doc
- value = computedFn((doc: Doc) => this._lastComputedResult = this.script.run({ this: doc, _last_: this._lastComputedResult }, console.log).result);
+ value = computedFn((doc: Doc) => this._lastComputedResult = this.script.run({ this: doc, self: Cast(doc.rootDocument, Doc, null) || doc, _last_: this._lastComputedResult }, console.log).result);
public static MakeScript(script: string, params: object = {}) {
const compiled = ScriptField.CompileScript(script, params, false);
return compiled.compiled ? new ComputedField(compiled) : undefined;
diff --git a/src/new_fields/URLField.ts b/src/new_fields/URLField.ts
index cfab36906..fb71160ca 100644
--- a/src/new_fields/URLField.ts
+++ b/src/new_fields/URLField.ts
@@ -49,3 +49,5 @@ export const nullAudio = "https://actions.google.com/sounds/v1/alarms/beep_short
@scriptingGlobal @Deserializable("pdf") export class PdfField extends URLField { }
@scriptingGlobal @Deserializable("web") export class WebField extends URLField { }
@scriptingGlobal @Deserializable("youtube") export class YoutubeField extends URLField { }
+@scriptingGlobal @Deserializable("webcam") export class WebCamField extends URLField { }
+
diff --git a/src/new_fields/documentSchemas.ts b/src/new_fields/documentSchemas.ts
index 4a5c1fdb0..91ea32bee 100644
--- a/src/new_fields/documentSchemas.ts
+++ b/src/new_fields/documentSchemas.ts
@@ -4,39 +4,49 @@ import { Doc } from "./Doc";
import { DateField } from "./DateField";
export const documentSchema = createSchema({
- layout: "string", // this is the native layout string for the document. templates can be added using other fields and setting layoutKey below (see layout_custom as an example)
+ type: "string", // enumerated type of document -- should be template-specific (ie, start with an '_')
+ layout: "string", // this is the native layout string for the document. templates can be added using other fields and setting layoutKey below
layoutKey: "string", // holds the field key for the field that actually holds the current lyoat
- layout_custom: Doc, // used to hold a custom layout (there's nothing special about this field .. any field could hold a custom layout that can be selected by setting 'layoutKey')
title: "string", // document title (can be on either data document or layout)
- _dropAction: "string", // override specifying what should happen when this document is dropped (can be "alias" or "copy")
- _nativeWidth: "number", // native width of document which determines how much document contents are scaled when the document's width is set
- _nativeHeight: "number", // "
- _width: "number", // width of document in its container's coordinate system
- _height: "number", // "
+ dropAction: "string", // override specifying what should happen when this document is dropped (can be "alias" or "copy")
+ childDropAction: "string", // specify the override for what should happen when the child of a collection is dragged from it and dropped (can be "alias" or "copy")
+ _autoHeight: "boolean", // whether the height of the document should be computed automatically based on its contents
+ _nativeWidth: "number", // native width of document which determines how much document contents are scaled when the document's width is set
+ _nativeHeight: "number", // "
+ _width: "number", // width of document in its container's coordinate system
+ _height: "number", // "
+ _xPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set
+ _yPadding: "number", // pixels of padding on top/bottom of collectionfreeformview contents when fitToBox is set
+ _xMargin: "number", // margin added on left/right of most documents to add separation from their container
+ _yMargin: "number", // margin added on top/bottom of most documents to add separation from their container
+ _showCaption: "string", // whether editable caption text is overlayed at the bottom of the document
+ _showTitle: "string", // the fieldkey whose contents should be displayed at the top of the document
+ _showTitleHover: "string", // the showTitle should be shown only on hover
+ _showAudio: "boolean", // whether to show the audio record icon on documents
_freeformLayoutEngine: "string",// the string ID for the layout engine to use to layout freeform view documents
- _LODdisable: "boolean", // whether to disbale LOD switching for CollectionFreeFormViews
+ _LODdisable: "boolean", // whether to disbale LOD switching for CollectionFreeFormViews
+ _pivotField: "string", // specifies which field should be used as the timeline/pivot axis
+ _replacedChrome: "string", // what the default chrome is replaced with. Currently only supports the value of 'replaced' for PresBox's.
+ _chromeStatus: "string", // determines the state of the collection chrome. values allowed are 'replaced', 'enabled', 'disabled', 'collapsed'
+ _freezeOnDrop: "boolean", // whether a document without native dimensions should have its width/height frozen as native dimensions on drop. Used by Timeline view to make sure documents are scaled to fit the display thumbnail
color: "string", // foreground color of document
backgroundColor: "string", // background color of document
opacity: "number", // opacity of document
- creationDate: DateField, // when the document was created
+ creationDate: DateField, // when the document was created
links: listSpec(Doc), // computed (readonly) list of links associated with this document
- removeDropProperties: listSpec("string"), // properties that should be removed from the alias/copy/etc of this document when it is dropped
onClick: ScriptField, // script to run when document is clicked (can be overriden by an onClick prop)
- onPointerDown: ScriptField, // script to run when document is clicked (can be overriden by an onClick prop)
- onPointerUp: ScriptField, // script to run when document is clicked (can be overriden by an onClick prop)
+ onPointerDown: ScriptField, // script to run when document is clicked (can be overriden by an onClick prop)
+ onPointerUp: ScriptField, // script to run when document is clicked (can be overriden by an onClick prop)
onDragStart: ScriptField, // script to run when document is dragged (without being selected). the script should return the Doc to be dropped.
dragFactory: Doc, // the document that serves as the "template" for the onDragStart script. ie, to drag out copies of the dragFactory document.
- ignoreAspect: "boolean", // whether aspect ratio should be ignored when laying out or manipulating the document
- autoHeight: "boolean", // whether the height of the document should be computed automatically based on its contents
+ removeDropProperties: listSpec("string"), // properties that should be removed from the alias/copy/etc of this document when it is dropped
isTemplateForField: "string",// when specifies a field key, then the containing document is a template that renders the specified field
isBackground: "boolean", // whether document is a background element and ignores input events (can only selet with marquee)
- type: "string", // enumerated type of document
+ dontSelect: "boolean", // whether document should be selected when clicked (usually set to false for buttons)
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
- preventTreeViewOpen: "boolean", // ignores the treeViewOpen flag (for allowing a view to not be slaved to other views of the document)
- currentTimecode: "number", // current play back time of a temporal document (video / audio)
- summarizedDocs: listSpec(Doc), // documents that are summarized by this document (and which will typically be opened by clicking this document)
- maximizedDocs: listSpec(Doc), // documents to maximize when clicking this document (generally this document will be an icon)
+ treeViewPreventOpen: "boolean", // ignores the treeViewOpen flag (for allowing a view to not be slaved to other views of the document)
+ currentTimecode: "number", // current play back time of a temporal document (video / audio)
maximizeLocation: "string", // flag for where to place content when following a click interaction (e.g., onRight, inPlace, inTab)
lockedPosition: "boolean", // whether the document can be moved (dragged)
lockedTransform: "boolean", // whether the document can be panned/zoomed
@@ -44,22 +54,15 @@ export const documentSchema = createSchema({
borderRounding: "string", // border radius rounding of document
searchFields: "string", // the search fields to display when this document matches a search in its metadata
heading: "number", // the logical layout 'heading' of this document (used by rule provider to stylize h1 header elements, from h2, etc)
- showCaption: "string", // whether editable caption text is overlayed at the bottom of the document
- showTitle: "string", // the fieldkey whose contents should be displayed at the top of the document
- showTitleHover: "string", // the showTitle should be shown only on hover
isButton: "boolean", // whether document functions as a button (overiding native interactions of its content)
ignoreClick: "boolean", // whether documents ignores input clicks (but does not ignore manipulation and other events)
- isAnimating: "string", // whether the document is in the midst of animating between two layouts (used by icons to de/iconify documents). value is undefined|"min"|"max"
- animateToDimensions: listSpec("number"), // layout information about the target rectangle a document is animating towards
scrollToLinkID: "string", // id of link being traversed. allows this doc to scroll/highlight/etc its link anchor. scrollToLinkID should be set to undefined by this doc after it sets up its scroll,etc.
strokeWidth: "number",
fontSize: "string",
fitToBox: "boolean", // whether freeform view contents should be zoomed/panned to fill the area of the document view
- xPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set
- yPadding: "number", // pixels of padding on left/right of collectionfreeformview contents when fitToBox is set
- LODarea: "number", // area (width*height) where CollectionFreeFormViews switch from a label to rendering contents
letterSpacing: "string",
- textTransform: "string"
+ textTransform: "string",
+ childTemplateName: "string" // the name of a template to use to override the layoutKey when rendering a document in DocumentBox
});
export const positionSchema = createSchema({
diff --git a/src/new_fields/util.ts b/src/new_fields/util.ts
index 2cedda7a6..8c719ccd8 100644
--- a/src/new_fields/util.ts
+++ b/src/new_fields/util.ts
@@ -1,5 +1,5 @@
import { UndoManager } from "../client/util/UndoManager";
-import { Doc, Field, FieldResult, UpdatingFromServer } from "./Doc";
+import { Doc, Field, FieldResult, UpdatingFromServer, LayoutSym } from "./Doc";
import { SerializationHelper } from "../client/util/SerializationHelper";
import { ProxyField, PrefetchProxy } from "./Proxy";
import { RefField } from "./RefField";
@@ -7,14 +7,14 @@ import { ObjectField } from "./ObjectField";
import { action, trace } from "mobx";
import { Parent, OnUpdate, Update, Id, SelfProxy, Self } from "./FieldSymbols";
import { DocServer } from "../client/DocServer";
-import { props } from "bluebird";
function _readOnlySetter(): never {
throw new Error("Documents can't be modified in read-only mode");
}
+const tracing = false;
export function TraceMobx() {
- //trace();
+ tracing && trace();
}
export interface GetterResult {
@@ -100,19 +100,17 @@ export function makeEditable() {
_setter = _setterImpl;
}
-let layoutProps = ["panX", "panY", "width", "height", "nativeWidth", "nativeHeight", "fitWidth", "fitToBox",
- "LODdisable", "dropAction", "chromeStatus", "viewType", "gridGap", "xMargin", "yMargin", "autoHeight"];
+const layoutProps = ["panX", "panY", "width", "height", "nativeWidth", "nativeHeight", "fitWidth", "fitToBox",
+ "LODdisable", "chromeStatus", "viewType", "gridGap", "xMargin", "yMargin", "autoHeight"];
export function setter(target: any, in_prop: string | symbol | number, value: any, receiver: any): boolean {
let prop = in_prop;
- if (typeof prop === "string" && prop !== "__id" && prop !== "__fields" &&
- ((prop as string).startsWith("_") || layoutProps.includes(prop))) {
+ if (typeof prop === "string" && prop !== "__id" && prop !== "__fields" && (prop.startsWith("_") || layoutProps.includes(prop))) {
if (!prop.startsWith("_")) {
console.log(prop + " is deprecated - switch to _" + prop);
prop = "_" + prop;
}
- const resolvedLayout = getFieldImpl(target, getFieldImpl(target, "layoutKey", receiver), receiver);
- if (resolvedLayout instanceof Doc) {
- resolvedLayout[prop] = value;
+ if (target.__LAYOUT__) {
+ target.__LAYOUT__[prop] = value;
return true;
}
}
@@ -121,16 +119,15 @@ export function setter(target: any, in_prop: string | symbol | number, value: an
export function getter(target: any, in_prop: string | symbol | number, receiver: any): any {
let prop = in_prop;
- if (typeof prop === "string" && prop !== "__id" && prop !== "__fields" &&
- ((prop as string).startsWith("_") || layoutProps.includes(prop))) {
+ if (prop === LayoutSym) {
+ return target.__LAYOUT__;
+ }
+ if (typeof prop === "string" && prop !== "__id" && prop !== "__fields" && (prop.startsWith("_") || layoutProps.includes(prop))) {
if (!prop.startsWith("_")) {
console.log(prop + " is deprecated - switch to _" + prop);
prop = "_" + prop;
}
- const resolvedLayout = getFieldImpl(target, getFieldImpl(target, "layoutKey", receiver), receiver);
- if (resolvedLayout instanceof Doc) {
- return resolvedLayout[prop];
- }
+ if (target.__LAYOUT__) return target.__LAYOUT__[prop];
}
if (prop === "then") {//If we're being awaited
return undefined;
diff --git a/src/scraping/buxton/.idea/buxton.iml b/src/scraping/buxton/.idea/buxton.iml
new file mode 100644
index 000000000..d0876a78d
--- /dev/null
+++ b/src/scraping/buxton/.idea/buxton.iml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="PYTHON_MODULE" version="4">
+ <component name="NewModuleRootManager">
+ <content url="file://$MODULE_DIR$" />
+ <orderEntry type="inheritedJdk" />
+ <orderEntry type="sourceFolder" forTests="false" />
+ </component>
+</module> \ No newline at end of file
diff --git a/src/scraping/buxton/.idea/inspectionProfiles/profiles_settings.xml b/src/scraping/buxton/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 000000000..105ce2da2
--- /dev/null
+++ b/src/scraping/buxton/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,6 @@
+<component name="InspectionProjectProfileManager">
+ <settings>
+ <option name="USE_PROJECT_PROFILE" value="false" />
+ <version value="1.0" />
+ </settings>
+</component> \ No newline at end of file
diff --git a/src/scraping/buxton/.idea/misc.xml b/src/scraping/buxton/.idea/misc.xml
new file mode 100644
index 000000000..a2e120dcc
--- /dev/null
+++ b/src/scraping/buxton/.idea/misc.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.7" project-jdk-type="Python SDK" />
+</project> \ No newline at end of file
diff --git a/src/scraping/buxton/.idea/modules.xml b/src/scraping/buxton/.idea/modules.xml
new file mode 100644
index 000000000..5bbca8f01
--- /dev/null
+++ b/src/scraping/buxton/.idea/modules.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="ProjectModuleManager">
+ <modules>
+ <module fileurl="file://$PROJECT_DIR$/.idea/buxton.iml" filepath="$PROJECT_DIR$/.idea/buxton.iml" />
+ </modules>
+ </component>
+</project> \ No newline at end of file
diff --git a/src/scraping/buxton/.idea/vcs.xml b/src/scraping/buxton/.idea/vcs.xml
new file mode 100644
index 000000000..c2365ab11
--- /dev/null
+++ b/src/scraping/buxton/.idea/vcs.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="VcsDirectoryMappings">
+ <mapping directory="$PROJECT_DIR$/../../.." vcs="Git" />
+ </component>
+</project> \ No newline at end of file
diff --git a/src/scraping/buxton/.idea/workspace.xml b/src/scraping/buxton/.idea/workspace.xml
new file mode 100644
index 000000000..c1db7a75b
--- /dev/null
+++ b/src/scraping/buxton/.idea/workspace.xml
@@ -0,0 +1,173 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="ChangeListManager">
+ <list default="true" id="693c6819-edcc-46d6-8260-3f51ec080a46" name="Default Changelist" comment="">
+ <change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
+ </list>
+ <option name="SHOW_DIALOG" value="false" />
+ <option name="HIGHLIGHT_CONFLICTS" value="true" />
+ <option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
+ <option name="LAST_RESOLUTION" value="IGNORE" />
+ </component>
+ <component name="Git.Settings">
+ <option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$/../../.." />
+ </component>
+ <component name="ProjectId" id="1XDYVVOvUV6lmODouwAWUpvxnni" />
+ <component name="ProjectLevelVcsManager" settingsEditedManually="true" />
+ <component name="ProjectViewState">
+ <option name="hideEmptyMiddlePackages" value="true" />
+ <option name="showExcludedFiles" value="true" />
+ <option name="showLibraryContents" value="true" />
+ </component>
+ <component name="PropertiesComponent">
+ <property name="ASKED_SHARE_PROJECT_CONFIGURATION_FILES" value="true" />
+ <property name="RunOnceActivity.ShowReadmeOnStart" value="true" />
+ <property name="last_opened_file_path" value="$PROJECT_DIR$" />
+ <property name="settings.editor.selected.configurable" value="com.jetbrains.python.configuration.PyActiveSdkModuleConfigurable" />
+ </component>
+ <component name="RunManager" selected="Python.narratives">
+ <configuration name="jsonifier" type="PythonConfigurationType" factoryName="Python" nameIsGenerated="true">
+ <module name="buxton" />
+ <option name="INTERPRETER_OPTIONS" value="" />
+ <option name="PARENT_ENVS" value="true" />
+ <envs>
+ <env name="PYTHONUNBUFFERED" value="1" />
+ </envs>
+ <option name="SDK_HOME" value="/usr/local/bin/python3.7" />
+ <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
+ <option name="IS_MODULE_SDK" value="false" />
+ <option name="ADD_CONTENT_ROOTS" value="true" />
+ <option name="ADD_SOURCE_ROOTS" value="true" />
+ <option name="SCRIPT_NAME" value="$PROJECT_DIR$/jsonifier.py" />
+ <option name="PARAMETERS" value="" />
+ <option name="SHOW_COMMAND_LINE" value="false" />
+ <option name="EMULATE_TERMINAL" value="false" />
+ <option name="MODULE_MODE" value="false" />
+ <option name="REDIRECT_INPUT" value="false" />
+ <option name="INPUT_FILE" value="" />
+ <method v="2" />
+ </configuration>
+ <configuration name="narratives" type="PythonConfigurationType" factoryName="Python" nameIsGenerated="true">
+ <module name="buxton" />
+ <option name="INTERPRETER_OPTIONS" value="" />
+ <option name="PARENT_ENVS" value="true" />
+ <envs>
+ <env name="PYTHONUNBUFFERED" value="1" />
+ </envs>
+ <option name="SDK_HOME" value="/usr/local/bin/python3.7" />
+ <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
+ <option name="IS_MODULE_SDK" value="false" />
+ <option name="ADD_CONTENT_ROOTS" value="true" />
+ <option name="ADD_SOURCE_ROOTS" value="true" />
+ <option name="SCRIPT_NAME" value="$PROJECT_DIR$/narratives.py" />
+ <option name="PARAMETERS" value="" />
+ <option name="SHOW_COMMAND_LINE" value="false" />
+ <option name="EMULATE_TERMINAL" value="false" />
+ <option name="MODULE_MODE" value="false" />
+ <option name="REDIRECT_INPUT" value="false" />
+ <option name="INPUT_FILE" value="" />
+ <method v="2" />
+ </configuration>
+ <configuration name="scraper" type="PythonConfigurationType" factoryName="Python">
+ <module name="buxton" />
+ <option name="INTERPRETER_OPTIONS" value="" />
+ <option name="PARENT_ENVS" value="true" />
+ <envs>
+ <env name="PYTHONUNBUFFERED" value="1" />
+ </envs>
+ <option name="SDK_HOME" value="/usr/local/bin/python3.7" />
+ <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
+ <option name="IS_MODULE_SDK" value="false" />
+ <option name="ADD_CONTENT_ROOTS" value="true" />
+ <option name="ADD_SOURCE_ROOTS" value="true" />
+ <option name="SCRIPT_NAME" value="$PROJECT_DIR$/scraper.py" />
+ <option name="PARAMETERS" value="" />
+ <option name="SHOW_COMMAND_LINE" value="false" />
+ <option name="EMULATE_TERMINAL" value="false" />
+ <option name="MODULE_MODE" value="false" />
+ <option name="REDIRECT_INPUT" value="false" />
+ <option name="INPUT_FILE" value="" />
+ <method v="2" />
+ </configuration>
+ <list>
+ <item itemvalue="Python.jsonifier" />
+ <item itemvalue="Python.narratives" />
+ <item itemvalue="Python.scraper" />
+ </list>
+ </component>
+ <component name="SvnConfiguration">
+ <configuration />
+ </component>
+ <component name="TaskManager">
+ <task active="true" id="Default" summary="Default task">
+ <changelist id="693c6819-edcc-46d6-8260-3f51ec080a46" name="Default Changelist" comment="" />
+ <created>1580582155646</created>
+ <option name="number" value="Default" />
+ <option name="presentableId" value="Default" />
+ <updated>1580582155646</updated>
+ </task>
+ <servers />
+ </component>
+ <component name="WindowStateProjectService">
+ <state x="184" y="103" key="#com.intellij.execution.impl.EditConfigurationsDialog" timestamp="1580656983882">
+ <screen x="0" y="23" width="1440" height="836" />
+ </state>
+ <state x="184" y="103" key="#com.intellij.execution.impl.EditConfigurationsDialog/0.23.1440.836@0.23.1440.836" timestamp="1580656983882" />
+ <state x="483" y="152" key="#xdebugger.evaluate" timestamp="1580601059439">
+ <screen x="0" y="23" width="1440" height="836" />
+ </state>
+ <state x="483" y="152" key="#xdebugger.evaluate/0.23.1440.836@0.23.1440.836" timestamp="1580601059439" />
+ <state width="1419" height="268" key="GridCell.Tab.0.bottom" timestamp="1580786975290">
+ <screen x="0" y="23" width="1440" height="836" />
+ </state>
+ <state width="1419" height="268" key="GridCell.Tab.0.bottom/0.23.1440.836@0.23.1440.836" timestamp="1580786975289" />
+ <state width="1419" height="268" key="GridCell.Tab.0.center" timestamp="1580786975289">
+ <screen x="0" y="23" width="1440" height="836" />
+ </state>
+ <state width="1419" height="268" key="GridCell.Tab.0.center/0.23.1440.836@0.23.1440.836" timestamp="1580786975289" />
+ <state width="1419" height="268" key="GridCell.Tab.0.left" timestamp="1580786975289">
+ <screen x="0" y="23" width="1440" height="836" />
+ </state>
+ <state width="1419" height="268" key="GridCell.Tab.0.left/0.23.1440.836@0.23.1440.836" timestamp="1580786975289" />
+ <state width="1419" height="268" key="GridCell.Tab.0.right" timestamp="1580786975289">
+ <screen x="0" y="23" width="1440" height="836" />
+ </state>
+ <state width="1419" height="268" key="GridCell.Tab.0.right/0.23.1440.836@0.23.1440.836" timestamp="1580786975289" />
+ <state width="1419" height="268" key="GridCell.Tab.1.bottom" timestamp="1580786975292">
+ <screen x="0" y="23" width="1440" height="836" />
+ </state>
+ <state width="1419" height="268" key="GridCell.Tab.1.bottom/0.23.1440.836@0.23.1440.836" timestamp="1580786975292" />
+ <state width="1419" height="268" key="GridCell.Tab.1.center" timestamp="1580786975291">
+ <screen x="0" y="23" width="1440" height="836" />
+ </state>
+ <state width="1419" height="268" key="GridCell.Tab.1.center/0.23.1440.836@0.23.1440.836" timestamp="1580786975291" />
+ <state width="1419" height="268" key="GridCell.Tab.1.left" timestamp="1580786975290">
+ <screen x="0" y="23" width="1440" height="836" />
+ </state>
+ <state width="1419" height="268" key="GridCell.Tab.1.left/0.23.1440.836@0.23.1440.836" timestamp="1580786975290" />
+ <state width="1419" height="268" key="GridCell.Tab.1.right" timestamp="1580786975292">
+ <screen x="0" y="23" width="1440" height="836" />
+ </state>
+ <state width="1419" height="268" key="GridCell.Tab.1.right/0.23.1440.836@0.23.1440.836" timestamp="1580786975292" />
+ <state x="229" y="80" key="SettingsEditor" timestamp="1580610123068">
+ <screen x="0" y="23" width="1440" height="836" />
+ </state>
+ <state x="229" y="80" key="SettingsEditor/0.23.1440.836@0.23.1440.836" timestamp="1580610123068" />
+ <state width="720" height="417" key="XDebugger.FullValuePopup" timestamp="1580584300118">
+ <screen x="0" y="23" width="1440" height="836" />
+ </state>
+ <state width="720" height="417" key="XDebugger.FullValuePopup/0.23.1440.836@0.23.1440.836" timestamp="1580584300118" />
+ <state x="399" y="273" key="com.intellij.ide.util.TipDialog" timestamp="1580799621511">
+ <screen x="0" y="23" width="1440" height="836" />
+ </state>
+ <state x="399" y="273" key="com.intellij.ide.util.TipDialog/0.23.1440.836@0.23.1440.836" timestamp="1580799621511" />
+ <state x="515" y="128" key="com.intellij.openapi.editor.actions.MultiplePasteAction$ClipboardContentChooser" timestamp="1580582281665">
+ <screen x="0" y="23" width="1440" height="836" />
+ </state>
+ <state x="515" y="128" key="com.intellij.openapi.editor.actions.MultiplePasteAction$ClipboardContentChooser/0.23.1440.836@0.23.1440.836" timestamp="1580582281665" />
+ <state x="385" y="183" width="670" height="676" key="search.everywhere.popup" timestamp="1580585906043">
+ <screen x="0" y="23" width="1440" height="836" />
+ </state>
+ <state x="385" y="183" width="670" height="676" key="search.everywhere.popup/0.23.1440.836@0.23.1440.836" timestamp="1580585906043" />
+ </component>
+</project> \ No newline at end of file
diff --git a/src/scraping/buxton/final/BuxtonImporter.ts b/src/scraping/buxton/final/BuxtonImporter.ts
new file mode 100644
index 000000000..e7a0d367d
--- /dev/null
+++ b/src/scraping/buxton/final/BuxtonImporter.ts
@@ -0,0 +1,389 @@
+import { readdirSync, writeFile, mkdirSync } from "fs";
+import * as path from "path";
+import { red, cyan, yellow } from "colors";
+import { Utils } from "../../../Utils";
+import rimraf = require("rimraf");
+import { DashUploadUtils } from "../../../server/DashUploadUtils";
+const StreamZip = require('node-stream-zip');
+const createImageSizeStream = require("image-size-stream");
+import { parseXml } from "libxmljs";
+import { strictEqual } from "assert";
+import { Readable, PassThrough } from "stream";
+
+interface DocumentContents {
+ body: string;
+ imageData: ImageData[];
+ hyperlinks: string[];
+ captions: string[];
+ embeddedFileNames: string[];
+}
+
+export interface DeviceDocument {
+ title: string;
+ shortDescription: string;
+ longDescription: string;
+ company: string;
+ year: number;
+ originalPrice?: number;
+ degreesOfFreedom?: number;
+ dimensions?: string;
+ primaryKey: string;
+ secondaryKey: string;
+ attribute: string;
+ __images: ImageData[];
+ hyperlinks: string[];
+ captions: string[];
+ embeddedFileNames: string[];
+}
+
+export interface AnalysisResult {
+ device?: DeviceDocument;
+ errors?: { [key: string]: string };
+}
+
+type Transformer<T> = (raw: string) => TransformResult<T>;
+interface TransformResult<T> {
+ transformed?: T;
+ error?: string;
+}
+
+export interface ImportResults {
+ deviceCount: number;
+ errorCount: number;
+}
+
+type ResultCallback = (result: AnalysisResult) => void;
+type TerminatorCallback = (result: ImportResults) => void;
+
+interface Processor<T> {
+ exp: RegExp;
+ matchIndex?: number;
+ transformer?: Transformer<T>;
+ required?: boolean;
+}
+
+interface ImageData {
+ url: string;
+ nativeWidth: number;
+ nativeHeight: number;
+}
+
+namespace Utilities {
+
+ export function numberValue(raw: string): TransformResult<number> {
+ const transformed = Number(raw);
+ if (isNaN(transformed)) {
+ return { error: `${raw} cannot be parsed to a numeric value.` };
+ }
+ return { transformed };
+ }
+
+ export function collectUniqueTokens(raw: string): TransformResult<string[]> {
+ const pieces = raw.replace(/,|\s+and\s+/g, " ").split(/\s+/).filter(piece => piece.length);
+ const unique = new Set(pieces.map(token => token.toLowerCase().trim()));
+ return { transformed: Array.from(unique).map(capitalize).sort() };
+ }
+
+ export function correctSentences(raw: string): TransformResult<string> {
+ raw = raw.replace(/\./g, ". ").replace(/\:/g, ": ").replace(/\,/g, ", ").replace(/\?/g, "? ").trimRight();
+ raw = raw.replace(/\s{2,}/g, " ");
+ return { transformed: raw };
+ }
+
+ export function capitalize(word: string): string {
+ const clean = word.trim();
+ if (!clean.length) {
+ return word;
+ }
+ return word.charAt(0).toUpperCase() + word.slice(1);
+ }
+
+ export async function readAndParseXml(zip: any, relativePath: string) {
+ console.log(`Text streaming ${relativePath}`);
+ const contents = await new Promise<string>((resolve, reject) => {
+ let body = "";
+ zip.stream(relativePath, (error: any, stream: any) => {
+ if (error) {
+ reject(error);
+ }
+ stream.on('data', (chunk: any) => body += chunk.toString());
+ stream.on('end', () => resolve(body));
+ });
+ });
+
+ return parseXml(contents);
+ }
+
+}
+
+const RegexMap = new Map<keyof DeviceDocument, Processor<any>>([
+ ["title", {
+ exp: /contact\s+(.*)Short Description:/
+ }],
+ ["company", {
+ exp: /Company:\s+([^\|]*)\s+\|/,
+ transformer: (raw: string) => ({ transformed: raw.replace(/\./g, "") })
+ }],
+ ["year", {
+ exp: /Year:\s+([^\|]*)\s+\|/,
+ transformer: (raw: string) => Utilities.numberValue(/[0-9]{4}/.exec(raw)![0])
+ }],
+ ["primaryKey", {
+ exp: /Primary:\s+(.*)(Secondary|Additional):/,
+ transformer: raw => {
+ const { transformed, error } = Utilities.collectUniqueTokens(raw);
+ return transformed ? { transformed: transformed[0] } : { error };
+ }
+ }],
+ ["secondaryKey", {
+ exp: /(Secondary|Additional):\s+(.*)Attributes?:/,
+ transformer: raw => {
+ const { transformed, error } = Utilities.collectUniqueTokens(raw);
+ return transformed ? { transformed: transformed[0] } : { error };
+ },
+ matchIndex: 2
+ }],
+ ["attribute", {
+ exp: /Attributes?:\s+(.*)Links/,
+ transformer: raw => {
+ const { transformed, error } = Utilities.collectUniqueTokens(raw);
+ return transformed ? { transformed: transformed[0] } : { error };
+ },
+ }],
+ ["originalPrice", {
+ exp: /Original Price \(USD\)\:\s+(\$[0-9\,]+\.[0-9]+|NFS)/,
+ transformer: (raw: string) => {
+ raw = raw.replace(/\,/g, "");
+ if (raw === "NFS") {
+ return { transformed: -1 };
+ }
+ return Utilities.numberValue(raw.slice(1));
+ },
+ required: false
+ }],
+ ["degreesOfFreedom", {
+ exp: /Degrees of Freedom:\s+([0-9]+)/,
+ transformer: Utilities.numberValue,
+ required: false
+ }],
+ ["dimensions", {
+ exp: /Dimensions\s+\(L x W x H\):\s+([0-9\.]+\s+x\s+[0-9\.]+\s+x\s+[0-9\.]+\s\([A-Za-z]+\))/,
+ transformer: (raw: string) => {
+ const [length, width, group] = raw.split(" x ");
+ const [height, unit] = group.split(" ");
+ return {
+ transformed: {
+ dim_length: Number(length),
+ dim_width: Number(width),
+ dim_height: Number(height),
+ dim_unit: unit.replace(/[\(\)]+/g, "")
+ }
+ };
+ },
+ required: false
+ }],
+ ["shortDescription", {
+ exp: /Short Description:\s+(.*)Bill Buxton[’']s Notes/,
+ transformer: Utilities.correctSentences
+ }],
+ ["longDescription", {
+ exp: /Bill Buxton[’']s Notes(.*)Device Details/,
+ transformer: Utilities.correctSentences
+ }],
+]);
+
+const sourceDir = path.resolve(__dirname, "source");
+const outDir = path.resolve(__dirname, "json");
+const imageDir = path.resolve(__dirname, "../../../server/public/files/images/buxton");
+const successOut = "buxton.json";
+const failOut = "incomplete.json";
+const deviceKeys = Array.from(RegexMap.keys());
+
+export default async function executeImport(emitter: ResultCallback, terminator: TerminatorCallback) {
+ try {
+ const contents = readdirSync(sourceDir);
+ const wordDocuments = contents.filter(file => /.*\.docx?$/.test(file)).map(file => `${sourceDir}/${file}`);
+ [outDir, imageDir].forEach(dir => {
+ rimraf.sync(dir);
+ mkdirSync(dir);
+ });
+ return parseFiles(wordDocuments, emitter, terminator);
+ } catch (e) {
+ const message = [
+ "Unable to find a source directory.",
+ "Please ensure that the following directory exists and is populated with Word documents:",
+ `${sourceDir}`
+ ].join('\n');
+ console.log(red(message));
+ return { error: message };
+ }
+}
+
+async function parseFiles(wordDocuments: string[], emitter: ResultCallback, terminator: TerminatorCallback): Promise<DeviceDocument[]> {
+ const results: AnalysisResult[] = [];
+ for (const filePath of wordDocuments) {
+ const fileName = path.basename(filePath).replace("Bill_Notes_", "");
+ console.log(cyan(`\nExtracting contents from ${fileName}...`));
+ const result = analyze(fileName, await extractFileContents(filePath));
+ emitter(result);
+ results.push(result);
+ }
+
+ const masterDevices: DeviceDocument[] = [];
+ const masterErrors: { [key: string]: string }[] = [];
+ results.forEach(({ device, errors }) => {
+ if (device) {
+ masterDevices.push(device);
+ } else if (errors) {
+ masterErrors.push(errors);
+ }
+ });
+
+ const total = wordDocuments.length;
+ if (masterDevices.length + masterErrors.length !== total) {
+ throw new Error(`Encountered a ${masterDevices.length} to ${masterErrors.length} mismatch in device / error split!`);
+ }
+
+ console.log();
+ await writeOutputFile(successOut, masterDevices, total, true);
+ await writeOutputFile(failOut, masterErrors, total, false);
+ console.log();
+
+ terminator({ deviceCount: masterDevices.length, errorCount: masterErrors.length });
+
+ return masterDevices;
+}
+
+const tableCellXPath = '//*[name()="w:tbl"]/*[name()="w:tr"]/*[name()="w:tc"]';
+const hyperlinkXPath = '//*[name()="Relationship" and contains(@Type, "hyperlink")]';
+
+async function extractFileContents(pathToDocument: string): Promise<DocumentContents> {
+ console.log('Extracting text...');
+ const zip = new StreamZip({ file: pathToDocument, storeEntries: true });
+ await new Promise<void>(resolve => zip.on('ready', resolve));
+
+ // extract the body of the document and, specifically, its captions
+ const document = await Utilities.readAndParseXml(zip, "word/document.xml");
+ const body = document.root()?.text() ?? "No body found. Check the import script's XML parser.";
+ const captions: string[] = [];
+ const embeddedFileNames: string[] = [];
+ const captionTargets = document.find(tableCellXPath).map(node => node.text());
+
+ const { length } = captionTargets;
+ strictEqual(length > 3, true, "No captions written.");
+ strictEqual(length % 3 === 0, true, "Improper caption formatting.");
+
+ for (let i = 3; i < captionTargets.length; i += 3) {
+ const row = captionTargets.slice(i, i + 3);
+ captions.push(row[1]);
+ embeddedFileNames.push(row[2]);
+ }
+
+ // extract all hyperlinks embedded in the document
+ const rels = await Utilities.readAndParseXml(zip, "word/_rels/document.xml.rels");
+ const hyperlinks = rels.find(hyperlinkXPath).map(el => el.attrs()[2].value());
+ console.log("Text extracted.");
+
+ console.log("Beginning image extraction...");
+ const imageData = await writeImages(zip);
+ console.log(`Extracted ${imageData.length} images.`);
+
+ zip.close();
+
+ return { body, imageData, captions, embeddedFileNames, hyperlinks };
+}
+
+const imageEntry = /^word\/media\/\w+\.(jpeg|jpg|png|gif)/;
+
+interface Dimensions {
+ width: number;
+ height: number;
+ type: string;
+}
+
+async function writeImages(zip: any): Promise<ImageData[]> {
+ const allEntries = Object.values<any>(zip.entries()).map(({ name }) => name);
+ const imageEntries = allEntries.filter(name => imageEntry.test(name));
+
+ const imageUrls: ImageData[] = [];
+ for (const mediaPath of imageEntries) {
+ const getImageStream = () => new Promise<Readable>((resolve, reject) => {
+ zip.stream(mediaPath, (error: any, stream: any) => error ? reject(error) : resolve(stream));
+ });
+
+ const { width, height, type } = await new Promise<Dimensions>(async resolve => {
+ const sizeStream = (createImageSizeStream() as PassThrough).on('size', (dimensions: Dimensions) => {
+ readStream.destroy();
+ resolve(dimensions);
+ }).on("error", () => readStream.destroy());
+ const readStream = await getImageStream();
+ readStream.pipe(sizeStream);
+ });
+ if (Math.abs(width - height) < 10) {
+ continue;
+ }
+
+ const generatedFileName = `upload_${Utils.GenerateGuid()}.${type.toLowerCase()}`;
+ await DashUploadUtils.outputResizedImages(getImageStream, generatedFileName, imageDir);
+
+ imageUrls.push({
+ url: `/files/images/buxton/${generatedFileName}`,
+ nativeWidth: width,
+ nativeHeight: height
+ });
+ }
+
+ return imageUrls;
+}
+
+function analyze(fileName: string, contents: DocumentContents): AnalysisResult {
+ const { body, imageData, captions, hyperlinks, embeddedFileNames } = contents;
+ const device: any = {
+ hyperlinks,
+ captions,
+ embeddedFileNames,
+ __images: imageData
+ };
+ const errors: { [key: string]: string } = { fileName };
+
+ for (const key of deviceKeys) {
+ const { exp, transformer, matchIndex, required } = RegexMap.get(key)!;
+ const matches = exp.exec(body);
+
+ let captured: string;
+ if (matches && (captured = matches[matchIndex ?? 1])) {
+ captured = captured.replace(/\s{2,}/g, " ");
+ if (transformer) {
+ const { error, transformed } = transformer(captured);
+ if (error) {
+ errors[key] = `__ERR__${key.toUpperCase()}__TRANSFORM__: ${error}`;
+ continue;
+ }
+ captured = transformed;
+ }
+
+ device[key] = captured;
+ } else if (required ?? true) {
+ errors[key] = `ERR__${key.toUpperCase()}__: outer match ${matches === null ? "wasn't" : "was"} captured.`;
+ continue;
+ }
+ }
+
+ const errorKeys = Object.keys(errors);
+ if (errorKeys.length > 1) {
+ console.log(red(`@ ${cyan(fileName.toUpperCase())}...`));
+ errorKeys.forEach(key => key !== "filename" && console.log(red(errors[key])));
+ return { errors };
+ }
+
+ return { device };
+}
+
+async function writeOutputFile(relativePath: string, data: any[], total: number, success: boolean) {
+ console.log(yellow(`Encountered ${data.length} ${success ? "valid" : "invalid"} documents out of ${total} candidates. Writing ${relativePath}...`));
+ return new Promise<void>((resolve, reject) => {
+ const destination = path.resolve(outDir, relativePath);
+ const contents = JSON.stringify(data, undefined, 4);
+ writeFile(destination, contents, err => err ? reject(err) : resolve());
+ });
+} \ No newline at end of file
diff --git a/src/scraping/buxton/jsonifier.py b/src/scraping/buxton/jsonifier.py
new file mode 100644
index 000000000..a315d49c0
--- /dev/null
+++ b/src/scraping/buxton/jsonifier.py
@@ -0,0 +1,231 @@
+import os
+import docx2txt
+from docx import Document
+from docx.opc.constants import RELATIONSHIP_TYPE as RT
+import re
+import shutil
+import uuid
+import json
+import base64
+from shutil import copyfile
+from PIL import Image
+
+files_path = "../../server/public/files"
+source_path = "./source"
+temp_images_path = "./extracted_images"
+server_images_path = f"{files_path}/images/buxton"
+json_path = "./json"
+
+
+# noinspection PyProtectedMember
+def extract_links(file):
+ links = []
+ doc = Document(file)
+ rels = doc.part.rels
+ for rel in rels:
+ item = rels[rel]
+ if item.reltype == RT.HYPERLINK and ".aspx" not in item._target:
+ links.append(item._target)
+ return links
+
+
+def extract_value(kv_string):
+ pieces = kv_string.split(":")
+ return (pieces[1] if len(pieces) > 1 else kv_string).strip()
+
+
+def mkdir_if_absent(path):
+ try:
+ if not os.path.exists(path):
+ os.mkdir(path)
+ except OSError:
+ print("failed to create the appropriate directory structures for %s" % file_name)
+
+
+def guid():
+ return str(uuid.uuid4())
+
+
+def encode_image(folder: str, name: str):
+ with open(f"{temp_images_path}/{folder}/{name}", "rb") as image:
+ encoded = base64.b64encode(image.read())
+ return encoded.decode("utf-8")
+
+
+def parse_document(name: str):
+ print(f"parsing {name}...")
+ pure_name = name.split(".")[0]
+
+ result = {}
+
+ saved_device_images_dir = server_images_path + "/" + pure_name
+ temp_device_images_dir = temp_images_path + "/" + pure_name
+ mkdir_if_absent(temp_device_images_dir)
+ mkdir_if_absent(saved_device_images_dir)
+
+ raw = str(docx2txt.process(source_path +
+ "/" + name, temp_device_images_dir))
+
+ extracted_images = []
+ for image in os.listdir(temp_device_images_dir):
+ temp = f"{temp_device_images_dir}/{image}"
+ native_width, native_height = Image.open(temp).size
+ if abs(native_width - native_height) < 10:
+ continue
+ original = saved_device_images_dir + "/" + image.replace(".", "_o.", 1)
+ medium = saved_device_images_dir + "/" + image.replace(".", "_m.", 1)
+ copyfile(temp, original)
+ copyfile(temp, medium)
+ server_path = f"http://localhost:1050/files/images/buxton/{pure_name}/{image}"
+ extracted_images.append(server_path)
+ result["extracted_images"] = extracted_images
+
+ def sanitize(line): return re.sub("[\n\t]+", "", line).replace(u"\u00A0", " ").replace(
+ u"\u2013", "-").replace(u"\u201c", '''"''').replace(u"\u201d", '''"''').strip()
+
+ def sanitize_price(raw_price: str):
+ raw_price = raw_price.replace(",", "")
+ start = raw_price.find("$")
+ if "x" in raw_price.lower():
+ return None
+ if start > -1:
+ i = start + 1
+ while i < len(raw_price) and re.match(r"[0-9.]", raw_price[i]):
+ i += 1
+ price = raw_price[start + 1: i + 1]
+ return float(price)
+ elif raw_price.lower().find("nfs"):
+ return -1
+ else:
+ return None
+
+ def remove_empty(line): return len(line) > 1
+
+ def try_parse(to_parse: int):
+ try:
+ value = int(to_parse)
+ return value
+ except ValueError:
+ value = None
+ return value
+
+ lines = list(map(sanitize, raw.split("\n")))
+ lines = list(filter(remove_empty, lines))
+
+ result["title"] = lines[2].strip()
+ result["short_description"] = lines[3].strip().replace(
+ "Short Description: ", "")
+
+ cur = 5
+ notes = ""
+ while lines[cur] != "Device Details":
+ notes += lines[cur] + " "
+ cur += 1
+ result["buxton_notes"] = notes.strip()
+
+ cur += 1
+ clean = list(
+ map(lambda data: data.strip().split(":"), lines[cur].split("|")))
+ result["company"] = clean[0][len(clean[0]) - 1].strip()
+
+ result["year"] = try_parse(clean[1][len(clean[1]) - 1].strip())
+ result["original_price"] = sanitize_price(
+ clean[2][len(clean[2]) - 1].strip())
+
+ cur += 1
+
+ result["degrees_of_freedom"] = try_parse(extract_value(
+ lines[cur]).replace("NA", "N/A"))
+ cur += 1
+
+ dimensions = lines[cur].lower()
+ if dimensions.startswith("dimensions"):
+ dim_concat = dimensions[11:].strip()
+ cur += 1
+ while lines[cur] != "Key Words":
+ dim_concat += (" " + lines[cur].strip())
+ cur += 1
+ result["dimensions"] = dim_concat
+ else:
+ result["dimensions"] = "N/A"
+
+ cur += 1
+ result["primary_key"] = extract_value(lines[cur])
+ cur += 1
+ result["secondary_key"] = extract_value(lines[cur])
+
+ while lines[cur] != "Links":
+ result["secondary_key"] += (" " + extract_value(lines[cur]).strip())
+ cur += 1
+
+ cur += 1
+ link_descriptions = []
+ while lines[cur] != "Image":
+ description = lines[cur].strip().lower()
+ valid = True
+ for ignored in ["powerpoint", "vimeo", "xxx"]:
+ if ignored in description:
+ valid = False
+ break
+ if valid:
+ link_descriptions.append(description)
+ cur += 1
+ result["link_descriptions"] = link_descriptions
+
+ result["hyperlinks"] = extract_links(source_path + "/" + name)
+
+ images = []
+ captions = []
+ cur += 3
+ while cur + 1 < len(lines) and lines[cur] != "NOTES:":
+ name = lines[cur]
+ if "full document" not in name.lower():
+ images.append(name)
+ captions.append(lines[cur + 1])
+ cur += 2
+ result["table_image_names"] = images
+
+ result["captions"] = captions
+
+ notes = []
+ if cur < len(lines) and lines[cur] == "NOTES:":
+ cur += 1
+ while cur < len(lines):
+ notes.append(lines[cur])
+ cur += 1
+ if len(notes) > 0:
+ result["notes"] = notes
+
+ return result
+
+
+if os.path.exists(server_images_path):
+ shutil.rmtree(server_images_path)
+while os.path.exists(server_images_path):
+ pass
+os.mkdir(server_images_path)
+
+mkdir_if_absent(source_path)
+mkdir_if_absent(json_path)
+mkdir_if_absent(temp_images_path)
+
+results = []
+
+candidates = 0
+for file_name in os.listdir(source_path):
+ if file_name.endswith('.docx') or file_name.endswith(".doc"):
+ candidates += 1
+ results.append(parse_document(file_name))
+
+
+with open(f"./json/buxton_collection.json", "w", encoding="utf-8") as out:
+ json.dump(results, out, ensure_ascii=False, indent=4)
+
+print(f"\nSuccessfully parsed {candidates} candidates.")
+
+print("\nrewriting .gitignore...")
+entries = ['*', '!.gitignore']
+with open(files_path + "/.gitignore", 'w') as f:
+ f.write('\n'.join(entries))
+
+shutil.rmtree(temp_images_path)
diff --git a/src/scraping/buxton/narratives.py b/src/scraping/buxton/narratives.py
new file mode 100644
index 000000000..947d60f91
--- /dev/null
+++ b/src/scraping/buxton/narratives.py
@@ -0,0 +1,38 @@
+from docx import Document
+import tempfile
+from zipfile import ZipFile
+import shutil
+from pathlib import Path
+from os import mkdir
+
+path = "./narratives/Theme - Chord Kbds.docx"
+doc = Document(path)
+
+# IMAGE_EXT = ('png', 'jpeg', 'jpg')
+#
+# with tempfile.TemporaryDirectory() as working_dir:
+# with ZipFile(path) as working_zip:
+# image_list = [name for name in working_zip.namelist() if any(name.endswith(ext) for ext in IMAGE_EXT)]
+# working_zip.extractall(working_dir, image_list)
+# mkdir("./test")
+# for image in image_list:
+# shutil.copy(Path(working_dir).resolve() / image, "./test")
+
+paragraphs = doc.paragraphs
+for i in range(len(paragraphs)):
+ print(f"{i}: {paragraphs[i].text}")
+
+# for section in doc.sections:
+# print(section.orientation)
+
+# for shape in doc.inline_shapes:
+# print(shape._inline)
+
+# images = doc.tables[0]
+# for row in images.rows:
+# contents = []
+# for cell in row.cells:
+# contents.append(cell.text)
+ # print(contents)
+
+
diff --git a/src/scraping/buxton/narratives/Theme - Chord Kbds.docx b/src/scraping/buxton/narratives/Theme - Chord Kbds.docx
new file mode 100644
index 000000000..439a7d975
--- /dev/null
+++ b/src/scraping/buxton/narratives/Theme - Chord Kbds.docx
Binary files differ
diff --git a/src/scraping/buxton/narratives/chord_keyboards.json b/src/scraping/buxton/narratives/chord_keyboards.json
new file mode 100644
index 000000000..748578769
--- /dev/null
+++ b/src/scraping/buxton/narratives/chord_keyboards.json
@@ -0,0 +1,39 @@
+{
+ "slides": [{
+ "text": "Theme: Chord Keyboards\nFrom music to type\n\nChord keyboards require 2 or more keys to be simultaneously pushed to spawn the intended output. Playing a chord on a piano or pushing both the shift + a letter key on a typewriter to enter an upper case character are examples.",
+ "devices": ["Casio CZ-101"]
+ },
+ {
+ "text": "This is an early mechanical keyboard for taking dictation. Instead of typing alphanumeric characters as on a typewriter, pressing different combinations prints shorthand symbols on the tape, each representing a different phoneme. Speech is easier to keep up with this way, since each phoneme typically represents multiple characters.\n\nThe downside – until AI came to the rescue – was that it then took hours to manually transcribe to shorthand into conventional readable text.",
+ "devices": ["Grandjean Sténotype"]
+ },
+ {
+ "text": "Designed and manufactured in the DDR, the purpose of this keyboard is to emboss dots representing Braille symbols onto paper. The effect is to enable blind users to use their tactile sensitivity to read with their fingers.\n\nEach Braille symbol consists of two columns of 3 embossed dots each. Which 3 dots are embossed in each column is determined by which of the three keys on either side are simultaneously pressed. The key in the middle, operated by either thumb, enters a space.",
+ "devices": ["Braille Writer"]
+ },
+ {
+ "text": "This combination is derived from the work of the inventor of the mouse, Doug Engelbart\n\nWhile these are 2 distinct devices, they are not what they appear to be.\n\nFunctionally, there is a virtual 7-button chord keyboard, employing the 5 buttons on the keyset and the middle and right button of the mouse. And, using the left mouse button, there is also a 1-button mouse\n\nText was entered using a minor variant of 7-bit ASCII. The intent was to enable entering small bits of text without moving back-and-forth between mouse and QWERTY keyboard. It didn’t catch on.",
+ "devices": ["Xerox PARC 5-Button Keyset & 3-Button Mouse"]
+ },
+ {
+ "text": "",
+ "devices": []
+ },
+ {
+ "text": "",
+ "devices": []
+ },
+ {
+ "text": "",
+ "devices": []
+ },
+ {
+ "text": "",
+ "devices": []
+ },
+ {
+ "text": "",
+ "devices": []
+ }
+ ]
+} \ No newline at end of file
diff --git a/src/scraping/buxton/node_scraper.ts b/src/scraping/buxton/node_scraper.ts
deleted file mode 100644
index ef1d989d4..000000000
--- a/src/scraping/buxton/node_scraper.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-import { readdirSync } from "fs";
-import { resolve } from "path";
-
-const StreamZip = require('node-stream-zip');
-
-export async function open(path: string) {
- const zip = new StreamZip({
- file: path,
- storeEntries: true
- });
- return new Promise<string>((resolve, reject) => {
- zip.on('ready', () => {
- console.log("READY!", zip.entriesCount);
- for (const entry of Object.values(zip.entries()) as any[]) {
- const desc = entry.isDirectory ? 'directory' : `${entry.size} bytes`;
- console.log(`Entry ${entry.name}: ${desc}`);
- }
- let body = "";
- zip.stream("word/document.xml", (error: any, stream: any) => {
- if (error) {
- reject(error);
- }
- stream.on('data', (chunk: any) => body += chunk.toString());
- stream.on('end', () => {
- resolve(body);
- zip.close();
- });
- });
- });
- });
-}
-
-export async function extract(path: string) {
- const contents = await open(path);
- let body = "";
- const components = contents.toString().split('<w:t');
- for (const component of components) {
- const tags = component.split('>');
- console.log(tags[1]);
- const content = tags[1].replace(/<.*$/, "");
- body += content;
- }
- return body;
-}
-
-async function parse(): Promise<string[]> {
- const sourceDirectory = resolve(`${__dirname}/source`);
- const candidates = readdirSync(sourceDirectory).filter(file => file.endsWith(".doc") || file.endsWith(".docx")).map(file => `${sourceDirectory}/${file}`);
- await extract(candidates[0]);
- try {
- return Promise.all(candidates.map(extract));
- } catch {
- return [];
- }
-}
-
-parse(); \ No newline at end of file
diff --git a/src/scraping/buxton/scraper.py b/src/scraping/buxton/scraper.py
index ec9c3f72c..1441a8621 100644
--- a/src/scraping/buxton/scraper.py
+++ b/src/scraping/buxton/scraper.py
@@ -10,7 +10,6 @@ import uuid
import datetime
from PIL import Image
import math
-import sys
source = "./source"
filesPath = "../../server/public/files"
@@ -116,8 +115,8 @@ def write_collection(parse_results, display_fields, storage_key, viewType):
target_collection.insert_one(view_doc)
data_doc_guid = data_doc["_id"]
- print(f"inserted view document ({view_doc_guid})")
- print(f"inserted data document ({data_doc_guid})\n")
+ # print(f"inserted view document ({view_doc_guid})")
+ # print(f"inserted data document ({data_doc_guid})\n")
return view_doc_guid
@@ -189,8 +188,8 @@ def write_image(folder, name):
"y": 10,
"_width": min(800, native_width),
"zIndex": 2,
- "widthUnit": "*",
- "widthMagnitude": 1
+ "dimUnit": "*",
+ "dimMagnitude": 1
},
"__type": "Doc"
}
@@ -234,7 +233,7 @@ def parse_document(file_name: str):
result = {}
dir_path = image_dist + "/" + pure_name
- print(dir_path)
+ # print(dir_path)
mkdir_if_absent(dir_path)
raw = str(docx2txt.process(source + "/" + file_name, dir_path))
@@ -253,13 +252,15 @@ def parse_document(file_name: str):
medium = dir_path + "/" + image.replace(".", "_m.", 1)
copyfile(resolved, original)
copyfile(resolved, medium)
- print(f"extracted {count} images...")
+ # print(f"extracted {count} images...")
def sanitize(line): return re.sub("[\n\t]+", "", line).replace(u"\u00A0", " ").replace(
u"\u2013", "-").replace(u"\u201c", '''"''').replace(u"\u201d", '''"''').strip()
def sanitize_price(raw: str):
raw = raw.replace(",", "")
+ if "x" in raw.lower():
+ return None
start = raw.find("$")
if start > -1:
i = start + 1
@@ -274,6 +275,14 @@ def parse_document(file_name: str):
def remove_empty(line): return len(line) > 1
+ def try_parse(to_parse: int):
+ value: int
+ try:
+ value = int(to_parse)
+ except ValueError:
+ value = None
+ return value
+
lines = list(map(sanitize, raw.split("\n")))
lines = list(filter(remove_empty, lines))
@@ -293,13 +302,13 @@ def parse_document(file_name: str):
clean = list(
map(lambda data: data.strip().split(":"), lines[cur].split("|")))
result["company"] = clean[0][len(clean[0]) - 1].strip()
- result["year"] = clean[1][len(clean[1]) - 1].strip()
+ result["year"] = try_parse(clean[1][len(clean[1]) - 1].strip())
result["original_price"] = sanitize_price(
clean[2][len(clean[2]) - 1].strip())
cur += 1
- result["degrees_of_freedom"] = extract_value(
- lines[cur]).replace("NA", "N/A")
+ result["degrees_of_freedom"] = try_parse(extract_value(
+ lines[cur]).replace("NA", "N/A"))
cur += 1
dimensions = lines[cur].lower()
@@ -351,7 +360,7 @@ def parse_document(file_name: str):
if len(notes) > 0:
result["notes"] = listify(notes)
- print("writing child schema...")
+ # print("writing child schema...")
return {
"schema": {
@@ -383,7 +392,7 @@ def write_common_proto():
if os.path.exists(image_dist):
- shutil.rmtree(image_dist)
+ shutil.rmtree(image_dist, True)
while os.path.exists(image_dist):
pass
os.mkdir(image_dist)
@@ -393,7 +402,7 @@ common_proto_id = write_common_proto()
candidates = 0
for file_name in os.listdir(source):
- if file_name.endswith('.docx'):
+ if file_name.endswith('.docx') or file_name.endswith('.doc'):
candidates += 1
schema_guids.append(write_collection(
parse_document(file_name), ["title", "data"], "data", 5))
@@ -406,7 +415,7 @@ parent_guid = write_collection({
"__type": "Doc"
},
"child_guids": schema_guids
-}, ["title", "short_description", "original_price"], "data", 2)
+}, ["title", "short_description", "original_price"], "data", 4)
print("appending parent schema to main workspace...\n")
target_collection.update_one(
diff --git a/src/scraping/buxton/source/Bill_Notes_3_button_optical_mouse.docx b/src/scraping/buxton/source/Bill_Notes_3_button_optical_mouse.docx
deleted file mode 100644
index a2ab04b78..000000000
--- a/src/scraping/buxton/source/Bill_Notes_3_button_optical_mouse.docx
+++ /dev/null
Binary files differ
diff --git a/src/scraping/buxton/source/Bill_Notes_Amazon_Kindle_Keyboard.docx b/src/scraping/buxton/source/Bill_Notes_Amazon_Kindle_Keyboard.docx
deleted file mode 100644
index e4375ebeb..000000000
--- a/src/scraping/buxton/source/Bill_Notes_Amazon_Kindle_Keyboard.docx
+++ /dev/null
Binary files differ
diff --git a/src/scraping/buxton/source/Bill_Notes_Apple_Adj_Keyboard.docx b/src/scraping/buxton/source/Bill_Notes_Apple_Adj_Keyboard.docx
deleted file mode 100644
index 99f7ad19d..000000000
--- a/src/scraping/buxton/source/Bill_Notes_Apple_Adj_Keyboard.docx
+++ /dev/null
Binary files differ
diff --git a/src/scraping/buxton/source/Bill_Notes_Apple_Mac_Portable.docx b/src/scraping/buxton/source/Bill_Notes_Apple_Mac_Portable.docx
deleted file mode 100644
index df1aafe9c..000000000
--- a/src/scraping/buxton/source/Bill_Notes_Apple_Mac_Portable.docx
+++ /dev/null
Binary files differ
diff --git a/src/scraping/buxton/source/Bill_Notes_BAT.docx b/src/scraping/buxton/source/Bill_Notes_BAT.docx
deleted file mode 100644
index 0e3368611..000000000
--- a/src/scraping/buxton/source/Bill_Notes_BAT.docx
+++ /dev/null
Binary files differ
diff --git a/src/scraping/buxton/source/Bill_Notes_Bill_Notes_CyKey.docx b/src/scraping/buxton/source/Bill_Notes_Bill_Notes_CyKey.docx
deleted file mode 100644
index 06094b4d3..000000000
--- a/src/scraping/buxton/source/Bill_Notes_Bill_Notes_CyKey.docx
+++ /dev/null
Binary files differ
diff --git a/src/scraping/buxton/source/Bill_Notes_Braun_T3.docx b/src/scraping/buxton/source/Bill_Notes_Braun_T3.docx
deleted file mode 100644
index b00080e08..000000000
--- a/src/scraping/buxton/source/Bill_Notes_Braun_T3.docx
+++ /dev/null
Binary files differ
diff --git a/src/scraping/buxton/source/Bill_Notes_CasioC801.docx b/src/scraping/buxton/source/Bill_Notes_CasioC801.docx
deleted file mode 100644
index 510a006e0..000000000
--- a/src/scraping/buxton/source/Bill_Notes_CasioC801.docx
+++ /dev/null
Binary files differ
diff --git a/src/scraping/buxton/source/Bill_Notes_Casio_CZ-101.docx b/src/scraping/buxton/source/Bill_Notes_Casio_CZ-101.docx
deleted file mode 100644
index c8d3943c0..000000000
--- a/src/scraping/buxton/source/Bill_Notes_Casio_CZ-101.docx
+++ /dev/null
Binary files differ
diff --git a/src/scraping/buxton/source/Bill_Notes_Casio_Mini.docx b/src/scraping/buxton/source/Bill_Notes_Casio_Mini.docx
deleted file mode 100644
index cea9e7b69..000000000
--- a/src/scraping/buxton/source/Bill_Notes_Casio_Mini.docx
+++ /dev/null
Binary files differ
diff --git a/src/scraping/buxton/source/Bill_Notes_FingerWorks_Prototype.docx b/src/scraping/buxton/source/Bill_Notes_FingerWorks_Prototype.docx
deleted file mode 100644
index f53402a06..000000000
--- a/src/scraping/buxton/source/Bill_Notes_FingerWorks_Prototype.docx
+++ /dev/null
Binary files differ
diff --git a/src/scraping/buxton/source/Bill_Notes_Fingerworks_TouchStream.docx b/src/scraping/buxton/source/Bill_Notes_Fingerworks_TouchStream.docx
deleted file mode 100644
index 0eec89949..000000000
--- a/src/scraping/buxton/source/Bill_Notes_Fingerworks_TouchStream.docx
+++ /dev/null
Binary files differ
diff --git a/src/scraping/buxton/source/Bill_Notes_FrogPad.docx b/src/scraping/buxton/source/Bill_Notes_FrogPad.docx
deleted file mode 100644
index d01e1bf5c..000000000
--- a/src/scraping/buxton/source/Bill_Notes_FrogPad.docx
+++ /dev/null
Binary files differ
diff --git a/src/scraping/buxton/source/Bill_Notes_Gavilan_SC.docx b/src/scraping/buxton/source/Bill_Notes_Gavilan_SC.docx
deleted file mode 100644
index b9a30c8a9..000000000
--- a/src/scraping/buxton/source/Bill_Notes_Gavilan_SC.docx
+++ /dev/null
Binary files differ
diff --git a/src/scraping/buxton/source/Bill_Notes_Grandjean_Stenotype.docx b/src/scraping/buxton/source/Bill_Notes_Grandjean_Stenotype.docx
deleted file mode 100644
index 0615c4953..000000000
--- a/src/scraping/buxton/source/Bill_Notes_Grandjean_Stenotype.docx
+++ /dev/null
Binary files differ
diff --git a/src/scraping/buxton/source/Bill_Notes_Kindle_3G_lighted_cover.docx b/src/scraping/buxton/source/Bill_Notes_Kindle_3G_lighted_cover.docx
deleted file mode 100644
index f00fcb772..000000000
--- a/src/scraping/buxton/source/Bill_Notes_Kindle_3G_lighted_cover.docx
+++ /dev/null
Binary files differ
diff --git a/src/scraping/buxton/source/Bill_Notes_Matias.docx b/src/scraping/buxton/source/Bill_Notes_Matias.docx
deleted file mode 100644
index d2d014bbe..000000000
--- a/src/scraping/buxton/source/Bill_Notes_Matias.docx
+++ /dev/null
Binary files differ
diff --git a/src/scraping/buxton/source/Bill_Notes_Microwriter.docx b/src/scraping/buxton/source/Bill_Notes_Microwriter.docx
deleted file mode 100644
index 3ac272e42..000000000
--- a/src/scraping/buxton/source/Bill_Notes_Microwriter.docx
+++ /dev/null
Binary files differ
diff --git a/src/scraping/buxton/source/Bill_Notes_MousePen.docx b/src/scraping/buxton/source/Bill_Notes_MousePen.docx
deleted file mode 100644
index cd0b3eab3..000000000
--- a/src/scraping/buxton/source/Bill_Notes_MousePen.docx
+++ /dev/null
Binary files differ
diff --git a/src/scraping/buxton/source/Bill_Notes_NB75D.docx b/src/scraping/buxton/source/Bill_Notes_NB75D.docx
deleted file mode 100644
index a5a5e3d90..000000000
--- a/src/scraping/buxton/source/Bill_Notes_NB75D.docx
+++ /dev/null
Binary files differ
diff --git a/src/scraping/buxton/source/Bill_Notes_PARCkbd.docx b/src/scraping/buxton/source/Bill_Notes_PARCkbd.docx
deleted file mode 100644
index c0cf6ba9a..000000000
--- a/src/scraping/buxton/source/Bill_Notes_PARCkbd.docx
+++ /dev/null
Binary files differ
diff --git a/src/scraping/buxton/source/Bill_Notes_PARCtab.doc b/src/scraping/buxton/source/Bill_Notes_PARCtab.doc
deleted file mode 100644
index 3cdc2d21b..000000000
--- a/src/scraping/buxton/source/Bill_Notes_PARCtab.doc
+++ /dev/null
Binary files differ
diff --git a/src/scraping/buxton/source/Bill_Notes_Philco_Mystery_Control.docx b/src/scraping/buxton/source/Bill_Notes_Philco_Mystery_Control.docx
deleted file mode 100644
index af72fa662..000000000
--- a/src/scraping/buxton/source/Bill_Notes_Philco_Mystery_Control.docx
+++ /dev/null
Binary files differ
diff --git a/src/scraping/buxton/source/Bill_Notes_TASA_Kbd.docx b/src/scraping/buxton/source/Bill_Notes_TASA_Kbd.docx
deleted file mode 100644
index 5c2eb8d7f..000000000
--- a/src/scraping/buxton/source/Bill_Notes_TASA_Kbd.docx
+++ /dev/null
Binary files differ
diff --git a/src/scraping/buxton/source/Bill_Notes_The_Tap.docx b/src/scraping/buxton/source/Bill_Notes_The_Tap.docx
deleted file mode 100644
index c9ee2eaea..000000000
--- a/src/scraping/buxton/source/Bill_Notes_The_Tap.docx
+++ /dev/null
Binary files differ
diff --git a/src/scraping/buxton/source/Bill_Notes_Twiddler.docx b/src/scraping/buxton/source/Bill_Notes_Twiddler.docx
deleted file mode 100644
index 27b4acc85..000000000
--- a/src/scraping/buxton/source/Bill_Notes_Twiddler.docx
+++ /dev/null
Binary files differ
diff --git a/src/scraping/buxton/source/Bill_Notes_orbiTouch.doc b/src/scraping/buxton/source/Bill_Notes_orbiTouch.doc
deleted file mode 100644
index 6bd71f20e..000000000
--- a/src/scraping/buxton/source/Bill_Notes_orbiTouch.doc
+++ /dev/null
Binary files differ
diff --git a/src/server/ApiManagers/DeleteManager.ts b/src/server/ApiManagers/DeleteManager.ts
index be452c0ff..9e70af2eb 100644
--- a/src/server/ApiManagers/DeleteManager.ts
+++ b/src/server/ApiManagers/DeleteManager.ts
@@ -2,6 +2,11 @@ import ApiManager, { Registration } from "./ApiManager";
import { Method, _permission_denied, PublicHandler } from "../RouteManager";
import { WebSocket } from "../Websocket/Websocket";
import { Database } from "../database";
+import rimraf = require("rimraf");
+import { pathToDirectory, Directory } from "./UploadManager";
+import { filesDirectory } from "..";
+import { DashUploadUtils } from "../DashUploadUtils";
+import { mkdirSync } from "fs";
export default class DeleteManager extends ApiManager {
@@ -31,21 +36,19 @@ export default class DeleteManager extends ApiManager {
}
});
- const hi: PublicHandler = async ({ res, isRelease }) => {
- if (isRelease) {
- return _permission_denied(res, deletionPermissionError);
+ register({
+ method: Method.GET,
+ subscription: "/deleteAssets",
+ secureHandler: async ({ res, isRelease }) => {
+ if (isRelease) {
+ return _permission_denied(res, deletionPermissionError);
+ }
+ rimraf.sync(filesDirectory);
+ mkdirSync(filesDirectory);
+ await DashUploadUtils.buildFileDirectories();
+ res.redirect("/delete");
}
- await Database.Instance.deleteAll('users');
- res.redirect("/home");
- };
-
- // register({
- // method: Method.GET,
- // subscription: "/deleteUsers",
- // onValidation: hi,
- // onUnauthenticated: hi
- // });
-
+ });
register({
method: Method.GET,
diff --git a/src/server/ApiManagers/DownloadManager.ts b/src/server/ApiManagers/DownloadManager.ts
index 1bb84f374..01d2dfcad 100644
--- a/src/server/ApiManagers/DownloadManager.ts
+++ b/src/server/ApiManagers/DownloadManager.ts
@@ -254,11 +254,13 @@ async function writeHierarchyRecursive(file: Archiver.Archiver, hierarchy: Hiera
// and dropped in the browser and thus hosted remotely) so we upload it
// to our server and point the zip file to it, so it can bundle up the bytes
const information = await DashUploadUtils.UploadImage(result);
- path = information.serverAccessPaths[SizeSuffix.Original];
+ path = information instanceof Error ? "" : information.accessPaths[SizeSuffix.Original].server;
}
// write the file specified by the path to the directory in the
// zip file given by the prefix.
- file.file(path, { name: documentTitle, prefix });
+ if (path) {
+ file.file(path, { name: documentTitle, prefix });
+ }
} else {
// we've hit a collection, so we have to recurse
await writeHierarchyRecursive(file, result, `${prefix}/${documentTitle}`);
diff --git a/src/server/ApiManagers/GooglePhotosManager.ts b/src/server/ApiManagers/GooglePhotosManager.ts
index 107542ce2..88219423d 100644
--- a/src/server/ApiManagers/GooglePhotosManager.ts
+++ b/src/server/ApiManagers/GooglePhotosManager.ts
@@ -3,33 +3,39 @@ import { Method, _error, _success, _invalid } from "../RouteManager";
import * as path from "path";
import { GoogleApiServerUtils } from "../apis/google/GoogleApiServerUtils";
import { BatchedArray, TimeUnit } from "array-batcher";
-import { GooglePhotosUploadUtils } from "../apis/google/GooglePhotosUploadUtils";
import { Opt } from "../../new_fields/Doc";
import { DashUploadUtils, InjectSize, SizeSuffix } from "../DashUploadUtils";
import { Database } from "../database";
+import { red } from "colors";
+import { Upload } from "../SharedMediaTypes";
+import request = require('request-promise');
+import { NewMediaItemResult } from "../apis/google/SharedTypes";
+const prefix = "google_photos_";
+const remoteUploadError = "None of the preliminary uploads to Google's servers was successful.";
const authenticationError = "Unable to authenticate Google credentials before uploading to Google Photos!";
const mediaError = "Unable to convert all uploaded bytes to media items!";
-const UploadError = (count: number) => `Unable to upload ${count} images to Dash's server`;
+const localUploadError = (count: number) => `Unable to upload ${count} images to Dash's server`;
const requestError = "Unable to execute download: the body's media items were malformed.";
const downloadError = "Encountered an error while executing downloads.";
+
interface GooglePhotosUploadFailure {
batch: number;
index: number;
url: string;
reason: string;
}
+
interface MediaItem {
baseUrl: string;
- filename: string;
}
+
interface NewMediaItem {
description: string;
simpleMediaItem: {
uploadToken: string;
};
}
-const prefix = "google_photos_";
/**
* This manager handles the creation of routes for google photos functionality.
@@ -38,27 +44,47 @@ export default class GooglePhotosManager extends ApiManager {
protected initialize(register: Registration): void {
+ /**
+ * This route receives a list of urls that point to images stored
+ * on Dash's file system, and, in a two step process, uploads them to Google's servers and
+ * returns the information Google generates about the associated uploaded remote images.
+ */
register({
method: Method.POST,
- subscription: "/googlePhotosMediaUpload",
+ subscription: "/googlePhotosMediaPost",
secureHandler: async ({ user, req, res }) => {
const { media } = req.body;
+
+ // first we need to ensure that we know the google account to which these photos will be uploaded
const token = await GoogleApiServerUtils.retrieveAccessToken(user.id);
if (!token) {
return _error(res, authenticationError);
}
+
+ // next, having one large list or even synchronously looping over things trips a threshold
+ // set on Google's servers, and would instantly return an error. So, we ease things out and send the photos to upload in
+ // batches of 25, where the next batch is sent 100 millieconds after we receive a response from Google's servers.
const failed: GooglePhotosUploadFailure[] = [];
- const batched = BatchedArray.from<GooglePhotosUploadUtils.UploadSource>(media, { batchSize: 25 });
+ const batched = BatchedArray.from<Uploader.UploadSource>(media, { batchSize: 25 });
+ const interval = { magnitude: 100, unit: TimeUnit.Milliseconds };
const newMediaItems = await batched.batchedMapPatientInterval<NewMediaItem>(
- { magnitude: 100, unit: TimeUnit.Milliseconds },
- async (batch: any, collector: any, { completedBatches }: any) => {
+ interval,
+ async (batch, collector, { completedBatches }) => {
for (let index = 0; index < batch.length; index++) {
const { url, description } = batch[index];
+ // a local function used to record failure of an upload
const fail = (reason: string) => failed.push({ reason, batch: completedBatches + 1, index, url });
- const uploadToken = await GooglePhotosUploadUtils.DispatchGooglePhotosUpload(token, InjectSize(url, SizeSuffix.Original)).catch(fail);
+ // see image resizing - we store the size-agnostic url in our logic, but write out size-suffixed images to the file system
+ // so here, given a size agnostic url, we're just making that conversion so that the file system knows which bytes to actually upload
+ const imageToUpload = InjectSize(url, SizeSuffix.Original);
+ // STEP 1/2: send the raw bytes of the image from our server to Google's servers. We'll get back an upload token
+ // which acts as a pointer to those bytes that we can use to locate them later on
+ const uploadToken = await Uploader.SendBytes(token, imageToUpload).catch(fail);
if (!uploadToken) {
fail(`${path.extname(url)} is not an accepted extension`);
} else {
+ // gather the upload token return from Google (a pointer they give us to the raw, currently useless bytes
+ // we've uploaded to their servers) and put in the JSON format that the API accepts for image creation (used soon, below)
collector.push({
description,
simpleMediaItem: { uploadToken }
@@ -67,49 +93,239 @@ export default class GooglePhotosManager extends ApiManager {
}
}
);
- const failedCount = failed.length;
- if (failedCount) {
- console.error(`Unable to upload ${failedCount} image${failedCount === 1 ? "" : "s"} to Google's servers`);
+
+ // inform the developer / server console of any failed upload attempts
+ // does not abort the operation, since some subset of the uploads may have been successful
+ const { length } = failed;
+ if (length) {
+ console.error(`Unable to upload ${length} image${length === 1 ? "" : "s"} to Google's servers`);
console.log(failed.map(({ reason, batch, index, url }) => `@${batch}.${index}: ${url} failed:\n${reason}`).join('\n\n'));
}
- return GooglePhotosUploadUtils.CreateMediaItems(token, newMediaItems, req.body.album).then(
+
+ // if none of the preliminary uploads was successful, no need to try and create images
+ // report the failure to the client and return
+ if (!newMediaItems.length) {
+ console.error(red(`${remoteUploadError} Thus, aborting image creation. Please try again.`));
+ _error(res, remoteUploadError);
+ return;
+ }
+
+ // STEP 2/2: create the media items and return the API's response to the client, along with any failures
+ return Uploader.CreateMediaItems(token, newMediaItems, req.body.album).then(
results => _success(res, { results, failed }),
error => _error(res, mediaError, error)
);
}
});
+ /**
+ * This route receives a list of urls that point to images
+ * stored on Google's servers and (following a *rough* heuristic)
+ * uploads each image to Dash's server if it hasn't already been uploaded.
+ * Unfortunately, since Google has so many of these images on its servers,
+ * these user content urls expire every 6 hours. So we can't store the url of a locally uploaded
+ * Google image and compare the candidate url to it to figure out if we already have it,
+ * since the same bytes on their server might now be associated with a new, random url.
+ * So, we do the next best thing and try to use an intrinsic attribute of those bytes as
+ * an identifier: the precise content size. This works in small cases, but has the obvious flaw of failing to upload
+ * an image locally if we already have uploaded another Google user content image with the exact same content size.
+ */
register({
method: Method.POST,
- subscription: "/googlePhotosMediaDownload",
+ subscription: "/googlePhotosMediaGet",
secureHandler: async ({ req, res }) => {
- const contents: { mediaItems: MediaItem[] } = req.body;
+ const { mediaItems } = req.body as { mediaItems: MediaItem[] };
+ if (!mediaItems) {
+ // non-starter, since the input was in an invalid format
+ _invalid(res, requestError);
+ return;
+ }
let failed = 0;
- if (contents) {
- const completed: Opt<DashUploadUtils.ImageUploadInformation>[] = [];
- for (const item of contents.mediaItems) {
- const { contentSize, ...attributes } = await DashUploadUtils.InspectImage(item.baseUrl);
- const found: Opt<DashUploadUtils.ImageUploadInformation> = await Database.Auxiliary.QueryUploadHistory(contentSize!);
- if (!found) {
- const upload = await DashUploadUtils.UploadInspectedImage({ contentSize, ...attributes }, item.filename, prefix).catch(error => _error(res, downloadError, error));
- if (upload) {
- completed.push(upload);
- await Database.Auxiliary.LogUpload(upload);
- } else {
- failed++;
- }
+ const completed: Opt<Upload.ImageInformation>[] = [];
+ for (const { baseUrl } of mediaItems) {
+ // start by getting the content size of the remote image
+ const results = await DashUploadUtils.InspectImage(baseUrl);
+ if (results instanceof Error) {
+ // if something went wrong here, we can't hope to upload it, so just move on to the next
+ failed++;
+ continue;
+ }
+ const { contentSize, ...attributes } = results;
+ // check to see if we have uploaded a Google user content image *specifically via this route* already
+ // that has this exact content size
+ const found: Opt<Upload.ImageInformation> = await Database.Auxiliary.QueryUploadHistory(contentSize);
+ if (!found) {
+ // if we haven't, then upload it locally to Dash's server
+ const upload = await DashUploadUtils.UploadInspectedImage({ contentSize, ...attributes }, undefined, prefix, false).catch(error => _error(res, downloadError, error));
+ if (upload) {
+ completed.push(upload);
+ // inform the heuristic that we've encountered an image with this content size,
+ // to be later checked against in future uploads
+ await Database.Auxiliary.LogUpload(upload);
} else {
- completed.push(found);
+ // make note of a failure to upload locallys
+ failed++;
}
+ } else {
+ // if we have, the variable 'found' is handily the upload information of the
+ // existing image, so we add it to the list as if we had just uploaded it now without actually
+ // making a duplicate write
+ completed.push(found);
}
- if (failed) {
- return _error(res, UploadError(failed));
- }
- return _success(res, completed);
}
- _invalid(res, requestError);
+ // if there are any failures, report a general failure to the client
+ if (failed) {
+ return _error(res, localUploadError(failed));
+ }
+ // otherwise, return the image upload information list corresponding to the newly (or previously)
+ // uploaded images
+ _success(res, completed);
}
});
}
+}
+
+/**
+ * This namespace encompasses the logic
+ * necessary to upload images to Google's server,
+ * and then initialize / create those images in the Photos
+ * API given the upload tokens returned from the initial
+ * uploading process.
+ *
+ * https://developers.google.com/photos/library/reference/rest/v1/mediaItems/batchCreate
+ */
+export namespace Uploader {
+
+ /**
+ * Specifies the structure of the object
+ * necessary to upload bytes to Google's servers.
+ * The url is streamed to access the image's bytes,
+ * and the description is what appears in Google Photos'
+ * description field.
+ */
+ export interface UploadSource {
+ url: string;
+ description: string;
+ }
+
+ /**
+ * This is the format needed to pass
+ * into the BatchCreate API request
+ * to take a reference to raw uploaded bytes
+ * and actually create an image in Google Photos.
+ *
+ * So, to instantiate this interface you must have already dispatched an upload
+ * and received an upload token.
+ */
+ export interface NewMediaItem {
+ description: string;
+ simpleMediaItem: {
+ uploadToken: string;
+ };
+ }
+
+ /**
+ * A utility function to streamline making
+ * calls to the API's url - accentuates
+ * the relative path in the caller.
+ * @param extension the desired
+ * subset of the API
+ */
+ function prepend(extension: string): string {
+ return `https://photoslibrary.googleapis.com/v1/${extension}`;
+ }
+
+ /**
+ * Factors out the creation of the API request's
+ * authentication elements stored in the header.
+ * @param type the contents of the request
+ * @param token the user-specific Google access token
+ */
+ function headers(type: string, token: string) {
+ return {
+ 'Content-Type': `application/${type}`,
+ 'Authorization': `Bearer ${token}`,
+ };
+ }
+
+ /**
+ * This is the first step in the remote image creation process.
+ * Here we upload the raw bytes of the image to Google's servers by
+ * setting authentication and other required header properties and including
+ * the raw bytes to the image, to be uploaded, in the body of the request.
+ * @param bearerToken the user-specific Google access token, specifies the account associated
+ * with the eventual image creation
+ * @param url the url of the image to upload
+ * @param filename an optional name associated with the uploaded image - if not specified
+ * defaults to the filename (basename) in the url
+ */
+ export const SendBytes = async (bearerToken: string, url: string, filename?: string): Promise<any> => {
+ // check if the url points to a non-image or an unsupported format
+ if (!DashUploadUtils.validateExtension(url)) {
+ return undefined;
+ }
+ const body = await request(url, { encoding: null }); // returns a readable stream with the unencoded binary image data
+ const parameters = {
+ method: 'POST',
+ uri: prepend('uploads'),
+ headers: {
+ ...headers('octet-stream', bearerToken),
+ 'X-Goog-Upload-File-Name': filename || path.basename(url),
+ 'X-Goog-Upload-Protocol': 'raw'
+ },
+ body
+ };
+ return new Promise((resolve, reject) => request(parameters, (error, _response, body) => {
+ if (error) {
+ // on rejection, the server logs the error and the offending image
+ return reject(error);
+ }
+ resolve(body);
+ }));
+ };
+
+ /**
+ * This is the second step in the remote image creation process: having uploaded
+ * the raw bytes of the image and received / stored pointers (upload tokens) to those
+ * bytes, we can now instruct the API to finalize the creation of those images by
+ * submitting a batch create request with the list of upload tokens and the description
+ * to be associated with reach resulting new image.
+ * @param bearerToken the user-specific Google access token, specifies the account associated
+ * with the eventual image creation
+ * @param newMediaItems a list of objects containing a description and, effectively, the
+ * pointer to the uploaded bytes
+ * @param album if included, will add all of the newly created remote images to the album
+ * with the specified id
+ */
+ export const CreateMediaItems = async (bearerToken: string, newMediaItems: NewMediaItem[], album?: { id: string }): Promise<NewMediaItemResult[]> => {
+ // it's important to note that the API can't handle more than 50 items in each request and
+ // seems to need at least some latency between requests (spamming it synchronously has led to the server returning errors)...
+ const batched = BatchedArray.from(newMediaItems, { batchSize: 50 });
+ // ...so we execute them in delayed batches and await the entire execution
+ return batched.batchedMapPatientInterval(
+ { magnitude: 100, unit: TimeUnit.Milliseconds },
+ async (batch: NewMediaItem[], collector): Promise<void> => {
+ const parameters = {
+ method: 'POST',
+ headers: headers('json', bearerToken),
+ uri: prepend('mediaItems:batchCreate'),
+ body: { newMediaItems: batch } as any,
+ json: true
+ };
+ // register the target album, if provided
+ album && (parameters.body.albumId = album.id);
+ collector.push(...(await new Promise<NewMediaItemResult[]>((resolve, reject) => {
+ request(parameters, (error, _response, body) => {
+ if (error) {
+ reject(error);
+ } else {
+ resolve(body.newMediaItemResults);
+ }
+ });
+ })));
+ }
+ );
+ };
+
} \ No newline at end of file
diff --git a/src/server/ApiManagers/SearchManager.ts b/src/server/ApiManagers/SearchManager.ts
index 4ce12f9f3..5f7d1cf6d 100644
--- a/src/server/ApiManagers/SearchManager.ts
+++ b/src/server/ApiManagers/SearchManager.ts
@@ -4,11 +4,15 @@ import { Search } from "../Search";
const findInFiles = require('find-in-files');
import * as path from 'path';
import { pathToDirectory, Directory } from "./UploadManager";
-import { red, cyan, yellow } from "colors";
+import { red, cyan, yellow, green } from "colors";
import RouteSubscriber from "../RouteSubscriber";
-import { exec } from "child_process";
+import { exec, execSync } from "child_process";
import { onWindows } from "..";
import { get } from "request-promise";
+import { log_execution } from "../ActionUtilities";
+import { Database } from "../database";
+import rimraf = require("rimraf");
+import { mkdirSync, chmod, chmodSync } from "fs";
export class SearchManager extends ApiManager {
@@ -19,10 +23,17 @@ export class SearchManager extends ApiManager {
subscription: new RouteSubscriber("solr").add("action"),
secureHandler: async ({ req, res }) => {
const { action } = req.params;
- if (["start", "stop"].includes(action)) {
- const status = req.params.action === "start";
- const success = await SolrManager.SetRunning(status);
- console.log(success ? `Successfully ${status ? "started" : "stopped"} Solr!` : `Uh oh! Check the console for the error that occurred while ${status ? "starting" : "stopping"} Solr`);
+ switch (action) {
+ case "start":
+ case "stop":
+ const status = req.params.action === "start";
+ SolrManager.SetRunning(status);
+ break;
+ case "update":
+ await SolrManager.update();
+ break;
+ default:
+ console.log(yellow(`${action} is an unknown solr operation.`));
}
res.redirect("/home");
}
@@ -50,7 +61,7 @@ export class SearchManager extends ApiManager {
register({
method: Method.GET,
- subscription: "/search",
+ subscription: "/dashsearch",
secureHandler: async ({ req, res }) => {
const solrQuery: any = {};
["q", "fq", "start", "rows", "hl", "hl.fl"].forEach(key => solrQuery[key] = req.query[key]);
@@ -69,12 +80,10 @@ export class SearchManager extends ApiManager {
export namespace SolrManager {
- const command = onWindows ? "solr.cmd" : "solr";
-
- export async function SetRunning(status: boolean): Promise<boolean> {
+ export function SetRunning(status: boolean) {
const args = status ? "start" : "stop -p 8983";
console.log(`solr management: trying to ${args}`);
- exec(`${command} ${args}`, { cwd: "./solr-8.3.1/bin" }, (error, stdout, stderr) => {
+ exec(`solr ${args}`, { cwd: "./solr-8.3.1/bin" }, (error, stdout, stderr) => {
if (error) {
console.log(red(`solr management error: unable to ${args} server`));
console.log(red(error.message));
@@ -82,12 +91,127 @@ export namespace SolrManager {
console.log(cyan(stdout));
console.log(yellow(stderr));
});
+ if (status) {
+ console.log(cyan("Start script is executing: please allow 15 seconds for solr to start on port 8983."));
+ }
+ }
+
+ export async function update() {
+ console.log(green("Beginning update..."));
+ await log_execution<void>({
+ startMessage: "Clearing existing Solr information...",
+ endMessage: "Solr information successfully cleared",
+ action: Search.clear,
+ color: cyan
+ });
+ const cursor = await log_execution({
+ startMessage: "Connecting to and querying for all documents from database...",
+ endMessage: ({ result, error }) => {
+ const success = error === null && result !== undefined;
+ if (!success) {
+ console.log(red("Unable to connect to the database."));
+ process.exit(0);
+ }
+ return "Connection successful and query complete";
+ },
+ action: () => Database.Instance.query({}),
+ color: yellow
+ });
+ const updates: any[] = [];
+ let numDocs = 0;
+ function updateDoc(doc: any) {
+ numDocs++;
+ if ((numDocs % 50) === 0) {
+ console.log(`Batch of 50 complete, total of ${numDocs}`);
+ }
+ if (doc.__type !== "Doc") {
+ return;
+ }
+ const fields = doc.fields;
+ if (!fields) {
+ return;
+ }
+ const update: any = { id: doc._id };
+ let dynfield = false;
+ for (const key in fields) {
+ const value = fields[key];
+ const term = ToSearchTerm(value);
+ if (term !== undefined) {
+ const { suffix, value } = term;
+ update[key + suffix] = value;
+ dynfield = true;
+ }
+ }
+ if (dynfield) {
+ updates.push(update);
+ }
+ }
+ await cursor?.forEach(updateDoc);
+ const result = await log_execution({
+ startMessage: `Dispatching updates for ${updates.length} documents`,
+ endMessage: "Dispatched updates complete",
+ action: () => Search.updateDocuments(updates),
+ color: cyan
+ });
try {
- await get("http://localhost:8983");
- return true;
- } catch {
- return false;
+ if (result) {
+ const { status } = JSON.parse(result).responseHeader;
+ console.log(status ? red(`Failed with status code (${status})`) : green("Success!"));
+ } else {
+ console.log(red("Solr is likely not running!"));
+ }
+ } catch (e) {
+ console.log(red("Error:"));
+ console.log(e);
+ console.log("\n");
}
+ await cursor?.close();
+ }
+
+ const suffixMap: { [type: string]: (string | [string, string | ((json: any) => any)]) } = {
+ "number": "_n",
+ "string": "_t",
+ "boolean": "_b",
+ "image": ["_t", "url"],
+ "video": ["_t", "url"],
+ "pdf": ["_t", "url"],
+ "audio": ["_t", "url"],
+ "web": ["_t", "url"],
+ "date": ["_d", value => new Date(value.date).toISOString()],
+ "proxy": ["_i", "fieldId"],
+ "list": ["_l", list => {
+ const results = [];
+ for (const value of list.fields) {
+ const term = ToSearchTerm(value);
+ if (term) {
+ results.push(term.value);
+ }
+ }
+ return results.length ? results : null;
+ }]
+ };
+
+ function ToSearchTerm(val: any): { suffix: string, value: any } | undefined {
+ if (val === null || val === undefined) {
+ return;
+ }
+ const type = val.__type || typeof val;
+ let suffix = suffixMap[type];
+ if (!suffix) {
+ return;
+ }
+
+ if (Array.isArray(suffix)) {
+ const accessor = suffix[1];
+ if (typeof accessor === "function") {
+ val = accessor(val);
+ } else {
+ val = val[accessor];
+ }
+ suffix = suffix[0];
+ }
+
+ return { suffix, value: val };
}
} \ No newline at end of file
diff --git a/src/server/ApiManagers/SessionManager.ts b/src/server/ApiManagers/SessionManager.ts
index 8ebd684bb..c993c985f 100644
--- a/src/server/ApiManagers/SessionManager.ts
+++ b/src/server/ApiManagers/SessionManager.ts
@@ -53,6 +53,15 @@ export default class SessionManager extends ApiManager {
})
});
+ register({
+ method: Method.GET,
+ subscription: this.secureSubscriber("delete"),
+ secureHandler: this.authorizedAction(async ({ res }) => {
+ const { error } = await sessionAgent.serverWorker.emit("delete");
+ res.send(error ? error.message : "Your request was successful: the server successfully deleted the database. Return to /home.");
+ })
+ });
+
}
} \ No newline at end of file
diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts
index a92b613b7..98f029c7d 100644
--- a/src/server/ApiManagers/UploadManager.ts
+++ b/src/server/ApiManagers/UploadManager.ts
@@ -4,12 +4,12 @@ import * as formidable from 'formidable';
import v4 = require('uuid/v4');
const AdmZip = require('adm-zip');
import { extname, basename, dirname } from 'path';
-import { createReadStream, createWriteStream, unlink, readFileSync } from "fs";
+import { createReadStream, createWriteStream, unlink } from "fs";
import { publicDirectory, filesDirectory } from "..";
import { Database } from "../database";
-import { DashUploadUtils, SizeSuffix } from "../DashUploadUtils";
+import { DashUploadUtils, InjectSize, SizeSuffix } from "../DashUploadUtils";
import * as sharp from 'sharp';
-import { AcceptibleMedia } from "../SharedMediaTypes";
+import { AcceptibleMedia, Upload } from "../SharedMediaTypes";
import { normalize } from "path";
const imageDataUri = require('image-data-uri');
@@ -19,7 +19,8 @@ export enum Directory {
videos = "videos",
pdfs = "pdfs",
text = "text",
- pdf_thumbnails = "pdf_thumbnails"
+ pdf_thumbnails = "pdf_thumbnails",
+ audio = "audio"
}
export function serverPathToFile(directory: Directory, filename: string) {
@@ -47,7 +48,7 @@ export default class UploadManager extends ApiManager {
form.keepExtensions = true;
return new Promise<void>(resolve => {
form.parse(req, async (_err, _fields, files) => {
- const results: any[] = [];
+ const results: Upload.FileResponse[] = [];
for (const key in files) {
const result = await DashUploadUtils.upload(files[key]);
result && results.push(result);
@@ -60,12 +61,22 @@ export default class UploadManager extends ApiManager {
});
register({
+ method: Method.GET,
+ subscription: "/hello",
+ secureHandler: ({ req, res }) => {
+ res.send("<h1>world!</h1>");
+ }
+ });
+
+ register({
method: Method.POST,
subscription: "/uploadRemoteImage",
secureHandler: async ({ req, res }) => {
+
const { sources } = req.body;
if (Array.isArray(sources)) {
- return res.send(await Promise.all(sources.map(url => DashUploadUtils.UploadImage(url))));
+ const results = await Promise.all(sources.map(source => DashUploadUtils.UploadImage(source)));
+ return res.send(results);
}
res.send();
}
@@ -75,6 +86,7 @@ export default class UploadManager extends ApiManager {
method: Method.POST,
subscription: "/uploadDoc",
secureHandler: ({ req, res }) => {
+
const form = new formidable.IncomingForm();
form.keepExtensions = true;
// let path = req.body.path;
@@ -179,6 +191,7 @@ export default class UploadManager extends ApiManager {
method: Method.POST,
subscription: "/inspectImage",
secureHandler: async ({ req, res }) => {
+
const { source } = req.body;
if (typeof source === "string") {
return res.send(await DashUploadUtils.InspectImage(source));
@@ -197,7 +210,7 @@ export default class UploadManager extends ApiManager {
res.status(401).send("incorrect parameters specified");
return;
}
- return imageDataUri.outputFile(uri, serverPathToFile(Directory.images, filename)).then((savedName: string) => {
+ return imageDataUri.outputFile(uri, serverPathToFile(Directory.images, InjectSize(filename, SizeSuffix.Original))).then((savedName: string) => {
const ext = extname(savedName).toLowerCase();
const { pngs, jpgs } = AcceptibleMedia;
const resizers = [
@@ -222,6 +235,7 @@ export default class UploadManager extends ApiManager {
const path = serverPathToFile(Directory.images, filename + resizer.suffix + ext);
createReadStream(savedName).pipe(resizer.resizer).pipe(createWriteStream(path));
});
+
}
res.send(clientPathToFile(Directory.images, filename + ext));
});
diff --git a/src/server/ApiManagers/UserManager.ts b/src/server/ApiManagers/UserManager.ts
index b0d868918..d9d346cc1 100644
--- a/src/server/ApiManagers/UserManager.ts
+++ b/src/server/ApiManagers/UserManager.ts
@@ -34,7 +34,7 @@ export default class UserManager extends ApiManager {
register({
method: Method.GET,
subscription: "/getCurrentUser",
- secureHandler: ({ res, user }) => res.send(JSON.stringify(user)),
+ secureHandler: ({ res, user: { _id, email } }) => res.send(JSON.stringify({ id: _id, email })),
publicHandler: ({ res }) => res.send(JSON.stringify({ id: "__guest__", email: "" }))
});
diff --git a/src/server/ApiManagers/UtilManager.ts b/src/server/ApiManagers/UtilManager.ts
index 32aecd3c6..ad8119bf4 100644
--- a/src/server/ApiManagers/UtilManager.ts
+++ b/src/server/ApiManagers/UtilManager.ts
@@ -1,14 +1,14 @@
import ApiManager, { Registration } from "./ApiManager";
import { Method } from "../RouteManager";
import { exec } from 'child_process';
-import { command_line } from "../ActionUtilities";
import RouteSubscriber from "../RouteSubscriber";
import { red } from "colors";
-import { IBM_Recommender } from "../../client/apis/IBM_Recommender";
-import { Recommender } from "../Recommender";
+// import { IBM_Recommender } from "../../client/apis/IBM_Recommender";
+// import { Recommender } from "../Recommender";
-const recommender = new Recommender();
-recommender.testModel();
+// const recommender = new Recommender();
+// recommender.testModel();
+import executeImport from "../../scraping/buxton/final/BuxtonImporter";
export default class UtilManager extends ApiManager {
@@ -27,25 +27,25 @@ export default class UtilManager extends ApiManager {
}
});
- register({
- method: Method.POST,
- subscription: "/IBMAnalysis",
- secureHandler: async ({ req, res }) => res.send(await IBM_Recommender.analyze(req.body))
- });
+ // register({
+ // method: Method.POST,
+ // subscription: "/IBMAnalysis",
+ // secureHandler: async ({ req, res }) => res.send(await IBM_Recommender.analyze(req.body))
+ // });
- register({
- method: Method.POST,
- subscription: "/recommender",
- secureHandler: async ({ req, res }) => {
- const keyphrases = req.body.keyphrases;
- const wordvecs = await recommender.vectorize(keyphrases);
- let embedding: Float32Array = new Float32Array();
- if (wordvecs && wordvecs.dataSync()) {
- embedding = wordvecs.dataSync() as Float32Array;
- }
- res.send(embedding);
- }
- });
+ // register({
+ // method: Method.POST,
+ // subscription: "/recommender",
+ // secureHandler: async ({ req, res }) => {
+ // const keyphrases = req.body.keyphrases;
+ // const wordvecs = await recommender.vectorize(keyphrases);
+ // let embedding: Float32Array = new Float32Array();
+ // if (wordvecs && wordvecs.dataSync()) {
+ // embedding = wordvecs.dataSync() as Float32Array;
+ // }
+ // res.send(embedding);
+ // }
+ // });
register({
@@ -67,20 +67,6 @@ export default class UtilManager extends ApiManager {
register({
method: Method.GET,
- subscription: "/buxton",
- secureHandler: async ({ res }) => {
- const cwd = './src/scraping/buxton';
-
- const onResolved = (stdout: string) => { console.log(stdout); res.redirect("/"); };
- const onRejected = (err: any) => { console.error(err.message); res.send(err); };
- const tryPython3 = () => command_line('python3 scraper.py', cwd).then(onResolved, onRejected);
-
- return command_line('python scraper.py', cwd).then(onResolved, tryPython3);
- },
- });
-
- register({
- method: Method.GET,
subscription: "/version",
secureHandler: ({ res }) => {
return new Promise<void>(resolve => {
diff --git a/src/server/DashSession/DashSessionAgent.ts b/src/server/DashSession/DashSessionAgent.ts
index f22d1cbbc..5cbba13de 100644
--- a/src/server/DashSession/DashSessionAgent.ts
+++ b/src/server/DashSession/DashSessionAgent.ts
@@ -1,223 +1,229 @@
-// import { Email, pathFromRoot } from "../ActionUtilities";
-// import { red, yellow, green, cyan } from "colors";
-// import { get } from "request-promise";
-// import { Utils } from "../../Utils";
-// import { WebSocket } from "../Websocket/Websocket";
-// import { MessageStore } from "../Message";
-// import { launchServer, onWindows } from "..";
-// import { readdirSync, statSync, createWriteStream, readFileSync, unlinkSync } from "fs";
-// import * as Archiver from "archiver";
-// import { resolve } from "path";
-// import { AppliedSessionAgent, MessageHandler, ExitHandler, Monitor, ServerWorker } from "resilient-server-session";
-// import rimraf = require("rimraf");
-
-// /**
-// * If we're the monitor (master) thread, we should launch the monitor logic for the session.
-// * Otherwise, we must be on a worker thread that was spawned *by* the monitor (master) thread, and thus
-// * our job should be to run the server.
-// */
-// export class DashSessionAgent extends AppliedSessionAgent {
-
-// private readonly signature = "-Dash Server Session Manager";
-// private readonly releaseDesktop = pathFromRoot("../../Desktop");
-
-// /**
-// * The core method invoked when the single master thread is initialized.
-// * Installs event hooks, repl commands and additional IPC listeners.
-// */
-// protected async initializeMonitor(monitor: Monitor, sessionKey: string): Promise<void> {
-// await this.dispatchSessionPassword(sessionKey);
-// monitor.addReplCommand("pull", [], () => monitor.exec("git pull"));
-// monitor.addReplCommand("solr", [/start|stop|index/], this.executeSolrCommand);
-// monitor.addReplCommand("backup", [], this.backup);
-// monitor.addReplCommand("debug", [/\S+\@\S+/], async ([to]) => this.dispatchZippedDebugBackup(to));
-// monitor.on("backup", this.backup);
-// monitor.on("debug", async ({ to }) => this.dispatchZippedDebugBackup(to));
-// monitor.coreHooks.onCrashDetected(this.dispatchCrashReport);
-// }
-
-// /**
-// * The core method invoked when a server worker thread is initialized.
-// * Installs logic to be executed when the server worker dies.
-// */
-// protected async initializeServerWorker(): Promise<ServerWorker> {
-// const worker = ServerWorker.Create(launchServer); // server initialization delegated to worker
-// worker.addExitHandler(this.notifyClient);
-// return worker;
-// }
-
-// /**
-// * Prepares the body of the email with instructions on restoring the transmitted remote database backup locally.
-// */
-// private _remoteDebugInstructions: string | undefined;
-// private generateDebugInstructions = (zipName: string, target: string): string => {
-// if (!this._remoteDebugInstructions) {
-// this._remoteDebugInstructions = readFileSync(resolve(__dirname, "./templates/remote_debug_instructions.txt"), { encoding: "utf8" });
-// }
-// return this._remoteDebugInstructions
-// .replace(/__zipname__/, zipName)
-// .replace(/__target__/, target)
-// .replace(/__signature__/, this.signature);
-// }
-
-// /**
-// * Prepares the body of the email with information regarding a crash event.
-// */
-// private _crashInstructions: string | undefined;
-// private generateCrashInstructions({ name, message, stack }: Error): string {
-// if (!this._crashInstructions) {
-// this._crashInstructions = readFileSync(resolve(__dirname, "./templates/crash_instructions.txt"), { encoding: "utf8" });
-// }
-// return this._crashInstructions
-// .replace(/__name__/, name || "[no error name found]")
-// .replace(/__message__/, message || "[no error message found]")
-// .replace(/__stack__/, stack || "[no error stack found]")
-// .replace(/__signature__/, this.signature);
-// }
-
-// /**
-// * This sends a pseudorandomly generated guid to the configuration's recipients, allowing them alone
-// * to kill the server via the /kill/:key route.
-// */
-// private dispatchSessionPassword = async (sessionKey: string): Promise<void> => {
-// const { mainLog } = this.sessionMonitor;
-// const { notificationRecipient } = DashSessionAgent;
-// mainLog(green("dispatching session key..."));
-// const error = await Email.dispatch({
-// to: notificationRecipient,
-// subject: "Dash Release Session Admin Authentication Key",
-// content: [
-// `Here's the key for this session (started @ ${new Date().toUTCString()}):`,
-// sessionKey,
-// this.signature
-// ].join("\n\n")
-// });
-// if (error) {
-// this.sessionMonitor.mainLog(red(`dispatch failure @ ${notificationRecipient} (${yellow(error.message)})`));
-// mainLog(red("distribution of session key experienced errors"));
-// } else {
-// mainLog(green("successfully distributed session key to recipients"));
-// }
-// }
-
-// /**
-// * This sends an email with the generated crash report.
-// */
-// private dispatchCrashReport: MessageHandler<{ error: Error }> = async ({ error: crashCause }) => {
-// const { mainLog } = this.sessionMonitor;
-// const { notificationRecipient } = DashSessionAgent;
-// const error = await Email.dispatch({
-// to: notificationRecipient,
-// subject: "Dash Web Server Crash",
-// content: this.generateCrashInstructions(crashCause)
-// });
-// if (error) {
-// this.sessionMonitor.mainLog(red(`dispatch failure @ ${notificationRecipient} ${yellow(`(${error.message})`)}`));
-// mainLog(red("distribution of crash notification experienced errors"));
-// } else {
-// mainLog(green("successfully distributed crash notification to recipients"));
-// }
-// }
-
-// /**
-// * Logic for interfacing with Solr. Either starts it,
-// * stops it, or rebuilds its indicies.
-// */
-// private executeSolrCommand = async (args: string[]): Promise<void> => {
-// const { exec, mainLog } = this.sessionMonitor;
-// const action = args[0];
-// if (action === "index") {
-// exec("npx ts-node ./updateSearch.ts", { cwd: pathFromRoot("./src/server") });
-// } else {
-// const command = `${onWindows ? "solr.cmd" : "solr"} ${args[0] === "start" ? "start" : "stop -p 8983"}`;
-// await exec(command, { cwd: "./solr-8.3.1/bin" });
-// try {
-// await get("http://localhost:8983");
-// mainLog(green("successfully connected to 8983 after running solr initialization"));
-// } catch {
-// mainLog(red("unable to connect at 8983 after running solr initialization"));
-// }
-// }
-// }
-
-// /**
-// * Broadcast to all clients that their connection
-// * is no longer valid, and explain why / what to expect.
-// */
-// private notifyClient: ExitHandler = reason => {
-// const { _socket } = WebSocket;
-// if (_socket) {
-// const message = typeof reason === "boolean" ? (reason ? "exit" : "temporary") : "crash";
-// Utils.Emit(_socket, MessageStore.ConnectionTerminated, message);
-// }
-// }
-
-// /**
-// * Performs a backup of the database, saved to the desktop subdirectory.
-// * This should work as is only on our specific release server.
-// */
-// private backup = async (): Promise<void> => this.sessionMonitor.exec("backup.bat", { cwd: this.releaseDesktop });
-
-// /**
-// * Compress either a brand new backup or the most recent backup and send it
-// * as an attachment to an email, dispatched to the requested recipient.
-// * @param mode specifies whether or not to make a new backup before exporting
-// * @param to the recipient of the email
-// */
-// private async dispatchZippedDebugBackup(to: string): Promise<void> {
-// const { mainLog } = this.sessionMonitor;
-// try {
-// // if desired, complete an immediate backup to send
-// await this.backup();
-// mainLog("backup complete");
-
-// const backupsDirectory = `${this.releaseDesktop}/backups`;
-
-// // sort all backups by their modified time, and choose the most recent one
-// const target = readdirSync(backupsDirectory).map(filename => ({
-// modifiedTime: statSync(`${backupsDirectory}/${filename}`).mtimeMs,
-// filename
-// })).sort((a, b) => b.modifiedTime - a.modifiedTime)[0].filename;
-// mainLog(`targeting ${target}...`);
-
-// // create a zip file and to it, write the contents of the backup directory
-// const zipName = `${target}.zip`;
-// const zipPath = `${this.releaseDesktop}/${zipName}`;
-// const targetPath = `${backupsDirectory}/${target}`;
-// const output = createWriteStream(zipPath);
-// const zip = Archiver('zip');
-// zip.pipe(output);
-// zip.directory(`${targetPath}/Dash`, false);
-// await zip.finalize();
-// mainLog(`zip finalized with size ${statSync(zipPath).size} bytes, saved to ${zipPath}`);
-
-// // dispatch the email to the recipient, containing the finalized zip file
-// const error = await Email.dispatch({
-// to,
-// subject: `Remote debug: compressed backup of ${target}...`,
-// content: this.generateDebugInstructions(zipName, target),
-// attachments: [{ filename: zipName, path: zipPath }]
-// });
-
-// // since this is intended to be a zero-footprint operation, clean up
-// // by unlinking both the backup generated earlier in the function and the compressed zip file.
-// // to generate a persistent backup, just run backup.
-// unlinkSync(zipPath);
-// rimraf.sync(targetPath);
-
-// // indicate success or failure
-// mainLog(`${error === null ? green("successfully dispatched") : red("failed to dispatch")} ${zipName} to ${cyan(to)}`);
-// error && mainLog(red(error.message));
-// } catch (error) {
-// mainLog(red("unable to dispatch zipped backup..."));
-// mainLog(red(error.message));
-// }
-// }
-
-// }
-
-// export namespace DashSessionAgent {
-
-// export const notificationRecipient = "brownptcdash@gmail.com";
-
-// } \ No newline at end of file
+import { Email, pathFromRoot } from "../ActionUtilities";
+import { red, yellow, green, cyan } from "colors";
+import { get } from "request-promise";
+import { Utils } from "../../Utils";
+import { WebSocket } from "../Websocket/Websocket";
+import { MessageStore } from "../Message";
+import { launchServer, onWindows } from "..";
+import { readdirSync, statSync, createWriteStream, readFileSync, unlinkSync } from "fs";
+import * as Archiver from "archiver";
+import { resolve } from "path";
+import rimraf = require("rimraf");
+import { AppliedSessionAgent, ExitHandler } from "./Session/agents/applied_session_agent";
+import { ServerWorker } from "./Session/agents/server_worker";
+import { Monitor } from "./Session/agents/monitor";
+import { MessageHandler } from "./Session/agents/promisified_ipc_manager";
+
+/**
+ * If we're the monitor (master) thread, we should launch the monitor logic for the session.
+ * Otherwise, we must be on a worker thread that was spawned *by* the monitor (master) thread, and thus
+ * our job should be to run the server.
+ */
+export class DashSessionAgent extends AppliedSessionAgent {
+
+ private readonly signature = "-Dash Server Session Manager";
+ private readonly releaseDesktop = pathFromRoot("../../Desktop");
+
+ /**
+ * The core method invoked when the single master thread is initialized.
+ * Installs event hooks, repl commands and additional IPC listeners.
+ */
+ protected async initializeMonitor(monitor: Monitor): Promise<string> {
+ const sessionKey = Utils.GenerateGuid();
+ await this.dispatchSessionPassword(sessionKey);
+ monitor.addReplCommand("pull", [], () => monitor.exec("git pull"));
+ monitor.addReplCommand("solr", [/start|stop|index/], this.executeSolrCommand);
+ monitor.addReplCommand("backup", [], this.backup);
+ monitor.addReplCommand("debug", [/\S+\@\S+/], async ([to]) => this.dispatchZippedDebugBackup(to));
+ monitor.on("backup", this.backup);
+ monitor.on("debug", async ({ to }) => this.dispatchZippedDebugBackup(to));
+ monitor.on("delete", WebSocket.deleteFields);
+ monitor.coreHooks.onCrashDetected(this.dispatchCrashReport);
+ return sessionKey;
+ }
+
+ /**
+ * The core method invoked when a server worker thread is initialized.
+ * Installs logic to be executed when the server worker dies.
+ */
+ protected async initializeServerWorker(): Promise<ServerWorker> {
+ const worker = ServerWorker.Create(launchServer); // server initialization delegated to worker
+ worker.addExitHandler(this.notifyClient);
+ return worker;
+ }
+
+ /**
+ * Prepares the body of the email with instructions on restoring the transmitted remote database backup locally.
+ */
+ private _remoteDebugInstructions: string | undefined;
+ private generateDebugInstructions = (zipName: string, target: string): string => {
+ if (!this._remoteDebugInstructions) {
+ this._remoteDebugInstructions = readFileSync(resolve(__dirname, "./templates/remote_debug_instructions.txt"), { encoding: "utf8" });
+ }
+ return this._remoteDebugInstructions
+ .replace(/__zipname__/, zipName)
+ .replace(/__target__/, target)
+ .replace(/__signature__/, this.signature);
+ }
+
+ /**
+ * Prepares the body of the email with information regarding a crash event.
+ */
+ private _crashInstructions: string | undefined;
+ private generateCrashInstructions({ name, message, stack }: Error): string {
+ if (!this._crashInstructions) {
+ this._crashInstructions = readFileSync(resolve(__dirname, "./templates/crash_instructions.txt"), { encoding: "utf8" });
+ }
+ return this._crashInstructions
+ .replace(/__name__/, name || "[no error name found]")
+ .replace(/__message__/, message || "[no error message found]")
+ .replace(/__stack__/, stack || "[no error stack found]")
+ .replace(/__signature__/, this.signature);
+ }
+
+ /**
+ * This sends a pseudorandomly generated guid to the configuration's recipients, allowing them alone
+ * to kill the server via the /kill/:key route.
+ */
+ private dispatchSessionPassword = async (sessionKey: string): Promise<void> => {
+ const { mainLog } = this.sessionMonitor;
+ const { notificationRecipient } = DashSessionAgent;
+ mainLog(green("dispatching session key..."));
+ const error = await Email.dispatch({
+ to: notificationRecipient,
+ subject: "Dash Release Session Admin Authentication Key",
+ content: [
+ `Here's the key for this session (started @ ${new Date().toUTCString()}):`,
+ sessionKey,
+ this.signature
+ ].join("\n\n")
+ });
+ if (error) {
+ this.sessionMonitor.mainLog(red(`dispatch failure @ ${notificationRecipient} (${yellow(error.message)})`));
+ mainLog(red("distribution of session key experienced errors"));
+ } else {
+ mainLog(green("successfully distributed session key to recipients"));
+ }
+ }
+
+ /**
+ * This sends an email with the generated crash report.
+ */
+ private dispatchCrashReport: MessageHandler<{ error: Error }> = async ({ error: crashCause }) => {
+ const { mainLog } = this.sessionMonitor;
+ const { notificationRecipient } = DashSessionAgent;
+ const error = await Email.dispatch({
+ to: notificationRecipient,
+ subject: "Dash Web Server Crash",
+ content: this.generateCrashInstructions(crashCause)
+ });
+ if (error) {
+ this.sessionMonitor.mainLog(red(`dispatch failure @ ${notificationRecipient} ${yellow(`(${error.message})`)}`));
+ mainLog(red("distribution of crash notification experienced errors"));
+ } else {
+ mainLog(green("successfully distributed crash notification to recipients"));
+ }
+ }
+
+ /**
+ * Logic for interfacing with Solr. Either starts it,
+ * stops it, or rebuilds its indicies.
+ */
+ private executeSolrCommand = async (args: string[]): Promise<void> => {
+ const { exec, mainLog } = this.sessionMonitor;
+ const action = args[0];
+ if (action === "index") {
+ exec("npx ts-node ./updateSearch.ts", { cwd: pathFromRoot("./src/server") });
+ } else {
+ const command = `${onWindows ? "solr.cmd" : "solr"} ${args[0] === "start" ? "start" : "stop -p 8983"}`;
+ await exec(command, { cwd: "./solr-8.3.1/bin" });
+ try {
+ await get("http://localhost:8983");
+ mainLog(green("successfully connected to 8983 after running solr initialization"));
+ } catch {
+ mainLog(red("unable to connect at 8983 after running solr initialization"));
+ }
+ }
+ }
+
+ /**
+ * Broadcast to all clients that their connection
+ * is no longer valid, and explain why / what to expect.
+ */
+ private notifyClient: ExitHandler = reason => {
+ const { _socket } = WebSocket;
+ if (_socket) {
+ const message = typeof reason === "boolean" ? (reason ? "exit" : "temporary") : "crash";
+ Utils.Emit(_socket, MessageStore.ConnectionTerminated, message);
+ }
+ }
+
+ /**
+ * Performs a backup of the database, saved to the desktop subdirectory.
+ * This should work as is only on our specific release server.
+ */
+ private backup = async (): Promise<void> => this.sessionMonitor.exec("backup.bat", { cwd: this.releaseDesktop });
+
+ /**
+ * Compress either a brand new backup or the most recent backup and send it
+ * as an attachment to an email, dispatched to the requested recipient.
+ * @param mode specifies whether or not to make a new backup before exporting
+ * @param to the recipient of the email
+ */
+ private async dispatchZippedDebugBackup(to: string): Promise<void> {
+ const { mainLog } = this.sessionMonitor;
+ try {
+ // if desired, complete an immediate backup to send
+ await this.backup();
+ mainLog("backup complete");
+
+ const backupsDirectory = `${this.releaseDesktop}/backups`;
+
+ // sort all backups by their modified time, and choose the most recent one
+ const target = readdirSync(backupsDirectory).map(filename => ({
+ modifiedTime: statSync(`${backupsDirectory}/${filename}`).mtimeMs,
+ filename
+ })).sort((a, b) => b.modifiedTime - a.modifiedTime)[0].filename;
+ mainLog(`targeting ${target}...`);
+
+ // create a zip file and to it, write the contents of the backup directory
+ const zipName = `${target}.zip`;
+ const zipPath = `${this.releaseDesktop}/${zipName}`;
+ const targetPath = `${backupsDirectory}/${target}`;
+ const output = createWriteStream(zipPath);
+ const zip = Archiver('zip');
+ zip.pipe(output);
+ zip.directory(`${targetPath}/Dash`, false);
+ await zip.finalize();
+ mainLog(`zip finalized with size ${statSync(zipPath).size} bytes, saved to ${zipPath}`);
+
+ // dispatch the email to the recipient, containing the finalized zip file
+ const error = await Email.dispatch({
+ to,
+ subject: `Remote debug: compressed backup of ${target}...`,
+ content: this.generateDebugInstructions(zipName, target),
+ attachments: [{ filename: zipName, path: zipPath }]
+ });
+
+ // since this is intended to be a zero-footprint operation, clean up
+ // by unlinking both the backup generated earlier in the function and the compressed zip file.
+ // to generate a persistent backup, just run backup.
+ unlinkSync(zipPath);
+ rimraf.sync(targetPath);
+
+ // indicate success or failure
+ mainLog(`${error === null ? green("successfully dispatched") : red("failed to dispatch")} ${zipName} to ${cyan(to)}`);
+ error && mainLog(red(error.message));
+ } catch (error) {
+ mainLog(red("unable to dispatch zipped backup..."));
+ mainLog(red(error.message));
+ }
+ }
+
+}
+
+export namespace DashSessionAgent {
+
+ export const notificationRecipient = "brownptcdash@gmail.com";
+
+}
diff --git a/src/server/DashSession/Session/agents/applied_session_agent.ts b/src/server/DashSession/Session/agents/applied_session_agent.ts
new file mode 100644
index 000000000..12064668b
--- /dev/null
+++ b/src/server/DashSession/Session/agents/applied_session_agent.ts
@@ -0,0 +1,58 @@
+import { isMaster } from "cluster";
+import { Monitor } from "./monitor";
+import { ServerWorker } from "./server_worker";
+import { Utilities } from "../utilities/utilities";
+
+export type ExitHandler = (reason: Error | boolean) => void | Promise<void>;
+
+export abstract class AppliedSessionAgent {
+
+ // the following two methods allow the developer to create a custom
+ // session and use the built in customization options for each thread
+ protected abstract async initializeMonitor(monitor: Monitor): Promise<string>;
+ protected abstract async initializeServerWorker(): Promise<ServerWorker>;
+
+ private launched = false;
+
+ public killSession = (reason: string, graceful = true, errorCode = 0) => {
+ const target = isMaster ? this.sessionMonitor : this.serverWorker;
+ target.killSession(reason, graceful, errorCode);
+ }
+
+ private sessionMonitorRef: Monitor | undefined;
+ public get sessionMonitor(): Monitor {
+ if (!isMaster) {
+ this.serverWorker.emit("kill", {
+ graceful: false,
+ reason: "Cannot access the session monitor directly from the server worker thread.",
+ errorCode: 1
+ });
+ throw new Error();
+ }
+ return this.sessionMonitorRef!;
+ }
+
+ private serverWorkerRef: ServerWorker | undefined;
+ public get serverWorker(): ServerWorker {
+ if (isMaster) {
+ throw new Error("Cannot access the server worker directly from the session monitor thread");
+ }
+ return this.serverWorkerRef!;
+ }
+
+ public async launch(): Promise<void> {
+ if (!this.launched) {
+ this.launched = true;
+ if (isMaster) {
+ this.sessionMonitorRef = Monitor.Create();
+ const sessionKey = await this.initializeMonitor(this.sessionMonitorRef);
+ this.sessionMonitorRef.finalize(sessionKey);
+ } else {
+ this.serverWorkerRef = await this.initializeServerWorker();
+ }
+ } else {
+ throw new Error("Cannot launch a session thread more than once per process.");
+ }
+ }
+
+} \ No newline at end of file
diff --git a/src/server/DashSession/Session/agents/monitor.ts b/src/server/DashSession/Session/agents/monitor.ts
new file mode 100644
index 000000000..ee8afee65
--- /dev/null
+++ b/src/server/DashSession/Session/agents/monitor.ts
@@ -0,0 +1,298 @@
+import { ExitHandler } from "./applied_session_agent";
+import { Configuration, configurationSchema, defaultConfig, Identifiers, colorMapping } from "../utilities/session_config";
+import Repl, { ReplAction } from "../utilities/repl";
+import { isWorker, setupMaster, on, Worker, fork } from "cluster";
+import { manage, MessageHandler } from "./promisified_ipc_manager";
+import { red, cyan, white, yellow, blue } from "colors";
+import { exec, ExecOptions } from "child_process";
+import { validate, ValidationError } from "jsonschema";
+import { Utilities } from "../utilities/utilities";
+import { readFileSync } from "fs";
+import IPCMessageReceiver from "./process_message_router";
+import { ServerWorker } from "./server_worker";
+
+/**
+ * Validates and reads the configuration file, accordingly builds a child process factory
+ * and spawns off an initial process that will respawn as predecessors die.
+ */
+export class Monitor extends IPCMessageReceiver {
+ private static count = 0;
+ private finalized = false;
+ private exitHandlers: ExitHandler[] = [];
+ private readonly config: Configuration;
+ private activeWorker: Worker | undefined;
+ private key: string | undefined;
+ // private repl: Repl;
+
+ public static Create() {
+ if (isWorker) {
+ ServerWorker.IPCManager.emit("kill", {
+ reason: "cannot create a monitor on the worker process.",
+ graceful: false,
+ errorCode: 1
+ });
+ process.exit(1);
+ } else if (++Monitor.count > 1) {
+ console.error(red("cannot create more than one monitor."));
+ process.exit(1);
+ } else {
+ return new Monitor();
+ }
+ }
+
+ private constructor() {
+ super();
+ console.log(this.timestamp(), cyan("initializing session..."));
+ this.configureInternalHandlers();
+ this.config = this.loadAndValidateConfiguration();
+ this.initializeClusterFunctions();
+ // this.repl = this.initializeRepl();
+ }
+
+ protected configureInternalHandlers = () => {
+ // handle exceptions in the master thread - there shouldn't be many of these
+ // the IPC (inter process communication) channel closed exception can't seem
+ // to be caught in a try catch, and is inconsequential, so it is ignored
+ process.on("uncaughtException", ({ message, stack }): void => {
+ if (message !== "Channel closed") {
+ this.mainLog(red(message));
+ if (stack) {
+ this.mainLog(`uncaught exception\n${red(stack)}`);
+ }
+ }
+ });
+
+ this.on("kill", ({ reason, graceful, errorCode }) => this.killSession(reason, graceful, errorCode));
+ this.on("lifecycle", ({ event }) => console.log(this.timestamp(), `${this.config.identifiers.worker.text} lifecycle phase (${event})`));
+ }
+
+ private initializeClusterFunctions = () => {
+ // determines whether or not we see the compilation / initialization / runtime output of each child server process
+ const output = this.config.showServerOutput ? "inherit" : "ignore";
+ setupMaster({ stdio: ["ignore", output, output, "ipc"] });
+
+ // a helpful cluster event called on the master thread each time a child process exits
+ on("exit", ({ process: { pid } }, code, signal) => {
+ const prompt = `server worker with process id ${pid} has exited with code ${code}${signal === null ? "" : `, having encountered signal ${signal}`}.`;
+ this.mainLog(cyan(prompt));
+ // to make this a robust, continuous session, every time a child process dies, we immediately spawn a new one
+ this.spawn();
+ });
+ }
+
+ public finalize = (sessionKey: string): void => {
+ if (this.finalized) {
+ throw new Error("Session monitor is already finalized");
+ }
+ this.finalized = true;
+ this.key = sessionKey;
+ this.spawn();
+ }
+
+ public readonly coreHooks = Object.freeze({
+ onCrashDetected: (listener: MessageHandler<{ error: Error }>) => this.on(Monitor.IntrinsicEvents.CrashDetected, listener),
+ onServerRunning: (listener: MessageHandler<{ isFirstTime: boolean }>) => this.on(Monitor.IntrinsicEvents.ServerRunning, listener)
+ });
+
+ /**
+ * Kill this session and its active child
+ * server process, either gracefully (may wait
+ * indefinitely, but at least allows active networking
+ * requests to complete) or immediately.
+ */
+ public killSession = async (reason: string, graceful = true, errorCode = 0) => {
+ this.mainLog(cyan(`exiting session ${graceful ? "clean" : "immediate"}ly`));
+ this.mainLog(`session exit reason: ${(red(reason))}`);
+ await this.executeExitHandlers(true);
+ await this.killActiveWorker(graceful, true);
+ process.exit(errorCode);
+ }
+
+ /**
+ * Execute the list of functions registered to be called
+ * whenever the process exits.
+ */
+ public addExitHandler = (handler: ExitHandler) => this.exitHandlers.push(handler);
+
+ /**
+ * Extend the default repl by adding in custom commands
+ * that can invoke application logic external to this module
+ */
+ public addReplCommand = (basename: string, argPatterns: (RegExp | string)[], action: ReplAction) => {
+ // this.repl.registerCommand(basename, argPatterns, action);
+ }
+
+ public exec = (command: string, options?: ExecOptions) => {
+ return new Promise<void>(resolve => {
+ exec(command, { ...options, encoding: "utf8" }, (error, stdout, stderr) => {
+ if (error) {
+ this.execLog(red(`unable to execute ${white(command)}`));
+ error.message.split("\n").forEach(line => line.length && this.execLog(red(`(error) ${line}`)));
+ } else {
+ let outLines: string[], errorLines: string[];
+ if ((outLines = stdout.split("\n").filter(line => line.length)).length) {
+ outLines.forEach(line => line.length && this.execLog(cyan(`(stdout) ${line}`)));
+ }
+ if ((errorLines = stderr.split("\n").filter(line => line.length)).length) {
+ errorLines.forEach(line => line.length && this.execLog(yellow(`(stderr) ${line}`)));
+ }
+ }
+ resolve();
+ });
+ });
+ }
+
+ /**
+ * Generates a blue UTC string associated with the time
+ * of invocation.
+ */
+ private timestamp = () => blue(`[${new Date().toUTCString()}]`);
+
+ /**
+ * A formatted, identified and timestamped log in color
+ */
+ public mainLog = (...optionalParams: any[]) => {
+ console.log(this.timestamp(), this.config.identifiers.master.text, ...optionalParams);
+ }
+
+ /**
+ * A formatted, identified and timestamped log in color for non-
+ */
+ private execLog = (...optionalParams: any[]) => {
+ console.log(this.timestamp(), this.config.identifiers.exec.text, ...optionalParams);
+ }
+
+ /**
+ * Reads in configuration .json file only once, in the master thread
+ * and pass down any variables the pertinent to the child processes as environment variables.
+ */
+ private loadAndValidateConfiguration = (): Configuration => {
+ let config: Configuration | undefined;
+ try {
+ console.log(this.timestamp(), cyan("validating configuration..."));
+ config = JSON.parse(readFileSync('./session.config.json', 'utf8'));
+ const options = {
+ throwError: true,
+ allowUnknownAttributes: false
+ };
+ // ensure all necessary and no excess information is specified by the configuration file
+ validate(config, configurationSchema, options);
+ config = Utilities.preciseAssign({}, defaultConfig, config);
+ } catch (error) {
+ if (error instanceof ValidationError) {
+ console.log(red("\nSession configuration failed."));
+ console.log("The given session.config.json configuration file is invalid.");
+ console.log(`${error.instance}: ${error.stack}`);
+ process.exit(0);
+ } else if (error.code === "ENOENT" && error.path === "./session.config.json") {
+ console.log(cyan("Loading default session parameters..."));
+ console.log("Consider including a session.config.json configuration file in your project root for customization.");
+ config = Utilities.preciseAssign({}, defaultConfig);
+ } else {
+ console.log(red("\nSession configuration failed."));
+ console.log("The following unknown error occurred during configuration.");
+ console.log(error.stack);
+ process.exit(0);
+ }
+ } finally {
+ const { identifiers } = config!;
+ Object.keys(identifiers).forEach(key => {
+ const resolved = key as keyof Identifiers;
+ const { text, color } = identifiers[resolved];
+ identifiers[resolved].text = (colorMapping.get(color) || white)(`${text}:`);
+ });
+ return config!;
+ }
+ }
+
+ /**
+ * Builds the repl that allows the following commands to be typed into stdin of the master thread.
+ */
+ private initializeRepl = (): Repl => {
+ const repl = new Repl({ identifier: () => `${this.timestamp()} ${this.config.identifiers.master.text}` });
+ const boolean = /true|false/;
+ const number = /\d+/;
+ const letters = /[a-zA-Z]+/;
+ repl.registerCommand("exit", [/clean|force/], args => this.killSession("manual exit requested by repl", args[0] === "clean", 0));
+ repl.registerCommand("restart", [/clean|force/], args => this.killActiveWorker(args[0] === "clean"));
+ repl.registerCommand("set", [letters, "port", number, boolean], args => this.setPort(args[0], Number(args[2]), args[3] === "true"));
+ repl.registerCommand("set", [/polling/, number, boolean], args => {
+ const newPollingIntervalSeconds = Math.floor(Number(args[1]));
+ if (newPollingIntervalSeconds < 0) {
+ this.mainLog(red("the polling interval must be a non-negative integer"));
+ } else {
+ if (newPollingIntervalSeconds !== this.config.polling.intervalSeconds) {
+ this.config.polling.intervalSeconds = newPollingIntervalSeconds;
+ if (args[2] === "true") {
+ Monitor.IPCManager.emit("updatePollingInterval", { newPollingIntervalSeconds });
+ }
+ }
+ }
+ });
+ return repl;
+ }
+
+ private executeExitHandlers = async (reason: Error | boolean) => Promise.all(this.exitHandlers.map(handler => handler(reason)));
+
+ /**
+ * Attempts to kill the active worker gracefully, unless otherwise specified.
+ */
+ private killActiveWorker = async (graceful = true, isSessionEnd = false): Promise<void> => {
+ if (this.activeWorker && !this.activeWorker.isDead()) {
+ if (graceful) {
+ Monitor.IPCManager.emit("manualExit", { isSessionEnd });
+ } else {
+ await ServerWorker.IPCManager.destroy();
+ this.activeWorker.process.kill();
+ }
+ }
+ }
+
+ /**
+ * Allows the caller to set the port at which the target (be it the server,
+ * the websocket, some other custom port) is listening. If an immediate restart
+ * is specified, this monitor will kill the active child and re-launch the server
+ * at the port. Otherwise, the updated port won't be used until / unless the child
+ * dies on its own and triggers a restart.
+ */
+ private setPort = (port: "server" | "socket" | string, value: number, immediateRestart: boolean): void => {
+ if (value > 1023 && value < 65536) {
+ this.config.ports[port] = value;
+ if (immediateRestart) {
+ this.killActiveWorker();
+ }
+ } else {
+ this.mainLog(red(`${port} is an invalid port number`));
+ }
+ }
+
+ /**
+ * Kills the current active worker and proceeds to spawn a new worker,
+ * feeding in configuration information as environment variables.
+ */
+ private spawn = async (): Promise<void> => {
+ await this.killActiveWorker();
+ const { config: { polling, ports }, key } = this;
+ this.activeWorker = fork({
+ pollingRoute: polling.route,
+ pollingFailureTolerance: polling.failureTolerance,
+ serverPort: ports.server,
+ socketPort: ports.socket,
+ pollingIntervalSeconds: polling.intervalSeconds,
+ session_key: key
+ });
+ Monitor.IPCManager = manage(this.activeWorker.process, this.handlers);
+ this.mainLog(cyan(`spawned new server worker with process id ${this.activeWorker?.process.pid}`));
+ }
+
+}
+
+export namespace Monitor {
+
+ export enum IntrinsicEvents {
+ KeyGenerated = "key_generated",
+ CrashDetected = "crash_detected",
+ ServerRunning = "server_running"
+ }
+
+} \ No newline at end of file
diff --git a/src/server/DashSession/Session/agents/process_message_router.ts b/src/server/DashSession/Session/agents/process_message_router.ts
new file mode 100644
index 000000000..6cc8aa941
--- /dev/null
+++ b/src/server/DashSession/Session/agents/process_message_router.ts
@@ -0,0 +1,41 @@
+import { MessageHandler, PromisifiedIPCManager, HandlerMap } from "./promisified_ipc_manager";
+
+export default abstract class IPCMessageReceiver {
+
+ protected static IPCManager: PromisifiedIPCManager;
+ protected handlers: HandlerMap = {};
+
+ protected abstract configureInternalHandlers: () => void;
+
+ /**
+ * Add a listener at this message. When the monitor process
+ * receives a message, it will invoke all registered functions.
+ */
+ public on = (name: string, handler: MessageHandler) => {
+ const handlers = this.handlers[name];
+ if (!handlers) {
+ this.handlers[name] = [handler];
+ } else {
+ handlers.push(handler);
+ }
+ }
+
+ /**
+ * Unregister a given listener at this message.
+ */
+ public off = (name: string, handler: MessageHandler) => {
+ const handlers = this.handlers[name];
+ if (handlers) {
+ const index = handlers.indexOf(handler);
+ if (index > -1) {
+ handlers.splice(index, 1);
+ }
+ }
+ }
+
+ /**
+ * Unregister all listeners at this message.
+ */
+ public clearMessageListeners = (...names: string[]) => names.map(name => delete this.handlers[name]);
+
+} \ No newline at end of file
diff --git a/src/server/DashSession/Session/agents/promisified_ipc_manager.ts b/src/server/DashSession/Session/agents/promisified_ipc_manager.ts
new file mode 100644
index 000000000..feff568e1
--- /dev/null
+++ b/src/server/DashSession/Session/agents/promisified_ipc_manager.ts
@@ -0,0 +1,173 @@
+import { Utilities } from "../utilities/utilities";
+import { ChildProcess } from "child_process";
+
+/**
+ * Convenience constructor
+ * @param target the process / worker to which to attach the specialized listeners
+ */
+export function manage(target: IPCTarget, handlers?: HandlerMap) {
+ return new PromisifiedIPCManager(target, handlers);
+}
+
+/**
+ * Captures the logic to execute upon receiving a message
+ * of a certain name.
+ */
+export type HandlerMap = { [name: string]: MessageHandler[] };
+
+/**
+ * This will always literally be a child process. But, though setting
+ * up a manager in the parent will indeed see the target as the ChildProcess,
+ * setting up a manager in the child will just see itself as a regular NodeJS.Process.
+ */
+export type IPCTarget = NodeJS.Process | ChildProcess;
+
+/**
+ * Specifies a general message format for this API
+ */
+export type Message<T = any> = {
+ name: string;
+ args?: T;
+};
+export type MessageHandler<T = any> = (args: T) => (any | Promise<any>);
+
+/**
+ * When a message is emitted, it is embedded with private metadata
+ * to facilitate the resolution of promises, etc.
+ */
+interface InternalMessage extends Message { metadata: Metadata; }
+interface Metadata { isResponse: boolean; id: string; }
+type InternalMessageHandler = (message: InternalMessage) => (any | Promise<any>);
+
+/**
+ * Allows for the transmission of the error's key features over IPC.
+ */
+export interface ErrorLike {
+ name?: string;
+ message?: string;
+ stack?: string;
+}
+
+/**
+ * The arguments returned in a message sent from the target upon completion.
+ */
+export interface Response<T = any> {
+ results?: T[];
+ error?: ErrorLike;
+}
+
+const destroyEvent = "__destroy__";
+
+/**
+ * This is a wrapper utility class that allows the caller process
+ * to emit an event and return a promise that resolves when it and all
+ * other processes listening to its emission of this event have completed.
+ */
+export class PromisifiedIPCManager {
+ private readonly target: IPCTarget;
+ private pendingMessages: { [id: string]: string } = {};
+ private isDestroyed = false;
+ private get callerIsTarget() {
+ return process.pid === this.target.pid;
+ }
+
+ constructor(target: IPCTarget, handlers?: HandlerMap) {
+ this.target = target;
+ if (handlers) {
+ handlers[destroyEvent] = [this.destroyHelper];
+ this.target.addListener("message", this.generateInternalHandler(handlers));
+ }
+ }
+
+ /**
+ * This routine uniquely identifies each message, then adds a general
+ * message listener that waits for a response with the same id before resolving
+ * the promise.
+ */
+ public emit = async <T = any>(name: string, args?: any): Promise<Response<T>> => {
+ if (this.isDestroyed) {
+ const error = { name: "FailedDispatch", message: "Cannot use a destroyed IPC manager to emit a message." };
+ return { error };
+ }
+ return new Promise<Response<T>>(resolve => {
+ const messageId = Utilities.guid();
+ const responseHandler: InternalMessageHandler = ({ metadata: { id, isResponse }, args }) => {
+ if (isResponse && id === messageId) {
+ this.target.removeListener("message", responseHandler);
+ resolve(args);
+ }
+ };
+ this.target.addListener("message", responseHandler);
+ const message = { name, args, metadata: { id: messageId, isResponse: false } };
+ if (!(this.target.send && this.target.send(message))) {
+ const error: ErrorLike = { name: "FailedDispatch", message: "Either the target's send method was undefined or the act of sending failed." };
+ resolve({ error });
+ this.target.removeListener("message", responseHandler);
+ }
+ });
+ }
+
+ /**
+ * Invoked from either the parent or the child process, this allows
+ * any unresolved promises to continue in the target process, but dispatches a dummy
+ * completion response for each of the pending messages, allowing their
+ * promises in the caller to resolve.
+ */
+ public destroy = () => {
+ return new Promise<void>(async resolve => {
+ if (this.callerIsTarget) {
+ this.destroyHelper();
+ } else {
+ await this.emit(destroyEvent);
+ }
+ resolve();
+ });
+ }
+
+ /**
+ * Dispatches the dummy responses and sets the isDestroyed flag to true.
+ */
+ private destroyHelper = () => {
+ const { pendingMessages } = this;
+ this.isDestroyed = true;
+ Object.keys(pendingMessages).forEach(id => {
+ const error: ErrorLike = { name: "ManagerDestroyed", message: "The IPC manager was destroyed before the response could be returned." };
+ const message: InternalMessage = { name: pendingMessages[id], args: { error }, metadata: { id, isResponse: true } };
+ this.target.send?.(message);
+ });
+ this.pendingMessages = {};
+ }
+
+ /**
+ * This routine receives a uniquely identified message. If the message is itself a response,
+ * it is ignored to avoid infinite mutual responses. Otherwise, the routine awaits its completion using whatever
+ * router the caller has installed, and then sends a response containing the original message id,
+ * which will ultimately invoke the responseHandler of the original emission and resolve the
+ * sender's promise.
+ */
+ private generateInternalHandler = (handlers: HandlerMap): MessageHandler => async (message: InternalMessage) => {
+ const { name, args, metadata } = message;
+ if (name && metadata && !metadata.isResponse) {
+ const { id } = metadata;
+ this.pendingMessages[id] = name;
+ let error: Error | undefined;
+ let results: any[] | undefined;
+ try {
+ const registered = handlers[name];
+ if (registered) {
+ results = await Promise.all(registered.map(handler => handler(args)));
+ }
+ } catch (e) {
+ error = e;
+ }
+ if (!this.isDestroyed && this.target.send) {
+ const metadata = { id, isResponse: true };
+ const response: Response = { results , error };
+ const message = { name, args: response , metadata };
+ delete this.pendingMessages[id];
+ this.target.send(message);
+ }
+ }
+ }
+
+} \ No newline at end of file
diff --git a/src/server/DashSession/Session/agents/server_worker.ts b/src/server/DashSession/Session/agents/server_worker.ts
new file mode 100644
index 000000000..976d27226
--- /dev/null
+++ b/src/server/DashSession/Session/agents/server_worker.ts
@@ -0,0 +1,160 @@
+import { ExitHandler } from "./applied_session_agent";
+import { isMaster } from "cluster";
+import { manage } from "./promisified_ipc_manager";
+import IPCMessageReceiver from "./process_message_router";
+import { red, green, white, yellow } from "colors";
+import { get } from "request-promise";
+import { Monitor } from "./monitor";
+
+/**
+ * Effectively, each worker repairs the connection to the server by reintroducing a consistent state
+ * if its predecessor has died. It itself also polls the server heartbeat, and exits with a notification
+ * email if the server encounters an uncaught exception or if the server cannot be reached.
+ */
+export class ServerWorker extends IPCMessageReceiver {
+ private static count = 0;
+ private shouldServerBeResponsive = false;
+ private exitHandlers: ExitHandler[] = [];
+ private pollingFailureCount = 0;
+ private pollingIntervalSeconds: number;
+ private pollingFailureTolerance: number;
+ private pollTarget: string;
+ private serverPort: number;
+ private isInitialized = false;
+
+ public static Create(work: Function) {
+ if (isMaster) {
+ console.error(red("cannot create a worker on the monitor process."));
+ process.exit(1);
+ } else if (++ServerWorker.count > 1) {
+ ServerWorker.IPCManager.emit("kill", {
+ reason: "cannot create more than one worker on a given worker process.",
+ graceful: false,
+ errorCode: 1
+ });
+ process.exit(1);
+ } else {
+ return new ServerWorker(work);
+ }
+ }
+
+ /**
+ * Allows developers to invoke application specific logic
+ * by hooking into the exiting of the server process.
+ */
+ public addExitHandler = (handler: ExitHandler) => this.exitHandlers.push(handler);
+
+ /**
+ * Kill the session monitor (parent process) from this
+ * server worker (child process). This will also kill
+ * this process (child process).
+ */
+ public killSession = (reason: string, graceful = true, errorCode = 0) => this.emit<never>("kill", { reason, graceful, errorCode });
+
+ /**
+ * A convenience wrapper to tell the session monitor (parent process)
+ * to carry out the action with the specified message and arguments.
+ */
+ public emit = async <T = any>(name: string, args?: any) => ServerWorker.IPCManager.emit<T>(name, args);
+
+ private constructor(work: Function) {
+ super();
+ this.configureInternalHandlers();
+ ServerWorker.IPCManager = manage(process, this.handlers);
+ this.lifecycleNotification(green(`initializing process... ${white(`[${process.execPath} ${process.execArgv.join(" ")}]`)}`));
+
+ const { pollingRoute, serverPort, pollingIntervalSeconds, pollingFailureTolerance } = process.env;
+ this.serverPort = Number(serverPort);
+ this.pollingIntervalSeconds = Number(pollingIntervalSeconds);
+ this.pollingFailureTolerance = Number(pollingFailureTolerance);
+ this.pollTarget = `http://localhost:${serverPort}${pollingRoute}`;
+
+ work();
+ this.pollServer();
+ }
+
+ /**
+ * Set up message and uncaught exception handlers for this
+ * server process.
+ */
+ protected configureInternalHandlers = () => {
+ // updates the local values of variables to the those sent from master
+ this.on("updatePollingInterval", ({ newPollingIntervalSeconds }) => this.pollingIntervalSeconds = newPollingIntervalSeconds);
+ this.on("manualExit", async ({ isSessionEnd }) => {
+ await ServerWorker.IPCManager.destroy();
+ await this.executeExitHandlers(isSessionEnd);
+ process.exit(0);
+ });
+
+ // one reason to exit, as the process might be in an inconsistent state after such an exception
+ process.on('uncaughtException', this.proactiveUnplannedExit);
+ process.on('unhandledRejection', reason => {
+ const appropriateError = reason instanceof Error ? reason : new Error(`unhandled rejection: ${reason}`);
+ this.proactiveUnplannedExit(appropriateError);
+ });
+ }
+
+ /**
+ * Execute the list of functions registered to be called
+ * whenever the process exits.
+ */
+ private executeExitHandlers = async (reason: Error | boolean) => Promise.all(this.exitHandlers.map(handler => handler(reason)));
+
+ /**
+ * Notify master thread (which will log update in the console) of initialization via IPC.
+ */
+ public lifecycleNotification = (event: string) => this.emit("lifecycle", { event });
+
+ /**
+ * Called whenever the process has a reason to terminate, either through an uncaught exception
+ * in the process (potentially inconsistent state) or the server cannot be reached.
+ */
+ private proactiveUnplannedExit = async (error: Error): Promise<void> => {
+ this.shouldServerBeResponsive = false;
+ // communicates via IPC to the master thread that it should dispatch a crash notification email
+ this.emit(Monitor.IntrinsicEvents.CrashDetected, { error });
+ await this.executeExitHandlers(error);
+ // notify master thread (which will log update in the console) of crash event via IPC
+ this.lifecycleNotification(red(`crash event detected @ ${new Date().toUTCString()}`));
+ this.lifecycleNotification(red(error.message));
+ await ServerWorker.IPCManager.destroy();
+ process.exit(1);
+ }
+
+ /**
+ * This monitors the health of the server by submitting a get request to whatever port / route specified
+ * by the configuration every n seconds, where n is also given by the configuration.
+ */
+ private pollServer = async (): Promise<void> => {
+ await new Promise<void>(resolve => {
+ setTimeout(async () => {
+ try {
+ await get(this.pollTarget);
+ if (!this.shouldServerBeResponsive) {
+ // notify monitor thread that the server is up and running
+ this.lifecycleNotification(green(`listening on ${this.serverPort}...`));
+ this.emit(Monitor.IntrinsicEvents.ServerRunning, { isFirstTime: !this.isInitialized });
+ this.isInitialized = true;
+ }
+ this.shouldServerBeResponsive = true;
+ } catch (error) {
+ // if we expect the server to be unavailable, i.e. during compilation,
+ // the listening variable is false, activeExit will return early and the child
+ // process will continue
+ if (this.shouldServerBeResponsive) {
+ if (++this.pollingFailureCount > this.pollingFailureTolerance) {
+ this.proactiveUnplannedExit(error);
+ } else {
+ this.lifecycleNotification(yellow(`the server has encountered ${this.pollingFailureCount} of ${this.pollingFailureTolerance} tolerable failures`));
+ }
+ }
+ } finally {
+ resolve();
+ }
+ }, 1000 * this.pollingIntervalSeconds);
+ });
+ // controlled, asynchronous infinite recursion achieves a persistent poll that does not submit a new request until the previous has completed
+ this.pollServer();
+ }
+
+} \ No newline at end of file
diff --git a/src/server/DashSession/Session/utilities/repl.ts b/src/server/DashSession/Session/utilities/repl.ts
new file mode 100644
index 000000000..643141286
--- /dev/null
+++ b/src/server/DashSession/Session/utilities/repl.ts
@@ -0,0 +1,128 @@
+import { createInterface, Interface } from "readline";
+import { red, green, white } from "colors";
+
+export interface Configuration {
+ identifier: () => string | string;
+ onInvalid?: (command: string, validCommand: boolean) => string | string;
+ onValid?: (success?: string) => string | string;
+ isCaseSensitive?: boolean;
+}
+
+export type ReplAction = (parsedArgs: Array<string>) => any | Promise<any>;
+export interface Registration {
+ argPatterns: RegExp[];
+ action: ReplAction;
+}
+
+export default class Repl {
+ private identifier: () => string | string;
+ private onInvalid: ((command: string, validCommand: boolean) => string) | string;
+ private onValid: ((success: string) => string) | string;
+ private isCaseSensitive: boolean;
+ private commandMap = new Map<string, Registration[]>();
+ public interface: Interface;
+ private busy = false;
+ private keys: string | undefined;
+
+ constructor({ identifier: prompt, onInvalid, onValid, isCaseSensitive }: Configuration) {
+ this.identifier = prompt;
+ this.onInvalid = onInvalid || this.usage;
+ this.onValid = onValid || this.success;
+ this.isCaseSensitive = isCaseSensitive ?? true;
+ this.interface = createInterface(process.stdin, process.stdout).on('line', this.considerInput);
+ }
+
+ private resolvedIdentifier = () => typeof this.identifier === "string" ? this.identifier : this.identifier();
+
+ private usage = (command: string, validCommand: boolean) => {
+ if (validCommand) {
+ const formatted = white(command);
+ const patterns = green(this.commandMap.get(command)!.map(({ argPatterns }) => `${formatted} ${argPatterns.join(" ")}`).join('\n'));
+ return `${this.resolvedIdentifier()}\nthe given arguments do not match any registered patterns for ${formatted}\nthe list of valid argument patterns is given by:\n${patterns}`;
+ } else {
+ const resolved = this.keys;
+ if (resolved) {
+ return resolved;
+ }
+ const members: string[] = [];
+ const keys = this.commandMap.keys();
+ let next: IteratorResult<string>;
+ while (!(next = keys.next()).done) {
+ members.push(next.value);
+ }
+ return `${this.resolvedIdentifier()} commands: { ${members.sort().join(", ")} }`;
+ }
+ }
+
+ private success = (command: string) => `${this.resolvedIdentifier()} completed local execution of ${white(command)}`;
+
+ public registerCommand = (basename: string, argPatterns: (RegExp | string)[], action: ReplAction) => {
+ const existing = this.commandMap.get(basename);
+ const converted = argPatterns.map(input => input instanceof RegExp ? input : new RegExp(input));
+ const registration = { argPatterns: converted, action };
+ if (existing) {
+ existing.push(registration);
+ } else {
+ this.commandMap.set(basename, [registration]);
+ }
+ }
+
+ private invalid = (command: string, validCommand: boolean) => {
+ console.log(red(typeof this.onInvalid === "string" ? this.onInvalid : this.onInvalid(command, validCommand)));
+ this.busy = false;
+ }
+
+ private valid = (command: string) => {
+ console.log(green(typeof this.onValid === "string" ? this.onValid : this.onValid(command)));
+ this.busy = false;
+ }
+
+ private considerInput = async (line: string) => {
+ if (this.busy) {
+ console.log(red("Busy"));
+ return;
+ }
+ this.busy = true;
+ line = line.trim();
+ if (this.isCaseSensitive) {
+ line = line.toLowerCase();
+ }
+ const [command, ...args] = line.split(/\s+/g);
+ if (!command) {
+ return this.invalid(command, false);
+ }
+ const registered = this.commandMap.get(command);
+ if (registered) {
+ const { length } = args;
+ const candidates = registered.filter(({ argPatterns: { length: count } }) => count === length);
+ for (const { argPatterns, action } of candidates) {
+ const parsed: string[] = [];
+ let matched = true;
+ if (length) {
+ for (let i = 0; i < length; i++) {
+ let matches: RegExpExecArray | null;
+ if ((matches = argPatterns[i].exec(args[i])) === null) {
+ matched = false;
+ break;
+ }
+ parsed.push(matches[0]);
+ }
+ }
+ if (!length || matched) {
+ const result = action(parsed);
+ const resolve = () => this.valid(`${command} ${parsed.join(" ")}`);
+ if (result instanceof Promise) {
+ result.then(resolve);
+ } else {
+ resolve();
+ }
+ return;
+ }
+ }
+ this.invalid(command, true);
+ } else {
+ this.invalid(command, false);
+ }
+ }
+
+} \ No newline at end of file
diff --git a/src/server/DashSession/Session/utilities/session_config.ts b/src/server/DashSession/Session/utilities/session_config.ts
new file mode 100644
index 000000000..b0e65dde4
--- /dev/null
+++ b/src/server/DashSession/Session/utilities/session_config.ts
@@ -0,0 +1,129 @@
+import { Schema } from "jsonschema";
+import { yellow, red, cyan, green, blue, magenta, Color, grey, gray, white, black } from "colors";
+
+const colorPattern = /black|red|green|yellow|blue|magenta|cyan|white|gray|grey/;
+
+const identifierProperties: Schema = {
+ type: "object",
+ properties: {
+ text: {
+ type: "string",
+ minLength: 1
+ },
+ color: {
+ type: "string",
+ pattern: colorPattern
+ }
+ }
+};
+
+const portProperties: Schema = {
+ type: "number",
+ minimum: 1024,
+ maximum: 65535
+};
+
+export const configurationSchema: Schema = {
+ id: "/configuration",
+ type: "object",
+ properties: {
+ showServerOutput: { type: "boolean" },
+ ports: {
+ type: "object",
+ properties: {
+ server: portProperties,
+ socket: portProperties
+ },
+ required: ["server"],
+ additionalProperties: true
+ },
+ identifiers: {
+ type: "object",
+ properties: {
+ master: identifierProperties,
+ worker: identifierProperties,
+ exec: identifierProperties
+ }
+ },
+ polling: {
+ type: "object",
+ additionalProperties: false,
+ properties: {
+ intervalSeconds: {
+ type: "number",
+ minimum: 1,
+ maximum: 86400
+ },
+ route: {
+ type: "string",
+ pattern: /\/[a-zA-Z]*/g
+ },
+ failureTolerance: {
+ type: "number",
+ minimum: 0,
+ }
+ }
+ },
+ }
+};
+
+type ColorLabel = "yellow" | "red" | "cyan" | "green" | "blue" | "magenta" | "grey" | "gray" | "white" | "black";
+
+export const colorMapping: Map<ColorLabel, Color> = new Map([
+ ["yellow", yellow],
+ ["red", red],
+ ["cyan", cyan],
+ ["green", green],
+ ["blue", blue],
+ ["magenta", magenta],
+ ["grey", grey],
+ ["gray", gray],
+ ["white", white],
+ ["black", black]
+]);
+
+interface Identifier {
+ text: string;
+ color: ColorLabel;
+}
+
+export interface Identifiers {
+ master: Identifier;
+ worker: Identifier;
+ exec: Identifier;
+}
+
+export interface Configuration {
+ showServerOutput: boolean;
+ identifiers: Identifiers;
+ ports: { [description: string]: number };
+ polling: {
+ route: string;
+ intervalSeconds: number;
+ failureTolerance: number;
+ };
+}
+
+export const defaultConfig: Configuration = {
+ showServerOutput: false,
+ identifiers: {
+ master: {
+ text: "__monitor__",
+ color: "yellow"
+ },
+ worker: {
+ text: "__server__",
+ color: "magenta"
+ },
+ exec: {
+ text: "__exec__",
+ color: "green"
+ }
+ },
+ ports: { server: 3000 },
+ polling: {
+ route: "/",
+ intervalSeconds: 30,
+ failureTolerance: 0
+ }
+}; \ No newline at end of file
diff --git a/src/server/DashSession/Session/utilities/utilities.ts b/src/server/DashSession/Session/utilities/utilities.ts
new file mode 100644
index 000000000..eb8de9d7e
--- /dev/null
+++ b/src/server/DashSession/Session/utilities/utilities.ts
@@ -0,0 +1,37 @@
+import { v4 } from "uuid";
+
+export namespace Utilities {
+
+ export function guid() {
+ return v4();
+ }
+
+ /**
+ * At any arbitrary layer of nesting within the configuration objects, any single value that
+ * is not specified by the configuration is given the default counterpart. If, within an object,
+ * one peer is given by configuration and two are not, the one is preserved while the two are given
+ * the default value.
+ * @returns the composition of all of the assigned objects, much like Object.assign(), but with more
+ * granularity in the overwriting of nested objects
+ */
+ export function preciseAssign(target: any, ...sources: any[]): any {
+ for (const source of sources) {
+ preciseAssignHelper(target, source);
+ }
+ return target;
+ }
+
+ export function preciseAssignHelper(target: any, source: any) {
+ Array.from(new Set([...Object.keys(target), ...Object.keys(source)])).map(property => {
+ let targetValue: any, sourceValue: any;
+ if (sourceValue = source[property]) {
+ if (typeof sourceValue === "object" && typeof (targetValue = target[property]) === "object") {
+ preciseAssignHelper(targetValue, sourceValue);
+ } else {
+ target[property] = sourceValue;
+ }
+ }
+ });
+ }
+
+} \ No newline at end of file
diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts
index cb7104757..2af816df8 100644
--- a/src/server/DashUploadUtils.ts
+++ b/src/server/DashUploadUtils.ts
@@ -1,11 +1,11 @@
-import { unlinkSync, createWriteStream, readFileSync, rename } from 'fs';
+import { unlinkSync, createWriteStream, readFileSync, rename, writeFile } from 'fs';
import { Utils } from '../Utils';
import * as path from 'path';
import * as sharp from 'sharp';
import request = require('request-promise');
-import { ExifData, ExifImage } from 'exif';
+import { ExifImage } from 'exif';
import { Opt } from '../new_fields/Doc';
-import { AcceptibleMedia } from './SharedMediaTypes';
+import { AcceptibleMedia, Upload } from './SharedMediaTypes';
import { filesDirectory } from '.';
import { File } from 'formidable';
import { basename } from "path";
@@ -14,6 +14,7 @@ import { ParsedPDF } from "../server/PdfTypes";
const parse = require('pdf-parse');
import { Directory, serverPathToFile, clientPathToFile, pathToDirectory } from './ApiManagers/UploadManager';
import { red } from 'colors';
+import { Stream } from 'stream';
const requestImageSize = require("../client/util/request-image-size");
export enum SizeSuffix {
@@ -39,13 +40,6 @@ export namespace DashUploadUtils {
suffix: SizeSuffix;
}
- export interface ImageFileResponse {
- name: string;
- path: string;
- type: string;
- exif: Opt<DashUploadUtils.EnrichedExifData>;
- }
-
export const Sizes: { [size: string]: Size } = {
SMALL: { width: 100, suffix: SizeSuffix.Small },
MEDIUM: { width: 400, suffix: SizeSuffix.Medium },
@@ -59,56 +53,62 @@ export namespace DashUploadUtils {
const size = "content-length";
const type = "content-type";
- export interface ImageUploadInformation {
- clientAccessPath: string;
- serverAccessPaths: { [key: string]: string };
- exifData: EnrichedExifData;
- contentSize?: number;
- contentType?: string;
- }
-
- const { imageFormats, videoFormats, applicationFormats } = AcceptibleMedia;
+ const { imageFormats, videoFormats, applicationFormats, audioFormats } = AcceptibleMedia;
- export async function upload(file: File): Promise<any> {
+ export async function upload(file: File): Promise<Upload.FileResponse> {
const { type, path, name } = file;
const types = type.split("/");
const category = types[0];
- const format = `.${types[1]}`;
+ let format = `.${types[1]}`;
switch (category) {
case "image":
if (imageFormats.includes(format)) {
- const results = await UploadImage(path, basename(path), format);
- return { ...results, name, type };
+ const result = await UploadImage(path, basename(path));
+ return { source: file, result };
}
case "video":
if (videoFormats.includes(format)) {
- return MoveParsedFile(path, Directory.videos);
+ return MoveParsedFile(file, Directory.videos);
}
case "application":
if (applicationFormats.includes(format)) {
- return UploadPdf(path);
+ return UploadPdf(file);
+ }
+ case "audio":
+ const components = format.split(";");
+ if (components.length > 1) {
+ format = components[0];
+ }
+ if (audioFormats.includes(format)) {
+ return UploadAudio(file, format);
}
}
console.log(red(`Ignoring unsupported file (${name}) with upload type (${type}).`));
- return { clientAccessPath: undefined };
+ return { source: file, result: new Error(`Could not upload unsupported file (${name}) with upload type (${type}).`) };
}
- async function UploadPdf(absolutePath: string) {
- const dataBuffer = readFileSync(absolutePath);
+ async function UploadPdf(file: File) {
+ const { path: sourcePath } = file;
+ const dataBuffer = readFileSync(sourcePath);
const result: ParsedPDF = await parse(dataBuffer);
- const parsedName = basename(absolutePath);
await new Promise<void>((resolve, reject) => {
- const textFilename = `${parsedName.substring(0, parsedName.length - 4)}.txt`;
+ const name = path.basename(sourcePath);
+ const textFilename = `${name.substring(0, name.length - 4)}.txt`;
const writeStream = createWriteStream(serverPathToFile(Directory.text, textFilename));
writeStream.write(result.text, error => error ? reject(error) : resolve());
});
- return MoveParsedFile(absolutePath, Directory.pdfs);
+ return MoveParsedFile(file, Directory.pdfs);
}
- const generate = (prefix: string, extension: string) => `${prefix}upload_${Utils.GenerateGuid()}.${extension}`;
+ const manualSuffixes = [".webm"];
+
+ async function UploadAudio(file: File, format: string) {
+ const suffix = manualSuffixes.includes(format) ? format : undefined;
+ return MoveParsedFile(file, Directory.audio, suffix);
+ }
/**
* Uploads an image specified by the @param source to Dash's /public/files/
@@ -121,32 +121,20 @@ export namespace DashUploadUtils {
* @param {string} prefix is a string prepended to the generated image name in the
* event that @param filename is not specified
*
- * @returns {ImageUploadInformation} This method returns
+ * @returns {ImageUploadInformation | Error} This method returns
* 1) the paths to the uploaded images (plural due to resizing)
- * 2) the file name of each of the resized images
+ * 2) the exif data embedded in the image, or the error explaining why exif couldn't be parsed
* 3) the size of the image, in bytes (4432130)
* 4) the content type of the image, i.e. image/(jpeg | png | ...)
*/
- export const UploadImage = async (source: string, filename?: string, format?: string, prefix: string = ""): Promise<ImageUploadInformation> => {
+ export const UploadImage = async (source: string, filename?: string, prefix: string = ""): Promise<Upload.ImageInformation | Error> => {
const metadata = await InspectImage(source);
- return UploadInspectedImage(metadata, filename, format, prefix);
+ if (metadata instanceof Error) {
+ return metadata;
+ }
+ return UploadInspectedImage(metadata, filename || metadata.filename, prefix);
};
- export interface InspectionResults {
- source: string;
- requestable: string;
- exifData: EnrichedExifData;
- contentSize: number;
- contentType: string;
- nativeWidth: number;
- nativeHeight: number;
- }
-
- export interface EnrichedExifData {
- data: ExifData;
- error?: string;
- }
-
export async function buildFileDirectories() {
const pending = Object.keys(Directory).map(sub => createIfNotExists(`${filesDirectory}/${sub}`));
return Promise.all(pending);
@@ -158,13 +146,31 @@ export namespace DashUploadUtils {
type: string;
}
+ export interface ImageResizer {
+ resizer?: sharp.Sharp;
+ suffix: SizeSuffix;
+ }
+
/**
* Based on the url's classification as local or remote, gleans
* as much information as possible about the specified image
*
* @param source is the path or url to the image in question
*/
- export const InspectImage = async (source: string): Promise<InspectionResults> => {
+ export const InspectImage = async (source: string): Promise<Upload.InspectionResults | Error> => {
+ let rawMatches: RegExpExecArray | null;
+ let filename: string | undefined;
+ 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;
+ }
+ source = `http://localhost:1050${clientPathToFile(Directory.images, resolved)}`;
+ }
let resolvedUrl: string;
const matches = isLocal().exec(source);
if (matches === null) {
@@ -187,62 +193,62 @@ export namespace DashUploadUtils {
contentType: headers[type],
nativeWidth,
nativeHeight,
+ filename,
...results
};
};
- export async function MoveParsedFile(absolutePath: string, destination: Directory): Promise<{ clientAccessPath: Opt<string> }> {
- return new Promise<{ clientAccessPath: Opt<string> }>(resolve => {
- const filename = basename(absolutePath);
- const destinationPath = serverPathToFile(destination, filename);
- rename(absolutePath, destinationPath, error => {
- resolve({ clientAccessPath: error ? undefined : clientPathToFile(destination, filename) });
+ export async function MoveParsedFile(file: File, destination: Directory, suffix: string | undefined = undefined): Promise<Upload.FileResponse> {
+ const { path: sourcePath } = file;
+ let name = path.basename(sourcePath);
+ suffix && (name += suffix);
+ return new Promise(resolve => {
+ const destinationPath = serverPathToFile(destination, name);
+ rename(sourcePath, destinationPath, error => {
+ resolve({
+ source: file,
+ result: error ? error : {
+ accessPaths: {
+ agnostic: getAccessPaths(destination, name)
+ }
+
+ }
+ }
+ );
});
});
}
- export const UploadInspectedImage = async (metadata: InspectionResults, filename?: string, format?: string, prefix = ""): Promise<ImageUploadInformation> => {
+ function getAccessPaths(directory: Directory, fileName: string) {
+ return {
+ client: clientPathToFile(directory, fileName),
+ server: serverPathToFile(directory, fileName)
+ };
+ }
+
+ export const UploadInspectedImage = async (metadata: Upload.InspectionResults, filename?: string, prefix = "", cleanUp = true): Promise<Upload.ImageInformation> => {
const { requestable, source, ...remaining } = metadata;
- const extension = remaining.contentType.toLowerCase().split("/")[1]; //format || sanitizeExtension(requestable || resolved);
- const resolved = filename || generate(prefix, extension);
- const information: ImageUploadInformation = {
- clientAccessPath: clientPathToFile(Directory.images, resolved),
- serverAccessPaths: {},
- ...remaining
+ const resolved = filename || `${prefix}upload_${Utils.GenerateGuid()}.${remaining.contentType.split("/")[1].toLowerCase()}`;
+ const { images } = Directory;
+ const information: Upload.ImageInformation = {
+ accessPaths: {
+ agnostic: getAccessPaths(images, resolved)
+ },
+ ...metadata
};
- const { pngs, jpgs } = AcceptibleMedia;
- return new Promise<ImageUploadInformation>(async (resolve, reject) => {
- const resizers = [
- { resizer: sharp().rotate(), suffix: SizeSuffix.Original },
- ...Object.values(Sizes).map(size => ({
- resizer: sharp().resize(size.width, undefined, { withoutEnlargement: true }).rotate(),
- suffix: size.suffix
- }))
- ];
- if (pngs.includes(extension)) {
- resizers.forEach(element => element.resizer = element.resizer.png());
- } else if (jpgs.includes(extension)) {
- resizers.forEach(element => element.resizer = element.resizer.jpeg());
- }
- for (const { resizer, suffix } of resizers) {
- await new Promise<void>(resolve => {
- const filename = InjectSize(resolved, suffix);
- information.serverAccessPaths[suffix] = serverPathToFile(Directory.images, filename);
- request(requestable).pipe(resizer).pipe(createWriteStream(serverPathToFile(Directory.images, filename)))
- .on('close', resolve)
- .on('error', reject);
- });
- }
- if (isLocal().test(source)) {
- unlinkSync(source);
- }
- resolve(information);
- });
+ const writtenFiles = await outputResizedImages(() => request(requestable), resolved, pathToDirectory(Directory.images));
+ for (const suffix of Object.keys(writtenFiles)) {
+ information.accessPaths[suffix] = getAccessPaths(images, writtenFiles[suffix]);
+ }
+ if (isLocal().test(source) && cleanUp) {
+ unlinkSync(source);
+ }
+ return information;
};
- const parseExifData = async (source: string): Promise<EnrichedExifData> => {
+ const parseExifData = async (source: string): Promise<Upload.EnrichedExifData> => {
const image = await request.get(source, { encoding: null });
- return new Promise<EnrichedExifData>(resolve => {
+ return new Promise(resolve => {
new ExifImage({ image }, (error, data) => {
let reason: Opt<string> = undefined;
if (error) {
@@ -253,4 +259,56 @@ export namespace DashUploadUtils {
});
};
+ const { pngs, jpgs, webps, tiffs } = AcceptibleMedia;
+ const pngOptions = {
+ compressionLevel: 9,
+ adaptiveFiltering: true,
+ force: true
+ };
+
+ export async function outputResizedImages(streamProvider: () => Stream | Promise<Stream>, outputFileName: string, outputDirectory: string) {
+ const writtenFiles: { [suffix: string]: string } = {};
+ for (const { resizer, suffix } of resizers(path.extname(outputFileName))) {
+ const outputPath = path.resolve(outputDirectory, writtenFiles[suffix] = InjectSize(outputFileName, suffix));
+ await new Promise<void>(async (resolve, reject) => {
+ const source = streamProvider();
+ let readStream: Stream;
+ if (source instanceof Promise) {
+ readStream = await source;
+ } else {
+ readStream = source;
+ }
+ if (resizer) {
+ readStream = readStream.pipe(resizer.withMetadata());
+ }
+ readStream.pipe(createWriteStream(outputPath)).on("close", resolve).on("error", reject);
+ });
+ }
+ return writtenFiles;
+ }
+
+ function resizers(ext: string): DashUploadUtils.ImageResizer[] {
+ return [
+ { suffix: SizeSuffix.Original },
+ ...Object.values(DashUploadUtils.Sizes).map(({ suffix, width }) => {
+ let initial: sharp.Sharp | undefined = sharp().resize(width, undefined, { withoutEnlargement: true });
+ if (pngs.includes(ext)) {
+ initial = initial.png(pngOptions);
+ } else if (jpgs.includes(ext)) {
+ initial = initial.jpeg();
+ } else if (webps.includes(ext)) {
+ initial = initial.webp();
+ } else if (tiffs.includes(ext)) {
+ initial = initial.tiff();
+ } else if (ext === ".gif") {
+ initial = undefined;
+ }
+ return {
+ resizer: initial,
+ suffix
+ };
+ })
+ ];
+ }
+
} \ No newline at end of file
diff --git a/src/server/Message.ts b/src/server/Message.ts
index 22d2fa8a8..81f63656b 100644
--- a/src/server/Message.ts
+++ b/src/server/Message.ts
@@ -1,5 +1,8 @@
import { Utils } from "../Utils";
+import { Point } from "../pen-gestures/ndollar";
+import { Doc } from "../new_fields/Doc";
import { Image } from "canvas";
+import { AnalysisResult, ImportResults } from "../scraping/buxton/final/BuxtonImporter";
export class Message<T> {
private _name: string;
@@ -43,6 +46,35 @@ export interface Diff extends Reference {
readonly diff: any;
}
+export interface GestureContent {
+ readonly points: Array<Point>;
+ readonly bounds: { right: number, left: number, bottom: number, top: number, width: number, height: number };
+ readonly width?: string;
+ readonly color?: string;
+}
+
+export interface MobileInkOverlayContent {
+ readonly enableOverlay: boolean;
+ readonly width?: number;
+ readonly height?: number;
+ readonly text?: string;
+}
+
+export interface UpdateMobileInkOverlayPositionContent {
+ readonly dx?: number;
+ readonly dy?: number;
+ readonly dsize?: number;
+}
+
+export interface MobileDocumentUploadContent {
+ readonly docId: string;
+}
+
+export interface RoomMessage {
+ readonly message: string;
+ readonly room: string;
+}
+
export namespace MessageStore {
export const Foo = new Message<string>("Foo");
export const Bar = new Message<string>("Bar");
@@ -52,6 +84,14 @@ export namespace MessageStore {
export const GetDocument = new Message<string>("Get Document");
export const DeleteAll = new Message<any>("Delete All");
export const ConnectionTerminated = new Message<string>("Connection Terminated");
+ export const BeginBuxtonImport = new Message<string>("Begin Buxton Import");
+ export const BuxtonDocumentResult = new Message<AnalysisResult>("Buxton Document Result");
+ export const BuxtonImportComplete = new Message<ImportResults>("Buxton Import Complete");
+
+ export const GesturePoints = new Message<GestureContent>("Gesture Points");
+ export const MobileInkOverlayTrigger = new Message<MobileInkOverlayContent>("Trigger Mobile Ink Overlay");
+ export const UpdateMobileInkOverlayPosition = new Message<UpdateMobileInkOverlayPositionContent>("Update Mobile Ink Overlay Position");
+ export const MobileDocumentUpload = new Message<MobileDocumentUploadContent>("Upload Document From Mobile");
export const GetRefField = new Message<string>("Get Ref Field");
export const GetRefFields = new Message<string[]>("Get Ref Fields");
@@ -60,5 +100,4 @@ export namespace MessageStore {
export const YoutubeApiQuery = new Message<YoutubeQueryInput>("Youtube Api Query");
export const DeleteField = new Message<string>("Delete field");
export const DeleteFields = new Message<string[]>("Delete fields");
- export const AnalyzeInk = new Message<string>("Analyze Ink");
}
diff --git a/src/server/Recommender.ts b/src/server/Recommender.ts
index 8684a29f1..aacdb4053 100644
--- a/src/server/Recommender.ts
+++ b/src/server/Recommender.ts
@@ -1,137 +1,137 @@
-//import { Doc } from "../new_fields/Doc";
-//import { StrCast } from "../new_fields/Types";
-//import { List } from "../new_fields/List";
-//import { CognitiveServices } from "../client/cognitive_services/CognitiveServices";
-
-// var w2v = require('word2vec');
-var assert = require('assert');
-var arxivapi = require('arxiv-api-node');
-import requestPromise = require("request-promise");
-import * as use from '@tensorflow-models/universal-sentence-encoder';
-import { Tensor } from "@tensorflow/tfjs-core/dist/tensor";
-//require('@tensorflow/tfjs-node');
-
-//http://gnuwin32.sourceforge.net/packages/make.htm
-
-export class Recommender {
-
- private _model: any;
- static Instance: Recommender;
- private dimension: number = 0;
- private choice: string = ""; // Tensorflow or Word2Vec
-
- constructor() {
- console.log("creating recommender...");
- Recommender.Instance = this;
- }
-
- /***
- * Loads pre-trained model from TF
- */
-
- public async loadTFModel() {
- let self = this;
- return new Promise(res => {
- use.load().then(model => {
- self.choice = "TF";
- self._model = model;
- self.dimension = 512;
- res(model);
- });
- }
-
- );
- }
-
- /***
- * Loads pre-trained model from word2vec
- */
-
- // private loadModel(): Promise<any> {
- // let self = this;
- // return new Promise(res => {
- // w2v.loadModel("./node_modules/word2vec/examples/fixtures/vectors.txt", function (err: any, model: any) {
- // self.choice = "WV";
- // self._model = model;
- // self.dimension = model.size;
- // res(model);
- // });
- // });
- // }
-
- /***
- * Testing
- */
-
- public async testModel() {
- if (!this._model) {
- await this.loadTFModel();
- }
- if (this._model) {
- if (this.choice === "WV") {
- let similarity = this._model.similarity('father', 'mother');
- console.log(similarity);
- }
- else if (this.choice === "TF") {
- const model = this._model as use.UniversalSentenceEncoder;
- // Embed an array of sentences.
- const sentences = [
- 'Hello.',
- 'How are you?'
- ];
- const embeddings = await this.vectorize(sentences);
- if (embeddings) embeddings.print(true /*verbose*/);
- // model.embed(sentences).then(embeddings => {
- // // `embeddings` is a 2D tensor consisting of the 512-dimensional embeddings for each sentence.
- // // So in this example `embeddings` has the shape [2, 512].
- // embeddings.print(true /* verbose */);
- // });
- }
- }
- else {
- console.log("model not found :(");
- }
- }
-
- /***
- * Uses model to convert words to vectors
- */
-
- public async vectorize(text: string[]): Promise<Tensor | undefined> {
- if (!this._model) {
- await this.loadTFModel();
- }
- if (this._model) {
- if (this.choice === "WV") {
- let word_vecs = this._model.getVectors(text);
- return word_vecs;
- }
- else if (this.choice === "TF") {
- const model = this._model as use.UniversalSentenceEncoder;
- return new Promise<Tensor>(res => {
- model.embed(text).then(embeddings => {
- res(embeddings);
- });
- });
-
- }
- }
- }
-
- // public async trainModel() {
- // console.log("phrasing...");
- // w2v.word2vec("./node_modules/word2vec/examples/eng_news-typical_2016_1M-sentences.txt", './node_modules/word2vec/examples/my_phrases.txt', {
- // cbow: 1,
- // size: 200,
- // window: 8,
- // negative: 25,
- // hs: 0,
- // sample: 1e-4,
- // threads: 20,
- // iter: 200,
- // minCount: 2
- // });
- // console.log("phrased!!!");
- // }
-
-}
+// //import { Doc } from "../new_fields/Doc";
+// //import { StrCast } from "../new_fields/Types";
+// //import { List } from "../new_fields/List";
+// //import { CognitiveServices } from "../client/cognitive_services/CognitiveServices";
+
+// // var w2v = require('word2vec');
+// var assert = require('assert');
+// var arxivapi = require('arxiv-api-node');
+// import requestPromise = require("request-promise");
+// import * as use from '@tensorflow-models/universal-sentence-encoder';
+// import { Tensor } from "@tensorflow/tfjs-core/dist/tensor";
+// require('@tensorflow/tfjs-node');
+
+// //http://gnuwin32.sourceforge.net/packages/make.htm
+
+// export class Recommender {
+
+// private _model: any;
+// static Instance: Recommender;
+// private dimension: number = 0;
+// private choice: string = ""; // Tensorflow or Word2Vec
+
+// constructor() {
+// console.log("creating recommender...");
+// Recommender.Instance = this;
+// }
+
+// /***
+// * Loads pre-trained model from TF
+// */
+
+// public async loadTFModel() {
+// let self = this;
+// return new Promise(res => {
+// use.load().then(model => {
+// self.choice = "TF";
+// self._model = model;
+// self.dimension = 512;
+// res(model);
+// });
+// }
+
+// );
+// }
+
+// /***
+// * Loads pre-trained model from word2vec
+// */
+
+// // private loadModel(): Promise<any> {
+// // let self = this;
+// // return new Promise(res => {
+// // w2v.loadModel("./node_modules/word2vec/examples/fixtures/vectors.txt", function (err: any, model: any) {
+// // self.choice = "WV";
+// // self._model = model;
+// // self.dimension = model.size;
+// // res(model);
+// // });
+// // });
+// // }
+
+// /***
+// * Testing
+// */
+
+// public async testModel() {
+// if (!this._model) {
+// await this.loadTFModel();
+// }
+// if (this._model) {
+// if (this.choice === "WV") {
+// let similarity = this._model.similarity('father', 'mother');
+// console.log(similarity);
+// }
+// else if (this.choice === "TF") {
+// const model = this._model as use.UniversalSentenceEncoder;
+// // Embed an array of sentences.
+// const sentences = [
+// 'Hello.',
+// 'How are you?'
+// ];
+// const embeddings = await this.vectorize(sentences);
+// if (embeddings) embeddings.print(true /*verbose*/);
+// // model.embed(sentences).then(embeddings => {
+// // // `embeddings` is a 2D tensor consisting of the 512-dimensional embeddings for each sentence.
+// // // So in this example `embeddings` has the shape [2, 512].
+// // embeddings.print(true /* verbose */);
+// // });
+// }
+// }
+// else {
+// console.log("model not found :(");
+// }
+// }
+
+// /***
+// * Uses model to convert words to vectors
+// */
+
+// public async vectorize(text: string[]): Promise<Tensor | undefined> {
+// if (!this._model) {
+// await this.loadTFModel();
+// }
+// if (this._model) {
+// if (this.choice === "WV") {
+// let word_vecs = this._model.getVectors(text);
+// return word_vecs;
+// }
+// else if (this.choice === "TF") {
+// const model = this._model as use.UniversalSentenceEncoder;
+// return new Promise<Tensor>(res => {
+// model.embed(text).then(embeddings => {
+// res(embeddings);
+// });
+// });
+
+// }
+// }
+// }
+
+// // public async trainModel() {
+// // console.log("phrasing...");
+// // w2v.word2vec("./node_modules/word2vec/examples/eng_news-typical_2016_1M-sentences.txt", './node_modules/word2vec/examples/my_phrases.txt', {
+// // cbow: 1,
+// // size: 200,
+// // window: 8,
+// // negative: 25,
+// // hs: 0,
+// // sample: 1e-4,
+// // threads: 20,
+// // iter: 200,
+// // minCount: 2
+// // });
+// // console.log("phrased!!!");
+// // }
+
+// }
diff --git a/src/server/RouteManager.ts b/src/server/RouteManager.ts
index 63e957cd1..a8680c0c9 100644
--- a/src/server/RouteManager.ts
+++ b/src/server/RouteManager.ts
@@ -2,6 +2,7 @@ import RouteSubscriber from "./RouteSubscriber";
import { DashUserModel } from "./authentication/models/user_model";
import { Request, Response, Express } from 'express';
import { cyan, red, green } from 'colors';
+import { Utils } from "../client/northstar/utils/Utils";
export enum Method {
GET,
@@ -78,6 +79,7 @@ export default class RouteManager {
}
}
+ static routes: string[] = [];
/**
*
* @param initializer
@@ -85,6 +87,12 @@ export default class RouteManager {
addSupervisedRoute = (initializer: RouteInitializer): void => {
const { method, subscription, secureHandler, publicHandler, errorHandler } = initializer;
+ typeof (initializer.subscription) === "string" && RouteManager.routes.push(initializer.subscription);
+ initializer.subscription instanceof RouteSubscriber && RouteManager.routes.push(initializer.subscription.root);
+ initializer.subscription instanceof Array && initializer.subscription.map(sub => {
+ typeof (sub) === "string" && RouteManager.routes.push(sub);
+ sub instanceof RouteSubscriber && RouteManager.routes.push(sub.root);
+ });
const isRelease = this._isRelease;
const supervised = async (req: Request, res: Response) => {
let { user } = req;
@@ -133,7 +141,7 @@ export default class RouteManager {
} else {
route = subscriber.build;
}
- if (!/^\/$|^\/[A-Za-z]+(\/\:[A-Za-z?_]+)*$/g.test(route)) {
+ if (!/^\/$|^\/[A-Za-z\*]+(\/\:[A-Za-z?_\*]+)*$/g.test(route)) {
this.failedRegistrations.push({
reason: RegistrationError.Malformed,
route
diff --git a/src/server/SharedMediaTypes.ts b/src/server/SharedMediaTypes.ts
index 8d0f441f0..2495123b7 100644
--- a/src/server/SharedMediaTypes.ts
+++ b/src/server/SharedMediaTypes.ts
@@ -1,8 +1,50 @@
+import { ExifData } from 'exif';
+import { File } from 'formidable';
+
export namespace AcceptibleMedia {
export const gifs = [".gif"];
export const pngs = [".png"];
export const jpgs = [".jpg", ".jpeg"];
- export const imageFormats = [...pngs, ...jpgs, ...gifs];
+ export const webps = [".webp"];
+ export const tiffs = [".tiff"];
+ export const imageFormats = [...pngs, ...jpgs, ...gifs, ...webps, ...tiffs];
export const videoFormats = [".mov", ".mp4"];
export const applicationFormats = [".pdf"];
+ export const audioFormats = [".wav", ".mp3", ".flac", ".au", ".aiff", ".m4a", ".webm"];
+}
+
+export namespace Upload {
+
+ export function isImageInformation(uploadResponse: Upload.FileInformation): uploadResponse is Upload.ImageInformation {
+ return "nativeWidth" in uploadResponse;
+ }
+
+ export interface FileInformation {
+ accessPaths: AccessPathInfo;
+ }
+
+ export type FileResponse<T extends FileInformation = FileInformation> = { source: File, result: T | Error };
+
+ export type ImageInformation = FileInformation & InspectionResults;
+
+ export interface AccessPathInfo {
+ [suffix: string]: { client: string, server: string };
+ }
+
+ export interface InspectionResults {
+ source: string;
+ requestable: string;
+ exifData: EnrichedExifData;
+ contentSize: number;
+ contentType: string;
+ nativeWidth: number;
+ nativeHeight: number;
+ filename?: string;
+ }
+
+ export interface EnrichedExifData {
+ data: ExifData;
+ error?: string;
+ }
+
} \ No newline at end of file
diff --git a/src/server/Websocket/Websocket.ts b/src/server/Websocket/Websocket.ts
index f485e1dcd..9f9fc9619 100644
--- a/src/server/Websocket/Websocket.ts
+++ b/src/server/Websocket/Websocket.ts
@@ -1,5 +1,5 @@
import { Utils } from "../../Utils";
-import { MessageStore, Transferable, Types, Diff, YoutubeQueryInput, YoutubeQueryTypes } from "../Message";
+import { MessageStore, Transferable, Types, Diff, YoutubeQueryInput, YoutubeQueryTypes, GestureContent, MobileInkOverlayContent, UpdateMobileInkOverlayPositionContent, MobileDocumentUploadContent, RoomMessage } from "../Message";
import { Client } from "../Client";
import { Socket } from "socket.io";
import { Database } from "../database";
@@ -10,16 +10,9 @@ import { GoogleCredentialsLoader } from "../credentials/CredentialsLoader";
import { logPort } from "../ActionUtilities";
import { timeMap } from "../ApiManagers/UserManager";
import { green } from "colors";
-import { Image } from "canvas";
-import { write, createWriteStream } from "fs";
import { serverPathToFile, Directory } from "../ApiManagers/UploadManager";
-const tesseract = require("node-tesseract-ocr");
-const config = {
- lang: "eng",
- oem: 1,
- psm: 8
-};
-const imageDataUri = require('image-data-uri');
+import { networkInterfaces } from "os";
+import executeImport from "../../scraping/buxton/final/BuxtonImporter";
export namespace WebSocket {
@@ -28,6 +21,7 @@ export namespace WebSocket {
export const socketMap = new Map<SocketIO.Socket, string>();
export let disconnect: Function;
+
export async function start(isRelease: boolean) {
await preliminaryFunctions();
initialize(isRelease);
@@ -35,7 +29,6 @@ export namespace WebSocket {
async function preliminaryFunctions() {
}
-
function initialize(isRelease: boolean) {
const endpoint = io();
endpoint.on("connection", function (socket: Socket) {
@@ -49,6 +42,54 @@ export namespace WebSocket {
next();
});
+ // convenience function to log server messages on the client
+ function log(message?: any, ...optionalParams: any[]) {
+ socket.emit('log', ['Message from server:', message, ...optionalParams]);
+ }
+
+ socket.on('message', function (message, room) {
+ console.log('Client said: ', message);
+ socket.in(room).emit('message', message);
+ });
+
+ socket.on('create or join', function (room) {
+ console.log('Received request to create or join room ' + room);
+
+ const clientsInRoom = socket.adapter.rooms[room];
+ const numClients = clientsInRoom ? Object.keys(clientsInRoom.sockets).length : 0;
+ console.log('Room ' + room + ' now has ' + numClients + ' client(s)');
+
+ if (numClients === 0) {
+ socket.join(room);
+ console.log('Client ID ' + socket.id + ' created room ' + room);
+ socket.emit('created', room, socket.id);
+
+ } else if (numClients === 1) {
+ console.log('Client ID ' + socket.id + ' joined room ' + room);
+ socket.in(room).emit('join', room);
+ socket.join(room);
+ socket.emit('joined', room, socket.id);
+ socket.in(room).emit('ready');
+ } else { // max two clients
+ socket.emit('full', room);
+ }
+ });
+
+ socket.on('ipaddr', function () {
+ const ifaces = networkInterfaces();
+ for (const dev in ifaces) {
+ ifaces[dev].forEach(function (details) {
+ if (details.family === 'IPv4' && details.address !== '127.0.0.1') {
+ socket.emit('ipaddr', details.address);
+ }
+ });
+ }
+ });
+
+ socket.on('bye', function () {
+ console.log('received bye');
+ });
+
Utils.Emit(socket, MessageStore.Foo, "handshooken");
Utils.AddServerHandler(socket, MessageStore.Bar, guid => barReceived(socket, guid));
@@ -61,12 +102,21 @@ export namespace WebSocket {
Utils.AddServerHandler(socket, MessageStore.CreateField, CreateField);
Utils.AddServerHandlerCallback(socket, MessageStore.YoutubeApiQuery, HandleYoutubeQuery);
- Utils.AddServerHandlerCallback(socket, MessageStore.AnalyzeInk, RecognizeImage);
Utils.AddServerHandler(socket, MessageStore.UpdateField, diff => UpdateField(socket, diff));
Utils.AddServerHandler(socket, MessageStore.DeleteField, id => DeleteField(socket, id));
Utils.AddServerHandler(socket, MessageStore.DeleteFields, ids => DeleteFields(socket, ids));
+ Utils.AddServerHandler(socket, MessageStore.GesturePoints, content => processGesturePoints(socket, content));
+ Utils.AddServerHandler(socket, MessageStore.MobileInkOverlayTrigger, content => processOverlayTrigger(socket, content));
+ Utils.AddServerHandler(socket, MessageStore.UpdateMobileInkOverlayPosition, content => processUpdateOverlayPosition(socket, content));
+ Utils.AddServerHandler(socket, MessageStore.MobileDocumentUpload, content => processMobileDocumentUpload(socket, content));
Utils.AddServerHandlerCallback(socket, MessageStore.GetRefField, GetRefField);
Utils.AddServerHandlerCallback(socket, MessageStore.GetRefFields, GetRefFields);
+ Utils.AddServerHandler(socket, MessageStore.BeginBuxtonImport, () => {
+ executeImport(
+ deviceOrError => Utils.Emit(socket, MessageStore.BuxtonDocumentResult, deviceOrError),
+ results => Utils.Emit(socket, MessageStore.BuxtonImportComplete, results)
+ );
+ });
disconnect = () => {
socket.broadcast.emit("connection_terminated", Date.now());
@@ -79,15 +129,20 @@ export namespace WebSocket {
logPort("websocket", socketPort);
}
- async function RecognizeImage([query, callback]: [string, (result: any) => any]) {
- const path = serverPathToFile(Directory.images, "handwriting.jpg");
- imageDataUri.outputFile(query, path).then((savedName: string) => {
- console.log("saved " + savedName);
- const remadePath = path.split("\\").join("\\\\");
- tesseract.recognize(remadePath, config)
- .then(callback)
- .catch(console.log);
- });
+ function processGesturePoints(socket: Socket, content: GestureContent) {
+ socket.broadcast.emit("receiveGesturePoints", content);
+ }
+
+ function processOverlayTrigger(socket: Socket, content: MobileInkOverlayContent) {
+ socket.broadcast.emit("receiveOverlayTrigger", content);
+ }
+
+ function processUpdateOverlayPosition(socket: Socket, content: UpdateMobileInkOverlayPositionContent) {
+ socket.broadcast.emit("receiveUpdateOverlayPosition", content);
+ }
+
+ function processMobileDocumentUpload(socket: Socket, content: MobileDocumentUploadContent) {
+ socket.broadcast.emit("receiveMobileDocumentUpload", content);
}
function HandleYoutubeQuery([query, callback]: [YoutubeQueryInput, (result?: any[]) => void]) {
diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts
index 329107a71..0f75833ee 100644
--- a/src/server/apis/google/GoogleApiServerUtils.ts
+++ b/src/server/apis/google/GoogleApiServerUtils.ts
@@ -318,13 +318,14 @@ export namespace GoogleApiServerUtils {
*/
async function retrieveCredentials(userId: string): Promise<{ credentials: Opt<Credentials>, refreshed: boolean }> {
let credentials: Opt<Credentials> = await Database.Auxiliary.GoogleAuthenticationToken.Fetch(userId);
- const refreshed = false;
+ let refreshed = false;
if (!credentials) {
return { credentials: undefined, refreshed };
}
// check for token expiry
if (credentials.expiry_date! <= new Date().getTime()) {
credentials = await refreshAccessToken(credentials, userId);
+ refreshed = true;
}
return { credentials, refreshed };
}
diff --git a/src/server/apis/google/GooglePhotosUploadUtils.ts b/src/server/apis/google/GooglePhotosUploadUtils.ts
deleted file mode 100644
index 8ae63caa3..000000000
--- a/src/server/apis/google/GooglePhotosUploadUtils.ts
+++ /dev/null
@@ -1,149 +0,0 @@
-
-import request = require('request-promise');
-import * as path from 'path';
-import { NewMediaItemResult } from './SharedTypes';
-import { BatchedArray, TimeUnit } from 'array-batcher';
-import { DashUploadUtils } from '../../DashUploadUtils';
-
-/**
- * This namespace encompasses the logic
- * necessary to upload images to Google's server,
- * and then initialize / create those images in the Photos
- * API given the upload tokens returned from the initial
- * uploading process.
- *
- * https://developers.google.com/photos/library/reference/rest/v1/mediaItems/batchCreate
- */
-export namespace GooglePhotosUploadUtils {
-
- /**
- * Specifies the structure of the object
- * necessary to upload bytes to Google's servers.
- * The url is streamed to access the image's bytes,
- * and the description is what appears in Google Photos'
- * description field.
- */
- export interface UploadSource {
- url: string;
- description: string;
- }
-
- /**
- * This is the format needed to pass
- * into the BatchCreate API request
- * to take a reference to raw uploaded bytes
- * and actually create an image in Google Photos.
- *
- * So, to instantiate this interface you must have already dispatched an upload
- * and received an upload token.
- */
- export interface NewMediaItem {
- description: string;
- simpleMediaItem: {
- uploadToken: string;
- };
- }
-
- /**
- * A utility function to streamline making
- * calls to the API's url - accentuates
- * the relative path in the caller.
- * @param extension the desired
- * subset of the API
- */
- function prepend(extension: string): string {
- return `https://photoslibrary.googleapis.com/v1/${extension}`;
- }
-
- /**
- * Factors out the creation of the API request's
- * authentication elements stored in the header.
- * @param type the contents of the request
- * @param token the user-specific Google access token
- */
- function headers(type: string, token: string) {
- return {
- 'Content-Type': `application/${type}`,
- 'Authorization': `Bearer ${token}`,
- };
- }
-
- /**
- * This is the first step in the remote image creation process.
- * Here we upload the raw bytes of the image to Google's servers by
- * setting authentication and other required header properties and including
- * the raw bytes to the image, to be uploaded, in the body of the request.
- * @param bearerToken the user-specific Google access token, specifies the account associated
- * with the eventual image creation
- * @param url the url of the image to upload
- * @param filename an optional name associated with the uploaded image - if not specified
- * defaults to the filename (basename) in the url
- */
- export const DispatchGooglePhotosUpload = async (bearerToken: string, url: string, filename?: string): Promise<any> => {
- // check if the url points to a non-image or an unsupported format
- if (!DashUploadUtils.validateExtension(url)) {
- return undefined;
- }
- const parameters = {
- method: 'POST',
- uri: prepend('uploads'),
- headers: {
- ...headers('octet-stream', bearerToken),
- 'X-Goog-Upload-File-Name': filename || path.basename(url),
- 'X-Goog-Upload-Protocol': 'raw'
- },
- body: await request(url, { encoding: null }) // returns a readable stream with the unencoded binary image data
- };
- return new Promise((resolve, reject) => request(parameters, (error, _response, body) => {
- if (error) {
- // on rejection, the server logs the error and the offending image
- return reject(error);
- }
- resolve(body);
- }));
- };
-
- /**
- * This is the second step in the remote image creation process: having uploaded
- * the raw bytes of the image and received / stored pointers (upload tokens) to those
- * bytes, we can now instruct the API to finalize the creation of those images by
- * submitting a batch create request with the list of upload tokens and the description
- * to be associated with reach resulting new image.
- * @param bearerToken the user-specific Google access token, specifies the account associated
- * with the eventual image creation
- * @param newMediaItems a list of objects containing a description and, effectively, the
- * pointer to the uploaded bytes
- * @param album if included, will add all of the newly created remote images to the album
- * with the specified id
- */
- export const CreateMediaItems = async (bearerToken: string, newMediaItems: NewMediaItem[], album?: { id: string }): Promise<NewMediaItemResult[]> => {
- // it's important to note that the API can't handle more than 50 items in each request and
- // seems to need at least some latency between requests (spamming it synchronously has led to the server returning errors)...
- const batched = BatchedArray.from(newMediaItems, { batchSize: 50 });
- // ...so we execute them in delayed batches and await the entire execution
- return batched.batchedMapPatientInterval(
- { magnitude: 100, unit: TimeUnit.Milliseconds },
- async (batch: NewMediaItem[], collector: any): Promise<any> => {
- const parameters = {
- method: 'POST',
- headers: headers('json', bearerToken),
- uri: prepend('mediaItems:batchCreate'),
- body: { newMediaItems: batch } as any,
- json: true
- };
- // register the target album, if provided
- album && (parameters.body.albumId = album.id);
- collector.push(...(await new Promise<NewMediaItemResult[]>((resolve, reject) => {
- request(parameters, (error, _response, body) => {
- if (error) {
- reject(error);
- } else {
- resolve(body.newMediaItemResults);
- }
- });
- })));
- }
- );
- };
-
-} \ No newline at end of file
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index 8c357884e..31667cbdc 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -1,7 +1,7 @@
import { action, computed, observable, reaction } from "mobx";
import * as rp from 'request-promise';
import { DocServer } from "../../../client/DocServer";
-import { Docs } from "../../../client/documents/Documents";
+import { Docs, DocumentOptions } from "../../../client/documents/Documents";
import { Attribute, AttributeGroup, Catalog, Schema } from "../../../client/northstar/model/idea/idea";
import { ArrayUtil } from "../../../client/northstar/utils/ArrayUtil";
import { UndoManager } from "../../../client/util/UndoManager";
@@ -11,10 +11,15 @@ import { listSpec } from "../../../new_fields/Schema";
import { ScriptField, ComputedField } from "../../../new_fields/ScriptField";
import { Cast, PromiseValue, StrCast } from "../../../new_fields/Types";
import { Utils } from "../../../Utils";
-import { nullAudio } from "../../../new_fields/URLField";
+import { nullAudio, ImageField } from "../../../new_fields/URLField";
import { DragManager } from "../../../client/util/DragManager";
import { InkingControl } from "../../../client/views/InkingControl";
+import { Scripting } from "../../../client/util/Scripting";
import { CollectionViewType } from "../../../client/views/collections/CollectionView";
+import { makeTemplate } from "../../../client/util/DropConverter";
+import { RichTextField } from "../../../new_fields/RichTextField";
+import { PrefetchProxy } from "../../../new_fields/Proxy";
+import { FormattedTextBox } from "../../../client/views/nodes/FormattedTextBox";
export class CurrentUserUtils {
private static curr_id: string;
@@ -31,47 +36,71 @@ export class CurrentUserUtils {
@observable public static GuestWorkspace: Doc | undefined;
@observable public static GuestMobile: Doc | undefined;
- // a default set of note types .. not being used yet...
- static setupNoteTypes(doc: Doc) {
- return [
- Docs.Create.TextDocument("", { title: "Note", backgroundColor: "yellow", isTemplateDoc: true }),
- Docs.Create.TextDocument("", { title: "Idea", backgroundColor: "pink", isTemplateDoc: true }),
- Docs.Create.TextDocument("", { title: "Topic", backgroundColor: "lightBlue", isTemplateDoc: true }),
- Docs.Create.TextDocument("", { title: "Person", backgroundColor: "lightGreen", isTemplateDoc: true }),
- Docs.Create.TextDocument("", { title: "Todo", backgroundColor: "orange", isTemplateDoc: true })
+ static setupDefaultDocTemplates(doc: Doc, buttons?: string[]) {
+ const taskStatusValues = [{ title: "todo", _backgroundColor: "blue", color: "white" },
+ { title: "in progress", _backgroundColor: "yellow", color: "black" },
+ { title: "completed", _backgroundColor: "green", color: "white" }
];
+ const noteTemplates = [
+ Docs.Create.TextDocument("", { title: "text", style: "Note", isTemplateDoc: true, backgroundColor: "yellow" }),
+ Docs.Create.TextDocument("", { title: "text", style: "Idea", isTemplateDoc: true, backgroundColor: "pink" }),
+ Docs.Create.TextDocument("", { title: "text", style: "Topic", isTemplateDoc: true, backgroundColor: "lightBlue" }),
+ Docs.Create.TextDocument("", { title: "text", style: "Person", isTemplateDoc: true, backgroundColor: "lightGreen" }),
+ Docs.Create.TextDocument("", {
+ title: "text", style: "Todo", isTemplateDoc: true, backgroundColor: "orange", _autoHeight: false,
+ layout: FormattedTextBox.LayoutString("Todo"), _height: 100, _showCaption: "caption", caption: RichTextField.DashField("taskStatus")
+ })
+ ];
+ doc.fieldTypes = Docs.Create.TreeDocument([], { title: "field enumerations" });
+ Doc.addFieldEnumerations(Doc.GetProto(noteTemplates[4]), "taskStatus", taskStatusValues);
+ doc.noteTypes = new PrefetchProxy(Docs.Create.TreeDocument(noteTemplates.map(nt => makeTemplate(nt, true, StrCast(nt.style)) ? nt : nt), { title: "Note Layouts", _height: 75 }));
+ }
+ static setupDefaultIconTypes(doc: Doc, buttons?: string[]) {
+ doc.iconView = new PrefetchProxy(Docs.Create.TextDocument("", { title: "icon", _width: 150, _height: 30, isTemplateDoc: true, onClick: ScriptField.MakeScript("deiconifyView(this)") }));
+ Doc.GetProto(doc.iconView as any as Doc).icon = new RichTextField('{"doc":{"type":"doc","content":[{"type":"paragraph","attrs":{"align":null,"color":null,"id":null,"indent":null,"inset":null,"lineSpacing":null,"paddingBottom":null,"paddingTop":null},"content":[{"type":"dashField","attrs":{"fieldKey":"title","docid":""}}]}]},"selection":{"type":"text","anchor":2,"head":2},"storedMarks":[]}', "");
+ doc.isTemplateDoc = makeTemplate(doc.iconView as any as Doc);
+ doc.iconImageView = new PrefetchProxy(Docs.Create.ImageDocument("http://www.cs.brown.edu/~bcz/face.gif", { title: "data", _width: 50, isTemplateDoc: true, onClick: ScriptField.MakeScript("deiconifyView(this)") }));
+ doc.isTemplateDoc = makeTemplate(doc.iconImageView as any as Doc, true, "image_icon");
+ doc.iconColView = new PrefetchProxy(Docs.Create.TreeDocument([], { title: "data", _width: 180, _height: 80, isTemplateDoc: true, onClick: ScriptField.MakeScript("deiconifyView(this)") }));
+ doc.isTemplateDoc = makeTemplate(doc.iconColView as any as Doc, true, "collection_icon");
+ doc.iconViews = Docs.Create.TreeDocument([doc.iconView as any as Doc, doc.iconImageView as any as Doc, doc.iconColView as any as Doc], { title: "icon types", _height: 75 });
}
// setup the "creator" buttons for the sidebar-- eg. the default set of draggable document creation tools
- static setupCreatorButtons(doc: Doc, buttons?: string[]) {
- const notes = CurrentUserUtils.setupNoteTypes(doc);
- doc.noteTypes = Docs.Create.TreeDocument(notes, { title: "Note Types", _height: 75 });
+ static setupCreatorButtons(doc: Doc, alreadyCreatedButtons?: string[]) {
+ const emptyPresentation = Docs.Create.PresDocument(new List<Doc>(), { title: "Presentation", _viewType: CollectionViewType.Stacking, _LODdisable: true, _chromeStatus: "replaced", _showTitle: "title", boxShadow: "0 0" });
+ const emptyCollection = Docs.Create.FreeformDocument([], { _nativeWidth: undefined, _nativeHeight: undefined, _LODdisable: true, _width: 150, _height: 100, title: "freeform" });
doc.activePen = doc;
const docProtoData: { title: string, icon: string, drag?: string, ignoreClick?: boolean, click?: string, ischecked?: string, activePen?: Doc, backgroundColor?: string, dragFactory?: Doc }[] = [
- { title: "collection", icon: "folder", ignoreClick: true, drag: 'Docs.Create.FreeformDocument([], { _nativeWidth: undefined, _nativeHeight: undefined, _LODdisable: true, _width: 150, _height: 100, title: "freeform" })' },
- { title: "preview", icon: "expand", ignoreClick: true, drag: 'Docs.Create.DocumentDocument(ComputedField.MakeFunction("selectedDocs(this,true,[_last_])?.[0]"), { _width: 250, _height: 250, title: "container" })' },
- { title: "todo item", icon: "check", ignoreClick: true, drag: 'getCopy(this.dragFactory, true)', dragFactory: notes[notes.length - 1] },
+ { title: "collection", icon: "folder", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: emptyCollection },
+ { title: "preview", icon: "expand", ignoreClick: true, drag: 'Docs.Create.DocumentDocument(ComputedField.MakeFunction("selectedDocs(this,this.excludeCollections,[_last_])?.[0]"), { _width: 250, _height: 250, title: "container" })' },
{ title: "web page", icon: "globe-asia", ignoreClick: true, drag: 'Docs.Create.WebDocument("https://en.wikipedia.org/wiki/Hedgehog", {_width: 300, _height: 300, title: "New Webpage" })' },
{ title: "cat image", icon: "cat", ignoreClick: true, drag: 'Docs.Create.ImageDocument("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", { _width: 200, title: "an image of a cat" })' },
+ { title: "buxton", icon: "cloud-upload-alt", ignoreClick: true, drag: "Docs.Create.Buxton()" },
+ { title: "screenshot", icon: "photo-video", ignoreClick: true, drag: 'Docs.Create.ScreenshotDocument("", { _width: 400, _height: 200, title: "screen snapshot" })' },
+ { title: "webcam", icon: "video", ignoreClick: true, drag: 'Docs.Create.WebCamDocument("", { _width: 400, _height: 400, title: "a test cam" })' },
{ title: "record", icon: "microphone", ignoreClick: true, drag: `Docs.Create.AudioDocument("${nullAudio}", { _width: 200, title: "ready to record audio" })` },
{ title: "clickable button", icon: "bolt", ignoreClick: true, drag: 'Docs.Create.ButtonDocument({ _width: 150, _height: 50, title: "Button" })' },
- { title: "presentation", icon: "tv", ignoreClick: true, drag: `Doc.UserDoc().curPresentation = Docs.Create.PresDocument(new List<Doc>(), { _width: 200, _height: 500, _viewType: ${CollectionViewType.Stacking}, title: "a presentation trail" })` },
+ { title: "presentation", icon: "tv", click: 'openOnRight(Doc.UserDoc().curPresentation = getCopy(this.dragFactory, true))', drag: `Doc.UserDoc().curPresentation = getCopy(this.dragFactory,true)`, dragFactory: emptyPresentation },
{ title: "import folder", icon: "cloud-upload-alt", ignoreClick: true, drag: 'Docs.Create.DirectoryImportDocument({ title: "Directory Import", _width: 400, _height: 400 })' },
{ title: "mobile view", icon: "phone", ignoreClick: true, drag: 'Doc.UserDoc().activeMobile' },
{ title: "use pen", icon: "pen-nib", click: 'activatePen(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this,2, this.backgroundColor)', backgroundColor: "blue", ischecked: `sameDocs(this.activePen.pen, this)`, activePen: doc },
{ title: "use highlighter", icon: "highlighter", click: 'activateBrush(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this,20,this.backgroundColor)', backgroundColor: "yellow", ischecked: `sameDocs(this.activePen.pen, this)`, activePen: doc },
{ title: "use stamp", icon: "stamp", click: 'activateStamp(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this)', backgroundColor: "orange", ischecked: `sameDocs(this.activePen.pen, this)`, activePen: doc },
{ title: "use eraser", icon: "eraser", click: 'activateEraser(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this);', ischecked: `sameDocs(this.activePen.pen, this)`, backgroundColor: "pink", activePen: doc },
- { title: "use scrubber", icon: "eraser", click: 'activateScrubber(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this);', ischecked: `sameDocs(this.activePen.pen, this)`, backgroundColor: "green", activePen: doc },
{ title: "use drag", icon: "mouse-pointer", click: 'deactivateInk();this.activePen.pen = this;', ischecked: `sameDocs(this.activePen.pen, this)`, backgroundColor: "white", activePen: doc },
{ title: "search", icon: "bolt", ignoreClick: true, drag: 'Docs.Create.SearchDocument({ _width: 200, title: "an image of a cat" })' },
];
- return docProtoData.filter(d => !buttons || !buttons.includes(d.title)).map(data => Docs.Create.FontIconDocument({
- _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, _dropAction: data.click ? "copy" : undefined, title: data.title, icon: data.icon, ignoreClick: data.ignoreClick,
+ return docProtoData.filter(d => !alreadyCreatedButtons?.includes(d.title)).map(data => Docs.Create.FontIconDocument({
+ _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100,
+ icon: data.icon,
+ title: data.title,
+ ignoreClick: data.ignoreClick,
+ dropAction: data.click ? "copy" : undefined,
onDragStart: data.drag ? ScriptField.MakeFunction(data.drag) : undefined, onClick: data.click ? ScriptField.MakeScript(data.click) : undefined,
- ischecked: data.ischecked ? ComputedField.MakeFunction(data.ischecked) : undefined, activePen: data.activePen,
+ ischecked: data.ischecked ? ComputedField.MakeFunction(data.ischecked) : undefined, activePen: data.activePen, dontSelect: true,
backgroundColor: data.backgroundColor, removeDropProperties: new List<string>(["dropAction"]), dragFactory: data.dragFactory,
}));
}
@@ -88,8 +117,7 @@ export class CurrentUserUtils {
const dragdocs = await Cast(dragset.data, listSpec(Doc));
if (dragdocs) {
const dragDocs = await Promise.all(dragdocs);
- const newButtons = this.setupCreatorButtons(doc, dragDocs.map(d => StrCast(d.title)));
- newButtons.map(nb => Doc.AddDocToList(dragset, "data", nb));
+ this.setupCreatorButtons(doc, dragDocs.map(d => StrCast(d.title))).map(nb => Doc.AddDocToList(dragset, "data", nb));
}
}
}
@@ -103,11 +131,13 @@ export class CurrentUserUtils {
{ title: "use pen", icon: "pen-nib", click: 'activatePen(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this,2, this.backgroundColor)', backgroundColor: "blue", ischecked: `sameDocs(this.activePen.pen, this)`, activePen: doc },
{ title: "use highlighter", icon: "highlighter", click: 'activateBrush(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this,20,this.backgroundColor)', backgroundColor: "yellow", ischecked: `sameDocs(this.activePen.pen, this)`, activePen: doc },
{ title: "use eraser", icon: "eraser", click: 'activateEraser(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this);', ischecked: `sameDocs(this.activePen.pen, this)`, backgroundColor: "pink", activePen: doc },
- { title: "use scrubber", icon: "eraser", click: 'activateScrubber(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this);', ischecked: `sameDocs(this.activePen.pen, this)`, backgroundColor: "green", activePen: doc },
{ title: "use drag", icon: "mouse-pointer", click: 'deactivateInk();this.activePen.pen = this;', ischecked: `sameDocs(this.activePen.pen, this)`, backgroundColor: "white", activePen: doc },
+ // { title: "draw", icon: "pen-nib", click: 'switchMobileView(setupMobileInkingDoc, renderMobileInking, onSwitchMobileInking);', ischecked: `sameDocs(this.activePen.pen, this)`, backgroundColor: "red", activePen: doc },
+ { title: "upload", icon: "upload", click: 'switchMobileView(setupMobileUploadDoc, renderMobileUpload, onSwitchMobileUpload);', backgroundColor: "orange" },
+ // { title: "upload", icon: "upload", click: 'uploadImageMobile();', backgroundColor: "cyan" },
];
return docProtoData.filter(d => !buttons || !buttons.includes(d.title)).map(data => Docs.Create.FontIconDocument({
- _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, _dropAction: data.click ? "copy" : undefined, title: data.title, icon: data.icon, ignoreClick: data.ignoreClick,
+ _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, dropAction: data.click ? "copy" : undefined, title: data.title, icon: data.icon, ignoreClick: data.ignoreClick,
onDragStart: data.drag ? ScriptField.MakeFunction(data.drag) : undefined, onClick: data.click ? ScriptField.MakeScript(data.click) : undefined,
ischecked: data.ischecked ? ComputedField.MakeFunction(data.ischecked) : undefined, activePen: data.activePen,
backgroundColor: data.backgroundColor, removeDropProperties: new List<string>(["dropAction"]), dragFactory: data.dragFactory,
@@ -123,7 +153,8 @@ export class CurrentUserUtils {
{ title: "ignore gestures", icon: "signature", pointerUp: "setToolglass('none')", pointerDown: "setToolglass('ignoregesture')", backgroundColor: "green", ischecked: `sameDocs(this.activePen.pen, this)`, activePen: doc },
];
return docProtoData.map(data => Docs.Create.FontIconDocument({
- _nativeWidth: 10, _nativeHeight: 10, _width: 10, _height: 10, _dropAction: data.pointerDown ? "copy" : undefined, title: data.title, icon: data.icon, ignoreClick: data.ignoreClick,
+ _nativeWidth: 10, _nativeHeight: 10, _width: 10, _height: 10, title: data.title, icon: data.icon,
+ dropAction: data.pointerDown ? "copy" : undefined, ignoreClick: data.ignoreClick,
onDragStart: data.drag ? ScriptField.MakeFunction(data.drag) : undefined,
clipboard: data.clipboard,
onPointerUp: data.pointerUp ? ScriptField.MakeScript(data.pointerUp) : undefined, onPointerDown: data.pointerDown ? ScriptField.MakeScript(data.pointerDown) : undefined,
@@ -135,9 +166,9 @@ export class CurrentUserUtils {
static setupThumbDoc(userDoc: Doc) {
if (!userDoc.thumbDoc) {
const thumbDoc = Docs.Create.LinearDocument(CurrentUserUtils.setupThumbButtons(userDoc), {
- _width: 100, _height: 50, ignoreClick: true, lockedPosition: true, _chromeStatus: "disabled", title: "buttons", _autoHeight: true, _yMargin: 5, isExpanded: true, backgroundColor: "white"
+ _width: 100, _height: 50, ignoreClick: true, lockedPosition: true, _chromeStatus: "disabled", title: "buttons", _autoHeight: true, _yMargin: 5, linearViewIsExpanded: true, backgroundColor: "white"
});
- thumbDoc.inkToTextDoc = Docs.Create.LinearDocument([], { _width: 300, _height: 25, _autoHeight: true, _chromeStatus: "disabled", isExpanded: true, flexDirection: "column" });
+ thumbDoc.inkToTextDoc = Docs.Create.LinearDocument([], { _width: 300, _height: 25, _autoHeight: true, _chromeStatus: "disabled", linearViewIsExpanded: true, flexDirection: "column" });
userDoc.thumbDoc = thumbDoc;
}
return Cast(userDoc.thumbDoc, Doc);
@@ -149,6 +180,23 @@ export class CurrentUserUtils {
});
}
+ static setupMobileInkingDoc(userDoc: Doc) {
+ return Docs.Create.FreeformDocument([], { title: "Mobile Inking", backgroundColor: "white" });
+ }
+
+ static setupMobileUploadDoc(userDoc: Doc) {
+ // const addButton = Docs.Create.FontIconDocument({ onDragStart: ScriptField.MakeScript('addWebToMobileUpload()'), title: "Add Web Doc to Upload Collection", icon: "plus", backgroundColor: "black" })
+ const webDoc = Docs.Create.WebDocument("https://www.britannica.com/biography/Miles-Davis", {
+ title: "Upload Images From the Web", _chromeStatus: "enabled", lockedPosition: true
+ });
+ const uploadDoc = Docs.Create.StackingDocument([], {
+ title: "Mobile Upload Collection", backgroundColor: "white", lockedPosition: true
+ });
+ return Docs.Create.StackingDocument([webDoc, uploadDoc], {
+ _width: screen.width, lockedPosition: true, _chromeStatus: "disabled", title: "Upload", _autoHeight: true, _yMargin: 80, backgroundColor: "lightgray"
+ });
+ }
+
// setup the Creator button which will display the creator panel. This panel will include the drag creators and the color picker. when clicked, this panel will be displayed in the target container (ie, sidebarContainer)
static setupToolsPanel(sidebarContainer: Doc, doc: Doc) {
// setup a masonry view of all he creators
@@ -158,11 +206,11 @@ export class CurrentUserUtils {
});
// setup a color picker
const color = Docs.Create.ColorDocument({
- title: "color picker", _width: 300, _dropAction: "alias", forceActive: true, removeDropProperties: new List<string>(["dropAction", "forceActive"])
+ title: "color picker", _width: 300, dropAction: "alias", forceActive: true, removeDropProperties: new List<string>(["dropAction", "forceActive"])
});
return Docs.Create.ButtonDocument({
- _width: 35, _height: 25, backgroundColor: "lightgrey", color: "rgb(34, 34, 34)", title: "Tools", fontSize: 10, targetContainer: sidebarContainer,
+ _width: 35, _height: 25, title: "Tools", fontSize: 10, targetContainer: sidebarContainer, dontSelect: true,
letterSpacing: "0px", textTransform: "unset", borderRounding: "5px 5px 0px 0px", boxShadow: "3px 3px 0px rgb(34, 34, 34)",
sourcePanel: Docs.Create.StackingDocument([dragCreators, color], {
_width: 500, lockedPosition: true, _chromeStatus: "disabled", title: "tools stack"
@@ -175,23 +223,23 @@ export class CurrentUserUtils {
static setupLibraryPanel(sidebarContainer: Doc, doc: Doc) {
// setup workspaces library item
doc.workspaces = Docs.Create.TreeDocument([], {
- title: "WORKSPACES", _height: 100, forceActive: true, boxShadow: "0 0", lockedPosition: true, backgroundColor: "#eeeeee"
+ title: "WORKSPACES", _height: 100, forceActive: true, boxShadow: "0 0", lockedPosition: true,
});
doc.documents = Docs.Create.TreeDocument([], {
- title: "DOCUMENTS", _height: 42, forceActive: true, boxShadow: "0 0", preventTreeViewOpen: true, lockedPosition: true, backgroundColor: "#eeeeee"
+ title: "DOCUMENTS", _height: 42, forceActive: true, boxShadow: "0 0", treeViewPreventOpen: true, lockedPosition: true,
});
// setup Recently Closed library item
doc.recentlyClosed = Docs.Create.TreeDocument([], {
- title: "RECENTLY CLOSED", _height: 75, forceActive: true, boxShadow: "0 0", preventTreeViewOpen: true, lockedPosition: true, backgroundColor: "#eeeeee"
+ title: "RECENTLY CLOSED", _height: 75, forceActive: true, boxShadow: "0 0", treeViewPreventOpen: true, lockedPosition: true,
});
return Docs.Create.ButtonDocument({
- _width: 50, _height: 25, backgroundColor: "lightgrey", color: "rgb(34, 34, 34)", title: "Library", fontSize: 10,
+ _width: 50, _height: 25, title: "Library", fontSize: 10, dontSelect: true,
letterSpacing: "0px", textTransform: "unset", borderRounding: "5px 5px 0px 0px", boxShadow: "3px 3px 0px rgb(34, 34, 34)",
- sourcePanel: Docs.Create.TreeDocument([doc.workspaces as Doc, doc.documents as Doc, doc.recentlyClosed as Doc], {
- title: "Library", _xMargin: 5, _yMargin: 5, _gridGap: 5, forceActive: true, _dropAction: "alias", lockedPosition: true, boxShadow: "0 0",
+ sourcePanel: Docs.Create.TreeDocument([doc.workspaces as Doc, doc.documents as Doc, Docs.Prototypes.MainLinkDocument(), doc, doc.recentlyClosed as Doc], {
+ title: "Library", _xMargin: 5, _yMargin: 5, _gridGap: 5, forceActive: true, childDropAction: "place", lockedPosition: true, boxShadow: "0 0", dontRegisterChildren: true
}),
targetContainer: sidebarContainer,
onClick: ScriptField.MakeScript("this.targetContainer.proto = this.sourcePanel;")
@@ -201,7 +249,7 @@ export class CurrentUserUtils {
// setup the Search button which will display the search panel.
static setupSearchPanel(sidebarContainer: Doc) {
return Docs.Create.ButtonDocument({
- _width: 50, _height: 25, backgroundColor: "lightgrey", color: "rgb(34, 34, 34)", title: "Search", fontSize: 10,
+ _width: 50, _height: 25, title: "Search", fontSize: 10, dontSelect: true,
letterSpacing: "0px", textTransform: "unset", borderRounding: "5px 5px 0px 0px", boxShadow: "3px 3px 0px rgb(34, 34, 34)",
sourcePanel: Docs.Create.QueryDocument({
title: "search stack",
@@ -214,55 +262,75 @@ export class CurrentUserUtils {
// setup the list of sidebar mode buttons which determine what is displayed in the sidebar
static setupSidebarButtons(doc: Doc) {
- doc.sidebarContainer = new Doc();
- (doc.sidebarContainer as Doc)._chromeStatus = "disabled";
- (doc.sidebarContainer as Doc).onClick = ScriptField.MakeScript("freezeSidebar()");
+ const sidebarContainer = new Doc();
+ doc.sidebarContainer = new PrefetchProxy(sidebarContainer);
+ sidebarContainer._chromeStatus = "disabled";
+ sidebarContainer.onClick = ScriptField.MakeScript("freezeSidebar()");
- doc.ToolsBtn = this.setupToolsPanel(doc.sidebarContainer as Doc, doc);
- doc.LibraryBtn = this.setupLibraryPanel(doc.sidebarContainer as Doc, doc);
- doc.SearchBtn = this.setupSearchPanel(doc.sidebarContainer as Doc);
+ doc.ToolsBtn = new PrefetchProxy(this.setupToolsPanel(sidebarContainer, doc));
+ doc.LibraryBtn = new PrefetchProxy(this.setupLibraryPanel(sidebarContainer, doc));
+ doc.SearchBtn = new PrefetchProxy(this.setupSearchPanel(sidebarContainer));
// Finally, setup the list of buttons to display in the sidebar
- doc.sidebarButtons = Docs.Create.StackingDocument([doc.SearchBtn as Doc, doc.LibraryBtn as Doc, doc.ToolsBtn as Doc], {
- _width: 500, _height: 80, boxShadow: "0 0", sectionFilter: "title", hideHeadings: true, ignoreClick: true,
- backgroundColor: "rgb(100, 100, 100)", _chromeStatus: "disabled", title: "library stack",
- _yMargin: 10,
- });
+ doc.sidebarButtons = new PrefetchProxy(Docs.Create.StackingDocument([doc.SearchBtn as any as Doc, doc.LibraryBtn as any as Doc, doc.ToolsBtn as any as Doc], {
+ _width: 500, _height: 80, boxShadow: "0 0", _pivotField: "title", hideHeadings: true, ignoreClick: true, _chromeStatus: "view-mode",
+ title: "sidebar btn row stack", backgroundColor: "dimGray",
+ }));
}
/// sets up the default list of buttons to be shown in the expanding button menu at the bottom of the Dash window
static setupExpandingButtons(doc: Doc) {
- doc.undoBtn = Docs.Create.FontIconDocument(
- { _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, _dropAction: "alias", onClick: ScriptField.MakeScript("undo()"), removeDropProperties: new List<string>(["dropAction"]), title: "undo button", icon: "undo-alt" });
- doc.redoBtn = Docs.Create.FontIconDocument(
- { _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, _dropAction: "alias", onClick: ScriptField.MakeScript("redo()"), removeDropProperties: new List<string>(["dropAction"]), title: "redo button", icon: "redo-alt" });
-
- doc.expandingButtons = Docs.Create.LinearDocument([doc.undoBtn as Doc, doc.redoBtn as Doc], {
- title: "expanding buttons", _gridGap: 5, _xMargin: 5, _yMargin: 5, _height: 42, _width: 100, boxShadow: "0 0",
- backgroundColor: "black", preventTreeViewOpen: true, forceActive: true, lockedPosition: true,
+ const slideTemplate = Docs.Create.MultirowDocument(
+ [
+ Docs.Create.MulticolumnDocument([], { title: "data", _height: 200 }),
+ Docs.Create.TextDocument("", { title: "text", _height: 100 })
+ ],
+ { _width: 400, _height: 300, title: "slideView", _chromeStatus: "disabled", _xMargin: 3, _yMargin: 3, _autoHeight: false });
+ slideTemplate.isTemplateDoc = makeTemplate(slideTemplate);
+ const descriptionTemplate = Docs.Create.TextDocument("", { title: "text", _height: 100, _showTitle: "title" });
+ Doc.GetProto(descriptionTemplate).layout = FormattedTextBox.LayoutString("description");
+ descriptionTemplate.isTemplateDoc = makeTemplate(descriptionTemplate, true, "descriptionView");
+
+ const ficon = (opts: DocumentOptions) => new PrefetchProxy(Docs.Create.FontIconDocument({ ...opts, dontSelect: true, dropAction: "alias", removeDropProperties: new List<string>(["dropAction"]), _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100 })) as any as Doc;
+ const blist = (opts: DocumentOptions, docs: Doc[]) => new PrefetchProxy(Docs.Create.LinearDocument(docs, {
+ ...opts,
+ _gridGap: 5, _xMargin: 5, _yMargin: 5, _height: 42, _width: 100, boxShadow: "0 0", dontSelect: true, forceActive: true,
+ dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }),
+ backgroundColor: "black", treeViewPreventOpen: true, lockedPosition: true, _chromeStatus: "disabled", linearViewIsExpanded: true
+ })) as any as Doc;
+
+ doc.undoBtn = ficon({ onClick: ScriptField.MakeScript("undo()"), title: "undo button", icon: "undo-alt" });
+ doc.redoBtn = ficon({ onClick: ScriptField.MakeScript("redo()"), title: "redo button", icon: "redo-alt" });
+ doc.slidesBtn = ficon({ onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'), dragFactory: slideTemplate, removeDropProperties: new List<string>(["dropAction"]), title: "presentation slide", icon: "sticky-note" });
+ doc.descriptionBtn = ficon({ onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'), dragFactory: descriptionTemplate, removeDropProperties: new List<string>(["dropAction"]), title: "description view", icon: "sticky-note" });
+ doc.templateButtons = blist({ title: "template buttons" }, [doc.slidesBtn as Doc, doc.descriptionBtn as Doc]);
+ doc.expandingButtons = blist({ title: "expanding buttons" }, [doc.undoBtn as Doc, doc.redoBtn as Doc, doc.templateButtons as Doc]);
+ doc.templateDocs = new PrefetchProxy(Docs.Create.TreeDocument([doc.noteTypes as Doc, doc.templateButtons as Doc], {
+ title: "template layouts", _xPadding: 0,
dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name })
- });
+ }));
}
// sets up the default set of documents to be shown in the Overlay layer
static setupOverlays(doc: Doc) {
- doc.overlays = Docs.Create.FreeformDocument([], { title: "Overlays", backgroundColor: "#aca3a6" });
- doc.linkFollowBox = Docs.Create.LinkFollowBoxDocument({ x: 250, y: 20, _width: 500, _height: 370, title: "Link Follower" });
- Doc.AddDocToList(doc.overlays as Doc, "data", doc.linkFollowBox as Doc);
+ doc.overlays = new PrefetchProxy(Docs.Create.FreeformDocument([], { title: "Overlays", backgroundColor: "#aca3a6" }));
}
// the initial presentation Doc to use
static setupDefaultPresentation(doc: Doc) {
- doc.curPresentation = Docs.Create.PresDocument(new List<Doc>(), { title: "Presentation", _viewType: CollectionViewType.Stacking, boxShadow: "0 0" });
+ doc.presentationTemplate = new PrefetchProxy(Docs.Create.PresElementBoxDocument({ backgroundColor: "transparent", _xMargin: 5, _height: 46, isTemplateDoc: true, isTemplateForField: "data" }));
+ doc.curPresentation = Docs.Create.PresDocument(new List<Doc>(), { title: "Presentation", _viewType: CollectionViewType.Stacking, _LODdisable: true, _chromeStatus: "replaced", _showTitle: "title", boxShadow: "0 0" });
}
static setupMobileUploads(doc: Doc) {
- doc.optionalRightCollection = Docs.Create.StackingDocument([], { title: "New mobile uploads" });
+ doc.optionalRightCollection = new PrefetchProxy(Docs.Create.StackingDocument([], { title: "New mobile uploads" }));
}
static updateUserDocument(doc: Doc) {
doc.title = Doc.CurrentUserEmail;
new InkingControl();
+ (doc.iconTypes === undefined) && CurrentUserUtils.setupDefaultIconTypes(doc);
+ (doc.noteTypes === undefined) && CurrentUserUtils.setupDefaultDocTemplates(doc);
(doc.optionalRightCollection === undefined) && CurrentUserUtils.setupMobileUploads(doc);
(doc.overlays === undefined) && CurrentUserUtils.setupOverlays(doc);
(doc.expandingButtons === undefined) && CurrentUserUtils.setupExpandingButtons(doc);
@@ -291,6 +359,15 @@ export class CurrentUserUtils {
return doc;
}
+ public static IsDocPinned(doc: Doc) {
+ //add this new doc to props.Document
+ const curPres = Cast(CurrentUserUtils.UserDocument.curPresentation, Doc) as Doc;
+ if (curPres) {
+ return DocListCast(curPres.data).findIndex((val) => Doc.AreProtosEqual(val, doc)) !== -1;
+ }
+ return false;
+ }
+
public static async loadCurrentUser() {
return rp.get(Utils.prepend("/getCurrentUser")).then(response => {
if (response) {
@@ -386,3 +463,6 @@ export class CurrentUserUtils {
return recurs([] as Attribute[], schema ? schema.rootAttributeGroup : undefined);
}
}
+
+Scripting.addGlobal(function setupMobileInkingDoc(userDoc: Doc) { return CurrentUserUtils.setupMobileInkingDoc(userDoc); });
+Scripting.addGlobal(function setupMobileUploadDoc(userDoc: Doc) { return CurrentUserUtils.setupMobileUploadDoc(userDoc); });
diff --git a/src/server/database.ts b/src/server/database.ts
index 83ce865c6..fc91ff3a2 100644
--- a/src/server/database.ts
+++ b/src/server/database.ts
@@ -2,12 +2,12 @@ import * as mongodb from 'mongodb';
import { Transferable } from './Message';
import { Opt } from '../new_fields/Doc';
import { Utils, emptyFunction } from '../Utils';
-import { DashUploadUtils } from './DashUploadUtils';
import { Credentials } from 'google-auth-library';
import { GoogleApiServerUtils } from './apis/google/GoogleApiServerUtils';
import { IDatabase } from './IDatabase';
import { MemoryDatabase } from './MemoryDatabase';
import * as mongoose from 'mongoose';
+import { Upload } from './SharedMediaTypes';
export namespace Database {
@@ -297,7 +297,7 @@ export namespace Database {
};
export const QueryUploadHistory = async (contentSize: number) => {
- return SanitizedSingletonQuery<DashUploadUtils.ImageUploadInformation>({ contentSize }, AuxiliaryCollections.GooglePhotosUploadHistory);
+ return SanitizedSingletonQuery<Upload.ImageInformation>({ contentSize }, AuxiliaryCollections.GooglePhotosUploadHistory);
};
export namespace GoogleAuthenticationToken {
@@ -326,9 +326,9 @@ export namespace Database {
}
- export const LogUpload = async (information: DashUploadUtils.ImageUploadInformation) => {
+ export const LogUpload = async (information: Upload.ImageInformation) => {
const bundle = {
- _id: Utils.GenerateDeterministicGuid(String(information.contentSize!)),
+ _id: Utils.GenerateDeterministicGuid(String(information.contentSize)),
...information
};
return Instance.insert(bundle, AuxiliaryCollections.GooglePhotosUploadHistory);
diff --git a/src/server/index.ts b/src/server/index.ts
index 454d99305..8325f5d44 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -24,7 +24,8 @@ import { Logger } from "./ProcessFactory";
import { yellow } from "colors";
// import { DashSessionAgent } from "./DashSession/DashSessionAgent";
import SessionManager from "./ApiManagers/SessionManager";
-import { AppliedSessionAgent } from "resilient-server-session";
+import { AppliedSessionAgent } from "./DashSession/Session/agents/applied_session_agent";
+import { Utils } from "../Utils";
export const onWindows = process.platform === "win32";
export let sessionAgent: AppliedSessionAgent;
@@ -37,6 +38,7 @@ export const filesDirectory = path.resolve(publicDirectory, "files");
* before clients can access the server should be run or awaited here.
*/
async function preliminaryFunctions() {
+ // Utils.TraceConsoleLog();
await Logger.initialize();
await GoogleCredentialsLoader.loadCredentials();
GoogleApiServerUtils.processProjectCredentials();
@@ -86,12 +88,14 @@ function routeSetter({ isRelease, addSupervisedRoute, logRegistrationOutcome }:
secureHandler: ({ res }) => res.redirect("/home")
});
+
addSupervisedRoute({
method: Method.GET,
subscription: "/serverHeartbeat",
secureHandler: ({ res }) => res.send(true)
});
+
const serve: PublicHandler = ({ req, res }) => {
const detector = new mobileDetect(req.headers['user-agent'] || "");
const filename = detector.mobile() !== null ? 'mobile/image.html' : 'index.html';
@@ -119,6 +123,7 @@ function routeSetter({ isRelease, addSupervisedRoute, logRegistrationOutcome }:
WebSocket.start(isRelease);
}
+
/**
* This function can be used in two different ways. If not in release mode,
* this is simply the logic that is invoked to start the server. In release mode,
diff --git a/src/server/server_Initialization.ts b/src/server/server_Initialization.ts
index 9f67c1dda..1150118f7 100644
--- a/src/server/server_Initialization.ts
+++ b/src/server/server_Initialization.ts
@@ -20,7 +20,7 @@ import * as request from 'request';
import RouteSubscriber from './RouteSubscriber';
import { publicDirectory } from '.';
import { logPort, } from './ActionUtilities';
-import { timeMap } from './ApiManagers/UserManager';
+import { Utils } from '../Utils';
import { blue, yellow } from 'colors';
import * as cors from "cors";
@@ -36,24 +36,7 @@ export default async function InitializeServer(routeSetter: RouteSetter) {
setHeaders: res => res.setHeader("Access-Control-Allow-Origin", "*")
}));
app.use("/images", express.static(publicDirectory));
- const corsOptions = {
- origin: function (_origin: any, callback: any) {
- callback(null, true);
- }
- };
- app.use(cors(corsOptions));
- app.use("*", ({ user, originalUrl }, res, next) => {
- if (user && !originalUrl.includes("Heartbeat")) {
- const userEmail = (user as any).email;
- if (userEmail) {
- timeMap[userEmail] = Date.now();
- }
- }
- if (!user && originalUrl === "/") {
- return res.redirect("/login");
- }
- next();
- });
+ app.use(cors({ origin: (_origin: any, callback: any) => callback(null, true) }));
app.use(wdm(compiler, { publicPath: config.output.publicPath }));
app.use(whm(compiler));
@@ -64,10 +47,11 @@ export default async function InitializeServer(routeSetter: RouteSetter) {
const isRelease = determineEnvironment();
routeSetter(new RouteManager(app, isRelease));
+ registerRelativePath(app);
const serverPort = isRelease ? Number(process.env.serverPort) : 1050;
const server = app.listen(serverPort, () => {
- logPort("server", Number(serverPort));
+ logPort("server", serverPort);
console.log();
});
disconnect = async () => new Promise<Error>(resolve => server.close(resolve));
@@ -138,18 +122,47 @@ function registerAuthenticationRoutes(server: express.Express) {
function registerCorsProxy(server: express.Express) {
const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/;
server.use("/corsProxy", (req, res) => {
- req.pipe(request(decodeURIComponent(req.url.substring(1)))).on("response", res => {
- const headers = Object.keys(res.headers);
- headers.forEach(headerName => {
- const header = res.headers[headerName];
- if (Array.isArray(header)) {
- res.headers[headerName] = header.filter(h => !headerCharRegex.test(h));
- } else if (header) {
- if (headerCharRegex.test(header as any)) {
- delete res.headers[headerName];
+
+ const requrl = decodeURIComponent(req.url.substring(1));
+ const referer = req.headers.referer ? decodeURIComponent(req.headers.referer) : "";
+ // 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 {
+ req.pipe(request(requrl)).on("response", res => {
+ const headers = Object.keys(res.headers);
+ headers.forEach(headerName => {
+ const header = res.headers[headerName];
+ if (Array.isArray(header)) {
+ res.headers[headerName] = header.filter(h => !headerCharRegex.test(h));
+ } else if (header) {
+ if (headerCharRegex.test(header as any)) {
+ delete res.headers[headerName];
+ }
}
- }
- });
- }).pipe(res);
+ });
+ }).pipe(res);
+ }
+ });
+}
+
+function registerRelativePath(server: express.Express) {
+ server.use("*", (req, res) => {
+ const relativeUrl = req.originalUrl;
+ if (!res.headersSent && req.headers.referer?.includes("corsProxy")) { // a request for something by a proxied referrer means it must be a relative reference. So construct a proxied absolute reference here.
+ const proxiedRefererUrl = decodeURIComponent(req.headers.referer); // (e.g., http://localhost:1050/corsProxy/https://en.wikipedia.org/wiki/Engelbart)
+ const dashServerUrl = proxiedRefererUrl.match(/.*corsProxy\//)![0]; // the dash server url (e.g.: http://localhost:1050/corsProxy/ )
+ const actualReferUrl = proxiedRefererUrl.replace(dashServerUrl, ""); // the url of the referer without the proxy (e.g., : http:s//en.wikipedia.org/wiki/Engelbart)
+ const absoluteTargetBaseUrl = actualReferUrl.match(/http[s]?:\/\/[^\/]*/)![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:1050/corsProxy/https://en.wikipedia.org/<somethingelse>)
+ res.redirect(redirectedProxiedUrl);
+ } else if (relativeUrl.startsWith("/search")) { // detect search query and use default search engine
+ res.redirect(req.headers.referer + "corsProxy/" + encodeURIComponent("http://www.google.com" + relativeUrl));
+ } else {
+ res.end();
+ }
});
}
diff --git a/src/server/updateSearch.ts b/src/server/updateSearch.ts
deleted file mode 100644
index 83094d36a..000000000
--- a/src/server/updateSearch.ts
+++ /dev/null
@@ -1,121 +0,0 @@
-import { Database } from "./database";
-import { Search } from "./Search";
-import { log_execution } from "./ActionUtilities";
-import { cyan, green, yellow, red } from "colors";
-
-const suffixMap: { [type: string]: (string | [string, string | ((json: any) => any)]) } = {
- "number": "_n",
- "string": "_t",
- "boolean": "_b",
- "image": ["_t", "url"],
- "video": ["_t", "url"],
- "pdf": ["_t", "url"],
- "audio": ["_t", "url"],
- "web": ["_t", "url"],
- "date": ["_d", value => new Date(value.date).toISOString()],
- "proxy": ["_i", "fieldId"],
- "list": ["_l", list => {
- const results = [];
- for (const value of list.fields) {
- const term = ToSearchTerm(value);
- if (term) {
- results.push(term.value);
- }
- }
- return results.length ? results : null;
- }]
-};
-
-function ToSearchTerm(val: any): { suffix: string, value: any } | undefined {
- if (val === null || val === undefined) {
- return;
- }
- const type = val.__type || typeof val;
- let suffix = suffixMap[type];
- if (!suffix) {
- return;
- }
-
- if (Array.isArray(suffix)) {
- const accessor = suffix[1];
- if (typeof accessor === "function") {
- val = accessor(val);
- } else {
- val = val[accessor];
- }
- suffix = suffix[0];
- }
-
- return { suffix, value: val };
-}
-
-async function update() {
- console.log(green("Beginning update..."));
- await log_execution<void>({
- startMessage: "Clearing existing Solr information...",
- endMessage: "Solr information successfully cleared",
- action: Search.clear,
- color: cyan
- });
- const cursor = await log_execution({
- startMessage: "Connecting to and querying for all documents from database...",
- endMessage: ({ result, error }) => {
- const success = error === null && result !== undefined;
- if (!success) {
- console.log(red("Unable to connect to the database."));
- process.exit(0);
- }
- return "Connection successful and query complete";
- },
- action: () => Database.Instance.query({}),
- color: yellow
- });
- const updates: any[] = [];
- let numDocs = 0;
- function updateDoc(doc: any) {
- numDocs++;
- if ((numDocs % 50) === 0) {
- console.log(`Batch of 50 complete, total of ${numDocs}`);
- }
- if (doc.__type !== "Doc") {
- return;
- }
- const fields = doc.fields;
- if (!fields) {
- return;
- }
- const update: any = { id: doc._id };
- let dynfield = false;
- for (const key in fields) {
- const value = fields[key];
- const term = ToSearchTerm(value);
- if (term !== undefined) {
- const { suffix, value } = term;
- update[key + suffix] = value;
- dynfield = true;
- }
- }
- if (dynfield) {
- updates.push(update);
- }
- }
- await cursor?.forEach(updateDoc);
- const result = await log_execution({
- startMessage: `Dispatching updates for ${updates.length} documents`,
- endMessage: "Dispatched updates complete",
- action: () => Search.updateDocuments(updates),
- color: cyan
- });
- try {
- const { status } = JSON.parse(result).responseHeader;
- console.log(status ? red(`Failed with status code (${status})`) : green("Success!"));
- } catch {
- console.log(red("Error:"));
- console.log(result);
- console.log("\n");
- }
- await cursor?.close();
- process.exit(0);
-}
-
-update(); \ No newline at end of file
diff --git a/src/typings/index.d.ts b/src/typings/index.d.ts
index 281bb3217..850c533fc 100644
--- a/src/typings/index.d.ts
+++ b/src/typings/index.d.ts
@@ -4,6 +4,9 @@ declare module 'googlephotos';
declare module 'react-image-lightbox-with-rotate';
declare module 'cors';
+declare module 'webrtc-adapter';
+
+
declare module '@react-pdf/renderer' {
import * as React from 'react';