aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/Utils.ts151
-rw-r--r--src/client/Network.ts33
-rw-r--r--src/client/apis/GoogleAuthenticationManager.scss19
-rw-r--r--src/client/apis/GoogleAuthenticationManager.tsx140
-rw-r--r--src/client/apis/google_docs/GoogleApiClientUtils.ts273
-rw-r--r--src/client/apis/google_docs/GooglePhotosClientUtils.ts362
-rw-r--r--src/client/apis/youtube/YoutubeBox.tsx2
-rw-r--r--src/client/documents/DocumentTypes.ts7
-rw-r--r--src/client/documents/Documents.ts211
-rw-r--r--src/client/northstar/dash-nodes/HistogramBox.tsx2
-rw-r--r--src/client/util/DictationManager.ts73
-rw-r--r--src/client/util/DocumentManager.ts182
-rw-r--r--src/client/util/DragManager.ts44
-rw-r--r--src/client/util/DropConverter.ts46
-rw-r--r--src/client/util/History.ts6
-rw-r--r--src/client/util/Import & Export/DirectoryImportBox.scss6
-rw-r--r--src/client/util/Import & Export/DirectoryImportBox.tsx164
-rw-r--r--src/client/util/Import & Export/ImageUtils.ts31
-rw-r--r--src/client/util/LinkManager.ts21
-rw-r--r--src/client/util/ProsemirrorExampleTransfer.ts6
-rw-r--r--src/client/util/RichTextRules.ts78
-rw-r--r--src/client/util/RichTextSchema.tsx405
-rw-r--r--src/client/util/SearchUtil.ts48
-rw-r--r--src/client/util/SelectionManager.ts13
-rw-r--r--src/client/util/SharingManager.scss140
-rw-r--r--src/client/util/SharingManager.tsx300
-rw-r--r--src/client/util/TooltipTextMenu.tsx363
-rw-r--r--src/client/util/UndoManager.ts4
-rw-r--r--src/client/views/CollectionLinearView.scss73
-rw-r--r--src/client/views/CollectionLinearView.tsx110
-rw-r--r--src/client/views/DictationOverlay.tsx71
-rw-r--r--src/client/views/DocComponent.tsx84
-rw-r--r--src/client/views/DocumentButtonBar.tsx67
-rw-r--r--src/client/views/DocumentDecorations.scss31
-rw-r--r--src/client/views/DocumentDecorations.tsx138
-rw-r--r--src/client/views/EditableView.tsx6
-rw-r--r--src/client/views/GlobalKeyHandler.ts40
-rw-r--r--src/client/views/InkingCanvas.scss5
-rw-r--r--src/client/views/InkingCanvas.tsx16
-rw-r--r--src/client/views/InkingControl.tsx89
-rw-r--r--src/client/views/InkingStroke.tsx18
-rw-r--r--src/client/views/Main.scss241
-rw-r--r--src/client/views/Main.tsx34
-rw-r--r--src/client/views/MainOverlayTextBox.scss29
-rw-r--r--src/client/views/MainOverlayTextBox.tsx155
-rw-r--r--src/client/views/MainView.scss96
-rw-r--r--src/client/views/MainView.tsx667
-rw-r--r--src/client/views/MainViewModal.scss25
-rw-r--r--src/client/views/MainViewModal.tsx44
-rw-r--r--src/client/views/MainViewNotifs.scss18
-rw-r--r--src/client/views/MainViewNotifs.tsx32
-rw-r--r--src/client/views/MetadataEntryMenu.tsx2
-rw-r--r--src/client/views/OverlayView.tsx16
-rw-r--r--src/client/views/PreviewCursor.tsx18
-rw-r--r--src/client/views/ScriptingRepl.scss1
-rw-r--r--src/client/views/TemplateMenu.tsx9
-rw-r--r--src/client/views/collections/CollectionBaseView.scss12
-rw-r--r--src/client/views/collections/CollectionBaseView.tsx179
-rw-r--r--src/client/views/collections/CollectionDockingView.scss3
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx83
-rw-r--r--src/client/views/collections/CollectionMasonryViewFieldRow.tsx367
-rw-r--r--src/client/views/collections/CollectionPDFView.scss11
-rw-r--r--src/client/views/collections/CollectionPDFView.tsx37
-rw-r--r--src/client/views/collections/CollectionSchemaCells.tsx9
-rw-r--r--src/client/views/collections/CollectionSchemaMovableTableHOC.tsx2
-rw-r--r--src/client/views/collections/CollectionSchemaView.scss27
-rw-r--r--src/client/views/collections/CollectionSchemaView.tsx449
-rw-r--r--src/client/views/collections/CollectionStackingView.scss110
-rw-r--r--src/client/views/collections/CollectionStackingView.tsx220
-rw-r--r--src/client/views/collections/CollectionStackingViewFieldColumn.tsx164
-rw-r--r--src/client/views/collections/CollectionSubView.tsx88
-rw-r--r--src/client/views/collections/CollectionTreeView.scss8
-rw-r--r--src/client/views/collections/CollectionTreeView.tsx197
-rw-r--r--src/client/views/collections/CollectionVideoView.scss51
-rw-r--r--src/client/views/collections/CollectionVideoView.tsx115
-rw-r--r--src/client/views/collections/CollectionView.scss26
-rw-r--r--src/client/views/collections/CollectionView.tsx213
-rw-r--r--src/client/views/collections/CollectionViewChromes.tsx83
-rw-r--r--src/client/views/collections/ParentDocumentSelector.tsx2
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx3
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss4
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx84
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss5
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx69
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss6
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx251
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.scss7
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx76
-rw-r--r--src/client/views/linking/LinkFollowBox.tsx106
-rw-r--r--src/client/views/linking/LinkMenuItem.tsx5
-rw-r--r--src/client/views/nodes/Annotation.tsx117
-rw-r--r--src/client/views/nodes/AudioBox.scss136
-rw-r--r--src/client/views/nodes/AudioBox.tsx269
-rw-r--r--src/client/views/nodes/ButtonBox.scss2
-rw-r--r--src/client/views/nodes/ButtonBox.tsx30
-rw-r--r--src/client/views/nodes/CollectionFreeFormDocumentView.scss10
-rw-r--r--src/client/views/nodes/CollectionFreeFormDocumentView.tsx101
-rw-r--r--src/client/views/nodes/ColorBox.scss22
-rw-r--r--src/client/views/nodes/ColorBox.tsx45
-rw-r--r--src/client/views/nodes/ContentFittingDocumentView.scss23
-rw-r--r--src/client/views/nodes/ContentFittingDocumentView.tsx117
-rw-r--r--src/client/views/nodes/DocuLinkBox.scss8
-rw-r--r--src/client/views/nodes/DocuLinkBox.tsx84
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx79
-rw-r--r--src/client/views/nodes/DocumentView.scss129
-rw-r--r--src/client/views/nodes/DocumentView.tsx531
-rw-r--r--src/client/views/nodes/DragBox.tsx99
-rw-r--r--src/client/views/nodes/FieldView.tsx15
-rw-r--r--src/client/views/nodes/FontIconBox.scss (renamed from src/client/views/nodes/DragBox.scss)2
-rw-r--r--src/client/views/nodes/FontIconBox.tsx46
-rw-r--r--src/client/views/nodes/FormattedTextBox.scss44
-rw-r--r--src/client/views/nodes/FormattedTextBox.tsx848
-rw-r--r--src/client/views/nodes/FormattedTextBoxComment.tsx98
-rw-r--r--src/client/views/nodes/IconBox.tsx12
-rw-r--r--src/client/views/nodes/ImageBox.scss133
-rw-r--r--src/client/views/nodes/ImageBox.tsx271
-rw-r--r--src/client/views/nodes/KeyValueBox.scss1
-rw-r--r--src/client/views/nodes/KeyValueBox.tsx27
-rw-r--r--src/client/views/nodes/KeyValuePair.tsx8
-rw-r--r--src/client/views/nodes/PDFBox.scss36
-rw-r--r--src/client/views/nodes/PDFBox.tsx151
-rw-r--r--src/client/views/nodes/PresBox.scss33
-rw-r--r--src/client/views/nodes/PresBox.tsx535
-rw-r--r--src/client/views/nodes/QueryBox.scss0
-rw-r--r--src/client/views/nodes/QueryBox.tsx35
-rw-r--r--src/client/views/nodes/VideoBox.scss58
-rw-r--r--src/client/views/nodes/VideoBox.tsx200
-rw-r--r--src/client/views/nodes/WebBox.tsx82
-rw-r--r--src/client/views/pdf/Annotation.scss3
-rw-r--r--src/client/views/pdf/Annotation.tsx40
-rw-r--r--src/client/views/pdf/PDFMenu.scss2
-rw-r--r--src/client/views/pdf/PDFMenu.tsx10
-rw-r--r--src/client/views/pdf/PDFViewer.scss27
-rw-r--r--src/client/views/pdf/PDFViewer.tsx433
-rw-r--r--src/client/views/presentationview/PresElementBox.scss84
-rw-r--r--src/client/views/presentationview/PresElementBox.tsx224
-rw-r--r--src/client/views/presentationview/PresentationElement.tsx426
-rw-r--r--src/client/views/presentationview/PresentationList.tsx74
-rw-r--r--src/client/views/presentationview/PresentationModeMenu.scss30
-rw-r--r--src/client/views/presentationview/PresentationModeMenu.tsx105
-rw-r--r--src/client/views/presentationview/PresentationView.scss113
-rw-r--r--src/client/views/search/FilterBox.tsx4
-rw-r--r--src/client/views/search/IconBar.tsx16
-rw-r--r--src/client/views/search/IconButton.scss2
-rw-r--r--src/client/views/search/SearchBox.scss22
-rw-r--r--src/client/views/search/SearchBox.tsx32
-rw-r--r--src/client/views/search/SearchItem.scss50
-rw-r--r--src/client/views/search/SearchItem.tsx43
-rw-r--r--src/extensions/ArrayExtensions.ts37
-rw-r--r--src/extensions/General/Extensions.ts9
-rw-r--r--src/extensions/General/ExtensionsTypings.ts8
-rw-r--r--src/extensions/StringExtensions.ts17
-rw-r--r--src/new_fields/Doc.ts228
-rw-r--r--src/new_fields/InkField.ts6
-rw-r--r--src/new_fields/RichTextField.ts49
-rw-r--r--src/new_fields/RichTextUtils.ts519
-rw-r--r--src/new_fields/SchemaHeaderField.ts3
-rw-r--r--src/new_fields/URLField.ts2
-rw-r--r--src/new_fields/documentSchemas.ts59
-rw-r--r--src/server/DashUploadUtils.ts200
-rw-r--r--src/server/Message.ts1
-rw-r--r--src/server/PdfTypes.ts21
-rw-r--r--src/server/RouteStore.ts9
-rw-r--r--src/server/RouteSubscriber.ts26
-rw-r--r--src/server/apis/google/GoogleApiServerUtils.ts233
-rw-r--r--src/server/apis/google/GooglePhotosUploadUtils.ts170
-rw-r--r--src/server/apis/google/SharedTypes.ts21
-rw-r--r--src/server/authentication/config/passport.ts13
-rw-r--r--src/server/authentication/models/current_user_utils.ts263
-rw-r--r--src/server/credentials/google_docs_credentials.json12
-rw-r--r--src/server/credentials/google_docs_token.json1
-rw-r--r--src/server/database.ts430
-rw-r--r--src/server/index.ts573
-rw-r--r--src/server/updateSearch.ts123
-rw-r--r--src/typings/index.d.ts632
175 files changed, 10489 insertions, 7690 deletions
diff --git a/src/Utils.ts b/src/Utils.ts
index 2b00a6530..7bb025e49 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -3,21 +3,20 @@ import v5 = require("uuid/v5");
import { Socket } from 'socket.io';
import { Message } from './server/Message';
import { RouteStore } from './server/RouteStore';
-import requestPromise = require('request-promise');
-export class Utils {
+export namespace Utils {
- public static DRAG_THRESHOLD = 4;
+ export const DRAG_THRESHOLD = 4;
- public static GenerateGuid(): string {
+ export function GenerateGuid(): string {
return v4();
}
- public static GenerateDeterministicGuid(seed: string): string {
+ export function GenerateDeterministicGuid(seed: string): string {
return v5(seed, v5.URL);
}
- public static GetScreenTransform(ele?: HTMLElement): { scale: number, translateX: number, translateY: number } {
+ export function GetScreenTransform(ele?: HTMLElement): { scale: number, translateX: number, translateY: number } {
if (!ele) {
return { scale: 1, translateX: 1, translateY: 1 };
}
@@ -34,15 +33,23 @@ export class Utils {
* requested extension
* @param extension the specified sub-path to append to the window origin
*/
- public static prepend(extension: string): string {
+ export function prepend(extension: string): string {
return window.location.origin + extension;
}
- public static CorsProxy(url: string): string {
- return this.prepend(RouteStore.corsProxy + "/") + encodeURIComponent(url);
+ export function fileUrl(filename: string): string {
+ return prepend(`/files/${filename}`);
}
- public static CopyText(text: string) {
+ export function shareUrl(documentId: string): string {
+ return prepend(`/doc/${documentId}?sharing=true`);
+ }
+
+ export function CorsProxy(url: string): string {
+ return prepend(RouteStore.corsProxy + "/") + encodeURIComponent(url);
+ }
+
+ export function CopyText(text: string) {
var textArea = document.createElement("textarea");
textArea.value = text;
document.body.appendChild(textArea);
@@ -54,22 +61,23 @@ export class Utils {
document.body.removeChild(textArea);
}
- public static fromRGBAstr(rgba: string) {
- let rm = rgba.match(/rgb[a]?\(([0-9]+)/);
+ export function fromRGBAstr(rgba: string) {
+ let rm = rgba.match(/rgb[a]?\(([ 0-9]+)/);
let r = rm ? Number(rm[1]) : 0;
- let gm = rgba.match(/rgb[a]?\([0-9]+,([0-9]+)/);
+ let gm = rgba.match(/rgb[a]?\([ 0-9]+,([ 0-9]+)/);
let g = gm ? Number(gm[1]) : 0;
- let bm = rgba.match(/rgb[a]?\([0-9]+,[0-9]+,([0-9]+)/);
+ let bm = rgba.match(/rgb[a]?\([ 0-9]+,[ 0-9]+,([ 0-9]+)/);
let b = bm ? Number(bm[1]) : 0;
- let am = rgba.match(/rgba?\([0-9]+,[0-9]+,[0-9]+,([0-9]+)/);
- let a = am ? Number(am[1]) : 0;
+ let am = rgba.match(/rgba?\([ 0-9]+,[ 0-9]+,[ 0-9]+,([ .0-9]+)/);
+ let a = am ? Number(am[1]) : 1;
return { r: r, g: g, b: b, a: a };
}
- public static toRGBAstr(col: { r: number, g: number, b: number, a?: number }) {
+
+ export function toRGBAstr(col: { r: number, g: number, b: number, a?: number }) {
return "rgba(" + col.r + "," + col.g + "," + col.b + (col.a !== undefined ? "," + col.a : "") + ")";
}
- public static HSLtoRGB(h: number, s: number, l: number) {
+ export function HSLtoRGB(h: number, s: number, l: number) {
// Must be fractions of 1
// s /= 100;
// l /= 100;
@@ -99,7 +107,7 @@ export class Utils {
return { r: r, g: g, b: b };
}
- public static RGBToHSL(r: number, g: number, b: number) {
+ export function RGBToHSL(r: number, g: number, b: number) {
// Make r, g, and b fractions of 1
r /= 255;
g /= 255;
@@ -141,7 +149,30 @@ export class Utils {
}
- public static GetClipboardText(): string {
+ export function clamp(n: number, lower: number, upper: number) {
+ return Math.max(lower, Math.min(upper, n));
+ }
+
+ export function getNearestPointInPerimeter(l: number, t: number, w: number, h: number, x: number, y: number) {
+ var r = l + w,
+ b = t + h;
+
+ var x = clamp(x, l, r),
+ y = clamp(y, t, b);
+
+ var dl = Math.abs(x - l),
+ dr = Math.abs(x - r),
+ dt = Math.abs(y - t),
+ db = Math.abs(y - b);
+
+ var m = Math.min(dl, dr, dt, db);
+
+ return (m === dt) ? [x, t] :
+ (m === db) ? [x, b] :
+ (m === dl) ? [l, y] : [r, y];
+ }
+
+ export function GetClipboardText(): string {
var textArea = document.createElement("textarea");
document.body.appendChild(textArea);
textArea.focus();
@@ -154,51 +185,53 @@ export class Utils {
return val;
}
- public static loggingEnabled: Boolean = false;
- public static logFilter: number | undefined = undefined;
- private static log(prefix: string, messageName: string, message: any, receiving: boolean) {
- if (!this.loggingEnabled) {
+ export const loggingEnabled: Boolean = false;
+ export const logFilter: number | undefined = undefined;
+
+ function log(prefix: string, messageName: string, message: any, receiving: boolean) {
+ if (!loggingEnabled) {
return;
}
message = message || {};
- if (this.logFilter !== undefined && this.logFilter !== message.type) {
+ if (logFilter !== undefined && logFilter !== message.type) {
return;
}
let idString = (message.id || "").padStart(36, ' ');
prefix = prefix.padEnd(16, ' ');
console.log(`${prefix}: ${idString}, ${receiving ? 'receiving' : 'sending'} ${messageName} with data ${JSON.stringify(message)}`);
}
- private static loggingCallback(prefix: string, func: (args: any) => any, messageName: string) {
+
+ function loggingCallback(prefix: string, func: (args: any) => any, messageName: string) {
return (args: any) => {
- this.log(prefix, messageName, args, true);
+ log(prefix, messageName, args, true);
func(args);
};
}
- public static Emit<T>(socket: Socket | SocketIOClient.Socket, message: Message<T>, args: T) {
- this.log("Emit", message.Name, args, false);
+ export function Emit<T>(socket: Socket | SocketIOClient.Socket, message: Message<T>, args: T) {
+ log("Emit", message.Name, args, false);
socket.emit(message.Message, args);
}
- public static EmitCallback<T>(socket: Socket | SocketIOClient.Socket, message: Message<T>, args: T): Promise<any>;
- public static EmitCallback<T>(socket: Socket | SocketIOClient.Socket, message: Message<T>, args: T, fn: (args: any) => any): void;
- public static EmitCallback<T>(socket: Socket | SocketIOClient.Socket, message: Message<T>, args: T, fn?: (args: any) => any): void | Promise<any> {
- this.log("Emit", message.Name, args, false);
+ export function EmitCallback<T>(socket: Socket | SocketIOClient.Socket, message: Message<T>, args: T): Promise<any>;
+ export function EmitCallback<T>(socket: Socket | SocketIOClient.Socket, message: Message<T>, args: T, fn: (args: any) => any): void;
+ export function EmitCallback<T>(socket: Socket | SocketIOClient.Socket, message: Message<T>, args: T, fn?: (args: any) => any): void | Promise<any> {
+ log("Emit", message.Name, args, false);
if (fn) {
- socket.emit(message.Message, args, this.loggingCallback('Receiving', fn, message.Name));
+ socket.emit(message.Message, args, loggingCallback('Receiving', fn, message.Name));
} else {
- return new Promise<any>(res => socket.emit(message.Message, args, this.loggingCallback('Receiving', res, message.Name)));
+ return new Promise<any>(res => socket.emit(message.Message, args, loggingCallback('Receiving', res, message.Name)));
}
}
- public static AddServerHandler<T>(socket: Socket | SocketIOClient.Socket, message: Message<T>, handler: (args: T) => any) {
- socket.on(message.Message, this.loggingCallback('Incoming', handler, message.Name));
+ export function AddServerHandler<T>(socket: Socket | SocketIOClient.Socket, message: Message<T>, handler: (args: T) => any) {
+ socket.on(message.Message, loggingCallback('Incoming', handler, message.Name));
}
- public static AddServerHandlerCallback<T>(socket: Socket, message: Message<T>, handler: (args: [T, (res: any) => any]) => any) {
+ export function AddServerHandlerCallback<T>(socket: Socket, message: Message<T>, handler: (args: [T, (res: any) => any]) => any) {
socket.on(message.Message, (arg: T, fn: (res: any) => any) => {
- this.log('S receiving', message.Name, arg, true);
- handler([arg, this.loggingCallback('S sending', fn, message.Name)]);
+ log('S receiving', message.Name, arg, true);
+ handler([arg, loggingCallback('S sending', fn, message.Name)]);
});
}
}
@@ -256,6 +289,8 @@ export function percent2frac(percent: string) {
export function numberRange(num: number) { return Array.from(Array(num)).map((v, i) => i); }
+export function returnTransparent() { return "transparent"; }
+
export function returnTrue() { return true; }
export function returnFalse() { return false; }
@@ -299,16 +334,6 @@ export namespace JSONUtils {
}
-export function PostToServer(relativeRoute: string, body: any) {
- let options = {
- method: "POST",
- uri: Utils.prepend(relativeRoute),
- json: true,
- body: body
- };
- return requestPromise.post(options);
-}
-
const easeInOutQuad = (currentTime: number, start: number, change: number, duration: number) => {
let newCurrentTime = currentTime / (duration / 2);
@@ -320,7 +345,7 @@ const easeInOutQuad = (currentTime: number, start: number, change: number, durat
return (-change / 2) * (newCurrentTime * (newCurrentTime - 2) - 1) + start;
};
-export default function smoothScroll(duration: number, element: HTMLElement, to: number) {
+export function smoothScroll(duration: number, element: HTMLElement, to: number) {
const start = element.scrollTop;
const change = to - start;
const startDate = new Date().getTime();
@@ -337,4 +362,28 @@ export default function smoothScroll(duration: number, element: HTMLElement, to:
}
};
animateScroll();
+}
+export function addStyleSheet(styleType: string = "text/css") {
+ let style = document.createElement("style");
+ style.type = styleType;
+ var sheets = document.head.appendChild(style);
+ return (sheets as any).sheet;
+}
+export function addStyleSheetRule(sheet: any, selector: any, css: any) {
+ var propText = typeof css === "string" ? css : Object.keys(css).map(p => p + ":" + (p === "content" ? "'" + css[p] + "'" : css[p])).join(";");
+ return sheet.insertRule("." + selector + "{" + propText + "}", sheet.cssRules.length);
+}
+export function removeStyleSheetRule(sheet: any, rule: number) {
+ if (sheet.rules.length) {
+ sheet.removeRule(rule);
+ return true;
+ }
+ return false;
+}
+export function clearStyleSheetRules(sheet: any) {
+ if (sheet.rules.length) {
+ numberRange(sheet.rules.length).map(n => sheet.removeRule(0));
+ return true;
+ }
+ return false;
} \ No newline at end of file
diff --git a/src/client/Network.ts b/src/client/Network.ts
new file mode 100644
index 000000000..75ccb5e99
--- /dev/null
+++ b/src/client/Network.ts
@@ -0,0 +1,33 @@
+import { Utils } from "../Utils";
+import { CurrentUserUtils } from "../server/authentication/models/current_user_utils";
+import requestPromise = require('request-promise');
+
+export namespace Identified {
+
+ export async function FetchFromServer(relativeRoute: string) {
+ return (await fetch(relativeRoute, { headers: { userId: CurrentUserUtils.id } })).text();
+ }
+
+ export async function PostToServer(relativeRoute: string, body?: any) {
+ let options = {
+ uri: Utils.prepend(relativeRoute),
+ method: "POST",
+ headers: { userId: CurrentUserUtils.id },
+ body,
+ json: true
+ };
+ return requestPromise.post(options);
+ }
+
+ export async function PostFormDataToServer(relativeRoute: string, formData: FormData) {
+ const parameters = {
+ method: 'POST',
+ headers: { userId: CurrentUserUtils.id },
+ body: formData,
+ };
+ const response = await fetch(relativeRoute, parameters);
+ const text = await response.json();
+ return text;
+ }
+
+} \ No newline at end of file
diff --git a/src/client/apis/GoogleAuthenticationManager.scss b/src/client/apis/GoogleAuthenticationManager.scss
new file mode 100644
index 000000000..13bde822d
--- /dev/null
+++ b/src/client/apis/GoogleAuthenticationManager.scss
@@ -0,0 +1,19 @@
+.authorize-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+
+ .paste-target {
+ padding: 5px;
+ width: 100%;
+ }
+
+ .avatar {
+ border-radius: 50%;
+ }
+
+ .welcome {
+ font-style: italic;
+ margin-top: 15px;
+ }
+} \ No newline at end of file
diff --git a/src/client/apis/GoogleAuthenticationManager.tsx b/src/client/apis/GoogleAuthenticationManager.tsx
new file mode 100644
index 000000000..01dac3996
--- /dev/null
+++ b/src/client/apis/GoogleAuthenticationManager.tsx
@@ -0,0 +1,140 @@
+import { observable, action, reaction, runInAction } from "mobx";
+import { observer } from "mobx-react";
+import * as React from "react";
+import MainViewModal from "../views/MainViewModal";
+import { Opt } from "../../new_fields/Doc";
+import { Identified } from "../Network";
+import { RouteStore } from "../../server/RouteStore";
+import "./GoogleAuthenticationManager.scss";
+
+const AuthenticationUrl = "https://accounts.google.com/o/oauth2/v2/auth";
+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 authenticationCode: Opt<string> = undefined;
+ @observable private clickedState = false;
+ @observable private success: Opt<boolean> = undefined;
+ @observable private displayLauncher = true;
+ @observable private avatar: Opt<string> = undefined;
+ @observable private username: Opt<string> = undefined;
+
+ private set isOpen(value: boolean) {
+ runInAction(() => this.openState = value);
+ }
+
+ private set hasBeenClicked(value: boolean) {
+ runInAction(() => this.clickedState = value);
+ }
+
+ public fetchOrGenerateAccessToken = async () => {
+ let response = await Identified.FetchFromServer(RouteStore.readGoogleAccessToken);
+ // if this is an authentication url, activate the UI to register the new access token
+ if (new RegExp(AuthenticationUrl).test(response)) {
+ this.isOpen = true;
+ this.authenticationLink = response;
+ return new Promise<string>(async resolve => {
+ const disposer = reaction(
+ () => this.authenticationCode,
+ authenticationCode => {
+ if (authenticationCode) {
+ Identified.PostToServer(RouteStore.writeGoogleAccessToken, { authenticationCode }).then(
+ ({ access_token, avatar, name }) => {
+ runInAction(() => {
+ this.avatar = avatar;
+ this.username = name;
+ });
+ this.beginFadeout();
+ disposer();
+ resolve(access_token);
+ },
+ action(() => {
+ this.hasBeenClicked = false;
+ this.success = false;
+ })
+ );
+ }
+ }
+ );
+ });
+ }
+ // otherwise, we already have a valid, stored access token
+ return response;
+ }
+
+ beginFadeout = action(() => {
+ this.success = true;
+ this.authenticationCode = undefined;
+ this.displayLauncher = false;
+ this.hasBeenClicked = false;
+ setTimeout(action(() => {
+ this.isOpen = false;
+ setTimeout(action(() => {
+ this.success = undefined;
+ this.displayLauncher = true;
+ this.avatar = undefined;
+ this.username = undefined;
+ }), 500);
+ }), 3000);
+ });
+
+ constructor(props: {}) {
+ super(props);
+ 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}
+ style={{ marginBottom: this.clickedState ? 15 : 0 }}
+ >Authorize a Google account...</button> : (null)}
+ {this.clickedState ? <input
+ className={'paste-target'}
+ onChange={this.handlePaste}
+ placeholder={prompt}
+ /> : (null)}
+ {this.avatar ? <img
+ className={'avatar'}
+ src={this.avatar}
+ /> : (null)}
+ {this.username ? <span
+ className={'welcome'}
+ >Welcome to Dash, {this.username}
+ </span> : (null)}
+ </div>
+ );
+ }
+
+ private get dialogueBoxStyle() {
+ const borderColor = this.success === undefined ? "black" : this.success ? "green" : "red";
+ return { borderColor, transition: "0.2s borderColor ease" };
+ }
+
+ render() {
+ return (
+ <MainViewModal
+ isDisplayed={this.openState}
+ interactive={true}
+ contents={this.renderPrompt}
+ overlayDisplayedOpacity={0.9}
+ dialogueBoxStyle={this.dialogueBoxStyle}
+ />
+ );
+ }
+
+} \ 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 798886def..1cf01fc3d 100644
--- a/src/client/apis/google_docs/GoogleApiClientUtils.ts
+++ b/src/client/apis/google_docs/GoogleApiClientUtils.ts
@@ -1,113 +1,136 @@
import { docs_v1, slides_v1 } from "googleapis";
-import { PostToServer } from "../../../Utils";
import { RouteStore } from "../../../server/RouteStore";
import { Opt } from "../../../new_fields/Doc";
import { isArray } from "util";
+import { EditorState } from "prosemirror-state";
+import { Identified } from "../../Network";
export const Pulls = "googleDocsPullCount";
export const Pushes = "googleDocsPushCount";
export namespace GoogleApiClientUtils {
- export enum Service {
- Documents = "Documents",
- Slides = "Slides"
- }
-
export enum Actions {
Create = "create",
Retrieve = "retrieve",
Update = "update"
}
- export enum WriteMode {
- Insert,
- Replace
- }
-
- export type Identifier = string;
- export type Reference = Identifier | CreateOptions;
- export type TextContent = string | string[];
- export type IdHandler = (id: Identifier) => any;
- export type CreationResult = Opt<Identifier>;
- export type ReadLinesResult = Opt<{ title?: string, bodyLines?: string[] }>;
- export type ReadResult = { title?: string, body?: string };
+ export namespace Docs {
- export interface CreateOptions {
- service: Service;
- title?: string; // if excluded, will use a default title annotated with the current date
- }
+ export type RetrievalResult = Opt<docs_v1.Schema$Document>;
+ export type UpdateResult = Opt<docs_v1.Schema$BatchUpdateDocumentResponse>;
- export interface RetrieveOptions {
- service: Service;
- identifier: Identifier;
- }
+ export interface UpdateOptions {
+ documentId: DocumentId;
+ requests: docs_v1.Schema$Request[];
+ }
- export interface ReadOptions {
- identifier: Identifier;
- removeNewlines?: boolean;
- }
+ export enum WriteMode {
+ Insert,
+ Replace
+ }
- export interface WriteOptions {
- mode: WriteMode;
- content: TextContent;
- reference: Reference;
- index?: number; // if excluded, will compute the last index of the document and append the content there
- }
+ export type DocumentId = string;
+ export type Reference = DocumentId | CreateOptions;
+ export interface Content {
+ text: string | string[];
+ requests: docs_v1.Schema$Request[];
+ }
+ export type IdHandler = (id: DocumentId) => any;
+ export type CreationResult = Opt<DocumentId>;
+ export type ReadLinesResult = Opt<{ title?: string, bodyLines?: string[] }>;
+ export type ReadResult = { title: string, body: string };
+ export interface ImportResult {
+ title: string;
+ text: string;
+ state: EditorState;
+ }
- /**
- * After following the authentication routine, which connects this API call to the current signed in account
- * and grants the appropriate permissions, this function programmatically creates an arbitrary Google Doc which
- * should appear in the user's Google Doc library instantaneously.
- *
- * @param options the title to assign to the new document, and the information necessary
- * to store the new documentId returned from the creation process
- * @returns the documentId of the newly generated document, or undefined if the creation process fails.
- */
- export const create = async (options: CreateOptions): Promise<CreationResult> => {
- const path = `${RouteStore.googleDocs}/${options.service}/${Actions.Create}`;
- const parameters = {
- requestBody: {
- title: options.title || `Dash Export (${new Date().toDateString()})`
- }
- };
- try {
- const schema: any = await PostToServer(path, parameters);
- let key = ["document", "presentation"].find(prefix => `${prefix}Id` in schema) + "Id";
- return schema[key];
- } catch {
- return undefined;
+ export interface CreateOptions {
+ title?: string; // if excluded, will use a default title annotated with the current date
}
- };
- export namespace Docs {
+ export interface RetrieveOptions {
+ documentId: DocumentId;
+ }
- export type RetrievalResult = Opt<docs_v1.Schema$Document | slides_v1.Schema$Presentation>;
- export type UpdateResult = Opt<docs_v1.Schema$BatchUpdateDocumentResponse>;
+ export interface ReadOptions {
+ documentId: DocumentId;
+ removeNewlines?: boolean;
+ }
- export interface UpdateOptions {
- documentId: Identifier;
- requests: docs_v1.Schema$Request[];
+ export interface WriteOptions {
+ mode: WriteMode;
+ content: Content;
+ reference: Reference;
+ index?: number; // if excluded, will compute the last index of the document and append the content there
}
+ /**
+ * After following the authentication routine, which connects this API call to the current signed in account
+ * and grants the appropriate permissions, this function programmatically creates an arbitrary Google Doc which
+ * should appear in the user's Google Doc library instantaneously.
+ *
+ * @param options the title to assign to the new document, and the information necessary
+ * to store the new documentId returned from the creation process
+ * @returns the documentId of the newly generated document, or undefined if the creation process fails.
+ */
+ export const create = async (options: CreateOptions): Promise<CreationResult> => {
+ const path = `${RouteStore.googleDocs}/Documents/${Actions.Create}`;
+ const parameters = {
+ requestBody: {
+ title: options.title || `Dash Export (${new Date().toDateString()})`
+ }
+ };
+ try {
+ const schema: docs_v1.Schema$Document = await Identified.PostToServer(path, parameters);
+ return schema.documentId;
+ } catch {
+ return undefined;
+ }
+ };
+
export namespace Utils {
- export const extractText = (document: docs_v1.Schema$Document, removeNewlines = false): string => {
- const fragments: string[] = [];
+ export type ExtractResult = { text: string, paragraphs: DeconstructedParagraph[] };
+ export const extractText = (document: docs_v1.Schema$Document, removeNewlines = false): ExtractResult => {
+ let paragraphs = extractParagraphs(document);
+ let text = paragraphs.map(paragraph => paragraph.contents.filter(content => !("inlineObjectId" in content)).map(run => run as docs_v1.Schema$TextRun).join("")).join("");
+ text = text.substring(0, text.length - 1);
+ removeNewlines && text.ReplaceAll("\n", "");
+ return { text, paragraphs };
+ };
+
+ export type ContentArray = (docs_v1.Schema$TextRun | docs_v1.Schema$InlineObjectElement)[];
+ export type DeconstructedParagraph = { contents: ContentArray, bullet: Opt<number> };
+ const extractParagraphs = (document: docs_v1.Schema$Document, filterEmpty = true): DeconstructedParagraph[] => {
+ const fragments: DeconstructedParagraph[] = [];
if (document.body && document.body.content) {
for (const element of document.body.content) {
- if (element.paragraph && element.paragraph.elements) {
- for (const inner of element.paragraph.elements) {
- if (inner && inner.textRun) {
- const fragment = inner.textRun.content;
- fragment && fragments.push(fragment);
+ let runs: ContentArray = [];
+ let bullet: Opt<number>;
+ if (element.paragraph) {
+ if (element.paragraph.elements) {
+ for (const inner of element.paragraph.elements) {
+ if (inner) {
+ if (inner.textRun) {
+ let run = inner.textRun;
+ (run.content || !filterEmpty) && runs.push(inner.textRun);
+ } else if (inner.inlineObjectElement) {
+ runs.push(inner.inlineObjectElement);
+ }
+ }
}
}
+ if (element.paragraph.bullet) {
+ bullet = element.paragraph.bullet.nestingLevel || 0;
+ }
}
+ (runs.length || !filterEmpty) && fragments.push({ contents: runs, bullet });
}
}
- const text = fragments.join("");
- return removeNewlines ? text.ReplaceAll("\n", "") : text;
+ return fragments;
};
export const endOf = (schema: docs_v1.Schema$Document): number | undefined => {
@@ -130,27 +153,19 @@ export namespace GoogleApiClientUtils {
}
- const KeyMapping = new Map<Service, string>([
- [Service.Documents, "documentId"],
- [Service.Slides, "presentationId"]
- ]);
-
export const retrieve = async (options: RetrieveOptions): Promise<RetrievalResult> => {
- const path = `${RouteStore.googleDocs}/${options.service}/${Actions.Retrieve}`;
+ const path = `${RouteStore.googleDocs}/Documents/${Actions.Retrieve}`;
try {
- let parameters: any = {}, key: string | undefined;
- if ((key = KeyMapping.get(options.service))) {
- parameters[key] = options.identifier;
- const schema: RetrievalResult = await PostToServer(path, parameters);
- return schema;
- }
+ const parameters = { documentId: options.documentId };
+ const schema: RetrievalResult = await Identified.PostToServer(path, parameters);
+ return schema;
} catch {
return undefined;
}
};
export const update = async (options: UpdateOptions): Promise<UpdateResult> => {
- const path = `${RouteStore.googleDocs}/${Service.Documents}/${Actions.Update}`;
+ const path = `${RouteStore.googleDocs}/Documents/${Actions.Update}`;
const parameters = {
documentId: options.documentId,
requestBody: {
@@ -158,48 +173,56 @@ export namespace GoogleApiClientUtils {
}
};
try {
- const replies: UpdateResult = await PostToServer(path, parameters);
+ const replies: UpdateResult = await Identified.PostToServer(path, parameters);
return replies;
} catch {
return undefined;
}
};
- export const read = async (options: ReadOptions): Promise<ReadResult> => {
- return retrieve({ ...options, service: Service.Documents }).then(document => {
- let result: ReadResult = {};
+ export const read = async (options: ReadOptions): Promise<Opt<ReadResult>> => {
+ return retrieve({ documentId: options.documentId }).then(document => {
if (document) {
- let title = document.title;
- let body = Utils.extractText(document, options.removeNewlines);
- result = { title, body };
+ let title = document.title!;
+ let body = Utils.extractText(document, options.removeNewlines).text;
+ return { title, body };
}
- return result;
});
};
- export const readLines = async (options: ReadOptions): Promise<ReadLinesResult> => {
- return retrieve({ ...options, service: Service.Documents }).then(document => {
- let result: ReadLinesResult = {};
+ export const readLines = async (options: ReadOptions): Promise<Opt<ReadLinesResult>> => {
+ return retrieve({ documentId: options.documentId }).then(document => {
if (document) {
let title = document.title;
- let bodyLines = Utils.extractText(document).split("\n");
+ let bodyLines = Utils.extractText(document).text.split("\n");
options.removeNewlines && (bodyLines = bodyLines.filter(line => line.length));
- result = { title, bodyLines };
+ return { title, bodyLines };
}
- return result;
});
};
+ export const setStyle = async (options: UpdateOptions) => {
+ let replies: any = await update({
+ documentId: options.documentId,
+ requests: options.requests
+ });
+ if ("errors" in replies) {
+ console.log("Write operation failed:");
+ console.log(replies.errors.map((error: any) => error.message));
+ }
+ return replies;
+ };
+
export const write = async (options: WriteOptions): Promise<UpdateResult> => {
const requests: docs_v1.Schema$Request[] = [];
- const identifier = await Utils.initialize(options.reference);
- if (!identifier) {
+ const documentId = await Utils.initialize(options.reference);
+ if (!documentId) {
return undefined;
}
let index = options.index;
const mode = options.mode;
if (!(index && mode === WriteMode.Insert)) {
- let schema = await retrieve({ identifier, service: Service.Documents });
+ let schema = await retrieve({ documentId });
if (!schema || !(index = Utils.endOf(schema))) {
return undefined;
}
@@ -215,7 +238,7 @@ export namespace GoogleApiClientUtils {
});
index = 1;
}
- const text = options.content;
+ const text = options.content.text;
text.length && requests.push({
insertText: {
text: isArray(text) ? text.join("\n") : text,
@@ -225,47 +248,15 @@ export namespace GoogleApiClientUtils {
if (!requests.length) {
return undefined;
}
- let replies: any = await update({ documentId: identifier, requests });
- let errors = "errors";
- if (errors in replies) {
+ requests.push(...options.content.requests);
+ let replies: any = await update({ documentId: documentId, requests });
+ if ("errors" in replies) {
console.log("Write operation failed:");
- console.log(replies[errors].map((error: any) => error.message));
+ console.log(replies.errors.map((error: any) => error.message));
}
return replies;
};
}
- export namespace Slides {
-
- export namespace Utils {
-
- export const extractTextBoxes = (slides: slides_v1.Schema$Page[]) => {
- slides.map(slide => {
- let elements = slide.pageElements;
- if (elements) {
- let textboxes: slides_v1.Schema$TextContent[] = [];
- for (let element of elements) {
- if (element && element.shape && element.shape.shapeType === "TEXT_BOX" && element.shape.text) {
- textboxes.push(element.shape.text);
- }
- }
- textboxes.map(text => {
- if (text.textElements) {
- text.textElements.map(element => {
-
- });
- }
- if (text.lists) {
-
- }
- });
- }
- });
- };
-
- }
-
- }
-
} \ No newline at end of file
diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
new file mode 100644
index 000000000..e93fa6eb4
--- /dev/null
+++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
@@ -0,0 +1,362 @@
+import { Utils } from "../../../Utils";
+import { RouteStore } from "../../../server/RouteStore";
+import { ImageField } from "../../../new_fields/URLField";
+import { Cast, StrCast } from "../../../new_fields/Types";
+import { Doc, Opt, DocListCastAsync } from "../../../new_fields/Doc";
+import { Id } from "../../../new_fields/FieldSymbols";
+import Photos = require('googlephotos');
+import { RichTextField } from "../../../new_fields/RichTextField";
+import { RichTextUtils } from "../../../new_fields/RichTextUtils";
+import { EditorState } from "prosemirror-state";
+import { FormattedTextBox } from "../../views/nodes/FormattedTextBox";
+import { Docs, DocumentOptions } from "../../documents/Documents";
+import { NewMediaItemResult, MediaItem } from "../../../server/apis/google/SharedTypes";
+import { AssertionError } from "assert";
+import { DocumentView } from "../../views/nodes/DocumentView";
+import { Identified } from "../../Network";
+import GoogleAuthenticationManager from "../GoogleAuthenticationManager";
+
+export namespace GooglePhotos {
+
+ const endpoint = async () => new Photos(await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken());
+
+ export enum MediaType {
+ ALL_MEDIA = 'ALL_MEDIA',
+ PHOTO = 'PHOTO',
+ VIDEO = 'VIDEO'
+ }
+
+ export type AlbumReference = { id: string } | { title: string };
+
+ export interface MediaInput {
+ url: string;
+ description: string;
+ }
+
+ export const ContentCategories = {
+ NONE: 'NONE',
+ LANDSCAPES: 'LANDSCAPES',
+ RECEIPTS: 'RECEIPTS',
+ CITYSCAPES: 'CITYSCAPES',
+ LANDMARKS: 'LANDMARKS',
+ SELFIES: 'SELFIES',
+ PEOPLE: 'PEOPLE',
+ PETS: 'PETS',
+ WEDDINGS: 'WEDDINGS',
+ BIRTHDAYS: 'BIRTHDAYS',
+ DOCUMENTS: 'DOCUMENTS',
+ TRAVEL: 'TRAVEL',
+ ANIMALS: 'ANIMALS',
+ FOOD: 'FOOD',
+ SPORT: 'SPORT',
+ NIGHT: 'NIGHT',
+ PERFORMANCES: 'PERFORMANCES',
+ WHITEBOARDS: 'WHITEBOARDS',
+ SCREENSHOTS: 'SCREENSHOTS',
+ UTILITY: 'UTILITY',
+ ARTS: 'ARTS',
+ CRAFTS: 'CRAFTS',
+ FASHION: 'FASHION',
+ HOUSES: 'HOUSES',
+ GARDENS: 'GARDENS',
+ FLOWERS: 'FLOWERS',
+ HOLIDAYS: 'HOLIDAYS'
+ };
+
+ export namespace Export {
+
+ export interface AlbumCreationResult {
+ albumId: string;
+ mediaItems: MediaItem[];
+ }
+
+ export interface AlbumCreationOptions {
+ collection: Doc;
+ title?: string;
+ descriptionKey?: string;
+ tag?: boolean;
+ }
+
+ export const CollectionToAlbum = async (options: AlbumCreationOptions): Promise<Opt<AlbumCreationResult>> => {
+ const { collection, title, descriptionKey, tag } = options;
+ const dataDocument = Doc.GetProto(collection);
+ const images = ((await DocListCastAsync(dataDocument.data)) || []).filter(doc => Cast(doc.data, ImageField));
+ if (!images || !images.length) {
+ return undefined;
+ }
+ const resolved = title ? title : (StrCast(collection.title) || `Dash Collection (${collection[Id]}`);
+ const { id, productUrl } = await Create.Album(resolved);
+ const response = await Transactions.UploadImages(images, { id }, descriptionKey);
+ if (response) {
+ const { results, failed } = response;
+ let index: Opt<number>;
+ while ((index = failed.pop()) !== undefined) {
+ Doc.RemoveDocFromList(dataDocument, "data", images.splice(index, 1)[0]);
+ }
+ const mediaItems: MediaItem[] = results.map(item => item.mediaItem);
+ if (mediaItems.length !== images.length) {
+ throw new AssertionError({ actual: mediaItems.length, expected: images.length });
+ }
+ const idMapping = new Doc;
+ for (let i = 0; i < images.length; i++) {
+ const image = Doc.GetProto(images[i]);
+ const mediaItem = mediaItems[i];
+ if (!mediaItem) {
+ continue;
+ }
+ image.googlePhotosId = mediaItem.id;
+ image.googlePhotosAlbumUrl = productUrl;
+ image.googlePhotosUrl = mediaItem.productUrl || mediaItem.baseUrl;
+ idMapping[mediaItem.id] = image;
+ }
+ collection.googlePhotosAlbumUrl = productUrl;
+ collection.googlePhotosIdMapping = idMapping;
+ if (tag) {
+ await Query.TagChildImages(collection);
+ }
+ collection.albumId = id;
+ Transactions.AddTextEnrichment(collection, `Find me at ${Utils.prepend(`/doc/${collection[Id]}?sharing=true`)}`);
+ return { albumId: id, mediaItems };
+ }
+ };
+
+ }
+
+ export namespace Import {
+
+ export type CollectionConstructor = (data: Array<Doc>, options: DocumentOptions, ...args: any) => Doc;
+
+ export const CollectionFromSearch = async (constructor: CollectionConstructor, requested: Opt<Partial<Query.SearchOptions>>): Promise<Doc> => {
+ let response = await Query.ContentSearch(requested);
+ let uploads = await Transactions.WriteMediaItemsToServer(response);
+ const children = uploads.map((upload: Transactions.UploadInformation) => {
+ let document = Docs.Create.ImageDocument(Utils.fileUrl(upload.fileNames.clean));
+ document.fillColumn = true;
+ document.contentSize = upload.contentSize;
+ return document;
+ });
+ const options = { width: 500, height: 500 };
+ return constructor(children, options);
+ };
+
+ }
+
+ export namespace Query {
+
+ const delimiter = ", ";
+ const comparator = (a: string, b: string) => (a < b) ? -1 : (a > b ? 1 : 0);
+
+ export const TagChildImages = async (collection: Doc) => {
+ const idMapping = await Cast(collection.googlePhotosIdMapping, Doc);
+ if (!idMapping) {
+ throw new Error("Appending image metadata requires that the targeted collection have already been mapped to an album!");
+ }
+ const tagMapping = new Map<string, string>();
+ const images = (await DocListCastAsync(collection.data))!.map(Doc.GetProto);
+ images && images.forEach(image => tagMapping.set(image[Id], ContentCategories.NONE));
+ const values = Object.values(ContentCategories);
+ for (let value of values) {
+ if (value !== ContentCategories.NONE) {
+ const results = await ContentSearch({ included: [value] });
+ if (results.mediaItems) {
+ const ids = results.mediaItems.map(item => item.id);
+ for (let id of ids) {
+ const image = await Cast(idMapping[id], Doc);
+ if (image) {
+ const key = image[Id];
+ const tags = tagMapping.get(key)!;
+ if (!tags.includes(value)) {
+ tagMapping.set(key, tags + delimiter + value);
+ }
+ }
+ }
+ }
+ }
+ }
+ images && images.forEach(image => {
+ const concatenated = tagMapping.get(image[Id])!;
+ const tags = concatenated.split(delimiter);
+ if (tags.length > 1) {
+ const cleaned = concatenated.replace(ContentCategories.NONE + delimiter, "");
+ image.googlePhotosTags = cleaned.split(delimiter).sort(comparator).join(delimiter);
+ } else {
+ image.googlePhotosTags = ContentCategories.NONE;
+ }
+ });
+
+ };
+
+ interface DateRange {
+ after: Date;
+ before: Date;
+ }
+
+ const DefaultSearchOptions: SearchOptions = {
+ pageSize: 50,
+ included: [],
+ excluded: [],
+ date: undefined,
+ includeArchivedMedia: true,
+ excludeNonAppCreatedData: false,
+ type: MediaType.ALL_MEDIA,
+ };
+
+ export interface SearchOptions {
+ pageSize: number;
+ included: string[];
+ excluded: string[];
+ date: Opt<Date | DateRange>;
+ includeArchivedMedia: boolean;
+ excludeNonAppCreatedData: boolean;
+ type: MediaType;
+ }
+
+ export interface SearchResponse {
+ mediaItems: any[];
+ nextPageToken: string;
+ }
+
+ export const AlbumSearch = async (albumId: string, pageSize = 100): Promise<MediaItem[]> => {
+ const photos = await endpoint();
+ let mediaItems: MediaItem[] = [];
+ let nextPageTokenStored: Opt<string> = undefined;
+ let found = 0;
+ do {
+ const response: any = await photos.mediaItems.search(albumId, pageSize, nextPageTokenStored);
+ mediaItems.push(...response.mediaItems);
+ nextPageTokenStored = response.nextPageToken;
+ } while (found);
+ return mediaItems;
+ };
+
+ export const ContentSearch = async (requested: Opt<Partial<SearchOptions>>): Promise<SearchResponse> => {
+ const options = requested || DefaultSearchOptions;
+ const photos = await endpoint();
+ const filters = new photos.Filters(options.includeArchivedMedia === undefined ? true : options.includeArchivedMedia);
+
+ const included = options.included || [];
+ const excluded = options.excluded || [];
+ const contentFilter = new photos.ContentFilter();
+ included.length && included.forEach(category => contentFilter.addIncludedContentCategories(category));
+ excluded.length && excluded.forEach(category => contentFilter.addExcludedContentCategories(category));
+ filters.setContentFilter(contentFilter);
+
+ const date = options.date;
+ if (date) {
+ const dateFilter = new photos.DateFilter();
+ if (date instanceof Date) {
+ dateFilter.addDate(date);
+ } else {
+ dateFilter.addRange(date.after, date.before);
+ }
+ filters.setDateFilter(dateFilter);
+ }
+
+ filters.setMediaTypeFilter(new photos.MediaTypeFilter(options.type || MediaType.ALL_MEDIA));
+
+ return new Promise<SearchResponse>(resolve => {
+ photos.mediaItems.search(filters, options.pageSize || 100).then(resolve);
+ });
+ };
+
+ export const GetImage = async (mediaItemId: string): Promise<Transactions.MediaItem> => {
+ return (await endpoint()).mediaItems.get(mediaItemId);
+ };
+
+ }
+
+ namespace Create {
+
+ export const Album = async (title: string) => {
+ return (await endpoint()).albums.create(title);
+ };
+
+ }
+
+ export namespace Transactions {
+
+ export interface UploadInformation {
+ mediaPaths: string[];
+ fileNames: { [key: string]: string };
+ contentSize?: number;
+ contentType?: string;
+ }
+
+ export interface MediaItem {
+ id: string;
+ filename: string;
+ baseUrl: string;
+ }
+
+ export const ListAlbums = async () => (await endpoint()).albums.list();
+
+ export const AddTextEnrichment = async (collection: Doc, content?: string) => {
+ const photos = await endpoint();
+ const albumId = StrCast(collection.albumId);
+ if (albumId && albumId.length) {
+ const enrichment = new photos.TextEnrichment(content || Utils.prepend("/doc/" + collection[Id]));
+ const position = new photos.AlbumPosition(photos.AlbumPosition.POSITIONS.FIRST_IN_ALBUM);
+ const enrichmentItem = await photos.albums.addEnrichment(albumId, enrichment, position);
+ if (enrichmentItem) {
+ return enrichmentItem.id;
+ }
+ }
+ };
+
+ export const WriteMediaItemsToServer = async (body: { mediaItems: any[] }): Promise<UploadInformation[]> => {
+ const uploads = await Identified.PostToServer(RouteStore.googlePhotosMediaDownload, body);
+ return uploads;
+ };
+
+ export const UploadThenFetch = async (sources: Doc[], album?: AlbumReference, descriptionKey = "caption") => {
+ const response = await UploadImages(sources, album, descriptionKey);
+ if (!response) {
+ return undefined;
+ }
+ const baseUrls: string[] = await Promise.all(response.results.map(item => {
+ return new Promise<string>(resolve => Query.GetImage(item.mediaItem.id).then(item => resolve(item.baseUrl)));
+ }));
+ return baseUrls;
+ };
+
+ export interface ImageUploadResults {
+ results: NewMediaItemResult[];
+ failed: number[];
+ }
+
+ export const UploadImages = async (sources: Doc[], album?: AlbumReference, descriptionKey = "caption"): Promise<Opt<ImageUploadResults>> => {
+ if (album && "title" in album) {
+ album = await Create.Album(album.title);
+ }
+ const media: MediaInput[] = [];
+ for (let source of sources) {
+ const data = Cast(Doc.GetProto(source).data, ImageField);
+ if (!data) {
+ return;
+ }
+ const url = data.url.href;
+ const target = Doc.MakeAlias(source);
+ const description = parseDescription(target, descriptionKey);
+ await DocumentView.makeCustomViewClicked(target, undefined);
+ media.push({ url, description });
+ }
+ if (media.length) {
+ const results = await Identified.PostToServer(RouteStore.googlePhotosMediaUpload, { media, album });
+ return results;
+ }
+ };
+
+ const parseDescription = (document: Doc, descriptionKey: string) => {
+ let description: string = Utils.prepend(`/doc/${document[Id]}?sharing=true`);
+ const target = document[descriptionKey];
+ if (typeof target === "string") {
+ description = target;
+ } else if (target instanceof RichTextField) {
+ description = RichTextUtils.ToPlainText(EditorState.fromJSON(FormattedTextBox.Instance.config, JSON.parse(target.Data)));
+ }
+ return description;
+ };
+
+ }
+
+} \ No newline at end of file
diff --git a/src/client/apis/youtube/YoutubeBox.tsx b/src/client/apis/youtube/YoutubeBox.tsx
index d73988bb8..bed812852 100644
--- a/src/client/apis/youtube/YoutubeBox.tsx
+++ b/src/client/apis/youtube/YoutubeBox.tsx
@@ -40,7 +40,7 @@ export class YoutubeBox extends React.Component<FieldViewProps> {
@observable curVideoTemplates: VideoTemplate[] = [];
- public static LayoutString() { return FieldView.LayoutString(YoutubeBox); }
+ public static LayoutString(fieldKey: string) { return FieldView.LayoutString(YoutubeBox, fieldKey); }
/**
* When component mounts, last search's results are laoded in based on the back up stored
diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts
index 381981e1b..12501065a 100644
--- a/src/client/documents/DocumentTypes.ts
+++ b/src/client/documents/DocumentTypes.ts
@@ -17,7 +17,12 @@ export enum DocumentType {
TEMPLATE = "template",
EXTENSION = "extension",
YOUTUBE = "youtube",
- DRAGBOX = "dragbox",
+ FONTICON = "fonticonbox",
PRES = "presentation",
LINKFOLLOW = "linkfollow",
+ PRESELEMENT = "preselement",
+ QUERY = "search",
+ COLOR = "color",
+ DOCULINK = "doculink",
+ PDFANNO = "pdfanno"
} \ No newline at end of file
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 64201f645..10879d1e3 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1,10 +1,8 @@
import { HistogramField } from "../northstar/dash-fields/HistogramField";
import { HistogramBox } from "../northstar/dash-nodes/HistogramBox";
import { HistogramOperation } from "../northstar/operations/HistogramOperation";
-import { CollectionPDFView } from "../views/collections/CollectionPDFView";
-import { CollectionVideoView } from "../views/collections/CollectionVideoView";
import { CollectionView } from "../views/collections/CollectionView";
-import { CollectionViewType } from "../views/collections/CollectionBaseView";
+import { CollectionViewType } from "../views/collections/CollectionView";
import { AudioBox } from "../views/nodes/AudioBox";
import { FormattedTextBox } from "../views/nodes/FormattedTextBox";
import { ImageBox } from "../views/nodes/ImageBox";
@@ -20,8 +18,8 @@ import { AttributeTransformationModel } from "../northstar/core/attribute/Attrib
import { AggregateFunction } from "../northstar/model/idea/idea";
import { MINIMIZED_ICON_SIZE } from "../views/globalCssVariables.scss";
import { IconBox } from "../views/nodes/IconBox";
-import { Field, Doc, Opt, DocListCastAsync } from "../../new_fields/Doc";
import { OmitKeys, JSONUtils } from "../../Utils";
+import { Field, Doc, Opt, DocListCastAsync } 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";
@@ -39,15 +37,17 @@ import { DocumentManager } from "../util/DocumentManager";
import DirectoryImportBox from "../util/Import & Export/DirectoryImportBox";
import { Scripting, CompileScript } from "../util/Scripting";
import { ButtonBox } from "../views/nodes/ButtonBox";
-import { DragBox } from "../views/nodes/DragBox";
+import { FontIconBox } from "../views/nodes/FontIconBox";
import { SchemaHeaderField, RandomPastel } from "../../new_fields/SchemaHeaderField";
import { PresBox } from "../views/nodes/PresBox";
-import { ComputedField } from "../../new_fields/ScriptField";
+import { ComputedField, ScriptField } from "../../new_fields/ScriptField";
import { ProxyField } from "../../new_fields/Proxy";
import { DocumentType } from "./DocumentTypes";
import { LinkFollowBox } from "../views/linking/LinkFollowBox";
-//import { PresBox } from "../views/nodes/PresBox";
-//import { PresField } from "../../new_fields/PresField";
+import { PresElementBox } from "../views/presentationview/PresElementBox";
+import { QueryBox } from "../views/nodes/QueryBox";
+import { ColorBox } from "../views/nodes/ColorBox";
+import { DocuLinkBox } from "../views/nodes/DocuLinkBox";
var requestImageSize = require('../util/request-image-size');
var path = require('path');
@@ -65,23 +65,48 @@ export interface DocumentOptions {
panY?: number;
page?: number;
scale?: number;
+ fitWidth?: boolean;
+ 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;
- isTemplate?: boolean;
+ hideHeadings?: boolean; // whether stacking view column headings should be hidden
+ isTemplateField?: boolean;
+ isTemplateDoc?: boolean;
templates?: List<string>;
viewType?: number;
backgroundColor?: string;
+ ignoreClick?: boolean;
+ lockedPosition?: boolean;
opacity?: number;
defaultBackgroundColor?: string;
dropAction?: dropActionType;
- backgroundLayout?: string;
chromeStatus?: string;
+ 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
schemaColumns?: List<SchemaHeaderField>;
dockingConfig?: string;
autoHeight?: boolean;
+ 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;
+ unchecked?: ScriptField; // returns whether a check box is unchecked
+ activePen?: Doc; // which pen document is currently active (used as the radio button state for the 'unhecked' pen tool scripts)
+ onClick?: ScriptField;
+ 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
+ icon?: string;
+ 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
+ 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.
// [key: string]: Opt<Field>;
}
@@ -95,89 +120,95 @@ export namespace Docs {
export namespace Prototypes {
- type LayoutSource = { LayoutString: (ext?: string) => string };
- type CollectionLayoutSource = { LayoutString: (fieldStr: string, fieldExt?: string) => string };
- type CollectionViewType = [CollectionLayoutSource, string, string?];
+ type LayoutSource = { LayoutString: (key: string) => string };
type PrototypeTemplate = {
layout: {
view: LayoutSource,
- ext?: string, // optional extension field for layout source
- collectionView?: CollectionViewType
+ dataField: string
},
options?: Partial<DocumentOptions>
};
type TemplateMap = Map<DocumentType, PrototypeTemplate>;
type PrototypeMap = Map<DocumentType, Doc>;
const data = "data";
- const anno = "annotations";
const TemplateMap: TemplateMap = new Map([
[DocumentType.TEXT, {
- layout: { view: FormattedTextBox },
+ layout: { view: FormattedTextBox, dataField: data },
options: { height: 150, backgroundColor: "#f1efeb", defaultBackgroundColor: "#f1efeb" }
}],
[DocumentType.HIST, {
- layout: { view: HistogramBox, collectionView: [CollectionView, data] as CollectionViewType },
+ layout: { view: HistogramBox, dataField: data },
options: { height: 300, backgroundColor: "black" }
}],
+ [DocumentType.QUERY, {
+ layout: { view: QueryBox, dataField: data },
+ options: { width: 400 }
+ }],
+ [DocumentType.COLOR, {
+ layout: { view: ColorBox, dataField: data },
+ options: { nativeWidth: 220, nativeHeight: 300 }
+ }],
[DocumentType.IMG, {
- layout: { view: ImageBox, collectionView: [CollectionView, data, anno] as CollectionViewType },
- options: { curPage: 0 }
+ layout: { view: ImageBox, dataField: data },
+ options: {}
}],
[DocumentType.WEB, {
- layout: { view: WebBox, collectionView: [CollectionView, data, anno] as CollectionViewType },
+ layout: { view: WebBox, dataField: data },
options: { height: 300 }
}],
[DocumentType.COL, {
- layout: { view: CollectionView },
+ layout: { view: CollectionView, dataField: data },
options: { panX: 0, panY: 0, scale: 1, width: 500, height: 500 }
}],
[DocumentType.KVP, {
- layout: { view: KeyValueBox },
+ layout: { view: KeyValueBox, dataField: data },
options: { height: 150 }
}],
[DocumentType.VID, {
- layout: { view: VideoBox, collectionView: [CollectionVideoView, data, anno] as CollectionViewType },
- options: { curPage: 0 },
+ layout: { view: VideoBox, dataField: data },
+ options: { currentTimecode: 0 },
}],
[DocumentType.AUDIO, {
- layout: { view: AudioBox },
- options: { height: 32 }
+ layout: { view: AudioBox, dataField: data },
+ options: { height: 35, backgroundColor: "lightGray" }
}],
[DocumentType.PDF, {
- layout: { view: PDFBox, ext: anno },
+ layout: { view: PDFBox, dataField: data },
options: { nativeWidth: 1200, curPage: 1 }
}],
[DocumentType.ICON, {
- layout: { view: IconBox },
+ layout: { view: IconBox, dataField: data },
options: { width: Number(MINIMIZED_ICON_SIZE), height: Number(MINIMIZED_ICON_SIZE) },
}],
[DocumentType.IMPORT, {
- layout: { view: DirectoryImportBox },
+ layout: { view: DirectoryImportBox, dataField: data },
options: { height: 150 }
}],
[DocumentType.LINKDOC, {
data: new List<Doc>(),
- layout: { view: EmptyBox },
- options: {}
+ layout: { view: EmptyBox, dataField: data },
}],
[DocumentType.YOUTUBE, {
- layout: { view: YoutubeBox }
+ layout: { view: YoutubeBox, dataField: data }
}],
[DocumentType.BUTTON, {
- layout: { view: ButtonBox },
+ layout: { view: ButtonBox, dataField: data },
}],
[DocumentType.PRES, {
- layout: { view: PresBox },
+ layout: { view: PresBox, dataField: data },
options: {}
}],
- [DocumentType.DRAGBOX, {
- layout: { view: DragBox },
- options: { width: 40, height: 40 },
+ [DocumentType.FONTICON, {
+ layout: { view: FontIconBox, dataField: data },
+ options: { width: 40, height: 40, borderRounding: "100%" },
}],
[DocumentType.LINKFOLLOW, {
- layout: { view: LinkFollowBox }
- }]
+ layout: { view: LinkFollowBox, dataField: data }
+ }],
+ [DocumentType.PRESELEMENT, {
+ layout: { view: PresElementBox, dataField: data }
+ }],
]);
// All document prototypes are initialized with at least these values
@@ -226,6 +257,9 @@ export namespace Docs {
return PrototypeMap.get(type)!;
}
+ /**
+ * 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);
}
@@ -254,16 +288,9 @@ export namespace Docs {
let title = prototypeId.toUpperCase().replace(upper, `_${upper}`);
// synthesize the default options, the type and title from computed values and
// whatever options pertain to this specific prototype
- let options = { title: title, type: type, baseProto: true, ...defaultOptions, ...(template.options || {}) };
- let primary = layout.view.LayoutString(layout.ext);
- let collectionView = layout.collectionView;
- if (collectionView) {
- options.layout = collectionView[0].LayoutString(collectionView[1], collectionView[2]);
- options.backgroundLayout = primary;
- } else {
- options.layout = primary;
- }
- return Doc.assign(new Doc(prototypeId, true), { ...options, baseLayout: primary });
+ let options = { title, type, baseProto: true, ...defaultOptions, ...(template.options || {}) };
+ options.layout = layout.view.LayoutString(layout.dataField);
+ return Doc.assign(new Doc(prototypeId, true), { ...options, baseLayout: options.layout });
}
}
@@ -274,7 +301,7 @@ export namespace Docs {
*/
export namespace Create {
- const delegateKeys = ["x", "y", "width", "height", "panX", "panY"];
+ const delegateKeys = ["x", "y", "width", "height", "panX", "panY", "nativeWidth", "nativeHeight", "dropAction", "forceActive", "fitWidth"];
/**
* This function receives the relevant document prototype and uses
@@ -310,6 +337,8 @@ export namespace Docs {
let dataDoc = MakeDataDelegate(proto, protoProps, data);
let viewDoc = Doc.MakeDelegate(dataDoc, delegId);
+ AudioBox.ActiveRecordings.map(d => DocUtils.MakeLink({ doc: viewDoc }, { doc: d }, "audio link", "link to audio: " + d.title));
+
return Doc.assign(viewDoc, delegateProps);
}
@@ -334,16 +363,23 @@ export namespace Docs {
export function ImageDocument(url: string, options: DocumentOptions = {}) {
let imgField = new ImageField(new URL(url));
let inst = InstanceFromProto(Prototypes.get(DocumentType.IMG), imgField, { title: path.basename(url), ...options });
- requestImageSize(imgField.url.href)
+ let target = imgField.url.href;
+ if (new RegExp(window.location.origin).test(target)) {
+ let extension = path.extname(target);
+ target = `${target.substring(0, target.length - extension.length)}_o${extension}`;
+ }
+ // if (target !== "http://www.cs.brown.edu/") {
+ requestImageSize(target)
.then((size: any) => {
let aspect = size.height / size.width;
- if (!inst.proto!.nativeWidth) {
- inst.proto!.nativeWidth = size.width;
+ if (!inst.nativeWidth) {
+ inst.nativeWidth = size.width;
}
- inst.proto!.nativeHeight = Number(inst.proto!.nativeWidth!) * aspect;
- inst.proto!.height = NumCast(inst.proto!.width) * aspect;
+ inst.nativeHeight = NumCast(inst.nativeWidth) * aspect;
+ inst.height = NumCast(inst.width) * aspect;
})
.catch((err: any) => console.log(err));
+ // }
return inst;
}
export function PresDocument(initial: List<Doc> = new List(), options: DocumentOptions = {}) {
@@ -366,6 +402,14 @@ export namespace Docs {
return InstanceFromProto(Prototypes.get(DocumentType.HIST), new HistogramField(histoOp), options);
}
+ export function QueryDocument(options: DocumentOptions = {}) {
+ return InstanceFromProto(Prototypes.get(DocumentType.QUERY), "", options);
+ }
+
+ export function ColorDocument(options: DocumentOptions = {}) {
+ return InstanceFromProto(Prototypes.get(DocumentType.COLOR), "", options);
+ }
+
export function TextDocument(options: DocumentOptions = {}) {
return InstanceFromProto(Prototypes.get(DocumentType.TEXT), "", options);
}
@@ -425,6 +469,10 @@ export namespace Docs {
return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, viewType: CollectionViewType.Freeform }, id);
}
+ export function LinearDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
+ return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { chromeStatus: "collapsed", backgroundColor: "black", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, viewType: CollectionViewType.Linear }, id);
+ }
+
export function SchemaDocument(schemaColumns: SchemaHeaderField[], documents: Array<Doc>, options: DocumentOptions) {
return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { chromeStatus: "collapsed", schemaColumns: new List(schemaColumns), ...options, viewType: CollectionViewType.Schema });
}
@@ -446,14 +494,18 @@ export namespace Docs {
}
- export function DragboxDocument(options?: DocumentOptions) {
- return InstanceFromProto(Prototypes.get(DocumentType.DRAGBOX), 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 || {}) });
+ }
+
export function DockDocument(documents: Array<Doc>, config: string, options: DocumentOptions, id?: string) {
return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, viewType: CollectionViewType.Docking, dockingConfig: config }, id);
}
@@ -508,10 +560,13 @@ export namespace Docs {
* @param title an optional title to give to the highest parent document in the hierarchy
*/
export function DocumentHierarchyFromJson(input: any, title?: string): Opt<Doc> {
- if (input === null || ![...primitives, "object"].includes(typeof input)) {
+ if (input === undefined || input === null || ![...primitives, "object"].includes(typeof input)) {
return undefined;
}
- let parsed: any = typeof input === "string" ? JSONUtils.tryParse(input) : input;
+ let parsed = input;
+ if (typeof input === "string") {
+ parsed = JSONUtils.tryParse(input);
+ }
let converted: Doc;
if (typeof parsed === "object" && !(parsed instanceof Array)) {
converted = convertObject(parsed, title);
@@ -577,6 +632,7 @@ export namespace Docs {
if (type.indexOf("pdf") !== -1) {
ctor = Docs.Create.PdfDocument;
options.nativeWidth = 1200;
+ options.nativeHeight = 1200;
}
if (type.indexOf("excel") !== -1) {
ctor = Docs.Create.DBDocument;
@@ -638,33 +694,38 @@ export namespace DocUtils {
}
});
}
- export function MakeLink(source: Doc, target: Doc, targetContext?: Doc, title: string = "", description: string = "", sourceContext?: Doc, id?: string, anchored1?: boolean) {
- let sv = DocumentManager.Instance.getDocumentView(source);
- if (sv && sv.props.ContainingCollectionDoc === target) return;
- if (target === CurrentUserUtils.UserDocument) return undefined;
+
+ export function MakeLink(source: { doc: Doc, ctx?: Doc }, target: { doc: Doc, ctx?: Doc }, title: string = "", description: string = "", id?: string) {
+ let sv = DocumentManager.Instance.getDocumentView(source.doc);
+ if (sv && sv.props.ContainingCollectionDoc === target.doc) return;
+ if (target.doc === CurrentUserUtils.UserDocument) return undefined;
let linkDocProto = new Doc(id, true);
UndoManager.RunInBatch(() => {
linkDocProto.type = DocumentType.LINK;
- linkDocProto.targetContext = targetContext;
- linkDocProto.sourceContext = sourceContext;
- linkDocProto.title = title === "" ? source.title + " to " + target.title : title;
+ linkDocProto.title = title === "" ? source.doc.title + " to " + target.doc.title : title;
linkDocProto.linkDescription = description;
+ linkDocProto.isPrototype = true;
- linkDocProto.anchor1 = source;
- linkDocProto.anchor1Page = source.curPage;
+ linkDocProto.anchor1 = source.doc;
+ linkDocProto.anchor2 = target.doc;
+ linkDocProto.anchor1Context = source.ctx;
+ linkDocProto.anchor2Context = target.ctx;
linkDocProto.anchor1Groups = new List<Doc>([]);
- linkDocProto.anchor1anchored = anchored1;
- linkDocProto.anchor2 = target;
- linkDocProto.anchor2Page = target.curPage;
linkDocProto.anchor2Groups = new List<Doc>([]);
+ linkDocProto.anchor1Timecode = source.doc.currentTimecode;
+ linkDocProto.anchor2Timecode = target.doc.currentTimecode;
+ linkDocProto.layoutKey1 = DocuLinkBox.LayoutString("anchor1");
+ linkDocProto.layoutKey2 = DocuLinkBox.LayoutString("anchor2");
+ linkDocProto.width = linkDocProto.height = 0;
+ linkDocProto.isBackground = true;
+ linkDocProto.isButton = true;
LinkManager.Instance.addLink(linkDocProto);
- Doc.GetProto(source).links = ComputedField.MakeFunction("links(this)");
- Doc.GetProto(target).links = ComputedField.MakeFunction("links(this)");
-
+ Doc.GetProto(source.doc).links = ComputedField.MakeFunction("links(this)");
+ Doc.GetProto(target.doc).links = ComputedField.MakeFunction("links(this)");
}, "make link");
return linkDocProto;
}
diff --git a/src/client/northstar/dash-nodes/HistogramBox.tsx b/src/client/northstar/dash-nodes/HistogramBox.tsx
index b81eafbee..854135648 100644
--- a/src/client/northstar/dash-nodes/HistogramBox.tsx
+++ b/src/client/northstar/dash-nodes/HistogramBox.tsx
@@ -24,7 +24,7 @@ import { Id } from "../../../new_fields/FieldSymbols";
@observer
export class HistogramBox extends React.Component<FieldViewProps> {
- public static LayoutString(fieldStr: string = "data") { return FieldView.LayoutString(HistogramBox, fieldStr); }
+ public static LayoutString(fieldStr: string) { return FieldView.LayoutString(HistogramBox, fieldStr); }
private _dropXRef = React.createRef<HTMLDivElement>();
private _dropYRef = React.createRef<HTMLDivElement>();
private _dropXDisposer?: DragManager.DragDropDisposer;
diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts
index c4016d2a5..6bbd3d0ed 100644
--- a/src/client/util/DictationManager.ts
+++ b/src/client/util/DictationManager.ts
@@ -3,10 +3,10 @@ import { DocumentView } from "../views/nodes/DocumentView";
import { UndoManager } from "./UndoManager";
import * as interpreter from "words-to-numbers";
import { DocumentType } from "../documents/DocumentTypes";
-import { Doc } from "../../new_fields/Doc";
+import { Doc, Opt } from "../../new_fields/Doc";
import { List } from "../../new_fields/List";
import { Docs } from "../documents/Documents";
-import { CollectionViewType } from "../views/collections/CollectionBaseView";
+import { CollectionViewType } from "../views/collections/CollectionView";
import { Cast, CastCtor } from "../../new_fields/Types";
import { listSpec } from "../../new_fields/Schema";
import { AudioField, ImageField } from "../../new_fields/URLField";
@@ -14,6 +14,7 @@ import { HistogramField } from "../northstar/dash-fields/HistogramField";
import { MainView } from "../views/MainView";
import { Utils } from "../../Utils";
import { RichTextField } from "../../new_fields/RichTextField";
+import { DictationOverlay } from "../views/DictationOverlay";
/**
* This namespace provides a singleton instance of a manager that
@@ -40,23 +41,36 @@ export namespace DictationManager {
webkitSpeechRecognition: any;
}
}
- const { webkitSpeechRecognition }: CORE.IWindow = window as CORE.IWindow;
+ const { webkitSpeechRecognition }: CORE.IWindow = window as any as CORE.IWindow;
export const placeholder = "Listening...";
export namespace Controls {
export const Infringed = "unable to process: dictation manager still involved in previous session";
+ const browser = (() => {
+ let identifier = navigator.userAgent.toLowerCase();
+ if (identifier.indexOf("safari") >= 0) {
+ return "Safari";
+ }
+ if (identifier.indexOf("chrome") >= 0) {
+ return "Chrome";
+ }
+ if (identifier.indexOf("firefox") >= 0) {
+ return "Firefox";
+ }
+ return "Unidentified Browser";
+ })();
+ const unsupported = `listening is not supported in ${browser}`;
const intraSession = ". ";
const interSession = " ... ";
- let isListening = false;
+ export let isListening = false;
let isManuallyStopped = false;
let current: string | undefined = undefined;
let sessionResults: string[] = [];
- const recognizer: SpeechRecognition = new webkitSpeechRecognition() || new SpeechRecognition();
- recognizer.onstart = () => console.log("initiating speech recognition session...");
+ const recognizer: Opt<SpeechRecognition> = webkitSpeechRecognition ? new webkitSpeechRecognition() : undefined;
export type InterimResultHandler = (results: string) => any;
export type ContinuityArgs = { indefinite: boolean } | false;
@@ -75,12 +89,11 @@ export namespace DictationManager {
export const listen = async (options?: Partial<ListeningOptions>) => {
let results: string | undefined;
- let main = MainView.Instance;
let overlay = options !== undefined && options.useOverlay;
if (overlay) {
- main.dictationOverlayVisible = true;
- main.isListening = { interim: false };
+ DictationOverlay.Instance.dictationOverlayVisible = true;
+ DictationOverlay.Instance.isListening = { interim: false };
}
try {
@@ -88,27 +101,31 @@ export namespace DictationManager {
if (results) {
Utils.CopyText(results);
if (overlay) {
- main.isListening = false;
+ DictationOverlay.Instance.isListening = false;
let execute = options && options.tryExecute;
- main.dictatedPhrase = execute ? results.toLowerCase() : results;
- main.dictationSuccess = execute ? await DictationManager.Commands.execute(results) : true;
+ DictationOverlay.Instance.dictatedPhrase = execute ? results.toLowerCase() : results;
+ DictationOverlay.Instance.dictationSuccess = execute ? await DictationManager.Commands.execute(results) : true;
}
options && options.tryExecute && await DictationManager.Commands.execute(results);
}
} catch (e) {
if (overlay) {
- main.isListening = false;
- main.dictatedPhrase = results = `dictation error: ${"error" in e ? e.error : "unknown error"}`;
- main.dictationSuccess = false;
+ DictationOverlay.Instance.isListening = false;
+ DictationOverlay.Instance.dictatedPhrase = results = `dictation error: ${"error" in e ? e.error : "unknown error"}`;
+ DictationOverlay.Instance.dictationSuccess = false;
}
} finally {
- overlay && main.initiateDictationFade();
+ overlay && DictationOverlay.Instance.initiateDictationFade();
}
return results;
};
const listenImpl = (options?: Partial<ListeningOptions>) => {
+ if (!recognizer) {
+ console.log(unsupported);
+ return unsupported;
+ }
if (isListening) {
return Infringed;
}
@@ -121,6 +138,7 @@ export namespace DictationManager {
let intra = options && options.delimiters ? options.delimiters.intra : undefined;
let inter = options && options.delimiters ? options.delimiters.inter : undefined;
+ recognizer.onstart = () => console.log("initiating speech recognition session...");
recognizer.interimResults = handler !== undefined;
recognizer.continuous = continuous === undefined ? false : continuous !== false;
recognizer.lang = language === undefined ? "en-US" : language;
@@ -128,7 +146,6 @@ export namespace DictationManager {
recognizer.start();
return new Promise<string>((resolve, reject) => {
-
recognizer.onerror = (e: SpeechRecognitionError) => {
if (!(indefinite && e.error === "no-speech")) {
recognizer.stop();
@@ -167,16 +184,23 @@ export namespace DictationManager {
} else {
resolve(current);
}
- reset();
+ current = undefined;
+ sessionResults = [];
+ isListening = false;
+ isManuallyStopped = false;
+ recognizer.onresult = null;
+ recognizer.onerror = null;
+ recognizer.onend = null;
};
});
};
export const stop = (salvageSession = true) => {
- if (!isListening) {
+ if (!isListening || !recognizer) {
return;
}
+ isListening = false;
isManuallyStopped = true;
salvageSession ? recognizer.stop() : recognizer.abort();
// let main = MainView.Instance;
@@ -197,16 +221,6 @@ export namespace DictationManager {
return transcripts.join(delimiter || intraSession);
};
- const reset = () => {
- current = undefined;
- sessionResults = [];
- isListening = false;
- isManuallyStopped = false;
- recognizer.onresult = null;
- recognizer.onerror = null;
- recognizer.onend = null;
- };
-
}
export namespace Commands {
@@ -322,7 +336,6 @@ export namespace DictationManager {
let newBox = Docs.Create.TextDocument({ width: 400, height: 200, title: "My Outline" });
newBox.autoHeight = true;
let proto = newBox.proto!;
- proto.page = -1;
let prompt = "Press alt + r to start dictating here...";
let head = 3;
let anchor = head + prompt.length;
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index c048125c5..346e88f40 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -1,16 +1,15 @@
import { action, computed, observable } from 'mobx';
-import { Doc, DocListCastAsync } from '../../new_fields/Doc';
+import { Doc, DocListCastAsync, DocListCast } from '../../new_fields/Doc';
import { Id } from '../../new_fields/FieldSymbols';
-import { Cast, NumCast } from '../../new_fields/Types';
+import { List } from '../../new_fields/List';
+import { Cast, NumCast, StrCast } from '../../new_fields/Types';
import { CollectionDockingView } from '../views/collections/CollectionDockingView';
-import { CollectionPDFView } from '../views/collections/CollectionPDFView';
-import { CollectionVideoView } from '../views/collections/CollectionVideoView';
import { CollectionView } from '../views/collections/CollectionView';
import { DocumentView } from '../views/nodes/DocumentView';
import { LinkManager } from './LinkManager';
-import { undoBatch, UndoManager } from './UndoManager';
import { Scripting } from './Scripting';
-import { List } from '../../new_fields/List';
+import { SelectionManager } from './SelectionManager';
+import { DocumentType } from '../documents/DocumentTypes';
export class DocumentManager {
@@ -43,8 +42,8 @@ export class DocumentManager {
if (toReturn.length === 0) {
DocumentManager.Instance.DocumentViews.map(view => {
let doc = view.props.Document.proto;
- if (doc && doc[Id]) {
- if (doc[Id] === id) { toReturn.push(view); }
+ if (doc && doc[Id] && doc[Id] === id) {
+ toReturn.push(view);
}
});
}
@@ -55,9 +54,9 @@ export class DocumentManager {
return this.getDocumentViewsById(doc[Id]);
}
- public getDocumentViewById(id: string, preferredCollection?: CollectionView | CollectionPDFView | CollectionVideoView): DocumentView | null {
+ public getDocumentViewById(id: string, preferredCollection?: CollectionView): DocumentView | undefined {
- let toReturn: DocumentView | null = null;
+ let toReturn: DocumentView | undefined;
let passes = preferredCollection ? [preferredCollection, undefined] : [undefined];
for (let pass of passes) {
@@ -74,116 +73,130 @@ export class DocumentManager {
toReturn = view;
}
});
+ } else {
+ break;
}
}
return toReturn;
}
- public getDocumentView(toFind: Doc, preferredCollection?: CollectionView | CollectionPDFView | CollectionVideoView): DocumentView | null {
+ public getDocumentView(toFind: Doc, preferredCollection?: CollectionView): DocumentView | undefined {
return this.getDocumentViewById(toFind[Id], preferredCollection);
}
+ public getFirstDocumentView(toFind: Doc): DocumentView | undefined {
+ const views = this.getDocumentViews(toFind);
+ return views.length ? views[0] : undefined;
+ }
public getDocumentViews(toFind: Doc): DocumentView[] {
-
let toReturn: DocumentView[] = [];
- //gets document view that is in a freeform canvas collection
- DocumentManager.Instance.DocumentViews.map(view => {
- let doc = view.props.Document;
-
- if (doc === toFind) {
- toReturn.push(view);
- } else {
- if (Doc.AreProtosEqual(doc, toFind)) {
- toReturn.push(view);
- }
- }
- });
+ DocumentManager.Instance.DocumentViews.map(view =>
+ Doc.AreProtosEqual(view.props.Document, toFind) && toReturn.push(view));
return toReturn;
}
@computed
public get LinkedDocumentViews() {
- let pairs = DocumentManager.Instance.DocumentViews.filter(dv => dv.isSelected() || Doc.IsBrushed(dv.props.Document)).reduce((pairs, dv) => {
+ let pairs = DocumentManager.Instance.DocumentViews.filter(dv =>
+ (dv.isSelected() || Doc.IsBrushed(dv.props.Document)) // draw links from DocumentViews that are selected or brushed OR
+ || DocumentManager.Instance.DocumentViews.some(dv2 => { // Documentviews which
+ let rest = DocListCast(dv2.props.Document.links).some(l => Doc.AreProtosEqual(l, dv.props.Document));// are link doc anchors
+ let init = (dv2.isSelected() || Doc.IsBrushed(dv2.props.Document)) && dv2.Document.type !== DocumentType.AUDIO; // on a view that is selected or brushed
+ return init && rest;
+ })
+ ).reduce((pairs, dv) => {
let linksList = LinkManager.Instance.getAllRelatedLinks(dv.props.Document);
pairs.push(...linksList.reduce((pairs, link) => {
- if (link) {
- let linkToDoc = LinkManager.Instance.getOppositeAnchor(link, dv.props.Document);
- if (linkToDoc) {
- DocumentManager.Instance.getDocumentViews(linkToDoc).map(docView1 => {
- pairs.push({ a: dv, b: docView1, l: link });
- });
+ let linkToDoc = link && LinkManager.Instance.getOppositeAnchor(link, dv.props.Document);
+ linkToDoc && DocumentManager.Instance.getDocumentViews(linkToDoc).map(docView1 => {
+ if (dv.props.Document.type !== DocumentType.LINK || dv.props.layoutKey !== docView1.props.layoutKey) {
+ pairs.push({ a: dv, b: docView1, l: link });
}
- }
+ });
return pairs;
}, [] as { a: DocumentView, b: DocumentView, l: Doc }[]));
- // }
return pairs;
}, [] as { a: DocumentView, b: DocumentView, l: Doc }[]);
return pairs;
}
-
- @undoBatch
- public jumpToDocument = async (docDelegate: Doc, willZoom: boolean, forceDockFunc: boolean = false, dockFunc?: (doc: Doc) => void, linkPage?: number, docContext?: Doc): Promise<void> => {
- let doc = Doc.GetProto(docDelegate);
- const contextDoc = await Cast(doc.annotationOn, Doc);
- if (contextDoc) {
- contextDoc.scrollY = NumCast(doc.y) - NumCast(contextDoc.height) / 2;
- }
-
- let docView: DocumentView | null;
- // using forceDockFunc as a flag for splitting linked to doc to the right...can change later if needed
- if (!forceDockFunc && (docView = DocumentManager.Instance.getDocumentView(doc))) {
- Doc.BrushDoc(docView.props.Document);
- if (linkPage !== undefined) docView.props.Document.curPage = linkPage;
- UndoManager.RunInBatch(() => docView!.props.focus(docView!.props.Document, willZoom), "focus");
+ public jumpToDocument = async (targetDoc: Doc, willZoom: boolean, dockFunc?: (doc: Doc) => void, docContext?: Doc, linkId?: string, closeContextIfNotFound: boolean = false): Promise<void> => {
+ let highlight = () => {
+ const finalDocView = DocumentManager.Instance.getFirstDocumentView(targetDoc);
+ finalDocView && (finalDocView.Document.scrollToLinkID = linkId);
+ finalDocView && Doc.linkFollowHighlight(finalDocView.props.Document);
+ };
+ const docView = DocumentManager.Instance.getFirstDocumentView(targetDoc);
+ const annotatedDoc = await Cast(targetDoc.annotationOn, Doc);
+ 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?
+ annotatedDoc && docView.props.focus(annotatedDoc, false);
+ docView.props.focus(docView.props.Document, willZoom);
+ highlight();
} else {
- if (!contextDoc) {
- let docs = docContext ? await DocListCastAsync(docContext.data) : undefined;
- let found = false;
- // bcz: this just searches within the context for the target -- perhaps it should recursively search through all children?
- docs && docs.map(d => found = found || Doc.AreProtosEqual(d, docDelegate));
- if (docContext && found) {
- let targetContextView: DocumentView | null;
-
- if (!forceDockFunc && docContext && (targetContextView = DocumentManager.Instance.getDocumentView(docContext))) {
- docContext.panTransformType = "Ease";
- targetContextView.props.focus(docDelegate, willZoom);
- } else {
- (dockFunc || CollectionDockingView.AddRightSplit)(docContext, undefined);
- setTimeout(() => {
- let dv = DocumentManager.Instance.getDocumentView(docContext);
- dv && this.jumpToDocument(docDelegate, willZoom, forceDockFunc,
- doc => dv!.props.focus(dv!.props.Document, true, 1, () => dv!.props.addDocTab(doc, undefined, "inPlace")),
- linkPage);
- }, 1050);
- }
- } else {
- const actualDoc = Doc.MakeAlias(docDelegate);
- Doc.BrushDoc(actualDoc);
- if (linkPage !== undefined) actualDoc.curPage = linkPage;
- (dockFunc || CollectionDockingView.AddRightSplit)(actualDoc, undefined);
- }
+ const contextDocs = docContext ? await DocListCastAsync(docContext.data) : undefined;
+ const contextDoc = contextDocs && contextDocs.find(doc => Doc.AreProtosEqual(doc, targetDoc)) ? docContext : undefined;
+ 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);
+ highlight();
} else {
- let contextView: DocumentView | null;
- Doc.BrushDoc(docDelegate);
- if (!forceDockFunc && (contextView = DocumentManager.Instance.getDocumentView(contextDoc))) {
- contextDoc.panTransformType = "Ease";
- contextView.props.focus(docDelegate, willZoom);
- } else {
- (dockFunc || CollectionDockingView.AddRightSplit)(contextDoc, undefined);
+ const targetDocContextView = DocumentManager.Instance.getFirstDocumentView(targetDocContext);
+ targetDocContext.scrollY = 0; // this will force PDFs to activate and load their annotations / allow scrolling
+ if (targetDocContextView) { // we have a context view and aren't forced to create a new one ... focus on the context
+ targetDocContext.panTransformType = "Ease";
+ 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);
+ } else { // there's no context view so we need to create one first and try again
+ (dockFunc || CollectionDockingView.AddRightSplit)(targetDocContext, undefined);
setTimeout(() => {
- this.jumpToDocument(docDelegate, willZoom, forceDockFunc, dockFunc, linkPage);
- }, 1000);
+ const finalDocView = DocumentManager.Instance.getFirstDocumentView(targetDoc);
+ const finalDocContextView = DocumentManager.Instance.getFirstDocumentView(targetDocContext);
+ setTimeout(() => // if not, wait a bit to see if the context can be loaded (e.g., a PDF). wait interval heurisitic tries to guess how we're animating based on what's just become visible
+ this.jumpToDocument(targetDoc, willZoom, dockFunc, undefined, linkId, true), finalDocView ? 0 : finalDocContextView ? 250 : 2000); // so call jump to doc again and if the doc isn't found, it will be created.
+ }, 0);
}
}
}
}
+ public async FollowLink(link: Doc | undefined, doc: Doc, focus: (doc: Doc, maxLocation: string) => void, zoom: boolean = false, reverse: boolean = false, currentContext?: Doc) {
+ const linkDocs = link ? [link] : LinkManager.Instance.getAllRelatedLinks(doc);
+ SelectionManager.DeselectAll();
+ const firstDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor1 as Doc, doc));
+ const secondDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor2 as Doc, doc));
+ const firstDocWithoutView = firstDocs.find(d => DocumentManager.Instance.getDocumentViews(d.anchor2 as Doc).length === 0);
+ const secondDocWithoutView = secondDocs.find(d => DocumentManager.Instance.getDocumentViews(d.anchor1 as Doc).length === 0);
+ const first = firstDocWithoutView ? [firstDocWithoutView] : firstDocs;
+ 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];
+ if (linkFollowDocs && linkDoc) {
+ const maxLocation = StrCast(linkFollowDocs[0].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]);
+ }
+ }
+
@action
zoomIntoScale = (docDelegate: Doc, scale: number) => {
let docView = DocumentManager.Instance.getDocumentView(Doc.GetProto(docDelegate));
@@ -193,8 +206,7 @@ export class DocumentManager {
getScaleOfDocView = (docDelegate: Doc) => {
let doc = Doc.GetProto(docDelegate);
- let docView: DocumentView | null;
- docView = DocumentManager.Instance.getDocumentView(doc);
+ const docView = DocumentManager.Instance.getDocumentView(doc);
if (docView) {
return docView.props.getScale();
} else {
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index ddc8fb62c..bbc29585c 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -1,6 +1,6 @@
import { action, runInAction } from "mobx";
import { Doc, Field } from "../../new_fields/Doc";
-import { Cast, StrCast } from "../../new_fields/Types";
+import { Cast, StrCast, ScriptCast } from "../../new_fields/Types";
import { URLField } from "../../new_fields/URLField";
import { emptyFunction } from "../../Utils";
import { CollectionDockingView } from "../views/collections/CollectionDockingView";
@@ -10,10 +10,12 @@ import { LinkManager } from "./LinkManager";
import { SelectionManager } from "./SelectionManager";
import { SchemaHeaderField } from "../../new_fields/SchemaHeaderField";
import { Docs } from "../documents/Documents";
-import { CompileScript } from "./Scripting";
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";
export type dropActionType = "alias" | "copy" | undefined;
export function SetupDrag(
@@ -87,12 +89,12 @@ export async function DragLinkAsDocument(dragEle: HTMLElement, x: number, y: num
}
}
-export async function DragLinksAsDocuments(dragEle: HTMLElement, x: number, y: number, sourceDoc: Doc) {
+export async function DragLinksAsDocuments(dragEle: HTMLElement, x: number, y: number, sourceDoc: Doc, singleLink?: Doc) {
let srcTarg = sourceDoc.proto;
let draggedDocs: Doc[] = [];
if (srcTarg) {
- let linkDocs = LinkManager.Instance.getAllRelatedLinks(srcTarg);
+ let linkDocs = singleLink ? [singleLink] : LinkManager.Instance.getAllRelatedLinks(srcTarg);
if (linkDocs) {
draggedDocs = linkDocs.map(link => {
let opp = LinkManager.Instance.getOppositeAnchor(link, sourceDoc);
@@ -211,7 +213,9 @@ export namespace DragManager {
offset: number[];
dropAction: dropActionType;
userDropAction: dropActionType;
+ embedDoc?: boolean;
moveDocument?: MoveFunction;
+ isSelectionMove?: boolean; // indicates that an explicitly selected Document is being dragged. this will suppress onDragStart scripts
applyAsTemplate?: boolean;
[id: string]: any;
}
@@ -236,14 +240,16 @@ export namespace DragManager {
export function StartDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions) {
runInAction(() => StartDragFunctions.map(func => func()));
+ dragData.draggedDocuments.map(d => d.dragFactory); // does this help? trying to make sure the dragFactory Doc is loaded
StartDrag(eles, dragData, downX, downY, options, options && options.finishDrag ? options.finishDrag :
(dropData: { [id: string]: any }) => {
- (dropData.droppedDocuments = dragData.userDropAction === "alias" || (!dragData.userDropAction && dragData.dropAction === "alias") ?
- dragData.draggedDocuments.map(d => Doc.MakeAlias(d)) :
- dragData.userDropAction === "copy" || (!dragData.userDropAction && dragData.dropAction === "copy") ?
- dragData.draggedDocuments.map(d => Doc.MakeCopy(d, true)) :
- dragData.draggedDocuments
+ (dropData.droppedDocuments =
+ dragData.draggedDocuments.map(d => !dragData.isSelectionMove && !dragData.userDropAction && ScriptCast(d.onDragStart) ? 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)
);
+ dropData.droppedDocuments.forEach((drop: Doc, i: number) =>
+ Cast(dragData.draggedDocuments[i].removeDropProperties, listSpec("string"), []).map(prop => drop[prop] = undefined));
});
}
@@ -302,16 +308,6 @@ export namespace DragManager {
[id: string]: any;
}
- export class EmbedDragData {
- constructor(embeddableSourceDoc: Doc) {
- this.embeddableSourceDoc = embeddableSourceDoc;
- this.urlField = embeddableSourceDoc.data instanceof URLField ? embeddableSourceDoc.data : undefined;
- }
- embeddableSourceDoc: Doc;
- urlField?: URLField;
- [id: string]: any;
- }
-
// for column dragging in schema view
export class ColumnDragData {
constructor(colKey: SchemaHeaderField) {
@@ -325,10 +321,6 @@ export namespace DragManager {
StartDrag([ele], dragData, downX, downY, options);
}
- export function StartEmbedDrag(ele: HTMLElement, dragData: EmbedDragData, downX: number, downY: number, options?: DragOptions) {
- StartDrag([ele], dragData, downX, downY, options);
- }
-
export function StartColumnDrag(ele: HTMLElement, dragData: ColumnDragData, downX: number, downY: number, options?: DragOptions) {
StartDrag([ele], dragData, downX, downY, options);
}
@@ -424,16 +416,17 @@ export namespace DragManager {
const moveHandler = (e: PointerEvent) => {
e.preventDefault(); // required or dragging text menu link item ends up dragging the link button as native drag/drop
if (dragData instanceof DocumentDragData) {
- dragData.userDropAction = e.ctrlKey || e.altKey ? "alias" : undefined;
+ dragData.userDropAction = e.ctrlKey ? "alias" : undefined;
}
if (((options && !options.withoutShiftDrag) || !options) && e.shiftKey && CollectionDockingView.Instance) {
AbortDrag();
+ finishDrag && finishDrag(dragData);
CollectionDockingView.Instance.StartOtherDrag({
pageX: e.pageX,
pageY: e.pageY,
preventDefault: emptyFunction,
button: 0
- }, docs);
+ }, dragData.droppedDocuments);
}
//TODO: Why can't we use e.movementX and e.movementY?
let moveX = e.pageX - lastX;
@@ -506,3 +499,4 @@ export namespace DragManager {
}
}
}
+Scripting.addGlobal(function convertToButtons(dragData: any) { convertDropDataToButtons(dragData as DragManager.DocumentDragData); });
diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts
new file mode 100644
index 000000000..6b53333d7
--- /dev/null
+++ b/src/client/util/DropConverter.ts
@@ -0,0 +1,46 @@
+import { DragManager } from "./DragManager";
+import { CollectionViewType } from "../views/collections/CollectionView";
+import { Doc, DocListCast } from "../../new_fields/Doc";
+import { DocumentType } from "../documents/DocumentTypes";
+import { ObjectField } from "../../new_fields/ObjectField";
+import { StrCast } from "../../new_fields/Types";
+import { Docs } from "../documents/Documents";
+import { ScriptField } from "../../new_fields/ScriptField";
+
+
+function makeTemplate(doc: Doc): boolean {
+ let layoutDoc = doc.layout instanceof Doc && doc.layout.isTemplateField ? doc.layout : doc;
+ let layout = StrCast(layoutDoc.layout).match(/fieldKey={"[^"]*"}/)![0];
+ let fieldKey = layout.replace('fieldKey={"', "").replace(/"}$/, "");
+ let docs = DocListCast(layoutDoc[fieldKey]);
+ let any = false;
+ docs.map(d => {
+ if (!StrCast(d.title).startsWith("-")) {
+ any = true;
+ return Doc.MakeMetadataFieldTemplate(d, Doc.GetProto(layoutDoc));
+ }
+ if (d.type === DocumentType.COL) return makeTemplate(d);
+ return false;
+ });
+ return any;
+}
+export function convertDropDataToButtons(data: DragManager.DocumentDragData) {
+ data && data.draggedDocuments.map((doc, i) => {
+ let dbox = doc;
+ if (!doc.onDragStart && !doc.onClick && doc.viewType !== CollectionViewType.Linear) {
+ let layoutDoc = doc.layout instanceof Doc && doc.layout.isTemplateField ? doc.layout : doc;
+ if (layoutDoc.type === DocumentType.COL) {
+ layoutDoc.isTemplateDoc = makeTemplate(layoutDoc);
+ } else {
+ layoutDoc.isTemplateDoc = (layoutDoc.type === DocumentType.TEXT || layoutDoc.layout instanceof Doc) && !data.userDropAction;
+ }
+ dbox = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, backgroundColor: StrCast(doc.backgroundColor), title: "Custom", 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)');
+ } else if (doc.viewType === CollectionViewType.Linear) {
+ dbox.ignoreClick = true;
+ }
+ data.droppedDocuments[i] = dbox;
+ });
+}
diff --git a/src/client/util/History.ts b/src/client/util/History.ts
index 67c8e931d..899abbe40 100644
--- a/src/client/util/History.ts
+++ b/src/client/util/History.ts
@@ -16,8 +16,10 @@ export namespace HistoryUtil {
initializers?: {
[docId: string]: DocInitializerList;
};
+ safe?: boolean;
readonly?: boolean;
nro?: boolean;
+ sharing?: boolean;
}
export type ParsedUrl = DocUrl;
@@ -143,7 +145,7 @@ export namespace HistoryUtil {
};
}
- addParser("doc", {}, { readonly: true, initializers: true, nro: true }, (pathname, opts, current) => {
+ addParser("doc", {}, { readonly: true, initializers: true, nro: true, sharing: true }, (pathname, opts, current) => {
if (pathname.length !== 2) return undefined;
current.initializers = current.initializers || {};
@@ -158,7 +160,7 @@ export namespace HistoryUtil {
export function parseUrl(location: Location | URL): ParsedUrl | undefined {
const pathname = location.pathname.substring(1);
const search = location.search;
- const opts = qs.parse(search, { sort: false });
+ const opts = search.length ? qs.parse(search, { sort: false }) : {};
let pathnameSplit = pathname.split("/");
const type = pathnameSplit[0];
diff --git a/src/client/util/Import & Export/DirectoryImportBox.scss b/src/client/util/Import & Export/DirectoryImportBox.scss
new file mode 100644
index 000000000..d33cb524b
--- /dev/null
+++ b/src/client/util/Import & Export/DirectoryImportBox.scss
@@ -0,0 +1,6 @@
+.phase {
+ position: absolute;
+ top: 15px;
+ left: 15px;
+ font-style: italic;
+} \ No newline at end of file
diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index dc6a0cb7a..5904088fc 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx
@@ -1,9 +1,8 @@
import "fs";
import React = require("react");
-import { Doc, Opt, DocListCast, DocListCastAsync } from "../../../new_fields/Doc";
-import { DocServer } from "../../DocServer";
+import { Doc, DocListCast, DocListCastAsync, Opt } from "../../../new_fields/Doc";
import { RouteStore } from "../../../server/RouteStore";
-import { action, observable, autorun, runInAction, computed } from "mobx";
+import { action, observable, autorun, runInAction, computed, reaction, IReactionDisposer } from "mobx";
import { FieldViewProps, FieldView } from "../../views/nodes/FieldView";
import Measure, { ContentRect } from "react-measure";
import { library } from '@fortawesome/fontawesome-svg-core';
@@ -18,25 +17,40 @@ import { Id } from "../../../new_fields/FieldSymbols";
import { List } from "../../../new_fields/List";
import { Cast, BoolCast, NumCast } from "../../../new_fields/Types";
import { listSpec } from "../../../new_fields/Schema";
+import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils";
+import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField";
+import "./DirectoryImportBox.scss";
+import { Identified } from "../../Network";
+import { BatchedArray } from "array-batcher";
+import { ExifData } from "exif";
const unsupported = ["text/html", "text/plain"];
+interface ImageUploadResponse {
+ name: string;
+ path: string;
+ type: string;
+ exif: any;
+}
+
@observer
export default class DirectoryImportBox extends React.Component<FieldViewProps> {
private selector = React.createRef<HTMLInputElement>();
@observable private top = 0;
@observable private left = 0;
private dimensions = 50;
+ @observable private phase = "";
+ private disposer: Opt<IReactionDisposer>;
@observable private entries: ImportMetadataEntry[] = [];
@observable private quota = 1;
- @observable private remaining = 1;
+ @observable private completed = 0;
@observable private uploading = false;
@observable private removeHover = false;
- public static LayoutString() { return FieldView.LayoutString(DirectoryImportBox); }
+ public static LayoutString(fieldKey: string) { return FieldView.LayoutString(DirectoryImportBox, fieldKey); }
constructor(props: FieldViewProps) {
super(props);
@@ -66,15 +80,17 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
}
handleSelection = async (e: React.ChangeEvent<HTMLInputElement>) => {
- runInAction(() => this.uploading = true);
+ runInAction(() => {
+ this.uploading = true;
+ this.phase = "Initializing download...";
+ });
- let promises: Promise<void>[] = [];
let docs: Doc[] = [];
let files = e.target.files;
if (!files || files.length === 0) return;
- let directory = (files.item(0) as any).webkitRelativePath.split("/", 1);
+ let directory = (files.item(0) as any).webkitRelativePath.split("/", 1)[0];
let validated: File[] = [];
for (let i = 0; i < files.length; i++) {
@@ -82,37 +98,45 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
file && !unsupported.includes(file.type) && validated.push(file);
}
- runInAction(() => this.quota = validated.length);
-
- let sizes = [];
- let modifiedDates = [];
+ runInAction(() => {
+ this.quota = validated.length;
+ this.completed = 0;
+ });
- for (let uploaded_file of validated) {
- let formData = new FormData();
- formData.append('file', uploaded_file);
- let dropFileName = uploaded_file ? uploaded_file.name : "-empty-";
- let type = uploaded_file.type;
+ let sizes: number[] = [];
+ let modifiedDates: number[] = [];
- sizes.push(uploaded_file.size);
- modifiedDates.push(uploaded_file.lastModified);
+ runInAction(() => this.phase = `Internal: uploading ${this.quota - this.completed} files to Dash...`);
- runInAction(() => this.remaining++);
+ const batched = BatchedArray.from(validated, { batchSize: 15 });
+ const uploads = await batched.batchedMapAsync<ImageUploadResponse>(async (batch, collector) => {
+ const formData = new FormData();
- let prom = fetch(Utils.prepend(RouteStore.upload), {
- method: 'POST',
- body: formData
- }).then(async (res: Response) => {
- (await res.json()).map(action((file: any) => {
- let docPromise = Docs.Get.DocumentFromType(type, Utils.prepend(file), { nativeWidth: 300, width: 300, title: dropFileName });
- docPromise.then(doc => {
- doc && docs.push(doc) && runInAction(() => this.remaining--);
- });
- }));
+ batch.forEach(file => {
+ sizes.push(file.size);
+ modifiedDates.push(file.lastModified);
+ formData.append(Utils.GenerateGuid(), file);
});
- promises.push(prom);
- }
- await Promise.all(promises);
+ collector.push(...(await Identified.PostFormDataToServer(RouteStore.upload, formData)));
+ runInAction(() => this.completed += batch.length);
+ });
+
+ await Promise.all(uploads.map(async upload => {
+ const type = upload.type;
+ const path = Utils.prepend(upload.path);
+ const options = {
+ nativeWidth: 300,
+ width: 300,
+ title: upload.name
+ };
+ const document = await Docs.Get.DocumentFromType(type, path, options);
+ const { data, error } = upload.exif;
+ if (document) {
+ Doc.GetProto(document).exif = error || Docs.Get.DocumentHierarchyFromJson(data);
+ docs.push(document);
+ }
+ }));
for (let i = 0; i < docs.length; i++) {
let doc = docs[i];
@@ -134,25 +158,41 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
x: NumCast(doc.x),
y: NumCast(doc.y) + offset
};
- if (this.props.ContainingCollectionDoc) {
- let importContainer = Docs.Create.StackingDocument(docs, options);
+ let parent = this.props.ContainingCollectionView;
+ if (parent) {
+ let importContainer: Doc;
+ if (docs.length < 50) {
+ importContainer = Docs.Create.MasonryDocument(docs, options);
+ } else {
+ const headers = [new SchemaHeaderField("title"), new SchemaHeaderField("size")];
+ importContainer = Docs.Create.SchemaDocument(headers, docs, options);
+ }
+ runInAction(() => this.phase = 'External: uploading files to Google Photos...');
importContainer.singleColumn = false;
- Doc.AddDocToList(Doc.GetProto(this.props.ContainingCollectionDoc), "data", importContainer);
+ await GooglePhotos.Export.CollectionToAlbum({ collection: importContainer });
+ Doc.AddDocToList(Doc.GetProto(parent.props.Document), "data", importContainer);
!this.persistent && this.props.removeDocument && this.props.removeDocument(doc);
DocumentManager.Instance.jumpToDocument(importContainer, true);
-
}
runInAction(() => {
this.uploading = false;
this.quota = 1;
- this.remaining = 1;
+ this.completed = 0;
});
}
componentDidMount() {
this.selector.current!.setAttribute("directory", "");
this.selector.current!.setAttribute("webkitdirectory", "");
+ this.disposer = reaction(
+ () => this.completed,
+ completed => runInAction(() => this.phase = `Internal: uploading ${this.quota - completed} files to Dash...`)
+ );
+ }
+
+ componentWillUnmount() {
+ this.disposer && this.disposer();
}
@action
@@ -187,7 +227,6 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
metadata.splice(index, 1);
}
}
-
}
}
@@ -195,19 +234,47 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
let dimensions = 50;
let entries = DocListCast(this.props.Document.data);
let isEditing = this.editingMetadata;
- let remaining = this.remaining;
+ let completed = this.completed;
let quota = this.quota;
let uploading = this.uploading;
let showRemoveLabel = this.removeHover;
let persistent = this.persistent;
- let percent = `${100 - (remaining / quota * 100)}`;
+ let percent = `${completed / quota * 100}`;
percent = percent.split(".")[0];
percent = percent.startsWith("100") ? "99" : percent;
let marginOffset = (percent.length === 1 ? 5 : 0) - 1.6;
+ const message = <span className={"phase"}>{this.phase}</span>;
+ const centerPiece = this.phase.includes("Google Photos") ?
+ <img src={"/assets/google_photos.png"} style={{
+ transition: "0.4s opacity ease",
+ width: 30,
+ height: 30,
+ opacity: uploading ? 1 : 0,
+ pointerEvents: "none",
+ position: "absolute",
+ left: 12,
+ top: this.top + 10,
+ fontSize: 18,
+ color: "white",
+ marginLeft: this.left + marginOffset
+ }} />
+ : <div
+ style={{
+ transition: "0.4s opacity ease",
+ opacity: uploading ? 1 : 0,
+ pointerEvents: "none",
+ position: "absolute",
+ left: 10,
+ top: this.top + 12.3,
+ fontSize: 18,
+ color: "white",
+ marginLeft: this.left + marginOffset
+ }}>{percent}%</div>;
return (
<Measure offset onResize={this.preserveCentering}>
{({ measureRef }) =>
<div ref={measureRef} style={{ width: "100%", height: "100%", pointerEvents: "all" }} >
+ {message}
<input
id={"selector"}
ref={this.selector}
@@ -280,18 +347,7 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
opacity: showRemoveLabel ? 1 : 0,
transition: "0.4s opacity ease"
}}>Template will be <span style={{ textDecoration: "underline", textDecorationColor: persistent ? "green" : "red", color: persistent ? "green" : "red" }}>{persistent ? "kept" : "removed"}</span> after upload</p>
- <div
- style={{
- transition: "0.4s opacity ease",
- opacity: uploading ? 1 : 0,
- pointerEvents: "none",
- position: "absolute",
- left: 10,
- top: this.top + 12.3,
- fontSize: 18,
- color: "white",
- marginLeft: this.left + marginOffset
- }}>{percent}%</div>
+ {centerPiece}
<div
style={{
position: "absolute",
@@ -312,7 +368,7 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
style={{
pointerEvents: "none",
position: "absolute",
- right: isEditing ? 16.3 : 14.5,
+ right: isEditing ? 14 : 15,
top: isEditing ? 15.4 : 16,
opacity: uploading ? 0 : 1,
transition: "0.4s opacity ease"
diff --git a/src/client/util/Import & Export/ImageUtils.ts b/src/client/util/Import & Export/ImageUtils.ts
new file mode 100644
index 000000000..c9abf38fa
--- /dev/null
+++ b/src/client/util/Import & Export/ImageUtils.ts
@@ -0,0 +1,31 @@
+import { Doc, DocListCast, DocListCastAsync, Opt } from "../../../new_fields/Doc";
+import { ImageField } from "../../../new_fields/URLField";
+import { Cast, StrCast } from "../../../new_fields/Types";
+import { RouteStore } from "../../../server/RouteStore";
+import { Docs } from "../../documents/Documents";
+import { Identified } from "../../Network";
+import { Id } from "../../../new_fields/FieldSymbols";
+import { Utils } from "../../../Utils";
+
+export namespace ImageUtils {
+
+ export const ExtractExif = async (document: Doc): Promise<boolean> => {
+ const field = Cast(document.data, ImageField);
+ if (!field) {
+ return false;
+ }
+ const source = field.url.href;
+ const response = await Identified.PostToServer(RouteStore.inspectImage, { source });
+ const { error, data } = response.exifData;
+ document.exif = error || Docs.Get.DocumentHierarchyFromJson(data);
+ return data !== undefined;
+ };
+
+ export const ExportHierarchyToFileSystem = async (collection: Doc): Promise<void> => {
+ const a = document.createElement("a");
+ a.href = Utils.prepend(`${RouteStore.imageHierarchyExport}/${collection[Id]}`);
+ a.download = `Dash Export [${StrCast(collection.title)}].zip`;
+ a.click();
+ };
+
+} \ No newline at end of file
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index 8a668e8d8..eedc4967d 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -1,10 +1,7 @@
-import { observable, action } from "mobx";
-import { StrCast, Cast, FieldValue } from "../../new_fields/Types";
import { Doc, DocListCast } from "../../new_fields/Doc";
-import { listSpec } from "../../new_fields/Schema";
import { List } from "../../new_fields/List";
-import { Id } from "../../new_fields/FieldSymbols";
-import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
+import { listSpec } from "../../new_fields/Schema";
+import { Cast, StrCast } from "../../new_fields/Types";
import { Docs } from "../documents/Documents";
import { Scripting } from "./Scripting";
@@ -37,8 +34,6 @@ export class LinkManager {
// the linkmanagerdoc stores a list of docs representing all linkdocs in 'allLinks' and a list of strings representing all group types in 'allGroupTypes'
// lists of strings representing the metadata keys for each group type is stored under a key that is the same as the group type
public get LinkManagerDoc(): Doc | undefined {
- // return FieldValue(Cast(CurrentUserUtils.UserDocument.linkManagerDoc, Doc));
-
return Docs.Prototypes.MainLinkDocument();
}
@@ -79,7 +74,7 @@ export class LinkManager {
let related = LinkManager.Instance.getAllLinks().filter(link => {
let protomatch1 = Doc.AreProtosEqual(anchor, Cast(link.anchor1, Doc, null));
let protomatch2 = Doc.AreProtosEqual(anchor, Cast(link.anchor2, Doc, null));
- return protomatch1 || protomatch2;
+ return protomatch1 || protomatch2 || Doc.AreProtosEqual(link, anchor);
});
return related;
}
@@ -242,11 +237,11 @@ export class LinkManager {
//TODO This should probably return undefined if there isn't an opposite anchor
//TODO This should also await the return value of the anchor so we don't filter out promises
public getOppositeAnchor(linkDoc: Doc, anchor: Doc): Doc | undefined {
- if (Doc.AreProtosEqual(anchor, Cast(linkDoc.anchor1, Doc, null))) {
- return Cast(linkDoc.anchor2, Doc, null);
- } else {
- return Cast(linkDoc.anchor1, Doc, null);
- }
+ let a1 = Cast(linkDoc.anchor1, Doc, null);
+ let a2 = Cast(linkDoc.anchor2, Doc, null);
+ if (Doc.AreProtosEqual(anchor, a1)) return a2;
+ if (Doc.AreProtosEqual(anchor, a2)) return a1;
+ if (Doc.AreProtosEqual(anchor, linkDoc)) return linkDoc;
}
}
Scripting.addGlobal(function links(doc: any) {
diff --git a/src/client/util/ProsemirrorExampleTransfer.ts b/src/client/util/ProsemirrorExampleTransfer.ts
index aab437176..003ff6272 100644
--- a/src/client/util/ProsemirrorExampleTransfer.ts
+++ b/src/client/util/ProsemirrorExampleTransfer.ts
@@ -11,7 +11,7 @@ const mac = typeof navigator !== "undefined" ? /Mac/.test(navigator.platform) :
export type KeyMap = { [key: string]: any };
-export let updateBullets = (tx2: Transaction, schema: Schema) => {
+export let updateBullets = (tx2: Transaction, schema: Schema, mapStyle?: string) => {
let fontSize: number | undefined = undefined;
tx2.doc.descendants((node: any, offset: any, index: any) => {
if (node.type === schema.nodes.ordered_list || node.type === schema.nodes.list_item) {
@@ -20,7 +20,7 @@ export let updateBullets = (tx2: Transaction, schema: Schema) => {
if (node.type === schema.nodes.ordered_list) depth++;
fontSize = depth === 1 && node.attrs.setFontSize ? Number(node.attrs.setFontSize) : fontSize;
let fsize = fontSize && node.type === schema.nodes.ordered_list ? Math.max(6, fontSize - (depth - 1) * 4) : undefined;
- tx2.setNodeMarkup(offset, node.type, { ...node.attrs, mapStyle: node.attrs.mapStyle, bulletStyle: depth, inheritedFontSize: fsize }, node.marks);
+ tx2.setNodeMarkup(offset, node.type, { ...node.attrs, mapStyle: mapStyle ? mapStyle : node.attrs.mapStyle, bulletStyle: depth, inheritedFontSize: fsize }, node.marks);
}
});
return tx2;
@@ -107,8 +107,6 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
bind("Mod-s", TooltipTextMenu.insertStar);
-
-
bind("Tab", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
var ref = state.selection;
var range = ref.$from.blockRange(ref.$to);
diff --git a/src/client/util/RichTextRules.ts b/src/client/util/RichTextRules.ts
index cd37ea0bb..ebb9bda8a 100644
--- a/src/client/util/RichTextRules.ts
+++ b/src/client/util/RichTextRules.ts
@@ -1,10 +1,12 @@
import { textblockTypeInputRule, smartQuotes, emDash, ellipsis, InputRule } from "prosemirror-inputrules";
import { schema } from "./RichTextSchema";
import { wrappingInputRule } from "./prosemirrorPatches";
-import { NodeSelection } from "prosemirror-state";
+import { NodeSelection, TextSelection } from "prosemirror-state";
import { NumCast, Cast } from "../../new_fields/Types";
import { Doc } from "../../new_fields/Doc";
import { FormattedTextBox } from "../views/nodes/FormattedTextBox";
+import { Docs } from "../documents/Documents";
+import { Id } from "../../new_fields/FieldSymbols";
export const inpRules = {
rules: [
@@ -70,6 +72,34 @@ export const inpRules = {
return state.tr.deleteRange(start, end).addStoredMark(schema.marks.pFontSize.create({ fontSize: Number(match[1]) }));
}),
new InputRule(
+ new RegExp(/t/),
+ (state, match, start, end) => {
+ if (state.selection.to === state.selection.from) return null;
+ let node = (state.doc.resolve(start) as any).nodeAfter;
+ return node ? state.tr.addMark(start, end, schema.marks.user_tag.create({ userid: Doc.CurrentUserEmail, tag: "todo", modified: Math.round(Date.now() / 1000 / 60) })) : state.tr;
+ }),
+ new InputRule(
+ new RegExp(/i/),
+ (state, match, start, end) => {
+ if (state.selection.to === state.selection.from) return null;
+ let node = (state.doc.resolve(start) as any).nodeAfter;
+ return node ? state.tr.addMark(start, end, schema.marks.user_tag.create({ userid: Doc.CurrentUserEmail, tag: "ignore", modified: Math.round(Date.now() / 1000 / 60) })) : state.tr;
+ }),
+ new InputRule(
+ new RegExp(/\!/),
+ (state, match, start, end) => {
+ if (state.selection.to === state.selection.from) return null;
+ let node = (state.doc.resolve(start) as any).nodeAfter;
+ return node ? state.tr.addMark(start, end, schema.marks.user_tag.create({ userid: Doc.CurrentUserEmail, tag: "important", modified: Math.round(Date.now() / 1000 / 60) })) : state.tr;
+ }),
+ new InputRule(
+ new RegExp(/\x/),
+ (state, match, start, end) => {
+ if (state.selection.to === state.selection.from) return null;
+ let node = (state.doc.resolve(start) as any).nodeAfter;
+ return node ? state.tr.addMark(start, end, schema.marks.user_tag.create({ userid: Doc.CurrentUserEmail, tag: "disagree", modified: Math.round(Date.now() / 1000 / 60) })) : state.tr;
+ }),
+ new InputRule(
new RegExp(/^\^\^\s$/),
(state, match, start, end) => {
let node = (state.doc.resolve(start) as any).nodeAfter;
@@ -80,8 +110,9 @@ export const inpRules = {
ruleProvider["ruleAlign_" + heading] = "center";
return node ? state.tr.deleteRange(start, end).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr;
}
- return node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "center" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
+ let 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)));
}),
new InputRule(
new RegExp(/^\[\[\s$/),
@@ -92,8 +123,11 @@ export const inpRules = {
let heading = NumCast(FormattedTextBox.InputBoxOverlay!.props.Document.heading);
if (ruleProvider && heading) {
ruleProvider["ruleAlign_" + heading] = "left";
+ return node ? state.tr.deleteRange(start, end).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr;
}
- return node ? state.tr.deleteRange(start, end).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr;
+ let 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)));
}),
new InputRule(
new RegExp(/^\]\]\s$/),
@@ -104,8 +138,44 @@ export const inpRules = {
let heading = NumCast(FormattedTextBox.InputBoxOverlay!.props.Document.heading);
if (ruleProvider && heading) {
ruleProvider["ruleAlign_" + heading] = "right";
+ return node ? state.tr.deleteRange(start, end).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr;
}
- return node ? state.tr.deleteRange(start, end).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr;
+ let 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(/##\s$/),
+ (state, match, start, end) => {
+ let node = (state.doc.resolve(start) as any).nodeAfter;
+ let sm = state.storedMarks || undefined;
+ let target = Docs.Create.TextDocument({ width: 75, height: 35, autoHeight: true, fontSize: 9, title: "inline comment" });
+ let replaced = node ? state.tr.insertText("←", start).replaceRangeWith(start + 1, end + 1, schema.nodes.dashDoc.create({
+ width: 75, height: 35,
+ title: "dashDoc", docid: target[Id],
+ float: "right"
+ })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
+ state.tr;
+ return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 1)));
+ }),
+ new InputRule(
+ new RegExp(/\(\(/),
+ (state, match, start, end) => {
+ let node = (state.doc.resolve(start) as any).nodeAfter;
+ let sm = state.storedMarks || undefined;
+ let mark = state.schema.marks.highlight.create();
+ let selected = state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))).addMark(start, end, mark);
+ let content = selected.selection.content();
+ let replaced = node ? selected.replaceRangeWith(start, start,
+ schema.nodes.star.create({ visibility: true, text: content, textslice: content.toJSON() })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
+ state.tr;
+ return replaced.setSelection(new TextSelection(replaced.doc.resolve(end + 1)));
+ }),
+ new InputRule(
+ new RegExp(/\)\)/),
+ (state, match, start, end) => {
+ let mark = state.schema.marks.highlight.create();
+ return state.tr.removeStoredMark(mark);
}),
new InputRule(
new RegExp(/\^f\s$/),
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index 710d55605..76b8aeaa1 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -1,18 +1,24 @@
+import { action, observable, runInAction, reaction, IReactionDisposer } 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, TextSelection } from "prosemirror-state";
+import { EditorState, NodeSelection, TextSelection, Plugin } from "prosemirror-state";
import { StepMap } from "prosemirror-transform";
import { EditorView } from "prosemirror-view";
-import { Doc } from "../../new_fields/Doc";
-import { FormattedTextBox } from "../views/nodes/FormattedTextBox";
+import * as ReactDOM from 'react-dom';
+import { Doc, WidthSym, HeightSym } from "../../new_fields/Doc";
+import { emptyFunction, returnEmptyString, returnFalse, returnOne, Utils } from "../../Utils";
import { DocServer } from "../DocServer";
-import { Cast, NumCast } from "../../new_fields/Types";
+import { DocumentView } from "../views/nodes/DocumentView";
import { DocumentManager } from "./DocumentManager";
import ParagraphNodeSpec from "./ParagraphNodeSpec";
-import { times } from "async";
+import { Transform } from "./Transform";
+import React = require("react");
+import { BoolCast, NumCast } from "../../new_fields/Types";
+import { FormattedTextBox } from "../views/nodes/FormattedTextBox";
+import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
const pDOM: DOMOutputSpecArray = ["p", 0], blockquoteDOM: DOMOutputSpecArray = ["blockquote", 0], hrDOM: DOMOutputSpecArray = ["hr"],
preDOM: DOMOutputSpecArray = ["pre", ["code", 0]], brDOM: DOMOutputSpecArray = ["br"], ulDOM: DOMOutputSpecArray = ["ul", 0];
@@ -135,6 +141,7 @@ export const nodes: { [index: string]: NodeSpec } = {
alt: { default: null },
title: { default: null },
float: { default: "left" },
+ location: { default: "onRight" },
docid: { default: "" }
},
group: "inline",
@@ -156,6 +163,35 @@ export const nodes: { [index: string]: NodeSpec } = {
}
},
+ dashDoc: {
+ inline: true,
+ attrs: {
+ width: { default: 200 },
+ height: { default: 100 },
+ title: { default: null },
+ float: { default: "right" },
+ location: { default: "onRight" },
+ docid: { default: "" }
+ },
+ group: "inline",
+ draggable: true,
+ // parseDOM: [{
+ // tag: "img[src]", getAttrs(dom: any) {
+ // return {
+ // src: dom.getAttribute("src"),
+ // title: dom.getAttribute("title"),
+ // alt: dom.getAttribute("alt"),
+ // width: Math.min(100, Number(dom.getAttribute("width"))),
+ // };
+ // }
+ // }],
+ // TODO if we don't define toDom, dragging the image crashes. Why?
+ toDOM(node) {
+ const attrs = { style: `width: ${node.attrs.width}, height: ${node.attrs.height}` };
+ return ["div", { ...node.attrs, ...attrs }];
+ }
+ },
+
video: {
inline: true,
attrs: {
@@ -199,17 +235,20 @@ export const nodes: { [index: string]: NodeSpec } = {
bulletStyle: { default: 0 },
mapStyle: { default: "decimal" },
setFontSize: { default: undefined },
+ setFontFamily: { default: undefined },
inheritedFontSize: { default: undefined },
visibility: { default: true }
},
toDOM(node: Node<any>) {
const bs = node.attrs.bulletStyle;
+ if (node.attrs.mapStyle === "bullet") return ['ul', 0];
const decMap = bs ? "decimal" + bs : "";
const multiMap = bs === 1 ? "decimal1" : bs === 2 ? "upper-alpha" : bs === 3 ? "lower-roman" : bs === 4 ? "lower-alpha" : "";
let map = node.attrs.mapStyle === "decimal" ? decMap : multiMap;
let fsize = node.attrs.setFontSize ? node.attrs.setFontSize : node.attrs.inheritedFontSize;
- return node.attrs.visibility ? ['ol', { class: `${map}-ol`, style: `list-style: none;font-size: ${fsize}` }, 0] :
- ['ol', { class: `${map}-ol`, style: `list-style: none; font-size: ${fsize}` }];
+ let ffam = node.attrs.setFontFamily;
+ return node.attrs.visibility ? ['ol', { class: `${map}-ol`, style: `list-style: none; font-size: ${fsize}; font-family: ${ffam}` }, 0] :
+ ['ol', { class: `${map}-ol`, style: `list-style: none; font-size: ${fsize}; font-family: ${ffam}` }];
}
},
@@ -235,7 +274,7 @@ export const nodes: { [index: string]: NodeSpec } = {
const bs = node.attrs.bulletStyle;
const decMap = bs ? "decimal" + bs : "";
const multiMap = bs === 1 ? "decimal1" : bs === 2 ? "upper-alpha" : bs === 3 ? "lower-roman" : bs === 4 ? "lower-alpha" : "";
- let map = node.attrs.mapStyle === "decimal" ? decMap : multiMap;
+ let map = node.attrs.mapStyle === "decimal" ? decMap : node.attrs.mapStyle === "multi" ? multiMap : "";
return node.attrs.visibility ? ["li", { class: `${map}` }, 0] : ["li", { class: `${map}` }, "..."];
//return ["li", { class: `${map}` }, 0];
}
@@ -266,8 +305,8 @@ 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" }, node.attrs.title], ["br"]] :
- ["a", { ...node.attrs }, 0];
+ ["div", ["span", `"`], ["span", 0], ["span", `"`], ["br"], ["a", { ...node.attrs, class: "prosemirror-attribution", title: `${node.attrs.title}` }, node.attrs.title], ["br"]] :
+ ["a", { ...node.attrs, title: `${node.attrs.title}` }, 0];
}
},
@@ -390,10 +429,13 @@ export const marks: { [index: string]: MarkSpec } = {
},
search_highlight: {
+ attrs: {
+ selected: { default: false }
+ },
parseDOM: [{ style: 'background: yellow' }],
- toDOM() {
+ toDOM(node: any) {
return ['span', {
- style: 'background: yellow'
+ style: `background: ${node.attrs.selected ? "orange" : "yellow"}`
}];
}
},
@@ -402,20 +444,35 @@ export const marks: { [index: string]: MarkSpec } = {
user_mark: {
attrs: {
userid: { default: "" },
- hide_users: { default: [] },
opened: { default: true },
- modified: { default: "when?" }
+ modified: { default: "when?" }, // 5 second intervals since 1970
+ },
+ group: "inline",
+ toDOM(node: any) {
+ let uid = node.attrs.userid.replace(".", "").replace("@", "");
+ let min = Math.round(node.attrs.modified / 12);
+ let hr = Math.round(min / 60);
+ let day = Math.round(hr / 60 / 24);
+ let remote = node.attrs.userid !== Doc.CurrentUserEmail ? " userMark-remote" : "";
+ return node.attrs.opened ?
+ ['span', { class: "userMark-" + uid + remote + " userMark-min-" + min + " userMark-hr-" + hr + " userMark-day-" + day }, 0] :
+ ['span', { class: "userMark-" + uid + remote + " userMark-min-" + min + " userMark-hr-" + hr + " userMark-day-" + day }, ['span', 0]];
+ }
+ },
+ // the id of the user who entered the text
+ user_tag: {
+ attrs: {
+ userid: { default: "" },
+ opened: { default: true },
+ modified: { default: "when?" }, // 5 second intervals since 1970
+ tag: { default: "" }
},
group: "inline",
toDOM(node: any) {
- let hideUsers = node.attrs.hide_users;
- let hidden = hideUsers.indexOf(node.attrs.userid) !== -1 || (hideUsers.length === 0 && node.attrs.userid !== Doc.CurrentUserEmail);
- return hidden ?
- (node.attrs.opened ?
- ['span', { class: "userMarkOpen" }, 0] :
- ['span', { class: "userMark" }, ['span', 0]]
- ) :
- ['span', 0];
+ let uid = node.attrs.userid.replace(".", "").replace("@", "");
+ return node.attrs.opened ?
+ ['span', { class: "userTag-" + uid + " userTag-" + node.attrs.tag }, 0] :
+ ['span', { class: "userTag-" + uid + " userTag-" + node.attrs.tag }, ['span', 0]];
}
},
@@ -426,72 +483,26 @@ export const marks: { [index: string]: MarkSpec } = {
toDOM() { return codeDOM; }
},
- // pFontFamily: {
- // attrs: {
- // style: { default: 'font-family: "Times New Roman", Times, serif;' },
- // },
- // parseDOM: [{
- // tag: "span", getAttrs(dom: any) {
- // if (getComputedStyle(dom).font === "Times New Roman") return { style: `font-family: "Times New Roman", Times, serif;` };
- // if (getComputedStyle(dom).font === "Arial, Helvetica") return { style: `font-family: Arial, Helvetica, sans-serif;` };
- // if (getComputedStyle(dom).font === "Georgia") return { style: `font-family: Georgia, serif;` };
- // if (getComputedStyle(dom).font === "Comic Sans") return { style: `font-family: "Comic Sans MS", cursive, sans-serif;` };
- // if (getComputedStyle(dom).font === "Tahoma, Geneva") return { style: `font-family: Tahoma, Geneva, sans-serif;` };
- // }
- // }],
- // toDOM: (node: any) => ['span', {
- // style: node.attrs.style
- // }]
- // },
-
-
/* FONTS */
- timesNewRoman: {
- parseDOM: [{ style: 'font-family: "Times New Roman", Times, serif;' }],
- toDOM: () => ['span', {
- style: 'font-family: "Times New Roman", Times, serif;'
- }]
- },
-
- arial: {
- parseDOM: [{ style: 'font-family: Arial, Helvetica, sans-serif;' }],
- toDOM: () => ['span', {
- style: 'font-family: Arial, Helvetica, sans-serif;'
- }]
- },
-
- georgia: {
- parseDOM: [{ style: 'font-family: Georgia, serif;' }],
- toDOM: () => ['span', {
- style: 'font-family: Georgia, serif;'
- }]
- },
-
- comicSans: {
- parseDOM: [{ style: 'font-family: "Comic Sans MS", cursive, sans-serif;' }],
- toDOM: () => ['span', {
- style: 'font-family: "Comic Sans MS", cursive, sans-serif;'
- }]
- },
-
- tahoma: {
- parseDOM: [{ style: 'font-family: Tahoma, Geneva, sans-serif;' }],
- toDOM: () => ['span', {
- style: 'font-family: Tahoma, Geneva, sans-serif;'
- }]
- },
-
- impact: {
- parseDOM: [{ style: 'font-family: Impact, Charcoal, sans-serif;' }],
- toDOM: () => ['span', {
- style: 'font-family: Impact, Charcoal, sans-serif;'
- }]
- },
-
- crimson: {
- parseDOM: [{ style: 'font-family: "Crimson Text", sans-serif;' }],
- toDOM: () => ['span', {
- style: 'font-family: "Crimson Text", sans-serif;'
+ pFontFamily: {
+ attrs: {
+ family: { default: "Crimson Text" },
+ },
+ parseDOM: [{
+ tag: "span", getAttrs(dom: any) {
+ let cstyle = getComputedStyle(dom);
+ if (cstyle.font) {
+ if (cstyle.font.indexOf("Times New Roman") !== -1) return { family: "Times New Roman" };
+ if (cstyle.font.indexOf("Arial") !== -1) return { family: "Arial" };
+ if (cstyle.font.indexOf("Georgia") !== -1) return { family: "Georgia" };
+ if (cstyle.font.indexOf("Comic Sans") !== -1) return { family: "Comic Sans MS" };
+ if (cstyle.font.indexOf("Tahoma") !== -1) return { family: "Tahoma" };
+ if (cstyle.font.indexOf("Crimson") !== -1) return { family: "Crimson Text" };
+ }
+ }
+ }],
+ toDOM: (node) => ['span', {
+ style: `font-family: "${node.attrs.family}";`
}]
},
@@ -517,76 +528,6 @@ export const marks: { [index: string]: MarkSpec } = {
style: `font-size: ${node.attrs.fontSize}px;`
}]
},
-
- p10: {
- parseDOM: [{ style: 'font-size: 10px;' }],
- toDOM: () => ['span', {
- style: 'font-size: 10px;'
- }]
- },
-
- p12: {
- parseDOM: [{ style: 'font-size: 12px;' }],
- toDOM: () => ['span', {
- style: 'font-size: 12px;'
- }]
- },
-
- p14: {
- parseDOM: [{ style: 'font-size: 14px;' }],
- toDOM: () => ['span', {
- style: 'font-size: 14px;'
- }]
- },
-
- p16: {
- parseDOM: [{ style: 'font-size: 16px;' }],
- toDOM: () => ['span', {
- style: 'font-size: 16px;'
- }]
- },
-
- p18: {
- parseDOM: [{ style: 'font-size: 18px;' }],
- toDOM: () => ['span', {
- style: 'font-size: 18px;'
- }]
- },
-
- p20: {
- parseDOM: [{ style: 'font-size: 20px;' }],
- toDOM: () => ['span', {
- style: 'font-size: 20px;'
- }]
- },
-
- p24: {
- parseDOM: [{ style: 'font-size: 24px;' }],
- toDOM: () => ['span', {
- style: 'font-size: 24px;'
- }]
- },
-
- p32: {
- parseDOM: [{ style: 'font-size: 32px;' }],
- toDOM: () => ['span', {
- style: 'font-size: 32px;'
- }]
- },
-
- p48: {
- parseDOM: [{ style: 'font-size: 48px;' }],
- toDOM: () => ['span', {
- style: 'font-size: 48px;'
- }]
- },
-
- p72: {
- parseDOM: [{ style: 'font-size: 72px;' }],
- toDOM: () => ['span', {
- style: 'font-size: 72px;'
- }]
- },
};
export class ImageResizeView {
@@ -599,6 +540,7 @@ export class ImageResizeView {
this._outer = document.createElement("span");
this._outer.style.position = "relative";
this._outer.style.width = node.attrs.width;
+ this._outer.style.height = node.attrs.height;
this._outer.style.display = "inline-block";
this._outer.style.overflow = "hidden";
(this._outer.style as any).float = node.attrs.float;
@@ -614,62 +556,51 @@ export class ImageResizeView {
this._handle.style.bottom = "-10px";
this._handle.style.right = "-10px";
let self = this;
+ this._img.onclick = function (e: any) {
+ e.stopPropagation();
+ e.preventDefault();
+ if (view.state.selection.node && view.state.selection.node.type !== view.state.schema.nodes.image) {
+ view.dispatch(view.state.tr.setSelection(new NodeSelection(view.state.doc.resolve(view.state.selection.from - 2))));
+ }
+ };
this._img.onpointerdown = function (e: any) {
- if (!view.isOverlay || e.ctrlKey) {
+ if (e.ctrlKey) {
e.preventDefault();
e.stopPropagation();
- DocServer.GetRefField(node.attrs.docid).then(async linkDoc => {
- if (linkDoc instanceof Doc) {
- let proto = Doc.GetProto(linkDoc);
- let targetContext = await Cast(proto.targetContext, Doc);
- let jumpToDoc = await Cast(linkDoc.anchor2, Doc);
- if (jumpToDoc) {
- if (DocumentManager.Instance.getDocumentView(jumpToDoc)) {
-
- DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, undefined, undefined, NumCast((jumpToDoc === linkDoc.anchor2 ? linkDoc.anchor2Page : linkDoc.anchor1Page)));
- return;
- }
- }
- if (targetContext) {
- DocumentManager.Instance.jumpToDocument(targetContext, e.ctrlKey, false, document => addDocTab(document, undefined, location ? location : "inTab"));
- } else if (jumpToDoc) {
- DocumentManager.Instance.jumpToDocument(jumpToDoc, e.ctrlKey, false, document => addDocTab(document, undefined, location ? location : "inTab"));
- } else {
- DocumentManager.Instance.jumpToDocument(linkDoc, e.ctrlKey, false, document => addDocTab(document, undefined, location ? location : "inTab"));
- }
- }
- });
+ 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));
}
};
this._handle.onpointerdown = function (e: any) {
e.preventDefault();
e.stopPropagation();
+ let wid = Number(getComputedStyle(self._img).width!.replace(/px/, ""));
+ let hgt = Number(getComputedStyle(self._img).height!.replace(/px/, ""));
const startX = e.pageX;
const startWidth = parseFloat(node.attrs.width);
const onpointermove = (e: any) => {
const currentX = e.pageX;
const diffInPx = currentX - startX;
self._outer.style.width = `${startWidth + diffInPx}`;
- //Array.from(FormattedTextBox.InputBoxOverlay!.CurrentDiv.getElementsByTagName("img")).map((img: any) => img.opacity = "0.1");
- FormattedTextBox.InputBoxOverlay!.CurrentDiv.style.opacity = "0";
+ self._outer.style.height = `${(startWidth + diffInPx) * hgt / wid}`;
};
const onpointerup = () => {
document.removeEventListener("pointermove", onpointermove);
document.removeEventListener("pointerup", onpointerup);
- view.dispatch(
- view.state.tr.setSelection(view.state.selection).setNodeMarkup(getPos(), null,
- { ...node.attrs, width: self._outer.style.width })
- );
- FormattedTextBox.InputBoxOverlay!.CurrentDiv.style.opacity = "1";
+ let pos = view.state.selection.from;
+ view.dispatch(view.state.tr.setNodeMarkup(getPos(), null, { ...node.attrs, width: self._outer.style.width, height: self._outer.style.height }));
+ view.dispatch(view.state.tr.setSelection(new NodeSelection(view.state.doc.resolve(pos))));
};
document.addEventListener("pointermove", onpointermove);
document.addEventListener("pointerup", onpointerup);
};
- this._outer.appendChild(this._handle);
this._outer.appendChild(this._img);
+ this._outer.appendChild(this._handle);
(this as any).dom = this._outer;
}
@@ -686,6 +617,88 @@ export class ImageResizeView {
}
}
+export class DashDocView {
+ _dashSpan: HTMLDivElement;
+ _outer: HTMLElement;
+ _dashDoc: Doc | undefined;
+ _reactionDisposer: IReactionDisposer | undefined;
+ _textBox: FormattedTextBox;
+
+ getDocTransform = () => {
+ let { 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;
+ constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) {
+ this._textBox = tbox;
+ this._dashSpan = document.createElement("div");
+ this._outer = document.createElement("span");
+ this._outer.style.position = "relative";
+ this._outer.style.width = node.attrs.width;
+ this._outer.style.height = node.attrs.height;
+ this._outer.style.display = "inline-block";
+ this._outer.style.overflow = "hidden";
+ (this._outer.style as any).float = node.attrs.float;
+
+ this._dashSpan.style.width = node.attrs.width;
+ this._dashSpan.style.height = node.attrs.height;
+ this._dashSpan.style.position = "absolute";
+ this._dashSpan.style.display = "inline-block";
+ let removeDoc = () => {
+ let pos = getPos();
+ let ns = new NodeSelection(view.state.doc.resolve(pos));
+ view.dispatch(view.state.tr.setSelection(ns).deleteSelection());
+ return true;
+ };
+ DocServer.GetRefField(node.attrs.docid).then(async dashDoc => {
+ if (dashDoc instanceof Doc) {
+ self._dashDoc = dashDoc;
+ if (node.attrs.width !== dashDoc.width + "px" || node.attrs.height !== dashDoc.height + "px") {
+ view.dispatch(view.state.tr.setNodeMarkup(getPos(), null, { ...node.attrs, width: dashDoc.width + "px", height: dashDoc.height + "px" }));
+ }
+ this._reactionDisposer && this._reactionDisposer();
+ this._reactionDisposer = reaction(() => dashDoc[HeightSym]() + dashDoc[WidthSym](), () => {
+ this._dashSpan.style.height = this._outer.style.height = dashDoc[HeightSym]() + "px";
+ this._dashSpan.style.width = this._outer.style.width = dashDoc[WidthSym]() + "px";
+ });
+ ReactDOM.render(<DocumentView
+ fitToBox={BoolCast(dashDoc.fitToBox)}
+ Document={dashDoc}
+ addDocument={returnFalse}
+ removeDocument={removeDoc}
+ ruleProvider={undefined}
+ ScreenToLocalTransform={this.getDocTransform}
+ addDocTab={self._textBox.props.addDocTab}
+ pinToPres={returnFalse}
+ renderDepth={1}
+ PanelWidth={self._dashDoc[WidthSym]}
+ PanelHeight={self._dashDoc[HeightSym]}
+ focus={emptyFunction}
+ backgroundColor={returnEmptyString}
+ parentActive={returnFalse}
+ whenActiveChanged={returnFalse}
+ bringToFront={emptyFunction}
+ zoomToScale={emptyFunction}
+ getScale={returnOne}
+ ContainingCollectionView={undefined}
+ ContainingCollectionDoc={undefined}
+ ContentScaling={this.contentScaling}
+ />, this._dashSpan);
+ }
+ });
+ let self = this;
+ this._dashSpan.onkeydown = function (e: any) { e.stopPropagation(); };
+ this._dashSpan.onkeypress = function (e: any) { e.stopPropagation(); };
+ this._dashSpan.onwheel = function (e: any) { e.preventDefault(); };
+ this._dashSpan.onkeyup = function (e: any) { e.stopPropagation(); };
+ this._outer.appendChild(this._dashSpan);
+ (this as any).dom = this._outer;
+ }
+ destroy() {
+ this._reactionDisposer && this._reactionDisposer();
+ }
+}
+
export class OrderedListView {
update(node: any) {
return false; // if attr's of an ordered_list (e.g., bulletStyle) change, return false forces the dom node to be recreated which is necessary for the bullet labels to update
@@ -725,7 +738,6 @@ export class FootnoteView {
if (this.innerView) this.close();
}
open() {
- if (!this.outerView.isOverlay) return;
// Append a tooltip to the outer node
let tooltip = this.dom.appendChild(document.createElement("div"));
tooltip.className = "footnote-tooltip";
@@ -739,7 +751,14 @@ export class FootnoteView {
"Mod-z": () => undo(this.outerView.state, this.outerView.dispatch),
"Mod-y": () => redo(this.outerView.state, this.outerView.dispatch),
"Mod-b": toggleMark(schema.marks.strong)
- })]
+ }),
+ new Plugin({
+ view(newView) {
+ return FormattedTextBox.getToolTip(newView);
+ }
+ })
+ ],
+
}),
// This is the magic part
dispatchTransaction: this.dispatchInner.bind(this),
@@ -780,10 +799,10 @@ export class FootnoteView {
if (!tr.getMeta("fromOutside")) {
let outerTr = this.outerView.state.tr, offsetMap = StepMap.offset(this.getPos() + 1);
- for (let i = 0; i < transactions.length; i++) {
- let steps = transactions[i].steps;
- for (let j = 0; j < steps.length; j++) {
- outerTr.step(steps[j].map(offsetMap));
+ for (let transaction of transactions) {
+ let steps = transaction.steps;
+ for (let step of steps) {
+ outerTr.step(step.map(offsetMap));
}
}
if (outerTr.docChanged) this.outerView.dispatch(outerTr);
diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts
index ee5a83710..6706dcb89 100644
--- a/src/client/util/SearchUtil.ts
+++ b/src/client/util/SearchUtil.ts
@@ -3,18 +3,21 @@ import { DocServer } from '../DocServer';
import { Doc } from '../../new_fields/Doc';
import { Id } from '../../new_fields/FieldSymbols';
import { Utils } from '../../Utils';
+import { DocumentType } from '../documents/DocumentTypes';
export namespace SearchUtil {
export type HighlightingResult = { [id: string]: { [key: string]: string[] } };
export interface IdSearchResult {
ids: string[];
+ lines: string[][];
numFound: number;
highlighting: HighlightingResult | undefined;
}
export interface DocSearchResult {
docs: Doc[];
+ lines: string[][];
numFound: number;
highlighting: HighlightingResult | undefined;
}
@@ -30,16 +33,53 @@ 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 result: IdSearchResult = JSON.parse(await rp.get(Utils.prepend("/search"), {
+ let result: IdSearchResult = JSON.parse(await rp.get(Utils.prepend("/search"), {
qs: { ...options, q: query },
}));
if (!returnDocs) {
return result;
}
- const { ids, numFound, highlighting } = result;
+
+ let { ids, numFound, highlighting } = result;
+
+ let txtresult = query !== "*" && JSON.parse(await rp.get(Utils.prepend("/textsearch"), {
+ qs: { ...options, q: query },
+ }));
+
+ let fileids = txtresult ? txtresult.ids : [];
+ let newIds: string[] = [];
+ let newLines: string[][] = [];
+ await Promise.all(fileids.map(async (tr: string, i: number) => {
+ let docQuery = "fileUpload_t:" + tr.substr(0, 7); //If we just have a filter query, search for * as the query
+ let docResult = JSON.parse(await rp.get(Utils.prepend("/search"), { qs: { ...options, q: docQuery } }));
+ newIds.push(...docResult.ids);
+ newLines.push(...docResult.ids.map((dr: any) => txtresult.lines[i]));
+ }));
+
+
+ let theDocs: Doc[] = [];
+ let theLines: string[][] = [];
+ const textDocMap = await DocServer.GetRefFields(newIds);
+ const textDocs = newIds.map((id: string) => textDocMap[id]).map(doc => doc as Doc);
+ for (let i = 0; i < textDocs.length; i++) {
+ let testDoc = textDocs[i];
+ if (testDoc instanceof Doc && testDoc.type !== DocumentType.KVP && theDocs.findIndex(d => Doc.AreProtosEqual(d, testDoc)) === -1) {
+ theDocs.push(Doc.GetProto(testDoc));
+ theLines.push(newLines[i].map(line => line.replace(query, query.toUpperCase())));
+ }
+ }
+
const docMap = await DocServer.GetRefFields(ids);
- const docs = ids.map((id: string) => docMap[id]).filter((doc: any) => doc instanceof Doc);
- return { docs, numFound, highlighting };
+ const docs = ids.map((id: string) => docMap[id]).map(doc => doc as Doc);
+ for (let i = 0; i < ids.length; i++) {
+ let testDoc = docs[i];
+ if (testDoc instanceof Doc && testDoc.type !== DocumentType.KVP && theDocs.findIndex(d => Doc.AreProtosEqual(d, testDoc)) === -1) {
+ theDocs.push(testDoc);
+ theLines.push([]);
+ }
+ }
+
+ return { docs: theDocs, numFound: theDocs.length, highlighting, lines: theLines };
}
export async function GetAliasesOfDocument(doc: Doc): Promise<Doc[]>;
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index a02a270ee..2d717ca57 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -27,7 +27,6 @@ export namespace SelectionManager {
} else if (!ctrlPressed && manager.SelectedDocuments.length > 1) {
manager.SelectedDocuments.map(dv => dv !== docView && dv.props.whenActiveChanged(false));
manager.SelectedDocuments = [docView];
- FormattedTextBox.InputBoxOverlay = undefined;
}
}
@action
@@ -42,22 +41,10 @@ export namespace SelectionManager {
DeselectAll(): void {
manager.SelectedDocuments.map(dv => dv.props.whenActiveChanged(false));
manager.SelectedDocuments = [];
- FormattedTextBox.InputBoxOverlay = undefined;
}
}
const manager = new Manager();
- reaction(() => manager.SelectedDocuments, sel => {
- let targetColor = "#FFFFFF";
- if (sel.length > 0) {
- let firstView = sel[0];
- let doc = firstView.props.Document;
- let targetDoc = doc.isTemplate ? doc : Doc.GetProto(doc);
- let stored = StrCast(targetDoc.backgroundColor);
- stored.length > 0 && (targetColor = stored);
- }
- InkingControl.Instance.updateSelectedColor(targetColor);
- }, { fireImmediately: true });
export function DeselectDoc(docView: DocumentView): void {
manager.DeselectDoc(docView);
diff --git a/src/client/util/SharingManager.scss b/src/client/util/SharingManager.scss
new file mode 100644
index 000000000..dec9f751a
--- /dev/null
+++ b/src/client/util/SharingManager.scss
@@ -0,0 +1,140 @@
+.sharing-interface {
+ display: flex;
+ flex-direction: column;
+
+ .focus-span {
+ text-decoration: underline;
+ }
+
+ p {
+ font-size: 20px;
+ text-align: left;
+ font-style: italic;
+ padding: 0;
+ margin: 0 0 20px 0;
+ }
+
+ .hr-substitute {
+ border: solid black 0.5px;
+ margin-top: 20px;
+ }
+
+ .people-with-container {
+ display: flex;
+ height: 25px;
+
+ .people-with {
+ font-size: 14px;
+ margin: 0;
+ padding-top: 3px;
+ font-style: normal;
+ }
+
+ .people-with-select {
+ width: 126px;
+ outline: none;
+ }
+ }
+
+ .share-individual {
+ margin-top: 20px;
+ margin-bottom: 20px;
+ }
+
+ .users-list {
+ font-style: italic;
+ background: white;
+ border: 1px solid black;
+ padding-left: 10px;
+ padding-right: 10px;
+ max-height: 200px;
+ overflow: scroll;
+ height: -webkit-fill-available;
+ text-align: left;
+ display: flex;
+ align-content: center;
+ align-items: center;
+ text-align: center;
+ justify-content: center;
+ color: red;
+ }
+
+ .container {
+ display: block;
+ position: relative;
+ margin-top: 10px;
+ margin-bottom: 10px;
+ font-size: 22px;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none;
+ width: 700px;
+ min-width: 700px;
+ max-width: 700px;
+ text-align: left;
+ font-style: normal;
+ font-size: 15;
+ font-weight: normal;
+ padding: 0;
+
+ .padding {
+ padding: 0 0 0 20px;
+ color: black;
+ }
+
+ .permissions-dropdown {
+ outline: none;
+ }
+ }
+
+ .no-users {
+ margin-top: 20px;
+ }
+
+ .link-container {
+ display: flex;
+ flex-direction: row;
+ margin-bottom: 10px;
+ margin-left: auto;
+ margin-right: auto;
+
+ .link-box,
+ .copy {
+ padding: 10px;
+ border-radius: 10px;
+ padding: 10px;
+ border: solid black 1px;
+ }
+
+ .link-box {
+ background: white;
+ color: blue;
+ text-decoration: underline;
+ }
+
+ .copy {
+ margin-left: 20px;
+ cursor: alias;
+ border-radius: 50%;
+ width: 42px;
+ height: 42px;
+ transition: 1.5s all ease;
+ padding-top: 12px;
+ }
+ }
+
+ .close-button {
+ border-radius: 5px;
+ margin-top: 20px;
+ padding: 10px 0;
+ background: aliceblue;
+ transition: 0.5s ease all;
+ border: 1px solid;
+ border-color: aliceblue;
+ }
+
+ .close-button:hover {
+ border-color: black;
+ }
+} \ No newline at end of file
diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx
new file mode 100644
index 000000000..2082d6324
--- /dev/null
+++ b/src/client/util/SharingManager.tsx
@@ -0,0 +1,300 @@
+import { observable, runInAction, action } from "mobx";
+import * as React from "react";
+import MainViewModal from "../views/MainViewModal";
+import { Doc, Opt, DocCastAsync } from "../../new_fields/Doc";
+import { DocServer } from "../DocServer";
+import { Cast, StrCast } from "../../new_fields/Types";
+import { RouteStore } from "../../server/RouteStore";
+import * as RequestPromise from "request-promise";
+import { Utils } from "../../Utils";
+import "./SharingManager.scss";
+import { Id } from "../../new_fields/FieldSymbols";
+import { observer } from "mobx-react";
+import { MainView } from "../views/MainView";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { library } from '@fortawesome/fontawesome-svg-core';
+import * as fa from '@fortawesome/free-solid-svg-icons';
+import { DocumentView } from "../views/nodes/DocumentView";
+import { SelectionManager } from "./SelectionManager";
+import { DocumentManager } from "./DocumentManager";
+import { CollectionView } from "../views/collections/CollectionView";
+import { DictationOverlay } from "../views/DictationOverlay";
+
+library.add(fa.faCopy);
+
+export interface User {
+ email: string;
+ userDocumentId: string;
+}
+
+export enum SharingPermissions {
+ None = "Not Shared",
+ View = "Can View",
+ Comment = "Can Comment",
+ Edit = "Can Edit"
+}
+
+const ColorMapping = new Map<string, string>([
+ [SharingPermissions.None, "red"],
+ [SharingPermissions.View, "maroon"],
+ [SharingPermissions.Comment, "blue"],
+ [SharingPermissions.Edit, "green"]
+]);
+
+const SharingKey = "sharingPermissions";
+const PublicKey = "publicLinkPermissions";
+const DefaultColor = "black";
+
+interface ValidatedUser {
+ user: User;
+ notificationDoc: Doc;
+}
+
+const storage = "data";
+
+@observer
+export default class SharingManager extends React.Component<{}> {
+ public static Instance: SharingManager;
+ @observable private isOpen = false;
+ @observable private users: ValidatedUser[] = [];
+ @observable private targetDoc: Doc | undefined;
+ @observable private targetDocView: DocumentView | undefined;
+ @observable private copied = false;
+ @observable private dialogueBoxOpacity = 1;
+ @observable private overlayOpacity = 0.4;
+
+ private get linkVisible() {
+ return this.sharingDoc ? this.sharingDoc[PublicKey] !== SharingPermissions.None : false;
+ }
+
+ public open = (target: DocumentView) => {
+ SelectionManager.DeselectAll();
+ this.populateUsers().then(action(() => {
+ this.targetDocView = target;
+ this.targetDoc = target.props.Document;
+ DictationOverlay.Instance.hasActiveModal = true;
+ this.isOpen = true;
+ if (!this.sharingDoc) {
+ this.sharingDoc = new Doc;
+ }
+ }));
+ }
+
+ public close = action(() => {
+ this.isOpen = false;
+ this.users = [];
+ setTimeout(action(() => {
+ this.copied = false;
+ DictationOverlay.Instance.hasActiveModal = false;
+ this.targetDoc = undefined;
+ }), 500);
+ });
+
+ private get sharingDoc() {
+ return this.targetDoc ? Cast(this.targetDoc[SharingKey], Doc) as Doc : undefined;
+ }
+
+ private set sharingDoc(value: Doc | undefined) {
+ this.targetDoc && (this.targetDoc[SharingKey] = value);
+ }
+
+ constructor(props: {}) {
+ super(props);
+ SharingManager.Instance = this;
+ }
+
+ populateUsers = async () => {
+ let userList = await RequestPromise.get(Utils.prepend(RouteStore.getUsers));
+ const raw = JSON.parse(userList) as User[];
+ const evaluating = raw.map(async user => {
+ let isCandidate = user.email !== Doc.CurrentUserEmail;
+ if (isCandidate) {
+ const userDocument = await DocServer.GetRefField(user.userDocumentId);
+ if (userDocument instanceof Doc) {
+ const notificationDoc = await Cast(userDocument.optionalRightCollection, Doc);
+ runInAction(() => {
+ if (notificationDoc instanceof Doc) {
+ this.users.push({ user, notificationDoc });
+ }
+ });
+ }
+ }
+ });
+ return Promise.all(evaluating);
+ }
+
+ setInternalSharing = async (recipient: ValidatedUser, state: string) => {
+ const { user, notificationDoc } = recipient;
+ const target = this.targetDoc!;
+ const manager = this.sharingDoc!;
+ const key = user.userDocumentId;
+ if (state === SharingPermissions.None) {
+ const metadata = (await DocCastAsync(manager[key]));
+ if (metadata) {
+ let sharedAlias = (await DocCastAsync(metadata.sharedAlias))!;
+ Doc.RemoveDocFromList(notificationDoc, storage, sharedAlias);
+ manager[key] = undefined;
+ }
+ } else {
+ const sharedAlias = Doc.MakeAlias(target);
+ Doc.AddDocToList(notificationDoc, storage, sharedAlias);
+ const metadata = new Doc;
+ metadata.permissions = state;
+ metadata.sharedAlias = sharedAlias;
+ manager[key] = metadata;
+ }
+ }
+
+ private setExternalSharing = (state: string) => {
+ let sharingDoc = this.sharingDoc;
+ if (!sharingDoc) {
+ return;
+ }
+ sharingDoc[PublicKey] = state;
+ }
+
+ private get sharingUrl() {
+ if (!this.targetDoc) {
+ return undefined;
+ }
+ let baseUrl = Utils.prepend("/doc/" + this.targetDoc[Id]);
+ return `${baseUrl}?sharing=true`;
+ }
+
+ copy = action(() => {
+ if (this.sharingUrl) {
+ Utils.CopyText(this.sharingUrl);
+ this.copied = true;
+ }
+ });
+
+ private get sharingOptions() {
+ return Object.values(SharingPermissions).map(permission => {
+ return (
+ <option key={permission} value={permission}>
+ {permission}
+ </option>
+ );
+ });
+ }
+
+ private focusOn = (contents: string) => {
+ let title = this.targetDoc ? StrCast(this.targetDoc.title) : "";
+ return (
+ <span
+ className={"focus-span"}
+ title={title}
+ onClick={() => {
+ let context: Opt<CollectionView>;
+ if (this.targetDoc && this.targetDocView && (context = this.targetDocView.props.ContainingCollectionView)) {
+ DocumentManager.Instance.jumpToDocument(this.targetDoc, true, undefined, context.props.Document);
+ }
+ }}
+ onPointerEnter={action(() => {
+ if (this.targetDoc) {
+ Doc.BrushDoc(this.targetDoc);
+ this.dialogueBoxOpacity = 0.1;
+ this.overlayOpacity = 0.1;
+ }
+ })}
+ onPointerLeave={action(() => {
+ this.targetDoc && Doc.UnBrushDoc(this.targetDoc);
+ this.dialogueBoxOpacity = 1;
+ this.overlayOpacity = 0.4;
+ })}
+ >
+ {contents}
+ </span>
+ );
+ }
+
+ private computePermissions = (userKey: string) => {
+ const sharingDoc = this.sharingDoc;
+ if (!sharingDoc) {
+ return SharingPermissions.None;
+ }
+ const metadata = sharingDoc[userKey] as Doc;
+ if (!metadata) {
+ return SharingPermissions.None;
+ }
+ return StrCast(metadata.permissions, SharingPermissions.None);
+ }
+
+ private get sharingInterface() {
+ const existOtherUsers = this.users.length > 0;
+ return (
+ <div className={"sharing-interface"}>
+ <p className={"share-link"}>Manage the public link to {this.focusOn("this document...")}</p>
+ {!this.linkVisible ? (null) :
+ <div className={"link-container"}>
+ <div className={"link-box"} onClick={this.copy}>{this.sharingUrl}</div>
+ <div
+ title={"Copy link to clipboard"}
+ className={"copy"}
+ style={{ backgroundColor: this.copied ? "lawngreen" : "gainsboro" }}
+ onClick={this.copy}
+ >
+ <FontAwesomeIcon icon={fa.faCopy} />
+ </div>
+ </div>
+ }
+ <div className={"people-with-container"}>
+ {!this.linkVisible ? (null) : <p className={"people-with"}>People with this link</p>}
+ <select
+ className={"people-with-select"}
+ value={this.sharingDoc ? StrCast(this.sharingDoc[PublicKey], SharingPermissions.None) : SharingPermissions.None}
+ style={{
+ marginLeft: this.linkVisible ? 10 : 0,
+ color: this.sharingDoc ? ColorMapping.get(StrCast(this.sharingDoc[PublicKey], SharingPermissions.None)) : DefaultColor,
+ borderColor: this.sharingDoc ? ColorMapping.get(StrCast(this.sharingDoc[PublicKey], SharingPermissions.None)) : DefaultColor
+ }}
+ onChange={e => this.setExternalSharing(e.currentTarget.value)}
+ >
+ {this.sharingOptions}
+ </select>
+ </div>
+ <div className={"hr-substitute"} />
+ <p className={"share-individual"}>Privately share {this.focusOn("this document")} with an individual...</p>
+ <div className={"users-list"} style={{ display: existOtherUsers ? "block" : "flex", minHeight: existOtherUsers ? undefined : 200 }}>
+ {!existOtherUsers ? "There are no other users in your database." :
+ this.users.map(({ user, notificationDoc }) => {
+ const userKey = user.userDocumentId;
+ const permissions = this.computePermissions(userKey);
+ const color = ColorMapping.get(permissions);
+ return (
+ <div
+ key={userKey}
+ className={"container"}
+ >
+ <select
+ className={"permissions-dropdown"}
+ value={permissions}
+ style={{ color, borderColor: color }}
+ onChange={e => this.setInternalSharing({ user, notificationDoc }, e.currentTarget.value)}
+ >
+ {this.sharingOptions}
+ </select>
+ <span className={"padding"}>{user.email}</span>
+ </div>
+ );
+ })
+ }
+ </div>
+ <div className={"close-button"} onClick={this.close}>Done</div>
+ </div>
+ );
+ }
+
+ render() {
+ return (
+ <MainViewModal
+ contents={this.sharingInterface}
+ isDisplayed={this.isOpen}
+ interactive={true}
+ dialogueBoxDisplayedOpacity={this.dialogueBoxOpacity}
+ overlayDisplayedOpacity={this.overlayOpacity}
+ />
+ );
+ }
+
+} \ No newline at end of file
diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx
index a83a3949d..38471a955 100644
--- a/src/client/util/TooltipTextMenu.tsx
+++ b/src/client/util/TooltipTextMenu.tsx
@@ -10,7 +10,6 @@ import { Doc, Field, Opt } from "../../new_fields/Doc";
import { Id } from "../../new_fields/FieldSymbols";
import { Utils } from "../../Utils";
import { DocServer } from "../DocServer";
-import { CollectionDockingView } from "../views/collections/CollectionDockingView";
import { FieldViewProps } from "../views/nodes/FieldView";
import { FormattedTextBoxProps } from "../views/nodes/FormattedTextBox";
import { DocumentManager } from "./DocumentManager";
@@ -18,8 +17,9 @@ import { DragManager } from "./DragManager";
import { LinkManager } from "./LinkManager";
import { schema } from "./RichTextSchema";
import "./TooltipTextMenu.scss";
-import { Cast, NumCast } from '../../new_fields/Types';
+import { Cast, NumCast, StrCast } from '../../new_fields/Types';
import { updateBullets } from './ProsemirrorExampleTransfer';
+import { DocumentDecorations } from '../views/DocumentDecorations';
const { toggleMark, setBlockType } = require("prosemirror-commands");
const { openPrompt, TextField } = require("./ProsemirrorCopy/prompt.js");
@@ -28,12 +28,10 @@ export class TooltipTextMenu {
public tooltip: HTMLElement;
private view: EditorView;
- private fontStyles: MarkType[];
- private fontSizes: MarkType[];
+ private editorProps: FieldViewProps & FormattedTextBoxProps | undefined;
private listTypes: (NodeType | any)[];
- private editorProps: FieldViewProps & FormattedTextBoxProps;
- private fontSizeToNum: Map<MarkType, number>;
- private fontStylesToName: Map<MarkType, string>;
+ private fontSizes: Mark[] = [];
+ private fontStyles: Mark[] = [];
private listTypeToIcon: Map<NodeType | any, string>;
//private link: HTMLAnchorElement;
private wrapper: HTMLDivElement;
@@ -59,9 +57,8 @@ export class TooltipTextMenu {
private _collapsed: boolean = false;
- constructor(view: EditorView, editorProps: FieldViewProps & FormattedTextBoxProps) {
+ constructor(view: EditorView) {
this.view = view;
- this.editorProps = editorProps;
this.wrapper = document.createElement("div");
this.tooltip = document.createElement("div");
@@ -120,10 +117,10 @@ export class TooltipTextMenu {
//pointer down handler to activate button effects
dom.addEventListener("pointerdown", e => {
e.preventDefault();
- view.focus();
+ this.view.focus();
if (dom.contains(e.target as Node)) {
e.stopPropagation();
- command(view.state, view.dispatch, view);
+ command(this.view.state, this.view.dispatch, this.view);
// if (this.view.state.selection.empty) {
// if (dom.style.color === "white") { dom.style.color = "greenyellow"; }
// else { dom.style.color = "white"; }
@@ -136,45 +133,32 @@ export class TooltipTextMenu {
//list of font styles
- this.fontStylesToName = new Map();
- this.fontStylesToName.set(schema.marks.timesNewRoman, "Times New Roman");
- this.fontStylesToName.set(schema.marks.arial, "Arial");
- this.fontStylesToName.set(schema.marks.georgia, "Georgia");
- this.fontStylesToName.set(schema.marks.comicSans, "Comic Sans MS");
- this.fontStylesToName.set(schema.marks.tahoma, "Tahoma");
- this.fontStylesToName.set(schema.marks.impact, "Impact");
- this.fontStylesToName.set(schema.marks.crimson, "Crimson Text");
- this.fontStyles = Array.from(this.fontStylesToName.keys());
+ this.fontStyles.push(schema.marks.pFontFamily.create({ family: "Times New Roman" }));
+ this.fontStyles.push(schema.marks.pFontFamily.create({ family: "Arial" }));
+ this.fontStyles.push(schema.marks.pFontFamily.create({ family: "Georgia" }));
+ this.fontStyles.push(schema.marks.pFontFamily.create({ family: "Comic Sans MS" }));
+ this.fontStyles.push(schema.marks.pFontFamily.create({ family: "Tahoma" }));
+ this.fontStyles.push(schema.marks.pFontFamily.create({ family: "Impact" }));
+ this.fontStyles.push(schema.marks.pFontFamily.create({ family: "Crimson Text" }));
//font sizes
- this.fontSizeToNum = new Map();
- this.fontSizeToNum.set(schema.marks.p10, 10);
- this.fontSizeToNum.set(schema.marks.p12, 12);
- this.fontSizeToNum.set(schema.marks.p14, 14);
- this.fontSizeToNum.set(schema.marks.p16, 16);
- this.fontSizeToNum.set(schema.marks.p18, 18);
- this.fontSizeToNum.set(schema.marks.p20, 20);
- this.fontSizeToNum.set(schema.marks.p24, 24);
- this.fontSizeToNum.set(schema.marks.p32, 32);
- this.fontSizeToNum.set(schema.marks.p48, 48);
- this.fontSizeToNum.set(schema.marks.p72, 72);
- this.fontSizeToNum.set(schema.marks.pFontSize, 10);
- // this.fontSizeToNum.set(schema.marks.pFontSize, 12);
- // this.fontSizeToNum.set(schema.marks.pFontSize, 14);
- // this.fontSizeToNum.set(schema.marks.pFontSize, 16);
- // this.fontSizeToNum.set(schema.marks.pFontSize, 18);
- // this.fontSizeToNum.set(schema.marks.pFontSize, 20);
- // this.fontSizeToNum.set(schema.marks.pFontSize, 24);
- // this.fontSizeToNum.set(schema.marks.pFontSize, 32);
- // this.fontSizeToNum.set(schema.marks.pFontSize, 48);
- // this.fontSizeToNum.set(schema.marks.pFontSize, 72);
- this.fontSizes = Array.from(this.fontSizeToNum.keys());
+ this.fontSizes.push(schema.marks.pFontSize.create({ fontSize: 10 }));
+ this.fontSizes.push(schema.marks.pFontSize.create({ fontSize: 12 }));
+ this.fontSizes.push(schema.marks.pFontSize.create({ fontSize: 14 }));
+ this.fontSizes.push(schema.marks.pFontSize.create({ fontSize: 16 }));
+ this.fontSizes.push(schema.marks.pFontSize.create({ fontSize: 18 }));
+ this.fontSizes.push(schema.marks.pFontSize.create({ fontSize: 20 }));
+ this.fontSizes.push(schema.marks.pFontSize.create({ fontSize: 24 }));
+ this.fontSizes.push(schema.marks.pFontSize.create({ fontSize: 32 }));
+ this.fontSizes.push(schema.marks.pFontSize.create({ fontSize: 48 }));
+ this.fontSizes.push(schema.marks.pFontSize.create({ fontSize: 72 }));
//list types
this.listTypeToIcon = new Map();
- this.listTypeToIcon.set(schema.nodes.bullet_list, ":");
- this.listTypeToIcon.set(schema.nodes.ordered_list.create({ mapStyle: "decimal" }), "1.1");
- this.listTypeToIcon.set(schema.nodes.ordered_list.create({ mapStyle: "multi" }), "1.A");
+ //this.listTypeToIcon.set(schema.nodes.bullet_list, ":");
+ this.listTypeToIcon.set(schema.nodes.ordered_list.create({ mapStyle: "bullet" }), ":");
+ this.listTypeToIcon.set(schema.nodes.ordered_list.create({ mapStyle: "decimal" }), "1.1)");
+ this.listTypeToIcon.set(schema.nodes.ordered_list.create({ mapStyle: "multi" }), "1.A)");
// this.listTypeToIcon.set(schema.nodes.bullet_list, "⬜");
this.listTypes = Array.from(this.listTypeToIcon.keys());
@@ -188,25 +172,20 @@ export class TooltipTextMenu {
this.updateListItemDropdown(":", this.listTypeBtnDom);
- this.update(view, undefined);
-
- // add tooltip to outerdiv to circumvent scaling problem
- const outer_div = this.editorProps.outer_div;
- outer_div && outer_div(this.wrapper);
+ this.updateFromDash(view, undefined, undefined);
+ TooltipTextMenu.Toolbar = this.wrapper;
}
+ public static Toolbar: HTMLDivElement | undefined;
//label of dropdown will change to given label
updateFontSizeDropdown(label: string) {
- //filtering function - might be unecessary
- let cut = (arr: MenuItem[]) => arr.filter(x => x);
-
//font SIZES
let fontSizeBtns: MenuItem[] = [];
- this.fontSizeToNum.forEach((number, mark) => {
- fontSizeBtns.push(this.dropdownMarkBtn(String(number), "color: black; width: 50px;", mark, this.view, this.changeToMarkInGroup, this.fontSizes));
+ this.fontSizes.forEach(mark => {
+ fontSizeBtns.push(this.dropdownFontSizeBtn(String(mark.attrs.fontSize), "color: black; width: 50px;", mark, this.view, this.changeToFontSize));
});
- let newfontSizeDom = (new Dropdown(cut(fontSizeBtns), {
+ let newfontSizeDom = (new Dropdown(fontSizeBtns, {
label: label,
css: "color:black; min-width: 60px; padding-left: 5px; margin-right: 0;"
}) as MenuItem).render(this.view).dom;
@@ -217,20 +196,15 @@ export class TooltipTextMenu {
this.fontSizeDom = newfontSizeDom;
}
- // Make the DIV element draggable
-
//label of dropdown will change to given label
updateFontStyleDropdown(label: string) {
- //filtering function - might be unecessary
- let cut = (arr: MenuItem[]) => arr.filter(x => x);
-
//font STYLES
let fontBtns: MenuItem[] = [];
- this.fontStylesToName.forEach((name, mark) => {
- fontBtns.push(this.dropdownMarkBtn(name, "color: black; font-family: " + name + ", sans-serif; width: 125px;", mark, this.view, this.changeToMarkInGroup, this.fontStyles));
+ this.fontStyles.forEach((mark) => {
+ fontBtns.push(this.dropdownFontFamilyBtn(mark.attrs.family, "color: black; font-family: " + mark.attrs.family + ", sans-serif; width: 125px;", mark, this.view, this.changeToFontFamily));
});
- let newfontStyleDom = (new Dropdown(cut(fontBtns), {
+ let newfontStyleDom = (new Dropdown(fontBtns, {
label: label,
css: "color:black; width: 125px; margin-left: -3px; padding-left: 2px;"
}) as MenuItem).render(this.view).dom;
@@ -275,7 +249,7 @@ export class TooltipTextMenu {
if (DocumentManager.Instance.getDocumentView(f)) {
DocumentManager.Instance.getDocumentView(f)!.props.focus(f, false);
}
- else this.editorProps.addDocTab(f, undefined, "onRight");
+ else this.editorProps && this.editorProps.addDocTab(f, undefined, "onRight");
}
}));
}
@@ -293,6 +267,7 @@ export class TooltipTextMenu {
this.linkDrag.style.background = "black";
this.linkDrag.style.cssFloat = "left";
this.linkDrag.onpointerdown = (e: PointerEvent) => {
+ if (!this.editorProps) return;
let dragData = new DragManager.LinkDragData(this.editorProps.Document);
dragData.dontClearTextBox = true;
// hack to get source context -sy
@@ -309,7 +284,7 @@ export class TooltipTextMenu {
if (proto && docView) {
proto.sourceContext = docView.props.ContainingCollectionDoc;
}
- let text = this.makeLink(linkDoc, ctrlKey ? "onRight" : "inTab");
+ let text = this.makeLink(linkDoc, StrCast(linkDoc.anchor2.title), ctrlKey ? "onRight" : "inTab");
if (linkDoc instanceof Doc && linkDoc.anchor2 instanceof Doc) {
proto.title = text === "" ? proto.title : text + " to " + linkDoc.anchor2.title; // TODODO open to more descriptive descriptions of following in text link
}
@@ -399,25 +374,20 @@ export class TooltipTextMenu {
// let link = state.schema.mark(state.schema.marks.link, { href: target, location: location });
// }
- makeLink = (targetDoc: Doc, location: string): string => {
- let target = Utils.prepend("/doc/" + targetDoc[Id]);
+ makeLink = (targetDoc: Doc, title: string, location: string): string => {
+ let link = this.view.state.schema.marks.link.create({ href: Utils.prepend("/doc/" + targetDoc[Id]), title: title, location: location });
+ 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));
let node = this.view.state.selection.$from.nodeAfter;
- let link = this.view.state.schema.mark(this.view.state.schema.marks.link, { href: target, location: location, guid: targetDoc[Id] });
- this.view.dispatch(this.view.state.tr.removeMark(this.view.state.selection.from, this.view.state.selection.to, this.view.state.schema.marks.link));
- this.view.dispatch(this.view.state.tr.addMark(this.view.state.selection.from, this.view.state.selection.to, link));
- node = this.view.state.selection.$from.nodeAfter;
- link = node && node.marks.find(m => m.type.name === "link");
- if (node) {
- if (node.text) {
- return node.text;
- }
+ if (node && node.text) {
+ return node.text;
}
return "";
}
deleteLink = () => {
let node = this.view.state.selection.$from.nodeAfter;
- let link = node && node.marks.find(m => m.type.name === "link");
+ let link = node && node.marks.find(m => m.type === this.view.state.schema.marks.link);
let href = link!.attrs.href;
if (href) {
if (href.indexOf(Utils.prepend("/doc/")) === 0) {
@@ -442,7 +412,7 @@ export class TooltipTextMenu {
let tr = state.tr;
tr.addMark(state.selection.from, state.selection.to, mark);
let content = tr.selection.content();
- let newNode = schema.nodes.star.create({ visibility: false, text: content, textslice: content.toJSON() });
+ let newNode = state.schema.nodes.star.create({ visibility: false, text: content, textslice: content.toJSON() });
dispatch && dispatch(tr.replaceSelectionWith(newNode).removeMark(tr.selection.from - 1, tr.selection.from, mark));
return true;
}
@@ -469,8 +439,7 @@ export class TooltipTextMenu {
this.tooltip.appendChild(listTypeBtn);
return listTypeBtn;
}
-
- //for a specific grouping of marks (passed in), remove all and apply the passed-in one to the selected text
+ //for a specific grouping of marks (passed in), remove all and apply the passed-in one to the selected textchangeToMarkInGroup = (markType: MarkType | undefined, view: EditorView, fontMarks: MarkType[]) => {
changeToMarkInGroup = (markType: MarkType | undefined, view: EditorView, fontMarks: MarkType[]) => {
let { $cursor, ranges } = view.state.selection as TextSelection;
let state = view.state;
@@ -499,71 +468,143 @@ export class TooltipTextMenu {
});
if (markType) {
- // fontsize
- if (markType.name[0] === 'p') {
- let size = this.fontSizeToNum.get(markType);
- if (size) { this.updateFontSizeDropdown(String(size) + " pt"); }
- let ruleProvider = this.editorProps.ruleProvider;
- let heading = NumCast(this.editorProps.Document.heading);
- if (ruleProvider && heading) {
- ruleProvider["ruleSize_" + heading] = size;
- }
- }
- else {
- let fontName = this.fontStylesToName.get(markType);
- if (fontName) { this.updateFontStyleDropdown(fontName); }
- let ruleProvider = this.editorProps.ruleProvider;
- let heading = NumCast(this.editorProps.Document.heading);
- if (ruleProvider && heading) {
- ruleProvider["ruleFont_" + heading] = fontName;
- }
- }
//actually apply font
if ((view.state.selection as any).node && (view.state.selection as any).node.type === view.state.schema.nodes.ordered_list) {
- view.dispatch(updateBullets(view.state.tr.setNodeMarkup(view.state.selection.from, (view.state.selection as any).node.type,
- { ...(view.state.selection as NodeSelection).node.attrs, setFontSize: Number(markType.name.replace(/p/, "")) }), view.state.schema));
+ let status = updateBullets(view.state.tr.setNodeMarkup(view.state.selection.from, (view.state.selection as any).node.type,
+ { ...(view.state.selection as NodeSelection).node.attrs, setFontFamily: markType.name, setFontSize: Number(markType.name.replace(/p/, "")) }), view.state.schema);
+ view.dispatch(status.setSelection(new NodeSelection(status.doc.resolve(view.state.selection.from))));
}
else toggleMark(markType)(view.state, view.dispatch, view);
}
}
- updateBullets = (tx2: Transaction, style: string) => {
- tx2.doc.descendants((node: any, offset: any, index: any) => {
- if (node.type === schema.nodes.ordered_list || node.type === schema.nodes.list_item) {
- let path = (tx2.doc.resolve(offset) as any).path;
- let depth = Array.from(path).reduce((p: number, c: any) => p + (c.hasOwnProperty("type") && c.type === schema.nodes.ordered_list ? 1 : 0), 0);
- if (node.type === schema.nodes.ordered_list) depth++;
- tx2.setNodeMarkup(offset, node.type, { mapStyle: style, bulletStyle: depth }, node.marks);
+ changeToFontFamily = (mark: Mark, view: EditorView) => {
+ let { $cursor, ranges } = view.state.selection as TextSelection;
+ let state = view.state;
+ let dispatch = view.dispatch;
+
+ //remove all other active font marks
+ if ($cursor) {
+ if (view.state.schema.marks.pFontFamily.isInSet(state.storedMarks || $cursor.marks())) {
+ dispatch(state.tr.removeStoredMark(view.state.schema.marks.pFontFamily));
}
- });
+ } else {
+ let has = false;
+ for (let i = 0; !has && i < ranges.length; i++) {
+ let { $from, $to } = ranges[i];
+ has = state.doc.rangeHasMark($from.pos, $to.pos, view.state.schema.marks.pFontFamily);
+ }
+ for (let i of ranges) {
+ if (has) {
+ toggleMark(view.state.schema.marks.pFontFamily)(view.state, view.dispatch, view);
+ }
+ }
+ }
+
+ let fontName = mark.attrs.family;
+ if (fontName) { this.updateFontStyleDropdown(fontName); }
+ if (this.editorProps) {
+ let ruleProvider = this.editorProps.ruleProvider;
+ let heading = NumCast(this.editorProps.Document.heading);
+ if (ruleProvider && heading) {
+ ruleProvider["ruleFont_" + heading] = fontName;
+ }
+ }
+ //actually apply font
+ if ((view.state.selection as any).node && (view.state.selection as any).node.type === view.state.schema.nodes.ordered_list) {
+ let status = updateBullets(view.state.tr.setNodeMarkup(view.state.selection.from, (view.state.selection as any).node.type,
+ { ...(view.state.selection as NodeSelection).node.attrs, setFontFamily: fontName }), view.state.schema);
+ view.dispatch(status.setSelection(new NodeSelection(status.doc.resolve(view.state.selection.from))));
+ }
+ else view.dispatch(view.state.tr.addMark(view.state.selection.from, view.state.selection.to, view.state.schema.marks.pFontFamily.create({ family: fontName })));
+ view.state.storedMarks = [...(view.state.storedMarks || []), view.state.schema.marks.pFontFamily.create({ family: fontName })];
}
+
+ changeToFontSize = (mark: Mark, view: EditorView) => {
+ let { $cursor, ranges } = view.state.selection as TextSelection;
+ let state = view.state;
+ let dispatch = view.dispatch;
+
+ //remove all other active font marks
+ if ($cursor) {
+ if (view.state.schema.marks.pFontSize.isInSet(state.storedMarks || $cursor.marks())) {
+ dispatch(state.tr.removeStoredMark(view.state.schema.marks.pFontSize));
+ }
+ } else {
+ let has = false;
+ for (let i = 0; !has && i < ranges.length; i++) {
+ let { $from, $to } = ranges[i];
+ has = state.doc.rangeHasMark($from.pos, $to.pos, view.state.schema.marks.pFontSize);
+ }
+ for (let i of ranges) {
+ if (has) {
+ toggleMark(view.state.schema.marks.pFontSize)(view.state, view.dispatch, view);
+ }
+ }
+ }
+
+ let size = mark.attrs.fontSize;
+ if (size) { this.updateFontSizeDropdown(String(size) + " pt"); }
+ if (this.editorProps) {
+ let ruleProvider = this.editorProps.ruleProvider;
+ let heading = NumCast(this.editorProps.Document.heading);
+ if (ruleProvider && heading) {
+ ruleProvider["ruleSize_" + heading] = size;
+ }
+ }
+ //actually apply font
+ if ((view.state.selection as any).node && (view.state.selection as any).node.type === view.state.schema.nodes.ordered_list) {
+ let status = updateBullets(view.state.tr.setNodeMarkup(view.state.selection.from, (view.state.selection as any).node.type,
+ { ...(view.state.selection as NodeSelection).node.attrs, setFontSize: size }), view.state.schema);
+ view.dispatch(status.setSelection(new NodeSelection(status.doc.resolve(view.state.selection.from))));
+ }
+ else view.dispatch(view.state.tr.addMark(view.state.selection.from, view.state.selection.to, view.state.schema.marks.pFontSize.create({ fontSize: size })));
+ view.state.storedMarks = [...(view.state.storedMarks || []), view.state.schema.marks.pFontSize.create({ fontSize: size })];
+ }
+
//remove all node typeand apply the passed-in one to the selected text
- changeToNodeType = (nodeType: NodeType | undefined, view: EditorView) => {
+ changeToNodeType = (nodeType: NodeType | undefined) => {
//remove oldif (nodeType) { //add new
+ let view = this.view;
if (nodeType === schema.nodes.bullet_list) {
wrapInList(nodeType)(view.state, view.dispatch);
} else {
var marks = view.state.storedMarks || (view.state.selection.$to.parentOffset && view.state.selection.$from.marks());
if (!wrapInList(schema.nodes.ordered_list)(view.state, (tx2: any) => {
- this.updateBullets(tx2, (nodeType as any).attrs.mapStyle);
- marks && tx2.ensureMarks([...marks]);
- marks && tx2.setStoredMarks([...marks]);
+ let tx3 = updateBullets(tx2, schema, nodeType && (nodeType as any).attrs.mapStyle);
+ marks && tx3.ensureMarks([...marks]);
+ marks && tx3.setStoredMarks([...marks]);
view.dispatch(tx2);
})) {
let tx2 = view.state.tr;
- this.updateBullets(tx2, (nodeType as any).attrs.mapStyle);
- marks && tx2.ensureMarks([...marks]);
- marks && tx2.setStoredMarks([...marks]);
+ let tx3 = updateBullets(tx2, schema, nodeType && (nodeType as any).attrs.mapStyle);
+ marks && tx3.ensureMarks([...marks]);
+ marks && tx3.setStoredMarks([...marks]);
- view.dispatch(tx2);
+ view.dispatch(tx3);
}
}
}
//makes a button for the drop down FOR MARKS
//css is the style you want applied to the button
- dropdownMarkBtn(label: string, css: string, markType: MarkType, view: EditorView, changeToMarkInGroup: (markType: MarkType<any>, view: EditorView, groupMarks: MarkType[]) => any, groupMarks: MarkType[]) {
+ dropdownFontFamilyBtn(label: string, css: string, mark: Mark, view: EditorView, changeFontFamily: (mark: Mark<any>, view: EditorView) => any) {
+ return new MenuItem({
+ title: "",
+ label: label,
+ execEvent: "",
+ class: "menuicon",
+ css: css,
+ enable() { return true; },
+ run() {
+ changeFontFamily(mark, view);
+ }
+ });
+ }
+ //makes a button for the drop down FOR MARKS
+ //css is the style you want applied to the button
+ dropdownFontSizeBtn(label: string, css: string, mark: Mark, view: EditorView, changeFontSize: (markType: Mark<any>, view: EditorView) => any) {
return new MenuItem({
title: "",
label: label,
@@ -572,7 +613,7 @@ export class TooltipTextMenu {
css: css,
enable() { return true; },
run() {
- changeToMarkInGroup(markType, view, groupMarks);
+ changeFontSize(mark, view);
}
});
}
@@ -586,7 +627,7 @@ export class TooltipTextMenu {
class: "summarize",
execEvent: "",
run: (state, dispatch) => {
- TooltipTextMenu.insertStar(state, dispatch);
+ TooltipTextMenu.insertStar(this.view.state, this.view.dispatch);
}
});
@@ -654,7 +695,9 @@ export class TooltipTextMenu {
this.view.dispatch(this.view.state.tr.removeMark(from, to));
Array.from(this._brushMarks).filter(m => m.type !== schema.marks.user_mark).forEach((mark: Mark) => {
const markType = mark.type;
- this.changeToMarkInGroup(markType, this.view, []);
+ if (mark.type === schema.marks.pFontFamily) this.changeToFontFamily(mark, this.view);
+ else if (mark.type === schema.marks.pFontSize) this.changeToFontSize(mark, this.view);
+ else this.changeToMarkInGroup(markType, this.view, []);
});
}
}
@@ -848,9 +891,17 @@ export class TooltipTextMenu {
}
}
+ update(view: EditorView, lastState: EditorState | undefined) { this.updateFromDash(view, lastState, this.editorProps); }
//updates the tooltip menu when the selection changes
- update(view: EditorView, lastState: EditorState | undefined) {
+ public updateFromDash(view: EditorView, lastState: EditorState | undefined, props: any) {
+ if (!view) {
+ console.log("no editor? why?");
+ return;
+ }
+ this.view = view;
let state = view.state;
+ DocumentDecorations.Instance.showTextBar();
+ props && (this.editorProps = props);
// Don't do anything if the document/selection didn't change
if (lastState && lastState.doc.eq(state.doc) &&
lastState.selection.eq(state.selection)) return;
@@ -865,50 +916,23 @@ export class TooltipTextMenu {
//UPDATE LIST ITEM DROPDOWN
//UPDATE FONT STYLE DROPDOWN
- let activeStyles = this.activeMarksOnSelection(this.fontStyles);
+ let activeStyles = this.activeFontFamilyOnSelection();
if (activeStyles !== undefined) {
- // activeStyles.forEach((markType) => {
- // this._activeMarks.push(this.view.state.schema.mark(markType));
- // });
if (activeStyles.length === 1) {
- // if we want to update something somewhere with active font name
- let fontName = this.fontStylesToName.get(activeStyles[0]);
- if (fontName) { this.updateFontStyleDropdown(fontName); }
- } else if (activeStyles.length === 0) {
- //crimson on default
- this.updateFontStyleDropdown("Crimson Text");
- } else {
- this.updateFontStyleDropdown("Various");
- }
+ activeStyles[0] && this.updateFontStyleDropdown(activeStyles[0]);
+ } else this.updateFontStyleDropdown(activeStyles.length ? "various" : "default");
}
//UPDATE FONT SIZE DROPDOWN
- let activeSizes = this.activeMarksOnSelection(this.fontSizes);
+ let activeSizes = this.activeFontSizeOnSelection();
if (activeSizes !== undefined) {
if (activeSizes.length === 1) { //if there's only one active font size
- // activeSizes.forEach((markType) => {
- // this._activeMarks.push(this.view.state.schema.mark(markType));
- // });
- let size = this.fontSizeToNum.get(activeSizes[0]);
- if (size) { this.updateFontSizeDropdown(String(size) + " pt"); }
- } else if (activeSizes.length === 0) {
- //should be 14 on default
- this.updateFontSizeDropdown("14 pt");
- } else { //multiple font sizes selected
- this.updateFontSizeDropdown("Various");
- }
+ activeSizes[0] && this.updateFontSizeDropdown(String(activeSizes[0]) + " pt");
+ } else this.updateFontSizeDropdown(activeSizes.length ? "various" : "default");
}
this.update_mark_doms();
}
-
- public mark_key_pressed(marks: Mark<any>[]) {
- if (this.view.state.selection.empty) {
- if (marks) this._activeMarks = marks;
- this.update_mark_doms();
- }
- }
-
update_mark_doms() {
this.reset_mark_doms();
let foundlink = false;
@@ -943,6 +967,30 @@ export class TooltipTextMenu {
}
+ //finds fontSize at start of selection
+ activeFontSizeOnSelection() {
+ //current selection
+ let state = this.view.state;
+ let activeSizes: number[] = [];
+ const pos = this.view.state.selection.$from;
+ const ref_node: ProsNode = this.reference_node(pos);
+ if (ref_node && ref_node !== this.view.state.doc && ref_node.isText) {
+ ref_node.marks.forEach(m => m.type === state.schema.marks.pFontSize && activeSizes.push(m.attrs.fontSize));
+ }
+ return activeSizes;
+ }
+ //finds fontSize at start of selection
+ activeFontFamilyOnSelection() {
+ //current selection
+ let state = this.view.state;
+ let activeFamilies: string[] = [];
+ const pos = this.view.state.selection.$from;
+ const ref_node: ProsNode = this.reference_node(pos);
+ if (ref_node && ref_node !== this.view.state.doc && ref_node.isText) {
+ ref_node.marks.forEach(m => m.type === state.schema.marks.pFontFamily && activeFamilies.push(m.attrs.family));
+ }
+ return activeFamilies;
+ }
//finds all active marks on selection in given group
activeMarksOnSelection(markGroup: MarkType[]) {
//current selection
@@ -977,6 +1025,9 @@ export class TooltipTextMenu {
this._activeMarks = ref_node.marks;
activeMarks = markGroup.filter(mark_type => {
if (dispatch) {
+ if (mark_type === state.schema.marks.pFontSize) {
+ return ref_node.marks.some(m => m.type.name === state.schema.marks.pFontSize.name);
+ }
let mark = state.schema.mark(mark_type);
return ref_node.marks.includes(mark);
}
diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts
index 7abb9d1ee..472afac1d 100644
--- a/src/client/util/UndoManager.ts
+++ b/src/client/util/UndoManager.ts
@@ -73,8 +73,8 @@ export namespace UndoManager {
}
type UndoBatch = UndoEvent[];
- let undoStack: UndoBatch[] = observable([]);
- let redoStack: UndoBatch[] = observable([]);
+ export let undoStack: UndoBatch[] = observable([]);
+ export let redoStack: UndoBatch[] = observable([]);
let currentBatch: UndoBatch | undefined;
let batchCounter = 0;
let undoing = false;
diff --git a/src/client/views/CollectionLinearView.scss b/src/client/views/CollectionLinearView.scss
new file mode 100644
index 000000000..4423a7020
--- /dev/null
+++ b/src/client/views/CollectionLinearView.scss
@@ -0,0 +1,73 @@
+@import "globalCssVariables";
+@import "nodeModuleOverrides";
+
+.collectionLinearView-outer{
+ overflow: hidden;
+ height:100%;
+ .collectionLinearView {
+ display:flex;
+ height: 100%;
+ >label {
+ background: $dark-color;
+ color: $light-color;
+ display: inline-block;
+ border-radius: 18px;
+ font-size: 12.5px;
+ width: 18px;
+ height: 18px;
+ margin-top:auto;
+ margin-bottom:auto;
+ cursor: pointer;
+ transition: transform 0.2s;
+ }
+
+ label p {
+ padding-left:5px;
+ }
+
+ label:hover {
+ background: $main-accent;
+ transform: scale(1.15);
+ }
+
+ >input {
+ display: none;
+ }
+ >input:not(:checked)~.collectionLinearView-content {
+ display: none;
+ }
+
+ >input:checked~label {
+ transform: rotate(45deg);
+ transition: transform 0.5s;
+ cursor: pointer;
+ }
+
+ .collectionLinearView-content {
+ display: flex;
+ opacity: 1;
+ position: relative;
+ margin-top: auto;
+
+ .collectionLinearView-docBtn, .collectionLinearView-docBtn-scalable {
+ position:relative;
+ margin-top: auto;
+ margin-bottom: auto;
+ }
+ .collectionLinearView-docBtn-scalable:hover {
+ transform: scale(1.15);
+ }
+
+ .collectionLinearView-round-button {
+ width: 18px;
+ height: 18px;
+ border-radius: 18px;
+ font-size: 15px;
+ }
+
+ .collectionLinearView-round-button:hover {
+ transform: scale(1.15);
+ }
+ }
+ }
+}
diff --git a/src/client/views/CollectionLinearView.tsx b/src/client/views/CollectionLinearView.tsx
new file mode 100644
index 000000000..7c6d33d36
--- /dev/null
+++ b/src/client/views/CollectionLinearView.tsx
@@ -0,0 +1,110 @@
+import { action, IReactionDisposer, observable, reaction } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { Doc, HeightSym, WidthSym } from '../../new_fields/Doc';
+import { makeInterface } from '../../new_fields/Schema';
+import { BoolCast, NumCast, StrCast } from '../../new_fields/Types';
+import { emptyFunction, returnEmptyString, returnOne, returnTrue, Utils } from '../../Utils';
+import { DragManager } from '../util/DragManager';
+import { Transform } from '../util/Transform';
+import "./CollectionLinearView.scss";
+import { CollectionViewType } from './collections/CollectionView';
+import { CollectionSubView } from './collections/CollectionSubView';
+import { DocumentView } from './nodes/DocumentView';
+import { documentSchema } from '../../new_fields/documentSchemas';
+import { Id } from '../../new_fields/FieldSymbols';
+
+
+type LinearDocument = makeInterface<[typeof documentSchema,]>;
+const LinearDocument = makeInterface(documentSchema);
+
+@observer
+export class CollectionLinearView extends CollectionSubView(LinearDocument) {
+ @observable public addMenuToggle = React.createRef<HTMLInputElement>();
+ private _dropDisposer?: DragManager.DragDropDisposer;
+ private _heightDisposer?: IReactionDisposer;
+ private _spacing = 20;
+
+ componentWillUnmount() {
+ this._dropDisposer && this._dropDisposer();
+ this._heightDisposer && this._heightDisposer();
+ }
+
+ componentDidMount() {
+ // is there any reason this needs to exist? -syip. yes, it handles autoHeight for stacking views (masonry isn't yet supported).
+ this._heightDisposer = reaction(() => NumCast(this.props.Document.height, 0) + this.childDocs.length + (this.props.Document.isExpanded ? 1 : 0),
+ () => this.props.Document.width = 18 + (this.props.Document.isExpanded ? this.childDocs.length * (this.props.Document[HeightSym]()) : 10),
+ { fireImmediately: true }
+ );
+ }
+ protected createDropTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view
+ this._dropDisposer && this._dropDisposer();
+ if (ele) {
+ this._dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.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();
+ let { scale, translateX, translateY } = Utils.GetScreenTransform(ele.current);
+ return new Transform(-translateX, -translateY, 1 / scale);
+ }
+ render() {
+ let guid = Utils.GenerateGuid();
+ return <div className="collectionLinearView-outer">
+ <div className="collectionLinearView" ref={this.createDropTarget} >
+ <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>
+
+ <div className="collectionLinearView-content">
+ {this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => {
+ let nested = pair.layout.viewType === CollectionViewType.Linear;
+ let dref = React.createRef<HTMLDivElement>();
+ let nativeWidth = NumCast(pair.layout.nativeWidth, this.dimension());
+ let scalingContent = nested ? 1 : this.dimension() / (this._spacing + nativeWidth);
+ let scalingBox = nested ? 1 : this.dimension() / nativeWidth;
+ let deltaSize = nativeWidth * scalingBox - nativeWidth * scalingContent;
+ return <div className={`collectionLinearView-docBtn` + (pair.layout.onClick || pair.layout.onDragStart ? "-scalable" : "")} key={pair.layout[Id]} ref={dref}
+ style={{
+ width: nested ? pair.layout[WidthSym]() : this.dimension(),
+ height: nested && pair.layout.isExpanded ? pair.layout[HeightSym]() : this.dimension(),
+ transform: nested ? undefined : `translate(${deltaSize / 2}px, ${deltaSize / 2}px)`
+ }} >
+ <DocumentView
+ Document={pair.layout}
+ DataDoc={pair.data}
+ addDocument={this.props.addDocument}
+ moveDocument={this.props.moveDocument}
+ addDocTab={this.props.addDocTab}
+ pinToPres={emptyFunction}
+ removeDocument={this.props.removeDocument}
+ ruleProvider={undefined}
+ onClick={undefined}
+ ScreenToLocalTransform={this.getTransform(dref)}
+ ContentScaling={() => scalingContent} // ugh - need to get rid of this inline function to avoid recomputing
+ PanelWidth={() => nested ? pair.layout[WidthSym]() : this.dimension()}
+ PanelHeight={() => nested ? pair.layout[HeightSym]() : this.dimension()}
+ renderDepth={this.props.renderDepth + 1}
+ focus={emptyFunction}
+ backgroundColor={returnEmptyString}
+ parentActive={returnTrue}
+ whenActiveChanged={emptyFunction}
+ bringToFront={emptyFunction}
+ ContainingCollectionView={undefined}
+ ContainingCollectionDoc={undefined}
+ zoomToScale={emptyFunction}
+ getScale={returnOne}>
+ </DocumentView>
+ </div>;
+ })}
+ {/* <li key="undoTest"><button className="add-button round-button" title="Click if undo isn't working" onClick={() => UndoManager.TraceOpenBatches()}><FontAwesomeIcon icon="exclamation" size="sm" /></button></li> */}
+
+ </div>
+ </div>
+ </div>;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/DictationOverlay.tsx b/src/client/views/DictationOverlay.tsx
new file mode 100644
index 000000000..2accf9bfd
--- /dev/null
+++ b/src/client/views/DictationOverlay.tsx
@@ -0,0 +1,71 @@
+import { computed, observable, runInAction } from 'mobx';
+import { observer } from 'mobx-react';
+import "normalize.css";
+import * as React from 'react';
+import { DictationManager } from '../util/DictationManager';
+import "./Main.scss";
+import MainViewModal from './MainViewModal';
+
+@observer
+export class DictationOverlay extends React.Component {
+ public static Instance: DictationOverlay;
+ @observable private _dictationState = DictationManager.placeholder;
+ @observable private _dictationSuccessState: boolean | undefined = undefined;
+ @observable private _dictationDisplayState = false;
+ @observable private _dictationListeningState: DictationManager.Controls.ListeningUIStatus = false;
+
+ public isPointerDown = false;
+ public overlayTimeout: NodeJS.Timeout | undefined;
+ public hasActiveModal = false;
+
+ constructor(props: any) {
+ super(props);
+ DictationOverlay.Instance = this;
+ }
+
+ public initiateDictationFade = () => {
+ let duration = DictationManager.Commands.dictationFadeDuration;
+ this.overlayTimeout = setTimeout(() => {
+ this.dictationOverlayVisible = false;
+ this.dictationSuccess = undefined;
+ DictationOverlay.Instance.hasActiveModal = false;
+ setTimeout(() => this.dictatedPhrase = DictationManager.placeholder, 500);
+ }, duration);
+ }
+ public cancelDictationFade = () => {
+ if (this.overlayTimeout) {
+ clearTimeout(this.overlayTimeout);
+ this.overlayTimeout = undefined;
+ }
+ }
+
+ @computed public get dictatedPhrase() { return this._dictationState; }
+ @computed public get dictationSuccess() { return this._dictationSuccessState; }
+ @computed public get dictationOverlayVisible() { return this._dictationDisplayState; }
+ @computed public get isListening() { return this._dictationListeningState; }
+
+ public set dictatedPhrase(value: string) { runInAction(() => this._dictationState = value); }
+ public set dictationSuccess(value: boolean | undefined) { runInAction(() => this._dictationSuccessState = value); }
+ public set dictationOverlayVisible(value: boolean) { runInAction(() => this._dictationDisplayState = value); }
+ public set isListening(value: DictationManager.Controls.ListeningUIStatus) { runInAction(() => this._dictationListeningState = value); }
+
+ render() {
+ let success = this.dictationSuccess;
+ let result = this.isListening && !this.isListening.interim ? DictationManager.placeholder : `"${this.dictatedPhrase}"`;
+ let dialogueBoxStyle = {
+ background: success === undefined ? "gainsboro" : success ? "lawngreen" : "red",
+ borderColor: this.isListening ? "red" : "black",
+ fontStyle: "italic"
+ };
+ let overlayStyle = {
+ backgroundColor: this.isListening ? "red" : "darkslategrey"
+ };
+ return (<MainViewModal
+ contents={result}
+ isDisplayed={this.dictationOverlayVisible}
+ interactive={false}
+ dialogueBoxStyle={dialogueBoxStyle}
+ overlayStyle={overlayStyle}
+ />);
+ }
+} \ No newline at end of file
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index d6562492f..ae4b7cf3a 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -1,14 +1,88 @@
import * as React from 'react';
import { Doc } from '../../new_fields/Doc';
-import { computed } from 'mobx';
+import { computed, action } from 'mobx';
+import { Cast } from '../../new_fields/Types';
+import { listSpec } from '../../new_fields/Schema';
+import { InkingControl } from './InkingControl';
+import { InkTool } from '../../new_fields/InkField';
+import { PositionDocument } from '../../new_fields/documentSchemas';
-export function DocComponent<P extends { Document: Doc }, T>(schemaCtor: (doc: Doc) => T) {
+
+/// 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;
+}
+export function DocComponent<P extends DocComponentProps, T>(schemaCtor: (doc: Doc) => T) {
+ class Component extends React.Component<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)); }
+ }
+ return Component;
+}
+
+/// DocStaticProps return a base class for React document views that have data extensions but aren't annotatable (e.g. AudioBox, FormattedTextBox)
+interface DocExtendableProps {
+ Document: Doc;
+ DataDoc?: Doc;
+ fieldKey: string;
+ isSelected: () => boolean;
+ renderDepth: number;
+}
+export function DocExtendableComponent<P extends DocExtendableProps, T>(schemaCtor: (doc: Doc) => T) {
+ class Component extends React.Component<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 Doc.Layout(this.props.Document); }
+ @computed get dataDoc() { return (this.props.DataDoc && this.props.Document.isTemplateField ? this.props.DataDoc : Doc.GetProto(this.props.Document)) as Doc; }
+ @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); }
+ active = () => !this.props.Document.isBackground && (this.props.Document.forceActive || this.props.isSelected() || this.props.renderDepth === 0);// && !InkingControl.Instance.selectedTool; // bcz: inking state shouldn't affect static tools
+ }
+ return Component;
+}
+
+
+/// DocAnnotatbleComponent return a base class for React views of document fields that are annotatable *and* interactive when selected (e.g., pdf, image)
+interface DocAnnotatableProps {
+ Document: Doc;
+ DataDoc?: Doc;
+ fieldKey: string;
+ whenActiveChanged: (isActive: boolean) => void;
+ isSelected: () => boolean;
+ renderDepth: number;
+}
+export function DocAnnotatableComponent<P extends DocAnnotatableProps, T>(schemaCtor: (doc: Doc) => T) {
class Component extends React.Component<P> {
+ _isChildActive = false;
//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 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.isTemplateField ? this.props.DataDoc : Doc.GetProto(this.props.Document)) as Doc; }
+ @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); }
+ @computed get annotationsKey() { return "annotations"; }
+
+ @action.bound
+ removeDocument(doc: Doc): boolean {
+ Doc.GetProto(doc).annotationOn = undefined;
+ let value = this.extensionDoc && Cast(this.extensionDoc[this.annotationsKey], listSpec(Doc), []);
+ let index = value ? Doc.IndexOf(doc, value.map(d => d as Doc), true) : -1;
+ return index !== -1 && value && value.splice(index, 1) ? true : false;
+ }
+ // if the moved document is already in this overlay collection nothing needs to be done.
+ // otherwise, if the document can be removed from where it was, it will then be added to this document's overlay collection.
+ @action.bound
+ moveDocument(doc: Doc, targetCollection: Doc, addDocument: (doc: Doc) => boolean): boolean {
+ return Doc.AreProtosEqual(this.props.Document, targetCollection) ? true : this.removeDocument(doc) ? addDocument(doc) : false;
}
+ @action.bound
+ addDocument(doc: Doc): boolean {
+ Doc.GetProto(doc).annotationOn = this.props.Document;
+ return this.extensionDoc && Doc.AddDocToList(this.extensionDoc, this.annotationsKey, doc) ? true : false;
+ }
+
+ whenActiveChanged = (isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive);
+ active = () => ((InkingControl.Instance.selectedTool === InkTool.None && !this.props.Document.isBackground) &&
+ (this.props.Document.forceActive || this.props.isSelected() || this._isChildActive || this.props.renderDepth === 0) ? true : false)
}
return Component;
} \ No newline at end of file
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 9ca54f738..ba87ecfb4 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -6,7 +6,6 @@ import { observer } from "mobx-react";
import { Doc } from "../../new_fields/Doc";
import { RichTextField } from '../../new_fields/RichTextField';
import { NumCast } from "../../new_fields/Types";
-import { URLField } from '../../new_fields/URLField';
import { emptyFunction } from "../../Utils";
import { Pulls, Pushes } from '../apis/google_docs/GoogleApiClientUtils';
import { DragLinksAsDocuments, DragManager } from "../util/DragManager";
@@ -23,7 +22,6 @@ import React = require("react");
import { DocumentView } from './nodes/DocumentView';
import { ParentDocSelector } from './collections/ParentDocumentSelector';
import { CollectionDockingView } from './collections/CollectionDockingView';
-import { DocumentDecorations } from './DocumentDecorations';
const higflyout = require("@hig/flyout");
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
@@ -46,10 +44,9 @@ const fetch: IconProp = "sync-alt";
export class DocumentButtonBar extends React.Component<{ views: DocumentView[], stack?: any }, {}> {
private _linkButton = React.createRef<HTMLDivElement>();
private _linkerButton = React.createRef<HTMLDivElement>();
- private _embedButton = React.createRef<HTMLDivElement>();
+ private _aliasButton = React.createRef<HTMLDivElement>();
private _tooltipoff = React.createRef<HTMLDivElement>();
private _textDoc?: Doc;
- private _linkDrag?: UndoManager.Batch;
public static Instance: DocumentButtonBar;
constructor(props: { views: DocumentView[] }) {
@@ -112,13 +109,13 @@ export class DocumentButtonBar extends React.Component<{ views: DocumentView[],
document.addEventListener("pointerup", this.onLinkerButtonUp);
}
- onEmbedButtonDown = (e: React.PointerEvent): void => {
+ onAliasButtonDown = (e: React.PointerEvent): void => {
e.stopPropagation();
e.preventDefault();
- document.removeEventListener("pointermove", this.onEmbedButtonMoved);
- document.addEventListener("pointermove", this.onEmbedButtonMoved);
- document.removeEventListener("pointerup", this.onEmbedButtonUp);
- document.addEventListener("pointerup", this.onEmbedButtonUp);
+ document.removeEventListener("pointermove", this.onAliasButtonMoved);
+ document.addEventListener("pointermove", this.onAliasButtonMoved);
+ document.removeEventListener("pointerup", this.onAliasButtonUp);
+ document.addEventListener("pointerup", this.onAliasButtonUp);
}
onLinkerButtonUp = (e: PointerEvent): void => {
@@ -127,9 +124,9 @@ export class DocumentButtonBar extends React.Component<{ views: DocumentView[],
e.stopPropagation();
}
- onEmbedButtonUp = (e: PointerEvent): void => {
- document.removeEventListener("pointermove", this.onEmbedButtonMoved);
- document.removeEventListener("pointerup", this.onEmbedButtonUp);
+ onAliasButtonUp = (e: PointerEvent): void => {
+ document.removeEventListener("pointermove", this.onAliasButtonMoved);
+ document.removeEventListener("pointerup", this.onAliasButtonUp);
e.stopPropagation();
}
@@ -141,16 +138,10 @@ export class DocumentButtonBar extends React.Component<{ views: DocumentView[],
let selDoc = this.props.views[0];
let container = selDoc.props.ContainingCollectionDoc ? selDoc.props.ContainingCollectionDoc.proto : undefined;
let dragData = new DragManager.LinkDragData(selDoc.props.Document, container ? [container] : []);
- FormattedTextBox.InputBoxOverlay = undefined;
- this._linkDrag = UndoManager.StartBatch("Drag Link");
+ let _linkDrag = UndoManager.StartBatch("Drag Link");
DragManager.StartLinkDrag(this._linkerButton.current, dragData, e.pageX, e.pageY, {
handlers: {
- dragComplete: () => {
- if (this._linkDrag) {
- this._linkDrag.end();
- this._linkDrag = undefined;
- }
- },
+ dragComplete: () => _linkDrag && _linkDrag.end()
},
hideSource: false
});
@@ -159,15 +150,20 @@ export class DocumentButtonBar extends React.Component<{ views: DocumentView[],
}
@action
- onEmbedButtonMoved = (e: PointerEvent): void => {
- if (this._embedButton.current !== null) {
- document.removeEventListener("pointermove", this.onEmbedButtonMoved);
- document.removeEventListener("pointerup", this.onEmbedButtonUp);
+ onAliasButtonMoved = (e: PointerEvent): void => {
+ if (this._aliasButton.current !== null) {
+ document.removeEventListener("pointermove", this.onAliasButtonMoved);
+ document.removeEventListener("pointerup", this.onAliasButtonUp);
let dragDocView = this.props.views[0];
- let dragData = new DragManager.EmbedDragData(dragDocView.props.Document);
-
- DragManager.StartEmbedDrag(dragDocView.ContentDiv!, dragData, e.x, e.y, {
+ let dragData = new DragManager.DocumentDragData([dragDocView.props.Document]);
+ const [left, top] = dragDocView.props.ScreenToLocalTransform().scale(dragDocView.props.ContentScaling()).inverse().transformPoint(0, 0);
+ dragData.offset = dragDocView.props.ScreenToLocalTransform().scale(dragDocView.props.ContentScaling()).transformDirection(e.clientX - left, e.clientY - top);
+ dragData.embedDoc = true;
+ dragData.dropAction = "alias";
+ DragManager.StartDocumentDrag([dragDocView.ContentDiv!], dragData, e.x, e.y, {
+ offsetX: dragData.offset[0],
+ offsetY: dragData.offset[1],
handlers: {
dragComplete: action(emptyFunction),
},
@@ -201,17 +197,12 @@ export class DocumentButtonBar extends React.Component<{ views: DocumentView[],
e.stopPropagation();
}
- considerEmbed = () => {
- let thisDoc = this.props.views[0].props.Document;
- let canEmbed = thisDoc.data && thisDoc.data instanceof URLField;
- if (!canEmbed) return (null);
- return (
- <div className="linkButtonWrapper">
- <div title="Drag Embed" className="linkButton-linker" ref={this._embedButton} onPointerDown={this.onEmbedButtonDown}>
- <FontAwesomeIcon className="documentdecorations-icon" icon="image" size="sm" />
- </div>
+ aliasDragger = () => {
+ return (<div className="linkButtonWrapper">
+ <div title="Drag Alias" className="linkButton-linker" ref={this._aliasButton} onPointerDown={this.onAliasButtonDown}>
+ <FontAwesomeIcon className="documentdecorations-icon" icon="image" size="sm" />
</div>
- );
+ </div>);
}
private get targetDoc() {
@@ -355,7 +346,7 @@ export class DocumentButtonBar extends React.Component<{ views: DocumentView[],
<TemplateMenu docs={this.props.views} templates={templates} />
</div>
{this.metadataMenu}
- {this.considerEmbed()}
+ {this.aliasDragger()}
{this.considerGoogleDocsPush()}
{this.considerGoogleDocsPull()}
<ParentDocSelector Document={this.props.views[0].props.Document} addDocTab={(doc, data, where) => {
diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index e68bfc6ad..32346165d 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -266,6 +266,31 @@ $linkGap : 3px;
}
}
-@-moz-keyframes spin { 100% { -moz-transform: rotate(360deg); } }
-@-webkit-keyframes spin { 100% { -webkit-transform: rotate(360deg); } }
-@keyframes spin { 100% { -webkit-transform: rotate(360deg); transform: rotate(360deg); } } \ No newline at end of file
+@-moz-keyframes spin {
+ 100% {
+ -moz-transform: rotate(360deg);
+ }
+}
+
+@-webkit-keyframes spin {
+ 100% {
+ -webkit-transform: rotate(360deg);
+ }
+}
+
+@keyframes spin {
+ 100% {
+ -webkit-transform: rotate(360deg);
+ transform: rotate(360deg);
+ }
+}
+
+@keyframes shadow-pulse {
+ 0% {
+ box-shadow: 0 0 0 0px rgba(0, 0, 0, 0.8);
+ }
+
+ 100% {
+ box-shadow: 0 0 0 10px rgba(0, 255, 0, 0);
+ }
+} \ No newline at end of file
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 944ae586c..55c211d1d 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -4,26 +4,27 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { action, computed, observable, reaction, runInAction } from "mobx";
import { observer } from "mobx-react";
import { Doc, DocListCastAsync } from "../../new_fields/Doc";
+import { PositionDocument } from '../../new_fields/documentSchemas';
import { List } from "../../new_fields/List";
import { ObjectField } from '../../new_fields/ObjectField';
-import { BoolCast, Cast, NumCast, StrCast } from "../../new_fields/Types";
+import { Cast, NumCast, StrCast } from "../../new_fields/Types";
import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils';
-import { emptyFunction, Utils } from "../../Utils";
+import { Utils } from "../../Utils";
import { Docs, DocUtils } from "../documents/Documents";
import { DocumentManager } from "../util/DocumentManager";
import { DragManager } from "../util/DragManager";
import { SelectionManager } from "../util/SelectionManager";
+import { TooltipTextMenu } from '../util/TooltipTextMenu';
import { undoBatch, UndoManager } from "../util/UndoManager";
import { MINIMIZED_ICON_SIZE } from "../views/globalCssVariables.scss";
import { CollectionView } from "./collections/CollectionView";
import { DocumentButtonBar } from './DocumentButtonBar';
import './DocumentDecorations.scss';
-import { PositionDocument } from './nodes/CollectionFreeFormDocumentView';
-import { DocumentView, swapViews } from "./nodes/DocumentView";
+import { DocumentView } from "./nodes/DocumentView";
import { FieldView } from "./nodes/FieldView";
-import { FormattedTextBox } from "./nodes/FormattedTextBox";
import { IconBox } from "./nodes/IconBox";
import React = require("react");
+import { DocumentType } from '../documents/DocumentTypes';
const higflyout = require("@hig/flyout");
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
@@ -48,7 +49,6 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
private _resizeBorderWidth = 16;
private _linkBoxHeight = 20 + 3; // link button height + margin
private _titleHeight = 20;
- private _embedButton = React.createRef<HTMLDivElement>();
private _downX = 0;
private _downY = 0;
private _iconDoc?: Doc = undefined;
@@ -68,6 +68,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
@observable public pullIcon: IconProp = "arrow-alt-circle-down";
@observable public pullColor: string = "white";
@observable public isAnimatingFetch = false;
+ @observable public isAnimatingPulse = false;
@observable public openHover = false;
constructor(props: Readonly<{}>) {
@@ -106,17 +107,13 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
fieldTemplate.title = metaKey;
Doc.MakeMetadataFieldTemplate(fieldTemplate, Doc.GetProto(docTemplate));
if (text.startsWith(">>")) {
- let layoutNative = Doc.MakeTitled("layoutNative");
- Doc.GetProto(docTemplate).layoutNative = layoutNative;
- swapViews(fieldTemplate, "", "layoutNative", layoutNative);
- layoutNative.layout = StrCast(fieldTemplateView.props.Document.layout).replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metaKey}"}`);
- layoutNative.backgroundLayout = StrCast(fieldTemplateView.props.Document.backgroundLayout).replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metaKey}"}`);
+ Doc.GetProto(docTemplate).layout = StrCast(fieldTemplateView.props.Document.layout).replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metaKey}"}`);
}
}
}
else {
if (SelectionManager.SelectedDocuments().length > 0) {
- SelectionManager.SelectedDocuments()[0].props.Document.customTitle = true;
+ SelectionManager.SelectedDocuments()[0].props.Document.customTitle = !this._title.startsWith("-");
let field = SelectionManager.SelectedDocuments()[0].props.Document[this._fieldKey];
if (typeof field === "number") {
SelectionManager.SelectedDocuments().forEach(d => {
@@ -178,6 +175,13 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
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) {
+ let 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)
@@ -201,6 +205,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
const [left, top] = dragDocView.props.ScreenToLocalTransform().scale(dragDocView.props.ContentScaling()).inverse().transformPoint(0, 0);
dragData.offset = dragDocView.props.ScreenToLocalTransform().scale(dragDocView.props.ContentScaling()).transformDirection(e.x - left, e.y - top);
dragData.moveDocument = SelectionManager.SelectedDocuments()[0].props.moveDocument;
+ dragData.isSelectionMove = true;
this.Interacting = true;
this._hidden = true;
document.removeEventListener("pointermove", this.onBackgroundMove);
@@ -283,7 +288,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
let selectedDocs = SelectionManager.SelectedDocuments().map(sd => sd);
if (selectedDocs.length > 1) {
- this._iconDoc = this._iconDoc ? this._iconDoc : this.createIcon(SelectionManager.SelectedDocuments(), CollectionView.LayoutString());
+ this._iconDoc = this._iconDoc ? this._iconDoc : this.createIcon(SelectionManager.SelectedDocuments(), CollectionView.LayoutString(""));
this.moveIconDoc(this._iconDoc);
} else {
this.getIconDoc(selectedDocs[0]).then(icon => icon && this.moveIconDoc(this._iconDoc = icon));
@@ -332,7 +337,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
iconDoc.y = NumCast(doc.y) - 24;
iconDoc.maximizedDocs = new List<Doc>(selected.map(s => s.props.Document));
selected.length === 1 && (doc.minimizedDoc = iconDoc);
- selected[0].props.addDocument && selected[0].props.addDocument(iconDoc, false);
+ selected[0].props.addDocument && selected[0].props.addDocument(iconDoc);
return iconDoc;
}
@action
@@ -341,7 +346,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
let iconDoc: Doc | undefined = await Cast(doc.minimizedDoc, Doc);
if (!iconDoc || !DocumentManager.Instance.getDocumentView(iconDoc)) {
- const layout = StrCast(doc.backgroundLayout, StrCast(doc.layout, FieldView.LayoutString(DocumentView)));
+ const layout = StrCast(doc.layout, FieldView.LayoutString(DocumentView, ""));
iconDoc = this.createIcon([docView], layout);
}
return iconDoc;
@@ -378,7 +383,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
ruleProvider && heading && (Doc.GetProto(ruleProvider)["ruleRounding_" + heading] = `${Math.min(100, dist)}%`);
usingRule = usingRule || (ruleProvider && heading ? true : false);
});
- !usingRule && SelectionManager.SelectedDocuments().map(dv => dv.props.Document.layout instanceof Doc ? dv.props.Document.layout : dv.props.Document.isTemplate ? dv.props.Document : Doc.GetProto(dv.props.Document)).
+ !usingRule && SelectionManager.SelectedDocuments().map(dv => dv.props.Document.layout instanceof Doc ? dv.props.Document.layout : dv.props.Document.isTemplateField ? dv.props.Document : Doc.GetProto(dv.props.Document)).
map(d => d.borderRounding = `${Math.min(100, dist)}%`);
e.stopPropagation();
e.preventDefault();
@@ -413,41 +418,6 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
}
- onEmbedButtonDown = (e: React.PointerEvent): void => {
- e.stopPropagation();
- document.removeEventListener("pointermove", this.onEmbedButtonMoved);
- document.addEventListener("pointermove", this.onEmbedButtonMoved);
- document.removeEventListener("pointerup", this.onEmbedButtonUp);
- document.addEventListener("pointerup", this.onEmbedButtonUp);
- }
-
-
-
- onEmbedButtonUp = (e: PointerEvent): void => {
- document.removeEventListener("pointermove", this.onEmbedButtonMoved);
- document.removeEventListener("pointerup", this.onEmbedButtonUp);
- e.stopPropagation();
- }
-
- @action
- onEmbedButtonMoved = (e: PointerEvent): void => {
- if (this._embedButton.current !== null) {
- document.removeEventListener("pointermove", this.onEmbedButtonMoved);
- document.removeEventListener("pointerup", this.onEmbedButtonUp);
-
- let dragDocView = SelectionManager.SelectedDocuments()[0];
- let dragData = new DragManager.EmbedDragData(dragDocView.props.Document);
-
- DragManager.StartEmbedDrag(dragDocView.ContentDiv!, dragData, e.x, e.y, {
- handlers: {
- dragComplete: action(emptyFunction),
- },
- hideSource: false
- });
- }
- e.stopPropagation();
- }
-
onPointerMove = (e: PointerEvent): void => {
e.stopPropagation();
e.preventDefault();
@@ -501,51 +471,50 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
break;
}
- if (!this._resizing) runInAction(() => FormattedTextBox.InputBoxOverlay = undefined);
SelectionManager.SelectedDocuments().forEach(element => {
if (dX !== 0 || dY !== 0 || dW !== 0 || dH !== 0) {
let doc = PositionDocument(element.props.Document);
- let nwidth = doc.nativeWidth || 0;
- let nheight = doc.nativeHeight || 0;
- let width = (doc.width || 0);
- let height = (doc.height || (nheight / nwidth * width));
+ let layoutDoc = PositionDocument(Doc.Layout(element.props.Document));
+ let nwidth = layoutDoc.nativeWidth || 0;
+ let nheight = layoutDoc.nativeHeight || 0;
+ let width = (layoutDoc.width || 0);
+ let height = (layoutDoc.height || (nheight / nwidth * width));
let scale = element.props.ScreenToLocalTransform().Scale * element.props.ContentScaling();
let actualdW = Math.max(width + (dW * scale), 20);
let actualdH = Math.max(height + (dH * scale), 20);
doc.x = (doc.x || 0) + dX * (actualdW - width);
doc.y = (doc.y || 0) + dY * (actualdH - height);
- let proto = doc.isTemplate ? doc : Doc.GetProto(element.props.Document); // bcz: 'doc' didn't work here...
- let fixedAspect = e.ctrlKey || (!doc.ignoreAspect && nwidth && nheight);
- if (fixedAspect && e.ctrlKey && doc.ignoreAspect) {
- doc.ignoreAspect = false;
- proto.nativeWidth = nwidth = doc.width || 0;
- proto.nativeHeight = nheight = doc.height || 0;
+ let 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;
}
if (fixedAspect && (!nwidth || !nheight)) {
- proto.nativeWidth = nwidth = doc.width || 0;
- proto.nativeHeight = nheight = doc.height || 0;
+ layoutDoc.nativeWidth = nwidth = layoutDoc.width || 0;
+ layoutDoc.nativeHeight = nheight = layoutDoc.height || 0;
}
- if (nwidth > 0 && nheight > 0 && !BoolCast(doc.ignoreAspect)) {
+ if (nwidth > 0 && nheight > 0 && !layoutDoc.ignoreAspect) {
if (Math.abs(dW) > Math.abs(dH)) {
if (!fixedAspect) {
- Doc.SetInPlace(element.props.Document, "nativeWidth", actualdW / (doc.width || 1) * (doc.nativeWidth || 0), true);
+ layoutDoc.nativeWidth = actualdW / (layoutDoc.width || 1) * (layoutDoc.nativeWidth || 0);
}
- doc.width = actualdW;
- if (fixedAspect && !doc.fitWidth) doc.height = nheight / nwidth * doc.width;
- else doc.height = actualdH;
+ layoutDoc.width = actualdW;
+ if (fixedAspect && !layoutDoc.fitWidth) layoutDoc.height = nheight / nwidth * layoutDoc.width;
+ else layoutDoc.height = actualdH;
}
else {
if (!fixedAspect) {
- Doc.SetInPlace(element.props.Document, "nativeHeight", actualdH / (doc.height || 1) * (doc.nativeHeight || 0), true);
+ layoutDoc.nativeHeight = actualdH / (layoutDoc.height || 1) * (doc.nativeHeight || 0);
}
- doc.height = actualdH;
- if (fixedAspect && !doc.fitWidth) doc.width = nwidth / nheight * doc.height;
- else doc.width = actualdW;
+ layoutDoc.height = actualdH;
+ if (fixedAspect && !layoutDoc.fitWidth) layoutDoc.width = nwidth / nheight * layoutDoc.height;
+ else layoutDoc.width = actualdW;
}
} else {
- dW && (doc.width = actualdW);
- dH && (doc.height = actualdH);
- dH && element.props.Document.autoHeight && Doc.SetInPlace(element.props.Document, "autoHeight", false, true);
+ dW && (layoutDoc.width = actualdW);
+ dH && (layoutDoc.height = actualdH);
+ dH && layoutDoc.autoHeight && (layoutDoc.autoHeight = false);
}
}
});
@@ -581,12 +550,21 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
return "-unset-";
}
-
-
+ TextBar: HTMLDivElement | undefined;
+ private setTextBar = (ele: HTMLDivElement) => {
+ if (ele) {
+ this.TextBar = ele;
+ }
+ }
+ public showTextBar = () => {
+ if (this.TextBar) {
+ TooltipTextMenu.Toolbar && Array.from(this.TextBar.childNodes).indexOf(TooltipTextMenu.Toolbar) === -1 && this.TextBar.appendChild(TooltipTextMenu.Toolbar);
+ }
+ }
render() {
var bounds = this.Bounds;
let seldoc = SelectionManager.SelectedDocuments().length ? SelectionManager.SelectedDocuments()[0] : undefined;
- if (bounds.x === Number.MAX_VALUE || !seldoc || this._hidden || isNaN(bounds.r) || isNaN(bounds.b) || isNaN(bounds.x) || isNaN(bounds.y)) {
+ if (SelectionManager.GetIsDragging() || bounds.x === Number.MAX_VALUE || !seldoc || this._hidden || isNaN(bounds.r) || isNaN(bounds.b) || isNaN(bounds.x) || isNaN(bounds.y)) {
return (null);
}
let minimizeIcon = (
@@ -615,7 +593,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
zIndex: SelectionManager.SelectedDocuments().length > 1 ? 900 : 0,
}} onPointerDown={this.onBackgroundDown} onContextMenu={(e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); }} >
</div>
- <div className="documentDecorations-container" style={{
+ <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",
left: bounds.x - this._resizeBorderWidth / 2,
diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx
index e9db4b048..8e86f58ee 100644
--- a/src/client/views/EditableView.tsx
+++ b/src/client/views/EditableView.tsx
@@ -4,6 +4,7 @@ import { observable, action, trace } from 'mobx';
import "./EditableView.scss";
import * as Autosuggest from 'react-autosuggest';
import { undoBatch } from '../util/UndoManager';
+import { SchemaHeaderField } from '../../new_fields/SchemaHeaderField';
export interface EditableProps {
/**
@@ -42,6 +43,10 @@ export interface EditableProps {
editing?: boolean;
onClick?: (e: React.MouseEvent) => boolean;
isEditingCallback?: (isEditing: boolean) => void;
+ HeadingObject?: SchemaHeaderField | undefined;
+ HeadingsHack?: number;
+ toggle?: () => void;
+ color?: string | undefined;
}
/**
@@ -52,6 +57,7 @@ export interface EditableProps {
@observer
export class EditableView extends React.Component<EditableProps> {
@observable _editing: boolean = false;
+ @observable _headingsHack: number = 1;
constructor(props: EditableProps) {
super(props);
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index 2fa03e969..8f397e331 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -6,6 +6,12 @@ import { DragManager } from "../util/DragManager";
import { action, runInAction } from "mobx";
import { Doc } from "../../new_fields/Doc";
import { DictationManager } from "../util/DictationManager";
+import SharingManager from "../util/SharingManager";
+import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
+import { Cast, PromiseValue } from "../../new_fields/Types";
+import { ScriptField } from "../../new_fields/ScriptField";
+import { InkingControl } from "./InkingControl";
+import { InkTool } from "../../new_fields/InkField";
const modifiers = ["control", "meta", "shift", "alt"];
type KeyHandler = (keycode: string, e: KeyboardEvent) => KeyControlInfo | Promise<KeyControlInfo>;
@@ -60,6 +66,7 @@ export default class KeyManager {
switch (keyname) {
case "escape":
let main = MainView.Instance;
+ InkingControl.Instance.switchTool(InkTool.None);
if (main.isPointerDown) {
DragManager.AbortDrag();
} else {
@@ -69,9 +76,9 @@ export default class KeyManager {
SelectionManager.DeselectAll();
}
}
- main.toggleColorPicker(true);
SelectionManager.DeselectAll();
DictationManager.Controls.stop();
+ SharingManager.Instance.close();
break;
case "delete":
case "backspace":
@@ -118,10 +125,10 @@ export default class KeyManager {
let preventDefault = true;
switch (keyname) {
- case "n":
- let toggle = MainView.Instance.addMenuToggle.current!;
- toggle.checked = !toggle.checked;
- break;
+ // case "n":
+ // let toggle = MainView.Instance.addMenuToggle.current!;
+ // toggle.checked = !toggle.checked;
+ // break;
}
return {
@@ -158,8 +165,29 @@ export default class KeyManager {
}
}
break;
+ case "t":
+ PromiseValue(Cast(CurrentUserUtils.UserDocument.Create, Doc)).then(pv => pv && (pv.onClick as ScriptField).script.run({ this: pv }));
+ if (MainView.Instance.flyoutWidth === 240) {
+ MainView.Instance.flyoutWidth = 0;
+ } else {
+ MainView.Instance.flyoutWidth = 240;
+ }
+ break;
+ case "l":
+ PromiseValue(Cast(CurrentUserUtils.UserDocument.Library, Doc)).then(pv => pv && (pv.onClick as ScriptField).script.run({ this: pv }));
+ if (MainView.Instance.flyoutWidth === 250) {
+ MainView.Instance.flyoutWidth = 0;
+ } else {
+ MainView.Instance.flyoutWidth = 250;
+ }
+ break;
case "f":
- MainView.Instance.isSearchVisible = !MainView.Instance.isSearchVisible;
+ PromiseValue(Cast(CurrentUserUtils.UserDocument.Search, Doc)).then(pv => pv && (pv.onClick as ScriptField).script.run({ this: pv }));
+ if (MainView.Instance.flyoutWidth === 400) {
+ MainView.Instance.flyoutWidth = 0;
+ } else {
+ MainView.Instance.flyoutWidth = 400;
+ }
break;
case "o":
let target = SelectionManager.SelectedDocuments()[0];
diff --git a/src/client/views/InkingCanvas.scss b/src/client/views/InkingCanvas.scss
index b3edad459..8f32652ed 100644
--- a/src/client/views/InkingCanvas.scss
+++ b/src/client/views/InkingCanvas.scss
@@ -8,7 +8,8 @@
position: absolute;
width: 100%;
height: 100%;
- z-index: -1; // allows annotations to appear on videos when screen is full-size & ...
+ background: inherit;
+ //z-index: -1; // allows annotations to appear on videos when screen is full-size & ...
}
}
@@ -35,7 +36,7 @@
.inkingCanvas-noSelect {
pointer-events: none;
- cursor: "arrow";
+ cursor: "crosshair";
}
.inkingCanvas-paths-ink,
diff --git a/src/client/views/InkingCanvas.tsx b/src/client/views/InkingCanvas.tsx
index 1cfa8d644..0037b95d0 100644
--- a/src/client/views/InkingCanvas.tsx
+++ b/src/client/views/InkingCanvas.tsx
@@ -78,7 +78,7 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
this.previousState = new Map(this.inkData);
- if (InkingControl.Instance.selectedTool !== InkTool.Eraser) {
+ if (InkingControl.Instance.selectedTool !== InkTool.Eraser && InkingControl.Instance.selectedTool !== InkTool.Scrubber) {
// start the new line, saves a uuid to represent the field of the stroke
this._currentStrokeId = Utils.GenerateGuid();
const data = this.inkData;
@@ -87,7 +87,8 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
color: InkingControl.Instance.selectedColor,
width: InkingControl.Instance.selectedWidth,
tool: InkingControl.Instance.selectedTool,
- page: NumCast(this.props.Document.curPage, -1)
+ displayTimecode: NumCast(this.props.Document.currentTimecode, -1),
+ creationTime: new Date().getTime()
});
this.inkData = data;
}
@@ -120,7 +121,7 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
onPointerMove = (e: PointerEvent): void => {
e.stopPropagation();
e.preventDefault();
- if (InkingControl.Instance.selectedTool !== InkTool.Eraser) {
+ if (InkingControl.Instance.selectedTool !== InkTool.Eraser && InkingControl.Instance.selectedTool !== InkTool.Scrubber) {
let data = this.inkData; // add points to new line as it is being drawn
let strokeData = data.get(this._currentStrokeId);
if (strokeData) {
@@ -150,9 +151,9 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
@computed
get drawnPaths() {
- let curPage = NumCast(this.props.Document.curPage, -1);
+ let curTimecode = NumCast(this.props.Document.currentTimecode, -1);
let paths = Array.from(this.inkData).reduce((paths, [id, strokeData]) => {
- if (strokeData.page === -1 || (Math.abs(Math.round(strokeData.page) - Math.round(curPage)) < 3)) {
+ if (strokeData.displayTimecode === -1 || (Math.abs(Math.round(strokeData.displayTimecode) - Math.round(curTimecode)) < 3)) {
paths.push(<InkingStroke key={id} id={id}
line={strokeData.pathData}
count={strokeData.pathData.length}
@@ -161,6 +162,7 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
color={strokeData.color}
width={strokeData.width}
tool={strokeData.tool}
+ creationTime={strokeData.creationTime}
deleteCallback={this.removeLine} />);
}
return paths;
@@ -181,9 +183,11 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
render() {
let svgCanvasStyle = InkingControl.Instance.selectedTool !== InkTool.None && !this.props.Document.isBackground ? "canSelect" : "noSelect";
+ let cursor = svgCanvasStyle === "canSelect" ? (InkingControl.Instance.selectedTool === InkTool.Eraser ||
+ InkingControl.Instance.selectedTool === InkTool.Scrubber ? "pointer" : "default") : undefined;
return (
<div className="inkingCanvas">
- <div className={`inkingCanvas-${svgCanvasStyle}`} onPointerDown={this.onPointerDown} />
+ <div className={`inkingCanvas-${svgCanvasStyle}`} onPointerDown={this.onPointerDown} style={{ cursor: cursor }} />
{this.props.children()}
{this.drawnPaths}
</div >
diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx
index a10df0e75..75faa9641 100644
--- a/src/client/views/InkingControl.tsx
+++ b/src/client/views/InkingControl.tsx
@@ -1,39 +1,31 @@
-import { observable, action, computed, runInAction } from "mobx";
+import { action, computed, observable } from "mobx";
import { ColorResult } from 'react-color';
-import React = require("react");
-import { observer } from "mobx-react";
-import "./InkingControl.scss";
-import { library } from '@fortawesome/fontawesome-svg-core';
-import { faPen, faHighlighter, faEraser, faBan } from '@fortawesome/free-solid-svg-icons';
-import { SelectionManager } from "../util/SelectionManager";
-import { InkTool } from "../../new_fields/InkField";
import { Doc } from "../../new_fields/Doc";
-import { undoBatch, UndoManager } from "../util/UndoManager";
-import { StrCast, NumCast, Cast } from "../../new_fields/Types";
-import { MainOverlayTextBox } from "./MainOverlayTextBox";
-import { listSpec } from "../../new_fields/Schema";
+import { InkTool } from "../../new_fields/InkField";
import { List } from "../../new_fields/List";
+import { listSpec } from "../../new_fields/Schema";
+import { Cast, NumCast, StrCast } from "../../new_fields/Types";
import { Utils } from "../../Utils";
+import { Scripting } from "../util/Scripting";
+import { SelectionManager } from "../util/SelectionManager";
+import { undoBatch, UndoManager } from "../util/UndoManager";
+import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
-library.add(faPen, faHighlighter, faEraser, faBan);
-@observer
-export class InkingControl extends React.Component {
- static Instance: InkingControl = new InkingControl({});
+export class InkingControl {
+ @observable static Instance: InkingControl;
@observable private _selectedTool: InkTool = InkTool.None;
@observable private _selectedColor: string = "rgb(244, 67, 54)";
@observable private _selectedWidth: string = "5";
@observable public _open: boolean = false;
- constructor(props: Readonly<{}>) {
- super(props);
+ constructor() {
InkingControl.Instance = this;
}
- @action
- switchTool = (tool: InkTool): void => {
+ switchTool = action((tool: InkTool): void => {
this._selectedTool = tool;
- }
+ });
decimalToHexString(number: number) {
if (number < 0) {
number = 0xFFFFFFFF + number + 1;
@@ -45,11 +37,21 @@ export class InkingControl extends React.Component {
@undoBatch
switchColor = action((color: ColorResult): void => {
this._selectedColor = color.hex + (color.rgb.a !== undefined ? this.decimalToHexString(Math.round(color.rgb.a * 255)) : "ff");
+
if (InkingControl.Instance.selectedTool === InkTool.None) {
- if (MainOverlayTextBox.Instance.SetColor(color.hex)) return;
let selected = SelectionManager.SelectedDocuments();
let oldColors = selected.map(view => {
- let targetDoc = view.props.Document.layout instanceof Doc ? view.props.Document.layout : view.props.Document.isTemplate ? view.props.Document : Doc.GetProto(view.props.Document);
+ let targetDoc = view.props.Document.dragFactory instanceof Doc ? view.props.Document.dragFactory :
+ view.props.Document.layout instanceof Doc ? view.props.Document.layout :
+ view.props.Document.isTemplateField ? view.props.Document : Doc.GetProto(view.props.Document);
+ let sel = window.getSelection();
+ if (StrCast(targetDoc.layout).indexOf("FormattedTextBox") !== -1 && (!sel || sel.toString() !== "")) {
+ targetDoc.color = this._selectedColor;
+ return {
+ target: targetDoc,
+ previous: StrCast(targetDoc.color)
+ };
+ }
let oldColor = StrCast(targetDoc.backgroundColor);
let matchedColor = this._selectedColor;
const cvd = view.props.ContainingCollectionDoc;
@@ -79,18 +81,20 @@ export class InkingControl extends React.Component {
ruleProvider = (view.props.Document.heading && ruleProvider) ? ruleProvider : undefined;
ruleProvider && ((Doc.GetProto(ruleProvider)["ruleColor_" + NumCast(view.props.Document.heading)] = Utils.toRGBAstr(color.rgb)));
}
- !ruleProvider && (targetDoc.backgroundColor = matchedColor);
+ (!ruleProvider && targetDoc) && (Doc.Layout(view.props.Document).backgroundColor = matchedColor);
return {
target: targetDoc,
previous: oldColor
};
});
- let captured = this._selectedColor;
- UndoManager.AddEvent({
- undo: () => oldColors.forEach(pair => pair.target.backgroundColor = pair.previous),
- redo: () => oldColors.forEach(pair => pair.target.backgroundColor = captured)
- });
+ //let captured = this._selectedColor;
+ // UndoManager.AddEvent({
+ // undo: () => oldColors.forEach(pair => pair.target.backgroundColor = pair.previous),
+ // redo: () => oldColors.forEach(pair => pair.target.backgroundColor = captured)
+ // });
+ } else {
+ CurrentUserUtils.ActivePen && (CurrentUserUtils.ActivePen.backgroundColor = this._selectedColor);
}
});
@action
@@ -118,22 +122,11 @@ export class InkingControl extends React.Component {
return this._selectedWidth;
}
- @action
- toggleDisplay = () => {
- this._open = !this._open;
- this.switchTool(this._open ? InkTool.Pen : InkTool.None);
- }
- render() {
- return (
- <ul className="inking-control" style={this._open ? { display: "flex" } : { display: "none" }}>
- <li className="ink-size ink-panel">
- <label htmlFor="stroke-width">SIZE: </label>
- <input type="text" min="1" max="100" value={this._selectedWidth} name="stroke-width"
- onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.switchWidth(e.target.value)} />
- <input type="range" min="1" max="100" value={this._selectedWidth} name="stroke-width"
- onChange={(e: React.ChangeEvent<HTMLInputElement>) => this.switchWidth(e.target.value)} />
- </li>
- </ul >
- );
- }
-} \ No newline at end of file
+}
+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 deactivateInk() { return InkingControl.Instance.switchTool(InkTool.None); });
+Scripting.addGlobal(function setInkWidth(width: any) { return InkingControl.Instance.switchWidth(width); });
+Scripting.addGlobal(function setInkColor(color: any) { return InkingControl.Instance.updateSelectedColor(color); }); \ No newline at end of file
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index b8d428d31..332c22512 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -1,9 +1,10 @@
import { observer } from "mobx-react";
-import { observable, trace } from "mobx";
+import { observable, trace, runInAction } from "mobx";
import { InkingControl } from "./InkingControl";
import React = require("react");
import { InkTool } from "../../new_fields/InkField";
import "./InkingStroke.scss";
+import { AudioBox } from "./nodes/AudioBox";
interface StrokeProps {
@@ -15,6 +16,7 @@ interface StrokeProps {
color: string;
width: string;
tool: InkTool;
+ creationTime: number;
deleteCallback: (index: string) => void;
}
@@ -31,6 +33,11 @@ export class InkingStroke extends React.Component<StrokeProps> {
e.stopPropagation();
e.preventDefault();
}
+ if (InkingControl.Instance.selectedTool === InkTool.Scrubber && e.buttons === 1) {
+ AudioBox.SetScrubTime(this.props.creationTime);
+ e.stopPropagation();
+ e.preventDefault();
+ }
}
parseData = (line: Array<{ x: number, y: number }>): string => {
@@ -55,10 +62,9 @@ export class InkingStroke extends React.Component<StrokeProps> {
let pathlength = this.props.count; // bcz: this is needed to force reactions to the line's data changes
let marker = this.props.tool === InkTool.Highlighter ? "-marker" : "";
- let pointerEvents: any = InkingControl.Instance.selectedTool === InkTool.Eraser ? "all" : "none";
- return (
- <path className={`inkingStroke${marker}`} d={pathData} style={{ ...pathStyle, pointerEvents: pointerEvents }} strokeLinejoin="round" strokeLinecap="round"
- onPointerOver={this.deleteStroke} onPointerDown={this.deleteStroke} />
- );
+ let pointerEvents: any = InkingControl.Instance.selectedTool === InkTool.Eraser ||
+ InkingControl.Instance.selectedTool === InkTool.Scrubber ? "all" : "none";
+ return (<path className={`inkingStroke${marker}`} d={pathData} style={{ ...pathStyle, pointerEvents: pointerEvents }}
+ strokeLinejoin="round" strokeLinecap="round" onPointerOver={this.deleteStroke} onPointerDown={this.deleteStroke} />);
}
} \ No newline at end of file
diff --git a/src/client/views/Main.scss b/src/client/views/Main.scss
index bc0975c86..134a4ac85 100644
--- a/src/client/views/Main.scss
+++ b/src/client/views/Main.scss
@@ -21,12 +21,13 @@ div {
}
-
.jsx-parser {
width: 100%;
height: 100%;
pointer-events: none;
border-radius: inherit;
+ position: inherit;
+ background: inherit;
}
p {
@@ -64,248 +65,10 @@ button:hover {
cursor: pointer;
}
-.clear-db-button {
- position: absolute;
- right: 45%;
- bottom: 3%;
- font-size: 50%;
-}
-
-.round-button {
- width: 36px;
- height: 36px;
- border-radius: 18px;
- font-size: 15px;
-}
-
-.round-button:hover {
- transform: scale(1.15);
-}
-
-.add-button {
- position: relative;
- margin-right: 10px;
-}
-
-.main-undoButtons {
- position: absolute;
- width: 150px;
- right: 0px;
-}
-
-.main-notifs-badge {
- position: absolute;
- top: -10px;
- right: -10px;
- color: white;
- background: #f44b42;
- font-weight: 300;
- border-radius: 100%;
- width: 25px;
- height: 25px;
- text-align: center;
- padding-top: 4px;
- font-size: 12px;
-}
-
-//toolbar stuff
-#toolbar {
- position: absolute;
- right: 8px;
- top: 5px;
-
- .toolbar-button {
- display: block;
- margin-bottom: 10px;
- }
-}
-
-.toolbar-color-picker {
- background-color: $light-color;
- border-radius: 5px;
- padding: 12px;
- position: absolute;
- bottom: 36px;
- left: -3px;
- box-shadow: $intermediate-color 0.2vw 0.2vw 0.8vw;
-}
-
-.toolbar-color-button {
- border-radius: 11px;
- width: 22px;
- height: 22px;
- cursor: pointer;
- text-align: center; // span {
- // color: $light-color;
- // font-size: 8px;
- // user-select: none;
- // }
- margin-top: -2.55px;
- margin-left: -2.55px;
-}
-
-// add nodes menu. Note that the + button is actually an input label, not an actual button.
-#add-nodes-menu {
- position: absolute;
- bottom: 22px;
- left: 250px;
-
- >label {
- background: $dark-color;
- color: $light-color;
- display: inline-block;
- border-radius: 18px;
- font-size: 25px;
- width: 36px;
- height: 36px;
- margin-right: 10px;
- cursor: pointer;
- transition: transform 0.2s;
- }
-
- label p {
- padding-left: 10.5px;
- }
-
- label:hover {
- background: $main-accent;
- transform: scale(1.15);
- }
-
- >input {
- display: none;
- }
-
- >input:not(:checked)~#add-options-content {
- display: none;
- }
-
- >input:checked~label {
- transform: rotate(45deg);
- transition: transform 0.5s;
- cursor: pointer;
- }
-}
-
#root {
overflow: visible;
}
-#main-div {
- width: 100%;
- height: 100%;
- position: absolute;
- top: 0;
- left: 0;
- overflow: auto;
- z-index: 1;
-}
-
-#mainContent-div {
- width: 100%;
- height: 100%;
- position: absolute;
- top: 0;
- left: 0;
- overflow: hidden;
-}
-
-#add-options-content {
- display: table;
- opacity: 1;
- margin: 0;
- padding: 0;
- position: relative;
- float: right;
- bottom: 0.3em;
- margin-bottom: -1.68em;
-}
-
-ul#add-options-list {
- list-style: none;
- padding: 5 0 0 0;
-
- >li {
- display: inline-block;
- padding: 0;
- }
-}
-
-.mainView-libraryFlyout {
- height: 100%;
- position: absolute;
- display: flex;
- flex-direction: column;
-}
-
-.mainView-libraryHandle {
- width: 20px;
- height: 40px;
- top: 50%;
- border: 1px solid black;
- border-radius: 5px;
- position: absolute;
- z-index: 1;
-}
-
.svg-inline--fa {
vertical-align: unset;
-}
-
-.mainView-workspace {
- height: 200px;
- position: relative;
- display: flex;
-}
-
-.mainView-library {
- height: 75%;
- position: relative;
- display: flex;
-}
-
-.mainView-recentlyClosed {
- height: 25%;
- position: relative;
- display: flex;
-}
-
-.dictation-prompt {
- position: absolute;
- z-index: 1000;
- text-align: center;
- justify-content: center;
- align-self: center;
- align-content: center;
- padding: 20px;
- background: gainsboro;
- border-radius: 10px;
- border: 3px solid black;
- box-shadow: #00000044 5px 5px 10px;
- transform: translate(-50%, -50%);
- top: 50%;
- font-style: italic;
- left: 50%;
- transition: 0.5s all ease;
- pointer-events: none;
-}
-
-.dictation-prompt-overlay {
- width: 100%;
- height: 100%;
- position: absolute;
- z-index: 999;
- transition: 0.5s all ease;
- pointer-events: none;
-}
-
-.webpage-input {
- display: none;
- height: 60px;
- width: 600px;
- position: absolute;
-
- .url-input {
- width: 80%;
- }
} \ No newline at end of file
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index 11ec6f0c9..b21eb9c8f 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -3,41 +3,19 @@ import { Docs } from "../documents/Documents";
import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
import * as ReactDOM from 'react-dom';
import * as React from 'react';
-import { Cast } from "../../new_fields/Types";
-import { Doc, DocListCastAsync } from "../../new_fields/Doc";
-import { List } from "../../new_fields/List";
import { DocServer } from "../DocServer";
+import { AssignAllExtensions } from "../../extensions/General/Extensions";
-let swapDocs = async () => {
- let oldDoc = await Cast(CurrentUserUtils.UserDocument.linkManagerDoc, Doc);
- // Docs.Prototypes.MainLinkDocument().allLinks = new List<Doc>();
- if (oldDoc) {
- let links = await DocListCastAsync(oldDoc.allLinks);
- // if (links && DocListCast(links)) {
- if (links && links.length) {
- let data = await DocListCastAsync(Docs.Prototypes.MainLinkDocument().allLinks);
- if (data) {
- data.push(...links.filter(i => data!.indexOf(i) === -1));
- Docs.Prototypes.MainLinkDocument().allLinks = new List<Doc>(data.filter((i, idx) => data!.indexOf(i) === idx));
- }
- else {
- Docs.Prototypes.MainLinkDocument().allLinks = new List<Doc>(links);
- }
- }
- CurrentUserUtils.UserDocument.linkManagerDoc = undefined;
- }
-};
+AssignAllExtensions();
(async () => {
const info = await CurrentUserUtils.loadCurrentUser();
DocServer.init(window.location.protocol, window.location.hostname, 4321, info.email);
await Docs.Prototypes.initialize();
- await CurrentUserUtils.loadUserDocument(info);
- // updates old user documents to prevent chrome on tree view.
- (await Cast(CurrentUserUtils.UserDocument.workspaces, Doc))!.chromeStatus = "disabled";
- (await Cast(CurrentUserUtils.UserDocument.recentlyClosed, Doc))!.chromeStatus = "disabled";
- (await Cast(CurrentUserUtils.UserDocument.sidebar, Doc))!.chromeStatus = "disabled";
- await swapDocs();
+ if (info.id !== "__guest__") {
+ // a guest will not have an id registered
+ await CurrentUserUtils.loadUserDocument(info);
+ }
document.getElementById('root')!.addEventListener('wheel', event => {
if (event.ctrlKey) {
event.preventDefault();
diff --git a/src/client/views/MainOverlayTextBox.scss b/src/client/views/MainOverlayTextBox.scss
deleted file mode 100644
index c9d44e194..000000000
--- a/src/client/views/MainOverlayTextBox.scss
+++ /dev/null
@@ -1,29 +0,0 @@
-@import "globalCssVariables";
-
-.mainOverlayTextBox-textInput {
- background-color: rgba(248, 6, 6, 0.001);
- width: 400px;
- height: 200px;
- position: absolute;
- overflow: visible;
- top: 0;
- left: 0;
- pointer-events: none;
- z-index: $mainTextInput-zindex;
-
- .formattedTextBox-cont {
- background-color: rgba(248, 6, 6, 0.001);
- width: 100%;
- height: 100%;
- position: absolute;
- top: 0;
- left: 0;
- }
-}
-
-.mainOverlayTextBox-unscaled_div {
- // width: 0px;
- z-index: 10000;
- position: absolute;
- pointer-events: none;
-} \ No newline at end of file
diff --git a/src/client/views/MainOverlayTextBox.tsx b/src/client/views/MainOverlayTextBox.tsx
deleted file mode 100644
index 335cc609f..000000000
--- a/src/client/views/MainOverlayTextBox.tsx
+++ /dev/null
@@ -1,155 +0,0 @@
-import { action, observable, reaction, trace } from 'mobx';
-import { observer } from 'mobx-react';
-import "normalize.css";
-import * as React from 'react';
-import { Doc, DocListCast, Opt } from '../../new_fields/Doc';
-import { BoolCast } from '../../new_fields/Types';
-import { emptyFunction, returnTrue, returnZero, Utils, returnOne } from '../../Utils';
-import { DragManager } from '../util/DragManager';
-import { Transform } from '../util/Transform';
-import { CollectionDockingView } from './collections/CollectionDockingView';
-import "./MainOverlayTextBox.scss";
-import { FormattedTextBox } from './nodes/FormattedTextBox';
-
-interface MainOverlayTextBoxProps {
- firstinstance?: boolean;
-}
-
-@observer
-export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps> {
- public static Instance: MainOverlayTextBox;
- @observable _textXf: () => Transform = () => Transform.Identity();
- public TextFieldKey: string = "data";
- private _textColor: string | null = null;
- private _textHideOnLeave?: boolean;
- private _textTargetDiv: HTMLDivElement | undefined;
- private _textProxyDiv: React.RefObject<HTMLDivElement>;
- private _textBottom: boolean | undefined;
- private _setouterdiv = (outerdiv: HTMLElement | null) => { this._outerdiv = outerdiv; this.updateTooltip(); };
- private _outerdiv: HTMLElement | null = null;
- private _textBox: FormattedTextBox | undefined;
- private _tooltip?: HTMLElement;
- ChromeHeight?: () => number;
- @observable public TextDoc?: Doc;
- @observable public TextDataDoc?: Doc;
-
- updateTooltip = () => {
- this._outerdiv && this._tooltip && !this._outerdiv.contains(this._tooltip) && this._outerdiv.appendChild(this._tooltip);
- }
-
- public SetColor(color: string) {
- return this._textBox && this._textBox.setFontColor(color);
- }
-
- constructor(props: MainOverlayTextBoxProps) {
- super(props);
- this._textProxyDiv = React.createRef();
- MainOverlayTextBox.Instance = this;
- reaction(() => FormattedTextBox.InputBoxOverlay,
- (box?: FormattedTextBox) => {
- this._textBox = box;
- if (box) {
- this.ChromeHeight = box.props.ChromeHeight;
- this.TextDoc = box.props.Document;
- this.TextDataDoc = box.props.DataDoc;
- let xf = () => {
- box.props.ScreenToLocalTransform();
- let sxf = Utils.GetScreenTransform(box ? box.CurrentDiv : undefined);
- return new Transform(-sxf.translateX, -sxf.translateY, 1 / sxf.scale);
- };
- this.setTextDoc(box.props.fieldKey, box.CurrentDiv, xf, BoolCast(box.props.Document.autoHeight) || box.props.height === "min-content");
- }
- else {
- this.TextDoc = undefined;
- this.TextDataDoc = undefined;
- this.setTextDoc();
- }
- });
- }
-
- @action
- private setTextDoc(textFieldKey?: string, div?: HTMLDivElement, tx?: () => Transform, autoHeight?: boolean) {
- if (this._textTargetDiv) {
- this._textTargetDiv.style.color = this._textColor;
- }
- this.TextFieldKey = textFieldKey!;
- let txf = tx ? tx : () => Transform.Identity();
- this._textXf = txf;
- this._textTargetDiv = div;
- this._textHideOnLeave = FormattedTextBox.InputBoxOverlay && FormattedTextBox.InputBoxOverlay.props.hideOnLeave;
- if (div) {
- this._textBottom = div.parentElement && getComputedStyle(div.parentElement).top !== "0px" ? true : false;
- this._textColor = (getComputedStyle(div) as any).color;
- div.style.color = "transparent";
- }
- }
-
- @action
- textScroll = (e: React.UIEvent) => {
- if (this._textProxyDiv.current && this._textTargetDiv) {
- this._textTargetDiv.scrollTop = (e as any)._targetInst.stateNode.scrollTop;
- }
- }
-
- textBoxDown = (e: React.PointerEvent) => {
- if (e.button !== 0 || e.metaKey || e.altKey) {
- document.addEventListener("pointermove", this.textBoxMove);
- document.addEventListener('pointerup', this.textBoxUp);
- }
- }
- @action
- textBoxMove = (e: PointerEvent) => {
- if ((e.movementX > 1 || e.movementY > 1) && FormattedTextBox.InputBoxOverlay) {
- document.removeEventListener("pointermove", this.textBoxMove);
- document.removeEventListener('pointerup', this.textBoxUp);
- let dragData = new DragManager.DocumentDragData([FormattedTextBox.InputBoxOverlay.props.Document]);
- const [left, top] = this._textXf().inverse().transformPoint(0, 0);
- dragData.offset = [e.clientX - left, e.clientY - top];
- DragManager.StartDocumentDrag([this._textTargetDiv!], dragData, e.clientX, e.clientY, {
- handlers: {
- dragComplete: action(emptyFunction),
- },
- hideSource: false
- });
- }
- }
- textBoxUp = (e: PointerEvent) => {
- document.removeEventListener("pointermove", this.textBoxMove);
- document.removeEventListener('pointerup', this.textBoxUp);
- }
-
- addDocTab = (doc: Doc, dataDoc: Opt<Doc>, location: string) => {
- return this._textBox && this._textBox.props.addDocTab(doc, dataDoc, location) ? true : false;
- }
- render() {
- this.TextDoc; this.TextDataDoc;
- if (FormattedTextBox.InputBoxOverlay && this._textTargetDiv) {
- let wid = FormattedTextBox.InputBoxOverlay.props.Document.width; // need to force overlay to render when underlying text box is resized (eg, w/ DocDecorations)
- let hgtx = FormattedTextBox.InputBoxOverlay.props.Document.height; // need to force overlay to render when underlying text box is resized (eg, w/ DocDecorations)
- let textRect = this._textTargetDiv.getBoundingClientRect();
- let s = this._textXf().Scale;
- let location = this._textBottom ? textRect.bottom : textRect.top;
- let hgt = (this._textBox && this._textBox.props.Document.autoHeight) || this._textBottom ? "auto" : this._textTargetDiv.clientHeight;
- return <div ref={this._setouterdiv} className="mainOverlayTextBox-unscaled_div" style={{ transform: `translate(${textRect.left}px, ${location}px)` }} >
- <div className="mainOverlayTextBox-textInput" style={{ transform: `scale(${1 / s})`, width: "auto", height: "0px" }} >
- <div className="mainOverlayTextBox-textInput" onPointerDown={this.textBoxDown} ref={this._textProxyDiv} onScroll={this.textScroll}
- style={{ width: `${textRect.width * s}px`, height: "0px" }}>
- <div style={{ height: hgt, width: "100%", position: "absolute", bottom: this._textBottom ? "0px" : undefined }}>
- <FormattedTextBox color={`${this._textColor}`} fieldKey={this.TextFieldKey} fieldExt="" hideOnLeave={this._textHideOnLeave} isOverlay={true}
- Document={FormattedTextBox.InputBoxOverlay.props.Document}
- DataDoc={FormattedTextBox.InputBoxOverlay.props.DataDoc}
- onClick={undefined}
- ruleProvider={this._textBox ? this._textBox.props.ruleProvider : undefined}
- ChromeHeight={this.ChromeHeight} isSelected={returnTrue} select={emptyFunction} renderDepth={0}
- ContainingCollectionDoc={undefined} ContainingCollectionView={undefined}
- whenActiveChanged={emptyFunction} active={returnTrue} ContentScaling={returnOne}
- ScreenToLocalTransform={this._textXf} PanelWidth={returnZero} PanelHeight={returnZero} focus={emptyFunction}
- pinToPres={returnZero} addDocTab={this.addDocTab} outer_div={(tooltip: HTMLElement) => { this._tooltip = tooltip; this.updateTooltip(); }} />
- </div>
- </div>
- </div>
- </ div>;
- }
- else return (null);
- }
-} \ No newline at end of file
diff --git a/src/client/views/MainView.scss b/src/client/views/MainView.scss
new file mode 100644
index 000000000..a858a73c7
--- /dev/null
+++ b/src/client/views/MainView.scss
@@ -0,0 +1,96 @@
+@import "globalCssVariables";
+@import "nodeModuleOverrides";
+
+
+.mainView-tabButtons {
+ position: relative;
+ width:100%;
+}
+// add nodes menu. Note that the + button is actually an input label, not an actual button.
+.mainView-docButtons {
+ position: absolute;
+ bottom: 20px;
+ left: 250px;
+}
+
+#mainView-container {
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ top: 0;
+ left: 0;
+ overflow: auto;
+ z-index: 1;
+}
+.mainView-mainContent {
+ width:100%;
+ height:100%;
+ position:absolute;
+}
+.mainView-flyoutContainer{
+ display:flex;
+ flex-direction: column;
+ position: absolute;
+ width:100%;
+ height:100%;
+ .documentView-node-topmost {
+ background: lightgrey;
+ }
+}
+.mainView-mainDiv {
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ top: 0;
+ left: 0;
+ overflow: hidden;
+}
+
+.mainView-logout {
+ position: absolute;
+ right: 0;
+ bottom: 0;
+ font-size: 8px;
+}
+
+.mainView-libraryFlyout {
+ height: 100%;
+ position: absolute;
+ display: flex;
+ flex-direction: column;
+}
+
+.mainView-expandFlyoutButton {
+ position: absolute;
+ top: 5px;
+ right: 5px;
+ cursor: pointer;
+}
+
+.mainView-libraryHandle {
+ width: 20px;
+ height: 40px;
+ top: 50%;
+ border: 1px solid black;
+ border-radius: 5px;
+ position: absolute;
+ z-index: 1;
+}
+
+.mainView-workspace {
+ height: 200px;
+ position: relative;
+ display: flex;
+}
+
+.mainView-library {
+ height: 75%;
+ position: relative;
+ display: flex;
+}
+
+.mainView-recentlyClosed {
+ height: 25%;
+ position: relative;
+ display: flex;
+} \ No newline at end of file
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index a244e22e7..90b3d4c1b 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -1,124 +1,63 @@
-import { IconName, library } from '@fortawesome/fontawesome-svg-core';
-import { faLink, faArrowDown, faArrowUp, faBolt, faCaretUp, faCat, faCheck, faClone, faCloudUploadAlt, faCommentAlt, faCut, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faLongArrowAltRight, faMusic, faObjectGroup, faPause, faPenNib, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faUndoAlt, faTv } from '@fortawesome/free-solid-svg-icons';
+import { library } from '@fortawesome/fontawesome-svg-core';
+import {
+ faArrowDown, 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
+} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed, configure, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import "normalize.css";
import * as React from 'react';
-import { SketchPicker } from 'react-color';
import Measure from 'react-measure';
-import { Doc, DocListCast, Opt, HeightSym } from '../../new_fields/Doc';
-import { List } from '../../new_fields/List';
+import { Doc, DocListCast, Field, FieldResult, Opt } from '../../new_fields/Doc';
import { Id } from '../../new_fields/FieldSymbols';
-import { InkTool } from '../../new_fields/InkField';
+import { List } from '../../new_fields/List';
import { listSpec } from '../../new_fields/Schema';
-import { BoolCast, Cast, FieldValue, StrCast, NumCast } from '../../new_fields/Types';
+import { Cast, FieldValue, StrCast } from '../../new_fields/Types';
import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils';
import { RouteStore } from '../../server/RouteStore';
-import { emptyFunction, returnOne, returnTrue, Utils, returnEmptyString } from '../../Utils';
+import { emptyFunction, returnEmptyString, returnFalse, returnOne, returnTrue, Utils } from '../../Utils';
+import GoogleAuthenticationManager from '../apis/GoogleAuthenticationManager';
import { DocServer } from '../DocServer';
-import { Docs } from '../documents/Documents';
-import { ClientUtils } from '../util/ClientUtils';
-import { DictationManager } from '../util/DictationManager';
-import { SetupDrag } from '../util/DragManager';
+import { Docs, DocumentOptions } from '../documents/Documents';
import { HistoryUtil } from '../util/History';
+import SharingManager from '../util/SharingManager';
import { Transform } from '../util/Transform';
-import { UndoManager, undoBatch } from '../util/UndoManager';
-import { CollectionBaseView } from './collections/CollectionBaseView';
+import { CollectionLinearView } from './CollectionLinearView';
+import { CollectionViewType, CollectionView } from './collections/CollectionView';
import { CollectionDockingView } from './collections/CollectionDockingView';
-import { CollectionTreeView } from './collections/CollectionTreeView';
import { ContextMenu } from './ContextMenu';
+import { DictationOverlay } from './DictationOverlay';
import { DocumentDecorations } from './DocumentDecorations';
import KeyManager from './GlobalKeyHandler';
-import { InkingControl } from './InkingControl';
-import "./Main.scss";
-import { MainOverlayTextBox } from './MainOverlayTextBox';
+import "./MainView.scss";
+import { MainViewNotifs } from './MainViewNotifs';
import { DocumentView } from './nodes/DocumentView';
import { OverlayView } from './OverlayView';
import PDFMenu from './pdf/PDFMenu';
import { PreviewCursor } from './PreviewCursor';
-import { FilterBox } from './search/FilterBox';
+import { Scripting } from '../util/Scripting';
+import { AudioBox } from './nodes/AudioBox';
+import {Timeline} from './animationtimeline/Timeline';
import { TimelineMenu } from './animationtimeline/TimelineMenu';
-import PresModeMenu from './presentationview/PresentationModeMenu';
-import { PresBox } from './nodes/PresBox';
-import { LinkFollowBox } from './linking/LinkFollowBox';
-import { DocumentManager } from '../util/DocumentManager';
-
@observer
export class MainView extends React.Component {
public static Instance: MainView;
- @observable addMenuToggle = React.createRef<HTMLInputElement>();
- @observable public pwidth: number = 0;
- @observable public pheight: number = 0;
+ private _buttonBarHeight = 75;
+ private _flyoutSizeOnDown = 0;
+ private _urlState: HistoryUtil.DocUrl;
+ private _docBtnRef = React.createRef<HTMLDivElement>();
- @observable private dictationState = DictationManager.placeholder;
- @observable private dictationSuccessState: boolean | undefined = undefined;
- @observable private dictationDisplayState = false;
- @observable private dictationListeningState: DictationManager.Controls.ListeningUIStatus = false;
-
- public overlayTimeout: NodeJS.Timeout | undefined;
-
- public initiateDictationFade = () => {
- let duration = DictationManager.Commands.dictationFadeDuration;
- this.overlayTimeout = setTimeout(() => {
- this.dictationOverlayVisible = false;
- this.dictationSuccess = undefined;
- setTimeout(() => this.dictatedPhrase = DictationManager.placeholder, 500);
- }, duration);
- }
+ @observable private _panelWidth: number = 0;
+ @observable private _panelHeight: number = 0;
+ @observable private _flyoutTranslate: boolean = true;
+ @observable public flyoutWidth: number = 250;
- public cancelDictationFade = () => {
- if (this.overlayTimeout) {
- clearTimeout(this.overlayTimeout);
- this.overlayTimeout = undefined;
- }
- }
+ @computed private get userDoc() { return CurrentUserUtils.UserDocument; }
+ @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 private get mainContainer(): Opt<Doc> {
- return FieldValue(Cast(CurrentUserUtils.UserDocument.activeWorkspace, Doc));
- }
- @computed get mainFreeform(): Opt<Doc> {
- let docs = DocListCast(this.mainContainer!.data);
- return (docs && docs.length > 1) ? docs[1] : undefined;
- }
public isPointerDown = false;
- private set mainContainer(doc: Opt<Doc>) {
- if (doc) {
- CurrentUserUtils.UserDocument.activeWorkspace = doc;
- }
- }
-
- @computed public get dictatedPhrase() {
- return this.dictationState;
- }
-
- public set dictatedPhrase(value: string) {
- runInAction(() => this.dictationState = value);
- }
-
- @computed public get dictationSuccess() {
- return this.dictationSuccessState;
- }
-
- public set dictationSuccess(value: boolean | undefined) {
- runInAction(() => this.dictationSuccessState = value);
- }
-
- @computed public get dictationOverlayVisible() {
- return this.dictationDisplayState;
- }
-
- public set dictationOverlayVisible(value: boolean) {
- runInAction(() => this.dictationDisplayState = value);
- }
-
- @computed public get isListening() {
- return this.dictationListeningState;
- }
-
- public set isListening(value: DictationManager.Controls.ListeningUIStatus) {
- runInAction(() => this.dictationListeningState = value);
- }
componentWillMount() {
var tag = document.createElement('script');
@@ -128,27 +67,10 @@ export class MainView extends React.Component {
firstScriptTag.parentNode!.insertBefore(tag, firstScriptTag);
window.removeEventListener("keydown", KeyManager.Instance.handle);
window.addEventListener("keydown", KeyManager.Instance.handle);
-
- reaction(() => {
- let workspaces = CurrentUserUtils.UserDocument.workspaces;
- let recent = CurrentUserUtils.UserDocument.recentlyClosed;
- if (!(recent instanceof Doc)) return 0;
- if (!(workspaces instanceof Doc)) return 0;
- let workspacesDoc = workspaces;
- let recentDoc = recent;
- let libraryHeight = this.getPHeight() - workspacesDoc[HeightSym]() - recentDoc[HeightSym]() - 20 + CurrentUserUtils.UserDocument[HeightSym]() * 0.00001;
- return libraryHeight;
- }, (libraryHeight: number) => {
- if (libraryHeight && Math.abs(CurrentUserUtils.UserDocument[HeightSym]() - libraryHeight) > 5) {
- CurrentUserUtils.UserDocument.height = libraryHeight;
- }
- (Cast(CurrentUserUtils.UserDocument.recentlyClosed, Doc) as Doc).allowClear = true;
- }, { fireImmediately: true });
}
componentWillUnMount() {
window.removeEventListener("keydown", KeyManager.Instance.handle);
- //close presentation
window.removeEventListener("pointerdown", this.globalPointerDown);
window.removeEventListener("pointerup", this.globalPointerUp);
}
@@ -156,7 +78,7 @@ export class MainView extends React.Component {
constructor(props: Readonly<{}>) {
super(props);
MainView.Instance = this;
-
+ this._urlState = HistoryUtil.parseUrl(window.location) || {} as any;
// causes errors to be generated when modifying an observable outside of an action
configure({ enforceActions: "observed" });
if (window.location.pathname !== RouteStore.home) {
@@ -165,6 +87,11 @@ export class MainView extends React.Component {
let type = pathname[0];
if (type === "doc") {
CurrentUserUtils.MainDocId = pathname[1];
+ if (!this.userDoc) {
+ runInAction(() => this.flyoutWidth = 0);
+ DocServer.GetRefField(CurrentUserUtils.MainDocId).then(action((field: Opt<Field>) =>
+ field instanceof Doc && (CurrentUserUtils.GuestTarget = field)));
+ }
}
}
}
@@ -179,7 +106,13 @@ export class MainView extends React.Component {
library.add(faGlobeAsia);
library.add(faUndoAlt);
library.add(faRedoAlt);
+ library.add(faMousePointer);
+ library.add(faPen);
+ library.add(faHighlighter);
+ library.add(faEraser);
+ library.add(faFileAudio);
library.add(faPenNib);
+ library.add(faMicrophone);
library.add(faFilm);
library.add(faMusic);
library.add(faTree);
@@ -196,12 +129,15 @@ export class MainView extends React.Component {
library.add(faArrowUp);
library.add(faCloudUploadAlt);
library.add(faBolt);
+ library.add(faChevronRight);
+ library.add(faEllipsisV);
this.initEventListeners();
this.initAuthenticationRouters();
}
globalPointerDown = action((e: PointerEvent) => {
this.isPointerDown = true;
+ AudioBox.Enabled = true;
const targets = document.elementsFromPoint(e.x, e.y);
if (targets && targets.length && targets[0].className.toString().indexOf("contextMenu") === -1) {
ContextMenu.Instance.closeMenu();
@@ -214,7 +150,6 @@ export class MainView extends React.Component {
globalPointerUp = () => this.isPointerDown = false;
initEventListeners = () => {
- // window.addEventListener("pointermove", (e) => this.reportLocation(e))
window.addEventListener("drop", (e) => e.preventDefault(), false); // drop event handler
window.addEventListener("dragover", (e) => e.preventDefault(), false); // drag event handler
// click interactions for the context menu
@@ -224,74 +159,91 @@ export class MainView extends React.Component {
initAuthenticationRouters = async () => {
// Load the user's active workspace, or create a new one if initial session after signup
- if (!CurrentUserUtils.MainDocId) {
- const doc = await Cast(CurrentUserUtils.UserDocument.activeWorkspace, Doc);
+ let received = CurrentUserUtils.MainDocId;
+ if (received && !this.userDoc) {
+ reaction(
+ () => CurrentUserUtils.GuestTarget,
+ target => target && this.createNewWorkspace(),
+ { fireImmediately: true }
+ );
+ } else {
+ if (received && this._urlState.sharing) {
+ reaction(() => CollectionDockingView.Instance && CollectionDockingView.Instance.initialized,
+ initialized => initialized && received && DocServer.GetRefField(received).then(field => {
+ if (field instanceof Doc && field.viewType !== CollectionViewType.Docking) {
+ CollectionDockingView.AddRightSplit(field, undefined);
+ }
+ }),
+ );
+ }
+ let doc = this.userDoc && await Cast(this.userDoc.activeWorkspace, Doc);
if (doc) {
this.openWorkspace(doc);
} else {
this.createNewWorkspace();
}
- } else {
- DocServer.GetRefField(CurrentUserUtils.MainDocId).then(field => {
- field instanceof Doc ? this.openWorkspace(field) :
- this.createNewWorkspace(CurrentUserUtils.MainDocId);
- });
}
}
-
@action
createNewWorkspace = async (id?: string) => {
- let workspaces = Cast(CurrentUserUtils.UserDocument.workspaces, Doc);
- if (!(workspaces instanceof Doc)) return;
- const list = Cast((CurrentUserUtils.UserDocument.workspaces as Doc).data, listSpec(Doc));
- if (list) {
- let freeformDoc = Docs.Create.FreeformDocument([], { x: 0, y: 400, width: this.pwidth * .7, height: this.pheight, title: `WS collection ${list.length + 1}` });
- var dockingLayout = { content: [{ type: 'row', content: [CollectionDockingView.makeDocumentConfig(freeformDoc, freeformDoc, 600)] }] };
- let mainDoc = Docs.Create.DockDocument([CurrentUserUtils.UserDocument, freeformDoc], JSON.stringify(dockingLayout), { title: `Workspace ${list.length + 1}` }, id);
- if (!CurrentUserUtils.UserDocument.linkManagerDoc) {
- let linkManagerDoc = new Doc();
- linkManagerDoc.allLinks = new List<Doc>([]);
- CurrentUserUtils.UserDocument.linkManagerDoc = linkManagerDoc;
- }
- list.push(mainDoc);
- // bcz: strangely, we need a timeout to prevent exceptions/issues initializing GoldenLayout (the rendering engine for Main Container)
- setTimeout(() => {
- this.openWorkspace(mainDoc);
- // let pendingDocument = Docs.StackingDocument([], { title: "New Mobile Uploads" });
- // mainDoc.optionalRightCollection = pendingDocument;
- }, 0);
+ let freeformOptions: DocumentOptions = {
+ x: 0,
+ y: 400,
+ width: this._panelWidth * .7,
+ height: this._panelHeight,
+ title: "My Blank Collection",
+ backgroundColor: "white"
+ };
+ let workspaces: FieldResult<Doc>;
+ let freeformDoc = CurrentUserUtils.GuestTarget || Docs.Create.FreeformDocument([], freeformOptions);
+ var dockingLayout = { content: [{ type: 'row', content: [CollectionDockingView.makeDocumentConfig(freeformDoc, freeformDoc, 600)] }] };
+ let mainDoc = Docs.Create.DockDocument([freeformDoc], JSON.stringify(dockingLayout), {}, id);
+ if (this.userDoc && ((workspaces = Cast(this.userDoc.workspaces, Doc)) instanceof Doc)) {
+ Doc.AddDocToList(workspaces, "data", mainDoc);
+ mainDoc.title = `Workspace ${DocListCast(workspaces.data).length}`;
}
+ // bcz: strangely, we need a timeout to prevent exceptions/issues initializing GoldenLayout (the rendering engine for Main Container)
+ setTimeout(() => this.openWorkspace(mainDoc), 0);
}
@action
openWorkspace = async (doc: Doc, fromHistory = false) => {
CurrentUserUtils.MainDocId = doc[Id];
- this.mainContainer = doc;
- const state = HistoryUtil.parseUrl(window.location) || {} as any;
- fromHistory || HistoryUtil.pushState({ type: "doc", docId: doc[Id], readonly: state.readonly, nro: state.nro });
- if (state.readonly === true || state.readonly === null) {
+
+ if (doc) { // this has the side-effect of setting the main container since we're assigning the active/guest workspace
+ !("presentationView" in doc) && (doc.presentationView = new List<Doc>([Docs.Create.TreeDocument([], { title: "Presentation" })]));
+ this.userDoc ? (this.userDoc.activeWorkspace = doc) : (CurrentUserUtils.GuestWorkspace = doc);
+ }
+ let state = this._urlState;
+ if (state.sharing === true && !this.userDoc) {
DocServer.Control.makeReadOnly();
- } else if (state.safe) {
- if (!state.nro) {
+ } else {
+ fromHistory || HistoryUtil.pushState({
+ type: "doc",
+ docId: doc[Id],
+ readonly: state.readonly,
+ nro: state.nro,
+ sharing: false,
+ });
+ if (state.readonly === true || state.readonly === null) {
DocServer.Control.makeReadOnly();
+ } else if (state.safe) {
+ if (!state.nro) {
+ DocServer.Control.makeReadOnly();
+ }
+ CollectionView.SetSafeMode(true);
+ } else if (state.nro || state.nro === null || state.readonly === false) {
+ } else if (doc.readOnly) {
+ DocServer.Control.makeReadOnly();
+ } else {
+ DocServer.Control.makeEditable();
}
- CollectionBaseView.SetSafeMode(true);
- } else if (state.nro || state.nro === null || state.readonly === false) {
- } else if (BoolCast(doc.readOnly)) {
- DocServer.Control.makeReadOnly();
- } else {
- DocServer.Control.makeEditable();
}
- const col = await Cast(CurrentUserUtils.UserDocument.optionalRightCollection, Doc);
// if there is a pending doc, and it has new data, show it (syip: we use a timeout to prevent collection docking view from being uninitialized)
setTimeout(async () => {
- if (col) {
- const l = Cast(col.data, listSpec(Doc));
- if (l) {
- runInAction(() => CollectionTreeView.NotifsCol = col);
- }
- }
+ const col = this.userDoc && await Cast(this.userDoc.optionalRightCollection, Doc);
+ col && Cast(col.data, listSpec(Doc)) && runInAction(() => MainViewNotifs.NotifsCol = col);
}, 100);
return true;
}
@@ -304,25 +256,22 @@ export class MainView extends React.Component {
@action
onResize = (r: any) => {
- this.pwidth = r.offset.width;
- this.pheight = r.offset.height;
- }
- getPWidth = () => {
- return this.pwidth;
- }
- getPHeight = () => {
- return this.pheight;
+ this._panelWidth = r.offset.width;
+ this._panelHeight = r.offset.height;
}
+ getPWidth = () => this._panelWidth;
+ getPHeight = () => this._panelHeight;
+ getContentsHeight = () => this._panelHeight - this._buttonBarHeight;
- @observable flyoutWidth: number = 250;
@computed get dockingContent() {
- let flyoutWidth = this.flyoutWidth;
- let mainCont = this.mainContainer;
+ const mainContainer = this.mainContainer;
+ let flyoutWidth = this.flyoutWidth; // bcz: need to be here because Measure messes with observables.
+ let flyoutTranslate = this._flyoutTranslate;
return <Measure offset onResize={this.onResize}>
{({ measureRef }) =>
- <div ref={measureRef} id="mainContent-div" style={{ width: `calc(100% - ${flyoutWidth}px`, transform: `translate(${flyoutWidth}px, 0px)` }} onDrop={this.onDrop}>
- {!mainCont ? (null) :
- <DocumentView Document={mainCont}
+ <div ref={measureRef} id="mainContent-div" style={{ width: `calc(100% - ${flyoutTranslate ? flyoutWidth : 0}px`, transform: `translate(${flyoutTranslate ? flyoutWidth : 0}px, 0px)` }} onDrop={this.onDrop}>
+ {!mainContainer ? (null) :
+ <DocumentView Document={mainContainer}
DataDoc={undefined}
addDocument={undefined}
addDocTab={this.addDocTabFunc}
@@ -350,9 +299,8 @@ export class MainView extends React.Component {
</Measure>;
}
- _downsize = 0;
onPointerDown = (e: React.PointerEvent) => {
- this._downsize = e.clientX;
+ this._flyoutSizeOnDown = e.clientX;
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
document.addEventListener("pointermove", this.onPointerMove);
@@ -360,15 +308,31 @@ export class MainView extends React.Component {
e.stopPropagation();
e.preventDefault();
}
+
+ @action
+ pointerOverDragger = () => {
+ if (this.flyoutWidth === 0) {
+ this.flyoutWidth = 250;
+ this._flyoutTranslate = false;
+ }
+ }
+
+ @action
+ pointerLeaveDragger = () => {
+ if (!this._flyoutTranslate) {
+ this.flyoutWidth = 0;
+ this._flyoutTranslate = true;
+ }
+ }
+
@action
onPointerMove = (e: PointerEvent) => {
this.flyoutWidth = Math.max(e.clientX, 0);
}
@action
onPointerUp = (e: PointerEvent) => {
- if (Math.abs(e.clientX - this._downsize) < 4) {
- if (this.flyoutWidth < 5) this.flyoutWidth = 250;
- else this.flyoutWidth = 0;
+ if (Math.abs(e.clientX - this._flyoutSizeOnDown) < 4) {
+ this.flyoutWidth = this.flyoutWidth < 5 ? 250 : 0;
}
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
@@ -385,207 +349,172 @@ export class MainView extends React.Component {
return CollectionDockingView.AddRightSplit(doc, undefined);
}
}
- @computed
- get flyout() {
- let sidebar = CurrentUserUtils.UserDocument.sidebar;
- if (!(sidebar instanceof Doc)) return (null);
- let sidebarDoc = sidebar;
- return <DocumentView
- Document={sidebarDoc}
- DataDoc={undefined}
- addDocument={undefined}
- addDocTab={this.addDocTabFunc}
- pinToPres={emptyFunction}
- removeDocument={undefined}
- ruleProvider={undefined}
- onClick={undefined}
- ScreenToLocalTransform={Transform.Identity}
- ContentScaling={returnOne}
- PanelWidth={this.flyoutWidthFunc}
- PanelHeight={this.getPHeight}
- renderDepth={0}
- focus={emptyFunction}
- backgroundColor={returnEmptyString}
- parentActive={returnTrue}
- whenActiveChanged={emptyFunction}
- bringToFront={emptyFunction}
- ContainingCollectionView={undefined}
- ContainingCollectionDoc={undefined}
- zoomToScale={emptyFunction}
- getScale={returnOne}>
- </DocumentView>;
- }
- @computed
- get mainContent() {
- let sidebar = CurrentUserUtils.UserDocument.sidebar;
- if (!(sidebar instanceof Doc)) return (null);
- return <div className="mainContent" style={{ width: "100%", height: "100%", position: "absolute" }}>
- <div className="mainView-libraryHandle"
- style={{ cursor: "ew-resize", left: `${this.flyoutWidth - 10}px`, backgroundColor: `${StrCast(sidebar.backgroundColor, "lightGray")}` }}
- onPointerDown={this.onPointerDown}>
- <span title="library View Dragger" style={{ width: "100%", height: "100%", position: "absolute" }} />
- </div>
- <div className="mainView-libraryFlyout" style={{ width: `${this.flyoutWidth}px`, zIndex: 1 }}>
- {this.flyout}
- </div>
- {this.dockingContent}
- </div>;
- }
-
- selected = (tool: InkTool) => {
- if (!InkingControl.Instance || InkingControl.Instance.selectedTool === InkTool.None) return { display: "none" };
- if (InkingControl.Instance.selectedTool === tool) {
- return { color: "#61aaa3", fontSize: "50%" };
- }
- return { fontSize: "50%" };
- }
-
- onColorClick = (e: React.MouseEvent) => {
- let target = (e.nativeEvent as any).path[0];
- let parent = (e.nativeEvent as any).path[1];
- if (target.localName === "input" || parent.localName === "span") {
- e.stopPropagation();
- }
- }
+ mainContainerXf = () => new Transform(0, -this._buttonBarHeight, 1);
- toggleLinkFollowBox = (shouldClose: boolean) => {
- if (LinkFollowBox.Instance) {
- let dvs = DocumentManager.Instance.getDocumentViews(LinkFollowBox.Instance.props.Document);
- // if it already exisits, close it
- LinkFollowBox.Instance.props.Document.isMinimized = (dvs.length > 0 && shouldClose);
+ @computed get flyout() {
+ let sidebarContent = this.userDoc && this.userDoc.sidebarContainer;
+ if (!(sidebarContent instanceof Doc)) {
+ return (null);
}
- }
-
- @observable private _colorPickerDisplay = false;
- /* for the expandable add nodes menu. Not included with the miscbuttons because once it expands it expands the whole div with it, making canvas interactions limited. */
- nodesMenu() {
- let imgurl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg";
-
- let addColNode = action(() => Docs.Create.FreeformDocument([], { width: this.pwidth * .7, height: this.pheight, title: "a freeform collection" }));
- let addPresNode = action(() => Doc.UserDoc().curPresentation = Docs.Create.PresDocument(new List<Doc>(), { width: 200, height: 500, title: "a presentation trail" }));
- let addWebNode = action(() => Docs.Create.WebDocument("https://en.wikipedia.org/wiki/Hedgehog", { width: 300, height: 300, title: "New Webpage" }));
- let addDragboxNode = action(() => Docs.Create.DragboxDocument({ width: 40, height: 40, title: "drag collection" }));
- let addImageNode = action(() => Docs.Create.ImageDocument(imgurl, { width: 200, title: "an image of a cat" }));
- let addButtonDocument = action(() => Docs.Create.ButtonDocument({ width: 150, height: 50, title: "Button" }));
- let addImportCollectionNode = action(() => Docs.Create.DirectoryImportDocument({ title: "Directory Import", width: 400, height: 400 }));
- // let youtubeurl = "https://www.youtube.com/embed/TqcApsGRzWw";
- // let addYoutubeSearcher = action(() => Docs.Create.YoutubeDocument(youtubeurl, { width: 600, height: 600, title: "youtube search" }));
-
- let btns: [React.RefObject<HTMLDivElement>, IconName, string, () => Doc][] = [
- [React.createRef<HTMLDivElement>(), "object-group", "Add Collection", addColNode],
- [React.createRef<HTMLDivElement>(), "tv", "Add Presentation Trail", addPresNode],
- [React.createRef<HTMLDivElement>(), "globe-asia", "Add Website", addWebNode],
- [React.createRef<HTMLDivElement>(), "bolt", "Add Button", addButtonDocument],
- [React.createRef<HTMLDivElement>(), "file", "Add Document Dragger", addDragboxNode],
- [React.createRef<HTMLDivElement>(), "cloud-upload-alt", "Import Directory", addImportCollectionNode], //remove at some point in favor of addImportCollectionNode
- //[React.createRef<HTMLDivElement>(), "play", "Add Youtube Searcher", addYoutubeSearcher],
- ];
- if (!ClientUtils.RELEASE) btns.unshift([React.createRef<HTMLDivElement>(), "cat", "Add Cat Image", addImageNode]);
-
- return < div id="add-nodes-menu" style={{ left: this.flyoutWidth + 20, bottom: 20 }} >
-
- <input type="checkbox" id="add-menu-toggle" ref={this.addMenuToggle} />
- <label htmlFor="add-menu-toggle" style={{ marginTop: 2 }} title="Close Menu"><p>+</p></label>
-
- <div id="add-options-content">
- <ul id="add-options-list">
- <li key="search"><button className="add-button round-button" title="Search" onClick={this.toggleSearch}><FontAwesomeIcon icon="search" size="sm" /></button></li>
- <li key="undo"><button className="add-button round-button" title="Undo" style={{ opacity: UndoManager.CanUndo() ? 1 : 0.5, transition: "0.4s ease all" }} onClick={() => UndoManager.Undo()}><FontAwesomeIcon icon="undo-alt" size="sm" /></button></li>
- <li key="redo"><button className="add-button round-button" title="Redo" style={{ opacity: UndoManager.CanRedo() ? 1 : 0.5, transition: "0.4s ease all" }} onClick={() => UndoManager.Redo()}><FontAwesomeIcon icon="redo-alt" size="sm" /></button></li>
- {btns.map(btn =>
- <li key={btn[1]} ><div ref={btn[0]}>
- <button className="round-button add-button" title={btn[2]} onPointerDown={SetupDrag(btn[0], btn[3])}>
- <FontAwesomeIcon icon={btn[1]} size="sm" />
- </button>
- </div></li>)}
- <li key="linkFollow"><button className="add-button round-button" title="Open Link Follower" onClick={() => this.toggleLinkFollowBox(true)}><FontAwesomeIcon icon="link" size="sm" /></button></li>
- <li key="color"><button className="add-button round-button" title="Select Color" style={{ zIndex: 1000 }} onClick={() => this.toggleColorPicker()}><div className="toolbar-color-button" style={{ backgroundColor: InkingControl.Instance.selectedColor }} >
- <div className="toolbar-color-picker" onClick={this.onColorClick} style={this._colorPickerDisplay ? { color: "black", display: "block" } : { color: "black", display: "none" }}>
- <SketchPicker color={InkingControl.Instance.selectedColor} onChange={InkingControl.Instance.switchColor} />
- </div>
- </div></button></li>
- <li key="ink" style={{ paddingRight: "6px" }}><button className="toolbar-button round-button" title="Ink" onClick={() => InkingControl.Instance.toggleDisplay()}><FontAwesomeIcon icon="pen-nib" size="sm" /> </button></li>
- <li key="pen"><button onClick={() => InkingControl.Instance.switchTool(InkTool.Pen)} title="Pen" style={this.selected(InkTool.Pen)}><FontAwesomeIcon icon="pen" size="lg" /></button></li>
- <li key="marker"><button onClick={() => InkingControl.Instance.switchTool(InkTool.Highlighter)} title="Highlighter" style={this.selected(InkTool.Highlighter)}><FontAwesomeIcon icon="highlighter" size="lg" /></button></li>
- <li key="eraser"><button onClick={() => InkingControl.Instance.switchTool(InkTool.Eraser)} title="Eraser" style={this.selected(InkTool.Eraser)}><FontAwesomeIcon icon="eraser" size="lg" /></button></li>
- <li key="inkControls"><InkingControl /></li>
- <li key="logout"><button onClick={() => window.location.assign(Utils.prepend(RouteStore.logout))}>Log Out</button></li>
- </ul>
+ let sidebarButtonsDoc = Cast(CurrentUserUtils.UserDocument.sidebarButtons, Doc) as Doc;
+ sidebarButtonsDoc.columnWidth = this.flyoutWidth / 3 - 30;
+ return <div className="mainView-flyoutContainer" >
+ <div className="mainView-tabButtons" style={{ height: `${this._buttonBarHeight}px` }}>
+ <DocumentView
+ Document={sidebarButtonsDoc}
+ DataDoc={undefined}
+ addDocument={undefined}
+ addDocTab={this.addDocTabFunc}
+ pinToPres={emptyFunction}
+ removeDocument={undefined}
+ ruleProvider={undefined}
+ onClick={undefined}
+ ScreenToLocalTransform={Transform.Identity}
+ ContentScaling={returnOne}
+ PanelWidth={this.flyoutWidthFunc}
+ PanelHeight={this.getPHeight}
+ renderDepth={0}
+ focus={emptyFunction}
+ backgroundColor={returnEmptyString}
+ parentActive={returnTrue}
+ whenActiveChanged={emptyFunction}
+ bringToFront={emptyFunction}
+ ContainingCollectionView={undefined}
+ ContainingCollectionDoc={undefined}
+ zoomToScale={emptyFunction}
+ getScale={returnOne}>
+ </DocumentView>
</div>
- </div >;
- }
-
-
-
- @action
- toggleColorPicker = (close = false) => {
- this._colorPickerDisplay = close ? false : !this._colorPickerDisplay;
- }
-
- /* @TODO this should really be moved into a moveable toolbar component, but for now let's put it here to meet the deadline */
- @computed
- get miscButtons() {
- return [
- this.isSearchVisible ? <div className="main-searchDiv" key="search" style={{ top: '34px', right: '1px', position: 'absolute' }} > <FilterBox /> </div> : null,
- ];
-
- }
-
- @observable isSearchVisible = false;
- @action.bound
- toggleSearch = () => {
- this.isSearchVisible = !this.isSearchVisible;
+ <div style={{ position: "relative", height: `calc(100% - ${this._buttonBarHeight}px)`, width: "100%", overflow: "auto" }}>
+ <DocumentView
+ Document={sidebarContent}
+ DataDoc={undefined}
+ addDocument={undefined}
+ addDocTab={this.addDocTabFunc}
+ pinToPres={emptyFunction}
+ removeDocument={returnFalse}
+ ruleProvider={undefined}
+ onClick={undefined}
+ ScreenToLocalTransform={this.mainContainerXf}
+ ContentScaling={returnOne}
+ PanelWidth={this.flyoutWidthFunc}
+ PanelHeight={this.getContentsHeight}
+ renderDepth={0}
+ focus={emptyFunction}
+ backgroundColor={returnEmptyString}
+ parentActive={returnTrue}
+ whenActiveChanged={emptyFunction}
+ bringToFront={emptyFunction}
+ ContainingCollectionView={undefined}
+ ContainingCollectionDoc={undefined}
+ zoomToScale={emptyFunction}
+ getScale={returnOne}>
+ </DocumentView>
+ <button className="mainView-logout" key="logout" onClick={() => window.location.assign(Utils.prepend(RouteStore.logout))}>
+ {CurrentUserUtils.GuestWorkspace ? "Exit" : "Log Out"}
+ </button>
+ </div></div>;
+ }
+
+ @computed get mainContent() {
+ const sidebar = this.userDoc && this.userDoc.sidebarContainer;
+ return !this.userDoc || !(sidebar instanceof Doc) ? (null) : (
+ <div className="mainView-mainContent" >
+ <div className="mainView-flyoutContainer" onPointerLeave={this.pointerLeaveDragger}>
+ <div className="mainView-libraryHandle"
+ style={{ cursor: "ew-resize", left: `${(this.flyoutWidth * (this._flyoutTranslate ? 1 : 0)) - 10}px`, backgroundColor: `${StrCast(sidebar.backgroundColor, "lightGray")}` }}
+ onPointerDown={this.onPointerDown} onPointerOver={this.pointerOverDragger}>
+ <span title="library View Dragger" style={{
+ width: (this.flyoutWidth !== 0 && this._flyoutTranslate) ? "100%" : "5vw",
+ height: (this.flyoutWidth !== 0 && this._flyoutTranslate) ? "100%" : "30vh",
+ position: "absolute",
+ top: (this.flyoutWidth !== 0 && this._flyoutTranslate) ? "" : "-10vh"
+ }} />
+ </div>
+ <div className="mainView-libraryFlyout" style={{
+ width: `${this.flyoutWidth}px`,
+ zIndex: 1,
+ transformOrigin: this._flyoutTranslate ? "" : "left center",
+ transition: this._flyoutTranslate ? "" : "width .5s",
+ transform: `scale(${this._flyoutTranslate ? 1 : 0.8})`,
+ boxShadow: this._flyoutTranslate ? "" : "rgb(156, 147, 150) 0.2vw 0.2vw 0.8vw"
+ }}>
+ {this.flyout}
+ {this.expandButton}
+ </div>
+ </div>
+ {this.dockingContent}
+ </div>);
}
- private get dictationOverlay() {
- let display = this.dictationOverlayVisible;
- let success = this.dictationSuccess;
- let result = this.isListening && !this.isListening.interim ? DictationManager.placeholder : `"${this.dictatedPhrase}"`;
- return (
- <div>
- <div
- className={"dictation-prompt"}
- style={{
- opacity: display ? 1 : 0,
- background: success === undefined ? "gainsboro" : success ? "lawngreen" : "red",
- borderColor: this.isListening ? "red" : "black",
- }}
- >{result}</div>
- <div
- className={"dictation-prompt-overlay"}
- style={{
- opacity: display ? 0.4 : 0,
- backgroundColor: this.isListening ? "red" : "darkslategrey"
- }}
- />
- </div>
- );
- }
+ public static expandFlyout = action(() => {
+ MainView.Instance._flyoutTranslate = true;
+ MainView.Instance.flyoutWidth = 250;
+ });
- @computed get miniPresentation() {
- let next = () => PresBox.CurrentPresentation.next();
- let back = () => PresBox.CurrentPresentation.back();
- let startOrResetPres = () => PresBox.CurrentPresentation.startOrResetPres();
- let closePresMode = action(() => { PresBox.CurrentPresentation.presMode = false; this.addDocTabFunc(PresBox.CurrentPresentation.props.Document, undefined, "onRight"); });
- return !PresBox.CurrentPresentation || !PresBox.CurrentPresentation.presMode ? (null) : <PresModeMenu next={next} back={back} presStatus={PresBox.CurrentPresentation.presStatus} startOrResetPres={startOrResetPres} closePresMode={closePresMode} > </PresModeMenu>;
+ @computed get expandButton() {
+ return !this._flyoutTranslate ? (<div className="mainView-expandFlyoutButton" title="Re-attach sidebar" onPointerDown={MainView.expandFlyout}><FontAwesomeIcon icon="chevron-right" color="grey" size="lg" /></div>) : (null);
+ }
+
+ addButtonDoc = (doc: Doc) => Doc.AddDocToList(CurrentUserUtils.UserDocument.expandingButtons as Doc, "data", doc);
+ remButtonDoc = (doc: Doc) => Doc.RemoveDocFromList(CurrentUserUtils.UserDocument.expandingButtons as Doc, "data", doc);
+ moveButtonDoc = (doc: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => this.remButtonDoc(doc) && addDocument(doc);
+
+ buttonBarXf = () => {
+ if (!this._docBtnRef.current) return Transform.Identity();
+ let { scale, translateX, translateY } = Utils.GetScreenTransform(this._docBtnRef.current);
+ return new Transform(-translateX, -translateY, 1 / scale);
+ }
+ @computed get docButtons() {
+ if (CurrentUserUtils.UserDocument.expandingButtons instanceof Doc) {
+ return <div className="mainView-docButtons" ref={this._docBtnRef}
+ style={{ left: (this._flyoutTranslate ? this.flyoutWidth : 0) + 20, height: !CurrentUserUtils.UserDocument.expandingButtons.isExpanded ? "42px" : undefined }} >
+ <MainViewNotifs />
+ <CollectionLinearView
+ Document={CurrentUserUtils.UserDocument.expandingButtons}
+ DataDoc={undefined}
+ fieldKey={"data"}
+ annotationsKey={""}
+ select={emptyFunction}
+ chromeCollapsed={true}
+ active={returnFalse}
+ isSelected={returnFalse}
+ moveDocument={this.moveButtonDoc}
+ CollectionView={undefined}
+ addDocument={this.addButtonDoc}
+ addDocTab={this.addDocTabFunc}
+ pinToPres={emptyFunction}
+ removeDocument={this.remButtonDoc}
+ ruleProvider={undefined}
+ onClick={undefined}
+ ScreenToLocalTransform={this.buttonBarXf}
+ ContentScaling={returnOne}
+ PanelWidth={this.flyoutWidthFunc}
+ PanelHeight={this.getContentsHeight}
+ renderDepth={0}
+ focus={emptyFunction}
+ whenActiveChanged={emptyFunction}
+ ContainingCollectionView={undefined}
+ ContainingCollectionDoc={undefined} />
+ </div>;
+ }
+ return (null);
}
render() {
- return (
- <div id="main-div">
- {this.dictationOverlay}
- <DocumentDecorations />
- {this.mainContent}
- {this.miniPresentation}
- <PreviewCursor />
- <ContextMenu />
- {this.nodesMenu()}
- {this.miscButtons}
- <PDFMenu />
- <TimelineMenu/>
- <MainOverlayTextBox firstinstance={true} />
- <OverlayView />
- </div >
- );
+ return (<div id="mainView-container">
+ <DictationOverlay />
+ <SharingManager />
+ <GoogleAuthenticationManager />
+ <DocumentDecorations />
+ {this.mainContent}
+ <PreviewCursor />
+ <ContextMenu />
+ {this.docButtons}
+ <PDFMenu />
+ <OverlayView />
+ <TimelineMenu/>
+ </div >);
}
}
+Scripting.addGlobal(function freezeSidebar() { MainView.expandFlyout(); }); \ No newline at end of file
diff --git a/src/client/views/MainViewModal.scss b/src/client/views/MainViewModal.scss
new file mode 100644
index 000000000..f5a9ee76c
--- /dev/null
+++ b/src/client/views/MainViewModal.scss
@@ -0,0 +1,25 @@
+.dialogue-box {
+ position: absolute;
+ z-index: 1000;
+ text-align: center;
+ justify-content: center;
+ align-self: center;
+ align-content: center;
+ padding: 20px;
+ background: gainsboro;
+ border-radius: 10px;
+ border: 3px solid black;
+ box-shadow: #00000044 5px 5px 10px;
+ transform: translate(-50%, -50%);
+ top: 50%;
+ left: 50%;
+ transition: 0.5s all ease;
+}
+
+.overlay {
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ z-index: 999;
+ transition: 0.5s all ease;
+} \ No newline at end of file
diff --git a/src/client/views/MainViewModal.tsx b/src/client/views/MainViewModal.tsx
new file mode 100644
index 000000000..221a0260a
--- /dev/null
+++ b/src/client/views/MainViewModal.tsx
@@ -0,0 +1,44 @@
+import * as React from 'react';
+import "./MainViewModal.scss";
+
+export interface MainViewOverlayProps {
+ isDisplayed: boolean;
+ interactive: boolean;
+ contents: string | JSX.Element;
+ dialogueBoxStyle?: React.CSSProperties;
+ overlayStyle?: React.CSSProperties;
+ dialogueBoxDisplayedOpacity?: number;
+ overlayDisplayedOpacity?: number;
+}
+
+export default class MainViewModal extends React.Component<MainViewOverlayProps> {
+
+ render() {
+ let p = this.props;
+ let dialogueOpacity = p.dialogueBoxDisplayedOpacity || 1;
+ let overlayOpacity = p.overlayDisplayedOpacity || 0.4;
+ return !p.isDisplayed ? (null) : (
+ <div style={{ pointerEvents: p.isDisplayed ? p.interactive ? "all" : "none" : "none" }}>
+ <div
+ className={"dialogue-box"}
+ style={{
+ backgroundColor: "gainsboro",
+ borderColor: "black",
+ ...(p.dialogueBoxStyle || {}),
+ opacity: p.isDisplayed ? dialogueOpacity : 0
+ }}
+ >{p.contents}</div>
+ <div
+ className={"overlay"}
+ style={{
+ backgroundColor: "gainsboro",
+ ...(p.overlayStyle || {}),
+ opacity: p.isDisplayed ? overlayOpacity : 0
+ }}
+ />
+ </div>
+ );
+ }
+
+
+} \ No newline at end of file
diff --git a/src/client/views/MainViewNotifs.scss b/src/client/views/MainViewNotifs.scss
new file mode 100644
index 000000000..25ec95643
--- /dev/null
+++ b/src/client/views/MainViewNotifs.scss
@@ -0,0 +1,18 @@
+.mainNotifs-container {
+ position:absolute;
+
+ .mainNotifs-badge {
+ position: absolute;
+ top: -10px;
+ right: -10px;
+ color: white;
+ background: #f44b42;
+ font-weight: 300;
+ border-radius: 100%;
+ width: 25px;
+ height: 25px;
+ text-align: center;
+ padding-top: 4px;
+ font-size: 12px;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/MainViewNotifs.tsx b/src/client/views/MainViewNotifs.tsx
new file mode 100644
index 000000000..09fa1cb0c
--- /dev/null
+++ b/src/client/views/MainViewNotifs.tsx
@@ -0,0 +1,32 @@
+import { action, computed, observable } from 'mobx';
+import { observer } from 'mobx-react';
+import "normalize.css";
+import * as React from 'react';
+import { Doc, DocListCast, Opt } from '../../new_fields/Doc';
+import { emptyFunction } from '../../Utils';
+import { SetupDrag } from '../util/DragManager';
+import "./MainViewNotifs.scss";
+import { CollectionDockingView } from './collections/CollectionDockingView';
+
+
+@observer
+export class MainViewNotifs extends React.Component {
+
+ @observable static NotifsCol: Opt<Doc>;
+ openNotifsCol = () => {
+ if (MainViewNotifs.NotifsCol) {
+ CollectionDockingView.AddRightSplit(MainViewNotifs.NotifsCol, undefined);
+ }
+ }
+ render() {
+ const length = MainViewNotifs.NotifsCol ? DocListCast(MainViewNotifs.NotifsCol.data).length : 0;
+ const notifsRef = React.createRef<HTMLDivElement>();
+ const dragNotifs = action(() => MainViewNotifs.NotifsCol!);
+ return <div className="mainNotifs-container" ref={notifsRef}>
+ <button className="mainNotifs-badge" style={length > 0 ? { "display": "initial" } : { "display": "none" }}
+ onClick={this.openNotifsCol} onPointerDown={MainViewNotifs.NotifsCol ? SetupDrag(notifsRef, dragNotifs) : emptyFunction}>
+ {length}
+ </button>
+ </div>;
+ }
+}
diff --git a/src/client/views/MetadataEntryMenu.tsx b/src/client/views/MetadataEntryMenu.tsx
index f1b101b8e..41453f8b2 100644
--- a/src/client/views/MetadataEntryMenu.tsx
+++ b/src/client/views/MetadataEntryMenu.tsx
@@ -3,7 +3,7 @@ import "./MetadataEntryMenu.scss";
import { observer } from 'mobx-react';
import { observable, action, runInAction, trace, computed, IReactionDisposer, reaction } from 'mobx';
import { KeyValueBox } from './nodes/KeyValueBox';
-import { Doc, Field, DocListCast, DocListCastAsync } from '../../new_fields/Doc';
+import { Doc, Field, DocListCastAsync } from '../../new_fields/Doc';
import * as Autosuggest from 'react-autosuggest';
import { undoBatch } from '../util/UndoManager';
import { emptyFunction } from '../../Utils';
diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx
index 45e0a3562..9869e24d1 100644
--- a/src/client/views/OverlayView.tsx
+++ b/src/client/views/OverlayView.tsx
@@ -12,6 +12,7 @@ import { Transform } from "../util/Transform";
import { CollectionFreeFormDocumentView } from "./nodes/CollectionFreeFormDocumentView";
import { DocumentContentsView } from "./nodes/DocumentContentsView";
import { NumCast } from "../../new_fields/Types";
+import { CollectionFreeFormLinksView } from "./collections/collectionFreeForm/CollectionFreeFormLinksView";
export type OverlayDisposer = () => void;
@@ -141,7 +142,11 @@ export class OverlayView extends React.Component {
}
@computed get overlayDocs() {
+ if (!CurrentUserUtils.UserDocument) {
+ return (null);
+ }
return CurrentUserUtils.UserDocument.overlays instanceof Doc && DocListCast(CurrentUserUtils.UserDocument.overlays.data).map(d => {
+ d.inOverlay = true;
let offsetx = 0, offsety = 0;
let onPointerMove = action((e: PointerEvent) => {
if (e.buttons === 1) {
@@ -167,14 +172,14 @@ export class OverlayView extends React.Component {
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" : "" }}>
- <DocumentContentsView
+ <DocumentView
Document={d}
ChromeHeight={returnZero}
- isSelected={returnFalse}
- select={emptyFunction}
- ruleProvider={undefined}
- layoutKey={"layout"}
+ // isSelected={returnFalse}
+ // select={emptyFunction}
+ // layoutKey={"layout"}
bringToFront={emptyFunction}
+ ruleProvider={undefined}
addDocument={undefined}
removeDocument={undefined}
ContentScaling={returnOne}
@@ -202,6 +207,7 @@ export class OverlayView extends React.Component {
<div>
{this._elements}
</div>
+ <CollectionFreeFormLinksView key="freeformLinks" />
{this.overlayDocs}
</div>
);
diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx
index 1aed51e64..eed2cc5da 100644
--- a/src/client/views/PreviewCursor.tsx
+++ b/src/client/views/PreviewCursor.tsx
@@ -14,7 +14,7 @@ export class PreviewCursor extends React.Component<{}> {
static _onKeyPress?: (e: KeyboardEvent) => void;
static _getTransform: () => Transform;
static _addLiveTextDoc: (doc: Doc) => void;
- static _addDocument: (doc: Doc, allowDuplicates: false) => boolean;
+ static _addDocument: (doc: Doc) => boolean;
@observable static _clickPoint = [0, 0];
@observable public static Visible = false;
//when focus is lost, this will remove the preview cursor
@@ -44,7 +44,7 @@ export class PreviewCursor extends React.Component<{}> {
title: url, width: 400, height: 315,
nativeWidth: 600, nativeHeight: 472.5,
x: newPoint[0], y: newPoint[1]
- }), false);
+ }));
return;
}
@@ -56,7 +56,7 @@ export class PreviewCursor extends React.Component<{}> {
title: url, width: 300, height: 300,
// nativeWidth: 300, nativeHeight: 472.5,
x: newPoint[0], y: newPoint[1]
- }), false);
+ }));
return;
}
@@ -79,11 +79,11 @@ export class PreviewCursor extends React.Component<{}> {
let img: Doc = Docs.Create.ImageDocument(
arr[1], {
- width: 300, title: arr[1],
- x: newPoint[0],
- y: newPoint[1],
- });
- PreviewCursor._addDocument(img, false);
+ width: 300, title: arr[1],
+ x: newPoint[0],
+ y: newPoint[1],
+ });
+ PreviewCursor._addDocument(img);
return;
}
@@ -113,7 +113,7 @@ export class PreviewCursor extends React.Component<{}> {
onKeyPress: (e: KeyboardEvent) => void,
addLiveText: (doc: Doc) => void,
getTransform: () => Transform,
- addDocument: (doc: Doc, allowDuplicates: false) => boolean) {
+ addDocument: (doc: Doc) => boolean) {
this._clickPoint = [x, y];
this._onKeyPress = onKeyPress;
this._addLiveTextDoc = addLiveText;
diff --git a/src/client/views/ScriptingRepl.scss b/src/client/views/ScriptingRepl.scss
index f1ef64193..778e9c445 100644
--- a/src/client/views/ScriptingRepl.scss
+++ b/src/client/views/ScriptingRepl.scss
@@ -21,6 +21,7 @@
.scriptingRepl-commandsContainer {
flex: 1 1 auto;
overflow-y: scroll;
+ height: 30px;
}
.documentIcon-outerDiv {
diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx
index 9e5e62e03..96265385e 100644
--- a/src/client/views/TemplateMenu.tsx
+++ b/src/client/views/TemplateMenu.tsx
@@ -9,6 +9,7 @@ import { DocumentView } from "./nodes/DocumentView";
import { Template, Templates } from "./Templates";
import React = require("react");
import { Doc } from "../../new_fields/Doc";
+import { StrCast } from "../../new_fields/Types";
const higflyout = require("@hig/flyout");
export const { anchorPoints } = higflyout;
export const Flyout = higflyout.default;
@@ -100,7 +101,7 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
clearTemplates = (event: React.MouseEvent) => {
Templates.TemplateList.forEach(template => this.props.docs.forEach(d => d.Document["show" + template.Name] = undefined));
["backgroundColor", "borderRounding", "width", "height"].forEach(field => this.props.docs.forEach(d => {
- if (d.Document.isTemplate && d.props.DataDoc) {
+ if (d.Document.isTemplateDoc && d.props.DataDoc) {
d.Document[field] = undefined;
} else if (d.Document["default" + field[0].toUpperCase() + field.slice(1)] !== undefined) {
d.Document[field] = Doc.GetProto(d.Document)[field] = undefined;
@@ -117,18 +118,18 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
@action
toggleChrome = (): void => {
this.props.docs.map(dv => {
- let layout = dv.Document.layout instanceof Doc ? dv.Document.layout : dv.Document;
+ let layout = Doc.Layout(dv.Document);
layout.chromeStatus = (layout.chromeStatus !== "disabled" ? "disabled" : "enabled");
});
}
render() {
- let layout = this.props.docs[0].Document.layout instanceof Doc ? this.props.docs[0].Document.layout : this.props.docs[0].Document;
+ let layout = Doc.Layout(this.props.docs[0].Document);
let 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.docs[0].Document.z ? true : false} toggle={this.toggleFloat} />);
- templateMenu.push(<OtherToggle key={"custom"} name={"Custom"} checked={typeof this.props.docs[0].Document.layout === "string" ? false : true} toggle={this.toggleCustom} />);
+ templateMenu.push(<OtherToggle key={"custom"} name={"Custom"} checked={StrCast(this.props.docs[0].Document.layoutKey, "layout") !== "layout"} toggle={this.toggleCustom} />);
templateMenu.push(<OtherToggle key={"chrome"} name={"Chrome"} checked={layout.chromeStatus !== "disabled"} toggle={this.toggleChrome} />);
return (
<div className="templating-menu" >
diff --git a/src/client/views/collections/CollectionBaseView.scss b/src/client/views/collections/CollectionBaseView.scss
deleted file mode 100644
index 583e6f6ca..000000000
--- a/src/client/views/collections/CollectionBaseView.scss
+++ /dev/null
@@ -1,12 +0,0 @@
-@import "../globalCssVariables";
-#collectionBaseView {
- border-width: 0;
- border-color: $light-color-secondary;
- border-style: solid;
- border-radius: 0 0 $border-radius $border-radius;
- box-sizing: border-box;
- border-radius: inherit;
- width:100%;
- height:100%;
- overflow: auto;
-} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx
deleted file mode 100644
index 0168c466f..000000000
--- a/src/client/views/collections/CollectionBaseView.tsx
+++ /dev/null
@@ -1,179 +0,0 @@
-import { action, computed, observable } from 'mobx';
-import { observer } from 'mobx-react';
-import * as React from 'react';
-import { Doc } from '../../../new_fields/Doc';
-import { Id } from '../../../new_fields/FieldSymbols';
-import { List } from '../../../new_fields/List';
-import { listSpec } from '../../../new_fields/Schema';
-import { BoolCast, Cast, NumCast, PromiseValue, StrCast } from '../../../new_fields/Types';
-import { DocumentManager } from '../../util/DocumentManager';
-import { SelectionManager } from '../../util/SelectionManager';
-import { ContextMenu } from '../ContextMenu';
-import { FieldViewProps } from '../nodes/FieldView';
-import './CollectionBaseView.scss';
-import { DateField } from '../../../new_fields/DateField';
-
-export enum CollectionViewType {
- Invalid,
- Freeform,
- Schema,
- Docking,
- Tree,
- Stacking,
- Masonry,
- Pivot,
-}
-
-export namespace CollectionViewType {
-
- const stringMapping = new Map<string, CollectionViewType>([
- ["invalid", CollectionViewType.Invalid],
- ["freeform", CollectionViewType.Freeform],
- ["schema", CollectionViewType.Schema],
- ["docking", CollectionViewType.Docking],
- ["tree", CollectionViewType.Tree],
- ["stacking", CollectionViewType.Stacking],
- ["masonry", CollectionViewType.Masonry],
- ["pivot", CollectionViewType.Pivot]
- ]);
-
- export const valueOf = (value: string) => {
- return stringMapping.get(value.toLowerCase());
- };
-
-}
-
-export interface CollectionRenderProps {
- addDocument: (document: Doc, allowDuplicates?: boolean) => boolean;
- removeDocument: (document: Doc) => boolean;
- moveDocument: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
- active: () => boolean;
- whenActiveChanged: (isActive: boolean) => void;
-}
-
-export interface CollectionViewProps extends FieldViewProps {
- onContextMenu?: (e: React.MouseEvent) => void;
- children: (type: CollectionViewType, props: CollectionRenderProps) => JSX.Element | JSX.Element[] | null | (JSX.Element | null)[];
- className?: string;
- contentRef?: React.Ref<HTMLDivElement>;
-}
-
-@observer
-export class CollectionBaseView extends React.Component<CollectionViewProps> {
- @observable private static _safeMode = false;
- static InSafeMode() { return this._safeMode; }
- static SetSafeMode(safeMode: boolean) { this._safeMode = safeMode; }
- get collectionViewType(): CollectionViewType | undefined {
- let Document = this.props.Document;
- let viewField = Cast(Document.viewType, "number");
- if (CollectionBaseView._safeMode) {
- if (viewField === CollectionViewType.Freeform) {
- return CollectionViewType.Tree;
- }
- if (viewField === CollectionViewType.Invalid) {
- return CollectionViewType.Freeform;
- }
- }
- if (viewField !== undefined) {
- return viewField;
- } else {
- return CollectionViewType.Invalid;
- }
- }
-
- @computed get dataDoc() { return Doc.fieldExtensionDoc(this.props.Document.isTemplate && this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, this.props.fieldExt); }
- @computed get dataField() { return this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey; }
-
- active = (): boolean => {
- var isSelected = this.props.isSelected();
- return isSelected || BoolCast(this.props.Document.forceActive) || this._isChildActive || this.props.renderDepth === 0;
- }
-
- //TODO should this be observable?
- private _isChildActive = false;
- whenActiveChanged = (isActive: boolean) => {
- this._isChildActive = isActive;
- this.props.whenActiveChanged(isActive);
- }
-
- @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, this.props.fieldExt); }
-
- @action.bound
- addDocument(doc: Doc, allowDuplicates: boolean = false): boolean {
- var curPage = NumCast(this.props.Document.curPage, -1);
- Doc.GetProto(doc).page = curPage;
- if (this.props.fieldExt) { // bcz: fieldExt !== undefined means this is an overlay layer
- Doc.GetProto(doc).annotationOn = this.props.Document;
- }
- let targetDataDoc = this.props.fieldExt || this.props.Document.isTemplate ? this.extensionDoc : this.props.Document;
- let targetField = (this.props.fieldExt || this.props.Document.isTemplate) && this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey;
- const value = Cast(targetDataDoc[targetField], listSpec(Doc));
- if (value !== undefined) {
- if (allowDuplicates || !value.some(v => v instanceof Doc && v[Id] === doc[Id])) {
- value.push(doc);
- }
- } else {
- Doc.GetProto(targetDataDoc)[targetField] = new List([doc]);
- }
- Doc.GetProto(doc).lastOpened = new DateField;
- return true;
- }
-
- @action.bound
- removeDocument(doc: Doc): boolean {
- let docView = DocumentManager.Instance.getDocumentView(doc, this.props.ContainingCollectionView);
- docView && SelectionManager.DeselectDoc(docView);
- //TODO This won't create the field if it doesn't already exist
- let targetDataDoc = this.props.fieldExt || this.props.Document.isTemplate ? this.extensionDoc : this.props.Document;
- let targetField = (this.props.fieldExt || this.props.Document.isTemplate) && this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey;
- let value = Cast(targetDataDoc[targetField], listSpec(Doc), []);
- 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);
- PromiseValue(Cast(doc.annotationOn, Doc)).then(annotationOn =>
- annotationOn === this.dataDoc.Document && (doc.annotationOn = undefined));
-
- if (index !== -1) {
- value.splice(index, 1);
-
- // SelectionManager.DeselectAll()
- ContextMenu.Instance.clearItems();
- return true;
- }
- return false;
- }
-
- // this is called with the document that was dragged and the collection to move it into.
- // if the target collection is the same as this collection, then the move will be allowed.
- // otherwise, the document being moved must be able to be removed from its container before
- // moving it into the target.
- @action.bound
- moveDocument(doc: Doc, targetCollection: Doc, addDocument: (doc: Doc) => boolean): boolean {
- if (Doc.AreProtosEqual(this.props.Document, targetCollection)) {
- return true;
- }
- return this.removeDocument(doc) ? addDocument(doc) : false;
- }
-
- render() {
- const props: CollectionRenderProps = {
- addDocument: this.addDocument,
- removeDocument: this.removeDocument,
- moveDocument: this.moveDocument,
- active: this.active,
- whenActiveChanged: this.whenActiveChanged,
- };
- const viewtype = this.collectionViewType;
- return (
- <div id="collectionBaseView"
- style={{
- pointerEvents: this.props.Document.isBackground ? "none" : "all",
- boxShadow: this.props.Document.isBackground ? undefined : `#9c9396 ${StrCast(this.props.Document.boxShadow, "0.2vw 0.2vw 0.8vw")}`
- }}
- className={this.props.className || "collectionView-cont"}
- onContextMenu={this.props.onContextMenu} ref={this.props.contentRef}>
- {viewtype !== undefined ? this.props.children(viewtype, props) : (null)}
- </div>
- );
- }
-
-}
diff --git a/src/client/views/collections/CollectionDockingView.scss b/src/client/views/collections/CollectionDockingView.scss
index 6f5abd05b..12f54d69d 100644
--- a/src/client/views/collections/CollectionDockingView.scss
+++ b/src/client/views/collections/CollectionDockingView.scss
@@ -25,6 +25,9 @@
top: 0;
left: 0;
overflow: hidden;
+ .lm_content {
+ background: white;
+ }
.lm_controls>li {
opacity: 0.6;
transform: scale(1.2);
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index b047e77a8..42d372f4a 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -3,13 +3,13 @@ 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, computed, Lambda, observable, reaction } from "mobx";
+import { action, Lambda, observable, reaction, computed, 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, WidthSym, HeightSym } from "../../../new_fields/Doc";
+import { Doc, DocListCast, Field, Opt } from "../../../new_fields/Doc";
import { Id } from '../../../new_fields/FieldSymbols';
import { List } from '../../../new_fields/List';
import { FieldId } from "../../../new_fields/RefField";
@@ -31,12 +31,14 @@ import { SubCollectionViewProps } from "./CollectionSubView";
import React = require("react");
import { ButtonSelector } from './ParentDocumentSelector';
import { DocumentType } from '../../documents/DocumentTypes';
+import { ComputedField } from '../../../new_fields/ScriptField';
library.add(faFile);
const _global = (window /* browser */ || global /* node */) as any;
@observer
export class CollectionDockingView extends React.Component<SubCollectionViewProps> {
- public static Instance: CollectionDockingView;
+ @observable public static Instances: CollectionDockingView[] = [];
+ @computed public static get Instance() { return CollectionDockingView.Instances[0]; }
public static makeDocumentConfig(document: Doc, dataDoc: Doc | undefined, width?: number) {
return {
type: 'react-component',
@@ -51,7 +53,11 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
};
}
- private _goldenLayout: any = null;
+ @computed public get initialized() {
+ return this._goldenLayout !== null;
+ }
+
+ @observable private _goldenLayout: any = null;
private _containerRef = React.createRef<HTMLDivElement>();
private _flush: boolean = false;
private _ignoreStateChange = "";
@@ -60,7 +66,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
constructor(props: SubCollectionViewProps) {
super(props);
- !CollectionDockingView.Instance && (CollectionDockingView.Instance = this);
+ runInAction(() => !CollectionDockingView.Instances ? CollectionDockingView.Instances = [this] : CollectionDockingView.Instances.push(this));
//Why is this here?
(window as any).React = React;
(window as any).ReactDOM = ReactDOM;
@@ -251,7 +257,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
var config = StrCast(this.props.Document.dockingConfig);
if (config) {
if (!this._goldenLayout) {
- this._goldenLayout = new GoldenLayout(JSON.parse(config));
+ runInAction(() => this._goldenLayout = new GoldenLayout(JSON.parse(config)));
}
else {
if (config === JSON.stringify(this._goldenLayout.toConfig())) {
@@ -264,7 +270,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
this._goldenLayout.unbind('stackCreated', this.stackCreated);
} catch (e) { }
this._goldenLayout.destroy();
- this._goldenLayout = new GoldenLayout(JSON.parse(config));
+ runInAction(() => this._goldenLayout = new GoldenLayout(JSON.parse(config)));
}
this._goldenLayout.on('itemDropped', this.itemDropped);
this._goldenLayout.on('tabCreated', this.tabCreated);
@@ -292,7 +298,8 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
// Because this is in a set timeout, if this component unmounts right after mounting,
// we will leak a GoldenLayout, because we try to destroy it before we ever create it
setTimeout(() => this.setupGoldenLayout(), 1);
- DocListCast((CurrentUserUtils.UserDocument.workspaces as Doc).data).map(d => d.workspaceBrush = false);
+ let userDoc = CurrentUserUtils.UserDocument;
+ userDoc && DocListCast((userDoc.workspaces as Doc).data).map(d => d.workspaceBrush = false);
this.props.Document.workspaceBrush = true;
}
this._ignoreStateChange = "";
@@ -311,13 +318,14 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
} catch (e) {
}
- if (this._goldenLayout) this._goldenLayout.destroy();
- this._goldenLayout = null;
+ this._goldenLayout && this._goldenLayout.destroy();
+ runInAction(() => {
+ CollectionDockingView.Instances.splice(CollectionDockingView.Instances.indexOf(this), 1);
+ this._goldenLayout = null;
+ });
window.removeEventListener('resize', this.onResize);
- if (this.reactionDisposer) {
- this.reactionDisposer();
- }
+ this.reactionDisposer && this.reactionDisposer();
}
@action
onResize = (event: any) => {
@@ -453,8 +461,9 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
let theDoc = doc;
CollectionDockingView.Instance._removedDocs.push(theDoc);
- const recent = await Cast(CurrentUserUtils.UserDocument.recentlyClosed, Doc);
- if (recent) {
+ let userDoc = CurrentUserUtils.UserDocument;
+ let recent: Doc | undefined;
+ if (userDoc && (recent = await Cast(CurrentUserUtils.UserDocument.recentlyClosed, Doc))) {
Doc.AddDocToList(recent, "data", doc, undefined, true, true);
}
SelectionManager.DeselectAll();
@@ -486,12 +495,13 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
.off('click') //unbind the current click handler
.click(action(async function () {
//if (confirm('really close this?')) {
- const recent = await Cast(CurrentUserUtils.UserDocument.recentlyClosed, Doc);
+
stack.remove();
stack.contentItems.forEach(async (contentItem: any) => {
let doc = await DocServer.GetRefField(contentItem.config.props.documentId);
if (doc instanceof Doc) {
- if (recent) {
+ let recent: Doc | undefined;
+ if (CurrentUserUtils.UserDocument && (recent = await Cast(CurrentUserUtils.UserDocument.recentlyClosed, Doc))) {
Doc.AddDocToList(recent, "data", doc, undefined, true, true);
}
let theDoc = doc;
@@ -564,11 +574,14 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
//add this new doc to props.Document
let curPres = Cast(CurrentUserUtils.UserDocument.curPresentation, Doc) as Doc;
if (curPres) {
+ let 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(doc);
+ data.push(pinDoc);
} else {
- curPres.data = new List([doc]);
+ curPres.data = new List([pinDoc]);
}
if (!DocumentManager.Instance.getDocumentView(curPres)) {
this.addDocTab(curPres, undefined, "onRight");
@@ -602,19 +615,20 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
}
}
- panelWidth = () => this._document!.ignoreAspect || this._document!.fitWidth ? this._panelWidth : Math.min(this._panelWidth, Math.max(NumCast(this._document!.width), this.nativeWidth()));
- panelHeight = () => this._document!.ignoreAspect || this._document!.fitWidth ? this._panelHeight : Math.min(this._panelHeight, Math.max(NumCast(this._document!.height), this.nativeHeight()));
+ get layoutDoc() { return this._document && Doc.Layout(this._document);}
+ 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._document!.ignoreAspect && !this._document!.fitWidth ? NumCast(this._document!.nativeWidth) || this._panelWidth : 0;
- nativeHeight = () => !this._document!.ignoreAspect && !this._document!.fitWidth ? NumCast(this._document!.nativeHeight) || this._panelHeight : 0;
+ 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;
contentScaling = () => {
- if (this._document!.type === DocumentType.PDF) {
- if ((this._document && this._document.fitWidth) ||
- this._panelHeight / NumCast(this._document!.nativeHeight) > this._panelWidth / NumCast(this._document!.nativeWidth)) {
- return this._panelWidth / NumCast(this._document!.nativeWidth);
+ if (this.layoutDoc!.type === DocumentType.PDF) {
+ if ((this.layoutDoc && this.layoutDoc.fitWidth) ||
+ this._panelHeight / NumCast(this.layoutDoc!.nativeHeight) > this._panelWidth / NumCast(this.layoutDoc!.nativeWidth)) {
+ return this._panelWidth / NumCast(this.layoutDoc!.nativeWidth);
} else {
- return this._panelHeight / NumCast(this._document!.nativeHeight);
+ return this._panelHeight / NumCast(this.layoutDoc!.nativeHeight);
}
}
const nativeH = this.nativeHeight();
@@ -632,7 +646,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
}
return Transform.Identity();
}
- get previewPanelCenteringOffset() { return this.nativeWidth() && !BoolCast(this._document!.ignoreAspect) ? (this._panelWidth - this.nativeWidth() / this.ScreenToLocalTransform().Scale) / 2 : 0; }
+ get previewPanelCenteringOffset() { return this.nativeWidth() && !this.layoutDoc!.ignoreAspect ? (this._panelWidth - this.nativeWidth() * this.contentScaling()) / 2 : 0; }
addDocTab = (doc: Doc, dataDoc: Opt<Doc>, location: string) => {
SelectionManager.DeselectAll();
@@ -647,7 +661,10 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
return CollectionDockingView.Instance.AddTab(this._stack, doc, dataDoc);
}
}
- docView(document: Doc) {
+
+ @computed get docView() {
+ if (!this._document) return (null);
+ const document = this._document;
let resolvedDataDoc = document.layout instanceof Doc ? document : this._dataDoc;
return <DocumentView key={document[Id]}
Document={document}
@@ -674,13 +691,13 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
}
render() {
- return (!this._isActive || !this._document) ? (null) :
+ return (!this._isActive || !this.layoutDoc) ? (null) :
(<div className="collectionDockingView-content" ref={ref => this._mainCont = ref}
style={{
transform: `translate(${this.previewPanelCenteringOffset}px, 0px)`,
- height: this._document && this._document.fitWidth ? undefined : "100%"
+ height: this.layoutDoc && this.layoutDoc.fitWidth ? undefined : "100%"
}}>
- {this.docView(this._document)}
+ {this.docView}
</div >);
}
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
new file mode 100644
index 000000000..52ebfafd3
--- /dev/null
+++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
@@ -0,0 +1,367 @@
+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 } 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 { numberRange } from "../../../Utils";
+import { Docs } from "../../documents/Documents";
+import { DragManager } from "../../util/DragManager";
+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";
+import { undo } from "prosemirror-history";
+
+library.add(faPalette);
+
+interface CMVFieldRowProps {
+ rows: () => number;
+ headings: () => object[];
+ heading: string;
+ headingObject: SchemaHeaderField | undefined;
+ docList: Doc[];
+ parent: CollectionStackingView;
+ type: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" | undefined;
+ createDropTarget: (ele: HTMLDivElement) => void;
+ screenToLocalTransform: () => Transform;
+ setDocHeight: (key: string, thisHeight: number) => void;
+}
+
+@observer
+export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowProps> {
+ @observable private _background = "inherit";
+ @observable private _createAliasSelected: boolean = false;
+ @observable private _collapsed: boolean = false;
+ @observable private _headingsHack: number = 1;
+ @observable private _heading = this.props.headingObject ? this.props.headingObject.heading : this.props.heading;
+ @observable private _color = this.props.headingObject ? this.props.headingObject.color : "#f1efeb";
+
+ private _dropDisposer?: DragManager.DragDropDisposer;
+ private _headerRef: React.RefObject<HTMLDivElement> = React.createRef();
+ private _startDragPosition: { x: number, y: number } = { x: 0, y: 0 };
+ private _contRef: React.RefObject<HTMLDivElement> = React.createRef();
+ private _sensitivity: number = 16;
+ private _counter: number = 0;
+
+
+ createRowDropRef = (ele: HTMLDivElement | null) => {
+ this._dropDisposer && this._dropDisposer();
+ if (ele) {
+ this._dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.rowDrop.bind(this) } });
+ }
+ }
+
+ getTrueHeight = () => {
+ if (this._collapsed) {
+ this.props.setDocHeight(this._heading, 20);
+ } else {
+ let rawHeight = this._contRef.current!.getBoundingClientRect().height + 15; //+ 15 accounts for the group header
+ let transformScale = this.props.screenToLocalTransform().Scale;
+ let trueHeight = rawHeight * transformScale;
+ this.props.setDocHeight(this._heading, trueHeight);
+ }
+ }
+
+ @undoBatch
+ rowDrop = action((e: Event, de: DragManager.DropEvent) => {
+ this._createAliasSelected = false;
+ if (de.data instanceof DragManager.DocumentDragData) {
+ (this.props.parent.Document.dropConverter instanceof ScriptField) &&
+ this.props.parent.Document.dropConverter.script.run({ dragData: de.data });
+ let key = StrCast(this.props.parent.props.Document.sectionFilter);
+ let castedValue = this.getValue(this._heading);
+ de.data.droppedDocuments.forEach(d => d[key] = castedValue);
+ this.props.parent.drop(e, de);
+ e.stopPropagation();
+ }
+ });
+
+ getValue = (value: string): any => {
+ let parsed = parseInt(value);
+ if (!isNaN(parsed)) return parsed;
+ if (value.toLowerCase().indexOf("true") > -1) return true;
+ if (value.toLowerCase().indexOf("false") > -1) return false;
+ return value;
+ }
+
+ @action
+ headingChanged = (value: string, shiftDown?: boolean) => {
+ this._createAliasSelected = false;
+ let key = StrCast(this.props.parent.props.Document.sectionFilter);
+ let castedValue = this.getValue(value);
+ if (castedValue) {
+ if (this.props.parent.sectionHeaders) {
+ if (this.props.parent.sectionHeaders.map(i => i.heading).indexOf(castedValue.toString()) > -1) {
+ return false;
+ }
+ }
+ this.props.docList.forEach(d => d[key] = castedValue);
+ if (this.props.headingObject) {
+ this.props.headingObject.setHeading(castedValue.toString());
+ this._heading = this.props.headingObject.heading;
+ }
+ return true;
+ }
+ return false;
+ }
+
+ @action
+ changeColumnColor = (color: string) => {
+ this._createAliasSelected = false;
+ if (this.props.headingObject) {
+ this.props.headingObject.setColor(color);
+ this._color = color;
+ }
+ }
+
+ pointerEnteredRow = action(() => SelectionManager.GetIsDragging() && (this._background = "#b4b4b4"));
+
+ @action
+ pointerLeaveRow = () => {
+ this._createAliasSelected = false;
+ this._background = "inherit";
+ document.removeEventListener("pointermove", this.startDrag);
+ }
+
+ @action
+ addDocument = (value: string, shiftDown?: boolean) => {
+ this._createAliasSelected = false;
+ let key = StrCast(this.props.parent.props.Document.sectionFilter);
+ let newDoc = Docs.Create.TextDocument({ height: 18, width: 200, title: value });
+ newDoc[key] = this.getValue(this.props.heading);
+ return this.props.parent.props.addDocument(newDoc);
+ }
+
+ deleteRow = undoBatch(action(() => {
+ this._createAliasSelected = false;
+ let key = StrCast(this.props.parent.props.Document.sectionFilter);
+ this.props.docList.forEach(d => d[key] = undefined);
+ if (this.props.parent.sectionHeaders && this.props.headingObject) {
+ let index = this.props.parent.sectionHeaders.indexOf(this.props.headingObject);
+ this.props.parent.sectionHeaders.splice(index, 1);
+ }
+ }));
+
+ @action
+ collapseSection = () => {
+ this._createAliasSelected = false;
+ if (this.props.headingObject) {
+ this._headingsHack++;
+ this.props.headingObject.setCollapsed(!this.props.headingObject.collapsed);
+ this.toggleVisibility();
+ }
+ }
+
+ startDrag = (e: PointerEvent) => {
+ let [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) {
+ let alias = Doc.MakeAlias(this.props.parent.props.Document);
+ let key = StrCast(this.props.parent.props.Document.sectionFilter);
+ let value = this.getValue(this._heading);
+ value = typeof value === "string" ? `"${value}"` : value;
+ let script = `return doc.${key} === ${value}`;
+ let compiled = CompileScript(script, { params: { doc: Doc.name } });
+ if (compiled.compiled) {
+ let scriptField = new ScriptField(compiled);
+ alias.viewSpecScript = scriptField;
+ let dragData = new DragManager.DocumentDragData([alias]);
+ DragManager.StartDocumentDrag([this._headerRef.current!], dragData, 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);
+ }
+
+ @action
+ headerDown = (e: React.PointerEvent<HTMLDivElement>) => {
+ e.stopPropagation();
+ e.preventDefault();
+
+ let [dx, dy] = this.props.screenToLocalTransform().transformDirection(e.clientX, e.clientY);
+ this._startDragPosition = { x: dx, y: dy };
+
+ if (this._createAliasSelected) {
+ document.removeEventListener("pointermove", this.startDrag);
+ document.addEventListener("pointermove", this.startDrag);
+ document.removeEventListener("pointerup", this.pointerUp);
+ document.addEventListener("pointerup", this.pointerUp);
+ }
+ this._createAliasSelected = false;
+ }
+
+ renderColorPicker = () => {
+ let selected = this.props.headingObject ? this.props.headingObject.color : "#f1efeb";
+
+ let pink = PastelSchemaPalette.get("pink2");
+ let purple = PastelSchemaPalette.get("purple4");
+ let blue = PastelSchemaPalette.get("bluegreen1");
+ let yellow = PastelSchemaPalette.get("yellow4");
+ let red = PastelSchemaPalette.get("red2");
+ let green = PastelSchemaPalette.get("bluegreen7");
+ let cyan = PastelSchemaPalette.get("bluegreen5");
+ let orange = PastelSchemaPalette.get("orange1");
+ let gray = "#f1efeb";
+
+ return (
+ <div className="collectionStackingView-colorPicker">
+ <div className="colorOptions">
+ <div className={"colorPicker" + (selected === pink ? " active" : "")} style={{ backgroundColor: pink }} onClick={() => this.changeColumnColor(pink!)}></div>
+ <div className={"colorPicker" + (selected === purple ? " active" : "")} style={{ backgroundColor: purple }} onClick={() => this.changeColumnColor(purple!)}></div>
+ <div className={"colorPicker" + (selected === blue ? " active" : "")} style={{ backgroundColor: blue }} onClick={() => this.changeColumnColor(blue!)}></div>
+ <div className={"colorPicker" + (selected === yellow ? " active" : "")} style={{ backgroundColor: yellow }} onClick={() => this.changeColumnColor(yellow!)}></div>
+ <div className={"colorPicker" + (selected === red ? " active" : "")} style={{ backgroundColor: red }} onClick={() => this.changeColumnColor(red!)}></div>
+ <div className={"colorPicker" + (selected === gray ? " active" : "")} style={{ backgroundColor: gray }} onClick={() => this.changeColumnColor(gray)}></div>
+ <div className={"colorPicker" + (selected === green ? " active" : "")} style={{ backgroundColor: green }} onClick={() => this.changeColumnColor(green!)}></div>
+ <div className={"colorPicker" + (selected === cyan ? " active" : "")} style={{ backgroundColor: cyan }} onClick={() => this.changeColumnColor(cyan!)}></div>
+ <div className={"colorPicker" + (selected === orange ? " active" : "")} style={{ backgroundColor: orange }} onClick={() => this.changeColumnColor(orange!)}></div>
+ </div>
+ </div>
+ );
+ }
+
+ toggleAlias = action(() => this._createAliasSelected = true);
+ toggleVisibility = action(() => this._collapsed = !this._collapsed);
+
+ renderMenu = () => {
+ let selected = this._createAliasSelected;
+ return (<div className="collectionStackingView-optionPicker">
+ <div className="optionOptions">
+ <div className={"optionPicker" + (selected === true ? " active" : "")} onClick={this.toggleAlias}>Create Alias</div>
+ <div className={"optionPicker" + (selected === true ? " active" : "")} onClick={this.deleteRow}>Delete</div>
+ </div>
+ </div>);
+ }
+
+ handleResize = (size: any) => {
+ if (++this._counter !== 1) {
+ this.getTrueHeight();
+ }
+ }
+
+
+ render() {
+ let 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))));
+ let key = StrCast(this.props.parent.props.Document.sectionFilter);
+ let heading = this._heading;
+ let style = this.props.parent;
+ let evContents = heading ? heading : this.props.type && this.props.type === "number" ? "0" : `NO ${key.toUpperCase()} VALUE`;
+ let headerEditableViewProps = {
+ GetValue: () => evContents,
+ SetValue: this.headingChanged,
+ contents: evContents,
+ oneLine: true,
+ HeadingObject: this.props.headingObject,
+ HeadingsHack: this._headingsHack,
+ toggle: this.toggleVisibility,
+ color: this._color
+ };
+ let newEditableViewProps = {
+ GetValue: () => "",
+ SetValue: this.addDocument,
+ contents: "+ NEW",
+ HeadingObject: this.props.headingObject,
+ HeadingsHack: this._headingsHack,
+ toggle: this.toggleVisibility,
+ color: this._color
+ };
+ let headingView = this.props.parent.props.Document.miniHeaders ?
+ <div className="collectionStackingView-miniHeader" style={{ width: "100%" }}>
+ {<EditableView {...headerEditableViewProps} />}
+ </div> :
+ this.props.headingObject ?
+ <div className="collectionStackingView-sectionHeader" ref={this._headerRef} >
+ <div className="collectionStackingView-sectionHeader-subCont" onPointerDown={this.headerDown}
+ title={evContents === `NO ${key.toUpperCase()} VALUE` ?
+ `Documents that don't have a ${key} value will go here. This column cannot be removed.` : ""}
+ style={{
+ width: "100%",
+ background: evContents !== `NO ${key.toUpperCase()} VALUE` ? this._color : "lightgrey",
+ color: "grey"
+ }}>
+ {<EditableView {...headerEditableViewProps} />}
+ {evContents === `NO ${key.toUpperCase()} VALUE` ? (null) :
+ <div className="collectionStackingView-sectionColor">
+ <Flyout anchorPoint={anchorPoints.CENTER_RIGHT} content={this.renderColorPicker()}>
+ <button className="collectionStackingView-sectionColorButton">
+ <FontAwesomeIcon icon="palette" size="lg" />
+ </button>
+ </ Flyout >
+ </div>
+ }
+ <button className="collectionStackingView-sectionDelete" onClick={this.collapseSection}>
+ <FontAwesomeIcon icon={this._collapsed ? "chevron-down" : "chevron-up"} size="lg" />
+ </button>
+ {evContents === `NO ${key.toUpperCase()} VALUE` ? (null) :
+ <div className="collectionStackingView-sectionOptions">
+ <Flyout anchorPoint={anchorPoints.TOP_RIGHT} content={this.renderMenu()}>
+ <button className="collectionStackingView-sectionOptionButton">
+ <FontAwesomeIcon icon="ellipsis-v" size="lg" />
+ </button>
+ </Flyout>
+ </div>
+ }
+ </div>
+ </div > : (null);
+ const background = this._background; //to account for observables in Measure
+ const collapsed = this._collapsed;
+ let chromeStatus = this.props.parent.props.Document.chromeStatus;
+ return (
+ <Measure offset onResize={this.handleResize}>
+ {({ measureRef }) => {
+ return <div ref={measureRef}>
+ <div className="collectionStackingView-masonrySection"
+ key={heading = "empty"}
+ style={{ width: this.props.parent.NodeWidth, background }}
+ ref={this.createRowDropRef}
+ onPointerEnter={this.pointerEnteredRow}
+ onPointerLeave={this.pointerLeaveRow}
+ >
+ {headingView}
+ {collapsed ? (null) :
+ < div style={{ position: "relative" }}>
+ <div key={`${heading}-stack`} className={`collectionStackingView-masonryGrid`}
+ ref={this._contRef}
+ style={{
+ padding: `${this.props.parent.yMargin}px ${this.props.parent.xMargin}px`,
+ width: this.props.parent.NodeWidth,
+ gridGap: this.props.parent.gridGap,
+ gridTemplateColumns: numberRange(rows).reduce((list: string, i: any) => list + ` ${this.props.parent.columnWidth}px`, ""),
+ }}>
+ {this.props.parent.children(this.props.docList)}
+ {this.props.parent.columnDragger}
+ </div>
+ {(chromeStatus !== 'view-mode' && chromeStatus !== 'disabled') ?
+ <div key={`${heading}-add-document`} className="collectionStackingView-addDocumentButton"
+ style={{ width: style.columnWidth / style.numGroupColumns }}>
+ <EditableView {...newEditableViewProps} />
+ </div> : null
+ }
+ </div>
+ }
+ </div >
+ </div>;
+ }}
+ </Measure>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionPDFView.scss b/src/client/views/collections/CollectionPDFView.scss
deleted file mode 100644
index 62ec8a5be..000000000
--- a/src/client/views/collections/CollectionPDFView.scss
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-.collectionPdfView-cont {
- width: 100%;
- height: 100%;
- position: absolute;
- top: 0;
- left: 0;
- z-index: -1;
- overflow: hidden !important;
-}
diff --git a/src/client/views/collections/CollectionPDFView.tsx b/src/client/views/collections/CollectionPDFView.tsx
deleted file mode 100644
index cc8142ec0..000000000
--- a/src/client/views/collections/CollectionPDFView.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import { trace } from "mobx";
-import { observer } from "mobx-react";
-import { Id } from "../../../new_fields/FieldSymbols";
-import { emptyFunction } from "../../../Utils";
-import { ContextMenu } from "../ContextMenu";
-import { FieldView, FieldViewProps } from "../nodes/FieldView";
-import { CollectionBaseView, CollectionRenderProps, CollectionViewType } from "./CollectionBaseView";
-import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView";
-import "./CollectionPDFView.scss";
-import React = require("react");
-
-
-@observer
-export class CollectionPDFView extends React.Component<FieldViewProps> {
- public static LayoutString(fieldKey: string = "data", fieldExt: string = "annotations") {
- return FieldView.LayoutString(CollectionPDFView, fieldKey, fieldExt);
- }
-
- 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: "PDFOptions", event: emptyFunction, icon: "file-pdf" });
- }
- }
-
- subView = (_type: CollectionViewType, renderProps: CollectionRenderProps) => {
- return (<CollectionFreeFormView {...this.props} {...renderProps} CollectionView={this} chromeCollapsed={true} />);
- }
-
- render() {
- trace();
- return (
- <CollectionBaseView {...this.props} className={"collectionPdfView-cont"} onContextMenu={this.onContextMenu}>
- {this.subView}
- </CollectionBaseView>
- );
- }
-} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx
index 179e44266..54a36f691 100644
--- a/src/client/views/collections/CollectionSchemaCells.tsx
+++ b/src/client/views/collections/CollectionSchemaCells.tsx
@@ -13,17 +13,13 @@ import { COLLECTION_BORDER_WIDTH, MAX_ROW_HEIGHT } from '../globalCssVariables.s
import '../DocumentDecorations.scss';
import { EditableView } from "../EditableView";
import { FieldView, FieldViewProps } from "../nodes/FieldView";
-import { CollectionPDFView } from "./CollectionPDFView";
import "./CollectionSchemaView.scss";
-import { CollectionVideoView } from "./CollectionVideoView";
import { CollectionView } from "./CollectionView";
import { NumCast, StrCast, BoolCast, FieldValue, Cast } from "../../../new_fields/Types";
import { Docs } from "../../documents/Documents";
-import { DocumentContentsView } from "../nodes/DocumentContentsView";
import { SelectionManager } from "../../util/SelectionManager";
import { library } from '@fortawesome/fontawesome-svg-core';
import { faExpand } from '@fortawesome/free-solid-svg-icons';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField";
import { KeyCodes } from "../../northstar/utils/KeyCodes";
import { undoBatch } from "../../util/UndoManager";
@@ -34,8 +30,8 @@ export interface CellProps {
row: number;
col: number;
rowProps: CellInfo;
- CollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
- ContainingCollection: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
+ CollectionView: Opt<CollectionView>;
+ ContainingCollection: Opt<CollectionView>;
Document: Doc;
fieldKey: string;
renderDepth: number;
@@ -148,7 +144,6 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
Document: this.props.rowProps.original,
DataDoc: this.props.rowProps.original,
fieldKey: this.props.rowProps.column.id as string,
- fieldExt: "",
ruleProvider: undefined,
ContainingCollectionView: this.props.CollectionView,
ContainingCollectionDoc: this.props.CollectionView && this.props.CollectionView.props.Document,
diff --git a/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx b/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx
index 39abc41ec..274c8b6d1 100644
--- a/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx
+++ b/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx
@@ -229,7 +229,7 @@ export class MovableRow extends React.Component<MovableRowProps> {
<div className="collectionSchema-row-wrapper" ref={this._header} onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}>
<ReactTableDefaults.TrComponent>
<div className="row-dragger">
- <div className="row-option" onClick={() => this.props.removeDoc(this.props.rowInfo.original)}><FontAwesomeIcon icon="trash" size="sm" /></div>
+ <div className="row-option" onClick={undoBatch(() => this.props.removeDoc(this.props.rowInfo.original))}><FontAwesomeIcon icon="trash" size="sm" /></div>
<div className="row-option" style={{ cursor: "grab" }} ref={reference} onPointerDown={onItemDown}><FontAwesomeIcon icon="grip-vertical" size="sm" /></div>
</div>
{children}
diff --git a/src/client/views/collections/CollectionSchemaView.scss b/src/client/views/collections/CollectionSchemaView.scss
index e0cedc210..cb95dcbbc 100644
--- a/src/client/views/collections/CollectionSchemaView.scss
+++ b/src/client/views/collections/CollectionSchemaView.scss
@@ -10,6 +10,7 @@
top: 0;
width: 100%;
height: 100%;
+ margin-top: 0;
transition: top 0.5s;
display: flex;
justify-content: space-between;
@@ -38,30 +39,6 @@
}
}
-.collectionSchemaView-previewRegion {
- position: relative;
- background: $light-color;
- height: auto !important;
-
- .collectionSchemaView-previewDoc {
- height: 100%;
- width: 100%;
- position: absolute;
- }
-
- .collectionSchemaView-input {
- position: absolute;
- max-width: 150px;
- width: 100%;
- bottom: 0px;
- }
-
- .documentView-node:first-child {
- position: relative;
- background: $light-color;
- }
-}
-
.ReactTable {
width: 100%;
background: white;
@@ -470,7 +447,7 @@ button.add-column {
overflow: visible;
}
-.sub {
+.reactTable-sub {
padding: 10px 30px;
background-color: rgb(252, 252, 252);
width: calc(100% - 50px);
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index 8d931f812..203c68463 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -1,39 +1,34 @@
import React = require("react");
import { library } from '@fortawesome/fontawesome-svg-core';
-import { faCog, faPlus, faTable, faSortUp, faSortDown } from '@fortawesome/free-solid-svg-icons';
+import { faCog, faPlus, faSortDown, faSortUp, faTable } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, observable, trace, untracked } from "mobx";
+import { action, computed, observable, untracked } from "mobx";
import { observer } from "mobx-react";
-import ReactTable, { CellInfo, ComponentPropsGetterR, Column, RowInfo, ResizedChangeFunction, Resize } from "react-table";
+import ReactTable, { CellInfo, Column, ComponentPropsGetterR, Resize, SortingRule } from "react-table";
import "react-table/react-table.css";
-import { emptyFunction, returnOne, returnEmptyString } from "../../../Utils";
import { Doc, DocListCast, Field, Opt } from "../../../new_fields/Doc";
import { Id } from "../../../new_fields/FieldSymbols";
import { List } from "../../../new_fields/List";
import { listSpec } from "../../../new_fields/Schema";
-import { Docs, DocumentOptions } from "../../documents/Documents";
+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 { DragManager } from "../../util/DragManager";
-import { CompileScript, ts, Transformer } from "../../util/Scripting";
+import { CompileScript, Transformer, ts } from "../../util/Scripting";
import { Transform } from "../../util/Transform";
+import { undoBatch } from "../../util/UndoManager";
import { COLLECTION_BORDER_WIDTH } from '../../views/globalCssVariables.scss';
import { ContextMenu } from "../ContextMenu";
import '../DocumentDecorations.scss';
-import { DocumentView } from "../nodes/DocumentView";
-import { CollectionPDFView } from "./CollectionPDFView";
+import { CellProps, CollectionSchemaCell, CollectionSchemaCheckboxCell, CollectionSchemaDocCell, CollectionSchemaNumberCell, CollectionSchemaStringCell } from "./CollectionSchemaCells";
+import { CollectionSchemaAddColumnHeader, CollectionSchemaHeader } from "./CollectionSchemaHeaders";
+import { MovableColumn, MovableRow } from "./CollectionSchemaMovableTableHOC";
import "./CollectionSchemaView.scss";
import { CollectionSubView } from "./CollectionSubView";
-import { CollectionVideoView } from "./CollectionVideoView";
import { CollectionView } from "./CollectionView";
-import { undoBatch } from "../../util/UndoManager";
-import { CollectionSchemaHeader, CollectionSchemaAddColumnHeader } from "./CollectionSchemaHeaders";
-import { CellProps, CollectionSchemaCell, CollectionSchemaNumberCell, CollectionSchemaStringCell, CollectionSchemaBooleanCell, CollectionSchemaCheckboxCell, CollectionSchemaDocCell } from "./CollectionSchemaCells";
-import { MovableColumn, MovableRow } from "./CollectionSchemaMovableTableHOC";
-import { ComputedField, ScriptField } from "../../../new_fields/ScriptField";
-import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField";
-import { DocumentType } from "../../documents/DocumentTypes";
-
+import { ContentFittingDocumentView } from "../nodes/ContentFittingDocumentView";
library.add(faCog, faPlus, faSortUp, faSortDown);
library.add(faTable);
@@ -51,7 +46,7 @@ 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],
- ["page", ColumnType.Number], ["curPage", ColumnType.Number], ["zIndex", ColumnType.Number]
+ ["page", ColumnType.Number], ["curPage", ColumnType.Number], ["currentTimecode", ColumnType.Number], ["zIndex", ColumnType.Number]
]);
@observer
@@ -75,20 +70,14 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
super.CreateDropTarget(ele);
}
- isFocused = (doc: Doc): boolean => {
- if (!this.props.isSelected()) return false;
- return doc === this._focusedTable;
- }
+ isFocused = (doc: Doc): boolean => this.props.isSelected() && doc === this._focusedTable;
- @action
- setFocused = (doc: Doc): void => {
- this._focusedTable = doc;
- }
+ @action setFocused = (doc: Doc) => this._focusedTable = doc;
- @action
- setPreviewDoc = (doc: Doc): void => {
- this.previewDoc = doc;
- }
+ @action setPreviewDoc = (doc: Doc) => this.previewDoc = doc;
+
+ @undoBatch
+ @action setPreviewScript = (script: string) => this.previewScript = script
//toggles preview side-panel of schema
@action
@@ -130,17 +119,9 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
}
}
- onWheel = (e: React.WheelEvent): void => {
- if (this.props.active()) {
- e.stopPropagation();
- }
- }
-
@computed
get previewDocument(): Doc | undefined {
- let selected = this.previewDoc;
- let pdc = selected ? (this.previewScript && this.previewScript !== "this" ? FieldValue(Cast(selected[this.previewScript], Doc)) : selected) : undefined;
- return pdc;
+ return this.previewDoc ? (this.previewScript && this.previewScript !== "this" ? FieldValue(Cast(this.previewDoc[this.previewScript], Doc)) : this.previewDoc) : undefined;
}
getPreviewTransform = (): Transform => {
@@ -157,15 +138,17 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
get previewPanel() {
let layoutDoc = this.previewDocument ? Doc.expandTemplateLayout(this.previewDocument, this.props.DataDoc) : undefined;
return <div ref={this.createTarget}>
- <CollectionSchemaPreview
+ <ContentFittingDocumentView
Document={layoutDoc}
DataDocument={this.previewDocument !== this.props.DataDoc ? this.props.DataDoc : undefined}
+ fieldKey={this.props.fieldKey}
childDocs={this.childDocs}
renderDepth={this.props.renderDepth}
ruleProvider={this.props.Document.isRuleProvider && layoutDoc && layoutDoc.type !== DocumentType.TEXT ? this.props.Document : this.props.ruleProvider}
- width={this.previewWidth}
- height={this.previewHeight}
+ PanelWidth={this.previewWidth}
+ PanelHeight={this.previewHeight}
getTransform={this.getPreviewTransform}
+ CollectionDoc={this.props.CollectionView && this.props.CollectionView.props.Document}
CollectionView={this.props.CollectionView}
moveDocument={this.props.moveDocument}
addDocument={this.props.addDocument}
@@ -180,63 +163,51 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
</div>;
}
- @undoBatch
- @action
- setPreviewScript = (script: string) => {
- this.previewScript = script;
- }
-
@computed
get schemaTable() {
- return (
- <SchemaTable
- Document={this.props.Document}
- PanelHeight={this.props.PanelHeight}
- PanelWidth={this.props.PanelWidth}
- childDocs={this.childDocs}
- CollectionView={this.props.CollectionView}
- ContainingCollectionView={this.props.ContainingCollectionView}
- ContainingCollectionDoc={this.props.ContainingCollectionDoc}
- fieldKey={this.props.fieldKey}
- renderDepth={this.props.renderDepth}
- moveDocument={this.props.moveDocument}
- ScreenToLocalTransform={this.props.ScreenToLocalTransform}
- active={this.props.active}
- onDrop={this.onDrop}
- addDocTab={this.props.addDocTab}
- pinToPres={this.props.pinToPres}
- isSelected={this.props.isSelected}
- isFocused={this.isFocused}
- setFocused={this.setFocused}
- setPreviewDoc={this.setPreviewDoc}
- deleteDocument={this.props.removeDocument}
- dataDoc={this.props.DataDoc}
- />
- );
+ return <SchemaTable
+ Document={this.props.Document}
+ PanelHeight={this.props.PanelHeight}
+ PanelWidth={this.props.PanelWidth}
+ childDocs={this.childDocs}
+ CollectionView={this.props.CollectionView}
+ ContainingCollectionView={this.props.ContainingCollectionView}
+ ContainingCollectionDoc={this.props.ContainingCollectionDoc}
+ fieldKey={this.props.fieldKey}
+ renderDepth={this.props.renderDepth}
+ moveDocument={this.props.moveDocument}
+ ScreenToLocalTransform={this.props.ScreenToLocalTransform}
+ active={this.props.active}
+ onDrop={this.onDrop}
+ addDocTab={this.props.addDocTab}
+ pinToPres={this.props.pinToPres}
+ isSelected={this.props.isSelected}
+ isFocused={this.isFocused}
+ setFocused={this.setFocused}
+ setPreviewDoc={this.setPreviewDoc}
+ deleteDocument={this.props.removeDocument}
+ addDocument={this.props.addDocument}
+ dataDoc={this.props.DataDoc}
+ />;
}
@computed
public get schemaToolbar() {
- return (
- <div className="collectionSchemaView-toolbar">
- <div className="collectionSchemaView-toolbar-item">
- <div id="preview-schema-checkbox-div"><input type="checkbox" key={"Show Preview"} checked={this.previewWidth() !== 0} onChange={this.toggleExpander} />Show Preview</div>
- </div>
+ return <div className="collectionSchemaView-toolbar">
+ <div className="collectionSchemaView-toolbar-item">
+ <div id="preview-schema-checkbox-div"><input type="checkbox" key={"Show Preview"} checked={this.previewWidth() !== 0} onChange={this.toggleExpander} />Show Preview</div>
</div>
- );
+ </div>;
}
render() {
- Doc.UpdateDocumentExtensionForField(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey);
- return (
- <div className="collectionSchemaView-container" style={{ height: "100%", marginTop: "0", }}>
- <div className="collectionSchemaView-tableContainer" onPointerDown={this.onPointerDown} onWheel={this.onWheel} onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createTarget}>
- {this.schemaTable}
- </div>
- {this.dividerDragger}
- {!this.previewWidth() ? (null) : this.previewPanel}
+ return <div className="collectionSchemaView-container">
+ <div className="collectionSchemaView-tableContainer" onPointerDown={this.onPointerDown} onWheel={e => this.props.active() && e.stopPropagation()} onDrop={e => this.onDrop(e, {})} ref={this.createTarget}>
+ {this.schemaTable}
</div>
- );
+ {this.dividerDragger}
+ {!this.previewWidth() ? (null) : this.previewPanel}
+ </div>;
}
}
@@ -246,12 +217,13 @@ export interface SchemaTableProps {
PanelHeight: () => number;
PanelWidth: () => number;
childDocs?: Doc[];
- CollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
- ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
+ CollectionView: Opt<CollectionView>;
+ ContainingCollectionView: Opt<CollectionView>;
ContainingCollectionDoc: Opt<Doc>;
fieldKey: string;
renderDepth: number;
deleteDocument: (document: Doc) => boolean;
+ addDocument: (document: Doc) => boolean;
moveDocument: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
ScreenToLocalTransform: () => Transform;
active: () => boolean;
@@ -302,21 +274,17 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
this.props.Document.textwrappedSchemaRows = new List<string>(textWrappedRows);
}
- @computed get resized(): { "id": string, "value": number }[] {
+ @computed get resized(): { id: string, value: number }[] {
return this.columns.reduce((resized, shf) => {
- if (shf.width > -1) {
- resized.push({ "id": shf.heading, "value": shf.width });
- }
+ (shf.width > -1) && resized.push({ id: shf.heading, value: shf.width });
return resized;
- }, [] as { "id": string, "value": number }[]);
+ }, [] as { id: string, value: number }[]);
}
- @computed get sorted(): { id: string, desc: boolean }[] {
+ @computed get sorted(): SortingRule[] {
return this.columns.reduce((sorted, shf) => {
- if (shf.desc) {
- sorted.push({ "id": shf.heading, "desc": shf.desc });
- }
+ shf.desc && sorted.push({ id: shf.heading, desc: shf.desc });
return sorted;
- }, [] as { id: string, desc: boolean }[]);
+ }, [] as SortingRule[]);
}
@computed get borderWidth() { return Number(COLLECTION_BORDER_WIDTH); }
@@ -326,11 +294,9 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
let tableIsFocused = this.props.isFocused(this.props.Document);
let focusedRow = this._focusedCell.row;
let focusedCol = this._focusedCell.col;
- let isEditable = !this._headerIsEditing;// && this.props.isSelected();
+ let isEditable = !this._headerIsEditing;
- let children = this.childDocs;
-
- if (children.reduce((found, doc) => found || doc.type === "collection", false)) {
+ if (this.childDocs.reduce((found, doc) => found || doc.type === "collection", false)) {
columns.push(
{
expander: true,
@@ -438,26 +404,11 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
return Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, relativeTo, before);
}
- tableRemoveDoc = (document: Doc): boolean => {
-
- let children = this.childDocs;
- if (children.indexOf(document) !== -1) {
- children.splice(children.indexOf(document), 1);
- this.childDocs = children;
- return true;
- }
- return false;
- }
-
private getTrProps: ComponentPropsGetterR = (state, rowInfo) => {
- const that = this;
- if (!rowInfo) {
- return {};
- }
- return {
+ return !rowInfo ? {} : {
ScreenToLocalTransform: this.props.ScreenToLocalTransform,
addDoc: this.tableAddDoc,
- removeDoc: this.tableRemoveDoc,
+ removeDoc: this.props.deleteDocument,
rowInfo,
rowFocused: !this._headerIsEditing && rowInfo.index === this._focusedCell.row && this.props.isFocused(this.props.Document),
textWrapRow: this.toggleTextWrapRow,
@@ -466,14 +417,12 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
}
private getTdProps: ComponentPropsGetterR = (state, rowInfo, column, instance) => {
- if (!rowInfo) return {};
- if (!column) return {};
+ if (!rowInfo || column) return {};
let row = rowInfo.index;
//@ts-ignore
let col = this.columns.map(c => c.heading).indexOf(column!.id);
let isFocused = this._focusedCell.row === row && this._focusedCell.col === col && this.props.isFocused(this.props.Document);
- let isEditing = this.props.isFocused(this.props.Document) && this._cellIsEditing;
// TODO: editing border doesn't work :(
return {
style: {
@@ -483,113 +432,68 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
}
@action
- onExpandCollection = (collection: Doc): void => {
- this._openCollections.push(collection[Id]);
- }
-
- @action
onCloseCollection = (collection: Doc): void => {
let index = this._openCollections.findIndex(col => col === collection[Id]);
if (index > -1) this._openCollections.splice(index, 1);
}
- @action
- setCellIsEditing = (isEditing: boolean): void => {
- this._cellIsEditing = isEditing;
- }
-
- @action
- setHeaderIsEditing = (isEditing: boolean): void => {
- this._headerIsEditing = isEditing;
- }
+ @action onExpandCollection = (collection: Doc) => this._openCollections.push(collection[Id]);
+ @action setCellIsEditing = (isEditing: boolean) => this._cellIsEditing = isEditing;
+ @action setHeaderIsEditing = (isEditing: boolean) => this._headerIsEditing = isEditing;
onPointerDown = (e: React.PointerEvent): void => {
this.props.setFocused(this.props.Document);
- if (e.button === 0 && !e.altKey && !e.ctrlKey && !e.metaKey) {
- if (this.props.isSelected()) e.stopPropagation();
- }
- }
-
- onWheel = (e: React.WheelEvent): void => {
- if (this.props.active()) {
+ if (e.button === 0 && !e.altKey && !e.ctrlKey && !e.metaKey && this.props.isSelected()) {
e.stopPropagation();
}
}
+ @action
onKeyDown = (e: KeyboardEvent): void => {
if (!this._cellIsEditing && !this._headerIsEditing && this.props.isFocused(this.props.Document)) {// && this.props.isSelected()) {
let direction = e.key === "Tab" ? "tab" : e.which === 39 ? "right" : e.which === 37 ? "left" : e.which === 38 ? "up" : e.which === 40 ? "down" : "";
- this.changeFocusedCellByDirection(direction);
+ this._focusedCell = this.changeFocusedCellByDirection(direction, this._focusedCell.row, this._focusedCell.col);
- let children = this.childDocs;
- const pdoc = FieldValue(children[this._focusedCell.row]);
+ const pdoc = FieldValue(this.childDocs[this._focusedCell.row]);
pdoc && this.props.setPreviewDoc(pdoc);
}
}
- @action
- changeFocusedCellByDirection = (direction: string): void => {
- let children = this.childDocs;
+ changeFocusedCellByDirection = (direction: string, curRow: number, curCol: number) => {
switch (direction) {
- case "tab":
- if (this._focusedCell.col + 1 === this.columns.length && this._focusedCell.row + 1 === children.length) {
- this._focusedCell = { row: 0, col: 0 };
- } else if (this._focusedCell.col + 1 === this.columns.length) {
- this._focusedCell = { row: this._focusedCell.row + 1, col: 0 };
- } else {
- this._focusedCell = { row: this._focusedCell.row, col: this._focusedCell.col + 1 };
- }
- break;
- case "right":
- this._focusedCell = { row: this._focusedCell.row, col: this._focusedCell.col + 1 === this.columns.length ? this._focusedCell.col : this._focusedCell.col + 1 };
- break;
- case "left":
- this._focusedCell = { row: this._focusedCell.row, col: this._focusedCell.col === 0 ? this._focusedCell.col : this._focusedCell.col - 1 };
- break;
- case "up":
- this._focusedCell = { row: this._focusedCell.row === 0 ? this._focusedCell.row : this._focusedCell.row - 1, col: this._focusedCell.col };
- break;
- case "down":
- this._focusedCell = { row: this._focusedCell.row + 1 === children.length ? this._focusedCell.row : this._focusedCell.row + 1, col: this._focusedCell.col };
- break;
+ case "tab": return { row: (curRow + 1 === this.childDocs.length ? 0 : curRow + 1), col: curCol + 1 === this.columns.length ? 0 : curCol + 1 };
+ case "right": return { row: curRow, col: curCol + 1 === this.columns.length ? curCol : curCol + 1 };
+ case "left": return { row: curRow, col: curCol === 0 ? curCol : curCol - 1 };
+ case "up": return { row: curRow === 0 ? curRow : curRow - 1, col: curCol };
+ case "down": return { row: curRow + 1 === this.childDocs.length ? curRow : curRow + 1, col: curCol };
}
+ return this._focusedCell;
}
@action
changeFocusedCellByIndex = (row: number, col: number): void => {
- this._focusedCell = { row: row, col: col };
+ if (this._focusedCell.row !== row || this._focusedCell.col !== col) {
+ this._focusedCell = { row: row, col: col };
+ }
this.props.setFocused(this.props.Document);
}
@undoBatch
createRow = () => {
- let children = this.childDocs;
-
- let newDoc = Docs.Create.TextDocument({ width: 100, height: 30 });
- let proto = Doc.GetProto(newDoc);
- proto.title = "";
- children.push(newDoc);
-
- this.childDocs = children;
+ let newDoc = Docs.Create.TextDocument({ title: "", width: 100, height: 30 });
+ this.props.addDocument(newDoc);
}
@undoBatch
@action
createColumn = () => {
let index = 0;
- let columns = this.columns;
- let found = columns.findIndex(col => col.heading.toUpperCase() === "New field".toUpperCase()) > -1;
- if (!found) {
- columns.push(new SchemaHeaderField("New field", "#f1efeb"));
- this.columns = columns;
- return;
- }
+ let found = this.columns.findIndex(col => col.heading.toUpperCase() === "New field".toUpperCase()) > -1;
while (found) {
index++;
- found = columns.findIndex(col => col.heading.toUpperCase() === ("New field (" + index + ")").toUpperCase()) > -1;
+ found = this.columns.findIndex(col => col.heading.toUpperCase() === ("New field (" + index + ")").toUpperCase()) > -1;
}
- columns.push(new SchemaHeaderField("New field (" + index + ")", "#f1efeb"));
- this.columns = columns;
+ this.columns.push(new SchemaHeaderField(`New field ${index ? "(" + index + ")" : ""}`, "#f1efeb"));
}
@undoBatch
@@ -682,9 +586,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
}
@action
- setColumns = (columns: SchemaHeaderField[]) => {
- this.columns = columns;
- }
+ setColumns = (columns: SchemaHeaderField[]) => this.columns = columns
@undoBatch
reorderColumns = (toMove: SchemaHeaderField, relativeTo: SchemaHeaderField, before: boolean, columnsValues: SchemaHeaderField[]) => {
@@ -730,11 +632,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
let textWrapped = this.textWrappedRows;
let index = textWrapped.findIndex(id => doc[Id] === id);
- if (index > -1) {
- textWrapped.splice(index, 1);
- } else {
- textWrapped.push(doc[Id]);
- }
+ index > -1 ? textWrapped.splice(index, 1) : textWrapped.push(doc[Id]);
this.textWrappedRows = textWrapped;
}
@@ -764,13 +662,8 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
expanded={expanded}
resized={this.resized}
onResizedChange={this.onResizedChange}
- SubComponent={hasCollectionChild ?
- row => {
- if (row.original.type === "collection") {
- return <div className="sub"><SchemaTable {...this.props} Document={row.original} childDocs={undefined} /></div>;
- }
- }
- : undefined}
+ SubComponent={!hasCollectionChild ? undefined : row => (row.original.type !== "collection") ? (null) :
+ <div className="reactTable-sub"><SchemaTable {...this.props} Document={row.original} childDocs={undefined} /></div>}
/>;
}
@@ -886,143 +779,9 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
}
render() {
- return (
- <div className="collectionSchemaView-table" onPointerDown={this.onPointerDown} onWheel={this.onWheel}
- onDrop={(e: React.DragEvent) => this.props.onDrop(e, {})} onContextMenu={this.onContextMenu} >
- {this.reactTable}
- <div className="collectionSchemaView-addRow" onClick={() => this.createRow()}>+ new</div>
- </div>
- );
- }
-}
-
-
-interface CollectionSchemaPreviewProps {
- Document?: Doc;
- DataDocument?: Doc;
- childDocs?: Doc[];
- renderDepth: number;
- fitToBox?: boolean;
- width: () => number;
- height: () => number;
- ruleProvider: Doc | undefined;
- showOverlays?: (doc: Doc) => { title?: string, caption?: string };
- CollectionView?: CollectionView | CollectionPDFView | CollectionVideoView;
- onClick?: ScriptField;
- getTransform: () => Transform;
- addDocument: (document: Doc, allowDuplicates?: boolean) => boolean;
- moveDocument: (document: Doc, target: Doc, addDoc: ((doc: Doc) => boolean)) => boolean;
- removeDocument: (document: Doc) => boolean;
- active: () => boolean;
- whenActiveChanged: (isActive: boolean) => void;
- addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean;
- pinToPres: (document: Doc) => void;
- setPreviewScript: (script: string) => void;
- previewScript?: string;
-}
-
-@observer
-export class CollectionSchemaPreview extends React.Component<CollectionSchemaPreviewProps>{
- private dropDisposer?: DragManager.DragDropDisposer;
- _mainCont?: HTMLDivElement;
- private get nativeWidth() { return NumCast(this.props.Document!.nativeWidth, this.props.width()); }
- private get nativeHeight() { return NumCast(this.props.Document!.nativeHeight, this.props.height()); }
- private contentScaling = () => {
- let wscale = this.props.width() / (this.nativeWidth ? this.nativeWidth : this.props.width());
- if (wscale * this.nativeHeight > this.props.height()) {
- return this.props.height() / (this.nativeHeight ? this.nativeHeight : this.props.height());
- }
- return wscale;
- }
- protected createDropTarget = (ele: HTMLDivElement) => {
- }
- private createTarget = (ele: HTMLDivElement) => {
- this._mainCont = ele;
- this.dropDisposer && this.dropDisposer();
- if (ele) {
- this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } });
- }
- }
-
- @undoBatch
- @action
- drop = (e: Event, de: DragManager.DropEvent) => {
- if (de.data instanceof DragManager.DocumentDragData) {
- this.props.childDocs && this.props.childDocs.map(otherdoc => {
- let target = Doc.GetProto(otherdoc);
- let layoutNative = Doc.MakeTitled("layoutNative");
- layoutNative.layout = ComputedField.MakeFunction("this.image_data[0]");
- target.layoutNative = layoutNative;
- target.layoutCUstom = target.layout = Doc.MakeDelegate(de.data.draggedDocuments[0]);
- });
- e.stopPropagation();
- }
- return true;
- }
- private PanelWidth = () => this.nativeWidth ? this.nativeWidth * this.contentScaling() : this.props.width();
- private PanelHeight = () => this.nativeHeight ? this.nativeHeight * this.contentScaling() : this.props.height();
- private getTransform = () => this.props.getTransform().translate(-this.centeringOffset, 0).scale(1 / this.contentScaling());
- get centeringOffset() { return this.nativeWidth ? (this.props.width() - this.nativeWidth * this.contentScaling()) / 2 : 0; }
- @action
- onPreviewScriptChange = (e: React.ChangeEvent<HTMLInputElement>) => {
- this.props.setPreviewScript(e.currentTarget.value);
- }
- @computed get borderRounding() {
- let br = StrCast(this.props.Document!.borderRounding);
- if (br.endsWith("%")) {
- let percent = Number(br.substr(0, br.length - 1)) / 100;
- let nativeDim = Math.min(NumCast(this.props.Document!.nativeWidth), NumCast(this.props.Document!.nativeHeight));
- let minDim = percent * (nativeDim ? nativeDim : Math.min(this.PanelWidth(), this.PanelHeight()));
- return minDim;
- }
- return undefined;
- }
-
-
- render() {
- let input = this.props.previewScript === undefined ? (null) :
- <div ref={this.createTarget}><input className="collectionSchemaView-input" value={this.props.previewScript} onChange={this.onPreviewScriptChange}
- style={{ left: `calc(50% - ${Math.min(75, (this.props.Document ? this.PanelWidth() / 2 : 75))}px)` }} /></div>;
- return (<div className="collectionSchemaView-previewRegion"
- style={{ width: this.props.width(), height: this.props.height() }}>
- {!this.props.Document || !this.props.width ? (null) : (
- <div className="collectionSchemaView-previewDoc"
- style={{
- transform: `translate(${this.centeringOffset}px, 0px)`,
- borderRadius: this.borderRounding,
- display: "inline",
- height: this.props.height(),
- width: this.props.width()
- }}>
- <DocumentView
- DataDoc={this.props.DataDocument}
- Document={this.props.Document}
- fitToBox={this.props.fitToBox}
- onClick={this.props.onClick}
- ruleProvider={this.props.ruleProvider}
- showOverlays={this.props.showOverlays}
- addDocument={this.props.addDocument}
- removeDocument={this.props.removeDocument}
- moveDocument={this.props.moveDocument}
- whenActiveChanged={this.props.whenActiveChanged}
- ContainingCollectionView={this.props.CollectionView}
- ContainingCollectionDoc={this.props.CollectionView && this.props.CollectionView.props.Document}
- addDocTab={this.props.addDocTab}
- pinToPres={this.props.pinToPres}
- parentActive={this.props.active}
- ScreenToLocalTransform={this.getTransform}
- renderDepth={this.props.renderDepth + 1}
- ContentScaling={this.contentScaling}
- PanelWidth={this.PanelWidth}
- PanelHeight={this.PanelHeight}
- focus={emptyFunction}
- backgroundColor={returnEmptyString}
- bringToFront={emptyFunction}
- zoomToScale={emptyFunction}
- getScale={returnOne}
- />
- </div>)}
- {input}
- </div>);
+ return <div className="collectionSchemaView-table" onPointerDown={this.onPointerDown} onWheel={e => this.props.active() && e.stopPropagation()} onDrop={e => this.props.onDrop(e, {})} onContextMenu={this.onContextMenu} >
+ {this.reactTable}
+ <div className="collectionSchemaView-addRow" onClick={() => this.createRow()}>+ new</div>
+ </div>;
}
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss
index 01d4ea2b6..29178b909 100644
--- a/src/client/views/collections/CollectionStackingView.scss
+++ b/src/client/views/collections/CollectionStackingView.scss
@@ -1,12 +1,19 @@
@import "../globalCssVariables";
.collectionMasonryView {
- display:inline;
+ display: inline;
}
-.collectionStackingView{
+
+.collectionStackingView {
display: flex;
}
-.collectionStackingView, .collectionMasonryView{
+.collectionStackingMasonry-cont {
+ position:relative;
+ height:100%;
+ width:100%;
+}
+.collectionStackingView,
+.collectionMasonryView {
height: 100%;
width: 100%;
position: absolute;
@@ -14,6 +21,11 @@
overflow-y: auto;
flex-wrap: wrap;
transition: top .5s;
+ >div {
+ position: relative;
+ display: block;
+ }
+
.collectionSchemaView-previewDoc {
height: 100%;
position: absolute;
@@ -40,10 +52,12 @@
top: 0;
left: 0;
}
+
.collectionStackingView-masonrySingle {
height: 100%;
position: absolute;
}
+
.collectionStackingView-masonryGrid {
margin: auto;
height: max-content;
@@ -90,18 +104,64 @@
grid-column-end: span 1;
height: 100%;
margin: auto;
+ display: inline-grid;
}
-
+
.collectionStackingView-masonrySection {
margin: auto;
}
+ .collectionStackingView-collapseBar {
+ margin-left: 2px;
+ margin-right: 2px;
+ margin-top: 2px;
+ background: $main-accent;
+ height: 5px;
+
+ &.active {
+ margin-left: 0;
+ margin-right: 0;
+ background: red;
+ }
+ }
+ .collectionStackingView-miniHeader {
+ width: 100%;
+ .editableView-container-editing-oneLine {
+ min-height: 20px;
+ display: flex;
+ align-items: center;
+ flex-direction: row;
+ }
+ span::before , span::after{
+ content: "";
+ width: 50%;
+ border-top: dashed gray 1px;
+ position: relative;
+ display: inline-block;
+ }
+ span::before {
+ margin-right: 10px;
+ }
+ span::after{
+ margin-left: 10px;
+ }
+ span {
+ position: relative;
+ text-align: center;
+ white-space: nowrap;
+ overflow: visible;
+ width: 100%;
+ display: flex;
+ color:gray;
+ align-items: center;
+ }
+ }
.collectionStackingView-sectionHeader {
text-align: center;
margin-left: 2px;
margin-right: 2px;
margin-top: 10px;
- background: gray;
+ background: $main-accent;
// overflow: hidden; overflow is visible so the color menu isn't hidden -ftong
.editableView-input {
@@ -132,7 +192,7 @@
.editableView-input:hover,
.editableView-container-editing:hover,
.editableView-container-editing-oneLine:hover {
- cursor: text
+ cursor: text;
}
.editableView-input {
@@ -182,11 +242,45 @@
}
}
- .collectionStackingView-sectionDelete {
+ .collectionStackingView-sectionOptions {
position: absolute;
right: 0;
top: 0;
height: 100%;
+
+ [class*="css"] {
+ max-width: 102px;
+ }
+
+ .collectionStackingView-sectionOptionButton {
+ height: 35px;
+ }
+
+ .collectionStackingView-optionPicker {
+
+ .optionOptions {
+ display: inline;
+ }
+
+ .optionPicker {
+ cursor: pointer;
+ height: 20px;
+ border-radius: 10px;
+ margin: 3px;
+ width:max-content;
+
+ &.active {
+ color: red;
+ }
+ }
+ }
+ }
+
+ .collectionStackingView-sectionDelete {
+ position: absolute;
+ right: 25px;
+ top: 0;
+ height: 100%;
}
}
@@ -288,6 +382,4 @@
.rc-switch-checked .rc-switch-inner {
left: 8px;
}
-
-
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 45de0fefa..15033e51a 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -1,7 +1,7 @@
import React = require("react");
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { CursorProperty } from "csstype";
-import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx";
+import { action, computed, IReactionDisposer, observable, reaction, runInAction, trace } from "mobx";
import { observer } from "mobx-react";
import Switch from 'rc-switch';
import { Doc, HeightSym, WidthSym } from "../../../new_fields/Doc";
@@ -16,13 +16,14 @@ import { DragManager } from "../../util/DragManager";
import { Transform } from "../../util/Transform";
import { undoBatch } from "../../util/UndoManager";
import { EditableView } from "../EditableView";
-import { CollectionSchemaPreview } from "./CollectionSchemaView";
+import { ContentFittingDocumentView } from "../nodes/ContentFittingDocumentView";
import "./CollectionStackingView.scss";
import { CollectionStackingViewFieldColumn } from "./CollectionStackingViewFieldColumn";
import { CollectionSubView } from "./CollectionSubView";
import { ContextMenu } from "../ContextMenu";
import { ContextMenuProps } from "../ContextMenuItem";
import { ScriptBox } from "../ScriptBox";
+import { CollectionMasonryViewFieldRow } from "./CollectionMasonryViewFieldRow";
@observer
export class CollectionStackingView extends CollectionSubView(doc => doc) {
@@ -32,7 +33,8 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
_sectionFilterDisposer?: IReactionDisposer;
_docXfs: any[] = [];
_columnStart: number = 0;
- @observable private cursor: CursorProperty = "grab";
+ @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); }
@@ -42,18 +44,35 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
@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.ContainingCollectionDoc && (this.props.ContainingCollectionDoc.chromeStatus !== 'view-mode' && this.props.ContainingCollectionDoc.chromeStatus !== 'disabled')); }
+ @computed get showAddAGroup() { return (this.sectionFilter && (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; }
childDocHeight(child: Doc) { return this.getDocHeight(Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, child).layout); }
- get layoutDoc() {
- // if this document's layout field contains a document (ie, a rendering template), then we will use that
- // to determine the render JSX string, otherwise the layout field should directly contain a JSX layout string.
- return this.props.Document.layout instanceof Doc ? this.props.Document.layout : this.props.Document;
+ children(docs: Doc[]) {
+ this._docXfs.length = 0;
+ return docs.map((d, i) => {
+ let pair = Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, d);
+ let layoutDoc = pair.layout ? Doc.Layout(pair.layout) : d;
+ let width = () => Math.min(layoutDoc.nativeWidth && !layoutDoc.ignoreAspect && !this.props.Document.fillColumn ? layoutDoc[WidthSym]() : Number.MAX_VALUE, this.columnWidth / this.numGroupColumns);
+ let height = () => this.getDocHeight(layoutDoc);
+ let dref = React.createRef<HTMLDivElement>();
+ let dxf = () => this.getDocTransform(layoutDoc, dref.current!);
+ this._docXfs.push({ dxf: dxf, width: width, height: height });
+ let rowSpan = Math.ceil((height() + this.gridGap) / this.gridGap);
+ let style = this.isStackingView ? { width: width(), margin: "auto", marginTop: i === 0 ? 0 : this.gridGap, height: height() } : { gridRowEnd: `span ${rowSpan}` };
+ return <div className={`collectionStackingView-${this.isStackingView ? "columnDoc" : "masonryDoc"}`} key={d[Id]} ref={dref} style={style} >
+ {this.getDisplayDoc(pair.layout || d, pair.data, dxf, width)}
+ </div>;
+ });
+ }
+ @action
+ setDocHeight = (key: string, sectionHeight: number) => {
+ this._heightMap.set(key, sectionHeight);
}
get Sections() {
@@ -86,19 +105,30 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
}
componentDidMount() {
- // is there any reason this needs to exist? -syip. yes, it handles autoHeight for stacking views (masonry isn't yet supported).
+ super.componentDidMount();
this._heightDisposer = reaction(() => {
- if (this.isStackingView && BoolCast(this.props.Document.autoHeight)) {
+ if (this.props.Document.autoHeight) {
let sectionsList = Array.from(this.Sections.size ? this.Sections.values() : [this.filteredChildren]);
- return this.props.ContentScaling() * sectionsList.reduce((maxHght, s) => Math.max(maxHght,
- (this.Sections.size ? 50 : 0) + s.reduce((height, d, i) => height + this.childDocHeight(d) + (i === s.length - 1 ? this.yMargin : this.gridGap), this.yMargin)
- ), 0);
+ if (this.isStackingView) {
+ let res = this.props.ContentScaling() * sectionsList.reduce((maxHght, s) => {
+ let r1 = Math.max(maxHght,
+ (this.Sections.size ? 50 : 0) + s.reduce((height, d, i) => {
+ let val = height + this.childDocHeight(d) + (i === s.length - 1 ? this.yMargin : this.gridGap);
+ return val;
+ }, this.yMargin));
+ return r1;
+ }, 0);
+ return res;
+ } else {
+ let sum = Array.from(this._heightMap.values()).reduce((acc: number, curr: number) => acc += curr, 0);
+ return this.props.ContentScaling() * (sum + (this.Sections.size ? (this.props.Document.miniHeaders ? 20 : 85) : -15));
+ }
}
return -1;
},
(hgt: number) => {
let doc = hgt === -1 ? undefined : this.props.DataDoc && this.props.DataDoc.layout === this.layoutDoc ? this.props.DataDoc : this.layoutDoc;
- doc && (doc.height = hgt);
+ doc && hgt > 0 && (Doc.Layout(doc).height = hgt);
},
{ fireImmediately: true }
);
@@ -110,6 +140,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
);
}
componentWillUnmount() {
+ super.componentWillUnmount();
this._heightDisposer && this._heightDisposer();
this._sectionFilterDisposer && this._sectionFilterDisposer();
}
@@ -120,7 +151,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
}
createRef = (ele: HTMLDivElement | null) => {
this._masonryGridRef = ele;
- this.createDropTarget(ele!);
+ this.createDropTarget(ele!); //so the whole grid is the drop target?
}
overlays = (doc: Doc) => {
@@ -130,20 +161,24 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
@computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); }
@computed get onClickHandler() { return ScriptCast(this.Document.onChildClick); }
- getDisplayDoc(layoutDoc: Doc, dataDoc: Doc | undefined, dxf: () => Transform, width: () => number) {
- let height = () => this.getDocHeight(layoutDoc);
+ getDisplayDoc(doc: Doc, dataDoc: Doc | undefined, dxf: () => Transform, width: () => number) {
+ let layoutDoc = Doc.Layout(doc);
+ let height = () => this.getDocHeight(doc);
let finalDxf = () => dxf().scale(this.columnWidth / layoutDoc[WidthSym]());
- return <CollectionSchemaPreview
- Document={layoutDoc}
+ return <ContentFittingDocumentView
+ Document={doc}
DataDocument={dataDoc}
+ fieldKey={this.props.fieldKey}
showOverlays={this.overlays}
renderDepth={this.props.renderDepth}
ruleProvider={this.props.Document.isRuleProvider && layoutDoc.type !== DocumentType.TEXT ? this.props.Document : this.props.ruleProvider}
fitToBox={this.props.fitToBox}
- onClick={layoutDoc.isTemplate ? this.onClickHandler : this.onChildClickHandler}
- width={width}
- height={height}
+ onClick={layoutDoc.isTemplateDoc ? this.onClickHandler : this.onChildClickHandler}
+ PanelWidth={width}
+ PanelHeight={height}
getTransform={finalDxf}
+ focus={this.props.focus}
+ CollectionDoc={this.props.CollectionView && this.props.CollectionView.props.Document}
CollectionView={this.props.CollectionView}
addDocument={this.props.addDocument}
moveDocument={this.props.moveDocument}
@@ -154,25 +189,27 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
pinToPres={this.props.pinToPres}
setPreviewScript={emptyFunction}
previewScript={undefined}>
- </CollectionSchemaPreview>;
+ </ContentFittingDocumentView>;
}
getDocHeight(d?: Doc) {
if (!d) return 0;
- let nw = NumCast(d.nativeWidth);
- let nh = NumCast(d.nativeHeight);
- if (!d.ignoreAspect && !d.fitWidth && nw && nh) {
+ let layoutDoc = Doc.Layout(d);
+ let nw = NumCast(layoutDoc.nativeWidth);
+ let nh = NumCast(layoutDoc.nativeHeight);
+ let wid = this.columnWidth / (this.isStackingView ? this.numGroupColumns : 1);
+ if (!layoutDoc.ignoreAspect && !layoutDoc.fitWidth && nw && nh) {
let aspect = nw && nh ? nh / nw : 1;
- let wid = this.columnWidth / (this.isStackingView ? this.numGroupColumns : 1);
- if (!(d.nativeWidth && !d.ignoreAspect && this.props.Document.fillColumn)) wid = Math.min(d[WidthSym](), wid);
+ if (!(d.nativeWidth && !layoutDoc.ignoreAspect && this.props.Document.fillColumn)) wid = Math.min(layoutDoc[WidthSym](), wid);
return wid * aspect;
}
- return d.fitWidth ? Math.min(this.props.PanelHeight() - 2 * this.yMargin, d[HeightSym]()) : d[HeightSym]();
+ 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]();
}
columnDividerDown = (e: React.PointerEvent) => {
e.stopPropagation();
e.preventDefault();
- runInAction(() => this.cursor = "grabbing");
+ 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];
@@ -182,18 +219,18 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
let dragPos = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY)[0];
let delta = dragPos - this._columnStart;
this._columnStart = dragPos;
- this.layoutDoc.columnWidth = this.columnWidth + delta;
+ this.layoutDoc.columnWidth = Math.max(10, this.columnWidth + delta);
}
@action
onDividerUp = (e: PointerEvent): void => {
- runInAction(() => this.cursor = "grab");
+ runInAction(() => this._cursor = "grab");
document.removeEventListener("pointermove", this.onDividerMove);
document.removeEventListener('pointerup', this.onDividerUp);
}
@computed get columnDragger() {
- return <div className="collectionStackingView-columnDragger" onPointerDown={this.columnDividerDown} ref={this._draggerRef} style={{ cursor: this.cursor, left: `${this.columnWidth + this.xMargin}px` }} >
+ return <div className="collectionStackingView-columnDragger" onPointerDown={this.columnDividerDown} ref={this._draggerRef} style={{ cursor: this._cursor, left: `${this.columnWidth + this.xMargin}px` }} >
<FontAwesomeIcon icon={"arrows-alt-h"} />
</div>;
}
@@ -203,12 +240,14 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
drop = (e: Event, de: DragManager.DropEvent) => {
let where = [de.x, de.y];
let targInd = -1;
+ let plusOne = false;
if (de.data instanceof DragManager.DocumentDragData) {
this._docXfs.map((cd, i) => {
let pos = cd.dxf().inverse().transformPoint(-2 * this.gridGap, -2 * this.gridGap);
let pos1 = cd.dxf().inverse().transformPoint(cd.width(), cd.height());
if (where[0] > pos[0] && where[0] < pos1[0] && where[1] > pos[1] && where[1] < pos1[1]) {
targInd = i;
+ plusOne = (where[1] > (pos[1] + pos1[1]) / 2 ? 1 : 0) ? true : false;
}
});
}
@@ -220,14 +259,14 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
else targInd = docs.indexOf(this.filteredChildren[targInd]);
let srcInd = docs.indexOf(newDoc);
docs.splice(srcInd, 1);
- docs.splice(targInd > srcInd ? targInd - 1 : targInd, 0, newDoc);
+ docs.splice((targInd > srcInd ? targInd - 1 : targInd) + (plusOne ? 1 : 0), 0, newDoc);
}
}
return false;
}
@undoBatch
@action
- onDrop = (e: React.DragEvent): void => {
+ onDrop = async (e: React.DragEvent): Promise<void> => {
let where = [e.clientX, e.clientY];
let targInd = -1;
this._docXfs.map((cd, i) => {
@@ -279,55 +318,32 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
let outerXf = Utils.GetScreenTransform(this._masonryGridRef!);
let offset = this.props.ScreenToLocalTransform().transformDirection(outerXf.translateX - translateX, outerXf.translateY - translateY);
return this.props.ScreenToLocalTransform().
- translate(offset[0], offset[1]).
+ translate(offset[0], offset[1] + (this.props.ChromeHeight ? this.props.ChromeHeight() : 0)).
scale(NumCast(doc.width, 1) / this.columnWidth);
}
- masonryChildren(docs: Doc[]) {
- this._docXfs.length = 0;
- return docs.map((d, i) => {
- const pair = Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, d);
- if (!pair.layout || pair.data instanceof Promise) {
- return (null);
- }
- let dref = React.createRef<HTMLDivElement>();
- let width = () => (d.nativeWidth && !d.ignoreAspect && !this.props.Document.fillColumn ? Math.min(d[WidthSym](), this.columnWidth) : this.columnWidth);/// (uniqueHeadings.length + 1);
- let height = () => this.getDocHeight(pair.layout);
- let dxf = () => this.getDocTransform(pair.layout!, dref.current!);
- let rowSpan = Math.ceil((height() + this.gridGap) / this.gridGap);
- this._docXfs.push({ dxf: dxf, width: width, height: height });
- return !pair.layout ? (null) : <div className="collectionStackingView-masonryDoc" key={d[Id]} ref={dref} style={{ gridRowEnd: `span ${rowSpan}` }} >
- {this.getDisplayDoc(pair.layout, pair.data, dxf, width)}
- </div>;
- });
- }
- @observable _headingsHack: number = 1;
- sectionMasonry(heading: SchemaHeaderField | undefined, docList: Doc[]) {
- let cols = Math.max(1, Math.min(docList.length,
- Math.floor((this.props.PanelWidth() - 2 * this.xMargin) / (this.columnWidth + this.gridGap))));
- if (isNaN(cols)) {
- console.log("naN");
- cols = 1;
+ sectionMasonry = (heading: SchemaHeaderField | undefined, docList: Doc[]) => {
+ let key = this.sectionFilter;
+ let type: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function" | undefined = undefined;
+ let types = docList.length ? docList.map(d => typeof d[key]) : this.childDocs.map(d => typeof d[key]);
+ if (types.map((i, idx) => types.indexOf(i) === idx).length === 1) {
+ type = types[0];
}
- return <div key={heading ? heading.heading : "empty"} className="collectionStackingView-masonrySection">
- {!heading ? (null) :
- <div key={`${heading.heading}`} className="collectionStackingView-sectionHeader" style={{ background: heading.color }}
- onClick={action(() => this._headingsHack++ && heading.setCollapsed(!heading.collapsed))} >
- {heading.heading}
- </div>}
- {this._headingsHack && heading && heading.collapsed ? (null) :
- <div key={`${heading}-stack`} className={`collectionStackingView-masonryGrid`}
- style={{
- padding: `${this.yMargin}px ${this.xMargin}px`,
- width: `${cols * (this.columnWidth + this.gridGap) + 2 * this.xMargin - this.gridGap}px`,
- gridGap: this.gridGap,
- gridTemplateColumns: numberRange(cols).reduce((list, i) => list + ` ${this.columnWidth}px`, ""),
- }}>
- {this.masonryChildren(docList)}
- {this.columnDragger}
- </div>
- }
- </div>;
+ let rows = () => !this.isStackingView ? 1 : Math.max(1, Math.min(docList.length,
+ Math.floor((this.props.PanelWidth() - 2 * this.xMargin) / (this.columnWidth + this.gridGap))));
+ return <CollectionMasonryViewFieldRow
+ key={heading ? heading.heading : ""}
+ rows={rows}
+ headings={this.headings}
+ heading={heading ? heading.heading : ""}
+ headingObject={heading}
+ docList={docList}
+ parent={this}
+ type={type}
+ createDropTarget={this.createDropTarget}
+ screenToLocalTransform={this.props.ScreenToLocalTransform}
+ setDocHeight={this.setDocHeight}
+ />;
}
@action
@@ -347,7 +363,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
}
onToggle = (checked: Boolean) => {
- this.props.ContainingCollectionDoc && (this.props.ContainingCollectionDoc.chromeStatus = checked ? "collapsed" : "view-mode");
+ this.props.Document.chromeStatus = checked ? "collapsed" : "view-mode";
}
onContextMenu = (e: React.MouseEvent): void => {
@@ -372,33 +388,33 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
SetValue: this.addGroup,
contents: "+ ADD A GROUP"
};
- Doc.UpdateDocumentExtensionForField(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey);
let sections = [[undefined, this.filteredChildren] as [SchemaHeaderField | undefined, Doc[]]];
if (this.sectionFilter) {
let entries = Array.from(this.Sections.entries());
sections = entries.sort(this.sortFunc);
}
return (
- <div className={this.isStackingView ? "collectionStackingView" : "collectionMasonryView"}
- ref={this.createRef}
- onScroll={action((e: React.UIEvent<HTMLDivElement>) => this._scroll = e.currentTarget.scrollTop)}
- onDrop={this.onDrop.bind(this)}
- onContextMenu={this.onContextMenu}
- onWheel={(e: React.WheelEvent) => e.stopPropagation()} >
- {sections.map(section => this.isStackingView ? this.sectionStacking(section[0], section[1]) : this.sectionMasonry(section[0], section[1]))}
- {!this.showAddAGroup ? (null) :
- <div key={`${this.props.Document[Id]}-addGroup`} className="collectionStackingView-addGroupButton"
- style={{ width: this.columnWidth / this.numGroupColumns - 10, marginTop: 10 }}>
- <EditableView {...editableViewProps} />
- </div>}
- {this.props.ContainingCollectionDoc && this.props.ContainingCollectionDoc.chromeStatus !== 'disabled' ? <Switch
- onChange={this.onToggle}
- onClick={this.onToggle}
- defaultChecked={this.props.ContainingCollectionDoc.chromeStatus !== 'view-mode'}
- checkedChildren="edit"
- unCheckedChildren="view"
- /> : null}
- </div>
+ <div className="collectionStackingMasonry-cont" >
+ <div className={this.isStackingView ? "collectionStackingView" : "collectionMasonryView"}
+ ref={this.createRef}
+ onScroll={action((e: React.UIEvent<HTMLDivElement>) => this._scroll = e.currentTarget.scrollTop)}
+ onDrop={this.onDrop.bind(this)}
+ onContextMenu={this.onContextMenu}
+ onWheel={(e: React.WheelEvent) => e.stopPropagation()} >
+ {sections.map(section => this.isStackingView ? this.sectionStacking(section[0], section[1]) : this.sectionMasonry(section[0], section[1]))}
+ {!this.showAddAGroup ? (null) :
+ <div key={`${this.props.Document[Id]}-addGroup`} className="collectionStackingView-addGroupButton"
+ style={{ width: !this.isStackingView ? "100%" : this.columnWidth / this.numGroupColumns - 10, marginTop: 10 }}>
+ <EditableView {...editableViewProps} />
+ </div>}
+ {this.props.Document.chromeStatus !== 'disabled' ? <Switch
+ onChange={this.onToggle}
+ onClick={this.onToggle}
+ defaultChecked={this.props.Document.chromeStatus !== 'view-mode'}
+ checkedChildren="edit"
+ unCheckedChildren="view"
+ /> : null}
+ </div> </div>
);
}
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
index 240adf428..b9d334b10 100644
--- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
+++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
@@ -2,7 +2,7 @@ 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, trace } from "mobx";
+import { action, observable, trace, runInAction } from "mobx";
import { observer } from "mobx-react";
import { Doc, WidthSym } from "../../../new_fields/Doc";
import { Id } from "../../../new_fields/FieldSymbols";
@@ -23,7 +23,6 @@ import "./CollectionStackingView.scss";
library.add(faPalette);
-
interface CSVFieldColumnProps {
cols: () => number;
headings: () => object[];
@@ -39,6 +38,7 @@ 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;
@@ -58,8 +58,8 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
}
@undoBatch
- @action
- columnDrop = (e: Event, de: DragManager.DropEvent) => {
+ columnDrop = action((e: Event, de: DragManager.DropEvent) => {
+ this._createAliasSelected = false;
if (de.data instanceof DragManager.DocumentDragData) {
let key = StrCast(this.props.parent.props.Document.sectionFilter);
let castedValue = this.getValue(this._heading);
@@ -72,29 +72,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
this.props.parent.drop(e, de);
e.stopPropagation();
}
- }
-
- children(docs: Doc[]) {
- let parent = this.props.parent;
- parent._docXfs.length = 0;
- return docs.map((d, i) => {
- const pair = Doc.GetLayoutDataDocPair(parent.props.Document, parent.props.DataDoc, parent.props.fieldKey, d);
- if (!pair.layout || pair.data instanceof Promise) {
- return (null);
- }
- let width = () => Math.min(d.nativeWidth && !d.ignoreAspect && !parent.props.Document.fillColumn ? d[WidthSym]() : Number.MAX_VALUE, parent.columnWidth / parent.numGroupColumns);
- let height = () => parent.getDocHeight(pair.layout);
- let dref = React.createRef<HTMLDivElement>();
- let dxf = () => parent.getDocTransform(pair.layout!, dref.current!);
- parent._docXfs.push({ dxf: dxf, width: width, height: height });
- let rowSpan = Math.ceil((height() + parent.gridGap) / parent.gridGap);
- let style = parent.isStackingView ? { width: width(), margin: "auto", marginTop: i === 0 ? 0 : parent.gridGap, height: height() } : { gridRowEnd: `span ${rowSpan}` };
- return <div className={`collectionStackingView-${parent.isStackingView ? "columnDoc" : "masonryDoc"}`} key={d[Id]} ref={dref} style={style} >
- {parent.getDisplayDoc(pair.layout, pair.data, dxf, width)}
- </div>;
- });
- }
-
+ });
getValue = (value: string): any => {
let parsed = parseInt(value);
if (!isNaN(parsed)) {
@@ -111,6 +89,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
@action
headingChanged = (value: string, shiftDown?: boolean) => {
+ this._createAliasSelected = false;
let key = StrCast(this.props.parent.props.Document.sectionFilter);
let castedValue = this.getValue(value);
if (castedValue) {
@@ -131,6 +110,7 @@ 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;
@@ -140,18 +120,21 @@ 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) => {
+ this._createAliasSelected = false;
let key = StrCast(this.props.parent.props.Document.sectionFilter);
let newDoc = Docs.Create.TextDocument({ height: 18, width: 200, documentText: "@@@" + value, title: value, autoHeight: true });
newDoc[key] = this.getValue(this.props.heading);
@@ -163,6 +146,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
@action
deleteColumn = () => {
+ this._createAliasSelected = false;
let key = StrCast(this.props.parent.props.Document.sectionFilter);
this.props.docList.forEach(d => d[key] = undefined);
if (this.props.parent.sectionHeaders && this.props.headingObject) {
@@ -171,6 +155,16 @@ 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);
+ this.toggleVisibility();
+ }
+ }
+
startDrag = (e: PointerEvent) => {
let [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) {
@@ -204,10 +198,13 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
let [dx, dy] = this.props.screenToLocalTransform().transformDirection(e.clientX, e.clientY);
this._startDragPosition = { x: dx, y: dy };
- document.removeEventListener("pointermove", this.startDrag);
- document.addEventListener("pointermove", this.startDrag);
- document.removeEventListener("pointerup", this.pointerUp);
- document.addEventListener("pointerup", this.pointerUp);
+ if (this._createAliasSelected) {
+ document.removeEventListener("pointermove", this.startDrag);
+ document.addEventListener("pointermove", this.startDrag);
+ document.removeEventListener("pointerup", this.pointerUp);
+ document.addEventListener("pointerup", this.pointerUp);
+ }
+ runInAction(() => this._createAliasSelected = false);
}
renderColorPicker = () => {
@@ -240,6 +237,30 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
);
}
+ @action
+ toggleAlias = () => {
+ this._createAliasSelected = true;
+ }
+
+ renderMenu = () => {
+ let selected = this._createAliasSelected;
+ return (
+ <div className="collectionStackingView-optionPicker">
+ <div className="optionOptions">
+ <div className={"optionPicker" + (selected === true ? " active" : "")} onClick={this.toggleAlias}>Create Alias</div>
+ </div>
+ </div >
+ );
+ }
+
+ @observable private collapsed: boolean = false;
+
+ private toggleVisibility = action(() => {
+ this.collapsed = !this.collapsed;
+ });
+
+ @observable _headingsHack: number = 1;
+
render() {
let cols = this.props.cols();
let key = StrCast(this.props.parent.props.Document.sectionFilter);
@@ -254,20 +275,29 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
GetValue: () => evContents,
SetValue: this.headingChanged,
contents: evContents,
- oneLine: true
+ oneLine: true,
+ HeadingObject: this.props.headingObject,
+ HeadingsHack: this._headingsHack,
+ toggle: this.toggleVisibility,
+ color: this._color
};
let newEditableViewProps = {
GetValue: () => "",
SetValue: this.addDocument,
- contents: "+ NEW"
+ contents: "+ NEW",
+ HeadingObject: this.props.headingObject,
+ HeadingsHack: this._headingsHack,
+ toggle: this.toggleVisibility,
+ color: this._color
};
let headingView = this.props.headingObject ?
<div key={heading} className="collectionStackingView-sectionHeader" ref={this._headerRef}
style={{
width: (style.columnWidth) /
((uniqueHeadings.length +
- ((this.props.parent.props.ContainingCollectionDoc && this.props.parent.props.ContainingCollectionDoc.chromeStatus !== 'view-mode' && this.props.parent.props.ContainingCollectionDoc.chromeStatus !== 'disabled') ? 1 : 0)) || 1)
+ ((this.props.parent.props.Document.chromeStatus !== 'view-mode' && this.props.parent.props.Document.chromeStatus !== 'disabled') ? 1 : 0)) || 1)
}}>
+ <div className={"collectionStackingView-collapseBar" + (this.props.headingObject.collapsed === true ? " active" : "")} onClick={this.collapseSection}></div>
{/* the default bucket (no key value) has a tooltip that describes what it is.
Further, it does not have a color and cannot be deleted. */}
<div className="collectionStackingView-sectionHeader-subCont" onPointerDown={this.headerDown}
@@ -281,9 +311,9 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
<EditableView {...headerEditableViewProps} />
{evContents === `NO ${key.toUpperCase()} VALUE` ? (null) :
<div className="collectionStackingView-sectionColor">
- <Flyout anchorPoint={anchorPoints.TOP_CENTER} content={this.renderColorPicker()}>
+ <Flyout anchorPoint={anchorPoints.CENTER_RIGHT} content={this.renderColorPicker()}>
<button className="collectionStackingView-sectionColorButton">
- <FontAwesomeIcon icon="palette" size="sm" />
+ <FontAwesomeIcon icon="palette" size="lg" />
</button>
</ Flyout >
</div>
@@ -291,36 +321,50 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
{evContents === `NO ${key.toUpperCase()} VALUE` ?
(null) :
<button className="collectionStackingView-sectionDelete" onClick={this.deleteColumn}>
- <FontAwesomeIcon icon="trash" />
+ <FontAwesomeIcon icon="trash" size="lg" />
</button>}
+ {evContents === `NO ${key.toUpperCase()} VALUE` ? (null) :
+ <div className="collectionStackingView-sectionOptions">
+ <Flyout anchorPoint={anchorPoints.TOP_RIGHT} content={this.renderMenu()}>
+ <button className="collectionStackingView-sectionOptionButton">
+ <FontAwesomeIcon icon="ellipsis-v" size="lg"></FontAwesomeIcon>
+ </button>
+ </Flyout>
+ </div>
+ }
</div>
</div> : (null);
for (let i = 0; i < cols; i++) templatecols += `${style.columnWidth / style.numGroupColumns}px `;
+ let chromeStatus = this.props.parent.props.Document.chromeStatus;
return (
- <div className="collectionStackingViewFieldColumn" key={heading} style={{ width: `${100 / ((uniqueHeadings.length + ((this.props.parent.props.ContainingCollectionDoc && this.props.parent.props.ContainingCollectionDoc.chromeStatus !== 'view-mode' && this.props.parent.props.ContainingCollectionDoc.chromeStatus !== 'disabled') ? 1 : 0)) || 1)}%`, background: this._background }}
+ <div className="collectionStackingViewFieldColumn" key={heading} style={{ width: `${100 / ((uniqueHeadings.length + ((chromeStatus !== 'view-mode' && chromeStatus !== 'disabled') ? 1 : 0)) || 1)}%`, background: this._background }}
ref={this.createColumnDropRef} onPointerEnter={this.pointerEntered} onPointerLeave={this.pointerLeave}>
- {headingView}
- <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`,
- margin: "auto",
- width: "max-content", //singleColumn ? undefined : `${cols * (style.columnWidth + style.gridGap) + 2 * style.xMargin - style.gridGap}px`,
- height: 'max-content',
- position: "relative",
- gridGap: style.gridGap,
- gridTemplateColumns: singleColumn ? undefined : templatecols,
- gridAutoRows: singleColumn ? undefined : "0px"
- }}
- >
- {this.children(this.props.docList)}
- {singleColumn ? (null) : this.props.parent.columnDragger}
- </div>
- {(this.props.parent.props.ContainingCollectionDoc && this.props.parent.props.ContainingCollectionDoc.chromeStatus !== 'view-mode' && this.props.parent.props.ContainingCollectionDoc.chromeStatus !== 'disabled') ?
- <div key={`${heading}-add-document`} className="collectionStackingView-addDocumentButton"
- style={{ width: style.columnWidth / style.numGroupColumns }}>
- <EditableView {...newEditableViewProps} />
- </div> : null}
- </div>
+ {this.props.parent.Document.hideHeadings ? (null) : headingView}
+ {
+ this.collapsed ? (null) :
+ <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`,
+ margin: "auto",
+ width: "max-content", //singleColumn ? undefined : `${cols * (style.columnWidth + style.gridGap) + 2 * style.xMargin - style.gridGap}px`,
+ height: 'max-content',
+ position: "relative",
+ gridGap: style.gridGap,
+ gridTemplateColumns: singleColumn ? undefined : templatecols,
+ gridAutoRows: singleColumn ? undefined : "0px"
+ }}>
+ {this.props.parent.children(this.props.docList)}
+ {singleColumn ? (null) : this.props.parent.columnDragger}
+ </div>
+ {(chromeStatus !== 'view-mode' && chromeStatus !== 'disabled') ?
+ <div key={`${heading}-add-document`} className="collectionStackingView-addDocumentButton"
+ style={{ width: style.columnWidth / style.numGroupColumns }}>
+ <EditableView {...newEditableViewProps} />
+ </div> : null}
+ </div>
+ }
+ </div >
);
}
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 069269b06..a2675c9a3 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -6,7 +6,7 @@ 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 { RouteStore } from "../../../server/RouteStore";
import { Utils } from "../../../Utils";
@@ -17,61 +17,74 @@ import { DragManager } from "../../util/DragManager";
import { undoBatch, UndoManager } from "../../util/UndoManager";
import { FieldViewProps } from "../nodes/FieldView";
import { FormattedTextBox, GoogleRef } from "../nodes/FormattedTextBox";
-import { CollectionPDFView } from "./CollectionPDFView";
-import { CollectionVideoView } from "./CollectionVideoView";
import { CollectionView } from "./CollectionView";
import React = require("react");
import { DocComponent } from "../DocComponent";
+var path = require('path');
+import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils";
+import { ImageUtils } from "../../util/Import & Export/ImageUtils";
export interface CollectionViewProps extends FieldViewProps {
- addDocument: (document: Doc, allowDuplicates?: boolean) => boolean;
+ addDocument: (document: Doc) => boolean;
removeDocument: (document: Doc) => boolean;
moveDocument: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
PanelWidth: () => number;
PanelHeight: () => number;
+ VisibleHeight?: () => number;
chromeCollapsed: boolean;
setPreviewCursor?: (func: (x: number, y: number, drag: boolean) => void) => void;
+ fieldKey: string;
}
export interface SubCollectionViewProps extends CollectionViewProps {
- CollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
+ CollectionView: Opt<CollectionView>;
ruleProvider: Doc | undefined;
+ children?: never | (() => JSX.Element[]) | React.ReactNode;
+ isAnnotationOverlay?: boolean;
+ annotationsKey: string;
}
export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
class CollectionSubView extends DocComponent<SubCollectionViewProps, T>(schemaCtor) {
private dropDisposer?: DragManager.DragDropDisposer;
private _childLayoutDisposer?: IReactionDisposer;
-
- protected createDropTarget = (ele: HTMLDivElement) => {
+ protected createDropTarget = (ele: HTMLDivElement) => { //used for stacking and masonry view
this.dropDisposer && this.dropDisposer();
if (ele) {
this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } });
}
}
- protected CreateDropTarget(ele: HTMLDivElement) {
+ protected CreateDropTarget(ele: HTMLDivElement) { //used in schema view
this.createDropTarget(ele);
}
componentDidMount() {
this._childLayoutDisposer = reaction(() => [this.childDocs, Cast(this.props.Document.childLayout, Doc)],
- async (args) => args[1] instanceof Doc &&
- this.childDocs.map(async doc => !Doc.AreProtosEqual(args[1] as Doc, (await doc).layout as Doc) && Doc.ApplyTemplateTo(args[1] as Doc, (await doc))));
+ async (args) => {
+ if (args[1] instanceof Doc) {
+ this.childDocs.map(async doc => !Doc.AreProtosEqual(args[1] as Doc, (await doc).layout as Doc) && Doc.ApplyTemplateTo(args[1] as Doc, (await doc), "layoutFromParent"));
+ }
+ else if (!(args[1] instanceof Promise)) {
+ this.childDocs.filter(d => !d.isTemplateField).map(async doc => doc.layoutKey === "layoutFromParent" && (doc.layoutKey = "layout"));
+ }
+ });
}
componentWillUnmount() {
this._childLayoutDisposer && this._childLayoutDisposer();
}
- // The data field for rendeing this collection will be on the this.props.Document unless we're rendering a template in which case we try to use props.DataDoc.
+ @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplateField ? Doc.GetProto(this.props.DataDoc) : Doc.GetProto(this.props.Document); }
+ @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); }
+
+ // 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.
- // The name of the data field comes from fieldExt if it's an extension, or fieldKey otherwise.
+ // 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 Doc.fieldExtensionDoc(this.props.Document.isTemplate && this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, this.props.fieldExt)[this.props.fieldExt || this.props.fieldKey];
+ return this.props.annotationsKey ? (this.extensionDoc ? this.extensionDoc[this.props.annotationsKey] : undefined) : this.dataDoc[this.props.fieldKey];
}
-
get childLayoutPairs() {
return this.childDocs.map(cd => Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, cd)).filter(pair => pair.layout).map(pair => ({ layout: pair.layout!, data: pair.data! }));
}
@@ -119,11 +132,12 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
@undoBatch
@action
protected drop(e: Event, de: DragManager.DropEvent): boolean {
+ (this.props.Document.dropConverter instanceof ScriptField) &&
+ this.props.Document.dropConverter.script.run({ dragData: de.data });
if (de.data instanceof DragManager.DocumentDragData && !de.data.applyAsTemplate) {
if (de.mods === "AltKey" && de.data.draggedDocuments.length) {
this.childDocs.map(doc =>
- Doc.ApplyTemplateTo(de.data.draggedDocuments[0], doc)
- );
+ Doc.ApplyTemplateTo(de.data.draggedDocuments[0], doc, "layoutFromParent"));
e.stopPropagation();
return true;
}
@@ -134,11 +148,10 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
return moved || added;
}, false);
} else if (de.data.moveDocument) {
- let movedDocs = de.data.draggedDocuments;// de.data.options === this.props.Document[Id] ? de.data.draggedDocuments : de.data.droppedDocuments;
- // note that it's possible the drag function might create a drop document that's not the same as the
- // original dragged document. So we explicitly call addDocument() with a droppedDocument and
+ let movedDocs = de.data.draggedDocuments;
added = movedDocs.reduce((added: boolean, d, i) =>
- de.data.moveDocument(d, this.props.Document, (doc: Doc) => this.props.addDocument(de.data.droppedDocuments[i])) || added, false);
+ de.data.droppedDocuments[i] !== d ? this.props.addDocument(de.data.droppedDocuments[i]) :
+ de.data.moveDocument(d, this.props.Document, this.props.addDocument) || added, false);
} else {
added = de.data.droppedDocuments.reduce((added: boolean, d) => {
let moved = this.props.addDocument(d);
@@ -157,7 +170,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
@undoBatch
@action
- protected onDrop(e: React.DragEvent, options: DocumentOptions, completed?: () => void) {
+ protected async onDrop(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;
@@ -179,14 +192,14 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
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, false);
+ (f instanceof Doc) && this.props.addDocument(f);
}
});
} else {
this.props.addDocument && this.props.addDocument(Docs.Create.WebDocument(href, options));
}
} else if (text) {
- this.props.addDocument && this.props.addDocument(Docs.Create.TextDocument({ ...options, width: 100, height: 25, documentText: "@@@" + text }), false);
+ this.props.addDocument && this.props.addDocument(Docs.Create.TextDocument({ ...options, width: 100, height: 25, documentText: "@@@" + text }));
}
return;
}
@@ -197,7 +210,8 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
if (img) {
let split = img.split("src=\"")[1].split("\"")[0];
let doc = Docs.Create.ImageDocument(split, { ...options, width: 300 });
- this.props.addDocument(doc, false);
+ ImageUtils.ExtractExif(doc);
+ this.props.addDocument(doc);
return;
} else {
let path = window.location.origin + "/doc/";
@@ -206,12 +220,12 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
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, false);
+ (f instanceof Doc) && this.props.addDocument(f);
}
});
} else {
let htmlDoc = Docs.Create.HtmlDocument(html, { ...options, width: 300, height: 300, documentText: text });
- this.props.addDocument(htmlDoc, false);
+ this.props.addDocument(htmlDoc);
}
return;
}
@@ -225,13 +239,22 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
if ((matches = /(https:\/\/)?docs\.google\.com\/document\/d\/([^\\]+)\/edit/g.exec(text)) !== null) {
let newBox = Docs.Create.TextDocument({ ...options, width: 400, height: 200, title: "Awaiting title from Google Docs..." });
let proto = newBox.proto!;
- proto.autoHeight = true;
- proto[GoogleRef] = matches[2];
+ 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]();
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);
+ }
let batch = UndoManager.StartBatch("collection view drop");
let promises: Promise<void>[] = [];
// tslint:disable-next-line:prefer-for-of
@@ -246,7 +269,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
let type = result["content-type"];
if (type) {
Docs.Get.DocumentFromType(type, str, { ...options, width: 300, nativeWidth: type.indexOf("video") !== -1 ? 600 : 300 })
- .then(doc => doc && this.props.addDocument(doc, false));
+ .then(doc => doc && this.props.addDocument(doc));
}
});
promises.push(prom);
@@ -267,8 +290,11 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
}).then(async (res: Response) => {
(await res.json()).map(action((file: any) => {
let full = { ...options, nativeWidth: type.indexOf("video") !== -1 ? 600 : 300, width: 300, title: dropFileName };
- let path = Utils.prepend(file);
- Docs.Get.DocumentFromType(type, path, full).then(doc => doc && this.props.addDocument(doc));
+ let pathname = Utils.prepend(file.path);
+ Docs.Get.DocumentFromType(type, pathname, full).then(doc => {
+ doc && (Doc.GetProto(doc).fileUpload = path.basename(pathname).replace("upload_", "").replace(/\.[a-z0-9]*$/, ""));
+ doc && this.props.addDocument(doc);
+ });
}));
});
promises.push(prom);
diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss
index 197e57808..7d0c900a6 100644
--- a/src/client/views/collections/CollectionTreeView.scss
+++ b/src/client/views/collections/CollectionTreeView.scss
@@ -8,14 +8,14 @@
box-sizing: border-box;
height: 100%;
width:100%;
- position: absolute;
+ position: relative;
top:0;
- padding-top: 20px;
padding-left: 10px;
- padding-right: 0px;
+ padding-right: 10px;
background: $light-color-secondary;
font-size: 13px;
overflow: auto;
+ cursor: default;
ul {
list-style: none;
@@ -51,7 +51,7 @@
.editableView-container-editing {
display: block;
text-overflow: ellipsis;
- font-size: 24px;
+ font-size: 1vw;
white-space: nowrap;
}
}
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index e5313f68c..8726726bb 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -1,14 +1,15 @@
import { library } from '@fortawesome/fontawesome-svg-core';
-import { faAngleRight, faCamera, faExpand, faTrash, faBell, faCaretDown, faCaretRight, faArrowsAltH, faCaretSquareDown, faCaretSquareRight, faTrashAlt, faPlus, faMinus } from '@fortawesome/free-solid-svg-icons';
+import { faAngleRight, faArrowsAltH, faBell, faCamera, faCaretDown, faCaretRight, faCaretSquareDown, faCaretSquareRight, faExpand, faMinus, faPlus, faTrash, faTrashAlt } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, observable, trace, untracked } from "mobx";
+import { action, computed, observable } from "mobx";
import { observer } from "mobx-react";
-import { Doc, DocListCast, HeightSym, WidthSym, Opt, Field } from '../../../new_fields/Doc';
+import { Doc, DocListCast, Field, HeightSym, Opt, WidthSym } from '../../../new_fields/Doc';
import { Id } from '../../../new_fields/FieldSymbols';
import { List } from '../../../new_fields/List';
import { Document, listSpec } from '../../../new_fields/Schema';
+import { ComputedField, ScriptField } from '../../../new_fields/ScriptField';
import { BoolCast, Cast, NumCast, StrCast } from '../../../new_fields/Types';
-import { emptyFunction, Utils } from '../../../Utils';
+import { emptyFunction, Utils, returnFalse } from '../../../Utils';
import { Docs, DocUtils } from '../../documents/Documents';
import { DocumentType } from "../../documents/DocumentTypes";
import { DocumentManager } from '../../util/DocumentManager';
@@ -17,18 +18,17 @@ import { SelectionManager } from '../../util/SelectionManager';
import { Transform } from '../../util/Transform';
import { undoBatch } from '../../util/UndoManager';
import { ContextMenu } from '../ContextMenu';
+import { ContextMenuProps } from '../ContextMenuItem';
import { EditableView } from "../EditableView";
import { MainView } from '../MainView';
+import { KeyValueBox } from '../nodes/KeyValueBox';
import { Templates } from '../Templates';
-import { CollectionViewType } from './CollectionBaseView';
-import { CollectionDockingView } from './CollectionDockingView';
-import { CollectionSchemaPreview } from './CollectionSchemaView';
+import { CollectionViewType } from './CollectionView';
+import { ContentFittingDocumentView } from '../nodes/ContentFittingDocumentView';
import { CollectionSubView } from "./CollectionSubView";
import "./CollectionTreeView.scss";
import React = require("react");
-import { ComputedField, ScriptField } from '../../../new_fields/ScriptField';
-import { KeyValueBox } from '../nodes/KeyValueBox';
-import { ContextMenuProps } from '../ContextMenuItem';
+import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils';
export interface TreeViewProps {
@@ -82,56 +82,46 @@ class TreeView extends React.Component<TreeViewProps> {
private _header?: React.RefObject<HTMLDivElement> = React.createRef();
private _treedropDisposer?: DragManager.DragDropDisposer;
private _dref = React.createRef<HTMLDivElement>();
- get defaultExpandedView() { return this.childDocs ? this.fieldKey : "fields"; }
+ 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
- @computed get treeViewOpen() { return (BoolCast(this.props.document.treeViewOpen) && !this.props.preventTreeViewOpen) || this._overrideTreeViewOpen; }
set treeViewOpen(c: boolean) { if (this.props.preventTreeViewOpen) this._overrideTreeViewOpen = c; else this.props.document.treeViewOpen = c; }
+ @computed get treeViewOpen() { return (BoolCast(this.props.document.treeViewOpen) && !this.props.preventTreeViewOpen) || 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.resolvedDataDoc ? this.resolvedDataDoc : this.props.document; }
+ @computed get dataDoc() { return this.templateDataDoc ? this.templateDataDoc : this.props.document; }
@computed get fieldKey() {
- let splits = StrCast(this.props.document.layout).split("fieldKey={\"");
+ let splits = StrCast(Doc.LayoutField(this.props.document)).split("fieldKey={\"");
return splits.length > 1 ? splits[1].split("\"")[0] : "data";
}
- @computed get childDocs() {
- let layout = this.props.document.layout instanceof Doc ? this.props.document.layout : undefined;
- return (this.props.dataDoc ? Cast(this.props.dataDoc[this.fieldKey], listSpec(Doc)) : undefined) ||
- (layout ? Cast(layout[this.fieldKey], listSpec(Doc)) : undefined) ||
- Cast(this.props.document[this.fieldKey], listSpec(Doc));
- }
- @computed get childLinks() {
- let layout = this.props.document.layout instanceof Doc ? this.props.document.layout : undefined;
- return (this.props.dataDoc ? Cast(this.props.dataDoc.links, listSpec(Doc)) : undefined) ||
- (layout instanceof Doc ? Cast(layout.links, listSpec(Doc)) : undefined) ||
- Cast(this.props.document.links, listSpec(Doc));
+ childDocList(field: string) {
+ let layout = Doc.LayoutField(this.props.document) instanceof Doc ? Doc.LayoutField(this.props.document) as Doc : undefined;
+ return ((this.props.dataDoc ? Cast(this.props.dataDoc[field], listSpec(Doc)) : undefined) ||
+ (layout ? Cast(layout[field], listSpec(Doc)) : undefined) ||
+ Cast(this.props.document[field], listSpec(Doc))) as Doc[];
}
- @computed get resolvedDataDoc() {
- if (this.props.dataDoc === undefined && this.props.document.layout instanceof Doc) {
- // if there is no dataDoc (ie, we're not rendering a template layout), but this document
- // has a template layout document, then we will render the template layout but use
- // this document as the data document for the layout.
+ @computed get childDocs() { return this.childDocList(this.fieldKey); }
+ @computed get childLinks() { return this.childDocList("links"); }
+ @computed get templateDataDoc() {
+ if (this.props.dataDoc === undefined && 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.
return this.props.document;
}
return this.props.dataDoc;
}
@computed get boundsOfCollectionDocument() {
return StrCast(this.props.document.type).indexOf(DocumentType.COL) === -1 ? undefined :
- Doc.ComputeContentBounds(DocListCast(this.props.document.data));
+ Doc.ComputeContentBounds(DocListCast(this.props.document[this.fieldKey]));
}
- @undoBatch delete = () => this.props.deleteDoc(this.dataDoc);
- @undoBatch openRight = () => this.props.addDocTab(this.props.document, undefined, "onRight");
+ @undoBatch delete = () => this.props.deleteDoc(this.props.document);
+ @undoBatch openRight = () => this.props.addDocTab(this.props.document, this.templateDataDoc, "onRight");
@undoBatch indent = () => this.props.addDocument(this.props.document) && this.delete();
@undoBatch move = (doc: Doc, target: Doc, addDoc: (doc: Doc) => boolean) => {
return this.props.document !== target && this.props.deleteDoc(doc) && addDoc(doc);
}
- @undoBatch @action remove = (document: Document, key: string): boolean => {
- let children = Cast(this.dataDoc[key], listSpec(Doc), []);
- if (children.indexOf(document) !== -1) {
- children.splice(children.indexOf(document), 1);
- return true;
- }
- return false;
+ @undoBatch @action remove = (document: Document, key: string) => {
+ return Doc.RemoveDocFromList(this.dataDoc, key, document);
}
protected createTreeDropTarget = (ele: HTMLDivElement) => {
@@ -175,9 +165,9 @@ class TreeView extends React.Component<TreeViewProps> {
fontStyle={style}
fontSize={12}
GetValue={() => StrCast(this.props.document[key])}
- SetValue={undoBatch((value: string) => (Doc.GetProto(this.dataDoc)[key] = value) ? true : true)}
+ SetValue={undoBatch((value: string) => Doc.SetInPlace(this.props.document, key, value, false) || true)}
OnFillDown={undoBatch((value: string) => {
- Doc.GetProto(this.dataDoc)[key] = value;
+ Doc.SetInPlace(this.props.document, key, value, false);
let doc = this.props.document.layoutCustom instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.document.layoutCustom)) : undefined;
if (!doc) doc = Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) });
TreeView.loadId = doc[Id];
@@ -188,17 +178,18 @@ class TreeView extends React.Component<TreeViewProps> {
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 view7
- if (NumCast(this.props.document.viewType) !== CollectionViewType.Docking) {
+ if (NumCast(this.props.document.viewType) !== CollectionViewType.Docking && 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.resolvedDataDoc, "inTab"), icon: "folder" });
- ContextMenu.Instance.addItem({ description: "Open Right", event: () => this.props.addDocTab(this.props.document, this.resolvedDataDoc, "onRight"), icon: "caret-square-right" });
+ ContextMenu.Instance.addItem({ description: "Open Tab", event: () => this.props.addDocTab(this.props.document, this.templateDataDoc, "inTab"), icon: "folder" });
+ ContextMenu.Instance.addItem({ description: "Open Right", event: () => this.props.addDocTab(this.props.document, this.templateDataDoc, "onRight"), icon: "caret-square-right" });
if (DocumentManager.Instance.getDocumentViews(this.dataDoc).length) {
- ContextMenu.Instance.addItem({ description: "Focus", event: () => DocumentManager.Instance.getDocumentViews(this.dataDoc).map(view => view.props.focus(this.props.document, true)), icon: "camera" });
+ ContextMenu.Instance.addItem({ description: "Focus", event: () => (view => view && view.props.focus(this.props.document, true))(DocumentManager.Instance.getFirstDocumentView(this.dataDoc)), icon: "camera" });
}
ContextMenu.Instance.addItem({ description: "Delete Item", event: () => this.props.deleteDoc(this.props.document), icon: "trash-alt" });
} else {
ContextMenu.Instance.addItem({ description: "Open as Workspace", event: () => MainView.Instance.openWorkspace(this.dataDoc), icon: "caret-square-right" });
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: () => { let 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: "Publish", event: () => DocUtils.Publish(this.props.document, StrCast(this.props.document.title), () => { }, () => { }), icon: "file" });
@@ -218,25 +209,22 @@ class TreeView extends React.Component<TreeViewProps> {
if (de.data instanceof DragManager.LinkDragData) {
let sourceDoc = de.data.linkSourceDocument;
let destDoc = this.props.document;
- DocUtils.MakeLink(sourceDoc, destDoc);
+ DocUtils.MakeLink({ doc: sourceDoc }, { doc: destDoc });
e.stopPropagation();
}
if (de.data instanceof DragManager.DocumentDragData) {
e.stopPropagation();
if (de.data.draggedDocuments[0] === this.props.document) return true;
- let addDoc = (doc: Doc) => this.props.addDocument(doc, this.resolvedDataDoc, before);
+ let addDoc = (doc: Doc) => this.props.addDocument(doc, undefined, before);
if (inside) {
- let docList = Cast(this.dataDoc.data, listSpec(Doc));
- if (docList !== undefined) {
- addDoc = (doc: Doc) => { docList && docList.push(doc); return true; };
- }
+ addDoc = (doc: Doc) => Doc.AddDocToList(this.dataDoc, this.fieldKey, doc) || addDoc(doc);
}
let movedDocs = (de.data.options === this.props.treeViewId ? de.data.draggedDocuments : de.data.droppedDocuments);
return (de.data.dropAction || de.data.userDropAction) ?
- de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d, this.resolvedDataDoc, before) || added, false)
- : (de.data.moveDocument) ?
- movedDocs.reduce((added: boolean, d) => de.data.moveDocument(d, this.resolvedDataDoc, addDoc) || added, false)
- : de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d, this.resolvedDataDoc, before), false);
+ de.data.droppedDocuments.reduce((added, d) => addDoc(d) || added, false)
+ : de.data.moveDocument ?
+ movedDocs.reduce((added, d) => de.data.moveDocument(d, undefined, addDoc) || added, false)
+ : de.data.droppedDocuments.reduce((added, d) => addDoc(d), false);
}
return false;
}
@@ -249,17 +237,22 @@ class TreeView extends React.Component<TreeViewProps> {
return finalXf;
}
docWidth = () => {
- let aspect = NumCast(this.props.document.nativeHeight) / NumCast(this.props.document.nativeWidth);
- if (aspect) return Math.min(this.props.document[WidthSym](), Math.min(this.MAX_EMBED_HEIGHT / aspect, this.props.panelWidth() - 5));
- return NumCast(this.props.document.nativeWidth) ? Math.min(this.props.document[WidthSym](), this.props.panelWidth() - 5) : this.props.panelWidth() - 5;
+ let layoutDoc = Doc.Layout(this.props.document);
+ let aspect = NumCast(layoutDoc.nativeHeight) / NumCast(layoutDoc.nativeWidth);
+ if (aspect) return Math.min(layoutDoc[WidthSym](), Math.min(this.MAX_EMBED_HEIGHT / aspect, this.props.panelWidth() - 20));
+ return NumCast(layoutDoc.nativeWidth) ? Math.min(layoutDoc[WidthSym](), this.props.panelWidth() - 20) : this.props.panelWidth() - 20;
}
docHeight = () => {
+ let layoutDoc = Doc.Layout(this.props.document);
let bounds = this.boundsOfCollectionDocument;
return Math.min(this.MAX_EMBED_HEIGHT, (() => {
- let aspect = NumCast(this.props.document.nativeHeight) / NumCast(this.props.document.nativeWidth);
+ let aspect = NumCast(layoutDoc.nativeHeight) / NumCast(layoutDoc.nativeWidth, 1);
if (aspect) return this.docWidth() * aspect;
if (bounds) return this.docWidth() * (bounds.b - bounds.y) / (bounds.r - bounds.x);
- return NumCast(this.props.document.height) ? NumCast(this.props.document.height) : 50;
+ return layoutDoc.fitWidth ? (!this.props.document.nativeHeight ? NumCast(this.props.containingCollection.height) :
+ Math.min(this.docWidth() * NumCast(layoutDoc.scrollHeight, NumCast(layoutDoc.nativeHeight)) / NumCast(layoutDoc.nativeWidth,
+ NumCast(this.props.containingCollection.height)))) :
+ NumCast(layoutDoc.height) ? NumCast(layoutDoc.height) : 50;
})());
}
@@ -308,39 +301,40 @@ class TreeView extends React.Component<TreeViewProps> {
let docs = expandKey === "links" ? this.childLinks : this.childDocs;
return <ul key={expandKey + "more"}>
{!docs ? (null) :
- TreeView.GetChildElements(docs as Doc[], this.props.treeViewId, this.props.document.layout as Doc,
- this.resolvedDataDoc, expandKey, addDoc, remDoc, this.move,
+ TreeView.GetChildElements(docs, this.props.treeViewId, Doc.Layout(this.props.document),
+ this.templateDataDoc, expandKey, 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.renderDepth, this.props.showHeaderFields, this.props.preventTreeViewOpen,
[...this.props.renderedIds, this.props.document[Id]])}
</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}>
- {this.dataDoc ? this.expandedField(this.dataDoc) : (null)}
+ {this.expandedField(this.props.document)}
</div></ul>;
} else {
- let layoutDoc = this.props.document;
+ let layoutDoc = Doc.Layout(this.props.document);
return <div ref={this._dref} style={{ display: "inline-block", height: this.docHeight() }} key={this.props.document[Id] + this.props.document.title}>
- <CollectionSchemaPreview
+ <ContentFittingDocumentView
Document={layoutDoc}
- DataDocument={this.resolvedDataDoc}
+ DataDocument={this.templateDataDoc}
+ fieldKey={this.fieldKey}
renderDepth={this.props.renderDepth}
showOverlays={this.noOverlays}
ruleProvider={this.props.document.isRuleProvider && layoutDoc.type !== DocumentType.TEXT ? this.props.document : this.props.ruleProvider}
fitToBox={this.boundsOfCollectionDocument !== undefined}
- width={this.docWidth}
- height={this.docHeight}
+ PanelWidth={this.docWidth}
+ PanelHeight={this.docHeight}
getTransform={this.docTransform}
+ CollectionDoc={this.props.containingCollection}
CollectionView={undefined}
- addDocument={emptyFunction as any}
+ addDocument={returnFalse}
moveDocument={this.props.moveDocument}
- removeDocument={emptyFunction as any}
+ removeDocument={returnFalse}
active={this.props.active}
- whenActiveChanged={emptyFunction as any}
+ whenActiveChanged={emptyFunction}
addDocTab={this.props.addDocTab}
pinToPres={this.props.pinToPres}
- setPreviewScript={emptyFunction}>
- </CollectionSchemaPreview>
+ setPreviewScript={emptyFunction} />
</div>;
}
}
@@ -364,7 +358,7 @@ class TreeView extends React.Component<TreeViewProps> {
onPointerDown={action(() => {
if (this.treeViewOpen) {
this.props.document.treeViewExpandedView = this.treeViewExpandedView === this.fieldKey ? "fields" :
- this.treeViewExpandedView === "fields" && this.props.document.layout ? "layout" :
+ this.treeViewExpandedView === "fields" && Doc.Layout(this.props.document) ? "layout" :
this.treeViewExpandedView === "layout" && this.props.document.links ? "links" :
this.childDocs ? this.fieldKey : "fields";
}
@@ -456,7 +450,7 @@ class TreeView extends React.Component<TreeViewProps> {
let rowWidth = () => panelWidth() - 20;
return docs.map((child, i) => {
- let pair = Doc.GetLayoutDataDocPair(containingCollection, dataDoc, key, child);
+ const pair = Doc.GetLayoutDataDocPair(containingCollection, dataDoc, key, child);
if (!pair.layout || pair.data instanceof Promise) {
return (null);
}
@@ -475,11 +469,12 @@ class TreeView extends React.Component<TreeViewProps> {
let addDocument = (doc: Doc, relativeTo?: Doc, before?: boolean) => {
return add(doc, relativeTo ? relativeTo : docs[i], before !== undefined ? before : false);
};
+ const childLayout = Doc.Layout(pair.layout);
let rowHeight = () => {
- let aspect = NumCast(child.nativeWidth, 0) / NumCast(child.nativeHeight, 0);
- return aspect ? Math.min(child[WidthSym](), rowWidth()) / aspect : child[HeightSym]();
+ let aspect = NumCast(childLayout.nativeWidth, 0) / NumCast(childLayout.nativeHeight, 0);
+ return aspect ? Math.min(childLayout[WidthSym](), rowWidth()) / aspect : childLayout[HeightSym]();
};
- return <TreeView
+ return !(child instanceof Doc) ? (null) : <TreeView
document={pair.layout}
dataDoc={pair.data}
containingCollection={containingCollection}
@@ -512,9 +507,7 @@ export class CollectionTreeView extends CollectionSubView(Document) {
private treedropDisposer?: DragManager.DragDropDisposer;
private _mainEle?: HTMLDivElement;
- @observable static NotifsCol: Opt<Doc>;
-
- @computed get resolvedDataDoc() { return BoolCast(this.props.Document.isTemplate) && this.props.DataDoc ? this.props.DataDoc : this.props.Document; }
+ @computed get dataDoc() { return this.props.DataDoc || this.props.Document; }
protected createTreeDropTarget = (ele: HTMLDivElement) => {
this.treedropDisposer && this.treedropDisposer();
@@ -524,6 +517,7 @@ export class CollectionTreeView extends CollectionSubView(Document) {
}
componentWillUnmount() {
+ super.componentWillUnmount();
this.treedropDisposer && this.treedropDisposer();
}
@@ -538,7 +532,7 @@ export class CollectionTreeView extends CollectionSubView(Document) {
}
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.workspaceLibrary) {
+ if (!e.isPropagationStopped() && this.props.Document === CurrentUserUtils.UserDocument.workspaces) {
ContextMenu.Instance.addItem({ description: "Create Workspace", event: () => MainView.Instance.createNewWorkspace(), icon: "plus" });
ContextMenu.Instance.addItem({ description: "Delete Workspace", event: () => this.remove(this.props.Document), icon: "minus" });
e.stopPropagation();
@@ -552,31 +546,10 @@ export class CollectionTreeView extends CollectionSubView(Document) {
}
outerXf = () => Utils.GetScreenTransform(this._mainEle!);
onTreeDrop = (e: React.DragEvent) => this.onDrop(e, {});
- openNotifsCol = () => {
- if (CollectionTreeView.NotifsCol) {
- this.props.addDocTab(CollectionTreeView.NotifsCol, undefined, "onRight");
- }
- }
- @computed get renderNotifsButton() {
- const length = CollectionTreeView.NotifsCol ? DocListCast(CollectionTreeView.NotifsCol.data).length : 0;
- const notifsRef = React.createRef<HTMLDivElement>();
- const dragNotifs = action(() => CollectionTreeView.NotifsCol!);
- return <div id="toolbar" key="toolbar">
- <div ref={notifsRef}>
- <button className="toolbar-button round-button" title="Notifs"
- onClick={this.openNotifsCol} onPointerDown={CollectionTreeView.NotifsCol ? SetupDrag(notifsRef, dragNotifs) : emptyFunction}>
- <FontAwesomeIcon icon={faBell} size="sm" />
- </button>
- <div className="main-notifs-badge" style={length > 0 ? { "display": "initial" } : { "display": "none" }}>
- {length}
- </div>
- </div>
- </div >;
- }
@computed get renderClearButton() {
return <div id="toolbar" key="toolbar">
- <button className="toolbar-button round-button" title="Notifs"
+ <button className="toolbar-button round-button" title="Empty"
onClick={undoBatch(action(() => Doc.GetProto(this.props.Document)[this.props.fieldKey] = undefined))}>
<FontAwesomeIcon icon={faTrash} size="sm" />
</button>
@@ -584,38 +557,36 @@ export class CollectionTreeView extends CollectionSubView(Document) {
}
render() {
- Doc.UpdateDocumentExtensionForField(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey);
let dropAction = StrCast(this.props.Document.dropAction) as dropActionType;
let addDoc = (doc: Doc, relativeTo?: Doc, before?: boolean) => Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, relativeTo, before, false, false, false);
let moveDoc = (d: Doc, target: Doc, addDoc: (doc: Doc) => boolean) => this.props.moveDocument(d, target, addDoc);
return !this.childDocs ? (null) : (
<div id="body" className="collectionTreeView-dropTarget"
- style={{ overflow: "auto", background: StrCast(this.props.Document.backgroundColor, "lightgray") }}
+ style={{ overflow: "auto", background: StrCast(this.props.Document.backgroundColor, "lightgray"), paddingTop: `${NumCast(this.props.Document.yMargin, 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}>
<EditableView
- contents={this.resolvedDataDoc.title}
+ contents={this.dataDoc.title}
display={"block"}
maxHeight={72}
height={"auto"}
- GetValue={() => StrCast(this.resolvedDataDoc.title)}
- SetValue={undoBatch((value: string) => (Doc.GetProto(this.resolvedDataDoc).title = value) ? true : true)}
+ GetValue={() => StrCast(this.dataDoc.title)}
+ SetValue={undoBatch((value: string) => Doc.SetInPlace(this.dataDoc, "title", value, false) || true)}
OnFillDown={undoBatch((value: string) => {
- Doc.GetProto(this.props.Document).title = value;
+ Doc.SetInPlace(this.dataDoc, "title", value, false);
let doc = this.props.Document.layoutCustom instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.Document.layoutCustom)) : undefined;
if (!doc) doc = 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);
})} />
- {this.props.Document.workspaceLibrary ? this.renderNotifsButton : (null)}
{this.props.Document.allowClear ? this.renderClearButton : (null)}
<ul className="no-indent" style={{ width: "max-content" }} >
{
TreeView.GetChildElements(this.childDocs, this.props.Document[Id], this.props.Document, this.props.DataDoc, this.props.fieldKey, addDoc, this.remove,
moveDoc, dropAction, this.props.addDocTab, this.props.pinToPres, this.props.ScreenToLocalTransform,
- this.outerXf, this.props.active, this.props.PanelWidth, this.props.renderDepth, () => this.props.Document.chromeStatus !== "disabled",
+ this.outerXf, this.props.active, this.props.PanelWidth, this.props.renderDepth, () => !this.props.Document.hideHeaderFields,
BoolCast(this.props.Document.preventTreeViewOpen), [])
}
</ul>
diff --git a/src/client/views/collections/CollectionVideoView.scss b/src/client/views/collections/CollectionVideoView.scss
deleted file mode 100644
index 509851ebb..000000000
--- a/src/client/views/collections/CollectionVideoView.scss
+++ /dev/null
@@ -1,51 +0,0 @@
-
-.collectionVideoView-cont{
- width: 100%;
- height: 100%;
- position: inherit;
- top: 0;
- left:0;
- z-index: -1;
- display:inline-table;
-}
-.collectionVideoView-time{
- color : white;
- top :25px;
- left : 25px;
- position: absolute;
- background-color: rgba(50, 50, 50, 0.2);
- transform-origin: left top;
-}
-.collectionVideoView-snapshot{
- color : white;
- top :25px;
- right : 25px;
- position: absolute;
- background-color: rgba(50, 50, 50, 0.2);
- transform-origin: left top;
-}
-.collectionVideoView-play {
- width: 25px;
- height: 20px;
- bottom: 25px;
- left : 25px;
- position: absolute;
- color : white;
- background-color: rgba(50, 50, 50, 0.2);
- border-radius: 4px;
- text-align: center;
- transform-origin: left bottom;
-}
-.collectionVideoView-full {
- width: 25px;
- height: 20px;
- bottom: 25px;
- right : 25px;
- position: absolute;
- color : white;
- background-color: rgba(50, 50, 50, 0.2);
- border-radius: 4px;
- text-align: center;
- transform-origin: right bottom;
-
-} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionVideoView.tsx b/src/client/views/collections/CollectionVideoView.tsx
deleted file mode 100644
index 5185d9d0e..000000000
--- a/src/client/views/collections/CollectionVideoView.tsx
+++ /dev/null
@@ -1,115 +0,0 @@
-import { action } from "mobx";
-import { observer } from "mobx-react";
-import { NumCast } from "../../../new_fields/Types";
-import { FieldView, FieldViewProps } from "../nodes/FieldView";
-import { VideoBox } from "../nodes/VideoBox";
-import { CollectionBaseView, CollectionRenderProps, CollectionViewType } from "./CollectionBaseView";
-import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView";
-import "./CollectionVideoView.scss";
-import React = require("react");
-import { InkingControl } from "../InkingControl";
-import { InkTool } from "../../../new_fields/InkField";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-
-
-@observer
-export class CollectionVideoView extends React.Component<FieldViewProps> {
- private _videoBox?: VideoBox;
-
- public static LayoutString(fieldKey: string = "data", fieldExt: string = "annotations") {
- return FieldView.LayoutString(CollectionVideoView, fieldKey, fieldExt);
- }
- private get uIButtons() {
- let scaling = Math.min(1.8, this.props.ScreenToLocalTransform().Scale);
- let curTime = NumCast(this.props.Document.curPage);
- return ([<div className="collectionVideoView-time" key="time" onPointerDown={this.onResetDown} style={{ transform: `scale(${scaling})` }}>
- <span>{"" + Math.round(curTime)}</span>
- <span style={{ fontSize: 8 }}>{" " + Math.round((curTime - Math.trunc(curTime)) * 100)}</span>
- </div>,
- <div className="collectionVideoView-snapshot" key="time" onPointerDown={this.onSnapshot} style={{ transform: `scale(${scaling})` }}>
- <FontAwesomeIcon icon="camera" size="lg" />
- </div>,
- VideoBox._showControls ? (null) : [
- <div className="collectionVideoView-play" key="play" onPointerDown={this.onPlayDown} style={{ transform: `scale(${scaling})` }}>
- <FontAwesomeIcon icon={this._videoBox && this._videoBox.Playing ? "pause" : "play"} size="lg" />
- </div>,
- <div className="collectionVideoView-full" key="full" onPointerDown={this.onFullDown} style={{ transform: `scale(${scaling})` }}>
- F
- </div>
- ]]);
- }
-
- @action
- onPlayDown = () => {
- if (this._videoBox) {
- if (this._videoBox.Playing) {
- this._videoBox.Pause();
- } else {
- this._videoBox.Play();
- }
- }
- }
-
- @action
- onFullDown = (e: React.PointerEvent) => {
- if (this._videoBox) {
- this._videoBox.FullScreen();
- e.stopPropagation();
- e.preventDefault();
- }
- }
-
- @action
- onSnapshot = (e: React.PointerEvent) => {
- if (this._videoBox) {
- this._videoBox.Snapshot();
- e.stopPropagation();
- e.preventDefault();
- }
- }
-
- _isclick = 0;
- @action
- onResetDown = (e: React.PointerEvent) => {
- if (this._videoBox) {
- this._videoBox.Pause();
- e.stopPropagation();
- this._isclick = 0;
- document.addEventListener("pointermove", this.onPointerMove, true);
- document.addEventListener("pointerup", this.onPointerUp, true);
- InkingControl.Instance.switchTool(InkTool.Eraser);
- }
- }
-
- @action
- onPointerMove = (e: PointerEvent) => {
- this._isclick += Math.abs(e.movementX) + Math.abs(e.movementY);
- if (this._videoBox) {
- this._videoBox.Seek(Math.max(0, NumCast(this.props.Document.curPage, 0) + Math.sign(e.movementX) * 0.0333));
- }
- e.stopImmediatePropagation();
- }
- @action
- onPointerUp = (e: PointerEvent) => {
- document.removeEventListener("pointermove", this.onPointerMove, true);
- document.removeEventListener("pointerup", this.onPointerUp, true);
- InkingControl.Instance.switchTool(InkTool.None);
- this._isclick < 10 && (this.props.Document.curPage = 0);
- }
- setVideoBox = (videoBox: VideoBox) => { this._videoBox = videoBox; };
-
- private subView = (_type: CollectionViewType, renderProps: CollectionRenderProps) => {
- let props = { ...this.props, ...renderProps };
- return (<>
- <CollectionFreeFormView {...props} setVideoBox={this.setVideoBox} CollectionView={this} chromeCollapsed={true} />
- {this.props.isSelected() ? this.uIButtons : (null)}
- </>);
- }
-
- render() {
- return (
- <CollectionBaseView {...this.props} className="collectionVideoView-cont" >
- {this.subView}
- </CollectionBaseView>);
- }
-} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionView.scss b/src/client/views/collections/CollectionView.scss
new file mode 100644
index 000000000..e4187e4d6
--- /dev/null
+++ b/src/client/views/collections/CollectionView.scss
@@ -0,0 +1,26 @@
+@import "../globalCssVariables";
+
+.collectionView {
+ border-width: 0;
+ border-color: $light-color-secondary;
+ border-style: solid;
+ border-radius: 0 0 $border-radius $border-radius;
+ box-sizing: border-box;
+ border-radius: inherit;
+ width: 100%;
+ height: 100%;
+ overflow: auto;
+}
+
+#google-tags {
+ transition: all 0.5s ease 0s;
+ width: 30px;
+ height: 30px;
+ position: absolute;
+ bottom: 15px;
+ left: 15px;
+ border: 2px solid black;
+ border-radius: 50%;
+ padding: 3px;
+ background: white;
+} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index d3072ff1e..8d5694bf0 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -1,38 +1,96 @@
import { library } from '@fortawesome/fontawesome-svg-core';
import { faEye } from '@fortawesome/free-regular-svg-icons';
-import { faColumns, faEllipsisV, faFingerprint, faImage, faProjectDiagram, faSignature, faSquare, faTh, faThList, faTree, faCopy } from '@fortawesome/free-solid-svg-icons';
+import { faColumns, faCopy, faEllipsisV, faFingerprint, faImage, faProjectDiagram, faSignature, faSquare, faTh, faThList, faTree } from '@fortawesome/free-solid-svg-icons';
import { action, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
import { observer } from "mobx-react";
import * as React from 'react';
-import { Doc } from '../../../new_fields/Doc';
import { Id } from '../../../new_fields/FieldSymbols';
-import { StrCast } from '../../../new_fields/Types';
+import { StrCast, BoolCast, Cast } from '../../../new_fields/Types';
import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils';
import { ContextMenu } from "../ContextMenu";
-import { ContextMenuProps } from '../ContextMenuItem';
-import { FieldView, FieldViewProps } from '../nodes/FieldView';
-import { CollectionBaseView, CollectionRenderProps, CollectionViewType } from './CollectionBaseView';
import { CollectionDockingView } from "./CollectionDockingView";
+import { AddCustomFreeFormLayout } from './collectionFreeForm/CollectionFreeFormLayoutEngines';
import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView';
import { CollectionSchemaView } from "./CollectionSchemaView";
import { CollectionStackingView } from './CollectionStackingView';
import { CollectionTreeView } from "./CollectionTreeView";
import { CollectionViewBaseChrome } from './CollectionViewChromes';
-import { AddCustomFreeFormLayout } from './collectionFreeForm/CollectionFreeFormLayoutEngines';
+import { ImageUtils } from '../../util/Import & Export/ImageUtils';
+import { CollectionLinearView } from '../CollectionLinearView';
+import { DocumentType } from '../../documents/DocumentTypes';
+import { ImageField } from '../../../new_fields/URLField';
+import { DocListCast } from '../../../new_fields/Doc';
+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
export const COLLECTION_BORDER_WIDTH = 2;
-
+import { DateField } from '../../../new_fields/DateField';
+import { Doc, } from '../../../new_fields/Doc';
+import { listSpec } from '../../../new_fields/Schema';
+import { DocumentManager } from '../../util/DocumentManager';
+import { SelectionManager } from '../../util/SelectionManager';
+import './CollectionView.scss';
+import { FieldViewProps, FieldView } from '../nodes/FieldView';
library.add(faTh, faTree, faSquare, faProjectDiagram, faSignature, faThList, faFingerprint, faColumns, faEllipsisV, faImage, faEye as any, faCopy);
+export enum CollectionViewType {
+ Invalid,
+ Freeform,
+ Schema,
+ Docking,
+ Tree,
+ Stacking,
+ Masonry,
+ Pivot,
+ Linear,
+}
+
+export namespace CollectionViewType {
+ const stringMapping = new Map<string, CollectionViewType>([
+ ["invalid", CollectionViewType.Invalid],
+ ["freeform", CollectionViewType.Freeform],
+ ["schema", CollectionViewType.Schema],
+ ["docking", CollectionViewType.Docking],
+ ["tree", CollectionViewType.Tree],
+ ["stacking", CollectionViewType.Stacking],
+ ["masonry", CollectionViewType.Masonry],
+ ["pivot", CollectionViewType.Pivot],
+ ["linear", CollectionViewType.Linear]
+ ]);
+
+ export const valueOf = (value: string) => stringMapping.get(value.toLowerCase());
+}
+
+export interface CollectionRenderProps {
+ addDocument: (document: Doc) => boolean;
+ removeDocument: (document: Doc) => boolean;
+ moveDocument: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
+ active: () => boolean;
+ whenActiveChanged: (isActive: boolean) => void;
+}
+
@observer
export class CollectionView extends React.Component<FieldViewProps> {
- @observable private _collapsed = true;
+ 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; }
- public static LayoutString(fieldStr: string = "data", fieldExt: string = "") { return FieldView.LayoutString(CollectionView, fieldStr, fieldExt); }
-
- constructor(props: any) {
- super(props);
+ get collectionViewType(): CollectionViewType | undefined {
+ let viewField = Cast(this.props.Document.viewType, "number");
+ if (CollectionView._safeMode) {
+ if (viewField === CollectionViewType.Freeform) {
+ return CollectionViewType.Tree;
+ }
+ if (viewField === CollectionViewType.Invalid) {
+ return CollectionViewType.Freeform;
+ }
+ }
+ return viewField === undefined ? CollectionViewType.Invalid : viewField;
}
componentDidMount = () => {
@@ -47,26 +105,73 @@ export class CollectionView extends React.Component<FieldViewProps> {
});
}
- componentWillUnmount = () => {
- this._reactionDisposer && this._reactionDisposer();
+ componentWillUnmount = () => this._reactionDisposer && this._reactionDisposer();
+
+ // bcz: Argh? What's the height of the collection chromes??
+ chromeHeight = () => (this.props.ChromeHeight ? this.props.ChromeHeight() : 0) + (this.props.Document.chromeStatus === "enabled" ? -60 : 0);
+
+ active = () => this.props.isSelected() || 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 {
+ let targetDataDoc = Doc.GetProto(this.props.Document);
+ Doc.AddDocToList(targetDataDoc, this.props.fieldKey, doc);
+ let extension = Doc.fieldExtensionDoc(targetDataDoc, this.props.fieldKey); // set metadata about the field being rendered (ie, the set of documents) on an extension field for that field
+ extension && (extension.lastModified = new DateField(new Date(Date.now())));
+ Doc.GetProto(doc).lastOpened = new DateField;
+ return true;
+ }
+
+ @action.bound
+ removeDocument(doc: Doc): boolean {
+ let docView = DocumentManager.Instance.getDocumentView(doc, this.props.ContainingCollectionView);
+ docView && SelectionManager.DeselectDoc(docView);
+ let value = Cast(this.props.Document[this.props.fieldKey], listSpec(Doc), []);
+ 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();
+ if (index !== -1) {
+ value.splice(index, 1);
+ return true;
+ }
+ return false;
+ }
+
+ // this is called with the document that was dragged and the collection to move it into.
+ // if the target collection is the same as this collection, then the move will be allowed.
+ // otherwise, the document being moved must be able to be removed from its container before
+ // moving it into the target.
+ @action.bound
+ moveDocument(doc: Doc, targetCollection: Doc, addDocument: (doc: Doc) => boolean): boolean {
+ if (Doc.AreProtosEqual(this.props.Document, targetCollection)) {
+ return true;
+ }
+ return this.removeDocument(doc) ? addDocument(doc) : false;
+ }
+
+ showIsTagged = () => {
+ const children = DocListCast(this.props.Document[this.props.fieldKey]);
+ const imageProtos = children.filter(doc => Cast(doc.data, ImageField)).map(Doc.GetProto);
+ const allTagged = imageProtos.length > 0 && imageProtos.every(image => image.googlePhotosTags);
+ return !allTagged ? (null) : <img id={"google-tags"} src={"/assets/google_tags.png"} />;
}
private SubViewHelper = (type: CollectionViewType, renderProps: CollectionRenderProps) => {
- let props = { ...this.props, ...renderProps };
- switch (this.isAnnotationOverlay ? CollectionViewType.Freeform : type) {
- case CollectionViewType.Schema: return (<CollectionSchemaView chromeCollapsed={this._collapsed} key="collview" {...props} CollectionView={this} />);
- // 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
- case CollectionViewType.Docking: return (<CollectionDockingView chromeCollapsed={true} key="collview" {...props} CollectionView={this} />);
- case CollectionViewType.Tree: return (<CollectionTreeView chromeCollapsed={this._collapsed} key="collview" {...props} CollectionView={this} />);
- case CollectionViewType.Stacking: { this.props.Document.singleColumn = true; return (<CollectionStackingView chromeCollapsed={this._collapsed} key="collview" {...props} CollectionView={this} />); }
- case CollectionViewType.Masonry: { this.props.Document.singleColumn = false; return (<CollectionStackingView chromeCollapsed={this._collapsed} key="collview" {...props} CollectionView={this} />); }
- case CollectionViewType.Pivot: { this.props.Document.freeformLayoutEngine = "pivot"; return (<CollectionFreeFormView chromeCollapsed={this._collapsed} key="collview" {...props} CollectionView={this} />); }
+ let props = { ...this.props, ...renderProps, chromeCollapsed: this._collapsed, ChromeHeight: this.chromeHeight, 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.Linear: { return (<CollectionLinearView 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: { this.props.Document.freeformLayoutEngine = "pivot"; return (<CollectionFreeFormView key="collview" {...props} />); }
case CollectionViewType.Freeform:
- default:
- this.props.Document.freeformLayoutEngine = undefined;
- return (<CollectionFreeFormView chromeCollapsed={this._collapsed} key="collview" {...props} CollectionView={this} />);
+ default: { this.props.Document.freeformLayoutEngine = undefined; return (<CollectionFreeFormView key="collview" {...props} />); }
}
- return (null);
}
@action
@@ -77,23 +182,18 @@ export class CollectionView extends React.Component<FieldViewProps> {
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
- if (this.isAnnotationOverlay || this.props.Document.chromeStatus === "disabled" || type === CollectionViewType.Docking) {
- return [(null), this.SubViewHelper(type, renderProps)];
- }
- return [
- <CollectionViewBaseChrome CollectionView={this} key="chrome" type={type} collapse={this.collapse} />,
- this.SubViewHelper(type, renderProps)
- ];
+ let chrome = this.props.Document.chromeStatus === "disabled" || type === CollectionViewType.Docking ? (null) :
+ <CollectionViewBaseChrome CollectionView={this} key="chrome" type={type} collapse={this.collapse} />;
+ return [chrome, this.SubViewHelper(type, renderProps)];
}
- get isAnnotationOverlay() { return this.props.fieldExt ? true : false; }
onContextMenu = (e: React.MouseEvent): void => {
- if (!this.isAnnotationOverlay && !e.isPropagationStopped() && this.props.Document[Id] !== CurrentUserUtils.MainDocId) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
+ if (!e.isPropagationStopped() && this.props.Document[Id] !== CurrentUserUtils.MainDocId) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
let existingVm = ContextMenu.Instance.findByDescription("View Modes...");
- let subItems: ContextMenuProps[] = existingVm && "subitems" in existingVm ? existingVm.subitems : [];
+ let subItems = existingVm && "subitems" in existingVm ? existingVm.subitems : [];
subItems.push({ description: "Freeform", event: () => { this.props.Document.viewType = CollectionViewType.Freeform; }, icon: "signature" });
- if (CollectionBaseView.InSafeMode()) {
+ if (CollectionView._safeMode) {
ContextMenu.Instance.addItem({ description: "Test Freeform", event: () => this.props.Document.viewType = CollectionViewType.Invalid, icon: "project-diagram" });
}
subItems.push({ description: "Schema", event: () => this.props.Document.viewType = CollectionViewType.Schema, icon: "th-list" });
@@ -113,20 +213,43 @@ export class CollectionView extends React.Component<FieldViewProps> {
break;
}
}
+ subItems.push({ description: "lightbox", event: action(() => this._isLightboxOpen = true), icon: "eye" });
!existingVm && ContextMenu.Instance.addItem({ description: "View Modes...", subitems: subItems, icon: "eye" });
let existing = ContextMenu.Instance.findByDescription("Layout...");
- let layoutItems: ContextMenuProps[] = existing && "subitems" in existing ? existing.subitems : [];
+ let 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" });
!existing && ContextMenu.Instance.addItem({ description: "Layout...", subitems: layoutItems, icon: "hand-point-right" });
+ ContextMenu.Instance.addItem({ description: "Export Image Hierarchy", icon: "columns", event: () => ImageUtils.ExportHierarchyToFileSystem(this.props.Document) });
}
}
+ lightbox = (images: string[]) => {
+ return !this._isLightboxOpen ? (null) : (<Lightbox key="lightbox"
+ mainSrc={images[this._curLightboxImg]}
+ nextSrc={images[(this._curLightboxImg + 1) % images.length]}
+ prevSrc={images[(this._curLightboxImg + images.length - 1) % images.length]}
+ onCloseRequest={action(() => this._isLightboxOpen = false)}
+ onMovePrevRequest={action(() => this._curLightboxImg = (this._curLightboxImg + images.length - 1) % images.length)}
+ onMoveNextRequest={action(() => this._curLightboxImg = (this._curLightboxImg + 1) % images.length)} />);
+ }
render() {
- return (
- <CollectionBaseView {...this.props} onContextMenu={this.onContextMenu}>
- {this.SubView}
- </CollectionBaseView>
- );
+ const props: CollectionRenderProps = {
+ addDocument: this.addDocument,
+ removeDocument: this.removeDocument,
+ moveDocument: this.moveDocument,
+ active: this.active,
+ whenActiveChanged: this.whenActiveChanged,
+ };
+ 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")}`
+ }}
+ onContextMenu={this.onContextMenu}>
+ {this.showIsTagged()}
+ {this.collectionViewType !== undefined ? this.SubView(this.collectionViewType, props) : (null)}
+ {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 : ""))}
+ </div>);
}
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx
index cefa9eebc..fb87996c6 100644
--- a/src/client/views/collections/CollectionViewChromes.tsx
+++ b/src/client/views/collections/CollectionViewChromes.tsx
@@ -10,12 +10,11 @@ import { ScriptField } from "../../../new_fields/ScriptField";
import { BoolCast, Cast, NumCast, StrCast } from "../../../new_fields/Types";
import { Utils, emptyFunction } from "../../../Utils";
import { DragManager } from "../../util/DragManager";
-import { CompileScript } from "../../util/Scripting";
import { undoBatch } from "../../util/UndoManager";
import { EditableView } from "../EditableView";
import { COLLECTION_BORDER_WIDTH } from "../globalCssVariables.scss";
import { DocLike } from "../MetadataEntryMenu";
-import { CollectionViewType } from "./CollectionBaseView";
+import { CollectionViewType } from "./CollectionView";
import { CollectionView } from "./CollectionView";
import "./CollectionViewChromes.scss";
import * as Autosuggest from 'react-autosuggest';
@@ -41,9 +40,9 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
//(!)?\(\(\(doc.(\w+) && \(doc.\w+ as \w+\).includes\(\"(\w+)\"\)
_templateCommand = {
- title: "set template", script: "this.target.childLayout = this.source ? this.source[0] : undefined", params: ["target", "source"],
+ title: "set template", script: "setChildLayout(this.target, this.source && this.source.length ? this.source[0]:undefined)", params: ["target", "source"],
initialize: emptyFunction,
- immediate: (draggedDocs: Doc[]) => this.props.CollectionView.props.Document.childLayout = draggedDocs.length ? draggedDocs[0] : undefined
+ immediate: (draggedDocs: Doc[]) => Doc.setChildLayout(this.props.CollectionView.props.Document, draggedDocs.length ? draggedDocs[0] : undefined)
};
_contentCommand = {
// title: "set content", script: "getProto(this.target).data = aliasDocs(this.source.map(async p => await p));", params: ["target", "source"], // bcz: doesn't look like we can do async stuff in scripting...
@@ -242,6 +241,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
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} />);
default: return null;
}
}
@@ -373,7 +373,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
render() {
let collapsed = this.props.CollectionView.props.Document.chromeStatus !== "enabled";
return (
- <div className="collectionViewChrome-cont" style={{ top: collapsed ? -70 : 0 }}>
+ <div className="collectionViewChrome-cont" style={{ top: collapsed ? -70 : 0, height: collapsed ? 0 : undefined }}>
<div className="collectionViewChrome">
<div className="collectionViewBaseChrome">
<button className="collectionViewBaseChrome-collapse"
@@ -397,6 +397,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
<option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="5">Stacking View</option>
<option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="6">Masonry View</option>
<option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="7">Pivot View</option>
+ <option className="collectionViewBaseChrome-viewOption" onPointerDown={stopPropagation} value="8">Linear View</option>
</select>
<div className="collectionViewBaseChrome-viewSpecs" style={{ display: collapsed ? "none" : "grid" }}>
<input className="collectionViewBaseChrome-viewSpecsInput"
@@ -477,12 +478,7 @@ export class CollectionStackingViewChrome extends React.Component<CollectionView
getKeySuggestions = async (value: string): Promise<string[]> => {
value = value.toLowerCase();
- let docs: Doc | Doc[] | Promise<Doc> | Promise<Doc[]> | (() => DocLike)
- = () => DocListCast(this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldExt ? this.props.CollectionView.props.fieldExt : this.props.CollectionView.props.fieldKey]);
- if (typeof docs === "function") {
- docs = docs();
- }
- docs = await docs;
+ let docs = DocListCast(this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldKey]);
if (docs instanceof Doc) {
return Object.keys(docs).filter(key => key.toLowerCase().startsWith(value));
} else {
@@ -588,19 +584,9 @@ export class CollectionSchemaViewChrome extends React.Component<CollectionViewCh
if (textwrappedRows.length) {
this.props.CollectionView.props.Document.textwrappedSchemaRows = new List<string>([]);
} else {
- let docs: Doc | Doc[] | Promise<Doc> | Promise<Doc[]> | (() => DocLike)
- = () => DocListCast(this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldExt ? this.props.CollectionView.props.fieldExt : this.props.CollectionView.props.fieldKey]);
- if (typeof docs === "function") {
- docs = docs();
- }
- docs = await docs;
- if (docs instanceof Doc) {
- let allRows = [docs[Id]];
- this.props.CollectionView.props.Document.textwrappedSchemaRows = new List<string>(allRows);
- } else {
- let allRows = docs.map(doc => doc[Id]);
- this.props.CollectionView.props.Document.textwrappedSchemaRows = new List<string>(allRows);
- }
+ let docs = DocListCast(this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldKey]);
+ let allRows = docs instanceof Doc ? [docs[Id]] : docs.map(doc => doc[Id]);
+ this.props.CollectionView.props.Document.textwrappedSchemaRows = new List<string>(allRows);
}
}
@@ -635,63 +621,14 @@ export class CollectionSchemaViewChrome extends React.Component<CollectionViewCh
@observer
export class CollectionTreeViewChrome extends React.Component<CollectionViewChromeProps> {
- @observable private _currentKey: string = "";
- @observable private suggestions: string[] = [];
@computed private get descending() { return Cast(this.props.CollectionView.props.Document.sortAscending, "boolean", null); }
- @computed get sectionFilter() { return StrCast(this.props.CollectionView.props.Document.sectionFilter); }
-
- getKeySuggestions = async (value: string): Promise<string[]> => {
- value = value.toLowerCase();
- let docs: Doc | Doc[] | Promise<Doc> | Promise<Doc[]> | (() => DocLike)
- = () => DocListCast(this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldExt ? this.props.CollectionView.props.fieldExt : this.props.CollectionView.props.fieldKey]);
- if (typeof docs === "function") {
- docs = docs();
- }
- docs = await docs;
- if (docs instanceof Doc) {
- return Object.keys(docs).filter(key => key.toLowerCase().startsWith(value));
- } else {
- const keys = new Set<string>();
- docs.forEach(doc => Doc.allKeys(doc).forEach(key => keys.add(key)));
- return Array.from(keys).filter(key => key.toLowerCase().startsWith(value));
- }
- }
-
- @action
- onKeyChange = (e: React.ChangeEvent, { newValue }: { newValue: string }) => {
- this._currentKey = newValue;
- }
-
- getSuggestionValue = (suggestion: string) => suggestion;
-
- renderSuggestion = (suggestion: string) => {
- return <p>{suggestion}</p>;
- }
-
- onSuggestionFetch = async ({ value }: { value: string }) => {
- const sugg = await this.getKeySuggestions(value);
- runInAction(() => {
- this.suggestions = sugg;
- });
- }
-
- @action
- onSuggestionClear = () => {
- this.suggestions = [];
- }
-
- setValue = (value: string) => {
- this.props.CollectionView.props.Document.sectionFilter = value;
- return true;
- }
@action toggleSort = () => {
if (this.props.CollectionView.props.Document.sortAscending) this.props.CollectionView.props.Document.sortAscending = undefined;
else if (this.props.CollectionView.props.Document.sortAscending === undefined) this.props.CollectionView.props.Document.sortAscending = false;
else this.props.CollectionView.props.Document.sortAscending = true;
}
- @action resetValue = () => { this._currentKey = this.sectionFilter; };
render() {
return (
diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx
index 7f2913214..8b6fa330c 100644
--- a/src/client/views/collections/ParentDocumentSelector.tsx
+++ b/src/client/views/collections/ParentDocumentSelector.tsx
@@ -7,7 +7,7 @@ import { Id } from "../../../new_fields/FieldSymbols";
import { SearchUtil } from "../../util/SearchUtil";
import { CollectionDockingView } from "./CollectionDockingView";
import { NumCast } from "../../../new_fields/Types";
-import { CollectionViewType } from "./CollectionBaseView";
+import { CollectionViewType } from "./CollectionView";
import { DocumentButtonBar } from "../DocumentButtonBar";
import { DocumentManager } from "../../util/DocumentManager";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
index 886692172..48d330674 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
@@ -63,11 +63,12 @@ export function computePivotLayout(pivotDoc: Doc, childDocs: Doc[], childPairs:
fontSize: NumCast(pivotDoc.pivotFontSize, 10)
});
for (const doc of val) {
+ let layoutDoc = Doc.Layout(doc);
docMap.set(doc, {
x: x + xCount * pivotAxisWidth * 1.25,
y: -y,
width: pivotAxisWidth,
- height: doc.nativeWidth ? (NumCast(doc.nativeHeight) / NumCast(doc.nativeWidth)) * pivotAxisWidth : pivotAxisWidth
+ height: layoutDoc.nativeWidth ? (NumCast(layoutDoc.nativeHeight) / NumCast(layoutDoc.nativeWidth)) * pivotAxisWidth : pivotAxisWidth
});
xCount++;
if (xCount >= numCols) {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
index cfd18ad35..75af11537 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
@@ -1,20 +1,18 @@
.collectionfreeformlinkview-linkLine {
stroke: black;
- transform: translate(10000px,10000px);
opacity: 0.8;
pointer-events: all;
stroke-width: 3px;
+ transition: opacity 0.5s ease-in;
}
.collectionfreeformlinkview-linkCircle {
stroke: rgb(0,0,0);
opacity: 0.5;
- transform: translate(10000px,10000px);
pointer-events: all;
cursor: pointer;
}
.collectionfreeformlinkview-linkText {
stroke: rgb(0,0,0);
opacity: 0.5;
- transform: translate(10000px,10000px);
pointer-events: all;
}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index df089eb00..837413842 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -1,63 +1,51 @@
import { observer } from "mobx-react";
-import { Doc, HeightSym, WidthSym } from "../../../../new_fields/Doc";
-import { BoolCast, NumCast, StrCast } from "../../../../new_fields/Types";
-import { InkingControl } from "../../InkingControl";
+import { Doc } from "../../../../new_fields/Doc";
+import { Utils } from '../../../../Utils';
+import { DocumentView } from "../../nodes/DocumentView";
import "./CollectionFreeFormLinkView.scss";
import React = require("react");
import v5 = require("uuid/v5");
+import { DocumentType } from "../../../documents/DocumentTypes";
+import { observable, action } from "mobx";
export interface CollectionFreeFormLinkViewProps {
- A: Doc;
- B: Doc;
+ A: DocumentView;
+ B: DocumentView;
LinkDocs: Doc[];
- addDocument: (document: Doc, allowDuplicates?: boolean) => boolean;
- removeDocument: (document: Doc) => boolean;
}
@observer
export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFormLinkViewProps> {
-
- onPointerDown = (e: React.PointerEvent) => {
- if (e.button === 0 && !InkingControl.Instance.selectedTool) {
- let a = this.props.A;
- let b = this.props.B;
- let x1 = NumCast(a.x) + (BoolCast(a.isMinimized) ? 5 : a[WidthSym]() / 2);
- let y1 = NumCast(a.y) + (BoolCast(a.isMinimized) ? 5 : a[HeightSym]() / 2);
- let x2 = NumCast(b.x) + (BoolCast(b.isMinimized) ? 5 : b[WidthSym]() / 2);
- let y2 = NumCast(b.y) + (BoolCast(b.isMinimized) ? 5 : b[HeightSym]() / 2);
- // this.props.LinkDocs.map(l => {
- // let width = l[WidthSym]();
- // l.x = (x1 + x2) / 2 - width / 2;
- // l.y = (y1 + y2) / 2 + 10;
- // if (!this.props.removeDocument(l)) this.props.addDocument(l, false);
- // });
- e.stopPropagation();
- e.preventDefault();
- }
+ @observable _alive: number = 0;
+ @observable _opacity: number = 1;
+ @action
+ componentDidMount() {
+ this._alive = 1;
+ setTimeout(this.rerender, 50);
+ setTimeout(action(() => this._opacity = 0.05), 50);
+ }
+ @action
+ componentWillUnmount() {
+ this._alive = 0;
}
+ rerender = action(() => {
+ if (this._alive) {
+ setTimeout(this.rerender, 50);
+ this._alive++;
+ }
+ });
+
render() {
- // let l = this.props.LinkDocs;
- let a = this.props.A;
- let b = this.props.B;
- let x1 = NumCast(a.x) + (BoolCast(a.isMinimized, false) ? 5 : NumCast(a.width) / 2);
- let y1 = NumCast(a.y) + (BoolCast(a.isMinimized, false) ? 5 : NumCast(a.height) / 2);
- let x2 = NumCast(b.x) + (BoolCast(b.isMinimized, false) ? 5 : NumCast(b.width) / 2);
- let y2 = NumCast(b.y) + (BoolCast(b.isMinimized, false) ? 5 : NumCast(b.height) / 2);
- let text = "";
- // let first = this.props.LinkDocs[0];
- // if (this.props.LinkDocs.length === 1) text += first.title + (first.linkDescription ? "(" + StrCast(first.linkDescription) + ")" : "");
- // else text = "-multiple-";
- return (
- <>
- <line key="linkLine" className="collectionfreeformlinkview-linkLine"
- x1={`${x1}`} y1={`${y1}`}
- x2={`${x2}`} y2={`${y2}`} />
- {/* <circle key="linkCircle" className="collectionfreeformlinkview-linkCircle"
- cx={(x1 + x2) / 2} cy={(y1 + y2) / 2} r={8} onPointerDown={this.onPointerDown} /> */}
- <text key="linkText" textAnchor="middle" className="collectionfreeformlinkview-linkText" x={`${(x1 + x2) / 2}`} y={`${(y1 + y2) / 2}`}>
- {text}
- </text>
- </>
- );
+ let y = this._alive;
+ let acont = this.props.A.props.Document.type === DocumentType.LINK ? this.props.A.ContentDiv!.getElementsByClassName("docuLinkBox-cont") : [];
+ let bcont = this.props.B.props.Document.type === DocumentType.LINK ? this.props.B.ContentDiv!.getElementsByClassName("docuLinkBox-cont") : [];
+ let a = (acont.length ? acont[0] : this.props.A.ContentDiv!).getBoundingClientRect();
+ let b = (bcont.length ? bcont[0] : this.props.B.ContentDiv!).getBoundingClientRect();
+ let pt1 = Utils.getNearestPointInPerimeter(a.left, a.top, a.width, a.height, b.left + b.width / 2, b.top + b.height / 2);
+ let pt2 = Utils.getNearestPointInPerimeter(b.left, b.top, b.width, b.height, a.left + a.width / 2, a.top + a.height / 2);
+ return (<line key="linkLine" className="collectionfreeformlinkview-linkLine"
+ style={{ opacity: this._opacity }}
+ x1={`${pt1[0]}`} y1={`${pt1[1]}`}
+ x2={`${pt2[0]}`} y2={`${pt2[1]}`} />);
}
} \ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss
index 30e158603..cb5cef29c 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss
@@ -1,10 +1,9 @@
.collectionfreeformlinksview-svgCanvas{
- transform: translate(-10000px,-10000px);
position: absolute;
top: 0;
left: 0;
- width: 20000px;
- height: 20000px;
+ width: 100%;
+ height: 100%;
pointer-events: none;
}
.collectionfreeformlinksview-container {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
index a81f5315a..e9191c176 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
@@ -1,19 +1,18 @@
-import { computed, IReactionDisposer, reaction } from "mobx";
+import { computed, IReactionDisposer } 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 { List } from "../../../../new_fields/List";
-import { listSpec } from "../../../../new_fields/Schema";
-import { Cast, FieldValue, NumCast, StrCast } from "../../../../new_fields/Types";
import { DocumentManager } from "../../../util/DocumentManager";
import { DocumentView } from "../../nodes/DocumentView";
-import { CollectionViewProps } from "../CollectionSubView";
import "./CollectionFreeFormLinksView.scss";
import { CollectionFreeFormLinkView } from "./CollectionFreeFormLinkView";
import React = require("react");
+import { Utils } from "../../../../Utils";
+import { SelectionManager } from "../../../util/SelectionManager";
+import { DocumentType } from "../../../documents/DocumentTypes";
@observer
-export class CollectionFreeFormLinksView extends React.Component<CollectionViewProps> {
+export class CollectionFreeFormLinksView extends React.Component {
_brushReactionDisposer?: IReactionDisposer;
componentDidMount() {
@@ -69,59 +68,33 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
// });
}
componentWillUnmount() {
- if (this._brushReactionDisposer) {
- this._brushReactionDisposer();
- }
+ this._brushReactionDisposer && this._brushReactionDisposer();
}
- documentAnchors(view: DocumentView) {
- let equalViews = [view];
- let containerDoc = FieldValue(Cast(view.props.Document.annotationOn, Doc));
- if (containerDoc) {
- equalViews = DocumentManager.Instance.getDocumentViews(containerDoc.proto!);
- }
- if (view.props.ContainingCollectionDoc) {
- let collid = view.props.ContainingCollectionDoc[Id];
- DocListCast(this.props.Document[this.props.fieldKey]).
- filter(child =>
- child[Id] === collid).map(view =>
- DocumentManager.Instance.getDocumentViews(view).map(view =>
- equalViews.push(view)));
- }
- return equalViews.filter(sv => sv.props.ContainingCollectionDoc === this.props.Document);
- }
-
@computed
get uniqueConnections() {
let connections = DocumentManager.Instance.LinkedDocumentViews.reduce((drawnPairs, connection) => {
- let srcViews = this.documentAnchors(connection.a);
- let targetViews = this.documentAnchors(connection.b);
-
- let possiblePairs: { a: Doc, b: Doc, }[] = [];
- srcViews.map(sv => targetViews.map(tv => possiblePairs.push({ a: sv.props.Document, b: tv.props.Document })));
- possiblePairs.map(possiblePair => {
- if (!drawnPairs.reduce((found, drawnPair) => {
- let match1 = (Doc.AreProtosEqual(possiblePair.a, drawnPair.a) && Doc.AreProtosEqual(possiblePair.b, drawnPair.b));
- let match2 = (Doc.AreProtosEqual(possiblePair.a, drawnPair.b) && Doc.AreProtosEqual(possiblePair.b, drawnPair.a));
- let match = match1 || match2;
- if (match && !drawnPair.l.reduce((found, link) => found || link[Id] === connection.l[Id], false)) {
- drawnPair.l.push(connection.l);
- }
- return match || found;
- }, false)) {
- drawnPairs.push({ a: possiblePair.a, b: possiblePair.b, l: [connection.l] });
+ if (!drawnPairs.reduce((found, drawnPair) => {
+ let match1 = (connection.a === drawnPair.a && connection.b === drawnPair.b);
+ let match2 = (connection.a === drawnPair.b && connection.b === drawnPair.a);
+ let match = match1 || match2;
+ if (match && !drawnPair.l.reduce((found, link) => found || link[Id] === connection.l[Id], false)) {
+ drawnPair.l.push(connection.l);
}
- });
+ return match || found;
+ }, false)) {
+ drawnPairs.push({ a: connection.a, b: connection.b, l: [connection.l] });
+ }
return drawnPairs;
- }, [] as { a: Doc, b: Doc, l: Doc[] }[]);
- return connections.map(c => <CollectionFreeFormLinkView key={c.l.reduce((p, l) => p + l[Id], "")} A={c.a} B={c.b} LinkDocs={c.l}
- removeDocument={this.props.removeDocument} addDocument={this.props.addDocument} />);
+ }, [] 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} />);
}
render() {
return (
<div className="collectionfreeformlinksview-container">
<svg className="collectionfreeformlinksview-svgCanvas">
- {this.uniqueConnections}
+ {SelectionManager.GetIsDragging() ? (null) : this.uniqueConnections}
</svg>
{this.props.children}
</div>
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
index c4311fa52..bb1a12f88 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
@@ -46,12 +46,6 @@
border-radius: inherit;
box-sizing: border-box;
position: absolute;
- overflow: hidden;
-
- .marqueeView {
- overflow: hidden;
- }
-
top: 0;
left: 0;
width: 100%;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index c6e8d7cf7..2d7a557b5 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,6 +1,6 @@
import { library } from "@fortawesome/fontawesome-svg-core";
import { faEye } from "@fortawesome/free-regular-svg-icons";
-import { faBraille, faChalkboard, faCompass, faCompressArrowsAlt, faExpandArrowsAlt, faPaintBrush, faTable, faUpload } from "@fortawesome/free-solid-svg-icons";
+import { faBraille, faChalkboard, faCompass, faCompressArrowsAlt, faExpandArrowsAlt, faFileUpload, faPaintBrush, faTable, faUpload } from "@fortawesome/free-solid-svg-icons";
import { action, computed, observable } from "mobx";
import { observer } from "mobx-react";
import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../../new_fields/Doc";
@@ -10,7 +10,7 @@ import { createSchema, makeInterface } from "../../../../new_fields/Schema";
import { ScriptField } from "../../../../new_fields/ScriptField";
import { BoolCast, Cast, DateCast, NumCast, StrCast } from "../../../../new_fields/Types";
import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils";
-import { aggregateBounds, emptyFunction, intersectRect, returnEmptyString, returnOne, Utils } from "../../../../Utils";
+import { aggregateBounds, emptyFunction, intersectRect, returnOne, Utils } from "../../../../Utils";
import { CognitiveServices } from "../../../cognitive_services/CognitiveServices";
import { DocServer } from "../../../DocServer";
import { Docs } from "../../../documents/Documents";
@@ -25,24 +25,20 @@ import { COLLECTION_BORDER_WIDTH } from "../../../views/globalCssVariables.scss"
import { ContextMenu } from "../../ContextMenu";
import { ContextMenuProps } from "../../ContextMenuItem";
import { InkingCanvas } from "../../InkingCanvas";
-import { CollectionFreeFormDocumentView, positionSchema } from "../../nodes/CollectionFreeFormDocumentView";
-import { DocumentContentsView } from "../../nodes/DocumentContentsView";
-import { documentSchema, DocumentViewProps } from "../../nodes/DocumentView";
+import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView";
+import { DocumentViewProps } from "../../nodes/DocumentView";
import { FormattedTextBox } from "../../nodes/FormattedTextBox";
import { pageSchema } from "../../nodes/ImageBox";
-import PDFMenu from "../../pdf/PDFMenu";
import { CollectionSubView } from "../CollectionSubView";
import { computePivotLayout, ViewDefResult } from "./CollectionFreeFormLayoutEngines";
-import { CollectionFreeFormLinksView } from "./CollectionFreeFormLinksView";
import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCursors";
import "./CollectionFreeFormView.scss";
import { MarqueeView } from "./MarqueeView";
import React = require("react");
-import v5 = require("uuid/v5");
+import { documentSchema, positionSchema } from "../../../../new_fields/documentSchemas";
import { Timeline } from "../../animationtimeline/Timeline";
-import { number } from "prop-types";
-library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard);
+library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload);
export const panZoomSchema = createSchema({
panX: "number",
@@ -54,6 +50,11 @@ export const panZoomSchema = createSchema({
isRuleProvider: "boolean",
fitToBox: "boolean",
panTransformType: "string",
+ scrollHeight: "number",
+ fitX: "number",
+ fitY: "number",
+ fitW: "number",
+ fitH: "number"
});
type PanZoomDocument = makeInterface<[typeof panZoomSchema, typeof documentSchema, typeof positionSchema, typeof pageSchema]>;
@@ -71,16 +72,16 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
@computed get fitToContent() { return (this.props.fitToBox || this.Document.fitToBox) && !this.isAnnotationOverlay; }
@computed get parentScaling() { return this.props.ContentScaling && this.fitToContent && !this.isAnnotationOverlay ? this.props.ContentScaling() : 1; }
@computed get contentBounds() { return aggregateBounds(this.elements.filter(e => e.bounds && !e.bounds.z).map(e => e.bounds!)); }
- @computed get nativeWidth() { return this.fitToContent ? 0 : this.Document.nativeWidth || 0; }
+ @computed get nativeWidth() { return this.Document.fitToContent ? 0 : this.Document.nativeWidth || 0; }
@computed get nativeHeight() { return this.fitToContent ? 0 : this.Document.nativeHeight || 0; }
- private get isAnnotationOverlay() { return this.props.fieldExt ? true : false; } // fieldExt will be "" or "annotation". should maybe generalize this, or make it more specific (ie, 'annotation' instead of 'fieldExt')
+ private get isAnnotationOverlay() { return this.props.isAnnotationOverlay; }
private get borderWidth() { return this.isAnnotationOverlay ? 0 : COLLECTION_BORDER_WIDTH; }
private easing = () => this.props.Document.panTransformType === "Ease";
private panX = () => this.fitToContent ? (this.contentBounds.x + this.contentBounds.r) / 2 : this.Document.panX || 0;
private panY = () => this.fitToContent ? (this.contentBounds.y + this.contentBounds.b) / 2 : this.Document.panY || 0;
private zoomScaling = () => (1 / this.parentScaling) * (this.fitToContent ?
Math.min(this.props.PanelHeight() / (this.contentBounds.b - this.contentBounds.y), this.props.PanelWidth() / (this.contentBounds.r - this.contentBounds.x)) :
- this.Document.scale || 1);
+ this.Document.scale || 1)
private centeringShiftX = () => !this.nativeWidth && !this.isAnnotationOverlay ? this.props.PanelWidth() / 2 / this.parentScaling : 0; // shift so pan position is at center of window for non-overlay collections
private centeringShiftY = () => !this.nativeHeight && !this.isAnnotationOverlay ? this.props.PanelHeight() / 2 / this.parentScaling : 0;// shift so pan position is at center of window for non-overlay collections
private getTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-this.borderWidth + 1, -this.borderWidth + 1).translate(-this.centeringShiftX(), -this.centeringShiftY()).transform(this.getLocalTransform());
@@ -97,10 +98,10 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
heading = !sorted.length ? Math.max(1, maxHeading) : NumCast(sorted[sorted.length - 1].heading) === 1 ? 2 : NumCast(sorted[sorted.length - 1].heading);
}
!this.Document.isRuleProvider && (newBox.heading = heading);
- this.addDocument(newBox, false);
+ this.addDocument(newBox);
}
- private addDocument = (newBox: Doc, allowDuplicates: boolean) => {
- let added = this.props.addDocument(newBox, false);
+ private addDocument = (newBox: Doc) => {
+ let added = this.props.addDocument(newBox);
added && this.bringToFront(newBox);
added && this.updateCluster(newBox);
return added;
@@ -109,20 +110,16 @@ 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 !this.props.Document.isMinimized && (Math.abs(NumCast(doc.page, -1) - NumCast(this.Document.curPage, -1)) < 1.5 || NumCast(doc.page, -1) === -1); }
+ 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 getActiveDocuments = () => {
return this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => pair.layout);
}
- @computed get fieldExtensionDoc() {
- return Doc.fieldExtensionDoc(this.props.DataDoc || this.props.Document, this.props.fieldKey);
- }
-
@action
- onDrop = (e: React.DragEvent): void => {
+ onDrop = (e: React.DragEvent): Promise<void> => {
var pt = this.getTransform().transformPoint(e.pageX, e.pageY);
- super.onDrop(e, { x: pt[0], y: pt[1] });
+ return super.onDrop(e, { x: pt[0], y: pt[1] });
}
@undoBatch
@@ -135,21 +132,23 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
if (super.drop(e, de)) {
if (de.data instanceof DragManager.DocumentDragData) {
if (de.data.droppedDocuments.length) {
- let z = NumCast(de.data.droppedDocuments[0].z);
+ let firstDoc = de.data.droppedDocuments[0];
+ let z = NumCast(firstDoc.z);
let x = (z ? xpo : xp) - de.data.offset[0];
let y = (z ? ypo : yp) - de.data.offset[1];
- let dropX = NumCast(de.data.droppedDocuments[0].x);
- let dropY = NumCast(de.data.droppedDocuments[0].y);
+ let dropX = NumCast(firstDoc.x);
+ let dropY = NumCast(firstDoc.y);
de.data.droppedDocuments.forEach(action((d: Doc) => {
+ let layoutDoc = Doc.Layout(d);
d.x = x + NumCast(d.x) - dropX;
d.y = y + NumCast(d.y) - dropY;
- if (!NumCast(d.width)) {
- d.width = 300;
+ if (!NumCast(layoutDoc.width)) {
+ layoutDoc.width = 300;
}
- if (!NumCast(d.height)) {
- let nw = NumCast(d.nativeWidth);
- let nh = NumCast(d.nativeHeight);
- d.height = nw && nh ? nh / nw * NumCast(d.width) : 300;
+ if (!NumCast(layoutDoc.height)) {
+ let nw = NumCast(layoutDoc.nativeWidth);
+ let nh = NumCast(layoutDoc.nativeHeight);
+ layoutDoc.height = nw && nh ? nh / nw * NumCast(layoutDoc.width) : 300;
}
this.bringToFront(d);
}));
@@ -162,12 +161,11 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
let dragDoc = de.data.dropDocument;
let x = xp - de.data.offset[0];
let y = yp - de.data.offset[1];
- let dropX = NumCast(de.data.dropDocument.x);
- let dropY = NumCast(de.data.dropDocument.y);
+ let dropX = NumCast(dragDoc.x);
+ let dropY = NumCast(dragDoc.y);
dragDoc.x = x + NumCast(dragDoc.x) - dropX;
dragDoc.y = y + NumCast(dragDoc.y) - dropY;
- de.data.targetContext = this.props.Document;
- dragDoc.targetContext = this.props.Document;
+ de.data.targetContext = this.props.Document; // dropped a PDF annotation, so we need to set the targetContext on the dragData which the PDF view uses at the end of the drop operation
this.bringToFront(dragDoc);
}
}
@@ -177,11 +175,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
pickCluster(probe: number[]) {
return this.childLayoutPairs.map(pair => pair.layout).reduce((cluster, cd) => {
+ let layoutDoc = Doc.Layout(cd);
let cx = NumCast(cd.x) - this._clusterDistance;
let cy = NumCast(cd.y) - this._clusterDistance;
- let cw = NumCast(cd.width) + 2 * this._clusterDistance;
- let ch = NumCast(cd.height) + 2 * this._clusterDistance;
- return !cd.z && intersectRect({ left: cx, top: cy, width: cw, height: ch }, { left: probe[0], top: probe[1], width: 1, height: 1 }) ?
+ let cw = NumCast(layoutDoc.width) + 2 * this._clusterDistance;
+ let ch = NumCast(layoutDoc.height) + 2 * this._clusterDistance;
+ return !layoutDoc.z && intersectRect({ left: cx, top: cy, width: cw, height: ch }, { left: probe[0], top: probe[1], width: 1, height: 1 }) ?
NumCast(cd.cluster) : cluster;
}, -1);
}
@@ -189,14 +188,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
let cluster = this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY));
if (cluster !== -1) {
let eles = this.childLayoutPairs.map(pair => pair.layout).filter(cd => NumCast(cd.cluster) === cluster);
-
- // hacky way to get a list of DocumentViews in the current view given a list of Documents in the current view
- let prevSelected = SelectionManager.SelectedDocuments();
- this.selectDocuments(eles);
- let clusterDocs = SelectionManager.SelectedDocuments();
- SelectionManager.DeselectAll();
- prevSelected.map(dv => SelectionManager.SelectDoc(dv, true));
-
+ let clusterDocs = eles.map(ele => DocumentManager.Instance.getDocumentView(ele, this.props.CollectionView)!);
let de = new DragManager.DocumentDragData(eles);
de.moveDocument = this.props.moveDocument;
const [left, top] = clusterDocs[0].props.ScreenToLocalTransform().scale(clusterDocs[0].props.ContentScaling()).inverse().transformPoint(0, 0);
@@ -271,8 +263,9 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
@action
onPointerDown = (e: React.PointerEvent): void => {
+ if (e.nativeEvent.cancelBubble) return;
this._hitCluster = this.props.Document.useClusters ? this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY)) !== -1 : false;
- if (e.button === 0 && !e.shiftKey && !e.altKey && (!this.isAnnotationOverlay || this.zoomScaling() !== 1) && this.props.active()) {
+ if (e.button === 0 && !e.shiftKey && !e.altKey && !e.ctrlKey && (!this.isAnnotationOverlay || this.zoomScaling() !== 1) && this.props.active()) {
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
document.addEventListener("pointermove", this.onPointerMove);
@@ -289,7 +282,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
@action
onPointerMove = (e: PointerEvent): void => {
- if (!e.cancelBubble && !this.isAnnotationOverlay) {
+ if (!e.cancelBubble) {
if (this._hitCluster && this.tryDragCluster(e)) {
e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers
e.preventDefault();
@@ -302,20 +295,20 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
let docs = this.childLayoutPairs.map(pair => pair.layout);
let [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY);
if (!this.isAnnotationOverlay) {
- PDFMenu.Instance.fadeOut(true);
let minx = docs.length ? NumCast(docs[0].x) : 0;
- let maxx = docs.length ? NumCast(docs[0].width) + minx : minx;
+ let maxx = docs.length ? NumCast(Doc.Layout(docs[0]).width) + minx : minx;
let miny = docs.length ? NumCast(docs[0].y) : 0;
- let maxy = docs.length ? NumCast(docs[0].height) + miny : miny;
+ let maxy = docs.length ? NumCast(Doc.Layout(docs[0]).height) + miny : miny;
let ranges = docs.filter(doc => doc).reduce((range, doc) => {
+ let layoutDoc = Doc.Layout(doc);
let x = NumCast(doc.x);
- let xe = x + NumCast(doc.width);
+ let xe = x + NumCast(layoutDoc.width);
let y = NumCast(doc.y);
- let ye = y + NumCast(doc.height);
+ let ye = y + NumCast(layoutDoc.height);
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]]);
- let ink = Cast(this.fieldExtensionDoc.ink, InkField);
+ let ink = this.extensionDoc && Cast(this.extensionDoc.ink, InkField);
if (ink && ink.inkData) {
ink.inkData.forEach((value: StrokeData, key: string) => {
let bounds = InkingCanvas.StrokeRect(value);
@@ -342,7 +335,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
@action
onPointerWheel = (e: React.WheelEvent): void => {
- if (this.props.Document.lockedPosition || this.isAnnotationOverlay) return;
+ if (this.props.Document.lockedPosition || this.props.Document.inOverlay) return;
if (!e.ctrlKey && this.props.Document.scrollHeight !== undefined) { // things that can scroll vertically should do that instead of zooming
e.stopPropagation();
}
@@ -363,14 +356,14 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
}
@action
- setPan(panX: number, panY: number) {
- if (!this.props.Document.lockedPosition) {
- this.props.Document.panTransformType = "None";
+ setPan(panX: number, panY: number, panType: string = "None") {
+ if (!this.Document.lockedPosition || this.Document.inOverlay) {
+ this.Document.panTransformType = panType;
var scale = this.getLocalTransform().inverse().Scale;
const newPanX = Math.min((1 - 1 / scale) * this.nativeWidth, Math.max(0, panX));
- const newPanY = Math.min((this.props.Document.scrollHeight !== undefined ? NumCast(this.props.Document.scrollHeight) : (1 - 1 / scale) * this.nativeHeight), Math.max(0, panY));
- this.props.Document.panX = this.isAnnotationOverlay ? newPanX : panX;
- this.props.Document.panY = this.isAnnotationOverlay ? newPanY : panY;
+ const newPanY = Math.min((this.props.Document.scrollHeight !== undefined ? NumCast(this.Document.scrollHeight) : (1 - 1 / scale) * this.nativeHeight), Math.max(0, panY));
+ this.Document.panX = this.isAnnotationOverlay ? newPanX : panX;
+ this.Document.panY = this.isAnnotationOverlay ? newPanY : panY;
}
}
@@ -407,21 +400,27 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
SelectionManager.DeselectAll();
if (this.props.Document.scrollHeight) {
let annotOn = Cast(doc.annotationOn, Doc) as Doc;
- let offset = annotOn && (NumCast(annotOn.height) / 2);
- this.props.Document.scrollY = NumCast(doc.y) - offset;
+ if (!annotOn) {
+ this.props.focus(doc);
+ } else {
+ let contextHgt = Doc.AreProtosEqual(annotOn, this.props.Document) && this.props.VisibleHeight ? this.props.VisibleHeight() : NumCast(annotOn.height);
+ let offset = annotOn && (contextHgt / 2 * 96 / 72);
+ this.props.Document.scrollY = NumCast(doc.y) - offset;
+ }
} else {
- const newPanX = NumCast(doc.x) + NumCast(doc.width) / 2;
- const newPanY = NumCast(doc.y) + NumCast(doc.height) / 2;
+ let layoutdoc = Doc.Layout(doc);
+ const newPanX = NumCast(doc.x) + NumCast(layoutdoc.width) / 2;
+ const newPanY = NumCast(doc.y) + NumCast(layoutdoc.height) / 2;
const newState = HistoryUtil.getState();
newState.initializers![this.Document[Id]] = { panX: newPanX, panY: newPanY };
HistoryUtil.pushState(newState);
let savedState = { px: this.Document.panX, py: this.Document.panY, s: this.Document.scale, pt: this.Document.panTransformType };
- this.setPan(newPanX, newPanY);
- this.Document.panTransformType = "Ease";
+ this.setPan(newPanX, newPanY, "Ease");
+ Doc.BrushDoc(this.props.Document);
this.props.focus(this.props.Document);
- willZoom && this.setScaleToZoom(doc, scale);
+ willZoom && this.setScaleToZoom(layoutdoc, scale);
afterFocus && setTimeout(() => {
if (afterFocus && afterFocus()) {
@@ -447,11 +446,10 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
getChildDocumentViewProps(childLayout: Doc, childData?: Doc): DocumentViewProps {
return {
+ ...this.props,
DataDoc: childData,
Document: childLayout,
- addDocument: this.props.addDocument,
- removeDocument: this.props.removeDocument,
- moveDocument: this.props.moveDocument,
+ layoutKey: undefined,
ruleProvider: this.Document.isRuleProvider && childLayout.type !== DocumentType.TEXT ? this.props.Document : this.props.ruleProvider, //bcz: hack! - currently ruleProviders apply to documents in nested colleciton, not direct children of themselves
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
ScreenToLocalTransform: childLayout.z ? this.getTransformOverlay : this.getTransform,
@@ -464,37 +462,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
focus: this.focusDocument,
backgroundColor: this.getClusterColor,
parentActive: this.props.active,
- whenActiveChanged: this.props.whenActiveChanged,
- bringToFront: this.bringToFront,
- addDocTab: this.props.addDocTab,
- pinToPres: this.props.pinToPres,
- zoomToScale: this.zoomToScale,
- getScale: this.getScale
- };
- }
- getDocumentViewProps(layoutDoc: Doc): DocumentViewProps {
- return {
- DataDoc: this.props.DataDoc,
- Document: this.props.Document,
- addDocument: this.props.addDocument,
- removeDocument: this.props.removeDocument,
- moveDocument: this.props.moveDocument,
- ruleProvider: this.props.ruleProvider,
- onClick: this.props.onClick,
- ScreenToLocalTransform: this.getTransform,
- renderDepth: this.props.renderDepth,
- PanelWidth: layoutDoc[WidthSym],
- PanelHeight: layoutDoc[HeightSym],
- ContentScaling: returnOne,
- ContainingCollectionView: this.props.CollectionView,
- ContainingCollectionDoc: this.props.ContainingCollectionDoc,
- focus: this.focusDocument,
- backgroundColor: returnEmptyString,
- parentActive: this.props.active,
- whenActiveChanged: this.props.whenActiveChanged,
bringToFront: this.bringToFront,
- addDocTab: this.props.addDocTab,
- pinToPres: this.props.pinToPres,
zoomToScale: this.zoomToScale,
getScale: this.getScale
};
@@ -503,10 +471,11 @@ 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 } {
const script = this.Document.arrangeScript;
const result = script && script.script.run(params, console.log);
+ const layoutDoc = Doc.Layout(params.doc);
if (result && result.success) {
return { ...result, transition: "transform 1s" };
}
- return { x: Cast(params.doc.x, "number"), y: Cast(params.doc.y, "number"), z: Cast(params.doc.z, "number"), width: Cast(params.doc.width, "number"), height: Cast(params.doc.height, "number") };
+ 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") };
}
viewDefsToJSX = (views: any[]) => {
@@ -623,7 +592,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
// find rule colorations when rule providing is turned on by looking at each document to see if it has a coloring -- if so, use it's color as the rule for its associated heading.
this.Document.isRuleProvider && this.childLayoutPairs.map(pair =>
// iterate over the children of a displayed document (or if the displayed document is a template, iterate over the children of that template)
- DocListCast(pair.layout.layout instanceof Doc ? pair.layout.layout.data : pair.layout.data).map(heading => {
+ DocListCast(Doc.Layout(pair.layout).data).map(heading => {
let headingPair = Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, heading);
let headingLayout = headingPair.layout && (pair.layout.data_ext instanceof Doc) && (pair.layout.data_ext[`Layout[${headingPair.layout[Id]}]`] as Doc) || headingPair.layout;
if (headingLayout && NumCast(headingLayout.heading) > 0 && headingLayout.backgroundColor !== headingLayout.defaultBackgroundColor) {
@@ -634,16 +603,17 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
}
analyzeStrokes = async () => {
- let data = Cast(this.fieldExtensionDoc.ink, InkField);
- if (data) {
- CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.fieldExtensionDoc, ["inkAnalysis", "handwriting"], data.inkData);
+ const extensionDoc = this.extensionDoc;
+ let data = extensionDoc && Cast(extensionDoc.ink, InkField);
+ if (data && extensionDoc) {
+ CognitiveServices.Inking.Appliers.ConcatenateHandwriting(extensionDoc, ["inkAnalysis", "handwriting"], data.inkData);
}
}
onContextMenu = (e: React.MouseEvent) => {
let layoutItems: ContextMenuProps[] = [];
- if (this.childDocs.some(d => BoolCast(d.isTemplate))) {
+ if (this.childDocs.some(d => BoolCast(d.isTemplateDoc))) {
layoutItems.push({ description: "Template Layout Instance", event: () => this.props.addDocTab(Doc.ApplyTemplate(this.props.Document)!, undefined, "onRight"), icon: "project-diagram" });
}
this._timelineRef.current!.timelineContextMenu(e.nativeEvent);
@@ -673,7 +643,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
if (doc instanceof Doc) {
const [xx, yy] = this.props.ScreenToLocalTransform().transformPoint(x, y);
doc.x = xx, doc.y = yy;
- this.props.addDocument && this.props.addDocument(doc, false);
+ this.props.addDocument && this.props.addDocument(doc);
}
}
}
@@ -702,58 +672,39 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
}
- private childViews = () => [
- <CollectionFreeFormBackgroundView key="backgroundView" {...this.props} {...this.getDocumentViewProps(this.props.Document)} />,
- ...this.views
- ]
+ private childViews = () => {
+ let children = typeof this.props.children === "function" ? (this.props.children as any)() as JSX.Element[] : [];
+ return [
+ ...children,
+ ...this.views,
+ ];
+ }
render() {
// update the actual dimensions of the collection so that they can inquired (e.g., by a minimap)
- this.props.Document.fitX = this.contentBounds && this.contentBounds.x;
- this.props.Document.fitY = this.contentBounds && this.contentBounds.y;
- this.props.Document.fitW = this.contentBounds && (this.contentBounds.r - this.contentBounds.x);
- this.props.Document.fitH = this.contentBounds && (this.contentBounds.b - this.contentBounds.y);
- // if fieldExt is set, then children will be stored in the extension document for the fieldKey.
+ this.Document.fitX = this.contentBounds && this.contentBounds.x;
+ this.Document.fitY = this.contentBounds && this.contentBounds.y;
+ this.Document.fitW = this.contentBounds && (this.contentBounds.r - this.contentBounds.x);
+ 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
- Doc.UpdateDocumentExtensionForField(this.props.DataDoc || this.props.Document, this.props.fieldKey);
- return (
+ return !this.extensionDoc ? (null) :
<div className={"collectionfreeformview-container"} ref={this.createDropTarget} onWheel={this.onPointerWheel}
- style={{ pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined, height: this.isAnnotationOverlay ? (NumCast(this.props.Document.scrollHeight) ? NumCast(this.props.Document.scrollHeight) : "100%") : this.props.PanelHeight() }}
+ style={{ pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined, height: this.isAnnotationOverlay ? (this.props.Document.scrollHeight ? this.Document.scrollHeight : "100%") : this.props.PanelHeight() }}
onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onContextMenu={this.onContextMenu}>
- <MarqueeView container={this} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} isSelected={this.props.isSelected}
- addDocument={this.addDocument} removeDocument={this.props.removeDocument} addLiveTextDocument={this.addLiveTextBox} setPreviewCursor={this.props.setPreviewCursor}
- getContainerTransform={this.getContainerTransform} getTransform={this.getTransform} isAnnotationOverlay={this.isAnnotationOverlay}>
+ <MarqueeView {...this.props} extensionDoc={this.extensionDoc} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} addDocument={this.addDocument}
+ addLiveTextDocument={this.addLiveTextBox} getContainerTransform={this.getContainerTransform} getTransform={this.getTransform} isAnnotationOverlay={this.isAnnotationOverlay}>
<CollectionFreeFormViewPannableContents centeringShiftX={this.centeringShiftX} centeringShiftY={this.centeringShiftY}
easing={this.easing} zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}>
- <CollectionFreeFormLinksView {...this.props} key="freeformLinks">
- <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} AnnotationDocument={this.fieldExtensionDoc} inkFieldKey={"ink"} >
+ {!this.extensionDoc ? (null) :
+ <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} AnnotationDocument={this.extensionDoc} inkFieldKey={"ink"} >
{this.childViews}
- </InkingCanvas>
- </CollectionFreeFormLinksView>
+ </InkingCanvas>}
<CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" />
</CollectionFreeFormViewPannableContents>
</MarqueeView>
<Timeline ref={this._timelineRef} {...this.props} />
{this.overlayViews}
- <CollectionFreeFormOverlayView {...this.props} {...this.getDocumentViewProps(this.props.Document)} />
- </div>
- );
- }
-}
-
-@observer
-class CollectionFreeFormOverlayView extends React.Component<DocumentViewProps & { isSelected: () => boolean }> {
- render() {
- return <DocumentContentsView {...this.props} layoutKey={"overlayLayout"}
- renderDepth={this.props.renderDepth} isSelected={this.props.isSelected} select={emptyFunction} />;
- }
-}
-
-@observer
-class CollectionFreeFormBackgroundView extends React.Component<DocumentViewProps & { isSelected: () => boolean }> {
- render() {
- return !this.props.Document.backgroundLayout ? (null) :
- (<DocumentContentsView {...this.props} layoutKey={"backgroundLayout"}
- renderDepth={this.props.renderDepth} isSelected={this.props.isSelected} select={emptyFunction} />);
+ </div>;
}
}
@@ -774,8 +725,8 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF
const ceny = this.props.centeringShiftY();
const panx = -this.props.panX();
const pany = -this.props.panY();
- const zoom = this.props.zoomScaling();// needs to be a variable outside of the <Measure> otherwise, reactions won't fire
- return <div className={freeformclass} style={{ borderRadius: "inherit", transform: `translate(${cenx}px, ${ceny}px) scale(${zoom}, ${zoom}) translate(${panx}px, ${pany}px)` }}>
+ const zoom = this.props.zoomScaling();
+ return <div className={freeformclass} style={{ borderRadius: "inherit", transform: `translate(${cenx}px, ${ceny}px) scale(${zoom}) translate(${panx}px, ${pany}px)` }}>
{this.props.children}
</div>;
}
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.scss b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
index 9fc2e44fb..04f6ec2ad 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.scss
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
@@ -6,6 +6,13 @@
width:100%;
height:100%;
}
+.marqueeView {
+ overflow: hidden;
+}
+
+.marqueeView:focus-within {
+ overflow: hidden;
+}
.marquee {
border-style: dashed;
box-sizing: border-box;
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 82193aefa..44b6fe030 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -15,27 +15,28 @@ import { Transform } from "../../../util/Transform";
import { undoBatch } from "../../../util/UndoManager";
import { InkingCanvas } from "../../InkingCanvas";
import { PreviewCursor } from "../../PreviewCursor";
-import { CollectionViewType } from "../CollectionBaseView";
+import { CollectionViewType } from "../CollectionView";
import { CollectionFreeFormView } from "./CollectionFreeFormView";
import "./MarqueeView.scss";
import React = require("react");
+import { SubCollectionViewProps } from "../CollectionSubView";
interface MarqueeViewProps {
getContainerTransform: () => Transform;
getTransform: () => Transform;
- container: CollectionFreeFormView;
- addDocument: (doc: Doc, allowDuplicates: false) => boolean;
+ addDocument: (doc: Doc) => boolean;
activeDocuments: () => Doc[];
selectDocuments: (docs: Doc[]) => void;
removeDocument: (doc: Doc) => boolean;
addLiveTextDocument: (doc: Doc) => void;
isSelected: () => boolean;
- isAnnotationOverlay: boolean;
+ extensionDoc: Doc;
+ isAnnotationOverlay?: boolean;
setPreviewCursor?: (func: (x: number, y: number, drag: boolean) => void) => void;
}
@observer
-export class MarqueeView extends React.Component<MarqueeViewProps>
+export class MarqueeView extends React.Component<SubCollectionViewProps & MarqueeViewProps>
{
private _mainCont = React.createRef<HTMLDivElement>();
@observable _lastX: number = 0;
@@ -83,7 +84,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
ns.map(line => {
let indent = line.search(/\S|$/);
let newBox = Docs.Create.TextDocument({ width: 200, height: 35, x: x + indent / 3 * 10, y: y, documentText: "@@@" + line, title: line });
- this.props.addDocument(newBox, false);
+ this.props.addDocument(newBox);
y += 40 * this.props.getTransform().Scale;
});
})();
@@ -92,7 +93,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
navigator.clipboard.readText().then(text => {
let ns = text.split("\n").filter(t => t.trim() !== "\r" && t.trim() !== "");
if (ns.length === 1 && text.startsWith("http")) {
- this.props.addDocument(Docs.Create.ImageDocument(text, { nativeWidth: 300, width: 300, x: x, y: y }), false);// paste an image from its URL in the paste buffer
+ this.props.addDocument(Docs.Create.ImageDocument(text, { nativeWidth: 300, width: 300, x: x, y: y }));// paste an image from its URL in the paste buffer
} else {
this.pasteTable(ns, x, y);
}
@@ -146,7 +147,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
}
let newCol = Docs.Create.SchemaDocument([...(groupAttr ? [new SchemaHeaderField("_group", "#f1efeb")] : []), ...columns.filter(c => c).map(c => new SchemaHeaderField(c, "#f1efeb"))], docList, { x: x, y: y, title: "droppedTable", width: 300, height: 100 });
- this.props.addDocument(newCol, false);
+ this.props.addDocument(newCol);
}
}
@action
@@ -187,13 +188,13 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
@action
onPointerUp = (e: PointerEvent): void => {
- if (!this.props.container.props.active()) this.props.selectDocuments([this.props.container.props.Document]);
+ if (!this.props.active()) this.props.selectDocuments([this.props.Document]);
if (this._visible) {
let mselect = this.marqueeSelect();
if (!e.shiftKey) {
- SelectionManager.DeselectAll(mselect.length ? undefined : this.props.container.props.Document);
+ SelectionManager.DeselectAll(mselect.length ? undefined : this.props.Document);
}
- this.props.selectDocuments(mselect.length ? mselect : [this.props.container.props.Document]);
+ this.props.selectDocuments(mselect.length ? mselect : [this.props.Document]);
}
this.cleanupInteractions(true);
@@ -202,7 +203,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
}
}
- setPreviewCursor = (x: number, y: number, drag: boolean) => {
+ setPreviewCursor = action((x: number, y: number, drag: boolean) => {
if (drag) {
this._downX = this._lastX = x;
this._downY = this._lastY = y;
@@ -217,7 +218,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
this._downY = y;
PreviewCursor.Show(x, y, this.onKeyPress, this.props.addLiveTextDocument, this.props.getTransform, this.props.addDocument);
}
- }
+ });
@action
onClick = (e: React.MouseEvent): void => {
@@ -246,13 +247,11 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
}
get ink() { // ink will be stored on the extension doc for the field (fieldKey) where the container's data is stored.
- let cprops = this.props.container.props;
- return Cast(Doc.fieldExtensionDoc(cprops.Document, cprops.fieldKey).ink, InkField);
+ return this.props.extensionDoc && Cast(this.props.extensionDoc.ink, InkField);
}
set ink(value: InkField | undefined) {
- let cprops = this.props.container.props;
- Doc.fieldExtensionDoc(cprops.Document, cprops.fieldKey).ink = value;
+ this.props.extensionDoc && (this.props.extensionDoc.ink = value);
}
@undoBatch
@@ -285,18 +284,18 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
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.page = -1;
+ d.displayTimecode = undefined;
return d;
});
}
let 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)",];
- let colorPalette = Cast(this.props.container.props.Document.colorPalette, listSpec("string"));
- if (!colorPalette) this.props.container.props.Document.colorPalette = new List<string>(defaultPalette);
- let palette = Array.from(Cast(this.props.container.props.Document.colorPalette, listSpec("string")) as string[]);
+ let colorPalette = Cast(this.props.Document.colorPalette, listSpec("string"));
+ if (!colorPalette) this.props.Document.colorPalette = new List<string>(defaultPalette);
+ let palette = Array.from(Cast(this.props.Document.colorPalette, listSpec("string")) as string[]);
let usedPaletted = new Map<string, number>();
- [...this.props.activeDocuments(), this.props.container.props.Document].map(child => {
- let bg = StrCast(child.layout instanceof Doc ? child.layout.backgroundColor : child.backgroundColor);
+ [...this.props.activeDocuments(), this.props.Document].map(child => {
+ let 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);
@@ -350,7 +349,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
}
}
else {
- this.props.addDocument(newCollection, false);
+ this.props.addDocument(newCollection);
this.props.selectDocuments([newCollection]);
}
this.cleanupInteractions(false);
@@ -394,20 +393,22 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
let selRect = this.Bounds;
let selection: Doc[] = [];
this.props.activeDocuments().filter(doc => !doc.isBackground && doc.z === undefined).map(doc => {
+ let layoutDoc = Doc.Layout(doc);
var x = NumCast(doc.x);
var y = NumCast(doc.y);
- var w = NumCast(doc.width);
- var h = NumCast(doc.height);
+ var w = NumCast(layoutDoc.width);
+ var h = NumCast(layoutDoc.height);
if (this.intersectRect({ left: x, top: y, width: w, height: h }, selRect)) {
selection.push(doc);
}
});
if (!selection.length && selectBackgrounds) {
this.props.activeDocuments().filter(doc => doc.z === undefined).map(doc => {
+ let layoutDoc = Doc.Layout(doc);
var x = NumCast(doc.x);
var y = NumCast(doc.y);
- var w = NumCast(doc.width);
- var h = NumCast(doc.height);
+ var w = NumCast(layoutDoc.width);
+ var h = NumCast(layoutDoc.height);
if (this.intersectRect({ left: x, top: y, width: w, height: h }, selRect)) {
selection.push(doc);
}
@@ -420,10 +421,11 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
let size = this.props.getContainerTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY);
let otherBounds = { left: topLeft[0], top: topLeft[1], width: Math.abs(size[0]), height: Math.abs(size[1]) };
this.props.activeDocuments().filter(doc => doc.z !== undefined).map(doc => {
+ let layoutDoc = Doc.Layout(doc);
var x = NumCast(doc.x);
var y = NumCast(doc.y);
- var w = NumCast(doc.width);
- var h = NumCast(doc.height);
+ var w = NumCast(layoutDoc.width);
+ var h = NumCast(layoutDoc.height);
if (this.intersectRect({ left: x, top: y, width: w, height: h }, otherBounds)) {
selection.push(doc);
}
@@ -434,21 +436,17 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
@computed
get marqueeDiv() {
+ let p: [number, number] = this._visible ? this.props.getContainerTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY) : [0, 0];
let v = this.props.getContainerTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY);
- return <div className="marquee" style={{ width: `${Math.abs(v[0])}`, height: `${Math.abs(v[1])}`, zIndex: 2000 }} >
+ return <div className="marquee" style={{ transform: `translate(${p[0]}px, ${p[1]}px)`, width: `${Math.abs(v[0])}`, height: `${Math.abs(v[1])}`, zIndex: 2000 }} >
<span className="marquee-legend" />
</div>;
}
render() {
- let p: [number, number] = this._visible ? this.props.getContainerTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY) : [0, 0];
- return <div className="marqueeView" style={{ borderRadius: "inherit" }} onClick={this.onClick} onPointerDown={this.onPointerDown}>
- <div style={{ position: "relative", transform: `translate(${p[0]}px, ${p[1]}px)` }} >
- {this._visible ? this.marqueeDiv : null}
- <div ref={this._mainCont} style={{ transform: `translate(${-p[0]}px, ${-p[1]}px)` }} >
- {this.props.children}
- </div>
- </div>
+ return <div className="marqueeView" onScroll={(e) => e.currentTarget.scrollTop = e.currentTarget.scrollLeft = 0} style={{ borderRadius: "inherit" }} onClick={this.onClick} onPointerDown={this.onPointerDown}>
+ {this._visible ? this.marqueeDiv : null}
+ {this.props.children}
</div>;
}
} \ No newline at end of file
diff --git a/src/client/views/linking/LinkFollowBox.tsx b/src/client/views/linking/LinkFollowBox.tsx
index cad404d1f..efe2c7f2a 100644
--- a/src/client/views/linking/LinkFollowBox.tsx
+++ b/src/client/views/linking/LinkFollowBox.tsx
@@ -5,7 +5,7 @@ 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/CollectionBaseView";
+import { CollectionViewType } from "../collections/CollectionView";
import { CollectionDockingView } from "../collections/CollectionDockingView";
import { SelectionManager } from "../../util/SelectionManager";
import { DocumentManager } from "../../util/DocumentManager";
@@ -19,6 +19,7 @@ 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",
@@ -36,7 +37,7 @@ enum FollowOptions {
@observer
export class LinkFollowBox extends React.Component<FieldViewProps> {
- public static LayoutString() { return FieldView.LayoutString(LinkFollowBox); }
+ 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;
@@ -127,7 +128,7 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
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 }));
- let tcontext = LinkFollowBox.linkDoc && (await Cast(LinkFollowBox.linkDoc.targetContext, Doc)) as Doc;
+ let tcontext = LinkFollowBox.linkDoc && (await Cast(LinkFollowBox.linkDoc.anchor2Context, Doc)) as Doc;
runInAction(() => tcontext && this._docs.splice(0, 0, { col: tcontext, target: dest }));
});
}
@@ -151,26 +152,12 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
this.resetPan();
}
- unhighlight = () => {
- Doc.UnhighlightAll();
- document.removeEventListener("pointerdown", this.unhighlight);
- }
-
- @action
- highlightDoc = () => {
- if (LinkFollowBox.destinationDoc) {
- document.removeEventListener("pointerdown", this.unhighlight);
- Doc.HighlightDoc(LinkFollowBox.destinationDoc);
- window.setTimeout(() => {
- document.addEventListener("pointerdown", this.unhighlight);
- }, 10000);
- }
- }
+ highlightDoc = () => LinkFollowBox.destinationDoc && Doc.linkFollowHighlight(LinkFollowBox.destinationDoc);
@undoBatch
openFullScreen = () => {
if (LinkFollowBox.destinationDoc) {
- let view: DocumentView | null = DocumentManager.Instance.getDocumentView(LinkFollowBox.destinationDoc);
+ let view = DocumentManager.Instance.getDocumentView(LinkFollowBox.destinationDoc);
view && CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(view);
}
}
@@ -196,10 +183,10 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
}
- _addDocTab: (undefined | ((doc: Doc, dataDoc: Opt<Doc>, where: string) => boolean));
+ static _addDocTab: (undefined | ((doc: Doc, dataDoc: Opt<Doc>, where: string) => boolean));
- setAddDocTab = (addFunc: (doc: Doc, dataDoc: Opt<Doc>, where: string) => boolean) => {
- this._addDocTab = addFunc;
+ static setAddDocTab = (addFunc: (doc: Doc, dataDoc: Opt<Doc>, where: string) => boolean) => {
+ LinkFollowBox._addDocTab = addFunc;
}
@undoBatch
@@ -212,7 +199,7 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
options.context.panX = newPanX;
options.context.panY = newPanY;
}
- (this._addDocTab || this.props.addDocTab)(options.context, undefined, "onRight");
+ (LinkFollowBox._addDocTab || this.props.addDocTab)(options.context, undefined, "onRight");
if (options.shouldZoom) this.jumpToLink({ shouldZoom: options.shouldZoom });
@@ -225,7 +212,7 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
openLinkRight = () => {
if (LinkFollowBox.destinationDoc) {
let alias = Doc.MakeAlias(LinkFollowBox.destinationDoc);
- (this._addDocTab || this.props.addDocTab)(alias, undefined, "onRight");
+ (LinkFollowBox._addDocTab || this.props.addDocTab)(alias, undefined, "onRight");
this.highlightDoc();
SelectionManager.DeselectAll();
}
@@ -234,45 +221,11 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
@undoBatch
jumpToLink = async (options: { shouldZoom: boolean }) => {
- if (LinkFollowBox.destinationDoc && LinkFollowBox.linkDoc) {
- let jumpToDoc: Doc = LinkFollowBox.destinationDoc;
- let pdfDoc = FieldValue(Cast(LinkFollowBox.destinationDoc, Doc));
- if (pdfDoc) {
- jumpToDoc = pdfDoc;
- }
- let proto = Doc.GetProto(LinkFollowBox.linkDoc);
- let targetContext = await Cast(proto.targetContext, Doc);
- let sourceContext = await Cast(proto.sourceContext, Doc);
- let guid = StrCast(LinkFollowBox.linkDoc[Id]);
- const shouldZoom = options ? options.shouldZoom : false;
-
- let dockingFunc = (document: Doc) => { (this._addDocTab || this.props.addDocTab)(document, undefined, "inTab"); SelectionManager.DeselectAll(); };
-
- if (LinkFollowBox.destinationDoc === LinkFollowBox.linkDoc.anchor2 && targetContext) {
- DocumentManager.Instance.jumpToDocument(jumpToDoc, shouldZoom, false, async document => dockingFunc(document), undefined, targetContext);
- }
- else if (LinkFollowBox.destinationDoc === LinkFollowBox.linkDoc.anchor1 && sourceContext) {
- DocumentManager.Instance.jumpToDocument(jumpToDoc, shouldZoom, false, document => dockingFunc(sourceContext!));
- if (LinkFollowBox.sourceDoc && LinkFollowBox.destinationDoc) {
- if (guid) {
- let views = DocumentManager.Instance.getDocumentViews(jumpToDoc);
- views.length && (views[0].props.Document.scrollToLinkID = guid);
- } else {
- jumpToDoc.linkHref = Utils.prepend("/doc/" + StrCast(LinkFollowBox.linkDoc[Id]));
- }
- }
- }
- else if (DocumentManager.Instance.getDocumentView(jumpToDoc)) {
- DocumentManager.Instance.jumpToDocument(jumpToDoc, shouldZoom, undefined, undefined,
- NumCast((LinkFollowBox.destinationDoc === LinkFollowBox.linkDoc.anchor2 ? LinkFollowBox.linkDoc.anchor2Page : LinkFollowBox.linkDoc.anchor1Page)));
-
- }
- else {
- DocumentManager.Instance.jumpToDocument(jumpToDoc, shouldZoom, false, dockingFunc);
- }
+ if (LinkFollowBox.sourceDoc && LinkFollowBox.linkDoc) {
+ let 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));
- this.highlightDoc();
- SelectionManager.DeselectAll();
+ DocumentManager.Instance.FollowLink(LinkFollowBox.linkDoc, LinkFollowBox.sourceDoc, focus, options && options.shouldZoom, false, undefined);
}
}
@@ -281,7 +234,7 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
if (LinkFollowBox.destinationDoc) {
let fullScreenAlias = Doc.MakeAlias(LinkFollowBox.destinationDoc);
// this.prosp.addDocTab is empty -- use the link source's addDocTab
- (this._addDocTab || this.props.addDocTab)(fullScreenAlias, undefined, "inTab");
+ (LinkFollowBox._addDocTab || this.props.addDocTab)(fullScreenAlias, undefined, "inTab");
this.highlightDoc();
SelectionManager.DeselectAll();
@@ -298,7 +251,7 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
options.context.panX = newPanX;
options.context.panY = newPanY;
}
- (this._addDocTab || this.props.addDocTab)(options.context, undefined, "inTab");
+ (LinkFollowBox._addDocTab || this.props.addDocTab)(options.context, undefined, "inTab");
if (options.shouldZoom) this.jumpToLink({ shouldZoom: options.shouldZoom });
this.highlightDoc();
@@ -310,20 +263,23 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
openLinkInPlace = (options: { shouldZoom: boolean }) => {
if (LinkFollowBox.destinationDoc && LinkFollowBox.sourceDoc) {
- let alias = Doc.MakeAlias(LinkFollowBox.destinationDoc);
- let y = NumCast(LinkFollowBox.sourceDoc.y);
- let x = NumCast(LinkFollowBox.sourceDoc.x);
+ if (this.sourceView && this.sourceView.props.addDocument) {
+ let destViews = DocumentManager.Instance.getDocumentViews(LinkFollowBox.destinationDoc);
+ if (!destViews.find(dv => dv.props.ContainingCollectionView === this.sourceView!.props.ContainingCollectionView)) {
+ let alias = Doc.MakeAlias(LinkFollowBox.destinationDoc);
+ let y = NumCast(LinkFollowBox.sourceDoc.y);
+ let x = NumCast(LinkFollowBox.sourceDoc.x);
- let width = NumCast(LinkFollowBox.sourceDoc.width);
- let height = NumCast(LinkFollowBox.sourceDoc.height);
+ let width = NumCast(LinkFollowBox.sourceDoc.width);
+ let height = NumCast(LinkFollowBox.sourceDoc.height);
- alias.x = x + width + 30;
- alias.y = y;
- alias.width = width;
- alias.height = height;
+ alias.x = x + width + 30;
+ alias.y = y;
+ alias.width = width;
+ alias.height = height;
- if (this.sourceView && this.sourceView.props.addDocument) {
- this.sourceView.props.addDocument(alias, false);
+ this.sourceView.props.addDocument(alias);
+ }
}
this.jumpToLink({ shouldZoom: options.shouldZoom });
diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index 835554ac0..238660de3 100644
--- a/src/client/views/linking/LinkMenuItem.tsx
+++ b/src/client/views/linking/LinkMenuItem.tsx
@@ -8,7 +8,6 @@ import { Cast, StrCast } from '../../../new_fields/Types';
import { DragLinkAsDocument } from '../../util/DragManager';
import { LinkManager } from '../../util/LinkManager';
import { ContextMenu } from '../ContextMenu';
-import { MainView } from '../MainView';
import { LinkFollowBox } from './LinkFollowBox';
import './LinkMenu.scss';
import React = require("react");
@@ -70,7 +69,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
if (LinkFollowBox.Instance !== undefined) {
LinkFollowBox.Instance.props.Document.isMinimized = false;
LinkFollowBox.Instance.setLinkDocs(this.props.linkDoc, this.props.sourceDoc, this.props.destinationDoc);
- LinkFollowBox.Instance.setAddDocTab(this.props.addDocTab);
+ LinkFollowBox.setAddDocTab(this.props.addDocTab);
}
e.stopPropagation();
}
@@ -95,6 +94,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
@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();
}
@@ -104,7 +104,6 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
async openLinkFollower() {
if (LinkFollowBox.Instance !== undefined) {
LinkFollowBox.Instance.props.Document.isMinimized = false;
- MainView.Instance.toggleLinkFollowBox(false);
LinkFollowBox.Instance.setLinkDocs(this.props.linkDoc, this.props.sourceDoc, this.props.destinationDoc);
}
}
diff --git a/src/client/views/nodes/Annotation.tsx b/src/client/views/nodes/Annotation.tsx
deleted file mode 100644
index 3e4ed6bf1..000000000
--- a/src/client/views/nodes/Annotation.tsx
+++ /dev/null
@@ -1,117 +0,0 @@
-import "./ImageBox.scss";
-import React = require("react");
-import { observer } from "mobx-react";
-import { observable, action } from 'mobx';
-import 'react-pdf/dist/Page/AnnotationLayer.css';
-
-interface IProps {
- Span: HTMLSpanElement;
- X: number;
- Y: number;
- Highlights: any[];
- Annotations: any[];
- CurrAnno: any[];
-
-}
-
-/**
- * Annotation class is used to take notes on a particular highlight. You can also change highlighted span's color
- * Improvements to be made: Removing the annotation when onRemove is called. (Removing this, not just the highlighted span).
- * Also need to support multiline highlighting
- *
- * Written by: Andrew Kim
- */
-@observer
-export class Annotation extends React.Component<IProps> {
-
- /**
- * changes color of the span (highlighted section)
- */
- onColorChange = (e: React.PointerEvent) => {
- if (e.currentTarget.innerHTML === "r") {
- this.props.Span.style.backgroundColor = "rgba(255,0,0, 0.3)";
- } else if (e.currentTarget.innerHTML === "b") {
- this.props.Span.style.backgroundColor = "rgba(0,255, 255, 0.3)";
- } else if (e.currentTarget.innerHTML === "y") {
- this.props.Span.style.backgroundColor = "rgba(255,255,0, 0.3)";
- } else if (e.currentTarget.innerHTML === "g") {
- this.props.Span.style.backgroundColor = "rgba(76, 175, 80, 0.3)";
- }
-
- }
-
- /**
- * removes the highlighted span. Supposed to remove Annotation too, but I don't know how to unmount this
- */
- @action
- onRemove = (e: any) => {
- let index: number = -1;
- //finding the highlight in the highlight array
- this.props.Highlights.forEach((e) => {
- for (const span of e.spans) {
- if (span === this.props.Span) {
- index = this.props.Highlights.indexOf(e);
- this.props.Highlights.splice(index, 1);
- }
- }
- });
-
- //removing from CurrAnno and Annotation array
- this.props.Annotations.splice(index, 1);
- this.props.CurrAnno.pop();
-
- //removing span from div
- if (this.props.Span.parentElement) {
- let nodesArray = this.props.Span.parentElement.childNodes;
- nodesArray.forEach((e) => {
- if (e === this.props.Span) {
- if (this.props.Span.parentElement) {
- this.props.Highlights.forEach((item) => {
- if (item === e) {
- item.remove();
- }
- });
- e.remove();
- }
- }
- });
- }
-
-
- }
-
- render() {
- return (
- <div
- style={{
- position: "absolute",
- top: "20px",
- left: "0px",
- zIndex: 1,
- transform: `translate(${this.props.X}px, ${this.props.Y}px)`,
-
- }}>
- <div style={{ width: "200px", height: "50px", backgroundColor: "orange" }}>
- <button
- style={{ borderRadius: "25px", width: "25%", height: "100%" }}
- onClick={this.onRemove}
- >x</button>
- <div style={{ width: "75%", height: "100%", display: "inline-block" }}>
- <button onPointerDown={this.onColorChange} style={{ backgroundColor: "red", borderRadius: "50%", color: "transparent" }}>r</button>
- <button onPointerDown={this.onColorChange} style={{ backgroundColor: "blue", borderRadius: "50%", color: "transparent" }}>b</button>
- <button onPointerDown={this.onColorChange} style={{ backgroundColor: "yellow", borderRadius: "50%", color: "transparent" }}>y</button>
- <button onPointerDown={this.onColorChange} style={{ backgroundColor: "green", borderRadius: "50%", color: "transparent" }}>g</button>
- </div>
-
- </div>
- <div style={{ width: "200px", height: "200" }}>
- <textarea style={{ width: "100%", height: "100%" }}
- defaultValue="Enter Text Here..."
-
- ></textarea>
- </div>
- </div>
-
- );
- }
-} \ No newline at end of file
diff --git a/src/client/views/nodes/AudioBox.scss b/src/client/views/nodes/AudioBox.scss
index 972966204..3b19a6dba 100644
--- a/src/client/views/nodes/AudioBox.scss
+++ b/src/client/views/nodes/AudioBox.scss
@@ -1,6 +1,134 @@
-.audiobox-cont{
- top:0;
- max-height: 32px;
- position: absolute;
+.audiobox-container, .audiobox-container-interactive {
width: 100%;
+ height: 100%;
+ position: inherit;
+ display:flex;
+ pointer-events: all;
+ cursor:default;
+ .audiobox-handle {
+ width:20px;
+ height:100%;
+ display:inline-block;
+ background: gray;
+ }
+ .audiobox-control, .audiobox-control-interactive {
+ top:0;
+ max-height: 32px;
+ width: 100%;
+ display:inline-block;
+ pointer-events: none;
+ }
+ .audiobox-control-interactive {
+ pointer-events: all;
+ }
+ .audiobox-record {
+ pointer-events: all;
+ width:100%;
+ height:100%;
+ position: absolute;
+ pointer-events: none;
+ }
+ .audiobox-record-interactive {
+ pointer-events: all;
+ }
+ .audiobox-controls {
+ width:100%;
+ height:100%;
+ position: relative;
+ display: flex;
+ padding-left: 2px;
+ border: gray solid 3px;
+ .audiobox-player {
+ margin-top:auto;
+ margin-bottom:auto;
+ width:100%;
+ height: 80%;
+ position: relative;
+ padding-right: 5px;
+ display: flex;
+ .audiobox-playhead {
+ position: relative;
+ margin-top: auto;
+ margin-bottom: auto;
+ width: 25px;
+ padding: 2px;
+ }
+ .audiobox-timeline {
+ position:relative;
+ height:100%;
+ width:100%;
+ background: white;
+ border: gray solid 1px;
+ border-radius: 3px;
+ .audiobox-current {
+ width: 1px;
+ height:100%;
+ background-color: red;
+ position: absolute;
+ }
+ .audiobox-linker, .audiobox-linker-mini {
+ position:absolute;
+ width:15px;
+ min-height:10px;
+ height:15px;
+ margin-left:-2.55px;
+ background:gray;
+ border-radius: 100%;
+ background-color: transparent;
+ box-shadow: black 2px 2px 1px;
+ .docuLinkBox-cont {
+ position: relative !important;
+ height: 100% !important;
+ width: 100% !important;
+ left:unset !important;
+ top:unset !important;
+ }
+ }
+ .audiobox-linker-mini {
+ width:8px;
+ min-height:8px;
+ height:8px;
+ box-shadow: black 1px 1px 1px;
+ margin-left: -1;
+ margin-top: -2;
+ .docuLinkBox-cont {
+ position: relative !important;
+ height: 100% !important;
+ width: 100% !important;
+ left:unset !important;
+ top:unset !important;
+ }
+ }
+ .audiobox-linker:hover, .audiobox-linker-mini:hover {
+ transform:scale(1.5);
+ }
+ .audiobox-marker-container, .audiobox-marker-minicontainer {
+ position:absolute;
+ width:10px;
+ height:90%;
+ top:2.5%;
+ background:gray;
+ border-radius: 5px;
+ box-shadow: black 2px 2px 1px;
+ .audiobox-marker {
+ position:relative;
+ height: calc(100% - 15px);
+ margin-top: 15px;
+ }
+ .audio-marker:hover {
+ border: orange 2px solid;
+ }
+ }
+ .audiobox-marker-minicontainer {
+ width:5px;
+ border-radius: 1px;
+ .audiobox-marker {
+ position:relative;
+ height: calc(100% - 8px);
+ margin-top: 8px;
+ }
+ }
+ }
+ }
+ }
} \ No newline at end of file
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index be6ae630f..86bd23b67 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -2,24 +2,267 @@ import React = require("react");
import { FieldViewProps, FieldView } from './FieldView';
import { observer } from "mobx-react";
import "./AudioBox.scss";
-import { Cast } from "../../../new_fields/Types";
-import { AudioField } from "../../../new_fields/URLField";
+import { Cast, DateCast, NumCast } from "../../../new_fields/Types";
+import { AudioField, nullAudio } from "../../../new_fields/URLField";
+import { DocExtendableComponent } from "../DocComponent";
+import { makeInterface, createSchema } from "../../../new_fields/Schema";
+import { documentSchema } from "../../../new_fields/documentSchemas";
+import { Utils, returnTrue, emptyFunction, returnOne, returnTransparent } from "../../../Utils";
+import { RouteStore } from "../../../server/RouteStore";
+import { runInAction, observable, reaction, IReactionDisposer, computed, action } from "mobx";
+import { DateField } from "../../../new_fields/DateField";
+import { SelectionManager } from "../../util/SelectionManager";
+import { Doc, DocListCast } from "../../../new_fields/Doc";
+import { ContextMenuProps } from "../ContextMenuItem";
+import { ContextMenu } from "../ContextMenu";
+import { Id } from "../../../new_fields/FieldSymbols";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { DocumentView } from "./DocumentView";
+
+interface Window {
+ MediaRecorder: MediaRecorder;
+}
+
+declare class MediaRecorder {
+ // whatever MediaRecorder has
+ constructor(e: any);
+}
+export const audioSchema = createSchema({
+ playOnSelect: "boolean"
+});
+
+type AudioDocument = makeInterface<[typeof documentSchema, typeof audioSchema]>;
+const AudioDocument = makeInterface(documentSchema, audioSchema);
-const defaultField: AudioField = new AudioField(new URL("http://techslides.com/demos/samples/sample.mp3"));
@observer
-export class AudioBox extends React.Component<FieldViewProps> {
+export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocument>(AudioDocument) {
+ public static LayoutString(fieldKey: string) { return FieldView.LayoutString(AudioBox, fieldKey); }
+ public static Enabled = false;
+
+ _linkPlayDisposer: IReactionDisposer | undefined;
+ _reactionDisposer: IReactionDisposer | undefined;
+ _scrubbingDisposer: IReactionDisposer | undefined;
+ _ele: HTMLAudioElement | null = null;
+ _recorder: any;
+ _recordStart = 0;
+
+ @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);
+ public static ActiveRecordings: Doc[] = [];
+
+ componentDidMount() {
+ runInAction(() => this._audioState = this.path ? "recorded" : "unrecorded");
+ this._linkPlayDisposer = reaction(() => this.layoutDoc.scrollToLinkID,
+ scrollLinkId => {
+ scrollLinkId && DocListCast(this.dataDoc.links).filter(l => l[Id] === scrollLinkId).map(l => {
+ let la1 = l.anchor1 as Doc;
+ let 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);
+ }, { fireImmediately: true });
+ this._reactionDisposer = reaction(() => SelectionManager.SelectedDocuments(),
+ selected => {
+ let 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._scrubbingDisposer = reaction(() => AudioBox._scrubTime, timeInMillisecondsFrom1970 => {
+ let start = this.extensionDoc && DateCast(this.extensionDoc.recordingStart);
+ start && this.playFrom((timeInMillisecondsFrom1970 - start.date.getTime()) / 1000);
+ });
+ }
+
+ timecodeChanged = () => {
+ const htmlEle = this._ele;
+ if (this._audioState === "recorded" && 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);
+ if (Doc.AreProtosEqual(la1, this.dataDoc)) {
+ la1 = l.anchor2 as Doc;
+ linkTime = NumCast(l.anchor1Timecode);
+ }
+ if (linkTime > NumCast(this.Document.currentTimecode) && linkTime < htmlEle.currentTime) {
+ Doc.linkFollowHighlight(la1);
+ }
+ });
+ this.Document.currentTimecode = htmlEle.currentTime;
+ }
+ }
+
+ pause = action(() => {
+ this._ele!.pause();
+ this._playing = false;
+ });
+
+ playFrom = (seekTimeInSeconds: number) => {
+ if (this._ele && AudioBox.Enabled) {
+ if (seekTimeInSeconds < 0) {
+ this.pause();
+ } else if (seekTimeInSeconds <= this._ele.duration) {
+ this._ele.currentTime = seekTimeInSeconds;
+ this._ele.play();
+ runInAction(() => this._playing = true);
+ } else {
+ this.pause();
+ }
+ }
+ }
+
+ componentWillUnmount() {
+ this._reactionDisposer && this._reactionDisposer();
+ this._linkPlayDisposer && this._linkPlayDisposer();
+ this._scrubbingDisposer && this._scrubbingDisposer();
+ }
+
+
+ updateRecordTime = () => {
+ if (this._audioState === "recording") {
+ setTimeout(this.updateRecordTime, 30);
+ this.Document.currentTimecode = (new Date().getTime() - this._recordStart) / 1000;
+ }
+ }
- public static LayoutString() { return FieldView.LayoutString(AudioBox); }
+ recordAudioAnnotation = () => {
+ let gumStream: any;
+ let self = this;
+ const extensionDoc = this.extensionDoc;
+ extensionDoc && navigator.mediaDevices.getUserMedia({
+ audio: true
+ }).then(function (stream) {
+ gumStream = stream;
+ self._recorder = new MediaRecorder(stream);
+ extensionDoc.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(RouteStore.upload), {
+ 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?
+ });
+ }
+
+ specificContextMenu = (e: React.MouseEvent): void => {
+ let funcs: ContextMenuProps[] = [];
+ funcs.push({ description: (this.Document.playOnSelect ? "Don't play" : "Play") + " when document selected", event: () => this.Document.playOnSelect = !this.Document.playOnSelect, icon: "expand-arrows-alt" });
+
+ ContextMenu.Instance.addItem({ description: "Audio Funcs...", subitems: funcs, icon: "asterisk" });
+ }
+
+ stopRecording = action(() => {
+ this._recorder.stop();
+ this.dataDoc.duration = (new Date().getTime() - this._recordStart) / 1000;
+ this._audioState = "recorded";
+ let ind = AudioBox.ActiveRecordings.indexOf(this.props.Document);
+ ind !== -1 && (AudioBox.ActiveRecordings.splice(ind, 1));
+ });
+
+ recordClick = (e: React.MouseEvent) => {
+ if (e.button === 0 && !e.ctrlKey) {
+ this._recorder ? this.stopRecording() : this.recordAudioAnnotation();
+ e.stopPropagation();
+ }
+ }
+
+ onPlay = (e: any) => {
+ this.playFrom(this._ele!.paused ? this._ele!.currentTime : -1);
+ e.stopPropagation();
+ }
+ onStop = (e: any) => {
+ this.pause();
+ this._ele!.currentTime = 0;
+ e.stopPropagation();
+ }
+
+ setRef = (e: HTMLAudioElement | null) => {
+ e && e.addEventListener("timeupdate", this.timecodeChanged);
+ e && e.addEventListener("ended", this.pause);
+ this._ele = e;
+ }
+
+ @computed get path() {
+ let field = Cast(this.props.Document[this.props.fieldKey], AudioField);
+ let path = (field instanceof AudioField) ? field.url.href : "";
+ return path === nullAudio ? "" : path;
+ }
+
+ @computed get audio() {
+ let interactive = this.active() ? "-interactive" : "";
+ return <audio ref={this.setRef} className={`audiobox-control${interactive}`}>
+ <source src={this.path} type="audio/mpeg" />
+ Not supported.
+ </audio>;
+ }
render() {
- let field = Cast(this.props.Document[this.props.fieldKey], AudioField, defaultField);
- let path = field.url.href;
-
- return (
- <audio controls className="audiobox-cont" style={{ pointerEvents: "all" }}>
- <source src={path} type="audio/mpeg" />
- Not supported.
- </audio>
+ let interactive = this.active() ? "-interactive" : "";
+ return (!this.extensionDoc ? (null) :
+ <div className={`audiobox-container`} onContextMenu={this.specificContextMenu}
+ onClick={!this.path ? this.recordClick : undefined}>
+ <div className="audiobox-handle"></div>
+ {!this.path ?
+ <button className={`audiobox-record${interactive}`} style={{ backgroundColor: this._audioState === "recording" ? "red" : "black" }}>
+ {this._audioState === "recording" ? "STOP" : "RECORD"}
+ </button> :
+ <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-timeline" onClick={e => e.stopPropagation()}
+ onPointerDown={e => {
+ if (e.button === 0 && !e.ctrlKey) {
+ let rect = (e.target as any).getBoundingClientRect();
+ this._ele!.currentTime = this.Document.currentTimecode = (e.clientX - rect.x) / rect.width * NumCast(this.dataDoc.duration);
+ 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);
+ if (Doc.AreProtosEqual(la1, this.dataDoc)) {
+ la1 = l.anchor2 as Doc;
+ la2 = l.anchor1 as Doc;
+ linkTime = NumCast(l.anchor1Timecode);
+ }
+ 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)}
+ 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(); } }} />
+ </div>;
+ })}
+ <div className="audiobox-current" style={{ left: `${NumCast(this.Document.currentTimecode) / NumCast(this.dataDoc.duration, 1) * 100}%` }} />
+ {this.audio}
+ </div>
+ </div>
+ </div>
+ }
+ </div>
);
}
} \ No newline at end of file
diff --git a/src/client/views/nodes/ButtonBox.scss b/src/client/views/nodes/ButtonBox.scss
index 75a790667..e8a3d1479 100644
--- a/src/client/views/nodes/ButtonBox.scss
+++ b/src/client/views/nodes/ButtonBox.scss
@@ -13,6 +13,8 @@
border-radius: inherit;
text-align: center;
display: table;
+ overflow: hidden;
+ text-overflow: ellipsis;
}
.buttonBox-mainButtonCenter {
height: 100%;
diff --git a/src/client/views/nodes/ButtonBox.tsx b/src/client/views/nodes/ButtonBox.tsx
index f08ea4891..beb2b30fd 100644
--- a/src/client/views/nodes/ButtonBox.tsx
+++ b/src/client/views/nodes/ButtonBox.tsx
@@ -3,11 +3,11 @@ import { faEdit } from '@fortawesome/free-regular-svg-icons';
import { action, computed } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { Doc, DocListCastAsync, DocListCast } from '../../../new_fields/Doc';
+import { Doc, DocListCast } from '../../../new_fields/Doc';
import { List } from '../../../new_fields/List';
import { createSchema, makeInterface, listSpec } from '../../../new_fields/Schema';
import { ScriptField } from '../../../new_fields/ScriptField';
-import { BoolCast, StrCast, Cast } from '../../../new_fields/Types';
+import { BoolCast, StrCast, Cast, FieldValue } from '../../../new_fields/Types';
import { DragManager } from '../../util/DragManager';
import { undoBatch } from '../../util/UndoManager';
import { DocComponent } from '../DocComponent';
@@ -15,24 +15,30 @@ import './ButtonBox.scss';
import { FieldView, FieldViewProps } from './FieldView';
import { ContextMenuProps } from '../ContextMenuItem';
import { ContextMenu } from '../ContextMenu';
+import { documentSchema } from '../../../new_fields/documentSchemas';
library.add(faEdit as any);
const ButtonSchema = createSchema({
onClick: ScriptField,
+ buttonParams: listSpec("string"),
text: "string"
});
-type ButtonDocument = makeInterface<[typeof ButtonSchema]>;
-const ButtonDocument = makeInterface(ButtonSchema);
+type ButtonDocument = makeInterface<[typeof ButtonSchema, typeof documentSchema]>;
+const ButtonDocument = makeInterface(ButtonSchema, documentSchema);
@observer
export class ButtonBox extends DocComponent<FieldViewProps, ButtonDocument>(ButtonDocument) {
- public static LayoutString() { return FieldView.LayoutString(ButtonBox); }
+ public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ButtonBox, fieldKey); }
private dropDisposer?: DragManager.DragDropDisposer;
- @computed get dataDoc() { return this.props.DataDoc && (BoolCast(this.props.Document.isTemplate) || BoolCast(this.props.DataDoc.isTemplate) || this.props.DataDoc.layout === this.props.Document) ? this.props.DataDoc : Doc.GetProto(this.props.Document); }
+ @computed get dataDoc() {
+ return this.props.DataDoc &&
+ (this.Document.isTemplateField || BoolCast(this.props.DataDoc.isTemplateField) ||
+ this.props.DataDoc.layout === this.props.Document) ? this.props.DataDoc : Doc.GetProto(this.props.Document);
+ }
protected createDropTarget = (ele: HTMLDivElement) => {
@@ -48,7 +54,7 @@ export class ButtonBox extends DocComponent<FieldViewProps, ButtonDocument>(Butt
let funcs: ContextMenuProps[] = [];
funcs.push({
description: "Clear Script Params", event: () => {
- let params = Cast(this.props.Document.buttonParams, listSpec("string"));
+ let params = FieldValue(this.Document.buttonParams);
params && params.map(p => this.props.Document[p] = undefined);
}, icon: "trash"
});
@@ -60,18 +66,20 @@ export class ButtonBox extends DocComponent<FieldViewProps, ButtonDocument>(Butt
@action
drop = (e: Event, de: DragManager.DropEvent) => {
if (de.data instanceof DragManager.DocumentDragData && e.target) {
- this.props.Document[(e.target as any).textContent] = new List<Doc>(de.data.droppedDocuments);
+ this.props.Document[(e.target as any).textContent] = new List<Doc>(de.data.droppedDocuments.map((d, i) =>
+ d.onDragStart ? de.data.draggedDocuments[i] : d));
e.stopPropagation();
}
}
// (!missingParams || !missingParams.length ? "" : "(" + missingParams.map(m => m + ":").join(" ") + ")")
render() {
- let params = Cast(this.props.Document.buttonParams, listSpec("string"));
+ let params = this.Document.buttonParams;
let missingParams = params && params.filter(p => this.props.Document[p] === undefined);
params && params.map(p => DocListCast(this.props.Document[p])); // bcz: really hacky form of prefetching ...
return (
- <div className="buttonBox-outerDiv" ref={this.createDropTarget} onContextMenu={this.specificContextMenu}>
- <div className="buttonBox-mainButton" style={{ background: StrCast(this.props.Document.backgroundColor), color: StrCast(this.props.Document.color, "black") }} >
+ <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" }} >
<div className="buttonBox-mainButtonCenter">
{(this.Document.text || this.Document.title)}
</div>
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.scss b/src/client/views/nodes/CollectionFreeFormDocumentView.scss
index de0b00a81..af9232c2f 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.scss
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.scss
@@ -1,5 +1,7 @@
-.collectionFreeFormDocumentView-container {
- transform-origin: left top;
- position: absolute;
- background-color: transparent;
+.collectionFreeFormDocumentView-container {
+ transform-origin: left top;
+ position: absolute;
+ background-color: transparent;
+ top:0;
+ left:0;
} \ No newline at end of file
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index dd063ec9d..a035bdc3d 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -1,15 +1,15 @@
-import { computed, action, observable, reaction, IReactionDisposer, trace } from "mobx";
+import { random } from "animejs";
+import { computed, IReactionDisposer, observable, reaction, trace } from "mobx";
import { observer } from "mobx-react";
-import { createSchema, makeInterface, listSpec } from "../../../new_fields/Schema";
-import { FieldValue, NumCast, StrCast, Cast } from "../../../new_fields/Types";
+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";
-import { percent2frac } from "../../../Utils";
-import { DocumentView, DocumentViewProps, documentSchema } from "./DocumentView";
import "./CollectionFreeFormDocumentView.scss";
+import { DocumentView, DocumentViewProps } from "./DocumentView";
import React = require("react");
-import { Doc, WidthSym, HeightSym } from "../../../new_fields/Doc";
-import { random } from "animejs";
+import { PositionDocument } from "../../../new_fields/documentSchemas";
export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
dataProvider?: (doc: Doc, dataDoc?: Doc) => { x: number, y: number, width: number, height: number, z: number, transition?: string } | undefined;
@@ -20,27 +20,18 @@ export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
jitterRotation: number;
transition?: string;
}
-export const positionSchema = createSchema({
- zIndex: "number",
- x: "number",
- y: "number",
- z: "number",
-});
-
-export type PositionDocument = makeInterface<[typeof documentSchema, typeof positionSchema]>;
-export const PositionDocument = makeInterface(documentSchema, positionSchema);
@observer
export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps, PositionDocument>(PositionDocument) {
_disposer: IReactionDisposer | undefined = undefined;
get transform() { return `scale(${this.props.ContentScaling()}) translate(${this.X}px, ${this.Y}px) rotate(${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 width() { return this.renderScriptDim ? this.renderScriptDim.width : this.props.width !== undefined ? this.props.width : this.props.dataProvider && this.dataProvider ? this.dataProvider.width : this.props.Document[WidthSym](); }
- get height() { return this.renderScriptDim ? this.renderScriptDim.height : this.props.height !== undefined ? this.props.height : this.props.dataProvider && this.dataProvider ? this.dataProvider.height : this.props.Document[HeightSym](); }
+ 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 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() { return this.renderScriptDim ? this.renderScriptDim.height : this.props.height !== undefined ? this.props.height : this.props.dataProvider && this.dataProvider ? this.dataProvider.height : this.layoutDoc[HeightSym](); }
@computed get dataProvider() { return this.props.dataProvider && this.props.dataProvider(this.props.Document, this.props.DataDoc) ? this.props.dataProvider(this.props.Document, this.props.DataDoc) : undefined; }
- @computed get nativeWidth() { return FieldValue(this.Document.nativeWidth, 0); }
- @computed get nativeHeight() { return FieldValue(this.Document.nativeHeight, 0); }
+ @computed get nativeWidth() { return NumCast(this.layoutDoc.nativeWidth); }
+ @computed get nativeHeight() { return NumCast(this.layoutDoc.nativeHeight); }
@computed get renderScriptDim() {
if (this.Document.renderScript) {
@@ -64,7 +55,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
this._disposer = reaction(() => [this.props.Document.animateToPos, this.props.Document.isAnimating],
() => {
const target = this.props.Document.animateToPos ? Array.from(Cast(this.props.Document.animateToPos, listSpec("number"))!) : undefined;
- this._animPos = !target ? undefined : target[2] ? [this.Document.x || 0, this.Document.y || 0] : this.props.ScreenToLocalTransform().transformPoint(target[0], target[1]);
+ this._animPos = !target ? undefined : target[2] ? [NumCast(this.layoutDoc.x), NumCast(this.layoutDoc.y)] : this.props.ScreenToLocalTransform().transformPoint(target[0], target[1]);
}, { fireImmediately: true });
}
@@ -77,14 +68,9 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
borderRounding = () => {
let ruleRounding = this.props.ruleProvider ? StrCast(this.props.ruleProvider["ruleRounding_" + this.Document.heading]) : undefined;
- let ld = this.layoutDoc.layout instanceof Doc ? this.layoutDoc.layout : undefined;
+ let ld = this.layoutDoc[StrCast(this.layoutDoc.layoutKey, "layout")] instanceof Doc ? this.layoutDoc[StrCast(this.layoutDoc.layoutKey, "layout")] as Doc : undefined;
let br = StrCast((ld || this.props.Document).borderRounding);
- br = !br && ruleRounding ? ruleRounding : br;
- if (br.endsWith("%")) {
- let nativeDim = Math.min(NumCast(this.layoutDoc.nativeWidth), NumCast(this.layoutDoc.nativeHeight));
- return percent2frac(br) * (nativeDim ? nativeDim : Math.min(this.props.PanelWidth(), this.props.PanelHeight()));
- }
- return undefined;
+ return !br && ruleRounding ? ruleRounding : br;
}
@computed
@@ -92,42 +78,35 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
clusterColorFunc = (doc: Doc) => this.clusterColor;
- get layoutDoc() {
- // if this document's layout field contains a document (ie, a rendering template), then we will use that
- // to determine the render JSX string, otherwise the layout field should directly contain a JSX layout string.
- return this.props.Document.layout instanceof Doc ? this.props.Document.layout : this.props.Document;
- }
-
@observable _animPos: number[] | undefined = undefined;
finalPanelWidth = () => this.dataProvider ? this.dataProvider.width : this.panelWidth();
finalPanelHeight = () => this.dataProvider ? this.dataProvider.height : this.panelHeight();
render() {
- return (
- <div className="collectionFreeFormDocumentView-container"
- style={{
- 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.layoutDoc.isBackground ? `1px 1px 1px ${this.clusterColor}` : // if it's a background & has a cluster color, make the shadow spread really big
- StrCast(this.layoutDoc.boxShadow, ""),
- borderRadius: this.borderRounding(),
- transform: this.transform,
- // transition: this.Document.isAnimating !== undefined ? ".5s ease-in" : 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,
- }} >
- <DocumentView {...this.props}
- ContentScaling={this.contentScaling}
- ScreenToLocalTransform={this.getTransform}
- backgroundColor={this.clusterColorFunc}
- PanelWidth={this.finalPanelWidth}
- PanelHeight={this.finalPanelHeight}
- />
- </div>
- );
+ trace();
+ return <div className="collectionFreeFormDocumentView-container"
+ style={{
+ 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.layoutDoc.isBackground ? `1px 1px 1px ${this.clusterColor}` : // if it's a background & has a cluster color, make the shadow spread really big
+ StrCast(this.layoutDoc.boxShadow, ""),
+ borderRadius: this.borderRounding(),
+ transform: this.transform,
+ transition: this.Document.isAnimating !== undefined ? ".5s ease-in" : 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,
+ }} >
+ <DocumentView {...this.props}
+ ContentScaling={this.contentScaling}
+ ScreenToLocalTransform={this.getTransform}
+ backgroundColor={this.clusterColorFunc}
+ PanelWidth={this.finalPanelWidth}
+ PanelHeight={this.finalPanelHeight}
+ />
+ </div>;
}
} \ No newline at end of file
diff --git a/src/client/views/nodes/ColorBox.scss b/src/client/views/nodes/ColorBox.scss
new file mode 100644
index 000000000..bf334c939
--- /dev/null
+++ b/src/client/views/nodes/ColorBox.scss
@@ -0,0 +1,22 @@
+.colorBox-container, .colorBox-container-interactive {
+ width:100%;
+ height:100%;
+ position: relative;
+ pointer-events: none;
+
+ .sketch-picker {
+ margin:auto;
+ div {
+ cursor: crosshair;
+ }
+ .flexbox-fix {
+ cursor: pointer;
+ div {
+ cursor:pointer;
+ }
+ }
+ }
+}
+.colorBox-container-interactive {
+ pointer-events:all;
+} \ No newline at end of file
diff --git a/src/client/views/nodes/ColorBox.tsx b/src/client/views/nodes/ColorBox.tsx
new file mode 100644
index 000000000..fda6d64f4
--- /dev/null
+++ b/src/client/views/nodes/ColorBox.tsx
@@ -0,0 +1,45 @@
+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 { 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";
+
+type ColorDocument = makeInterface<[typeof documentSchema]>;
+const ColorDocument = makeInterface(documentSchema);
+
+@observer
+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() {
+ return <div className={`colorBox-container${this.active() ? "-interactive" : ""}`}
+ onPointerDown={e => e.button === 0 && !e.ctrlKey && e.stopPropagation()}>
+ <SketchPicker color={this._startupColor} onChange={InkingControl.Instance.switchColor} />
+ </div>;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/ContentFittingDocumentView.scss b/src/client/views/nodes/ContentFittingDocumentView.scss
new file mode 100644
index 000000000..796e67269
--- /dev/null
+++ b/src/client/views/nodes/ContentFittingDocumentView.scss
@@ -0,0 +1,23 @@
+@import "../globalCssVariables";
+
+.contentFittingDocumentView {
+ position: relative;
+ height: auto !important;
+
+ .contentFittingDocumentView-previewDoc {
+ position: absolute;
+ display: inline;
+ }
+
+ .contentFittingDocumentView-input {
+ position: absolute;
+ max-width: 150px;
+ width: 100%;
+ bottom: 0px;
+ }
+
+ .documentView-node:first-child {
+ position: relative;
+ background: $light-color;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/ContentFittingDocumentView.tsx b/src/client/views/nodes/ContentFittingDocumentView.tsx
new file mode 100644
index 000000000..f41c4fc91
--- /dev/null
+++ b/src/client/views/nodes/ContentFittingDocumentView.tsx
@@ -0,0 +1,117 @@
+import React = require("react");
+import { action, 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 { NumCast, StrCast } from "../../../new_fields/Types";
+import { emptyFunction, returnEmptyString, returnOne } from "../../../Utils";
+import { DragManager } from "../../util/DragManager";
+import { Transform } from "../../util/Transform";
+import { undoBatch } from "../../util/UndoManager";
+import '../DocumentDecorations.scss';
+import { DocumentView } from "../nodes/DocumentView";
+import "./ContentFittingDocumentView.scss";
+import { CollectionView } from "../collections/CollectionView";
+
+interface ContentFittingDocumentViewProps {
+ Document?: Doc;
+ DataDocument?: Doc;
+ childDocs?: Doc[];
+ renderDepth: number;
+ fitToBox?: boolean;
+ fieldKey: string;
+ PanelWidth: () => number;
+ PanelHeight: () => number;
+ ruleProvider: Doc | undefined;
+ focus?: (doc: Doc) => void;
+ showOverlays?: (doc: Doc) => { title?: string, caption?: string };
+ CollectionView?: CollectionView;
+ CollectionDoc?: Doc;
+ onClick?: ScriptField;
+ getTransform: () => Transform;
+ addDocument: (document: Doc) => boolean;
+ moveDocument: (document: Doc, target: Doc, addDoc: ((doc: Doc) => boolean)) => boolean;
+ removeDocument: (document: Doc) => boolean;
+ active: () => boolean;
+ whenActiveChanged: (isActive: boolean) => void;
+ addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean;
+ pinToPres: (document: Doc) => void;
+ setPreviewScript: (script: string) => void;
+ previewScript?: string;
+}
+
+@observer
+export class ContentFittingDocumentView extends React.Component<ContentFittingDocumentViewProps>{
+ private get layoutDoc() { return this.props.Document && 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 = () => {
+ let wscale = this.props.PanelWidth() / (this.nativeWidth ? this.nativeWidth : this.props.PanelWidth());
+ if (wscale * this.nativeHeight > this.props.PanelHeight()) {
+ return this.props.PanelHeight() / (this.nativeHeight ? this.nativeHeight : this.props.PanelHeight());
+ }
+ return wscale;
+ }
+
+ @undoBatch
+ @action
+ drop = (e: Event, de: DragManager.DropEvent) => {
+ if (de.data instanceof DragManager.DocumentDragData) {
+ this.props.childDocs && this.props.childDocs.map(otherdoc => {
+ let target = Doc.GetProto(otherdoc);
+ target.layout = ComputedField.MakeFunction("this.image_data[0]");
+ target.layoutCustom = Doc.MakeDelegate(de.data.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, 0).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; }
+
+ @computed get borderRounding() { return StrCast(this.props.Document!.borderRounding); }
+
+ render() {
+ return (<div className="contentFittingDocumentView" style={{ width: this.props.PanelWidth(), height: this.props.PanelHeight() }}>
+ {!this.props.Document || !this.props.PanelWidth ? (null) : (
+ <div className="contentFittingDocumentView-previewDoc"
+ style={{
+ transform: `translate(${this.centeringOffset}px, 0px)`,
+ borderRadius: this.borderRounding,
+ height: this.props.PanelHeight(),
+ width: this.props.PanelWidth()
+ }}>
+ <DocumentView {...this.props}
+ DataDoc={this.props.DataDocument}
+ Document={this.props.Document}
+ fitToBox={this.props.fitToBox}
+ onClick={this.props.onClick}
+ ruleProvider={this.props.ruleProvider}
+ showOverlays={this.props.showOverlays}
+ addDocument={this.props.addDocument}
+ removeDocument={this.props.removeDocument}
+ moveDocument={this.props.moveDocument}
+ whenActiveChanged={this.props.whenActiveChanged}
+ ContainingCollectionView={this.props.CollectionView}
+ ContainingCollectionDoc={this.props.CollectionDoc}
+ addDocTab={this.props.addDocTab}
+ pinToPres={this.props.pinToPres}
+ parentActive={this.props.active}
+ ScreenToLocalTransform={this.getTransform}
+ renderDepth={this.props.renderDepth + 1}
+ ContentScaling={this.contentScaling}
+ PanelWidth={this.PanelWidth}
+ PanelHeight={this.PanelHeight}
+ focus={this.props.focus || emptyFunction}
+ backgroundColor={returnEmptyString}
+ bringToFront={emptyFunction}
+ zoomToScale={emptyFunction}
+ getScale={returnOne}
+ />
+ </div>)}
+ </div>);
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/DocuLinkBox.scss b/src/client/views/nodes/DocuLinkBox.scss
new file mode 100644
index 000000000..57c1a66e0
--- /dev/null
+++ b/src/client/views/nodes/DocuLinkBox.scss
@@ -0,0 +1,8 @@
+.docuLinkBox-cont {
+ cursor: default;
+ position: absolute;
+ width: 25px;
+ height: 25px;
+ border-radius: 20px;
+ pointer-events: all;
+} \ No newline at end of file
diff --git a/src/client/views/nodes/DocuLinkBox.tsx b/src/client/views/nodes/DocuLinkBox.tsx
new file mode 100644
index 000000000..d73407903
--- /dev/null
+++ b/src/client/views/nodes/DocuLinkBox.tsx
@@ -0,0 +1,84 @@
+import { action, observable } from "mobx";
+import { observer } from "mobx-react";
+import { Doc } from "../../../new_fields/Doc";
+import { makeInterface } from "../../../new_fields/Schema";
+import { NumCast, StrCast, Cast } from "../../../new_fields/Types";
+import { Utils } from '../../../Utils';
+import { DocumentManager } from "../../util/DocumentManager";
+import { DragLinksAsDocuments } from "../../util/DragManager";
+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";
+
+type DocLinkSchema = makeInterface<[typeof documentSchema]>;
+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;
+ @observable _x = 0;
+ @observable _y = 0;
+ @observable _selected = false;
+ _ref = React.createRef<HTMLDivElement>();
+
+ onPointerDown = (e: React.PointerEvent) => {
+ this._downx = e.clientX;
+ this._downy = e.clientY;
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ document.addEventListener("pointermove", this.onPointerMove);
+ document.addEventListener("pointerup", this.onPointerUp);
+ (e.button === 0 && !e.ctrlKey) && e.stopPropagation();
+ }
+ onPointerMove = action((e: PointerEvent) => {
+ let 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)) {
+ let bounds = cdiv.getBoundingClientRect();
+ let pt = Utils.getNearestPointInPerimeter(bounds.left, bounds.top, bounds.width, bounds.height, e.clientX, e.clientY);
+ let separation = Math.sqrt((pt[0] - e.clientX) * (pt[0] - e.clientX) + (pt[1] - e.clientY) * (pt[1] - e.clientY));
+ let dragdist = Math.sqrt((pt[0] - this._downx) * (pt[0] - this._downx) + (pt[1] - this._downy) * (pt[1] - this._downy));
+ if (separation > 100) {
+ DragLinksAsDocuments(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.
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ } else if (dragdist > separation) {
+ this.props.Document[this.props.fieldKey + "_x"] = (pt[0] - bounds.left) / bounds.width * 100;
+ this.props.Document[this.props.fieldKey + "_y"] = (pt[1] - bounds.top) / bounds.height * 100;
+ }
+ }
+ });
+ 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)) {
+ this.props.select(false);
+ }
+ }
+ onClick = (e: React.MouseEvent) => {
+ 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);
+ }
+ e.stopPropagation();
+ }
+
+ render() {
+ let anchorDoc = Cast(this.props.Document[this.props.fieldKey], Doc);
+ let hasAnchor = anchorDoc instanceof Doc && anchorDoc.type === DocumentType.PDFANNO;
+ let y = NumCast(this.props.Document[this.props.fieldKey + "_y"], 100);
+ let x = NumCast(this.props.Document[this.props.fieldKey + "_x"], 100);
+ let c = StrCast(this.props.Document.backgroundColor, "lightblue");
+ let anchor = this.props.fieldKey === "anchor1" ? "anchor2" : "anchor1";
+ let timecode = this.props.Document[anchor + "Timecode"];
+ let 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}
+ ref={this._ref} style={{
+ background: c, left: `calc(${x}% - 12.5px)`, top: `calc(${y}% - 12.5px)`,
+ transform: `scale(${hasAnchor ? 0.333 : 1 / this.props.ContentScaling()})`
+ }} />;
+ }
+}
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index 3c3cc0d91..779d25cdd 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -1,37 +1,36 @@
-import { computed, trace } from "mobx";
+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";
+import DirectoryImportBox from "../../util/Import & Export/DirectoryImportBox";
import { CollectionDockingView } from "../collections/CollectionDockingView";
import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
-import { CollectionPDFView } from "../collections/CollectionPDFView";
import { CollectionSchemaView } from "../collections/CollectionSchemaView";
-import { CollectionVideoView } from "../collections/CollectionVideoView";
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 { DocumentViewProps } from "./DocumentView";
import "./DocumentView.scss";
+import { FontIconBox } from "./FontIconBox";
+import { FieldView, FieldViewProps } from "./FieldView";
import { FormattedTextBox } from "./FormattedTextBox";
-import { ImageBox } from "./ImageBox";
-import { DragBox } from "./DragBox";
-import { ButtonBox } from "./ButtonBox";
-import { PresBox } from "./PresBox";
-import { LinkFollowBox } from "../linking/LinkFollowBox";
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 { DocuLinkBox } from "./DocuLinkBox";
+import { PresElementBox } from "../presentationview/PresElementBox";
import { VideoBox } from "./VideoBox";
-import { FieldView } from "./FieldView";
import { WebBox } from "./WebBox";
-import { YoutubeBox } from "./../../apis/youtube/YoutubeBox";
-import { HistogramBox } from "../../northstar/dash-nodes/HistogramBox";
import React = require("react");
-import { FieldViewProps } from "./FieldView";
-import { Without, OmitKeys } from "../../../Utils";
-import { Cast, StrCast, NumCast } from "../../../new_fields/Types";
-import { List } from "../../../new_fields/List";
-import { Doc } from "../../../new_fields/Doc";
-import DirectoryImportBox from "../../util/Import & Export/DirectoryImportBox";
-import { ScriptField } from "../../../new_fields/ScriptField";
-import { fromPromise } from "mobx-utils";
const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
type BindingProps = Without<FieldViewProps, 'fieldKey'>;
@@ -57,6 +56,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
hideOnLeave?: boolean
}> {
@computed get layout(): string {
+ if (!this.layoutDoc) return "<p>awaiting layout</p>";
const layout = Cast(this.layoutDoc[this.props.layoutKey], "string");
if (layout === undefined) {
return this.props.Document.data ?
@@ -70,45 +70,40 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
}
get dataDoc() {
- if (this.props.DataDoc === undefined && (this.props.Document.layout instanceof Doc || this.props.Document instanceof Promise)) {
- // if there is no dataDoc (ie, we're not rendering a template layout), but this document
- // has a template layout document, then we will render the template layout but use
- // this document as the data document for the layout.
+ 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.
return this.props.Document;
}
return this.props.DataDoc;
}
get layoutDoc() {
- // if this document's layout field contains a document (ie, a rendering template), then we will use that
- // to determine the render JSX string, otherwise the layout field should directly contain a JSX layout string.
- return this.props.Document.layout instanceof Doc ? this.props.Document.layout : this.props.Document;
+ return this.props.DataDoc === undefined ? Doc.expandTemplateLayout(Doc.Layout(this.props.Document), this.props.Document) : Doc.Layout(this.props.Document);
}
CreateBindings(): JsxBindings {
let list = {
...OmitKeys(this.props, ['parentActive'], (obj: any) => obj.active = this.props.parentActive).omit,
Document: this.layoutDoc,
- DataDoc: this.dataDoc
+ DataDoc: this.dataDoc,
};
return { props: list };
}
- @computed get finalLayout() {
- return this.props.layoutKey === "overlayLayout" ? "<div/>" : this.layout;
- }
-
render() {
- let self = this;
- if (this.props.renderDepth > 7) return (null);
- if (!this.layout && this.props.layoutKey !== "overlayLayout") return (null);
- return <ObserverJsxParser
- blacklistedAttrs={[]}
- components={{ FormattedTextBox, ImageBox, IconBox, DirectoryImportBox, DragBox, ButtonBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox, PresBox, YoutubeBox, LinkFollowBox }}
- bindings={this.CreateBindings()}
- jsx={this.finalLayout}
- showWarnings={true}
+ 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
+ }}
+ 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 63011f271..65df86d27 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -1,75 +1,82 @@
@import "../globalCssVariables";
.documentView-node, .documentView-node-topmost {
- position: inherit;
- top: 0;
- left:0;
- border-radius: inherit;
-
- // background: $light-color; //overflow: hidden;
- transform-origin: left top;
+ position: inherit;
+ top: 0;
+ left:0;
+ border-radius: inherit;
+ transition : outline .3s linear;
+ cursor: grab;
+
+ // background: $light-color; //overflow: hidden;
+ transform-origin: left top;
- &.minimized {
- width: 30px;
- height: 30px;
- }
-
- .top {
- height: 20px;
- cursor: pointer;
- }
+ &.minimized {
+ width: 30px;
+ height: 30px;
+ }
- .content {
- padding: 20px 20px;
- height: auto;
- box-sizing: border-box;
- }
+ .top {
+ height: 20px;
+ cursor: pointer;
+ }
- .scroll-box {
- overflow-y: scroll;
- height: calc(100% - 20px);
- }
-}
-.documentView-node-topmost {
- background: white;
-}
+ .content {
+ padding: 20px 20px;
+ height: auto;
+ box-sizing: border-box;
+ }
-.documentView-styleWrapper {
- position: absolute;
- display: inline-block;
- width:100%;
- height:100%;
- pointer-events: none;
+ .scroll-box {
+ overflow-y: scroll;
+ height: calc(100% - 20px);
+ }
- .documentView-styleContentWrapper {
- width:100%;
- display: inline-block;
+ .documentView-docuLinkWrapper {
+ pointer-events: none;
position: absolute;
+ transform-origin: top left;
+ width: 100%;
+ height: 100%;
}
- .documentView-titleWrapper {
- overflow:hidden;
- color: white;
- transform-origin: top left;
- top: 0;
- height: 25;
- background: rgba(0, 0, 0, .4);
- padding: 4px;
- text-align: center;
- text-overflow: ellipsis;
- white-space: pre;
- }
-
- .documentView-searchHighlight {
+ .documentView-styleWrapper {
position: absolute;
- background: yellow;
- bottom: -20px;
- border-radius: 5px;
- transform-origin: bottom left;
- }
+ display: inline-block;
+ width:100%;
+ height:100%;
+ pointer-events: none;
- .documentView-captionWrapper {
- position: absolute;
- bottom: 0;
- transform-origin: bottom left;
+ .documentView-styleContentWrapper {
+ width:100%;
+ display: inline-block;
+ position: absolute;
+ }
+ .documentView-titleWrapper {
+ overflow:hidden;
+ color: white;
+ transform-origin: top left;
+ top: 0;
+ height: 25;
+ background: rgba(0, 0, 0, .4);
+ padding: 4px;
+ text-align: center;
+ text-overflow: ellipsis;
+ white-space: pre;
+ }
+
+ .documentView-searchHighlight {
+ position: absolute;
+ background: yellow;
+ bottom: -20px;
+ border-radius: 5px;
+ transform-origin: bottom left;
+ }
+
+ .documentView-captionWrapper {
+ position: absolute;
+ bottom: 0;
+ transform-origin: bottom left;
+ }
}
+
} \ No newline at end of file
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index ea669b23c..93052ea73 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1,18 +1,18 @@
import { library } from '@fortawesome/fontawesome-svg-core';
import * as fa from '@fortawesome/free-solid-svg-icons';
-import { action, computed, runInAction } from "mobx";
+import { action, computed, runInAction, trace } from "mobx";
import { observer } from "mobx-react";
import * as rp from "request-promise";
import { Doc, DocListCast, DocListCastAsync, Opt } from "../../../new_fields/Doc";
-import { Copy, 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 { Document } from '../../../new_fields/documentSchemas';
+import { Id } from '../../../new_fields/FieldSymbols';
+import { listSpec } from "../../../new_fields/Schema";
import { ScriptField } from '../../../new_fields/ScriptField';
-import { BoolCast, Cast, FieldValue, NumCast, PromiseValue, StrCast } from "../../../new_fields/Types";
+import { BoolCast, Cast, NumCast, StrCast } from "../../../new_fields/Types";
+import { ImageField } from '../../../new_fields/URLField';
import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
-import { RouteStore } from '../../../server/RouteStore';
-import { emptyFunction, returnTrue, Utils } from "../../../Utils";
+import { emptyFunction, returnTransparent, returnTrue, Utils } from "../../../Utils";
+import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
import { DocServer } from "../../DocServer";
import { Docs, DocUtils } from "../../documents/Documents";
import { DocumentType } from '../../documents/DocumentTypes';
@@ -21,18 +21,19 @@ import { DictationManager } from '../../util/DictationManager';
import { DocumentManager } from "../../util/DocumentManager";
import { DragManager, dropActionType } from "../../util/DragManager";
import { LinkManager } from '../../util/LinkManager';
+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 { CollectionPDFView } from "../collections/CollectionPDFView";
-import { CollectionVideoView } from "../collections/CollectionVideoView";
import { CollectionView } from "../collections/CollectionView";
import { ContextMenu } from "../ContextMenu";
import { ContextMenuProps } from '../ContextMenuItem';
+import { DictationOverlay } from '../DictationOverlay';
import { DocComponent } from "../DocComponent";
import { EditableView } from '../EditableView';
-import { MainView } from '../MainView';
import { OverlayView } from '../OverlayView';
import { ScriptBox } from '../ScriptBox';
import { ScriptingRepl } from '../ScriptingRepl';
@@ -40,39 +41,19 @@ import { DocumentContentsView } from "./DocumentContentsView";
import "./DocumentView.scss";
import { FormattedTextBox } from './FormattedTextBox';
import React = require("react");
-import { Scripting } from '../../util/Scripting';
-const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
-
-library.add(fa.faTrash);
-library.add(fa.faShare);
-library.add(fa.faDownload);
-library.add(fa.faExpandArrowsAlt);
-library.add(fa.faCompressArrowsAlt);
-library.add(fa.faLayerGroup);
-library.add(fa.faExternalLinkAlt);
-library.add(fa.faAlignCenter);
-library.add(fa.faCaretSquareRight);
-library.add(fa.faSquare);
-library.add(fa.faConciergeBell);
-library.add(fa.faWindowRestore);
-library.add(fa.faFolder);
-library.add(fa.faMapPin);
-library.add(fa.faLink);
-library.add(fa.faFingerprint);
-library.add(fa.faCrosshairs);
-library.add(fa.faDesktop);
-library.add(fa.faUnlock);
-library.add(fa.faLock);
-library.add(fa.faLaptopCode, fa.faMale, fa.faCopy, fa.faHandPointRight, fa.faCompass, fa.faSnowflake, fa.faMicrophone);
+
+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,
+ fa.faCopy, fa.faHandPointRight, fa.faCompass, fa.faSnowflake, fa.faMicrophone);
export interface DocumentViewProps {
- ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
+ ContainingCollectionView: Opt<CollectionView>;
ContainingCollectionDoc: Opt<Doc>;
Document: Doc;
DataDoc?: Doc;
fitToBox?: boolean;
onClick?: ScriptField;
- addDocument?: (doc: Doc, allowDuplicates?: boolean) => boolean;
+ addDocument?: (doc: Doc) => boolean;
removeDocument?: (doc: Doc) => boolean;
moveDocument?: (doc: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
ScreenToLocalTransform: () => Transform;
@@ -93,41 +74,9 @@ export interface DocumentViewProps {
getScale: () => number;
animateBetweenIcon?: (maximize: boolean, target: number[]) => void;
ChromeHeight?: () => number;
+ layoutKey?: string;
}
-export const documentSchema = createSchema({
- layout: "string", // should also allow Doc but that can't be expressed in the schema
- hidden: "boolean",
- excludeFromLibrary: "boolean",
- fitToBox: "boolean",
- // layout: "string", // this should be a "string" or Doc, but can't do that in schemas, so best to leave it out
- title: "string", // document title (can be on either data document or layout)
- 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", // "
- backgroundColor: "string", // background color of document
- opacity: "number", // opacity of document
- onClick: ScriptField, // script to run when document is clicked (can be overriden by an onClick prop)
- 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
- isTemplate: "boolean", // whether this document acts as a template layout for describing how other documents should be displayed
- isBackground: "boolean", // whether document is a background element and ignores input events (can only selet with marquee)
- type: "string", // enumerated type of document
- 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 spatially manipulated
- 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", // whether an editable title banner is displayed at tht top of the document
- 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)
-});
-
-
-type Document = makeInterface<[typeof documentSchema]>;
-const Document = makeInterface(documentSchema);
@observer
export class DocumentView extends DocComponent<DocumentViewProps, Document>(Document) {
@@ -142,8 +91,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
public get ContentDiv() { return this._mainCont.current; }
@computed get active() { return SelectionManager.IsSelected(this) || this.props.parentActive(); }
@computed get topMost() { return this.props.renderDepth === 0; }
- @computed get nativeWidth() { return this.Document.nativeWidth || 0; }
- @computed get nativeHeight() { return this.Document.nativeHeight || 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; }
@action
@@ -161,6 +110,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
@action
componentWillUnmount() {
this._dropDisposer && this._dropDisposer();
+ Doc.UnBrushDoc(this.props.Document);
DocumentManager.Instance.DocumentViews.splice(DocumentManager.Instance.DocumentViews.indexOf(this), 1);
}
@@ -170,13 +120,13 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
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;
+ dragData.moveDocument = this.Document.onDragStart ? undefined : this.props.moveDocument;
dragData.applyAsTemplate = applyAsTemplate;
DragManager.StartDocumentDrag([this._mainCont.current], dragData, x, y, {
handlers: {
- dragComplete: action(emptyFunction)
+ dragComplete: action((emptyFunction))
},
- hideSource: !dropAction
+ hideSource: !dropAction && !this.Document.onDragStart
});
}
}
@@ -185,29 +135,34 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
if (!e.nativeEvent.cancelBubble && !this.Document.ignoreClick && CurrentUserUtils.MainDocId !== this.props.Document[Id] &&
(Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD)) {
e.stopPropagation();
- e.preventDefault();
- if (this._doubleTap && this.props.renderDepth) {
+ let preventDefault = true;
+ if (this._doubleTap && this.props.renderDepth && (!this.onClickHandler || !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
let fullScreenAlias = Doc.MakeAlias(this.props.Document);
- let layoutNative = await PromiseValue(Cast(this.props.Document.layoutNative, Doc));
- if (layoutNative && fullScreenAlias.layout === layoutNative.layout) {
- await swapViews(fullScreenAlias, "layoutCustom", "layoutNative");
+ if (StrCast(fullScreenAlias.layoutKey) !== "layoutCustom" && fullScreenAlias.layoutCustom !== undefined) {
+ fullScreenAlias.layoutKey = "layoutCustom";
}
this.props.addDocTab(fullScreenAlias, undefined, "inTab");
SelectionManager.DeselectAll();
Doc.UnBrushDoc(this.props.Document);
} else if (this.onClickHandler && this.onClickHandler.script) {
- this.onClickHandler.script.run({ this: this.Document.isTemplate && this.props.DataDoc ? this.props.DataDoc : this.props.Document }, console.log);
+ this.onClickHandler.script.run({ this: this.Document.isTemplateField && this.props.DataDoc ? this.props.DataDoc : this.props.Document }, console.log);
+ } else if (this.Document.type === DocumentType.BUTTON) {
+ ScriptBox.EditButtonScript("On Button Clicked ...", this.props.Document, "onClick", e.clientX, e.clientY);
} else if (this.Document.isButton) {
SelectionManager.SelectDoc(this, e.ctrlKey); // don't think this should happen if a button action is actually triggered.
this.buttonClick(e.altKey, e.ctrlKey);
- } else SelectionManager.SelectDoc(this, e.ctrlKey);
+ } else {
+ SelectionManager.SelectDoc(this, e.ctrlKey);
+ preventDefault = false;
+ }
+ preventDefault && e.preventDefault();
}
}
buttonClick = async (altKey: boolean, ctrlKey: boolean) => {
- let maximizedDocs = await DocListCastAsync(this.props.Document.maximizedDocs);
- let summarizedDocs = await DocListCastAsync(this.props.Document.summarizedDocs);
- let linkedDocs = LinkManager.Instance.getAllRelatedLinks(this.props.Document);
+ let maximizedDocs = await DocListCastAsync(this.Document.maximizedDocs);
+ let summarizedDocs = await DocListCastAsync(this.Document.summarizedDocs);
+ let linkDocs = LinkManager.Instance.getAllRelatedLinks(this.props.Document);
let expandedDocs: Doc[] = [];
expandedDocs = maximizedDocs ? [...maximizedDocs, ...expandedDocs] : expandedDocs;
expandedDocs = summarizedDocs ? [...summarizedDocs, ...expandedDocs] : expandedDocs;
@@ -217,40 +172,23 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
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, false));
- let scrpt = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(NumCast(this.Document.width) / 2, NumCast(this.Document.height) / 2);
+ expandedDocs.forEach(maxDoc => this.props.addDocument && this.props.addDocument(maxDoc));
+ let 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 (linkedDocs.length) {
- SelectionManager.DeselectAll();
- let first = linkedDocs.filter(d => Doc.AreProtosEqual(d.anchor1 as Doc, this.props.Document) && !d.anchor1anchored);
- let second = linkedDocs.filter(d => Doc.AreProtosEqual(d.anchor2 as Doc, this.props.Document) && !d.anchor2anchored);
- let firstUnshown = first.filter(d => DocumentManager.Instance.getDocumentViews(d.anchor2 as Doc).length === 0);
- let secondUnshown = second.filter(d => DocumentManager.Instance.getDocumentViews(d.anchor1 as Doc).length === 0);
- if (firstUnshown.length) first = [firstUnshown[0]];
- if (secondUnshown.length) second = [secondUnshown[0]];
- let linkedFwdDocs = first.length ? [first[0].anchor2 as Doc, first[0].anchor1 as Doc] : second.length ? [second[0].anchor1 as Doc, second[0].anchor1 as Doc] : undefined;
-
- // @TODO: shouldn't always follow target context
- let linkedFwdContextDocs = [first.length ? await (first[0].targetContext) as Doc : undefined, undefined];
- let linkedFwdPage = [first.length ? NumCast(first[0].anchor2Page, undefined) : undefined, undefined];
-
- if (linkedFwdDocs && !linkedFwdDocs.some(l => l instanceof Promise)) {
- let maxLocation = StrCast(linkedFwdDocs[0].maximizeLocation, "inTab");
- let targetContext = !Doc.AreProtosEqual(linkedFwdContextDocs[altKey ? 1 : 0], this.props.ContainingCollectionDoc) ? linkedFwdContextDocs[altKey ? 1 : 0] : undefined;
- DocumentManager.Instance.jumpToDocument(linkedFwdDocs[altKey ? 1 : 0], ctrlKey, false,
- // 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 => this.props.focus(this.props.Document, true, 1, () => this.props.addDocTab(doc, undefined, maxLocation)),
- linkedFwdPage[altKey ? 1 : 0], targetContext);
- }
+ else 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)),
+ ctrlKey, altKey, this.props.ContainingCollectionDoc);
}
}
onPointerDown = (e: React.PointerEvent): void => {
- if (e.nativeEvent.cancelBubble) return;
+ if (e.nativeEvent.cancelBubble && e.button === 0) return;
this._downX = e.clientX;
this._downY = e.clientY;
this._hitTemplateDrag = false;
@@ -261,22 +199,24 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
this._hitTemplateDrag = true;
}
}
- if (this.active && e.button === 0 && !this.Document.lockedPosition) 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 && !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);
document.addEventListener("pointerup", this.onPointerUp);
+ if ((e.nativeEvent as any).formattedHandled) { e.stopPropagation(); }
}
onPointerMove = (e: PointerEvent): void => {
+ if ((e as any).formattedHandled) { e.stopPropagation(); return; }
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) || this.props.parentActive()) && !this.Document.lockedPosition) {
+ else if (!e.cancelBubble && (SelectionManager.IsSelected(this) || this.props.parentActive() || this.Document.onDragStart || this.Document.onClick) && !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 && e.buttons === 1) {
+ if (!e.altKey && (!this.topMost || this.Document.onDragStart || this.Document.onClick) && e.buttons === 1) {
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
- this.startDragging(this._downX, this._downY, 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, this._hitTemplateDrag);
}
}
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
@@ -293,32 +233,47 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
@undoBatch
deleteClicked = (): void => { SelectionManager.DeselectAll(); this.props.removeDocument && this.props.removeDocument(this.props.Document); }
- @undoBatch
- makeNativeViewClicked = (): void => { swapViews(this.props.Document, "layoutNative", "layoutCustom"); }
-
- @undoBatch
- makeCustomViewClicked = async () => {
- if (this.props.Document.layoutCustom === undefined) {
- Doc.GetProto(this.props.DataDoc || this.props.Document).layoutNative = Doc.MakeTitled("layoutNative");
- await swapViews(this.props.Document, "", "layoutNative");
+ static makeNativeViewClicked = (doc: Doc) => {
+ undoBatch(() => doc.layoutKey = "layout")();
+ }
- let options = { title: "data", width: (this.Document.width || 0), x: -(this.Document.width || 0) / 2, y: - (this.Document.height || 0) / 2, };
- let fieldTemplate = this.Document.type === DocumentType.TEXT ? Docs.Create.TextDocument(options) : this.Document.type === DocumentType.PDF ? Docs.Create.PdfDocument("http://www.msn.com", options) :
- this.Document.type === DocumentType.VID ? Docs.Create.VideoDocument("http://www.cs.brown.edu", options) :
- Docs.Create.ImageDocument("http://www.cs.brown.edu", options);
+ static makeCustomViewClicked = (doc: Doc, dataDoc: Opt<Doc>) => {
+ const batch = UndoManager.StartBatch("CustomViewClicked");
+ if (doc.layoutCustom === undefined) {
+ const width = NumCast(doc.width);
+ const height = NumCast(doc.height);
+ const options = { title: "data", width, x: -width / 2, y: - height / 2, };
+
+ let fieldTemplate: Doc;
+ switch (doc.type) {
+ case DocumentType.TEXT:
+ fieldTemplate = Docs.Create.TextDocument(options);
+ break;
+ case DocumentType.PDF:
+ fieldTemplate = Docs.Create.PdfDocument("http://www.msn.com", options);
+ break;
+ case DocumentType.VID:
+ fieldTemplate = Docs.Create.VideoDocument("http://www.cs.brown.edu", options);
+ break;
+ case DocumentType.AUDIO:
+ fieldTemplate = Docs.Create.AudioDocument("http://www.cs.brown.edu", options);
+ break;
+ default:
+ fieldTemplate = Docs.Create.ImageDocument("http://www.cs.brown.edu", options);
+ }
- fieldTemplate.backgroundColor = this.Document.backgroundColor;
+ fieldTemplate.backgroundColor = doc.backgroundColor;
fieldTemplate.heading = 1;
fieldTemplate.autoHeight = true;
- let docTemplate = Docs.Create.FreeformDocument([fieldTemplate], { title: this.Document.title + "_layout", width: (this.Document.width || 0) + 20, height: Math.max(100, (this.Document.height || 0) + 45) });
+ let docTemplate = Docs.Create.FreeformDocument([fieldTemplate], { title: doc.title + "_layout", width: width + 20, height: Math.max(100, height + 45) });
Doc.MakeMetadataFieldTemplate(fieldTemplate, Doc.GetProto(docTemplate), true);
- Doc.ApplyTemplateTo(docTemplate, this.props.Document, undefined);
- Doc.GetProto(this.props.DataDoc || this.props.Document).layoutCustom = Doc.MakeTitled("layoutCustom");
+ Doc.ApplyTemplateTo(docTemplate, dataDoc || doc, "layoutCustom", undefined);
} else {
- swapViews(this.props.Document, "layoutCustom", "layoutNative");
+ doc.layoutKey = "layoutCustom";
}
+ batch.end();
}
@undoBatch
@@ -338,18 +293,12 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
if (de.data instanceof DragManager.AnnotationDragData) {
/// this whole section for handling PDF annotations looks weird. Need to rethink this to make it cleaner
e.stopPropagation();
- let sourceDoc = de.data.annotationDocument;
- let targetDoc = this.props.Document;
- let annotations = await DocListCastAsync(sourceDoc.annotations);
- sourceDoc.linkedToDoc = true;
- de.data.targetContext = this.props.ContainingCollectionDoc;
- targetDoc.targetContext = de.data.targetContext;
- annotations && annotations.forEach(anno => anno.target = targetDoc);
-
- DocUtils.MakeLink(sourceDoc, targetDoc, this.props.ContainingCollectionDoc, `Link from ${StrCast(sourceDoc.title)}`);
+ (de.data as any).linkedToDoc = true;
+
+ DocUtils.MakeLink({ doc: de.data.annotationDocument }, { doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, `Link from ${StrCast(de.data.annotationDocument.title)}`);
}
if (de.data instanceof DragManager.DocumentDragData && de.data.applyAsTemplate) {
- Doc.ApplyTemplateTo(de.data.draggedDocuments[0], this.props.Document);
+ Doc.ApplyTemplateTo(de.data.draggedDocuments[0], this.props.Document, "layoutCustom");
e.stopPropagation();
}
if (de.data instanceof DragManager.LinkDragData) {
@@ -357,7 +306,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
// 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.data.linkSourceDocument !== this.props.Document &&
- (de.data.linkDocument = DocUtils.MakeLink(de.data.linkSourceDocument, this.props.Document, this.props.ContainingCollectionDoc, undefined, "in-text link being created")); // TODODO this is where in text links get passed
+ (de.data.linkDocument = DocUtils.MakeLink({ doc: de.data.linkSourceDocument }, { doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, "in-text link being created")); // TODODO this is where in text links get passed
}
}
@@ -365,9 +314,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
onDrop = (e: React.DragEvent) => {
let text = e.dataTransfer.getData("text/plain");
if (!e.isDefaultPrevented() && text && text.startsWith("<div")) {
- let oldLayout = StrCast(this.props.Document.layout);
+ let oldLayout = this.Document.layout || "";
let layout = text.replace("{layout}", oldLayout);
- this.props.Document.layout = layout;
+ this.Document.layout = layout;
e.stopPropagation();
e.preventDefault();
}
@@ -376,24 +325,23 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
@undoBatch
@action
freezeNativeDimensions = (): void => {
- let proto = this.Document.isTemplate ? this.props.Document : Doc.GetProto(this.props.Document);
- proto.autoHeight = this.Document.autoHeight = false;
- proto.ignoreAspect = !proto.ignoreAspect;
- if (!proto.ignoreAspect && !proto.nativeWidth) {
- proto.nativeWidth = this.props.PanelWidth();
- proto.nativeHeight = this.props.PanelHeight();
+ this.layoutDoc.autoHeight = 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();
}
}
@undoBatch
@action
makeIntoPortal = async () => {
- let anchors = await Promise.all(DocListCast(this.props.Document.links).map(async (d: Doc) => Cast(d.anchor2, Doc)));
+ let 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)) {
let portalID = (this.Document.title + ".portal").replace(/^-/, "").replace(/\([0-9]*\)$/, "");
DocServer.GetRefField(portalID).then(existingPortal => {
- let portal = existingPortal instanceof Doc ? existingPortal : Docs.Create.FreeformDocument([], { width: (this.Document.width || 0) + 10, height: this.Document.height || 0, title: portalID });
- DocUtils.MakeLink(this.props.Document, portal, undefined, portalID);
+ let 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;
});
}
@@ -405,7 +353,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
if (this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.DataDoc) {
Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.ContainingCollectionView.props.DataDoc);
} else { // bcz: not robust -- for now documents with string layout are native documents, and those with Doc layouts are customized
- custom ? this.makeCustomViewClicked() : this.makeNativeViewClicked();
+ custom ? DocumentView.makeCustomViewClicked(this.props.Document, this.props.DataDoc) : DocumentView.makeNativeViewClicked(this.props.Document);
}
}
@@ -426,10 +374,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
Doc.GetProto(this.props.Document).transcript = await DictationManager.Controls.listen({
continuous: { indefinite: true },
interimHandler: (results: string) => {
- let main = MainView.Instance;
- main.dictationSuccess = true;
- main.dictatedPhrase = results;
- main.isListening = { interim: true };
+ DictationOverlay.Instance.dictationSuccess = true;
+ DictationOverlay.Instance.dictatedPhrase = results;
+ DictationOverlay.Instance.isListening = { interim: true };
}
});
}
@@ -455,6 +402,15 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
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" });
+ if (Cast(this.props.Document.data, ImageField)) {
+ cm.addItem({ description: "Export to Google Photos", event: () => GooglePhotos.Transactions.UploadImages([this.props.Document]), icon: "caret-square-right" });
+ }
+ if (Cast(Doc.GetProto(this.props.Document).data, listSpec(Doc))) {
+ cm.addItem({ description: "Export to Google Photos Album", event: () => GooglePhotos.Export.CollectionToAlbum({ collection: this.props.Document }).then(console.log), icon: "caret-square-right" });
+ cm.addItem({ description: "Tag Child Images via Google Photos", event: () => GooglePhotos.Query.TagChildImages(this.props.Document), icon: "caret-square-right" });
+ cm.addItem({ description: "Write Back Link to Album", event: () => GooglePhotos.Transactions.AddTextEnrichment(this.props.Document), icon: "caret-square-right" });
+ }
+
let existingOnClick = ContextMenu.Instance.findByDescription("OnClick...");
let onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : [];
onClicks.push({ description: "Enter Portal", event: this.makeIntoPortal, icon: "window-restore" });
@@ -470,6 +426,12 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
});
!existingOnClick && cm.addItem({ description: "OnClick...", subitems: onClicks, icon: "hand-point-right" });
+ let funcs: ContextMenuProps[] = [];
+ 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" });
+
let existing = ContextMenu.Instance.findByDescription("Layout...");
let 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" });
@@ -477,22 +439,21 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
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.Document.autoHeight = !this.Document.autoHeight, icon: "plus" });
+ 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: "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" });
if (this.Document.type !== DocumentType.COL && this.Document.type !== DocumentType.TEMPLATE) {
- layoutItems.push({ description: "Use Custom Layout", event: this.makeCustomViewClicked, icon: "concierge-bell" });
- } else if (this.props.Document.layoutNative) {
- layoutItems.push({ description: "Use Native Layout", event: this.makeNativeViewClicked, icon: "concierge-bell" });
+ layoutItems.push({ description: "Use Custom Layout", event: () => DocumentView.makeCustomViewClicked(this.props.Document, this.props.DataDoc), icon: "concierge-bell" });
+ } else {
+ layoutItems.push({ description: "Use Native Layout", event: () => DocumentView.makeNativeViewClicked(this.props.Document), icon: "concierge-bell" });
}
!existing && cm.addItem({ description: "Layout...", subitems: layoutItems, icon: "compass" });
if (!ClientUtils.RELEASE) {
- let copies: ContextMenuProps[] = [];
- copies.push({ description: "Copy URL", event: () => Utils.CopyText(Utils.prepend("/doc/" + this.props.Document[Id])), icon: "link" });
- copies.push({ description: "Copy ID", event: () => Utils.CopyText(this.props.Document[Id]), icon: "fingerprint" });
- cm.addItem({ description: "Copy...", subitems: copies, icon: "copy" });
+ // let copies: ContextMenuProps[] = [];
+ cm.addItem({ description: "Copy ID", event: () => Utils.CopyText(this.props.Document[Id]), icon: "fingerprint" });
+ // cm.addItem({ description: "Copy...", subitems: copies, icon: "copy" });
}
let existingAnalyze = ContextMenu.Instance.findByDescription("Analyzers...");
let analyzers: ContextMenuProps[] = existingAnalyze && "subitems" in existingAnalyze ? existingAnalyze.subitems : [];
@@ -514,32 +475,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
cm.addItem({ description: "Publish", event: () => DocUtils.Publish(this.props.Document, this.Document.title || "", this.props.addDocument, this.props.removeDocument), icon: "file" });
cm.addItem({ description: "Delete", event: this.deleteClicked, icon: "trash" });
- try {
- type User = { email: string, userDocumentId: string };
- const users: User[] = JSON.parse(await rp.get(Utils.prepend(RouteStore.getUsers)));
- let usersMenu = users.filter(({ email }) => email !== Doc.CurrentUserEmail).map(({ email, userDocumentId }) => ({
- description: email,
- event: async () => {
- const userDocument = await Cast(DocServer.GetRefField(userDocumentId), Doc);
- if (userDocument) {
- const notifDoc = await Cast(userDocument.optionalRightCollection, Doc);
- if (notifDoc) {
- const data = await Cast(notifDoc.data, listSpec(Doc));
- const sharedDoc = Doc.MakeAlias(this.props.Document);
- if (data) {
- data.push(sharedDoc);
- } else {
- notifDoc.data = new List([sharedDoc]);
- }
- }
- }
- },
- icon: "male"
- } as ContextMenuProps));
- cm.addItem({ description: "Share...", subitems: usersMenu, icon: "share" });
- } catch {
-
- }
runInAction(() => {
if (!ClientUtils.RELEASE) {
let setWriteMode = (mode: DocServer.WriteMode) => {
@@ -564,6 +499,13 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
cm.addItem({ description: "Collaboration ACLs...", subitems: aclsMenu, icon: "share" });
cm.addItem({ description: "Undo Debug Test", event: () => UndoManager.TraceOpenBatches(), icon: "exclamation" });
}
+ });
+ runInAction(() => {
+ cm.addItem({
+ description: "Share",
+ event: () => SharingManager.Instance.open(this),
+ icon: "external-link-alt"
+ });
if (!this.topMost) {
// DocumentViews should stop propagation of this event
@@ -576,13 +518,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
});
}
-
- // 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(this.props.Document.layout instanceof Doc ? this.props.Document.layout : this.props.Document);
- }
-
// does Document set a layout prop
setsLayoutProp = (prop: string) => this.props.Document[prop] !== this.props.Document["default" + prop[0].toUpperCase() + prop.slice(1)];
// get the a layout prop by first choosing the prop from Document, then falling back to the layout doc otherwise.
@@ -598,24 +533,55 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
return (showTitle ? 25 : 0) + 1;
}
- render() {
- const ruleColor = this.props.ruleProvider ? StrCast(this.props.ruleProvider["ruleColor_" + this.Document.heading]) : undefined;
- const ruleRounding = this.props.ruleProvider ? StrCast(this.props.ruleProvider["ruleRounding_" + this.Document.heading]) : undefined;
- const colorSet = this.setsLayoutProp("backgroundColor");
- const clusterCol = this.props.ContainingCollectionDoc && this.props.ContainingCollectionDoc.clusterOverridesDefaultBackground;
- const backgroundColor = this.Document.isBackground || (clusterCol && !colorSet) ?
- this.props.backgroundColor(this.Document) || StrCast(this.layoutDoc.backgroundColor) :
- ruleColor && !colorSet ? ruleColor : StrCast(this.layoutDoc.backgroundColor) || this.props.backgroundColor(this.Document);
+ childScaling = () => (this.layoutDoc.fitWidth ? this.props.PanelWidth() / this.nativeWidth : this.props.ContentScaling());
+ @computed get contents() {
+ return (<DocumentContentsView ContainingCollectionView={this.props.ContainingCollectionView}
+ ContainingCollectionDoc={this.props.ContainingCollectionDoc}
+ Document={this.props.Document}
+ fitToBox={this.props.fitToBox}
+ addDocument={this.props.addDocument}
+ removeDocument={this.props.removeDocument}
+ moveDocument={this.props.moveDocument}
+ ScreenToLocalTransform={this.props.ScreenToLocalTransform}
+ renderDepth={this.props.renderDepth}
+ showOverlays={this.props.showOverlays}
+ ContentScaling={this.childScaling}
+ ruleProvider={this.props.ruleProvider}
+ PanelWidth={this.props.PanelWidth}
+ PanelHeight={this.props.PanelHeight}
+ focus={this.props.focus}
+ parentActive={this.props.parentActive}
+ whenActiveChanged={this.props.whenActiveChanged}
+ bringToFront={this.props.bringToFront}
+ addDocTab={this.props.addDocTab}
+ pinToPres={this.props.pinToPres}
+ zoomToScale={this.props.zoomToScale}
+ backgroundColor={this.props.backgroundColor}
+ animateBetweenIcon={this.props.animateBetweenIcon}
+ getScale={this.props.getScale}
+ ChromeHeight={this.chromeHeight}
+ isSelected={this.isSelected}
+ select={this.select}
+ onClick={this.onClickHandler}
+ layoutKey={this.props.layoutKey || "layout"}
+ DataDoc={this.props.DataDoc} />);
+ }
+ linkEndpoint = (linkDoc: Doc) => Doc.LinkEndpoint(linkDoc, this.props.Document);
+
+ // used to decide whether a link document should be created or not.
+ // if it's a tempoarl link (currently just for Audio), then the audioBox will display the anchor and we don't want to display it here.
+ // would be good to generalize this some way.
+ isNonTemporalLink = (linkDoc: Doc) => {
+ let anchor = Cast(Doc.AreProtosEqual(this.props.Document, Cast(linkDoc.anchor1, Doc) as Doc) ? linkDoc.anchor1 : linkDoc.anchor2, Doc) as Doc;
+ let ept = Doc.AreProtosEqual(this.props.Document, Cast(linkDoc.anchor1, Doc) as Doc) ? linkDoc.anchor1Timecode : linkDoc.anchor2Timecode;
+ return anchor.type === DocumentType.AUDIO && NumCast(ept) ? false : true;
+ }
- const nativeWidth = this.nativeWidth > 0 && !this.Document.ignoreAspect ? `${this.nativeWidth}px` : "100%";
- const nativeHeight = this.Document.ignoreAspect || this.props.Document.fitWidth ? this.props.PanelHeight() / this.props.ContentScaling() : this.nativeHeight > 0 ? `${this.nativeHeight}px` : nativeWidth !== "100%" ? nativeWidth : "100%";
+ @computed get innards() {
const showOverlays = this.props.showOverlays ? this.props.showOverlays(this.Document) : undefined;
const showTitle = showOverlays && "title" in showOverlays ? showOverlays.title : this.getLayoutPropStr("showTitle");
const showCaption = showOverlays && "caption" in showOverlays ? showOverlays.caption : this.getLayoutPropStr("showCaption");
const showTextTitle = showTitle && StrCast(this.Document.layout).indexOf("FormattedTextBox") !== -1 ? showTitle : undefined;
- const fullDegree = Doc.isBrushedHighlightedDegree(this.props.Document);
- const borderRounding = this.getLayoutPropStr("borderRounding") || ruleRounding;
- const localScale = this.props.ScreenToLocalTransform().Scale * fullDegree;
const searchHighlight = (!this.Document.searchFields ? (null) :
<div className="documentView-searchHighlight" style={{ width: `${100 * this.props.ContentScaling()}%`, transform: `scale(${1 / this.props.ContentScaling()})` }}>
{this.Document.searchFields}
@@ -625,15 +591,14 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
<FormattedTextBox {...this.props}
onClick={this.onClickHandler} DataDoc={this.props.DataDoc} active={returnTrue}
isSelected={this.isSelected} focus={emptyFunction} select={this.select}
- fieldExt={""} hideOnLeave={true} fieldKey={showCaption}
+ hideOnLeave={true} fieldKey={showCaption}
/>
</div>);
const titleView = (!showTitle ? (null) :
<div className="documentView-titleWrapper" style={{
+ width: `${100 * this.props.ContentScaling()}%`, transform: `scale(${1 / this.props.ContentScaling()})`,
position: showTextTitle ? "relative" : "absolute",
pointerEvents: SelectionManager.GetIsDragging() ? "none" : "all",
- width: `${100 * this.props.ContentScaling()}%`,
- transform: `scale(${1 / this.props.ContentScaling()})`
}}>
<EditableView
contents={this.Document[showTitle]}
@@ -642,85 +607,73 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
SetValue={(value: string) => (Doc.GetProto(this.Document)[showTitle] = value) ? true : true}
/>
</div>);
- const contents = (<DocumentContentsView {...this.props}
- ChromeHeight={this.chromeHeight}
- isSelected={this.isSelected}
- select={this.select}
- onClick={this.onClickHandler}
- layoutKey={"layout"}
- DataDoc={this.props.DataDoc} />);
- return (
- <div className={`documentView-node${this.topMost ? "-topmost" : ""}`}
- ref={this._mainCont}
- style={{
- //transition: this.props.Document.isAnimating !== undefined ? ".5s linear" : StrCast(this.Document.transition),
- pointerEvents: this.Document.isBackground && !this.isSelected() ? "none" : "all",
- color: StrCast(this.Document.color),
- outlineColor: ["transparent", "maroon", "maroon", "yellow"][fullDegree],
- outlineStyle: ["none", "dashed", "solid", "solid"][fullDegree],
- outlineWidth: fullDegree && !borderRounding ? `${localScale}px` : "0px",
- border: fullDegree && borderRounding ? `${["none", "dashed", "solid", "solid"][fullDegree]} ${["transparent", "maroon", "maroon", "yellow"][fullDegree]} ${localScale}px` : undefined,
- background: backgroundColor,
- width: nativeWidth,
- height: nativeHeight,
- transform: `scale(${this.props.ContentScaling()})`,
- opacity: this.Document.opacity
- }}
- onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick}
- onPointerEnter={() => Doc.BrushDoc(this.props.Document)} onPointerLeave={() => Doc.UnBrushDoc(this.props.Document)}
- >
- {!showTitle && !showCaption ?
- this.Document.searchFields ?
- (<div className="documentView-searchWrapper">
- {contents}
- {searchHighlight}
- </div>)
- :
- contents
- :
- <div className="documentView-styleWrapper" >
- <div className="documentView-styleContentWrapper" style={{ height: showTextTitle ? "calc(100% - 29px)" : "100%", top: showTextTitle ? "29px" : undefined }}>
- {contents}
- </div>
- {titleView}
- {captionView}
+ return <>
+ {this.Document.links && DocListCast(this.Document.links).filter((d) => !DocListCast(this.layoutDoc.hiddenLinks).some(hidden => Doc.AreProtosEqual(hidden, d))).filter(this.isNonTemporalLink).map((d, i) =>
+ <div className="documentView-docuLinkWrapper" key={`${d[Id]}`} style={{ transform: `scale(${this.layoutDoc.fitWidth ? 1 : 1 / this.props.ContentScaling()})` }}>
+ <DocumentView {...this.props} Document={d} layoutKey={this.linkEndpoint(d)} backgroundColor={returnTransparent} removeDocument={undoBatch(doc => Doc.AddDocToList(this.layoutDoc, "hiddenLinks", doc))} />
+ </div>)}
+ {!showTitle && !showCaption ?
+ this.Document.searchFields ?
+ (<div className="documentView-searchWrapper">
+ {this.contents}
{searchHighlight}
+ </div>)
+ :
+ this.contents
+ :
+ <div className="documentView-styleWrapper" >
+ <div className="documentView-styleContentWrapper" style={{ height: showTextTitle ? "calc(100% - 29px)" : "100%", top: showTextTitle ? "29px" : undefined }}>
+ {this.contents}
</div>
- }
- </div>
- );
+ {titleView}
+ {captionView}
+ {searchHighlight}
+ </div>
+ }
+ </>
}
-}
+ render() {
+ if (!this.props.Document) return (null);
+ trace();
+ const animDims = this.Document.animateToDimensions ? Array.from(this.Document.animateToDimensions) : undefined;
+ const ruleColor = this.props.ruleProvider ? StrCast(this.props.ruleProvider["ruleColor_" + this.Document.heading]) : undefined;
+ const ruleRounding = this.props.ruleProvider ? StrCast(this.props.ruleProvider["ruleRounding_" + this.Document.heading]) : undefined;
+ const colorSet = this.setsLayoutProp("backgroundColor");
+ const clusterCol = this.props.ContainingCollectionDoc && this.props.ContainingCollectionDoc.clusterOverridesDefaultBackground;
+ const backgroundColor = this.Document.isBackground || (clusterCol && !colorSet) ?
+ this.props.backgroundColor(this.Document) || StrCast(this.layoutDoc.backgroundColor) :
+ ruleColor && !colorSet ? ruleColor : StrCast(this.layoutDoc.backgroundColor) || this.props.backgroundColor(this.Document);
-export async function swapViews(doc: Doc, newLayoutField: string, oldLayoutField: string, oldLayout?: Doc) {
- let oldLayoutExt = oldLayout || await Cast(doc[oldLayoutField], Doc);
- if (oldLayoutExt) {
- oldLayoutExt.autoHeight = doc.autoHeight;
- oldLayoutExt.width = doc.width;
- oldLayoutExt.height = doc.height;
- oldLayoutExt.nativeWidth = doc.nativeWidth;
- oldLayoutExt.nativeHeight = doc.nativeHeight;
- oldLayoutExt.ignoreAspect = doc.ignoreAspect;
- oldLayoutExt.backgroundLayout = doc.backgroundLayout;
- oldLayoutExt.type = doc.type;
- oldLayoutExt.layout = doc.layout;
- }
+ const nativeWidth = this.layoutDoc.fitWidth ? this.props.PanelWidth() - 2 : this.nativeWidth > 0 && !this.layoutDoc.ignoreAspect ? `${this.nativeWidth}px` : "100%";
+ const nativeHeight = this.layoutDoc.fitWidth ? this.props.PanelHeight() - 2 : this.Document.ignoreAspect ? this.props.PanelHeight() / this.props.ContentScaling() : this.nativeHeight > 0 ? `${this.nativeHeight}px` : "100%";
+ const fullDegree = Doc.isBrushedHighlightedDegree(this.props.Document);
+ const borderRounding = this.getLayoutPropStr("borderRounding") || ruleRounding;
+ const localScale = this.props.ScreenToLocalTransform().Scale * fullDegree;
- let newLayoutExt = newLayoutField && await Cast(doc[newLayoutField], Doc);
- if (newLayoutExt) {
- doc.autoHeight = newLayoutExt.autoHeight;
- doc.width = newLayoutExt.width;
- doc.height = newLayoutExt.height;
- doc.nativeWidth = newLayoutExt.nativeWidth;
- doc.nativeHeight = newLayoutExt.nativeHeight;
- doc.ignoreAspect = newLayoutExt.ignoreAspect;
- doc.backgroundLayout = newLayoutExt.backgroundLayout;
- doc.type = newLayoutExt.type;
- doc.layout = await newLayoutExt.layout;
+ let animheight = animDims ? animDims[1] : nativeHeight;
+ let animwidth = animDims ? animDims[0] : nativeWidth;
+
+ const highlightColors = ["transparent", "maroon", "maroon", "yellow", "magenta", "cyan", "orange"];
+ const highlightStyles = ["solid", "dashed", "solid", "solid", "solid", "solid", "solid", "solid"];
+ let highlighting = fullDegree && this.layoutDoc.type !== DocumentType.FONTICON && this.layoutDoc.viewType !== CollectionViewType.Linear;
+ return <div className={`documentView-node${this.topMost ? "-topmost" : ""}`} ref={this._mainCont}
+ onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick}
+ onPointerEnter={() => Doc.BrushDoc(this.props.Document)} onPointerLeave={e => Doc.UnBrushDoc(this.props.Document)}
+ style={{
+ transition: this.Document.isAnimating !== undefined ? ".5s linear" : StrCast(this.Document.transition),
+ pointerEvents: this.Document.isBackground && !this.isSelected() ? "none" : "all",
+ color: StrCast(this.Document.color),
+ outline: highlighting && !borderRounding ? `${highlightColors[fullDegree]} ${highlightStyles[fullDegree]} ${localScale}px` : "solid 0px",
+ border: highlighting && borderRounding ? `${highlightStyles[fullDegree]} ${highlightColors[fullDegree]} ${localScale}px` : undefined,
+ background: this.layoutDoc.type === DocumentType.FONTICON || this.layoutDoc.viewType === CollectionViewType.Linear ? undefined : backgroundColor,
+ width: animwidth,
+ height: animheight,
+ transform: `scale(${this.layoutDoc.fitWidth ? 1 : this.props.ContentScaling()})`,
+ opacity: this.Document.opacity
+ }} >
+ {this.innards}
+ </div>;
}
}
-Scripting.addGlobal(function toggleDetail(doc: any) {
- let native = typeof doc.layout === "string";
- swapViews(doc, native ? "layoutCustom" : "layoutNative", native ? "layoutNative" : "layoutCustom");
-}); \ No newline at end of file
+Scripting.addGlobal(function toggleDetail(doc: any) { doc.layoutKey = StrCast(doc.layoutKey, "layout") === "layout" ? "layoutCustom" : "layout"; }); \ No newline at end of file
diff --git a/src/client/views/nodes/DragBox.tsx b/src/client/views/nodes/DragBox.tsx
deleted file mode 100644
index 6c3db18c4..000000000
--- a/src/client/views/nodes/DragBox.tsx
+++ /dev/null
@@ -1,99 +0,0 @@
-import { library } from '@fortawesome/fontawesome-svg-core';
-import { faEdit } from '@fortawesome/free-regular-svg-icons';
-import { observer } from 'mobx-react';
-import * as React from 'react';
-import { Doc } from '../../../new_fields/Doc';
-import { createSchema, makeInterface } from '../../../new_fields/Schema';
-import { ScriptField } from '../../../new_fields/ScriptField';
-import { emptyFunction } from '../../../Utils';
-import { CompileScript } from '../../util/Scripting';
-import { ContextMenu } from '../ContextMenu';
-import { DocComponent } from '../DocComponent';
-import { OverlayView } from '../OverlayView';
-import { ScriptBox } from '../ScriptBox';
-import { DocumentIconContainer } from './DocumentIcon';
-import './DragBox.scss';
-import { FieldView, FieldViewProps } from './FieldView';
-import { DragManager } from '../../util/DragManager';
-import { Docs } from '../../documents/Documents';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-
-library.add(faEdit as any);
-
-const DragSchema = createSchema({
- onDragStart: ScriptField,
- text: "string"
-});
-
-type DragDocument = makeInterface<[typeof DragSchema]>;
-const DragDocument = makeInterface(DragSchema);
-@observer
-export class DragBox extends DocComponent<FieldViewProps, DragDocument>(DragDocument) {
- _downX: number = 0;
- _downY: number = 0;
- public static LayoutString() { return FieldView.LayoutString(DragBox); }
- _mainCont = React.createRef<HTMLDivElement>();
- onDragStart = (e: React.PointerEvent) => {
- if (!e.ctrlKey && !e.altKey && !e.shiftKey && !this.props.isSelected() && e.button === 0) {
- document.removeEventListener("pointermove", this.onDragMove);
- document.addEventListener("pointermove", this.onDragMove);
- document.removeEventListener("pointerup", this.onDragUp);
- document.addEventListener("pointerup", this.onDragUp);
- e.stopPropagation();
- e.preventDefault();
- }
- }
-
- onDragMove = (e: MouseEvent) => {
- if (!e.cancelBubble && (Math.abs(this._downX - e.clientX) > 5 || Math.abs(this._downY - e.clientY) > 5)) {
- document.removeEventListener("pointermove", this.onDragMove);
- document.removeEventListener("pointerup", this.onDragUp);
- const onDragStart = this.Document.onDragStart;
- e.stopPropagation();
- e.preventDefault();
- let res = onDragStart && onDragStart.script.run({ this: this.props.Document }).result;
- let doc = (res as Doc) || Docs.Create.FreeformDocument([], { nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: "freeform" });
- DragManager.StartDocumentDrag([this._mainCont.current!], new DragManager.DocumentDragData([doc]), e.clientX, e.clientY);
- }
- e.stopPropagation();
- e.preventDefault();
- }
-
- onDragUp = (e: MouseEvent) => {
- document.removeEventListener("pointermove", this.onDragMove);
- document.removeEventListener("pointerup", this.onDragUp);
- }
-
- onContextMenu = () => {
- ContextMenu.Instance.addItem({
- description: "Edit OnClick script", icon: "edit", event: () => {
- let overlayDisposer: () => void = emptyFunction;
- const script = this.Document.onDragStart;
- let originalText: string | undefined = undefined;
- if (script) originalText = script.script.originalScript;
- // tslint:disable-next-line: no-unnecessary-callback-wrapper
- let scriptingBox = <ScriptBox initialText={originalText} onCancel={() => overlayDisposer()} onSave={(text, onError) => {
- const script = CompileScript(text, {
- params: { this: Doc.name },
- typecheck: false,
- editable: true,
- transformer: DocumentIconContainer.getTransformer()
- });
- if (!script.compiled) {
- onError(script.errors.map(error => error.messageText).join("\n"));
- return;
- }
- this.Document.onClick = new ScriptField(script);
- overlayDisposer();
- }} showDocumentIcons />;
- overlayDisposer = OverlayView.Instance.addWindow(scriptingBox, { x: 400, y: 200, width: 500, height: 400, title: `${this.Document.title || ""} OnDragStart` });
- }
- });
- }
-
- render() {
- return (<div className="dragBox-outerDiv" onContextMenu={this.onContextMenu} onPointerDown={this.onDragStart} ref={this._mainCont}>
- <FontAwesomeIcon className="dragBox-icon" icon="folder" size="lg" color="white" />
- </div>);
- }
-} \ No newline at end of file
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index b93c78cfd..5108954bb 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -8,8 +8,6 @@ import { List } from "../../../new_fields/List";
import { RichTextField } from "../../../new_fields/RichTextField";
import { AudioField, ImageField, VideoField } from "../../../new_fields/URLField";
import { Transform } from "../../util/Transform";
-import { CollectionPDFView } from "../collections/CollectionPDFView";
-import { CollectionVideoView } from "../collections/CollectionVideoView";
import { CollectionView } from "../collections/CollectionView";
import { AudioBox } from "./AudioBox";
import { FormattedTextBox } from "./FormattedTextBox";
@@ -26,10 +24,8 @@ import { ScriptField } from "../../../new_fields/ScriptField";
//
export interface FieldViewProps {
fieldKey: string;
- fieldExt: string;
- leaveNativeSize?: boolean;
fitToBox?: boolean;
- ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
+ ContainingCollectionView: Opt<CollectionView>;
ContainingCollectionDoc: Opt<Doc>;
ruleProvider: Doc | undefined;
Document: Doc;
@@ -38,7 +34,7 @@ export interface FieldViewProps {
isSelected: () => boolean;
select: (isCtrlPressed: boolean) => void;
renderDepth: number;
- addDocument?: (document: Doc, allowDuplicates?: boolean) => boolean;
+ addDocument?: (document: Doc) => boolean;
addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean;
pinToPres: (document: Doc) => void;
removeDocument?: (document: Doc) => boolean;
@@ -56,9 +52,8 @@ export interface FieldViewProps {
@observer
export class FieldView extends React.Component<FieldViewProps> {
- public static LayoutString(fieldType: { name: string }, fieldStr: string = "data", fieldExt: string = "") {
- return `<${fieldType.name} {...props} fieldKey={"${fieldStr}"} fieldExt={"${fieldExt}"} />`;
- //"<ImageBox {...props} />"
+ public static LayoutString(fieldType: { name: string }, fieldStr: string) {
+ return `<${fieldType.name} {...props} fieldKey={"${fieldStr}"}/>`; //e.g., "<ImageBox {...props} fieldKey={"dada} />"
}
@computed
@@ -78,7 +73,7 @@ export class FieldView extends React.Component<FieldViewProps> {
return <FormattedTextBox {...this.props} />;
}
else if (field instanceof ImageField) {
- return <ImageBox {...this.props} leaveNativeSize={true} />;
+ return <ImageBox {...this.props} />;
}
// else if (field instaceof PresBox) {
// return <PresBox {...this.props} />;
diff --git a/src/client/views/nodes/DragBox.scss b/src/client/views/nodes/FontIconBox.scss
index fbb9b9c1c..75d093fcb 100644
--- a/src/client/views/nodes/DragBox.scss
+++ b/src/client/views/nodes/FontIconBox.scss
@@ -1,4 +1,4 @@
-.dragBox-outerDiv {
+.fontIconBox-outerDiv {
width: 100%;
height: 100%;
pointer-events: all;
diff --git a/src/client/views/nodes/FontIconBox.tsx b/src/client/views/nodes/FontIconBox.tsx
new file mode 100644
index 000000000..ae9273639
--- /dev/null
+++ b/src/client/views/nodes/FontIconBox.tsx
@@ -0,0 +1,46 @@
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { createSchema, makeInterface } from '../../../new_fields/Schema';
+import { DocComponent } from '../DocComponent';
+import './FontIconBox.scss';
+import { FieldView, FieldViewProps } from './FieldView';
+import { StrCast } from '../../../new_fields/Types';
+import { Utils } from "../../../Utils";
+import { runInAction, observable, reaction, IReactionDisposer } from 'mobx';
+import { Doc } from '../../../new_fields/Doc';
+const FontIconSchema = createSchema({
+ icon: "string"
+});
+
+type FontIconDocument = makeInterface<[typeof FontIconSchema]>;
+const FontIconDocument = makeInterface(FontIconSchema);
+@observer
+export class FontIconBox extends DocComponent<FieldViewProps, FontIconDocument>(FontIconDocument) {
+ public static LayoutString(fieldKey: string) { return FieldView.LayoutString(FontIconBox, fieldKey); }
+ @observable _foregroundColor = "white";
+ _ref: React.RefObject<HTMLButtonElement> = React.createRef();
+ _backgroundReaction: IReactionDisposer | undefined;
+ componentDidMount() {
+ this._backgroundReaction = reaction(() => this.props.Document.backgroundColor,
+ () => {
+ if (this._ref && this._ref.current) {
+ let col = Utils.fromRGBAstr(getComputedStyle(this._ref.current).backgroundColor!);
+ let colsum = (col.r + col.g + col.b);
+ if (colsum / col.a > 600 || col.a < 0.25) runInAction(() => this._foregroundColor = "black");
+ else if (colsum / col.a <= 600 || col.a >= .25) runInAction(() => this._foregroundColor = "white");
+ }
+ }, { fireImmediately: true });
+ }
+ componentWillUnmount() {
+ this._backgroundReaction && this._backgroundReaction();
+ }
+ render() {
+ let referenceDoc = (this.props.Document.dragFactory instanceof Doc ? this.props.Document.dragFactory : this.props.Document);
+ let referenceLayout = Doc.Layout(referenceDoc);
+ return <button className="fontIconBox-outerDiv" title={StrCast(this.props.Document.title)} ref={this._ref}
+ style={{ background: StrCast(referenceLayout.backgroundColor), boxShadow: this.props.Document.unchecked ? undefined : `4px 4px 12px black` }}>
+ <FontAwesomeIcon className="fontIconBox-icon" icon={this.Document.icon as any} color={this._foregroundColor} size="sm" />
+ </button>;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss
index 45e516015..a4acd3b82 100644
--- a/src/client/views/nodes/FormattedTextBox.scss
+++ b/src/client/views/nodes/FormattedTextBox.scss
@@ -10,8 +10,8 @@
outline: none !important;
}
-.formattedTextBox-cont-scroll,
-.formattedTextBox-cont-hidden {
+.formattedTextBox-cont {
+ cursor: text;
background: inherit;
padding: 0;
border-width: 0px;
@@ -25,10 +25,15 @@
color: initial;
height: 100%;
pointer-events: all;
-}
-
-.formattedTextBox-cont-hidden {
- pointer-events: none;
+ overflow-y: auto;
+ max-height: 100%;
+ .formattedTextBox-dictation {
+ height: 20px;
+ width: 20px;
+ top: 0px;
+ left: 0px;
+ position: absolute;
+ }
}
.formattedTextBox-inner-rounded {
@@ -40,9 +45,10 @@
left: 10%;
}
-.formattedTextBox-inner-rounded div,
-.formattedTextBox-inner div {
+.formattedTextBox-inner-rounded ,
+.formattedTextBox-inner {
padding: 10px 10px;
+ height: 100%;
}
.menuicon {
@@ -153,17 +159,18 @@ footnote::after {
content: "...";
}
-ol { counter-reset: deci1 0;}
-.decimal1-ol {counter-reset: deci1; p { display: inline }; font-size: 24 }
-.decimal2-ol {counter-reset: deci2; p { display: inline }; font-size: 18 }
-.decimal3-ol {counter-reset: deci3; p { display: inline }; font-size: 14 }
-.decimal4-ol {counter-reset: deci4; p { display: inline }; font-size: 10 }
-.decimal5-ol {counter-reset: deci5; p { display: inline }; font-size: 10 }
-.decimal6-ol {counter-reset: deci6; p { display: inline }; font-size: 10 }
-.decimal7-ol {counter-reset: deci7; p { display: inline }; font-size: 10 }
-.upper-alpha-ol {counter-reset: ualph; p { display: inline }; font-size: 18 }
+.ProseMirror {
+ol { counter-reset: deci1 0; padding-left: 0px; }
+.decimal1-ol {counter-reset: deci1; p { display: inline }; font-size: 24 ; ul, ol { padding-left:30px; } }
+.decimal2-ol {counter-reset: deci2; p { display: inline }; font-size: 18 ; ul, ol { padding-left:30px; } }
+.decimal3-ol {counter-reset: deci3; p { display: inline }; font-size: 14 ; ul, ol { padding-left:30px; } }
+.decimal4-ol {counter-reset: deci4; p { display: inline }; font-size: 10 ; ul, ol { padding-left:30px; } }
+.decimal5-ol {counter-reset: deci5; p { display: inline }; font-size: 10 ; ul, ol { padding-left:30px; } }
+.decimal6-ol {counter-reset: deci6; p { display: inline }; font-size: 10 ; ul, ol { padding-left:30px; } }
+.decimal7-ol {counter-reset: deci7; p { display: inline }; font-size: 10 ; ul, ol { padding-left:30px; } }
+.upper-alpha-ol {counter-reset: ualph; p { display: inline }; font-size: 18; }
.lower-roman-ol {counter-reset: lroman; p { display: inline }; font-size: 14; }
-.lower-alpha-ol {counter-reset: lalpha; p { display: inline }; font-size: 10;}
+.lower-alpha-ol {counter-reset: lalpha; p { display: inline }; font-size: 10; }
.decimal1:before { content: counter(deci1) ") "; counter-increment: deci1; display:inline-block; min-width: 30;}
.decimal2:before { content: counter(deci1) "." counter(deci2) ") "; counter-increment: deci2; display:inline-block; min-width: 35}
.decimal3:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) ") "; counter-increment: deci3; display:inline-block; min-width: 35}
@@ -174,3 +181,4 @@ ol { counter-reset: deci1 0;}
.upper-alpha:before { content: counter(deci1) "." counter(ualph, upper-alpha) ") "; counter-increment: ualph; display:inline-block; min-width: 35 }
.lower-roman:before { content: counter(deci1) "." counter(ualph, upper-alpha) "." counter(lroman, lower-roman) ") "; counter-increment: lroman;display:inline-block; min-width: 50 }
.lower-alpha:before { content: counter(deci1) "." counter(ualph, upper-alpha) "." counter(lroman, lower-roman) "." counter(lalpha, lower-alpha) ") "; counter-increment: lalpha; display:inline-block; min-width: 35}
+}
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 63a16f90c..24d6f2509 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -1,53 +1,56 @@
import { library } from '@fortawesome/fontawesome-svg-core';
import { faEdit, faSmile, faTextHeight, faUpload } from '@fortawesome/free-solid-svg-icons';
-import { action, computed, IReactionDisposer, Lambda, observable, reaction, runInAction } from "mobx";
+import _ from "lodash";
+import { action, computed, IReactionDisposer, Lambda, observable, reaction, runInAction, trace } 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, Node, Node as ProsNode, NodeType, Slice, Mark, ResolvedPos } from "prosemirror-model";
-import { EditorState, Plugin, Transaction, TextSelection, NodeSelection } from "prosemirror-state";
+import { Fragment, Mark, Node, Node as ProsNode, 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, DocListCast, Opt, WidthSym } from "../../../new_fields/Doc";
+import { Doc, DocListCastAsync, Opt, WidthSym, HeightSym } from "../../../new_fields/Doc";
import { Copy, Id } from '../../../new_fields/FieldSymbols';
-import { List } from '../../../new_fields/List';
-import { RichTextField, ToPlainText, FromPlainText } from "../../../new_fields/RichTextField";
-import { BoolCast, Cast, NumCast, StrCast, DateCast, PromiseValue } from "../../../new_fields/Types";
+import { RichTextField } from "../../../new_fields/RichTextField";
+import { RichTextUtils } from '../../../new_fields/RichTextUtils';
import { createSchema, makeInterface } from "../../../new_fields/Schema";
-import { Utils, numberRange, timenow } from '../../../Utils';
+import { Cast, DateCast, NumCast, StrCast } from "../../../new_fields/Types";
+import { numberRange, Utils, addStyleSheet, addStyleSheetRule, clearStyleSheetRules } from '../../../Utils';
+import { GoogleApiClientUtils, Pulls, Pushes } from '../../apis/google_docs/GoogleApiClientUtils';
import { DocServer } from "../../DocServer";
import { Docs, DocUtils } from '../../documents/Documents';
+import { DocumentType } from '../../documents/DocumentTypes';
+import { DictationManager } from '../../util/DictationManager';
import { DocumentManager } from '../../util/DocumentManager';
import { DragManager } from "../../util/DragManager";
import buildKeymap from "../../util/ProsemirrorExampleTransfer";
import { inpRules } from "../../util/RichTextRules";
-import { ImageResizeView, schema, SummarizedView, OrderedListView, FootnoteView } from "../../util/RichTextSchema";
+import { FootnoteView, ImageResizeView, DashDocView, OrderedListView, schema, SummarizedView } from "../../util/RichTextSchema";
import { SelectionManager } from "../../util/SelectionManager";
import { TooltipLinkingMenu } from "../../util/TooltipLinkingMenu";
import { TooltipTextMenu } from "../../util/TooltipTextMenu";
import { undoBatch, UndoManager } from "../../util/UndoManager";
-import { DocComponent } from "../DocComponent";
+import { DocExtendableComponent } from "../DocComponent";
+import { DocumentButtonBar } from '../DocumentButtonBar';
+import { DocumentDecorations } from '../DocumentDecorations';
import { InkingControl } from "../InkingControl";
import { FieldView, FieldViewProps } from "./FieldView";
import "./FormattedTextBox.scss";
+import { FormattedTextBoxComment, formattedTextBoxCommentPlugin } from './FormattedTextBoxComment';
import React = require("react");
-import { GoogleApiClientUtils, Pulls, Pushes } from '../../apis/google_docs/GoogleApiClientUtils';
-import { DocumentDecorations } from '../DocumentDecorations';
-import { DictationManager } from '../../util/DictationManager';
-import { ReplaceStep } from 'prosemirror-transform';
-import { DocumentType } from '../../documents/DocumentTypes';
-import { formattedTextBoxCommentPlugin, FormattedTextBoxComment } from './FormattedTextBoxComment';
-import { inputRules } from 'prosemirror-inputrules';
-import { DocumentButtonBar } from '../DocumentButtonBar';
+import { ContextMenuProps } from '../ContextMenuItem';
+import { ContextMenu } from '../ContextMenu';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { documentSchema } from '../../../new_fields/documentSchemas';
+import { AudioBox } from './AudioBox';
library.add(faEdit);
library.add(faSmile, faTextHeight, faUpload);
-export const Blank = `{"doc":{"type":"doc","content":[]},"selection":{"type":"text","anchor":0,"head":0}}`;
-
export interface FormattedTextBoxProps {
- isOverlay?: boolean;
hideOnLeave?: boolean;
height?: string;
color?: string;
@@ -61,23 +64,23 @@ const richTextSchema = createSchema({
export const GoogleRef = "googleDocId";
-type RichTextDocument = makeInterface<[typeof richTextSchema]>;
-const RichTextDocument = makeInterface(richTextSchema);
+type RichTextDocument = makeInterface<[typeof richTextSchema, typeof documentSchema]>;
+const RichTextDocument = makeInterface(richTextSchema, documentSchema);
-type PullHandler = (exportState: GoogleApiClientUtils.ReadResult, dataDoc: Doc) => void;
+type PullHandler = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => void;
@observer
-export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTextBoxProps), RichTextDocument>(RichTextDocument) {
- public static LayoutString(fieldStr: string = "data") {
- return FieldView.LayoutString(FormattedTextBox, fieldStr);
- }
+export class FormattedTextBox extends DocExtendableComponent<(FieldViewProps & FormattedTextBoxProps), RichTextDocument>(RichTextDocument) {
+ public static LayoutString(fieldStr: string) { return FieldView.LayoutString(FormattedTextBox, fieldStr); }
+ public static blankState = () => EditorState.create(FormattedTextBox.Instance.config);
+ public static Instance: FormattedTextBox;
private static _toolTipTextMenu: TooltipTextMenu | undefined = undefined;
private _ref: React.RefObject<HTMLDivElement> = React.createRef();
private _proseRef?: HTMLDivElement;
private _editorView: Opt<EditorView>;
private _applyingChange: boolean = false;
- private _linkClicked = "";
private _nodeClicked: any;
+ private _searchIndex = 0;
private _undoTyping?: UndoManager.Batch;
private _searchReactionDisposer?: Lambda;
private _scrollToRegionReactionDisposer: Opt<IReactionDisposer>;
@@ -90,13 +93,11 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
private _pushReactionDisposer: Opt<IReactionDisposer>;
private dropDisposer?: DragManager.DragDropDisposer;
- @observable private _fontSize = 13;
- @observable private _fontFamily = "Arial";
+ @observable private _ruleFontSize = 0;
+ @observable private _ruleFontFamily = "Arial";
@observable private _fontAlign = "";
@observable private _entered = false;
- @observable public static InputBoxOverlay?: FormattedTextBox = undefined;
public static SelectOnLoad = "";
- public static InputBoxOverlayScroll: number = 0;
public static IsFragment(html: string) {
return html.indexOf("data-pm-slice") !== -1;
}
@@ -118,8 +119,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
return "";
}
- public static getToolTip() {
- return this._toolTipTextMenu;
+ public static getToolTip(ev: EditorView) {
+ return this._toolTipTextMenu ? this._toolTipTextMenu : this._toolTipTextMenu = new TooltipTextMenu(ev);
}
@undoBatch
@@ -127,95 +128,20 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
let view = this._editorView!;
if (view.state.selection.from === view.state.selection.to) return false;
if (view.state.selection.to - view.state.selection.from > view.state.doc.nodeSize - 3) {
- this.props.Document.color = color;
+ this.layoutDoc.color = color;
}
let colorMark = view.state.schema.mark(view.state.schema.marks.pFontColor, { color: color });
view.dispatch(view.state.tr.addMark(view.state.selection.from, view.state.selection.to, colorMark));
return true;
}
- constructor(props: FieldViewProps) {
+ constructor(props: any) {
super(props);
- if (this.props.isOverlay) {
- DragManager.StartDragFunctions.push(() => FormattedTextBox.InputBoxOverlay = undefined);
- }
-
- this._scrollToRegionReactionDisposer = reaction(
- () => StrCast(this.props.Document.scrollToLinkID),
- async (scrollToLinkID) => {
- let findLinkFrag = (frag: Fragment, editor: EditorView) => {
- const nodes: Node[] = [];
- frag.forEach((node, index) => {
- let examinedNode = findLinkNode(node, editor);
- if (examinedNode && examinedNode.textContent) {
- nodes.push(examinedNode);
- start += index;
- }
- });
- return { frag: Fragment.fromArray(nodes), start: start };
- };
- let findLinkNode = (node: Node, editor: EditorView) => {
- if (!node.isText) {
- const content = findLinkFrag(node.content, editor);
- return node.copy(content.frag);
- }
- const marks = [...node.marks];
- const linkIndex = marks.findIndex(mark => mark.type === editor.state.schema.marks.link);
- return linkIndex !== -1 && scrollToLinkID === marks[linkIndex].attrs.href.replace(/.*\/doc\//, "") ? node : undefined;
- };
-
- let start = -1;
- if (this._editorView && scrollToLinkID) {
- let editor = this._editorView;
- let ret = findLinkFrag(editor.state.doc.content, editor);
-
- if (ret.frag.size > 2 && ((!this.props.isOverlay && !this.props.isSelected()) || (this.props.isSelected() && this.props.isOverlay))) {
- 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
- }
- 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);
- setTimeout(() => editor.dispatch(editor.state.tr.addMark(selection.from, selection.to, mark)), 0);
- setTimeout(() => this.unhighlightSearchTerms(), 2000);
- }
- this.props.Document.scrollToLinkID = undefined;
- }
-
- },
- { fireImmediately: true }
- );
+ FormattedTextBox.Instance = this;
}
public get CurrentDiv(): HTMLDivElement { return this._ref.current!; }
- @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); }
-
- @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplate ? this.props.DataDoc : Doc.GetProto(this.props.Document); }
-
- // this should be internal to prosemirror, but is needed
- // here to make sure that footnote view nodes in the overlay editor
- // get removed when they're not selected.
-
- syncNodeSelection(view: any, sel: any) {
- if (sel instanceof NodeSelection) {
- var desc = view.docView.descAt(sel.from);
- if (desc !== view.lastSelectedViewDesc) {
- if (view.lastSelectedViewDesc) {
- view.lastSelectedViewDesc.deselectNode();
- view.lastSelectedViewDesc = null;
- }
- if (desc) { desc.selectNode(); }
- view.lastSelectedViewDesc = desc;
- }
- } else {
- if (view.lastSelectedViewDesc) {
- view.lastSelectedViewDesc.deselectNode();
- view.lastSelectedViewDesc = null;
- }
- }
- }
-
linkOnDeselect: Map<string, string> = new Map();
doLinkOnDeselect() {
@@ -228,7 +154,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
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(this.dataDoc, this.dataDoc[key] as Doc, undefined, "Ref:" + value, undefined, undefined, id, true);
+ else DocUtils.MakeLink({ doc: this.dataDoc, ctx: this.props.ContainingCollectionDoc }, { doc: this.dataDoc[key] as Doc }, "Ref:" + value, "link to named target", id);
});
});
});
@@ -252,8 +178,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
this.linkOnDeselect.set(key, value);
let id = Utils.GenerateDeterministicGuid(this.dataDoc[Id] + key);
- const link = this._editorView!.state.schema.marks.link.create({ href: `http://localhost:1050/doc/${id}`, location: "onRight", title: value });
- const mval = this._editorView!.state.schema.marks.metadataVal.create();
+ const link = this._editorView.state.schema.marks.link.create({ href: `http://localhost:1050/doc/${id}`, location: "onRight", title: value });
+ const mval = this._editorView.state.schema.marks.metadataVal.create();
let offset = (tx.selection.to === range!.end - 1 ? -1 : 0);
tx = tx.addMark(textEndSelection - value.length + offset, textEndSelection, link).addMark(textEndSelection - value.length + offset, textEndSelection, mval);
this.dataDoc[key] = value;
@@ -261,11 +187,9 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
const state = this._editorView.state.apply(tx);
this._editorView.updateState(state);
- this.syncNodeSelection(this._editorView, this._editorView.state.selection); // bcz: ugh -- shouldn't be needed but without this the overlay view's footnote popup doesn't get deselected
- if (state.selection.empty && FormattedTextBox._toolTipTextMenu && tx.storedMarks) {
- FormattedTextBox._toolTipTextMenu.mark_key_pressed(tx.storedMarks);
- }
+ let 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;
this.extensionDoc && (this.extensionDoc.text = state.doc.textBetween(0, state.doc.content.size, "\n\n"));
this.extensionDoc && (this.extensionDoc.lastModified = new DateField(new Date(Date.now())));
@@ -284,44 +208,33 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
}
- public highlightSearchTerms = (terms: String[]) => {
+ public highlightSearchTerms = (terms: string[]) => {
if (this._editorView && (this._editorView as any).docView) {
- const doc = this._editorView.state.doc;
const mark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight);
- doc.nodesBetween(0, doc.content.size, (node: ProsNode, pos: number, parent: ProsNode, index: number) => {
- if (node.isLeaf && node.isText && node.text) {
- let nodeText: String = node.text;
- let tokens = nodeText.split(" ");
- let start = pos;
- tokens.forEach((word) => {
- if (terms.includes(word) && this._editorView) {
- this._editorView.dispatch(this._editorView.state.tr.addMark(start, start + word.length, mark).removeStoredMark(mark));
- }
- start += word.length + 1;
- });
- }
- });
+ const activeMark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight, { selected: true });
+ let res = terms.map(term => this.findInNode(this._editorView!, this._editorView!.state.doc, term));
+ let tr = this._editorView.state.tr;
+ let flattened: TextSelection[] = [];
+ res.map(r => r.map(h => flattened.push(h)));
+ let lastSel = Math.min(flattened.length - 1, this._searchIndex);
+ flattened.forEach((h: TextSelection, ind: number) => tr = tr.addMark(h.from, h.to, ind === lastSel ? activeMark : mark));
+ this._searchIndex = ++this._searchIndex > flattened.length - 1 ? 0 : this._searchIndex;
+ this._editorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(flattened[lastSel].from), tr.doc.resolve(flattened[lastSel].to))).scrollIntoView());
}
}
public unhighlightSearchTerms = () => {
if (this._editorView && (this._editorView as any).docView) {
- const doc = this._editorView.state.doc;
const mark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight);
- doc.nodesBetween(0, doc.content.size, (node: ProsNode, pos: number, parent: ProsNode, index: number) => {
- if (node.isLeaf && node.isText && node.text) {
- if (node.marks.includes(mark) && this._editorView) {
- this._editorView.dispatch(this._editorView.state.tr.removeMark(pos, pos + node.nodeSize, mark));
- }
- }
- });
+ const activeMark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight, { selected: true });
+ let end = this._editorView.state.doc.nodeSize - 2;
+ this._editorView.dispatch(this._editorView.state.tr.removeMark(0, end, mark).removeMark(0, end, activeMark));
}
}
setAnnotation = (start: number, end: number, mark: Mark, opened: boolean, keep: boolean = false) => {
let view = this._editorView!;
- let mid = view.state.doc.resolve(Math.round((start + end) / 2));
let nmark = view.state.schema.marks.user_mark.create({ ...mark.attrs, userid: keep ? Doc.CurrentUserEmail : mark.attrs.userid, opened: opened });
- view.dispatch(view.state.tr.removeMark(start, end, nmark).addMark(start, end, nmark).setSelection(new TextSelection(mid)));
+ view.dispatch(view.state.tr.removeMark(start, end, nmark).addMark(start, end, nmark));
}
protected createDropTarget = (ele: HTMLDivElement) => {
this._proseRef = ele;
@@ -332,49 +245,159 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
@undoBatch
@action
drop = async (e: Event, de: DragManager.DropEvent) => {
- // We're dealing with a link to a document
- if (de.data instanceof DragManager.EmbedDragData && de.data.urlField) {
- let target = de.data.embeddableSourceDoc;
- // We're dealing with an internal document drop
- let url = de.data.urlField.url.href;
- let model: NodeType = (url.includes(".mov") || url.includes(".mp4")) ? schema.nodes.video : schema.nodes.image;
- let pos = this._editorView!.posAtCoords({ left: de.x, top: de.y });
- this._editorView!.dispatch(this._editorView!.state.tr.insert(pos!.pos, model.create({ src: url, docid: target[Id] })));
- DocUtils.MakeLink(this.dataDoc, target, undefined, "ImgRef:" + target.title, undefined, undefined, target[Id]);
- e.stopPropagation();
- } else if (de.data instanceof DragManager.DocumentDragData) {
+ if (de.data instanceof DragManager.DocumentDragData) {
const draggedDoc = de.data.draggedDocuments.length && de.data.draggedDocuments[0];
- if (draggedDoc && draggedDoc.type === DocumentType.TEXT && !Doc.AreProtosEqual(draggedDoc, this.props.Document)) {
- if (de.mods === "AltKey") {
- if (draggedDoc.data instanceof RichTextField) {
- Doc.GetProto(this.dataDoc)[this.props.fieldKey] = new RichTextField(draggedDoc.data.Data);
- e.stopPropagation();
- }
- } else {
- draggedDoc.isTemplate = true;
- if (typeof (draggedDoc.layout) === "string") {
- let layoutDelegateToOverrideFieldKey = Doc.MakeDelegate(draggedDoc);
- layoutDelegateToOverrideFieldKey.layout = StrCast(layoutDelegateToOverrideFieldKey.layout).replace(/fieldKey={"[^"]*"}/, `fieldKey={"${this.props.fieldKey}"}`);
- this.props.Document.layout = layoutDelegateToOverrideFieldKey;
- } else {
- this.props.Document.layout = draggedDoc.layout instanceof Doc ? draggedDoc.layout : draggedDoc;
- }
+ // replace text contents whend dragging with Alt
+ if (draggedDoc && draggedDoc.type === DocumentType.TEXT && !Doc.AreProtosEqual(draggedDoc, this.props.Document) && de.mods === "AltKey") {
+ if (draggedDoc.data instanceof RichTextField) {
+ Doc.GetProto(this.dataDoc)[this.props.fieldKey] = new RichTextField(draggedDoc.data.Data);
+ e.stopPropagation();
+ }
+ // apply as template when dragging with Meta
+ } else if (draggedDoc && draggedDoc.type === DocumentType.TEXT && !Doc.AreProtosEqual(draggedDoc, this.props.Document) && de.mods === "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.layoutCustom = newLayout;
+ this.Document.layoutKey = "layoutCustom";
e.stopPropagation();
+ // embed document when dragging with a userDropAction or an embedDoc flag set
+ } else if (de.data.userDropAction || de.data.embedDoc) {
+ let target = de.data.droppedDocuments[0];
+ const link = DocUtils.MakeLink({ doc: this.dataDoc, ctx: this.props.ContainingCollectionDoc }, { doc: target }, "Embedded Doc:" + target.title);
+ if (link) {
+ target.fitToBox = true;
+ let node = schema.nodes.dashDoc.create({
+ width: target[WidthSym](), height: target[HeightSym](),
+ title: "dashDoc", docid: target[Id],
+ float: "right"
+ });
+ let view = this._editorView!;
+ view.dispatch(view.state.tr.insert(view.posAtCoords({ left: de.x, top: de.y })!.pos, node));
+ this.tryUpdateHeight();
+ e.stopPropagation();
+ }
+ } // otherwise, fall through to outer collection to handle drop
+ }
+ }
+
+ getNodeEndpoints(context: Node, node: Node): { from: number, to: number } | null {
+ let offset = 0;
+
+ if (context === node) return { from: offset, to: offset + node.nodeSize };
+
+ if (node.isBlock) {
+ for (let i = 0; i < (context.content as any).content.length; i++) {
+ let result = this.getNodeEndpoints((context.content as any).content[i], node);
+ if (result) {
+ return {
+ from: result.from + offset + (context.type.name === "doc" ? 0 : 1),
+ to: result.to + offset + (context.type.name === "doc" ? 0 : 1)
+ };
+ }
+ offset += (context.content as any).content[i].nodeSize;
}
+ return null;
+ } else {
+ return null;
}
}
- recordKeyHandler = (e: KeyboardEvent) => {
- if (SelectionManager.SelectedDocuments().length && this.props.Document === SelectionManager.SelectedDocuments()[0].props.Document) {
- if (e.key === "R" && e.altKey) {
- e.stopPropagation();
- e.preventDefault();
- this.recordBullet();
+
+ //Recursively finds matches within a given node
+ findInNode(pm: EditorView, node: Node, find: string) {
+ let ret: TextSelection[] = [];
+
+ if (node.isTextblock) {
+ let index = 0, foundAt, ep = this.getNodeEndpoints(pm.state.doc, node);
+ while (ep && (foundAt = node.textContent.slice(index).search(RegExp(find, "i"))) > -1) {
+ let sel = new TextSelection(pm.state.doc.resolve(ep.from + index + foundAt + 1), pm.state.doc.resolve(ep.from + index + foundAt + find.length + 1));
+ ret.push(sel);
+ index = index + foundAt + find.length;
}
+ } else {
+ node.content.forEach((child, i) => ret = ret.concat(this.findInNode(pm, child, find)));
+ }
+ return ret;
+ }
+ static _highlights: string[] = [];
+
+ updateHighlights = () => {
+ clearStyleSheetRules(FormattedTextBox._userStyleSheet);
+ if (FormattedTextBox._highlights.indexOf("Text from Others") !== -1) {
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-remote", { background: "yellow" });
+ }
+ if (FormattedTextBox._highlights.indexOf("My Text") !== -1) {
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { background: "moccasin" });
+ }
+ if (FormattedTextBox._highlights.indexOf("Todo Items") !== -1) {
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "todo", { outline: "black solid 1px" });
+ }
+ if (FormattedTextBox._highlights.indexOf("Important Items") !== -1) {
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "important", { "font-size": "larger" });
+ }
+ if (FormattedTextBox._highlights.indexOf("Disagree Items") !== -1) {
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "disagree", { "text-decoration": "line-through" });
+ }
+ if (FormattedTextBox._highlights.indexOf("Ignore Items") !== -1) {
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "ignore", { "font-size": "0" });
+ }
+ if (FormattedTextBox._highlights.indexOf("By Recent Minute") !== -1) {
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { opacity: "0.1" });
+ let min = Math.round(Date.now() / 1000 / 60);
+ numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-min-" + (min - i), { opacity: ((10 - i - 1) / 10).toString() }));
+ setTimeout(() => this.updateHighlights());
+ }
+ if (FormattedTextBox._highlights.indexOf("By Recent Hour") !== -1) {
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { opacity: "0.1" });
+ let hr = Math.round(Date.now() / 1000 / 60 / 60);
+ numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-hr-" + (hr - i), { opacity: ((10 - i - 1) / 10).toString() }));
}
}
+ specificContextMenu = (e: React.MouseEvent): void => {
+ let funcs: ContextMenuProps[] = [];
+ funcs.push({ description: "Record Bullet", event: () => { e.stopPropagation(); this.recordBullet(); }, 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: () => {
+ e.stopPropagation();
+ if (FormattedTextBox._highlights.indexOf(option) === -1) {
+ FormattedTextBox._highlights.push(option);
+ } else {
+ FormattedTextBox._highlights.splice(FormattedTextBox._highlights.indexOf(option), 1);
+ }
+ this.updateHighlights();
+ }, icon: "expand-arrows-alt"
+ }));
+
+ 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 },
+ }).then(results => {
+ if (results && [DictationManager.Controls.Infringed].includes(results)) {
+ DictationManager.Controls.stop();
+ }
+ this._editorView!.focus();
+ });
+ }
+ stopDictation = (abort: boolean) => {
+ runInAction(() => this._recording = false);
+ DictationManager.Controls.stop(!abort);
+ }
+
recordBullet = async () => {
let completedCue = "end session";
let results = await DictationManager.Controls.listen({
@@ -405,12 +428,14 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
nextBullet = (pos: number) => {
if (this._editorView) {
let frag = Fragment.fromArray(this.newListItems(2));
- let slice = new Slice(frag, 2, 2);
- let state = this._editorView.state;
- this._editorView.dispatch(state.tr.step(new ReplaceStep(pos, pos, slice)));
- pos += 4;
- state = this._editorView.state;
- this._editorView.dispatch(state.tr.setSelection(TextSelection.create(this._editorView.state.doc, pos, pos)));
+ if (this._editorView.state.doc.resolve(pos).depth >= 2) {
+ let slice = new Slice(frag, 2, 2);
+ let state = this._editorView.state;
+ this._editorView.dispatch(state.tr.step(new ReplaceStep(pos, pos, slice)));
+ pos += 4;
+ state = this._editorView.state;
+ this._editorView.dispatch(state.tr.setSelection(TextSelection.create(this._editorView.state.doc, pos, pos)));
+ }
}
}
@@ -421,9 +446,10 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
_keymap: any = undefined;
@computed get config() {
this._keymap = buildKeymap(schema);
+ (schema as any).Document = this.props.Document;
return {
schema,
- plugins: this.props.isOverlay ? [
+ plugins: [
inputRules(inpRules),
this.tooltipTextMenuPlugin(),
history(),
@@ -436,33 +462,18 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
}),
formattedTextBoxCommentPlugin
- ] : [
- history(),
- keymap(this._keymap),
- keymap(baseKeymap),
- ]
+ ]
};
}
componentDidMount() {
-
- if (!this.props.isOverlay) {
- this._proxyReactionDisposer = reaction(() => this.props.isSelected(),
- () => {
- if (this.props.isSelected()) {
- FormattedTextBox.InputBoxOverlay = this;
- FormattedTextBox.InputBoxOverlayScroll = this._ref.current!.scrollTop;
- }
- }, { fireImmediately: true });
- }
-
this.pullFromGoogleDoc(this.checkState);
this.dataDoc[GoogleRef] && this.dataDoc.unchanged && runInAction(() => DocumentDecorations.Instance.isAnimatingFetch = true);
this._reactionDisposer = reaction(
() => {
const field = this.dataDoc ? Cast(this.dataDoc[this.props.fieldKey], RichTextField) : undefined;
- return field ? field.Data : Blank;
+ return field ? field.Data : RichTextUtils.Initialize();
},
incomingValue => {
if (this._editorView && !this._applyingChange) {
@@ -495,14 +506,14 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
);
this._heightReactionDisposer = reaction(
- () => this.props.Document[WidthSym](),
+ () => [this.layoutDoc[WidthSym](), this.layoutDoc.autoHeight],
() => this.tryUpdateHeight()
);
this._textReactionDisposer = reaction(
() => this.extensionDoc,
() => {
- if (this.dataDoc.text || this.dataDoc.lastModified) {
+ if (this.extensionDoc && (this.dataDoc.text || this.dataDoc.lastModified)) {
this.extensionDoc.text = this.dataDoc.text;
this.extensionDoc.lastModified = DateCast(this.dataDoc.lastModified)[Copy]();
this.dataDoc.text = undefined;
@@ -514,13 +525,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
this.setupEditor(this.config, this.dataDoc, this.props.fieldKey);
this._searchReactionDisposer = reaction(() => {
- return StrCast(this.props.Document.search_string);
+ return StrCast(this.layoutDoc.search_string);
}, searchString => {
- const fieldkey = 'preview';
- let preview = false;
- // if (!this._editorView && Object.keys(this.props.Document).indexOf(fieldkey) !== -1) {
- // preview = true;
- // }
if (searchString) {
this.highlightSearchTerms([searchString]);
}
@@ -532,7 +538,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
this._rulesReactionDisposer = reaction(() => {
let ruleProvider = this.props.ruleProvider;
- let heading = NumCast(this.props.Document.heading);
+ let heading = NumCast(this.layoutDoc.heading);
if (ruleProvider instanceof Doc) {
return {
align: StrCast(ruleProvider["ruleAlign_" + heading], ""),
@@ -543,8 +549,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
return undefined;
},
action((rules: any) => {
- this._fontFamily = rules ? rules.font : "Arial";
- this._fontSize = rules ? rules.size : 13;
+ this._ruleFontFamily = rules ? rules.font : "Arial";
+ this._ruleFontSize = rules ? rules.size : 0;
rules && setTimeout(() => {
const view = this._editorView!;
if (this._proseRef) {
@@ -560,23 +566,67 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}, 0);
}), { fireImmediately: true }
);
+ this._scrollToRegionReactionDisposer = reaction(
+ () => StrCast(this.layoutDoc.scrollToLinkID),
+ async (scrollToLinkID) => {
+ let findLinkFrag = (frag: Fragment, editor: EditorView) => {
+ const nodes: Node[] = [];
+ frag.forEach((node, index) => {
+ let examinedNode = findLinkNode(node, editor);
+ if (examinedNode && examinedNode.textContent) {
+ nodes.push(examinedNode);
+ start += index;
+ }
+ });
+ return { frag: Fragment.fromArray(nodes), start: start };
+ };
+ let findLinkNode = (node: Node, editor: EditorView) => {
+ if (!node.isText) {
+ const content = findLinkFrag(node.content, editor);
+ return node.copy(content.frag);
+ }
+ const marks = [...node.marks];
+ const linkIndex = marks.findIndex(mark => mark.type === editor.state.schema.marks.link);
+ return linkIndex !== -1 && scrollToLinkID === marks[linkIndex].attrs.href.replace(/.*\/doc\//, "") ? node : undefined;
+ };
+
+ let start = -1;
+ if (this._editorView && scrollToLinkID) {
+ let editor = this._editorView;
+ let ret = findLinkFrag(editor.state.doc.content, editor);
+
+ 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
+ }
+ 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);
+ setTimeout(() => editor.dispatch(editor.state.tr.addMark(selection.from, selection.to, mark)), 0);
+ setTimeout(() => this.unhighlightSearchTerms(), 2000);
+ }
+ Doc.SetInPlace(this.layoutDoc, "scrollToLinkID", undefined, false);
+ }
+
+ },
+ { fireImmediately: true }
+ );
setTimeout(() => this.tryUpdateHeight(), 0);
}
pushToGoogleDoc = async () => {
- this.pullFromGoogleDoc(async (exportState: GoogleApiClientUtils.ReadResult, dataDoc: Doc) => {
- let modes = GoogleApiClientUtils.WriteMode;
+ this.pullFromGoogleDoc(async (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => {
+ let modes = GoogleApiClientUtils.Docs.WriteMode;
let mode = modes.Replace;
- let reference: Opt<GoogleApiClientUtils.Reference> = Cast(this.dataDoc[GoogleRef], "string");
+ let reference: Opt<GoogleApiClientUtils.Docs.Reference> = Cast(this.dataDoc[GoogleRef], "string");
if (!reference) {
mode = modes.Insert;
- reference = { service: GoogleApiClientUtils.Service.Documents, title: StrCast(this.dataDoc.title) };
+ reference = { title: StrCast(this.dataDoc.title) };
}
let redo = async () => {
- let data = Cast(this.dataDoc.data, RichTextField);
- if (this._editorView && reference && data) {
- let content = data[ToPlainText]();
+ if (this._editorView && reference) {
+ let content = await RichTextUtils.GoogleDocs.Export(this._editorView.state);
let response = await GoogleApiClientUtils.Docs.write({ reference, content, mode });
response && (this.dataDoc[GoogleRef] = response.documentId);
let pushSuccess = response !== undefined && !("errors" in response);
@@ -585,7 +635,13 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
};
let undo = () => {
- let content = exportState.body;
+ if (!exportState) {
+ return;
+ }
+ let content: GoogleApiClientUtils.Docs.Content = {
+ text: exportState.text,
+ requests: []
+ };
if (reference && content) {
GoogleApiClientUtils.Docs.write({ reference, content, mode });
}
@@ -598,49 +654,41 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
pullFromGoogleDoc = async (handler: PullHandler) => {
let dataDoc = this.dataDoc;
let documentId = StrCast(dataDoc[GoogleRef]);
- let exportState: GoogleApiClientUtils.ReadResult = {};
+ let exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>;
if (documentId) {
- exportState = await GoogleApiClientUtils.Docs.read({ identifier: documentId });
+ exportState = await RichTextUtils.GoogleDocs.Import(documentId, dataDoc);
}
UndoManager.RunInBatch(() => handler(exportState, dataDoc), Pulls);
}
- updateState = (exportState: GoogleApiClientUtils.ReadResult, dataDoc: Doc) => {
+ updateState = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => {
let pullSuccess = false;
- if (exportState !== undefined && exportState.body !== undefined && exportState.title !== undefined) {
- const data = Cast(dataDoc.data, RichTextField);
- if (data instanceof RichTextField) {
- pullSuccess = true;
- dataDoc.data = new RichTextField(data[FromPlainText](exportState.body));
- setTimeout(() => {
- if (this._editorView) {
- let state = this._editorView.state;
- let end = state.doc.content.size - 1;
- this._editorView.dispatch(state.tr.setSelection(TextSelection.create(state.doc, end, end)));
- }
- }, 0);
- dataDoc.title = exportState.title;
- this.Document.customTitle = true;
- dataDoc.unchanged = true;
- }
+ if (exportState !== undefined) {
+ pullSuccess = true;
+ dataDoc.data = new RichTextField(JSON.stringify(exportState.state.toJSON()));
+ setTimeout(() => {
+ if (this._editorView) {
+ let state = this._editorView.state;
+ let end = state.doc.content.size - 1;
+ this._editorView.dispatch(state.tr.setSelection(TextSelection.create(state.doc, end, end)));
+ }
+ }, 0);
+ dataDoc.title = exportState.title;
+ this.Document.customTitle = true;
+ dataDoc.unchanged = true;
} else {
delete dataDoc[GoogleRef];
}
DocumentButtonBar.Instance.startPullOutcome(pullSuccess);
}
- checkState = (exportState: GoogleApiClientUtils.ReadResult, dataDoc: Doc) => {
- if (exportState !== undefined && exportState.body !== undefined && exportState.title !== undefined) {
- let data = Cast(dataDoc.data, RichTextField);
- if (data) {
- let storedPlainText = data[ToPlainText]() + "\n";
- let receivedPlainText = exportState.body;
- let storedTitle = dataDoc.title;
- let receivedTitle = exportState.title;
- let unchanged = storedPlainText === receivedPlainText && storedTitle === receivedTitle;
- dataDoc.unchanged = unchanged;
- DocumentButtonBar.Instance.setPullState(unchanged);
- }
+ checkState = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => {
+ if (exportState && this._editorView) {
+ let equalContent = _.isEqual(this._editorView.state.doc, exportState.state.doc);
+ let equalTitles = dataDoc.title === exportState.title;
+ let unchanged = equalContent && equalTitles;
+ dataDoc.unchanged = unchanged;
+ DocumentButtonBar.Instance.setPullState(unchanged);
}
}
@@ -667,55 +715,45 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
handlePaste = (view: EditorView, event: Event, slice: Slice): boolean => {
let cbe = event as ClipboardEvent;
- let docId: string;
- let regionId: string;
- if (!cbe.clipboardData) {
- return false;
- }
- let linkId: string;
- docId = cbe.clipboardData.getData("dash/pdfOrigin");
- regionId = cbe.clipboardData.getData("dash/pdfRegion");
- if (!docId || !regionId) {
- return false;
- }
-
- DocServer.GetRefField(docId).then(doc => {
- DocServer.GetRefField(regionId).then(region => {
- if (!(doc instanceof Doc) || !(region instanceof Doc)) {
- return;
- }
-
- let annotations = DocListCast(region.annotations);
- annotations.forEach(anno => anno.target = this.props.Document);
- let fieldExtDoc = Doc.fieldExtensionDoc(doc, "data");
- let targetAnnotations = DocListCast(fieldExtDoc.annotations);
- if (targetAnnotations) {
- targetAnnotations.push(region);
- fieldExtDoc.annotations = new List<Doc>(targetAnnotations);
- }
+ const pdfDocId = cbe.clipboardData && cbe.clipboardData.getData("dash/pdfOrigin");
+ const pdfRegionId = cbe.clipboardData && cbe.clipboardData.getData("dash/pdfRegion");
+ if (pdfDocId && pdfRegionId) {
+ DocServer.GetRefField(pdfDocId).then(pdfDoc => {
+ DocServer.GetRefField(pdfRegionId).then(pdfRegion => {
+ if ((pdfDoc instanceof Doc) && (pdfRegion instanceof Doc)) {
+ setTimeout(async () => {
+ const extension = Doc.fieldExtensionDoc(pdfDoc, "data");
+ if (extension) {
+ let targetAnnotations = await DocListCastAsync(extension.annotations);// bcz: NO... this assumes the pdf is using its 'data' field. need to have the PDF's view handle updating its own annotations
+ targetAnnotations && targetAnnotations.push(pdfRegion);
+ }
+ });
- let link = DocUtils.MakeLink(this.props.Document, region, doc);
- if (link) {
- cbe.clipboardData!.setData("dash/linkDoc", link[Id]);
- linkId = link[Id];
- let frag = addMarkToFrag(slice.content, (node: Node) => addLinkMark(node, StrCast(doc.title)));
- slice = new Slice(frag, slice.openStart, slice.openEnd);
- var tr = view.state.tr.replaceSelection(slice);
- view.dispatch(tr.scrollIntoView().setMeta("paste", true).setMeta("uiEvent", "paste"));
- }
+ let link = DocUtils.MakeLink({ doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, { doc: pdfRegion, ctx: pdfDoc }, "note on " + pdfDoc.title, "pasted PDF link");
+ if (link) {
+ cbe.clipboardData!.setData("dash/linkDoc", link[Id]);
+ let linkId = link[Id];
+ let frag = addMarkToFrag(slice.content, (node: Node) => addLinkMark(node, StrCast(pdfDoc.title), linkId));
+ slice = new Slice(frag, slice.openStart, slice.openEnd);
+ var tr = view.state.tr.replaceSelection(slice);
+ view.dispatch(tr.scrollIntoView().setMeta("paste", true).setMeta("uiEvent", "paste"));
+ }
+ }
+ });
});
- });
+ return true;
+ }
+ return false;
- return true;
function addMarkToFrag(frag: Fragment, marker: (node: Node) => Node) {
const nodes: Node[] = [];
frag.forEach(node => nodes.push(marker(node)));
return Fragment.fromArray(nodes);
}
- function addLinkMark(node: Node, title: string) {
+ function addLinkMark(node: Node, title: string, linkId: string) {
if (!node.isText) {
- const content = addMarkToFrag(node.content, (node: Node) => addLinkMark(node, title));
+ const content = addMarkToFrag(node.content, (node: Node) => addLinkMark(node, title, linkId));
return node.copy(content);
}
const marks = [...node.marks];
@@ -754,6 +792,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
},
dispatchTransaction: this.dispatchTransaction,
nodeViews: {
+ dashDoc(node, view, getPos) { return new DashDocView(node, view, getPos, self); },
image(node, view, getPos) { return new ImageResizeView(node, view, getPos, self.props.addDocTab); },
star(node, view, getPos) { return new SummarizedView(node, view, getPos); },
ordered_list(node, view, getPos) { return new OrderedListView(); },
@@ -762,7 +801,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
clipboardTextSerializer: this.clipboardTextSerializer,
handlePaste: this.handlePaste,
});
- (this._editorView as any).isOverlay = this.props.isOverlay;
if (startup) {
Doc.GetProto(doc).documentText = undefined;
this._editorView.dispatch(this._editorView.state.tr.insertText(startup));
@@ -774,9 +812,9 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
FormattedTextBox.SelectOnLoad = "";
this.props.select(false);
}
- else if (this.props.isOverlay) this._editorView!.focus();
+ 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: timenow() })];
+ 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) })];
}
getFont(font: string) {
switch (font) {
@@ -803,9 +841,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
this._searchReactionDisposer && this._searchReactionDisposer();
this._editorView && this._editorView.destroy();
}
-
-
onPointerDown = (e: React.PointerEvent): void => {
+ FormattedTextBoxComment.textBox = this;
let pos = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY });
pos && (this._nodeClicked = this._editorView!.state.doc.nodeAt(pos.pos));
if (this.props.onClick && e.button === 0) {
@@ -814,31 +851,25 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
if (e.button === 0 && this.props.isSelected() && !e.altKey && !e.ctrlKey && !e.metaKey) {
e.stopPropagation();
}
- let ctrlKey = e.ctrlKey;
if (e.button === 2 || (e.button === 0 && e.ctrlKey)) {
e.preventDefault();
}
}
onPointerUp = (e: React.PointerEvent): void => {
- FormattedTextBoxComment.textBox = this;
+ if (!(e.nativeEvent as any).formattedHandled) { FormattedTextBoxComment.textBox = this; }
+ (e.nativeEvent as any).formattedHandled = true;
+
if (e.buttons === 1 && this.props.isSelected() && !e.altKey) {
e.stopPropagation();
}
}
+ static InputBoxOverlay: FormattedTextBox | undefined;
@action
onFocused = (e: React.FocusEvent): void => {
- document.removeEventListener("keypress", this.recordKeyHandler);
- document.addEventListener("keypress", this.recordKeyHandler);
+ FormattedTextBox.InputBoxOverlay = this;
this.tryUpdateHeight();
- if (!this.props.isOverlay) {
- FormattedTextBox.InputBoxOverlay = this;
- } else {
- if (this._ref.current) {
- this._ref.current.scrollTop = FormattedTextBox.InputBoxOverlayScroll;
- }
- }
}
onPointerWheel = (e: React.WheelEvent): void => {
// if a text note is not selected and scrollable, this prevents us from being able to scroll and zoom out at the same time
@@ -847,79 +878,66 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
}
+ static _bulletStyleSheet: any = addStyleSheet();
+ static _userStyleSheet: any = addStyleSheet();
+
onClick = (e: React.MouseEvent): void => {
- let ctrlKey = e.ctrlKey;
- if (e.button === 0 && ((!this.props.isSelected() && !e.ctrlKey) || (this.props.isSelected() && e.ctrlKey)) && !e.metaKey && e.target) {
- let href = (e.target as any).href;
- let location: string;
- if ((e.target as any).attributes.location) {
- location = (e.target as any).attributes.location.value;
- }
- for (let parent = (e.target as any).parentNode; !href && parent; parent = parent.parentNode) {
- href = parent.childNodes[0].href ? parent.childNodes[0].href : parent.href;
- }
- let pcords = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY });
- let node = pcords && this._editorView!.state.doc.nodeAt(pcords.pos);
- if (node) {
- let link = node.marks.find(m => m.type === this._editorView!.state.schema.marks.link);
- if (link && !(link.attrs.docref && link.attrs.title)) { // bcz: getting hacky. this indicates that we clicked on a PDF excerpt quotation. In this case, we don't want to follow the link (we follow only the actual hyperlink for the quotation which is handled above).
- href = link && link.attrs.href;
- location = link && link.attrs.location;
- }
- }
- if (href) {
- if (href.indexOf(Utils.prepend("/doc/")) === 0) {
- this._linkClicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0];
- if (this._linkClicked) {
- DocServer.GetRefField(this._linkClicked).then(async linkDoc => {
- if (linkDoc instanceof Doc) {
- let proto = Doc.GetProto(linkDoc);
- let targetContext = await Cast(proto.targetContext, Doc);
- let jumpToDoc = await Cast(linkDoc.anchor2, Doc);
-
- if (jumpToDoc) {
- if (DocumentManager.Instance.getDocumentView(jumpToDoc)) {
- DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, undefined, undefined, NumCast((jumpToDoc === linkDoc.anchor2 ? linkDoc.anchor2Page : linkDoc.anchor1Page)));
- return;
- }
- }
- if (targetContext && (!jumpToDoc || targetContext !== await jumpToDoc.annotationOn)) {
- DocumentManager.Instance.jumpToDocument(targetContext, ctrlKey, false, document => this.props.addDocTab(document, undefined, location ? location : "inTab"));
- } else if (jumpToDoc) {
- DocumentManager.Instance.jumpToDocument(jumpToDoc, ctrlKey, false, document => this.props.addDocTab(document, undefined, location ? location : "inTab"));
- } else {
- DocumentManager.Instance.jumpToDocument(linkDoc, ctrlKey, false, document => this.props.addDocTab(document, undefined, location ? location : "inTab"));
- }
- }
- });
- e.stopPropagation();
- e.preventDefault();
- }
- } else {
- let webDoc = Docs.Create.WebDocument(href, { x: NumCast(this.props.Document.x, 0) + NumCast(this.props.Document.width, 0), y: NumCast(this.props.Document.y) });
- this.props.addDocument && this.props.addDocument(webDoc);
- this._linkClicked = webDoc[Id];
- }
- e.stopPropagation();
- e.preventDefault();
- }
+ if ((e.nativeEvent as any).formattedHandled) { e.stopPropagation(); return; }
+ (e.nativeEvent as any).formattedHandled = true;
+ // if (e.button === 0 && ((!this.props.isSelected() && !e.ctrlKey) || (this.props.isSelected() && e.ctrlKey)) && !e.metaKey && e.target) {
+ // let href = (e.target as any).href;
+ // let location: string;
+ // if ((e.target as any).attributes.location) {
+ // location = (e.target as any).attributes.location.value;
+ // }
+ // let pcords = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY });
+ // let node = pcords && this._editorView!.state.doc.nodeAt(pcords.pos);
+ // if (node) {
+ // let link = node.marks.find(m => m.type === this._editorView!.state.schema.marks.link);
+ // if (link && !(link.attrs.docref && link.attrs.title)) { // bcz: getting hacky. this indicates that we clicked on a PDF excerpt quotation. In this case, we don't want to follow the link (we follow only the actual hyperlink for the quotation which is handled above).
+ // href = link && link.attrs.href;
+ // location = link && link.attrs.location;
+ // }
+ // }
+ // if (href) {
+ // if (href.indexOf(Utils.prepend("/doc/")) === 0) {
+ // let linkClicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0];
+ // 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);
+ // });
+ // }
+ // } else {
+ // let webDoc = Docs.Create.WebDocument(href, { x: NumCast(this.layoutDoc.x, 0) + NumCast(this.layoutDoc.width, 0), y: NumCast(this.layoutDoc.y) });
+ // this.props.addDocument && this.props.addDocument(webDoc);
+ // }
+ // e.stopPropagation();
+ // e.preventDefault();
+ // }
+ // }
+
+ this.hitBulletTargets(e.clientX, e.clientY, e.nativeEvent.offsetX, e.shiftKey);
+ if (this._recording) setTimeout(() => { this.stopDictation(true); setTimeout(() => this.recordDictation(), 500); }, 500);
+ }
- }
- // 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.
- if (this.props.isSelected() && e.nativeEvent.offsetX < 40) {
- let pos = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY });
+ // 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.
+ hitBulletTargets(x: number, y: number, offsetX: number, select: boolean = false) {
+ clearStyleSheetRules(FormattedTextBox._bulletStyleSheet);
+ if (this.props.isSelected() && offsetX < 40) {
+ let pos = this._editorView!.posAtCoords({ left: x, top: y });
if (pos && pos.pos > 0) {
let node = this._editorView!.state.doc.nodeAt(pos.pos);
let node2 = node && node.type === schema.nodes.paragraph ? this._editorView!.state.doc.nodeAt(pos.pos - 1) : undefined;
if (node === this._nodeClicked && node2 && (node2.type === schema.nodes.ordered_list || node2.type === schema.nodes.list_item)) {
- let hit = this._editorView!.domAtPos(pos.pos).node as any;
- let beforeEle = document.querySelector("." + hit.className) as Element;
- let before = beforeEle ? window.getComputedStyle(beforeEle, ':before') : undefined;
+ let hit = this._editorView!.domAtPos(pos.pos).node as any; // let beforeEle = document.querySelector("." + hit.className) as Element;
+ let before = hit ? window.getComputedStyle(hit, ':before') : undefined;
let beforeWidth = before ? Number(before.getPropertyValue('width').replace("px", "")) : undefined;
- if (beforeWidth && e.nativeEvent.offsetX < beforeWidth) {
+ if (beforeWidth && offsetX < beforeWidth) {
let ol = this._editorView!.state.doc.nodeAt(pos.pos - 2) ? this._editorView!.state.doc.nodeAt(pos.pos - 2) : undefined;
- if (ol && ol.type === schema.nodes.ordered_list && !e.shiftKey) {
+ if (ol && ol.type === schema.nodes.ordered_list && select) {
this._editorView!.dispatch(this._editorView!.state.tr.setSelection(new NodeSelection(this._editorView!.state.doc.resolve(pos.pos - 2))));
+ addStyleSheetRule(FormattedTextBox._bulletStyleSheet, hit.className + ":before", { background: "gray" });
} else {
this._editorView!.dispatch(this._editorView!.state.tr.setNodeMarkup(pos.pos - 1, node2.type, { ...node2.attrs, visibility: !node2.attrs.visibility }));
}
@@ -927,25 +945,30 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
}
}
- this._proseRef!.focus();
- if (this._linkClicked) {
- this._linkClicked = "";
- e.preventDefault();
- e.stopPropagation();
- }
}
- onMouseDown = (e: React.MouseEvent): void => {
- if (!this.props.isSelected()) { // preventing default allows the onClick to be generated instead of being swallowed by the text box itself
- e.preventDefault(); // bcz: this would normally be in OnPointerDown - however, if done there, no mouse move events will be generated which makes transititioning to GoldenLayout's drag interactions impossible
+ onMouseUp = (e: React.MouseEvent): void => {
+ e.stopPropagation();
+
+ let view = this._editorView as any;
+ // this interposes on prosemirror's upHandler to prevent prosemirror's up from invoked multiple times when there
+ // are nested prosemirrors. We only want the lowest level prosemirror to be invoked.
+ if (view.mouseDown) {
+ let originalUpHandler = view.mouseDown.up;
+ view.root.removeEventListener("mouseup", originalUpHandler);
+ view.mouseDown.up = (e: MouseEvent) => {
+ !(e as any).formattedHandled && originalUpHandler(e);
+ // e.stopPropagation();
+ (e as any).formattedHandled = true;
+ };
+ view.root.addEventListener("mouseup", view.mouseDown.up);
}
}
tooltipTextMenuPlugin() {
- let myprops = this.props;
let self = FormattedTextBox;
return new Plugin({
- view(_editorView) {
- return self._toolTipTextMenu = new TooltipTextMenu(_editorView, myprops);
+ view(newView) {
+ return self._toolTipTextMenu = FormattedTextBox.getToolTip(newView);
}
});
}
@@ -959,7 +982,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
});
}
onBlur = (e: any) => {
- document.removeEventListener("keypress", this.recordKeyHandler);
+ //DictationManager.Controls.stop(false);
if (this._undoTyping) {
this._undoTyping.end();
this._undoTyping = undefined;
@@ -974,55 +997,70 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
if (e.key === "Tab" || e.key === "Enter") {
e.preventDefault();
}
- this._editorView!.dispatch(this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark.create({})).addStoredMark(schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: timenow() })));
+ let mark = schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.round(Date.now() / 1000 / 5) });
+ 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); }
}
@action
tryUpdateHeight() {
- const ChromeHeight = this.props.ChromeHeight;
- let sh = this._ref.current ? this._ref.current.scrollHeight : 0;
- if (!this.props.isOverlay && !this.props.Document.isAnimating && this.props.Document.autoHeight && sh !== 0) {
- let nh = this.props.Document.isTemplate ? 0 : NumCast(this.dataDoc.nativeHeight, 0);
- let dh = NumCast(this.props.Document.height, 0);
- this.props.Document.height = Math.max(10, (nh ? dh / nh * sh : sh) + (ChromeHeight ? ChromeHeight() : 0));
- this.dataDoc.nativeHeight = nh ? sh : undefined;
+ let scrollHeight = this._ref.current ? this._ref.current.scrollHeight : 0;
+ if (!this.layoutDoc.isAnimating && this.layoutDoc.autoHeight && scrollHeight !== 0 &&
+ 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
+ let nh = this.Document.isTemplateField ? 0 : NumCast(this.dataDoc.nativeHeight, 0);
+ let dh = NumCast(this.layoutDoc.height, 0);
+ this.layoutDoc.height = Math.max(10, (nh ? dh / nh * scrollHeight : scrollHeight) + (this.props.ChromeHeight ? this.props.ChromeHeight() : 0));
+ this.dataDoc.nativeHeight = nh ? scrollHeight : undefined;
}
}
render() {
- let style = this.props.isOverlay ? "scroll" : "hidden";
- let rounded = StrCast(this.props.Document.borderRounding) === "100%" ? "-rounded" : "";
- let interactive: "all" | "none" = InkingControl.Instance.selectedTool || this.props.Document.isBackground
- ? "none" : "all";
- Doc.UpdateDocumentExtensionForField(this.dataDoc, this.props.fieldKey);
+ trace();
+ let rounded = StrCast(this.layoutDoc.borderRounding) === "100%" ? "-rounded" : "";
+ let interactive = InkingControl.Instance.selectedTool || this.layoutDoc.isBackground;
+ if (this.props.isSelected()) {
+ FormattedTextBox._toolTipTextMenu!.updateFromDash(this._editorView!, undefined, this.props);
+ } else if (FormattedTextBoxComment.textBox === this) {
+ FormattedTextBoxComment.Hide();
+ }
return (
- <div className={`formattedTextBox-cont-${style}`} ref={this._ref}
+ <div className={`formattedTextBox-cont`} ref={this._ref}
style={{
- overflowY: this.props.Document.autoHeight ? "hidden" : "auto",
- height: this.props.Document.autoHeight ? "max-content" : this.props.height ? this.props.height : undefined,
+ height: this.layoutDoc.autoHeight ? "max-content" : this.props.height ? this.props.height : undefined,
background: this.props.hideOnLeave ? "rgba(0,0,0 ,0.4)" : undefined,
- opacity: this.props.hideOnLeave ? (this._entered || this.props.isSelected() || Doc.IsBrushed(this.props.Document) ? 1 : 0.1) : 1,
+ opacity: this.props.hideOnLeave ? (this._entered ? 1 : 0.1) : 1,
color: this.props.color ? this.props.color : this.props.hideOnLeave ? "white" : "inherit",
- pointerEvents: interactive,
- fontSize: this._fontSize,
- fontFamily: this._fontFamily,
+ pointerEvents: interactive ? "none" : "all",
+ fontSize: this._ruleFontSize ? this._ruleFontSize : NumCast(this.layoutDoc.fontSize, 13),
+ fontFamily: this._ruleFontFamily ? this._ruleFontFamily : StrCast(this.layoutDoc.fontFamily, "Crimson Text"),
}}
+ onContextMenu={this.specificContextMenu}
onKeyDown={this.onKeyPress}
onFocus={this.onFocused}
onClick={this.onClick}
onBlur={this.onBlur}
onPointerUp={this.onPointerUp}
onPointerDown={this.onPointerDown}
- onMouseDown={this.onMouseDown}
+ onMouseUp={this.onMouseUp}
onWheel={this.onPointerWheel}
onPointerEnter={action(() => this._entered = true)}
onPointerLeave={action(() => this._entered = false)}
>
- <div className={`formattedTextBox-inner${rounded}`} style={{ whiteSpace: "pre-wrap", pointerEvents: ((this.props.Document.isButton || this.props.onClick) && !this.props.isSelected()) ? "none" : undefined }} ref={this.createDropTarget} />
+ <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-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.2 }} icon={"microphone"} size="sm" />
+ </div>
</div>
);
}
diff --git a/src/client/views/nodes/FormattedTextBoxComment.tsx b/src/client/views/nodes/FormattedTextBoxComment.tsx
index 2d2fff06d..f35ca502f 100644
--- a/src/client/views/nodes/FormattedTextBoxComment.tsx
+++ b/src/client/views/nodes/FormattedTextBoxComment.tsx
@@ -6,7 +6,12 @@ import { Doc } from "../../../new_fields/Doc";
import { schema } from "../../util/RichTextSchema";
import { DocServer } from "../../DocServer";
import { Utils } from "../../../Utils";
-import { StrCast } from "../../../new_fields/Types";
+import { StrCast, Cast, FieldValue } from "../../../new_fields/Types";
+import { FormattedTextBox } from "./FormattedTextBox";
+import { DocUtils } from "../../documents/Documents";
+import { isBuffer } from "util";
+import { DocumentManager } from "../../util/DocumentManager";
+import { DocumentType } from "../../documents/DocumentTypes";
export let formattedTextBoxCommentPlugin = new Plugin({
view(editorView) { return new FormattedTextBoxComment(editorView); }
@@ -49,7 +54,8 @@ export class FormattedTextBoxComment {
static end: number;
static mark: Mark;
static opened: boolean;
- static textBox: any;
+ static textBox: FormattedTextBox | undefined;
+ static linkDoc: Doc | undefined;
constructor(view: any) {
if (!FormattedTextBoxComment.tooltip) {
const root = document.getElementById("root");
@@ -60,13 +66,19 @@ export class FormattedTextBoxComment {
FormattedTextBoxComment.tooltip.appendChild(FormattedTextBoxComment.tooltipText);
FormattedTextBoxComment.tooltip.className = "FormattedTextBox-tooltip";
FormattedTextBoxComment.tooltip.style.pointerEvents = "all";
+ FormattedTextBoxComment.tooltip.style.maxWidth = "350px";
FormattedTextBoxComment.tooltip.appendChild(input);
FormattedTextBoxComment.tooltip.onpointerdown = (e: PointerEvent) => {
- let keep = e.target && (e.target as any).type === "checkbox";
+ let keep = e.target && (e.target as any).type === "checkbox" ? true : false;
+ if (FormattedTextBoxComment.linkDoc && !keep && FormattedTextBoxComment.textBox) {
+ DocumentManager.Instance.FollowLink(FormattedTextBoxComment.linkDoc, FormattedTextBoxComment.textBox.props.Document,
+ (doc: Doc, maxLocation: string) => FormattedTextBoxComment.textBox!.props.addDocTab(doc, undefined, e.ctrlKey ? "inTab" : "onRight"));
+ }
FormattedTextBoxComment.opened = keep || !FormattedTextBoxComment.opened;
- FormattedTextBoxComment.textBox && FormattedTextBoxComment.textBox.setAnnotation(
+ FormattedTextBoxComment.textBox && FormattedTextBoxComment.start !== undefined && FormattedTextBoxComment.textBox.setAnnotation(
FormattedTextBoxComment.start, FormattedTextBoxComment.end, FormattedTextBoxComment.mark,
FormattedTextBoxComment.opened, keep);
+ e.stopPropagation();
};
root && root.appendChild(FormattedTextBoxComment.tooltip);
}
@@ -90,61 +102,69 @@ export class FormattedTextBoxComment {
let state = view.state;
// Don't do anything if the document/selection didn't change
if (lastState && lastState.doc.eq(state.doc) &&
- lastState.selection.eq(state.selection)) return;
+ lastState.selection.eq(state.selection)) {
+ return;
+ }
+ FormattedTextBoxComment.linkDoc = undefined;
+ const textBox = FormattedTextBoxComment.textBox;
+ if (!textBox || !textBox.props) {
+ return;
+ }
let set = "none";
+ let nbef = 0;
+ // this section checks to see if the insertion point is over text entered by a different user. If so, it sets ths comment text to indicate the user and the modification date
if (state.selection.$from) {
- let nbef = findStartOfMark(state.selection.$from, view, findOtherUserMark);
+ nbef = findStartOfMark(state.selection.$from, view, findOtherUserMark);
let naft = findEndOfMark(state.selection.$from, view, findOtherUserMark);
- const spos = state.selection.$from.pos - nbef;
- const epos = state.selection.$from.pos + naft;
- let child = state.selection.$from.nodeBefore;
- let mark = child && findOtherUserMark(child.marks);
let noselection = view.state.selection.$from === view.state.selection.$to;
+ let child: any = null;
+ state.doc.nodesBetween(state.selection.from, state.selection.to, (node: any, pos: number, parent: any) => !child && node.marks.length && (child = node));
+ let mark = child && findOtherUserMark(child.marks);
if (mark && child && (nbef || naft) && (!mark.attrs.opened || noselection)) {
- FormattedTextBoxComment.SetState(this, mark.attrs.opened, spos, epos, mark);
+ FormattedTextBoxComment.SetState(FormattedTextBoxComment.textBox, mark.attrs.opened, state.selection.$from.pos - nbef, state.selection.$from.pos + naft, mark);
}
- if (mark && child && nbef && naft) {
- FormattedTextBoxComment.tooltipText.textContent = mark.attrs.userid + " " + mark.attrs.modified;
- // These are in screen coordinates
- // let start = view.coordsAtPos(state.selection.from), end = view.coordsAtPos(state.selection.to);
- let 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
- let box = (document.getElementById("main-div") as any).getBoundingClientRect();
- // Find a center-ish x position from the selection endpoints (when
- // crossing lines, end may be more to the left)
- let left = Math.max((start.left + end.left) / 2, start.left + 3);
- FormattedTextBoxComment.tooltip.style.left = (left - box.left) + "px";
- FormattedTextBoxComment.tooltip.style.bottom = (box.bottom - start.top) + "px";
+ if (mark && child && ((nbef && naft) || !noselection)) {
+ FormattedTextBoxComment.tooltipText.textContent = mark.attrs.userid + " date=" + (new Date(mark.attrs.modified * 5000)).toDateString();
set = "";
}
}
+ // this checks if the selection is a hyperlink. If so, it displays the target doc's text for internal links, and the url of the target for external links.
if (set === "none" && state.selection.$from) {
- FormattedTextBoxComment.textBox = undefined;
- let nbef = findStartOfMark(state.selection.$from, view, findLinkMark);
+ nbef = findStartOfMark(state.selection.$from, view, findLinkMark);
let naft = findEndOfMark(state.selection.$from, view, findLinkMark);
- let child = state.selection.$from.nodeBefore;
+ let child: any = null;
+ state.doc.nodesBetween(state.selection.from, state.selection.to, (node: any, pos: number, parent: any) => !child && node.marks.length && (child = node));
let mark = child && findLinkMark(child.marks);
if (mark && child && nbef && naft) {
- FormattedTextBoxComment.tooltipText.textContent = "link : " + (mark.attrs.title || mark.attrs.href);
+ FormattedTextBoxComment.tooltipText.textContent = "external => " + mark.attrs.href;
if (mark.attrs.href.indexOf(Utils.prepend("/doc/")) === 0) {
let docTarget = mark.attrs.href.replace(Utils.prepend("/doc/"), "").split("?")[0];
- docTarget && DocServer.GetRefField(docTarget).then(linkDoc =>
- (linkDoc as Doc) && (FormattedTextBoxComment.tooltipText.textContent = "link :" + StrCast((linkDoc as Doc).title)));
+ docTarget && DocServer.GetRefField(docTarget).then(linkDoc => {
+ if (linkDoc instanceof Doc) {
+ FormattedTextBoxComment.linkDoc = linkDoc;
+ let target = FieldValue(Doc.AreProtosEqual(FieldValue(Cast(linkDoc.anchor1, Doc)), textBox.props.Document) ? Cast(linkDoc.anchor2, Doc) : Cast(linkDoc.anchor1, Doc));
+ let ext = (target && target.type !== DocumentType.PDFANNO && Doc.fieldExtensionDoc(target, "data")) || target; // try guessing that the target doc's data is in the 'data' field. probably need an 'overviewLayout' and then just display the target Document ....
+ let text = ext && StrCast(ext.text);
+ ext && (FormattedTextBoxComment.tooltipText.textContent = (target && target.type === DocumentType.PDFANNO ? "Quoted from " : "") + "=> " + (text || StrCast(ext.title)));
+ }
+ });
}
- // These are in screen coordinates
- // let start = view.coordsAtPos(state.selection.from), end = view.coordsAtPos(state.selection.to);
- let 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
- let box = (document.getElementById("main-div") as any).getBoundingClientRect();
- // Find a center-ish x position from the selection endpoints (when
- // crossing lines, end may be more to the left)
- let left = Math.max((start.left + end.left) / 2, start.left + 3);
- FormattedTextBoxComment.tooltip.style.left = (left - box.left) + "px";
- FormattedTextBoxComment.tooltip.style.bottom = (box.bottom - start.top) + "px";
set = "";
}
}
+ if (set !== "none") {
+ // These are in screen coordinates
+ // let start = view.coordsAtPos(state.selection.from), end = view.coordsAtPos(state.selection.to);
+ let 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
+ let box = (document.getElementById("mainView-container") as any).getBoundingClientRect();
+ // Find a center-ish x position from the selection endpoints (when
+ // crossing lines, end may be more to the left)
+ let left = Math.max((start.left + end.left) / 2, start.left + 3);
+ FormattedTextBoxComment.tooltip.style.left = (left - box.left) + "px";
+ FormattedTextBoxComment.tooltip.style.bottom = (box.bottom - start.top) + "px";
+ }
FormattedTextBoxComment.tooltip && (FormattedTextBoxComment.tooltip.style.display = set);
}
diff --git a/src/client/views/nodes/IconBox.tsx b/src/client/views/nodes/IconBox.tsx
index 63a504d1a..60f547b1e 100644
--- a/src/client/views/nodes/IconBox.tsx
+++ b/src/client/views/nodes/IconBox.tsx
@@ -24,8 +24,10 @@ library.add(faFilm, faTag, faTextHeight);
@observer
export class IconBox extends React.Component<FieldViewProps> {
- public static LayoutString() { return FieldView.LayoutString(IconBox); }
+ 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); }
@@ -69,17 +71,15 @@ export class IconBox extends React.Component<FieldViewProps> {
cm.addItem({ description: "Use Target Title", event: () => IconBox.AutomaticTitle(this.props.Document), icon: "text-height" });
}
}
- @observable _panelWidth: number = 0;
- @observable _panelHeight: number = 0;
render() {
let 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 || BoolCast(this.props.Document.hideLabel)) {
- this.props.Document.nativeWidth = (r.offset!.width + Number(MINIMIZED_ICON_SIZE));
- if (this.props.Document.height === Number(MINIMIZED_ICON_SIZE)) this.props.Document.width = this.props.Document.nativeWidth;
+ 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 }) =>
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index 00c069e1f..57c024bbf 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -1,43 +1,82 @@
-.imageBox-cont {
- padding: 0vw;
- position: relative;
- text-align: center;
- width: 100%;
- height: auto;
- max-width: 100%;
- max-height: 100%;
- pointer-events: none;
+.imageBox-cont, .imageBox-cont-interactive {
+ padding: 0vw;
+ position: absolute;
+ text-align: center;
+ width: 100%;
+ height: 100%;
+ max-width: 100%;
+ max-height: 100%;
+ pointer-events: none;
+ background:transparent;
}
+
+.imageBox-container {
+ border-radius: inherit;
+ width:100%;
+ height:100%;
+ position: absolute;
+}
+
.imageBox-cont-interactive {
- pointer-events: all;
- width:100%;
- height:auto;
+ pointer-events: all;
}
.imageBox-dot {
- position:absolute;
+ position: absolute;
bottom: 10;
left: 0;
border-radius: 10px;
- width:20px;
- height:20px;
- background:gray;
+ width: 20px;
+ height: 20px;
+ background: gray;
}
.imageBox-cont img {
height: auto;
- width:100%;
+ width: 100%;
}
+
.imageBox-cont-interactive img {
height: auto;
- width:100%;
+ width: 100%;
+}
+
+#google-photos {
+ transition: all 0.5s ease 0s;
+ width: 30px;
+ height: 30px;
+ position: absolute;
+ top: 15px;
+ right: 15px;
+ border: 2px solid black;
+ border-radius: 50%;
+ padding: 3px;
+ background: white;
+ cursor: pointer;
+ opacity:0.15;
+}
+#google-photos:hover{
+ opacity: 1;
+}
+
+#google-tags {
+ transition: all 0.5s ease 0s;
+ width: 30px;
+ height: 30px;
+ position: absolute;
+ bottom: 15px;
+ right: 15px;
+ border: 2px solid black;
+ border-radius: 50%;
+ padding: 3px;
+ background: white;
}
.imageBox-button {
- padding: 0vw;
- border: none;
- width: 100%;
- height: 100%;
+ padding: 0vw;
+ border: none;
+ width: 100%;
+ height: 100%;
}
.imageBox-audioBackground {
@@ -49,6 +88,7 @@
border-radius: 25px;
background: white;
opacity: 0.3;
+
svg {
width: 90% !important;
height: 70%;
@@ -59,44 +99,47 @@
}
#cf {
- position:relative;
- width:100%;
- margin:0 auto;
- display:flex;
+ position: relative;
+ width: 100%;
+ margin: 0 auto;
+ display: flex;
align-items: center;
- height:100%;
- overflow:hidden;
+ height: 100%;
+ overflow: hidden;
+
.imageBox-fadeBlocker {
- width:100%;
- height:100%;
+ width: 100%;
+ height: 100%;
background: black;
- display:flex;
+ display: flex;
flex-direction: row;
align-items: center;
z-index: 1;
+
.imageBox-fadeaway {
object-fit: contain;
- width:100%;
- height:100%;
+ width: 100%;
+ height: 100%;
}
}
- }
-
- #cf img {
- position:absolute;
- left:0;
- }
-
- .imageBox-fadeBlocker {
+}
+
+#cf img {
+ position: absolute;
+ left: 0;
+}
+
+.imageBox-fadeBlocker {
-webkit-transition: opacity 1s ease-in-out;
-moz-transition: opacity 1s ease-in-out;
-o-transition: opacity 1s ease-in-out;
transition: opacity 1s ease-in-out;
- }
- .imageBox-fadeBlocker:hover {
+}
+
+.imageBox-fadeBlocker:hover {
-webkit-transition: opacity 1s ease-in-out;
-moz-transition: opacity 1s ease-in-out;
-o-transition: opacity 1s ease-in-out;
transition: opacity 1s ease-in-out;
- opacity:0;
- } \ No newline at end of file
+ opacity: 0;
+} \ No newline at end of file
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 004f50590..212c99f9d 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -2,10 +2,8 @@ import { library } from '@fortawesome/fontawesome-svg-core';
import { faEye } from '@fortawesome/free-regular-svg-icons';
import { faAsterisk, faFileAudio, faImage, faPaintBrush } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, observable, runInAction } from 'mobx';
+import { action, computed, observable, runInAction, trace } from 'mobx';
import { observer } from "mobx-react";
-import Lightbox from 'react-image-lightbox';
-import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app
import { Doc, DocListCast, HeightSym, WidthSym } from '../../../new_fields/Doc';
import { List } from '../../../new_fields/List';
import { createSchema, listSpec, makeInterface } from '../../../new_fields/Schema';
@@ -13,20 +11,22 @@ import { ComputedField } from '../../../new_fields/ScriptField';
import { BoolCast, Cast, FieldValue, NumCast, StrCast } from '../../../new_fields/Types';
import { AudioField, ImageField } from '../../../new_fields/URLField';
import { RouteStore } from '../../../server/RouteStore';
-import { Utils } from '../../../Utils';
+import { Utils, returnOne, emptyFunction, OmitKeys } from '../../../Utils';
import { CognitiveServices, Confidence, Service, Tag } from '../../cognitive_services/CognitiveServices';
import { Docs } from '../../documents/Documents';
import { DragManager } from '../../util/DragManager';
import { undoBatch } from '../../util/UndoManager';
import { ContextMenu } from "../../views/ContextMenu";
import { ContextMenuProps } from '../ContextMenuItem';
-import { DocComponent } from '../DocComponent';
+import { DocAnnotatableComponent } from '../DocComponent';
import { InkingControl } from '../InkingControl';
-import { documentSchema } from './DocumentView';
import FaceRectangles from './FaceRectangles';
import { FieldView, FieldViewProps } from './FieldView';
import "./ImageBox.scss";
import React = require("react");
+import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
+import { documentSchema } from '../../../new_fields/documentSchemas';
+import { Id } from '../../../new_fields/FieldSymbols';
var requestImageSize = require('../../util/request-image-size');
var path = require('path');
const { Howl } = require('howler');
@@ -38,7 +38,10 @@ library.add(faFileAudio, faAsterisk);
export const pageSchema = createSchema({
curPage: "number",
- fitWidth: "boolean"
+ fitWidth: "boolean",
+ rotation: "number",
+ googlePhotosUrl: "string",
+ googlePhotosTags: "string"
});
interface Window {
@@ -54,32 +57,16 @@ type ImageDocument = makeInterface<[typeof pageSchema, typeof documentSchema]>;
const ImageDocument = makeInterface(pageSchema, documentSchema);
@observer
-export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageDocument) {
-
- public static LayoutString(fieldKey?: string) { return FieldView.LayoutString(ImageBox, fieldKey); }
+export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocument>(ImageDocument) {
+ public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ImageBox, fieldKey); }
private _imgRef: React.RefObject<HTMLImageElement> = React.createRef();
- private _downX: number = 0;
- private _downY: number = 0;
- private _lastTap: number = 0;
- @observable private _isOpen: boolean = false;
- private dropDisposer?: DragManager.DragDropDisposer;
-
- @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); }
-
- @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplate ? this.props.DataDoc : Doc.GetProto(this.props.Document); }
+ private _dropDisposer?: DragManager.DragDropDisposer;
+ @observable private _audioState = 0;
+ @observable static _showControls: boolean;
protected createDropTarget = (ele: HTMLDivElement) => {
- if (this.dropDisposer) {
- this.dropDisposer();
- }
- if (ele) {
- this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } });
- }
- }
- onDrop = (e: React.DragEvent) => {
- e.stopPropagation();
- e.preventDefault();
- console.log("IMPLEMENT ME PLEASE");
+ this._dropDisposer && this._dropDisposer();
+ ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } }));
}
@undoBatch
@@ -91,61 +78,18 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
e.stopPropagation();
}
de.mods === "MetaKey" && de.data.droppedDocuments.forEach(action((drop: Doc) => {
- Doc.AddDocToList(Doc.GetProto(this.extensionDoc), "Alternates", drop);
+ this.extensionDoc && Doc.AddDocToList(Doc.GetProto(this.extensionDoc), "Alternates", drop);
e.stopPropagation();
}));
}
}
- onPointerDown = (e: React.PointerEvent): void => {
- if (e.shiftKey && e.ctrlKey) {
- e.stopPropagation(); // allows default system drag drop of images with shift+ctrl only
- }
- // if (Date.now() - this._lastTap < 300) {
- // if (e.buttons === 1) {
- // this._downX = e.clientX;
- // this._downY = e.clientY;
- // document.removeEventListener("pointerup", this.onPointerUp);
- // document.addEventListener("pointerup", this.onPointerUp);
- // }
- // } else {
- // this._lastTap = Date.now();
- // }
- }
- @action
- onPointerUp = (e: PointerEvent): void => {
- document.removeEventListener("pointerup", this.onPointerUp);
- if (Math.abs(e.clientX - this._downX) < 2 && Math.abs(e.clientY - this._downY) < 2) {
- this._isOpen = true;
- }
- e.stopPropagation();
- }
-
- @action
- lightbox = (images: string[]) => {
- if (this._isOpen) {
- return (<Lightbox
- mainSrc={images[this.Document.curPage || 0]}
- nextSrc={images[((this.Document.curPage || 0) + 1) % images.length]}
- prevSrc={images[((this.Document.curPage || 0) + images.length - 1) % images.length]}
- onCloseRequest={action(() =>
- this._isOpen = false
- )}
- onMovePrevRequest={action(() =>
- this.Document.curPage = ((this.Document.curPage || 0) + images.length - 1) % images.length
- )}
- onMoveNextRequest={action(() =>
- this.Document.curPage = ((this.Document.curPage || 0) + 1) % images.length
- )}
- />);
- }
- }
-
recordAudioAnnotation = () => {
let gumStream: any;
let recorder: any;
let self = this;
- navigator.mediaDevices.getUserMedia({
+ const extensionDoc = this.extensionDoc;
+ extensionDoc && navigator.mediaDevices.getUserMedia({
audio: true
}).then(function (stream) {
gumStream = stream;
@@ -158,13 +102,13 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
body: formData
});
const files = await res.json();
- const url = Utils.prepend(files[0]);
+ const url = Utils.prepend(files[0].path);
// upload to server with known URL
- let audioDoc = Docs.Create.AudioDocument(url, { title: "audio test", x: NumCast(self.props.Document.x), y: NumCast(self.props.Document.y), width: 200, height: 32 });
+ let audioDoc = Docs.Create.AudioDocument(url, { title: "audio test", width: 200, height: 32 });
audioDoc.treeViewExpandedView = "layout";
- let audioAnnos = Cast(self.extensionDoc.audioAnnotations, listSpec(Doc));
+ let audioAnnos = Cast(extensionDoc.audioAnnotations, listSpec(Doc));
if (audioAnnos === undefined) {
- self.extensionDoc.audioAnnotations = new List([audioDoc]);
+ extensionDoc.audioAnnotations = new List([audioDoc]);
} else {
audioAnnos.push(audioDoc);
}
@@ -181,25 +125,22 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
@undoBatch
rotate = action(() => {
- let proto = Doc.GetProto(this.props.Document);
- let nw = this.props.Document.nativeWidth;
- let nh = this.props.Document.nativeHeight;
- let w = this.props.Document.width;
- let h = this.props.Document.height;
- proto.rotation = (NumCast(this.props.Document.rotation) + 90) % 360;
- proto.nativeWidth = nh;
- proto.nativeHeight = nw;
- this.props.Document.width = h;
- this.props.Document.height = w;
+ let nw = this.Document.nativeWidth;
+ let nh = this.Document.nativeHeight;
+ let w = this.Document.width;
+ let h = this.Document.height;
+ this.Document.rotation = ((this.Document.rotation || 0) + 90) % 360;
+ this.Document.nativeWidth = nh;
+ this.Document.nativeHeight = nw;
+ this.Document.width = h;
+ this.Document.height = w;
});
specificContextMenu = (e: React.MouseEvent): void => {
- let field = Cast(this.Document[this.props.fieldKey], ImageField);
+ const field = Cast(this.Document[this.props.fieldKey], ImageField);
if (field) {
- let url = field.url.href;
let funcs: ContextMenuProps[] = [];
- funcs.push({ description: "Copy path", event: () => Utils.CopyText(url), icon: "expand-arrows-alt" });
- funcs.push({ description: "Record 1sec audio", event: this.recordAudioAnnotation, icon: "expand-arrows-alt" });
+ 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" });
let existingAnalyze = ContextMenu.Instance.findByDescription("Analyzers...");
@@ -215,12 +156,10 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
extractFaces = () => {
let converter = (results: any) => {
let faceDocs = new List<Doc>();
- results.map((face: CognitiveServices.Image.Face) => faceDocs.push(Docs.Get.DocumentHierarchyFromJson(face, `Face: ${face.faceId}`)!));
+ results.reduce((face: CognitiveServices.Image.Face, faceDocs: List<Doc>) => faceDocs.push(Docs.Get.DocumentHierarchyFromJson(face, `Face: ${face.faceId}`)!), new List<Doc>());
return faceDocs;
};
- if (this.url) {
- CognitiveServices.Image.Appliers.ProcessImage(this.extensionDoc, ["faces"], this.url, Service.Face, converter);
- }
+ this.url && this.extensionDoc && CognitiveServices.Image.Appliers.ProcessImage(this.extensionDoc, ["faces"], this.url, Service.Face, converter);
}
generateMetadata = (threshold: Confidence = Confidence.Excellent) => {
@@ -232,36 +171,19 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
let sanitized = tag.name.replace(" ", "_");
tagDoc[sanitized] = ComputedField.MakeFunction(`(${tag.confidence} >= this.confidence) ? ${tag.confidence} : "${ComputedField.undefined}"`);
});
- this.extensionDoc.generatedTags = tagsList;
+ this.extensionDoc && (this.extensionDoc.generatedTags = tagsList);
tagDoc.title = "Generated Tags Doc";
tagDoc.confidence = threshold;
return tagDoc;
};
- if (this.url) {
- CognitiveServices.Image.Appliers.ProcessImage(this.extensionDoc, ["generatedTagsDoc"], this.url, Service.ComputerVision, converter);
- }
+ this.url && this.extensionDoc && CognitiveServices.Image.Appliers.ProcessImage(this.extensionDoc, ["generatedTagsDoc"], this.url, Service.ComputerVision, converter);
}
- @action
- onDotDown(index: number) {
- this.Document.curPage = index;
- }
@computed private get url() {
- let data = Cast(Doc.GetProto(this.props.Document)[this.props.fieldKey], ImageField);
+ let data = Cast(this.dataDoc[this.props.fieldKey], ImageField);
return data ? data.url.href : undefined;
}
- dots(paths: string[]) {
- let nativeWidth = FieldValue(this.Document.nativeWidth, 1);
- let dist = Math.min(nativeWidth / paths.length, 40);
- let left = (nativeWidth - paths.length * dist) / 2;
- return paths.map((p, i) =>
- <div className="imageBox-placer" key={i} >
- <div className="imageBox-dot" style={{ background: (i === this.Document.curPage ? "black" : "gray"), transform: `translate(${i * dist + left}px, 0px)` }} onPointerDown={(e: React.PointerEvent) => { e.stopPropagation(); this.onDotDown(i); }} />
- </div>
- );
- }
-
choosePath(url: URL) {
const lower = url.href.toLowerCase();
if (url.protocol === "data") {
@@ -292,32 +214,28 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
}
_curSuffix = "_m";
- resize(srcpath: string, layoutdoc: Doc) {
+ resize = (srcpath: string) => {
requestImageSize(srcpath)
.then((size: any) => {
let rotation = NumCast(this.dataDoc.rotation) % 180;
let realsize = rotation === 90 || rotation === 270 ? { height: size.width, width: size.height } : size;
let aspect = realsize.height / realsize.width;
- if (layoutdoc.nativeHeight !== 0 && layoutdoc.nativeWidth !== 0 && (Math.abs(NumCast(layoutdoc.height) - realsize.height) > 1 || Math.abs(NumCast(layoutdoc.width) - realsize.width) > 1)) {
+ if (this.Document.width && (Math.abs(1 - NumCast(this.Document.height) / NumCast(this.Document.width) / (realsize.height / realsize.width)) > 0.1)) {
setTimeout(action(() => {
- layoutdoc.height = layoutdoc[WidthSym]() * aspect;
- layoutdoc.nativeHeight = realsize.height;
- layoutdoc.nativeWidth = realsize.width;
+ this.Document.height = this.Document[WidthSym]() * aspect;
+ this.Document.nativeHeight = realsize.height;
+ this.Document.nativeWidth = realsize.width;
}), 0);
}
})
- .catch((err: any) => {
- console.log(err);
- });
+ .catch((err: any) => console.log(err));
}
- @observable _audioState = 0;
-
@action
onPointerEnter = () => {
let self = this;
- let audioAnnos = DocListCast(this.extensionDoc.audioAnnotations);
- if (audioAnnos.length && this._audioState === 0) {
+ let audioAnnos = this.extensionDoc && DocListCast(this.extensionDoc.audioAnnotations);
+ if (audioAnnos && audioAnnos.length && this._audioState === 0) {
let anno = audioAnnos[Math.floor(Math.random() * audioAnnos.length)];
anno.data instanceof AudioField && new Howl({
src: [anno.data.url.href],
@@ -331,43 +249,40 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
});
this._audioState = 1;
}
- // else {
- // if (this._audioState === 0) {
- // this._audioState = 1;
- // new Howl({
- // src: ["https://www.kozco.com/tech/piano2-CoolEdit.mp3"],
- // autoplay: true,
- // loop: false,
- // volume: 0.5,
- // onend: function () {
- // runInAction(() => self._audioState = 0);
- // }
- // });
- // }
- // }
}
- @action
- audioDown = () => {
- this.recordAudioAnnotation();
+ audioDown = () => this.recordAudioAnnotation();
+
+ considerGooglePhotosLink = () => {
+ const remoteUrl = this.Document.googlePhotosUrl;
+ return !remoteUrl ? (null) : (<img
+ id={"google-photos"}
+ src={"/assets/google_photos.png"}
+ onClick={() => window.open(remoteUrl)}
+ />);
}
+ considerGooglePhotosTags = () => {
+ const tags = this.Document.googlePhotosTags;
+ return !tags ? (null) : (<img id={"google-tags"} src={"/assets/google_tags.png"} />);
+ }
- render() {
+ @computed get content() {
+ const extensionDoc = this.extensionDoc;
+ if (!extensionDoc) return (null);
// let transform = this.props.ScreenToLocalTransform().inverse();
let pw = typeof this.props.PanelWidth === "function" ? this.props.PanelWidth() : typeof this.props.PanelWidth === "number" ? (this.props.PanelWidth as any) as number : 50;
// var [sptX, sptY] = transform.transformPoint(0, 0);
// let [bptX, bptY] = transform.transformPoint(pw, this.props.PanelHeight());
// let w = bptX - sptX;
- let nativeWidth = FieldValue(this.Document.nativeWidth, pw);
- let nativeHeight = FieldValue(this.Document.nativeHeight, 0);
- let paths: string[] = [Utils.CorsProxy("http://www.cs.brown.edu/~bcz/noImage.png")];
+ let nativeWidth = (this.Document.nativeWidth || pw);
+ let nativeHeight = (this.Document.nativeHeight || 0);
+ let paths = [Utils.CorsProxy("http://www.cs.brown.edu/~bcz/noImage.png")];
// this._curSuffix = "";
// if (w > 20) {
- Doc.UpdateDocumentExtensionForField(this.dataDoc, this.props.fieldKey);
- let alts = DocListCast(this.extensionDoc.Alternates);
- let altpaths: string[] = alts.filter(doc => doc.data instanceof ImageField).map(doc => this.choosePath((doc.data as ImageField).url));
+ let alts = DocListCast(extensionDoc.Alternates);
+ let altpaths = alts.filter(doc => doc.data instanceof ImageField).map(doc => this.choosePath((doc.data as ImageField).url));
let field = this.dataDoc[this.props.fieldKey];
// if (w < 100 && this._smallRetryCount < 10) this._curSuffix = "_s";
// else if (w < 600 && this._mediumRetryCount < 10) this._curSuffix = "_m";
@@ -375,19 +290,17 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
if (field instanceof ImageField) paths = [this.choosePath(field.url)];
paths.push(...altpaths);
// }
- let interactive = InkingControl.Instance.selectedTool || this.props.Document.isBackground ? "" : "-interactive";
- let rotation = NumCast(this.dataDoc.rotation, 0);
- let aspect = (rotation % 180) ? this.dataDoc[HeightSym]() / this.dataDoc[WidthSym]() : 1;
+ let interactive = InkingControl.Instance.selectedTool || this.Document.isBackground ? "" : "-interactive";
+ let rotation = NumCast(this.Document.rotation, 0);
+ let aspect = (rotation % 180) ? this.Document[HeightSym]() / this.Document[WidthSym]() : 1;
let shift = (rotation % 180) ? (nativeHeight - nativeWidth / aspect) / 2 : 0;
- let srcpath = paths[Math.min(paths.length - 1, this.Document.curPage || 0)];
+ let srcpath = paths[Math.min(paths.length - 1, (this.Document.curPage || 0))];
let fadepath = paths[Math.min(paths.length - 1, 1)];
- if (!this.props.Document.ignoreAspect && !this.props.leaveNativeSize) this.resize(srcpath, this.props.Document);
+ !this.Document.ignoreAspect && this.resize(srcpath);
return (
- <div className={`imageBox-cont${interactive}`} style={{ background: "transparent" }}
- onPointerDown={this.onPointerDown}
- onDrop={this.onDrop} ref={this.createDropTarget} onContextMenu={this.specificContextMenu}>
+ <div className={`imageBox-cont${interactive}`} key={this.props.Document[Id]} ref={this.createDropTarget} onContextMenu={this.specificContextMenu}>
<div id="cf">
<img
key={this._smallRetryCount + (this._mediumRetryCount << 4) + (this._largeRetryCount << 8)} // force cache to update on retrys
@@ -404,17 +317,43 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
ref={this._imgRef}
onError={this.onError} /></div>}
</div>
- {paths.length > 1 ? this.dots(paths) : (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.extensionDoc.audioAnnotations).length ? "blue" : "gray", "green", "red"][this._audioState] }} icon={faFileAudio} size="sm" />
+ style={{ color: [DocListCast(extensionDoc.audioAnnotations).length ? "blue" : "gray", "green", "red"][this._audioState] }} icon={!DocListCast(extensionDoc.audioAnnotations).length ? "microphone" : faFileAudio} size="sm" />
</div>
- {/* {this.lightbox(paths)} */}
- <FaceRectangles document={this.extensionDoc} color={"#0000FF"} backgroundColor={"#0000FF"} />
+ {this.considerGooglePhotosLink()}
+ <FaceRectangles document={extensionDoc} color={"#0000FF"} backgroundColor={"#0000FF"} />
</div>);
}
+
+ render() {
+ return (<div className={"imageBox-container"} onContextMenu={this.specificContextMenu}>
+ <CollectionFreeFormView {...this.props}
+ PanelHeight={this.props.PanelHeight}
+ PanelWidth={this.props.PanelWidth}
+ annotationsKey={this.annotationsKey}
+ isAnnotationOverlay={true}
+ focus={this.props.focus}
+ isSelected={this.props.isSelected}
+ select={emptyFunction}
+ active={this.active}
+ ContentScaling={returnOne}
+ whenActiveChanged={this.whenActiveChanged}
+ removeDocument={this.removeDocument}
+ moveDocument={this.moveDocument}
+ addDocument={this.addDocument}
+ CollectionView={undefined}
+ ScreenToLocalTransform={this.props.ScreenToLocalTransform}
+ ruleProvider={undefined}
+ renderDepth={this.props.renderDepth + 1}
+ ContainingCollectionDoc={this.props.ContainingCollectionDoc}
+ chromeCollapsed={true}>
+ {() => [this.content]}
+ </CollectionFreeFormView>
+ </div >);
+ }
} \ No newline at end of file
diff --git a/src/client/views/nodes/KeyValueBox.scss b/src/client/views/nodes/KeyValueBox.scss
index 87a9565e8..6e8a36c6a 100644
--- a/src/client/views/nodes/KeyValueBox.scss
+++ b/src/client/views/nodes/KeyValueBox.scss
@@ -9,6 +9,7 @@
box-sizing: border-box;
display: inline-block;
pointer-events: all;
+ cursor: default;
.imageBox-cont img {
width: auto;
}
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index 3a9318469..35e9e4862 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -1,7 +1,6 @@
import { action, computed, observable } from "mobx";
import { observer } from "mobx-react";
-import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app
import { Doc, Field, FieldResult } from "../../../new_fields/Doc";
import { List } from "../../../new_fields/List";
import { RichTextField } from "../../../new_fields/RichTextField";
@@ -26,11 +25,12 @@ export type KVPScript = {
@observer
export class KeyValueBox extends React.Component<FieldViewProps> {
+ public static LayoutString(fieldStr: string) { return FieldView.LayoutString(KeyValueBox, fieldStr); }
+
private _mainCont = React.createRef<HTMLDivElement>();
private _keyHeader = React.createRef<HTMLTableHeaderCellElement>();
- @observable private rows: KeyValuePair[] = [];
- public static LayoutString(fieldStr: string = "data") { return FieldView.LayoutString(KeyValueBox, fieldStr); }
+ @observable private rows: KeyValuePair[] = [];
@observable private _keyInput: string = "";
@observable private _valueInput: string = "";
@computed get splitPercentage() { return NumCast(this.props.Document.schemaSplitPercentage, 50); }
@@ -68,7 +68,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
public static ApplyKVPScript(doc: Doc, key: string, kvpScript: KVPScript): boolean {
const { script, type, onDelegate } = kvpScript;
- //const target = onDelegate ? (doc.layout instanceof Doc ? doc.layout : doc) : Doc.GetProto(doc); // bcz: need to be able to set fields on layout templates
+ //const target = onDelegate ? Doc.Layout(doc.layout) : Doc.GetProto(doc); // bcz: TODO need to be able to set fields on layout templates
const target = onDelegate ? doc : Doc.GetProto(doc);
let field: Field;
if (type === "computed") {
@@ -103,6 +103,8 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
e.stopPropagation();
}
+ rowHeight = () => 30;
+
createTable = () => {
let doc = this.fieldDocToLayout;
if (!doc) {
@@ -124,14 +126,15 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
let i = 0;
const self = this;
for (let key of Object.keys(ids).slice().sort()) {
- rows.push(<KeyValuePair doc={realDoc} addDocTab={this.props.addDocTab} ref={(function () {
- let oldEl: KeyValuePair | undefined;
- return (el: KeyValuePair) => {
- if (oldEl) self.rows.splice(self.rows.indexOf(oldEl), 1);
- oldEl = el;
- if (el) self.rows.push(el);
- };
- })()} keyWidth={100 - this.splitPercentage} rowStyle={"keyValueBox-" + (i++ % 2 ? "oddRow" : "evenRow")} key={key} keyName={key} />);
+ rows.push(<KeyValuePair doc={realDoc} addDocTab={this.props.addDocTab} PanelWidth={this.props.PanelWidth} PanelHeight={this.rowHeight}
+ ref={(function () {
+ let oldEl: KeyValuePair | undefined;
+ return (el: KeyValuePair) => {
+ if (oldEl) self.rows.splice(self.rows.indexOf(oldEl), 1);
+ oldEl = el;
+ if (el) self.rows.push(el);
+ };
+ })()} keyWidth={100 - this.splitPercentage} rowStyle={"keyValueBox-" + (i++ % 2 ? "oddRow" : "evenRow")} key={key} keyName={key} />);
}
return rows;
}
diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx
index 1fed4c8bb..225565964 100644
--- a/src/client/views/nodes/KeyValuePair.tsx
+++ b/src/client/views/nodes/KeyValuePair.tsx
@@ -1,6 +1,5 @@
import { action, observable } from 'mobx';
import { observer } from "mobx-react";
-import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app
import { Doc, Field, Opt } from '../../../new_fields/Doc';
import { emptyFunction, returnFalse, returnOne, returnZero } from '../../../Utils';
import { Docs } from '../../documents/Documents';
@@ -22,6 +21,8 @@ export interface KeyValuePairProps {
keyName: string;
doc: Doc;
keyWidth: number;
+ PanelHeight: () => number;
+ PanelWidth: () => number;
addDocTab: (doc: Doc, data: Opt<Doc>, where: string) => boolean;
}
@observer
@@ -59,7 +60,6 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
ContainingCollectionDoc: undefined,
ruleProvider: undefined,
fieldKey: this.props.keyName,
- fieldExt: "",
isSelected: returnFalse,
select: emptyFunction,
renderDepth: 1,
@@ -67,8 +67,8 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
whenActiveChanged: emptyFunction,
ScreenToLocalTransform: Transform.Identity,
focus: emptyFunction,
- PanelWidth: returnZero,
- PanelHeight: returnZero,
+ PanelWidth: this.props.PanelWidth,
+ PanelHeight: this.props.PanelHeight,
addDocTab: returnFalse,
pinToPres: returnZero,
ContentScaling: returnOne
diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss
index 2917c81cb..2d92c9581 100644
--- a/src/client/views/nodes/PDFBox.scss
+++ b/src/client/views/nodes/PDFBox.scss
@@ -6,9 +6,41 @@
width:100%;
overflow: hidden;
position:absolute;
- z-index: -1;
+ cursor:auto;
+ transform-origin: top left;
}
+.pdfBox-title-outer {
+ z-index: 0;
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ background: lightslategray;
+ .pdfBox-cont, .pdfBox-cont-interactive{
+ width: 150%;
+ height: 100%;
+ position: relative;
+ display: grid;
+
+ .pdfBox-title {
+ color:lightgray;
+ margin-top: auto;
+ margin-bottom: auto;
+ transform-origin: 42% -18%;
+ width: 100%;
+ transform: rotate(55deg);
+ font-size: 144;
+ padding: 5%;
+ overflow: hidden;
+ display: inline-block;
+ white-space: pre;
+ text-overflow: ellipsis;
+ text-align: center;
+ }
+ }
+}
+
+
.pdfBox-cont {
pointer-events: none;
.collectionFreeFormView-none {
@@ -16,6 +48,7 @@
}
.pdfViewer-text {
.textLayer {
+ will-change: transform;
span {
user-select: none;
}
@@ -27,6 +60,7 @@
pointer-events: all;
.pdfViewer-text {
.textLayer {
+ will-change: transform;
span {
user-select: text;
}
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index fe71e76fd..8e0515f8a 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -1,75 +1,96 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, IReactionDisposer, observable, reaction, runInAction, untracked, trace } from 'mobx';
+import { action, observable, runInAction, reaction, IReactionDisposer, trace, untracked, computed } from 'mobx';
import { observer } from "mobx-react";
import * as Pdfjs from "pdfjs-dist";
import "pdfjs-dist/web/pdf_viewer.css";
-import 'react-image-lightbox/style.css';
-import { Doc, Opt, WidthSym } from "../../../new_fields/Doc";
+import { Opt, WidthSym, Doc } from "../../../new_fields/Doc";
import { makeInterface } from "../../../new_fields/Schema";
import { ScriptField } from '../../../new_fields/ScriptField';
-import { Cast, NumCast } from "../../../new_fields/Types";
+import { Cast } from "../../../new_fields/Types";
import { PdfField } from "../../../new_fields/URLField";
+import { Utils } from '../../../Utils';
import { KeyCodes } from '../../northstar/utils/KeyCodes';
+import { undoBatch } from '../../util/UndoManager';
import { panZoomSchema } from '../collections/collectionFreeForm/CollectionFreeFormView';
-import { DocComponent } from "../DocComponent";
-import { InkingControl } from "../InkingControl";
+import { ContextMenu } from '../ContextMenu';
+import { ContextMenuProps } from '../ContextMenuItem';
+import { DocAnnotatableComponent } from "../DocComponent";
import { PDFViewer } from "../pdf/PDFViewer";
-import { documentSchema } from "./DocumentView";
import { FieldView, FieldViewProps } from './FieldView';
import { pageSchema } from "./ImageBox";
import "./PDFBox.scss";
import React = require("react");
-import { undoBatch } from '../../util/UndoManager';
-import { ContextMenuProps } from '../ContextMenuItem';
-import { ContextMenu } from '../ContextMenu';
-import { Utils } from '../../../Utils';
+import { documentSchema } from '../../../new_fields/documentSchemas';
type PdfDocument = makeInterface<[typeof documentSchema, typeof panZoomSchema, typeof pageSchema]>;
const PdfDocument = makeInterface(documentSchema, panZoomSchema, pageSchema);
@observer
-export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocument) {
- public static LayoutString(fieldExt?: string) { return FieldView.LayoutString(PDFBox, "data", fieldExt); }
+export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>(PdfDocument) {
+ public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PDFBox, fieldKey); }
private _keyValue: string = "";
private _valueValue: string = "";
private _scriptValue: string = "";
private _searchString: string = "";
- private _isChildActive = false;
+ private _initialScale: number = 0; // the initial scale of the PDF when first rendered which determines whether the document will be live on startup or not. Getting bigger after startup won't make it automatically be live.
+ private _everActive = false; // has this box ever had its contents activated -- if so, stop drawing the overlay title
private _pdfViewer: PDFViewer | undefined;
- private _keyRef: React.RefObject<HTMLInputElement> = React.createRef();
- private _valueRef: React.RefObject<HTMLInputElement> = React.createRef();
- private _scriptRef: React.RefObject<HTMLInputElement> = React.createRef();
+ private _searchRef = React.createRef<HTMLInputElement>();
+ private _keyRef = React.createRef<HTMLInputElement>();
+ private _valueRef = React.createRef<HTMLInputElement>();
+ private _scriptRef = React.createRef<HTMLInputElement>();
+ private _selectReactionDisposer: IReactionDisposer | undefined;
@observable private _searching: boolean = false;
@observable private _flyout: boolean = false;
@observable private _pdf: Opt<Pdfjs.PDFDocumentProxy>;
@observable private _pageControls = false;
- @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); }
- @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplate ? this.props.DataDoc : Doc.GetProto(this.props.Document); }
+ constructor(props: any) {
+ super(props);
+ this._initialScale = this.props.ScreenToLocalTransform().Scale;
+ }
+ componentWillUnmount() {
+ this._selectReactionDisposer && this._selectReactionDisposer();
+ }
componentDidMount() {
const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField);
if (pdfUrl instanceof PdfField) {
Pdfjs.getDocument(pdfUrl.url.pathname).promise.then(pdf => runInAction(() => this._pdf = pdf));
}
+ this._selectReactionDisposer = reaction(() => this.props.isSelected(),
+ () => {
+ document.removeEventListener("keydown", this.onKeyDown);
+ this.props.isSelected() && document.addEventListener("keydown", this.onKeyDown);
+ }, { fireImmediately: true });
}
+
loaded = (nw: number, nh: number, np: number) => {
this.dataDoc.numPages = np;
- if (!this.Document.nativeWidth || !this.Document.nativeHeight || !this.Document.scrollHeight) {
- let oldaspect = (this.Document.nativeHeight || 0) / (this.Document.nativeWidth || 1);
- this.Document.nativeWidth = nw * 96 / 72;
- this.Document.nativeHeight = this.Document.nativeHeight ? nw * 96 / 72 * oldaspect : nh * 96 / 72;
- }
+ this.Document.nativeWidth = nw * 96 / 72;
+ this.Document.nativeHeight = nh * 96 / 72;
!this.Document.fitWidth && !this.Document.ignoreAspect && (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(NumCast(this.props.Document.curPage) - 1); }
+ 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); };
- public forwardPage() { this._pdfViewer!.gotoPage(NumCast(this.props.Document.curPage) + 1); }
+
+ @undoBatch
+ onKeyDown = action((e: KeyboardEvent) => {
+ if (e.key === "f" && e.ctrlKey) {
+ this._searching = true;
+ setTimeout(() => this._searchRef.current && this._searchRef.current.focus(), 100);
+ e.stopImmediatePropagation();
+ e.preventDefault();
+ }
+ if (e.key === "PageDown" || e.key === "ArrowDown" || e.key === "ArrowRight") this.forwardPage();
+ if (e.key === "PageUp" || e.key === "ArrowUp" || e.key === "ArrowLeft") this.backPage();
+ });
@undoBatch
@action
@@ -91,37 +112,34 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
private newScriptChange = (e: React.ChangeEvent<HTMLInputElement>) => this._scriptValue = e.currentTarget.value;
whenActiveChanged = (isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive);
- active = () => this.props.isSelected() || this._isChildActive || this.props.renderDepth === 0;
setPdfViewer = (pdfViewer: PDFViewer) => { this._pdfViewer = pdfViewer; };
searchStringChanged = (e: React.ChangeEvent<HTMLInputElement>) => this._searchString = e.currentTarget.value;
settingsPanel() {
let pageBtns = <>
<button className="pdfBox-overlayButton-iconCont" key="back" title="Page Back"
- onPointerDown={(e) => e.stopPropagation()}
- onClick={() => this.backPage()}
+ onPointerDown={e => e.stopPropagation()} onClick={this.backPage}
style={{ left: 50, top: 5, height: "30px", position: "absolute", pointerEvents: "all" }}>
<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={() => this.forwardPage()}
+ onPointerDown={e => e.stopPropagation()} onClick={this.forwardPage}
style={{ left: 80, top: 5, height: "30px", position: "absolute", pointerEvents: "all" }}>
<FontAwesomeIcon style={{ color: "white" }} icon={"arrow-right"} size="sm" />
</button>
</>;
- return !this.props.active() ? (null) :
+ return !this.active() ? (null) :
(<div className="pdfBox-ui" onKeyDown={e => e.keyCode === KeyCodes.BACKSPACE || e.keyCode === KeyCodes.DELETE ? e.stopPropagation() : true}
onPointerDown={e => e.stopPropagation()} style={{ display: this.active() ? "flex" : "none", position: "absolute", width: "100%", height: "100%", zIndex: 1, pointerEvents: "none" }}>
<div className="pdfBox-overlayCont" key="cont" onPointerDown={(e) => e.stopPropagation()} style={{ left: `${this._searching ? 0 : 100}%` }}>
<button className="pdfBox-overlayButton" title="Open Search Bar" />
- <input className="pdfBox-searchBar" placeholder="Search" onChange={this.searchStringChanged} onKeyDown={e => e.keyCode === KeyCodes.ENTER && this.search(this._searchString, !e.shiftKey)} />
+ <input className="pdfBox-searchBar" placeholder="Search" ref={this._searchRef} onChange={this.searchStringChanged} onKeyDown={e => e.keyCode === KeyCodes.ENTER && this.search(this._searchString, !e.shiftKey)} />
<button title="Search" onClick={e => this.search(this._searchString, !e.shiftKey)}>
<FontAwesomeIcon icon="search" size="sm" color="white" /></button>
- <button className="pdfBox-prevIcon " title="Previous Annotation" onClick={e => this.prevAnnotation()} >
+ <button className="pdfBox-prevIcon " title="Previous Annotation" onClick={this.prevAnnotation} >
<FontAwesomeIcon style={{ color: "white" }} icon={"arrow-up"} size="sm" />
</button>
- <button className="pdfBox-nextIcon" title="Next Annotation" onClick={e => this.nextAnnotation()} >
+ <button className="pdfBox-nextIcon" title="Next Annotation" onClick={this.nextAnnotation} >
<FontAwesomeIcon style={{ color: "white" }} icon={"arrow-down"} size="sm" />
</button>
</div>
@@ -130,7 +148,7 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
<div className="pdfBox-overlayButton-iconCont" onPointerDown={(e) => e.stopPropagation()}>
<FontAwesomeIcon style={{ color: "white", padding: 5 }} icon={this._searching ? "times" : "search"} size="3x" /></div>
</button>
- <input value={`${NumCast(this.props.Document.curPage)}`}
+ <input value={`${(this.Document.curPage || 1)}`}
onChange={e => this.gotoPage(Number(e.currentTarget.value))}
style={{ left: 20, top: 5, height: "30px", width: "30px", position: "absolute", pointerEvents: "all" }}
onClick={action(() => this._pageControls = !this._pageControls)} />
@@ -177,27 +195,46 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
ContextMenu.Instance.addItem({ description: "Pdf Funcs...", subitems: funcs, icon: "asterisk" });
}
- render() {
+ @computed get renderTitleBox() {
+ let classname = "pdfBox-cont" + (this.active() ? "-interactive" : "");
+ return <div className="pdfBox-title-outer" >
+ <div className={classname} >
+ <strong className="pdfBox-title" >{` ${this.props.Document.title}`}</strong>
+ </div>
+ </div>;
+ }
+
+ @computed get renderPdfView() {
+ trace();
const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField);
- let classname = "pdfBox-cont" + (InkingControl.Instance.selectedTool || !this.active ? "" : "-interactive");
- return (!(pdfUrl instanceof PdfField) || !this._pdf ?
- <div>{`pdf, ${this.dataDoc[this.props.fieldKey]}, not found`}</div> :
- <div className={classname} onContextMenu={this.specificContextMenu} onPointerDown={(e: React.PointerEvent) => {
- let hit = document.elementFromPoint(e.clientX, e.clientY);
- if (hit && hit.localName === "span" && this.props.isSelected()) { // drag selecting text stops propagation
- e.button === 0 && e.stopPropagation();
- }
- }}>
- <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}
- Document={this.props.Document} DataDoc={this.dataDoc} ContentScaling={this.props.ContentScaling}
- addDocTab={this.props.addDocTab} GoToPage={this.gotoPage}
- pinToPres={this.props.pinToPres} addDocument={this.props.addDocument}
- ScreenToLocalTransform={this.props.ScreenToLocalTransform} select={this.props.select}
- isSelected={this.props.isSelected} whenActiveChanged={this.whenActiveChanged}
- fieldKey={this.props.fieldKey} fieldExtensionDoc={this.extensionDoc} />
- {this.settingsPanel()}
- </div>);
+ let classname = "pdfBox-cont" + (this.active() ? "-interactive" : "");
+ return <div className={classname} style={{
+ width: this.props.Document.fitWidth ? `${100 / this.props.ContentScaling()}%` : undefined,
+ height: this.props.Document.fitWidth ? `${100 / this.props.ContentScaling()}%` : undefined,
+ transform: `scale(${this.props.Document.fitWidth ? this.props.ContentScaling() : 1})`
+ }} onContextMenu={this.specificContextMenu} onPointerDown={e => {
+ let hit = document.elementFromPoint(e.clientX, e.clientY);
+ if (hit && hit.localName === "span" && this.props.isSelected()) { // drag selecting text stops propagation
+ e.button === 0 && e.stopPropagation();
+ }
+ }}>
+ <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}
+ Document={this.props.Document} DataDoc={this.dataDoc} ContentScaling={this.props.ContentScaling}
+ addDocTab={this.props.addDocTab} focus={this.props.focus}
+ pinToPres={this.props.pinToPres} addDocument={this.addDocument}
+ ScreenToLocalTransform={this.props.ScreenToLocalTransform} select={this.props.select}
+ isSelected={this.props.isSelected} whenActiveChanged={this.whenActiveChanged}
+ fieldKey={this.props.fieldKey} startupLive={this._initialScale < 2.5 ? true : false} />
+ {this.settingsPanel()}
+ </div>;
+ }
+
+ render() {
+ const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField, null);
+ if (this.props.isSelected() || this.props.Document.scrollY !== undefined) this._everActive = true;
+ return !pdfUrl || !this._pdf || !this.extensionDoc || (!this._everActive && this.props.ScreenToLocalTransform().Scale > 2.5) ?
+ this.renderTitleBox : this.renderPdfView;
}
} \ No newline at end of file
diff --git a/src/client/views/nodes/PresBox.scss b/src/client/views/nodes/PresBox.scss
new file mode 100644
index 000000000..e5a79ab11
--- /dev/null
+++ b/src/client/views/nodes/PresBox.scss
@@ -0,0 +1,33 @@
+.presBox-cont {
+ position: absolute;
+ z-index: 2;
+ box-shadow: #AAAAAA .2vw .2vw .4vw;
+ right: 0;
+ top: 0;
+ bottom: 0;
+ width: 100%;
+ min-width: 200px;
+ height: 100%;
+ min-height: 50px;
+ 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%;
+ .presBox-button {
+ margin-right: 2.5%;
+ margin-left: 2.5%;
+ width: 20%;
+ border-radius: 5px;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx
index 5afd85430..cbb83b511 100644
--- a/src/client/views/nodes/PresBox.tsx
+++ b/src/client/views/nodes/PresBox.tsx
@@ -2,21 +2,23 @@ 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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, computed, observable, runInAction } from "mobx";
+import { action, computed, reaction, IReactionDisposer } from "mobx";
import { observer } from "mobx-react";
import { Doc, DocListCast, DocListCastAsync } from "../../../new_fields/Doc";
-import { Id } from "../../../new_fields/FieldSymbols";
-import { List } from "../../../new_fields/List";
import { listSpec } from "../../../new_fields/Schema";
-import { BoolCast, Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types";
-import { Utils } from "../../../Utils";
+import { Cast, FieldValue, NumCast } from "../../../new_fields/Types";
+import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
import { DocumentManager } from "../../util/DocumentManager";
import { undoBatch } from "../../util/UndoManager";
-import PresentationElement from "../presentationview/PresentationElement";
-import PresentationViewList from "../presentationview/PresentationList";
-import "../presentationview/PresentationView.scss";
-import { FieldView, FieldViewProps } from './FieldView';
+import { CollectionViewType } from "../collections/CollectionView";
+import { CollectionDockingView } from "../collections/CollectionDockingView";
+import { CollectionView } from "../collections/CollectionView";
import { ContextMenu } from "../ContextMenu";
+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);
@@ -27,147 +29,90 @@ library.add(faTimes);
library.add(faMinus);
library.add(faEdit);
-
-export interface PresViewProps {
- Documents: List<Doc>;
-}
-
-const expandedWidth = 450;
-
@observer
-export class PresBox extends React.Component<FieldViewProps> { //FieldViewProps?
-
-
- public static LayoutString(fieldKey?: string) { return FieldView.LayoutString(PresBox, fieldKey); }
-
- public static Instance: PresBox;
-
- //Keeping track of the doc for the current presentation -- bcz: keeping a list of current presentations shouldn't be needed. Let users create them, store them, as they see fit.
- @computed get curPresentation() { return this.props.Document; }
-
- //mapping from docs to their rendered component
- @observable presElementsMappings: Map<Doc, PresentationElement> = new Map();
- //variable that holds all the docs in the presentation
- @observable childrenDocs: Doc[] = [];
- //variable to hold if presentation is started
- @observable presStatus: boolean = false;
- //Mapping of guids to presentations.
- @observable presentationsMapping: Map<String, Doc> = new Map();
- //Mapping of presentations to guid, so that select option values can be given.
- @observable presentationsKeyMapping: Map<Doc, String> = new Map();
- //Variable to keep track of guid of the current presentation
- @observable currentSelectedPresValue: string | undefined;
- //A flag to keep track if title input is open, which is used in rendering.
- @observable PresTitleInputOpen: boolean = false;
- //Variable that holds reference to title input, so that new presentations get titles assigned.
- @observable titleInputElement: HTMLInputElement | undefined;
- @observable PresTitleChangeOpen: boolean = false;
-
- @observable opacity = 1;
- @observable persistOpacity = true;
- @observable labelOpacity = 0;
- @observable presMode = false;
-
- @observable public static CurrentPresentation: PresBox;
-
- //initilize class variables
- constructor(props: FieldViewProps) {
- super(props);
- runInAction(() => PresBox.CurrentPresentation = this);
- }
-
- @action
- toggle = (forcedValue: boolean | undefined) => {
- if (forcedValue !== undefined) {
- this.curPresentation.width = forcedValue ? expandedWidth : 0;
- } else {
- this.curPresentation.width = this.curPresentation.width === expandedWidth ? 0 : expandedWidth;
- }
+export class PresBox extends React.Component<FieldViewProps> {
+ public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PresBox, fieldKey); }
+ _docListChangedReaction: IReactionDisposer | undefined;
+ 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) {
+ let pinDoc = Docs.Create.PresElementBoxDocument({ backgroundColor: "transparent" });
+ Doc.GetProto(pinDoc).presentationTargetDoc = item;
+ Doc.GetProto(pinDoc).title = ComputedField.MakeFunction('(this.presentationTargetDoc instanceof Doc) && this.presentationTargetDoc.title.toString()');
+ value.splice(i, 1, pinDoc);
+ }
+ });
+ }
+ });
}
- //Second lifecycle function that gets called when component mounts. It makes sure toS
- //get the back-up information from previous session for the current presentation.
- async componentDidMount() {
- this.setPresentationBackUps();
+ componentWillUnmount() {
+ this._docListChangedReaction && this._docListChangedReaction();
}
+ @computed get childDocs() { return DocListCast(this.props.Document[this.props.fieldKey]); }
- /**
- * The function that retrieves the backUps for the current Presentation if present,
- * otherwise initializes.
- */
- setPresentationBackUps = async () => {
- //storing the presentation status,ie. whether it was stopped or playing
- let presStatusBackUp = BoolCast(this.curPresentation.presStatus);
- runInAction(() => this.presStatus = presStatusBackUp);
- }
-
- //observable means render is re-called every time variable is changed
- @observable
- collapsed: boolean = false;
next = async () => {
- const current = NumCast(this.curPresentation.selectedDoc);
+ const current = NumCast(this.props.Document.selectedDoc);
//asking to get document at current index
let docAtCurrentNext = await this.getDocAtIndex(current + 1);
- if (docAtCurrentNext === undefined) {
- return;
- }
- let nextSelected = current + 1;
+ if (docAtCurrentNext !== undefined) {
+ let presDocs = DocListCast(this.props.Document[this.props.fieldKey]);
+ let nextSelected = current + 1;
- let presDocs = DocListCast(this.curPresentation.data);
- for (; nextSelected < presDocs.length - 1; nextSelected++) {
- if (!this.presElementsMappings.get(presDocs[nextSelected + 1])!.props.document.groupButton) {
- break;
+ for (; nextSelected < presDocs.length - 1; nextSelected++) {
+ if (!presDocs[nextSelected + 1].groupButton) {
+ break;
+ }
}
- }
-
- this.gotoDocument(nextSelected, current);
+ this.gotoDocument(nextSelected, current);
+ }
}
back = async () => {
- const current = NumCast(this.curPresentation.selectedDoc);
+ const current = NumCast(this.props.Document.selectedDoc);
//requesting for the doc at current index
let docAtCurrent = await this.getDocAtIndex(current);
- if (docAtCurrent === undefined) {
- return;
- }
+ if (docAtCurrent !== undefined) {
- //asking for its presentation id.
- let curPresId = StrCast(docAtCurrent.presentId);
- let prevSelected = current;
- let zoomOut: boolean = false;
+ //asking for its presentation id.
+ let prevSelected = current;
+ let zoomOut: boolean = false;
- //checking if this presentation id is mapped to a group, if so chosing the first element in group
- let presDocs = DocListCast(this.curPresentation.data);
- let currentsArray: Doc[] = [];
- for (; prevSelected > 0 && presDocs[prevSelected].groupButton; prevSelected--) {
- currentsArray.push(presDocs[prevSelected]);
- }
- prevSelected = Math.max(0, prevSelected - 1);
-
- //checking if any of the group members had used zooming in
- currentsArray.forEach((doc: Doc) => {
- //let presElem: PresentationElement | undefined = this.presElementsMappings.get(doc);
- if (this.presElementsMappings.get(doc)!.props.document.showButton) {
- zoomOut = true;
- return;
+ let presDocs = await DocListCastAsync(this.props.Document[this.props.fieldKey]);
+ let currentsArray: Doc[] = [];
+ for (; presDocs && prevSelected > 0 && presDocs[prevSelected].groupButton; prevSelected--) {
+ currentsArray.push(presDocs[prevSelected]);
}
- });
+ prevSelected = Math.max(0, prevSelected - 1);
-
- // 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 || this.presElementsMappings.get(docAtCurrent)!.showButton) {
- let prevScale = NumCast(this.childrenDocs[prevSelected].viewScale, null);
- let curScale = DocumentManager.Instance.getScaleOfDocView(this.childrenDocs[current]);
- if (prevScale !== undefined && prevScale !== curScale) {
- DocumentManager.Instance.zoomIntoScale(docAtCurrent, prevScale);
+ //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) {
+ let prevScale = NumCast(this.childDocs[prevSelected].viewScale, null);
+ let curScale = DocumentManager.Instance.getScaleOfDocView(this.childDocs[current]);
+ if (prevScale !== undefined && prevScale !== curScale) {
+ DocumentManager.Instance.zoomIntoScale(docAtCurrent, prevScale);
+ }
}
}
+ this.gotoDocument(prevSelected, current);
}
- this.gotoDocument(prevSelected, current);
-
}
/**
@@ -176,22 +121,16 @@ export class PresBox extends React.Component<FieldViewProps> { //FieldViewProps?
* Hide Until Presented, Hide After Presented, Fade After Presented
*/
showAfterPresented = (index: number) => {
- this.presElementsMappings.forEach((presElem: PresentationElement, key: Doc) => {
+ this.childDocs.forEach((doc, ind) => {
//the order of cases is aligned based on priority
- if (presElem.props.document.hideTillShownButton) {
- if (this.childrenDocs.indexOf(key) <= index) {
- key.opacity = 1;
- }
+ if (doc.hideTillShownButton && ind <= index) {
+ (doc.presentationTargetDoc as Doc).opacity = 1;
}
- if (presElem.props.document.hideAfterButton) {
- if (this.childrenDocs.indexOf(key) < index) {
- key.opacity = 0;
- }
+ if (doc.hideAfterButton && ind < index) {
+ (doc.presentationTargetDoc as Doc).opacity = 0;
}
- if (presElem.props.document.fadeButton) {
- if (this.childrenDocs.indexOf(key) < index) {
- key.opacity = 0.5;
- }
+ if (doc.fadeButton && ind < index) {
+ (doc.presentationTargetDoc as Doc).opacity = 0.5;
}
});
}
@@ -202,23 +141,17 @@ export class PresBox extends React.Component<FieldViewProps> { //FieldViewProps?
* Hide Until Presented, Hide After Presented, Fade After Presented
*/
hideIfNotPresented = (index: number) => {
- this.presElementsMappings.forEach((presElem: PresentationElement, key: Doc) => {
+ this.childDocs.forEach((key, ind) => {
//the order of cases is aligned based on priority
- if (presElem.props.document.hideAfterButton) {
- if (this.childrenDocs.indexOf(key) >= index) {
- key.opacity = 1;
- }
+ if (key.hideAfterButton && ind >= index) {
+ (key.presentationTargetDoc as Doc).opacity = 1;
}
- if (presElem.props.document.fadeButton) {
- if (this.childrenDocs.indexOf(key) >= index) {
- key.opacity = 1;
- }
+ if (key.fadeButton && ind >= index) {
+ (key.presentationTargetDoc as Doc).opacity = 1;
}
- if (presElem.props.document.hideTillShownButton) {
- if (this.childrenDocs.indexOf(key) > index) {
- key.opacity = 0;
- }
+ if (key.hideTillShownButton && ind > index) {
+ (key.presentationTargetDoc as Doc).opacity = 0;
}
});
}
@@ -228,27 +161,27 @@ export class PresBox extends React.Component<FieldViewProps> { //FieldViewProps?
* has the option open and last in the group. If not in the group, and it has
* te option open, navigates to that element.
*/
- navigateToElement = async (curDoc: Doc, fromDoc: number) => {
- let docToJump: Doc = curDoc;
- let willZoom: boolean = false;
+ navigateToElement = async (curDoc: Doc, fromDocIndex: number) => {
+ let fromDoc = this.childDocs[fromDocIndex].presentationTargetDoc as Doc;
+ let docToJump = curDoc;
+ let willZoom = false;
-
- let presDocs = DocListCast(this.curPresentation.data);
+ let presDocs = DocListCast(this.props.Document[this.props.fieldKey]);
let nextSelected = presDocs.indexOf(curDoc);
let currentDocGroups: Doc[] = [];
for (; nextSelected < presDocs.length - 1; nextSelected++) {
- if (!this.presElementsMappings.get(presDocs[nextSelected + 1])!.props.document.groupButton) {
+ if (!presDocs[nextSelected + 1].groupButton) {
break;
}
currentDocGroups.push(presDocs[nextSelected]);
}
currentDocGroups.forEach((doc: Doc, index: number) => {
- if (this.presElementsMappings.get(doc)!.navButton) {
+ if (doc.navButton) {
docToJump = doc;
willZoom = false;
}
- if (this.presElementsMappings.get(doc)!.showButton) {
+ if (doc.showButton) {
docToJump = doc;
willZoom = true;
}
@@ -257,32 +190,32 @@ export class PresBox extends React.Component<FieldViewProps> { //FieldViewProps?
//docToJump stayed same meaning, it was not in the group or was the last element in the group
if (docToJump === curDoc) {
//checking if curDoc has navigation open
- if (this.presElementsMappings.get(curDoc)!.navButton) {
- DocumentManager.Instance.jumpToDocument(curDoc, false);
- } else if (this.presElementsMappings.get(curDoc)!.showButton) {
- let curScale = DocumentManager.Instance.getScaleOfDocView(this.childrenDocs[fromDoc]);
+ let target = await curDoc.presentationTargetDoc as Doc;
+ if (curDoc.navButton) {
+ DocumentManager.Instance.jumpToDocument(target, false);
+ } else if (curDoc.showButton) {
+ let curScale = DocumentManager.Instance.getScaleOfDocView(fromDoc);
//awaiting jump so that new scale can be found, since jumping is async
- await DocumentManager.Instance.jumpToDocument(curDoc, true);
- let newScale = DocumentManager.Instance.getScaleOfDocView(curDoc);
- curDoc.viewScale = newScale;
+ await DocumentManager.Instance.jumpToDocument(target, true);
+ curDoc.viewScale = DocumentManager.Instance.getScaleOfDocView(target);
//saving the scale user was on before zooming in
if (curScale !== 1) {
- this.childrenDocs[fromDoc].viewScale = curScale;
+ fromDoc.viewScale = curScale;
}
}
return;
}
- let curScale = DocumentManager.Instance.getScaleOfDocView(this.childrenDocs[fromDoc]);
+ let curScale = DocumentManager.Instance.getScaleOfDocView(fromDoc);
//awaiting jump so that new scale can be found, since jumping is async
- await DocumentManager.Instance.jumpToDocument(docToJump, willZoom);
- let newScale = DocumentManager.Instance.getScaleOfDocView(curDoc);
+ await DocumentManager.Instance.jumpToDocument(await docToJump.presentationTargetDoc as Doc, willZoom);
+ let newScale = DocumentManager.Instance.getScaleOfDocView(await curDoc.presentationTargetDoc as Doc);
curDoc.viewScale = newScale;
//saving the scale that user was on
if (curScale !== 1) {
- this.childrenDocs[fromDoc].viewScale = curScale;
+ fromDoc.viewScale = curScale;
}
}
@@ -291,45 +224,25 @@ export class PresBox extends React.Component<FieldViewProps> { //FieldViewProps?
* Async function that supposedly return the doc that is located at given index.
*/
getDocAtIndex = async (index: number) => {
- const list = FieldValue(Cast(this.curPresentation.data, listSpec(Doc)));
- if (!list) {
- return undefined;
- }
- if (index < 0 || index >= list.length) {
- return undefined;
+ 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];
}
-
- this.curPresentation.selectedDoc = index;
- //awaiting async call to finish to get Doc instance
- const doc = await list[index];
- return doc;
+ return undefined;
}
- /**
- * The function that removes a doc from a presentation. It also makes sure to
- * do necessary updates to backUps and mappings stored locally.
- */
- @action
- public RemoveDoc = async (index: number) => {
- const value = FieldValue(Cast(this.curPresentation.data, listSpec(Doc)));
- if (value) {
- let removedDoc = await value.splice(index, 1)[0];
-
- //removing the Presentation Element stored for it
- this.presElementsMappings.delete(removedDoc);
-
- }
- }
- public removeDocByRef = (doc: Doc) => {
- let indexOfDoc = this.childrenDocs.indexOf(doc);
- const value = FieldValue(Cast(this.curPresentation.data, listSpec(Doc)));
+ @undoBatch
+ public removeDocument = (doc: Doc) => {
+ const value = FieldValue(Cast(this.props.Document[this.props.fieldKey], listSpec(Doc)));
if (value) {
- value.splice(indexOfDoc, 1)[0];
- }
- //this.RemoveDoc(indexOfDoc, true);
- if (indexOfDoc !== - 1) {
- return true;
+ let indexOfDoc = value.indexOf(doc);
+ if (indexOfDoc !== - 1) {
+ value.splice(indexOfDoc, 1)[0];
+ return true;
+ }
}
return false;
}
@@ -339,189 +252,131 @@ export class PresBox extends React.Component<FieldViewProps> { //FieldViewProps?
@action
public gotoDocument = async (index: number, fromDoc: number) => {
Doc.UnBrushAllDocs();
- const list = FieldValue(Cast(this.curPresentation.data, listSpec(Doc)));
- if (!list) {
- return;
- }
- if (index < 0 || index >= list.length) {
- return;
- }
- this.curPresentation.selectedDoc = index;
+ 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 (!this.presStatus) {
- this.presStatus = true;
- this.startPresentation(index);
- }
-
- const doc = await list[index];
- if (this.presStatus) {
- this.navigateToElement(doc, fromDoc);
- this.hideIfNotPresented(index);
- this.showAfterPresented(index);
- }
- }
- //Function that sets the store of the children docs.
- @action
- setChildrenDocs = (docList: Doc[]) => {
- this.childrenDocs = docList;
- }
+ if (!this.props.Document.presStatus) {
+ this.props.Document.presStatus = true;
+ this.startPresentation(index);
+ }
- //The function that is called to render the play or pause button depending on
- //if presentation is running or not.
- renderPlayPauseButton = () => {
- if (this.presStatus) {
- return <button title="Reset Presentation" className="presentation-button" onClick={this.startOrResetPres}><FontAwesomeIcon icon="stop" /></button>;
- } else {
- return <button title="Start Presentation From Start" className="presentation-button" onClick={this.startOrResetPres}><FontAwesomeIcon icon="play" /></button>;
+ const doc = await list[index];
+ if (this.props.Document.presStatus) {
+ this.navigateToElement(doc, fromDoc);
+ this.hideIfNotPresented(index);
+ this.showAfterPresented(index);
+ }
}
}
//The function that starts or resets presentaton functionally, depending on status flag.
@action
startOrResetPres = () => {
- if (this.presStatus) {
+ if (this.props.Document.presStatus) {
this.resetPresentation();
} else {
- this.presStatus = true;
+ this.props.Document.presStatus = true;
this.startPresentation(0);
- const current = NumCast(this.curPresentation.selectedDoc);
- this.gotoDocument(0, current);
+ this.gotoDocument(0, NumCast(this.props.Document.selectedDoc));
}
- this.curPresentation.presStatus = this.presStatus;
}
//The function that resets the presentation by removing every action done by it. It also
//stops the presentaton.
@action
resetPresentation = () => {
- this.childrenDocs.forEach((doc: Doc) => {
+ this.childDocs.forEach((doc: Doc) => {
doc.opacity = 1;
doc.viewScale = 1;
});
- this.curPresentation.selectedDoc = 0;
- this.presStatus = false;
- this.curPresentation.presStatus = this.presStatus;
- if (this.childrenDocs.length === 0) {
- return;
+ this.props.Document.selectedDoc = 0;
+ this.props.Document.presStatus = false;
+ if (this.childDocs.length !== 0) {
+ DocumentManager.Instance.zoomIntoScale(this.childDocs[0], 1);
}
- DocumentManager.Instance.zoomIntoScale(this.childrenDocs[0], 1);
}
-
//The function that starts the presentation, also checking if actions should be applied
//directly at start.
startPresentation = (startIndex: number) => {
- this.presElementsMappings.forEach((component: PresentationElement, doc: Doc) => {
- if (component.props.document.hideTillShownButton) {
- if (this.childrenDocs.indexOf(doc) > startIndex) {
- doc.opacity = 0;
- }
-
+ this.childDocs.map(doc => {
+ if (doc.hideTillShownButton && this.childDocs.indexOf(doc) > startIndex) {
+ doc.opacity = 0;
}
- if (component.props.document.hideAfterButton) {
- if (this.childrenDocs.indexOf(doc) < startIndex) {
- doc.opacity = 0;
- }
+ if (doc.hideAfterButton && this.childDocs.indexOf(doc) < startIndex) {
+ doc.opacity = 0;
}
- if (component.props.document.fadeButton) {
- if (this.childrenDocs.indexOf(doc) < startIndex) {
- doc.opacity = 0.5;
- }
+ if (doc.fadeButton && this.childDocs.indexOf(doc) < startIndex) {
+ doc.opacity = 0.5;
}
-
});
-
}
-
- /**
- * The function that is called to render either select for presentations, or title inputting.
- */
- renderSelectOrPresSelection = () => {
- if (this.PresTitleInputOpen || this.PresTitleChangeOpen) {
- return <input ref={(e) => this.titleInputElement = e!} type="text" className="presentationView-title" placeholder="Enter Name!" onKeyDown={this.submitPresentationTitle} />;
+ 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 {
- return (null);
+ 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);
}
+ }));
+
+ specificContextMenu = (e: React.MouseEvent): void => {
+ ContextMenu.Instance.addItem({ description: "Make Current Presentation", event: action(() => Doc.UserDoc().curPresentation = this.props.Document), icon: "asterisk" });
}
/**
- * The function that is called on enter press of title input. It gives the
- * new presentation the title user entered. If nothing is entered, gives a default title.
+ * Initially every document starts with a viewScale 1, which means
+ * that they will be displayed in a canvas with scale 1.
*/
@action
- submitPresentationTitle = (e: React.KeyboardEvent) => {
- if (e.keyCode === 13) {
- let presTitle = this.titleInputElement!.value;
- this.titleInputElement!.value = "";
- if (this.PresTitleChangeOpen) {
- this.PresTitleChangeOpen = false;
- this.changePresentationTitle(presTitle);
+ initializeScaleViews = (docList: Doc[], viewtype: number) => {
+ this.props.Document.chromeStatus = "disabled";
+ let 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)");
+ let curScale = NumCast(doc.viewScale, null);
+ if (curScale === undefined) {
+ doc.viewScale = 1;
}
- }
- }
- /**
- * The function that is called to change title of presentation to what user entered.
- */
- @undoBatch
- changePresentationTitle = (newTitle: string) => {
- if (newTitle === "") {
- return;
- }
- this.curPresentation.title = newTitle;
- }
-
- addPressElem = (keyDoc: Doc, elem: PresentationElement) => {
- this.presElementsMappings.set(keyDoc, elem);
+ });
}
- minimize = undoBatch(action(() => {
- this.presMode = true;
- this.props.addDocTab && this.props.addDocTab(this.props.Document, this.props.DataDoc, "close");
- }));
- specificContextMenu = (e: React.MouseEvent): void => {
- ContextMenu.Instance.addItem({ description: "Make Current Presentation", event: action(() => Doc.UserDoc().curPresentation = this.props.Document), icon: "asterisk" });
+ selectElement = (doc: Doc) => {
+ let index = DocListCast(this.props.Document[this.props.fieldKey]).indexOf(doc);
+ index !== -1 && this.gotoDocument(index, NumCast(this.props.Document.selectedDoc));
}
+ getTransform = () => {
+ return this.props.ScreenToLocalTransform().translate(-10, -50);// listBox padding-left and pres-box-cont minHeight
+ }
render() {
-
- let width = "100%"; //NumCast(this.curPresentation.width)
+ this.initializeScaleViews(this.childDocs, NumCast(this.props.Document.viewType));
return (
- <div className="presentationView-cont" onPointerEnter={action(() => !this.persistOpacity && (this.opacity = 1))} onContextMenu={this.specificContextMenu}
- onPointerLeave={action(() => !this.persistOpacity && (this.opacity = 0.4))}
- style={{ width: width, opacity: this.opacity, }}>
- <div className="presentation-buttons">
- <button title="Back" className="presentation-button" onClick={this.back}><FontAwesomeIcon icon={"arrow-left"} /></button>
- {this.renderPlayPauseButton()}
- <button title="Next" className="presentation-button" onClick={this.next}><FontAwesomeIcon icon={"arrow-right"} /></button>
- <button title="Minimize" className="presentation-button" onClick={this.minimize}><FontAwesomeIcon icon={"eye"} /></button>
+ <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>
- <input
- type="checkbox"
- onChange={action((e: React.ChangeEvent<HTMLInputElement>) => {
- this.persistOpacity = e.target.checked;
- this.opacity = this.persistOpacity ? 1 : 0.4;
- })}
- checked={this.persistOpacity}
- style={{ position: "absolute", bottom: 5, left: 5 }}
- onPointerEnter={action(() => this.labelOpacity = 1)}
- onPointerLeave={action(() => this.labelOpacity = 0)}
- />
- <p style={{ position: "absolute", bottom: 1, left: 22, opacity: this.labelOpacity, transition: "0.7s opacity ease" }}>opacity {this.persistOpacity ? "persistent" : "on focus"}</p>
- <PresentationViewList
- mainDocument={this.curPresentation}
- deleteDocument={this.RemoveDoc}
- gotoDocument={this.gotoDocument}
- PresElementsMappings={this.presElementsMappings}
- setChildrenDocs={this.setChildrenDocs}
- presStatus={this.presStatus}
- removeDocByRef={this.removeDocByRef}
- clearElemMap={() => this.presElementsMappings.clear()}
- />
+ {this.props.Document.inOverlay ? (null) :
+ <div className="presBox-listCont" >
+ <CollectionView {...this.props} focus={this.selectElement} ScreenToLocalTransform={this.getTransform} />
+ </div>
+ }
</div>
);
}
-
-
} \ No newline at end of file
diff --git a/src/client/views/nodes/QueryBox.scss b/src/client/views/nodes/QueryBox.scss
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/client/views/nodes/QueryBox.scss
diff --git a/src/client/views/nodes/QueryBox.tsx b/src/client/views/nodes/QueryBox.tsx
new file mode 100644
index 000000000..99b5810fc
--- /dev/null
+++ b/src/client/views/nodes/QueryBox.tsx
@@ -0,0 +1,35 @@
+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 { IReactionDisposer } from "mobx";
+import { observer } from "mobx-react";
+import { FilterBox } from "../search/FilterBox";
+import { FieldView, FieldViewProps } from './FieldView';
+import "./PresBox.scss";
+
+library.add(faArrowLeft);
+library.add(faArrowRight);
+library.add(faPlay);
+library.add(faStop);
+library.add(faPlus);
+library.add(faTimes);
+library.add(faMinus);
+library.add(faEdit);
+
+@observer
+export class QueryBox extends React.Component<FieldViewProps> {
+ public static LayoutString(fieldKey: string) { return FieldView.LayoutString(QueryBox, fieldKey); }
+ _docListChangedReaction: IReactionDisposer | undefined;
+ componentDidMount() {
+ }
+
+ componentWillUnmount() {
+ this._docListChangedReaction && this._docListChangedReaction();
+ }
+
+ render() {
+ return <div style={{ width: "100%", height: "100%", position: "absolute", pointerEvents: "all" }}>
+ <FilterBox></FilterBox>
+ </div>;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss
index d651a8621..5829c1bd9 100644
--- a/src/client/views/nodes/VideoBox.scss
+++ b/src/client/views/nodes/VideoBox.scss
@@ -1,6 +1,15 @@
+.videoBox-container {
+ pointer-events: all;
+ .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
+ }
+}
+
.videoBox-content-YouTube, .videoBox-content-YouTube-fullScreen,
.videoBox-content, .videoBox-content-interactive, .videoBox-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;
}
.videoBox-content, .videoBox-content-interactive, .videoBox-content-fullScreen {
@@ -11,7 +20,52 @@
height: 100%;
}
-.videoBox-content-interactive, .videoBox-content-fullScreen,
-.videoBox-content-YouTube-fullScreen {
+.videoBox-content-interactive, .videoBox-content-fullScreen, .videoBox-content-YouTube-fullScreen {
pointer-events: all;
+}
+
+.videoBox-time{
+ color : white;
+ top :25px;
+ left : 25px;
+ position: absolute;
+ background-color: rgba(50, 50, 50, 0.2);
+ transform-origin: left top;
+ pointer-events:all;
+}
+.videoBox-snapshot{
+ color : white;
+ top :25px;
+ right : 25px;
+ position: absolute;
+ background-color:rgba(50, 50, 50, 0.2);
+ transform-origin: left top;
+ pointer-events:all;
+}
+.videoBox-play {
+ width: 25px;
+ height: 20px;
+ bottom: 25px;
+ left : 25px;
+ position: absolute;
+ color : white;
+ background-color: rgba(50, 50, 50, 0.2);
+ border-radius: 4px;
+ text-align: center;
+ transform-origin: left bottom;
+ pointer-events:all;
+}
+.videoBox-full {
+ width: 25px;
+ height: 20px;
+ bottom: 25px;
+ right : 25px;
+ position: absolute;
+ color : white;
+ background-color: rgba(50, 50, 50, 0.2);
+ border-radius: 4px;
+ text-align: center;
+ transform-origin: right bottom;
+ pointer-events:all;
+
} \ No newline at end of file
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 9469a2a0f..1e48000b6 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -1,49 +1,54 @@
import React = require("react");
-import { action, computed, IReactionDisposer, observable, reaction, runInAction, untracked, trace } from "mobx";
+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, reaction, runInAction, untracked } from "mobx";
import { observer } from "mobx-react";
import * as rp from 'request-promise';
+import { Doc } from "../../../new_fields/Doc";
import { InkTool } from "../../../new_fields/InkField";
-import { makeInterface } from "../../../new_fields/Schema";
-import { Cast, FieldValue, NumCast, BoolCast } from "../../../new_fields/Types";
+import { createSchema, 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 { RouteStore } from "../../../server/RouteStore";
-import { Utils } from "../../../Utils";
+import { emptyFunction, 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 { DocComponent } from "../DocComponent";
+import { DocAnnotatableComponent } from "../DocComponent";
import { DocumentDecorations } from "../DocumentDecorations";
import { InkingControl } from "../InkingControl";
-import { documentSchema } from "./DocumentView";
import { FieldView, FieldViewProps } from './FieldView';
-import { pageSchema } from "./ImageBox";
import "./VideoBox.scss";
-import { library } from "@fortawesome/fontawesome-svg-core";
-import { faVideo } from "@fortawesome/free-solid-svg-icons";
-import { Doc } from "../../../new_fields/Doc";
-import { ScriptField } from "../../../new_fields/ScriptField";
+import { documentSchema, positionSchema } from "../../../new_fields/documentSchemas";
var path = require('path');
-type VideoDocument = makeInterface<[typeof documentSchema, typeof pageSchema]>;
-const VideoDocument = makeInterface(documentSchema, pageSchema);
+export const timeSchema = createSchema({
+ currentTimecode: "number", // the current time of a video or other linear, time-based document. Note, should really get set on an extension field, but that's more complicated when it needs to be set since the extension doc needs to be found first
+});
+type VideoDocument = makeInterface<[typeof documentSchema, typeof positionSchema, typeof timeSchema]>;
+const VideoDocument = makeInterface(documentSchema, positionSchema, timeSchema);
library.add(faVideo);
@observer
-export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoDocument) {
+export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocument>(VideoDocument) {
+ static _youtubeIframeCounter: number = 0;
private _reactionDisposer?: IReactionDisposer;
private _youtubeReactionDisposer?: IReactionDisposer;
private _youtubePlayer: YT.Player | undefined = undefined;
private _videoRef: HTMLVideoElement | null = null;
private _youtubeIframeId: number = -1;
private _youtubeContentCreated = false;
- static _youtubeIframeCounter: number = 0;
+ private _isResetClick = 0;
@observable _forceCreateYouTubeIFrame = false;
- @observable static _showControls: boolean;
@observable _playTimer?: NodeJS.Timeout = undefined;
@observable _fullScreen = false;
- @observable public Playing: boolean = false;
- public static LayoutString() { return FieldView.LayoutString(VideoBox); }
+ @observable _playing = false;
+ @observable static _showControls: boolean;
+ public static LayoutString(fieldKey: string) { return FieldView.LayoutString(VideoBox, fieldKey); }
public get player(): HTMLVideoElement | null {
return this._videoRef;
@@ -51,18 +56,18 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
videoLoad = () => {
let aspect = this.player!.videoWidth / this.player!.videoHeight;
- var nativeWidth = FieldValue(this.Document.nativeWidth, 0);
- var nativeHeight = FieldValue(this.Document.nativeHeight, 0);
+ var nativeWidth = (this.Document.nativeWidth || 0);
+ var nativeHeight = (this.Document.nativeHeight || 0);
if (!nativeWidth || !nativeHeight) {
if (!this.Document.nativeWidth) this.Document.nativeWidth = this.player!.videoWidth;
- this.Document.nativeHeight = this.Document.nativeWidth / aspect;
- this.Document.height = FieldValue(this.Document.width, 0) / aspect;
+ 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 Play = (update: boolean = true) => {
- this.Playing = true;
+ this._playing = true;
update && this.player && this.player.play();
update && this._youtubePlayer && this._youtubePlayer.playVideo();
this._youtubePlayer && !this._playTimer && (this._playTimer = setInterval(this.updateTimecode, 5));
@@ -75,7 +80,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
}
@action public Pause = (update: boolean = true) => {
- this.Playing = false;
+ this._playing = false;
update && this.player && this.player.pause();
update && this._youtubePlayer && this._youtubePlayer.pauseVideo && this._youtubePlayer.pauseVideo();
this._youtubePlayer && this._playTimer && clearInterval(this._playTimer);
@@ -97,11 +102,11 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
}
@action public Snapshot() {
- let width = NumCast(this.props.Document.width);
- let height = NumCast(this.props.Document.height);
+ let width = this.Document.width || 0;
+ let height = this.Document.height || 0;
var canvas = document.createElement('canvas');
canvas.width = 640;
- canvas.height = 640 * NumCast(this.props.Document.nativeHeight) / NumCast(this.props.Document.nativeWidth);
+ canvas.height = 640 * (this.Document.nativeHeight || 0) / (this.Document.nativeWidth || 1);
var ctx = canvas.getContext('2d');//draw image to canvas. scale to target dimensions
if (ctx) {
ctx.rect(0, 0, canvas.width, canvas.height);
@@ -112,24 +117,25 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
if (!this._videoRef) { // can't find a way to take snapshots of videos
let b = Docs.Create.ButtonDocument({
- x: NumCast(this.props.Document.x) + width, y: NumCast(this.props.Document.y),
- width: 150, height: 50, title: NumCast(this.props.Document.curPage).toString()
+ x: (this.Document.x || 0) + width, y: (this.Document.y || 0),
+ width: 150, height: 50, title: (this.Document.currentTimecode || 0).toString()
});
- b.onClick = ScriptField.MakeScript(`this.curPage = ${NumCast(this.props.Document.curPage)}`);
+ b.onClick = ScriptField.MakeScript(`this.currentTimecode = ${(this.Document.currentTimecode || 0)}`);
} else {
//convert to desired file format
var dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png'
// if you want to preview the captured image,
- let filename = encodeURIComponent("snapshot" + this.props.Document.title + "_" + this.props.Document.curPage).replace(/\./g, "");
+ let filename = path.basename(encodeURIComponent("snapshot" + StrCast(this.Document.title).replace(/\..*$/, "") + "_" + (this.Document.currentTimecode || 0).toString().replace(/\./, "_")));
VideoBox.convertDataUri(dataUrl, filename).then(returnedFilename => {
if (returnedFilename) {
let url = this.choosePath(Utils.prepend(returnedFilename));
let imageSummary = Docs.Create.ImageDocument(url, {
- x: NumCast(this.props.Document.x) + width, y: NumCast(this.props.Document.y),
- width: 150, height: height / width * 150, title: "--snapshot" + NumCast(this.props.Document.curPage) + " image-"
+ x: (this.Document.x || 0) + width, y: (this.Document.y || 0),
+ width: 150, height: height / width * 150, title: "--snapshot" + (this.Document.currentTimecode || 0) + " image-"
});
- this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.addDocument && this.props.ContainingCollectionView.props.addDocument(imageSummary, false);
- DocUtils.MakeLink(imageSummary, this.props.Document);
+ 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");
}
});
}
@@ -137,8 +143,8 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
@action
updateTimecode = () => {
- this.player && (this.props.Document.curPage = this.player.currentTime);
- this._youtubePlayer && (this.props.Document.curPage = this._youtubePlayer.getCurrentTime());
+ this.player && (this.Document.currentTimecode = this.player.currentTime);
+ this._youtubePlayer && (this.Document.currentTimecode = this._youtubePlayer.getCurrentTime());
}
componentDidMount() {
@@ -146,12 +152,12 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
if (this.youtubeVideoId) {
let youtubeaspect = 400 / 315;
- var nativeWidth = FieldValue(this.Document.nativeWidth, 0);
- var nativeHeight = FieldValue(this.Document.nativeHeight, 0);
+ var nativeWidth = (this.Document.nativeWidth || 0);
+ var nativeHeight = (this.Document.nativeHeight || 0);
if (!nativeWidth || !nativeHeight) {
if (!this.Document.nativeWidth) this.Document.nativeWidth = 600;
- this.Document.nativeHeight = this.Document.nativeWidth / youtubeaspect;
- this.Document.height = FieldValue(this.Document.width, 0) / youtubeaspect;
+ this.Document.nativeHeight = (this.Document.nativeWidth || 0) / youtubeaspect;
+ this.Document.height = (this.Document.width || 0) / youtubeaspect;
}
}
@@ -170,10 +176,9 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
if (vref) {
this._videoRef!.ontimeupdate = this.updateTimecode;
vref.onfullscreenchange = action((e) => this._fullScreen = vref.webkitDisplayingFullscreen);
- if (this._reactionDisposer) this._reactionDisposer();
- this._reactionDisposer = reaction(() => this.props.Document.curPage, () =>
- !this.Playing && (vref.currentTime = this.Document.curPage || 0)
- , { fireImmediately: true });
+ this._reactionDisposer && this._reactionDisposer();
+ this._reactionDisposer = reaction(() => this.Document.currentTimecode || 0,
+ time => !this._playing && (vref.currentTime = time), { fireImmediately: true });
}
}
@@ -210,7 +215,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
let interactive = InkingControl.Instance.selectedTool || !this.props.isSelected() ? "" : "-interactive";
let style = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive;
return !field ? <div>Loading</div> :
- <video className={`${style}`} ref={this.setVideoRef} onCanPlay={this.videoLoad} controls={VideoBox._showControls}
+ <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()}>
<source src={field.url.href} type="video/mp4" />
Not supported.
@@ -238,13 +243,13 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
this.Pause();
return;
}
- if (event.data === YT.PlayerState.PLAYING && !this.Playing) this.Play(false);
- if (event.data === YT.PlayerState.PAUSED && this.Playing) this.Pause(false);
+ if (event.data === YT.PlayerState.PLAYING && !this._playing) this.Play(false);
+ if (event.data === YT.PlayerState.PAUSED && this._playing) this.Pause(false);
});
let onYoutubePlayerReady = (event: any) => {
this._reactionDisposer && this._reactionDisposer();
this._youtubeReactionDisposer && this._youtubeReactionDisposer();
- this._reactionDisposer = reaction(() => this.props.Document.curPage, () => !this.Playing && this.Seek(this.Document.curPage || 0));
+ this._reactionDisposer = reaction(() => this.Document.currentTimecode, () => !this._playing && this.Seek(this.Document.currentTimecode || 0));
this._youtubeReactionDisposer = reaction(() => [this.props.isSelected(), DocumentDecorations.Instance.Interacting, InkingControl.Instance.selectedTool], () => {
let interactive = InkingControl.Instance.selectedTool === InkTool.None && this.props.isSelected() && !DocumentDecorations.Instance.Interacting;
iframe.style.pointerEvents = interactive ? "all" : "none";
@@ -258,25 +263,104 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
});
}
+ private get uIButtons() {
+ let scaling = Math.min(1.8, this.props.ScreenToLocalTransform().Scale);
+ let curTime = (this.Document.currentTimecode || 0);
+ return ([<div className="videoBox-time" key="time" onPointerDown={this.onResetDown} style={{ transform: `scale(${scaling})` }}>
+ <span>{"" + Math.round(curTime)}</span>
+ <span style={{ fontSize: 8 }}>{" " + Math.round((curTime - Math.trunc(curTime)) * 100)}</span>
+ </div>,
+ <div className="videoBox-snapshot" key="snap" onPointerDown={this.onSnapshot} style={{ transform: `scale(${scaling})` }}>
+ <FontAwesomeIcon icon="camera" size="lg" />
+ </div>,
+ VideoBox._showControls ? (null) : [
+ <div className="videoBox-play" key="play" onPointerDown={this.onPlayDown} style={{ transform: `scale(${scaling})` }}>
+ <FontAwesomeIcon icon={this._playing ? "pause" : "play"} size="lg" />
+ </div>,
+ <div className="videoBox-full" key="full" onPointerDown={this.onFullDown} style={{ transform: `scale(${scaling})` }}>
+ F
+ </div>
+ ]]);
+ }
+
+ onPlayDown = () => this._playing ? this.Pause() : this.Play();
+
+ onFullDown = (e: React.PointerEvent) => {
+ this.FullScreen();
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+ onSnapshot = (e: React.PointerEvent) => {
+ this.Snapshot();
+ e.stopPropagation();
+ e.preventDefault();
+ }
+
+ onResetDown = (e: React.PointerEvent) => {
+ this.Pause();
+ e.stopPropagation();
+ this._isResetClick = 0;
+ document.addEventListener("pointermove", this.onResetMove, true);
+ document.addEventListener("pointerup", this.onResetUp, true);
+ }
- @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplate ? this.props.DataDoc : Doc.GetProto(this.props.Document); }
+ onResetMove = (e: PointerEvent) => {
+ this._isResetClick += Math.abs(e.movementX) + Math.abs(e.movementY);
+ this.Seek(Math.max(0, (this.Document.currentTimecode || 0) + Math.sign(e.movementX) * 0.0333));
+ e.stopImmediatePropagation();
+ }
+
+ @action
+ onResetUp = (e: PointerEvent) => {
+ document.removeEventListener("pointermove", this.onResetMove, true);
+ document.removeEventListener("pointerup", this.onResetUp, true);
+ this._isResetClick < 10 && (this.Document.currentTimecode = 0);
+ }
@computed get youtubeContent() {
this._youtubeIframeId = VideoBox._youtubeIframeCounter++;
this._youtubeContentCreated = this._forceCreateYouTubeIFrame ? true : true;
let style = "videoBox-content-YouTube" + (this._fullScreen ? "-fullScreen" : "");
- let start = untracked(() => Math.round(NumCast(this.props.Document.curPage)));
+ let start = untracked(() => Math.round(this.Document.currentTimecode || 0));
return <iframe key={this._youtubeIframeId} id={`${this.youtubeVideoId + this._youtubeIframeId}-player`}
- onLoad={this.youtubeIframeLoaded} className={`${style}`} width={NumCast(this.props.Document.nativeWidth, 640)} height={NumCast(this.props.Document.nativeHeight, 390)}
- src={`https://www.youtube.com/embed/${this.youtubeVideoId}?enablejsapi=1&rel=0&showinfo=1&autoplay=1&mute=1&start=${start}&modestbranding=1&controls=${VideoBox._showControls ? 1 : 0}`}
- ></iframe>;
+ onLoad={this.youtubeIframeLoaded} className={`${style}`} width={(this.Document.nativeWidth || 640)} height={(this.Document.nativeHeight || 390)}
+ src={`https://www.youtube.com/embed/${this.youtubeVideoId}?enablejsapi=1&rel=0&showinfo=1&autoplay=1&mute=1&start=${start}&modestbranding=1&controls=${VideoBox._showControls ? 1 : 0}`} />;
+ }
+
+ @action.bound
+ addDocumentWithTimestamp(doc: Doc): boolean {
+ var curTime = (this.Document.currentTimecode || -1);
+ curTime !== -1 && (doc.displayTimecode = curTime);
+ return this.addDocument(doc);
}
render() {
- Doc.UpdateDocumentExtensionForField(this.dataDoc, this.props.fieldKey);
- return <div style={{ pointerEvents: "all", width: "100%", height: "100%" }} onContextMenu={this.specificContextMenu}>
- {this.youtubeVideoId ? this.youtubeContent : this.content}
- </div>;
+ return (<div className={"videoBox-container"} onContextMenu={this.specificContextMenu}>
+ <CollectionFreeFormView {...this.props}
+ PanelHeight={this.props.PanelHeight}
+ PanelWidth={this.props.PanelWidth}
+ annotationsKey={this.annotationsKey}
+ focus={this.props.focus}
+ isSelected={this.props.isSelected}
+ isAnnotationOverlay={true}
+ select={emptyFunction}
+ active={this.active}
+ ContentScaling={returnOne}
+ whenActiveChanged={this.whenActiveChanged}
+ removeDocument={this.removeDocument}
+ moveDocument={this.moveDocument}
+ addDocument={this.addDocumentWithTimestamp}
+ CollectionView={undefined}
+ ScreenToLocalTransform={this.props.ScreenToLocalTransform}
+ ruleProvider={undefined}
+ renderDepth={this.props.renderDepth + 1}
+ ContainingCollectionDoc={this.props.ContainingCollectionDoc}
+ chromeCollapsed={true}>
+ {() => [this.youtubeVideoId ? this.youtubeContent : this.content]}
+ </CollectionFreeFormView>
+ {this.uIButtons}
+ </div >);
}
}
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 29eef27a0..5af743859 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -1,34 +1,36 @@
+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 { FieldResult, Doc, Field } from "../../../new_fields/Doc";
+import { Doc, FieldResult } from "../../../new_fields/Doc";
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 { Docs } from "../../documents/Documents";
+import { SelectionManager } from "../../util/SelectionManager";
+import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
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 { InkTool } from "../../../new_fields/InkField";
-import { Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types";
-import { Utils } from "../../../Utils";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { faStickyNote } from '@fortawesome/free-solid-svg-icons';
-import { observable, action, computed } from "mobx";
-import { listSpec } from "../../../new_fields/Schema";
-import { RefField } from "../../../new_fields/RefField";
-import { ObjectField } from "../../../new_fields/ObjectField";
-import { updateSourceFile } from "typescript";
-import { KeyValueBox } from "./KeyValueBox";
-import { setReactionScheduler } from "mobx/lib/internal";
-import { library } from "@fortawesome/fontawesome-svg-core";
-import { SelectionManager } from "../../util/SelectionManager";
-import { Docs } from "../../documents/Documents";
+import { DocAnnotatableComponent } from "../DocComponent";
+import { documentSchema } from "../../../new_fields/documentSchemas";
library.add(faStickyNote);
+type WebDocument = makeInterface<[typeof documentSchema]>;
+const WebDocument = makeInterface(documentSchema);
+
@observer
-export class WebBox extends React.Component<FieldViewProps> {
+export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument>(WebDocument) {
- public static LayoutString() { return FieldView.LayoutString(WebBox); }
+ public static LayoutString(fieldKey: string) { return FieldView.LayoutString(WebBox, fieldKey); }
@observable private collapsed: boolean = true;
@observable private url: string = "";
@@ -37,12 +39,12 @@ export class WebBox extends React.Component<FieldViewProps> {
let field = Cast(this.props.Document[this.props.fieldKey], WebField);
if (field && field.url.href.indexOf("youtube") !== -1) {
let youtubeaspect = 400 / 315;
- var nativeWidth = NumCast(this.props.Document.nativeWidth, 0);
- var nativeHeight = NumCast(this.props.Document.nativeHeight, 0);
+ var nativeWidth = NumCast(this.layoutDoc.nativeWidth);
+ var nativeHeight = NumCast(this.layoutDoc.nativeHeight);
if (!nativeWidth || !nativeHeight || Math.abs(nativeWidth / nativeHeight - youtubeaspect) > 0.05) {
- if (!nativeWidth) this.props.Document.nativeWidth = 600;
- this.props.Document.nativeHeight = NumCast(this.props.Document.nativeWidth) / youtubeaspect;
- this.props.Document.height = NumCast(this.props.Document.width) / youtubeaspect;
+ if (!nativeWidth) this.layoutDoc.nativeWidth = 600;
+ this.layoutDoc.nativeHeight = NumCast(this.layoutDoc.nativeWidth) / youtubeaspect;
+ this.layoutDoc.height = NumCast(this.layoutDoc.width) / youtubeaspect;
}
}
@@ -91,7 +93,7 @@ export class WebBox extends React.Component<FieldViewProps> {
});
SelectionManager.SelectedDocuments().map(dv => {
- dv.props.addDocument && dv.props.addDocument(newBox, false);
+ dv.props.addDocument && dv.props.addDocument(newBox);
dv.props.removeDocument && dv.props.removeDocument(dv.props.Document);
});
@@ -162,8 +164,10 @@ export class WebBox extends React.Component<FieldViewProps> {
e.stopPropagation();
}
}
- render() {
- let field = this.props.Document[this.props.fieldKey];
+
+ @computed
+ get content() {
+ let field = this.dataDoc[this.props.fieldKey];
let view;
if (field instanceof HtmlField) {
view = <span id="webBox-htmlSpan" dangerouslySetInnerHTML={{ __html: field.html }} />;
@@ -189,4 +193,30 @@ export class WebBox extends React.Component<FieldViewProps> {
{!frozen ? (null) : <div className="webBox-overlay" onWheel={this.onPreWheel} onPointerDown={this.onPrePointer} onPointerMove={this.onPrePointer} onPointerUp={this.onPrePointer} />}
</>);
}
+ render() {
+ return (<div className={"imageBox-container"} >
+ <CollectionFreeFormView {...this.props}
+ PanelHeight={this.props.PanelHeight}
+ PanelWidth={this.props.PanelWidth}
+ annotationsKey={this.annotationsKey}
+ focus={this.props.focus}
+ isSelected={this.props.isSelected}
+ isAnnotationOverlay={true}
+ select={emptyFunction}
+ active={this.active}
+ ContentScaling={returnOne}
+ whenActiveChanged={this.whenActiveChanged}
+ removeDocument={this.removeDocument}
+ moveDocument={this.moveDocument}
+ addDocument={this.addDocument}
+ CollectionView={undefined}
+ ScreenToLocalTransform={this.props.ScreenToLocalTransform}
+ ruleProvider={undefined}
+ renderDepth={this.props.renderDepth + 1}
+ ContainingCollectionDoc={this.props.ContainingCollectionDoc}
+ chromeCollapsed={true}>
+ {() => [this.content]}
+ </CollectionFreeFormView>
+ </div >);
+ }
} \ No newline at end of file
diff --git a/src/client/views/pdf/Annotation.scss b/src/client/views/pdf/Annotation.scss
index 0c6df74f0..cc326eb93 100644
--- a/src/client/views/pdf/Annotation.scss
+++ b/src/client/views/pdf/Annotation.scss
@@ -2,6 +2,5 @@
pointer-events: all;
user-select: none;
position: absolute;
- background-color: red;
- opacity: 0.1;
+ background-color: rgba(146, 245, 95, 0.467);
} \ No newline at end of file
diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx
index 3ed85f6a5..936af9ab8 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 } from "../../../new_fields/Doc";
+import { Doc, DocListCast, HeightSym, WidthSym, Opt, DocListCastAsync } 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,9 +11,10 @@ import "./Annotation.scss";
interface IAnnotationProps {
anno: Doc;
- fieldExtensionDoc: Doc;
+ extensionDoc: Doc;
addDocTab: (document: Doc, dataDoc: Opt<Doc>, where: string) => boolean;
pinToPres: (document: Doc) => void;
+ focus: (doc: Doc) => void;
}
export default class Annotation extends React.Component<IAnnotationProps> {
@@ -28,7 +29,7 @@ interface IRegionAnnotationProps {
y: number;
width: number;
height: number;
- fieldExtensionDoc: Doc;
+ extensionDoc: Doc;
addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean;
pinToPres: (document: Doc) => void;
document: Doc;
@@ -50,26 +51,23 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
);
this._brushDisposer = reaction(
- () => FieldValue(Cast(this.props.document.group, Doc)) && Doc.IsBrushed(FieldValue(Cast(this.props.document.group, Doc))!),
- (brushed) => {
- if (brushed !== undefined) {
- runInAction(() => this._brushed = brushed);
- }
- }
+ () => FieldValue(Cast(this.props.document.group, Doc)) && Doc.isBrushedHighlightedDegree(FieldValue(Cast(this.props.document.group, Doc))!),
+ brushed => brushed !== undefined && runInAction(() => this._brushed = brushed !== 0)
);
}
componentWillUnmount() {
+ this._brushDisposer && this._brushDisposer();
this._reactionDisposer && this._reactionDisposer();
}
deleteAnnotation = () => {
- let annotation = DocListCast(this.props.fieldExtensionDoc.annotations);
+ let annotation = DocListCast(this.props.extensionDoc.annotations);
let group = FieldValue(Cast(this.props.document.group, Doc));
if (group) {
if (annotation.indexOf(group) !== -1) {
let newAnnotations = annotation.filter(a => a !== FieldValue(Cast(this.props.document.group, Doc)));
- this.props.fieldExtensionDoc.annotations = new List<Doc>(newAnnotations);
+ this.props.extensionDoc.annotations = new List<Doc>(newAnnotations);
}
DocListCast(group.annotations).forEach(anno => anno.delete = true);
@@ -92,20 +90,20 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
PDFMenu.Instance.AddTag = this.addTag.bind(this);
PDFMenu.Instance.PinToPres = this.pinToPres;
PDFMenu.Instance.jumpTo(e.clientX, e.clientY, true);
+ e.stopPropagation();
}
else if (e.button === 0) {
- let targetDoc = await Cast(this.props.document.target, Doc);
- if (targetDoc) {
- let context = await Cast(targetDoc.targetContext, Doc);
- if (context) {
- DocumentManager.Instance.jumpToDocument(targetDoc, false, false,
- ((doc) => this.props.addDocTab(targetDoc!, undefined, e.ctrlKey ? "onRight" : "inTab")),
- undefined, undefined);
- }
+ let 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"),
+ false, false, undefined);
+ e.stopPropagation();
}
}
}
+
addTag = (key: string, value: string): boolean => {
let group = FieldValue(Cast(this.props.document.group, Doc));
if (group) {
@@ -123,7 +121,9 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
left: this.props.x,
width: this.props.width,
height: this.props.height,
- backgroundColor: this._brushed ? "green" : StrCast(this.props.document.color)
+ opacity: this._brushed ? 0.5 : undefined,
+ backgroundColor: this._brushed ? "orange" : StrCast(this.props.document.backgroundColor),
+ transition: "opacity 0.5s",
}} />);
}
} \ No newline at end of file
diff --git a/src/client/views/pdf/PDFMenu.scss b/src/client/views/pdf/PDFMenu.scss
index b06d19c53..44e075153 100644
--- a/src/client/views/pdf/PDFMenu.scss
+++ b/src/client/views/pdf/PDFMenu.scss
@@ -15,7 +15,7 @@
}
.pdfMenu-button:hover {
- background-color: #121212;
+ background-color: #d4d4d4;
}
.pdfMenu-dragger {
diff --git a/src/client/views/pdf/PDFMenu.tsx b/src/client/views/pdf/PDFMenu.tsx
index 2202351ee..517a99a68 100644
--- a/src/client/views/pdf/PDFMenu.tsx
+++ b/src/client/views/pdf/PDFMenu.tsx
@@ -4,7 +4,7 @@ import { observable, action, } from "mobx";
import { observer } from "mobx-react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { emptyFunction, returnFalse } from "../../../Utils";
-import { Doc } from "../../../new_fields/Doc";
+import { Doc, Opt } from "../../../new_fields/Doc";
@observer
export default class PDFMenu extends React.Component {
@@ -31,7 +31,7 @@ export default class PDFMenu extends React.Component {
@observable public Pinned: boolean = false;
public StartDrag: (e: PointerEvent, ele: HTMLElement) => void = emptyFunction;
- public Highlight: (d: Doc | undefined, color: string) => void = emptyFunction;
+ public Highlight: (color: string) => Opt<Doc> = (color: string) => undefined;
public Delete: () => void = emptyFunction;
public Snippet: (marquee: { left: number, top: number, width: number, height: number }) => void = emptyFunction;
public AddTag: (key: string, value: string) => boolean = returnFalse;
@@ -155,12 +155,8 @@ export default class PDFMenu extends React.Component {
@action
highlightClicked = (e: React.MouseEvent) => {
- if (!this.Pinned) {
- this.Highlight(undefined, "#f4f442");
- }
- else {
+ if (!this.Highlight("rgba(245, 230, 95, 0.616)") && this.Pinned) { // yellowish highlight color for a marker type highlight
this.Highlighting = !this.Highlighting;
- this.Highlight(undefined, "#f4f442");
}
}
diff --git a/src/client/views/pdf/PDFViewer.scss b/src/client/views/pdf/PDFViewer.scss
index 8027e93a3..f6fedf3da 100644
--- a/src/client/views/pdf/PDFViewer.scss
+++ b/src/client/views/pdf/PDFViewer.scss
@@ -11,10 +11,22 @@
// transform: scale(0.75);
// transform-origin: top left;
// }
- // .textLayer {
- // transform: scale(0.75);
- // transform-origin: top left;
- // }
+ .textLayer {
+
+ mix-blend-mode: multiply;
+ opacity: 0.9;
+ span {
+ padding-right: 5px;
+ padding-bottom: 4px;
+ }
+ }
+ .textLayer ::selection { background: yellow; } // should match the backgroundColor in createAnnotation()
+ .textLayer .highlight {
+ background-color: yellow;
+ }
+ .textLayer .highlight.selected {
+ background-color: orange;
+ }
.page {
position: relative;
@@ -30,7 +42,6 @@
}
.pdfViewer-overlay {
- transform: scale(2.14359);
transform-origin: left top;
position: absolute;
top: 0px;
@@ -43,11 +54,11 @@
top: 0;
width: 100%;
pointer-events: none;
+ mix-blend-mode: multiply;
- .pdfPage-annotationBox {
+ .pdfViewer-annotationBox {
position: absolute;
- background-color: red;
- opacity: 0.1;
+ background-color: rgba(245, 230, 95, 0.616);
}
}
.pdfViewer-waiting {
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 13fd8ea98..0cb671156 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -6,11 +6,10 @@ import { Dictionary } from "typescript-collections";
import { Doc, DocListCast, FieldResult, WidthSym, Opt, HeightSym } from "../../../new_fields/Doc";
import { Id } from "../../../new_fields/FieldSymbols";
import { List } from "../../../new_fields/List";
-import { listSpec } from "../../../new_fields/Schema";
+import { makeInterface, createSchema } from "../../../new_fields/Schema";
import { ScriptField } from "../../../new_fields/ScriptField";
import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
-import smoothScroll, { Utils, emptyFunction, returnOne } from "../../../Utils";
-import { DocServer } from "../../DocServer";
+import { smoothScroll, Utils, emptyFunction, returnOne, intersectRect, addStyleSheet, addStyleSheetRule, clearStyleSheetRules } from "../../../Utils";
import { Docs, DocUtils } from "../../documents/Documents";
import { DragManager } from "../../util/DragManager";
import { CompiledScript, CompileScript } from "../../util/Scripting";
@@ -19,39 +18,52 @@ import PDFMenu from "./PDFMenu";
import "./PDFViewer.scss";
import React = require("react");
import * as rp from "request-promise";
-import { CollectionPDFView } from "../collections/CollectionPDFView";
-import { CollectionVideoView } from "../collections/CollectionVideoView";
import { CollectionView } from "../collections/CollectionView";
import Annotation from "./Annotation";
import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
+import { SelectionManager } from "../../util/SelectionManager";
+import { undoBatch } from "../../util/UndoManager";
+import { DocAnnotatableComponent } from "../DocComponent";
+import { DocumentType } from "../../documents/DocumentTypes";
+import { documentSchema } from "../../../new_fields/documentSchemas";
const PDFJSViewer = require("pdfjs-dist/web/pdf_viewer");
const pdfjsLib = require("pdfjs-dist");
+export const pageSchema = createSchema({
+ curPage: "number",
+ fitWidth: "boolean",
+ rotation: "number",
+ scrollY: "number",
+ scrollHeight: "number",
+ search_string: "string"
+});
+
pdfjsLib.GlobalWorkerOptions.workerSrc = `/assets/pdf.worker.js`;
+type PdfDocument = makeInterface<[typeof documentSchema, typeof pageSchema]>;
+const PdfDocument = makeInterface(documentSchema, pageSchema);
interface IViewerProps {
pdf: Pdfjs.PDFDocumentProxy;
url: string;
+ fieldKey: string;
Document: Doc;
DataDoc?: Doc;
- fieldExtensionDoc: Doc;
- fieldKey: string;
- fieldExt: string;
+ ContainingCollectionView: Opt<CollectionView>;
PanelWidth: () => number;
PanelHeight: () => number;
ContentScaling: () => number;
select: (isCtrlPressed: boolean) => void;
+ startupLive: boolean;
renderDepth: number;
+ focus: (doc: Doc) => void;
isSelected: () => boolean;
loaded: (nw: number, nh: number, np: number) => void;
active: () => boolean;
- GoToPage?: (n: number) => void;
addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean;
pinToPres: (document: Doc) => void;
- addDocument?: (doc: Doc, allowDuplicates?: boolean) => boolean;
+ addDocument?: (doc: Doc) => boolean;
setPdfViewer: (view: PDFViewer) => void;
ScreenToLocalTransform: () => Transform;
- ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
whenActiveChanged: (isActive: boolean) => void;
}
@@ -59,7 +71,8 @@ interface IViewerProps {
* Handles rendering and virtualization of the pdf
*/
@observer
-export class PDFViewer extends React.Component<IViewerProps> {
+export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument>(PdfDocument) {
+ static _annotationStyle: any = addStyleSheet();
@observable private _pageSizes: { width: number, height: number }[] = [];
@observable private _annotations: Doc[] = [];
@observable private _savedAnnotations: Dictionary<number, HTMLDivElement[]> = new Dictionary<number, HTMLDivElement[]>();
@@ -73,18 +86,19 @@ export class PDFViewer extends React.Component<IViewerProps> {
@observable private _showWaiting = true;
@observable private _showCover = false;
@observable private _zoomed = 1;
+ @observable private _scrollTop = 0;
- public pdfViewer: any;
- private _isChildActive = false;
+ private _pdfViewer: any;
+ private _retries = 0; // number of times tried to create the PDF viewer
private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean) => void);
private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
private _reactionDisposer?: IReactionDisposer;
private _selectionReactionDisposer?: IReactionDisposer;
private _annotationReactionDisposer?: IReactionDisposer;
private _filterReactionDisposer?: IReactionDisposer;
+ private _searchReactionDisposer?: IReactionDisposer;
private _viewer: React.RefObject<HTMLDivElement> = React.createRef();
private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
- private _marquee: React.RefObject<HTMLDivElement> = React.createRef();
private _selectionText: string = "";
private _startX: number = 0;
private _startY: number = 0;
@@ -93,29 +107,44 @@ export class PDFViewer extends React.Component<IViewerProps> {
private _coverPath: any;
@computed get allAnnotations() {
- return DocListCast(this.props.fieldExtensionDoc.annotations).filter(
- anno => this._script.run({ this: anno }, console.log, true).result);
+ return this.extensionDoc ? DocListCast(this.extensionDoc.annotations).filter(
+ anno => this._script.run({ this: anno }, console.log, true).result) : [];
}
@computed get nonDocAnnotations() {
return this._annotations.filter(anno => this._script.run({ this: anno }, console.log, true).result);
}
+ _lastSearch: string = "";
componentDidMount = async () => {
// change the address to be the file address of the PNG version of each page
// file address of the pdf
- this._coverPath = JSON.parse(await rp.get(Utils.prepend(`/thumbnail${this.props.url.substring("files/".length, this.props.url.length - ".pdf".length)}-${NumCast(this.props.Document.curPage, 1)}.PNG`)));
+ this._coverPath = JSON.parse(await rp.get(Utils.prepend(`/thumbnail${this.props.url.substring("files/".length, this.props.url.length - ".pdf".length)}-${(this.Document.curPage || 1)}.PNG`)));
runInAction(() => this._showWaiting = this._showCover = true);
- this._selectionReactionDisposer = reaction(() => this.props.isSelected(), () => this.setupPdfJsViewer());
+ this.props.startupLive && this.setupPdfJsViewer();
+ this._searchReactionDisposer = reaction(() => this.Document.search_string, searchString => {
+ if (searchString) {
+ this.search(searchString, true);
+ this._lastSearch = searchString;
+ }
+ else {
+ setTimeout(() => this._lastSearch === "mxytzlaf" && this.search("mxytzlaf", true), 200); // bcz: how do we clear search highlights?
+ this._lastSearch && (this._lastSearch = "mxytzlaf");
+ }
+ }, { fireImmediately: true });
+
+ this._selectionReactionDisposer = reaction(() => this.props.isSelected(),
+ () => (SelectionManager.SelectedDocuments().length === 1) && this.setupPdfJsViewer(),
+ { fireImmediately: true });
this._reactionDisposer = reaction(
- () => this.props.Document.scrollY,
+ () => this.Document.scrollY,
(scrollY) => {
if (scrollY !== undefined) {
if (this._showCover || this._showWaiting) {
this.setupPdfJsViewer();
}
- this._mainCont.current && smoothScroll(1000, this._mainCont.current, NumCast(this.props.Document.scrollY) || 0);
- this.props.Document.scrollY = undefined;
+ this._mainCont.current && smoothScroll(1000, this._mainCont.current, (this.Document.scrollY || 0));
+ this.Document.scrollY = undefined;
}
},
{ fireImmediately: true }
@@ -127,28 +156,22 @@ export class PDFViewer extends React.Component<IViewerProps> {
this._annotationReactionDisposer && this._annotationReactionDisposer();
this._filterReactionDisposer && this._filterReactionDisposer();
this._selectionReactionDisposer && this._selectionReactionDisposer();
+ this._searchReactionDisposer && this._searchReactionDisposer();
document.removeEventListener("copy", this.copy);
}
copy = (e: ClipboardEvent) => {
if (this.props.active() && e.clipboardData) {
- e.clipboardData.setData("text/plain", this._selectionText);
- e.clipboardData.setData("dash/pdfOrigin", this.props.Document[Id]);
- e.clipboardData.setData("dash/pdfRegion", this.makeAnnotationDocument(undefined, "#0390fc")[Id]);
+ let annoDoc = this.makeAnnotationDocument("rgba(3,144,152,0.3)"); // copied text markup color (blueish)
+ if (annoDoc) {
+ e.clipboardData.setData("text/plain", this._selectionText);
+ e.clipboardData.setData("dash/pdfOrigin", this.props.Document[Id]);
+ e.clipboardData.setData("dash/pdfRegion", annoDoc[Id]);
+ }
e.preventDefault();
}
}
- paste = (e: ClipboardEvent) => {
- if (e.clipboardData && e.clipboardData.getData("dash/pdfOrigin") === this.props.Document[Id]) {
- let linkDocId = e.clipboardData.getData("dash/linkDoc");
- linkDocId && DocServer.GetRefField(linkDocId).then(async (link) =>
- (link instanceof Doc) && (Doc.GetProto(link).anchor2 = this.makeAnnotationDocument(await Cast(Doc.GetProto(link), Doc), "#0390fc", false)));
- }
- }
-
- setSelectionText = (text: string) => this._selectionText = text;
-
@action
initialLoad = async () => {
if (this._pageSizes.length === 0) {
@@ -162,7 +185,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
i === this.props.pdf.numPages - 1 && this.props.loaded((page.view[page.rotate === 0 || page.rotate === 180 ? 2 : 3] - page.view[page.rotate === 0 || page.rotate === 180 ? 0 : 1]),
(page.view[page.rotate === 0 || page.rotate === 180 ? 3 : 2] - page.view[page.rotate === 0 || page.rotate === 180 ? 1 : 0]), i);
}))));
- Doc.GetProto(this.props.Document).scrollHeight = this._pageSizes.reduce((size, page) => size + page.height, 0) * 96 / 72;
+ this.Document.scrollHeight = this._pageSizes.reduce((size, page) => size + page.height, 0) * 96 / 72;
}
}
@@ -175,12 +198,12 @@ export class PDFViewer extends React.Component<IViewerProps> {
await this.initialLoad();
this._annotationReactionDisposer = reaction(
- () => this.props.fieldExtensionDoc && DocListCast(this.props.fieldExtensionDoc.annotations),
- annotations => annotations && annotations.length && this.renderAnnotations(annotations, true),
+ () => this.extensionDoc && DocListCast(this.extensionDoc.annotations),
+ annotations => annotations && annotations.length && (this._annotations = annotations),
{ fireImmediately: true });
this._filterReactionDisposer = reaction(
- () => ({ scriptField: Cast(this.props.Document.filterScript, ScriptField), annos: this._annotations.slice() }),
+ () => ({ scriptField: Cast(this.Document.filterScript, ScriptField), annos: this._annotations.slice() }),
action(({ scriptField, annos }: { scriptField: FieldResult<ScriptField>, annos: Doc[] }) => {
let oldScript = this._script.originalScript;
this._script = scriptField && scriptField.script.compiled ? scriptField.script : CompileScript("return true") as CompiledScript;
@@ -192,50 +215,60 @@ export class PDFViewer extends React.Component<IViewerProps> {
{ fireImmediately: true }
);
+ this.createPdfViewer();
+ }
+
+ createPdfViewer() {
+ if (!this._mainCont.current) { // bcz: I don't think this is ever triggered or needed
+ if (this._retries < 5) {
+ this._retries++;
+ setTimeout(() => this.createPdfViewer(), 1000);
+ }
+ return;
+ }
document.removeEventListener("copy", this.copy);
document.addEventListener("copy", this.copy);
document.addEventListener("pagesinit", action(() => {
- this.pdfViewer.currentScaleValue = this._zoomed = 1;
- this.gotoPage(NumCast(this.props.Document.curPage, 1));
+ this._pdfViewer.currentScaleValue = this._zoomed = 1;
+ this.gotoPage(this.Document.curPage || 1);
}));
document.addEventListener("pagerendered", action(() => this._showCover = this._showWaiting = false));
var pdfLinkService = new PDFJSViewer.PDFLinkService();
- let pdfFindController = new PDFJSViewer.PDFFindController({
- linkService: pdfLinkService,
- });
- this.pdfViewer = new PDFJSViewer.PDFViewer({
+ let pdfFindController = new PDFJSViewer.PDFFindController({ linkService: pdfLinkService });
+ this._pdfViewer = new PDFJSViewer.PDFViewer({
container: this._mainCont.current,
viewer: this._viewer.current,
linkService: pdfLinkService,
findController: pdfFindController,
renderer: "canvas",
});
- pdfLinkService.setViewer(this.pdfViewer);
+ pdfLinkService.setViewer(this._pdfViewer);
pdfLinkService.setDocument(this.props.pdf, null);
- this.pdfViewer.setDocument(this.props.pdf);
+ this._pdfViewer.setDocument(this.props.pdf);
}
+ @undoBatch
@action
- makeAnnotationDocument = (sourceDoc: Doc | undefined, color: string, createLink: boolean = true): Doc => {
+ makeAnnotationDocument = (color: string): Opt<Doc> => {
+ if (this._savedAnnotations.size() === 0) return undefined;
let mainAnnoDoc = Docs.Create.InstanceFromProto(new Doc(), "", {});
let mainAnnoDocProto = Doc.GetProto(mainAnnoDoc);
let annoDocs: Doc[] = [];
+ let maxX = -Number.MAX_VALUE;
let minY = Number.MAX_VALUE;
- if (this._savedAnnotations.size() === 1 && this._savedAnnotations.values()[0].length === 1 && !createLink) {
+ if ((this._savedAnnotations.values()[0][0] as any).marqueeing) {
let anno = this._savedAnnotations.values()[0][0];
- let annoDoc = Docs.Create.FreeformDocument([], { backgroundColor: "rgba(255, 0, 0, 0.1)", title: "Annotation on " + StrCast(this.props.Document.title) });
+ let annoDoc = Docs.Create.FreeformDocument([], { backgroundColor: color, 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);
if (anno.style.width) annoDoc.width = parseInt(anno.style.width);
- annoDoc.target = sourceDoc;
annoDoc.group = mainAnnoDoc;
- annoDoc.color = color;
- annoDoc.type = AnnotationTypes.Region;
- annoDocs.push(annoDoc);
annoDoc.isButton = true;
+ annoDocs.push(annoDoc);
anno.remove();
mainAnnoDoc = annoDoc;
+ mainAnnoDocProto.type = DocumentType.COL;
mainAnnoDocProto = Doc.GetProto(mainAnnoDoc);
mainAnnoDocProto.y = annoDoc.y;
} else {
@@ -245,39 +278,25 @@ export class PDFViewer extends React.Component<IViewerProps> {
if (anno.style.top) annoDoc.y = parseInt(anno.style.top);
if (anno.style.height) annoDoc.height = parseInt(anno.style.height);
if (anno.style.width) annoDoc.width = parseInt(anno.style.width);
- annoDoc.target = sourceDoc;
annoDoc.group = mainAnnoDoc;
- annoDoc.color = color;
- annoDoc.type = AnnotationTypes.Region;
+ annoDoc.backgroundColor = color;
annoDocs.push(annoDoc);
anno.remove();
(annoDoc.y !== undefined) && (minY = Math.min(NumCast(annoDoc.y), minY));
+ (annoDoc.x !== undefined) && (maxX = Math.max(NumCast(annoDoc.x) + NumCast(annoDoc.width), maxX));
}));
mainAnnoDocProto.y = Math.max(minY, 0);
+ mainAnnoDocProto.x = Math.max(maxX, 0);
+ mainAnnoDocProto.type = DocumentType.PDFANNO;
mainAnnoDocProto.annotations = new List<Doc>(annoDocs);
}
- mainAnnoDocProto.title = "Annotation on " + StrCast(this.props.Document.title);
+ mainAnnoDocProto.title = "Annotation on " + this.Document.title;
mainAnnoDocProto.annotationOn = this.props.Document;
- if (sourceDoc && createLink) {
- DocUtils.MakeLink(sourceDoc, mainAnnoDocProto, undefined, `Annotation from ${StrCast(this.props.Document.title)}`);
- }
this._savedAnnotations.clear();
this.Index = -1;
return mainAnnoDoc;
}
-
- @action
- renderAnnotations = (annotations: Doc[], removeOldAnnotations: boolean): void => {
- if (removeOldAnnotations) {
- this._annotations = annotations;
- }
- else {
- this._annotations.push(...annotations);
- this._annotations = new Array<Doc>(...this._annotations);
- }
- }
-
@action
prevAnnotation = () => {
this.Index = Math.max(this.Index - 1, 0);
@@ -292,36 +311,23 @@ export class PDFViewer extends React.Component<IViewerProps> {
@action
gotoPage = (p: number) => {
- this.pdfViewer && this.pdfViewer.scrollPageIntoView({ pageNumber: Math.min(Math.max(1, p), this._pageSizes.length) });
+ this._pdfViewer && this._pdfViewer.scrollPageIntoView({ pageNumber: Math.min(Math.max(1, p), this._pageSizes.length) });
}
@action
scrollToAnnotation = (scrollToAnnotation: Doc) => {
- this.allAnnotations.forEach(d => Doc.UnBrushDoc(d));
- let windowHgt = this.props.PanelHeight() / this.props.ContentScaling();
- let scrollRange = this._mainCont.current!.scrollHeight - windowHgt;
- let pgScroll = scrollRange / this._pageSizes.length;
- this._mainCont.current!.scrollTo(0, NumCast(scrollToAnnotation.y) - pgScroll / 2);
- Doc.BrushDoc(scrollToAnnotation);
- }
-
- sendAnnotations = (page: number) => {
- return this._savedAnnotations.getValue(page);
- }
-
- receiveAnnotations = (annotations: HTMLDivElement[], page: number) => {
- if (page === -1) {
- this._savedAnnotations.values().forEach(v => v.forEach(a => a.remove()));
- this._savedAnnotations.keys().forEach(k => this._savedAnnotations.setValue(k, annotations));
- }
- else {
- this._savedAnnotations.setValue(page, annotations);
+ if (scrollToAnnotation) {
+ let offset = this.visibleHeight() / 2 * 96 / 72;
+ this._mainCont.current && smoothScroll(500, this._mainCont.current, NumCast(scrollToAnnotation.y) - offset);
+ Doc.linkFollowHighlight(scrollToAnnotation);
}
}
+
@action
onScroll = (e: React.UIEvent<HTMLElement>) => {
- this.pdfViewer && (this.props.Document.curPage = this.pdfViewer.currentPageNumber);
+ this._scrollTop = this._mainCont.current!.scrollTop;
+ this._pdfViewer && (this.Document.curPage = this._pdfViewer.currentPageNumber);
}
// get the page index that the vertical offset passed in is on
@@ -341,6 +347,8 @@ export class PDFViewer extends React.Component<IViewerProps> {
div.style.top = (parseInt(div.style.top)/*+ this.getScrollFromPage(page)*/).toString();
}
this._annotationLayer.current.append(div);
+ div.style.backgroundColor = "yellow";
+ div.style.opacity = "0.5";
let savedPage = this._savedAnnotations.getValue(page);
if (savedPage) {
savedPage.push(div);
@@ -357,8 +365,8 @@ export class PDFViewer extends React.Component<IViewerProps> {
if (!searchString) {
fwd ? this.nextAnnotation() : this.prevAnnotation();
}
- else if (this.pdfViewer._pageViewsReady) {
- this.pdfViewer.findController.executeCommand('findagain', {
+ else if (this._pdfViewer._pageViewsReady) {
+ this._pdfViewer.findController.executeCommand('findagain', {
caseSensitive: false,
findPrevious: !fwd,
highlightAll: true,
@@ -368,7 +376,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
}
else if (this._mainCont.current) {
let executeFind = () => {
- this.pdfViewer.findController.executeCommand('find', {
+ this._pdfViewer.findController.executeCommand('find', {
caseSensitive: false,
findPrevious: !fwd,
highlightAll: true,
@@ -386,36 +394,31 @@ export class PDFViewer extends React.Component<IViewerProps> {
// if alt+left click, drag and annotate
this._downX = e.clientX;
this._downY = e.clientY;
- if (NumCast(this.props.Document.scale, 1) !== 1) return;
+ addStyleSheetRule(PDFViewer._annotationStyle, "pdfAnnotation", { "pointer-events": "none" });
+ if ((this.Document.scale || 1) !== 1) return;
if ((e.button !== 0 || e.altKey) && this.active()) {
this._setPreviewCursor && this._setPreviewCursor(e.clientX, e.clientY, true);
}
this._marqueeing = false;
if (!e.altKey && e.button === 0 && this.active()) {
+ // clear out old marquees and initialize menu for new selection
PDFMenu.Instance.StartDrag = this.startDrag;
PDFMenu.Instance.Highlight = this.highlight;
PDFMenu.Instance.Snippet = this.createSnippet;
PDFMenu.Instance.Status = "pdf";
PDFMenu.Instance.fadeOut(true);
+ this._savedAnnotations.values().forEach(v => v.forEach(a => a.remove()));
+ this._savedAnnotations.keys().forEach(k => this._savedAnnotations.setValue(k, []));
if (e.target && (e.target as any).parentElement.className === "textLayer") {
- if (!e.ctrlKey) {
- this.receiveAnnotations([], -1);
- }
+ // start selecting text if mouse down on textLayer spans
}
- else {
+ else if (this._mainCont.current) {
// set marquee x and y positions to the spatially transformed position
- if (this._mainCont.current) {
- let boundingRect = this._mainCont.current.getBoundingClientRect();
- this._startX = this._marqueeX = (e.clientX - boundingRect.left) * (this._mainCont.current.offsetWidth / boundingRect.width);
- this._startY = this._marqueeY = (e.clientY - boundingRect.top) * (this._mainCont.current.offsetHeight / boundingRect.height) + this._mainCont.current.scrollTop;
- }
+ let boundingRect = this._mainCont.current.getBoundingClientRect();
+ this._startX = this._marqueeX = (e.clientX - boundingRect.left) * (this._mainCont.current.offsetWidth / boundingRect.width);
+ this._startY = this._marqueeY = (e.clientY - boundingRect.top) * (this._mainCont.current.offsetHeight / boundingRect.height) + this._mainCont.current.scrollTop;
+ this._marqueeHeight = this._marqueeWidth = 0;
this._marqueeing = true;
- let marquees = this._mainCont.current!.getElementsByClassName("pdfViewer-dragAnnotationBox");
- if (marquees && marquees.length) { // make a copy of the marquee
- let marquee = marquees[0] as HTMLDivElement;
- marquee.style.opacity = "0.2";
- }
- this.receiveAnnotations([], -1);
}
document.removeEventListener("pointermove", this.onSelectMove);
document.addEventListener("pointermove", this.onSelectMove);
@@ -450,12 +453,13 @@ export class PDFViewer extends React.Component<IViewerProps> {
let clientRects = selRange.getClientRects();
for (let i = 0; i < clientRects.length; i++) {
let rect = clientRects.item(i);
- if (rect/* && rect.width !== this._mainCont.current.getBoundingClientRect().width && rect.height !== this._mainCont.current.getBoundingClientRect().height / this.props.pdf.numPages*/) {
+ if (rect) {
let scaleY = this._mainCont.current.offsetHeight / boundingRect.height;
let scaleX = this._mainCont.current.offsetWidth / boundingRect.width;
- if (rect.width !== this._mainCont.current.clientWidth) {
+ if (rect.width !== this._mainCont.current.clientWidth &&
+ (i === 0 || !intersectRect(clientRects[i], clientRects[i - 1]))) {
let annoBox = document.createElement("div");
- annoBox.className = "pdfPage-annotationBox";
+ annoBox.className = "pdfViewer-annotationBox";
// transforms the positions from screen onto the pdf div
annoBox.style.top = ((rect.top - boundingRect.top) * scaleY + this._mainCont.current.scrollTop).toString();
annoBox.style.left = ((rect.left - boundingRect.left) * scaleX).toString();
@@ -466,8 +470,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
}
}
}
- let text = selRange.cloneContents().textContent;
- text && this.setSelectionText(text);
+ this._selectionText = selRange.cloneContents().textContent || "";
// clear selection
if (sel.empty) { // Chrome
@@ -479,22 +482,23 @@ export class PDFViewer extends React.Component<IViewerProps> {
@action
onSelectEnd = (e: PointerEvent): void => {
+ clearStyleSheetRules(PDFViewer._annotationStyle);
+ this._savedAnnotations.clear();
if (this._marqueeing) {
if (this._marqueeWidth > 10 || this._marqueeHeight > 10) {
let marquees = this._mainCont.current!.getElementsByClassName("pdfViewer-dragAnnotationBox");
- if (marquees && marquees.length) { // make a copy of the marquee
+ if (marquees && marquees.length) { // copy the marquee and convert it to a permanent annotation.
+ let style = (marquees[0] as HTMLDivElement).style;
let copy = document.createElement("div");
- let marquee = marquees[0] as HTMLDivElement;
- let style = marquee.style;
copy.style.left = style.left;
copy.style.top = style.top;
copy.style.width = style.width;
copy.style.height = style.height;
copy.style.border = style.border;
copy.style.opacity = style.opacity;
- copy.className = "pdfPage-annotationBox";
+ (copy as any).marqueeing = true;
+ copy.className = "pdfViewer-annotationBox";
this.createAnnotation(copy, this.getPageFromScroll(this._marqueeY));
- marquee.style.opacity = "0";
}
if (!e.ctrlKey) {
@@ -503,8 +507,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
}
PDFMenu.Instance.jumpTo(e.clientX, e.clientY);
}
-
- this._marqueeHeight = this._marqueeWidth = 0;
+ this._marqueeing = false;
}
else {
let sel = window.getSelection();
@@ -515,8 +518,8 @@ export class PDFViewer extends React.Component<IViewerProps> {
}
}
- if (PDFMenu.Instance.Highlighting) {
- this.highlight(undefined, "goldenrod");
+ if (PDFMenu.Instance.Highlighting) {// when highlighter has been toggled when menu is pinned, we auto-highlight immediately on mouse up
+ this.highlight("rgba(245, 230, 95, 0.616)"); // yellowish highlight color for highlighted text (should match PDFMenu's highlight color)
}
else {
PDFMenu.Instance.StartDrag = this.startDrag;
@@ -527,10 +530,10 @@ export class PDFViewer extends React.Component<IViewerProps> {
}
@action
- highlight = (targetDoc: Doc | undefined, color: string) => {
+ highlight = (color: string) => {
// creates annotation documents for current highlights
- let annotationDoc = this.makeAnnotationDocument(targetDoc, color, false);
- Doc.AddDocToList(this.props.fieldExtensionDoc, this.props.fieldExt, annotationDoc);
+ let annotationDoc = this.makeAnnotationDocument(color);
+ annotationDoc && this.props.addDocument && this.props.addDocument(annotationDoc);
return annotationDoc;
}
@@ -542,23 +545,19 @@ export class PDFViewer extends React.Component<IViewerProps> {
startDrag = (e: PointerEvent, ele: HTMLElement): void => {
e.preventDefault();
e.stopPropagation();
- let targetDoc = Docs.Create.TextDocument({ width: 200, height: 200, title: "New Annotation" });
- targetDoc.targetPage = this.getPageFromScroll(this._marqueeY);
- let annotationDoc = this.highlight(undefined, "red");
- annotationDoc.linkedToDoc = false;
- let dragData = new DragManager.AnnotationDragData(this.props.Document, annotationDoc, targetDoc);
- DragManager.StartAnnotationDrag([ele], dragData, e.pageX, e.pageY, {
- handlers: {
- dragComplete: () => {
- if (!annotationDoc.linkedToDoc) {
- let annotations = DocListCast(annotationDoc.annotations);
- annotations && annotations.forEach(anno => anno.target = targetDoc);
- DocUtils.MakeLink(annotationDoc, targetDoc, dragData.targetContext, `Annotation from ${StrCast(this.props.Document.title)}`);
- }
- }
- },
- hideSource: false
- });
+ let targetDoc = Docs.Create.TextDocument({ width: 200, height: 200, title: "Note linked to " + this.props.Document.title });
+ const annotationDoc = this.highlight("rgba(146, 245, 95, 0.467)"); // yellowish highlight color when dragging out a text selection
+ if (annotationDoc) {
+ let dragData = new DragManager.AnnotationDragData(this.props.Document, annotationDoc, targetDoc);
+ DragManager.StartAnnotationDrag([ele], dragData, e.pageX, e.pageY, {
+ handlers: {
+ dragComplete: () => !(dragData as any).linkedToDoc &&
+ DocUtils.MakeLink({ doc: annotationDoc }, { doc: dragData.dropDocument, ctx: dragData.targetContext }, `Annotation from ${this.Document.title}`, "link from PDF")
+
+ },
+ hideSource: false
+ });
+ }
}
createSnippet = (marquee: { left: number, top: number, width: number, height: number }): void => {
@@ -567,42 +566,15 @@ export class PDFViewer extends React.Component<IViewerProps> {
data.title = StrCast(data.title) + "_snippet";
view.proto = data;
view.nativeHeight = marquee.height;
- view.height = (this.props.Document[WidthSym]() / NumCast(this.props.Document.nativeWidth)) * marquee.height;
- view.nativeWidth = this.props.Document.nativeWidth;
+ view.height = (this.Document[WidthSym]() / (this.Document.nativeWidth || 1)) * marquee.height;
+ view.nativeWidth = this.Document.nativeWidth;
view.startY = marquee.top;
- view.width = this.props.Document[WidthSym]();
+ view.width = this.Document[WidthSym]();
DragManager.StartDocumentDrag([], new DragManager.DocumentDragData([view]), 0, 0);
}
- // this is called with the document that was dragged and the collection to move it into.
- // if the target collection is the same as this collection, then the move will be allowed.
- // otherwise, the document being moved must be able to be removed from its container before
- // moving it into the target.
- @action.bound
- moveDocument(doc: Doc, targetCollection: Doc, addDocument: (doc: Doc) => boolean): boolean {
- if (Doc.AreProtosEqual(this.props.Document, targetCollection)) {
- return true;
- }
- return this.removeDocument(doc) ? addDocument(doc) : false;
- }
-
-
- @action.bound
- removeDocument(doc: Doc): boolean {
- //TODO This won't create the field if it doesn't already exist
- let targetDataDoc = this.props.fieldExtensionDoc;
- let targetField = this.props.fieldExt;
- let value = Cast(targetDataDoc[targetField], listSpec(Doc), []);
- 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);
- index !== -1 && value.splice(index, 1);
- return true;
- }
scrollXf = () => {
- return this._mainCont.current ? this.props.ScreenToLocalTransform().translate(0, this._mainCont.current.scrollTop) : this.props.ScreenToLocalTransform();
- }
- setPreviewCursor = (func?: (x: number, y: number, drag: boolean) => void) => {
- this._setPreviewCursor = func;
+ return this._mainCont.current ? this.props.ScreenToLocalTransform().translate(0, this._scrollTop) : this.props.ScreenToLocalTransform();
}
onClick = (e: React.MouseEvent) => {
this._setPreviewCursor &&
@@ -611,42 +583,63 @@ export class PDFViewer extends React.Component<IViewerProps> {
Math.abs(e.clientY - this._downY) < 3 &&
this._setPreviewCursor(e.clientX, e.clientY, false);
}
- whenActiveChanged = (isActive: boolean) => {
- this._isChildActive = isActive;
- this.props.whenActiveChanged(isActive);
- }
- active = () => {
- return this.props.isSelected() || this._isChildActive || this.props.renderDepth === 0;
- }
+
+ setPreviewCursor = (func?: (x: number, y: number, drag: boolean) => void) => this._setPreviewCursor = func;
+
getCoverImage = () => {
- if (!this.props.Document[HeightSym]()) {
- setTimeout(() => {
- this.props.Document.height = this.props.Document[WidthSym]() * this._coverPath.height / this._coverPath.width;
- this.props.Document.nativeHeight = nativeWidth * this._coverPath.height / this._coverPath.width;
- }, 0);
+ if (!this.props.Document[HeightSym]() || !this.props.Document.nativeHeight) {
+ setTimeout((() => {
+ this.Document.height = this.Document[WidthSym]() * this._coverPath.height / this._coverPath.width;
+ this.Document.nativeHeight = nativeWidth * this._coverPath.height / this._coverPath.width;
+ }).bind(this), 0);
}
- let nativeWidth = NumCast(this.props.Document.nativeWidth);
- let nativeHeight = NumCast(this.props.Document.nativeHeight);
- return <img key={this._coverPath.path} src={this._coverPath.path} onLoad={action(() => this._showWaiting = false)}
+ let nativeWidth = (this.Document.nativeWidth || 0);
+ let nativeHeight = (this.Document.nativeHeight || 0);
+ return <img key={this._coverPath.path} src={this._coverPath.path} onError={action(() => this._coverPath.path = "http://www.cs.brown.edu/~bcz/face.gif")} onLoad={action(() => this._showWaiting = false)}
style={{ position: "absolute", display: "inline-block", top: 0, left: 0, width: `${nativeWidth}px`, height: `${nativeHeight}px` }} />;
}
-
@action
onZoomWheel = (e: React.WheelEvent) => {
e.stopPropagation();
if (e.ctrlKey) {
- let curScale = Number(this.pdfViewer.currentScaleValue);
- this.pdfViewer.currentScaleValue = Math.max(1, Math.min(10, curScale + curScale * e.deltaY / 1000));
- this._zoomed = Number(this.pdfViewer.currentScaleValue);
+ let curScale = Number(this._pdfViewer.currentScaleValue);
+ this._pdfViewer.currentScaleValue = Math.max(1, Math.min(10, curScale + curScale * e.deltaY / 1000));
+ this._zoomed = Number(this._pdfViewer.currentScaleValue);
}
}
@computed get annotationLayer() {
- return <div className="pdfViewer-annotationLayer" style={{ height: NumCast(this.props.Document.nativeHeight) }} ref={this._annotationLayer}>
+ trace();
+ return <div className="pdfViewer-annotationLayer" style={{ height: (this.Document.nativeHeight || 0) }} ref={this._annotationLayer}>
{this.nonDocAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y)).map((anno, index) =>
- <Annotation {...this.props} anno={anno} key={`${anno[Id]}-annotation`} />)}
+ <Annotation {...this.props} focus={this.props.focus} extensionDoc={this.extensionDoc!} anno={anno} key={`${anno[Id]}-annotation`} />)}
+ <div className="pdfViewer-overlay" id="overlay" style={{ transform: `scale(${this._zoomed})` }}>
+ <CollectionFreeFormView {...this.props}
+ annotationsKey={this.annotationsKey}
+ setPreviewCursor={this.setPreviewCursor}
+ PanelHeight={() => (this.Document.scrollHeight || this.Document.nativeHeight || 0)}
+ PanelWidth={() => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : (this.Document.nativeWidth || 0)}
+ VisibleHeight={this.visibleHeight}
+ focus={this.props.focus}
+ isSelected={this.props.isSelected}
+ isAnnotationOverlay={true}
+ select={emptyFunction}
+ active={this.active}
+ ContentScaling={returnOne}
+ whenActiveChanged={this.whenActiveChanged}
+ removeDocument={this.removeDocument}
+ moveDocument={this.moveDocument}
+ addDocument={this.addDocument}
+ CollectionView={undefined}
+ ScreenToLocalTransform={this.scrollXf}
+ ruleProvider={undefined}
+ renderDepth={this.props.renderDepth + 1}
+ ContainingCollectionDoc={this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.Document}
+ chromeCollapsed={true}>
+ </CollectionFreeFormView>
+ </div>
</div>;
}
@computed get pdfViewerDiv() {
@@ -663,36 +656,16 @@ export class PDFViewer extends React.Component<IViewerProps> {
marqueeX = () => this._marqueeX;
marqueeY = () => this._marqueeY;
marqueeing = () => this._marqueeing;
+ visibleHeight = () => this.props.PanelHeight() / this.props.ContentScaling() * 72 / 96;
render() {
trace();
- return (<div className={"pdfViewer-viewer" + (this._zoomed !== 1 ? "-zoomed" : "")} onScroll={this.onScroll} onWheel={this.onZoomWheel} onPointerDown={this.onPointerDown} onClick={this.onClick} ref={this._mainCont}>
- {this.pdfViewerDiv}
- <PdfViewerMarquee isMarqueeing={this.marqueeing} width={this.marqueeWidth} height={this.marqueeHeight} x={this.marqueeX} y={this.marqueeY} />
- <div className="pdfViewer-overlay" style={{ transform: `scale(${this._zoomed})` }}>
+ return !this.extensionDoc ? (null) :
+ <div className={"pdfViewer-viewer" + (this._zoomed !== 1 ? "-zoomed" : "")} onScroll={this.onScroll} onWheel={this.onZoomWheel} onPointerDown={this.onPointerDown} onClick={this.onClick} ref={this._mainCont}>
+ {this.pdfViewerDiv}
{this.annotationLayer}
- <CollectionFreeFormView {...this.props}
- setPreviewCursor={this.setPreviewCursor}
- PanelHeight={() => NumCast(this.props.Document.scrollHeight, NumCast(this.props.Document.nativeHeight))}
- PanelWidth={() => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : NumCast(this.props.Document.nativeWidth)}
- focus={emptyFunction}
- isSelected={this.props.isSelected}
- select={emptyFunction}
- active={this.active}
- ContentScaling={returnOne}
- whenActiveChanged={this.whenActiveChanged}
- removeDocument={this.removeDocument}
- moveDocument={this.moveDocument}
- addDocument={(doc: Doc, allow: boolean | undefined) => { Doc.AddDocToList(this.props.fieldExtensionDoc, this.props.fieldExt, doc); return true; }}
- CollectionView={this.props.ContainingCollectionView}
- ScreenToLocalTransform={this.scrollXf}
- ruleProvider={undefined}
- renderDepth={this.props.renderDepth + 1}
- ContainingCollectionDoc={this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.Document}
- chromeCollapsed={true}>
- </CollectionFreeFormView>
- </div>
- {this.standinViews}
- </div >);
+ {this.standinViews}
+ <PdfViewerMarquee isMarqueeing={this.marqueeing} width={this.marqueeWidth} height={this.marqueeHeight} x={this.marqueeX} y={this.marqueeY} />
+ </div >;
}
}
@@ -711,11 +684,9 @@ class PdfViewerMarquee extends React.Component<PdfViewerMarqueeProps> {
style={{
left: `${this.props.x()}px`, top: `${this.props.y()}px`,
width: `${this.props.width()}px`, height: `${this.props.height()}px`,
- border: `${this.props.width() === 0 ? "" : "2px dashed black"}`
+ border: `${this.props.width() === 0 ? "" : "2px dashed black"}`,
+ opacity: 0.2
}}>
</div>;
}
-}
-
-
-export enum AnnotationTypes { Region }
+} \ No newline at end of file
diff --git a/src/client/views/presentationview/PresElementBox.scss b/src/client/views/presentationview/PresElementBox.scss
new file mode 100644
index 000000000..34c170be2
--- /dev/null
+++ b/src/client/views/presentationview/PresElementBox.scss
@@ -0,0 +1,84 @@
+.presElementBox-item {
+ padding: 10px;
+ display: inline-block;
+ background-color: #eeeeee;
+ pointer-events: all;
+ width: 100%;
+ outline-color: maroon;
+ outline-style: dashed;
+ border-radius: 12px;
+ -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;
+
+ .documentView-node {
+ position: absolute;
+ z-index: 1;
+ }
+}
+
+.presElementBox-item-above {
+ border-top: black 2px solid;
+}
+
+.presElementBox-item-below {
+ border-bottom: black 2px solid;
+}
+
+.presElementBox-item:hover {
+ transition: all .1s;
+ background: #AAAAAA;
+ border-radius: 12px;
+}
+
+.presElementBox-selected {
+ background: gray;
+ color: black;
+ border-radius: 12px;
+ box-shadow: black 2px 2px 5px;
+}
+
+.presElementBox-closeIcon {
+ float: right;
+ border-radius: 20px;
+ transform:scale(0.7);
+}
+
+.presElementBox-interaction {
+ color: gray;
+ float: left;
+}
+
+.presElementBox-interaction-selected {
+ color: white;
+ float: left;
+}
+
+.presElementBox-name {
+ font-size: 15px;
+ position: absolute;
+ display: inline-block;
+ width: calc(100% - 45px);
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: pre;
+}
+
+.presElementBox-embedded {
+ position: relative;
+ margin-top: 30;
+}
+
+.presElementBox-embeddedMask {
+ width:100%;
+ height:100%;
+ position: absolute;
+ left:0;
+ top:0;
+ background: transparent;
+ z-index:2;
+} \ No newline at end of file
diff --git a/src/client/views/presentationview/PresElementBox.tsx b/src/client/views/presentationview/PresElementBox.tsx
new file mode 100644
index 000000000..17b2094ec
--- /dev/null
+++ b/src/client/views/presentationview/PresElementBox.tsx
@@ -0,0 +1,224 @@
+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 { observer } from "mobx-react";
+import { Doc, DocListCast } 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 } from "../../../Utils";
+import { DocumentType } from "../../documents/DocumentTypes";
+import { Transform } from "../../util/Transform";
+import { CollectionViewType } from '../collections/CollectionView';
+import { ContentFittingDocumentView } from '../nodes/ContentFittingDocumentView';
+import { DocComponent } from '../DocComponent';
+import { FieldView, FieldViewProps } from '../nodes/FieldView';
+import "./PresElementBox.scss";
+import React = require("react");
+
+library.add(faArrowUp);
+library.add(fileSolid);
+library.add(faLocationArrow);
+library.add(fileRegular as any);
+library.add(faSearch);
+library.add(faArrowDown);
+
+export const presSchema = createSchema({
+ presentationTargetDoc: Doc,
+ presBox: Doc,
+ presBoxKey: "string",
+ showButton: "boolean",
+ navButton: "boolean",
+ hideTillShownButton: "boolean",
+ fadeButton: "boolean",
+ hideAfterButton: "boolean",
+ groupButton: "boolean",
+ embedOpen: "boolean"
+});
+
+type PresDocument = makeInterface<[typeof presSchema, typeof documentSchema]>;
+const PresDocument = makeInterface(presSchema, documentSchema);
+/**
+ * This class models the view a document added to presentation will have in the presentation.
+ * It involves some functionality for its buttons and options.
+ */
+@observer
+export class PresElementBox extends DocComponent<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); }
+
+ /**
+ * The function that is called on click to turn Hiding document till press option on/off.
+ * It also sets the beginning and end opacitys.
+ */
+ @action
+ onHideDocumentUntilPressClick = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ this.Document.hideTillShownButton = !this.Document.hideTillShownButton;
+ if (!this.Document.hideTillShownButton) {
+ if (this.indexInPres >= this.currentIndex && this.targetDoc) {
+ this.targetDoc.opacity = 1;
+ }
+ } else {
+ if (this.presentationDoc.presStatus && this.indexInPres > this.currentIndex && this.targetDoc) {
+ this.targetDoc.opacity = 0;
+ }
+ }
+ }
+
+ /**
+ * The function that is called on click to turn Hiding document after presented option on/off.
+ * It also makes sure that the option swithches from fade-after to this one, since both
+ * can't coexist.
+ */
+ @action
+ onHideDocumentAfterPresentedClick = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ this.Document.hideAfterButton = !this.Document.hideAfterButton;
+ if (!this.Document.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) {
+ this.targetDoc.opacity = 0;
+ }
+ }
+ }
+
+ /**
+ * The function that is called on click to turn fading document after presented option on/off.
+ * It also makes sure that the option swithches from hide-after to this one, since both
+ * can't coexist.
+ */
+ @action
+ onFadeDocumentAfterPresentedClick = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ this.Document.fadeButton = !this.Document.fadeButton;
+ if (!this.Document.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.targetDoc.opacity = 0.5;
+ }
+ }
+ }
+
+ /**
+ * The function that is called on click to turn navigation option of docs on/off.
+ */
+ @action
+ onNavigateDocumentClick = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ this.Document.navButton = !this.Document.navButton;
+ if (this.Document.navButton) {
+ this.Document.showButton = false;
+ if (this.currentIndex === this.indexInPres) {
+ this.props.focus(this.props.Document);
+ }
+ }
+ }
+
+ /**
+ * The function that is called on click to turn zoom option of docs on/off.
+ */
+ @action
+ onZoomDocumentClick = (e: React.MouseEvent) => {
+ e.stopPropagation();
+
+ this.Document.showButton = !this.Document.showButton;
+ if (!this.Document.showButton) {
+ this.props.Document.viewScale = 1;
+ } else {
+ this.Document.navButton = false;
+ if (this.currentIndex === this.indexInPres) {
+ this.props.focus(this.props.Document);
+ }
+ }
+ }
+ /**
+ * Returns a local transformed coordinate array for given coordinates.
+ */
+ ScreenToLocalListTransform = (xCord: number, yCord: number) => [xCord, yCord];
+
+ /**
+ * 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);
+ }
+
+ let propDocWidth = NumCast(this.layoutDoc.nativeWidth);
+ let propDocHeight = NumCast(this.layoutDoc.nativeHeight);
+ let 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(),
+ }}>
+ <ContentFittingDocumentView
+ fitToBox={StrCast(this.targetDoc.type).indexOf(DocumentType.COL) !== -1}
+ Document={this.targetDoc}
+ fieldKey={this.props.fieldKey}
+ addDocument={returnFalse}
+ removeDocument={returnFalse}
+ ruleProvider={undefined}
+ addDocTab={returnFalse}
+ pinToPres={returnFalse}
+ PanelWidth={() => this.props.PanelWidth() - 20}
+ PanelHeight={() => 100}
+ setPreviewScript={emptyFunction}
+ getTransform={Transform.Identity}
+ active={this.props.active}
+ moveDocument={this.props.moveDocument!}
+ renderDepth={1}
+ focus={emptyFunction}
+ whenActiveChanged={returnFalse}
+ />
+ <div className="presElementBox-embeddedMask" />
+ </div>
+ );
+ }
+
+ render() {
+ let treecontainer = this.props.ContainingCollectionDoc && this.props.ContainingCollectionDoc.viewType === CollectionViewType.Tree;
+ let className = "presElementBox-item" + (this.currentIndex === this.indexInPres ? " presElementBox-selected" : "");
+ let pbi = "presElementBox-interaction";
+ return (
+ <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(); }}>
+ {treecontainer ? (null) : <>
+ <strong className="presElementBox-name">
+ {`${this.indexInPres + 1}. ${this.Document.title}`}
+ </strong>
+ <button className="presElementBox-closeIcon" onPointerDown={e => e.stopPropagation()} onClick={e => this.props.removeDocument && this.props.removeDocument(this.props.Document)}>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>
+
+ <br style={{ lineHeight: 0.1 }} />
+ {this.renderEmbeddedInline()}
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/presentationview/PresentationElement.tsx b/src/client/views/presentationview/PresentationElement.tsx
deleted file mode 100644
index 126d62c52..000000000
--- a/src/client/views/presentationview/PresentationElement.tsx
+++ /dev/null
@@ -1,426 +0,0 @@
-import { library } from '@fortawesome/fontawesome-svg-core';
-import { faFile as fileRegular } from '@fortawesome/free-regular-svg-icons';
-import { faArrowRight, 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 { observer } from "mobx-react";
-import { Doc } from "../../../new_fields/Doc";
-import { Id } from "../../../new_fields/FieldSymbols";
-import { BoolCast, NumCast, StrCast } from "../../../new_fields/Types";
-import { emptyFunction, returnEmptyString, returnFalse, returnOne } from "../../../Utils";
-import { DocumentType } from "../../documents/DocumentTypes";
-import { DragManager, dropActionType, SetupDrag } from "../../util/DragManager";
-import { SelectionManager } from "../../util/SelectionManager";
-import { Transform } from "../../util/Transform";
-import { ContextMenu } from "../ContextMenu";
-import { DocumentView } from "../nodes/DocumentView";
-import React = require("react");
-
-
-library.add(faArrowUp);
-library.add(fileSolid);
-library.add(faLocationArrow);
-library.add(fileRegular as any);
-library.add(faSearch);
-library.add(faArrowRight);
-
-interface PresentationElementProps {
- mainDocument: Doc;
- document: Doc;
- index: number;
- deleteDocument(index: number): void;
- gotoDocument(index: number, fromDoc: number): Promise<void>;
- allListElements: Doc[];
- presStatus: boolean;
- removeDocByRef(doc: Doc): boolean;
- PresElementsMappings: Map<Doc, PresentationElement>;
-}
-
-/**
- * This class models the view a document added to presentation will have in the presentation.
- * It involves some functionality for its buttons and options.
- */
-@observer
-export default class PresentationElement extends React.Component<PresentationElementProps> {
-
- private header?: HTMLDivElement | undefined;
- private listdropDisposer?: DragManager.DragDropDisposer;
- private presElRef: React.RefObject<HTMLDivElement> = React.createRef();
-
- componentWillUnmount() {
- this.listdropDisposer && this.listdropDisposer();
- }
-
- @computed get currentIndex() { return NumCast(this.props.mainDocument.selectedDoc); }
-
- @computed get showButton() { return BoolCast(this.props.document.showButton); }
- @computed get navButton() { return BoolCast(this.props.document.navButton); }
- @computed get hideTillShownButton() { return BoolCast(this.props.document.hideTillShownButton); }
- @computed get fadeButton() { return BoolCast(this.props.document.fadeButton); }
- @computed get hideAfterButton() { return BoolCast(this.props.document.hideAfterButton); }
- @computed get groupButton() { return BoolCast(this.props.document.groupButton); }
- @computed get openRightButton() { return BoolCast(this.props.document.openRightButton); }
- set showButton(val: boolean) { this.props.document.showButton = val; }
- set navButton(val: boolean) { this.props.document.navButton = val; }
- set hideTillShownButton(val: boolean) { this.props.document.hideTillShownButton = val; }
- set fadeButton(val: boolean) { this.props.document.fadeButton = val; }
- set hideAfterButton(val: boolean) { this.props.document.hideAfterButton = val; }
- set groupButton(val: boolean) { this.props.document.groupButton = val; }
- set openRightButton(val: boolean) { this.props.document.openRightButton = val; }
-
- //Lifecycle function that makes sure that button BackUp is received when mounted.
- async componentDidMount() {
- if (this.presElRef.current) {
- this.header = this.presElRef.current;
- this.createListDropTarget(this.presElRef.current);
- }
- }
-
- //Lifecycle function that makes sure button BackUp is received when not re-mounted bu re-rendered.
- async componentDidUpdate() {
- if (this.presElRef.current) {
- this.header = this.presElRef.current;
- this.createListDropTarget(this.presElRef.current);
- }
- }
-
- @action
- onGroupClick = (e: React.MouseEvent) => {
- this.groupButton = !this.groupButton;
- }
-
- /**
- * The function that is called on click to turn Hiding document till press option on/off.
- * It also sets the beginning and end opacitys.
- */
- @action
- onHideDocumentUntilPressClick = (e: React.MouseEvent) => {
- e.stopPropagation();
- this.hideTillShownButton = !this.hideTillShownButton;
- if (!this.hideTillShownButton) {
- if (this.props.index >= this.currentIndex) {
- this.props.document.opacity = 1;
- }
- } else {
- if (this.props.presStatus) {
- if (this.props.index > this.currentIndex) {
- this.props.document.opacity = 0;
- }
- }
- }
- }
-
- /**
- * The function that is called on click to turn Hiding document after presented option on/off.
- * It also makes sure that the option swithches from fade-after to this one, since both
- * can't coexist.
- */
- @action
- onHideDocumentAfterPresentedClick = (e: React.MouseEvent) => {
- e.stopPropagation();
- this.hideAfterButton = !this.hideAfterButton;
- if (!this.hideAfterButton) {
- if (this.props.index <= this.currentIndex) {
- this.props.document.opacity = 1;
- }
- } else {
- if (this.fadeButton) this.fadeButton = false;
- if (this.props.presStatus) {
- if (this.props.index < this.currentIndex) {
- this.props.document.opacity = 0;
- }
- }
- }
- }
-
- /**
- * The function that is called on click to turn fading document after presented option on/off.
- * It also makes sure that the option swithches from hide-after to this one, since both
- * can't coexist.
- */
- @action
- onFadeDocumentAfterPresentedClick = (e: React.MouseEvent) => {
- e.stopPropagation();
- this.fadeButton = !this.fadeButton;
- if (!this.fadeButton) {
- if (this.props.index <= this.currentIndex) {
- this.props.document.opacity = 1;
- }
- } else {
- this.hideAfterButton = false;
- if (this.props.presStatus) {
- if (this.props.index < this.currentIndex) {
- this.props.document.opacity = 0.5;
- }
- }
- }
- }
-
- /**
- * The function that is called on click to turn navigation option of docs on/off.
- */
- @action
- onNavigateDocumentClick = (e: React.MouseEvent) => {
- e.stopPropagation();
- this.navButton = !this.navButton;
- if (this.navButton) {
- this.showButton = false;
- if (this.currentIndex === this.props.index) {
- this.props.gotoDocument(this.props.index, this.props.index);
- }
- }
- }
-
- /**
- * The function that is called on click to turn zoom option of docs on/off.
- */
- @action
- onZoomDocumentClick = (e: React.MouseEvent) => {
- e.stopPropagation();
-
- this.showButton = !this.showButton;
- if (!this.showButton) {
- this.props.document.viewScale = 1;
- } else {
- this.navButton = false;
- if (this.currentIndex === this.props.index) {
- this.props.gotoDocument(this.props.index, this.props.index);
- }
- }
- }
-
- /**
- * Function that opens up the option to open a element on right when navigated,
- * instead of openening it as tab as default.
- */
- @action
- onRightTabClick = (e: React.MouseEvent) => {
- e.stopPropagation();
-
- this.openRightButton = !this.openRightButton;
- }
-
- /**
- * Creating a drop target for drag and drop when called.
- */
- protected createListDropTarget = (ele: HTMLDivElement) => {
- this.listdropDisposer && this.listdropDisposer();
- if (ele) {
- this.listdropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.listDrop.bind(this) } });
- }
- }
-
- /**
- * Returns a local transformed coordinate array for given coordinates.
- */
- ScreenToLocalListTransform = (xCord: number, yCord: number) => {
- return [xCord, yCord];
- }
-
- /**
- * This method is called when a element is dropped on a already esstablished target.
- * It makes sure to do appropirate action depending on if the item is dropped before
- * or after the target.
- */
- listDrop = async (e: Event, de: DragManager.DropEvent) => {
- let x = this.ScreenToLocalListTransform(de.x, de.y);
- let rect = this.header!.getBoundingClientRect();
- let bounds = this.ScreenToLocalListTransform(rect.left, rect.top + rect.height / 2);
- let before = x[1] < bounds[1];
- if (de.data instanceof DragManager.DocumentDragData) {
- let addDoc = (doc: Doc) => Doc.AddDocToList(this.props.mainDocument, "data", doc, this.props.document, before);
- e.stopPropagation();
- //where does treeViewId come from
- let movedDocs = (de.data.options === this.props.mainDocument[Id] ? de.data.draggedDocuments : de.data.droppedDocuments);
- //console.log("How is this causing an issue");
- document.removeEventListener("pointermove", this.onDragMove, true);
- return (de.data.dropAction || de.data.userDropAction) ?
- de.data.droppedDocuments.reduce((added: boolean, d: Doc) => Doc.AddDocToList(this.props.mainDocument, "data", d, this.props.document, before) || added, false)
- : (de.data.moveDocument) ?
- movedDocs.reduce((added: boolean, d: Doc) => de.data.moveDocument(d, this.props.document, addDoc) || added, false)
- : de.data.droppedDocuments.reduce((added: boolean, d: Doc) => Doc.AddDocToList(this.props.mainDocument, "data", d, this.props.document, before), false);
- }
- document.removeEventListener("pointermove", this.onDragMove, true);
-
- return false;
- }
-
- //This is used to add dragging as an event.
- onPointerEnter = (e: React.PointerEvent): void => {
- if (e.buttons === 1 && SelectionManager.GetIsDragging()) {
-
- this.header!.className = "presentationView-item";
-
- if (this.currentIndex === this.props.index) {
- //this doc is selected
- this.header!.className = "presentationView-item presentationView-selected";
- }
- document.addEventListener("pointermove", this.onDragMove, true);
- }
- }
-
- //This is used to remove the dragging when dropped.
- onPointerLeave = (e: React.PointerEvent): void => {
- this.header!.className = "presentationView-item";
-
- if (this.currentIndex === this.props.index) {
- //this doc is selected
- this.header!.className = "presentationView-item presentationView-selected";
-
- }
- document.removeEventListener("pointermove", this.onDragMove, true);
- }
-
- /**
- * This method is passed in to be used when dragging a document.
- * It makes it possible to show dropping lines on drop targets.
- */
- onDragMove = (e: PointerEvent): void => {
- Doc.UnBrushDoc(this.props.document);
- let x = this.ScreenToLocalListTransform(e.clientX, e.clientY);
- let rect = this.header!.getBoundingClientRect();
- let bounds = this.ScreenToLocalListTransform(rect.left, rect.top + rect.height / 2);
- let before = x[1] < bounds[1];
- this.header!.className = "presentationView-item";
- if (before) {
- this.header!.className += " presentationView-item-above";
- }
- else if (!before) {
- this.header!.className += " presentationView-item-below";
- }
- e.stopPropagation();
- }
-
- /**
- * This method is passed in to on down event of presElement, so that drag and
- * drop can be completed with DragManager functionality.
- */
- @action
- move: DragManager.MoveFunction = (doc: Doc, target: Doc, addDoc) => {
- return this.props.document !== target && this.props.removeDocByRef(doc) && addDoc(doc);
- }
- /**
- * This function is a getter to get if a document is in previewMode.
- */
- private get embedInline() {
- return BoolCast(this.props.document.embedOpen);
- }
-
- /**
- * This function sets document in presentation preview mode as the given value.
- */
- private set embedInline(value: boolean) {
- this.props.document.embedOpen = value;
- }
-
- /**
- * The function that recreates that context menu of presentation elements.
- */
- onContextMenu = (e: React.MouseEvent<HTMLDivElement>) => {
- e.preventDefault();
- e.stopPropagation();
- ContextMenu.Instance.addItem({ description: this.embedInline ? "Collapse Inline" : "Expand Inline", event: () => this.embedInline = !this.embedInline, icon: "expand" });
- ContextMenu.Instance.displayMenu(e.clientX, e.clientY);
- }
-
- /**
- * The function that is responsible for rendering the a preview or not for this
- * presentation element.
- */
- renderEmbeddedInline = () => {
- if (!this.embedInline) {
- return (null);
- }
-
- let propDocWidth = NumCast(this.props.document.nativeWidth);
- let propDocHeight = NumCast(this.props.document.nativeHeight);
- let scale = () => {
- let newScale = 175 / NumCast(this.props.document.nativeWidth, 175);
- return newScale;
- };
- return (
- <div style={{
- position: "relative",
- height: propDocHeight === 0 ? 100 : propDocHeight * scale(),
- width: propDocWidth === 0 ? "auto" : propDocWidth * scale(),
- marginTop: 15
-
- }}>
- <DocumentView
- fitToBox={StrCast(this.props.document.type).indexOf(DocumentType.COL) !== -1}
- Document={this.props.document}
- addDocument={returnFalse}
- removeDocument={returnFalse}
- ruleProvider={undefined}
- ScreenToLocalTransform={Transform.Identity}
- addDocTab={returnFalse}
- pinToPres={returnFalse}
- renderDepth={1}
- PanelWidth={() => 350}
- PanelHeight={() => 90}
- focus={emptyFunction}
- backgroundColor={returnEmptyString}
- parentActive={returnFalse}
- whenActiveChanged={returnFalse}
- bringToFront={emptyFunction}
- zoomToScale={emptyFunction}
- getScale={returnOne}
- ContainingCollectionView={undefined}
- ContainingCollectionDoc={undefined}
- ContentScaling={scale}
- />
- <div style={{
- width: " 100%",
- height: " 100%",
- position: "absolute",
- left: 0,
- top: 0,
- background: "transparent",
- zIndex: 2,
-
- }}></div>
- </div>
- );
- }
-
- render() {
- let p = this.props;
- let title = p.document.title;
-
- let className = " presentationView-item";
- if (this.currentIndex === p.index) {
- //this doc is selected
- className += " presentationView-selected";
- }
- let dropAction = StrCast(this.props.document.dropAction) as dropActionType;
- let onItemDown = SetupDrag(this.presElRef, () => p.document, this.move, dropAction, this.props.mainDocument[Id], true);
- return (
- <div className={className} onContextMenu={this.onContextMenu} key={p.document[Id] + p.index}
- ref={this.presElRef}
- onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}
- onPointerDown={onItemDown}
- style={{
- outlineColor: "maroon",
- outlineStyle: "dashed",
- outlineWidth: Doc.IsBrushed(p.document) ? `1px` : "0px",
- }}
- onClick={e => { p.gotoDocument(p.index, this.currentIndex); e.stopPropagation(); }}>
- <strong className="presentationView-name">
- {`${p.index + 1}. ${title}`}
- </strong>
- <button className="presentation-icon" onPointerDown={(e) => e.stopPropagation()} onClick={e => { this.props.deleteDocument(p.index); e.stopPropagation(); }}>X</button>
- <br></br>
- <button title="Zoom" className={this.showButton ? "presentation-interaction-selected" : "presentation-interaction"} onPointerDown={(e) => e.stopPropagation()} onClick={this.onZoomDocumentClick}><FontAwesomeIcon icon={"search"} /></button>
- <button title="Navigate" className={this.navButton ? "presentation-interaction-selected" : "presentation-interaction"} onPointerDown={(e) => e.stopPropagation()} onClick={this.onNavigateDocumentClick}><FontAwesomeIcon icon={"location-arrow"} /></button>
- <button title="Hide Document Till Presented" className={this.hideTillShownButton ? "presentation-interaction-selected" : "presentation-interaction"} onPointerDown={(e) => e.stopPropagation()} onClick={this.onHideDocumentUntilPressClick}><FontAwesomeIcon icon={fileSolid} /></button>
- <button title="Fade Document After Presented" className={this.fadeButton ? "presentation-interaction-selected" : "presentation-interaction"} onPointerDown={(e) => e.stopPropagation()} onClick={this.onFadeDocumentAfterPresentedClick}><FontAwesomeIcon icon={faFileDownload} /></button>
- <button title="Hide Document After Presented" className={this.hideAfterButton ? "presentation-interaction-selected" : "presentation-interaction"} onPointerDown={(e) => e.stopPropagation()} onClick={this.onHideDocumentAfterPresentedClick}><FontAwesomeIcon icon={faFileDownload} /></button>
- <button title="Group With Up" className={this.groupButton ? "presentation-interaction-selected" : "presentation-interaction"} onPointerDown={(e) => e.stopPropagation()} onClick={this.onGroupClick}> <FontAwesomeIcon icon={"arrow-up"} /> </button>
- <button title="Open Right" className={this.openRightButton ? "presentation-interaction-selected" : "presentation-interaction"} onPointerDown={(e) => e.stopPropagation()} onClick={this.onRightTabClick}><FontAwesomeIcon icon={"arrow-right"} /></button>
-
- <br />
- {this.renderEmbeddedInline()}
- </div>
- );
- }
-} \ No newline at end of file
diff --git a/src/client/views/presentationview/PresentationList.tsx b/src/client/views/presentationview/PresentationList.tsx
deleted file mode 100644
index 930ce202e..000000000
--- a/src/client/views/presentationview/PresentationList.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-import { action } from "mobx";
-import { observer } from "mobx-react";
-import { Doc, DocListCast } from "../../../new_fields/Doc";
-import { Id } from "../../../new_fields/FieldSymbols";
-import { NumCast } from "../../../new_fields/Types";
-import PresentationElement from "./PresentationElement";
-import "./PresentationView.scss";
-import React = require("react");
-
-
-interface PresListProps {
- mainDocument: Doc;
- deleteDocument(index: number): void;
- gotoDocument(index: number, fromDoc: number): Promise<void>;
- PresElementsMappings: Map<Doc, PresentationElement>;
- setChildrenDocs: (docList: Doc[]) => void;
- presStatus: boolean;
- removeDocByRef(doc: Doc): boolean;
- clearElemMap(): void;
-
-}
-
-
-@observer
-/**
- * Component that takes in a document prop and a boolean whether it's collapsed or not.
- */
-export default class PresentationViewList extends React.Component<PresListProps> {
-
-
- /**
- * 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[]) => {
- docList.forEach((doc: Doc) => {
- let curScale = NumCast(doc.viewScale, null);
- if (curScale === undefined) {
- doc.viewScale = 1;
- }
- });
- }
-
- render() {
- const children = DocListCast(this.props.mainDocument.data);
- this.initializeScaleViews(children);
- this.props.setChildrenDocs(children);
- this.props.clearElemMap();
- return (
- <div className="presentationView-listCont" >
- {children.map((doc: Doc, index: number) =>
- <PresentationElement
- ref={(e) => {
- if (e && e !== null) {
- this.props.PresElementsMappings.set(doc, e);
- }
- }}
- key={doc[Id]}
- mainDocument={this.props.mainDocument}
- document={doc}
- index={index}
- deleteDocument={this.props.deleteDocument}
- gotoDocument={this.props.gotoDocument}
- allListElements={children}
- presStatus={this.props.presStatus}
- removeDocByRef={this.props.removeDocByRef}
- PresElementsMappings={this.props.PresElementsMappings}
- />
- )}
- </div>
- );
- }
-}
diff --git a/src/client/views/presentationview/PresentationModeMenu.scss b/src/client/views/presentationview/PresentationModeMenu.scss
deleted file mode 100644
index 336f43d20..000000000
--- a/src/client/views/presentationview/PresentationModeMenu.scss
+++ /dev/null
@@ -1,30 +0,0 @@
-.presMenu-cont {
- position: fixed;
- z-index: 10000;
- height: 35px;
- background: #323232;
- box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25);
- border-radius: 0px 6px 6px 6px;
- overflow: hidden;
- display: flex;
-
- .presMenu-button {
- background-color: transparent;
- width: 35px;
- height: 35px;
- }
-
- .presMenu-button:hover {
- background-color: #121212;
- }
-
- .presMenu-dragger {
- height: 100%;
- transition: width .2s;
- background-image: url("https://logodix.com/logo/1020374.png");
- background-size: 90% 100%;
- background-repeat: no-repeat;
- background-position: left center;
- }
-
-} \ No newline at end of file
diff --git a/src/client/views/presentationview/PresentationModeMenu.tsx b/src/client/views/presentationview/PresentationModeMenu.tsx
deleted file mode 100644
index 0dd2b7731..000000000
--- a/src/client/views/presentationview/PresentationModeMenu.tsx
+++ /dev/null
@@ -1,105 +0,0 @@
-import React = require("react");
-import { observable, action, runInAction } from "mobx";
-import "./PresentationModeMenu.scss";
-import { observer } from "mobx-react";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-
-
-export interface PresModeMenuProps {
- next: () => void;
- back: () => void;
- presStatus: boolean;
- startOrResetPres: () => void;
- closePresMode: () => void;
-}
-
-/**
- * This class is responsible for modeling of the Presentation Mode Menu. The menu allows
- * user to navigate through presentation elements, and start/stop the presentation.
- */
-@observer
-export default class PresModeMenu extends React.Component<PresModeMenuProps> {
-
- @observable private _top: number = 20;
- @observable private _left: number = window.innerWidth - 160;
- @observable private _opacity: number = 1;
- @observable private _transition: string = "opacity 0.5s";
- @observable private _transitionDelay: string = "";
- private _offsetY: number = 0;
- private _offsetX: number = 0;
-
-
- private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
-
- /**
- * The function that changes the coordinates of the menu, depending on the
- * movement of the mouse when it's being dragged.
- */
- @action
- dragging = (e: PointerEvent) => {
- this._left = e.pageX - this._offsetX;
- this._top = e.pageY - this._offsetY;
-
- e.stopPropagation();
- e.preventDefault();
- }
-
- /**
- * The function that removes the event listeners that are responsible for
- * dragging of the menu.
- */
- dragEnd = (e: PointerEvent) => {
- document.removeEventListener("pointermove", this.dragging);
- document.removeEventListener("pointerup", this.dragEnd);
- e.stopPropagation();
- e.preventDefault();
- }
-
- /**
- * The function that starts the dragging of the presentation mode menu. When
- * the lines on further right are clicked on.
- */
- dragStart = (e: React.PointerEvent) => {
- document.removeEventListener("pointermove", this.dragging);
- document.addEventListener("pointermove", this.dragging);
- document.removeEventListener("pointerup", this.dragEnd);
- document.addEventListener("pointerup", this.dragEnd);
-
- this._offsetX = this._mainCont.current!.getBoundingClientRect().width - e.nativeEvent.offsetX;
- this._offsetY = e.nativeEvent.offsetY;
-
- e.stopPropagation();
- e.preventDefault();
- }
-
- /**
- * The function that is responsible for rendering the play or pause button, depending on the
- * status of the presentation.
- */
- renderPlayPauseButton = () => {
- if (this.props.presStatus) {
- return <button title="Reset Presentation" className="presMenu-button" onClick={this.props.startOrResetPres}><FontAwesomeIcon icon="stop" /></button>;
- } else {
- return <button title="Start Presentation From Start" className="presMenu-button" onClick={this.props.startOrResetPres}><FontAwesomeIcon icon="play" /></button>;
- }
- }
-
- render() {
- return (
- <div className="presMenu-cont" ref={this._mainCont}
- style={{ left: this._left, top: this._top, opacity: this._opacity, transition: this._transition, transitionDelay: this._transitionDelay }}>
- <button title="Back" className="presMenu-button" onClick={this.props.back}><FontAwesomeIcon icon={"arrow-left"} /></button>
- {this.renderPlayPauseButton()}
- <button title="Next" className="presMenu-button" onClick={this.props.next}><FontAwesomeIcon icon={"arrow-right"} /></button>
- <button className="presMenu-button" title="Close Presentation Menu" onClick={this.props.closePresMode}>
- <FontAwesomeIcon icon="times" size="lg" />
- </button>
- <div className="presMenu-dragger" onPointerDown={this.dragStart} style={{ width: "20px" }} />
- </div >
- );
- }
-
-
-
-
-} \ No newline at end of file
diff --git a/src/client/views/presentationview/PresentationView.scss b/src/client/views/presentationview/PresentationView.scss
deleted file mode 100644
index 5c40a8808..000000000
--- a/src/client/views/presentationview/PresentationView.scss
+++ /dev/null
@@ -1,113 +0,0 @@
-.presentationView-cont {
- position: absolute;
- z-index: 2;
- box-shadow: #AAAAAA .2vw .2vw .4vw;
- right: 0;
- top: 0;
- bottom: 0;
- letter-spacing: 2px;
- overflow: hidden;
- transition: 0.7s opacity ease;
- pointer-events: all;
-}
-
-.presentationView-item {
- padding: 10px;
- display: inline-block;
- width: 100%;
- -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;
-
- .documentView-node {
-
- position: absolute;
- z-index: 1;
- }
-}
-
-.presentationView-item-above {
- border-top: black 2px solid;
-}
-
-.presentationView-item-below {
- border-bottom: black 2px solid;
-}
-
-.presentationView-listCont {
- padding-left: 10px;
- padding-right: 10px;
-}
-
-.presentationView-item:hover {
- transition: all .1s;
- background: #AAAAAA;
- border-radius: 12px;
-}
-
-.presentationView-selected {
- background: gray;
- color: black;
- border-radius: 12px;
- box-shadow: black 2px 2px 5px;
-}
-
-.presentationView-heading {
- background: gray;
- padding: 10px;
- display: inline-block;
- width: 100%;
-}
-
-.presentationView-title {
- padding-top: 3px;
- padding-bottom: 3px;
- font-size: 25px;
- display: inline-block;
- width: calc(100% - 200px);
- letter-spacing: 2px;
-}
-
-.presentation-icon {
- float: right;
-}
-
-.presentation-interaction {
- color: gray;
- float: left;
-}
-
-.presentation-interaction-selected {
- color: white;
- float: left;
-}
-
-.presentationView-name {
- font-size: 15px;
- display: inline-block;
-}
-
-.presentation-button {
- margin-right: 2.5%;
- margin-left: 2.5%;
- width: 20%;
- border-radius: 5px;
-}
-
-.presentation-buttons {
- padding: 10px;
-}
-
-.presentation-next:hover {
- transition: all .1s;
- background: #AAAAAA
-}
-
-.presentation-back:hover {
- transition: all .1s;
- background: #AAAAAA
-} \ No newline at end of file
diff --git a/src/client/views/search/FilterBox.tsx b/src/client/views/search/FilterBox.tsx
index da733d64b..b841190d4 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.HIST, DocumentType.IMG, DocumentType.LINK, DocumentType.PDF, DocumentType.TEXT, DocumentType.VID, DocumentType.WEB];
+ 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
@@ -393,7 +393,7 @@ export class FilterBox extends React.Component {
<div>
<div style={{ display: "flex", flexDirection: "row-reverse" }}>
<SearchBox />
- {this.getActiveFilters()}
+ {/* {this.getActiveFilters()} */}
</div>
{this._filterOpen ? (
<div className="filter-form" onPointerDown={this.stopProp} id="filter-form" style={this._filterOpen ? { display: "flex" } : { display: "none" }}>
diff --git a/src/client/views/search/IconBar.tsx b/src/client/views/search/IconBar.tsx
index c9924222f..cff397407 100644
--- a/src/client/views/search/IconBar.tsx
+++ b/src/client/views/search/IconBar.tsx
@@ -59,23 +59,9 @@ export class IconBar extends React.Component {
render() {
return (
<div className="icon-bar">
- <div className="type-outer">
- <div className={"type-icon all"}
- onClick={this.selectAll}>
- <FontAwesomeIcon className="fontawesome-icon" icon={faCheckCircle} />
- </div>
- <div className="filter-description">Select All</div>
- </div>
{FilterBox.Instance._allIcons.map((type: string) =>
- <IconButton type={type} />
+ <IconButton key={type.toString()} type={type} />
)}
- <div className="type-outer">
- <div className={"type-icon none"}
- onClick={this.resetSelf}>
- <FontAwesomeIcon className="fontawesome-icon" icon={faTimesCircle} />
- </div>
- <div className="filter-description">Clear</div>
- </div>
</div>
);
}
diff --git a/src/client/views/search/IconButton.scss b/src/client/views/search/IconButton.scss
index d1853177e..4a3107676 100644
--- a/src/client/views/search/IconButton.scss
+++ b/src/client/views/search/IconButton.scss
@@ -35,7 +35,7 @@
text-align: center;
height: 15px;
margin-top: 5px;
- opacity: 0;
+ opacity: 1;
-webkit-transition: all 0.2s ease-in-out;
-moz-transition: all 0.2s ease-in-out;
-o-transition: all 0.2s ease-in-out;
diff --git a/src/client/views/search/SearchBox.scss b/src/client/views/search/SearchBox.scss
index 5ed33a596..bc11604a5 100644
--- a/src/client/views/search/SearchBox.scss
+++ b/src/client/views/search/SearchBox.scss
@@ -1,6 +1,15 @@
@import "../globalCssVariables";
@import "./NaviconButton.scss";
+.searchBox-container {
+ display: flex;
+ flex-direction: column;
+ width:100%;
+ height:100%;
+ position: absolute;
+ font-size: 10px;
+ line-height: 1;
+}
.searchBox-bar {
height: 32px;
display: flex;
@@ -34,6 +43,9 @@
&.searchBox-filter {
align-self: stretch;
+ }
+
+ &.searchBox-submit {
margin-left: 2px;
margin-right: 2px
}
@@ -45,12 +57,18 @@
}
}
+.searchBox-quickFilter {
+ width: 100%;
+ height: 40px;
+ margin-top: 10px;
+}
+
.searchBox-results {
- margin-right: 136px;
+ display:flex;
+ flex-direction: column;
top: 300px;
display: flex;
flex-direction: column;
- margin-right: 72px;
// height: 560px;
height: 100%;
// overflow: hidden;
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index 0d50124dd..899a35f48 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -18,6 +18,8 @@ import { FilterBox } from './FilterBox';
import "./FilterBox.scss";
import "./SearchBox.scss";
import { SearchItem } from './SearchItem';
+import { IconBar } from './IconBar';
+import { string } from 'prop-types';
library.add(faTimes);
@@ -27,7 +29,7 @@ export class SearchBox extends React.Component {
@observable private _searchString: string = "";
@observable private _resultsOpen: boolean = false;
@observable private _searchbarOpen: boolean = false;
- @observable private _results: [Doc, string[]][] = [];
+ @observable private _results: [Doc, string[], string[]][] = [];
private _resultsSet = new Map<Doc, number>();
@observable private _openNoResults: boolean = false;
@observable private _visibleElements: JSX.Element[] = [];
@@ -139,7 +141,7 @@ export class SearchBox extends React.Component {
private get filterQuery() {
const types = FilterBox.Instance.filterTypes;
const includeDeleted = FilterBox.Instance.getDataStatus();
- return "NOT baseProto_b:true" + (includeDeleted ? "" : " AND NOT deleted_b:true") + (types ? ` AND (${types.map(type => `({!join from=id to=proto_i}type_t:"${type}" AND NOT type_t:*) OR type_t:"${type}"`).join(" ")})` : "");
+ return "NOT baseProto_b:true" + (includeDeleted ? "" : " AND NOT deleted_b:true") + (types ? ` AND (${types.map(type => `({!join from=id to=proto_i}type_t:"${type}" AND NOT type_t:*) OR type_t:"${type}" OR type_t:"extension"`).join(" ")})` : "");
}
@@ -159,6 +161,8 @@ export class SearchBox extends React.Component {
const highlighting = res.highlighting || {};
const highlightList = res.docs.map(doc => highlighting[doc[Id]]);
+ const lines = new Map<string, string[]>();
+ res.docs.map((doc, i) => lines.set(doc[Id], res.lines[i]));
const docs = await Promise.all(res.docs.map(async doc => (await Cast(doc.extendsDoc, Doc)) || doc));
const highlights: typeof res.highlighting = {};
docs.forEach((doc, index) => highlights[doc[Id]] = highlightList[index]);
@@ -168,12 +172,14 @@ export class SearchBox extends React.Component {
filteredDocs.forEach(doc => {
const index = this._resultsSet.get(doc);
const highlight = highlights[doc[Id]];
+ const line = lines.get(doc[Id]) || [];
const hlights = highlight ? Object.keys(highlight).map(key => key.substring(0, key.length - 2)) : [];
if (index === undefined) {
this._resultsSet.set(doc, this._results.length);
- this._results.push([doc, hlights]);
+ this._results.push([doc, hlights, line]);
} else {
this._results[index][1].push(...hlights);
+ this._results[index][2].push(...line);
}
});
});
@@ -205,7 +211,7 @@ export class SearchBox extends React.Component {
});
let x = 0;
let y = 0;
- for (const doc of docs) {
+ for (const doc of docs.map(d => Doc.Layout(d))) {
doc.x = x;
doc.y = y;
const size = 200;
@@ -231,7 +237,7 @@ export class SearchBox extends React.Component {
}
@action.bound
- openSearch(e: React.PointerEvent) {
+ openSearch(e: React.SyntheticEvent) {
e.stopPropagation();
this._openNoResults = false;
FilterBox.Instance.closeFilter();
@@ -296,13 +302,13 @@ export class SearchBox extends React.Component {
}
else {
if (this._isSearch[i] !== "search") {
- let result: [Doc, string[]] | undefined = undefined;
+ let result: [Doc, string[], string[]] | undefined = undefined;
if (i >= this._results.length) {
this.getResults(this._searchString);
if (i < this._results.length) result = this._results[i];
if (result) {
let highlights = Array.from([...Array.from(new Set(result[1]).values())]).filter(v => v !== "search_string");
- this._visibleElements[i] = <SearchItem doc={result[0]} query={this._searchString} key={result[0][Id]} highlighting={highlights} />;
+ this._visibleElements[i] = <SearchItem doc={result[0]} query={this._searchString} key={result[0][Id]} lines={result[2]} highlighting={highlights} />;
this._isSearch[i] = "search";
}
}
@@ -310,7 +316,7 @@ export class SearchBox extends React.Component {
result = this._results[i];
if (result) {
let highlights = Array.from([...Array.from(new Set(result[1]).values())]).filter(v => v !== "search_string");
- this._visibleElements[i] = <SearchItem doc={result[0]} query={this._searchString} key={result[0][Id]} highlighting={highlights} />;
+ this._visibleElements[i] = <SearchItem doc={result[0]} query={this._searchString} key={result[0][Id]} lines={result[2]} highlighting={highlights} />;
this._isSearch[i] = "search";
}
}
@@ -337,12 +343,14 @@ export class SearchBox extends React.Component {
<FontAwesomeIcon icon="object-group" size="lg" />
</span>
<input value={this._searchString} onChange={this.onChange} type="text" placeholder="Search..." id="search-input" ref={this.inputRef}
- className="searchBox-barChild searchBox-input" onPointerDown={this.openSearch} onKeyPress={this.enter}
+ className="searchBox-barChild searchBox-input" onPointerDown={this.openSearch} onKeyPress={this.enter} onFocus={this.openSearch}
style={{ width: this._searchbarOpen ? "500px" : "100px" }} />
- <button className="searchBox-barChild searchBox-submit" onClick={this.submitSearch} onPointerDown={FilterBox.Instance.stopProp}>Submit</button>
- <button className="searchBox-barChild searchBox-filter" onClick={FilterBox.Instance.openFilter} onPointerDown={FilterBox.Instance.stopProp}>Filter</button>
- <button className="searchBox-barChild searchBox-close" title={"Close Search Bar"} onPointerDown={MainView.Instance.toggleSearch}><FontAwesomeIcon icon={faTimes} size="lg" /></button>
+ <button className="searchBox-barChild searchBox-filter" title="Advanced Filtering Options" onClick={FilterBox.Instance.openFilter} onPointerDown={FilterBox.Instance.stopProp}><FontAwesomeIcon icon="ellipsis-v" color="white" /></button>
</div>
+ {(this._numTotalResults > 0 || !this._searchbarOpen) ? (null) :
+ (<div className="searchBox-quickFilter" onPointerDown={this.openSearch}>
+ <div className="filter-panel"><IconBar /></div>
+ </div>)}
<div className="searchBox-results" onScroll={this.resultsScrolled} style={{
display: this._resultsOpen ? "flex" : "none",
height: this.resFull ? "560px" : this.resultHeight, overflow: this.resFull ? "auto" : "visible"
diff --git a/src/client/views/search/SearchItem.scss b/src/client/views/search/SearchItem.scss
index 273d49349..9f12994c3 100644
--- a/src/client/views/search/SearchItem.scss
+++ b/src/client/views/search/SearchItem.scss
@@ -2,22 +2,32 @@
.search-overview {
display: flex;
- flex-direction: row-reverse;
+ flex-direction: reverse;
justify-content: flex-end;
- height: 70px;
z-index: 0;
}
+.link-count {
+ background: black;
+ border-radius: 20px;
+ color: white;
+ width: 15px;
+ text-align: center;
+ margin-top: 5px;
+}
.searchBox-placeholder,
.search-overview .search-item {
- width: 500px;
+ width: 100%;
background: $light-color-secondary;
border-color: $intermediate-color;
border-bottom-style: solid;
padding: 10px;
- height: 70px;
+ min-height: 50px;
+ max-height: 150px;
+ height: auto;
z-index: 0;
display: inline-block;
+ overflow: auto;
.main-search-info {
display: flex;
@@ -26,6 +36,7 @@
.search-title-container {
width: 100%;
+ overflow: hidden;
.search-title {
text-transform: uppercase;
@@ -58,16 +69,6 @@
overflow: hidden;
position: relative;
- .link-count {
- opacity: 1;
- position: absolute;
- z-index: 1000;
- text-align: center;
- -webkit-transition: opacity 0.2s ease-in-out;
- -moz-transition: opacity 0.2s ease-in-out;
- -o-transition: opacity 0.2s ease-in-out;
- transition: opacity 0.2s ease-in-out;
- }
.link-extended {
// display: none;
@@ -109,7 +110,7 @@
.icon-icons,
.icon-live {
- height: 50px;
+ height: auto;
margin: auto;
overflow: hidden;
@@ -171,9 +172,7 @@
.searchBox-instances:active {
opacity: 1;
background: $lighter-alt-accent;
- -webkit-transform: scale(1);
- -ms-transform: scale(1);
- transform: scale(1);
+ width:150px
}
.search-item:hover {
@@ -181,16 +180,19 @@
background: $lighter-alt-accent;
}
+.search-highlighting {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: pre;
+}
+
.searchBox-instances {
float: left;
opacity: 1;
- width: 150px;
+ width: 0px;
transition: all 0.2s ease;
color: black;
- transform-origin: top right;
- -webkit-transform: scale(0);
- -ms-transform: scale(0);
- transform: scale(0);
+ overflow: hidden;
}
@@ -199,7 +201,7 @@
}
.searchBox-placeholder {
- min-height: 70px;
+ min-height: 50px;
margin-left: 150px;
text-transform: uppercase;
text-align: left;
diff --git a/src/client/views/search/SearchItem.tsx b/src/client/views/search/SearchItem.tsx
index 96eefacc2..f1d825aa0 100644
--- a/src/client/views/search/SearchItem.tsx
+++ b/src/client/views/search/SearchItem.tsx
@@ -4,13 +4,10 @@ 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, HeightSym, WidthSym } from "../../../new_fields/Doc";
+import { Doc } from "../../../new_fields/Doc";
import { Id } from "../../../new_fields/FieldSymbols";
-import { ObjectField } from "../../../new_fields/ObjectField";
-import { RichTextField } from "../../../new_fields/RichTextField";
import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
import { emptyFunction, returnEmptyString, returnFalse, returnOne, Utils } from "../../../Utils";
-import { DocServer } from "../../DocServer";
import { DocumentType } from "../../documents/DocumentTypes";
import { DocumentManager } from "../../util/DocumentManager";
import { DragManager, SetupDrag } from "../../util/DragManager";
@@ -18,7 +15,7 @@ import { LinkManager } from "../../util/LinkManager";
import { SearchUtil } from "../../util/SearchUtil";
import { Transform } from "../../util/Transform";
import { SEARCH_THUMBNAIL_SIZE } from "../../views/globalCssVariables.scss";
-import { CollectionViewType } from "../collections/CollectionBaseView";
+import { CollectionViewType } from "../collections/CollectionView";
import { CollectionDockingView } from "../collections/CollectionDockingView";
import { ContextMenu } from "../ContextMenu";
import { DocumentView } from "../nodes/DocumentView";
@@ -30,6 +27,7 @@ export interface SearchItemProps {
doc: Doc;
query: string;
highlighting: string[];
+ lines: string[];
}
library.add(faCaretUp);
@@ -152,7 +150,7 @@ export class SearchItem extends React.Component<SearchItemProps> {
if (!this._useIcons) {
let returnXDimension = () => this._useIcons ? 50 : Number(SEARCH_THUMBNAIL_SIZE);
let returnYDimension = () => this._displayDim;
- let scale = () => returnXDimension() / NumCast(this.props.doc.nativeWidth, returnXDimension());
+ let scale = () => returnXDimension() / NumCast(Doc.Layout(this.props.doc).nativeWidth, returnXDimension());
const docview = <div
onPointerDown={action(() => {
this._useIcons = !this._useIcons;
@@ -215,18 +213,15 @@ export class SearchItem extends React.Component<SearchItemProps> {
@computed
get linkCount() { return LinkManager.Instance.getAllRelatedLinks(this.props.doc).length; }
- @computed
- get linkString(): string {
- let num = this.linkCount;
- if (num === 1) {
- return num.toString() + " link";
- }
- return num.toString() + " links";
- }
-
@action
pointerDown = (e: React.PointerEvent) => { e.preventDefault(); e.button === 0 && SearchBox.Instance.openSearch(e); }
+ nextHighlight = (e: React.PointerEvent) => {
+ e.preventDefault(); e.button === 0 && SearchBox.Instance.openSearch(e);
+ let sstring = StrCast(this.props.doc.search_string);
+ this.props.doc.search_string = "";
+ setTimeout(() => this.props.doc.search_string = sstring, 0);
+ }
highlightDoc = (e: React.PointerEvent) => {
if (this.props.doc.type === DocumentType.LINK) {
if (this.props.doc.anchor1 && this.props.doc.anchor2) {
@@ -239,6 +234,7 @@ export class SearchItem extends React.Component<SearchItemProps> {
} else {
Doc.BrushDoc(this.props.doc);
}
+ e.stopPropagation();
}
unHighlightDoc = (e: React.PointerEvent) => {
@@ -282,23 +278,24 @@ export class SearchItem extends React.Component<SearchItemProps> {
const doc2 = Cast(this.props.doc.anchor2, Doc);
return (
<div className="search-overview" onPointerDown={this.pointerDown} onContextMenu={this.onContextMenu}>
- <div className="search-item" onPointerEnter={this.highlightDoc} onPointerLeave={this.unHighlightDoc} id="result"
- onClick={this.onClick} onPointerDown={this.pointerDown} >
+ <div className="search-item" onPointerDown={this.nextHighlight} onPointerEnter={this.highlightDoc} onPointerLeave={this.unHighlightDoc} id="result"
+ onClick={this.onClick}>
<div className="main-search-info">
- <div title="Drag as document" onPointerDown={this.onPointerDown} style={{ marginRight: "7px" }}> <FontAwesomeIcon icon="file" size="lg" /> </div>
+ <div title="Drag as document" onPointerDown={this.onPointerDown} style={{ marginRight: "7px" }}> <FontAwesomeIcon icon="file" size="lg" />
+ <div className="link-container item">
+ <div className="link-count" title={`${this.linkCount + " links"}`}>{this.linkCount}</div>
+ </div>
+ </div>
<div className="search-title-container">
<div className="search-title">{StrCast(this.props.doc.title)}</div>
- <div className="search-highlighting">Matched fields: {this.props.highlighting.join(", ")}</div>
+ <div className="search-highlighting">{this.props.highlighting.length ? "Matched fields:" + this.props.highlighting.join(", ") : this.props.lines.length ? this.props.lines[0] : ""}</div>
+ {this.props.lines.filter((m, i) => i).map((l, i) => <div id={i.toString()} className="search-highlighting">`${l}`</div>)}
</div>
<div className="search-info" style={{ width: this._useIcons ? "15%" : "400px" }}>
<div className={`icon-${this._useIcons ? "icons" : "live"}`}>
<div className="search-type" title="Click to Preview">{this.DocumentIcon()}</div>
<div className="search-label">{this.props.doc.type ? this.props.doc.type : "Other"}</div>
</div>
- <div className="link-container item">
- <div className="link-count">{this.linkCount}</div>
- <div className="link-extended">{this.linkString}</div>
- </div>
</div>
</div>
</div>
diff --git a/src/extensions/ArrayExtensions.ts b/src/extensions/ArrayExtensions.ts
new file mode 100644
index 000000000..8e125766d
--- /dev/null
+++ b/src/extensions/ArrayExtensions.ts
@@ -0,0 +1,37 @@
+export default class ArrayExtension {
+ private readonly property: string;
+ private readonly body: <T>(this: Array<T>) => any;
+
+ constructor(property: string, body: <T>(this: Array<T>) => any) {
+ this.property = property;
+ this.body = body;
+ }
+
+ assign() {
+ Object.defineProperty(Array.prototype, this.property, {
+ value: this.body,
+ enumerable: false
+ });
+ }
+
+}
+
+/**
+ * IMPORTANT: Any extension you add here *must* have a corresponding type definition
+ * in the Array<T> interface in ./General/ExtensionsTypings.ts. Otherwise,
+ * Typescript will not recognize your new function.
+ */
+const extensions = [
+ new ArrayExtension("lastElement", function () {
+ if (!this.length) {
+ return undefined;
+ }
+ return this[this.length - 1];
+ })
+];
+
+function Assign() {
+ extensions.forEach(extension => extension.assign());
+}
+
+export { Assign }; \ No newline at end of file
diff --git a/src/extensions/General/Extensions.ts b/src/extensions/General/Extensions.ts
new file mode 100644
index 000000000..4b6d05d5f
--- /dev/null
+++ b/src/extensions/General/Extensions.ts
@@ -0,0 +1,9 @@
+import { Assign as ArrayAssign } from "../ArrayExtensions";
+import { Assign as StringAssign } from "../StringExtensions";
+
+function AssignAllExtensions() {
+ ArrayAssign();
+ StringAssign();
+}
+
+export { AssignAllExtensions }; \ No newline at end of file
diff --git a/src/extensions/General/ExtensionsTypings.ts b/src/extensions/General/ExtensionsTypings.ts
new file mode 100644
index 000000000..370157ed0
--- /dev/null
+++ b/src/extensions/General/ExtensionsTypings.ts
@@ -0,0 +1,8 @@
+interface Array<T> {
+ lastElement(): T;
+}
+
+interface String {
+ removeTrailingNewlines(): string;
+ hasNewline(): boolean;
+} \ No newline at end of file
diff --git a/src/extensions/StringExtensions.ts b/src/extensions/StringExtensions.ts
new file mode 100644
index 000000000..2c76e56c8
--- /dev/null
+++ b/src/extensions/StringExtensions.ts
@@ -0,0 +1,17 @@
+function Assign() {
+
+ String.prototype.removeTrailingNewlines = function () {
+ let sliced = this;
+ while (sliced.endsWith("\n")) {
+ sliced = sliced.substring(0, this.length - 1);
+ }
+ return sliced as string;
+ };
+
+ String.prototype.hasNewline = function () {
+ return this.endsWith("\n");
+ };
+
+}
+
+export { Assign }; \ No newline at end of file
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index 605877efa..f3c74428a 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -14,6 +14,7 @@ import { ComputedField } from "./ScriptField";
import { BoolCast, Cast, FieldValue, NumCast, PromiseValue, StrCast, ToConstructor } from "./Types";
import { deleteProperty, getField, getter, makeEditable, makeReadOnly, setter, updateFunction } from "./util";
import { intersectRect } from "../Utils";
+import { UndoManager } from "../client/util/UndoManager";
export namespace Field {
export function toKeyValueString(doc: Doc, key: string): string {
@@ -63,6 +64,10 @@ export function DocListCastAsync(field: FieldResult, defaultValue?: Doc[]) {
return list ? Promise.all(list).then(() => list) : Promise.resolve(defaultValue);
}
+export async function DocCastAsync(field: FieldResult): Promise<Opt<Doc>> {
+ return Cast(field, Doc);
+}
+
export function DocListCast(field: FieldResult): Doc[] {
return Cast(field, listSpec(Doc), []).filter(d => d instanceof Doc) as Doc[];
}
@@ -144,14 +149,8 @@ export class Doc extends RefField {
private [Self] = this;
private [SelfProxy]: any;
- public [WidthSym] = () => {
- let animDims = this[SelfProxy].animateToDimensions ? Array.from(Cast(this[SelfProxy].animateToDimensions, listSpec("number"))!) : undefined;
- return animDims ? animDims[0] : NumCast(this[SelfProxy].width);
- }
- public [HeightSym] = () => {
- let animDims = this[SelfProxy].animateToDimensions ? Array.from(Cast(this[SelfProxy].animateToDimensions, listSpec("number"))!) : undefined;
- return animDims ? animDims[1] : NumCast(this[SelfProxy].height);
- }
+ public [WidthSym] = () => NumCast(this[SelfProxy].width);
+ public [HeightSym] = () => NumCast(this[SelfProxy].height);
[ToScriptString]() {
return "invalid";
@@ -334,14 +333,30 @@ export namespace Doc {
return Array.from(results);
}
- export function IndexOf(toFind: Doc, list: Doc[]) {
- return list.findIndex(doc => doc === toFind || Doc.AreProtosEqual(doc, toFind));
+ export function IndexOf(toFind: Doc, list: Doc[], allowProtos: boolean = true) {
+ let index = list.reduce((p, v, i) => (v instanceof Doc && v === toFind) ? i : p, -1);
+ 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) {
+ if (listDoc[key] === undefined) {
+ Doc.GetProto(listDoc)[key] = new List<Doc>();
+ }
+ let list = Cast(listDoc[key], listSpec(Doc));
+ if (list) {
+ let ind = list.indexOf(doc);
+ if (ind !== -1) {
+ list.splice(ind, 1);
+ return true;
+ }
+ }
+ return false;
}
- export function AddDocToList(target: Doc, key: string, doc: Doc, relativeTo?: Doc, before?: boolean, first?: boolean, allowDuplicates?: boolean, reversed?: boolean) {
- if (target[key] === undefined) {
- Doc.GetProto(target)[key] = new List<Doc>();
+ export function AddDocToList(listDoc: Doc, key: string, doc: Doc, relativeTo?: Doc, before?: boolean, first?: boolean, allowDuplicates?: boolean, reversed?: boolean) {
+ if (listDoc[key] === undefined) {
+ Doc.GetProto(listDoc)[key] = new List<Doc>();
}
- let list = Cast(target[key], listSpec(Doc));
+ let list = Cast(listDoc[key], listSpec(Doc));
if (list) {
if (allowDuplicates !== true) {
let pind = list.reduce((l, d, i) => d instanceof Doc && d[Id] === doc[Id] ? i : l, -1);
@@ -363,8 +378,9 @@ export namespace Doc {
else list.splice(before ? ind : ind + 1, 0, doc);
}
}
+ return true;
}
- return true;
+ return false;
}
//
@@ -382,40 +398,6 @@ export namespace Doc {
return bounds;
}
- //
- // Resolves a reference to a field by returning 'doc' if no field extension is specified,
- // otherwise, it returns the extension document stored in doc.<fieldKey>_ext.
- // This mechanism allows any fields to be extended with an extension document that can
- // be used to capture field-specific metadata. For example, an image field can be extended
- // to store annotations, ink, and other data.
- //
- export function fieldExtensionDoc(doc: Doc, fieldKey: string, fieldExt: string = "yes") {
- return fieldExt && doc[fieldKey + "_ext"] instanceof Doc ? doc[fieldKey + "_ext"] as Doc : doc;
- }
-
- export function CreateDocumentExtensionForField(doc: Doc, fieldKey: string) {
- let docExtensionForField = new Doc(doc[Id] + fieldKey, true);
- docExtensionForField.title = fieldKey + ".ext";
- docExtensionForField.extendsDoc = doc; // this is used by search to map field matches on the extension doc back to the document it extends.
- docExtensionForField.type = DocumentType.EXTENSION;
- let proto: Doc | undefined = doc;
- while (proto && !Doc.IsPrototype(proto) && proto.proto) {
- proto = proto.proto;
- }
- (proto ? proto : doc)[fieldKey + "_ext"] = new PrefetchProxy(docExtensionForField);
- return docExtensionForField;
- }
-
- export function UpdateDocumentExtensionForField(doc: Doc, fieldKey: string, immediate: boolean = false) {
- let docExtensionForField = doc[fieldKey + "_ext"] as Doc;
- if (docExtensionForField === undefined) {
- if (immediate) CreateDocumentExtensionForField(doc, fieldKey);
- else setTimeout(() => CreateDocumentExtensionForField(doc, fieldKey), 0);
- } else if (doc instanceof Doc) { // backward compatibility -- add fields for docs that don't have them already
- docExtensionForField.extendsDoc === undefined && setTimeout(() => docExtensionForField.extendsDoc = doc, 0);
- docExtensionForField.type === undefined && setTimeout(() => docExtensionForField.type = DocumentType.EXTENSION, 0);
- }
- }
export function MakeTitled(title: string) {
let doc = new Doc();
doc.title = title;
@@ -438,7 +420,7 @@ export namespace Doc {
// for individual layout properties to be overridden in the expanded layout.
//
export function WillExpandTemplateLayout(layoutDoc: Doc, dataDoc?: Doc) {
- return BoolCast(layoutDoc.isTemplate) && dataDoc && layoutDoc !== dataDoc && !(layoutDoc.layout instanceof Doc);
+ return BoolCast(layoutDoc.isTemplateField) && dataDoc && layoutDoc !== dataDoc && !(layoutDoc[StrCast(layoutDoc.layoutKey, "layout")] instanceof Doc);
}
//
@@ -454,15 +436,8 @@ export namespace Doc {
// ... which means we change the layout to be an expanded view of the template layout.
// This allows the view override the template's properties and be referenceable as its own document.
- let expandedTemplateLayout = dataDoc[templateLayoutDoc[Id]];
- if (expandedTemplateLayout instanceof Doc) {
- return expandedTemplateLayout;
- }
- if (expandedTemplateLayout instanceof Promise) {
- return undefined;
- }
let expandedLayoutFieldKey = "Layout[" + templateLayoutDoc[Id] + "]";
- expandedTemplateLayout = dataDoc[expandedLayoutFieldKey];
+ let expandedTemplateLayout = dataDoc[expandedLayoutFieldKey];
if (expandedTemplateLayout instanceof Doc) {
return expandedTemplateLayout;
}
@@ -475,15 +450,40 @@ export namespace Doc {
export function GetLayoutDataDocPair(doc: Doc, dataDoc: Doc | undefined, fieldKey: string, childDocLayout: Doc) {
let layoutDoc: Doc | undefined = childDocLayout;
- let resolvedDataDoc = !doc.isTemplate && dataDoc !== doc && dataDoc ? Doc.GetDataDoc(dataDoc) : undefined;
+ let resolvedDataDoc = !doc.isTemplateField && dataDoc !== doc && dataDoc ? Doc.GetDataDoc(dataDoc) : undefined;
if (resolvedDataDoc && Doc.WillExpandTemplateLayout(childDocLayout, resolvedDataDoc)) {
- Doc.UpdateDocumentExtensionForField(resolvedDataDoc, fieldKey);
- let fieldExtensionDoc = Doc.fieldExtensionDoc(resolvedDataDoc, StrCast(childDocLayout.templateField, StrCast(childDocLayout.title)), "dummy");
- layoutDoc = Doc.expandTemplateLayout(childDocLayout, fieldExtensionDoc !== resolvedDataDoc ? fieldExtensionDoc : undefined);
- } else layoutDoc = Doc.expandTemplateLayout(childDocLayout, resolvedDataDoc);
+ let extensionDoc = fieldExtensionDoc(resolvedDataDoc, StrCast(childDocLayout.templateField, StrCast(childDocLayout.title)));
+ layoutDoc = Doc.expandTemplateLayout(childDocLayout, extensionDoc !== resolvedDataDoc ? extensionDoc : undefined);
+ } else layoutDoc = childDocLayout;
return { layout: layoutDoc, data: resolvedDataDoc };
}
+ //
+ // Resolves a reference to a field by returning 'doc' if no field extension is specified,
+ // otherwise, it returns the extension document stored in doc.<fieldKey>_ext.
+ // This mechanism allows any fields to be extended with an extension document that can
+ // be used to capture field-specific metadata. For example, an image field can be extended
+ // to store annotations, ink, and other data.
+ //
+ export function fieldExtensionDoc(doc: Doc, fieldKey: string) {
+ let extension = doc[fieldKey + "_ext"] as Doc;
+ (extension === undefined) && setTimeout(() => CreateDocumentExtensionForField(doc, fieldKey), 0);
+ return extension ? extension : undefined;
+ }
+
+ export function CreateDocumentExtensionForField(doc: Doc, fieldKey: string) {
+ let docExtensionForField = new Doc(doc[Id] + fieldKey, true);
+ docExtensionForField.title = fieldKey + ".ext";
+ docExtensionForField.extendsDoc = doc; // this is used by search to map field matches on the extension doc back to the document it extends.
+ docExtensionForField.type = DocumentType.EXTENSION;
+ let proto: Doc | undefined = doc;
+ while (proto && !Doc.IsPrototype(proto) && proto.proto) {
+ proto = proto.proto;
+ }
+ (proto ? proto : doc)[fieldKey + "_ext"] = new PrefetchProxy(docExtensionForField);
+ return docExtensionForField;
+ }
+
export function Overwrite(doc: Doc, overwrite: Doc, copyProto: boolean = false): Doc {
Object.keys(doc).forEach(key => {
const field = ProxyField.WithoutProxy(() => doc[key]);
@@ -510,6 +510,8 @@ export namespace Doc {
export function MakeCopy(doc: Doc, copyProto: boolean = false, copyProtoId?: string): Doc {
const copy = new Doc(copyProtoId, true);
Object.keys(doc).forEach(key => {
+ let cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
+ const field = ProxyField.WithoutProxy(() => doc[key]);
if (key === "proto" && copyProto) {
if (doc[key] instanceof Doc) {
copy[key] = Doc.MakeCopy(doc[key]!, false);
@@ -518,6 +520,8 @@ export namespace Doc {
const field = ProxyField.WithoutProxy(() => doc[key]);
if (field instanceof RefField) {
copy[key] = field;
+ } else if (cfield instanceof ComputedField) {
+ copy[key] = ComputedField.MakeFunction(cfield.script.originalScript);
} else if (field instanceof ObjectField) {
copy[key] = ObjectField.MakeCopy(field);
} else if (field instanceof Promise) {
@@ -545,9 +549,14 @@ export namespace Doc {
let _applyCount: number = 0;
export function ApplyTemplate(templateDoc: Doc) {
- return !templateDoc ? undefined : ApplyTemplateTo(templateDoc, Doc.MakeDelegate(new Doc()), templateDoc.title + "(..." + _applyCount++ + ")");
+ if (templateDoc) {
+ let applied = ApplyTemplateTo(templateDoc, Doc.MakeDelegate(new Doc()), "layoutCustom", templateDoc.title + "(..." + _applyCount++ + ")");
+ applied && (Doc.GetProto(applied).layout = applied.layout);
+ return applied;
+ }
+ return undefined;
}
- export function ApplyTemplateTo(templateDoc: Doc, target: Doc, titleTarget: string | undefined = undefined) {
+ export function ApplyTemplateTo(templateDoc: Doc, target: Doc, targetKey: string, titleTarget: string | undefined = undefined) {
if (!templateDoc) {
target.layout = undefined;
target.nativeWidth = undefined;
@@ -557,41 +566,28 @@ export namespace Doc {
return;
}
- let layoutCustom = Doc.MakeTitled("layoutCustom");
let layoutCustomLayout = Doc.MakeDelegate(templateDoc);
- titleTarget && (target.title = titleTarget);
+ titleTarget && (Doc.GetProto(target).title = titleTarget);
target.type = DocumentType.TEMPLATE;
- target.width = templateDoc.width;
- target.height = templateDoc.height;
- target.nativeWidth = templateDoc.nativeWidth ? templateDoc.nativeWidth : 0;
- target.nativeHeight = templateDoc.nativeHeight ? templateDoc.nativeHeight : 0;
- target.ignoreAspect = templateDoc.nativeWidth ? true : false;
target.onClick = templateDoc.onClick instanceof ObjectField && templateDoc.onClick[Copy]();
- target.layout = layoutCustomLayout;
- target.backgroundLayout = layoutCustomLayout.backgroundLayout;
- target.layoutNative = Cast(templateDoc.layoutNative, Doc) as Doc;
- target.layoutCustom = layoutCustom;
+ Doc.GetProto(target)[targetKey] = layoutCustomLayout;
+ target.layoutKey = targetKey;
return target;
}
- export function MakeMetadataFieldTemplate(fieldTemplate: Doc, templateDataDoc: Doc, suppressTitle: boolean = false) {
+ export function MakeMetadataFieldTemplate(fieldTemplate: Doc, templateDataDoc: Doc, suppressTitle: boolean = false): boolean {
// move data doc fields to layout doc as needed (nativeWidth/nativeHeight, data, ??)
let metadataFieldName = StrCast(fieldTemplate.title).replace(/^-/, "");
- let backgroundLayout = StrCast(fieldTemplate.backgroundLayout);
let fieldLayoutDoc = fieldTemplate;
if (fieldTemplate.layout instanceof Doc) {
fieldLayoutDoc = Doc.MakeDelegate(fieldTemplate.layout);
}
- if (backgroundLayout) {
- backgroundLayout = backgroundLayout.replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metadataFieldName}"}`);
- }
fieldTemplate.templateField = metadataFieldName;
fieldTemplate.title = metadataFieldName;
- fieldTemplate.isTemplate = true;
- fieldTemplate.backgroundLayout = backgroundLayout;
+ fieldTemplate.isTemplateField = true;
/* move certain layout properties from the original data doc to the template layout to avoid
inheriting them from the template's data doc which may also define these fields for its own use.
*/
@@ -599,6 +595,7 @@ export namespace Doc {
fieldTemplate.singleColumn = BoolCast(fieldTemplate.singleColumn);
fieldTemplate.nativeWidth = Cast(fieldTemplate.nativeWidth, "number");
fieldTemplate.nativeHeight = Cast(fieldTemplate.nativeHeight, "number");
+ fieldTemplate.type = fieldTemplate.type;
fieldTemplate.panX = 0;
fieldTemplate.panY = 0;
fieldTemplate.scale = 1;
@@ -607,29 +604,32 @@ export namespace Doc {
setTimeout(action(() => {
!templateDataDoc[metadataFieldName] && data instanceof ObjectField && (Doc.GetProto(templateDataDoc)[metadataFieldName] = ObjectField.MakeCopy(data));
let layout = StrCast(fieldLayoutDoc.layout).replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metadataFieldName}"}`);
- let layoutDelegate = fieldTemplate.layout instanceof Doc ? fieldLayoutDoc : fieldTemplate;
+ let layoutDelegate = Doc.Layout(fieldTemplate);
layoutDelegate.layout = layout;
fieldTemplate.layout = layoutDelegate !== fieldTemplate ? layoutDelegate : layout;
if (fieldTemplate.backgroundColor !== templateDataDoc.defaultBackgroundColor) fieldTemplate.defaultBackgroundColor = fieldTemplate.backgroundColor;
fieldTemplate.proto = templateDataDoc;
}), 0);
+ return true;
}
- export function overlapping(doc: Doc, doc2: Doc, clusterDistance: number) {
+ export function overlapping(doc1: Doc, doc2: Doc, clusterDistance: number) {
+ let doc2Layout = Doc.Layout(doc2);
+ let doc1Layout = Doc.Layout(doc1);
var x2 = NumCast(doc2.x) - clusterDistance;
var y2 = NumCast(doc2.y) - clusterDistance;
- var w2 = NumCast(doc2.width) + clusterDistance;
- var h2 = NumCast(doc2.height) + clusterDistance;
- var x = NumCast(doc.x) - clusterDistance;
- var y = NumCast(doc.y) - clusterDistance;
- var w = NumCast(doc.width) + clusterDistance;
- var h = NumCast(doc.height) + clusterDistance;
- return doc.z === doc2.z && intersectRect({ left: x, top: y, width: w, height: h }, { left: x2, top: y2, width: w2, height: h2 });
+ var w2 = NumCast(doc2Layout.width) + clusterDistance;
+ var h2 = NumCast(doc2Layout.height) + clusterDistance;
+ var x = NumCast(doc1.x) - clusterDistance;
+ var y = NumCast(doc1.y) - clusterDistance;
+ var w = NumCast(doc1Layout.width) + clusterDistance;
+ var h = NumCast(doc1Layout.height) + clusterDistance;
+ return doc1.z === doc2.z && intersectRect({ left: x, top: y, width: w, height: h }, { left: x2, top: y2, width: w2, height: h2 });
}
export function isBrushedHighlightedDegree(doc: Doc) {
if (Doc.IsHighlighted(doc)) {
- return 3;
+ return 6;
}
else {
return Doc.IsBrushedDegree(doc);
@@ -645,6 +645,11 @@ export namespace Doc {
@observable _user_doc: Doc = undefined!;
@observable BrushedDoc: ObservableMap<Doc, boolean> = new ObservableMap();
}
+
+ // 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[StrCast(doc.layoutKey, "layout")] as Doc : doc; }
+ export function LayoutField(doc: Doc) { return doc[StrCast(doc.layoutKey, "layout")]; }
const manager = new DocData();
export function UserDoc(): Doc { return manager._user_doc; }
export function SetUserDoc(doc: Doc) { manager._user_doc = doc; }
@@ -657,10 +662,29 @@ export namespace Doc {
export function BrushDoc(doc: Doc) {
brushManager.BrushedDoc.set(doc, true);
brushManager.BrushedDoc.set(Doc.GetDataDoc(doc), true);
+ return doc;
}
export function UnBrushDoc(doc: Doc) {
brushManager.BrushedDoc.delete(doc);
brushManager.BrushedDoc.delete(Doc.GetDataDoc(doc));
+ return doc;
+ }
+
+
+ export function LinkEndpoint(linkDoc: Doc, anchorDoc: Doc) { return Doc.AreProtosEqual(anchorDoc, Cast(linkDoc.anchor1, Doc) as Doc) ? "layoutKey1" : "layoutKey2"; }
+ export function linkFollowUnhighlight() {
+ Doc.UnhighlightAll();
+ document.removeEventListener("pointerdown", linkFollowUnhighlight);
+ }
+
+ let dt = 0;
+ export function linkFollowHighlight(destDoc: Doc) {
+ linkFollowUnhighlight();
+ Doc.HighlightDoc(destDoc);
+ document.removeEventListener("pointerdown", linkFollowUnhighlight);
+ document.addEventListener("pointerdown", linkFollowUnhighlight);
+ let x = dt = Date.now();
+ window.setTimeout(() => dt === x && linkFollowUnhighlight(), 5000);
}
export class HighlightBrush {
@@ -693,11 +717,25 @@ export namespace Doc {
}
export function UnBrushAllDocs() {
- manager.BrushedDoc.clear();
+ brushManager.BrushedDoc.clear();
+ }
+
+ export function setChildLayout(target: Doc, source?: Doc) {
+ target.childLayout = source && source.isTemplateDoc ? source : source &&
+ source.dragFactory instanceof Doc && source.dragFactory.isTemplateDoc ? source.dragFactory :
+ source && source.layout instanceof Doc && source.layout.isTemplateDoc ? source.layout : undefined;
}
}
+
+
Scripting.addGlobal(function renameAlias(doc: any, n: any) { return StrCast(Doc.GetProto(doc).title).replace(/\([0-9]*\)/, "") + `(${n})`; });
Scripting.addGlobal(function getProto(doc: any) { return Doc.GetProto(doc); });
+Scripting.addGlobal(function setChildLayout(target: any, source: any) { Doc.setChildLayout(target, source); });
+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 docList(field: any) { return DocListCast(field); }); \ No newline at end of file
+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 undo() { return UndoManager.Undo(); });
+Scripting.addGlobal(function redo() { return UndoManager.Redo(); }); \ No newline at end of file
diff --git a/src/new_fields/InkField.ts b/src/new_fields/InkField.ts
index 8f64c1c2e..d94834e91 100644
--- a/src/new_fields/InkField.ts
+++ b/src/new_fields/InkField.ts
@@ -8,7 +8,8 @@ export enum InkTool {
None,
Pen,
Highlighter,
- Eraser
+ Eraser,
+ Scrubber
}
export interface StrokeData {
@@ -16,7 +17,8 @@ export interface StrokeData {
color: string;
width: string;
tool: InkTool;
- page: number;
+ displayTimecode: number;
+ creationTime: number;
}
export type InkData = Map<string, StrokeData>;
diff --git a/src/new_fields/RichTextField.ts b/src/new_fields/RichTextField.ts
index 9af0d94ff..499d7b949 100644
--- a/src/new_fields/RichTextField.ts
+++ b/src/new_fields/RichTextField.ts
@@ -4,11 +4,6 @@ import { Deserializable } from "../client/util/SerializationHelper";
import { Copy, ToScriptString } from "./FieldSymbols";
import { scriptingGlobal } from "../client/util/Scripting";
-export const ToPlainText = Symbol("PlainText");
-export const FromPlainText = Symbol("PlainText");
-const delimiter = "\n";
-const joiner = "";
-
@scriptingGlobal
@Deserializable("RichTextField")
export class RichTextField extends ObjectField {
@@ -29,48 +24,4 @@ export class RichTextField extends ObjectField {
return `new RichTextField("${this.Data}")`;
}
- public static Initialize = (initial: string) => {
- !initial.length && (initial = " ");
- let pos = initial.length + 1;
- return `{"doc":{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"${initial}"}]}]},"selection":{"type":"text","anchor":${pos},"head":${pos}}}`;
- }
-
- [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);
- }
-
- [FromPlainText](plainText: string) {
- // Remap the text, creating blocks split on newlines
- let elements = plainText.split(delimiter);
-
- // Google Docs adds in an extra carriage return automatically, so this counteracts it
- !elements[elements.length - 1].length && elements.pop();
-
- // Preserve the current state, but re-write the content to be the blocks
- let parsed = JSON.parse(this.Data);
- parsed.doc.content = elements.map(text => {
- let paragraph: any = { type: "paragraph" };
- text.length && (paragraph.content = [{ type: "text", marks: [], text }]); // An empty paragraph gets treated as a line break
- return paragraph;
- });
-
- // If the new content is shorter than the previous content and selection is unchanged, may throw an out of bounds exception, so we reset it
- parsed.selection = { type: "text", anchor: 1, head: 1 };
-
- // Export the ProseMirror-compatible state object we've jsut built
- return JSON.stringify(parsed);
- }
-
} \ No newline at end of file
diff --git a/src/new_fields/RichTextUtils.ts b/src/new_fields/RichTextUtils.ts
new file mode 100644
index 000000000..601939ed2
--- /dev/null
+++ b/src/new_fields/RichTextUtils.ts
@@ -0,0 +1,519 @@
+import { EditorState, Transaction, TextSelection } from "prosemirror-state";
+import { Node, Fragment, Mark, MarkType } from "prosemirror-model";
+import { RichTextField } from "./RichTextField";
+import { docs_v1 } from "googleapis";
+import { GoogleApiClientUtils } from "../client/apis/google_docs/GoogleApiClientUtils";
+import { FormattedTextBox } from "../client/views/nodes/FormattedTextBox";
+import { Opt, Doc } from "./Doc";
+import Color = require('color');
+import { sinkListItem } from "prosemirror-schema-list";
+import { Utils } from "../Utils";
+import { RouteStore } from "../server/RouteStore";
+import { Docs } from "../client/documents/Documents";
+import { schema } from "../client/util/RichTextSchema";
+import { GooglePhotos } from "../client/apis/google_docs/GooglePhotosClientUtils";
+import { DocServer } from "../client/DocServer";
+import { Cast, StrCast } from "./Types";
+import { Id } from "./FieldSymbols";
+import { DocumentView } from "../client/views/nodes/DocumentView";
+import { AssertionError } from "assert";
+import { Identified } from "../client/Network";
+
+export namespace RichTextUtils {
+
+ const delimiter = "\n";
+ const joiner = "";
+
+
+ export const Initialize = (initial?: string) => {
+ let content: any[] = [];
+ let state = {
+ doc: {
+ type: "doc",
+ content,
+ },
+ selection: {
+ type: "text",
+ anchor: 0,
+ head: 0
+ }
+ };
+ if (initial && initial.length) {
+ content.push({
+ type: "paragraph",
+ content: {
+ type: "text",
+ text: initial
+ }
+ });
+ state.selection.anchor = state.selection.head = initial.length + 1;
+ }
+ return JSON.stringify(state);
+ };
+
+ export const Synthesize = (plainText: string, oldState?: RichTextField) => {
+ return new RichTextField(ToProsemirrorState(plainText, oldState));
+ };
+
+ export const ToPlainText = (state: EditorState) => {
+ // Because we're working with plain text, just concatenate all paragraphs
+ let content = state.doc.content;
+ let paragraphs: Node<any>[] = [];
+ content.forEach(node => node.type.name === "paragraph" && paragraphs.push(node));
+
+ // Functions to flatten ProseMirror paragraph objects (and their components) to plain text
+ // Concatentate paragraphs and string the result together
+ let textParagraphs: string[] = paragraphs.map(paragraph => {
+ let text: string[] = [];
+ paragraph.content.forEach(node => node.text && text.push(node.text));
+ return text.join(joiner) + delimiter;
+ });
+ let plainText = textParagraphs.join(joiner);
+ return plainText.substring(0, plainText.length - 1);
+ };
+
+ export const ToProsemirrorState = (plainText: string, oldState?: RichTextField) => {
+ // Remap the text, creating blocks split on newlines
+ let elements = plainText.split(delimiter);
+
+ // Google Docs adds in an extra carriage return automatically, so this counteracts it
+ !elements[elements.length - 1].length && elements.pop();
+
+ // Preserve the current state, but re-write the content to be the blocks
+ let parsed = JSON.parse(oldState ? oldState.Data : Initialize());
+ parsed.doc.content = elements.map(text => {
+ let paragraph: any = { type: "paragraph" };
+ text.length && (paragraph.content = [{ type: "text", marks: [], text }]); // An empty paragraph gets treated as a line break
+ return paragraph;
+ });
+
+ // If the new content is shorter than the previous content and selection is unchanged, may throw an out of bounds exception, so we reset it
+ parsed.selection = { type: "text", anchor: 1, head: 1 };
+
+ // Export the ProseMirror-compatible state object we've just built
+ return JSON.stringify(parsed);
+ };
+
+ export namespace GoogleDocs {
+
+ export const Export = async (state: EditorState): Promise<GoogleApiClientUtils.Docs.Content> => {
+ const nodes: (Node<any> | null)[] = [];
+ let text = ToPlainText(state);
+ state.doc.content.forEach(node => {
+ if (!node.childCount) {
+ nodes.push(null);
+ } else {
+ node.content.forEach(child => nodes.push(child));
+ }
+ });
+ const requests = await marksToStyle(nodes);
+ return { text, requests };
+ };
+
+ interface ImageTemplate {
+ width: number;
+ title: string;
+ url: string;
+ }
+
+ const parseInlineObjects = async (document: docs_v1.Schema$Document): Promise<Map<string, ImageTemplate>> => {
+ const inlineObjectMap = new Map<string, ImageTemplate>();
+ const inlineObjects = document.inlineObjects;
+
+ if (inlineObjects) {
+ 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 };
+ });
+
+ const uploads = await Identified.PostToServer(RouteStore.googlePhotosMediaDownload, { mediaItems });
+
+ if (uploads.length !== mediaItems.length) {
+ throw new AssertionError({ expected: mediaItems.length, actual: uploads.length, message: "Error with internally uploading inlineObjects!" });
+ }
+
+ for (let i = 0; i < objects.length; i++) {
+ const object = objects[i];
+ const { fileNames } = uploads[i];
+ 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
+ });
+ }
+ }
+ return inlineObjectMap;
+ };
+
+ type BulletPosition = { value: number, sinks: number };
+
+ interface MediaItem {
+ baseUrl: string;
+ filename: string;
+ }
+
+ export const Import = async (documentId: GoogleApiClientUtils.Docs.DocumentId, textNote: Doc): Promise<Opt<GoogleApiClientUtils.Docs.ImportResult>> => {
+ const document = await GoogleApiClientUtils.Docs.retrieve({ documentId });
+ if (!document) {
+ return undefined;
+ }
+ const inlineObjectMap = await parseInlineObjects(document);
+ const title = document.title!;
+ const { text, paragraphs } = GoogleApiClientUtils.Docs.Utils.extractText(document);
+ let state = FormattedTextBox.blankState();
+ let structured = parseLists(paragraphs);
+
+ let position = 3;
+ let lists: ListGroup[] = [];
+ const indentMap = new Map<ListGroup, BulletPosition[]>();
+ let globalOffset = 0;
+ const nodes: Node<any>[] = [];
+ for (let element of structured) {
+ if (Array.isArray(element)) {
+ lists.push(element);
+ let positions: BulletPosition[] = [];
+ let items = element.map(paragraph => {
+ let item = listItem(state.schema, paragraph.contents);
+ let sinks = paragraph.bullet!;
+ positions.push({
+ value: position + globalOffset,
+ sinks
+ });
+ position += item.nodeSize;
+ globalOffset += 2 * sinks;
+ return item;
+ });
+ indentMap.set(element, positions);
+ nodes.push(list(state.schema, items));
+ } else {
+ if (element.contents.some(child => "inlineObjectId" in child)) {
+ const group = element.contents;
+ group.forEach((child, i) => {
+ let node: Opt<Node<any>>;
+ if ("inlineObjectId" in child) {
+ node = imageNode(state.schema, inlineObjectMap.get(child.inlineObjectId!)!, textNote);
+ } else if ("content" in child && (i !== group.length - 1 || child.content!.removeTrailingNewlines().length)) {
+ node = paragraphNode(state.schema, [child]);
+ }
+ if (node) {
+ position += node.nodeSize;
+ nodes.push(node);
+ }
+ });
+ } else {
+ let paragraph = paragraphNode(state.schema, element.contents);
+ nodes.push(paragraph);
+ position += paragraph.nodeSize;
+ }
+ }
+ }
+ state = state.apply(state.tr.replaceWith(0, 2, nodes));
+
+ let sink = sinkListItem(state.schema.nodes.list_item);
+ let dispatcher = (tr: Transaction) => state = state.apply(tr);
+ for (let list of lists) {
+ for (let pos of indentMap.get(list)!) {
+ let resolved = state.doc.resolve(pos.value);
+ state = state.apply(state.tr.setSelection(new TextSelection(resolved)));
+ for (let i = 0; i < pos.sinks; i++) {
+ sink(state, dispatcher);
+ }
+ }
+ }
+
+ return { title, text, state };
+ };
+
+ type Paragraph = GoogleApiClientUtils.Docs.Utils.DeconstructedParagraph;
+ type ListGroup = Paragraph[];
+ type PreparedParagraphs = (ListGroup | Paragraph)[];
+
+ const parseLists = (paragraphs: ListGroup) => {
+ let groups: PreparedParagraphs = [];
+ let group: ListGroup = [];
+ for (let paragraph of paragraphs) {
+ if (paragraph.bullet !== undefined) {
+ group.push(paragraph);
+ } else {
+ if (group.length) {
+ groups.push(group);
+ group = [];
+ }
+ groups.push(paragraph);
+ }
+ }
+ group.length && groups.push(group);
+ return groups;
+ };
+
+ const listItem = (schema: any, runs: docs_v1.Schema$TextRun[]): Node => {
+ return schema.node("list_item", null, paragraphNode(schema, runs));
+ };
+
+ const list = (schema: any, items: Node[]): Node => {
+ return schema.node("bullet_list", null, items);
+ };
+
+ const paragraphNode = (schema: any, runs: docs_v1.Schema$TextRun[]): Node => {
+ let children = runs.map(run => textNode(schema, run)).filter(child => child !== undefined);
+ let fragment = children.length ? Fragment.from(children) : undefined;
+ return schema.node("paragraph", null, fragment);
+ };
+
+ const imageNode = (schema: any, image: ImageTemplate, textNote: Doc) => {
+ const { url: src, width } = image;
+ let docid: string;
+ const guid = Utils.GenerateDeterministicGuid(src);
+ const backingDocId = StrCast(textNote[guid]);
+ if (!backingDocId) {
+ const backingDoc = Docs.Create.ImageDocument(src, { width: 300, height: 300 });
+ DocumentView.makeCustomViewClicked(backingDoc, undefined);
+ docid = backingDoc[Id];
+ textNote[guid] = docid;
+ } else {
+ docid = backingDocId;
+ }
+ return schema.node("image", { src, width, docid, float: null, location: "onRight" });
+ };
+
+ const textNode = (schema: any, run: docs_v1.Schema$TextRun) => {
+ let text = run.content!.removeTrailingNewlines();
+ return text.length ? schema.text(text, styleToMarks(schema, run.textStyle)) : undefined;
+ };
+
+ const StyleToMark = new Map<keyof docs_v1.Schema$TextStyle, keyof typeof schema.marks>([
+ ["bold", "strong"],
+ ["italic", "em"],
+ ["foregroundColor", "pFontColor"],
+ ["fontSize", "pFontSize"]
+ ]);
+
+ const styleToMarks = (schema: any, textStyle?: docs_v1.Schema$TextStyle) => {
+ if (!textStyle) {
+ return undefined;
+ }
+ let marks: Mark[] = [];
+ Object.keys(textStyle).forEach(key => {
+ let value: any;
+ let targeted = key as keyof docs_v1.Schema$TextStyle;
+ if (value = textStyle[targeted]) {
+ let attributes: any = {};
+ let converted = StyleToMark.get(targeted) || targeted;
+
+ value.url && (attributes.href = value.url);
+ if (value.color) {
+ let object = value.color.rgbColor;
+ attributes.color = Color.rgb(["red", "green", "blue"].map(color => object[color] * 255 || 0)).hex();
+ }
+ if (value.magnitude) {
+ attributes.fontSize = value.magnitude;
+ }
+
+ if (converted === "weightedFontFamily") {
+ converted = ImportFontFamilyMapping.get(value.fontFamily) || "timesNewRoman";
+ }
+
+ let mapped = schema.marks[converted];
+ if (!mapped) {
+ alert(`No mapping found for ${converted}!`);
+ return;
+ }
+
+ let mark = schema.mark(mapped, attributes);
+ mark && marks.push(mark);
+ }
+ });
+ return marks;
+ };
+
+ const MarkToStyle = new Map<keyof typeof schema.marks, keyof docs_v1.Schema$TextStyle>([
+ ["strong", "bold"],
+ ["em", "italic"],
+ ["pFontColor", "foregroundColor"],
+ ["pFontSize", "fontSize"],
+ ["timesNewRoman", "weightedFontFamily"],
+ ["georgia", "weightedFontFamily"],
+ ["comicSans", "weightedFontFamily"],
+ ["tahoma", "weightedFontFamily"],
+ ["impact", "weightedFontFamily"]
+ ]);
+
+ const ExportFontFamilyMapping = new Map<string, string>([
+ ["timesNewRoman", "Times New Roman"],
+ ["arial", "Arial"],
+ ["georgia", "Georgia"],
+ ["comicSans", "Comic Sans MS"],
+ ["tahoma", "Tahoma"],
+ ["impact", "Impact"]
+ ]);
+
+ const ImportFontFamilyMapping = new Map<string, string>([
+ ["Times New Roman", "timesNewRoman"],
+ ["Arial", "arial"],
+ ["Georgia", "georgia"],
+ ["Comic Sans MS", "comicSans"],
+ ["Tahoma", "tahoma"],
+ ["Impact", "impact"]
+ ]);
+
+ const ignored = ["user_mark"];
+
+ const marksToStyle = async (nodes: (Node<any> | null)[]): Promise<docs_v1.Schema$Request[]> => {
+ let requests: docs_v1.Schema$Request[] = [];
+ let position = 1;
+ for (let node of nodes) {
+ if (node === null) {
+ position += 2;
+ continue;
+ }
+ const { marks, attrs, nodeSize } = node;
+ const textStyle: docs_v1.Schema$TextStyle = {};
+ const information: LinkInformation = {
+ startIndex: position,
+ endIndex: position + nodeSize,
+ textStyle
+ };
+ let mark: Mark<any>;
+ const markMap = BuildMarkMap(marks);
+ for (let markName of Object.keys(schema.marks)) {
+ if (ignored.includes(markName) || !(mark = markMap[markName])) {
+ continue;
+ }
+ let converted = MarkToStyle.get(markName) || markName as keyof docs_v1.Schema$TextStyle;
+ let value: any = true;
+ if (!converted) {
+ continue;
+ }
+ const { attrs } = mark;
+ switch (converted) {
+ case "link":
+ let url = attrs.href;
+ const delimiter = "/doc/";
+ const alreadyShared = "?sharing=true";
+ if (new RegExp(window.location.origin + delimiter).test(url) && !url.endsWith(alreadyShared)) {
+ const linkDoc = await DocServer.GetRefField(url.split(delimiter)[1]);
+ if (linkDoc instanceof Doc) {
+ let exported = (await Cast(linkDoc.anchor2, Doc))!;
+ if (!exported.customLayout) {
+ exported = Doc.MakeAlias(exported);
+ DocumentView.makeCustomViewClicked(exported, undefined);
+ linkDoc.anchor2 = exported;
+ }
+ url = Utils.shareUrl(exported[Id]);
+ }
+ }
+ value = { url };
+ textStyle.foregroundColor = fromRgb.blue;
+ textStyle.bold = true;
+ break;
+ case "fontSize":
+ value = { magnitude: attrs.fontSize, unit: "PT" };
+ break;
+ case "foregroundColor":
+ value = fromHex(attrs.color);
+ break;
+ case "weightedFontFamily":
+ value = { fontFamily: ExportFontFamilyMapping.get(markName) };
+ }
+ let matches: RegExpExecArray | null;
+ if ((matches = /p(\d+)/g.exec(markName)) !== null) {
+ converted = "fontSize";
+ value = { magnitude: parseInt(matches[1].replace("px", "")), unit: "PT" };
+ }
+ textStyle[converted] = value;
+ }
+ if (Object.keys(textStyle).length) {
+ requests.push(EncodeStyleUpdate(information));
+ }
+ if (node.type.name === "image") {
+ const width = attrs.width;
+ requests.push(await EncodeImage({
+ startIndex: position + nodeSize - 1,
+ uri: attrs.src,
+ width: Number(typeof width === "string" ? width.replace("px", "") : width)
+ }));
+ }
+ position += nodeSize;
+ }
+ return requests;
+ };
+
+ const BuildMarkMap = (marks: Mark<any>[]) => {
+ const markMap: { [type: string]: Mark<any> } = {};
+ marks.forEach(mark => markMap[mark.type.name] = mark);
+ return markMap;
+ };
+
+ interface LinkInformation {
+ startIndex: number;
+ endIndex: number;
+ textStyle: docs_v1.Schema$TextStyle;
+ }
+
+ interface ImageInformation {
+ startIndex: number;
+ width: number;
+ uri: string;
+ }
+
+ namespace fromRgb {
+
+ export const convert = (red: number, green: number, blue: number): docs_v1.Schema$OptionalColor => {
+ return {
+ color: {
+ rgbColor: {
+ red: red / 255,
+ green: green / 255,
+ blue: blue / 255
+ }
+ }
+ };
+ };
+
+ export const red = convert(255, 0, 0);
+ export const green = convert(0, 255, 0);
+ export const blue = convert(0, 0, 255);
+
+ }
+
+ const fromHex = (color: string): docs_v1.Schema$OptionalColor => {
+ const c = Color(color);
+ return fromRgb.convert(c.red(), c.green(), c.blue());
+ };
+
+ const EncodeStyleUpdate = (information: LinkInformation): docs_v1.Schema$Request => {
+ const { startIndex, endIndex, textStyle } = information;
+ return {
+ updateTextStyle: {
+ fields: "*",
+ range: { startIndex, endIndex },
+ textStyle
+ } as docs_v1.Schema$UpdateTextStyleRequest
+ };
+ };
+
+ const EncodeImage = async (information: ImageInformation) => {
+ const source = [Docs.Create.ImageDocument(information.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 }
+ }
+ };
+ }
+ return {};
+ };
+ }
+
+} \ No newline at end of file
diff --git a/src/new_fields/SchemaHeaderField.ts b/src/new_fields/SchemaHeaderField.ts
index 7494c9bd1..92d0aec9a 100644
--- a/src/new_fields/SchemaHeaderField.ts
+++ b/src/new_fields/SchemaHeaderField.ts
@@ -105,5 +105,4 @@ export class SchemaHeaderField extends ObjectField {
[ToScriptString]() {
return `invalid`;
}
-}
-
+} \ No newline at end of file
diff --git a/src/new_fields/URLField.ts b/src/new_fields/URLField.ts
index b9ad96450..35ef6dd02 100644
--- a/src/new_fields/URLField.ts
+++ b/src/new_fields/URLField.ts
@@ -38,6 +38,8 @@ export abstract class URLField extends ObjectField {
}
}
+export const nullAudio = "https://actions.google.com/sounds/v1/alarms/beep_short.ogg";
+
@scriptingGlobal @Deserializable("audio") export class AudioField extends URLField { }
@scriptingGlobal @Deserializable("image") export class ImageField extends URLField { }
@scriptingGlobal @Deserializable("video") export class VideoField extends URLField { }
diff --git a/src/new_fields/documentSchemas.ts b/src/new_fields/documentSchemas.ts
new file mode 100644
index 000000000..e2730914f
--- /dev/null
+++ b/src/new_fields/documentSchemas.ts
@@ -0,0 +1,59 @@
+import { makeInterface, createSchema, listSpec } from "./Schema";
+import { ScriptField } from "./ScriptField";
+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 layoutCustom as an example)
+ layoutKey: "string", // holds the field key for the field that actually holds the current lyoat
+ layoutCustom: 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)
+ 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", // "
+ color: "string", // foreground color of document
+ backgroundColor: "string", // background color of document
+ opacity: "number", // opacity of document
+ creationDate: DateField, // when the document was created
+ links: listSpec(Doc), // computed (readonly) list of links associated with this document
+ dropAction: "string", // override specifying what should happen when this document is dropped (can be "alias" or "copy")
+ removeDropProperties: listSpec("string"), // properties that should be removed from the alias/copy/etc of this document when it is dropped
+ onClick: 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
+ isTemplateField: "boolean", // whether this document acts as a template layout for describing how other documents should be displayed
+ isBackground: "boolean", // whether document is a background element and ignores input events (can only selet with marquee)
+ type: "string", // enumerated type of 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)
+ 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 spatially manipulated
+ inOverlay: "boolean", // whether the document is rendered in an OverlayView which handles selection/dragging differently
+ 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", // whether an editable title banner is displayed at tht top of the document
+ 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: "boolean", // whether the document is in the midst of animating between two layouts (used by icons to de/iconify documents).
+ 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.
+});
+
+export const positionSchema = createSchema({
+ zIndex: "number",
+ x: "number",
+ y: "number",
+ z: "number",
+});
+
+export type Document = makeInterface<[typeof documentSchema]>;
+export const Document = makeInterface(documentSchema);
+
+export type PositionDocument = makeInterface<[typeof documentSchema, typeof positionSchema]>;
+export const PositionDocument = makeInterface(documentSchema, positionSchema);
diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts
new file mode 100644
index 000000000..46d897339
--- /dev/null
+++ b/src/server/DashUploadUtils.ts
@@ -0,0 +1,200 @@
+import * as fs 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 { Opt } from '../new_fields/Doc';
+
+const uploadDirectory = path.join(__dirname, './public/files/');
+
+export namespace DashUploadUtils {
+
+ export interface Size {
+ width: number;
+ suffix: string;
+ }
+
+ export const Sizes: { [size: string]: Size } = {
+ SMALL: { width: 100, suffix: "_s" },
+ MEDIUM: { width: 400, suffix: "_m" },
+ LARGE: { width: 900, suffix: "_l" },
+ };
+
+ const gifs = [".gif"];
+ const pngs = [".png"];
+ const jpgs = [".jpg", ".jpeg"];
+ export const imageFormats = [...pngs, ...jpgs, ...gifs];
+ const videoFormats = [".mov", ".mp4"];
+
+ const size = "content-length";
+ const type = "content-type";
+
+ export interface UploadInformation {
+ mediaPaths: string[];
+ fileNames: { [key: string]: string };
+ exifData: EnrichedExifData;
+ contentSize?: number;
+ contentType?: string;
+ }
+
+ const generate = (prefix: string, url: string) => `${prefix}upload_${Utils.GenerateGuid()}${sanitizeExtension(url)}`;
+ const sanitize = (filename: string) => filename.replace(/\s+/g, "_");
+ const sanitizeExtension = (source: string) => {
+ let extension = path.extname(source);
+ extension = extension.toLowerCase();
+ extension = extension.split("?")[0];
+ return extension;
+ };
+
+ /**
+ * Uploads an image specified by the @param source to Dash's /public/files/
+ * directory, and returns information generated during that upload
+ *
+ * @param {string} source is either the absolute path of an already uploaded image or
+ * the url of a remote image
+ * @param {string} filename dictates what to call the image. If not specified,
+ * the name {@param prefix}_upload_{GUID}
+ * @param {string} prefix is a string prepended to the generated image name in the
+ * event that @param filename is not specified
+ *
+ * @returns {UploadInformation} This method returns
+ * 1) the paths to the uploaded images (plural due to resizing)
+ * 2) the file name of each of the resized images
+ * 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, prefix: string = ""): Promise<UploadInformation> => {
+ const metadata = await InspectImage(source);
+ return UploadInspectedImage(metadata, filename, prefix);
+ };
+
+ export interface InspectionResults {
+ isLocal: boolean;
+ stream: any;
+ normalizedUrl: string;
+ exifData: EnrichedExifData;
+ contentSize?: number;
+ contentType?: string;
+ }
+
+ export interface EnrichedExifData {
+ data: ExifData;
+ error?: string;
+ }
+
+ /**
+ * 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> => {
+ const { isLocal, stream, normalized: normalizedUrl } = classify(source);
+ const exifData = await parseExifData(source);
+ const results = {
+ exifData,
+ isLocal,
+ stream,
+ normalizedUrl
+ };
+ // stop here if local, since request.head() can't handle local paths, only urls on the web
+ if (isLocal) {
+ return results;
+ }
+ const metadata = (await new Promise<any>((resolve, reject) => {
+ request.head(source, async (error, res) => {
+ if (error) {
+ return reject(error);
+ }
+ resolve(res);
+ });
+ })).headers;
+ return {
+ contentSize: parseInt(metadata[size]),
+ contentType: metadata[type],
+ ...results
+ };
+ };
+
+ export const UploadInspectedImage = async (metadata: InspectionResults, filename?: string, prefix = ""): Promise<UploadInformation> => {
+ const { isLocal, stream, normalizedUrl, contentSize, contentType, exifData } = metadata;
+ const resolved = filename ? sanitize(filename) : generate(prefix, normalizedUrl);
+ const extension = sanitizeExtension(normalizedUrl || resolved);
+ let information: UploadInformation = {
+ mediaPaths: [],
+ fileNames: { clean: resolved },
+ exifData,
+ contentSize,
+ contentType,
+ };
+ return new Promise<UploadInformation>(async (resolve, reject) => {
+ const resizers = [
+ { resizer: sharp().rotate(), suffix: "_o" },
+ ...Object.values(Sizes).map(size => ({
+ resizer: sharp().resize(size.width, undefined, { withoutEnlargement: true }).rotate(),
+ suffix: size.suffix
+ }))
+ ];
+ let nonVisual = false;
+ 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());
+ } else if (![...imageFormats, ...videoFormats].includes(extension.toLowerCase())) {
+ nonVisual = true;
+ }
+ if (imageFormats.includes(extension)) {
+ for (let resizer of resizers) {
+ const suffix = resizer.suffix;
+ let mediaPath: string;
+ await new Promise<void>(resolve => {
+ const filename = resolved.substring(0, resolved.length - extension.length) + suffix + extension;
+ information.mediaPaths.push(mediaPath = uploadDirectory + filename);
+ information.fileNames[suffix] = filename;
+ stream(normalizedUrl).pipe(resizer.resizer).pipe(fs.createWriteStream(mediaPath))
+ .on('close', resolve)
+ .on('error', reject);
+ });
+ }
+ }
+ if (!isLocal || nonVisual) {
+ await new Promise<void>(resolve => {
+ stream(normalizedUrl).pipe(fs.createWriteStream(uploadDirectory + resolved)).on('close', resolve);
+ });
+ }
+ resolve(information);
+ });
+ };
+
+ const classify = (url: string) => {
+ const isLocal = /Dash-Web(\\|\/)src(\\|\/)server(\\|\/)public(\\|\/)files/g.test(url);
+ return {
+ isLocal,
+ stream: isLocal ? fs.createReadStream : request,
+ normalized: isLocal ? path.normalize(url) : url
+ };
+ };
+
+ const parseExifData = async (source: string): Promise<EnrichedExifData> => {
+ return new Promise<EnrichedExifData>(resolve => {
+ new ExifImage(source, (error, data) => {
+ let reason: Opt<string> = undefined;
+ if (error) {
+ reason = (error as any).code;
+ }
+ resolve({ data, error: reason });
+ });
+ });
+ };
+
+ export const createIfNotExists = async (path: string) => {
+ if (await new Promise<boolean>(resolve => fs.exists(path, resolve))) {
+ return true;
+ }
+ return new Promise<boolean>(resolve => fs.mkdir(path, error => resolve(error === null)));
+ };
+
+ export const Destroy = (mediaPath: string) => new Promise<boolean>(resolve => fs.unlink(mediaPath, error => resolve(error === null)));
+
+} \ No newline at end of file
diff --git a/src/server/Message.ts b/src/server/Message.ts
index 4ec390ade..aaee143e8 100644
--- a/src/server/Message.ts
+++ b/src/server/Message.ts
@@ -1,5 +1,4 @@
import { Utils } from "../Utils";
-import { google, docs_v1 } from "googleapis";
export class Message<T> {
private _name: string;
diff --git a/src/server/PdfTypes.ts b/src/server/PdfTypes.ts
new file mode 100644
index 000000000..e87f08e1d
--- /dev/null
+++ b/src/server/PdfTypes.ts
@@ -0,0 +1,21 @@
+export interface ParsedPDF {
+ numpages: number;
+ numrender: number;
+ info: PDFInfo;
+ metadata: PDFMetadata;
+ version: string; //https://mozilla.github.io/pdf.js/getting_started/
+ text: string;
+}
+
+export interface PDFInfo {
+ PDFFormatVersion: string;
+ IsAcroFormPresent: boolean;
+ IsXFAPresent: boolean;
+ [key: string]: any;
+}
+
+export interface PDFMetadata {
+ parse(): void;
+ get(name: string): string;
+ has(name: string): boolean;
+} \ No newline at end of file
diff --git a/src/server/RouteStore.ts b/src/server/RouteStore.ts
index 014906054..7426ffb39 100644
--- a/src/server/RouteStore.ts
+++ b/src/server/RouteStore.ts
@@ -13,6 +13,8 @@ export enum RouteStore {
upload = "/upload",
dataUriToImage = "/uploadURI",
images = "/images",
+ inspectImage = "/inspectImage",
+ imageHierarchyExport = "/imageHierarchyExport",
// USER AND WORKSPACES
getCurrUser = "/getCurrentUser",
@@ -31,6 +33,11 @@ export enum RouteStore {
// APIS
cognitiveServices = "/cognitiveservices",
- googleDocs = "/googleDocs"
+ googleDocs = "/googleDocs",
+ readGoogleAccessToken = "/readGoogleAccessToken",
+ writeGoogleAccessToken = "/writeGoogleAccessToken",
+ googlePhotosMediaUpload = "/googlePhotosMediaUpload",
+ googlePhotosMediaDownload = "/googlePhotosMediaDownload",
+ googleDocsGet = "/googleDocsGet"
} \ No newline at end of file
diff --git a/src/server/RouteSubscriber.ts b/src/server/RouteSubscriber.ts
new file mode 100644
index 000000000..e49be8af5
--- /dev/null
+++ b/src/server/RouteSubscriber.ts
@@ -0,0 +1,26 @@
+export default class RouteSubscriber {
+ private _root: string;
+ private requestParameters: string[] = [];
+
+ constructor(root: string) {
+ this._root = root;
+ }
+
+ add(...parameters: string[]) {
+ this.requestParameters.push(...parameters);
+ return this;
+ }
+
+ public get root() {
+ return this._root;
+ }
+
+ public get build() {
+ let output = this._root;
+ if (this.requestParameters.length) {
+ output = `${output}/:${this.requestParameters.join("/:")}`;
+ }
+ return output;
+ }
+
+} \ No newline at end of file
diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts
index 8785cd974..5714c9928 100644
--- a/src/server/apis/google/GoogleApiServerUtils.ts
+++ b/src/server/apis/google/GoogleApiServerUtils.ts
@@ -1,11 +1,14 @@
-import { google, docs_v1, slides_v1 } from "googleapis";
+import { google } from "googleapis";
import { createInterface } from "readline";
import { readFile, writeFile } from "fs";
-import { OAuth2Client } from "google-auth-library";
+import { OAuth2Client, Credentials } from "google-auth-library";
import { Opt } from "../../../new_fields/Doc";
import { GlobalOptions } from "googleapis-common";
import { GaxiosResponse } from "gaxios";
-
+import request = require('request-promise');
+import * as qs from 'query-string';
+import Photos = require('googlephotos');
+import { Database } from "../../database";
/**
* Server side authentication for Google Api queries.
*/
@@ -20,6 +23,10 @@ export namespace GoogleApiServerUtils {
'presentations.readonly',
'drive',
'drive.file',
+ 'photoslibrary',
+ 'photoslibrary.appendonly',
+ 'photoslibrary.sharing',
+ 'userinfo.profile'
];
export const parseBuffer = (data: Buffer) => JSON.parse(data.toString());
@@ -29,102 +36,186 @@ export namespace GoogleApiServerUtils {
Slides = "Slides"
}
-
- export interface CredentialPaths {
- credentials: string;
- token: string;
+ export interface CredentialInformation {
+ credentialsPath: string;
+ userId: string;
}
export type ApiResponse = Promise<GaxiosResponse>;
- export type ApiRouter = (endpoint: Endpoint, paramters: any) => ApiResponse;
- export type ApiHandler = (parameters: any) => ApiResponse;
+ export type ApiRouter = (endpoint: Endpoint, parameters: any) => ApiResponse;
+ export type ApiHandler = (parameters: any, methodOptions?: any) => ApiResponse;
export type Action = "create" | "retrieve" | "update";
export type Endpoint = { get: ApiHandler, create: ApiHandler, batchUpdate: ApiHandler };
export type EndpointParameters = GlobalOptions & { version: "v1" };
- export const GetEndpoint = async (sector: string, paths: CredentialPaths) => {
- return new Promise<Opt<Endpoint>>((resolve, reject) => {
- readFile(paths.credentials, (err, credentials) => {
+ export const GetEndpoint = (sector: string, paths: CredentialInformation) => {
+ return new Promise<Opt<Endpoint>>(resolve => {
+ RetrieveCredentials(paths).then(authentication => {
+ let routed: Opt<Endpoint>;
+ let parameters: EndpointParameters = { auth: authentication.client, version: "v1" };
+ switch (sector) {
+ case Service.Documents:
+ routed = google.docs(parameters).documents;
+ break;
+ case Service.Slides:
+ routed = google.slides(parameters).presentations;
+ break;
+ }
+ resolve(routed);
+ });
+ });
+ };
+
+ export const RetrieveAccessToken = (information: CredentialInformation) => {
+ return new Promise<string>((resolve, reject) => {
+ RetrieveCredentials(information).then(
+ credentials => resolve(credentials.token.access_token!),
+ error => reject(`Error: unable to authenticate Google Photos API request.\n${error}`)
+ );
+ });
+ };
+
+ const RetrieveOAuthClient = async (information: CredentialInformation) => {
+ return new Promise<OAuth2Client>((resolve, reject) => {
+ readFile(information.credentialsPath, async (err, credentials) => {
if (err) {
reject(err);
return console.log('Error loading client secret file:', err);
}
- return authorize(parseBuffer(credentials), paths.token).then(auth => {
- let routed: Opt<Endpoint>;
- let parameters: EndpointParameters = { auth, version: "v1" };
- switch (sector) {
- case Service.Documents:
- routed = google.docs(parameters).documents;
- break;
- case Service.Slides:
- routed = google.slides(parameters).presentations;
- break;
- }
- resolve(routed);
- });
+ const { client_secret, client_id, redirect_uris } = parseBuffer(credentials).installed;
+ resolve(new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]));
});
});
};
+ export const GenerateAuthenticationUrl = async (information: CredentialInformation) => {
+ const client = await RetrieveOAuthClient(information);
+ return client.generateAuthUrl({
+ access_type: 'offline',
+ scope: SCOPES.map(relative => prefix + relative),
+ });
+ };
+
+ export interface GoogleAuthenticationResult {
+ access_token: string;
+ avatar: string;
+ name: string;
+ }
+ export const ProcessClientSideCode = async (information: CredentialInformation, authenticationCode: string): Promise<GoogleAuthenticationResult> => {
+ const oAuth2Client = await RetrieveOAuthClient(information);
+ return new Promise<GoogleAuthenticationResult>((resolve, reject) => {
+ oAuth2Client.getToken(authenticationCode, async (err, token) => {
+ if (err || !token) {
+ reject(err);
+ return console.error('Error retrieving access token', err);
+ }
+ oAuth2Client.setCredentials(token);
+ const enriched = injectUserInfo(token);
+ await Database.Auxiliary.GoogleAuthenticationToken.Write(information.userId, enriched);
+ const { given_name, picture } = enriched.userInfo;
+ resolve({
+ access_token: enriched.access_token!,
+ avatar: picture,
+ name: given_name
+ });
+ });
+ });
+ };
/**
- * Create an OAuth2 client with the given credentials, and returns the promise resolving to the authenticated client
- * @param {Object} credentials The authorization client credentials.
+ * It's pretty cool: the credentials id_token is split into thirds by periods.
+ * The middle third contains a base64-encoded JSON string with all the
+ * user info contained in the interface below. So, we isolate that middle third,
+ * base64 decode with atob and parse the JSON.
+ * @param credentials the client credentials returned from OAuth after the user
+ * has executed the authentication routine
*/
- export function authorize(credentials: any, token_path: string): Promise<OAuth2Client> {
- const { client_secret, client_id, redirect_uris } = credentials.installed;
- const oAuth2Client = new google.auth.OAuth2(
- client_id, client_secret, redirect_uris[0]);
+ const injectUserInfo = (credentials: Credentials): EnrichedCredentials => {
+ const userInfo = JSON.parse(atob(credentials.id_token!.split(".")[1]));
+ return { ...credentials, userInfo };
+ };
- return new Promise<OAuth2Client>((resolve, reject) => {
- readFile(token_path, (err, token) => {
- // Check if we have previously stored a token.
+ export type EnrichedCredentials = Credentials & { userInfo: UserInfo };
+ export interface UserInfo {
+ at_hash: string;
+ aud: string;
+ azp: string;
+ exp: number;
+ family_name: string;
+ given_name: string;
+ iat: number;
+ iss: string;
+ locale: string;
+ name: string;
+ picture: string;
+ sub: string;
+ }
+
+ export const RetrieveCredentials = (information: CredentialInformation) => {
+ return new Promise<TokenResult>((resolve, reject) => {
+ readFile(information.credentialsPath, async (err, credentials) => {
if (err) {
- return getNewToken(oAuth2Client, token_path).then(resolve, reject);
+ reject(err);
+ return console.log('Error loading client secret file:', err);
}
- oAuth2Client.setCredentials(parseBuffer(token));
- resolve(oAuth2Client);
+ authorize(parseBuffer(credentials), information.userId).then(resolve, reject);
});
});
- }
+ };
+
+ export const RetrievePhotosEndpoint = (paths: CredentialInformation) => {
+ return new Promise<any>((resolve, reject) => {
+ RetrieveAccessToken(paths).then(
+ token => resolve(new Photos(token)),
+ reject
+ );
+ });
+ };
+ type TokenResult = { token: Credentials, client: OAuth2Client };
/**
- * Get and store new token after prompting for user authorization, and then
- * execute the given callback with the authorized OAuth2 client.
- * @param {google.auth.OAuth2} oAuth2Client The OAuth2 client to get token for.
- * @param {getEventsCallback} callback The callback for the authorized client.
+ * Create an OAuth2 client with the given credentials, and returns the promise resolving to the authenticated client
+ * @param {Object} credentials The authorization client credentials.
*/
- function getNewToken(oAuth2Client: OAuth2Client, token_path: string) {
- return new Promise<OAuth2Client>((resolve, reject) => {
- const authUrl = oAuth2Client.generateAuthUrl({
- access_type: 'offline',
- scope: SCOPES.map(relative => prefix + relative),
- });
- console.log('Authorize this app by visiting this url:', authUrl);
- const rl = createInterface({
- input: process.stdin,
- output: process.stdout,
- });
- rl.question('Enter the code from that page here: ', (code) => {
- rl.close();
- oAuth2Client.getToken(code, (err, token) => {
- if (err || !token) {
- reject(err);
- return console.error('Error retrieving access token', err);
- }
- oAuth2Client.setCredentials(token);
- // Store the token to disk for later program executions
- writeFile(token_path, JSON.stringify(token), (err) => {
- if (err) {
- console.error(err);
- reject(err);
- }
- console.log('Token stored to', token_path);
- });
- resolve(oAuth2Client);
- });
+ export function authorize(credentials: any, userId: string): Promise<TokenResult> {
+ const { client_secret, client_id, redirect_uris } = credentials.installed;
+ const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]);
+ return new Promise<TokenResult>((resolve, reject) => {
+ // Attempting to authorize user (${userId})
+ Database.Auxiliary.GoogleAuthenticationToken.Fetch(userId).then(token => {
+ if (token!.expiry_date! < new Date().getTime()) {
+ // Token has expired, so submitting a request for a refreshed access token
+ return refreshToken(token!, client_id, client_secret, oAuth2Client, userId).then(resolve, reject);
+ }
+ // Authentication successful!
+ oAuth2Client.setCredentials(token!);
+ resolve({ token: token!, client: oAuth2Client });
});
});
}
+
+ const refreshEndpoint = "https://oauth2.googleapis.com/token";
+ const refreshToken = (credentials: Credentials, client_id: string, client_secret: string, oAuth2Client: OAuth2Client, userId: string) => {
+ return new Promise<TokenResult>(resolve => {
+ let headerParameters = { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } };
+ let queryParameters = {
+ refreshToken: credentials.refresh_token,
+ client_id,
+ client_secret,
+ grant_type: "refresh_token"
+ };
+ let url = `${refreshEndpoint}?${qs.stringify(queryParameters)}`;
+ request.post(url, headerParameters).then(async response => {
+ let { access_token, expires_in } = JSON.parse(response);
+ const expiry_date = new Date().getTime() + (expires_in * 1000);
+ await Database.Auxiliary.GoogleAuthenticationToken.Update(userId, access_token, expiry_date);
+ credentials.access_token = access_token;
+ credentials.expiry_date = expiry_date;
+ oAuth2Client.setCredentials(credentials);
+ resolve({ token: credentials, client: oAuth2Client });
+ });
+ });
+ };
+
} \ No newline at end of file
diff --git a/src/server/apis/google/GooglePhotosUploadUtils.ts b/src/server/apis/google/GooglePhotosUploadUtils.ts
index 35f986250..36256822c 100644
--- a/src/server/apis/google/GooglePhotosUploadUtils.ts
+++ b/src/server/apis/google/GooglePhotosUploadUtils.ts
@@ -1,12 +1,11 @@
+
import request = require('request-promise');
import { GoogleApiServerUtils } from './GoogleApiServerUtils';
-import * as fs from 'fs';
-import { Utils } from '../../../Utils';
import * as path from 'path';
-import { Opt } from '../../../new_fields/Doc';
-import * as sharp from 'sharp';
-
-const uploadDirectory = path.join(__dirname, "../../public/files/");
+import { MediaItemCreationResult, NewMediaItemResult } from './SharedTypes';
+import { NewMediaItem } from "../../index";
+import { BatchedArray, TimeUnit } from 'array-batcher';
+import { DashUploadUtils } from '../../DashUploadUtils';
export namespace GooglePhotosUploadUtils {
@@ -28,16 +27,16 @@ export namespace GooglePhotosUploadUtils {
});
let Bearer: string;
- let Paths: Paths;
- export const initialize = async (paths: Paths) => {
- Paths = paths;
- const { tokenPath, credentialsPath } = paths;
- const token = await GoogleApiServerUtils.RetrieveAccessToken({ tokenPath, credentialsPath });
+ export const initialize = async (information: GoogleApiServerUtils.CredentialInformation) => {
+ const token = await GoogleApiServerUtils.RetrieveAccessToken(information);
Bearer = `Bearer ${token}`;
};
export const DispatchGooglePhotosUpload = async (url: string) => {
+ if (!DashUploadUtils.imageFormats.includes(path.extname(url))) {
+ return undefined;
+ }
const body = await request(url, { encoding: null });
const parameters = {
method: 'POST',
@@ -49,128 +48,39 @@ export namespace GooglePhotosUploadUtils {
uri: prepend('uploads'),
body
};
- return new Promise<any>(resolve => request(parameters, (error, _response, body) => resolve(error ? undefined : body)));
- };
-
- export const CreateMediaItems = (newMediaItems: any[], album?: { id: string }) => {
- return new Promise<any>((resolve, reject) => {
- const parameters = {
- method: 'POST',
- headers: headers('json'),
- uri: prepend('mediaItems:batchCreate'),
- body: { newMediaItems } as any,
- json: true
- };
- album && (parameters.body.albumId = album.id);
- request(parameters, (error, _response, body) => {
- if (error) {
- reject(error);
- } else {
- resolve(body);
- }
- });
- });
- };
-
-}
-
-export namespace DownloadUtils {
-
- export interface Size {
- width: number;
- suffix: string;
- }
-
- export const Sizes: { [size: string]: Size } = {
- SMALL: { width: 100, suffix: "_s" },
- MEDIUM: { width: 400, suffix: "_m" },
- LARGE: { width: 900, suffix: "_l" },
- };
-
- const png = ".png";
- const pngs = [".png", ".PNG"];
- const jpgs = [".jpg", ".JPG", ".jpeg", ".JPEG"];
- const formats = [".jpg", ".png", ".gif"];
- const size = "content-length";
- const type = "content-type";
-
- export interface UploadInformation {
- mediaPaths: string[];
- fileNames: { [key: string]: string };
- contentSize?: number;
- contentType?: string;
- }
-
- const generate = (prefix: string, url: string) => `${prefix}upload_${Utils.GenerateGuid()}${path.extname(url).toLowerCase()}`;
- const sanitize = (filename: string) => filename.replace(/\s+/g, "_");
-
- export const UploadImage = async (url: string, filename?: string, prefix = ""): Promise<Opt<UploadInformation>> => {
- const resolved = filename ? sanitize(filename) : generate(prefix, url);
- const extension = path.extname(url) || path.extname(resolved) || png;
- let information: UploadInformation = {
- mediaPaths: [],
- fileNames: { clean: resolved }
- };
- const { isLocal, stream, normalized } = classify(url);
- url = normalized;
- if (!isLocal) {
- const metadata = (await new Promise<any>((resolve, reject) => {
- request.head(url, async (error, res) => {
- if (error) {
- return reject(error);
- }
- resolve(res);
- });
- })).headers;
- information.contentSize = parseInt(metadata[size]);
- information.contentType = metadata[type];
- }
- return new Promise<UploadInformation>(async (resolve, reject) => {
- const resizers = [
- { resizer: sharp().rotate(), suffix: "_o" },
- ...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());
- } else if (!formats.includes(extension.toLowerCase())) {
- return reject();
+ return new Promise<any>((resolve, reject) => request(parameters, (error, _response, body) => {
+ if (error) {
+ console.log(error);
+ return reject(error);
}
- for (let resizer of resizers) {
- const suffix = resizer.suffix;
- let mediaPath: string;
- await new Promise<void>(resolve => {
- const filename = resolved.substring(0, resolved.length - extension.length) + suffix + extension;
- information.mediaPaths.push(mediaPath = uploadDirectory + filename);
- information.fileNames[suffix] = filename;
- stream(url).pipe(resizer.resizer).pipe(fs.createWriteStream(mediaPath))
- .on('close', resolve)
- .on('error', reject);
- });
- }
- resolve(information);
- });
- };
-
- const classify = (url: string) => {
- const isLocal = /Dash-Web(\\|\/)src(\\|\/)server(\\|\/)public(\\|\/)files/g.test(url);
- return {
- isLocal,
- stream: isLocal ? fs.createReadStream : request,
- normalized: isLocal ? path.normalize(url) : url
- };
+ resolve(body);
+ }));
};
- export const createIfNotExists = async (path: string) => {
- if (await new Promise<boolean>(resolve => fs.exists(path, resolve))) {
- return true;
- }
- return new Promise<boolean>(resolve => fs.mkdir(path, error => resolve(error === null)));
+ export const CreateMediaItems = async (newMediaItems: NewMediaItem[], album?: { id: string }): Promise<NewMediaItemResult[]> => {
+ const batched = BatchedArray.from(newMediaItems, { batchSize: 50 });
+ return batched.batchedMapPatientInterval(
+ { magnitude: 100, unit: TimeUnit.Milliseconds },
+ async (batch, collector) => {
+ const parameters = {
+ method: 'POST',
+ headers: headers('json'),
+ uri: prepend('mediaItems:batchCreate'),
+ body: { newMediaItems: batch } as any,
+ json: true
+ };
+ 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);
+ }
+ });
+ })));
+ }
+ );
};
- export const Destroy = (mediaPath: string) => new Promise<boolean>(resolve => fs.unlink(mediaPath, error => resolve(error === null)));
} \ No newline at end of file
diff --git a/src/server/apis/google/SharedTypes.ts b/src/server/apis/google/SharedTypes.ts
new file mode 100644
index 000000000..9ad6130b6
--- /dev/null
+++ b/src/server/apis/google/SharedTypes.ts
@@ -0,0 +1,21 @@
+export interface NewMediaItemResult {
+ uploadToken: string;
+ status: { code: number, message: string };
+ mediaItem: MediaItem;
+}
+
+export interface MediaItem {
+ id: string;
+ description: string;
+ productUrl: string;
+ baseUrl: string;
+ mimeType: string;
+ mediaMetadata: {
+ creationTime: string;
+ width: string;
+ height: string;
+ };
+ filename: string;
+}
+
+export type MediaItemCreationResult = { newMediaItemResults: NewMediaItemResult[] }; \ No newline at end of file
diff --git a/src/server/authentication/config/passport.ts b/src/server/authentication/config/passport.ts
index 10b17de71..8915a4abf 100644
--- a/src/server/authentication/config/passport.ts
+++ b/src/server/authentication/config/passport.ts
@@ -1,7 +1,6 @@
import * as passport from 'passport';
import * as passportLocal from 'passport-local';
-import * as mongodb from 'mongodb';
-import * as _ from "lodash";
+import _ from "lodash";
import { default as User } from '../models/user_model';
import { Request, Response, NextFunction } from "express";
import { RouteStore } from '../../RouteStore';
@@ -42,11 +41,9 @@ export let isAuthenticated = (req: Request, res: Response, next: NextFunction) =
export let isAuthorized = (req: Request, res: Response, next: NextFunction) => {
const provider = req.path.split("/").slice(-1)[0];
- if (req.user) {
- if (_.find((req.user as any).tokens, { kind: provider })) {
- next();
- } else {
- res.redirect(`/auth/${provider}`);
- }
+ if (_.find((req.user as any).tokens, { kind: provider })) {
+ next();
+ } else {
+ res.redirect(`/auth/${provider}`);
}
}; \ 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 1af36fccd..833e44bf6 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -1,18 +1,20 @@
-import { action, computed, observable, runInAction } from "mobx";
+import { action, computed, observable, reaction, runInAction } from "mobx";
import * as rp from 'request-promise';
import { DocServer } from "../../../client/DocServer";
import { Docs } from "../../../client/documents/Documents";
-import { Gateway, NorthstarSettings } from "../../../client/northstar/manager/Gateway";
import { Attribute, AttributeGroup, Catalog, Schema } from "../../../client/northstar/model/idea/idea";
import { ArrayUtil } from "../../../client/northstar/utils/ArrayUtil";
-import { CollectionViewType } from "../../../client/views/collections/CollectionBaseView";
-import { CollectionView } from "../../../client/views/collections/CollectionView";
+import { UndoManager } from "../../../client/util/UndoManager";
import { Doc, DocListCast } from "../../../new_fields/Doc";
import { List } from "../../../new_fields/List";
import { listSpec } from "../../../new_fields/Schema";
-import { Cast, StrCast, PromiseValue } from "../../../new_fields/Types";
+import { ScriptField, ComputedField } from "../../../new_fields/ScriptField";
+import { Cast, PromiseValue } from "../../../new_fields/Types";
import { Utils } from "../../../Utils";
import { RouteStore } from "../../RouteStore";
+import { InkingControl } from "../../../client/views/InkingControl";
+import { DragManager } from "../../../client/util/DragManager";
+import { nullAudio } from "../../../new_fields/URLField";
export class CurrentUserUtils {
private static curr_id: string;
@@ -20,101 +22,186 @@ export class CurrentUserUtils {
private static mainDocId: string | undefined;
public static get id() { return this.curr_id; }
- @computed public static get UserDocument() { return Doc.UserDoc(); }
public static get MainDocId() { return this.mainDocId; }
public static set MainDocId(id: string | undefined) { this.mainDocId = id; }
+ @computed public static get UserDocument() { return Doc.UserDoc(); }
+ @computed public static get ActivePen() { return Doc.UserDoc().activePen instanceof Doc && (Doc.UserDoc().activePen as Doc).pen as Doc; }
- private static createUserDocument(id: string): Doc {
- let doc = new Doc(id, true);
- doc.viewType = CollectionViewType.Tree;
- doc.dropAction = "alias";
- doc.layout = CollectionView.LayoutString();
- doc.title = Doc.CurrentUserEmail;
- this.updateUserDocument(doc);
- doc.data = new List<Doc>();
- doc.gridGap = 5;
- doc.xMargin = 5;
- doc.yMargin = 5;
- doc.boxShadow = "0 0";
- doc.optionalRightCollection = Docs.Create.StackingDocument([], { title: "New mobile uploads" });
- return doc;
+ @observable public static GuestTarget: Doc | undefined;
+ @observable public static GuestWorkspace: 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 updateUserDocument(doc: Doc) {
+ // setup the "creator" buttons for the sidebar-- eg. the default set of draggable document creation tools
+ static setupCreatorButtons(doc: Doc) {
+ let notes = CurrentUserUtils.setupNoteTypes(doc);
+ doc.noteTypes = Docs.Create.TreeDocument(notes, { title: "Note Types", height: 75 });
+ doc.activePen = doc;
+ let docProtoData: { title: string, icon: string, drag?: string, ignoreClick?: boolean, click?: string, unchecked?: string, activePen?: Doc, backgroundColor?: string, dragFactory?: Doc }[] = [
+ { title: "collection", icon: "folder", ignoreClick: true, drag: 'Docs.Create.FreeformDocument([], { nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: "freeform" })' },
+ { title: "todo item", icon: "check", ignoreClick: true, drag: 'getCopy(this.dragFactory, true)', dragFactory: notes[notes.length - 1] },
+ { 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: "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, title: "a presentation trail" })' },
+ { title: "import folder", icon: "cloud-upload-alt", ignoreClick: true, drag: 'Docs.Create.DirectoryImportDocument({ title: "Directory Import", width: 400, height: 400 })' },
+ { title: "use pen", icon: "pen-nib", click: 'activatePen(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this,2, this.backgroundColor)', backgroundColor: "blue", unchecked: `!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", unchecked: `!sameDocs(this.activePen.pen, this)`, activePen: doc },
+ { title: "use eraser", icon: "eraser", click: 'activateEraser(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this);', unchecked: `!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);', unchecked: `!sameDocs(this.activePen.pen, this)`, backgroundColor: "green", activePen: doc },
+ { title: "use drag", icon: "mouse-pointer", click: 'deactivateInk();this.activePen.pen = this;', unchecked: `!sameDocs(this.activePen.pen, this) && this.activePen.pen !== undefined`, backgroundColor: "white", activePen: doc },
+ ];
+ return docProtoData.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,
+ onDragStart: data.drag ? ScriptField.MakeFunction(data.drag) : undefined, onClick: data.click ? ScriptField.MakeScript(data.click) : undefined,
+ unchecked: data.unchecked ? ComputedField.MakeFunction(data.unchecked) : undefined, activePen: data.activePen,
+ backgroundColor: data.backgroundColor, removeDropProperties: new List<string>(["dropAction"]), dragFactory: data.dragFactory,
+ }));
+ }
+
+ // 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
+ const dragCreators = Docs.Create.MasonryDocument(CurrentUserUtils.setupCreatorButtons(doc), {
+ width: 500, autoHeight: true, columnWidth: 35, ignoreClick: true, lockedPosition: true, chromeStatus: "disabled", title: "buttons",
+ dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }), yMargin: 5
+ });
+ // setup a color picker
+ const color = Docs.Create.ColorDocument({
+ title: "color picker", width: 400, dropAction: "alias", forceActive: true, removeDropProperties: new List<string>(["dropAction", "forceActive"])
+ });
+
+ return Docs.Create.ButtonDocument({
+ width: 35, height: 35, borderRounding: "50%", boxShadow: "2px 2px 1px", title: "Tools", targetContainer: sidebarContainer,
+ sourcePanel: Docs.Create.StackingDocument([dragCreators, color], {
+ width: 500, height: 800, lockedPosition: true, chromeStatus: "disabled", title: "tools stack"
+ }),
+ onClick: ScriptField.MakeScript("this.targetContainer.proto = this.sourcePanel"),
+ });
+ }
+ // setup the Library button which will display the library panel. This panel includes a collection of workspaces, documents, and recently closed views
+ static setupLibraryPanel(sidebarContainer: Doc, doc: Doc) {
// setup workspaces library item
- if (doc.workspaces === undefined) {
- const workspaces = Docs.Create.TreeDocument([], { title: "Workspaces", height: 100 });
- workspaces.boxShadow = "0 0";
- doc.workspaces = workspaces;
- }
- PromiseValue(Cast(doc.workspaces, Doc)).then(workspaces => {
- if (workspaces) {
- workspaces.preventTreeViewOpen = true;
- workspaces.forceActive = true;
- workspaces.lockedPosition = true;
- }
+ doc.workspaces = Docs.Create.TreeDocument([], {
+ title: "WORKSPACES", height: 100, forceActive: true, boxShadow: "0 0", lockedPosition: true, backgroundColor: "#eeeeee"
});
- // setup notes list
- if (doc.noteTypes === undefined) {
- let notes = [Docs.Create.TextDocument({ title: "Note", backgroundColor: "yellow", isTemplate: true }),
- Docs.Create.TextDocument({ title: "Idea", backgroundColor: "pink", isTemplate: true }),
- Docs.Create.TextDocument({ title: "Topic", backgroundColor: "lightBlue", isTemplate: true }),
- Docs.Create.TextDocument({ title: "Person", backgroundColor: "lightGreen", isTemplate: true })];
- const noteTypes = Docs.Create.TreeDocument(notes, { title: "Note Types", height: 75 });
- doc.noteTypes = noteTypes;
- }
- PromiseValue(Cast(doc.noteTypes, Doc)).then(noteTypes => noteTypes && PromiseValue(noteTypes.data).then(DocListCast));
+ doc.documents = Docs.Create.TreeDocument([], {
+ title: "DOCUMENTS", height: 42, forceActive: true, boxShadow: "0 0", preventTreeViewOpen: true, lockedPosition: true, backgroundColor: "#eeeeee"
+ });
// setup Recently Closed library item
- if (doc.recentlyClosed === undefined) {
- const recentlyClosed = Docs.Create.TreeDocument([], { title: "Recently Closed", height: 75 });
- recentlyClosed.boxShadow = "0 0";
- doc.recentlyClosed = recentlyClosed;
- }
- PromiseValue(Cast(doc.recentlyClosed, Doc)).then(recent => {
- if (recent) {
- recent.preventTreeViewOpen = true;
- recent.forceActive = true;
- recent.lockedPosition = true;
- }
+ doc.recentlyClosed = Docs.Create.TreeDocument([], {
+ title: "RECENTLY CLOSED", height: 75, forceActive: true, boxShadow: "0 0", preventTreeViewOpen: true, lockedPosition: true, backgroundColor: "#eeeeee"
});
+ return Docs.Create.ButtonDocument({
+ width: 50, height: 35, borderRounding: "50%", boxShadow: "2px 2px 1px", title: "Library",
+ 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
+ }),
+ targetContainer: sidebarContainer,
+ onClick: ScriptField.MakeScript("this.targetContainer.proto = this.sourcePanel")
+ });
+ }
- if (doc.curPresentation === undefined) {
- const curPresentation = Docs.Create.PresDocument(new List<Doc>(), { title: "Presentation" });
- curPresentation.boxShadow = "0 0";
- doc.curPresentation = curPresentation;
- }
+ // setup the Search button which will display the search panel.
+ static setupSearchPanel(sidebarContainer: Doc) {
+ return Docs.Create.ButtonDocument({
+ width: 50, height: 35, borderRounding: "50%", boxShadow: "2px 2px 1px", title: "Search",
+ sourcePanel: Docs.Create.QueryDocument({
+ title: "search stack", ignoreClick: true
+ }),
+ targetContainer: sidebarContainer,
+ lockedPosition: true,
+ onClick: ScriptField.MakeScript("this.targetContainer.proto = this.sourcePanel")
+ });
+ }
- if (doc.sidebar === undefined) {
- const sidebar = Docs.Create.StackingDocument([doc.workspaces as Doc, doc, doc.recentlyClosed as Doc], { title: "Sidebar" });
- sidebar.forceActive = true;
- sidebar.lockedPosition = true;
- sidebar.gridGap = 5;
- sidebar.xMargin = 5;
- sidebar.yMargin = 5;
- Doc.GetProto(sidebar).backgroundColor = "#aca3a6";
- sidebar.boxShadow = "1 1 3";
- doc.sidebar = sidebar;
- }
+ // 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()");
- if (doc.overlays === undefined) {
- const overlays = Docs.Create.FreeformDocument([], { title: "Overlays" });
- Doc.GetProto(overlays).backgroundColor = "#aca3a6";
- doc.overlays = overlays;
- }
+ 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);
- if (doc.linkFollowBox === undefined) {
- PromiseValue(Cast(doc.overlays, Doc)).then(overlays => overlays && Doc.AddDocToList(overlays, "data", doc.linkFollowBox = Docs.Create.LinkFollowBoxDocument({ x: 250, y: 20, width: 500, height: 370, title: "Link Follower" })));
- }
+ // 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: "lightgrey", chromeStatus: "disabled", title: "library stack"
+ });
+ }
+
+ /// 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,
+ 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);
+ }
+
+ // the initial presentation Doc to use
+ static setupDefaultPresentation(doc: Doc) {
+ doc.curPresentation = Docs.Create.PresDocument(new List<Doc>(), { title: "Presentation", boxShadow: "0 0" });
+ }
- StrCast(doc.title).indexOf("@") !== -1 && (doc.title = StrCast(doc.title).split("@")[0] + "'s Library");
- doc.width = 100;
- doc.preventTreeViewOpen = true;
- doc.forceActive = true;
- doc.lockedPosition = true;
+ static setupMobileUploads(doc: Doc) {
+ doc.optionalRightCollection = Docs.Create.StackingDocument([], { title: "New mobile uploads" });
+ }
+
+ static updateUserDocument(doc: Doc) {
+ doc.title = Doc.CurrentUserEmail;
+ new InkingControl();
+ (doc.optionalRightCollection === undefined) && CurrentUserUtils.setupMobileUploads(doc);
+ (doc.overlays === undefined) && CurrentUserUtils.setupOverlays(doc);
+ (doc.expandingButtons === undefined) && CurrentUserUtils.setupExpandingButtons(doc);
+ (doc.curPresentation === undefined) && CurrentUserUtils.setupDefaultPresentation(doc);
+ (doc.sidebarButtons === undefined) && CurrentUserUtils.setupSidebarButtons(doc);
+
+ // this is equivalent to using PrefetchProxies to make sure all the sidebarButtons and noteType internal Doc's have been retrieved.
+ PromiseValue(Cast(doc.noteTypes, Doc)).then(noteTypes => noteTypes && PromiseValue(noteTypes.data).then(DocListCast));
+ PromiseValue(Cast(doc.sidebarButtons, Doc)).then(stackingDoc => {
+ stackingDoc && PromiseValue(Cast(stackingDoc.data, listSpec(Doc))).then(sidebarButtons => {
+ sidebarButtons && sidebarButtons.map((sidebarBtn, i) => {
+ sidebarBtn && PromiseValue(Cast(sidebarBtn, Doc)).then(async btn => {
+ btn && btn.sourcePanel && btn.targetContainer && i === 1 && (btn.onClick as ScriptField).script.run({ this: btn });
+ });
+ });
+ });
+ });
+
+ // setup reactions to change the highlights on the undo/redo buttons -- would be better to encode this in the undo/redo buttons, but the undo/redo stacks are not wired up that way yet
+ doc.undoBtn && reaction(() => UndoManager.undoStack.slice(), () => Doc.GetProto(doc.undoBtn as Doc).opacity = UndoManager.CanUndo() ? 1 : 0.4, { fireImmediately: true });
+ doc.redoBtn && reaction(() => UndoManager.redoStack.slice(), () => Doc.GetProto(doc.redoBtn as Doc).opacity = UndoManager.CanRedo() ? 1 : 0.4, { fireImmediately: true });
+
+ return doc;
}
public static loadCurrentUser() {
@@ -132,15 +219,9 @@ export class CurrentUserUtils {
this.curr_id = id;
Doc.CurrentUserEmail = email;
await rp.get(Utils.prepend(RouteStore.getUserDocumentId)).then(id => {
- if (id) {
- return DocServer.GetRefField(id).then(async field => {
- if (field instanceof Doc) {
- await this.updateUserDocument(field);
- runInAction(() => Doc.SetUserDoc(field));
- } else {
- runInAction(() => Doc.SetUserDoc(this.createUserDocument(id)));
- }
- });
+ if (id && id !== "guest") {
+ return DocServer.GetRefField(id).then(async field =>
+ Doc.SetUserDoc(await this.updateUserDocument(field instanceof Doc ? field : new Doc(id, true))));
} else {
throw new Error("There should be a user id! Why does Dash think there isn't one?");
}
diff --git a/src/server/credentials/google_docs_credentials.json b/src/server/credentials/google_docs_credentials.json
index 8d097d363..955c5a3c1 100644
--- a/src/server/credentials/google_docs_credentials.json
+++ b/src/server/credentials/google_docs_credentials.json
@@ -1 +1,11 @@
-{"installed":{"client_id":"343179513178-ud6tvmh275r2fq93u9eesrnc66t6akh9.apps.googleusercontent.com","project_id":"quickstart-1565056383187","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":"w8KIFSc0MQpmUYHed4qEzn8b","redirect_uris":["urn:ietf:wg:oauth:2.0:oob","http://localhost"]}} \ No newline at end of file
+{
+ "installed": {
+ "client_id": "343179513178-ud6tvmh275r2fq93u9eesrnc66t6akh9.apps.googleusercontent.com",
+ "project_id": "quickstart-1565056383187",
+ "auth_uri": "https://accounts.google.com/o/oauth2/auth",
+ "token_uri": "https://oauth2.googleapis.com/token",
+ "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
+ "client_secret": "w8KIFSc0MQpmUYHed4qEzn8b",
+ "redirect_uris": ["urn:ietf:wg:oauth:2.0:oob", "http://localhost"]
+ }
+} \ No newline at end of file
diff --git a/src/server/credentials/google_docs_token.json b/src/server/credentials/google_docs_token.json
deleted file mode 100644
index 07c02d56c..000000000
--- a/src/server/credentials/google_docs_token.json
+++ /dev/null
@@ -1 +0,0 @@
-{"access_token":"ya29.GltjB4-x03xFpd2NY2555cxg1xlT_ajqRi78M9osOfdOF2jTIjlPkn_UZL8cUwVP0DPC8rH3vhhg8RpspFe8Vewx92shAO3RPos_uMH0CUqEiCiZlaaB5I3Jq3Mv","refresh_token":"1/teUKUqGKMLjVqs-eed0L8omI02pzSxMUYaxGc2QxBw0","scope":"https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents.readonly","token_type":"Bearer","expiry_date":1565654175862} \ No newline at end of file
diff --git a/src/server/database.ts b/src/server/database.ts
index a7254fb0c..db86b472d 100644
--- a/src/server/database.ts
+++ b/src/server/database.ts
@@ -1,209 +1,295 @@
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';
-export class Database {
- public static DocumentsCollection = 'documents';
- public static Instance = new Database();
- private MongoClient = mongodb.MongoClient;
- private url = 'mongodb://localhost:27017/Dash';
- private currentWrites: { [id: string]: Promise<void> } = {};
- private db?: mongodb.Db;
- private onConnect: (() => void)[] = [];
-
- constructor() {
- this.MongoClient.connect(this.url, (err, client) => {
- this.db = client.db();
- this.onConnect.forEach(fn => fn());
- });
- }
+export namespace Database {
- public update(id: string, value: any, callback: (err: mongodb.MongoError, res: mongodb.UpdateWriteOpResult) => void, upsert = true, collectionName = Database.DocumentsCollection) {
- if (this.db) {
- let collection = this.db.collection(collectionName);
- const prom = this.currentWrites[id];
- let newProm: Promise<void>;
- const run = (): Promise<void> => {
- return new Promise<void>(resolve => {
- collection.updateOne({ _id: id }, value, { upsert }
- , (err, res) => {
- if (this.currentWrites[id] === newProm) {
- delete this.currentWrites[id];
- }
- resolve();
- callback(err, res);
- });
- });
- };
- newProm = prom ? prom.then(run) : run();
- this.currentWrites[id] = newProm;
- } else {
- this.onConnect.push(() => this.update(id, value, callback, upsert, collectionName));
- }
- }
+ class Database {
+ public static DocumentsCollection = 'documents';
+ private MongoClient = mongodb.MongoClient;
+ private url = 'mongodb://localhost:27017/Dash';
+ private currentWrites: { [id: string]: Promise<void> } = {};
+ private db?: mongodb.Db;
+ private onConnect: (() => void)[] = [];
- public replace(id: string, value: any, callback: (err: mongodb.MongoError, res: mongodb.UpdateWriteOpResult) => void, upsert = true, collectionName = Database.DocumentsCollection) {
- if (this.db) {
- let collection = this.db.collection(collectionName);
- const prom = this.currentWrites[id];
- let newProm: Promise<void>;
- const run = (): Promise<void> => {
- return new Promise<void>(resolve => {
- collection.replaceOne({ _id: id }, value, { upsert }
- , (err, res) => {
- if (this.currentWrites[id] === newProm) {
- delete this.currentWrites[id];
- }
- resolve();
- callback(err, res);
- });
- });
- };
- newProm = prom ? prom.then(run) : run();
- this.currentWrites[id] = newProm;
- } else {
- this.onConnect.push(() => this.replace(id, value, callback, upsert, collectionName));
+ constructor() {
+ this.MongoClient.connect(this.url, (err, client) => {
+ this.db = client.db();
+ this.onConnect.forEach(fn => fn());
+ });
}
- }
- public delete(query: any, collectionName?: string): Promise<mongodb.DeleteWriteOpResultObject>;
- public delete(id: string, collectionName?: string): Promise<mongodb.DeleteWriteOpResultObject>;
- public delete(id: any, collectionName = Database.DocumentsCollection) {
- if (typeof id === "string") {
- id = { _id: id };
- }
- if (this.db) {
- const db = this.db;
- return new Promise(res => db.collection(collectionName).deleteMany(id, (err, result) => res(result)));
- } else {
- return new Promise(res => this.onConnect.push(() => res(this.delete(id, collectionName))));
+ public async update(id: string, value: any, callback: (err: mongodb.MongoError, res: mongodb.UpdateWriteOpResult) => void, upsert = true, collectionName = Database.DocumentsCollection) {
+ if (this.db) {
+ let collection = this.db.collection(collectionName);
+ const prom = this.currentWrites[id];
+ let newProm: Promise<void>;
+ const run = (): Promise<void> => {
+ return new Promise<void>(resolve => {
+ collection.updateOne({ _id: id }, value, { upsert }
+ , (err, res) => {
+ if (this.currentWrites[id] === newProm) {
+ delete this.currentWrites[id];
+ }
+ resolve();
+ callback(err, res);
+ });
+ });
+ };
+ newProm = prom ? prom.then(run) : run();
+ this.currentWrites[id] = newProm;
+ return newProm;
+ } else {
+ this.onConnect.push(() => this.update(id, value, callback, upsert, collectionName));
+ }
}
- }
- public deleteAll(collectionName = Database.DocumentsCollection): Promise<any> {
- return new Promise(res => {
+ public replace(id: string, value: any, callback: (err: mongodb.MongoError, res: mongodb.UpdateWriteOpResult) => void, upsert = true, collectionName = Database.DocumentsCollection) {
if (this.db) {
- this.db.collection(collectionName).deleteMany({}, res);
+ let collection = this.db.collection(collectionName);
+ const prom = this.currentWrites[id];
+ let newProm: Promise<void>;
+ const run = (): Promise<void> => {
+ return new Promise<void>(resolve => {
+ collection.replaceOne({ _id: id }, value, { upsert }
+ , (err, res) => {
+ if (this.currentWrites[id] === newProm) {
+ delete this.currentWrites[id];
+ }
+ resolve();
+ callback(err, res);
+ });
+ });
+ };
+ newProm = prom ? prom.then(run) : run();
+ this.currentWrites[id] = newProm;
} else {
- this.onConnect.push(() => this.db && this.db.collection(collectionName).deleteMany({}, res));
+ this.onConnect.push(() => this.replace(id, value, callback, upsert, collectionName));
}
- });
- }
+ }
- public insert(value: any, collectionName = Database.DocumentsCollection) {
- if (this.db) {
- if ("id" in value) {
- value._id = value.id;
- delete value.id;
+ public delete(query: any, collectionName?: string): Promise<mongodb.DeleteWriteOpResultObject>;
+ public delete(id: string, collectionName?: string): Promise<mongodb.DeleteWriteOpResultObject>;
+ public delete(id: any, collectionName = Database.DocumentsCollection) {
+ if (typeof id === "string") {
+ id = { _id: id };
+ }
+ if (this.db) {
+ const db = this.db;
+ return new Promise(res => db.collection(collectionName).deleteMany(id, (err, result) => res(result)));
+ } else {
+ return new Promise(res => this.onConnect.push(() => res(this.delete(id, collectionName))));
}
- const id = value._id;
- const collection = this.db.collection(collectionName);
- const prom = this.currentWrites[id];
- let newProm: Promise<void>;
- const run = (): Promise<void> => {
- return new Promise<void>(resolve => {
- collection.insertOne(value, (err, res) => {
- if (this.currentWrites[id] === newProm) {
- delete this.currentWrites[id];
- }
- resolve();
- });
- });
- };
- newProm = prom ? prom.then(run) : run();
- this.currentWrites[id] = newProm;
- } else {
- this.onConnect.push(() => this.insert(value, collectionName));
}
- }
- public getDocument(id: string, fn: (result?: Transferable) => void, collectionName = Database.DocumentsCollection) {
- if (this.db) {
- this.db.collection(collectionName).findOne({ _id: id }, (err, result) => {
- if (result) {
- result.id = result._id;
- delete result._id;
- fn(result);
+ public async deleteAll(collectionName = Database.DocumentsCollection, persist = true): Promise<any> {
+ return new Promise(resolve => {
+ const executor = async (database: mongodb.Db) => {
+ if (persist) {
+ await database.collection(collectionName).deleteMany({});
+ } else {
+ await database.dropCollection(collectionName);
+ }
+ resolve();
+ };
+ if (this.db) {
+ executor(this.db);
} else {
- fn(undefined);
+ this.onConnect.push(() => this.db && executor(this.db));
}
});
- } else {
- this.onConnect.push(() => this.getDocument(id, fn, collectionName));
}
- }
- public getDocuments(ids: string[], fn: (result: Transferable[]) => void, collectionName = Database.DocumentsCollection) {
- if (this.db) {
- this.db.collection(collectionName).find({ _id: { "$in": ids } }).toArray((err, docs) => {
- if (err) {
- console.log(err.message);
- console.log(err.errmsg);
+ public async insert(value: any, collectionName = Database.DocumentsCollection) {
+ if (this.db) {
+ if ("id" in value) {
+ value._id = value.id;
+ delete value.id;
}
- fn(docs.map(doc => {
- doc.id = doc._id;
- delete doc._id;
- return doc;
- }));
- });
- } else {
- this.onConnect.push(() => this.getDocuments(ids, fn, collectionName));
+ const id = value._id;
+ const collection = this.db.collection(collectionName);
+ const prom = this.currentWrites[id];
+ let newProm: Promise<void>;
+ const run = (): Promise<void> => {
+ return new Promise<void>(resolve => {
+ collection.insertOne(value, (err, res) => {
+ if (this.currentWrites[id] === newProm) {
+ delete this.currentWrites[id];
+ }
+ resolve();
+ });
+ });
+ };
+ newProm = prom ? prom.then(run) : run();
+ this.currentWrites[id] = newProm;
+ return newProm;
+ } else {
+ this.onConnect.push(() => this.insert(value, collectionName));
+ }
}
- }
- public async visit(ids: string[], fn: (result: any) => string[], collectionName = "newDocuments"): Promise<void> {
- if (this.db) {
- const visited = new Set<string>();
- while (ids.length) {
- const count = Math.min(ids.length, 1000);
- const index = ids.length - count;
- const fetchIds = ids.splice(index, count).filter(id => !visited.has(id));
- if (!fetchIds.length) {
- continue;
- }
- const docs = await new Promise<{ [key: string]: any }[]>(res => Database.Instance.getDocuments(fetchIds, res, "newDocuments"));
- for (const doc of docs) {
- const id = doc.id;
- visited.add(id);
- ids.push(...fn(doc));
+ public getDocument(id: string, fn: (result?: Transferable) => void, collectionName = "newDocuments") {
+ if (this.db) {
+ this.db.collection(collectionName).findOne({ _id: id }, (err, result) => {
+ if (result) {
+ result.id = result._id;
+ delete result._id;
+ fn(result);
+ } else {
+ fn(undefined);
+ }
+ });
+ } else {
+ this.onConnect.push(() => this.getDocument(id, fn, collectionName));
+ }
+ }
+
+ public getDocuments(ids: string[], fn: (result: Transferable[]) => void, collectionName = Database.DocumentsCollection) {
+ if (this.db) {
+ this.db.collection(collectionName).find({ _id: { "$in": ids } }).toArray((err, docs) => {
+ if (err) {
+ console.log(err.message);
+ console.log(err.errmsg);
+ }
+ fn(docs.map(doc => {
+ doc.id = doc._id;
+ delete doc._id;
+ return doc;
+ }));
+ });
+ } else {
+ this.onConnect.push(() => this.getDocuments(ids, fn, collectionName));
+ }
+ }
+
+ public async visit(ids: string[], fn: (result: any) => string[] | Promise<string[]>, collectionName = "newDocuments"): Promise<void> {
+ if (this.db) {
+ const visited = new Set<string>();
+ while (ids.length) {
+ const count = Math.min(ids.length, 1000);
+ const index = ids.length - count;
+ const fetchIds = ids.splice(index, count).filter(id => !visited.has(id));
+ if (!fetchIds.length) {
+ continue;
+ }
+ const docs = await new Promise<{ [key: string]: any }[]>(res => Instance.getDocuments(fetchIds, res, "newDocuments"));
+ for (const doc of docs) {
+ const id = doc.id;
+ visited.add(id);
+ ids.push(...(await fn(doc)));
+ }
}
+ } else {
+ return new Promise(res => {
+ this.onConnect.push(() => {
+ this.visit(ids, fn, collectionName);
+ res();
+ });
+ });
}
+ }
- } else {
- return new Promise(res => {
- this.onConnect.push(() => {
- this.visit(ids, fn, collectionName);
- res();
+ public query(query: { [key: string]: any }, projection?: { [key: string]: 0 | 1 }, collectionName = "newDocuments"): Promise<mongodb.Cursor> {
+ if (this.db) {
+ let cursor = this.db.collection(collectionName).find(query);
+ if (projection) {
+ cursor = cursor.project(projection);
+ }
+ return Promise.resolve<mongodb.Cursor>(cursor);
+ } else {
+ return new Promise<mongodb.Cursor>(res => {
+ this.onConnect.push(() => res(this.query(query, projection, collectionName)));
});
- });
+ }
}
- }
- public query(query: { [key: string]: any }, projection?: { [key: string]: 0 | 1 }, collectionName = "newDocuments"): Promise<mongodb.Cursor> {
- if (this.db) {
- let cursor = this.db.collection(collectionName).find(query);
- if (projection) {
- cursor = cursor.project(projection);
+ public updateMany(query: any, update: any, collectionName = "newDocuments") {
+ if (this.db) {
+ const db = this.db;
+ return new Promise<mongodb.WriteOpResult>(res => db.collection(collectionName).update(query, update, (_, result) => res(result)));
+ } else {
+ return new Promise<mongodb.WriteOpResult>(res => {
+ this.onConnect.push(() => this.updateMany(query, update, collectionName).then(res));
+ });
}
- return Promise.resolve<mongodb.Cursor>(cursor);
- } else {
- return new Promise<mongodb.Cursor>(res => {
- this.onConnect.push(() => res(this.query(query, projection, collectionName)));
- });
}
- }
- public updateMany(query: any, update: any, collectionName = "newDocuments") {
- if (this.db) {
- const db = this.db;
- return new Promise<mongodb.WriteOpResult>(res => db.collection(collectionName).update(query, update, (_, result) => res(result)));
- } else {
- return new Promise<mongodb.WriteOpResult>(res => {
- this.onConnect.push(() => this.updateMany(query, update, collectionName).then(res));
- });
+ public print() {
+ console.log("db says hi!");
}
}
- public print() {
- console.log("db says hi!");
+ export const Instance = new Database();
+
+ export namespace Auxiliary {
+
+ export enum AuxiliaryCollections {
+ GooglePhotosUploadHistory = "uploadedFromGooglePhotos"
+ }
+
+ const SanitizedCappedQuery = async (query: { [key: string]: any }, collection: string, cap: number, removeId = true) => {
+ const cursor = await Instance.query(query, undefined, collection);
+ const results = await cursor.toArray();
+ const slice = results.slice(0, Math.min(cap, results.length));
+ return removeId ? slice.map(result => {
+ delete result._id;
+ return result;
+ }) : slice;
+ };
+
+ const SanitizedSingletonQuery = async <T>(query: { [key: string]: any }, collection: string, removeId = true): Promise<Opt<T>> => {
+ const results = await SanitizedCappedQuery(query, collection, 1, removeId);
+ return results.length ? results[0] : undefined;
+ };
+
+ export const QueryUploadHistory = async (contentSize: number) => {
+ return SanitizedSingletonQuery<DashUploadUtils.UploadInformation>({ contentSize }, AuxiliaryCollections.GooglePhotosUploadHistory);
+ };
+
+ export namespace GoogleAuthenticationToken {
+
+ const GoogleAuthentication = "googleAuthentication";
+
+ export type StoredCredentials = Credentials & { _id: string };
+
+ export const Fetch = async (userId: string, removeId = true) => {
+ return SanitizedSingletonQuery<StoredCredentials>({ userId }, GoogleAuthentication, removeId);
+ };
+
+ export const Write = async (userId: string, enrichedCredentials: GoogleApiServerUtils.EnrichedCredentials) => {
+ return Instance.insert({ userId, canAccess: [], ...enrichedCredentials }, GoogleAuthentication);
+ };
+
+ export const Update = async (userId: string, access_token: string, expiry_date: number) => {
+ const entry = await Fetch(userId, false);
+ if (entry) {
+ const parameters = { $set: { access_token, expiry_date } };
+ return Instance.update(entry._id, parameters, emptyFunction, true, GoogleAuthentication);
+ }
+ };
+
+ export const DeleteAll = () => Instance.deleteAll(GoogleAuthentication, false);
+
+ }
+
+ export const LogUpload = async (information: DashUploadUtils.UploadInformation) => {
+ const bundle = {
+ _id: Utils.GenerateDeterministicGuid(String(information.contentSize!)),
+ ...information
+ };
+ return Instance.insert(bundle, AuxiliaryCollections.GooglePhotosUploadHistory);
+ };
+
+ export const DeleteAll = async (persist = false) => {
+ const collectionNames = Object.values(AuxiliaryCollections);
+ const pendingDeletions = collectionNames.map(name => Instance.deleteAll(name, persist));
+ return Promise.all(pendingDeletions);
+ };
+
}
-}
+
+} \ No newline at end of file
diff --git a/src/server/index.ts b/src/server/index.ts
index 50ce2b14e..1595781dc 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -29,26 +29,33 @@ import { RouteStore } from './RouteStore';
import v4 = require('uuid/v4');
const app = express();
const config = require('../../webpack.config');
-import { createCanvas, loadImage, Canvas } from "canvas";
+import { createCanvas } from "canvas";
const compiler = webpack(config);
const port = 1050; // default port to listen
const serverPort = 4321;
import expressFlash = require('express-flash');
import flash = require('connect-flash');
import { Search } from './Search';
-import _ = require('lodash');
import * as Archiver from 'archiver';
var AdmZip = require('adm-zip');
import * as YoutubeApi from "./apis/youtube/youtubeApiSample";
import { Response } from 'express-serve-static-core';
import { GoogleApiServerUtils } from "./apis/google/GoogleApiServerUtils";
-import { GaxiosResponse } from 'gaxios';
-import { Opt } from '../new_fields/Doc';
-import { docs_v1 } from 'googleapis';
-import { Endpoint } from 'googleapis-common';
const MongoStore = require('connect-mongo')(session);
const mongoose = require('mongoose');
const probe = require("probe-image-size");
+const pdf = require('pdf-parse');
+var findInFiles = require('find-in-files');
+import { GooglePhotosUploadUtils } from './apis/google/GooglePhotosUploadUtils';
+import * as qs from 'query-string';
+import { Opt } from '../new_fields/Doc';
+import { DashUploadUtils } from './DashUploadUtils';
+import { BatchedArray, TimeUnit } from 'array-batcher';
+import { ParsedPDF } from "./PdfTypes";
+import { reject } from 'bluebird';
+import { ExifData } from 'exif';
+import { Result } from '../client/northstar/model/idea/idea';
+import RouteSubscriber from './RouteSubscriber';
const download = (url: string, dest: fs.PathLike) => request.get(url).pipe(fs.createWriteStream(dest));
let youtubeApiKey: string;
@@ -100,6 +107,26 @@ enum Method {
POST
}
+export type ValidationHandler = (user: DashUserModel, req: express.Request, res: express.Response) => any | Promise<any>;
+export type RejectionHandler = (req: express.Request, res: express.Response) => any | Promise<any>;
+export type ErrorHandler = (req: express.Request, res: express.Response, error: any) => any | Promise<any>;
+
+const LoginRedirect: RejectionHandler = (_req, res) => res.redirect(RouteStore.login);
+
+export interface RouteInitializer {
+ method: Method;
+ subscribers: string | RouteSubscriber | (string | RouteSubscriber)[];
+ onValidation: ValidationHandler;
+ onRejection?: RejectionHandler;
+ onError?: ErrorHandler;
+}
+
+const isSharedDocAccess = (target: string) => {
+ const shared = qs.parse(qs.extract(target), { sort: false }).sharing === "true";
+ const docAccess = target.startsWith("/doc/");
+ return shared && docAccess;
+};
+
/**
* Please invoke this function when adding a new route to Dash's server.
* It ensures that any requests leading to or containing user-sensitive information
@@ -109,20 +136,40 @@ enum Method {
* @param onRejection an optional callback invoked on return if no user is found to be logged in
* @param subscribers the forward slash prepended path names (reference and add to RouteStore.ts) that will all invoke the given @param handler
*/
-function addSecureRoute(method: Method,
- handler: (user: DashUserModel, res: express.Response, req: express.Request) => void,
- onRejection: (res: express.Response, req: express.Request) => any = res => res.redirect(RouteStore.login),
- ...subscribers: string[]
-) {
- let abstracted = (req: express.Request, res: express.Response) => {
- if (req.user) {
- handler(req.user as any, res, req);
+function addSecureRoute(initializer: RouteInitializer) {
+ const { method, subscribers, onValidation, onRejection, onError } = initializer;
+ let abstracted = async (req: express.Request, res: express.Response) => {
+ const { user, originalUrl: target } = req;
+ if (user || isSharedDocAccess(target)) {
+ try {
+ await onValidation(user as any, req, res);
+ } catch (e) {
+ if (onError) {
+ onError(req, res, e);
+ } else {
+ _error(res, `The server encountered an internal error handling ${target}.`, e);
+ }
+ }
} else {
- req.session!.target = req.originalUrl;
- onRejection(res, req);
+ req.session!.target = target;
+ try {
+ await (onRejection || LoginRedirect)(req, res);
+ } catch (e) {
+ if (onError) {
+ onError(req, res, e);
+ } else {
+ _error(res, `The server encountered an internal error when rejecting ${target}.`, e);
+ }
+ }
}
};
- subscribers.forEach(route => {
+ const subscribe = (subscriber: RouteSubscriber | string) => {
+ let route: string;
+ if (typeof subscriber === "string") {
+ route = subscriber;
+ } else {
+ route = subscriber.build;
+ }
switch (method) {
case Method.GET:
app.get(route, abstracted);
@@ -131,7 +178,12 @@ function addSecureRoute(method: Method,
app.post(route, abstracted);
break;
}
- });
+ };
+ if (Array.isArray(subscribers)) {
+ subscribers.forEach(subscribe);
+ } else {
+ subscribe(subscribers);
+ }
}
// STATIC FILE SERVING
@@ -157,6 +209,13 @@ app.get("/buxton", (req, res) => {
command_line('python scraper.py', cwd).then(onResolved, tryPython3);
});
+const STATUS = {
+ OK: 200,
+ BAD_REQUEST: 400,
+ EXECUTION_ERROR: 500,
+ PERMISSION_DENIED: 403
+};
+
const command_line = (command: string, fromDirectory?: string) => {
return new Promise<string>((resolve, reject) => {
let options: ExecOptions = {};
@@ -196,6 +255,22 @@ const solrURL = "http://localhost:8983/solr/#/dash";
// GETTERS
+app.get("/textsearch", async (req, res) => {
+ let q = req.query.q;
+ if (q === undefined) {
+ res.send([]);
+ return;
+ }
+ let results = await findInFiles.find({ 'term': q, 'flags': 'ig' }, uploadDirectory + "text", ".txt$");
+ let resObj: { ids: string[], numFound: number, lines: string[] } = { ids: [], numFound: 0, lines: [] };
+ for (var result in results) {
+ resObj.ids.push(path.basename(result, ".txt").replace(/upload_/, ""));
+ resObj.lines.push(results[result].line);
+ resObj.numFound++;
+ }
+ res.send(resObj);
+});
+
app.get("/search", async (req, res) => {
const solrQuery: any = {};
["q", "fq", "start", "rows", "hl", "hl.fl"].forEach(key => solrQuery[key] = req.query[key]);
@@ -289,6 +364,80 @@ app.get("/serializeDoc/:docId", async (req, res) => {
res.send({ docs, files: Array.from(files) });
});
+export type Hierarchy = { [id: string]: string | Hierarchy };
+export type ZipMutator = (file: Archiver.Archiver) => void | Promise<void>;
+
+addSecureRoute({
+ method: Method.GET,
+ subscribers: new RouteSubscriber(RouteStore.imageHierarchyExport).add('docId'),
+ onValidation: async (_user, req, res) => {
+ const id = req.params.docId;
+ const hierarchy: Hierarchy = {};
+ await targetedVisitorRecursive(id, hierarchy);
+ BuildAndDispatchZip(res, async zip => {
+ await hierarchyTraverserRecursive(zip, hierarchy);
+ });
+ }
+});
+
+const BuildAndDispatchZip = async (res: Response, mutator: ZipMutator): Promise<void> => {
+ const zip = Archiver('zip');
+ zip.pipe(res);
+ await mutator(zip);
+ return zip.finalize();
+};
+
+const targetedVisitorRecursive = async (seedId: string, hierarchy: Hierarchy): Promise<void> => {
+ const local: Hierarchy = {};
+ const { title, data } = await getData(seedId);
+ const label = `${title} (${seedId})`;
+ if (Array.isArray(data)) {
+ hierarchy[label] = local;
+ await Promise.all(data.map(proxy => targetedVisitorRecursive(proxy.fieldId, local)));
+ } else {
+ hierarchy[label + path.extname(data)] = data;
+ }
+};
+
+const getData = async (seedId: string): Promise<{ data: string | any[], title: string }> => {
+ return new Promise<{ data: string | any[], title: string }>((resolve, reject) => {
+ Database.Instance.getDocument(seedId, async (result: any) => {
+ const { data, proto, title } = result.fields;
+ if (data) {
+ if (data.url) {
+ resolve({ data: data.url, title });
+ } else if (data.fields) {
+ resolve({ data: data.fields, title });
+ } else {
+ reject();
+ }
+ }
+ if (proto) {
+ getData(proto.fieldId).then(resolve, reject);
+ }
+ });
+ });
+};
+
+const hierarchyTraverserRecursive = async (file: Archiver.Archiver, hierarchy: Hierarchy, prefix = "Dash Export"): Promise<void> => {
+ for (const key of Object.keys(hierarchy)) {
+ const result = hierarchy[key];
+ if (typeof result === "string") {
+ let path: string;
+ let matches: RegExpExecArray | null;
+ if ((matches = /\:1050\/files\/(upload\_[\da-z]{32}.*)/g.exec(result)) !== null) {
+ path = `${__dirname}/public/files/${matches[1]}`;
+ } else {
+ const information = await DashUploadUtils.UploadImage(result);
+ path = information.mediaPaths[0];
+ }
+ file.file(path, { name: key, prefix });
+ } else {
+ await hierarchyTraverserRecursive(file, result, `${prefix}/${key}`);
+ }
+ }
+};
+
app.get("/downloadId/:docId", async (req, res) => {
res.set('Content-disposition', `attachment;`);
res.set('Content-Type', "application/zip");
@@ -420,10 +569,10 @@ app.get("/thumbnail/:filename", (req, res) => {
let filename = req.params.filename;
let noExt = filename.substring(0, filename.length - ".png".length);
let pagenumber = parseInt(noExt.split('-')[1]);
- fs.exists(uploadDir + filename, (exists: boolean) => {
- console.log(`${uploadDir + filename} ${exists ? "exists" : "does not exist"}`);
+ fs.exists(uploadDirectory + filename, (exists: boolean) => {
+ console.log(`${uploadDirectory + filename} ${exists ? "exists" : "does not exist"}`);
if (exists) {
- let input = fs.createReadStream(uploadDir + filename);
+ let input = fs.createReadStream(uploadDirectory + filename);
probe(input, (err: any, result: any) => {
if (err) {
console.log(err);
@@ -434,7 +583,7 @@ app.get("/thumbnail/:filename", (req, res) => {
});
}
else {
- LoadPage(uploadDir + filename.substring(0, filename.length - noExt.split('-')[1].length - ".PNG".length - 1) + ".pdf", pagenumber, res);
+ LoadPage(uploadDirectory + filename.substring(0, filename.length - noExt.split('-')[1].length - ".PNG".length - 1) + ".pdf", pagenumber, res);
}
});
});
@@ -475,51 +624,49 @@ function LoadPage(file: string, pageNumber: number, res: Response) {
});
}
-// anyone attempting to navigate to localhost at this port will
-// first have to login
-addSecureRoute(
- Method.GET,
- (user, res) => res.redirect(RouteStore.home),
- undefined,
- RouteStore.root
-);
-
-addSecureRoute(
- Method.GET,
- async (_, res) => {
+/**
+ * Anyone attempting to navigate to localhost at this port will
+ * first have to log in.
+ */
+addSecureRoute({
+ method: Method.GET,
+ subscribers: RouteStore.root,
+ onValidation: (_user, _req, res) => res.redirect(RouteStore.home)
+});
+
+addSecureRoute({
+ method: Method.GET,
+ subscribers: RouteStore.getUsers,
+ onValidation: async (_user, _req, res) => {
const cursor = await Database.Instance.query({}, { email: 1, userDocumentId: 1 }, "users");
const results = await cursor.toArray();
res.send(results.map(user => ({ email: user.email, userDocumentId: user.userDocumentId })));
},
- undefined,
- RouteStore.getUsers
-);
+});
-addSecureRoute(
- Method.GET,
- (user, res, req) => {
+addSecureRoute({
+ method: Method.GET,
+ subscribers: [RouteStore.home, RouteStore.openDocumentWithId],
+ onValidation: (_user, req, res) => {
let detector = new mobileDetect(req.headers['user-agent'] || "");
let filename = detector.mobile() !== null ? 'mobile/image.html' : 'index.html';
res.sendFile(path.join(__dirname, '../../deploy/' + filename));
},
- undefined,
- RouteStore.home,
- RouteStore.openDocumentWithId
-);
-
-addSecureRoute(
- Method.GET,
- (user, res) => res.send(user.userDocumentId || ""),
- undefined,
- RouteStore.getUserDocumentId,
-);
-
-addSecureRoute(
- Method.GET,
- (user, res) => res.send(JSON.stringify({ id: user.id, email: user.email })),
- undefined,
- RouteStore.getCurrUser
-);
+});
+
+addSecureRoute({
+ method: Method.GET,
+ subscribers: RouteStore.getUserDocumentId,
+ onValidation: (user, _req, res) => res.send(user.userDocumentId),
+ onRejection: (_req, res) => res.send(undefined)
+});
+
+addSecureRoute({
+ method: Method.GET,
+ subscribers: RouteStore.getCurrUser,
+ onValidation: (user, _req, res) => { res.send(JSON.stringify(user)); },
+ onRejection: (_req, res) => res.send(JSON.stringify({ id: "__guest__", email: "" }))
+});
const ServicesApiKeyMap = new Map<string, string | undefined>([
["face", process.env.FACE],
@@ -527,10 +674,14 @@ const ServicesApiKeyMap = new Map<string, string | undefined>([
["handwriting", process.env.HANDWRITING]
]);
-addSecureRoute(Method.GET, (user, res, req) => {
- let service = req.params.requestedservice;
- res.send(ServicesApiKeyMap.get(service));
-}, undefined, `${RouteStore.cognitiveServices}/:requestedservice`);
+addSecureRoute({
+ method: Method.GET,
+ subscribers: new RouteSubscriber(RouteStore.cognitiveServices).add('requestedservice'),
+ onValidation: (_user, req, res) => {
+ let service = req.params.requestedservice;
+ res.send(ServicesApiKeyMap.get(service));
+ }
+});
class NodeCanvasFactory {
create = (width: number, height: number) => {
@@ -556,64 +707,82 @@ class NodeCanvasFactory {
}
const pngTypes = [".png", ".PNG"];
-const pdfTypes = [".pdf", ".PDF"];
const jpgTypes = [".jpg", ".JPG", ".jpeg", ".JPEG"];
-const uploadDir = __dirname + "/public/files/";
-// SETTERS
-app.post(
- RouteStore.upload,
- (req, res) => {
+const uploadDirectory = __dirname + "/public/files/";
+const pdfDirectory = uploadDirectory + "text";
+DashUploadUtils.createIfNotExists(pdfDirectory);
+
+interface ImageFileResponse {
+ name: string;
+ path: string;
+ type: string;
+ exif: Opt<DashUploadUtils.EnrichedExifData>;
+}
+
+addSecureRoute({
+ method: Method.POST,
+ subscribers: RouteStore.upload,
+ onValidation: (_user, req, res) => {
let form = new formidable.IncomingForm();
- form.uploadDir = uploadDir;
+ form.uploadDir = uploadDirectory;
form.keepExtensions = true;
- // let path = req.body.path;
- console.log("upload");
- form.parse(req, (err, fields, files) => {
- console.log("parsing");
- let names: string[] = [];
- for (const name in files) {
- const file = path.basename(files[name].path);
- const ext = path.extname(file);
- let resizers = [
- { resizer: sharp().rotate(), suffix: "_o" },
- { resizer: sharp().resize(100, undefined, { withoutEnlargement: true }).rotate(), suffix: "_s" },
- { resizer: sharp().resize(400, undefined, { withoutEnlargement: true }).rotate(), suffix: "_m" },
- { resizer: sharp().resize(900, undefined, { withoutEnlargement: true }).rotate(), suffix: "_l" },
- ];
- let isImage = false;
- if (pngTypes.includes(ext)) {
- resizers.forEach(element => {
- element.resizer = element.resizer.png();
- });
- isImage = true;
- } else if (jpgTypes.includes(ext)) {
- resizers.forEach(element => {
- element.resizer = element.resizer.jpeg();
- });
- isImage = true;
- }
- if (isImage) {
- resizers.forEach(resizer => {
- fs.createReadStream(uploadDir + file).pipe(resizer.resizer).pipe(fs.createWriteStream(uploadDir + file.substring(0, file.length - ext.length) + resizer.suffix + ext));
+ form.parse(req, async (_err, _fields, files) => {
+ let results: ImageFileResponse[] = [];
+ for (const key in files) {
+ const { type, path: location, name } = files[key];
+ const filename = path.basename(location);
+ let uploadInformation: Opt<DashUploadUtils.UploadInformation>;
+ if (filename.endsWith(".pdf")) {
+ let dataBuffer = fs.readFileSync(uploadDirectory + filename);
+ const result: ParsedPDF = await pdf(dataBuffer);
+ await new Promise<void>(resolve => {
+ const path = pdfDirectory + "/" + filename.substring(0, filename.length - ".pdf".length) + ".txt";
+ fs.createWriteStream(path).write(result.text, error => {
+ if (!error) {
+ resolve();
+ } else {
+ reject(error);
+ }
+ });
});
+ } else if (type.indexOf("audio") !== -1) {
+ // nothing to be done yet-- although transcribing the audio a la pdfs would make sense.
+ } else {
+ uploadInformation = await DashUploadUtils.UploadImage(uploadDirectory + filename, filename);
}
- names.push(`/files/` + file);
+ const exif = uploadInformation ? uploadInformation.exifData : undefined;
+ results.push({ name, type, path: `/files/${filename}`, exif });
+
}
- res.send(names);
+ _success(res, results);
});
}
-);
+});
-addSecureRoute(
- Method.POST,
- (user, res, req) => {
+addSecureRoute({
+ method: Method.POST,
+ subscribers: RouteStore.inspectImage,
+ onValidation: async (_user, req, res) => {
+ const { source } = req.body;
+ if (typeof source === "string") {
+ const uploadInformation = await DashUploadUtils.UploadImage(source);
+ return res.send(await DashUploadUtils.InspectImage(uploadInformation.mediaPaths[0]));
+ }
+ res.send({});
+ }
+});
+
+addSecureRoute({
+ method: Method.POST,
+ subscribers: RouteStore.dataUriToImage,
+ onValidation: (_user, req, res) => {
const uri = req.body.uri;
const filename = req.body.name;
if (!uri || !filename) {
res.status(401).send("incorrect parameters specified");
return;
}
- imageDataUri.outputFile(uri, uploadDir + filename).then((savedName: string) => {
+ imageDataUri.outputFile(uri, uploadDirectory + filename).then((savedName: string) => {
const ext = path.extname(savedName);
let resizers = [
{ resizer: sharp().resize(100, undefined, { withoutEnlargement: true }), suffix: "_s" },
@@ -634,15 +803,14 @@ addSecureRoute(
}
if (isImage) {
resizers.forEach(resizer => {
- fs.createReadStream(savedName).pipe(resizer.resizer).pipe(fs.createWriteStream(uploadDir + filename + resizer.suffix + ext));
+ fs.createReadStream(savedName).pipe(resizer.resizer).pipe(fs.createWriteStream(uploadDirectory + filename + resizer.suffix + ext));
});
}
res.send("/files/" + filename + ext);
});
- },
- undefined,
- RouteStore.dataUriToImage
-);
+ }
+});
+
// AUTHENTICATION
// Sign Up
@@ -681,20 +849,26 @@ app.use(RouteStore.corsProxy, (req, res) => {
}).pipe(res);
});
-app.get(RouteStore.delete, (req, res) => {
- if (release) {
- res.send("no");
- return;
+addSecureRoute({
+ method: Method.GET,
+ subscribers: RouteStore.delete,
+ onValidation: (_user, _req, res) => {
+ if (release) {
+ return _permission_denied(res, deletionPermissionError);
+ }
+ deleteFields().then(() => res.redirect(RouteStore.home));
}
- deleteFields().then(() => res.redirect(RouteStore.home));
});
-app.get(RouteStore.deleteAll, (req, res) => {
- if (release) {
- res.send("no");
- return;
+addSecureRoute({
+ method: Method.GET,
+ subscribers: RouteStore.deleteAll,
+ onValidation: (_user, _req, res) => {
+ if (release) {
+ return _permission_denied(res, deletionPermissionError);
+ }
+ deleteAll().then(() => res.redirect(RouteStore.home));
}
- deleteAll().then(() => res.redirect(RouteStore.home));
});
app.use(wdm(compiler, { publicPath: config.output.publicPath }));
@@ -801,8 +975,7 @@ function HandleYoutubeQuery([query, callback]: [YoutubeQueryInput, (result?: any
}
}
-const credentials = path.join(__dirname, "./credentials/google_docs_credentials.json");
-const token = path.join(__dirname, "./credentials/google_docs_token.json");
+const credentialsPath = path.join(__dirname, "./credentials/google_docs_credentials.json");
const EndpointHandlerMap = new Map<GoogleApiServerUtils.Action, GoogleApiServerUtils.ApiRouter>([
["create", (api, params) => api.create(params)],
@@ -813,7 +986,7 @@ const EndpointHandlerMap = new Map<GoogleApiServerUtils.Action, GoogleApiServerU
app.post(RouteStore.googleDocs + "/:sector/:action", (req, res) => {
let sector: GoogleApiServerUtils.Service = req.params.sector as GoogleApiServerUtils.Service;
let action: GoogleApiServerUtils.Action = req.params.action as GoogleApiServerUtils.Action;
- GoogleApiServerUtils.GetEndpoint(GoogleApiServerUtils.Service[sector], { credentials, token }).then(endpoint => {
+ GoogleApiServerUtils.GetEndpoint(GoogleApiServerUtils.Service[sector], { credentialsPath, userId: req.headers.userId as string }).then(endpoint => {
let handler = EndpointHandlerMap.get(action);
if (endpoint && handler) {
let execute = handler(endpoint, req.body).then(
@@ -827,6 +1000,160 @@ app.post(RouteStore.googleDocs + "/:sector/:action", (req, res) => {
});
});
+addSecureRoute({
+ method: Method.GET,
+ subscribers: RouteStore.readGoogleAccessToken,
+ onValidation: async (user, _req, res) => {
+ const userId = user.id;
+ const token = await Database.Auxiliary.GoogleAuthenticationToken.Fetch(userId);
+ const information = { credentialsPath, userId };
+ if (!token) {
+ return res.send(await GoogleApiServerUtils.GenerateAuthenticationUrl(information));
+ }
+ GoogleApiServerUtils.RetrieveAccessToken(information).then(token => res.send(token));
+ }
+});
+
+addSecureRoute({
+ method: Method.POST,
+ subscribers: RouteStore.writeGoogleAccessToken,
+ onValidation: async (user, req, res) => {
+ const userId = user.id;
+ const information = { credentialsPath, userId };
+ res.send(await GoogleApiServerUtils.ProcessClientSideCode(information, req.body.authenticationCode));
+ }
+});
+
+const tokenError = "Unable to successfully upload bytes for all images!";
+const mediaError = "Unable to convert all uploaded bytes to media items!";
+const userIdError = "Unable to parse the identification of the user!";
+
+export interface NewMediaItem {
+ description: string;
+ simpleMediaItem: {
+ uploadToken: string;
+ };
+}
+
+addSecureRoute({
+ method: Method.POST,
+ subscribers: RouteStore.googlePhotosMediaUpload,
+ onValidation: async (user, req, res) => {
+ const { media } = req.body;
+ const userId = user.id;
+ if (!userId) {
+ return _error(res, userIdError);
+ }
+
+ await GooglePhotosUploadUtils.initialize({ credentialsPath, userId });
+
+ let failed: number[] = [];
+
+ const batched = BatchedArray.from<GooglePhotosUploadUtils.MediaInput>(media, { batchSize: 25 });
+ const newMediaItems = await batched.batchedMapPatientInterval<NewMediaItem>(
+ { magnitude: 100, unit: TimeUnit.Milliseconds },
+ async (batch, collector) => {
+ for (let index = 0; index < batch.length; index++) {
+ const { url, description } = batch[index];
+ const uploadToken = await GooglePhotosUploadUtils.DispatchGooglePhotosUpload(url);
+ if (!uploadToken) {
+ failed.push(index);
+ } else {
+ collector.push({
+ description,
+ simpleMediaItem: { uploadToken }
+ });
+ }
+ }
+ }
+ );
+
+ const failedCount = failed.length;
+ if (failedCount) {
+ console.error(`Unable to upload ${failedCount} image${failedCount === 1 ? "" : "s"} to Google's servers`);
+ }
+
+ GooglePhotosUploadUtils.CreateMediaItems(newMediaItems, req.body.album).then(
+ results => _success(res, { results, failed }),
+ error => _error(res, mediaError, error)
+ );
+ }
+});
+
+interface MediaItem {
+ baseUrl: string;
+ filename: string;
+}
+const prefix = "google_photos_";
+
+const downloadError = "Encountered an error while executing downloads.";
+const requestError = "Unable to execute download: the body's media items were malformed.";
+const deletionPermissionError = "Cannot perform specialized delete outside of the development environment!";
+
+app.get("/deleteWithAux", async (_req, res) => {
+ if (release) {
+ return _permission_denied(res, deletionPermissionError);
+ }
+ await Database.Auxiliary.DeleteAll();
+ res.redirect(RouteStore.delete);
+});
+
+app.get("/deleteWithGoogleCredentials", async (req, res) => {
+ if (release) {
+ return _permission_denied(res, deletionPermissionError);
+ }
+ await Database.Auxiliary.GoogleAuthenticationToken.DeleteAll();
+ res.redirect(RouteStore.delete);
+});
+
+const UploadError = (count: number) => `Unable to upload ${count} images to Dash's server`;
+app.post(RouteStore.googlePhotosMediaDownload, async (req, res) => {
+ const contents: { mediaItems: MediaItem[] } = req.body;
+ let failed = 0;
+ if (contents) {
+ const completed: Opt<DashUploadUtils.UploadInformation>[] = [];
+ for (let item of contents.mediaItems) {
+ const { contentSize, ...attributes } = await DashUploadUtils.InspectImage(item.baseUrl);
+ const found: Opt<DashUploadUtils.UploadInformation> = 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++;
+ }
+ } else {
+ completed.push(found);
+ }
+ }
+ if (failed) {
+ return _error(res, UploadError(failed));
+ }
+ return _success(res, completed);
+ }
+ _invalid(res, requestError);
+});
+
+const _error = (res: Response, message: string, error?: any) => {
+ res.statusMessage = message;
+ res.status(STATUS.EXECUTION_ERROR).send(error);
+};
+
+const _success = (res: Response, body: any) => {
+ res.status(STATUS.OK).send(body);
+};
+
+const _invalid = (res: Response, message: string) => {
+ res.statusMessage = message;
+ res.status(STATUS.BAD_REQUEST).send();
+};
+
+const _permission_denied = (res: Response, message: string) => {
+ res.statusMessage = message;
+ res.status(STATUS.BAD_REQUEST).send("Permission Denied!");
+};
+
const suffixMap: { [type: string]: (string | [string, string | ((json: any) => any)]) } = {
"number": "_n",
"string": "_t",
diff --git a/src/server/updateSearch.ts b/src/server/updateSearch.ts
deleted file mode 100644
index 906b795f1..000000000
--- a/src/server/updateSearch.ts
+++ /dev/null
@@ -1,123 +0,0 @@
-import { Database } from "./database";
-import { Cursor } from "mongodb";
-import { Search } from "./Search";
-import pLimit from 'p-limit';
-
-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 };
-}
-
-function getSuffix(value: string | [string, any]): string {
- return typeof value === "string" ? value : value[0];
-}
-
-const limit = pLimit(5);
-async function update() {
- // await new Promise(res => setTimeout(res, 5));
- console.log("update");
- await Search.Instance.clear();
- const cursor = await Database.Instance.query({});
- console.log("Cleared");
- const updates: any[] = [];
- let numDocs = 0;
- function updateDoc(doc: any) {
- numDocs++;
- if ((numDocs % 50) === 0) {
- console.log("updateDoc " + numDocs);
- }
- // console.log("doc " + 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) {
- let { suffix, value } = term;
- update[key + suffix] = value;
- dynfield = true;
- }
- }
- if (dynfield) {
- updates.push(update);
- // console.log(updates.length);
- }
- }
- await cursor.forEach(updateDoc);
- console.log(`Updating ${updates.length} documents`);
- const result = await Search.Instance.updateDocuments(updates);
- try {
- console.log(JSON.parse(result).responseHeader.status);
- } catch {
- console.log("Error:");
- // console.log(updates[i]);
- console.log(result);
- console.log("\n");
- }
- // for (let i = 0; i < updates.length; i++) {
- // console.log(i);
- // const result = await Search.Instance.updateDocument(updates[i]);
- // try {
- // console.log(JSON.parse(result).responseHeader.status);
- // } catch {
- // console.log("Error:");
- // console.log(updates[i]);
- // console.log(result);
- // console.log("\n");
- // }
- // }
- // await Promise.all(updates.map(update => {
- // return limit(() => Search.Instance.updateDocument(update));
- // }));
- cursor.close();
-}
-
-update(); \ No newline at end of file
diff --git a/src/typings/index.d.ts b/src/typings/index.d.ts
index 7939ae8be..36d828fdb 100644
--- a/src/typings/index.d.ts
+++ b/src/typings/index.d.ts
@@ -1,322 +1,324 @@
/// <reference types="node" />
-declare module '@react-pdf/renderer' {
- import * as React from 'react';
-
- namespace ReactPDF {
- interface Style {
- [property: string]: any;
- }
- interface Styles {
- [key: string]: Style;
- }
- type Orientation = 'portrait' | 'landscape';
-
- interface DocumentProps {
- title?: string;
- author?: string;
- subject?: string;
- keywords?: string;
- creator?: string;
- producer?: string;
- onRender?: () => any;
- }
-
- /**
- * This component represent the PDF document itself. It must be the root
- * of your tree element structure, and under no circumstances should it be
- * used as children of another react-pdf component. In addition, it should
- * only have childs of type <Page />.
- */
- class Document extends React.Component<DocumentProps> { }
-
- interface NodeProps {
- style?: Style | Style[];
- /**
- * Render component in all wrapped pages.
- * @see https://react-pdf.org/advanced#fixed-components
- */
- fixed?: boolean;
- /**
- * Force the wrapping algorithm to start a new page when rendering the
- * element.
- * @see https://react-pdf.org/advanced#page-breaks
- */
- break?: boolean;
- }
-
- interface PageProps extends NodeProps {
- /**
- * Enable page wrapping for this page.
- * @see https://react-pdf.org/components#page-wrapping
- */
- wrap?: boolean;
- debug?: boolean;
- size?: string | [number, number] | { width: number; height: number };
- orientation?: Orientation;
- ruler?: boolean;
- rulerSteps?: number;
- verticalRuler?: boolean;
- verticalRulerSteps?: number;
- horizontalRuler?: boolean;
- horizontalRulerSteps?: number;
- ref?: Page;
- }
-
- /**
- * Represents single page inside the PDF document, or a subset of them if
- * using the wrapping feature. A <Document /> can contain as many pages as
- * you want, but ensure not rendering a page inside any component besides
- * Document.
- */
- class Page extends React.Component<PageProps> { }
-
- interface ViewProps extends NodeProps {
- /**
- * Enable/disable page wrapping for element.
- * @see https://react-pdf.org/components#page-wrapping
- */
- wrap?: boolean;
- debug?: boolean;
- render?: (props: { pageNumber: number }) => React.ReactNode;
- children?: React.ReactNode;
- }
-
- /**
- * The most fundamental component for building a UI and is designed to be
- * nested inside other views and can have 0 to many children.
- */
- class View extends React.Component<ViewProps> { }
-
- interface ImageProps extends NodeProps {
- debug?: boolean;
- src: string | { data: Buffer; format: 'png' | 'jpg' };
- cache?: boolean;
- }
-
- /**
- * A React component for displaying network or local (Node only) JPG or
- * PNG images, as well as base64 encoded image strings.
- */
- class Image extends React.Component<ImageProps> { }
-
- interface TextProps extends NodeProps {
- /**
- * Enable/disable page wrapping for element.
- * @see https://react-pdf.org/components#page-wrapping
- */
- wrap?: boolean;
- debug?: boolean;
- render?: (
- props: { pageNumber: number; totalPages: number },
- ) => React.ReactNode;
- children?: React.ReactNode;
- /**
- * How much hyphenated breaks should be avoided.
- */
- hyphenationCallback?: number;
- }
-
- /**
- * A React component for displaying text. Text supports nesting of other
- * Text or Link components to create inline styling.
- */
- class Text extends React.Component<TextProps> { }
-
- interface LinkProps extends NodeProps {
- /**
- * Enable/disable page wrapping for element.
- * @see https://react-pdf.org/components#page-wrapping
- */
- wrap?: boolean;
- debug?: boolean;
- src: string;
- children?: React.ReactNode;
- }
+declare module 'googlephotos';
- /**
- * A React component for displaying an hyperlink. Link’s can be nested
- * inside a Text component, or being inside any other valid primitive.
- */
- class Link extends React.Component<LinkProps> { }
-
- interface NoteProps extends NodeProps {
- children: string;
- }
-
- class Note extends React.Component<NoteProps> { }
-
- interface BlobProviderParams {
- blob: Blob | null;
- url: string | null;
- loading: boolean;
- error: Error | null;
- }
- interface BlobProviderProps {
- document: React.ReactElement<DocumentProps>;
- children: (params: BlobProviderParams) => React.ReactNode;
- }
-
- /**
- * Easy and declarative way of getting document's blob data without
- * showing it on screen.
- * @see https://react-pdf.org/advanced#on-the-fly-rendering
- * @platform web
- */
- class BlobProvider extends React.Component<BlobProviderProps> { }
-
- interface PDFViewerProps {
- width?: number;
- height?: number;
- style?: Style | Style[];
- className?: string;
- children?: React.ReactElement<DocumentProps>;
- }
-
- /**
- * Iframe PDF viewer for client-side generated documents.
- * @platform web
- */
- class PDFViewer extends React.Component<PDFViewerProps> { }
-
- interface PDFDownloadLinkProps {
- document: React.ReactElement<DocumentProps>;
- fileName?: string;
- style?: Style | Style[];
- className?: string;
- children?:
- | React.ReactNode
- | ((params: BlobProviderParams) => React.ReactNode);
+declare module '@react-pdf/renderer' {
+ import * as React from 'react';
+
+ namespace ReactPDF {
+ interface Style {
+ [property: string]: any;
+ }
+ interface Styles {
+ [key: string]: Style;
+ }
+ type Orientation = 'portrait' | 'landscape';
+
+ interface DocumentProps {
+ title?: string;
+ author?: string;
+ subject?: string;
+ keywords?: string;
+ creator?: string;
+ producer?: string;
+ onRender?: () => any;
+ }
+
+ /**
+ * This component represent the PDF document itself. It must be the root
+ * of your tree element structure, and under no circumstances should it be
+ * used as children of another react-pdf component. In addition, it should
+ * only have childs of type <Page />.
+ */
+ class Document extends React.Component<DocumentProps> { }
+
+ interface NodeProps {
+ style?: Style | Style[];
+ /**
+ * Render component in all wrapped pages.
+ * @see https://react-pdf.org/advanced#fixed-components
+ */
+ fixed?: boolean;
+ /**
+ * Force the wrapping algorithm to start a new page when rendering the
+ * element.
+ * @see https://react-pdf.org/advanced#page-breaks
+ */
+ break?: boolean;
+ }
+
+ interface PageProps extends NodeProps {
+ /**
+ * Enable page wrapping for this page.
+ * @see https://react-pdf.org/components#page-wrapping
+ */
+ wrap?: boolean;
+ debug?: boolean;
+ size?: string | [number, number] | { width: number; height: number };
+ orientation?: Orientation;
+ ruler?: boolean;
+ rulerSteps?: number;
+ verticalRuler?: boolean;
+ verticalRulerSteps?: number;
+ horizontalRuler?: boolean;
+ horizontalRulerSteps?: number;
+ ref?: Page;
+ }
+
+ /**
+ * Represents single page inside the PDF document, or a subset of them if
+ * using the wrapping feature. A <Document /> can contain as many pages as
+ * you want, but ensure not rendering a page inside any component besides
+ * Document.
+ */
+ class Page extends React.Component<PageProps> { }
+
+ interface ViewProps extends NodeProps {
+ /**
+ * Enable/disable page wrapping for element.
+ * @see https://react-pdf.org/components#page-wrapping
+ */
+ wrap?: boolean;
+ debug?: boolean;
+ render?: (props: { pageNumber: number }) => React.ReactNode;
+ children?: React.ReactNode;
+ }
+
+ /**
+ * The most fundamental component for building a UI and is designed to be
+ * nested inside other views and can have 0 to many children.
+ */
+ class View extends React.Component<ViewProps> { }
+
+ interface ImageProps extends NodeProps {
+ debug?: boolean;
+ src: string | { data: Buffer; format: 'png' | 'jpg' };
+ cache?: boolean;
+ }
+
+ /**
+ * A React component for displaying network or local (Node only) JPG or
+ * PNG images, as well as base64 encoded image strings.
+ */
+ class Image extends React.Component<ImageProps> { }
+
+ interface TextProps extends NodeProps {
+ /**
+ * Enable/disable page wrapping for element.
+ * @see https://react-pdf.org/components#page-wrapping
+ */
+ wrap?: boolean;
+ debug?: boolean;
+ render?: (
+ props: { pageNumber: number; totalPages: number },
+ ) => React.ReactNode;
+ children?: React.ReactNode;
+ /**
+ * How much hyphenated breaks should be avoided.
+ */
+ hyphenationCallback?: number;
+ }
+
+ /**
+ * A React component for displaying text. Text supports nesting of other
+ * Text or Link components to create inline styling.
+ */
+ class Text extends React.Component<TextProps> { }
+
+ interface LinkProps extends NodeProps {
+ /**
+ * Enable/disable page wrapping for element.
+ * @see https://react-pdf.org/components#page-wrapping
+ */
+ wrap?: boolean;
+ debug?: boolean;
+ src: string;
+ children?: React.ReactNode;
+ }
+
+ /**
+ * A React component for displaying an hyperlink. Link’s can be nested
+ * inside a Text component, or being inside any other valid primitive.
+ */
+ class Link extends React.Component<LinkProps> { }
+
+ interface NoteProps extends NodeProps {
+ children: string;
+ }
+
+ class Note extends React.Component<NoteProps> { }
+
+ interface BlobProviderParams {
+ blob: Blob | null;
+ url: string | null;
+ loading: boolean;
+ error: Error | null;
+ }
+ interface BlobProviderProps {
+ document: React.ReactElement<DocumentProps>;
+ children: (params: BlobProviderParams) => React.ReactNode;
+ }
+
+ /**
+ * Easy and declarative way of getting document's blob data without
+ * showing it on screen.
+ * @see https://react-pdf.org/advanced#on-the-fly-rendering
+ * @platform web
+ */
+ class BlobProvider extends React.Component<BlobProviderProps> { }
+
+ interface PDFViewerProps {
+ width?: number;
+ height?: number;
+ style?: Style | Style[];
+ className?: string;
+ children?: React.ReactElement<DocumentProps>;
+ }
+
+ /**
+ * Iframe PDF viewer for client-side generated documents.
+ * @platform web
+ */
+ class PDFViewer extends React.Component<PDFViewerProps> { }
+
+ interface PDFDownloadLinkProps {
+ document: React.ReactElement<DocumentProps>;
+ fileName?: string;
+ style?: Style | Style[];
+ className?: string;
+ children?:
+ | React.ReactNode
+ | ((params: BlobProviderParams) => React.ReactNode);
+ }
+
+ /**
+ * Anchor tag to enable generate and download PDF documents on the fly.
+ * @see https://react-pdf.org/advanced#on-the-fly-rendering
+ * @platform web
+ */
+ class PDFDownloadLink extends React.Component<PDFDownloadLinkProps> { }
+
+ interface EmojiSource {
+ url: string;
+ format: string;
+ }
+ interface RegisteredFont {
+ src: string;
+ loaded: boolean;
+ loading: boolean;
+ data: any;
+ [key: string]: any;
+ }
+ type HyphenationCallback = (
+ words: string[],
+ glyphString: { [key: string]: any },
+ ) => string[];
+
+ const Font: {
+ register: (
+ src: string,
+ options: { family: string;[key: string]: any },
+ ) => void;
+ getEmojiSource: () => EmojiSource;
+ getRegisteredFonts: () => string[];
+ registerEmojiSource: (emojiSource: EmojiSource) => void;
+ registerHyphenationCallback: (
+ hyphenationCallback: HyphenationCallback,
+ ) => void;
+ getHyphenationCallback: () => HyphenationCallback;
+ getFont: (fontFamily: string) => RegisteredFont | undefined;
+ load: (
+ fontFamily: string,
+ document: React.ReactElement<DocumentProps>,
+ ) => Promise<void>;
+ clear: () => void;
+ reset: () => void;
+ };
+
+ const StyleSheet: {
+ hairlineWidth: number;
+ create: <TStyles>(styles: TStyles) => TStyles;
+ resolve: (
+ style: Style,
+ container: {
+ width: number;
+ height: number;
+ orientation: Orientation;
+ },
+ ) => Style;
+ flatten: (...styles: Style[]) => Style;
+ absoluteFillObject: {
+ position: 'absolute';
+ left: 0;
+ right: 0;
+ top: 0;
+ bottom: 0;
+ };
+ };
+
+ const version: any;
+
+ const PDFRenderer: any;
+
+ const createInstance: (
+ element: {
+ type: string;
+ props: { [key: string]: any };
+ },
+ root?: any,
+ ) => any;
+
+ const pdf: (
+ document: React.ReactElement<DocumentProps>,
+ ) => {
+ isDirty: () => boolean;
+ updateContainer: (document: React.ReactElement<any>) => void;
+ toBuffer: () => NodeJS.ReadableStream;
+ toBlob: () => Blob;
+ toString: () => string;
+ };
+
+ const renderToStream: (
+ document: React.ReactElement<DocumentProps>,
+ ) => NodeJS.ReadableStream;
+
+ const renderToFile: (
+ document: React.ReactElement<DocumentProps>,
+ filePath: string,
+ callback?: (output: NodeJS.ReadableStream, filePath: string) => any,
+ ) => Promise<NodeJS.ReadableStream>;
+
+ const render: typeof renderToFile;
}
- /**
- * Anchor tag to enable generate and download PDF documents on the fly.
- * @see https://react-pdf.org/advanced#on-the-fly-rendering
- * @platform web
- */
- class PDFDownloadLink extends React.Component<PDFDownloadLinkProps> { }
-
- interface EmojiSource {
- url: string;
- format: string;
- }
- interface RegisteredFont {
- src: string;
- loaded: boolean;
- loading: boolean;
- data: any;
- [key: string]: any;
- }
- type HyphenationCallback = (
- words: string[],
- glyphString: { [key: string]: any },
- ) => string[];
-
- const Font: {
- register: (
- src: string,
- options: { family: string;[key: string]: any },
- ) => void;
- getEmojiSource: () => EmojiSource;
- getRegisteredFonts: () => string[];
- registerEmojiSource: (emojiSource: EmojiSource) => void;
- registerHyphenationCallback: (
- hyphenationCallback: HyphenationCallback,
- ) => void;
- getHyphenationCallback: () => HyphenationCallback;
- getFont: (fontFamily: string) => RegisteredFont | undefined;
- load: (
- fontFamily: string,
- document: React.ReactElement<DocumentProps>,
- ) => Promise<void>;
- clear: () => void;
- reset: () => void;
+ const Document: typeof ReactPDF.Document;
+ const Page: typeof ReactPDF.Page;
+ const View: typeof ReactPDF.View;
+ const Image: typeof ReactPDF.Image;
+ const Text: typeof ReactPDF.Text;
+ const Link: typeof ReactPDF.Link;
+ const Note: typeof ReactPDF.Note;
+ const Font: typeof ReactPDF.Font;
+ const StyleSheet: typeof ReactPDF.StyleSheet;
+ const createInstance: typeof ReactPDF.createInstance;
+ const PDFRenderer: typeof ReactPDF.PDFRenderer;
+ const version: typeof ReactPDF.version;
+ const pdf: typeof ReactPDF.pdf;
+
+ export default ReactPDF;
+ export {
+ Document,
+ Page,
+ View,
+ Image,
+ Text,
+ Link,
+ Note,
+ Font,
+ StyleSheet,
+ createInstance,
+ PDFRenderer,
+ version,
+ pdf,
};
-
- const StyleSheet: {
- hairlineWidth: number;
- create: <TStyles>(styles: TStyles) => TStyles;
- resolve: (
- style: Style,
- container: {
- width: number;
- height: number;
- orientation: Orientation;
- },
- ) => Style;
- flatten: (...styles: Style[]) => Style;
- absoluteFillObject: {
- position: 'absolute';
- left: 0;
- right: 0;
- top: 0;
- bottom: 0;
- };
- };
-
- const version: any;
-
- const PDFRenderer: any;
-
- const createInstance: (
- element: {
- type: string;
- props: { [key: string]: any };
- },
- root?: any,
- ) => any;
-
- const pdf: (
- document: React.ReactElement<DocumentProps>,
- ) => {
- isDirty: () => boolean;
- updateContainer: (document: React.ReactElement<any>) => void;
- toBuffer: () => NodeJS.ReadableStream;
- toBlob: () => Blob;
- toString: () => string;
- };
-
- const renderToStream: (
- document: React.ReactElement<DocumentProps>,
- ) => NodeJS.ReadableStream;
-
- const renderToFile: (
- document: React.ReactElement<DocumentProps>,
- filePath: string,
- callback?: (output: NodeJS.ReadableStream, filePath: string) => any,
- ) => Promise<NodeJS.ReadableStream>;
-
- const render: typeof renderToFile;
- }
-
- const Document: typeof ReactPDF.Document;
- const Page: typeof ReactPDF.Page;
- const View: typeof ReactPDF.View;
- const Image: typeof ReactPDF.Image;
- const Text: typeof ReactPDF.Text;
- const Link: typeof ReactPDF.Link;
- const Note: typeof ReactPDF.Note;
- const Font: typeof ReactPDF.Font;
- const StyleSheet: typeof ReactPDF.StyleSheet;
- const createInstance: typeof ReactPDF.createInstance;
- const PDFRenderer: typeof ReactPDF.PDFRenderer;
- const version: typeof ReactPDF.version;
- const pdf: typeof ReactPDF.pdf;
-
- export default ReactPDF;
- export {
- Document,
- Page,
- View,
- Image,
- Text,
- Link,
- Note,
- Font,
- StyleSheet,
- createInstance,
- PDFRenderer,
- version,
- pdf,
- };
} \ No newline at end of file